Search code examples
androidcordovaionic-frameworkkillkill-process

Is there a way to handle an Android app getting killed in the Paused state to make the system restart the activity automatically?


I have an Ionic/Cordova app that makes quite heavy use of the camera plugin and has a problem when being run on Android.

As described in the Android Activity Lifecycle docs docs and also Cordova's Lifecycle Docs for the Android Platform, when the camera comes to the foreground the main activity of the app gets put into either a paused or stopped state in the background.

While the main activity is in either of these states it is susceptible to being killed by the system if it needs to free up memory, which happens in this app often enough for users to complain.

In testing, I see that sometimes the app will restart itself when the camera is dismissed, and other times the activity seems to have disappeared behind the camera so when the user dismisses it, it appears the app has crashed.

Having read the above docs I added some logging to the MainActivty android code generated by Cordova so I could observe state changes as the camera gets used, and also added an uncaught exception handler to onCreate:

public class MainActivity extends CordovaActivity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        Log.e("com.xxx.test", "onCreate");
        super.onCreate(savedInstanceState);

        // enable Cordova apps to be started in the background
        Bundle extras = getIntent().getExtras();

        if (extras != null && extras.getBoolean("cdvStartInBackground", false)) {
            moveTaskToBack(true);
        }

        // Set by <content src="index.html" /> in config.xml
        loadUrl(launchUrl);


        Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
            @Override
            public void uncaughtException(Thread paramThread, Throwable paramThrowable) {
                Log.e("com.xxx.test", "onCreate, UNCAUGHT EXCEPTION!!!");
                System.exit(2);
            }
        });
    }

    @Override
    public void onStart() {
        Log.e("com.xxx.test", "onStart");
        super.onStart();

    }

    @Override
    public void onResume() {
        Log.e("com.xxx.test", "onResume");
        super.onResume();

    }

    @Override
    public void onPause() {
        Log.e("com.xxx.test", "onPause");
        super.onPause();
    }

    @Override
    public void onStop() {
        Log.e("com.xxx.test", "onStop");
        super.onStop();
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        Log.e("com.xxx.test", "onDestroy");

    }

    @Override
    public void onSaveInstanceState(Bundle savedInstanceState) {
        Log.e("com.xxx.test", "onSaveInstanceState");
        super.onSaveInstanceState(savedInstanceState);
    }

    @Override
    public void onRestoreInstanceState(Bundle savedInstanceState) {
        Log.e("com.xxx.test", "onRestoreInstanceState");
        super.onRestoreInstanceState(savedInstanceState);
    }
}

I run the app and filter the logcat for lines containing "com.xxx.test" with the following results:

Normal Camera Operation

//camera button tapped
onPause
onSaveInstanceState
onStop

//user takes photo and dismisses camera
onResume

App Restart

So far so good. Now, if the app restarts itself when the camera is dismissed I get:

//camera button tapped
onPause
onSaveInstanceState
onStop

//user takes photo and dismisses camera, app restarts itself here
onCreate
onStart
onRestoreInstanceState
onResume

This is what I would expect to see when the app kills the activity in the background - when the user navigates back to it the the system recreates the activity and loads any saved state etc. This is fine and I will be able to handle the interruption.

Crash

However, sometimes the logs do this:

//camera button tapped
onPause

//sometimes it stops here, sometimes I also get
onSaveInstanceState

//boom! - the app seems to have crashed and the user is taken back to the home screen when dismissing the camera

So, what I suspect is happening is the system is killing the activity before it gets to the stopped state, and isn't saving whatever reference it needs to relaunch it. Then, when we try and go back to that activity the system doesn't know what to load so just exits the app.

I'm not 100% sure that's the case though. Maybe the app's crashing somewhere else and I just don't know about it.

So my question is, if the system kills an activity in the paused state on Android, should we be able to navigate back to it again on demand or is this standard behaviour that the app just exits and the user has to relaunch it? And if so, are there any workarounds?

If the activity should come back without exiting the app, how can I find out what is causing the crash? My exception handler never gets called and I've waded through all the lines in logcat but can't see anything that would give me any clues as to what the problem is.

I've run the app with the profiler and although memory usage is high at around 350mb there don't appear to be any leaks that push it higher and higher as more photos are taken.

Also there doesn't seem to be any regular pattern as to how many photos you need to take to recreate the issue, and sometimes it will restart over and over, some times it will exit over and over and other times I get a mixture.

Any ideas on how I can stop this crash like behaviour much appreciated.

Edit

I've been going through the logs trying to piece together a timeline of what's going on:

So the user taps the camera button in the app and the main activity goes into the paused state and onSaveInstanceState() is called...

16:23:10.244 505-505/com.xxx.test I/chromium: [INFO:CONSOLE(24978)] "photo button tapped", source: file:///android_asset/www/build/main.js (24978)
16:23:10.315 505-505/com.xxx.test E/com.xxx.test: onPause
16:23:10.315 505-505/com.xxx.test D/CordovaActivity: Paused the activity.
16:23:10.322 505-505/com.xxx.test E/com.xxx.test: onSaveInstanceState

After this there's a whole bunch of camera set up logs but in amongst all that the main activity dies, presumably killed by the system to free up resources...

16:23:11.415 3696-6128/? I/WindowManager: WIN DEATH: Window{2ae98e1d0 u0 com.xxx.test/com.xxx.test.MainActivity}
16:23:11.415 3696-4312/? I/ActivityManager: Process com.xxx.test (pid 505) has died(377,151)

Despite onSaveInstanceState() being called, the system claims there is no saved state...

16:23:11.419 3696-4312/? W/ActivityManager: Force removing ActivityRecord{c76b13bd0 u0 com.xxx.test/.MainActivity t2117}: app died, no saved state

Then around half a second later the system starts a new process for my package name while the camera is still visible...

16:23:12.047 3696-9816/? D/MountService: getExternalStorageMountMode : 3
    getExternalStorageMountMode : 3
    getExternalStorageMountMode : final mountMode=3, uid : 10298, packageName : com.xxx.test
16:23:12.061 3696-9816/? I/ActivityManager: Start proc 6426:com.xxx.test/u0a298 for content provider com.xxx.test/android.support.v4.content.FileProvider

Not much else gets logged though for process 6426...

16:23:12.062 6426-6426/? E/Zygote: v2
16:23:12.062 6426-6426/? I/libpersona: KNOX_SDCARD checking this for 10298
16:23:12.063 6426-6426/? I/libpersona: KNOX_SDCARD not a persona
16:23:12.063 6426-6426/? E/Zygote: accessInfo : 0
16:23:12.064 6426-6426/? W/SELinux: SELinux selinux_android_compute_policy_index : Policy Index[2],  Con:u:r:zygote:s0 RAM:SEPF_SECMOBILE_7.0_0007, [-1 -1 -1 -1 0 1]
16:23:12.064 6426-6426/? I/SELinux: SELinux: seapp_context_lookup: seinfo=untrusted, level=s0:c512,c768, pkgname=com.xxx.test
16:23:12.069 6426-6426/? I/art: Late-enabling -Xcheck:jni
16:23:12.098 6426-6426/? D/TimaKeyStoreProvider: TimaKeyStore is not enabled: cannot add TimaSignature Service and generateKeyPair Service

And that's it. Soon though, the ActivityManager tries to move the recreated activity to the front...

16:23:12.212 3696-5435/? D/ActivityManager: moveToFront() : reason=startedActivity setFocusedActivity isAttached=true TaskRecord{d952617d0 #2117 A=com.xxx.test U=0 StackId=1 sz=2}
16:23:12.213 3696-5435/? D/InputDispatcher: Focused application set to: xxxx

Then the camera gets paused, even though it is still visible to the user...

16:23:12.239 30891-30891/? V/Camera6: onPause

The ActivityManager mentions my package name again...

16:23:12.558 3696-6838/? D/ActivityManager: resumeTopActivityInnerLocked() : #1 prevTask=TaskRecord{d952617d0 #2117 A=com.xxx.test U=0 StackId=1 sz=2} next=ActivityRecord{bc76899d0 u0 com.sec.android.app.camera/.AttachActivity t2117} mFocusedStack=ActivityStack{33a525ad0 stackId=1, 2 tasks}

Shortly after, the MultiScreenManagerService complains the task has more than one activity and the ActivityManager starts doing more stuff...

16:23:14.529 3696-13852/? W/MultiScreenManagerService: moveTaskBackToDisplayIfNeeded(): The task has more than one activity
16:23:14.530 3696-13852/? D/ActivityManager: moveToFront() : reason=finishActivity adjustFocus setFocusedActivity isAttached=true TaskRecord{d952617d0 #2117 A=com.xxx.test U=0 StackId=1 sz=2}
16:23:14.535 3696-13852/? D/InputDispatcher: Focused application set to: xxxx
16:23:14.536 3696-13852/? D/ActivityTrigger: ActivityTrigger activityPauseTrigger
16:23:14.544 30891-30891/? V/AttachActivity: onPause
16:23:14.545 3696-5485/? D/ActivityManager: setAppIconInfo(), x : 0, y : 0, width : 0, height : 0, isHomeItem : false
    resumeTopActivityInnerLocked() : #1 prevTask=TaskRecord{d952617d0 #2117 A=com.xxx.test U=0 StackId=1 sz=2} next=ActivityRecord{1da4823d0 u0 com.sec.android.app.camera/.Camera t2117} mFocusedStack=ActivityStack{33a525ad0 stackId=1, 2 tasks}

Eventually, the camera is dismissed by the user...

16:23:14.634 30891-30891/? V/Camera6: finish

At which point we get more complaints from the MultiScreenManagerService and now the ActivityManager wants to move com.sec.android.app.launcher to the front, which I'm guessing is the home screen...

16:23:14.637 3696-9814/? W/MultiScreenManagerService: moveTaskBackToDisplayIfNeeded(): root is not base activity
16:23:14.642 3696-9814/? D/ActivityManager: moveToFront() : reason=finishActivity adjustFocus setFocusedActivity isAttached=true TaskRecord{3965df5d0 #2072 A=com.sec.android.app.launcher U=0 StackId=0 sz=1}
16:23:14.642 3696-9814/? W/MultiScreenManagerService: moveTaskBackToDisplayIfNeeded(): root activity or app is null

There is another log from the ActivityManager talking about a duplicate finish request for the camera...

16:23:14.663 3696-6002/? W/ActivityManager: Duplicate finish request for ActivityRecord{1da4823d0 u0 com.sec.android.app.camera/.Camera t2117 f}

At this point I'm assuming the home screen is being displayed but the ActivityManager has another go with my package...

16:23:14.715 3696-5436/? D/ActivityManager: resumeTopActivityInnerLocked() : #0 prevTask=TaskRecord{d952617d0 #2117 A=com.xxx.test U=0 StackId=1 sz=2} next=ActivityRecord{eed8739d0 u0 com.sec.android.app.launcher/.activities.LauncherActivity t2072} mFocusedStack=ActivityStack{514789d0 stackId=0, 2 tasks}
    applyOptionsLocked(), pendingOptions : null

but to no avail, and then the only references to my activity after that are occasional groups of logs like...

16:23:14.844 3696-3707/? D/PackageManager: getComponentMetadataForIconTray : com.xxx.test.MainActivity does not exist in mServices
    getComponentMetadataForIconTray : com.xxx.test.MainActivity does not exist in mProviders
    getComponentMetadataForIconTray : com.xxx.test.MainActivity does not exist in mReceivers
16:23:14.846 4439-4439/? I/ApplicationPackageManager: load=com.xxx.test, bg=192-192, dr=192-192, forDefault=true
    reset dr=192,192, bg=192,192

So, somewhere in there lies the problem. The system seems to try and restart my activity while the camera is still visible and this screws things up but I'm still not exactly sure what's going on or what I can do about it.

Anybody got any ideas?


Solution

  • For @mushishi78 and anybody else who may come across this issue, I did eventually find out what the problem was in my case.

    In my app I use a drawing library called konva.js that lets the user create drawings and diagrams with their finger on an HTML canvas.

    The component I made to use this library has properties for various aspects of the drawing i.e. the main stage, some layers, nodes etc which get instantiated at initialisation.

    After doing lots of testing I realised that the issue with the camera was only happening after the user had interacted with this diagram component so started looking in there.

    I noticed that I hadn't explicitly deallocated the properties that held references to the layers and so on as I just expected the garbage collection to sort it out.

    So, for every property I made sure when ionViewWillLeave() is called that I removed any references that might be hanging around:

    @Component({
      templateUrl: 'chamber-sketch-page.html'
    })
        
    export class ChamberSketchPage {
    
      // properties declared here
    
      @ViewChild('konvaStage') stageElementRef: ElementRef;
    
        stage: any;
        backgroundLayer: any;
    
      ...
    }
    
    ...
    
    ngAfterViewInit() {
    
      // properties instantiated here and listeners set up
    
      this.stage = new Konva.Stage({
        container: "konvaStage",
        width: window.innerWidth,
        height: window.innerWidth
      });
    
        this.backgroundLayer = new Konva.Layer({
            name: "background"
        });
    
      this.platform.ready().then(() => {
            window.addEventListener('native.keyboardshow', this.keyboardShowHandler);
            window.addEventListener('native.keyboardhide', this.keyboardHideHandler);
        });
    }
    
    ...
    
    ionViewWillLeave() : void {
    
      // clean up everything possible here - listeners, properties etc
    
      window.removeEventListener('native.keyboardshow', this.keyboardShowHandler);
      window.removeEventListener('native.keyboardhide', this.keyboardHideHandler);
    
      this.stage.destroy()
      this.stage = null
    
      this.backgroundLayer.destroy()
      this.backgroundLayer = null
    
      this.stageElementRef.nativeElement.remove();
    }
    

    I don't know exactly which item was causing the problem, but once I went through and made sure that everything that was created at the beginning was removed at the end, my app started working properly.

    My guess is that after the component was dismissed there were references left lying around that shouldn't have been there which caused some strange memory voodoo in the system somewhere, perhaps via one of the plugins, and that's what caused the crash.

    We'll never know, but my advice to anybody having this issue would be to make sure you clean up any objects you instantiate once the component is not needed any more.