Bug 1506649 - Part 3: Guess ExternalStorageProvider file paths for non-primary volumes. r=snorp
authorJan Henning <jh+bugzilla@buttercookie.de>
Wed, 26 Dec 2018 20:38:01 +0000
changeset 509289 5651b431fe2cecfc584357461be0822b84238952
parent 509288 fd078630d6661dfa3cc43dc793c45fda91e4b78c
child 509290 20c58f920675ca1518432c982e678386b6c2d9e9
push id10547
push userffxbld-merge
push dateMon, 21 Jan 2019 13:03:58 +0000
treeherdermozilla-beta@24ec1916bffe [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerssnorp
bugs1506649
milestone66.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1506649 - Part 3: Guess ExternalStorageProvider file paths for non-primary volumes. r=snorp The AOSP ExternalStorageProvider always creates document IDs of the form "storage device ID" + ':' + "document path", where the storage device ID will be "primary" for the primary emulated storage and the file system UUID for everything else. Assuming that OEMs won't customise this behaviour, the main problem that needs to be handled is how to turn the file system UUID back into a file system path. Differential Revision: https://phabricator.services.mozilla.com/D15259
mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/ContentUriUtils.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/FileUtils.java
--- a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/ContentUriUtils.java
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/ContentUriUtils.java
@@ -46,24 +46,30 @@ public class ContentUriUtils {
      * @author paulburke
      */
     public static @Nullable String getOriginalFilePathFromUri(final Context context, final Uri uri) {
         // DocumentProvider
         if (Build.VERSION.SDK_INT >= 19 && DocumentsContract.isDocumentUri(context, uri)) {
             // ExternalStorageProvider
             if (isExternalStorageDocument(uri)) {
                 final String docId = DocumentsContract.getDocumentId(uri);
+                // The AOSP ExternalStorageProvider creates document IDs of the form
+                // "storage device ID" + ':' + "document path".
                 final String[] split = docId.split(":");
                 final String type = split[0];
+                final String docPath = split[1];
 
+                final String rootPath;
                 if ("primary".equalsIgnoreCase(type)) {
-                    return Environment.getExternalStorageDirectory() + "/" + split[1];
+                    rootPath = Environment.getExternalStorageDirectory().getAbsolutePath();
+                } else {
+                    rootPath = FileUtils.getExternalStoragePath(context, type);
                 }
-
-                // TODO handle non-primary volumes
+                return !TextUtils.isEmpty(rootPath) ?
+                        rootPath + "/" + docPath : null;
             }
             // DownloadsProvider
             else if (isDownloadsDocument(uri)) {
                 final String id = DocumentsContract.getDocumentId(uri);
                 // workaround for issue (https://bugzilla.mozilla.org/show_bug.cgi?id=1502721) and
                 // as per https://github.com/Yalantis/uCrop/issues/318#issuecomment-333066640
                 if (!TextUtils.isEmpty(id)) {
                     if (id.startsWith("raw:")) {
--- a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/FileUtils.java
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/FileUtils.java
@@ -1,19 +1,24 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.util;
 
+import android.annotation.TargetApi;
 import android.content.ContentResolver;
 import android.content.Context;
 import android.database.Cursor;
 import android.net.Uri;
+import android.os.Build;
+import android.os.Environment;
+import android.os.storage.StorageVolume;
 import android.provider.MediaStore;
+import android.support.annotation.Nullable;
 import android.text.TextUtils;
 import android.util.Log;
 
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileNotFoundException;
 import java.io.FileOutputStream;
 import java.io.IOException;
@@ -332,9 +337,81 @@ public class FileUtils {
 
     public static boolean isContentUri(Uri uri) {
         return uri != null && uri.getScheme() != null && CONTENT_SCHEME.equals(uri.getScheme());
     }
 
     public static boolean isContentUri(String sUri) {
         return sUri != null && sUri.startsWith(CONTENT_SCHEME);
     }
+
+    /**
+     * Attempts to find the root path of an external (removable) SD card.
+     *
+     * @param uuid If you know the file system UUID (as returned e.g. by
+     *             {@link StorageVolume#getUuid()}) of the storage device you're looking for, this
+     *             may be used to filter down the selection of available non-emulated storage
+     *             devices. If no storage device matching the given UUID was found, the first
+     *             non-emulated storage device will be returned.
+     * @return The root path of the storage device.
+     */
+    @TargetApi(19)
+    public static @Nullable String getExternalStoragePath(Context context, @Nullable String uuid) {
+        // Since around the time of Lollipop or Marshmallow, the common convention is for external
+        // SD cards to be mounted at /storage/<file system UUID>/, however this pattern is still not
+        // guaranteed to be 100 % reliable. Therefore we need another way of getting all potential
+        // mount points for external storage devices.
+        // StorageManager.getStorageVolumes() might possibly do the trick and be just what we need
+        // to enumerate all mount points, but it only works on API24+.
+        // So instead, we use the output of getExternalFilesDirs for this purpose, which works on
+        // API19 and up.
+        File [] externalStorages = context.getExternalFilesDirs(null);
+        String uuidDir = !TextUtils.isEmpty(uuid) ? '/' + uuid + '/' : null;
+
+        String firstNonEmulatedStorage = null;
+        String targetStorage = null;
+        for (File externalStorage : externalStorages) {
+            if (isExternalStorageEmulated(externalStorage)) {
+                // The paths returned by getExternalFilesDirs also include locations that actually
+                // sit on the internal "external" storage, so we need to filter them out again.
+                continue;
+            }
+            String storagePath = externalStorage.getAbsolutePath();
+            /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+             * NOTE: This is our big assumption in this function: That the folders returned by   *
+             * context.getExternalFilesDir() will always be located somewhere inside             *
+             * /<storage root path>/Android/<app specific directories>, so that we can retrieve  *
+             * the storage root by simply snipping off everything starting from "/Android".      *
+             * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
+            storagePath = storagePath.substring(0, storagePath.indexOf("/Android"));
+            if (firstNonEmulatedStorage == null) {
+                firstNonEmulatedStorage = storagePath;
+            }
+            if (!TextUtils.isEmpty(uuidDir) && storagePath.contains(uuidDir)) {
+                targetStorage = storagePath;
+                break;
+            }
+        }
+        if (targetStorage == null) {
+            // Either no UUID to narrow down the selection was given, or else this device doesn't
+            // mount its SD cards using the file system UUID, so we just fall back to the first
+            // non-emulated storage path we found.
+            targetStorage = firstNonEmulatedStorage;
+        }
+        return targetStorage;
+    }
+
+    /**
+     * Helper method because the framework version of this function is only available from API21+.
+     *
+     * @see Environment#isExternalStorageEmulated(File)
+     */
+    public static boolean isExternalStorageEmulated(File path) {
+        if (Build.VERSION.SDK_INT >= 21) {
+            return Environment.isExternalStorageEmulated(path);
+        } else {
+            String absPath = path.getAbsolutePath();
+            // This is rather hacky, but then SD card support on older Android versions
+            // was equally messy.
+            return absPath.contains("/sdcard0") || absPath.contains("/storage/emulated");
+        }
+    }
 }