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