Include external storage devices in DocumentsUI.

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
diff --git a/core/java/android/os/storage/StorageVolume.java b/core/java/android/os/storage/StorageVolume.java
index 1668f59..0285cb9 100644
--- a/core/java/android/os/storage/StorageVolume.java
+++ b/core/java/android/os/storage/StorageVolume.java
@@ -51,6 +51,7 @@
 
     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 @@
         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 @@
         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 @@
         pw.printPair("mOwner", mOwner);
         pw.printPair("mUuid", mUuid);
         pw.printPair("mUserLabel", mUserLabel);
+        pw.printPair("mState", mState);
         pw.decreaseIndent();
     }
 
@@ -298,5 +309,6 @@
         parcel.writeParcelable(mOwner, flags);
         parcel.writeString(mUuid);
         parcel.writeString(mUserLabel);
+        parcel.writeString(mState);
     }
 }
diff --git a/packages/ExternalStorageProvider/AndroidManifest.xml b/packages/ExternalStorageProvider/AndroidManifest.xml
index 99a4260..5169fef 100644
--- a/packages/ExternalStorageProvider/AndroidManifest.xml
+++ b/packages/ExternalStorageProvider/AndroidManifest.xml
@@ -16,6 +16,14 @@
             </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"
diff --git a/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java b/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java
index 11ff2d8..d42354f 100644
--- a/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java
+++ b/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java
@@ -16,21 +16,25 @@
 
 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 @@
 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 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 @@
 
         // 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 @@
         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 @@
     @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);
+        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());
+                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 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
index 0000000..8a6c7d6
--- /dev/null
+++ b/packages/ExternalStorageProvider/src/com/android/externalstorage/MountReceiver.java
@@ -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);
+        }
+    }
+}
diff --git a/services/java/com/android/server/MountService.java b/services/java/com/android/server/MountService.java
index 7308b7d..e60231a 100644
--- a/services/java/com/android/server/MountService.java
+++ b/services/java/com/android/server/MountService.java
@@ -56,7 +56,6 @@
 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.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 @@
         final String oldState;
         synchronized (mVolumesLock) {
             oldState = mVolumeStates.put(path, state);
+            volume.setState(state);
         }
 
         if (state.equals(oldState)) {
@@ -1255,6 +1254,7 @@
 
                             // Until we hear otherwise, treat as unmounted
                             mVolumeStates.put(volume.getPath(), Environment.MEDIA_UNMOUNTED);
+                            volume.setState(Environment.MEDIA_UNMOUNTED);
                         }
                     }
 
@@ -1298,6 +1298,7 @@
         } else {
             // Place stub status for early callers to find
             mVolumeStates.put(volume.getPath(), Environment.MEDIA_MOUNTED);
+            volume.setState(Environment.MEDIA_MOUNTED);
         }
     }