/*
* Copyright 2010 Emmanuel Astier & Kevin Gaudin
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.acra;
import android.app.Application;
import android.content.SharedPreferences;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import org.acra.annotation.ReportsCrashes;
import org.acra.builder.LastActivityManager;
import org.acra.builder.NoOpReportPrimer;
import org.acra.builder.ReportBuilder;
import org.acra.builder.ReportExecutor;
import org.acra.builder.ReportPrimer;
import org.acra.collector.ConfigurationCollector;
import org.acra.collector.CrashReportDataFactory;
import org.acra.config.ACRAConfiguration;
import org.acra.model.Element;
import org.acra.util.ApplicationStartupProcessor;
import org.acra.util.InstanceCreator;
import org.acra.util.ProcessFinisher;
import java.lang.Thread.UncaughtExceptionHandler;
import java.util.Calendar;
import java.util.GregorianCalendar;
import static org.acra.ACRA.LOG_TAG;
/**
* <p>
* The ErrorReporter is a Singleton object in charge of collecting crash context
* data and sending crash reports. It registers itself as the Application's
* Thread default {@link UncaughtExceptionHandler}.
* </p>
* <p>
* When a crash occurs, it collects data of the crash context (device, system,
* stack trace...) and writes a report file in the application private
* directory. This report file is then sent:
* </p>
* <ul>
* <li>immediately if {@link ReportsCrashes#reportingInteractionMode()} is set to
* {@link ReportingInteractionMode#SILENT} or
* {@link ReportingInteractionMode#TOAST},</li>
* <li>on application start if in the previous case the transmission could not
* technically be made,</li>
* <li>when the user accepts to send it if {@link ReportsCrashes#reportingInteractionMode()} is set
* to {@link ReportingInteractionMode#NOTIFICATION}.</li>
* </ul>
* <p>
* If an error occurs while sending a report, it is kept for later attempts.
* </p>
*/
public class ErrorReporter implements Thread.UncaughtExceptionHandler {
private final boolean supportedAndroidVersion;
private final Application context;
@NonNull
private final ACRAConfiguration config;
@NonNull
private final CrashReportDataFactory crashReportDataFactory;
@NonNull
private final ReportExecutor reportExecutor;
@NonNull
private volatile ExceptionHandlerInitializer exceptionHandlerInitializer = new ExceptionHandlerInitializer() {
@Override
public void initializeExceptionHandler(ErrorReporter reporter) {
}
};
/**
* Can only be constructed from within this class.
*
* @param context Context for the application in which ACRA is running.
* @param config AcraConfig to use when reporting and sending errors.
* @param prefs SharedPreferences used by ACRA.
* @param enabled Whether this ErrorReporter should capture Exceptions and forward their reports.
* @param listenForUncaughtExceptions Whether to listen for uncaught Exceptions.
*/
ErrorReporter(@NonNull Application context, @NonNull ACRAConfiguration config, @NonNull SharedPreferences prefs,
boolean enabled, boolean supportedAndroidVersion, boolean listenForUncaughtExceptions) {
this.context = context;
this.config = config;
this.supportedAndroidVersion = supportedAndroidVersion;
// Store the initial Configuration state.
// This is expensive to gather, so only do so if we plan to report it.
final Element initialConfiguration;
if (config.reportContent().contains(ReportField.INITIAL_CONFIGURATION)) {
initialConfiguration = ConfigurationCollector.collectConfiguration(this.context);
} else {
initialConfiguration = ACRAConstants.NOT_AVAILABLE;
}
// Sets the application start date.
// This will be included in the reports, will be helpful compared to user_crash date.
final Calendar appStartDate = new GregorianCalendar();
crashReportDataFactory = new CrashReportDataFactory(this.context, config, prefs, appStartDate, initialConfiguration);
final Thread.UncaughtExceptionHandler defaultExceptionHandler;
if (listenForUncaughtExceptions) {
defaultExceptionHandler = Thread.getDefaultUncaughtExceptionHandler();
Thread.setDefaultUncaughtExceptionHandler(this);
} else {
defaultExceptionHandler = null;
}
final LastActivityManager lastActivityManager = new LastActivityManager(this.context);
final InstanceCreator instanceCreator = new InstanceCreator();
final ReportPrimer reportPrimer = instanceCreator.create(config.reportPrimerClass(), new NoOpReportPrimer());
final ProcessFinisher processFinisher = new ProcessFinisher(context, config, lastActivityManager);
reportExecutor = new ReportExecutor(context, config, crashReportDataFactory, defaultExceptionHandler, reportPrimer, processFinisher);
reportExecutor.setEnabled(enabled);
}
/**
* Deprecated. Use {@link #putCustomData(String, String)}.
*
* @param key A key for your custom data.
* @param value The value associated to your key.
*/
@Deprecated
@SuppressWarnings("unused")
public void addCustomData(@NonNull String key, String value) {
putCustomData(key, value);
}
/**
* <p>
* Use this method to provide the ErrorReporter with data of your running
* application. You should call this at several key places in your code the
* same way as you would output important debug data in a log file. Only the
* latest value is kept for each key (no history of the values is sent in
* the report).
* </p>
*
* @param key A key for your custom data.
* @param value The value associated to your key.
* @return The previous value for this key if there was one, or null.
* @see #removeCustomData(String)
* @see #getCustomData(String)
*/
@SuppressWarnings("unused")
public String putCustomData(@NonNull String key, String value) {
return crashReportDataFactory.putCustomData(key, value);
}
/**
* <p>
* Use this method to perform additional initialization before the
* ErrorReporter handles a throwable. This can be used, for example, to put
* custom data using {@link #putCustomData(String, String)}, which is not
* available immediately after startup. It can be, for example, last 20
* requests or something else. The call is thread safe.
* </p>
* <p>
* {@link ExceptionHandlerInitializer#initializeExceptionHandler(ErrorReporter)}
* will be executed on the main thread in case of uncaught exception and on
* the caller thread of {@link #handleSilentException(Throwable)} or
* {@link #handleException(Throwable)}.
* </p>
* <p>
* Example. Add to the {@link Application#onCreate()}:
* </p>
*
* <pre>
* ACRA.getErrorReporter().setExceptionHandlerInitializer(new ExceptionHandlerInitializer() {
* <code>@Override</code> public void initializeExceptionHandler(ErrorReporter reporter) {
* reporter.putCustomData("CUSTOM_ACCUMULATED_DATA_TAG", someAccumulatedData.toString);
* }
* });
* </pre>
*
* @param initializer The initializer. Can be <code>null</code>.
* @deprecated since 4.8.0 use {@link ReportPrimer} mechanism instead.
*/
public void setExceptionHandlerInitializer(@Nullable ExceptionHandlerInitializer initializer) {
exceptionHandlerInitializer = (initializer != null)
? initializer
: new ExceptionHandlerInitializer() {
@Override
public void initializeExceptionHandler(ErrorReporter reporter) {
}
};
}
/**
* Removes a key/value pair from your reports custom data field.
*
* @param key The key of the data to be removed.
* @return The value for this key before removal.
* @see #putCustomData(String, String)
* @see #getCustomData(String)
*/
@SuppressWarnings("unused")
public String removeCustomData(@NonNull String key) {
return crashReportDataFactory.removeCustomData(key);
}
/**
* Removes all key/value pairs from your reports custom data field.
*/
@SuppressWarnings("unused")
public void clearCustomData() {
crashReportDataFactory.clearCustomData();
}
/**
* Gets the current value for a key in your reports custom data field.
*
* @param key
* The key of the data to be retrieved.
* @return The value for this key.
* @see #putCustomData(String, String)
* @see #removeCustomData(String)
*/
@SuppressWarnings("unused")
public String getCustomData(@NonNull String key) {
return crashReportDataFactory.getCustomData(key);
}
/*
* (non-Javadoc)
*
* @see
* java.lang.Thread.UncaughtExceptionHandler#uncaughtException(java.lang
* .Thread, java.lang.Throwable)
*/
@Override
public void uncaughtException(@Nullable Thread t, @NonNull Throwable e) {
// If we're not enabled then just pass the Exception on to the defaultExceptionHandler.
if (!reportExecutor.isEnabled()) {
reportExecutor.handReportToDefaultExceptionHandler(t, e);
return;
}
try {
ACRA.log.e(LOG_TAG, "ACRA caught a " + e.getClass().getSimpleName() + " for " + context.getPackageName(), e);
if (ACRA.DEV_LOGGING) ACRA.log.d(LOG_TAG, "Building report");
performDeprecatedReportPriming();
// Generate and send crash report
new ReportBuilder()
.uncaughtExceptionThread(t)
.exception(e)
.endApplication()
.build(reportExecutor);
} catch (Throwable fatality) {
// ACRA failed. Prevent any recursive call to ACRA.uncaughtException(), let the native reporter do its job.
ACRA.log.e(LOG_TAG, "ACRA failed to capture the error - handing off to native error reporter" , fatality);
reportExecutor.handReportToDefaultExceptionHandler(t, e);
}
}
/**
* Mark this report as silent as send it.
*
* @param e The {@link Throwable} to be reported. If null the report will
* contain a new Exception("Report requested by developer").
*/
@SuppressWarnings("unused")
public void handleSilentException(@Nullable Throwable e) {
performDeprecatedReportPriming();
new ReportBuilder()
.exception(e)
.sendSilently()
.build(reportExecutor);
}
/**
* Enable or disable this ErrorReporter. By default it is enabled.
*
* @param enabled
* Whether this ErrorReporter should capture Exceptions and
* forward them as crash reports.
*/
public void setEnabled(boolean enabled) {
if (supportedAndroidVersion) {
ACRA.log.i(LOG_TAG, "ACRA is " + (enabled ? "enabled" : "disabled") + " for " + context.getPackageName());
reportExecutor.setEnabled(enabled);
} else {
ACRA.log.w(LOG_TAG, "ACRA 4.7.0+ requires Froyo or greater. ACRA is disabled and will NOT catch crashes or send messages.");
}
}
/**
* This method looks for pending reports and does the action required depending on the interaction mode set.
*
* There is no need to call this method as ACRA will by default check for errors on report start.
*
* Whether ACRA checks for reports on app start is controlled by {@link ACRA#init(Application, ACRAConfiguration, boolean)},
* but the default is that it will.
*
* @deprecated since 4.8.0 No replacement.
*/
@SuppressWarnings( " unused" )
public void checkReportsOnApplicationStart() {
final ApplicationStartupProcessor startupProcessor = new ApplicationStartupProcessor(context, config);
if (config.deleteOldUnsentReportsOnApplicationStart()) {
startupProcessor.deleteUnsentReportsFromOldAppVersion();
}
if (config.deleteUnapprovedReportsOnApplicationStart()) {
startupProcessor.deleteAllUnapprovedReportsBarOne();
}
if (reportExecutor.isEnabled()) {
startupProcessor.sendApprovedReports();
}
}
/**
* Send a report for a {@link Throwable} with the reporting interaction mode
* configured by the developer.
*
* @param e
* The {@link Throwable} to be reported. If null the report will
* contain a new Exception("Report requested by developer").
* @param endApplication
* Set this to true if you want the application to be ended after
* sending the report.
*/
@SuppressWarnings("unused")
public void handleException(@Nullable Throwable e, boolean endApplication) {
performDeprecatedReportPriming();
final ReportBuilder builder = new ReportBuilder();
builder.exception(e);
if (endApplication) {
builder.endApplication();
}
builder.build(reportExecutor);
}
/**
* Send a report for a {@link Throwable} with the reporting interaction mode
* configured by the developer, the application is then killed and restarted
* by the system.
*
* @param e
* The {@link Throwable} to be reported. If null the report will
* contain a new Exception("Report requested by developer").
*/
@SuppressWarnings("unused")
public void handleException(@Nullable Throwable e) {
handleException(e, false);
}
/**
* This method is only here to support the deprecated {@link ExceptionHandlerInitializer} mechanism
* for adding additional data to a crash report.
*/
private void performDeprecatedReportPriming() {
try {
exceptionHandlerInitializer.initializeExceptionHandler(this);
} catch (Exception exceptionInRunnable) {
ACRA.log.w(LOG_TAG, "Failed to initialize " + exceptionHandlerInitializer + " from #handleException");
}
}
}