Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 97f2bce9 authored by Jack He's avatar Jack He
Browse files

Move BT framework test utils to test_utils package

* Leaving an alias in android.bluetooth.cts so that dependencies can
  still compile

MUST_SLEEP=Legacy CTS code still use Thread.sleep(). Hence this library
cannot avoid it.

Bug: 300052980
Test: atest CtsBluetoothTestCases
Test: atest CtsRootBluetoothTestCases
Change-Id: I14f5f334144834710f14616301ae8b5e4df9068d
parent 965b0a31
Loading
Loading
Loading
Loading
+3 −269
Original line number Diff line number Diff line
/*
 * Copyright (C) 2020 The Android Open Source Project
 * Copyright (C) 2023 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.
@@ -16,272 +16,6 @@

package android.bluetooth.cts;

import static android.Manifest.permission.BLUETOOTH_CONNECT;
import static android.Manifest.permission.BLUETOOTH_PRIVILEGED;
import android.bluetooth.test_utils.BluetoothAdapterUtils;

import android.bluetooth.BluetoothAdapter;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.util.Log;

import java.time.Duration;
import java.util.HashMap;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

/** Utility for controlling the Bluetooth adapter from CTS test. */
public class BTAdapterUtils {
    private static final String TAG = BTAdapterUtils.class.getSimpleName();

    /**
     * ADAPTER_ENABLE_TIMEOUT_MS = AdapterState.BLE_START_TIMEOUT_DELAY +
     * AdapterState.BREDR_START_TIMEOUT_DELAY + (10 seconds of additional delay)
     */
    private static final Duration ADAPTER_ENABLE_TIMEOUT = Duration.ofSeconds(18);
    /**
     * ADAPTER_DISABLE_TIMEOUT_MS = AdapterState.BLE_STOP_TIMEOUT_DELAY +
     * AdapterState.BREDR_STOP_TIMEOUT_DELAY
     */
    private static final Duration ADAPTER_DISABLE_TIMEOUT = Duration.ofSeconds(5);

    /** Redefined from {@link BluetoothAdapter} because of hidden APIs */
    public static final int STATE_BLE_TURNING_ON = 14;

    public static final int STATE_BLE_TURNING_OFF = 16;

    private static final HashMap<Integer, Duration> sStateTimeouts = new HashMap<>();

    static {
        sStateTimeouts.put(BluetoothAdapter.STATE_OFF, ADAPTER_DISABLE_TIMEOUT);
        sStateTimeouts.put(BluetoothAdapter.STATE_TURNING_ON, ADAPTER_ENABLE_TIMEOUT);
        sStateTimeouts.put(BluetoothAdapter.STATE_ON, ADAPTER_ENABLE_TIMEOUT);
        sStateTimeouts.put(BluetoothAdapter.STATE_TURNING_OFF, ADAPTER_DISABLE_TIMEOUT);
        sStateTimeouts.put(STATE_BLE_TURNING_ON, ADAPTER_ENABLE_TIMEOUT);
        sStateTimeouts.put(BluetoothAdapter.STATE_BLE_ON, ADAPTER_ENABLE_TIMEOUT);
        sStateTimeouts.put(STATE_BLE_TURNING_OFF, ADAPTER_DISABLE_TIMEOUT);
    }

    private static boolean sAdapterVarsInitialized;
    private static ReentrantLock sBluetoothAdapterLock;
    private static Condition sConditionAdapterStateReached;
    private static int sDesiredState;
    private static int sAdapterState;

    /** Handles BluetoothAdapter state changes and signals when we have reached a desired state */
    private static class BluetoothAdapterReceiver extends BroadcastReceiver {
        @Override
        public void onReceive(Context context, Intent intent) {
            String action = intent.getAction();
            if (BluetoothAdapter.ACTION_BLE_STATE_CHANGED.equals(action)) {
                int newState = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, -1);
                Log.d(TAG, "Bluetooth adapter state changed: " + newState);

                // Signal if the state is set to the one we are waiting on
                sBluetoothAdapterLock.lock();
                sAdapterState = newState;
                try {
                    if (sDesiredState == newState) {
                        Log.d(TAG, "Adapter has reached desired state: " + sDesiredState);
                        sConditionAdapterStateReached.signal();
                    }
                } finally {
                    sBluetoothAdapterLock.unlock();
                }
            }
        }
    }

    /** Initialize all static state variables */
    private static void initAdapterStateVariables(Context context) {
        Log.d(TAG, "Initializing adapter state variables");
        BluetoothAdapterReceiver sAdapterReceiver = new BluetoothAdapterReceiver();
        sBluetoothAdapterLock = new ReentrantLock();
        sConditionAdapterStateReached = sBluetoothAdapterLock.newCondition();
        sDesiredState = -1;
        sAdapterState = -1;
        IntentFilter filter = new IntentFilter(BluetoothAdapter.ACTION_BLE_STATE_CHANGED);
        context.registerReceiver(sAdapterReceiver, filter);
        sAdapterVarsInitialized = true;
    }

    /**
     * Helper method to wait for the bluetooth adapter to be in a given state
     *
     * <p>Assumes all state variables are initialized. Assumes it's being run with
     * sBluetoothAdapterLock in the locked state.
     */
    private static boolean waitForAdapterStateLocked(int desiredState, BluetoothAdapter adapter)
            throws InterruptedException {
        Duration timeout = sStateTimeouts.getOrDefault(desiredState, ADAPTER_ENABLE_TIMEOUT);

        Log.d(TAG, "Waiting for adapter state " + desiredState);
        sDesiredState = desiredState;

        // Wait until we have reached the desired state
        // Handle spurious wakeup
        while (desiredState != sAdapterState) {
            if (sConditionAdapterStateReached.await(timeout.toMillis(), TimeUnit.MILLISECONDS)) {
                // Handle spurious wakeup
                continue;
            }
            // Handle timeout cases
            // Case 1: situation where state change occurs, but we don't receive the broadcast
            if (desiredState >= BluetoothAdapter.STATE_OFF
                    && desiredState <= BluetoothAdapter.STATE_TURNING_OFF) {
                int currentState = adapter.getState();
                Log.d(TAG, "desiredState: " + desiredState + ", currentState: " + currentState);
                return desiredState == currentState;
            } else if (desiredState == BluetoothAdapter.STATE_BLE_ON) {
                Log.d(TAG, "adapter isLeEnabled: " + adapter.isLeEnabled());
                return adapter.isLeEnabled();
            }
            // Case 2: Actual timeout
            Log.e(
                    TAG,
                    "Timeout while waiting for Bluetooth adapter state "
                            + desiredState
                            + " while current state is "
                            + sAdapterState);
            break;
        }

        Log.d(TAG, "Final state while waiting: " + sAdapterState);
        return sAdapterState == desiredState;
    }

    /** Utility method to wait on any specific adapter state */
    public static boolean waitForAdapterState(int desiredState, BluetoothAdapter adapter) {
        sBluetoothAdapterLock.lock();
        try {
            return waitForAdapterStateLocked(desiredState, adapter);
        } catch (InterruptedException e) {
            Log.w(TAG, "waitForAdapterState(): interrupted", e);
        } finally {
            sBluetoothAdapterLock.unlock();
        }
        return false;
    }

    /** Enables Bluetooth to a Low Energy only mode */
    public static boolean enableBLE(BluetoothAdapter bluetoothAdapter, Context context) {
        if (!sAdapterVarsInitialized) {
            initAdapterStateVariables(context);
        }

        if (bluetoothAdapter.isLeEnabled()) {
            return true;
        }

        sBluetoothAdapterLock.lock();
        try {
            Log.d(TAG, "Enabling Bluetooth low energy only mode");
            if (!bluetoothAdapter.enableBLE()) {
                Log.e(TAG, "Unable to enable Bluetooth low energy only mode");
                return false;
            }
            return waitForAdapterStateLocked(BluetoothAdapter.STATE_BLE_ON, bluetoothAdapter);
        } catch (InterruptedException e) {
            Log.w(TAG, "enableBLE(): interrupted", e);
        } finally {
            sBluetoothAdapterLock.unlock();
        }
        return false;
    }

    /** Disable Bluetooth Low Energy mode */
    public static boolean disableBLE(BluetoothAdapter bluetoothAdapter, Context context) {
        if (!sAdapterVarsInitialized) {
            initAdapterStateVariables(context);
        }

        if (bluetoothAdapter.getState() == BluetoothAdapter.STATE_OFF) {
            return true;
        }

        sBluetoothAdapterLock.lock();
        try {
            Log.d(TAG, "Disabling Bluetooth low energy");
            bluetoothAdapter.disableBLE();
            return waitForAdapterStateLocked(BluetoothAdapter.STATE_OFF, bluetoothAdapter);
        } catch (InterruptedException e) {
            Log.w(TAG, "disableBLE(): interrupted", e);
        } finally {
            sBluetoothAdapterLock.unlock();
        }
        return false;
    }

    /** Enables the Bluetooth Adapter. Return true if it is already enabled or is enabled. */
    public static boolean enableAdapter(BluetoothAdapter bluetoothAdapter, Context context) {
        if (!sAdapterVarsInitialized) {
            initAdapterStateVariables(context);
        }

        if (bluetoothAdapter.isEnabled()) {
            return true;
        }

        Set<String> permissionsAdopted = TestUtils.getAdoptedShellPermissions();
        String[] permissionArray = permissionsAdopted.toArray(String[]::new);

        sBluetoothAdapterLock.lock();
        try {
            Log.d(TAG, "Enabling Bluetooth adapter");
            TestUtils.dropPermissionAsShellUid();
            TestUtils.adoptPermissionAsShellUid(BLUETOOTH_CONNECT, BLUETOOTH_PRIVILEGED);
            bluetoothAdapter.enable();
            return waitForAdapterStateLocked(BluetoothAdapter.STATE_ON, bluetoothAdapter);
        } catch (InterruptedException e) {
            Log.w(TAG, "enableAdapter(): interrupted", e);
        } finally {
            TestUtils.dropPermissionAsShellUid();
            TestUtils.adoptPermissionAsShellUid(permissionArray);
            sBluetoothAdapterLock.unlock();
        }
        return false;
    }

    /** Disable the Bluetooth Adapter. Return true if it is already disabled or is disabled. */
    public static boolean disableAdapter(BluetoothAdapter bluetoothAdapter, Context context) {
        return disableAdapter(bluetoothAdapter, true, context);
    }

    /**
     * Disable the Bluetooth Adapter with then option to persist the off state or not.
     *
     * <p>Returns true if the adapter is already disabled or was disabled.
     */
    public static boolean disableAdapter(
            BluetoothAdapter bluetoothAdapter, boolean persist, Context context) {
        if (!sAdapterVarsInitialized) {
            initAdapterStateVariables(context);
        }

        if (bluetoothAdapter.getState() == BluetoothAdapter.STATE_OFF) {
            return true;
        }

        Set<String> permissionsAdopted = TestUtils.getAdoptedShellPermissions();
        String[] permissionArray = permissionsAdopted.toArray(String[]::new);

        sBluetoothAdapterLock.lock();
        try {
            Log.d(TAG, "Disabling Bluetooth adapter, persist=" + persist);
            TestUtils.dropPermissionAsShellUid();
            TestUtils.adoptPermissionAsShellUid(BLUETOOTH_CONNECT, BLUETOOTH_PRIVILEGED);
            bluetoothAdapter.disable(persist);
            return waitForAdapterStateLocked(BluetoothAdapter.STATE_OFF, bluetoothAdapter);
        } catch (InterruptedException e) {
            Log.w(TAG, "disableAdapter(persist=" + persist + "): interrupted", e);
        } finally {
            TestUtils.dropPermissionAsShellUid();
            TestUtils.adoptPermissionAsShellUid(permissionArray);
            sBluetoothAdapterLock.unlock();
        }
        return false;
    }
}
public class BTAdapterUtils extends BluetoothAdapterUtils {}
+3 −86
Original line number Diff line number Diff line
@@ -16,44 +16,7 @@

package android.bluetooth.cts;

import static com.android.compatibility.common.util.SystemUtil.runShellCommandOrThrow;

import static com.google.common.truth.Truth.assertThat;

import static org.junit.Assume.assumeTrue;

import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothManager;
import android.content.Context;

import androidx.test.platform.app.InstrumentationRegistry;

import com.android.compatibility.common.util.BeforeAfterRule;

import org.junit.runner.Description;
import org.junit.runners.model.Statement;

/**
 * This is a test rule that, when used in a test, will enable Bluetooth before the test starts. When
 * the test is done, Bluetooth will be disabled if and only if it was disabled before the test
 * started. If setTestMode is set to true, the Bluetooth scanner will return a hardcoded set of
 * Bluetooth scan results while the test runs .
 */
public class EnableBluetoothRule extends BeforeAfterRule {
    private final Context mContext = InstrumentationRegistry.getInstrumentation().getContext();
    private final BluetoothAdapter mBluetoothAdapter =
            mContext.getSystemService(BluetoothManager.class).getAdapter();
    private final boolean mEnableTestMode;
    private final boolean mToggleBluetooth;

    private boolean mWasBluetoothAdapterEnabled = true;

    /** Empty constructor */
    public EnableBluetoothRule() {
        mEnableTestMode = false;
        mToggleBluetooth = false;
    }

public class EnableBluetoothRule extends android.bluetooth.test_utils.EnableBluetoothRule {
    /**
     * Constructor that allows test mode
     *
@@ -62,8 +25,7 @@ public class EnableBluetoothRule extends BeforeAfterRule {
     *     already enabled
     */
    public EnableBluetoothRule(boolean enableTestMode, boolean toggleBluetooth) {
        mEnableTestMode = enableTestMode;
        mToggleBluetooth = toggleBluetooth;
        super(enableTestMode, toggleBluetooth);
    }

    /**
@@ -72,51 +34,6 @@ public class EnableBluetoothRule extends BeforeAfterRule {
     * @param enableTestMode whether test mode is enabled
     */
    public EnableBluetoothRule(boolean enableTestMode) {
        mEnableTestMode = enableTestMode;
        mToggleBluetooth = false;
    }

    private void enableBluetoothAdapter() {
        assertThat(BTAdapterUtils.enableAdapter(mBluetoothAdapter, mContext)).isTrue();
    }

    private void disableBluetoothAdapter() {
        assertThat(BTAdapterUtils.disableAdapter(mBluetoothAdapter, mContext)).isTrue();
    }

    private void enableBluetoothTestMode() {
        runShellCommandOrThrow(
                "dumpsys activity service"
                        + " com.android.bluetooth.btservice.AdapterService set-test-mode enabled");
    }

    private void disableBluetoothTestMode() {
        runShellCommandOrThrow(
                "dumpsys activity service"
                        + " com.android.bluetooth.btservice.AdapterService set-test-mode disabled");
    }

    @Override
    protected void onBefore(Statement base, Description description) {
        assumeTrue(TestUtils.hasBluetooth());
        mWasBluetoothAdapterEnabled = mBluetoothAdapter.isEnabled();
        if (!mWasBluetoothAdapterEnabled) {
            enableBluetoothAdapter();
        } else if (mToggleBluetooth) {
            disableBluetoothAdapter();
            enableBluetoothAdapter();
        }
        if (mEnableTestMode) {
            enableBluetoothTestMode();
        }
    }

    @Override
    protected void onAfter(Statement base, Description description) {
        assumeTrue(TestUtils.hasBluetooth());
        disableBluetoothTestMode();
        if (!mWasBluetoothAdapterEnabled) {
            disableBluetoothAdapter();
        }
        super(enableTestMode);
    }
}
+6 −297

File changed.

Preview size limit exceeded, changes collapsed.

+287 −0

File added.

Preview size limit exceeded, changes collapsed.

+122 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 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 android.bluetooth.test_utils;

import static com.android.compatibility.common.util.SystemUtil.runShellCommandOrThrow;

import static com.google.common.truth.Truth.assertThat;

import static org.junit.Assume.assumeTrue;

import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothManager;
import android.content.Context;

import androidx.test.platform.app.InstrumentationRegistry;

import com.android.compatibility.common.util.BeforeAfterRule;

import org.junit.runner.Description;
import org.junit.runners.model.Statement;

/**
 * This is a test rule that, when used in a test, will enable Bluetooth before the test starts. When
 * the test is done, Bluetooth will be disabled if and only if it was disabled before the test
 * started. If setTestMode is set to true, the Bluetooth scanner will return a hardcoded set of
 * Bluetooth scan results while the test runs .
 */
public class EnableBluetoothRule extends BeforeAfterRule {
    private final Context mContext = InstrumentationRegistry.getInstrumentation().getContext();
    private final BluetoothAdapter mBluetoothAdapter =
            mContext.getSystemService(BluetoothManager.class).getAdapter();
    private final boolean mEnableTestMode;
    private final boolean mToggleBluetooth;

    private boolean mWasBluetoothAdapterEnabled = true;

    /** Empty constructor */
    public EnableBluetoothRule() {
        mEnableTestMode = false;
        mToggleBluetooth = false;
    }

    /**
     * Constructor that allows test mode
     *
     * @param enableTestMode whether test mode is enabled
     * @param toggleBluetooth whether to toggle Bluetooth at the beginning of the test if it is
     *     already enabled
     */
    public EnableBluetoothRule(boolean enableTestMode, boolean toggleBluetooth) {
        mEnableTestMode = enableTestMode;
        mToggleBluetooth = toggleBluetooth;
    }

    /**
     * Constructor that allows test mode
     *
     * @param enableTestMode whether test mode is enabled
     */
    public EnableBluetoothRule(boolean enableTestMode) {
        mEnableTestMode = enableTestMode;
        mToggleBluetooth = false;
    }

    private void enableBluetoothAdapter() {
        assertThat(BluetoothAdapterUtils.enableAdapter(mBluetoothAdapter, mContext)).isTrue();
    }

    private void disableBluetoothAdapter() {
        assertThat(BluetoothAdapterUtils.disableAdapter(mBluetoothAdapter, mContext)).isTrue();
    }

    private void enableBluetoothTestMode() {
        runShellCommandOrThrow(
                "dumpsys activity service"
                        + " com.android.bluetooth.btservice.AdapterService set-test-mode enabled");
    }

    private void disableBluetoothTestMode() {
        runShellCommandOrThrow(
                "dumpsys activity service"
                        + " com.android.bluetooth.btservice.AdapterService set-test-mode disabled");
    }

    @Override
    protected void onBefore(Statement base, Description description) {
        assumeTrue(TestUtils.hasBluetooth());
        mWasBluetoothAdapterEnabled = mBluetoothAdapter.isEnabled();
        if (!mWasBluetoothAdapterEnabled) {
            enableBluetoothAdapter();
        } else if (mToggleBluetooth) {
            disableBluetoothAdapter();
            enableBluetoothAdapter();
        }
        if (mEnableTestMode) {
            enableBluetoothTestMode();
        }
    }

    @Override
    protected void onAfter(Statement base, Description description) {
        assumeTrue(TestUtils.hasBluetooth());
        disableBluetoothTestMode();
        if (!mWasBluetoothAdapterEnabled) {
            disableBluetoothAdapter();
        }
    }
}
Loading