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;
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[] {
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;
}
// 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;
+ }
}
}
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);
}
@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;
}
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);