RecentsPanelView.java revision 6311d0a079702b29984c0d31937345be105e1a5e
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; 20import java.util.List; 21 22import android.animation.Animator; 23import android.animation.LayoutTransition; 24import android.app.ActivityManager; 25import android.content.Context; 26import android.content.Intent; 27import android.content.pm.ActivityInfo; 28import android.content.pm.PackageManager; 29import android.content.pm.ResolveInfo; 30import android.content.res.Configuration; 31import android.content.res.Resources; 32import android.graphics.Bitmap; 33import android.graphics.BitmapFactory; 34import android.graphics.Canvas; 35import android.graphics.Matrix; 36import android.graphics.Paint; 37import android.graphics.Rect; 38import android.graphics.RectF; 39import android.graphics.Shader.TileMode; 40import android.graphics.drawable.BitmapDrawable; 41import android.graphics.drawable.Drawable; 42import android.graphics.drawable.LayerDrawable; 43import android.net.Uri; 44import android.provider.Settings; 45import android.util.AttributeSet; 46import android.util.DisplayMetrics; 47import android.util.Log; 48import android.view.KeyEvent; 49import android.view.LayoutInflater; 50import android.view.MenuItem; 51import android.view.MotionEvent; 52import android.view.View; 53import android.view.ViewGroup; 54import android.widget.AdapterView; 55import android.widget.AdapterView.OnItemClickListener; 56import android.widget.BaseAdapter; 57import android.widget.ImageView; 58import android.widget.PopupMenu; 59import android.widget.RelativeLayout; 60import android.widget.TextView; 61 62import com.android.systemui.R; 63import com.android.systemui.statusbar.StatusBar; 64import com.android.systemui.statusbar.phone.PhoneStatusBar; 65import com.android.systemui.statusbar.tablet.StatusBarPanel; 66import com.android.systemui.statusbar.tablet.TabletStatusBar; 67 68public class RecentsPanelView extends RelativeLayout 69 implements OnItemClickListener, RecentsCallback, StatusBarPanel, Animator.AnimatorListener { 70 static final String TAG = "RecentsListView"; 71 static final boolean DEBUG = TabletStatusBar.DEBUG || PhoneStatusBar.DEBUG; 72 private static final int DISPLAY_TASKS = 20; 73 private static final int MAX_TASKS = DISPLAY_TASKS + 1; // allow extra for non-apps 74 private StatusBar mBar; 75 private ArrayList<ActivityDescription> mActivityDescriptions; 76 private int mIconDpi; 77 private View mRecentsScrim; 78 private View mRecentsGlowView; 79 private ViewGroup mRecentsContainer; 80 private Bitmap mGlowBitmap; 81 // TODO: add these widgets attributes to the layout file 82 private int mGlowBitmapPaddingLeftPx; 83 private int mGlowBitmapPaddingTopPx; 84 private int mGlowBitmapPaddingRightPx; 85 private int mGlowBitmapPaddingBottomPx; 86 private boolean mShowing; 87 private Choreographer mChoreo; 88 private View mRecentsDismissButton; 89 private ActivityDescriptionAdapter mListAdapter; 90 91 /* package */ final static class ActivityDescription { 92 int taskId; // application task id for curating apps 93 Bitmap thumbnail; // generated by Activity.onCreateThumbnail() 94 Drawable icon; // application package icon 95 String label; // application package label 96 CharSequence description; // generated by Activity.onCreateDescription() 97 Intent intent; // launch intent for application 98 Matrix matrix; // arbitrary rotation matrix to correct orientation 99 String packageName; // used to override animations (see onClick()) 100 int position; // position in list 101 102 public ActivityDescription(Bitmap _thumbnail, 103 Drawable _icon, String _label, CharSequence _desc, Intent _intent, 104 int _id, int _pos, String _packageName) 105 { 106 thumbnail = _thumbnail; 107 icon = _icon; 108 label = _label; 109 description = _desc; 110 intent = _intent; 111 taskId = _id; 112 position = _pos; 113 packageName = _packageName; 114 } 115 } 116 117 private final class OnLongClickDelegate implements View.OnLongClickListener { 118 View mOtherView; 119 OnLongClickDelegate(View other) { 120 mOtherView = other; 121 } 122 public boolean onLongClick(View v) { 123 return mOtherView.performLongClick(); 124 } 125 } 126 127 /* package */ final static class ViewHolder { 128 View thumbnailView; 129 ImageView iconView; 130 TextView labelView; 131 TextView descriptionView; 132 ActivityDescription activityDescription; 133 } 134 135 /* package */ final class ActivityDescriptionAdapter extends BaseAdapter { 136 private LayoutInflater mInflater; 137 138 public ActivityDescriptionAdapter(Context context) { 139 mInflater = LayoutInflater.from(context); 140 } 141 142 public int getCount() { 143 return mActivityDescriptions != null ? mActivityDescriptions.size() : 0; 144 } 145 146 public Object getItem(int position) { 147 return position; // we only need the index 148 } 149 150 public long getItemId(int position) { 151 return position; // we just need something unique for this position 152 } 153 154 public View getView(int position, View convertView, ViewGroup parent) { 155 ViewHolder holder; 156 if (convertView == null) { 157 convertView = mInflater.inflate(R.layout.status_bar_recent_item, null); 158 holder = new ViewHolder(); 159 holder.thumbnailView = convertView.findViewById(R.id.app_thumbnail); 160 holder.iconView = (ImageView) convertView.findViewById(R.id.app_icon); 161 holder.labelView = (TextView) convertView.findViewById(R.id.app_label); 162 holder.descriptionView = (TextView) convertView.findViewById(R.id.app_description); 163 convertView.setTag(holder); 164 } else { 165 holder = (ViewHolder) convertView.getTag(); 166 } 167 168 // activityId is reverse since most recent appears at the bottom... 169 final int activityId = mActivityDescriptions.size() - position - 1; 170 171 final ActivityDescription activityDescription = mActivityDescriptions.get(activityId); 172 final Bitmap thumb = activityDescription.thumbnail; 173 updateDrawable(holder.thumbnailView, compositeBitmap(mGlowBitmap, thumb)); 174 holder.iconView.setImageDrawable(activityDescription.icon); 175 holder.labelView.setText(activityDescription.label); 176 holder.descriptionView.setText(activityDescription.description); 177 holder.thumbnailView.setTag(activityDescription); 178 holder.thumbnailView.setOnLongClickListener(new OnLongClickDelegate(convertView)); 179 holder.activityDescription = activityDescription; 180 181 return convertView; 182 } 183 } 184 185 @Override 186 public boolean onKeyUp(int keyCode, KeyEvent event) { 187 if (keyCode == KeyEvent.KEYCODE_BACK && !event.isCanceled()) { 188 show(false, true); 189 return true; 190 } 191 return super.onKeyUp(keyCode, event); 192 } 193 194 public boolean isInContentArea(int x, int y) { 195 // use mRecentsContainer's exact bounds to determine horizontal position 196 final int l = mRecentsContainer.getLeft(); 197 final int r = mRecentsContainer.getRight(); 198 // use surrounding mRecentsGlowView's position in parent determine vertical bounds 199 final int t = mRecentsGlowView.getTop(); 200 final int b = mRecentsGlowView.getBottom(); 201 return x >= l && x < r && y >= t && y < b; 202 } 203 204 private void updateDrawable(View thumbnailView, Bitmap bitmap) { 205 Drawable d = thumbnailView.getBackground(); 206 if (d instanceof LayerDrawable) { 207 LayerDrawable layerD = (LayerDrawable) d; 208 Drawable thumb = layerD.findDrawableByLayerId(R.id.base_layer); 209 if (thumb != null) { 210 layerD.setDrawableByLayerId(R.id.base_layer, 211 new BitmapDrawable(getResources(), bitmap)); 212 return; 213 } 214 } 215 Log.w(TAG, "Failed to update drawable"); 216 } 217 218 public void show(boolean show, boolean animate) { 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 } 232 if (show) { 233 setFocusable(true); 234 setFocusableInTouchMode(true); 235 requestFocus(); 236 } 237 } 238 239 public void onAnimationCancel(Animator animation) { 240 } 241 242 public void onAnimationEnd(Animator animation) { 243 if (mShowing) { 244 final LayoutTransition transitioner = new LayoutTransition(); 245 ((ViewGroup)mRecentsContainer).setLayoutTransition(transitioner); 246 createCustomAnimations(transitioner); 247 } else { 248 ((ViewGroup)mRecentsContainer).setLayoutTransition(null); 249 } 250 } 251 252 public void onAnimationRepeat(Animator animation) { 253 } 254 255 public void onAnimationStart(Animator animation) { 256 } 257 258 259 /** 260 * We need to be aligned at the bottom. LinearLayout can't do this, so instead, 261 * let LinearLayout do all the hard work, and then shift everything down to the bottom. 262 */ 263 @Override 264 protected void onLayout(boolean changed, int l, int t, int r, int b) { 265 super.onLayout(changed, l, t, r, b); 266 mChoreo.setPanelHeight(mRecentsContainer.getHeight()); 267 } 268 269 @Override 270 public boolean dispatchHoverEvent(MotionEvent event) { 271 // Ignore hover events outside of this panel bounds since such events 272 // generate spurious accessibility events with the panel content when 273 // tapping outside of it, thus confusing the user. 274 final int x = (int) event.getX(); 275 final int y = (int) event.getY(); 276 if (x >= 0 && x < getWidth() && y >= 0 && y < getHeight()) { 277 return super.dispatchHoverEvent(event); 278 } 279 return true; 280 } 281 282 /** 283 * Whether the panel is showing, or, if it's animating, whether it will be 284 * when the animation is done. 285 */ 286 public boolean isShowing() { 287 return mShowing; 288 } 289 290 public void setBar(StatusBar bar) { 291 mBar = bar; 292 } 293 294 public RecentsPanelView(Context context, AttributeSet attrs) { 295 this(context, attrs, 0); 296 } 297 298 public RecentsPanelView(Context context, AttributeSet attrs, int defStyle) { 299 super(context, attrs, defStyle); 300 301 Resources res = context.getResources(); 302 boolean xlarge = (res.getConfiguration().screenLayout 303 & Configuration.SCREENLAYOUT_SIZE_MASK) == Configuration.SCREENLAYOUT_SIZE_XLARGE; 304 305 mIconDpi = xlarge ? DisplayMetrics.DENSITY_HIGH : res.getDisplayMetrics().densityDpi; 306 307 mGlowBitmap = BitmapFactory.decodeResource(res, R.drawable.recents_thumbnail_bg); 308 mGlowBitmapPaddingLeftPx = 309 res.getDimensionPixelSize(R.dimen.recents_thumbnail_bg_padding_left); 310 mGlowBitmapPaddingTopPx = 311 res.getDimensionPixelSize(R.dimen.recents_thumbnail_bg_padding_top); 312 mGlowBitmapPaddingRightPx = 313 res.getDimensionPixelSize(R.dimen.recents_thumbnail_bg_padding_right); 314 mGlowBitmapPaddingBottomPx = 315 res.getDimensionPixelSize(R.dimen.recents_thumbnail_bg_padding_bottom); 316 } 317 318 @Override 319 protected void onFinishInflate() { 320 super.onFinishInflate(); 321 mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); 322 mRecentsContainer = (ViewGroup) findViewById(R.id.recents_container); 323 mListAdapter = new ActivityDescriptionAdapter(mContext); 324 if (mRecentsContainer instanceof RecentsListView) { 325 RecentsListView listView = (RecentsListView) mRecentsContainer; 326 listView.setAdapter(mListAdapter); 327 listView.setOnItemClickListener(this); 328 listView.setCallback(this); 329 } else if (mRecentsContainer instanceof RecentsHorizontalScrollView){ 330 RecentsHorizontalScrollView scrollView 331 = (RecentsHorizontalScrollView) mRecentsContainer; 332 scrollView.setAdapter(mListAdapter); 333 scrollView.setCallback(this); 334 } else if (mRecentsContainer instanceof RecentsVerticalScrollView){ 335 RecentsVerticalScrollView scrollView 336 = (RecentsVerticalScrollView) mRecentsContainer; 337 scrollView.setAdapter(mListAdapter); 338 scrollView.setCallback(this); 339 } 340 else { 341 throw new IllegalArgumentException("missing RecentsListView/RecentsScrollView"); 342 } 343 344 345 mRecentsGlowView = findViewById(R.id.recents_glow); 346 mRecentsScrim = (View) findViewById(R.id.recents_bg_protect); 347 mChoreo = new Choreographer(this, mRecentsScrim, mRecentsGlowView, this); 348 mRecentsDismissButton = findViewById(R.id.recents_dismiss_button); 349 mRecentsDismissButton.setOnClickListener(new OnClickListener() { 350 public void onClick(View v) { 351 hide(true); 352 } 353 }); 354 355 // In order to save space, we make the background texture repeat in the Y direction 356 if (mRecentsScrim != null && mRecentsScrim.getBackground() instanceof BitmapDrawable) { 357 ((BitmapDrawable) mRecentsScrim.getBackground()).setTileModeY(TileMode.REPEAT); 358 } 359 } 360 361 private void createCustomAnimations(LayoutTransition transitioner) { 362 transitioner.setDuration(LayoutTransition.DISAPPEARING, 250); 363 } 364 365 @Override 366 protected void onVisibilityChanged(View changedView, int visibility) { 367 super.onVisibilityChanged(changedView, visibility); 368 if (DEBUG) Log.v(TAG, "onVisibilityChanged(" + changedView + ", " + visibility + ")"); 369 if (visibility == View.VISIBLE && changedView == this) { 370 refreshApplicationList(); 371 } 372 } 373 374 private Drawable getFullResDefaultActivityIcon() { 375 return getFullResIcon(Resources.getSystem(), 376 com.android.internal.R.mipmap.sym_def_app_icon); 377 } 378 379 private Drawable getFullResIcon(Resources resources, int iconId) { 380 try { 381 return resources.getDrawableForDensity(iconId, mIconDpi); 382 } catch (Resources.NotFoundException e) { 383 return getFullResDefaultActivityIcon(); 384 } 385 } 386 387 private Drawable getFullResIcon(ResolveInfo info, PackageManager packageManager) { 388 Resources resources; 389 try { 390 resources = packageManager.getResourcesForApplication( 391 info.activityInfo.applicationInfo); 392 } catch (PackageManager.NameNotFoundException e) { 393 resources = null; 394 } 395 if (resources != null) { 396 int iconId = info.activityInfo.getIconResource(); 397 if (iconId != 0) { 398 return getFullResIcon(resources, iconId); 399 } 400 } 401 return getFullResDefaultActivityIcon(); 402 } 403 404 private ArrayList<ActivityDescription> getRecentTasks() { 405 ArrayList<ActivityDescription> activityDescriptions = new ArrayList<ActivityDescription>(); 406 final PackageManager pm = mContext.getPackageManager(); 407 final ActivityManager am = (ActivityManager) 408 mContext.getSystemService(Context.ACTIVITY_SERVICE); 409 410 final List<ActivityManager.RecentTaskInfo> recentTasks = 411 am.getRecentTasks(MAX_TASKS, ActivityManager.RECENT_IGNORE_UNAVAILABLE); 412 413 ActivityInfo homeInfo = new Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_HOME) 414 .resolveActivityInfo(pm, 0); 415 416 int numTasks = recentTasks.size(); 417 418 // skip the first activity - assume it's either the home screen or the current app. 419 final int first = 1; 420 for (int i = first, index = 0; i < numTasks && (index < MAX_TASKS); ++i) { 421 final ActivityManager.RecentTaskInfo recentInfo = recentTasks.get(i); 422 423 Intent intent = new Intent(recentInfo.baseIntent); 424 if (recentInfo.origActivity != null) { 425 intent.setComponent(recentInfo.origActivity); 426 } 427 428 // Skip the current home activity. 429 if (homeInfo != null 430 && homeInfo.packageName.equals(intent.getComponent().getPackageName()) 431 && homeInfo.name.equals(intent.getComponent().getClassName())) { 432 continue; 433 } 434 435 intent.setFlags((intent.getFlags()&~Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED) 436 | Intent.FLAG_ACTIVITY_NEW_TASK); 437 final ResolveInfo resolveInfo = pm.resolveActivity(intent, 0); 438 if (resolveInfo != null) { 439 final ActivityInfo info = resolveInfo.activityInfo; 440 final String title = info.loadLabel(pm).toString(); 441 // Drawable icon = info.loadIcon(pm); 442 Drawable icon = getFullResIcon(resolveInfo, pm); 443 int id = recentTasks.get(i).id; 444 if (title != null && title.length() > 0 && icon != null) { 445 if (DEBUG) Log.v(TAG, "creating activity desc for id=" + id + ", label=" + title); 446 ActivityManager.TaskThumbnails thumbs = am.getTaskThumbnails( 447 recentInfo.persistentId); 448 ActivityDescription item = new ActivityDescription( 449 thumbs != null ? thumbs.mainThumbnail : null, 450 icon, title, recentInfo.description, intent, id, 451 index, info.packageName); 452 activityDescriptions.add(item); 453 ++index; 454 } else { 455 if (DEBUG) Log.v(TAG, "SKIPPING item " + id); 456 } 457 } 458 } 459 return activityDescriptions; 460 } 461 462 ActivityDescription findActivityDescription(int id) 463 { 464 ActivityDescription desc = null; 465 for (int i = 0; i < mActivityDescriptions.size(); i++) { 466 ActivityDescription item = mActivityDescriptions.get(i); 467 if (item != null && item.taskId == id) { 468 desc = item; 469 break; 470 } 471 } 472 return desc; 473 } 474 475 private void refreshApplicationList() { 476 mActivityDescriptions = getRecentTasks(); 477 mListAdapter.notifyDataSetInvalidated(); 478 if (mActivityDescriptions.size() > 0) { 479 if (DEBUG) Log.v(TAG, "Showing " + mActivityDescriptions.size() + " apps"); 480 updateUiElements(getResources().getConfiguration()); 481 } else { 482 // Immediately hide this panel 483 if (DEBUG) Log.v(TAG, "Nothing to show"); 484 hide(false); 485 } 486 } 487 488 private Bitmap compositeBitmap(Bitmap background, Bitmap thumbnail) { 489 Bitmap outBitmap = background.copy(background.getConfig(), true); 490 if (thumbnail != null) { 491 Canvas canvas = new Canvas(outBitmap); 492 Paint paint = new Paint(); 493 paint.setAntiAlias(true); 494 paint.setFilterBitmap(true); 495 paint.setAlpha(255); 496 final int srcWidth = thumbnail.getWidth(); 497 final int srcHeight = thumbnail.getHeight(); 498 if (DEBUG) Log.v(TAG, "Source thumb: " + srcWidth + "x" + srcHeight); 499 canvas.drawBitmap(thumbnail, 500 new Rect(0, 0, srcWidth-1, srcHeight-1), 501 new RectF(mGlowBitmapPaddingLeftPx, mGlowBitmapPaddingTopPx, 502 outBitmap.getWidth() - mGlowBitmapPaddingRightPx, 503 outBitmap.getHeight() - mGlowBitmapPaddingBottomPx), paint); 504 canvas.setBitmap(null); 505 } 506 return outBitmap; 507 } 508 509 private void updateUiElements(Configuration config) { 510 final int items = mActivityDescriptions.size(); 511 512 mRecentsContainer.setVisibility(items > 0 ? View.VISIBLE : View.GONE); 513 mRecentsGlowView.setVisibility(items > 0 ? View.VISIBLE : View.GONE); 514 } 515 516 public void hide(boolean animate) { 517 if (!animate) { 518 setVisibility(View.GONE); 519 } 520 if (mBar != null) { 521 mBar.animateCollapse(); 522 } 523 } 524 525 public void handleOnClick(View view) { 526 ActivityDescription ad = ((ViewHolder) view.getTag()).activityDescription; 527 final Context context = view.getContext(); 528 final ActivityManager am = (ActivityManager) 529 context.getSystemService(Context.ACTIVITY_SERVICE); 530 if (ad.taskId >= 0) { 531 // This is an active task; it should just go to the foreground. 532 am.moveTaskToFront(ad.taskId, ActivityManager.MOVE_TASK_WITH_HOME); 533 } else { 534 Intent intent = ad.intent; 535 intent.addFlags(Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY 536 | Intent.FLAG_ACTIVITY_TASK_ON_HOME); 537 if (DEBUG) Log.v(TAG, "Starting activity " + intent); 538 context.startActivity(intent); 539 } 540 hide(true); 541 } 542 543 public void onItemClick(AdapterView<?> parent, View view, int position, long id) { 544 handleOnClick(view); 545 } 546 547 public void handleSwipe(View view) { 548 ActivityDescription ad = ((ViewHolder) view.getTag()).activityDescription; 549 if (DEBUG) Log.v(TAG, "Jettison " + ad.label); 550 mActivityDescriptions.remove(ad); 551 552 // Handled by widget containers to enable LayoutTransitions properly 553 // mListAdapter.notifyDataSetChanged(); 554 555 if (mActivityDescriptions.size() == 0) { 556 hide(false); 557 } 558 559 // Currently, either direction means the same thing, so ignore direction and remove 560 // the task. 561 final ActivityManager am = (ActivityManager) 562 mContext.getSystemService(Context.ACTIVITY_SERVICE); 563 am.removeTask(ad.taskId, ActivityManager.REMOVE_TASK_KILL_PROCESS); 564 } 565 566 private void startApplicationDetailsActivity(String packageName) { 567 Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, 568 Uri.fromParts("package", packageName, null)); 569 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 570 getContext().startActivity(intent); 571 } 572 573 public void handleLongPress(final View selectedView, final View anchorView) { 574 PopupMenu popup = new PopupMenu(mContext, anchorView == null ? selectedView : anchorView); 575 popup.getMenuInflater().inflate(R.menu.recent_popup_menu, popup.getMenu()); 576 popup.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() { 577 public boolean onMenuItemClick(MenuItem item) { 578 if (item.getItemId() == R.id.recent_remove_item) { 579 mRecentsContainer.removeViewInLayout(selectedView); 580 } else if (item.getItemId() == R.id.recent_inspect_item) { 581 ViewHolder viewHolder = (ViewHolder) selectedView.getTag(); 582 if (viewHolder != null) { 583 final ActivityDescription ad = viewHolder.activityDescription; 584 startApplicationDetailsActivity(ad.packageName); 585 mBar.animateCollapse(); 586 } else { 587 throw new IllegalStateException("Oops, no tag on view " + selectedView); 588 } 589 } else { 590 return false; 591 } 592 return true; 593 } 594 }); 595 popup.show(); 596 } 597} 598