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:
Jeff Sharkey
2015-04-11 21:27:33 -07:00
parent 09c0c1385a
commit 42833b2ff4
20 changed files with 1565 additions and 1502 deletions

View File

@@ -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);
}