Skip to content
Open
Show file tree
Hide file tree
Changes from all 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,48 @@
/*
* 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.pm.PackageManager
import com.facebook.react.common.build.ReactBuildConfig
import com.facebook.react.modules.core.PermissionAwareActivity
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). Requested only in debuggable builds, and always (like iOS), since
* the dev-server host can change at runtime (e.g. switching from `adb reverse` to a LAN IP).
*/
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 (!ReactBuildConfig.DEBUG) return false // dev-server only; never prompt in release builds
if (!AndroidVersion.isAtLeastSdk37()) return false // enforced by the device, not app targetSdk
return activity.checkSelfPermission(PERMISSION) != PackageManager.PERMISSION_GRANTED
}
}
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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the permission can only be requested if targetSdkVersion is >= VERSION_CODE_CINNAMON_BUN too.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's required on any device running android 17 regardless of the target level

}
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