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