blob: 5a9c4704aa7dcd43b9a4580ed67da1779d294d3d [file] [log] [blame]
/*
* 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.Slog;
import android.util.SparseArray;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.infra.PerUser;
import com.android.server.companion.presence.CompanionDevicePresenceMonitor;
import java.io.PrintWriter;
import java.util.ArrayList;
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, boolean)}
* <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")
public class CompanionApplicationController {
static final boolean DEBUG = false;
private static final String TAG = "CDM_CompanionApplicationController";
private static final long REBIND_TIMEOUT = 10 * 1000; // 10 sec
private final @NonNull Context mContext;
private final @NonNull AssociationStore mAssociationStore;
private final @NonNull CompanionDevicePresenceMonitor mDevicePresenceMonitor;
private final @NonNull CompanionServicesRegister mCompanionServicesRegister;
@GuardedBy("mBoundCompanionApplications")
private final @NonNull AndroidPackageMap<List<CompanionDeviceServiceConnector>>
mBoundCompanionApplications;
@GuardedBy("mScheduledForRebindingCompanionApplications")
private final @NonNull AndroidPackageMap<Boolean> mScheduledForRebindingCompanionApplications;
CompanionApplicationController(Context context, AssociationStore associationStore,
CompanionDevicePresenceMonitor companionDevicePresenceMonitor) {
mContext = context;
mAssociationStore = associationStore;
mDevicePresenceMonitor = companionDevicePresenceMonitor;
mCompanionServicesRegister = new CompanionServicesRegister();
mBoundCompanionApplications = new AndroidPackageMap<>();
mScheduledForRebindingCompanionApplications = new AndroidPackageMap<>();
}
void onPackagesChanged(@UserIdInt int userId) {
mCompanionServicesRegister.invalidate(userId);
}
/**
* CDM binds to the companion app.
*/
public void bindCompanionApplication(@UserIdInt int userId, @NonNull String packageName,
boolean isSelfManaged) {
if (DEBUG) {
Log.i(TAG, "bind() u" + userId + "/" + packageName
+ " isSelfManaged=" + isSelfManaged);
}
final List<ComponentName> companionServices =
mCompanionServicesRegister.forPackage(userId, packageName);
if (companionServices.isEmpty()) {
Slog.w(TAG, "Can not bind companion applications u" + userId + "/" + packageName + ": "
+ "eligible CompanionDeviceService not found.\n"
+ "A CompanionDeviceService should declare an intent-filter for "
+ "\"android.companion.CompanionDeviceService\" action and require "
+ "\"android.permission.BIND_COMPANION_DEVICE_SERVICE\" permission.");
return;
}
final List<CompanionDeviceServiceConnector> serviceConnectors = new ArrayList<>();
synchronized (mBoundCompanionApplications) {
if (mBoundCompanionApplications.containsValueForPackage(userId, packageName)) {
if (DEBUG) Log.e(TAG, "u" + userId + "/" + packageName + " is ALREADY bound.");
return;
}
for (int i = 0; i < companionServices.size(); i++) {
boolean isPrimary = i == 0;
serviceConnectors.add(CompanionDeviceServiceConnector.newInstance(mContext, userId,
companionServices.get(i), isSelfManaged, isPrimary));
}
mBoundCompanionApplications.setValueForPackage(userId, packageName, serviceConnectors);
}
// Set listeners for both Primary and Secondary connectors.
for (CompanionDeviceServiceConnector serviceConnector : serviceConnectors) {
serviceConnector.setListener(this::onBinderDied);
}
// Now "bind" all the connectors: the primary one and the rest of them.
for (CompanionDeviceServiceConnector serviceConnector : serviceConnectors) {
serviceConnector.connect();
}
}
/**
* CDM unbinds the companion app.
*/
public 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);
}
synchronized (mScheduledForRebindingCompanionApplications) {
mScheduledForRebindingCompanionApplications.removePackage(userId, packageName);
}
if (serviceConnectors == null) {
if (DEBUG) {
Log.e(TAG, "unbindCompanionApplication(): "
+ "u" + userId + "/" + packageName + " is NOT bound");
Log.d(TAG, "Stacktrace", new Throwable());
}
return;
}
for (CompanionDeviceServiceConnector serviceConnector : serviceConnectors) {
serviceConnector.postUnbind();
}
}
/**
* @return whether the companion application is bound now.
*/
public boolean isCompanionApplicationBound(@UserIdInt int userId, @NonNull String packageName) {
synchronized (mBoundCompanionApplications) {
return mBoundCompanionApplications.containsValueForPackage(userId, packageName);
}
}
private void scheduleRebinding(@UserIdInt int userId, @NonNull String packageName,
CompanionDeviceServiceConnector serviceConnector) {
Slog.i(TAG, "scheduleRebinding() " + userId + "/" + packageName);
if (isRebindingCompanionApplicationScheduled(userId, packageName)) {
if (DEBUG) {
Log.i(TAG, "CompanionApplication rebinding has been scheduled, skipping "
+ serviceConnector.getComponentName());
}
return;
}
if (serviceConnector.isPrimary()) {
synchronized (mScheduledForRebindingCompanionApplications) {
mScheduledForRebindingCompanionApplications.setValueForPackage(
userId, packageName, true);
}
}
// Rebinding in 10 seconds.
Handler.getMain().postDelayed(() ->
onRebindingCompanionApplicationTimeout(userId, packageName, serviceConnector),
REBIND_TIMEOUT);
}
private boolean isRebindingCompanionApplicationScheduled(
@UserIdInt int userId, @NonNull String packageName) {
synchronized (mScheduledForRebindingCompanionApplications) {
return mScheduledForRebindingCompanionApplications.containsValueForPackage(
userId, packageName);
}
}
private void onRebindingCompanionApplicationTimeout(
@UserIdInt int userId, @NonNull String packageName,
@NonNull CompanionDeviceServiceConnector serviceConnector) {
// Re-mark the application is bound.
if (serviceConnector.isPrimary()) {
synchronized (mBoundCompanionApplications) {
if (!mBoundCompanionApplications.containsValueForPackage(userId, packageName)) {
List<CompanionDeviceServiceConnector> serviceConnectors =
Collections.singletonList(serviceConnector);
mBoundCompanionApplications.setValueForPackage(userId, packageName,
serviceConnectors);
}
}
synchronized (mScheduledForRebindingCompanionApplications) {
mScheduledForRebindingCompanionApplications.removePackage(userId, packageName);
}
}
serviceConnector.connect();
}
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, "notify_CompanionApplicationDevice_Appeared(): "
+ "u" + userId + "/" + packageName + " is NOT bound.");
Log.d(TAG, "Stacktrace", new Throwable());
}
return;
}
Log.i(TAG, "Calling onDeviceAppeared to userId=[" + userId + "] package=["
+ packageName + "] associationId=[" + association.getId() + "]");
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, "notify_CompanionApplicationDevice_Disappeared(): "
+ "u" + userId + "/" + packageName + " is NOT bound.");
Log.d(TAG, "Stacktrace", new Throwable());
}
return;
}
Log.i(TAG, "Calling onDeviceDisappeared to userId=[" + userId + "] package=["
+ packageName + "] associationId=[" + association.getId() + "]");
primaryServiceConnector.postOnDeviceDisappeared(association);
}
void dump(@NonNull PrintWriter out) {
out.append("Companion Device Application Controller: \n");
synchronized (mBoundCompanionApplications) {
out.append(" Bound Companion Applications: ");
if (mBoundCompanionApplications.size() == 0) {
out.append("<empty>\n");
} else {
out.append("\n");
mBoundCompanionApplications.dump(out);
}
}
out.append(" Companion Applications Scheduled For Rebinding: ");
if (mScheduledForRebindingCompanionApplications.size() == 0) {
out.append("<empty>\n");
} else {
out.append("\n");
mScheduledForRebindingCompanionApplications.dump(out);
}
}
/**
* Rebinding for Self-Managed secondary services OR Non-Self-Managed services.
*/
private void onBinderDied(@UserIdInt int userId, @NonNull String packageName,
@NonNull CompanionDeviceServiceConnector serviceConnector) {
boolean isPrimary = serviceConnector.isPrimary();
Slog.i(TAG, "onBinderDied() u" + userId + "/" + packageName + " isPrimary: " + isPrimary);
// First: Only mark not BOUND for primary service.
synchronized (mBoundCompanionApplications) {
if (serviceConnector.isPrimary()) {
mBoundCompanionApplications.removePackage(userId, packageName);
}
}
// Second: schedule rebinding if needed.
final boolean shouldScheduleRebind = shouldScheduleRebind(userId, packageName, isPrimary);
if (shouldScheduleRebind) {
scheduleRebinding(userId, packageName, serviceConnector);
}
}
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 boolean shouldScheduleRebind(int userId, String packageName, boolean isPrimary) {
// Make sure do not schedule rebind for the case ServiceConnector still gets callback after
// app is uninstalled.
boolean stillAssociated = false;
// Make sure to clean up the state for all the associations
// that associate with this package.
boolean shouldScheduleRebind = false;
for (AssociationInfo ai :
mAssociationStore.getAssociationsForPackage(userId, packageName)) {
final int associationId = ai.getId();
stillAssociated = true;
if (ai.isSelfManaged()) {
// Do not rebind if primary one is died for selfManaged application.
if (isPrimary
&& mDevicePresenceMonitor.isDevicePresent(associationId)) {
mDevicePresenceMonitor.onSelfManagedDeviceReporterBinderDied(associationId);
shouldScheduleRebind = false;
}
// Do not rebind if both primary and secondary services are died for
// selfManaged application.
shouldScheduleRebind = isCompanionApplicationBound(userId, packageName);
} else if (ai.isNotifyOnDeviceNearby()) {
// Always rebind for non-selfManaged devices.
shouldScheduleRebind = true;
}
}
return stillAssociated && shouldScheduleRebind;
}
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 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);
}
}
}
private void dump(@NonNull PrintWriter out) {
for (int i = 0; i < size(); i++) {
final int userId = keyAt(i);
final Map<String, T> forUser = get(userId);
if (forUser.isEmpty()) {
out.append(" u").append(String.valueOf(userId)).append(": <empty>\n");
}
for (Map.Entry<String, T> packageValue : forUser.entrySet()) {
final String packageName = packageValue.getKey();
final T value = packageValue.getValue();
out.append(" u").append(String.valueOf(userId)).append("\\")
.append(packageName).append(" -> ")
.append(value.toString()).append('\n');
}
}
}
}
}