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 android.app.ActivityManager; 20import android.content.ComponentName; 21import android.content.Context; 22import android.content.Intent; 23import android.content.pm.ActivityInfo; 24import android.content.pm.PackageManager; 25import android.content.pm.ResolveInfo; 26import android.content.res.Resources; 27import android.graphics.Bitmap; 28import android.graphics.Canvas; 29import android.graphics.drawable.Drawable; 30import android.os.AsyncTask; 31import android.os.Handler; 32import android.os.Process; 33import android.os.UserHandle; 34import android.util.Log; 35import android.view.MotionEvent; 36import android.view.View; 37 38import com.android.systemui.R; 39import com.android.systemui.statusbar.phone.PhoneStatusBar; 40import com.android.systemui.statusbar.tablet.TabletStatusBar; 41 42import java.util.ArrayList; 43import java.util.List; 44import java.util.concurrent.BlockingQueue; 45import java.util.concurrent.LinkedBlockingQueue; 46 47public 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 (mTaskLoader != null) { 290 mTaskLoader.cancel(false); 291 mTaskLoader = null; 292 } 293 if (mThumbnailLoader != null) { 294 mThumbnailLoader.cancel(false); 295 mThumbnailLoader = null; 296 } 297 mLoadedTasks = null; 298 if (mRecentsPanel != null) { 299 mRecentsPanel.onTaskLoadingCancelled(); 300 } 301 mFirstScreenful = false; 302 mState = State.CANCELLED; 303 } 304 305 private void clearFirstTask() { 306 synchronized (mFirstTaskLock) { 307 mFirstTask = null; 308 mFirstTaskLoaded = false; 309 } 310 } 311 312 public void preloadFirstTask() { 313 Thread bgLoad = new Thread() { 314 public void run() { 315 TaskDescription first = loadFirstTask(); 316 synchronized(mFirstTaskLock) { 317 if (mCancelPreloadingFirstTask) { 318 clearFirstTask(); 319 } else { 320 mFirstTask = first; 321 mFirstTaskLoaded = true; 322 } 323 mPreloadingFirstTask = false; 324 } 325 } 326 }; 327 synchronized(mFirstTaskLock) { 328 if (!mPreloadingFirstTask) { 329 clearFirstTask(); 330 mPreloadingFirstTask = true; 331 bgLoad.start(); 332 } 333 } 334 } 335 336 public void cancelPreloadingFirstTask() { 337 synchronized(mFirstTaskLock) { 338 if (mPreloadingFirstTask) { 339 mCancelPreloadingFirstTask = true; 340 } else { 341 clearFirstTask(); 342 } 343 } 344 } 345 346 boolean mPreloadingFirstTask; 347 boolean mCancelPreloadingFirstTask; 348 public TaskDescription getFirstTask() { 349 while(true) { 350 synchronized(mFirstTaskLock) { 351 if (mFirstTaskLoaded) { 352 return mFirstTask; 353 } else if (!mFirstTaskLoaded && !mPreloadingFirstTask) { 354 mFirstTask = loadFirstTask(); 355 mFirstTaskLoaded = true; 356 return mFirstTask; 357 } 358 } 359 try { 360 Thread.sleep(3); 361 } catch (InterruptedException e) { 362 } 363 } 364 } 365 366 public TaskDescription loadFirstTask() { 367 final ActivityManager am = (ActivityManager) mContext.getSystemService(Context.ACTIVITY_SERVICE); 368 369 final List<ActivityManager.RecentTaskInfo> recentTasks = am.getRecentTasksForUser( 370 1, ActivityManager.RECENT_IGNORE_UNAVAILABLE, UserHandle.CURRENT.getIdentifier()); 371 TaskDescription item = null; 372 if (recentTasks.size() > 0) { 373 ActivityManager.RecentTaskInfo recentInfo = recentTasks.get(0); 374 375 Intent intent = new Intent(recentInfo.baseIntent); 376 if (recentInfo.origActivity != null) { 377 intent.setComponent(recentInfo.origActivity); 378 } 379 380 // Don't load the current home activity. 381 if (isCurrentHomeActivity(intent.getComponent(), null)) { 382 return null; 383 } 384 385 // Don't load ourselves 386 if (intent.getComponent().getPackageName().equals(mContext.getPackageName())) { 387 return null; 388 } 389 390 item = createTaskDescription(recentInfo.id, 391 recentInfo.persistentId, recentInfo.baseIntent, 392 recentInfo.origActivity, recentInfo.description); 393 if (item != null) { 394 loadThumbnailAndIcon(item); 395 } 396 return item; 397 } 398 return null; 399 } 400 401 public void loadTasksInBackground() { 402 loadTasksInBackground(false); 403 } 404 public void loadTasksInBackground(final boolean zeroeth) { 405 if (mState != State.CANCELLED) { 406 return; 407 } 408 mState = State.LOADING; 409 mFirstScreenful = true; 410 411 final LinkedBlockingQueue<TaskDescription> tasksWaitingForThumbnails = 412 new LinkedBlockingQueue<TaskDescription>(); 413 mTaskLoader = new AsyncTask<Void, ArrayList<TaskDescription>, Void>() { 414 @Override 415 protected void onProgressUpdate(ArrayList<TaskDescription>... values) { 416 if (!isCancelled()) { 417 ArrayList<TaskDescription> newTasks = values[0]; 418 // do a callback to RecentsPanelView to let it know we have more values 419 // how do we let it know we're all done? just always call back twice 420 if (mRecentsPanel != null) { 421 mRecentsPanel.onTasksLoaded(newTasks, mFirstScreenful); 422 } 423 if (mLoadedTasks == null) { 424 mLoadedTasks = new ArrayList<TaskDescription>(); 425 } 426 mLoadedTasks.addAll(newTasks); 427 mFirstScreenful = false; 428 } 429 } 430 @Override 431 protected Void doInBackground(Void... params) { 432 // We load in two stages: first, we update progress with just the first screenful 433 // of items. Then, we update with the rest of the items 434 final int origPri = Process.getThreadPriority(Process.myTid()); 435 Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); 436 final PackageManager pm = mContext.getPackageManager(); 437 final ActivityManager am = (ActivityManager) 438 mContext.getSystemService(Context.ACTIVITY_SERVICE); 439 440 final List<ActivityManager.RecentTaskInfo> recentTasks = 441 am.getRecentTasks(MAX_TASKS, ActivityManager.RECENT_IGNORE_UNAVAILABLE); 442 int numTasks = recentTasks.size(); 443 ActivityInfo homeInfo = new Intent(Intent.ACTION_MAIN) 444 .addCategory(Intent.CATEGORY_HOME).resolveActivityInfo(pm, 0); 445 446 boolean firstScreenful = true; 447 ArrayList<TaskDescription> tasks = new ArrayList<TaskDescription>(); 448 449 // skip the first task - assume it's either the home screen or the current activity. 450 final int first = 0; 451 for (int i = first, index = 0; i < numTasks && (index < MAX_TASKS); ++i) { 452 if (isCancelled()) { 453 break; 454 } 455 final ActivityManager.RecentTaskInfo recentInfo = recentTasks.get(i); 456 457 Intent intent = new Intent(recentInfo.baseIntent); 458 if (recentInfo.origActivity != null) { 459 intent.setComponent(recentInfo.origActivity); 460 } 461 462 // Don't load the current home activity. 463 if (isCurrentHomeActivity(intent.getComponent(), homeInfo)) { 464 continue; 465 } 466 467 // Don't load ourselves 468 if (intent.getComponent().getPackageName().equals(mContext.getPackageName())) { 469 continue; 470 } 471 472 TaskDescription item = createTaskDescription(recentInfo.id, 473 recentInfo.persistentId, recentInfo.baseIntent, 474 recentInfo.origActivity, recentInfo.description); 475 476 if (item != null) { 477 while (true) { 478 try { 479 tasksWaitingForThumbnails.put(item); 480 break; 481 } catch (InterruptedException e) { 482 } 483 } 484 tasks.add(item); 485 if (firstScreenful && tasks.size() == mNumTasksInFirstScreenful) { 486 publishProgress(tasks); 487 tasks = new ArrayList<TaskDescription>(); 488 firstScreenful = false; 489 //break; 490 } 491 ++index; 492 } 493 } 494 495 if (!isCancelled()) { 496 publishProgress(tasks); 497 if (firstScreenful) { 498 // always should publish two updates 499 publishProgress(new ArrayList<TaskDescription>()); 500 } 501 } 502 503 while (true) { 504 try { 505 tasksWaitingForThumbnails.put(new TaskDescription()); 506 break; 507 } catch (InterruptedException e) { 508 } 509 } 510 511 Process.setThreadPriority(origPri); 512 return null; 513 } 514 }; 515 mTaskLoader.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); 516 loadThumbnailsAndIconsInBackground(tasksWaitingForThumbnails); 517 } 518 519 private void loadThumbnailsAndIconsInBackground( 520 final BlockingQueue<TaskDescription> tasksWaitingForThumbnails) { 521 // continually read items from tasksWaitingForThumbnails and load 522 // thumbnails and icons for them. finish thread when cancelled or there 523 // is a null item in tasksWaitingForThumbnails 524 mThumbnailLoader = new AsyncTask<Void, TaskDescription, Void>() { 525 @Override 526 protected void onProgressUpdate(TaskDescription... values) { 527 if (!isCancelled()) { 528 TaskDescription td = values[0]; 529 if (td.isNull()) { // end sentinel 530 mState = State.LOADED; 531 } else { 532 if (mRecentsPanel != null) { 533 mRecentsPanel.onTaskThumbnailLoaded(td); 534 } 535 } 536 } 537 } 538 @Override 539 protected Void doInBackground(Void... params) { 540 final int origPri = Process.getThreadPriority(Process.myTid()); 541 Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); 542 543 while (true) { 544 if (isCancelled()) { 545 break; 546 } 547 TaskDescription td = null; 548 while (td == null) { 549 try { 550 td = tasksWaitingForThumbnails.take(); 551 } catch (InterruptedException e) { 552 } 553 } 554 if (td.isNull()) { // end sentinel 555 publishProgress(td); 556 break; 557 } 558 loadThumbnailAndIcon(td); 559 560 publishProgress(td); 561 } 562 563 Process.setThreadPriority(origPri); 564 return null; 565 } 566 }; 567 mThumbnailLoader.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); 568 } 569} 570