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