• 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     @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