Storage stats on external SD card in Settings.
Storage categories are dynamically created from list of StorageVolumes instead of a static XML. Unknown files' sizes are part of Misc rather than Apps. Categories with a size of 0 are removed. TODO : remove the notion of a "nosdcard" product. Change strings accordingly. See all TODO in code Change-Id: I017ac20f5fa50ad9bdeba8e666754ec84acf3858
This commit is contained in:
527
src/com/android/settings/deviceinfo/StorageMeasurement.java
Normal file
527
src/com/android/settings/deviceinfo/StorageMeasurement.java
Normal file
@@ -0,0 +1,527 @@
|
||||
/*
|
||||
* 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 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;
|
||||
|
||||
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;
|
||||
mIsPrimary = 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 (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) {
|
||||
mAppsSizeForThisStatsObserver += stats.codeSize + stats.dataSize +
|
||||
stats.externalCacheSize + stats.externalDataSize +
|
||||
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.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) {
|
||||
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();
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user