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

Commit 5df9de8b authored by TreeHugger Robot's avatar TreeHugger Robot Committed by Android (Google) Code Review
Browse files

Merge changes from topic "jeffdq_flag_proposal"

* changes:
  Generalize some flag manager methods by flag type.
  FeatureFlags Listeners:
parents b1db3547 3b6176a6
Loading
Loading
Loading
Loading
+14 −18
Original line number Diff line number Diff line
@@ -18,28 +18,24 @@ package com.android.systemui.flags
/**
 * Plugin for loading flag values
 */
interface FlagReader {
    /** Returns a boolean value for the given flag.  */
    fun isEnabled(flag: BooleanFlag): Boolean {
        return flag.default
    }

    fun isEnabled(flag: ResourceBooleanFlag): Boolean

    /** Returns a boolean value for the given flag.  */
    fun isEnabled(id: Int, def: Boolean): Boolean {
        return def
    }

    /** Add a listener to be alerted when any flag changes.  */
    fun addListener(listener: Listener) {}
interface FlagListenable {
    /** Add a listener to be alerted when the given flag changes.  */
    fun addListener(flag: Flag<*>, listener: Listener)

    /** Remove a listener to be alerted when any flag changes.  */
    fun removeListener(listener: Listener) {}
    fun removeListener(listener: Listener)

    /** A simple listener to be alerted when a flag changes.  */
    fun interface Listener {
        /**  */
        fun onFlagChanged(id: Int)
        /** Called when the flag changes */
        fun onFlagChanged(event: FlagEvent)
    }

    /** An event representing the change */
    interface FlagEvent {
        /** the id of the flag which changed */
        val flagId: Int
        /** if all listeners alerted invoke this method, the restart will be skipped */
        fun requestNoRestart()
    }
}
 No newline at end of file
+77 −62
Original line number Diff line number Diff line
@@ -24,30 +24,39 @@ import android.database.ContentObserver
import android.net.Uri
import android.os.Bundle
import android.os.Handler
import android.provider.Settings
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(
    private val context: Context,
    private val settings: FlagSettingsHelper,
    private val handler: Handler
) : FlagReader {
) : FlagListenable {
    companion object {
        const val RECEIVING_PACKAGE = "com.android.systemui"
        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"
    }

    private val listeners: MutableSet<FlagReader.Listener> = mutableSetOf()
    constructor(context: Context, handler: Handler) : this(
        context,
        FlagSettingsHelper(context.contentResolver),
        handler
    )

    /**
     * An action called on restart which takes as an argument whether the listeners requested
     * that the restart be suppressed
     */
    var restartAction: Consumer<Boolean>? = null
    var clearCacheAction: Consumer<Int>? = null
    private val listeners: MutableSet<PerFlagListener> = mutableSetOf()
    private val settingsObserver: ContentObserver = SettingsObserver()

    fun getFlagsFuture(): ListenableFuture<Collection<Flag<*>>> {
@@ -61,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 {
@@ -73,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)
    }
@@ -86,49 +105,30 @@ class FlagManager constructor(
        context.sendBroadcast(intent)
    }

    override fun isEnabled(id: Int, def: Boolean): Boolean {
        return isEnabled(id) ?: def
    }

    /** Returns the stored value or null if not set.  */
    fun isEnabled(id: Int): Boolean? {
        val data: String? = Settings.Secure.getString(
            context.contentResolver, 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 isEnabled(flag: ResourceBooleanFlag): Boolean {
        throw RuntimeException("Not implemented in FlagManager")
    }

    override fun addListener(listener: FlagReader.Listener) {
    override fun addListener(flag: Flag<*>, listener: FlagListenable.Listener) {
        synchronized(listeners) {
            val registerNeeded = listeners.isEmpty()
            listeners.add(listener)
            listeners.add(PerFlagListener(flag.id, listener))
            if (registerNeeded) {
                context.contentResolver.registerContentObserver(
                    Settings.Secure.getUriFor(SETTINGS_PREFIX), true, settingsObserver)
                settings.registerContentObserver(SETTINGS_PREFIX, true, settingsObserver)
            }
        }
    }

    override fun removeListener(listener: FlagReader.Listener) {
    override fun removeListener(listener: FlagListenable.Listener) {
        synchronized(listeners) {
            val isRegistered = !listeners.isEmpty()
            listeners.remove(listener)
            if (isRegistered && listeners.isEmpty()) {
                context.contentResolver.unregisterContentObserver(settingsObserver)
            if (listeners.isEmpty()) {
                return
            }
            listeners.removeIf { it.listener == listener }
            if (listeners.isEmpty()) {
                settings.unregisterContentObserver(settingsObserver)
            }
        }
    }
@@ -136,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) {
@@ -160,17 +152,40 @@ class FlagManager constructor(
            }
            val parts = uri.pathSegments
            val idStr = parts[parts.size - 1]
            try {
                val id = idStr.toInt()
                listeners.forEach { l -> l.onFlagChanged(id) }
            } catch (e: NumberFormatException) {
                // no-op
            val id = try { idStr.toInt() } catch (e: NumberFormatException) { return }
            clearCacheAction?.accept(id)
            dispatchListenersAndMaybeRestart(id)
        }
    }

    fun dispatchListenersAndMaybeRestart(id: Int) {
        val filteredListeners: List<FlagListenable.Listener> = synchronized(listeners) {
            listeners.mapNotNull { if (it.id == id) it.listener else null }
        }
        // If there are no listeners, there's nothing to dispatch to, and nothing to suppress it.
        if (filteredListeners.isEmpty()) {
            restartAction?.accept(false)
            return
        }
        // Dispatch to every listener and save whether each one called requestNoRestart.
        val suppressRestartList: List<Boolean> = filteredListeners.map { listener ->
            var didRequestNoRestart = false
            val event = object : FlagListenable.FlagEvent {
                override val flagId = id
                override fun requestNoRestart() {
                    didRequestNoRestart = true
                }
            }
            listener.onFlagChanged(event)
            didRequestNoRestart
        }
        // Suppress restart only if ALL listeners request it.
        val suppressRestart = suppressRestartList.all { it }
        restartAction?.accept(suppressRestart)
    }

class InvalidFlagStorageException : Exception("Data found but is invalid")
    private data class PerFlagListener(val id: Int, val listener: FlagListenable.Listener)
}

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")
+42 −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.content.ContentResolver
import android.database.ContentObserver
import android.provider.Settings

class FlagSettingsHelper(private val contentResolver: ContentResolver) {

    fun getString(key: String): String? = Settings.Secure.getString(contentResolver, key)

    fun registerContentObserver(
        name: String,
        notifyForDescendants: Boolean,
        observer: ContentObserver
    ) {
        contentResolver.registerContentObserver(
            Settings.Secure.getUriFor(name),
            notifyForDescendants,
            observer
        )
    }

    fun unregisterContentObserver(observer: ContentObserver) {
        contentResolver.unregisterContentObserver(observer)
    }
}
 No newline at end of file
+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
Loading