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.assertWithMessage; 19 20 import static org.mockito.ArgumentMatchers.eq; 21 import static org.mockito.Mockito.*; 22 23 import android.bluetooth.BluetoothAdapter; 24 import android.bluetooth.BluetoothDevice; 25 import android.content.Context; 26 import android.content.Intent; 27 import android.content.pm.PackageManager; 28 import android.content.res.Resources; 29 import android.os.Handler; 30 import android.os.Looper; 31 import android.os.Message; 32 import android.os.MessageQueue; 33 import android.os.test.TestLooper; 34 import android.service.media.MediaBrowserService; 35 import android.util.Log; 36 37 import androidx.test.platform.app.InstrumentationRegistry; 38 import androidx.test.uiautomator.UiDevice; 39 40 import com.android.bluetooth.avrcpcontroller.BluetoothMediaBrowserService; 41 import com.android.bluetooth.btservice.AdapterService; 42 43 import org.junit.Assert; 44 import org.junit.rules.TestRule; 45 import org.junit.runner.Description; 46 import org.junit.runners.model.Statement; 47 48 import java.io.BufferedReader; 49 import java.io.FileReader; 50 import java.io.IOException; 51 import java.lang.reflect.Field; 52 import java.time.Duration; 53 import java.time.Instant; 54 import java.util.HashMap; 55 import java.util.Map; 56 import java.util.concurrent.BlockingQueue; 57 import java.util.concurrent.TimeUnit; 58 import java.util.stream.IntStream; 59 60 /** A set of methods useful in Bluetooth instrumentation tests */ 61 public class TestUtils { 62 private static String sSystemScreenOffTimeout = "10000"; 63 64 private static final String TAG = "BluetoothTestUtils"; 65 66 /** 67 * Utility method to replace obj.fieldName with newValue where obj is of type c 68 * 69 * @param c type of obj 70 * @param fieldName field name to be replaced 71 * @param obj instance of type c whose fieldName is to be replaced, null for static fields 72 * @param newValue object used to replace fieldName 73 * @return the old value of fieldName that got replaced, caller is responsible for restoring it 74 * back to obj 75 * @throws NoSuchFieldException when fieldName is not found in type c 76 * @throws IllegalAccessException when fieldName cannot be accessed in type c 77 */ replaceField( final Class c, final String fieldName, final Object obj, final Object newValue)78 public static Object replaceField( 79 final Class c, final String fieldName, final Object obj, final Object newValue) 80 throws NoSuchFieldException, IllegalAccessException { 81 Field field = c.getDeclaredField(fieldName); 82 field.setAccessible(true); 83 84 Object oldValue = field.get(obj); 85 field.set(obj, newValue); 86 return oldValue; 87 } 88 89 /** 90 * Set the return value of {@link AdapterService#getAdapterService()} to a test specified value 91 * 92 * @param adapterService the designated {@link AdapterService} in test, must not be null, can be 93 * mocked or spied 94 */ setAdapterService(AdapterService adapterService)95 public static void setAdapterService(AdapterService adapterService) { 96 Assert.assertNull( 97 "AdapterService.getAdapterService() must be null before setting another" 98 + " AdapterService", 99 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 set AdapterService.sAdapterService field. 103 AdapterService.setAdapterService(adapterService); 104 } 105 106 /** 107 * Clear the return value of {@link AdapterService#getAdapterService()} to null 108 * 109 * @param adapterService the {@link AdapterService} used when calling {@link 110 * TestUtils#setAdapterService(AdapterService)} 111 */ clearAdapterService(AdapterService adapterService)112 public static void clearAdapterService(AdapterService adapterService) { 113 Assert.assertSame( 114 "AdapterService.getAdapterService() must return the same object as the" 115 + " supplied adapterService in this method", 116 adapterService, 117 AdapterService.getAdapterService()); 118 Assert.assertNotNull("Adapter service should not be null", adapterService); 119 AdapterService.clearAdapterService(adapterService); 120 } 121 122 /** Helper function to mock getSystemService calls */ mockGetSystemService( Context ctx, String serviceName, Class<T> serviceClass, T mockService)123 public static <T> void mockGetSystemService( 124 Context ctx, String serviceName, Class<T> serviceClass, T mockService) { 125 when(ctx.getSystemService(eq(serviceName))).thenReturn(mockService); 126 when(ctx.getSystemServiceName(eq(serviceClass))).thenReturn(serviceName); 127 } 128 129 /** Helper function to mock getSystemService calls */ mockGetSystemService( Context ctx, String serviceName, Class<T> serviceClass)130 public static <T> T mockGetSystemService( 131 Context ctx, String serviceName, Class<T> serviceClass) { 132 T mockedService = mock(serviceClass); 133 mockGetSystemService(ctx, serviceName, serviceClass, mockedService); 134 return mockedService; 135 } 136 137 /** 138 * Create a test device. 139 * 140 * @param bluetoothAdapter the Bluetooth adapter to use 141 * @param id the test device ID. It must be an integer in the interval [0, 0xFF]. 142 * @return {@link BluetoothDevice} test device for the device ID 143 */ getTestDevice(BluetoothAdapter bluetoothAdapter, int id)144 public static BluetoothDevice getTestDevice(BluetoothAdapter bluetoothAdapter, int id) { 145 Assert.assertTrue(id <= 0xFF); 146 Assert.assertNotNull(bluetoothAdapter); 147 BluetoothDevice testDevice = 148 bluetoothAdapter.getRemoteDevice(String.format("00:01:02:03:04:%02X", id)); 149 Assert.assertNotNull(testDevice); 150 return testDevice; 151 } 152 getTestApplicationResources(Context context)153 public static Resources getTestApplicationResources(Context context) { 154 try { 155 return context.getPackageManager() 156 .getResourcesForApplication("com.android.bluetooth.tests"); 157 } catch (PackageManager.NameNotFoundException e) { 158 assertWithMessage( 159 "Setup Failure: Unable to get test application resources" 160 + e.toString()) 161 .fail(); 162 return null; 163 } 164 } 165 166 /** 167 * Wait and verify that an intent has been received. 168 * 169 * @param timeoutMs the time (in milliseconds) to wait for the intent 170 * @param queue the queue for the intent 171 * @return the received intent 172 */ waitForIntent(int timeoutMs, BlockingQueue<Intent> queue)173 public static Intent waitForIntent(int timeoutMs, BlockingQueue<Intent> queue) { 174 try { 175 Intent intent = queue.poll(timeoutMs, TimeUnit.MILLISECONDS); 176 Assert.assertNotNull(intent); 177 return intent; 178 } catch (InterruptedException e) { 179 Assert.fail("Cannot obtain an Intent from the queue: " + e.getMessage()); 180 } 181 return null; 182 } 183 184 /** 185 * Wait and verify that no intent has been received. 186 * 187 * @param timeoutMs the time (in milliseconds) to wait and verify no intent has been received 188 * @param queue the queue for the intent 189 * @return the received intent. Should be null under normal circumstances 190 */ waitForNoIntent(int timeoutMs, BlockingQueue<Intent> queue)191 public static Intent waitForNoIntent(int timeoutMs, BlockingQueue<Intent> queue) { 192 try { 193 Intent intent = queue.poll(timeoutMs, TimeUnit.MILLISECONDS); 194 Assert.assertNull(intent); 195 return intent; 196 } catch (InterruptedException e) { 197 Assert.fail("Cannot obtain an Intent from the queue: " + e.getMessage()); 198 } 199 return null; 200 } 201 202 /** 203 * Wait for looper to finish its current task and all tasks schedule before this 204 * 205 * @param looper looper of interest 206 */ waitForLooperToFinishScheduledTask(Looper looper)207 public static void waitForLooperToFinishScheduledTask(Looper looper) { 208 runOnLooperSync( 209 looper, 210 () -> { 211 // do nothing, just need to make sure looper finishes current task 212 }); 213 } 214 215 /** 216 * Dispatch all the message on the Loopper and check that the `what` is expected 217 * 218 * @param looper looper to execute the message from 219 * @param what list of Messages.what that are expected to be run by the handler 220 */ syncHandler(TestLooper looper, int... what)221 public static void syncHandler(TestLooper looper, int... what) { 222 IntStream.of(what) 223 .forEach( 224 w -> { 225 Message msg = looper.nextMessage(); 226 assertWithMessage("Expecting [" + w + "] instead of null Msg") 227 .that(msg) 228 .isNotNull(); 229 assertWithMessage("Not the expected Message:\n" + msg) 230 .that(msg.what) 231 .isEqualTo(w); 232 Log.d(TAG, "Processing message: " + msg); 233 msg.getTarget().dispatchMessage(msg); 234 }); 235 } 236 237 /** 238 * Wait for looper to become idle 239 * 240 * @param looper looper of interest 241 */ waitForLooperToBeIdle(Looper looper)242 public static void waitForLooperToBeIdle(Looper looper) { 243 class Idler implements MessageQueue.IdleHandler { 244 private boolean mIdle = false; 245 246 @Override 247 public boolean queueIdle() { 248 synchronized (this) { 249 mIdle = true; 250 notifyAll(); 251 } 252 return false; 253 } 254 255 public synchronized void waitForIdle() { 256 while (!mIdle) { 257 try { 258 wait(); 259 } catch (InterruptedException e) { 260 Log.w(TAG, "waitForIdle got interrupted", e); 261 } 262 } 263 } 264 } 265 266 Idler idle = new Idler(); 267 looper.getQueue().addIdleHandler(idle); 268 // Ensure we are not Idle to begin with so the idle handler will run 269 waitForLooperToFinishScheduledTask(looper); 270 idle.waitForIdle(); 271 } 272 273 /** 274 * Run synchronously a runnable action on a looper. The method will return after the action has 275 * been execution to completion. 276 * 277 * <p>Example: 278 * 279 * <pre>{@code 280 * TestUtils.runOnMainSync(new Runnable() { 281 * public void run() { 282 * Assert.assertTrue(mA2dpService.stop()); 283 * } 284 * }); 285 * }</pre> 286 * 287 * @param looper the looper used to run the action 288 * @param action the action to run 289 */ runOnLooperSync(Looper looper, Runnable action)290 public static void runOnLooperSync(Looper looper, Runnable action) { 291 if (Looper.myLooper() == looper) { 292 // requested thread is the same as the current thread. call directly. 293 action.run(); 294 } else { 295 Handler handler = new Handler(looper); 296 SyncRunnable sr = new SyncRunnable(action); 297 handler.post(sr); 298 sr.waitForComplete(); 299 } 300 } 301 302 /** 303 * Read Bluetooth adapter configuration from the filesystem 304 * 305 * @return A {@link HashMap} of Bluetooth configs in the format: section -> key1 -> value1 -> 306 * key2 -> value2 Assume no empty section name, no duplicate keys in the same section 307 */ readAdapterConfig()308 public static Map<String, Map<String, String>> readAdapterConfig() { 309 Map<String, Map<String, String>> adapterConfig = new HashMap<>(); 310 try (BufferedReader reader = 311 new BufferedReader(new FileReader("/data/misc/bluedroid/bt_config.conf"))) { 312 String section = ""; 313 for (String line; (line = reader.readLine()) != null; ) { 314 line = line.trim(); 315 if (line.isEmpty() || line.startsWith("#")) { 316 continue; 317 } 318 if (line.startsWith("[")) { 319 if (line.charAt(line.length() - 1) != ']') { 320 Log.e(TAG, "readAdapterConfig: config line is not correct: " + line); 321 return null; 322 } 323 section = line.substring(1, line.length() - 1); 324 adapterConfig.put(section, new HashMap<>()); 325 } else { 326 String[] keyValue = line.split("="); 327 adapterConfig 328 .get(section) 329 .put( 330 keyValue[0].trim(), 331 keyValue.length == 1 ? "" : keyValue[1].trim()); 332 } 333 } 334 } catch (IOException e) { 335 Log.e(TAG, "readAdapterConfig: Exception while reading the config" + e); 336 return null; 337 } 338 return adapterConfig; 339 } 340 341 /** 342 * Prepare the intent to start bluetooth browser media service. 343 * 344 * @return intent with the appropriate component & action set. 345 */ prepareIntentToStartBluetoothBrowserMediaService()346 public static Intent prepareIntentToStartBluetoothBrowserMediaService() { 347 final Intent intent = 348 new Intent( 349 InstrumentationRegistry.getInstrumentation().getTargetContext(), 350 BluetoothMediaBrowserService.class); 351 intent.setAction(MediaBrowserService.SERVICE_INTERFACE); 352 return intent; 353 } 354 setUpUiTest()355 public static void setUpUiTest() throws Exception { 356 final UiDevice device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()); 357 // Disable animation 358 device.executeShellCommand("settings put global window_animation_scale 0.0"); 359 device.executeShellCommand("settings put global transition_animation_scale 0.0"); 360 device.executeShellCommand("settings put global animator_duration_scale 0.0"); 361 362 // change device screen_off_timeout to 5 minutes 363 sSystemScreenOffTimeout = 364 device.executeShellCommand("settings get system screen_off_timeout"); 365 device.executeShellCommand("settings put system screen_off_timeout 300000"); 366 367 // Turn on screen and unlock 368 device.wakeUp(); 369 device.executeShellCommand("wm dismiss-keyguard"); 370 371 // Back to home screen, in case some dialog/activity is in front 372 UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()).pressHome(); 373 } 374 tearDownUiTest()375 public static void tearDownUiTest() throws Exception { 376 final UiDevice device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()); 377 device.executeShellCommand("wm dismiss-keyguard"); 378 379 // Re-enable animation 380 device.executeShellCommand("settings put global window_animation_scale 1.0"); 381 device.executeShellCommand("settings put global transition_animation_scale 1.0"); 382 device.executeShellCommand("settings put global animator_duration_scale 1.0"); 383 384 // restore screen_off_timeout 385 device.executeShellCommand( 386 "settings put system screen_off_timeout " + sSystemScreenOffTimeout); 387 } 388 389 public static class RetryTestRule implements TestRule { 390 private int retryCount = 5; 391 RetryTestRule()392 public RetryTestRule() { 393 this(5); 394 } 395 RetryTestRule(int retryCount)396 public RetryTestRule(int retryCount) { 397 this.retryCount = retryCount; 398 } 399 apply(Statement base, Description description)400 public Statement apply(Statement base, Description description) { 401 return new Statement() { 402 @Override 403 public void evaluate() throws Throwable { 404 Throwable caughtThrowable = null; 405 406 // implement retry logic here 407 for (int i = 0; i < retryCount; i++) { 408 try { 409 base.evaluate(); 410 return; 411 } catch (Throwable t) { 412 caughtThrowable = t; 413 Log.e( 414 TAG, 415 description.getDisplayName() + ": run " + (i + 1) + " failed", 416 t); 417 } 418 } 419 Log.e( 420 TAG, 421 description.getDisplayName() 422 + ": giving up after " 423 + retryCount 424 + " failures"); 425 throw caughtThrowable; 426 } 427 }; 428 } 429 } 430 431 /** Helper class used to run synchronously a runnable action on a looper. */ 432 private static final class SyncRunnable implements Runnable { 433 private final Runnable mTarget; 434 private volatile boolean mComplete = false; 435 436 SyncRunnable(Runnable target) { 437 mTarget = target; 438 } 439 440 @Override 441 public void run() { 442 mTarget.run(); 443 synchronized (this) { 444 mComplete = true; 445 notifyAll(); 446 } 447 } 448 449 public void waitForComplete() { 450 synchronized (this) { 451 while (!mComplete) { 452 try { 453 wait(); 454 } catch (InterruptedException e) { 455 Log.w(TAG, "waitForComplete got interrupted", e); 456 } 457 } 458 } 459 } 460 } 461 462 public static final class FakeTimeProvider implements Utils.TimeProvider { 463 private Instant currentTime = Instant.EPOCH; 464 465 @Override 466 public long elapsedRealtime() { 467 return currentTime.toEpochMilli(); 468 } 469 470 public void advanceTime(Duration amountToAdvance) { 471 currentTime = currentTime.plus(amountToAdvance); 472 } 473 } 474 } 475