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 return state.waitForStateWithTimeout(stateChangeTimeout, STATE_OFF) 149 } 150 } 151 152 private class AdapterStateListener(context: Context, private val adapter: BluetoothAdapter) { 153 private val STATE_BLE_TURNING_ON = 14 // BluetoothAdapter.STATE_BLE_TURNING_ON 154 private val STATE_BLE_TURNING_OFF = 16 // BluetoothAdapter.STATE_BLE_TURNING_OFF 155 156 val adapterStateFlow = <lambda>null157 callbackFlow<Intent> { 158 val broadcastReceiver = 159 object : BroadcastReceiver() { 160 override fun onReceive(context: Context, intent: Intent) { 161 trySendBlocking(intent) 162 } 163 } 164 context.registerReceiver(broadcastReceiver, IntentFilter(ACTION_BLE_STATE_CHANGED)) 165 166 awaitClose { context.unregisterReceiver(broadcastReceiver) } 167 } <lambda>null168 .map { it.getIntExtra(BluetoothAdapter.EXTRA_STATE, -1) } <lambda>null169 .onEach { Log.d(TAG, "State changed to ${nameForState(it)}") } 170 .shareIn(CoroutineScope(Dispatchers.Default), SharingStarted.Eagerly, 1) 171 getnull172 private fun get(): Int = 173 adapterStateFlow.replayCache.getOrElse(0) { 174 val state: Int = adapter.getState() 175 if (state != STATE_OFF) { 176 state 177 } else if (adapter.isLeEnabled()) { 178 STATE_BLE_ON 179 } else { 180 STATE_OFF 181 } 182 } 183 eqnull184 fun eq(state: Int): Boolean = state == get() 185 186 override fun toString(): String { 187 return nameForState(get()) 188 } 189 190 // Cts cannot use BluetoothAdapter.nameForState prior to T, some module test on R nameForStatenull191 private fun nameForState(state: Int): String { 192 return when (state) { 193 STATE_OFF -> "OFF" 194 STATE_TURNING_ON -> "TURNING_ON" 195 STATE_ON -> "ON" 196 STATE_TURNING_OFF -> "TURNING_OFF" 197 STATE_BLE_TURNING_ON -> "BLE_TURNING_ON" 198 STATE_BLE_ON -> "BLE_ON" 199 STATE_BLE_TURNING_OFF -> "BLE_TURNING_OFF" 200 else -> "?!?!? ($state) ?!?!? " 201 } 202 } 203 <lambda>null204 fun waitForStateWithTimeout(timeout: Duration, state: Int): Boolean = runBlocking { 205 withTimeoutOrNull(timeout) { adapterStateFlow.filter { it == state }.first() } != null 206 } 207 } 208