| /* |
| * 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 com.google.common.truth.Truth.assertWithMessage; |
| |
| import static org.mockito.ArgumentMatchers.eq; |
| import static org.mockito.Mockito.*; |
| |
| import android.bluetooth.BluetoothAdapter; |
| import android.bluetooth.BluetoothDevice; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.pm.PackageManager; |
| import android.content.res.Resources; |
| import android.os.Handler; |
| import android.os.Looper; |
| import android.os.MessageQueue; |
| import android.service.media.MediaBrowserService; |
| import android.util.Log; |
| |
| import androidx.test.InstrumentationRegistry; |
| import androidx.test.rule.ServiceTestRule; |
| import androidx.test.uiautomator.UiDevice; |
| |
| import com.android.bluetooth.avrcpcontroller.BluetoothMediaBrowserService; |
| import com.android.bluetooth.btservice.AdapterService; |
| import com.android.bluetooth.btservice.ProfileService; |
| import com.android.bluetooth.gatt.GattService; |
| |
| import org.junit.Assert; |
| import org.junit.rules.TestRule; |
| import org.junit.runner.Description; |
| import org.junit.runners.model.Statement; |
| 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.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 |
| |
| private static String sSystemScreenOffTimeout = "10000"; |
| |
| private static final String TAG = "BluetoothTestUtils"; |
| |
| /** |
| * 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 |
| */ |
| public static void setAdapterService(AdapterService adapterService) { |
| Assert.assertNull("AdapterService.getAdapterService() must be null before setting another" |
| + " AdapterService", AdapterService.getAdapterService()); |
| Assert.assertNotNull("Adapter service should not be null", adapterService); |
| // We cannot mock AdapterService.getAdapterService() with Mockito. |
| // Hence we need to set AdapterService.sAdapterService field. |
| AdapterService.setAdapterService(adapterService); |
| } |
| |
| /** |
| * Clear the return value of {@link AdapterService#getAdapterService()} to null |
| * |
| * @param adapterService the {@link AdapterService} used when calling {@link |
| * TestUtils#setAdapterService(AdapterService)} |
| */ |
| public static void clearAdapterService(AdapterService adapterService) { |
| Assert.assertSame("AdapterService.getAdapterService() must return the same object as the" |
| + " supplied adapterService in this method", adapterService, |
| AdapterService.getAdapterService()); |
| Assert.assertNotNull("Adapter service should not be null", adapterService); |
| AdapterService.clearAdapterService(adapterService); |
| } |
| |
| /** Helper function to mock getSystemService calls */ |
| public static <T> void mockGetSystemService( |
| Context ctx, String serviceName, Class<T> serviceClass, T mockService) { |
| when(ctx.getSystemService(eq(serviceName))).thenReturn(mockService); |
| when(ctx.getSystemServiceName(eq(serviceClass))).thenReturn(serviceName); |
| } |
| |
| /** Helper function to mock getSystemService calls */ |
| public static <T> T mockGetSystemService( |
| Context ctx, String serviceName, Class<T> serviceClass) { |
| T mockedService = mock(serviceClass); |
| mockGetSystemService(ctx, serviceName, serviceClass, mockedService); |
| return mockedService; |
| } |
| |
| /** |
| * 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 <T extends ProfileService> void startService(ServiceTestRule serviceTestRule, |
| Class<T> profileServiceClass) throws TimeoutException { |
| if (profileServiceClass == GattService.class) { |
| Assert.assertFalse("GattService cannot be started as a service", true); |
| } |
| AdapterService adapterService = AdapterService.getAdapterService(); |
| Assert.assertNotNull("Adapter service should not be null", 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<ProfileService> 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 <T extends ProfileService> void stopService(ServiceTestRule serviceTestRule, |
| Class<T> 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<ProfileService> 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()); |
| ArgumentCaptor<ProfileService> profile2 = ArgumentCaptor.forClass(profileServiceClass); |
| verify(adapterService, timeout(SERVICE_TOGGLE_TIMEOUT_MS)).removeProfile( |
| profile2.capture()); |
| Assert.assertEquals(profileServiceClass.getName(), |
| profile2.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; |
| } |
| |
| public static Resources getTestApplicationResources(Context context) { |
| try { |
| return context.getPackageManager().getResourcesForApplication( |
| "com.android.bluetooth.tests"); |
| } catch (PackageManager.NameNotFoundException e) { |
| assertWithMessage("Setup Failure: Unable to get test application resources" |
| + e.toString()).fail(); |
| return null; |
| } |
| } |
| |
| /** |
| * 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<Intent> 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<Intent> 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 |
| }); |
| } |
| |
| /** |
| * Wait for looper to become idle |
| * |
| * @param looper looper of interest |
| */ |
| public static void waitForLooperToBeIdle(Looper looper) { |
| class Idler implements MessageQueue.IdleHandler { |
| private boolean mIdle = false; |
| |
| @Override |
| public boolean queueIdle() { |
| synchronized (this) { |
| mIdle = true; |
| notifyAll(); |
| } |
| return false; |
| } |
| |
| public synchronized void waitForIdle() { |
| while (!mIdle) { |
| try { |
| wait(); |
| } catch (InterruptedException e) { |
| } |
| } |
| } |
| } |
| |
| Idler idle = new Idler(); |
| looper.getQueue().addIdleHandler(idle); |
| // Ensure we are not Idle to begin with so the idle handler will run |
| waitForLooperToFinishScheduledTask(looper); |
| idle.waitForIdle(); |
| } |
| |
| /** |
| * Run synchronously a runnable action on a looper. |
| * The method will return after the action has been execution to completion. |
| * |
| * Example: |
| * <pre> |
| * {@code |
| * TestUtils.runOnMainSync(new Runnable() { |
| * public void run() { |
| * Assert.assertTrue(mA2dpService.stop()); |
| * } |
| * }); |
| * } |
| * </pre> |
| * |
| * @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<String, HashMap<String, String>> readAdapterConfig() { |
| HashMap<String, HashMap<String, String>> 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; |
| } |
| |
| /** |
| * Prepare the intent to start bluetooth browser media service. |
| * |
| * @return intent with the appropriate component & action set. |
| */ |
| public static Intent prepareIntentToStartBluetoothBrowserMediaService() { |
| final Intent intent = new Intent(InstrumentationRegistry.getTargetContext(), |
| BluetoothMediaBrowserService.class); |
| intent.setAction(MediaBrowserService.SERVICE_INTERFACE); |
| return intent; |
| } |
| |
| public static void setUpUiTest() throws Exception { |
| final UiDevice device = UiDevice.getInstance( |
| androidx.test.platform.app.InstrumentationRegistry.getInstrumentation()); |
| // Disable animation |
| device.executeShellCommand("settings put global window_animation_scale 0.0"); |
| device.executeShellCommand("settings put global transition_animation_scale 0.0"); |
| device.executeShellCommand("settings put global animator_duration_scale 0.0"); |
| |
| // change device screen_off_timeout to 5 minutes |
| sSystemScreenOffTimeout = |
| device.executeShellCommand("settings get system screen_off_timeout"); |
| device.executeShellCommand("settings put system screen_off_timeout 300000"); |
| |
| // Turn on screen and unlock |
| device.wakeUp(); |
| device.executeShellCommand("wm dismiss-keyguard"); |
| |
| // Back to home screen, in case some dialog/activity is in front |
| UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()).pressHome(); |
| } |
| |
| public static void tearDownUiTest() throws Exception { |
| final UiDevice device = UiDevice.getInstance( |
| androidx.test.platform.app.InstrumentationRegistry.getInstrumentation()); |
| device.executeShellCommand("wm dismiss-keyguard"); |
| |
| // Re-enable animation |
| device.executeShellCommand("settings put global window_animation_scale 1.0"); |
| device.executeShellCommand("settings put global transition_animation_scale 1.0"); |
| device.executeShellCommand("settings put global animator_duration_scale 1.0"); |
| |
| // restore screen_off_timeout |
| device.executeShellCommand("settings put system screen_off_timeout " |
| + sSystemScreenOffTimeout); |
| } |
| |
| public static class RetryTestRule implements TestRule { |
| private int retryCount = 5; |
| |
| public RetryTestRule() { |
| this(5); |
| } |
| |
| public RetryTestRule(int retryCount) { |
| this.retryCount = retryCount; |
| } |
| |
| public Statement apply(Statement base, Description description) { |
| return new Statement() { |
| @Override |
| public void evaluate() throws Throwable { |
| Throwable caughtThrowable = null; |
| |
| // implement retry logic here |
| for (int i = 0; i < retryCount; i++) { |
| try { |
| base.evaluate(); |
| return; |
| } catch (Throwable t) { |
| caughtThrowable = t; |
| Log.e( |
| TAG, |
| description.getDisplayName() + ": run " + (i + 1) + " failed", |
| t); |
| } |
| } |
| Log.e( |
| TAG, |
| description.getDisplayName() |
| + ": giving up after " |
| + retryCount |
| + " failures"); |
| throw caughtThrowable; |
| } |
| }; |
| } |
| } |
| |
| /** |
| * 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) { |
| } |
| } |
| } |
| } |
| } |
| } |