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

Commit 5df5c83f authored by William Escande's avatar William Escande Committed by Gerrit Code Review
Browse files

Merge changes I26c678c1,I93f3a728 into main

* changes:
  Cts Tests: Wait correctly for state transition
  Cts Tests: Moved adapter utils to kotlin
parents ab89d1f4 8a4b4d5d
Loading
Loading
Loading
Loading
+15 −3
Original line number Diff line number Diff line
@@ -26,8 +26,20 @@ java_library {
        "compatibility-device-util-axt",
        "junit",
    ],
    srcs: ["src/**/*.java"],
    srcs: [
        "src/**/*.java",
        "src/BlockingBluetoothAdapter.kt",
    ],
    sdk_version: "test_current",
    // Keep it public for now to avoid merge conflicts
    visibility: ["//visibility:public"],
    visibility: [
        "//cts/tests/tests/bluetooth",
        "//packages/modules/Bluetooth/framework/tests/bumble",

        // TODO: b/339938196 -- remove export for other modules and test
        "//cts/tests/tests/appop",
        "//packages/modules/Connectivity/nearby/tests/cts/fastpair",
        "//packages/modules/Permission/tests/cts/permission",
        "//packages/modules/Permission/tests/cts/permissionui",
        "//test/cts-root/tests/bluetooth",
    ],
}
+213 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 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 android.Manifest.permission.BLUETOOTH_CONNECT
import android.Manifest.permission.BLUETOOTH_PRIVILEGED
import android.app.UiAutomation
import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothAdapter.ACTION_BLE_STATE_CHANGED
import android.bluetooth.BluetoothAdapter.STATE_BLE_ON
import android.bluetooth.BluetoothAdapter.STATE_OFF
import android.bluetooth.BluetoothAdapter.STATE_ON
import android.bluetooth.BluetoothManager
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.util.Log
import androidx.test.platform.app.InstrumentationRegistry
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.channels.trySendBlocking
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withTimeoutOrNull

private const val TAG: String = "BlockingBluetoothAdapter"

object BlockingBluetoothAdapter {
    private val context = InstrumentationRegistry.getInstrumentation().getContext()
    @JvmStatic val adapter = context.getSystemService(BluetoothManager::class.java).getAdapter()

    private val state = AdapterStateListener(context, adapter)
    private val uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation()

    // BLE_START_TIMEOUT_DELAY + BREDR_START_TIMEOUT_DELAY + (10 seconds of additional delay)
    private val stateChangeTimeout = 18.seconds

    init {
        Log.d(TAG, "Started with initial state to $state")
    }

    /** Set Bluetooth in BLE mode. Only works if it was OFF before */
    @JvmStatic
    fun enableBLE(): Boolean {
        if (!state.eq(STATE_OFF)) {
            throw IllegalStateException("Invalid call to enableBLE while current state is: $state")
        }
        Log.d(TAG, "Call to enableBLE")
        if (!withPermission(BLUETOOTH_CONNECT).use { adapter.enableBLE() }) {
            Log.e(TAG, "enableBLE: Failed")
            return false
        }
        return state.waitForStateWithTimeout(stateChangeTimeout, STATE_BLE_ON)
    }

    /** Restore Bluetooth to OFF. Only works if it was in BLE_ON due to enableBLE call */
    @JvmStatic
    fun disableBLE(): Boolean {
        if (!state.eq(STATE_BLE_ON)) {
            throw IllegalStateException("Invalid call to disableBLE while current state is: $state")
        }
        Log.d(TAG, "Call to disableBLE")
        if (!withPermission(BLUETOOTH_CONNECT).use { adapter.disableBLE() }) {
            Log.e(TAG, "disableBLE: Failed")
            return false
        }
        return state.waitForStateWithTimeout(stateChangeTimeout, STATE_OFF)
    }

    /** Turn Bluetooth ON and wait for state change */
    @JvmStatic
    fun enable(): Boolean {
        if (state.eq(STATE_ON)) {
            Log.i(TAG, "enable: state is already $state")
            return true
        }
        Log.d(TAG, "Call to enable")
        if (
            !withPermission(BLUETOOTH_CONNECT, BLUETOOTH_PRIVILEGED).use {
                @Suppress("DEPRECATION") adapter.enable()
            }
        ) {
            Log.e(TAG, "enable: Failed")
            return false
        }
        return state.waitForStateWithTimeout(stateChangeTimeout, STATE_ON)
    }

    /** Turn Bluetooth OFF and wait for state change */
    @JvmStatic
    fun disable(persist: Boolean = true): Boolean {
        if (state.eq(STATE_OFF)) {
            Log.i(TAG, "disable: state is already $state")
            return true
        }
        Log.d(TAG, "Call to disable($persist)")
        if (
            !withPermission(BLUETOOTH_CONNECT, BLUETOOTH_PRIVILEGED).use {
                adapter.disable(persist)
            }
        ) {
            Log.e(TAG, "disable: Failed")
            return false
        }
        // Notify that disable was call.
        state.wasDisabled = true
        return state.waitForStateWithTimeout(stateChangeTimeout, STATE_OFF)
    }

    private fun restorePermissions(permissions: Set<String>) {
        if (UiAutomation.ALL_PERMISSIONS.equals(permissions)) {
            uiAutomation.adoptShellPermissionIdentity()
        } else {
            uiAutomation.adoptShellPermissionIdentity(*permissions.map { it }.toTypedArray())
        }
    }

    private fun replacePermissionsWith(vararg newPermissions: String): Set<String> {
        val currentPermissions = uiAutomation.getAdoptedShellPermissions()
        if (newPermissions.size == 0) {
            // Throw even if the code support it as we are not expecting this by design
            throw IllegalArgumentException("Invalid permissions replacement with no permissions.")
        }
        uiAutomation.adoptShellPermissionIdentity(*newPermissions)
        return currentPermissions
    }

    // Set permissions to be used as long as the resource is open.
    // Restore initial permissions after closing resource.
    private fun withPermission(
        vararg newPermissions: String,
    ): AutoCloseable {
        val savedPermissions = replacePermissionsWith(*newPermissions)
        return AutoCloseable { restorePermissions(savedPermissions) }
    }
}

private class AdapterStateListener(context: Context, private val adapter: BluetoothAdapter) {
    private val STATE_UNKNOWN = -42

    // Set to true once a call to disable is made, in order to force the differentiation between the
    // various state hidden within STATE_OFF (OFF, BLE_TURNING_ON, BLE_TURNING_OFF)
    // Once true, getter will return STATE_OFF when there has not been any callback sent to it
    var wasDisabled = false

    val adapterStateFlow =
        callbackFlow<Intent> {
                val broadcastReceiver =
                    object : BroadcastReceiver() {
                        override fun onReceive(context: Context, intent: Intent) {
                            trySendBlocking(intent)
                        }
                    }
                context.registerReceiver(broadcastReceiver, IntentFilter(ACTION_BLE_STATE_CHANGED))

                awaitClose { context.unregisterReceiver(broadcastReceiver) }
            }
            .map { it.getIntExtra(BluetoothAdapter.EXTRA_STATE, -1) }
            .onEach { Log.d(TAG, "State changed to ${BluetoothAdapter.nameForState(it)}") }
            .shareIn(CoroutineScope(Dispatchers.Default), SharingStarted.Eagerly, 1)

    private fun get(): Int =
        adapterStateFlow.replayCache.getOrElse(0) {
            val state: Int = adapter.getState()
            if (state != STATE_OFF) {
                state
            } else if (adapter.isLeEnabled()) {
                STATE_BLE_ON
            } else if (wasDisabled) {
                STATE_OFF
            } else {
                STATE_UNKNOWN
            }
        }

    fun eq(state: Int): Boolean = state == get()

    override fun toString(): String {
        val currentState = get()
        return if (currentState == STATE_UNKNOWN) {
            "UNKNOWN: State is uncertain, oneOf(OFF, BLE_TURNING_ON, BLE_TURNING_OFF)"
        } else {
            BluetoothAdapter.nameForState(currentState)
        }
    }

    fun waitForStateWithTimeout(timeout: Duration, state: Int): Boolean = runBlocking {
        withTimeoutOrNull(timeout) { adapterStateFlow.filter { it == state }.first() } != null
    }
}
+51 −2
Original line number Diff line number Diff line
@@ -16,6 +16,55 @@

package android.bluetooth.cts;

import android.bluetooth.test_utils.BluetoothAdapterUtils;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.test_utils.BlockingBluetoothAdapter;
import android.content.Context;

public class BTAdapterUtils extends BluetoothAdapterUtils {}
/**
 * @deprecated see {@link android.bluetooth.test_utils.BlockingBluetoothAdapter}
 */
@Deprecated
public class BTAdapterUtils {
    private BTAdapterUtils() {}

    /**
     * @deprecated see {@link android.bluetooth.test_utils.BlockingBluetoothAdapter#enable}
     */
    @Deprecated
    public static final boolean enableAdapter(BluetoothAdapter adapter, Context ctx) {
        return BlockingBluetoothAdapter.enable();
    }

    /**
     * @deprecated see {@link android.bluetooth.test_utils.BlockingBluetoothAdapter#disable}
     */
    @Deprecated
    public static final boolean disableAdapter(BluetoothAdapter adapter, Context ctx) {
        return BlockingBluetoothAdapter.disable(true);
    }

    /**
     * @deprecated see {@link android.bluetooth.test_utils.BlockingBluetoothAdapter#disable}
     */
    @Deprecated
    public static final boolean disableAdapter(
            BluetoothAdapter adapter, boolean persist, Context ctx) {
        return BlockingBluetoothAdapter.disable(persist);
    }

    /**
     * @deprecated see {@link android.bluetooth.test_utils.BlockingBluetoothAdapter#enableBLE}
     */
    @Deprecated
    public static final boolean enableBLE(BluetoothAdapter adapter, Context ctx) {
        return BlockingBluetoothAdapter.enableBLE();
    }

    /**
     * @deprecated see {@link android.bluetooth.test_utils.BlockingBluetoothAdapter#disableBLE}
     */
    @Deprecated
    public static final boolean disableBLE(BluetoothAdapter adapter, Context ctx) {
        return BlockingBluetoothAdapter.disableBLE();
    }
}
+0 −295
Original line number Diff line number Diff line
/*
 * Copyright (C) 2020 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 android.Manifest.permission.BLUETOOTH_CONNECT;
import static android.Manifest.permission.BLUETOOTH_PRIVILEGED;

import android.app.UiAutomation;
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 BluetoothAdapterUtils {
    private static final String TAG = BluetoothAdapterUtils.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();
                try {
                    sAdapterState = newState;
                    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);
        if (UiAutomation.ALL_PERMISSIONS.equals(permissionsAdopted)) {
            permissionArray = null;
        }

        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);
        if (UiAutomation.ALL_PERMISSIONS.equals(permissionsAdopted)) {
            permissionArray = null;
        }

        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;
    }
}
+23 −35

File changed.

Preview size limit exceeded, changes collapsed.