/*
* 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.Context;
import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.os.Build;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import org.acra.annotation.ReportsCrashes;
import org.acra.config.ACRAConfiguration;
import org.acra.config.ACRAConfigurationException;
import org.acra.config.ConfigurationBuilder;
import org.acra.legacy.LegacyFileHandler;
import org.acra.log.ACRALog;
import org.acra.log.AndroidLogDelegate;
import org.acra.prefs.SharedPreferencesFactory;
import org.acra.util.ApplicationStartupProcessor;
import org.acra.util.IOUtils;
import java.io.FileInputStream;
import java.io.IOException;
/**
* Use this class to initialize the crash reporting feature using
* {@link #init(Application)} as soon as possible in your {@link Application}
* subclass {@link Application#onCreate()} method. Configuration items must have
* been set by using {@link ReportsCrashes} above the declaration of your
* {@link Application} subclass.
*
* @author Kevin Gaudin
*
*/
@SuppressWarnings({"WeakerAccess","unused"})
public final class ACRA {
private ACRA(){}
public static /*non-final*/ boolean DEV_LOGGING = false; // Should be false for release.
public static final String LOG_TAG = ACRA.class.getSimpleName();
@NonNull
public static ACRALog log = new AndroidLogDelegate();
private static final String ACRA_PRIVATE_PROCESS_NAME= ":acra";
/**
* The key of the application default SharedPreference where you can put a
* 'true' Boolean value to disable ACRA.
*/
public static final String PREF_DISABLE_ACRA = "acra.disable";
/**
* Alternatively, you can use this key if you prefer your users to have the
* checkbox ticked to enable crash reports. If both acra.disable and
* acra.enable are set, the value of acra.disable takes over the other.
*/
public static final String PREF_ENABLE_ACRA = "acra.enable";
/**
* The key of the SharedPreference allowing the user to disable sending
* content of logcat/dropbox. System logs collection is also dependent of
* the READ_LOGS permission.
*/
public static final String PREF_ENABLE_SYSTEM_LOGS = "acra.syslog.enable";
/**
* The key of the SharedPreference allowing the user to disable sending his
* device id. Device ID collection is also dependent of the READ_PHONE_STATE
* permission.
*/
public static final String PREF_ENABLE_DEVICE_ID = "acra.deviceid.enable";
/**
* The key of the SharedPreference allowing the user to always include his
* email address.
*/
public static final String PREF_USER_EMAIL_ADDRESS = "acra.user.email";
/**
* The key of the SharedPreference allowing the user to automatically accept
* sending reports.
*/
public static final String PREF_ALWAYS_ACCEPT = "acra.alwaysaccept";
/**
* The version number of the application the last time ACRA was started.
* This is used to determine whether unsent reports should be discarded
* because they are old and out of date.
*/
public static final String PREF_LAST_VERSION_NR = "acra.lastVersionNr";
private static Application mApplication;
@Nullable
private static ACRAConfiguration configProxy;
// Accessible via ACRA#getErrorReporter().
@Nullable
private static ErrorReporter errorReporterSingleton;
// NB don't convert to a local field because then it could be garbage
// collected and then we would have no PreferenceListener.
private static OnSharedPreferenceChangeListener mPrefListener; // TODO consider moving to ErrorReport so it doesn't need to be a static field.
/**
* <p>
* Initialize ACRA for a given Application.
*
* The call to this method should be placed as soon as possible in the {@link Application#attachBaseContext(Context)} method.
*
* Uses the configuration as configured with the @ReportCrashes annotation.
* Sends any unsent reports.
* </p>
*
* @param app Your Application class.
* @throws IllegalStateException if it is called more than once.
*/
public static void init(@NonNull Application app) {
final ReportsCrashes reportsCrashes = app.getClass().getAnnotation(ReportsCrashes.class);
if (reportsCrashes == null) {
log.e(LOG_TAG, "ACRA#init(Application) called but no ReportsCrashes annotation on Application " + app.getPackageName());
return;
}
init(app, new ConfigurationBuilder(app));
}
/**
* <p>
* Initialize ACRA for a given Application.
*
* The call to this method should be placed as soon as possible in the {@link Application#attachBaseContext(Context)} method.
*
* Uses the configuration as configured with the @ReportCrashes annotation.
* Sends any unsent reports.
* </p>
*
* @param app Your Application class.
* @param builder ConfigurationBuilder to manually set up ACRA configuration.
*/
public static void init(@NonNull Application app, @NonNull ConfigurationBuilder builder) {
init(app, builder, true);
}
/**
* <p>
* Initialize ACRA for a given Application.
*
* The call to this method should be placed as soon as possible in the {@link Application#attachBaseContext(Context)} method.
* </p>
*
* @param app Your Application class.
* @param builder ConfigurationBuilder to manually set up ACRA configuration.
* @param checkReportsOnApplicationStart Whether to invoke ErrorReporter.checkReportsOnApplicationStart().
*/
public static void init(@NonNull Application app, @NonNull ConfigurationBuilder builder, boolean checkReportsOnApplicationStart) {
try {
init(app, builder.build(), checkReportsOnApplicationStart);
} catch (ACRAConfigurationException e) {
log.w(LOG_TAG, "Configuration Error - ACRA not started : " + e.getMessage());
}
}
/**
* <p>
* Initialize ACRA for a given Application.
*
* The call to this method should be placed as soon as possible in the {@link Application#attachBaseContext(Context)} method.
*
* Sends any unsent reports.
* </p>
*
* @param app Your Application class.
* @param config ACRAConfiguration to manually set up ACRA configuration.
* @throws IllegalStateException if it is called more than once.
*/
public static void init(@NonNull Application app, @NonNull ACRAConfiguration config) {
init(app, config, true);
}
/**
* <p>
* Initialize ACRA for a given Application. The call to this method should
* be placed as soon as possible in the {@link Application#attachBaseContext(Context)}
* method.
* </p>
*
* @param app Your Application class.
* @param config ACRAConfiguration to manually set up ACRA configuration.
* @param checkReportsOnApplicationStart Whether to invoke ErrorReporter.checkReportsOnApplicationStart().
* @throws IllegalStateException if it is called more than once.
*/
public static void init(@NonNull Application app, @NonNull ACRAConfiguration config, boolean checkReportsOnApplicationStart){
final boolean senderServiceProcess = isACRASenderServiceProcess();
if (senderServiceProcess) {
if (ACRA.DEV_LOGGING) log.d(LOG_TAG, "Not initialising ACRA to listen for uncaught Exceptions as this is the SendWorker process and we only send reports, we don't capture them to avoid infinite loops");
}
final boolean supportedAndroidVersion = Build.VERSION.SDK_INT >= Build.VERSION_CODES.FROYO;
if (!supportedAndroidVersion){
// NB We keep initialising so that everything is configured. But ACRA is never enabled below.
log.w(LOG_TAG, "ACRA 4.7.0+ requires Froyo or greater. ACRA is disabled and will NOT catch crashes or send messages.");
}
if (mApplication != null) {
log.w(LOG_TAG, "ACRA#init called more than once. Won't do anything more.");
return;
}
mApplication = app;
//noinspection ConstantConditions
if (config == null) {
log.e(LOG_TAG, "ACRA#init called but no ACRAConfiguration provided");
return;
}
configProxy = config;
final SharedPreferences prefs = new SharedPreferencesFactory(mApplication, configProxy).create();
new LegacyFileHandler(app, prefs).updateToCurrentVersionIfNecessary();
// Initialize ErrorReporter with all required data
final boolean enableAcra = supportedAndroidVersion && !shouldDisableACRA(prefs);
if (!senderServiceProcess) {
// Indicate that ACRA is or is not listening for crashes.
log.i(LOG_TAG, "ACRA is " + (enableAcra ? "enabled" : "disabled") + " for " + mApplication.getPackageName() + ", initializing...");
}
errorReporterSingleton = new ErrorReporter(mApplication, configProxy, prefs, enableAcra, supportedAndroidVersion, !senderServiceProcess);
// Check for approved reports and send them (if enabled).
// NB don't check if senderServiceProcess as it will gather these reports itself.
if (checkReportsOnApplicationStart && !senderServiceProcess) {
final ApplicationStartupProcessor startupProcessor = new ApplicationStartupProcessor(mApplication, config);
if (config.deleteOldUnsentReportsOnApplicationStart()) {
startupProcessor.deleteUnsentReportsFromOldAppVersion();
}
if (config.deleteUnapprovedReportsOnApplicationStart()) {
startupProcessor.deleteAllUnapprovedReportsBarOne();
}
if (enableAcra) {
startupProcessor.sendApprovedReports();
}
}
// We HAVE to keep a reference otherwise the listener could be garbage
// collected:
// http://stackoverflow.com/questions/2542938/sharedpreferences-onsharedpreferencechangelistener-not-being-called-consistently/3104265#3104265
mPrefListener = new OnSharedPreferenceChangeListener() {
@Override
public void onSharedPreferenceChanged(@NonNull SharedPreferences sharedPreferences, String key) {
if (PREF_DISABLE_ACRA.equals(key) || PREF_ENABLE_ACRA.equals(key)) {
final boolean enableAcra = !shouldDisableACRA(sharedPreferences);
getErrorReporter().setEnabled(enableAcra);
}
}
};
// This listener has to be set after initAcra is called to avoid a
// NPE in ErrorReporter.disable() because
// the context could be null at this moment.
prefs.registerOnSharedPreferenceChangeListener(mPrefListener);
}
/**
* @return true is ACRA has been initialised.
*/
@SuppressWarnings("unused")
public static boolean isInitialised() {
return configProxy != null;
}
/**
* @return true if the current process is the process running the SenderService.
* NB this assumes that your SenderService is configured to used the default ':acra' process.
*/
public static boolean isACRASenderServiceProcess() {
final String processName = getCurrentProcessName();
if (ACRA.DEV_LOGGING) log.d(LOG_TAG, "ACRA processName='" + processName + '\'');
//processName sometimes (or always?) starts with the package name, so we use endsWith instead of equals
return processName != null && processName.endsWith(ACRA_PRIVATE_PROCESS_NAME);
}
@Nullable
private static String getCurrentProcessName() {
try {
return IOUtils.streamToString(new FileInputStream("/proc/self/cmdline")).trim();
} catch (IOException e) {
return null;
}
}
/**
* @return the current instance of ErrorReporter.
* @throws IllegalStateException if {@link ACRA#init(android.app.Application)} has not yet been called.
*/
@NonNull
public static ErrorReporter getErrorReporter() {
if (errorReporterSingleton == null) {
throw new IllegalStateException("Cannot access ErrorReporter before ACRA#init");
}
return errorReporterSingleton;
}
/**
* Check if the application default shared preferences contains true for the
* key "acra.disable", do not activate ACRA. Also checks the alternative
* opposite setting "acra.enable" if "acra.disable" is not found.
*
* @param prefs
* SharedPreferences to check to see whether ACRA should be
* disabled.
* @return true if prefs indicate that ACRA should be disabled.
*/
private static boolean shouldDisableACRA(@NonNull SharedPreferences prefs) {
boolean disableAcra = false;
try {
final boolean enableAcra = prefs.getBoolean(PREF_ENABLE_ACRA, true);
disableAcra = prefs.getBoolean(PREF_DISABLE_ACRA, !enableAcra);
} catch (Exception e) {
// In case of a ClassCastException
}
return disableAcra;
}
/**
* @return The Shared Preferences where ACRA will retrieve its user adjustable setting.
* @deprecated since 4.8.0 use {@link SharedPreferencesFactory} instead.
*/
@SuppressWarnings( "unused" )
@NonNull
public static SharedPreferences getACRASharedPreferences() {
if (configProxy == null) {
throw new IllegalStateException("Cannot call ACRA.getACRASharedPreferences() before ACRA.init().");
}
return new SharedPreferencesFactory(mApplication, configProxy).create();
}
/**
* Provides the current ACRA configuration.
*
* @return Current ACRA {@link ReportsCrashes} configuration instance.
* @deprecated since 4.8.0 {@link ACRAConfiguration} should be passed into classes instead of retrieved statically.
*/
@NonNull
public static ACRAConfiguration getConfig() {
if (configProxy == null) {
throw new IllegalStateException("Cannot call ACRA.getConfig() before ACRA.init().");
}
return configProxy;
}
public static void setLog(@NonNull ACRALog log) {
//noinspection ConstantConditions (do not rely on annotation alone)
if (log == null) {
throw new NullPointerException("ACRALog cannot be null");
}
ACRA.log = log;
}
}