/* * Copyright 2011-2014 the original author or authors. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package devcoin.wallet.util; import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.io.PrintWriter; import java.util.Collections; import java.util.Comparator; import java.util.Formatter; import java.util.List; import java.util.Set; import javax.annotation.Nonnull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import android.app.ActivityManager; import android.content.Context; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.content.res.Configuration; import android.content.res.Resources; import android.os.Build; import com.google.devcoin.core.Transaction; import com.google.devcoin.core.TransactionOutput; import com.google.devcoin.core.Wallet; import devcoin.wallet.Constants; import devcoin.wallet.WalletApplication; /** * @author Andreas Schildbach */ public class CrashReporter { private static final String BACKGROUND_TRACES_FILENAME = "background.trace"; private static final String CRASH_TRACE_FILENAME = "crash.trace"; private static final long TIME_CREATE_APPLICATION = System.currentTimeMillis(); private static File backgroundTracesFile; private static File crashTraceFile; private static final Logger log = LoggerFactory.getLogger(CrashReporter.class); public static void init(@Nonnull final File cacheDir) { backgroundTracesFile = new File(cacheDir, BACKGROUND_TRACES_FILENAME); crashTraceFile = new File(cacheDir, CRASH_TRACE_FILENAME); Thread.setDefaultUncaughtExceptionHandler(new ExceptionHandler(Thread.getDefaultUncaughtExceptionHandler())); } public static boolean hasSavedBackgroundTraces() { return backgroundTracesFile.exists(); } public static void appendSavedBackgroundTraces(@Nonnull final Appendable report) throws IOException { BufferedReader reader = null; try { reader = new BufferedReader(new InputStreamReader(new FileInputStream(backgroundTracesFile), Constants.UTF_8)); copy(reader, report); } finally { if (reader != null) reader.close(); backgroundTracesFile.delete(); } } public static boolean hasSavedCrashTrace() { return crashTraceFile.exists(); } public static void appendSavedCrashTrace(@Nonnull final Appendable report) throws IOException { BufferedReader reader = null; try { reader = new BufferedReader(new InputStreamReader(new FileInputStream(crashTraceFile), Constants.UTF_8)); copy(reader, report); } finally { if (reader != null) reader.close(); crashTraceFile.delete(); } } private static void copy(@Nonnull final BufferedReader in, @Nonnull final Appendable out) throws IOException { while (true) { final String line = in.readLine(); if (line == null) break; out.append(line).append('\n'); } } public static void appendDeviceInfo(@Nonnull final Appendable report, final Context context) throws IOException { final Resources res = context.getResources(); final Configuration config = res.getConfiguration(); final ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); report.append("Device Model: " + android.os.Build.MODEL + "\n"); report.append("Android Version: " + android.os.Build.VERSION.RELEASE + "\n"); report.append("Board: " + android.os.Build.BOARD + "\n"); report.append("Brand: " + android.os.Build.BRAND + "\n"); report.append("Device: " + android.os.Build.DEVICE + "\n"); report.append("Display: " + android.os.Build.DISPLAY + "\n"); report.append("Finger Print: " + android.os.Build.FINGERPRINT + "\n"); report.append("Host: " + android.os.Build.HOST + "\n"); report.append("ID: " + android.os.Build.ID + "\n"); // report.append("Manufacturer: " + manufacturer + "\n"); report.append("Product: " + android.os.Build.PRODUCT + "\n"); report.append("Tags: " + android.os.Build.TAGS + "\n"); report.append("Time: " + android.os.Build.TIME + "\n"); report.append("Type: " + android.os.Build.TYPE + "\n"); report.append("User: " + android.os.Build.USER + "\n"); report.append("Configuration: " + config + "\n"); report.append("Screen Layout: size " + (config.screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK) + " long " + (config.screenLayout & Configuration.SCREENLAYOUT_LONG_MASK) + "\n"); report.append("Display Metrics: " + res.getDisplayMetrics() + "\n"); report.append("Memory Class: " + activityManager.getMemoryClass() + (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB ? "/" + largeMemoryClass(activityManager) : "") + "\n"); } private static int largeMemoryClass(@Nonnull final ActivityManager activityManager) { try { return (Integer) ActivityManager.class.getMethod("getLargeMemoryClass").invoke(activityManager); } catch (final Exception x) { throw new RuntimeException(x); } } public static void appendInstalledPackages(@Nonnull final Appendable report, final Context context) throws IOException { final PackageManager pm = context.getPackageManager(); final List<PackageInfo> installedPackages = pm.getInstalledPackages(0); // sort by package name Collections.sort(installedPackages, new Comparator<PackageInfo>() { @Override public int compare(final PackageInfo lhs, final PackageInfo rhs) { return lhs.packageName.compareTo(rhs.packageName); } }); for (final PackageInfo p : installedPackages) report.append(String.format("%s %s (%d) - %tF %tF\n", p.packageName, p.versionName, p.versionCode, p.firstInstallTime, p.lastUpdateTime)); } public static void appendApplicationInfo(@Nonnull final Appendable report, @Nonnull final WalletApplication application) throws IOException { try { final PackageManager pm = application.getPackageManager(); final PackageInfo pi = pm.getPackageInfo(application.getPackageName(), 0); final long now = System.currentTimeMillis(); report.append("Version: " + pi.versionName + " (" + pi.versionCode + ")\n"); report.append("Package: " + pi.packageName + "\n"); report.append("Test/Prod: " + (Constants.TEST ? "test" : "prod") + "\n"); report.append("Time: " + String.format("%tF %tT %tz", now, now, now) + "\n"); report.append("Time of launch: " + String.format("%tF %tT %tz", TIME_CREATE_APPLICATION, TIME_CREATE_APPLICATION, TIME_CREATE_APPLICATION) + "\n"); report.append("Time of last update: " + String.format("%tF %tT %tz", pi.lastUpdateTime, pi.lastUpdateTime, pi.lastUpdateTime) + "\n"); report.append("Time of first install: " + String.format("%tF %tT %tz", pi.firstInstallTime, pi.firstInstallTime, pi.firstInstallTime) + "\n"); report.append("Network: " + Constants.NETWORK_PARAMETERS.getId() + "\n"); final Wallet wallet = application.getWallet(); report.append("Keychain size: " + wallet.getKeychainSize() + "\n"); final Set<Transaction> transactions = wallet.getTransactions(true); int numInputs = 0; int numOutputs = 0; int numSpentOutputs = 0; for (final Transaction tx : transactions) { numInputs += tx.getInputs().size(); final List<TransactionOutput> outputs = tx.getOutputs(); numOutputs += outputs.size(); for (final TransactionOutput txout : outputs) { if (!txout.isAvailableForSpending()) numSpentOutputs++; } } report.append("Transactions: " + transactions.size() + "\n"); report.append("Inputs: " + numInputs + "\n"); report.append("Outputs: " + numOutputs + " (spent: " + numSpentOutputs + ")\n"); report.append("Last block seen: " + wallet.getLastBlockSeenHeight() + " (" + wallet.getLastBlockSeenHash() + ")\n"); report.append("Databases:"); for (final String db : application.databaseList()) report.append(" " + db); report.append("\n"); final File filesDir = application.getFilesDir(); report.append("\nContents of FilesDir " + filesDir + ":\n"); appendDir(report, filesDir, 0); final File logDir = application.getDir("log", Context.MODE_PRIVATE); report.append("\nContents of LogDir " + logDir + ":\n"); appendDir(report, logDir, 0); } catch (final NameNotFoundException x) { throw new IOException(x); } } private static void appendDir(@Nonnull final Appendable report, @Nonnull final File file, final int indent) throws IOException { for (int i = 0; i < indent; i++) report.append(" - "); final Formatter formatter = new Formatter(report); formatter.format("%tF %tT %8d %s\n", file.lastModified(), file.lastModified(), file.length(), file.getName()); formatter.close(); if (file.isDirectory()) for (final File f : file.listFiles()) appendDir(report, f, indent + 1); } public static void saveBackgroundTrace(@Nonnull final Throwable throwable, @Nonnull final PackageInfo packageInfo) { synchronized (backgroundTracesFile) { PrintWriter writer = null; try { writer = new PrintWriter(new OutputStreamWriter(new FileOutputStream(backgroundTracesFile, true), Constants.UTF_8)); final long now = System.currentTimeMillis(); writer.println(String.format("\n--- collected at %tF %tT %tz on version %s (%d)", now, now, now, packageInfo.versionName, packageInfo.versionCode)); appendTrace(writer, throwable); } catch (final IOException x) { log.error("problem writing background trace", x); } finally { if (writer != null) writer.close(); } } } private static void appendTrace(@Nonnull final PrintWriter writer, @Nonnull final Throwable throwable) { throwable.printStackTrace(writer); // If the exception was thrown in a background thread inside // AsyncTask, then the actual exception can be found with getCause Throwable cause = throwable.getCause(); while (cause != null) { writer.println("\nCause:\n"); cause.printStackTrace(writer); cause = cause.getCause(); } } private static class ExceptionHandler implements Thread.UncaughtExceptionHandler { private final Thread.UncaughtExceptionHandler previousHandler; public ExceptionHandler(@Nonnull final Thread.UncaughtExceptionHandler previousHandler) { this.previousHandler = previousHandler; } @Override public synchronized void uncaughtException(final Thread t, final Throwable exception) { try { saveCrashTrace(exception); } catch (final IOException x) { log.info("problem writing crash trace", x); } previousHandler.uncaughtException(t, exception); } private void saveCrashTrace(@Nonnull final Throwable throwable) throws IOException { final PrintWriter writer = new PrintWriter(new OutputStreamWriter(new FileOutputStream(crashTraceFile), Constants.UTF_8)); appendTrace(writer, throwable); writer.close(); } } }