diff options
| author | 2019-07-03 17:27:55 -0700 | |
|---|---|---|
| committer | 2019-07-03 17:27:55 -0700 | |
| commit | fcb0f0c2de5a15bc2302e08517708ebdb502e71f (patch) | |
| tree | a3b6237c9b971d9df260e551deb06c6baf2e0c16 | |
| parent | 8f8f4da0ca72f6d870a65cf98a2ecb382756d9d3 (diff) | |
| parent | c4cedf7b5651ceb30862c1123716f2d2219f34f2 (diff) | |
Merge "Add basic logic for new platform compatibilty framework."
am: c4cedf7b56
Change-Id: I2adc341bb00304d02566383451138f8ba8027a4d
4 files changed, 515 insertions, 0 deletions
diff --git a/services/core/java/com/android/server/compat/CompatChange.java b/services/core/java/com/android/server/compat/CompatChange.java new file mode 100644 index 000000000000..bb3b9be2bd2f --- /dev/null +++ b/services/core/java/com/android/server/compat/CompatChange.java @@ -0,0 +1,139 @@ +/* + * Copyright (C) 2019 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; + +import android.annotation.Nullable; +import android.compat.annotation.EnabledAfter; +import android.content.pm.ApplicationInfo; + +import java.util.HashMap; +import java.util.Map; + +/** + * Represents the state of a single compatibility change. + * + * <p>A compatibility change has a default setting, determined by the {@code enableAfterTargetSdk} + * and {@code disabled} constructor parameters. If a change is {@code disabled}, this overrides any + * target SDK criteria set. These settings can be overridden for a specific package using + * {@link #addPackageOverride(String, boolean)}. + * + * <p>Note, this class is not thread safe so callers must ensure thread safety. + */ +public final class CompatChange { + + private final long mChangeId; + @Nullable private final String mName; + private final int mEnableAfterTargetSdk; + private final boolean mDisabled; + private Map<String, Boolean> mPackageOverrides; + + public CompatChange(long changeId) { + this(changeId, null, -1, false); + } + + /** + * @param changeId Unique ID for the change. See {@link android.compat.Compatibility}. + * @param name Short descriptive name. + * @param enableAfterTargetSdk {@code targetSdkVersion} restriction. See {@link EnabledAfter}; + * -1 if the change is always enabled. + * @param disabled If {@code true}, overrides any {@code enableAfterTargetSdk} set. + */ + public CompatChange(long changeId, @Nullable String name, int enableAfterTargetSdk, + boolean disabled) { + mChangeId = changeId; + mName = name; + mEnableAfterTargetSdk = enableAfterTargetSdk; + mDisabled = disabled; + } + + long getId() { + return mChangeId; + } + + @Nullable + String getName() { + return mName; + } + + /** + * Force the enabled state of this change for a given package name. The change will only take + * effect after that packages process is killed and restarted. + * + * <p>Note, this method is not thread safe so callers must ensure thread safety. + * + * @param pname Package name to enable the change for. + * @param enabled Whether or not to enable the change. + */ + void addPackageOverride(String pname, boolean enabled) { + if (mPackageOverrides == null) { + mPackageOverrides = new HashMap<>(); + } + mPackageOverrides.put(pname, enabled); + } + + /** + * Remove any package override for the given package name, restoring the default behaviour. + * + * <p>Note, this method is not thread safe so callers must ensure thread safety. + * + * @param pname Package name to reset to defaults for. + */ + void removePackageOverride(String pname) { + if (mPackageOverrides != null) { + mPackageOverrides.remove(pname); + } + } + + /** + * Find if this change is enabled for the given package, taking into account any overrides that + * exist. + * + * @param app Info about the app in question + * @return {@code true} if the change should be enabled for the package. + */ + boolean isEnabled(ApplicationInfo app) { + if (mPackageOverrides != null && mPackageOverrides.containsKey(app.packageName)) { + return mPackageOverrides.get(app.packageName); + } + if (mDisabled) { + return false; + } + if (mEnableAfterTargetSdk != -1) { + return app.targetSdkVersion > mEnableAfterTargetSdk; + } + return true; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder("ChangeId(") + .append(mChangeId); + if (mName != null) { + sb.append("; name=").append(mName); + } + if (mEnableAfterTargetSdk != -1) { + sb.append("; enableAfterTargetSdk=").append(mEnableAfterTargetSdk); + } + if (mDisabled) { + sb.append("; disabled"); + } + if (mPackageOverrides != null && mPackageOverrides.size() > 0) { + sb.append("; packageOverrides=").append(mPackageOverrides); + } + return sb.append(")").toString(); + } +} diff --git a/services/core/java/com/android/server/compat/CompatConfig.java b/services/core/java/com/android/server/compat/CompatConfig.java new file mode 100644 index 000000000000..fea5d836ac25 --- /dev/null +++ b/services/core/java/com/android/server/compat/CompatConfig.java @@ -0,0 +1,164 @@ +/* + * Copyright (C) 2019 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; + +import android.content.pm.ApplicationInfo; +import android.text.TextUtils; +import android.util.LongArray; +import android.util.LongSparseArray; + +import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; + +/** + * This class maintains state relating to platform compatibility changes. + * + * <p>It stores the default configuration for each change, and any per-package overrides that have + * been configured. + */ +public final class CompatConfig { + + private static final CompatConfig sInstance = new CompatConfig(); + + @GuardedBy("mChanges") + private final LongSparseArray<CompatChange> mChanges = new LongSparseArray<>(); + + @VisibleForTesting + public CompatConfig() { + } + + /** + * @return The static instance of this class to be used within the system server. + */ + public static CompatConfig get() { + return sInstance; + } + + /** + * Add a change. This is intended to be used by code that reads change config from the + * filesystem. This should be done at system startup time. + * + * @param change The change to add. Any change with the same ID will be overwritten. + */ + public void addChange(CompatChange change) { + synchronized (mChanges) { + mChanges.put(change.getId(), change); + } + } + + /** + * Retrieves the set of disabled changes for a given app. Any change ID not in the returned + * array is by default enabled for the app. + * + * @param app The app in question + * @return A sorted long array of change IDs. We use a primitive array to minimize memory + * footprint: Every app process will store this array statically so we aim to reduce + * overhead as much as possible. + */ + public long[] getDisabledChanges(ApplicationInfo app) { + LongArray disabled = new LongArray(); + synchronized (mChanges) { + for (int i = 0; i < mChanges.size(); ++i) { + CompatChange c = mChanges.valueAt(i); + if (!c.isEnabled(app)) { + disabled.add(c.getId()); + } + } + } + // Note: we don't need to explicitly sort the array, as the behaviour of LongSparseArray + // (mChanges) ensures it's already sorted. + return disabled.toArray(); + } + + /** + * Look up a change ID by name. + * + * @param name Name of the change to look up + * @return The change ID, or {@code -1} if no change with that name exists. + */ + public long lookupChangeId(String name) { + synchronized (mChanges) { + for (int i = 0; i < mChanges.size(); ++i) { + if (TextUtils.equals(mChanges.valueAt(i).getName(), name)) { + return mChanges.keyAt(i); + } + } + } + return -1; + } + + /** + * Find if a given change is enabled for a given application. + * + * @param changeId The ID of the change in question + * @param app App to check for + * @return {@code true} if the change is enabled for this app. Also returns {@code true} if the + * change ID is not known, as unknown changes are enabled by default. + */ + public boolean isChangeEnabled(long changeId, ApplicationInfo app) { + synchronized (mChanges) { + CompatChange c = mChanges.get(changeId); + if (c == null) { + // we know nothing about this change: default behaviour is enabled. + return true; + } + return c.isEnabled(app); + } + } + + /** + * Overrides the enabled state for a given change and app. This method is intended to be used + * *only* for debugging purposes, ultimately invoked either by an adb command, or from some + * developer settings UI. + * + * <p>Note, package overrides are not persistent and will be lost on system or runtime restart. + * + * @param changeId The ID of the change to be overridden. Note, this call will succeed even if + * this change is not known; it will only have any affect if any code in the + * platform is gated on the ID given. + * @param packageName The app package name to override the change for. + * @param enabled If the change should be enabled or disabled. + */ + public void addOverride(long changeId, String packageName, boolean enabled) { + synchronized (mChanges) { + CompatChange c = mChanges.get(changeId); + if (c == null) { + c = new CompatChange(changeId); + addChange(c); + } + c.addPackageOverride(packageName, enabled); + } + } + + /** + * Removes an override previously added via {@link #addOverride(long, String, boolean)}. This + * restores the default behaviour for the given change and app, once any app processes have been + * restarted. + * + * @param changeId The ID of the change that was overridden. + * @param packageName The app package name that was overridden. + */ + public void removeOverride(long changeId, String packageName) { + synchronized (mChanges) { + CompatChange c = mChanges.get(changeId); + if (c != null) { + c.removePackageOverride(packageName); + } + } + } + +} diff --git a/services/core/java/com/android/server/compat/PlatformCompat.java b/services/core/java/com/android/server/compat/PlatformCompat.java new file mode 100644 index 000000000000..456d15e4fba8 --- /dev/null +++ b/services/core/java/com/android/server/compat/PlatformCompat.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2019 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; + +import android.content.pm.ApplicationInfo; +import android.util.Slog; + +/** + * System server internal API for gating and reporting compatibility changes. + */ +public class PlatformCompat { + + private static final String TAG = "Compatibility"; + + /** + * Reports that a compatibility change is affecting an app process now. + * + * <p>Note: for changes that are gated using {@link #isChangeEnabled(long, ApplicationInfo)}, + * you do not need to call this API directly. The change will be reported for you in the case + * that {@link #isChangeEnabled(long, ApplicationInfo)} returns {@code true}. + * + * @param changeId The ID of the compatibility change taking effect. + * @param appInfo Representing the affected app. + */ + public static void reportChange(long changeId, ApplicationInfo appInfo) { + Slog.d(TAG, "Compat change reported: " + changeId + "; UID " + appInfo.uid); + // TODO log via StatsLog + } + + /** + * Query if a given compatibility change is enabled for an app process. This method should + * be called when implementing functionality on behalf of the affected app. + * + * <p>If this method returns {@code true}, the calling code should implement the compatibility + * change, resulting in differing behaviour compared to earlier releases. If this method returns + * {@code false}, the calling code should behave as it did in earlier releases. + * + * <p>When this method returns {@code true}, it will also report the change as + * {@link #reportChange(long, ApplicationInfo)} would, so there is no need to call that method + * directly. + * + * @param changeId The ID of the compatibility change in question. + * @param appInfo Representing the app in question. + * @return {@code true} if the change is enabled for the current app. + */ + public static boolean isChangeEnabled(long changeId, ApplicationInfo appInfo) { + if (CompatConfig.get().isChangeEnabled(changeId, appInfo)) { + reportChange(changeId, appInfo); + return true; + } + return false; + } +} diff --git a/services/tests/servicestests/src/com/android/server/compat/CompatConfigTest.java b/services/tests/servicestests/src/com/android/server/compat/CompatConfigTest.java new file mode 100644 index 000000000000..e6c484a8dbbc --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/compat/CompatConfigTest.java @@ -0,0 +1,145 @@ +/* + * Copyright (C) 2019 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; + +import static com.google.common.truth.Truth.assertThat; + +import android.content.pm.ApplicationInfo; + +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +public class CompatConfigTest { + + private ApplicationInfo makeAppInfo(String pName, int targetSdkVersion) { + ApplicationInfo ai = new ApplicationInfo(); + ai.packageName = pName; + ai.targetSdkVersion = targetSdkVersion; + return ai; + } + + @Test + public void testUnknownChangeEnabled() { + CompatConfig pc = new CompatConfig(); + assertThat(pc.isChangeEnabled(1234L, makeAppInfo("com.some.package", 1))).isTrue(); + } + + @Test + public void testDisabledChangeDisabled() { + CompatConfig pc = new CompatConfig(); + pc.addChange(new CompatChange(1234L, "MY_CHANGE", -1, true)); + assertThat(pc.isChangeEnabled(1234L, makeAppInfo("com.some.package", 1))).isFalse(); + } + + @Test + public void testTargetSdkChangeDisabled() { + CompatConfig pc = new CompatConfig(); + pc.addChange(new CompatChange(1234L, "MY_CHANGE", 2, false)); + assertThat(pc.isChangeEnabled(1234L, makeAppInfo("com.some.package", 2))).isFalse(); + } + + @Test + public void testTargetSdkChangeEnabled() { + CompatConfig pc = new CompatConfig(); + pc.addChange(new CompatChange(1234L, "MY_CHANGE", 2, false)); + assertThat(pc.isChangeEnabled(1234L, makeAppInfo("com.some.package", 3))).isTrue(); + } + + @Test + public void testDisabledOverrideTargetSdkChange() { + CompatConfig pc = new CompatConfig(); + pc.addChange(new CompatChange(1234L, "MY_CHANGE", 2, true)); + assertThat(pc.isChangeEnabled(1234L, makeAppInfo("com.some.package", 3))).isFalse(); + } + + @Test + public void testGetDisabledChanges() { + CompatConfig pc = new CompatConfig(); + pc.addChange(new CompatChange(1234L, "MY_CHANGE", -1, true)); + pc.addChange(new CompatChange(2345L, "OTHER_CHANGE", -1, false)); + assertThat(pc.getDisabledChanges( + makeAppInfo("com.some.package", 2))).asList().containsExactly(1234L); + } + + @Test + public void testGetDisabledChangesSorted() { + CompatConfig pc = new CompatConfig(); + pc.addChange(new CompatChange(1234L, "MY_CHANGE", 2, true)); + pc.addChange(new CompatChange(123L, "OTHER_CHANGE", 2, true)); + pc.addChange(new CompatChange(12L, "THIRD_CHANGE", 2, true)); + assertThat(pc.getDisabledChanges( + makeAppInfo("com.some.package", 2))).asList().containsExactly(12L, 123L, 1234L); + } + + @Test + public void testPackageOverrideEnabled() { + CompatConfig pc = new CompatConfig(); + pc.addChange(new CompatChange(1234L, "MY_CHANGE", -1, true)); // disabled + pc.addOverride(1234L, "com.some.package", true); + assertThat(pc.isChangeEnabled(1234L, makeAppInfo("com.some.package", 2))).isTrue(); + assertThat(pc.isChangeEnabled(1234L, makeAppInfo("com.other.package", 2))).isFalse(); + } + + @Test + public void testPackageOverrideDisabled() { + CompatConfig pc = new CompatConfig(); + pc.addChange(new CompatChange(1234L, "MY_CHANGE", -1, false)); + pc.addOverride(1234L, "com.some.package", false); + assertThat(pc.isChangeEnabled(1234L, makeAppInfo("com.some.package", 2))).isFalse(); + assertThat(pc.isChangeEnabled(1234L, makeAppInfo("com.other.package", 2))).isTrue(); + } + + @Test + public void testPackageOverrideUnknownPackage() { + CompatConfig pc = new CompatConfig(); + pc.addOverride(1234L, "com.some.package", false); + assertThat(pc.isChangeEnabled(1234L, makeAppInfo("com.some.package", 2))).isFalse(); + assertThat(pc.isChangeEnabled(1234L, makeAppInfo("com.other.package", 2))).isTrue(); + } + + @Test + public void testPackageOverrideUnknownChange() { + CompatConfig pc = new CompatConfig(); + assertThat(pc.isChangeEnabled(1234L, makeAppInfo("com.some.package", 1))).isTrue(); + } + + @Test + public void testRemovePackageOverride() { + CompatConfig pc = new CompatConfig(); + pc.addChange(new CompatChange(1234L, "MY_CHANGE", -1, false)); + pc.addOverride(1234L, "com.some.package", false); + pc.removeOverride(1234L, "com.some.package"); + assertThat(pc.isChangeEnabled(1234L, makeAppInfo("com.some.package", 2))).isTrue(); + } + + @Test + public void testLookupChangeId() { + CompatConfig pc = new CompatConfig(); + pc.addChange(new CompatChange(1234L, "MY_CHANGE", -1, false)); + pc.addChange(new CompatChange(2345L, "ANOTHER_CHANGE", -1, false)); + assertThat(pc.lookupChangeId("MY_CHANGE")).isEqualTo(1234L); + } + + @Test + public void testLookupChangeIdNotPresent() { + CompatConfig pc = new CompatConfig(); + assertThat(pc.lookupChangeId("MY_CHANGE")).isEqualTo(-1L); + } +} |