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