summaryrefslogtreecommitdiffstats
path: root/mobile/android/components/HelperAppDialog.js
diff options
context:
space:
mode:
Diffstat (limited to 'mobile/android/components/HelperAppDialog.js')
-rw-r--r--mobile/android/components/HelperAppDialog.js373
1 files changed, 373 insertions, 0 deletions
diff --git a/mobile/android/components/HelperAppDialog.js b/mobile/android/components/HelperAppDialog.js
new file mode 100644
index 000000000..f127fb0b3
--- /dev/null
+++ b/mobile/android/components/HelperAppDialog.js
@@ -0,0 +1,373 @@
+// -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+/* 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/. */
+
+/*globals ContentAreaUtils */
+
+const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+const APK_MIME_TYPE = "application/vnd.android.package-archive";
+
+const OMA_DOWNLOAD_DESCRIPTOR_MIME_TYPE = "application/vnd.oma.dd+xml";
+const OMA_DRM_MESSAGE_MIME = "application/vnd.oma.drm.message";
+const OMA_DRM_CONTENT_MIME = "application/vnd.oma.drm.content";
+const OMA_DRM_RIGHTS_MIME = "application/vnd.oma.drm.rights+wbxml";
+
+const PREF_BD_USEDOWNLOADDIR = "browser.download.useDownloadDir";
+const URI_GENERIC_ICON_DOWNLOAD = "drawable://alert_download";
+
+Cu.import("resource://gre/modules/Downloads.jsm");
+Cu.import("resource://gre/modules/FileUtils.jsm");
+Cu.import("resource://gre/modules/HelperApps.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/NetUtil.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "RuntimePermissions", "resource://gre/modules/RuntimePermissions.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Messaging", "resource://gre/modules/Messaging.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Snackbars", "resource://gre/modules/Snackbars.jsm");
+
+// -----------------------------------------------------------------------
+// HelperApp Launcher Dialog
+// -----------------------------------------------------------------------
+
+XPCOMUtils.defineLazyGetter(this, "ContentAreaUtils", function() {
+ let ContentAreaUtils = {};
+ Services.scriptloader.loadSubScript("chrome://global/content/contentAreaUtils.js", ContentAreaUtils);
+ return ContentAreaUtils;
+});
+
+function HelperAppLauncherDialog() { }
+
+HelperAppLauncherDialog.prototype = {
+ classID: Components.ID("{e9d277a0-268a-4ec2-bb8c-10fdf3e44611}"),
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIHelperAppLauncherDialog]),
+
+ /**
+ * Returns false if `url` represents a local or special URL that we don't
+ * wish to ever download.
+ *
+ * Returns true otherwise.
+ */
+ _canDownload: function (url, alreadyResolved=false) {
+ // The common case.
+ if (url.schemeIs("http") ||
+ url.schemeIs("https") ||
+ url.schemeIs("ftp")) {
+ return true;
+ }
+
+ // The less-common opposite case.
+ if (url.schemeIs("chrome") ||
+ url.schemeIs("jar") ||
+ url.schemeIs("resource") ||
+ url.schemeIs("wyciwyg") ||
+ url.schemeIs("file")) {
+ return false;
+ }
+
+ // For all other URIs, try to resolve them to an inner URI, and check that.
+ if (!alreadyResolved) {
+ let innerURI = NetUtil.newChannel({
+ uri: url,
+ loadUsingSystemPrincipal: true
+ }).URI;
+
+ if (!url.equals(innerURI)) {
+ return this._canDownload(innerURI, true);
+ }
+ }
+
+ // Anything else is fine to download.
+ return true;
+ },
+
+ /**
+ * Returns true if `launcher` represents a download for which we wish
+ * to prompt.
+ */
+ _shouldPrompt: function (launcher) {
+ let mimeType = this._getMimeTypeFromLauncher(launcher);
+
+ // Straight equality: nsIMIMEInfo normalizes.
+ return APK_MIME_TYPE == mimeType || OMA_DOWNLOAD_DESCRIPTOR_MIME_TYPE == mimeType;
+ },
+
+ /**
+ * Returns true if `launcher` represents a download for which we wish to
+ * offer a "Save to disk" option.
+ */
+ _shouldAddSaveToDiskIntent: function(launcher) {
+ let mimeType = this._getMimeTypeFromLauncher(launcher);
+
+ // We can't handle OMA downloads. So don't even try. (Bug 1219078)
+ return mimeType != OMA_DOWNLOAD_DESCRIPTOR_MIME_TYPE;
+ },
+
+ /**
+ * Returns true if `launcher`represents a download that should not be handled by Firefox
+ * or a third-party app and instead be forwarded to Android's download manager.
+ */
+ _shouldForwardToAndroidDownloadManager: function(aLauncher) {
+ let forwardDownload = Services.prefs.getBoolPref('browser.download.forward_oma_android_download_manager');
+ if (!forwardDownload) {
+ return false;
+ }
+
+ let mimeType = aLauncher.MIMEInfo.MIMEType;
+ if (!mimeType) {
+ mimeType = ContentAreaUtils.getMIMETypeForURI(aLauncher.source) || "";
+ }
+
+ return [
+ OMA_DOWNLOAD_DESCRIPTOR_MIME_TYPE,
+ OMA_DRM_MESSAGE_MIME,
+ OMA_DRM_CONTENT_MIME,
+ OMA_DRM_RIGHTS_MIME
+ ].indexOf(mimeType) != -1;
+ },
+
+ show: function hald_show(aLauncher, aContext, aReason) {
+ if (!this._canDownload(aLauncher.source)) {
+ this._refuseDownload(aLauncher);
+ return;
+ }
+
+ if (this._shouldForwardToAndroidDownloadManager(aLauncher)) {
+ Task.spawn(function* () {
+ try {
+ let hasPermission = yield RuntimePermissions.waitForPermissions(RuntimePermissions.WRITE_EXTERNAL_STORAGE);
+ if (hasPermission) {
+ this._downloadWithAndroidDownloadManager(aLauncher);
+ aLauncher.cancel(Cr.NS_BINDING_ABORTED);
+ }
+ } finally {
+ }
+ }.bind(this)).catch(Cu.reportError);
+ return;
+ }
+
+ let bundle = Services.strings.createBundle("chrome://browser/locale/browser.properties");
+
+ let defaultHandler = new Object();
+ let apps = HelperApps.getAppsForUri(aLauncher.source, {
+ mimeType: aLauncher.MIMEInfo.MIMEType,
+ });
+
+ if (this._shouldAddSaveToDiskIntent(aLauncher)) {
+ // Add a fake intent for save to disk at the top of the list.
+ apps.unshift({
+ name: bundle.GetStringFromName("helperapps.saveToDisk"),
+ packageName: "org.mozilla.gecko.Download",
+ iconUri: "drawable://icon",
+ selected: true, // Default to download for files
+ launch: function() {
+ // Reset the preferredAction here.
+ aLauncher.MIMEInfo.preferredAction = Ci.nsIMIMEInfo.saveToDisk;
+ aLauncher.saveToDisk(null, false);
+ return true;
+ }
+ });
+ }
+
+ // We do not handle this download and there are no apps that want to do it
+ if (apps.length === 0) {
+ this._refuseDownload(aLauncher);
+ return;
+ }
+
+ let callback = function(app) {
+ aLauncher.MIMEInfo.preferredAction = Ci.nsIMIMEInfo.useHelperApp;
+ if (!app.launch(aLauncher.source)) {
+ // Once the app is done we need to get rid of the temp file. This shouldn't
+ // get run in the saveToDisk case.
+ aLauncher.cancel(Cr.NS_BINDING_ABORTED);
+ }
+ }
+
+ // See if the user already marked something as the default for this mimetype,
+ // and if that app is still installed.
+ let preferredApp = this._getPreferredApp(aLauncher);
+ if (preferredApp) {
+ let pref = apps.filter(function(app) {
+ return app.packageName === preferredApp;
+ });
+
+ if (pref.length > 0) {
+ callback(pref[0]);
+ return;
+ }
+ }
+
+ // If there's only one choice, and we don't want to prompt, go right ahead
+ // and choose that app automatically.
+ if (!this._shouldPrompt(aLauncher) && (apps.length === 1)) {
+ callback(apps[0]);
+ return;
+ }
+
+ // Otherwise, let's go through the prompt.
+ HelperApps.prompt(apps, {
+ title: bundle.GetStringFromName("helperapps.pick"),
+ buttons: [
+ bundle.GetStringFromName("helperapps.alwaysUse"),
+ bundle.GetStringFromName("helperapps.useJustOnce")
+ ],
+ // Tapping an app twice should choose "Just once".
+ doubleTapButton: 1
+ }, (data) => {
+ if (data.button < 0) {
+ return;
+ }
+
+ callback(apps[data.icongrid0]);
+
+ if (data.button === 0) {
+ this._setPreferredApp(aLauncher, apps[data.icongrid0]);
+ }
+ });
+ },
+
+ _refuseDownload: function(aLauncher) {
+ aLauncher.cancel(Cr.NS_BINDING_ABORTED);
+
+ Services.console.logStringMessage("Refusing download of non-downloadable file.");
+
+ let bundle = Services.strings.createBundle("chrome://browser/locale/handling.properties");
+ let failedText = bundle.GetStringFromName("download.blocked");
+
+ Snackbars.show(failedText, Snackbars.LENGTH_LONG);
+ },
+
+ _downloadWithAndroidDownloadManager(aLauncher) {
+ let mimeType = aLauncher.MIMEInfo.MIMEType;
+ if (!mimeType) {
+ mimeType = ContentAreaUtils.getMIMETypeForURI(aLauncher.source) || "";
+ }
+
+ Messaging.sendRequest({
+ 'type': 'Download:AndroidDownloadManager',
+ 'uri': aLauncher.source.spec,
+ 'mimeType': mimeType,
+ 'filename': aLauncher.suggestedFileName
+ });
+ },
+
+ _getPrefName: function getPrefName(mimetype) {
+ return "browser.download.preferred." + mimetype.replace("\\", ".");
+ },
+
+ _getMimeTypeFromLauncher: function (launcher) {
+ let mime = launcher.MIMEInfo.MIMEType;
+ if (!mime)
+ mime = ContentAreaUtils.getMIMETypeForURI(launcher.source) || "";
+ return mime;
+ },
+
+ _getPreferredApp: function getPreferredApp(launcher) {
+ let mime = this._getMimeTypeFromLauncher(launcher);
+ if (!mime)
+ return;
+
+ try {
+ return Services.prefs.getCharPref(this._getPrefName(mime));
+ } catch(ex) {
+ Services.console.logStringMessage("Error getting pref for " + mime + ".");
+ }
+ return null;
+ },
+
+ _setPreferredApp: function setPreferredApp(launcher, app) {
+ let mime = this._getMimeTypeFromLauncher(launcher);
+ if (!mime)
+ return;
+
+ if (app)
+ Services.prefs.setCharPref(this._getPrefName(mime), app.packageName);
+ else
+ Services.prefs.clearUserPref(this._getPrefName(mime));
+ },
+
+ promptForSaveToFileAsync: function (aLauncher, aContext, aDefaultFile,
+ aSuggestedFileExt, aForcePrompt) {
+ Task.spawn(function* () {
+ let file = null;
+ try {
+ let hasPermission = yield RuntimePermissions.waitForPermissions(RuntimePermissions.WRITE_EXTERNAL_STORAGE);
+ if (hasPermission) {
+ // If we do have the STORAGE permission then pick the public downloads directory as destination
+ // for this file. Without the permission saveDestinationAvailable(null) will be called which
+ // will effectively cancel the download.
+ let preferredDir = yield Downloads.getPreferredDownloadsDirectory();
+ file = this.validateLeafName(new FileUtils.File(preferredDir),
+ aDefaultFile, aSuggestedFileExt);
+ }
+ } finally {
+ // The file argument will be null in case any exception occurred.
+ aLauncher.saveDestinationAvailable(file);
+ }
+ }.bind(this)).catch(Cu.reportError);
+ },
+
+ validateLeafName: function hald_validateLeafName(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 hald_makeFileUnique(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, 0o600);
+ }
+ catch (e) {
+ dump("*** exception in validateLeafName: " + 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, 0o600);
+ }
+ }
+ },
+
+ isUsableDirectory: function hald_isUsableDirectory(aDirectory) {
+ return aDirectory.exists() && aDirectory.isDirectory() && aDirectory.isWritable();
+ },
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([HelperAppLauncherDialog]);