/* * 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.Activity; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.ApplicationInfo; import android.content.pm.IPackageStatsObserver; import android.content.pm.PackageManager; import android.content.pm.PackageStats; import android.content.pm.PackageManager.NameNotFoundException; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.util.Config; import android.util.Log; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.widget.AdapterView; import android.widget.ImageView; import android.widget.ListView; import android.widget.SimpleAdapter; import android.widget.TextView; import android.widget.AdapterView.OnItemClickListener; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.TreeMap; /** * Activity to pick an application that will be used to display installation information and * options to upgrade/uninstall/delete user data for system applications. * Initially a compute in progress message is displayed while the application retrieves * the size information of installed packages which is done asynchronously through a * handler. Once the computation is done package resource information is retrieved * and then the information is displayed on the screen. All * messages are passed through a Handler object. * Known issue: There could be some ordering issues when installing/uninstalling * applications when the application list is being scanned. */ public class ManageApplications extends Activity implements SimpleAdapter.ViewBinder, OnItemClickListener { private static final String TAG = "ManageApplications"; //Application prefix information public static final String APP_PKG_PREFIX="com.android.settings."; public static final String APP_PKG_NAME=APP_PKG_PREFIX+"ApplicationPkgName"; public static final String APP_PKG_SIZE= APP_PKG_PREFIX+"size"; public static final String APP_CHG=APP_PKG_PREFIX+"changed"; //constant value that can be used to check return code from sub activity. private static final int INSTALLED_APP_DETAILS = 1; //application attributes passed to sub activity that displays more app info private static final String KEY_APP_NAME = "ApplicationName"; private static final String KEY_APP_ICON = "ApplicationIcon"; private static final String KEY_APP_DESC = "ApplicationDescription"; private static final String KEY_APP_SIZE= "ApplicationSize"; //sort order that can be changed through the menu public static final int SORT_ORDER_ALPHA = 0; public static final int SORT_ORDER_SIZE = 1; //key and resource values used in constructing map for SimpleAdapter private static final String sKeys[] = new String[] { KEY_APP_NAME, KEY_APP_ICON, KEY_APP_DESC, KEY_APP_SIZE}; private static final int sResourceIds[] = new int[] { R.id.app_name, R.id.app_icon, R.id.app_description, R.id.app_size}; //List of ApplicationInfo objects for various applications private List mAppList; //SimpleAdapter used for managing items in the list private SimpleAdapter mAppAdapter; //map used to store size information which is used for displaying size information //in this activity as well as the subactivity. this is to avoid invoking package manager //api to retrieve size information private HashMap mSizeMap; private HashMap > mAppAdapterMap; //sort order private int mSortOrder = SORT_ORDER_ALPHA; //log information boolean private boolean localLOGV = Config.LOGV || false; private ApplicationInfo mCurrentPkg; private int mCurrentPkgIdx = 0; private static final int COMPUTE_PKG_SIZE_START = 1; private static final int COMPUTE_PKG_SIZE_DONE = 2; private static final int REMOVE_PKG=3; private static final int REORDER_LIST=4; private static final int ADD_PKG=5; private static final String ATTR_APP_IDX="ApplicationIndex"; private static final String ATTR_CHAINED="Chained"; private static final String ATTR_PKG_NAME="PackageName"; private PkgSizeObserver mObserver; private PackageManager mPm; private PackageIntentReceiver mReceiver; private boolean mDoneIniting = false; private String mKbStr; private String mMbStr; private String mBStr; /* * Handler class to handle messages for various operations */ private Handler mHandler = new Handler() { public void handleMessage(Message msg) { PackageStats ps; ApplicationInfo info; Bundle data; String pkgName; int idx; int size; boolean chained = false; data = msg.getData(); switch (msg.what) { case COMPUTE_PKG_SIZE_START: mDoneIniting = false; //initialize lists mAppList = new ArrayList(); mSizeMap = new HashMap(); mAppAdapterMap = new HashMap >(); //update application list from PackageManager mAppList = mPm.getInstalledApplications(0); if(mAppList.size() == 0) { return; } mCurrentPkgIdx = 0; mCurrentPkg = mAppList.get(0); if(localLOGV) Log.i(TAG, "Initiating compute sizes for first time"); //register receiver mReceiver = new PackageIntentReceiver(); mReceiver.registerReceiver(); pkgName = mCurrentPkg.packageName; mObserver = new PkgSizeObserver(0); mObserver.invokeGetSizeInfo(pkgName, true); break; case COMPUTE_PKG_SIZE_DONE: ps = mObserver.ps; info = mObserver.appInfo; chained = data.getBoolean(ATTR_CHAINED); if(!mObserver.succeeded) { if(chained) { removePackageFromAppList(ps.packageName); } else { //do not go to adding phase break; } } else { //insert size value mSizeMap.put(ps.packageName, ps); Map entry = createMapEntry(mPm.getApplicationLabel(info), mPm.getApplicationIcon(info), info.loadDescription(mPm), getSizeStr(ps)); mAppAdapterMap.put(ps.packageName, entry); } if(chained) { //here app list is precomputed idx = data.getInt(ATTR_APP_IDX); //increment only if succeded if(mObserver.succeeded) { idx++; } if(idx < mAppList.size()) { pkgName = mAppList.get(idx).packageName; //increment record index and invoke getSizeInfo for next record mObserver.invokeGetSizeInfo(pkgName, true); } else { sortAppList(); createListFromValues(); mDoneIniting = true; } } else { //add app info object as well mAppList.add(info); sortAppList(); size = mAppList.size(); int i; for(i = 0; i < size; i++) { if(mAppList.get(i).packageName.equalsIgnoreCase(mCurrentPkg.packageName)) { if(i > mCurrentPkgIdx) { mCurrentPkgIdx = i; } break; } } createListFromValues(); } break; case REMOVE_PKG: if(!mDoneIniting) { //insert message again after some delay sendMessageToHandler(REMOVE_PKG, data, 10*1000); break; } pkgName = data.getString(ATTR_PKG_NAME); removePackageFromAppList(pkgName); if(mSizeMap.remove(pkgName) == null) { Log.i(TAG, "Coudnt remove from size map package:"+pkgName); } if(mAppAdapterMap.remove(pkgName) == null) { Log.i(TAG, "Coudnt remove from app adapter map package:"+pkgName); } if(mCurrentPkg.packageName.equalsIgnoreCase(pkgName)) { if(mCurrentPkgIdx == (mAppList.size()-1)) { mCurrentPkgIdx--; } mCurrentPkg = mAppList.get(mCurrentPkgIdx); } createListFromValues(); break; case REORDER_LIST: int sortOrder = msg.arg1; if(sortOrder != mSortOrder) { mSortOrder = sortOrder; if(localLOGV) Log.i(TAG, "Changing sort order to "+mSortOrder); sortAppList(); mCurrentPkgIdx = 0; mCurrentPkg = mAppList.get(mCurrentPkgIdx); createListFromValues(); } break; case ADD_PKG: pkgName = data.getString(ATTR_PKG_NAME); if(!mDoneIniting) { //insert message again after some delay sendMessageToHandler(ADD_PKG, data, 10*1000); break; } mObserver.invokeGetSizeInfo(pkgName, false); break; default: break; } } }; private void removePackageFromAppList(String pkgName) { int size = mAppList.size(); for(int i = 0; i < size; i++) { if(mAppList.get(i).packageName.equalsIgnoreCase(pkgName)) { mAppList.remove(i); break; } } } private void clearMessages() { synchronized(mHandler) { mHandler.removeMessages(COMPUTE_PKG_SIZE_START); mHandler.removeMessages(COMPUTE_PKG_SIZE_DONE); mHandler.removeMessages(REMOVE_PKG); mHandler.removeMessages(REORDER_LIST); mHandler.removeMessages(ADD_PKG); } } private void sendMessageToHandler(int msgId, Bundle data, long delayMillis) { synchronized(mHandler) { Message msg = mHandler.obtainMessage(msgId); msg.setData(data); if(delayMillis == 0) { mHandler.sendMessage(msg); } else { mHandler.sendMessageDelayed(msg, delayMillis); } } } private void sendMessageToHandler(int msgId, int arg1) { synchronized(mHandler) { Message msg = mHandler.obtainMessage(msgId); msg.arg1 = arg1; mHandler.sendMessage(msg); } } private void sendMessageToHandler(int msgId) { synchronized(mHandler) { mHandler.sendEmptyMessage(msgId); } } class PkgSizeObserver extends IPackageStatsObserver.Stub { public PackageStats ps; public ApplicationInfo appInfo; public Drawable appIcon; public CharSequence appName; public CharSequence appDesc = ""; private int mIdx = 0; private boolean mChained = false; public boolean succeeded; PkgSizeObserver(int i) { mIdx = i; } private void getAppDetails() { try { appInfo = mPm.getApplicationInfo(ps.packageName, 0); } catch (NameNotFoundException e) { return; } appName = appInfo.loadLabel(mPm); appIcon = appInfo.loadIcon(mPm); } public void onGetStatsCompleted(PackageStats pStats, boolean pSucceeded) { Bundle data = new Bundle(); ps = pStats; succeeded = pSucceeded; if(mChained) { data.putInt(ATTR_APP_IDX, mIdx); if(succeeded) { mIdx++; } } data.putBoolean(ATTR_CHAINED, mChained); getAppDetails(); if(localLOGV) Log.i(TAG, "onGetStatsCompleted::"+appInfo.packageName+", ("+ps.cacheSize+","+ ps.codeSize+", "+ps.dataSize); sendMessageToHandler(COMPUTE_PKG_SIZE_DONE, data, 0); } public void invokeGetSizeInfo(String packageName, boolean chained) { mChained = chained; 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, data, 0); } else if(Intent.ACTION_PACKAGE_REMOVED.equalsIgnoreCase(actionStr)) { Bundle data = new Bundle(); data.putString(ATTR_PKG_NAME, pkgName); sendMessageToHandler(REMOVE_PKG, data, 0); } else if(Intent.ACTION_PACKAGE_CHANGED.equalsIgnoreCase(actionStr)) { //force adapter to draw the list again. TODO derive from SimpleAdapter //to avoid this } } /* * Utility method to create an array of map objects from a map of map objects * for displaying list items to be used in SimpleAdapter. */ private void createListFromValues() { findViewById(R.id.center_text).setVisibility(View.GONE); populateAdapterList(); mAppAdapter.setViewBinder(this); ListView lv= (ListView) findViewById(android.R.id.list); lv.setOnItemClickListener(this); lv.setAdapter(mAppAdapter); if(mCurrentPkgIdx != -1) { lv.setSelection(mCurrentPkgIdx); } } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); String action = getIntent().getAction(); if(action.equals(Intent.ACTION_MANAGE_PACKAGE_STORAGE)) { mSortOrder = SORT_ORDER_SIZE; } mPm = getPackageManager(); //load strings from resources mBStr = getString(R.string.b_text); mKbStr = getString(R.string.kb_text); mMbStr = getString(R.string.mb_text); } @Override public void onStart() { super.onStart(); setContentView(R.layout.compute_sizes); //clear all messages related to application list clearMessages(); sendMessageToHandler(COMPUTE_PKG_SIZE_START); } @Override public void onStop() { super.onStop(); //register receiver here unregisterReceiver(mReceiver); } public static class AppInfoComparator implements Comparator { public AppInfoComparator(HashMap pSizeMap) { mSizeMap= pSizeMap; } public final int compare(ApplicationInfo a, ApplicationInfo b) { PackageStats aps, bps; aps = mSizeMap.get(a.packageName); bps = mSizeMap.get(b.packageName); if (aps == null && bps == null) { return 0; } else if (aps == null) { return 1; } else if (bps == null) { return -1; } long atotal = aps.dataSize+aps.codeSize+aps.cacheSize; long btotal = bps.dataSize+bps.codeSize+bps.cacheSize; long ret = atotal-btotal; //negate result to sort in descending order if(ret < 0) { return 1; } if(ret == 0) { return 0; } return -1; } private HashMap mSizeMap; } /* * Have to extract elements form map and populate a list ot be used by * SimpleAdapter when displaying list elements. The sort order has to follow * the order of elements in mAppList. */ private List> createAdapterListFromMap() { //get the index from mAppInfo which gives the correct sort position int imax = mAppList.size(); if(localLOGV) Log.i(TAG, "Creating new adapter list"); List> adapterList = new ArrayList>(); ApplicationInfo tmpInfo; for(int i = 0; i < imax; i++) { tmpInfo = mAppList.get(i); MapnewObj = new TreeMap( mAppAdapterMap.get(tmpInfo.packageName)); adapterList.add(newObj); } return adapterList; } private void populateAdapterList() { mAppAdapter = new SimpleAdapter(this, createAdapterListFromMap(), R.layout.manage_applications_item, sKeys, sResourceIds); } private String getSizeStr(PackageStats ps) { String retStr = ""; //insert total size information into map to display in view //at this point its guaranteed that ps is not null. but checking anyway if(ps != null) { long size = ps.cacheSize+ps.codeSize+ps.dataSize; if(size < 1024) { return String.valueOf(size)+mBStr; } long kb, mb, rem; kb = size >> 10; rem = size - (kb << 10); if(kb < 1024) { if(rem > 512) { kb++; } retStr += String.valueOf(kb)+mKbStr; return retStr; } mb = kb >> 10; if(kb >= 512) { //round off mb++; } retStr += String.valueOf(mb)+ mMbStr; return retStr; } else { Log.w(TAG, "Something fishy, cannot find size info for package:"+ps.packageName); } return retStr; } public void sortAppList() { // Sort application list if(mSortOrder == SORT_ORDER_ALPHA) { Collections.sort(mAppList, new ApplicationInfo.DisplayNameComparator(mPm)); } else if(mSortOrder == SORT_ORDER_SIZE) { Collections.sort(mAppList, new AppInfoComparator(mSizeMap)); } } private Map createMapEntry(CharSequence appName, Drawable appIcon, CharSequence appDesc, String sizeStr) { Map map = new TreeMap(); map.put(KEY_APP_NAME, appName); //the icon cannot be null. if the application hasnt set it, the default icon is returned. map.put(KEY_APP_ICON, appIcon); if(appDesc == null) { appDesc=""; } map.put(KEY_APP_DESC, appDesc); map.put(KEY_APP_SIZE, sizeStr); return map; } private void startApplicationDetailsActivity(ApplicationInfo info, PackageStats ps) { //Create intent to start new activity Intent intent = new Intent(Intent.ACTION_VIEW); intent.setClass(this, InstalledAppDetails.class); intent.putExtra(APP_PKG_NAME, info.packageName); if(localLOGV) Log.i(TAG, "code="+ps.codeSize+", cache="+ps.cacheSize+", data="+ps.dataSize); intent.putExtra(APP_PKG_SIZE, ps); if(localLOGV) Log.i(TAG, "Starting sub activity to display info for app:"+info +" with intent:"+intent); //start new activity to display extended information if ((info.flags&ApplicationInfo.FLAG_SYSTEM) != 0) { } startActivityForResult(intent, INSTALLED_APP_DETAILS); } public boolean setViewValue(View view, Object data, String textRepresentation) { if(data == null) { return false; } int id = view.getId(); switch(id) { case R.id.app_name: ((TextView)view).setText((String)data); break; case R.id.app_icon: ((ImageView)view).setImageDrawable((Drawable)data); break; case R.id.app_description: ((TextView)view).setText((String)data); break; case R.id.app_size: ((TextView)view).setText((String)data); break; default: break; } return true; } @Override public boolean onCreateOptionsMenu(Menu menu) { menu.add(0, SORT_ORDER_ALPHA, 0, R.string.sort_order_alpha) .setIcon(android.R.drawable.ic_menu_sort_alphabetically); menu.add(0, SORT_ORDER_SIZE, 0, R.string.sort_order_size) .setIcon(android.R.drawable.ic_menu_sort_by_size); return true; } @Override public boolean onPrepareOptionsMenu(Menu menu) { if(mDoneIniting) { 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(); sendMessageToHandler(REORDER_LIST, menuId); return true; } public void onItemClick(AdapterView parent, View view, int position, long id) { mCurrentPkgIdx=position; ApplicationInfo info = mAppList.get(position); mCurrentPkg = info; PackageStats ps = mSizeMap.get(info.packageName); startApplicationDetailsActivity(info, ps); } }