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

Commit 165edbb1 authored by Sergey Nikolaienkov's avatar Sergey Nikolaienkov Committed by Android (Google) Code Review
Browse files

Merge changes If0ffc8d5,I2934747b

* changes:
  [4/X] Introduce BluetoothCompanionDeviceConnectionListener
  [3/X] Introduce CompanionApplicationController
parents e0e9d4ef a774ef6b
Loading
Loading
Loading
Loading
+317 −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;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SuppressLint;
import android.annotation.UserIdInt;
import android.companion.AssociationInfo;
import android.companion.CompanionDeviceService;
import android.content.ComponentName;
import android.content.Context;
import android.os.Handler;
import android.util.Log;
import android.util.SparseArray;

import com.android.internal.annotations.GuardedBy;
import com.android.internal.infra.PerUser;
import com.android.internal.util.CollectionUtils;

import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Manages communication with companion applications via
 * {@link android.companion.ICompanionDeviceService} interface, including "connecting" (binding) to
 * the services, maintaining the connection (the binding), and invoking callback methods such as
 * {@link CompanionDeviceService#onDeviceAppeared(AssociationInfo)} and
 * {@link CompanionDeviceService#onDeviceDisappeared(AssociationInfo)} in the application process.
 *
 * <p>
 * The following is the list of the APIs provided by {@link CompanionApplicationController} (to be
 * utilized by {@link CompanionDeviceManagerService}):
 * <ul>
 * <li> {@link #bindCompanionApplication(int, String)}
 * <li> {@link #unbindCompanionApplication(int, String)}
 * <li> {@link #notifyCompanionApplicationDeviceAppeared(AssociationInfo)}
 * <li> {@link #notifyCompanionApplicationDeviceDisappeared(AssociationInfo)}
 * <li> {@link #isCompanionApplicationBound(int, String)}
 * <li> {@link #isRebindingCompanionApplicationScheduled(int, String)}
 * </ul>
 *
 * @see CompanionDeviceService
 * @see android.companion.ICompanionDeviceService
 * @see CompanionDeviceServiceConnector
 */
@SuppressLint("LongLogTag")
class CompanionApplicationController {
    static final boolean DEBUG = false;
    private static final String TAG = "CompanionDevice_ApplicationController";

    private static final long REBIND_TIMEOUT = 10 * 1000; // 10 sec

    interface Callback {
        /**
         * @return {@code true} if should schedule rebinding.
         *         {@code false} if we do not need to rebind.
         */
        boolean onCompanionApplicationBindingDied(
                @UserIdInt int userId, @NonNull String packageName);

        /**
         * Callback after timeout for previously scheduled rebind has passed.
         */
        void onRebindCompanionApplicationTimeout(
                @UserIdInt int userId, @NonNull String packageName);
    }

    private final @NonNull Context mContext;
    private final @NonNull Callback mCallback;
    private final @NonNull CompanionServicesRegister mCompanionServicesRegister;
    @GuardedBy("mBoundCompanionApplications")
    private final @NonNull AndroidPackageMap<List<CompanionDeviceServiceConnector>>
            mBoundCompanionApplications;
    private final @NonNull AndroidPackageMap<Boolean> mScheduledForRebindingCompanionApplications;

    CompanionApplicationController(Context context, Callback callback) {
        mContext = context;
        mCallback = callback;
        mCompanionServicesRegister = new CompanionServicesRegister();
        mBoundCompanionApplications = new AndroidPackageMap<>();
        mScheduledForRebindingCompanionApplications = new AndroidPackageMap<>();
    }

    void onPackagesChanged(@UserIdInt int userId) {
        mCompanionServicesRegister.invalidate(userId);
    }

    void bindCompanionApplication(@UserIdInt int userId, @NonNull String packageName) {
        if (DEBUG) Log.i(TAG, "bind() u" + userId + "/" + packageName);

        final List<ComponentName> companionServices =
                mCompanionServicesRegister.forPackage(userId, packageName);
        final List<CompanionDeviceServiceConnector> serviceConnectors;

        synchronized (mBoundCompanionApplications) {
            if (mBoundCompanionApplications.containsValueForPackage(userId, packageName)) {
                if (DEBUG) Log.e(TAG, "u" + userId + "/" + packageName + " is ALREADY bound.");
                return;
            }

            serviceConnectors = CollectionUtils.map(companionServices, componentName ->
                            new CompanionDeviceServiceConnector(mContext, userId, componentName));
            mBoundCompanionApplications.setValueForPackage(userId, packageName, serviceConnectors);
        }

        // The first connector in the list is always the primary connector: set a listener to it.
        serviceConnectors.get(0).setListener(this::onPrimaryServiceBindingDied);

        // Now "bind" all the connectors: the primary one and the rest of them.
        for (CompanionDeviceServiceConnector serviceConnector : serviceConnectors) {
            serviceConnector.connect();
        }
    }

    void unbindCompanionApplication(@UserIdInt int userId, @NonNull String packageName) {
        if (DEBUG) Log.i(TAG, "unbind() u" + userId + "/" + packageName);

        final List<CompanionDeviceServiceConnector> serviceConnectors;
        synchronized (mBoundCompanionApplications) {
            serviceConnectors = mBoundCompanionApplications.removePackage(userId, packageName);
        }
        if (serviceConnectors == null) {
            if (DEBUG) Log.e(TAG, "u" + userId + "/" + packageName + " is NOT bound");
            return;
        }

        for (CompanionDeviceServiceConnector serviceConnector : serviceConnectors) {
            serviceConnector.postUnbind();
        }
    }

    boolean isCompanionApplicationBound(@UserIdInt int userId, @NonNull String packageName) {
        synchronized (mBoundCompanionApplications) {
            return mBoundCompanionApplications.containsValueForPackage(userId, packageName);
        }
    }

    private void scheduleRebinding(@UserIdInt int userId, @NonNull String packageName) {
        mScheduledForRebindingCompanionApplications.setValueForPackage(userId, packageName, true);

        Handler.getMain().postDelayed(() ->
                onRebindingCompanionApplicationTimeout(userId, packageName), REBIND_TIMEOUT);
    }

    boolean isRebindingCompanionApplicationScheduled(
            @UserIdInt int userId, @NonNull String packageName) {
        return mScheduledForRebindingCompanionApplications
                .containsValueForPackage(userId, packageName);
    }

    private void onRebindingCompanionApplicationTimeout(
            @UserIdInt int userId, @NonNull String packageName) {
        mScheduledForRebindingCompanionApplications.removePackage(userId, packageName);

        mCallback.onRebindCompanionApplicationTimeout(userId, packageName);
    }

    void notifyCompanionApplicationDeviceAppeared(AssociationInfo association) {
        final int userId = association.getUserId();
        final String packageName = association.getPackageName();
        if (DEBUG) {
            Log.i(TAG, "notifyDevice_Appeared() id=" + association.getId() + " u" + userId
                    + "/" + packageName);
        }

        final CompanionDeviceServiceConnector primaryServiceConnector =
                getPrimaryServiceConnector(userId, packageName);
        if (primaryServiceConnector == null) {
            if (DEBUG) Log.e(TAG, "u" + userId + "/" + packageName + " is NOT bound.");
            return;
        }

        primaryServiceConnector.postOnDeviceAppeared(association);
    }

    void notifyCompanionApplicationDeviceDisappeared(AssociationInfo association) {
        final int userId = association.getUserId();
        final String packageName = association.getPackageName();
        if (DEBUG) {
            Log.i(TAG, "notifyDevice_Disappeared() id=" + association.getId() + " u" + userId
                    + "/" + packageName);
        }

        final CompanionDeviceServiceConnector primaryServiceConnector =
                getPrimaryServiceConnector(userId, packageName);
        if (primaryServiceConnector == null) {
            if (DEBUG) Log.e(TAG, "u" + userId + "/" + packageName + " is NOT bound.");
            return;
        }

        primaryServiceConnector.postOnDeviceDisappeared(association);
    }

    private void onPrimaryServiceBindingDied(@UserIdInt int userId, @NonNull String packageName) {
        if (DEBUG) Log.i(TAG, "onPrimaryServiceBindingDied() u" + userId + "/" + packageName);

        // First: mark as NOT bound.
        synchronized (mBoundCompanionApplications) {
            mBoundCompanionApplications.removePackage(userId, packageName);
        }

        // Second: invoke callback, schedule rebinding if needed.
        final boolean shouldScheduleRebind =
                mCallback.onCompanionApplicationBindingDied(userId, packageName);
        if (shouldScheduleRebind) {
            scheduleRebinding(userId, packageName);
        }
    }

    private @Nullable CompanionDeviceServiceConnector getPrimaryServiceConnector(
            @UserIdInt int userId, @NonNull String packageName) {
        final List<CompanionDeviceServiceConnector> connectors;
        synchronized (mBoundCompanionApplications) {
            connectors = mBoundCompanionApplications.getValueForPackage(userId, packageName);
        }
        return connectors != null ? connectors.get(0) : null;
    }

    private class CompanionServicesRegister extends PerUser<Map<String, List<ComponentName>>> {
        @Override
        public synchronized @NonNull Map<String, List<ComponentName>> forUser(
                @UserIdInt int userId) {
            return super.forUser(userId);
        }

        synchronized @NonNull List<ComponentName> forPackage(
                @UserIdInt int userId, @NonNull String packageName) {
            return forUser(userId).getOrDefault(packageName, Collections.emptyList());
        }

        synchronized @NonNull ComponentName primaryForPackage(
                @UserIdInt int userId, @NonNull String packageName) {
            // The primary service is always at the head of the list.
            return forPackage(userId, packageName).get(0);
        }

        synchronized void invalidate(@UserIdInt int userId) {
            remove(userId);
        }

        @Override
        protected final @NonNull Map<String, List<ComponentName>> create(@UserIdInt int userId) {
            return PackageUtils.getCompanionServicesForUser(mContext, userId);
        }
    }

    /**
     * Associates an Android package (defined by userId + packageName) with a value of type T.
     */
    private static class AndroidPackageMap<T> extends SparseArray<Map<String, T>> {

        void setValueForPackage(
                @UserIdInt int userId, @NonNull String packageName, @NonNull T value) {
            Map<String, T> forUser = get(userId);
            if (forUser == null) {
                forUser = /* Map<String, T> */ new HashMap();
                put(userId, forUser);
            }

            forUser.put(packageName, value);
        }

        boolean containsValueForPackage(@UserIdInt int userId, @NonNull String packageName) {
            final Map<String, ?> forUser = get(userId);
            return forUser != null && forUser.containsKey(packageName);
        }

        T getValueForPackage(@UserIdInt int userId, @NonNull String packageName) {
            final Map<String, T> forUser = get(userId);
            return forUser != null ? forUser.get(packageName) : null;
        }

        T removePackage(@UserIdInt int userId, @NonNull String packageName) {
            final Map<String, T> forUser = get(userId);
            if (forUser == null) return null;
            return forUser.remove(packageName);
        }

        void dump() {
            if (size() == 0) {
                Log.d(TAG, "<empty>");
                return;
            }

            for (int i = 0; i < size(); i++) {
                final int userId = keyAt(i);
                final Map<String, T> forUser = get(userId);
                if (forUser.isEmpty()) {
                    Log.d(TAG, "u" + userId + ": <empty>");
                }

                for (Map.Entry<String, T> packageValue : forUser.entrySet()) {
                    final String packageName = packageValue.getKey();
                    final T value = packageValue.getValue();
                    Log.d(TAG, "u" + userId + "\\" + packageName + " -> " + value);
                }
            }
        }
    }
}
+175 −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 android.annotation.NonNull;
import android.annotation.SuppressLint;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.companion.AssociationInfo;
import android.net.MacAddress;
import android.os.Handler;
import android.os.HandlerExecutor;
import android.util.Log;

import com.android.server.companion.AssociationStore;

import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@SuppressLint("LongLogTag")
class BluetoothCompanionDeviceConnectionListener
        extends BluetoothAdapter.BluetoothConnectionCallback
        implements AssociationStore.OnChangeListener {
    private static final boolean DEBUG = false;
    private static final String TAG = "CompanionDevice_PresenceMonitor_BT";

    interface Callback {
        void onBluetoothCompanionDeviceConnected(int associationId);

        void onBluetoothCompanionDeviceDisconnected(int associationId);
    }

    private final @NonNull AssociationStore mAssociationStore;
    private final @NonNull Callback mCallback;
    /** A set of ALL connected BT device (not only companion.) */
    private final @NonNull Map<MacAddress, BluetoothDevice> mAllConnectedDevices = new HashMap<>();

    BluetoothCompanionDeviceConnectionListener(@NonNull AssociationStore associationStore,
            @NonNull Callback callback) {
        mAssociationStore = associationStore;
        mCallback = callback;
    }

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

        btAdapter.registerBluetoothConnectionCallback(
                new HandlerExecutor(Handler.getMain()), /* callback */this);
        mAssociationStore.registerListener(this);
    }

    /**
     * Overrides
     * {@link BluetoothAdapter.BluetoothConnectionCallback#onDeviceConnected(BluetoothDevice)}.
     */
    @Override
    public void onDeviceConnected(@NonNull BluetoothDevice device) {
        if (DEBUG) Log.i(TAG, "onDevice_Connected() " + toString(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.");
            return;
        }

        onDeviceConnectivityChanged(device, true);
    }

    /**
     * Overrides
     * {@link BluetoothAdapter.BluetoothConnectionCallback#onDeviceConnected(BluetoothDevice)}.
     * Also invoked when user turns BT off while the device is connected.
     */
    @Override
    public void onDeviceDisconnected(@NonNull BluetoothDevice device,
            @DisconnectReason int reason) {
        if (DEBUG) {
            Log.i(TAG, "onDevice_Disconnected() " + toString(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));
            return;
        }

        onDeviceConnectivityChanged(device, false);
    }

    private void onDeviceConnectivityChanged(@NonNull BluetoothDevice device, boolean connected) {
        final List<AssociationInfo> associations =
                mAssociationStore.getAssociationsByAddress(device.getAddress());

        if (DEBUG) {
            Log.d(TAG, "onDevice_ConnectivityChanged() " + toString(device)
                    + " connected=" + connected);
            if (associations.isEmpty()) {
                Log.d(TAG, "  > No CDM associations");
            } else {
                Log.d(TAG, "  > associations=" + Arrays.toString(associations.toArray()));
            }
        }

        for (AssociationInfo association : associations) {
            final int id = association.getId();
            if (connected) {
                mCallback.onBluetoothCompanionDeviceConnected(id);
            } else {
                mCallback.onBluetoothCompanionDeviceDisconnected(id);
            }
        }
    }

    @Override
    public void onAssociationAdded(AssociationInfo association) {
        if (DEBUG) Log.d(TAG, "onAssociation_Added() " + association);

        if (mAllConnectedDevices.containsKey(association.getDeviceMacAddress())) {
            mCallback.onBluetoothCompanionDeviceConnected(association.getId());
        }
    }

    @Override
    public void onAssociationUpdated(AssociationInfo association, boolean addressChanged) {
        if (DEBUG) {
            Log.d(TAG, "onAssociation_Updated() addrChange=" + addressChanged
                    + " " + association);
        }

        if (!addressChanged) {
            // Don't need to do anything.
            return;
        }

        // At the moment CDM does allow changing association addresses, so we will never come here.
        // 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();
    }
}