Merge fx-team to m-c.
|
@ -56,8 +56,10 @@ let gFxAccounts = {
|
|||
if (!service.ready) {
|
||||
return false;
|
||||
}
|
||||
return Weave.Service.identity.readyToAuthenticate &&
|
||||
Weave.Status.login != Weave.LOGIN_SUCCEEDED;
|
||||
// LOGIN_FAILED_LOGIN_REJECTED explicitly means "you must log back in".
|
||||
// All other login failures are assumed to be transient and should go
|
||||
// away by themselves, so aren't reflected here.
|
||||
return Weave.Status.login == Weave.LOGIN_FAILED_LOGIN_REJECTED;
|
||||
},
|
||||
|
||||
get isActiveWindow() {
|
||||
|
|
|
@ -469,6 +469,11 @@
|
|||
accesskey="&syncSyncNowItem.accesskey;"
|
||||
observes="sync-syncnow-state"
|
||||
oncommand="gSyncUI.doSync(event);"/>
|
||||
<menuitem id="sync-reauthitem"
|
||||
label="&syncReAuthItem.label;"
|
||||
accesskey="&syncReAuthItem.accesskey;"
|
||||
observes="sync-reauth-state"
|
||||
oncommand="gSyncUI.openSignInAgainPage();"/>
|
||||
#endif
|
||||
<menuseparator id="devToolsSeparator"/>
|
||||
<menu id="webDeveloperMenu"
|
||||
|
|
|
@ -177,6 +177,7 @@
|
|||
#ifdef MOZ_SERVICES_SYNC
|
||||
<broadcaster id="sync-setup-state"/>
|
||||
<broadcaster id="sync-syncnow-state"/>
|
||||
<broadcaster id="sync-reauth-state"/>
|
||||
#endif
|
||||
<broadcaster id="workOfflineMenuitemState"/>
|
||||
<broadcaster id="socialSidebarBroadcaster" hidden="true"/>
|
||||
|
|
|
@ -96,10 +96,24 @@ let gSyncUI = {
|
|||
firstSync == "notReady";
|
||||
},
|
||||
|
||||
_loginFailed: function () {
|
||||
// Referencing Weave.Service will implicitly initialize sync, and we don't
|
||||
// want to force that - so first check if it is ready.
|
||||
let service = Cc["@mozilla.org/weave/service;1"]
|
||||
.getService(Components.interfaces.nsISupports)
|
||||
.wrappedJSObject;
|
||||
if (!service.ready) {
|
||||
return false;
|
||||
}
|
||||
return Weave.Status.login == Weave.LOGIN_FAILED_LOGIN_REJECTED;
|
||||
},
|
||||
|
||||
updateUI: function SUI_updateUI() {
|
||||
let needsSetup = this._needsSetup();
|
||||
document.getElementById("sync-setup-state").hidden = !needsSetup;
|
||||
document.getElementById("sync-syncnow-state").hidden = needsSetup;
|
||||
let loginFailed = this._loginFailed();
|
||||
document.getElementById("sync-setup-state").hidden = loginFailed || !needsSetup;
|
||||
document.getElementById("sync-syncnow-state").hidden = loginFailed || needsSetup;
|
||||
document.getElementById("sync-reauth-state").hidden = !loginFailed;
|
||||
|
||||
if (!gBrowser)
|
||||
return;
|
||||
|
@ -338,6 +352,9 @@ let gSyncUI = {
|
|||
openPreferences("paneSync");
|
||||
},
|
||||
|
||||
openSignInAgainPage: function () {
|
||||
switchToTabHavingURI("about:accounts?action=reauth", true);
|
||||
},
|
||||
|
||||
// Helpers
|
||||
_updateLastSyncTime: function SUI__updateLastSyncTime() {
|
||||
|
|
|
@ -22,7 +22,7 @@ function observer(aSubject, aTopic, aData) {
|
|||
++gObservedTopics[aTopic];
|
||||
}
|
||||
|
||||
function promiseNotification(aTopic, aAction) {
|
||||
function promiseObserverCalled(aTopic, aAction) {
|
||||
let deferred = Promise.defer();
|
||||
|
||||
Services.obs.addObserver(function observer() {
|
||||
|
@ -45,13 +45,13 @@ function promiseNotification(aTopic, aAction) {
|
|||
return deferred.promise;
|
||||
}
|
||||
|
||||
function expectNotification(aTopic) {
|
||||
function expectObserverCalled(aTopic) {
|
||||
is(gObservedTopics[aTopic], 1, "expected notification " + aTopic);
|
||||
if (aTopic in gObservedTopics)
|
||||
--gObservedTopics[aTopic];
|
||||
}
|
||||
|
||||
function expectNoNotifications() {
|
||||
function expectNoObserverCalled() {
|
||||
for (let topic in gObservedTopics) {
|
||||
if (gObservedTopics[topic])
|
||||
is(gObservedTopics[topic], 0, topic + " notification unexpected");
|
||||
|
@ -77,35 +77,33 @@ function promiseMessage(aMessage, aAction) {
|
|||
return deferred.promise;
|
||||
}
|
||||
|
||||
function promisePopupNotificationShown(aName, aAction) {
|
||||
let deferred = Promise.defer();
|
||||
|
||||
function promisePopupNotification(aName, aShown) {
|
||||
PopupNotifications.panel.addEventListener("popupshown", function popupNotifShown() {
|
||||
PopupNotifications.panel.removeEventListener("popupshown", popupNotifShown);
|
||||
|
||||
ok(!!PopupNotifications.getNotification(aName), aName + " notification shown");
|
||||
ok(!!PopupNotifications.panel.firstChild, "notification panel populated");
|
||||
|
||||
deferred.resolve();
|
||||
});
|
||||
|
||||
if (aAction)
|
||||
aAction();
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
function promisePopupNotification(aName) {
|
||||
let deferred = Promise.defer();
|
||||
|
||||
waitForCondition(() => PopupNotifications.getNotification(aName),
|
||||
() => {
|
||||
let notification = PopupNotifications.getNotification(aName);
|
||||
ok(!!notification, aName + " notification appeared");
|
||||
ok(!!PopupNotifications.getNotification(aName),
|
||||
aName + " notification appeared");
|
||||
|
||||
if (!notification || !aShown) {
|
||||
deferred.resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
// If aShown is true, the notification is expected to be opened by
|
||||
// default, so we check that the panel has been populated.
|
||||
if (PopupNotifications.panel.firstChild) {
|
||||
ok(true, "notification panel populated");
|
||||
deferred.resolve();
|
||||
}
|
||||
else {
|
||||
todo(false, "We shouldn't have to force re-open the panel, see bug 976544");
|
||||
notification.reshow();
|
||||
waitForCondition(() => PopupNotifications.panel.firstChild,
|
||||
() => {
|
||||
ok(PopupNotifications.panel.firstChild, "notification panel populated");
|
||||
deferred.resolve();
|
||||
}, "timeout waiting for notification to be reshown");
|
||||
}
|
||||
}, "timeout waiting for popup notification " + aName);
|
||||
|
||||
return deferred.promise;
|
||||
|
@ -170,17 +168,17 @@ function getMediaCaptureState() {
|
|||
}
|
||||
|
||||
function closeStream(aAlreadyClosed) {
|
||||
expectNoNotifications();
|
||||
expectNoObserverCalled();
|
||||
|
||||
info("closing the stream");
|
||||
content.wrappedJSObject.closeStream();
|
||||
|
||||
if (!aAlreadyClosed)
|
||||
yield promiseNotification("recording-device-events");
|
||||
yield promiseObserverCalled("recording-device-events");
|
||||
|
||||
yield promiseNoPopupNotification("webRTC-sharingDevices");
|
||||
if (!aAlreadyClosed)
|
||||
expectNotification("recording-window-ended");
|
||||
expectObserverCalled("recording-window-ended");
|
||||
|
||||
let statusButton = document.getElementById("webrtc-status-button");
|
||||
ok(statusButton.hidden, "WebRTC status button hidden");
|
||||
|
@ -221,12 +219,12 @@ let gTests = [
|
|||
{
|
||||
desc: "getUserMedia audio+video",
|
||||
run: function checkAudioVideo() {
|
||||
yield promiseNotification("getUserMedia:request", () => {
|
||||
yield promisePopupNotificationShown("webRTC-shareDevices", () => {
|
||||
info("requesting devices");
|
||||
content.wrappedJSObject.requestDevice(true, true);
|
||||
});
|
||||
expectObserverCalled("getUserMedia:request");
|
||||
|
||||
yield promisePopupNotification("webRTC-shareDevices", true);
|
||||
is(PopupNotifications.getNotification("webRTC-shareDevices").anchorID,
|
||||
"webRTC-shareDevices-notification-icon", "anchored to device icon");
|
||||
checkDeviceSelectors(true, true);
|
||||
|
@ -236,8 +234,8 @@ let gTests = [
|
|||
yield promiseMessage("ok", () => {
|
||||
PopupNotifications.panel.firstChild.button.click();
|
||||
});
|
||||
expectNotification("getUserMedia:response:allow");
|
||||
expectNotification("recording-device-events");
|
||||
expectObserverCalled("getUserMedia:response:allow");
|
||||
expectObserverCalled("recording-device-events");
|
||||
is(getMediaCaptureState(), "CameraAndMicrophone",
|
||||
"expected camera and microphone to be shared");
|
||||
|
||||
|
@ -249,12 +247,12 @@ let gTests = [
|
|||
{
|
||||
desc: "getUserMedia audio only",
|
||||
run: function checkAudioOnly() {
|
||||
yield promiseNotification("getUserMedia:request", () => {
|
||||
yield promisePopupNotificationShown("webRTC-shareDevices", () => {
|
||||
info("requesting devices");
|
||||
content.wrappedJSObject.requestDevice(true);
|
||||
});
|
||||
expectObserverCalled("getUserMedia:request");
|
||||
|
||||
yield promisePopupNotification("webRTC-shareDevices", true);
|
||||
is(PopupNotifications.getNotification("webRTC-shareDevices").anchorID,
|
||||
"webRTC-shareMicrophone-notification-icon", "anchored to mic icon");
|
||||
checkDeviceSelectors(true);
|
||||
|
@ -264,8 +262,8 @@ let gTests = [
|
|||
yield promiseMessage("ok", () => {
|
||||
PopupNotifications.panel.firstChild.button.click();
|
||||
});
|
||||
expectNotification("getUserMedia:response:allow");
|
||||
expectNotification("recording-device-events");
|
||||
expectObserverCalled("getUserMedia:response:allow");
|
||||
expectObserverCalled("recording-device-events");
|
||||
is(getMediaCaptureState(), "Microphone", "expected microphone to be shared");
|
||||
|
||||
yield checkSharingUI();
|
||||
|
@ -276,12 +274,12 @@ let gTests = [
|
|||
{
|
||||
desc: "getUserMedia video only",
|
||||
run: function checkVideoOnly() {
|
||||
yield promiseNotification("getUserMedia:request", () => {
|
||||
yield promisePopupNotificationShown("webRTC-shareDevices", () => {
|
||||
info("requesting devices");
|
||||
content.wrappedJSObject.requestDevice(false, true);
|
||||
});
|
||||
expectObserverCalled("getUserMedia:request");
|
||||
|
||||
yield promisePopupNotification("webRTC-shareDevices", true);
|
||||
is(PopupNotifications.getNotification("webRTC-shareDevices").anchorID,
|
||||
"webRTC-shareDevices-notification-icon", "anchored to device icon");
|
||||
checkDeviceSelectors(false, true);
|
||||
|
@ -291,8 +289,8 @@ let gTests = [
|
|||
yield promiseMessage("ok", () => {
|
||||
PopupNotifications.panel.firstChild.button.click();
|
||||
});
|
||||
expectNotification("getUserMedia:response:allow");
|
||||
expectNotification("recording-device-events");
|
||||
expectObserverCalled("getUserMedia:response:allow");
|
||||
expectObserverCalled("recording-device-events");
|
||||
is(getMediaCaptureState(), "Camera", "expected camera to be shared");
|
||||
|
||||
yield checkSharingUI();
|
||||
|
@ -303,12 +301,11 @@ let gTests = [
|
|||
{
|
||||
desc: "getUserMedia audio+video, user disables video",
|
||||
run: function checkDisableVideo() {
|
||||
yield promiseNotification("getUserMedia:request", () => {
|
||||
yield promisePopupNotificationShown("webRTC-shareDevices", () => {
|
||||
info("requesting devices");
|
||||
content.wrappedJSObject.requestDevice(true, true);
|
||||
});
|
||||
|
||||
yield promisePopupNotification("webRTC-shareDevices", true);
|
||||
expectObserverCalled("getUserMedia:request");
|
||||
checkDeviceSelectors(true, true);
|
||||
|
||||
// disable the camera
|
||||
|
@ -321,8 +318,8 @@ let gTests = [
|
|||
// reset the menuitem to have no impact on the following tests.
|
||||
document.getElementById("webRTC-selectCamera-menulist").value = 0;
|
||||
|
||||
expectNotification("getUserMedia:response:allow");
|
||||
expectNotification("recording-device-events");
|
||||
expectObserverCalled("getUserMedia:response:allow");
|
||||
expectObserverCalled("recording-device-events");
|
||||
is(getMediaCaptureState(), "Microphone",
|
||||
"expected microphone to be shared");
|
||||
|
||||
|
@ -334,12 +331,11 @@ let gTests = [
|
|||
{
|
||||
desc: "getUserMedia audio+video, user disables audio",
|
||||
run: function checkDisableAudio() {
|
||||
yield promiseNotification("getUserMedia:request", () => {
|
||||
yield promisePopupNotificationShown("webRTC-shareDevices", () => {
|
||||
info("requesting devices");
|
||||
content.wrappedJSObject.requestDevice(true, true);
|
||||
});
|
||||
|
||||
yield promisePopupNotification("webRTC-shareDevices", true);
|
||||
expectObserverCalled("getUserMedia:request");
|
||||
checkDeviceSelectors(true, true);
|
||||
|
||||
// disable the microphone
|
||||
|
@ -352,8 +348,8 @@ let gTests = [
|
|||
// reset the menuitem to have no impact on the following tests.
|
||||
document.getElementById("webRTC-selectMicrophone-menulist").value = 0;
|
||||
|
||||
expectNotification("getUserMedia:response:allow");
|
||||
expectNotification("recording-device-events");
|
||||
expectObserverCalled("getUserMedia:response:allow");
|
||||
expectObserverCalled("recording-device-events");
|
||||
is(getMediaCaptureState(), "Camera",
|
||||
"expected microphone to be shared");
|
||||
|
||||
|
@ -365,12 +361,11 @@ let gTests = [
|
|||
{
|
||||
desc: "getUserMedia audio+video, user disables both audio and video",
|
||||
run: function checkDisableAudioVideo() {
|
||||
yield promiseNotification("getUserMedia:request", () => {
|
||||
yield promisePopupNotificationShown("webRTC-shareDevices", () => {
|
||||
info("requesting devices");
|
||||
content.wrappedJSObject.requestDevice(true, true);
|
||||
});
|
||||
|
||||
yield promisePopupNotification("webRTC-shareDevices", true);
|
||||
expectObserverCalled("getUserMedia:request");
|
||||
checkDeviceSelectors(true, true);
|
||||
|
||||
// disable the camera and microphone
|
||||
|
@ -385,8 +380,8 @@ let gTests = [
|
|||
document.getElementById("webRTC-selectCamera-menulist").value = 0;
|
||||
document.getElementById("webRTC-selectMicrophone-menulist").value = 0;
|
||||
|
||||
expectNotification("getUserMedia:response:deny");
|
||||
expectNotification("recording-window-ended");
|
||||
expectObserverCalled("getUserMedia:response:deny");
|
||||
expectObserverCalled("recording-window-ended");
|
||||
checkNotSharing();
|
||||
}
|
||||
},
|
||||
|
@ -394,20 +389,19 @@ let gTests = [
|
|||
{
|
||||
desc: "getUserMedia audio+video, user clicks \"Don't Share\"",
|
||||
run: function checkDontShare() {
|
||||
yield promiseNotification("getUserMedia:request", () => {
|
||||
yield promisePopupNotificationShown("webRTC-shareDevices", () => {
|
||||
info("requesting devices");
|
||||
content.wrappedJSObject.requestDevice(true, true);
|
||||
});
|
||||
|
||||
yield promisePopupNotification("webRTC-shareDevices", true);
|
||||
expectObserverCalled("getUserMedia:request");
|
||||
checkDeviceSelectors(true, true);
|
||||
|
||||
yield promiseMessage("error: PERMISSION_DENIED", () => {
|
||||
activateSecondaryAction(kActionDeny);
|
||||
});
|
||||
|
||||
expectNotification("getUserMedia:response:deny");
|
||||
expectNotification("recording-window-ended");
|
||||
expectObserverCalled("getUserMedia:response:deny");
|
||||
expectObserverCalled("recording-window-ended");
|
||||
checkNotSharing();
|
||||
}
|
||||
},
|
||||
|
@ -415,19 +409,18 @@ let gTests = [
|
|||
{
|
||||
desc: "getUserMedia audio+video: stop sharing",
|
||||
run: function checkStopSharing() {
|
||||
yield promiseNotification("getUserMedia:request", () => {
|
||||
yield promisePopupNotificationShown("webRTC-shareDevices", () => {
|
||||
info("requesting devices");
|
||||
content.wrappedJSObject.requestDevice(true, true);
|
||||
});
|
||||
|
||||
yield promisePopupNotification("webRTC-shareDevices", true);
|
||||
expectObserverCalled("getUserMedia:request");
|
||||
checkDeviceSelectors(true, true);
|
||||
|
||||
yield promiseMessage("ok", () => {
|
||||
PopupNotifications.panel.firstChild.button.click();
|
||||
});
|
||||
expectNotification("getUserMedia:response:allow");
|
||||
expectNotification("recording-device-events");
|
||||
expectObserverCalled("getUserMedia:response:allow");
|
||||
expectObserverCalled("recording-device-events");
|
||||
is(getMediaCaptureState(), "CameraAndMicrophone",
|
||||
"expected camera and microphone to be shared");
|
||||
|
||||
|
@ -436,8 +429,8 @@ let gTests = [
|
|||
PopupNotifications.getNotification("webRTC-sharingDevices").reshow();
|
||||
activateSecondaryAction(kActionDeny);
|
||||
|
||||
yield promiseNotification("recording-device-events");
|
||||
expectNotification("getUserMedia:revoke");
|
||||
yield promiseObserverCalled("recording-device-events");
|
||||
expectObserverCalled("getUserMedia:revoke");
|
||||
|
||||
yield promiseNoPopupNotification("webRTC-sharingDevices");
|
||||
|
||||
|
@ -446,7 +439,7 @@ let gTests = [
|
|||
gObservedTopics["recording-device-events"] = 0;
|
||||
}
|
||||
|
||||
expectNoNotifications();
|
||||
expectNoObserverCalled();
|
||||
checkNotSharing();
|
||||
|
||||
// the stream is already closed, but this will do some cleanup anyway
|
||||
|
@ -461,11 +454,10 @@ let gTests = [
|
|||
|
||||
function checkPerm(aRequestAudio, aRequestVideo, aAllowAudio, aAllowVideo,
|
||||
aExpectedAudioPerm, aExpectedVideoPerm, aNever) {
|
||||
yield promiseNotification("getUserMedia:request", () => {
|
||||
yield promisePopupNotificationShown("webRTC-shareDevices", () => {
|
||||
content.wrappedJSObject.requestDevice(aRequestAudio, aRequestVideo);
|
||||
});
|
||||
|
||||
yield promisePopupNotification("webRTC-shareDevices", true);
|
||||
expectObserverCalled("getUserMedia:request");
|
||||
|
||||
let noAudio = aAllowAudio === undefined;
|
||||
is(elt("webRTC-selectMicrophone").hidden, noAudio,
|
||||
|
@ -486,8 +478,8 @@ let gTests = [
|
|||
});
|
||||
let expected = [];
|
||||
if (expectedMessage == "ok") {
|
||||
expectNotification("getUserMedia:response:allow");
|
||||
expectNotification("recording-device-events");
|
||||
expectObserverCalled("getUserMedia:response:allow");
|
||||
expectObserverCalled("recording-device-events");
|
||||
if (aAllowVideo)
|
||||
expected.push("Camera");
|
||||
if (aAllowAudio)
|
||||
|
@ -495,8 +487,8 @@ let gTests = [
|
|||
expected = expected.join("And");
|
||||
}
|
||||
else {
|
||||
expectNotification("getUserMedia:response:deny");
|
||||
expectNotification("recording-window-ended");
|
||||
expectObserverCalled("getUserMedia:response:deny");
|
||||
expectObserverCalled("recording-window-ended");
|
||||
expected = "none";
|
||||
}
|
||||
is(getMediaCaptureState(), expected,
|
||||
|
@ -581,15 +573,15 @@ let gTests = [
|
|||
|
||||
if (aExpectStream === undefined) {
|
||||
// Check that we get a prompt.
|
||||
yield promiseNotification("getUserMedia:request", gum);
|
||||
yield promisePopupNotification("webRTC-shareDevices", true);
|
||||
yield promisePopupNotificationShown("webRTC-shareDevices", gum);
|
||||
expectObserverCalled("getUserMedia:request");
|
||||
|
||||
// Deny the request to cleanup...
|
||||
yield promiseMessage("error: PERMISSION_DENIED", () => {
|
||||
activateSecondaryAction(kActionDeny);
|
||||
});
|
||||
expectNotification("getUserMedia:response:deny");
|
||||
expectNotification("recording-window-ended");
|
||||
expectObserverCalled("getUserMedia:response:deny");
|
||||
expectObserverCalled("recording-window-ended");
|
||||
}
|
||||
else {
|
||||
let allow = (aAllowVideo && aRequestVideo) || (aAllowAudio && aRequestAudio);
|
||||
|
@ -597,7 +589,7 @@ let gTests = [
|
|||
yield promiseMessage(expectedMessage, gum);
|
||||
|
||||
if (expectedMessage == "ok") {
|
||||
expectNotification("recording-device-events");
|
||||
expectObserverCalled("recording-device-events");
|
||||
|
||||
// Check what's actually shared.
|
||||
let expected = [];
|
||||
|
@ -612,7 +604,7 @@ let gTests = [
|
|||
yield closeStream();
|
||||
}
|
||||
else {
|
||||
expectNotification("recording-window-ended");
|
||||
expectObserverCalled("recording-window-ended");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -691,7 +683,7 @@ let gTests = [
|
|||
yield promiseMessage("ok", () => {
|
||||
content.wrappedJSObject.requestDevice(aRequestAudio, aRequestVideo);
|
||||
});
|
||||
expectNotification("recording-device-events");
|
||||
expectObserverCalled("recording-device-events");
|
||||
yield checkSharingUI();
|
||||
|
||||
PopupNotifications.getNotification("webRTC-sharingDevices").reshow();
|
||||
|
@ -706,8 +698,8 @@ let gTests = [
|
|||
// Stop sharing.
|
||||
activateSecondaryAction(kActionDeny);
|
||||
|
||||
yield promiseNotification("recording-device-events");
|
||||
expectNotification("getUserMedia:revoke");
|
||||
yield promiseObserverCalled("recording-device-events");
|
||||
expectObserverCalled("getUserMedia:revoke");
|
||||
|
||||
yield promiseNoPopupNotification("webRTC-sharingDevices");
|
||||
|
||||
|
@ -766,7 +758,7 @@ function test() {
|
|||
yield test.run();
|
||||
|
||||
// Cleanup before the next test
|
||||
expectNoNotifications();
|
||||
expectNoObserverCalled();
|
||||
}
|
||||
}).then(finish, ex => {
|
||||
ok(false, "Unexpected Exception: " + ex);
|
||||
|
|
|
@ -273,6 +273,8 @@
|
|||
}
|
||||
break;
|
||||
case "overflow":
|
||||
switch (aEvent.target.localName) {
|
||||
case "vbox":
|
||||
// Resize the right view on the next tick.
|
||||
if (this.showingSubView) {
|
||||
setTimeout(this._syncContainerWithSubView.bind(this), 0);
|
||||
|
@ -280,6 +282,11 @@
|
|||
setTimeout(this._syncContainerWithMainView.bind(this), 0);
|
||||
}
|
||||
break;
|
||||
case "toolbarbutton":
|
||||
aEvent.target.setAttribute("fadelabel", "true");
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case "popupshowing":
|
||||
this.setAttribute("panelopen", "true");
|
||||
// Bug 941196 - The panel can get taller when opening a subview. Disabling
|
||||
|
|
|
@ -38,7 +38,8 @@
|
|||
<stack flex="1">
|
||||
<richlistbox id="downloadsRichListBox"/>
|
||||
<description id="downloadsListEmptyDescription"
|
||||
value="&downloadsListEmpty.label;"/>
|
||||
value="&downloadsListEmpty.label;"
|
||||
mousethrough="always"/>
|
||||
</stack>
|
||||
<commandset id="downloadCommands"/>
|
||||
<menupopup id="downloadsContextMenu"/>
|
||||
|
|
|
@ -134,11 +134,14 @@ let gSyncPane = {
|
|||
// So we think we are logged in, so login problems are next.
|
||||
// (Although if the Sync identity manager is still initializing, we
|
||||
// ignore login errors and assume all will eventually be good.)
|
||||
} else if (Weave.Service.identity.readyToAuthenticate &&
|
||||
Weave.Status.login != Weave.LOGIN_SUCCEEDED) {
|
||||
// LOGIN_FAILED_LOGIN_REJECTED explicitly means "you must log back in".
|
||||
// All other login failures are assumed to be transient and should go
|
||||
// away by themselves, so aren't reflected here.
|
||||
} else if (Weave.Status.login == Weave.LOGIN_FAILED_LOGIN_REJECTED) {
|
||||
fxaLoginStatus.selectedIndex = FXA_LOGIN_FAILED;
|
||||
enginesListDisabled = true;
|
||||
// Else we must be golden!
|
||||
// Else we must be golden (or in an error state we expect to magically
|
||||
// resolve itself)
|
||||
} else {
|
||||
fxaLoginStatus.selectedIndex = FXA_LOGIN_VERIFIED;
|
||||
enginesListDisabled = false;
|
||||
|
|
|
@ -660,6 +660,8 @@ just addresses the organization to follow, e.g. "This site is run by " -->
|
|||
<!ENTITY syncSetup.accesskey "Y">
|
||||
<!ENTITY syncSyncNowItem.label "Sync Now">
|
||||
<!ENTITY syncSyncNowItem.accesskey "S">
|
||||
<!ENTITY syncReAuthItem.label "Reconnect to &syncBrand.shortName.label;…">
|
||||
<!ENTITY syncReAuthItem.accesskey "R">
|
||||
<!ENTITY syncToolbarButton.label "Sync">
|
||||
|
||||
<!ENTITY socialToolbar.title "Social Toolbar Button">
|
||||
|
|
|
@ -55,6 +55,7 @@ this.UITour = {
|
|||
availableTargetsCache: new WeakMap(),
|
||||
|
||||
_detachingTab: false,
|
||||
_annotationPanelMutationObservers: new WeakMap(),
|
||||
_queuedEvents: [],
|
||||
_pendingDoc: null,
|
||||
|
||||
|
@ -324,7 +325,12 @@ this.UITour = {
|
|||
}
|
||||
}
|
||||
|
||||
this.showInfo(contentDocument, target, data.title, data.text, iconURL, buttons);
|
||||
let infoOptions = {};
|
||||
|
||||
if (typeof data.closeButtonCallbackID == "string")
|
||||
infoOptions.closeButtonCallbackID = data.closeButtonCallbackID;
|
||||
|
||||
this.showInfo(contentDocument, target, data.title, data.text, iconURL, buttons, infoOptions);
|
||||
}).then(null, Cu.reportError);
|
||||
break;
|
||||
}
|
||||
|
@ -507,14 +513,6 @@ this.UITour = {
|
|||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case "command": {
|
||||
if (aEvent.target.id == "UITourTooltipClose") {
|
||||
let window = aEvent.target.ownerDocument.defaultView;
|
||||
this.hideInfo(window);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -842,6 +840,8 @@ this.UITour = {
|
|||
- (Math.max(0, highlightWidthWithMin - targetRect.width) / 2);
|
||||
let offsetY = paddingLeftPx
|
||||
- (Math.max(0, highlightHeightWithMin - targetRect.height) / 2);
|
||||
|
||||
this._addAnnotationPanelMutationObserver(highlighter.parentElement);
|
||||
highlighter.parentElement.openPopup(aTargetEl, "overlap", offsetX, offsetY);
|
||||
}
|
||||
|
||||
|
@ -860,13 +860,27 @@ this.UITour = {
|
|||
this.removePinnedTab(aWindow);
|
||||
|
||||
let highlighter = aWindow.document.getElementById("UITourHighlight");
|
||||
this._removeAnnotationPanelMutationObserver(highlighter.parentElement);
|
||||
highlighter.parentElement.hidePopup();
|
||||
highlighter.removeAttribute("active");
|
||||
|
||||
this._setAppMenuStateForAnnotation(aWindow, "highlight", false);
|
||||
},
|
||||
|
||||
showInfo: function(aContentDocument, aAnchor, aTitle = "", aDescription = "", aIconURL = "", aButtons = []) {
|
||||
/**
|
||||
* Show an info panel.
|
||||
*
|
||||
* @param {Document} aContentDocument
|
||||
* @param {Node} aAnchor
|
||||
* @param {String} [aTitle=""]
|
||||
* @param {String} [aDescription=""]
|
||||
* @param {String} [aIconURL=""]
|
||||
* @param {Object[]} [aButtons=[]]
|
||||
* @param {Object} [aOptions={}]
|
||||
* @param {String} [aOptions.closeButtonCallbackID]
|
||||
*/
|
||||
showInfo: function(aContentDocument, aAnchor, aTitle = "", aDescription = "", aIconURL = "",
|
||||
aButtons = [], aOptions = {}) {
|
||||
function showInfoPanel(aAnchorEl) {
|
||||
aAnchorEl.focus();
|
||||
|
||||
|
@ -910,11 +924,21 @@ this.UITour = {
|
|||
tooltipButtons.hidden = !aButtons.length;
|
||||
|
||||
let tooltipClose = document.getElementById("UITourTooltipClose");
|
||||
tooltipClose.addEventListener("command", this);
|
||||
let closeButtonCallback = (event) => {
|
||||
this.hideInfo(document.defaultView);
|
||||
if (aOptions && aOptions.closeButtonCallbackID)
|
||||
this.sendPageCallback(aContentDocument, aOptions.closeButtonCallbackID);
|
||||
};
|
||||
tooltipClose.addEventListener("command", closeButtonCallback);
|
||||
tooltip.addEventListener("popuphiding", function tooltipHiding(event) {
|
||||
tooltip.removeEventListener("popuphiding", tooltipHiding);
|
||||
tooltipClose.removeEventListener("command", closeButtonCallback);
|
||||
});
|
||||
|
||||
tooltip.setAttribute("targetName", aAnchor.targetName);
|
||||
tooltip.hidden = false;
|
||||
let alignment = "bottomcenter topright";
|
||||
this._addAnnotationPanelMutationObserver(tooltip);
|
||||
tooltip.openPopup(aAnchorEl, alignment);
|
||||
}
|
||||
|
||||
|
@ -931,6 +955,7 @@ this.UITour = {
|
|||
let document = aWindow.document;
|
||||
|
||||
let tooltip = document.getElementById("UITourTooltip");
|
||||
this._removeAnnotationPanelMutationObserver(tooltip);
|
||||
tooltip.hidePopup();
|
||||
this._setAppMenuStateForAnnotation(aWindow, "info", false);
|
||||
|
||||
|
@ -1110,6 +1135,47 @@ this.UITour = {
|
|||
});
|
||||
});
|
||||
},
|
||||
|
||||
_addAnnotationPanelMutationObserver: function(aPanelEl) {
|
||||
#ifdef XP_LINUX
|
||||
let observer = this._annotationPanelMutationObservers.get(aPanelEl);
|
||||
if (observer) {
|
||||
return;
|
||||
}
|
||||
let win = aPanelEl.ownerDocument.defaultView;
|
||||
observer = new win.MutationObserver(this._annotationMutationCallback);
|
||||
this._annotationPanelMutationObservers.set(aPanelEl, observer);
|
||||
let observerOptions = {
|
||||
attributeFilter: ["height", "width"],
|
||||
attributes: true,
|
||||
};
|
||||
observer.observe(aPanelEl, observerOptions);
|
||||
#endif
|
||||
},
|
||||
|
||||
_removeAnnotationPanelMutationObserver: function(aPanelEl) {
|
||||
#ifdef XP_LINUX
|
||||
let observer = this._annotationPanelMutationObservers.get(aPanelEl);
|
||||
if (observer) {
|
||||
observer.disconnect();
|
||||
this._annotationPanelMutationObservers.delete(aPanelEl);
|
||||
}
|
||||
#endif
|
||||
},
|
||||
|
||||
/**
|
||||
* Workaround for Ubuntu panel craziness in bug 970788 where incorrect sizes get passed to
|
||||
* nsXULPopupManager::PopupResized and lead to incorrect width and height attributes getting
|
||||
* set on the panel.
|
||||
*/
|
||||
_annotationMutationCallback: function(aMutations) {
|
||||
for (let mutation of aMutations) {
|
||||
// Remove both attributes at once and ignore remaining mutations to be proccessed.
|
||||
mutation.target.removeAttribute("width");
|
||||
mutation.target.removeAttribute("height");
|
||||
return;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
this.UITour.init();
|
||||
|
|
|
@ -20,7 +20,6 @@ EXTRA_JS_MODULES += [
|
|||
'SitePermissions.jsm',
|
||||
'Social.jsm',
|
||||
'TabCrashReporter.jsm',
|
||||
'UITour.jsm',
|
||||
'webappsUI.jsm',
|
||||
'webrtcUI.jsm',
|
||||
]
|
||||
|
@ -34,6 +33,7 @@ if CONFIG['MOZ_WIDGET_TOOLKIT'] == 'windows':
|
|||
EXTRA_PP_JS_MODULES += [
|
||||
'AboutHome.jsm',
|
||||
'RecentWindow.jsm',
|
||||
'UITour.jsm',
|
||||
]
|
||||
|
||||
if CONFIG['MOZILLA_OFFICIAL']:
|
||||
|
|
|
@ -12,8 +12,9 @@ skip-if = os == "linux" # Intermittent failures, bug 951965
|
|||
[browser_UITour2.js]
|
||||
[browser_UITour3.js]
|
||||
[browser_UITour_availableTargets.js]
|
||||
[browser_UITour_panel_close_annotation.js]
|
||||
[browser_UITour_detach_tab.js]
|
||||
[browser_UITour_annotation_size_attributes.js]
|
||||
[browser_UITour_panel_close_annotation.js]
|
||||
[browser_UITour_registerPageID.js]
|
||||
[browser_UITour_sync.js]
|
||||
[browser_taskbar_preview.js]
|
||||
|
|
|
@ -122,4 +122,21 @@ let tests = [
|
|||
let buttons = gContentWindow.makeButtons();
|
||||
gContentAPI.showInfo("urlbar", "another title", "moar text", "./image.png", buttons);
|
||||
},
|
||||
|
||||
function test_info_close_button(done) {
|
||||
let popup = document.getElementById("UITourTooltip");
|
||||
let closeButton = document.getElementById("UITourTooltipClose");
|
||||
|
||||
popup.addEventListener("popupshown", function onPopupShown() {
|
||||
popup.removeEventListener("popupshown", onPopupShown);
|
||||
EventUtils.synthesizeMouseAtCenter(closeButton, {}, window);
|
||||
executeSoon(function() {
|
||||
is(gContentWindow.callbackResult, "closeButton", "Close button callback called");
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
let infoOptions = gContentWindow.makeInfoOptions();
|
||||
gContentAPI.showInfo("urlbar", "Close me", "X marks the spot", null, null, infoOptions);
|
||||
}
|
||||
];
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
/**
|
||||
* Test that width and height attributes don't get set by widget code on the highlight panel.
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
let gTestTab;
|
||||
let gContentAPI;
|
||||
let gContentWindow;
|
||||
let highlight = document.getElementById("UITourHighlightContainer");
|
||||
let tooltip = document.getElementById("UITourTooltip");
|
||||
|
||||
Components.utils.import("resource:///modules/UITour.jsm");
|
||||
|
||||
function test() {
|
||||
UITourTest();
|
||||
}
|
||||
|
||||
let tests = [
|
||||
function test_highlight_size_attributes(done) {
|
||||
gContentAPI.showHighlight("appMenu");
|
||||
waitForElementToBeVisible(highlight, function moveTheHighlight() {
|
||||
gContentAPI.showHighlight("urlbar");
|
||||
waitForElementToBeVisible(highlight, function checkPanelAttributes() {
|
||||
SimpleTest.executeSoon(() => {
|
||||
ise(highlight.height, "", "Highlight panel should have no explicit height set");
|
||||
ise(highlight.width, "", "Highlight panel should have no explicit width set");
|
||||
done();
|
||||
});
|
||||
}, "Highlight should be moved to the urlbar");
|
||||
}, "Highlight should be shown after showHighlight() for the appMenu");
|
||||
},
|
||||
|
||||
function test_info_size_attributes(done) {
|
||||
gContentAPI.showInfo("appMenu", "test title", "test text");
|
||||
waitForElementToBeVisible(tooltip, function moveTheTooltip() {
|
||||
gContentAPI.showInfo("urlbar", "new title", "new text");
|
||||
waitForElementToBeVisible(tooltip, function checkPanelAttributes() {
|
||||
SimpleTest.executeSoon(() => {
|
||||
ise(tooltip.height, "", "Info panel should have no explicit height set");
|
||||
ise(tooltip.width, "", "Info panel should have no explicit width set");
|
||||
done();
|
||||
});
|
||||
}, "Tooltip should be moved to the urlbar");
|
||||
}, "Tooltip should be shown after showInfo() for the appMenu");
|
||||
},
|
||||
|
||||
];
|
|
@ -2,7 +2,7 @@
|
|||
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
/**
|
||||
* Tests that annotations disappear when their target is hidden.
|
||||
* Detaching a tab to a new window shouldn't break the menu panel.
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
|
|
@ -20,6 +20,12 @@
|
|||
{label: "Button 2", callback: makeCallback("button2"), icon: "image.png"}
|
||||
];
|
||||
}
|
||||
|
||||
function makeInfoOptions() {
|
||||
return {
|
||||
closeButtonCallback: makeCallback("closeButton")
|
||||
};
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
|
|
|
@ -77,7 +77,7 @@ if (typeof Mozilla == 'undefined') {
|
|||
_sendEvent('hideHighlight');
|
||||
};
|
||||
|
||||
Mozilla.UITour.showInfo = function(target, title, text, icon, buttons) {
|
||||
Mozilla.UITour.showInfo = function(target, title, text, icon, buttons, options) {
|
||||
var buttonData = [];
|
||||
if (Array.isArray(buttons)) {
|
||||
for (var i = 0; i < buttons.length; i++) {
|
||||
|
@ -90,12 +90,17 @@ if (typeof Mozilla == 'undefined') {
|
|||
}
|
||||
}
|
||||
|
||||
var closeButtonCallbackID;
|
||||
if (options && options.closeButtonCallback)
|
||||
closeButtonCallbackID = _waitForCallback(options.closeButtonCallback);
|
||||
|
||||
_sendEvent('showInfo', {
|
||||
target: target,
|
||||
title: title,
|
||||
text: text,
|
||||
icon: icon,
|
||||
buttons: buttonData
|
||||
buttons: buttonData,
|
||||
closeButtonCallbackID: closeButtonCallbackID
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -17,6 +17,18 @@
|
|||
-moz-margin-start: 0;
|
||||
}
|
||||
|
||||
.subviewbutton > .toolbarbutton-text {
|
||||
-moz-padding-start: 16px;
|
||||
}
|
||||
|
||||
.subviewbutton:-moz-any([image],[targetURI],.cui-withicon, .restoreallitem, .bookmark-item) > .toolbarbutton-text {
|
||||
-moz-padding-start: 0;
|
||||
}
|
||||
|
||||
.subviewbutton.bookmark-item > .toolbarbutton-icon {
|
||||
-moz-margin-start: 3px;
|
||||
}
|
||||
|
||||
.PanelUI-subView toolbarseparator,
|
||||
.PanelUI-subView menuseparator,
|
||||
.cui-widget-panelview menuseparator,
|
||||
|
|
|
@ -58,6 +58,11 @@
|
|||
#PanelUI-quit:not([disabled]):hover:active {
|
||||
-moz-image-region: rect(0, 96px, 32px, 64px);
|
||||
}
|
||||
|
||||
.subviewbutton[checked="true"] {
|
||||
background-image: url("chrome://global/skin/menu/menu-check@2x.png");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.panelUI-grid .toolbarbutton-1 {
|
||||
|
@ -66,6 +71,31 @@
|
|||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.subviewbutton > .toolbarbutton-text,
|
||||
.subviewbutton > .menu-iconic-text {
|
||||
margin: 2px 0px;
|
||||
}
|
||||
|
||||
.subviewbutton:-moz-any([image],[targetURI],.cui-withicon, .bookmark-item) > .toolbarbutton-text {
|
||||
margin: 2px 6px;
|
||||
}
|
||||
|
||||
.restoreallitem > .toolbarbutton-icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.subviewbutton {
|
||||
-moz-padding-start: 18px;
|
||||
}
|
||||
|
||||
.subviewbutton[checked="true"] {
|
||||
background-position: top 5px left 4px;
|
||||
}
|
||||
|
||||
.subviewbutton:not(:-moz-any([image],[targetURI],.cui-withicon, .bookmark-item)) > .menu-iconic-left {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#BMB_bookmarksPopup > menu,
|
||||
#BMB_bookmarksPopup > menuitem:not(.panel-subview-footer) {
|
||||
padding-top: 5px;
|
||||
|
@ -94,8 +124,3 @@
|
|||
.cui-widget-panelview menuseparator {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.PanelUI-subView menuitem[checked="true"]::before,
|
||||
.PanelUI-subView toolbarbutton[checked="true"]::before {
|
||||
margin: 0 5px;
|
||||
}
|
||||
|
|
|
@ -51,7 +51,6 @@
|
|||
}
|
||||
|
||||
#UITourTooltipClose {
|
||||
visibility: hidden; /* XXX Temporarily disabled by bug 966913 */
|
||||
-moz-appearance: none;
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
|
|
|
@ -12,10 +12,10 @@
|
|||
% we don't support nested calc(): https://bugzilla.mozilla.org/show_bug.cgi?id=968761
|
||||
%define menuPanelButtonWidth (@menuPanelWidth@ / 3 - 0.1px)
|
||||
%define exitSubviewGutterWidth 38px
|
||||
%define buttonStateHover :not(:-moz-any([disabled],[open],[checked="true"],:active)):hover
|
||||
%define menuStateHover :not(:-moz-any([disabled],[checked="true"],:active))[_moz-menuactive]
|
||||
%define buttonStateActive :not([disabled]):-moz-any([open],[checked="true"],:hover:active)
|
||||
%define menuStateActive :not([disabled]):-moz-any([checked="true"],[_moz-menuactive]:active)
|
||||
%define buttonStateHover :not(:-moz-any([disabled],[open],:active)):hover
|
||||
%define menuStateHover :not(:-moz-any([disabled],:active))[_moz-menuactive]
|
||||
%define buttonStateActive :not([disabled]):-moz-any([open],:hover:active)
|
||||
%define menuStateActive :not([disabled])[_moz-menuactive]:active
|
||||
|
||||
%include ../browser.inc
|
||||
|
||||
|
@ -55,10 +55,6 @@
|
|||
-moz-box-flex: 1;
|
||||
}
|
||||
|
||||
.subviewbutton:not(:-moz-any([image],[targetURI],.cui-withicon)) > .toolbarbutton-text {
|
||||
-moz-margin-start: 0;
|
||||
}
|
||||
|
||||
.panel-subview-body {
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
|
@ -122,14 +118,13 @@
|
|||
padding: 0;
|
||||
}
|
||||
|
||||
.panelUI-grid .toolbarbutton-menubutton-button > .toolbarbutton-multiline-text,
|
||||
.panelUI-grid .toolbarbutton-1 > .toolbarbutton-menubutton-button > .toolbarbutton-multiline-text,
|
||||
.panelUI-grid .toolbarbutton-1 > .toolbarbutton-multiline-text {
|
||||
-moz-hyphens: auto;
|
||||
min-height: 3.5em;
|
||||
}
|
||||
|
||||
.panelUI-grid:not([customize-transitioning]) .toolbarbutton-menubutton-button > .toolbarbutton-multiline-text,
|
||||
.panelUI-grid:not([customize-transitioning]) .toolbarbutton-1 > .toolbarbutton-multiline-text {
|
||||
.panelUI-grid:not([customize-transitioning]) .toolbarbutton-1[fadelabel] > .toolbarbutton-menubutton-button > .toolbarbutton-multiline-text,
|
||||
.panelUI-grid:not([customize-transitioning]) .toolbarbutton-1[fadelabel] > .toolbarbutton-multiline-text {
|
||||
mask: url(chrome://browser/content/browser.xul#menuPanelButtonTextFadeOutMask);
|
||||
}
|
||||
|
||||
|
@ -139,7 +134,7 @@
|
|||
margin: 2px 0 0;
|
||||
}
|
||||
|
||||
.panelUI-grid .toolbarbutton-menubutton-button > .toolbarbutton-multiline-text {
|
||||
.panelUI-grid .toolbarbutton-1 > .toolbarbutton-menubutton-button > .toolbarbutton-multiline-text {
|
||||
text-align: center;
|
||||
margin: -1px 0 0;
|
||||
}
|
||||
|
@ -689,10 +684,6 @@ menuitem.subviewbutton@menuStateActive@,
|
|||
list-style-image: url("chrome://mozapps/skin/places/defaultFavicon.png");
|
||||
}
|
||||
|
||||
.restoreallitem.subviewbutton > .toolbarbutton-icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#PanelUI-recentlyClosedWindows > toolbarbutton > .toolbarbutton-icon,
|
||||
#PanelUI-recentlyClosedTabs > toolbarbutton > .toolbarbutton-icon,
|
||||
#PanelUI-historyItems > toolbarbutton > .toolbarbutton-icon {
|
||||
|
@ -820,14 +811,9 @@ toolbarpaletteitem[place="palette"] > #search-container {
|
|||
}
|
||||
|
||||
.toolbaritem-combined-buttons@inAnyPanel@ > separator {
|
||||
/**
|
||||
* The calculation below is a layout hack. Without it, when hovering over
|
||||
* a .toolbaritem-combined-buttons element in the menu panel, the disappearance
|
||||
* of the separator margins causes things in the menu panel to shift by a few
|
||||
* pixels on Linux. See bug 978767.
|
||||
*/
|
||||
margin: calc(0.5em - 1px) 0;
|
||||
margin: .5em 0;
|
||||
width: 1px;
|
||||
height: auto;
|
||||
background: hsla(210,4%,10%,.15);
|
||||
transition-property: margin;
|
||||
transition-duration: 10ms;
|
||||
|
@ -899,22 +885,16 @@ toolbaritem[overflowedItem=true],
|
|||
box-shadow: 0 0 0 1px hsla(0,0%,100%,.2);
|
||||
}
|
||||
|
||||
.PanelUI-subView toolbarbutton[checked="true"] {
|
||||
-moz-padding-start: 4px;
|
||||
.subviewbutton[checked="true"] {
|
||||
background: url("chrome://global/skin/menu/menu-check.png") top 7px left 7px / 11px 11px no-repeat transparent;
|
||||
}
|
||||
|
||||
.PanelUI-subView toolbarbutton[checked="true"] > .toolbarbutton-text {
|
||||
-moz-padding-start: 0px;
|
||||
.PanelUI-subView > menu > .menu-iconic-left,
|
||||
.PanelUI-subView > menuitem > .menu-iconic-left {
|
||||
-moz-appearance: none;
|
||||
-moz-margin-end: 3px;
|
||||
}
|
||||
|
||||
.PanelUI-subView menuitem[checked="true"]::before,
|
||||
.PanelUI-subView toolbarbutton[checked="true"]::before {
|
||||
content: "✓";
|
||||
display: -moz-box;
|
||||
width: 12px;
|
||||
margin: 0 2px;
|
||||
}
|
||||
|
||||
#BMB_bookmarksPopup > menuitem[checked="true"] > .menu-iconic-left {
|
||||
display: none;
|
||||
.PanelUI-subView > menuitem[checked="true"] > .menu-iconic-left {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
|
|
@ -35,9 +35,16 @@
|
|||
padding: 0 6px;
|
||||
}
|
||||
|
||||
#BMB_bookmarksPopup menuitem[checked="true"]::before,
|
||||
#BMB_bookmarksPopup toolbarbutton[checked="true"]::before {
|
||||
margin: 0 9px;
|
||||
.subviewbutton > .toolbarbutton-text {
|
||||
-moz-padding-start: 16px;
|
||||
}
|
||||
|
||||
.subviewbutton:-moz-any([image],[targetURI],.cui-withicon, .restoreallitem, .bookmark-item) > .toolbarbutton-text {
|
||||
-moz-padding-start: 0;
|
||||
}
|
||||
|
||||
.subviewbutton.bookmark-item > .toolbarbutton-icon {
|
||||
-moz-margin-start: 3px;
|
||||
}
|
||||
|
||||
%ifdef WINDOWS_AERO
|
||||
|
|
|
@ -67,7 +67,7 @@ public class Tab {
|
|||
private Context mAppContext;
|
||||
private ErrorType mErrorType = ErrorType.NONE;
|
||||
private static final int MAX_HISTORY_LIST_SIZE = 50;
|
||||
private int mLoadProgress;
|
||||
private volatile int mLoadProgress;
|
||||
|
||||
public static final int STATE_DELAYED = 0;
|
||||
public static final int STATE_LOADING = 1;
|
||||
|
@ -635,7 +635,7 @@ public class Tab {
|
|||
setHasTouchListeners(false);
|
||||
setBackgroundColor(DEFAULT_BACKGROUND_COLOR);
|
||||
setErrorType(ErrorType.NONE);
|
||||
setLoadProgress(LOAD_PROGRESS_LOCATION_CHANGE);
|
||||
setLoadProgressIfLoading(LOAD_PROGRESS_LOCATION_CHANGE);
|
||||
|
||||
Tabs.getInstance().notifyListeners(this, Tabs.TabEvents.LOCATION_CHANGE, oldUrl);
|
||||
}
|
||||
|
@ -672,7 +672,7 @@ public class Tab {
|
|||
}
|
||||
|
||||
void handleContentLoaded() {
|
||||
setLoadProgress(LOAD_PROGRESS_LOADED);
|
||||
setLoadProgressIfLoading(LOAD_PROGRESS_LOADED);
|
||||
}
|
||||
|
||||
protected void saveThumbnailToDB() {
|
||||
|
@ -776,6 +776,22 @@ public class Tab {
|
|||
mLoadProgress = progressPercentage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the tab load progress to the given percentage only if the tab is
|
||||
* currently loading.
|
||||
*
|
||||
* about:neterror can trigger a STOP before other page load events (bug
|
||||
* 976426), so any post-START events should make sure the page is loading
|
||||
* before updating progress.
|
||||
*
|
||||
* @param progressPercentage Percentage to set progress to (0-100)
|
||||
*/
|
||||
void setLoadProgressIfLoading(int progressPercentage) {
|
||||
if (getState() == STATE_LOADING) {
|
||||
setLoadProgress(progressPercentage);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the tab load progress percentage.
|
||||
*
|
||||
|
|
|
@ -887,7 +887,7 @@ public class GeckoLayerClient implements LayerView.Listener, PanZoomTarget
|
|||
* You must hold the monitor while calling this.
|
||||
*/
|
||||
@Override
|
||||
public void onSubdocumentScrollBy(float dx, float dy) {
|
||||
public void scrollMarginsBy(float dx, float dy) {
|
||||
ImmutableViewportMetrics newMarginsMetrics =
|
||||
mMarginsAnimator.scrollBy(mViewportMetrics, dx, dy);
|
||||
mViewportMetrics = mViewportMetrics.setMarginsFrom(newMarginsMetrics);
|
||||
|
|
|
@ -128,6 +128,8 @@ class JavaPanZoomController
|
|||
private boolean mMediumPress;
|
||||
/* Used to change the scrollY direction */
|
||||
private boolean mNegateWheelScrollY;
|
||||
/* Whether the current event has been default-prevented. */
|
||||
private boolean mDefaultPrevented;
|
||||
|
||||
// Handler to be notified when overscroll occurs
|
||||
private Overscroll mOverscroll;
|
||||
|
@ -343,7 +345,9 @@ class JavaPanZoomController
|
|||
return mTouchEventHandler.handleEvent(event);
|
||||
}
|
||||
|
||||
boolean handleEvent(MotionEvent event) {
|
||||
boolean handleEvent(MotionEvent event, boolean defaultPrevented) {
|
||||
mDefaultPrevented = defaultPrevented;
|
||||
|
||||
switch (event.getAction() & MotionEvent.ACTION_MASK) {
|
||||
case MotionEvent.ACTION_DOWN: return handleTouchStart(event);
|
||||
case MotionEvent.ACTION_MOVE: return handleTouchMove(event);
|
||||
|
@ -401,17 +405,6 @@ class JavaPanZoomController
|
|||
}
|
||||
}
|
||||
|
||||
/** This function must be called on the UI thread. */
|
||||
public void preventedTouchFinished() {
|
||||
checkMainThread();
|
||||
if (mState == PanZoomState.WAITING_LISTENERS) {
|
||||
// if we enter here, we just finished a block of events whose default actions
|
||||
// were prevented by touch listeners. Now there are no touch points left, so
|
||||
// we need to reset our state and re-bounce because we might be in overscroll
|
||||
bounce();
|
||||
}
|
||||
}
|
||||
|
||||
/** This must be called on the UI thread. */
|
||||
@Override
|
||||
public void pageRectUpdated() {
|
||||
|
@ -524,16 +517,18 @@ class JavaPanZoomController
|
|||
case FLING:
|
||||
case AUTONAV:
|
||||
case BOUNCE:
|
||||
case WAITING_LISTENERS:
|
||||
// should never happen
|
||||
Log.e(LOGTAG, "Received impossible touch end while in " + mState);
|
||||
// fall through
|
||||
case ANIMATED_ZOOM:
|
||||
case NOTHING:
|
||||
// may happen if user double-taps and drags without lifting after the
|
||||
// second tap. ignore if this happens.
|
||||
return false;
|
||||
|
||||
case WAITING_LISTENERS:
|
||||
if (!mDefaultPrevented) {
|
||||
// should never happen
|
||||
Log.e(LOGTAG, "Received impossible touch end while in " + mState);
|
||||
}
|
||||
// fall through
|
||||
case TOUCHING:
|
||||
// the switch into TOUCHING might have happened while the page was
|
||||
// snapping back after overscroll. we need to finish the snap if that
|
||||
|
@ -562,16 +557,6 @@ class JavaPanZoomController
|
|||
private boolean handleTouchCancel(MotionEvent event) {
|
||||
cancelTouch();
|
||||
|
||||
if (mState == PanZoomState.WAITING_LISTENERS) {
|
||||
// we might get a cancel event from the TouchEventHandler while in the
|
||||
// WAITING_LISTENERS state if the touch listeners prevent-default the
|
||||
// block of events. at this point being in WAITING_LISTENERS is equivalent
|
||||
// to being in NOTHING with the exception of possibly being in overscroll.
|
||||
// so here we don't want to do anything right now; the overscroll will be
|
||||
// corrected in preventedTouchFinished().
|
||||
return false;
|
||||
}
|
||||
|
||||
// ensure we snap back if we're overscrolled
|
||||
bounce();
|
||||
return false;
|
||||
|
@ -827,9 +812,9 @@ class JavaPanZoomController
|
|||
if (FloatUtils.fuzzyEquals(displacement.x, 0.0f) && FloatUtils.fuzzyEquals(displacement.y, 0.0f)) {
|
||||
return;
|
||||
}
|
||||
if (mSubscroller.scrollBy(displacement)) {
|
||||
if (mDefaultPrevented || mSubscroller.scrollBy(displacement)) {
|
||||
synchronized (mTarget.getLock()) {
|
||||
mTarget.onSubdocumentScrollBy(displacement.x, displacement.y);
|
||||
mTarget.scrollMarginsBy(displacement.x, displacement.y);
|
||||
}
|
||||
} else {
|
||||
synchronized (mTarget.getLock()) {
|
||||
|
|
|
@ -19,7 +19,7 @@ public interface PanZoomTarget {
|
|||
public void setAnimationTarget(ImmutableViewportMetrics viewport);
|
||||
public void setViewportMetrics(ImmutableViewportMetrics viewport);
|
||||
public void scrollBy(float dx, float dy);
|
||||
public void onSubdocumentScrollBy(float dx, float dy);
|
||||
public void scrollMarginsBy(float dx, float dy);
|
||||
public void panZoomStopped();
|
||||
/** This triggers an (asynchronous) viewport update/redraw. */
|
||||
public void forceRedraw(DisplayPortMetrics displayPort);
|
||||
|
|
|
@ -74,11 +74,11 @@ final class TouchEventHandler implements Tabs.OnTabsChangedListener {
|
|||
// default-prevented or not (or we time out waiting for that).
|
||||
private boolean mHoldInQueue;
|
||||
|
||||
// true if we should dispatch incoming events to the gesture detector and the pan/zoom
|
||||
// controller. if this is false, then the current block of events has been
|
||||
// default-prevented, and we should not dispatch these events (although we'll still send
|
||||
// them to gecko listeners).
|
||||
private boolean mDispatchEvents;
|
||||
// false if the current event block has been default-prevented. In this case,
|
||||
// we still pass the event to both Gecko and the pan/zoom controller, but the
|
||||
// latter will not use it to scroll content. It may still use the events for
|
||||
// other things, such as making the dynamic toolbar visible.
|
||||
private boolean mAllowDefaultAction;
|
||||
|
||||
// this next variable requires some explanation. strap yourself in.
|
||||
//
|
||||
|
@ -128,7 +128,7 @@ final class TouchEventHandler implements Tabs.OnTabsChangedListener {
|
|||
mGestureDetector = new GestureDetector(context, mPanZoomController);
|
||||
mScaleGestureDetector = new SimpleScaleGestureDetector(mPanZoomController);
|
||||
mListenerTimeoutProcessor = new ListenerTimeoutProcessor();
|
||||
mDispatchEvents = true;
|
||||
mAllowDefaultAction = true;
|
||||
|
||||
mGestureDetector.setOnDoubleTapListener(mPanZoomController);
|
||||
|
||||
|
@ -145,10 +145,10 @@ final class TouchEventHandler implements Tabs.OnTabsChangedListener {
|
|||
// this is the start of a new block of events! whee!
|
||||
mHoldInQueue = mWaitForTouchListeners;
|
||||
|
||||
// Set mDispatchEvents to true so that we are guaranteed to either queue these
|
||||
// events or dispatch them. The only time we should not do either is once we've
|
||||
// heard back from content to preventDefault this block.
|
||||
mDispatchEvents = true;
|
||||
// Set mAllowDefaultAction to true so that in the event we dispatch events, the
|
||||
// PanZoomController doesn't treat them as if they've been prevent-defaulted
|
||||
// when they haven't.
|
||||
mAllowDefaultAction = true;
|
||||
if (mHoldInQueue) {
|
||||
// if the new block we are starting is the current block (i.e. there are no
|
||||
// other blocks waiting in the queue, then we should let the pan/zoom controller
|
||||
|
@ -170,17 +170,12 @@ final class TouchEventHandler implements Tabs.OnTabsChangedListener {
|
|||
mView.postDelayed(mListenerTimeoutProcessor, EVENT_LISTENER_TIMEOUT);
|
||||
}
|
||||
|
||||
// if we need to hold the events, add it to the queue. if we need to dispatch
|
||||
// it directly, do that. it is possible that both mHoldInQueue and mDispatchEvents
|
||||
// are false, in which case we are processing a block of events that we know
|
||||
// has been default-prevented. in that case we don't keep the events as we don't
|
||||
// need them (but we still pass them to the gecko listener).
|
||||
// if we need to hold the events, add it to the queue, otherwise dispatch
|
||||
// it directly.
|
||||
if (mHoldInQueue) {
|
||||
mEventQueue.add(MotionEvent.obtain(event));
|
||||
} else if (mDispatchEvents) {
|
||||
dispatchEvent(event);
|
||||
} else if (touchFinished(event)) {
|
||||
mPanZoomController.preventedTouchFinished();
|
||||
} else {
|
||||
dispatchEvent(event, mAllowDefaultAction);
|
||||
}
|
||||
|
||||
return false;
|
||||
|
@ -224,7 +219,8 @@ final class TouchEventHandler implements Tabs.OnTabsChangedListener {
|
|||
/**
|
||||
* Dispatch the event to the gesture detectors and the pan/zoom controller.
|
||||
*/
|
||||
private void dispatchEvent(MotionEvent event) {
|
||||
private void dispatchEvent(MotionEvent event, boolean allowDefaultAction) {
|
||||
if (allowDefaultAction) {
|
||||
if (mGestureDetector.onTouchEvent(event)) {
|
||||
return;
|
||||
}
|
||||
|
@ -232,7 +228,8 @@ final class TouchEventHandler implements Tabs.OnTabsChangedListener {
|
|||
if (mScaleGestureDetector.isInProgress()) {
|
||||
return;
|
||||
}
|
||||
mPanZoomController.handleEvent(event);
|
||||
}
|
||||
mPanZoomController.handleEvent(event, !allowDefaultAction);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -240,13 +237,6 @@ final class TouchEventHandler implements Tabs.OnTabsChangedListener {
|
|||
* whether it has been default-prevented or not.
|
||||
*/
|
||||
private void processEventBlock(boolean allowDefaultAction) {
|
||||
if (!allowDefaultAction) {
|
||||
// if the block has been default-prevented, cancel whatever stuff we had in
|
||||
// progress in the gesture detector and pan zoom controller
|
||||
long now = SystemClock.uptimeMillis();
|
||||
dispatchEvent(MotionEvent.obtain(now, now, MotionEvent.ACTION_CANCEL, 0, 0, 0));
|
||||
}
|
||||
|
||||
if (mEventQueue.isEmpty()) {
|
||||
Log.e(LOGTAG, "Unexpected empty event queue in processEventBlock!", new Exception());
|
||||
return;
|
||||
|
@ -263,13 +253,7 @@ final class TouchEventHandler implements Tabs.OnTabsChangedListener {
|
|||
// that has already been dispatched.
|
||||
|
||||
if (event != null) {
|
||||
// for each event we process, only dispatch it if the block hasn't been
|
||||
// default-prevented.
|
||||
if (allowDefaultAction) {
|
||||
dispatchEvent(event);
|
||||
} else if (touchFinished(event)) {
|
||||
mPanZoomController.preventedTouchFinished();
|
||||
}
|
||||
dispatchEvent(event, allowDefaultAction);
|
||||
}
|
||||
if (mEventQueue.isEmpty()) {
|
||||
// we have processed the backlog of events, and are all caught up.
|
||||
|
@ -278,7 +262,7 @@ final class TouchEventHandler implements Tabs.OnTabsChangedListener {
|
|||
// remaining events in this block (which is still ongoing) without
|
||||
// having to put them in the queue.
|
||||
mHoldInQueue = false;
|
||||
mDispatchEvents = allowDefaultAction;
|
||||
mAllowDefaultAction = allowDefaultAction;
|
||||
break;
|
||||
}
|
||||
event = mEventQueue.peek();
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
package org.mozilla.gecko.home;
|
||||
|
||||
import org.mozilla.gecko.R;
|
||||
import org.mozilla.gecko.util.ThreadUtils;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
|
@ -17,7 +18,11 @@ import android.os.Parcelable;
|
|||
import android.text.TextUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.EnumSet;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
|
||||
public final class HomeConfig {
|
||||
|
@ -265,7 +270,7 @@ public final class HomeConfig {
|
|||
return mFlags.contains(Flags.DEFAULT_PANEL);
|
||||
}
|
||||
|
||||
public void setIsDefault(boolean isDefault) {
|
||||
private void setIsDefault(boolean isDefault) {
|
||||
if (isDefault) {
|
||||
mFlags.add(Flags.DEFAULT_PANEL);
|
||||
} else {
|
||||
|
@ -277,7 +282,7 @@ public final class HomeConfig {
|
|||
return mFlags.contains(Flags.DISABLED_PANEL);
|
||||
}
|
||||
|
||||
public void setIsDisabled(boolean isDisabled) {
|
||||
private void setIsDisabled(boolean isDisabled) {
|
||||
if (isDisabled) {
|
||||
mFlags.add(Flags.DISABLED_PANEL);
|
||||
} else {
|
||||
|
@ -692,6 +697,374 @@ public final class HomeConfig {
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Immutable representation of the current state of {@code HomeConfig}.
|
||||
* This is what HomeConfig returns from a load() call and takes as
|
||||
* input to save a new state.
|
||||
*
|
||||
* Users of {@code State} should use an {@code Iterator} to iterate
|
||||
* through the contained {@code PanelConfig} instances.
|
||||
*
|
||||
* {@code State} is immutable i.e. you can't add, remove, or update
|
||||
* contained elements directly. You have to use an {@code Editor} to
|
||||
* change the state, which can be created through the {@code edit()}
|
||||
* method.
|
||||
*/
|
||||
public static class State implements Iterable<PanelConfig> {
|
||||
private final HomeConfig mHomeConfig;
|
||||
private final List<PanelConfig> mPanelConfigs;
|
||||
|
||||
private State(HomeConfig homeConfig, List<PanelConfig> panelConfigs) {
|
||||
mHomeConfig = homeConfig;
|
||||
mPanelConfigs = Collections.unmodifiableList(panelConfigs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterator<PanelConfig> iterator() {
|
||||
return mPanelConfigs.iterator();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an {@code Editor} for this state.
|
||||
*/
|
||||
public Editor edit() {
|
||||
return new Editor(mHomeConfig, this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@code Editor} allows you to make changes to a {@code State}. You
|
||||
* can create {@code Editor} by calling {@code edit()} on the target
|
||||
* {@code State} instance.
|
||||
*
|
||||
* {@code Editor} works on a copy of the {@code State} that originated
|
||||
* it. This means that adding, removing, or updating panels in an
|
||||
* {@code Editor} will never change the {@code State} which you
|
||||
* created the {@code Editor} from. Calling {@code commit()} or
|
||||
* {@code apply()} will cause the new {@code State} instance to be
|
||||
* created and saved using the {@code HomeConfig} instance that
|
||||
* created the source {@code State}.
|
||||
*
|
||||
* {@code Editor} is *not* thread-safe. You can only make calls on it
|
||||
* from the thread where it was originally created. It will throw an
|
||||
* exception if you don't follow this invariant.
|
||||
*/
|
||||
public static class Editor implements Iterable<PanelConfig> {
|
||||
private final HomeConfig mHomeConfig;
|
||||
private final HashMap<String, PanelConfig> mConfigMap;
|
||||
private final Thread mOriginalThread;
|
||||
|
||||
private PanelConfig mDefaultPanel;
|
||||
private int mEnabledCount;
|
||||
|
||||
private Editor(HomeConfig homeConfig, State configState) {
|
||||
mHomeConfig = homeConfig;
|
||||
mOriginalThread = Thread.currentThread();
|
||||
mConfigMap = new LinkedHashMap<String, PanelConfig>();
|
||||
mEnabledCount = 0;
|
||||
|
||||
initFromState(configState);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the initial state of the editor from the given
|
||||
* {@sode State}. A LinkedHashMap is used to represent the list of
|
||||
* panels as it provides fast access to specific panels from IDs
|
||||
* while also being order-aware. We keep a reference to the
|
||||
* default panel and the number of enabled panels to avoid iterating
|
||||
* through the map every time we need those.
|
||||
*
|
||||
* @param configState The source State to load the editor from.
|
||||
*/
|
||||
private void initFromState(State configState) {
|
||||
for (PanelConfig panelConfig : configState) {
|
||||
final PanelConfig panelCopy = new PanelConfig(panelConfig);
|
||||
|
||||
if (!panelCopy.isDisabled()) {
|
||||
mEnabledCount++;
|
||||
}
|
||||
|
||||
if (panelCopy.isDefault()) {
|
||||
if (mDefaultPanel == null) {
|
||||
mDefaultPanel = panelCopy;
|
||||
} else {
|
||||
throw new IllegalStateException("Multiple default panels in HomeConfig state");
|
||||
}
|
||||
}
|
||||
|
||||
mConfigMap.put(panelConfig.getId(), panelCopy);
|
||||
}
|
||||
|
||||
// We should always have a defined default panel if there's
|
||||
// at least one enabled panel around.
|
||||
if (mEnabledCount > 0 && mDefaultPanel == null) {
|
||||
throw new IllegalStateException("Default panel in HomeConfig state is undefined");
|
||||
}
|
||||
}
|
||||
|
||||
private PanelConfig getPanelOrThrow(String panelId) {
|
||||
final PanelConfig panelConfig = mConfigMap.get(panelId);
|
||||
if (panelConfig == null) {
|
||||
throw new IllegalStateException("Tried to access non-existing panel: " + panelId);
|
||||
}
|
||||
|
||||
return panelConfig;
|
||||
}
|
||||
|
||||
private boolean isCurrentDefaultPanel(PanelConfig panelConfig) {
|
||||
if (mDefaultPanel == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return mDefaultPanel.equals(panelConfig);
|
||||
}
|
||||
|
||||
private void findNewDefault() {
|
||||
// Pick the first panel that is neither disabled nor currently
|
||||
// set as default.
|
||||
for (PanelConfig panelConfig : mConfigMap.values()) {
|
||||
if (!panelConfig.isDefault() && !panelConfig.isDisabled()) {
|
||||
setDefault(panelConfig.getId());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
mDefaultPanel = null;
|
||||
}
|
||||
|
||||
private List<PanelConfig> makeDeepCopy() {
|
||||
List<PanelConfig> copiedList = new ArrayList<PanelConfig>();
|
||||
for (PanelConfig panelConfig : mConfigMap.values()) {
|
||||
copiedList.add(new PanelConfig(panelConfig));
|
||||
}
|
||||
|
||||
return copiedList;
|
||||
}
|
||||
|
||||
private void setPanelIsDisabled(PanelConfig panelConfig, boolean disabled) {
|
||||
if (panelConfig.isDisabled() == disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
panelConfig.setIsDisabled(disabled);
|
||||
mEnabledCount += (disabled ? -1 : 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the ID of the current default panel.
|
||||
*/
|
||||
public String getDefaultPanelId() {
|
||||
ThreadUtils.assertOnThread(mOriginalThread);
|
||||
|
||||
if (mDefaultPanel == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return mDefaultPanel.getId();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a new default panel.
|
||||
*
|
||||
* @param panelId the ID of the new default panel.
|
||||
*/
|
||||
public void setDefault(String panelId) {
|
||||
ThreadUtils.assertOnThread(mOriginalThread);
|
||||
|
||||
final PanelConfig panelConfig = getPanelOrThrow(panelId);
|
||||
if (isCurrentDefaultPanel(panelConfig)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (mDefaultPanel != null) {
|
||||
mDefaultPanel.setIsDefault(false);
|
||||
}
|
||||
|
||||
panelConfig.setIsDefault(true);
|
||||
setPanelIsDisabled(panelConfig, false);
|
||||
|
||||
mDefaultPanel = panelConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles disabled state for a panel.
|
||||
*
|
||||
* @param panelId the ID of the target panel.
|
||||
* @param disabled true to disable the panel.
|
||||
*/
|
||||
public void setDisabled(String panelId, boolean disabled) {
|
||||
ThreadUtils.assertOnThread(mOriginalThread);
|
||||
|
||||
final PanelConfig panelConfig = getPanelOrThrow(panelId);
|
||||
if (panelConfig.isDisabled() == disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
setPanelIsDisabled(panelConfig, disabled);
|
||||
|
||||
if (disabled) {
|
||||
if (isCurrentDefaultPanel(panelConfig)) {
|
||||
panelConfig.setIsDefault(false);
|
||||
findNewDefault();
|
||||
}
|
||||
} else if (mEnabledCount == 1) {
|
||||
setDefault(panelId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new {@code PanelConfig}. It will do nothing if the
|
||||
* {@code Editor} already contains a panel with the same ID.
|
||||
*
|
||||
* @param panelConfig the {@code PanelConfig} instance to be added.
|
||||
* @return true if the item has been added.
|
||||
*/
|
||||
public boolean install(PanelConfig panelConfig) {
|
||||
ThreadUtils.assertOnThread(mOriginalThread);
|
||||
|
||||
if (panelConfig == null) {
|
||||
throw new IllegalStateException("Can't install a null panel");
|
||||
}
|
||||
|
||||
if (!panelConfig.isDynamic()) {
|
||||
throw new IllegalStateException("Can't install a built-in panel: " + panelConfig.getId());
|
||||
}
|
||||
|
||||
if (panelConfig.isDisabled()) {
|
||||
throw new IllegalStateException("Can't install a disabled panel: " + panelConfig.getId());
|
||||
}
|
||||
|
||||
boolean installed = false;
|
||||
|
||||
final String id = panelConfig.getId();
|
||||
if (!mConfigMap.containsKey(id)) {
|
||||
mConfigMap.put(id, panelConfig);
|
||||
|
||||
mEnabledCount++;
|
||||
if (mEnabledCount == 1 || panelConfig.isDefault()) {
|
||||
setDefault(panelConfig.getId());
|
||||
}
|
||||
|
||||
installed = true;
|
||||
}
|
||||
|
||||
return installed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes an existing panel.
|
||||
*
|
||||
* @return true if the item has been removed.
|
||||
*/
|
||||
public boolean uninstall(String panelId) {
|
||||
ThreadUtils.assertOnThread(mOriginalThread);
|
||||
|
||||
final PanelConfig panelConfig = mConfigMap.get(panelId);
|
||||
if (panelConfig == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!panelConfig.isDynamic()) {
|
||||
throw new IllegalStateException("Can't uninstall a built-in panel: " + panelConfig.getId());
|
||||
}
|
||||
|
||||
mConfigMap.remove(panelId);
|
||||
|
||||
if (!panelConfig.isDisabled()) {
|
||||
mEnabledCount--;
|
||||
}
|
||||
|
||||
if (isCurrentDefaultPanel(panelConfig)) {
|
||||
findNewDefault();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces an existing panel with a new {@code PanelConfig} instance.
|
||||
*
|
||||
* @return true if the item has been updated.
|
||||
*/
|
||||
public boolean update(PanelConfig panelConfig) {
|
||||
ThreadUtils.assertOnThread(mOriginalThread);
|
||||
|
||||
if (panelConfig == null) {
|
||||
throw new IllegalStateException("Can't update a null panel");
|
||||
}
|
||||
|
||||
boolean updated = false;
|
||||
|
||||
final String id = panelConfig.getId();
|
||||
if (mConfigMap.containsKey(id)) {
|
||||
final PanelConfig oldPanelConfig = mConfigMap.put(id, panelConfig);
|
||||
|
||||
// The disabled and default states can't never be
|
||||
// changed by an update operation.
|
||||
panelConfig.setIsDefault(oldPanelConfig.isDefault());
|
||||
panelConfig.setIsDisabled(oldPanelConfig.isDisabled());
|
||||
|
||||
updated = true;
|
||||
}
|
||||
|
||||
return updated;
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the current {@code Editor} state asynchronously in the
|
||||
* background thread.
|
||||
*
|
||||
* @return the resulting {@code State} instance.
|
||||
*/
|
||||
public State apply() {
|
||||
ThreadUtils.assertOnThread(mOriginalThread);
|
||||
|
||||
// We're about to save the current state in the background thread
|
||||
// so we should use a deep copy of the PanelConfig instances to
|
||||
// avoid saving corrupted state.
|
||||
final State newConfigState = new State(mHomeConfig, makeDeepCopy());
|
||||
|
||||
ThreadUtils.getBackgroundHandler().post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
mHomeConfig.save(newConfigState);
|
||||
}
|
||||
});
|
||||
|
||||
return newConfigState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the current {@code Editor} state synchronously in the
|
||||
* current thread.
|
||||
*
|
||||
* @return the resulting {@code State} instance.
|
||||
*/
|
||||
public State commit() {
|
||||
ThreadUtils.assertOnThread(mOriginalThread);
|
||||
|
||||
final State newConfigState =
|
||||
new State(mHomeConfig, new ArrayList<PanelConfig>(mConfigMap.values()));
|
||||
|
||||
// This is a synchronous blocking operation, hence no
|
||||
// need to deep copy the current PanelConfig instances.
|
||||
mHomeConfig.save(newConfigState);
|
||||
|
||||
return newConfigState;
|
||||
}
|
||||
|
||||
public boolean isEmpty() {
|
||||
return mConfigMap.isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterator<PanelConfig> iterator() {
|
||||
ThreadUtils.assertOnThread(mOriginalThread);
|
||||
|
||||
return mConfigMap.values().iterator();
|
||||
}
|
||||
}
|
||||
|
||||
public interface OnChangeListener {
|
||||
public void onChange();
|
||||
}
|
||||
|
@ -715,16 +1088,17 @@ public final class HomeConfig {
|
|||
mBackend = backend;
|
||||
}
|
||||
|
||||
public List<PanelConfig> load() {
|
||||
return mBackend.load();
|
||||
public State load() {
|
||||
final List<PanelConfig> panelConfigs = mBackend.load();
|
||||
return new State(this, panelConfigs);
|
||||
}
|
||||
|
||||
public String getLocale() {
|
||||
return mBackend.getLocale();
|
||||
}
|
||||
|
||||
public void save(List<PanelConfig> panelConfigs) {
|
||||
mBackend.save(panelConfigs);
|
||||
public void save(State configState) {
|
||||
mBackend.save(configState.mPanelConfigs);
|
||||
}
|
||||
|
||||
public void setOnChangeListener(OnChangeListener listener) {
|
||||
|
|
|
@ -189,36 +189,10 @@ public class HomeConfigInvalidator implements GeckoEventListener {
|
|||
Log.d(LOGTAG, "scheduleInvalidation: scheduled new invalidation: " + mode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace an element if a matching PanelConfig is
|
||||
* present in the given list.
|
||||
*/
|
||||
private boolean replacePanelConfig(List<PanelConfig> panelConfigs, PanelConfig panelConfig) {
|
||||
final int index = panelConfigs.indexOf(panelConfig);
|
||||
if (index >= 0) {
|
||||
panelConfigs.set(index, panelConfig);
|
||||
Log.d(LOGTAG, "executePendingChanges: replaced position " + index + " with " + panelConfig.getId());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private PanelConfig findPanelConfigWithId(List<PanelConfig> panelConfigs, String panelId) {
|
||||
for (PanelConfig panelConfig : panelConfigs) {
|
||||
if (panelConfig.getId().equals(panelId)) {
|
||||
return panelConfig;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs in the background thread.
|
||||
*/
|
||||
private List<PanelConfig> executePendingChanges(List<PanelConfig> panelConfigs) {
|
||||
private void executePendingChanges(HomeConfig.Editor editor) {
|
||||
boolean shouldRefresh = false;
|
||||
|
||||
while (!mPendingChanges.isEmpty()) {
|
||||
|
@ -227,17 +201,15 @@ public class HomeConfigInvalidator implements GeckoEventListener {
|
|||
switch (pendingChange.type) {
|
||||
case UNINSTALL: {
|
||||
final String panelId = (String) pendingChange.target;
|
||||
final PanelConfig panelConfig = findPanelConfigWithId(panelConfigs, panelId);
|
||||
if (panelConfig != null && panelConfigs.remove(panelConfig)) {
|
||||
Log.d(LOGTAG, "executePendingChanges: removed panel " + panelConfig.getId());
|
||||
if (editor.uninstall(panelId)) {
|
||||
Log.d(LOGTAG, "executePendingChanges: uninstalled panel " + panelId);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case INSTALL: {
|
||||
final PanelConfig panelConfig = (PanelConfig) pendingChange.target;
|
||||
if (!replacePanelConfig(panelConfigs, panelConfig)) {
|
||||
panelConfigs.add(panelConfig);
|
||||
if (editor.install(panelConfig)) {
|
||||
Log.d(LOGTAG, "executePendingChanges: added panel " + panelConfig.getId());
|
||||
}
|
||||
break;
|
||||
|
@ -245,8 +217,8 @@ public class HomeConfigInvalidator implements GeckoEventListener {
|
|||
|
||||
case UPDATE: {
|
||||
final PanelConfig panelConfig = (PanelConfig) pendingChange.target;
|
||||
if (!replacePanelConfig(panelConfigs, panelConfig)) {
|
||||
Log.w(LOGTAG, "Tried to update non-existing panel " + panelConfig.getId());
|
||||
if (editor.update(panelConfig)) {
|
||||
Log.w(LOGTAG, "executePendingChanges: updated panel " + panelConfig.getId());
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
@ -258,23 +230,19 @@ public class HomeConfigInvalidator implements GeckoEventListener {
|
|||
}
|
||||
|
||||
if (shouldRefresh) {
|
||||
return executeRefresh(panelConfigs);
|
||||
} else {
|
||||
return panelConfigs;
|
||||
executeRefresh(editor);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs in the background thread.
|
||||
*/
|
||||
private List<PanelConfig> refreshFromPanelInfos(List<PanelConfig> panelConfigs, List<PanelInfo> panelInfos) {
|
||||
private void refreshFromPanelInfos(HomeConfig.Editor editor, List<PanelInfo> panelInfos) {
|
||||
Log.d(LOGTAG, "refreshFromPanelInfos");
|
||||
|
||||
final int count = panelConfigs.size();
|
||||
for (int i = 0; i < count; i++) {
|
||||
final PanelConfig panelConfig = panelConfigs.get(i);
|
||||
|
||||
for (PanelConfig panelConfig : editor) {
|
||||
PanelConfig refreshedPanelConfig = null;
|
||||
|
||||
if (panelConfig.isDynamic()) {
|
||||
for (PanelInfo panelInfo : panelInfos) {
|
||||
if (panelInfo.getId().equals(panelConfig.getId())) {
|
||||
|
@ -290,31 +258,26 @@ public class HomeConfigInvalidator implements GeckoEventListener {
|
|||
|
||||
if (refreshedPanelConfig == null) {
|
||||
Log.d(LOGTAG, "refreshFromPanelInfos: no refreshed panel, falling back: " + panelConfig.getId());
|
||||
refreshedPanelConfig = panelConfig;
|
||||
continue;
|
||||
}
|
||||
|
||||
refreshedPanelConfig.setIsDefault(panelConfig.isDefault());
|
||||
refreshedPanelConfig.setIsDisabled(panelConfig.isDisabled());
|
||||
|
||||
Log.d(LOGTAG, "refreshFromPanelInfos: set " + i + " with " + refreshedPanelConfig.getId());
|
||||
panelConfigs.set(i, refreshedPanelConfig);
|
||||
Log.d(LOGTAG, "refreshFromPanelInfos: refreshed panel " + refreshedPanelConfig.getId());
|
||||
editor.update(refreshedPanelConfig);
|
||||
}
|
||||
|
||||
return panelConfigs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs in the background thread.
|
||||
*/
|
||||
private List<PanelConfig> executeRefresh(List<PanelConfig> panelConfigs) {
|
||||
if (panelConfigs.isEmpty()) {
|
||||
return panelConfigs;
|
||||
private void executeRefresh(HomeConfig.Editor editor) {
|
||||
if (editor.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Log.d(LOGTAG, "executeRefresh");
|
||||
|
||||
final Set<String> ids = new HashSet<String>();
|
||||
for (PanelConfig panelConfig : panelConfigs) {
|
||||
for (PanelConfig panelConfig : editor) {
|
||||
ids.add(panelConfig.getId());
|
||||
}
|
||||
|
||||
|
@ -339,11 +302,10 @@ public class HomeConfigInvalidator implements GeckoEventListener {
|
|||
panelRequestLock.wait(PANEL_INFO_TIMEOUT_MSEC);
|
||||
|
||||
Log.d(LOGTAG, "executeRefresh: done fetching panel infos");
|
||||
return refreshFromPanelInfos(panelConfigs, latestPanelInfos);
|
||||
refreshFromPanelInfos(editor, latestPanelInfos);
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
Log.e(LOGTAG, "Failed to fetch panels from gecko", e);
|
||||
return panelConfigs;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -353,7 +315,9 @@ public class HomeConfigInvalidator implements GeckoEventListener {
|
|||
private class InvalidationRunnable implements Runnable {
|
||||
@Override
|
||||
public void run() {
|
||||
mHomeConfig.save(executePendingChanges(mHomeConfig.load()));
|
||||
final HomeConfig.Editor editor = mHomeConfig.load().edit();
|
||||
executePendingChanges(editor);
|
||||
editor.commit();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -13,9 +13,9 @@ import android.support.v4.content.AsyncTaskLoader;
|
|||
|
||||
import java.util.List;
|
||||
|
||||
public class HomeConfigLoader extends AsyncTaskLoader<List<PanelConfig>> {
|
||||
public class HomeConfigLoader extends AsyncTaskLoader<HomeConfig.State> {
|
||||
private final HomeConfig mConfig;
|
||||
private List<PanelConfig> mPanelConfigs;
|
||||
private HomeConfig.State mConfigState;
|
||||
|
||||
public HomeConfigLoader(Context context, HomeConfig homeConfig) {
|
||||
super(context);
|
||||
|
@ -23,32 +23,32 @@ public class HomeConfigLoader extends AsyncTaskLoader<List<PanelConfig>> {
|
|||
}
|
||||
|
||||
@Override
|
||||
public List<PanelConfig> loadInBackground() {
|
||||
public HomeConfig.State loadInBackground() {
|
||||
return mConfig.load();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deliverResult(List<PanelConfig> panelConfigs) {
|
||||
public void deliverResult(HomeConfig.State configState) {
|
||||
if (isReset()) {
|
||||
mPanelConfigs = null;
|
||||
mConfigState = null;
|
||||
return;
|
||||
}
|
||||
|
||||
mPanelConfigs = panelConfigs;
|
||||
mConfigState = configState;
|
||||
mConfig.setOnChangeListener(new ForceLoadChangeListener());
|
||||
|
||||
if (isStarted()) {
|
||||
super.deliverResult(panelConfigs);
|
||||
super.deliverResult(configState);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStartLoading() {
|
||||
if (mPanelConfigs != null) {
|
||||
deliverResult(mPanelConfigs);
|
||||
if (mConfigState != null) {
|
||||
deliverResult(mConfigState);
|
||||
}
|
||||
|
||||
if (takeContentChanged() || mPanelConfigs == null) {
|
||||
if (takeContentChanged() || mConfigState == null) {
|
||||
forceLoad();
|
||||
}
|
||||
}
|
||||
|
@ -59,8 +59,8 @@ public class HomeConfigLoader extends AsyncTaskLoader<List<PanelConfig>> {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void onCanceled(List<PanelConfig> panelConfigs) {
|
||||
mPanelConfigs = null;
|
||||
public void onCanceled(HomeConfig.State configState) {
|
||||
mConfigState = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -70,7 +70,7 @@ public class HomeConfigLoader extends AsyncTaskLoader<List<PanelConfig>> {
|
|||
// Ensure the loader is stopped.
|
||||
onStopLoading();
|
||||
|
||||
mPanelConfigs = null;
|
||||
mConfigState = null;
|
||||
mConfig.setOnChangeListener(null);
|
||||
}
|
||||
|
||||
|
|
|
@ -250,7 +250,7 @@ public class HomePager extends ViewPager {
|
|||
mHomeBanner.setActive(active);
|
||||
}
|
||||
|
||||
private void updateUiFromPanelConfigs(List<PanelConfig> panelConfigs) {
|
||||
private void updateUiFromConfigState(HomeConfig.State configState) {
|
||||
// We only care about the adapter if HomePager is currently
|
||||
// loaded, which means it's visible in the activity.
|
||||
if (!mLoaded) {
|
||||
|
@ -270,7 +270,7 @@ public class HomePager extends ViewPager {
|
|||
// Only keep enabled panels.
|
||||
final List<PanelConfig> enabledPanels = new ArrayList<PanelConfig>();
|
||||
|
||||
for (PanelConfig panelConfig : panelConfigs) {
|
||||
for (PanelConfig panelConfig : configState) {
|
||||
if (!panelConfig.isDisabled()) {
|
||||
enabledPanels.add(panelConfig);
|
||||
}
|
||||
|
@ -314,19 +314,19 @@ public class HomePager extends ViewPager {
|
|||
}
|
||||
}
|
||||
|
||||
private class ConfigLoaderCallbacks implements LoaderCallbacks<List<PanelConfig>> {
|
||||
private class ConfigLoaderCallbacks implements LoaderCallbacks<HomeConfig.State> {
|
||||
@Override
|
||||
public Loader<List<PanelConfig>> onCreateLoader(int id, Bundle args) {
|
||||
public Loader<HomeConfig.State> onCreateLoader(int id, Bundle args) {
|
||||
return new HomeConfigLoader(mContext, mConfig);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadFinished(Loader<List<PanelConfig>> loader, List<PanelConfig> panelConfigs) {
|
||||
updateUiFromPanelConfigs(panelConfigs);
|
||||
public void onLoadFinished(Loader<HomeConfig.State> loader, HomeConfig.State configState) {
|
||||
updateUiFromConfigState(configState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoaderReset(Loader<List<PanelConfig>> loader) {
|
||||
public void onLoaderReset(Loader<HomeConfig.State> loader) {
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -224,17 +224,17 @@ public class HomePanelPicker extends FragmentActivity {
|
|||
/**
|
||||
* Fetch installed Home panels and update the adapter for this activity.
|
||||
*/
|
||||
private class ConfigLoaderCallbacks implements LoaderCallbacks<List<PanelConfig>> {
|
||||
private class ConfigLoaderCallbacks implements LoaderCallbacks<HomeConfig.State> {
|
||||
@Override
|
||||
public Loader<List<PanelConfig>> onCreateLoader(int id, Bundle args) {
|
||||
public Loader<HomeConfig.State> onCreateLoader(int id, Bundle args) {
|
||||
final HomeConfig homeConfig = HomeConfig.getDefault(HomePanelPicker.this);
|
||||
return new HomeConfigLoader(HomePanelPicker.this, homeConfig);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadFinished(Loader<List<PanelConfig>> loader, List<PanelConfig> panelConfigs) {
|
||||
public void onLoadFinished(Loader<HomeConfig.State> loader, HomeConfig.State configState) {
|
||||
mCurrentPanelsIds = new ArrayList<String>();
|
||||
for (PanelConfig panelConfig : panelConfigs) {
|
||||
for (PanelConfig panelConfig : configState) {
|
||||
mCurrentPanelsIds.add(panelConfig.getId());
|
||||
}
|
||||
|
||||
|
@ -242,6 +242,6 @@ public class HomePanelPicker extends FragmentActivity {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void onLoaderReset(Loader<List<PanelConfig>> loader) {}
|
||||
public void onLoaderReset(Loader<HomeConfig.State> loader) {}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,10 +20,9 @@ public class PanelsPreferenceCategory extends CustomListCategory {
|
|||
public static final String LOGTAG = "PanelsPrefCategory";
|
||||
|
||||
protected HomeConfig mHomeConfig;
|
||||
protected List<PanelConfig> mPanelConfigs;
|
||||
protected HomeConfig.Editor mConfigEditor;
|
||||
|
||||
protected UiAsyncTask<Void, Void, List<PanelConfig>> mLoadTask;
|
||||
protected UiAsyncTask<Void, Void, Void> mSaveTask;
|
||||
protected UiAsyncTask<Void, Void, HomeConfig.State> mLoadTask;
|
||||
|
||||
public PanelsPreferenceCategory(Context context) {
|
||||
super(context);
|
||||
|
@ -55,23 +54,23 @@ public class PanelsPreferenceCategory extends CustomListCategory {
|
|||
* Load the Home Panels config and populate the preferences screen and maintain local state.
|
||||
*/
|
||||
private void loadHomeConfig() {
|
||||
mLoadTask = new UiAsyncTask<Void, Void, List<PanelConfig>>(ThreadUtils.getBackgroundHandler()) {
|
||||
mLoadTask = new UiAsyncTask<Void, Void, HomeConfig.State>(ThreadUtils.getBackgroundHandler()) {
|
||||
@Override
|
||||
public List<PanelConfig> doInBackground(Void... params) {
|
||||
public HomeConfig.State doInBackground(Void... params) {
|
||||
return mHomeConfig.load();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPostExecute(List<PanelConfig> panelConfigs) {
|
||||
mPanelConfigs = panelConfigs;
|
||||
displayHomeConfig();
|
||||
public void onPostExecute(HomeConfig.State configState) {
|
||||
mConfigEditor = configState.edit();
|
||||
displayHomeConfig(configState);
|
||||
}
|
||||
};
|
||||
mLoadTask.execute();
|
||||
}
|
||||
|
||||
private void displayHomeConfig() {
|
||||
for (PanelConfig panelConfig : mPanelConfigs) {
|
||||
private void displayHomeConfig(HomeConfig.State configState) {
|
||||
for (PanelConfig panelConfig : configState) {
|
||||
// Create and add the pref.
|
||||
final PanelsPreference pref = new PanelsPreference(getContext(), PanelsPreferenceCategory.this);
|
||||
pref.setTitle(panelConfig.getTitle());
|
||||
|
@ -79,51 +78,47 @@ public class PanelsPreferenceCategory extends CustomListCategory {
|
|||
// XXX: Pull icon from PanelInfo.
|
||||
addPreference(pref);
|
||||
|
||||
if (panelConfig.isDefault()) {
|
||||
mDefaultReference = pref;
|
||||
pref.setIsDefault(true);
|
||||
}
|
||||
|
||||
if (panelConfig.isDisabled()) {
|
||||
pref.setHidden(true);
|
||||
}
|
||||
}
|
||||
|
||||
setDefaultFromConfig();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update HomeConfig off the main thread.
|
||||
*
|
||||
* @param panelConfigs Configuration to be saved
|
||||
*/
|
||||
private void saveHomeConfig() {
|
||||
if (mPanelConfigs == null) {
|
||||
private void setDefaultFromConfig() {
|
||||
final String defaultPanelId = mConfigEditor.getDefaultPanelId();
|
||||
if (defaultPanelId == null) {
|
||||
mDefaultReference = null;
|
||||
return;
|
||||
}
|
||||
|
||||
final List<PanelConfig> panelConfigs = makeConfigListDeepCopy();
|
||||
mSaveTask = new UiAsyncTask<Void, Void, Void>(ThreadUtils.getBackgroundHandler()) {
|
||||
@Override
|
||||
public Void doInBackground(Void... params) {
|
||||
mHomeConfig.save(panelConfigs);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
mSaveTask.execute();
|
||||
}
|
||||
final int prefCount = getPreferenceCount();
|
||||
|
||||
private List<PanelConfig> makeConfigListDeepCopy() {
|
||||
List<PanelConfig> copiedList = new ArrayList<PanelConfig>();
|
||||
for (PanelConfig panelConfig : mPanelConfigs) {
|
||||
copiedList.add(new PanelConfig(panelConfig));
|
||||
// First preference (index 0) is Preference to add panels.
|
||||
for (int i = 1; i < prefCount; i++) {
|
||||
final PanelsPreference pref = (PanelsPreference) getPreference(i);
|
||||
|
||||
if (defaultPanelId.equals(pref.getKey())) {
|
||||
super.setDefault(pref);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return copiedList;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setDefault(CustomListPreference pref) {
|
||||
super.setDefault(pref);
|
||||
updateConfigDefault();
|
||||
saveHomeConfig();
|
||||
|
||||
final String id = pref.getKey();
|
||||
|
||||
final String defaultPanelId = mConfigEditor.getDefaultPanelId();
|
||||
if (defaultPanelId != null && defaultPanelId.equals(id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
mConfigEditor.setDefault(id);
|
||||
mConfigEditor.apply();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -131,48 +126,14 @@ public class PanelsPreferenceCategory extends CustomListCategory {
|
|||
if (mLoadTask != null) {
|
||||
mLoadTask.cancel(true);
|
||||
}
|
||||
|
||||
if (mSaveTask != null) {
|
||||
mSaveTask.cancel(true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the local HomeConfig default state from mDefaultReference.
|
||||
*/
|
||||
private void updateConfigDefault() {
|
||||
String id = null;
|
||||
if (mDefaultReference != null) {
|
||||
id = mDefaultReference.getKey();
|
||||
}
|
||||
|
||||
for (PanelConfig panelConfig : mPanelConfigs) {
|
||||
if (TextUtils.equals(panelConfig.getId(), id)) {
|
||||
panelConfig.setIsDefault(true);
|
||||
panelConfig.setIsDisabled(false);
|
||||
} else {
|
||||
panelConfig.setIsDefault(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void uninstall(CustomListPreference pref) {
|
||||
mConfigEditor.uninstall(pref.getKey());
|
||||
mConfigEditor.apply();
|
||||
|
||||
super.uninstall(pref);
|
||||
// This could change the default, so update the local version of the config.
|
||||
updateConfigDefault();
|
||||
|
||||
final String id = pref.getKey();
|
||||
PanelConfig toRemove = null;
|
||||
for (PanelConfig panelConfig : mPanelConfigs) {
|
||||
if (TextUtils.equals(panelConfig.getId(), id)) {
|
||||
toRemove = panelConfig;
|
||||
break;
|
||||
}
|
||||
}
|
||||
mPanelConfigs.remove(toRemove);
|
||||
|
||||
saveHomeConfig();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -183,43 +144,11 @@ public class PanelsPreferenceCategory extends CustomListCategory {
|
|||
* @param toHide New hidden state of the preference
|
||||
*/
|
||||
protected void setHidden(PanelsPreference pref, boolean toHide) {
|
||||
mConfigEditor.setDisabled(pref.getKey(), toHide);
|
||||
mConfigEditor.apply();
|
||||
|
||||
pref.setHidden(toHide);
|
||||
ensureDefaultForHide(pref, toHide);
|
||||
|
||||
final String id = pref.getKey();
|
||||
for (PanelConfig panelConfig : mPanelConfigs) {
|
||||
if (TextUtils.equals(panelConfig.getId(), id)) {
|
||||
panelConfig.setIsDisabled(toHide);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
saveHomeConfig();
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure a default is set (if possible) for hiding/showing a pref.
|
||||
* If hiding, try to find an enabled pref to set as the default.
|
||||
* If showing, set it as the default if there is no default currently.
|
||||
*
|
||||
* This updates the local HomeConfig state.
|
||||
*
|
||||
* @param pref Preference getting updated
|
||||
* @param toHide Boolean of the new hidden state
|
||||
*/
|
||||
private void ensureDefaultForHide(PanelsPreference pref, boolean toHide) {
|
||||
if (toHide) {
|
||||
// Set a default if there is an enabled panel left.
|
||||
if (pref == mDefaultReference) {
|
||||
setFallbackDefault();
|
||||
updateConfigDefault();
|
||||
}
|
||||
} else {
|
||||
if (mDefaultReference == null) {
|
||||
super.setDefault(pref);
|
||||
updateConfigDefault();
|
||||
}
|
||||
}
|
||||
setDefaultFromConfig();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -228,15 +157,6 @@ public class PanelsPreferenceCategory extends CustomListCategory {
|
|||
*/
|
||||
@Override
|
||||
protected void setFallbackDefault() {
|
||||
// First preference (index 0) is Preference to add panels.
|
||||
final int prefsCount = getPreferenceCount();
|
||||
for (int i = 1; i < prefsCount; i++) {
|
||||
final PanelsPreference pref = (PanelsPreference) getPreference(i);
|
||||
if (!pref.isHidden()) {
|
||||
super.setDefault(pref);
|
||||
return;
|
||||
}
|
||||
}
|
||||
mDefaultReference = null;
|
||||
setDefaultFromConfig();
|
||||
}
|
||||
}
|
||||
|
|
Двоичные данные
mobile/android/base/resources/drawable-hdpi/progress.9.png
До Ширина: | Высота: | Размер: 811 B После Ширина: | Высота: | Размер: 451 B |
Двоичные данные
mobile/android/base/resources/drawable-mdpi/progress.9.png
До Ширина: | Высота: | Размер: 524 B После Ширина: | Высота: | Размер: 426 B |
Двоичные данные
mobile/android/base/resources/drawable-xhdpi/progress.9.png
До Ширина: | Высота: | Размер: 1.0 KiB После Ширина: | Высота: | Размер: 572 B |
|
@ -107,7 +107,8 @@
|
|||
|
||||
<org.mozilla.gecko.toolbar.ToolbarProgressView android:id="@+id/progress"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="2dp"
|
||||
android:layout_height="14dp"
|
||||
android:layout_marginBottom="-7dp"
|
||||
android:layout_alignBottom="@id/shadow"
|
||||
android:src="@drawable/progress"
|
||||
android:background="@null"
|
||||
|
|
|
@ -107,8 +107,8 @@
|
|||
|
||||
<org.mozilla.gecko.toolbar.ToolbarProgressView android:id="@+id/progress"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="16dp"
|
||||
android:layout_marginBottom="-8dp"
|
||||
android:layout_height="14dp"
|
||||
android:layout_marginBottom="-7dp"
|
||||
android:layout_alignBottom="@id/shadow"
|
||||
android:src="@drawable/progress"
|
||||
android:background="@null"
|
||||
|
|
|
@ -94,6 +94,10 @@
|
|||
android:clipChildren="false"
|
||||
android:focusable="true">
|
||||
|
||||
<!-- clipChildren="false" allows the child ToolbarProgressView to be drawn
|
||||
outside of BrowserToolbar's boundaries. Likewise, we need this property
|
||||
on BrowserToolbar's parent ViewFlipper, then on its parent MainLayout
|
||||
to allow the progress to overlap the content LayerView. -->
|
||||
<org.mozilla.gecko.toolbar.BrowserToolbar
|
||||
android:id="@+id/browser_toolbar"
|
||||
style="@style/BrowserToolbar"
|
||||
|
|
|
@ -199,7 +199,7 @@ this.HawkClient.prototype = {
|
|||
localtimeOffsetMsec: this.localtimeOffsetMsec,
|
||||
};
|
||||
|
||||
let request = new HAWKAuthenticatedRESTRequest(uri, credentials, extra);
|
||||
let request = this.newHAWKAuthenticatedRESTRequest(uri, credentials, extra);
|
||||
if (method == "post" || method == "put") {
|
||||
request[method](payloadObj, onComplete);
|
||||
} else {
|
||||
|
@ -207,5 +207,10 @@ this.HawkClient.prototype = {
|
|||
}
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
},
|
||||
|
||||
// override points for testing.
|
||||
newHAWKAuthenticatedRESTRequest: function(uri, credentials, extra) {
|
||||
return new HAWKAuthenticatedRESTRequest(uri, credentials, extra);
|
||||
},
|
||||
}
|
||||
|
|
|
@ -245,7 +245,7 @@ TokenServerClient.prototype = {
|
|||
|
||||
this._log.debug("Beginning BID assertion exchange: " + url);
|
||||
|
||||
let req = new RESTRequest(url);
|
||||
let req = this.newRESTRequest(url);
|
||||
req.setHeader("Accept", "application/json");
|
||||
req.setHeader("Authorization", "BrowserID " + assertion);
|
||||
|
||||
|
@ -403,5 +403,10 @@ TokenServerClient.prototype = {
|
|||
uid: result.uid,
|
||||
duration: result.duration,
|
||||
});
|
||||
},
|
||||
|
||||
// override points for testing.
|
||||
newRESTRequest: function(url) {
|
||||
return new RESTRequest(url);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -117,6 +117,9 @@ this.configureFxAccountIdentity = function(authService,
|
|||
let MockInternal = {};
|
||||
let fxa = new FxAccounts(MockInternal);
|
||||
|
||||
// until we get better test infrastructure for bid_identity, we set the
|
||||
// signedin user's "email" to the username, simply as many tests rely on this.
|
||||
config.fxaccount.user.email = config.username;
|
||||
fxa.internal.currentAccountState.signedInUser = {
|
||||
version: DATA_FORMAT_VERSION,
|
||||
accountData: config.fxaccount.user
|
||||
|
@ -139,6 +142,7 @@ this.configureFxAccountIdentity = function(authService,
|
|||
authService._tokenServerClient = mockTSC;
|
||||
// Set the "account" of the browserId manager to be the "email" of the
|
||||
// logged in user of the mockFXA service.
|
||||
authService._signedInUser = fxa.internal.currentAccountState.signedInUser.accountData;
|
||||
authService._account = config.fxaccount.user.email;
|
||||
}
|
||||
|
||||
|
|
|
@ -23,18 +23,15 @@ Cu.import("resource://services-sync/stages/cluster.js");
|
|||
Cu.import("resource://gre/modules/FxAccounts.jsm");
|
||||
|
||||
// Lazy imports to prevent unnecessary load on startup.
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "Weave",
|
||||
"resource://services-sync/main.js");
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "BulkKeyBundle",
|
||||
"resource://services-sync/keys.js");
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "fxAccounts",
|
||||
"resource://gre/modules/FxAccounts.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyGetter(this, 'fxAccountsCommon', function() {
|
||||
let ob = {};
|
||||
Cu.import("resource://gre/modules/FxAccountsCommon.js", ob);
|
||||
return ob;
|
||||
});
|
||||
|
||||
XPCOMUtils.defineLazyGetter(this, 'log', function() {
|
||||
let log = Log.repository.getLogger("Sync.BrowserIDManager");
|
||||
log.addAppender(new Log.DumpAppender());
|
||||
|
@ -42,6 +39,15 @@ XPCOMUtils.defineLazyGetter(this, 'log', function() {
|
|||
return log;
|
||||
});
|
||||
|
||||
// FxAccountsCommon.js doesn't use a "namespace", so create one here.
|
||||
let fxAccountsCommon = {};
|
||||
Cu.import("resource://gre/modules/FxAccountsCommon.js", fxAccountsCommon);
|
||||
|
||||
const OBSERVER_TOPICS = [
|
||||
fxAccountsCommon.ONLOGIN_NOTIFICATION,
|
||||
fxAccountsCommon.ONLOGOUT_NOTIFICATION,
|
||||
];
|
||||
|
||||
const PREF_SYNC_SHOW_CUSTOMIZATION = "services.sync.ui.showCustomizationDialog";
|
||||
|
||||
function deriveKeyBundle(kB) {
|
||||
|
@ -87,7 +93,12 @@ this.BrowserIDManager.prototype = {
|
|||
_tokenServerClient: null,
|
||||
// https://docs.services.mozilla.com/token/apis.html
|
||||
_token: null,
|
||||
_account: null,
|
||||
_signedInUser: null, // the signedinuser we got from FxAccounts.
|
||||
|
||||
// null if no error, otherwise a LOGIN_FAILED_* value that indicates why
|
||||
// we failed to authenticate (but note it might not be an actual
|
||||
// authentication problem, just a transient network error or similar)
|
||||
_authFailureReason: null,
|
||||
|
||||
// it takes some time to fetch a sync key bundle, so until this flag is set,
|
||||
// we don't consider the lack of a keybundle as a failure state.
|
||||
|
@ -108,34 +119,79 @@ this.BrowserIDManager.prototype = {
|
|||
},
|
||||
|
||||
initialize: function() {
|
||||
Services.obs.addObserver(this, fxAccountsCommon.ONLOGIN_NOTIFICATION, false);
|
||||
Services.obs.addObserver(this, fxAccountsCommon.ONLOGOUT_NOTIFICATION, false);
|
||||
Services.obs.addObserver(this, "weave:service:logout:finish", false);
|
||||
for (let topic of OBSERVER_TOPICS) {
|
||||
Services.obs.addObserver(this, topic, false);
|
||||
}
|
||||
return this.initializeWithCurrentIdentity();
|
||||
},
|
||||
|
||||
/**
|
||||
* Ensure the user is logged in. Returns a promise that resolves when
|
||||
* the user is logged in, or is rejected if the login attempt has failed.
|
||||
*/
|
||||
ensureLoggedIn: function() {
|
||||
if (!this._shouldHaveSyncKeyBundle) {
|
||||
// We are already in the process of logging in.
|
||||
return this.whenReadyToAuthenticate.promise;
|
||||
}
|
||||
|
||||
// If we are already happy then there is nothing more to do.
|
||||
if (Weave.Status.login == LOGIN_SUCCEEDED) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
// Similarly, if we have a previous failure that implies an explicit
|
||||
// re-entering of credentials by the user is necessary we don't take any
|
||||
// further action - an observer will fire when the user does that.
|
||||
if (Weave.Status.login == LOGIN_FAILED_LOGIN_REJECTED) {
|
||||
return Promise.reject();
|
||||
}
|
||||
|
||||
// So - we've a previous auth problem and aren't currently attempting to
|
||||
// log in - so fire that off.
|
||||
this.initializeWithCurrentIdentity();
|
||||
return this.whenReadyToAuthenticate.promise;
|
||||
},
|
||||
|
||||
finalize: function() {
|
||||
// After this is called, we can expect Service.identity != this.
|
||||
for (let topic of OBSERVER_TOPICS) {
|
||||
Services.obs.removeObserver(this, topic);
|
||||
}
|
||||
this.resetCredentials();
|
||||
this._signedInUser = null;
|
||||
return Promise.resolve();
|
||||
},
|
||||
|
||||
initializeWithCurrentIdentity: function(isInitialSync=false) {
|
||||
// While this function returns a promise that resolves once we've started
|
||||
// the auth process, that process is complete when
|
||||
// this.whenReadyToAuthenticate.promise resolves.
|
||||
this._log.trace("initializeWithCurrentIdentity");
|
||||
Components.utils.import("resource://services-sync/main.js");
|
||||
|
||||
// Reset the world before we do anything async.
|
||||
this.whenReadyToAuthenticate = Promise.defer();
|
||||
this._shouldHaveSyncKeyBundle = false;
|
||||
this._authFailureReason = null;
|
||||
|
||||
return this._fxaService.getSignedInUser().then(accountData => {
|
||||
if (!accountData) {
|
||||
this._log.info("initializeWithCurrentIdentity has no user logged in");
|
||||
this._account = null;
|
||||
this.account = null;
|
||||
// and we are as ready as we can ever be for auth.
|
||||
this._shouldHaveSyncKeyBundle = true;
|
||||
this.whenReadyToAuthenticate.reject("no user is logged in");
|
||||
return;
|
||||
}
|
||||
|
||||
this._account = accountData.email;
|
||||
this.account = accountData.email;
|
||||
this._updateSignedInUser(accountData);
|
||||
// The user must be verified before we can do anything at all; we kick
|
||||
// this and the rest of initialization off in the background (ie, we
|
||||
// don't return the promise)
|
||||
this._log.info("Waiting for user to be verified.");
|
||||
this._fxaService.whenVerified(accountData).then(accountData => {
|
||||
// We do the background keybundle fetch...
|
||||
this._updateSignedInUser(accountData);
|
||||
this._log.info("Starting fetch for key bundle.");
|
||||
if (this.needsCustomization) {
|
||||
// If the user chose to "Customize sync options" when signing
|
||||
|
@ -160,6 +216,7 @@ this.BrowserIDManager.prototype = {
|
|||
this._shouldHaveSyncKeyBundle = true; // and we should actually have one...
|
||||
this.whenReadyToAuthenticate.resolve();
|
||||
this._log.info("Background fetch for key bundle done");
|
||||
Weave.Status.login = LOGIN_SUCCEEDED;
|
||||
if (isInitialSync) {
|
||||
this._log.info("Doing initial sync actions");
|
||||
Svc.Prefs.set("firstSync", "resetClient");
|
||||
|
@ -178,28 +235,45 @@ this.BrowserIDManager.prototype = {
|
|||
});
|
||||
},
|
||||
|
||||
_updateSignedInUser: function(userData) {
|
||||
// This object should only ever be used for a single user. It is an
|
||||
// error to update the data if the user changes (but updates are still
|
||||
// necessary, as each call may add more attributes to the user).
|
||||
// We start with no user, so an initial update is always ok.
|
||||
if (this._signedInUser && this._signedInUser.email != userData.email) {
|
||||
throw new Error("Attempting to update to a different user.")
|
||||
}
|
||||
this._signedInUser = userData;
|
||||
},
|
||||
|
||||
logout: function() {
|
||||
// This will be called when sync fails (or when the account is being
|
||||
// unlinked etc). It may have failed because we got a 401 from a sync
|
||||
// server, so we nuke the token. Next time sync runs and wants an
|
||||
// authentication header, we will notice the lack of the token and fetch a
|
||||
// new one.
|
||||
this._token = null;
|
||||
},
|
||||
|
||||
observe: function (subject, topic, data) {
|
||||
this._log.debug("observed " + topic);
|
||||
switch (topic) {
|
||||
case fxAccountsCommon.ONLOGIN_NOTIFICATION:
|
||||
// This should only happen if we've been initialized without a current
|
||||
// user - otherwise we'd have seen the LOGOUT notification and been
|
||||
// thrown away.
|
||||
// The exception is when we've initialized with a user that needs to
|
||||
// reauth with the server - in that case we will also get here, but
|
||||
// should have the same identity.
|
||||
// initializeWithCurrentIdentity will throw and log if these contraints
|
||||
// aren't met, so just go ahead and do the init.
|
||||
this.initializeWithCurrentIdentity(true);
|
||||
break;
|
||||
|
||||
case fxAccountsCommon.ONLOGOUT_NOTIFICATION:
|
||||
Components.utils.import("resource://services-sync/main.js");
|
||||
// Setting .username calls resetCredentials which drops the key bundle
|
||||
// and resets _shouldHaveSyncKeyBundle.
|
||||
this.username = "";
|
||||
this._account = null;
|
||||
Weave.Service.logout();
|
||||
break;
|
||||
|
||||
case "weave:service:logout:finish":
|
||||
// This signals an auth error with the storage server,
|
||||
// or that the user unlinked her account from the browser.
|
||||
// Either way, we clear our auth token. In the case of an
|
||||
// auth error, this will force the fetch of a new one.
|
||||
this._token = null;
|
||||
Weave.Service.startOver();
|
||||
// startOver will cause this instance to be thrown away, so there's
|
||||
// nothing else to do.
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
@ -234,23 +308,9 @@ this.BrowserIDManager.prototype = {
|
|||
return this._fxaService.localtimeOffsetMsec;
|
||||
},
|
||||
|
||||
get account() {
|
||||
return this._account;
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets the active account name.
|
||||
*
|
||||
* This should almost always be called in favor of setting username, as
|
||||
* username is derived from account.
|
||||
*
|
||||
* Changing the account name has the side-effect of wiping out stored
|
||||
* credentials.
|
||||
*
|
||||
* Set this value to null to clear out identity information.
|
||||
*/
|
||||
set account(value) {
|
||||
throw "account setter should be not used in BrowserIDManager";
|
||||
usernameFromAccount: function(val) {
|
||||
// we don't differentiate between "username" and "account"
|
||||
return val;
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -308,8 +368,8 @@ this.BrowserIDManager.prototype = {
|
|||
* Resets/Drops all credentials we hold for the current user.
|
||||
*/
|
||||
resetCredentials: function() {
|
||||
// the only credentials we hold are the sync key.
|
||||
this.resetSyncKey();
|
||||
this._token = null;
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -329,6 +389,11 @@ this.BrowserIDManager.prototype = {
|
|||
* Sync.
|
||||
*/
|
||||
get currentAuthState() {
|
||||
if (this._authFailureReason) {
|
||||
this._log.info("currentAuthState returning " + this._authFailureReason +
|
||||
" due to previous failure");
|
||||
return this._authFailureReason;
|
||||
}
|
||||
// TODO: need to revisit this. Currently this isn't ready to go until
|
||||
// both the username and syncKeyBundle are both configured and having no
|
||||
// username seems to make things fail fast so that's good.
|
||||
|
@ -347,8 +412,8 @@ this.BrowserIDManager.prototype = {
|
|||
},
|
||||
|
||||
/**
|
||||
* Do we have a non-null, not yet expired token whose email field
|
||||
* matches (when normalized) our account field?
|
||||
* Do we have a non-null, not yet expired token for the user currently
|
||||
* signed in?
|
||||
*/
|
||||
hasValidToken: function() {
|
||||
if (!this._token) {
|
||||
|
@ -357,56 +422,15 @@ this.BrowserIDManager.prototype = {
|
|||
if (this._token.expiration < this._now()) {
|
||||
return false;
|
||||
}
|
||||
let signedInUser = this._getSignedInUser();
|
||||
if (!signedInUser) {
|
||||
return false;
|
||||
}
|
||||
// Does the signed in user match the user we retrieved the token for?
|
||||
if (signedInUser.email !== this.account) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Wrap and synchronize FxAccounts.getSignedInUser().
|
||||
*
|
||||
* @return credentials per wrapped.
|
||||
*/
|
||||
_getSignedInUser: function() {
|
||||
let userData;
|
||||
let cb = Async.makeSpinningCallback();
|
||||
|
||||
this._fxaService.getSignedInUser().then(function (result) {
|
||||
cb(null, result);
|
||||
},
|
||||
function (err) {
|
||||
cb(err);
|
||||
});
|
||||
|
||||
try {
|
||||
userData = cb.wait();
|
||||
} catch (err) {
|
||||
this._log.error("FxAccounts.getSignedInUser() failed with: " + err);
|
||||
return null;
|
||||
}
|
||||
return userData;
|
||||
},
|
||||
|
||||
_fetchSyncKeyBundle: function() {
|
||||
// Fetch a sync token for the logged in user from the token server.
|
||||
return this._fxaService.getKeys().then(userData => {
|
||||
// Unlikely, but if the logged in user somehow changed between these
|
||||
// calls we better fail. TODO: add tests for these
|
||||
if (!userData) {
|
||||
throw new AuthenticationError("No userData in _fetchSyncKeyBundle");
|
||||
} else if (userData.email !== this.account) {
|
||||
throw new AuthenticationError("Unexpected user change in _fetchSyncKeyBundle");
|
||||
}
|
||||
return this._fetchTokenForUser(userData).then(token => {
|
||||
this._updateSignedInUser(userData); // throws if the user changed.
|
||||
return this._fetchTokenForUser().then(token => {
|
||||
this._token = token;
|
||||
// Set the username to be the uid returned by the token server.
|
||||
this.username = this._token.uid.toString();
|
||||
// both Jelly and FxAccounts give us kA/kB as hex.
|
||||
let kB = Utils.hexToBytes(userData.kB);
|
||||
this._syncKeyBundle = deriveKeyBundle(kB);
|
||||
|
@ -415,12 +439,13 @@ this.BrowserIDManager.prototype = {
|
|||
});
|
||||
},
|
||||
|
||||
// Refresh the sync token for the specified Firefox Accounts user.
|
||||
_fetchTokenForUser: function(userData) {
|
||||
// Refresh the sync token for our user.
|
||||
_fetchTokenForUser: function() {
|
||||
let tokenServerURI = Svc.Prefs.get("tokenServerURI");
|
||||
let log = this._log;
|
||||
let client = this._tokenServerClient;
|
||||
let fxa = this._fxaService;
|
||||
let userData = this._signedInUser;
|
||||
|
||||
// Both Jelly and FxAccounts give us kB as hex
|
||||
let kBbytes = CommonUtils.hexToBytes(userData.kB);
|
||||
|
@ -432,8 +457,11 @@ this.BrowserIDManager.prototype = {
|
|||
let deferred = Promise.defer();
|
||||
let cb = function (err, token) {
|
||||
if (err) {
|
||||
log.info("TokenServerClient.getTokenFromBrowserIDAssertion() failed with: " + err.message);
|
||||
return deferred.reject(new AuthenticationError(err));
|
||||
log.info("TokenServerClient.getTokenFromBrowserIDAssertion() failed with: " + err);
|
||||
if (err.response && err.response.status === 401) {
|
||||
err = new AuthenticationError(err);
|
||||
}
|
||||
return deferred.reject(err);
|
||||
} else {
|
||||
log.debug("Successfully got a sync token");
|
||||
return deferred.resolve(token);
|
||||
|
@ -448,6 +476,7 @@ this.BrowserIDManager.prototype = {
|
|||
log.debug("Getting an assertion");
|
||||
let audience = Services.io.newURI(tokenServerURI, null, null).prePath;
|
||||
return fxa.getAssertion(audience).then(null, err => {
|
||||
log.error("fxa.getAssertion() failed with: " + err.code + " - " + err.message);
|
||||
if (err.code === 401) {
|
||||
throw new AuthenticationError("Unable to get assertion for user");
|
||||
} else {
|
||||
|
@ -458,7 +487,7 @@ this.BrowserIDManager.prototype = {
|
|||
|
||||
// wait until the account email is verified and we know that
|
||||
// getAssertion() will return a real assertion (not null).
|
||||
return fxa.whenVerified(userData)
|
||||
return fxa.whenVerified(this._signedInUser)
|
||||
.then(() => getAssertion())
|
||||
.then(assertion => getToken(tokenServerURI, assertion))
|
||||
.then(token => {
|
||||
|
@ -474,34 +503,34 @@ this.BrowserIDManager.prototype = {
|
|||
// and client-state error)
|
||||
if (err instanceof AuthenticationError) {
|
||||
this._log.error("Authentication error in _fetchTokenForUser: " + err);
|
||||
// set it to the "fatal" LOGIN_FAILED_LOGIN_REJECTED reason.
|
||||
this._authFailureReason = LOGIN_FAILED_LOGIN_REJECTED;
|
||||
} else {
|
||||
this._log.error("Non-authentication error in _fetchTokenForUser: " + err.message);
|
||||
// for now assume it is just a transient network related problem.
|
||||
this._authFailureReason = LOGIN_FAILED_NETWORK_ERROR;
|
||||
}
|
||||
// Drop the sync key bundle, but still expect to have one.
|
||||
// This will arrange for us to be in the right 'currentAuthState'
|
||||
// such that UI will show the right error.
|
||||
this._shouldHaveSyncKeyBundle = true;
|
||||
this._syncKeyBundle = null;
|
||||
Weave.Status.login = this.currentAuthState;
|
||||
Services.obs.notifyObservers(null, "weave:service:login:error", null);
|
||||
}
|
||||
Weave.Status.login = this._authFailureReason;
|
||||
throw err;
|
||||
});
|
||||
},
|
||||
|
||||
_fetchTokenForLoggedInUserSync: function() {
|
||||
let cb = Async.makeSpinningCallback();
|
||||
|
||||
this._fxaService.getSignedInUser().then(userData => {
|
||||
this._fetchTokenForUser(userData).then(token => {
|
||||
cb(null, token);
|
||||
}, err => {
|
||||
cb(err);
|
||||
});
|
||||
});
|
||||
try {
|
||||
return cb.wait();
|
||||
} catch (err) {
|
||||
this._log.info("_fetchTokenForLoggedInUserSync: " + err.message);
|
||||
return null;
|
||||
// Returns a promise that is resolved when we have a valid token for the
|
||||
// current user stored in this._token. When resolved, this._token is valid.
|
||||
_ensureValidToken: function() {
|
||||
if (this.hasValidToken()) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return this._fetchTokenForUser().then(
|
||||
token => {
|
||||
this._token = token;
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
getResourceAuthenticator: function () {
|
||||
|
@ -520,12 +549,16 @@ this.BrowserIDManager.prototype = {
|
|||
* of a RESTRequest or AsyncResponse object.
|
||||
*/
|
||||
_getAuthenticationHeader: function(httpObject, method) {
|
||||
if (!this.hasValidToken()) {
|
||||
// Refresh token for the currently logged in FxA user
|
||||
this._token = this._fetchTokenForLoggedInUserSync();
|
||||
if (!this._token) {
|
||||
let cb = Async.makeSpinningCallback();
|
||||
this._ensureValidToken().then(cb, cb);
|
||||
try {
|
||||
cb.wait();
|
||||
} catch (ex) {
|
||||
this._log.error("Failed to fetch a token for authentication: " + ex);
|
||||
return null;
|
||||
}
|
||||
if (!this._token) {
|
||||
return null;
|
||||
}
|
||||
let credentials = {algorithm: "sha256",
|
||||
id: this._token.id,
|
||||
|
@ -571,11 +604,8 @@ BrowserIDClusterManager.prototype = {
|
|||
__proto__: ClusterManager.prototype,
|
||||
|
||||
_findCluster: function() {
|
||||
let fxa = this.identity._fxaService; // will be mocked for tests.
|
||||
let promiseClusterURL = function() {
|
||||
return fxa.getSignedInUser().then(userData => {
|
||||
return this.identity._fetchTokenForUser(userData).then(token => {
|
||||
let endpoint = token.endpoint;
|
||||
let endPointFromIdentityToken = function() {
|
||||
let endpoint = this.identity._token.endpoint;
|
||||
// For Sync 1.5 storage endpoints, we use the base endpoint verbatim.
|
||||
// However, it should end in "/" because we will extend it with
|
||||
// well known path components. So we add a "/" if it's missing.
|
||||
|
@ -583,17 +613,21 @@ BrowserIDClusterManager.prototype = {
|
|||
endpoint += "/";
|
||||
}
|
||||
return endpoint;
|
||||
});
|
||||
});
|
||||
}.bind(this);
|
||||
|
||||
// Spinningly ensure we are ready to authenticate and have a valid token.
|
||||
let promiseClusterURL = function() {
|
||||
return this.identity.whenReadyToAuthenticate.promise.then(
|
||||
() => this.identity._ensureValidToken()
|
||||
).then(
|
||||
() => endPointFromIdentityToken()
|
||||
);
|
||||
}.bind(this);
|
||||
|
||||
let cb = Async.makeSpinningCallback();
|
||||
promiseClusterURL().then(function (clusterURL) {
|
||||
cb(null, clusterURL);
|
||||
},
|
||||
function (err) {
|
||||
cb(err);
|
||||
});
|
||||
}).then(null, cb);
|
||||
return cb.wait();
|
||||
},
|
||||
|
||||
|
|
|
@ -54,11 +54,6 @@ HMAC_EVENT_INTERVAL: 600000,
|
|||
// How long to wait between sync attempts if the Master Password is locked.
|
||||
MASTER_PASSWORD_LOCKED_RETRY_INTERVAL: 15 * 60 * 1000, // 15 minutes
|
||||
|
||||
// How long to initially wait between sync attempts if the identity manager is
|
||||
// not ready. As we expect this to become ready relatively quickly, we retry
|
||||
// in (IDENTITY_NOT_READY_RETRY_INTERVAL * num_failures) seconds.
|
||||
IDENTITY_NOT_READY_RETRY_INTERVAL: 5 * 1000, // 5 seconds
|
||||
|
||||
// Separate from the ID fetch batch size to allow tuning for mobile.
|
||||
MOBILE_BATCH_SIZE: 50,
|
||||
|
||||
|
|
|
@ -89,6 +89,27 @@ IdentityManager.prototype = {
|
|||
* its state
|
||||
*/
|
||||
initialize: function() {
|
||||
// Nothing to do for this identity provider.
|
||||
return Promise.resolve();
|
||||
},
|
||||
|
||||
finalize: function() {
|
||||
// Nothing to do for this identity provider.
|
||||
return Promise.resolve();
|
||||
},
|
||||
|
||||
/**
|
||||
* Called whenever Service.logout() is called.
|
||||
*/
|
||||
logout: function() {
|
||||
// nothing to do for this identity provider.
|
||||
},
|
||||
|
||||
/**
|
||||
* Ensure the user is logged in. Returns a promise that resolves when
|
||||
* the user is logged in, or is rejected if the login attempt has failed.
|
||||
*/
|
||||
ensureLoggedIn: function() {
|
||||
// nothing to do for this identity provider
|
||||
return Promise.resolve();
|
||||
},
|
||||
|
|
|
@ -30,8 +30,6 @@ SyncScheduler.prototype = {
|
|||
LOGIN_FAILED_INVALID_PASSPHRASE,
|
||||
LOGIN_FAILED_LOGIN_REJECTED],
|
||||
|
||||
_loginNotReadyCounter: 0,
|
||||
|
||||
/**
|
||||
* The nsITimer object that schedules the next sync. See scheduleNextSync().
|
||||
*/
|
||||
|
@ -115,10 +113,6 @@ SyncScheduler.prototype = {
|
|||
// we'll handle that later
|
||||
Status.resetBackoff();
|
||||
|
||||
// Reset the loginNotReady counter, just in-case the user signs in
|
||||
// as another user and re-hits the not-ready state.
|
||||
this._loginNotReadyCounter = 0;
|
||||
|
||||
this.globalScore = 0;
|
||||
break;
|
||||
case "weave:service:sync:finish":
|
||||
|
@ -161,13 +155,6 @@ SyncScheduler.prototype = {
|
|||
this._log.debug("Couldn't log in: master password is locked.");
|
||||
this._log.trace("Scheduling a sync at MASTER_PASSWORD_LOCKED_RETRY_INTERVAL");
|
||||
this.scheduleAtInterval(MASTER_PASSWORD_LOCKED_RETRY_INTERVAL);
|
||||
} else if (Status.login == LOGIN_FAILED_NOT_READY) {
|
||||
this._loginNotReadyCounter++;
|
||||
this._log.debug("Couldn't log in: identity not ready.");
|
||||
this._log.trace("Scheduling a sync at IDENTITY_NOT_READY_RETRY_INTERVAL * " +
|
||||
this._loginNotReadyCounter);
|
||||
this.scheduleAtInterval(IDENTITY_NOT_READY_RETRY_INTERVAL *
|
||||
this._loginNotReadyCounter);
|
||||
} else if (this._fatalLoginStatus.indexOf(Status.login) == -1) {
|
||||
// Not a fatal login error, just an intermittent network or server
|
||||
// issue. Keep on syncin'.
|
||||
|
|
|
@ -164,7 +164,7 @@ Sync11Service.prototype = {
|
|||
|
||||
_updateCachedURLs: function _updateCachedURLs() {
|
||||
// Nothing to cache yet if we don't have the building blocks
|
||||
if (this.clusterURL == "" || this.identity.username == "")
|
||||
if (!this.clusterURL || !this.identity.username)
|
||||
return;
|
||||
|
||||
this._log.debug("Caching URLs under storage user base: " + this.userBaseURL);
|
||||
|
@ -852,14 +852,6 @@ Sync11Service.prototype = {
|
|||
Svc.Obs.notify("weave:engine:stop-tracking");
|
||||
this.status.resetSync();
|
||||
|
||||
// We want let UI consumers of the following notification know as soon as
|
||||
// possible, so let's fake for the CLIENT_NOT_CONFIGURED status for now
|
||||
// by emptying the passphrase (we still need the password).
|
||||
this.identity.resetSyncKey();
|
||||
this.status.login = LOGIN_FAILED_NO_PASSPHRASE;
|
||||
this.logout();
|
||||
Svc.Obs.notify("weave:service:start-over");
|
||||
|
||||
// Deletion doesn't make sense if we aren't set up yet!
|
||||
if (this.clusterURL != "") {
|
||||
// Clear client-specific data from the server, including disabled engines.
|
||||
|
@ -871,10 +863,20 @@ Sync11Service.prototype = {
|
|||
+ Utils.exceptionStr(ex));
|
||||
}
|
||||
}
|
||||
this._log.debug("Finished deleting client data.");
|
||||
} else {
|
||||
this._log.debug("Skipping client data removal: no cluster URL.");
|
||||
}
|
||||
|
||||
// We want let UI consumers of the following notification know as soon as
|
||||
// possible, so let's fake for the CLIENT_NOT_CONFIGURED status for now
|
||||
// by emptying the passphrase (we still need the password).
|
||||
this._log.info("Service.startOver dropping sync key and logging out.");
|
||||
this.identity.resetSyncKey();
|
||||
this.status.login = LOGIN_FAILED_NO_PASSPHRASE;
|
||||
this.logout();
|
||||
Svc.Obs.notify("weave:service:start-over");
|
||||
|
||||
// Reset all engines and clear keys.
|
||||
this.resetClient();
|
||||
this.collectionKeys.clear();
|
||||
|
@ -900,21 +902,23 @@ Sync11Service.prototype = {
|
|||
return;
|
||||
}
|
||||
|
||||
this.identity.finalize().then(
|
||||
() => {
|
||||
this.identity.username = "";
|
||||
Services.prefs.clearUserPref("services.sync.fxaccounts.enabled");
|
||||
this.status.__authManager = null;
|
||||
this.identity = Status._authManager;
|
||||
this._clusterManager = this.identity.createClusterManager(this);
|
||||
|
||||
// Tell the new identity manager to initialize itself
|
||||
this.identity.initialize().then(() => {
|
||||
Svc.Obs.notify("weave:service:start-over:finish");
|
||||
}).then(null, err => {
|
||||
}
|
||||
).then(null,
|
||||
err => {
|
||||
this._log.error("startOver failed to re-initialize the identity manager: " + err);
|
||||
// Still send the observer notification so the current state is
|
||||
// reflected in the UI.
|
||||
Svc.Obs.notify("weave:service:start-over:finish");
|
||||
});
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
persistLogin: function persistLogin() {
|
||||
|
@ -948,6 +952,13 @@ Sync11Service.prototype = {
|
|||
throw "Aborting login, client not configured.";
|
||||
}
|
||||
|
||||
// Ask the identity manager to explicitly login now.
|
||||
let cb = Async.makeSpinningCallback();
|
||||
this.identity.ensureLoggedIn().then(cb, cb);
|
||||
|
||||
// Just let any errors bubble up - they've more context than we do!
|
||||
cb.wait();
|
||||
|
||||
// Calling login() with parameters when the client was
|
||||
// previously not configured means setup was completed.
|
||||
if (initialStatus == CLIENT_NOT_CONFIGURED
|
||||
|
@ -978,6 +989,7 @@ Sync11Service.prototype = {
|
|||
return;
|
||||
|
||||
this._log.info("Logging out");
|
||||
this.identity.logout();
|
||||
this._loggedIn = false;
|
||||
|
||||
Svc.Obs.notify("weave:service:logout:finish");
|
||||
|
|
|
@ -12,6 +12,9 @@ Cu.import("resource://services-common/hawkclient.js");
|
|||
Cu.import("resource://gre/modules/FxAccounts.jsm");
|
||||
Cu.import("resource://gre/modules/FxAccountsClient.jsm");
|
||||
Cu.import("resource://gre/modules/FxAccountsCommon.js");
|
||||
Cu.import("resource://services-common/tokenserverclient.js");
|
||||
Cu.import("resource://services-sync/status.js");
|
||||
Cu.import("resource://services-sync/constants.js");
|
||||
|
||||
const SECOND_MS = 1000;
|
||||
const MINUTE_MS = SECOND_MS * 60;
|
||||
|
@ -57,6 +60,7 @@ function MockFxAccounts() {
|
|||
function run_test() {
|
||||
initTestLogging("Trace");
|
||||
Log.repository.getLogger("Sync.Identity").level = Log.Level.Trace;
|
||||
Log.repository.getLogger("Sync.BrowserIDManager").level = Log.Level.Trace;
|
||||
run_next_test();
|
||||
};
|
||||
|
||||
|
@ -68,6 +72,17 @@ add_test(function test_initial_state() {
|
|||
}
|
||||
);
|
||||
|
||||
add_task(function test_initialializeWithCurrentIdentity() {
|
||||
_("Verify start after initializeWithCurrentIdentity");
|
||||
browseridManager.initializeWithCurrentIdentity();
|
||||
yield browseridManager.whenReadyToAuthenticate.promise;
|
||||
do_check_true(!!browseridManager._token);
|
||||
do_check_true(browseridManager.hasValidToken());
|
||||
do_check_eq(browseridManager.account, identityConfig.fxaccount.user.email);
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
add_test(function test_getResourceAuthenticator() {
|
||||
_("BrowserIDManager supplies a Resource Authenticator callback which returns a Hawk header.");
|
||||
let authenticator = browseridManager.getResourceAuthenticator();
|
||||
|
@ -81,7 +96,6 @@ add_test(function test_getResourceAuthenticator() {
|
|||
do_check_true(output.headers.authorization.startsWith('Hawk'));
|
||||
_("Expected internal state after successful call.");
|
||||
do_check_eq(browseridManager._token.uid, identityConfig.fxaccount.token.uid);
|
||||
do_check_eq(browseridManager.account, identityConfig.fxaccount.user.email);
|
||||
run_next_test();
|
||||
}
|
||||
);
|
||||
|
@ -222,6 +236,44 @@ add_test(function test_RESTResourceAuthenticatorSkew() {
|
|||
run_next_test();
|
||||
});
|
||||
|
||||
add_task(function test_ensureLoggedIn() {
|
||||
configureFxAccountIdentity(browseridManager);
|
||||
yield browseridManager.initializeWithCurrentIdentity();
|
||||
Assert.equal(Status.login, LOGIN_SUCCEEDED, "original initialize worked");
|
||||
yield browseridManager.ensureLoggedIn();
|
||||
Assert.equal(Status.login, LOGIN_SUCCEEDED, "original ensureLoggedIn worked");
|
||||
Assert.ok(browseridManager._shouldHaveSyncKeyBundle,
|
||||
"_shouldHaveSyncKeyBundle should always be true after ensureLogin completes.");
|
||||
|
||||
// arrange for no logged in user.
|
||||
let fxa = browseridManager._fxaService
|
||||
let signedInUser = fxa.internal.currentAccountState.signedInUser;
|
||||
fxa.internal.currentAccountState.signedInUser = null;
|
||||
browseridManager.initializeWithCurrentIdentity();
|
||||
Assert.ok(!browseridManager._shouldHaveSyncKeyBundle,
|
||||
"_shouldHaveSyncKeyBundle should be false so we know we are testing what we think we are.");
|
||||
Status.login = LOGIN_FAILED_NO_USERNAME;
|
||||
try {
|
||||
yield browseridManager.ensureLoggedIn();
|
||||
Assert.ok(false, "promise should have been rejected.")
|
||||
} catch(_) {
|
||||
}
|
||||
Assert.ok(browseridManager._shouldHaveSyncKeyBundle,
|
||||
"_shouldHaveSyncKeyBundle should always be true after ensureLogin completes.");
|
||||
fxa.internal.currentAccountState.signedInUser = signedInUser;
|
||||
Status.login = LOGIN_FAILED_LOGIN_REJECTED;
|
||||
try {
|
||||
yield browseridManager.ensureLoggedIn();
|
||||
Assert.ok(false, "LOGIN_FAILED_LOGIN_REJECTED should have caused immediate rejection");
|
||||
} catch (_) {
|
||||
}
|
||||
Assert.equal(Status.login, LOGIN_FAILED_LOGIN_REJECTED,
|
||||
"status should remain LOGIN_FAILED_LOGIN_REJECTED");
|
||||
Status.login = LOGIN_FAILED_NETWORK_ERROR;
|
||||
yield browseridManager.ensureLoggedIn();
|
||||
Assert.equal(Status.login, LOGIN_SUCCEEDED, "final ensureLoggedIn worked");
|
||||
});
|
||||
|
||||
add_test(function test_tokenExpiration() {
|
||||
_("BrowserIDManager notices token expiration:");
|
||||
let bimExp = new BrowserIDManager();
|
||||
|
@ -249,24 +301,6 @@ add_test(function test_tokenExpiration() {
|
|||
}
|
||||
);
|
||||
|
||||
add_test(function test_userChangeAndLogOut() {
|
||||
_("BrowserIDManager notices when the FxAccounts.getSignedInUser().email changes.");
|
||||
let bidUser = new BrowserIDManager();
|
||||
configureFxAccountIdentity(bidUser, identityConfig);
|
||||
let request = new SyncStorageRequest(
|
||||
"https://example.net/somewhere/over/the/rainbow");
|
||||
let authenticator = bidUser.getRESTRequestAuthenticator();
|
||||
do_check_true(!!authenticator);
|
||||
let output = authenticator(request, 'GET');
|
||||
do_check_true(!!output);
|
||||
do_check_eq(bidUser.account, identityConfig.fxaccount.user.email);
|
||||
do_check_true(bidUser.hasValidToken());
|
||||
identityConfig.fxaccount.user.email = "something@new";
|
||||
do_check_false(bidUser.hasValidToken());
|
||||
run_next_test();
|
||||
}
|
||||
);
|
||||
|
||||
add_test(function test_sha256() {
|
||||
// Test vectors from http://www.bichlmeier.info/sha256test.html
|
||||
let vectors = [
|
||||
|
@ -305,9 +339,143 @@ add_test(function test_computeXClientStateHeader() {
|
|||
run_next_test();
|
||||
});
|
||||
|
||||
add_task(function test_getTokenErrors() {
|
||||
_("BrowserIDManager correctly handles various failures to get a token.");
|
||||
|
||||
_("Arrange for a 401 - Sync should reflect an auth error.");
|
||||
yield initializeIdentityWithTokenServerFailure({
|
||||
status: 401,
|
||||
headers: {"content-type": "application/json"},
|
||||
body: JSON.stringify({}),
|
||||
});
|
||||
Assert.equal(Status.login, LOGIN_FAILED_LOGIN_REJECTED, "login was rejected");
|
||||
|
||||
// XXX - other interesting responses to return?
|
||||
|
||||
// And for good measure, some totally "unexpected" errors - we generally
|
||||
// assume these problems are going to magically go away at some point.
|
||||
_("Arrange for an empty body with a 200 response - should reflect a network error.");
|
||||
yield initializeIdentityWithTokenServerFailure({
|
||||
status: 200,
|
||||
headers: [],
|
||||
body: "",
|
||||
});
|
||||
Assert.equal(Status.login, LOGIN_FAILED_NETWORK_ERROR, "login state is LOGIN_FAILED_NETWORK_ERROR");
|
||||
});
|
||||
|
||||
add_task(function test_getHAWKErrors() {
|
||||
_("BrowserIDManager correctly handles various HAWK failures.");
|
||||
|
||||
_("Arrange for a 401 - Sync should reflect an auth error.");
|
||||
yield initializeIdentityWithHAWKFailure({
|
||||
status: 401,
|
||||
headers: {"content-type": "application/json"},
|
||||
body: JSON.stringify({}),
|
||||
});
|
||||
Assert.equal(Status.login, LOGIN_FAILED_LOGIN_REJECTED, "login was rejected");
|
||||
|
||||
// XXX - other interesting responses to return?
|
||||
|
||||
// And for good measure, some totally "unexpected" errors - we generally
|
||||
// assume these problems are going to magically go away at some point.
|
||||
_("Arrange for an empty body with a 200 response - should reflect a network error.");
|
||||
yield initializeIdentityWithHAWKFailure({
|
||||
status: 200,
|
||||
headers: [],
|
||||
body: "",
|
||||
});
|
||||
Assert.equal(Status.login, LOGIN_FAILED_NETWORK_ERROR, "login state is LOGIN_FAILED_NETWORK_ERROR");
|
||||
});
|
||||
|
||||
// End of tests
|
||||
// Utility functions follow
|
||||
|
||||
// Create a new browserid_identity object and initialize it with a
|
||||
// mocked TokenServerClient which always gets the specified response.
|
||||
function* initializeIdentityWithTokenServerFailure(response) {
|
||||
// First create a mock "request" object that well' hack into the token server.
|
||||
// A log for it
|
||||
let requestLog = Log.repository.getLogger("testing.mock-rest");
|
||||
if (!requestLog.appenders.length) { // might as well see what it says :)
|
||||
requestLog.addAppender(new Log.DumpAppender());
|
||||
requestLog.level = Log.Level.Trace;
|
||||
}
|
||||
|
||||
// A mock request object.
|
||||
function MockRESTRequest(url) {};
|
||||
MockRESTRequest.prototype = {
|
||||
_log: requestLog,
|
||||
setHeader: function() {},
|
||||
get: function(callback) {
|
||||
this.response = response;
|
||||
callback.call(this);
|
||||
}
|
||||
}
|
||||
// The mocked TokenServer client which will get the response.
|
||||
function MockTSC() { }
|
||||
MockTSC.prototype = new TokenServerClient();
|
||||
MockTSC.prototype.constructor = MockTSC;
|
||||
MockTSC.prototype.newRESTRequest = function(url) {
|
||||
return new MockRESTRequest(url);
|
||||
}
|
||||
// tie it all together.
|
||||
let mockTSC = new MockTSC()
|
||||
configureFxAccountIdentity(browseridManager);
|
||||
browseridManager._tokenServerClient = mockTSC;
|
||||
|
||||
yield browseridManager.initializeWithCurrentIdentity();
|
||||
try {
|
||||
yield browseridManager.whenReadyToAuthenticate.promise;
|
||||
Assert.ok(false, "expecting this promise to resolve with an error");
|
||||
} catch (ex) {}
|
||||
}
|
||||
|
||||
|
||||
// Create a new browserid_identity object and initialize it with a
|
||||
// hawk mock that simulates a failure.
|
||||
// A token server mock will be used that doesn't hit a server, so we move
|
||||
// directly to a hawk request.
|
||||
function* initializeIdentityWithHAWKFailure(response) {
|
||||
// A mock request object.
|
||||
function MockRESTRequest() {};
|
||||
MockRESTRequest.prototype = {
|
||||
setHeader: function() {},
|
||||
post: function(data, callback) {
|
||||
this.response = response;
|
||||
callback.call(this);
|
||||
}
|
||||
}
|
||||
|
||||
// The hawk client.
|
||||
function MockedHawkClient() {}
|
||||
MockedHawkClient.prototype = new HawkClient();
|
||||
MockedHawkClient.prototype.constructor = MockedHawkClient;
|
||||
MockedHawkClient.prototype.newHAWKAuthenticatedRESTRequest = function(uri, credentials, extra) {
|
||||
return new MockRESTRequest();
|
||||
}
|
||||
|
||||
// tie it all together - configureFxAccountIdentity isn't useful here :(
|
||||
let fxaClient = new MockFxAccountsClient();
|
||||
fxaClient.hawk = new MockedHawkClient();
|
||||
let config = makeIdentityConfig();
|
||||
let internal = {
|
||||
fxAccountsClient: fxaClient,
|
||||
}
|
||||
let fxa = new FxAccounts(internal);
|
||||
fxa.internal.currentAccountState.signedInUser = {
|
||||
accountData: config.fxaccount.user,
|
||||
};
|
||||
|
||||
browseridManager._fxaService = fxa;
|
||||
browseridManager._signedInUser = null;
|
||||
yield browseridManager.initializeWithCurrentIdentity();
|
||||
try {
|
||||
yield browseridManager.whenReadyToAuthenticate.promise;
|
||||
Assert.ok(false, "expecting this promise to resolve with an error");
|
||||
} catch (ex) {}
|
||||
}
|
||||
|
||||
|
||||
function getTimestamp(hawkAuthHeader) {
|
||||
return parseInt(/ts="(\d+)"/.exec(hawkAuthHeader)[1], 10) * SECOND_MS;
|
||||
}
|
||||
|
|
|
@ -126,13 +126,14 @@ function sync_httpd_setup() {
|
|||
}
|
||||
|
||||
function setUp(server) {
|
||||
let deferred = Promise.defer();
|
||||
configureIdentity({username: "johndoe"}).then(() => {
|
||||
deferred.resolve(generateAndUploadKeys());
|
||||
});
|
||||
return configureIdentity({username: "johndoe"}).then(
|
||||
() => {
|
||||
Service.serverURL = server.baseURI + "/";
|
||||
Service.clusterURL = server.baseURI + "/";
|
||||
return deferred.promise;
|
||||
}
|
||||
).then(
|
||||
() => generateAndUploadKeys()
|
||||
);
|
||||
}
|
||||
|
||||
function generateAndUploadKeys() {
|
||||
|
|
|
@ -32,7 +32,7 @@ add_task(function* test_startover() {
|
|||
do_check_true(Service.clusterURL.length > 0);
|
||||
|
||||
// remember some stuff so we can reset it after.
|
||||
let oldIdentidy = Service.identity;
|
||||
let oldIdentity = Service.identity;
|
||||
let oldClusterManager = Service._clusterManager;
|
||||
let deferred = Promise.defer();
|
||||
Services.obs.addObserver(function observeStartOverFinished() {
|
||||
|
@ -41,7 +41,7 @@ add_task(function* test_startover() {
|
|||
}, "weave:service:start-over:finish", false);
|
||||
|
||||
Service.startOver();
|
||||
yield deferred; // wait for the observer to fire.
|
||||
yield deferred.promise; // wait for the observer to fire.
|
||||
|
||||
// should have reset the pref that indicates if FxA is enabled.
|
||||
do_check_true(Services.prefs.getBoolPref("services.sync.fxaccounts.enabled"));
|
||||
|
@ -52,10 +52,11 @@ add_task(function* test_startover() {
|
|||
// should have clobbered the cluster URL
|
||||
do_check_eq(Service.clusterURL, "");
|
||||
|
||||
// reset the world.
|
||||
Service.identity = oldIdentity = Service.identity;
|
||||
Service._clusterManager = Service._clusterManager;
|
||||
Services.prefs.setBoolPref("services.sync.fxaccounts.enabled", false);
|
||||
// we should have thrown away the old identity provider and cluster manager.
|
||||
do_check_neq(oldIdentity, Service.identity);
|
||||
do_check_neq(oldClusterManager, Service._clusterManager);
|
||||
|
||||
// reset the world.
|
||||
Services.prefs.setBoolPref("services.sync.fxaccounts.enabled", false);
|
||||
Services.prefs.setBoolPref("services.sync-testing.startOverKeepIdentity", oldValue);
|
||||
});
|
||||
|
|
|
@ -82,9 +82,9 @@ add_test(function test_credentials_preserved() {
|
|||
_("Ensure that credentials are preserved if client is wiped.");
|
||||
|
||||
// Required for wipeClient().
|
||||
Service.clusterURL = "http://dummy:9000/";
|
||||
Service.identity.account = "testaccount";
|
||||
Service.identity.basicPassword = "testpassword";
|
||||
Service.clusterURL = "http://dummy:9000/";
|
||||
let key = Utils.generatePassphrase();
|
||||
Service.identity.syncKey = key;
|
||||
Service.identity.persistCredentials();
|
||||
|
|
|
@ -109,6 +109,7 @@
|
|||
<li><a href="about:license#nicer">nICEr License</a></li>
|
||||
<li><a href="about:license#nrappkit">nrappkit License</a></li>
|
||||
<li><a href="about:license#openvision">OpenVision License</a></li>
|
||||
<li><a href="about:license#pbkdf2-sha256">pbkdf2_sha256 License</a></li>
|
||||
<li><a href="about:license#praton">praton License</a></li>
|
||||
<li><a href="about:license#qcms">qcms License</a></li>
|
||||
<li><a href="about:license#xdg">Red Hat xdg_user_dir_lookup License</a></li>
|
||||
|
@ -3460,6 +3461,42 @@ OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|||
</pre>
|
||||
|
||||
|
||||
<hr>
|
||||
|
||||
<h1><a id="pbkdf2-sha256"></a>pbkdf2_sha256 License</h1>
|
||||
|
||||
<p>This license applies to the code
|
||||
<span class="path">mozglue/android/pbkdf2_sha256.c</span> and
|
||||
<span class="path">mozglue/android/pbkdf2_sha256.h</span>.
|
||||
</p>
|
||||
|
||||
<pre>
|
||||
Copyright 2005,2007,2009 Colin Percival
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions
|
||||
are met:
|
||||
1. Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
2. Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
|
||||
OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
||||
HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
||||
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
|
||||
OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGE.
|
||||
</pre>
|
||||
|
||||
|
||||
<hr>
|
||||
|
||||
<h1><a id="qcms"></a>qcms License</h1>
|
||||
|
|
|
@ -0,0 +1,71 @@
|
|||
/* -*- js2-basic-offset: 2; indent-tabs-mode: nil; -*- */
|
||||
// Any copyright is dedicated to the Public Domain.
|
||||
// http://creativecommons.org/publicdomain/zero/1.0/
|
||||
|
||||
"use strict";
|
||||
const { devtools } = Components.utils.import("resource://gre/modules/devtools/Loader.jsm", {});
|
||||
let JSPropertyProvider = devtools.require("devtools/toolkit/webconsole/utils").JSPropertyProvider;
|
||||
|
||||
Components.utils.import("resource://gre/modules/jsdebugger.jsm");
|
||||
addDebuggerToGlobal(this);
|
||||
|
||||
function run_test() {
|
||||
const testArray = 'var testArray = [\
|
||||
{propA: "A"},\
|
||||
{\
|
||||
propB: "B", \
|
||||
propC: [\
|
||||
{propD: "D"}\
|
||||
]\
|
||||
},\
|
||||
[\
|
||||
{propE: "E"}\
|
||||
]\
|
||||
];'
|
||||
|
||||
const testObject = 'var testObject = {"propA": [{"propB": "B"}]}';
|
||||
|
||||
let sandbox = Components.utils.Sandbox("http://example.com");
|
||||
let dbg = new Debugger;
|
||||
let dbgObject = dbg.addDebuggee(sandbox);
|
||||
Components.utils.evalInSandbox(testArray, sandbox);
|
||||
Components.utils.evalInSandbox(testObject, sandbox);
|
||||
|
||||
let results = JSPropertyProvider(dbgObject, null, "testArray[0].");
|
||||
do_print("Test that suggestions are given for 'foo[n]' where n is an integer.");
|
||||
test_has_result(results, "propA");
|
||||
|
||||
do_print("Test that suggestions are given for multidimensional arrays.");
|
||||
results = JSPropertyProvider(dbgObject, null, "testArray[2][0].");
|
||||
test_has_result(results, "propE");
|
||||
|
||||
do_print("Test that suggestions are not given for index that's out of bounds.");
|
||||
results = JSPropertyProvider(dbgObject, null, "testArray[10].");
|
||||
do_check_null(results);
|
||||
|
||||
do_print("Test that no suggestions are given if an index is not numerical somewhere in the chain.");
|
||||
results = JSPropertyProvider(dbgObject, null, "testArray[0]['propC'][0].");
|
||||
do_check_null(results);
|
||||
|
||||
results = JSPropertyProvider(dbgObject, null, "testObject['propA'][0].");
|
||||
do_check_null(results);
|
||||
|
||||
results = JSPropertyProvider(dbgObject, null, "testArray[0]['propC'].");
|
||||
do_check_null(results);
|
||||
|
||||
results = JSPropertyProvider(dbgObject, null, "testArray[][1].");
|
||||
do_check_null(results);
|
||||
}
|
||||
|
||||
/**
|
||||
* A helper that ensures (required) results were found.
|
||||
* @param Object aResults
|
||||
* The results returned by JSPropertyProvider.
|
||||
* @param String aRequiredSuggestion
|
||||
* A suggestion that must be found from the results.
|
||||
*/
|
||||
function test_has_result(aResults, aRequiredSuggestion) {
|
||||
do_check_neq(aResults, null);
|
||||
do_check_true(aResults.matches.length > 0);
|
||||
do_check_true(aResults.matches.indexOf(aRequiredSuggestion) !== -1);
|
||||
}
|
|
@ -3,4 +3,5 @@ head =
|
|||
tail =
|
||||
support-files =
|
||||
|
||||
[test_js_property_provider.js]
|
||||
[test_network_helper.js]
|
|
@ -838,7 +838,14 @@ function JSPropertyProvider(aDbgObject, anEnvironment, aInputValue, aCursor)
|
|||
return null;
|
||||
}
|
||||
|
||||
if (/\[\d+\]$/.test(prop)) {
|
||||
// The property to autocomplete is a member of array. For example
|
||||
// list[i][j]..[n]. Traverse the array to get the actual element.
|
||||
obj = getArrayMemberProperty(obj, prop);
|
||||
}
|
||||
else {
|
||||
obj = DevToolsUtils.getProperty(obj, prop);
|
||||
}
|
||||
|
||||
if (!isObjectUsable(obj)) {
|
||||
return null;
|
||||
|
@ -853,6 +860,50 @@ function JSPropertyProvider(aDbgObject, anEnvironment, aInputValue, aCursor)
|
|||
return getMatchedPropsInDbgObject(obj, matchProp);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the array member of aObj for the given aProp. For example, given
|
||||
* aProp='list[0][1]' the element at [0][1] of aObj.list is returned.
|
||||
*
|
||||
* @param object aObj
|
||||
* The object to operate on.
|
||||
* @param string aProp
|
||||
* The property to return.
|
||||
* @return null or Object
|
||||
* Returns null if the property couldn't be located. Otherwise the array
|
||||
* member identified by aProp.
|
||||
*/
|
||||
function getArrayMemberProperty(aObj, aProp)
|
||||
{
|
||||
// First get the array.
|
||||
let obj = aObj;
|
||||
let propWithoutIndices = aProp.substr(0, aProp.indexOf("["));
|
||||
obj = DevToolsUtils.getProperty(obj, propWithoutIndices);
|
||||
if (!isObjectUsable(obj)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Then traverse the list of indices to get the actual element.
|
||||
let result;
|
||||
let arrayIndicesRegex = /\[[^\]]*\]/g;
|
||||
while ((result = arrayIndicesRegex.exec(aProp)) !== null) {
|
||||
let indexWithBrackets = result[0];
|
||||
let indexAsText = indexWithBrackets.substr(1, indexWithBrackets.length - 2);
|
||||
let index = parseInt(indexAsText);
|
||||
|
||||
if (isNaN(index)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
obj = DevToolsUtils.getProperty(obj, index);
|
||||
|
||||
if (!isObjectUsable(obj)) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the given Debugger.Object can be used for autocomplete.
|
||||
*
|
||||
|
|
|
@ -52,4 +52,5 @@ toolkit.jar:
|
|||
+ skin/classic/global/icons/wrap.png (icons/wrap.png)
|
||||
+ skin/classic/global/icons/webapps-16.png (icons/webapps-16.png)
|
||||
+ skin/classic/global/icons/webapps-64.png (icons/webapps-64.png)
|
||||
skin/classic/global/menu/menu-check.png (../../shared/menu-check.png)
|
||||
+ skin/classic/global/toolbar/spring.png (toolbar/spring.png)
|
||||
|
|
|
@ -181,8 +181,8 @@ toolkit.jar:
|
|||
skin/classic/global/media/videoClickToPlayButton.svg (media/videoClickToPlayButton.svg)
|
||||
skin/classic/global/menu/menu-arrow.png (menu/menu-arrow.png)
|
||||
skin/classic/global/menu/menu-arrow@2x.png (menu/menu-arrow@2x.png)
|
||||
skin/classic/global/menu/menu-check.png (menu/menu-check.png)
|
||||
skin/classic/global/menu/menu-check@2x.png (menu/menu-check@2x.png)
|
||||
skin/classic/global/menu/menu-check.png (../../shared/menu-check.png)
|
||||
skin/classic/global/menu/menu-check@2x.png (../../shared/menu-check@2x.png)
|
||||
skin/classic/global/scale/scale-tray-horiz.gif (scale/scale-tray-horiz.gif)
|
||||
skin/classic/global/scale/scale-tray-vert.gif (scale/scale-tray-vert.gif)
|
||||
skin/classic/global/splitter/dimple.png (splitter/dimple.png)
|
||||
|
|
После Ширина: | Высота: | Размер: 197 B |
После Ширина: | Высота: | Размер: 377 B |
|
@ -168,6 +168,7 @@ toolkit.jar:
|
|||
skin/classic/global/media/error.png (media/error.png)
|
||||
skin/classic/global/media/clicktoplay-bgtexture.png (media/clicktoplay-bgtexture.png)
|
||||
skin/classic/global/media/videoClickToPlayButton.svg (media/videoClickToPlayButton.svg)
|
||||
skin/classic/global/menu/menu-check.png (../../shared/menu-check.png)
|
||||
skin/classic/global/printpreview/arrow-left.png (printpreview/arrow-left.png)
|
||||
skin/classic/global/printpreview/arrow-left-end.png (printpreview/arrow-left-end.png)
|
||||
skin/classic/global/printpreview/arrow-right.png (printpreview/arrow-right.png)
|
||||
|
@ -350,6 +351,7 @@ toolkit.jar:
|
|||
skin/classic/aero/global/media/error.png (media/error.png)
|
||||
skin/classic/aero/global/media/clicktoplay-bgtexture.png (media/clicktoplay-bgtexture.png)
|
||||
skin/classic/aero/global/media/videoClickToPlayButton.svg (media/videoClickToPlayButton.svg)
|
||||
skin/classic/aero/global/menu/menu-check.png (../../shared/menu-check.png)
|
||||
skin/classic/aero/global/printpreview/arrow-left.png (printpreview/arrow-left-aero.png)
|
||||
skin/classic/aero/global/printpreview/arrow-left-end.png (printpreview/arrow-left-end-aero.png)
|
||||
skin/classic/aero/global/printpreview/arrow-right.png (printpreview/arrow-right-aero.png)
|
||||
|
|