Loading core/java/android/flags/BooleanFlag.java +2 −23 Original line number Diff line number Diff line Loading @@ -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; /** Loading @@ -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; } Loading @@ -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() + "]"; } } core/java/android/flags/BooleanFlagBase.java 0 → 100644 +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() + "]"; } } core/java/android/flags/DynamicBooleanFlag.java +2 −20 Original line number Diff line number Diff line Loading @@ -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; /** Loading @@ -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() + "]"; } } core/java/android/flags/FeatureFlags.java +260 −19 Original line number Diff line number Diff line Loading @@ -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; /** Loading @@ -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() { Loading @@ -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); } } /** Loading @@ -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); } /** Loading @@ -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. * Loading core/java/android/flags/FeatureFlagsFake.java +72 −6 Original line number Diff line number Diff line Loading @@ -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
core/java/android/flags/BooleanFlag.java +2 −23 Original line number Diff line number Diff line Loading @@ -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; /** Loading @@ -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; } Loading @@ -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() + "]"; } }
core/java/android/flags/BooleanFlagBase.java 0 → 100644 +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() + "]"; } }
core/java/android/flags/DynamicBooleanFlag.java +2 −20 Original line number Diff line number Diff line Loading @@ -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; /** Loading @@ -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() + "]"; } }
core/java/android/flags/FeatureFlags.java +260 −19 Original line number Diff line number Diff line Loading @@ -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; /** Loading @@ -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() { Loading @@ -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); } } /** Loading @@ -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); } /** Loading @@ -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. * Loading
core/java/android/flags/FeatureFlagsFake.java +72 −6 Original line number Diff line number Diff line Loading @@ -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); } }