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

Commit a658fb22 authored by Dave Mankoff's avatar Dave Mankoff Committed by Automerger Merge Worker
Browse files

Connect FeatureFlags with FeatureFlagsService. am: f593888d

parents 572c31e8 f593888d
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