Home | History | Annotate | Download | only in widgets
      1 /*
      2 
      3  * Copyright (C) 2011 The Android Open Source Project
      4  *
      5  * Licensed under the Apache License, Version 2.0 (the "License");
      6  * you may not use this file except in compliance with the License.
      7  * You may obtain a copy of the License at
      8  *
      9  *      http://www.apache.org/licenses/LICENSE-2.0
     10  *
     11  * Unless required by applicable law or agreed to in writing, software
     12  * distributed under the License is distributed on an "AS IS" BASIS,
     13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     14  * See the License for the specific language governing permissions and
     15  * limitations under the License.
     16  */
     17 
     18 package com.android.videoeditor.widgets;
     19 
     20 import com.android.videoeditor.service.ApiService;
     21 import com.android.videoeditor.service.MovieMediaItem;
     22 import com.android.videoeditor.R;
     23 
     24 import android.content.Context;
     25 import android.graphics.Bitmap;
     26 import android.graphics.Canvas;
     27 import android.graphics.Color;
     28 import android.graphics.Paint;
     29 import android.graphics.Point;
     30 import android.graphics.Rect;
     31 import android.graphics.drawable.Drawable;
     32 import android.util.AttributeSet;
     33 import android.util.DisplayMetrics;
     34 import android.util.Log;
     35 import android.util.LruCache;
     36 import android.view.Display;
     37 import android.view.GestureDetector;
     38 import android.view.MotionEvent;
     39 import android.view.View;
     40 import android.view.WindowManager;
     41 
     42 import java.util.ArrayList;
     43 import java.util.HashSet;
     44 import java.util.Map;
     45 
     46 /**
     47  * Media item preview view on the timeline. This class assumes the media item is always put on a
     48  * MediaLinearLayout and is wrapped with a timeline scroll view.
     49  */
     50 public class MediaItemView extends View {
     51     private static final String TAG = "MediaItemView";
     52 
     53     // Static variables
     54     private static Drawable sAddTransitionDrawable;
     55     private static Drawable sEmptyFrameDrawable;
     56     private static ThumbnailCache sThumbnailCache;
     57 
     58     // Because MediaItemView may be recreated for the same MediaItem (it happens
     59     // when the device orientation is changed), we use a globally unique
     60     // generation counter to reject thumbnail results (passed to setBitmap())
     61     // requested by a previous incarnation of MediaItemView.
     62     private static int sGenerationCounter;
     63 
     64     // Instance variables
     65     private final GestureDetector mGestureDetector;
     66     private final ScrollViewListener mScrollListener;
     67     private final Rect mGeneratingEffectProgressDestRect;
     68 
     69     private boolean mIsScrolling;
     70     private boolean mIsPlaying;
     71 
     72     // Progress of generation of the effect applied on this media item view.
     73     // -1 indicates the generation is not in progress. 0-100 indicates the
     74     // generation is in progress. Currently only Ken Burns effect is used with
     75     // the progress bar.
     76     private int mGeneratingEffectProgress;
     77 
     78     // The scrolled left pixels of this view.
     79     private int mScrollX;
     80 
     81     private String mProjectPath;
     82     private MovieMediaItem mMediaItem;
     83     // Convenient handle to the parent timeline scroll view.
     84     private TimelineHorizontalScrollView mScrollView;
     85     // Convenient handle to the parent timeline linear layout.
     86     private MediaLinearLayout mTimeline;
     87     private ItemSimpleGestureListener mGestureListener;
     88     private int[] mLeftState, mRightState;
     89 
     90     private int mScreenWidth;
     91     private int mThumbnailWidth, mThumbnailHeight;
     92     private int mNumberOfThumbnails;
     93     private long mBeginTimeMs, mEndTimeMs;
     94 
     95     private int mGeneration;
     96     private HashSet<Integer> mPending;
     97     private ArrayList<Integer> mWantThumbnails;
     98 
     99     public MediaItemView(Context context, AttributeSet attrs) {
    100         this(context, attrs, 0);
    101     }
    102 
    103     public MediaItemView(Context context) {
    104         this(context, null, 0);
    105     }
    106 
    107     public MediaItemView(Context context, AttributeSet attrs, int defStyle) {
    108         super(context, attrs, defStyle);
    109 
    110         // Initialize static data
    111         if (sAddTransitionDrawable == null) {
    112             sAddTransitionDrawable = getResources().getDrawable(
    113                     R.drawable.add_transition_selector);
    114             sEmptyFrameDrawable = getResources().getDrawable(
    115                     R.drawable.timeline_loading);
    116 
    117             // Initialize the thumbnail cache, limit the memory usage to 3MB
    118             sThumbnailCache = new ThumbnailCache(3*1024*1024);
    119         }
    120 
    121         // Get the screen width
    122         final Display display = ((WindowManager)context.getSystemService(
    123                 Context.WINDOW_SERVICE)).getDefaultDisplay();
    124         final DisplayMetrics metrics = new DisplayMetrics();
    125         display.getMetrics(metrics);
    126         mScreenWidth = metrics.widthPixels;
    127 
    128         // Setup our gesture detector and scroll listener
    129         mGestureDetector = new GestureDetector(context, new MyGestureListener());
    130         mScrollListener = new MyScrollViewListener();
    131 
    132         // Prepare the progress bar rectangles
    133         final ProgressBar progressBar = ProgressBar.getProgressBar(context);
    134         final int layoutHeight = (int)(
    135                 getResources().getDimension(R.dimen.media_layout_height) -
    136                 getResources().getDimension(R.dimen.media_layout_padding));
    137         mGeneratingEffectProgressDestRect = new Rect(getPaddingLeft(),
    138                 layoutHeight - progressBar.getHeight() - getPaddingBottom(), 0,
    139                 layoutHeight - getPaddingBottom());
    140 
    141         // Initialize the progress value
    142         mGeneratingEffectProgress = -1;
    143 
    144         // Initialize the "Add transition" indicators state
    145         mLeftState = View.EMPTY_STATE_SET;
    146         mRightState = View.EMPTY_STATE_SET;
    147 
    148         // Initialize the thumbnail indices we want to request
    149         mWantThumbnails = new ArrayList<Integer>();
    150 
    151         // Initialize the set of indices we are waiting
    152         mPending = new HashSet<Integer>();
    153 
    154         // Initialize the generation number
    155         mGeneration = sGenerationCounter++;
    156     }
    157 
    158     private class MyGestureListener extends GestureDetector.SimpleOnGestureListener {
    159         @Override
    160         public boolean onSingleTapConfirmed(MotionEvent e) {
    161             if (mGestureListener == null) {
    162                 return false;
    163             }
    164 
    165             int tappedArea = ItemSimpleGestureListener.CENTER_AREA;
    166 
    167             if (hasSpaceForAddTransitionIcons()) {
    168                 if (mMediaItem.getBeginTransition() == null &&
    169                         e.getX() < sAddTransitionDrawable.getIntrinsicWidth() +
    170                         getPaddingLeft()) {
    171                     tappedArea = ItemSimpleGestureListener.LEFT_AREA;
    172                 } else if (mMediaItem.getEndTransition() == null &&
    173                         e.getX() >= getWidth() - getPaddingRight() -
    174                         sAddTransitionDrawable.getIntrinsicWidth()) {
    175                     tappedArea = ItemSimpleGestureListener.RIGHT_AREA;
    176                 }
    177             }
    178             return mGestureListener.onSingleTapConfirmed(
    179                     MediaItemView.this, tappedArea, e);
    180         }
    181 
    182         @Override
    183         public void onLongPress(MotionEvent e) {
    184             if (mGestureListener != null) {
    185                 mGestureListener.onLongPress(MediaItemView.this, e);
    186             }
    187         }
    188     }
    189 
    190     private class MyScrollViewListener implements ScrollViewListener {
    191         @Override
    192         public void onScrollBegin(View view, int scrollX, int scrollY, boolean appScroll) {
    193             mIsScrolling = true;
    194         }
    195 
    196         @Override
    197         public void onScrollProgress(View view, int scrollX, int scrollY, boolean appScroll) {
    198             mScrollX = scrollX;
    199             invalidate();
    200         }
    201 
    202         @Override
    203         public void onScrollEnd(View view, int scrollX, int scrollY, boolean appScroll) {
    204             mIsScrolling = false;
    205             mScrollX = scrollX;
    206             invalidate();
    207         }
    208     }
    209 
    210     @Override
    211     protected void onAttachedToWindow() {
    212         mMediaItem = (MovieMediaItem) getTag();
    213 
    214         mScrollView = (TimelineHorizontalScrollView) getRootView().findViewById(
    215                 R.id.timeline_scroller);
    216         mScrollView.addScrollListener(mScrollListener);
    217         // Add the horizontal scroll view listener
    218         mScrollX = mScrollView.getScrollX();
    219 
    220         mTimeline = (MediaLinearLayout) getRootView().findViewById(R.id.timeline_media);
    221     }
    222 
    223     @Override
    224     protected void onDetachedFromWindow() {
    225         mScrollView.removeScrollListener(mScrollListener);
    226         // Release the cached bitmaps
    227         releaseBitmapsAndClear();
    228     }
    229 
    230     /**
    231      * @return The shadow builder
    232      */
    233     public DragShadowBuilder getShadowBuilder() {
    234         return new MediaItemShadowBuilder(this);
    235     }
    236 
    237     /**
    238      * Shadow builder for the media item
    239      */
    240     private class MediaItemShadowBuilder extends DragShadowBuilder {
    241         private final Drawable mFrame;
    242 
    243         public MediaItemShadowBuilder(View view) {
    244             super(view);
    245             mFrame = view.getContext().getResources().getDrawable(
    246                     R.drawable.timeline_item_pressed);
    247         }
    248 
    249         @Override
    250         public void onProvideShadowMetrics(Point shadowSize, Point shadowTouchPoint) {
    251             shadowSize.set(getShadowWidth(), getShadowHeight());
    252             shadowTouchPoint.set(shadowSize.x / 2, shadowSize.y);
    253         }
    254 
    255         @Override
    256         public void onDrawShadow(Canvas canvas) {
    257             mFrame.setBounds(0, 0, getShadowWidth(), getShadowHeight());
    258             mFrame.draw(canvas);
    259 
    260             Bitmap bitmap = getOneThumbnail();
    261             if (bitmap != null) {
    262                 final View view = getView();
    263                 canvas.drawBitmap(bitmap, view.getPaddingLeft(),
    264                         view.getPaddingTop(), null);
    265             }
    266         }
    267     }
    268 
    269     /**
    270      * @return The shadow width
    271      */
    272     private int getShadowWidth() {
    273         final int thumbnailHeight = getHeight() - getPaddingTop() - getPaddingBottom();
    274         final int thumbnailWidth = (thumbnailHeight * mMediaItem.getWidth()) /
    275             mMediaItem.getHeight();
    276         return thumbnailWidth + getPaddingLeft() + getPaddingRight();
    277     }
    278 
    279     /**
    280      * @return The shadow height
    281      */
    282     private int getShadowHeight() {
    283         return getHeight();
    284     }
    285 
    286     private Bitmap getOneThumbnail() {
    287         ThumbnailKey key = new ThumbnailKey();
    288         key.mediaItemId = mMediaItem.getId();
    289 
    290         // Find any one cached thumbnail
    291         for (int i = 0; i < mNumberOfThumbnails; i++) {
    292             key.index = i;
    293             Bitmap bitmap = sThumbnailCache.get(key);
    294             if (bitmap != null) {
    295                 return bitmap;
    296             }
    297         }
    298 
    299         return null;
    300     }
    301 
    302     /**
    303      * @param projectPath The project path
    304      */
    305     public void setProjectPath(String projectPath) {
    306         mProjectPath = projectPath;
    307     }
    308 
    309     /**
    310      * @param listener The gesture listener
    311      */
    312     public void setGestureListener(ItemSimpleGestureListener listener) {
    313         mGestureListener = listener;
    314     }
    315 
    316     /**
    317      * A view enters or exits the playback mode
    318      *
    319      * @param playback true if playback is in progress
    320      */
    321     public void setPlaybackMode(boolean playback) {
    322         mIsPlaying = playback;
    323         invalidate();
    324     }
    325 
    326     /**
    327      * Resets the effect generation progress status.
    328      */
    329     public void resetGeneratingEffectProgress() {
    330         setGeneratingEffectProgress(-1);
    331     }
    332 
    333     /**
    334      * Sets the effect generation progress of this view.
    335      */
    336     public void setGeneratingEffectProgress(int progress) {
    337         if (progress == 0) {
    338             mGeneratingEffectProgress = progress;
    339             // Release the current set of bitmaps. New content is being generated.
    340             releaseBitmapsAndClear();
    341         } else if (progress == 100) {
    342             mGeneratingEffectProgress = -1;
    343         } else {
    344             mGeneratingEffectProgress = progress;
    345         }
    346 
    347         invalidate();
    348     }
    349 
    350     /**
    351      * The view has been layout out.
    352      *
    353      * @param oldLeft The old left position
    354      * @param oldRight The old right position
    355      */
    356     public void onLayoutPerformed(int oldLeft, int oldRight) {
    357         // Compute the thumbnail width and height
    358         mThumbnailHeight = getHeight() - getPaddingTop() - getPaddingBottom();
    359         mThumbnailWidth = (mThumbnailHeight * mMediaItem.getWidth()) / mMediaItem.getHeight();
    360 
    361         // We are not able to display a bitmap with width or height > 2048.
    362         while (mThumbnailWidth > 2048 || mThumbnailHeight > 2048) {
    363             mThumbnailHeight /= 2;
    364             mThumbnailWidth /= 2;
    365         }
    366 
    367         int usableWidth = getWidth() - getPaddingLeft() - getPaddingRight();
    368         // Compute the ceiling of (usableWidth / mThumbnailWidth).
    369         mNumberOfThumbnails = (usableWidth + mThumbnailWidth - 1) / mThumbnailWidth;
    370         mBeginTimeMs = mMediaItem.getAppBoundaryBeginTime();
    371         mEndTimeMs = mMediaItem.getAppBoundaryEndTime();
    372 
    373         releaseBitmapsAndClear();
    374         invalidate();
    375     }
    376 
    377     /**
    378      * @return True if the effect generation is in progress
    379      */
    380     public boolean isGeneratingEffect() {
    381         return (mGeneratingEffectProgress >= 0);
    382     }
    383 
    384     public boolean setBitmap(Bitmap bitmap, int index, int token) {
    385         // Ignore results from previous requests
    386         if (token != mGeneration) {
    387             return false;
    388         }
    389         if (!mPending.contains(index)) {
    390             Log.e(TAG, "received unasked bitmap, index = " + index);
    391             return false;
    392         }
    393         if (bitmap == null) {
    394             Log.w(TAG, "receive null bitmap for index = " + index);
    395             // We keep this request in mPending, so we won't request it again.
    396             return false;
    397         }
    398         mPending.remove(index);
    399         ThumbnailKey key = new ThumbnailKey(mMediaItem.getId(), index);
    400         sThumbnailCache.put(key, bitmap);
    401 
    402         invalidate();
    403         return true;
    404     }
    405 
    406     @Override
    407     protected void onDraw(Canvas canvas) {
    408         super.onDraw(canvas);
    409         if (mGeneratingEffectProgress >= 0) {
    410             ProgressBar.getProgressBar(getContext()).draw(
    411                     canvas, mGeneratingEffectProgress, mGeneratingEffectProgressDestRect,
    412                     getPaddingLeft(), getWidth() - getPaddingRight());
    413         } else {
    414             // Do not draw in the padding area
    415             canvas.clipRect(getPaddingLeft(), getPaddingTop(),
    416                     getWidth() - getPaddingRight(),
    417                     getHeight() - getPaddingBottom());
    418 
    419             // Draw thumbnails
    420             drawThumbnails(canvas);
    421 
    422             // Draw the "Add transition" indicators
    423             if (isSelected()) {
    424                 drawAddTransitionIcons(canvas);
    425             } else if (mTimeline.hasItemSelected()) {
    426                 // Dim myself if some view on the timeline is selected but not me
    427                 // by drawing a transparent black overlay.
    428                 final Paint paint = new Paint();
    429                 paint.setColor(Color.BLACK);
    430                 paint.setAlpha(192);
    431                 canvas.drawPaint(paint);
    432             }
    433 
    434             // Request thumbnails if things are not moving
    435             boolean isBusy = mIsPlaying || mTimeline.isTrimming() || mIsScrolling;
    436             if (!isBusy && !mWantThumbnails.isEmpty()) {
    437                 requestThumbnails();
    438             }
    439         }
    440     }
    441 
    442     // Draws the thumbnails, also put unavailable thumbnail indices in
    443     // mWantThumbnails.
    444     private void drawThumbnails(Canvas canvas) {
    445         mWantThumbnails.clear();
    446 
    447         // The screen coordinate of the left edge of the usable area.
    448         int left = getLeft() + getPaddingLeft() - mScrollX;
    449         // The screen coordinate of the right edge of the usable area.
    450         int right = getRight() - getPaddingRight() - mScrollX;
    451         // Return if the usable area is not on screen.
    452         if (left >= mScreenWidth || right <= 0 || left >= right) {
    453             return;
    454         }
    455 
    456         // Map [0, mScreenWidth - 1] to the indices of the thumbnail.
    457         int startIdx = (0 - left) / mThumbnailWidth;
    458         int endIdx = (mScreenWidth - 1 - left) / mThumbnailWidth;
    459 
    460         startIdx = clamp(startIdx, 0, mNumberOfThumbnails - 1);
    461         endIdx = clamp(endIdx, 0, mNumberOfThumbnails - 1);
    462 
    463         // Prepare variables used in the loop
    464         ThumbnailKey key = new ThumbnailKey();
    465         key.mediaItemId = mMediaItem.getId();
    466         int x = getPaddingLeft() + startIdx * mThumbnailWidth;
    467         int y = getPaddingTop();
    468 
    469         // Center the thumbnail vertically
    470         int spacing = (getHeight() - getPaddingTop() - getPaddingBottom() -
    471                 mThumbnailHeight) / 2;
    472         y += spacing;
    473 
    474         // Loop through the thumbnails on screen and draw it
    475         for (int i = startIdx; i <= endIdx; i++) {
    476             key.index = i;
    477             Bitmap bitmap = sThumbnailCache.get(key);
    478             if (bitmap == null) {
    479                 // Draw a frame placeholder
    480                 sEmptyFrameDrawable.setBounds(
    481                         x, y, x + mThumbnailWidth, y + mThumbnailHeight);
    482                 sEmptyFrameDrawable.draw(canvas);
    483                 if (!mPending.contains(i)) {
    484                     mWantThumbnails.add(Integer.valueOf(i));
    485                 }
    486             } else {
    487                 canvas.drawBitmap(bitmap, x, y, null);
    488             }
    489             x += mThumbnailWidth;
    490         }
    491     }
    492 
    493     /**
    494      * Draws the "Add transition" icons at the beginning and end of the media item.
    495      *
    496      * @param canvas Canvas to be drawn
    497      */
    498     private void drawAddTransitionIcons(Canvas canvas) {
    499         if (hasSpaceForAddTransitionIcons()) {
    500             if (mMediaItem.getBeginTransition() == null) {
    501                 sAddTransitionDrawable.setState(mLeftState);
    502                 sAddTransitionDrawable.setBounds(getPaddingLeft(), getPaddingTop(),
    503                         sAddTransitionDrawable.getIntrinsicWidth() + getPaddingLeft(),
    504                         getPaddingTop() + sAddTransitionDrawable.getIntrinsicHeight());
    505                 sAddTransitionDrawable.draw(canvas);
    506             }
    507 
    508             if (mMediaItem.getEndTransition() == null) {
    509                 sAddTransitionDrawable.setState(mRightState);
    510                 sAddTransitionDrawable.setBounds(
    511                         getWidth() - getPaddingRight() -
    512                         sAddTransitionDrawable.getIntrinsicWidth(),
    513                         getPaddingTop(), getWidth() - getPaddingRight(),
    514                         getPaddingTop() + sAddTransitionDrawable.getIntrinsicHeight());
    515                 sAddTransitionDrawable.draw(canvas);
    516             }
    517         }
    518     }
    519 
    520     /**
    521      * @return true if the visible area of this view is big enough to display
    522      *      "add transition" icons on both sides; false otherwise.
    523      */
    524     private boolean hasSpaceForAddTransitionIcons() {
    525         if (mTimeline.isTrimming()) {
    526             return false;
    527         }
    528 
    529         return (getWidth() - getPaddingLeft() - getPaddingRight() >=
    530                 2 * sAddTransitionDrawable.getIntrinsicWidth());
    531     }
    532 
    533     /**
    534      * Clamps the input value v to the range [low, high].
    535      */
    536     private static int clamp(int v, int low, int high) {
    537         return Math.min(Math.max(v, low), high);
    538     }
    539 
    540     /**
    541      * Requests the thumbnails in mWantThumbnails (which is filled by onDraw).
    542      */
    543     private void requestThumbnails() {
    544         // Copy mWantThumbnails to an array
    545         int indices[] = new int[mWantThumbnails.size()];
    546         for (int i = 0; i < mWantThumbnails.size(); i++) {
    547             indices[i] = mWantThumbnails.get(i);
    548         }
    549 
    550         // Put them in the pending set
    551         mPending.addAll(mWantThumbnails);
    552 
    553         ApiService.getMediaItemThumbnails(getContext(), mProjectPath,
    554                 mMediaItem.getId(), mThumbnailWidth, mThumbnailHeight,
    555                 mBeginTimeMs, mEndTimeMs, mNumberOfThumbnails, mGeneration,
    556                 indices);
    557     }
    558 
    559     @Override
    560     public boolean onTouchEvent(MotionEvent ev) {
    561         // Let the gesture detector inspect all events.
    562         mGestureDetector.onTouchEvent(ev);
    563         super.onTouchEvent(ev);
    564 
    565         switch (ev.getAction()) {
    566             case MotionEvent.ACTION_DOWN: {
    567                 mLeftState = View.EMPTY_STATE_SET;
    568                 mRightState = View.EMPTY_STATE_SET;
    569                 if (isSelected() && hasSpaceForAddTransitionIcons()) {
    570                     if (ev.getX() < sAddTransitionDrawable.getIntrinsicWidth() +
    571                             getPaddingLeft()) {
    572                         if (mMediaItem.getBeginTransition() == null) {
    573                             mLeftState = View.PRESSED_WINDOW_FOCUSED_STATE_SET;
    574                         }
    575                     } else if (ev.getX() >= getWidth() - getPaddingRight() -
    576                             sAddTransitionDrawable.getIntrinsicWidth()) {
    577                         if (mMediaItem.getEndTransition() == null) {
    578                             mRightState = View.PRESSED_WINDOW_FOCUSED_STATE_SET;
    579                         }
    580                     }
    581                 }
    582                 invalidate();
    583                 break;
    584             }
    585 
    586             case MotionEvent.ACTION_UP:
    587             case MotionEvent.ACTION_CANCEL: {
    588                 mRightState = View.EMPTY_STATE_SET;
    589                 mLeftState = View.EMPTY_STATE_SET;
    590                 invalidate();
    591                 break;
    592             }
    593 
    594             default: {
    595                 break;
    596             }
    597         }
    598 
    599         return true;
    600     }
    601 
    602     private void releaseBitmapsAndClear() {
    603         sThumbnailCache.clearForMediaItemId(mMediaItem.getId());
    604         mPending.clear();
    605         mGeneration = sGenerationCounter++;
    606     }
    607 }
    608 
    609 class ThumbnailKey {
    610     public String mediaItemId;
    611     public int index;
    612 
    613     public ThumbnailKey() {
    614     }
    615 
    616     public ThumbnailKey(String id, int idx) {
    617         mediaItemId = id;
    618         index = idx;
    619     }
    620 
    621     @Override
    622     public boolean equals(Object o) {
    623         if (!(o instanceof ThumbnailKey)) {
    624             return false;
    625         }
    626         ThumbnailKey key = (ThumbnailKey) o;
    627         return index == key.index && mediaItemId.equals(key.mediaItemId);
    628     }
    629 
    630     @Override
    631     public int hashCode() {
    632         return mediaItemId.hashCode() ^ index;
    633     }
    634 }
    635 
    636 class ThumbnailCache {
    637     private LruCache<ThumbnailKey, Bitmap> mCache;
    638 
    639     public ThumbnailCache(int size) {
    640         mCache = new LruCache<ThumbnailKey, Bitmap>(size) {
    641             @Override
    642             protected int sizeOf(ThumbnailKey key, Bitmap value) {
    643                 return value.getByteCount();
    644             }
    645         };
    646     }
    647 
    648     void put(ThumbnailKey key, Bitmap value) {
    649         mCache.put(key, value);
    650     }
    651 
    652     Bitmap get(ThumbnailKey key) {
    653         return mCache.get(key);
    654     }
    655 
    656     void clearForMediaItemId(String id) {
    657         Map<ThumbnailKey, Bitmap> map = mCache.snapshot();
    658         for (ThumbnailKey key : map.keySet()) {
    659             if (key.mediaItemId.equals(id)) {
    660                 mCache.remove(key);
    661             }
    662         }
    663     }
    664 }
    665