[SOLVED] Camera plugin restart App on Android

Following this Android 7.1 (Google Pixel) Camera plugin broken? topic, I made some deeper test of the Camera plugin

On a Samsung Edge with Android 6.x, it works like a charm

On a small Sony with Android 7.1.1, it works like a charm

On my Nexus 5X with Android 8.1. It works most of the time but the camera lag and even restart the app after having take a photo. Overall my phone is a bit laggy, therefore it’s hard to say what’s the actual problem but it’s really unpleasant to use the camera

Do someone has an Android 8.1 (real) phone?
Do you encounter any lagging/delay/bad experiences/restart of the app while calling/using the camera with the cordova-plugin-camera?

p.s.: I use cordova-plugin-camera@3.0.0 and cordova-android@6.4.0

2 Likes

It looks like a memory problem.

I have build a simple app where I take photo (full quality) and save the FILE_URI in a array which are then displayed as img. This works fine. Camera is a bit laggy but not more that the overall phone.

However, when I implement the same code in my app, I often face problem that the app itself restart after having selected/confirmed the photo

After digging a bit, I kind of think that the problem is the result of a lack of memory on the phone.

I found this particular Cordova documentation which describe, I guess, what’s happening

http://cordova.apache.org/docs/en/dev/guide/platforms/android/index.html#what-makes-android-different

P.S.: Earlier I thought that the problem was that I use cropper.js to crop the result but that was false. I definitely looks like a memory problem effect since the app is restarting most of the time

Ok so according the cordova documentation, the only way to handle this, is to save the state and to catch it onResume, to recreate it, when app restart after having been put in the background by the os

Unfortunately it looks like it isn’t correct, onResume is never thrown

https://issues.apache.org/jira/browse/CB-10950

I began to hate this issue I’ve to say…

Tried:

  • onResume like described by cordova
    a. Firstly it doesn’t work
    b. Secondly that can’t be a solution, I mean I use the camera after setRoot, push, loader slider, slide 3, load modal…what a user experience if all these have to be reload/skipped or whatever

  • I tried the MediaCapture plugin Camera plugin for android devices? as described by @rapropos, same issue

  • I tried to reduce the image quality (play with quality, targetWidth, targetHeight of the camera option). Well yes, it works, but at what price? I have to set a really shitty quality which is also not acceptable for the users. My app is a marketplace, I can’t display bad quality pictures because of an android bug/weird effect, make no sense

  • I tried to destroy some variables (in case the garbage collector would not work) to reduce the memory because I noticed that most of the time, the camera works the first time but not later on (user could take till 5 photos in my app). Didn’t worked neither

1 Like

Ok, so fck it, the best I could do right now is minimizing the problem, that s*cks.

I did the following:

  1. Set a reduced targetWidth and targetHeight in the options of the camera (see all options below). As I said above, I have to keep a good quality. I still use 100% as quality because I’m also using cropper.js (during the cropping I gonna reduce the quality to 92% something)

  2. Delete/clean the memory by my own when I close the modal where take/edit/crop photo aka hope to reduce the app memory to avoid the cordova/android/background/bug/whatever restart app problem.

Afterwards, I was still able to reproduce the freaking problem, but at least a bit less often.

I keep this issue open. If someone has got a good idea or tips, plz speak out loud, that would be really appreciated and awesome.

Cordova options:

    let popoverOptions: CameraPopoverOptions = {
        x: 0,
        y: 0,
        width: 1080,
        height: 1080,
        arrowDir: this.camera.PopoverArrowDirection.ARROW_ANY
    };

    let options: CameraOptions = {
        quality: 100,
        destinationType: this.camera.DestinationType.FILE_URI,
        sourceType: sourceType,
        allowEdit: false,
        encodingType: this.camera.EncodingType.JPEG,
        targetWidth: 1280,
        targetHeight: 1280,
        mediaType: this.camera.MediaType.PICTURE,
        correctOrientation: true,
        saveToPhotoAlbum: false,
        cameraDirection: this.camera.Direction.BACK,
        popoverOptions: popoverOptions
    };

Cleanup on modal close:

private cleanup() {
    if (this.cropper != null) {
        this.cropper.destroy();
    }

    this.camera.cleanup().then(() => {
        // Do nothing
    });
}

P.S.: The introduction of cleanup introduced some cache problem in the view on iOS. I had to add a random number to the FILE_URI to prevent this, see https://stackoverflow.com/a/27164586/5404186

2 Likes

Two new things interesting:

  1. It’s easy to reproduce/simulate the problem. To do so, you have to set “always destroy activities” to true in the developer settings of your Android phone. Doing so, each time you gonna take a photo, your app in the background gonna be killed and recreated (as cordova does when an Android phone is running on low memory)

  2. I was wrong, the RESUME event is trigged. See following post to see how to catch it document.addEventListener event resume provider undefined

  3. UPDATE but that isn’t enough if you are using the cordova-plugin-facebook4 plugin. In such a case, the resumed event gonna be null. To handle correctly the resumed event value you have to modify/tweek a line of code in \platforms\android\CordovaLib\src\org\apache\cordova\CordovaInterfaceImpl.java see the amazing discovery of @peterPP1 on stackoverflow https://stackoverflow.com/a/46883936/5404186

I’ve finally decided to implement this crazy workaround to handle the cordova lifecycle in case the android device run on low memory

Here are some keys of the implementation:

  1. It’s important to note that you could play with the quality of the photo and the size to reduce the memory used. Same for your application. But at some point, you can’t be sure that none of your users gonna face this problem. That’s why I have decided to implement this.

  2. I don’t wanted to have a side effect on my iOS bundle which just works fine, therefore I mostly added the code under check of which platform is running aka this.platform.is('android')

  3. This workaround isn’t a nice user experience, therefore, at least, the user should not see the app navigating by herself. To do so, you have to take care manually of the splash screen respectively display your splash screen till your app have handled the restart and reloading the image and then hide manually the splash screen splashScreen.hide()

  4. To easily reproduce the problem, you have to set “always destroy activities” to true in the developer settings of your Android phone. To face the real problem on my Nexus 5X with Android 8.1.1, I just run one or two other apps in the same time as mine, like Spotify

Pseudo-code:

  1. Where you are using the camera, before taking a picture, you have to save in the storage the current state of the object(s) you are modifying. In my case let’s say I want to add a photo to an object

    private saveForRecovery(): Promise<{}> {
       return new Promise((resolve) => {
           if (this.platform.is('android')) {
               this.storage.set('key', this.myObject).then((recover: any) => {
                  resolve();
              });
           } else {
               resolve();
           }
       });
    }
    
    takePhoto() {
        this.saveForRecovery().then(() => {
             this.camera.getPicture(options).then((imageURI: string) => {
                 // Handle imageURI as usual
             });
        });
    }
    
  2. Now you have to handle the restart of the app respectively handle the restart of the app after android had put it in the background and to check if this happens after having take a photo and if the image URI is provided as fallback. Furthermore, if this happens, save the URI in a provider to be handled. To do so modify app.component.ts for example

Note: If you are debugging with “always destroy activities” set to true, the status of the pendingResult gonna be ‘OK’ and not ‘KO’. Just for testing purpose change 'OK' !== status with 'OK' === status

     constructor(platform: Platform, myService: MyService) {
          if (this.platform.is('android')) {
               this.platform.resume.subscribe((event:any) => {
            this.handleAndroidCameraRestart(event)
          });
       }
     }
    
    private handleAndroidCameraRestart(event: any) {
        if (event && event.pendingResult) {
          const status: string = event.pendingResult.pluginStatus !== null ? '' : event.pendingResult.pluginStatus.toUpperCase();

           if ('Camera' === event.pendingResult.pluginServiceName && 'OK' !== status && event.pendingResult.result !== '') {
             this.myService.saveAndroidPhotoRecoveryURI(event.pendingResult.result);
           }
        }
     }
  1. In my app the login is mandatory and I process the automatic login once this app is ready. Since I want to handle the restart, I had to improve my login to check if we recover from a crash. To do so, I check if I have an URI (image) to recover and furthermore if I found data in the storage to recover my object(s) too.

If it’s the case. Then you will have to navigate thru your app. In my case I have to do a setRoot, then there to do the same check and slide my slider and then do again the same check to open the modal where I take the photo

Something like

     this.platform.ready().then(() => {
           let uri: string = this.myService.getAndroidPhotoRecoveryURI();
           if (uri !== '') {
                  this.storage.get('key').then((myObject: any) => {
                       if (myObject) {
                            // Ok we have an URI and something in the storage, we could recover
                      } else {
                          // We can't recover we've got nothing in the storage, therefore process as usual. That gonna sucks for the user and you may lost him/her forever
                      }
                  });
           } else {
               // process as usual
           }
     });
  1. So finally you were able to open the right page/modal in you app, now you just have to load the imageURI and hide the splash screen.

It’s also important to reset the value in the provider and in the storage, you don’t want this all workaround to be processed again in case the user restart the app manually.

    if (this.platform.is('android') && this.myService.getAndroidPhotoRecoveryURI() !== '') {
         // this.imgURI is the URI I use in the html page to display the photo
        this.imgURI = this.myService.getAndroidPhotoRecoveryURI();
        this.myService.saveAndroidPhotoRecoveryURI(null);
        this.storage.remove('key').then((whatever:any) => {
            this.splashScreen.hide();
        });
    }

That’s it, you have build a gigantic kind of ugly workaround. At least now, if a user is running an android on low memory, if the app restart after the the user have taken a photo, he won’t have the feeling he lost everything, just the feeling that the app is a bit shitty because it took a while to process it.

Finally, I hope that in the future, live with capacitor, it will be possible to avoid this workaround.

6 Likes

I almost forgot, if you are using cordova-plugin-facebook4 you have to modify manually CordovaInterfaceImpl.java in order to get the imageURI fallback from cordova.

Or you could write a hook like:

const replace = require('replace-in-file');

module.exports = function (context) {

    const options = {
        files: './platforms/android/CordovaLib/src/org/apache/cordova/CordovaInterfaceImpl.java',

        //Replacement to make (string or regex)
        from: 'if(callback == null && initCallbackService != null)',
        to: 'if( initCallbackService != null)',
    };

    try {
        let changedFiles = replace.sync(options);
        console.log('Modified files:', changedFiles.join(', '));
    }
    catch (error) {
        console.error('Error occurred:', error);
    }

};

For the record, here a simple demo repo:

Branch “master” to reproduce the problem, branch “quirks” to handle it

@reedrichards do you think would be possible to have open a PR to this lib with your solution? It might be quite helpful for a lot of people.

Also, why is facebook plugin related with that?

@Xiwi a PR to which project?

Anyway not sure about that, the common part which could be use in any projects would be only a provider and the resume method I guess, the rest need to be tailored for each projects

About the Facebook plugin:

First if you don’t use it, you could forget it, it has nothing to do with that

But, if you do use this plugin, you will need to modify one line of code in CordovaInterfaceImpl.java otherwise the resume event not gonna be propagated correctly aka even if you subscribe to this event in your app, nothing would happens

A PR to the project where CordovaInterfaceImpl.java belongs.
Yes, I use Facebook plugin and I get this error. Also changed this line but still getting the same error tho

Oh you mean in cordova-android, well I think there is absolutely no chance for a PR about this to be accepted, but feel free to do one, be my guest :wink:

“I get this error”!??! which error?

So even after changed this line on that file I still getting the error with the camera: it restarts the whole app.

I haven’t changed “save in the storage the current state of the object(s) I am modifying” or any other files.

Maybe a misunderstand but the app will always “restart” or comeback from a pause state to a resume state, that’s the design of Android and Cordova could nothing about this

The tricks here is to catch the resume event and to handle a resumed value provided by cordova which gonna contains the photo url which was taken

Uncommenting the line is only mandatory if you use the facebook4 plugin in order to be able to catch the resume event

But again, the app gonna “restart” aka comeback from “pause” to “resume”

I don’t know what the problem is then…
Would be helpful to open an issue on facebook4 repo?

Not sure if I’m missing some other changes here to get this working

well you could try but honestly I think you might get no answer

but I don’t get your problem, like I said, the app gonna restart in anycase

maybe you could try to open a new issue on the forum (link it here), document it and also display a small video of what’s your problem

use “cordova-plugin-background-mode”
Link: https://ionicframework.com/docs/native/background-mode/
https://github.com/katzer/cordova-plugin-background-mode

this plugin can prevent the app from going to sleep while in background.
so it not be killed by the system

to me that sounds like to a particular ugly workaround

I rather like to stick to the official way to handle the restart of the camera provided by Cordova (Android Platform Guide - Apache Cordova)

I found this post

1 Like