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