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

Commit 3b6176a6 authored by Jeff DeCew's avatar Jeff DeCew
Browse files

Generalize some flag manager methods by flag type.

Bug: 209081785
Test: atest FeatureFlagsDebugTest FlagManagerTest
Change-Id: Ie92667e33e7c086258cdd03e7b9ebccdefef5938
parent 59595648
Loading
Loading
Loading
Loading
+21 −36
Original line number Diff line number Diff line
@@ -26,8 +26,6 @@ import android.os.Bundle
import android.os.Handler
import androidx.concurrent.futures.CallbackToFutureAdapter
import com.google.common.util.concurrent.ListenableFuture
import org.json.JSONException
import org.json.JSONObject
import java.util.function.Consumer

class FlagManager constructor(
@@ -40,11 +38,9 @@ class FlagManager constructor(
        const val ACTION_SET_FLAG = "com.android.systemui.action.SET_FLAG"
        const val ACTION_GET_FLAGS = "com.android.systemui.action.GET_FLAGS"
        const val FLAGS_PERMISSION = "com.android.systemui.permission.FLAGS"
        const val FIELD_ID = "id"
        const val FIELD_VALUE = "value"
        const val FIELD_TYPE = "type"
        const val FIELD_FLAGS = "flags"
        const val TYPE_BOOLEAN = "boolean"
        const val EXTRA_ID = "id"
        const val EXTRA_VALUE = "value"
        const val EXTRA_FLAGS = "flags"
        private const val SETTINGS_PREFIX = "systemui/flags"
    }

@@ -74,7 +70,7 @@ class FlagManager constructor(
                        override fun onReceive(context: Context, intent: Intent) {
                            val extras: Bundle? = getResultExtras(false)
                            val listOfFlags: java.util.ArrayList<ParcelableFlag<*>>? =
                                extras?.getParcelableArrayList(FIELD_FLAGS)
                                extras?.getParcelableArrayList(EXTRA_FLAGS)
                            if (listOfFlags != null) {
                                completer.set(listOfFlags)
                            } else {
@@ -86,9 +82,19 @@ class FlagManager constructor(
        } as ListenableFuture<Collection<Flag<*>>>
    }

    /**
     * Returns the stored value or null if not set.
     * This API is used by TheFlippinApp.
     */
    fun isEnabled(id: Int): Boolean? = readFlagValue(id, BooleanFlagSerializer)

    /**
     * Sets the value of a boolean flag.
     * This API is used by TheFlippinApp.
     */
    fun setFlagValue(id: Int, enabled: Boolean) {
        val intent = createIntent(id)
        intent.putExtra(FIELD_VALUE, enabled)
        intent.putExtra(EXTRA_VALUE, enabled)

        context.sendBroadcast(intent)
    }
@@ -100,20 +106,9 @@ class FlagManager constructor(
    }

    /** Returns the stored value or null if not set.  */
    fun isEnabled(id: Int): Boolean? {
        val data = settings.getString(keyToSettingsPrefix(id))
        if (data == null || data?.isEmpty()) {
            return null
        }
        val json: JSONObject
        try {
            json = JSONObject(data)
            return if (!assertType(json, TYPE_BOOLEAN)) {
                null
            } else json.getBoolean(FIELD_VALUE)
        } catch (e: JSONException) {
            throw InvalidFlagStorageException()
        }
    fun <T> readFlagValue(id: Int, serializer: FlagSerializer<T>): T? {
        val data = settings.getString(idToSettingsKey(id))
        return serializer.fromSettingsData(data)
    }

    override fun addListener(flag: Flag<*>, listener: FlagListenable.Listener) {
@@ -141,21 +136,13 @@ class FlagManager constructor(
    private fun createIntent(id: Int): Intent {
        val intent = Intent(ACTION_SET_FLAG)
        intent.setPackage(RECEIVING_PACKAGE)
        intent.putExtra(FIELD_ID, id)
        intent.putExtra(EXTRA_ID, id)

        return intent
    }

    fun keyToSettingsPrefix(key: Int): String {
        return "$SETTINGS_PREFIX/$key"
    }

    private fun assertType(json: JSONObject, type: String): Boolean {
        return try {
            json.getString(FIELD_TYPE) == TYPE_BOOLEAN
        } catch (e: JSONException) {
            false
        }
    fun idToSettingsKey(id: Int): String {
        return "$SETTINGS_PREFIX/$id"
    }

    inner class SettingsObserver : ContentObserver(handler) {
@@ -200,7 +187,5 @@ class FlagManager constructor(
    private data class PerFlagListener(val id: Int, val listener: FlagListenable.Listener)
}

class InvalidFlagStorageException : Exception("Data found but is invalid")

class NoFlagResultsException : Exception(
    "SystemUI failed to communicate its flags back successfully")
 No newline at end of file
+80 −0
Original line number Diff line number Diff line
/*
 * 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 android.util.Log
import org.json.JSONException
import org.json.JSONObject

private const val FIELD_VALUE = "value"
private const val FIELD_TYPE = "type"
private const val TYPE_BOOLEAN = "boolean"
private const val TYPE_STRING = "string"

private const val TAG = "FlagSerializer"

abstract class FlagSerializer<T>(
    private val type: String,
    private val setter: (JSONObject, String, T) -> Unit,
    private val getter: (JSONObject, String) -> T
) {
    fun toSettingsData(value: T): String? {
        return try {
            JSONObject()
                .put(FIELD_TYPE, type)
                .also { setter(it, FIELD_VALUE, value) }
                .toString()
        } catch (e: JSONException) {
            Log.w(TAG, "write error", e)
            null
        }
    }

    /**
     * @throws InvalidFlagStorageException
     */
    fun fromSettingsData(data: String?): T? {
        if (data == null || data.isEmpty()) {
            return null
        }
        try {
            val json = JSONObject(data)
            return if (json.getString(FIELD_TYPE) == type) {
                getter(json, FIELD_VALUE)
            } else {
                null
            }
        } catch (e: JSONException) {
            Log.w(TAG, "read error", e)
            throw InvalidFlagStorageException()
        }
    }
}

object BooleanFlagSerializer : FlagSerializer<Boolean>(
    TYPE_BOOLEAN,
    JSONObject::put,
    JSONObject::getBoolean
)

object StringFlagSerializer : FlagSerializer<String>(
    TYPE_STRING,
    JSONObject::put,
    JSONObject::getString
)

class InvalidFlagStorageException : Exception("Data found but is invalid")
+5 −0
Original line number Diff line number Diff line
@@ -23,6 +23,7 @@ import com.android.systemui.util.settings.SettingsUtilModule
import dagger.Binds
import dagger.Module
import dagger.Provides
import java.util.function.Supplier

@Module(includes = [
    SettingsUtilModule::class
@@ -38,5 +39,9 @@ abstract class FlagsModule {
        fun provideFlagManager(context: Context, @Main handler: Handler): FlagManager {
            return FlagManager(context, handler)
        }

        @JvmStatic
        @Provides
        fun providesFlagCollector(): Supplier<Map<Int, Flag<*>>>? = null
    }
}
 No newline at end of file
+6 −0
Original line number Diff line number Diff line
@@ -27,4 +27,10 @@ interface FeatureFlags : FlagListenable {

    /** Returns a boolean value for the given flag.  */
    fun isEnabled(flag: ResourceBooleanFlag): Boolean

    /** Returns a string value for the given flag.  */
    fun getString(flag: StringFlag): String

    /** Returns a string value for the given flag.  */
    fun getString(flag: ResourceStringFlag): String
}
 No newline at end of file
+89 −48
Original line number Diff line number Diff line
@@ -18,9 +18,11 @@ package com.android.systemui.flags;

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.FIELD_FLAGS;
import static com.android.systemui.flags.FlagManager.FIELD_ID;
import static com.android.systemui.flags.FlagManager.FIELD_VALUE;
import static com.android.systemui.flags.FlagManager.EXTRA_FLAGS;
import static com.android.systemui.flags.FlagManager.EXTRA_ID;
import static com.android.systemui.flags.FlagManager.EXTRA_VALUE;

import static java.util.Objects.requireNonNull;

import android.content.BroadcastReceiver;
import android.content.Context;
@@ -39,14 +41,13 @@ import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.dump.DumpManager;
import com.android.systemui.util.settings.SecureSettings;

import org.json.JSONException;
import org.json.JSONObject;

import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.TreeMap;
import java.util.function.Supplier;

import javax.inject.Inject;

@@ -66,7 +67,9 @@ public class FeatureFlagsDebug implements FeatureFlags, Dumpable {
    private final FlagManager mFlagManager;
    private final SecureSettings mSecureSettings;
    private final Resources mResources;
    private final Map<Integer, Boolean> mBooleanFlagCache = new HashMap<>();
    private final Supplier<Map<Integer, Flag<?>>> mFlagsCollector;
    private final Map<Integer, Boolean> mBooleanFlagCache = new TreeMap<>();
    private final Map<Integer, String> mStringFlagCache = new TreeMap<>();

    @Inject
    public FeatureFlagsDebug(
@@ -74,10 +77,12 @@ public class FeatureFlagsDebug implements FeatureFlags, Dumpable {
            Context context,
            SecureSettings secureSettings,
            @Main Resources resources,
            DumpManager dumpManager) {
            DumpManager dumpManager,
            @Nullable Supplier<Map<Integer, Flag<?>>> flagsCollector) {
        mFlagManager = flagManager;
        mSecureSettings = secureSettings;
        mResources = resources;
        mFlagsCollector = flagsCollector != null ? flagsCollector : Flags::collectFlags;
        IntentFilter filter = new IntentFilter();
        filter.addAction(ACTION_SET_FLAG);
        filter.addAction(ACTION_GET_FLAGS);
@@ -88,58 +93,85 @@ public class FeatureFlagsDebug implements FeatureFlags, Dumpable {
    }

    @Override
    public boolean isEnabled(BooleanFlag flag) {
    public boolean isEnabled(@NonNull BooleanFlag flag) {
        int id = flag.getId();
        if (!mBooleanFlagCache.containsKey(id)) {
            mBooleanFlagCache.put(id, isEnabled(id, flag.getDefault()));
            mBooleanFlagCache.put(id,
                    readFlagValue(id, flag.getDefault(), BooleanFlagSerializer.INSTANCE));
        }

        return mBooleanFlagCache.get(id);
    }

    @Override
    public boolean isEnabled(ResourceBooleanFlag flag) {
    public boolean isEnabled(@NonNull ResourceBooleanFlag flag) {
        int id = flag.getId();
        if (!mBooleanFlagCache.containsKey(id)) {
            mBooleanFlagCache.put(
                    id, isEnabled(id, mResources.getBoolean(flag.getResourceId())));
            mBooleanFlagCache.put(id,
                    readFlagValue(id, mResources.getBoolean(flag.getResourceId()),
                            BooleanFlagSerializer.INSTANCE));
        }

        return mBooleanFlagCache.get(id);
    }

    /** Return a flag's value. */
    private boolean isEnabled(int id, boolean defaultValue) {
        Boolean result = isEnabledInternal(id);
    @NonNull
    @Override
    public String getString(@NonNull StringFlag flag) {
        int id = flag.getId();
        if (!mStringFlagCache.containsKey(id)) {
            mStringFlagCache.put(id,
                    readFlagValue(id, flag.getDefault(), StringFlagSerializer.INSTANCE));
        }

        return mStringFlagCache.get(id);
    }

    @NonNull
    @Override
    public String getString(@NonNull ResourceStringFlag flag) {
        int id = flag.getId();
        if (!mStringFlagCache.containsKey(id)) {
            mStringFlagCache.put(id,
                    readFlagValue(id, mResources.getString(flag.getResourceId()),
                            StringFlagSerializer.INSTANCE));
        }

        return mStringFlagCache.get(id);
    }

    @NonNull
    private <T> T readFlagValue(int id, @NonNull T defaultValue, FlagSerializer<T> serializer) {
        requireNonNull(defaultValue, "defaultValue");
        T result = readFlagValueInternal(id, serializer);
        return result == null ? defaultValue : result;
    }


    /** Returns the stored value or null if not set. */
    private Boolean isEnabledInternal(int id) {
    @Nullable
    private <T> T readFlagValueInternal(int id, FlagSerializer<T> serializer) {
        try {
            return mFlagManager.isEnabled(id);
            return mFlagManager.readFlagValue(id, serializer);
        } catch (Exception e) {
            eraseInternal(id);
        }
        return null;
    }

    /** Set whether a given {@link BooleanFlag} is enabled or not. */
    public void setEnabled(int id, boolean value) {
        Boolean currentValue = isEnabledInternal(id);
        if (currentValue != null && currentValue == value) {
    private <T> void setFlagValue(int id, @NonNull T value, FlagSerializer<T> serializer) {
        requireNonNull(value, "Cannot set a null value");
        T currentValue = readFlagValueInternal(id, serializer);
        if (Objects.equals(currentValue, value)) {
            Log.i(TAG, "Flag id " + id + " is already " + value);
            return;
        }

        JSONObject json = new JSONObject();
        try {
            json.put(FlagManager.FIELD_TYPE, FlagManager.TYPE_BOOLEAN);
            json.put(FIELD_VALUE, value);
            mSecureSettings.putString(mFlagManager.keyToSettingsPrefix(id), json.toString());
        } catch (JSONException e) {
            return;  // ignore
        final String data = serializer.toSettingsData(value);
        if (data == null) {
            Log.w(TAG, "Failed to set id " + id + " to " + value);
            return;
        }
        mSecureSettings.putString(mFlagManager.idToSettingsKey(id), data);
        Log.i(TAG, "Set id " + id + " to " + value);
        removeFromCache(id);
        mFlagManager.dispatchListenersAndMaybeRestart(id);
@@ -155,7 +187,7 @@ public class FeatureFlagsDebug implements FeatureFlags, Dumpable {
    /** Works just like {@link #eraseFlag(int)} except that it doesn't restart SystemUI. */
    private void eraseInternal(int id) {
        // We can't actually "erase" things from sysprops, but we can set them to empty!
        mSecureSettings.putString(mFlagManager.keyToSettingsPrefix(id), "");
        mSecureSettings.putString(mFlagManager.idToSettingsKey(id), "");
        Log.i(TAG, "Erase id " + id);
    }

@@ -182,14 +214,14 @@ public class FeatureFlagsDebug implements FeatureFlags, Dumpable {
    private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            String action = intent.getAction();
            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)) {
                Map<Integer, Flag<?>> knownFlagMap = Flags.collectFlags();
                Map<Integer, Flag<?>> knownFlagMap = mFlagsCollector.get();
                ArrayList<Flag<?>> flags = new ArrayList<>(knownFlagMap.values());

                // Convert all flags to parcelable flags.
@@ -203,7 +235,7 @@ public class FeatureFlagsDebug implements FeatureFlags, Dumpable {

                Bundle extras =  getResultExtras(true);
                if (extras != null) {
                    extras.putParcelableArrayList(FIELD_FLAGS, pFlags);
                    extras.putParcelableArrayList(EXTRA_FLAGS, pFlags);
                }
            }
        }
@@ -213,26 +245,37 @@ public class FeatureFlagsDebug implements FeatureFlags, Dumpable {
                Log.w(TAG, "No extras");
                return;
            }
            int id = extras.getInt(FIELD_ID);
            int id = extras.getInt(EXTRA_ID);
            if (id <= 0) {
                Log.w(TAG, "ID not set or less than  or equal to 0: " + id);
                return;
            }

            Map<Integer, Flag<?>> flagMap = Flags.collectFlags();
            Map<Integer, Flag<?>> flagMap = mFlagsCollector.get();
            if (!flagMap.containsKey(id)) {
                Log.w(TAG, "Tried to set unknown id: " + id);
                return;
            }
            Flag<?> flag = flagMap.get(id);

            if (!extras.containsKey(FIELD_VALUE)) {
            if (!extras.containsKey(EXTRA_VALUE)) {
                eraseFlag(id);
                return;
            }

            if (flag instanceof BooleanFlag) {
                setEnabled(id, extras.getBoolean(FIELD_VALUE));
            Object value = extras.get(EXTRA_VALUE);
            if (flag instanceof BooleanFlag && value instanceof Boolean) {
                setFlagValue(id, (Boolean) value, BooleanFlagSerializer.INSTANCE);
            } else  if (flag instanceof ResourceBooleanFlag && value instanceof Boolean) {
                setFlagValue(id, (Boolean) value, BooleanFlagSerializer.INSTANCE);
            } else if (flag instanceof StringFlag && value instanceof String) {
                setFlagValue(id, (String) value, StringFlagSerializer.INSTANCE);
            } else if (flag instanceof ResourceStringFlag && value instanceof String) {
                setFlagValue(id, (String) value, StringFlagSerializer.INSTANCE);
            } else {
                Log.w(TAG,
                        "Unable to set " + id + " of type " + flag.getClass() + " to value of type "
                                + (value == null ? null : value.getClass()));
            }
        }

@@ -258,18 +301,16 @@ public class FeatureFlagsDebug implements FeatureFlags, Dumpable {

    private void removeFromCache(int id) {
        mBooleanFlagCache.remove(id);
        mStringFlagCache.remove(id);
    }

    @Override
    public void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter pw, @NonNull String[] args) {
        pw.println("can override: true");
        ArrayList<String> flagStrings = new ArrayList<>(mBooleanFlagCache.size());
        for (Map.Entry<Integer, Boolean> entry : mBooleanFlagCache.entrySet()) {
            flagStrings.add("  sysui_flag_" + entry.getKey() + ": " + entry.getValue());
        }
        flagStrings.sort(String.CASE_INSENSITIVE_ORDER);
        for (String flagString : flagStrings) {
            pw.println(flagString);
        }
        pw.println("booleans: " + mBooleanFlagCache.size());
        mBooleanFlagCache.forEach((key, value) -> pw.println("  sysui_flag_" + key + ": " + value));
        pw.println("Strings: " + mStringFlagCache.size());
        mStringFlagCache.forEach((key, value) -> pw.println("  sysui_flag_" + key
                + ": [length=" + value.length() + "] \"" + value + "\""));
    }
}
Loading