[2/X] Introduce CDM PackageUtils

Bug: 211398735
Test: atest CtsCompanionDeviceManagerCoreTestCases
Test: atest CtsCompanionDeviceManagerUiAutomationTestCases
Test: atest CtsOsTestCases:CompanionDeviceManagerTest
Change-Id: Ie0400e0678363704754e18d176bd30b7658fe44a
diff --git a/services/companion/java/com/android/server/companion/AssociationRequestsProcessor.java b/services/companion/java/com/android/server/companion/AssociationRequestsProcessor.java
index 1914164..93fc0e72 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.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 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 @@
  * @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 @@
 
         // 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/PackageUtils.java b/services/companion/java/com/android/server/companion/PackageUtils.java
new file mode 100644
index 0000000..985daa3
--- /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<String, List<ComponentName>> getCompanionServicesForUser(
+            @NonNull Context context, @UserIdInt int userId) {
+        final PackageManager pm = context.getPackageManager();
+        final ResolveInfoFlags flags = ResolveInfoFlags.of(GET_META_DATA);
+        final List<ResolveInfo> companionServices =
+                pm.queryIntentServicesAsUser(COMPANION_SERVICE_INTENT, flags, userId);
+
+        final Map<String, List<ComponentName>> 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<ComponentName> services =
+                    (LinkedList<ComponentName>) 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);
+    }
+}