Merge changes If0ffc8d5,I2934747b

* changes:
  [4/X] Introduce BluetoothCompanionDeviceConnectionListener
  [3/X] Introduce CompanionApplicationController
This commit is contained in:
Sergey Nikolaienkov
2022-01-20 15:40:20 +00:00
committed by Android (Google) Code Review
2 changed files with 492 additions and 0 deletions

View File

@@ -0,0 +1,317 @@
/*
* 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);
}
}
}
}
}

View File

@@ -0,0 +1,175 @@
/*
* 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();
}
}