1 /* 2 * Copyright (C) 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.doReturn; 23 import static org.mockito.Mockito.mock; 24 25 import android.annotation.IntRange; 26 import android.bluetooth.BluetoothDevice; 27 import android.bluetooth.BluetoothManager; 28 import android.content.Context; 29 import android.content.Intent; 30 import android.content.pm.PackageManager; 31 import android.content.res.Resources; 32 import android.os.Handler; 33 import android.os.Looper; 34 import android.os.Message; 35 import android.os.MessageQueue; 36 import android.service.media.MediaBrowserService; 37 import android.util.Log; 38 39 import androidx.test.platform.app.InstrumentationRegistry; 40 import androidx.test.uiautomator.UiDevice; 41 42 import com.android.bluetooth.avrcpcontroller.BluetoothMediaBrowserService; 43 import com.android.bluetooth.btservice.AdapterService; 44 45 import org.junit.rules.MethodRule; 46 import org.junit.rules.TestRule; 47 import org.junit.runner.Description; 48 import org.junit.runners.model.FrameworkMethod; 49 import org.junit.runners.model.Statement; 50 import org.mockito.Mockito; 51 import org.mockito.junit.MockitoJUnit; 52 53 import java.time.Duration; 54 import java.time.Instant; 55 import java.util.stream.IntStream; 56 57 /** A set of methods useful in Bluetooth instrumentation tests */ 58 public class TestUtils { 59 private static final String TAG = Utils.TAG_PREFIX_BLUETOOTH + TestUtils.class.getSimpleName(); 60 61 private static String sSystemScreenOffTimeout = "10000"; 62 63 /** 64 * Set the return value of {@link AdapterService#getAdapterService()} to a test specified value 65 * 66 * @param adapterService the designated {@link AdapterService} in test, must not be null, can be 67 * mocked or spied 68 */ setAdapterService(AdapterService adapterService)69 public static void setAdapterService(AdapterService adapterService) { 70 assertWithMessage( 71 "AdapterService.getAdapterService() must be null before setting another" 72 + " AdapterService") 73 .that(AdapterService.getAdapterService()) 74 .isNull(); 75 assertThat(adapterService).isNotNull(); 76 // We cannot mock AdapterService.getAdapterService() with Mockito. 77 // Hence we need to set AdapterService.sAdapterService field. 78 AdapterService.setAdapterService(adapterService); 79 } 80 81 /** 82 * Clear the return value of {@link AdapterService#getAdapterService()} to null 83 * 84 * @param adapterService the {@link AdapterService} used when calling {@link 85 * TestUtils#setAdapterService(AdapterService)} 86 */ clearAdapterService(AdapterService adapterService)87 public static void clearAdapterService(AdapterService adapterService) { 88 assertWithMessage( 89 "AdapterService.getAdapterService() must return the same object as the" 90 + " supplied adapterService in this method") 91 .that(adapterService) 92 .isSameInstanceAs(AdapterService.getAdapterService()); 93 assertThat(adapterService).isNotNull(); 94 AdapterService.clearAdapterService(adapterService); 95 } 96 97 /** Helper function to mock getSystemService calls */ mockGetSystemService( Context ctx, String serviceName, Class<T> serviceClass, T mockService)98 public static <T> void mockGetSystemService( 99 Context ctx, String serviceName, Class<T> serviceClass, T mockService) { 100 doReturn(mockService).when(ctx).getSystemService(eq(serviceClass)); 101 doReturn(mockService).when(ctx).getSystemService(eq(serviceName)); 102 doReturn(serviceName).when(ctx).getSystemServiceName(eq(serviceClass)); 103 } 104 105 /** Helper function to mock getSystemService calls */ mockGetSystemService( Context ctx, String serviceName, Class<T> serviceClass)106 public static <T> T mockGetSystemService( 107 Context ctx, String serviceName, Class<T> serviceClass) { 108 T mockedService = mock(serviceClass); 109 mockGetSystemService(ctx, serviceName, serviceClass, mockedService); 110 return mockedService; 111 } 112 113 /** 114 * Create a test device. 115 * 116 * @param id the test device ID. It must be an integer in the interval [0, 0xFF]. 117 * @return {@link BluetoothDevice} test device for the device ID 118 */ getTestDevice(@ntRangefrom = 0x00, to = 0xFF) int id)119 public static BluetoothDevice getTestDevice(@IntRange(from = 0x00, to = 0xFF) int id) { 120 assertThat(id).isAtMost(0xFF); 121 BluetoothDevice testDevice = 122 InstrumentationRegistry.getInstrumentation() 123 .getTargetContext() 124 .getSystemService(BluetoothManager.class) 125 .getAdapter() 126 .getRemoteDevice(String.format("00:01:02:03:04:%02X", id)); 127 assertThat(testDevice).isNotNull(); 128 return testDevice; 129 } 130 getTestApplicationResources(Context context)131 public static Resources getTestApplicationResources(Context context) { 132 try { 133 return context.getPackageManager() 134 .getResourcesForApplication("com.android.bluetooth.tests"); 135 } catch (PackageManager.NameNotFoundException e) { 136 assertWithMessage("Unable to get test application resources: " + e.toString()).fail(); 137 return null; 138 } 139 } 140 141 142 /** 143 * Wait for looper to finish its current task and all tasks schedule before this 144 * 145 * @param looper looper of interest 146 */ waitForLooperToFinishScheduledTask(Looper looper)147 public static void waitForLooperToFinishScheduledTask(Looper looper) { 148 runOnLooperSync( 149 looper, 150 () -> { 151 // do nothing, just need to make sure looper finishes current task 152 }); 153 } 154 155 /** 156 * Dispatch all the message on the Looper and check that the `what` is expected 157 * 158 * @param looper looper to execute the message from 159 * @param what list of Messages.what that are expected to be run by the handler 160 */ syncHandler(TestLooper looper, int... what)161 public static void syncHandler(TestLooper looper, int... what) { 162 IntStream.of(what) 163 .forEach( 164 w -> { 165 Message msg = looper.nextMessage(); 166 assertWithMessage("Expecting [" + w + "] instead of null Msg") 167 .that(msg) 168 .isNotNull(); 169 assertWithMessage("Not the expected Message:\n" + msg) 170 .that(msg.what) 171 .isEqualTo(w); 172 Log.d(TAG, "Processing message: " + msg); 173 msg.getTarget().dispatchMessage(msg); 174 }); 175 } 176 177 /** 178 * Wait for looper to become idle 179 * 180 * @param looper looper of interest 181 */ waitForLooperToBeIdle(Looper looper)182 public static void waitForLooperToBeIdle(Looper looper) { 183 class Idler implements MessageQueue.IdleHandler { 184 private boolean mIdle = false; 185 186 @Override 187 public boolean queueIdle() { 188 synchronized (this) { 189 mIdle = true; 190 notifyAll(); 191 } 192 return false; 193 } 194 195 public synchronized void waitForIdle() { 196 while (!mIdle) { 197 try { 198 wait(); 199 } catch (InterruptedException e) { 200 Log.w(TAG, "waitForIdle got interrupted", e); 201 } 202 } 203 } 204 } 205 206 Idler idle = new Idler(); 207 looper.getQueue().addIdleHandler(idle); 208 // Ensure we are not Idle to begin with so the idle handler will run 209 waitForLooperToFinishScheduledTask(looper); 210 idle.waitForIdle(); 211 } 212 213 /** 214 * Run synchronously a runnable action on a looper. The method will return after the action has 215 * been execution to completion. 216 * 217 * <p>Example: 218 * 219 * <pre>{@code 220 * TestUtils.runOnMainSync(new Runnable() { 221 * public void run() { 222 * assertThat(mA2dpService.stop()).isTrue(); 223 * } 224 * }); 225 * }</pre> 226 * 227 * @param looper the looper used to run the action 228 * @param action the action to run 229 */ runOnLooperSync(Looper looper, Runnable action)230 private static void runOnLooperSync(Looper looper, Runnable action) { 231 if (Looper.myLooper() == looper) { 232 // requested thread is the same as the current thread. call directly. 233 action.run(); 234 } else { 235 Handler handler = new Handler(looper); 236 SyncRunnable sr = new SyncRunnable(action); 237 handler.post(sr); 238 sr.waitForComplete(); 239 } 240 } 241 242 /** 243 * Prepare the intent to start bluetooth browser media service. 244 * 245 * @return intent with the appropriate component & action set. 246 */ prepareIntentToStartBluetoothBrowserMediaService()247 public static Intent prepareIntentToStartBluetoothBrowserMediaService() { 248 final Intent intent = 249 new Intent( 250 InstrumentationRegistry.getInstrumentation().getTargetContext(), 251 BluetoothMediaBrowserService.class); 252 intent.setAction(MediaBrowserService.SERVICE_INTERFACE); 253 return intent; 254 } 255 setUpUiTest()256 public static void setUpUiTest() throws Exception { 257 final UiDevice device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()); 258 // Disable animation 259 device.executeShellCommand("settings put global window_animation_scale 0.0"); 260 device.executeShellCommand("settings put global transition_animation_scale 0.0"); 261 device.executeShellCommand("settings put global animator_duration_scale 0.0"); 262 263 // change device screen_off_timeout to 5 minutes 264 sSystemScreenOffTimeout = 265 device.executeShellCommand("settings get system screen_off_timeout"); 266 device.executeShellCommand("settings put system screen_off_timeout 300000"); 267 268 // Turn on screen and unlock 269 device.wakeUp(); 270 device.executeShellCommand("wm dismiss-keyguard"); 271 272 // Back to home screen, in case some dialog/activity is in front 273 UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()).pressHome(); 274 } 275 tearDownUiTest()276 public static void tearDownUiTest() throws Exception { 277 final UiDevice device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()); 278 device.executeShellCommand("wm dismiss-keyguard"); 279 280 // Re-enable animation 281 device.executeShellCommand("settings put global window_animation_scale 1.0"); 282 device.executeShellCommand("settings put global transition_animation_scale 1.0"); 283 device.executeShellCommand("settings put global animator_duration_scale 1.0"); 284 285 // restore screen_off_timeout 286 device.executeShellCommand( 287 "settings put system screen_off_timeout " + sSystemScreenOffTimeout); 288 } 289 290 public static class RetryTestRule implements TestRule { 291 private int retryCount = 5; 292 RetryTestRule()293 public RetryTestRule() { 294 this(5); 295 } 296 RetryTestRule(int retryCount)297 public RetryTestRule(int retryCount) { 298 this.retryCount = retryCount; 299 } 300 apply(Statement base, Description description)301 public Statement apply(Statement base, Description description) { 302 return new Statement() { 303 @Override 304 public void evaluate() throws Throwable { 305 Throwable caughtThrowable = null; 306 307 // implement retry logic here 308 for (int i = 0; i < retryCount; i++) { 309 try { 310 base.evaluate(); 311 return; 312 } catch (Throwable t) { 313 caughtThrowable = t; 314 Log.e( 315 TAG, 316 description.getDisplayName() + ": run " + (i + 1) + " failed", 317 t); 318 } 319 } 320 Log.e( 321 TAG, 322 description.getDisplayName() 323 + ": giving up after " 324 + retryCount 325 + " failures"); 326 throw caughtThrowable; 327 } 328 }; 329 } 330 } 331 332 /** Wrapper around MockitoJUnit.rule() to clear the inline mock at the end of the test. */ 333 public static class MockitoRule implements MethodRule { 334 private final org.mockito.junit.MockitoRule mMockitoRule = MockitoJUnit.rule(); 335 336 public Statement apply(Statement base, FrameworkMethod method, Object target) { 337 Statement nestedStatement = mMockitoRule.apply(base, method, target); 338 339 return new Statement() { 340 @Override 341 public void evaluate() throws Throwable { 342 nestedStatement.evaluate(); 343 344 // Prevent OutOfMemory errors due to mock maker leaks. 345 // See https://github.com/mockito/mockito/issues/1614, b/259280359, b/396177821 346 Mockito.framework().clearInlineMocks(); 347 } 348 }; 349 } 350 } 351 352 /** Helper class used to run synchronously a runnable action on a looper. */ 353 private static final class SyncRunnable implements Runnable { 354 private final Runnable mTarget; 355 private volatile boolean mComplete = false; 356 357 SyncRunnable(Runnable target) { 358 mTarget = target; 359 } 360 361 @Override 362 public void run() { 363 mTarget.run(); 364 synchronized (this) { 365 mComplete = true; 366 notifyAll(); 367 } 368 } 369 370 public void waitForComplete() { 371 synchronized (this) { 372 while (!mComplete) { 373 try { 374 wait(); 375 } catch (InterruptedException e) { 376 Log.w(TAG, "waitForComplete got interrupted", e); 377 } 378 } 379 } 380 } 381 } 382 383 public static final class FakeTimeProvider implements Utils.TimeProvider { 384 private Instant currentTime = Instant.EPOCH; 385 386 @Override 387 public long elapsedRealtime() { 388 return currentTime.toEpochMilli(); 389 } 390 391 public void advanceTime(Duration amountToAdvance) { 392 currentTime = currentTime.plus(amountToAdvance); 393 } 394 } 395 } 396