blob: 512e547e6d428c5a234f9a012c12260f40ec8eb8 [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.AlertDialog;
import android.app.ListActivity;
import android.app.ProgressDialog;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.IntentFilter;
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.Resources;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.text.format.Formatter;
import android.util.Config;
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.ImageView;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.AdapterView.OnItemClickListener;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
/**
* 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.
* This activity passes the package name and size information to the
* InstalledAppDetailsActivity to avoid recomputation of the package size information.
*/
public class ManageApplications extends ListActivity implements
OnItemClickListener, DialogInterface.OnCancelListener,
DialogInterface.OnClickListener {
// TAG for this activity
private static final String TAG = "ManageApplications";
// log information boolean
private boolean localLOGV = Config.LOGV || false;
// attributes used as keys when passing values to InstalledAppDetails activity
public static final String APP_PKG_PREFIX = "com.android.settings.";
public static final String APP_PKG_NAME = APP_PKG_PREFIX+"ApplicationPkgName";
public static final String APP_PKG_SIZE = APP_PKG_PREFIX+"size";
public static final String APP_CHG = APP_PKG_PREFIX+"changed";
// attribute name used in receiver for tagging names of added/deleted packages
private static final String ATTR_PKG_NAME="PackageName";
private static final String ATTR_APP_PKG_STATS="ApplicationPackageStats";
// 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;
public static final int SORT_ORDER_ALPHA = MENU_OPTIONS_BASE + 0;
public static final int SORT_ORDER_SIZE = MENU_OPTIONS_BASE + 1;
// Filter options used for displayed list of applications
public static final int FILTER_APPS_ALL = MENU_OPTIONS_BASE + 2;
public static final int FILTER_APPS_THIRD_PARTY = MENU_OPTIONS_BASE + 3;
public static final int FILTER_APPS_RUNNING = MENU_OPTIONS_BASE + 4;
public static final int FILTER_OPTIONS = MENU_OPTIONS_BASE + 5;
// Alert Dialog presented to user to find out the filter option
AlertDialog.Builder mAlertDlgBuilder;
// sort order
private int mSortOrder = SORT_ORDER_ALPHA;
// Filter value
int mFilterApps = FILTER_APPS_ALL;
// 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_PKG_SIZE_DONE = 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_ICONS = HANDLER_MESSAGE_BASE+7;
private static final int NEXT_LOAD_STEP = HANDLER_MESSAGE_BASE+8;
// 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 mComputeSizes = false;
// default icon thats used when displaying applications initially before resource info is
// retrieved
private Drawable mDefaultAppIcon;
// temporary dialog displayed while the application info loads
private ProgressDialog mLoadingDlg = null;
// compute index used to track the application size computations
private int mComputeIndex;
// 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;
String mCurrentPkgName;
//TODO implement a cache system
private Map<String, AppInfo> mAppPropCache;
// empty message displayed when list is empty
private TextView mEmptyView;
// Boolean variables indicating state
private boolean mLoadLabels = false;
private boolean mSizesFirst = false;
/*
* 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., is 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) {
PackageStats ps;
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");
setProgressBarIndeterminateVisibility(true);
mComputeIndex = 0;
// Retrieve the package list and init some structures
initAppList(mFilterApps);
mHandler.sendEmptyMessage(NEXT_LOAD_STEP);
break;
case COMPUTE_PKG_SIZE_DONE:
if(localLOGV) Log.i(TAG, "Message COMPUTE_PKG_SIZE_DONE");
if(pkgName == null) {
Log.w(TAG, "Ignoring message");
break;
}
ps = data.getParcelable(ATTR_APP_PKG_STATS);
if(ps == null) {
Log.i(TAG, "Invalid package stats for package:"+pkgName);
} else {
int pkgId = mAppInfoAdapter.getIndex(pkgName);
if(mComputeIndex != pkgId) {
//spurious call from stale observer
Log.w(TAG, "Stale call back from PkgSizeObserver");
break;
}
mAppInfoAdapter.updateAppSize(pkgName, ps);
}
mComputeIndex++;
if (mComputeIndex < mAppInfoAdapter.getCount()) {
// initiate compute package size for next pkg in list
mObserver.invokeGetSizeInfo(mAppInfoAdapter.getApplicationInfo(
mComputeIndex),
COMPUTE_PKG_SIZE_DONE);
} else {
// check for added/removed packages
Set<String> keys = mAddRemoveMap.keySet();
Iterator<String> iter = keys.iterator();
List<String> removeList = new ArrayList<String>();
boolean added = false;
boolean removed = false;
while (iter.hasNext()) {
String key = iter.next();
if (mAddRemoveMap.get(key) == Boolean.TRUE) {
// add
try {
info = mPm.getApplicationInfo(key, 0);
mAppInfoAdapter.addApplicationInfo(info);
added = true;
} catch (NameNotFoundException e) {
Log.w(TAG, "Invalid added package:"+key+" Ignoring entry");
}
} else {
// remove
removeList.add(key);
removed = true;
}
}
// remove uninstalled packages from list
if (removed) {
mAppInfoAdapter.removeFromList(removeList);
}
// handle newly installed packages
if (added) {
mObserver.invokeGetSizeInfo(mAppInfoAdapter.getApplicationInfo(
mComputeIndex),
COMPUTE_PKG_SIZE_DONE);
} else {
// end computation here
mComputeSizes = true;
mAppInfoAdapter.sortList(mSortOrder);
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 (!mComputeSizes) {
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,
getInstalledApps(mFilterApps));
if(!ret) {
// Reset cache
mAppPropCache = null;
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 (!mComputeSizes) {
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(info, ADD_PKG_DONE);
break;
case ADD_PKG_DONE:
if(localLOGV) Log.i(TAG, "Message COMPUTE_PKG_SIZE_DONE");
if(pkgName == null) {
Log.w(TAG, "Ignoring message:ADD_PKG_START for null pkgName");
break;
}
ps = data.getParcelable(ATTR_APP_PKG_STATS);
mAppInfoAdapter.addToList(pkgName, ps);
break;
case REFRESH_ICONS:
Map<String, AppInfo> iconMap = (Map<String, AppInfo>) msg.obj;
if(iconMap == null) {
Log.w(TAG, "Error loading icons for applications");
} else {
mAppInfoAdapter.updateAppsResourceInfo(iconMap);
}
mLoadLabels = true;
mHandler.sendEmptyMessage(NEXT_LOAD_STEP);
break;
case NEXT_LOAD_STEP:
if (mComputeSizes && mLoadLabels) {
doneLoadingData();
} else if (!mComputeSizes && !mLoadLabels) {
// Either load the package labels or initiate get size info
if (mSizesFirst) {
initComputeSizes();
} else {
initResourceThread();
}
} else {
// 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 be complete before creating the list
createListView();
if (!mComputeSizes) {
initComputeSizes();
} else if (!mLoadLabels) {
initResourceThread();
}
}
break;
default:
break;
}
}
};
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_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 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(int filterOption) {
mComputeSizes = false;
// Initialize lists
List<ApplicationInfo> appList = getInstalledApps(filterOption);
mAddRemoveMap = new TreeMap<String, Boolean>();
mAppInfoAdapter = new AppInfoAdapter(this, appList);
// register receiver
mReceiver.registerReceiver();
}
// Utility method to start a thread to read application labels and icons
private void initResourceThread() {
//load resources now
if(mResourceThread.isAlive()) {
mResourceThread.interrupt();
}
mResourceThread.loadAllResources(mAppInfoAdapter.getAppList());
}
private void initComputeSizes() {
// initiate compute pkg sizes
if (localLOGV) Log.i(TAG, "Initiating compute sizes for first time");
if (mAppInfoAdapter.getCount() > 0) {
mObserver.invokeGetSizeInfo(mAppInfoAdapter.getApplicationInfo(0),
COMPUTE_PKG_SIZE_DONE);
} else {
mComputeSizes = true;
}
}
private void showEmptyViewIfListEmpty() {
if (localLOGV) Log.i(TAG, "Checking for empty view");
if (mAppInfoAdapter.getCount() > 0) {
mEmptyView.setVisibility(View.GONE);
} else {
mEmptyView.setVisibility(View.VISIBLE);
}
}
private void createListView() {
dismissLoadingMsg();
// get list and set listeners and adapter
ListView lv= (ListView) findViewById(android.R.id.list);
lv.setAdapter(mAppInfoAdapter);
lv.setOnItemClickListener(this);
lv.setSaveEnabled(true);
lv.setItemsCanFocus(true);
lv.setOnItemClickListener(this);
showEmptyViewIfListEmpty();
}
// internal structure used to track added and deleted packages when
// the activity has focus
class AddRemoveInfo {
String pkgName;
boolean add;
public AddRemoveInfo(String pPkgName, boolean pAdd) {
pkgName = pPkgName;
add = pAdd;
}
}
class ResourceLoaderThread extends Thread {
List<ApplicationInfo> mAppList;
void loadAllResources(List<ApplicationInfo> appList) {
mAppList = appList;
start();
}
public void run() {
Map<String, AppInfo> iconMap = new HashMap<String, AppInfo>();
if(mAppList == null || mAppList.size() <= 0) {
Log.w(TAG, "Empty or null application list");
} else {
for (ApplicationInfo appInfo : mAppList) {
CharSequence appName = appInfo.loadLabel(mPm);
Drawable appIcon = appInfo.loadIcon(mPm);
iconMap.put(appInfo.packageName,
new AppInfo(appInfo.packageName, appName, appIcon));
}
}
Message msg = mHandler.obtainMessage(REFRESH_ICONS);
msg.obj = iconMap;
mHandler.sendMessage(msg);
}
}
/* Internal class representing an application or packages displayable attributes
*
*/
class AppInfo {
public String pkgName;
int index;
public CharSequence appName;
public Drawable appIcon;
public CharSequence appSize;
public PackageStats appStats;
public void refreshIcon(AppInfo pInfo) {
appName = pInfo.appName;
appIcon = pInfo.appIcon;
}
public AppInfo(String pName, CharSequence aName, Drawable aIcon) {
index = -1;
pkgName = pName;
appName = aName;
appIcon = aIcon;
appStats = null;
appSize = mComputingSizeStr;
}
public AppInfo(String pName, int pIndex, CharSequence aName, Drawable aIcon,
PackageStats ps) {
index = pIndex;
pkgName = pName;
appName = aName;
appIcon = aIcon;
if(ps == null) {
appSize = mComputingSizeStr;
} else {
appStats = ps;
appSize = getSizeStr();
}
}
public void setSize(PackageStats ps) {
appStats = ps;
if (ps != null) {
appSize = getSizeStr();
}
}
public long getTotalSize() {
PackageStats ps = appStats;
if (ps != null) {
return ps.cacheSize+ps.codeSize+ps.dataSize;
}
return SIZE_INVALID;
}
private String getSizeStr() {
PackageStats ps = appStats;
String retStr = "";
// insert total size information into map to display in view
// at this point its guaranteed that ps is not null. but checking anyway
if (ps != null) {
long size = getTotalSize();
if (size == SIZE_INVALID) {
return mInvalidSizeStr.toString();
}
return Formatter.formatFileSize(ManageApplications.this, size);
}
return retStr;
}
}
// 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 {
private Map<String, AppInfo> mAppPropMap;
private List<ApplicationInfo> mAppLocalList;
ApplicationInfo.DisplayNameComparator mAlphaComparator;
AppInfoComparator mSizeComparator;
private AppInfo getFromCache(String packageName) {
if(mAppPropCache == null) {
return null;
}
return mAppPropCache.get(packageName);
}
public AppInfoAdapter(Context c, List<ApplicationInfo> appList) {
mAppLocalList = appList;
boolean useCache = false;
int sortOrder = SORT_ORDER_ALPHA;
int imax = mAppLocalList.size();
if(mAppPropCache != null) {
useCache = true;
// Activity has been resumed. can use the cache to populate values initially
mAppPropMap = mAppPropCache;
sortOrder = mSortOrder;
}
sortAppList(sortOrder);
// Recreate property map
mAppPropMap = new TreeMap<String, AppInfo>();
for (int i = 0; i < imax; i++) {
ApplicationInfo info = mAppLocalList.get(i);
AppInfo aInfo = getFromCache(info.packageName);
if(aInfo == null){
aInfo = new AppInfo(info.packageName, i,
info.packageName, mDefaultAppIcon, null);
} else {
aInfo.index = i;
}
mAppPropMap.put(info.packageName, aInfo);
}
}
public int getCount() {
return mAppLocalList.size();
}
public Object getItem(int position) {
return mAppLocalList.get(position);
}
/*
* This method returns the index of the package position in the application list
*/
public int getIndex(String pkgName) {
if(pkgName == null) {
Log.w(TAG, "Getting index of null package in List Adapter");
}
int imax = mAppLocalList.size();
ApplicationInfo appInfo;
for(int i = 0; i < imax; i++) {
appInfo = mAppLocalList.get(i);
if(appInfo.packageName.equalsIgnoreCase(pkgName)) {
return i;
}
}
return -1;
}
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 void addApplicationInfo(ApplicationInfo info) {
if(info == null) {
Log.w(TAG, "Ignoring null add in List Adapter");
return;
}
mAppLocalList.add(info);
}
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;
}
return mAppPropMap.get(mAppLocalList.get(position).packageName).index;
}
public List<ApplicationInfo> getAppList() {
return mAppLocalList;
}
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 unneccessary 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 = mAppPropMap.get(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();
ApplicationInfo info;
for (int i = 0; i < imax; i++) {
info = mAppLocalList.get(i);
mAppPropMap.get(info.packageName).index = i;
}
}
public void sortAppList(int sortOrder) {
Collections.sort(mAppLocalList, getAppComparator(sortOrder));
}
public void sortList(int sortOrder) {
sortAppList(sortOrder);
adjustIndex();
notifyDataSetChanged();
}
public boolean resetAppList(int filterOption, List<ApplicationInfo> appList) {
// Create application list based on the filter value
mAppLocalList = appList;
// Check for all properties in map before sorting. Populate values from cache
for(ApplicationInfo applicationInfo : mAppLocalList) {
AppInfo appInfo = mAppPropMap.get(applicationInfo.packageName);
if(appInfo == null) {
AppInfo rInfo = getFromCache(applicationInfo.packageName);
if(rInfo == null) {
// Need to load resources again. Inconsistency somewhere
return false;
}
mAppPropMap.put(applicationInfo.packageName, rInfo);
}
}
if (mAppLocalList.size() > 0) {
sortList(mSortOrder);
} else {
notifyDataSetChanged();
}
showEmptyViewIfListEmpty();
return true;
}
private Comparator<ApplicationInfo> getAppComparator(int sortOrder) {
if (sortOrder == SORT_ORDER_ALPHA) {
// Lazy initialization
if (mAlphaComparator == null) {
mAlphaComparator = new ApplicationInfo.DisplayNameComparator(mPm);
}
return mAlphaComparator;
}
// Lazy initialization
if(mSizeComparator == null) {
mSizeComparator = new AppInfoComparator(mAppPropMap);
}
return mSizeComparator;
}
public void updateAppsResourceInfo(Map<String, AppInfo> iconMap) {
if(iconMap == null) {
Log.w(TAG, "Null iconMap when refreshing icon in List Adapter");
return;
}
boolean changed = false;
for (ApplicationInfo info : mAppLocalList) {
AppInfo pInfo = iconMap.get(info.packageName);
if(pInfo != null) {
AppInfo aInfo = mAppPropMap.get(info.packageName);
aInfo.refreshIcon(pInfo);
changed = true;
}
}
if(changed) {
notifyDataSetChanged();
}
}
public void addToList(String pkgName, PackageStats ps) {
if(pkgName == null) {
Log.w(TAG, "Adding null pkg to List Adapter");
return;
}
ApplicationInfo info;
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;
}
// 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) {
Log.i(TAG, "Strange. Package:"+pkgName+" is not new");
return;
}
// New entry
newIdx = -newIdx-1;
mAppLocalList.add(newIdx, info);
mAppPropMap.put(info.packageName, new AppInfo(pkgName, newIdx,
info.loadLabel(mPm), info.loadIcon(mPm), ps));
adjustIndex();
notifyDataSetChanged();
}
public void removeFromList(List<String> pkgNames) {
if(pkgNames == null) {
Log.w(TAG, "Removing null pkg list from List Adapter");
return;
}
int imax = mAppLocalList.size();
boolean found = false;
ApplicationInfo info;
int i, k;
String pkgName;
int kmax = pkgNames.size();
if(kmax <= 0) {
Log.w(TAG, "Removing empty pkg list from List Adapter");
return;
}
int idxArr[] = new int[kmax];
for (k = 0; k < kmax; k++) {
idxArr[k] = -1;
}
for (i = 0; i < imax; i++) {
info = mAppLocalList.get(i);
for (k = 0; k < kmax; k++) {
pkgName = pkgNames.get(k);
if (info.packageName.equalsIgnoreCase(pkgName)) {
idxArr[k] = i;
found = true;
break;
}
}
}
// Sort idxArr
Arrays.sort(idxArr);
// remove the packages based on decending indices
for (k = kmax-1; k >= 0; k--) {
// Check if package has been found in the list of existing apps first
if(idxArr[k] == -1) {
break;
}
info = mAppLocalList.get(idxArr[k]);
mAppLocalList.remove(idxArr[k]);
mAppPropMap.remove(info.packageName);
if (localLOGV) Log.i(TAG, "Removed pkg:"+info.packageName+ " list");
}
if (found) {
adjustIndex();
notifyDataSetChanged();
}
}
public void updateAppSize(String pkgName, PackageStats ps) {
if(pkgName == null) {
return;
}
AppInfo entry = mAppPropMap.get(pkgName);
if (entry == null) {
Log.w(TAG, "Entry for package:"+pkgName+"doesnt exist in map");
return;
}
// Copy the index into the newly updated entry
entry.setSize(ps);
notifyDataSetChanged();
}
public PackageStats getAppStats(String pkgName) {
if(pkgName == null) {
return null;
}
AppInfo entry = mAppPropMap.get(pkgName);
if (entry == null) {
return null;
}
return entry.appStats;
}
}
/*
* 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_PKG_SIZE_DONE);
mHandler.removeMessages(REMOVE_PKG);
mHandler.removeMessages(REORDER_LIST);
mHandler.removeMessages(ADD_PKG_START);
mHandler.removeMessages(ADD_PKG_DONE);
}
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 {
private ApplicationInfo mAppInfo;
private int mMsgId;
public void onGetStatsCompleted(PackageStats pStats, boolean pSucceeded) {
if(DEBUG_PKG_DELAY) {
try {
Thread.sleep(10*1000);
} catch (InterruptedException e) {
}
}
AppInfo appInfo = null;
Bundle data = new Bundle();
data.putString(ATTR_PKG_NAME, mAppInfo.packageName);
if(pSucceeded && pStats != null) {
if (localLOGV) Log.i(TAG, "onGetStatsCompleted::"+pStats.packageName+", ("+
pStats.cacheSize+","+
pStats.codeSize+", "+pStats.dataSize);
data.putParcelable(ATTR_APP_PKG_STATS, pStats);
} else {
Log.w(TAG, "Invalid package stats from PackageManager");
}
//post message to Handler
Message msg = mHandler.obtainMessage(mMsgId, data);
msg.setData(data);
mHandler.sendMessage(msg);
}
public void invokeGetSizeInfo(ApplicationInfo pAppInfo, int msgId) {
if(pAppInfo == null || pAppInfo.packageName == null) {
return;
}
if(localLOGV) Log.i(TAG, "Invoking getPackageSizeInfo for package:"+
pAppInfo.packageName);
mMsgId = msgId;
mAppInfo = pAppInfo;
mPm.getPackageSizeInfo(pAppInfo.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);
}
@Override
public void onReceive(Context context, Intent intent) {
String actionStr = intent.getAction();
Uri data = intent.getData();
String pkgName = data.getEncodedSchemeSpecificPart();
if (localLOGV) Log.i(TAG, "action:"+actionStr+", for package:"+pkgName);
updatePackageList(actionStr, pkgName);
}
}
private void updatePackageList(String actionStr, String pkgName) {
// technically we dont have to invoke handler since onReceive is invoked on
// the main thread but doing it here for better clarity
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);
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Intent lIntent = getIntent();
String action = lIntent.getAction();
if (action.equals(Intent.ACTION_MANAGE_PACKAGE_STORAGE)) {
mSortOrder = SORT_ORDER_SIZE;
mSizesFirst = true;
}
mPm = getPackageManager();
// initialize some window features
requestWindowFeature(Window.FEATURE_RIGHT_ICON);
requestWindowFeature(Window.FEATURE_PROGRESS);
requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
setContentView(R.layout.compute_sizes);
// init mLoadingDlg
mLoadingDlg = new ProgressDialog(this);
mLoadingDlg.setProgressStyle(ProgressDialog.STYLE_SPINNER);
mLoadingDlg.setMessage(getText(R.string.loading));
mLoadingDlg.setIndeterminate(true);
mLoadingDlg.setOnCancelListener(this);
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);
mReceiver = new PackageIntentReceiver();
mEmptyView = (TextView) findViewById(R.id.empty_view);
mObserver = new PkgSizeObserver();
}
private void showLoadingMsg() {
if (mLoadingDlg != null) {
if(localLOGV) Log.i(TAG, "Displaying Loading message");
mLoadingDlg.show();
}
}
private void dismissLoadingMsg() {
if ((mLoadingDlg != null) && (mLoadingDlg.isShowing())) {
if(localLOGV) Log.i(TAG, "Dismissing Loading message");
mLoadingDlg.dismiss();
}
}
@Override
public void onStart() {
super.onStart();
showLoadingMsg();
// Create a thread to load resources
mResourceThread = new ResourceLoaderThread();
sendMessageToHandler(INIT_PKG_INFO);
}
@Override
public void onStop() {
super.onStop();
// clear all messages related to application list
clearMessagesInHandler();
// register receiver here
unregisterReceiver(mReceiver);
mAppPropCache = mAppInfoAdapter.mAppPropMap;
}
/*
* comparator class used to sort AppInfo objects based on size
*/
public static class AppInfoComparator implements Comparator<ApplicationInfo> {
public AppInfoComparator(Map<String, AppInfo> pAppPropMap) {
mAppPropMap= pAppPropMap;
}
public final int compare(ApplicationInfo a, ApplicationInfo b) {
AppInfo ainfo = mAppPropMap.get(a.packageName);
AppInfo binfo = mAppPropMap.get(b.packageName);
long atotal = ainfo.getTotalSize();
long btotal = binfo.getTotalSize();
long ret = atotal - btotal;
// negate result to sort in descending order
if (ret < 0) {
return 1;
}
if (ret == 0) {
return 0;
}
return -1;
}
private Map<String, AppInfo> mAppPropMap;
}
// utility method used to start sub activity
private void startApplicationDetailsActivity(ApplicationInfo info, PackageStats ps) {
// Create intent to start new activity
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setClass(this, InstalledAppDetails.class);
mCurrentPkgName = info.packageName;
intent.putExtra(APP_PKG_NAME, mCurrentPkgName);
intent.putExtra(APP_PKG_SIZE, ps);
// 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);
menu.add(0, FILTER_OPTIONS, 3, R.string.filter)
.setIcon(R.drawable.ic_menu_filter_settings);
return true;
}
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
if (mComputeSizes) {
menu.findItem(SORT_ORDER_ALPHA).setVisible(mSortOrder != SORT_ORDER_ALPHA);
menu.findItem(SORT_ORDER_SIZE).setVisible(mSortOrder != SORT_ORDER_SIZE);
menu.findItem(FILTER_OPTIONS).setVisible(true);
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);
} else if (menuId == FILTER_OPTIONS) {
if (mAlertDlgBuilder == null) {
mAlertDlgBuilder = new AlertDialog.Builder(this).
setTitle(R.string.filter_dlg_title).
setNeutralButton(R.string.cancel, this).
setSingleChoiceItems(new CharSequence[] {getText(R.string.filter_apps_all),
getText(R.string.filter_apps_running),
getText(R.string.filter_apps_third_party)},
-1, this);
}
mAlertDlgBuilder.show();
}
return true;
}
public void onItemClick(AdapterView<?> parent, View view, int position,
long id) {
ApplicationInfo info = (ApplicationInfo)mAppInfoAdapter.getItem(position);
startApplicationDetailsActivity(info, mAppInfoAdapter.getAppStats(info.packageName));
}
// onCancel call back for dialog thats displayed when data is being loaded
public void onCancel(DialogInterface dialog) {
mLoadingDlg = null;
finish();
}
public void onClick(DialogInterface dialog, int which) {
int newOption;
switch (which) {
// Make sure that values of 0, 1, 2 match options all, running, third_party when
// created via the AlertDialog.Builder
case 0:
newOption = FILTER_APPS_ALL;
break;
case 1:
newOption = FILTER_APPS_RUNNING;
break;
case 2:
newOption = FILTER_APPS_THIRD_PARTY;
break;
default:
return;
}
sendMessageToHandler(REORDER_LIST, newOption);
}
}