Skip to content
Closed
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,8 @@ export type Permission =
| 'android.permission.READ_PHONE_NUMBERS'
| 'android.permission.UWB_RANGING'
| 'android.permission.POST_NOTIFICATIONS'
| 'android.permission.NEARBY_WIFI_DEVICES';
| 'android.permission.NEARBY_WIFI_DEVICES'
| 'android.permission.ACCESS_LOCAL_NETWORK';

export type PermissionStatus = 'granted' | 'denied' | 'never_ask_again';

Expand Down Expand Up @@ -116,7 +117,8 @@ export interface PermissionsAndroidStatic {
| 'READ_PHONE_NUMBERS'
| 'UWB_RANGING'
| 'POST_NOTIFICATIONS'
| 'NEARBY_WIFI_DEVICES']: Permission;
| 'NEARBY_WIFI_DEVICES'
| 'ACCESS_LOCAL_NETWORK']: Permission;
};
new (): PermissionsAndroidStatic;
/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ type PermissionsType = Readonly<{
UWB_RANGING: 'android.permission.UWB_RANGING',
POST_NOTIFICATIONS: 'android.permission.POST_NOTIFICATIONS',
NEARBY_WIFI_DEVICES: 'android.permission.NEARBY_WIFI_DEVICES',
ACCESS_LOCAL_NETWORK: 'android.permission.ACCESS_LOCAL_NETWORK',
}>;

export type PermissionStatus = 'granted' | 'denied' | 'never_ask_again';
Expand Down Expand Up @@ -125,6 +126,7 @@ const PERMISSIONS = Object.freeze({
UWB_RANGING: 'android.permission.UWB_RANGING',
POST_NOTIFICATIONS: 'android.permission.POST_NOTIFICATIONS',
NEARBY_WIFI_DEVICES: 'android.permission.NEARBY_WIFI_DEVICES',
ACCESS_LOCAL_NETWORK: 'android.permission.ACCESS_LOCAL_NETWORK',
}) as PermissionsType;

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@
-->
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>

<!--
On Android 17 (SDK 37) devices, apps that declare this must hold ACCESS_LOCAL_NETWORK
to reach the dev server over the local network. Loopback via `adb reverse` is exempt.
-->
<uses-permission android:name="android.permission.ACCESS_LOCAL_NETWORK"/>

<application>
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity"
android:exported="false" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import com.facebook.react.bridge.Callback;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.common.LifecycleState;
import com.facebook.react.devsupport.LocalNetworkPermissionUtil;
import com.facebook.react.interfaces.fabric.ReactSurface;
import com.facebook.react.internal.featureflags.ReactNativeNewArchitectureFeatureFlags;
import com.facebook.react.modules.core.PermissionListener;
Expand Down Expand Up @@ -168,7 +169,10 @@ protected ReactRootView createRootView() {
};
}
if (mainComponentName != null) {
loadApp(mainComponentName);
LocalNetworkPermissionUtil.requestLocalNetworkAccessIfNeeded(
getPlainActivity(),
() -> loadApp(mainComponentName)
);
}
});
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*
* 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.devsupport

import android.app.Activity
import android.content.Context
import android.content.pm.PackageManager
import com.facebook.react.modules.core.PermissionAwareActivity
import com.facebook.react.modules.systeminfo.AndroidInfoHelpers
import com.facebook.react.util.AndroidVersion

/**
* Debug-only helper to request the runtime `ACCESS_LOCAL_NETWORK` permission needed to reach Metro
* on Android 17 (SDK 37) devices, which gate local-network addresses (the emulator's `10.0.2.2`
* alias, a device's Wi-Fi/LAN IP) for any app that declares the permission, regardless of targetSdk.
* Loopback via `adb reverse` (`localhost`) is exempt.
*/
internal object LocalNetworkPermissionUtil {
private const val PERMISSION = "android.permission.ACCESS_LOCAL_NETWORK"
private const val PERMISSION_REQUEST_CODE = 1

/**
* Invokes [onResolved] once it is safe to connect to Metro: immediately when no permission is
* needed, or after the user answers the `ACCESS_LOCAL_NETWORK` prompt otherwise.
*/
@JvmStatic
fun requestLocalNetworkAccessIfNeeded(activity: Activity, onResolved: Runnable) {
if (activity is PermissionAwareActivity && needsLocalNetworkPrompt(activity)) {
activity.requestPermissions(arrayOf(PERMISSION), PERMISSION_REQUEST_CODE) { _, _, _ ->
onResolved.run()
true
}
} else {
onResolved.run()
}
}

/** Whether the `ACCESS_LOCAL_NETWORK` prompt must be shown before reaching the dev server. */
private fun needsLocalNetworkPrompt(activity: Activity): Boolean {
Comment thread
alanjhughes marked this conversation as resolved.
if (!AndroidVersion.isAtLeastSdk37()) return false // enforced by the device, not app targetSdk
if (!isPermissionDeclared(activity)) return false // debug manifest only
if (activity.checkSelfPermission(PERMISSION) == PackageManager.PERMISSION_GRANTED) return false
return !isExemptDevServerHost(activity) // loopback needs no permission
}

/**
* True for loopback dev servers (`localhost` / `127.x` / `::1`, e.g. USB + `adb reverse`), which
* are exempt. The emulator's `10.0.2.2` is not loopback here and does need the permission.
*/
private fun isExemptDevServerHost(context: Context): Boolean {
Comment thread
alanjhughes marked this conversation as resolved.
Outdated
val host = AndroidInfoHelpers.getServerHost(context).substringBeforeLast(':').trim('[', ']')
return host == "localhost" || host == "::1" || host.startsWith("127.")
}

private fun isPermissionDeclared(activity: Activity): Boolean =
try {
activity.packageManager
.getPackageInfo(activity.packageName, PackageManager.GET_PERMISSIONS)
.requestedPermissions
?.contains(PERMISSION) == true
} catch (_: PackageManager.NameNotFoundException) {
false
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,13 @@ internal object AndroidVersion {
*/
internal const val VERSION_CODE_BAKLAVA: Int = 36

/**
* This is the version code for Android 17 (SDK Level 37). Internally at Meta this code is also
* compiled against SDK 34, so we need to retain this constant instead of using
* [Build.VERSION_CODES.CINNAMON_BUN] directly.
*/
internal const val VERSION_CODE_CINNAMON_BUN: Int = 37

/**
* android.R.attr.windowOptOutEdgeToEdgeEnforcement added in API 35. Internally at Meta this code
* is compiled against an SDK that may not have this attribute defined.
Expand All @@ -51,4 +58,13 @@ internal object AndroidVersion {
fun isAtLeastTargetSdk36(context: Context): Boolean =
Build.VERSION.SDK_INT >= VERSION_CODE_BAKLAVA &&
context.applicationInfo.targetSdkVersion >= VERSION_CODE_BAKLAVA

/**
* This method is used to check if the current device is running Android 17 (SDK Level 37) or
* higher. Unlike the `isAtLeastTargetSdk*` helpers, this checks the device API level only and not
* the app's targetSdk, because Android 17 gates the local-network runtime permission for any app
* that declares it, regardless of targetSdk.
*/
@JvmStatic
internal fun isAtLeastSdk37(): Boolean = Build.VERSION.SDK_INT >= VERSION_CODE_CINNAMON_BUN
Comment thread
alanjhughes marked this conversation as resolved.
Outdated
Comment thread
alanjhughes marked this conversation as resolved.
Outdated
}
7 changes: 4 additions & 3 deletions packages/react-native/ReactNativeApi.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<6fd7172a54686bbab8a03359976ef63d>>
* @generated SignedSource<<b039d3ad12debfc2af81bc7dc85fb584>>
*
* This file was generated by scripts/js-api/build-types/index.js.
*/
Expand Down Expand Up @@ -3548,6 +3548,7 @@ declare type PermissionsType = {
readonly ACCESS_BACKGROUND_LOCATION: "android.permission.ACCESS_BACKGROUND_LOCATION"
readonly ACCESS_COARSE_LOCATION: "android.permission.ACCESS_COARSE_LOCATION"
readonly ACCESS_FINE_LOCATION: "android.permission.ACCESS_FINE_LOCATION"
readonly ACCESS_LOCAL_NETWORK: "android.permission.ACCESS_LOCAL_NETWORK"
readonly ACCESS_MEDIA_LOCATION: "android.permission.ACCESS_MEDIA_LOCATION"
readonly ACTIVITY_RECOGNITION: "android.permission.ACTIVITY_RECOGNITION"
readonly ADD_VOICEMAIL: "com.android.voicemail.permission.ADD_VOICEMAIL"
Expand Down Expand Up @@ -6053,9 +6054,9 @@ export {
PanResponderCallbacks, // 6d63e7be
PanResponderGestureState, // 54baf558
PanResponderInstance, // 69cebbe8
Permission, // 06473f4f
Permission, // 08f1c82f
PermissionStatus, // 4b7de97b
PermissionsAndroid, // db2a401e
PermissionsAndroid, // 8a0bc8d8
PixelRatio, // 10d9e32d
Platform, // b73caa89
PlatformColor, // 8297ec62
Expand Down
Loading