1 /*
2  * Copyright 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.photopicker.core.configuration
18 
19 import android.provider.DeviceConfig
20 import java.util.concurrent.Executor
21 
22 /**
23  * This is an in-memory implementation of [DeviceConfigProxy] which can be used for classes under
24  * test that depend on [DeviceConfig] values. All flags are kept in memory, and the actual device
25  * state is not leaked into the test.
26  *
27  * @param initialFlagStore An optional initial flag store value to initialize the proxy with.
28  */
29 class TestDeviceConfigProxyImpl(
30     initialFlagStore: MutableMap<String, MutableMap<String, String>>? = null
31 ) : DeviceConfigProxy {
32 
33     private val flagStore: MutableMap<String, MutableMap<String, String>> =
34         initialFlagStore ?: mutableMapOf()
35 
36     private val observers:
37         MutableMap<String, MutableList<DeviceConfig.OnPropertiesChangedListener>> =
38         mutableMapOf()
39 
addOnPropertiesChangedListenernull40     override fun addOnPropertiesChangedListener(
41         namespace: String,
42         executor: Executor, // Unused in test implementation but required in the API
43         listener: DeviceConfig.OnPropertiesChangedListener,
44     ) {
45         if (!observers.contains(namespace)) {
46             observers.put(namespace, mutableListOf())
47         }
48         observers.get(namespace)?.add(listener)
49     }
50 
removeOnPropertiesChangedListenernull51     override fun removeOnPropertiesChangedListener(
52         listener: DeviceConfig.OnPropertiesChangedListener
53     ) {
54 
55         // The listener could be listening to more than one namespace, so iterate all namespaces.
56         for (list in observers.values) {
57 
58             // Iterate all the listeners in each namespace
59             for (callback in list) {
60                 if (callback == listener) {
61                     list.remove(callback)
62                 }
63             }
64         }
65     }
66 
getFlagnull67     override fun <T> getFlag(namespace: String, key: String, defaultValue: T): T {
68         val rawValue: String? = flagStore.get(namespace)?.get(key)
69 
70         // The casts below are definitely safe casts, Use of the "as?" ensures the type
71         // and in the case it cannot be cast to the type, instead default back to the provided
72         // default value which is known to match the correct type.
73         // As a result, we silence the unchecked cast compiler warnings in the block below.
74         return when {
75             defaultValue is Boolean -> {
76                 @Suppress("UNCHECKED_CAST") return (rawValue?.toBoolean() as? T) ?: defaultValue
77             }
78             defaultValue is String -> {
79                 @Suppress("UNCHECKED_CAST") return (rawValue as? T) ?: defaultValue
80             }
81             (defaultValue is Array<*> && defaultValue.isArrayOf<String>()) ->
82                 @Suppress("UNCHECKED_CAST")
83                 return rawValue?.split(",")?.toTypedArray<String>() as? T ?: defaultValue
84             else -> defaultValue
85         }
86     }
87 
88     /**
89      * Returns this [DeviceConfigProxy] implementation to an empty state. Drops all known
90      * namespaces, flags values. Drops all known listeners.
91      *
92      * @return [this] so the method can be chained
93      */
resetnull94     fun reset(): TestDeviceConfigProxyImpl {
95         flagStore.clear()
96         observers.clear()
97         return this
98     }
99 
100     /**
101      * Set the flag value.
102      *
103      * @param namespace the flag's namespace
104      * @param key the name of the flag to set
105      * @param value the value of this flag
106      * @return [this] so that this method can be chained.
107      */
setFlagnull108     fun setFlag(namespace: String, key: String, value: String): TestDeviceConfigProxyImpl {
109         ensureNamespace(namespace)
110         flagStore.get(namespace)?.put(key, value)
111         notifyKeyChanged(namespace, key, value)
112         return this
113     }
114 
115     /**
116      * Set the flag value.
117      *
118      * @param namespace the flag's namespace
119      * @param key the name of the flag to set
120      * @param value the value of this flag
121      * @return [this] so that this method can be chained.
122      */
setFlagnull123     fun setFlag(namespace: String, key: String, value: Boolean): TestDeviceConfigProxyImpl {
124         ensureNamespace(namespace)
125         flagStore.get(namespace)?.put(key, "$value")
126         notifyKeyChanged(namespace, key, "$value")
127         return this
128     }
129 
130     /** Runs callbacks for any listeners listening to changes to the namespace. */
notifyKeyChangednull131     private fun notifyKeyChanged(namespace: String, key: String, value: String) {
132 
133         val observersToNotify = observers.get(namespace) ?: emptyList()
134 
135         val properties = DeviceConfig.Properties.Builder(namespace).setString(key, value).build()
136 
137         for (listener in observersToNotify) {
138             listener.onPropertiesChanged(properties)
139         }
140     }
141 
142     /** Ensures that the given namespace exists in the current Flag store. */
ensureNamespacenull143     private fun ensureNamespace(namespace: String) {
144         if (!flagStore.contains(namespace)) {
145             flagStore.put(namespace, mutableMapOf())
146         }
147     }
148 }
149