Checkpoint of new storage UI.
Top-level storage UI now shows list of all devices, both internal and adopted/private volumes, and public/shared volumes. When viewing a private volume, show traditional clustering of data types, including summary of other users. For adopted volumes, any actions are tucked away in a menu, since they're not primary. Misc files browsing is now provided by DocumentsUI. Teach StorageMeasurement about new private volumes, including handling emulated volumes stacked above them. When measuring, only consider apps actually hosted on the current volume UUID. When viewing a public volume, we default to launching into file management mode, and offer a simple eject button at the top-level view. File management mode is offered by new DocumentsUI browse intent, and a Settings link there redirects back to us for actual operations like ejecting/formatting. When unmounted, we launch into our action view. Actions like ejecting/formatting just show simple toasts for now. Bug: 19993667 Change-Id: Ie990ef3c01fb3717aaf8c79bfc53aac7edefdcf7
This commit is contained in:
@@ -27,7 +27,6 @@ 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;
|
||||
@@ -36,23 +35,22 @@ import android.os.Message;
|
||||
import android.os.UserHandle;
|
||||
import android.os.UserManager;
|
||||
import android.os.storage.StorageVolume;
|
||||
import android.os.storage.VolumeInfo;
|
||||
import android.util.Log;
|
||||
import android.util.SparseLongArray;
|
||||
|
||||
import com.android.internal.app.IMediaContainerService;
|
||||
import com.google.android.collect.Maps;
|
||||
import com.android.internal.util.ArrayUtils;
|
||||
import com.google.android.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.Objects;
|
||||
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}
|
||||
@@ -77,28 +75,7 @@ public class StorageMeasurement {
|
||||
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>
|
||||
@@ -128,7 +105,7 @@ public class StorageMeasurement {
|
||||
* When measuring a physical {@link StorageVolume}, this reflects media
|
||||
* on that volume.
|
||||
*/
|
||||
public HashMap<String, Long> mediaSize = Maps.newHashMap();
|
||||
public HashMap<String, Long> mediaSize = new HashMap<>();
|
||||
|
||||
/**
|
||||
* Misc external disk usage for the current user, unaccounted in
|
||||
@@ -144,34 +121,31 @@ public class StorageMeasurement {
|
||||
}
|
||||
|
||||
public interface MeasurementReceiver {
|
||||
public void updateApproximate(StorageMeasurement meas, long totalSize, long availSize);
|
||||
public void updateDetails(StorageMeasurement meas, MeasurementDetails details);
|
||||
public void onDetailsChanged(MeasurementDetails details);
|
||||
}
|
||||
|
||||
private volatile WeakReference<MeasurementReceiver> mReceiver;
|
||||
private WeakReference<MeasurementReceiver> mReceiver;
|
||||
|
||||
/** Physical volume being measured, or {@code null} for internal. */
|
||||
private final StorageVolume mVolume;
|
||||
private final Context mContext;
|
||||
|
||||
private final boolean mIsInternal;
|
||||
private final boolean mIsPrimary;
|
||||
private final VolumeInfo mVolume;
|
||||
private final VolumeInfo mSharedVolume;
|
||||
|
||||
private final MeasurementHandler mHandler;
|
||||
private final MainHandler mMainHandler;
|
||||
private final MeasurementHandler mMeasurementHandler;
|
||||
|
||||
private long mTotalSize;
|
||||
private long mAvailSize;
|
||||
public StorageMeasurement(Context context, VolumeInfo volume, VolumeInfo sharedVolume) {
|
||||
mContext = context.getApplicationContext();
|
||||
|
||||
List<FileInfo> mFileInfoForMisc;
|
||||
|
||||
private StorageMeasurement(Context context, StorageVolume volume) {
|
||||
mVolume = volume;
|
||||
mIsInternal = volume == null;
|
||||
mIsPrimary = volume != null ? volume.isPrimary() : false;
|
||||
mSharedVolume = sharedVolume;
|
||||
|
||||
// Start the thread that will measure the disk usage.
|
||||
final HandlerThread handlerThread = new HandlerThread("MemoryMeasurement");
|
||||
handlerThread.start();
|
||||
mHandler = new MeasurementHandler(context, handlerThread.getLooper());
|
||||
|
||||
mMainHandler = new MainHandler();
|
||||
mMeasurementHandler = new MeasurementHandler(handlerThread.getLooper());
|
||||
}
|
||||
|
||||
public void setReceiver(MeasurementReceiver receiver) {
|
||||
@@ -180,52 +154,38 @@ public class StorageMeasurement {
|
||||
}
|
||||
}
|
||||
|
||||
public void forceMeasure() {
|
||||
invalidate();
|
||||
measure();
|
||||
}
|
||||
|
||||
public void measure() {
|
||||
if (!mHandler.hasMessages(MeasurementHandler.MSG_MEASURE)) {
|
||||
mHandler.sendEmptyMessage(MeasurementHandler.MSG_MEASURE);
|
||||
if (!mMeasurementHandler.hasMessages(MeasurementHandler.MSG_MEASURE)) {
|
||||
mMeasurementHandler.sendEmptyMessage(MeasurementHandler.MSG_MEASURE);
|
||||
}
|
||||
}
|
||||
|
||||
public void cleanUp() {
|
||||
public void onDestroy() {
|
||||
mReceiver = null;
|
||||
mHandler.removeMessages(MeasurementHandler.MSG_MEASURE);
|
||||
mHandler.sendEmptyMessage(MeasurementHandler.MSG_DISCONNECT);
|
||||
mMeasurementHandler.removeMessages(MeasurementHandler.MSG_MEASURE);
|
||||
mMeasurementHandler.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 void invalidate() {
|
||||
mMeasurementHandler.sendEmptyMessage(MeasurementHandler.MSG_INVALIDATE);
|
||||
}
|
||||
|
||||
private static class StatsObserver extends IPackageStatsObserver.Stub {
|
||||
private final boolean mIsInternal;
|
||||
private final boolean mIsPrivate;
|
||||
private final MeasurementDetails mDetails;
|
||||
private final int mCurrentUser;
|
||||
private final Message mFinished;
|
||||
|
||||
private int mRemaining;
|
||||
|
||||
public StatsObserver(boolean isInternal, MeasurementDetails details, int currentUser,
|
||||
public StatsObserver(boolean isPrivate, MeasurementDetails details, int currentUser,
|
||||
Message finished, int remaining) {
|
||||
mIsInternal = isInternal;
|
||||
mIsPrivate = isPrivate;
|
||||
mDetails = details;
|
||||
mCurrentUser = currentUser;
|
||||
mFinished = finished;
|
||||
@@ -245,7 +205,7 @@ public class StorageMeasurement {
|
||||
}
|
||||
|
||||
private void addStatsLocked(PackageStats stats) {
|
||||
if (mIsInternal) {
|
||||
if (mIsPrivate) {
|
||||
long codeSize = stats.codeSize;
|
||||
long dataSize = stats.dataSize;
|
||||
long cacheSize = stats.cacheSize;
|
||||
@@ -279,6 +239,17 @@ public class StorageMeasurement {
|
||||
}
|
||||
}
|
||||
|
||||
private class MainHandler extends Handler {
|
||||
@Override
|
||||
public void handleMessage(Message msg) {
|
||||
final MeasurementDetails details = (MeasurementDetails) msg.obj;
|
||||
final MeasurementReceiver receiver = (mReceiver != null) ? mReceiver.get() : null;
|
||||
if (receiver != null) {
|
||||
receiver.onDetailsChanged(details);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class MeasurementHandler extends Handler {
|
||||
public static final int MSG_MEASURE = 1;
|
||||
public static final int MSG_CONNECTED = 2;
|
||||
@@ -294,8 +265,6 @@ public class StorageMeasurement {
|
||||
|
||||
private MeasurementDetails mCached;
|
||||
|
||||
private final WeakReference<Context> mContext;
|
||||
|
||||
private final ServiceConnection mDefContainerConn = new ServiceConnection() {
|
||||
@Override
|
||||
public void onServiceConnected(ComponentName name, IBinder service) {
|
||||
@@ -313,9 +282,8 @@ public class StorageMeasurement {
|
||||
}
|
||||
};
|
||||
|
||||
public MeasurementHandler(Context context, Looper looper) {
|
||||
public MeasurementHandler(Looper looper) {
|
||||
super(looper);
|
||||
mContext = new WeakReference<Context>(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -323,50 +291,39 @@ public class StorageMeasurement {
|
||||
switch (msg.what) {
|
||||
case MSG_MEASURE: {
|
||||
if (mCached != null) {
|
||||
sendExactUpdate(mCached);
|
||||
mMainHandler.obtainMessage(0, mCached).sendToTarget();
|
||||
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.bindServiceAsUser(service, mDefContainerConn, Context.BIND_AUTO_CREATE,
|
||||
UserHandle.OWNER);
|
||||
mContext.bindServiceAsUser(service, mDefContainerConn,
|
||||
Context.BIND_AUTO_CREATE, UserHandle.OWNER);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case MSG_CONNECTED: {
|
||||
IMediaContainerService imcs = (IMediaContainerService) msg.obj;
|
||||
measureApproximateStorage(imcs);
|
||||
final 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);
|
||||
mContext.unbindService(mDefContainerConn);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case MSG_COMPLETED: {
|
||||
mCached = (MeasurementDetails) msg.obj;
|
||||
sendExactUpdate(mCached);
|
||||
mMainHandler.obtainMessage(0, mCached).sendToTarget();
|
||||
break;
|
||||
}
|
||||
case MSG_INVALIDATE: {
|
||||
@@ -375,88 +332,75 @@ public class StorageMeasurement {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
private void measureExactStorage(IMediaContainerService imcs) {
|
||||
final UserManager userManager = mContext.getSystemService(UserManager.class);
|
||||
final PackageManager packageManager = mContext.getPackageManager();
|
||||
|
||||
sendInternalApproximateUpdate();
|
||||
}
|
||||
final List<UserInfo> users = userManager.getUsers();
|
||||
final int currentUser = ActivityManager.getCurrentUser();
|
||||
|
||||
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 = mMeasurementHandler.obtainMessage(MeasurementHandler.MSG_COMPLETED,
|
||||
details);
|
||||
|
||||
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);
|
||||
if (mSharedVolume != null && mSharedVolume.state == VolumeInfo.STATE_MOUNTED) {
|
||||
final File basePath = mSharedVolume.getPathForUser(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);
|
||||
}
|
||||
for (String type : sMeasureMediaTypes) {
|
||||
final File path = new File(basePath, 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);
|
||||
details.miscSize = measureMisc(imcs, basePath);
|
||||
|
||||
if (mSharedVolume.type == VolumeInfo.TYPE_EMULATED) {
|
||||
// Measure total emulated storage of all users; internal apps data
|
||||
// will be spliced in later
|
||||
for (UserInfo user : users) {
|
||||
for (ApplicationInfo app : apps) {
|
||||
pm.getPackageSizeInfo(app.packageName, user.id, observer);
|
||||
}
|
||||
final File userPath = mSharedVolume.getPathForUser(user.id);
|
||||
final long size = getDirectorySize(imcs, userPath);
|
||||
addValue(details.usersSize, user.id, size);
|
||||
}
|
||||
|
||||
} else {
|
||||
finished.sendToTarget();
|
||||
}
|
||||
}
|
||||
|
||||
// Measure all apps hosted on this volume for all users
|
||||
if (mVolume.type == VolumeInfo.TYPE_PRIVATE) {
|
||||
final List<ApplicationInfo> apps = packageManager.getInstalledApplications(
|
||||
PackageManager.GET_UNINSTALLED_PACKAGES
|
||||
| PackageManager.GET_DISABLED_COMPONENTS);
|
||||
|
||||
final List<ApplicationInfo> volumeApps = new ArrayList<>();
|
||||
for (ApplicationInfo app : apps) {
|
||||
if (Objects.equals(app.volumeUuid, mVolume.fsUuid)) {
|
||||
volumeApps.add(app);
|
||||
}
|
||||
}
|
||||
|
||||
final int count = users.size() * volumeApps.size();
|
||||
if (count == 0) {
|
||||
finished.sendToTarget();
|
||||
return;
|
||||
}
|
||||
|
||||
final StatsObserver observer = new StatsObserver(
|
||||
true, details, currentUser, finished, count);
|
||||
for (UserInfo user : users) {
|
||||
for (ApplicationInfo app : volumeApps) {
|
||||
packageManager.getPackageSizeInfo(app.packageName, user.id, observer);
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
finished.sendToTarget();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private static long getDirectorySize(IMediaContainerService imcs, File path) {
|
||||
@@ -471,64 +415,26 @@ public class StorageMeasurement {
|
||||
}
|
||||
|
||||
private long measureMisc(IMediaContainerService imcs, File dir) {
|
||||
mFileInfoForMisc = new ArrayList<FileInfo>();
|
||||
|
||||
final File[] files = dir.listFiles();
|
||||
if (files == null) return 0;
|
||||
if (ArrayUtils.isEmpty(files)) 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;
|
||||
miscSize += file.length();
|
||||
} 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
|
||||
miscSize += getDirectorySize(imcs, file);
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user