From 74ed831a33d734def76b1a78ef8547c2f093133a Mon Sep 17 00:00:00 2001 From: Alan Lee Date: Thu, 14 Nov 2024 16:28:13 -0800 Subject: [PATCH] com.facebook.react.modules.intent.IntentModule.java (#47603) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/47603 Convert Java to Kotlin Changelog: [Internal] Reviewed By: tdn120 Differential Revision: D65874900 fbshipit-source-id: 19dbce0a6d822aae8c39860f45b90d064acebd74 --- .../ReactAndroid/api/ReactAndroid.api | 5 + .../react/modules/intent/IntentModule.java | 300 ------------------ .../react/modules/intent/IntentModule.kt | 270 ++++++++++++++++ 3 files changed, 275 insertions(+), 300 deletions(-) delete mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/intent/IntentModule.java create mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/intent/IntentModule.kt diff --git a/packages/react-native/ReactAndroid/api/ReactAndroid.api b/packages/react-native/ReactAndroid/api/ReactAndroid.api index 9838a965e2c..6495a03e550 100644 --- a/packages/react-native/ReactAndroid/api/ReactAndroid.api +++ b/packages/react-native/ReactAndroid/api/ReactAndroid.api @@ -3401,6 +3401,8 @@ public final class com/facebook/react/modules/image/ImageLoaderModule$Companion } public class com/facebook/react/modules/intent/IntentModule : com/facebook/fbreact/specs/NativeIntentAndroidSpec { + public static final field Companion Lcom/facebook/react/modules/intent/IntentModule$Companion; + public static final field NAME Ljava/lang/String; public fun (Lcom/facebook/react/bridge/ReactApplicationContext;)V public fun canOpenURL (Ljava/lang/String;Lcom/facebook/react/bridge/Promise;)V public fun getInitialURL (Lcom/facebook/react/bridge/Promise;)V @@ -3410,6 +3412,9 @@ public class com/facebook/react/modules/intent/IntentModule : com/facebook/fbrea public fun sendIntent (Ljava/lang/String;Lcom/facebook/react/bridge/ReadableArray;Lcom/facebook/react/bridge/Promise;)V } +public final class com/facebook/react/modules/intent/IntentModule$Companion { +} + public abstract interface class com/facebook/react/modules/network/CookieJarContainer : okhttp3/CookieJar { public abstract fun removeCookieJar ()V public abstract fun setCookieJar (Lokhttp3/CookieJar;)V diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/intent/IntentModule.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/intent/IntentModule.java deleted file mode 100644 index c471c56af3e..00000000000 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/intent/IntentModule.java +++ /dev/null @@ -1,300 +0,0 @@ -/* - * 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.modules.intent; - -import android.app.Activity; -import android.content.ComponentName; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.net.Uri; -import android.nfc.NfcAdapter; -import android.provider.Settings; -import androidx.annotation.Nullable; -import androidx.core.util.Preconditions; -import com.facebook.fbreact.specs.NativeIntentAndroidSpec; -import com.facebook.infer.annotation.Nullsafe; -import com.facebook.react.bridge.JSApplicationIllegalArgumentException; -import com.facebook.react.bridge.LifecycleEventListener; -import com.facebook.react.bridge.Promise; -import com.facebook.react.bridge.ReactApplicationContext; -import com.facebook.react.bridge.ReadableArray; -import com.facebook.react.bridge.ReadableMap; -import com.facebook.react.bridge.ReadableType; -import com.facebook.react.module.annotations.ReactModule; -import java.util.ArrayList; -import java.util.List; - -/** Intent module. Launch other activities or open URLs. */ -@Nullsafe(Nullsafe.Mode.LOCAL) -@ReactModule(name = NativeIntentAndroidSpec.NAME) -public class IntentModule extends NativeIntentAndroidSpec { - - private @Nullable LifecycleEventListener mInitialURLListener = null; - private final List mPendingOpenURLPromises = new ArrayList<>(); - - private static final String EXTRA_MAP_KEY_FOR_VALUE = "value"; - - public IntentModule(ReactApplicationContext reactContext) { - super(reactContext); - } - - @Override - public void invalidate() { - synchronized (this) { - mPendingOpenURLPromises.clear(); - if (mInitialURLListener != null) { - getReactApplicationContext().removeLifecycleEventListener(mInitialURLListener); - mInitialURLListener = null; - } - } - super.invalidate(); - } - - /** - * Return the URL the activity was started with - * - * @param promise a promise which is resolved with the initial URL - */ - @Override - public void getInitialURL(Promise promise) { - try { - Activity currentActivity = getCurrentActivity(); - if (currentActivity == null) { - waitForActivityAndGetInitialURL(promise); - return; - } - - Intent intent = currentActivity.getIntent(); - String action = intent.getAction(); - Uri uri = intent.getData(); - - String initialURL = null; - if (uri != null - && (Intent.ACTION_VIEW.equals(action) - || NfcAdapter.ACTION_NDEF_DISCOVERED.equals(action))) { - initialURL = uri.toString(); - } - - promise.resolve(initialURL); - } catch (Exception e) { - promise.reject( - new JSApplicationIllegalArgumentException( - "Could not get the initial URL : " + e.getMessage())); - } - } - - private synchronized void waitForActivityAndGetInitialURL(final Promise promise) { - mPendingOpenURLPromises.add(promise); - if (mInitialURLListener != null) { - return; - } - - mInitialURLListener = - new LifecycleEventListener() { - @Override - public void onHostResume() { - getReactApplicationContext().removeLifecycleEventListener(this); - synchronized (IntentModule.this) { - for (Promise promise : mPendingOpenURLPromises) { - getInitialURL(promise); - } - - mInitialURLListener = null; - mPendingOpenURLPromises.clear(); - } - } - - @Override - public void onHostPause() {} - - @Override - public void onHostDestroy() {} - }; - getReactApplicationContext().addLifecycleEventListener(mInitialURLListener); - } - - /** - * Starts a corresponding external activity for the given URL. - * - *

For example, if the URL is "https://www.facebook.com", the system browser will be opened, or - * the "choose application" dialog will be shown. - * - * @param url the URL to open - */ - @Override - public void openURL(@Nullable String url, Promise promise) { - if (url == null || url.isEmpty()) { - promise.reject(new JSApplicationIllegalArgumentException("Invalid URL: " + url)); - return; - } - - try { - Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url).normalizeScheme()); - sendOSIntent(intent, false); - - promise.resolve(true); - } catch (Exception e) { - promise.reject( - new JSApplicationIllegalArgumentException( - "Could not open URL '" + url + "': " + e.getMessage())); - } - } - - /** - * Determine whether or not an installed app can handle a given URL. - * - * @param url the URL to open - * @param promise a promise that is always resolved with a boolean argument - */ - @Override - public void canOpenURL(@Nullable String url, Promise promise) { - if (url == null || url.isEmpty()) { - promise.reject(new JSApplicationIllegalArgumentException("Invalid URL: " + url)); - return; - } - - try { - Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); - // We need Intent.FLAG_ACTIVITY_NEW_TASK since getReactApplicationContext() returns - // the ApplicationContext instead of the Activity context. - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - PackageManager packageManager = getReactApplicationContext().getPackageManager(); - boolean canOpen = packageManager != null && intent.resolveActivity(packageManager) != null; - promise.resolve(canOpen); - } catch (Exception e) { - promise.reject( - new JSApplicationIllegalArgumentException( - "Could not check if URL '" + url + "' can be opened: " + e.getMessage())); - } - } - - /** - * Starts an external activity to open app's settings into Android Settings - * - * @param promise a promise which is resolved when the Settings is opened - */ - @Override - public void openSettings(Promise promise) { - try { - Intent intent = new Intent(); - Activity currentActivity = Preconditions.checkNotNull(getCurrentActivity()); - String selfPackageName = getReactApplicationContext().getPackageName(); - - intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); - intent.addCategory(Intent.CATEGORY_DEFAULT); - intent.setData(Uri.parse("package:" + selfPackageName)); - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY); - intent.addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS); - currentActivity.startActivity(intent); - - promise.resolve(true); - } catch (Exception e) { - promise.reject( - new JSApplicationIllegalArgumentException( - "Could not open the Settings: " + e.getMessage())); - } - } - - /** - * Allows to send intents on Android - * - *

For example, you can open the Notification Category screen for a specific application - * passing action = 'android.settings.CHANNEL_NOTIFICATION_SETTINGS' and extras = [ { - * 'android.provider.extra.APP_PACKAGE': 'your.package.name.here' }, { - * 'android.provider.extra.CHANNEL_ID': 'your.channel.id.here } ] - * - * @param action The general action to be performed - * @param extras An array of extras [{ String, String | Number | Boolean }] - */ - @Override - public void sendIntent(String action, @Nullable ReadableArray extras, Promise promise) { - if (action == null || action.isEmpty()) { - promise.reject(new JSApplicationIllegalArgumentException("Invalid Action: " + action + ".")); - return; - } - - Intent intent = new Intent(action); - - PackageManager packageManager = getReactApplicationContext().getPackageManager(); - if (packageManager == null || intent.resolveActivity(packageManager) == null) { - promise.reject( - new JSApplicationIllegalArgumentException( - "Could not launch Intent with action " + action + ".")); - return; - } - - if (extras != null) { - for (int i = 0; i < extras.size(); i++) { - ReadableMap map = extras.getMap(i); - String name = map.getString("key"); - ReadableType type = map.getType(EXTRA_MAP_KEY_FOR_VALUE); - - switch (type) { - case String: - { - // NULLSAFE_FIXME[Parameter Not Nullable] - intent.putExtra(name, map.getString(EXTRA_MAP_KEY_FOR_VALUE)); - break; - } - case Number: - { - // We cannot know from JS if is an Integer or Double - // See: https://github.com/facebook/react-native/issues/4141 - // We might need to find a workaround if this is really an issue - Double number = map.getDouble(EXTRA_MAP_KEY_FOR_VALUE); - // NULLSAFE_FIXME[Parameter Not Nullable] - intent.putExtra(name, number); - break; - } - case Boolean: - { - // NULLSAFE_FIXME[Parameter Not Nullable] - intent.putExtra(name, map.getBoolean(EXTRA_MAP_KEY_FOR_VALUE)); - break; - } - default: - { - promise.reject( - new JSApplicationIllegalArgumentException( - "Extra type for " + name + " not supported.")); - return; - } - } - } - } - - sendOSIntent(intent, true); - } - - private void sendOSIntent(Intent intent, Boolean useNewTaskFlag) { - Activity currentActivity = getCurrentActivity(); - - String selfPackageName = getReactApplicationContext().getPackageName(); - PackageManager packageManager = getReactApplicationContext().getPackageManager(); - ComponentName componentName = null; - if (packageManager == null) { - componentName = intent.getComponent(); - } else { - componentName = intent.resolveActivity(packageManager); - } - String otherPackageName = (componentName != null ? componentName.getPackageName() : ""); - - // If there is no currentActivity or we are launching to a different package we need to set - // the FLAG_ACTIVITY_NEW_TASK flag - if (useNewTaskFlag || currentActivity == null || !selfPackageName.equals(otherPackageName)) { - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - } - - if (currentActivity != null) { - currentActivity.startActivity(intent); - } else { - getReactApplicationContext().startActivity(intent); - } - } -} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/intent/IntentModule.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/intent/IntentModule.kt new file mode 100644 index 00000000000..bb238774927 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/intent/IntentModule.kt @@ -0,0 +1,270 @@ +/* + * 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.modules.intent + +import android.app.Activity +import android.content.Intent +import android.net.Uri +import android.nfc.NfcAdapter +import android.provider.Settings +import com.facebook.fbreact.specs.NativeIntentAndroidSpec +import com.facebook.react.bridge.JSApplicationIllegalArgumentException +import com.facebook.react.bridge.LifecycleEventListener +import com.facebook.react.bridge.Promise +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.bridge.ReadableArray +import com.facebook.react.bridge.ReadableType +import com.facebook.react.module.annotations.ReactModule +import java.util.ArrayList + +/** Intent module. Launch other activities or open URLs. */ +@ReactModule(name = NativeIntentAndroidSpec.NAME) +public open class IntentModule(reactContext: ReactApplicationContext) : + NativeIntentAndroidSpec(reactContext) { + + private var initialURLListener: LifecycleEventListener? = null + private val pendingOpenURLPromises: MutableList = ArrayList() + + override fun invalidate() { + synchronized(this) { + pendingOpenURLPromises.clear() + initialURLListener + ?.let { listener -> getReactApplicationContext().removeLifecycleEventListener(listener) } + .also { initialURLListener = null } + } + super.invalidate() + } + + /** + * Return the URL the activity was started with + * + * @param promise a promise which is resolved with the initial URL + */ + override fun getInitialURL(promise: Promise) { + try { + val currentActivity = getCurrentActivity() + if (currentActivity == null) { + waitForActivityAndGetInitialURL(promise) + return + } + + val intent = currentActivity.intent + val action = intent.action + val uri = intent.data + + val initialURL = + if (uri != null && + (Intent.ACTION_VIEW == action || NfcAdapter.ACTION_NDEF_DISCOVERED == action)) { + uri.toString() + } else { + null + } + + promise.resolve(initialURL) + } catch (e: Exception) { + promise.reject( + JSApplicationIllegalArgumentException("Could not get the initial URL : ${e.message}")) + } + } + + @Synchronized + private fun waitForActivityAndGetInitialURL(promise: Promise) { + pendingOpenURLPromises.add(promise) + if (initialURLListener != null) { + return + } + + initialURLListener = + object : LifecycleEventListener { + override fun onHostResume() { + getReactApplicationContext().removeLifecycleEventListener(this) + synchronized(this@IntentModule) { + for (pendingPromise in pendingOpenURLPromises) { + getInitialURL(pendingPromise) + } + initialURLListener = null + pendingOpenURLPromises.clear() + } + } + + override fun onHostPause() = Unit + + override fun onHostDestroy() = Unit + } + getReactApplicationContext().addLifecycleEventListener(initialURLListener) + } + + /** + * Starts a corresponding external activity for the given URL. + * + * For example, if the URL is "https://www.facebook.com", the system browser will be opened, or + * the "choose application" dialog will be shown. + * + * @param url the URL to open + */ + override fun openURL(url: String?, promise: Promise) { + if (url == null || url.isEmpty()) { + promise.reject(JSApplicationIllegalArgumentException("Invalid URL: $url")) + return + } + + try { + val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url).normalizeScheme()) + sendOSIntent(intent, false) + + promise.resolve(true) + } catch (e: Exception) { + promise.reject( + JSApplicationIllegalArgumentException("Could not open URL '${url}': ${e.message}")) + } + } + + /** + * Determine whether or not an installed app can handle a given URL. + * + * @param url the URL to open + * @param promise a promise that is always resolved with a boolean argument + */ + override fun canOpenURL(url: String?, promise: Promise) { + if (url == null || url.isEmpty()) { + promise.reject(JSApplicationIllegalArgumentException("Invalid URL: $url")) + return + } + + try { + val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url)) + // We need Intent.FLAG_ACTIVITY_NEW_TASK since getReactApplicationContext() returns + // the ApplicationContext instead of the Activity context. + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + val packageManager = getReactApplicationContext().getPackageManager() + val canOpen = packageManager != null && intent.resolveActivity(packageManager) != null + promise.resolve(canOpen) + } catch (e: Exception) { + promise.reject( + JSApplicationIllegalArgumentException( + "Could not check if URL '${url}' can be opened: ${e.message}")) + } + } + + /** + * Starts an external activity to open app's settings into Android Settings + * + * @param promise a promise which is resolved when the Settings is opened + */ + override fun openSettings(promise: Promise) { + try { + val intent = Intent() + val currentActivity: Activity = checkNotNull(getCurrentActivity()) + val selfPackageName = getReactApplicationContext().getPackageName() + + intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) + intent.addCategory(Intent.CATEGORY_DEFAULT) + intent.setData(Uri.parse("package:$selfPackageName")) + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY) + intent.addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS) + currentActivity.startActivity(intent) + + promise.resolve(true) + } catch (e: Exception) { + promise.reject( + JSApplicationIllegalArgumentException("Could not open the Settings: ${e.message}")) + } + } + + /** + * Allows to send intents on Android + * + * For example, you can open the Notification Category screen for a specific application passing + * action = 'android.settings.CHANNEL_NOTIFICATION_SETTINGS' and extras = + * [ { 'android.provider.extra.APP_PACKAGE': 'your.package.name.here' }, { 'android.provider.extra.CHANNEL_ID': 'your.channel.id.here } ] + * + * @param action The general action to be performed + * @param extras An array of extras [{ String, String | Number | Boolean }] + */ + override fun sendIntent(action: String?, extras: ReadableArray?, promise: Promise) { + if (action == null || action.isEmpty()) { + promise.reject(JSApplicationIllegalArgumentException("Invalid Action: $action.")) + return + } + + val intent = Intent(action) + + val packageManager = getReactApplicationContext().getPackageManager() + if (packageManager == null || intent.resolveActivity(packageManager) == null) { + promise.reject( + JSApplicationIllegalArgumentException("Could not launch Intent with action $action.")) + return + } + + if (extras != null) { + for (i in 0.. { + intent.putExtra(name, map.getString(EXTRA_MAP_KEY_FOR_VALUE)) + } + + ReadableType.Number -> { + // We cannot know from JS if is an Integer or Double + // See: https://github.com/facebook/react-native/issues/4141 + // We might need to find a workaround if this is really an issue + val number = map.getDouble(EXTRA_MAP_KEY_FOR_VALUE) + intent.putExtra(name, number) + } + + ReadableType.Boolean -> { + intent.putExtra(name, map.getBoolean(EXTRA_MAP_KEY_FOR_VALUE)) + } + + else -> { + promise.reject( + JSApplicationIllegalArgumentException("Extra type for $name not supported.")) + return + } + } + } + } + + sendOSIntent(intent, true) + } + + private fun sendOSIntent(intent: Intent, useNewTaskFlag: Boolean) { + val currentActivity = getCurrentActivity() + + val selfPackageName = getReactApplicationContext().getPackageName() + val packageManager = getReactApplicationContext().getPackageManager() + val componentName = + if (packageManager == null) { + intent.component + } else { + intent.resolveActivity(packageManager) + } + val otherPackageName = componentName?.packageName ?: "" + + // If there is no currentActivity or we are launching to a different package we need to set + // the FLAG_ACTIVITY_NEW_TASK flag + if (useNewTaskFlag || currentActivity == null || (selfPackageName != otherPackageName)) { + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } + + if (currentActivity != null) { + currentActivity.startActivity(intent) + } else { + getReactApplicationContext().startActivity(intent) + } + } + + public companion object { + private const val EXTRA_MAP_KEY_FOR_VALUE = "value" + public const val NAME: String = NativeIntentAndroidSpec.NAME + } +}