• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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