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.Canvas;
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;
40import com.android.systemui.statusbar.tablet.TabletStatusBar;
41
42import java.util.ArrayList;
43import java.util.List;
44import java.util.concurrent.BlockingQueue;
45import java.util.concurrent.LinkedBlockingQueue;
46
47public class RecentTasksLoader implements View.OnTouchListener {
48    static final String TAG = "RecentTasksLoader";
49    static final boolean DEBUG = TabletStatusBar.DEBUG || PhoneStatusBar.DEBUG || false;
50
51    private static final int DISPLAY_TASKS = 20;
52    private static final int MAX_TASKS = DISPLAY_TASKS + 1; // allow extra for non-apps
53
54    private Context mContext;
55    private RecentsPanelView mRecentsPanel;
56
57    private Object mFirstTaskLock = new Object();
58    private TaskDescription mFirstTask;
59    private boolean mFirstTaskLoaded;
60
61    private AsyncTask<Void, ArrayList<TaskDescription>, Void> mTaskLoader;
62    private AsyncTask<Void, TaskDescription, Void> mThumbnailLoader;
63    private Handler mHandler;
64
65    private int mIconDpi;
66    private Bitmap mDefaultThumbnailBackground;
67    private Bitmap mDefaultIconBackground;
68    private int mNumTasksInFirstScreenful = Integer.MAX_VALUE;
69
70    private boolean mFirstScreenful;
71    private ArrayList<TaskDescription> mLoadedTasks;
72
73    private enum State { LOADING, LOADED, CANCELLED };
74    private State mState = State.CANCELLED;
75
76
77    private static RecentTasksLoader sInstance;
78    public static RecentTasksLoader getInstance(Context context) {
79        if (sInstance == null) {
80            sInstance = new RecentTasksLoader(context);
81        }
82        return sInstance;
83    }
84
85    private RecentTasksLoader(Context context) {
86        mContext = context;
87        mHandler = new Handler();
88
89        final Resources res = context.getResources();
90
91        // get the icon size we want -- on tablets, we use bigger icons
92        boolean isTablet = res.getBoolean(R.bool.config_recents_interface_for_tablets);
93        if (isTablet) {
94            ActivityManager activityManager =
95                    (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
96            mIconDpi = activityManager.getLauncherLargeIconDensity();
97        } else {
98            mIconDpi = res.getDisplayMetrics().densityDpi;
99        }
100
101        // Render default icon (just a blank image)
102        int defaultIconSize = res.getDimensionPixelSize(com.android.internal.R.dimen.app_icon_size);
103        int iconSize = (int) (defaultIconSize * mIconDpi / res.getDisplayMetrics().densityDpi);
104        mDefaultIconBackground = Bitmap.createBitmap(iconSize, iconSize, Bitmap.Config.ARGB_8888);
105
106        // Render the default thumbnail background
107        int thumbnailWidth =
108                (int) res.getDimensionPixelSize(com.android.internal.R.dimen.thumbnail_width);
109        int thumbnailHeight =
110                (int) res.getDimensionPixelSize(com.android.internal.R.dimen.thumbnail_height);
111        int color = res.getColor(R.drawable.status_bar_recents_app_thumbnail_background);
112
113        mDefaultThumbnailBackground =
114                Bitmap.createBitmap(thumbnailWidth, thumbnailHeight, Bitmap.Config.ARGB_8888);
115        Canvas c = new Canvas(mDefaultThumbnailBackground);
116        c.drawColor(color);
117    }
118
119    public void setRecentsPanel(RecentsPanelView newRecentsPanel, RecentsPanelView caller) {
120        // Only allow clearing mRecentsPanel if the caller is the current recentsPanel
121        if (newRecentsPanel != null || mRecentsPanel == caller) {
122            mRecentsPanel = newRecentsPanel;
123            if (mRecentsPanel != null) {
124                mNumTasksInFirstScreenful = mRecentsPanel.numItemsInOneScreenful();
125            }
126        }
127    }
128
129    public Bitmap getDefaultThumbnail() {
130        return mDefaultThumbnailBackground;
131    }
132
133    public Bitmap getDefaultIcon() {
134        return mDefaultIconBackground;
135    }
136
137    public ArrayList<TaskDescription> getLoadedTasks() {
138        return mLoadedTasks;
139    }
140
141    public void remove(TaskDescription td) {
142        mLoadedTasks.remove(td);
143    }
144
145    public boolean isFirstScreenful() {
146        return mFirstScreenful;
147    }
148
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
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
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
214    Drawable getFullResDefaultActivityIcon() {
215        return getFullResIcon(Resources.getSystem(),
216                com.android.internal.R.mipmap.sym_def_app_icon);
217    }
218
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
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
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
270    public void preloadRecentTasksList() {
271        mHandler.post(mPreloadTasksRunnable);
272    }
273
274    public void cancelPreloadingRecentTasksList() {
275        cancelLoadingThumbnailsAndIcons();
276        mHandler.removeCallbacks(mPreloadTasksRunnable);
277    }
278
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
288    private void cancelLoadingThumbnailsAndIcons() {
289        if (mTaskLoader != null) {
290            mTaskLoader.cancel(false);
291            mTaskLoader = null;
292        }
293        if (mThumbnailLoader != null) {
294            mThumbnailLoader.cancel(false);
295            mThumbnailLoader = null;
296        }
297        mLoadedTasks = null;
298        if (mRecentsPanel != null) {
299            mRecentsPanel.onTaskLoadingCancelled();
300        }
301        mFirstScreenful = false;
302        mState = State.CANCELLED;
303    }
304
305    private void clearFirstTask() {
306        synchronized (mFirstTaskLock) {
307            mFirstTask = null;
308            mFirstTaskLoaded = false;
309        }
310    }
311
312    public void preloadFirstTask() {
313        Thread bgLoad = new Thread() {
314            public void run() {
315                TaskDescription first = loadFirstTask();
316                synchronized(mFirstTaskLock) {
317                    if (mCancelPreloadingFirstTask) {
318                        clearFirstTask();
319                    } else {
320                        mFirstTask = first;
321                        mFirstTaskLoaded = true;
322                    }
323                    mPreloadingFirstTask = false;
324                }
325            }
326        };
327        synchronized(mFirstTaskLock) {
328            if (!mPreloadingFirstTask) {
329                clearFirstTask();
330                mPreloadingFirstTask = true;
331                bgLoad.start();
332            }
333        }
334    }
335
336    public void cancelPreloadingFirstTask() {
337        synchronized(mFirstTaskLock) {
338            if (mPreloadingFirstTask) {
339                mCancelPreloadingFirstTask = true;
340            } else {
341                clearFirstTask();
342            }
343        }
344    }
345
346    boolean mPreloadingFirstTask;
347    boolean mCancelPreloadingFirstTask;
348    public TaskDescription getFirstTask() {
349        while(true) {
350            synchronized(mFirstTaskLock) {
351                if (mFirstTaskLoaded) {
352                    return mFirstTask;
353                } else if (!mFirstTaskLoaded && !mPreloadingFirstTask) {
354                    mFirstTask = loadFirstTask();
355                    mFirstTaskLoaded = true;
356                    return mFirstTask;
357                }
358            }
359            try {
360                Thread.sleep(3);
361            } catch (InterruptedException e) {
362            }
363        }
364    }
365
366    public TaskDescription loadFirstTask() {
367        final ActivityManager am = (ActivityManager) mContext.getSystemService(Context.ACTIVITY_SERVICE);
368
369        final List<ActivityManager.RecentTaskInfo> recentTasks = am.getRecentTasksForUser(
370                1, ActivityManager.RECENT_IGNORE_UNAVAILABLE, UserHandle.CURRENT.getIdentifier());
371        TaskDescription item = null;
372        if (recentTasks.size() > 0) {
373            ActivityManager.RecentTaskInfo recentInfo = recentTasks.get(0);
374
375            Intent intent = new Intent(recentInfo.baseIntent);
376            if (recentInfo.origActivity != null) {
377                intent.setComponent(recentInfo.origActivity);
378            }
379
380            // Don't load the current home activity.
381            if (isCurrentHomeActivity(intent.getComponent(), null)) {
382                return null;
383            }
384
385            // Don't load ourselves
386            if (intent.getComponent().getPackageName().equals(mContext.getPackageName())) {
387                return null;
388            }
389
390            item = createTaskDescription(recentInfo.id,
391                    recentInfo.persistentId, recentInfo.baseIntent,
392                    recentInfo.origActivity, recentInfo.description);
393            if (item != null) {
394                loadThumbnailAndIcon(item);
395            }
396            return item;
397        }
398        return null;
399    }
400
401    public void loadTasksInBackground() {
402        loadTasksInBackground(false);
403    }
404    public void loadTasksInBackground(final boolean zeroeth) {
405        if (mState != State.CANCELLED) {
406            return;
407        }
408        mState = State.LOADING;
409        mFirstScreenful = true;
410
411        final LinkedBlockingQueue<TaskDescription> tasksWaitingForThumbnails =
412                new LinkedBlockingQueue<TaskDescription>();
413        mTaskLoader = new AsyncTask<Void, ArrayList<TaskDescription>, Void>() {
414            @Override
415            protected void onProgressUpdate(ArrayList<TaskDescription>... values) {
416                if (!isCancelled()) {
417                    ArrayList<TaskDescription> newTasks = values[0];
418                    // do a callback to RecentsPanelView to let it know we have more values
419                    // how do we let it know we're all done? just always call back twice
420                    if (mRecentsPanel != null) {
421                        mRecentsPanel.onTasksLoaded(newTasks, mFirstScreenful);
422                    }
423                    if (mLoadedTasks == null) {
424                        mLoadedTasks = new ArrayList<TaskDescription>();
425                    }
426                    mLoadedTasks.addAll(newTasks);
427                    mFirstScreenful = false;
428                }
429            }
430            @Override
431            protected Void doInBackground(Void... params) {
432                // We load in two stages: first, we update progress with just the first screenful
433                // of items. Then, we update with the rest of the items
434                final int origPri = Process.getThreadPriority(Process.myTid());
435                Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
436                final PackageManager pm = mContext.getPackageManager();
437                final ActivityManager am = (ActivityManager)
438                mContext.getSystemService(Context.ACTIVITY_SERVICE);
439
440                final List<ActivityManager.RecentTaskInfo> recentTasks =
441                        am.getRecentTasks(MAX_TASKS, ActivityManager.RECENT_IGNORE_UNAVAILABLE);
442                int numTasks = recentTasks.size();
443                ActivityInfo homeInfo = new Intent(Intent.ACTION_MAIN)
444                        .addCategory(Intent.CATEGORY_HOME).resolveActivityInfo(pm, 0);
445
446                boolean firstScreenful = true;
447                ArrayList<TaskDescription> tasks = new ArrayList<TaskDescription>();
448
449                // skip the first task - assume it's either the home screen or the current activity.
450                final int first = 0;
451                for (int i = first, index = 0; i < numTasks && (index < MAX_TASKS); ++i) {
452                    if (isCancelled()) {
453                        break;
454                    }
455                    final ActivityManager.RecentTaskInfo recentInfo = recentTasks.get(i);
456
457                    Intent intent = new Intent(recentInfo.baseIntent);
458                    if (recentInfo.origActivity != null) {
459                        intent.setComponent(recentInfo.origActivity);
460                    }
461
462                    // Don't load the current home activity.
463                    if (isCurrentHomeActivity(intent.getComponent(), homeInfo)) {
464                        continue;
465                    }
466
467                    // Don't load ourselves
468                    if (intent.getComponent().getPackageName().equals(mContext.getPackageName())) {
469                        continue;
470                    }
471
472                    TaskDescription item = createTaskDescription(recentInfo.id,
473                            recentInfo.persistentId, recentInfo.baseIntent,
474                            recentInfo.origActivity, recentInfo.description);
475
476                    if (item != null) {
477                        while (true) {
478                            try {
479                                tasksWaitingForThumbnails.put(item);
480                                break;
481                            } catch (InterruptedException e) {
482                            }
483                        }
484                        tasks.add(item);
485                        if (firstScreenful && tasks.size() == mNumTasksInFirstScreenful) {
486                            publishProgress(tasks);
487                            tasks = new ArrayList<TaskDescription>();
488                            firstScreenful = false;
489                            //break;
490                        }
491                        ++index;
492                    }
493                }
494
495                if (!isCancelled()) {
496                    publishProgress(tasks);
497                    if (firstScreenful) {
498                        // always should publish two updates
499                        publishProgress(new ArrayList<TaskDescription>());
500                    }
501                }
502
503                while (true) {
504                    try {
505                        tasksWaitingForThumbnails.put(new TaskDescription());
506                        break;
507                    } catch (InterruptedException e) {
508                    }
509                }
510
511                Process.setThreadPriority(origPri);
512                return null;
513            }
514        };
515        mTaskLoader.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
516        loadThumbnailsAndIconsInBackground(tasksWaitingForThumbnails);
517    }
518
519    private void loadThumbnailsAndIconsInBackground(
520            final BlockingQueue<TaskDescription> tasksWaitingForThumbnails) {
521        // continually read items from tasksWaitingForThumbnails and load
522        // thumbnails and icons for them. finish thread when cancelled or there
523        // is a null item in tasksWaitingForThumbnails
524        mThumbnailLoader = new AsyncTask<Void, TaskDescription, Void>() {
525            @Override
526            protected void onProgressUpdate(TaskDescription... values) {
527                if (!isCancelled()) {
528                    TaskDescription td = values[0];
529                    if (td.isNull()) { // end sentinel
530                        mState = State.LOADED;
531                    } else {
532                        if (mRecentsPanel != null) {
533                            mRecentsPanel.onTaskThumbnailLoaded(td);
534                        }
535                    }
536                }
537            }
538            @Override
539            protected Void doInBackground(Void... params) {
540                final int origPri = Process.getThreadPriority(Process.myTid());
541                Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
542
543                while (true) {
544                    if (isCancelled()) {
545                        break;
546                    }
547                    TaskDescription td = null;
548                    while (td == null) {
549                        try {
550                            td = tasksWaitingForThumbnails.take();
551                        } catch (InterruptedException e) {
552                        }
553                    }
554                    if (td.isNull()) { // end sentinel
555                        publishProgress(td);
556                        break;
557                    }
558                    loadThumbnailAndIcon(td);
559
560                    publishProgress(td);
561                }
562
563                Process.setThreadPriority(origPri);
564                return null;
565            }
566        };
567        mThumbnailLoader.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
568    }
569}
570