blob: 2792d0904151a4afb055e6daab2d38cc6dfeb6a6 [file] [log] [blame]
/*
* 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;
}
}