diff options
Diffstat (limited to 'mobile/android/components/HelperAppDialog.js')
-rw-r--r-- | mobile/android/components/HelperAppDialog.js | 373 |
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]); |