OSDN Git Service

Include external storage devices in DocumentsUI.
authorJeff Sharkey <jsharkey@android.com>
Thu, 17 Oct 2013 17:52:17 +0000 (10:52 -0700)
committerJeff Sharkey <jsharkey@android.com>
Thu, 17 Oct 2013 17:55:32 +0000 (10:55 -0700)
Include volume UUID in generated document IDs to uniquely identify
volumes over time.  Show volume label to users.  Watch for mount
changes to update available roots.

Bug: 11175082
Change-Id: Ia151bde768587468efde0c1d97a740b5353d1582

core/java/android/os/storage/StorageVolume.java
packages/ExternalStorageProvider/AndroidManifest.xml
packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java
packages/ExternalStorageProvider/src/com/android/externalstorage/MountReceiver.java [new file with mode: 0644]
services/java/com/android/server/MountService.java

index 1668f59..0285cb9 100644 (file)
@@ -51,6 +51,7 @@ public class StorageVolume implements Parcelable {
 
     private String mUuid;
     private String mUserLabel;
+    private String mState;
 
     // StorageVolume extra for ACTION_MEDIA_REMOVED, ACTION_MEDIA_UNMOUNTED, ACTION_MEDIA_CHECKING,
     // ACTION_MEDIA_NOFS, ACTION_MEDIA_MOUNTED, ACTION_MEDIA_SHARED, ACTION_MEDIA_UNSHARED,
@@ -84,6 +85,7 @@ public class StorageVolume implements Parcelable {
         mOwner = in.readParcelable(null);
         mUuid = in.readString();
         mUserLabel = in.readString();
+        mState = in.readString();
     }
 
     public static StorageVolume fromTemplate(StorageVolume template, File path, UserHandle owner) {
@@ -228,6 +230,14 @@ public class StorageVolume implements Parcelable {
         return mUserLabel;
     }
 
+    public void setState(String state) {
+        mState = state;
+    }
+
+    public String getState() {
+        return mState;
+    }
+
     @Override
     public boolean equals(Object obj) {
         if (obj instanceof StorageVolume && mPath != null) {
@@ -264,6 +274,7 @@ public class StorageVolume implements Parcelable {
         pw.printPair("mOwner", mOwner);
         pw.printPair("mUuid", mUuid);
         pw.printPair("mUserLabel", mUserLabel);
+        pw.printPair("mState", mState);
         pw.decreaseIndent();
     }
 
@@ -298,5 +309,6 @@ public class StorageVolume implements Parcelable {
         parcel.writeParcelable(mOwner, flags);
         parcel.writeString(mUuid);
         parcel.writeString(mUserLabel);
+        parcel.writeString(mState);
     }
 }
index 99a4260..5169fef 100644 (file)
             </intent-filter>
         </provider>
 
+        <receiver android:name=".MountReceiver">
+            <intent-filter>
+                <action android:name="android.intent.action.MEDIA_MOUNTED" />
+                <action android:name="android.intent.action.MEDIA_UNMOUNTED" />
+                <data android:scheme="file" />
+            </intent-filter>
+        </receiver>
+
         <!-- TODO: find a better place for tests to live -->
         <provider
             android:name=".TestDocumentsProvider"
index 11ff2d8..d42354f 100644 (file)
 
 package com.android.externalstorage;
 
+import android.content.Context;
 import android.content.res.AssetFileDescriptor;
 import android.database.Cursor;
 import android.database.MatrixCursor;
 import android.database.MatrixCursor.RowBuilder;
 import android.graphics.Point;
-import android.media.ExifInterface;
 import android.os.CancellationSignal;
 import android.os.Environment;
 import android.os.ParcelFileDescriptor;
+import android.os.storage.StorageManager;
+import android.os.storage.StorageVolume;
+import android.provider.DocumentsContract;
 import android.provider.DocumentsContract.Document;
 import android.provider.DocumentsContract.Root;
-import android.provider.DocumentsContract;
 import android.provider.DocumentsProvider;
+import android.util.Log;
 import android.webkit.MimeTypeMap;
 
+import com.android.internal.annotations.GuardedBy;
 import com.google.android.collect.Lists;
 import com.google.android.collect.Maps;
 
@@ -45,6 +49,8 @@ import java.util.Map;
 public class ExternalStorageProvider extends DocumentsProvider {
     private static final String TAG = "ExternalStorage";
 
+    public static final String AUTHORITY = "com.android.externalstorage.documents";
+
     // docId format: root:path/to/file
 
     private static final String[] DEFAULT_ROOT_PROJECTION = new String[] {
@@ -64,42 +70,91 @@ public class ExternalStorageProvider extends DocumentsProvider {
         public String docId;
     }
 
+    private static final String ROOT_ID_PRIMARY_EMULATED = "primary";
+
+    private StorageManager mStorageManager;
+
+    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;
 
     @Override
     public boolean onCreate() {
+        mStorageManager = (StorageManager) getContext().getSystemService(Context.STORAGE_SERVICE);
+
         mRoots = Lists.newArrayList();
         mIdToRoot = Maps.newHashMap();
         mIdToPath = Maps.newHashMap();
 
-        // TODO: support multiple storage devices, requiring that volume serial
-        // number be burned into rootId so we can identify files from different
-        // volumes. currently we only use a static rootId for emulated storage,
-        // since that storage never changes.
-        if (!Environment.isExternalStorageEmulated()) return true;
-
-        try {
-            final String rootId = "primary";
-            final File path = Environment.getExternalStorageDirectory();
-            mIdToPath.put(rootId, path);
-
-            final RootInfo root = new RootInfo();
-            root.rootId = rootId;
-            root.flags = Root.FLAG_SUPPORTS_CREATE | Root.FLAG_LOCAL_ONLY | Root.FLAG_ADVANCED
-                    | Root.FLAG_SUPPORTS_SEARCH;
-            root.title = getContext().getString(R.string.root_internal_storage);
-            root.docId = getDocIdForFile(path);
-            mRoots.add(root);
-            mIdToRoot.put(rootId, root);
-        } catch (FileNotFoundException e) {
-            throw new IllegalStateException(e);
-        }
+        updateVolumes();
 
         return true;
     }
 
+    public void updateVolumes() {
+        synchronized (mRootsLock) {
+            updateVolumesLocked();
+        }
+    }
+
+    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 String rootId;
+            if (volume.isPrimary() && volume.isEmulated()) {
+                rootId = ROOT_ID_PRIMARY_EMULATED;
+            } else if (volume.getUuid() != null) {
+                rootId = volume.getUuid();
+            } else {
+                Log.d(TAG, "Missing UUID for " + volume.getPath() + "; skipping");
+                continue;
+            }
+
+            if (mIdToPath.containsKey(rootId)) {
+                Log.w(TAG, "Duplicate UUID " + rootId + "; skipping");
+                continue;
+            }
+
+            try {
+                final File path = volume.getPathFile();
+                mIdToPath.put(rootId, path);
+
+                final RootInfo root = new RootInfo();
+                root.rootId = rootId;
+                root.flags = Root.FLAG_SUPPORTS_CREATE | Root.FLAG_LOCAL_ONLY | Root.FLAG_ADVANCED
+                        | Root.FLAG_SUPPORTS_SEARCH;
+                if (ROOT_ID_PRIMARY_EMULATED.equals(rootId)) {
+                    root.title = getContext().getString(R.string.root_internal_storage);
+                } else {
+                    root.title = volume.getUserLabel();
+                }
+                root.docId = getDocIdForFile(path);
+                mRoots.add(root);
+                mIdToRoot.put(rootId, root);
+            } catch (FileNotFoundException e) {
+                throw new IllegalStateException(e);
+            }
+        }
+
+        Log.d(TAG, "After updating volumes, found " + mRoots.size() + " active roots");
+
+        getContext().getContentResolver()
+                .notifyChange(DocumentsContract.buildRootsUri(AUTHORITY), null, false);
+    }
+
     private static String[] resolveRootProjection(String[] projection) {
         return projection != null ? projection : DEFAULT_ROOT_PROJECTION;
     }
@@ -113,11 +168,13 @@ public class ExternalStorageProvider extends DocumentsProvider {
 
         // Find the most-specific root path
         Map.Entry<String, File> mostSpecific = null;
-        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;
+        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;
+                }
             }
         }
 
@@ -143,7 +200,10 @@ public class ExternalStorageProvider extends DocumentsProvider {
         final String tag = docId.substring(0, splitIndex);
         final String path = docId.substring(splitIndex + 1);
 
-        File target = mIdToPath.get(tag);
+        File target;
+        synchronized (mRootsLock) {
+            target = mIdToPath.get(tag);
+        }
         if (target == null) {
             throw new FileNotFoundException("No root for " + tag);
         }
@@ -199,16 +259,18 @@ public class ExternalStorageProvider extends DocumentsProvider {
     @Override
     public Cursor queryRoots(String[] projection) throws FileNotFoundException {
         final MatrixCursor result = new MatrixCursor(resolveRootProjection(projection));
-        for (String rootId : mIdToPath.keySet()) {
-            final RootInfo root = mIdToRoot.get(rootId);
-            final File path = mIdToPath.get(rootId);
-
-            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());
+        synchronized (mRootsLock) {
+            for (String rootId : mIdToPath.keySet()) {
+                final RootInfo root = mIdToRoot.get(rootId);
+                final File path = mIdToPath.get(rootId);
+
+                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());
+            }
         }
         return result;
     }
@@ -277,7 +339,11 @@ public class ExternalStorageProvider extends DocumentsProvider {
     public Cursor querySearchDocuments(String rootId, String query, String[] projection)
             throws FileNotFoundException {
         final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection));
-        final File parent = mIdToPath.get(rootId);
+
+        final File parent;
+        synchronized (mRootsLock) {
+            parent = mIdToPath.get(rootId);
+        }
 
         final LinkedList<File> pending = new LinkedList<File>();
         pending.add(parent);
diff --git a/packages/ExternalStorageProvider/src/com/android/externalstorage/MountReceiver.java b/packages/ExternalStorageProvider/src/com/android/externalstorage/MountReceiver.java
new file mode 100644 (file)
index 0000000..8a6c7d6
--- /dev/null
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2013 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.externalstorage;
+
+import android.content.BroadcastReceiver;
+import android.content.ContentProviderClient;
+import android.content.Context;
+import android.content.Intent;
+
+public class MountReceiver extends BroadcastReceiver {
+    @Override
+    public void onReceive(Context context, Intent intent) {
+        final ContentProviderClient client = context.getContentResolver()
+                .acquireContentProviderClient(ExternalStorageProvider.AUTHORITY);
+        try {
+            ((ExternalStorageProvider) client.getLocalContentProvider()).updateVolumes();
+        } finally {
+            ContentProviderClient.releaseQuietly(client);
+        }
+    }
+}
index 7308b7d..e60231a 100644 (file)
@@ -56,7 +56,6 @@ import android.os.storage.StorageResultCode;
 import android.os.storage.StorageVolume;
 import android.text.TextUtils;
 import android.util.AttributeSet;
-import android.util.Log;
 import android.util.Slog;
 import android.util.Xml;
 
@@ -85,7 +84,6 @@ import java.security.NoSuchAlgorithmException;
 import java.security.spec.InvalidKeySpecException;
 import java.security.spec.KeySpec;
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Iterator;
@@ -666,6 +664,7 @@ class MountService extends IMountService.Stub
         final String oldState;
         synchronized (mVolumesLock) {
             oldState = mVolumeStates.put(path, state);
+            volume.setState(state);
         }
 
         if (state.equals(oldState)) {
@@ -1255,6 +1254,7 @@ class MountService extends IMountService.Stub
 
                             // Until we hear otherwise, treat as unmounted
                             mVolumeStates.put(volume.getPath(), Environment.MEDIA_UNMOUNTED);
+                            volume.setState(Environment.MEDIA_UNMOUNTED);
                         }
                     }
 
@@ -1298,6 +1298,7 @@ class MountService extends IMountService.Stub
         } else {
             // Place stub status for early callers to find
             mVolumeStates.put(volume.getPath(), Environment.MEDIA_MOUNTED);
+            volume.setState(Environment.MEDIA_MOUNTED);
         }
     }