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