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 org.mockito.ArgumentMatchers.eq;
19 import static org.mockito.Mockito.*;
20 
21 import android.bluetooth.BluetoothAdapter;
22 import android.bluetooth.BluetoothDevice;
23 import android.content.Intent;
24 import android.os.Handler;
25 import android.os.Looper;
26 
27 import androidx.test.InstrumentationRegistry;
28 import androidx.test.rule.ServiceTestRule;
29 
30 import com.android.bluetooth.btservice.AdapterService;
31 import com.android.bluetooth.btservice.ProfileService;
32 
33 import org.junit.Assert;
34 import org.mockito.ArgumentCaptor;
35 import org.mockito.internal.util.MockUtil;
36 
37 import java.io.BufferedReader;
38 import java.io.FileReader;
39 import java.io.IOException;
40 import java.lang.reflect.Field;
41 import java.lang.reflect.InvocationTargetException;
42 import java.lang.reflect.Method;
43 import java.util.HashMap;
44 import java.util.concurrent.BlockingQueue;
45 import java.util.concurrent.TimeUnit;
46 import java.util.concurrent.TimeoutException;
47 
48 /**
49  * A set of methods useful in Bluetooth instrumentation tests
50  */
51 public class TestUtils {
52     private static final int SERVICE_TOGGLE_TIMEOUT_MS = 1000;    // 1s
53 
54     /**
55      * Utility method to replace obj.fieldName with newValue where obj is of type c
56      *
57      * @param c type of obj
58      * @param fieldName field name to be replaced
59      * @param obj instance of type c whose fieldName is to be replaced, null for static fields
60      * @param newValue object used to replace fieldName
61      * @return the old value of fieldName that got replaced, caller is responsible for restoring
62      *         it back to obj
63      * @throws NoSuchFieldException when fieldName is not found in type c
64      * @throws IllegalAccessException when fieldName cannot be accessed in type c
65      */
replaceField(final Class c, final String fieldName, final Object obj, final Object newValue)66     public static Object replaceField(final Class c, final String fieldName, final Object obj,
67             final Object newValue) throws NoSuchFieldException, IllegalAccessException {
68         Field field = c.getDeclaredField(fieldName);
69         field.setAccessible(true);
70 
71         Object oldValue = field.get(obj);
72         field.set(obj, newValue);
73         return oldValue;
74     }
75 
76     /**
77      * Set the return value of {@link AdapterService#getAdapterService()} to a test specified value
78      *
79      * @param adapterService the designated {@link AdapterService} in test, must not be null, can
80      * be mocked or spied
81      * @throws NoSuchMethodException when setAdapterService method is not found
82      * @throws IllegalAccessException when setAdapterService method cannot be accessed
83      * @throws InvocationTargetException when setAdapterService method cannot be invoked, which
84      * should never happen since setAdapterService is a static method
85      */
setAdapterService(AdapterService adapterService)86     public static void setAdapterService(AdapterService adapterService)
87             throws NoSuchMethodException, IllegalAccessException, InvocationTargetException {
88         Assert.assertNull("AdapterService.getAdapterService() must be null before setting another"
89                 + " AdapterService", AdapterService.getAdapterService());
90         Assert.assertNotNull(adapterService);
91         // We cannot mock AdapterService.getAdapterService() with Mockito.
92         // Hence we need to use reflection to call a private method to
93         // initialize properly the AdapterService.sAdapterService field.
94         Method method =
95                 AdapterService.class.getDeclaredMethod("setAdapterService", AdapterService.class);
96         method.setAccessible(true);
97         method.invoke(null, adapterService);
98     }
99 
100     /**
101      * Clear the return value of {@link AdapterService#getAdapterService()} to null
102      *
103      * @param adapterService the {@link AdapterService} used when calling
104      * {@link TestUtils#setAdapterService(AdapterService)}
105      * @throws NoSuchMethodException when clearAdapterService method is not found
106      * @throws IllegalAccessException when clearAdapterService method cannot be accessed
107      * @throws InvocationTargetException when clearAdappterService method cannot be invoked,
108      * which should never happen since clearAdapterService is a static method
109      */
clearAdapterService(AdapterService adapterService)110     public static void clearAdapterService(AdapterService adapterService)
111             throws NoSuchMethodException, IllegalAccessException, InvocationTargetException {
112         Assert.assertSame("AdapterService.getAdapterService() must return the same object as the"
113                         + " supplied adapterService in this method", adapterService,
114                 AdapterService.getAdapterService());
115         Assert.assertNotNull(adapterService);
116         Method method =
117                 AdapterService.class.getDeclaredMethod("clearAdapterService", AdapterService.class);
118         method.setAccessible(true);
119         method.invoke(null, adapterService);
120     }
121 
122     /**
123      * Start a profile service using the given {@link ServiceTestRule} and verify through
124      * {@link AdapterService#getAdapterService()} that the service is actually started within
125      * {@link TestUtils#SERVICE_TOGGLE_TIMEOUT_MS} milliseconds.
126      * {@link #setAdapterService(AdapterService)} must be called with a mocked
127      * {@link AdapterService} before calling this method
128      *
129      * @param serviceTestRule the {@link ServiceTestRule} used to execute the service start request
130      * @param profileServiceClass a class from one of {@link ProfileService}'s child classes
131      * @throws TimeoutException when service failed to start within either default timeout of
132      * {@link ServiceTestRule#DEFAULT_TIMEOUT} (normally 5s) or user specified time when creating
133      * {@link ServiceTestRule} through {@link ServiceTestRule#withTimeout(long, TimeUnit)} method
134      */
startService(ServiceTestRule serviceTestRule, Class<T> profileServiceClass)135     public static <T extends ProfileService> void startService(ServiceTestRule serviceTestRule,
136             Class<T> profileServiceClass) throws TimeoutException {
137         AdapterService adapterService = AdapterService.getAdapterService();
138         Assert.assertNotNull(adapterService);
139         Assert.assertTrue("AdapterService.getAdapterService() must return a mocked or spied object"
140                 + " before calling this method", MockUtil.isMock(adapterService));
141         Intent startIntent =
142                 new Intent(InstrumentationRegistry.getTargetContext(), profileServiceClass);
143         startIntent.putExtra(AdapterService.EXTRA_ACTION,
144                 AdapterService.ACTION_SERVICE_STATE_CHANGED);
145         startIntent.putExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.STATE_ON);
146         serviceTestRule.startService(startIntent);
147         ArgumentCaptor<ProfileService> profile = ArgumentCaptor.forClass(profileServiceClass);
148         verify(adapterService, timeout(SERVICE_TOGGLE_TIMEOUT_MS)).onProfileServiceStateChanged(
149                 profile.capture(), eq(BluetoothAdapter.STATE_ON));
150         Assert.assertEquals(profileServiceClass.getName(), profile.getValue().getClass().getName());
151     }
152 
153     /**
154      * Stop a profile service using the given {@link ServiceTestRule} and verify through
155      * {@link AdapterService#getAdapterService()} that the service is actually stopped within
156      * {@link TestUtils#SERVICE_TOGGLE_TIMEOUT_MS} milliseconds.
157      * {@link #setAdapterService(AdapterService)} must be called with a mocked
158      * {@link AdapterService} before calling this method
159      *
160      * @param serviceTestRule the {@link ServiceTestRule} used to execute the service start request
161      * @param profileServiceClass a class from one of {@link ProfileService}'s child classes
162      * @throws TimeoutException when service failed to start within either default timeout of
163      * {@link ServiceTestRule#DEFAULT_TIMEOUT} (normally 5s) or user specified time when creating
164      * {@link ServiceTestRule} through {@link ServiceTestRule#withTimeout(long, TimeUnit)} method
165      */
stopService(ServiceTestRule serviceTestRule, Class<T> profileServiceClass)166     public static <T extends ProfileService> void stopService(ServiceTestRule serviceTestRule,
167             Class<T> profileServiceClass) throws TimeoutException {
168         AdapterService adapterService = AdapterService.getAdapterService();
169         Assert.assertNotNull(adapterService);
170         Assert.assertTrue("AdapterService.getAdapterService() must return a mocked or spied object"
171                 + " before calling this method", MockUtil.isMock(adapterService));
172         Intent stopIntent =
173                 new Intent(InstrumentationRegistry.getTargetContext(), profileServiceClass);
174         stopIntent.putExtra(AdapterService.EXTRA_ACTION,
175                 AdapterService.ACTION_SERVICE_STATE_CHANGED);
176         stopIntent.putExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.STATE_OFF);
177         serviceTestRule.startService(stopIntent);
178         ArgumentCaptor<ProfileService> profile = ArgumentCaptor.forClass(profileServiceClass);
179         verify(adapterService, timeout(SERVICE_TOGGLE_TIMEOUT_MS)).onProfileServiceStateChanged(
180                 profile.capture(), eq(BluetoothAdapter.STATE_OFF));
181         Assert.assertEquals(profileServiceClass.getName(), profile.getValue().getClass().getName());
182     }
183 
184     /**
185      * Create a test device.
186      *
187      * @param bluetoothAdapter the Bluetooth adapter to use
188      * @param id the test device ID. It must be an integer in the interval [0, 0xFF].
189      * @return {@link BluetoothDevice} test device for the device ID
190      */
getTestDevice(BluetoothAdapter bluetoothAdapter, int id)191     public static BluetoothDevice getTestDevice(BluetoothAdapter bluetoothAdapter, int id) {
192         Assert.assertTrue(id <= 0xFF);
193         Assert.assertNotNull(bluetoothAdapter);
194         BluetoothDevice testDevice =
195                 bluetoothAdapter.getRemoteDevice(String.format("00:01:02:03:04:%02X", id));
196         Assert.assertNotNull(testDevice);
197         return testDevice;
198     }
199 
200     /**
201      * Wait and verify that an intent has been received.
202      *
203      * @param timeoutMs the time (in milliseconds) to wait for the intent
204      * @param queue the queue for the intent
205      * @return the received intent
206      */
waitForIntent(int timeoutMs, BlockingQueue<Intent> queue)207     public static Intent waitForIntent(int timeoutMs, BlockingQueue<Intent> queue) {
208         try {
209             Intent intent = queue.poll(timeoutMs, TimeUnit.MILLISECONDS);
210             Assert.assertNotNull(intent);
211             return intent;
212         } catch (InterruptedException e) {
213             Assert.fail("Cannot obtain an Intent from the queue: " + e.getMessage());
214         }
215         return null;
216     }
217 
218     /**
219      * Wait and verify that no intent has been received.
220      *
221      * @param timeoutMs the time (in milliseconds) to wait and verify no intent
222      * has been received
223      * @param queue the queue for the intent
224      * @return the received intent. Should be null under normal circumstances
225      */
waitForNoIntent(int timeoutMs, BlockingQueue<Intent> queue)226     public static Intent waitForNoIntent(int timeoutMs, BlockingQueue<Intent> queue) {
227         try {
228             Intent intent = queue.poll(timeoutMs, TimeUnit.MILLISECONDS);
229             Assert.assertNull(intent);
230             return intent;
231         } catch (InterruptedException e) {
232             Assert.fail("Cannot obtain an Intent from the queue: " + e.getMessage());
233         }
234         return null;
235     }
236 
237     /**
238      * Wait for looper to finish its current task and all tasks schedule before this
239      *
240      * @param looper looper of interest
241      */
waitForLooperToFinishScheduledTask(Looper looper)242     public static void waitForLooperToFinishScheduledTask(Looper looper) {
243         runOnLooperSync(looper, () -> {
244             // do nothing, just need to make sure looper finishes current task
245         });
246     }
247 
248     /**
249      * Run synchronously a runnable action on a looper.
250      * The method will return after the action has been execution to completion.
251      *
252      * Example:
253      * <pre>
254      * {@code
255      * TestUtils.runOnMainSync(new Runnable() {
256      *       public void run() {
257      *           Assert.assertTrue(mA2dpService.stop());
258      *       }
259      *   });
260      * }
261      * </pre>
262      *
263      * @param looper the looper used to run the action
264      * @param action the action to run
265      */
runOnLooperSync(Looper looper, Runnable action)266     public static void runOnLooperSync(Looper looper, Runnable action) {
267         if (Looper.myLooper() == looper) {
268             // requested thread is the same as the current thread. call directly.
269             action.run();
270         } else {
271             Handler handler = new Handler(looper);
272             SyncRunnable sr = new SyncRunnable(action);
273             handler.post(sr);
274             sr.waitForComplete();
275         }
276     }
277 
278     /**
279      * Read Bluetooth adapter configuration from the filesystem
280      *
281      * @return A {@link HashMap} of Bluetooth configs in the format:
282      *  section -> key1 -> value1
283      *          -> key2 -> value2
284      *  Assume no empty section name, no duplicate keys in the same section
285      */
readAdapterConfig()286     public static HashMap<String, HashMap<String, String>> readAdapterConfig() {
287         HashMap<String, HashMap<String, String>> adapterConfig = new HashMap<>();
288         try (BufferedReader reader =
289                 new BufferedReader(new FileReader("/data/misc/bluedroid/bt_config.conf"))) {
290             String section = "";
291             for (String line; (line = reader.readLine()) != null;) {
292                 line = line.trim();
293                 if (line.isEmpty() || line.startsWith("#")) {
294                     continue;
295                 }
296                 if (line.startsWith("[")) {
297                     if (line.charAt(line.length() - 1) != ']') {
298                         return null;
299                     }
300                     section = line.substring(1, line.length() - 1);
301                     adapterConfig.put(section, new HashMap<>());
302                 } else {
303                     String[] keyValue = line.split("=");
304                     adapterConfig.get(section).put(keyValue[0].trim(),
305                             keyValue.length == 1 ? "" : keyValue[1].trim());
306                 }
307             }
308         } catch (IOException e) {
309             return null;
310         }
311         return adapterConfig;
312     }
313 
314     /**
315      * Helper class used to run synchronously a runnable action on a looper.
316      */
317     private static final class SyncRunnable implements Runnable {
318         private final Runnable mTarget;
319         private volatile boolean mComplete = false;
320 
SyncRunnable(Runnable target)321         SyncRunnable(Runnable target) {
322             mTarget = target;
323         }
324 
325         @Override
run()326         public void run() {
327             mTarget.run();
328             synchronized (this) {
329                 mComplete = true;
330                 notifyAll();
331             }
332         }
333 
waitForComplete()334         public void waitForComplete() {
335             synchronized (this) {
336                 while (!mComplete) {
337                     try {
338                         wait();
339                     } catch (InterruptedException e) {
340                     }
341                 }
342             }
343         }
344     }
345 }
346