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 17 package com.android.systemui.recent; 18 19 import android.animation.Animator; 20 import android.animation.LayoutTransition; 21 import android.app.ActivityManager; 22 import android.app.ActivityManagerNative; 23 import android.app.ActivityOptions; 24 import android.content.Context; 25 import android.content.Intent; 26 import android.content.res.Configuration; 27 import android.content.res.Resources; 28 import android.content.res.TypedArray; 29 import android.graphics.Bitmap; 30 import android.graphics.Matrix; 31 import android.graphics.Rect; 32 import android.graphics.Shader.TileMode; 33 import android.graphics.drawable.BitmapDrawable; 34 import android.graphics.drawable.Drawable; 35 import android.net.Uri; 36 import android.os.RemoteException; 37 import android.os.ServiceManager; 38 import android.provider.Settings; 39 import android.util.AttributeSet; 40 import android.util.Log; 41 import android.view.Display; 42 import android.view.KeyEvent; 43 import android.view.IWindowManager; 44 import android.view.LayoutInflater; 45 import android.view.MenuItem; 46 import android.view.MotionEvent; 47 import android.view.View; 48 import android.view.ViewGroup; 49 import android.view.WindowManager; 50 import android.view.accessibility.AccessibilityEvent; 51 import android.view.animation.AnimationUtils; 52 import android.widget.AdapterView; 53 import android.widget.AdapterView.OnItemClickListener; 54 import android.widget.BaseAdapter; 55 import android.widget.FrameLayout; 56 import android.widget.ImageView; 57 import android.widget.ImageView.ScaleType; 58 import android.widget.PopupMenu; 59 import android.widget.TextView; 60 61 import com.android.systemui.R; 62 import com.android.systemui.statusbar.BaseStatusBar; 63 import com.android.systemui.statusbar.CommandQueue; 64 import com.android.systemui.statusbar.phone.PhoneStatusBar; 65 import com.android.systemui.statusbar.tablet.StatusBarPanel; 66 import com.android.systemui.statusbar.tablet.TabletStatusBar; 67 68 import java.util.ArrayList; 69 70 public class RecentsPanelView extends FrameLayout implements OnItemClickListener, RecentsCallback, 71 StatusBarPanel, Animator.AnimatorListener, View.OnTouchListener { 72 static final String TAG = "RecentsPanelView"; 73 static final boolean DEBUG = TabletStatusBar.DEBUG || PhoneStatusBar.DEBUG || false; 74 private Context mContext; 75 private BaseStatusBar mBar; 76 private PopupMenu mPopup; 77 private View mRecentsScrim; 78 private View mRecentsNoApps; 79 private ViewGroup mRecentsContainer; 80 private StatusBarTouchProxy mStatusBarTouchProxy; 81 82 private boolean mShowing; 83 private boolean mWaitingToShow; 84 private boolean mWaitingToShowAnimated; 85 private boolean mReadyToShow; 86 private int mNumItemsWaitingForThumbnailsAndIcons; 87 private Choreographer mChoreo; 88 OnRecentsPanelVisibilityChangedListener mVisibilityChangedListener; 89 90 ImageView mPlaceholderThumbnail; 91 View mTransitionBg; 92 boolean mHideRecentsAfterThumbnailScaleUpStarted; 93 94 private RecentTasksLoader mRecentTasksLoader; 95 private ArrayList<TaskDescription> mRecentTaskDescriptions; 96 private Runnable mPreloadTasksRunnable; 97 private boolean mRecentTasksDirty = true; 98 private TaskDescriptionAdapter mListAdapter; 99 private int mThumbnailWidth; 100 private boolean mFitThumbnailToXY; 101 private int mRecentItemLayoutId; 102 private boolean mFirstScreenful = true; 103 private boolean mHighEndGfx; 104 105 public static interface OnRecentsPanelVisibilityChangedListener { onRecentsPanelVisibilityChanged(boolean visible)106 public void onRecentsPanelVisibilityChanged(boolean visible); 107 } 108 109 public static interface RecentsScrollView { numItemsInOneScreenful()110 public int numItemsInOneScreenful(); setAdapter(TaskDescriptionAdapter adapter)111 public void setAdapter(TaskDescriptionAdapter adapter); setCallback(RecentsCallback callback)112 public void setCallback(RecentsCallback callback); setMinSwipeAlpha(float minAlpha)113 public void setMinSwipeAlpha(float minAlpha); 114 } 115 116 private final class OnLongClickDelegate implements View.OnLongClickListener { 117 View mOtherView; OnLongClickDelegate(View other)118 OnLongClickDelegate(View other) { 119 mOtherView = other; 120 } onLongClick(View v)121 public boolean onLongClick(View v) { 122 return mOtherView.performLongClick(); 123 } 124 } 125 126 /* package */ final static class ViewHolder { 127 View thumbnailView; 128 ImageView thumbnailViewImage; 129 Bitmap thumbnailViewImageBitmap; 130 ImageView iconView; 131 TextView labelView; 132 TextView descriptionView; 133 TaskDescription taskDescription; 134 boolean loadedThumbnailAndIcon; 135 } 136 137 /* package */ final class TaskDescriptionAdapter extends BaseAdapter { 138 private LayoutInflater mInflater; 139 TaskDescriptionAdapter(Context context)140 public TaskDescriptionAdapter(Context context) { 141 mInflater = LayoutInflater.from(context); 142 } 143 getCount()144 public int getCount() { 145 return mRecentTaskDescriptions != null ? mRecentTaskDescriptions.size() : 0; 146 } 147 getItem(int position)148 public Object getItem(int position) { 149 return position; // we only need the index 150 } 151 getItemId(int position)152 public long getItemId(int position) { 153 return position; // we just need something unique for this position 154 } 155 createView(ViewGroup parent)156 public View createView(ViewGroup parent) { 157 View convertView = mInflater.inflate(mRecentItemLayoutId, parent, false); 158 ViewHolder holder = new ViewHolder(); 159 holder.thumbnailView = convertView.findViewById(R.id.app_thumbnail); 160 holder.thumbnailViewImage = 161 (ImageView) convertView.findViewById(R.id.app_thumbnail_image); 162 // If we set the default thumbnail now, we avoid an onLayout when we update 163 // the thumbnail later (if they both have the same dimensions) 164 if (mRecentTasksLoader != null) { 165 updateThumbnail(holder, mRecentTasksLoader.getDefaultThumbnail(), false, false); 166 } 167 holder.iconView = (ImageView) convertView.findViewById(R.id.app_icon); 168 if (mRecentTasksLoader != null) { 169 holder.iconView.setImageBitmap(mRecentTasksLoader.getDefaultIcon()); 170 } 171 holder.labelView = (TextView) convertView.findViewById(R.id.app_label); 172 holder.descriptionView = (TextView) convertView.findViewById(R.id.app_description); 173 174 convertView.setTag(holder); 175 return convertView; 176 } 177 getView(int position, View convertView, ViewGroup parent)178 public View getView(int position, View convertView, ViewGroup parent) { 179 if (convertView == null) { 180 convertView = createView(parent); 181 if (convertView.getParent() != null) { 182 throw new RuntimeException("Recycled child has parent"); 183 } 184 } else { 185 if (convertView.getParent() != null) { 186 throw new RuntimeException("Recycled child has parent"); 187 } 188 } 189 ViewHolder holder = (ViewHolder) convertView.getTag(); 190 191 // index is reverse since most recent appears at the bottom... 192 final int index = mRecentTaskDescriptions.size() - position - 1; 193 194 final TaskDescription td = mRecentTaskDescriptions.get(index); 195 196 holder.labelView.setText(td.getLabel()); 197 holder.thumbnailView.setContentDescription(td.getLabel()); 198 holder.loadedThumbnailAndIcon = td.isLoaded(); 199 if (td.isLoaded()) { 200 updateThumbnail(holder, td.getThumbnail(), true, false); 201 updateIcon(holder, td.getIcon(), true, false); 202 mNumItemsWaitingForThumbnailsAndIcons--; 203 } 204 205 holder.thumbnailView.setTag(td); 206 holder.thumbnailView.setOnLongClickListener(new OnLongClickDelegate(convertView)); 207 holder.taskDescription = td; 208 return convertView; 209 } 210 recycleView(View v)211 public void recycleView(View v) { 212 ViewHolder holder = (ViewHolder) v.getTag(); 213 updateThumbnail(holder, mRecentTasksLoader.getDefaultThumbnail(), false, false); 214 holder.iconView.setImageBitmap(mRecentTasksLoader.getDefaultIcon()); 215 holder.iconView.setVisibility(INVISIBLE); 216 holder.labelView.setText(null); 217 holder.thumbnailView.setContentDescription(null); 218 holder.thumbnailView.setTag(null); 219 holder.thumbnailView.setOnLongClickListener(null); 220 holder.thumbnailView.setVisibility(INVISIBLE); 221 holder.taskDescription = null; 222 holder.loadedThumbnailAndIcon = false; 223 } 224 } 225 RecentsPanelView(Context context, AttributeSet attrs)226 public RecentsPanelView(Context context, AttributeSet attrs) { 227 this(context, attrs, 0); 228 } 229 RecentsPanelView(Context context, AttributeSet attrs, int defStyle)230 public RecentsPanelView(Context context, AttributeSet attrs, int defStyle) { 231 super(context, attrs, defStyle); 232 mContext = context; 233 updateValuesFromResources(); 234 235 TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.RecentsPanelView, 236 defStyle, 0); 237 238 mRecentItemLayoutId = a.getResourceId(R.styleable.RecentsPanelView_recentItemLayout, 0); 239 a.recycle(); 240 } 241 numItemsInOneScreenful()242 public int numItemsInOneScreenful() { 243 if (mRecentsContainer instanceof RecentsScrollView){ 244 RecentsScrollView scrollView 245 = (RecentsScrollView) mRecentsContainer; 246 return scrollView.numItemsInOneScreenful(); 247 } else { 248 throw new IllegalArgumentException("missing Recents[Horizontal]ScrollView"); 249 } 250 } 251 252 @Override onKeyUp(int keyCode, KeyEvent event)253 public boolean onKeyUp(int keyCode, KeyEvent event) { 254 if (keyCode == KeyEvent.KEYCODE_BACK && !event.isCanceled()) { 255 show(false, false); 256 return true; 257 } 258 return super.onKeyUp(keyCode, event); 259 } 260 pointInside(int x, int y, View v)261 private boolean pointInside(int x, int y, View v) { 262 final int l = v.getLeft(); 263 final int r = v.getRight(); 264 final int t = v.getTop(); 265 final int b = v.getBottom(); 266 return x >= l && x < r && y >= t && y < b; 267 } 268 isInContentArea(int x, int y)269 public boolean isInContentArea(int x, int y) { 270 if (pointInside(x, y, mRecentsContainer)) { 271 return true; 272 } else if (mStatusBarTouchProxy != null && 273 pointInside(x, y, mStatusBarTouchProxy)) { 274 return true; 275 } else { 276 return false; 277 } 278 } 279 show(boolean show, boolean animate)280 public void show(boolean show, boolean animate) { 281 if (show) { 282 refreshRecentTasksList(null, true); 283 mWaitingToShow = true; 284 mWaitingToShowAnimated = animate; 285 showIfReady(); 286 } else { 287 show(show, animate, null, false); 288 } 289 } 290 showIfReady()291 private void showIfReady() { 292 // mWaitingToShow = there was a touch up on the recents button 293 // mReadyToShow = we've created views for the first screenful of items 294 if (mWaitingToShow && mReadyToShow) { // && mNumItemsWaitingForThumbnailsAndIcons <= 0 295 show(true, mWaitingToShowAnimated, null, false); 296 } 297 } 298 sendCloseSystemWindows(Context context, String reason)299 static void sendCloseSystemWindows(Context context, String reason) { 300 if (ActivityManagerNative.isSystemReady()) { 301 try { 302 ActivityManagerNative.getDefault().closeSystemDialogs(reason); 303 } catch (RemoteException e) { 304 } 305 } 306 } 307 show(boolean show, boolean animate, ArrayList<TaskDescription> recentTaskDescriptions, boolean firstScreenful)308 public void show(boolean show, boolean animate, 309 ArrayList<TaskDescription> recentTaskDescriptions, boolean firstScreenful) { 310 sendCloseSystemWindows(mContext, BaseStatusBar.SYSTEM_DIALOG_REASON_RECENT_APPS); 311 312 if (show) { 313 // Need to update list of recent apps before we set visibility so this view's 314 // content description is updated before it gets focus for TalkBack mode 315 refreshRecentTasksList(recentTaskDescriptions, firstScreenful); 316 317 // if there are no apps, either bring up a "No recent apps" message, or just 318 // quit early 319 boolean noApps = !mFirstScreenful && (mRecentTaskDescriptions.size() == 0); 320 if (mRecentsNoApps != null) { 321 mRecentsNoApps.setAlpha(1f); 322 mRecentsNoApps.setVisibility(noApps ? View.VISIBLE : View.INVISIBLE); 323 } else { 324 if (noApps) { 325 if (DEBUG) Log.v(TAG, "Nothing to show"); 326 // Need to set recent tasks to dirty so that next time we load, we 327 // refresh the list of tasks 328 mRecentTasksLoader.cancelLoadingThumbnailsAndIcons(); 329 mRecentTasksDirty = true; 330 331 mWaitingToShow = false; 332 mReadyToShow = false; 333 return; 334 } 335 } 336 } else { 337 // Need to set recent tasks to dirty so that next time we load, we 338 // refresh the list of tasks 339 mRecentTasksLoader.cancelLoadingThumbnailsAndIcons(); 340 mRecentTasksDirty = true; 341 mWaitingToShow = false; 342 mReadyToShow = false; 343 } 344 if (animate) { 345 if (mShowing != show) { 346 mShowing = show; 347 if (show) { 348 setVisibility(View.VISIBLE); 349 } 350 mChoreo.startAnimation(show); 351 } 352 } else { 353 mShowing = show; 354 setVisibility(show ? View.VISIBLE : View.GONE); 355 mChoreo.jumpTo(show); 356 onAnimationEnd(null); 357 } 358 if (show) { 359 setFocusable(true); 360 setFocusableInTouchMode(true); 361 requestFocus(); 362 } else { 363 if (mPopup != null) { 364 mPopup.dismiss(); 365 } 366 } 367 } 368 dismiss()369 public void dismiss() { 370 hide(true); 371 } 372 hide(boolean animate)373 public void hide(boolean animate) { 374 if (!animate) { 375 setVisibility(View.GONE); 376 } 377 if (mBar != null) { 378 // This will indirectly cause show(false, ...) to get called 379 mBar.animateCollapse(CommandQueue.FLAG_EXCLUDE_NONE); 380 } 381 } 382 onAnimationCancel(Animator animation)383 public void onAnimationCancel(Animator animation) { 384 } 385 onAnimationEnd(Animator animation)386 public void onAnimationEnd(Animator animation) { 387 if (mShowing) { 388 final LayoutTransition transitioner = new LayoutTransition(); 389 ((ViewGroup)mRecentsContainer).setLayoutTransition(transitioner); 390 createCustomAnimations(transitioner); 391 } else { 392 ((ViewGroup)mRecentsContainer).setLayoutTransition(null); 393 clearRecentTasksList(); 394 } 395 } 396 onAnimationRepeat(Animator animation)397 public void onAnimationRepeat(Animator animation) { 398 } 399 onAnimationStart(Animator animation)400 public void onAnimationStart(Animator animation) { 401 } 402 403 /** 404 * We need to be aligned at the bottom. LinearLayout can't do this, so instead, 405 * let LinearLayout do all the hard work, and then shift everything down to the bottom. 406 */ 407 @Override onLayout(boolean changed, int l, int t, int r, int b)408 protected void onLayout(boolean changed, int l, int t, int r, int b) { 409 super.onLayout(changed, l, t, r, b); 410 mChoreo.setPanelHeight(mRecentsContainer.getHeight()); 411 } 412 413 @Override dispatchHoverEvent(MotionEvent event)414 public boolean dispatchHoverEvent(MotionEvent event) { 415 // Ignore hover events outside of this panel bounds since such events 416 // generate spurious accessibility events with the panel content when 417 // tapping outside of it, thus confusing the user. 418 final int x = (int) event.getX(); 419 final int y = (int) event.getY(); 420 if (x >= 0 && x < getWidth() && y >= 0 && y < getHeight()) { 421 return super.dispatchHoverEvent(event); 422 } 423 return true; 424 } 425 426 /** 427 * Whether the panel is showing, or, if it's animating, whether it will be 428 * when the animation is done. 429 */ isShowing()430 public boolean isShowing() { 431 return mShowing; 432 } 433 setBar(BaseStatusBar bar)434 public void setBar(BaseStatusBar bar) { 435 mBar = bar; 436 437 } 438 setStatusBarView(View statusBarView)439 public void setStatusBarView(View statusBarView) { 440 if (mStatusBarTouchProxy != null) { 441 mStatusBarTouchProxy.setStatusBar(statusBarView); 442 } 443 } 444 setRecentTasksLoader(RecentTasksLoader loader)445 public void setRecentTasksLoader(RecentTasksLoader loader) { 446 mRecentTasksLoader = loader; 447 } 448 setOnVisibilityChangedListener(OnRecentsPanelVisibilityChangedListener l)449 public void setOnVisibilityChangedListener(OnRecentsPanelVisibilityChangedListener l) { 450 mVisibilityChangedListener = l; 451 452 } 453 setVisibility(int visibility)454 public void setVisibility(int visibility) { 455 if (mVisibilityChangedListener != null) { 456 mVisibilityChangedListener.onRecentsPanelVisibilityChanged(visibility == VISIBLE); 457 } 458 super.setVisibility(visibility); 459 } 460 updateValuesFromResources()461 public void updateValuesFromResources() { 462 final Resources res = mContext.getResources(); 463 mThumbnailWidth = Math.round(res.getDimension(R.dimen.status_bar_recents_thumbnail_width)); 464 mFitThumbnailToXY = res.getBoolean(R.bool.config_recents_thumbnail_image_fits_to_xy); 465 } 466 467 @Override onFinishInflate()468 protected void onFinishInflate() { 469 super.onFinishInflate(); 470 471 mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); 472 mRecentsContainer = (ViewGroup) findViewById(R.id.recents_container); 473 mStatusBarTouchProxy = (StatusBarTouchProxy) findViewById(R.id.status_bar_touch_proxy); 474 mListAdapter = new TaskDescriptionAdapter(mContext); 475 if (mRecentsContainer instanceof RecentsScrollView){ 476 RecentsScrollView scrollView 477 = (RecentsScrollView) mRecentsContainer; 478 scrollView.setAdapter(mListAdapter); 479 scrollView.setCallback(this); 480 } else { 481 throw new IllegalArgumentException("missing Recents[Horizontal]ScrollView"); 482 } 483 484 mRecentsScrim = findViewById(R.id.recents_bg_protect); 485 mRecentsNoApps = findViewById(R.id.recents_no_apps); 486 mChoreo = new Choreographer(this, mRecentsScrim, mRecentsContainer, mRecentsNoApps, this); 487 488 if (mRecentsScrim != null) { 489 Display d = ((WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE)) 490 .getDefaultDisplay(); 491 mHighEndGfx = ActivityManager.isHighEndGfx(d); 492 if (!mHighEndGfx) { 493 mRecentsScrim.setBackground(null); 494 } else if (mRecentsScrim.getBackground() instanceof BitmapDrawable) { 495 // In order to save space, we make the background texture repeat in the Y direction 496 ((BitmapDrawable) mRecentsScrim.getBackground()).setTileModeY(TileMode.REPEAT); 497 } 498 } 499 500 mPreloadTasksRunnable = new Runnable() { 501 public void run() { 502 // If we set our visibility to INVISIBLE here, we avoid an extra call to 503 // onLayout later when we become visible (because onLayout is always called 504 // when going from GONE) 505 if (!mShowing) { 506 setVisibility(INVISIBLE); 507 refreshRecentTasksList(); 508 } 509 } 510 }; 511 } 512 setMinSwipeAlpha(float minAlpha)513 public void setMinSwipeAlpha(float minAlpha) { 514 if (mRecentsContainer instanceof RecentsScrollView){ 515 RecentsScrollView scrollView 516 = (RecentsScrollView) mRecentsContainer; 517 scrollView.setMinSwipeAlpha(minAlpha); 518 } 519 } 520 createCustomAnimations(LayoutTransition transitioner)521 private void createCustomAnimations(LayoutTransition transitioner) { 522 transitioner.setDuration(200); 523 transitioner.setStartDelay(LayoutTransition.CHANGE_DISAPPEARING, 0); 524 transitioner.setAnimator(LayoutTransition.DISAPPEARING, null); 525 } 526 updateIcon(ViewHolder h, Drawable icon, boolean show, boolean anim)527 private void updateIcon(ViewHolder h, Drawable icon, boolean show, boolean anim) { 528 if (icon != null) { 529 h.iconView.setImageDrawable(icon); 530 if (show && h.iconView.getVisibility() != View.VISIBLE) { 531 if (anim) { 532 h.iconView.setAnimation( 533 AnimationUtils.loadAnimation(mContext, R.anim.recent_appear)); 534 } 535 h.iconView.setVisibility(View.VISIBLE); 536 } 537 } 538 } 539 updateThumbnail(ViewHolder h, Bitmap thumbnail, boolean show, boolean anim)540 private void updateThumbnail(ViewHolder h, Bitmap thumbnail, boolean show, boolean anim) { 541 if (thumbnail != null) { 542 // Should remove the default image in the frame 543 // that this now covers, to improve scrolling speed. 544 // That can't be done until the anim is complete though. 545 h.thumbnailViewImage.setImageBitmap(thumbnail); 546 547 // scale the image to fill the full width of the ImageView. do this only if 548 // we haven't set a bitmap before, or if the bitmap size has changed 549 if (h.thumbnailViewImageBitmap == null || 550 h.thumbnailViewImageBitmap.getWidth() != thumbnail.getWidth() || 551 h.thumbnailViewImageBitmap.getHeight() != thumbnail.getHeight()) { 552 if (mFitThumbnailToXY) { 553 h.thumbnailViewImage.setScaleType(ScaleType.FIT_XY); 554 } else { 555 Matrix scaleMatrix = new Matrix(); 556 float scale = mThumbnailWidth / (float) thumbnail.getWidth(); 557 scaleMatrix.setScale(scale, scale); 558 h.thumbnailViewImage.setScaleType(ScaleType.MATRIX); 559 h.thumbnailViewImage.setImageMatrix(scaleMatrix); 560 } 561 } 562 if (show && h.thumbnailView.getVisibility() != View.VISIBLE) { 563 if (anim) { 564 h.thumbnailView.setAnimation( 565 AnimationUtils.loadAnimation(mContext, R.anim.recent_appear)); 566 } 567 h.thumbnailView.setVisibility(View.VISIBLE); 568 } 569 h.thumbnailViewImageBitmap = thumbnail; 570 } 571 } 572 onTaskThumbnailLoaded(TaskDescription td)573 void onTaskThumbnailLoaded(TaskDescription td) { 574 synchronized (td) { 575 if (mRecentsContainer != null) { 576 ViewGroup container = mRecentsContainer; 577 if (container instanceof RecentsScrollView) { 578 container = (ViewGroup) container.findViewById( 579 R.id.recents_linear_layout); 580 } 581 // Look for a view showing this thumbnail, to update. 582 for (int i=0; i < container.getChildCount(); i++) { 583 View v = container.getChildAt(i); 584 if (v.getTag() instanceof ViewHolder) { 585 ViewHolder h = (ViewHolder)v.getTag(); 586 if (!h.loadedThumbnailAndIcon && h.taskDescription == td) { 587 // only fade in the thumbnail if recents is already visible-- we 588 // show it immediately otherwise 589 //boolean animateShow = mShowing && 590 // mRecentsContainer.getAlpha() > ViewConfiguration.ALPHA_THRESHOLD; 591 boolean animateShow = false; 592 updateIcon(h, td.getIcon(), true, animateShow); 593 updateThumbnail(h, td.getThumbnail(), true, animateShow); 594 h.loadedThumbnailAndIcon = true; 595 mNumItemsWaitingForThumbnailsAndIcons--; 596 } 597 } 598 } 599 } 600 } 601 showIfReady(); 602 } 603 604 // additional optimization when we have software system buttons - start loading the recent 605 // tasks on touch down 606 @Override onTouch(View v, MotionEvent ev)607 public boolean onTouch(View v, MotionEvent ev) { 608 if (!mShowing) { 609 int action = ev.getAction() & MotionEvent.ACTION_MASK; 610 if (action == MotionEvent.ACTION_DOWN) { 611 post(mPreloadTasksRunnable); 612 } else if (action == MotionEvent.ACTION_CANCEL) { 613 setVisibility(GONE); 614 clearRecentTasksList(); 615 // Remove the preloader if we haven't called it yet 616 removeCallbacks(mPreloadTasksRunnable); 617 } else if (action == MotionEvent.ACTION_UP) { 618 // Remove the preloader if we haven't called it yet 619 removeCallbacks(mPreloadTasksRunnable); 620 if (!v.isPressed()) { 621 setVisibility(GONE); 622 clearRecentTasksList(); 623 } 624 } 625 } 626 return false; 627 } 628 preloadRecentTasksList()629 public void preloadRecentTasksList() { 630 if (!mShowing) { 631 mPreloadTasksRunnable.run(); 632 } 633 } 634 clearRecentTasksList()635 public void clearRecentTasksList() { 636 // Clear memory used by screenshots 637 if (!mShowing && mRecentTaskDescriptions != null) { 638 mRecentTasksLoader.cancelLoadingThumbnailsAndIcons(); 639 mRecentTaskDescriptions.clear(); 640 mListAdapter.notifyDataSetInvalidated(); 641 mRecentTasksDirty = true; 642 } 643 } 644 refreshRecentTasksList()645 public void refreshRecentTasksList() { 646 refreshRecentTasksList(null, false); 647 } 648 refreshRecentTasksList( ArrayList<TaskDescription> recentTasksList, boolean firstScreenful)649 private void refreshRecentTasksList( 650 ArrayList<TaskDescription> recentTasksList, boolean firstScreenful) { 651 if (mRecentTasksDirty) { 652 if (recentTasksList != null) { 653 mFirstScreenful = true; 654 onTasksLoaded(recentTasksList); 655 } else { 656 mFirstScreenful = true; 657 mRecentTasksLoader.loadTasksInBackground(); 658 } 659 mRecentTasksDirty = false; 660 } 661 } 662 onTasksLoaded(ArrayList<TaskDescription> tasks)663 public void onTasksLoaded(ArrayList<TaskDescription> tasks) { 664 if (!mFirstScreenful && tasks.size() == 0) { 665 return; 666 } 667 mNumItemsWaitingForThumbnailsAndIcons = mFirstScreenful 668 ? tasks.size() : mRecentTaskDescriptions == null 669 ? 0 : mRecentTaskDescriptions.size(); 670 if (mRecentTaskDescriptions == null) { 671 mRecentTaskDescriptions = new ArrayList<TaskDescription>(tasks); 672 } else { 673 mRecentTaskDescriptions.addAll(tasks); 674 } 675 mListAdapter.notifyDataSetInvalidated(); 676 updateUiElements(getResources().getConfiguration()); 677 mReadyToShow = true; 678 mFirstScreenful = false; 679 showIfReady(); 680 } 681 getRecentTasksList()682 public ArrayList<TaskDescription> getRecentTasksList() { 683 return mRecentTaskDescriptions; 684 } 685 getFirstScreenful()686 public boolean getFirstScreenful() { 687 return mFirstScreenful; 688 } 689 updateUiElements(Configuration config)690 private void updateUiElements(Configuration config) { 691 final int items = mRecentTaskDescriptions.size(); 692 693 mRecentsContainer.setVisibility(items > 0 ? View.VISIBLE : View.GONE); 694 695 // Set description for accessibility 696 int numRecentApps = mRecentTaskDescriptions.size(); 697 String recentAppsAccessibilityDescription; 698 if (numRecentApps == 0) { 699 recentAppsAccessibilityDescription = 700 getResources().getString(R.string.status_bar_no_recent_apps); 701 } else { 702 recentAppsAccessibilityDescription = getResources().getQuantityString( 703 R.plurals.status_bar_accessibility_recent_apps, numRecentApps, numRecentApps); 704 } 705 setContentDescription(recentAppsAccessibilityDescription); 706 } 707 708 709 boolean mThumbnailScaleUpStarted; handleOnClick(View view)710 public void handleOnClick(View view) { 711 ViewHolder holder = (ViewHolder)view.getTag(); 712 TaskDescription ad = holder.taskDescription; 713 final Context context = view.getContext(); 714 final ActivityManager am = (ActivityManager) 715 context.getSystemService(Context.ACTIVITY_SERVICE); 716 Bitmap bm = holder.thumbnailViewImageBitmap; 717 boolean usingDrawingCache; 718 if (bm.getWidth() == holder.thumbnailViewImage.getWidth() && 719 bm.getHeight() == holder.thumbnailViewImage.getHeight()) { 720 usingDrawingCache = false; 721 } else { 722 holder.thumbnailViewImage.setDrawingCacheEnabled(true); 723 bm = holder.thumbnailViewImage.getDrawingCache(); 724 usingDrawingCache = true; 725 } 726 727 if (mPlaceholderThumbnail == null) { 728 mPlaceholderThumbnail = 729 (ImageView) findViewById(R.id.recents_transition_placeholder_icon); 730 } 731 if (mTransitionBg == null) { 732 mTransitionBg = (View) findViewById(R.id.recents_transition_background); 733 734 IWindowManager wm = IWindowManager.Stub.asInterface( 735 ServiceManager.getService(Context.WINDOW_SERVICE)); 736 try { 737 if (!wm.hasSystemNavBar()) { 738 FrameLayout.LayoutParams lp = 739 (FrameLayout.LayoutParams) mTransitionBg.getLayoutParams(); 740 int statusBarHeight = getResources(). 741 getDimensionPixelSize(com.android.internal.R.dimen.status_bar_height); 742 lp.setMargins(0, statusBarHeight, 0, 0); 743 mTransitionBg.setLayoutParams(lp); 744 } 745 } catch (RemoteException e) { 746 Log.w(TAG, "Failing checking whether status bar is visible", e); 747 } 748 } 749 750 final ImageView placeholderThumbnail = mPlaceholderThumbnail; 751 mHideRecentsAfterThumbnailScaleUpStarted = false; 752 placeholderThumbnail.setVisibility(VISIBLE); 753 if (!usingDrawingCache) { 754 placeholderThumbnail.setImageBitmap(bm); 755 } else { 756 Bitmap b2 = bm.copy(bm.getConfig(), true); 757 placeholderThumbnail.setImageBitmap(b2); 758 } 759 Rect r = new Rect(); 760 holder.thumbnailViewImage.getGlobalVisibleRect(r); 761 762 placeholderThumbnail.setTranslationX(r.left); 763 placeholderThumbnail.setTranslationY(r.top); 764 765 show(false, true); 766 767 mThumbnailScaleUpStarted = false; 768 ActivityOptions opts = ActivityOptions.makeDelayedThumbnailScaleUpAnimation( 769 holder.thumbnailViewImage, bm, 0, 0, 770 new ActivityOptions.OnAnimationStartedListener() { 771 @Override public void onAnimationStarted() { 772 mThumbnailScaleUpStarted = true; 773 if (!mHighEndGfx) { 774 mPlaceholderThumbnail.setVisibility(INVISIBLE); 775 } 776 if (mHideRecentsAfterThumbnailScaleUpStarted) { 777 hideWindow(); 778 } 779 } 780 }); 781 if (ad.taskId >= 0) { 782 // This is an active task; it should just go to the foreground. 783 am.moveTaskToFront(ad.taskId, ActivityManager.MOVE_TASK_WITH_HOME, 784 opts.toBundle()); 785 } else { 786 Intent intent = ad.intent; 787 intent.addFlags(Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY 788 | Intent.FLAG_ACTIVITY_TASK_ON_HOME 789 | Intent.FLAG_ACTIVITY_NEW_TASK); 790 if (DEBUG) Log.v(TAG, "Starting activity " + intent); 791 context.startActivity(intent, opts.toBundle()); 792 } 793 if (usingDrawingCache) { 794 holder.thumbnailViewImage.setDrawingCacheEnabled(false); 795 } 796 } 797 hideWindow()798 public void hideWindow() { 799 if (!mThumbnailScaleUpStarted) { 800 mHideRecentsAfterThumbnailScaleUpStarted = true; 801 } else { 802 setVisibility(GONE); 803 mTransitionBg.setVisibility(INVISIBLE); 804 mPlaceholderThumbnail.setVisibility(INVISIBLE); 805 mHideRecentsAfterThumbnailScaleUpStarted = false; 806 } 807 } 808 onItemClick(AdapterView<?> parent, View view, int position, long id)809 public void onItemClick(AdapterView<?> parent, View view, int position, long id) { 810 handleOnClick(view); 811 } 812 handleSwipe(View view)813 public void handleSwipe(View view) { 814 TaskDescription ad = ((ViewHolder) view.getTag()).taskDescription; 815 if (ad == null) { 816 Log.v(TAG, "Not able to find activity description for swiped task; view=" + view + 817 " tag=" + view.getTag()); 818 return; 819 } 820 if (DEBUG) Log.v(TAG, "Jettison " + ad.getLabel()); 821 mRecentTaskDescriptions.remove(ad); 822 823 // Handled by widget containers to enable LayoutTransitions properly 824 // mListAdapter.notifyDataSetChanged(); 825 826 if (mRecentTaskDescriptions.size() == 0) { 827 hide(false); 828 } 829 830 // Currently, either direction means the same thing, so ignore direction and remove 831 // the task. 832 final ActivityManager am = (ActivityManager) 833 mContext.getSystemService(Context.ACTIVITY_SERVICE); 834 if (am != null) { 835 am.removeTask(ad.persistentTaskId, ActivityManager.REMOVE_TASK_KILL_PROCESS); 836 837 // Accessibility feedback 838 setContentDescription( 839 mContext.getString(R.string.accessibility_recents_item_dismissed, ad.getLabel())); 840 sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED); 841 setContentDescription(null); 842 } 843 } 844 startApplicationDetailsActivity(String packageName)845 private void startApplicationDetailsActivity(String packageName) { 846 Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, 847 Uri.fromParts("package", packageName, null)); 848 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 849 getContext().startActivity(intent); 850 } 851 onInterceptTouchEvent(MotionEvent ev)852 public boolean onInterceptTouchEvent(MotionEvent ev) { 853 if (mPopup != null) { 854 return true; 855 } else { 856 return super.onInterceptTouchEvent(ev); 857 } 858 } 859 handleLongPress( final View selectedView, final View anchorView, final View thumbnailView)860 public void handleLongPress( 861 final View selectedView, final View anchorView, final View thumbnailView) { 862 thumbnailView.setSelected(true); 863 final PopupMenu popup = 864 new PopupMenu(mContext, anchorView == null ? selectedView : anchorView); 865 mPopup = popup; 866 popup.getMenuInflater().inflate(R.menu.recent_popup_menu, popup.getMenu()); 867 popup.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() { 868 public boolean onMenuItemClick(MenuItem item) { 869 if (item.getItemId() == R.id.recent_remove_item) { 870 mRecentsContainer.removeViewInLayout(selectedView); 871 } else if (item.getItemId() == R.id.recent_inspect_item) { 872 ViewHolder viewHolder = (ViewHolder) selectedView.getTag(); 873 if (viewHolder != null) { 874 final TaskDescription ad = viewHolder.taskDescription; 875 startApplicationDetailsActivity(ad.packageName); 876 mBar.animateCollapse(CommandQueue.FLAG_EXCLUDE_NONE); 877 } else { 878 throw new IllegalStateException("Oops, no tag on view " + selectedView); 879 } 880 } else { 881 return false; 882 } 883 return true; 884 } 885 }); 886 popup.setOnDismissListener(new PopupMenu.OnDismissListener() { 887 public void onDismiss(PopupMenu menu) { 888 thumbnailView.setSelected(false); 889 mPopup = null; 890 } 891 }); 892 popup.show(); 893 } 894 } 895