Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Prevent unwanted app destroy on Android #2043

Closed
wants to merge 9 commits into from
Closed

Conversation

eeynard
Copy link
Contributor

@eeynard eeynard commented Oct 19, 2017

Hi,

I was running into an annoying issue while trying to ask user permissions in my home screen on android (I have a map on the home screen so I need to ask the user for his location). I am using react-native-permissions and since it use react-native PermissionsAndroid API you must call the Permissions.request function after starting an activity or you will end up with the error Tried to use permissions API while not attached to an Activity.

The problem is that even when you call the Permissions.request function in the componentDidMount callback of your home screen, your app ends up crashing when the user accept or decline the permission with the error shown above.

So after some investigations I realize that when the native permissions pop up shows up, the app is put in pause and the currentActivity is null. I forgot to mention that I have a Login flow first and at the end I call startSingleScreenApp so the first activity is destroyed and replaced by the new one. The problem here is that the onDestroy callback is called after the onPause of the new screen. So when is get trough destroyJsIfNeeded function, currentActivity is null and the app is destroyed, causing the Tried to use permissions API while not attached to an Activity.

I apologize if it is not very clear but it is pretty hard to explain !
I don't have a global view of the project so please tell me if I am doing something wrong.

@@ -133,6 +135,7 @@ protected void onNewIntent(Intent intent) {
@Override
protected void onPause() {
super.onPause();
lastActivity = currentActivity;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're leaking the activity by saving it in a static field after onPause.

Note that currentActivity is static, onDestroy of first activity is called after onResume of second activity. Perhaps we can change the conditional in destroyJsIfNeeded to to:
(currentActivity == null || (currentActivity.isFinishing() && currentActivity == this)) this way onDestroyApp is called only if the finishing activity is the current activity.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh I see ..

The problem is not related to the second part of the conditional but with the first one: currentActivity == null In fact, if the app is paused between the second screen onResume and the first screen onDestroy the currentActivity will be null and de app will be destroyed.

I will try to find another way to handle this case.

@guyca guyca self-assigned this Oct 19, 2017
@eeynard
Copy link
Contributor Author

eeynard commented Oct 19, 2017

@guyca what about this approach ? Not the best code I wrote but I don't see any other straightforward solution.

@guyca
Copy link
Collaborator

guyca commented Oct 19, 2017

The issue is basically that the react context is lost since host.clear() is called from, right?
You can override clearHostOnActivityDestroy in MainApplication and return false
Can you true that before we merge you pr?

@eeynard
Copy link
Contributor Author

eeynard commented Oct 19, 2017

I already tried that solution and it partially worked, in fact the app does not crash when the permissions are accepted/declined and you can use it without any problem but when you close it with the back button and try to relaunch it, it gets stucked on the SplashScreen. Is it another issue ? Or maybe I miss used this clearHostOnActivityDestroy ?

@guyca
Copy link
Collaborator

guyca commented Oct 22, 2017

Did you modify your Js code accordingly?

  Promise.resolve(Navigation.isAppLaunched())
      .then((appLaunched) => {
        if (appLaunched) {
          startApp();
        }
        new NativeEventsReceiver().appLaunched(() => {
          // The event is dispatched from SplashActivity and starts the app.
          // This flow happens when the app was started while react context was already initialized
          startApp();
        });
      })

I noticed you're incrementing activity count in onCreate and decrementing in onDestroy. This doesn't seem like what you want as the activity can be recreated after onStop

@@ -151,6 +153,7 @@ protected void onDestroy() {
destroyLayouts();
destroyJsIfNeeded();
NavigationApplication.instance.getActivityCallbacks().onActivityDestroyed(this);
numberOfActivities -= 1;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I noticed you're incrementing activity count in onCreate and decrementing in onDestroy. This doesn't seem like what you want as the activity can be recreated after onStop

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You’re right, I was looking at the android Activity Lifecycle and I am afraid I can’t find any way to reliably use this counter.

@eeynard
Copy link
Contributor Author

eeynard commented Oct 25, 2017

@guyca I tried your advice and it seems to work on Android :)
To recap, from a clean react-native-navigation version (I mean without the stuff in my PR):

  • I overrided clearHostOnActivityDestroy in my MainApplication.java
@Override
public boolean clearHostOnActivityDestroy() {
    return false;
}
  • I changed my index.js for:
import 'react-native';
import { Navigation, NativeEventsReceiver } from 'react-native-navigation';

import app from './src/app';

Promise.resolve(Navigation.isAppLaunched())
.then((appLaunched) => {
  if (appLaunched) {
    app();
  }
  new NativeEventsReceiver().appLaunched(() => {
    app();
  });
});

And everything seems OK ! Except for iOS which throw TypeError: _platformSpecificDeprecated2.default.isAppLaunched is not a function Should we add a dummy function to src/platformSpecificDeprecated.ios.js which always return true:

async function isAppLaunched() {
  return Promise.resolve(true);
}

or should I update my code and have a different implementation for Android and iOS ?

@eeynard
Copy link
Contributor Author

eeynard commented Oct 31, 2017

This PR is no longer justified, I close it. Thanks for the help @guyca :)

@eeynard eeynard closed this Oct 31, 2017
@guyca
Copy link
Collaborator

guyca commented Nov 6, 2017

Hey @eeynard
Just saw your comment, sorry for the delay. I don't think we should add a dummy function as that might be misleading. Glad it worked out.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants