blob: 32d37fc096e374a0f50f25aae9374c088892318d [file] [log] [blame]
/*
* Copyright (C) 2020 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.cts;
import static android.Manifest.permission.BLUETOOTH_CONNECT;
import static android.Manifest.permission.BLUETOOTH_PRIVILEGED;
import android.bluetooth.BluetoothAdapter;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.util.Log;
import android.util.SparseIntArray;
import androidx.test.platform.app.InstrumentationRegistry;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
/**
* Utility for controlling the Bluetooth adapter from CTS test.
*/
public class BTAdapterUtils {
private static final String TAG = "BTAdapterUtils";
private static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG);
// ADAPTER_ENABLE_TIMEOUT_MS = AdapterState.BLE_START_TIMEOUT_DELAY +
// AdapterState.BREDR_START_TIMEOUT_DELAY
private static final int ADAPTER_ENABLE_TIMEOUT_MS = 8000;
// ADAPTER_DISABLE_TIMEOUT_MS = AdapterState.BLE_STOP_TIMEOUT_DELAY +
// AdapterState.BREDR_STOP_TIMEOUT_DELAY
private static final int ADAPTER_DISABLE_TIMEOUT_MS = 5000;
public static final int STATE_BLE_TURNING_ON = 14;
public static final int STATE_BLE_ON = 15;
public static final int STATE_BLE_TURNING_OFF = 16;
private static final SparseIntArray sStateTimeouts = new SparseIntArray();
static {
sStateTimeouts.put(BluetoothAdapter.STATE_OFF, ADAPTER_DISABLE_TIMEOUT_MS);
sStateTimeouts.put(BluetoothAdapter.STATE_TURNING_ON, ADAPTER_ENABLE_TIMEOUT_MS);
sStateTimeouts.put(BluetoothAdapter.STATE_ON, ADAPTER_ENABLE_TIMEOUT_MS);
sStateTimeouts.put(BluetoothAdapter.STATE_TURNING_OFF, ADAPTER_DISABLE_TIMEOUT_MS);
sStateTimeouts.put(STATE_BLE_TURNING_ON, ADAPTER_ENABLE_TIMEOUT_MS);
sStateTimeouts.put(STATE_BLE_ON, ADAPTER_ENABLE_TIMEOUT_MS);
sStateTimeouts.put(STATE_BLE_TURNING_OFF, ADAPTER_DISABLE_TIMEOUT_MS);
}
private static BluetoothAdapterReceiver sAdapterReceiver;
private static boolean sAdapterVarsInitialized;
private static ReentrantLock sBluetoothAdapterLock;
private static Condition sConditionAdapterStateReached;
private static int sDesiredState;
private static int sAdapterState;
/**
* Handles BluetoothAdapter state changes and signals when we have reached a desired state
*/
private static class BluetoothAdapterReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (BluetoothAdapter.ACTION_BLE_STATE_CHANGED.equals(action)) {
int newState = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, -1);
if (DBG) {
Log.d(TAG, "Bluetooth adapter state changed: " + newState);
}
// Signal if the state is set to the one we are waiting on
sBluetoothAdapterLock.lock();
sAdapterState = newState;
try {
if (sDesiredState == newState) {
if (DBG) {
Log.d(TAG, "Adapter has reached desired state: " + sDesiredState);
}
sConditionAdapterStateReached.signal();
}
} finally {
sBluetoothAdapterLock.unlock();
}
}
}
}
/**
* Initialize all static state variables
*/
private static void initAdapterStateVariables(Context context) {
if (DBG) {
Log.d(TAG, "Initializing adapter state variables");
}
sAdapterReceiver = new BluetoothAdapterReceiver();
sBluetoothAdapterLock = new ReentrantLock();
sConditionAdapterStateReached = sBluetoothAdapterLock.newCondition();
sDesiredState = -1;
sAdapterState = -1;
IntentFilter filter = new IntentFilter(BluetoothAdapter.ACTION_BLE_STATE_CHANGED);
context.registerReceiver(sAdapterReceiver, filter);
sAdapterVarsInitialized = true;
}
/**
* Wait for the bluetooth adapter to be in a given state
*
* Assumes all state variables are initialized. Assumes it's being run with
* sBluetoothAdapterLock in the locked state.
*/
private static boolean waitForAdapterStateLocked(int desiredState, BluetoothAdapter adapter)
throws InterruptedException {
int timeout = sStateTimeouts.get(desiredState, ADAPTER_ENABLE_TIMEOUT_MS);
if (DBG) {
Log.d(TAG, "Waiting for adapter state " + desiredState);
}
sDesiredState = desiredState;
// Wait until we have reached the desired state
while (desiredState != sAdapterState) {
if (!sConditionAdapterStateReached.await(timeout, TimeUnit.MILLISECONDS)) {
// Handle situation where state change occurs, but we don't receive the broadcast
if (desiredState >= BluetoothAdapter.STATE_OFF
&& desiredState <= BluetoothAdapter.STATE_TURNING_OFF) {
return adapter.getState() == desiredState;
} else if (desiredState == STATE_BLE_ON) {
Log.d(TAG, "adapter isLeEnabled: " + adapter.isLeEnabled());
return adapter.isLeEnabled();
}
Log.e(TAG, "Timeout while waiting for Bluetooth adapter state " + desiredState
+ " while current state is " + sAdapterState);
break;
}
}
if (DBG) {
Log.d(TAG, "Final state while waiting: " + sAdapterState);
}
return sAdapterState == desiredState;
}
/**
* Utility method to wait on any specific adapter state
*/
public static boolean waitForAdapterState(int desiredState, BluetoothAdapter adapter) {
sBluetoothAdapterLock.lock();
try {
return waitForAdapterStateLocked(desiredState, adapter);
} catch (InterruptedException e) {
Log.w(TAG, "waitForAdapterState(): interrupted", e);
} finally {
sBluetoothAdapterLock.unlock();
}
return false;
}
/**
* Enables Bluetooth to a Low Energy only mode
*/
public static boolean enableBLE(BluetoothAdapter bluetoothAdapter, Context context) {
if (!sAdapterVarsInitialized) {
initAdapterStateVariables(context);
}
if (bluetoothAdapter.isLeEnabled()) {
return true;
}
sBluetoothAdapterLock.lock();
try {
if (DBG) {
Log.d(TAG, "Enabling Bluetooth low energy only mode");
}
if (!bluetoothAdapter.enableBLE()) {
Log.e(TAG, "Unable to enable Bluetooth low energy only mode");
return false;
}
return waitForAdapterStateLocked(STATE_BLE_ON, bluetoothAdapter);
} catch (InterruptedException e) {
Log.w(TAG, "enableBLE(): interrupted", e);
} finally {
sBluetoothAdapterLock.unlock();
}
return false;
}
/**
* Disable Bluetooth Low Energy mode
*/
public static boolean disableBLE(BluetoothAdapter bluetoothAdapter, Context context) {
if (!sAdapterVarsInitialized) {
initAdapterStateVariables(context);
}
if (bluetoothAdapter.getState() == BluetoothAdapter.STATE_OFF) {
return true;
}
sBluetoothAdapterLock.lock();
try {
if (DBG) {
Log.d(TAG, "Disabling Bluetooth low energy");
}
bluetoothAdapter.disableBLE();
return waitForAdapterStateLocked(BluetoothAdapter.STATE_OFF, bluetoothAdapter);
} catch (InterruptedException e) {
Log.w(TAG, "disableBLE(): interrupted", e);
} finally {
sBluetoothAdapterLock.unlock();
}
return false;
}
/**
* Enables the Bluetooth Adapter. Return true if it is already enabled or is enabled.
*/
public static boolean enableAdapter(BluetoothAdapter bluetoothAdapter, Context context) {
if (!sAdapterVarsInitialized) {
initAdapterStateVariables(context);
}
if (bluetoothAdapter.isEnabled()) {
return true;
}
Set<String> permissionsAdopted = getPermissionsAdoptedAsShellUid();
sBluetoothAdapterLock.lock();
try {
if (DBG) {
Log.d(TAG, "Enabling Bluetooth adapter");
}
adoptPermissionAsShellUid(BLUETOOTH_CONNECT, BLUETOOTH_PRIVILEGED);
bluetoothAdapter.enable();
return waitForAdapterStateLocked(BluetoothAdapter.STATE_ON, bluetoothAdapter);
} catch (InterruptedException e) {
Log.w(TAG, "enableAdapter(): interrupted", e);
} finally {
adoptPermissionAsShellUid(permissionsAdopted.toArray(new String[0]));
sBluetoothAdapterLock.unlock();
}
return false;
}
/**
* Disable the Bluetooth Adapter. Return true if it is already disabled or is disabled.
*/
public static boolean disableAdapter(BluetoothAdapter bluetoothAdapter, Context context) {
if (!sAdapterVarsInitialized) {
initAdapterStateVariables(context);
}
if (bluetoothAdapter.getState() == BluetoothAdapter.STATE_OFF) {
return true;
}
if (DBG) {
Log.d(TAG, "Disabling Bluetooth adapter");
}
Set<String> permissionsAdopted = getPermissionsAdoptedAsShellUid();
sBluetoothAdapterLock.lock();
try {
adoptPermissionAsShellUid(BLUETOOTH_CONNECT, BLUETOOTH_PRIVILEGED);
bluetoothAdapter.disable();
return waitForAdapterStateLocked(BluetoothAdapter.STATE_OFF, bluetoothAdapter);
} catch (InterruptedException e) {
Log.w(TAG, "disableAdapter(): interrupted", e);
} finally {
adoptPermissionAsShellUid(permissionsAdopted.toArray(new String[0]));
sBluetoothAdapterLock.unlock();
}
return false;
}
/**
* Disable the Bluetooth Adapter with then option to persist the off state or not.
*
* Returns true if the adapter is already disabled or was disabled.
*/
public static boolean disableAdapter(BluetoothAdapter bluetoothAdapter, boolean persist,
Context context) {
if (!sAdapterVarsInitialized) {
initAdapterStateVariables(context);
}
if (bluetoothAdapter.getState() == BluetoothAdapter.STATE_OFF) {
return true;
}
Set<String> permissionsAdopted = getPermissionsAdoptedAsShellUid();
sBluetoothAdapterLock.lock();
try {
if (DBG) {
Log.d(TAG, "Disabling Bluetooth adapter, persist=" + persist);
}
adoptPermissionAsShellUid(BLUETOOTH_CONNECT, BLUETOOTH_PRIVILEGED);
bluetoothAdapter.disable(persist);
return waitForAdapterStateLocked(BluetoothAdapter.STATE_OFF, bluetoothAdapter);
} catch (InterruptedException e) {
Log.w(TAG, "disableAdapter(persist=" + persist + "): interrupted", e);
} finally {
adoptPermissionAsShellUid(permissionsAdopted.toArray(new String[0]));
sBluetoothAdapterLock.unlock();
}
return false;
}
/**
* Adopt shell UID's permission via {@link android.app.UiAutomation}
* @param permission permission to adopt
*/
private static void adoptPermissionAsShellUid(String... permission) {
InstrumentationRegistry.getInstrumentation().getUiAutomation()
.adoptShellPermissionIdentity(permission);
}
/**
* Gets all the permissions adopted as the shell UID
*
* @return a {@link java.util.Set} of the adopted shell permissions
*/
private static Set<String> getPermissionsAdoptedAsShellUid() {
return InstrumentationRegistry.getInstrumentation().getUiAutomation()
.getAdoptedShellPermissions();
}
}