diff options
7 files changed, 364 insertions, 114 deletions
diff --git a/core/java/android/app/admin/flags/flags.aconfig b/core/java/android/app/admin/flags/flags.aconfig index 441d52148b7b..3ec6fe7728e5 100644 --- a/core/java/android/app/admin/flags/flags.aconfig +++ b/core/java/android/app/admin/flags/flags.aconfig @@ -1,3 +1,6 @@ +# proto-file: build/make/tools/aconfig/aconfig_protos/protos/aconfig.proto +# proto-message: flag_declarations + package: "android.app.admin.flags" flag { @@ -180,3 +183,10 @@ flag { description: "Allow COPE admin to control screen brightness and timeout." bug: "323894620" } + +flag { + name: "is_recursive_required_app_merging_enabled" + namespace: "enterprise" + description: "Guards a new flow for recursive required enterprise app list merging" + bug: "319084618" +} diff --git a/services/devicepolicy/Android.bp b/services/devicepolicy/Android.bp index 8dfa685bf6ff..da965bb02460 100644 --- a/services/devicepolicy/Android.bp +++ b/services/devicepolicy/Android.bp @@ -24,5 +24,6 @@ java_library_static { "app-compat-annotations", "service-permission.stubs.system_server", "device_policy_aconfig_flags_lib", + "androidx.annotation_annotation", ], } diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/OverlayPackagesProvider.java b/services/devicepolicy/java/com/android/server/devicepolicy/OverlayPackagesProvider.java index f3b164c6501c..94c137444ede 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/OverlayPackagesProvider.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/OverlayPackagesProvider.java @@ -25,15 +25,16 @@ import static android.app.admin.DevicePolicyManager.REQUIRED_APP_MANAGED_USER; import static android.content.pm.PackageManager.GET_META_DATA; import static com.android.internal.util.Preconditions.checkArgument; -import static com.android.internal.util.Preconditions.checkNotNull; -import static com.android.server.devicepolicy.DevicePolicyManagerService.dumpResources; +import static com.android.server.devicepolicy.DevicePolicyManagerService.dumpApps; import static java.util.Objects.requireNonNull; +import android.annotation.ArrayRes; import android.annotation.NonNull; import android.annotation.UserIdInt; import android.app.admin.DeviceAdminReceiver; import android.app.admin.DevicePolicyManager; +import android.app.admin.flags.Flags; import android.app.role.RoleManager; import android.content.ComponentName; import android.content.Context; @@ -67,13 +68,16 @@ public class OverlayPackagesProvider { protected static final String TAG = "OverlayPackagesProvider"; private static final Map<String, String> sActionToMetadataKeyMap = new HashMap<>(); - { + + static { sActionToMetadataKeyMap.put(ACTION_PROVISION_MANAGED_USER, REQUIRED_APP_MANAGED_USER); sActionToMetadataKeyMap.put(ACTION_PROVISION_MANAGED_PROFILE, REQUIRED_APP_MANAGED_PROFILE); sActionToMetadataKeyMap.put(ACTION_PROVISION_MANAGED_DEVICE, REQUIRED_APP_MANAGED_DEVICE); } + private static final Set<String> sAllowedActions = new HashSet<>(); - { + + static { sAllowedActions.add(ACTION_PROVISION_MANAGED_USER); sAllowedActions.add(ACTION_PROVISION_MANAGED_PROFILE); sAllowedActions.add(ACTION_PROVISION_MANAGED_DEVICE); @@ -83,8 +87,13 @@ public class OverlayPackagesProvider { private final Context mContext; private final Injector mInjector; + private final RecursiveStringArrayResourceResolver mRecursiveStringArrayResourceResolver; + public OverlayPackagesProvider(Context context) { - this(context, new DefaultInjector()); + this( + context, + new DefaultInjector(), + new RecursiveStringArrayResourceResolver(context.getResources())); } @VisibleForTesting @@ -113,8 +122,8 @@ public class OverlayPackagesProvider { public String getDevicePolicyManagementRoleHolderPackageName(Context context) { return Binder.withCleanCallingIdentity(() -> { RoleManager roleManager = context.getSystemService(RoleManager.class); - List<String> roleHolders = - roleManager.getRoleHolders(RoleManager.ROLE_DEVICE_POLICY_MANAGEMENT); + List<String> roleHolders = roleManager.getRoleHolders( + RoleManager.ROLE_DEVICE_POLICY_MANAGEMENT); if (roleHolders.isEmpty()) { return null; } @@ -124,17 +133,20 @@ public class OverlayPackagesProvider { } @VisibleForTesting - OverlayPackagesProvider(Context context, Injector injector) { + OverlayPackagesProvider(Context context, Injector injector, + RecursiveStringArrayResourceResolver recursiveStringArrayResourceResolver) { mContext = context; - mPm = checkNotNull(context.getPackageManager()); - mInjector = checkNotNull(injector); + mPm = requireNonNull(context.getPackageManager()); + mInjector = requireNonNull(injector); + mRecursiveStringArrayResourceResolver = requireNonNull( + recursiveStringArrayResourceResolver); } /** * Computes non-required apps. All the system apps with a launcher that are not in * the required set of packages, and all mainline modules that are not declared as required * via metadata in their manifests, will be considered as non-required apps. - * + * <p> * Note: If an app is mistakenly listed as both required and disallowed, it will be treated as * disallowed. * @@ -176,12 +188,12 @@ public class OverlayPackagesProvider { /** * Returns a subset of {@code packageNames} whose packages are mainline modules declared as * required apps via their app metadata. + * * @see DevicePolicyManager#REQUIRED_APP_MANAGED_USER * @see DevicePolicyManager#REQUIRED_APP_MANAGED_DEVICE * @see DevicePolicyManager#REQUIRED_APP_MANAGED_PROFILE */ - private Set<String> getRequiredAppsMainlineModules( - Set<String> packageNames, + private Set<String> getRequiredAppsMainlineModules(Set<String> packageNames, String provisioningAction) { final Set<String> result = new HashSet<>(); for (String packageName : packageNames) { @@ -225,8 +237,8 @@ public class OverlayPackagesProvider { } private boolean isApkInApexMainlineModule(String packageName) { - final String apexPackageName = - mInjector.getActiveApexPackageNameContainingPackage(packageName); + final String apexPackageName = mInjector.getActiveApexPackageNameContainingPackage( + packageName); return apexPackageName != null; } @@ -274,112 +286,94 @@ public class OverlayPackagesProvider { } private Set<String> getRequiredAppsSet(String provisioningAction) { - final int resId; - switch (provisioningAction) { - case ACTION_PROVISION_MANAGED_USER: - resId = R.array.required_apps_managed_user; - break; - case ACTION_PROVISION_MANAGED_PROFILE: - resId = R.array.required_apps_managed_profile; - break; - case ACTION_PROVISION_MANAGED_DEVICE: - resId = R.array.required_apps_managed_device; - break; - default: - throw new IllegalArgumentException("Provisioning type " - + provisioningAction + " not supported."); - } - return new ArraySet<>(Arrays.asList(mContext.getResources().getStringArray(resId))); + final int resId = switch (provisioningAction) { + case ACTION_PROVISION_MANAGED_USER -> R.array.required_apps_managed_user; + case ACTION_PROVISION_MANAGED_PROFILE -> R.array.required_apps_managed_profile; + case ACTION_PROVISION_MANAGED_DEVICE -> R.array.required_apps_managed_device; + default -> throw new IllegalArgumentException( + "Provisioning type " + provisioningAction + " not supported."); + }; + return resolveStringArray(resId); } private Set<String> getDisallowedAppsSet(String provisioningAction) { - final int resId; - switch (provisioningAction) { - case ACTION_PROVISION_MANAGED_USER: - resId = R.array.disallowed_apps_managed_user; - break; - case ACTION_PROVISION_MANAGED_PROFILE: - resId = R.array.disallowed_apps_managed_profile; - break; - case ACTION_PROVISION_MANAGED_DEVICE: - resId = R.array.disallowed_apps_managed_device; - break; - default: - throw new IllegalArgumentException("Provisioning type " - + provisioningAction + " not supported."); - } - return new ArraySet<>(Arrays.asList(mContext.getResources().getStringArray(resId))); + final int resId = switch (provisioningAction) { + case ACTION_PROVISION_MANAGED_USER -> R.array.disallowed_apps_managed_user; + case ACTION_PROVISION_MANAGED_PROFILE -> R.array.disallowed_apps_managed_profile; + case ACTION_PROVISION_MANAGED_DEVICE -> R.array.disallowed_apps_managed_device; + default -> throw new IllegalArgumentException( + "Provisioning type " + provisioningAction + " not supported."); + }; + return resolveStringArray(resId); } private Set<String> getVendorRequiredAppsSet(String provisioningAction) { - final int resId; - switch (provisioningAction) { - case ACTION_PROVISION_MANAGED_USER: - resId = R.array.vendor_required_apps_managed_user; - break; - case ACTION_PROVISION_MANAGED_PROFILE: - resId = R.array.vendor_required_apps_managed_profile; - break; - case ACTION_PROVISION_MANAGED_DEVICE: - resId = R.array.vendor_required_apps_managed_device; - break; - default: - throw new IllegalArgumentException("Provisioning type " - + provisioningAction + " not supported."); - } - return new ArraySet<>(Arrays.asList(mContext.getResources().getStringArray(resId))); + final int resId = switch (provisioningAction) { + case ACTION_PROVISION_MANAGED_USER -> R.array.vendor_required_apps_managed_user; + case ACTION_PROVISION_MANAGED_PROFILE -> R.array.vendor_required_apps_managed_profile; + case ACTION_PROVISION_MANAGED_DEVICE -> R.array.vendor_required_apps_managed_device; + default -> throw new IllegalArgumentException( + "Provisioning type " + provisioningAction + " not supported."); + }; + return resolveStringArray(resId); } private Set<String> getVendorDisallowedAppsSet(String provisioningAction) { - final int resId; - switch (provisioningAction) { - case ACTION_PROVISION_MANAGED_USER: - resId = R.array.vendor_disallowed_apps_managed_user; - break; - case ACTION_PROVISION_MANAGED_PROFILE: - resId = R.array.vendor_disallowed_apps_managed_profile; - break; - case ACTION_PROVISION_MANAGED_DEVICE: - resId = R.array.vendor_disallowed_apps_managed_device; - break; - default: - throw new IllegalArgumentException("Provisioning type " - + provisioningAction + " not supported."); + final int resId = switch (provisioningAction) { + case ACTION_PROVISION_MANAGED_USER -> R.array.vendor_disallowed_apps_managed_user; + case ACTION_PROVISION_MANAGED_PROFILE -> R.array.vendor_disallowed_apps_managed_profile; + case ACTION_PROVISION_MANAGED_DEVICE -> R.array.vendor_disallowed_apps_managed_device; + default -> throw new IllegalArgumentException( + "Provisioning type " + provisioningAction + " not supported."); + }; + return resolveStringArray(resId); + } + + private Set<String> resolveStringArray(@ArrayRes int resId) { + if (Flags.isRecursiveRequiredAppMergingEnabled()) { + return mRecursiveStringArrayResourceResolver.resolve(mContext.getPackageName(), resId); + } else { + return new ArraySet<>(Arrays.asList(mContext.getResources().getStringArray(resId))); } - return new ArraySet<>(Arrays.asList(mContext.getResources().getStringArray(resId))); } void dump(IndentingPrintWriter pw) { pw.println("OverlayPackagesProvider"); pw.increaseIndent(); - dumpResources(pw, mContext, "required_apps_managed_device", - R.array.required_apps_managed_device); - dumpResources(pw, mContext, "required_apps_managed_user", - R.array.required_apps_managed_user); - dumpResources(pw, mContext, "required_apps_managed_profile", - R.array.required_apps_managed_profile); - - dumpResources(pw, mContext, "disallowed_apps_managed_device", - R.array.disallowed_apps_managed_device); - dumpResources(pw, mContext, "disallowed_apps_managed_user", - R.array.disallowed_apps_managed_user); - dumpResources(pw, mContext, "disallowed_apps_managed_device", - R.array.disallowed_apps_managed_device); - - dumpResources(pw, mContext, "vendor_required_apps_managed_device", - R.array.vendor_required_apps_managed_device); - dumpResources(pw, mContext, "vendor_required_apps_managed_user", - R.array.vendor_required_apps_managed_user); - dumpResources(pw, mContext, "vendor_required_apps_managed_profile", - R.array.vendor_required_apps_managed_profile); - - dumpResources(pw, mContext, "vendor_disallowed_apps_managed_user", - R.array.vendor_disallowed_apps_managed_user); - dumpResources(pw, mContext, "vendor_disallowed_apps_managed_device", - R.array.vendor_disallowed_apps_managed_device); - dumpResources(pw, mContext, "vendor_disallowed_apps_managed_profile", - R.array.vendor_disallowed_apps_managed_profile); + dumpApps(pw, "required_apps_managed_device", + resolveStringArray(R.array.required_apps_managed_device).toArray(String[]::new)); + dumpApps(pw, "required_apps_managed_user", + resolveStringArray(R.array.required_apps_managed_user).toArray(String[]::new)); + dumpApps(pw, "required_apps_managed_profile", + resolveStringArray(R.array.required_apps_managed_profile).toArray(String[]::new)); + + dumpApps(pw, "disallowed_apps_managed_device", + resolveStringArray(R.array.disallowed_apps_managed_device).toArray(String[]::new)); + dumpApps(pw, "disallowed_apps_managed_user", + resolveStringArray(R.array.disallowed_apps_managed_user).toArray(String[]::new)); + dumpApps(pw, "disallowed_apps_managed_device", + resolveStringArray(R.array.disallowed_apps_managed_device).toArray(String[]::new)); + + dumpApps(pw, "vendor_required_apps_managed_device", + resolveStringArray(R.array.vendor_required_apps_managed_device).toArray( + String[]::new)); + dumpApps(pw, "vendor_required_apps_managed_user", + resolveStringArray(R.array.vendor_required_apps_managed_user).toArray( + String[]::new)); + dumpApps(pw, "vendor_required_apps_managed_profile", + resolveStringArray(R.array.vendor_required_apps_managed_profile).toArray( + String[]::new)); + + dumpApps(pw, "vendor_disallowed_apps_managed_user", + resolveStringArray(R.array.vendor_disallowed_apps_managed_user).toArray( + String[]::new)); + dumpApps(pw, "vendor_disallowed_apps_managed_device", + resolveStringArray(R.array.vendor_disallowed_apps_managed_device).toArray( + String[]::new)); + dumpApps(pw, "vendor_disallowed_apps_managed_profile", + resolveStringArray(R.array.vendor_disallowed_apps_managed_profile).toArray( + String[]::new)); pw.decreaseIndent(); } diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/RecursiveStringArrayResourceResolver.java b/services/devicepolicy/java/com/android/server/devicepolicy/RecursiveStringArrayResourceResolver.java new file mode 100644 index 000000000000..935e051b64ea --- /dev/null +++ b/services/devicepolicy/java/com/android/server/devicepolicy/RecursiveStringArrayResourceResolver.java @@ -0,0 +1,147 @@ +/* + * Copyright (C) 2021 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.devicepolicy; + +import android.annotation.SuppressLint; +import android.content.res.Resources; + +import androidx.annotation.ArrayRes; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; + +/** + * A class encapsulating all the logic for recursive string-array resource resolution. + */ +public class RecursiveStringArrayResourceResolver { + private static final String IMPORT_PREFIX = "#import:"; + private static final String SEPARATOR = "/"; + private static final String PWP = "."; + + private final Resources mResources; + + /** + * @param resources Android resource access object to use when resolving resources + */ + public RecursiveStringArrayResourceResolver(Resources resources) { + this.mResources = resources; + } + + /** + * Resolves a given {@code <string-array/>} resource specified via + * {@param rootId} in {@param pkg}. During resolution all values prefixed with + * {@link #IMPORT_PREFIX} are expanded and injected + * into the final list at the position of the import statement, + * pushing all the following values (and their expansions) down. + * Circular imports are tracked and skipped to avoid infinite resolution loops without losing + * data. + * + * <p> + * The import statements are expected in a form of + * "{@link #IMPORT_PREFIX}{package}{@link #SEPARATOR}{resourceName}" + * If the resource being imported is from the same package, its package can be specified as a + * {@link #PWP} shorthand `.` + * > e.g.: + * > {@code "#import:com.android.internal/disallowed_apps_managed_user"} + * > {@code "#import:./disallowed_apps_managed_user"} + * + * <p> + * Any incorrect or unresolvable import statement + * will cause the entire resolution to fail with an error. + * + * @param pkg the package owning the resource + * @param rootId the id of the {@code <string-array>} resource within {@param pkg} to start the + * resolution from + * @return a flattened list of all the resolved string array values from the root resource + * as well as all the imported arrays + */ + public Set<String> resolve(String pkg, @ArrayRes int rootId) { + return resolve(List.of(), pkg, rootId); + } + + /** + * A version of resolve that tracks already imported resources + * to avoid circular imports and wasted work. + * + * @param cache a list of already resolved packages to be skipped for further resolution + */ + private Set<String> resolve(Collection<String> cache, String pkg, @ArrayRes int rootId) { + final var strings = mResources.getStringArray(rootId); + final var runningCache = new ArrayList<>(cache); + + final var result = new HashSet<String>(); + for (var string : strings) { + final String ref; + if (string.startsWith(IMPORT_PREFIX)) { + ref = string.substring(IMPORT_PREFIX.length()); + } else { + ref = null; + } + + if (ref == null) { + result.add(string); + } else if (!runningCache.contains(ref)) { + final var next = resolveImport(runningCache, pkg, ref); + runningCache.addAll(next); + result.addAll(next); + } + } + return result; + } + + /** + * Resolves an import of the {@code <string-array>} resource + * in the context of {@param importingPackage} by the provided {@param ref}. + * + * @param cache a list of already resolved packages to be passed along into chained + * {@link #resolve} calls + * @param importingPackage the package that owns the resource which defined the import being + * processed. + * It is also used to expand all {@link #PWP} shorthands in + * {@param ref} + * @param ref reference to the resource to be imported in a form of + * "{package}{@link #SEPARATOR}{resourceName}". + * e.g.: {@code com.android.internal/disallowed_apps_managed_user} + */ + private Set<String> resolveImport( + Collection<String> cache, + String importingPackage, + String ref) { + final var chunks = ref.split(SEPARATOR, 2); + final var pkg = chunks[0]; + final var name = chunks[1]; + final String resolvedPkg; + if (Objects.equals(pkg, PWP)) { + resolvedPkg = importingPackage; + } else { + resolvedPkg = pkg; + } + @SuppressLint("DiscouragedApi") final var importId = mResources.getIdentifier( + /* name = */ name, + /* defType = */ "array", + /* defPackage = */ resolvedPkg); + if (importId == 0) { + throw new Resources.NotFoundException( + /* name= */ String.format("%s:array/%s", resolvedPkg, name)); + } + return resolve(cache, resolvedPkg, importId); + } +} diff --git a/services/tests/servicestests/Android.bp b/services/tests/servicestests/Android.bp index 37967fa86b0f..65986ea063fe 100644 --- a/services/tests/servicestests/Android.bp +++ b/services/tests/servicestests/Android.bp @@ -62,6 +62,7 @@ android_test { "cts-wm-util", "platform-compat-test-rules", "mockito-target-minus-junit4", + "mockito-kotlin2", "platform-test-annotations", "ShortcutManagerTestUtils", "truth", diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/OverlayPackagesProviderTest.java b/services/tests/servicestests/src/com/android/server/devicepolicy/OverlayPackagesProviderTest.java index 4f6fc3dc1f93..0a696ef44897 100644 --- a/services/tests/servicestests/src/com/android/server/devicepolicy/OverlayPackagesProviderTest.java +++ b/services/tests/servicestests/src/com/android/server/devicepolicy/OverlayPackagesProviderTest.java @@ -47,7 +47,7 @@ import android.view.inputmethod.InputMethodInfo; import androidx.annotation.NonNull; import androidx.test.InstrumentationRegistry; -import androidx.test.runner.AndroidJUnit4; +import androidx.test.ext.junit.runners.AndroidJUnit4; import com.android.internal.R; @@ -67,9 +67,7 @@ import java.util.Set; /** * Run this test with: - * * {@code atest FrameworksServicesTests:com.android.server.devicepolicy.OwnersTest} - * */ @RunWith(AndroidJUnit4.class) public class OverlayPackagesProviderTest { @@ -87,8 +85,8 @@ public class OverlayPackagesProviderTest { private FakePackageManager mPackageManager; private String[] mSystemAppsWithLauncher; - private Set<String> mRegularMainlineModules = new HashSet<>(); - private Map<String, String> mMainlineModuleToDeclaredMetadataMap = new HashMap<>(); + private final Set<String> mRegularMainlineModules = new HashSet<>(); + private final Map<String, String> mMainlineModuleToDeclaredMetadataMap = new HashMap<>(); private OverlayPackagesProvider mHelper; @Before @@ -115,7 +113,8 @@ public class OverlayPackagesProviderTest { setVendorDisallowedAppsManagedUser(); mRealResources = InstrumentationRegistry.getTargetContext().getResources(); - mHelper = new OverlayPackagesProvider(mTestContext, mInjector); + mHelper = new OverlayPackagesProvider(mTestContext, mInjector, + new RecursiveStringArrayResourceResolver(mResources)); } @Test @@ -213,7 +212,7 @@ public class OverlayPackagesProviderTest { } /** - * @see {@link #testAllowedAndDisallowedAtTheSameTimeManagedDevice} + * @see #testAllowedAndDisallowedAtTheSameTimeManagedDevice */ @Test public void testAllowedAndDisallowedAtTheSameTimeManagedUser() { @@ -224,7 +223,7 @@ public class OverlayPackagesProviderTest { } /** - * @see {@link #testAllowedAndDisallowedAtTheSameTimeManagedDevice} + * @see #testAllowedAndDisallowedAtTheSameTimeManagedDevice */ @Test public void testAllowedAndDisallowedAtTheSameTimeManagedProfile() { @@ -447,7 +446,7 @@ public class OverlayPackagesProviderTest { } private void setSystemInputMethods(String... packageNames) { - List<InputMethodInfo> inputMethods = new ArrayList<InputMethodInfo>(); + List<InputMethodInfo> inputMethods = new ArrayList<>(); for (String packageName : packageNames) { ApplicationInfo aInfo = new ApplicationInfo(); aInfo.flags = ApplicationInfo.FLAG_SYSTEM; @@ -467,6 +466,7 @@ public class OverlayPackagesProviderTest { mSystemAppsWithLauncher = apps; } + @SafeVarargs private <T> Set<T> setFromArray(T... array) { if (array == null) { return null; @@ -475,6 +475,7 @@ public class OverlayPackagesProviderTest { } class FakePackageManager extends MockPackageManager { + @NonNull @Override public List<ResolveInfo> queryIntentActivitiesAsUser(Intent intent, int flags, int userId) { assertWithMessage("Expected an intent with action ACTION_MAIN") diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/RecursiveStringArrayResourceResolverTest.kt b/services/tests/servicestests/src/com/android/server/devicepolicy/RecursiveStringArrayResourceResolverTest.kt new file mode 100644 index 000000000000..647f6c78f29f --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/devicepolicy/RecursiveStringArrayResourceResolverTest.kt @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2024 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.devicepolicy + +import android.annotation.ArrayRes +import android.content.res.Resources +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.google.common.truth.Truth.assertWithMessage +import com.google.errorprone.annotations.CanIgnoreReturnValue +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever + + +/** + * Run this test with: + * `atest FrameworksServicesTests:com.android.server.devicepolicy.RecursiveStringArrayResourceResolverTest` + */ +@SmallTest +@RunWith(AndroidJUnit4::class) +class RecursiveStringArrayResourceResolverTest { + private companion object { + const val PACKAGE = "com.android.test" + const val ROOT_RESOURCE = "my_root_resource" + const val SUB_RESOURCE = "my_sub_resource" + const val EXTERNAL_PACKAGE = "com.external.test" + const val EXTERNAL_RESOURCE = "my_external_resource" + } + + private val mResources = mock<Resources>() + private val mTarget = RecursiveStringArrayResourceResolver(mResources) + + /** + * Mocks [Resources.getIdentifier] and [Resources.getStringArray] to return [values] and reference under a generated ID. + * @receiver mocked [Resources] container to configure + * @param pkg package name to "contain" mocked resource + * @param name mocked resource name + * @param values string-array resource values to return when mock is queried + * @return generated resource ID + */ + @ArrayRes + @CanIgnoreReturnValue + private fun Resources.mockStringArrayResource(pkg: String, name: String, vararg values: String): Int { + val anId = (pkg + name).hashCode() + println("Mocking Resources::getIdentifier(name=\"$name\", defType=\"array\", defPackage=\"$pkg\") -> $anId") + whenever(getIdentifier(eq(name), eq("array"), eq(pkg))).thenReturn(anId) + println("Mocking Resources::getStringArray(id=$anId) -> ${values.asList()}") + whenever(getStringArray(eq(anId))).thenReturn(values) + return anId + } + + @Test + fun testCanResolveTheArrayWithoutImports() { + val values = arrayOf("app.a", "app.b") + val mockId = mResources.mockStringArrayResource(pkg = PACKAGE, name = ROOT_RESOURCE, values = values) + + val actual = mTarget.resolve(/* pkg= */ PACKAGE, /* rootId = */ mockId) + + assertWithMessage("Values are resolved correctly") + .that(actual).containsExactlyElementsIn(values) + } + + @Test + fun testCanResolveTheArrayWithImports() { + val externalValues = arrayOf("ext.a", "ext.b", "#import:$PACKAGE/$SUB_RESOURCE") + mResources.mockStringArrayResource(pkg = EXTERNAL_PACKAGE, name = EXTERNAL_RESOURCE, values = externalValues) + val subValues = arrayOf("sub.a", "sub.b") + mResources.mockStringArrayResource(pkg = PACKAGE, name = SUB_RESOURCE, values = subValues) + val values = arrayOf("app.a", "#import:./$SUB_RESOURCE", "app.b", "#import:$EXTERNAL_PACKAGE/$EXTERNAL_RESOURCE", "app.c") + val mockId = mResources.mockStringArrayResource(pkg = PACKAGE, name = ROOT_RESOURCE, values = values) + + val actual = mTarget.resolve(/* pkg= */ PACKAGE, /* rootId= */ mockId) + + assertWithMessage("Values are resolved correctly") + .that(actual).containsExactlyElementsIn((externalValues + subValues + values) + .filterNot { it.startsWith("#import:") } + .toSet()) + } +} |