/*
* 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 org.acra.annotation.ReportsCrashes;
import org.acra.log.ACRALog;
import org.acra.log.HollowLog;
import org.acra.log.AndroidLogDelegate;
import android.app.Application;
import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.preference.PreferenceManager;
/**
* 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
*
*/
public class ACRA {
public static final boolean DEV_LOGGING = false; // Should be false for
// release.
public static final String LOG_TAG = ACRA.class.getSimpleName();
public static ACRALog log = new AndroidLogDelegate();
/**
* 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;
// Accessible via ACRA#getErrorReporter().
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;
/**
* <p>
* Initialize ACRA for a given Application. The call to this method should
* be placed as soon as possible in the {@link Application#onCreate()}
* method.
* </p>
*
* @param app Your Application class.
* @throws IllegalStateException if it is called more than once.
*/
public static void init(Application app) {
final ReportsCrashes reportsCrashes = app.getClass().getAnnotation(ReportsCrashes.class);
if (reportsCrashes == null) {
log.e(LOG_TAG,
"ACRA#init called but no ReportsCrashes annotation on Application " + app.getPackageName());
return;
}
init(app, new ACRAConfiguration(reportsCrashes));
}
/**
* <p>
* Initialize ACRA for a given Application. The call to this method should
* be placed as soon as possible in the {@link Application#onCreate()}
* method.
* </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(Application app, 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#onCreate()}
* method.
* </p>
*
* @param app Your Application class.
* @param config ACRAConfiguration to manually set up ACRA configuration.
* @param checkReportsOnApplicationStart Whether to invoke
* ErrorReporter.checkReportsOnApplicationStart(). Apps which adjust the
* ReportSenders should set this to false and call
* checkReportsOnApplicationStart() themselves to prevent a potential
* race with the SendWorker and list of ReportSenders.
* @throws IllegalStateException if it is called more than once.
*/
public static void init(Application app, ACRAConfiguration config, boolean checkReportsOnApplicationStart){
if (mApplication != null) {
log.w(LOG_TAG, "ACRA#init called more than once. Won't do anything more.");
return;
}
mApplication = app;
if (config == null) {
log.e(LOG_TAG, "ACRA#init called but no ACRAConfiguration provided");
return;
}
configProxy = config;
final SharedPreferences prefs = getACRASharedPreferences();
try {
checkCrashResources(config);
log.d(LOG_TAG, "ACRA is enabled for " + mApplication.getPackageName() + ", initializing...");
// Initialize ErrorReporter with all required data
final boolean enableAcra = !shouldDisableACRA(prefs);
final ErrorReporter errorReporter = new ErrorReporter(mApplication, prefs, enableAcra);
// Append ReportSenders.
errorReporter.setDefaultReportSenders();
errorReporterSingleton = errorReporter;
// Check for pending reports
if (checkReportsOnApplicationStart) {
errorReporter.checkReportsOnApplicationStart();
}
} catch (ACRAConfigurationException e) {
log.w(LOG_TAG, "Error : ", e);
}
// 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(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 the current instance of ErrorReporter.
* @throws IllegalStateException
* if {@link ACRA#init(Application)} has not yet
* been called.
*/
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(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;
}
/**
* Checks that mandatory configuration items have been provided.
*
* @throws ACRAConfigurationException
* if required values are missing.
*/
static void checkCrashResources(ReportsCrashes conf) throws ACRAConfigurationException {
switch (conf.mode()) {
case TOAST:
if (conf.resToastText() == 0) {
throw new ACRAConfigurationException(
"TOAST mode: you have to define the resToastText parameter in your application @ReportsCrashes() annotation.");
}
break;
case NOTIFICATION:
if (conf.resNotifTickerText() == 0 || conf.resNotifTitle() == 0 || conf.resNotifText() == 0) {
throw new ACRAConfigurationException(
"NOTIFICATION mode: you have to define at least the resNotifTickerText, resNotifTitle, resNotifText parameters in your application @ReportsCrashes() annotation.");
}
if (CrashReportDialog.class.equals(conf.reportDialogClass()) && conf.resDialogText() == 0) {
throw new ACRAConfigurationException(
"NOTIFICATION mode: using the (default) CrashReportDialog requires you have to define the resDialogText parameter in your application @ReportsCrashes() annotation.");
}
break;
case DIALOG:
if (CrashReportDialog.class.equals(conf.reportDialogClass()) && conf.resDialogText() == 0) {
throw new ACRAConfigurationException(
"DIALOG mode: using the (default) CrashReportDialog requires you to define the resDialogText parameter in your application @ReportsCrashes() annotation.");
}
break;
default:
break;
}
}
/**
* Retrieves the {@link SharedPreferences} instance where user adjustable
* settings for ACRA are stored. Default are the Application default
* SharedPreferences, but you can provide another SharedPreferences name
* with {@link ReportsCrashes#sharedPreferencesName()}.
*
* @return The Shared Preferences where ACRA will retrieve its user
* adjustable setting.
*/
public static SharedPreferences getACRASharedPreferences() {
ReportsCrashes conf = getConfig();
if (!"".equals(conf.sharedPreferencesName())) {
return mApplication.getSharedPreferences(conf.sharedPreferencesName(), conf.sharedPreferencesMode());
} else {
return PreferenceManager.getDefaultSharedPreferences(mApplication);
}
}
/**
* Provides the current ACRA configuration.
*
* @return Current ACRA {@link ReportsCrashes} configuration instance.
*/
public static ACRAConfiguration getConfig() {
if (configProxy == null) {
if (mApplication == null) {
log.w(LOG_TAG,
"Calling ACRA.getConfig() before ACRA.init() gives you an empty configuration instance. You might prefer calling ACRA.getNewDefaultConfig(Application) to get an instance with default values taken from a @ReportsCrashes annotation.");
}
configProxy = getNewDefaultConfig(mApplication);
}
return configProxy;
}
/**
* @param app Your Application class.
* @return new {@link ACRAConfiguration} instance with values initialized
* from the {@link ReportsCrashes} annotation.
*/
public static ACRAConfiguration getNewDefaultConfig(Application app) {
if(app != null) {
return new ACRAConfiguration(app.getClass().getAnnotation(ReportsCrashes.class));
} else {
return new ACRAConfiguration(null);
}
}
private static ACRAConfiguration configProxy;
/**
* Returns true if the application is debuggable.
*
* @return true if the application is debuggable.
*/
static boolean isDebuggable() {
PackageManager pm = mApplication.getPackageManager();
try {
return ((pm.getApplicationInfo(mApplication.getPackageName(), 0).flags & ApplicationInfo.FLAG_DEBUGGABLE) > 0);
} catch (NameNotFoundException e) {
return false;
}
}
static Application getApplication() {
return mApplication;
}
public static void setLog(ACRALog log) {
if (log == null) {
throw new NullPointerException("ACRALog cannot be null");
}
ACRA.log = log;
}
}