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.app.ActivityManager; 20 import android.app.AppGlobals; 21 import android.content.ComponentName; 22 import android.content.Context; 23 import android.content.Intent; 24 import android.content.pm.ActivityInfo; 25 import android.content.pm.IPackageManager; 26 import android.content.pm.PackageManager; 27 import android.content.pm.ResolveInfo; 28 import android.content.res.Resources; 29 import android.graphics.Bitmap; 30 import android.graphics.drawable.BitmapDrawable; 31 import android.graphics.drawable.Drawable; 32 import android.os.AsyncTask; 33 import android.os.Handler; 34 import android.os.Process; 35 import android.os.RemoteException; 36 import android.os.UserHandle; 37 import android.util.Log; 38 import android.view.MotionEvent; 39 import android.view.View; 40 41 import com.android.systemui.R; 42 import com.android.systemui.recents.misc.SystemServicesProxy; 43 import com.android.systemui.statusbar.phone.PhoneStatusBar; 44 45 import java.util.ArrayList; 46 import java.util.List; 47 import java.util.concurrent.BlockingQueue; 48 import java.util.concurrent.LinkedBlockingQueue; 49 50 public class RecentTasksLoader implements View.OnTouchListener { 51 static final String TAG = "RecentTasksLoader"; 52 static final boolean DEBUG = PhoneStatusBar.DEBUG || false; 53 54 private static final int DISPLAY_TASKS = 20; 55 private static final int MAX_TASKS = DISPLAY_TASKS + 1; // allow extra for non-apps 56 57 private Context mContext; 58 private RecentsPanelView mRecentsPanel; 59 60 private Object mFirstTaskLock = new Object(); 61 private TaskDescription mFirstTask; 62 private boolean mFirstTaskLoaded; 63 64 private AsyncTask<Void, ArrayList<TaskDescription>, Void> mTaskLoader; 65 private AsyncTask<Void, TaskDescription, Void> mThumbnailLoader; 66 private Handler mHandler; 67 68 private int mIconDpi; 69 private ColorDrawableWithDimensions mDefaultThumbnailBackground; 70 private ColorDrawableWithDimensions mDefaultIconBackground; 71 private int mNumTasksInFirstScreenful = Integer.MAX_VALUE; 72 73 private boolean mFirstScreenful; 74 private ArrayList<TaskDescription> mLoadedTasks; 75 76 private enum State { LOADING, LOADED, CANCELLED }; 77 private State mState = State.CANCELLED; 78 79 80 private static RecentTasksLoader sInstance; getInstance(Context context)81 public static RecentTasksLoader getInstance(Context context) { 82 if (sInstance == null) { 83 sInstance = new RecentTasksLoader(context); 84 } 85 return sInstance; 86 } 87 RecentTasksLoader(Context context)88 private RecentTasksLoader(Context context) { 89 mContext = context; 90 mHandler = new Handler(); 91 92 final Resources res = context.getResources(); 93 94 // get the icon size we want -- on tablets, we use bigger icons 95 boolean isTablet = res.getBoolean(R.bool.config_recents_interface_for_tablets); 96 if (isTablet) { 97 ActivityManager activityManager = 98 (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); 99 mIconDpi = activityManager.getLauncherLargeIconDensity(); 100 } else { 101 mIconDpi = res.getDisplayMetrics().densityDpi; 102 } 103 104 // Render default icon (just a blank image) 105 int defaultIconSize = res.getDimensionPixelSize(com.android.internal.R.dimen.app_icon_size); 106 int iconSize = (int) (defaultIconSize * mIconDpi / res.getDisplayMetrics().densityDpi); 107 mDefaultIconBackground = new ColorDrawableWithDimensions(0x00000000, iconSize, iconSize); 108 109 // Render the default thumbnail background 110 int thumbnailWidth = 111 (int) res.getDimensionPixelSize(com.android.internal.R.dimen.thumbnail_width); 112 int thumbnailHeight = 113 (int) res.getDimensionPixelSize(com.android.internal.R.dimen.thumbnail_height); 114 int color = res.getColor(R.drawable.status_bar_recents_app_thumbnail_background); 115 116 mDefaultThumbnailBackground = 117 new ColorDrawableWithDimensions(color, thumbnailWidth, thumbnailHeight); 118 } 119 setRecentsPanel(RecentsPanelView newRecentsPanel, RecentsPanelView caller)120 public void setRecentsPanel(RecentsPanelView newRecentsPanel, RecentsPanelView caller) { 121 // Only allow clearing mRecentsPanel if the caller is the current recentsPanel 122 if (newRecentsPanel != null || mRecentsPanel == caller) { 123 mRecentsPanel = newRecentsPanel; 124 if (mRecentsPanel != null) { 125 mNumTasksInFirstScreenful = mRecentsPanel.numItemsInOneScreenful(); 126 } 127 } 128 } 129 getDefaultThumbnail()130 public Drawable getDefaultThumbnail() { 131 return mDefaultThumbnailBackground; 132 } 133 getDefaultIcon()134 public Drawable getDefaultIcon() { 135 return mDefaultIconBackground; 136 } 137 getLoadedTasks()138 public ArrayList<TaskDescription> getLoadedTasks() { 139 return mLoadedTasks; 140 } 141 remove(TaskDescription td)142 public void remove(TaskDescription td) { 143 mLoadedTasks.remove(td); 144 } 145 isFirstScreenful()146 public boolean isFirstScreenful() { 147 return mFirstScreenful; 148 } 149 isCurrentHomeActivity(ComponentName component, ActivityInfo homeInfo)150 private boolean isCurrentHomeActivity(ComponentName component, ActivityInfo homeInfo) { 151 if (homeInfo == null) { 152 final PackageManager pm = mContext.getPackageManager(); 153 homeInfo = new Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_HOME) 154 .resolveActivityInfo(pm, 0); 155 } 156 return homeInfo != null 157 && homeInfo.packageName.equals(component.getPackageName()) 158 && homeInfo.name.equals(component.getClassName()); 159 } 160 161 // Create an TaskDescription, returning null if the title or icon is null createTaskDescription(int taskId, int persistentTaskId, Intent baseIntent, ComponentName origActivity, CharSequence description, int userId)162 TaskDescription createTaskDescription(int taskId, int persistentTaskId, Intent baseIntent, 163 ComponentName origActivity, CharSequence description, int userId) { 164 Intent intent = new Intent(baseIntent); 165 if (origActivity != null) { 166 intent.setComponent(origActivity); 167 } 168 final PackageManager pm = mContext.getPackageManager(); 169 final IPackageManager ipm = AppGlobals.getPackageManager(); 170 intent.setFlags((intent.getFlags()&~Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED) 171 | Intent.FLAG_ACTIVITY_NEW_TASK); 172 ResolveInfo resolveInfo = null; 173 try { 174 resolveInfo = ipm.resolveIntent(intent, null, 0, userId); 175 } catch (RemoteException re) { 176 } 177 if (resolveInfo != null) { 178 final ActivityInfo info = resolveInfo.activityInfo; 179 final String title = info.loadLabel(pm).toString(); 180 181 if (title != null && title.length() > 0) { 182 if (DEBUG) Log.v(TAG, "creating activity desc for id=" 183 + persistentTaskId + ", label=" + title); 184 185 TaskDescription item = new TaskDescription(taskId, 186 persistentTaskId, resolveInfo, baseIntent, info.packageName, 187 description, userId); 188 item.setLabel(title); 189 190 return item; 191 } else { 192 if (DEBUG) Log.v(TAG, "SKIPPING item " + persistentTaskId); 193 } 194 } 195 return null; 196 } 197 loadThumbnailAndIcon(TaskDescription td)198 void loadThumbnailAndIcon(TaskDescription td) { 199 final ActivityManager am = (ActivityManager) 200 mContext.getSystemService(Context.ACTIVITY_SERVICE); 201 final PackageManager pm = mContext.getPackageManager(); 202 final Bitmap thumbnail = SystemServicesProxy.getThumbnail(am, td.persistentTaskId); 203 Drawable icon = getFullResIcon(td.resolveInfo, pm); 204 if (td.userId != UserHandle.myUserId()) { 205 // Need to badge the icon 206 icon = mContext.getPackageManager().getUserBadgedIcon(icon, new UserHandle(td.userId)); 207 } 208 if (DEBUG) Log.v(TAG, "Loaded bitmap for task " 209 + td + ": " + thumbnail); 210 synchronized (td) { 211 if (thumbnail != null) { 212 td.setThumbnail(new BitmapDrawable(mContext.getResources(), thumbnail)); 213 } else { 214 td.setThumbnail(mDefaultThumbnailBackground); 215 } 216 if (icon != null) { 217 td.setIcon(icon); 218 } 219 td.setLoaded(true); 220 } 221 } 222 getFullResDefaultActivityIcon()223 Drawable getFullResDefaultActivityIcon() { 224 return getFullResIcon(Resources.getSystem(), 225 com.android.internal.R.mipmap.sym_def_app_icon); 226 } 227 getFullResIcon(Resources resources, int iconId)228 Drawable getFullResIcon(Resources resources, int iconId) { 229 try { 230 return resources.getDrawableForDensity(iconId, mIconDpi); 231 } catch (Resources.NotFoundException e) { 232 return getFullResDefaultActivityIcon(); 233 } 234 } 235 getFullResIcon(ResolveInfo info, PackageManager packageManager)236 private Drawable getFullResIcon(ResolveInfo info, PackageManager packageManager) { 237 Resources resources; 238 try { 239 resources = packageManager.getResourcesForApplication( 240 info.activityInfo.applicationInfo); 241 } catch (PackageManager.NameNotFoundException e) { 242 resources = null; 243 } 244 if (resources != null) { 245 int iconId = info.activityInfo.getIconResource(); 246 if (iconId != 0) { 247 return getFullResIcon(resources, iconId); 248 } 249 } 250 return getFullResDefaultActivityIcon(); 251 } 252 253 Runnable mPreloadTasksRunnable = new Runnable() { 254 public void run() { 255 loadTasksInBackground(); 256 } 257 }; 258 259 // additional optimization when we have software system buttons - start loading the recent 260 // tasks on touch down 261 @Override onTouch(View v, MotionEvent ev)262 public boolean onTouch(View v, MotionEvent ev) { 263 int action = ev.getAction() & MotionEvent.ACTION_MASK; 264 if (action == MotionEvent.ACTION_DOWN) { 265 preloadRecentTasksList(); 266 } else if (action == MotionEvent.ACTION_CANCEL) { 267 cancelPreloadingRecentTasksList(); 268 } else if (action == MotionEvent.ACTION_UP) { 269 // Remove the preloader if we haven't called it yet 270 mHandler.removeCallbacks(mPreloadTasksRunnable); 271 if (!v.isPressed()) { 272 cancelLoadingThumbnailsAndIcons(); 273 } 274 275 } 276 return false; 277 } 278 preloadRecentTasksList()279 public void preloadRecentTasksList() { 280 mHandler.post(mPreloadTasksRunnable); 281 } 282 cancelPreloadingRecentTasksList()283 public void cancelPreloadingRecentTasksList() { 284 cancelLoadingThumbnailsAndIcons(); 285 mHandler.removeCallbacks(mPreloadTasksRunnable); 286 } 287 cancelLoadingThumbnailsAndIcons(RecentsPanelView caller)288 public void cancelLoadingThumbnailsAndIcons(RecentsPanelView caller) { 289 // Only oblige this request if it comes from the current RecentsPanel 290 // (eg when you rotate, the old RecentsPanel request should be ignored) 291 if (mRecentsPanel == caller) { 292 cancelLoadingThumbnailsAndIcons(); 293 } 294 } 295 296 cancelLoadingThumbnailsAndIcons()297 private void cancelLoadingThumbnailsAndIcons() { 298 if (mRecentsPanel != null && mRecentsPanel.isShowing()) { 299 return; 300 } 301 302 if (mTaskLoader != null) { 303 mTaskLoader.cancel(false); 304 mTaskLoader = null; 305 } 306 if (mThumbnailLoader != null) { 307 mThumbnailLoader.cancel(false); 308 mThumbnailLoader = null; 309 } 310 mLoadedTasks = null; 311 if (mRecentsPanel != null) { 312 mRecentsPanel.onTaskLoadingCancelled(); 313 } 314 mFirstScreenful = false; 315 mState = State.CANCELLED; 316 } 317 clearFirstTask()318 private void clearFirstTask() { 319 synchronized (mFirstTaskLock) { 320 mFirstTask = null; 321 mFirstTaskLoaded = false; 322 } 323 } 324 preloadFirstTask()325 public void preloadFirstTask() { 326 Thread bgLoad = new Thread() { 327 public void run() { 328 TaskDescription first = loadFirstTask(); 329 synchronized(mFirstTaskLock) { 330 if (mCancelPreloadingFirstTask) { 331 clearFirstTask(); 332 } else { 333 mFirstTask = first; 334 mFirstTaskLoaded = true; 335 } 336 mPreloadingFirstTask = false; 337 } 338 } 339 }; 340 synchronized(mFirstTaskLock) { 341 if (!mPreloadingFirstTask) { 342 clearFirstTask(); 343 mPreloadingFirstTask = true; 344 bgLoad.start(); 345 } 346 } 347 } 348 cancelPreloadingFirstTask()349 public void cancelPreloadingFirstTask() { 350 synchronized(mFirstTaskLock) { 351 if (mPreloadingFirstTask) { 352 mCancelPreloadingFirstTask = true; 353 } else { 354 clearFirstTask(); 355 } 356 } 357 } 358 359 boolean mPreloadingFirstTask; 360 boolean mCancelPreloadingFirstTask; getFirstTask()361 public TaskDescription getFirstTask() { 362 while(true) { 363 synchronized(mFirstTaskLock) { 364 if (mFirstTaskLoaded) { 365 return mFirstTask; 366 } else if (!mFirstTaskLoaded && !mPreloadingFirstTask) { 367 mFirstTask = loadFirstTask(); 368 mFirstTaskLoaded = true; 369 return mFirstTask; 370 } 371 } 372 try { 373 Thread.sleep(3); 374 } catch (InterruptedException e) { 375 } 376 } 377 } 378 loadFirstTask()379 public TaskDescription loadFirstTask() { 380 final ActivityManager am = (ActivityManager) mContext.getSystemService(Context.ACTIVITY_SERVICE); 381 382 final List<ActivityManager.RecentTaskInfo> recentTasks = am.getRecentTasksForUser(1, 383 ActivityManager.RECENT_IGNORE_UNAVAILABLE | ActivityManager.RECENT_INCLUDE_PROFILES, 384 UserHandle.CURRENT.getIdentifier()); 385 TaskDescription item = null; 386 if (recentTasks.size() > 0) { 387 ActivityManager.RecentTaskInfo recentInfo = recentTasks.get(0); 388 389 Intent intent = new Intent(recentInfo.baseIntent); 390 if (recentInfo.origActivity != null) { 391 intent.setComponent(recentInfo.origActivity); 392 } 393 394 // Don't load the current home activity. 395 if (isCurrentHomeActivity(intent.getComponent(), null)) { 396 return null; 397 } 398 399 // Don't load ourselves 400 if (intent.getComponent().getPackageName().equals(mContext.getPackageName())) { 401 return null; 402 } 403 404 item = createTaskDescription(recentInfo.id, 405 recentInfo.persistentId, recentInfo.baseIntent, 406 recentInfo.origActivity, recentInfo.description, 407 recentInfo.userId); 408 if (item != null) { 409 loadThumbnailAndIcon(item); 410 } 411 return item; 412 } 413 return null; 414 } 415 loadTasksInBackground()416 public void loadTasksInBackground() { 417 loadTasksInBackground(false); 418 } loadTasksInBackground(final boolean zeroeth)419 public void loadTasksInBackground(final boolean zeroeth) { 420 if (mState != State.CANCELLED) { 421 return; 422 } 423 mState = State.LOADING; 424 mFirstScreenful = true; 425 426 final LinkedBlockingQueue<TaskDescription> tasksWaitingForThumbnails = 427 new LinkedBlockingQueue<TaskDescription>(); 428 mTaskLoader = new AsyncTask<Void, ArrayList<TaskDescription>, Void>() { 429 @Override 430 protected void onProgressUpdate(ArrayList<TaskDescription>... values) { 431 if (!isCancelled()) { 432 ArrayList<TaskDescription> newTasks = values[0]; 433 // do a callback to RecentsPanelView to let it know we have more values 434 // how do we let it know we're all done? just always call back twice 435 if (mRecentsPanel != null) { 436 mRecentsPanel.onTasksLoaded(newTasks, mFirstScreenful); 437 } 438 if (mLoadedTasks == null) { 439 mLoadedTasks = new ArrayList<TaskDescription>(); 440 } 441 mLoadedTasks.addAll(newTasks); 442 mFirstScreenful = false; 443 } 444 } 445 @Override 446 protected Void doInBackground(Void... params) { 447 // We load in two stages: first, we update progress with just the first screenful 448 // of items. Then, we update with the rest of the items 449 final int origPri = Process.getThreadPriority(Process.myTid()); 450 Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); 451 final PackageManager pm = mContext.getPackageManager(); 452 final ActivityManager am = (ActivityManager) 453 mContext.getSystemService(Context.ACTIVITY_SERVICE); 454 455 final List<ActivityManager.RecentTaskInfo> recentTasks = 456 am.getRecentTasks(MAX_TASKS, ActivityManager.RECENT_IGNORE_UNAVAILABLE 457 | ActivityManager.RECENT_INCLUDE_PROFILES); 458 int numTasks = recentTasks.size(); 459 ActivityInfo homeInfo = new Intent(Intent.ACTION_MAIN) 460 .addCategory(Intent.CATEGORY_HOME).resolveActivityInfo(pm, 0); 461 462 boolean firstScreenful = true; 463 ArrayList<TaskDescription> tasks = new ArrayList<TaskDescription>(); 464 465 // skip the first task - assume it's either the home screen or the current activity. 466 final int first = 0; 467 for (int i = first, index = 0; i < numTasks && (index < MAX_TASKS); ++i) { 468 if (isCancelled()) { 469 break; 470 } 471 final ActivityManager.RecentTaskInfo recentInfo = recentTasks.get(i); 472 473 Intent intent = new Intent(recentInfo.baseIntent); 474 if (recentInfo.origActivity != null) { 475 intent.setComponent(recentInfo.origActivity); 476 } 477 478 // Don't load the current home activity. 479 if (isCurrentHomeActivity(intent.getComponent(), homeInfo)) { 480 continue; 481 } 482 483 // Don't load ourselves 484 if (intent.getComponent().getPackageName().equals(mContext.getPackageName())) { 485 continue; 486 } 487 488 TaskDescription item = createTaskDescription(recentInfo.id, 489 recentInfo.persistentId, recentInfo.baseIntent, 490 recentInfo.origActivity, recentInfo.description, 491 recentInfo.userId); 492 493 if (item != null) { 494 while (true) { 495 try { 496 tasksWaitingForThumbnails.put(item); 497 break; 498 } catch (InterruptedException e) { 499 } 500 } 501 tasks.add(item); 502 if (firstScreenful && tasks.size() == mNumTasksInFirstScreenful) { 503 publishProgress(tasks); 504 tasks = new ArrayList<TaskDescription>(); 505 firstScreenful = false; 506 //break; 507 } 508 ++index; 509 } 510 } 511 512 if (!isCancelled()) { 513 publishProgress(tasks); 514 if (firstScreenful) { 515 // always should publish two updates 516 publishProgress(new ArrayList<TaskDescription>()); 517 } 518 } 519 520 while (true) { 521 try { 522 tasksWaitingForThumbnails.put(new TaskDescription()); 523 break; 524 } catch (InterruptedException e) { 525 } 526 } 527 528 Process.setThreadPriority(origPri); 529 return null; 530 } 531 }; 532 mTaskLoader.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); 533 loadThumbnailsAndIconsInBackground(tasksWaitingForThumbnails); 534 } 535 loadThumbnailsAndIconsInBackground( final BlockingQueue<TaskDescription> tasksWaitingForThumbnails)536 private void loadThumbnailsAndIconsInBackground( 537 final BlockingQueue<TaskDescription> tasksWaitingForThumbnails) { 538 // continually read items from tasksWaitingForThumbnails and load 539 // thumbnails and icons for them. finish thread when cancelled or there 540 // is a null item in tasksWaitingForThumbnails 541 mThumbnailLoader = new AsyncTask<Void, TaskDescription, Void>() { 542 @Override 543 protected void onProgressUpdate(TaskDescription... values) { 544 if (!isCancelled()) { 545 TaskDescription td = values[0]; 546 if (td.isNull()) { // end sentinel 547 mState = State.LOADED; 548 } else { 549 if (mRecentsPanel != null) { 550 mRecentsPanel.onTaskThumbnailLoaded(td); 551 } 552 } 553 } 554 } 555 @Override 556 protected Void doInBackground(Void... params) { 557 final int origPri = Process.getThreadPriority(Process.myTid()); 558 Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); 559 560 while (true) { 561 if (isCancelled()) { 562 break; 563 } 564 TaskDescription td = null; 565 while (td == null) { 566 try { 567 td = tasksWaitingForThumbnails.take(); 568 } catch (InterruptedException e) { 569 } 570 } 571 if (td.isNull()) { // end sentinel 572 publishProgress(td); 573 break; 574 } 575 loadThumbnailAndIcon(td); 576 577 publishProgress(td); 578 } 579 580 Process.setThreadPriority(origPri); 581 return null; 582 } 583 }; 584 mThumbnailLoader.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); 585 } 586 } 587