package com.android.settings.applications; import android.app.Application; 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.Handler; import android.os.HandlerThread; import android.os.Looper; import android.os.Message; import android.os.Process; import android.os.SystemClock; import android.os.UserHandle; import android.text.format.Formatter; import android.util.Log; import java.io.File; import java.text.Collator; import java.text.Normalizer; import java.text.Normalizer.Form; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.regex.Pattern; /** * Keeps track of information about all installed applications, lazy-loading * as needed. */ public class ApplicationsState { static final String TAG = "ApplicationsState"; static final boolean DEBUG = false; static final boolean DEBUG_LOCKING = false; public static interface Callbacks { public void onRunningStateChanged(boolean running); public void onPackageListChanged(); public void onRebuildComplete(ArrayList apps); public void onPackageIconChanged(); public void onPackageSizeChanged(String packageName); public void onAllSizesComputed(); } public static interface AppFilter { public void init(); public boolean filterApp(ApplicationInfo info); } static final int SIZE_UNKNOWN = -1; static final int SIZE_INVALID = -2; static final Pattern REMOVE_DIACRITICALS_PATTERN = Pattern.compile("\\p{InCombiningDiacriticalMarks}+"); public static String normalize(String str) { String tmp = Normalizer.normalize(str, Form.NFD); return REMOVE_DIACRITICALS_PATTERN.matcher(tmp) .replaceAll("").toLowerCase(); } public static class SizeInfo { long cacheSize; long codeSize; long dataSize; long externalCodeSize; long externalDataSize; // This is the part of externalDataSize that is in the cache // section of external storage. Note that we don't just combine // this with cacheSize because currently the platform can't // automatically trim this data when needed, so it is something // the user may need to manage. The externalDataSize also includes // this value, since what this is here is really the part of // externalDataSize that we can just consider to be "cache" files // for purposes of cleaning them up in the app details UI. long externalCacheSize; } public static class AppEntry extends SizeInfo { final File apkFile; final long id; String label; long size; long internalSize; long externalSize; boolean mounted; String getNormalizedLabel() { if (normalizedLabel != null) { return normalizedLabel; } normalizedLabel = normalize(label); return normalizedLabel; } // Need to synchronize on 'this' for the following. ApplicationInfo info; Drawable icon; String sizeStr; String internalSizeStr; String externalSizeStr; boolean sizeStale; long sizeLoadStart; String normalizedLabel; AppEntry(Context context, ApplicationInfo info, long id) { apkFile = new File(info.sourceDir); this.id = id; this.info = info; this.size = SIZE_UNKNOWN; this.sizeStale = true; ensureLabel(context); } void ensureLabel(Context context) { if (this.label == null || !this.mounted) { if (!this.apkFile.exists()) { this.mounted = false; this.label = info.packageName; } else { this.mounted = true; CharSequence label = info.loadLabel(context.getPackageManager()); this.label = label != null ? label.toString() : info.packageName; } } } boolean ensureIconLocked(Context context, PackageManager pm) { if (this.icon == null) { if (this.apkFile.exists()) { this.icon = this.info.loadIcon(pm); return true; } else { this.mounted = false; this.icon = context.getResources().getDrawable( com.android.internal.R.drawable.sym_app_on_sd_unavailable_icon); } } else if (!this.mounted) { // If the app wasn't mounted but is now mounted, reload // its icon. if (this.apkFile.exists()) { this.mounted = true; this.icon = this.info.loadIcon(pm); return true; } } return false; } } public static final Comparator ALPHA_COMPARATOR = new Comparator() { private final Collator sCollator = Collator.getInstance(); @Override public int compare(AppEntry object1, AppEntry object2) { final boolean normal1 = object1.info.enabled && (object1.info.flags&ApplicationInfo.FLAG_INSTALLED) != 0; final boolean normal2 = object2.info.enabled && (object2.info.flags&ApplicationInfo.FLAG_INSTALLED) != 0; if (normal1 != normal2) { return normal1 ? -1 : 1; } return sCollator.compare(object1.label, object2.label); } }; public static final Comparator SIZE_COMPARATOR = new Comparator() { private final Collator sCollator = Collator.getInstance(); @Override public int compare(AppEntry object1, AppEntry object2) { if (object1.size < object2.size) return 1; if (object1.size > object2.size) return -1; return sCollator.compare(object1.label, object2.label); } }; public static final Comparator INTERNAL_SIZE_COMPARATOR = new Comparator() { private final Collator sCollator = Collator.getInstance(); @Override public int compare(AppEntry object1, AppEntry object2) { if (object1.internalSize < object2.internalSize) return 1; if (object1.internalSize > object2.internalSize) return -1; return sCollator.compare(object1.label, object2.label); } }; public static final Comparator EXTERNAL_SIZE_COMPARATOR = new Comparator() { private final Collator sCollator = Collator.getInstance(); @Override public int compare(AppEntry object1, AppEntry object2) { if (object1.externalSize < object2.externalSize) return 1; if (object1.externalSize > object2.externalSize) return -1; return sCollator.compare(object1.label, object2.label); } }; public static final AppFilter THIRD_PARTY_FILTER = new AppFilter() { public void init() { } @Override public boolean filterApp(ApplicationInfo info) { if ((info.flags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0) { return true; } else if ((info.flags & ApplicationInfo.FLAG_SYSTEM) == 0) { return true; } return false; } }; public static final AppFilter ON_SD_CARD_FILTER = new AppFilter() { final CanBeOnSdCardChecker mCanBeOnSdCardChecker = new CanBeOnSdCardChecker(); public void init() { mCanBeOnSdCardChecker.init(); } @Override public boolean filterApp(ApplicationInfo info) { return mCanBeOnSdCardChecker.check(info); } }; public static final AppFilter DISABLED_FILTER = new AppFilter() { public void init() { } @Override public boolean filterApp(ApplicationInfo info) { if (!info.enabled) { return true; } return false; } }; public static final AppFilter ALL_ENABLED_FILTER = new AppFilter() { public void init() { } @Override public boolean filterApp(ApplicationInfo info) { if (info.enabled) { return true; } return false; } }; final Context mContext; final PackageManager mPm; final int mRetrieveFlags; PackageIntentReceiver mPackageIntentReceiver; boolean mResumed; boolean mHaveDisabledApps; // Information about all applications. Synchronize on mEntriesMap // to protect access to these. final ArrayList mSessions = new ArrayList(); final ArrayList mRebuildingSessions = new ArrayList(); final InterestingConfigChanges mInterestingConfigChanges = new InterestingConfigChanges(); final HashMap mEntriesMap = new HashMap(); final ArrayList mAppEntries = new ArrayList(); List mApplications = new ArrayList(); long mCurId = 1; String mCurComputingSizePkg; boolean mSessionsChanged; // Temporary for dispatching session callbacks. Only touched by main thread. final ArrayList mActiveSessions = new ArrayList(); /** * 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"); mContext.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); mContext.registerReceiver(this, sdFilter); } void unregisterReceiver() { mContext.unregisterReceiver(this); } @Override public void onReceive(Context context, Intent intent) { String actionStr = intent.getAction(); if (Intent.ACTION_PACKAGE_ADDED.equals(actionStr)) { Uri data = intent.getData(); String pkgName = data.getEncodedSchemeSpecificPart(); addPackage(pkgName); } else if (Intent.ACTION_PACKAGE_REMOVED.equals(actionStr)) { Uri data = intent.getData(); String pkgName = data.getEncodedSchemeSpecificPart(); removePackage(pkgName); } else if (Intent.ACTION_PACKAGE_CHANGED.equals(actionStr)) { Uri data = intent.getData(); String pkgName = data.getEncodedSchemeSpecificPart(); invalidatePackage(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; } boolean avail = Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE.equals(actionStr); if (avail) { for (String pkgName : pkgList) { invalidatePackage(pkgName); } } } } } void rebuildActiveSessions() { synchronized (mEntriesMap) { if (!mSessionsChanged) { return; } mActiveSessions.clear(); for (int i=0; i 5 seconds * (leading to an ANR). * * Dalvik will promote a monitor to a "real" lock if it detects enough * contention on it. It doesn't figure this out fast enough for us * here, though, so this little trick will force it to turn into a real * lock immediately. */ synchronized (mEntriesMap) { try { mEntriesMap.wait(1); } catch (InterruptedException e) { } } } public class Session { final Callbacks mCallbacks; boolean mResumed; // Rebuilding of app list. Synchronized on mRebuildSync. final Object mRebuildSync = new Object(); boolean mRebuildRequested; boolean mRebuildAsync; AppFilter mRebuildFilter; Comparator mRebuildComparator; ArrayList mRebuildResult; ArrayList mLastAppList; Session(Callbacks callbacks) { mCallbacks = callbacks; } public void resume() { if (DEBUG_LOCKING) Log.v(TAG, "resume about to acquire lock..."); synchronized (mEntriesMap) { if (!mResumed) { mResumed = true; mSessionsChanged = true; doResumeIfNeededLocked(); } } if (DEBUG_LOCKING) Log.v(TAG, "...resume releasing lock"); } public void pause() { if (DEBUG_LOCKING) Log.v(TAG, "pause about to acquire lock..."); synchronized (mEntriesMap) { if (mResumed) { mResumed = false; mSessionsChanged = true; mBackgroundHandler.removeMessages(BackgroundHandler.MSG_REBUILD_LIST, this); doPauseIfNeededLocked(); } if (DEBUG_LOCKING) Log.v(TAG, "...pause releasing lock"); } } // Creates a new list of app entries with the given filter and comparator. ArrayList rebuild(AppFilter filter, Comparator comparator) { synchronized (mRebuildSync) { synchronized (mEntriesMap) { mRebuildingSessions.add(this); mRebuildRequested = true; mRebuildAsync = false; mRebuildFilter = filter; mRebuildComparator = comparator; mRebuildResult = null; if (!mBackgroundHandler.hasMessages(BackgroundHandler.MSG_REBUILD_LIST)) { Message msg = mBackgroundHandler.obtainMessage( BackgroundHandler.MSG_REBUILD_LIST); mBackgroundHandler.sendMessage(msg); } } // We will wait for .25s for the list to be built. long waitend = SystemClock.uptimeMillis()+250; while (mRebuildResult == null) { long now = SystemClock.uptimeMillis(); if (now >= waitend) { break; } try { mRebuildSync.wait(waitend - now); } catch (InterruptedException e) { } } mRebuildAsync = true; return mRebuildResult; } } void handleRebuildList() { AppFilter filter; Comparator comparator; synchronized (mRebuildSync) { if (!mRebuildRequested) { return; } filter = mRebuildFilter; comparator = mRebuildComparator; mRebuildRequested = false; mRebuildFilter = null; mRebuildComparator = null; } Process.setThreadPriority(Process.THREAD_PRIORITY_FOREGROUND); if (filter != null) { filter.init(); } List apps; synchronized (mEntriesMap) { apps = new ArrayList(mApplications); } ArrayList filteredApps = new ArrayList(); if (DEBUG) Log.i(TAG, "Rebuilding..."); for (int i=0; i(); } if (mInterestingConfigChanges.applyNewConfig(mContext.getResources())) { // If an interesting part of the configuration has changed, we // should completely reload the app entries. mEntriesMap.clear(); mAppEntries.clear(); } else { for (int i=0; i sumCacheSizes now has lock"); for (int i=mAppEntries.size()-1; i>=0; i--) { sum += mAppEntries.get(i).cacheSize; } if (DEBUG_LOCKING) Log.v(TAG, "...sumCacheSizes releasing lock"); } return sum; } int indexOfApplicationInfoLocked(String pkgName) { for (int i=mApplications.size()-1; i>=0; i--) { if (mApplications.get(i).packageName.equals(pkgName)) { return i; } } return -1; } void addPackage(String pkgName) { try { synchronized (mEntriesMap) { if (DEBUG_LOCKING) Log.v(TAG, "addPackage acquired lock"); if (DEBUG) Log.i(TAG, "Adding package " + pkgName); if (!mResumed) { // If we are not resumed, we will do a full query the // next time we resume, so there is no reason to do work // here. if (DEBUG_LOCKING) Log.v(TAG, "addPackage release lock: not resumed"); return; } if (indexOfApplicationInfoLocked(pkgName) >= 0) { if (DEBUG) Log.i(TAG, "Package already exists!"); if (DEBUG_LOCKING) Log.v(TAG, "addPackage release lock: already exists"); return; } ApplicationInfo info = mPm.getApplicationInfo(pkgName, mRetrieveFlags); if (!info.enabled) { if (info.enabledSetting != PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER) { return; } mHaveDisabledApps = true; } mApplications.add(info); if (!mBackgroundHandler.hasMessages(BackgroundHandler.MSG_LOAD_ENTRIES)) { mBackgroundHandler.sendEmptyMessage(BackgroundHandler.MSG_LOAD_ENTRIES); } if (!mMainHandler.hasMessages(MainHandler.MSG_PACKAGE_LIST_CHANGED)) { mMainHandler.sendEmptyMessage(MainHandler.MSG_PACKAGE_LIST_CHANGED); } if (DEBUG_LOCKING) Log.v(TAG, "addPackage releasing lock"); } } catch (NameNotFoundException e) { } } void removePackage(String pkgName) { synchronized (mEntriesMap) { if (DEBUG_LOCKING) Log.v(TAG, "removePackage acquired lock"); int idx = indexOfApplicationInfoLocked(pkgName); if (DEBUG) Log.i(TAG, "removePackage: " + pkgName + " @ " + idx); if (idx >= 0) { AppEntry entry = mEntriesMap.get(pkgName); if (DEBUG) Log.i(TAG, "removePackage: " + entry); if (entry != null) { mEntriesMap.remove(pkgName); mAppEntries.remove(entry); } ApplicationInfo info = mApplications.get(idx); mApplications.remove(idx); if (!info.enabled) { mHaveDisabledApps = false; for (int i=0; i= 0) { return Formatter.formatFileSize(mContext, size); } return null; } final HandlerThread mThread; final BackgroundHandler mBackgroundHandler; class BackgroundHandler extends Handler { static final int MSG_REBUILD_LIST = 1; static final int MSG_LOAD_ENTRIES = 2; static final int MSG_LOAD_ICONS = 3; static final int MSG_LOAD_SIZES = 4; boolean mRunning; final IPackageStatsObserver.Stub mStatsObserver = new IPackageStatsObserver.Stub() { public void onGetStatsCompleted(PackageStats stats, boolean succeeded) { boolean sizeChanged = false; synchronized (mEntriesMap) { if (DEBUG_LOCKING) Log.v(TAG, "onGetStatsCompleted acquired lock"); AppEntry entry = mEntriesMap.get(stats.packageName); if (entry != null) { synchronized (entry) { entry.sizeStale = false; entry.sizeLoadStart = 0; long externalCodeSize = stats.externalCodeSize + stats.externalObbSize; long externalDataSize = stats.externalDataSize + stats.externalMediaSize; long newSize = externalCodeSize + externalDataSize + getTotalInternalSize(stats); if (entry.size != newSize || entry.cacheSize != stats.cacheSize || entry.codeSize != stats.codeSize || entry.dataSize != stats.dataSize || entry.externalCodeSize != externalCodeSize || entry.externalDataSize != externalDataSize || entry.externalCacheSize != stats.externalCacheSize) { entry.size = newSize; entry.cacheSize = stats.cacheSize; entry.codeSize = stats.codeSize; entry.dataSize = stats.dataSize; entry.externalCodeSize = externalCodeSize; entry.externalDataSize = externalDataSize; entry.externalCacheSize = stats.externalCacheSize; entry.sizeStr = getSizeStr(entry.size); entry.internalSize = getTotalInternalSize(stats); entry.internalSizeStr = getSizeStr(entry.internalSize); entry.externalSize = getTotalExternalSize(stats); entry.externalSizeStr = getSizeStr(entry.externalSize); if (DEBUG) Log.i(TAG, "Set size of " + entry.label + " " + entry + ": " + entry.sizeStr); sizeChanged = true; } } if (sizeChanged) { Message msg = mMainHandler.obtainMessage( MainHandler.MSG_PACKAGE_SIZE_CHANGED, stats.packageName); mMainHandler.sendMessage(msg); } } if (mCurComputingSizePkg == null || mCurComputingSizePkg.equals(stats.packageName)) { mCurComputingSizePkg = null; sendEmptyMessage(MSG_LOAD_SIZES); } if (DEBUG_LOCKING) Log.v(TAG, "onGetStatsCompleted releasing lock"); } } }; BackgroundHandler(Looper looper) { super(looper); } @Override public void handleMessage(Message msg) { // Always try rebuilding list first thing, if needed. ArrayList rebuildingSessions = null; synchronized (mEntriesMap) { if (mRebuildingSessions.size() > 0) { rebuildingSessions = new ArrayList(mRebuildingSessions); mRebuildingSessions.clear(); } } if (rebuildingSessions != null) { for (int i=0; i= 6) { sendEmptyMessage(MSG_LOAD_ENTRIES); } else { sendEmptyMessage(MSG_LOAD_ICONS); } } break; case MSG_LOAD_ICONS: { int numDone = 0; synchronized (mEntriesMap) { if (DEBUG_LOCKING) Log.v(TAG, "MSG_LOAD_ICONS acquired lock"); for (int i=0; i 0) { if (!mMainHandler.hasMessages(MainHandler.MSG_PACKAGE_ICON_CHANGED)) { mMainHandler.sendEmptyMessage(MainHandler.MSG_PACKAGE_ICON_CHANGED); } } if (numDone >= 2) { sendEmptyMessage(MSG_LOAD_ICONS); } else { sendEmptyMessage(MSG_LOAD_SIZES); } } break; case MSG_LOAD_SIZES: { synchronized (mEntriesMap) { if (DEBUG_LOCKING) Log.v(TAG, "MSG_LOAD_SIZES acquired lock"); if (mCurComputingSizePkg != null) { if (DEBUG_LOCKING) Log.v(TAG, "MSG_LOAD_SIZES releasing: currently computing"); return; } long now = SystemClock.uptimeMillis(); for (int i=0; i