• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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