/* * 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 -> reactApplicationContext.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 = reactApplicationContext.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() { reactApplicationContext.removeLifecycleEventListener(this) synchronized(this@IntentModule) { for (pendingPromise in pendingOpenURLPromises) { getInitialURL(pendingPromise) } initialURLListener = null pendingOpenURLPromises.clear() } } override fun onHostPause() = Unit override fun onHostDestroy() = Unit } reactApplicationContext.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.isNullOrEmpty()) { 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.isNullOrEmpty()) { 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 = reactApplicationContext.packageManager 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(reactApplicationContext.getCurrentActivity()) val selfPackageName = reactApplicationContext.packageName 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.isNullOrEmpty()) { promise.reject(JSApplicationIllegalArgumentException("Invalid Action: $action.")) return } val intent = Intent(action) val packageManager = reactApplicationContext.packageManager if (packageManager == null || intent.resolveActivity(packageManager) == null) { promise.reject( JSApplicationIllegalArgumentException("Could not launch Intent with action $action.") ) return } try { 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) promise.resolve(null) } catch (e: Exception) { promise.reject(e) } } private fun sendOSIntent(intent: Intent, useNewTaskFlag: Boolean) { val currentActivity = reactApplicationContext.getCurrentActivity() val selfPackageName = reactApplicationContext.packageName val packageManager = reactApplicationContext.packageManager 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 { reactApplicationContext.startActivity(intent) } } public companion object { private const val EXTRA_MAP_KEY_FOR_VALUE = "value" public const val NAME: String = NativeIntentAndroidSpec.NAME } }