[go: nahoru, domu]

blob: edf9e768380cb8876e44238e266fb2384306182d [file] [log] [blame]
/*
* Copyright 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 androidx.ui.res
import android.os.Handler
import android.os.Looper
import android.util.LruCache
import androidx.annotation.GuardedBy
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import java.util.concurrent.Executor
import java.util.concurrent.Executors
private val cacheLock = Object()
private const val CACHE_SIZE = 500
/**
* Exposed only for testing purposes. Do not touch directly.
*/
@GuardedBy("cacheLock")
internal val requestCache = mutableMapOf<String, MutableList<DeferredResource<*>>>()
@GuardedBy("cacheLock")
internal val resourceCache = LruCache<String, Any>(CACHE_SIZE)
// TODO(nona): Reimplement coroutine once IR compiler support suspend function.
private val executor = Executors.newFixedThreadPool(1) { Thread(it, "ResourceThread") }
private val handler by lazy { Handler(Looper.getMainLooper()) }
private val postAtFrontOfQueue: (() -> Unit) -> Unit = { handler.postAtFrontOfQueue(it) }
internal enum class LoadingState { PENDING, LOADED, FAILED }
/**
* A class used for the result of the asynchronous resource loading.
*/
@Stable
class DeferredResource<T> internal constructor(
state: LoadingState = LoadingState.PENDING,
private val pendingResource: T? = null,
private val failedResource: T? = null
) {
internal var state by mutableStateOf(state)
private var loadedResource: T? by mutableStateOf<T?>(null)
private var failedReason: Throwable? by mutableStateOf<Throwable?>(null)
internal fun loadCompleted(loadedResource: T) {
state = LoadingState.LOADED
this.loadedResource = loadedResource
}
internal fun failed(t: Throwable) {
state = LoadingState.FAILED
failedReason = t
}
/**
* Returns the resource.
*
* The resource can be [PendingResource], [LoadedResource], or [FailedResource].
*/
val resource: Resource<T>
get() = when (state) {
LoadingState.FAILED -> FailedResource(failedResource, failedReason)
LoadingState.PENDING -> PendingResource(pendingResource)
LoadingState.LOADED -> LoadedResource(loadedResource!!)
}
}
/**
* The base resource class for background resource loading.
*
* @param resource the resource
*/
sealed class Resource<T>(val resource: T?)
/**
* A class represents the loaded resource.
*/
class LoadedResource<T>(resource: T) : Resource<T>(resource)
/**
* A class represents the alternative resource due to background loading.
*/
class PendingResource<T>(resource: T?) : Resource<T>(resource)
/**
* A class represents the alternative resource due to failed to load the requested resource.
* @param throwable the reason of the failure.
*/
class FailedResource<T>(resource: T?, val throwable: Throwable?) : Resource<T>(resource)
/**
* A common resource loading method.
*/
// TODO(nona): Accept CoroutineScope for customizing fetching coroutine.
@Composable
internal fun <T> loadResource(
key: String,
pendingResource: T? = null,
failedResource: T? = null,
loader: () -> T
): DeferredResource<T> {
return loadResourceInternal(
key,
pendingResource,
failedResource,
executor,
postAtFrontOfQueue,
cacheLock,
requestCache,
resourceCache,
loader)
}
/**
* This function is exposed only for testing purpose. Do not use this directly.
*/
@Suppress("UNCHECKED_CAST")
@Composable
internal fun <T> loadResourceInternal(
key: String,
pendingResource: T? = null,
failedResource: T? = null,
executor: Executor,
uiThreadHandler: (() -> Unit) -> Unit,
cacheLock: Any,
requestCache: MutableMap<String, MutableList<DeferredResource<*>>>,
resourceCache: LruCache<String, Any>,
loader: () -> T
): DeferredResource<T> {
val deferred = remember(key, pendingResource, failedResource) {
DeferredResource(
state = LoadingState.PENDING,
pendingResource = pendingResource,
failedResource = failedResource
)
}
// First, if the deferred is not pending, the loading is completed or failed. Do nothing and
// return the memorized result.
if (deferred.state != LoadingState.PENDING) {
return deferred
}
synchronized(cacheLock) {
// Check if we already know the loadedresource, return with marking load completed.
resourceCache.get(key)?.let { return deferred.apply { loadCompleted(it as T) } }
requestCache.getOrPut(key, { mutableListOf() }).let {
it.add(deferred)
if (it.size == 1) {
// This is the first time to request the resource. schedule the background loading.
executor.execute {
try {
val loaded = loader()
uiThreadHandler {
synchronized(cacheLock) {
requestCache.remove(key)?.forEach {
(it as DeferredResource<T>).loadCompleted(loaded)
}
}
}
} catch (t: Throwable) {
uiThreadHandler {
synchronized(cacheLock) {
requestCache.remove(key)?.forEach {
(it as DeferredResource<T>).failed(t)
}
}
}
}
}
}
}
}
return deferred
}