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