Bug 1191067 - Add Fennec version of about:accounts. r=antlam,margaret

This is a Fennec version of about:accounts, cribbed largely from
Desktop's implementation.  This implementation serves two purposes:

One, it allows all fxa-content-server pref handling to remain in
Gecko.  Java-side consumers redirect to about:accounts?action=... and
have pref munging and parameter addition (like context=fx_fennec_v1,
etc) handled by about:accounts itself.

Two, it handles network connectivity display and error handling.  When
a request is started, we display an animated spinner.  We transition
smoothly from the spinner to the iframe display if we can, and if not
we hide any network error and offer to retry.  This is more important
in Fennec than it is on Desktop.  This approach agrees with Firefox
for iOS.

Some additional notes:

The spinner to iframe transition uses the WebChannel listener to send
LOADED messages to the appropriate XUL <browser> element.  It's worth
remembering that Fennec's Gecko is single process, so the <browser> in
question is in the same process.  None-the-less, we are close to e10s
safe.

There are four actions: signup/signin/force_reauth, and manage.  The
first three try to produce a LOGIN message.  The last uses the
fxa-content-server to manage the Account settings.  *This is not how
this is arranged on Desktop: Desktop redirects to a new tab, not
wrapped in about:accounts.*

--HG--
extra : commitid : F2waTwe355B
extra : rebase_source : f63c96f676d1300c774d091968ec8d88bb7a86dc
This commit is contained in:
Nick Alexander 2015-09-18 16:28:14 -04:00
Родитель 811d17425f
Коммит cdb092fe66
11 изменённых файлов: 517 добавлений и 1 удалений

Просмотреть файл

@ -945,3 +945,15 @@ pref("browser.tabs.showAudioPlayingIcon", true);
pref("dom.serviceWorkers.enabled", true);
pref("dom.serviceWorkers.interception.enabled", true);
#endif
// The remote content URL where FxAccountsWebChannel messages originate. Must use HTTPS.
pref("identity.fxaccounts.remote.webchannel.uri", "https://accounts.firefox.com");
// The remote URL of the Firefox Account profile server.
pref("identity.fxaccounts.remote.profile.uri", "https://profile.accounts.firefox.com/v1");
// The remote URL of the Firefox Account oauth server.
pref("identity.fxaccounts.remote.oauth.uri", "https://oauth.accounts.firefox.com/v1");
// Token server used by Firefox Account-authenticated Sync.
pref("identity.sync.tokenserver.uri", "https://token.services.mozilla.com/1.0/sync/1.5");

Просмотреть файл

@ -12,6 +12,7 @@ import org.mozilla.gecko.util.StringUtils;
public class AboutPages {
// All of our special pages.
public static final String ACCOUNTS = "about:accounts";
public static final String ADDONS = "about:addons";
public static final String CONFIG = "about:config";
public static final String DOWNLOADS = "about:downloads";
@ -72,6 +73,7 @@ public class AboutPages {
}
private static final String[] DEFAULT_ICON_PAGES = new String[] {
ACCOUNTS,
ADDONS,
CONFIG,
DOWNLOADS,

Просмотреть файл

@ -0,0 +1,311 @@
// -*- 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/. */
/**
* Wrap a remote fxa-content-server.
*
* An about:accounts tab loads and displays an fxa-content-server page,
* depending on the current Android Account status and an optional 'action'
* parameter.
*
* We show a spinner while the remote iframe is loading. We expect the
* WebChannel message listening to the fxa-content-server to send this tab's
* <browser>'s messageManager a LOADED message when the remote iframe provides
* the WebChannel LOADED message. See the messageManager registration and the
* |loadedDeferred| promise. This loosely couples the WebChannel implementation
* and about:accounts! (We need this coupling in order to distinguish
* WebChannel LOADED messages produced by multiple about:accounts tabs.)
*
* We capture error conditions by accessing the inner nsIWebNavigation of the
* iframe directly.
*/
"use strict";
const {classes: Cc, interfaces: Ci, utils: Cu} = Components; /*global Components */
Cu.import("resource://gre/modules/Accounts.jsm"); /*global Accounts */
Cu.import("resource://gre/modules/PromiseUtils.jsm"); /*global PromiseUtils */
Cu.import("resource://gre/modules/Services.jsm"); /*global Services */
Cu.import("resource://gre/modules/XPCOMUtils.jsm"); /*global XPCOMUtils */
const ACTION_URL_PARAM = "action";
const COMMAND_LOADED = "fxaccounts:loaded";
const log = Cu.import("resource://gre/modules/AndroidLog.jsm", {}).AndroidLog.bind("FxAccounts");
// Shows the toplevel element with |id| to be shown - all other top-level
// elements are hidden.
// If |id| is 'spinner', then 'remote' is also shown, with opacity 0.
function show(id) {
let allTop = document.querySelectorAll(".toplevel");
for (let elt of allTop) {
if (elt.getAttribute("id") == id) {
elt.style.display = 'block';
} else {
elt.style.display = 'none';
}
}
if (id == 'spinner') {
document.getElementById('remote').style.display = 'block';
document.getElementById('remote').style.opacity = 0;
}
}
var loadedDeferred = null;
// We have a new load starting. Replace the existing promise with a new one,
// and queue up the transition to remote content.
function deferTransitionToRemoteAfterLoaded() {
log.d('Waiting for LOADED message.');
loadedDeferred = PromiseUtils.defer();
loadedDeferred.promise.then(() => {
document.getElementById("remote").style.opacity = 0;
show("remote");
document.getElementById("remote").style.opacity = 1;
});
}
function handleLoadedMessage(message) {
log.d('Got LOADED message!');
loadedDeferred.resolve();
};
let wrapper = {
iframe: null,
url: null,
init: function (url) {
deferTransitionToRemoteAfterLoaded();
let iframe = document.getElementById("remote");
this.iframe = iframe;
this.iframe.QueryInterface(Ci.nsIFrameLoaderOwner);
let docShell = this.iframe.frameLoader.docShell;
docShell.QueryInterface(Ci.nsIWebProgress);
docShell.addProgressListener(this.iframeListener, Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT);
this.url = url;
// Set the iframe's location with loadURI/LOAD_FLAGS_BYPASS_HISTORY to
// avoid having a new history entry being added.
let webNav = iframe.frameLoader.docShell.QueryInterface(Ci.nsIWebNavigation);
webNav.loadURI(url, Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_HISTORY, null, null, null);
},
retry: function () {
deferTransitionToRemoteAfterLoaded();
let webNav = this.iframe.frameLoader.docShell.QueryInterface(Ci.nsIWebNavigation);
webNav.loadURI(this.url, Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_HISTORY, null, null, null);
},
iframeListener: {
QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener,
Ci.nsISupportsWeakReference,
Ci.nsISupports]),
onStateChange: function(aWebProgress, aRequest, aState, aStatus) {
let failure = false;
// Captive portals sometimes redirect users
if ((aState & Ci.nsIWebProgressListener.STATE_REDIRECTING)) {
failure = true;
} else if ((aState & Ci.nsIWebProgressListener.STATE_STOP)) {
if (aRequest instanceof Ci.nsIHttpChannel) {
try {
failure = aRequest.responseStatus != 200;
} catch (e) {
failure = aStatus != Components.results.NS_OK;
}
}
}
// Calling cancel() will raise some OnStateChange notifications by itself,
// so avoid doing that more than once
if (failure && aStatus != Components.results.NS_BINDING_ABORTED) {
aRequest.cancel(Components.results.NS_BINDING_ABORTED);
show("networkError");
}
},
onLocationChange: function(aWebProgress, aRequest, aLocation, aFlags) {
if (aRequest && aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_ERROR_PAGE) {
aRequest.cancel(Components.results.NS_BINDING_ABORTED);
show("networkError");
}
},
onProgressChange: function() {},
onStatusChange: function() {},
onSecurityChange: function() {},
},
};
function retry() {
log.i("Retrying.");
show("spinner");
wrapper.retry();
}
function openPrefs() {
log.i("Opening Sync preferences.");
// If an Android Account exists, this will open the Status Activity.
// Otherwise, it will begin the Get Started flow. This should only be shown
// when an Account actually exists.
Accounts.launchSetup();
}
function getURLForAction(action, urlParams) {
let url = Services.urlFormatter.formatURLPref("identity.fxaccounts.remote.webchannel.uri");
url = url + (url.endsWith("/") ? "" : "/") + action;
const CONTEXT = "fx_fennec_v1";
// The only service managed by Fennec, to date, is Firefox Sync.
const SERVICE = "sync";
urlParams = urlParams || new URLSearchParams("");
urlParams.set('service', SERVICE);
urlParams.set('context', CONTEXT);
// Ideally we'd just merge urlParams with new URL(url).searchParams, but our
// URLSearchParams implementation doesn't support iteration (bug 1085284).
let urlParamStr = urlParams.toString();
if (urlParamStr) {
url += (url.includes("?") ? "&" : "?") + urlParamStr;
}
return url;
}
function updateDisplayedEmail(user) {
let emailDiv = document.getElementById("email");
if (emailDiv && user) {
emailDiv.textContent = user.email;
}
}
function init() {
Accounts.getFirefoxAccount().then(user => {
// It's possible for the window to start closing before getting the user
// completes. Tests in particular can cause this.
if (window.closed) {
return;
}
updateDisplayedEmail(user);
// Ideally we'd use new URL(document.URL).searchParams, but for about: URIs,
// searchParams is empty.
let urlParams = new URLSearchParams(document.URL.split("?")[1] || "");
let action = urlParams.get(ACTION_URL_PARAM);
urlParams.delete(ACTION_URL_PARAM);
switch (action) {
case "signup":
if (user) {
// Asking to sign-up when already signed in just shows prefs.
show("prefs");
} else {
show("spinner");
wrapper.init(getURLForAction("signup", urlParams));
}
break;
case "signin":
if (user) {
// Asking to sign-in when already signed in just shows prefs.
show("prefs");
} else {
show("spinner");
wrapper.init(getURLForAction("signin", urlParams));
}
break;
case "force_auth":
if (user) {
show("spinner");
urlParams.set("email", user.email); // In future, pin using the UID.
wrapper.init(getURLForAction("force_auth", urlParams));
} else {
show("spinner");
wrapper.init(getURLForAction("signup", urlParams));
}
break;
case "manage":
if (user) {
show("spinner");
urlParams.set("email", user.email); // In future, pin using the UID.
wrapper.init(getURLForAction("settings", urlParams));
} else {
show("spinner");
wrapper.init(getURLForAction("signup", urlParams));
}
break;
default:
// Unrecognized or no action specified.
if (action) {
log.w("Ignoring unrecognized action: " + action);
}
if (user) {
show("prefs");
} else {
show("spinner");
wrapper.init(getURLForAction("signup", urlParams));
}
break;
}
}).catch(e => {
log.e("Failed to get the signed in user: " + e.toString());
});
}
document.addEventListener("DOMContentLoaded", function onload() {
document.removeEventListener("DOMContentLoaded", onload, true);
init();
var buttonRetry = document.getElementById('buttonRetry');
buttonRetry.addEventListener('click', retry);
var buttonOpenPrefs = document.getElementById('buttonOpenPrefs');
buttonOpenPrefs.addEventListener('click', openPrefs);
}, true);
// This window is contained in a XUL <browser> element. Return the
// messageManager of that <browser> element, or null.
function getBrowserMessageManager() {
let browser = window
.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebNavigation)
.QueryInterface(Ci.nsIDocShellTreeItem)
.rootTreeItem
.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindow)
.QueryInterface(Ci.nsIDOMChromeWindow)
.BrowserApp
.getBrowserForDocument(document);
if (browser) {
return browser.messageManager;
}
return null;
}
// Add a single listener for 'loaded' messages from the iframe in this
// <browser>. These 'loaded' messages are ferried from the WebChannel to just
// this <browser>.
let mm = getBrowserMessageManager();
if (mm) {
mm.addMessageListener(COMMAND_LOADED, handleLoadedMessage);
} else {
log.e('No messageManager, not listening for LOADED message!');
}
window.addEventListener("unload", function(event) {
try {
let mm = getBrowserMessageManager();
if (mm) {
mm.removeMessageListener(COMMAND_LOADED, handleLoadedMessage);
}
} catch (e) {
// This could fail if the page is being torn down, the tab is being
// destroyed, etc.
log.w('Not removing listener for LOADED message: ' + e.toString());
}
});

Просмотреть файл

@ -0,0 +1,74 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- 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/. -->
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd" [
<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd" >
%brandDTD;
<!ENTITY % globalDTD SYSTEM "chrome://global/locale/global.dtd" >
%globalDTD;
<!ENTITY % aboutDTD SYSTEM "chrome://browser/locale/aboutAccounts.dtd">
%aboutDTD;
]>
<html xmlns="http://www.w3.org/1999/xhtml" dir="&locale.dir;">
<head>
<title>Firefox Sync</title>
<meta name="viewport" content="width=device-width; user-scalable=0" />
<link rel="icon" type="image/png" sizes="64x64" href="chrome://branding/content/favicon64.png" />
<link rel="stylesheet" href="chrome://browser/skin/spinner.css" type="text/css"/>
<link rel="stylesheet" href="chrome://browser/skin/aboutBase.css" type="text/css"/>
<link rel="stylesheet" href="chrome://browser/skin/aboutAccounts.css" type="text/css"/>
</head>
<body>
<div id="spinner" class="toplevel">
<div class="container flex-column">
<!-- Empty text-container for spacing. -->
<div class="text-container flex-column" />
<div class="mui-refresh-main">
<div class="mui-refresh-wrapper">
<div class="mui-spinner-wrapper">
<div class="mui-spinner-main">
<div class="mui-spinner-left">
<div class="mui-half-circle-left" />
</div>
<div class="mui-spinner-right">
<div class="mui-half-circle-right" />
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<iframe mozframetype="content" id="remote" class="toplevel" />
<div id="prefs" class="toplevel">
<div class="container flex-column">
<div class="text-container flex-column">
<div class="text">&aboutAccounts.connected.title;</div>
<div class="hint">&aboutAccounts.connected.description;</div>
<div id="email" class="hint"></div>
</div>
<a id="buttonOpenPrefs" tabindex="0" href="#">&aboutAccounts.syncPreferences.label;</a>
</div>
</div>
<div id="networkError" class="toplevel">
<div class="container flex-column">
<div class="text-container flex-column">
<div class="text">&aboutAccounts.noConnection.title;</div>
</div>
<div class="button-row">
<button id="buttonRetry" class="button" tabindex="1">&aboutAccounts.retry.label;</button>
</div>
</div>
</div>
<script type="application/javascript;version=1.8" src="chrome://browser/content/aboutAccounts.js"></script>
</body>
</html>

Просмотреть файл

@ -59,6 +59,10 @@ chrome.jar:
#ifdef MOZ_DEVICES
content/aboutDevices.xhtml (content/aboutDevices.xhtml)
content/aboutDevices.js (content/aboutDevices.js)
#endif
#ifndef MOZ_ANDROID_NATIVE_ACCOUNT_UI
content/aboutAccounts.xhtml (content/aboutAccounts.xhtml)
content/aboutAccounts.js (content/aboutAccounts.js)
#endif
content/aboutLogins.xhtml (content/aboutLogins.xhtml)
content/aboutLogins.js (content/aboutLogins.js)

Просмотреть файл

@ -89,6 +89,12 @@ if (AppConstants.MOZ_DEVICES) {
privileged: true
};
}
if (!AppConstants.MOZ_ANDROID_NATIVE_ACCOUNT_UI) {
modules['accounts'] = {
uri: "chrome://browser/content/aboutAccounts.xhtml",
privileged: true
};
}
function AboutRedirector() {}
AboutRedirector.prototype = {

Просмотреть файл

@ -20,7 +20,9 @@ contract @mozilla.org/network/protocol/about;1?what=blocked {322ba47e-7047-4f71-
#ifdef MOZ_DEVICES
contract @mozilla.org/network/protocol/about;1?what=devices {322ba47e-7047-4f71-aebf-cb7d69325cd9}
#endif
#ifndef MOZ_ANDROID_NATIVE_ACCOUNT_UI
contract @mozilla.org/network/protocol/about;1?what=accounts {322ba47e-7047-4f71-aebf-cb7d69325cd9}
#endif
contract @mozilla.org/network/protocol/about;1?what=logins {322ba47e-7047-4f71-aebf-cb7d69325cd9}
# DirectoryProvider.js

Просмотреть файл

@ -0,0 +1,10 @@
<!-- 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/. -->
<!ENTITY aboutAccounts.connected.title "Firefox Accounts">
<!ENTITY aboutAccounts.connected.description "You are connected as">
<!ENTITY aboutAccounts.syncPreferences.label "Tap here to check Sync settings">
<!ENTITY aboutAccounts.noConnection.title "No Internet connection">
<!ENTITY aboutAccounts.retry.label "Try again">

Просмотреть файл

@ -8,6 +8,7 @@
% locale browser @AB_CD@ %locale/@AB_CD@/browser/
locale/@AB_CD@/browser/about.dtd (%chrome/about.dtd)
#ifndef MOZ_ANDROID_NATIVE_ACCOUNT_UI
locale/@AB_CD@/browser/aboutAccounts.dtd (%chrome/aboutAccounts.dtd)
locale/@AB_CD@/browser/aboutAccounts.properties (%chrome/aboutAccounts.properties)
#endif
locale/@AB_CD@/browser/aboutAddons.dtd (%chrome/aboutAddons.dtd)

Просмотреть файл

@ -0,0 +1,91 @@
/* 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/. */
html, body {
height: 100%;
width: 100%;
}
div {
transition: opacity 0.4s ease-in;
}
#spinner {
transition: opacity 0.2s ease-in;
}
#remote {
border: 0;
opacity: 0;
transition: opacity 0.4s ease-in;
}
.text {
color: #363B40;
font-size: 25px;
font-weight: lighter;
margin-bottom: 20px;
}
.hint {
color: #777777;
font-size: 20px;
margin-bottom: 20px;
}
a {
color: #0096DD; /* link_blue */
text-decoration: none;
font-size: 20px;
margin-bottom: 20px;
}
a:active {
color: #0082C6; /* link_blue_pressed */
}
.toplevel {
width: 100%;
height: 100%;
position: absolute;
}
.container {
height: 100%;
padding-left: 30px;
padding-right: 30px;
}
.text-container {
padding-top: 60px;
padding-left: 30px;
padding-right: 30px;
}
.flex-column {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
}
.button-row {
flex: 0;
width: 100%;
display: flex;
flex-direction: row;
align-items: center;
text-align: center;
justify-content: center;
}
.button {
flex: 1;
height: 60px;
background-color: #E66000; /*matched to action_orange in java codebase*/
color: #FFFFFF;
font-size: 20px;
border-radius: 4px;
border-width: 0px;
}

Просмотреть файл

@ -8,6 +8,9 @@ chrome.jar:
% skin browser classic/1.0 %skin/
skin/aboutPage.css (aboutPage.css)
skin/about.css (about.css)
#ifndef MOZ_ANDROID_NATIVE_ACCOUNT_UI
skin/aboutAccounts.css (aboutAccounts.css)
#endif
* skin/aboutAddons.css (aboutAddons.css)
* skin/aboutBase.css (aboutBase.css)
#ifdef MOZ_DEVICES