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