Bug 936342 - Make downloads end up in the default storage area on b2g r=dhylands,paolo
authorFabrice Desré <fabrice@mozilla.com>
Tue, 12 Nov 2013 13:17:24 -0800
changeset 154643 9e31c9c04ccfdcb432462036798d662500a83b4a
parent 154642 6e129231a605e296de5a4a6d6b898110171ae34d
child 154644 9d504773c9df338bc629ad67ee8cd119382b6a2f
push id25651
push userkwierso@gmail.com
push dateWed, 13 Nov 2013 00:40:26 +0000
treeherdermozilla-central@bb502bb5ed5f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersdhylands, paolo
bugs936342
milestone28.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 936342 - Make downloads end up in the default storage area on b2g r=dhylands,paolo
b2g/components/B2GComponents.manifest
b2g/components/HelperAppDialog.js
b2g/components/moz.build
b2g/installer/package-manifest.in
toolkit/components/jsdownloads/src/DownloadIntegration.jsm
uriloader/exthandler/nsExternalHelperAppService.cpp
--- a/b2g/components/B2GComponents.manifest
+++ b/b2g/components/B2GComponents.manifest
@@ -66,8 +66,12 @@ contract @mozilla.org/network/protocol/a
 # FilePicker.js
 component {436ff8f9-0acc-4b11-8ec7-e293efba3141} FilePicker.js
 contract @mozilla.org/filepicker;1 {436ff8f9-0acc-4b11-8ec7-e293efba3141}
 
 # WebappsUpdateTimer.js
 component {637b0f77-2429-49a0-915f-abf5d0db8b9a} WebappsUpdateTimer.js
 contract @mozilla.org/b2g/webapps-update-timer;1 {637b0f77-2429-49a0-915f-abf5d0db8b9a}
 category update-timer WebappsUpdateTimer @mozilla.org/b2g/webapps-update-timer;1,getService,background-update-timer,webapps.update.interval,86400
+
+# HelperAppDialog.js
+component {710322af-e6ae-4b0c-b2c9-1474a87b077e} HelperAppDialog.js
+contract @mozilla.org/helperapplauncherdialog;1 {710322af-e6ae-4b0c-b2c9-1474a87b077e}
new file mode 100644
--- /dev/null
+++ b/b2g/components/HelperAppDialog.js
@@ -0,0 +1,123 @@
+// -*- Mode: js2; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*-
+/* 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/. */
+
+const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Downloads",
+                                  "resource://gre/modules/Downloads.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Task",
+                                  "resource://gre/modules/Task.jsm");
+
+// -----------------------------------------------------------------------
+// HelperApp Launcher Dialog
+//
+// For now on b2g we never prompt and just download to the default
+// location.
+//
+// -----------------------------------------------------------------------
+
+function HelperAppLauncherDialog() { }
+
+HelperAppLauncherDialog.prototype = {
+  classID: Components.ID("{710322af-e6ae-4b0c-b2c9-1474a87b077e}"),
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIHelperAppLauncherDialog]),
+
+  show: function(aLauncher, aContext, aReason) {
+    aLauncher.MIMEInfo.preferredAction = Ci.nsIMIMEInfo.saveToDisk;
+    aLauncher.saveToDisk(null, false);
+  },
+
+  promptForSaveToFile: function(aLauncher,
+                                aContext,
+                                aDefaultFile,
+                                aSuggestedFileExt,
+                                aForcePrompt) {
+    throw Cr.NS_ERROR_NOT_AVAILABLE;
+  },
+
+  promptForSaveToFileAsync: function(aLauncher,
+                                     aContext,
+                                     aDefaultFile,
+                                     aSuggestedFileExt,
+                                     aForcePrompt) {
+    // Retrieve the user's default download directory.
+    Task.spawn(function() {
+      let file = null;
+      try {
+        let defaultFolder = yield Downloads.getPreferredDownloadsDirectory();
+        let dir = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+        dir.initWithPath(defaultFolder);
+        file = this.validateLeafName(dir, aDefaultFile, aSuggestedFileExt);
+      } catch(e) { }
+      aLauncher.saveDestinationAvailable(file);
+    }.bind(this)).then(null, Cu.reportError);
+  },
+
+  validateLeafName: function(aLocalFile, aLeafName, aFileExt) {
+    if (!(aLocalFile && this.isUsableDirectory(aLocalFile)))
+      return null;
+
+    // Remove any leading periods, since we don't want to save hidden files
+    // automatically.
+    aLeafName = aLeafName.replace(/^\.+/, "");
+
+    if (aLeafName == "")
+      aLeafName = "unnamed" + (aFileExt ? "." + aFileExt : "");
+    aLocalFile.append(aLeafName);
+
+    this.makeFileUnique(aLocalFile);
+    return aLocalFile;
+  },
+
+  makeFileUnique: function(aLocalFile) {
+    try {
+      // Note - this code is identical to that in
+      //   toolkit/content/contentAreaUtils.js.
+      // If you are updating this code, update that code too! We can't share code
+      // here since this is called in a js component.
+      let collisionCount = 0;
+      while (aLocalFile.exists()) {
+        collisionCount++;
+        if (collisionCount == 1) {
+          // Append "(2)" before the last dot in (or at the end of) the filename
+          // special case .ext.gz etc files so we don't wind up with .tar(2).gz
+          if (aLocalFile.leafName.match(/\.[^\.]{1,3}\.(gz|bz2|Z)$/i))
+            aLocalFile.leafName = aLocalFile.leafName.replace(/\.[^\.]{1,3}\.(gz|bz2|Z)$/i, "(2)$&");
+          else
+            aLocalFile.leafName = aLocalFile.leafName.replace(/(\.[^\.]*)?$/, "(2)$&");
+        }
+        else {
+          // replace the last (n) in the filename with (n+1)
+          aLocalFile.leafName = aLocalFile.leafName.replace(/^(.*\()\d+\)/, "$1" + (collisionCount+1) + ")");
+        }
+      }
+      aLocalFile.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0600);
+    }
+    catch (e) {
+      dump("*** exception in makeFileUnique: " + e + "\n");
+
+      if (e.result == Cr.NS_ERROR_FILE_ACCESS_DENIED)
+        throw e;
+
+      if (aLocalFile.leafName == "" || aLocalFile.isDirectory()) {
+        aLocalFile.append("unnamed");
+        if (aLocalFile.exists())
+          aLocalFile.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0600);
+      }
+    }
+  },
+
+  isUsableDirectory: function(aDirectory) {
+    return aDirectory.exists() &&
+           aDirectory.isDirectory() &&
+           aDirectory.isWritable();
+  },
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([HelperAppLauncherDialog]);
--- a/b2g/components/moz.build
+++ b/b2g/components/moz.build
@@ -10,16 +10,17 @@ MODULE = 'B2GComponents'
 
 EXTRA_COMPONENTS += [
     'ActivitiesGlue.js',
     'AlertsService.js',
     'B2GAboutRedirector.js',
     'ContentHandler.js',
     'ContentPermissionPrompt.js',
     'FilePicker.js',
+    'HelperAppDialog.js',
     'MailtoProtocolHandler.js',
     'PaymentGlue.js',
     'ProcessGlobal.js',
     'SmsProtocolHandler.js',
     'TelProtocolHandler.js',
     'WebappsUpdateTimer.js',
     'YoutubeProtocolHandler.js',
 ]
--- a/b2g/installer/package-manifest.in
+++ b/b2g/installer/package-manifest.in
@@ -390,18 +390,16 @@
 @BINPATH@/components/HttpDataUsage.manifest
 @BINPATH@/components/HttpDataUsage.js
 @BINPATH@/components/SiteSpecificUserAgent.js
 @BINPATH@/components/SiteSpecificUserAgent.manifest
 @BINPATH@/components/storage-mozStorage.js
 @BINPATH@/components/crypto-SDR.js
 @BINPATH@/components/jsconsole-clhandler.manifest
 @BINPATH@/components/jsconsole-clhandler.js
-@BINPATH@/components/nsHelperAppDlg.manifest
-@BINPATH@/components/nsHelperAppDlg.js
 @BINPATH@/components/nsDownloadManagerUI.manifest
 @BINPATH@/components/nsDownloadManagerUI.js
 @BINPATH@/components/nsSidebar.manifest
 @BINPATH@/components/nsSidebar.js
 
 ; WiFi, NetworkManager, NetworkStats
 #ifdef MOZ_WIDGET_GONK
 @BINPATH@/components/DOMWifiManager.js
@@ -769,16 +767,17 @@ bin/components/@DLL_PREFIX@nkgnomevfs@DL
 @BINPATH@/components/PaymentGlue.js
 @BINPATH@/components/YoutubeProtocolHandler.js
 @BINPATH@/components/RecoveryService.js
 @BINPATH@/components/MailtoProtocolHandler.js
 @BINPATH@/components/SmsProtocolHandler.js
 @BINPATH@/components/TelProtocolHandler.js
 @BINPATH@/components/B2GAboutRedirector.js
 @BINPATH@/components/FilePicker.js
+@BINPATH@/components/HelperAppDialog.js
 
 @BINPATH@/components/DataStore.manifest
 @BINPATH@/components/DataStoreService.js
 @BINPATH@/components/dom_datastore.xpt
 
 #ifdef MOZ_WEBSPEECH
 @BINPATH@/components/dom_webspeechsynth.xpt
 #endif
--- a/toolkit/components/jsdownloads/src/DownloadIntegration.jsm
+++ b/toolkit/components/jsdownloads/src/DownloadIntegration.jsm
@@ -78,16 +78,20 @@ XPCOMUtils.defineLazyServiceGetter(this,
 /**
  * ArrayBufferView representing the bytes to be written to the "Zone.Identifier"
  * Alternate Data Stream to mark a file as coming from the Internet zone.
  */
 XPCOMUtils.defineLazyGetter(this, "gInternetZoneIdentifier", function() {
   return new TextEncoder().encode("[ZoneTransfer]\r\nZoneId=3\r\n");
 });
 
+XPCOMUtils.defineLazyServiceGetter(this, "volumeService",
+                                   "@mozilla.org/telephony/volume-service;1",
+                                   "nsIVolumeService");
+
 const Timer = Components.Constructor("@mozilla.org/timer;1", "nsITimer",
                                      "initWithCallback");
 
 /**
  * Indicates the delay between a change to the downloads data and the related
  * save operation.  This value is the result of a delicate trade-off, assuming
  * the host application uses the browser history instead of the download store
  * to save completed downloads.
@@ -226,16 +230,58 @@ this.DownloadIntegration = {
       // complete initialization of the view used for detecting changes to
       // downloads to be persisted, before other callers get a chance to modify
       // the list without being detected.
       yield new DownloadAutoSaveView(aList, this._store).initialize();
       new DownloadHistoryObserver(aList);
     }.bind(this));
   },
 
+#ifdef MOZ_WIDGET_GONK
+  /**
+    * Finds the default download directory which can be either in the
+    * internal storage or on the sdcard.
+    *
+    * @return {Promise}
+    * @resolves The downloads directory string path.
+    */
+  _getDefaultDownloadDirectory: function() {
+    return Task.spawn(function() {
+      let directoryPath;
+      let win = Services.wm.getMostRecentWindow("navigator:browser");
+      let storages = win.navigator.getDeviceStorages("sdcard");
+      let preferredStorageName;
+      // Use the first one or the default storage.
+      storages.forEach((aStorage) => {
+        if (aStorage.default || !preferredStorageName) {
+          preferredStorageName = aStorage.storageName;
+        }
+      });
+
+      // Now get the path for this storage area.
+      if (preferredStorageName) {
+        let volume = volumeService.getVolumeByName(preferredStorageName);
+        if (volume &&
+            volume.isMediaPresent &&
+            !volume.isMountLocked &&
+            !volume.isSharing) {
+          directoryPath = OS.Path.join(volume.mountPoint, "downloads");
+          yield OS.File.makeDir(directoryPath, { ignoreExisting: true });
+        }
+      }
+      if (directoryPath) {
+        throw new Task.Result(directoryPath);
+      } else {
+        throw new Components.Exception("No suitable storage for downloads.",
+                                       Cr.NS_ERROR_FILE_UNRECOGNIZED_PATH);
+      }
+    });
+  },
+#endif
+
   /**
    * Determines if a Download object from the list of persistent downloads
    * should be saved into a file, so that it can be restored across sessions.
    *
    * This function allows filtering out downloads that the host application is
    * not interested in persisting across sessions, for example downloads that
    * finished successfully.
    *
@@ -281,24 +327,26 @@ this.DownloadIntegration = {
       // the default Downloads directory.
       let version = parseFloat(Services.sysinfo.getProperty("version"));
       if (version < 6) {
         directoryPath = yield this._createDownloadsDirectory("Pers");
       } else {
         directoryPath = this._getDirectory("DfltDwnld");
       }
 #elifdef XP_UNIX
-#ifdef ANDROID
+#ifdef MOZ_WIDGET_ANDROID
       // Android doesn't have a $HOME directory, and by default we only have
       // write access to /data/data/org.mozilla.{$APP} and /sdcard
       directoryPath = gEnvironment.get("DOWNLOADS_DIRECTORY");
       if (!directoryPath) {
         throw new Components.Exception("DOWNLOADS_DIRECTORY is not set.",
                                        Cr.NS_ERROR_FILE_UNRECOGNIZED_PATH);
       }
+#elifdef MOZ_WIDGET_GONK
+      directoryPath = this._getDefaultDownloadDirectory();
 #else
       // For Linux, use XDG download dir, with a fallback to Home/Downloads
       // if the XDG user dirs are disabled.
       try {
         directoryPath = this._getDirectory("DfltDwnld");
       } catch(e) {
         directoryPath = yield this._createDownloadsDirectory("Home");
       }
@@ -316,16 +364,19 @@ this.DownloadIntegration = {
    * Returns the user downloads directory asynchronously.
    *
    * @return {Promise}
    * @resolves The downloads directory string path.
    */
   getPreferredDownloadsDirectory: function DI_getPreferredDownloadsDirectory() {
     return Task.spawn(function() {
       let directoryPath = null;
+#ifdef MOZ_WIDGET_GONK
+      directoryPath = this._getDefaultDownloadDirectory();
+#else
       let prefValue = 1;
 
       try {
         prefValue = Services.prefs.getIntPref("browser.download.folderList");
       } catch(e) {}
 
       switch(prefValue) {
         case 0: // Desktop
@@ -343,32 +394,35 @@ this.DownloadIntegration = {
           } catch(ex) {
             // Either the preference isn't set or the directory cannot be created.
             directoryPath = yield this.getSystemDownloadsDirectory();
           }
           break;
         default:
           directoryPath = yield this.getSystemDownloadsDirectory();
       }
+#endif
       throw new Task.Result(directoryPath);
     }.bind(this));
   },
 
   /**
    * Returns the temporary downloads directory asynchronously.
    *
    * @return {Promise}
    * @resolves The downloads directory string path.
    */
   getTemporaryDownloadsDirectory: function DI_getTemporaryDownloadsDirectory() {
     return Task.spawn(function() {
       let directoryPath = null;
 #ifdef XP_MACOSX
       directoryPath = yield this.getPreferredDownloadsDirectory();
-#elifdef ANDROID
+#elifdef MOZ_WIDGET_ANDROID
+      directoryPath = yield this.getSystemDownloadsDirectory();
+#elifdef MOZ_WIDGET_GONK
       directoryPath = yield this.getSystemDownloadsDirectory();
 #else
       // For Metro mode on Windows 8,  we want searchability for documents
       // that the user chose to open with an external application.
       if (Services.metro && Services.metro.immersive) {
         directoryPath = yield this.getSystemDownloadsDirectory();
       } else {
         directoryPath = this._getDirectory("TmpD");
--- a/uriloader/exthandler/nsExternalHelperAppService.cpp
+++ b/uriloader/exthandler/nsExternalHelperAppService.cpp
@@ -111,16 +111,20 @@
 
 #ifdef MOZ_WIDGET_ANDROID
 #include "AndroidBridge.h"
 #endif
 
 #include "mozilla/Preferences.h"
 #include "mozilla/ipc/URIUtils.h"
 
+#ifdef MOZ_WIDGET_GONK
+#include "nsDeviceStorage.h"
+#endif
+
 using namespace mozilla;
 using namespace mozilla::ipc;
 
 // Buffer file writes in 32kb chunks
 #define BUFFERED_OUTPUT_SIZE (1024 * 32)
 
 // Download Folder location constants
 #define NS_PREF_DOWNLOAD_DIR        "browser.download.dir"
@@ -323,16 +327,42 @@ static nsresult GetDownloadDirectory(nsI
   }
 
   if (!dir) {
     // If not, we default to the OS X default download location.
     nsresult rv = NS_GetSpecialDirectory(NS_OSX_DEFAULT_DOWNLOAD_DIR,
                                          getter_AddRefs(dir));
     NS_ENSURE_SUCCESS(rv, rv);
   }
+#elif defined(MOZ_WIDGET_GONK)
+  // On Gonk, store the files on the sdcard in the downloads directory.
+  // We need to check with the volume manager which storage point is
+  // available.
+
+  // Pick the default storage in case multiple (internal and external) ones
+  // are available.
+  nsString storageName;
+  nsDOMDeviceStorage::GetDefaultStorageName(NS_LITERAL_STRING("sdcard"),
+                                            storageName);
+  NS_ENSURE_TRUE(!storageName.IsEmpty(), NS_ERROR_FAILURE);
+
+  DeviceStorageFile dsf(NS_LITERAL_STRING("sdcard"),
+                        storageName,
+                        NS_LITERAL_STRING("downloads"));
+  NS_ENSURE_TRUE(dsf.mFile, NS_ERROR_FAILURE);
+  NS_ENSURE_TRUE(dsf.IsAvailable(), NS_ERROR_FAILURE);
+
+  bool alreadyThere;
+  nsresult rv = dsf.mFile->Exists(&alreadyThere);
+  NS_ENSURE_SUCCESS(rv, rv);
+  if (!alreadyThere) {
+    rv = dsf.mFile->Create(nsIFile::DIRECTORY_TYPE, 0770);
+    NS_ENSURE_SUCCESS(rv, rv);
+  }
+  dir = dsf.mFile;
 #elif defined(ANDROID)
   // On mobile devices, we are avoiding exposing users to the file
   // system, and don't save downloads to temp directories
 
   // On Android we only return something if we have and SD-card
   char* downloadDir = getenv("DOWNLOADS_DIRECTORY");
   nsresult rv;
   if (downloadDir) {