1 /* 2 * Copyright 2018 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 com.android.bluetooth; 17 18 import static com.google.common.truth.Truth.assertThat; 19 import static com.google.common.truth.Truth.assertWithMessage; 20 21 import static org.mockito.ArgumentMatchers.eq; 22 import static org.mockito.Mockito.*; 23 24 import android.bluetooth.BluetoothAdapter; 25 import android.bluetooth.BluetoothDevice; 26 import android.content.Context; 27 import android.content.Intent; 28 import android.content.pm.PackageManager; 29 import android.content.res.Resources; 30 import android.os.Handler; 31 import android.os.Looper; 32 import android.os.MessageQueue; 33 import android.service.media.MediaBrowserService; 34 35 import androidx.test.InstrumentationRegistry; 36 import androidx.test.rule.ServiceTestRule; 37 import androidx.test.uiautomator.UiDevice; 38 39 import com.android.bluetooth.avrcpcontroller.BluetoothMediaBrowserService; 40 import com.android.bluetooth.btservice.AdapterService; 41 import com.android.bluetooth.btservice.ProfileService; 42 43 import org.junit.Assert; 44 import org.mockito.ArgumentCaptor; 45 import org.mockito.internal.util.MockUtil; 46 47 import java.io.BufferedReader; 48 import java.io.FileReader; 49 import java.io.IOException; 50 import java.lang.reflect.Field; 51 import java.lang.reflect.InvocationTargetException; 52 import java.lang.reflect.Method; 53 import java.util.HashMap; 54 import java.util.concurrent.BlockingQueue; 55 import java.util.concurrent.TimeUnit; 56 import java.util.concurrent.TimeoutException; 57 58 /** 59 * A set of methods useful in Bluetooth instrumentation tests 60 */ 61 public class TestUtils { 62 private static final int SERVICE_TOGGLE_TIMEOUT_MS = 1000; // 1s 63 64 /** 65 * Utility method to replace obj.fieldName with newValue where obj is of type c 66 * 67 * @param c type of obj 68 * @param fieldName field name to be replaced 69 * @param obj instance of type c whose fieldName is to be replaced, null for static fields 70 * @param newValue object used to replace fieldName 71 * @return the old value of fieldName that got replaced, caller is responsible for restoring 72 * it back to obj 73 * @throws NoSuchFieldException when fieldName is not found in type c 74 * @throws IllegalAccessException when fieldName cannot be accessed in type c 75 */ replaceField(final Class c, final String fieldName, final Object obj, final Object newValue)76 public static Object replaceField(final Class c, final String fieldName, final Object obj, 77 final Object newValue) throws NoSuchFieldException, IllegalAccessException { 78 Field field = c.getDeclaredField(fieldName); 79 field.setAccessible(true); 80 81 Object oldValue = field.get(obj); 82 field.set(obj, newValue); 83 return oldValue; 84 } 85 86 /** 87 * Set the return value of {@link AdapterService#getAdapterService()} to a test specified value 88 * 89 * @param adapterService the designated {@link AdapterService} in test, must not be null, can 90 * be mocked or spied 91 * @throws NoSuchMethodException when setAdapterService method is not found 92 * @throws IllegalAccessException when setAdapterService method cannot be accessed 93 * @throws InvocationTargetException when setAdapterService method cannot be invoked, which 94 * should never happen since setAdapterService is a static method 95 */ setAdapterService(AdapterService adapterService)96 public static void setAdapterService(AdapterService adapterService) 97 throws NoSuchMethodException, IllegalAccessException, InvocationTargetException { 98 Assert.assertNull("AdapterService.getAdapterService() must be null before setting another" 99 + " AdapterService", AdapterService.getAdapterService()); 100 Assert.assertNotNull("Adapter service should not be null", adapterService); 101 // We cannot mock AdapterService.getAdapterService() with Mockito. 102 // Hence we need to use reflection to call a private method to 103 // initialize properly the AdapterService.sAdapterService field. 104 Method method = 105 AdapterService.class.getDeclaredMethod("setAdapterService", AdapterService.class); 106 method.setAccessible(true); 107 method.invoke(null, adapterService); 108 } 109 110 /** 111 * Clear the return value of {@link AdapterService#getAdapterService()} to null 112 * 113 * @param adapterService the {@link AdapterService} used when calling 114 * {@link TestUtils#setAdapterService(AdapterService)} 115 * @throws NoSuchMethodException when clearAdapterService method is not found 116 * @throws IllegalAccessException when clearAdapterService method cannot be accessed 117 * @throws InvocationTargetException when clearAdappterService method cannot be invoked, 118 * which should never happen since clearAdapterService is a static method 119 */ clearAdapterService(AdapterService adapterService)120 public static void clearAdapterService(AdapterService adapterService) 121 throws NoSuchMethodException, IllegalAccessException, InvocationTargetException { 122 Assert.assertSame("AdapterService.getAdapterService() must return the same object as the" 123 + " supplied adapterService in this method", adapterService, 124 AdapterService.getAdapterService()); 125 Assert.assertNotNull("Adapter service should not be null", adapterService); 126 Method method = 127 AdapterService.class.getDeclaredMethod("clearAdapterService", AdapterService.class); 128 method.setAccessible(true); 129 method.invoke(null, adapterService); 130 } 131 132 /** 133 * Start a profile service using the given {@link ServiceTestRule} and verify through 134 * {@link AdapterService#getAdapterService()} that the service is actually started within 135 * {@link TestUtils#SERVICE_TOGGLE_TIMEOUT_MS} milliseconds. 136 * {@link #setAdapterService(AdapterService)} must be called with a mocked 137 * {@link AdapterService} before calling this method 138 * 139 * @param serviceTestRule the {@link ServiceTestRule} used to execute the service start request 140 * @param profileServiceClass a class from one of {@link ProfileService}'s child classes 141 * @throws TimeoutException when service failed to start within either default timeout of 142 * {@link ServiceTestRule#DEFAULT_TIMEOUT} (normally 5s) or user specified time when creating 143 * {@link ServiceTestRule} through {@link ServiceTestRule#withTimeout(long, TimeUnit)} method 144 */ startService(ServiceTestRule serviceTestRule, Class<T> profileServiceClass)145 public static <T extends ProfileService> void startService(ServiceTestRule serviceTestRule, 146 Class<T> profileServiceClass) throws TimeoutException { 147 AdapterService adapterService = AdapterService.getAdapterService(); 148 Assert.assertNotNull("Adapter service should not be null", adapterService); 149 Assert.assertTrue("AdapterService.getAdapterService() must return a mocked or spied object" 150 + " before calling this method", MockUtil.isMock(adapterService)); 151 Intent startIntent = 152 new Intent(InstrumentationRegistry.getTargetContext(), profileServiceClass); 153 startIntent.putExtra(AdapterService.EXTRA_ACTION, 154 AdapterService.ACTION_SERVICE_STATE_CHANGED); 155 startIntent.putExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.STATE_ON); 156 serviceTestRule.startService(startIntent); 157 ArgumentCaptor<ProfileService> profile = ArgumentCaptor.forClass(profileServiceClass); 158 verify(adapterService, timeout(SERVICE_TOGGLE_TIMEOUT_MS)).onProfileServiceStateChanged( 159 profile.capture(), eq(BluetoothAdapter.STATE_ON)); 160 Assert.assertEquals(profileServiceClass.getName(), profile.getValue().getClass().getName()); 161 } 162 163 /** 164 * Stop a profile service using the given {@link ServiceTestRule} and verify through 165 * {@link AdapterService#getAdapterService()} that the service is actually stopped within 166 * {@link TestUtils#SERVICE_TOGGLE_TIMEOUT_MS} milliseconds. 167 * {@link #setAdapterService(AdapterService)} must be called with a mocked 168 * {@link AdapterService} before calling this method 169 * 170 * @param serviceTestRule the {@link ServiceTestRule} used to execute the service start request 171 * @param profileServiceClass a class from one of {@link ProfileService}'s child classes 172 * @throws TimeoutException when service failed to start within either default timeout of 173 * {@link ServiceTestRule#DEFAULT_TIMEOUT} (normally 5s) or user specified time when creating 174 * {@link ServiceTestRule} through {@link ServiceTestRule#withTimeout(long, TimeUnit)} method 175 */ stopService(ServiceTestRule serviceTestRule, Class<T> profileServiceClass)176 public static <T extends ProfileService> void stopService(ServiceTestRule serviceTestRule, 177 Class<T> profileServiceClass) throws TimeoutException { 178 AdapterService adapterService = AdapterService.getAdapterService(); 179 Assert.assertNotNull(adapterService); 180 Assert.assertTrue("AdapterService.getAdapterService() must return a mocked or spied object" 181 + " before calling this method", MockUtil.isMock(adapterService)); 182 Intent stopIntent = 183 new Intent(InstrumentationRegistry.getTargetContext(), profileServiceClass); 184 stopIntent.putExtra(AdapterService.EXTRA_ACTION, 185 AdapterService.ACTION_SERVICE_STATE_CHANGED); 186 stopIntent.putExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.STATE_OFF); 187 serviceTestRule.startService(stopIntent); 188 ArgumentCaptor<ProfileService> profile = ArgumentCaptor.forClass(profileServiceClass); 189 verify(adapterService, timeout(SERVICE_TOGGLE_TIMEOUT_MS)).onProfileServiceStateChanged( 190 profile.capture(), eq(BluetoothAdapter.STATE_OFF)); 191 Assert.assertEquals(profileServiceClass.getName(), profile.getValue().getClass().getName()); 192 ArgumentCaptor<ProfileService> profile2 = ArgumentCaptor.forClass(profileServiceClass); 193 verify(adapterService, timeout(SERVICE_TOGGLE_TIMEOUT_MS)).removeProfile(profile2.capture()); 194 Assert.assertEquals(profileServiceClass.getName(), profile2.getValue().getClass().getName()); 195 } 196 197 /** 198 * Create a test device. 199 * 200 * @param bluetoothAdapter the Bluetooth adapter to use 201 * @param id the test device ID. It must be an integer in the interval [0, 0xFF]. 202 * @return {@link BluetoothDevice} test device for the device ID 203 */ getTestDevice(BluetoothAdapter bluetoothAdapter, int id)204 public static BluetoothDevice getTestDevice(BluetoothAdapter bluetoothAdapter, int id) { 205 Assert.assertTrue(id <= 0xFF); 206 Assert.assertNotNull(bluetoothAdapter); 207 BluetoothDevice testDevice = 208 bluetoothAdapter.getRemoteDevice(String.format("00:01:02:03:04:%02X", id)); 209 Assert.assertNotNull(testDevice); 210 return testDevice; 211 } 212 getTestApplicationResources(Context context)213 public static Resources getTestApplicationResources(Context context) { 214 try { 215 return context.getPackageManager().getResourcesForApplication("com.android.bluetooth.tests"); 216 } catch (PackageManager.NameNotFoundException e) { 217 assertWithMessage("Setup Failure: Unable to get test application resources" 218 + e.toString()).fail(); 219 return null; 220 } 221 } 222 223 /** 224 * Wait and verify that an intent has been received. 225 * 226 * @param timeoutMs the time (in milliseconds) to wait for the intent 227 * @param queue the queue for the intent 228 * @return the received intent 229 */ waitForIntent(int timeoutMs, BlockingQueue<Intent> queue)230 public static Intent waitForIntent(int timeoutMs, BlockingQueue<Intent> queue) { 231 try { 232 Intent intent = queue.poll(timeoutMs, TimeUnit.MILLISECONDS); 233 Assert.assertNotNull(intent); 234 return intent; 235 } catch (InterruptedException e) { 236 Assert.fail("Cannot obtain an Intent from the queue: " + e.getMessage()); 237 } 238 return null; 239 } 240 241 /** 242 * Wait and verify that no intent has been received. 243 * 244 * @param timeoutMs the time (in milliseconds) to wait and verify no intent 245 * has been received 246 * @param queue the queue for the intent 247 * @return the received intent. Should be null under normal circumstances 248 */ waitForNoIntent(int timeoutMs, BlockingQueue<Intent> queue)249 public static Intent waitForNoIntent(int timeoutMs, BlockingQueue<Intent> queue) { 250 try { 251 Intent intent = queue.poll(timeoutMs, TimeUnit.MILLISECONDS); 252 Assert.assertNull(intent); 253 return intent; 254 } catch (InterruptedException e) { 255 Assert.fail("Cannot obtain an Intent from the queue: " + e.getMessage()); 256 } 257 return null; 258 } 259 260 /** 261 * Wait for looper to finish its current task and all tasks schedule before this 262 * 263 * @param looper looper of interest 264 */ waitForLooperToFinishScheduledTask(Looper looper)265 public static void waitForLooperToFinishScheduledTask(Looper looper) { 266 runOnLooperSync(looper, () -> { 267 // do nothing, just need to make sure looper finishes current task 268 }); 269 } 270 271 /** 272 * Wait for looper to become idle 273 * 274 * @param looper looper of interest 275 */ waitForLooperToBeIdle(Looper looper)276 public static void waitForLooperToBeIdle(Looper looper) { 277 class Idler implements MessageQueue.IdleHandler { 278 private boolean mIdle = false; 279 280 @Override 281 public boolean queueIdle() { 282 synchronized (this) { 283 mIdle = true; 284 notifyAll(); 285 } 286 return false; 287 } 288 289 public synchronized void waitForIdle() { 290 while (!mIdle) { 291 try { 292 wait(); 293 } catch (InterruptedException e) { 294 } 295 } 296 } 297 } 298 299 Idler idle = new Idler(); 300 looper.getQueue().addIdleHandler(idle); 301 // Ensure we are not Idle to begin with so the idle handler will run 302 waitForLooperToFinishScheduledTask(looper); 303 idle.waitForIdle(); 304 } 305 306 /** 307 * Run synchronously a runnable action on a looper. 308 * The method will return after the action has been execution to completion. 309 * 310 * Example: 311 * <pre> 312 * {@code 313 * TestUtils.runOnMainSync(new Runnable() { 314 * public void run() { 315 * Assert.assertTrue(mA2dpService.stop()); 316 * } 317 * }); 318 * } 319 * </pre> 320 * 321 * @param looper the looper used to run the action 322 * @param action the action to run 323 */ runOnLooperSync(Looper looper, Runnable action)324 public static void runOnLooperSync(Looper looper, Runnable action) { 325 if (Looper.myLooper() == looper) { 326 // requested thread is the same as the current thread. call directly. 327 action.run(); 328 } else { 329 Handler handler = new Handler(looper); 330 SyncRunnable sr = new SyncRunnable(action); 331 handler.post(sr); 332 sr.waitForComplete(); 333 } 334 } 335 336 /** 337 * Read Bluetooth adapter configuration from the filesystem 338 * 339 * @return A {@link HashMap} of Bluetooth configs in the format: 340 * section -> key1 -> value1 341 * -> key2 -> value2 342 * Assume no empty section name, no duplicate keys in the same section 343 */ readAdapterConfig()344 public static HashMap<String, HashMap<String, String>> readAdapterConfig() { 345 HashMap<String, HashMap<String, String>> adapterConfig = new HashMap<>(); 346 try (BufferedReader reader = 347 new BufferedReader(new FileReader("/data/misc/bluedroid/bt_config.conf"))) { 348 String section = ""; 349 for (String line; (line = reader.readLine()) != null;) { 350 line = line.trim(); 351 if (line.isEmpty() || line.startsWith("#")) { 352 continue; 353 } 354 if (line.startsWith("[")) { 355 if (line.charAt(line.length() - 1) != ']') { 356 return null; 357 } 358 section = line.substring(1, line.length() - 1); 359 adapterConfig.put(section, new HashMap<>()); 360 } else { 361 String[] keyValue = line.split("="); 362 adapterConfig.get(section).put(keyValue[0].trim(), 363 keyValue.length == 1 ? "" : keyValue[1].trim()); 364 } 365 } 366 } catch (IOException e) { 367 return null; 368 } 369 return adapterConfig; 370 } 371 372 /** 373 * Prepare the intent to start bluetooth browser media service. 374 * 375 * @return intent with the appropriate component & action set. 376 */ prepareIntentToStartBluetoothBrowserMediaService()377 public static Intent prepareIntentToStartBluetoothBrowserMediaService() { 378 final Intent intent = new Intent(InstrumentationRegistry.getTargetContext(), 379 BluetoothMediaBrowserService.class); 380 intent.setAction(MediaBrowserService.SERVICE_INTERFACE); 381 return intent; 382 } 383 wakeUpAndDismissKeyGuard()384 public static void wakeUpAndDismissKeyGuard() throws Exception { 385 final UiDevice device = UiDevice.getInstance( 386 androidx.test.platform.app.InstrumentationRegistry.getInstrumentation()); 387 device.wakeUp(); 388 device.executeShellCommand("wm dismiss-keyguard"); 389 } 390 391 /** 392 * Helper class used to run synchronously a runnable action on a looper. 393 */ 394 private static final class SyncRunnable implements Runnable { 395 private final Runnable mTarget; 396 private volatile boolean mComplete = false; 397 SyncRunnable(Runnable target)398 SyncRunnable(Runnable target) { 399 mTarget = target; 400 } 401 402 @Override run()403 public void run() { 404 mTarget.run(); 405 synchronized (this) { 406 mComplete = true; 407 notifyAll(); 408 } 409 } 410 waitForComplete()411 public void waitForComplete() { 412 synchronized (this) { 413 while (!mComplete) { 414 try { 415 wait(); 416 } catch (InterruptedException e) { 417 } 418 } 419 } 420 } 421 } 422 } 423