/*
* Copyright (C) 2015 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.messaging.datamodel.media;
import android.os.AsyncTask;
import com.android.messaging.Factory;
import com.android.messaging.util.Assert;
import com.android.messaging.util.Assert.RunsOnAnyThread;
import com.android.messaging.util.LogUtil;
import com.google.common.annotations.VisibleForTesting;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
/**
*
Loads and maintains a set of in-memory LRU caches for different types of media resources.
* Right now we don't utilize any disk cache as all media urls are expected to be resolved to
* local content.
*
* The MediaResourceManager takes media loading requests through one of two ways:
*
*
* - {@link #requestMediaResourceAsync(MediaRequest)} that takes a MediaRequest, which may be a
* regular request if the caller doesn't want to listen for events (fire-and-forget),
* or an async request wrapper if event callback is needed.
* - {@link #requestMediaResourceSync(MediaRequest)} which takes a MediaRequest and synchronously
* returns the loaded result, or null if failed.
*
*
* For each media loading task, MediaResourceManager starts an AsyncTask that runs on a
* dedicated thread, which calls MediaRequest.loadMediaBlocking() to perform the actual media
* loading work. As the media resources are loaded, MediaResourceManager notifies the callers
* (which must implement the MediaResourceLoadListener interface) via onMediaResourceLoaded()
* callback. Meanwhile, MediaResourceManager also pushes the loaded resource onto its dedicated
* cache.
*
* The media resource caches ({@link MediaCache}) are maintained as a set of LRU caches. They are
* created on demand by the incoming MediaRequest's getCacheId() method. The implementations of
* MediaRequest (such as {@link ImageRequest}) get to determine the desired cache id. For Bugle,
* the list of available caches are in {@link BugleMediaCacheManager}
*
* Optionally, media loading can support on-demand media encoding and decoding.
* All {@link MediaRequest}'s can opt to chain additional {@link MediaRequest}'s to be executed
* after the completion of the main media loading task, by adding new tasks to the chained
* task list in {@link MediaRequest#loadMediaBlocking(List)}. One possible type of chained task is
* media encoding task. Loaded media will be encoded on a dedicated single threaded executor
* *after* the UI is notified of the loaded media. In this case, the encoded media resource will
* be eventually pushed to the cache, which will later be decoded before posting to the UI thread
* on cache hit.
*
* To add support for a new type of media resource,
*
*
* - Create a new subclass of {@link RefCountedMediaResource} for the new resource type (example:
* {@link ImageResource} class).
*
* - Implement the {@link MediaRequest} interface (example: {@link ImageRequest}). Perform the
* media loading work in loadMediaBlocking() and return a cache id in getCacheId().
*
* - For the UI component that requests the media resource, let it implement
* {@link MediaResourceLoadListener} interface to listen for resource load callback. Let the
* UI component call MediaResourceManager.requestMediaResourceAsync() to request a media source.
* (example: {@link com.android.messaging.ui.ContactIconView}
*
*/
public class MediaResourceManager {
private static final String TAG = LogUtil.BUGLE_TAG;
public static MediaResourceManager get() {
return Factory.get().getMediaResourceManager();
}
/**
* Listener for asynchronous callback from media loading events.
*/
public interface MediaResourceLoadListener {
void onMediaResourceLoaded(MediaRequest request, T resource, boolean cached);
void onMediaResourceLoadError(MediaRequest request, Exception exception);
}
// We use a fixed thread pool for handling media loading tasks. Using a cached thread pool
// allows for unlimited thread creation which can lead to OOMs so we limit the threads here.
private static final Executor MEDIA_LOADING_EXECUTOR = Executors.newFixedThreadPool(10);
// A dedicated single thread executor for performing background task after loading the resource
// on the media loading executor. This includes work such as encoding loaded media to be cached.
// These tasks are run on a single worker thread with low priority so as not to contend with the
// media loading tasks.
private static final Executor MEDIA_BACKGROUND_EXECUTOR = Executors.newSingleThreadExecutor(
new ThreadFactory() {
@Override
public Thread newThread(final Runnable runnable) {
final Thread encodingThread = new Thread(runnable);
encodingThread.setPriority(Thread.MIN_PRIORITY);
return encodingThread;
}
});
/**
* Requests a media resource asynchronously. Upon completion of the media loading task,
* the listener will be notified of success/failure iff it's still bound. A refcount on the
* resource is held and guaranteed for the caller for the duration of the
* {@link MediaResourceLoadListener#onMediaResourceLoaded(
* MediaRequest, RefCountedMediaResource, boolean)} callback.
* @param mediaRequest the media request. May be either an
* {@link AsyncMediaRequestWrapper} for listening for event callbacks, or a regular media
* request for fire-and-forget type of behavior.
*/
public void requestMediaResourceAsync(
final MediaRequest mediaRequest) {
scheduleAsyncMediaRequest(mediaRequest, MEDIA_LOADING_EXECUTOR);
}
/**
* Requests a media resource synchronously.
* @return the loaded resource with a refcount reserved for the caller. The caller must call
* release() on the resource once it's done using it (like with Cursors).
*/
public T requestMediaResourceSync(
final MediaRequest mediaRequest) {
Assert.isNotMainThread();
// Block and load media.
MediaLoadingResult loadResult = null;
try {
loadResult = processMediaRequestInternal(mediaRequest);
// The loaded resource should have at least one refcount by now reserved for the caller.
Assert.isTrue(loadResult.loadedResource.getRefCount() > 0);
return loadResult.loadedResource;
} catch (final Exception e) {
LogUtil.e(LogUtil.BUGLE_TAG, "Synchronous media loading failed, key=" +
mediaRequest.getKey(), e);
return null;
} finally {
if (loadResult != null) {
// Schedule the background requests chained to the main request.
loadResult.scheduleChainedRequests();
}
}
}
@SuppressWarnings("unchecked")
private MediaLoadingResult processMediaRequestInternal(
final MediaRequest mediaRequest)
throws Exception {
final List> chainedRequests = new ArrayList<>();
T loadedResource = null;
// Try fetching from cache first.
final T cachedResource = loadMediaFromCache(mediaRequest);
if (cachedResource != null) {
if (cachedResource.isEncoded()) {
// The resource is encoded, issue a decoding request.
final MediaRequest decodeRequest = (MediaRequest) cachedResource
.getMediaDecodingRequest(mediaRequest);
Assert.notNull(decodeRequest);
cachedResource.release();
loadedResource = loadMediaFromRequest(decodeRequest, chainedRequests);
} else {
// The resource is ready-to-use.
loadedResource = cachedResource;
}
} else {
// Actually load the media after cache miss.
loadedResource = loadMediaFromRequest(mediaRequest, chainedRequests);
}
return new MediaLoadingResult<>(loadedResource, cachedResource != null /* fromCache */,
chainedRequests);
}
private T loadMediaFromCache(
final MediaRequest mediaRequest) {
if (mediaRequest.getRequestType() != MediaRequest.REQUEST_LOAD_MEDIA) {
// Only look up in the cache if we are loading media.
return null;
}
final MediaCache mediaCache = mediaRequest.getMediaCache();
if (mediaCache != null) {
final T mediaResource = mediaCache.fetchResourceFromCache(mediaRequest.getKey());
if (mediaResource != null) {
return mediaResource;
}
}
return null;
}
private T loadMediaFromRequest(
final MediaRequest mediaRequest, final List> chainedRequests)
throws Exception {
final T resource = mediaRequest.loadMediaBlocking(chainedRequests);
// mediaRequest.loadMediaBlocking() should never return null without
// throwing an exception.
Assert.notNull(resource);
// It's possible for the media to be evicted right after it's added to
// the cache (possibly because it's by itself too big for the cache).
// It's also possible that, after added to the cache, something else comes
// to the cache and evicts this media resource. To prevent this from
// recycling the underlying resource objects, make sure to add ref before
// adding to cache so that the caller is guaranteed a ref on the resource.
resource.addRef();
// Don't cache the media request if it is defined as non-cacheable.
if (resource.isCacheable()) {
addResourceToMemoryCache(mediaRequest, resource);
}
return resource;
}
/**
* Schedule an async media request on the given executor
.
* @param mediaRequest the media request to be processed asynchronously. May be either an
* {@link AsyncMediaRequestWrapper} for listening for event callbacks, or a regular media
* request for fire-and-forget type of behavior.
*/
private void scheduleAsyncMediaRequest(
final MediaRequest mediaRequest, final Executor executor) {
final BindableMediaRequest bindableRequest =
(mediaRequest instanceof BindableMediaRequest>) ?
(BindableMediaRequest) mediaRequest : null;
if (bindableRequest != null && !bindableRequest.isBound()) {
return; // Request is obsolete
}
// We don't use SafeAsyncTask here since it enforces the shared thread pool executor
// whereas we want a dedicated thread pool executor.
AsyncTask> mediaLoadingTask =
new AsyncTask>() {
private Exception mException;
@Override
protected MediaLoadingResult doInBackground(Void... params) {
// Double check the request is still valid by the time we start processing it
if (bindableRequest != null && !bindableRequest.isBound()) {
return null; // Request is obsolete
}
try {
return processMediaRequestInternal(mediaRequest);
} catch (Exception e) {
mException = e;
return null;
}
}
@Override
protected void onPostExecute(final MediaLoadingResult result) {
if (result != null) {
Assert.isNull(mException);
Assert.isTrue(result.loadedResource.getRefCount() > 0);
try {
if (bindableRequest != null) {
bindableRequest.onMediaResourceLoaded(
bindableRequest, result.loadedResource, result.fromCache);
}
} finally {
result.loadedResource.release();
result.scheduleChainedRequests();
}
} else if (mException != null) {
LogUtil.e(LogUtil.BUGLE_TAG, "Asynchronous media loading failed, key=" +
mediaRequest.getKey(), mException);
if (bindableRequest != null) {
bindableRequest.onMediaResourceLoadError(bindableRequest, mException);
}
} else {
Assert.isTrue(bindableRequest == null || !bindableRequest.isBound());
if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
LogUtil.v(TAG, "media request not processed, no longer bound; key=" +
LogUtil.sanitizePII(mediaRequest.getKey()) /* key with phone# */);
}
}
}
};
mediaLoadingTask.executeOnExecutor(executor, (Void) null);
}
@VisibleForTesting
@RunsOnAnyThread
void addResourceToMemoryCache(
final MediaRequest mediaRequest, final T mediaResource) {
Assert.isTrue(mediaResource != null);
final MediaCache mediaCache = mediaRequest.getMediaCache();
if (mediaCache != null) {
mediaCache.addResourceToCache(mediaRequest.getKey(), mediaResource);
if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
LogUtil.v(TAG, "added media resource to " + mediaCache.getName() + ". key=" +
LogUtil.sanitizePII(mediaRequest.getKey()) /* key can contain phone# */);
}
}
}
private class MediaLoadingResult {
public final T loadedResource;
public final boolean fromCache;
private final List> mChainedRequests;
MediaLoadingResult(final T loadedResource, final boolean fromCache,
final List> chainedRequests) {
this.loadedResource = loadedResource;
this.fromCache = fromCache;
mChainedRequests = chainedRequests;
}
/**
* Asynchronously schedule a list of chained requests on the background thread.
*/
public void scheduleChainedRequests() {
for (final MediaRequest mediaRequest : mChainedRequests) {
scheduleAsyncMediaRequest(mediaRequest, MEDIA_BACKGROUND_EXECUTOR);
}
}
}
}