package de.tum.in.tumcampusapp.trace; import android.content.Context; import android.content.pm.PackageInfo; import android.os.Build; import android.preference.PreferenceManager; import android.util.Log; import com.google.common.base.Charsets; import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FilenameFilter; import java.io.IOException; import java.io.InputStreamReader; import java.io.Reader; import java.lang.Thread.UncaughtExceptionHandler; import java.util.ArrayList; import java.util.Collection; import java.util.List; import de.tum.in.tumcampusapp.api.TUMCabeClient; import de.tum.in.tumcampusapp.auxiliary.AuthenticationManager; import de.tum.in.tumcampusapp.auxiliary.Const; import de.tum.in.tumcampusapp.auxiliary.FileUtils; import de.tum.in.tumcampusapp.auxiliary.Utils; import de.tum.in.tumcampusapp.models.tumcabe.BugReport; public final class ExceptionHandler { public static final String STACKTRACE_ENDING = ".stacktrace"; public static final String LINE_SEPARATOR = "line.separator"; // Stores loaded stack traces in memory. Each element is contains a full stacktrace private static List<String[]> sStackTraces; private static ActivityAsyncTask<Processor, Void, Void, Void> sTask; private static final int S_MIN_DELAY = 0; private static boolean sSetupCalled; private ExceptionHandler() { // ExceptionHandler is a utility class } /** * Setup the handler for unhandled exceptions, and submit stack * traces from a previous crash. * * @param context context * @param processor processor */ public static boolean setup(Context context, final Processor processor) { // Make sure this is only called once. if (sSetupCalled) { // Tell the task that it now has a new context. if (sTask != null && !sTask.postProcessingDone()) { // We don't want to force the user to call our notifyContextGone() if he doesn't care about that functionality anyway, so in order to avoid the // InvalidStateException, ensure first that we are disconnected. sTask.connectTo(null); sTask.connectTo(processor); } return false; } sSetupCalled = true; Log.i(G.TAG, "Registering default exceptions handler"); // Files dir for storing the stack traces G.filesPath = context.getFilesDir().getAbsolutePath(); // Device model G.phoneModel = Build.MODEL; // Android version G.androidVersion = Build.VERSION.RELEASE; //Get the device ID G.deviceId = AuthenticationManager.getDeviceID(context); // Get information about the Package PackageInfo pi = Util.getPackageInfo(context); if (pi != null) { G.appVersion = pi.versionName; // Version G.appPackage = pi.packageName; // Package name G.appVersionCode = pi.versionCode; //Version code e.g.: 45 } // First, search for and load stack traces getStackTraces(); // Second, install the exception handler installHandler(); processor.handlerInstalled(); // Third, submit any traces we may have found return submit(processor, context); } /** * Setup the handler for unhandled exceptions, and submit stack * traces from a previous crash. * <p> * Simplified version that uses a default processor. * * @param context context */ public static boolean setup(Context context) { return setup(context, new Processor() { @Override public boolean beginSubmit() { return true; } @Override public void submitDone() { // NOP } @Override public void handlerInstalled() { // NOP } }); } /** * Submit stack traces. * This is public because in some cases you might want to manually ask the traces to be submitted, for example after asking the user's permission. */ public static boolean submit(final Processor processor, final Context context) { if (!sSetupCalled) { throw new IllegalStateException("you need to call setup() first"); } // If traces exist, we need to submit them if (ExceptionHandler.hasStrackTraces() && processor.beginSubmit()) { // Move the list of traces to a private variable. This ensures that subsequent calls to hasStackTraces() // while the submission thread is ongoing, will return false, or at least would refer to some new set of traces. // // Yes, it would not be a problem from our side to have two of these submission threads ongoing at the same time (although it wouldn't currently happen as no new // traces can be added to the list besides through crashing the process); however, the user's callback processor might not be written to deal with that scenario. final List<String[]> tracesNowSubmitting = sStackTraces; sStackTraces = null; sTask = new ActivityAsyncTask<Processor, Void, Void, Void>(processor) { private long mTimeStarted; @Override protected void onPreExecute() { super.onPreExecute(); mTimeStarted = System.currentTimeMillis(); } @Override protected Void doInBackground(Void... params) { ExceptionHandler.submitStackTraces(tracesNowSubmitting, context); long rest = S_MIN_DELAY - System.currentTimeMillis() + mTimeStarted; if (rest > 0) { try { Thread.sleep(rest); } catch (InterruptedException e) { Utils.log(e); } } return null; } @Override protected void processPostExecute(Void result) { mWrapped.submitDone(); } }; sTask.execute(); } return ExceptionHandler.hasStrackTraces(); } /** * Return true if there are stacktraces that need to be submitted. * <p> * Useful for example if you would like to ask the user's permission * before submitting. You can then use Processor.beginSubmit() to * stop the submission from occurring. */ public static boolean hasStrackTraces() { return !getStackTraces().isEmpty(); } /** * Search for stack trace files, read them into memory and delete * them from disk. * <p> * They are read into memory immediately so we can go ahead and * install the exception handler right away, and only then try * and submit the traces. */ private static Collection<String[]> getStackTraces() { if (sStackTraces != null) { return sStackTraces; } Utils.logv("Looking for exceptions in: " + G.filesPath); // Find list of .stacktrace files File dir = new File(G.filesPath + '/'); // Try to create the files folder if it doesn't exist if (!dir.exists()) { dir.mkdir(); } //Look into the files folder to see if there are any "*.stacktrace" files. String[] list = dir.list(new FilenameFilter() { @Override public boolean accept(File dir, String name) { return name.endsWith(STACKTRACE_ENDING); } }); Utils.logv("Found " + list.length + " stacktrace(s)"); //Try to read all of them try { sStackTraces = new ArrayList<>(); for (String aList : list) { // Limit to a certain number of SUCCESSFULLY read traces if (sStackTraces.size() >= G.MAX_TRACES) { break; } //Full File path String filePath = G.filesPath + '/' + aList; try { // Read contents of stacktrace StringBuilder stacktrace = new StringBuilder(); BufferedReader input = new BufferedReader(getFileReader(filePath)); try { String line; while ((line = input.readLine()) != null) { stacktrace.append(line); stacktrace.append(System.getProperty(LINE_SEPARATOR)); } } finally { input.close(); } //Create the array containing the trace and the log file String[] a = {stacktrace.toString(), FileUtils.getStringFromFile(filePath + ".log")}; sStackTraces.add(a); } catch (IOException e) { Log.e(G.TAG, "Failed to load stack trace", e); } } return sStackTraces; } finally { // Delete ALL the stack traces, even those not read (if there were too many), and do this within a finally clause so that even if something very unexpected went // wrong above, it hopefully won't happen again the next time around (because the offending files are gone). for (String aList : list) { try { File file = new File(G.filesPath + '/' + aList); file.delete(); } catch (Exception e) { Log.e(G.TAG, "Error deleting trace file: " + aList, e); } } } } /** * If any are present, submit them to the trace server. */ private static void submitStackTraces(List<String[]> list, Context context) { //Check if we user gave permission to send these reports G.preferences = PreferenceManager.getDefaultSharedPreferences(context); if (!G.preferences.getBoolean(Const.BUG_REPORTS, G.BUG_REPORT_DEFAULT)) { return; } //If nothing passed we have nothing to submit if (list == null) { return; } //Otherwise do some hard work and submit all of them after eachother try { for (int i = 0; i < list.size(); i++) { String stacktrace = list.get(i)[0]; // Transmit stack trace with PUT request TUMCabeClient client = TUMCabeClient.getInstance(context); BugReport r = new BugReport(context, stacktrace, list.get(i)[1]); client.putBugReport(r); // We don't care about the response, so we just hope it went well and on with it. } } catch (Exception e) { Log.e(G.TAG, "Error submitting trace", e); } } private static void installHandler() { UncaughtExceptionHandler currentHandler = Thread.getDefaultUncaughtExceptionHandler(); // don't register again if already registered if (!(currentHandler instanceof DefaultExceptionHandler)) { // Register our default exceptions handler Thread.setDefaultUncaughtExceptionHandler(new DefaultExceptionHandler(currentHandler)); } } public interface Processor { boolean beginSubmit(); void submitDone(); void handlerInstalled(); } private static Reader getFileReader(String path) throws FileNotFoundException { return new InputStreamReader(new FileInputStream(path), Charsets.UTF_8.newDecoder()); } }