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