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; 78 public static RecentTasksLoader getInstance(Context context) { 79 if (sInstance == null) { 80 sInstance = new RecentTasksLoader(context); 81 } 82 return sInstance; 83 } 84 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 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 129 public Bitmap getDefaultThumbnail() { 130 return mDefaultThumbnailBackground; 131 } 132 133 public Bitmap getDefaultIcon() { 134 return mDefaultIconBackground; 135 } 136 137 public ArrayList<TaskDescription> getLoadedTasks() { 138 return mLoadedTasks; 139 } 140 141 public void remove(TaskDescription td) { 142 mLoadedTasks.remove(td); 143 } 144 145 public boolean isFirstScreenful() { 146 return mFirstScreenful; 147 } 148 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 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 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 214 Drawable getFullResDefaultActivityIcon() { 215 return getFullResIcon(Resources.getSystem(), 216 com.android.internal.R.mipmap.sym_def_app_icon); 217 } 218 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 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 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 270 public void preloadRecentTasksList() { 271 mHandler.post(mPreloadTasksRunnable); 272 } 273 274 public void cancelPreloadingRecentTasksList() { 275 cancelLoadingThumbnailsAndIcons(); 276 mHandler.removeCallbacks(mPreloadTasksRunnable); 277 } 278 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 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 309 private void clearFirstTask() { 310 synchronized (mFirstTaskLock) { 311 mFirstTask = null; 312 mFirstTaskLoaded = false; 313 } 314 } 315 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 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; 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 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 405 public void loadTasksInBackground() { 406 loadTasksInBackground(false); 407 } 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 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