blob: e61cb5c9a53ebc5f673a2bff5f5a2dff37d40319 [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 android.provider.Settings
import androidx.concurrent.futures.CallbackToFutureAdapter
import com.google.common.util.concurrent.ListenableFuture
import org.json.JSONException
import org.json.JSONObject
class FlagManager constructor(
private val context: Context,
private val handler: Handler
) : FlagReader {
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"
private const val SETTINGS_PREFIX = "systemui/flags"
}
private val listeners: MutableSet<FlagReader.Listener> = 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<Any?> ->
context.sendOrderedBroadcast(intent, null,
object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val extras: Bundle? = getResultExtras(false)
val listOfFlags: java.util.ArrayList<Flag<*>>? =
extras?.getParcelableArrayList(FIELD_FLAGS)
if (listOfFlags != null) {
completer.set(listOfFlags)
} else {
completer.setException(NoFlagResultsException())
}
}
}, null, Activity.RESULT_OK, "extra data", null)
"QueryingFlags"
} as ListenableFuture<Collection<Flag<*>>>
}
fun setFlagValue(id: Int, enabled: Boolean) {
val intent = createIntent(id)
intent.putExtra(FIELD_VALUE, enabled)
context.sendBroadcast(intent)
}
fun eraseFlag(id: Int) {
val intent = createIntent(id)
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()
}
}
override fun addListener(listener: FlagReader.Listener) {
synchronized(listeners) {
val registerNeeded = listeners.isEmpty()
listeners.add(listener)
if (registerNeeded) {
context.contentResolver.registerContentObserver(
Settings.Secure.getUriFor(SETTINGS_PREFIX), true, settingsObserver)
}
}
}
override fun removeListener(listener: FlagReader.Listener) {
synchronized(listeners) {
val isRegistered = !listeners.isEmpty()
listeners.remove(listener)
if (isRegistered && listeners.isEmpty()) {
context.contentResolver.unregisterContentObserver(settingsObserver)
}
}
}
private fun createIntent(id: Int): Intent {
val intent = Intent(ACTION_SET_FLAG)
intent.setPackage(RECEIVING_PACKAGE)
intent.putExtra(FIELD_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
}
}
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]
try {
val id = idStr.toInt()
listeners.forEach { l -> l.onFlagChanged(id) }
} catch (e: NumberFormatException) {
// no-op
}
}
}
}
class InvalidFlagStorageException : Exception("Data found but is invalid")
class NoFlagResultsException : Exception(
"SystemUI failed to communicate its flags back successfully")