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

Commit f593888d authored by Dave Mankoff's avatar Dave Mankoff
Browse files

Connect FeatureFlags with FeatureFlagsService.

With this change, android.flags.FeatureFlags' internals are
filled out, connecting with with FeatureFlagsService to create
a working implementation.

Bug: 279054964
Test: atest FrameworkksCoreTests:android.flags
Test: manually run adb shell commands.
Change-Id: I37b641a5571cf57dd9b5f1373549931fdd9bb5a3
parent 4f7ccd42
Loading
Loading
Loading
Loading
+2 −23
Original line number Diff line number Diff line
@@ -25,9 +25,7 @@ import android.annotation.NonNull;
 *
 * @hide
 */
public class BooleanFlag implements Flag<Boolean> {
    private final String mNamespace;
    private final String mName;
public class BooleanFlag extends BooleanFlagBase {
    private final boolean mDefault;

    /**
@@ -36,8 +34,7 @@ public class BooleanFlag implements Flag<Boolean> {
     * @param defaultValue The value of this flag if no other override is present.
     */
    BooleanFlag(String namespace, String name, boolean defaultValue) {
        mNamespace = namespace;
        mName = name;
        super(namespace, name);
        mDefault = defaultValue;
    }

@@ -46,22 +43,4 @@ public class BooleanFlag implements Flag<Boolean> {
    public Boolean getDefault() {
        return mDefault;
    }

    @Override
    @NonNull
    public String getNamespace() {
        return mNamespace;
    }

    @Override
    @NonNull
    public String getName() {
        return mName;
    }

    @Override
    @NonNull
    public String toString() {
        return getNamespace() + "." + getName() + "[" + getDefault() + "]";
    }
}
+54 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 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 android.flags;

import android.annotation.NonNull;

abstract class BooleanFlagBase implements Flag<Boolean> {

    private final String mNamespace;
    private final String mName;

    /**
     * @param namespace A namespace for this flag. See {@link android.provider.DeviceConfig}.
     * @param name A name for this flag.
     */
    BooleanFlagBase(String namespace, String name) {
        mNamespace = namespace;
        mName = name;
    }

    public abstract Boolean getDefault();

    @Override
    @NonNull
    public String getNamespace() {
        return mNamespace;
    }

    @Override
    @NonNull
    public String getName() {
        return mName;
    }

    @Override
    @NonNull
    public String toString() {
        return getNamespace() + "." + getName() + "[" + getDefault() + "]";
    }
}
+2 −20
Original line number Diff line number Diff line
@@ -23,10 +23,8 @@ package android.flags;
 *
 * @hide
 */
public class DynamicBooleanFlag implements DynamicFlag<Boolean> {
public class DynamicBooleanFlag extends BooleanFlagBase implements DynamicFlag<Boolean> {

    private final String mNamespace;
    private final String mName;
    private final boolean mDefault;

    /**
@@ -35,28 +33,12 @@ public class DynamicBooleanFlag implements DynamicFlag<Boolean> {
     * @param defaultValue The value of this flag if no other override is present.
     */
    DynamicBooleanFlag(String namespace, String name, boolean defaultValue) {
        mNamespace = namespace;
        mName = name;
        super(namespace, name);
        mDefault = defaultValue;
    }

    @Override
    public String getNamespace() {
        return mNamespace;
    }

    @Override
    public String getName() {
        return mName;
    }

    @Override
    public Boolean getDefault() {
        return mDefault;
    }

    @Override
    public String toString() {
        return getNamespace() + "." + getName() + "[" + getDefault() + "]";
    }
}
+260 −19
Original line number Diff line number Diff line
@@ -17,8 +17,19 @@
package android.flags;

import android.annotation.NonNull;
import android.content.Context;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.util.ArraySet;
import android.util.Log;

import com.android.internal.annotations.VisibleForTesting;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
@@ -32,14 +43,20 @@ import java.util.Set;
 * @hide
 */
public class FeatureFlags {
    private static final String TAG = "FeatureFlags";
    private static FeatureFlags sInstance;
    private static final Object sInstanceLock = new Object();

    private final Set<Flag<?>> mKnownFlags = new ArraySet<>();
    private final Set<Flag<?>> mDirtyFlags = new ArraySet<>();

    private IFeatureFlags mIFeatureFlags;
    private final Map<String, Map<String, Boolean>> mBooleanOverrides = new HashMap<>();
    private final Set<ChangeListener> mListeners = new HashSet<>();

    /**
     * Obtain a per-process instance of FeatureFlags.
     * @return
     * @return A singleton instance of {@link FeatureFlags}.
     */
    @NonNull
    public static FeatureFlags getInstance() {
@@ -52,7 +69,124 @@ public class FeatureFlags {
        return sInstance;
    }

    FeatureFlags() {
    /** See {@link FeatureFlagsFake}. */
    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
    public static void setInstance(FeatureFlags instance) {
        synchronized (sInstanceLock) {
            sInstance = instance;
        }
    }

    private final IFeatureFlagsCallback mIFeatureFlagsCallback = new IFeatureFlagsCallback.Stub() {
        @Override
        public void onFlagChange(SyncableFlag flag) {
            for (Flag<?> f : mKnownFlags) {
                if (flagEqualsSyncableFlag(f, flag)) {
                    if (f instanceof DynamicFlag<?>) {
                        if (f instanceof DynamicBooleanFlag) {
                            String value = flag.getValue();
                            if (value == null) {  // Null means any existing overrides were erased.
                                value = ((DynamicBooleanFlag) f).getDefault().toString();
                            }
                            addBooleanOverride(flag.getNamespace(), flag.getName(), value);
                        }
                        FeatureFlags.this.onFlagChange((DynamicFlag<?>) f);
                    }
                    break;
                }
            }
        }
    };

    private FeatureFlags() {
        this(null);
    }

    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
    public FeatureFlags(IFeatureFlags iFeatureFlags) {
        mIFeatureFlags = iFeatureFlags;

        if (mIFeatureFlags != null) {
            try {
                mIFeatureFlags.registerCallback(mIFeatureFlagsCallback);
            } catch (RemoteException e) {
                // Won't happen in tests.
            }
        }
    }

    /**
     * Construct a new {@link BooleanFlag}.
     *
     * Use this instead of constructing a {@link BooleanFlag} directly, as it registers the flag
     * with the internals of the flagging system.
     */
    @NonNull
    public static BooleanFlag booleanFlag(
            @NonNull String namespace, @NonNull String name, boolean def) {
        return getInstance().addFlag(new BooleanFlag(namespace, name, def));
    }

    /**
     * Construct a new {@link FusedOffFlag}.
     *
     * Use this instead of constructing a {@link FusedOffFlag} directly, as it registers the
     * flag with the internals of the flagging system.
     */
    @NonNull
    public static FusedOffFlag fusedOffFlag(@NonNull String namespace, @NonNull String name) {
        return getInstance().addFlag(new FusedOffFlag(namespace, name));
    }

    /**
     * Construct a new {@link FusedOnFlag}.
     *
     * Use this instead of constructing a {@link FusedOnFlag} directly, as it registers the flag
     * with the internals of the flagging system.
     */
    @NonNull
    public static FusedOnFlag fusedOnFlag(@NonNull String namespace, @NonNull String name) {
        return getInstance().addFlag(new FusedOnFlag(namespace, name));
    }

    /**
     * Construct a new {@link DynamicBooleanFlag}.
     *
     * Use this instead of constructing a {@link DynamicBooleanFlag} directly, as it registers
     * the flag with the internals of the flagging system.
     */
    @NonNull
    public static DynamicBooleanFlag dynamicBooleanFlag(
            @NonNull String namespace, @NonNull String name, boolean def) {
        return getInstance().addFlag(new DynamicBooleanFlag(namespace, name, def));
    }

    /**
     * Add a listener to be alerted when a {@link DynamicFlag} changes.
     *
     * See also {@link #removeChangeListener(ChangeListener)}.
     *
     * @param listener The listener to add.
     */
    public void addChangeListener(@NonNull ChangeListener listener) {
        mListeners.add(listener);
    }

    /**
     * Remove a listener that was added earlier.
     *
     * See also {@link #addChangeListener(ChangeListener)}.
     *
     * @param listener The listener to remove.
     */
    public void removeChangeListener(@NonNull ChangeListener listener) {
        mListeners.remove(listener);
    }

    protected void onFlagChange(@NonNull DynamicFlag<?> flag) {
        for (ChangeListener l : mListeners) {
            l.onFlagChanged(flag);
        }
    }

    /**
@@ -63,7 +197,7 @@ public class FeatureFlags {
     * The first time a flag is read, its value is cached for the lifetime of the process.
     */
    public boolean isEnabled(@NonNull BooleanFlag flag) {
        return flag.getDefault();
        return getBooleanInternal(flag);
    }

    /**
@@ -90,31 +224,138 @@ public class FeatureFlags {
     * Can return a different value for the flag each time it is called if an override comes in.
     */
    public boolean isCurrentlyEnabled(@NonNull DynamicBooleanFlag flag) {
        return flag.getDefault();
        return getBooleanInternal(flag);
    }

    /**
     * Add a listener to be alerted when a {@link DynamicFlag} changes.
     *
     * See also {@link #removeChangeListener(ChangeListener)}.
     *
     * @param listener The listener to add.
     */
    public void addChangeListener(@NonNull ChangeListener listener) {
        mListeners.add(listener);
    private boolean getBooleanInternal(Flag<Boolean> flag) {
        sync();
        Map<String, Boolean> ns = mBooleanOverrides.get(flag.getNamespace());
        Boolean value = null;
        if (ns != null) {
            value = ns.get(flag.getName());
        }
        if (value == null) {
            throw new IllegalStateException("Boolean flag being read but was not synced: " + flag);
        }

        return value;
    }

    private <T extends Flag<?>> T addFlag(T flag)  {
        synchronized (FeatureFlags.class) {
            mDirtyFlags.add(flag);
            mKnownFlags.add(flag);
        }
        return flag;
    }

    protected void sync() {
        synchronized (FeatureFlags.class) {
            if (mDirtyFlags.isEmpty()) {
                return;
            }
            syncInternal(mDirtyFlags);
            mDirtyFlags.clear();
        }
    }

    /**
     * Remove a listener that was added earlier.
     * Called when new flags have been declared. Gives the implementation a chance to act on them.
     *
     * See also {@link #addChangeListener(ChangeListener)}.
     *
     * @param listener The listener to remove.
     * Guaranteed to be called from a synchronized, thread-safe context.
     */
    public void removeChangeListener(@NonNull ChangeListener listener) {
        mListeners.remove(listener);
    protected void syncInternal(Set<Flag<?>> dirtyFlags) {
        IFeatureFlags iFeatureFlags = bind();
        List<SyncableFlag> syncableFlags = new ArrayList<>();
        for (Flag<?> f : dirtyFlags) {
            syncableFlags.add(flagToSyncableFlag(f));
        }

        List<SyncableFlag> serverFlags = List.of();  // Need to initialize the list with something.
        try {
            // New values come back from the service.
            serverFlags = iFeatureFlags.syncFlags(syncableFlags);
        } catch (RemoteException e) {
            e.rethrowFromSystemServer();
        }

        for (Flag<?> f : dirtyFlags) {
            boolean found = false;
            for (SyncableFlag sf : serverFlags) {
                if (flagEqualsSyncableFlag(f, sf)) {
                    if (f instanceof BooleanFlag || f instanceof DynamicBooleanFlag) {
                        addBooleanOverride(sf.getNamespace(), sf.getName(), sf.getValue());
                    }
                    found = true;
                    break;
                }
            }
            if (!found) {
                if (f instanceof BooleanFlag) {
                    addBooleanOverride(
                            f.getNamespace(),
                            f.getName(),
                            ((BooleanFlag) f).getDefault() ? "true" : "false");
                }
            }
        }
    }

    private void addBooleanOverride(String namespace, String name, String override) {
        Map<String, Boolean> nsOverrides = mBooleanOverrides.get(namespace);
        if (nsOverrides == null) {
            nsOverrides = new HashMap<>();
            mBooleanOverrides.put(namespace, nsOverrides);
        }
        nsOverrides.put(name, parseBoolean(override));
    }

    private SyncableFlag flagToSyncableFlag(Flag<?> f) {
        return new SyncableFlag(
                f.getNamespace(),
                f.getName(),
                f.getDefault().toString(),
                f instanceof DynamicFlag<?>);
    }

    private IFeatureFlags bind() {
        if (mIFeatureFlags == null) {
            mIFeatureFlags = IFeatureFlags.Stub.asInterface(
                    ServiceManager.getService(Context.FEATURE_FLAGS_SERVICE));
            try {
                mIFeatureFlags.registerCallback(mIFeatureFlagsCallback);
            } catch (RemoteException e) {
                Log.e(TAG, "Failed to listen for flag changes!");
            }
        }

        return mIFeatureFlags;
    }

    static boolean parseBoolean(String value) {
        // Check for a truish string.
        boolean result = value.equalsIgnoreCase("true")
                || value.equals("1")
                || value.equalsIgnoreCase("t")
                || value.equalsIgnoreCase("on");
        if (!result) {  // Expect a falsish string, else log an error.
            if (!(value.equalsIgnoreCase("false")
                    || value.equals("0")
                    || value.equalsIgnoreCase("f")
                    || value.equalsIgnoreCase("off"))) {
                Log.e(TAG,
                        "Tried parsing " + value + " as boolean but it doesn't look like one. "
                                + "Value expected to be one of true|false, 1|0, t|f, on|off.");
            }
        }
        return result;
    }

    private static boolean flagEqualsSyncableFlag(Flag<?> f, SyncableFlag sf) {
        return f.getName().equals(sf.getName()) && f.getNamespace().equals(sf.getNamespace());
    }


    /**
     * A simpler listener that is alerted when a {@link DynamicFlag} changes.
     *
+72 −6
Original line number Diff line number Diff line
@@ -18,32 +18,98 @@ package android.flags;

import android.annotation.NonNull;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

/**
 * An implementation of {@link FeatureFlags} for testing.
 *
 * Before you read a flag from using this Fake, you must set that flag using
 * {@link #setFlagValue(BooleanFlagBase, boolean)}. This ensures that your tests are deterministic.
 *
 * If you are relying on {@link FeatureFlags#getInstance()} to access FeatureFlags in your code
 * under test, (instead of dependency injection), you can pass an instance of this fake to
 * {@link FeatureFlags#setInstance(FeatureFlags)}. Be sure to call that method again, passing null,
 * to ensure hermetic testing - you don't want static state persisting between your test methods.
 *
 * @hide
 */
public class FeatureFlagsFake extends FeatureFlags {
    public FeatureFlagsFake() {
        super();
    private final Map<BooleanFlagBase, Boolean> mFlagValues = new HashMap<>();
    private final Set<BooleanFlagBase> mReadFlags = new HashSet<>();

    public FeatureFlagsFake(IFeatureFlags iFeatureFlags) {
        super(iFeatureFlags);
    }

    @Override
    public boolean isEnabled(@NonNull BooleanFlag flag) {
        return flag.getDefault();
        return requireFlag(flag);
    }

    @Override
    public boolean isEnabled(@NonNull FusedOffFlag flag) {
        return false;
        return requireFlag(flag);
    }

    @Override
    public boolean isEnabled(@NonNull FusedOnFlag flag) {
        return true;
        return requireFlag(flag);
    }

    @Override
    public boolean isCurrentlyEnabled(@NonNull DynamicBooleanFlag flag) {
        return flag.getDefault();
        return requireFlag(flag);
    }

    @Override
    protected void syncInternal(Set<Flag<?>> dirtyFlags) {
    }

    /**
     * Explicitly set a flag's value for reading in tests.
     *
     * You _must_ call this for every flag your code-under-test will read. Otherwise, an
     * {@link IllegalStateException} will be thrown.
     *
     * You are able to set values for {@link FusedOffFlag} and {@link FusedOnFlag}, despite those
     * flags having a fixed value at compile time, since unit tests should still test the state of
     * those flags as both true and false. I.e. a flag that is off might be turned on in a future
     * build or vice versa.
     *
     * You can not call this method _after_ a non-dynamic flag has been read. Non-dynamic flags
     * are held stable in the system, so changing a value after reading would not match
     * real-implementation behavior.
     *
     * Calling this method will trigger any {@link android.flags.FeatureFlags.ChangeListener}s that
     * are registered for the supplied flag if the flag is a {@link DynamicFlag}.
     *
     * @param flag  The BooleanFlag that you want to set a value for.
     * @param value The value that the flag should return when accessed.
     */
    public void setFlagValue(@NonNull BooleanFlagBase flag, boolean value) {
        if (!(flag instanceof DynamicBooleanFlag) && mReadFlags.contains(flag)) {
            throw new RuntimeException(
                    "You can not set the value of a flag after it has been read. Tried to set "
                            + flag + " to " + value + " but it already " + mFlagValues.get(flag));
        }
        mFlagValues.put(flag, value);
        if (flag instanceof DynamicBooleanFlag) {
            onFlagChange((DynamicFlag<?>) flag);
        }
    }

    private boolean requireFlag(BooleanFlagBase flag) {
        if (!mFlagValues.containsKey(flag)) {
            throw new IllegalStateException(
                    "Tried to access " + flag + " in test but no overrided specified. You must "
                            + "call #setFlagValue for each flag read in a test.");
        }
        mReadFlags.add(flag);

        return mFlagValues.get(flag);
    }

}
Loading