blob: 424beb79aafc765edfa54f165b829c447672ce95 [file]
/*
* Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.bluetooth.test_utils
import android.Manifest.permission.BLUETOOTH_CONNECT
import android.Manifest.permission.BLUETOOTH_PRIVILEGED
import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothAdapter.ACTION_BLE_STATE_CHANGED
import android.bluetooth.BluetoothAdapter.STATE_BLE_ON
import android.bluetooth.BluetoothAdapter.STATE_OFF
import android.bluetooth.BluetoothAdapter.STATE_ON
import android.bluetooth.BluetoothAdapter.STATE_TURNING_OFF
import android.bluetooth.BluetoothAdapter.STATE_TURNING_ON
import android.bluetooth.BluetoothManager
import android.bluetooth.test_utils.Permissions.withPermissions
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.provider.Settings
import android.util.Log
import androidx.test.platform.app.InstrumentationRegistry
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.channels.trySendBlocking
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withTimeoutOrNull
private const val TAG: String = "BlockingBluetoothAdapter"
// There is no access to the module only API Settings.Global.BLE_SCAN_ALWAYS_AVAILABLE
private const val BLE_SCAN_ALWAYS_AVAILABLE = "ble_scan_always_enabled"
object BlockingBluetoothAdapter {
private val context = InstrumentationRegistry.getInstrumentation().getContext()
@JvmStatic val adapter = context.getSystemService(BluetoothManager::class.java).getAdapter()
private val state = AdapterStateListener(context, adapter)
// BLE_START_TIMEOUT_DELAY + BREDR_START_TIMEOUT_DELAY + (10 seconds of additional delay)
private val stateChangeTimeout = 18.seconds
init {
Log.d(TAG, "Started with initial state to $state")
}
/** Set Bluetooth in BLE mode. Only works if it was OFF before */
@JvmStatic
fun enableBLE(toggleScanSetting: Boolean): Boolean {
if (!state.eq(STATE_OFF)) {
throw IllegalStateException("Invalid call to enableBLE while current state is: $state")
}
if (toggleScanSetting) {
Log.d(TAG, "Allowing the scan to be perform while Bluetooth is OFF")
Settings.Global.putInt(context.contentResolver, BLE_SCAN_ALWAYS_AVAILABLE, 1)
for (i in 1..5) {
if (adapter.isBleScanAlwaysAvailable()) {
break
}
Log.d(TAG, "Ble scan not yet available… Sleeping 20 ms $i/5")
Thread.sleep(20)
}
if (!adapter.isBleScanAlwaysAvailable()) {
throw IllegalStateException("Could not enable BLE scan")
}
}
Log.d(TAG, "Call to enableBLE")
if (!withPermissions(BLUETOOTH_CONNECT).use { adapter.enableBLE() }) {
Log.e(TAG, "enableBLE: Failed")
return false
}
return state.waitForStateWithTimeout(stateChangeTimeout, STATE_BLE_ON)
}
/** Restore Bluetooth to OFF. Only works if it was in BLE_ON due to enableBLE call */
@JvmStatic
fun disableBLE(): Boolean {
if (!state.eq(STATE_BLE_ON)) {
throw IllegalStateException("Invalid call to disableBLE while current state is: $state")
}
Log.d(TAG, "Call to disableBLE")
if (!withPermissions(BLUETOOTH_CONNECT).use { adapter.disableBLE() }) {
Log.e(TAG, "disableBLE: Failed")
return false
}
Log.d(TAG, "Disallowing the scan to be perform while Bluetooth is OFF")
Settings.Global.putInt(context.contentResolver, BLE_SCAN_ALWAYS_AVAILABLE, 0)
return state.waitForStateWithTimeout(stateChangeTimeout, STATE_OFF)
}
/** Turn Bluetooth ON and wait for state change */
@JvmStatic
fun enable(): Boolean {
if (state.eq(STATE_ON)) {
Log.i(TAG, "enable: state is already $state")
return true
}
Log.d(TAG, "Call to enable")
if (
!withPermissions(BLUETOOTH_CONNECT, BLUETOOTH_PRIVILEGED).use {
@Suppress("DEPRECATION") adapter.enable()
}
) {
Log.e(TAG, "enable: Failed")
return false
}
return state.waitForStateWithTimeout(stateChangeTimeout, STATE_ON)
}
/** Turn Bluetooth OFF and wait for state change */
@JvmStatic
fun disable(persist: Boolean = true): Boolean {
if (state.eq(STATE_OFF)) {
Log.i(TAG, "disable: state is already $state")
return true
}
Log.d(TAG, "Call to disable($persist)")
if (
!withPermissions(BLUETOOTH_CONNECT, BLUETOOTH_PRIVILEGED).use {
adapter.disable(persist)
}
) {
Log.e(TAG, "disable: Failed")
return false
}
// Notify that disable was call.
state.wasDisabled = true
return state.waitForStateWithTimeout(stateChangeTimeout, STATE_OFF)
}
}
private class AdapterStateListener(context: Context, private val adapter: BluetoothAdapter) {
private val STATE_UNKNOWN = -42
private val STATE_BLE_TURNING_ON = 14 // BluetoothAdapter.STATE_BLE_TURNING_ON
private val STATE_BLE_TURNING_OFF = 16 // BluetoothAdapter.STATE_BLE_TURNING_OFF
// Set to true once a call to disable is made, in order to force the differentiation between the
// various state hidden within STATE_OFF (OFF, BLE_TURNING_ON, BLE_TURNING_OFF)
// Once true, getter will return STATE_OFF when there has not been any callback sent to it
var wasDisabled = false
val adapterStateFlow =
callbackFlow<Intent> {
val broadcastReceiver =
object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
trySendBlocking(intent)
}
}
context.registerReceiver(broadcastReceiver, IntentFilter(ACTION_BLE_STATE_CHANGED))
awaitClose { context.unregisterReceiver(broadcastReceiver) }
}
.map { it.getIntExtra(BluetoothAdapter.EXTRA_STATE, -1) }
.onEach { Log.d(TAG, "State changed to ${nameForState(it)}") }
.shareIn(CoroutineScope(Dispatchers.Default), SharingStarted.Eagerly, 1)
private fun get(): Int =
adapterStateFlow.replayCache.getOrElse(0) {
val state: Int = adapter.getState()
if (state != STATE_OFF) {
state
} else if (adapter.isLeEnabled()) {
STATE_BLE_ON
} else if (wasDisabled) {
STATE_OFF
} else {
STATE_UNKNOWN
}
}
fun eq(state: Int): Boolean = state == get()
override fun toString(): String {
return nameForState(get())
}
// Cts cannot use BluetoothAdapter.nameForState prior to T, some module test on R
private fun nameForState(state: Int): String {
return when (state) {
STATE_UNKNOWN -> "UNKNOWN: State is oneOf(OFF, BLE_TURNING_ON, BLE_TURNING_OFF)"
STATE_OFF -> "OFF"
STATE_TURNING_ON -> "TURNING_ON"
STATE_ON -> "ON"
STATE_TURNING_OFF -> "TURNING_OFF"
STATE_BLE_TURNING_ON -> "BLE_TURNING_ON"
STATE_BLE_ON -> "BLE_ON"
STATE_BLE_TURNING_OFF -> "BLE_TURNING_OFF"
else -> "?!?!? ($state) ?!?!? "
}
}
fun waitForStateWithTimeout(timeout: Duration, state: Int): Boolean = runBlocking {
withTimeoutOrNull(timeout) { adapterStateFlow.filter { it == state }.first() } != null
}
}