RecentsPanelView.java revision dee4eaf02d4054986afea01876f824461625ebbf
1/*
2 * Copyright (C) 2011 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
17package com.android.systemui.recent;
18
19import android.animation.Animator;
20import android.animation.LayoutTransition;
21import android.app.ActivityManager;
22import android.content.Context;
23import android.content.Intent;
24import android.content.res.Configuration;
25import android.content.res.Resources;
26import android.graphics.Bitmap;
27import android.graphics.Matrix;
28import android.graphics.Shader.TileMode;
29import android.graphics.drawable.BitmapDrawable;
30import android.net.Uri;
31import android.provider.Settings;
32import android.util.AttributeSet;
33import android.util.Log;
34import android.view.KeyEvent;
35import android.view.LayoutInflater;
36import android.view.MenuItem;
37import android.view.MotionEvent;
38import android.view.View;
39import android.view.ViewConfiguration;
40import android.view.ViewGroup;
41import android.view.accessibility.AccessibilityEvent;
42import android.view.animation.AnimationUtils;
43import android.widget.AdapterView;
44import android.widget.AdapterView.OnItemClickListener;
45import android.widget.BaseAdapter;
46import android.widget.HorizontalScrollView;
47import android.widget.ImageView;
48import android.widget.ImageView.ScaleType;
49import android.widget.PopupMenu;
50import android.widget.RelativeLayout;
51import android.widget.ScrollView;
52import android.widget.TextView;
53
54import com.android.systemui.R;
55import com.android.systemui.statusbar.StatusBar;
56import com.android.systemui.statusbar.phone.PhoneStatusBar;
57import com.android.systemui.statusbar.tablet.StatusBarPanel;
58import com.android.systemui.statusbar.tablet.TabletStatusBar;
59
60import java.util.ArrayList;
61
62public class RecentsPanelView extends RelativeLayout implements OnItemClickListener, RecentsCallback,
63        StatusBarPanel, Animator.AnimatorListener, View.OnTouchListener {
64    static final String TAG = "RecentsPanelView";
65    static final boolean DEBUG = TabletStatusBar.DEBUG || PhoneStatusBar.DEBUG || false;
66    private Context mContext;
67    private StatusBar mBar;
68    private View mRecentsScrim;
69    private View mRecentsNoApps;
70    private ViewGroup mRecentsContainer;
71    private StatusBarTouchProxy mStatusBarTouchProxy;
72
73    private boolean mShowing;
74    private Choreographer mChoreo;
75    OnRecentsPanelVisibilityChangedListener mVisibilityChangedListener;
76
77    private RecentTasksLoader mRecentTasksLoader;
78    private ArrayList<TaskDescription> mRecentTaskDescriptions;
79    private Runnable mPreloadTasksRunnable;
80    private boolean mRecentTasksDirty = true;
81    private TaskDescriptionAdapter mListAdapter;
82    private int mThumbnailWidth;
83    private boolean mFitThumbnailToXY;
84
85    public static interface OnRecentsPanelVisibilityChangedListener {
86        public void onRecentsPanelVisibilityChanged(boolean visible);
87    }
88
89    private final class OnLongClickDelegate implements View.OnLongClickListener {
90        View mOtherView;
91        OnLongClickDelegate(View other) {
92            mOtherView = other;
93        }
94        public boolean onLongClick(View v) {
95            return mOtherView.performLongClick();
96        }
97    }
98
99    /* package */ final static class ViewHolder {
100        View thumbnailView;
101        ImageView thumbnailViewImage;
102        Bitmap thumbnailViewImageBitmap;
103        ImageView iconView;
104        TextView labelView;
105        TextView descriptionView;
106        TaskDescription taskDescription;
107    }
108
109    /* package */ final class TaskDescriptionAdapter extends BaseAdapter {
110        private LayoutInflater mInflater;
111
112        public TaskDescriptionAdapter(Context context) {
113            mInflater = LayoutInflater.from(context);
114        }
115
116        public int getCount() {
117            return mRecentTaskDescriptions != null ? mRecentTaskDescriptions.size() : 0;
118        }
119
120        public Object getItem(int position) {
121            return position; // we only need the index
122        }
123
124        public long getItemId(int position) {
125            return position; // we just need something unique for this position
126        }
127
128        public View getView(int position, View convertView, ViewGroup parent) {
129            ViewHolder holder;
130            if (convertView == null) {
131                convertView = mInflater.inflate(R.layout.status_bar_recent_item, parent, false);
132                holder = new ViewHolder();
133                holder.thumbnailView = convertView.findViewById(R.id.app_thumbnail);
134                holder.thumbnailViewImage = (ImageView) convertView.findViewById(
135                        R.id.app_thumbnail_image);
136                // If we set the default thumbnail now, we avoid an onLayout when we update
137                // the thumbnail later (if they both have the same dimensions)
138                updateThumbnail(holder, mRecentTasksLoader.getDefaultThumbnail(), false, false);
139
140                holder.iconView = (ImageView) convertView.findViewById(R.id.app_icon);
141                holder.labelView = (TextView) convertView.findViewById(R.id.app_label);
142                holder.descriptionView = (TextView) convertView.findViewById(R.id.app_description);
143
144                convertView.setTag(holder);
145            } else {
146                holder = (ViewHolder) convertView.getTag();
147            }
148
149            // index is reverse since most recent appears at the bottom...
150            final int index = mRecentTaskDescriptions.size() - position - 1;
151
152            final TaskDescription td = mRecentTaskDescriptions.get(index);
153            holder.iconView.setImageDrawable(td.getIcon());
154            holder.labelView.setText(td.getLabel());
155            holder.thumbnailView.setContentDescription(td.getLabel());
156            updateThumbnail(holder, td.getThumbnail(), true, false);
157
158            holder.thumbnailView.setTag(td);
159            holder.thumbnailView.setOnLongClickListener(new OnLongClickDelegate(convertView));
160            holder.taskDescription = td;
161
162            return convertView;
163        }
164    }
165
166    @Override
167    public boolean onKeyUp(int keyCode, KeyEvent event) {
168        if (keyCode == KeyEvent.KEYCODE_BACK && !event.isCanceled()) {
169            show(false, true);
170            return true;
171        }
172        return super.onKeyUp(keyCode, event);
173    }
174
175    private boolean pointInside(int x, int y, View v) {
176        final int l = v.getLeft();
177        final int r = v.getRight();
178        final int t = v.getTop();
179        final int b = v.getBottom();
180        return x >= l && x < r && y >= t && y < b;
181    }
182
183    public boolean isInContentArea(int x, int y) {
184        if (pointInside(x, y, mRecentsContainer)) {
185            return true;
186        } else if (mStatusBarTouchProxy != null &&
187                pointInside(x, y, mStatusBarTouchProxy)) {
188            return true;
189        } else {
190            return false;
191        }
192    }
193
194    public void show(boolean show, boolean animate) {
195        show(show, animate, null);
196    }
197
198    public void show(boolean show, boolean animate,
199            ArrayList<TaskDescription> recentTaskDescriptions) {
200        if (show) {
201            // Need to update list of recent apps before we set visibility so this view's
202            // content description is updated before it gets focus for TalkBack mode
203            refreshRecentTasksList(recentTaskDescriptions);
204
205            // if there are no apps, either bring up a "No recent apps" message, or just
206            // quit early
207            boolean noApps = (mRecentTaskDescriptions.size() == 0);
208            if (mRecentsNoApps != null) {
209                mRecentsNoApps.setVisibility(noApps ? View.VISIBLE : View.INVISIBLE);
210            } else {
211                if (noApps) {
212                    if (DEBUG) Log.v(TAG, "Nothing to show");
213                    // Need to set recent tasks to dirty so that next time we load, we
214                    // refresh the list of tasks
215                    mRecentTasksLoader.cancelLoadingThumbnails();
216                    mRecentTasksDirty = true;
217                    return;
218                }
219            }
220        } else {
221            // Need to set recent tasks to dirty so that next time we load, we
222            // refresh the list of tasks
223            mRecentTasksLoader.cancelLoadingThumbnails();
224            mRecentTasksDirty = true;
225        }
226        if (animate) {
227            if (mShowing != show) {
228                mShowing = show;
229                if (show) {
230                    setVisibility(View.VISIBLE);
231                }
232                mChoreo.startAnimation(show);
233            }
234        } else {
235            mShowing = show;
236            setVisibility(show ? View.VISIBLE : View.GONE);
237            mChoreo.jumpTo(show);
238            onAnimationEnd(null);
239        }
240        if (show) {
241            setFocusable(true);
242            setFocusableInTouchMode(true);
243            requestFocus();
244        }
245    }
246
247    public void dismiss() {
248        hide(true);
249    }
250
251    public void hide(boolean animate) {
252        if (!animate) {
253            setVisibility(View.GONE);
254        }
255        if (mBar != null) {
256            mBar.animateCollapse();
257        }
258    }
259
260    public void handleShowBackground(boolean show) {
261        if (show) {
262            mRecentsScrim.setBackgroundResource(R.drawable.status_bar_recents_background_solid);
263        } else {
264            mRecentsScrim.setBackgroundDrawable(null);
265        }
266    }
267
268    public boolean isRecentsVisible() {
269        return getVisibility() == VISIBLE;
270    }
271
272    public void onAnimationCancel(Animator animation) {
273    }
274
275    public void onAnimationEnd(Animator animation) {
276        if (mShowing) {
277            final LayoutTransition transitioner = new LayoutTransition();
278            ((ViewGroup)mRecentsContainer).setLayoutTransition(transitioner);
279            createCustomAnimations(transitioner);
280        } else {
281            ((ViewGroup)mRecentsContainer).setLayoutTransition(null);
282            clearRecentTasksList();
283        }
284    }
285
286    public void onAnimationRepeat(Animator animation) {
287    }
288
289    public void onAnimationStart(Animator animation) {
290    }
291
292    /**
293     * We need to be aligned at the bottom.  LinearLayout can't do this, so instead,
294     * let LinearLayout do all the hard work, and then shift everything down to the bottom.
295     */
296    @Override
297    protected void onLayout(boolean changed, int l, int t, int r, int b) {
298        super.onLayout(changed, l, t, r, b);
299        mChoreo.setPanelHeight(mRecentsContainer.getHeight());
300    }
301
302    @Override
303    public boolean dispatchHoverEvent(MotionEvent event) {
304        // Ignore hover events outside of this panel bounds since such events
305        // generate spurious accessibility events with the panel content when
306        // tapping outside of it, thus confusing the user.
307        final int x = (int) event.getX();
308        final int y = (int) event.getY();
309        if (x >= 0 && x < getWidth() && y >= 0 && y < getHeight()) {
310            return super.dispatchHoverEvent(event);
311        }
312        return true;
313    }
314
315    /**
316     * Whether the panel is showing, or, if it's animating, whether it will be
317     * when the animation is done.
318     */
319    public boolean isShowing() {
320        return mShowing;
321    }
322
323    public void setBar(StatusBar bar) {
324        mBar = bar;
325
326    }
327
328    public void setStatusBarView(View statusBarView) {
329        if (mStatusBarTouchProxy != null) {
330            mStatusBarTouchProxy.setStatusBar(statusBarView);
331        }
332    }
333
334    public void setRecentTasksLoader(RecentTasksLoader loader) {
335        mRecentTasksLoader = loader;
336    }
337
338    public void setOnVisibilityChangedListener(OnRecentsPanelVisibilityChangedListener l) {
339        mVisibilityChangedListener = l;
340
341    }
342
343    public void setVisibility(int visibility) {
344        if (mVisibilityChangedListener != null) {
345            mVisibilityChangedListener.onRecentsPanelVisibilityChanged(visibility == VISIBLE);
346        }
347        super.setVisibility(visibility);
348    }
349
350    public RecentsPanelView(Context context, AttributeSet attrs) {
351        this(context, attrs, 0);
352    }
353
354    public RecentsPanelView(Context context, AttributeSet attrs, int defStyle) {
355        super(context, attrs, defStyle);
356        mContext = context;
357        updateValuesFromResources();
358    }
359
360    public void updateValuesFromResources() {
361        final Resources res = mContext.getResources();
362        mThumbnailWidth = Math.round(res.getDimension(R.dimen.status_bar_recents_thumbnail_width));
363        mFitThumbnailToXY = res.getBoolean(R.bool.config_recents_thumbnail_image_fits_to_xy);
364    }
365
366    @Override
367    protected void onFinishInflate() {
368        super.onFinishInflate();
369        mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
370        mRecentsContainer = (ViewGroup) findViewById(R.id.recents_container);
371        mStatusBarTouchProxy = (StatusBarTouchProxy) findViewById(R.id.status_bar_touch_proxy);
372        mListAdapter = new TaskDescriptionAdapter(mContext);
373        if (mRecentsContainer instanceof RecentsHorizontalScrollView){
374            RecentsHorizontalScrollView scrollView
375                    = (RecentsHorizontalScrollView) mRecentsContainer;
376            scrollView.setAdapter(mListAdapter);
377            scrollView.setCallback(this);
378        } else if (mRecentsContainer instanceof RecentsVerticalScrollView){
379            RecentsVerticalScrollView scrollView
380                    = (RecentsVerticalScrollView) mRecentsContainer;
381            scrollView.setAdapter(mListAdapter);
382            scrollView.setCallback(this);
383        }
384        else {
385            throw new IllegalArgumentException("missing Recents[Horizontal]ScrollView");
386        }
387
388
389        mRecentsScrim = findViewById(R.id.recents_bg_protect);
390        mRecentsNoApps = findViewById(R.id.recents_no_apps);
391        mChoreo = new Choreographer(this, mRecentsScrim, mRecentsContainer, mRecentsNoApps, this);
392
393        // In order to save space, we make the background texture repeat in the Y direction
394        if (mRecentsScrim != null && mRecentsScrim.getBackground() instanceof BitmapDrawable) {
395            ((BitmapDrawable) mRecentsScrim.getBackground()).setTileModeY(TileMode.REPEAT);
396        }
397
398        mPreloadTasksRunnable = new Runnable() {
399            public void run() {
400                if (!mShowing) {
401                    setVisibility(INVISIBLE);
402                    refreshRecentTasksList();
403                }
404            }
405        };
406    }
407
408    private void createCustomAnimations(LayoutTransition transitioner) {
409        transitioner.setDuration(200);
410        transitioner.setStartDelay(LayoutTransition.CHANGE_DISAPPEARING, 0);
411        transitioner.setAnimator(LayoutTransition.DISAPPEARING, null);
412    }
413
414    @Override
415    protected void onVisibilityChanged(View changedView, int visibility) {
416        super.onVisibilityChanged(changedView, visibility);
417        if (DEBUG) Log.v(TAG, "onVisibilityChanged(" + changedView + ", " + visibility + ")");
418
419        if (mRecentsContainer instanceof RecentsHorizontalScrollView) {
420            ((RecentsHorizontalScrollView) mRecentsContainer).onRecentsVisibilityChanged();
421        } else if (mRecentsContainer instanceof RecentsVerticalScrollView) {
422            ((RecentsVerticalScrollView) mRecentsContainer).onRecentsVisibilityChanged();
423        } else {
424            throw new IllegalArgumentException("missing Recents[Horizontal]ScrollView");
425        }
426    }
427
428    private void updateThumbnail(ViewHolder h, Bitmap thumbnail, boolean show, boolean anim) {
429        if (thumbnail != null) {
430            // Should remove the default image in the frame
431            // that this now covers, to improve scrolling speed.
432            // That can't be done until the anim is complete though.
433            h.thumbnailViewImage.setImageBitmap(thumbnail);
434
435            // scale the image to fill the full width of the ImageView. do this only if
436            // we haven't set a bitmap before, or if the bitmap size has changed
437            if (h.thumbnailViewImageBitmap == null ||
438                h.thumbnailViewImageBitmap.getWidth() != thumbnail.getWidth() ||
439                h.thumbnailViewImageBitmap.getHeight() != thumbnail.getHeight()) {
440                if (mFitThumbnailToXY) {
441                    h.thumbnailViewImage.setScaleType(ScaleType.FIT_XY);
442                } else {
443                    Matrix scaleMatrix = new Matrix();
444                    float scale = mThumbnailWidth / (float) thumbnail.getWidth();
445                    scaleMatrix.setScale(scale, scale);
446                    h.thumbnailViewImage.setScaleType(ScaleType.MATRIX);
447                    h.thumbnailViewImage.setImageMatrix(scaleMatrix);
448                }
449            }
450            if (show && h.thumbnailView.getVisibility() != View.VISIBLE) {
451                if (anim) {
452                    h.thumbnailView.setAnimation(
453                            AnimationUtils.loadAnimation(mContext, R.anim.recent_appear));
454                }
455                h.thumbnailView.setVisibility(View.VISIBLE);
456            }
457            h.thumbnailViewImageBitmap = thumbnail;
458        }
459    }
460
461    void onTaskThumbnailLoaded(TaskDescription ad) {
462        synchronized (ad) {
463            if (mRecentsContainer != null) {
464                ViewGroup container = mRecentsContainer;
465                if (container instanceof HorizontalScrollView
466                        || container instanceof ScrollView) {
467                    container = (ViewGroup)container.findViewById(
468                            R.id.recents_linear_layout);
469                }
470                // Look for a view showing this thumbnail, to update.
471                for (int i=0; i<container.getChildCount(); i++) {
472                    View v = container.getChildAt(i);
473                    if (v.getTag() instanceof ViewHolder) {
474                        ViewHolder h = (ViewHolder)v.getTag();
475                        if (h.taskDescription == ad) {
476                            // only fade in the thumbnail if recents is already visible-- we
477                            // show it immediately otherwise
478                            boolean animateShow = mShowing &&
479                                mRecentsContainer.getAlpha() > ViewConfiguration.ALPHA_THRESHOLD;
480                            updateThumbnail(h, ad.getThumbnail(), true, animateShow);
481                        }
482                    }
483                }
484            }
485        }
486    }
487
488    // additional optimization when we have sofware system buttons - start loading the recent
489    // tasks on touch down
490    @Override
491    public boolean onTouch(View v, MotionEvent ev) {
492        if (!mShowing) {
493            int action = ev.getAction() & MotionEvent.ACTION_MASK;
494            if (action == MotionEvent.ACTION_DOWN) {
495                // If we set our visibility to INVISIBLE here, we avoid an extra call to
496                // onLayout later when we become visible (because onLayout is always called
497                // when going from GONE)
498                post(mPreloadTasksRunnable);
499            } else if (action == MotionEvent.ACTION_CANCEL) {
500                setVisibility(GONE);
501                clearRecentTasksList();
502                // Remove the preloader if we haven't called it yet
503                removeCallbacks(mPreloadTasksRunnable);
504            } else if (action == MotionEvent.ACTION_UP) {
505                // Remove the preloader if we haven't called it yet
506                removeCallbacks(mPreloadTasksRunnable);
507                if (!v.isPressed()) {
508                    setVisibility(GONE);
509                    clearRecentTasksList();
510                }
511            }
512        }
513        return false;
514    }
515
516    public void clearRecentTasksList() {
517        // Clear memory used by screenshots
518        if (mRecentTaskDescriptions != null) {
519            mRecentTasksLoader.cancelLoadingThumbnails();
520            mRecentTaskDescriptions.clear();
521            mListAdapter.notifyDataSetInvalidated();
522            mRecentTasksDirty = true;
523        }
524    }
525
526    public void refreshRecentTasksList() {
527        refreshRecentTasksList(null);
528    }
529
530    private void refreshRecentTasksList(ArrayList<TaskDescription> recentTasksList) {
531        if (mRecentTasksDirty) {
532            if (recentTasksList != null) {
533                mRecentTaskDescriptions = recentTasksList;
534            } else {
535                mRecentTaskDescriptions = mRecentTasksLoader.getRecentTasks();
536            }
537            mListAdapter.notifyDataSetInvalidated();
538            updateUiElements(getResources().getConfiguration());
539            mRecentTasksDirty = false;
540        }
541    }
542
543    public ArrayList<TaskDescription> getRecentTasksList() {
544        return mRecentTaskDescriptions;
545    }
546
547    private void updateUiElements(Configuration config) {
548        final int items = mRecentTaskDescriptions.size();
549
550        mRecentsContainer.setVisibility(items > 0 ? View.VISIBLE : View.GONE);
551
552        // Set description for accessibility
553        int numRecentApps = mRecentTaskDescriptions.size();
554        String recentAppsAccessibilityDescription;
555        if (numRecentApps == 0) {
556            recentAppsAccessibilityDescription =
557                getResources().getString(R.string.status_bar_no_recent_apps);
558        } else {
559            recentAppsAccessibilityDescription = getResources().getQuantityString(
560                R.plurals.status_bar_accessibility_recent_apps, numRecentApps, numRecentApps);
561        }
562        setContentDescription(recentAppsAccessibilityDescription);
563    }
564
565    public void handleOnClick(View view) {
566        TaskDescription ad = ((ViewHolder) view.getTag()).taskDescription;
567        final Context context = view.getContext();
568        final ActivityManager am = (ActivityManager)
569                context.getSystemService(Context.ACTIVITY_SERVICE);
570        if (ad.taskId >= 0) {
571            // This is an active task; it should just go to the foreground.
572            am.moveTaskToFront(ad.taskId, ActivityManager.MOVE_TASK_WITH_HOME);
573        } else {
574            Intent intent = ad.intent;
575            intent.addFlags(Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY
576                    | Intent.FLAG_ACTIVITY_TASK_ON_HOME
577                    | Intent.FLAG_ACTIVITY_NEW_TASK);
578            if (DEBUG) Log.v(TAG, "Starting activity " + intent);
579            context.startActivity(intent);
580        }
581        hide(true);
582    }
583
584    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
585        handleOnClick(view);
586    }
587
588    public void handleSwipe(View view) {
589        TaskDescription ad = ((ViewHolder) view.getTag()).taskDescription;
590        if (DEBUG) Log.v(TAG, "Jettison " + ad.getLabel());
591        mRecentTaskDescriptions.remove(ad);
592
593        // Handled by widget containers to enable LayoutTransitions properly
594        // mListAdapter.notifyDataSetChanged();
595
596        if (mRecentTaskDescriptions.size() == 0) {
597            hide(false);
598        }
599
600        // Currently, either direction means the same thing, so ignore direction and remove
601        // the task.
602        final ActivityManager am = (ActivityManager)
603                mContext.getSystemService(Context.ACTIVITY_SERVICE);
604        am.removeTask(ad.persistentTaskId, ActivityManager.REMOVE_TASK_KILL_PROCESS);
605
606        // Accessibility feedback
607        setContentDescription(
608                mContext.getString(R.string.accessibility_recents_item_dismissed, ad.getLabel()));
609        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED);
610        setContentDescription(null);
611    }
612
613    private void startApplicationDetailsActivity(String packageName) {
614        Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
615                Uri.fromParts("package", packageName, null));
616        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
617        getContext().startActivity(intent);
618    }
619
620    public void handleLongPress(
621            final View selectedView, final View anchorView, final View thumbnailView) {
622        thumbnailView.setSelected(true);
623        PopupMenu popup = new PopupMenu(mContext, anchorView == null ? selectedView : anchorView);
624        popup.getMenuInflater().inflate(R.menu.recent_popup_menu, popup.getMenu());
625        popup.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
626            public boolean onMenuItemClick(MenuItem item) {
627                if (item.getItemId() == R.id.recent_remove_item) {
628                    mRecentsContainer.removeViewInLayout(selectedView);
629                } else if (item.getItemId() == R.id.recent_inspect_item) {
630                    ViewHolder viewHolder = (ViewHolder) selectedView.getTag();
631                    if (viewHolder != null) {
632                        final TaskDescription ad = viewHolder.taskDescription;
633                        startApplicationDetailsActivity(ad.packageName);
634                        mBar.animateCollapse();
635                    } else {
636                        throw new IllegalStateException("Oops, no tag on view " + selectedView);
637                    }
638                } else {
639                    return false;
640                }
641                return true;
642            }
643        });
644        popup.setOnDismissListener(new PopupMenu.OnDismissListener() {
645            public void onDismiss(PopupMenu menu) {
646                thumbnailView.setSelected(false);
647            }
648        });
649        popup.show();
650    }
651}
652