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