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
This commit is contained in:
Alan Lee 2024-11-14 16:28:13 -08:00 committed by Facebook GitHub Bot
parent 1afde8bd25
commit 74ed831a33
3 changed files with 275 additions and 300 deletions

View File

@ -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 <init> (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

View File

@ -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<Promise> 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.
*
* <p>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
*
* <p>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);
}
}
}

View File

@ -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<Promise> = ArrayList<Promise>()
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..<extras.size()) {
val map = extras.getMap(i)
val name = map.getString("key")
val type = map.getType(EXTRA_MAP_KEY_FOR_VALUE)
when (type) {
ReadableType.String -> {
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
}
}