/* * 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.app.Activity import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.database.ContentObserver import android.net.Uri import android.os.Bundle import android.os.Handler import androidx.concurrent.futures.CallbackToFutureAdapter import com.google.common.util.concurrent.ListenableFuture import java.util.function.Consumer class FlagManager constructor( private val context: Context, private val settings: FlagSettingsHelper, private val handler: Handler ) : 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 ACTION_SYSUI_STARTED = "com.android.systemui.STARTED" const val EXTRA_NAME = "name" const val EXTRA_VALUE = "value" const val EXTRA_FLAGS = "flags" private const val SETTINGS_PREFIX = "systemui/flags" } 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 onSettingsChangedAction: Consumer? = null var clearCacheAction: Consumer? = null private val listeners: MutableSet = mutableSetOf() private val settingsObserver: ContentObserver = SettingsObserver() fun getFlagsFuture(): ListenableFuture>> { val intent = Intent(ACTION_GET_FLAGS) intent.setPackage(RECEIVING_PACKAGE) return CallbackToFutureAdapter.getFuture { completer: CallbackToFutureAdapter.Completer>> -> context.sendOrderedBroadcast( intent, null, object : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { val extras: Bundle? = getResultExtras(false) val listOfFlags: java.util.ArrayList>? = extras?.getParcelableArrayList( EXTRA_FLAGS, ParcelableFlag::class.java ) if (listOfFlags != null) { completer.set(listOfFlags) } else { completer.setException(NoFlagResultsException()) } } }, null, Activity.RESULT_OK, "extra data", null ) "QueryingFlags" } } /** * Returns the stored value or null if not set. * This API is used by TheFlippinApp. */ fun isEnabled(name: String): Boolean? = readFlagValue(name, BooleanFlagSerializer) /** * Sets the value of a boolean flag. * This API is used by TheFlippinApp. */ fun setFlagValue(name: String, enabled: Boolean) { val intent = createIntent(name) intent.putExtra(EXTRA_VALUE, enabled) context.sendBroadcast(intent) } fun eraseFlag(name: String) { val intent = createIntent(name) context.sendBroadcast(intent) } /** Returns the stored value or null if not set. */ fun readFlagValue(name: String, serializer: FlagSerializer): T? { val data = settings.getString(nameToSettingsKey(name)) return serializer.fromSettingsData(data) } override fun addListener(flag: Flag<*>, listener: FlagListenable.Listener) { synchronized(listeners) { val registerNeeded = listeners.isEmpty() listeners.add(PerFlagListener(flag.name, listener)) if (registerNeeded) { settings.registerContentObserver(SETTINGS_PREFIX, true, settingsObserver) } } } override fun removeListener(listener: FlagListenable.Listener) { synchronized(listeners) { if (listeners.isEmpty()) { return } listeners.removeIf { it.listener == listener } if (listeners.isEmpty()) { settings.unregisterContentObserver(settingsObserver) } } } private fun createIntent(name: String): Intent { val intent = Intent(ACTION_SET_FLAG) intent.setPackage(RECEIVING_PACKAGE) intent.putExtra(EXTRA_NAME, name) return intent } fun nameToSettingsKey(name: String): String { return "$SETTINGS_PREFIX/$name" } inner class SettingsObserver : ContentObserver(handler) { override fun onChange(selfChange: Boolean, uri: Uri?) { if (uri == null) { return } val parts = uri.pathSegments val name = parts[parts.size - 1] clearCacheAction?.accept(name) dispatchListenersAndMaybeRestart(name, onSettingsChangedAction) } } fun dispatchListenersAndMaybeRestart(name: String, restartAction: Consumer?) { val filteredListeners: List = synchronized(listeners) { listeners.mapNotNull { if (it.name == name) 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 = filteredListeners.map { listener -> var didRequestNoRestart = false val event = object : FlagListenable.FlagEvent { override val flagName = name 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) } private data class PerFlagListener(val name: String, val listener: FlagListenable.Listener) } class NoFlagResultsException : Exception( "SystemUI failed to communicate its flags back successfully" )