1 /* 2 * Copyright (C) 2024 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package android.bluetooth.test_utils 17 18 import android.Manifest.permission.BLUETOOTH_CONNECT 19 import android.Manifest.permission.BLUETOOTH_PRIVILEGED 20 import android.bluetooth.BluetoothAdapter 21 import android.bluetooth.BluetoothAdapter.ACTION_BLE_STATE_CHANGED 22 import android.bluetooth.BluetoothAdapter.STATE_BLE_ON 23 import android.bluetooth.BluetoothAdapter.STATE_OFF 24 import android.bluetooth.BluetoothAdapter.STATE_ON 25 import android.bluetooth.BluetoothAdapter.STATE_TURNING_OFF 26 import android.bluetooth.BluetoothAdapter.STATE_TURNING_ON 27 import android.bluetooth.BluetoothManager 28 import android.bluetooth.test_utils.Permissions.withPermissions 29 import android.content.BroadcastReceiver 30 import android.content.Context 31 import android.content.Intent 32 import android.content.IntentFilter 33 import android.provider.Settings 34 import android.util.Log 35 import androidx.test.platform.app.InstrumentationRegistry 36 import kotlin.time.Duration 37 import kotlin.time.Duration.Companion.seconds 38 import kotlinx.coroutines.CoroutineScope 39 import kotlinx.coroutines.Dispatchers 40 import kotlinx.coroutines.channels.awaitClose 41 import kotlinx.coroutines.channels.trySendBlocking 42 import kotlinx.coroutines.flow.SharingStarted 43 import kotlinx.coroutines.flow.callbackFlow 44 import kotlinx.coroutines.flow.filter 45 import kotlinx.coroutines.flow.first 46 import kotlinx.coroutines.flow.map 47 import kotlinx.coroutines.flow.onEach 48 import kotlinx.coroutines.flow.shareIn 49 import kotlinx.coroutines.runBlocking 50 import kotlinx.coroutines.withTimeoutOrNull 51 52 private const val TAG: String = "BlockingBluetoothAdapter" 53 // There is no access to the module only API Settings.Global.BLE_SCAN_ALWAYS_AVAILABLE 54 private const val BLE_SCAN_ALWAYS_AVAILABLE = "ble_scan_always_enabled" 55 56 object BlockingBluetoothAdapter { 57 private val context = InstrumentationRegistry.getInstrumentation().getContext() 58 @JvmStatic val adapter = context.getSystemService(BluetoothManager::class.java).getAdapter() 59 60 private val state = AdapterStateListener(context, adapter) 61 62 // BLE_START_TIMEOUT_DELAY + BREDR_START_TIMEOUT_DELAY + (10 seconds of additional delay) 63 private val stateChangeTimeout = 18.seconds 64 65 init { 66 Log.d(TAG, "Started with initial state to $state") 67 } 68 69 /** Set Bluetooth in BLE mode. Only works if it was OFF before */ 70 @JvmStatic enableBLEnull71 fun enableBLE(toggleScanSetting: Boolean): Boolean { 72 if (!state.eq(STATE_OFF)) { 73 throw IllegalStateException("Invalid call to enableBLE while current state is: $state") 74 } 75 if (toggleScanSetting) { 76 Log.d(TAG, "Allowing the scan to be perform while Bluetooth is OFF") 77 Settings.Global.putInt(context.contentResolver, BLE_SCAN_ALWAYS_AVAILABLE, 1) 78 for (i in 1..10) { 79 if (adapter.isBleScanAlwaysAvailable()) { 80 break 81 } 82 Log.d(TAG, "Ble scan not yet available... Sleeping 50 ms $i/10") 83 Thread.sleep(50) 84 } 85 if (!adapter.isBleScanAlwaysAvailable()) { 86 throw IllegalStateException("Could not enable BLE scan") 87 } 88 } 89 Log.d(TAG, "Call to enableBLE") 90 if (!withPermissions(BLUETOOTH_CONNECT).use { adapter.enableBLE() }) { 91 Log.e(TAG, "enableBLE: Failed") 92 return false 93 } 94 return state.waitForStateWithTimeout(stateChangeTimeout, STATE_BLE_ON) 95 } 96 97 /** Restore Bluetooth to OFF. Only works if it was in BLE_ON due to enableBLE call */ 98 @JvmStatic disableBLEnull99 fun disableBLE(): Boolean { 100 if (!state.eq(STATE_BLE_ON)) { 101 throw IllegalStateException("Invalid call to disableBLE while current state is: $state") 102 } 103 Log.d(TAG, "Call to disableBLE") 104 if (!withPermissions(BLUETOOTH_CONNECT).use { adapter.disableBLE() }) { 105 Log.e(TAG, "disableBLE: Failed") 106 return false 107 } 108 Log.d(TAG, "Disallowing the scan to be perform while Bluetooth is OFF") 109 Settings.Global.putInt(context.contentResolver, BLE_SCAN_ALWAYS_AVAILABLE, 0) 110 return state.waitForStateWithTimeout(stateChangeTimeout, STATE_OFF) 111 } 112 113 /** Turn Bluetooth ON and wait for state change */ 114 @JvmStatic enablenull115 fun enable(): Boolean { 116 if (state.eq(STATE_ON)) { 117 Log.i(TAG, "enable: state is already $state") 118 return true 119 } 120 Log.d(TAG, "Call to enable") 121 if ( 122 !withPermissions(BLUETOOTH_CONNECT, BLUETOOTH_PRIVILEGED).use { 123 @Suppress("DEPRECATION") adapter.enable() 124 } 125 ) { 126 Log.e(TAG, "enable: Failed") 127 return false 128 } 129 return state.waitForStateWithTimeout(stateChangeTimeout, STATE_ON) 130 } 131 132 /** Turn Bluetooth OFF and wait for state change */ 133 @JvmStatic disablenull134 fun disable(persist: Boolean = true): Boolean { 135 if (state.eq(STATE_OFF)) { 136 Log.i(TAG, "disable: state is already $state") 137 return true 138 } 139 Log.d(TAG, "Call to disable($persist)") 140 if ( 141 !withPermissions(BLUETOOTH_CONNECT, BLUETOOTH_PRIVILEGED).use { 142 adapter.disable(persist) 143 } 144 ) { 145 Log.e(TAG, "disable: Failed") 146 return false 147 } 148 // Notify that disable was call. 149 state.wasDisabled = true 150 return state.waitForStateWithTimeout(stateChangeTimeout, STATE_OFF) 151 } 152 } 153 154 private class AdapterStateListener(context: Context, private val adapter: BluetoothAdapter) { 155 private val STATE_UNKNOWN = -42 156 private val STATE_BLE_TURNING_ON = 14 // BluetoothAdapter.STATE_BLE_TURNING_ON 157 private val STATE_BLE_TURNING_OFF = 16 // BluetoothAdapter.STATE_BLE_TURNING_OFF 158 159 // Set to true once a call to disable is made, in order to force the differentiation between the 160 // various state hidden within STATE_OFF (OFF, BLE_TURNING_ON, BLE_TURNING_OFF) 161 // Once true, getter will return STATE_OFF when there has not been any callback sent to it 162 var wasDisabled = false 163 164 val adapterStateFlow = <lambda>null165 callbackFlow<Intent> { 166 val broadcastReceiver = 167 object : BroadcastReceiver() { 168 override fun onReceive(context: Context, intent: Intent) { 169 trySendBlocking(intent) 170 } 171 } 172 context.registerReceiver(broadcastReceiver, IntentFilter(ACTION_BLE_STATE_CHANGED)) 173 174 awaitClose { context.unregisterReceiver(broadcastReceiver) } 175 } <lambda>null176 .map { it.getIntExtra(BluetoothAdapter.EXTRA_STATE, -1) } <lambda>null177 .onEach { Log.d(TAG, "State changed to ${nameForState(it)}") } 178 .shareIn(CoroutineScope(Dispatchers.Default), SharingStarted.Eagerly, 1) 179 getnull180 private fun get(): Int = 181 adapterStateFlow.replayCache.getOrElse(0) { 182 val state: Int = adapter.getState() 183 if (state != STATE_OFF) { 184 state 185 } else if (adapter.isLeEnabled()) { 186 STATE_BLE_ON 187 } else if (wasDisabled) { 188 STATE_OFF 189 } else { 190 STATE_UNKNOWN 191 } 192 } 193 eqnull194 fun eq(state: Int): Boolean = state == get() 195 196 override fun toString(): String { 197 return nameForState(get()) 198 } 199 200 // Cts cannot use BluetoothAdapter.nameForState prior to T, some module test on R nameForStatenull201 private fun nameForState(state: Int): String { 202 return when (state) { 203 STATE_UNKNOWN -> "UNKNOWN: State is oneOf(OFF, BLE_TURNING_ON, BLE_TURNING_OFF)" 204 STATE_OFF -> "OFF" 205 STATE_TURNING_ON -> "TURNING_ON" 206 STATE_ON -> "ON" 207 STATE_TURNING_OFF -> "TURNING_OFF" 208 STATE_BLE_TURNING_ON -> "BLE_TURNING_ON" 209 STATE_BLE_ON -> "BLE_ON" 210 STATE_BLE_TURNING_OFF -> "BLE_TURNING_OFF" 211 else -> "?!?!? ($state) ?!?!? " 212 } 213 } 214 <lambda>null215 fun waitForStateWithTimeout(timeout: Duration, state: Int): Boolean = runBlocking { 216 withTimeoutOrNull(timeout) { adapterStateFlow.filter { it == state }.first() } != null 217 } 218 } 219