blob: 7c3ebe0b381462efc048c31ca291b59659304507 [file] [log] [blame]
/*
* 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) })
}
}