/* * Copyright 2018 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.bluetooth; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.*; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.content.Intent; import android.os.Handler; import android.os.Looper; import androidx.test.InstrumentationRegistry; import androidx.test.rule.ServiceTestRule; import com.android.bluetooth.btservice.AdapterService; import com.android.bluetooth.btservice.ProfileService; import org.junit.Assert; import org.mockito.ArgumentCaptor; import org.mockito.internal.util.MockUtil; import java.io.BufferedReader; import java.io.FileReader; import java.io.IOException; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.HashMap; import java.util.concurrent.BlockingQueue; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; /** * A set of methods useful in Bluetooth instrumentation tests */ public class TestUtils { private static final int SERVICE_TOGGLE_TIMEOUT_MS = 1000; // 1s /** * Utility method to replace obj.fieldName with newValue where obj is of type c * * @param c type of obj * @param fieldName field name to be replaced * @param obj instance of type c whose fieldName is to be replaced, null for static fields * @param newValue object used to replace fieldName * @return the old value of fieldName that got replaced, caller is responsible for restoring * it back to obj * @throws NoSuchFieldException when fieldName is not found in type c * @throws IllegalAccessException when fieldName cannot be accessed in type c */ public static Object replaceField(final Class c, final String fieldName, final Object obj, final Object newValue) throws NoSuchFieldException, IllegalAccessException { Field field = c.getDeclaredField(fieldName); field.setAccessible(true); Object oldValue = field.get(obj); field.set(obj, newValue); return oldValue; } /** * Set the return value of {@link AdapterService#getAdapterService()} to a test specified value * * @param adapterService the designated {@link AdapterService} in test, must not be null, can * be mocked or spied * @throws NoSuchMethodException when setAdapterService method is not found * @throws IllegalAccessException when setAdapterService method cannot be accessed * @throws InvocationTargetException when setAdapterService method cannot be invoked, which * should never happen since setAdapterService is a static method */ public static void setAdapterService(AdapterService adapterService) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException { Assert.assertNull("AdapterService.getAdapterService() must be null before setting another" + " AdapterService", AdapterService.getAdapterService()); Assert.assertNotNull(adapterService); // We cannot mock AdapterService.getAdapterService() with Mockito. // Hence we need to use reflection to call a private method to // initialize properly the AdapterService.sAdapterService field. Method method = AdapterService.class.getDeclaredMethod("setAdapterService", AdapterService.class); method.setAccessible(true); method.invoke(null, adapterService); } /** * Clear the return value of {@link AdapterService#getAdapterService()} to null * * @param adapterService the {@link AdapterService} used when calling * {@link TestUtils#setAdapterService(AdapterService)} * @throws NoSuchMethodException when clearAdapterService method is not found * @throws IllegalAccessException when clearAdapterService method cannot be accessed * @throws InvocationTargetException when clearAdappterService method cannot be invoked, * which should never happen since clearAdapterService is a static method */ public static void clearAdapterService(AdapterService adapterService) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException { Assert.assertSame("AdapterService.getAdapterService() must return the same object as the" + " supplied adapterService in this method", adapterService, AdapterService.getAdapterService()); Assert.assertNotNull(adapterService); Method method = AdapterService.class.getDeclaredMethod("clearAdapterService", AdapterService.class); method.setAccessible(true); method.invoke(null, adapterService); } /** * Start a profile service using the given {@link ServiceTestRule} and verify through * {@link AdapterService#getAdapterService()} that the service is actually started within * {@link TestUtils#SERVICE_TOGGLE_TIMEOUT_MS} milliseconds. * {@link #setAdapterService(AdapterService)} must be called with a mocked * {@link AdapterService} before calling this method * * @param serviceTestRule the {@link ServiceTestRule} used to execute the service start request * @param profileServiceClass a class from one of {@link ProfileService}'s child classes * @throws TimeoutException when service failed to start within either default timeout of * {@link ServiceTestRule#DEFAULT_TIMEOUT} (normally 5s) or user specified time when creating * {@link ServiceTestRule} through {@link ServiceTestRule#withTimeout(long, TimeUnit)} method */ public static void startService(ServiceTestRule serviceTestRule, Class profileServiceClass) throws TimeoutException { AdapterService adapterService = AdapterService.getAdapterService(); Assert.assertNotNull(adapterService); Assert.assertTrue("AdapterService.getAdapterService() must return a mocked or spied object" + " before calling this method", MockUtil.isMock(adapterService)); Intent startIntent = new Intent(InstrumentationRegistry.getTargetContext(), profileServiceClass); startIntent.putExtra(AdapterService.EXTRA_ACTION, AdapterService.ACTION_SERVICE_STATE_CHANGED); startIntent.putExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.STATE_ON); serviceTestRule.startService(startIntent); ArgumentCaptor profile = ArgumentCaptor.forClass(profileServiceClass); verify(adapterService, timeout(SERVICE_TOGGLE_TIMEOUT_MS)).onProfileServiceStateChanged( profile.capture(), eq(BluetoothAdapter.STATE_ON)); Assert.assertEquals(profileServiceClass.getName(), profile.getValue().getClass().getName()); } /** * Stop a profile service using the given {@link ServiceTestRule} and verify through * {@link AdapterService#getAdapterService()} that the service is actually stopped within * {@link TestUtils#SERVICE_TOGGLE_TIMEOUT_MS} milliseconds. * {@link #setAdapterService(AdapterService)} must be called with a mocked * {@link AdapterService} before calling this method * * @param serviceTestRule the {@link ServiceTestRule} used to execute the service start request * @param profileServiceClass a class from one of {@link ProfileService}'s child classes * @throws TimeoutException when service failed to start within either default timeout of * {@link ServiceTestRule#DEFAULT_TIMEOUT} (normally 5s) or user specified time when creating * {@link ServiceTestRule} through {@link ServiceTestRule#withTimeout(long, TimeUnit)} method */ public static void stopService(ServiceTestRule serviceTestRule, Class profileServiceClass) throws TimeoutException { AdapterService adapterService = AdapterService.getAdapterService(); Assert.assertNotNull(adapterService); Assert.assertTrue("AdapterService.getAdapterService() must return a mocked or spied object" + " before calling this method", MockUtil.isMock(adapterService)); Intent stopIntent = new Intent(InstrumentationRegistry.getTargetContext(), profileServiceClass); stopIntent.putExtra(AdapterService.EXTRA_ACTION, AdapterService.ACTION_SERVICE_STATE_CHANGED); stopIntent.putExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.STATE_OFF); serviceTestRule.startService(stopIntent); ArgumentCaptor profile = ArgumentCaptor.forClass(profileServiceClass); verify(adapterService, timeout(SERVICE_TOGGLE_TIMEOUT_MS)).onProfileServiceStateChanged( profile.capture(), eq(BluetoothAdapter.STATE_OFF)); Assert.assertEquals(profileServiceClass.getName(), profile.getValue().getClass().getName()); } /** * Create a test device. * * @param bluetoothAdapter the Bluetooth adapter to use * @param id the test device ID. It must be an integer in the interval [0, 0xFF]. * @return {@link BluetoothDevice} test device for the device ID */ public static BluetoothDevice getTestDevice(BluetoothAdapter bluetoothAdapter, int id) { Assert.assertTrue(id <= 0xFF); Assert.assertNotNull(bluetoothAdapter); BluetoothDevice testDevice = bluetoothAdapter.getRemoteDevice(String.format("00:01:02:03:04:%02X", id)); Assert.assertNotNull(testDevice); return testDevice; } /** * Wait and verify that an intent has been received. * * @param timeoutMs the time (in milliseconds) to wait for the intent * @param queue the queue for the intent * @return the received intent */ public static Intent waitForIntent(int timeoutMs, BlockingQueue queue) { try { Intent intent = queue.poll(timeoutMs, TimeUnit.MILLISECONDS); Assert.assertNotNull(intent); return intent; } catch (InterruptedException e) { Assert.fail("Cannot obtain an Intent from the queue: " + e.getMessage()); } return null; } /** * Wait and verify that no intent has been received. * * @param timeoutMs the time (in milliseconds) to wait and verify no intent * has been received * @param queue the queue for the intent * @return the received intent. Should be null under normal circumstances */ public static Intent waitForNoIntent(int timeoutMs, BlockingQueue queue) { try { Intent intent = queue.poll(timeoutMs, TimeUnit.MILLISECONDS); Assert.assertNull(intent); return intent; } catch (InterruptedException e) { Assert.fail("Cannot obtain an Intent from the queue: " + e.getMessage()); } return null; } /** * Wait for looper to finish its current task and all tasks schedule before this * * @param looper looper of interest */ public static void waitForLooperToFinishScheduledTask(Looper looper) { runOnLooperSync(looper, () -> { // do nothing, just need to make sure looper finishes current task }); } /** * Run synchronously a runnable action on a looper. * The method will return after the action has been execution to completion. * * Example: *
     * {@code
     * TestUtils.runOnMainSync(new Runnable() {
     *       public void run() {
     *           Assert.assertTrue(mA2dpService.stop());
     *       }
     *   });
     * }
     * 
* * @param looper the looper used to run the action * @param action the action to run */ public static void runOnLooperSync(Looper looper, Runnable action) { if (Looper.myLooper() == looper) { // requested thread is the same as the current thread. call directly. action.run(); } else { Handler handler = new Handler(looper); SyncRunnable sr = new SyncRunnable(action); handler.post(sr); sr.waitForComplete(); } } /** * Read Bluetooth adapter configuration from the filesystem * * @return A {@link HashMap} of Bluetooth configs in the format: * section -> key1 -> value1 * -> key2 -> value2 * Assume no empty section name, no duplicate keys in the same section */ public static HashMap> readAdapterConfig() { HashMap> adapterConfig = new HashMap<>(); try (BufferedReader reader = new BufferedReader(new FileReader("/data/misc/bluedroid/bt_config.conf"))) { String section = ""; for (String line; (line = reader.readLine()) != null;) { line = line.trim(); if (line.isEmpty() || line.startsWith("#")) { continue; } if (line.startsWith("[")) { if (line.charAt(line.length() - 1) != ']') { return null; } section = line.substring(1, line.length() - 1); adapterConfig.put(section, new HashMap<>()); } else { String[] keyValue = line.split("="); adapterConfig.get(section).put(keyValue[0].trim(), keyValue.length == 1 ? "" : keyValue[1].trim()); } } } catch (IOException e) { return null; } return adapterConfig; } /** * Helper class used to run synchronously a runnable action on a looper. */ private static final class SyncRunnable implements Runnable { private final Runnable mTarget; private volatile boolean mComplete = false; SyncRunnable(Runnable target) { mTarget = target; } @Override public void run() { mTarget.run(); synchronized (this) { mComplete = true; notifyAll(); } } public void waitForComplete() { synchronized (this) { while (!mComplete) { try { wait(); } catch (InterruptedException e) { } } } } } }