package com.apigee.sdk.apm.android.crashlogging;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.lang.Thread.UncaughtExceptionHandler;
import java.util.UUID;
import android.content.Context;
import android.util.Log;
import com.apigee.sdk.AppIdentification;
import com.apigee.sdk.apm.android.AndroidLog;
import com.apigee.sdk.apm.android.ApigeeMonitoringClient;
import com.apigee.sdk.apm.android.crashlogging.internal.ExceptionHandler;
import com.apigee.sdk.apm.android.model.ClientLog;
import com.apigee.sdk.apm.android.util.StringUtils;
/**
* <h4>Description</h4>
*
* The crash manager sets an exception handler to catch all unhandled
* exceptions. The handler writes the stack trace and additional meta data to
* a file. If it finds one or more of these files at the next start, it shows
* an alert dialog to ask the user if he want the send the crash data to
* HockeyApp.
*
* <h4>License</h4>
*
* <pre>
* Copyright (c) 2012 Codenauts UG
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
* </pre>
*
* @author Thomas Dohmke
* @y.exclude
**/
public class CrashManager {
public static String CRASH_LOG_TAG = "CRASH";
protected static String CRASH_LOG_KEY_STRING_FORMAT = "%s/crashlog/%s/%s";
public static AppIdentification appIdentification = null;
public static String appUniqueIdentifier = null;
protected static AndroidLog logger;
/**
* Registers new crash manager and handles existing crash logs.
*
* @param context The context to use. Usually your Activity object.
* @param appIdentifier App ID of your app on HockeyApp.
*/
public static void register(Context context, AppIdentification appIdentification, ApigeeMonitoringClient monitoringClient) {
register(context, appIdentification, null, monitoringClient);
}
/**
* Registers new crash manager and handles existing crash logs.
*
* @param context The context to use. Usually your Activity object.
* @param appIdentifier App ID of your app on HockeyApp.
* @param listener Implement for callback functions.
*/
public static void register(Context context, AppIdentification appIdentification, CrashManagerListener listener, ApigeeMonitoringClient monitoringClient) {
initialize(context, appIdentification, listener, false);
execute(context, listener, monitoringClient);
}
/**
* Initializes the crash manager, but does not handle crash log. Use this
* method only if you want to split the process into two parts, i.e. when
* your app has multiple entry points. You need to call the method 'execute'
* at some point after this method.
*
* @param context The context to use. Usually your Activity object.
* @param appIdentifier App ID of your app on HockeyApp.
* @param listener Implement for callback functions.
*/
public static void initialize(Context context, AppIdentification appIdentification, CrashManagerListener listener) {
initialize(context, appIdentification, listener, true);
}
/**
* Executes the crash manager. You need to call this method if you have used
* the method 'initialize' before.
*
* @param context The context to use. Usually your Activity object.
* @param listener Implement for callback functions.
*/
public static void execute(Context context, CrashManagerListener listener, ApigeeMonitoringClient monitoringClient) {
Boolean ignoreDefaultHandler = (listener != null) && (listener.ignoreDefaultHandler());
if( hasStackTraces() ) {
if (listener != null) {
listener.onCrashesFound();
}
sendCrashes(context, listener, ignoreDefaultHandler, monitoringClient);
} else {
registerHandler(context, listener, ignoreDefaultHandler);
}
}
protected static String getCrashFilesDirectory() {
return Constants.FILES_PATH + "/";
}
/**
* Checks if there are any saved stack traces in the files dir.
*
* @param context The context to use. Usually your Activity object.
* @return 0 if there are no stack traces,
* 1 if there are any new stack traces,
* 2 if there are confirmed stack traces
*/
public static boolean hasStackTraces() {
String[] filenames = searchForStackTraces();
if( (filenames != null) && (filenames.length > 0) ) {
return true;
}
return false;
}
/**
* Submits all stack traces in the files dir to server.
*
* @param context The context to use. Usually your Activity object.
* @param listener Implement for callback functions.
*/
public static void submitStackTraces(Context context, CrashManagerListener listener, ApigeeMonitoringClient monitoringClient) {
Log.d(ClientLog.TAG_MONITORING_CLIENT, "Looking for exceptions in: " + Constants.FILES_PATH);
String[] list = searchForStackTraces();
Boolean successful = false;
if ((list != null) && (list.length > 0)) {
Log.d(ClientLog.TAG_MONITORING_CLIENT, "Found " + list.length + " stacktrace(s).");
if (!monitoringClient.isAbleToSendDataToServer()) {
Log.w(ClientLog.TAG_MONITORING_CLIENT,"Unable to send stack trace(s) to server, missing server configuration");
return;
}
for (int index = 0; index < list.length; index++) {
try {
// Read contents of stack trace
String filename = list[index];
Log.v(ClientLog.TAG_MONITORING_CLIENT, "crash file found: '" + filename + "'");
String stacktrace = contentsOfFile(context, filename);
if ( (stacktrace != null) && (stacktrace.length() > 0) ) {
submitStackTrace(context, filename, monitoringClient);
successful = true;
}
}
catch (Exception e) {
e.printStackTrace();
}
finally {
if (successful) {
deleteStackTrace(context, list[index]);
if (listener != null) {
listener.onCrashesSent();
}
}
else {
if (listener != null) {
listener.onCrashesNotSent();
}
}
}
}
}
}
/**
* Deletes all stack traces and meta files from files dir.
*
* @param context The context to use. Usually your Activity object.
*/
public static void deleteStackTraces(Context context) {
Log.d(ClientLog.TAG_MONITORING_CLIENT, "Looking for exceptions in: " + Constants.FILES_PATH);
String[] list = searchForStackTraces();
if ((list != null) && (list.length > 0)) {
Log.d(ClientLog.TAG_MONITORING_CLIENT, "Found " + list.length + " stacktrace(s).");
for (int index = 0; index < list.length; index++) {
String fileName = list[index];
try {
Log.d(ClientLog.TAG_MONITORING_CLIENT, "Delete stacktrace " + fileName + ".");
deleteStackTrace(context, list[index]);
context.deleteFile(list[index]);
}
catch (Exception e) {
e.printStackTrace();
}
}
}
}
/**
* Private method to initialize the crash manager. This method has an
* additional parameter to decide whether to register the exception handler
* at the end or not.
*/
private static void initialize(Context context, AppIdentification appIdentification, CrashManagerListener listener, boolean registerHandler) {
CrashManager.appIdentification = appIdentification;
Constants.loadFromContext(context);
if (CrashManager.appIdentification == null) {
CrashManager.appUniqueIdentifier = Constants.APP_PACKAGE;
}
if (registerHandler) {
Boolean ignoreDefaultHandler = (listener != null) && (listener.ignoreDefaultHandler());
registerHandler(context, listener, ignoreDefaultHandler);
}
}
/**
* Starts thread to send crashes to HockeyApp, then registers the exception
* handler.
*/
private static void sendCrashes(final Context context, final CrashManagerListener listener, final boolean ignoreDefaultHandler, final ApigeeMonitoringClient monitoringClient) {
new Thread() {
@Override
public void run() {
submitStackTraces(context, listener, monitoringClient);
registerHandler(context, listener, ignoreDefaultHandler);
}
}.start();
}
/**
* Registers the exception handler.
*/
private static void registerHandler(Context context, CrashManagerListener listener, boolean ignoreDefaultHandler) {
if ((Constants.APP_VERSION != null) && (Constants.APP_PACKAGE != null)) {
// Get current handler
UncaughtExceptionHandler currentHandler = Thread.getDefaultUncaughtExceptionHandler();
if (currentHandler != null) {
Log.w(ClientLog.TAG_MONITORING_CLIENT, "Multiple crash reporters detected");
Log.d(ClientLog.TAG_MONITORING_CLIENT, "Current handler class = " + currentHandler.getClass().getName());
// Register if not already registered
if (!(currentHandler instanceof ExceptionHandler)) {
Log.w(ClientLog.TAG_MONITORING_CLIENT, "Replacing existing crash reporter");
Thread.setDefaultUncaughtExceptionHandler(new ExceptionHandler(currentHandler, listener, ignoreDefaultHandler));
}
}
else {
Thread.setDefaultUncaughtExceptionHandler(new ExceptionHandler(currentHandler, listener, ignoreDefaultHandler));
}
}
else {
Log.d(ClientLog.TAG_MONITORING_CLIENT, "Exception handler not set because version or package is null.");
}
}
/**
* Deletes the give filename and all corresponding files (same name,
* different extension).
*/
protected static void deleteStackTrace(Context context, String filename) {
context.deleteFile(filename);
String user = filename.replace(".stacktrace", ".user");
context.deleteFile(user);
String contact = filename.replace(".stacktrace", ".contact");
context.deleteFile(contact);
String description = filename.replace(".stacktrace", ".description");
context.deleteFile(description);
}
/**
* Returns the content of a file as a string.
*/
protected static String contentsOfFile(Context context, String filename) {
StringBuilder contents = new StringBuilder();
BufferedReader reader = null;
try {
reader = new BufferedReader(new InputStreamReader(context.openFileInput(filename)));
String line = null;
String lineSeparator = System.getProperty("line.separator");
while ((line = reader.readLine()) != null) {
contents.append(line);
contents.append(lineSeparator);
}
}
catch (FileNotFoundException e) {
}
catch (IOException e) {
e.printStackTrace();
}
finally {
if (reader != null) {
try {
reader.close();
}
catch (IOException ignored) {
}
}
}
return contents.toString();
}
/**
* Searches .stacktrace files and returns then as array.
*/
protected static String[] searchForStackTraces() {
// Try to create the files folder if it doesn't exist
File dir = new File(getCrashFilesDirectory());
dir.mkdir();
// Filter for ".stacktrace" files
FilenameFilter filter = new FilenameFilter() {
public boolean accept(File dir, String name) {
return name.endsWith(".stacktrace");
}
};
return dir.list(filter);
}
protected static void submitStackTrace(Context context, String fileNameOnDevice, ApigeeMonitoringClient monitoringClient) throws IOException
{
UUID uuid = UUID.randomUUID();
String uuidAsString = uuid.toString();
String fileNameForServer = uuidAsString + ".stacktrace";
if(logger != null)
{
logger.wtf(CRASH_LOG_TAG, fileNameForServer);
}
String crashFilePath = getCrashFilesDirectory() + fileNameOnDevice;
String crashFileContents = StringUtils.fileToString(crashFilePath);
if( (crashFileContents != null) && (crashFileContents.length() > 0) ) {
String postURL = monitoringClient.getCrashReportUploadURL(fileNameForServer);
monitoringClient.onCrashReportUpload(crashFileContents);
if( monitoringClient.putString(crashFileContents, postURL, "text/plain") != null ) {
Log.i(ClientLog.TAG_MONITORING_CLIENT,"Sent crash file to server '" + fileNameForServer + "'");
} else {
Log.e(ClientLog.TAG_MONITORING_CLIENT,"There was an error with the request to upload the crash report");
}
} else {
// can't read crash file
Log.e(ClientLog.TAG_MONITORING_CLIENT,"Error: unable to read crash file on device '" + fileNameOnDevice + "'");
}
}
//Apigee specific logger
public static void register(Context context, AndroidLog log, AppIdentification appIdentification, ApigeeMonitoringClient monitoringClient) {
logger = log;
register(context, appIdentification, new CrashManagerListener() {
@Override
public Boolean onCrashesFound() {
logger.wtf(ClientLog.TAG_MONITORING_CLIENT, "1 or more crashes occurred");
return true; // auto-send (don't ask the user)
}
@Override
public void onCrashesSent() {
logger.i(ClientLog.TAG_MONITORING_CLIENT, "Sent Crashlogs to Server");
super.onCrashesSent();
}
@Override
public void onCrashesNotSent() {
logger.w(ClientLog.TAG_MONITORING_CLIENT, "Unable to send crashlogs to server");
super.onCrashesNotSent();
}
}, monitoringClient);
}
}