Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 27de30d3 authored by Jeff Sharkey's avatar Jeff Sharkey
Browse files

Wire up non-visible volumes, more states.

Adds logic to ExternalStorageProvider to scan non-visible volumes,
such as USB OTG devices.  We use internal paths when surfacing these
volumes, which also optimizes around the FUSE daemon for public
devices.  Also dumps internal state when requested.

VolumeInfo now directly contains DiskInfo, which means it's
snapshotted when sending events, avoiding teardown races.  Switch
notifications to use this DiskInfo directly.

Finish wiring up new volume state, including helper methods to make
it readable/writable state clearer.  Handle disks and volumes with
spaces in their labels.

Bug: 19993667
Change-Id: I5c75e5658a6415976811477aebafee7694bde0f4
parent 7e92ef3a
Loading
Loading
Loading
Loading
+13 −4
Original line number Diff line number Diff line
@@ -505,6 +505,16 @@ public class StorageManager {
        return null;
    }

    /** {@hide} */
    public @Nullable VolumeInfo findPrivateForEmulated(VolumeInfo emulatedVol) {
        return findVolumeById(emulatedVol.getId().replace("emulated", "private"));
    }

    /** {@hide} */
    public @Nullable VolumeInfo findEmulatedForPrivate(VolumeInfo privateVol) {
        return findVolumeById(privateVol.getId().replace("private", "emulated"));
    }

    /** {@hide} */
    public @NonNull List<VolumeInfo> getVolumes() {
        return getVolumes(0);
@@ -523,10 +533,9 @@ public class StorageManager {
    public @Nullable String getBestVolumeDescription(VolumeInfo vol) {
        String descrip = vol.getDescription();

        if (vol.diskId != null) {
            final DiskInfo disk = findDiskById(vol.diskId);
            if (disk != null && TextUtils.isEmpty(descrip)) {
                descrip = disk.getDescription();
        if (vol.disk != null) {
            if (TextUtils.isEmpty(descrip)) {
                descrip = vol.disk.getDescription();
            }
        }

+49 −7
Original line number Diff line number Diff line
@@ -101,23 +101,27 @@ public class VolumeInfo implements Parcelable {
        sStateToEnvironment.put(VolumeInfo.STATE_UNMOUNTED, Environment.MEDIA_UNMOUNTED);
        sStateToEnvironment.put(VolumeInfo.STATE_CHECKING, Environment.MEDIA_CHECKING);
        sStateToEnvironment.put(VolumeInfo.STATE_MOUNTED, Environment.MEDIA_MOUNTED);
        sStateToEnvironment.put(VolumeInfo.STATE_MOUNTED_READ_ONLY, Environment.MEDIA_MOUNTED_READ_ONLY);
        sStateToEnvironment.put(VolumeInfo.STATE_FORMATTING, Environment.MEDIA_UNMOUNTED);
        sStateToEnvironment.put(VolumeInfo.STATE_EJECTING, Environment.MEDIA_EJECTING);
        sStateToEnvironment.put(VolumeInfo.STATE_UNMOUNTABLE, Environment.MEDIA_UNMOUNTABLE);
        sStateToEnvironment.put(VolumeInfo.STATE_REMOVED, Environment.MEDIA_REMOVED);
        sStateToEnvironment.put(VolumeInfo.STATE_BAD_REMOVAL, Environment.MEDIA_BAD_REMOVAL);

        sEnvironmentToBroadcast.put(Environment.MEDIA_UNMOUNTED, Intent.ACTION_MEDIA_UNMOUNTED);
        sEnvironmentToBroadcast.put(Environment.MEDIA_CHECKING, Intent.ACTION_MEDIA_CHECKING);
        sEnvironmentToBroadcast.put(Environment.MEDIA_MOUNTED, Intent.ACTION_MEDIA_MOUNTED);
        sEnvironmentToBroadcast.put(Environment.MEDIA_MOUNTED_READ_ONLY, Intent.ACTION_MEDIA_MOUNTED);
        sEnvironmentToBroadcast.put(Environment.MEDIA_EJECTING, Intent.ACTION_MEDIA_EJECT);
        sEnvironmentToBroadcast.put(Environment.MEDIA_UNMOUNTABLE, Intent.ACTION_MEDIA_UNMOUNTABLE);
        sEnvironmentToBroadcast.put(Environment.MEDIA_REMOVED, Intent.ACTION_MEDIA_REMOVED);
        sEnvironmentToBroadcast.put(Environment.MEDIA_BAD_REMOVAL, Intent.ACTION_MEDIA_BAD_REMOVAL);
    }

    /** vold state */
    public final String id;
    public final int type;
    public final String diskId;
    public final DiskInfo disk;
    public int mountFlags = 0;
    public int mountUserId = -1;
    public int state = STATE_UNMOUNTED;
@@ -131,17 +135,21 @@ public class VolumeInfo implements Parcelable {
    public String nickname;
    public int userFlags = 0;

    public VolumeInfo(String id, int type, String diskId, int mtpIndex) {
    public VolumeInfo(String id, int type, DiskInfo disk, int mtpIndex) {
        this.id = Preconditions.checkNotNull(id);
        this.type = type;
        this.diskId = diskId;
        this.disk = disk;
        this.mtpIndex = mtpIndex;
    }

    public VolumeInfo(Parcel parcel) {
        id = parcel.readString();
        type = parcel.readInt();
        diskId = parcel.readString();
        if (parcel.readInt() != 0) {
            disk = DiskInfo.CREATOR.createFromParcel(parcel);
        } else {
            disk = null;
        }
        mountFlags = parcel.readInt();
        mountUserId = parcel.readInt();
        state = parcel.readInt();
@@ -179,8 +187,12 @@ public class VolumeInfo implements Parcelable {
        return id;
    }

    public @Nullable DiskInfo getDisk() {
        return disk;
    }

    public @Nullable String getDiskId() {
        return diskId;
        return (disk != null) ? disk.id : null;
    }

    public int getType() {
@@ -199,6 +211,10 @@ public class VolumeInfo implements Parcelable {
        return nickname;
    }

    public int getMountUserId() {
        return mountUserId;
    }

    public @Nullable String getDescription() {
        if (ID_PRIVATE_INTERNAL.equals(id)) {
            return Resources.getSystem().getString(com.android.internal.R.string.storage_internal);
@@ -211,6 +227,14 @@ public class VolumeInfo implements Parcelable {
        }
    }

    public boolean isMountedReadable() {
        return state == STATE_MOUNTED || state == STATE_MOUNTED_READ_ONLY;
    }

    public boolean isMountedWritable() {
        return state == STATE_MOUNTED;
    }

    public boolean isPrimary() {
        return (mountFlags & MOUNT_FLAG_PRIMARY) != 0;
    }
@@ -253,6 +277,19 @@ public class VolumeInfo implements Parcelable {
        }
    }

    /**
     * Path which is accessible to apps holding
     * {@link android.Manifest.permission#WRITE_MEDIA_STORAGE}.
     */
    public File getInternalPathForUser(int userId) {
        if (type == TYPE_PUBLIC) {
            // TODO: plumb through cleaner path from vold
            return new File(path.replace("/storage/", "/mnt/media_rw/"));
        } else {
            return getPathForUser(userId);
        }
    }

    public StorageVolume buildStorageVolume(Context context, int userId) {
        final boolean removable;
        final boolean emulated;
@@ -339,7 +376,7 @@ public class VolumeInfo implements Parcelable {
        pw.println("VolumeInfo{" + id + "}:");
        pw.increaseIndent();
        pw.printPair("type", DebugUtils.valueToString(getClass(), "TYPE_", type));
        pw.printPair("diskId", diskId);
        pw.printPair("diskId", getDiskId());
        pw.printPair("mountFlags", DebugUtils.flagsToString(getClass(), "MOUNT_FLAG_", mountFlags));
        pw.printPair("mountUserId", mountUserId);
        pw.printPair("state", DebugUtils.valueToString(getClass(), "STATE_", state));
@@ -403,7 +440,12 @@ public class VolumeInfo implements Parcelable {
    public void writeToParcel(Parcel parcel, int flags) {
        parcel.writeString(id);
        parcel.writeInt(type);
        parcel.writeString(diskId);
        if (disk != null) {
            parcel.writeInt(1);
            disk.writeToParcel(parcel, flags);
        } else {
            parcel.writeInt(0);
        }
        parcel.writeInt(mountFlags);
        parcel.writeInt(mountUserId);
        parcel.writeInt(state);
+1 −1
Original line number Diff line number Diff line
@@ -361,7 +361,7 @@ public class PackageHelper {
        VolumeInfo bestCandidate = null;
        long bestCandidateAvailBytes = Long.MIN_VALUE;
        for (VolumeInfo vol : storageManager.getVolumes()) {
            if (vol.type == VolumeInfo.TYPE_PRIVATE && vol.state == VolumeInfo.STATE_MOUNTED) {
            if (vol.type == VolumeInfo.TYPE_PRIVATE && vol.isMountedWritable()) {
                final long availBytes = storageManager.getStorageBytesUntilLow(new File(vol.path));
                if (availBytes >= sizeBytes) {
                    allCandidates.add(vol.fsUuid);
+90 −63
Original line number Diff line number Diff line
@@ -26,34 +26,35 @@ import android.database.MatrixCursor.RowBuilder;
import android.graphics.Point;
import android.net.Uri;
import android.os.CancellationSignal;
import android.os.Environment;
import android.os.FileObserver;
import android.os.FileUtils;
import android.os.Handler;
import android.os.ParcelFileDescriptor;
import android.os.ParcelFileDescriptor.OnCloseListener;
import android.os.UserHandle;
import android.os.storage.StorageManager;
import android.os.storage.StorageVolume;
import android.os.storage.VolumeInfo;
import android.provider.DocumentsContract;
import android.provider.DocumentsContract.Document;
import android.provider.DocumentsContract.Root;
import android.provider.DocumentsProvider;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.DebugUtils;
import android.util.Log;
import android.webkit.MimeTypeMap;

import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.google.android.collect.Lists;
import com.google.android.collect.Maps;
import com.android.internal.util.IndentingPrintWriter;

import java.io.File;
import java.io.FileDescriptor;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.io.PrintWriter;
import java.util.LinkedList;
import java.util.Map;
import java.util.List;
import java.util.Objects;

public class ExternalStorageProvider extends DocumentsProvider {
@@ -80,6 +81,8 @@ public class ExternalStorageProvider extends DocumentsProvider {
        public int flags;
        public String title;
        public String docId;
        public File visiblePath;
        public File path;
    }

    private static final String ROOT_ID_PRIMARY_EMULATED = "primary";
@@ -90,26 +93,17 @@ public class ExternalStorageProvider extends DocumentsProvider {
    private final Object mRootsLock = new Object();

    @GuardedBy("mRootsLock")
    private ArrayList<RootInfo> mRoots;
    @GuardedBy("mRootsLock")
    private HashMap<String, RootInfo> mIdToRoot;
    @GuardedBy("mRootsLock")
    private HashMap<String, File> mIdToPath;
    private ArrayMap<String, RootInfo> mRoots = new ArrayMap<>();

    @GuardedBy("mObservers")
    private Map<File, DirectoryObserver> mObservers = Maps.newHashMap();
    private ArrayMap<File, DirectoryObserver> mObservers = new ArrayMap<>();

    @Override
    public boolean onCreate() {
        mStorageManager = (StorageManager) getContext().getSystemService(Context.STORAGE_SERVICE);
        mHandler = new Handler();

        mRoots = Lists.newArrayList();
        mIdToRoot = Maps.newHashMap();
        mIdToPath = Maps.newHashMap();

        updateVolumes();

        return true;
    }

@@ -121,52 +115,53 @@ public class ExternalStorageProvider extends DocumentsProvider {

    private void updateVolumesLocked() {
        mRoots.clear();
        mIdToPath.clear();
        mIdToRoot.clear();

        final StorageVolume[] volumes = mStorageManager.getVolumeList();
        for (StorageVolume volume : volumes) {
            final boolean mounted = Environment.MEDIA_MOUNTED.equals(volume.getState())
                    || Environment.MEDIA_MOUNTED_READ_ONLY.equals(volume.getState());
            if (!mounted) continue;
        final int userId = UserHandle.myUserId();
        final List<VolumeInfo> volumes = mStorageManager.getVolumes();
        for (VolumeInfo volume : volumes) {
            if (!volume.isMountedReadable()) continue;

            final String rootId;
            if (volume.isPrimary() && volume.isEmulated()) {
            if (VolumeInfo.ID_EMULATED_INTERNAL.equals(volume.getId())) {
                rootId = ROOT_ID_PRIMARY_EMULATED;
            } else if (volume.getUuid() != null) {
                rootId = volume.getUuid();
            } else if (volume.getType() == VolumeInfo.TYPE_EMULATED) {
                final VolumeInfo privateVol = mStorageManager.findPrivateForEmulated(volume);
                rootId = privateVol.getFsUuid();
            } else if (volume.getType() == VolumeInfo.TYPE_PUBLIC) {
                rootId = volume.getFsUuid();
            } else {
                Log.d(TAG, "Missing UUID for " + volume.getPath() + "; skipping");
                // Unsupported volume; ignore
                continue;
            }

            if (mIdToPath.containsKey(rootId)) {
                Log.w(TAG, "Duplicate UUID " + rootId + "; skipping");
            if (TextUtils.isEmpty(rootId)) {
                Log.d(TAG, "Missing UUID for " + volume.getId() + "; skipping");
                continue;
            }
            if (mRoots.containsKey(rootId)) {
                Log.w(TAG, "Duplicate UUID " + rootId + " for " + volume.getId() + "; skipping");
                continue;
            }

            try {
                final File path = volume.getPathFile();
                mIdToPath.put(rootId, path);

                final RootInfo root = new RootInfo();
                mRoots.put(rootId, root);

                root.rootId = rootId;
                root.flags = Root.FLAG_SUPPORTS_CREATE | Root.FLAG_LOCAL_ONLY | Root.FLAG_ADVANCED
                        | Root.FLAG_SUPPORTS_SEARCH | Root.FLAG_SUPPORTS_IS_CHILD;
                if (ROOT_ID_PRIMARY_EMULATED.equals(rootId)) {
                    root.title = getContext().getString(R.string.root_internal_storage);
                } else {
                    final String userLabel = volume.getUserLabel();
                    if (!TextUtils.isEmpty(userLabel)) {
                        root.title = userLabel;
                    } else {
                        root.title = volume.getDescription(getContext());
                    root.title = mStorageManager.getBestVolumeDescription(volume);
                }
                if (volume.getType() == VolumeInfo.TYPE_PUBLIC) {
                    root.flags |= Root.FLAG_HAS_SETTINGS;
                }
                root.docId = getDocIdForFile(path);
                mRoots.add(root);
                mIdToRoot.put(rootId, root);
                root.visiblePath = volume.getPathForUser(userId);
                root.path = volume.getInternalPathForUser(userId);
                root.docId = getDocIdForFile(root.path);

            } catch (FileNotFoundException e) {
                throw new IllegalStateException(e);
            }
@@ -190,23 +185,26 @@ public class ExternalStorageProvider extends DocumentsProvider {
        String path = file.getAbsolutePath();

        // Find the most-specific root path
        Map.Entry<String, File> mostSpecific = null;
        String mostSpecificId = null;
        String mostSpecificPath = null;
        synchronized (mRootsLock) {
            for (Map.Entry<String, File> root : mIdToPath.entrySet()) {
                final String rootPath = root.getValue().getPath();
                if (path.startsWith(rootPath) && (mostSpecific == null
                        || rootPath.length() > mostSpecific.getValue().getPath().length())) {
                    mostSpecific = root;
            for (int i = 0; i < mRoots.size(); i++) {
                final String rootId = mRoots.keyAt(i);
                final String rootPath = mRoots.valueAt(i).path.getAbsolutePath();
                if (path.startsWith(rootPath) && (mostSpecificPath == null
                        || rootPath.length() > mostSpecificPath.length())) {
                    mostSpecificId = rootId;
                    mostSpecificPath = rootPath;
                }
            }
        }

        if (mostSpecific == null) {
        if (mostSpecificPath == null) {
            throw new FileNotFoundException("Failed to find root that contains " + path);
        }

        // Start at first char of path under root
        final String rootPath = mostSpecific.getValue().getPath();
        final String rootPath = mostSpecificPath;
        if (rootPath.equals(path)) {
            path = "";
        } else if (rootPath.endsWith("/")) {
@@ -215,21 +213,30 @@ public class ExternalStorageProvider extends DocumentsProvider {
            path = path.substring(rootPath.length() + 1);
        }

        return mostSpecific.getKey() + ':' + path;
        return mostSpecificId + ':' + path;
    }

    private File getFileForDocId(String docId) throws FileNotFoundException {
        return getFileForDocId(docId, false);
    }

    private File getFileForDocId(String docId, boolean visible) throws FileNotFoundException {
        final int splitIndex = docId.indexOf(':', 1);
        final String tag = docId.substring(0, splitIndex);
        final String path = docId.substring(splitIndex + 1);

        File target;
        RootInfo root;
        synchronized (mRootsLock) {
            target = mIdToPath.get(tag);
            root = mRoots.get(tag);
        }
        if (target == null) {
        if (root == null) {
            throw new FileNotFoundException("No root for " + tag);
        }

        File target = visible ? root.visiblePath : root.path;
        if (target == null) {
            return null;
        }
        if (!target.exists()) {
            target.mkdirs();
        }
@@ -286,16 +293,13 @@ public class ExternalStorageProvider extends DocumentsProvider {
    public Cursor queryRoots(String[] projection) throws FileNotFoundException {
        final MatrixCursor result = new MatrixCursor(resolveRootProjection(projection));
        synchronized (mRootsLock) {
            for (String rootId : mIdToPath.keySet()) {
                final RootInfo root = mIdToRoot.get(rootId);
                final File path = mIdToPath.get(rootId);

            for (RootInfo root : mRoots.values()) {
                final RowBuilder row = result.newRow();
                row.add(Root.COLUMN_ROOT_ID, root.rootId);
                row.add(Root.COLUMN_FLAGS, root.flags);
                row.add(Root.COLUMN_TITLE, root.title);
                row.add(Root.COLUMN_DOCUMENT_ID, root.docId);
                row.add(Root.COLUMN_AVAILABLE_BYTES, path.getFreeSpace());
                row.add(Root.COLUMN_AVAILABLE_BYTES, root.path.getFreeSpace());
            }
        }
        return result;
@@ -464,7 +468,7 @@ public class ExternalStorageProvider extends DocumentsProvider {

        final File parent;
        synchronized (mRootsLock) {
            parent = mIdToPath.get(rootId);
            parent = mRoots.get(rootId).path;
        }

        final LinkedList<File> pending = new LinkedList<File>();
@@ -494,8 +498,10 @@ public class ExternalStorageProvider extends DocumentsProvider {
            String documentId, String mode, CancellationSignal signal)
            throws FileNotFoundException {
        final File file = getFileForDocId(documentId);
        final File visibleFile = getFileForDocId(documentId, true);

        final int pfdMode = ParcelFileDescriptor.parseMode(mode);
        if (pfdMode == ParcelFileDescriptor.MODE_READ_ONLY) {
        if (pfdMode == ParcelFileDescriptor.MODE_READ_ONLY || visibleFile == null) {
            return ParcelFileDescriptor.open(file, pfdMode);
        } else {
            try {
@@ -505,7 +511,7 @@ public class ExternalStorageProvider extends DocumentsProvider {
                    public void onClose(IOException e) {
                        final Intent intent = new Intent(
                                Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
                        intent.setData(Uri.fromFile(file));
                        intent.setData(Uri.fromFile(visibleFile));
                        getContext().sendBroadcast(intent);
                    }
                });
@@ -523,6 +529,27 @@ public class ExternalStorageProvider extends DocumentsProvider {
        return DocumentsContract.openImageThumbnail(file);
    }

    @Override
    public void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
        final IndentingPrintWriter pw = new IndentingPrintWriter(writer, "  ", 160);
        synchronized (mRootsLock) {
            for (int i = 0; i < mRoots.size(); i++) {
                final RootInfo root = mRoots.valueAt(i);
                pw.println("Root{" + root.rootId + "}:");
                pw.increaseIndent();
                pw.printPair("flags", DebugUtils.flagsToString(Root.class, "FLAG_", root.flags));
                pw.println();
                pw.printPair("title", root.title);
                pw.printPair("docId", root.docId);
                pw.println();
                pw.printPair("path", root.path);
                pw.printPair("visiblePath", root.visiblePath);
                pw.decreaseIndent();
                pw.println();
            }
        }
    }

    private static String getTypeForFile(File file) {
        if (file.isDirectory()) {
            return Document.MIME_TYPE_DIR;
+8 −7
Original line number Diff line number Diff line
@@ -60,7 +60,7 @@ public class StorageNotification extends SystemUI {
            // Avoid kicking notifications when getting early metadata before
            // mounted. If already mounted, we're being kicked because of a
            // nickname or init'ed change.
            if (vol.getState() == VolumeInfo.STATE_MOUNTED) {
            if (vol.isMountedReadable()) {
                onVolumeStateChangedInternal(vol, vol.getState(), vol.getState());
            }
        }
@@ -111,6 +111,7 @@ public class StorageNotification extends SystemUI {
                onVolumeChecking(vol);
                break;
            case VolumeInfo.STATE_MOUNTED:
            case VolumeInfo.STATE_MOUNTED_READ_ONLY:
                onVolumeMounted(vol);
                break;
            case VolumeInfo.STATE_FORMATTING:
@@ -136,7 +137,7 @@ public class StorageNotification extends SystemUI {
    }

    private void onVolumeChecking(VolumeInfo vol) {
        final DiskInfo disk = mStorageManager.findDiskById(vol.getDiskId());
        final DiskInfo disk = vol.getDisk();
        final CharSequence title = mContext.getString(
                R.string.ext_media_checking_notification_title, disk.getDescription());
        final CharSequence text = mContext.getString(
@@ -156,7 +157,7 @@ public class StorageNotification extends SystemUI {
        // Don't annoy when user dismissed in past
        if (vol.isSnoozed()) return;

        final DiskInfo disk = mStorageManager.findDiskById(vol.getDiskId());
        final DiskInfo disk = vol.getDisk();
        final Notification notif;
        if (disk.isAdoptable() && !vol.isInited()) {
            final CharSequence title = disk.getDescription();
@@ -198,7 +199,7 @@ public class StorageNotification extends SystemUI {
    }

    private void onVolumeEjecting(VolumeInfo vol) {
        final DiskInfo disk = mStorageManager.findDiskById(vol.getDiskId());
        final DiskInfo disk = vol.getDisk();
        final CharSequence title = mContext.getString(
                R.string.ext_media_unmounting_notification_title, disk.getDescription());
        final CharSequence text = mContext.getString(
@@ -215,7 +216,7 @@ public class StorageNotification extends SystemUI {
    }

    private void onVolumeUnmountable(VolumeInfo vol) {
        final DiskInfo disk = mStorageManager.findDiskById(vol.getDiskId());
        final DiskInfo disk = vol.getDisk();
        final CharSequence title = mContext.getString(
                R.string.ext_media_unmountable_notification_title, disk.getDescription());
        final CharSequence text = mContext.getString(
@@ -236,7 +237,7 @@ public class StorageNotification extends SystemUI {
            return;
        }

        final DiskInfo disk = mStorageManager.findDiskById(vol.getDiskId());
        final DiskInfo disk = vol.getDisk();
        final CharSequence title = mContext.getString(
                R.string.ext_media_nomedia_notification_title, disk.getDescription());
        final CharSequence text = mContext.getString(
@@ -256,7 +257,7 @@ public class StorageNotification extends SystemUI {
            return;
        }

        final DiskInfo disk = mStorageManager.findDiskById(vol.getDiskId());
        final DiskInfo disk = vol.getDisk();
        final CharSequence title = mContext.getString(
                R.string.ext_media_badremoval_notification_title, disk.getDescription());
        final CharSequence text = mContext.getString(
Loading