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