7 月 20 日,Android Developers Blog 发了一条 Nearby Connections API 的变更通知。
到 2026 年底以后,Nearby Connections 不会再帮 1P 和 3P 应用自动打开 Wi-Fi / Bluetooth。应用在调用发现、广播和连接前,要自己确认无线开关状态;如果用户关了开关,就要让用户手动打开。
这类变化看起来只影响一个 SDK 行为,实际会改掉很多近场连接功能的失败路径。以前用户点了"搜索附近设备",SDK 可能在背后把需要的无线能力拉起来;后面这个动作要回到应用自己的 UI 和状态机里。

影响
Nearby Connections 是 Google Play services 里的近场 P2P API,用来做附近设备发现、连接和数据交换。它底层会用到 Bluetooth、BLE 和 Wi-Fi,开发者不用自己拼这些无线协议。
常见入口是两个:一台设备调用 startAdvertising() 让自己可被发现,另一台设备调用 startDiscovery() 搜索附近端点。找到端点以后再 requestConnection(),双方接受连接后用 sendPayload() 传 bytes、file 或 stream。
代码大概是这样:
bash
private const val SERVICE_ID = "com.example.nearby"
private val STRATEGY = Strategy.P2P_POINT_TO_POINT
fun startDiscovery(context: Context) {
val options = DiscoveryOptions.Builder()
.setStrategy(STRATEGY)
.build()
Nearby.getConnectionsClient(context)
.startDiscovery(SERVICE_ID, endpointDiscoveryCallback, options)
.addOnSuccessListener {
// 开始发现附近设备
}
.addOnFailureListener { error ->
// 这里要展示明确失败原因,而不是一直停在搜索中
}
}
这段代码变化在调用它之前。以前很多项目把权限判断放在最前面,权限过了就直接启动发现或广播。后面还要补一个无线开关判断,否则用户关着 Wi-Fi 或 Bluetooth 时,页面可能只会表现成"搜不到设备"。
连接前检查
状态检查不需要写得很复杂,核心是把"权限没给"和"开关没开"分成两个 UI 状态。权限没给,走运行时权限;开关没开,走系统设置面板或系统设置页。
一个简单的状态对象可以这样写:
bash
data class NearbyRadioState(
val wifiEnabled: Boolean,
val bluetoothEnabled: Boolean,
) {
val disabledItems: List<String>
get() = buildList {
if (!wifiEnabled) add("Wi-Fi")
if (!bluetoothEnabled) add("Bluetooth")
}
}
fun Context.nearbyRadioState(): NearbyRadioState {
val wifiManager = getSystemService(WifiManager::class.java)
val bluetoothManager = getSystemService(BluetoothManager::class.java)
val adapter = bluetoothManager.adapter
return NearbyRadioState(
wifiEnabled = wifiManager.isWifiEnabled,
bluetoothEnabled = adapter?.isEnabled == true,
)
}
这里没有去调用 setWifiEnabled(true) 或 BluetoothAdapter.enable()。原因也很直接:面向新系统和新 target 的普通应用,已经不能靠这两个 API 静默打开无线开关。
WifiManager.setWifiEnabled() 从 Android 10 开始对 target Q 及以上应用会失败并返回 false。BluetoothAdapter.enable() 从 Android 13 开始对 target TIRAMISU 及以上应用也会失败并返回 false。设备管理器、Profile Owner、系统应用有豁免,普通业务 App 不应该把它当兜底方案。

引导用户打开
Wi-Fi 这边可以优先用 Settings Panel。它是 Android 10 加的浮层设置入口,Settings.Panel.ACTION_WIFI 会展示包含 Wi-Fi 控件的系统面板,用户处理完以后回到当前 App。
Bluetooth 没有同样的通用 Panel 常量,通常跳到蓝牙设置页:
bash
fun Activity.openWifiSettings() {
val intent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
Intent(Settings.Panel.ACTION_WIFI)
} else {
Intent(Settings.ACTION_WIFI_SETTINGS)
}
startActivity(intent)
}
fun Activity.openBluetoothSettings() {
startActivity(Intent(Settings.ACTION_BLUETOOTH_SETTINGS))
}
页面文案不要写成"应用正在打开蓝牙"。更准确的表达是"需要打开 Bluetooth 才能搜索附近设备",然后给一个跳转设置的按钮。用户从设置页回来以后,重新读取 nearbyRadioState(),状态满足再调用 Nearby Connections。
如果两个开关都关着,不建议连续弹两个系统页面。先在业务页把缺失项列出来,让用户知道为什么不能继续。点"去设置"以后,可以先处理 Wi-Fi,再让用户处理 Bluetooth;也可以放两个明确按钮,避免用户在系统设置里来回找。
权限别混在一起
Nearby Connections 的权限声明这些年已经变得比较碎。老版本里会看到 ACCESS_WIFI_STATE、CHANGE_WIFI_STATE、BLUETOOTH、BLUETOOTH_ADMIN,Android 12 以后又有 BLUETOOTH_ADVERTISE、BLUETOOTH_CONNECT、BLUETOOTH_SCAN,Android 13 以后还会碰到 NEARBY_WIFI_DEVICES。
如果项目 target 到 Android 17,并且使用 WIFI_LAN,文档里还列了 ACCESS_LOCAL_NETWORK。这类权限要按项目的 SDK、策略和 payload 类型裁剪,不能直接拿旧 Manifest 混过去。
示例可以先写成这样,再按项目实际入口收敛:
bash
<!-- Nearby Connections: legacy permissions -->
<uses-permission
android:name="android.permission.ACCESS_WIFI_STATE"
android:maxSdkVersion="31" />
<uses-permission
android:name="android.permission.CHANGE_WIFI_STATE"
android:maxSdkVersion="31" />
<uses-permission
android:name="android.permission.BLUETOOTH"
android:maxSdkVersion="30" />
<uses-permission
android:name="android.permission.BLUETOOTH_ADMIN"
android:maxSdkVersion="30" />
<!-- Android 12+ Bluetooth runtime permissions -->
<uses-permission
android:name="android.permission.BLUETOOTH_ADVERTISE"
android:minSdkVersion="31" />
<uses-permission
android:name="android.permission.BLUETOOTH_CONNECT"
android:minSdkVersion="31" />
<uses-permission
android:name="android.permission.BLUETOOTH_SCAN"
android:minSdkVersion="31" />
<!-- Android 13+ nearby Wi-Fi permission -->
<uses-permission android:name="android.permission.NEARBY_WIFI_DEVICES" />
这里要分清两件事:权限允许 App 使用相关能力,开关状态决定设备当前是否能执行对应无线动作。用户同意了蓝牙权限,但系统蓝牙开关是关的,Nearby Connections 仍然不能按预期发现和连接。

失败状态
这次变化最容易漏的是失败展示。很多"搜索附近设备"的页面只有 loading 和空列表,用户看到的是一直转圈,开发者日志里才有失败异常。无线开关不再自动打开以后,这种 UI 会更容易出问题。
我会把启动发现拆成三个判断:运行时权限、无线开关、Nearby 调用结果。前两个是同步的业务状态,最后一个才是 SDK 返回。
bash
fun startNearbyDiscovery(context: Context) {
if (!hasNearbyPermissions(context)) {
showPermissionRequest()
return
}
val radioState = context.nearbyRadioState()
if (radioState.disabledItems.isNotEmpty()) {
showRadioDisabled(radioState.disabledItems)
return
}
Nearby.getConnectionsClient(context)
.startDiscovery(SERVICE_ID, endpointDiscoveryCallback, discoveryOptions)
.addOnFailureListener { error ->
showNearbyError(error)
}
}
showRadioDisabled() 里不要只给一句"连接失败"。更有用的是把缺失项写出来,例如"打开 Wi-Fi 后再搜索附近设备"。用户打开设置返回 App 后,页面重新检查状态;如果状态还没变,就继续停在引导页,不要立刻进入搜索 loading。

还有一个细节是恢复流程。Activity 回到前台时重新读一次无线状态,连接页从 RadioDisabled 切到 Ready 后再启动发现。不要只依赖设置页返回结果,因为系统设置页通常不会给业务 App 返回"用户确实打开了开关"的强语义结果。
最后
Nearby Connections 后面不会再替应用自动打开 Wi-Fi / Bluetooth。调用 startAdvertising() 或 startDiscovery() 前,把运行时权限和无线开关分开检查;缺开关时引导用户到系统设置,回来后重新读状态。
这个改动不需要重写 Nearby Connections 连接流程,但会影响搜索页、配网页、面对面传输、本地多人游戏这类入口的失败处理。