1 /* 2 * Copyright (C) 2020 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 17 package android.bluetooth.cts; 18 19 import static android.Manifest.permission.BLUETOOTH_CONNECT; 20 import static android.Manifest.permission.BLUETOOTH_PRIVILEGED; 21 22 import android.bluetooth.BluetoothAdapter; 23 import android.content.BroadcastReceiver; 24 import android.content.Context; 25 import android.content.Intent; 26 import android.content.IntentFilter; 27 import android.util.Log; 28 import android.util.SparseIntArray; 29 30 import androidx.test.platform.app.InstrumentationRegistry; 31 32 import java.util.Set; 33 import java.util.concurrent.TimeUnit; 34 import java.util.concurrent.locks.Condition; 35 import java.util.concurrent.locks.ReentrantLock; 36 37 /** 38 * Utility for controlling the Bluetooth adapter from CTS test. 39 */ 40 public class BTAdapterUtils { 41 private static final String TAG = "BTAdapterUtils"; 42 private static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG); 43 44 // ADAPTER_ENABLE_TIMEOUT_MS = AdapterState.BLE_START_TIMEOUT_DELAY + 45 // AdapterState.BREDR_START_TIMEOUT_DELAY 46 private static final int ADAPTER_ENABLE_TIMEOUT_MS = 8000; 47 // ADAPTER_DISABLE_TIMEOUT_MS = AdapterState.BLE_STOP_TIMEOUT_DELAY + 48 // AdapterState.BREDR_STOP_TIMEOUT_DELAY 49 private static final int ADAPTER_DISABLE_TIMEOUT_MS = 5000; 50 51 public static final int STATE_BLE_TURNING_ON = 14; 52 public static final int STATE_BLE_ON = 15; 53 public static final int STATE_BLE_TURNING_OFF = 16; 54 55 private static final SparseIntArray sStateTimeouts = new SparseIntArray(); 56 static { sStateTimeouts.put(BluetoothAdapter.STATE_OFF, ADAPTER_DISABLE_TIMEOUT_MS)57 sStateTimeouts.put(BluetoothAdapter.STATE_OFF, ADAPTER_DISABLE_TIMEOUT_MS); sStateTimeouts.put(BluetoothAdapter.STATE_TURNING_ON, ADAPTER_ENABLE_TIMEOUT_MS)58 sStateTimeouts.put(BluetoothAdapter.STATE_TURNING_ON, ADAPTER_ENABLE_TIMEOUT_MS); sStateTimeouts.put(BluetoothAdapter.STATE_ON, ADAPTER_ENABLE_TIMEOUT_MS)59 sStateTimeouts.put(BluetoothAdapter.STATE_ON, ADAPTER_ENABLE_TIMEOUT_MS); sStateTimeouts.put(BluetoothAdapter.STATE_TURNING_OFF, ADAPTER_DISABLE_TIMEOUT_MS)60 sStateTimeouts.put(BluetoothAdapter.STATE_TURNING_OFF, ADAPTER_DISABLE_TIMEOUT_MS); sStateTimeouts.put(STATE_BLE_TURNING_ON, ADAPTER_ENABLE_TIMEOUT_MS)61 sStateTimeouts.put(STATE_BLE_TURNING_ON, ADAPTER_ENABLE_TIMEOUT_MS); sStateTimeouts.put(STATE_BLE_ON, ADAPTER_ENABLE_TIMEOUT_MS)62 sStateTimeouts.put(STATE_BLE_ON, ADAPTER_ENABLE_TIMEOUT_MS); sStateTimeouts.put(STATE_BLE_TURNING_OFF, ADAPTER_DISABLE_TIMEOUT_MS)63 sStateTimeouts.put(STATE_BLE_TURNING_OFF, ADAPTER_DISABLE_TIMEOUT_MS); 64 } 65 66 private static BluetoothAdapterReceiver sAdapterReceiver; 67 68 private static boolean sAdapterVarsInitialized; 69 private static ReentrantLock sBluetoothAdapterLock; 70 private static Condition sConditionAdapterStateReached; 71 private static int sDesiredState; 72 private static int sAdapterState; 73 74 /** 75 * Handles BluetoothAdapter state changes and signals when we have reached a desired state 76 */ 77 private static class BluetoothAdapterReceiver extends BroadcastReceiver { 78 @Override onReceive(Context context, Intent intent)79 public void onReceive(Context context, Intent intent) { 80 String action = intent.getAction(); 81 if (BluetoothAdapter.ACTION_BLE_STATE_CHANGED.equals(action)) { 82 int newState = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, -1); 83 if (DBG) { 84 Log.d(TAG, "Bluetooth adapter state changed: " + newState); 85 } 86 87 // Signal if the state is set to the one we are waiting on 88 sBluetoothAdapterLock.lock(); 89 sAdapterState = newState; 90 try { 91 if (sDesiredState == newState) { 92 if (DBG) { 93 Log.d(TAG, "Adapter has reached desired state: " + sDesiredState); 94 } 95 sConditionAdapterStateReached.signal(); 96 } 97 } finally { 98 sBluetoothAdapterLock.unlock(); 99 } 100 } 101 } 102 } 103 104 /** 105 * Initialize all static state variables 106 */ initAdapterStateVariables(Context context)107 private static void initAdapterStateVariables(Context context) { 108 if (DBG) { 109 Log.d(TAG, "Initializing adapter state variables"); 110 } 111 sAdapterReceiver = new BluetoothAdapterReceiver(); 112 sBluetoothAdapterLock = new ReentrantLock(); 113 sConditionAdapterStateReached = sBluetoothAdapterLock.newCondition(); 114 sDesiredState = -1; 115 sAdapterState = -1; 116 IntentFilter filter = new IntentFilter(BluetoothAdapter.ACTION_BLE_STATE_CHANGED); 117 context.registerReceiver(sAdapterReceiver, filter); 118 sAdapterVarsInitialized = true; 119 } 120 121 /** 122 * Wait for the bluetooth adapter to be in a given state 123 * 124 * Assumes all state variables are initialized. Assumes it's being run with 125 * sBluetoothAdapterLock in the locked state. 126 */ waitForAdapterStateLocked(int desiredState, BluetoothAdapter adapter)127 private static boolean waitForAdapterStateLocked(int desiredState, BluetoothAdapter adapter) 128 throws InterruptedException { 129 int timeout = sStateTimeouts.get(desiredState, ADAPTER_ENABLE_TIMEOUT_MS); 130 131 if (DBG) { 132 Log.d(TAG, "Waiting for adapter state " + desiredState); 133 } 134 sDesiredState = desiredState; 135 136 // Wait until we have reached the desired state 137 while (desiredState != sAdapterState) { 138 if (!sConditionAdapterStateReached.await(timeout, TimeUnit.MILLISECONDS)) { 139 // Handle situation where state change occurs, but we don't receive the broadcast 140 if (desiredState >= BluetoothAdapter.STATE_OFF 141 && desiredState <= BluetoothAdapter.STATE_TURNING_OFF) { 142 return adapter.getState() == desiredState; 143 } else if (desiredState == STATE_BLE_ON) { 144 Log.d(TAG, "adapter isLeEnabled: " + adapter.isLeEnabled()); 145 return adapter.isLeEnabled(); 146 } 147 Log.e(TAG, "Timeout while waiting for Bluetooth adapter state " + desiredState 148 + " while current state is " + sAdapterState); 149 break; 150 } 151 } 152 153 if (DBG) { 154 Log.d(TAG, "Final state while waiting: " + sAdapterState); 155 } 156 157 return sAdapterState == desiredState; 158 } 159 160 /** 161 * Utility method to wait on any specific adapter state 162 */ waitForAdapterState(int desiredState, BluetoothAdapter adapter)163 public static boolean waitForAdapterState(int desiredState, BluetoothAdapter adapter) { 164 sBluetoothAdapterLock.lock(); 165 try { 166 return waitForAdapterStateLocked(desiredState, adapter); 167 } catch (InterruptedException e) { 168 Log.w(TAG, "waitForAdapterState(): interrupted", e); 169 } finally { 170 sBluetoothAdapterLock.unlock(); 171 } 172 return false; 173 } 174 175 /** 176 * Enables Bluetooth to a Low Energy only mode 177 */ enableBLE(BluetoothAdapter bluetoothAdapter, Context context)178 public static boolean enableBLE(BluetoothAdapter bluetoothAdapter, Context context) { 179 if (!sAdapterVarsInitialized) { 180 initAdapterStateVariables(context); 181 } 182 183 if (bluetoothAdapter.isLeEnabled()) { 184 return true; 185 } 186 187 sBluetoothAdapterLock.lock(); 188 try { 189 if (DBG) { 190 Log.d(TAG, "Enabling Bluetooth low energy only mode"); 191 } 192 if (!bluetoothAdapter.enableBLE()) { 193 Log.e(TAG, "Unable to enable Bluetooth low energy only mode"); 194 return false; 195 } 196 return waitForAdapterStateLocked(STATE_BLE_ON, bluetoothAdapter); 197 } catch (InterruptedException e) { 198 Log.w(TAG, "enableBLE(): interrupted", e); 199 } finally { 200 sBluetoothAdapterLock.unlock(); 201 } 202 return false; 203 } 204 205 /** 206 * Disable Bluetooth Low Energy mode 207 */ disableBLE(BluetoothAdapter bluetoothAdapter, Context context)208 public static boolean disableBLE(BluetoothAdapter bluetoothAdapter, Context context) { 209 if (!sAdapterVarsInitialized) { 210 initAdapterStateVariables(context); 211 } 212 213 if (bluetoothAdapter.getState() == BluetoothAdapter.STATE_OFF) { 214 return true; 215 } 216 217 sBluetoothAdapterLock.lock(); 218 try { 219 if (DBG) { 220 Log.d(TAG, "Disabling Bluetooth low energy"); 221 } 222 bluetoothAdapter.disableBLE(); 223 return waitForAdapterStateLocked(BluetoothAdapter.STATE_OFF, bluetoothAdapter); 224 } catch (InterruptedException e) { 225 Log.w(TAG, "disableBLE(): interrupted", e); 226 } finally { 227 sBluetoothAdapterLock.unlock(); 228 } 229 return false; 230 } 231 232 /** 233 * Enables the Bluetooth Adapter. Return true if it is already enabled or is enabled. 234 */ enableAdapter(BluetoothAdapter bluetoothAdapter, Context context)235 public static boolean enableAdapter(BluetoothAdapter bluetoothAdapter, Context context) { 236 if (!sAdapterVarsInitialized) { 237 initAdapterStateVariables(context); 238 } 239 240 if (bluetoothAdapter.isEnabled()) { 241 return true; 242 } 243 244 Set<String> permissionsAdopted = getPermissionsAdoptedAsShellUid(); 245 sBluetoothAdapterLock.lock(); 246 try { 247 if (DBG) { 248 Log.d(TAG, "Enabling Bluetooth adapter"); 249 } 250 adoptPermissionAsShellUid(BLUETOOTH_CONNECT, BLUETOOTH_PRIVILEGED); 251 bluetoothAdapter.enable(); 252 return waitForAdapterStateLocked(BluetoothAdapter.STATE_ON, bluetoothAdapter); 253 } catch (InterruptedException e) { 254 Log.w(TAG, "enableAdapter(): interrupted", e); 255 } finally { 256 adoptPermissionAsShellUid(permissionsAdopted.toArray(new String[0])); 257 sBluetoothAdapterLock.unlock(); 258 } 259 return false; 260 } 261 262 /** 263 * Disable the Bluetooth Adapter. Return true if it is already disabled or is disabled. 264 */ disableAdapter(BluetoothAdapter bluetoothAdapter, Context context)265 public static boolean disableAdapter(BluetoothAdapter bluetoothAdapter, Context context) { 266 if (!sAdapterVarsInitialized) { 267 initAdapterStateVariables(context); 268 } 269 270 if (bluetoothAdapter.getState() == BluetoothAdapter.STATE_OFF) { 271 return true; 272 } 273 274 if (DBG) { 275 Log.d(TAG, "Disabling Bluetooth adapter"); 276 } 277 278 Set<String> permissionsAdopted = getPermissionsAdoptedAsShellUid(); 279 sBluetoothAdapterLock.lock(); 280 try { 281 adoptPermissionAsShellUid(BLUETOOTH_CONNECT, BLUETOOTH_PRIVILEGED); 282 bluetoothAdapter.disable(); 283 if (waitForAdapterStateLocked(BluetoothAdapter.STATE_OFF, bluetoothAdapter)) { 284 //TODO b/234892968 285 Thread.sleep(2000); 286 return true; 287 } 288 return false; 289 } catch (InterruptedException e) { 290 Log.w(TAG, "disableAdapter(): interrupted", e); 291 } finally { 292 adoptPermissionAsShellUid(permissionsAdopted.toArray(new String[0])); 293 sBluetoothAdapterLock.unlock(); 294 } 295 return false; 296 } 297 298 /** 299 * Disable the Bluetooth Adapter with then option to persist the off state or not. 300 * 301 * Returns true if the adapter is already disabled or was disabled. 302 */ disableAdapter(BluetoothAdapter bluetoothAdapter, boolean persist, Context context)303 public static boolean disableAdapter(BluetoothAdapter bluetoothAdapter, boolean persist, 304 Context context) { 305 if (!sAdapterVarsInitialized) { 306 initAdapterStateVariables(context); 307 } 308 309 if (bluetoothAdapter.getState() == BluetoothAdapter.STATE_OFF) { 310 return true; 311 } 312 313 Set<String> permissionsAdopted = getPermissionsAdoptedAsShellUid(); 314 sBluetoothAdapterLock.lock(); 315 try { 316 if (DBG) { 317 Log.d(TAG, "Disabling Bluetooth adapter, persist=" + persist); 318 } 319 adoptPermissionAsShellUid(BLUETOOTH_CONNECT, BLUETOOTH_PRIVILEGED); 320 bluetoothAdapter.disable(persist); 321 return waitForAdapterStateLocked(BluetoothAdapter.STATE_OFF, bluetoothAdapter); 322 } catch (InterruptedException e) { 323 Log.w(TAG, "disableAdapter(persist=" + persist + "): interrupted", e); 324 } finally { 325 adoptPermissionAsShellUid(permissionsAdopted.toArray(new String[0])); 326 sBluetoothAdapterLock.unlock(); 327 } 328 return false; 329 } 330 331 /** 332 * Adopt shell UID's permission via {@link android.app.UiAutomation} 333 * @param permission permission to adopt 334 */ adoptPermissionAsShellUid(String... permission)335 private static void adoptPermissionAsShellUid(String... permission) { 336 InstrumentationRegistry.getInstrumentation().getUiAutomation() 337 .adoptShellPermissionIdentity(permission); 338 } 339 340 /** 341 * Gets all the permissions adopted as the shell UID 342 * 343 * @return a {@link java.util.Set} of the adopted shell permissions 344 */ getPermissionsAdoptedAsShellUid()345 private static Set<String> getPermissionsAdoptedAsShellUid() { 346 return InstrumentationRegistry.getInstrumentation().getUiAutomation() 347 .getAdoptedShellPermissions(); 348 } 349 }