Add an API method for clearing compat overrides on release builds

This will be useful when we want to clear overrides for apps that are no longer needed regardless of the version that is installed on device, e.g., when an app is being uninstalled, when we want to rollback an override, or when we deprecate a change-id.

In addition, rename CompatChanges#setPackageOverride to
addPackageOverrides.

Bug: 183183372
Test: atest FrameworksServicesTests:CompatConfigTest
Test: atest FrameworksServicesTests:PlatformCompatTest
Change-Id: Iff416c8b4c88b5eddc9e21c73219198d23266fa1
This commit is contained in:
tomnatan
2021-04-27 15:45:13 +00:00
parent bef2f5be94
commit 2697018f70
10 changed files with 330 additions and 18 deletions

View File

@@ -1124,10 +1124,11 @@ package android.app.backup {
package android.app.compat {
public final class CompatChanges {
method @RequiresPermission(android.Manifest.permission.OVERRIDE_COMPAT_CHANGE_CONFIG_ON_RELEASE_BUILD) public static void addPackageOverrides(@NonNull String, @NonNull java.util.Map<java.lang.Long,android.app.compat.PackageOverride>);
method public static boolean isChangeEnabled(long);
method @RequiresPermission(allOf={"android.permission.READ_COMPAT_CHANGE_CONFIG", "android.permission.LOG_COMPAT_CHANGE"}) public static boolean isChangeEnabled(long, @NonNull String, @NonNull android.os.UserHandle);
method @RequiresPermission(allOf={"android.permission.READ_COMPAT_CHANGE_CONFIG", "android.permission.LOG_COMPAT_CHANGE"}) public static boolean isChangeEnabled(long, int);
method @RequiresPermission(android.Manifest.permission.OVERRIDE_COMPAT_CHANGE_CONFIG_ON_RELEASE_BUILD) public static void setPackageOverride(@NonNull String, @NonNull java.util.Map<java.lang.Long,android.app.compat.PackageOverride>);
method @RequiresPermission(android.Manifest.permission.OVERRIDE_COMPAT_CHANGE_CONFIG_ON_RELEASE_BUILD) public static void removePackageOverrides(@NonNull String, @NonNull java.util.Set<java.lang.Long>);
}
public final class PackageOverride {

View File

@@ -26,9 +26,11 @@ import android.os.ServiceManager;
import android.os.UserHandle;
import com.android.internal.compat.CompatibilityOverrideConfig;
import com.android.internal.compat.CompatibilityOverridesToRemoveConfig;
import com.android.internal.compat.IPlatformCompat;
import java.util.Map;
import java.util.Set;
/**
* CompatChanges APIs - to be used by platform code only (including mainline
@@ -98,15 +100,19 @@ public final class CompatChanges {
}
/**
* Set an app compat override for a given package. This will check whether the caller is allowed
* Adds app compat overrides for a given package. This will check whether the caller is allowed
* to perform this operation on the given apk and build. Only the installer package is allowed
* to set overrides on a non-debuggable final build and a non-test apk.
*
* <p>Note that calling this method doesn't remove previously added overrides for the given
* package if their change ID isn't in the given map, only replaces those that have the same
* change ID.
*
* @param packageName The package name of the app in question.
* @param overrides A map from changeId to the override applied for this change id.
* @param overrides A map from change ID to the override applied for this change ID.
*/
@RequiresPermission(android.Manifest.permission.OVERRIDE_COMPAT_CHANGE_CONFIG_ON_RELEASE_BUILD)
public static void setPackageOverride(@NonNull String packageName,
public static void addPackageOverrides(@NonNull String packageName,
@NonNull Map<Long, PackageOverride> overrides) {
IPlatformCompat platformCompat = IPlatformCompat.Stub.asInterface(
ServiceManager.getService(Context.PLATFORM_COMPAT_SERVICE));
@@ -117,4 +123,29 @@ public final class CompatChanges {
e.rethrowFromSystemServer();
}
}
/**
* Removes app compat overrides for a given package. This will check whether the caller is
* allowed to perform this operation on the given apk and build. Only the installer package is
* allowed to clear overrides on a non-debuggable final build and a non-test apk.
*
* <p>Note that calling this method with an empty set is a no-op and no overrides will be
* removed for the given package.
*
* @param packageName The package name of the app in question.
* @param overridesToRemove A set of change IDs for which to remove overrides.
*/
@RequiresPermission(android.Manifest.permission.OVERRIDE_COMPAT_CHANGE_CONFIG_ON_RELEASE_BUILD)
public static void removePackageOverrides(@NonNull String packageName,
@NonNull Set<Long> overridesToRemove) {
IPlatformCompat platformCompat = IPlatformCompat.Stub.asInterface(
ServiceManager.getService(Context.PLATFORM_COMPAT_SERVICE));
CompatibilityOverridesToRemoveConfig config = new CompatibilityOverridesToRemoveConfig(
overridesToRemove);
try {
platformCompat.removeOverridesOnReleaseBuilds(config, packageName);
} catch (RemoteException e) {
e.rethrowFromSystemServer();
}
}
}

View File

@@ -0,0 +1,19 @@
/*
* 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.internal.compat;
parcelable CompatibilityOverridesToRemoveConfig;

View File

@@ -0,0 +1,72 @@
/*
* 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.internal.compat;
import android.os.Parcel;
import android.os.Parcelable;
import java.util.HashSet;
import java.util.Set;
/**
* Parcelable containing compat config change IDs for which to remove overrides for a given
* application.
* @hide
*/
public final class CompatibilityOverridesToRemoveConfig implements Parcelable {
public final Set<Long> changeIds;
public CompatibilityOverridesToRemoveConfig(Set<Long> changeIds) {
this.changeIds = changeIds;
}
private CompatibilityOverridesToRemoveConfig(Parcel in) {
int keyCount = in.readInt();
changeIds = new HashSet<>();
for (int i = 0; i < keyCount; i++) {
changeIds.add(in.readLong());
}
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(changeIds.size());
for (Long changeId : changeIds) {
dest.writeLong(changeId);
}
}
public static final Creator<CompatibilityOverridesToRemoveConfig> CREATOR =
new Creator<CompatibilityOverridesToRemoveConfig>() {
@Override
public CompatibilityOverridesToRemoveConfig createFromParcel(Parcel in) {
return new CompatibilityOverridesToRemoveConfig(in);
}
@Override
public CompatibilityOverridesToRemoveConfig[] newArray(int size) {
return new CompatibilityOverridesToRemoveConfig[size];
}
};
}

View File

@@ -22,6 +22,7 @@ import java.util.Map;
parcelable CompatibilityChangeConfig;
parcelable CompatibilityOverrideConfig;
parcelable CompatibilityOverridesToRemoveConfig;
parcelable CompatibilityChangeInfo;
/**
* Platform private API for talking with the PlatformCompat service.
@@ -203,6 +204,27 @@ interface IPlatformCompat {
*/
void clearOverrideForTest(long changeId, String packageName);
/**
* Restores the default behaviour for compatibility changes on release builds.
*
* <p>The caller to this API needs to hold
* {@code android.permission.OVERRIDE_COMPAT_CHANGE_CONFIG_ON_RELEASE_BUILD} and all change ids
* in {@code overridesToRemove} need to annotated with
* {@link android.compat.annotation.Overridable}.
*
* A release build in this definition means that {@link android.os.Build#IS_DEBUGGABLE} needs to
* be {@code false}.
*
* <p>Note that this does not kill the app, and therefore overrides read from the app process
* will not be updated. Overrides read from the system process do take effect.
*
* @param overridesToRemove parcelable containing the compat change overrides to be removed
* @param packageName the package name of the app whose changes will be restored to the
* default behaviour
* @throws SecurityException if overriding changes is not permitted
*/
void removeOverridesOnReleaseBuilds(in CompatibilityOverridesToRemoveConfig overridesToRemove, in String packageName);
/**
* Enables all compatibility changes that have enabledSinceTargetSdk ==
* {@param targetSdkVersion} for an app, subject to the policy.

View File

@@ -33,6 +33,7 @@ import com.android.internal.compat.AndroidBuildClassifier;
import com.android.internal.compat.CompatibilityChangeConfig;
import com.android.internal.compat.CompatibilityChangeInfo;
import com.android.internal.compat.CompatibilityOverrideConfig;
import com.android.internal.compat.CompatibilityOverridesToRemoveConfig;
import com.android.internal.compat.IOverrideValidator;
import com.android.internal.compat.OverrideAllowedState;
import com.android.server.compat.config.Change;
@@ -370,6 +371,27 @@ final class CompatConfig {
}
}
/**
* Removes overrides whose change ID is specified in {@code overridesToRemove} that were
* previously added via {@link #addOverride(long, String, boolean)} or
* {@link #addOverrides(CompatibilityOverrideConfig, String)} for a certain package.
*
* <p>This restores the default behaviour for the given change IDs and app.
*
* @param overridesToRemove list of change IDs for which to restore the default behaviour.
* @param packageName the package for which the overrides should be purged
*/
void removePackageOverrides(CompatibilityOverridesToRemoveConfig overridesToRemove,
String packageName) {
synchronized (mChanges) {
for (Long changeId : overridesToRemove.changeIds) {
removeOverrideUnsafe(changeId, packageName);
}
saveOverrides();
invalidateCache();
}
}
private long[] getAllowedChangesSinceTargetSdkForPackage(String packageName,
int targetSdkVersion) {
LongArray allowed = new LongArray();

View File

@@ -46,6 +46,7 @@ import com.android.internal.compat.ChangeReporter;
import com.android.internal.compat.CompatibilityChangeConfig;
import com.android.internal.compat.CompatibilityChangeInfo;
import com.android.internal.compat.CompatibilityOverrideConfig;
import com.android.internal.compat.CompatibilityOverridesToRemoveConfig;
import com.android.internal.compat.IOverrideValidator;
import com.android.internal.compat.IPlatformCompat;
import com.android.internal.util.DumpUtils;
@@ -54,6 +55,7 @@ import com.android.server.LocalServices;
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
@@ -187,7 +189,7 @@ public class PlatformCompat extends IPlatformCompat.Stub {
String packageName) {
// TODO(b/183630314): Unify the permission enforcement with the other setOverrides* methods.
checkCompatChangeOverrideOverridablePermission();
checkAllCompatOverridesAreOverridable(overrides);
checkAllCompatOverridesAreOverridable(overrides.overrides.keySet());
mCompatConfig.addOverrides(overrides, packageName);
}
@@ -250,6 +252,16 @@ public class PlatformCompat extends IPlatformCompat.Stub {
mCompatConfig.removeOverride(changeId, packageName);
}
@Override
public void removeOverridesOnReleaseBuilds(
CompatibilityOverridesToRemoveConfig overridesToRemove,
String packageName) {
// TODO(b/183630314): Unify the permission enforcement with the other setOverrides* methods.
checkCompatChangeOverrideOverridablePermission();
checkAllCompatOverridesAreOverridable(overridesToRemove.changeIds);
mCompatConfig.removePackageOverrides(overridesToRemove, packageName);
}
@Override
public CompatibilityChangeConfig getAppConfig(ApplicationInfo appInfo) {
checkCompatChangeReadAndLogPermission();
@@ -396,8 +408,8 @@ public class PlatformCompat extends IPlatformCompat.Stub {
}
}
private void checkAllCompatOverridesAreOverridable(CompatibilityOverrideConfig overrides) {
for (Long changeId : overrides.overrides.keySet()) {
private void checkAllCompatOverridesAreOverridable(Collection<Long> changeIds) {
for (Long changeId : changeIds) {
if (!mCompatConfig.isOverridable(changeId)) {
throw new SecurityException("Only change ids marked as Overridable can be "
+ "overridden.");

View File

@@ -115,7 +115,12 @@ class CompatConfigBuilder {
return this;
}
CompatConfigBuilder addOverridableChangeWithId(long id) {
CompatConfigBuilder addEnabledOverridableChangeWithId(long id) {
mChanges.add(new CompatChange(id, "", -1, -1, false, false, "", true));
return this;
}
CompatConfigBuilder addDisabledOverridableChangeWithId(long id) {
mChanges.add(new CompatChange(id, "", -1, -1, true, false, "", true));
return this;
}

View File

@@ -36,6 +36,7 @@ import androidx.test.runner.AndroidJUnit4;
import com.android.internal.compat.AndroidBuildClassifier;
import com.android.internal.compat.CompatibilityOverrideConfig;
import com.android.internal.compat.CompatibilityOverridesToRemoveConfig;
import org.junit.Before;
import org.junit.Test;
@@ -50,6 +51,10 @@ import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
@RunWith(AndroidJUnit4.class)
@@ -249,7 +254,7 @@ public class CompatConfigTest {
when(packageManager.getApplicationInfo(eq("com.some.package"), anyInt()))
.thenReturn(applicationInfo);
// Force the validator to prevent overriding the change by using a user build.
// Force the validator to prevent overriding non-overridable changes by using a user build.
when(mBuildClassifier.isDebuggableBuild()).thenReturn(false);
when(mBuildClassifier.isFinalBuild()).thenReturn(true);
@@ -261,10 +266,12 @@ public class CompatConfigTest {
@Test
public void testInstallerCanSetOverrides() throws Exception {
final long changeId = 1234L;
final int installerUid = 23;
final long disabledChangeId1 = 1234L;
final long disabledChangeId2 = 1235L;
// We make disabledChangeId2 non-overridable to make sure it is ignored.
CompatConfig compatConfig = CompatConfigBuilder.create(mBuildClassifier, mContext)
.addOverridableChangeWithId(1234L)
.addDisabledOverridableChangeWithId(disabledChangeId1)
.addDisabledChangeWithId(disabledChangeId2)
.build();
ApplicationInfo applicationInfo = ApplicationInfoBuilder.create()
.withPackageName("com.some.package")
@@ -274,19 +281,56 @@ public class CompatConfigTest {
when(packageManager.getApplicationInfo(eq("com.some.package"), anyInt()))
.thenReturn(applicationInfo);
// Force the validator to prevent overriding the change by using a user build.
// Force the validator to prevent overriding non-overridable changes by using a user build.
when(mBuildClassifier.isDebuggableBuild()).thenReturn(false);
when(mBuildClassifier.isFinalBuild()).thenReturn(true);
CompatibilityOverrideConfig config = new CompatibilityOverrideConfig(
Collections.singletonMap(1234L,
Collections.singletonMap(disabledChangeId1,
new PackageOverride.Builder()
.setMaxVersionCode(99L)
.setEnabled(true)
.build()));
compatConfig.addOverrides(config, "com.some.package");
assertThat(compatConfig.isChangeEnabled(1234L, applicationInfo)).isTrue();
assertThat(compatConfig.isChangeEnabled(disabledChangeId1, applicationInfo)).isTrue();
assertThat(compatConfig.isChangeEnabled(disabledChangeId2, applicationInfo)).isFalse();
}
@Test
public void testPreventInstallerSetNonOverridable() throws Exception {
final long disabledChangeId1 = 1234L;
final long disabledChangeId2 = 1235L;
final long disabledChangeId3 = 1236L;
CompatConfig compatConfig = CompatConfigBuilder.create(mBuildClassifier, mContext)
.addDisabledOverridableChangeWithId(disabledChangeId1)
.addDisabledChangeWithId(disabledChangeId2)
.addDisabledOverridableChangeWithId(disabledChangeId3)
.build();
ApplicationInfo applicationInfo = ApplicationInfoBuilder.create()
.withPackageName("com.some.package")
.build();
PackageManager packageManager = mock(PackageManager.class);
when(mContext.getPackageManager()).thenReturn(packageManager);
when(packageManager.getApplicationInfo(eq("com.some.package"), anyInt()))
.thenReturn(applicationInfo);
// Force the validator to prevent overriding non-overridable changes by using a user build.
when(mBuildClassifier.isDebuggableBuild()).thenReturn(false);
when(mBuildClassifier.isFinalBuild()).thenReturn(true);
Map<Long, PackageOverride> overrides = new HashMap<>();
overrides.put(disabledChangeId1, new PackageOverride.Builder().setEnabled(true).build());
overrides.put(disabledChangeId2, new PackageOverride.Builder().setEnabled(true).build());
overrides.put(disabledChangeId3, new PackageOverride.Builder().setEnabled(true).build());
CompatibilityOverrideConfig config = new CompatibilityOverrideConfig(overrides);
assertThrows(SecurityException.class,
() -> compatConfig.addOverrides(config, "com.some.package")
);
assertThat(compatConfig.isChangeEnabled(disabledChangeId1, applicationInfo)).isTrue();
assertThat(compatConfig.isChangeEnabled(disabledChangeId2, applicationInfo)).isFalse();
assertThat(compatConfig.isChangeEnabled(disabledChangeId3, applicationInfo)).isFalse();
}
@Test
@@ -459,7 +503,7 @@ public class CompatConfigTest {
assertThat(compatConfig.isChangeEnabled(1234L, applicationInfo)).isTrue();
// Reject all override attempts.
// Force the validator to prevent overriding the change by using a user build.
// Force the validator to prevent overriding non-overridable changes by using a user build.
when(mBuildClassifier.isDebuggableBuild()).thenReturn(false);
when(mBuildClassifier.isFinalBuild()).thenReturn(false);
// Try to turn off change, but validator prevents it.
@@ -481,7 +525,7 @@ public class CompatConfigTest {
.thenReturn(applicationInfo);
// Reject all override attempts.
// Force the validator to prevent overriding the change by using a user build.
// Force the validator to prevent overriding non-overridable changes by using a user build.
when(mBuildClassifier.isDebuggableBuild()).thenReturn(false);
when(mBuildClassifier.isFinalBuild()).thenReturn(true);
// Try to remove a non existing override, and it doesn't fail.
@@ -508,6 +552,90 @@ public class CompatConfigTest {
assertThat(compatConfig.isChangeEnabled(1234L, applicationInfo)).isTrue();
}
@Test
public void testInstallerCanRemoveOverrides() throws Exception {
final long disabledChangeId1 = 1234L;
final long disabledChangeId2 = 1235L;
final long enabledChangeId = 1236L;
// We make disabledChangeId2 non-overridable to make sure it is ignored.
CompatConfig compatConfig = CompatConfigBuilder.create(mBuildClassifier, mContext)
.addDisabledOverridableChangeWithId(disabledChangeId1)
.addDisabledChangeWithId(disabledChangeId2)
.addEnabledOverridableChangeWithId(enabledChangeId)
.build();
ApplicationInfo applicationInfo = ApplicationInfoBuilder.create()
.withPackageName("com.some.package")
.build();
when(mPackageManager.getApplicationInfo(eq("com.some.package"), anyInt()))
.thenReturn(applicationInfo);
assertThat(compatConfig.addOverride(disabledChangeId1, "com.some.package", true)).isTrue();
assertThat(compatConfig.addOverride(disabledChangeId2, "com.some.package", true)).isTrue();
assertThat(compatConfig.addOverride(enabledChangeId, "com.some.package", false)).isTrue();
assertThat(compatConfig.isChangeEnabled(disabledChangeId1, applicationInfo)).isTrue();
assertThat(compatConfig.isChangeEnabled(disabledChangeId2, applicationInfo)).isTrue();
assertThat(compatConfig.isChangeEnabled(enabledChangeId, applicationInfo)).isFalse();
// Force the validator to prevent overriding non-overridable changes by using a user build.
when(mBuildClassifier.isDebuggableBuild()).thenReturn(false);
when(mBuildClassifier.isFinalBuild()).thenReturn(true);
Set<Long> overridesToRemove = new HashSet<>();
overridesToRemove.add(disabledChangeId1);
overridesToRemove.add(enabledChangeId);
CompatibilityOverridesToRemoveConfig config = new CompatibilityOverridesToRemoveConfig(
overridesToRemove);
compatConfig.removePackageOverrides(config, "com.some.package");
assertThat(compatConfig.isChangeEnabled(disabledChangeId1, applicationInfo)).isFalse();
assertThat(compatConfig.isChangeEnabled(disabledChangeId2, applicationInfo)).isTrue();
assertThat(compatConfig.isChangeEnabled(enabledChangeId, applicationInfo)).isTrue();
}
@Test
public void testPreventInstallerRemoveNonOverridable() throws Exception {
final long disabledChangeId1 = 1234L;
final long disabledChangeId2 = 1235L;
final long disabledChangeId3 = 1236L;
CompatConfig compatConfig = CompatConfigBuilder.create(mBuildClassifier, mContext)
.addDisabledOverridableChangeWithId(disabledChangeId1)
.addDisabledChangeWithId(disabledChangeId2)
.addDisabledOverridableChangeWithId(disabledChangeId3)
.build();
ApplicationInfo applicationInfo = ApplicationInfoBuilder.create()
.withPackageName("com.some.package")
.build();
PackageManager packageManager = mock(PackageManager.class);
when(mContext.getPackageManager()).thenReturn(packageManager);
when(packageManager.getApplicationInfo(eq("com.some.package"), anyInt()))
.thenReturn(applicationInfo);
assertThat(compatConfig.addOverride(disabledChangeId1, "com.some.package", true)).isTrue();
assertThat(compatConfig.addOverride(disabledChangeId2, "com.some.package", true)).isTrue();
assertThat(compatConfig.addOverride(disabledChangeId3, "com.some.package", true)).isTrue();
assertThat(compatConfig.isChangeEnabled(disabledChangeId1, applicationInfo)).isTrue();
assertThat(compatConfig.isChangeEnabled(disabledChangeId2, applicationInfo)).isTrue();
assertThat(compatConfig.isChangeEnabled(disabledChangeId3, applicationInfo)).isTrue();
// Force the validator to prevent overriding non-overridable changes by using a user build.
when(mBuildClassifier.isDebuggableBuild()).thenReturn(false);
when(mBuildClassifier.isFinalBuild()).thenReturn(true);
Set<Long> overridesToRemove = new HashSet<>();
overridesToRemove.add(disabledChangeId1);
overridesToRemove.add(disabledChangeId2);
overridesToRemove.add(disabledChangeId3);
CompatibilityOverridesToRemoveConfig config = new CompatibilityOverridesToRemoveConfig(
overridesToRemove);
assertThrows(SecurityException.class,
() -> compatConfig.removePackageOverrides(config, "com.some.package")
);
assertThat(compatConfig.isChangeEnabled(disabledChangeId1, applicationInfo)).isFalse();
assertThat(compatConfig.isChangeEnabled(disabledChangeId2, applicationInfo)).isTrue();
assertThat(compatConfig.isChangeEnabled(disabledChangeId3, applicationInfo)).isTrue();
}
@Test
public void testEnableTargetSdkChangesForPackage() throws Exception {
CompatConfig compatConfig = CompatConfigBuilder.create(mBuildClassifier, mContext)

View File

@@ -98,7 +98,7 @@ public class PlatformCompatTest {
.addEnableAfterSdkChangeWithId(Build.VERSION_CODES.Q, 5L)
.addEnableAfterSdkChangeWithId(Build.VERSION_CODES.R, 6L)
.addLoggingOnlyChangeWithId(7L)
.addOverridableChangeWithId(8L)
.addDisabledOverridableChangeWithId(8L)
.build();
mPlatformCompat = new PlatformCompat(mContext, mCompatConfig, mBuildClassifier);
assertThat(mPlatformCompat.listAllChanges()).asList().containsExactly(