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

Commit 78005618 authored by Jeff Sharkey's avatar Jeff Sharkey
Browse files

Rudimentary automated testing for LE beacons.

Talking with the Bluetooth team, we know there are more robust
testing strategies coming with Root Canal and Blueberry, but those
aren't ready for use today.

As part of building out the "Nearby devices" permission we'd really
like to get automated CTS testing in place, so this change adds a
very rudimentary testing mode to the Bluetooth stack.  When enabled,
this mode suppresses scan results returned from the native stack,
and replaces them with a set of hard-coded BT LE beacons which can
be used for CTS purposes.

This new testing mode can be controlled via the commands below which
are only available to AID_SHELL, and which CTS can invoke using
executeShellCommand().

$ adb shell dumpsys activity service \
    com.android.bluetooth/.btservice.AdapterService \
    set-test-mode [enabled|disabled]

Bug: 179808874
Test: atest BluetoothInstrumentationTests
Tag: #feature
Change-Id: Iddae5dc450dd83071bdb62841d28667dddc48474
parent 0a5dbb60
Loading
Loading
Loading
Loading
+16 −1
Original line number Diff line number Diff line
@@ -3271,6 +3271,14 @@ public class AdapterService extends Service {
            return;
        }

        if ("set-test-mode".equals(args[0])) {
            final boolean testModeEnabled = "enabled".equalsIgnoreCase(args[1]);
            for (ProfileService profile : mRunningProfiles) {
                profile.setTestModeEnabled(testModeEnabled);
            }
            return;
        }

        verboseLog("dumpsys arguments, check for protobuf output: " + TextUtils.join(" ", args));
        if (args[0].equals("--proto-bin")) {
            dumpMetrics(fd);
@@ -3494,6 +3502,12 @@ public class AdapterService extends Service {
        private static final String LOCATION_DENYLIST_ADVERTISING_DATA =
                "location_denylist_advertising_data";

        /**
         * Default denylist which matches Eddystone and iBeacon payloads.
         */
        private static final String DEFAULT_LOCATION_DENYLIST_ADVERTISING_DATA =
                "⊆0016AAFE/00FFFFFF,⊆00FF4C0002/00FFFFFFFF";

        public void start() {
            DeviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_BLUETOOTH,
                    BackgroundThread.getExecutor(), this);
@@ -3510,7 +3524,8 @@ public class AdapterService extends Service {
                mLocationDenylistMac = BytesMatcher
                        .decode(properties.getString(LOCATION_DENYLIST_MAC, null));
                mLocationDenylistAdvertisingData = BytesMatcher
                        .decode(properties.getString(LOCATION_DENYLIST_ADVERTISING_DATA, null));
                        .decode(properties.getString(LOCATION_DENYLIST_ADVERTISING_DATA,
                                DEFAULT_LOCATION_DENYLIST_ADVERTISING_DATA));
            }
        }
    }
+12 −0
Original line number Diff line number Diff line
@@ -60,6 +60,7 @@ public abstract class ProfileService extends Service {
    private AdapterService mAdapterService;
    private BroadcastReceiver mUserSwitchedReceiver;
    private boolean mProfileStarted = false;
    private boolean mTestModeEnabled = false;

    public String getName() {
        return getClass().getSimpleName();
@@ -69,6 +70,10 @@ public abstract class ProfileService extends Service {
        return mProfileStarted;
    }

    protected boolean isTestModeEnabled() {
        return mTestModeEnabled;
    }

    /**
     * Called in {@link #onCreate()} to init binder interface for this profile service
     *
@@ -110,6 +115,13 @@ public abstract class ProfileService extends Service {
     */
    protected void setUserUnlocked(int userId) {}

    /**
     * @param testEnabled if the profile should enter or exit a testing mode
     */
    protected void setTestModeEnabled(boolean testModeEnabled) {
        mTestModeEnabled = testModeEnabled;
    }

    protected ProfileService() {
        mName = getName();
    }
+60 −0
Original line number Diff line number Diff line
@@ -50,7 +50,9 @@ import android.content.Context;
import android.content.Intent;
import android.net.MacAddress;
import android.os.Binder;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.os.ParcelUuid;
import android.os.RemoteException;
import android.os.ServiceManager;
@@ -58,6 +60,7 @@ import android.os.SystemClock;
import android.os.UserHandle;
import android.os.WorkSource;
import android.provider.Settings;
import android.text.format.DateUtils;
import android.util.Log;

import com.android.bluetooth.BluetoothMetricsProto;
@@ -68,6 +71,8 @@ import com.android.bluetooth.btservice.AdapterService;
import com.android.bluetooth.btservice.ProfileService;
import com.android.bluetooth.util.NumberUtils;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.os.BackgroundThread;
import com.android.internal.util.HexDump;

import java.util.ArrayDeque;
import java.util.ArrayList;
@@ -124,6 +129,17 @@ public class GattService extends ProfileService {
    private static final UUID FIDO_SERVICE_UUID =
            UUID.fromString("0000FFFD-0000-1000-8000-00805F9B34FB"); // U2F

    /**
     * Example raw beacons captured from a Blue Charm BC011
     */
    private static final String[] TEST_MODE_BEACONS = new String[] {
            "020106",
            "0201060303AAFE1716AAFE10EE01626C7565636861726D626561636F6E730009168020691E0EFE13551109426C7565436861726D5F313639363835000000",
            "0201060303AAFE1716AAFE00EE626C7565636861726D31000000000001000009168020691E0EFE13551109426C7565436861726D5F313639363835000000",
            "0201060303AAFE1116AAFE20000BF017000008874803FB93540916802069080EFE13551109426C7565436861726D5F313639363835000000000000000000",
            "0201061AFF4C000215426C7565436861726D426561636F6E730EFE1355C509168020691E0EFE13551109426C7565436861726D5F31363936383500000000",
    };

    /**
     * Keep the arguments passed in for the PendingIntent.
     */
@@ -191,6 +207,7 @@ public class GattService extends ProfileService {
    private AppOpsManager mAppOps;
    private ICompanionDeviceManager mCompanionManager;
    private String mExposureNotificationPackage;
    private Handler mTestModeHandler;

    /**
     */
@@ -293,6 +310,31 @@ public class GattService extends ProfileService {
        }
    }

    @Override
    protected void setTestModeEnabled(boolean testModeEnabled) {
        super.setTestModeEnabled(testModeEnabled);

        // While test mode is enabled, pretend as if the underlying stack
        // discovered a specific set of well-known beacons every second
        if (testModeEnabled) {
            mTestModeHandler = new Handler(BackgroundThread.get().getLooper()) {
                public void handleMessage(Message msg) {
                    for (String test : TEST_MODE_BEACONS) {
                        onScanResultInternal(0x1b, 0x1, "DD:34:02:05:5C:4D", 1, 0, 0xff, 127, -54,
                                0x0, HexDump.hexStringToByteArray(test));
                    }

                    final Handler handler = mTestModeHandler;
                    if (handler != null) {
                        handler.sendEmptyMessageDelayed(0, DateUtils.SECOND_IN_MILLIS);
                    }
                }
            };
            mTestModeHandler.sendEmptyMessageDelayed(0, DateUtils.SECOND_IN_MILLIS);
        } else {
            mTestModeHandler = null;
        }
    }

    /**
     * Get the current instance of {@link GattService}
@@ -1042,6 +1084,16 @@ public class GattService extends ProfileService {
    void onScanResult(int eventType, int addressType, String address, int primaryPhy,
            int secondaryPhy, int advertisingSid, int txPower, int rssi, int periodicAdvInt,
            byte[] advData) {
        // When in testing mode, ignore all real-world events
        if (isTestModeEnabled()) return;

        onScanResultInternal(eventType, addressType, address, primaryPhy, secondaryPhy,
                advertisingSid, txPower, rssi, periodicAdvInt, advData);
    }

    void onScanResultInternal(int eventType, int addressType, String address, int primaryPhy,
            int secondaryPhy, int advertisingSid, int txPower, int rssi, int periodicAdvInt,
            byte[] advData) {
        if (VDBG) {
            Log.d(TAG, "onScanResult() - eventType=0x" + Integer.toHexString(eventType)
                    + ", addressType=" + addressType + ", address=" + address + ", primaryPhy="
@@ -1679,6 +1731,14 @@ public class GattService extends ProfileService {

    void onBatchScanReports(int status, int scannerId, int reportType, int numRecords,
            byte[] recordData) throws RemoteException {
        // When in testing mode, ignore all real-world events
        if (isTestModeEnabled()) return;

        onBatchScanReportsInternal(status, scannerId, reportType, numRecords, recordData);
    }

    void onBatchScanReportsInternal(int status, int scannerId, int reportType, int numRecords,
            byte[] recordData) throws RemoteException {
        if (DBG) {
            Log.d(TAG, "onBatchScanReports() - scannerId=" + scannerId + ", status=" + status
                    + ", reportType=" + reportType + ", numRecords=" + numRecords);