| /* |
| * 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.RemoteException; |
| 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 DefaultContainerService) |
| * 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"; |
| |
| public 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; |
| } |
| |
| 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; |
| measureApproximateStorage(imcs); |
| 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(IMediaContainerService imcs) { |
| final String path = mStorageVolume != null ? mStorageVolume.getPath() |
| : Environment.getDataDirectory().getPath(); |
| try { |
| final long[] stats = imcs.getFileSystemStats(path); |
| mTotalSize = stats[0]; |
| mAvailSize = stats[1]; |
| } catch (RemoteException e) { |
| Log.w(TAG, "Problem in container service", e); |
| } |
| |
| 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; |
| } |
| } |