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