From 0e3764c8aa75e13fac684c27474f1136bf70d0fe Mon Sep 17 00:00:00 2001 From: Alan Lee Date: Thu, 8 May 2025 08:50:45 -0700 Subject: [PATCH] migrate deprecated onBackPressed() (#51096) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/51096 **Problem:** `Activity.onBackPressed()` has been deprecated and with targetSdk 36, predictive back will be enforced and the API no longer called. We need to migrate to backward compatible AndroidX `OnBackPressedCallback`. - https://developer.android.com/about/versions/16/behavior-changes-16#predictive-back. **Solution:** `OnBackPressedCallback` is registered conditionally only if it is `targetSdk` 36 or greater. If the callback in enabled, `onBackPressed()` is not called and callback is used regardless of `android:enableOnBackInvokedCallback` property in or set in AndroidManifest.xml. As a workaround callback is manually calling existing `onBackPressed()`. This is done rather than removing onBackPressed() completely and using only `OnBackPressedCallback` as we are not sure of the impact of removing the implementation entirely. Once we determine it is safe to do so then, we should remove the workaround and fully transition to `OnBackPressedCallback` * I also surveyed child classes extending ReactActivity for overridden `onPressedBack()` usage and found only one usage which will be handled later. NOTE: `ReactDelegate.onHostResume()` sets up the `DefaultHardwareBackBtnHandler` using `ReactActivity` (https://fburl.com/ul47tbeo) and will be called from JS `BackHanderl.exitApp` (https://fburl.com/code/a4l2pjsw). Calling `BackHanderl.exitApp` enables predictive back to work. Changelog: [Internal] Reviewed By: mdvacca Differential Revision: D74161428 fbshipit-source-id: 2e081ba6922b315e9d1746e83a41bab5277fa62e --- .../com/facebook/react/ReactActivity.java | 20 +++++++++++++ .../com/facebook/react/util/AndroidVersion.kt | 30 +++++++++++++++++++ 2 files changed, 50 insertions(+) create mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/util/AndroidVersion.kt diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/ReactActivity.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/ReactActivity.java index 757ba90d5be..031f534afec 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/ReactActivity.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/ReactActivity.java @@ -11,11 +11,13 @@ import android.content.Intent; import android.content.res.Configuration; import android.os.Bundle; import android.view.KeyEvent; +import androidx.activity.OnBackPressedCallback; import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler; import com.facebook.react.modules.core.PermissionAwareActivity; import com.facebook.react.modules.core.PermissionListener; +import com.facebook.react.util.AndroidVersion; import org.jetbrains.annotations.NotNull; /** Base Activity for React Native applications. */ @@ -24,6 +26,18 @@ public abstract class ReactActivity extends AppCompatActivity private final ReactActivityDelegate mDelegate; + // Due to enforced predictive back on targetSdk 36, 'onBackPressed()' is disabled by default. + // Using a workaround to trigger it manually. + private final OnBackPressedCallback mBackPressedCallback = + new OnBackPressedCallback(true) { + @Override + public void handleOnBackPressed() { + setEnabled(false); + onBackPressed(); + setEnabled(true); + } + }; + protected ReactActivity() { mDelegate = createReactActivityDelegate(); } @@ -45,6 +59,9 @@ public abstract class ReactActivity extends AppCompatActivity protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mDelegate.onCreate(savedInstanceState); + if (AndroidVersion.isAtLeastTargetSdk36(this)) { + getOnBackPressedDispatcher().addCallback(this, mBackPressedCallback); + } } @Override @@ -103,6 +120,9 @@ public abstract class ReactActivity extends AppCompatActivity @Override public void invokeDefaultOnBackPressed() { + // Disabling callback so the fallback logic (finish activity) can run + // as super.onBackPressed() will call all enabled callbacks in the dispatcher. + mBackPressedCallback.setEnabled(false); super.onBackPressed(); } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/util/AndroidVersion.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/util/AndroidVersion.kt new file mode 100644 index 00000000000..325ab60f901 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/util/AndroidVersion.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package com.facebook.react.util + +import android.content.Context +import android.os.Build + +/** Helper class for checking Android version-related information. */ +internal object AndroidVersion { + + /** + * This is the version code for Android 16 (SDK Level 36). Delete it once we bump up the default + * compile SDK version to 36. + */ + private const val VERSION_CODE_BAKLAVA: Int = 36 + + /** + * This method is used to check if the current device is running Android 16 (SDK Level 36) or + * higher and the app is targeting Android 16 (SDK Level 36) or higher. + */ + @JvmStatic + fun isAtLeastTargetSdk36(context: Context): Boolean = + Build.VERSION.SDK_INT >= VERSION_CODE_BAKLAVA && + context.applicationInfo.targetSdkVersion >= VERSION_CODE_BAKLAVA +}