blob: dfff8c92fdb738c6d84a387d24d40d98706b867d [file] [log] [blame]
/*
* Copyright (C) 2006 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settings;
import com.android.settings.R;
import android.app.ActivityManager;
import android.app.Dialog;
import android.app.ProgressDialog;
import android.app.TabActivity;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.pm.ApplicationInfo;
import android.content.pm.IPackageStatsObserver;
import android.content.pm.PackageManager;
import android.content.pm.PackageStats;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.os.SystemClock;
import android.text.format.Formatter;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
import android.widget.AdapterView;
import android.widget.BaseAdapter;
import android.widget.Filter;
import android.widget.Filterable;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.TabHost;
import android.widget.TextView;
import android.widget.AdapterView.OnItemClickListener;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.text.Collator;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.concurrent.CountDownLatch;
/**
* Activity to pick an application that will be used to display installation information and
* options to uninstall/delete user data for system applications. This activity
* can be launched through Settings or via the ACTION_MANAGE_PACKAGE_STORAGE
* intent.
*
* Initially a compute in progress message is displayed while the application retrieves
* the list of application information from the PackageManager. The size information
* for each package is refreshed to the screen. The resource (app description and
* icon) information for each package is not available yet, so some default values for size
* icon and descriptions are used initially. Later the resource information for each
* application is retrieved and dynamically updated on the screen.
*
* A Broadcast receiver registers for package additions or deletions when the activity is
* in focus. If the user installs or deletes packages when the activity has focus, the receiver
* gets notified and proceeds to add/delete these packages from the list on the screen.
* This is an unlikely scenario but could happen. The entire list gets created every time
* the activity's onStart gets invoked. This is to avoid having the receiver for the entire
* life cycle of the application.
*
* The applications can be sorted either alphabetically or
* based on size (descending). If this activity gets launched under low memory
* situations (a low memory notification dispatches intent
* ACTION_MANAGE_PACKAGE_STORAGE) the list is sorted per size.
*
* If the user selects an application, extended info (like size, uninstall/clear data options,
* permissions info etc.,) is displayed via the InstalledAppDetails activity.
*/
public class ManageApplications extends TabActivity implements
OnItemClickListener, DialogInterface.OnCancelListener,
TabHost.TabContentFactory,
TabHost.OnTabChangeListener {
// TAG for this activity
private static final String TAG = "ManageApplications";
private static final String PREFS_NAME = "ManageAppsInfo.prefs";
private static final String PREF_DISABLE_CACHE = "disableCache";
// Log information boolean
private boolean localLOGV = false;
private static final boolean DEBUG_SIZE = false;
private static final boolean DEBUG_TIME = false;
// attributes used as keys when passing values to InstalledAppDetails activity
public static final String APP_PKG_NAME = "pkg";
public static final String APP_CHG = "chg";
// attribute name used in receiver for tagging names of added/deleted packages
private static final String ATTR_PKG_NAME="p";
private static final String ATTR_PKGS="ps";
private static final String ATTR_STATS="ss";
private static final String ATTR_SIZE_STRS="fs";
private static final String ATTR_GET_SIZE_STATUS="passed";
private static final String ATTR_PKG_STATS="s";
private static final String ATTR_PKG_SIZE_STR="f";
// constant value that can be used to check return code from sub activity.
private static final int INSTALLED_APP_DETAILS = 1;
// sort order that can be changed through the menu can be sorted alphabetically
// or size(descending)
private static final int MENU_OPTIONS_BASE = 0;
// Filter options used for displayed list of applications
public static final int FILTER_APPS_ALL = MENU_OPTIONS_BASE + 0;
public static final int FILTER_APPS_RUNNING = MENU_OPTIONS_BASE + 1;
public static final int FILTER_APPS_THIRD_PARTY = MENU_OPTIONS_BASE + 2;
public static final int FILTER_APPS_SDCARD = MENU_OPTIONS_BASE + 3;
public static final int SORT_ORDER_ALPHA = MENU_OPTIONS_BASE + 4;
public static final int SORT_ORDER_SIZE = MENU_OPTIONS_BASE + 5;
// sort order
private int mSortOrder = SORT_ORDER_ALPHA;
// Filter value
private int mFilterApps = FILTER_APPS_THIRD_PARTY;
// Custom Adapter used for managing items in the list
private AppInfoAdapter mAppInfoAdapter;
// messages posted to the handler
private static final int HANDLER_MESSAGE_BASE = 0;
private static final int INIT_PKG_INFO = HANDLER_MESSAGE_BASE+1;
private static final int COMPUTE_BULK_SIZE = HANDLER_MESSAGE_BASE+2;
private static final int REMOVE_PKG = HANDLER_MESSAGE_BASE+3;
private static final int REORDER_LIST = HANDLER_MESSAGE_BASE+4;
private static final int ADD_PKG_START = HANDLER_MESSAGE_BASE+5;
private static final int ADD_PKG_DONE = HANDLER_MESSAGE_BASE+6;
private static final int REFRESH_LABELS = HANDLER_MESSAGE_BASE+7;
private static final int REFRESH_DONE = HANDLER_MESSAGE_BASE+8;
private static final int NEXT_LOAD_STEP = HANDLER_MESSAGE_BASE+9;
private static final int COMPUTE_END = HANDLER_MESSAGE_BASE+10;
private static final int REFRESH_ICONS = HANDLER_MESSAGE_BASE+11;
// observer object used for computing pkg sizes
private PkgSizeObserver mObserver;
// local handle to PackageManager
private PackageManager mPm;
// Broadcast Receiver object that receives notifications for added/deleted
// packages
private PackageIntentReceiver mReceiver;
// atomic variable used to track if computing pkg sizes is in progress. should be volatile?
private boolean mComputeSizesFinished = false;
// default icon thats used when displaying applications initially before resource info is
// retrieved
private static Drawable mDefaultAppIcon;
// temporary dialog displayed while the application info loads
private static final int DLG_BASE = 0;
private static final int DLG_LOADING = DLG_BASE + 1;
// Size resource used for packages whose size computation failed for some reason
private CharSequence mInvalidSizeStr;
private CharSequence mComputingSizeStr;
// map used to store list of added and removed packages. Immutable Boolean
// variables indicate if a package has been added or removed. If a package is
// added or deleted multiple times a single entry with the latest operation will
// be recorded in the map.
private Map<String, Boolean> mAddRemoveMap;
// layout inflater object used to inflate views
private LayoutInflater mInflater;
// invalid size value used initially and also when size retrieval through PackageManager
// fails for whatever reason
private static final int SIZE_INVALID = -1;
// debug boolean variable to test delays from PackageManager API's
private boolean DEBUG_PKG_DELAY = false;
// Thread to load resources
ResourceLoaderThread mResourceThread;
private TaskRunner mSizeComputor;
private String mCurrentPkgName;
// Cache application attributes
private AppInfoCache mCache = new AppInfoCache();
// Boolean variables indicating state
private boolean mLoadLabelsFinished = false;
private boolean mSizesFirst = false;
// ListView used to display list
private ListView mListView;
// State variables used to figure out menu options and also
// initiate the first computation and loading of resources
private boolean mJustCreated = true;
private boolean mFirst = false;
private long mLoadTimeStart;
private boolean mSetListViewLater = true;
/*
* Handler class to handle messages for various operations.
* Most of the operations that effect Application related data
* are posted as messages to the handler to avoid synchronization
* when accessing these structures.
*
* When the size retrieval gets kicked off for the first time, a COMPUTE_PKG_SIZE_START
* message is posted to the handler which invokes the getSizeInfo for the pkg at index 0.
*
* When the PackageManager's asynchronous call back through
* PkgSizeObserver.onGetStatsCompleted gets invoked, the application resources like
* label, description, icon etc., are loaded in the same thread and these values are
* set on the observer. The observer then posts a COMPUTE_PKG_SIZE_DONE message
* to the handler. This information is updated on the AppInfoAdapter associated with
* the list view of this activity and size info retrieval is initiated for the next package as
* indicated by mComputeIndex.
*
* When a package gets added while the activity has focus, the PkgSizeObserver posts
* ADD_PKG_START message to the handler. If the computation is not in progress, the size
* is retrieved for the newly added package through the observer object and the newly
* installed app info is updated on the screen. If the computation is still in progress
* the package is added to an internal structure and action deferred till the computation
* is done for all the packages.
*
* When a package gets deleted, REMOVE_PKG is posted to the handler
* if computation is not in progress (as indicated by
* mDoneIniting), the package is deleted from the displayed list of apps. If computation is
* still in progress the package is added to an internal structure and action deferred till
* the computation is done for all packages.
*
* When the sizes of all packages is computed, the newly
* added or removed packages are processed in order.
* If the user changes the order in which these applications are viewed by hitting the
* menu key, REORDER_LIST message is posted to the handler. this sorts the list
* of items based on the sort order.
*/
private Handler mHandler = new Handler() {
public void handleMessage(Message msg) {
boolean status;
long size;
String formattedSize;
ApplicationInfo info;
Bundle data;
String pkgName = null;
AppInfo appInfo;
data = msg.getData();
if(data != null) {
pkgName = data.getString(ATTR_PKG_NAME);
}
switch (msg.what) {
case INIT_PKG_INFO:
if(localLOGV) Log.i(TAG, "Message INIT_PKG_INFO, justCreated = " + mJustCreated);
List<ApplicationInfo> newList = null;
if (!mJustCreated) {
if (localLOGV) Log.i(TAG, "List already created");
// Add or delete newly created packages by comparing lists
newList = getInstalledApps(FILTER_APPS_ALL);
updateAppList(newList);
}
// Retrieve the package list and init some structures
initAppList(newList, mFilterApps);
mHandler.sendEmptyMessage(NEXT_LOAD_STEP);
break;
case COMPUTE_BULK_SIZE:
if(localLOGV) Log.i(TAG, "Message COMPUTE_BULK_PKG_SIZE");
String[] pkgs = data.getStringArray(ATTR_PKGS);
long[] sizes = data.getLongArray(ATTR_STATS);
String[] formatted = data.getStringArray(ATTR_SIZE_STRS);
if(pkgs == null || sizes == null || formatted == null) {
Log.w(TAG, "Ignoring message");
break;
}
mAppInfoAdapter.bulkUpdateSizes(pkgs, sizes, formatted);
break;
case COMPUTE_END:
mComputeSizesFinished = true;
mFirst = true;
mHandler.sendEmptyMessage(NEXT_LOAD_STEP);
break;
case REMOVE_PKG:
if(localLOGV) Log.i(TAG, "Message REMOVE_PKG");
if(pkgName == null) {
Log.w(TAG, "Ignoring message:REMOVE_PKG for null pkgName");
break;
}
if (!mComputeSizesFinished) {
Boolean currB = mAddRemoveMap.get(pkgName);
if (currB == null || (currB.equals(Boolean.TRUE))) {
mAddRemoveMap.put(pkgName, Boolean.FALSE);
}
break;
}
List<String> pkgList = new ArrayList<String>();
pkgList.add(pkgName);
mAppInfoAdapter.removeFromList(pkgList);
break;
case REORDER_LIST:
if(localLOGV) Log.i(TAG, "Message REORDER_LIST");
int menuOption = msg.arg1;
if((menuOption == SORT_ORDER_ALPHA) ||
(menuOption == SORT_ORDER_SIZE)) {
// Option to sort list
if (menuOption != mSortOrder) {
mSortOrder = menuOption;
if (localLOGV) Log.i(TAG, "Changing sort order to "+mSortOrder);
mAppInfoAdapter.sortList(mSortOrder);
}
} else if(menuOption != mFilterApps) {
// Option to filter list
mFilterApps = menuOption;
boolean ret = mAppInfoAdapter.resetAppList(mFilterApps);
if(!ret) {
// Reset cache
mFilterApps = FILTER_APPS_ALL;
mHandler.sendEmptyMessage(INIT_PKG_INFO);
sendMessageToHandler(REORDER_LIST, menuOption);
}
}
break;
case ADD_PKG_START:
if(localLOGV) Log.i(TAG, "Message ADD_PKG_START");
if(pkgName == null) {
Log.w(TAG, "Ignoring message:ADD_PKG_START for null pkgName");
break;
}
if (!mComputeSizesFinished || !mLoadLabelsFinished) {
Boolean currB = mAddRemoveMap.get(pkgName);
if (currB == null || (currB.equals(Boolean.FALSE))) {
mAddRemoveMap.put(pkgName, Boolean.TRUE);
}
break;
}
try {
info = mPm.getApplicationInfo(pkgName, 0);
} catch (NameNotFoundException e) {
Log.w(TAG, "Couldnt find application info for:"+pkgName);
break;
}
mObserver.invokeGetSizeInfo(pkgName);
break;
case ADD_PKG_DONE:
if(localLOGV) Log.i(TAG, "Message ADD_PKG_DONE");
if(pkgName == null) {
Log.w(TAG, "Ignoring message:ADD_PKG_START for null pkgName");
break;
}
status = data.getBoolean(ATTR_GET_SIZE_STATUS);
if (status) {
size = data.getLong(ATTR_PKG_STATS);
formattedSize = data.getString(ATTR_PKG_SIZE_STR);
if (!mAppInfoAdapter.isInstalled(pkgName)) {
mAppInfoAdapter.addToList(pkgName, size, formattedSize);
} else {
mAppInfoAdapter.updatePackage(pkgName, size, formattedSize);
}
}
break;
case REFRESH_LABELS:
Map<String, CharSequence> labelMap = (Map<String, CharSequence>) msg.obj;
if (labelMap != null) {
mAppInfoAdapter.bulkUpdateLabels(labelMap);
}
break;
case REFRESH_ICONS:
Map<String, Drawable> iconMap = (Map<String, Drawable>) msg.obj;
if (iconMap != null) {
mAppInfoAdapter.bulkUpdateIcons(iconMap);
}
break;
case REFRESH_DONE:
mLoadLabelsFinished = true;
mHandler.sendEmptyMessage(NEXT_LOAD_STEP);
break;
case NEXT_LOAD_STEP:
if (!mCache.isEmpty() && mSetListViewLater) {
if (localLOGV) Log.i(TAG, "Using cache to populate list view");
initListView();
mSetListViewLater = false;
mFirst = true;
}
if (mComputeSizesFinished && mLoadLabelsFinished) {
doneLoadingData();
// Check for added/removed packages
Set<String> keys = mAddRemoveMap.keySet();
for (String key : keys) {
if (mAddRemoveMap.get(key) == Boolean.TRUE) {
// Add the package
updatePackageList(Intent.ACTION_PACKAGE_ADDED, key);
} else {
// Remove the package
updatePackageList(Intent.ACTION_PACKAGE_REMOVED, key);
}
}
mAddRemoveMap.clear();
} else if (!mComputeSizesFinished && !mLoadLabelsFinished) {
// Either load the package labels or initiate get size info
if (mSizesFirst) {
initComputeSizes();
} else {
initResourceThread();
}
} else {
if (mSetListViewLater) {
if (localLOGV) Log.i(TAG, "Initing list view for very first time");
initListView();
mSetListViewLater = false;
}
if (!mComputeSizesFinished) {
initComputeSizes();
} else if (!mLoadLabelsFinished) {
initResourceThread();
}
}
break;
default:
break;
}
}
};
private void initListView() {
// Create list view from the adapter here. Wait till the sort order
// of list is defined. its either by label or by size. So atleast one of the
// first steps should have been completed before the list gets filled.
mAppInfoAdapter.sortBaseList(mSortOrder);
if (mJustCreated) {
// Set the adapter here.
mJustCreated = false;
mListView.setAdapter(mAppInfoAdapter);
dismissLoadingMsg();
}
}
class SizeObserver extends IPackageStatsObserver.Stub {
private CountDownLatch mCount;
PackageStats stats;
boolean succeeded;
public void invokeGetSize(String packageName, CountDownLatch count) {
mCount = count;
mPm.getPackageSizeInfo(packageName, this);
}
public void onGetStatsCompleted(PackageStats pStats, boolean pSucceeded) {
succeeded = pSucceeded;
stats = pStats;
mCount.countDown();
}
}
class TaskRunner extends Thread {
private List<ApplicationInfo> mPkgList;
private SizeObserver mSizeObserver;
private static final int END_MSG = COMPUTE_END;
private static final int SEND_PKG_SIZES = COMPUTE_BULK_SIZE;
volatile boolean abort = false;
static final int MSG_PKG_SIZE = 8;
TaskRunner(List<ApplicationInfo> appList) {
mPkgList = appList;
mSizeObserver = new SizeObserver();
start();
}
public void setAbort() {
abort = true;
}
public void run() {
long startTime;
if (DEBUG_SIZE || DEBUG_TIME) {
startTime = SystemClock.elapsedRealtime();
}
int size = mPkgList.size();
int numMsgs = size / MSG_PKG_SIZE;
if (size > (numMsgs * MSG_PKG_SIZE)) {
numMsgs++;
}
int endi = 0;
for (int j = 0; j < size; j += MSG_PKG_SIZE) {
long sizes[];
String formatted[];
String packages[];
endi += MSG_PKG_SIZE;
if (endi > size) {
endi = size;
}
sizes = new long[endi-j];
formatted = new String[endi-j];
packages = new String[endi-j];
for (int i = j; i < endi; i++) {
if (abort) {
// Exit if abort has been set.
break;
}
CountDownLatch count = new CountDownLatch(1);
String packageName = mPkgList.get(i).packageName;
mSizeObserver.invokeGetSize(packageName, count);
try {
count.await();
} catch (InterruptedException e) {
Log.i(TAG, "Failed computing size for pkg : "+packageName);
}
// Process the package statistics
PackageStats pStats = mSizeObserver.stats;
boolean succeeded = mSizeObserver.succeeded;
long total;
if(succeeded && pStats != null) {
total = getTotalSize(pStats);
} else {
total = SIZE_INVALID;
}
sizes[i-j] = total;
formatted[i-j] = getSizeStr(total).toString();
packages[i-j] = packageName;
}
// Post update message
Bundle data = new Bundle();
data.putStringArray(ATTR_PKGS, packages);
data.putLongArray(ATTR_STATS, sizes);
data.putStringArray(ATTR_SIZE_STRS, formatted);
Message msg = mHandler.obtainMessage(SEND_PKG_SIZES, data);
msg.setData(data);
mHandler.sendMessage(msg);
}
if (DEBUG_SIZE || DEBUG_TIME) Log.i(TAG, "Took "+
(SystemClock.elapsedRealtime() - startTime)+
" ms to compute sizes of all packages ");
mHandler.sendEmptyMessage(END_MSG);
}
}
/*
* This method compares the current cache against a new list of
* installed applications and tries to update the list with add or remove
* messages.
*/
private boolean updateAppList(List<ApplicationInfo> newList) {
if ((newList == null) || mCache.isEmpty()) {
return false;
}
Set<String> existingList = new HashSet<String>();
boolean ret = false;
// Loop over new list and find out common elements between old and new lists
int N = newList.size();
for (int i = (N-1); i >= 0; i--) {
ApplicationInfo info = newList.get(i);
String pkgName = info.packageName;
AppInfo aInfo = mCache.getEntry(pkgName);
if (aInfo != null) {
existingList.add(pkgName);
} else {
// New package. update info by refreshing
if (localLOGV) Log.i(TAG, "New pkg :"+pkgName+" installed when paused");
updatePackageList(Intent.ACTION_PACKAGE_ADDED, pkgName);
// Remove from current list so that the newly added package can
// be handled later
newList.remove(i);
ret = true;
}
}
// Loop over old list and figure out stale entries
List<String> deletedList = null;
Set<String> staleList = mCache.getPkgList();
for (String pkgName : staleList) {
if (!existingList.contains(pkgName)) {
if (localLOGV) Log.i(TAG, "Pkg :"+pkgName+" deleted when paused");
if (deletedList == null) {
deletedList = new ArrayList<String>();
deletedList.add(pkgName);
}
ret = true;
}
}
// Delete right away
if (deletedList != null) {
if (localLOGV) Log.i(TAG, "Deleting right away");
mAppInfoAdapter.removeFromList(deletedList);
}
return ret;
}
private void doneLoadingData() {
setProgressBarIndeterminateVisibility(false);
}
List<ApplicationInfo> getInstalledApps(int filterOption) {
List<ApplicationInfo> installedAppList = mPm.getInstalledApplications(
PackageManager.GET_UNINSTALLED_PACKAGES);
if (installedAppList == null) {
return new ArrayList<ApplicationInfo> ();
}
if (filterOption == FILTER_APPS_SDCARD) {
List<ApplicationInfo> appList =new ArrayList<ApplicationInfo> ();
for (ApplicationInfo appInfo : installedAppList) {
if ((appInfo.flags & ApplicationInfo.FLAG_EXTERNAL_STORAGE) != 0) {
// App on sdcard
appList.add(appInfo);
}
}
return appList;
} else if (filterOption == FILTER_APPS_THIRD_PARTY) {
List<ApplicationInfo> appList =new ArrayList<ApplicationInfo> ();
for (ApplicationInfo appInfo : installedAppList) {
boolean flag = false;
if ((appInfo.flags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0) {
// Updated system app
flag = true;
} else if ((appInfo.flags & ApplicationInfo.FLAG_SYSTEM) == 0) {
// Non-system app
flag = true;
}
if (flag) {
appList.add(appInfo);
}
}
return appList;
} else if (filterOption == FILTER_APPS_RUNNING) {
List<ApplicationInfo> appList =new ArrayList<ApplicationInfo> ();
List<ActivityManager.RunningAppProcessInfo> procList = getRunningAppProcessesList();
if ((procList == null) || (procList.size() == 0)) {
return appList;
}
// Retrieve running processes from ActivityManager
for (ActivityManager.RunningAppProcessInfo appProcInfo : procList) {
if ((appProcInfo != null) && (appProcInfo.pkgList != null)){
int size = appProcInfo.pkgList.length;
for (int i = 0; i < size; i++) {
ApplicationInfo appInfo = null;
try {
appInfo = mPm.getApplicationInfo(appProcInfo.pkgList[i],
PackageManager.GET_UNINSTALLED_PACKAGES);
} catch (NameNotFoundException e) {
Log.w(TAG, "Error retrieving ApplicationInfo for pkg:"+appProcInfo.pkgList[i]);
continue;
}
if(appInfo != null) {
appList.add(appInfo);
}
}
}
}
return appList;
} else {
return installedAppList;
}
}
private static boolean matchFilter(boolean filter, Map<String, String> filterMap, String pkg) {
boolean add = true;
if (filter) {
if (filterMap == null || !filterMap.containsKey(pkg)) {
add = false;
}
}
return add;
}
/*
* Utility method used to figure out list of apps based on filterOption
* If the framework supports an additional flag to indicate running apps
* we can get away with some code here.
*/
List<ApplicationInfo> getFilteredApps(List<ApplicationInfo> pAppList, int filterOption, boolean filter,
Map<String, String> filterMap) {
List<ApplicationInfo> retList = new ArrayList<ApplicationInfo>();
if(pAppList == null) {
return retList;
}
if (filterOption == FILTER_APPS_SDCARD) {
for (ApplicationInfo appInfo : pAppList) {
boolean flag = false;
if ((appInfo.flags & ApplicationInfo.FLAG_EXTERNAL_STORAGE) != 0) {
// App on sdcard
flag = true;
}
if (flag) {
if (matchFilter(filter, filterMap, appInfo.packageName)) {
retList.add(appInfo);
}
}
}
return retList;
} else if (filterOption == FILTER_APPS_THIRD_PARTY) {
for (ApplicationInfo appInfo : pAppList) {
boolean flag = false;
if ((appInfo.flags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0) {
// Updated system app
flag = true;
} else if ((appInfo.flags & ApplicationInfo.FLAG_SYSTEM) == 0) {
// Non-system app
flag = true;
}
if (flag) {
if (matchFilter(filter, filterMap, appInfo.packageName)) {
retList.add(appInfo);
}
}
}
return retList;
} else if (filterOption == FILTER_APPS_RUNNING) {
List<ActivityManager.RunningAppProcessInfo> procList = getRunningAppProcessesList();
if ((procList == null) || (procList.size() == 0)) {
return retList;
}
// Retrieve running processes from ActivityManager
HashMap<String, ActivityManager.RunningAppProcessInfo> runningMap =
new HashMap<String, ActivityManager.RunningAppProcessInfo>();
for (ActivityManager.RunningAppProcessInfo appProcInfo : procList) {
if ((appProcInfo != null) && (appProcInfo.pkgList != null)){
int size = appProcInfo.pkgList.length;
for (int i = 0; i < size; i++) {
runningMap.put(appProcInfo.pkgList[i], appProcInfo);
}
}
}
// Query list to find running processes in current list
for (ApplicationInfo appInfo : pAppList) {
if (runningMap.get(appInfo.packageName) != null) {
if (matchFilter(filter, filterMap, appInfo.packageName)) {
retList.add(appInfo);
}
}
}
return retList;
} else {
for (ApplicationInfo appInfo : pAppList) {
if (matchFilter(filter, filterMap, appInfo.packageName)) {
retList.add(appInfo);
}
}
return retList;
}
}
private List<ActivityManager.RunningAppProcessInfo> getRunningAppProcessesList() {
ActivityManager am = (ActivityManager)getSystemService(Context.ACTIVITY_SERVICE);
return am.getRunningAppProcesses();
}
// Some initialization code used when kicking off the size computation
private void initAppList(List<ApplicationInfo> appList, int filterOption) {
setProgressBarIndeterminateVisibility(true);
mComputeSizesFinished = false;
mLoadLabelsFinished = false;
// Initialize lists
mAddRemoveMap = new TreeMap<String, Boolean>();
mAppInfoAdapter.initMapFromList(appList, filterOption);
}
// Utility method to start a thread to read application labels and icons
private void initResourceThread() {
if ((mResourceThread != null) && mResourceThread.isAlive()) {
mResourceThread.setAbort();
}
mResourceThread = new ResourceLoaderThread();
List<ApplicationInfo> appList = mAppInfoAdapter.getBaseAppList();
if ((appList != null) && (appList.size()) > 0) {
mResourceThread.loadAllResources(appList);
}
}
private void initComputeSizes() {
// Initiate compute package sizes
if (localLOGV) Log.i(TAG, "Initiating compute sizes for first time");
if ((mSizeComputor != null) && (mSizeComputor.isAlive())) {
mSizeComputor.setAbort();
}
List<ApplicationInfo> appList = mAppInfoAdapter.getBaseAppList();
if ((appList != null) && (appList.size()) > 0) {
mSizeComputor = new TaskRunner(appList);
} else {
mComputeSizesFinished = true;
}
}
// internal structure used to track added and deleted packages when
// the activity has focus
static class AddRemoveInfo {
String pkgName;
boolean add;
public AddRemoveInfo(String pPkgName, boolean pAdd) {
pkgName = pPkgName;
add = pAdd;
}
}
class ResourceLoaderThread extends Thread {
List<ApplicationInfo> mAppList;
volatile boolean abort = false;
static final int MSG_PKG_SIZE = 8;
public void setAbort() {
abort = true;
}
void loadAllResources(List<ApplicationInfo> appList) {
mAppList = appList;
start();
}
public void run() {
long start;
if (DEBUG_TIME) {
start = SystemClock.elapsedRealtime();
}
int imax;
if(mAppList == null || (imax = mAppList.size()) <= 0) {
Log.w(TAG, "Empty or null application list");
} else {
int size = mAppList.size();
int numMsgs = size / MSG_PKG_SIZE;
if (size > (numMsgs * MSG_PKG_SIZE)) {
numMsgs++;
}
int endi = 0;
for (int j = 0; j < size; j += MSG_PKG_SIZE) {
Map<String, CharSequence> map = new HashMap<String, CharSequence>();
endi += MSG_PKG_SIZE;
if (endi > size) {
endi = size;
}
for (int i = j; i < endi; i++) {
if (abort) {
// Exit if abort has been set.
break;
}
ApplicationInfo appInfo = mAppList.get(i);
map.put(appInfo.packageName, appInfo.loadLabel(mPm));
}
// Post update message
Message msg = mHandler.obtainMessage(REFRESH_LABELS);
msg.obj = map;
mHandler.sendMessage(msg);
}
Message doneMsg = mHandler.obtainMessage(REFRESH_DONE);
mHandler.sendMessage(doneMsg);
if (DEBUG_TIME) Log.i(TAG, "Took "+(SystemClock.elapsedRealtime()-start)+
" ms to load app labels");
long startIcons;
if (DEBUG_TIME) {
startIcons = SystemClock.elapsedRealtime();
}
Map<String, Drawable> map = new HashMap<String, Drawable>();
for (int i = (imax-1); i >= 0; i--) {
if (abort) {
return;
}
ApplicationInfo appInfo = mAppList.get(i);
map.put(appInfo.packageName, appInfo.loadIcon(mPm));
}
Message msg = mHandler.obtainMessage(REFRESH_ICONS);
msg.obj = map;
mHandler.sendMessage(msg);
if (DEBUG_TIME) Log.i(TAG, "Took "+(SystemClock.elapsedRealtime()-startIcons)+" ms to load app icons");
}
if (DEBUG_TIME) Log.i(TAG, "Took "+(SystemClock.elapsedRealtime()-start)+" ms to load app resources");
}
}
/* Internal class representing an application or packages displayable attributes
*
*/
static private class AppInfo {
public String pkgName;
int index;
public CharSequence appName;
public Drawable appIcon;
public CharSequence appSize;
long size;
public void refreshIcon(Drawable icon) {
if (icon == null) {
return;
}
appIcon = icon;
}
public void refreshLabel(CharSequence label) {
if (label == null) {
return;
}
appName = label;
}
public AppInfo(String pName, int pIndex, CharSequence aName,
long pSize,
CharSequence pSizeStr) {
this(pName, pIndex, aName, mDefaultAppIcon, pSize, pSizeStr);
}
public AppInfo(String pName, int pIndex, CharSequence aName, Drawable aIcon,
long pSize,
CharSequence pSizeStr) {
index = pIndex;
pkgName = pName;
appName = aName;
appIcon = aIcon;
size = pSize;
appSize = pSizeStr;
}
public boolean setSize(long newSize, String formattedSize) {
if (size != newSize) {
size = newSize;
appSize = formattedSize;
return true;
}
return false;
}
}
private long getTotalSize(PackageStats ps) {
if (ps != null) {
return ps.cacheSize+ps.codeSize+ps.dataSize;
}
return SIZE_INVALID;
}
private CharSequence getSizeStr(long size) {
CharSequence appSize = null;
if (size == SIZE_INVALID) {
return mInvalidSizeStr;
}
appSize = Formatter.formatFileSize(ManageApplications.this, size);
return appSize;
}
// View Holder used when displaying views
static class AppViewHolder {
TextView appName;
ImageView appIcon;
TextView appSize;
}
/*
* Custom adapter implementation for the ListView
* This adapter maintains a map for each displayed application and its properties
* An index value on each AppInfo object indicates the correct position or index
* in the list. If the list gets updated dynamically when the user is viewing the list of
* applications, we need to return the correct index of position. This is done by mapping
* the getId methods via the package name into the internal maps and indices.
* The order of applications in the list is mirrored in mAppLocalList
*/
class AppInfoAdapter extends BaseAdapter implements Filterable {
private List<ApplicationInfo> mAppList;
private List<ApplicationInfo> mAppLocalList;
private Map<String, String> mFilterMap = new HashMap<String, String>();
AlphaComparator mAlphaComparator = new AlphaComparator();
SizeComparator mSizeComparator = new SizeComparator();
private Filter mAppFilter = new AppFilter();
final private Object mFilterLock = new Object();
private Map<String, String> mCurrentFilterMap = null;
private void generateFilterListLocked(List<ApplicationInfo> list) {
mAppLocalList = new ArrayList<ApplicationInfo>(list);
synchronized(mFilterLock) {
for (ApplicationInfo info : mAppLocalList) {
String label = info.packageName;
AppInfo aInfo = mCache.getEntry(info.packageName);
if ((aInfo != null) && (aInfo.appName != null)) {
label = aInfo.appName.toString();
}
mFilterMap.put(info.packageName, label.toLowerCase());
}
}
}
private void addFilterListLocked(int newIdx, ApplicationInfo info, CharSequence pLabel) {
mAppLocalList.add(newIdx, info);
synchronized (mFilterLock) {
String label = info.packageName;
if (pLabel != null) {
label = pLabel.toString();
}
mFilterMap.put(info.packageName, label.toLowerCase());
}
}
private boolean removeFilterListLocked(String removePkg) {
// Remove from filtered list
int N = mAppLocalList.size();
int i;
for (i = (N-1); i >= 0; i--) {
ApplicationInfo info = mAppLocalList.get(i);
if (info.packageName.equalsIgnoreCase(removePkg)) {
if (localLOGV) Log.i(TAG, "Removing " + removePkg + " from local list");
mAppLocalList.remove(i);
synchronized (mFilterLock) {
mFilterMap.remove(removePkg);
}
return true;
}
}
return false;
}
private void reverseGenerateList() {
generateFilterListLocked(getFilteredApps(mAppList, mFilterApps, mCurrentFilterMap!= null, mCurrentFilterMap));
sortListInner(mSortOrder);
}
// Make sure the cache or map contains entries for all elements
// in appList for a valid sort.
public void initMapFromList(List<ApplicationInfo> pAppList, int filterOption) {
boolean notify = false;
List<ApplicationInfo> appList = null;
if (pAppList == null) {
// Just refresh the list
appList = mAppList;
} else {
mAppList = new ArrayList<ApplicationInfo>(pAppList);
appList = pAppList;
notify = true;
}
generateFilterListLocked(getFilteredApps(appList, filterOption, mCurrentFilterMap!= null, mCurrentFilterMap));
// This loop verifies and creates new entries for new packages in list
int imax = appList.size();
for (int i = 0; i < imax; i++) {
ApplicationInfo info = appList.get(i);
AppInfo aInfo = mCache.getEntry(info.packageName);
if(aInfo == null){
aInfo = new AppInfo(info.packageName, i,
info.packageName, -1, mComputingSizeStr);
if (localLOGV) Log.i(TAG, "Creating entry pkg:"+info.packageName+" to map");
mCache.addEntry(aInfo);
}
}
sortListInner(mSortOrder);
if (notify) {
notifyDataSetChanged();
}
}
public AppInfoAdapter(Context c, List<ApplicationInfo> appList) {
mAppList = appList;
}
public int getCount() {
return mAppLocalList.size();
}
public Object getItem(int position) {
return mAppLocalList.get(position);
}
public boolean isInstalled(String pkgName) {
if(pkgName == null) {
if (localLOGV) Log.w(TAG, "Null pkg name when checking if installed");
return false;
}
for (ApplicationInfo info : mAppList) {
if (info.packageName.equalsIgnoreCase(pkgName)) {
return true;
}
}
return false;
}
public ApplicationInfo getApplicationInfo(int position) {
int imax = mAppLocalList.size();
if( (position < 0) || (position >= imax)) {
Log.w(TAG, "Position out of bounds in List Adapter");
return null;
}
return mAppLocalList.get(position);
}
public long getItemId(int position) {
int imax = mAppLocalList.size();
if( (position < 0) || (position >= imax)) {
Log.w(TAG, "Position out of bounds in List Adapter");
return -1;
}
AppInfo aInfo = mCache.getEntry(mAppLocalList.get(position).packageName);
if (aInfo == null) {
return -1;
}
return aInfo.index;
}
public List<ApplicationInfo> getBaseAppList() {
return mAppList;
}
public View getView(int position, View convertView, ViewGroup parent) {
if (position >= mAppLocalList.size()) {
Log.w(TAG, "Invalid view position:"+position+", actual size is:"+mAppLocalList.size());
return null;
}
// A ViewHolder keeps references to children views to avoid unnecessary calls
// to findViewById() on each row.
AppViewHolder holder;
// When convertView is not null, we can reuse it directly, there is no need
// to reinflate it. We only inflate a new View when the convertView supplied
// by ListView is null.
if (convertView == null) {
convertView = mInflater.inflate(R.layout.manage_applications_item, null);
// Creates a ViewHolder and store references to the two children views
// we want to bind data to.
holder = new AppViewHolder();
holder.appName = (TextView) convertView.findViewById(R.id.app_name);
holder.appIcon = (ImageView) convertView.findViewById(R.id.app_icon);
holder.appSize = (TextView) convertView.findViewById(R.id.app_size);
convertView.setTag(holder);
} else {
// Get the ViewHolder back to get fast access to the TextView
// and the ImageView.
holder = (AppViewHolder) convertView.getTag();
}
// Bind the data efficiently with the holder
ApplicationInfo appInfo = mAppLocalList.get(position);
AppInfo mInfo = mCache.getEntry(appInfo.packageName);
if(mInfo != null) {
if(mInfo.appName != null) {
holder.appName.setText(mInfo.appName);
}
if(mInfo.appIcon != null) {
holder.appIcon.setImageDrawable(mInfo.appIcon);
}
if (mInfo.appSize != null) {
holder.appSize.setText(mInfo.appSize);
}
} else {
Log.w(TAG, "No info for package:"+appInfo.packageName+" in property map");
}
return convertView;
}
private void adjustIndex() {
int imax = mAppLocalList.size();
for (int i = 0; i < imax; i++) {
ApplicationInfo info = mAppLocalList.get(i);
mCache.getEntry(info.packageName).index = i;
}
}
public void sortAppList(List<ApplicationInfo> appList, int sortOrder) {
Collections.sort(appList, getAppComparator(sortOrder));
}
public void sortBaseList(int sortOrder) {
if (localLOGV) Log.i(TAG, "Sorting base list based on sortOrder = "+sortOrder);
sortAppList(mAppList, sortOrder);
generateFilterListLocked(getFilteredApps(mAppList, mFilterApps, mCurrentFilterMap!= null, mCurrentFilterMap));
adjustIndex();
}
private void sortListInner(int sortOrder) {
sortAppList(mAppLocalList, sortOrder);
adjustIndex();
}
public void sortList(int sortOrder) {
if (localLOGV) Log.i(TAG, "sortOrder = "+sortOrder);
sortListInner(sortOrder);
notifyDataSetChanged();
}
/*
* Reset the application list associated with this adapter.
* @param filterOption Sort the list based on this value
* @param appList the actual application list that is used to reset
* @return Return a boolean value to indicate inconsistency
*/
public boolean resetAppList(int filterOption) {
// Change application list based on filter option
generateFilterListLocked(getFilteredApps(mAppList, filterOption, mCurrentFilterMap!= null, mCurrentFilterMap));
// Check for all properties in map before sorting. Populate values from cache
for(ApplicationInfo applicationInfo : mAppLocalList) {
AppInfo appInfo = mCache.getEntry(applicationInfo.packageName);
if(appInfo == null) {
Log.i(TAG, " Entry does not exist for pkg: " + applicationInfo.packageName);
}
}
if (mAppLocalList.size() > 0) {
sortList(mSortOrder);
} else {
notifyDataSetChanged();
}
return true;
}
private Comparator<ApplicationInfo> getAppComparator(int sortOrder) {
if (sortOrder == SORT_ORDER_ALPHA) {
return mAlphaComparator;
}
return mSizeComparator;
}
public void bulkUpdateIcons(Map<String, Drawable> icons) {
if (icons == null) {
return;
}
Set<String> keys = icons.keySet();
boolean changed = false;
for (String key : keys) {
Drawable ic = icons.get(key);
if (ic != null) {
AppInfo aInfo = mCache.getEntry(key);
if (aInfo != null) {
aInfo.refreshIcon(ic);
changed = true;
}
}
}
if (changed) {
notifyDataSetChanged();
}
}
public void bulkUpdateLabels(Map<String, CharSequence> map) {
if (map == null) {
return;
}
Set<String> keys = map.keySet();
boolean changed = false;
for (String key : keys) {
CharSequence label = map.get(key);
AppInfo aInfo = mCache.getEntry(key);
if (aInfo != null) {
aInfo.refreshLabel(label);
changed = true;
}
}
if (changed) {
notifyDataSetChanged();
}
}
private boolean shouldBeInList(int filterOption, ApplicationInfo info) {
// Match filter here
if (filterOption == FILTER_APPS_RUNNING) {
List<ApplicationInfo> runningList = getInstalledApps(FILTER_APPS_RUNNING);
for (ApplicationInfo running : runningList) {
if (running.packageName.equalsIgnoreCase(info.packageName)) {
return true;
}
}
} else if (filterOption == FILTER_APPS_THIRD_PARTY) {
if ((info.flags & ApplicationInfo.FLAG_SYSTEM) == 0) {
return true;
} else if ((info.flags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0) {
return true;
}
} else if (filterOption == FILTER_APPS_SDCARD) {
if ((info.flags & ApplicationInfo.FLAG_EXTERNAL_STORAGE) != 0) {
return true;
}
} else {
return true;
}
return false;
}
/*
* Add a package to the current list.
* The package is only added to the displayed list
* based on the filter value. The package is always added to the property map.
* @param pkgName name of package to be added
* @param ps PackageStats of new package
*/
public void addToList(String pkgName, long size, String formattedSize) {
if (pkgName == null) {
return;
}
// Get ApplicationInfo
ApplicationInfo info = null;
try {
info = mPm.getApplicationInfo(pkgName, 0);
} catch (NameNotFoundException e) {
Log.w(TAG, "Ignoring non-existent package:"+pkgName);
return;
}
if(info == null) {
// Nothing to do log error message and return
Log.i(TAG, "Null ApplicationInfo for package:"+pkgName);
return;
}
// Add entry to base list
mAppList.add(info);
// Add entry to map. Note that the index gets adjusted later on based on
// whether the newly added package is part of displayed list
CharSequence label = info.loadLabel(mPm);
mCache.addEntry(new AppInfo(pkgName, -1,
label, info.loadIcon(mPm), size, formattedSize));
if (addLocalEntry(info, label)) {
notifyDataSetChanged();
}
}
private boolean addLocalEntry(ApplicationInfo info, CharSequence label) {
String pkgName = info.packageName;
// Add to list
if (shouldBeInList(mFilterApps, info)) {
// Binary search returns a negative index (ie -index) of the position where
// this might be inserted.
int newIdx = Collections.binarySearch(mAppLocalList, info,
getAppComparator(mSortOrder));
if(newIdx >= 0) {
if (localLOGV) Log.i(TAG, "Strange. Package:" + pkgName + " is not new");
return false;
}
// New entry
newIdx = -newIdx-1;
addFilterListLocked(newIdx, info, label);
// Adjust index
adjustIndex();
return true;
}
return false;
}
public void updatePackage(String pkgName,
long size, String formattedSize) {
ApplicationInfo info = null;
try {
info = mPm.getApplicationInfo(pkgName,
PackageManager.GET_UNINSTALLED_PACKAGES);
} catch (NameNotFoundException e) {
return;
}
AppInfo aInfo = mCache.getEntry(pkgName);
if (aInfo != null) {
CharSequence label = info.loadLabel(mPm);
aInfo.refreshLabel(label);
aInfo.refreshIcon(info.loadIcon(mPm));
aInfo.setSize(size, formattedSize);
// Check if the entry has to be added to the displayed list
addLocalEntry(info, label);
// Refresh list since size might have changed
notifyDataSetChanged();
}
}
private void removePkgBase(String pkgName) {
int imax = mAppList.size();
for (int i = 0; i < imax; i++) {
ApplicationInfo app = mAppList.get(i);
if (app.packageName.equalsIgnoreCase(pkgName)) {
if (localLOGV) Log.i(TAG, "Removing pkg: "+pkgName+" from base list");
mAppList.remove(i);
return;
}
}
}
public void removeFromList(List<String> pkgNames) {
if(pkgNames == null) {
return;
}
if(pkgNames.size() <= 0) {
return;
}
boolean found = false;
for (String pkg : pkgNames) {
// Remove from the base application list
removePkgBase(pkg);
// Remove from cache
if (localLOGV) Log.i(TAG, "Removing " + pkg + " from cache");
mCache.removeEntry(pkg);
// Remove from filtered list
if (removeFilterListLocked(pkg)) {
found = true;
}
}
// Adjust indices of list entries
if (found) {
adjustIndex();
if (localLOGV) Log.i(TAG, "adjusting index and notifying list view");
notifyDataSetChanged();
}
}
public void bulkUpdateSizes(String pkgs[], long sizes[], String formatted[]) {
if(pkgs == null || sizes == null || formatted == null) {
return;
}
boolean changed = false;
for (int i = 0; i < pkgs.length; i++) {
AppInfo entry = mCache.getEntry(pkgs[i]);
if (entry == null) {
if (localLOGV) Log.w(TAG, "Entry for package:"+ pkgs[i] +"doesn't exist in map");
continue;
}
if (entry.setSize(sizes[i], formatted[i])) {
changed = true;
}
}
if (changed) {
notifyDataSetChanged();
}
}
public Filter getFilter() {
return mAppFilter;
}
private class AppFilter extends Filter {
@Override
protected FilterResults performFiltering(CharSequence prefix) {
FilterResults results = new FilterResults();
if (prefix == null || prefix.length() == 0) {
synchronized (mFilterLock) {
results.values = new HashMap<String, String>(mFilterMap);
results.count = mFilterMap.size();
}
} else {
final String prefixString = prefix.toString().toLowerCase();
final String spacePrefixString = " " + prefixString;
Map<String, String> newMap = new HashMap<String, String>();
synchronized (mFilterLock) {
Map<String, String> localMap = mFilterMap;
Set<String> keys = mFilterMap.keySet();
for (String key : keys) {
String label = localMap.get(key);
if (label == null) continue;
label = label.toLowerCase();
if (label.startsWith(prefixString)
|| label.indexOf(spacePrefixString) != -1) {
newMap.put(key, label);
}
}
}
results.values = newMap;
results.count = newMap.size();
}
return results;
}
@Override
protected void publishResults(CharSequence constraint, FilterResults results) {
mCurrentFilterMap = (Map<String, String>) results.values;
reverseGenerateList();
if (results.count > 0) {
notifyDataSetChanged();
} else {
notifyDataSetInvalidated();
}
}
}
}
/*
* Utility method to clear messages to Handler
* We need'nt synchronize on the Handler since posting messages is guaranteed
* to be thread safe. Even if the other thread that retrieves package sizes
* posts a message, we do a cursory check of validity on mAppInfoAdapter's applist
*/
private void clearMessagesInHandler() {
mHandler.removeMessages(INIT_PKG_INFO);
mHandler.removeMessages(COMPUTE_BULK_SIZE);
mHandler.removeMessages(REMOVE_PKG);
mHandler.removeMessages(REORDER_LIST);
mHandler.removeMessages(ADD_PKG_START);
mHandler.removeMessages(ADD_PKG_DONE);
mHandler.removeMessages(REFRESH_LABELS);
mHandler.removeMessages(REFRESH_DONE);
mHandler.removeMessages(NEXT_LOAD_STEP);
mHandler.removeMessages(COMPUTE_END);
}
private void sendMessageToHandler(int msgId, int arg1) {
Message msg = mHandler.obtainMessage(msgId);
msg.arg1 = arg1;
mHandler.sendMessage(msg);
}
private void sendMessageToHandler(int msgId, Bundle data) {
Message msg = mHandler.obtainMessage(msgId);
msg.setData(data);
mHandler.sendMessage(msg);
}
private void sendMessageToHandler(int msgId) {
mHandler.sendEmptyMessage(msgId);
}
/*
* Stats Observer class used to compute package sizes and retrieve size information
* PkgSizeOberver is the call back thats used when invoking getPackageSizeInfo on
* PackageManager. The values in call back onGetStatsCompleted are validated
* and the specified message is passed to mHandler. The package name
* and the AppInfo object corresponding to the package name are set on the message
*/
class PkgSizeObserver extends IPackageStatsObserver.Stub {
String pkgName;
public void onGetStatsCompleted(PackageStats pStats, boolean pSucceeded) {
if(DEBUG_PKG_DELAY) {
try {
Thread.sleep(10*1000);
} catch (InterruptedException e) {
}
}
Bundle data = new Bundle();
data.putString(ATTR_PKG_NAME, pkgName);
data.putBoolean(ATTR_GET_SIZE_STATUS, pSucceeded);
if(pSucceeded && pStats != null) {
if (localLOGV) Log.i(TAG, "onGetStatsCompleted::"+pkgName+", ("+
pStats.cacheSize+","+
pStats.codeSize+", "+pStats.dataSize);
long total = getTotalSize(pStats);
data.putLong(ATTR_PKG_STATS, total);
CharSequence sizeStr = getSizeStr(total);
data.putString(ATTR_PKG_SIZE_STR, sizeStr.toString());
} else {
Log.w(TAG, "Invalid package stats from PackageManager");
}
// Post message to Handler
Message msg = mHandler.obtainMessage(ADD_PKG_DONE, data);
msg.setData(data);
mHandler.sendMessage(msg);
}
public void invokeGetSizeInfo(String packageName) {
if (packageName == null) {
return;
}
pkgName = packageName;
if(localLOGV) Log.i(TAG, "Invoking getPackageSizeInfo for package:"+
packageName);
mPm.getPackageSizeInfo(packageName, this);
}
}
/**
* Receives notifications when applications are added/removed.
*/
private class PackageIntentReceiver extends BroadcastReceiver {
void registerReceiver() {
IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED);
filter.addAction(Intent.ACTION_PACKAGE_REMOVED);
filter.addAction(Intent.ACTION_PACKAGE_CHANGED);
filter.addDataScheme("package");
ManageApplications.this.registerReceiver(this, filter);
// Register for events related to sdcard installation.
IntentFilter sdFilter = new IntentFilter();
sdFilter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE);
sdFilter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE);
ManageApplications.this.registerReceiver(this, sdFilter);
}
@Override
public void onReceive(Context context, Intent intent) {
String actionStr = intent.getAction();
if (Intent.ACTION_PACKAGE_ADDED.equals(actionStr) ||
Intent.ACTION_PACKAGE_REMOVED.equals(actionStr)) {
Uri data = intent.getData();
String pkgName = data.getEncodedSchemeSpecificPart();
updatePackageList(actionStr, pkgName);
} else if (Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE.equals(actionStr) ||
Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE.equals(actionStr)) {
// When applications become available or unavailable (perhaps because
// the SD card was inserted or ejected) we need to refresh the
// AppInfo with new label, icon and size information as appropriate
// given the newfound (un)availability of the application.
// A simple way to do that is to treat the refresh as a package
// removal followed by a package addition.
String pkgList[] = intent.getStringArrayExtra(Intent.EXTRA_CHANGED_PACKAGE_LIST);
if (pkgList == null || pkgList.length == 0) {
// Ignore
return;
}
for (String pkgName : pkgList) {
updatePackageList(Intent.ACTION_PACKAGE_REMOVED, pkgName);
updatePackageList(Intent.ACTION_PACKAGE_ADDED, pkgName);
}
}
}
}
private void updatePackageList(String actionStr, String pkgName) {
if (Intent.ACTION_PACKAGE_ADDED.equalsIgnoreCase(actionStr)) {
Bundle data = new Bundle();
data.putString(ATTR_PKG_NAME, pkgName);
sendMessageToHandler(ADD_PKG_START, data);
} else if (Intent.ACTION_PACKAGE_REMOVED.equalsIgnoreCase(actionStr)) {
Bundle data = new Bundle();
data.putString(ATTR_PKG_NAME, pkgName);
sendMessageToHandler(REMOVE_PKG, data);
}
}
static final String TAB_DOWNLOADED = "Downloaded";
static final String TAB_RUNNING = "Running";
static final String TAB_ALL = "All";
static final String TAB_SDCARD = "OnSdCard";
private View mRootView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if(localLOGV) Log.i(TAG, "Activity created");
long sCreate;
if (DEBUG_TIME) {
sCreate = SystemClock.elapsedRealtime();
}
Intent intent = getIntent();
String action = intent.getAction();
String defaultTabTag = TAB_DOWNLOADED;
if (action.equals(Intent.ACTION_MANAGE_PACKAGE_STORAGE)) {
mSortOrder = SORT_ORDER_SIZE;
mFilterApps = FILTER_APPS_ALL;
defaultTabTag = TAB_ALL;
mSizesFirst = true;
}
mPm = getPackageManager();
// initialize some window features
requestWindowFeature(Window.FEATURE_RIGHT_ICON);
requestWindowFeature(Window.FEATURE_PROGRESS);
requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
showLoadingMsg();
mDefaultAppIcon = Resources.getSystem().getDrawable(
com.android.internal.R.drawable.sym_def_app_icon);
mInvalidSizeStr = getText(R.string.invalid_size_value);
mComputingSizeStr = getText(R.string.computing_size);
// initialize the inflater
mInflater = (LayoutInflater)getSystemService(Context.LAYOUT_INFLATER_SERVICE);
mRootView = mInflater.inflate(R.layout.compute_sizes, null);
mReceiver = new PackageIntentReceiver();
mObserver = new PkgSizeObserver();
// Create adapter and list view here
List<ApplicationInfo> appList = getInstalledApps(FILTER_APPS_ALL);
mAppInfoAdapter = new AppInfoAdapter(this, appList);
ListView lv = (ListView) mRootView.findViewById(android.R.id.list);
lv.setOnItemClickListener(this);
lv.setSaveEnabled(true);
lv.setItemsCanFocus(true);
lv.setOnItemClickListener(this);
lv.setTextFilterEnabled(true);
mListView = lv;
if (DEBUG_TIME) {
Log.i(TAG, "Total time in Activity.create:: " +
(SystemClock.elapsedRealtime() - sCreate)+ " ms");
}
// Get initial info from file for the very first time this activity started
long sStart;
if (DEBUG_TIME) {
sStart = SystemClock.elapsedRealtime();
}
mCache.loadCache();
if (DEBUG_TIME) {
Log.i(TAG, "Took " + (SystemClock.elapsedRealtime()-sStart) + " ms to init cache");
}
final TabHost tabHost = getTabHost();
tabHost.addTab(tabHost.newTabSpec(TAB_DOWNLOADED)
.setIndicator(getString(R.string.filter_apps_third_party),
getResources().getDrawable(R.drawable.ic_tab_download))
.setContent(this));
tabHost.addTab(tabHost.newTabSpec(TAB_RUNNING)
.setIndicator(getString(R.string.filter_apps_running),
getResources().getDrawable(R.drawable.ic_tab_running))
.setContent(this));
tabHost.addTab(tabHost.newTabSpec(TAB_ALL)
.setIndicator(getString(R.string.filter_apps_all),
getResources().getDrawable(R.drawable.ic_tab_all))
.setContent(this));
tabHost.addTab(tabHost.newTabSpec(TAB_SDCARD)
.setIndicator(getString(R.string.filter_apps_onsdcard),
getResources().getDrawable(R.drawable.ic_tab_sdcard))
.setContent(this));
tabHost.setCurrentTabByTag(defaultTabTag);
tabHost.setOnTabChangedListener(this);
}
@Override
protected void onDestroy() {
// Persist values in cache
mCache.updateCache();
super.onDestroy();
}
@Override
public Dialog onCreateDialog(int id, Bundle args) {
if (id == DLG_LOADING) {
ProgressDialog dlg = new ProgressDialog(this);
dlg.setProgressStyle(ProgressDialog.STYLE_SPINNER);
dlg.setMessage(getText(R.string.loading));
dlg.setIndeterminate(true);
dlg.setOnCancelListener(this);
return dlg;
}
return null;
}
private void showLoadingMsg() {
if (DEBUG_TIME) {
mLoadTimeStart = SystemClock.elapsedRealtime();
}
showDialog(DLG_LOADING);
if(localLOGV) Log.i(TAG, "Displaying Loading message");
}
private void dismissLoadingMsg() {
if(localLOGV) Log.i(TAG, "Dismissing Loading message");
dismissDialog(DLG_LOADING);
if (DEBUG_TIME) Log.i(TAG, "Displayed loading message for "+
(SystemClock.elapsedRealtime() - mLoadTimeStart) + " ms");
}
class AppInfoCache {
final static boolean FILE_CACHE = true;
private static final String mFileCacheName="ManageAppsInfo.txt";
private static final int FILE_BUFFER_SIZE = 1024;
private static final boolean DEBUG_CACHE = false;
private static final boolean DEBUG_CACHE_TIME = false;
private Map<String, AppInfo> mAppPropCache = new HashMap<String, AppInfo>();
private boolean isEmpty() {
return (mAppPropCache.size() == 0);
}
private AppInfo getEntry(String pkgName) {
return mAppPropCache.get(pkgName);
}
private Set<String> getPkgList() {
return mAppPropCache.keySet();
}
public void addEntry(AppInfo aInfo) {
if ((aInfo != null) && (aInfo.pkgName != null)) {
mAppPropCache.put(aInfo.pkgName, aInfo);
}
}
public void removeEntry(String pkgName) {
if (pkgName != null) {
mAppPropCache.remove(pkgName);
}
}
private void readFromFile() {
File cacheFile = new File(getFilesDir(), mFileCacheName);
if (!cacheFile.exists()) {
return;
}
FileInputStream fis = null;
boolean err = false;
try {
fis = new FileInputStream(cacheFile);
} catch (FileNotFoundException e) {
Log.w(TAG, "Error opening file for read operation : " + cacheFile
+ " with exception " + e);
return;
}
try {
byte[] byteBuff = new byte[FILE_BUFFER_SIZE];
byte[] lenBytes = new byte[2];
mAppPropCache.clear();
while(fis.available() > 0) {
fis.read(lenBytes, 0, 2);
int buffLen = (lenBytes[0] << 8) | lenBytes[1];
if ((buffLen <= 0) || (buffLen > byteBuff.length)) {
err = true;
break;
}
// Buffer length cannot be greater than max.
fis.read(byteBuff, 0, buffLen);
String buffStr = new String(byteBuff);
if (DEBUG_CACHE) {
Log.i(TAG, "Read string of len= " + buffLen + " :: " + buffStr + " from file");
}
// Parse string for sizes
String substrs[] = buffStr.split(",");
if (substrs.length < 4) {
// Something wrong. Bail out and let recomputation proceed.
err = true;
break;
}
long size = -1;
int idx = -1;
try {
size = Long.parseLong(substrs[1]);
} catch (NumberFormatException e) {
err = true;
break;
}
if (DEBUG_CACHE) {
Log.i(TAG, "Creating entry(" + substrs[0] + ", " + idx+"," + size + ", " + substrs[2] + ")");
}
AppInfo aInfo = new AppInfo(substrs[0], idx, substrs[3], size, substrs[2]);
mAppPropCache.put(aInfo.pkgName, aInfo);
}
} catch (IOException e) {
Log.w(TAG, "Failed reading from file : " + cacheFile + " with exception : " + e);
err = true;
} finally {
if (fis != null) {
try {
fis.close();
} catch (IOException e) {
Log.w(TAG, "Failed to close file " + cacheFile + " with exception : " +e);
err = true;
}
}
if (err) {
Log.i(TAG, "Failed to load cache. Not using cache for now.");
// Clear cache and bail out
mAppPropCache.clear();
}
}
}
boolean writeToFile() {
File cacheFile = new File(getFilesDir(), mFileCacheName);
FileOutputStream fos = null;
try {
long opStartTime = SystemClock.uptimeMillis();
fos = new FileOutputStream(cacheFile);
Set<String> keys = mAppPropCache.keySet();
byte[] lenBytes = new byte[2];
for (String key : keys) {
AppInfo aInfo = mAppPropCache.get(key);
StringBuilder buff = new StringBuilder(aInfo.pkgName);
buff.append(",");
buff.append(aInfo.size);
buff.append(",");
buff.append(aInfo.appSize);
buff.append(",");
buff.append(aInfo.appName);
if (DEBUG_CACHE) {
Log.i(TAG, "Writing str : " + buff.toString() + " to file of length:" +
buff.toString().length());
}
try {
byte[] byteBuff = buff.toString().getBytes();
int len = byteBuff.length;
if (byteBuff.length >= FILE_BUFFER_SIZE) {
// Truncate the output
len = FILE_BUFFER_SIZE;
}
// Use 2 bytes to write length
lenBytes[1] = (byte) (len & 0x00ff);
lenBytes[0] = (byte) ((len & 0x00ff00) >> 8);
fos.write(lenBytes, 0, 2);
fos.write(byteBuff, 0, len);
} catch (IOException e) {
Log.w(TAG, "Failed to write to file : " + cacheFile + " with exception : " + e);
return false;
}
}
if (DEBUG_CACHE_TIME) {
Log.i(TAG, "Took " + (SystemClock.uptimeMillis() - opStartTime) + " ms to write and process from file");
}
return true;
} catch (FileNotFoundException e) {
Log.w(TAG, "Error opening file for write operation : " + cacheFile+
" with exception : " + e);
return false;
} finally {
if (fos != null) {
try {
fos.close();
} catch (IOException e) {
Log.w(TAG, "Failed closing file : " + cacheFile + " with exception : " + e);
return false;
}
}
}
}
private void loadCache() {
// Restore preferences
SharedPreferences settings = getSharedPreferences(PREFS_NAME, 0);
boolean disable = settings.getBoolean(PREF_DISABLE_CACHE, true);
if (disable) Log.w(TAG, "Cache has been disabled");
// Disable cache till the data is loaded successfully
SharedPreferences.Editor editor = settings.edit();
editor.putBoolean(PREF_DISABLE_CACHE, true);
editor.commit();
if (FILE_CACHE && !disable) {
readFromFile();
// Enable cache since the file has been read successfully
editor.putBoolean(PREF_DISABLE_CACHE, false);
editor.commit();
}
}
private void updateCache() {
SharedPreferences settings = getSharedPreferences(PREFS_NAME, 0);
SharedPreferences.Editor editor = settings.edit();
editor.putBoolean(PREF_DISABLE_CACHE, true);
editor.commit();
if (FILE_CACHE) {
boolean writeStatus = writeToFile();
mAppPropCache.clear();
if (writeStatus) {
// Enable cache since the file has been read successfully
editor.putBoolean(PREF_DISABLE_CACHE, false);
editor.commit();
}
}
}
}
@Override
public void onStart() {
super.onStart();
// Register receiver
mReceiver.registerReceiver();
sendMessageToHandler(INIT_PKG_INFO);
}
@Override
public void onStop() {
super.onStop();
// Stop the background threads
if (mResourceThread != null) {
mResourceThread.setAbort();
}
if (mSizeComputor != null) {
mSizeComputor.setAbort();
}
// clear all messages related to application list
clearMessagesInHandler();
// register receiver here
unregisterReceiver(mReceiver);
}
// Avoid the restart and pause when orientation changes
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
}
/*
* comparator class used to sort AppInfo objects based on size
*/
class SizeComparator implements Comparator<ApplicationInfo> {
public final int compare(ApplicationInfo a, ApplicationInfo b) {
AppInfo ainfo = mCache.getEntry(a.packageName);
AppInfo binfo = mCache.getEntry(b.packageName);
long atotal = ainfo.size;
long btotal = binfo.size;
long ret = atotal - btotal;
// negate result to sort in descending order
if (ret < 0) {
return 1;
}
if (ret == 0) {
return 0;
}
return -1;
}
}
/*
* Customized comparator class to compare labels.
* Don't use the one defined in ApplicationInfo since that loads the labels again.
*/
class AlphaComparator implements Comparator<ApplicationInfo> {
private final Collator sCollator = Collator.getInstance();
public final int compare(ApplicationInfo a, ApplicationInfo b) {
AppInfo ainfo = mCache.getEntry(a.packageName);
AppInfo binfo = mCache.getEntry(b.packageName);
// Check for null app names, to avoid NPE in rare cases
if (ainfo == null || ainfo.appName == null) return -1;
if (binfo == null || binfo.appName == null) return 1;
return sCollator.compare(ainfo.appName.toString(), binfo.appName.toString());
}
}
// utility method used to start sub activity
private void startApplicationDetailsActivity() {
// Create intent to start new activity
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setClass(this, InstalledAppDetails.class);
intent.putExtra(APP_PKG_NAME, mCurrentPkgName);
// start new activity to display extended information
startActivityForResult(intent, INSTALLED_APP_DETAILS);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
menu.add(0, SORT_ORDER_ALPHA, 1, R.string.sort_order_alpha)
.setIcon(android.R.drawable.ic_menu_sort_alphabetically);
menu.add(0, SORT_ORDER_SIZE, 2, R.string.sort_order_size)
.setIcon(android.R.drawable.ic_menu_sort_by_size);
return true;
}
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
if (mFirst) {
menu.findItem(SORT_ORDER_ALPHA).setVisible(mSortOrder != SORT_ORDER_ALPHA);
menu.findItem(SORT_ORDER_SIZE).setVisible(mSortOrder != SORT_ORDER_SIZE);
return true;
}
return false;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
int menuId = item.getItemId();
if ((menuId == SORT_ORDER_ALPHA) || (menuId == SORT_ORDER_SIZE)) {
sendMessageToHandler(REORDER_LIST, menuId);
}
return true;
}
public void onItemClick(AdapterView<?> parent, View view, int position,
long id) {
ApplicationInfo info = (ApplicationInfo)mAppInfoAdapter.getItem(position);
mCurrentPkgName = info.packageName;
startApplicationDetailsActivity();
}
// Finish the activity if the user presses the back button to cancel the activity
public void onCancel(DialogInterface dialog) {
finish();
}
public View createTabContent(String tag) {
return mRootView;
}
public void onTabChanged(String tabId) {
int newOption;
if (TAB_DOWNLOADED.equalsIgnoreCase(tabId)) {
newOption = FILTER_APPS_THIRD_PARTY;
} else if (TAB_RUNNING.equalsIgnoreCase(tabId)) {
newOption = FILTER_APPS_RUNNING;
} else if (TAB_ALL.equalsIgnoreCase(tabId)) {
newOption = FILTER_APPS_ALL;
} else if (TAB_SDCARD.equalsIgnoreCase(tabId)) {
newOption = FILTER_APPS_SDCARD;
} else {
// Invalid option. Do nothing
return;
}
sendMessageToHandler(REORDER_LIST, newOption);
}
@Override
protected void onActivityResult(int requestCode, int resultCode,
Intent data) {
if (requestCode == INSTALLED_APP_DETAILS && mCurrentPkgName != null) {
// Refresh package attributes
try {
ApplicationInfo info = mPm.getApplicationInfo(mCurrentPkgName,
PackageManager.GET_UNINSTALLED_PACKAGES);
} catch (NameNotFoundException e) {
Bundle rData = new Bundle();
rData.putString(ATTR_PKG_NAME, mCurrentPkgName);
sendMessageToHandler(REMOVE_PKG, rData);
mCurrentPkgName = null;
}
}
}
}