1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/mobile/android/components/HelperAppDialog.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,303 @@ 1.4 +// -*- Mode: js2; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*- 1.5 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.6 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.7 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.8 + 1.9 +const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components; 1.10 + 1.11 +const APK_MIME_TYPE = "application/vnd.android.package-archive"; 1.12 +const PREF_BD_USEDOWNLOADDIR = "browser.download.useDownloadDir"; 1.13 +const URI_GENERIC_ICON_DOWNLOAD = "drawable://alert_download"; 1.14 + 1.15 +Cu.import("resource://gre/modules/FileUtils.jsm"); 1.16 +Cu.import("resource://gre/modules/HelperApps.jsm"); 1.17 +Cu.import("resource://gre/modules/Prompt.jsm"); 1.18 +Cu.import("resource://gre/modules/Services.jsm"); 1.19 +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); 1.20 + 1.21 +// ----------------------------------------------------------------------- 1.22 +// HelperApp Launcher Dialog 1.23 +// ----------------------------------------------------------------------- 1.24 + 1.25 +function HelperAppLauncherDialog() { } 1.26 + 1.27 +HelperAppLauncherDialog.prototype = { 1.28 + classID: Components.ID("{e9d277a0-268a-4ec2-bb8c-10fdf3e44611}"), 1.29 + QueryInterface: XPCOMUtils.generateQI([Ci.nsIHelperAppLauncherDialog]), 1.30 + 1.31 + getNativeWindow: function () { 1.32 + try { 1.33 + let win = Services.wm.getMostRecentWindow("navigator:browser"); 1.34 + if (win && win.NativeWindow) { 1.35 + return win.NativeWindow; 1.36 + } 1.37 + } catch (e) { 1.38 + } 1.39 + return null; 1.40 + }, 1.41 + 1.42 + /** 1.43 + * Returns false if `url` represents a local or special URL that we don't 1.44 + * wish to ever download. 1.45 + * 1.46 + * Returns true otherwise. 1.47 + */ 1.48 + _canDownload: function (url, alreadyResolved=false) { 1.49 + // The common case. 1.50 + if (url.schemeIs("http") || 1.51 + url.schemeIs("https") || 1.52 + url.schemeIs("ftp")) { 1.53 + return true; 1.54 + } 1.55 + 1.56 + // The less-common opposite case. 1.57 + if (url.schemeIs("chrome") || 1.58 + url.schemeIs("jar") || 1.59 + url.schemeIs("resource") || 1.60 + url.schemeIs("wyciwyg")) { 1.61 + return false; 1.62 + } 1.63 + 1.64 + // For all other URIs, try to resolve them to an inner URI, and check that. 1.65 + if (!alreadyResolved) { 1.66 + let ioSvc = Cc["@mozilla.org/network/io-service;1"].getService(Components.interfaces.nsIIOService); 1.67 + let innerURI = ioSvc.newChannelFromURI(url).URI; 1.68 + if (!url.equals(innerURI)) { 1.69 + return this._canDownload(innerURI, true); 1.70 + } 1.71 + } 1.72 + 1.73 + if (url.schemeIs("file")) { 1.74 + // If it's in our app directory or profile directory, we never ever 1.75 + // want to do anything with it, including saving to disk or passing the 1.76 + // file to another application. 1.77 + let file = url.QueryInterface(Ci.nsIFileURL).file; 1.78 + 1.79 + // Normalize the nsILocalFile in-place. This will ensure that paths 1.80 + // can be correctly compared via `contains`, below. 1.81 + file.normalize(); 1.82 + 1.83 + // TODO: pref blacklist? 1.84 + 1.85 + let appRoot = FileUtils.getFile("XREExeF", []); 1.86 + if (appRoot.contains(file, true)) { 1.87 + return false; 1.88 + } 1.89 + 1.90 + let profileRoot = FileUtils.getFile("ProfD", []); 1.91 + if (profileRoot.contains(file, true)) { 1.92 + return false; 1.93 + } 1.94 + 1.95 + return true; 1.96 + } 1.97 + 1.98 + // Anything else is fine to download. 1.99 + return true; 1.100 + }, 1.101 + 1.102 + /** 1.103 + * Returns true if `launcher` represents a download for which we wish 1.104 + * to prompt. 1.105 + */ 1.106 + _shouldPrompt: function (launcher) { 1.107 + let mimeType = this._getMimeTypeFromLauncher(launcher); 1.108 + 1.109 + // Straight equality: nsIMIMEInfo normalizes. 1.110 + return APK_MIME_TYPE == mimeType; 1.111 + }, 1.112 + 1.113 + show: function hald_show(aLauncher, aContext, aReason) { 1.114 + if (!this._canDownload(aLauncher.source)) { 1.115 + aLauncher.cancel(Cr.NS_BINDING_ABORTED); 1.116 + 1.117 + let win = this.getNativeWindow(); 1.118 + if (!win) { 1.119 + // Oops. 1.120 + Services.console.logStringMessage("Refusing download, but can't show a toast."); 1.121 + return; 1.122 + } 1.123 + 1.124 + Services.console.logStringMessage("Refusing download of non-downloadable file."); 1.125 + let bundle = Services.strings.createBundle("chrome://browser/locale/handling.properties"); 1.126 + let failedText = bundle.GetStringFromName("protocol.failed"); 1.127 + win.toast.show(failedText, "long"); 1.128 + 1.129 + return; 1.130 + } 1.131 + 1.132 + let bundle = Services.strings.createBundle("chrome://browser/locale/browser.properties"); 1.133 + 1.134 + let defaultHandler = new Object(); 1.135 + let apps = HelperApps.getAppsForUri(aLauncher.source, { 1.136 + mimeType: aLauncher.MIMEInfo.MIMEType, 1.137 + }); 1.138 + 1.139 + // Add a fake intent for save to disk at the top of the list. 1.140 + apps.unshift({ 1.141 + name: bundle.GetStringFromName("helperapps.saveToDisk"), 1.142 + packageName: "org.mozilla.gecko.Download", 1.143 + iconUri: "drawable://icon", 1.144 + launch: function() { 1.145 + // Reset the preferredAction here. 1.146 + aLauncher.MIMEInfo.preferredAction = Ci.nsIMIMEInfo.saveToDisk; 1.147 + aLauncher.saveToDisk(null, false); 1.148 + return true; 1.149 + } 1.150 + }); 1.151 + 1.152 + // See if the user already marked something as the default for this mimetype, 1.153 + // and if that app is still installed. 1.154 + let preferredApp = this._getPreferredApp(aLauncher); 1.155 + if (preferredApp) { 1.156 + let pref = apps.filter(function(app) { 1.157 + return app.packageName === preferredApp; 1.158 + }); 1.159 + 1.160 + if (pref.length > 0) { 1.161 + pref[0].launch(aLauncher.source); 1.162 + return; 1.163 + } 1.164 + } 1.165 + 1.166 + let callback = function(app) { 1.167 + aLauncher.MIMEInfo.preferredAction = Ci.nsIMIMEInfo.useHelperApp; 1.168 + if (!app.launch(aLauncher.source)) { 1.169 + aLauncher.cancel(Cr.NS_BINDING_ABORTED); 1.170 + } 1.171 + } 1.172 + 1.173 + // If there's only one choice, and we don't want to prompt, go right ahead 1.174 + // and choose that app automatically. 1.175 + if (!this._shouldPrompt(aLauncher) && (apps.length === 1)) { 1.176 + callback(apps[0]); 1.177 + return; 1.178 + } 1.179 + 1.180 + // Otherwise, let's go through the prompt. 1.181 + HelperApps.prompt(apps, { 1.182 + title: bundle.GetStringFromName("helperapps.pick"), 1.183 + buttons: [ 1.184 + bundle.GetStringFromName("helperapps.alwaysUse"), 1.185 + bundle.GetStringFromName("helperapps.useJustOnce") 1.186 + ] 1.187 + }, (data) => { 1.188 + if (data.button < 0) { 1.189 + return; 1.190 + } 1.191 + 1.192 + callback(apps[data.icongrid0]); 1.193 + 1.194 + if (data.button === 0) { 1.195 + this._setPreferredApp(aLauncher, apps[data.icongrid0]); 1.196 + } 1.197 + }); 1.198 + }, 1.199 + 1.200 + _getPrefName: function getPrefName(mimetype) { 1.201 + return "browser.download.preferred." + mimetype.replace("\\", "."); 1.202 + }, 1.203 + 1.204 + _getMimeTypeFromLauncher: function (launcher) { 1.205 + let mime = launcher.MIMEInfo.MIMEType; 1.206 + if (!mime) 1.207 + mime = ContentAreaUtils.getMIMETypeForURI(launcher.source) || ""; 1.208 + return mime; 1.209 + }, 1.210 + 1.211 + _getPreferredApp: function getPreferredApp(launcher) { 1.212 + let mime = this._getMimeTypeFromLauncher(launcher); 1.213 + if (!mime) 1.214 + return; 1.215 + 1.216 + try { 1.217 + return Services.prefs.getCharPref(this._getPrefName(mime)); 1.218 + } catch(ex) { 1.219 + Services.console.logStringMessage("Error getting pref for " + mime + "."); 1.220 + } 1.221 + return null; 1.222 + }, 1.223 + 1.224 + _setPreferredApp: function setPreferredApp(launcher, app) { 1.225 + let mime = this._getMimeTypeFromLauncher(launcher); 1.226 + if (!mime) 1.227 + return; 1.228 + 1.229 + if (app) 1.230 + Services.prefs.setCharPref(this._getPrefName(mime), app.packageName); 1.231 + else 1.232 + Services.prefs.clearUserPref(this._getPrefName(mime)); 1.233 + }, 1.234 + 1.235 + promptForSaveToFile: function hald_promptForSaveToFile(aLauncher, aContext, aDefaultFile, aSuggestedFileExt, aForcePrompt) { 1.236 + // Retrieve the user's default download directory 1.237 + let dnldMgr = Cc["@mozilla.org/download-manager;1"].getService(Ci.nsIDownloadManager); 1.238 + let defaultFolder = dnldMgr.userDownloadsDirectory; 1.239 + 1.240 + try { 1.241 + file = this.validateLeafName(defaultFolder, aDefaultFile, aSuggestedFileExt); 1.242 + } catch (e) { } 1.243 + 1.244 + return file; 1.245 + }, 1.246 + 1.247 + validateLeafName: function hald_validateLeafName(aLocalFile, aLeafName, aFileExt) { 1.248 + if (!(aLocalFile && this.isUsableDirectory(aLocalFile))) 1.249 + return null; 1.250 + 1.251 + // Remove any leading periods, since we don't want to save hidden files 1.252 + // automatically. 1.253 + aLeafName = aLeafName.replace(/^\.+/, ""); 1.254 + 1.255 + if (aLeafName == "") 1.256 + aLeafName = "unnamed" + (aFileExt ? "." + aFileExt : ""); 1.257 + aLocalFile.append(aLeafName); 1.258 + 1.259 + this.makeFileUnique(aLocalFile); 1.260 + return aLocalFile; 1.261 + }, 1.262 + 1.263 + makeFileUnique: function hald_makeFileUnique(aLocalFile) { 1.264 + try { 1.265 + // Note - this code is identical to that in 1.266 + // toolkit/content/contentAreaUtils.js. 1.267 + // If you are updating this code, update that code too! We can't share code 1.268 + // here since this is called in a js component. 1.269 + let collisionCount = 0; 1.270 + while (aLocalFile.exists()) { 1.271 + collisionCount++; 1.272 + if (collisionCount == 1) { 1.273 + // Append "(2)" before the last dot in (or at the end of) the filename 1.274 + // special case .ext.gz etc files so we don't wind up with .tar(2).gz 1.275 + if (aLocalFile.leafName.match(/\.[^\.]{1,3}\.(gz|bz2|Z)$/i)) 1.276 + aLocalFile.leafName = aLocalFile.leafName.replace(/\.[^\.]{1,3}\.(gz|bz2|Z)$/i, "(2)$&"); 1.277 + else 1.278 + aLocalFile.leafName = aLocalFile.leafName.replace(/(\.[^\.]*)?$/, "(2)$&"); 1.279 + } 1.280 + else { 1.281 + // replace the last (n) in the filename with (n+1) 1.282 + aLocalFile.leafName = aLocalFile.leafName.replace(/^(.*\()\d+\)/, "$1" + (collisionCount+1) + ")"); 1.283 + } 1.284 + } 1.285 + aLocalFile.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0600); 1.286 + } 1.287 + catch (e) { 1.288 + dump("*** exception in validateLeafName: " + e + "\n"); 1.289 + 1.290 + if (e.result == Cr.NS_ERROR_FILE_ACCESS_DENIED) 1.291 + throw e; 1.292 + 1.293 + if (aLocalFile.leafName == "" || aLocalFile.isDirectory()) { 1.294 + aLocalFile.append("unnamed"); 1.295 + if (aLocalFile.exists()) 1.296 + aLocalFile.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0600); 1.297 + } 1.298 + } 1.299 + }, 1.300 + 1.301 + isUsableDirectory: function hald_isUsableDirectory(aDirectory) { 1.302 + return aDirectory.exists() && aDirectory.isDirectory() && aDirectory.isWritable(); 1.303 + }, 1.304 +}; 1.305 + 1.306 +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([HelperAppLauncherDialog]);