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