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