1 /*
<lambda>null2  * Copyright (C) 2024 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.quickstep.util
18 
19 import android.app.ActivityThread
20 import android.content.Context
21 import android.content.SharedPreferences
22 import android.content.SharedPreferences.OnSharedPreferenceChangeListener
23 import android.provider.DeviceConfig
24 import android.provider.DeviceConfig.OnPropertiesChangedListener
25 import android.provider.DeviceConfig.Properties
26 import androidx.annotation.WorkerThread
27 import com.android.launcher3.BuildConfig
28 import com.android.launcher3.util.Executors
29 import java.util.concurrent.CopyOnWriteArrayList
30 
31 /** Utility class to manage a set of device configurations */
32 class DeviceConfigHelper<ConfigType>(private val factory: (PropReader) -> ConfigType) {
33 
34     var config: ConfigType
35         private set
36 
37     private val allKeys: Set<String>
38     private val propertiesListener = OnPropertiesChangedListener { onDevicePropsChanges(it) }
39     private val sharedPrefChangeListener = OnSharedPreferenceChangeListener { _, _ ->
40         recreateConfig()
41     }
42 
43     private val changeListeners = CopyOnWriteArrayList<Runnable>()
44 
45     init {
46         // Initialize the default config once.
47         allKeys = HashSet()
48         config =
49             factory(
50                 PropReader(
51                     object : PropProvider {
52                         override fun <T : Any> get(key: String, fallback: T): T {
53                             if (fallback is Int) {
54                                 allKeys.add(key)
55                                 return DeviceConfig.getInt(NAMESPACE_LAUNCHER, key, fallback) as T
56                             } else if (fallback is Boolean) {
57                                 allKeys.add(key)
58                                 return DeviceConfig.getBoolean(NAMESPACE_LAUNCHER, key, fallback)
59                                     as T
60                             } else return fallback
61                         }
62                     }
63                 )
64             )
65 
66         DeviceConfig.addOnPropertiesChangedListener(
67             NAMESPACE_LAUNCHER,
68             Executors.UI_HELPER_EXECUTOR,
69             propertiesListener
70         )
71         if (BuildConfig.IS_DEBUG_DEVICE) {
72             prefs.registerOnSharedPreferenceChangeListener(sharedPrefChangeListener)
73         }
74     }
75 
76     @WorkerThread
77     private fun onDevicePropsChanges(properties: Properties) {
78         if (NAMESPACE_LAUNCHER != properties.namespace) return
79         if (!allKeys.any(properties.keyset::contains)) return
80         recreateConfig()
81     }
82 
83     private fun recreateConfig() {
84         val myProps =
85             DeviceConfig.getProperties(NAMESPACE_LAUNCHER, *allKeys.toTypedArray<String>())
86         config =
87             factory(
88                 PropReader(
89                     object : PropProvider {
90                         override fun <T : Any> get(key: String, fallback: T): T {
91                             if (fallback is Int) return myProps.getInt(key, fallback) as T
92                             else if (fallback is Boolean)
93                                 return myProps.getBoolean(key, fallback) as T
94                             else return fallback
95                         }
96                     }
97                 )
98             )
99         Executors.MAIN_EXECUTOR.execute { changeListeners.forEach(Runnable::run) }
100     }
101 
102     /** Adds a listener for property changes */
103     fun addChangeListener(r: Runnable) = changeListeners.add(r)
104 
105     /** Removes a previously added listener */
106     fun removeChangeListener(r: Runnable) = changeListeners.remove(r)
107 
108     fun close() {
109         DeviceConfig.removeOnPropertiesChangedListener(propertiesListener)
110         if (BuildConfig.IS_DEBUG_DEVICE) {
111             prefs.unregisterOnSharedPreferenceChangeListener(sharedPrefChangeListener)
112         }
113     }
114 
115     internal interface PropProvider {
116         fun <T : Any> get(key: String, fallback: T): T
117     }
118 
119     /** The reader is sent to the config for initialization */
120     class PropReader internal constructor(private val f: PropProvider) {
121 
122         @JvmOverloads
123         fun <T : Any> get(key: String, fallback: T, desc: String? = null): T {
124             val v = f.get(key, fallback)
125             if (BuildConfig.IS_DEBUG_DEVICE && desc != null) {
126                 if (v is Int) {
127                     allProps[key] = DebugInfo(key, desc, true, fallback)
128                     return prefs.getInt(key, v) as T
129                 } else if (v is Boolean) {
130                     allProps[key] = DebugInfo(key, desc, false, fallback)
131                     return prefs.getBoolean(key, v) as T
132                 }
133             }
134             return v
135         }
136     }
137 
138     class DebugInfo<T>(
139         val key: String,
140         val desc: String,
141         val isInt: Boolean,
142         val valueInCode: T,
143     )
144 
145     companion object {
146         const val NAMESPACE_LAUNCHER = "launcher"
147 
148         val allProps = mutableMapOf<String, DebugInfo<*>>()
149 
150         private const val FLAGS_PREF_NAME = "featureFlags"
151 
152         val prefs: SharedPreferences by lazy {
153             ActivityThread.currentApplication()
154                 .createDeviceProtectedStorageContext()
155                 .getSharedPreferences(FLAGS_PREF_NAME, Context.MODE_PRIVATE)
156         }
157     }
158 }
159