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

Commit c9d72dcc authored by Jeff Sharkey's avatar Jeff Sharkey Committed by Android (Google) Code Review
Browse files

Merge changes from topic "mar24" into sc-dev

* changes:
  Rudimentary automated testing for LE beacons.
  Revision of denylist filtering.
parents 17443bd0 78005618
Loading
Loading
Loading
Loading
+96 −1
Original line number Diff line number Diff line
@@ -28,6 +28,7 @@ import static com.android.bluetooth.Utils.enforceLocalMacAddressPermission;
import static com.android.bluetooth.Utils.hasBluetoothPrivilegedPermission;
import static com.android.bluetooth.Utils.isPackageNameAccurate;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.ActivityManager;
import android.app.AlarmManager;
@@ -63,6 +64,7 @@ import android.os.AsyncTask;
import android.os.BatteryStats;
import android.os.Binder;
import android.os.Bundle;
import android.os.BytesMatcher;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
@@ -109,8 +111,10 @@ import com.android.bluetooth.sap.SapService;
import com.android.bluetooth.sdp.SdpManager;
import com.android.bluetooth.telephony.BluetoothInCallService;
import com.android.internal.R;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.app.IBatteryStats;
import com.android.internal.os.BackgroundThread;
import com.android.internal.util.ArrayUtils;

import com.google.protobuf.InvalidProtocolBufferException;
@@ -125,6 +129,8 @@ import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Predicate;
import java.util.regex.Pattern;

public class AdapterService extends Service {
    private static final String TAG = "BluetoothAdapterService";
@@ -478,6 +484,7 @@ public class AdapterService extends Service {
    public void onCreate() {
        super.onCreate();
        debugLog("onCreate()");
        mDeviceConfigListener.start();
        mRemoteDevices = new RemoteDevices(this, Looper.getMainLooper());
        mRemoteDevices.init();
        clearDiscoveringPackages();
@@ -2421,7 +2428,8 @@ public class AdapterService extends Service {
        }

        synchronized (mDiscoveringPackages) {
            mDiscoveringPackages.add(new DiscoveringPackage(callingPackage, permission));
            mDiscoveringPackages.add(
                    new DiscoveringPackage(callingPackage, permission, hasDisavowedLocation));
        }
        return startDiscoveryNative();
    }
@@ -3263,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);
@@ -3435,6 +3451,85 @@ public class AdapterService extends Service {
        return initFlags.toArray(new String[0]);
    }

    private final Object mDeviceConfigLock = new Object();

    /**
     * Predicate that can be applied to names to determine if a device is
     * well-known to be used for physical location.
     */
    @GuardedBy("mDeviceConfigLock")
    private Predicate<String> mLocationDenylistName = (v) -> false;

    /**
     * Predicate that can be applied to MAC addresses to determine if a device
     * is well-known to be used for physical location.
     */
    @GuardedBy("mDeviceConfigLock")
    private Predicate<byte[]> mLocationDenylistMac = (v) -> false;

    /**
     * Predicate that can be applied to Advertising Data payloads to determine
     * if a device is well-known to be used for physical location.
     */
    @GuardedBy("mDeviceConfigLock")
    private Predicate<byte[]> mLocationDenylistAdvertisingData = (v) -> false;

    public @NonNull Predicate<String> getLocationDenylistName() {
        synchronized (mDeviceConfigLock) {
            return mLocationDenylistName;
        }
    }

    public @NonNull Predicate<byte[]> getLocationDenylistMac() {
        synchronized (mDeviceConfigLock) {
            return mLocationDenylistMac;
        }
    }

    public @NonNull Predicate<byte[]> getLocationDenylistAdvertisingData() {
        synchronized (mDeviceConfigLock) {
            return mLocationDenylistAdvertisingData;
        }
    }

    private final DeviceConfigListener mDeviceConfigListener = new DeviceConfigListener();

    private class DeviceConfigListener implements DeviceConfig.OnPropertiesChangedListener {
        private static final String LOCATION_DENYLIST_NAME =
                "location_denylist_name";
        private static final String LOCATION_DENYLIST_MAC =
                "location_denylist_mac";
        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);
            onPropertiesChanged(DeviceConfig.getProperties(DeviceConfig.NAMESPACE_BLUETOOTH));
        }

        @Override
        public void onPropertiesChanged(DeviceConfig.Properties properties) {
            synchronized (mDeviceConfigLock) {
                final String name = properties.getString(LOCATION_DENYLIST_NAME, null);
                mLocationDenylistName = !TextUtils.isEmpty(name)
                        ? Pattern.compile(name).asPredicate()
                        : (v) -> false;
                mLocationDenylistMac = BytesMatcher
                        .decode(properties.getString(LOCATION_DENYLIST_MAC, null));
                mLocationDenylistAdvertisingData = BytesMatcher
                        .decode(properties.getString(LOCATION_DENYLIST_ADVERTISING_DATA,
                                DEFAULT_LOCATION_DENYLIST_ADVERTISING_DATA));
            }
        }
    }

    /**
     *  Obfuscate Bluetooth MAC address into a PII free ID string
     *
+13 −8
Original line number Diff line number Diff line
@@ -16,25 +16,30 @@

package com.android.bluetooth.btservice;

import android.annotation.NonNull;
import android.annotation.Nullable;

final class DiscoveringPackage {
    private String mPackageName;
    private @NonNull String mPackageName;
    private @Nullable String mPermission;
    private boolean mHasDisavowedLocation;

    @Nullable
    private String mPermission;

    DiscoveringPackage(String packageName, String permission) {
    DiscoveringPackage(@NonNull String packageName, @Nullable String permission,
            boolean hasDisavowedLocation) {
        mPackageName = packageName;
        mPermission = permission;
        mHasDisavowedLocation = hasDisavowedLocation;
    }

    public String getPackageName() {
    public @NonNull String getPackageName() {
        return mPackageName;
    }

    @Nullable
    public String getPermission() {
    public @Nullable String getPermission() {
        return mPermission;
    }

    public boolean hasDisavowedLocation() {
        return mHasDisavowedLocation;
    }
}
+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();
    }
+26 −0
Original line number Diff line number Diff line
@@ -29,6 +29,7 @@ import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.MacAddress;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
@@ -48,6 +49,7 @@ import java.util.HashSet;
import java.util.LinkedList;
import java.util.Queue;
import java.util.Set;
import java.util.function.Predicate;

final class RemoteDevices {
    private static final boolean DBG = false;
@@ -113,6 +115,24 @@ final class RemoteDevices {
        }
    };

    /**
     * Predicate that tests if the given {@link BluetoothDevice} is well-known
     * to be used for physical location.
     */
    private final Predicate<BluetoothDevice> mLocationDenylistPredicate = (device) -> {
        final MacAddress parsedAddress = MacAddress.fromString(device.getAddress());
        if (sAdapterService.getLocationDenylistMac().test(parsedAddress.toByteArray())) {
            Log.v(TAG, "Skipping device matching denylist: " + parsedAddress);
            return true;
        }
        final String name = device.getName();
        if (sAdapterService.getLocationDenylistName().test(name)) {
            Log.v(TAG, "Skipping name matching denylist: " + name);
            return true;
        }
        return false;
    };

    RemoteDevices(AdapterService service, Looper looper) {
        sAdapter = BluetoothAdapter.getDefaultAdapter();
        sAdapterService = service;
@@ -602,6 +622,12 @@ final class RemoteDevices {
        final ArrayList<DiscoveringPackage> packages = sAdapterService.getDiscoveringPackages();
        synchronized (packages) {
            for (DiscoveringPackage pkg : packages) {
                if (pkg.hasDisavowedLocation()) {
                    if (mLocationDenylistPredicate.test(device)) {
                        continue;
                    }
                }

                intent.setPackage(pkg.getPackageName());

                String[] perms;
+83 −55
Original line number Diff line number Diff line
@@ -50,16 +50,17 @@ import android.content.Context;
import android.content.Intent;
import android.net.MacAddress;
import android.os.Binder;
import android.os.BytesMatcher;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.os.ParcelUuid;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.SystemClock;
import android.os.UserHandle;
import android.os.WorkSource;
import android.provider.DeviceConfig;
import android.provider.Settings;
import android.text.format.DateUtils;
import android.util.Log;

import com.android.bluetooth.BluetoothMetricsProto;
@@ -69,9 +70,9 @@ import com.android.bluetooth.btservice.AbstractionLayer;
import com.android.bluetooth.btservice.AdapterService;
import com.android.bluetooth.btservice.ProfileService;
import com.android.bluetooth.util.NumberUtils;
import com.android.internal.annotations.GuardedBy;
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;
@@ -84,6 +85,7 @@ import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.function.Predicate;

/**
 * Provides Bluetooth Gatt profile, as a service in
@@ -127,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.
     */
@@ -194,47 +207,25 @@ public class GattService extends ProfileService {
    private AppOpsManager mAppOps;
    private ICompanionDeviceManager mCompanionManager;
    private String mExposureNotificationPackage;

    private final Object mDeviceConfigLock = new Object();

    /**
     * Matcher that can be applied to MAC addresses to determine if a
     * {@link BluetoothDevice} is well-known to be used for physical location.
     */
    @GuardedBy("mDeviceConfigLock")
    private BytesMatcher mLocationDenylistMac = new BytesMatcher();
    private Handler mTestModeHandler;

    /**
     * Matcher that can be applied to Advertising Data payloads to determine if
     * a {@link ScanRecord} is well-known to be used for physical location.
     */
    @GuardedBy("mDeviceConfigLock")
    private BytesMatcher mLocationDenylistAdvertisingData = new BytesMatcher();

    private final DeviceConfigListener mDeviceConfigListener = new DeviceConfigListener();

    private class DeviceConfigListener implements DeviceConfig.OnPropertiesChangedListener {
        private static final String LOCATION_DENYLIST_MAC =
                "location_denylist_mac";
        private static final String LOCATION_DENYLIST_ADVERTISING_DATA =
                "location_denylist_advertising_data";

        public void start() {
            DeviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_BLUETOOTH,
                    BackgroundThread.getExecutor(), this);
            onPropertiesChanged(DeviceConfig.getProperties(DeviceConfig.NAMESPACE_BLUETOOTH));
        }

        @Override
        public void onPropertiesChanged(DeviceConfig.Properties properties) {
            synchronized (mDeviceConfigLock) {
                mLocationDenylistMac = BytesMatcher
                        .decode(properties.getString(LOCATION_DENYLIST_MAC, null));
                mLocationDenylistAdvertisingData = BytesMatcher
                        .decode(properties.getString(LOCATION_DENYLIST_ADVERTISING_DATA, null));
            }
    private final Predicate<ScanResult> mLocationDenylistPredicate = (scanResult) -> {
        final AdapterService adapterService = AdapterService.getAdapterService();
        final MacAddress parsedAddress = MacAddress
                .fromString(scanResult.getDevice().getAddress());
        if (adapterService.getLocationDenylistMac().test(parsedAddress.toByteArray())) {
            Log.v(TAG, "Skipping device matching denylist: " + parsedAddress);
            return true;
        }
        final ScanRecord scanRecord = scanResult.getScanRecord();
        if (scanRecord.matchesAnyField(adapterService.getLocationDenylistAdvertisingData())) {
            Log.v(TAG, "Skipping data matching denylist: " + scanRecord);
            return true;
        }
        return false;
    };

    private static GattService sGattService;

@@ -261,8 +252,6 @@ public class GattService extends ProfileService {
        Settings.Global.putInt(
                getContentResolver(), "bluetooth_sanitized_exposure_notification_supported", 1);

        mDeviceConfigListener.start();

        initializeNative();
        mAdapter = BluetoothAdapter.getDefaultAdapter();
        mCompanionManager = ICompanionDeviceManager.Stub.asInterface(
@@ -321,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}
@@ -1070,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="
@@ -1104,25 +1128,17 @@ public class GattService extends ProfileService {
            }

            ScanRecord scanRecord = ScanRecord.parseFromBytes(scanRecordData);
            ScanResult result =
                    new ScanResult(device, eventType, primaryPhy, secondaryPhy, advertisingSid,
                            txPower, rssi, periodicAdvInt, scanRecord,
                            SystemClock.elapsedRealtimeNanos());

            if (client.hasDisavowedLocation) {
                synchronized (mDeviceConfigLock) {
                    final MacAddress parsedAddress = MacAddress.fromString(address);
                    if (mLocationDenylistMac.testMacAddress(parsedAddress)) {
                        Log.v(TAG, "Skipping device matching denylist: " + parsedAddress);
                        continue;
                    }
                    if (scanRecord.matchesAnyField(mLocationDenylistAdvertisingData)) {
                        Log.v(TAG, "Skipping data matching denylist: " + scanRecord);
                if (mLocationDenylistPredicate.test(result)) {
                    continue;
                }
            }
            }

            ScanResult result =
                    new ScanResult(device, eventType, primaryPhy, secondaryPhy, advertisingSid,
                            txPower, rssi, periodicAdvInt, scanRecord,
                            SystemClock.elapsedRealtimeNanos());
            boolean hasPermission = hasScanResultPermission(client);
            if (!hasPermission) {
                for (String associatedDevice : client.associatedDevices) {
@@ -1715,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);
@@ -1751,6 +1775,10 @@ public class GattService extends ProfileService {
                }
            }

            if (client.hasDisavowedLocation) {
                permittedResults.removeIf(mLocationDenylistPredicate);
            }

            if (app.callback != null) {
                app.callback.onBatchScanResults(permittedResults);
            } else {