diff options
Diffstat (limited to 'src/com/android/settings/applications/ManageApplications.java')
-rw-r--r-- | src/com/android/settings/applications/ManageApplications.java | 2076 |
1 files changed, 2076 insertions, 0 deletions
diff --git a/src/com/android/settings/applications/ManageApplications.java b/src/com/android/settings/applications/ManageApplications.java new file mode 100644 index 0000000..52ea376 --- /dev/null +++ b/src/com/android/settings/applications/ManageApplications.java @@ -0,0 +1,2076 @@ +/* + * 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.applications; + +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.provider.Settings; +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_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_THIRD_PARTY = MENU_OPTIONS_BASE + 1; + public static final int FILTER_APPS_SDCARD = MENU_OPTIONS_BASE + 2; + + 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; + // Custom view used to display running processes + private RunningProcessesView mRunningProcessesView; + // 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; + + // These are for keeping track of activity and tab switch state. + private int mCurView; + private boolean mCreatedRunning; + + private boolean mResumedRunning; + private boolean mActivityResumed; + private Object mNonConfigInstance; + + /* + * 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; + Bundle data; + String pkgName = null; + 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; + } + 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); + } + } + + 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 { + 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 { + for (ApplicationInfo appInfo : pAppList) { + if (matchFilter(filter, filterMap, appInfo.packageName)) { + retList.add(appInfo); + } + } + return retList; + } + } + + // 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_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 int VIEW_NOTHING = 0; + static final int VIEW_LIST = 1; + static final int VIEW_RUNNING = 2; + + private void selectView(int which) { + if (mCurView == which) { + return; + } + + mCurView = which; + + if (which == VIEW_LIST) { + if (mResumedRunning) { + mRunningProcessesView.doPause(); + mResumedRunning = false; + } + mRunningProcessesView.setVisibility(View.GONE); + mListView.setVisibility(View.VISIBLE); + } else if (which == VIEW_RUNNING) { + if (!mCreatedRunning) { + mRunningProcessesView.doCreate(null, mNonConfigInstance); + mCreatedRunning = true; + } + if (mActivityResumed && !mResumedRunning) { + mRunningProcessesView.doResume(); + mResumedRunning = true; + } + mRunningProcessesView.setVisibility(View.VISIBLE); + mListView.setVisibility(View.GONE); + } + } + + 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 (intent.getComponent().getClassName().equals( + "com.android.settings.RunningServices")) { + defaultTabTag = TAB_RUNNING; + } + if (action.equals(Intent.ACTION_MANAGE_PACKAGE_STORAGE)) { + mSortOrder = SORT_ORDER_SIZE; + mFilterApps = FILTER_APPS_ALL; + defaultTabTag = TAB_ALL; + mSizesFirst = true; + } + + if (savedInstanceState != null) { + mSortOrder = savedInstanceState.getInt("sortOrder", mSortOrder); + mFilterApps = savedInstanceState.getInt("filterApps", mFilterApps); + String tmp = savedInstanceState.getString("defaultTabTag"); + if (tmp != null) defaultTabTag = tmp; + mSizesFirst = savedInstanceState.getBoolean("sizesFirst", mSizesFirst); + } + + mNonConfigInstance = getLastNonConfigurationInstance(); + + mPm = getPackageManager(); + // initialize some window features + requestWindowFeature(Window.FEATURE_RIGHT_ICON); + requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS); + 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; + mRunningProcessesView = (RunningProcessesView)mRootView.findViewById( + R.id.running_processes); + 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_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.addTab(tabHost.newTabSpec(TAB_RUNNING) + .setIndicator(getString(R.string.filter_apps_running), + getResources().getDrawable(R.drawable.ic_tab_running)) + .setContent(this)); + tabHost.setCurrentTabByTag(defaultTabTag); + tabHost.setOnTabChangedListener(this); + + selectView(TAB_RUNNING.equals(defaultTabTag) ? VIEW_RUNNING : VIEW_LIST); + } + + @Override + public void onStart() { + super.onStart(); + // Register receiver + mReceiver.registerReceiver(); + sendMessageToHandler(INIT_PKG_INFO); + } + + @Override + protected void onResume() { + super.onResume(); + mActivityResumed = true; + if (mCurView == VIEW_RUNNING) { + mRunningProcessesView.doResume(); + mResumedRunning = true; + } + } + + @Override + protected void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putInt("sortOrder", mSortOrder); + outState.putInt("filterApps", mFilterApps); + outState.putString("defautTabTag", getTabHost().getCurrentTabTag()); + outState.putBoolean("sizesFirst", mSizesFirst); + } + + @Override + public Object onRetainNonConfigurationInstance() { + return mRunningProcessesView.doRetainNonConfigurationInstance(); + } + + @Override + protected void onPause() { + super.onPause(); + mActivityResumed = false; + if (mResumedRunning) { + mRunningProcessesView.doPause(); + mResumedRunning = false; + } + } + + @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); + } + + @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; + } + } + } + + // Avoid the restart and pause when orientation changes + @Override + public void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + } + + @Override + protected void onDestroy() { + // Persist values in cache + mCache.updateCache(); + super.onDestroy(); + } + + 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(); + } + } + } + } + + /* + * 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(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, + Uri.fromParts("package", mCurrentPkgName, null)); + // 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_ALL.equalsIgnoreCase(tabId)) { + newOption = FILTER_APPS_ALL; + } else if (TAB_SDCARD.equalsIgnoreCase(tabId)) { + newOption = FILTER_APPS_SDCARD; + } else if (TAB_RUNNING.equalsIgnoreCase(tabId)) { + selectView(VIEW_RUNNING); + return; + } else { + // Invalid option. Do nothing + return; + } + + selectView(VIEW_LIST); + sendMessageToHandler(REORDER_LIST, newOption); + } +} |