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 private val stacktraceExceptionMessage = "Caller of coroutine" 65 66 @MainThread 67 override fun setValue(newValue: T?) { 68 ensureMainThread() 69 70 if (!isInitialized) { 71 isInitialized = true 72 // If we have received an invalid value, and this is the first time we are set, 73 // notify observers. 74 if (newValue == null) { 75 isStale = false 76 super.setValue(newValue) 77 return 78 } 79 } 80 81 val wasStale = isStale 82 // If this liveData is not active, and is not a static value, then it is stale 83 val isActiveOrStaticVal = isStaticVal || hasActiveObservers() 84 // If all of this liveData's sources are non-stale, and this liveData is active or is a 85 // static val, then it is non stale 86 isStale = !(sources.all { !it.isStale } && isActiveOrStaticVal) 87 88 if (valueNotEqual(super.getValue(), newValue) || (wasStale && !isStale)) { 89 super.setValue(newValue) 90 } 91 } 92 93 /** 94 * Update the value of this LiveData. 95 * 96 * This usually results in an IPC when active and no action otherwise. 97 */ 98 @MainThread 99 fun update() { 100 if (DEBUG_UPDATES) { 101 Log.i(LOG_TAG, "update ${javaClass.simpleName} ${shortStackTrace()}") 102 } 103 104 if (this is SmartAsyncMediatorLiveData<T>) { 105 isStale = true 106 } 107 onUpdate() 108 } 109 110 @MainThread 111 protected abstract fun onUpdate() 112 113 override var timeWentInactive: Long? = System.nanoTime() 114 115 /** 116 * Some LiveDatas have types, like Drawables which do not have a non-default equals method. 117 * Those classes can override this method to change when the value is set upon calling setValue. 118 * 119 * @param valOne The first T to be compared 120 * @param valTwo The second T to be compared 121 * 122 * @return True if the two values are different, false otherwise 123 */ 124 protected open fun valueNotEqual(valOne: T?, valTwo: T?): Boolean { 125 return valOne != valTwo 126 } 127 128 override fun <S : Any?> addSource(source: LiveData<S>, onChanged: Observer<in S>) { 129 addSourceWithError(source, onChanged) 130 } 131 132 private fun <S : Any?> addSourceWithError( 133 source: LiveData<S>, 134 onChanged: Observer<in S>, 135 e: IllegalStateException? = null 136 ) { 137 // Get the stacktrace of the call to addSource, so it isn't lost in any errors 138 val exception = e ?: IllegalStateException(stacktraceExceptionMessage) 139 140 GlobalScope.launch(Main.immediate) { 141 if (source is SmartUpdateMediatorLiveData) { 142 if (source in sources) { 143 return@launch 144 } 145 sources.add(source) 146 } 147 try { 148 super.addSource(source, onChanged) 149 } catch (other: IllegalStateException) { 150 throw other.apply { initCause(exception) } 151 } 152 } 153 } 154 155 override fun <S : Any?> removeSource(toRemote: LiveData<S>) { 156 GlobalScope.launch(Main.immediate) { 157 if (toRemote is SmartUpdateMediatorLiveData) { 158 sources.remove(toRemote) 159 } 160 super.removeSource(toRemote) 161 } 162 } 163 164 /** 165 * Gets the difference between a list and a map of livedatas, and then will add as a source all 166 * livedatas which are in the list, but not the map, and will remove all livedatas which are in 167 * the map, but not the list 168 * 169 * @param desired The list of liveDatas we want in our map, represented by a key 170 * @param have The map of livedatas we currently have as sources 171 * @param getLiveDataFun A function to turn a key into a liveData 172 * @param onUpdateFun An optional function which will update differently based on different 173 * LiveDatas. If blank, will simply call update. 174 * 175 * @return a pair of (all keys added, all keys removed) 176 */ 177 fun <K, V : LiveData<*>> setSourcesToDifference( 178 desired: Collection<K>, 179 have: MutableMap<K, V>, 180 getLiveDataFun: (K) -> V, 181 onUpdateFun: ((K) -> Unit)? = null 182 ) : Pair<Set<K>, Set<K>>{ 183 // Ensure the map is correct when method returns 184 val (toAdd, toRemove) = KotlinUtils.getMapAndListDifferences(desired, have) 185 for (key in toAdd) { 186 have[key] = getLiveDataFun(key) 187 } 188 189 val removed = toRemove.map { have.remove(it) }.toMutableList() 190 191 val stackTraceException = java.lang.IllegalStateException(stacktraceExceptionMessage) 192 193 GlobalScope.launch(Main.immediate) { 194 // If any state got out of sorts before this coroutine ran, correct it 195 for (key in toRemove) { 196 removed.add(have.remove(key) ?: continue) 197 } 198 199 for (liveData in removed) { 200 removeSource(liveData ?: continue) 201 } 202 203 for (key in toAdd) { 204 val liveData = getLiveDataFun(key) 205 // Should be a no op, but there is a slight possibility it isn't 206 have[key] = liveData 207 val observer = Observer<Any> { 208 if (onUpdateFun != null) { 209 onUpdateFun(key) 210 } else { 211 update() 212 } 213 } 214 addSourceWithError(liveData, observer, stackTraceException) 215 } 216 } 217 return toAdd to toRemove 218 } 219 220 override fun onActive() { 221 timeWentInactive = null 222 // If this is not an async livedata, and we have sources, and all sources are non-stale, 223 // force update our value 224 if (sources.isNotEmpty() && sources.all { !it.isStale } && 225 this !is SmartAsyncMediatorLiveData<T>) { 226 update() 227 } 228 super.onActive() 229 } 230 231 override fun onInactive() { 232 timeWentInactive = System.nanoTime() 233 if (!isStaticVal) { 234 isStale = true 235 } 236 super.onInactive() 237 } 238 239 /** 240 * Get the [initialized][isInitialized] value, suspending until one is available 241 * 242 * @param staleOk whether [isStale] value is ok to return 243 * @param forceUpdate whether to call [update] (usually triggers an IPC) 244 */ 245 suspend fun getInitializedValue(staleOk: Boolean = false, forceUpdate: Boolean = false): T { 246 return getInitializedValue( 247 observe = { observer -> 248 observeForever(observer) 249 if (forceUpdate || (!staleOk && isStale)) { 250 update() 251 } 252 }, 253 isInitialized = { isInitialized && (staleOk || !isStale) }) 254 } 255 }