blob: d172690e2488ab2b3b7052200467b595adae1baf [file] [log] [blame]
/*
* 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 EXTRA_ID = "id"
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<Boolean>? = null
var clearCacheAction: Consumer<Int>? = null
private val listeners: MutableSet<PerFlagListener> = mutableSetOf()
private val settingsObserver: ContentObserver = SettingsObserver()
fun getFlagsFuture(): ListenableFuture<Collection<Flag<*>>> {
val intent = Intent(ACTION_GET_FLAGS)
intent.setPackage(RECEIVING_PACKAGE)
return CallbackToFutureAdapter.getFuture {
completer: CallbackToFutureAdapter.Completer<Collection<Flag<*>>> ->
context.sendOrderedBroadcast(
intent,
null,
object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val extras: Bundle? = getResultExtras(false)
val listOfFlags: java.util.ArrayList<ParcelableFlag<*>>? =
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(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(EXTRA_VALUE, enabled)
context.sendBroadcast(intent)
}
fun eraseFlag(id: Int) {
val intent = createIntent(id)
context.sendBroadcast(intent)
}
/** Returns the stored value or null if not set. */
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) {
synchronized(listeners) {
val registerNeeded = listeners.isEmpty()
listeners.add(PerFlagListener(flag.id, 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(id: Int): Intent {
val intent = Intent(ACTION_SET_FLAG)
intent.setPackage(RECEIVING_PACKAGE)
intent.putExtra(EXTRA_ID, id)
return intent
}
fun idToSettingsKey(id: Int): String {
return "$SETTINGS_PREFIX/$id"
}
inner class SettingsObserver : ContentObserver(handler) {
override fun onChange(selfChange: Boolean, uri: Uri?) {
if (uri == null) {
return
}
val parts = uri.pathSegments
val idStr = parts[parts.size - 1]
val id = try {
idStr.toInt()
} catch (e: NumberFormatException) {
return
}
clearCacheAction?.accept(id)
dispatchListenersAndMaybeRestart(id, onSettingsChangedAction)
}
}
fun dispatchListenersAndMaybeRestart(id: Int, restartAction: Consumer<Boolean>?) {
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)
}
private data class PerFlagListener(val id: Int, val listener: FlagListenable.Listener)
}
class NoFlagResultsException : Exception(
"SystemUI failed to communicate its flags back successfully"
)