Bug 1362919 - Allow falling back to basic file picker if permissions are denied. r=nechen
authorJan Henning <jh+bugzilla@buttercookie.de>
Mon, 23 Oct 2017 18:43:45 +0200
changeset 387941 51484f33898df80318c982ccc5bf8218c81185b0
parent 387940 b627cabbeec98ba6a6d528db492f5fa70180ba0a
child 387942 e04990f5e538e65d640c94078e340b7c168a88d0
push id32739
push useracraciun@mozilla.com
push dateWed, 25 Oct 2017 09:29:21 +0000
treeherdermozilla-central@252a8528c5ab [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersnechen
bugs1362919
milestone58.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 1362919 - Allow falling back to basic file picker if permissions are denied. r=nechen Theoretically this patch would also handle the case where the user has granted only some of the requested permissions, but at the moment our Permissions implementation doesn't make it easy to find out *which* permissions have been denied in the fallback case. So for the time being, we assume having no permissions at all in the fallback case. MozReview-Commit-ID: EtxFfiLQONF
mobile/android/base/java/org/mozilla/gecko/FilePicker.java
mobile/android/base/java/org/mozilla/gecko/IntentHelper.java
--- a/mobile/android/base/java/org/mozilla/gecko/FilePicker.java
+++ b/mobile/android/base/java/org/mozilla/gecko/FilePicker.java
@@ -17,21 +17,23 @@ import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
 import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
 import android.net.Uri;
 import android.os.Environment;
 import android.os.Parcelable;
 import android.provider.MediaStore;
+import android.support.annotation.NonNull;
 import android.text.TextUtils;
 import android.util.Log;
 
 import java.io.File;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.HashMap;
 import java.util.List;
 
 public class FilePicker implements BundleEventListener {
     private static final String LOGTAG = "GeckoFilePicker";
     private static FilePicker sFilePicker;
     private final Context context;
 
@@ -77,23 +79,33 @@ public class FilePicker implements Bundl
             } else {
                 perm = Permissions.from(context).doNotPrompt();
             }
 
             perm.withPermissions(requiredPermission)
                 .andFallback(new Runnable() {
                     @Override
                     public void run() {
-                        callback.sendError(null);
+                        // In the fallback case, we still show the picker, just
+                        // with the default file list.
+                        // TODO: Figure out which permissions have been denied and use that
+                        // knowledge for availPermissions. For now we assume we don't have any
+                        // permissions at all (bug 1411014).
+                        showFilePickerAsync(title, "*/*", new String[0], new ResultHandler() {
+                            @Override
+                            public void gotFile(final String filename) {
+                                callback.sendSuccess(filename);
+                            }
+                        }, tabId);
                     }
                 })
                 .run(new Runnable() {
                     @Override
                     public void run() {
-                        showFilePickerAsync(title, finalMimeType, new ResultHandler() {
+                        showFilePickerAsync(title, finalMimeType, requiredPermission, new ResultHandler() {
                             @Override
                             public void gotFile(final String filename) {
                                 callback.sendSuccess(filename);
                             }
                         }, tabId);
                     }
                 });
         }
@@ -105,16 +117,21 @@ public class FilePicker implements Bundl
         } else if (mimeType.startsWith("image/")) {
             return new String[] { Manifest.permission.CAMERA, Manifest.permission.READ_EXTERNAL_STORAGE };
         } else if (mimeType.startsWith("video/")) {
             return new String[] { Manifest.permission.CAMERA, Manifest.permission.READ_EXTERNAL_STORAGE };
         }
         return new String[] { Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO, Manifest.permission.READ_EXTERNAL_STORAGE };
     }
 
+    private static boolean hasPermissionsForMimeType(final String mimeType, final String[] availPermissions) {
+        return Arrays.asList(availPermissions)
+                .containsAll(Arrays.asList(getPermissionsForMimeType(mimeType)));
+    }
+
     private void addActivities(Intent intent, HashMap<String, Intent> intents, HashMap<String, Intent> filters) {
         PackageManager pm = context.getPackageManager();
         List<ResolveInfo> lri = pm.queryIntentActivities(intent, 0);
         for (ResolveInfo ri : lri) {
             ComponentName cn = new ComponentName(ri.activityInfo.applicationInfo.packageName, ri.activityInfo.name);
             if (filters != null && !filters.containsKey(cn.toString())) {
                 Intent rintent = new Intent(intent);
                 rintent.setComponent(cn);
@@ -125,59 +142,69 @@ public class FilePicker implements Bundl
 
     private Intent getIntent(String mimeType) {
         Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
         intent.setType(mimeType);
         intent.addCategory(Intent.CATEGORY_OPENABLE);
         return intent;
     }
 
-    private List<Intent> getIntentsForFilePicker(final String mimeType,
+    private List<Intent> getIntentsForFilePicker(final @NonNull String mimeType,
+                                                 final @NonNull String[] availPermissions,
                                                  final FilePickerResultHandler fileHandler) {
         // The base intent to use for the file picker. Even if this is an implicit intent, Android will
         // still show a list of Activities that match this action/type.
         Intent baseIntent;
         // A HashMap of Activities the base intent will show in the chooser. This is used
         // to filter activities from other intents so that we don't show duplicates.
         HashMap<String, Intent> baseIntents = new HashMap<String, Intent>();
-        // A list of other activities to shwo in the picker (and the intents to launch them).
+        // A list of other activities to show in the picker (and the intents to launch them).
         HashMap<String, Intent> intents = new HashMap<String, Intent> ();
 
-        if ("audio/*".equals(mimeType)) {
+        if (mimeType.startsWith("audio/")) {
             // For audio the only intent is the mimetype
             baseIntent = getIntent(mimeType);
             addActivities(baseIntent, baseIntents, null);
-        } else if ("image/*".equals(mimeType)) {
-            // For images the base is a capture intent
-            baseIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
-            baseIntent.putExtra(MediaStore.EXTRA_OUTPUT,
-                            Uri.fromFile(new File(Environment.getExternalStorageDirectory(),
-                                                  fileHandler.generateImageName())));
+        } else if (mimeType.startsWith("image/")) {
+            baseIntent = getIntent(mimeType);
             addActivities(baseIntent, baseIntents, null);
 
-            // We also add the mimetype intent
-            addActivities(getIntent(mimeType), intents, baseIntents);
-        } else if ("video/*".equals(mimeType)) {
-            // For videos the base is a capture intent
-            baseIntent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE);
+            if (mimeType.equals("image/*") &&
+                    hasPermissionsForMimeType(mimeType, availPermissions)) {
+                // We also add a capture intent
+                Intent intent = IntentHelper.getImageCaptureIntent(
+                        new File(Environment.getExternalStorageDirectory(),
+                                fileHandler.generateImageName()));
+                addActivities(intent, intents, baseIntents);
+            }
+        } else if (mimeType.startsWith("video/")) {
+            baseIntent = getIntent(mimeType);
             addActivities(baseIntent, baseIntents, null);
 
-            // We also add the mimetype intent
-            addActivities(getIntent(mimeType), intents, baseIntents);
+            if (mimeType.equals("video/*") &&
+                    hasPermissionsForMimeType(mimeType, availPermissions)) {
+                // We also add a capture intent
+                Intent intent = IntentHelper.getVideoCaptureIntent();
+                addActivities(intent, intents, baseIntents);
+            }
         } else {
             baseIntent = getIntent("*/*");
             addActivities(baseIntent, baseIntents, null);
 
-            Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
-            intent.putExtra(MediaStore.EXTRA_OUTPUT,
-                            Uri.fromFile(new File(Environment.getExternalStorageDirectory(),
-                                                  fileHandler.generateImageName())));
-            addActivities(intent, intents, baseIntents);
-            intent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE);
-            addActivities(intent, intents, baseIntents);
+            Intent intent;
+            if (hasPermissionsForMimeType("image/*", availPermissions)) {
+                intent = IntentHelper.getImageCaptureIntent(
+                        new File(Environment.getExternalStorageDirectory(),
+                                fileHandler.generateImageName()));
+                addActivities(intent, intents, baseIntents);
+            }
+            if (hasPermissionsForMimeType("video/*", availPermissions)) {
+                intent = IntentHelper.getVideoCaptureIntent();
+                addActivities(intent, intents, baseIntents);
+            }
         }
 
         // If we didn't find any activities, we fall back to the */* mimetype intent
         if (baseIntents.size() == 0 && intents.size() == 0) {
             intents.clear();
 
             baseIntent = getIntent("*/*");
             addActivities(baseIntent, baseIntents, null);
@@ -201,19 +228,20 @@ public class FilePicker implements Bundl
     }
 
     /* Gets an intent that can open a particular mimetype. Will show a prompt with a list
      * of Activities that can handle the mietype. Asynchronously calls the handler when
      * one of the intents is selected. If the caller passes in null for the handler, will still
      * prompt for the activity, but will throw away the result.
      */
     private Intent getFilePickerIntent(String title,
-                                       final String mimeType,
+                                       final @NonNull String mimeType,
+                                       final @NonNull String[] availPermissions,
                                        final FilePickerResultHandler fileHandler) {
-        final List<Intent> intents = getIntentsForFilePicker(mimeType, fileHandler);
+        final List<Intent> intents = getIntentsForFilePicker(mimeType, availPermissions, fileHandler);
 
         if (intents.size() == 0) {
             Log.i(LOGTAG, "no activities for the file picker!");
             return null;
         }
 
         final Intent base = intents.remove(0);
 
@@ -229,21 +257,22 @@ public class FilePicker implements Bundl
                          intents.toArray(new Parcelable[intents.size()]));
         return chooser;
     }
 
     /* Allows the user to pick an activity to load files from using a list prompt. Then opens the activity and
      * sends the file returned to the passed in handler. If a null handler is passed in, will still
      * pick and launch the file picker, but will throw away the result.
      */
-    protected void showFilePickerAsync(final String title, final String mimeType,
+    protected void showFilePickerAsync(final String title, final @NonNull String mimeType,
+                                       final @NonNull String[] availPermissions,
                                        final ResultHandler handler, final int tabId) {
         final FilePickerResultHandler fileHandler =
                 new FilePickerResultHandler(handler, context, tabId);
-        final Intent intent = getFilePickerIntent(title, mimeType, fileHandler);
+        final Intent intent = getFilePickerIntent(title, mimeType, availPermissions, fileHandler);
         final Activity currentActivity =
                 GeckoActivityMonitor.getInstance().getCurrentActivity();
 
         if (intent == null || currentActivity == null) {
             handler.gotFile("");
             return;
         }
 
--- a/mobile/android/base/java/org/mozilla/gecko/IntentHelper.java
+++ b/mobile/android/base/java/org/mozilla/gecko/IntentHelper.java
@@ -19,17 +19,19 @@ import org.mozilla.gecko.widget.External
 
 import android.annotation.TargetApi;
 import android.app.Activity;
 import android.content.Context;
 import android.content.Intent;
 import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
 import android.net.Uri;
+import android.os.Environment;
 import android.provider.Browser;
+import android.provider.MediaStore;
 import android.support.annotation.Nullable;
 import android.support.v4.app.FragmentActivity;
 import android.text.TextUtils;
 import android.util.Log;
 import android.webkit.MimeTypeMap;
 
 import java.io.File;
 import java.io.IOException;
@@ -232,16 +234,27 @@ public final class IntentHelper implemen
         intent.setClassName(AppConstants.ANDROID_PACKAGE_NAME, AppConstants.MOZ_ANDROID_BROWSER_INTENT_CLASS);
         intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
         intent.putExtra(BrowserContract.SKIP_TAB_QUEUE_FLAG, true);
         intent.putExtra(INTENT_EXTRA_TAB_ID, tab.getId());
         intent.putExtra(INTENT_EXTRA_SESSION_UUID, GeckoApplication.getSessionUUID());
         return intent;
     }
 
+    public static Intent getImageCaptureIntent(final File destinationFile) {
+        final Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
+        intent.putExtra(MediaStore.EXTRA_OUTPUT,
+                Uri.fromFile(destinationFile));
+        return intent;
+    }
+
+    public static Intent getVideoCaptureIntent() {
+        return new Intent(MediaStore.ACTION_VIDEO_CAPTURE);
+    }
+
     /**
      * Given a URI, a MIME type, an Android intent "action", and a title,
      * produce an intent which can be used to start an activity to open
      * the specified URI.
      *
      * @param context a <code>Context</code> instance.
      * @param targetURI the string spec of the URI to open.
      * @param mimeType an optional MIME type string.