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