package net.hockeyapp.android; import android.text.TextUtils; import net.hockeyapp.android.objects.CrashDetails; import net.hockeyapp.android.utils.HockeyLog; import java.io.BufferedWriter; import java.io.FileWriter; import java.io.IOException; import java.io.PrintWriter; import java.io.StringWriter; import java.io.Writer; import java.lang.Thread.UncaughtExceptionHandler; import java.util.Date; import java.util.UUID; /** * <h3>Description</h3> * Helper class to catch exceptions. Saves the stack trace * as a file and executes callback methods to ask the app for * additional information and meta data (see CrashManagerListener). * **/ public class ExceptionHandler implements UncaughtExceptionHandler { private boolean mIgnoreDefaultHandler = false; private CrashManagerListener mCrashManagerListener; private UncaughtExceptionHandler mDefaultExceptionHandler; public ExceptionHandler(UncaughtExceptionHandler defaultExceptionHandler, CrashManagerListener listener, boolean ignoreDefaultHandler) { mDefaultExceptionHandler = defaultExceptionHandler; mIgnoreDefaultHandler = ignoreDefaultHandler; mCrashManagerListener = listener; } public void setListener(CrashManagerListener listener) { mCrashManagerListener = listener; } /** * Save a caught exception to disk. * * @param exception Exception to save. * @param listener Custom CrashManager listener instance. * @deprecated in 3.7.0-beta.2. Use saveException(Throwable exception, Thread thread, * CrashManagerListener listener) instead. */ @Deprecated @SuppressWarnings("unused") public static void saveException(Throwable exception, CrashManagerListener listener) { saveException(exception, null, listener); } /** * Save a caught exception to disk. * * @param exception Exception to save. * @param thread Thread that crashed. * @param listener Custom CrashManager listener instance. */ public static void saveException(Throwable exception, Thread thread, CrashManagerListener listener) { final Date now = new Date(); final Date startDate = new Date(CrashManager.getInitializeTimestamp()); final Writer result = new StringWriter(); final PrintWriter printWriter = new PrintWriter(result); BufferedWriter writer = null; exception.printStackTrace(printWriter); String filename = UUID.randomUUID().toString(); CrashDetails crashDetails = new CrashDetails(filename, exception); crashDetails.setAppPackage(Constants.APP_PACKAGE); crashDetails.setAppVersionCode(Constants.APP_VERSION); crashDetails.setAppVersionName(Constants.APP_VERSION_NAME); crashDetails.setAppStartDate(startDate); crashDetails.setAppCrashDate(now); if ((listener == null) || (listener.includeDeviceData())) { crashDetails.setOsVersion(Constants.ANDROID_VERSION); crashDetails.setOsBuild(Constants.ANDROID_BUILD); crashDetails.setDeviceManufacturer(Constants.PHONE_MANUFACTURER); crashDetails.setDeviceModel(Constants.PHONE_MODEL); } if (thread != null && ((listener == null) || (listener.includeThreadDetails()))) { crashDetails.setThreadName(thread.getName() + "-" + thread.getId()); } if (Constants.CRASH_IDENTIFIER != null && (listener == null || listener.includeDeviceIdentifier())) { crashDetails.setReporterKey(Constants.CRASH_IDENTIFIER); } crashDetails.writeCrashReport(); if (listener != null) { try { writeValueToFile(limitedString(listener.getUserID()), filename + ".user"); writeValueToFile(limitedString(listener.getContact()), filename + ".contact"); writeValueToFile(listener.getDescription(), filename + ".description"); } catch (IOException e) { HockeyLog.error("Error saving crash meta data!", e); } } } /** * Save java exception(s) caught by HockeySDK-Xamarin to disk. * * @param exception The native java exception to save. * @param managedExceptionString String representation of the full exception including the managed exception. * @param thread Thread that crashed. * @param listener Custom CrashManager listener instance. */ @SuppressWarnings("unused") public static void saveNativeException(Throwable exception, String managedExceptionString, Thread thread, CrashManagerListener listener) { // the throwable will a "native" Java exception. In this case managedExceptionString contains the full, "unconverted" exception // which contains information about the managed exception, too. We don't want to loose that part. Sadly, passing a managed // exception as an additional throwable strips that info, so we pass in the full managed exception as a string // and extract the first part that contains the info about the managed code that was calling the java code. // In case there is no managedExceptionString, we just forward the java exception if (!TextUtils.isEmpty(managedExceptionString)) { String[] splits = managedExceptionString.split("--- End of managed exception stack trace ---", 2); if (splits != null && splits.length > 0) { managedExceptionString = splits[0]; } } saveXamarinException(exception, thread, managedExceptionString, false, listener); } /** * Save managed exception(s) caught by HockeySDK-Xamarin to disk. * * @param exception The managed exception to save. * @param thread Thread that crashed. * @param listener Custom CrashManager listener instance. */ @SuppressWarnings("unused") public static void saveManagedException(Throwable exception, Thread thread, CrashManagerListener listener) { saveXamarinException(exception, thread, null, true, listener); } private static void saveXamarinException(Throwable exception, Thread thread, String additionalManagedException, Boolean isManagedException, CrashManagerListener listener) { final Date startDate = new Date(CrashManager.getInitializeTimestamp()); String filename = UUID.randomUUID().toString(); final Date now = new Date(); final Writer result = new StringWriter(); final PrintWriter printWriter = new PrintWriter(result); if (exception != null) { exception.printStackTrace(printWriter); } CrashDetails crashDetails = new CrashDetails(filename, exception, additionalManagedException, isManagedException); crashDetails.setAppPackage(Constants.APP_PACKAGE); crashDetails.setAppVersionCode(Constants.APP_VERSION); crashDetails.setAppVersionName(Constants.APP_VERSION_NAME); crashDetails.setAppStartDate(startDate); crashDetails.setAppCrashDate(now); if ((listener == null) || (listener.includeDeviceData())) { crashDetails.setOsVersion(Constants.ANDROID_VERSION); crashDetails.setOsBuild(Constants.ANDROID_BUILD); crashDetails.setDeviceManufacturer(Constants.PHONE_MANUFACTURER); crashDetails.setDeviceModel(Constants.PHONE_MODEL); } if (thread != null && ((listener == null) || (listener.includeThreadDetails()))) { crashDetails.setThreadName(thread.getName() + "-" + thread.getId()); } if (Constants.CRASH_IDENTIFIER != null && (listener == null || listener.includeDeviceIdentifier())) { crashDetails.setReporterKey(Constants.CRASH_IDENTIFIER); } crashDetails.writeCrashReport(); if (listener != null) { try { writeValueToFile(limitedString(listener.getUserID()), filename + ".user"); writeValueToFile(limitedString(listener.getContact()), filename + ".contact"); writeValueToFile(listener.getDescription(), filename + ".description"); } catch (IOException e) { HockeyLog.error("Error saving crash meta data!", e); } } } public void uncaughtException(Thread thread, Throwable exception) { if (Constants.FILES_PATH == null) { // If the files path is null, the exception can't be stored // Always call the default handler instead mDefaultExceptionHandler.uncaughtException(thread, exception); } else { saveException(exception, thread, mCrashManagerListener); if (!mIgnoreDefaultHandler) { mDefaultExceptionHandler.uncaughtException(thread, exception); } else { android.os.Process.killProcess(android.os.Process.myPid()); System.exit(10); } } } private static void writeValueToFile(String value, String filename) throws IOException { if (TextUtils.isEmpty(value)) { return; } BufferedWriter writer = null; try { String path = Constants.FILES_PATH + "/" + filename; if (!TextUtils.isEmpty(value) && TextUtils.getTrimmedLength(value) > 0) { writer = new BufferedWriter(new FileWriter(path)); writer.write(value); writer.flush(); } } catch (IOException e) { // TODO: Handle exception here } finally { if (writer != null) { writer.close(); } } } private static String limitedString(String string) { if (!TextUtils.isEmpty(string) && string.length() > 255) { string = string.substring(0, 255); } return string; } }