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

Commit 101ef2ce authored by TreeHugger Robot's avatar TreeHugger Robot Committed by Android (Google) Code Review
Browse files

Merge changes from topic "cdm-role-grant"

* changes:
  [5/X] Introduce BleCompanionDeviceScanner
  Grant CDM roles when association is created
parents f2b01df8 c1215eb0
Loading
Loading
Loading
Loading
+8 −3
Original line number Diff line number Diff line
@@ -254,9 +254,14 @@ class AssociationRequestsProcessor {
    private AssociationInfo createAssociationAndNotifyApplication(
            @NonNull AssociationRequest request, @NonNull String packageName, @UserIdInt int userId,
            @Nullable MacAddress macAddress, @NonNull IAssociationRequestCallback callback) {
        final AssociationInfo association = mService.createAssociation(userId, packageName,
                macAddress, request.getDisplayName(), request.getDeviceProfile(),
                request.isSelfManaged());
        final AssociationInfo association;
        final long callingIdentity = Binder.clearCallingIdentity();
        try {
            association = mService.createAssociation(userId, packageName, macAddress,
                    request.getDisplayName(), request.getDeviceProfile(), request.isSelfManaged());
        } finally {
            Binder.restoreCallingIdentity(callingIdentity);
        }

        try {
            callback.onAssociationCreated(association);
+8 −20
Original line number Diff line number Diff line
@@ -787,6 +787,12 @@ public class CompanionDeviceManagerService extends SystemService
        Slog.i(LOG_TAG, "New CDM association created=" + association);
        mAssociationStore.addAssociation(association);

        // If the "Device Profile" is specified, make the companion application a holder of the
        // corresponding role.
        if (deviceProfile != null) {
            addRoleHolderForAssociation(getContext(), association);
        }

        updateSpecialAccessPermissionForAssociatedPackage(association);

        return association;
@@ -930,16 +936,10 @@ public class CompanionDeviceManagerService extends SystemService

        exemptFromAutoRevoke(packageInfo.packageName, packageInfo.applicationInfo.uid);

        if (!association.isSelfManaged()) {
            if (mCurrentlyConnectedDevices.contains(association.getDeviceMacAddressAsString())) {
                addRoleHolderForAssociation(getContext(), association);
            }

        if (association.isNotifyOnDeviceNearby()) {
            restartBleScan();
        }
    }
    }

    private void exemptFromAutoRevoke(String packageName, int uid) {
        try {
@@ -983,19 +983,7 @@ public class CompanionDeviceManagerService extends SystemService

    void onDeviceConnected(String address) {
        Slog.d(LOG_TAG, "onDeviceConnected(address = " + address + ")");

        mCurrentlyConnectedDevices.add(address);

        for (AssociationInfo association : mAssociationStore.getAssociationsByAddress(address)) {
            if (association.getDeviceProfile() != null) {
                Slog.i(LOG_TAG, "Granting role " + association.getDeviceProfile()
                        + " to " + association.getPackageName()
                        + " due to device connected: " + association.getDeviceMacAddress());

                addRoleHolderForAssociation(getContext(), association);
            }
        }

        onDeviceNearby(address);
    }

+1 −9
Original line number Diff line number Diff line
@@ -73,20 +73,12 @@ class CompanionDeviceShellCommand extends android.os.ShellCommand {
                }
                break;

                case "simulate_connect": {
                    mService.onDeviceConnected(getNextArgRequired());
                }
                break;

                case "simulate_disconnect": {
                    mService.onDeviceDisconnected(getNextArgRequired());
                }
                break;
                case "clear-association-memory-cache": {
                    mService.persistState();
                    mService.loadAssociationsFromDisk();
                }
                break;

                default:
                    return handleDefaultCommands(cmd);
            }
+436 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2022 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.server.companion.presence;

import static android.bluetooth.BluetoothAdapter.ACTION_BLE_STATE_CHANGED;
import static android.bluetooth.BluetoothAdapter.ACTION_STATE_CHANGED;
import static android.bluetooth.BluetoothAdapter.EXTRA_PREVIOUS_STATE;
import static android.bluetooth.BluetoothAdapter.EXTRA_STATE;
import static android.bluetooth.BluetoothAdapter.STATE_BLE_ON;
import static android.bluetooth.BluetoothAdapter.STATE_ON;
import static android.bluetooth.BluetoothAdapter.nameForState;
import static android.bluetooth.le.ScanCallback.SCAN_FAILED_ALREADY_STARTED;
import static android.bluetooth.le.ScanCallback.SCAN_FAILED_APPLICATION_REGISTRATION_FAILED;
import static android.bluetooth.le.ScanCallback.SCAN_FAILED_FEATURE_UNSUPPORTED;
import static android.bluetooth.le.ScanCallback.SCAN_FAILED_INTERNAL_ERROR;
import static android.bluetooth.le.ScanCallback.SCAN_FAILED_OUT_OF_HARDWARE_RESOURCES;
import static android.bluetooth.le.ScanCallback.SCAN_FAILED_SCANNING_TOO_FREQUENTLY;
import static android.bluetooth.le.ScanSettings.CALLBACK_TYPE_ALL_MATCHES;
import static android.bluetooth.le.ScanSettings.CALLBACK_TYPE_FIRST_MATCH;
import static android.bluetooth.le.ScanSettings.CALLBACK_TYPE_MATCH_LOST;
import static android.bluetooth.le.ScanSettings.SCAN_MODE_LOW_POWER;

import static com.android.server.companion.presence.Utils.btDeviceToString;

import static java.util.Objects.requireNonNull;

import android.annotation.MainThread;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SuppressLint;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.le.BluetoothLeScanner;
import android.bluetooth.le.ScanCallback;
import android.bluetooth.le.ScanFilter;
import android.bluetooth.le.ScanResult;
import android.bluetooth.le.ScanSettings;
import android.companion.AssociationInfo;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.util.Log;
import android.util.Slog;

import com.android.server.companion.AssociationStore;
import com.android.server.companion.AssociationStore.ChangeType;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

@SuppressLint("LongLogTag")
class BleCompanionDeviceScanner implements AssociationStore.OnChangeListener {
    private static final boolean DEBUG = false;
    private static final String TAG = "CompanionDevice_PresenceMonitor_BLE";

    /**
     * When using {@link ScanSettings#SCAN_MODE_LOW_POWER}, it usually takes from 20 seconds to
     * 2 minutes for the BLE scanner to find advertisements sent from the same device.
     * On the other hand, {@link android.bluetooth.BluetoothAdapter.LeScanCallback} will report
     * {@link ScanSettings#CALLBACK_TYPE_MATCH_LOST MATCH_LOST} 10 sec after it finds the
     * advertisement for the first time (add reports
     * {@link ScanSettings#CALLBACK_TYPE_FIRST_MATCH FIRST_MATCH}).
     * To avoid constantly reporting {@link Callback#onBleCompanionDeviceFound(int) onDeviceFound()}
     * and {@link Callback#onBleCompanionDeviceLost(int) onDeviceLost()} (while the device is
     * actually present) to its clients, {@link BleCompanionDeviceScanner}, will add 1-minute delay
     * after it receives {@link ScanSettings#CALLBACK_TYPE_MATCH_LOST MATCH_LOST}.
     */
    private static final int NOTIFY_DEVICE_LOST_DELAY = 2 * 60 * 1000; // 2 Min.

    interface Callback {
        void onBleCompanionDeviceFound(int associationId);

        void onBleCompanionDeviceLost(int associationId);
    }

    private final @NonNull AssociationStore mAssociationStore;
    private final @NonNull Callback mCallback;
    private final @NonNull MainThreadHandler mMainThreadHandler;

    // Non-null after init().
    private @Nullable BluetoothAdapter mBtAdapter;
    // Non-null after init() and when BLE is available. Otherwise - null.
    private @Nullable BluetoothLeScanner mBleScanner;
    // Only accessed from the Main thread.
    private boolean mScanning = false;

    BleCompanionDeviceScanner(
            @NonNull AssociationStore associationStore, @NonNull Callback callback) {
        mAssociationStore = associationStore;
        mCallback = callback;
        mMainThreadHandler = new MainThreadHandler();
    }

    @MainThread
    void init(@NonNull Context context, @NonNull BluetoothAdapter btAdapter) {
        if (DEBUG) Log.i(TAG, "init()");

        if (mBtAdapter != null) {
            throw new IllegalStateException(getClass().getSimpleName() + " is already initialized");
        }
        mBtAdapter = requireNonNull(btAdapter);

        checkBleState();
        registerBluetoothStateBroadcastReceiver(context);

        mAssociationStore.registerListener(this);
    }

    @MainThread
    final void restartScan() {
        enforceInitialized();

        if (DEBUG) Log.i(TAG , "restartScan()");
        if (mBleScanner == null) {
            if (DEBUG) Log.d(TAG, "  > BLE is not available");
            return;
        }

        stopScanIfNeeded();
        startScan();
    }

    @Override
    public void onAssociationChanged(@ChangeType int changeType, AssociationInfo association) {
        // Simply restart scanning.
        if (Looper.getMainLooper().isCurrentThread()) {
            restartScan();
        } else {
            mMainThreadHandler.post(this::restartScan);
        }
    }

    @MainThread
    private void checkBleState() {
        enforceInitialized();

        final boolean bleAvailable = isBleAvailable();
        if (DEBUG) {
            Log.i(TAG, "checkBleState() bleAvailable=" + bleAvailable);
        }
        if ((bleAvailable && mBleScanner != null) || (!bleAvailable && mBleScanner == null)) {
            // Nothing changed.
            if (DEBUG) Log.i(TAG, "  > BLE status did not change");
            return;
        }

        if (bleAvailable) {
            mBleScanner = mBtAdapter.getBluetoothLeScanner();
            if (mBleScanner == null) {
                // Oops, that's a race condition. Can return.
                return;
            }
            if (DEBUG) Log.i(TAG, "  > BLE is now available");

            startScan();
        } else {
            if (DEBUG) Log.i(TAG, "  > BLE is now unavailable");

            stopScanIfNeeded();
            mBleScanner = null;
        }
    }

    /**
     * A duplicate of {@code BluetoothAdapter.getLeAccess()} method which has the package-private
     * access level, so it's not accessible from here.
     */
    private boolean isBleAvailable() {
        final int state = mBtAdapter.getLeState();
        if (DEBUG) Log.d(TAG, "getLeAccess() state=" + nameForBtState(state));
        return state == STATE_ON || state == STATE_BLE_ON;
    }

    @MainThread
    private void startScan() {
        enforceInitialized();
        // This method should not be called if scan is already in progress.
        if (mScanning) throw new IllegalStateException("Scan is already in progress.");
        // Neither should this method be called if the adapter is not available.
        if (mBleScanner == null) throw new IllegalStateException("BLE is not available.");

        if (DEBUG) Log.i(TAG, "startScan()");

        // Collect MAC addresses from all associations.
        final Set<String> macAddresses = new HashSet<>();
        for (AssociationInfo association : mAssociationStore.getAssociations()) {
            // Beware that BT stack does not consider low-case MAC addresses valid, while
            // MacAddress.toString() return a low-case String.
            final String macAddress = association.getDeviceMacAddressAsString();
            if (macAddress != null) {
                macAddresses.add(macAddress);
            }
        }
        if (macAddresses.isEmpty()) {
            if (DEBUG) Log.i(TAG, "  > there are no (associated) devices to Scan for.");
            return;
        } else {
            if (DEBUG) {
                Log.d(TAG, "  > addresses=(n=" + macAddresses.size() + ")"
                        + "[" + String.join(", ", macAddresses) + "]");
            }
        }

        final List<ScanFilter> filters = new ArrayList<>(macAddresses.size());
        for (String macAddress : macAddresses) {
            final ScanFilter filter = new ScanFilter.Builder()
                    .setDeviceAddress(macAddress)
                    .build();
            filters.add(filter);
        }

        mBleScanner.startScan(filters, SCAN_SETTINGS, mScanCallback);
        mScanning = true;
    }

    private void stopScanIfNeeded() {
        enforceInitialized();

        if (DEBUG) Log.i(TAG, "stopScan()");
        if (!mScanning) {
            Log.d(TAG, "  > not scanning.");
            return;
        }

        mBleScanner.stopScan(mScanCallback);
        mScanning = false;
    }

    @MainThread
    private void notifyDeviceFound(@NonNull BluetoothDevice device) {
        if (DEBUG) Log.i(TAG, "notifyDevice_Found()" + btDeviceToString(device));

        final List<AssociationInfo> associations =
                mAssociationStore.getAssociationsByAddress(device.getAddress());
        if (DEBUG) Log.d(TAG, "  > associations=" + Arrays.toString(associations.toArray()));

        for (AssociationInfo association : associations) {
            mCallback.onBleCompanionDeviceFound(association.getId());
        }
    }

    @MainThread
    private void notifyDeviceLost(@NonNull BluetoothDevice device) {
        if (DEBUG) Log.i(TAG, "notifyDevice_Lost()" + btDeviceToString(device));

        final List<AssociationInfo> associations =
                mAssociationStore.getAssociationsByAddress(device.getAddress());
        if (DEBUG) Log.d(TAG, "  > associations=" + Arrays.toString(associations.toArray()));

        for (AssociationInfo association : associations) {
            mCallback.onBleCompanionDeviceLost(association.getId());
        }
    }

    private void registerBluetoothStateBroadcastReceiver(Context context) {
        final BroadcastReceiver receiver = new BroadcastReceiver() {
            @Override
            public void onReceive(Context context, Intent intent) {
                final int prevState = intent.getIntExtra(EXTRA_PREVIOUS_STATE, -1);
                final int state = intent.getIntExtra(EXTRA_STATE, -1);

                if (DEBUG) {
                    // The action is either STATE_CHANGED or BLE_STATE_CHANGED.
                    final String action =
                            intent.getAction().replace("android.bluetooth.adapter.", "bt.");
                    Log.d(TAG, "on(Broadcast)Receive() " + action + ": "
                            + nameForBtState(prevState) + "->" + nameForBtState(state));
                }

                checkBleState();
            }
        };

        final IntentFilter filter = new IntentFilter();
        filter.addAction(ACTION_STATE_CHANGED);
        filter.addAction(ACTION_BLE_STATE_CHANGED);

        context.registerReceiver(receiver, filter);
    }

    private void enforceInitialized() {
        if (mBtAdapter != null) return;
        throw new IllegalStateException(getClass().getSimpleName() + " is not initialized");
    }

    private final ScanCallback mScanCallback = new ScanCallback() {
        @MainThread
        @Override
        public void onScanResult(int callbackType, ScanResult result) {
            final BluetoothDevice device = result.getDevice();

            if (DEBUG) {
                Log.d(TAG, "onScanResult() " + nameForBleScanCallbackType(callbackType)
                        + " device=" + btDeviceToString(device));
                Log.v(TAG, "  > scanResult=" + result);

                final List<AssociationInfo> associations =
                        mAssociationStore.getAssociationsByAddress(device.getAddress());
                Log.v(TAG, "  > associations=" + Arrays.toString(associations.toArray()));
            }

            switch (callbackType) {
                case CALLBACK_TYPE_FIRST_MATCH:
                    if (mMainThreadHandler.hasNotifyDeviceLostMessages(device)) {
                        mMainThreadHandler.removeNotifyDeviceLostMessages(device);
                        return;
                    }

                    notifyDeviceFound(device);
                    break;

                case CALLBACK_TYPE_MATCH_LOST:
                    mMainThreadHandler.sendNotifyDeviceLostDelayedMessage(device);
                    break;

                default:
                    Slog.wtf(TAG, "Unexpected callback "
                            + nameForBleScanCallbackType(callbackType));
                    break;
            }
        }

        @MainThread
        @Override
        public void onScanFailed(int errorCode) {
            if (DEBUG) Log.w(TAG, "onScanFailed() " + nameForBleScanErrorCode(errorCode));
            mScanning = false;
        }
    };

    @SuppressLint("HandlerLeak")
    private class MainThreadHandler extends Handler {
        private static final int NOTIFY_DEVICE_LOST = 1;

        MainThreadHandler() {
            super(Looper.getMainLooper());
        }

        @Override
        public void handleMessage(@NonNull Message message) {
            if  (message.what != NOTIFY_DEVICE_LOST) return;

            final BluetoothDevice device = (BluetoothDevice) message.obj;
            notifyDeviceLost(device);
        }

        void sendNotifyDeviceLostDelayedMessage(BluetoothDevice device) {
            final Message message = obtainMessage(NOTIFY_DEVICE_LOST, device);
            sendMessageDelayed(message, NOTIFY_DEVICE_LOST_DELAY);
        }

        boolean hasNotifyDeviceLostMessages(BluetoothDevice device) {
            return hasEqualMessages(NOTIFY_DEVICE_LOST, device);
        }

        void removeNotifyDeviceLostMessages(BluetoothDevice device) {
            removeEqualMessages(NOTIFY_DEVICE_LOST, device);
        }
    }

    private static String nameForBtState(int state) {
        return nameForState(state) + "(" + state + ")";
    }

    private static String nameForBleScanCallbackType(int callbackType) {
        final String name;
        switch (callbackType) {
            case CALLBACK_TYPE_ALL_MATCHES:
                name = "ALL_MATCHES";
                break;
            case CALLBACK_TYPE_FIRST_MATCH:
                name = "FIRST_MATCH";
                break;
            case CALLBACK_TYPE_MATCH_LOST:
                name = "MATCH_LOST";
                break;
            default:
                name = "Unknown";
        }
        return name + "(" + callbackType + ")";
    }

    private static String nameForBleScanErrorCode(int errorCode) {
        final String name;
        switch (errorCode) {
            case SCAN_FAILED_ALREADY_STARTED:
                name = "ALREADY_STARTED";
                break;
            case SCAN_FAILED_APPLICATION_REGISTRATION_FAILED:
                name = "APPLICATION_REGISTRATION_FAILED";
                break;
            case SCAN_FAILED_INTERNAL_ERROR:
                name = "INTERNAL_ERROR";
                break;
            case SCAN_FAILED_FEATURE_UNSUPPORTED:
                name = "FEATURE_UNSUPPORTED";
                break;
            case SCAN_FAILED_OUT_OF_HARDWARE_RESOURCES:
                name = "OUT_OF_HARDWARE_RESOURCES";
                break;
            case SCAN_FAILED_SCANNING_TOO_FREQUENTLY:
                name = "SCANNING_TOO_FREQUENTLY";
                break;
            default:
                name = "Unknown";
        }
        return name + "(" + errorCode + ")";
    }

    private static final ScanSettings SCAN_SETTINGS = new ScanSettings.Builder()
            .setCallbackType(CALLBACK_TYPE_FIRST_MATCH | CALLBACK_TYPE_MATCH_LOST)
            .setScanMode(SCAN_MODE_LOW_POWER)
            .build();
}
+15 −24
Original line number Diff line number Diff line
@@ -16,6 +16,8 @@

package com.android.server.companion.presence;

import static com.android.server.companion.presence.Utils.btDeviceToString;

import android.annotation.NonNull;
import android.annotation.SuppressLint;
import android.bluetooth.BluetoothAdapter;
@@ -71,11 +73,11 @@ class BluetoothCompanionDeviceConnectionListener
     */
    @Override
    public void onDeviceConnected(@NonNull BluetoothDevice device) {
        if (DEBUG) Log.i(TAG, "onDevice_Connected() " + toString(device));
        if (DEBUG) Log.i(TAG, "onDevice_Connected() " + btDeviceToString(device));

        final MacAddress macAddress = MacAddress.fromString(device.getAddress());
        if (mAllConnectedDevices.put(macAddress, device) != null) {
            if (DEBUG) Log.w(TAG, "Device " + toString(device) + " is already connected.");
            if (DEBUG) Log.w(TAG, "Device " + btDeviceToString(device) + " is already connected.");
            return;
        }

@@ -91,13 +93,15 @@ class BluetoothCompanionDeviceConnectionListener
    public void onDeviceDisconnected(@NonNull BluetoothDevice device,
            @DisconnectReason int reason) {
        if (DEBUG) {
            Log.i(TAG, "onDevice_Disconnected() " + toString(device));
            Log.i(TAG, "onDevice_Disconnected() " + btDeviceToString(device));
            Log.d(TAG, "  reason=" + disconnectReasonText(reason));
        }

        final MacAddress macAddress = MacAddress.fromString(device.getAddress());
        if (mAllConnectedDevices.remove(macAddress) == null) {
            if (DEBUG) Log.w(TAG, "The device wasn't tracked as connected " + toString(device));
            if (DEBUG) {
                Log.w(TAG, "The device wasn't tracked as connected " + btDeviceToString(device));
            }
            return;
        }

@@ -109,7 +113,7 @@ class BluetoothCompanionDeviceConnectionListener
                mAssociationStore.getAssociationsByAddress(device.getAddress());

        if (DEBUG) {
            Log.d(TAG, "onDevice_ConnectivityChanged() " + toString(device)
            Log.d(TAG, "onDevice_ConnectivityChanged() " + btDeviceToString(device)
                    + " connected=" + connected);
            if (associations.isEmpty()) {
                Log.d(TAG, "  > No CDM associations");
@@ -137,6 +141,12 @@ class BluetoothCompanionDeviceConnectionListener
        }
    }

    @Override
    public void onAssociationRemoved(AssociationInfo association) {
        // Intentionally do nothing: CompanionDevicePresenceMonitor will do all the bookkeeping
        // required.
    }

    @Override
    public void onAssociationUpdated(AssociationInfo association, boolean addressChanged) {
        if (DEBUG) {
@@ -153,23 +163,4 @@ class BluetoothCompanionDeviceConnectionListener
        // This will be implemented when CDM support updating addresses.
        throw new IllegalArgumentException("Address changes are not supported.");
    }

    private static String toString(@NonNull BluetoothDevice btDevice) {
        final StringBuilder sb = new StringBuilder(btDevice.getAddress());

        sb.append(" [name=");
        final String name = btDevice.getName();
        if (name != null) {
            sb.append('\'').append(name).append('\'');
        } else {
            sb.append("null");
        }

        final String alias = btDevice.getAlias();
        if (alias != null) {
            sb.append(", alias='").append(alias).append("'");
        }

        return sb.append(']').toString();
    }
}
Loading