diff --git a/services/core/java/com/android/server/compat/overrides/AppCompatOverridesParser.java b/services/core/java/com/android/server/compat/overrides/AppCompatOverridesParser.java new file mode 100644 index 0000000000000..a81213df6fe30 --- /dev/null +++ b/services/core/java/com/android/server/compat/overrides/AppCompatOverridesParser.java @@ -0,0 +1,383 @@ +/* + * 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.compat.overrides; + +import static android.content.pm.PackageManager.MATCH_ANY_USER; + +import static java.util.Collections.emptyMap; +import static java.util.Collections.emptySet; + +import android.app.compat.PackageOverride; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.util.ArrayMap; +import android.util.ArraySet; +import android.util.KeyValueListParser; +import android.util.Slog; + +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.regex.Pattern; + +/** + * A utility class for parsing App Compat Overrides flags. + * + * @hide + */ +final class AppCompatOverridesParser { + /** + * Flag for specifying all compat change IDs owned by a namespace. See {@link + * #parseOwnedChangeIds} for information on how this flag is parsed. + */ + static final String FLAG_OWNED_CHANGE_IDS = "owned_change_ids"; + + /** + * Flag for immediately removing overrides for certain packages and change IDs (from the compat + * platform), as well as stopping to apply them, in case of an emergency. See {@link + * #parseRemoveOverrides} for information on how this flag is parsed. + */ + static final String FLAG_REMOVE_OVERRIDES = "remove_overrides"; + + private static final String TAG = "AppCompatOverridesParser"; + + private static final String WILDCARD_SYMBOL = "*"; + + private static final Pattern BOOLEAN_PATTERN = + Pattern.compile("true|false", Pattern.CASE_INSENSITIVE); + + private static final String WILDCARD_NO_OWNED_CHANGE_IDS_WARNING = + "Wildcard can't be used in '" + FLAG_REMOVE_OVERRIDES + "' flag with an empty " + + FLAG_OWNED_CHANGE_IDS + "' flag"; + + private final PackageManager mPackageManager; + + AppCompatOverridesParser(PackageManager packageManager) { + mPackageManager = packageManager; + } + + /** + * Parses the given {@code configStr} and returns a map from package name to a set of change + * IDs to remove for that package. + * + *

The given {@code configStr} is expected to either be: + * + *

+ * + *

If the given {@code configStr} doesn't match the expected format, an empty map will be + * returned. If a specific change ID isn't a valid long, it will be ignored. + */ + Map> parseRemoveOverrides(String configStr, Set ownedChangeIds) { + if (configStr.isEmpty()) { + return emptyMap(); + } + + Map> result = new ArrayMap<>(); + if (configStr.equals(WILDCARD_SYMBOL)) { + if (ownedChangeIds.isEmpty()) { + Slog.w(TAG, WILDCARD_NO_OWNED_CHANGE_IDS_WARNING); + return emptyMap(); + } + List installedApps = mPackageManager.getInstalledApplications( + MATCH_ANY_USER); + for (ApplicationInfo appInfo : installedApps) { + result.put(appInfo.packageName, ownedChangeIds); + } + return result; + } + + KeyValueListParser parser = new KeyValueListParser(','); + try { + parser.setString(configStr); + } catch (IllegalArgumentException e) { + Slog.w( + TAG, + "Invalid format in '" + FLAG_REMOVE_OVERRIDES + "' flag: " + configStr, e); + return emptyMap(); + } + for (int i = 0; i < parser.size(); i++) { + String packageName = parser.keyAt(i); + String changeIdsStr = parser.getString(packageName, /* def= */ ""); + if (changeIdsStr.equals(WILDCARD_SYMBOL)) { + if (ownedChangeIds.isEmpty()) { + Slog.w(TAG, WILDCARD_NO_OWNED_CHANGE_IDS_WARNING); + continue; + } + result.put(packageName, ownedChangeIds); + } else { + for (String changeIdStr : changeIdsStr.split(":")) { + try { + long changeId = Long.parseLong(changeIdStr); + result.computeIfAbsent(packageName, k -> new ArraySet<>()).add(changeId); + } catch (NumberFormatException e) { + Slog.w( + TAG, + "Invalid change ID in '" + FLAG_REMOVE_OVERRIDES + "' flag: " + + changeIdStr, e); + } + } + } + } + + return result; + } + + + /** + * Parses the given {@code configStr}, that is expected to be a comma separated list of change + * IDs, into a set. + * + *

If any of the change IDs isn't a valid long, it will be ignored. + */ + static Set parseOwnedChangeIds(String configStr) { + if (configStr.isEmpty()) { + return emptySet(); + } + + Set result = new ArraySet<>(); + for (String changeIdStr : configStr.split(",")) { + try { + result.add(Long.parseLong(changeIdStr)); + } catch (NumberFormatException e) { + Slog.w(TAG, + "Invalid change ID in '" + FLAG_OWNED_CHANGE_IDS + "' flag: " + changeIdStr, + e); + } + } + return result; + } + + /** + * Parses the given {@code configStr}, that is expected to be a comma separated list of changes + * overrides, and returns a {@link PackageOverrides}. + * + *

Each change override is in the following format: + * ':::'. If is empty, + * this indicates that any override for the specified change ID should be removed. + * + *

If there are multiple overrides that should be added with the same change ID, the one + * that best fits the given {@code versionCode} is added. + * + *

Any overrides whose change ID is in {@code changeIdsToSkip} are ignored. + * + *

If a change override entry in {@code configStr} is invalid, it will be ignored. If the + * same change ID is both added and removed, i.e., has a change override entry with an empty + * enabled and another with a non-empty enabled, the change ID will only be removed. + */ + static PackageOverrides parsePackageOverrides( + String configStr, long versionCode, Set changeIdsToSkip) { + if (configStr.isEmpty()) { + return new PackageOverrides(); + } + PackageOverrideComparator comparator = new PackageOverrideComparator(versionCode); + Map overridesToAdd = new ArrayMap<>(); + Set overridesToRemove = new ArraySet<>(); + for (String overrideEntryString : configStr.split(",")) { + List changeIdAndVersions = Arrays.asList(overrideEntryString.split(":", 4)); + if (changeIdAndVersions.size() != 4) { + Slog.w(TAG, "Invalid change override entry: " + overrideEntryString); + continue; + } + long changeId; + try { + changeId = Long.parseLong(changeIdAndVersions.get(0)); + } catch (NumberFormatException e) { + Slog.w(TAG, "Invalid change ID in override entry: " + overrideEntryString, e); + continue; + } + + if (changeIdsToSkip.contains(changeId)) { + continue; + } + + String minVersionCodeStr = changeIdAndVersions.get(1); + String maxVersionCodeStr = changeIdAndVersions.get(2); + + String enabledStr = changeIdAndVersions.get(3); + if (enabledStr.isEmpty()) { + if (!minVersionCodeStr.isEmpty() || !maxVersionCodeStr.isEmpty()) { + Slog.w( + TAG, + "min/max version code should be empty if enabled is empty: " + + overrideEntryString); + } + overridesToRemove.add(changeId); + continue; + } + if (!BOOLEAN_PATTERN.matcher(enabledStr).matches()) { + Slog.w(TAG, "Invalid enabled string in override entry: " + overrideEntryString); + continue; + } + boolean enabled = Boolean.parseBoolean(enabledStr); + PackageOverride.Builder overrideBuilder = new PackageOverride.Builder().setEnabled( + enabled); + try { + if (!minVersionCodeStr.isEmpty()) { + overrideBuilder.setMinVersionCode(Long.parseLong(minVersionCodeStr)); + } + if (!maxVersionCodeStr.isEmpty()) { + overrideBuilder.setMaxVersionCode(Long.parseLong(maxVersionCodeStr)); + } + } catch (NumberFormatException e) { + Slog.w(TAG, + "Invalid min/max version code in override entry: " + overrideEntryString, + e); + continue; + } + + try { + PackageOverride override = overrideBuilder.build(); + if (!overridesToAdd.containsKey(changeId) + || comparator.compare(override, overridesToAdd.get(changeId)) < 0) { + overridesToAdd.put(changeId, override); + } + } catch (IllegalArgumentException e) { + Slog.w(TAG, "Failed to build PackageOverride", e); + } + } + + for (Long changeId : overridesToRemove) { + if (overridesToAdd.containsKey(changeId)) { + Slog.w( + TAG, + "Change ID [" + + changeId + + "] is both added and removed in package override flag: " + + configStr); + overridesToAdd.remove(changeId); + } + } + + return new PackageOverrides(overridesToAdd, overridesToRemove); + } + + /** + * A container for a map from change ID to {@link PackageOverride} to add and a set of change + * IDs to remove overrides for. + * + *

The map of overrides to add and the set of overrides to remove are mutually exclusive. + */ + static final class PackageOverrides { + public final Map overridesToAdd; + public final Set overridesToRemove; + + PackageOverrides() { + this(emptyMap(), emptySet()); + } + + PackageOverrides(Map overridesToAdd, Set overridesToRemove) { + this.overridesToAdd = overridesToAdd; + this.overridesToRemove = overridesToRemove; + } + } + + /** + * A {@link Comparator} that compares @link PackageOverride} instances with respect to a + * specified {@code versionCode} as follows: + * + *

+ */ + private static final class PackageOverrideComparator implements Comparator { + private final long mVersionCode; + + PackageOverrideComparator(long versionCode) { + this.mVersionCode = versionCode; + } + + @Override + public int compare(PackageOverride o1, PackageOverride o2) { + // Prefer overrides whose version range contains versionCode. + boolean isVersionInRange1 = isVersionInRange(o1, mVersionCode); + boolean isVersionInRange2 = isVersionInRange(o2, mVersionCode); + if (isVersionInRange1 != isVersionInRange2) { + return isVersionInRange1 ? -1 : 1; + } + + // Otherwise, prefer overrides whose version range is before versionCode. + boolean isVersionAfterRange1 = isVersionAfterRange(o1, mVersionCode); + boolean isVersionAfterRange2 = isVersionAfterRange(o2, mVersionCode); + if (isVersionAfterRange1 != isVersionAfterRange2) { + return isVersionAfterRange1 ? -1 : 1; + } + + // If both overrides' version ranges are either before or after versionCode, prefer + // those whose version range is closer to versionCode. + return Long.compare( + getVersionProximity(o1, mVersionCode), getVersionProximity(o2, mVersionCode)); + } + + /** + * Returns true if the version range in the given {@code override} contains {@code + * versionCode}. + */ + private static boolean isVersionInRange(PackageOverride override, long versionCode) { + return override.getMinVersionCode() <= versionCode + && versionCode <= override.getMaxVersionCode(); + } + + /** + * Returns true if the given {@code versionCode} is strictly after the version range in the + * given {@code override}. + */ + private static boolean isVersionAfterRange(PackageOverride override, long versionCode) { + return override.getMaxVersionCode() < versionCode; + } + + /** + * Returns true if the given {@code versionCode} is strictly before the version range in the + * given {@code override}. + */ + private static boolean isVersionBeforeRange(PackageOverride override, long versionCode) { + return override.getMinVersionCode() > versionCode; + } + + /** + * In case the given {@code versionCode} is strictly before or after the version range in + * the given {@code override}, returns the distance from it, otherwise returns zero. + */ + private static long getVersionProximity(PackageOverride override, long versionCode) { + if (isVersionAfterRange(override, versionCode)) { + return versionCode - override.getMaxVersionCode(); + } + if (isVersionBeforeRange(override, versionCode)) { + return override.getMinVersionCode() - versionCode; + } + + // Version is in range. Note that when two overrides have a zero version proximity + // they will be ordered arbitrarily. + return 0; + } + } +} diff --git a/services/core/java/com/android/server/compat/overrides/AppCompatOverridesService.java b/services/core/java/com/android/server/compat/overrides/AppCompatOverridesService.java new file mode 100644 index 0000000000000..a81b18b690c45 --- /dev/null +++ b/services/core/java/com/android/server/compat/overrides/AppCompatOverridesService.java @@ -0,0 +1,279 @@ +/* + * 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.compat.overrides; + +import static android.content.pm.PackageManager.MATCH_ANY_USER; +import static android.provider.DeviceConfig.NAMESPACE_APP_COMPAT_OVERRIDES; + +import static com.android.server.compat.overrides.AppCompatOverridesParser.FLAG_OWNED_CHANGE_IDS; +import static com.android.server.compat.overrides.AppCompatOverridesParser.FLAG_REMOVE_OVERRIDES; + +import static java.util.Collections.emptySet; + +import android.annotation.Nullable; +import android.app.compat.PackageOverride; +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.provider.DeviceConfig; +import android.provider.DeviceConfig.Properties; +import android.util.ArraySet; +import android.util.Slog; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.compat.CompatibilityOverrideConfig; +import com.android.internal.compat.CompatibilityOverridesToRemoveConfig; +import com.android.internal.compat.IPlatformCompat; +import com.android.server.SystemService; +import com.android.server.compat.overrides.AppCompatOverridesParser.PackageOverrides; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Service for applying per-app compat overrides delivered via Device Config. + * + *

The service listens both on changes to supported Device Config namespaces and on package + * added/changed/removed events, and applies overrides accordingly. + * + * @hide + */ +public final class AppCompatOverridesService { + private static final String TAG = "AppCompatOverridesService"; + + private static final List SUPPORTED_NAMESPACES = Arrays.asList( + NAMESPACE_APP_COMPAT_OVERRIDES); + + private final Context mContext; + private final PackageManager mPackageManager; + private final IPlatformCompat mPlatformCompat; + private final List mSupportedNamespaces; + private final List mDeviceConfigListeners; + private final AppCompatOverridesParser mOverridesParser; + + private AppCompatOverridesService(Context context) { + this(context, IPlatformCompat.Stub.asInterface( + ServiceManager.getService(Context.PLATFORM_COMPAT_SERVICE)), SUPPORTED_NAMESPACES); + } + + @VisibleForTesting + AppCompatOverridesService(Context context, IPlatformCompat platformCompat, + List supportedNamespaces) { + mContext = context; + mPackageManager = mContext.getPackageManager(); + mPlatformCompat = platformCompat; + mSupportedNamespaces = supportedNamespaces; + mDeviceConfigListeners = new ArrayList<>(); + mOverridesParser = new AppCompatOverridesParser(mPackageManager); + } + + @Override + public void finalize() { + unregisterDeviceConfigListeners(); + } + + @VisibleForTesting + void registerDeviceConfigListeners() { + for (String namespace : mSupportedNamespaces) { + DeviceConfigListener listener = new DeviceConfigListener(namespace); + DeviceConfig.addOnPropertiesChangedListener(namespace, mContext.getMainExecutor(), + listener); + mDeviceConfigListeners.add(listener); + } + } + + private void unregisterDeviceConfigListeners() { + for (DeviceConfigListener listener : mDeviceConfigListeners) { + DeviceConfig.removeOnPropertiesChangedListener(listener); + } + } + + /** + * Same as {@link #applyOverrides(Properties, Map)} except all properties of the given {@code + * namespace} are fetched via {@link DeviceConfig#getProperties}. + */ + private void applyAllOverrides(String namespace, + Map> packageToChangeIdsToSkip) { + applyOverrides(DeviceConfig.getProperties(namespace), packageToChangeIdsToSkip); + } + + /** + * Iterates all package override flags in the given {@code properties}, and for each flag whose + * package is installed on the device, parses its value and applies the overrides in it with + * respect to the package's current installed version. + */ + private void applyOverrides(Properties properties, + Map> packageToChangeIdsToSkip) { + Set packageNames = new ArraySet<>(properties.getKeyset()); + packageNames.remove(FLAG_OWNED_CHANGE_IDS); + packageNames.remove(FLAG_REMOVE_OVERRIDES); + for (String packageName : packageNames) { + Long versionCode = getVersionCodeOrNull(packageName); + if (versionCode == null) { + // Package isn't installed yet. + continue; + } + + applyPackageOverrides(properties.getString(packageName, /* defaultValue= */ ""), + packageName, versionCode, + packageToChangeIdsToSkip.getOrDefault(packageName, emptySet())); + } + } + + /** + * Calls {@link AppCompatOverridesParser#parsePackageOverrides} on the given arguments, adds the + * resulting {@link PackageOverrides#overridesToAdd} via {@link + * IPlatformCompat#putOverridesOnReleaseBuilds}, and removes the resulting {@link + * PackageOverrides#overridesToRemove} via {@link + * IPlatformCompat#removeOverridesOnReleaseBuilds}. + */ + private void applyPackageOverrides(String configStr, String packageName, + long versionCode, Set changeIdsToSkip) { + PackageOverrides packageOverrides = AppCompatOverridesParser.parsePackageOverrides( + configStr, versionCode, changeIdsToSkip); + putPackageOverrides(packageName, packageOverrides.overridesToAdd); + removePackageOverrides(packageName, packageOverrides.overridesToRemove); + } + + /** + * Calls {@link IPlatformCompat#removeOverridesOnReleaseBuilds} on each package name and + * respective change IDs in {@code overridesToRemove}. + */ + private void removeOverrides(Map> overridesToRemove) { + for (Map.Entry> packageNameAndOverrides : overridesToRemove.entrySet()) { + removePackageOverrides(packageNameAndOverrides.getKey(), + packageNameAndOverrides.getValue()); + } + } + + /** + * Fetches the value of {@link AppCompatOverridesParser#FLAG_REMOVE_OVERRIDES} for the given + * {@code namespace} and parses it into a map from package name to a set of change IDs to + * remove for that package. + */ + private Map> getOverridesToRemove(String namespace) { + return mOverridesParser.parseRemoveOverrides( + DeviceConfig.getString(namespace, FLAG_REMOVE_OVERRIDES, /* defaultValue= */ ""), + getOwnedChangeIds(namespace)); + } + + /** + * Fetches the value of {@link AppCompatOverridesParser#FLAG_OWNED_CHANGE_IDS} for the given + * {@code namespace} and parses it into a set of change IDs. + */ + private static Set getOwnedChangeIds(String namespace) { + return AppCompatOverridesParser.parseOwnedChangeIds( + DeviceConfig.getString(namespace, FLAG_OWNED_CHANGE_IDS, /* defaultValue= */ "")); + } + + private void putPackageOverrides(String packageName, + Map overridesToAdd) { + if (overridesToAdd.isEmpty()) { + return; + } + CompatibilityOverrideConfig config = new CompatibilityOverrideConfig(overridesToAdd); + try { + mPlatformCompat.putOverridesOnReleaseBuilds(config, packageName); + } catch (RemoteException e) { + Slog.w(TAG, "Failed to call IPlatformCompat#putOverridesOnReleaseBuilds", e); + } + } + + private void removePackageOverrides(String packageName, Set overridesToRemove) { + if (overridesToRemove.isEmpty()) { + return; + } + CompatibilityOverridesToRemoveConfig config = new CompatibilityOverridesToRemoveConfig( + overridesToRemove); + try { + mPlatformCompat.removeOverridesOnReleaseBuilds(config, packageName); + } catch (RemoteException e) { + Slog.w(TAG, "Failed to call IPlatformCompat#removeOverridesOnReleaseBuilds", e); + } + } + + @Nullable + private Long getVersionCodeOrNull(String packageName) { + try { + ApplicationInfo applicationInfo = mPackageManager.getApplicationInfo(packageName, + MATCH_ANY_USER); + return applicationInfo.longVersionCode; + } catch (PackageManager.NameNotFoundException e) { + // Package isn't installed yet. + return null; + } + } + + /** + * SystemService lifecycle for AppCompatOverridesService. + * + * @hide + */ + public static final class Lifecycle extends SystemService { + private AppCompatOverridesService mService; + + public Lifecycle(Context context) { + super(context); + } + + @Override + public void onStart() { + mService = new AppCompatOverridesService(getContext()); + mService.registerDeviceConfigListeners(); + } + } + + /** + * A {@link DeviceConfig.OnPropertiesChangedListener} that listens on changes to a given + * namespace and adds/removes overrides according to the changed flags. + */ + private final class DeviceConfigListener implements DeviceConfig.OnPropertiesChangedListener { + private final String mNamespace; + + private DeviceConfigListener(String namespace) { + mNamespace = namespace; + } + + @Override + public void onPropertiesChanged(Properties properties) { + boolean removeOverridesFlagChanged = properties.getKeyset().contains( + FLAG_REMOVE_OVERRIDES); + boolean ownedChangedIdsFlagChanged = properties.getKeyset().contains( + FLAG_OWNED_CHANGE_IDS); + + Map> overridesToRemove = getOverridesToRemove(mNamespace); + if (removeOverridesFlagChanged || ownedChangedIdsFlagChanged) { + // In both cases it's possible that overrides that weren't removed before should + // now be removed. + removeOverrides(overridesToRemove); + } + + if (removeOverridesFlagChanged) { + // We need to re-apply all overrides in the namespace since the remove overrides + // flag might have blocked some of them from being applied before. + applyAllOverrides(mNamespace, overridesToRemove); + } else { + applyOverrides(properties, overridesToRemove); + } + } + } +} diff --git a/services/core/java/com/android/server/compat/overrides/TEST_MAPPING b/services/core/java/com/android/server/compat/overrides/TEST_MAPPING new file mode 100644 index 0000000000000..4b8f08ec9164c --- /dev/null +++ b/services/core/java/com/android/server/compat/overrides/TEST_MAPPING @@ -0,0 +1,12 @@ +{ + "presubmit": [ + { + "name": "FrameworksMockingServicesTests", + "options": [ + { + "include-filter": "com.android.server.compat.overrides" + } + ] + } + ] +} diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java index aca7cc9070b17..508606051ab57 100644 --- a/services/java/com/android/server/SystemServer.java +++ b/services/java/com/android/server/SystemServer.java @@ -378,6 +378,8 @@ public final class SystemServer implements Dumpable { "com.android.server.connectivity.IpConnectivityMetrics"; private static final String MEDIA_COMMUNICATION_SERVICE_CLASS = "com.android.server.media.MediaCommunicationService"; + private static final String APP_COMPAT_OVERRIDES_SERVICE_CLASS = + "com.android.server.compat.overrides.AppCompatOverridesService$Lifecycle"; private static final String ROLE_SERVICE_CLASS = "com.android.role.RoleService"; private static final String GAME_MANAGER_SERVICE_CLASS = @@ -2655,6 +2657,10 @@ public final class SystemServer implements Dumpable { mSystemServiceManager.startService(MEDIA_COMMUNICATION_SERVICE_CLASS); t.traceEnd(); + t.traceBegin("AppCompatOverridesService"); + mSystemServiceManager.startService(APP_COMPAT_OVERRIDES_SERVICE_CLASS); + t.traceEnd(); + ConcurrentUtils.waitForFutureNoInterrupt(mBlobStoreServiceStart, START_BLOB_STORE_SERVICE); diff --git a/services/tests/mockingservicestests/src/com/android/server/compat/overrides/AppCompatOverridesParserTest.java b/services/tests/mockingservicestests/src/com/android/server/compat/overrides/AppCompatOverridesParserTest.java new file mode 100644 index 0000000000000..bf97042d7b884 --- /dev/null +++ b/services/tests/mockingservicestests/src/com/android/server/compat/overrides/AppCompatOverridesParserTest.java @@ -0,0 +1,302 @@ +/* + * 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.compat.overrides; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.when; + +import static java.util.Collections.emptySet; + +import android.app.compat.PackageOverride; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.platform.test.annotations.Presubmit; +import android.util.ArraySet; + +import androidx.test.filters.SmallTest; + +import com.android.server.compat.overrides.AppCompatOverridesParser.PackageOverrides; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import java.util.Arrays; +import java.util.Map; +import java.util.Set; + +/** + * Test class for {@link AppCompatOverridesParser}. + * + * Build/Install/Run: + * atest FrameworksMockingServicesTests:AppCompatOverridesParserTest + */ +@RunWith(MockitoJUnitRunner.class) +@SmallTest +@Presubmit +public class AppCompatOverridesParserTest { + private static final String PACKAGE_1 = "com.android.test1"; + private static final String PACKAGE_2 = "com.android.test2"; + private static final String PACKAGE_3 = "com.android.test3"; + private static final String PACKAGE_4 = "com.android.test4"; + + private AppCompatOverridesParser mParser; + + @Mock + private PackageManager mPackageManager; + + @Before + public void setUp() throws Exception { + mParser = new AppCompatOverridesParser(mPackageManager); + } + + @Test + public void parseRemoveOverrides_emptyConfig_returnsEmpty() { + Set ownedChangeIds = new ArraySet<>(Arrays.asList(123L, 456L)); + + assertThat(mParser.parseRemoveOverrides("", ownedChangeIds)).isEmpty(); + } + + @Test + public void parseRemoveOverrides_configHasWildcardNoOwnedChangeIds_returnsEmpty() { + when(mPackageManager.getInstalledApplications(anyInt())) + .thenReturn(Arrays.asList(createAppInfo(PACKAGE_1), createAppInfo(PACKAGE_2))); + + assertThat(mParser.parseRemoveOverrides("*", /* ownedChangeIds= */ emptySet())).isEmpty(); + } + + @Test + public void parseRemoveOverrides_configHasWildcard_returnsAllInstalledPackagesToAllOwnedIds() { + Set ownedChangeIds = new ArraySet<>(Arrays.asList(123L, 456L)); + when(mPackageManager.getInstalledApplications(anyInt())) + .thenReturn(Arrays.asList(createAppInfo(PACKAGE_1), createAppInfo(PACKAGE_2), + createAppInfo(PACKAGE_3))); + + Map> result = mParser.parseRemoveOverrides("*", ownedChangeIds); + + assertThat(result).hasSize(3); + assertThat(result.get(PACKAGE_1)).containsExactly(123L, 456L); + assertThat(result.get(PACKAGE_2)).containsExactly(123L, 456L); + assertThat(result.get(PACKAGE_3)).containsExactly(123L, 456L); + } + + @Test + public void parseRemoveOverrides_configHasInvalidWildcardSymbol_returnsEmpty() { + Set ownedChangeIds = new ArraySet<>(Arrays.asList(123L, 456L)); + when(mPackageManager.getInstalledApplications(anyInt())).thenReturn( + Arrays.asList(createAppInfo(PACKAGE_1), createAppInfo(PACKAGE_2))); + + assertThat(mParser.parseRemoveOverrides("**", ownedChangeIds)).isEmpty(); + } + + @Test + public void parseRemoveOverrides_configHasSingleEntry_returnsPackageToChangeIds() { + Map> result = mParser.parseRemoveOverrides( + PACKAGE_1 + "=12:34", /* ownedChangeIds= */ emptySet()); + + assertThat(result).hasSize(1); + assertThat(result.get(PACKAGE_1)).containsExactly(12L, 34L); + } + + @Test + public void parseRemoveOverrides_configHasMultipleEntries_returnsPackagesToChangeIds() { + Set ownedChangeIds = new ArraySet<>(Arrays.asList(12L, 34L, 56L, 78L)); + + Map> result = mParser.parseRemoveOverrides( + PACKAGE_1 + "=12," + PACKAGE_2 + "=*," + PACKAGE_3 + "=12:56:78," + PACKAGE_4 + + "=", ownedChangeIds); + + assertThat(result).hasSize(3); + assertThat(result.get(PACKAGE_1)).containsExactly(12L); + assertThat(result.get(PACKAGE_2)).containsExactly(12L, 34L, 56L, 78L); + assertThat(result.get(PACKAGE_3)).containsExactly(12L, 56L, 78L); + } + + @Test + public void parseRemoveOverrides_configHasPackageWithWildcardNoOwnedId_returnsWithoutPackage() { + Map> result = mParser.parseRemoveOverrides( + PACKAGE_1 + "=*," + PACKAGE_2 + "=12", /* ownedChangeIds= */ emptySet()); + + assertThat(result).hasSize(1); + assertThat(result.get(PACKAGE_2)).containsExactly(12L); + } + + @Test + public void parseRemoveOverrides_configHasInvalidKeyValueListFormat_returnsEmpty() { + Set ownedChangeIds = new ArraySet<>(Arrays.asList(12L, 34L)); + + assertThat(mParser.parseRemoveOverrides( + PACKAGE_1 + "=12," + PACKAGE_2 + ">34", ownedChangeIds)).isEmpty(); + } + + + @Test + public void parseRemoveOverrides_configHasInvalidChangeIds_returnsWithoutInvalidChangeIds() { + Map> result = mParser.parseRemoveOverrides( + PACKAGE_1 + "=12," + PACKAGE_2 + "=12:56L:78," + PACKAGE_3 + + "=34L", /* ownedChangeIds= */ emptySet()); + + assertThat(result).hasSize(2); + assertThat(result.get(PACKAGE_1)).containsExactly(12L); + assertThat(result.get(PACKAGE_2)).containsExactly(12L, 78L); + } + + @Test + public void parseOwnedChangeIds_emptyConfig_returnsEmpty() { + assertThat(AppCompatOverridesParser.parseOwnedChangeIds("")).isEmpty(); + } + + @Test + public void parseOwnedChangeIds_configHasSingleChangeId_returnsChangeId() { + assertThat(AppCompatOverridesParser.parseOwnedChangeIds("123")).containsExactly(123L); + } + + @Test + public void parseOwnedChangeIds_configHasMultipleChangeIds_returnsChangeIds() { + assertThat(AppCompatOverridesParser.parseOwnedChangeIds("12,34,56")).containsExactly(12L, + 34L, 56L); + } + + @Test + public void parseOwnedChangeIds_configHasInvalidChangeIds_returnsWithoutInvalidChangeIds() { + // We add a valid entry before and after the invalid ones to make sure they are applied. + assertThat(AppCompatOverridesParser.parseOwnedChangeIds("12,C34,56")).containsExactly(12L, + 56L); + } + + @Test + public void parsePackageOverrides_emptyConfig_returnsEmpty() { + PackageOverrides result = AppCompatOverridesParser.parsePackageOverrides(/* configStr= */ + "", /* versionCode= */ 0, /* changeIdsToSkip= */ emptySet()); + + assertThat(result.overridesToAdd).isEmpty(); + assertThat(result.overridesToRemove).isEmpty(); + } + + @Test + public void parsePackageOverrides_configWithSingleOverride_returnsOverride() { + PackageOverrides result = AppCompatOverridesParser.parsePackageOverrides(/* configStr= */ + "123:::true", /* versionCode= */ 5, /* changeIdsToSkip= */ emptySet()); + + assertThat(result.overridesToAdd).hasSize(1); + assertThat(result.overridesToAdd.get(123L)).isEqualTo( + new PackageOverride.Builder().setEnabled(true).build()); + } + + @Test + public void parsePackageOverrides_configWithMultipleOverridesToAdd_returnsOverrides() { + PackageOverrides result = AppCompatOverridesParser.parsePackageOverrides(/* configStr= */ + "910:3:4:false,78:10::false,12:::false,34:1:2:true,34:10::true,56::2:true," + + "56:3:4:false,34:4:8:true,78:6:7:true,910:5::true,1112::5:true," + + "56:6::true,1112:6:7:false", /* versionCode= */ + 5, /* changeIdsToSkip= */ emptySet()); + + assertThat(result.overridesToAdd).hasSize(6); + assertThat(result.overridesToAdd.get(12L)).isEqualTo( + new PackageOverride.Builder().setEnabled(false).build()); + assertThat(result.overridesToAdd.get(34L)).isEqualTo( + new PackageOverride.Builder().setMinVersionCode(4).setMaxVersionCode(8).setEnabled( + true).build()); + assertThat(result.overridesToAdd.get(56L)).isEqualTo( + new PackageOverride.Builder().setMinVersionCode(3).setMaxVersionCode(4).setEnabled( + false).build()); + assertThat(result.overridesToAdd.get(78L)).isEqualTo( + new PackageOverride.Builder().setMinVersionCode(6).setMaxVersionCode(7).setEnabled( + true).build()); + assertThat(result.overridesToAdd.get(910L)).isEqualTo( + new PackageOverride.Builder().setMinVersionCode(5).setEnabled(true).build()); + assertThat(result.overridesToAdd.get(1112L)).isEqualTo( + new PackageOverride.Builder().setMaxVersionCode(5).setEnabled(true).build()); + assertThat(result.overridesToRemove).isEmpty(); + } + + @Test + public void parsePackageOverrides_configWithMultipleOverridesToRemove_returnsOverrides() { + PackageOverrides result = AppCompatOverridesParser.parsePackageOverrides(/* configStr= */ + "12:::,34:1:2:", /* versionCode= */ 5, /* changeIdsToSkip= */ emptySet()); + + assertThat(result.overridesToRemove).containsExactly(12L, 34L); + assertThat(result.overridesToAdd).isEmpty(); + } + + @Test + public void parsePackageOverrides_configWithBothOverridesToAddAndRemove_returnsOverrides() { + // Note that change 56 is both added and removed, therefore it will only be removed. + PackageOverrides result = AppCompatOverridesParser.parsePackageOverrides(/* configStr= */ + "56:::,12:::true,34:::,56:3:7:true", /* versionCode= */ 5, /* changeIdsToSkip= */ + emptySet()); + + assertThat(result.overridesToAdd).hasSize(1); + assertThat(result.overridesToAdd.get(12L)).isEqualTo( + new PackageOverride.Builder().setEnabled(true).build()); + assertThat(result.overridesToRemove).containsExactly(34L, 56L); + } + + @Test + public void parsePackageOverrides_changeIdsToSkipSpecified_returnsWithoutChangeIdsToSkip() { + ArraySet changeIdsToSkip = new ArraySet<>(); + changeIdsToSkip.add(34L); + changeIdsToSkip.add(56L); + changeIdsToSkip.add(910L); + PackageOverrides result = AppCompatOverridesParser.parsePackageOverrides(/* configStr= */ + "12:::true,34:::,56:3:7:true,78:::", /* versionCode= */ 5, changeIdsToSkip); + + assertThat(result.overridesToAdd).hasSize(1); + assertThat(result.overridesToAdd.get(12L)).isEqualTo( + new PackageOverride.Builder().setEnabled(true).build()); + assertThat(result.overridesToRemove).containsExactly(78L); + } + + @Test + public void parsePackageOverrides_changeIdsToSkipContainsAllIds_returnsEmpty() { + ArraySet changeIdsToSkip = new ArraySet<>(); + changeIdsToSkip.add(12L); + changeIdsToSkip.add(34L); + PackageOverrides result = AppCompatOverridesParser.parsePackageOverrides(/* configStr= */ + "12:::true,34:::", /* versionCode= */ 5, changeIdsToSkip); + + assertThat(result.overridesToAdd).isEmpty(); + assertThat(result.overridesToRemove).isEmpty(); + } + + @Test + public void parsePackageOverrides_someOverridesAreInvalid_returnsWithoutInvalidOverrides() { + // We add a valid entry before and after the invalid ones to make sure they are applied. + PackageOverrides result = AppCompatOverridesParser.parsePackageOverrides(/* configStr= */ + "12:::True,56:1:2:FALSE,56:3:true,78:4:8:true:,C1:::true,910:::no," + + "1112:1:ten:true,1112:one:10:true,,1314:7:3:false,34:one:ten:", + /* versionCode= */ 5, /* changeIdsToSkip= */ emptySet()); + + assertThat(result.overridesToAdd).hasSize(2); + assertThat(result.overridesToAdd.get(12L)).isEqualTo( + new PackageOverride.Builder().setEnabled(true).build()); + assertThat(result.overridesToAdd.get(56L)).isEqualTo( + new PackageOverride.Builder().setMinVersionCode(1).setMaxVersionCode(2).setEnabled( + false).build()); + assertThat(result.overridesToRemove).containsExactly(34L); + } + + private static ApplicationInfo createAppInfo(String packageName) { + ApplicationInfo appInfo = new ApplicationInfo(); + appInfo.packageName = packageName; + return appInfo; + } +} diff --git a/services/tests/mockingservicestests/src/com/android/server/compat/overrides/AppCompatOverridesServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/compat/overrides/AppCompatOverridesServiceTest.java new file mode 100644 index 0000000000000..d3f04f30a0b8e --- /dev/null +++ b/services/tests/mockingservicestests/src/com/android/server/compat/overrides/AppCompatOverridesServiceTest.java @@ -0,0 +1,380 @@ +/* + * 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.compat.overrides; + +import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; +import static com.android.server.compat.overrides.AppCompatOverridesParser.FLAG_OWNED_CHANGE_IDS; +import static com.android.server.compat.overrides.AppCompatOverridesParser.FLAG_REMOVE_OVERRIDES; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.when; + +import android.app.compat.PackageOverride; +import android.content.Context; +import android.content.ContextWrapper; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.os.RemoteException; +import android.platform.test.annotations.Presubmit; +import android.provider.DeviceConfig; +import android.provider.DeviceConfig.Properties; + +import androidx.test.InstrumentationRegistry; +import androidx.test.filters.SmallTest; + +import com.android.internal.compat.CompatibilityOverrideConfig; +import com.android.internal.compat.CompatibilityOverridesToRemoveConfig; +import com.android.internal.compat.IPlatformCompat; +import com.android.server.testables.TestableDeviceConfig.TestableDeviceConfigRule; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Executor; + +/** + * Test class for {@link AppCompatOverridesService}. + * + * Build/Install/Run: + * atest FrameworksMockingServicesTests:AppCompatOverridesServiceTest + */ +@RunWith(MockitoJUnitRunner.class) +@SmallTest +@Presubmit +public class AppCompatOverridesServiceTest { + private static final String NAMESPACE_1 = "namespace_1"; + private static final List SUPPORTED_NAMESPACES = Arrays.asList(NAMESPACE_1); + + private static final String PACKAGE_1 = "com.android.test1"; + private static final String PACKAGE_2 = "com.android.test2"; + private static final String PACKAGE_3 = "com.android.test3"; + private static final String PACKAGE_4 = "com.android.test4"; + private static final String PACKAGE_5 = "com.android.test5"; + + private MockContext mMockContext; + private AppCompatOverridesService mService; + + @Mock + private PackageManager mPackageManager; + @Mock + private IPlatformCompat mPlatformCompat; + + @Captor + private ArgumentCaptor mOverridesToAddConfigCaptor; + @Captor + private ArgumentCaptor mOverridesToRemoveConfigCaptor; + + @Rule + public TestableDeviceConfigRule mDeviceConfigRule = new TestableDeviceConfigRule(); + + class MockContext extends ContextWrapper { + MockContext(Context base) { + super(base); + } + + @Override + public PackageManager getPackageManager() { + return mPackageManager; + } + + @Override + public Executor getMainExecutor() { + // Run on current thread + return Runnable::run; + } + } + + @Before + public void setUp() throws Exception { + mMockContext = new MockContext( + InstrumentationRegistry.getInstrumentation().getTargetContext()); + mService = new AppCompatOverridesService(mMockContext, mPlatformCompat, + SUPPORTED_NAMESPACES); + } + + @Test + public void onPropertiesChanged_removeOverridesFlagNotSet_appliesPackageOverrides() + throws Exception { + mockGetApplicationInfo(PACKAGE_1, /* versionCode= */ 3); + mockGetApplicationInfoNotInstalled(PACKAGE_2); + mockGetApplicationInfo(PACKAGE_3, /* versionCode= */ 10); + mockGetApplicationInfo(PACKAGE_4, /* versionCode= */ 1); + mockGetApplicationInfo(PACKAGE_5, /* versionCode= */ 1); + + mService.registerDeviceConfigListeners(); + DeviceConfig.setProperties(new Properties.Builder(NAMESPACE_1) + .setString(PACKAGE_1, "123:::true,456::1:false,456:2::true") + .setString(PACKAGE_2, "123:::true") + .setString(PACKAGE_3, "123:1:9:true,123:10:11:false,123:11::true,456:::") + .setString(PACKAGE_4, "") + .setString(PACKAGE_5, "123:::,789:::") + .setString(FLAG_OWNED_CHANGE_IDS, "123,456,789").build()); + + Map addedOverrides; + // Package 1 + verify(mPlatformCompat).putOverridesOnReleaseBuilds(mOverridesToAddConfigCaptor.capture(), + eq(PACKAGE_1)); + verify(mPlatformCompat, never()).removeOverridesOnReleaseBuilds( + any(CompatibilityOverridesToRemoveConfig.class), eq(PACKAGE_1)); + addedOverrides = mOverridesToAddConfigCaptor.getValue().overrides; + assertThat(addedOverrides).hasSize(2); + assertThat(addedOverrides.get(123L)).isEqualTo( + new PackageOverride.Builder().setEnabled(true).build()); + assertThat(addedOverrides.get(456L)).isEqualTo( + new PackageOverride.Builder().setMinVersionCode(2).setEnabled(true).build()); + // Package 2 + verify(mPlatformCompat, never()).putOverridesOnReleaseBuilds( + any(CompatibilityOverrideConfig.class), eq(PACKAGE_2)); + verify(mPlatformCompat, never()).removeOverridesOnReleaseBuilds( + any(CompatibilityOverridesToRemoveConfig.class), eq(PACKAGE_2)); + // Package 3 + verify(mPlatformCompat).putOverridesOnReleaseBuilds(mOverridesToAddConfigCaptor.capture(), + eq(PACKAGE_3)); + verify(mPlatformCompat).removeOverridesOnReleaseBuilds( + mOverridesToRemoveConfigCaptor.capture(), eq(PACKAGE_3)); + addedOverrides = mOverridesToAddConfigCaptor.getValue().overrides; + assertThat(addedOverrides).hasSize(1); + assertThat(addedOverrides.get(123L)).isEqualTo( + new PackageOverride.Builder().setMinVersionCode(10).setMaxVersionCode( + 11).setEnabled(false).build()); + assertThat(mOverridesToRemoveConfigCaptor.getValue().changeIds).containsExactly(456L); + // Package 4 + verify(mPlatformCompat, never()).putOverridesOnReleaseBuilds( + any(CompatibilityOverrideConfig.class), eq(PACKAGE_4)); + verify(mPlatformCompat, never()).removeOverridesOnReleaseBuilds( + any(CompatibilityOverridesToRemoveConfig.class), eq(PACKAGE_4)); + // Package 5 + verify(mPlatformCompat, never()).putOverridesOnReleaseBuilds( + any(CompatibilityOverrideConfig.class), eq(PACKAGE_5)); + verify(mPlatformCompat).removeOverridesOnReleaseBuilds( + mOverridesToRemoveConfigCaptor.capture(), eq(PACKAGE_5)); + assertThat(mOverridesToRemoveConfigCaptor.getValue().changeIds).containsExactly(123L, 789L); + } + + @Test + public void onPropertiesChanged_removeOverridesFlagSetBefore_skipsOverridesToRemove() + throws Exception { + DeviceConfig.setProperties(new Properties.Builder(NAMESPACE_1) + .setString(FLAG_REMOVE_OVERRIDES, PACKAGE_1 + "=123:456," + PACKAGE_2 + "=123") + .setString(PACKAGE_1, "123:::true") + .setString(PACKAGE_4, "123:::true").build()); + mockGetApplicationInfo(PACKAGE_1, /* versionCode= */ 0); + mockGetApplicationInfo(PACKAGE_2, /* versionCode= */ 0); + mockGetApplicationInfo(PACKAGE_3, /* versionCode= */ 0); + + mService.registerDeviceConfigListeners(); + DeviceConfig.setProperties(new Properties.Builder(NAMESPACE_1) + .setString(PACKAGE_1, "123:::true,456:::,789:::false") + .setString(PACKAGE_2, "123:::true") + .setString(PACKAGE_3, "456:::true").build()); + + // Package 1 + verify(mPlatformCompat).putOverridesOnReleaseBuilds(mOverridesToAddConfigCaptor.capture(), + eq(PACKAGE_1)); + verify(mPlatformCompat, never()).removeOverridesOnReleaseBuilds( + any(CompatibilityOverridesToRemoveConfig.class), eq(PACKAGE_1)); + assertThat(mOverridesToAddConfigCaptor.getValue().overrides.keySet()).containsExactly(789L); + // Package 2 + verify(mPlatformCompat, never()).putOverridesOnReleaseBuilds( + any(CompatibilityOverrideConfig.class), eq(PACKAGE_2)); + verify(mPlatformCompat, never()).removeOverridesOnReleaseBuilds( + any(CompatibilityOverridesToRemoveConfig.class), eq(PACKAGE_2)); + // Package 3 + verify(mPlatformCompat).putOverridesOnReleaseBuilds(mOverridesToAddConfigCaptor.capture(), + eq(PACKAGE_3)); + verify(mPlatformCompat, never()).removeOverridesOnReleaseBuilds( + any(CompatibilityOverridesToRemoveConfig.class), eq(PACKAGE_3)); + assertThat(mOverridesToAddConfigCaptor.getValue().overrides.keySet()).containsExactly(456L); + // Package 4 (not applied because it hasn't changed after the listener was added) + verify(mPlatformCompat, never()).putOverridesOnReleaseBuilds( + any(CompatibilityOverrideConfig.class), eq(PACKAGE_4)); + verify(mPlatformCompat, never()).removeOverridesOnReleaseBuilds( + any(CompatibilityOverridesToRemoveConfig.class), eq(PACKAGE_4)); + } + + @Test + public void onPropertiesChanged_removeOverridesFlagChangedNoPackageOverridesFlags_removesOnly() + throws Exception { + mService.registerDeviceConfigListeners(); + DeviceConfig.setProperties(new Properties.Builder(NAMESPACE_1) + .setString(FLAG_REMOVE_OVERRIDES, + PACKAGE_1 + "=123:456," + PACKAGE_2 + "=789").build()); + + // Package 1 + verify(mPlatformCompat).removeOverridesOnReleaseBuilds( + mOverridesToRemoveConfigCaptor.capture(), eq(PACKAGE_1)); + assertThat(mOverridesToRemoveConfigCaptor.getValue().changeIds).containsExactly(123L, 456L); + // Package 2 + verify(mPlatformCompat).removeOverridesOnReleaseBuilds( + mOverridesToRemoveConfigCaptor.capture(), eq(PACKAGE_2)); + assertThat(mOverridesToRemoveConfigCaptor.getValue().changeIds).containsExactly(789L); + } + + @Test + public void onPropertiesChanged_removeOverridesFlagAndSomePackageOverrideFlagsChanged_ok() + throws Exception { + DeviceConfig.setProperties(new Properties.Builder(NAMESPACE_1) + .setString(FLAG_REMOVE_OVERRIDES, PACKAGE_1 + "=123:456") + .setString(PACKAGE_1, "123:::true,456:::,789:::false") + .setString(PACKAGE_3, "456:::false,789:::true").build()); + mockGetApplicationInfo(PACKAGE_1, /* versionCode= */ 0); + mockGetApplicationInfo(PACKAGE_2, /* versionCode= */ 0); + mockGetApplicationInfo(PACKAGE_3, /* versionCode= */ 0); + + mService.registerDeviceConfigListeners(); + DeviceConfig.setProperties(new Properties.Builder(NAMESPACE_1) + .setString(FLAG_REMOVE_OVERRIDES, PACKAGE_2 + "=123," + PACKAGE_3 + "=789") + .setString(PACKAGE_2, "123:::true,456:::").build()); + + // Package 1 + verify(mPlatformCompat).putOverridesOnReleaseBuilds(mOverridesToAddConfigCaptor.capture(), + eq(PACKAGE_1)); + verify(mPlatformCompat).removeOverridesOnReleaseBuilds( + mOverridesToRemoveConfigCaptor.capture(), eq(PACKAGE_1)); + assertThat(mOverridesToAddConfigCaptor.getValue().overrides.keySet()).containsExactly(123L, + 789L); + assertThat(mOverridesToRemoveConfigCaptor.getValue().changeIds).containsExactly(456L); + // Package 2 + verify(mPlatformCompat, never()).putOverridesOnReleaseBuilds( + any(CompatibilityOverrideConfig.class), eq(PACKAGE_2)); + verify(mPlatformCompat, times(2)).removeOverridesOnReleaseBuilds( + mOverridesToRemoveConfigCaptor.capture(), eq(PACKAGE_2)); + List configs = + mOverridesToRemoveConfigCaptor.getAllValues(); + assertThat(configs.size()).isAtLeast(2); + assertThat(configs.get(configs.size() - 2).changeIds).containsExactly(123L); + assertThat(configs.get(configs.size() - 1).changeIds).containsExactly(456L); + // Package 3 + verify(mPlatformCompat).putOverridesOnReleaseBuilds(mOverridesToAddConfigCaptor.capture(), + eq(PACKAGE_3)); + verify(mPlatformCompat).removeOverridesOnReleaseBuilds( + mOverridesToRemoveConfigCaptor.capture(), eq(PACKAGE_3)); + assertThat(mOverridesToAddConfigCaptor.getValue().overrides.keySet()).containsExactly(456L); + assertThat(mOverridesToRemoveConfigCaptor.getValue().changeIds).containsExactly(789L); + } + + @Test + public void onPropertiesChanged_ownedChangeIdsFlagAndSomePackageOverrideFlagsChanged_ok() + throws Exception { + DeviceConfig.setProperties(new Properties.Builder(NAMESPACE_1) + .setString(FLAG_REMOVE_OVERRIDES, PACKAGE_1 + "=*") + .setString(FLAG_OWNED_CHANGE_IDS, "123,456") + .setString(PACKAGE_1, "123:::true") + .setString(PACKAGE_3, "456:::false").build()); + mockGetApplicationInfo(PACKAGE_1, /* versionCode= */ 0); + mockGetApplicationInfo(PACKAGE_2, /* versionCode= */ 0); + mockGetApplicationInfo(PACKAGE_3, /* versionCode= */ 0); + + mService.registerDeviceConfigListeners(); + DeviceConfig.setProperties(new Properties.Builder(NAMESPACE_1) + .setString(FLAG_OWNED_CHANGE_IDS, "123,456,789") + .setString(PACKAGE_2, "123:::true").build()); + + // Package 1 + verify(mPlatformCompat, never()).putOverridesOnReleaseBuilds( + any(CompatibilityOverrideConfig.class), eq(PACKAGE_1)); + verify(mPlatformCompat).removeOverridesOnReleaseBuilds( + mOverridesToRemoveConfigCaptor.capture(), eq(PACKAGE_1)); + assertThat(mOverridesToRemoveConfigCaptor.getValue().changeIds).containsExactly(123L, 456L, + 789L); + // Package 2 + verify(mPlatformCompat).putOverridesOnReleaseBuilds(mOverridesToAddConfigCaptor.capture(), + eq(PACKAGE_2)); + verify(mPlatformCompat, never()).removeOverridesOnReleaseBuilds( + any(CompatibilityOverridesToRemoveConfig.class), eq(PACKAGE_2)); + assertThat(mOverridesToAddConfigCaptor.getValue().overrides.keySet()).containsExactly(123L); + // Package 3 + verify(mPlatformCompat, never()).putOverridesOnReleaseBuilds( + any(CompatibilityOverrideConfig.class), eq(PACKAGE_3)); + verify(mPlatformCompat, never()).removeOverridesOnReleaseBuilds( + any(CompatibilityOverridesToRemoveConfig.class), eq(PACKAGE_3)); + } + + @Test + public void onPropertiesChanged_platformCompatThrowsExceptionForSomeCalls_skipsFailedCalls() + throws Exception { + mockGetApplicationInfo(PACKAGE_1, /* versionCode= */ 0); + mockGetApplicationInfo(PACKAGE_2, /* versionCode= */ 0); + mockGetApplicationInfo(PACKAGE_3, /* versionCode= */ 0); + mockGetApplicationInfo(PACKAGE_4, /* versionCode= */ 0); + doThrow(new RemoteException()).when(mPlatformCompat).putOverridesOnReleaseBuilds( + any(CompatibilityOverrideConfig.class), eq(PACKAGE_2)); + doThrow(new RemoteException()).when(mPlatformCompat).removeOverridesOnReleaseBuilds( + any(CompatibilityOverridesToRemoveConfig.class), eq(PACKAGE_3)); + + mService.registerDeviceConfigListeners(); + DeviceConfig.setProperties(new Properties.Builder(NAMESPACE_1) + .setString(PACKAGE_1, "123:::true,456:::") + .setString(PACKAGE_2, "123:::true,456:::") + .setString(PACKAGE_3, "123:::true,456:::") + .setString(PACKAGE_4, "123:::true,456:::").build()); + + // Package 1 + verify(mPlatformCompat).putOverridesOnReleaseBuilds(any(CompatibilityOverrideConfig.class), + eq(PACKAGE_1)); + verify(mPlatformCompat).removeOverridesOnReleaseBuilds( + any(CompatibilityOverridesToRemoveConfig.class), eq(PACKAGE_1)); + // Package 2 + verify(mPlatformCompat).putOverridesOnReleaseBuilds(any(CompatibilityOverrideConfig.class), + eq(PACKAGE_2)); + verify(mPlatformCompat).removeOverridesOnReleaseBuilds( + any(CompatibilityOverridesToRemoveConfig.class), eq(PACKAGE_2)); + // Package 3 + verify(mPlatformCompat).putOverridesOnReleaseBuilds(any(CompatibilityOverrideConfig.class), + eq(PACKAGE_3)); + verify(mPlatformCompat).removeOverridesOnReleaseBuilds( + any(CompatibilityOverridesToRemoveConfig.class), eq(PACKAGE_3)); + // Package 4 + verify(mPlatformCompat).putOverridesOnReleaseBuilds(any(CompatibilityOverrideConfig.class), + eq(PACKAGE_1)); + verify(mPlatformCompat).removeOverridesOnReleaseBuilds( + any(CompatibilityOverridesToRemoveConfig.class), eq(PACKAGE_4)); + } + + private void mockGetApplicationInfo(String packageName, long versionCode) + throws Exception { + when(mPackageManager.getApplicationInfo(eq(packageName), anyInt())).thenReturn( + createAppInfo(versionCode)); + } + + private void mockGetApplicationInfoNotInstalled(String packageName) throws Exception { + when(mPackageManager.getApplicationInfo(eq(packageName), anyInt())) + .thenThrow(new PackageManager.NameNotFoundException()); + } + + private static ApplicationInfo createAppInfo(long versionCode) { + ApplicationInfo appInfo = new ApplicationInfo(); + appInfo.longVersionCode = versionCode; + return appInfo; + } +}