package org.acra.builder; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.os.Debug; import android.os.Looper; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.app.NotificationCompat; import android.widget.Toast; import org.acra.ACRA; import org.acra.ACRAConstants; import org.acra.ReportingInteractionMode; import org.acra.collector.CrashReportData; import org.acra.collector.CrashReportDataFactory; import org.acra.config.ACRAConfiguration; import org.acra.dialog.CrashReportDialog; import org.acra.file.CrashReportPersister; import org.acra.file.ReportLocator; import org.acra.prefs.SharedPreferencesFactory; import org.acra.sender.SenderServiceStarter; import org.acra.util.ProcessFinisher; import org.acra.util.ToastSender; import java.io.File; import java.util.Date; import static org.acra.ACRA.LOG_TAG; import static org.acra.ReportField.IS_SILENT; import static org.acra.ReportField.USER_CRASH_DATE; /** * Collates, records and initiates the sending of a report. * * @since 4.8.0 */ public final class ReportExecutor { private final Context context; private final ACRAConfiguration config; private final CrashReportDataFactory crashReportDataFactory; // A reference to the system's previous default UncaughtExceptionHandler // kept in order to execute the default exception handling after sending the report. private final Thread.UncaughtExceptionHandler defaultExceptionHandler; private final ReportPrimer reportPrimer; private final ProcessFinisher processFinisher; private boolean enabled = false; /** * Used to create a new (non-cached) PendingIntent each time a new crash occurs. */ private static int mNotificationCounter = 0; public ReportExecutor(@NonNull Context context, @NonNull ACRAConfiguration config, @NonNull CrashReportDataFactory crashReportDataFactory, @Nullable Thread.UncaughtExceptionHandler defaultExceptionHandler, @NonNull ReportPrimer reportPrimer, @NonNull ProcessFinisher processFinisher) { this.context = context; this.config = config; this.crashReportDataFactory = crashReportDataFactory; this.defaultExceptionHandler = defaultExceptionHandler; this.reportPrimer = reportPrimer; this.processFinisher = processFinisher; } /** * Helps manage */ private static class TimeHelper { private Long initialTimeMillis; public void setInitialTimeMillis(long initialTimeMillis) { this.initialTimeMillis = initialTimeMillis; } /** * @return 0 if the initial time has yet to be set otherwise returns the difference between now and the initial time. */ public long getElapsedTime() { return (initialTimeMillis == null) ? 0 : System.currentTimeMillis() - initialTimeMillis; } } public void handReportToDefaultExceptionHandler(@Nullable Thread t, @NonNull Throwable e) { if (defaultExceptionHandler != null) { ACRA.log.i(LOG_TAG, "ACRA is disabled for " + context.getPackageName() + " - forwarding uncaught Exception on to default ExceptionHandler"); defaultExceptionHandler.uncaughtException(t, e); } else { ACRA.log.e(LOG_TAG, "ACRA is disabled for " + context.getPackageName() + " - no default ExceptionHandler"); ACRA.log.e(LOG_TAG, "ACRA caught a " + e.getClass().getSimpleName() + " for " + context.getPackageName(), e); } } public boolean isEnabled() { return enabled; } public void setEnabled(boolean enabled) { this.enabled = enabled; } /** * Try to send a report, if an error occurs stores a report file for a later attempt. * * @param reportBuilder The report builder used to assemble the report */ public void execute(@NonNull final ReportBuilder reportBuilder) { if (!enabled) { ACRA.log.v(LOG_TAG, "ACRA is disabled. Report not sent."); return; } // Prime this crash report with any extra data. reportPrimer.primeReport(context, reportBuilder); boolean sendOnlySilentReports = false; final ReportingInteractionMode reportingInteractionMode; if (!reportBuilder.isSendSilently()) { // No interaction mode defined in the ReportBuilder, we assume it has been set during ACRA.initACRA() reportingInteractionMode = config.reportingInteractionMode(); } else { reportingInteractionMode = ReportingInteractionMode.SILENT; // An interaction mode has been provided. If ACRA has been // initialized with a non SILENT mode and this mode is overridden // with SILENT, then we have to send only reports which have been // explicitly declared as silent via handleSilentException(). if (config.reportingInteractionMode() != ReportingInteractionMode.SILENT) { sendOnlySilentReports = true; } } final boolean shouldDisplayToast = reportingInteractionMode == ReportingInteractionMode.TOAST || (config.resToastText() != 0 && (reportingInteractionMode == ReportingInteractionMode.NOTIFICATION || reportingInteractionMode == ReportingInteractionMode.DIALOG)); final TimeHelper sentToastTimeMillis = new TimeHelper(); if (shouldDisplayToast) { new Thread() { /* * (non-Javadoc) * * @see java.lang.Thread#run() */ @Override public void run() { Looper.prepare(); ToastSender.sendToast(context, config.resToastText(), Toast.LENGTH_LONG); sentToastTimeMillis.setInitialTimeMillis(System.currentTimeMillis()); Looper.loop(); } }.start(); // We will wait a few seconds at the end of the method to be sure // that the Toast can be read by the user. } final CrashReportData crashReportData = crashReportDataFactory.createCrashData(reportBuilder); // Always write the report file final File reportFile = getReportFileName(crashReportData); saveCrashReportFile(reportFile, crashReportData); final SharedPreferences prefs = new SharedPreferencesFactory(context, config).create(); if (reportingInteractionMode == ReportingInteractionMode.SILENT || reportingInteractionMode == ReportingInteractionMode.TOAST || prefs.getBoolean(ACRA.PREF_ALWAYS_ACCEPT, false)) { // Approve and then send reports now startSendingReports(sendOnlySilentReports); if ((reportingInteractionMode == ReportingInteractionMode.SILENT) && !reportBuilder.isEndApplication()) { // Report is being sent silently and the application is not ending. // So no need to wait around for the sender to complete. return; } } else if (reportingInteractionMode == ReportingInteractionMode.NOTIFICATION) { if (ACRA.DEV_LOGGING) ACRA.log.d(LOG_TAG, "Creating Notification."); createNotification(reportFile, reportBuilder); } final boolean showDirectDialog = (reportingInteractionMode == ReportingInteractionMode.DIALOG) && !prefs.getBoolean(ACRA.PREF_ALWAYS_ACCEPT, false); if (shouldDisplayToast) { // A toast is being displayed, we have to wait for its end before doing anything else. new Thread() { @Override public void run() { if (ACRA.DEV_LOGGING) ACRA.log.d(LOG_TAG, "Waiting for " + ACRAConstants.TOAST_WAIT_DURATION + " millis from " + sentToastTimeMillis.initialTimeMillis + " currentMillis=" + System.currentTimeMillis()); final long sleep = ACRAConstants.TOAST_WAIT_DURATION - sentToastTimeMillis.getElapsedTime(); try { // Wait a bit to let the user read the toast if (sleep > 0L) Thread.sleep(sleep); } catch (InterruptedException e1) { if (ACRA.DEV_LOGGING) ACRA.log.d(LOG_TAG, "Interrupted while waiting for Toast to end.", e1); } if (ACRA.DEV_LOGGING) ACRA.log.d(LOG_TAG, "Finished waiting for Toast"); dialogAndEnd(reportBuilder, reportFile, showDirectDialog); } }.start(); } else { dialogAndEnd(reportBuilder, reportFile, showDirectDialog); } } private void dialogAndEnd(@NonNull ReportBuilder reportBuilder, @NonNull File reportFile, boolean shouldShowDialog) { if (shouldShowDialog) { // Create a new activity task with the confirmation dialog. // This new task will be persisted on application restart // right after its death. if (ACRA.DEV_LOGGING) ACRA.log.d(LOG_TAG, "Creating CrashReportDialog for " + reportFile); final Intent dialogIntent = createCrashReportDialogIntent(reportFile, reportBuilder); dialogIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); context.startActivity(dialogIntent); } if (ACRA.DEV_LOGGING) ACRA.log.d(LOG_TAG, "Wait for Toast + worker ended. Kill Application ? " + reportBuilder.isEndApplication()); if (reportBuilder.isEndApplication()) { if(Debug.isDebuggerConnected()){ //Killing a process with a debugger attached would kill the whole application, so don't do that. final String warning = "Warning: Acra may behave differently with a debugger attached"; new Thread() { @Override public void run() { Looper.prepare(); Toast.makeText(context, warning, Toast.LENGTH_LONG).show(); Looper.loop(); } }.start(); ACRA.log.w(LOG_TAG, warning); //do as much cleanup as we can without killing the process processFinisher.finishLastActivity(reportBuilder.getUncaughtExceptionThread()); }else { endApplication(reportBuilder.getUncaughtExceptionThread(), reportBuilder.getException()); } } } /** * End the application. */ private void endApplication(@Nullable Thread uncaughtExceptionThread, Throwable th) { final boolean letDefaultHandlerEndApplication = config.alsoReportToAndroidFramework(); final boolean handlingUncaughtException = uncaughtExceptionThread != null; if (handlingUncaughtException && letDefaultHandlerEndApplication && defaultExceptionHandler != null) { // Let the system default handler do it's job and display the force close dialog. if (ACRA.DEV_LOGGING) ACRA.log.d(LOG_TAG, "Handing Exception on to default ExceptionHandler"); defaultExceptionHandler.uncaughtException(uncaughtExceptionThread, th); } else { processFinisher.endApplication(uncaughtExceptionThread); } } /** * Starts a Thread to start sending outstanding error reports. * * @param onlySendSilentReports If true then only send silent reports. */ private void startSendingReports(boolean onlySendSilentReports) { if (enabled) { final SenderServiceStarter starter = new SenderServiceStarter(context, config); starter.startService(onlySendSilentReports, true); } else { ACRA.log.w(LOG_TAG, "Would be sending reports, but ACRA is disabled"); } } /** * Creates a status bar notification. * * The action triggered when the notification is selected is to start the * {@link CrashReportDialog} Activity. * * @param reportFile Report file to send. */ private void createNotification(@NonNull File reportFile, @NonNull ReportBuilder reportBuilder) { final NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); // Default notification icon is the warning symbol final int icon = config.resNotifIcon(); final CharSequence tickerText = context.getText(config.resNotifTickerText()); final long when = System.currentTimeMillis(); if (ACRA.DEV_LOGGING) ACRA.log.d(LOG_TAG, "Creating Notification for " + reportFile); final Intent crashReportDialogIntent = createCrashReportDialogIntent(reportFile, reportBuilder); final PendingIntent contentIntent = PendingIntent.getActivity(context, mNotificationCounter++, crashReportDialogIntent, PendingIntent.FLAG_UPDATE_CURRENT); final CharSequence contentTitle = context.getText(config.resNotifTitle()); final CharSequence contentText = context.getText(config.resNotifText()); final NotificationCompat.Builder builder = new NotificationCompat.Builder(context); final Notification notification = builder .setSmallIcon(icon) .setTicker(tickerText) .setWhen(when) .setAutoCancel(true) .setContentTitle(contentTitle) .setContentText(contentText) .setContentIntent(contentIntent) .build(); notification.flags |= Notification.FLAG_AUTO_CANCEL; // The deleteIntent is invoked when the user swipes away the Notification. // In this case we invoke the CrashReportDialog with EXTRA_FORCE_CANCEL==true // which will cause BaseCrashReportDialog to clear the crash report and finish itself. final Intent deleteIntent = createCrashReportDialogIntent(reportFile, reportBuilder); deleteIntent.putExtra(ACRAConstants.EXTRA_FORCE_CANCEL, true); notification.deleteIntent = PendingIntent.getActivity(context, -1, deleteIntent, 0); // Send new notification notificationManager.notify(ACRAConstants.NOTIF_CRASH_ID, notification); } @NonNull private File getReportFileName(@NonNull CrashReportData crashData) { final String timestamp = crashData.getProperty(USER_CRASH_DATE); final String isSilent = crashData.getProperty(IS_SILENT); final String fileName = (timestamp != null ? timestamp : new Date().getTime()) // Need to check for null because old version of ACRA did not always capture USER_CRASH_DATE + (isSilent != null ? ACRAConstants.SILENT_SUFFIX : "") + ACRAConstants.REPORTFILE_EXTENSION; final ReportLocator reportLocator = new ReportLocator(context); return new File(reportLocator.getUnapprovedFolder(), fileName); } /** * When a report can't be sent, it is saved here in a file in the root of * the application private directory. * * @param file * In a few rare cases, we write the report again with additional * data (user comment for example). In such cases, you can * provide the already existing file name here to overwrite the * report file. If null, a new file report will be generated * @param crashData * Can be used to save an alternative (or previously generated) * report data. Used to store again a report with the addition of * user comment. If null, the default current crash data are * used. */ private void saveCrashReportFile(@NonNull File file, @NonNull CrashReportData crashData) { try { if (ACRA.DEV_LOGGING) ACRA.log.d(LOG_TAG, "Writing crash report file " + file); final CrashReportPersister persister = new CrashReportPersister(); persister.store(crashData, file); } catch (Exception e) { ACRA.log.e(LOG_TAG, "An error occurred while writing the report file...", e); } } /** * Creates an Intent that can be used to create and show a CrashReportDialog. * * @param reportFile Error report file to display in the crash report dialog. * @param reportBuilder ReportBuilder containing the details of the crash. */ @NonNull private Intent createCrashReportDialogIntent(@NonNull File reportFile, @NonNull ReportBuilder reportBuilder) { if (ACRA.DEV_LOGGING) ACRA.log.d(LOG_TAG, "Creating DialogIntent for " + reportFile + " exception=" + reportBuilder.getException()); final Intent dialogIntent = new Intent(context, config.reportDialogClass()); dialogIntent.putExtra(ACRAConstants.EXTRA_REPORT_FILE, reportFile); dialogIntent.putExtra(ACRAConstants.EXTRA_REPORT_EXCEPTION, reportBuilder.getException()); dialogIntent.putExtra(ACRAConstants.EXTRA_REPORT_CONFIG, config); return dialogIntent; } }