• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 }