/*
* 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.app.ActivityManager;
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.content.pm.UserInfo;
import android.os.Environment;
import android.os.Environment.UserEnvironment;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.IBinder;
import android.os.Looper;
import android.os.Message;
import android.os.Parcel;
import android.os.UserHandle;
import android.os.UserManager;
import android.os.storage.StorageVolume;
import android.util.Log;
import android.util.SparseLongArray;
import com.android.internal.app.IMediaContainerService;
import com.google.android.collect.Maps;
import com.google.common.collect.Sets;
import java.io.File;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Set;
import javax.annotation.concurrent.GuardedBy;
/**
* Utility for measuring the disk usage of internal storage or a physical
* {@link StorageVolume}. Connects with a remote {@link IMediaContainerService}
* and delivers results to {@link MeasurementReceiver}.
*/
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);
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");
/** Media types to measure on external storage. */
private static final Set<String> sMeasureMediaTypes = Sets.newHashSet(
Environment.DIRECTORY_DCIM, Environment.DIRECTORY_MOVIES,
Environment.DIRECTORY_PICTURES, Environment.DIRECTORY_MUSIC,
Environment.DIRECTORY_ALARMS, Environment.DIRECTORY_NOTIFICATIONS,
Environment.DIRECTORY_RINGTONES, Environment.DIRECTORY_PODCASTS,
Environment.DIRECTORY_DOWNLOADS, Environment.DIRECTORY_ANDROID);
@GuardedBy("sInstances")
private static HashMap<StorageVolume, StorageMeasurement> sInstances = Maps.newHashMap();
/**
* Obtain shared instance of {@link StorageMeasurement} for given physical
* {@link StorageVolume}, or internal storage if {@code null}.
*/
public static StorageMeasurement getInstance(Context context, StorageVolume volume) {
synchronized (sInstances) {
StorageMeasurement value = sInstances.get(volume);
if (value == null) {
value = new StorageMeasurement(context.getApplicationContext(), volume);
sInstances.put(volume, value);
}
return value;
}
}
public static class MeasurementDetails {
public long totalSize;
public long availSize;
/**
* Total apps disk usage.
* <p>
* When measuring internal storage, this value includes the code size of
* all apps (regardless of install status for current user), and
* internal disk used by the current user's apps. When the device
* emulates external storage, this value also includes emulated storage
* used by the current user's apps.
* <p>
* When measuring a physical {@link StorageVolume}, this value includes
* usage by all apps on that volume.
*/
public long appsSize;
/**
* Total cache disk usage by apps.
*/
public long cacheSize;
/**
* Total media disk usage, categorized by types such as
* {@link Environment#DIRECTORY_MUSIC}.
* <p>
* When measuring internal storage, this reflects media on emulated
* storage for the current user.
* <p>
* When measuring a physical {@link StorageVolume}, this reflects media
* on that volume.
*/
public HashMap<String, Long> mediaSize = Maps.newHashMap();
/**
* Misc external disk usage for the current user, unaccounted in
* {@link #mediaSize}.
*/
public long miscSize;
/**
* Total disk usage for users, which is only meaningful for emulated
* internal storage. Key is {@link UserHandle}.
*/
public SparseLongArray usersSize = new SparseLongArray();
}
public interface MeasurementReceiver {
public void updateApproximate(StorageMeasurement meas, long totalSize, long availSize);
public void updateDetails(StorageMeasurement meas, MeasurementDetails details);
}
private volatile WeakReference<MeasurementReceiver> mReceiver;
/** Physical volume being measured, or {@code null} for internal. */
private final StorageVolume mVolume;
private final boolean mIsInternal;
private final boolean mIsPrimary;
private final MeasurementHandler mHandler;
private long mTotalSize;
private long mAvailSize;
List<FileInfo> mFileInfoForMisc;
private StorageMeasurement(Context context, StorageVolume volume) {
mVolume = volume;
mIsInternal = volume == null;
mIsPrimary = volume != null ? volume.isPrimary() : false;
// Start the thread that will measure the disk usage.
final HandlerThread handlerThread = new HandlerThread("MemoryMeasurement");
handlerThread.start();
mHandler = new MeasurementHandler(context, handlerThread.getLooper());
}
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;
}
receiver.updateApproximate(this, mTotalSize, mAvailSize);
}
private void sendExactUpdate(MeasurementDetails details) {
MeasurementReceiver receiver = (mReceiver != null) ? mReceiver.get() : null;
if (receiver == null) {
if (LOGV) {
Log.i(TAG, "measurements dropped because receiver is null! wasted effort");
}
return;
}
receiver.updateDetails(this, details);
}
private static class StatsObserver extends IPackageStatsObserver.Stub {
private final boolean mIsInternal;
private final MeasurementDetails mDetails;
private final int mCurrentUser;
private final Message mFinished;
private int mRemaining;
public StatsObserver(boolean isInternal, MeasurementDetails details, int currentUser,
Message finished, int remaining) {
mIsInternal = isInternal;
mDetails = details;
mCurrentUser = currentUser;
mFinished = finished;
mRemaining = remaining;
}
@Override
public void onGetStatsCompleted(PackageStats stats, boolean succeeded) {
synchronized (mDetails) {
if (succeeded) {
addStatsLocked(stats);
}
if (--mRemaining == 0) {
mFinished.sendToTarget();
}
}
}
private void addStatsLocked(PackageStats stats) {
if (mIsInternal) {
long codeSize = stats.codeSize;
long dataSize = stats.dataSize;
long cacheSize = stats.cacheSize;
if (Environment.isExternalStorageEmulated()) {
// Include emulated storage when measuring internal. OBB is
// shared on emulated storage, so treat as code.
codeSize += stats.externalCodeSize + stats.externalObbSize;
dataSize += stats.externalDataSize + stats.externalMediaSize;
cacheSize += stats.externalCacheSize;
}
// Count code and data for current user
if (stats.userHandle == mCurrentUser) {
mDetails.appsSize += codeSize;
mDetails.appsSize += dataSize;
}
// User summary only includes data (code is only counted once
// for the current user)
addValue(mDetails.usersSize, stats.userHandle, dataSize);
// Include cache for all users
mDetails.cacheSize += cacheSize;
} else {
// Physical storage; only count external sizes
mDetails.appsSize += stats.externalCodeSize + stats.externalDataSize
+ stats.externalMediaSize + stats.externalObbSize;
mDetails.cacheSize += stats.externalCacheSize;
}
}
}
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 MeasurementDetails mCached;
private final WeakReference<Context> mContext;
private final ServiceConnection mDefContainerConn = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
final IMediaContainerService imcs = IMediaContainerService.Stub.asInterface(
service);
mDefaultContainer = imcs;
mBound = true;
sendMessage(obtainMessage(MSG_CONNECTED, imcs));
}
@Override
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 (mCached != null) {
sendExactUpdate(mCached);
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,
UserHandle.USER_OWNER);
}
}
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: {
mCached = (MeasurementDetails) msg.obj;
sendExactUpdate(mCached);
break;
}
case MSG_INVALIDATE: {
mCached = null;
break;
}
}
}
private void measureApproximateStorage(IMediaContainerService imcs) {
final String path = mVolume != null ? mVolume.getPath()
: Environment.getDataDirectory().getPath();
try {
final long[] stats = imcs.getFileSystemStats(path);
mTotalSize = stats[0];
mAvailSize = stats[1];
} catch (Exception e) {
Log.w(TAG, "Problem in container service", e);
}
sendInternalApproximateUpdate();
}
private void measureExactStorage(IMediaContainerService imcs) {
final Context context = mContext != null ? mContext.get() : null;
if (context == null) {
return;
}
final MeasurementDetails details = new MeasurementDetails();
final Message finished = obtainMessage(MSG_COMPLETED, details);
details.totalSize = mTotalSize;
details.availSize = mAvailSize;
final UserManager userManager = (UserManager) context.getSystemService(
Context.USER_SERVICE);
final List<UserInfo> users = userManager.getUsers();
final int currentUser = ActivityManager.getCurrentUser();
final UserEnvironment currentEnv = new UserEnvironment(currentUser);
// Measure media types for emulated storage, or for primary physical
// external volume
final boolean measureMedia = (mIsInternal && Environment.isExternalStorageEmulated())
|| mIsPrimary;
if (measureMedia) {
for (String type : sMeasureMediaTypes) {
final File path = currentEnv.getExternalStoragePublicDirectory(type);
final long size = getDirectorySize(imcs, path);
details.mediaSize.put(type, size);
}
}
// Measure misc files not counted under media
if (measureMedia) {
final File path = mIsInternal ? currentEnv.getExternalStorageDirectory()
: mVolume.getPathFile();
details.miscSize = measureMisc(imcs, path);
}
// Measure total emulated storage of all users; internal apps data
// will be spliced in later
for (UserInfo user : users) {
final UserEnvironment userEnv = new UserEnvironment(user.id);
final long size = getDirectorySize(imcs, userEnv.getExternalStorageDirectory());
addValue(details.usersSize, user.id, size);
}
// Measure all apps for all users
final PackageManager pm = context.getPackageManager();
if (mIsInternal || mIsPrimary) {
final List<ApplicationInfo> apps = pm.getInstalledApplications(
PackageManager.GET_UNINSTALLED_PACKAGES
| PackageManager.GET_DISABLED_COMPONENTS);
final int count = users.size() * apps.size();
final StatsObserver observer = new StatsObserver(
mIsInternal, details, currentUser, finished, count);
for (UserInfo user : users) {
for (ApplicationInfo app : apps) {
pm.getPackageSizeInfo(app.packageName, user.id, observer);
}
}
} else {
finished.sendToTarget();
}
}
}
private static long getDirectorySize(IMediaContainerService imcs, File path) {
try {
final long size = imcs.calculateDirectorySize(path.toString());
Log.d(TAG, "getDirectorySize(" + path + ") returned " + size);
return size;
} catch (Exception e) {
Log.w(TAG, "Could not read memory from default container service for " + path, e);
return 0;
}
}
private long measureMisc(IMediaContainerService imcs, File dir) {
mFileInfoForMisc = new ArrayList<FileInfo>();
final Parcel p = Parcel.obtain();
try {
final byte[] bytes = imcs.listDirectory(dir.toString());
p.unmarshall(bytes, 0, bytes.length);
p.setDataPosition(0);
} catch (Exception e) {
Log.w(TAG, "Could not list directory from default container service for " + dir, e);
return 0;
}
// Get sizes of all top level nodes except the ones already computed
long counter = 0;
long miscSize = 0;
int count = p.readInt();
for (int i = 0; i < count; i++) {
final String path = p.readString();
final String name = p.readString();
final boolean isDirectory = p.readInt() == 1;
final boolean isFile = p.readInt() == 1;
final long fileSize = isFile ? p.readLong() : 0;
if (sMeasureMediaTypes.contains(name)) {
continue;
}
if (isFile) {
mFileInfoForMisc.add(new FileInfo(path, fileSize, counter++));
miscSize += fileSize;
} else if (isDirectory) {
final long dirSize = getDirectorySize(imcs, new File(path));
mFileInfoForMisc.add(new FileInfo(path, dirSize, counter++));
miscSize += 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);
return miscSize;
}
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;
}
}
private static void addValue(SparseLongArray array, int key, long value) {
array.put(key, array.get(key) + value);
}
}