Skip to content

Commit

Permalink
Add onUserLeaveHint support to ReactActivityDelegate (#42741)
Browse files Browse the repository at this point in the history
Summary:
This PR adds `onUserLeaveHint` support into the `ReactActivityDelegate`. It allows modules to receive an event every time user moves the app into the background. This is slightly different than `onPause` - it's called only when the user intentionally moves the app into the background, e.g. when receiving a call `onPause` should be called but `onUserLeaveHint` shouldn't.

This feature is especially useful for libraries implementing features like Picture in Picture (PiP), where using `onUserLeaveHint` is the [recommended way of auto-entering PiP](https://developer.android.com/develop/ui/views/picture-in-picture#:~:text=You%20might%20want%20to%20include%20logic%20that%20switches%20an%20activity%20into%20PiP%20mode%20instead%20of%20going%20into%20the%20background.%20For%20example%2C%20Google%20Maps%20switches%20to%20PiP%20mode%20if%20the%20user%20presses%20the%20home%20or%20recents%20button%20while%20the%20app%20is%20navigating.%20You%20can%20catch%20this%20case%20by%20overriding%20onUserLeaveHint()%3A) for android < 12.

## Changelog:

[ANDROID] [ADDED] - Added `onUserLeaveHint` support into `ReactActivityDelegate`

Pull Request resolved: #42741

Test Plan: Tested in the `rn-tester` app - callbacks are correctly called on both old and new architecture.

Reviewed By: javache

Differential Revision: D53279501

Pulled By: cortinico

fbshipit-source-id: 491fc062421da7e05b78dc818b22cd1ee79af791
  • Loading branch information
behenate authored and facebook-github-bot committed Feb 1, 2024
1 parent aefca27 commit 3b6c522
Show file tree
Hide file tree
Showing 9 changed files with 112 additions and 31 deletions.
8 changes: 8 additions & 0 deletions packages/react-native/ReactAndroid/api/ReactAndroid.api
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ public abstract class com/facebook/react/ReactActivity : androidx/appcompat/app/
protected fun onPause ()V
public fun onRequestPermissionsResult (I[Ljava/lang/String;[I)V
protected fun onResume ()V
public fun onUserLeaveHint ()V
public fun onWindowFocusChanged (Z)V
public fun requestPermissions ([Ljava/lang/String;ILcom/facebook/react/modules/core/PermissionListener;)V
}
Expand Down Expand Up @@ -130,6 +131,7 @@ public class com/facebook/react/ReactActivityDelegate {
protected fun onPause ()V
public fun onRequestPermissionsResult (I[Ljava/lang/String;[I)V
protected fun onResume ()V
protected fun onUserLeaveHint ()V
public fun onWindowFocusChanged (Z)V
public fun requestPermissions ([Ljava/lang/String;ILcom/facebook/react/modules/core/PermissionListener;)V
}
Expand All @@ -154,6 +156,7 @@ public class com/facebook/react/ReactDelegate {
public fun onHostDestroy ()V
public fun onHostPause ()V
public fun onHostResume ()V
public fun onUserLeaveHint ()V
public fun shouldShowDevMenuOrReload (ILandroid/view/KeyEvent;)Z
}

Expand Down Expand Up @@ -199,6 +202,7 @@ public abstract interface class com/facebook/react/ReactHost {
public abstract fun onBackPressed ()Z
public abstract fun onHostDestroy ()V
public abstract fun onHostDestroy (Landroid/app/Activity;)V
public abstract fun onHostLeaveHint (Landroid/app/Activity;)V
public abstract fun onHostPause ()V
public abstract fun onHostPause (Landroid/app/Activity;)V
public abstract fun onHostResume (Landroid/app/Activity;)V
Expand Down Expand Up @@ -240,6 +244,7 @@ public class com/facebook/react/ReactInstanceManager {
public fun onHostResume (Landroid/app/Activity;)V
public fun onHostResume (Landroid/app/Activity;Lcom/facebook/react/modules/core/DefaultHardwareBackBtnHandler;)V
public fun onNewIntent (Landroid/content/Intent;)V
public fun onUserLeaveHint (Landroid/app/Activity;)V
public fun onWindowFocusChange (Z)V
public fun recreateReactContextInBackground ()V
public fun removeReactInstanceEventListener (Lcom/facebook/react/ReactInstanceEventListener;)V
Expand Down Expand Up @@ -474,6 +479,7 @@ public class com/facebook/react/animated/NativeAnimatedNodesManager : com/facebo
public abstract interface class com/facebook/react/bridge/ActivityEventListener {
public abstract fun onActivityResult (Landroid/app/Activity;IILandroid/content/Intent;)V
public abstract fun onNewIntent (Landroid/content/Intent;)V
public fun onUserLeaveHint (Landroid/app/Activity;)V
}

public class com/facebook/react/bridge/Arguments {
Expand Down Expand Up @@ -1092,6 +1098,7 @@ public class com/facebook/react/bridge/ReactContext : android/content/ContextWra
public fun onHostPause ()V
public fun onHostResume (Landroid/app/Activity;)V
public fun onNewIntent (Landroid/app/Activity;Landroid/content/Intent;)V
public fun onUserLeaveHint (Landroid/app/Activity;)V
public fun onWindowFocusChange (Z)V
public fun registerSegment (ILjava/lang/String;Lcom/facebook/react/bridge/Callback;)V
public fun removeActivityEventListener (Lcom/facebook/react/bridge/ActivityEventListener;)V
Expand Down Expand Up @@ -3621,6 +3628,7 @@ public class com/facebook/react/runtime/ReactHostImpl : com/facebook/react/React
public fun onBackPressed ()Z
public fun onHostDestroy ()V
public fun onHostDestroy (Landroid/app/Activity;)V
public fun onHostLeaveHint (Landroid/app/Activity;)V
public fun onHostPause ()V
public fun onHostPause (Landroid/app/Activity;)V
public fun onHostResume (Landroid/app/Activity;)V
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,12 @@ public void onNewIntent(Intent intent) {
}
}

@Override
public void onUserLeaveHint() {
super.onUserLeaveHint();
mDelegate.onUserLeaveHint();
}

@Override
public void requestPermissions(
String[] permissions, int requestCode, PermissionListener listener) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,10 @@ protected void loadApp(String appKey) {
getPlainActivity().setContentView(mReactDelegate.getReactRootView());
}

protected void onUserLeaveHint() {
mReactDelegate.onUserLeaveHint();
}

protected void onPause() {
mReactDelegate.onHostPause();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,16 @@ public void onHostResume() {
}
}

public void onUserLeaveHint() {
if (ReactFeatureFlags.enableBridgelessArchitecture) {
mReactHost.onHostLeaveHint(mActivity);
} else {
if (getReactNativeHost().hasInstance()) {
getReactNativeHost().getReactInstanceManager().onUserLeaveHint(mActivity);
}
}
}

public void onHostPause() {
if (ReactFeatureFlags.enableBridgelessArchitecture) {
mReactHost.onHostPause(mActivity);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,12 @@ interface ReactHost {
/** To be called when the host activity is resumed. */
fun onHostResume(activity: Activity?)

/**
* To be called when the host activity is about to go into the background as the result of user
* choice.
*/
fun onHostLeaveHint(activity: Activity?)

/** To be called when the host activity is paused. */
fun onHostPause(activity: Activity?)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -585,6 +585,38 @@ public void onHostPause() {
moveToBeforeResumeLifecycleState();
}

/**
* This method should be called from {@link Activity#onUserLeaveHint()}. It notifies all listening
* modules that the user is about to leave the activity. The passed Activity is has to be the
* current Activity.
*
* @param activity the activity being backgrounded as a result of user action
*/
@ThreadConfined(UI)
public void onUserLeaveHint(@Nullable Activity activity) {
if (mRequireActivity) {
Assertions.assertCondition(mCurrentActivity != null);
}

if (mCurrentActivity != null) {
Assertions.assertCondition(
activity == mCurrentActivity,
"Called onUserLeaveHint on an activity that is not the current activity, this is incorrect! "
+ "Current activity: "
+ mCurrentActivity.getClass().getSimpleName()
+ " "
+ "Leaving activity: "
+ activity.getClass().getSimpleName());

UiThreadUtil.assertOnUiThread();

ReactContext currentContext = getCurrentReactContext();
if (currentContext != null) {
currentContext.onUserLeaveHint(activity);
}
}
}

/**
* Call this from {@link Activity#onPause()}. This notifies any listening modules so they can do
* any necessary cleanup. The passed Activity is the current Activity being paused. This will
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,7 @@ public interface ActivityEventListener {

/** Called when a new intent is passed to the activity */
void onNewIntent(Intent intent);

/** Called when host activity receives an {@link Activity#onUserLeaveHint()} call. */
default void onUserLeaveHint(Activity activity) {};
}
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,17 @@ public void onHostResume(@Nullable Activity activity) {
ReactMarker.logMarker(ReactMarkerConstants.ON_HOST_RESUME_END);
}

@ThreadConfined(UI)
public void onUserLeaveHint(@Nullable Activity activity) {
for (ActivityEventListener listener : mActivityEventListeners) {
try {
listener.onUserLeaveHint(activity);
} catch (RuntimeException e) {
handleException(e);
}
}
}

@ThreadConfined(UI)
public void onNewIntent(@Nullable Activity activity, Intent intent) {
UiThreadUtil.assertOnUiThread();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -276,8 +276,7 @@ public void onHostResume(
@ThreadConfined(UI)
@Override
public void onHostResume(final @Nullable Activity activity) {
final String method = "onHostResume(activity)";
log(method);
log("onHostResume(activity)");

setCurrentActivity(activity);
ReactContext currentContext = getCurrentReactContext();
Expand All @@ -286,11 +285,21 @@ public void onHostResume(final @Nullable Activity activity) {
mReactLifecycleStateManager.moveToOnHostResume(currentContext, getCurrentActivity());
}

@ThreadConfined(UI)
@Override
public void onHostLeaveHint(final @Nullable Activity activity) {
log("onUserLeaveHint(activity)");

ReactContext currentContext = getCurrentReactContext();
if (currentContext != null) {
currentContext.onUserLeaveHint(activity);
}
}

@ThreadConfined(UI)
@Override
public void onHostPause(final @Nullable Activity activity) {
final String method = "onHostPause(activity)";
log(method);
log("onHostPause(activity)");

ReactContext currentContext = getCurrentReactContext();

Expand All @@ -317,8 +326,7 @@ public void onHostPause(final @Nullable Activity activity) {
@ThreadConfined(UI)
@Override
public void onHostPause() {
final String method = "onHostPause()";
log(method);
log("onHostPause()");

ReactContext currentContext = getCurrentReactContext();

Expand All @@ -331,8 +339,7 @@ public void onHostPause() {
@ThreadConfined(UI)
@Override
public void onHostDestroy() {
final String method = "onHostDestroy()";
log(method);
log("onHostDestroy()");

// TODO(T137233065): Disable DevSupportManager here
moveToHostDestroy(getCurrentReactContext());
Expand All @@ -341,8 +348,7 @@ public void onHostDestroy() {
@ThreadConfined(UI)
@Override
public void onHostDestroy(@Nullable Activity activity) {
final String method = "onHostDestroy(activity)";
log(method);
log("onHostDestroy(activity)");

Activity currentActivity = getCurrentActivity();

Expand Down Expand Up @@ -475,12 +481,11 @@ public TaskInterface<Void> reload(String reason) {
*/
@Override
public TaskInterface<Void> destroy(String reason, @Nullable Exception ex) {
final String method = "destroy()";
return Task.call(
() -> {
if (mReloadTask != null) {
log(
method,
"destroy()",
"Reloading React Native. Waiting for reload to finish before destroying React Native.");
return mReloadTask.continueWithTask(
task -> getOrCreateDestroyTask(reason, ex), mBGExecutor);
Expand Down Expand Up @@ -641,26 +646,23 @@ DefaultHardwareBackBtnHandler getDefaultBackButtonHandler() {
*/
/* package */ Task<Boolean> callFunctionOnModule(
final String moduleName, final String methodName, final NativeArray args) {
final String method = "callFunctionOnModule(\"" + moduleName + "\", \"" + methodName + "\")";
return callWithExistingReactInstance(
method,
"callFunctionOnModule(\"" + moduleName + "\", \"" + methodName + "\")",
reactInstance -> {
reactInstance.callFunctionOnModule(moduleName, methodName, args);
});
}

/* package */ void attachSurface(ReactSurfaceImpl surface) {
final String method = "attachSurface(surfaceId = " + surface.getSurfaceID() + ")";
log(method);
log("attachSurface(surfaceId = " + surface.getSurfaceID() + ")");

synchronized (mAttachedSurfaces) {
mAttachedSurfaces.add(surface);
}
}

/* package */ void detachSurface(ReactSurfaceImpl surface) {
final String method = "detachSurface(surfaceId = " + surface.getSurfaceID() + ")";
log(method);
log("detachSurface(surfaceId = " + surface.getSurfaceID() + ")");

synchronized (mAttachedSurfaces) {
mAttachedSurfaces.remove(surface);
Expand Down Expand Up @@ -707,9 +709,8 @@ public void removeBeforeDestroyListener(@NonNull Function0<Unit> onBeforeDestroy

@ThreadConfined("ReactHost")
private Task<Void> getOrCreateStartTask() {
final String method = "getOrCreateStartTask()";
if (mStartTask == null) {
log(method, "Schedule");
log("getOrCreateStartTask()", "Schedule");
mStartTask =
waitThenCallGetOrCreateReactInstanceTask()
.continueWithTask(
Expand Down Expand Up @@ -756,15 +757,16 @@ private void raiseSoftException(String method, String message, @Nullable Throwab

private Task<Boolean> callWithExistingReactInstance(
final String callingMethod, final VeniceThenable<ReactInstance> continuation) {
final String method = "callWithExistingReactInstance(" + callingMethod + ")";

return mReactInstanceTaskRef
.get()
.onSuccess(
task -> {
final ReactInstance reactInstance = task.getResult();
if (reactInstance == null) {
raiseSoftException(method, "Execute: ReactInstance null. Dropping work.");
raiseSoftException(
"callWithExistingReactInstance(" + callingMethod + ")",
"Execute: ReactInstance null. Dropping work.");
return FALSE;
}

Expand All @@ -776,15 +778,16 @@ private Task<Boolean> callWithExistingReactInstance(

private Task<Void> callAfterGetOrCreateReactInstance(
final String callingMethod, final VeniceThenable<ReactInstance> runnable) {
final String method = "callAfterGetOrCreateReactInstance(" + callingMethod + ")";

return getOrCreateReactInstance()
.onSuccess(
(Continuation<ReactInstance, Void>)
task -> {
final ReactInstance reactInstance = task.getResult();
if (reactInstance == null) {
raiseSoftException(method, "Execute: ReactInstance is null");
raiseSoftException(
"callAfterGetOrCreateReactInstance(" + callingMethod + ")",
"Execute: ReactInstance is null");
return null;
}

Expand All @@ -803,10 +806,9 @@ private Task<Void> callAfterGetOrCreateReactInstance(
}

private BridgelessReactContext getOrCreateReactContext() {
final String method = "getOrCreateReactContext()";
return mBridgelessReactContextRef.getOrCreate(
() -> {
log(method, "Creating BridgelessReactContext");
log("getOrCreateReactContext()", "Creating BridgelessReactContext");
return new BridgelessReactContext(mContext, ReactHostImpl.this);
});
}
Expand Down Expand Up @@ -929,7 +931,7 @@ class Result {
final boolean isManagerResumed =
mReactLifecycleStateManager.getLifecycleState() == LifecycleState.RESUMED;

/**
/*
* ReactContext.onHostResume() should only be called when the user navigates to
* the first React Native screen.
*
Expand All @@ -953,7 +955,7 @@ class Result {
mReactLifecycleStateManager.moveToOnHostResume(
reactContext, getCurrentActivity());
} else {
/**
/*
* Call ReactContext.onHostResume() only when already in the resumed state
* which aligns with the bridge https://fburl.com/diffusion/2qhxmudv.
*/
Expand All @@ -979,8 +981,7 @@ class Result {
}

private Task<JSBundleLoader> getJsBundleLoader() {
final String method = "getJSBundleLoader()";
log(method);
log("getJSBundleLoader()");

if (DEV && mAllowPackagerServerAccess) {
return isMetroRunning()
Expand Down Expand Up @@ -1012,7 +1013,7 @@ private Task<JSBundleLoader> getJsBundleLoader() {

private Task<Boolean> isMetroRunning() {
final String method = "isMetroRunning()";
log(method);
log("isMetroRunning()");

final TaskCompletionSource<Boolean> taskCompletionSource = new TaskCompletionSource<>();
final DevSupportManager asyncDevSupportManager = getDevSupportManager();
Expand Down

0 comments on commit 3b6c522

Please sign in to comment.