Android Open Source - HockeySDK-Android Crash Manager






From Project

Back to project page HockeySDK-Android.

License

The source code is released under:

Apache License

If you think the Android project HockeySDK-Android listed in this page is inappropriate, such as containing malicious code/tools or violating the copyright, please email info at java2s dot com, thanks.

Java Source Code

package net.hockeyapp.android;
/* w ww .j a v  a  2  s  . c o  m*/
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.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;


import android.preference.PreferenceManager;
import net.hockeyapp.android.utils.ConnectionManager;
import net.hockeyapp.android.utils.PrefsUtil;

import net.hockeyapp.android.utils.Util;
import org.apache.http.NameValuePair;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.protocol.HTTP;

import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.util.Log;

/**
 * <h3>Description</h3>
 * 
 * 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. 
 * 
 * <h3>License</h3>
 * 
 * <pre>
 * Copyright (c) 2011-2014 Bit Stadium GmbH
 * 
 * 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
 **/
public class CrashManager {
  /**
   * App identifier from HockeyApp.
   */
  private static String identifier = null;
  
  /**
   * URL of HockeyApp service.
   */
  private static String urlString = null;

  /**
   * Stack traces are currently submitted
   */
  private static boolean submitting = false;

  /**
   * Shared preferences key for always send dialog button.
   */
  private static final String ALWAYS_SEND_KEY = "always_send_crash_reports";

  /**
   * 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, String appIdentifier) {
    register(context, Constants.BASE_URL, appIdentifier, null);
  }

  /**
   * 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, String appIdentifier, CrashManagerListener listener) {
    register(context, Constants.BASE_URL, appIdentifier, listener);
  }

  /**
   * Registers new crash manager and handles existing crash logs.
   * 
   * @param context The context to use. Usually your Activity object.
   * @param urlString URL of the HockeyApp server.
   * @param appIdentifier App ID of your app on HockeyApp.
   * @param listener Implement for callback functions.
   */
  public static void register(Context context, String urlString, String appIdentifier, CrashManagerListener listener) {
    initialize(context, urlString, appIdentifier, listener, false);
    execute(context, listener);
  }

  /**
   * 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, String appIdentifier, CrashManagerListener listener) {
    initialize(context, Constants.BASE_URL, appIdentifier, listener, true);
  }

  /**
   * 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 urlString URL of the HockeyApp server.
   * @param appIdentifier App ID of your app on HockeyApp.
   * @param listener Implement for callback functions.
   */
  public static void initialize(Context context, String urlString, String appIdentifier, CrashManagerListener listener) {
    initialize(context, urlString, appIdentifier, 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.
   */
  @SuppressWarnings("deprecation")
  public static void execute(Context context, CrashManagerListener listener) {
    Boolean ignoreDefaultHandler = (listener != null) && (listener.ignoreDefaultHandler());
    WeakReference<Context> weakContext = new WeakReference<Context>(context);
    
    int foundOrSend = hasStackTraces(weakContext);
    if (foundOrSend == 1) {
      Boolean autoSend = false;
      SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
      autoSend |= prefs.getBoolean(ALWAYS_SEND_KEY, false);

      if (listener != null) {
        autoSend |= listener.shouldAutoUploadCrashes();
        autoSend |= listener.onCrashesFound();
        
        listener.onNewCrashesFound();
      }
      
      if (!autoSend) {
        showDialog(weakContext, listener, ignoreDefaultHandler);
      }
      else {
        sendCrashes(weakContext, listener, ignoreDefaultHandler);
      }
    }
    else if (foundOrSend == 2) {
      if (listener != null) {
        listener.onConfirmedCrashesFound();
      }
      
      sendCrashes(weakContext, listener, ignoreDefaultHandler);
    }
    else {
      registerHandler(weakContext, listener, ignoreDefaultHandler);
    }
  }
  
  /**
   * Checks if there are any saved stack traces in the files dir.
   * 
   * @param weakContext 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 int hasStackTraces(WeakReference<Context> weakContext) {
    String[] filenames = searchForStackTraces();
    List<String> confirmedFilenames = null;
    int result = 0;
    if ((filenames != null) && (filenames.length > 0)) {
      try {
        Context context = null;
        if (weakContext != null) {
          context = weakContext.get();
          if (context != null) {
            SharedPreferences preferences = context.getSharedPreferences("HockeySDK", Context.MODE_PRIVATE);
            confirmedFilenames = Arrays.asList(preferences.getString("ConfirmedFilenames", "").split("\\|"));    
          }
        }
        
      }
      catch (Exception e) {
        // Just in case, we catch all exceptions here
      }

      if (confirmedFilenames != null) {
        result = 2;
        
        for (String filename : filenames) {
          if (!confirmedFilenames.contains(filename)) {
            result = 1;
            break;
          }
        }
      }
      else {
        result = 1;
      }
    }

    return result;
  }

  /**
   * Submits all stack traces in the files dir to HockeyApp.
   * 
   * @param weakContext The context to use. Usually your Activity object.
   * @param listener Implement for callback functions.
   */
  public static void submitStackTraces(WeakReference<Context> weakContext, CrashManagerListener listener) {
    String[] list = searchForStackTraces();
    Boolean successful = false;

    if ((list != null) && (list.length > 0)) {
      Log.d(Constants.TAG, "Found " + list.length + " stacktrace(s).");

      for (int index = 0; index < list.length; index++) {
        try {
          // Read contents of stack trace
          String filename = list[index];
          String stacktrace = contentsOfFile(weakContext, filename);
          if (stacktrace.length() > 0) {
            // Transmit stack trace with POST request
            Log.d(Constants.TAG, "Transmitting crash data: \n" + stacktrace);
            DefaultHttpClient httpClient = (DefaultHttpClient)ConnectionManager.getInstance().getHttpClient();
            HttpPost httpPost = new HttpPost(getURLString());
            
            List <NameValuePair> parameters = new ArrayList <NameValuePair>(); 
            parameters.add(new BasicNameValuePair("raw", stacktrace));
            parameters.add(new BasicNameValuePair("userID", contentsOfFile(weakContext, filename.replace(".stacktrace", ".user"))));
            parameters.add(new BasicNameValuePair("contact", contentsOfFile(weakContext, filename.replace(".stacktrace", ".contact"))));
            parameters.add(new BasicNameValuePair("description", contentsOfFile(weakContext, filename.replace(".stacktrace", ".description"))));
            parameters.add(new BasicNameValuePair("sdk", Constants.SDK_NAME));
            parameters.add(new BasicNameValuePair("sdk_version", Constants.SDK_VERSION));
            
            httpPost.setEntity(new UrlEncodedFormEntity(parameters, HTTP.UTF_8));
            
            httpClient.execute(httpPost);   
            successful = true;
          }
        }
        catch (Exception e) {
          e.printStackTrace();
        } 
        finally {
          if (successful) {
            Log.d(Constants.TAG, "Transmission succeeded");
            deleteStackTrace(weakContext, list[index]);

            if (listener != null) {
              listener.onCrashesSent();
            }
          }
          else {
            Log.d(Constants.TAG, "Transmission failed, will retry on next register() call");
            if (listener != null) {
              listener.onCrashesNotSent();
            }
          }
        }
      }
    }
  } 

  /**
   * Deletes all stack traces and meta files from files dir.
   * 
   * @param weakContext The context to use. Usually your Activity object.
   */
  public static void deleteStackTraces(WeakReference<Context> weakContext) {
    String[] list = searchForStackTraces();

    if ((list != null) && (list.length > 0)) {
      Log.d(Constants.TAG, "Found " + list.length + " stacktrace(s).");

      for (int index = 0; index < list.length; index++) {
        try {
          Context context = null;
          if (weakContext != null) {
            Log.d(Constants.TAG, "Delete stacktrace " + list[index] + ".");
            deleteStackTrace(weakContext, list[index]);
            
            context = weakContext.get();
            if (context != null) {
              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, String urlString, String appIdentifier, CrashManagerListener listener, boolean registerHandler) {
    if (context != null) {
      CrashManager.urlString = urlString;
      CrashManager.identifier = Util.sanitizeAppIdentifier(appIdentifier);
  
      Constants.loadFromContext(context);
      
      if (CrashManager.identifier == null) {
        CrashManager.identifier = Constants.APP_PACKAGE;
      }
      
      if (registerHandler) {
        Boolean ignoreDefaultHandler = (listener != null) && (listener.ignoreDefaultHandler());
        WeakReference<Context> weakContext = new WeakReference<Context>(context); 
        registerHandler(weakContext, listener, ignoreDefaultHandler);
      }
    }
  }

  /**
   * Shows a dialog to ask the user whether he wants to send crash reports to 
   * HockeyApp or delete them.
   */
  private static void showDialog(final WeakReference<Context> weakContext, final CrashManagerListener listener, final boolean ignoreDefaultHandler) {
    Context context = null;
    if (weakContext != null) {
      context = weakContext.get();
    }
    
    if (context == null) {
      return;
    }

    AlertDialog.Builder builder = new AlertDialog.Builder(context);
    builder.setTitle(Strings.get(listener, Strings.CRASH_DIALOG_TITLE_ID));
    builder.setMessage(Strings.get(listener, Strings.CRASH_DIALOG_MESSAGE_ID));

    builder.setNegativeButton(Strings.get(listener, Strings.CRASH_DIALOG_NEGATIVE_BUTTON_ID), new DialogInterface.OnClickListener() {
      public void onClick(DialogInterface dialog, int which) {
        if (listener != null) {
          listener.onUserDeniedCrashes();
        }
        
        deleteStackTraces(weakContext);
        registerHandler(weakContext, listener, ignoreDefaultHandler);
      } 
    });

    builder.setNeutralButton(Strings.get(listener, Strings.CRASH_DIALOG_NEUTRAL_BUTTON_ID), new DialogInterface.OnClickListener() {
        @Override
        public void onClick(DialogInterface dialog, int which) {
            Context context = null;
            if (weakContext != null) {
                context = weakContext.get();
            }

            if (context == null) {
                return;
            }

            SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
            prefs.edit().putBoolean(ALWAYS_SEND_KEY, true).commit();

            sendCrashes(weakContext, listener, ignoreDefaultHandler);
        }
    });

    builder.setPositiveButton(Strings.get(listener, Strings.CRASH_DIALOG_POSITIVE_BUTTON_ID), new DialogInterface.OnClickListener() {
      public void onClick(DialogInterface dialog, int which) {
        sendCrashes(weakContext, listener, ignoreDefaultHandler);
      } 
    });

    builder.create().show();
  }
  
  /**
   * Starts thread to send crashes to HockeyApp, then registers the exception 
   * handler. 
   */
  private static void sendCrashes(final WeakReference<Context> weakContext, final CrashManagerListener listener, final boolean ignoreDefaultHandler) {
    saveConfirmedStackTraces(weakContext);
    registerHandler(weakContext, listener, ignoreDefaultHandler);
    
    if (!submitting) {
      submitting = true;
      
      new Thread() {
        @Override
        public void run() {
          submitStackTraces(weakContext, listener);
          submitting = false;
        }
      }.start();
    }
  }

  /**
   * Registers the exception handler. 
   */
  private static void registerHandler(WeakReference<Context> weakContext, CrashManagerListener listener, boolean ignoreDefaultHandler) {
    if ((Constants.APP_VERSION != null) && (Constants.APP_PACKAGE != null)) {
      // Get current handler
      UncaughtExceptionHandler currentHandler = Thread.getDefaultUncaughtExceptionHandler();
      if (currentHandler != null) {
        Log.d(Constants.TAG, "Current handler class = " + currentHandler.getClass().getName());
      }
  
      // Update listener if already registered, otherwise set new handler 
      if (currentHandler instanceof ExceptionHandler) {
        ((ExceptionHandler)currentHandler).setListener(listener);
      }
      else {
        Thread.setDefaultUncaughtExceptionHandler(new ExceptionHandler(currentHandler, listener, ignoreDefaultHandler));
      }
    }
    else {
      Log.d(Constants.TAG, "Exception handler not set because version or package is null.");
    }
  }

  /**
   * Returns the complete URL for the HockeyApp API. 
   */
  private static String getURLString() {
    return urlString + "api/2/apps/" + identifier + "/crashes/";      
  }
  
  /**
   * Deletes the give filename and all corresponding files (same name, 
   * different extension).
   */
  private static void deleteStackTrace(WeakReference<Context> weakContext, String filename) {
    Context context = null;
    if (weakContext != null) {
      context = weakContext.get();
      if (context != null) {
        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. 
   */
  private static String contentsOfFile(WeakReference<Context> weakContext, String filename) {
    Context context = null;
    if (weakContext != null) {
      context = weakContext.get();
      if (context != null) {
        StringBuilder contents = new StringBuilder();
        BufferedReader reader = null;
        try {
          reader = new BufferedReader(new InputStreamReader(context.openFileInput(filename)));
          String line = null;
          while ((line = reader.readLine()) != null) {
            contents.append(line);
            contents.append(System.getProperty("line.separator"));
          }
        }
        catch (FileNotFoundException e) {
        }
        catch (IOException e) {
          e.printStackTrace();
        }
        finally {
          if (reader != null) {
            try { 
              reader.close(); 
            } 
            catch (IOException ignored) { 
            }
          }
        }
        
        return contents.toString();        
      }
    }
    
    return null;
  }
  
  /**
   * Saves the list of the stack traces' file names in shared preferences.  
   */
  private static void saveConfirmedStackTraces(WeakReference<Context> weakContext) {
    Context context = null;
    if (weakContext != null) {
      context = weakContext.get();
      if (context != null) {
        try {
          String[] filenames = searchForStackTraces();
          SharedPreferences preferences = context.getSharedPreferences("HockeySDK", Context.MODE_PRIVATE);
          Editor editor = preferences.edit();
          editor.putString("ConfirmedFilenames", joinArray(filenames, "|"));
          PrefsUtil.applyChanges(editor);
        }
        catch (Exception e) {
          // Just in case, we catch all exceptions here
        }
      }
    }
  }
  
  /**
   * Returns a string created by each element of the array, separated by 
   * delimiter. 
   */
  private static String joinArray(String[] array, String delimiter) {
    StringBuffer buffer = new StringBuffer();
    for (int index = 0; index < array.length; index++) {
      buffer.append(array[index]);
      if (index < array.length - 1) {
        buffer.append(delimiter);
      }
    }
    return buffer.toString();
  }

  /**
   * Searches .stacktrace files and returns then as array. 
   */
  private static String[] searchForStackTraces() {
    if (Constants.FILES_PATH != null) {
      Log.d(Constants.TAG, "Looking for exceptions in: " + Constants.FILES_PATH);
  
      // Try to create the files folder if it doesn't exist
      File dir = new File(Constants.FILES_PATH + "/");
      boolean created = dir.mkdir();
      if (!created && !dir.exists()) {
        return new String[0];
      }
  
      // Filter for ".stacktrace" files
      FilenameFilter filter = new FilenameFilter() { 
        public boolean accept(File dir, String name) {
          return name.endsWith(".stacktrace"); 
        } 
      }; 
      return dir.list(filter);
    }
    else {
      Log.d(Constants.TAG, "Can't search for exception as file path is null.");
      return null;
    }
  }
}




Java Source Code List

net.hockeyapp.android.Constants.java
net.hockeyapp.android.CrashManagerListener.java
net.hockeyapp.android.CrashManager.java
net.hockeyapp.android.ExceptionHandler.java
net.hockeyapp.android.ExpiryInfoActivity.java
net.hockeyapp.android.FeedbackActivityInterface.java
net.hockeyapp.android.FeedbackActivity.java
net.hockeyapp.android.FeedbackManagerListener.java
net.hockeyapp.android.FeedbackManager.java
net.hockeyapp.android.LocaleManager.java
net.hockeyapp.android.LoginActivity.java
net.hockeyapp.android.LoginManagerListener.java
net.hockeyapp.android.LoginManager.java
net.hockeyapp.android.PaintActivity.java
net.hockeyapp.android.StringListener.java
net.hockeyapp.android.Strings.java
net.hockeyapp.android.Tracking.java
net.hockeyapp.android.UpdateActivityInterface.java
net.hockeyapp.android.UpdateActivity.java
net.hockeyapp.android.UpdateFragment.java
net.hockeyapp.android.UpdateInfoListener.java
net.hockeyapp.android.UpdateManagerListener.java
net.hockeyapp.android.UpdateManager.java
net.hockeyapp.android.adapters.MessagesAdapter.java
net.hockeyapp.android.listeners.DownloadFileListener.java
net.hockeyapp.android.listeners.SendFeedbackListener.java
net.hockeyapp.android.objects.ErrorObject.java
net.hockeyapp.android.objects.FeedbackAttachment.java
net.hockeyapp.android.objects.FeedbackMessage.java
net.hockeyapp.android.objects.FeedbackResponse.java
net.hockeyapp.android.objects.Feedback.java
net.hockeyapp.android.tasks.AttachmentDownloader.java
net.hockeyapp.android.tasks.CheckUpdateTaskWithUI.java
net.hockeyapp.android.tasks.CheckUpdateTask.java
net.hockeyapp.android.tasks.DownloadFileTask.java
net.hockeyapp.android.tasks.GetFileSizeTask.java
net.hockeyapp.android.tasks.LoginTask.java
net.hockeyapp.android.tasks.ParseFeedbackTask.java
net.hockeyapp.android.tasks.SendFeedbackTask.java
net.hockeyapp.android.utils.AsyncTaskUtils.java
net.hockeyapp.android.utils.Base64.java
net.hockeyapp.android.utils.ConnectionManager.java
net.hockeyapp.android.utils.DeviceUtils.java
net.hockeyapp.android.utils.FeedbackParser.java
net.hockeyapp.android.utils.ImageUtils.java
net.hockeyapp.android.utils.PrefsUtil.java
net.hockeyapp.android.utils.SimpleMultipartEntity.java
net.hockeyapp.android.utils.UiThreadUtil.java
net.hockeyapp.android.utils.Util.java
net.hockeyapp.android.utils.VersionCache.java
net.hockeyapp.android.utils.VersionHelper.java
net.hockeyapp.android.utils.ViewHelper.java
net.hockeyapp.android.views.AttachmentListView.java
net.hockeyapp.android.views.AttachmentView.java
net.hockeyapp.android.views.ExpiryInfoView.java
net.hockeyapp.android.views.FeedbackMessageView.java
net.hockeyapp.android.views.FeedbackView.java
net.hockeyapp.android.views.LoginView.java
net.hockeyapp.android.views.PaintView.java
net.hockeyapp.android.views.UpdateView.java