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