1 /*
2  * Copyright (C) 2021 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.systemui.flags
18 
19 import android.app.Activity
20 import android.content.BroadcastReceiver
21 import android.content.Context
22 import android.content.Intent
23 import android.database.ContentObserver
24 import android.net.Uri
25 import android.os.Bundle
26 import android.os.Handler
27 import androidx.concurrent.futures.CallbackToFutureAdapter
28 import com.google.common.util.concurrent.ListenableFuture
29 import java.util.function.Consumer
30 
31 class FlagManager constructor(
32     private val context: Context,
33     private val settings: FlagSettingsHelper,
34     private val handler: Handler
35 ) : FlagListenable {
36     companion object {
37         const val RECEIVING_PACKAGE = "com.android.systemui"
38         const val ACTION_SET_FLAG = "com.android.systemui.action.SET_FLAG"
39         const val ACTION_GET_FLAGS = "com.android.systemui.action.GET_FLAGS"
40         const val FLAGS_PERMISSION = "com.android.systemui.permission.FLAGS"
41         const val ACTION_SYSUI_STARTED = "com.android.systemui.STARTED"
42         const val EXTRA_NAME = "name"
43         const val EXTRA_VALUE = "value"
44         const val EXTRA_FLAGS = "flags"
45         private const val SETTINGS_PREFIX = "systemui/flags"
46     }
47 
48     constructor(context: Context, handler: Handler) : this(
49         context,
50         FlagSettingsHelper(context.contentResolver),
51         handler
52     )
53 
54     /**
55      * An action called on restart which takes as an argument whether the listeners requested
56      * that the restart be suppressed
57      */
58     var onSettingsChangedAction: Consumer<Boolean>? = null
59     var clearCacheAction: Consumer<String>? = null
60     private val listeners: MutableSet<PerFlagListener> = mutableSetOf()
61     private val settingsObserver: ContentObserver = SettingsObserver()
62 
63     fun getFlagsFuture(): ListenableFuture<Collection<Flag<*>>> {
64         val intent = Intent(ACTION_GET_FLAGS)
65         intent.setPackage(RECEIVING_PACKAGE)
66 
67         return CallbackToFutureAdapter.getFuture {
68                 completer: CallbackToFutureAdapter.Completer<Collection<Flag<*>>> ->
69             context.sendOrderedBroadcast(
70                 intent,
71                 null,
72                 object : BroadcastReceiver() {
73                     override fun onReceive(context: Context, intent: Intent) {
74                         val extras: Bundle? = getResultExtras(false)
75                         val listOfFlags: java.util.ArrayList<ParcelableFlag<*>>? =
76                             extras?.getParcelableArrayList(
77                                 EXTRA_FLAGS, ParcelableFlag::class.java
78                             )
79                         if (listOfFlags != null) {
80                             completer.set(listOfFlags)
81                         } else {
82                             completer.setException(NoFlagResultsException())
83                         }
84                     }
85                 },
86                 null,
87                 Activity.RESULT_OK,
88                 "extra data",
89                 null
90             )
91             "QueryingFlags"
92         }
93     }
94 
95     /**
96      * Returns the stored value or null if not set.
97      * This API is used by TheFlippinApp.
98      */
99     fun isEnabled(name: String): Boolean? = readFlagValue(name, BooleanFlagSerializer)
100 
101     /**
102      * Sets the value of a boolean flag.
103      * This API is used by TheFlippinApp.
104      */
105     fun setFlagValue(name: String, enabled: Boolean) {
106         val intent = createIntent(name)
107         intent.putExtra(EXTRA_VALUE, enabled)
108 
109         context.sendBroadcast(intent)
110     }
111 
112     fun eraseFlag(name: String) {
113         val intent = createIntent(name)
114 
115         context.sendBroadcast(intent)
116     }
117 
118     /** Returns the stored value or null if not set.  */
119     fun <T> readFlagValue(name: String, serializer: FlagSerializer<T>): T? {
120         val data = settings.getString(nameToSettingsKey(name))
121         return serializer.fromSettingsData(data)
122     }
123 
124     override fun addListener(flag: Flag<*>, listener: FlagListenable.Listener) {
125         synchronized(listeners) {
126             val registerNeeded = listeners.isEmpty()
127             listeners.add(PerFlagListener(flag.name, listener))
128             if (registerNeeded) {
129                 settings.registerContentObserver(SETTINGS_PREFIX, true, settingsObserver)
130             }
131         }
132     }
133 
134     override fun removeListener(listener: FlagListenable.Listener) {
135         synchronized(listeners) {
136             if (listeners.isEmpty()) {
137                 return
138             }
139             listeners.removeIf { it.listener == listener }
140             if (listeners.isEmpty()) {
141                 settings.unregisterContentObserver(settingsObserver)
142             }
143         }
144     }
145 
146     private fun createIntent(name: String): Intent {
147         val intent = Intent(ACTION_SET_FLAG)
148         intent.setPackage(RECEIVING_PACKAGE)
149         intent.putExtra(EXTRA_NAME, name)
150 
151         return intent
152     }
153 
154     fun nameToSettingsKey(name: String): String {
155         return "$SETTINGS_PREFIX/$name"
156     }
157 
158     inner class SettingsObserver : ContentObserver(handler) {
159         override fun onChange(selfChange: Boolean, uri: Uri?) {
160             if (uri == null) {
161                 return
162             }
163             val parts = uri.pathSegments
164             val name = parts[parts.size - 1]
165             clearCacheAction?.accept(name)
166             dispatchListenersAndMaybeRestart(name, onSettingsChangedAction)
167         }
168     }
169 
170     fun dispatchListenersAndMaybeRestart(name: String, restartAction: Consumer<Boolean>?) {
171         val filteredListeners: List<FlagListenable.Listener> = synchronized(listeners) {
172             listeners.mapNotNull { if (it.name == name) it.listener else null }
173         }
174         // If there are no listeners, there's nothing to dispatch to, and nothing to suppress it.
175         if (filteredListeners.isEmpty()) {
176             restartAction?.accept(false)
177             return
178         }
179         // Dispatch to every listener and save whether each one called requestNoRestart.
180         val suppressRestartList: List<Boolean> = filteredListeners.map { listener ->
181             var didRequestNoRestart = false
182             val event = object : FlagListenable.FlagEvent {
183                 override val flagName = name
184                 override fun requestNoRestart() {
185                     didRequestNoRestart = true
186                 }
187             }
188             listener.onFlagChanged(event)
189             didRequestNoRestart
190         }
191         // Suppress restart only if ALL listeners request it.
192         val suppressRestart = suppressRestartList.all { it }
193         restartAction?.accept(suppressRestart)
194     }
195 
196     private data class PerFlagListener(val name: String, val listener: FlagListenable.Listener)
197 }
198 
199 class NoFlagResultsException : Exception(
200     "SystemUI failed to communicate its flags back successfully"
201 )
202