[3/X] Introduce CompanionApplicationController
Bug: 211398735
Test: atest CtsCompanionDeviceManagerCoreTestCases
Test: atest CtsCompanionDeviceManagerUiAutomationTestCases
Test: atest CtsOsTestCases:CompanionDeviceManagerTest
Change-Id: I2934747bbc53f38221015f21e38aa2bc08641f77
diff --git a/services/companion/java/com/android/server/companion/CompanionApplicationController.java b/services/companion/java/com/android/server/companion/CompanionApplicationController.java
new file mode 100644
index 0000000..be1bc79
--- /dev/null
+++ b/services/companion/java/com/android/server/companion/CompanionApplicationController.java
@@ -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);
+ }
+ }
+ }
+ }
+}