/* * 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.Dialog; 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.SharedPreferences; import android.content.pm.ApplicationInfo; import android.content.pm.IPackageStatsObserver; import android.content.pm.PackageManager; import android.content.pm.PackageStats; import android.content.pm.PackageManager.NameNotFoundException; import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.os.SystemClock; import android.text.format.Formatter; import android.util.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.Filter; import android.widget.Filterable; import android.widget.ImageView; import android.widget.ListView; 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 ListActivity implements OnItemClickListener, DialogInterface.OnCancelListener, DialogInterface.OnClickListener { // 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 = Config.LOGV || false; private static final boolean DEBUG_SIZE = false; private static final boolean DEBUG_TIME = false; // attributes used as keys when passing values to InstalledAppDetails activity public static final String APP_PKG_PREFIX = "com.android.settings."; public static final String APP_PKG_NAME = APP_PKG_PREFIX+"ApplicationPkgName"; 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="p"; private static final String ATTR_PKGS="ps"; private static final String ATTR_STATS="ss"; private static final String ATTR_SIZE_STRS="fs"; private static final String ATTR_GET_SIZE_STATUS="passed"; private static final String ATTR_PKG_STATS="s"; private static final String ATTR_PKG_SIZE_STR="f"; // constant value that can be used to check return code from sub activity. private static final int INSTALLED_APP_DETAILS = 1; // sort order that can be changed through the menu can be sorted alphabetically // or size(descending) private static final int MENU_OPTIONS_BASE = 0; // Filter options used for displayed list of applications public static final int FILTER_APPS_ALL = MENU_OPTIONS_BASE + 0; public static final int FILTER_APPS_RUNNING = MENU_OPTIONS_BASE + 1; public static final int FILTER_APPS_THIRD_PARTY = MENU_OPTIONS_BASE + 2; public static final int FILTER_OPTIONS = MENU_OPTIONS_BASE + 3; public static final int SORT_ORDER_ALPHA = MENU_OPTIONS_BASE + 4; public static final int SORT_ORDER_SIZE = MENU_OPTIONS_BASE + 5; // Alert Dialog presented to user to find out the filter option private AlertDialog mAlertDlg; // 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 mComputeSizes = 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 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; String mCurrentPkgName; // Cache application attributes private AppInfoCache mCache = new AppInfoCache(); // Boolean variables indicating state private boolean mLoadLabels = false; private boolean mSizesFirst = false; // ListView used to display list private ListView mListView; // State variables used to figure out menu options and also // initiate the first computation and loading of resources private boolean mJustCreated = true; private boolean mFirst = false; private long mLoadTimeStart; private boolean mSetListViewLater = true; /* * Handler class to handle messages for various operations * Most of the operations that effect Application related data * are posted as messages to the handler to avoid synchronization * when accessing these structures. * When the size retrieval gets kicked off for the first time, a COMPUTE_PKG_SIZE_START * message is posted to the handler which invokes the getSizeInfo for the pkg at index 0 * When the PackageManager's asynchronous call back through * PkgSizeObserver.onGetStatsCompleted gets invoked, the application resources like * label, description, icon etc., 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) { boolean status; long size; String formattedSize; ApplicationInfo info; Bundle data; String pkgName = null; AppInfo appInfo; data = msg.getData(); if(data != null) { pkgName = data.getString(ATTR_PKG_NAME); } switch (msg.what) { case INIT_PKG_INFO: if(localLOGV) Log.i(TAG, "Message INIT_PKG_INFO, justCreated = " + mJustCreated); List 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 : mComputeSizes = 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 (!mComputeSizes) { Boolean currB = mAddRemoveMap.get(pkgName); if (currB == null || (currB.equals(Boolean.TRUE))) { mAddRemoveMap.put(pkgName, Boolean.FALSE); } break; } List pkgList = new ArrayList(); 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 (!mComputeSizes || !mLoadLabels) { Boolean currB = mAddRemoveMap.get(pkgName); if (currB == null || (currB.equals(Boolean.FALSE))) { mAddRemoveMap.put(pkgName, Boolean.TRUE); } break; } try { info = mPm.getApplicationInfo(pkgName, 0); } catch (NameNotFoundException e) { Log.w(TAG, "Couldnt find application info for:"+pkgName); break; } mObserver.invokeGetSizeInfo(pkgName); break; case ADD_PKG_DONE: if(localLOGV) Log.i(TAG, "Message ADD_PKG_DONE"); if(pkgName == null) { Log.w(TAG, "Ignoring message:ADD_PKG_START for null pkgName"); break; } status = data.getBoolean(ATTR_GET_SIZE_STATUS); if (status) { size = data.getLong(ATTR_PKG_STATS); formattedSize = data.getString(ATTR_PKG_SIZE_STR); if (!mAppInfoAdapter.isInstalled(pkgName)) { mAppInfoAdapter.addToList(pkgName, size, formattedSize); } else { mAppInfoAdapter.updatePackage(pkgName, size, formattedSize); } } break; case REFRESH_LABELS: Map labelMap = (Map) msg.obj; if (labelMap != null) { mAppInfoAdapter.bulkUpdateLabels(labelMap); } break; case REFRESH_ICONS: Map iconMap = (Map) msg.obj; if (iconMap != null) { mAppInfoAdapter.bulkUpdateIcons(iconMap); } break; case REFRESH_DONE: mLoadLabels = 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 (mComputeSizes && mLoadLabels) { doneLoadingData(); // Check for added/removed packages Set 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 (!mComputeSizes && !mLoadLabels) { // 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 (!mComputeSizes) { initComputeSizes(); } else if (!mLoadLabels) { initResourceThread(); } } break; default: break; } } }; private void initListView() { // Create list view from the adapter here. Wait till the sort order // of list is defined. its either by label or by size. So atleast one of the // first steps should have been completed before the list gets filled. mAppInfoAdapter.sortBaseList(mSortOrder); if (mJustCreated) { // Set the adapter here. mJustCreated = false; mListView.setAdapter(mAppInfoAdapter); dismissLoadingMsg(); } } class SizeObserver extends IPackageStatsObserver.Stub { private CountDownLatch mCount; PackageStats stats; boolean succeeded; public void invokeGetSize(String packageName, CountDownLatch count) { mCount = count; mPm.getPackageSizeInfo(packageName, this); } public void onGetStatsCompleted(PackageStats pStats, boolean pSucceeded) { succeeded = pSucceeded; stats = pStats; mCount.countDown(); } } class TaskRunner extends Thread { private List 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 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 newList) { if ((newList == null) || mCache.isEmpty()) { return false; } Set existingList = new HashSet(); 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 deletedList = null; Set 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(); 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 getInstalledApps(int filterOption) { List installedAppList = mPm.getInstalledApplications( PackageManager.GET_UNINSTALLED_PACKAGES); if (installedAppList == null) { return new ArrayList (); } if (filterOption == FILTER_APPS_THIRD_PARTY) { List appList =new ArrayList (); 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 appList =new ArrayList (); List procList = getRunningAppProcessesList(); if ((procList == null) || (procList.size() == 0)) { return appList; } // Retrieve running processes from ActivityManager for (ActivityManager.RunningAppProcessInfo appProcInfo : procList) { if ((appProcInfo != null) && (appProcInfo.pkgList != null)){ int size = appProcInfo.pkgList.length; for (int i = 0; i < size; i++) { ApplicationInfo appInfo = null; try { appInfo = mPm.getApplicationInfo(appProcInfo.pkgList[i], PackageManager.GET_UNINSTALLED_PACKAGES); } catch (NameNotFoundException e) { Log.w(TAG, "Error retrieving ApplicationInfo for pkg:"+appProcInfo.pkgList[i]); continue; } if(appInfo != null) { appList.add(appInfo); } } } } return appList; } else { return installedAppList; } } private static boolean matchFilter(boolean filter, Map 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 getFilteredApps(List pAppList, int filterOption, boolean filter, Map filterMap) { List retList = new ArrayList(); if(pAppList == null) { return retList; } if (filterOption == FILTER_APPS_THIRD_PARTY) { for (ApplicationInfo appInfo : pAppList) { boolean flag = false; if ((appInfo.flags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0) { // Updated system app flag = true; } else if ((appInfo.flags & ApplicationInfo.FLAG_SYSTEM) == 0) { // Non-system app flag = true; } if (flag) { if (matchFilter(filter, filterMap, appInfo.packageName)) { retList.add(appInfo); } } } return retList; } else if (filterOption == FILTER_APPS_RUNNING) { List procList = getRunningAppProcessesList(); if ((procList == null) || (procList.size() == 0)) { return retList; } // Retrieve running processes from ActivityManager HashMap runningMap = new HashMap(); for (ActivityManager.RunningAppProcessInfo appProcInfo : procList) { if ((appProcInfo != null) && (appProcInfo.pkgList != null)){ int size = appProcInfo.pkgList.length; for (int i = 0; i < size; i++) { runningMap.put(appProcInfo.pkgList[i], appProcInfo); } } } // Query list to find running processes in current list for (ApplicationInfo appInfo : pAppList) { if (runningMap.get(appInfo.packageName) != null) { if (matchFilter(filter, filterMap, appInfo.packageName)) { retList.add(appInfo); } } } return retList; } else { for (ApplicationInfo appInfo : pAppList) { if (matchFilter(filter, filterMap, appInfo.packageName)) { retList.add(appInfo); } } return retList; } } private List getRunningAppProcessesList() { ActivityManager am = (ActivityManager)getSystemService(Context.ACTIVITY_SERVICE); return am.getRunningAppProcesses(); } // Some initialization code used when kicking off the size computation private void initAppList(List appList, int filterOption) { setProgressBarIndeterminateVisibility(true); mComputeSizes = false; mLoadLabels = false; // Initialize lists mAddRemoveMap = new TreeMap(); 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 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 appList = mAppInfoAdapter.getBaseAppList(); if ((appList != null) && (appList.size()) > 0) { mSizeComputor = new TaskRunner(appList); } else { mComputeSizes = 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 mAppList; volatile boolean abort = false; static final int MSG_PKG_SIZE = 8; public void setAbort() { abort = true; } void loadAllResources(List 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 map = new HashMap(); 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 map = new HashMap(); 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 mAppList; private List mAppLocalList; private Map mFilterMap = new HashMap(); AlphaComparator mAlphaComparator = new AlphaComparator(); SizeComparator mSizeComparator = new SizeComparator(); private Filter mAppFilter = new AppFilter(); final private Object mFilterLock = new Object(); private Map mCurrentFilterMap = null; private void generateFilterListLocked(List list) { mAppLocalList = new ArrayList(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 pAppList, int filterOption) { boolean notify = false; List appList = null; if (pAppList == null) { // Just refresh the list appList = mAppList; } else { mAppList = new ArrayList(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 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 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 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 = 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 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 getAppComparator(int sortOrder) { if (sortOrder == SORT_ORDER_ALPHA) { return mAlphaComparator; } return mSizeComparator; } public void bulkUpdateIcons(Map icons) { if (icons == null) { return; } Set 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 map) { if (map == null) { return; } Set keys = map.keySet(); boolean changed = false; for (String key : keys) { CharSequence label = map.get(key); AppInfo aInfo = mCache.getEntry(key); if (aInfo != null) { aInfo.refreshLabel(label); changed = true; } } if (changed) { notifyDataSetChanged(); } } private boolean shouldBeInList(int filterOption, ApplicationInfo info) { // Match filter here if (filterOption == FILTER_APPS_RUNNING) { List runningList = getInstalledApps(FILTER_APPS_RUNNING); for (ApplicationInfo running : runningList) { if (running.packageName.equalsIgnoreCase(info.packageName)) { return true; } } } else if (filterOption == FILTER_APPS_THIRD_PARTY) { if ((info.flags & ApplicationInfo.FLAG_SYSTEM) == 0) { return true; } else if ((info.flags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0) { return true; } } else { 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 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(mFilterMap); results.count = mFilterMap.size(); } } else { final String prefixString = prefix.toString().toLowerCase(); final String spacePrefixString = " " + prefixString; Map newMap = new HashMap(); synchronized (mFilterLock) { Map localMap = mFilterMap; Set 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) 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); } @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); if(localLOGV) Log.i(TAG, "Activity created"); long sCreate; if (DEBUG_TIME) { sCreate = SystemClock.elapsedRealtime(); } Intent intent = getIntent(); String action = intent.getAction(); if (action.equals(Intent.ACTION_MANAGE_PACKAGE_STORAGE)) { mSortOrder = SORT_ORDER_SIZE; mFilterApps = FILTER_APPS_ALL; 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); showLoadingMsg(); mDefaultAppIcon =Resources.getSystem().getDrawable( com.android.internal.R.drawable.sym_def_app_icon); mInvalidSizeStr = getText(R.string.invalid_size_value); mComputingSizeStr = getText(R.string.computing_size); // initialize the inflater mInflater = (LayoutInflater)getSystemService(Context.LAYOUT_INFLATER_SERVICE); mReceiver = new PackageIntentReceiver(); mObserver = new PkgSizeObserver(); // Create adapter and list view here List appList = getInstalledApps(mSortOrder); mAppInfoAdapter = new AppInfoAdapter(this, appList); ListView lv= (ListView) findViewById(android.R.id.list); lv.setOnItemClickListener(this); lv.setSaveEnabled(true); lv.setItemsCanFocus(true); lv.setOnItemClickListener(this); lv.setTextFilterEnabled(true); mListView = lv; if (DEBUG_TIME) { Log.i(TAG, "Total time in Activity.create:: " + (SystemClock.elapsedRealtime() - sCreate)+ " ms"); } // Get initial info from file for the very first time this activity started long sStart; if (DEBUG_TIME) { sStart = SystemClock.elapsedRealtime(); } mCache.loadCache(); if (DEBUG_TIME) { Log.i(TAG, "Took " + (SystemClock.elapsedRealtime()-sStart) + " ms to init cache"); } } protected void onDestroy() { // Persist values in cache mCache.updateCache(); super.onDestroy(); } @Override public Dialog onCreateDialog(int id) { if (id == DLG_LOADING) { ProgressDialog dlg = new ProgressDialog(this); dlg.setProgressStyle(ProgressDialog.STYLE_SPINNER); dlg.setMessage(getText(R.string.loading)); dlg.setIndeterminate(true); dlg.setOnCancelListener(this); return dlg; } return null; } private void showLoadingMsg() { if (DEBUG_TIME) { mLoadTimeStart = SystemClock.elapsedRealtime(); } showDialog(DLG_LOADING); if(localLOGV) Log.i(TAG, "Displaying Loading message"); } private void dismissLoadingMsg() { if(localLOGV) Log.i(TAG, "Dismissing Loading message"); dismissDialog(DLG_LOADING); if (DEBUG_TIME) Log.i(TAG, "Displayed loading message for "+ (SystemClock.elapsedRealtime() - mLoadTimeStart) + " ms"); } class AppInfoCache { final static boolean FILE_CACHE = true; private static final String mFileCacheName="ManageAppsInfo.txt"; private static final int FILE_BUFFER_SIZE = 1024; private static final boolean DEBUG_CACHE = false; private static final boolean DEBUG_CACHE_TIME = false; private Map mAppPropCache = new HashMap(); private boolean isEmpty() { return (mAppPropCache.size() == 0); } private AppInfo getEntry(String pkgName) { return mAppPropCache.get(pkgName); } private Set 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 great then 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 keys = mAppPropCache.keySet(); byte[] lenBytes = new byte[2]; for (String key : keys) { AppInfo aInfo = mAppPropCache.get(key); StringBuilder buff = new StringBuilder(aInfo.pkgName); buff.append(","); buff.append(aInfo.size); buff.append(","); buff.append(aInfo.appSize); buff.append(","); buff.append(aInfo.appName); if (DEBUG_CACHE) { Log.i(TAG, "Writing str : " + buff.toString() + " to file of length:" + buff.toString().length()); } try { byte[] byteBuff = buff.toString().getBytes(); int len = byteBuff.length; if (byteBuff.length >= FILE_BUFFER_SIZE) { // Truncate the output len = FILE_BUFFER_SIZE; } // Use 2 bytes to write length lenBytes[1] = (byte) (len & 0x00ff); lenBytes[0] = (byte) ((len & 0x00ff00) >> 8); fos.write(lenBytes, 0, 2); fos.write(byteBuff, 0, len); } catch (IOException e) { Log.w(TAG, "Failed to write to file : " + cacheFile + " with exception : " + e); return false; } } if (DEBUG_CACHE_TIME) { Log.i(TAG, "Took " + (SystemClock.uptimeMillis() - opStartTime) + " ms to write and process from file"); } return true; } catch (FileNotFoundException e) { Log.w(TAG, "Error opening file for write operation : " + cacheFile+ " with exception : " + e); return false; } finally { if (fos != null) { try { fos.close(); } catch (IOException e) { Log.w(TAG, "Failed closing file : " + cacheFile + " with exception : " + e); return false; } } } } private void loadCache() { // Restore preferences SharedPreferences settings = getSharedPreferences(PREFS_NAME, 0); boolean disable = settings.getBoolean(PREF_DISABLE_CACHE, true); if (disable) Log.w(TAG, "Cache has been disabled"); // Disable cache till the data is loaded successfully SharedPreferences.Editor editor = settings.edit(); editor.putBoolean(PREF_DISABLE_CACHE, true); editor.commit(); if (FILE_CACHE && !disable) { readFromFile(); // Enable cache since the file has been read successfully editor.putBoolean(PREF_DISABLE_CACHE, false); editor.commit(); } } private void updateCache() { SharedPreferences settings = getSharedPreferences(PREFS_NAME, 0); SharedPreferences.Editor editor = settings.edit(); editor.putBoolean(PREF_DISABLE_CACHE, true); editor.commit(); if (FILE_CACHE) { boolean writeStatus = writeToFile(); mAppPropCache.clear(); if (writeStatus) { // Enable cache since the file has been read successfully editor.putBoolean(PREF_DISABLE_CACHE, false); editor.commit(); } } } } @Override public void onStart() { super.onStart(); // Register receiver mReceiver.registerReceiver(); sendMessageToHandler(INIT_PKG_INFO); } @Override public void onStop() { super.onStop(); // Stop the background threads if (mResourceThread != null) { mResourceThread.setAbort(); } if (mSizeComputor != null) { mSizeComputor.setAbort(); } // clear all messages related to application list clearMessagesInHandler(); // register receiver here unregisterReceiver(mReceiver); } // Avoid the restart and pause when orientation changes @Override public void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); } /* * comparator class used to sort AppInfo objects based on size */ class SizeComparator implements Comparator { 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 { private final Collator sCollator = Collator.getInstance(); public final int compare(ApplicationInfo a, ApplicationInfo b) { AppInfo ainfo = mCache.getEntry(a.packageName); AppInfo binfo = mCache.getEntry(b.packageName); // Check for null app names, to avoid NPE in rare cases if (ainfo == null || ainfo.appName == null) return -1; if (binfo == null || binfo.appName == null) return 1; return sCollator.compare(ainfo.appName.toString(), binfo.appName.toString()); } } // utility method used to start sub activity private void startApplicationDetailsActivity() { // Create intent to start new activity Intent intent = new Intent(Intent.ACTION_VIEW); intent.setClass(this, InstalledAppDetails.class); intent.putExtra(APP_PKG_NAME, mCurrentPkgName); // start new activity to display extended information startActivityForResult(intent, INSTALLED_APP_DETAILS); } @Override public boolean onCreateOptionsMenu(Menu menu) { menu.add(0, SORT_ORDER_ALPHA, 1, R.string.sort_order_alpha) .setIcon(android.R.drawable.ic_menu_sort_alphabetically); menu.add(0, SORT_ORDER_SIZE, 2, R.string.sort_order_size) .setIcon(android.R.drawable.ic_menu_sort_by_size); 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 (mFirst) { 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) { // Pick up the selection value from the list of added choice items. int selection = mFilterApps - MENU_OPTIONS_BASE; if (mAlertDlg == null) { mAlertDlg = 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)}, selection, this). create(); } mAlertDlg.show(); } 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 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 FILTER_APPS_ALL: break; case FILTER_APPS_RUNNING: break; case FILTER_APPS_THIRD_PARTY: break; default: return; } newOption = which; mAlertDlg.dismiss(); sendMessageToHandler(REORDER_LIST, newOption); } }