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

Commit 63bf93cf authored by Sunny Goyal's avatar Sunny Goyal Committed by Android (Google) Code Review
Browse files

Merge "Freezing legacy feature flags" into main

parents 24f88860 8049369a
Loading
Loading
Loading
Loading
+0 −59
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 com.android.launcher3.uioverrides.flags;

import static com.android.launcher3.uioverrides.flags.FlagsFactory.TEAMFOOD_FLAG;

import androidx.annotation.NonNull;

import com.android.launcher3.config.FeatureFlags.BooleanFlag;
import com.android.launcher3.config.FeatureFlags.FlagState;

class DebugFlag extends BooleanFlag {

    public final String key;
    public final String description;

    @NonNull
    public final FlagState defaultValue;

    DebugFlag(String key, String description, FlagState defaultValue, boolean currentValue) {
        super(currentValue);
        this.key = key;
        this.defaultValue = defaultValue;
        this.description = description;
    }

    /**
     * Returns {@code true} if this flag's value has been modified from its default.
     * <p>
     * This helps to identify which flags have been toggled in log dumps and bug reports to
     * further help triaging and debugging.
     */
    boolean currentValueModified() {
        switch (defaultValue) {
            case ENABLED: return !get();
            case TEAMFOOD: return TEAMFOOD_FLAG.get() != get();
            case DISABLED: return get();
            default: return true;
        }
    }

    @Override
    public String toString() {
        return key + ": defaultValue=" + defaultValue + ", mCurrentValue=" + get();
    }
}
+1 −3
Original line number Diff line number Diff line
@@ -75,7 +75,6 @@ public class DeveloperOptionsUI {

    private static final String ACTION_PLUGIN_SETTINGS =
            "com.android.systemui.action.PLUGIN_SETTINGS";
    private static final String TAG = "DeveloperOptionsUI";
    private static final String PLUGIN_PERMISSION = "com.android.systemui.permission.PLUGIN";

    private final PreferenceFragmentCompat mFragment;
@@ -86,6 +85,7 @@ public class DeveloperOptionsUI {
    public DeveloperOptionsUI(PreferenceFragmentCompat fragment, PreferenceCategory flags) {
        mFragment = fragment;
        mPreferenceScreen = fragment.getPreferenceScreen();
        flags.getParent().removePreference(flags);

        // Add search bar
        View listView = mFragment.getListView();
@@ -95,8 +95,6 @@ public class DeveloperOptionsUI {
        parent.addView(topBar, parent.indexOfChild(listView));
        initSearch(topBar.findViewById(R.id.filter_box));

        new FlagTogglerPrefUi(mFragment.requireActivity(), topBar.findViewById(R.id.flag_apply_btn))
                .applyTo(flags);
        DevOptionsUiHelper uiHelper = new DevOptionsUiHelper();
        uiHelper.inflateServerFlags(newCategory("Server flags"));

+0 −40
Original line number Diff line number Diff line
/*
 * Copyright (C) 2019 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.launcher3.uioverrides.flags;

import com.android.launcher3.config.FeatureFlags.FlagState;

class DeviceFlag extends DebugFlag {

    private final boolean mDefaultValueInCode;

    DeviceFlag(String key, String description, FlagState defaultValue,
            boolean currentValue, boolean defaultValueInCode) {
        super(key, description, defaultValue, currentValue);
        mDefaultValueInCode = defaultValueInCode;
    }

    @Override
    boolean currentValueModified() {
        return super.currentValueModified() || mDefaultValueInCode != get();
    }

    @Override
    public String toString() {
        return super.toString() + ", mDefaultValueInCode=" + mDefaultValueInCode;
    }
}
+0 −172
Original line number Diff line number Diff line
/*
 * Copyright (C) 2018 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.launcher3.uioverrides.flags;

import static com.android.launcher3.config.FeatureFlags.FlagState.TEAMFOOD;
import static com.android.launcher3.uioverrides.flags.FlagsFactory.TEAMFOOD_FLAG;

import android.app.Activity;
import android.content.Context;
import android.os.Handler;
import android.os.Process;
import android.text.Html;
import android.text.TextUtils;
import android.util.Log;
import android.view.View;
import android.widget.Toast;

import androidx.preference.PreferenceDataStore;
import androidx.preference.PreferenceGroup;
import androidx.preference.PreferenceViewHolder;
import androidx.preference.SwitchPreference;

import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.util.ActivityLifecycleCallbacksAdapter;

import java.util.List;
import java.util.Set;

/**
 * Dev-build only UI allowing developers to toggle flag settings. See {@link FeatureFlags}.
 */
public final class FlagTogglerPrefUi implements ActivityLifecycleCallbacksAdapter {

    private static final String TAG = "FlagTogglerPrefFrag";

    private final View mFlagsApplyButton;
    private final Context mContext;

    private final PreferenceDataStore mDataStore = new PreferenceDataStore() {

        @Override
        public void putBoolean(String key, boolean value) {
            FlagsFactory.getSharedPreferences().edit().putBoolean(key, value).apply();
            updateMenu();
        }

        @Override
        public boolean getBoolean(String key, boolean defaultValue) {
            return FlagsFactory.getSharedPreferences().getBoolean(key, defaultValue);
        }
    };

    public FlagTogglerPrefUi(Activity activity, View flagsApplyButton) {
        mFlagsApplyButton = flagsApplyButton;
        mContext = mFlagsApplyButton.getContext();
        activity.registerActivityLifecycleCallbacks(this);

        mFlagsApplyButton.setOnClickListener(v -> {
            FlagsFactory.getSharedPreferences().edit().commit();
            Log.e(TAG,
                    "Killing launcher process " + Process.myPid() + " to apply new flag values");
            System.exit(0);
        });
    }

    public void applyTo(PreferenceGroup parent) {
        Set<String> modifiedPrefs = FlagsFactory.getSharedPreferences().getAll().keySet();
        List<DebugFlag> flags = FlagsFactory.getDebugFlags();
        flags.sort((f1, f2) -> {
            // Sort first by any prefs that the user has changed, then alphabetically.
            int changeComparison = Boolean.compare(
                    modifiedPrefs.contains(f2.key), modifiedPrefs.contains(f1.key));
            return changeComparison != 0
                    ? changeComparison
                    : f1.key.compareToIgnoreCase(f2.key);
        });

        // Ensure that teamfood flag comes on the top
        if (flags.remove(TEAMFOOD_FLAG)) {
            flags.add(0, (DebugFlag) TEAMFOOD_FLAG);
        }

        // For flag overrides we only want to store when the engineer chose to override the
        // flag with a different value than the default. That way, when we flip flags in
        // future, engineers will pick up the new value immediately. To accomplish this, we use a
        // custom preference data store.
        for (DebugFlag flag : flags) {
            SwitchPreference switchPreference = new SwitchPreference(mContext) {
                @Override
                public void onBindViewHolder(PreferenceViewHolder holder) {
                    super.onBindViewHolder(holder);
                    holder.itemView.setOnLongClickListener(v -> {
                        FlagsFactory.getSharedPreferences().edit().remove(flag.key).apply();
                        setChecked(getFlagStateFromSharedPrefs(flag));
                        updateSummary(this, flag);
                        updateMenu();
                        return true;
                    });
                }
            };
            switchPreference.setKey(flag.key);
            switchPreference.setDefaultValue(FlagsFactory.getEnabledValue(flag.defaultValue));
            switchPreference.setChecked(getFlagStateFromSharedPrefs(flag));
            switchPreference.setTitle(flag.key);
            updateSummary(switchPreference, flag);
            switchPreference.setPreferenceDataStore(mDataStore);
            switchPreference.setOnPreferenceChangeListener((p, v) -> {
                new Handler().post(() -> updateSummary(switchPreference, flag));
                return true;
            });


            parent.addPreference(switchPreference);
        }
        updateMenu();
    }

    /**
     * Updates the summary to show the description and whether the flag overrides the default value.
     */
    private void updateSummary(SwitchPreference switchPreference, DebugFlag flag) {
        String summary = flag.defaultValue == TEAMFOOD
                ? "<font color='blue'><b>[TEAMFOOD]</b> </font>" : "";
        if (FlagsFactory.getSharedPreferences().contains(flag.key)) {
            summary += "<font color='red'><b>[OVERRIDDEN]</b> </font>";
        }
        if (!TextUtils.isEmpty(summary)) {
            summary += "<br>";
        }
        switchPreference.setSummary(Html.fromHtml(summary + flag.description));
    }

    public void updateMenu() {
        mFlagsApplyButton.setVisibility(anyChanged() ? View.VISIBLE : View.INVISIBLE);
    }

    @Override
    public void onActivityStopped(Activity activity) {
        if (anyChanged()) {
            Toast.makeText(mContext, "Flag won't be applied until you restart launcher",
                    Toast.LENGTH_LONG).show();
        }
    }

    private boolean getFlagStateFromSharedPrefs(DebugFlag flag) {
        boolean defaultValue = FlagsFactory.getEnabledValue(flag.defaultValue);
        return mDataStore.getBoolean(flag.key, defaultValue);
    }

    private boolean anyChanged() {
        for (DebugFlag flag : FlagsFactory.getDebugFlags()) {
            if (getFlagStateFromSharedPrefs(flag) != flag.get()) {
                return true;
            }
        }
        return false;
    }
}
+0 −198
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 com.android.launcher3.uioverrides.flags;

import static android.app.ActivityThread.currentApplication;

import static com.android.launcher3.BuildConfig.IS_DEBUG_DEVICE;
import static com.android.launcher3.config.FeatureFlags.FlagState.DISABLED;
import static com.android.launcher3.config.FeatureFlags.FlagState.ENABLED;
import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;

import android.content.Context;
import android.content.SharedPreferences;
import android.provider.DeviceConfig;
import android.provider.DeviceConfig.Properties;
import android.util.Log;

import androidx.annotation.NonNull;

import com.android.launcher3.config.FeatureFlags.BooleanFlag;
import com.android.launcher3.config.FeatureFlags.FlagState;
import com.android.launcher3.util.ScreenOnTracker;

import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

/**
 * Helper class to create various flags for system build
 */
public class FlagsFactory {

    private static final String TAG = "FlagsFactory";

    private static final FlagsFactory INSTANCE = new FlagsFactory();
    private static final boolean FLAG_AUTO_APPLY_ENABLED = true;

    private static final String FLAGS_PREF_NAME = "featureFlags";
    public static final String NAMESPACE_LAUNCHER = "launcher";

    private static final List<DebugFlag> sDebugFlags = new ArrayList<>();
    private static SharedPreferences sSharedPreferences;

    static final BooleanFlag TEAMFOOD_FLAG = getReleaseFlag(
            0, "LAUNCHER_TEAMFOOD", DISABLED, "Enable this flag to opt-in all team food flags");

    private final Set<String> mKeySet = new HashSet<>();
    private boolean mRestartRequested = false;

    private FlagsFactory() {
        if (!FLAG_AUTO_APPLY_ENABLED) {
            return;
        }
        DeviceConfig.addOnPropertiesChangedListener(
                NAMESPACE_LAUNCHER, UI_HELPER_EXECUTOR, this::onPropertiesChanged);
    }

    static boolean getEnabledValue(FlagState flagState) {
        if (IS_DEBUG_DEVICE) {
            switch (flagState) {
                case ENABLED:
                    return true;
                case TEAMFOOD:
                    return TEAMFOOD_FLAG.get();
                default:
                    return false;
            }
        } else {
            return flagState == ENABLED;
        }
    }

    /**
     * Creates a new debug flag. Debug flags always take their default value in release builds. On
     * dogfood builds, they can be manually turned on using the flag toggle UI.
     */
    public static BooleanFlag getDebugFlag(
            int bugId, String key, FlagState flagState, String description) {
        if (IS_DEBUG_DEVICE) {
            boolean defaultValue = getEnabledValue(flagState);
            boolean currentValue = getSharedPreferences().getBoolean(key, defaultValue);
            DebugFlag flag = new DebugFlag(key, description, flagState, currentValue);
            sDebugFlags.add(flag);
            return flag;
        } else {
            return new BooleanFlag(getEnabledValue(flagState));
        }
    }

    /**
     * Creates a new release flag. Release flags can be rolled out using server configurations and
     * also allow manual overrides on debug builds.
     */
    public static BooleanFlag getReleaseFlag(
            int bugId, String key, FlagState flagState, String description) {
        INSTANCE.mKeySet.add(key);
        boolean defaultValueInCode = getEnabledValue(flagState);
        boolean defaultValue = DeviceConfig.getBoolean(NAMESPACE_LAUNCHER, key, defaultValueInCode);
        if (IS_DEBUG_DEVICE) {
            boolean currentValue = getSharedPreferences().getBoolean(key, defaultValue);
            DebugFlag flag = new DeviceFlag(key, description,
                    (defaultValue == defaultValueInCode) ? flagState
                            : defaultValue ? ENABLED : DISABLED, currentValue, defaultValueInCode);
            sDebugFlags.add(flag);
            return flag;
        } else {
            return new BooleanFlag(defaultValue);
        }
    }


    static List<DebugFlag> getDebugFlags() {
        if (!IS_DEBUG_DEVICE) {
            return Collections.emptyList();
        }
        synchronized (sDebugFlags) {
            return new ArrayList<>(sDebugFlags);
        }
    }

    /** Returns the SharedPreferences instance backing Debug FeatureFlags. */
    @NonNull
    static SharedPreferences getSharedPreferences() {
        if (sSharedPreferences == null) {
            sSharedPreferences = currentApplication()
                    .createDeviceProtectedStorageContext()
                    .getSharedPreferences(FLAGS_PREF_NAME, Context.MODE_PRIVATE);
        }
        return sSharedPreferences;
    }

    /**
     * Dumps the current flags state to the print writer
     */
    public static void dump(PrintWriter pw) {
        if (!IS_DEBUG_DEVICE) {
            return;
        }
        pw.println("DeviceFlags:");
        pw.println("  BooleanFlags:");
        synchronized (sDebugFlags) {
            for (DebugFlag flag : sDebugFlags) {
                if (flag instanceof DeviceFlag) {
                    pw.println((flag.currentValueModified() ? "  ->" : "    ") + flag);
                }
            }
        }
        pw.println("  DebugFlags:");
        synchronized (sDebugFlags) {
            for (DebugFlag flag : sDebugFlags) {
                if (!(flag instanceof DeviceFlag)) {
                    pw.println((flag.currentValueModified() ? "  ->" : "    ") + flag);
                }
            }
        }
    }

    private void onPropertiesChanged(Properties properties) {
        if (!Collections.disjoint(properties.getKeyset(), mKeySet)) {
            // Schedule a restart
            if (mRestartRequested) {
                return;
            }
            Log.e(TAG, "Flag changed, scheduling restart");
            mRestartRequested = true;
            ScreenOnTracker sot = ScreenOnTracker.INSTANCE.get(currentApplication());
            if (sot.isScreenOn()) {
                sot.addListener(this::onScreenOnChanged);
            } else {
                onScreenOnChanged(false);
            }
        }
    }

    private void onScreenOnChanged(boolean isOn) {
        if (mRestartRequested && !isOn) {
            Log.e(TAG, "Restart requested, killing process");
            System.exit(0);
        }
    }
}
Loading