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; 81 public static RecentTasksLoader getInstance(Context context) { 82 if (sInstance == null) { 83 sInstance = new RecentTasksLoader(context); 84 } 85 return sInstance; 86 } 87 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 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 130 public Drawable getDefaultThumbnail() { 131 return mDefaultThumbnailBackground; 132 } 133 134 public Drawable getDefaultIcon() { 135 return mDefaultIconBackground; 136 } 137 138 public ArrayList<TaskDescription> getLoadedTasks() { 139 return mLoadedTasks; 140 } 141 142 public void remove(TaskDescription td) { 143 mLoadedTasks.remove(td); 144 } 145 146 public boolean isFirstScreenful() { 147 return mFirstScreenful; 148 } 149 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 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 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 223 Drawable getFullResDefaultActivityIcon() { 224 return getFullResIcon(Resources.getSystem(), 225 com.android.internal.R.mipmap.sym_def_app_icon); 226 } 227 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 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 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 279 public void preloadRecentTasksList() { 280 mHandler.post(mPreloadTasksRunnable); 281 } 282 283 public void cancelPreloadingRecentTasksList() { 284 cancelLoadingThumbnailsAndIcons(); 285 mHandler.removeCallbacks(mPreloadTasksRunnable); 286 } 287 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 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 318 private void clearFirstTask() { 319 synchronized (mFirstTaskLock) { 320 mFirstTask = null; 321 mFirstTaskLoaded = false; 322 } 323 } 324 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 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; 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 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 416 public void loadTasksInBackground() { 417 loadTasksInBackground(false); 418 } 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 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