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