Bug 780379 - Show a custom intent chooser for downloads that can be downloaded by an external app. r=mfinkle

--HG--
rename : mobile/android/chrome/content/HelperApps.js => mobile/android/modules/HelperApps.jsm
This commit is contained in:
Wes Johnston 2013-10-17 16:30:47 -07:00
parent 8aa06f6035
commit 0becd4b76a
7 changed files with 245 additions and 209 deletions

View File

@ -1,196 +0,0 @@
/* 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/. */
"use strict";
XPCOMUtils.defineLazyGetter(this, "ContentAreaUtils", function() {
let ContentAreaUtils = {};
Services.scriptloader.loadSubScript("chrome://global/content/contentAreaUtils.js", ContentAreaUtils);
return ContentAreaUtils;
});
function getBridge() {
return Cc["@mozilla.org/android/bridge;1"].getService(Ci.nsIAndroidBridge);
}
function sendMessageToJava(aMessage) {
return getBridge().handleGeckoMessage(JSON.stringify(aMessage));
}
var HelperApps = {
get defaultHttpHandlers() {
let protoHandlers = this.getAppsForProtocol("http");
var results = {};
for (var i = 0; i < protoHandlers.length; i++) {
try {
let protoApp = protoHandlers.queryElementAt(i, Ci.nsIHandlerApp);
results[protoApp.name] = protoApp;
} catch(e) {}
}
delete this.defaultHttpHandlers;
return this.defaultHttpHandlers = results;
},
get protoSvc() {
delete this.protoSvc;
return this.protoSvc = Cc["@mozilla.org/uriloader/external-protocol-service;1"].getService(Ci.nsIExternalProtocolService);
},
get urlHandlerService() {
delete this.urlHandlerService;
return this.urlHandlerService = Cc["@mozilla.org/uriloader/external-url-handler-service;1"].getService(Ci.nsIExternalURLHandlerService);
},
getAppsForProtocol: function getAppsForProtocol(uri) {
let handlerInfoProto = this.protoSvc.getProtocolHandlerInfoFromOS(uri, {});
return handlerInfoProto.possibleApplicationHandlers;
},
getAppsForUri: function getAppsFor(uri) {
let found = [];
let mimeType = ContentAreaUtils.getMIMETypeForURI(uri) || "";
// empty action string defaults to android.intent.action.VIEW
let msg = {
type: "Intent:GetHandlers",
mime: mimeType,
action: "",
url: uri.spec,
packageName: "",
className: ""
};
let data = sendMessageToJava(msg);
if (!data)
return found;
let apps = this._parseApps(JSON.parse(data));
for (let i = 0; i < apps.length; i++) {
let appName = apps[i].name;
if (appName.length > 0 && !this.defaultHttpHandlers[appName])
found.push(apps[i]);
}
return found;
},
updatePageAction: function setPageAction(uri) {
let apps = this.getAppsForUri(uri);
if (apps.length > 0)
this._setPageActionFor(uri, apps);
else
this._removePageAction();
},
_setPageActionFor: function setPageActionFor(uri, apps) {
this._pageActionUri = uri;
// If the pageaction is already added, simply update the URI to be launched when 'onclick' is triggered.
if (this._pageActionId != undefined)
return;
this._pageActionId = NativeWindow.pageactions.add({
title: Strings.browser.GetStringFromName("openInApp.pageAction"),
icon: "drawable://icon_openinapp",
clickCallback: function() {
if (apps.length == 1)
this._launchApp(apps[0], this._pageActionUri);
else
this.openUriInApp(this._pageActionUri);
}.bind(this)
});
},
_removePageAction: function removePageAction() {
if(!this._pageActionId)
return;
NativeWindow.pageactions.remove(this._pageActionId);
delete this._pageActionId;
},
_launchApp: function launchApp(appData, uri) {
let mimeType = ContentAreaUtils.getMIMETypeForURI(uri) || "";
let msg = {
type: "Intent:Open",
mime: mimeType,
action: "android.intent.action.VIEW",
url: uri.spec,
packageName: appData.packageName,
className: appData.activityName
};
sendMessageToJava(msg);
},
openUriInApp: function openUriInApp(uri) {
let mimeType = ContentAreaUtils.getMIMETypeForURI(uri) || "";
let msg = {
type: "Intent:Open",
mime: mimeType,
action: "",
url: uri.spec,
packageName: "",
className: ""
};
sendMessageToJava(msg);
},
_parseApps: function _parseApps(aJSON) {
// aJSON -> {apps: [app1Label, app1Default, app1PackageName, app1ActivityName, app2Label, app2Defaut, ...]}
// see GeckoAppShell.java getHandlersForIntent function for details
let appInfo = aJSON.apps;
const numAttr = 4; // 4 elements per ResolveInfo: label, default, package name, activity name.
let apps = [];
for (let i = 0; i < appInfo.length; i += numAttr) {
apps.push({"name" : appInfo[i],
"isDefault" : appInfo[i+1],
"packageName" : appInfo[i+2],
"activityName" : appInfo[i+3]});
}
return apps;
},
showDoorhanger: function showDoorhanger(aUri, aCallback) {
let permValue = Services.perms.testPermission(aUri, "native-intent");
if (permValue != Services.perms.UNKNOWN_ACTION) {
if (permValue == Services.perms.ALLOW_ACTION) {
if (aCallback)
aCallback(aUri);
else
this.openUriInApp(aUri);
} else if (permValue == Services.perms.DENY_ACTION) {
// do nothing
}
return;
}
let apps = this.getAppsForUri(aUri);
let strings = Strings.browser;
let message = "";
if (apps.length == 1)
message = strings.formatStringFromName("helperapps.openWithApp2", [apps[0].name], 1);
else
message = strings.GetStringFromName("helperapps.openWithList2");
let buttons = [{
label: strings.GetStringFromName("helperapps.open"),
callback: function(aChecked) {
if (aChecked)
Services.perms.add(aUri, "native-intent", Ci.nsIPermissionManager.ALLOW_ACTION);
if (aCallback)
aCallback(aUri);
else
this.openUriInApp(aUri);
}
}, {
label: strings.GetStringFromName("helperapps.ignore"),
callback: function(aChecked) {
if (aChecked)
Services.perms.add(aUri, "native-intent", Ci.nsIPermissionManager.DENY_ACTION);
}
}];
let options = { checkbox: Strings.browser.GetStringFromName("helperapps.dontAskAgain") };
NativeWindow.doorhanger.show(message, "helperapps-open", buttons, BrowserApp.selectedTab.id, options);
}
};

View File

@ -53,6 +53,9 @@ XPCOMUtils.defineLazyModuleGetter(this, "Sanitizer",
XPCOMUtils.defineLazyModuleGetter(this, "Prompt",
"resource://gre/modules/Prompt.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "HelperApps",
"resource://gre/modules/HelperApps.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "FormHistory",
"resource://gre/modules/FormHistory.jsm");
@ -62,7 +65,6 @@ XPCOMUtils.defineLazyServiceGetter(this, "uuidgen",
// Lazily-loaded browser scripts:
[
["HelperApps", "chrome://browser/content/HelperApps.js"],
["SelectHelper", "chrome://browser/content/SelectHelper.js"],
["InputWidgetHelper", "chrome://browser/content/InputWidgetHelper.js"],
["AboutReader", "chrome://browser/content/aboutReader.js"],
@ -2873,7 +2875,7 @@ Tab.prototype = {
this.browser.focus();
this.browser.docShellIsActive = true;
Reader.updatePageAction(this);
HelperApps.updatePageAction(this.browser.currentURI);
ExternalApps.updatePageAction(this.browser.currentURI);
} else {
this.browser.setAttribute("type", "content-targetable");
this.browser.docShellIsActive = false;
@ -3583,7 +3585,7 @@ Tab.prototype = {
// Show page actions for helper apps.
if (BrowserApp.selectedTab == this)
HelperApps.updatePageAction(this.browser.currentURI);
ExternalApps.updatePageAction(this.browser.currentURI);
if (!Reader.isEnabledForParseOnLoad)
return;
@ -7755,7 +7757,60 @@ var ExternalApps = {
openExternal: function(aElement) {
let uri = ExternalApps._getMediaLink(aElement);
HelperApps.openUriInApp(uri);
}
},
updatePageAction: function updatePageAction(uri) {
let apps = HelperApps.getAppsForUri(uri);
if (apps.length > 0)
this._setUriForPageAction(uri, apps);
else
this._removePageAction();
},
_setUriForPageAction: function setUriForPageAction(uri, apps) {
this._pageActionUri = uri;
// If the pageaction is already added, simply update the URI to be launched when 'onclick' is triggered.
if (this._pageActionId != undefined)
return;
this._pageActionId = NativeWindow.pageactions.add({
title: Strings.browser.GetStringFromName("openInApp.pageAction"),
icon: "drawable://icon_openinapp",
clickCallback: (function() {
let callback = function(app) {
app.launch(uri);
}
if (apps.length > 1) {
// Use the HelperApps prompt here to filter out any Http handlers
HelperApps.prompt(apps, {
title: Strings.browser.GetStringFromName("openInApp.pageAction"),
buttons: [
Strings.browser.GetStringFromName("openInApp.ok"),
Strings.browser.GetStringFromName("openInApp.cancel")
]
}, function(result) {
if (result.button != 0)
return;
callback(apps[result.icongrid0]);
});
} else {
callback(apps[0]);
}
}).bind(this)
});
},
_removePageAction: function removePageAction() {
if(!this._pageActionId)
return;
NativeWindow.pageactions.remove(this._pageActionId);
delete this._pageActionId;
},
};
var Distribution = {

View File

@ -36,7 +36,6 @@ chrome.jar:
content/netError.xhtml (content/netError.xhtml)
content/SelectHelper.js (content/SelectHelper.js)
content/SelectionHandler.js (content/SelectionHandler.js)
content/HelperApps.js (content/HelperApps.js)
content/dbg-browser-actors.js (content/dbg-browser-actors.js)
content/WebAppRT.js (content/WebAppRT.js)
content/InputWidgetHelper.js (content/InputWidgetHelper.js)

View File

@ -1,17 +1,17 @@
// -*- 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 Cc = Components.classes;
const Ci = Components.interfaces;
const Cu = Components.utils;
const Cr = Components.results;
const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
const PREF_BD_USEDOWNLOADDIR = "browser.download.useDownloadDir";
const URI_GENERIC_ICON_DOWNLOAD = "drawable://alert_download";
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/Prompt.jsm");
Cu.import("resource://gre/modules/HelperApps.jsm");
// -----------------------------------------------------------------------
// HelperApp Launcher Dialog
@ -24,9 +24,42 @@ HelperAppLauncherDialog.prototype = {
QueryInterface: XPCOMUtils.generateQI([Ci.nsIHelperAppLauncherDialog]),
show: function hald_show(aLauncher, aContext, aReason) {
// Save everything by default
aLauncher.MIMEInfo.preferredAction = Ci.nsIMIMEInfo.useSystemDefault;
aLauncher.saveToDisk(null, false);
let bundle = Services.strings.createBundle("chrome://browser/locale/browser.properties");
let defaultHandler = new Object();
let apps = HelperApps.getAppsForUri(aLauncher.source, {
mimeType: aLauncher.MIMEInfo.MIMEType,
});
// Add a fake intent for save to disk at the top of the list
apps.unshift({
name: bundle.GetStringFromName("helperapps.saveToDisk"),
iconUri: "drawable://icon",
launch: function() {
// Reset the preferredAction here
aLauncher.MIMEInfo.preferredAction = Ci.nsIMIMEInfo.saveToDisk;
aLauncher.saveToDisk(null, false);
return true;
}
});
let app = apps[0];
if (apps.length > 1) {
app = HelperApps.prompt(apps, {
title: bundle.GetStringFromName("helperapps.pick")
});
}
if (app) {
aLauncher.MIMEInfo.preferredAction = Ci.nsIMIMEInfo.useHelperApp;
if (!app.launch(aLauncher.source)) {
aLauncher.cancel();
}
} else {
// Something weird happened. Log an error
Services.console.logStringMessage("Unexpected selection from grid: " + app);
}
},
promptForSaveToFile: function hald_promptForSaveToFile(aLauncher, aContext, aDefaultFile, aSuggestedFileExt, aForcePrompt) {
@ -63,7 +96,7 @@ HelperAppLauncherDialog.prototype = {
// 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.
var collisionCount = 0;
let collisionCount = 0;
while (aLocalFile.exists()) {
collisionCount++;
if (collisionCount == 1) {

View File

@ -263,6 +263,8 @@ helperapps.openWithApp2=Open With %S App
helperapps.openWithList2=Open With an App
helperapps.always=Always
helperapps.never=Never
helperapps.pick=Complete action using
helperapps.saveToDisk=Download
#Lightweight themes
# LOCALIZATION NOTE (lwthemeInstallRequest.message): %S will be replaced with
@ -294,3 +296,6 @@ readerMode.exit = Exit Reader Mode
#Open in App
openInApp.pageAction = Open in App
openInApp.ok = OK
openInApp.cancel = Cancel

View File

@ -0,0 +1,139 @@
/* 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/. */
"use strict";
const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/Prompt.jsm");
XPCOMUtils.defineLazyGetter(this, "ContentAreaUtils", function() {
let ContentAreaUtils = {};
Services.scriptloader.loadSubScript("chrome://global/content/contentAreaUtils.js", ContentAreaUtils);
return ContentAreaUtils;
});
this.EXPORTED_SYMBOLS = ["App","HelperApps"];
function App(data) {
this.name = data.name;
this.isDefault = data.isDefault;
this.packageName = data.packageName;
this.activityName = data.activityName;
this.iconUri = "-moz-icon://" + data.packageName;
}
App.prototype = {
launch: function(uri) {
HelperApps._launchApp(this, uri);
return false;
}
}
var HelperApps = {
get defaultHttpHandlers() {
delete this.defaultHttpHandlers;
return this.defaultHttpHandlers = this.getAppsForProtocol("http");
},
get protoSvc() {
delete this.protoSvc;
return this.protoSvc = Cc["@mozilla.org/uriloader/external-protocol-service;1"].getService(Ci.nsIExternalProtocolService);
},
get urlHandlerService() {
delete this.urlHandlerService;
return this.urlHandlerService = Cc["@mozilla.org/uriloader/external-url-handler-service;1"].getService(Ci.nsIExternalURLHandlerService);
},
prompt: function showPicker(apps, promptOptions, callback) {
let p = new Prompt(promptOptions).addIconGrid({ items: apps });
p.show(callback);
},
getAppsForProtocol: function getAppsForProtocol(scheme) {
let protoHandlers = this.protoSvc.getProtocolHandlerInfoFromOS(scheme, {}).possibleApplicationHandlers;
let results = {};
for (let i = 0; i < protoHandlers.length; i++) {
try {
let protoApp = protoHandlers.queryElementAt(i, Ci.nsIHandlerApp);
results[protoApp.name] = new App({
name: protoApp.name,
description: protoApp.detailedDescription,
});
} catch(e) {}
}
return results;
},
getAppsForUri: function getAppsForUri(uri, flags = { filterHttp: true }) {
flags.filterHttp = "filterHttp" in flags ? flags.filterHttp : true;
// Query for apps that can/can't handle the mimetype
let msg = this._getMessage("Intent:GetHandlers", uri, flags);
let apps = this._parseApps(this._sendMessage(msg).apps);
if (flags.filterHttp) {
apps = apps.filter(function(app) {
return app.name && !this.defaultHttpHandlers[app.name];
}, this);
}
return apps;
},
launchUri: function launchUri(uri) {
let msg = this._getMessage("Intent:Open", uri);
this._sendMessage(msg);
},
_parseApps: function _parseApps(appInfo) {
// appInfo -> {apps: [app1Label, app1Default, app1PackageName, app1ActivityName, app2Label, app2Defaut, ...]}
// see GeckoAppShell.java getHandlersForIntent function for details
const numAttr = 4; // 4 elements per ResolveInfo: label, default, package name, activity name.
let apps = [];
for (let i = 0; i < appInfo.length; i += numAttr) {
apps.push(new App({"name" : appInfo[i],
"isDefault" : appInfo[i+1],
"packageName" : appInfo[i+2],
"activityName" : appInfo[i+3]}));
}
return apps;
},
_getMessage: function(type, uri, options = {}) {
let mimeType = options.mimeType;
if (uri && mimeType == undefined)
mimeType = ContentAreaUtils.getMIMETypeForURI(uri) || "";
return {
type: type,
mime: mimeType,
action: options.action || "", // empty action string defaults to android.intent.action.VIEW
url: uri ? uri.spec : "",
packageName: options.packageName || "",
className: options.className || ""
};
},
_launchApp: function launchApp(app, uri) {
let msg = this._getMessage("Intent:Open", uri, {
packageName: app.packageName,
className: app.activityName
});
this._sendMessage(msg);
},
_sendMessage: function(msg) {
Services.console.logStringMessage("Sending: " + JSON.stringify(msg));
let res = Services.androidBridge.handleGeckoMessage(JSON.stringify(msg));
return JSON.parse(res);
},
};

View File

@ -6,6 +6,7 @@
EXTRA_JS_MODULES += [
'ContactService.jsm',
'HelperApps.jsm',
'Home.jsm',
'JNI.jsm',
'LightweightThemeConsumer.jsm',