diff --git a/services/companion/java/com/android/server/companion/AssociationRequestsProcessor.java b/services/companion/java/com/android/server/companion/AssociationRequestsProcessor.java index 1914164f195c4..93fc0e7262aa4 100644 --- a/services/companion/java/com/android/server/companion/AssociationRequestsProcessor.java +++ b/services/companion/java/com/android/server/companion/AssociationRequestsProcessor.java @@ -23,7 +23,7 @@ import static android.companion.CompanionDeviceManager.COMPANION_DEVICE_DISCOVER import static android.content.ComponentName.createRelative; import static com.android.server.companion.CompanionDeviceManagerService.DEBUG; -import static com.android.server.companion.CompanionDeviceManagerService.LOG_TAG; +import static com.android.server.companion.PackageUtils.enforceUsesCompanionDeviceFeature; import static com.android.server.companion.PermissionsUtils.enforcePermissionsForAssociation; import static com.android.server.companion.RolesUtils.isRoleHolder; @@ -31,6 +31,7 @@ import static java.util.Objects.requireNonNull; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.SuppressLint; import android.annotation.UserIdInt; import android.app.PendingIntent; import android.companion.AssociationInfo; @@ -102,8 +103,9 @@ import java.util.Set; * @see #processAssociationRequestApproval(AssociationRequest, IAssociationRequestCallback, * ResultReceiver, MacAddress) */ +@SuppressLint("LongLogTag") class AssociationRequestsProcessor { - private static final String TAG = LOG_TAG + ".AssociationRequestsProcessor"; + private static final String TAG = "CompanionDevice_AssociationRequestsProcessor"; private static final ComponentName ASSOCIATION_REQUEST_APPROVAL_ACTIVITY = createRelative(COMPANION_DEVICE_DISCOVERY_PACKAGE_NAME, ".CompanionDeviceActivity"); @@ -161,7 +163,7 @@ class AssociationRequestsProcessor { // 1. Enforce permissions and other requirements. enforcePermissionsForAssociation(mContext, request, packageUid); - mService.checkUsesFeature(packageName, userId); + enforceUsesCompanionDeviceFeature(mContext, userId, packageName); // 2. Check if association can be created without launching UI (i.e. CDM needs NEITHER // to perform discovery NOR to collect user consent). diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceServiceConnector.java b/services/companion/java/com/android/server/companion/CompanionDeviceServiceConnector.java new file mode 100644 index 0000000000000..777917cd5a9ef --- /dev/null +++ b/services/companion/java/com/android/server/companion/CompanionDeviceServiceConnector.java @@ -0,0 +1,119 @@ +/* + * 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 static android.content.Context.BIND_IMPORTANT; + +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.companion.ICompanionDeviceService; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.os.IBinder; +import android.util.Log; + +import com.android.internal.infra.ServiceConnector; + +/** + * Manages a connection (binding) to an instance of {@link CompanionDeviceService} running in the + * application process. + */ +@SuppressLint("LongLogTag") +class CompanionDeviceServiceConnector extends ServiceConnector.Impl { + private static final String TAG = "CompanionDevice_ServiceConnector"; + private static final boolean DEBUG = false; + private static final int BINDING_FLAGS = BIND_IMPORTANT; + + /** Listener for changes to the state of the {@link CompanionDeviceServiceConnector} */ + interface Listener { + void onBindingDied(@UserIdInt int userId, @NonNull String packageName); + } + + private final @UserIdInt int mUserId; + private final @NonNull ComponentName mComponentName; + private @Nullable Listener mListener; + + CompanionDeviceServiceConnector(@NonNull Context context, @UserIdInt int userId, + @NonNull ComponentName componentName) { + super(context, buildIntent(componentName), BINDING_FLAGS, userId, null); + mUserId = userId; + mComponentName = componentName; + } + + void setListener(@Nullable Listener listener) { + mListener = listener; + } + + void postOnDeviceAppeared(@NonNull AssociationInfo associationInfo) { + post(companionService -> companionService.onDeviceAppeared(associationInfo)); + } + + void postOnDeviceDisappeared(@NonNull AssociationInfo associationInfo) { + post(companionService -> companionService.onDeviceDisappeared(associationInfo)); + } + + /** + * Post "unbind" job, which will run *after* all previously posted jobs complete. + * + * IMPORTANT: use this method instead of invoking {@link ServiceConnector#unbind()} directly, + * because the latter may cause previously posted callback, such as + * {@link ICompanionDeviceService#onDeviceDisappeared(AssociationInfo)} to be dropped. + */ + void postUnbind() { + post(it -> unbind()); + } + + @Override + protected void onServiceConnectionStatusChanged( + @NonNull ICompanionDeviceService service, boolean isConnected) { + if (DEBUG) { + Log.d(TAG, "onServiceConnection_StatusChanged() " + mComponentName.toShortString() + + " connected=" + isConnected); + } + } + + @Override + public void onBindingDied(@NonNull ComponentName name) { + // IMPORTANT: call super! + super.onBindingDied(name); + + if (DEBUG) Log.d(TAG, "onBindingDied() " + mComponentName.toShortString()); + + mListener.onBindingDied(mUserId, mComponentName.getPackageName()); + } + + @Override + protected ICompanionDeviceService binderAsInterface(@NonNull IBinder service) { + return ICompanionDeviceService.Stub.asInterface(service); + } + + @Override + protected long getAutoDisconnectTimeoutMs() { + // Do NOT auto-disconnect. + return -1; + } + + private static @NonNull Intent buildIntent(@NonNull ComponentName componentName) { + return new Intent(CompanionDeviceService.SERVICE_INTERFACE) + .setComponent(componentName); + } +} diff --git a/services/companion/java/com/android/server/companion/PackageUtils.java b/services/companion/java/com/android/server/companion/PackageUtils.java new file mode 100644 index 0000000000000..985daa356a537 --- /dev/null +++ b/services/companion/java/com/android/server/companion/PackageUtils.java @@ -0,0 +1,126 @@ +/* + * 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 static android.content.pm.PackageManager.FEATURE_COMPANION_DEVICE_SETUP; +import static android.content.pm.PackageManager.GET_CONFIGURATIONS; +import static android.content.pm.PackageManager.GET_META_DATA; +import static android.content.pm.PackageManager.GET_PERMISSIONS; + +import static com.android.server.companion.CompanionDeviceManagerService.LOG_TAG; + +import android.Manifest; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.UserIdInt; +import android.companion.CompanionDeviceService; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.PackageInfoFlags; +import android.content.pm.PackageManager.ResolveInfoFlags; +import android.content.pm.ResolveInfo; +import android.content.pm.ServiceInfo; +import android.os.Binder; +import android.util.Slog; + +import com.android.internal.util.ArrayUtils; + +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +/** + * Utility methods for working with {@link PackageInfo}-s. + */ +final class PackageUtils { + private static final Intent COMPANION_SERVICE_INTENT = + new Intent(CompanionDeviceService.SERVICE_INTERFACE); + private static final String META_DATA_KEY_PRIMARY = "primary"; + + static @Nullable PackageInfo getPackageInfo(@NonNull Context context, + @UserIdInt int userId, @NonNull String packageName) { + final PackageManager pm = context.getPackageManager(); + final PackageInfoFlags flags = PackageInfoFlags.of(GET_PERMISSIONS | GET_CONFIGURATIONS); + return Binder.withCleanCallingIdentity(() -> + pm.getPackageInfoAsUser(packageName, flags , userId)); + } + + static void enforceUsesCompanionDeviceFeature(@NonNull Context context, + @UserIdInt int userId, @NonNull String packageName) { + final boolean requested = ArrayUtils.contains( + getPackageInfo(context, userId, packageName).reqFeatures, + FEATURE_COMPANION_DEVICE_SETUP); + + if (requested) { + throw new IllegalStateException("Must declare uses-feature " + + FEATURE_COMPANION_DEVICE_SETUP + + " in manifest to use this API"); + } + } + + /** + * @return list of {@link CompanionDeviceService}-s per package for a given user. + * Services marked as "primary" would always appear at the head of the lists, *before* + * all non-primary services. + */ + static @NonNull Map> getCompanionServicesForUser( + @NonNull Context context, @UserIdInt int userId) { + final PackageManager pm = context.getPackageManager(); + final ResolveInfoFlags flags = ResolveInfoFlags.of(GET_META_DATA); + final List companionServices = + pm.queryIntentServicesAsUser(COMPANION_SERVICE_INTENT, flags, userId); + + final Map> packageNameToServiceInfoList = new HashMap<>(); + + for (ResolveInfo resolveInfo : companionServices) { + final ServiceInfo service = resolveInfo.serviceInfo; + + final boolean requiresPermission = Manifest.permission.BIND_COMPANION_DEVICE_SERVICE + .equals(resolveInfo.serviceInfo.permission); + if (!requiresPermission) { + Slog.w(LOG_TAG, "CompanionDeviceService " + + service.getComponentName().flattenToShortString() + " must require " + + "android.permission.BIND_COMPANION_DEVICE_SERVICE"); + continue; + } + + // Use LinkedList, because we'll need to prepend "primary" services, while appending the + // other (non-primary) services to the list. + final LinkedList services = + (LinkedList) packageNameToServiceInfoList.computeIfAbsent( + service.packageName, it -> new LinkedList<>()); + + final ComponentName componentName = service.getComponentName(); + if (isPrimaryCompanionDeviceService(service)) { + // "Primary" service should be at the head of the list. + services.addFirst(componentName); + } else { + services.addLast(componentName); + } + } + + return packageNameToServiceInfoList; + } + + private static boolean isPrimaryCompanionDeviceService(ServiceInfo service) { + return service.metaData != null && service.metaData.getBoolean(META_DATA_KEY_PRIMARY); + } +}