Now that DefaultContainerService has visibility of external storage for all users, we can measure internal storage in a single pass. Internal storage measurement now iterates across all known users, counting both apps and emulated storage usage. Create MeasurementDetails object with documentation about what is counted under various device configurations. Generalize to measure each Environment.DIRECTORY separately, so it can be combined as needed. General cleanup of how measurements are passed to UI. Bug: 7003520 Change-Id: Ib89c185296a0c9debdc20beeaa98584d803a84e8
523 lines
19 KiB
Java
523 lines
19 KiB
Java
/*
|
|
* 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.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 {
|
|
/**
|
|
* 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 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) {
|
|
final long externalSize = stats.externalCodeSize + stats.externalDataSize
|
|
+ stats.externalCacheSize + stats.externalMediaSize;
|
|
|
|
if (mIsInternal) {
|
|
final long codeSize;
|
|
final long dataSize;
|
|
if (Environment.isExternalStorageEmulated()) {
|
|
// OBB is shared on emulated storage, so count once as code,
|
|
// and data includes emulated storage.
|
|
codeSize = stats.codeSize + stats.externalObbSize;
|
|
dataSize = stats.dataSize + externalSize;
|
|
} else {
|
|
codeSize = stats.codeSize;
|
|
dataSize = stats.dataSize;
|
|
}
|
|
|
|
// Include code and combined data for current user
|
|
if (stats.userHandle == mCurrentUser) {
|
|
mDetails.appsSize += codeSize;
|
|
mDetails.appsSize += dataSize;
|
|
}
|
|
|
|
// Include combined data for user summary
|
|
addValue(mDetails.usersSize, stats.userHandle, dataSize);
|
|
|
|
} else {
|
|
// Physical storage; only count external sizes
|
|
mDetails.appsSize += externalSize + stats.externalObbSize;
|
|
}
|
|
}
|
|
}
|
|
|
|
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);
|
|
|
|
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 (mIsInternal || mIsPrimary) {
|
|
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 File[] files = dir.listFiles();
|
|
if (files == null) return 0;
|
|
|
|
// Get sizes of all top level nodes except the ones already computed
|
|
long counter = 0;
|
|
long miscSize = 0;
|
|
|
|
for (File file : files) {
|
|
final String path = file.getAbsolutePath();
|
|
final String name = file.getName();
|
|
if (sMeasureMediaTypes.contains(name)) {
|
|
continue;
|
|
}
|
|
|
|
if (file.isFile()) {
|
|
final long fileSize = file.length();
|
|
mFileInfoForMisc.add(new FileInfo(path, fileSize, counter++));
|
|
miscSize += fileSize;
|
|
} else if (file.isDirectory()) {
|
|
final long dirSize = getDirectorySize(imcs, file);
|
|
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);
|
|
}
|
|
}
|