summaryrefslogtreecommitdiff
path: root/PermissionController/src/com/android/permissioncontroller/permission/data/SmartUpdateMediatorLiveData.kt
blob: 7c3ebe0b381462efc048c31ca291b59659304507 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
/*
 * Copyright (C) 2019 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.permissioncontroller.permission.data

import android.util.Log
import androidx.annotation.MainThread
import androidx.lifecycle.LiveData
import androidx.lifecycle.MediatorLiveData
import androidx.lifecycle.Observer
import com.android.permissioncontroller.permission.utils.KotlinUtils
import com.android.permissioncontroller.permission.utils.ensureMainThread
import com.android.permissioncontroller.permission.utils.getInitializedValue
import com.android.permissioncontroller.permission.utils.shortStackTrace
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch

/**
 * A MediatorLiveData which tracks how long it has been inactive, compares new values before setting
 * its value (avoiding unnecessary updates), and can calculate the set difference between a list
 * and a map (used when determining whether or not to add a LiveData as a source).
 *
 * @param isStaticVal Whether or not this LiveData value is expected to change
 */
abstract class SmartUpdateMediatorLiveData<T>(private val isStaticVal: Boolean = false) :
    MediatorLiveData<T>(), DataRepository.InactiveTimekeeper {

    companion object {
        const val DEBUG_UPDATES = false
        val LOG_TAG = SmartUpdateMediatorLiveData::class.java.simpleName
    }

    /**
     * Boolean, whether or not this liveData has a stale value or not. Every time the liveData goes
     * inactive, its data becomes stale, until it goes active again, and is explicitly set.
     */
    var isStale = true
        private set

    private val sources = mutableListOf<SmartUpdateMediatorLiveData<*>>()

    @MainThread
    override fun setValue(newValue: T?) {
        ensureMainThread()

        if (!isInitialized) {
            // If we have received an invalid value, and this is the first time we are set,
            // notify observers.
            if (newValue == null) {
                isStale = false
                super.setValue(newValue)
                return
            }
        }

        val wasStale = isStale
        // If this liveData is not active, and is not a static value, then it is stale
        val isActiveOrStaticVal = isStaticVal || hasActiveObservers()
        // If all of this liveData's sources are non-stale, and this liveData is active or is a
        // static val, then it is non stale
        isStale = !(sources.all { !it.isStale } && isActiveOrStaticVal)

        if (valueNotEqual(super.getValue(), newValue) || (wasStale && !isStale)) {
            super.setValue(newValue)
        }
    }

    /**
     * Update the value of this LiveData.
     *
     * This usually results in an IPC when active and no action otherwise.
     */
    @MainThread
    fun update() {
        if (DEBUG_UPDATES) {
            Log.i(LOG_TAG, "update ${javaClass.simpleName} ${shortStackTrace()}")
        }

        if (this is SmartAsyncMediatorLiveData<T>) {
            isStale = true
        }
        onUpdate()
    }

    @MainThread
    protected abstract fun onUpdate()

    override var timeWentInactive: Long? = System.nanoTime()

    /**
     * Some LiveDatas have types, like Drawables which do not have a non-default equals method.
     * Those classes can override this method to change when the value is set upon calling setValue.
     *
     * @param valOne The first T to be compared
     * @param valTwo The second T to be compared
     *
     * @return True if the two values are different, false otherwise
     */
    protected open fun valueNotEqual(valOne: T?, valTwo: T?): Boolean {
        return valOne != valTwo
    }

    override fun <S : Any?> addSource(source: LiveData<S>, onChanged: Observer<in S>) {
        addSourceWithStackTraceAttribution(source, onChanged,
            IllegalStateException().getStackTrace())
    }

    private fun <S : Any?> addSourceWithStackTraceAttribution(
        source: LiveData<S>,
        onChanged: Observer<in S>,
        stackTrace: Array<StackTraceElement>
    ) {
        GlobalScope.launch(Main.immediate) {
            if (source is SmartUpdateMediatorLiveData) {
                if (source in sources) {
                    return@launch
                }
                sources.add(source)
            }
            try {
                super.addSource(source, onChanged)
            } catch (ex: IllegalStateException) {
                ex.setStackTrace(stackTrace)
                throw ex
            }
        }
    }

    override fun <S : Any?> removeSource(toRemote: LiveData<S>) {
        GlobalScope.launch(Main.immediate) {
            if (toRemote is SmartUpdateMediatorLiveData) {
                sources.remove(toRemote)
            }
            super.removeSource(toRemote)
        }
    }

    /**
     * Gets the difference between a list and a map of livedatas, and then will add as a source all
     * livedatas which are in the list, but not the map, and will remove all livedatas which are in
     * the map, but not the list
     *
     * @param desired The list of liveDatas we want in our map, represented by a key
     * @param have The map of livedatas we currently have as sources
     * @param getLiveDataFun A function to turn a key into a liveData
     * @param onUpdateFun An optional function which will update differently based on different
     * LiveDatas. If blank, will simply call update.
     *
     * @return a pair of (all keys added, all keys removed)
     */
    fun <K, V : LiveData<*>> setSourcesToDifference(
        desired: Collection<K>,
        have: MutableMap<K, V>,
        getLiveDataFun: (K) -> V,
        onUpdateFun: ((K) -> Unit)? = null
    ): Pair<Set<K>, Set<K>>{
        // Ensure the map is correct when method returns
        val (toAdd, toRemove) = KotlinUtils.getMapAndListDifferences(desired, have)
        for (key in toAdd) {
            have[key] = getLiveDataFun(key)
        }

        val removed = toRemove.map { have.remove(it) }.toMutableList()

        val stackTrace = IllegalStateException().getStackTrace()

        GlobalScope.launch(Main.immediate) {
            // If any state got out of sorts before this coroutine ran, correct it
            for (key in toRemove) {
                removed.add(have.remove(key) ?: continue)
            }

            for (liveData in removed) {
                removeSource(liveData ?: continue)
            }

            for (key in toAdd) {
                val liveData = getLiveDataFun(key)
                // Should be a no op, but there is a slight possibility it isn't
                have[key] = liveData
                val observer = Observer<Any?> {
                    if (onUpdateFun != null) {
                        onUpdateFun(key)
                    } else {
                        update()
                    }
                }
                addSourceWithStackTraceAttribution(liveData, observer, stackTrace)
            }
        }
        return toAdd to toRemove
    }

    override fun onActive() {
        timeWentInactive = null
        // If this is not an async livedata, and we have sources, and all sources are non-stale,
        // force update our value
        if (sources.isNotEmpty() && sources.all { !it.isStale } &&
            this !is SmartAsyncMediatorLiveData<T>) {
            update()
        }
        super.onActive()
    }

    override fun onInactive() {
        timeWentInactive = System.nanoTime()
        if (!isStaticVal) {
            isStale = true
        }
        super.onInactive()
    }

    /**
     * Get the [initialized][isInitialized] value, suspending until one is available
     *
     * @param staleOk whether [isStale] value is ok to return
     * @param forceUpdate whether to call [update] (usually triggers an IPC)
     */
    suspend fun getInitializedValue(staleOk: Boolean = false, forceUpdate: Boolean = false): T {
        return getInitializedValue(
            observe = { observer ->
                observeForever(observer)
                if (forceUpdate || (!staleOk && isStale)) {
                    update()
                }
            },
            isValueInitialized = { isInitialized && (staleOk || !isStale) })
    }
}