package net.hockeyapp.android.objects; import android.text.TextUtils; import net.hockeyapp.android.Constants; import net.hockeyapp.android.utils.HockeyLog; import java.io.*; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Locale; public class CrashDetails { public static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("EEE MMM dd HH:mm:ss zzz yyyy", Locale.US); private static final String FIELD_CRASH_REPORTER_KEY = "CrashReporter Key"; private static final String FIELD_APP_START_DATE = "Start Date"; private static final String FIELD_APP_CRASH_DATE = "Date"; private static final String FIELD_OS_VERSION = "Android"; private static final String FIELD_OS_BUILD = "Android Build"; private static final String FIELD_DEVICE_MANUFACTURER = "Manufacturer"; private static final String FIELD_DEVICE_MODEL = "Model"; private static final String FIELD_APP_PACKAGE = "Package"; private static final String FIELD_APP_VERSION_NAME = "Version Name"; private static final String FIELD_APP_VERSION_CODE = "Version Code"; private static final String FIELD_THREAD_NAME = "Thread"; private static final String FIELD_FORMAT = "Format"; private static final String FIELD_FORMAT_VALUE = "Xamarin"; private static final String FIELD_XAMARIN_CAUSED_BY = "Xamarin caused by: "; //Field that marks a Xamarin Exception private final String crashIdentifier; private String reporterKey; private Date appStartDate; private Date appCrashDate; private String osVersion; private String osBuild; private String deviceManufacturer; private String deviceModel; private String appPackage; private String appVersionName; private String appVersionCode; private String threadName; private String throwableStackTrace; private Boolean isXamarinException; private String format; public CrashDetails(String crashIdentifier) { this.crashIdentifier = crashIdentifier; isXamarinException = false; throwableStackTrace = ""; } public CrashDetails(String crashIdentifier, Throwable throwable) { this(crashIdentifier); isXamarinException = false; final Writer stackTraceResult = new StringWriter(); final PrintWriter printWriter = new PrintWriter(stackTraceResult); throwable.printStackTrace(printWriter); throwableStackTrace = stackTraceResult.toString(); } public CrashDetails(String crashIdentifier, Throwable throwable, String managedExceptionString, Boolean isManagedException) { this(crashIdentifier); final Writer stackTraceResult = new StringWriter(); final PrintWriter printWriter = new PrintWriter(stackTraceResult); isXamarinException = true; //Add the header field "Format" to the crash //the value is "Xamarin", for now there are no other values and it's only set in case we have an exception coming from //the Xamarin SDK. It can be a java exception, a managed exception, or a mixed one. setFormat(FIELD_FORMAT_VALUE); if (isManagedException) { //add "Xamarin Caused By" before the managed stacktrace. No new line after it. printWriter.print(FIELD_XAMARIN_CAUSED_BY); //print the managed exception throwable.printStackTrace(printWriter); } else { //If we have managedExceptionString, we hava a MIXED (Java & C#) //exception, The throwable will be the Java exception. if (!TextUtils.isEmpty(managedExceptionString)) { //Print the java exception throwable.printStackTrace(printWriter); //Add "Xamarin Caused By" before the managed stacktrace. No new line after it. printWriter.print(FIELD_XAMARIN_CAUSED_BY); //print the stacktrace of the managed exception printWriter.print(managedExceptionString); } else { //we have a java exception, no "Xamarin Caused By:" throwable.printStackTrace(printWriter); } } throwableStackTrace = stackTraceResult.toString(); } public static CrashDetails fromFile(File file) throws IOException { String crashIdentifier = file.getName().substring(0, file.getName().indexOf(".stacktrace")); return fromReader(crashIdentifier, new FileReader(file)); } public static CrashDetails fromReader(String crashIdentifier, Reader in) throws IOException { BufferedReader bufferedReader = new BufferedReader(in); CrashDetails result = new CrashDetails(crashIdentifier); String readLine, headerName, headerValue; boolean headersProcessed = false; StringBuilder stackTraceBuilder = new StringBuilder(); while ((readLine = bufferedReader.readLine()) != null) { if (!headersProcessed) { if (readLine.isEmpty()) { // empty line denotes break between headers and stack trace headersProcessed = true; continue; } int colonIndex = readLine.indexOf(":"); if (colonIndex < 0) { HockeyLog.error("Malformed header line when parsing crash details: \"" + readLine + "\""); } headerName = readLine.substring(0, colonIndex).trim(); headerValue = readLine.substring(colonIndex + 1, readLine.length()).trim(); if (headerName.equals(FIELD_CRASH_REPORTER_KEY)) { result.setReporterKey(headerValue); } else if (headerName.equals(FIELD_APP_START_DATE)) { try { result.setAppStartDate(DATE_FORMAT.parse(headerValue)); } catch (ParseException e) { throw new RuntimeException(e); } } else if (headerName.equals(FIELD_APP_CRASH_DATE)) { try { result.setAppCrashDate(DATE_FORMAT.parse(headerValue)); } catch (ParseException e) { throw new RuntimeException(e); } } else if (headerName.equals(FIELD_OS_VERSION)) { result.setOsVersion(headerValue); } else if (headerName.equals(FIELD_OS_BUILD)) { result.setOsBuild(headerValue); } else if (headerName.equals(FIELD_DEVICE_MANUFACTURER)) { result.setDeviceManufacturer(headerValue); } else if (headerName.equals(FIELD_DEVICE_MODEL)) { result.setDeviceModel(headerValue); } else if (headerName.equals(FIELD_APP_PACKAGE)) { result.setAppPackage(headerValue); } else if (headerName.equals(FIELD_APP_VERSION_NAME)) { result.setAppVersionName(headerValue); } else if (headerName.equals(FIELD_APP_VERSION_CODE)) { result.setAppVersionCode(headerValue); } else if (headerName.equals(FIELD_THREAD_NAME)) { result.setThreadName(headerValue); } else if (headerName.equals(FIELD_FORMAT)) { result.setFormat(headerValue); } } else { stackTraceBuilder.append(readLine).append("\n"); } } result.setThrowableStackTrace(stackTraceBuilder.toString()); return result; } public void writeCrashReport() { String path = Constants.FILES_PATH + "/" + crashIdentifier + ".stacktrace"; writeCrashReport(path); } public void writeCrashReport(final String path) { HockeyLog.debug("Writing unhandled exception to: " + path); BufferedWriter writer = null; try { writer = new BufferedWriter(new FileWriter(path)); writeHeader(writer, FIELD_APP_PACKAGE, appPackage); writeHeader(writer, FIELD_APP_VERSION_CODE, appVersionCode); writeHeader(writer, FIELD_APP_VERSION_NAME, appVersionName); writeHeader(writer, FIELD_OS_VERSION, osVersion); writeHeader(writer, FIELD_OS_BUILD, osBuild); writeHeader(writer, FIELD_DEVICE_MANUFACTURER, deviceManufacturer); writeHeader(writer, FIELD_DEVICE_MODEL, deviceModel); writeHeader(writer, FIELD_THREAD_NAME, threadName); writeHeader(writer, FIELD_CRASH_REPORTER_KEY, reporterKey); writeHeader(writer, FIELD_APP_START_DATE, DATE_FORMAT.format(appStartDate)); writeHeader(writer, FIELD_APP_CRASH_DATE, DATE_FORMAT.format(appCrashDate)); if (isXamarinException) { writeHeader(writer, FIELD_FORMAT, FIELD_FORMAT_VALUE); } writer.write("\n"); writer.write(throwableStackTrace); writer.flush(); } catch (IOException e) { HockeyLog.error("Error saving crash report!", e); } finally { try { if (writer != null) { writer.close(); } } catch (IOException e1) { HockeyLog.error("Error saving crash report!", e1); } } } private void writeHeader(Writer writer, String name, String value) throws IOException { writer.write(name + ": " + value + "\n"); } public String getCrashIdentifier() { return crashIdentifier; } public String getReporterKey() { return reporterKey; } public void setReporterKey(String reporterKey) { this.reporterKey = reporterKey; } public Date getAppStartDate() { return appStartDate; } public void setAppStartDate(Date appStartDate) { this.appStartDate = appStartDate; } public Date getAppCrashDate() { return appCrashDate; } public void setAppCrashDate(Date appCrashDate) { this.appCrashDate = appCrashDate; } public String getOsVersion() { return osVersion; } public void setOsVersion(String osVersion) { this.osVersion = osVersion; } public String getOsBuild() { return osBuild; } public void setOsBuild(String osBuild) { this.osBuild = osBuild; } public String getDeviceManufacturer() { return deviceManufacturer; } public void setDeviceManufacturer(String deviceManufacturer) { this.deviceManufacturer = deviceManufacturer; } public String getDeviceModel() { return deviceModel; } public void setDeviceModel(String deviceModel) { this.deviceModel = deviceModel; } public String getAppPackage() { return appPackage; } public void setAppPackage(String appPackage) { this.appPackage = appPackage; } public String getAppVersionName() { return appVersionName; } public void setAppVersionName(String appVersionName) { this.appVersionName = appVersionName; } public String getAppVersionCode() { return appVersionCode; } public void setAppVersionCode(String appVersionCode) { this.appVersionCode = appVersionCode; } public String getThreadName() { return threadName; } public void setThreadName(String threadName) { this.threadName = threadName; } public String getThrowableStackTrace() { return throwableStackTrace; } public void setThrowableStackTrace(String throwableStackTrace) { this.throwableStackTrace = throwableStackTrace; } public Boolean getIsXamarinException() { return isXamarinException; } public void setIsXamarinException(Boolean isXamarinException) { this.isXamarinException = isXamarinException; } //We could to without a Format property and getters/setters, but we will eventually use this public String getFormat() { return format; } public void setFormat(String format) { this.format = format; } }