1 /* <lambda>null2 * Copyright (C) 2019 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.permissioncontroller.permission.data 18 19 import android.util.Log 20 import androidx.annotation.MainThread 21 import androidx.lifecycle.LiveData 22 import androidx.lifecycle.MediatorLiveData 23 import androidx.lifecycle.Observer 24 import com.android.permissioncontroller.permission.utils.KotlinUtils 25 import com.android.permissioncontroller.permission.utils.ensureMainThread 26 import com.android.permissioncontroller.permission.utils.getInitializedValue 27 import com.android.permissioncontroller.permission.utils.shortStackTrace 28 import kotlinx.coroutines.Dispatchers.Main 29 import kotlinx.coroutines.GlobalScope 30 import kotlinx.coroutines.launch 31 32 /** 33 * A MediatorLiveData which tracks how long it has been inactive, compares new values before setting 34 * its value (avoiding unnecessary updates), and can calculate the set difference between a list 35 * and a map (used when determining whether or not to add a LiveData as a source). 36 * 37 * @param isStaticVal Whether or not this LiveData value is expected to change 38 */ 39 abstract class SmartUpdateMediatorLiveData<T>(private val isStaticVal: Boolean = false) : 40 MediatorLiveData<T>(), DataRepository.InactiveTimekeeper { 41 42 companion object { 43 const val DEBUG_UPDATES = false 44 val LOG_TAG = SmartUpdateMediatorLiveData::class.java.simpleName 45 } 46 47 /** 48 * Boolean, whether or not the value of this uiDataLiveData has been explicitly set yet. 49 * Differentiates between "null value because liveData is new" and "null value because 50 * liveData is invalid" 51 */ 52 var isInitialized = false 53 private set 54 55 /** 56 * Boolean, whether or not this liveData has a stale value or not. Every time the liveData goes 57 * inactive, its data becomes stale, until it goes active again, and is explicitly set. 58 */ 59 var isStale = true 60 private set 61 62 private val sources = mutableListOf<SmartUpdateMediatorLiveData<*>>() 63 64 @MainThread 65 override fun setValue(newValue: T?) { 66 ensureMainThread() 67 68 if (!isInitialized) { 69 isInitialized = true 70 // If we have received an invalid value, and this is the first time we are set, 71 // notify observers. 72 if (newValue == null) { 73 isStale = false 74 super.setValue(newValue) 75 return 76 } 77 } 78 79 val wasStale = isStale 80 // If this liveData is not active, and is not a static value, then it is stale 81 val isActiveOrStaticVal = isStaticVal || hasActiveObservers() 82 // If all of this liveData's sources are non-stale, and this liveData is active or is a 83 // static val, then it is non stale 84 isStale = !(sources.all { !it.isStale } && isActiveOrStaticVal) 85 86 if (valueNotEqual(super.getValue(), newValue) || (wasStale && !isStale)) { 87 super.setValue(newValue) 88 } 89 } 90 91 /** 92 * Update the value of this LiveData. 93 * 94 * This usually results in an IPC when active and no action otherwise. 95 */ 96 @MainThread 97 fun update() { 98 if (DEBUG_UPDATES) { 99 Log.i(LOG_TAG, "update ${javaClass.simpleName} ${shortStackTrace()}") 100 } 101 102 if (this is SmartAsyncMediatorLiveData<T>) { 103 isStale = true 104 } 105 onUpdate() 106 } 107 108 @MainThread 109 protected abstract fun onUpdate() 110 111 override var timeWentInactive: Long? = System.nanoTime() 112 113 /** 114 * Some LiveDatas have types, like Drawables which do not have a non-default equals method. 115 * Those classes can override this method to change when the value is set upon calling setValue. 116 * 117 * @param valOne The first T to be compared 118 * @param valTwo The second T to be compared 119 * 120 * @return True if the two values are different, false otherwise 121 */ 122 protected open fun valueNotEqual(valOne: T?, valTwo: T?): Boolean { 123 return valOne != valTwo 124 } 125 126 override fun <S : Any?> addSource(source: LiveData<S>, onChanged: Observer<in S>) { 127 addSourceWithStackTraceAttribution(source, onChanged, 128 IllegalStateException().getStackTrace()) 129 } 130 131 private fun <S : Any?> addSourceWithStackTraceAttribution( 132 source: LiveData<S>, 133 onChanged: Observer<in S>, 134 stackTrace: Array<StackTraceElement> 135 ) { 136 GlobalScope.launch(Main.immediate) { 137 if (source is SmartUpdateMediatorLiveData) { 138 if (source in sources) { 139 return@launch 140 } 141 sources.add(source) 142 } 143 try { 144 super.addSource(source, onChanged) 145 } catch (ex: IllegalStateException) { 146 ex.setStackTrace(stackTrace) 147 throw ex 148 } 149 } 150 } 151 152 override fun <S : Any?> removeSource(toRemote: LiveData<S>) { 153 GlobalScope.launch(Main.immediate) { 154 if (toRemote is SmartUpdateMediatorLiveData) { 155 sources.remove(toRemote) 156 } 157 super.removeSource(toRemote) 158 } 159 } 160 161 /** 162 * Gets the difference between a list and a map of livedatas, and then will add as a source all 163 * livedatas which are in the list, but not the map, and will remove all livedatas which are in 164 * the map, but not the list 165 * 166 * @param desired The list of liveDatas we want in our map, represented by a key 167 * @param have The map of livedatas we currently have as sources 168 * @param getLiveDataFun A function to turn a key into a liveData 169 * @param onUpdateFun An optional function which will update differently based on different 170 * LiveDatas. If blank, will simply call update. 171 * 172 * @return a pair of (all keys added, all keys removed) 173 */ 174 fun <K, V : LiveData<*>> setSourcesToDifference( 175 desired: Collection<K>, 176 have: MutableMap<K, V>, 177 getLiveDataFun: (K) -> V, 178 onUpdateFun: ((K) -> Unit)? = null 179 ): Pair<Set<K>, Set<K>>{ 180 // Ensure the map is correct when method returns 181 val (toAdd, toRemove) = KotlinUtils.getMapAndListDifferences(desired, have) 182 for (key in toAdd) { 183 have[key] = getLiveDataFun(key) 184 } 185 186 val removed = toRemove.map { have.remove(it) }.toMutableList() 187 188 val stackTrace = IllegalStateException().getStackTrace() 189 190 GlobalScope.launch(Main.immediate) { 191 // If any state got out of sorts before this coroutine ran, correct it 192 for (key in toRemove) { 193 removed.add(have.remove(key) ?: continue) 194 } 195 196 for (liveData in removed) { 197 removeSource(liveData ?: continue) 198 } 199 200 for (key in toAdd) { 201 val liveData = getLiveDataFun(key) 202 // Should be a no op, but there is a slight possibility it isn't 203 have[key] = liveData 204 val observer = Observer<Any?> { 205 if (onUpdateFun != null) { 206 onUpdateFun(key) 207 } else { 208 update() 209 } 210 } 211 addSourceWithStackTraceAttribution(liveData, observer, stackTrace) 212 } 213 } 214 return toAdd to toRemove 215 } 216 217 override fun onActive() { 218 timeWentInactive = null 219 // If this is not an async livedata, and we have sources, and all sources are non-stale, 220 // force update our value 221 if (sources.isNotEmpty() && sources.all { !it.isStale } && 222 this !is SmartAsyncMediatorLiveData<T>) { 223 update() 224 } 225 super.onActive() 226 } 227 228 override fun onInactive() { 229 timeWentInactive = System.nanoTime() 230 if (!isStaticVal) { 231 isStale = true 232 } 233 super.onInactive() 234 } 235 236 /** 237 * Get the [initialized][isInitialized] value, suspending until one is available 238 * 239 * @param staleOk whether [isStale] value is ok to return 240 * @param forceUpdate whether to call [update] (usually triggers an IPC) 241 */ 242 suspend fun getInitializedValue(staleOk: Boolean = false, forceUpdate: Boolean = false): T { 243 return getInitializedValue( 244 observe = { observer -> 245 observeForever(observer) 246 if (forceUpdate || (!staleOk && isStale)) { 247 update() 248 } 249 }, 250 isInitialized = { isInitialized && (staleOk || !isStale) }) 251 } 252 } 253