Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit c4cedf7b authored by Mathew Inwood's avatar Mathew Inwood Committed by Gerrit Code Review
Browse files

Merge "Add basic logic for new platform compatibilty framework."

parents 71fd6f01 e188acc6
Loading
Loading
Loading
Loading
+139 −0
Original line number Diff line number Diff line
/*
 * 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();
    }
}
+164 −0
Original line number Diff line number Diff line
/*
 * 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);
            }
        }
    }

}
+67 −0
Original line number Diff line number Diff line
/*
 * 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;
    }
}
+145 −0
Original line number Diff line number Diff line
/*
 * 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);
    }
}