Bug 1355389 - Filepicker can now handle multiple files r=AndreiLazar
☠☠ backed out by 601d0c8dceac ☠ ☠
authorPeter <cardb@planet-elektronik.de>
Wed, 25 Sep 2019 13:56:51 +0000
changeset 494934 f04641fbd14787d57547375aebbfea16abe64f8b
parent 494933 7b6ae979745160ea591785e5e98ca773484edb92
child 494935 58d3b17e4f8c156133db3713bc6f38466d73aadc
push id114131
push userdluca@mozilla.com
push dateThu, 26 Sep 2019 09:47:34 +0000
treeherdermozilla-inbound@1dc1a755079a [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersAndreiLazar
bugs1355389
milestone71.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 1355389 - Filepicker can now handle multiple files r=AndreiLazar Differential Revision: https://phabricator.services.mozilla.com/D46790
mobile/android/base/java/org/mozilla/gecko/FilePicker.java
mobile/android/base/java/org/mozilla/gecko/FilePickerResultHandler.java
mobile/android/components/FilePicker.js
old mode 100644
new mode 100755
--- a/mobile/android/base/java/org/mozilla/gecko/FilePicker.java
+++ b/mobile/android/base/java/org/mozilla/gecko/FilePicker.java
@@ -33,17 +33,17 @@ import java.util.List;
 public class FilePicker implements BundleEventListener {
     private static final String LOGTAG = "GeckoFilePicker";
     private static FilePicker sFilePicker;
     private static final int MODE_OPEN_MULTIPLE_ATTRIBUTE_VALUE = 3;
     private static final int MODE_OPEN_SINGLE_ATTRIBUTE_VALUE = 0;
     private final Context context;
 
     public interface ResultHandler {
-        void gotFile(String filename);
+        void gotFiles(List<String> filenames);
     }
 
     public static void init(Context context) {
         if (sFilePicker == null) {
             sFilePicker = new FilePicker(context.getApplicationContext());
         }
     }
 
@@ -89,29 +89,29 @@ public class FilePicker implements Bundl
                         // 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).
                         Toast.makeText(context, context.getString(R.string.filepicker_permission_denied), Toast.LENGTH_LONG).show();
                         showFilePickerAsync(title, "*/*", new String[0], isModeOpenMultiple, new ResultHandler() {
                             @Override
-                            public void gotFile(final String filename) {
-                                callback.sendSuccess(filename);
+                            public void gotFiles(final List<String> filenames) {
+                                callback.sendSuccess(filenames.toArray(new String[filenames.size()]));
                             }
                         }, tabId);
                     }
                 })
                 .run(new Runnable() {
                     @Override
                     public void run() {
                         showFilePickerAsync(title, finalMimeType, requiredPermission, isModeOpenMultiple, new ResultHandler() {
                             @Override
-                            public void gotFile(final String filename) {
-                                callback.sendSuccess(filename);
+                            public void gotFiles(final List<String> filenames) {
+                                callback.sendSuccess(filenames.toArray(new String[filenames.size()]));
                             }
                         }, tabId);
                     }
                 });
         }
     }
 
     private static String[] getPermissionsForMimeType(final String mimeType) {
@@ -285,15 +285,15 @@ public class FilePicker implements Bundl
                                        final ResultHandler handler, final int tabId) {
         final FilePickerResultHandler fileHandler =
                 new FilePickerResultHandler(handler, context, tabId);
         final Intent intent = getFilePickerIntent(title, mimeType, availPermissions, isModeOpenMultiple, fileHandler);
         final Activity currentActivity =
                 GeckoActivityMonitor.getInstance().getCurrentActivity();
 
         if (intent == null || currentActivity == null) {
-            handler.gotFile("");
+            handler.gotFiles(new ArrayList<String>());
             return;
         }
 
         ActivityHandlerHelper.startIntentForActivity(currentActivity, intent, fileHandler);
     }
 }
old mode 100644
new mode 100755
--- a/mobile/android/base/java/org/mozilla/gecko/FilePickerResultHandler.java
+++ b/mobile/android/base/java/org/mozilla/gecko/FilePickerResultHandler.java
@@ -4,26 +4,31 @@
 
 package org.mozilla.gecko;
 
 import java.io.File;
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
 
+import java.util.ArrayList;
+import java.util.List;
+
 import org.mozilla.gecko.util.ActivityResultHandler;
 import org.mozilla.gecko.util.FileUtils;
 import org.mozilla.gecko.util.ThreadUtils;
 
 import android.app.Activity;
+import android.content.ClipData;
 import android.content.ContentResolver;
 import android.content.Context;
 import android.content.Intent;
 import android.database.Cursor;
 import android.net.Uri;
+import android.os.Build;
 import android.os.Bundle;
 import android.os.Environment;
 import android.os.Process;
 import android.provider.MediaStore;
 import android.provider.OpenableColumns;
 import android.support.v4.app.LoaderManager.LoaderCallbacks;
 import android.support.v4.content.CursorLoader;
 import android.support.v4.content.Loader;
@@ -35,30 +40,39 @@ class FilePickerResultHandler implements
     private static final String LOGTAG = "FilePickerResultHandler";
     private static final String UPLOADS_DIR = "uploads";
     private static final String ACTION_INLINE_DATA = "inline-data";
 
     private final FilePicker.ResultHandler handler;
     private final int tabId;
     private final File cacheDir;
 
+    private int filesToToWaitFor = 0;
+    private final List<String> filesLoaded = new ArrayList<>();
+
     // this code is really hacky and doesn't belong anywhere so I'm putting it here for now
     // until I can come up with a better solution.
     private String mImageName = "";
 
     /* Use this constructor to asynchronously listen for results */
     public FilePickerResultHandler(final FilePicker.ResultHandler handler, final Context context, final int tabId) {
         this.tabId = tabId;
         this.cacheDir = new File(context.getCacheDir(), UPLOADS_DIR);
         this.handler = handler;
     }
 
     void sendResult(String res) {
+        List<String> files = new ArrayList<String>();
+        files.add(res);
+        sendResults(files);
+    }
+
+    void sendResults(List<String> res) {
         if (handler != null) {
-            handler.gotFile(res);
+            handler.gotFiles(res);
         }
     }
 
     private <T> void initLoader(final LoaderCallbacks<T> callbacks) {
         final Loader<T> loader = callbacks.onCreateLoader(/* id */ 0, /* args */ null);
         loader.registerListener(/* id */ 0, new Loader.OnLoadCompleteListener<T>() {
             @Override
             public void onLoadComplete(final Loader<T> loader, final T data) {
@@ -71,16 +85,32 @@ class FilePickerResultHandler implements
 
     @Override
     public void onActivityResult(int resultCode, Intent intent) {
         if (resultCode != Activity.RESULT_OK) {
             sendResult("");
             return;
         }
 
+        // Since JELLY_BEAN_MR2 multiple files are returned via intent.getClipData.
+        if (intent != null) {
+            if ((android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN_MR2) && (null == intent.getData())) {
+                ClipData clipdata = intent.getClipData();
+
+                filesToToWaitFor = clipdata.getItemCount();
+                for (int i = 0; i < clipdata.getItemCount(); i++) {
+                    ClipData.Item item = clipdata.getItemAt(i);
+                    Uri uri = item.getUri();
+                    String uristr = uri.toString();
+                    initLoader(new FileLoaderCallbacks(uri, cacheDir, tabId));
+                }
+                return;
+            }
+        }
+
         // Camera results won't return an Intent. Use the file name we passed to the original intent.
         // In Android M, camera results return an empty Intent rather than null.
         final boolean emptyResult = intent == null || (intent.getAction() == null && intent.getData() == null);
         // Camera returned a thumbnail of the photo. Use the file name we passed to the original intent.
         final boolean haveThumbnail = intent != null && intent.getAction() != null && intent.getAction().equals(ACTION_INLINE_DATA);
         if (emptyResult || haveThumbnail) {
             if (mImageName != null) {
                 File file = new File(Environment.getExternalStorageDirectory(), mImageName);
@@ -218,16 +248,17 @@ class FilePickerResultHandler implements
                     String fileExt = "." + GeckoAppShell.getExtensionFromMimeType(mimeType);
                     if (fileName == null) {
                         // tmp filenames must be at least 3 characters long. Add a prefix to make sure that happens
                         fileName = "tmp_" + Process.myPid() + fileExt;
                     } else {
                         fileName += fileExt;
                     }
                 }
+
                 final String tempFileName = fileName;
                 ThreadUtils.postToBackgroundThread(() -> {
                     // Now write the data to the temp file
                     FileOutputStream fos = null;
                     try {
                         tempDir = FileUtils.createTempDir(cacheDir, "tmp_");
 
                         File file = new File(tempDir, tempFileName);
@@ -237,17 +268,26 @@ class FilePickerResultHandler implements
                         int len = is.read(buf);
                         while (len != -1) {
                             fos.write(buf, 0, len);
                             len = is.read(buf);
                         }
                         fos.close();
                         is.close();
                         String tempFile = file.getAbsolutePath();
-                        sendResult(tempFile);
+
+                        if (filesToToWaitFor > 0) {
+                            // multiple items?
+                            filesLoaded.add(tempFile);
+                            if (filesLoaded.size() >= filesToToWaitFor) {
+                                sendResults(filesLoaded);
+                            }
+                        } else {
+                            sendResult(tempFile);
+                        }
 
                         if (tabId > -1 && tempDir != null) {
                             Tabs.registerOnTabsChangedListener(this);
                         }
                     } catch (IOException ex) {
                         Log.i(LOGTAG, "Error writing file", ex);
                     } finally {
                         if (fos != null) {
old mode 100644
new mode 100755
--- a/mobile/android/components/FilePicker.js
+++ b/mobile/android/components/FilePicker.js
@@ -17,25 +17,25 @@ XPCOMUtils.defineLazyGlobalGetters(this,
 
 function FilePicker() {}
 
 FilePicker.prototype = {
   _mimeTypeFilter: 0,
   _extensionsFilter: "",
   _defaultString: "",
   _domWin: null,
-  _domFile: null,
   _defaultExtension: null,
   _displayDirectory: null,
   _displaySpecialDirectory: null,
-  _filePath: null,
+  _filePaths: [],
   _promptActive: false,
   _filterIndex: 0,
   _addToRecentDocs: false,
   _title: "",
+  _domFiles: [],
 
   init: function(aParent, aTitle, aMode) {
     this._domWin = aParent;
     this._mode = aMode;
     this._title = aTitle;
 
     let idService = Cc["@mozilla.org/uuid-generator;1"].getService(
       Ci.nsIUUIDGenerator
@@ -132,39 +132,41 @@ FilePicker.prototype = {
     return this._displaySpecialDirectory;
   },
 
   set displaySpecialDirectory(dir) {
     this._displaySpecialDirectory = dir;
   },
 
   get file() {
-    if (!this._filePath) {
-      return null;
-    }
-
-    return new FileUtils.File(this._filePath);
+    return this._filePaths.length
+      ? new FileUtils.File(this._filePaths[0])
+      : null;
   },
 
   get fileURL() {
     let file = this.getFile();
     return Services.io.newFileURI(file);
   },
 
   get files() {
-    return [this.file].values();
+    let files = [];
+    for (var i in this._filePaths) {
+      files.push(new FileUtils.File(this._filePaths[i]));
+    }
+    return files.values();
   },
 
   // We don't support directory selection yet.
   get domFileOrDirectory() {
-    return this._domFile;
+    return this._domFiles.length ? this._domFiles[0] : null;
   },
 
   get domFileOrDirectoryEnumerator() {
-    return [this._domFile].values();
+    return this._domFiles.values();
   },
 
   get addToRecentDocs() {
     return this._addToRecentDocs;
   },
 
   set addToRecentDocs(val) {
     this._addToRecentDocs = val;
@@ -188,17 +190,17 @@ FilePicker.prototype = {
     delete this._promptActive;
 
     if (this._domWin) {
       let winUtils = this._domWin.windowUtils;
       winUtils.leaveModalState();
       this.fireDialogEvent(this._domWin, "DOMModalDialogClosed");
     }
 
-    if (this._filePath) {
+    if (this._filePaths.length) {
       return Ci.nsIFilePicker.returnOK;
     }
 
     return Ci.nsIFilePicker.returnCancel;
   },
 
   open: function(callback) {
     this._callback = callback;
@@ -231,50 +233,59 @@ FilePicker.prototype = {
     } else {
       msg.mode = "mimeType";
       msg.mimeType = this._mimeTypeFilter;
     }
     if (this._mode) {
       msg.modeOpenAttribute = this._mode;
     }
 
-    EventDispatcher.instance
-      .sendRequestForResult(msg)
-      .then(file => {
-        this._filePath = file || null;
-        this._promptActive = false;
-
-        if (!file) {
-          return;
-        }
-
-        if (this._domWin) {
-          return this._domWin.File.createFromNsIFile(this.file, {
-            existenceCheck: false,
-          });
-        }
+    EventDispatcher.instance.sendRequestForResult(msg).then(files => {
+      this._filePaths = files || [];
+      this._promptActive = false;
 
-        return File.createFromNsIFile(this.file, { existenceCheck: false });
-      })
-      .then(
-        domFile => {
-          this._domFile = domFile;
-        },
-        () => {}
-      )
-      .then(() => {
-        if (this._callback) {
-          this._callback.done(
-            this._filePath
-              ? Ci.nsIFilePicker.returnOK
-              : Ci.nsIFilePicker.returnCancel
-          );
+      var result = [];
+      for (var i in this._filePaths) {
+        if (this._filePaths[i]) {
+          if (this._domWin) {
+            result.push(
+              this._domWin.File.createFromNsIFile(
+                new FileUtils.File(this._filePaths[i]),
+                { existenceCheck: false }
+              )
+            );
+          } else {
+            result.push(
+              File.createFromNsIFile(new FileUtils.File(this._filePaths[i]), {
+                existenceCheck: false,
+              })
+            );
+          }
         }
-        delete this._callback;
-      });
+      }
+
+      //return result;
+      Promise.all(result)
+        .then(
+          domFiles => {
+            this._domFiles = domFiles;
+          },
+          () => {}
+        )
+        .then(() => {
+          if (this._callback) {
+            this._callback.done(
+              this._filePaths.length
+                ? Ci.nsIFilePicker.returnOK
+                : Ci.nsIFilePicker.returnCancel
+            );
+          }
+          delete this._callback;
+        });
+    });
   },
 
   fireDialogEvent: function(aDomWin, aEventName) {
     // accessing the document object can throw if this window no longer exists. See bug 789888.
     try {
       if (!aDomWin.document) {
         return;
       }