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