/* * Copyright (C) 2011 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.deviceinfo; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.ServiceConnection; import android.content.pm.ApplicationInfo; import android.content.pm.IPackageStatsObserver; import android.content.pm.PackageManager; import android.content.pm.PackageStats; import android.os.Bundle; import android.os.Environment; import android.os.Handler; import android.os.HandlerThread; import android.os.IBinder; import android.os.Looper; import android.os.Message; import android.os.StatFs; import android.os.storage.StorageVolume; import android.util.Log; import com.android.internal.app.IMediaContainerService; import java.io.File; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; /** * Measure the memory for various systems. * * TODO: This class should ideally have less knowledge about what the context * it's measuring is. In the future, reduce the amount of stuff it needs to * know about by just keeping an array of measurement types of the following * properties: * * Filesystem stats (using StatFs) * Directory measurements (using DefaultContainerService.measureDir) * Application measurements (using PackageManager) * * Then the calling application would just specify the type and an argument. * This class would keep track of it while the calling application would * decide on how to use it. */ public class StorageMeasurement { private static final String TAG = "StorageMeasurement"; private static final boolean LOCAL_LOGV = true; static final boolean LOGV = LOCAL_LOGV && Log.isLoggable(TAG, Log.VERBOSE); public static final String TOTAL_SIZE = "total_size"; public static final String AVAIL_SIZE = "avail_size"; public static final String APPS_USED = "apps_used"; public static final String DOWNLOADS_SIZE = "downloads_size"; public static final String MISC_SIZE = "misc_size"; public static final String MEDIA_SIZES = "media_sizes"; private static final String DEFAULT_CONTAINER_PACKAGE = "com.android.defcontainer"; private static final ComponentName DEFAULT_CONTAINER_COMPONENT = new ComponentName( DEFAULT_CONTAINER_PACKAGE, "com.android.defcontainer.DefaultContainerService"); private final MeasurementHandler mHandler; private static Map<StorageVolume, StorageMeasurement> sInstances = new ConcurrentHashMap<StorageVolume, StorageMeasurement>(); private static StorageMeasurement sInternalInstance; private volatile WeakReference<MeasurementReceiver> mReceiver; private long mTotalSize; private long mAvailSize; private long mAppsSize; private long mDownloadsSize; private long mMiscSize; private long[] mMediaSizes = new long[StorageVolumePreferenceCategory.sMediaCategories.length]; final private StorageVolume mStorageVolume; final private boolean mIsPrimary; final private boolean mIsInternal; List<FileInfo> mFileInfoForMisc; public interface MeasurementReceiver { public void updateApproximate(Bundle bundle); public void updateExact(Bundle bundle); } private StorageMeasurement(Context context, StorageVolume storageVolume, boolean isPrimary) { mStorageVolume = storageVolume; mIsInternal = storageVolume == null; mIsPrimary = !mIsInternal && isPrimary; // Start the thread that will measure the disk usage. final HandlerThread handlerThread = new HandlerThread("MemoryMeasurement"); handlerThread.start(); mHandler = new MeasurementHandler(context, handlerThread.getLooper()); } /** * Get the singleton of the StorageMeasurement class. The application * context is used to avoid leaking activities. * @param storageVolume The {@link StorageVolume} that will be measured * @param isPrimary true when this storage volume is the primary volume */ public static StorageMeasurement getInstance(Context context, StorageVolume storageVolume, boolean isPrimary) { if (storageVolume == null) { if (sInternalInstance == null) { sInternalInstance = new StorageMeasurement(context.getApplicationContext(), storageVolume, isPrimary); } return sInternalInstance; } if (sInstances.containsKey(storageVolume)) { return sInstances.get(storageVolume); } else { StorageMeasurement storageMeasurement = new StorageMeasurement(context.getApplicationContext(), storageVolume, isPrimary); sInstances.put(storageVolume, storageMeasurement); return storageMeasurement; } } public void setReceiver(MeasurementReceiver receiver) { if (mReceiver == null || mReceiver.get() == null) { mReceiver = new WeakReference<MeasurementReceiver>(receiver); } } public void measure() { if (!mHandler.hasMessages(MeasurementHandler.MSG_MEASURE)) { mHandler.sendEmptyMessage(MeasurementHandler.MSG_MEASURE); } } public void cleanUp() { mReceiver = null; mHandler.removeMessages(MeasurementHandler.MSG_MEASURE); mHandler.sendEmptyMessage(MeasurementHandler.MSG_DISCONNECT); } public void invalidate() { mHandler.sendEmptyMessage(MeasurementHandler.MSG_INVALIDATE); } private void sendInternalApproximateUpdate() { MeasurementReceiver receiver = (mReceiver != null) ? mReceiver.get() : null; if (receiver == null) { return; } Bundle bundle = new Bundle(); bundle.putLong(TOTAL_SIZE, mTotalSize); bundle.putLong(AVAIL_SIZE, mAvailSize); receiver.updateApproximate(bundle); } private void sendExactUpdate() { MeasurementReceiver receiver = (mReceiver != null) ? mReceiver.get() : null; if (receiver == null) { if (LOGV) { Log.i(TAG, "measurements dropped because receiver is null! wasted effort"); } return; } Bundle bundle = new Bundle(); bundle.putLong(TOTAL_SIZE, mTotalSize); bundle.putLong(AVAIL_SIZE, mAvailSize); bundle.putLong(APPS_USED, mAppsSize); bundle.putLong(DOWNLOADS_SIZE, mDownloadsSize); bundle.putLong(MISC_SIZE, mMiscSize); bundle.putLongArray(MEDIA_SIZES, mMediaSizes); receiver.updateExact(bundle); } private class MeasurementHandler extends Handler { public static final int MSG_MEASURE = 1; public static final int MSG_CONNECTED = 2; public static final int MSG_DISCONNECT = 3; public static final int MSG_COMPLETED = 4; public static final int MSG_INVALIDATE = 5; private Object mLock = new Object(); private IMediaContainerService mDefaultContainer; private volatile boolean mBound = false; private volatile boolean mMeasured = false; private StatsObserver mStatsObserver; private final WeakReference<Context> mContext; final private ServiceConnection mDefContainerConn = new ServiceConnection() { public void onServiceConnected(ComponentName name, IBinder service) { final IMediaContainerService imcs = IMediaContainerService.Stub .asInterface(service); mDefaultContainer = imcs; mBound = true; sendMessage(obtainMessage(MSG_CONNECTED, imcs)); } public void onServiceDisconnected(ComponentName name) { mBound = false; removeMessages(MSG_CONNECTED); } }; public MeasurementHandler(Context context, Looper looper) { super(looper); mContext = new WeakReference<Context>(context); } @Override public void handleMessage(Message msg) { switch (msg.what) { case MSG_MEASURE: { if (mMeasured) { sendExactUpdate(); break; } final Context context = (mContext != null) ? mContext.get() : null; if (context == null) { return; } measureApproximateStorage(); synchronized (mLock) { if (mBound) { removeMessages(MSG_DISCONNECT); sendMessage(obtainMessage(MSG_CONNECTED, mDefaultContainer)); } else { Intent service = new Intent().setComponent(DEFAULT_CONTAINER_COMPONENT); context.bindService(service, mDefContainerConn, Context.BIND_AUTO_CREATE); } } break; } case MSG_CONNECTED: { IMediaContainerService imcs = (IMediaContainerService) msg.obj; measureExactStorage(imcs); break; } case MSG_DISCONNECT: { synchronized (mLock) { if (mBound) { final Context context = (mContext != null) ? mContext.get() : null; if (context == null) { return; } mBound = false; context.unbindService(mDefContainerConn); } } break; } case MSG_COMPLETED: { mMeasured = true; sendExactUpdate(); break; } case MSG_INVALIDATE: { mMeasured = false; break; } } } /** * Request measurement of each package. * * @param pm PackageManager instance to query */ public void requestQueuedMeasurementsLocked(PackageManager pm) { final String[] appsList = mStatsObserver.getAppsList(); final int N = appsList.length; for (int i = 0; i < N; i++) { pm.getPackageSizeInfo(appsList[i], mStatsObserver); } } private class StatsObserver extends IPackageStatsObserver.Stub { private long mAppsSizeForThisStatsObserver = 0; private final List<String> mAppsList = new ArrayList<String>(); public void onGetStatsCompleted(PackageStats stats, boolean succeeded) { if (!mStatsObserver.equals(this)) { // this callback's class object is no longer in use. ignore this callback. return; } if (succeeded) { if (mIsInternal) { mAppsSizeForThisStatsObserver += stats.codeSize + stats.dataSize; } else if (!Environment.isExternalStorageEmulated()) { mAppsSizeForThisStatsObserver += stats.externalObbSize + stats.externalCodeSize + stats.externalDataSize + stats.externalCacheSize + stats.externalMediaSize; } else { mAppsSizeForThisStatsObserver += stats.codeSize + stats.dataSize + stats.externalCodeSize + stats.externalDataSize + stats.externalCacheSize + stats.externalMediaSize + stats.externalObbSize; } } synchronized (mAppsList) { mAppsList.remove(stats.packageName); if (mAppsList.size() > 0) return; } mAppsSize = mAppsSizeForThisStatsObserver; onInternalMeasurementComplete(); } public void queuePackageMeasurementLocked(String packageName) { synchronized (mAppsList) { mAppsList.add(packageName); } } public String[] getAppsList() { synchronized (mAppsList) { return mAppsList.toArray(new String[mAppsList.size()]); } } } private void onInternalMeasurementComplete() { sendEmptyMessage(MSG_COMPLETED); } private void measureApproximateStorage() { final StatFs stat = new StatFs(mStorageVolume != null ? mStorageVolume.getPath() : Environment.getDataDirectory().getPath()); final long blockSize = stat.getBlockSize(); final long totalBlocks = stat.getBlockCount(); final long availableBlocks = stat.getAvailableBlocks(); mTotalSize = totalBlocks * blockSize; mAvailSize = availableBlocks * blockSize; sendInternalApproximateUpdate(); } private void measureExactStorage(IMediaContainerService imcs) { Context context = mContext != null ? mContext.get() : null; if (context == null) { return; } // Media for (int i = 0; i < StorageVolumePreferenceCategory.sMediaCategories.length; i++) { if (mIsPrimary) { String[] dirs = StorageVolumePreferenceCategory.sMediaCategories[i].mDirPaths; final int length = dirs.length; mMediaSizes[i] = 0; for (int d = 0; d < length; d++) { final String path = dirs[d]; mMediaSizes[i] += getDirectorySize(imcs, path); } } else { // TODO Compute sizes using the MediaStore mMediaSizes[i] = 0; } } /* Compute sizes using the media provider // Media sizes are measured by the MediaStore. Query database. ContentResolver contentResolver = context.getContentResolver(); // TODO "external" as a static String from MediaStore? Uri audioUri = MediaStore.Files.getContentUri("external"); final String[] projection = new String[] { "sum(" + MediaStore.Files.FileColumns.SIZE + ")" }; final String selection = MediaStore.Files.FileColumns.STORAGE_ID + "=" + Integer.toString(mStorageVolume.getStorageId()) + " AND " + MediaStore.Files.FileColumns.MEDIA_TYPE + "=?"; for (int i = 0; i < StorageVolumePreferenceCategory.sMediaCategories.length; i++) { mMediaSizes[i] = 0; int mediaType = StorageVolumePreferenceCategory.sMediaCategories[i].mediaType; Cursor c = null; try { c = contentResolver.query(audioUri, projection, selection, new String[] { Integer.toString(mediaType) } , null); if (c != null && c.moveToNext()) { long size = c.getLong(0); mMediaSizes[i] = size; } } finally { if (c != null) c.close(); } } */ // Downloads (primary volume only) if (mIsPrimary) { final String downloadsPath = Environment.getExternalStoragePublicDirectory( Environment.DIRECTORY_DOWNLOADS).getAbsolutePath(); mDownloadsSize = getDirectorySize(imcs, downloadsPath); } else { mDownloadsSize = 0; } // Misc mMiscSize = 0; if (mIsPrimary) { measureSizesOfMisc(imcs); } // Apps // We have to get installd to measure the package sizes. PackageManager pm = context.getPackageManager(); if (pm == null) { return; } final List<ApplicationInfo> apps; if (mIsPrimary || mIsInternal) { apps = pm.getInstalledApplications(PackageManager.GET_UNINSTALLED_PACKAGES | PackageManager.GET_DISABLED_COMPONENTS); } else { // TODO also measure apps installed on the SD card apps = Collections.emptyList(); } if (apps != null && apps.size() > 0) { // initiate measurement of all package sizes. need new StatsObserver object. mStatsObserver = new StatsObserver(); synchronized (mStatsObserver.mAppsList) { for (int i = 0; i < apps.size(); i++) { final ApplicationInfo info = apps.get(i); mStatsObserver.queuePackageMeasurementLocked(info.packageName); } } requestQueuedMeasurementsLocked(pm); // Sending of the message back to the MeasurementReceiver is // completed in the PackageObserver } else { onInternalMeasurementComplete(); } } } private long getDirectorySize(IMediaContainerService imcs, String dir) { try { return imcs.calculateDirectorySize(dir); } catch (Exception e) { Log.w(TAG, "Could not read memory from default container service for " + dir, e); return 0; } } long getMiscSize() { return mMiscSize; } private void measureSizesOfMisc(IMediaContainerService imcs) { File top = new File(mStorageVolume.getPath()); mFileInfoForMisc = new ArrayList<FileInfo>(); File[] files = top.listFiles(); if (files == null) return; final int len = files.length; // Get sizes of all top level nodes except the ones already computed... long counter = 0; for (int i = 0; i < len; i++) { String path = files[i].getAbsolutePath(); if (StorageVolumePreferenceCategory.sPathsExcludedForMisc.contains(path)) { continue; } if (files[i].isFile()) { final long fileSize = files[i].length(); mFileInfoForMisc.add(new FileInfo(path, fileSize, counter++)); mMiscSize += fileSize; } else if (files[i].isDirectory()) { final long dirSize = getDirectorySize(imcs, path); mFileInfoForMisc.add(new FileInfo(path, dirSize, counter++)); mMiscSize += dirSize; } else { // Non directory, non file: not listed } } // sort the list of FileInfo objects collected above in descending order of their sizes Collections.sort(mFileInfoForMisc); } static class FileInfo implements Comparable<FileInfo> { final String mFileName; final long mSize; final long mId; FileInfo(String fileName, long size, long id) { mFileName = fileName; mSize = size; mId = id; } @Override public int compareTo(FileInfo that) { if (this == that || mSize == that.mSize) return 0; else return (mSize < that.mSize) ? 1 : -1; // for descending sort } @Override public String toString() { return mFileName + " : " + mSize + ", id:" + mId; } } /** * TODO remove this method, only used because external SD Card needs a special treatment. */ boolean isExternalSDCard() { return !mIsPrimary && !mIsInternal; } }