/* * 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; 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; /** * A class for querying constants from the system - primarily booleans. * * Clients using this class can define their flags and their default values in one place, * can override those values on running devices for debugging and testing purposes, and can control * what flags are available to be used on release builds. * * TODO(b/279054964): A lot. This is skeleton code right now. * @hide */ public class FeatureFlags { private static final String TAG = "FeatureFlags"; private static FeatureFlags sInstance; private static final Object sInstanceLock = new Object(); private final Set> mKnownFlags = new ArraySet<>(); private final Set> mDirtyFlags = new ArraySet<>(); private IFeatureFlags mIFeatureFlags; private final Map> mBooleanOverrides = new HashMap<>(); private final Set mListeners = new HashSet<>(); /** * Obtain a per-process instance of FeatureFlags. * @return A singleton instance of {@link FeatureFlags}. */ @NonNull public static FeatureFlags getInstance() { synchronized (sInstanceLock) { if (sInstance == null) { sInstance = new FeatureFlags(); } } return sInstance; } /** 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) { // Shouldn't happen with things passed into tests. Log.e(TAG, "Could not register callbacks!", e); } } } /** * 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); } } /** * Returns whether the supplied flag is true or not. * * {@link BooleanFlag} should only be used in debug builds. They do not get optimized out. * * The first time a flag is read, its value is cached for the lifetime of the process. */ public boolean isEnabled(@NonNull BooleanFlag flag) { return getBooleanInternal(flag); } /** * Returns whether the supplied flag is true or not. * * Always returns false. */ public boolean isEnabled(@NonNull FusedOffFlag flag) { return false; } /** * Returns whether the supplied flag is true or not. * * Always returns true; */ public boolean isEnabled(@NonNull FusedOnFlag flag) { return true; } /** * Returns whether the supplied flag is true or not. * * 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 getBooleanInternal(flag); } private boolean getBooleanInternal(Flag flag) { sync(); Map 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 addFlag(T flag) { synchronized (FeatureFlags.class) { mDirtyFlags.add(flag); mKnownFlags.add(flag); } return flag; } /** * Sync any known flags that have not yet been synced. * * This is called implicitly when any flag is read, and is not generally needed except in * exceptional circumstances. */ public void sync() { synchronized (FeatureFlags.class) { if (mDirtyFlags.isEmpty()) { return; } syncInternal(mDirtyFlags); mDirtyFlags.clear(); } } /** * Called when new flags have been declared. Gives the implementation a chance to act on them. * * Guaranteed to be called from a synchronized, thread-safe context. */ protected void syncInternal(Set> dirtyFlags) { IFeatureFlags iFeatureFlags = bind(); List syncableFlags = new ArrayList<>(); for (Flag f : dirtyFlags) { syncableFlags.add(flagToSyncableFlag(f)); } List 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 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. * * See {@link #addChangeListener(ChangeListener)} */ public interface ChangeListener { /** * Called when a {@link DynamicFlag} changes. * * @param flag The flag that has changed. */ void onFlagChanged(DynamicFlag flag); } }