/*
* 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 static org.acra.ACRA.LOG_TAG;
import static org.acra.ReportField.*;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.io.Writer;
import java.lang.Thread.UncaughtExceptionHandler;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.InvalidPropertiesFormatException;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import org.acra.annotation.ReportsCrashes;
import org.acra.sender.ReportSender;
import org.acra.sender.ReportSenderException;
import org.acra.util.Installation;
import android.Manifest.permission;
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.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.res.Configuration;
import android.graphics.Point;
import android.os.Environment;
import android.os.Looper;
import android.os.PowerManager;
import android.os.StatFs;
import android.telephony.TelephonyManager;
import android.text.format.Time;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.Display;
import android.view.WindowManager;
import android.widget.Toast;
/**
* <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 :
* <ul>
* <li>immediately if {@link #mReportingInteractionMode} 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 #mReportingInteractionMode} is set to
* {@link ReportingInteractionMode#NOTIFICATION}.</li>
* </ul>
* </p>
* <p>
* If an error occurs while sending a report, it is kept for later attempts.
* </p>
*/
public class ErrorReporter implements Thread.UncaughtExceptionHandler {
public static final String REPORTFILE_EXTENSION = ".stacktrace";
private static boolean enabled = false;
/**
* Contains the active {@link ReportSender}s.
*/
private static ArrayList<ReportSender> mReportSenders = new ArrayList<ReportSender>();
/**
* Checks and send reports on a separate Thread.
*
* @author Kevin Gaudin
*/
final class ReportsSenderWorker extends Thread {
private String mCommentedReportFileName = null;
private String mUserComment = null;
private String mUserEmail = null;
private boolean mSendOnlySilentReports = false;
private boolean mApprovePendingReports = false;
/**
* Creates a new {@link ReportsSenderWorker} to try sending pending reports.
*
* @param sendOnlySilentReports
* If set to true, will send only reports which have been explicitly declared as silent by the
* application developer.
*/
public ReportsSenderWorker(boolean sendOnlySilentReports) {
mSendOnlySilentReports = sendOnlySilentReports;
}
/**
* Creates a new {@link ReportsSenderWorker} which will try to send ALL pending reports.
*/
public ReportsSenderWorker() {
}
/*
* (non-Javadoc)
*
* @see java.lang.Thread#run()
*/
@Override
public void run() {
final PowerManager.WakeLock wakeLock = acquireWakeLock();
try {
if (mApprovePendingReports) {
approvePendingReports();
mCommentedReportFileName = mCommentedReportFileName.replace(REPORTFILE_EXTENSION, APPROVED_SUFFIX + REPORTFILE_EXTENSION);
}
addUserDataToReport(mContext, mCommentedReportFileName, mUserComment, mUserEmail);
checkAndSendReports(mContext, mSendOnlySilentReports);
} finally {
if (wakeLock != null) {
wakeLock.release();
}
}
}
/**
* @return an acquired (partial) WakeLock if the app has the WAKE_LOCK permission, otherwise returns null.
*/
private PowerManager.WakeLock acquireWakeLock() {
final PackageManager pm = mContext.getPackageManager();
final boolean hasPermission = (pm != null) ? (pm.checkPermission(permission.WAKE_LOCK, mContext.getPackageName()) == PackageManager.PERMISSION_GRANTED) : false;
if (!hasPermission) {
return null;
}
final PowerManager powerManager = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
final PowerManager.WakeLock wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "ACRA wakelock");
wakeLock.acquire();
return wakeLock;
}
/**
* Associates a user comment to a specific report file name.
*
* @param reportFileName
* The file name of the report.
* @param userComment
* The comment given by the user.
*/
void setUserComment(String reportFileName, String userComment) {
mCommentedReportFileName = reportFileName;
mUserComment = userComment;
}
/**
* Associates a user email to a specific report file name.
*
* @param reportFileName
* The file name of the report.
* @param userEmail
* The email address given by the user.
*/
void setUserEmail(String reportFileName, String userEmail) {
mCommentedReportFileName = reportFileName;
mUserEmail = userEmail;
}
/**
* Sets all pending reports as approved for sending by the user.
*/
public void setApprovePendingReports() {
mApprovePendingReports = true;
}
}
/**
* This is the number of previously stored reports that we send in {@link #checkAndSendReports(Context, boolean)}.
* The number of reports is limited to avoid ANR on application start.
*/
private static final int MAX_SEND_REPORTS = 5;
// This is where we collect crash data
private static CrashReportData mCrashProperties = new CrashReportData();
// Some custom parameters can be added by the application developer. These
// parameters are stored here.
Map<String, String> mCustomParameters = new HashMap<String, String>();
// This key is used to store the silent state of a report sent by
// handleSilentException().
static final String SILENT_SUFFIX = "-" + IS_SILENT;
// Suffix to be added to report files when they have been approved by the
// user in NOTIFICATION mode
static final String APPROVED_SUFFIX = "-approved";
// Used in the intent starting CrashReportDialog to provide the name of the
// latest generated report file in order to be able to associate the user
// comment.
static final String EXTRA_REPORT_FILE_NAME = "REPORT_FILE_NAME";
// A reference to the system's previous default UncaughtExceptionHandler
// kept in order to execute the default exception handling after sending
// the report.
private Thread.UncaughtExceptionHandler mDfltExceptionHandler;
// Our singleton instance.
private static ErrorReporter mInstanceSingleton;
// The application context
private static Context mContext;
// The Configuration obtained on application start.
private String mInitialConfiguration;
// User interaction mode defined by the application developer.
private ReportingInteractionMode mReportingInteractionMode = ReportingInteractionMode.SILENT;
/**
* Flag all pending reports as "approved" by the user. These reports can be sent.
*/
public void approvePendingReports() {
Log.d(LOG_TAG, "Mark all pending reports as approved.");
String[] reportFileNames = getCrashReportFilesList();
File reportFile = null;
String newName;
for (String reportFileName : reportFileNames) {
if (!isApproved(reportFileName)) {
reportFile = new File(mContext.getFilesDir(), reportFileName);
newName = reportFileName.replace(REPORTFILE_EXTENSION, APPROVED_SUFFIX + REPORTFILE_EXTENSION);
reportFile.renameTo(new File(mContext.getFilesDir(), newName));
}
}
}
/**
* Deprecated. Use {@link #putCustomData(String, String)}.
*
* @param key
* @param value
*/
@Deprecated
public void addCustomData(String key, String value) {
mCustomParameters.put(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>
* <p>
* The key/value pairs will be stored in the GoogleDoc spreadsheet in the "custom" column, as a text containing a
* 'key = value' pair on each line.
* </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)
*/
public String putCustomData(String key, String value) {
return mCustomParameters.put(key, value);
}
/**
* Removes a key/value pair from your reports custom data field.
*
* @param key
* The key to be removed.
* @return The value for this key before removal.
* @see #putCustomData(String, String)
* @see #getCustomData(String)
*/
public String removeCustomData(String key) {
return mCustomParameters.remove(key);
}
/**
* Gets the current value for a key in your reports custom data field.
*
* @param key
* The key to be retrieved.
* @return The value for this key.
* @see #putCustomData(String, String)
* @see #removeCustomData(String)
*/
public String getCustomData(String key) {
return mCustomParameters.get(key);
}
/**
* Generates the string which is posted in the single custom data field in the GoogleDocs Form.
*
* @return A string with a 'key = value' pair on each line.
*/
private String createCustomInfoString() {
String CustomInfo = "";
Iterator<String> iterator = mCustomParameters.keySet().iterator();
while (iterator.hasNext()) {
String CurrentKey = iterator.next();
String CurrentVal = mCustomParameters.get(CurrentKey);
CustomInfo += CurrentKey + " = " + CurrentVal + "\n";
}
return CustomInfo;
}
/**
* Create or return the singleton instance.
*
* @return the current instance of ErrorReporter.
*/
public static ErrorReporter getInstance() {
if (mInstanceSingleton == null) {
mInstanceSingleton = new ErrorReporter();
}
return mInstanceSingleton;
}
/**
* <p>
* This is where the ErrorReporter replaces the default {@link UncaughtExceptionHandler}.
* </p>
*
* @param context
* The android application context.
*/
public void init(Context context) {
// If mDfltExceptionHandler is not null, initialization is already done.
// Don't do it twice to avoid losing the original handler.
if (mDfltExceptionHandler == null) {
mDfltExceptionHandler = Thread.getDefaultUncaughtExceptionHandler();
enabled = true;
Thread.setDefaultUncaughtExceptionHandler(this);
mContext = context;
// Store the initial Configuration state.
mInitialConfiguration = ConfigurationInspector.toString(mContext.getResources().getConfiguration());
}
}
/**
* Calculates the free memory of the device. This is based on an inspection of the filesystem, which in android
* devices is stored in RAM.
*
* @return Number of bytes available.
*/
private static long getAvailableInternalMemorySize() {
File path = Environment.getDataDirectory();
StatFs stat = new StatFs(path.getPath());
long blockSize = stat.getBlockSize();
long availableBlocks = stat.getAvailableBlocks();
return availableBlocks * blockSize;
}
/**
* Calculates the total memory of the device. This is based on an inspection of the filesystem, which in android
* devices is stored in RAM.
*
* @return Total number of bytes.
*/
private static long getTotalInternalMemorySize() {
File path = Environment.getDataDirectory();
StatFs stat = new StatFs(path.getPath());
long blockSize = stat.getBlockSize();
long totalBlocks = stat.getBlockCount();
return totalBlocks * blockSize;
}
/**
* Collects crash data.
*
* @param context
* The application context.
*/
private void retrieveCrashData(Context context) {
try {
ReportsCrashes config = ACRA.getConfig();
ReportField[] fields = config.customReportContent();
if (fields.length == 0) {
if (config.mailTo() == null || "".equals(config.mailTo())) {
fields = ACRA.DEFAULT_REPORT_FIELDS;
} else if (!"".equals(config.mailTo())) {
fields = ACRA.DEFAULT_MAIL_REPORT_FIELDS;
}
}
List<ReportField> fieldsList = Arrays.asList(fields);
SharedPreferences prefs = ACRA.getACRASharedPreferences();
// Generate report uuid
if (fieldsList.contains(REPORT_ID)) {
mCrashProperties.put(ReportField.REPORT_ID, UUID.randomUUID().toString());
}
// Collect meminfo
if (fieldsList.contains(DUMPSYS_MEMINFO)) {
mCrashProperties.put(DUMPSYS_MEMINFO, DumpSysCollector.collectMemInfo());
}
PackageManager pm = context.getPackageManager();
// Collect DropBox and logcat
if (pm != null) {
if (prefs.getBoolean(ACRA.PREF_ENABLE_SYSTEM_LOGS, true)
&& pm.checkPermission(permission.READ_LOGS, context.getPackageName()) == PackageManager.PERMISSION_GRANTED) {
Log.i(ACRA.LOG_TAG, "READ_LOGS granted! ACRA can include LogCat and DropBox data.");
if (fieldsList.contains(LOGCAT)) {
mCrashProperties.put(LOGCAT, LogCatCollector.collectLogCat(null).toString());
}
if (fieldsList.contains(EVENTSLOG)) {
mCrashProperties.put(EVENTSLOG, LogCatCollector.collectLogCat("events").toString());
}
if (fieldsList.contains(RADIOLOG)) {
mCrashProperties.put(RADIOLOG, LogCatCollector.collectLogCat("radio").toString());
}
if (fieldsList.contains(DROPBOX)) {
mCrashProperties.put(DROPBOX,
DropBoxCollector.read(mContext, ACRA.getConfig().additionalDropBoxTags()));
}
} else {
Log.i(ACRA.LOG_TAG, "READ_LOGS not allowed. ACRA will not include LogCat and DropBox data.");
}
// Retrieve UDID(IMEI) if permission is available
if (fieldsList.contains(DEVICE_ID)
&& prefs.getBoolean(ACRA.PREF_ENABLE_DEVICE_ID, true)
&& pm.checkPermission(permission.READ_PHONE_STATE, context.getPackageName()) == PackageManager.PERMISSION_GRANTED) {
TelephonyManager tm = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
String deviceId = tm.getDeviceId();
if (deviceId != null) {
mCrashProperties.put(DEVICE_ID, deviceId);
}
}
}
// Installation unique ID
if (fieldsList.contains(INSTALLATION_ID)) {
mCrashProperties.put(INSTALLATION_ID, Installation.id(mContext));
}
// Device Configuration when crashing
if (fieldsList.contains(INITIAL_CONFIGURATION)) {
mCrashProperties.put(INITIAL_CONFIGURATION, mInitialConfiguration);
}
if (fieldsList.contains(CRASH_CONFIGURATION)) {
Configuration crashConf = context.getResources().getConfiguration();
mCrashProperties.put(CRASH_CONFIGURATION, ConfigurationInspector.toString(crashConf));
}
PackageInfo pi;
pi = pm.getPackageInfo(context.getPackageName(), 0);
if (pi != null) {
// Application Version
if (fieldsList.contains(APP_VERSION_CODE)) {
mCrashProperties.put(APP_VERSION_CODE, Integer.toString(pi.versionCode));
}
if (fieldsList.contains(APP_VERSION_NAME)) {
mCrashProperties.put(APP_VERSION_NAME, pi.versionName != null ? pi.versionName : "not set");
}
} else {
// Could not retrieve package info...
mCrashProperties.put(APP_VERSION_NAME, "Package info unavailable");
}
// Application Package name
if (fieldsList.contains(PACKAGE_NAME)) {
mCrashProperties.put(PACKAGE_NAME, context.getPackageName());
}
// Android OS Build details
if (fieldsList.contains(BUILD)) {
mCrashProperties.put(BUILD, ReflectionCollector.collectConstants(android.os.Build.class));
}
// Device model
if (fieldsList.contains(PHONE_MODEL)) {
mCrashProperties.put(PHONE_MODEL, android.os.Build.MODEL);
}
// Android version
if (fieldsList.contains(ANDROID_VERSION)) {
mCrashProperties.put(ANDROID_VERSION, android.os.Build.VERSION.RELEASE);
}
// Device Brand (manufacturer)
if (fieldsList.contains(BRAND)) {
mCrashProperties.put(BRAND, android.os.Build.BRAND);
}
if (fieldsList.contains(PRODUCT)) {
mCrashProperties.put(PRODUCT, android.os.Build.PRODUCT);
}
// Device Memory
if (fieldsList.contains(TOTAL_MEM_SIZE)) {
mCrashProperties.put(TOTAL_MEM_SIZE, Long.toString(getTotalInternalMemorySize()));
}
if (fieldsList.contains(AVAILABLE_MEM_SIZE)) {
mCrashProperties.put(AVAILABLE_MEM_SIZE, Long.toString(getAvailableInternalMemorySize()));
}
// Application file path
if (fieldsList.contains(FILE_PATH)) {
mCrashProperties.put(FILE_PATH, context.getFilesDir().getAbsolutePath());
}
// Main display details
if (fieldsList.contains(DISPLAY)) {
Display display = ((WindowManager) context.getSystemService(Context.WINDOW_SERVICE))
.getDefaultDisplay();
mCrashProperties.put(DISPLAY, toString(display));
}
// User crash date with local timezone
if (fieldsList.contains(USER_CRASH_DATE)) {
Time curDate = new Time();
curDate.setToNow();
mCrashProperties.put(USER_CRASH_DATE, curDate.format3339(false));
}
// Add custom info, they are all stored in a single field
if (fieldsList.contains(CUSTOM_DATA)) {
mCrashProperties.put(CUSTOM_DATA, createCustomInfoString());
}
// Add user email address, if set in the app's preferences
if (fieldsList.contains(USER_EMAIL)) {
mCrashProperties.put(USER_EMAIL, prefs.getString(ACRA.PREF_USER_EMAIL_ADDRESS, "N/A"));
}
// Device features
if (fieldsList.contains(DEVICE_FEATURES)) {
mCrashProperties.put(DEVICE_FEATURES, DeviceFeaturesCollector.getFeatures(context));
}
// Environment (External storage state)
if (fieldsList.contains(ENVIRONMENT)) {
mCrashProperties.put(ENVIRONMENT, ReflectionCollector.collectStaticGettersResults(Environment.class));
}
// System settings
if (fieldsList.contains(SETTINGS_SYSTEM)) {
mCrashProperties.put(SETTINGS_SYSTEM, SettingsCollector.collectSystemSettings(mContext));
}
// Secure settings
if (fieldsList.contains(SETTINGS_SECURE)) {
mCrashProperties.put(SETTINGS_SECURE, SettingsCollector.collectSecureSettings(mContext));
}
// SharedPreferences
if (fieldsList.contains(SHARED_PREFERENCES)) {
mCrashProperties.put(SHARED_PREFERENCES, SharedPreferencesCollector.collect(mContext));
}
} catch (Exception e) {
Log.e(LOG_TAG, "Error while retrieving crash data", e);
}
}
/**
* Returns a String representation of the content of a {@link Display} object. It might be interesting in a future
* release to replace this with a reflection-based collector like {@link ConfigurationInspector}.
*
* @param display
* A Display instance to be inspected.
* @return A String representation of the content of the given {@link Display} object.
*/
private static String toString(Display display) {
DisplayMetrics metrics = new DisplayMetrics();
display.getMetrics(metrics);
StringBuilder result = new StringBuilder();
result.append("width=").append(display.getWidth()).append('\n').append("height=").append(display.getHeight())
.append('\n').append("pixelFormat=").append(display.getPixelFormat()).append('\n')
.append("refreshRate=").append(display.getRefreshRate()).append("fps").append('\n')
.append("metrics.density=x").append(metrics.density).append('\n').append("metrics.scaledDensity=x")
.append(metrics.scaledDensity).append('\n').append("metrics.widthPixels=").append(metrics.widthPixels)
.append('\n').append("metrics.heightPixels=").append(metrics.heightPixels).append('\n')
.append("metrics.xdpi=").append(metrics.xdpi).append('\n').append("metrics.ydpi=").append(metrics.ydpi);
return result.toString();
}
/*
* (non-Javadoc)
*
* @see java.lang.Thread.UncaughtExceptionHandler#uncaughtException(java.lang .Thread, java.lang.Throwable)
*/
public void uncaughtException(Thread t, Throwable e) {
Log.e(ACRA.LOG_TAG,
"ACRA caught a " + e.getClass().getSimpleName() + " exception for " + mContext.getPackageName()
+ ". Building report.");
// This is a real exception, clear the IS_SILENT field from any previous silent exception
mCrashProperties.remove(IS_SILENT);
// Generate and send crash report
ReportsSenderWorker worker = handleException(e);
if (mReportingInteractionMode == ReportingInteractionMode.TOAST) {
try {
// Wait a bit to let the user read the toast
Thread.sleep(4000);
} catch (InterruptedException e1) {
Log.e(LOG_TAG, "Error : ", e1);
}
}
if (worker != null) {
while (worker.isAlive()) {
try {
// Wait for the report sender to finish it's task before
// killing the process
Thread.sleep(100);
} catch (InterruptedException e1) {
Log.e(LOG_TAG, "Error : ", e1);
}
}
}
if (mReportingInteractionMode == ReportingInteractionMode.SILENT
|| (mReportingInteractionMode == ReportingInteractionMode.TOAST && ACRA.getConfig()
.forceCloseDialogAfterToast())) {
// If using silent mode, let the system default handler do it's job
// and display the force close dialog.
mDfltExceptionHandler.uncaughtException(t, e);
} else {
// If ACRA handles user notifications with a Toast or a Notification
// the Force Close dialog is one more notification to the user...
// We choose to close the process ourselves using the same actions.
CharSequence appName = "Application";
try {
PackageManager pm = mContext.getPackageManager();
appName = pm.getApplicationInfo(mContext.getPackageName(), 0).loadLabel(mContext.getPackageManager());
Log.e(LOG_TAG, appName + " fatal error : " + e.getMessage(), e);
} catch (NameNotFoundException e2) {
Log.e(LOG_TAG, "Error : ", e2);
} finally {
android.os.Process.killProcess(android.os.Process.myPid());
System.exit(10);
}
}
}
/**
* Try to send a report, if an error occurs stores a report file for a later attempt. You can set the
* {@link ReportingInteractionMode} for this specific report. Use {@link #handleException(Throwable)} to use the
* Application default interaction mode.
*
* @param e
* The Throwable to be reported. If null the report will contain a new
* Exception("Report requested by developer").
* @param reportingInteractionMode
* The desired interaction mode.
*/
ReportsSenderWorker handleException(Throwable e, ReportingInteractionMode reportingInteractionMode) {
boolean sendOnlySilentReports = false;
if (reportingInteractionMode == null) {
// No interaction mode defined, we assume it has been set during
// ACRA.initACRA()
reportingInteractionMode = mReportingInteractionMode;
} else {
// 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 (reportingInteractionMode == ReportingInteractionMode.SILENT
&& mReportingInteractionMode != ReportingInteractionMode.SILENT) {
sendOnlySilentReports = true;
}
}
if (e == null) {
e = new Exception("Report requested by developer");
}
if (reportingInteractionMode == ReportingInteractionMode.TOAST
|| (reportingInteractionMode == ReportingInteractionMode.NOTIFICATION && ACRA.getConfig()
.resToastText() != 0)) {
new Thread() {
/*
* (non-Javadoc)
*
* @see java.lang.Thread#run()
*/
@Override
public void run() {
Looper.prepare();
Toast.makeText(mContext, ACRA.getConfig().resToastText(), Toast.LENGTH_LONG).show();
Looper.loop();
}
}.start();
}
retrieveCrashData(mContext);
// Build stack trace
final Writer result = new StringWriter();
final PrintWriter printWriter = new PrintWriter(result);
e.printStackTrace(printWriter);
Log.getStackTraceString(e);
// If the exception was thrown in a background thread inside
// AsyncTask, then the actual exception can be found with getCause
Throwable cause = e.getCause();
while (cause != null) {
cause.printStackTrace(printWriter);
cause = cause.getCause();
}
mCrashProperties.put(STACK_TRACE, result.toString());
printWriter.close();
// Always write the report file
String reportFileName = saveCrashReportFile(null, null);
// Remove IS_SILENT if it was set, or it will persist in the next non-silent report
mCrashProperties.remove(IS_SILENT);
mCrashProperties.remove(USER_COMMENT);
if (reportingInteractionMode == ReportingInteractionMode.SILENT
|| reportingInteractionMode == ReportingInteractionMode.TOAST
|| ACRA.getACRASharedPreferences().getBoolean(ACRA.PREF_ALWAYS_ACCEPT, false)) {
// Send reports now
approvePendingReports();
ReportsSenderWorker wk = new ReportsSenderWorker(sendOnlySilentReports);
Log.v(ACRA.LOG_TAG, "About to start ReportSenderWorker from #handleException");
wk.start();
return wk;
} else if (reportingInteractionMode == ReportingInteractionMode.NOTIFICATION) {
// Send reports when user accepts
notifySendReport(reportFileName);
}
return null;
}
/**
* Send a report for this {@link Throwable} with the reporting interaction mode set on the Application level by the
* developer.
*
* @param e
* The {@link Throwable} to be reported. If null the report will contain a new
* Exception("Report requested by developer").
*/
public ReportsSenderWorker handleException(Throwable e) {
return handleException(e, mReportingInteractionMode);
}
/**
* Send a report for this {@link Throwable} silently (forces the use of {@link ReportingInteractionMode#SILENT} for
* this report, whatever is the mode set for the application. Very useful for tracking difficult defects.
*
* @param e
* The {@link Throwable} to be reported. If null the report will contain a new
* Exception("Report requested by developer").
* @return The Thread which has been created to send the report or null if ACRA is disabled.
*/
public Thread handleSilentException(Throwable e) {
// Mark this report as silent.
if (enabled) {
mCrashProperties.put(IS_SILENT, "true");
Thread result = handleException(e, ReportingInteractionMode.SILENT);
return result;
} else {
Log.d(LOG_TAG, "ACRA is disabled. Silent report not sent.");
return null;
}
}
/**
* Send a status bar notification. The action triggered when the notification is selected is to start the
* {@link CrashReportDialog} Activity.
*/
void notifySendReport(String reportFileName) {
// This notification can't be set to AUTO_CANCEL because after a crash,
// clicking on it restarts the application and this triggers a check
// for pending reports which issues the notification back.
// Notification cancellation is done in the dialog activity displayed
// on notification click.
NotificationManager notificationManager = (NotificationManager) mContext
.getSystemService(Context.NOTIFICATION_SERVICE);
ReportsCrashes conf = ACRA.getConfig();
// Default notification icon is the warning symbol
int icon = conf.resNotifIcon();
CharSequence tickerText = mContext.getText(conf.resNotifTickerText());
long when = System.currentTimeMillis();
Notification notification = new Notification(icon, tickerText, when);
CharSequence contentTitle = mContext.getText(conf.resNotifTitle());
CharSequence contentText = mContext.getText(conf.resNotifText());
Intent notificationIntent = new Intent(mContext, CrashReportDialog.class);
Log.d(LOG_TAG, "Creating Notification for " + reportFileName);
notificationIntent.putExtra(EXTRA_REPORT_FILE_NAME, reportFileName);
PendingIntent contentIntent = PendingIntent.getActivity(mContext, 0, notificationIntent,
PendingIntent.FLAG_UPDATE_CURRENT);
notification.setLatestEventInfo(mContext, contentTitle, contentText, contentIntent);
// Send new notification
notificationManager.cancelAll();
notificationManager.notify(ACRA.NOTIF_CRASH_ID, notification);
}
/**
* Sends the report with all configured ReportSenders. If at least one sender completed its job, the report is
* considered as sent and will not be sent again for failing senders.
*
* @param context
* The application context.
* @param errorContent
* Crash data.
* @throws ReportSenderException
* If unable to send the crash report.
*/
private static void sendCrashReport(Context context, CrashReportData errorContent) throws ReportSenderException {
boolean sentAtLeastOnce = false;
for (ReportSender sender : mReportSenders) {
try {
sender.send(errorContent);
// If at least one sender worked, don't re-send the report
// later.
sentAtLeastOnce = true;
} catch (ReportSenderException e) {
if (!sentAtLeastOnce) {
throw e; // Don't log here because we aren't dealing with the Exception here.
} else {
Log.w(LOG_TAG, "ReportSender of class " + sender.getClass().getName()
+ " failed but other senders completed their task. ACRA will not send this report again.");
}
}
}
}
/**
* When a report can't be sent, it is saved here in a file in the root of the application private directory.
*
* @param fileName
* 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 static String saveCrashReportFile(String fileName, CrashReportData crashData) {
try {
Log.d(LOG_TAG, "Writing crash report file.");
if (crashData == null) {
crashData = mCrashProperties;
}
if (fileName == null) {
Time now = new Time();
now.setToNow();
long timestamp = now.toMillis(false);
String isSilent = crashData.getProperty(IS_SILENT);
fileName = "" + timestamp + (isSilent != null ? SILENT_SUFFIX : "") + REPORTFILE_EXTENSION;
}
final FileOutputStream reportFile = mContext.openFileOutput(fileName, Context.MODE_PRIVATE);
try {
crashData.store(reportFile, "");
} finally {
reportFile.close();
}
return fileName;
} catch (Exception e) {
Log.e(LOG_TAG, "An error occured while writing the report file...", e);
}
return null;
}
/**
* Returns an array containing the names of pending crash report files.
*
* @return an array containing the names of pending crash report files.
*/
String[] getCrashReportFilesList() {
if (mContext == null) {
Log.e(LOG_TAG, "Trying to get ACRA reports but ACRA is not initialized.");
return new String[0];
}
File dir = mContext.getFilesDir();
if (dir != null) {
Log.d(LOG_TAG, "Looking for error files in " + dir.getAbsolutePath());
// Filter for ".stacktrace" files
FilenameFilter filter = new FilenameFilter() {
public boolean accept(File dir, String name) {
return name.endsWith(REPORTFILE_EXTENSION);
}
};
final String[] result = dir.list(filter);
return (result == null) ? new String[0] : result;
} else {
Log.w(LOG_TAG,
"Application files directory does not exist! The application may not be installed correctly. Please try reinstalling.");
return new String[0];
}
}
/**
* Send pending reports.
*
* @param context
* The application context.
* @param sendOnlySilentReports
* Send only reports explicitly declared as SILENT by the developer (sent via
* {@link #handleSilentException(Throwable)}.
*/
synchronized void checkAndSendReports(Context context, boolean sendOnlySilentReports) {
Log.d(LOG_TAG, "#checkAndSendReports - start");
final String[] reportFiles = getCrashReportFilesList();
Arrays.sort(reportFiles);
int reportsSentCount = 0;
for (String curFileName : reportFiles) {
if (sendOnlySilentReports && !isSilent(curFileName)) {
continue;
}
if (reportsSentCount >= MAX_SEND_REPORTS) {
break; // send only a few reports to avoid overloading the network
}
Log.i(LOG_TAG, "Sending file " + curFileName);
try {
final CrashReportData previousCrashReport = loadCrashReport(context, curFileName);
sendCrashReport(context, previousCrashReport);
deleteFile(context, curFileName);
} catch (RuntimeException e) {
Log.e(ACRA.LOG_TAG, "Failed to send crash reports", e);
deleteFile(context, curFileName);
break; // Something really unexpected happened. Don't try to send any more reports now.
} catch (IOException e) {
Log.e(ACRA.LOG_TAG, "Failed to load crash report for " + curFileName, e);
deleteFile(context, curFileName);
break; // Something unexpected happened when reading the crash report. Don't try to send any more reports now.
} catch (ReportSenderException e) {
Log.e(ACRA.LOG_TAG, "Failed to send crash report for " + curFileName, e);
break; // Something stopped the report being sent. Don't try to send any more reports now.
}
reportsSentCount++;
}
Log.d(LOG_TAG, "#checkAndSendReports - finish");
}
private CrashReportData loadCrashReport(Context context, String fileName) throws IOException {
final CrashReportData crashReport = new CrashReportData();
final FileInputStream input = context.openFileInput(fileName);
try {
//crashReport.clear();
crashReport.load(input);
} finally {
input.close();
}
return crashReport;
}
private void deleteFile(Context context, String fileName) {
final boolean deleted = context.deleteFile(fileName);
if (!deleted) {
Log.w(ACRA.LOG_TAG, "Could not deleted error report : " + fileName);
}
}
/**
* Set the wanted user interaction mode for sending reports.
*
* @param reportingInteractionMode
*/
void setReportingInteractionMode(ReportingInteractionMode reportingInteractionMode) {
mReportingInteractionMode = reportingInteractionMode;
}
/**
* This method looks for pending reports and does the action required depending on the interaction mode set.
*/
public void checkReportsOnApplicationStart() {
String[] filesList = getCrashReportFilesList();
if (filesList != null && filesList.length > 0) {
boolean onlySilentOrApprovedReports = containsOnlySilentOrApprovedReports(filesList);
// Immediately send reports for SILENT and TOAST modes.
// Immediately send reports in NOTIFICATION mode only if they are
// all silent or approved.
if (mReportingInteractionMode == ReportingInteractionMode.SILENT
|| mReportingInteractionMode == ReportingInteractionMode.TOAST
|| (mReportingInteractionMode == ReportingInteractionMode.NOTIFICATION && onlySilentOrApprovedReports)) {
if (mReportingInteractionMode == ReportingInteractionMode.TOAST && !onlySilentOrApprovedReports) {
// Display the Toast in TOAST mode only if there are
// non-silent reports.
Toast.makeText(mContext, ACRA.getConfig().resToastText(), Toast.LENGTH_LONG).show();
}
Log.v(ACRA.LOG_TAG, "About to start ReportSenderWorker from #checkReportOnApplicationStart");
new ReportsSenderWorker().start();
} else if (ACRA.getConfig().deleteUnapprovedReportsOnApplicationStart()) {
// NOTIFICATION mode, and there are unapproved reports to send
// (latest notification has been ignored: neither accepted nor
// refused). The application developer has decided that these
// reports should not be renotified ==> destroy them.
ErrorReporter.getInstance().deletePendingNonApprovedReports();
} else {
// NOTIFICATION mode, and there are unapproved reports to send
// (latest notification has been ignored: neither accepted nor
// refused).
// Display the notification.
// The user comment will be associated to the latest report
ErrorReporter.getInstance().notifySendReport(getLatestNonSilentReport(filesList));
}
}
}
/**
* Retrieve the most recently created "non silent" report from an array of report file names. A non silent is any
* report which has not been created with {@link #handleSilentException(Throwable)}.
*
* @param filesList
* An array of report file names.
* @return The most recently created "non silent" report file name.
*/
private String getLatestNonSilentReport(String[] filesList) {
if (filesList != null && filesList.length > 0) {
for (int i = filesList.length - 1; i >= 0; i--) {
if (!isSilent(filesList[i])) {
return filesList[i];
}
}
// We should never have this result, but this should be secure...
return filesList[filesList.length - 1];
} else {
return null;
}
}
/**
* Delete all report files stored.
*/
public void deletePendingReports() {
deletePendingReports(true, true, 0);
}
/**
* Delete all pending SILENT reports. These are the reports created with {@link #handleSilentException(Throwable)}.
*/
public void deletePendingSilentReports() {
deletePendingReports(true, false, 0);
}
/**
* Delete all pending non approved reports.
*/
public void deletePendingNonApprovedReports() {
// In NOTIFICATION mode, we have to keep the latest report which could
// be needed for an existing not yet discarded notification.
int nbReportsToKeep = mReportingInteractionMode == ReportingInteractionMode.NOTIFICATION ? 1 : 0;
deletePendingReports(false, true, nbReportsToKeep);
}
/**
* Delete pending reports.
*
* @param deleteApprovedReports
* Set to true to delete approved and silent reports.
* @param deleteNonApprovedReports
* Set to true to delete non approved/silent reports.
*/
private void deletePendingReports(boolean deleteApprovedReports, boolean deleteNonApprovedReports,
int nbOfLatestToKeep) {
String[] filesList = getCrashReportFilesList();
Arrays.sort(filesList);
if (filesList != null) {
boolean isReportApproved = false;
String fileName;
for (int iFile = 0; iFile < filesList.length - nbOfLatestToKeep; iFile++) {
fileName = filesList[iFile];
isReportApproved = isApproved(fileName);
if ((isReportApproved && deleteApprovedReports) || (!isReportApproved && deleteNonApprovedReports)) {
new File(mContext.getFilesDir(), fileName).delete();
}
}
}
}
/**
* Disable ACRA : sets this Thread's {@link UncaughtExceptionHandler} back to the system default.
*/
public void disable() {
if (mContext != null) {
Log.d(ACRA.LOG_TAG, "ACRA is disabled for " + mContext.getPackageName());
} else {
Log.d(ACRA.LOG_TAG, "ACRA is disabled.");
}
if (mDfltExceptionHandler != null) {
Thread.setDefaultUncaughtExceptionHandler(mDfltExceptionHandler);
enabled = false;
}
}
/**
* Checks if an array of reports files names contains only silent or approved reports.
*
* @param reportFileNames
* the list of reports (as provided by {@link #getCrashReportFilesList()})
* @return True if there are only silent or approved reports. False if there is at least one non-approved report.
*/
private boolean containsOnlySilentOrApprovedReports(String[] reportFileNames) {
for (String reportFileName : reportFileNames) {
if (!isApproved(reportFileName)) {
return false;
}
}
return true;
}
/**
* Guess that a report is silent from its file name.
*
* @param reportFileName
* @return True if the report has been declared explicitly silent using {@link #handleSilentException(Throwable)}.
*/
private boolean isSilent(String reportFileName) {
return reportFileName.contains(SILENT_SUFFIX);
}
/**
* <p>
* Returns true if the report is considered as approved. This includes:
* </p>
* <ul>
* <li>Reports which were pending when the user agreed to send a report in the NOTIFICATION mode Dialog.</li>
* <li>Explicit silent reports</li>
* </ul>
*
* @param reportFileName
* @return True if a report can be sent.
*/
private boolean isApproved(String reportFileName) {
return isSilent(reportFileName) || reportFileName.contains(APPROVED_SUFFIX);
}
/**
* Sets the user comment value in an existing report file. User comments are ALWAYS entered by the user in a Dialog
* which is displayed after application restart. This means that the report file has already been generated and
* saved to the filesystem. Associating the comment to the report requires to reopen an existing report, insert the
* comment value and save the report back.
*
* @param context
* The application context.
* @param commentedReportFileName
* The file name of the report which should receive the comment.
* @param userComment
* The comment entered by the user.
* @param userEmail
*/
private static void addUserDataToReport(Context context, String commentedReportFileName, String userComment,
String userEmail) {
Log.d(LOG_TAG, "Add user comment to " + commentedReportFileName);
if (commentedReportFileName != null && userComment != null) {
try {
final FileInputStream input = context.openFileInput(commentedReportFileName);
final CrashReportData commentedCrashReport = new CrashReportData();
try {
Log.d(LOG_TAG, "Loading Properties report to insert user comment.");
commentedCrashReport.load(input);
} finally {
input.close();
}
commentedCrashReport.put(USER_COMMENT, userComment);
commentedCrashReport.put(USER_EMAIL, userEmail == null ? "" : userEmail);
saveCrashReportFile(commentedReportFileName, commentedCrashReport);
} catch (FileNotFoundException e) {
Log.w(LOG_TAG, "User comment not added: ", e);
} catch (InvalidPropertiesFormatException e) {
Log.w(LOG_TAG, "User comment not added: ", e);
} catch (IOException e) {
Log.w(LOG_TAG, "User comment not added: ", e);
}
}
}
/**
* Add a {@link ReportSender} to the list of active {@link ReportSender}s.
*
* @param sender
* The {@link ReportSender} to be added.
*/
public void addReportSender(ReportSender sender) {
mReportSenders.add(sender);
}
/**
* Remove a specific instance of {@link ReportSender} from the list of active {@link ReportSender}s.
*
* @param sender
* The {@link ReportSender} instance to be removed.
*/
public void removeReportSender(ReportSender sender) {
mReportSenders.remove(sender);
}
/**
* Remove all {@link ReportSender} instances from a specific class.
*
* @param senderClass
*/
public void removeReportSenders(Class<?> senderClass) {
if (ReportSender.class.isAssignableFrom(senderClass)) {
for (ReportSender sender : mReportSenders) {
if (senderClass.isInstance(sender)) {
mReportSenders.remove(sender);
}
}
}
}
/**
* Clears the list of active {@link ReportSender}s. You should then call {@link #addReportSender(ReportSender)} or
* ACRA will not send any report anymore.
*/
public void removeAllReportSenders() {
mReportSenders.clear();
}
/**
* Removes all previously set {@link ReportSender}s and set the given one as the new {@link ReportSender}.
*
* @param sender
*/
public void setReportSender(ReportSender sender) {
removeAllReportSenders();
addReportSender(sender);
}
/**
* Sets the application start date. This will be included in the reports, will be helpfull compared to user_crash
* date.
*
* @param appStartDate
*/
public void setAppStartDate(Time appStartDate) {
mCrashProperties.put(ReportField.USER_APP_START_DATE, appStartDate.format3339(false));
}
}