RecentsPanelView.java revision 328310c6fac6066d338926bb43d359862cae36d2
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 return pointInside(x, y, mRecentsContainer) || pointInside(x, y, mStatusBarTouchProxy); 185 } 186 187 public void show(boolean show, boolean animate) { 188 show(show, animate, null); 189 } 190 191 public void show(boolean show, boolean animate, 192 ArrayList<TaskDescription> recentTaskDescriptions) { 193 if (show) { 194 // Need to update list of recent apps before we set visibility so this view's 195 // content description is updated before it gets focus for TalkBack mode 196 refreshRecentTasksList(recentTaskDescriptions); 197 198 // if there are no apps, either bring up a "No recent apps" message, or just 199 // quit early 200 boolean noApps = (mRecentTaskDescriptions.size() == 0); 201 if (mRecentsNoApps != null) { 202 mRecentsNoApps.setVisibility(noApps ? View.VISIBLE : View.INVISIBLE); 203 } else { 204 if (noApps) { 205 if (DEBUG) Log.v(TAG, "Nothing to show"); 206 // Need to set recent tasks to dirty so that next time we load, we 207 // refresh the list of tasks 208 mRecentTasksLoader.cancelLoadingThumbnails(); 209 mRecentTasksDirty = true; 210 return; 211 } 212 } 213 } else { 214 // Need to set recent tasks to dirty so that next time we load, we 215 // refresh the list of tasks 216 mRecentTasksLoader.cancelLoadingThumbnails(); 217 mRecentTasksDirty = true; 218 } 219 if (animate) { 220 if (mShowing != show) { 221 mShowing = show; 222 if (show) { 223 setVisibility(View.VISIBLE); 224 } 225 mChoreo.startAnimation(show); 226 } 227 } else { 228 mShowing = show; 229 setVisibility(show ? View.VISIBLE : View.GONE); 230 mChoreo.jumpTo(show); 231 onAnimationEnd(null); 232 } 233 if (show) { 234 setFocusable(true); 235 setFocusableInTouchMode(true); 236 requestFocus(); 237 } 238 } 239 240 public void dismiss() { 241 hide(true); 242 } 243 244 public void hide(boolean animate) { 245 if (!animate) { 246 setVisibility(View.GONE); 247 } 248 if (mBar != null) { 249 mBar.animateCollapse(); 250 } 251 } 252 253 public void handleShowBackground(boolean show) { 254 if (show) { 255 mRecentsScrim.setBackgroundResource(R.drawable.status_bar_recents_background_solid); 256 } else { 257 mRecentsScrim.setBackgroundDrawable(null); 258 } 259 } 260 261 public boolean isRecentsVisible() { 262 return getVisibility() == VISIBLE; 263 } 264 265 public void onAnimationCancel(Animator animation) { 266 } 267 268 public void onAnimationEnd(Animator animation) { 269 if (mShowing) { 270 final LayoutTransition transitioner = new LayoutTransition(); 271 ((ViewGroup)mRecentsContainer).setLayoutTransition(transitioner); 272 createCustomAnimations(transitioner); 273 } else { 274 ((ViewGroup)mRecentsContainer).setLayoutTransition(null); 275 clearRecentTasksList(); 276 } 277 } 278 279 public void onAnimationRepeat(Animator animation) { 280 } 281 282 public void onAnimationStart(Animator animation) { 283 } 284 285 /** 286 * We need to be aligned at the bottom. LinearLayout can't do this, so instead, 287 * let LinearLayout do all the hard work, and then shift everything down to the bottom. 288 */ 289 @Override 290 protected void onLayout(boolean changed, int l, int t, int r, int b) { 291 super.onLayout(changed, l, t, r, b); 292 mChoreo.setPanelHeight(mRecentsContainer.getHeight()); 293 } 294 295 @Override 296 public boolean dispatchHoverEvent(MotionEvent event) { 297 // Ignore hover events outside of this panel bounds since such events 298 // generate spurious accessibility events with the panel content when 299 // tapping outside of it, thus confusing the user. 300 final int x = (int) event.getX(); 301 final int y = (int) event.getY(); 302 if (x >= 0 && x < getWidth() && y >= 0 && y < getHeight()) { 303 return super.dispatchHoverEvent(event); 304 } 305 return true; 306 } 307 308 /** 309 * Whether the panel is showing, or, if it's animating, whether it will be 310 * when the animation is done. 311 */ 312 public boolean isShowing() { 313 return mShowing; 314 } 315 316 public void setBar(StatusBar bar) { 317 mBar = bar; 318 319 } 320 321 public void setStatusBarView(View statusBarView) { 322 if (mStatusBarTouchProxy != null) { 323 mStatusBarTouchProxy.setStatusBar(statusBarView); 324 } 325 } 326 327 public void setRecentTasksLoader(RecentTasksLoader loader) { 328 mRecentTasksLoader = loader; 329 } 330 331 public void setOnVisibilityChangedListener(OnRecentsPanelVisibilityChangedListener l) { 332 mVisibilityChangedListener = l; 333 334 } 335 336 public void setVisibility(int visibility) { 337 if (mVisibilityChangedListener != null) { 338 mVisibilityChangedListener.onRecentsPanelVisibilityChanged(visibility == VISIBLE); 339 } 340 super.setVisibility(visibility); 341 } 342 343 public RecentsPanelView(Context context, AttributeSet attrs) { 344 this(context, attrs, 0); 345 } 346 347 public RecentsPanelView(Context context, AttributeSet attrs, int defStyle) { 348 super(context, attrs, defStyle); 349 mContext = context; 350 updateValuesFromResources(); 351 } 352 353 public void updateValuesFromResources() { 354 final Resources res = mContext.getResources(); 355 mThumbnailWidth = Math.round(res.getDimension(R.dimen.status_bar_recents_thumbnail_width)); 356 mFitThumbnailToXY = res.getBoolean(R.bool.config_recents_thumbnail_image_fits_to_xy); 357 } 358 359 @Override 360 protected void onFinishInflate() { 361 super.onFinishInflate(); 362 mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); 363 mRecentsContainer = (ViewGroup) findViewById(R.id.recents_container); 364 mStatusBarTouchProxy = (StatusBarTouchProxy) findViewById(R.id.status_bar_touch_proxy); 365 mListAdapter = new TaskDescriptionAdapter(mContext); 366 if (mRecentsContainer instanceof RecentsHorizontalScrollView){ 367 RecentsHorizontalScrollView scrollView 368 = (RecentsHorizontalScrollView) mRecentsContainer; 369 scrollView.setAdapter(mListAdapter); 370 scrollView.setCallback(this); 371 } else if (mRecentsContainer instanceof RecentsVerticalScrollView){ 372 RecentsVerticalScrollView scrollView 373 = (RecentsVerticalScrollView) mRecentsContainer; 374 scrollView.setAdapter(mListAdapter); 375 scrollView.setCallback(this); 376 } 377 else { 378 throw new IllegalArgumentException("missing Recents[Horizontal]ScrollView"); 379 } 380 381 382 mRecentsScrim = findViewById(R.id.recents_bg_protect); 383 mRecentsNoApps = findViewById(R.id.recents_no_apps); 384 mChoreo = new Choreographer(this, mRecentsScrim, mRecentsContainer, mRecentsNoApps, this); 385 386 // In order to save space, we make the background texture repeat in the Y direction 387 if (mRecentsScrim != null && mRecentsScrim.getBackground() instanceof BitmapDrawable) { 388 ((BitmapDrawable) mRecentsScrim.getBackground()).setTileModeY(TileMode.REPEAT); 389 } 390 391 mPreloadTasksRunnable = new Runnable() { 392 public void run() { 393 setVisibility(INVISIBLE); 394 refreshRecentTasksList(); 395 } 396 }; 397 } 398 399 private void createCustomAnimations(LayoutTransition transitioner) { 400 transitioner.setDuration(200); 401 transitioner.setStartDelay(LayoutTransition.CHANGE_DISAPPEARING, 0); 402 transitioner.setAnimator(LayoutTransition.DISAPPEARING, null); 403 } 404 405 @Override 406 protected void onVisibilityChanged(View changedView, int visibility) { 407 super.onVisibilityChanged(changedView, visibility); 408 if (DEBUG) Log.v(TAG, "onVisibilityChanged(" + changedView + ", " + visibility + ")"); 409 410 if (mRecentsContainer instanceof RecentsHorizontalScrollView) { 411 ((RecentsHorizontalScrollView) mRecentsContainer).onRecentsVisibilityChanged(); 412 } else if (mRecentsContainer instanceof RecentsVerticalScrollView) { 413 ((RecentsVerticalScrollView) mRecentsContainer).onRecentsVisibilityChanged(); 414 } else { 415 throw new IllegalArgumentException("missing Recents[Horizontal]ScrollView"); 416 } 417 } 418 419 private void updateThumbnail(ViewHolder h, Bitmap thumbnail, boolean show, boolean anim) { 420 if (thumbnail != null) { 421 // Should remove the default image in the frame 422 // that this now covers, to improve scrolling speed. 423 // That can't be done until the anim is complete though. 424 h.thumbnailViewImage.setImageBitmap(thumbnail); 425 426 // scale the image to fill the full width of the ImageView. do this only if 427 // we haven't set a bitmap before, or if the bitmap size has changed 428 if (h.thumbnailViewImageBitmap == null || 429 h.thumbnailViewImageBitmap.getWidth() != thumbnail.getWidth() || 430 h.thumbnailViewImageBitmap.getHeight() != thumbnail.getHeight()) { 431 if (mFitThumbnailToXY) { 432 h.thumbnailViewImage.setScaleType(ScaleType.FIT_XY); 433 } else { 434 Matrix scaleMatrix = new Matrix(); 435 float scale = mThumbnailWidth / (float) thumbnail.getWidth(); 436 scaleMatrix.setScale(scale, scale); 437 h.thumbnailViewImage.setScaleType(ScaleType.MATRIX); 438 h.thumbnailViewImage.setImageMatrix(scaleMatrix); 439 } 440 } 441 if (show && h.thumbnailView.getVisibility() != View.VISIBLE) { 442 if (anim) { 443 h.thumbnailView.setAnimation( 444 AnimationUtils.loadAnimation(mContext, R.anim.recent_appear)); 445 } 446 h.thumbnailView.setVisibility(View.VISIBLE); 447 } 448 h.thumbnailViewImageBitmap = thumbnail; 449 } 450 } 451 452 void onTaskThumbnailLoaded(TaskDescription ad) { 453 synchronized (ad) { 454 if (mRecentsContainer != null) { 455 ViewGroup container = mRecentsContainer; 456 if (container instanceof HorizontalScrollView 457 || container instanceof ScrollView) { 458 container = (ViewGroup)container.findViewById( 459 R.id.recents_linear_layout); 460 } 461 // Look for a view showing this thumbnail, to update. 462 for (int i=0; i<container.getChildCount(); i++) { 463 View v = container.getChildAt(i); 464 if (v.getTag() instanceof ViewHolder) { 465 ViewHolder h = (ViewHolder)v.getTag(); 466 if (h.taskDescription == ad) { 467 // only fade in the thumbnail if recents is already visible-- we 468 // show it immediately otherwise 469 boolean animateShow = mShowing && 470 mRecentsContainer.getAlpha() > ViewConfiguration.ALPHA_THRESHOLD; 471 updateThumbnail(h, ad.getThumbnail(), true, animateShow); 472 } 473 } 474 } 475 } 476 } 477 } 478 479 // additional optimization when we have sofware system buttons - start loading the recent 480 // tasks on touch down 481 @Override 482 public boolean onTouch(View v, MotionEvent ev) { 483 if (!mShowing) { 484 int action = ev.getAction() & MotionEvent.ACTION_MASK; 485 if (action == MotionEvent.ACTION_DOWN) { 486 // If we set our visibility to INVISIBLE here, we avoid an extra call to 487 // onLayout later when we become visible (because onLayout is always called 488 // when going from GONE) 489 post(mPreloadTasksRunnable); 490 } else if (action == MotionEvent.ACTION_CANCEL) { 491 setVisibility(GONE); 492 clearRecentTasksList(); 493 // Remove the preloader if we haven't called it yet 494 removeCallbacks(mPreloadTasksRunnable); 495 } else if (action == MotionEvent.ACTION_UP) { 496 // Remove the preloader if we haven't called it yet 497 removeCallbacks(mPreloadTasksRunnable); 498 if (!v.isPressed()) { 499 setVisibility(GONE); 500 clearRecentTasksList(); 501 } 502 } 503 } 504 return false; 505 } 506 507 public void clearRecentTasksList() { 508 // Clear memory used by screenshots 509 if (mRecentTaskDescriptions != null) { 510 mRecentTasksLoader.cancelLoadingThumbnails(); 511 mRecentTaskDescriptions.clear(); 512 mListAdapter.notifyDataSetInvalidated(); 513 mRecentTasksDirty = true; 514 } 515 } 516 517 public void refreshRecentTasksList() { 518 refreshRecentTasksList(null); 519 } 520 521 private void refreshRecentTasksList(ArrayList<TaskDescription> recentTasksList) { 522 if (mRecentTasksDirty) { 523 if (recentTasksList != null) { 524 mRecentTaskDescriptions = recentTasksList; 525 } else { 526 mRecentTaskDescriptions = mRecentTasksLoader.getRecentTasks(); 527 } 528 mListAdapter.notifyDataSetInvalidated(); 529 updateUiElements(getResources().getConfiguration()); 530 mRecentTasksDirty = false; 531 } 532 } 533 534 public ArrayList<TaskDescription> getRecentTasksList() { 535 return mRecentTaskDescriptions; 536 } 537 538 private void updateUiElements(Configuration config) { 539 final int items = mRecentTaskDescriptions.size(); 540 541 mRecentsContainer.setVisibility(items > 0 ? View.VISIBLE : View.GONE); 542 543 // Set description for accessibility 544 int numRecentApps = mRecentTaskDescriptions.size(); 545 String recentAppsAccessibilityDescription; 546 if (numRecentApps == 0) { 547 recentAppsAccessibilityDescription = 548 getResources().getString(R.string.status_bar_no_recent_apps); 549 } else { 550 recentAppsAccessibilityDescription = getResources().getQuantityString( 551 R.plurals.status_bar_accessibility_recent_apps, numRecentApps, numRecentApps); 552 } 553 setContentDescription(recentAppsAccessibilityDescription); 554 } 555 556 public void handleOnClick(View view) { 557 TaskDescription ad = ((ViewHolder) view.getTag()).taskDescription; 558 final Context context = view.getContext(); 559 final ActivityManager am = (ActivityManager) 560 context.getSystemService(Context.ACTIVITY_SERVICE); 561 if (ad.taskId >= 0) { 562 // This is an active task; it should just go to the foreground. 563 am.moveTaskToFront(ad.taskId, ActivityManager.MOVE_TASK_WITH_HOME); 564 } else { 565 Intent intent = ad.intent; 566 intent.addFlags(Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY 567 | Intent.FLAG_ACTIVITY_TASK_ON_HOME 568 | Intent.FLAG_ACTIVITY_NEW_TASK); 569 if (DEBUG) Log.v(TAG, "Starting activity " + intent); 570 context.startActivity(intent); 571 } 572 hide(true); 573 } 574 575 public void onItemClick(AdapterView<?> parent, View view, int position, long id) { 576 handleOnClick(view); 577 } 578 579 public void handleSwipe(View view) { 580 TaskDescription ad = ((ViewHolder) view.getTag()).taskDescription; 581 if (DEBUG) Log.v(TAG, "Jettison " + ad.getLabel()); 582 mRecentTaskDescriptions.remove(ad); 583 584 // Handled by widget containers to enable LayoutTransitions properly 585 // mListAdapter.notifyDataSetChanged(); 586 587 if (mRecentTaskDescriptions.size() == 0) { 588 hide(false); 589 } 590 591 // Currently, either direction means the same thing, so ignore direction and remove 592 // the task. 593 final ActivityManager am = (ActivityManager) 594 mContext.getSystemService(Context.ACTIVITY_SERVICE); 595 am.removeTask(ad.persistentTaskId, ActivityManager.REMOVE_TASK_KILL_PROCESS); 596 597 // Accessibility feedback 598 setContentDescription( 599 mContext.getString(R.string.accessibility_recents_item_dismissed, ad.getLabel())); 600 sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED); 601 setContentDescription(null); 602 } 603 604 private void startApplicationDetailsActivity(String packageName) { 605 Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, 606 Uri.fromParts("package", packageName, null)); 607 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 608 getContext().startActivity(intent); 609 } 610 611 public void handleLongPress( 612 final View selectedView, final View anchorView, final View thumbnailView) { 613 thumbnailView.setSelected(true); 614 PopupMenu popup = new PopupMenu(mContext, anchorView == null ? selectedView : anchorView); 615 popup.getMenuInflater().inflate(R.menu.recent_popup_menu, popup.getMenu()); 616 popup.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() { 617 public boolean onMenuItemClick(MenuItem item) { 618 if (item.getItemId() == R.id.recent_remove_item) { 619 mRecentsContainer.removeViewInLayout(selectedView); 620 } else if (item.getItemId() == R.id.recent_inspect_item) { 621 ViewHolder viewHolder = (ViewHolder) selectedView.getTag(); 622 if (viewHolder != null) { 623 final TaskDescription ad = viewHolder.taskDescription; 624 startApplicationDetailsActivity(ad.packageName); 625 mBar.animateCollapse(); 626 } else { 627 throw new IllegalStateException("Oops, no tag on view " + selectedView); 628 } 629 } else { 630 return false; 631 } 632 return true; 633 } 634 }); 635 popup.setOnDismissListener(new PopupMenu.OnDismissListener() { 636 public void onDismiss(PopupMenu menu) { 637 thumbnailView.setSelected(false); 638 } 639 }); 640 popup.show(); 641 } 642} 643