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