/*
* Copyright 2014 Sebastiano Poggi and Francesco Pontillo
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.frakbot.util.feedback;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.widget.Toast;
import net.frakbot.FWeather.R;
import net.frakbot.global.Const;
import net.frakbot.util.log.FLog;
import java.io.*;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
/**
* A Runnable that prepares and sends a feedback email.
*/
/*package*/ class EmailSender implements Runnable {
private static final String TAG = EmailSender.class.getSimpleName();
private Context mContext;
public EmailSender(Context context) {
mContext = context;
}
@Override
public void run() {
final File cacheDir = mContext.getExternalCacheDir();
Intent email = new Intent(Intent.ACTION_SEND);
email.putExtra(Intent.EXTRA_EMAIL, new String[] {"frakbot+fweather@gmail.com"});
email.putExtra(Intent.EXTRA_SUBJECT, "[FEEDBACK] " + mContext.getString(R.string.app_name));
email.setType("message/rfc822");
final File logFile = collectLogcat(cacheDir);
if (logFile != null && logFile.exists()) {
email.putExtra(Intent.EXTRA_STREAM, Uri.fromFile(logFile));
email.putExtra(Intent.EXTRA_TEXT, generateFeedbackBody(mContext));
}
else {
email.putExtra(Intent.EXTRA_TEXT, "\n\n** Couldn't attach logcat **\n\n" + generateFeedbackBody(
mContext));
}
try {
FLog.i(TAG, "Sending feedback email");
mContext.startActivity(
Intent.createChooser(email, mContext.getString(R.string.feedback_send_chooser_title)));
}
catch (Exception e) {
Toast.makeText(mContext, mContext.getString(R.string.toast_feedback_mail_error), Toast.LENGTH_LONG)
.show();
FLog.e(TAG, "Unable to send the feedback email", e);
}
FLog.d(TAG, "Email sender thread finished");
}
/**
* Builds a feedback email body with some basic system info.
*
* @param c The context used to retrieve the system information.
*
* @return Returns the generated system info.
*/
@SuppressWarnings("StringBufferReplaceableByString")
private static String generateFeedbackBody(Context c) {
StringBuilder sb = new StringBuilder("\n\n\n" +
"(only write above this line)\n\n" +
"-----------\n" +
"System info\n" +
"-----------\n\n");
// HW information
sb.append("Device model: ").append(Build.MODEL).append("\n");
sb.append("Manifacturer: ").append(Build.MANUFACTURER).append("\n");
sb.append("Brand: ").append(Build.BRAND).append("\n");
sb.append("CPU ABI: ").append(Build.CPU_ABI).append("\n");
sb.append("Product: ").append(Build.PRODUCT).append("\n").append("\n");
// SW information
sb.append("Android version: ").append(Build.VERSION.CODENAME).append("\n");
sb.append("Release: ").append(Build.VERSION.RELEASE).append("\n");
sb.append("Incremental: ").append(Build.VERSION.INCREMENTAL).append("\n");
sb.append("Build: ").append(Build.FINGERPRINT).append("\n");
sb.append("Kernel: ").append(getKernelVersion()).append("\n");
// App info
sb.append("App version: ").append(getAppVersionNumber(c));
return sb.toString();
}
/**
* Retrieves the app version number.
*
* @param c The Context to retrieve the number for
*
* @return Returns the app version, or "N/A" if it can't be read
*/
private static String getAppVersionNumber(Context c) {
String version = "N/A";
try {
final PackageManager packageManager = c.getPackageManager();
if (packageManager == null) {
return version;
}
PackageInfo packageInfo = packageManager.getPackageInfo(c.getPackageName(), 0);
version = packageInfo.versionName;
}
catch (Exception e) {
FLog.e(TAG, "Unable to read the app version!", e);
}
return version;
}
/**
* Reads the kernel version to a string.
*
* @return Returns the kernel version, or "N/A" if it can't be read
*/
private static String getKernelVersion() {
String procVersionStr;
try {
BufferedReader reader = new BufferedReader(new FileReader("/proc/version"), 256);
try {
procVersionStr = reader.readLine();
}
finally {
reader.close();
}
if (procVersionStr == null) {
FLog.e(TAG, "Unable to read the kernel version!");
return "N/A";
}
return procVersionStr;
}
catch (IOException e) {
FLog.e(TAG, "Getting kernel version failed", e);
return "N/A";
}
}
/**
* Collects a dump of the system log (logcat) to a file.
*
* @param externalCacheDir The cache directory.
*
* @return Returns the File where the logcat has been dumped to,
* or null if there was any problem.
*/
@SuppressWarnings("ResultOfMethodCallIgnored")
private static File collectLogcat(File externalCacheDir) {
FLog.i(TAG, "Collecting logcat");
File shareFile = null;
// Do the housekeeping
cleanupLogcatCache(externalCacheDir);
if (externalCacheDir == null) {
FLog.w(TAG, "External storage not available. Can't attach logcat!");
return null;
}
try {
// Create the temp logcat file
final DateFormat dateFormat = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US);
shareFile = new File(externalCacheDir, Const.APP_NAME + "-" + dateFormat.format(new Date()) + ".log");
shareFile.createNewFile();
String commandLine = "logcat -d " + // DUMP the whole log
"-v threadtime " + // in the threadtime format
"*:V"; // for all tags, starting at VERBOSE level
Process process = Runtime.getRuntime().exec(commandLine);
process.waitFor();
BufferedReader bufferedReader =
new BufferedReader(new InputStreamReader(process.getInputStream()));
FileWriter fw = new FileWriter(shareFile, false);
String line;
int linesCount = 0;
while ((line = bufferedReader.readLine()) != null) {
fw.write(line);
fw.write("\n");
linesCount++;
}
fw.flush();
fw.close();
FLog.d(TAG, "Logcat collected to " + shareFile.getAbsolutePath() + " (" + linesCount + " lines)");
}
catch (IOException e) {
FLog.e(TAG, "Log collection failed", e);
}
catch (InterruptedException e) {
FLog.w(TAG, "Log collection waiting was interrupted - log might be truncated!");
}
return shareFile;
}
/**
* Cleans up the cache dir from any leftover cache files.
* Do not invoke this method right after starting the log collection
* or it might delete the log you're collecting as well!
*
* @param cacheDir The cache directory.
*/
private static void cleanupLogcatCache(File cacheDir) {
FLog.d(TAG, "Cleaning up log files from cache dir");
if (cacheDir == null) {
FLog.w(TAG, "External storage not available. Can't cleanup cache");
return;
}
final File[] logFiles = cacheDir.listFiles(new FilenameFilter() {
@Override
public boolean accept(File dir, String filename) {
return filename.toLowerCase().endsWith(".log");
}
});
if (logFiles == null) {
FLog.d(TAG, "No log files to prune from cache");
return;
}
int count = 0;
for (File logFile : logFiles) {
count += logFile.delete() ? 1 : 0;
}
FLog.d(TAG, "Pruned " + count + " log file(s) from cache");
}
}