• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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