3

My Android app currently uses a custom UncaughtExceptionHandler that aims to capture any crash, and schedules an app restart for several seconds in the future with AlarmManager before manually calling Process.killProcess(Process.myPid()) to avoid Android's Force Close popup as in my app's use case, the user will not be able to interact with the device to tap "ok" on the FC dialog and restart the app.

Now, I'd like to integrate with Firebase Crash reports, but I fear wrong behaviors, so here are my questions:

  1. How should I make my code so my custom UncaughtExceptionHandler passes the exception to Firebase Crash Report before killing it's process? Would calling Thread.getDefaultUncaughtExceptionHandler() give me the Firebase Crash report UncaughtExceptionHandler so I can just call uncaughtException(...) on it?
  2. May Process.killProcess(Process.myPid()) prevent Firebase Crash reporting library to do it's reporting work? Would Firebase have started it's crash report in a separated process before it's uncaughtException(...) returns? Does Firebase own UncaughtExceptionHandler calls back to Android default's UncaughtExceptionHandler, showing the FC dialog?
  3. May Process.killProcess(Process.myPid()) kill Firebase Crash Reporting process in addition to the default process?
  4. How can my custom Application class detect if it is instantiated in Firebase Crash Reporting process? Treating both processes the same way would probably lead to inconsistent states.

Thanks to anyone that tries to help me!

Louis CAD
  • 10,965
  • 2
  • 39
  • 58
  • [This Google I/O video](https://www.youtube.com/watch?v=AJqakuas_6g) includes a description of Crash Reporting starting at 16:30. You might find it helpful. – Bob Snyder Jun 27 '16 at 18:29
  • @qbix I already watched it, but it doesn't cover this tricky case. Thanks anyway! – Louis CAD Jun 27 '16 at 18:44

2 Answers2

3

If you kill the process in your exception handler, you will not be able to receive crashes. It will interfere with the ability to persist the crash for either immediate or delayed transmission. It will possibly interfere with any library that has registered uncaught exception handlers that behave well.

In fact, Process.killProcess(Process.myPid()) is very much an anti-pattern for Android development. Android apps should not be concerned at all with the overall lifecycle if the process that hosts the app. The fact that Android manages the process for you is an optimization designed for the benefit of the users.

I strongly recommend, for uncaught exceptions in your app, to simply let the app die as it normally would. Masking the proper effect of the crash is like sweeping dirt under a rug. You might be resolving a short term problem, but what really needs to happen is the normal logging and handling of the error so you can fix it.

Don't depend on the fact that Firebase Crash Reporting transmits exceptions in another process. That other process will be removed in the full non-beta release.

The best situation for your Application subclass is to not depend at all which process it's operating. In fact, the Android team at Google does not recommend use of Application subclasses at all since it only leads to trouble for multi-process apps. If you must use an Application subclass, it should expect to run within multiple processes.

Community
  • 1
  • 1
Doug Stevenson
  • 297,357
  • 32
  • 422
  • 441
  • First, thanks for your answer! I knew that killing the process is an anti-pattern, but my doesn't have any "quit" button, I use this because if I don't, the app will wait for the user to press "ok" to kill the application and finally let it restart, and my app is used to protect lone workers. Is there a better solution to ensure my app restarts AUTOMATICALLY if it crashs while the user has fallen from a ladder, is stunned and expects the app to detect he is motionless and automatically call for help? – Louis CAD Jun 28 '16 at 07:38
  • I can let it work as it works today, so my app is crash-proof, but I'd like to receive crashes too, so I can fix them. **I'd like to use Firebase Crash reporting and still make sure my app will restart after a crash, without any user interaction**, and it's not to force user engagement or anything evil, but for his security (as long as the device itself works properly) – Louis CAD Jun 28 '16 at 07:41
  • Apps that crash with a dialog should eventually terminate without having to OK the resulting dialog. You can also still schedule jobs to run periodically with libraries such as firebase-jobdispatcher if you really need to keep the process alive during certain times. But you can't guarantee that any process is alive 100% of the time. Android does not allow for that case unless you create a custom ROM. – Doug Stevenson Jun 28 '16 at 07:57
  • After some testing, the app does NOT eventually terminate and restart according to overdue AlarmManager requests. I still found a hacky way to ensure my app does restart and that the crash report gets sent to Firebase, so I'm going to write an answer, and I hope Firebase Crash Reporting will officially play well with such use cases in the future – Louis CAD Jul 11 '16 at 13:51
  • I added a deep-diving answer that solves my problem, please, take a look at how I did it and consider the suggestion part for Firebase Crash Reporting library – Louis CAD Jul 11 '16 at 15:34
3

After some testing, I finally found a way to both ensure my app restarts properly after an UncaughtException. I attempted three different approaches, but only the first, which is my original code, with just a little tweak to pass the uncaught Throwable to `FirebaseCrash, and ensure it is considered as a FATAL error.

The code that works:

final UncaughtExceptionHandler crashShield = new UncaughtExceptionHandler() {

    private static final int RESTART_APP_REQUEST = 2;

    @Override
    public void uncaughtException(Thread thread, Throwable ex) {
        if (BuildConfig.DEBUG) ex.printStackTrace();
        reportFatalCrash(ex);
        restartApp(MyApp.this, 5000L);
    }

    private void reportFatalCrash(Throwable exception) {
        FirebaseApp firebaseApp = FirebaseApp.getInstance();
        if (firebaseApp != null) {
            try {
                FirebaseCrash.getInstance(firebaseApp)
                        .zzg(exception); // Reports the exception as fatal.
            } catch (com.google.firebase.crash.internal.zzb zzb) {
                Timber.wtf(zzb, "Internal firebase crash reporting error");
            } catch (Throwable t) {
                Timber.wtf(t, "Unknown error during firebase crash reporting");
            }
        } else Timber.wtf("no FirebaseApp!!");
    }

    /**
     * Schedules an app restart with {@link AlarmManager} and exit the process.
     * @param restartDelay in milliseconds. Min 3s to let the user got in settings force
     *                     close the app manually if needed.
     */
    private void restartApp(Context context, @IntRange(from = 3000) long restartDelay) {
        Intent restartReceiver = new Intent(context, StartReceiver_.class)
                .setAction(StartReceiver.ACTION_RESTART_AFTER_CRASH);
        PendingIntent restartApp = PendingIntent.getBroadcast(
                context,
                RESTART_APP_REQUEST,
                restartReceiver,
                PendingIntent.FLAG_ONE_SHOT
        );
        final long now = SystemClock.elapsedRealtime();
        // Line below schedules an app restart 5s from now.
        mAlarmManager.set(ELAPSED_REALTIME_WAKEUP, now + restartDelay, restartApp);
        Timber.i("just requested app restart, killing process");
        System.exit(2);
    }
};
Thread.setDefaultUncaughtExceptionHandler(crashShield);

Explanation of why and unsuccessful attempts

It's weird that the hypothetically named reportFatal(Throwable ex) method from FirebaseCrash class has it's name proguarded while being still (and thankfully) public, giving it the following signature: zzg(Throwable ex).

This method should stay public, but not being obfuscated IMHO.

To ensure my app works properly with multi-process introduced by Firebase Crash Report library, I had to move code away from the application class (which was a great thing) and put it in lazily loaded singletons instead, following Doug Stevenson's advice, and it is now multi-process ready.

You can see that nowhere in my code, I called/delegated to the default UncaughtExceptionHandler, which would be Firebase Crash Reporting one here. I didn't do so because it always calls the default one, which is Android's one, which has the following issue:

All code written after the line where I pass the exception to Android's default UncaughtExceptionHandler will never be executed, because the call is blocking, and process termination is the only thing that can happen after, except already running threads.

The only way to let the app die and restart is by killing the process programmatically with System.exit(int whatever) or Process.kill(Process.myPid()) after having scheduled a restart with AlarmManager in the very near future.

Given this, I started a new Thread before calling the default UncaughtExceptionHandler, which would kill the running process after Firebase Crash Reporting library would have got the exception but before the scheduled restart fires (requires magic numbers). It worked on the first time, removing the Force Close dialog when the background thread killed the process, and then, the AlarmManager waked up my app, letting it know that it crashed and has a chance to restart.

The problem is that the second time didn't worked for some obscure and absolutely undocumented reasons. The app would never restart even though the code that schedules a restart calling the AlarmManager was properly run.

Also, the Force Close popup would never show up. After seeing that whether Firebase Crash reporting was included (thus automatically enabled) or not didn't change anything about this behavior, it was tied to Android (I tested on a Kyocera KC-S701 running Android 4.4.2).

So I finally searched what Firebase own UncaughtExceptionHandler called to report the throwable and saw that I could call the code myself and manage myself how my app behaves on an uncaught Throwable.

How Firebase could improve such scenarios Making the hypothetically named reportFatal(Throwable ex) method non name-obfuscated and documented, or letting us decide what happens after Firebase catches the Throwable in it's UncaughtExceptionHandler instead of delegating inflexibly to the dumb Android's default UncaughtExceptionHandler would help a lot.

It would allow developers which develop critical apps that run on Android to ensure their app keep running if the user is not able to it (think about medical monitoring apps, monitoring apps, etc).

It would also allow developers to launch a custom activity to ask users to explain how it occurred, with the ability to post screenshots, etc.

BTW, my app is meant to monitor humans well-being in critical situations, hence cannot tolerate to stop running. All exceptions must be recovered to ensure user safety.

Louis CAD
  • 10,965
  • 2
  • 39
  • 58
  • 1
    This code that depends on symbols obfuscated by proguard (zzg) could break in a future version of the SDK. Also bear in mind that Crash Reporting is currently in beta and some of the strategies it uses to store and transmit data are going to change significantly. So I can't really say here if what you're doing is future-proof. You might have to revisit the whole scheme later. Just be aware. :-) – Doug Stevenson Jul 12 '16 at 01:09
  • If an upcoming version don't allow me to let my app restart using my own technique, I won't be able to update the SDK, because my app is way to critical for it's users. Anyway, I know that any new version could change the name of the obfuscated method, but if you, as a member of the Firebase team, ensure that next version allow us to report manually fatal crashes, or allow us to ask Firebase to don't call Android's misbehaving default UncaughtExceptionHandler, but let us handle the after report, I will be able to write future-proof code that won't be broken by an update. – Louis CAD Jul 12 '16 at 06:30
  • 1
    I personally can't guarantee anything in the future, but I encourage you to say what you are trying to do in a feature request. You can enter the details here: https://firebase.google.com/support/contact/bugs-features/. This will get prioritized with all the other work the team is scheduled to do. – Doug Stevenson Jul 12 '16 at 10:09
  • 1
    `Timber.wtf` So are you gonna explain the future developer who will maintain your code, why you named this method such a delightful name ? – Sharp Edge Mar 17 '17 at 23:06
  • -0.5 for app restart ( don't use alarm use instrumentation to restart app ) -0.5 for method could look much better - overall -1 – ceph3us Apr 06 '17 at 00:38