blob: 43ec4a9f34be258a47b0d1bda30bc27c27b920fa [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.Lifecycle
import androidx.lifecycle.Lifecycle.State
import androidx.lifecycle.Lifecycle.State.STARTED
import androidx.lifecycle.LifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LiveData
import androidx.lifecycle.MediatorLiveData
import androidx.lifecycle.Observer
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).
*/
abstract class SmartUpdateMediatorLiveData<T> : MediatorLiveData<T>(),
DataRepository.InactiveTimekeeper {
companion object {
const val DEBUG_UPDATES = false
val LOG_TAG = SmartUpdateMediatorLiveData::class.java.simpleName
}
/**
* Boolean, whether or not the value of this uiDataLiveData has been explicitly set yet.
* Differentiates between "null value because liveData is new" and "null value because
* liveData is invalid"
*/
var isInitialized = false
private set
/**
* 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 staleObservers = mutableListOf<Pair<LifecycleOwner, Observer<in T>>>()
private val sources = mutableListOf<SmartUpdateMediatorLiveData<*>>()
private val children =
mutableListOf<Triple<SmartUpdateMediatorLiveData<*>, Observer<in T>, Boolean>>()
@MainThread
override fun setValue(newValue: T?) {
ensureMainThread()
if (!isInitialized) {
isInitialized = true
isStale = false
// If we have received an invalid value, and this is the first time we are set,
// notify observers.
if (newValue == null) {
super.setValue(newValue)
return
}
}
if (valueNotEqual(super.getValue(), newValue)) {
isStale = false
super.setValue(newValue)
} else if (isStale) {
isStale = false
// We are no longer stale- notify active stale observers we are up-to-date
val liveObservers = staleObservers.filter { it.first.lifecycle.currentState >= STARTED }
for ((_, observer) in liveObservers) {
observer.onChanged(newValue)
}
for ((liveData, observer, shouldUpdate) in children.toList()) {
if (liveData.hasActiveObservers() && shouldUpdate) {
observer.onChanged(newValue)
}
}
}
}
/**
* Update the value of this LiveData.
*
* This usually results in an IPC when active and no action otherwise.
*/
@MainThread
fun updateIfActive() {
if (DEBUG_UPDATES) {
Log.i(LOG_TAG, "updateIfActive ${javaClass.simpleName} ${shortStackTrace()}")
}
onUpdate()
}
@MainThread
protected abstract fun onUpdate()
override var timeWentInactive: Long? = null
/**
* 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
}
fun observeStale(owner: LifecycleOwner, observer: Observer<in T>) {
val oldStaleObserver = hasStaleObserver()
staleObservers.add(owner to observer)
notifySourcesOnStaleUpdates(oldStaleObserver, true)
if (owner == ForeverActiveLifecycle) {
observeForever(observer)
} else {
observe(owner, observer)
}
}
@MainThread
override fun <S : Any?> addSource(source: LiveData<S>, onChanged: Observer<in S>) {
// ensureMainThread() //TODO (b/148458939): violated in PackagePermissionsLiveData
if (source is SmartUpdateMediatorLiveData) {
source.addChild(this, onChanged,
staleObservers.isNotEmpty() || children.any { it.third })
sources.add(source)
}
super.addSource(source, onChanged)
}
@MainThread
override fun <S : Any?> removeSource(toRemote: LiveData<S>) {
if (toRemote is SmartUpdateMediatorLiveData) {
toRemote.removeChild(this)
sources.remove(toRemote)
}
super.removeSource(toRemote)
}
fun <S : Any?> postAddSource(source: LiveData<S>, onChanged: Observer<in S>) {
GlobalScope.launch(Main) { addSource(source, onChanged) }
}
fun <S : Any?> postRemoveSource(toRemote: LiveData<S>) {
GlobalScope.launch(Main) { removeSource(toRemote) }
}
private fun <S : Any?> removeChild(liveData: LiveData<S>) {
children.removeIf { it.first == liveData }
}
private fun <S : Any?> addChild(
liveData: SmartUpdateMediatorLiveData<S>,
onChanged: Observer<in T>,
sendStaleUpdates: Boolean
) {
children.add(Triple(liveData, onChanged, sendStaleUpdates))
}
private fun <S : Any?> updateStaleChildNotify(
liveData: SmartUpdateMediatorLiveData<S>,
sendStaleUpdates: Boolean
) {
for ((idx, childTriple) in children.withIndex()) {
if (childTriple.first == liveData) {
children[idx] = Triple(liveData, childTriple.second, sendStaleUpdates)
}
}
}
override fun removeObserver(observer: Observer<in T>) {
val oldStaleObserver = hasStaleObserver()
staleObservers.removeIf { it.second == observer }
notifySourcesOnStaleUpdates(oldStaleObserver, hasStaleObserver())
super.removeObserver(observer)
}
override fun removeObservers(owner: LifecycleOwner) {
val oldStaleObserver = hasStaleObserver()
staleObservers.removeIf { it.first == owner }
notifySourcesOnStaleUpdates(oldStaleObserver, hasStaleObserver())
super.removeObservers(owner)
}
private fun notifySourcesOnStaleUpdates(oldHasStale: Boolean, newHasStale: Boolean) {
if (oldHasStale == newHasStale) {
return
}
for (liveData in sources) {
liveData.updateStaleChildNotify(this, hasStaleObserver())
}
// if all sources are not stale, and we just requested stale updates, and we are stale,
// update our value
if (sources.all { !it.isStale } && newHasStale && isStale) {
updateIfActive()
}
}
private fun hasStaleObserver(): Boolean {
return staleObservers.isNotEmpty() || children.any { it.third }
}
override fun onActive() {
timeWentInactive = null
super.onActive()
}
override fun onInactive() {
timeWentInactive = System.nanoTime()
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 [updateIfActive] (usually triggers an IPC)
*/
suspend fun getInitializedValue(staleOk: Boolean = false, forceUpdate: Boolean = false): T {
return getInitializedValue(
observe = { observer ->
observeStale(ForeverActiveLifecycle, observer)
if (forceUpdate) {
updateIfActive()
}
},
isInitialized = { isInitialized && (staleOk || !isStale) })
}
/**
* A [Lifecycle]/[LifecycleOwner] that is permanently [State.STARTED]
*
* Passing this to [LiveData.observe] is essentially equivalent to using
* [LiveData.observeForever], so you have to make sure you handle your own cleanup whenever
* using this.
*/
private object ForeverActiveLifecycle : Lifecycle(), LifecycleOwner {
override fun getLifecycle(): Lifecycle = this
override fun addObserver(observer: LifecycleObserver) {}
override fun removeObserver(observer: LifecycleObserver) {}
override fun getCurrentState(): State = State.STARTED
}
}