RecentTasksLoader.java revision 4f0a49e6b9ad1b00972dbe8a751263aa6c482538
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.app.AppGlobals;
21import android.content.ComponentName;
22import android.content.Context;
23import android.content.Intent;
24import android.content.pm.ActivityInfo;
25import android.content.pm.IPackageManager;
26import android.content.pm.PackageManager;
27import android.content.pm.ResolveInfo;
28import android.content.res.Resources;
29import android.graphics.Bitmap;
30import android.graphics.drawable.BitmapDrawable;
31import android.graphics.drawable.Drawable;
32import android.os.AsyncTask;
33import android.os.Handler;
34import android.os.Process;
35import android.os.RemoteException;
36import android.os.UserHandle;
37import android.os.UserManager;
38import android.util.Log;
39import android.view.MotionEvent;
40import android.view.View;
41
42import com.android.systemui.R;
43import com.android.systemui.statusbar.phone.PhoneStatusBar;
44
45import java.util.ArrayList;
46import java.util.List;
47import java.util.concurrent.BlockingQueue;
48import java.util.concurrent.LinkedBlockingQueue;
49
50public 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;
81    public static RecentTasksLoader getInstance(Context context) {
82        if (sInstance == null) {
83            sInstance = new RecentTasksLoader(context);
84        }
85        return sInstance;
86    }
87
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
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
130    public Drawable getDefaultThumbnail() {
131        return mDefaultThumbnailBackground;
132    }
133
134    public Drawable getDefaultIcon() {
135        return mDefaultIconBackground;
136    }
137
138    public ArrayList<TaskDescription> getLoadedTasks() {
139        return mLoadedTasks;
140    }
141
142    public void remove(TaskDescription td) {
143        mLoadedTasks.remove(td);
144    }
145
146    public boolean isFirstScreenful() {
147        return mFirstScreenful;
148    }
149
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
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
198    void loadThumbnailAndIcon(TaskDescription td) {
199        final ActivityManager am = (ActivityManager)
200                mContext.getSystemService(Context.ACTIVITY_SERVICE);
201        final PackageManager pm = mContext.getPackageManager();
202        Bitmap thumbnail = am.getTaskTopThumbnail(td.persistentTaskId);
203        Drawable icon = getFullResIcon(td.resolveInfo, pm);
204        if (td.userId != UserHandle.myUserId()) {
205            // Need to badge the icon
206            final UserManager um = (UserManager) mContext.getSystemService(Context.USER_SERVICE);
207            icon = um.getBadgedDrawableForUser(icon, new UserHandle(td.userId));
208        }
209        if (DEBUG) Log.v(TAG, "Loaded bitmap for task "
210                + td + ": " + thumbnail);
211        synchronized (td) {
212            if (thumbnail != null) {
213                td.setThumbnail(new BitmapDrawable(mContext.getResources(), thumbnail));
214            } else {
215                td.setThumbnail(mDefaultThumbnailBackground);
216            }
217            if (icon != null) {
218                td.setIcon(icon);
219            }
220            td.setLoaded(true);
221        }
222    }
223
224    Drawable getFullResDefaultActivityIcon() {
225        return getFullResIcon(Resources.getSystem(),
226                com.android.internal.R.mipmap.sym_def_app_icon);
227    }
228
229    Drawable getFullResIcon(Resources resources, int iconId) {
230        try {
231            return resources.getDrawableForDensity(iconId, mIconDpi);
232        } catch (Resources.NotFoundException e) {
233            return getFullResDefaultActivityIcon();
234        }
235    }
236
237    private Drawable getFullResIcon(ResolveInfo info, PackageManager packageManager) {
238        Resources resources;
239        try {
240            resources = packageManager.getResourcesForApplication(
241                    info.activityInfo.applicationInfo);
242        } catch (PackageManager.NameNotFoundException e) {
243            resources = null;
244        }
245        if (resources != null) {
246            int iconId = info.activityInfo.getIconResource();
247            if (iconId != 0) {
248                return getFullResIcon(resources, iconId);
249            }
250        }
251        return getFullResDefaultActivityIcon();
252    }
253
254    Runnable mPreloadTasksRunnable = new Runnable() {
255            public void run() {
256                loadTasksInBackground();
257            }
258        };
259
260    // additional optimization when we have software system buttons - start loading the recent
261    // tasks on touch down
262    @Override
263    public boolean onTouch(View v, MotionEvent ev) {
264        int action = ev.getAction() & MotionEvent.ACTION_MASK;
265        if (action == MotionEvent.ACTION_DOWN) {
266            preloadRecentTasksList();
267        } else if (action == MotionEvent.ACTION_CANCEL) {
268            cancelPreloadingRecentTasksList();
269        } else if (action == MotionEvent.ACTION_UP) {
270            // Remove the preloader if we haven't called it yet
271            mHandler.removeCallbacks(mPreloadTasksRunnable);
272            if (!v.isPressed()) {
273                cancelLoadingThumbnailsAndIcons();
274            }
275
276        }
277        return false;
278    }
279
280    public void preloadRecentTasksList() {
281        mHandler.post(mPreloadTasksRunnable);
282    }
283
284    public void cancelPreloadingRecentTasksList() {
285        cancelLoadingThumbnailsAndIcons();
286        mHandler.removeCallbacks(mPreloadTasksRunnable);
287    }
288
289    public void cancelLoadingThumbnailsAndIcons(RecentsPanelView caller) {
290        // Only oblige this request if it comes from the current RecentsPanel
291        // (eg when you rotate, the old RecentsPanel request should be ignored)
292        if (mRecentsPanel == caller) {
293            cancelLoadingThumbnailsAndIcons();
294        }
295    }
296
297
298    private void cancelLoadingThumbnailsAndIcons() {
299        if (mRecentsPanel != null && mRecentsPanel.isShowing()) {
300            return;
301        }
302
303        if (mTaskLoader != null) {
304            mTaskLoader.cancel(false);
305            mTaskLoader = null;
306        }
307        if (mThumbnailLoader != null) {
308            mThumbnailLoader.cancel(false);
309            mThumbnailLoader = null;
310        }
311        mLoadedTasks = null;
312        if (mRecentsPanel != null) {
313            mRecentsPanel.onTaskLoadingCancelled();
314        }
315        mFirstScreenful = false;
316        mState = State.CANCELLED;
317    }
318
319    private void clearFirstTask() {
320        synchronized (mFirstTaskLock) {
321            mFirstTask = null;
322            mFirstTaskLoaded = false;
323        }
324    }
325
326    public void preloadFirstTask() {
327        Thread bgLoad = new Thread() {
328            public void run() {
329                TaskDescription first = loadFirstTask();
330                synchronized(mFirstTaskLock) {
331                    if (mCancelPreloadingFirstTask) {
332                        clearFirstTask();
333                    } else {
334                        mFirstTask = first;
335                        mFirstTaskLoaded = true;
336                    }
337                    mPreloadingFirstTask = false;
338                }
339            }
340        };
341        synchronized(mFirstTaskLock) {
342            if (!mPreloadingFirstTask) {
343                clearFirstTask();
344                mPreloadingFirstTask = true;
345                bgLoad.start();
346            }
347        }
348    }
349
350    public void cancelPreloadingFirstTask() {
351        synchronized(mFirstTaskLock) {
352            if (mPreloadingFirstTask) {
353                mCancelPreloadingFirstTask = true;
354            } else {
355                clearFirstTask();
356            }
357        }
358    }
359
360    boolean mPreloadingFirstTask;
361    boolean mCancelPreloadingFirstTask;
362    public TaskDescription getFirstTask() {
363        while(true) {
364            synchronized(mFirstTaskLock) {
365                if (mFirstTaskLoaded) {
366                    return mFirstTask;
367                } else if (!mFirstTaskLoaded && !mPreloadingFirstTask) {
368                    mFirstTask = loadFirstTask();
369                    mFirstTaskLoaded = true;
370                    return mFirstTask;
371                }
372            }
373            try {
374                Thread.sleep(3);
375            } catch (InterruptedException e) {
376            }
377        }
378    }
379
380    public TaskDescription loadFirstTask() {
381        final ActivityManager am = (ActivityManager) mContext.getSystemService(Context.ACTIVITY_SERVICE);
382
383        final List<ActivityManager.RecentTaskInfo> recentTasks = am.getRecentTasksForUser(1,
384                ActivityManager.RECENT_IGNORE_UNAVAILABLE | ActivityManager.RECENT_INCLUDE_PROFILES,
385                UserHandle.CURRENT.getIdentifier());
386        TaskDescription item = null;
387        if (recentTasks.size() > 0) {
388            ActivityManager.RecentTaskInfo recentInfo = recentTasks.get(0);
389
390            Intent intent = new Intent(recentInfo.baseIntent);
391            if (recentInfo.origActivity != null) {
392                intent.setComponent(recentInfo.origActivity);
393            }
394
395            // Don't load the current home activity.
396            if (isCurrentHomeActivity(intent.getComponent(), null)) {
397                return null;
398            }
399
400            // Don't load ourselves
401            if (intent.getComponent().getPackageName().equals(mContext.getPackageName())) {
402                return null;
403            }
404
405            item = createTaskDescription(recentInfo.id,
406                    recentInfo.persistentId, recentInfo.baseIntent,
407                    recentInfo.origActivity, recentInfo.description,
408                    recentInfo.userId);
409            if (item != null) {
410                loadThumbnailAndIcon(item);
411            }
412            return item;
413        }
414        return null;
415    }
416
417    public void loadTasksInBackground() {
418        loadTasksInBackground(false);
419    }
420    public void loadTasksInBackground(final boolean zeroeth) {
421        if (mState != State.CANCELLED) {
422            return;
423        }
424        mState = State.LOADING;
425        mFirstScreenful = true;
426
427        final LinkedBlockingQueue<TaskDescription> tasksWaitingForThumbnails =
428                new LinkedBlockingQueue<TaskDescription>();
429        mTaskLoader = new AsyncTask<Void, ArrayList<TaskDescription>, Void>() {
430            @Override
431            protected void onProgressUpdate(ArrayList<TaskDescription>... values) {
432                if (!isCancelled()) {
433                    ArrayList<TaskDescription> newTasks = values[0];
434                    // do a callback to RecentsPanelView to let it know we have more values
435                    // how do we let it know we're all done? just always call back twice
436                    if (mRecentsPanel != null) {
437                        mRecentsPanel.onTasksLoaded(newTasks, mFirstScreenful);
438                    }
439                    if (mLoadedTasks == null) {
440                        mLoadedTasks = new ArrayList<TaskDescription>();
441                    }
442                    mLoadedTasks.addAll(newTasks);
443                    mFirstScreenful = false;
444                }
445            }
446            @Override
447            protected Void doInBackground(Void... params) {
448                // We load in two stages: first, we update progress with just the first screenful
449                // of items. Then, we update with the rest of the items
450                final int origPri = Process.getThreadPriority(Process.myTid());
451                Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
452                final PackageManager pm = mContext.getPackageManager();
453                final ActivityManager am = (ActivityManager)
454                mContext.getSystemService(Context.ACTIVITY_SERVICE);
455
456                final List<ActivityManager.RecentTaskInfo> recentTasks =
457                        am.getRecentTasks(MAX_TASKS, ActivityManager.RECENT_IGNORE_UNAVAILABLE
458                        | ActivityManager.RECENT_INCLUDE_PROFILES);
459                int numTasks = recentTasks.size();
460                ActivityInfo homeInfo = new Intent(Intent.ACTION_MAIN)
461                        .addCategory(Intent.CATEGORY_HOME).resolveActivityInfo(pm, 0);
462
463                boolean firstScreenful = true;
464                ArrayList<TaskDescription> tasks = new ArrayList<TaskDescription>();
465
466                // skip the first task - assume it's either the home screen or the current activity.
467                final int first = 0;
468                for (int i = first, index = 0; i < numTasks && (index < MAX_TASKS); ++i) {
469                    if (isCancelled()) {
470                        break;
471                    }
472                    final ActivityManager.RecentTaskInfo recentInfo = recentTasks.get(i);
473
474                    Intent intent = new Intent(recentInfo.baseIntent);
475                    if (recentInfo.origActivity != null) {
476                        intent.setComponent(recentInfo.origActivity);
477                    }
478
479                    // Don't load the current home activity.
480                    if (isCurrentHomeActivity(intent.getComponent(), homeInfo)) {
481                        continue;
482                    }
483
484                    // Don't load ourselves
485                    if (intent.getComponent().getPackageName().equals(mContext.getPackageName())) {
486                        continue;
487                    }
488
489                    TaskDescription item = createTaskDescription(recentInfo.id,
490                            recentInfo.persistentId, recentInfo.baseIntent,
491                            recentInfo.origActivity, recentInfo.description,
492                            recentInfo.userId);
493
494                    if (item != null) {
495                        while (true) {
496                            try {
497                                tasksWaitingForThumbnails.put(item);
498                                break;
499                            } catch (InterruptedException e) {
500                            }
501                        }
502                        tasks.add(item);
503                        if (firstScreenful && tasks.size() == mNumTasksInFirstScreenful) {
504                            publishProgress(tasks);
505                            tasks = new ArrayList<TaskDescription>();
506                            firstScreenful = false;
507                            //break;
508                        }
509                        ++index;
510                    }
511                }
512
513                if (!isCancelled()) {
514                    publishProgress(tasks);
515                    if (firstScreenful) {
516                        // always should publish two updates
517                        publishProgress(new ArrayList<TaskDescription>());
518                    }
519                }
520
521                while (true) {
522                    try {
523                        tasksWaitingForThumbnails.put(new TaskDescription());
524                        break;
525                    } catch (InterruptedException e) {
526                    }
527                }
528
529                Process.setThreadPriority(origPri);
530                return null;
531            }
532        };
533        mTaskLoader.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
534        loadThumbnailsAndIconsInBackground(tasksWaitingForThumbnails);
535    }
536
537    private void loadThumbnailsAndIconsInBackground(
538            final BlockingQueue<TaskDescription> tasksWaitingForThumbnails) {
539        // continually read items from tasksWaitingForThumbnails and load
540        // thumbnails and icons for them. finish thread when cancelled or there
541        // is a null item in tasksWaitingForThumbnails
542        mThumbnailLoader = new AsyncTask<Void, TaskDescription, Void>() {
543            @Override
544            protected void onProgressUpdate(TaskDescription... values) {
545                if (!isCancelled()) {
546                    TaskDescription td = values[0];
547                    if (td.isNull()) { // end sentinel
548                        mState = State.LOADED;
549                    } else {
550                        if (mRecentsPanel != null) {
551                            mRecentsPanel.onTaskThumbnailLoaded(td);
552                        }
553                    }
554                }
555            }
556            @Override
557            protected Void doInBackground(Void... params) {
558                final int origPri = Process.getThreadPriority(Process.myTid());
559                Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
560
561                while (true) {
562                    if (isCancelled()) {
563                        break;
564                    }
565                    TaskDescription td = null;
566                    while (td == null) {
567                        try {
568                            td = tasksWaitingForThumbnails.take();
569                        } catch (InterruptedException e) {
570                        }
571                    }
572                    if (td.isNull()) { // end sentinel
573                        publishProgress(td);
574                        break;
575                    }
576                    loadThumbnailAndIcon(td);
577
578                    publishProgress(td);
579                }
580
581                Process.setThreadPriority(origPri);
582                return null;
583            }
584        };
585        mThumbnailLoader.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
586    }
587}
588