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