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