131

When my app creates an unhandled exception, rather than simply terminating, I'd like to first give the user an opportunity to send a log file. I realize that doing more work after getting a random exception is risky but, hey, the worst is the app finishes crashing and the log file doesn't get sent. This is turning out to be trickier than I expected :)

What works: (1) trapping the uncaught exception, (2) extracting log info and writing to a file.

What doesn't work yet: (3) starting an activity to send email. Ultimately, I'll have yet another activity to ask the user's permission. If I get the email activity working, I don't expect much trouble for the other.

The crux of the problem is that the unhandled exception is caught in my Application class. Since that isn't an Activity, it's not obvious how to start an activity with Intent.ACTION_SEND. That is, normally to start an activity one calls startActivity and resumes with onActivityResult. These methods are supported by Activity but not by Application.

Any suggestions on how to do this?

Here are some code snips as a starting guide:

public class MyApplication extends Application
{
  defaultUncaughtHandler = Thread.getDefaultUncaughtExceptionHandler();
  public void onCreate ()
  {
    Thread.setDefaultUncaughtExceptionHandler (new Thread.UncaughtExceptionHandler()
    {
      @Override
      public void uncaughtException (Thread thread, Throwable e)
      {
        handleUncaughtException (thread, e);
      }
    });
  }

  private void handleUncaughtException (Thread thread, Throwable e)
  {
    String fullFileName = extractLogToFile(); // code not shown

    // The following shows what I'd like, though it won't work like this.
    Intent intent = new Intent (Intent.ACTION_SEND);
    intent.setType ("plain/text");
    intent.putExtra (Intent.EXTRA_EMAIL, new String[] {"me@mydomain.example"});
    intent.putExtra (Intent.EXTRA_SUBJECT, "log file");
    intent.putExtra (Intent.EXTRA_STREAM, Uri.parse ("file://" + fullFileName));
    startActivityForResult (intent, ACTIVITY_REQUEST_SEND_LOG);
  }

  public void onActivityResult (int requestCode, int resultCode, Intent data)
  {
    if (requestCode == ACTIVITY_REQUEST_SEND_LOG)
      System.exit(1);
  }
}
Stephen Ostermiller
  • 23,933
  • 14
  • 88
  • 109
Peri Hartman
  • 19,314
  • 18
  • 55
  • 101

9 Answers9

267

Here's the complete solution (almost: I omitted the UI layout and button handling) - derived from a lot of experimentation and various posts from others related to issues that came up along the way.

There are a number of things you need to do:

  1. Handle uncaughtException in your Application subclass.
  2. After catching an exception, start a new activity to ask the user to send a log.
  3. Extract the log info from logcat's files and write to your own file.
  4. Start an email app, providing your file as an attachment.
  5. Manifest: filter your activity to be recognized by your exception handler.
  6. Optionally, setup Proguard to strip out Log.d() and Log.v().

Now, here are the details:

(1 & 2) Handle uncaughtException, start send log activity:

public class MyApplication extends Application
{
  public void onCreate ()
  {
    // Setup handler for uncaught exceptions.
    Thread.setDefaultUncaughtExceptionHandler (new Thread.UncaughtExceptionHandler()
    {
      @Override
      public void uncaughtException (Thread thread, Throwable e)
      {
        handleUncaughtException (thread, e);
      }
    });
  }

  public void handleUncaughtException (Thread thread, Throwable e)
  {
    e.printStackTrace(); // not all Android versions will print the stack trace automatically

    Intent intent = new Intent ();
    intent.setAction ("com.mydomain.SEND_LOG"); // see step 5.
    intent.setFlags (Intent.FLAG_ACTIVITY_NEW_TASK); // required when starting from Application
    startActivity (intent);

    System.exit(1); // kill off the crashed app
  }
}

(3) Extract log (I put this an my SendLog Activity):

private String extractLogToFile()
{
  PackageManager manager = this.getPackageManager();
  PackageInfo info = null;
  try {
    info = manager.getPackageInfo (this.getPackageName(), 0);
  } catch (NameNotFoundException e2) {
  }
  String model = Build.MODEL;
  if (!model.startsWith(Build.MANUFACTURER))
    model = Build.MANUFACTURER + " " + model;

  // Make file name - file must be saved to external storage or it wont be readable by
  // the email app.
  String path = Environment.getExternalStorageDirectory() + "/" + "MyApp/";
  String fullName = path + <some name>;

  // Extract to file.
  File file = new File (fullName);
  InputStreamReader reader = null;
  FileWriter writer = null;
  try
  {
    // For Android 4.0 and earlier, you will get all app's log output, so filter it to
    // mostly limit it to your app's output.  In later versions, the filtering isn't needed.
    String cmd = (Build.VERSION.SDK_INT <= Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) ?
                  "logcat -d -v time MyApp:v dalvikvm:v System.err:v *:s" :
                  "logcat -d -v time";

    // get input stream
    Process process = Runtime.getRuntime().exec(cmd);
    reader = new InputStreamReader (process.getInputStream());

    // write output stream
    writer = new FileWriter (file);
    writer.write ("Android version: " +  Build.VERSION.SDK_INT + "\n");
    writer.write ("Device: " + model + "\n");
    writer.write ("App version: " + (info == null ? "(null)" : info.versionCode) + "\n");

    char[] buffer = new char[10000];
    do
    {
      int n = reader.read (buffer, 0, buffer.length);
      if (n == -1)
        break;
      writer.write (buffer, 0, n);
    } while (true);

    reader.close();
    writer.close();
  }
  catch (IOException e)
  {
    if (writer != null)
      try {
        writer.close();
      } catch (IOException e1) {
      }
    if (reader != null)
      try {
        reader.close();
      } catch (IOException e1) {
      }

    // You might want to write a failure message to the log here.
    return null;
  }

  return fullName;
}

(4) Start an email app (also in my SendLog Activity):

private void sendLogFile ()
{
  String fullName = extractLogToFile();
  if (fullName == null)
    return;

  Intent intent = new Intent (Intent.ACTION_SEND);
  intent.setType ("plain/text");
  intent.putExtra (Intent.EXTRA_EMAIL, new String[] {"log@mydomain.example"});
  intent.putExtra (Intent.EXTRA_SUBJECT, "MyApp log file");
  intent.putExtra (Intent.EXTRA_STREAM, Uri.parse ("file://" + fullName));
  intent.putExtra (Intent.EXTRA_TEXT, "Log file attached."); // do this so some email clients don't complain about empty body.
  startActivity (intent);
}

(3 & 4) Here's what SendLog looks like (you'll have to add the UI, though):

public class SendLog extends Activity implements OnClickListener
{
  @Override
  public void onCreate(Bundle savedInstanceState)
  {
    super.onCreate(savedInstanceState);
    requestWindowFeature (Window.FEATURE_NO_TITLE); // make a dialog without a titlebar
    setFinishOnTouchOutside (false); // prevent users from dismissing the dialog by tapping outside
    setContentView (R.layout.send_log);
  }

  @Override
  public void onClick (View v)
  {
    // respond to button clicks in your UI
  }

  private void sendLogFile ()
  {
    // method as shown above
  }

  private String extractLogToFile()
  {
    // method as shown above
  }
}

(5) Manifest:

<manifest xmlns:android="http://schemas.android.com/apk/res/android" ... >
    <!-- needed for Android 4.0.x and eariler -->
    <uses-permission android:name="android.permission.READ_LOGS" />

    <application ... >
        <activity
            android:name="com.mydomain.SendLog"
            android:theme="@android:style/Theme.Dialog"
            android:textAppearance="@android:style/TextAppearance.Large"
            android:windowSoftInputMode="stateHidden">
            <intent-filter>
              <action android:name="com.mydomain.SEND_LOG" />
              <category android:name="android.intent.category.DEFAULT" />
            </intent-filter>
        </activity>
     </application>
</manifest>

(6) Setup Proguard:

In project.properties, change the config line. You must specify "optimize" or Proguard will not remove Log.v() and Log.d() calls.

proguard.config=${sdk.dir}/tools/proguard/proguard-android-optimize.txt:proguard-project.txt

In proguard-project.txt, add the following. This tell Proguard to assume Log.v and Log.d have no side effects (even though they do since they write to the logs) and thus can be removed during optimization:

-assumenosideeffects class android.util.Log {
    public static int v(...);
    public static int d(...);
}

That's it! If you have any suggestions for improvements to this, please let me know and I may update this.

Stephen Ostermiller
  • 23,933
  • 14
  • 88
  • 109
Peri Hartman
  • 19,314
  • 18
  • 55
  • 101
  • 12
    Just a note: never call System.exit. You'll be breaking the uncaught exception handlers chain. Just forward it to the next. You already have "defaultUncaughtHandler" from getDefaultUncaughtExceptionHandler, just pass the call to it when you're done. – gilm Mar 03 '14 at 14:02
  • 2
    @gilm, can you provide an example on how to "forward it to the next" and what else might happen in the handler chain? It's been a while, but I tested a number of scenarios, and calling System.exit() seemed to be the best solution. After all, the app has crashed and it needs to terminate. – Peri Hartman Mar 03 '14 at 18:08
  • I think I recall what happens if you let the uncaught exception continue: the system puts up the "app terminated" notice. Normally, that would be fine. But since the new activity (that sends email with the log) is already putting up its own message, having the system put up another is confusing. So I force it to abort quietly with System.exit(). – Peri Hartman Mar 03 '14 at 18:58
  • 8
    @PeriHartman sure: there is only one default handler. before calling setDefaultUncaughtExceptionHandler(), you must call getDefaultUncaughtExceptionHandler() and keep that reference. So, say you have bugsense, crashlytics and your handler is the last installed, the system will call yours only. it's your task to call the reference that you received via getDefaultUncaughtExceptionHandler() and pass the thread and throwable to the next in chain. if you just System.exit(), the others won't be called. – gilm Mar 04 '14 at 10:25
  • @PeriHartman Is it code can be used for handle crashed application? Meanings, don't display "force close" dialog and keep application still running? – Yohanim Sep 28 '14 at 12:00
  • I had to add android:process=":report_process" to the Activity in the Manifest to launch the Error logging activity in a separate process. Otherwise, it would never show on a crash. – Andrei Drynov Oct 10 '14 at 11:02
  • 4
    You also need to register your implementation of Application in the manifest with an attribute in the Application tag, e.g.: – Matt Nov 05 '14 at 16:26
  • 1
    @gilm - I have revisited this code and tried forwarding to the prior default handler. The effect is to show the system "app crashed" dialog as well as my custom one. It seems the only solution that works is to call System.exit(). – Peri Hartman Mar 17 '15 at 21:16
  • @PeriHartman get the dialog with button. But by clicking on the button the mail app not opening.Any solution – Jyothish Jul 26 '15 at 04:36
  • @Jyothish - sorry, but I'm not going to debug your code. – Peri Hartman Jul 26 '15 at 14:09
  • @PeriHartman Nice solution! Thanks for sharing! I realize this question/answer is old, but would you know the equivalent step 6 for Android Studio? Those files don't appear to be in use with Android Studio (at least not in the recent versions). – Nullqwerty Jul 28 '15 at 21:10
  • @Nullqwerty - I haven't used Android Studio yet (I guess my days are numbered :) but if it also uses proguard then the progaurd files must be somewhere. Your answer is probably somewhere in SO. – Peri Hartman Jul 28 '15 at 21:50
  • @PeriHartman Yes, think I found them. Will know on my next uncaught exception ;) The assumenosideeffects section can go in the file proguard-rules.pro. The optimize piece goes in build.gradle in the buildTypes->release section using this line as a replacement buildTypes { release { : runProguard true signingConfig signingConfigs.release proguardFile getDefaultProguardFile('proguard-android-optimize.txt') } } – Nullqwerty Jul 29 '15 at 13:42
  • Thanks for the answer. I received "file not found" exception for the log path, so I had to add following code: File folder = new File(path); if (!folder.exists()) { folder.mkdirs(); } – Azho KG Sep 12 '15 at 01:10
  • Sorry, that's off topic. You can easily design a layout that goes with the SendLog class. – Peri Hartman Oct 10 '15 at 16:15
  • I dont like showing an activity to the user to send a crash log, i think it's better to collect data and store them in a database, then send them regularly with a SyncAdapter or similar background thread without the user action. it's better because you are certain that all the exceptions will be sent to you, not the only ones the users wants send. (prevent dialog dismissing is not enough) – MatPag Oct 30 '15 at 10:47
  • @Mat - Better for whom? Of course it's more reliable to simply send the log data than to involve the user. However, you may want to disclose what you are doing (or planning to do) in your privacy policy. In my case, my app stores a lot of user-sensitive data and it sends nothing to my company without his explicit permission. – Peri Hartman Oct 30 '15 at 14:24
  • @PeriHartman it depends on the information you need to send for have a reproducible test case of the problem (not all of us need so many sensitive information to track a bug). But your point is perfectly correct. – MatPag Oct 30 '15 at 16:15
  • As someone said below, you can use [ACRA](https://github.com/ACRA/acra). It can do this very thing. Follow [this](https://github.com/ACRA/acra/wiki/BasicSetup) and check out the email section, just need to add a mailTo with your address. – lockwobr Jan 17 '16 at 23:58
  • I have implemented like this and is working fine. But application gets crashed when an uncaught exception occurs in a service running. Am I doing something wrong? Any additional things to be done for handling exceptions in service? – Rino Feb 15 '17 at 09:15
  • I got Error inflating class android.widget.Button error in Binary XML file. but if I remove button then the SendLog dialog opens normally. can you post your send_log.xml layout or help me how to resolve this? thanks in advance... – Homayoon Ahmadi Oct 20 '17 at 09:32
  • For SDK 23 and above you need runtime permission to write the log file. – Sadegh Ghanbari Nov 19 '20 at 08:38
11

Today there are many crash reprting tools that do this easily.

  1. crashlytics - A crash reporting tool, free of charge but gives you basic reports Advantages : Free

  2. Gryphonet - A more advanced reporting tool, requires some kind of fee. Advantages : Easy recreation of crashes, ANR's, slowness...

If you are a private developer I would suggest Crashlytics, but if it's a big organization, I would go for Gryphonet.

Good Luck!

Ariel Bell
  • 161
  • 2
  • 4
7

@PeriHartman's answer works well when the UI thread throws uncaught exception. I made some improvements for when the uncaught exception is thrown by a non UI thread.

public boolean isUIThread(){
    return Looper.getMainLooper().getThread() == Thread.currentThread();
}

public void handleUncaughtException(Thread thread, Throwable e) {
    e.printStackTrace(); // not all Android versions will print the stack trace automatically

    if(isUIThread()) {
        invokeLogActivity();
    }else{  //handle non UI thread throw uncaught exception

        new Handler(Looper.getMainLooper()).post(new Runnable() {
            @Override
            public void run() {
                invokeLogActivity();
            }
        });
    }
}

private void invokeLogActivity(){
    Intent intent = new Intent ();
    intent.setAction ("com.mydomain.SEND_LOG"); // see step 5.
    intent.setFlags (Intent.FLAG_ACTIVITY_NEW_TASK); // required when starting from Application
    startActivity (intent);

    System.exit(1); // kill off the crashed app
}
erikprice
  • 6,240
  • 3
  • 30
  • 40
Jack Ruan
  • 101
  • 1
  • 4
6

Try using ACRA instead - it handles sending the stack trace as well as tons of other useful debug information to your backend, or to Google Docs document you've set up.

https://github.com/ACRA/acra

Martin Konecny
  • 57,827
  • 19
  • 139
  • 159
4

Handling uncaught Exceptions: as @gilm explained just do this, (kotlin):

private val defaultUncaughtHandler = Thread.getDefaultUncaughtExceptionHandler();

override fun onCreate() {
  //...
    Thread.setDefaultUncaughtExceptionHandler { t, e ->
        Crashlytics.logException(e)
        defaultUncaughtHandler?.uncaughtException(t, e)
    }
}

I hope it helps, it worked for me.. (:y). In my case I've used 'com.microsoft.appcenter.crashes.Crashes' library for error tracking.

Bill Mote
  • 12,644
  • 7
  • 58
  • 82
Kreshnik
  • 2,661
  • 5
  • 31
  • 39
2

Nicely explained. But one observation here, instead of writing into file using File Writer and Streaming, I made use of the logcat -f option directly. Here is the code

String[] cmd = new String[] {"logcat","-f",filePath,"-v","time","<MyTagName>:D","*:S"};
        try {
            Runtime.getRuntime().exec(cmd);
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }

This helped me in flushing the latest buffer info. Using File streaming gave me one issue that it was not flushing the latest logs from buffer. But anyway, this was really helpful guide. Thank you.

King of Masses
  • 18,405
  • 4
  • 60
  • 77
schow
  • 33
  • 1
  • 7
  • I believe I tried that (or something similar). If I recall, I ran into permission problems. Are you doing this on a rooted device? – Peri Hartman Apr 10 '14 at 03:45
  • Oh, I am sorry if I confused you, but I am so far trying with my emulators. I am yet to port it to a device (but yes the device I use for testing is a rooted one). – schow Apr 10 '14 at 04:05
1

Here's a solution I devised based on Peri's answer and gilm's comments.

Create the following Kotlin UncaughtExceptionHandler Class:

import android.content.Context
import android.content.Intent
import com.you.website.presentation.activity.MainActivity
import java.io.StringWriter
import android.os.Process
import android.util.Log
import com.google.firebase.crashlytics.FirebaseCrashlytics
import kotlin.system.exitProcess

/**
 * When a crash occurs, Android attempts to restart the current and preceding activity by default.
 * This is not desired functionality because in most apps there is not enough data persisted to properly recreate recreate the activities.
 * This can result in restarting with "Welcome, null" at the top of the page.
 * Instead, when a crash occurs, we want the app to crash gracefully and restart from its initial screen.
 * This UncaughtExceptionHandler catches uncaught exceptions and returns to the its initial startup Activity.
 */
class UncaughtExceptionHandler(val context: Context, private val defaultUncaughtHandler: Thread.UncaughtExceptionHandler?) : Thread.UncaughtExceptionHandler {
    override fun uncaughtException(thread: Thread, exception: Throwable) {
        // Begin the main activity so when the app is killed, we return to it instead of the currently active activity.
        val intent = Intent(context, MainActivity::class.java)
        context.startActivity(intent)
        // Return to the normal flow of an uncaught exception
        if (defaultUncaughtHandler != null) {
            defaultUncaughtHandler.uncaughtException(thread, exception)
        } else {
            // This scenario should never occur. It can only happen if there was no defaultUncaughtHandler when the handler was set up.
            val stackTrace = StringWriter()
            System.err.println(stackTrace) // print exception in the 'run' tab.
            Log.e("UNCAUGHT_EXCEPTION", "exception", exception) // print exception in 'Logcat' tab.
            FirebaseCrashlytics.getInstance().recordException(exception) // Record exception in Firebase Crashlytics
            Process.killProcess(Process.myPid())
            exitProcess(0)
        }
    }
}

Add the following to your MainActivity's onCreate() method (First activity to open):

Thread.setDefaultUncaughtExceptionHandler(UncaughtExceptionHandler(this, Thread.getDefaultUncaughtExceptionHandler()))
Alan Nelson
  • 1,129
  • 2
  • 11
  • 27
0

You can handle the uncaught exceptions using FireCrasher library and do a recovery from it.

you can know more about the library in this Medium Article

Osama Raddad
  • 344
  • 3
  • 17
0

I modified Peri's answer which was extremely helpful. Instead of using Logcat, the Throwable is sent to SEND_LOG via intent putExtra. Then loop though the stack trace in SEND_LOG. Instead of using email, the string is posted to a web page.

Code excerpts that append to his answer:

Application

public void handleUncaughtException (Thread thread, Throwable e)
    {
        e.printStackTrace();

        // Launch error handler
        Intent intent = new Intent ();
        intent.setAction ("com.mydomain.SEND_LOG");
        intent.setFlags (Intent.FLAG_ACTIVITY_NEW_TASK); from Application
        intent.putExtra("com.mydomain.thrownexception",e);
        startActivity (intent);

        Runtime.getRuntime().exit(1);
    }

SEND_LOG

public class SendLog extends AppCompatActivity {
    public Throwable thrownexception;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        if (getIntent().hasExtra("com.mydomain.thrownexception")) {
            thrownexception = (Throwable) getIntent().getExtras().get("com.mydomain.thrownexception");
        }
    }

    private String getLogString() {
        String logToUpload = "";
        PackageManager manager = context.getPackageManager();
        PackageInfo info = null;
        try {
            info = manager.getPackageInfo(context.getPackageName(), 0);
        } catch (PackageManager.NameNotFoundException e2) {
            e2.printStackTrace();
        }
        String model = Build.MODEL;
        if (!model.startsWith(Build.MANUFACTURER))
            model = Build.MANUFACTURER + " " + model;

        try {
            logToUpload = logToUpload + "Android version | " + Build.VERSION.SDK_INT + "\n";
            logToUpload = logToUpload + "Device | " + model + "\n";
            logToUpload = logToUpload + "App version | " + (info == null ? "(null)" : info.versionCode) + "\n";
            logToUpload = logToUpload + "Exception | " + thrownException.toString();
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
        logToUpload = logToUpload + stackTraceToString(thrownException);

        return logToUpload;
}

     public static String stackTraceToString(Throwable throwable) {
        String strStackTrace = "";
        strStackTrace = strStackTrace + "\nStack Trace |";
        strStackTrace = strStackTrace + throwable.toString();
        try {
            for (StackTraceElement element : throwable.getStackTrace()) {
                strStackTrace = strStackTrace + "\n" + element.toString();
            }
            //  Suppressed exceptions 
             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
                Throwable[] suppressed = throwable.getSuppressed();
                if (suppressed.length > 0) {
                    strStackTrace = strStackTrace + "\nSuppressed |";
                    for (Throwable elementThrowable : suppressed) {
                        strStackTrace = strStackTrace + "\n" + elementThrowable.toString();
                        for (StackTraceElement element : elementThrowable.getStackTrace()) {
                            strStackTrace = strStackTrace + "\n" + element.toString();
                        }
                    }
                }
            }
            //   Recursively call method to include stack traces of exceptions that caused the throwable
            if (throwable.getCause() != null) {
                strStackTrace = strStackTrace + "\nCaused By:";
                strStackTrace = strStackTrace + stackTraceToString(throwable.getCause());
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

    return strStackTrace;
}

(I don't have enough points to comment on his answer)

Lee
  • 13
  • 1
  • 3