536 lines
20 KiB
Java
536 lines
20 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 {
|
|
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 (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);
|
|
}
|
|
}
|