This commit is contained in:
Ryan VanderMeulen 2014-03-07 15:26:12 -05:00
Родитель c9b7c8fe66 d982916c7e
Коммит 38c345c6c6
60 изменённых файлов: 1519 добавлений и 676 удалений

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

@ -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");
}
deferred.resolve();
}, "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);

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

@ -266,18 +266,25 @@
// Shouldn't act on e.g. context menus being shown from within the panel.
return;
}
switch(aEvent.type) {
switch (aEvent.type) {
case "click":
if (aEvent.originalTarget == this._clickCapturer) {
this.showMainView();
}
break;
case "overflow":
// Resize the right view on the next tick.
if (this.showingSubView) {
setTimeout(this._syncContainerWithSubView.bind(this), 0);
} else if (!this.transitioning) {
setTimeout(this._syncContainerWithMainView.bind(this), 0);
switch (aEvent.target.localName) {
case "vbox":
// Resize the right view on the next tick.
if (this.showingSubView) {
setTimeout(this._syncContainerWithSubView.bind(this), 0);
} else if (!this.transitioning) {
setTimeout(this._syncContainerWithMainView.bind(this), 0);
}
break;
case "toolbarbutton":
aEvent.target.setAttribute("fadelabel", "true");
break;
}
break;
case "popupshowing":

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

@ -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,15 +219,17 @@ final class TouchEventHandler implements Tabs.OnTabsChangedListener {
/**
* Dispatch the event to the gesture detectors and the pan/zoom controller.
*/
private void dispatchEvent(MotionEvent event) {
if (mGestureDetector.onTouchEvent(event)) {
return;
private void dispatchEvent(MotionEvent event, boolean allowDefaultAction) {
if (allowDefaultAction) {
if (mGestureDetector.onTouchEvent(event)) {
return;
}
mScaleGestureDetector.onTouchEvent(event);
if (mScaleGestureDetector.isInProgress()) {
return;
}
}
mScaleGestureDetector.onTouchEvent(event);
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();
}
}

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 811 B

После

Ширина:  |  Высота:  |  Размер: 451 B

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 524 B

После

Ширина:  |  Высота:  |  Размер: 426 B

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 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,33 +235,50 @@ 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;
}
},
/**
/**
* Compute the sha256 of the message bytes. Return bytes.
*/
_sha256: function(message) {
@ -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);
// 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);
// 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._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) {
return null;
}
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,29 +604,30 @@ BrowserIDClusterManager.prototype = {
__proto__: ClusterManager.prototype,
_findCluster: function() {
let fxa = this.identity._fxaService; // will be mocked for tests.
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.
if (!endpoint.endsWith("/")) {
endpoint += "/";
}
return endpoint;
}.bind(this);
// Spinningly ensure we are ready to authenticate and have a valid token.
let promiseClusterURL = function() {
return fxa.getSignedInUser().then(userData => {
return this.identity._fetchTokenForUser(userData).then(token => {
let endpoint = 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.
if (!endpoint.endsWith("/")) {
endpoint += "/";
}
return endpoint;
});
});
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);
});
cb(null, clusterURL);
}).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.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 => {
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");
});
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);
Svc.Obs.notify("weave:service:start-over:finish");
}
).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());
});
Service.serverURL = server.baseURI + "/";
Service.clusterURL = server.baseURI + "/";
return deferred.promise;
return configureIdentity({username: "johndoe"}).then(
() => {
Service.serverURL = server.baseURI + "/";
Service.clusterURL = server.baseURI + "/";
}
).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_network_helper.js]
[test_js_property_provider.js]
[test_network_helper.js]

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

@ -838,7 +838,14 @@ function JSPropertyProvider(aDbgObject, anEnvironment, aInputValue, aCursor)
return null;
}
obj = DevToolsUtils.getProperty(obj, prop);
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)

Двоичные данные
toolkit/themes/shared/menu-check.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 197 B

Двоичные данные
toolkit/themes/shared/menu-check@2x.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 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)