Home | History | Annotate | Download | only in media
      1 /*
      2  * Copyright (C) 2015 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 package com.android.messaging.datamodel.media;
     17 
     18 import android.os.AsyncTask;
     19 
     20 import com.android.messaging.Factory;
     21 import com.android.messaging.util.Assert;
     22 import com.android.messaging.util.Assert.RunsOnAnyThread;
     23 import com.android.messaging.util.LogUtil;
     24 import com.google.common.annotations.VisibleForTesting;
     25 
     26 import java.util.ArrayList;
     27 import java.util.List;
     28 import java.util.concurrent.Executor;
     29 import java.util.concurrent.Executors;
     30 import java.util.concurrent.ThreadFactory;
     31 
     32 /**
     33  * <p>Loads and maintains a set of in-memory LRU caches for different types of media resources.
     34  * Right now we don't utilize any disk cache as all media urls are expected to be resolved to
     35  * local content.<p/>
     36  *
     37  * <p>The MediaResourceManager takes media loading requests through one of two ways:</p>
     38  *
     39  * <ol>
     40  * <li>{@link #requestMediaResourceAsync(MediaRequest)} that takes a MediaRequest, which may be a
     41  *  regular request if the caller doesn't want to listen for events (fire-and-forget),
     42  *  or an async request wrapper if event callback is needed.</li>
     43  * <li>{@link #requestMediaResourceSync(MediaRequest)} which takes a MediaRequest and synchronously
     44  *  returns the loaded result, or null if failed.</li>
     45  * </ol>
     46  *
     47  * <p>For each media loading task, MediaResourceManager starts an AsyncTask that runs on a
     48  * dedicated thread, which calls MediaRequest.loadMediaBlocking() to perform the actual media
     49  * loading work. As the media resources are loaded, MediaResourceManager notifies the callers
     50  * (which must implement the MediaResourceLoadListener interface) via onMediaResourceLoaded()
     51  * callback. Meanwhile, MediaResourceManager also pushes the loaded resource onto its dedicated
     52  * cache.</p>
     53  *
     54  * <p>The media resource caches ({@link MediaCache}) are maintained as a set of LRU caches. They are
     55  * created on demand by the incoming MediaRequest's getCacheId() method. The implementations of
     56  * MediaRequest (such as {@link ImageRequest}) get to determine the desired cache id. For Bugle,
     57  * the list of available caches are in {@link BugleMediaCacheManager}</p>
     58  *
     59  * <p>Optionally, media loading can support on-demand media encoding and decoding.
     60  * All {@link MediaRequest}'s can opt to chain additional {@link MediaRequest}'s to be executed
     61  * after the completion of the main media loading task, by adding new tasks to the chained
     62  * task list in {@link MediaRequest#loadMediaBlocking(List)}. One possible type of chained task is
     63  * media encoding task. Loaded media will be encoded on a dedicated single threaded executor
     64  * *after* the UI is notified of the loaded media. In this case, the encoded media resource will
     65  * be eventually pushed to the cache, which will later be decoded before posting to the UI thread
     66  * on cache hit.</p>
     67  *
     68  * <p><b>To add support for a new type of media resource,</b></p>
     69  *
     70  * <ol>
     71  * <li>Create a new subclass of {@link RefCountedMediaResource} for the new resource type (example:
     72  *    {@link ImageResource} class).</li>
     73  *
     74  * <li>Implement the {@link MediaRequest} interface (example: {@link ImageRequest}). Perform the
     75  *    media loading work in loadMediaBlocking() and return a cache id in getCacheId().</li>
     76  *
     77  * <li>For the UI component that requests the media resource, let it implement
     78  *    {@link MediaResourceLoadListener} interface to listen for resource load callback. Let the
     79  *    UI component call MediaResourceManager.requestMediaResourceAsync() to request a media source.
     80  *    (example: {@link com.android.messaging.ui.ContactIconView}</li>
     81  * </ol>
     82  */
     83 public class MediaResourceManager {
     84     private static final String TAG = LogUtil.BUGLE_TAG;
     85 
     86     public static MediaResourceManager get() {
     87         return Factory.get().getMediaResourceManager();
     88     }
     89 
     90     /**
     91      * Listener for asynchronous callback from media loading events.
     92      */
     93     public interface MediaResourceLoadListener<T extends RefCountedMediaResource> {
     94         void onMediaResourceLoaded(MediaRequest<T> request, T resource, boolean cached);
     95         void onMediaResourceLoadError(MediaRequest<T> request, Exception exception);
     96     }
     97 
     98     // We use a fixed thread pool for handling media loading tasks. Using a cached thread pool
     99     // allows for unlimited thread creation which can lead to OOMs so we limit the threads here.
    100     private static final Executor MEDIA_LOADING_EXECUTOR = Executors.newFixedThreadPool(10);
    101 
    102     // A dedicated single thread executor for performing background task after loading the resource
    103     // on the media loading executor. This includes work such as encoding loaded media to be cached.
    104     // These tasks are run on a single worker thread with low priority so as not to contend with the
    105     // media loading tasks.
    106     private static final Executor MEDIA_BACKGROUND_EXECUTOR = Executors.newSingleThreadExecutor(
    107             new ThreadFactory() {
    108                 @Override
    109                 public Thread newThread(final Runnable runnable) {
    110                     final Thread encodingThread = new Thread(runnable);
    111                     encodingThread.setPriority(Thread.MIN_PRIORITY);
    112                     return encodingThread;
    113                 }
    114             });
    115 
    116     /**
    117      * Requests a media resource asynchronously. Upon completion of the media loading task,
    118      * the listener will be notified of success/failure iff it's still bound. A refcount on the
    119      * resource is held and guaranteed for the caller for the duration of the
    120      * {@link MediaResourceLoadListener#onMediaResourceLoaded(
    121      * MediaRequest, RefCountedMediaResource, boolean)} callback.
    122      * @param mediaRequest the media request. May be either an
    123      * {@link AsyncMediaRequestWrapper} for listening for event callbacks, or a regular media
    124      * request for fire-and-forget type of behavior.
    125      */
    126     public <T extends RefCountedMediaResource> void requestMediaResourceAsync(
    127             final MediaRequest<T> mediaRequest) {
    128         scheduleAsyncMediaRequest(mediaRequest, MEDIA_LOADING_EXECUTOR);
    129     }
    130 
    131     /**
    132      * Requests a media resource synchronously.
    133      * @return the loaded resource with a refcount reserved for the caller. The caller must call
    134      * release() on the resource once it's done using it (like with Cursors).
    135      */
    136     public <T extends RefCountedMediaResource> T requestMediaResourceSync(
    137             final MediaRequest<T> mediaRequest) {
    138         Assert.isNotMainThread();
    139         // Block and load media.
    140         MediaLoadingResult<T> loadResult = null;
    141         try {
    142             loadResult = processMediaRequestInternal(mediaRequest);
    143             // The loaded resource should have at least one refcount by now reserved for the caller.
    144             Assert.isTrue(loadResult.loadedResource.getRefCount() > 0);
    145             return loadResult.loadedResource;
    146         } catch (final Exception e) {
    147             LogUtil.e(LogUtil.BUGLE_TAG, "Synchronous media loading failed, key=" +
    148                     mediaRequest.getKey(), e);
    149             return null;
    150         } finally {
    151             if (loadResult != null) {
    152                 // Schedule the background requests chained to the main request.
    153                 loadResult.scheduleChainedRequests();
    154             }
    155         }
    156     }
    157 
    158     @SuppressWarnings("unchecked")
    159     private <T extends RefCountedMediaResource> MediaLoadingResult<T> processMediaRequestInternal(
    160             final MediaRequest<T> mediaRequest)
    161                     throws Exception {
    162         final List<MediaRequest<T>> chainedRequests = new ArrayList<>();
    163         T loadedResource = null;
    164         // Try fetching from cache first.
    165         final T cachedResource = loadMediaFromCache(mediaRequest);
    166         if (cachedResource != null) {
    167             if (cachedResource.isEncoded()) {
    168                 // The resource is encoded, issue a decoding request.
    169                 final MediaRequest<T> decodeRequest = (MediaRequest<T>) cachedResource
    170                         .getMediaDecodingRequest(mediaRequest);
    171                 Assert.notNull(decodeRequest);
    172                 cachedResource.release();
    173                 loadedResource = loadMediaFromRequest(decodeRequest, chainedRequests);
    174             } else {
    175                 // The resource is ready-to-use.
    176                 loadedResource = cachedResource;
    177             }
    178         } else {
    179             // Actually load the media after cache miss.
    180             loadedResource = loadMediaFromRequest(mediaRequest, chainedRequests);
    181         }
    182         return new MediaLoadingResult<>(loadedResource, cachedResource != null /* fromCache */,
    183                 chainedRequests);
    184     }
    185 
    186     private <T extends RefCountedMediaResource> T loadMediaFromCache(
    187             final MediaRequest<T> mediaRequest) {
    188         if (mediaRequest.getRequestType() != MediaRequest.REQUEST_LOAD_MEDIA) {
    189             // Only look up in the cache if we are loading media.
    190             return null;
    191         }
    192         final MediaCache<T> mediaCache = mediaRequest.getMediaCache();
    193         if (mediaCache != null) {
    194             final T mediaResource = mediaCache.fetchResourceFromCache(mediaRequest.getKey());
    195             if (mediaResource != null) {
    196                 return mediaResource;
    197             }
    198         }
    199         return null;
    200     }
    201 
    202     private <T extends RefCountedMediaResource> T loadMediaFromRequest(
    203             final MediaRequest<T> mediaRequest, final List<MediaRequest<T>> chainedRequests)
    204                     throws Exception {
    205         final T resource = mediaRequest.loadMediaBlocking(chainedRequests);
    206         // mediaRequest.loadMediaBlocking() should never return null without
    207         // throwing an exception.
    208         Assert.notNull(resource);
    209         // It's possible for the media to be evicted right after it's added to
    210         // the cache (possibly because it's by itself too big for the cache).
    211         // It's also possible that, after added to the cache, something else comes
    212         // to the cache and evicts this media resource. To prevent this from
    213         // recycling the underlying resource objects, make sure to add ref before
    214         // adding to cache so that the caller is guaranteed a ref on the resource.
    215         resource.addRef();
    216         // Don't cache the media request if it is defined as non-cacheable.
    217         if (resource.isCacheable()) {
    218             addResourceToMemoryCache(mediaRequest, resource);
    219         }
    220         return resource;
    221     }
    222 
    223     /**
    224      * Schedule an async media request on the given <code>executor</code>.
    225      * @param mediaRequest the media request to be processed asynchronously. May be either an
    226      * {@link AsyncMediaRequestWrapper} for listening for event callbacks, or a regular media
    227      * request for fire-and-forget type of behavior.
    228      */
    229     private <T extends RefCountedMediaResource> void scheduleAsyncMediaRequest(
    230             final MediaRequest<T> mediaRequest, final Executor executor) {
    231         final BindableMediaRequest<T> bindableRequest =
    232                 (mediaRequest instanceof BindableMediaRequest<?>) ?
    233                         (BindableMediaRequest<T>) mediaRequest : null;
    234         if (bindableRequest != null && !bindableRequest.isBound()) {
    235             return; // Request is obsolete
    236         }
    237         // We don't use SafeAsyncTask here since it enforces the shared thread pool executor
    238         // whereas we want a dedicated thread pool executor.
    239         AsyncTask<Void, Void, MediaLoadingResult<T>> mediaLoadingTask =
    240                 new AsyncTask<Void, Void, MediaLoadingResult<T>>() {
    241             private Exception mException;
    242 
    243             @Override
    244             protected MediaLoadingResult<T> doInBackground(Void... params) {
    245                 // Double check the request is still valid by the time we start processing it
    246                 if (bindableRequest != null && !bindableRequest.isBound()) {
    247                     return null; // Request is obsolete
    248                 }
    249                 try {
    250                     return processMediaRequestInternal(mediaRequest);
    251                 } catch (Exception e) {
    252                     mException = e;
    253                     return null;
    254                 }
    255             }
    256 
    257             @Override
    258             protected void onPostExecute(final MediaLoadingResult<T> result) {
    259                 if (result != null) {
    260                     Assert.isNull(mException);
    261                     Assert.isTrue(result.loadedResource.getRefCount() > 0);
    262                     try {
    263                         if (bindableRequest != null) {
    264                             bindableRequest.onMediaResourceLoaded(
    265                                     bindableRequest, result.loadedResource, result.fromCache);
    266                         }
    267                     } finally {
    268                         result.loadedResource.release();
    269                         result.scheduleChainedRequests();
    270                     }
    271                 } else if (mException != null) {
    272                     LogUtil.e(LogUtil.BUGLE_TAG, "Asynchronous media loading failed, key=" +
    273                             mediaRequest.getKey(), mException);
    274                     if (bindableRequest != null) {
    275                         bindableRequest.onMediaResourceLoadError(bindableRequest, mException);
    276                     }
    277                 } else {
    278                     Assert.isTrue(bindableRequest == null || !bindableRequest.isBound());
    279                     if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
    280                         LogUtil.v(TAG, "media request not processed, no longer bound; key=" +
    281                                 LogUtil.sanitizePII(mediaRequest.getKey()) /* key with phone# */);
    282                     }
    283                 }
    284             }
    285         };
    286         mediaLoadingTask.executeOnExecutor(executor, (Void) null);
    287     }
    288 
    289     @VisibleForTesting
    290     @RunsOnAnyThread
    291     <T extends RefCountedMediaResource> void addResourceToMemoryCache(
    292             final MediaRequest<T> mediaRequest, final T mediaResource) {
    293         Assert.isTrue(mediaResource != null);
    294         final MediaCache<T> mediaCache = mediaRequest.getMediaCache();
    295         if (mediaCache != null) {
    296             mediaCache.addResourceToCache(mediaRequest.getKey(), mediaResource);
    297             if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
    298                 LogUtil.v(TAG, "added media resource to " + mediaCache.getName() + ". key=" +
    299                         LogUtil.sanitizePII(mediaRequest.getKey()) /* key can contain phone# */);
    300             }
    301         }
    302     }
    303 
    304     private class MediaLoadingResult<T extends RefCountedMediaResource> {
    305         public final T loadedResource;
    306         public final boolean fromCache;
    307         private final List<MediaRequest<T>> mChainedRequests;
    308 
    309         MediaLoadingResult(final T loadedResource, final boolean fromCache,
    310                 final List<MediaRequest<T>> chainedRequests) {
    311             this.loadedResource = loadedResource;
    312             this.fromCache = fromCache;
    313             mChainedRequests = chainedRequests;
    314         }
    315 
    316         /**
    317          * Asynchronously schedule a list of chained requests on the background thread.
    318          */
    319         public void scheduleChainedRequests() {
    320             for (final MediaRequest<T> mediaRequest : mChainedRequests) {
    321                 scheduleAsyncMediaRequest(mediaRequest, MEDIA_BACKGROUND_EXECUTOR);
    322             }
    323         }
    324     }
    325 }
    326