/* * Copyright (C) 2021 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.systemui.flags; import static com.android.systemui.Flags.exampleFlag; import static com.android.systemui.Flags.sysuiTeamfood; import static com.android.systemui.flags.FlagManager.ACTION_GET_FLAGS; import static com.android.systemui.flags.FlagManager.ACTION_SET_FLAG; import static com.android.systemui.flags.FlagManager.EXTRA_FLAGS; import static com.android.systemui.flags.FlagManager.EXTRA_NAME; import static com.android.systemui.flags.FlagManager.EXTRA_VALUE; import static com.android.systemui.flags.FlagsCommonModule.ALL_FLAGS; import static com.android.systemui.shared.Flags.exampleSharedFlag; import static java.util.Objects.requireNonNull; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.res.Resources; import android.os.Bundle; import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.util.settings.GlobalSettings; import java.io.PrintWriter; import java.util.ArrayList; import java.util.Map; import java.util.Objects; import java.util.TreeMap; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Consumer; import javax.inject.Inject; import javax.inject.Named; /** * Concrete implementation of the a Flag manager that returns default values for debug builds *

* Flags can be set (or unset) via the following adb command: *

* adb shell cmd statusbar flag *

* Alternatively, you can change flags via a broadcast intent: *

* adb shell am broadcast -a com.android.systemui.action.SET_FLAG --ei id [--ez value <0|1>] *

* To restore a flag back to its default, leave the `--ez value <0|1>` off of the command. */ @SysUISingleton public class FeatureFlagsClassicDebug implements FeatureFlagsClassic { static final String TAG = "SysUIFlags"; private final FlagManager mFlagManager; private final Context mContext; private final GlobalSettings mGlobalSettings; private final Resources mResources; private final SystemPropertiesHelper mSystemProperties; private final ServerFlagReader mServerFlagReader; private final Map> mAllFlags; private final Map mBooleanFlagCache = new ConcurrentHashMap<>(); private final Map mStringFlagCache = new ConcurrentHashMap<>(); private final Map mIntFlagCache = new ConcurrentHashMap<>(); private final Restarter mRestarter; private final ServerFlagReader.ChangeListener mOnPropertiesChanged = new ServerFlagReader.ChangeListener() { @Override public void onChange(Flag flag, String value) { boolean shouldRestart = false; if (mBooleanFlagCache.containsKey(flag.getName())) { boolean newValue = value == null ? false : Boolean.parseBoolean(value); if (mBooleanFlagCache.get(flag.getName()) != newValue) { shouldRestart = true; } } else if (mStringFlagCache.containsKey(flag.getName())) { if (!mStringFlagCache.get(flag.getName()).equals(value)) { shouldRestart = true; } } else if (mIntFlagCache.containsKey(flag.getName())) { int newValue = 0; try { newValue = value == null ? 0 : Integer.parseInt(value); } catch (NumberFormatException e) { } if (mIntFlagCache.get(flag.getName()) != newValue) { shouldRestart = true; } } if (shouldRestart) { mRestarter.restartSystemUI( "Server flag change: " + flag.getNamespace() + "." + flag.getName()); } } }; @Inject public FeatureFlagsClassicDebug( FlagManager flagManager, Context context, GlobalSettings globalSettings, SystemPropertiesHelper systemProperties, @Main Resources resources, ServerFlagReader serverFlagReader, @Named(ALL_FLAGS) Map> allFlags, Restarter restarter) { mFlagManager = flagManager; mContext = context; mGlobalSettings = globalSettings; mResources = resources; mSystemProperties = systemProperties; mServerFlagReader = serverFlagReader; mAllFlags = allFlags; mRestarter = restarter; } /** Call after construction to setup listeners. */ void init() { IntentFilter filter = new IntentFilter(); filter.addAction(ACTION_SET_FLAG); filter.addAction(ACTION_GET_FLAGS); mFlagManager.setOnSettingsChangedAction( suppressRestart -> restartSystemUI(suppressRestart, "Settings changed")); mFlagManager.setClearCacheAction(this::removeFromCache); mContext.registerReceiver(mReceiver, filter, null, null, Context.RECEIVER_EXPORTED_UNAUDITED); mServerFlagReader.listenForChanges(mAllFlags.values(), mOnPropertiesChanged); } @Override public boolean isEnabled(@NonNull UnreleasedFlag flag) { return isEnabledInternal(flag); } @Override public boolean isEnabled(@NonNull ReleasedFlag flag) { return isEnabledInternal(flag); } private boolean isEnabledInternal(@NonNull BooleanFlag flag) { String name = flag.getName(); Boolean value = mBooleanFlagCache.get(name); if (value == null) { value = readBooleanFlagInternal(flag, flag.getDefault()); mBooleanFlagCache.put(name, value); } return value; } @Override public boolean isEnabled(@NonNull ResourceBooleanFlag flag) { String name = flag.getName(); Boolean value = mBooleanFlagCache.get(name); if (value == null) { value = readBooleanFlagInternal(flag, mResources.getBoolean(flag.getResourceId())); mBooleanFlagCache.put(name, value); } return value; } @Override public boolean isEnabled(@NonNull SysPropBooleanFlag flag) { String name = flag.getName(); Boolean value = mBooleanFlagCache.get(name); if (value == null) { value = readBooleanFlagInternal(flag, mSystemProperties.getBoolean( flag.getName(), readBooleanFlagInternal(flag, flag.getDefault()))); mBooleanFlagCache.put(name, value); } return value; } @NonNull @Override public String getString(@NonNull StringFlag flag) { String name = flag.getName(); String value = mStringFlagCache.get(name); if (value == null) { value = readFlagValueInternal(name, flag.getDefault(), StringFlagSerializer.INSTANCE); mStringFlagCache.put(name, value); } return value; } @NonNull @Override public String getString(@NonNull ResourceStringFlag flag) { String name = flag.getName(); String value = mStringFlagCache.get(name); if (value == null) { value = readFlagValueInternal( name, mResources.getString(flag.getResourceId()), StringFlagSerializer.INSTANCE); mStringFlagCache.put(name, value); } return value; } @Override public int getInt(@NonNull IntFlag flag) { String name = flag.getName(); Integer value = mIntFlagCache.get(name); if (value == null) { value = readFlagValueInternal(name, flag.getDefault(), IntFlagSerializer.INSTANCE); mIntFlagCache.put(name, value); } return value; } @Override public int getInt(@NonNull ResourceIntFlag flag) { String name = flag.getName(); Integer value = mIntFlagCache.get(name); if (value == null) { value = readFlagValueInternal( name, mResources.getInteger(flag.getResourceId()), IntFlagSerializer.INSTANCE); mIntFlagCache.put(name, value); } return value; } /** Specific override for Boolean flags that checks against the teamfood list.*/ private boolean readBooleanFlagInternal(Flag flag, boolean defaultValue) { Boolean result = readBooleanFlagOverride(flag.getName()); boolean hasServerOverride = mServerFlagReader.hasOverride( flag.getNamespace(), flag.getName()); // Only check for teamfood if the default is false // and there is no server override. if (!hasServerOverride && !defaultValue && result == null && flag.getTeamfood()) { return sysuiTeamfood(); } return result == null ? mServerFlagReader.readServerOverride( flag.getNamespace(), flag.getName(), defaultValue) : result; } private Boolean readBooleanFlagOverride(String name) { return readFlagValueInternal(name, BooleanFlagSerializer.INSTANCE); } @NonNull private T readFlagValueInternal( String name, @NonNull T defaultValue, FlagSerializer serializer) { requireNonNull(defaultValue, "defaultValue"); T resultForName = readFlagValueInternal(name, serializer); if (resultForName == null) { return defaultValue; } return resultForName; } /** Returns the stored value or null if not set. */ @Nullable private T readFlagValueInternal(String name, FlagSerializer serializer) { try { return mFlagManager.readFlagValue(name, serializer); } catch (Exception e) { eraseInternal(name); } return null; } private void setFlagValue(String name, @NonNull T value, FlagSerializer serializer) { requireNonNull(value, "Cannot set a null value"); T currentValue = readFlagValueInternal(name, serializer); if (Objects.equals(currentValue, value)) { Log.i(TAG, "Flag \"" + name + "\" is already " + value); return; } setFlagValueInternal(name, value, serializer); Log.i(TAG, "Set flag \"" + name + "\" to " + value); removeFromCache(name); mFlagManager.dispatchListenersAndMaybeRestart( name, suppressRestart -> restartSystemUI( suppressRestart, "Flag \"" + name + "\" changed to " + value)); } private void setFlagValueInternal( String name, @NonNull T value, FlagSerializer serializer) { final String data = serializer.toSettingsData(value); if (data == null) { Log.w(TAG, "Failed to set flag " + name + " to " + value); return; } mGlobalSettings.putString(mFlagManager.nameToSettingsKey(name), data); } void eraseFlag(Flag flag) { if (flag instanceof SysPropFlag) { mSystemProperties.erase(flag.getName()); dispatchListenersAndMaybeRestart( flag.getName(), suppressRestart -> restartSystemUI( suppressRestart, "SysProp Flag \"" + flag.getNamespace() + "." + flag.getName() + "\" reset to default.")); } else { eraseFlag(flag.getName()); } } /** Erase a flag's overridden value if there is one. */ private void eraseFlag(String name) { eraseInternal(name); removeFromCache(name); dispatchListenersAndMaybeRestart( name, suppressRestart -> restartSystemUI( suppressRestart, "Flag \"" + name + "\" reset to default")); } private void dispatchListenersAndMaybeRestart(String name, Consumer restartAction) { mFlagManager.dispatchListenersAndMaybeRestart(name, restartAction); } /** Works just like {@link #eraseFlag(String)} except that it doesn't restart SystemUI. */ private void eraseInternal(String name) { // We can't actually "erase" things from settings, but we can set them to empty! mGlobalSettings.putString(mFlagManager.nameToSettingsKey(name), ""); Log.i(TAG, "Erase name " + name); } @Override public void addListener(@NonNull Flag flag, @NonNull Listener listener) { mFlagManager.addListener(flag, listener); } @Override public void removeListener(@NonNull Listener listener) { mFlagManager.removeListener(listener); } private void restartSystemUI(boolean requestSuppress, String reason) { if (requestSuppress) { Log.i(TAG, "SystemUI Restart Suppressed"); return; } mRestarter.restartSystemUI(reason); } private void restartAndroid(boolean requestSuppress, String reason) { if (requestSuppress) { Log.i(TAG, "Android Restart Suppressed"); return; } mRestarter.restartAndroid(reason); } void setBooleanFlagInternal(Flag flag, boolean value) { if (flag instanceof BooleanFlag) { setFlagValue(flag.getName(), value, BooleanFlagSerializer.INSTANCE); } else if (flag instanceof ResourceBooleanFlag) { setFlagValue(flag.getName(), value, BooleanFlagSerializer.INSTANCE); } else if (flag instanceof SysPropBooleanFlag) { // Store SysProp flags in SystemProperties where they can read by outside parties. mSystemProperties.setBoolean(flag.getName(), value); dispatchListenersAndMaybeRestart( flag.getName(), suppressRestart -> restartSystemUI( suppressRestart, "Flag \"" + flag.getName() + "\" changed to " + value)); } else { throw new IllegalArgumentException("Unknown flag type"); } } void setStringFlagInternal(Flag flag, String value) { if (flag instanceof StringFlag) { setFlagValue(flag.getName(), value, StringFlagSerializer.INSTANCE); } else if (flag instanceof ResourceStringFlag) { setFlagValue(flag.getName(), value, StringFlagSerializer.INSTANCE); } else { throw new IllegalArgumentException("Unknown flag type"); } } void setIntFlagInternal(Flag flag, int value) { if (flag instanceof IntFlag) { setFlagValue(flag.getName(), value, IntFlagSerializer.INSTANCE); } else if (flag instanceof ResourceIntFlag) { setFlagValue(flag.getName(), value, IntFlagSerializer.INSTANCE); } else { throw new IllegalArgumentException("Unknown flag type"); } } private final BroadcastReceiver mReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { String action = intent == null ? null : intent.getAction(); if (action == null) { return; } if (ACTION_SET_FLAG.equals(action)) { handleSetFlag(intent.getExtras()); } else if (ACTION_GET_FLAGS.equals(action)) { ArrayList> flags = new ArrayList<>(mAllFlags.values()); // Convert all flags to parcelable flags. ArrayList> pFlags = new ArrayList<>(); for (Flag f : flags) { ParcelableFlag pf = toParcelableFlag(f); if (pf != null) { pFlags.add(pf); } } Bundle extras = getResultExtras(true); if (extras != null) { extras.putParcelableArrayList(EXTRA_FLAGS, pFlags); } } } private void handleSetFlag(Bundle extras) { if (extras == null) { Log.w(TAG, "No extras"); return; } String name = extras.getString(EXTRA_NAME); if (name == null || name.isEmpty()) { Log.w(TAG, "NAME not set or is empty: " + name); return; } if (!mAllFlags.containsKey(name)) { Log.w(TAG, "Tried to set unknown name: " + name); return; } Flag flag = mAllFlags.get(name); if (!extras.containsKey(EXTRA_VALUE)) { eraseFlag(flag); return; } Object value = extras.get(EXTRA_VALUE); try { if (value instanceof Boolean) { setBooleanFlagInternal(flag, (Boolean) value); } else if (value instanceof String) { setStringFlagInternal(flag, (String) value); } else { throw new IllegalArgumentException("Unknown value type"); } } catch (IllegalArgumentException e) { Log.w(TAG, "Unable to set " + flag.getName() + " of type " + flag.getClass() + " to value of type " + (value == null ? null : value.getClass())); } } /** * Ensures that the data we send to the app reflects the current state of the flags. * * Also converts an non-parcelable versions of the flags to their parcelable versions. */ @Nullable private ParcelableFlag toParcelableFlag(Flag f) { boolean enabled; boolean teamfood = f.getTeamfood(); boolean overridden; if (f instanceof ReleasedFlag) { enabled = isEnabled((ReleasedFlag) f); overridden = readBooleanFlagOverride(f.getName()) != null; } else if (f instanceof UnreleasedFlag) { enabled = isEnabled((UnreleasedFlag) f); overridden = readBooleanFlagOverride(f.getName()) != null; } else if (f instanceof ResourceBooleanFlag) { enabled = isEnabled((ResourceBooleanFlag) f); overridden = readBooleanFlagOverride(f.getName()) != null; } else if (f instanceof SysPropBooleanFlag) { enabled = isEnabled((SysPropBooleanFlag) f); overridden = !mSystemProperties.get(f.getName()).isEmpty(); } else { // TODO: add support for other flag types. Log.w(TAG, "Unsupported Flag Type. Please file a bug."); return null; } if (enabled) { return new ReleasedFlag(f.getName(), f.getNamespace(), overridden); } else { return new UnreleasedFlag(f.getName(), f.getNamespace(), teamfood, overridden); } } }; private void removeFromCache(String name) { mBooleanFlagCache.remove(name); mStringFlagCache.remove(name); } @Override public void dump(@NonNull PrintWriter pw, @NonNull String[] args) { pw.println("can override: true"); pw.println("teamfood: " + sysuiTeamfood()); pw.println("booleans: " + mBooleanFlagCache.size()); pw.println("example_flag: " + exampleFlag()); pw.println("example_shared_flag: " + exampleSharedFlag()); // Sort our flags for dumping TreeMap dumpBooleanMap = new TreeMap<>(mBooleanFlagCache); dumpBooleanMap.forEach((key, value) -> pw.println(" sysui_flag_" + key + ": " + value)); pw.println("Strings: " + mStringFlagCache.size()); // Sort our flags for dumping TreeMap dumpStringMap = new TreeMap<>(mStringFlagCache); dumpStringMap.forEach((key, value) -> pw.println(" sysui_flag_" + key + ": [length=" + value.length() + "] \"" + value + "\"")); pw.println("Integers: " + mIntFlagCache.size()); // Sort our flags for dumping TreeMap dumpIntMap = new TreeMap<>(mIntFlagCache); dumpIntMap.forEach((key, value) -> pw.println(" sysui_flag_" + key + ": " + value)); } }