This commit is contained in:
Ryan VanderMeulen 2014-02-25 15:20:54 -05:00
Родитель 182c55b7a4 ccbf4870b7
Коммит 8333762d4c
131 изменённых файлов: 3491 добавлений и 819 удалений

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

@ -39,7 +39,7 @@ this.FxAccountsMgmtService = {
this._shell.sendCustomEvent(aEventName, aMsg);
},
_onFullfill: function(aMsgId, aData) {
_onFulfill: function(aMsgId, aData) {
this._sendChromeEvent("mozFxAccountsChromeEvent", {
id: aMsgId,
data: aData ? aData : null
@ -100,7 +100,7 @@ this.FxAccountsMgmtService = {
FxAccountsManager.getAccount().then(
account => {
// We only expose the email and verification status so far.
self._onFullfill(msg.id, account);
self._onFulfill(msg.id, account);
},
reason => {
self._onReject(msg.id, reason);
@ -110,7 +110,7 @@ this.FxAccountsMgmtService = {
case "logout":
FxAccountsManager.signOut().then(
() => {
self._onFullfill(msg.id);
self._onFulfill(msg.id);
},
reason => {
self._onReject(msg.id, reason);
@ -120,7 +120,7 @@ this.FxAccountsMgmtService = {
case "queryAccount":
FxAccountsManager.queryAccount(data.accountId).then(
result => {
self._onFullfill(msg.id, result);
self._onFulfill(msg.id, result);
},
reason => {
self._onReject(msg.id, reason);
@ -132,7 +132,7 @@ this.FxAccountsMgmtService = {
case "refreshAuthentication":
FxAccountsManager[data.method](data.accountId, data.password).then(
user => {
self._onFullfill(msg.id, user);
self._onFulfill(msg.id, user);
},
reason => {
self._onReject(msg.id, reason);

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

@ -0,0 +1,184 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
const {utils: Cu} = Components;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://services-common/utils.js");
Cu.import("resource://testing-common/httpd.js");
XPCOMUtils.defineLazyModuleGetter(this, "FxAccountsMgmtService",
"resource://gre/modules/FxAccountsMgmtService.jsm",
"FxAccountsMgmtService");
// At end of test, restore original state
const ORIGINAL_AUTH_URI = Services.prefs.getCharPref("identity.fxaccounts.auth.uri");
const ORIGINAL_SHELL = FxAccountsMgmtService._shell;
do_register_cleanup(function() {
Services.prefs.setCharPref("identity.fxaccounts.auth.uri", ORIGINAL_AUTH_URI);
FxAccountsMgmtService._shell = ORIGINAL_SHELL;
});
// Make profile available so that fxaccounts can store user data
do_get_profile();
// Mock the b2g shell; make message passing possible
let mockShell = {
sendCustomEvent: function(aEventName, aMsg) {
Services.obs.notifyObservers({wrappedJSObject: aMsg}, aEventName, null);
},
};
function run_test() {
run_next_test();
}
add_task(function test_overall() {
do_check_neq(FxAccountsMgmtService, null);
});
// Check that invalid email capitalization is corrected on signIn.
// https://github.com/mozilla/fxa-auth-server/blob/master/docs/api.md#post-v1accountlogin
add_test(function test_invalidEmailCase_signIn() {
do_test_pending();
let clientEmail = "greta.garbo@gmail.com";
let canonicalEmail = "Greta.Garbo@gmail.COM";
let attempts = 0;
function writeResp(response, msg) {
if (typeof msg === "object") {
msg = JSON.stringify(msg);
}
response.bodyOutputStream.write(msg, msg.length);
}
// Mock of the fxa accounts auth server, reproducing the behavior of
// /account/login when email capitalization is incorrect on signIn.
let server = httpd_setup({
"/account/login": function(request, response) {
response.setHeader("Content-Type", "application/json");
attempts += 1;
// Ensure we don't get in an endless loop
if (attempts > 2) {
response.setStatusLine(request.httpVersion, 429, "Sorry, you had your chance");
writeResp(response, {});
return;
}
let body = CommonUtils.readBytesFromInputStream(request.bodyInputStream);
let jsonBody = JSON.parse(body);
let email = jsonBody.email;
// The second time through, the accounts client will call the api with
// the correct email capitalization.
if (email == canonicalEmail) {
response.setStatusLine(request.httpVersion, 200, "Yay");
writeResp(response, {
uid: "your-uid",
sessionToken: "your-sessionToken",
keyFetchToken: "your-keyFetchToken",
verified: true,
authAt: 1392144866,
});
return;
}
// If the client has the wrong case on the email, we return a 400, with
// the capitalization of the email as saved in the accounts database.
response.setStatusLine(request.httpVersion, 400, "Incorrect email case");
writeResp(response, {
code: 400,
errno: 120,
error: "Incorrect email case",
email: canonicalEmail,
});
return;
},
});
// Point the FxAccountsClient's hawk rest request client to the mock server
Services.prefs.setCharPref("identity.fxaccounts.auth.uri", server.baseURI);
// Receive a mozFxAccountsChromeEvent message
function onMessage(subject, topic, data) {
let message = subject.wrappedJSObject;
switch (message.id) {
// When we signed in as "Greta.Garbo", the server should have told us
// that the proper capitalization is really "greta.garbo". Call
// getAccounts to get the signed-in user and ensure that the
// capitalization is correct.
case "signIn":
FxAccountsMgmtService.handleEvent({
detail: {
id: "getAccounts",
data: {
method: "getAccounts",
}
}
});
break;
// Having initially signed in as "Greta.Garbo", getAccounts should show
// us that the signed-in user has the properly-capitalized email,
// "greta.garbo".
case "getAccounts":
Services.obs.removeObserver(onMessage, "mozFxAccountsChromeEvent");
do_check_eq(message.data.accountId, canonicalEmail);
do_test_finished();
server.stop(run_next_test);
break;
// We should not receive any other mozFxAccountsChromeEvent messages
default:
do_throw("wat!");
break;
}
}
Services.obs.addObserver(onMessage, "mozFxAccountsChromeEvent", false);
FxAccountsMgmtService._shell = mockShell;
// Trigger signIn using an email with incorrect capitalization
FxAccountsMgmtService.handleEvent({
detail: {
id: "signIn",
data: {
method: "signIn",
accountId: clientEmail,
password: "123456",
},
},
});
});
// End of tests
// Utility functions follow
function httpd_setup (handlers, port=-1) {
let server = new HttpServer();
for (let path in handlers) {
server.registerPathHandler(path, handlers[path]);
}
try {
server.start(port);
} catch (ex) {
dump("ERROR starting server on port " + port + ". Already a process listening?");
do_throw(ex);
}
// Set the base URI for convenience.
let i = server.identity;
server.baseURI = i.primaryScheme + "://" + i.primaryHost + ":" + i.primaryPort;
return server;
}

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

@ -6,6 +6,7 @@ tail =
[test_bug832946.js]
[test_fxaccounts.js]
[test_signintowebsite.js]
head = head_identity.js
tail =

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

@ -954,6 +954,9 @@ pref("toolkit.crashreporter.infoURL",
// base URL for web-based support pages
pref("app.support.baseURL", "https://support.mozilla.org/1/firefox/%VERSION%/%OS%/%LOCALE%/");
// base url for web-based feedback pages
pref("app.feedback.baseURL", "https://input.mozilla.org/%LOCALE%/feedback/%APP%/%VERSION%/");
// Name of alternate about: page for certificate errors (when undefined, defaults to about:neterror)
pref("security.alternate_certificate_error_page", "certerror");

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

@ -1061,6 +1061,16 @@ let BookmarkingUI = {
if (event.target != event.currentTarget)
return;
// Ideally this code would never be reached, but if you click the outer
// button's border, some cpp code for the menu button's so-called XBL binding
// decides to open the popup even though the dropmarker is invisible.
if (this._currentAreaType == CustomizableUI.TYPE_MENU_PANEL) {
this._showSubview();
event.preventDefault();
event.stopPropagation();
return;
}
let widget = CustomizableUI.getWidget("bookmarks-menu-button")
.forWindow(window);
if (widget.overflowed) {
@ -1346,25 +1356,30 @@ let BookmarkingUI = {
}, 1000);
},
_showSubview: function() {
let view = document.getElementById("PanelUI-bookmarks");
view.addEventListener("ViewShowing", this);
view.addEventListener("ViewHiding", this);
let anchor = document.getElementById("bookmarks-menu-button");
anchor.setAttribute("closemenu", "none");
PanelUI.showSubView("PanelUI-bookmarks", anchor,
CustomizableUI.AREA_PANEL);
},
onCommand: function BUI_onCommand(aEvent) {
if (aEvent.target != aEvent.currentTarget) {
return;
}
// Handle special case when the button is in the panel.
let widget = CustomizableUI.getWidget("bookmarks-menu-button")
.forWindow(window);
let isBookmarked = this._itemIds.length > 0;
if (this._currentAreaType == CustomizableUI.TYPE_MENU_PANEL) {
let view = document.getElementById("PanelUI-bookmarks");
view.addEventListener("ViewShowing", this);
view.addEventListener("ViewHiding", this);
widget.node.setAttribute("closemenu", "none");
PanelUI.showSubView("PanelUI-bookmarks", widget.node,
CustomizableUI.AREA_PANEL);
this._showSubview();
return;
}
let widget = CustomizableUI.getWidget("bookmarks-menu-button")
.forWindow(window);
if (widget.overflowed) {
// Allow to close the panel if the page is already bookmarked, cause
// we are going to open the edit bookmark panel.

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

@ -421,18 +421,16 @@ let TabView = {
_addToolbarButton: function TabView__addToolbarButton() {
let buttonId = "tabview-button";
if (document.getElementById(buttonId))
if (CustomizableUI.getPlacementOfWidget(buttonId))
return;
let toolbar = document.getElementById("TabsToolbar");
let currentSet = toolbar.currentSet.split(",");
let alltabsPos = currentSet.indexOf("alltabs-button");
if (-1 == alltabsPos)
return;
let allTabsBtn = document.getElementById("alltabs-button");
let nextItem = allTabsBtn.nextSibling;
toolbar.insertItem(buttonId, nextItem);
let allTabsBtnPlacement = CustomizableUI.getPlacementOfWidget("alltabs-button");
// allTabsBtnPlacement can never be null because the button isn't removable
let desiredPosition = allTabsBtnPlacement.position + 1;
CustomizableUI.addWidgetToArea(buttonId, "TabsToolbar", desiredPosition);
// NB: this is for backwards compatibility, and should be removed by
// https://bugzilla.mozilla.org/show_bug.cgi?id=976041
document.persist("TabsToolbar", "currentset");
},
// ----------

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

@ -4243,7 +4243,7 @@ function onViewToolbarsPopupShowing(aEvent, aInsertPoint) {
if (toolbarItem && toolbarItem.localName == "toolbarpaletteitem") {
toolbarItem = toolbarItem.firstChild;
} else {
} else if (toolbarItem && toolbarItem.localName != "toolbar") {
while (toolbarItem && toolbarItem.parentNode) {
let parent = toolbarItem.parentNode;
if ((parent.classList && parent.classList.contains("customization-target")) ||
@ -4253,6 +4253,8 @@ function onViewToolbarsPopupShowing(aEvent, aInsertPoint) {
break;
toolbarItem = parent;
}
} else {
toolbarItem = null;
}
// Right-clicking on an empty part of the tabstrip will exit

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

@ -712,9 +712,6 @@
hidden="true"
tooltiptext="&pageReportIcon.tooltip;"
onclick="gPopupBlockerObserver.onReportButtonClick(event);"/>
<image id="star-button"
class="urlbar-icon"
onclick="if (event.button === 0) BookmarkingUI.onCommand(event);"/>
</hbox>
<toolbarbutton id="urlbar-go-button"
class="chromeclass-toolbar-additional"

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

@ -57,6 +57,7 @@ support-files =
file_bug970276_favicon2.ico
file_dom_notifications.html
file_fullscreen-window-open.html
get_user_media.html
head.js
healthreport_testRemoteCommands.html
moz.png
@ -264,6 +265,7 @@ skip-if = true # browser_drag.js is disabled, as it needs to be updated for the
[browser_findbarClose.js]
[browser_fullscreen-window-open.js]
[browser_gestureSupport.js]
[browser_get_user_media.js]
[browser_getshortcutoruri.js]
[browser_hide_removing.js]
[browser_homeDrop.js]

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

@ -0,0 +1,742 @@
const kObservedTopics = [
"getUserMedia:response:allow",
"getUserMedia:revoke",
"getUserMedia:response:deny",
"getUserMedia:request",
"recording-device-events",
"recording-window-ended"
];
const PREF_PERMISSION_FAKE = "media.navigator.permission.fake";
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyServiceGetter(this, "MediaManagerService",
"@mozilla.org/mediaManagerService;1",
"nsIMediaManagerService");
var gObservedTopics = {};
function observer(aSubject, aTopic, aData) {
if (!(aTopic in gObservedTopics))
gObservedTopics[aTopic] = 1;
else
++gObservedTopics[aTopic];
}
function promiseNotification(aTopic, aAction) {
let deferred = Promise.defer();
Services.obs.addObserver(function observer() {
ok(true, "got " + aTopic + " notification");
Services.obs.removeObserver(observer, aTopic);
if (kObservedTopics.indexOf(aTopic) != -1) {
if (!(aTopic in gObservedTopics))
gObservedTopics[aTopic] = -1;
else
--gObservedTopics[aTopic];
}
deferred.resolve();
}, aTopic, false);
if (aAction)
aAction();
return deferred.promise;
}
function expectNotification(aTopic) {
is(gObservedTopics[aTopic], 1, "expected notification " + aTopic);
if (aTopic in gObservedTopics)
--gObservedTopics[aTopic];
}
function expectNoNotifications() {
for (let topic in gObservedTopics) {
if (gObservedTopics[topic])
is(gObservedTopics[topic], 0, topic + " notification unexpected");
}
gObservedTopics = {}
}
function promiseMessage(aMessage, aAction) {
let deferred = Promise.defer();
content.addEventListener("message", function messageListener(event) {
content.removeEventListener("message", messageListener);
is(event.data, aMessage, "received " + aMessage);
if (event.data == aMessage)
deferred.resolve();
else
deferred.reject();
});
if (aAction)
aAction();
return deferred.promise;
}
function promisePopupNotification(aName) {
let deferred = Promise.defer();
waitForCondition(() => PopupNotifications.getNotification(aName),
() => {
ok(!!PopupNotifications.getNotification(aName),
aName + " notification appeared");
deferred.resolve();
}, "timeout waiting for popup notification " + aName);
return deferred.promise;
}
function promiseNoPopupNotification(aName) {
let deferred = Promise.defer();
waitForCondition(() => !PopupNotifications.getNotification(aName),
() => {
ok(!PopupNotifications.getNotification(aName),
aName + " notification removed");
deferred.resolve();
}, "timeout waiting for popup notification " + aName + " to disappear");
return deferred.promise;
}
const kActionAlways = 1;
const kActionDeny = 2;
const kActionNever = 3;
function activateSecondaryAction(aAction) {
let notification = PopupNotifications.panel.firstChild;
notification.button.focus();
let popup = notification.menupopup;
popup.addEventListener("popupshown", function () {
popup.removeEventListener("popupshown", arguments.callee, false);
// Press 'down' as many time as needed to select the requested action.
while (aAction--)
EventUtils.synthesizeKey("VK_DOWN", {});
// Activate
EventUtils.synthesizeKey("VK_RETURN", {});
}, false);
// One down event to open the popup
EventUtils.synthesizeKey("VK_DOWN",
{ altKey: !navigator.platform.contains("Mac") });
}
registerCleanupFunction(function() {
gBrowser.removeCurrentTab();
kObservedTopics.forEach(topic => {
Services.obs.removeObserver(observer, topic);
});
Services.prefs.clearUserPref(PREF_PERMISSION_FAKE);
});
function getMediaCaptureState() {
let hasVideo = {};
let hasAudio = {};
MediaManagerService.mediaCaptureWindowState(content, hasVideo, hasAudio);
if (hasVideo.value && hasAudio.value)
return "CameraAndMicrophone";
if (hasVideo.value)
return "Camera";
if (hasAudio.value)
return "Microphone";
return "none";
}
function closeStream(aAlreadyClosed) {
expectNoNotifications();
info("closing the stream");
content.wrappedJSObject.closeStream();
if (!aAlreadyClosed)
yield promiseNotification("recording-device-events");
yield promiseNoPopupNotification("webRTC-sharingDevices");
if (!aAlreadyClosed)
expectNotification("recording-window-ended");
let statusButton = document.getElementById("webrtc-status-button");
ok(statusButton.hidden, "WebRTC status button hidden");
}
function checkDeviceSelectors(aAudio, aVideo) {
let micSelector = document.getElementById("webRTC-selectMicrophone");
if (aAudio)
ok(!micSelector.hidden, "microphone selector visible");
else
ok(micSelector.hidden, "microphone selector hidden");
let cameraSelector = document.getElementById("webRTC-selectCamera");
if (aVideo)
ok(!cameraSelector.hidden, "camera selector visible");
else
ok(cameraSelector.hidden, "camera selector hidden");
}
function checkSharingUI() {
yield promisePopupNotification("webRTC-sharingDevices");
let statusButton = document.getElementById("webrtc-status-button");
ok(!statusButton.hidden, "WebRTC status button visible");
}
function checkNotSharing() {
is(getMediaCaptureState(), "none", "expected nothing to be shared");
ok(!PopupNotifications.getNotification("webRTC-sharingDevices"),
"no webRTC-sharingDevices popup notification");
let statusButton = document.getElementById("webrtc-status-button");
ok(statusButton.hidden, "WebRTC status button hidden");
}
let gTests = [
{
desc: "getUserMedia audio+video",
run: function checkAudioVideo() {
yield promiseNotification("getUserMedia:request", () => {
info("requesting devices");
content.wrappedJSObject.requestDevice(true, true);
});
yield promisePopupNotification("webRTC-shareDevices");
checkDeviceSelectors(true, true);
yield promiseMessage("ok", () => {
PopupNotifications.panel.firstChild.button.click();
});
expectNotification("getUserMedia:response:allow");
expectNotification("recording-device-events");
is(getMediaCaptureState(), "CameraAndMicrophone",
"expected camera and microphone to be shared");
yield checkSharingUI();
yield closeStream();
}
},
{
desc: "getUserMedia audio only",
run: function checkAudioOnly() {
yield promiseNotification("getUserMedia:request", () => {
info("requesting devices");
content.wrappedJSObject.requestDevice(true);
});
yield promisePopupNotification("webRTC-shareDevices");
checkDeviceSelectors(true);
yield promiseMessage("ok", () => {
PopupNotifications.panel.firstChild.button.click();
});
expectNotification("getUserMedia:response:allow");
expectNotification("recording-device-events");
is(getMediaCaptureState(), "Microphone", "expected microphone to be shared");
yield checkSharingUI();
yield closeStream();
}
},
{
desc: "getUserMedia video only",
run: function checkVideoOnly() {
yield promiseNotification("getUserMedia:request", () => {
info("requesting devices");
content.wrappedJSObject.requestDevice(false, true);
});
yield promisePopupNotification("webRTC-shareDevices");
checkDeviceSelectors(false, true);
yield promiseMessage("ok", () => {
PopupNotifications.panel.firstChild.button.click();
});
expectNotification("getUserMedia:response:allow");
expectNotification("recording-device-events");
is(getMediaCaptureState(), "Camera", "expected camera to be shared");
yield checkSharingUI();
yield closeStream();
}
},
{
desc: "getUserMedia audio+video, user disables video",
run: function checkDisableVideo() {
yield promiseNotification("getUserMedia:request", () => {
info("requesting devices");
content.wrappedJSObject.requestDevice(true, true);
});
yield promisePopupNotification("webRTC-shareDevices");
checkDeviceSelectors(true, true);
// disable the camera
document.getElementById("webRTC-selectCamera-menulist").value = -1;
yield promiseMessage("ok", () => {
PopupNotifications.panel.firstChild.button.click();
});
// 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");
is(getMediaCaptureState(), "Microphone",
"expected microphone to be shared");
yield checkSharingUI();
yield closeStream();
}
},
{
desc: "getUserMedia audio+video, user disables audio",
run: function checkDisableAudio() {
yield promiseNotification("getUserMedia:request", () => {
info("requesting devices");
content.wrappedJSObject.requestDevice(true, true);
});
yield promisePopupNotification("webRTC-shareDevices");
checkDeviceSelectors(true, true);
// disable the microphone
document.getElementById("webRTC-selectMicrophone-menulist").value = -1;
yield promiseMessage("ok", () => {
PopupNotifications.panel.firstChild.button.click();
});
// 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");
is(getMediaCaptureState(), "Camera",
"expected microphone to be shared");
yield checkSharingUI();
yield closeStream();
}
},
{
desc: "getUserMedia audio+video, user disables both audio and video",
run: function checkDisableAudioVideo() {
yield promiseNotification("getUserMedia:request", () => {
info("requesting devices");
content.wrappedJSObject.requestDevice(true, true);
});
yield promisePopupNotification("webRTC-shareDevices");
checkDeviceSelectors(true, true);
// disable the camera and microphone
document.getElementById("webRTC-selectCamera-menulist").value = -1;
document.getElementById("webRTC-selectMicrophone-menulist").value = -1;
yield promiseMessage("error: PERMISSION_DENIED", () => {
PopupNotifications.panel.firstChild.button.click();
});
// reset the menuitems to have no impact on the following tests.
document.getElementById("webRTC-selectCamera-menulist").value = 0;
document.getElementById("webRTC-selectMicrophone-menulist").value = 0;
expectNotification("getUserMedia:response:deny");
expectNotification("recording-window-ended");
checkNotSharing();
}
},
{
desc: "getUserMedia audio+video, user clicks \"Don't Share\"",
run: function checkDontShare() {
yield promiseNotification("getUserMedia:request", () => {
info("requesting devices");
content.wrappedJSObject.requestDevice(true, true);
});
yield promisePopupNotification("webRTC-shareDevices");
checkDeviceSelectors(true, true);
yield promiseMessage("error: PERMISSION_DENIED", () => {
activateSecondaryAction(kActionDeny);
});
expectNotification("getUserMedia:response:deny");
expectNotification("recording-window-ended");
checkNotSharing();
}
},
{
desc: "getUserMedia audio+video: stop sharing",
run: function checkStopSharing() {
yield promiseNotification("getUserMedia:request", () => {
info("requesting devices");
content.wrappedJSObject.requestDevice(true, true);
});
yield promisePopupNotification("webRTC-shareDevices");
checkDeviceSelectors(true, true);
yield promiseMessage("ok", () => {
PopupNotifications.panel.firstChild.button.click();
});
expectNotification("getUserMedia:response:allow");
expectNotification("recording-device-events");
is(getMediaCaptureState(), "CameraAndMicrophone",
"expected camera and microphone to be shared");
yield checkSharingUI();
PopupNotifications.getNotification("webRTC-sharingDevices").reshow();
activateSecondaryAction(kActionDeny);
yield promiseNotification("recording-device-events");
expectNotification("getUserMedia:revoke");
yield promiseNoPopupNotification("webRTC-sharingDevices");
if (gObservedTopics["recording-device-events"] == 1) {
todo(false, "Got the 'recording-device-events' notification twice, likely because of bug 962719");
gObservedTopics["recording-device-events"] = 0;
}
expectNoNotifications();
checkNotSharing();
// the stream is already closed, but this will do some cleanup anyway
yield closeStream(true);
}
},
{
desc: "getUserMedia prompt: Always/Never Share",
run: function checkRememberCheckbox() {
function checkPerm(aRequestAudio, aRequestVideo, aAllowAudio, aAllowVideo,
aExpectedAudioPerm, aExpectedVideoPerm, aNever) {
yield promiseNotification("getUserMedia:request", () => {
content.wrappedJSObject.requestDevice(aRequestAudio, aRequestVideo);
});
yield promisePopupNotification("webRTC-shareDevices");
let elt = id => document.getElementById(id);
let noAudio = aAllowAudio === undefined;
is(elt("webRTC-selectMicrophone").hidden, noAudio,
"microphone selector expected to be " + (noAudio ? "hidden" : "visible"));
if (!noAudio)
elt("webRTC-selectMicrophone-menulist").value = (aAllowAudio || aNever) ? 0 : -1;
let noVideo = aAllowVideo === undefined;
is(elt("webRTC-selectCamera").hidden, noVideo,
"camera selector expected to be " + (noVideo ? "hidden" : "visible"));
if (!noVideo)
elt("webRTC-selectCamera-menulist").value = (aAllowVideo || aNever) ? 0 : -1;
let expectedMessage =
(aAllowVideo || aAllowAudio) ? "ok" : "error: PERMISSION_DENIED";
yield promiseMessage(expectedMessage, () => {
activateSecondaryAction(aNever ? kActionNever : kActionAlways);
});
let expected = [];
if (expectedMessage == "ok") {
expectNotification("getUserMedia:response:allow");
expectNotification("recording-device-events");
if (aAllowVideo)
expected.push("Camera");
if (aAllowAudio)
expected.push("Microphone");
expected = expected.join("And");
}
else {
expectNotification("getUserMedia:response:deny");
expectNotification("recording-window-ended");
expected = "none";
}
is(getMediaCaptureState(), expected,
"expected " + expected + " to be shared");
function checkDevicePermissions(aDevice, aExpected) {
let Perms = Services.perms;
let uri = content.document.documentURIObject;
let devicePerms = Perms.testExactPermission(uri, aDevice);
if (aExpected === undefined)
is(devicePerms, Perms.UNKNOWN_ACTION, "no " + aDevice + " persistent permissions");
else {
is(devicePerms, aExpected ? Perms.ALLOW_ACTION : Perms.DENY_ACTION,
aDevice + " persistently " + (aExpected ? "allowed" : "denied"));
}
Perms.remove(uri.host, aDevice);
}
checkDevicePermissions("microphone", aExpectedAudioPerm);
checkDevicePermissions("camera", aExpectedVideoPerm);
if (expectedMessage == "ok")
yield closeStream();
}
// 3 cases where the user accepts the device prompt.
info("audio+video, user grants, expect both perms set to allow");
yield checkPerm(true, true, true, true, true, true);
info("audio only, user grants, check audio perm set to allow, video perm not set");
yield checkPerm(true, false, true, undefined, true, undefined);
info("video only, user grants, check video perm set to allow, audio perm not set");
yield checkPerm(false, true, undefined, true, undefined, true);
// 3 cases where the user rejects the device request.
// First test these cases by setting the device to 'No Audio'/'No Video'
info("audio+video, user denies, expect both perms set to deny");
yield checkPerm(true, true, false, false, false, false);
info("audio only, user denies, expect audio perm set to deny, video not set");
yield checkPerm(true, false, false, undefined, false, undefined);
info("video only, user denies, expect video perm set to deny, audio perm not set");
yield checkPerm(false, true, undefined, false, undefined, false);
// Now test these 3 cases again by using the 'Never Share' action.
info("audio+video, user denies, expect both perms set to deny");
yield checkPerm(true, true, false, false, false, false, true);
info("audio only, user denies, expect audio perm set to deny, video not set");
yield checkPerm(true, false, false, undefined, false, undefined, true);
info("video only, user denies, expect video perm set to deny, audio perm not set");
yield checkPerm(false, true, undefined, false, undefined, false, true);
// 2 cases where the user allows half of what's requested.
info("audio+video, user denies video, grants audio, " +
"expect video perm set to deny, audio perm set to allow.");
yield checkPerm(true, true, true, false, true, false);
info("audio+video, user denies audio, grants video, " +
"expect video perm set to allow, audio perm set to deny.");
yield checkPerm(true, true, false, true, false, true);
}
},
{
desc: "getUserMedia without prompt: use persistent permissions",
run: function checkUsePersistentPermissions() {
function usePerm(aAllowAudio, aAllowVideo, aRequestAudio, aRequestVideo,
aExpectStream) {
let Perms = Services.perms;
let uri = content.document.documentURIObject;
if (aAllowAudio !== undefined) {
Perms.add(uri, "microphone", aAllowAudio ? Perms.ALLOW_ACTION
: Perms.DENY_ACTION);
}
if (aAllowVideo !== undefined) {
Perms.add(uri, "camera", aAllowVideo ? Perms.ALLOW_ACTION
: Perms.DENY_ACTION);
}
let gum = function() {
content.wrappedJSObject.requestDevice(aRequestAudio, aRequestVideo);
};
if (aExpectStream === undefined) {
// Check that we get a prompt.
yield promiseNotification("getUserMedia:request", gum);
yield promisePopupNotification("webRTC-shareDevices");
// Deny the request to cleanup...
yield promiseMessage("error: PERMISSION_DENIED", () => {
activateSecondaryAction(kActionDeny);
});
expectNotification("getUserMedia:response:deny");
expectNotification("recording-window-ended");
}
else {
let allow = (aAllowVideo && aRequestVideo) || (aAllowAudio && aRequestAudio);
let expectedMessage = allow ? "ok" : "error: PERMISSION_DENIED";
yield promiseMessage(expectedMessage, gum);
if (expectedMessage == "ok") {
expectNotification("recording-device-events");
// Check what's actually shared.
let expected = [];
if (aAllowVideo && aRequestVideo)
expected.push("Camera");
if (aAllowAudio && aRequestAudio)
expected.push("Microphone");
expected = expected.join("And");
is(getMediaCaptureState(), expected,
"expected " + expected + " to be shared");
yield closeStream();
}
else {
expectNotification("recording-window-ended");
}
}
Perms.remove(uri.host, "camera");
Perms.remove(uri.host, "microphone");
}
// Set both permissions identically
info("allow audio+video, request audio+video, expect ok (audio+video)");
yield usePerm(true, true, true, true, true);
info("deny audio+video, request audio+video, expect denied");
yield usePerm(false, false, true, true, false);
// Allow audio, deny video.
info("allow audio, deny video, request audio+video, expect ok (audio)");
yield usePerm(true, false, true, true, true);
info("allow audio, deny video, request audio, expect ok (audio)");
yield usePerm(true, false, true, false, true);
info("allow audio, deny video, request video, expect denied");
yield usePerm(true, false, false, true, false);
// Deny audio, allow video.
info("deny audio, allow video, request audio+video, expect ok (video)");
yield usePerm(false, true, true, true, true);
info("deny audio, allow video, request audio, expect denied");
yield usePerm(false, true, true, false, true);
info("deny audio, allow video, request video, expect ok (video)");
yield usePerm(false, true, false, true, false);
// Allow audio, video not set.
info("allow audio, request audio+video, expect prompt");
yield usePerm(true, undefined, true, true, undefined);
info("allow audio, request audio, expect ok (audio)");
yield usePerm(true, undefined, true, false, true);
info("allow audio, request video, expect prompt");
yield usePerm(true, undefined, false, true, undefined);
// Deny audio, video not set.
info("deny audio, request audio+video, expect prompt");
yield usePerm(false, undefined, true, true, undefined);
info("deny audio, request audio, expect denied");
yield usePerm(false, undefined, true, false, false);
info("deny audio, request video, expect prompt");
yield usePerm(false, undefined, false, true, undefined);
// Allow video, video not set.
info("allow video, request audio+video, expect prompt");
yield usePerm(undefined, true, true, true, undefined);
info("allow video, request audio, expect prompt");
yield usePerm(undefined, true, true, false, undefined);
info("allow video, request video, expect ok (video)");
yield usePerm(undefined, true, false, true, true);
// Deny video, video not set.
info("deny video, request audio+video, expect prompt");
yield usePerm(undefined, false, true, true, undefined);
info("deny video, request audio, expect prompt");
yield usePerm(undefined, false, true, false, undefined);
info("deny video, request video, expect denied");
yield usePerm(undefined, false, false, true, false);
}
},
{
desc: "Stop Sharing removes persistent permissions",
run: function checkStopSharingRemovesPersistentPermissions() {
function stopAndCheckPerm(aRequestAudio, aRequestVideo) {
let Perms = Services.perms;
let uri = content.document.documentURIObject;
// Initially set both permissions to 'allow'.
Perms.add(uri, "microphone", Perms.ALLOW_ACTION);
Perms.add(uri, "camera", Perms.ALLOW_ACTION);
// Start sharing what's been requested.
yield promiseMessage("ok", () => {
content.wrappedJSObject.requestDevice(aRequestAudio, aRequestVideo);
});
expectNotification("recording-device-events");
yield checkSharingUI();
// Stop sharing.
PopupNotifications.getNotification("webRTC-sharingDevices").reshow();
activateSecondaryAction(kActionDeny);
yield promiseNotification("recording-device-events");
expectNotification("getUserMedia:revoke");
yield promiseNoPopupNotification("webRTC-sharingDevices");
if (gObservedTopics["recording-device-events"] == 1) {
todo(false, "Got the 'recording-device-events' notification twice, likely because of bug 962719");
gObservedTopics["recording-device-events"] = 0;
}
// Check that permissions have been removed as expected.
let audioPerm = Perms.testExactPermission(uri, "microphone");
if (aRequestAudio)
is(audioPerm, Perms.UNKNOWN_ACTION, "microphone permissions removed");
else
is(audioPerm, Perms.ALLOW_ACTION, "microphone permissions untouched");
let videoPerm = Perms.testExactPermission(uri, "camera");
if (aRequestVideo)
is(videoPerm, Perms.UNKNOWN_ACTION, "camera permissions removed");
else
is(videoPerm, Perms.ALLOW_ACTION, "camera permissions untouched");
// Cleanup.
yield closeStream(true);
Perms.remove(uri.host, "camera");
Perms.remove(uri.host, "microphone");
}
info("request audio+video, stop sharing resets both");
yield stopAndCheckPerm(true, true);
info("request audio, stop sharing resets audio only");
yield stopAndCheckPerm(true, false);
info("request video, stop sharing resets video only");
yield stopAndCheckPerm(false, true);
}
}
];
function test() {
waitForExplicitFinish();
let tab = gBrowser.addTab();
gBrowser.selectedTab = tab;
tab.linkedBrowser.addEventListener("load", function onload() {
tab.linkedBrowser.removeEventListener("load", onload, true);
kObservedTopics.forEach(topic => {
Services.obs.addObserver(observer, topic, false);
});
Services.prefs.setBoolPref(PREF_PERMISSION_FAKE, true);
Task.spawn(function () {
for (let test of gTests) {
info(test.desc);
yield test.run();
// Cleanup before the next test
expectNoNotifications();
}
}).then(finish, ex => {
ok(false, "Unexpected Exception: " + ex);
finish();
});
}, true);
let rootDir = getRootDirectory(gTestPath)
rootDir = rootDir.replace("chrome://mochitests/content/",
"http://127.0.0.1:8888/");
content.location = rootDir + "get_user_media.html";
}
function wait(time) {
let deferred = Promise.defer();
setTimeout(deferred.resolve, time);
return deferred.promise;
}

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

@ -0,0 +1,32 @@
<!DOCTYPE html>
<html>
<head><meta charset="UTF-8"></head>
<body>
<div id="message"></div>
<script>
function message(m) {
document.getElementById("message").innerHTML = m;
window.parent.postMessage(m, "*");
}
var gStream;
function requestDevice(aAudio, aVideo) {
window.navigator.mozGetUserMedia({video: aVideo, audio: aAudio, fake: true},
function(stream) {
gStream = stream;
message("ok");
}, function(err) { message("error: " + err); });
}
message("pending");
function closeStream() {
if (!gStream)
return;
gStream.stop();
gStream = null;
message("closed");
}
</script>
</body>
</html>

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

@ -559,7 +559,10 @@ function openHealthReport()
*/
function openFeedbackPage()
{
openUILinkIn("https://input.mozilla.org/feedback", "tab");
var url = Components.classes["@mozilla.org/toolkit/URLFormatterService;1"]
.getService(Components.interfaces.nsIURLFormatter)
.formatURLPref("app.feedback.baseURL");
openUILinkIn(url, "tab");
}
function buildHelpMenu()

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

@ -225,7 +225,7 @@ let CustomizableUIInternal = {
"alltabs-button",
"tabs-closebutton",
],
defaultCollapsed: false,
defaultCollapsed: null,
}, true);
this.registerArea(CustomizableUI.AREA_BOOKMARKS, {
legacy: true,
@ -2127,7 +2127,9 @@ let CustomizableUIInternal = {
if (area.get("type") == CustomizableUI.TYPE_TOOLBAR) {
let defaultCollapsed = area.get("defaultCollapsed");
let win = areaNode.ownerDocument.defaultView;
win.setToolbarVisibility(areaNode, !defaultCollapsed);
if (defaultCollapsed !== null) {
win.setToolbarVisibility(areaNode, !defaultCollapsed);
}
}
}
}
@ -2151,11 +2153,15 @@ let CustomizableUIInternal = {
Services.prefs.setCharPref(kPrefCustomizationState, uiCustomizationState);
Services.prefs.setBoolPref(kPrefDrawInTitlebar, drawInTitlebar);
this.loadSavedState();
for (let areaId of Object.keys(gSavedState.placements)) {
let placements = gSavedState.placements[areaId];
gPlacements.set(areaId, placements);
// If the user just customizes toolbar/titlebar visibility, gSavedState will be null
// and we don't need to do anything else here:
if (gSavedState) {
for (let areaId of Object.keys(gSavedState.placements)) {
let placements = gSavedState.placements[areaId];
gPlacements.set(areaId, placements);
}
this._rebuildRegisteredAreas();
}
this._rebuildRegisteredAreas();
},
_clearPreviousUIState: function() {
@ -2286,7 +2292,7 @@ let CustomizableUIInternal = {
let attribute = container.getAttribute("type") == "menubar" ? "autohide" : "collapsed";
let collapsed = container.getAttribute(attribute) == "true";
let defaultCollapsed = props.get("defaultCollapsed");
if (collapsed != defaultCollapsed) {
if (defaultCollapsed !== null && collapsed != defaultCollapsed) {
LOG("Found " + areaId + " had non-default toolbar visibility (expected " + defaultCollapsed + ", was " + collapsed + ")");
return false;
}
@ -2494,7 +2500,9 @@ this.CustomizableUI = {
* - defaultPlacements: an array of widget IDs making up the
* default contents of the area
* - defaultCollapsed: (INTERNAL ONLY) applies if the type is TYPE_TOOLBAR, specifies
* if toolbar is collapsed by default (default to true)
* if toolbar is collapsed by default (default to true).
* Specify null to ensure that reset/inDefaultArea don't care
* about a toolbar's collapsed state
*/
registerArea: function(aName, aProperties) {
CustomizableUIInternal.registerArea(aName, aProperties);
@ -2877,7 +2885,8 @@ this.CustomizableUI = {
* Check if a toolbar is collapsed by default.
*
* @param aArea the ID of the area whose default-collapsed state you want to know.
* @return `true` or `false` depending on the area, null if the area is unknown.
* @return `true` or `false` depending on the area, null if the area is unknown,
* or its collapsed state cannot normally be controlled by the user
*/
isToolbarDefaultCollapsed: function(aArea) {
let area = gAreas.get(aArea);

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

@ -383,6 +383,7 @@ const CustomizableWidgets = [{
let node = aDocument.createElementNS(kNSXUL, "toolbaritem");
node.setAttribute("id", "zoom-controls");
node.setAttribute("label", CustomizableUI.getLocalizedProperty(this, "label"));
node.setAttribute("title", CustomizableUI.getLocalizedProperty(this, "tooltiptext"));
// Set this as an attribute in addition to the property to make sure we can style correctly.
node.setAttribute("removable", "true");
@ -541,6 +542,7 @@ const CustomizableWidgets = [{
let node = aDocument.createElementNS(kNSXUL, "toolbaritem");
node.setAttribute("id", "edit-controls");
node.setAttribute("label", CustomizableUI.getLocalizedProperty(this, "label"));
node.setAttribute("title", CustomizableUI.getLocalizedProperty(this, "tooltiptext"));
// Set this as an attribute in addition to the property to make sure we can style correctly.
node.setAttribute("removable", "true");

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

@ -691,10 +691,10 @@ CustomizeMode.prototype = {
wrapper.setAttribute("id", "wrapper-" + aNode.getAttribute("id"));
}
if (aNode.hasAttribute("title")) {
wrapper.setAttribute("title", aNode.getAttribute("title"));
} else if (aNode.hasAttribute("label")) {
if (aNode.hasAttribute("label")) {
wrapper.setAttribute("title", aNode.getAttribute("label"));
} else if (aNode.hasAttribute("title")) {
wrapper.setAttribute("title", aNode.getAttribute("title"));
}
if (aNode.hasAttribute("flex")) {

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

@ -35,6 +35,41 @@ add_task(function() {
yield hiddenPromise;
});
// Right-click on an empty bit of extra toolbar should
// show a context menu with moving options disabled,
// and a toggle option for the extra toolbar
add_task(function() {
let contextMenu = document.getElementById("toolbar-context-menu");
let shownPromise = contextMenuShown(contextMenu);
let toolbar = createToolbarWithPlacements("880164_empty_toolbar", []);
toolbar.setAttribute("context", "toolbar-context-menu");
toolbar.setAttribute("toolbarname", "Fancy Toolbar for Context Menu");
EventUtils.synthesizeMouseAtCenter(toolbar, {type: "contextmenu", button: 2 });
yield shownPromise;
let expectedEntries = [
[".customize-context-moveToPanel", false],
[".customize-context-removeFromToolbar", false],
["---"]
];
if (!isOSX) {
expectedEntries.push(["#toggle_toolbar-menubar", true]);
}
expectedEntries.push(
["#toggle_PersonalToolbar", true],
["#toggle_880164_empty_toolbar", true],
["---"],
[".viewCustomizeToolbar", true]
);
checkContextMenu(contextMenu, expectedEntries);
let hiddenPromise = contextMenuHidden(contextMenu);
contextMenu.hidePopup();
yield hiddenPromise;
removeCustomToolbars();
});
// Right-click on the urlbar-container should
// show a context menu with disabled options to move it.
add_task(function() {

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

@ -83,7 +83,7 @@ XPCOMUtils.defineLazyModuleGetter(this, "BrowserUITelemetry",
"resource:///modules/BrowserUITelemetry.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "AsyncShutdown",
"resource:///modules/AsyncShutdown.jsm");
"resource://gre/modules/AsyncShutdown.jsm");
const PREF_PLUGINS_NOTIFYUSER = "plugins.update.notifyUser";
const PREF_PLUGINS_UPDATEURL = "plugins.update.url";
@ -638,6 +638,9 @@ BrowserGlue.prototype = {
// This pref must be set here because SessionStore will use its value
// on quit-application.
this._setPrefToSaveSession();
// Call trackStartupCrashEnd here in case the delayed call on startup hasn't
// yet occurred (see trackStartupCrashEnd caller in browser.js).
try {
let appStartup = Cc["@mozilla.org/toolkit/app-startup;1"]
.getService(Ci.nsIAppStartup);
@ -1294,7 +1297,7 @@ BrowserGlue.prototype = {
},
_migrateUI: function BG__migrateUI() {
const UI_VERSION = 19;
const UI_VERSION = 20;
const BROWSER_DOCURL = "chrome://browser/content/browser.xul#";
let currentUIVersion = 0;
try {
@ -1557,6 +1560,15 @@ BrowserGlue.prototype = {
}
}
if (currentUIVersion < 20) {
// Remove persisted collapsed state from TabsToolbar.
let resource = this._rdf.GetResource("collapsed");
let toolbar = this._rdf.GetResource(BROWSER_DOCURL + "TabsToolbar");
if (this._getPersist(toolbar, resource)) {
this._setPersist(toolbar, resource);
}
}
if (this._dirty)
this._dataSource.QueryInterface(Ci.nsIRDFRemoteDataSource).Flush();

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

@ -578,6 +578,22 @@
]]></handler>
<handler event="popupshown" phase="target"><![CDATA[
this.setAttribute("panelopen", "true");
//XXXgijs: this is sadfaces, reading styles right after we dirty layout, but
//I don't know of a way around it.
let container = document.getAnonymousElementByAttribute(this, "anonid", "container");
let cs = getComputedStyle(container);
let transitionProp = cs.transitionProperty;
let transitionTime = parseFloat(cs.transitionDuration);
if ((transitionProp.indexOf("transform") > -1 || transitionProp == "all") &&
transitionTime > 0) {
this.style.pointerEvents = 'none';
}
]]></handler>
<handler event="transitionend"><![CDATA[
if (event.originalTarget.getAttribute("anonid") == "container" &&
event.propertyName == "transform") {
this.style.removeProperty("pointer-events");
}
]]></handler>
<handler event="popuphidden" phase="target"><![CDATA[
this.removeAttribute("panelopen");

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

@ -38,7 +38,7 @@ let gVisitStmt = gPlacesDatabase.createAsyncStatement(
* Permission types that should be tested with testExactPermission, as opposed
* to testPermission. This is based on what consumers use to test these permissions.
*/
let TEST_EXACT_PERM_TYPES = ["geo"];
let TEST_EXACT_PERM_TYPES = ["geo", "camera", "microphone"];
/**
* Site object represents a single site, uniquely identified by a host.
@ -330,8 +330,11 @@ let PermissionDefaults = {
set fullscreen(aValue) {
let value = (aValue != this.DENY);
Services.prefs.setBoolPref("full-screen-api.enabled", value);
}
}
},
get camera() this.UNKNOWN,
get microphone() this.UNKNOWN
};
/**
* AboutPermissions manages the about:permissions page.
@ -339,7 +342,7 @@ let PermissionDefaults = {
let AboutPermissions = {
/**
* Number of sites to return from the places database.
*/
*/
PLACES_SITES_LIMIT: 50,
/**
@ -369,17 +372,18 @@ let AboutPermissions = {
*
* Potential future additions: "sts/use", "sts/subd"
*/
_supportedPermissions: ["password", "cookie", "geo", "indexedDB", "popup", "fullscreen"],
_supportedPermissions: ["password", "cookie", "geo", "indexedDB", "popup",
"fullscreen", "camera", "microphone"],
/**
* Permissions that don't have a global "Allow" option.
*/
_noGlobalAllow: ["geo", "indexedDB", "fullscreen"],
_noGlobalAllow: ["geo", "indexedDB", "fullscreen", "camera", "microphone"],
/**
* Permissions that don't have a global "Deny" option.
*/
_noGlobalDeny: [],
_noGlobalDeny: ["camera", "microphone"],
_stringBundle: Services.strings.
createBundle("chrome://browser/locale/preferences/aboutPermissions.properties"),
@ -407,7 +411,7 @@ let AboutPermissions = {
Services.obs.addObserver(this, "passwordmgr-storage-changed", false);
Services.obs.addObserver(this, "cookie-changed", false);
Services.obs.addObserver(this, "browser:purge-domain-data", false);
this._observersInitialized = true;
Services.obs.notifyObservers(null, "browser-permissions-preinit", null);
},
@ -542,7 +546,7 @@ let AboutPermissions = {
let uri = NetUtil.newURI(aLogin.hostname);
this.addHost(uri.host);
} catch (e) {
// newURI will throw for add-ons logins stored in chrome:// URIs
// newURI will throw for add-ons logins stored in chrome:// URIs
}
itemCnt++;
}, this);
@ -557,7 +561,7 @@ let AboutPermissions = {
let uri = NetUtil.newURI(aHostname);
this.addHost(uri.host);
} catch (e) {
// newURI will throw for add-ons logins stored in chrome:// URIs
// newURI will throw for add-ons logins stored in chrome:// URIs
}
itemCnt++;
}, this);
@ -778,7 +782,7 @@ let AboutPermissions = {
let visitLabel = PluralForm.get(aCount, visitForm)
.replace("#1", aCount);
document.getElementById("site-visit-count").value = visitLabel;
});
});
},
updatePasswordsCount: function() {

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

@ -113,6 +113,48 @@
</vbox>
</hbox>
<!-- Camera -->
<hbox id="camera-pref-item"
class="pref-item" align="top">
<image class="pref-icon" type="camera"/>
<vbox>
<label class="pref-title" value="&camera.label;"/>
<hbox align="center">
<menulist id="camera-menulist"
class="pref-menulist"
type="camera"
oncommand="AboutPermissions.onPermissionCommand(event);">
<menupopup>
<menuitem id="camera-0" value="0" label="&permission.alwaysAsk;"/>
<menuitem id="camera-1" value="1" label="&permission.allow;"/>
<menuitem id="camera-2" value="2" label="&permission.block;"/>
</menupopup>
</menulist>
</hbox>
</vbox>
</hbox>
<!-- Microphone -->
<hbox id="microphone-pref-item"
class="pref-item" align="top">
<image class="pref-icon" type="microphone"/>
<vbox>
<label class="pref-title" value="&microphone.label;"/>
<hbox align="center">
<menulist id="microphone-menulist"
class="pref-menulist"
type="microphone"
oncommand="AboutPermissions.onPermissionCommand(event);">
<menupopup>
<menuitem id="microphone-0" value="0" label="&permission.alwaysAsk;"/>
<menuitem id="microphone-1" value="1" label="&permission.allow;"/>
<menuitem id="microphone-2" value="2" label="&permission.block;"/>
</menupopup>
</menulist>
</hbox>
</vbox>
</hbox>
<!-- Cookies -->
<hbox id="cookie-pref-item"
class="pref-item" align="top">

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

@ -47,8 +47,7 @@
<stringbundle id="bundleAccepted" src="resource://gre/res/language.properties"/>
</stringbundleset>
<description>&languages.customize.prefLangDescript;</description>
<label>&languages.customize.active.label;</label>
<description>&languages.customize.description;</description>
<grid flex="1">
<columns>
<column flex="1"/>

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

@ -297,13 +297,14 @@
</richlistbox>
</vbox>
</groupbox>
<vbox>
<hbox align="center">
<label value="&syncDeviceName.label;"
accesskey="&syncDeviceName.accesskey;"
control="syncComputerName"/>
<textbox id="fxaSyncComputerName"
flex="1"
onchange="gSyncUtils.changeName(this)"/>
</vbox>
</hbox>
<hbox id="tosPP" pack="center">
<label class="text-link"
onclick="event.stopPropagation();gSyncUtils.openToS();"

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

@ -27,6 +27,8 @@ const TEST_PERMS = {
"indexedDB": PERM_UNKNOWN,
"popup": PERM_DENY,
"fullscreen" : PERM_UNKNOWN,
"camera": PERM_UNKNOWN,
"microphone": PERM_UNKNOWN
};
const NO_GLOBAL_ALLOW = [
@ -36,7 +38,7 @@ const NO_GLOBAL_ALLOW = [
];
// number of managed permissions in the interface
const TEST_PERMS_COUNT = 6;
const TEST_PERMS_COUNT = 8;
function test() {
waitForExplicitFinish();
@ -164,7 +166,7 @@ var tests = [
function test_all_sites_permission() {
// apply the old default of allowing all cookies
Services.prefs.setIntPref("network.cookie.cookieBehavior", 0);
// there should be no user-set pref for cookie behavior
is(Services.prefs.getIntPref("network.cookie.cookieBehavior"), PERM_UNKNOWN,
"network.cookie.cookieBehavior is expected default");
@ -189,12 +191,12 @@ var tests = [
// make sure "Manage All Passwords..." button opens the correct dialog
addWindowListener("chrome://passwordmgr/content/passwordManager.xul", runNextTest);
gBrowser.contentDocument.getElementById("passwords-manage-all-button").doCommand();
},
function test_manage_all_cookies() {
// make sure "Manage All Cookies..." button opens the correct dialog
addWindowListener("chrome://browser/content/preferences/cookies.xul", runNextTest);
addWindowListener("chrome://browser/content/preferences/cookies.xul", runNextTest);
gBrowser.contentDocument.getElementById("cookies-manage-all-button").doCommand();
},

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

@ -1712,6 +1712,10 @@ let SessionStoreInternal = {
},
setWindowValue: function ssi_setWindowValue(aWindow, aKey, aStringValue) {
if (typeof aStringValue != "string") {
throw new TypeError("setWindowValue only accepts string values");
}
if (!("__SSi" in aWindow)) {
throw Components.Exception("Window is not tracked", Cr.NS_ERROR_INVALID_ARG);
}
@ -1742,6 +1746,10 @@ let SessionStoreInternal = {
},
setTabValue: function ssi_setTabValue(aTab, aKey, aStringValue) {
if (typeof aStringValue != "string") {
throw new TypeError("setTabValue only accepts string values");
}
// If the tab hasn't been restored, then set the data there, otherwise we
// could lose newly added data.
let saveTo;
@ -1783,6 +1791,10 @@ let SessionStoreInternal = {
},
setGlobalValue: function ssi_setGlobalValue(aKey, aStringValue) {
if (typeof aStringValue != "string") {
throw new TypeError("setGlobalValue only accepts string values");
}
this._globalState.set(aKey, aStringValue);
this.saveStateDelayed();
},

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

@ -262,25 +262,13 @@ InspectorPanel.prototype = {
* Hooks the searchbar to show result and auto completion suggestions.
*/
setupSearchBox: function InspectorPanel_setupSearchBox() {
let searchDoc;
if (this.target.isLocalTab) {
searchDoc = this.browser.contentDocument;
} else if (this.target.window) {
searchDoc = this.target.window.document;
} else {
searchDoc = null;
}
// Initiate the selectors search object.
let setNodeFunction = function(eventName, node) {
this.selection.setNodeFront(node, "selectorsearch");
}.bind(this);
if (this.searchSuggestions) {
this.searchSuggestions.destroy();
this.searchSuggestions = null;
}
this.searchBox = this.panelDoc.getElementById("inspector-searchbox");
this.searchSuggestions = new SelectorSearch(this, searchDoc, this.searchBox);
this.searchSuggestions.on("node-selected", setNodeFunction);
this.searchSuggestions = new SelectorSearch(this, this.searchBox);
},
/**

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

@ -4,7 +4,6 @@
"use strict";
const EventEmitter = require("devtools/shared/event-emitter");
const promise = require("sdk/core/promise");
loader.lazyGetter(this, "AutocompletePopup", () => require("devtools/shared/autocomplete-popup").AutocompletePopup);
@ -19,16 +18,12 @@ const MAX_SUGGESTIONS = 15;
* @param InspectorPanel aInspector
* The InspectorPanel whose `walker` attribute should be used for
* document traversal.
* @param nsIDOMDocument aContentDocument
* The content document which inspector is attached to, or null if
* a remote document.
* @param nsiInputElement aInputNode
* The input element to which the panel will be attached and from where
* search input will be taken.
*/
function SelectorSearch(aInspector, aContentDocument, aInputNode) {
function SelectorSearch(aInspector, aInputNode) {
this.inspector = aInspector;
this.doc = aContentDocument;
this.searchBox = aInputNode;
this.panelDoc = this.searchBox.ownerDocument;
@ -55,7 +50,7 @@ function SelectorSearch(aInspector, aContentDocument, aInputNode) {
direction: "ltr",
theme: "auto",
onClick: this._onListBoxKeypress,
onKeypress: this._onListBoxKeypress,
onKeypress: this._onListBoxKeypress
};
this.searchPopup = new AutocompletePopup(this.panelDoc, options);
@ -66,8 +61,6 @@ function SelectorSearch(aInspector, aContentDocument, aInputNode) {
// For testing, we need to be able to wait for the most recent node request
// to finish. Tests can watch this promise for that.
this._lastQuery = promise.resolve(null);
EventEmitter.decorate(this);
}
exports.SelectorSearch = SelectorSearch;
@ -165,23 +158,21 @@ SelectorSearch.prototype = {
/**
* Removes event listeners and cleans up references.
*/
destroy: function SelectorSearch_destroy() {
destroy: function() {
// event listeners.
this.searchBox.removeEventListener("command", this._onHTMLSearch, true);
this.searchBox.removeEventListener("keypress", this._onSearchKeypress, true);
this.searchPopup.destroy();
this.searchPopup = null;
this.searchBox = null;
this.doc = null;
this.panelDoc = null;
this._searchResults = null;
this._searchSuggestions = null;
EventEmitter.decorate(this);
},
_selectResult: function(index) {
return this._searchResults.item(index).then(node => {
this.emit("node-selected", node);
this.inspector.selection.setNodeFront(node, "selectorsearch");
});
},
@ -189,7 +180,7 @@ SelectorSearch.prototype = {
* The command callback for the input box. This function is automatically
* invoked as the user is typing if the input box type is search.
*/
_onHTMLSearch: function SelectorSearch__onHTMLSearch() {
_onHTMLSearch: function() {
let query = this.searchBox.value;
if (query == this._lastSearched) {
return;
@ -256,7 +247,7 @@ SelectorSearch.prototype = {
}
return this._selectResult(0).then(() => {
this.searchBox.classList.remove("devtools-no-search-result");
}).then( () => this.showSuggestions());
}).then(() => this.showSuggestions());
}
if (query.match(/[\s>+]$/)) {
this._lastValidSearch = query + "*";
@ -273,7 +264,7 @@ SelectorSearch.prototype = {
/**
* Handles keypresses inside the input box.
*/
_onSearchKeypress: function SelectorSearch__onSearchKeypress(aEvent) {
_onSearchKeypress: function(aEvent) {
let query = this.searchBox.value;
switch(aEvent.keyCode) {
case aEvent.DOM_VK_RETURN:
@ -348,7 +339,7 @@ SelectorSearch.prototype = {
/**
* Handles keypress and mouse click on the suggestions richlistbox.
*/
_onListBoxKeypress: function SelectorSearch__onListBoxKeypress(aEvent) {
_onListBoxKeypress: function(aEvent) {
switch(aEvent.keyCode || aEvent.button) {
case aEvent.DOM_VK_RETURN:
case aEvent.DOM_VK_TAB:
@ -404,11 +395,10 @@ SelectorSearch.prototype = {
}
},
/**
* Populates the suggestions list and show the suggestion popup.
*/
_showPopup: function SelectorSearch__showPopup(aList, aFirstPart) {
_showPopup: function(aList, aFirstPart) {
let total = 0;
let query = this.searchBox.value;
let toLowerCase = false;
@ -458,7 +448,7 @@ SelectorSearch.prototype = {
* Suggests classes,ids and tags based on the user input as user types in the
* searchbox.
*/
showSuggestions: function SelectorSearch_showSuggestions() {
showSuggestions: function() {
let query = this.searchBox.value;
let firstPart = "";
if (this.state == this.States.TAG) {
@ -498,5 +488,5 @@ SelectorSearch.prototype = {
}
this._showPopup(result.suggestions, firstPart);
});
},
}
};

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

@ -166,6 +166,9 @@ MarkupView.prototype = {
_onMouseLeave: function() {
this._hideBoxModel();
if (this._hoveredNode) {
this._containers.get(this._hoveredNode).hovered = false;
}
this._hoveredNode = null;
},

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

@ -7,6 +7,7 @@ support-files =
browser_inspector_markup_subset.html
browser_inspector_markup_765105_tooltip.png
browser_inspector_markup_950732.html
browser_inspector_markup_962647_search.html
head.js
[browser_bug896181_css_mixed_completion_new_attribute.js]
@ -28,3 +29,4 @@ skip-if = true
[browser_inspector_markup_964014_copy_image_data.js]
[browser_inspector_markup_968316_highlit_node_on_hover_then_select.js]
[browser_inspector_markup_968316_highlight_node_after_mouseleave_mousemove.js]
[browser_inspector_markup_962647_search.js]

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

@ -0,0 +1,11 @@
<!DOCTYPE html>
<html>
<head></head>
<body>
<ul>
<li>
<span>this is an <em>important</em> node</span>
</li>
</ul>
</body>
</html>

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

@ -0,0 +1,50 @@
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
// Test that searching for nodes using the selector-search input expands and
// selects the right nodes in the markup-view, even when those nodes are deeply
// nested (and therefore not attached yet when the markup-view is initialized).
const TEST_URL = "http://mochi.test:8888/browser/browser/devtools/markupview/test/browser_inspector_markup_962647_search.html";
function test() {
waitForExplicitFinish();
let p = content.document.querySelector("p");
Task.spawn(function() {
info("loading the test page");
yield addTab(TEST_URL);
info("opening the inspector");
let {inspector, toolbox} = yield openInspector();
ok(!getContainerForRawNode(inspector.markup, getNode("em")),
"The <em> tag isn't present yet in the markup-view");
// Searching for the innermost element first makes sure that the inspector
// back-end is able to attach the resulting node to the tree it knows at the
// moment. When the inspector is started, the <body> is the default selected
// node, and only the parents up to the ROOT are known, and its direct children
info("searching for the innermost child: <em>");
let updated = inspector.once("inspector-updated");
searchUsingSelectorSearch("em", inspector);
yield updated;
ok(getContainerForRawNode(inspector.markup, getNode("em")),
"The <em> tag is now imported in the markup-view");
is(inspector.selection.node, getNode("em"),
"The <em> tag is the currently selected node");
info("searching for other nodes too");
for (let node of ["span", "li", "ul"]) {
let updated = inspector.once("inspector-updated");
searchUsingSelectorSearch(node, inspector);
yield updated;
is(inspector.selection.node, getNode(node),
"The <" + node + "> tag is the currently selected node");
}
gBrowser.removeCurrentTab();
}).then(null, ok.bind(null, false)).then(finish);
}

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

@ -68,7 +68,6 @@ function openInspector() {
function getContainerForRawNode(markupView, rawNode) {
let front = markupView.walker.frontForRawNode(rawNode);
let container = markupView.getContainer(front);
ok(container, "A markup-container object was found");
return container;
}
@ -240,3 +239,26 @@ function redoChange(inspector) {
inspector.markup.undo.redo();
return mutated;
}
/**
* Get the selector-search input box from the inspector panel
* @return {DOMNode}
*/
function getSelectorSearchBox(inspector) {
return inspector.panelWin.document.getElementById("inspector-searchbox");
}
/**
* Using the inspector panel's selector search box, search for a given selector.
* The selector input string will be entered in the input field and the <ENTER>
* keypress will be simulated.
* This function won't wait for any events and is not async. It's up to callers
* to subscribe to events and react accordingly.
*/
function searchUsingSelectorSearch(selector, inspector) {
info("Entering \"" + selector + "\" into the selector-search input field");
let field = getSelectorSearchBox(inspector);
field.focus();
field.value = selector;
EventUtils.sendKey("return", inspector.panelWin);
}

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

@ -226,6 +226,7 @@ run-if = os == "mac"
[browser_webconsole_bug_821877_csp_errors.js]
[browser_webconsole_bug_837351_securityerrors.js]
[browser_webconsole_bug_846918_hsts_invalid-headers.js]
[browser_webconsole_bug_915141_toggle_response_logging_with_keyboard.js]
[browser_webconsole_cached_autocomplete.js]
[browser_webconsole_change_font_size.js]
[browser_webconsole_chrome.js]
@ -234,6 +235,7 @@ run-if = os == "mac"
[browser_webconsole_console_extras.js]
[browser_webconsole_console_logging_api.js]
[browser_webconsole_count.js]
[browser_webconsole_dont_navigate_on_doubleclick.js]
[browser_webconsole_execution_scope.js]
[browser_webconsole_for_of.js]
[browser_webconsole_history.js]

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

@ -182,7 +182,7 @@ function testCompletion(hud) {
return item.label != "prop1";
}), "autocomplete results do contain prop1");
// Test if 'foo1Obj.prop1.' gives 'prop11'
// Test if 'foo2Obj.prop1.' gives 'prop11'
input.value = "foo2Obj.prop1.";
input.setSelectionRange(14, 14);
jsterm.complete(jsterm.COMPLETE_HINT_ONLY, testNext);
@ -193,7 +193,7 @@ function testCompletion(hud) {
return item.label != "prop11";
}), "autocomplete results do contain prop11");
// Test if 'foo1Obj.prop1.prop11.' gives suggestions for a string i.e. 'length'
// Test if 'foo2Obj.prop1.prop11.' gives suggestions for a string i.e. 'length'
input.value = "foo2Obj.prop1.prop11.";
input.setSelectionRange(21, 21);
jsterm.complete(jsterm.COMPLETE_HINT_ONLY, testNext);
@ -204,6 +204,15 @@ function testCompletion(hud) {
return item.label != "length";
}), "autocomplete results do contain length");
// Test if 'foo1Obj[0].' throws no errors.
input.value = "foo2Obj[0].";
input.setSelectionRange(11, 11);
jsterm.complete(jsterm.COMPLETE_HINT_ONLY, testNext);
yield undefined;
newItems = popup.getItems();
is(newItems.length, 0, "no items for foo2Obj[0]");
testDriver = null;
executeSoon(finishTest);
yield undefined;

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

@ -0,0 +1,112 @@
/* vim:set ts=2 sw=2 sts=2 et: */
/*
* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/
*/
// Tests that the 'Log Request and Response Bodies' buttons can be toggled with keyboard.
const TEST_URI = "data:text/html;charset=utf-8,Web Console test for bug 915141: Toggle log response bodies with keyboard";
let hud;
function test() {
let saveBodiesMenuItem;
let saveBodiesContextMenuItem;
loadTab(TEST_URI).then(({tab: tab}) => {
return openConsole(tab);
})
.then((aHud) => {
hud = aHud;
saveBodiesMenuItem = hud.ui.rootElement.querySelector("#saveBodies");
saveBodiesContextMenuItem = hud.ui.rootElement.querySelector("#saveBodiesContextMenu");
// Test the context menu action.
info("Testing 'Log Request and Response Bodies' menuitem of right click context menu.");
return openPopup(saveBodiesContextMenuItem);
})
.then(() => {
is(saveBodiesContextMenuItem.getAttribute("checked"), "false",
"Context menu: 'log responses' is not checked before action.");
is(hud.ui._saveRequestAndResponseBodies, false,
"Context menu: Responses are not logged before action.");
EventUtils.synthesizeKey("VK_DOWN", {});
EventUtils.synthesizeKey("VK_RETURN", {});
return waitForUpdate(saveBodiesContextMenuItem);
})
.then(() => {
is(saveBodiesContextMenuItem.getAttribute("checked"), "true",
"Context menu: 'log responses' is checked after menuitem was selected with keyboard.");
is(hud.ui._saveRequestAndResponseBodies, true,
"Context menu: Responses are saved after menuitem was selected with keyboard.");
return openPopup(saveBodiesMenuItem);
})
.then(() => {
// Test the 'Net' menu item.
info("Testing 'Log Request and Response Bodies' menuitem of 'Net' menu in the console.");
// 'Log Request and Response Bodies' should be selected due to previous test.
is(saveBodiesMenuItem.getAttribute("checked"), "true",
"Console net menu: 'log responses' is checked before action.");
is(hud.ui._saveRequestAndResponseBodies, true,
"Console net menu: Responses are logged before action.");
// The correct item is the last one in the menu.
EventUtils.synthesizeKey("VK_UP", {});
EventUtils.synthesizeKey("VK_RETURN", {});
return waitForUpdate(saveBodiesMenuItem);
})
.then(() => {
is(saveBodiesMenuItem.getAttribute("checked"), "false",
"Console net menu: 'log responses' is NOT checked after menuitem was selected with keyboard.");
is(hud.ui._saveRequestAndResponseBodies, false,
"Responses are NOT saved after menuitem was selected with keyboard.");
})
.then(finishTest);
}
/**
* Opens and waits for the menu containing aMenuItem to open.
* @param aMenuItem MenuItem
* A MenuItem in a menu that should be opened.
* @return A promise that's resolved once menu is open.
*/
function openPopup(aMenuItem) {
let menu = aMenuItem.parentNode;
let menuOpened = promise.defer();
let uiUpdated = promise.defer();
// The checkbox menuitem is updated asynchronously on 'popupshowing' event so
// it's better to wait for both the update to happen and the menu to open
// before continuing or the test might fail due to a race between menu being
// shown and the item updated to have the correct state.
hud.ui.once("save-bodies-ui-toggled", uiUpdated.resolve);
menu.addEventListener("popupshown", function onPopup () {
menu.removeEventListener("popupshown", onPopup);
menuOpened.resolve();
});
menu.openPopup();
return Promise.all([menuOpened.promise, uiUpdated.promise]);
}
/**
* Waits for the settings and menu containing aMenuItem to update.
* @param aMenuItem MenuItem
* The menuitem that should be updated.
* @return A promise that's resolved once the settings and menus are updated.
*/
function waitForUpdate(aMenuItem) {
info("Waiting for settings update to complete.");
let deferred = promise.defer();
hud.ui.once("save-bodies-pref-reversed", function () {
hud.ui.once("save-bodies-ui-toggled", deferred.resolve);
// The checked state is only updated once the popup is shown.
aMenuItem.parentNode.openPopup();
});
return deferred.promise;
}

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

@ -0,0 +1,43 @@
/* vim:set ts=2 sw=2 sts=2 et: */
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
// Tests that if a link in console is double clicked, the console frame doesn't
// navigate to that destination (bug 975707).
function test() {
Task.spawn(runner).then(finishTest);
function* runner() {
const TEST_PAGE_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console.html" + "?_uniq=" + Date.now();
const {tab} = yield loadTab("data:text/html;charset=utf8,<p>hello</p>");
const hud = yield openConsole(tab);
content.location = TEST_PAGE_URI;
let messages = yield waitForMessages({
webconsole: hud,
messages: [{
name: "Network request message",
url: TEST_PAGE_URI,
category: CATEGORY_NETWORK
}]
});
let networkEventMessage = messages[0].matched.values().next().value;
let urlNode = networkEventMessage.querySelector(".url");
let deferred = promise.defer();
urlNode.addEventListener("click", function onClick(aEvent) {
urlNode.removeEventListener("click", onClick);
ok(aEvent.defaultPrevented, "The default action was prevented.");
deferred.resolve();
});
EventUtils.synthesizeMouseAtCenter(urlNode, {clickCount: 2}, hud.iframeWindow);
yield deferred.promise;
}
}

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

@ -537,12 +537,12 @@ WebConsoleFrame.prototype = {
}
let saveBodies = doc.getElementById("saveBodies");
saveBodies.addEventListener("click", reverseSaveBodiesPref);
saveBodies.addEventListener("command", reverseSaveBodiesPref);
saveBodies.disabled = !this.getFilterState("networkinfo") &&
!this.getFilterState("network");
let saveBodiesContextMenu = doc.getElementById("saveBodiesContextMenu");
saveBodiesContextMenu.addEventListener("click", reverseSaveBodiesPref);
saveBodiesContextMenu.addEventListener("command", reverseSaveBodiesPref);
saveBodiesContextMenu.disabled = !this.getFilterState("networkinfo") &&
!this.getFilterState("network");
@ -2676,13 +2676,13 @@ WebConsoleFrame.prototype = {
let mousedown = this._mousedown;
this._mousedown = false;
aEvent.preventDefault();
// Do not allow middle/right-click or 2+ clicks.
if (aEvent.detail != 1 || aEvent.button != 0) {
return;
}
aEvent.preventDefault();
// If this event started with a mousedown event and it ends at a different
// location, we consider this text selection.
if (mousedown &&

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

@ -497,8 +497,12 @@ getUserMedia.noVideo.label = No Video
getUserMedia.noAudio.label = No Audio
getUserMedia.shareSelectedDevices.label = Share Selected Device;Share Selected Devices
getUserMedia.shareSelectedDevices.accesskey = S
getUserMedia.always.label = Always Share
getUserMedia.always.accesskey = A
getUserMedia.denyRequest.label = Don't Share
getUserMedia.denyRequest.accesskey = D
getUserMedia.never.label = Never Share
getUserMedia.never.accesskey = N
getUserMedia.sharingCamera.message2 = You are currently sharing your camera with this page.
getUserMedia.sharingMicrophone.message2 = You are currently sharing your microphone with this page.
getUserMedia.sharingCameraAndMicrophone.message2 = You are currently sharing your camera and microphone with this page.

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

@ -42,3 +42,5 @@
<!ENTITY popup.label "Open Pop-up Windows">
<!ENTITY fullscreen.label "Fullscreen">
<!ENTITY camera.label "Use the Camera">
<!ENTITY microphone.label "Use the Microphone">

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

@ -5,8 +5,7 @@
<!ENTITY window.width "30em">
<!ENTITY languages.customize.Header "Languages">
<!ENTITY languages.customize.prefLangDescript "Web pages are sometimes offered in more than one language. Choose languages for displaying these web pages, in order of preference.">
<!ENTITY languages.customize.active.label "Languages in order of preference:">
<!ENTITY languages.customize.description "Web pages are sometimes offered in more than one language. Choose languages for displaying these web pages, in order of preference:">
<!ENTITY languages.customize.moveUp.label "Move Up">
<!ENTITY languages.customize.moveUp.accesskey "U">
<!ENTITY languages.customize.moveDown.label "Move Down">

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

@ -10,6 +10,8 @@ alwaysAsk = Always Ask
permission.cookie.label = Set Cookies
permission.desktop-notification.label = Show Notifications
permission.image.label = Load Images
permission.camera.label = Use the Camera
permission.microphone.label = Use the Microphone
permission.install.label = Install Add-ons
permission.popup.label = Open Pop-up Windows
permission.geo.label = Access Your Location

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

@ -4,7 +4,7 @@
#filter emptyLines
# LOCALIZATION NOTE: The 'en-US' strings in the URLs will be replaced with
# your locale code, and link to your translated pages as soon as they're
# your locale code, and link to your translated pages as soon as they're
# live.
#define bookmarks_title Bookmarks
@ -37,4 +37,9 @@
# link title for https://www.mozilla.org/en-US/about/
#define firefox_about About Us
# LOCALIZATION NOTE (firefox_feedback):
# link title for browser feedback page
# currently used by Metro only: https://input.mozilla.org/feedback/metrofirefox
#define firefox_feedback Give Feedback
#unfilter emptyLines

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

@ -163,7 +163,6 @@ var Browser = {
messageManager.addMessageListener("scroll", this);
messageManager.addMessageListener("Browser:CertException", this);
messageManager.addMessageListener("Browser:BlockedSite", this);
messageManager.addMessageListener("Browser:TapOnSelection", this);
Task.spawn(function() {
// Activation URIs come from protocol activations, secondary tiles, and file activations
@ -236,7 +235,6 @@ var Browser = {
messageManager.removeMessageListener("scroll", this);
messageManager.removeMessageListener("Browser:CertException", this);
messageManager.removeMessageListener("Browser:BlockedSite", this);
messageManager.removeMessageListener("Browser:TapOnSelection", this);
Services.obs.removeObserver(SessionHistoryObserver, "browser:purge-session-history");
@ -866,16 +864,6 @@ var Browser = {
case "Browser:BlockedSite":
this._handleBlockedSite(aMessage);
break;
case "Browser:TapOnSelection":
if (!InputSourceHelper.isPrecise) {
if (SelectionHelperUI.isActive) {
SelectionHelperUI.shutdown();
}
if (SelectionHelperUI.canHandle(aMessage)) {
SelectionHelperUI.openEditSession(aMessage);
}
}
break;
}
},
};

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

@ -67,7 +67,7 @@
<html:div class="meta-section-title narrow-title" onclick="StartUI.onNarrowTitleClick('start-bookmarks')">
&narrowBookmarksHeader.label;
</html:div>
<richgrid id="start-bookmarks-grid" observes="bcast_windowState" set-name="bookmarks" seltype="multiple" fade="true" flex="1" minSlots="2">
<richgrid id="start-bookmarks-grid" observes="bcast_windowState" set-name="bookmarks" seltype="multiple" fade="true" flex="1" minSlots="3">
<richgriditem/>
<richgriditem/>
</richgrid>

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

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

@ -186,6 +186,9 @@ let gPermissionObject = {
"desktop-notification": {},
"camera": {},
"microphone": {},
"popup": {
getDefault: function () {
return Services.prefs.getBoolPref("dom.disable_open_during_load") ?

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

@ -120,13 +120,13 @@ function prompt(aContentWindow, aCallID, aAudioRequested, aVideoRequested, aDevi
return;
}
let host = aContentWindow.document.documentURIObject.host;
let uri = aContentWindow.document.documentURIObject;
let browser = getBrowserForWindow(aContentWindow);
let chromeDoc = browser.ownerDocument;
let chromeWin = chromeDoc.defaultView;
let stringBundle = chromeWin.gNavigatorBundle;
let message = stringBundle.getFormattedString("getUserMedia.share" + requestType + ".message",
[ host ]);
[ uri.host ]);
let mainAction = {
label: PluralForm.get(requestType == "CameraAndMicrophone" ? 2 : 1,
@ -138,13 +138,34 @@ function prompt(aContentWindow, aCallID, aAudioRequested, aVideoRequested, aDevi
callback: function() {}
};
let secondaryActions = [{
label: stringBundle.getString("getUserMedia.denyRequest.label"),
accessKey: stringBundle.getString("getUserMedia.denyRequest.accesskey"),
callback: function () {
denyRequest(aCallID);
let secondaryActions = [
{
label: stringBundle.getString("getUserMedia.always.label"),
accessKey: stringBundle.getString("getUserMedia.always.accesskey"),
callback: function () {
mainAction.callback(true);
}
},
{
label: stringBundle.getString("getUserMedia.denyRequest.label"),
accessKey: stringBundle.getString("getUserMedia.denyRequest.accesskey"),
callback: function () {
denyRequest(aCallID);
}
},
{
label: stringBundle.getString("getUserMedia.never.label"),
accessKey: stringBundle.getString("getUserMedia.never.accesskey"),
callback: function () {
denyRequest(aCallID);
let perms = Services.perms;
if (audioDevices.length)
perms.add(uri, "microphone", perms.DENY_ACTION);
if (videoDevices.length)
perms.add(uri, "camera", perms.DENY_ACTION);
}
}
}];
];
let options = {
eventCallback: function(aTopic, aNewBrowser) {
@ -188,18 +209,29 @@ function prompt(aContentWindow, aCallID, aAudioRequested, aVideoRequested, aDevi
addDeviceToList(micMenupopup, stringBundle.getString("getUserMedia.noAudio.label"), "-1");
}
this.mainAction.callback = function() {
this.mainAction.callback = function(aRemember) {
let allowedDevices = Cc["@mozilla.org/supports-array;1"]
.createInstance(Ci.nsISupportsArray);
let perms = Services.perms;
if (videoDevices.length) {
let videoDeviceIndex = chromeDoc.getElementById("webRTC-selectCamera-menulist").value;
if (videoDeviceIndex != "-1")
let allowCamera = videoDeviceIndex != "-1";
if (allowCamera)
allowedDevices.AppendElement(videoDevices[videoDeviceIndex]);
if (aRemember) {
perms.add(uri, "camera",
allowCamera ? perms.ALLOW_ACTION : perms.DENY_ACTION);
}
}
if (audioDevices.length) {
let audioDeviceIndex = chromeDoc.getElementById("webRTC-selectMicrophone-menulist").value;
if (audioDeviceIndex != "-1")
let allowMic = audioDeviceIndex != "-1";
if (allowMic)
allowedDevices.AppendElement(audioDevices[audioDeviceIndex]);
if (aRemember) {
perms.add(uri, "microphone",
allowMic ? perms.ALLOW_ACTION : perms.DENY_ACTION);
}
}
if (allowedDevices.Count() == 0) {
@ -252,6 +284,7 @@ function showBrowserSpecificIndicator(aBrowser) {
let message = stringBundle.getString("getUserMedia.sharing" + captureState + ".message2");
let uri = aBrowser.contentWindow.document.documentURIObject;
let windowId = aBrowser.contentWindow
.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils)
@ -266,6 +299,14 @@ function showBrowserSpecificIndicator(aBrowser) {
label: stringBundle.getString("getUserMedia.stopSharing.label"),
accessKey: stringBundle.getString("getUserMedia.stopSharing.accesskey"),
callback: function () {
let perms = Services.perms;
if (hasVideo.value &&
perms.testExactPermission(uri, "camera") == perms.ALLOW_ACTION)
perms.remove(uri.host, "camera");
if (hasAudio.value &&
perms.testExactPermission(uri, "microphone") == perms.ALLOW_ACTION)
perms.remove(uri.host, "microphone");
Services.obs.notifyObservers(null, "getUserMedia:revoke", windowId);
}
}];

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

@ -97,6 +97,12 @@
.pref-icon[type="fullscreen"] {
list-style-image: url(chrome://global/skin/icons/question-64.png);
}
.pref-icon[type="camera"] {
list-style-image: url(chrome://global/skin/icons/question-64.png);
}
.pref-icon[type="microphone"] {
list-style-image: url(chrome://global/skin/icons/question-64.png);
}
.pref-title {
font-size: 125%;

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

@ -107,6 +107,12 @@
.pref-icon[type="fullscreen"] {
list-style-image: url(chrome://global/skin/icons/question-64.png);
}
.pref-icon[type="camera"] {
list-style-image: url(chrome://global/skin/icons/question-64.png);
}
.pref-icon[type="microphone"] {
list-style-image: url(chrome://global/skin/icons/question-64.png);
}
@media (min-resolution: 2dppx) {
.pref-icon[type="geo"] {

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

@ -132,34 +132,6 @@
color: #666;
}
/* Responsive container */
.devtools-responsive-container {
-moz-box-orient: horizontal;
}
@media (max-width: 700px) {
.devtools-responsive-container {
-moz-box-orient: vertical;
}
.devtools-responsive-container > .devtools-side-splitter {
border: 0;
margin: 0;
border-top: 1px solid black;
min-height: 3px;
height: 3px;
margin-bottom: -3px;
/* In some edge case the cursor is not changed to n-resize */
cursor: n-resize;
}
.devtools-responsive-container > .devtools-sidebar-tabs {
min-height: 35vh;
max-height: 75vh;
}
}
/* Tooltip widget (see browser/devtools/shared/widgets/Tooltip.js) */
.devtools-tooltip .panel-arrowcontent {

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

@ -315,6 +315,10 @@ div.CodeMirror span.eval-text {
-moz-border-end: 1px solid black;
}
.devtools-responsive-container > .devtools-side-splitter {
border-top: 1px solid black;
}
.devtools-textinput,
.devtools-searchinput {
background-color: rgba(24, 29, 32, 1);

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

@ -314,4 +314,8 @@ div.CodeMirror span.eval-text {
-moz-border-end: 1px solid #aaa;
}
.devtools-responsive-container > .devtools-side-splitter {
border-top: 1px solid #aaa;
}
%include toolbars.inc.css

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

@ -16,6 +16,33 @@
transition: margin 0.25s ease-in-out;
}
/* Responsive container */
.devtools-responsive-container {
-moz-box-orient: horizontal;
}
@media (max-width: 700px) {
.devtools-responsive-container {
-moz-box-orient: vertical;
}
.devtools-responsive-container > .devtools-side-splitter {
border: 0;
margin: 0;
min-height: 3px;
height: 3px;
margin-bottom: -3px;
/* In some edge case the cursor is not changed to n-resize */
cursor: n-resize;
}
.devtools-responsive-container > .devtools-sidebar-tabs {
min-height: 35vh;
max-height: 75vh;
}
}
/* BreacrumbsWidget */
.breadcrumbs-widget-container {

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

@ -223,19 +223,13 @@
background-color: #556;
}
/* Use inverted icons for glassed toolbars */
#TabsToolbar > toolbarpaletteitem > #bookmarks-menu-button > .toolbarbutton-menubutton-dropmarker > .dropmarker-icon:not(:-moz-lwtheme),
#TabsToolbar > #bookmarks-menu-button > .toolbarbutton-menubutton-dropmarker > .dropmarker-icon:not(:-moz-lwtheme),
/* Use inverted icons for non-fogged glassed toolbars */
#toolbar-menubar > toolbarpaletteitem > #bookmarks-menu-button > .toolbarbutton-menubutton-dropmarker > .dropmarker-icon:not(:-moz-lwtheme),
#toolbar-menubar > #bookmarks-menu-button > .toolbarbutton-menubutton-dropmarker > .dropmarker-icon:not(:-moz-lwtheme),
#toolbar-menubar > toolbarpaletteitem > toolbaritem > :-moz-any(@nestedButtons@):not(:-moz-lwtheme),
#toolbar-menubar > toolbaritem > :-moz-any(@nestedButtons@):not(:-moz-lwtheme),
#TabsToolbar > toolbarpaletteitem > toolbaritem > :-moz-any(@nestedButtons@):not(:-moz-lwtheme),
#TabsToolbar > toolbaritem > :-moz-any(@nestedButtons@):not(:-moz-lwtheme),
#toolbar-menubar > toolbarpaletteitem > :-moz-any(@primaryToolbarButtons@):not(:-moz-lwtheme),
#toolbar-menubar > :-moz-any(@primaryToolbarButtons@):not(:-moz-lwtheme),
#TabsToolbar > toolbarpaletteitem > :-moz-any(@primaryToolbarButtons@):not(:-moz-lwtheme),
#TabsToolbar > :-moz-any(@primaryToolbarButtons@):not(:-moz-lwtheme) {
#toolbar-menubar > :-moz-any(@primaryToolbarButtons@):not(:-moz-lwtheme) {
list-style-image: url("chrome://browser/skin/Toolbar-inverted.png");
}

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

@ -2,24 +2,22 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
@media (-moz-windows-compositor) {
@media (-moz-windows-glass) {
/* The following rules are for the downloads indicator when in its normal,
non-downloading, non-paused state (ie, it's just showing the downloads
button icon). */
:-moz-any(#toolbar-menubar, #TabsToolbar) #downloads-button:not([attention]) > #downloads-indicator-anchor > #downloads-indicator-icon:not(:-moz-lwtheme),
#toolbar-menubar #downloads-button:not([attention]) > #downloads-indicator-anchor > #downloads-indicator-icon:not(:-moz-lwtheme),
/* The following rules are for the downloads indicator when in its paused
or undetermined progress state. We use :not([counter]) as a shortcut for
:-moz-any([progress], [paused]). */
/* This is the case where the downloads indicator has been moved next to the menubar as well as
the case where the downloads indicator is in the tabstrip toolbar. */
#toolbar-menubar #downloads-button:not([counter]) > #downloads-indicator-anchor > #downloads-indicator-progress-area > #downloads-indicator-counter,
#TabsToolbar #downloads-button:not(:-moz-lwtheme):not([counter]) > #downloads-indicator-anchor > #downloads-indicator-progress-area > #downloads-indicator-counter {
/* This is the case where the downloads indicator has been moved next to the menubar. */
#toolbar-menubar #downloads-button:not([counter]) > #downloads-indicator-anchor > #downloads-indicator-progress-area > #downloads-indicator-counter {
background-image: -moz-image-rect(url("chrome://browser/skin/Toolbar-inverted.png"), 0, 198, 18, 180);
}
:-moz-any(#toolbar-menubar, #TabsToolbar) #downloads-indicator-counter:not(:-moz-lwtheme) {
#toolbar-menubar #downloads-indicator-counter:not(:-moz-lwtheme) {
color: white;
text-shadow: 0 0 1px rgba(0,0,0,.7),
0 1px 1.5px rgba(0,0,0,.5);

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

@ -100,6 +100,12 @@
.pref-icon[type="fullscreen"] {
list-style-image: url(chrome://global/skin/icons/question-64.png);
}
.pref-icon[type="camera"] {
list-style-image: url(chrome://global/skin/icons/question-64.png);
}
.pref-icon[type="microphone"] {
list-style-image: url(chrome://global/skin/icons/question-64.png);
}
.pref-title {
font-size: 125%;

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

@ -45,6 +45,7 @@ testconstants_PATH := $(dir-tests)
PP_TARGETS += manifest
manifest := $(srcdir)/AndroidManifest.xml.in
manifest_TARGET := AndroidManifest.xml
ANDROID_MANIFEST_FILE := $(CURDIR)/AndroidManifest.xml
# Install robocop configs and helper
INSTALL_TARGETS += robocop

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

@ -11,7 +11,7 @@ main.package_name = 'org.mozilla.roboexample.test'
main.res = SRCDIR + '/res'
main.recursive_make_targets += [
OBJDIR + '/AndroidManifest.xml',
TOPOBJDIR + '/mobile/android/base/tests/TestConstants.java']
'../../../mobile/android/base/tests/TestConstants.java']
main.extra_jars += [SRCDIR + '/robotium-solo-4.3.1.jar']
main.assets = TOPSRCDIR + '/mobile/android/base/tests/assets'
main.referenced_projects += ['Fennec']

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

@ -19,14 +19,17 @@ ifdef ANDROID_APK_NAME #{
android_res_dirs := $(addprefix $(srcdir)/,$(or $(ANDROID_RES_DIRS),res))
_ANDROID_RES_FLAG := $(addprefix -S ,$(android_res_dirs))
_ANDROID_ASSETS_FLAG := $(addprefix -A ,$(ANDROID_ASSETS_DIR))
android_manifest := $(or $(ANDROID_MANIFEST_FILE),AndroidManifest.xml)
GENERATED_DIRS += classes
classes.dex: $(call mkdir_deps,classes)
classes.dex: R.java
classes.dex: $(ANDROID_APK_NAME).ap_
classes.dex: $(ANDROID_EXTRA_JARS)
classes.dex: $(JAVAFILES)
$(JAVAC) $(JAVAC_FLAGS) -d classes $(filter %.java,$^)
$(JAVAC) $(JAVAC_FLAGS) -d classes $(filter %.java,$^) \
$(if $(strip $(ANDROID_EXTRA_JARS)),-classpath $(subst $(NULL) ,:,$(strip $(ANDROID_EXTRA_JARS))))
$(DX) --dex --output=$@ classes $(ANDROID_EXTRA_JARS)
# R.java and $(ANDROID_APK_NAME).ap_ are both produced by aapt. To
@ -39,7 +42,7 @@ $(ANDROID_APK_NAME).ap_: .aapt.deps
# resource files one subdirectory below the parent resource directory.
android_res_files := $(wildcard $(addsuffix /*,$(wildcard $(addsuffix /*,$(android_res_dirs)))))
.aapt.deps: AndroidManifest.xml $(android_res_files) $(wildcard $(ANDROID_ASSETS_DIR))
.aapt.deps: $(android_manifest) $(android_res_files) $(wildcard $(ANDROID_ASSETS_DIR))
$(AAPT) package -f -M $< -I $(ANDROID_SDK)/android.jar $(_ANDROID_RES_FLAG) $(_ANDROID_ASSETS_FLAG) \
-J ${@D} \
-F $(ANDROID_APK_NAME).ap_
@ -66,9 +69,6 @@ GARBAGE += \
$(NULL)
JAVA_CLASSPATH := $(ANDROID_SDK)/android.jar
ifdef ANDROID_EXTRA_JARS #{
JAVA_CLASSPATH := $(JAVA_CLASSPATH):$(subst $(NULL) ,:,$(strip $(ANDROID_EXTRA_JARS)))
endif #} ANDROID_EXTRA_JARS
# Include Android specific java flags, instead of what's in rules.mk.
include $(topsrcdir)/config/android-common.mk

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

@ -14,6 +14,7 @@
#include "nsIEventTarget.h"
#include "nsIUUIDGenerator.h"
#include "nsIScriptGlobalObject.h"
#include "nsIPermissionManager.h"
#include "nsIPopupWindowManager.h"
#include "nsISupportsArray.h"
#include "nsIDocShell.h"
@ -33,8 +34,6 @@
#include "nsDOMFile.h"
#include "nsGlobalWindow.h"
#include "mozilla/Preferences.h"
/* Using WebRTC backend on Desktops (Mac, Windows, Linux), otherwise default */
#include "MediaEngineDefault.h"
#if defined(MOZ_WEBRTC)
@ -907,6 +906,13 @@ public:
return NS_OK;
}
nsresult
SetContraints(const MediaStreamConstraintsInternal& aConstraints)
{
mConstraints = aConstraints;
return NS_OK;
}
nsresult
SetAudioDevice(MediaDevice* aAudioDevice)
{
@ -1075,7 +1081,11 @@ public:
{
NS_ASSERTION(!NS_IsMainThread(), "Don't call on main thread");
MediaEngine *backend = mManager->GetBackend(mWindowId);
nsRefPtr<MediaEngine> backend;
if (mConstraints.mFake)
backend = new MediaEngineDefault();
else
backend = mManager->GetBackend(mWindowId);
ScopedDeletePtr<SourceSet> final (GetSources(backend, mConstraints.mVideom,
&MediaEngine::EnumerateVideoDevices,
@ -1415,14 +1425,59 @@ MediaManager::GetUserMedia(JSContext* aCx, bool aPrivileged,
}
#endif
// XXX No full support for picture in Desktop yet (needs proper UI)
if (aPrivileged || c.mFake) {
if (aPrivileged ||
(c.mFake && !Preferences::GetBool("media.navigator.permission.fake"))) {
runnable->Arm();
mMediaThread->Dispatch(runnable, NS_DISPATCH_NORMAL);
} else {
// Check if this site has persistent permissions.
nsresult rv;
nsCOMPtr<nsIPermissionManager> permManager =
do_GetService(NS_PERMISSIONMANAGER_CONTRACTID, &rv);
NS_ENSURE_SUCCESS(rv, rv);
uint32_t audioPerm = nsIPermissionManager::UNKNOWN_ACTION;
if (c.mAudio) {
rv = permManager->TestExactPermissionFromPrincipal(
aWindow->GetExtantDoc()->NodePrincipal(), "microphone", &audioPerm);
NS_ENSURE_SUCCESS(rv, rv);
if (audioPerm == nsIPermissionManager::PROMPT_ACTION) {
audioPerm = nsIPermissionManager::UNKNOWN_ACTION;
}
}
uint32_t videoPerm = nsIPermissionManager::UNKNOWN_ACTION;
if (c.mVideo) {
rv = permManager->TestExactPermissionFromPrincipal(
aWindow->GetExtantDoc()->NodePrincipal(), "camera", &videoPerm);
NS_ENSURE_SUCCESS(rv, rv);
if (videoPerm == nsIPermissionManager::PROMPT_ACTION) {
videoPerm = nsIPermissionManager::UNKNOWN_ACTION;
}
}
if ((!c.mAudio || audioPerm) && (!c.mVideo || videoPerm)) {
// All permissions we were about to request already have a saved value.
if (c.mAudio && audioPerm == nsIPermissionManager::DENY_ACTION) {
c.mAudio = false;
runnable->SetContraints(c);
}
if (c.mVideo && videoPerm == nsIPermissionManager::DENY_ACTION) {
c.mVideo = false;
runnable->SetContraints(c);
}
runnable->Arm();
if (!c.mAudio && !c.mVideo) {
return runnable->Denied(NS_LITERAL_STRING("PERMISSION_DENIED"));
}
return mMediaThread->Dispatch(runnable, NS_DISPATCH_NORMAL);
}
// Ask for user permission, and dispatch runnable (or not) when a response
// is received via an observer notification. Each call is paired with its
// runnable by a GUID.
nsresult rv;
nsCOMPtr<nsIUUIDGenerator> uuidgen =
do_GetService("@mozilla.org/uuid-generator;1", &rv);
NS_ENSURE_SUCCESS(rv, rv);

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

@ -19,6 +19,7 @@
#include "nsIDOMNavigatorUserMedia.h"
#include "nsXULAppAPI.h"
#include "mozilla/Attributes.h"
#include "mozilla/Preferences.h"
#include "mozilla/StaticPtr.h"
#include "mozilla/dom/MediaStreamTrackBinding.h"
#include "prlog.h"
@ -102,12 +103,16 @@ public:
bool CapturingVideo()
{
NS_ASSERTION(NS_IsMainThread(), "Only call on main thread");
return mVideoSource && !mVideoSource->IsFake() && !mStopped;
return mVideoSource && !mStopped &&
(!mVideoSource->IsFake() ||
Preferences::GetBool("media.navigator.permission.fake"));
}
bool CapturingAudio()
{
NS_ASSERTION(NS_IsMainThread(), "Only call on main thread");
return mAudioSource && !mAudioSource->IsFake() && !mStopped;
return mAudioSource && !mStopped &&
(!mAudioSource->IsFake() ||
Preferences::GetBool("media.navigator.permission.fake"));
}
void SetStopped()

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

@ -701,6 +701,7 @@ static const dom::ConstantSpec gWinProperties[] =
INT_CONSTANT(DACL_SECURITY_INFORMATION),
// Errors
INT_CONSTANT(ERROR_INVALID_HANDLE),
INT_CONSTANT(ERROR_ACCESS_DENIED),
INT_CONSTANT(ERROR_DIR_NOT_EMPTY),
INT_CONSTANT(ERROR_FILE_EXISTS),

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

@ -41,6 +41,7 @@ import org.mozilla.gecko.home.SearchEngine;
import org.mozilla.gecko.menu.GeckoMenu;
import org.mozilla.gecko.preferences.GeckoPreferences;
import org.mozilla.gecko.prompts.Prompt;
import org.mozilla.gecko.prompts.PromptListItem;
import org.mozilla.gecko.sync.setup.SyncAccounts;
import org.mozilla.gecko.toolbar.AutocompleteHandler;
import org.mozilla.gecko.toolbar.BrowserToolbar;
@ -91,6 +92,7 @@ import android.view.ViewTreeObserver;
import android.view.Window;
import android.view.animation.Interpolator;
import android.widget.RelativeLayout;
import android.widget.ListView;
import android.widget.Toast;
import android.widget.ViewFlipper;
@ -506,6 +508,15 @@ abstract public class BrowserApp extends GeckoApp
}
});
mBrowserToolbar.setOnFocusChangeListener(new View.OnFocusChangeListener() {
@Override
public void onFocusChange(View v, boolean hasFocus) {
if (isHomePagerVisible()) {
mHomePager.onToolbarFocusChange(hasFocus);
}
}
});
mBrowserToolbar.setOnStartEditingListener(new BrowserToolbar.OnStartEditingListener() {
public void onStartEditing() {
// Temporarily disable doorhanger notifications.
@ -2414,7 +2425,7 @@ abstract public class BrowserApp extends GeckoApp
msgString = R.string.exit_guest_session_text;
}
ps.show(res.getString(titleString), res.getString(msgString), null, false);
ps.show(res.getString(titleString), res.getString(msgString), null, ListView.CHOICE_MODE_NONE);
}
public void subscribeToFeeds(Tab tab) {

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

@ -106,13 +106,17 @@ public final class EventDispatcher {
try {
response.put(GUID, message.getString(GUID));
GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent(message.getString("type") + ":Return", response.toString()));
} catch(Exception ex) { }
} catch (Exception ex) {
Log.e(LOGTAG, "Unable to send response", ex);
}
}
public static void sendError(JSONObject message, JSONObject error) {
try {
error.put(GUID, message.getString(GUID));
GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent(message.getString("type") + ":Error", error.toString()));
} catch(Exception ex) { }
} catch (Exception ex) {
Log.e(LOGTAG, "Unable to send error", ex);
}
}
}

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

@ -416,12 +416,8 @@ public abstract class GeckoApp
@Override
public boolean onPreparePanel(int featureId, View view, Menu menu) {
if (Build.VERSION.SDK_INT >= 11 && featureId == Window.FEATURE_OPTIONS_PANEL) {
if (menu instanceof GeckoMenu) {
((GeckoMenu) menu).refresh();
}
if (Build.VERSION.SDK_INT >= 11 && featureId == Window.FEATURE_OPTIONS_PANEL)
return onPrepareOptionsMenu(menu);
}
return super.onPreparePanel(featureId, view, menu);
}
@ -686,6 +682,36 @@ public abstract class GeckoApp
GeckoAppShell.openUriExternal(message.optString("url"),
message.optString("mime"), message.optString("packageName"),
message.optString("className"), message.optString("action"), message.optString("title"));
} else if (event.equals("Intent:OpenForResult")) {
Intent intent = GeckoAppShell.getOpenURIIntent(this,
message.optString("url"),
message.optString("mime"),
message.optString("action"),
message.optString("title"));
intent.setClassName(message.optString("packageName"), message.optString("className"));
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
final JSONObject originalMessage = message;
ActivityHandlerHelper.startIntentForActivity(this,
intent,
new ActivityResultHandler() {
@Override
public void onActivityResult (int resultCode, Intent data) {
JSONObject response = new JSONObject();
try {
if (data != null) {
response.put("extras", bundleToJSON(data.getExtras()));
}
response.put("resultCode", resultCode);
} catch (JSONException e) {
Log.w(LOGTAG, "Error building JSON response.", e);
}
EventDispatcher.sendResponse(originalMessage, response);
}
});
} else if (event.equals("Locale:Set")) {
setLocale(message.getString("locale"));
} else if (event.equals("NativeApp:IsDebuggable")) {
@ -846,6 +872,23 @@ public abstract class GeckoApp
});
}
private JSONObject bundleToJSON(Bundle bundle) {
JSONObject json = new JSONObject();
if (bundle == null) {
return json;
}
for (String key : bundle.keySet()) {
try {
json.put(key, bundle.get(key));
} catch (JSONException e) {
Log.w(LOGTAG, "Error building JSON response.", e);
}
}
return json;
}
private void addFullScreenPluginView(View view) {
if (mFullScreenPluginView != null) {
Log.w(LOGTAG, "Already have a fullscreen plugin view");
@ -1537,6 +1580,7 @@ public abstract class GeckoApp
registerEventListener("PrivateBrowsing:Data");
registerEventListener("Contact:Add");
registerEventListener("Intent:Open");
registerEventListener("Intent:OpenForResult");
registerEventListener("Intent:GetHandlers");
registerEventListener("Locale:Set");
registerEventListener("NativeApp:IsDebuggable");

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

@ -69,7 +69,6 @@ public class GeckoApplication extends Application {
GeckoBatteryManager.getInstance().start();
GeckoNetworkManager.getInstance().init(getApplicationContext());
MemoryMonitor.getInstance().init(getApplicationContext());
HomeConfigInvalidator.getInstance().init(getApplicationContext());
mInited = true;
}
@ -116,6 +115,7 @@ public class GeckoApplication extends Application {
Clipboard.init(getApplicationContext());
FilePicker.init(getApplicationContext());
GeckoLoader.loadMozGlue();
HomeConfigInvalidator.getInstance().init(getApplicationContext());
super.onCreate();
}

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

@ -19,6 +19,7 @@ import org.mozilla.gecko.util.ThreadUtils;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.text.Html;
import android.text.Spanned;
import android.text.TextUtils;
@ -105,11 +106,22 @@ public class HomeBanner extends LinearLayout
super.onDetachedFromWindow();
GeckoAppShell.getEventDispatcher().unregisterEventListener("HomeBanner:Data", this);
}
}
public void setScrollingPages(boolean scrollingPages) {
mScrollingPages = scrollingPages;
}
@Override
public void setVisibility(int visibility) {
// On pre-Honeycomb devices, setting the visibility to GONE won't actually
// hide the view unless we clear animations first.
if (Build.VERSION.SDK_INT < 11 && visibility == View.GONE) {
clearAnimation();
}
super.setVisibility(visibility);
}
public void setScrollingPages(boolean scrollingPages) {
mScrollingPages = scrollingPages;
}
@Override
public void handleMessage(String event, JSONObject message) {

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

@ -244,6 +244,12 @@ public class HomePager extends ViewPager {
return super.dispatchTouchEvent(event);
}
public void onToolbarFocusChange(boolean hasFocus) {
// We should only enable the banner if the toolbar is not focused and we are on the default page
final boolean enabled = !hasFocus && getCurrentItem() == mDefaultPageIndex;
mHomeBanner.setEnabled(enabled);
}
private void updateUiFromPanelConfigs(List<PanelConfig> panelConfigs) {
// We only care about the adapter if HomePager is currently
// loaded, which means it's visible in the activity.

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

@ -27,20 +27,24 @@ public class PanelGridView extends GridView
private final ViewConfig mViewConfig;
private final PanelViewAdapter mAdapter;
protected OnUrlOpenListener mUrlOpenListener;
private PanelViewUrlHandler mUrlHandler;
public PanelGridView(Context context, ViewConfig viewConfig) {
super(context, null, R.attr.panelGridViewStyle);
mViewConfig = viewConfig;
mUrlHandler = new PanelViewUrlHandler(viewConfig);
mAdapter = new PanelViewAdapter(context, viewConfig.getItemType());
setAdapter(mAdapter);
setOnItemClickListener(new PanelGridItemClickListener());
}
@Override
public void onDetachedFromWindow() {
super.onDetachedFromWindow();
mUrlOpenListener = null;
mUrlHandler.setOnUrlOpenListener(null);
}
@Override
@ -50,26 +54,13 @@ public class PanelGridView extends GridView
@Override
public void setOnUrlOpenListener(OnUrlOpenListener listener) {
mUrlOpenListener = listener;
mUrlHandler.setOnUrlOpenListener(listener);
}
private class PanelGridItemClickListener implements AdapterView.OnItemClickListener {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
Cursor cursor = mAdapter.getCursor();
if (cursor == null || !cursor.moveToPosition(position)) {
throw new IllegalStateException("Couldn't move cursor to position " + position);
}
int urlIndex = cursor.getColumnIndexOrThrow(HomeItems.URL);
final String url = cursor.getString(urlIndex);
EnumSet<OnUrlOpenListener.Flags> flags = EnumSet.noneOf(OnUrlOpenListener.Flags.class);
if (mViewConfig.getItemHandler() == ItemHandler.INTENT) {
flags.add(OnUrlOpenListener.Flags.OPEN_WITH_INTENT);
}
mUrlOpenListener.onUrlOpen(url, flags);
mUrlHandler.openUrlAtPosition(mAdapter.getCursor(), position);
}
}
}

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

@ -66,7 +66,6 @@ class PanelItemView extends LinearLayout {
if (hasImageUrl) {
Picasso.with(getContext())
.load(imageUrl)
.error(R.drawable.favicon)
.into(mImage);
}
}

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

@ -27,12 +27,17 @@ public class PanelListView extends HomeListView
private final PanelViewAdapter mAdapter;
private final ViewConfig mViewConfig;
private final PanelViewUrlHandler mUrlHandler;
public PanelListView(Context context, ViewConfig viewConfig) {
super(context);
mViewConfig = viewConfig;
mUrlHandler = new PanelViewUrlHandler(viewConfig);
mAdapter = new PanelViewAdapter(context, viewConfig.getItemType());
setAdapter(mAdapter);
setOnItemClickListener(new PanelListItemClickListener());
}
@ -42,23 +47,16 @@ public class PanelListView extends HomeListView
mAdapter.swapCursor(cursor);
}
@Override
public void setOnUrlOpenListener(OnUrlOpenListener listener) {
super.setOnUrlOpenListener(listener);
mUrlHandler.setOnUrlOpenListener(listener);
}
private class PanelListItemClickListener implements AdapterView.OnItemClickListener {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
Cursor cursor = mAdapter.getCursor();
if (cursor == null || !cursor.moveToPosition(position)) {
throw new IllegalStateException("Couldn't move cursor to position " + position);
}
int urlIndex = cursor.getColumnIndexOrThrow(HomeItems.URL);
final String url = cursor.getString(urlIndex);
EnumSet<OnUrlOpenListener.Flags> flags = EnumSet.noneOf(OnUrlOpenListener.Flags.class);
if (mViewConfig.getItemHandler() == ItemHandler.INTENT) {
flags.add(OnUrlOpenListener.Flags.OPEN_WITH_INTENT);
}
mUrlOpenListener.onUrlOpen(url, flags);
mUrlHandler.openUrlAtPosition(mAdapter.getCursor(), position);
}
}
}

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

@ -0,0 +1,46 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.home;
import org.mozilla.gecko.db.BrowserContract.HomeItems;
import org.mozilla.gecko.home.HomeConfig.ItemHandler;
import org.mozilla.gecko.home.HomeConfig.ViewConfig;
import org.mozilla.gecko.home.HomePager.OnUrlOpenListener;
import android.database.Cursor;
import java.util.EnumSet;
class PanelViewUrlHandler {
private final ViewConfig mViewConfig;
private OnUrlOpenListener mUrlOpenListener;
public PanelViewUrlHandler(ViewConfig viewConfig) {
mViewConfig = viewConfig;
}
public void setOnUrlOpenListener(OnUrlOpenListener listener) {
mUrlOpenListener = listener;
}
public void openUrlAtPosition(Cursor cursor, int position) {
if (cursor == null || !cursor.moveToPosition(position)) {
throw new IllegalStateException("Couldn't move cursor to position " + position);
}
int urlIndex = cursor.getColumnIndexOrThrow(HomeItems.URL);
final String url = cursor.getString(urlIndex);
EnumSet<OnUrlOpenListener.Flags> flags = EnumSet.noneOf(OnUrlOpenListener.Flags.class);
if (mViewConfig.getItemHandler() == ItemHandler.INTENT) {
flags.add(OnUrlOpenListener.Flags.OPEN_WITH_INTENT);
}
if (mUrlOpenListener != null) {
mUrlOpenListener.onUrlOpen(url, flags);
}
}
}

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

@ -26,7 +26,6 @@ import android.widget.ListView;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
@ -637,18 +636,6 @@ public class GeckoMenu extends ListView
}
}
public void refresh() {
for (Iterator<GeckoMenuItem> i = mPrimaryActionItems.keySet().iterator(); i.hasNext();) {
GeckoMenuItem item = i.next();
item.refreshIfChanged();
}
for (Iterator<GeckoMenuItem> i = mSecondaryActionItems.keySet().iterator(); i.hasNext();) {
GeckoMenuItem item = i.next();
item.refreshIfChanged();
}
}
// Adapter to bind menu items to the list.
private class MenuItemsAdapter extends BaseAdapter {
private static final int VIEW_TYPE_DEFAULT = 0;
@ -747,11 +734,8 @@ public class GeckoMenu extends ListView
@Override
public boolean areAllItemsEnabled() {
for (GeckoMenuItem item : mItems) {
if (!item.isEnabled())
return false;
}
// Setting this to true is a workaround to fix disappearing
// dividers in the menu (bug 963249).
return true;
}

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

@ -222,17 +222,6 @@ public class GeckoMenuItem implements MenuItem {
return this;
}
public void refreshIfChanged() {
if (mActionProvider == null)
return;
if (mActionProvider instanceof GeckoActionProvider) {
if (((GeckoActionProvider) mActionProvider).hasChanged()) {
mShowAsActionChangedListener.onShowAsActionChanged(GeckoMenuItem.this);
}
}
}
@Override
public MenuItem setActionView(int resId) {
return this;

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

@ -48,7 +48,7 @@ public class MenuItemDefault extends TextView
int stateIconSize = res.getDimensionPixelSize(R.dimen.menu_item_state_icon);
Rect stateIconBounds = new Rect(0, 0, stateIconSize, stateIconSize);
mState = res.getDrawable(R.drawable.menu_item_state);
mState = res.getDrawable(R.drawable.menu_item_state).mutate();
mState.setBounds(stateIconBounds);
if (sIconBounds == null) {

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

@ -239,6 +239,7 @@ gbjar.sources += [
'home/PanelListView.java',
'home/PanelManager.java',
'home/PanelViewAdapter.java',
'home/PanelViewUrlHandler.java',
'home/PinSiteDialog.java',
'home/ReadingListPanel.java',
'home/SearchEngine.java',
@ -298,6 +299,8 @@ gbjar.sources += [
'prompts/IconGridInput.java',
'prompts/Prompt.java',
'prompts/PromptInput.java',
'prompts/PromptListAdapter.java',
'prompts/PromptListItem.java',
'prompts/PromptService.java',
'ReaderModeUtils.java',
'ReferrerReceiver.java',
@ -485,17 +488,16 @@ if '-march=armv7' in CONFIG['OS_CFLAGS']:
else:
DEFINES['MOZ_MIN_CPU_VERSION'] = 5
generated = add_android_eclipse_library_project('FennecGeneratedResources')
generated.package_name = 'org.mozilla.fennec.generatedresources'
generated = add_android_eclipse_library_project('FennecResourcesGenerated')
generated.package_name = 'org.mozilla.fennec.resources.generated'
generated.res = OBJDIR + '/res'
branding = add_android_eclipse_library_project('FennecBrandingResources')
branding.package_name = 'org.mozilla.fennec.brandingresources'
branding = add_android_eclipse_library_project('FennecResourcesBranding')
branding.package_name = 'org.mozilla.fennec.resources.branding'
branding.res = TOPSRCDIR + '/' + CONFIG['MOZ_BRANDING_DIRECTORY'] + '/res'
main = add_android_eclipse_project('Fennec', OBJDIR + '/AndroidManifest.xml')
main.package_name = 'org.mozilla.gecko'
main.res = SRCDIR + '/resources'
main.recursive_make_targets += ['.aapt.deps'] # Captures dependencies on Android manifest and all resources.
main.recursive_make_targets += [OBJDIR + '/generated/' + f for f in mgjar.generated_sources]
@ -506,6 +508,7 @@ main.referenced_projects += [generated.name, branding.name]
main.extra_jars += [CONFIG['ANDROID_COMPAT_LIB']]
main.assets = TOPOBJDIR + '/dist/fennec/assets'
main.libs = TOPOBJDIR + '/dist/fennec/lib'
main.res = None
cpe = main.add_classpathentry('src', SRCDIR,
dstdir='src/org/mozilla/gecko',
@ -518,3 +521,22 @@ main.add_classpathentry('generated', OBJDIR + '/generated',
main.add_classpathentry('thirdparty', TOPSRCDIR + '/mobile/android/thirdparty',
dstdir='thirdparty',
ignore_warnings=True)
resources = add_android_eclipse_library_project('FennecResources')
resources.package_name = 'org.mozilla.fennec.resources'
resources.res = SRCDIR + '/resources'
resources.included_projects += ['../' + generated.name, '../' + branding.name]
resources.referenced_projects += [generated.name, branding.name]
main.included_projects += ['../' + resources.name]
main.referenced_projects += [resources.name]
if CONFIG['MOZ_CRASHREPORTER']:
crashreporter = add_android_eclipse_library_project('FennecResourcesCrashReporter')
crashreporter.package_name = 'org.mozilla.fennec.resources.crashreporter'
crashreporter.res = SRCDIR + '/crashreporter/res'
crashreporter.included_projects += ['../' + resources.name]
crashreporter.referenced_projects += [resources.name]
main.included_projects += ['../' + crashreporter.name]
main.referenced_projects += [crashreporter.name]

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

@ -36,18 +36,20 @@ import android.widget.ListView;
import android.widget.ScrollView;
import android.widget.TextView;
import java.util.ArrayList;
public class Prompt implements OnClickListener, OnCancelListener, OnItemClickListener {
private static final String LOGTAG = "GeckoPromptService";
private String[] mButtons;
private PromptInput[] mInputs;
private boolean[] mSelected;
private AlertDialog mDialog;
private final LayoutInflater mInflater;
private final Context mContext;
private PromptCallback mCallback;
private String mGuid;
private PromptListAdapter mAdapter;
private static boolean mInitialized = false;
private static int mGroupPaddingSize;
@ -84,7 +86,7 @@ public class Prompt implements OnClickListener, OnCancelListener, OnItemClickLis
// Don't add padding to color picker views
if (input.canApplyInputStyle()) {
view.setPadding(mInputPaddingSize, 0, mInputPaddingSize, 0);
}
}
return view;
}
@ -92,7 +94,8 @@ public class Prompt implements OnClickListener, OnCancelListener, OnItemClickLis
processMessage(message);
}
public void show(String title, String text, PromptListItem[] listItems, boolean multipleSelection) {
public void show(String title, String text, PromptListItem[] listItems, int choiceMode) {
ThreadUtils.assertOnUiThread();
GeckoAppShell.getLayerView().abortPanning();
@ -109,7 +112,7 @@ public class Prompt implements OnClickListener, OnCancelListener, OnItemClickLis
// Because lists are currently added through the normal Android AlertBuilder interface, they're
// incompatible with also adding additional input elements to a dialog.
if (listItems != null && listItems.length > 0) {
addlistItems(builder, listItems, multipleSelection);
addListItems(builder, listItems, choiceMode);
} else if (!addInputs(builder)) {
// If we failed to add any requested input elements, don't show the dialog
return;
@ -146,22 +149,25 @@ public class Prompt implements OnClickListener, OnCancelListener, OnItemClickLis
*/
private void addListResult(final JSONObject result, int which) {
try {
if (mSelected != null) {
JSONArray selected = new JSONArray();
for (int i = 0; i < mSelected.length; i++) {
if (mSelected[i]) {
selected.put(i);
}
JSONArray selected = new JSONArray();
// If the button has already been filled in
ArrayList<Integer> selectedItems = mAdapter.getSelected();
for (Integer item : selectedItems) {
selected.put(item);
}
// If we haven't assigned a button yet, or we assigned it to -1, assign the which
// parameter to both selected and the button.
if (!result.has("button") || result.optInt("button") == -1) {
if (!selectedItems.contains(which)) {
selected.put(which);
}
result.put("list", selected);
} else {
// Mirror the selected array from multi choice for consistency.
JSONArray selected = new JSONArray();
selected.put(which);
result.put("list", selected);
// Make the button be the index of the select item.
result.put("button", which);
}
result.put("list", selected);
} catch(JSONException ex) { }
}
@ -199,11 +205,10 @@ public class Prompt implements OnClickListener, OnCancelListener, OnItemClickLis
ThreadUtils.assertOnUiThread();
JSONObject ret = new JSONObject();
try {
ListView list = mDialog.getListView();
addButtonResult(ret, which);
addInputValues(ret);
if (list != null || mSelected != null) {
if (mAdapter != null) {
addListResult(ret, which);
}
} catch(Exception ex) {
@ -218,26 +223,27 @@ public class Prompt implements OnClickListener, OnCancelListener, OnItemClickLis
}
/* Adds a set of list items to the prompt. This can be used for either context menu type dialogs, checked lists,
* or multiple selection lists. If mSelected is set in the prompt before addlistItems is called, the items will be
* shown with "checkmarks" on their left side.
* or multiple selection lists.
*
* @param builder
* The alert builder currently building this dialog.
* @param listItems
* The items to add.
* @param multipleSelection
* If true, and mSelected is defined to be a non-zero-length list, the list will show checkmarks on the
* left and allow multiple selection.
* @param choiceMode
* One of the ListView.CHOICE_MODE constants to designate whether this list shows checkmarks, radios buttons, or nothing.
*/
private void addlistItems(AlertDialog.Builder builder, PromptListItem[] listItems, boolean multipleSelection) {
if (mSelected != null && mSelected.length > 0) {
if (multipleSelection) {
private void addListItems(AlertDialog.Builder builder, PromptListItem[] listItems, int choiceMode) {
switch(choiceMode) {
case ListView.CHOICE_MODE_MULTIPLE_MODAL:
case ListView.CHOICE_MODE_MULTIPLE:
addMultiSelectList(builder, listItems);
} else {
break;
case ListView.CHOICE_MODE_SINGLE:
addSingleSelectList(builder, listItems);
}
} else {
addMenuList(builder, listItems);
break;
case ListView.CHOICE_MODE_NONE:
default:
addMenuList(builder, listItems);
}
}
@ -251,13 +257,13 @@ public class Prompt implements OnClickListener, OnCancelListener, OnItemClickLis
* The items to add.
*/
private void addMultiSelectList(AlertDialog.Builder builder, PromptListItem[] listItems) {
PromptListAdapter adapter = new PromptListAdapter(mContext, R.layout.select_dialog_multichoice, listItems);
adapter.listView = (ListView) mInflater.inflate(R.layout.select_dialog_list, null);
adapter.listView.setOnItemClickListener(this);
builder.setInverseBackgroundForced(true);
adapter.listView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE);
adapter.listView.setAdapter(adapter);
builder.setView(adapter.listView);
ListView listView = (ListView) mInflater.inflate(R.layout.select_dialog_list, null);
listView.setOnItemClickListener(this);
listView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE);
mAdapter = new PromptListAdapter(mContext, R.layout.select_dialog_multichoice, listItems);
listView.setAdapter(mAdapter);
builder.setView(listView);
}
/* Shows a single-select list with radio boxes on the side.
@ -268,18 +274,8 @@ public class Prompt implements OnClickListener, OnCancelListener, OnItemClickLis
* The items to add.
*/
private void addSingleSelectList(AlertDialog.Builder builder, PromptListItem[] listItems) {
PromptListAdapter adapter = new PromptListAdapter(mContext, R.layout.select_dialog_singlechoice, listItems);
// For single select, we only maintain a single index of the selected row
int selectedIndex = -1;
for (int i = 0; i < mSelected.length; i++) {
if (mSelected[i]) {
selectedIndex = i;
break;
}
}
mSelected = null;
builder.setSingleChoiceItems(adapter, selectedIndex, this);
mAdapter = new PromptListAdapter(mContext, R.layout.select_dialog_singlechoice, listItems);
builder.setSingleChoiceItems(mAdapter, mAdapter.getSelectedIndex(), this);
}
/* Shows a single-select list.
@ -290,12 +286,10 @@ public class Prompt implements OnClickListener, OnCancelListener, OnItemClickLis
* The items to add.
*/
private void addMenuList(AlertDialog.Builder builder, PromptListItem[] listItems) {
PromptListAdapter adapter = new PromptListAdapter(mContext, android.R.layout.simple_list_item_1, listItems);
builder.setAdapter(adapter, this);
mSelected = null;
mAdapter = new PromptListAdapter(mContext, android.R.layout.simple_list_item_1, listItems);
builder.setAdapter(mAdapter, this);
}
/* Wraps an input in a linearlayout. We do this so that we can set padding that appears outside the background
* drawable for the view.
*/
@ -333,13 +327,11 @@ public class Prompt implements OnClickListener, OnCancelListener, OnItemClickLis
} else if (length > 1) {
LinearLayout linearLayout = new LinearLayout(mContext);
linearLayout.setOrientation(LinearLayout.VERTICAL);
for (int i = 0; i < length; i++) {
View content = wrapInput(mInputs[i]);
linearLayout.addView(content);
scrollable |= mInputs[i].getScrollable();
}
root = linearLayout;
}
@ -367,7 +359,7 @@ public class Prompt implements OnClickListener, OnCancelListener, OnItemClickLis
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
ThreadUtils.assertOnUiThread();
mSelected[position] = !mSelected[position];
mAdapter.toggleSelected(position);
}
/* @DialogInterface.OnCancelListener
@ -402,7 +394,6 @@ public class Prompt implements OnClickListener, OnCancelListener, OnItemClickLis
mInputs = null;
mButtons = null;
mDialog = null;
mSelected = null;
try {
aReturn.put("guid", mGuid);
} catch(JSONException ex) { }
@ -433,10 +424,17 @@ public class Prompt implements OnClickListener, OnCancelListener, OnItemClickLis
} catch(Exception ex) { }
}
PromptListItem[] menuitems = getListItemArray(geckoObject, "listitems");
mSelected = getBooleanArray(geckoObject, "selected");
boolean multiple = geckoObject.optBoolean("multiple");
show(title, text, menuitems, multiple);
PromptListItem[] menuitems = PromptListItem.getArray(geckoObject.optJSONArray("listitems"));
String selected = geckoObject.optString("choiceMode");
int choiceMode = ListView.CHOICE_MODE_NONE;
if ("single".equals(selected)) {
choiceMode = ListView.CHOICE_MODE_SINGLE;
} else if ("multiple".equals(selected)) {
choiceMode = ListView.CHOICE_MODE_MULTIPLE;
}
show(title, text, menuitems, choiceMode);
}
private static JSONArray getSafeArray(JSONObject json, String key) {
@ -474,189 +472,7 @@ public class Prompt implements OnClickListener, OnCancelListener, OnItemClickLis
return list;
}
private PromptListItem[] getListItemArray(JSONObject aObject, String aName) {
JSONArray items = getSafeArray(aObject, aName);
int length = items.length();
PromptListItem[] list = new PromptListItem[length];
for (int i = 0; i < length; i++) {
try {
list[i] = new PromptListItem(items.getJSONObject(i));
} catch(Exception ex) { }
}
return list;
}
public static class PromptListItem {
public final String label;
public final boolean isGroup;
public final boolean inGroup;
public final boolean disabled;
public final int id;
public final boolean isParent;
// This member can't be accessible from JS, see bug 733749.
public Drawable icon;
PromptListItem(JSONObject aObject) {
label = aObject.optString("label");
isGroup = aObject.optBoolean("isGroup");
inGroup = aObject.optBoolean("inGroup");
disabled = aObject.optBoolean("disabled");
id = aObject.optInt("id");
isParent = aObject.optBoolean("isParent");
}
public PromptListItem(String aLabel) {
label = aLabel;
isGroup = false;
inGroup = false;
disabled = false;
id = 0;
isParent = false;
}
}
public interface PromptCallback {
public void onPromptFinished(String jsonResult);
}
public class PromptListAdapter extends ArrayAdapter<PromptListItem> {
private static final int VIEW_TYPE_ITEM = 0;
private static final int VIEW_TYPE_GROUP = 1;
private static final int VIEW_TYPE_COUNT = 2;
public ListView listView;
private int mResourceId = -1;
private Drawable mBlankDrawable = null;
private Drawable mMoreDrawable = null;
PromptListAdapter(Context context, int textViewResourceId, PromptListItem[] objects) {
super(context, textViewResourceId, objects);
mResourceId = textViewResourceId;
}
@Override
public int getItemViewType(int position) {
PromptListItem item = getItem(position);
return (item.isGroup ? VIEW_TYPE_GROUP : VIEW_TYPE_ITEM);
}
@Override
public int getViewTypeCount() {
return VIEW_TYPE_COUNT;
}
private Drawable getMoreDrawable(Resources res) {
if (mMoreDrawable == null) {
mMoreDrawable = res.getDrawable(android.R.drawable.ic_menu_more);
}
return mMoreDrawable;
}
private Drawable getBlankDrawable(Resources res) {
if (mBlankDrawable == null) {
mBlankDrawable = res.getDrawable(R.drawable.blank);
}
return mBlankDrawable;
}
private void maybeUpdateIcon(PromptListItem item, TextView t) {
if (item.icon == null && !item.inGroup && !item.isParent) {
t.setCompoundDrawablesWithIntrinsicBounds(null, null, null, null);
return;
}
Drawable d = null;
Resources res = mContext.getResources();
// Set the padding between the icon and the text.
t.setCompoundDrawablePadding(mIconTextPadding);
if (item.icon != null) {
// We want the icon to be of a specific size. Some do not
// follow this rule so we have to resize them.
Bitmap bitmap = ((BitmapDrawable) item.icon).getBitmap();
d = new BitmapDrawable(res, Bitmap.createScaledBitmap(bitmap, mIconSize, mIconSize, true));
} else if (item.inGroup) {
// We don't currently support "indenting" items with icons
d = getBlankDrawable(res);
}
Drawable moreDrawable = null;
if (item.isParent) {
moreDrawable = getMoreDrawable(res);
}
if (d != null || moreDrawable != null) {
t.setCompoundDrawablesWithIntrinsicBounds(d, null, moreDrawable, null);
}
}
private void maybeUpdateCheckedState(int position, PromptListItem item, ViewHolder viewHolder) {
viewHolder.textView.setEnabled(!item.disabled && !item.isGroup);
viewHolder.textView.setClickable(item.isGroup || item.disabled);
if (mSelected == null) {
return;
}
CheckedTextView ct;
try {
ct = (CheckedTextView) viewHolder.textView;
// Apparently just using ct.setChecked(true) doesn't work, so this
// is stolen from the android source code as a way to set the checked
// state of these items
if (listView != null) {
listView.setItemChecked(position, mSelected[position]);
}
} catch (Exception e) {
return;
}
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
PromptListItem item = getItem(position);
ViewHolder viewHolder = null;
if (convertView == null) {
int resourceId = mResourceId;
if (item.isGroup) {
resourceId = R.layout.list_item_header;
}
convertView = mInflater.inflate(resourceId, null);
convertView.setMinimumHeight(mMinRowSize);
TextView tv = (TextView) convertView.findViewById(android.R.id.text1);
viewHolder = new ViewHolder(tv, tv.getPaddingLeft(), tv.getPaddingRight(),
tv.getPaddingTop(), tv.getPaddingBottom());
convertView.setTag(viewHolder);
} else {
viewHolder = (ViewHolder) convertView.getTag();
}
viewHolder.textView.setText(item.label);
maybeUpdateCheckedState(position, item, viewHolder);
maybeUpdateIcon(item, viewHolder.textView);
return convertView;
}
private class ViewHolder {
public final TextView textView;
public final int paddingLeft;
public final int paddingRight;
public final int paddingTop;
public final int paddingBottom;
ViewHolder(TextView aTextView, int aLeft, int aRight, int aTop, int aBottom) {
textView = aTextView;
paddingLeft = aLeft;
paddingRight = aRight;
paddingTop = aTop;
paddingBottom = aBottom;
}
}
}
}

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

@ -0,0 +1,210 @@
package org.mozilla.gecko.prompts;
import org.mozilla.gecko.R;
import org.json.JSONArray;
import org.json.JSONObject;
import org.json.JSONException;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.drawable.Drawable;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.AdapterView;
import android.widget.CheckedTextView;
import android.widget.TextView;
import android.widget.ListView;
import java.util.ArrayList;
public class PromptListAdapter extends ArrayAdapter<PromptListItem> {
private static final int VIEW_TYPE_ITEM = 0;
private static final int VIEW_TYPE_GROUP = 1;
private static final int VIEW_TYPE_COUNT = 2;
private static final String LOGTAG = "GeckoPromptListAdapter";
private final int mResourceId;
private Drawable mBlankDrawable;
private Drawable mMoreDrawable;
private static int mGroupPaddingSize;
private static int mLeftRightTextWithIconPadding;
private static int mTopBottomTextWithIconPadding;
private static int mIconSize;
private static int mMinRowSize;
private static int mIconTextPadding;
private static boolean mInitialized = false;
PromptListAdapter(Context context, int textViewResourceId, PromptListItem[] objects) {
super(context, textViewResourceId, objects);
mResourceId = textViewResourceId;
init();
}
private void init() {
if (!mInitialized) {
Resources res = getContext().getResources();
mGroupPaddingSize = (int) (res.getDimension(R.dimen.prompt_service_group_padding_size));
mLeftRightTextWithIconPadding = (int) (res.getDimension(R.dimen.prompt_service_left_right_text_with_icon_padding));
mTopBottomTextWithIconPadding = (int) (res.getDimension(R.dimen.prompt_service_top_bottom_text_with_icon_padding));
mIconTextPadding = (int) (res.getDimension(R.dimen.prompt_service_icon_text_padding));
mIconSize = (int) (res.getDimension(R.dimen.prompt_service_icon_size));
mMinRowSize = (int) (res.getDimension(R.dimen.prompt_service_min_list_item_height));
mInitialized = true;
}
}
@Override
public int getItemViewType(int position) {
PromptListItem item = getItem(position);
if (item.isGroup) {
return VIEW_TYPE_GROUP;
} else {
return VIEW_TYPE_ITEM;
}
}
@Override
public int getViewTypeCount() {
return VIEW_TYPE_COUNT;
}
private Drawable getMoreDrawable(Resources res) {
if (mMoreDrawable == null) {
mMoreDrawable = res.getDrawable(R.drawable.menu_item_more);
}
return mMoreDrawable;
}
private Drawable getBlankDrawable(Resources res) {
if (mBlankDrawable == null) {
mBlankDrawable = res.getDrawable(R.drawable.blank);
}
return mBlankDrawable;
}
public void toggleSelected(int position) {
PromptListItem item = getItem(position);
item.selected = !item.selected;
}
private void maybeUpdateIcon(PromptListItem item, TextView t) {
if (item.icon == null && !item.inGroup && !item.isParent) {
t.setCompoundDrawablesWithIntrinsicBounds(null, null, null, null);
return;
}
Drawable d = null;
Resources res = getContext().getResources();
// Set the padding between the icon and the text.
t.setCompoundDrawablePadding(mIconTextPadding);
if (item.icon != null) {
// We want the icon to be of a specific size. Some do not
// follow this rule so we have to resize them.
Bitmap bitmap = ((BitmapDrawable) item.icon).getBitmap();
d = new BitmapDrawable(res, Bitmap.createScaledBitmap(bitmap, mIconSize, mIconSize, true));
} else if (item.inGroup) {
// We don't currently support "indenting" items with icons
d = getBlankDrawable(res);
}
Drawable moreDrawable = null;
if (item.isParent) {
moreDrawable = getMoreDrawable(res);
}
if (d != null || moreDrawable != null) {
t.setCompoundDrawablesWithIntrinsicBounds(d, null, moreDrawable, null);
}
}
private void maybeUpdateCheckedState(ListView list, int position, PromptListItem item, ViewHolder viewHolder) {
viewHolder.textView.setEnabled(!item.disabled && !item.isGroup);
viewHolder.textView.setClickable(item.isGroup || item.disabled);
if (viewHolder.textView instanceof CheckedTextView) {
// Apparently just using ct.setChecked(true) doesn't work, so this
// is stolen from the android source code as a way to set the checked
// state of these items
list.setItemChecked(position, item.selected);
}
}
boolean isSelected(int position){
return getItem(position).selected;
}
ArrayList<Integer> getSelected() {
int length = getCount();
ArrayList<Integer> selected = new ArrayList<Integer>();
for (int i = 0; i< length; i++) {
if (isSelected(i)) {
selected.add(i);
}
}
return selected;
}
int getSelectedIndex() {
int length = getCount();
for (int i = 0; i< length; i++) {
if (isSelected(i)) {
return i;
}
}
return -1;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
PromptListItem item = getItem(position);
int type = getItemViewType(position);
ViewHolder viewHolder = null;
if (convertView == null) {
int resourceId = mResourceId;
if (item.isGroup) {
resourceId = R.layout.list_item_header;
}
LayoutInflater mInflater = LayoutInflater.from(getContext());
convertView = mInflater.inflate(resourceId, null);
convertView.setMinimumHeight(mMinRowSize);
TextView tv = (TextView) convertView.findViewById(android.R.id.text1);
viewHolder = new ViewHolder(tv, tv.getPaddingLeft(), tv.getPaddingRight(),
tv.getPaddingTop(), tv.getPaddingBottom());
convertView.setTag(viewHolder);
} else {
viewHolder = (ViewHolder) convertView.getTag();
}
viewHolder.textView.setText(item.label);
maybeUpdateCheckedState((ListView) parent, position, item, viewHolder);
maybeUpdateIcon(item, viewHolder.textView);
return convertView;
}
private static class ViewHolder {
public final TextView textView;
public final int paddingLeft;
public final int paddingRight;
public final int paddingTop;
public final int paddingBottom;
ViewHolder(TextView aTextView, int aLeft, int aRight, int aTop, int aBottom) {
textView = aTextView;
paddingLeft = aLeft;
paddingRight = aRight;
paddingTop = aTop;
paddingBottom = aBottom;
}
}
}

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

@ -0,0 +1,60 @@
package org.mozilla.gecko.prompts;
import org.json.JSONArray;
import org.json.JSONObject;
import org.json.JSONException;
import android.graphics.drawable.Drawable;
import java.util.List;
import java.util.ArrayList;
// This class should die and be replaced with normal menu items
public class PromptListItem {
private static final String LOGTAG = "GeckoPromptListItem";
public final String label;
public final boolean isGroup;
public final boolean inGroup;
public final boolean disabled;
public final int id;
public boolean selected;
public boolean isParent;
public Drawable icon;
PromptListItem(JSONObject aObject) {
label = aObject.optString("label");
isGroup = aObject.optBoolean("isGroup");
inGroup = aObject.optBoolean("inGroup");
disabled = aObject.optBoolean("disabled");
id = aObject.optInt("id");
isParent = aObject.optBoolean("isParent");
selected = aObject.optBoolean("selected");
}
public PromptListItem(String aLabel) {
label = aLabel;
isGroup = false;
inGroup = false;
disabled = false;
id = 0;
}
static PromptListItem[] getArray(JSONArray items) {
if (items == null) {
return new PromptListItem[0];
}
int length = items.length();
List<PromptListItem> list = new ArrayList<PromptListItem>(length);
for (int i = 0; i < length; i++) {
try {
PromptListItem item = new PromptListItem(items.getJSONObject(i));
list.add(item);
} catch(Exception ex) { }
}
PromptListItem[] arrays = new PromptListItem[length];
list.toArray(arrays);
return arrays;
}
}

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

@ -31,15 +31,15 @@ public class PromptService implements GeckoEventListener {
GeckoAppShell.getEventDispatcher().unregisterEventListener("Prompt:ShowTop", this);
}
public void show(final String aTitle, final String aText, final Prompt.PromptListItem[] aMenuList,
final boolean aMultipleSelection, final Prompt.PromptCallback callback) {
public void show(final String aTitle, final String aText, final PromptListItem[] aMenuList,
final int aChoiceMode, final Prompt.PromptCallback callback) {
// The dialog must be created on the UI thread.
ThreadUtils.postToUiThread(new Runnable() {
@Override
public void run() {
Prompt p;
p = new Prompt(mContext, callback);
p.show(aTitle, aText, aMenuList, aMultipleSelection);
p.show(aTitle, aText, aMenuList, aChoiceMode);
}
});
}

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

@ -28,7 +28,8 @@
<FrameLayout android:id="@+id/home_pager_container"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
android:layout_height="fill_parent"
android:visibility="gone">
<ViewStub android:id="@+id/home_pager_stub"
android:layout="@layout/home_pager"

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

@ -22,6 +22,10 @@ public class testHomeBanner extends UITest {
// These test methods depend on being run in this order.
addBannerTest();
// Make sure the banner hides when the user starts interacting with the url bar.
hideOnToolbarFocusTest();
// TODO: API doesn't actually support this but it used to work due to how the banner was
// part of TopSitesPanel's lifecycle
// removeBannerTest();
@ -96,6 +100,18 @@ public class testHomeBanner extends UITest {
mAboutHome.assertBannerNotVisible();
}
private void hideOnToolbarFocusTest() {
NavigationHelper.enterAndLoadUrl("about:home");
mAboutHome.assertVisible()
.assertBannerVisible();
mToolbar.enterEditingMode();
mAboutHome.assertBannerNotVisible();
mToolbar.dismissEditingMode();
mAboutHome.assertBannerVisible();
}
/**
* Loads the roboextender page to add a message to the banner.
*/

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

@ -137,6 +137,7 @@ public class BrowserToolbar extends GeckoRelativeLayout
private OnCommitListener mCommitListener;
private OnDismissListener mDismissListener;
private OnFilterListener mFilterListener;
private OnFocusChangeListener mFocusChangeListener;
private OnStartEditingListener mStartEditingListener;
private OnStopEditingListener mStopEditingListener;
@ -315,6 +316,9 @@ public class BrowserToolbar extends GeckoRelativeLayout
@Override
public void onFocusChange(View v, boolean hasFocus) {
setSelected(hasFocus);
if (mFocusChangeListener != null) {
mFocusChangeListener.onFocusChange(v, hasFocus);
}
}
});
@ -785,6 +789,10 @@ public class BrowserToolbar extends GeckoRelativeLayout
mUrlEditLayout.setOnFilterListener(listener);
}
public void setOnFocusChangeListener(OnFocusChangeListener listener) {
mFocusChangeListener = listener;
}
public void setOnStartEditingListener(OnStartEditingListener listener) {
mStartEditingListener = listener;
}

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

@ -319,8 +319,6 @@ public class ActivityChooserModel extends DataSetObservable {
*/
private boolean mReloadActivities = false;
private long mLastChanged = 0;
/**
* Policy for controlling how the model handles chosen activities.
*/
@ -747,7 +745,6 @@ public class ActivityChooserModel extends DataSetObservable {
ResolveInfo resolveInfo = resolveInfos.get(i);
mActivities.add(new ActivityResolveInfo(resolveInfo));
}
mLastChanged = System.currentTimeMillis();
return true;
}
return false;
@ -1223,11 +1220,7 @@ public class ActivityChooserModel extends DataSetObservable {
}
mReloadActivities = true;
mLastChanged = System.currentTimeMillis();
}
}
public long getLastChanged() {
return mLastChanged;
}
}

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

@ -21,7 +21,6 @@ import android.view.View.OnClickListener;
public class GeckoActionProvider extends ActionProvider {
private static int MAX_HISTORY_SIZE = 2;
private long mLastChanged = 0;
/**
* A listener to know when a target was selected.
@ -80,14 +79,6 @@ public class GeckoActionProvider extends ActionProvider {
return onCreateActionView();
}
public boolean hasChanged() {
ActivityChooserModel dataModel = ActivityChooserModel.get(mContext, mHistoryFileName);
long lastChanged = dataModel.getLastChanged();
boolean ret = lastChanged != mLastChanged;
mLastChanged = lastChanged;
return ret;
}
@Override
public boolean hasSubMenu() {
return true;

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

@ -27,8 +27,9 @@ function App(data) {
}
App.prototype = {
launch: function(uri) {
HelperApps._launchApp(this, uri);
// callback will be null if a result is not requested
launch: function(uri, callback) {
HelperApps._launchApp(this, uri, callback);
return false;
}
}
@ -166,13 +167,24 @@ var HelperApps = {
};
},
_launchApp: function launchApp(app, uri) {
let msg = this._getMessage("Intent:Open", uri, {
packageName: app.packageName,
className: app.activityName
});
_launchApp: function launchApp(app, uri, callback) {
if (callback) {
let msg = this._getMessage("Intent:OpenForResult", uri, {
packageName: app.packageName,
className: app.activityName
});
sendMessageToJava(msg);
sendMessageToJava(msg, function(data) {
callback(JSON.parse(data));
});
} else {
let msg = this._getMessage("Intent:Open", uri, {
packageName: app.packageName,
className: app.activityName
});
sendMessageToJava(msg);
}
},
_sendMessageSync: function(msg) {

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

@ -156,12 +156,9 @@ let HomeBanner = (function () {
});
})();
function Panel(options) {
if ("id" in options)
this.id = options.id;
if ("title" in options)
this.title = options.title;
function Panel(id, options) {
this.id = id;
this.title = options.title;
if ("layout" in options)
this.layout = options.layout;
@ -170,30 +167,101 @@ function Panel(options) {
this.views = options.views;
}
let HomePanels = (function () {
// Holds the currrent set of registered panels.
let _panels = {};
// We need this function to have access to the HomePanels
// private members without leaking it outside Home.jsm.
let handlePanelsGet;
let _panelToJSON = function(panel) {
return {
id: panel.id,
title: panel.title,
layout: panel.layout,
views: panel.views
};
let HomePanels = (function () {
// Holds the current set of registered panels that can be
// installed, updated, uninstalled, or unregistered. It maps
// panel ids with the functions that dynamically generate
// their respective panel options. This is used to retrieve
// the current list of available panels in the system.
// See HomePanels:Get handler.
let _registeredPanels = {};
// Valid layouts for a panel.
let Layout = Object.freeze({
FRAME: "frame"
});
// Valid types of views for a dataset.
let View = Object.freeze({
LIST: "list",
GRID: "grid"
});
// Valid item types for a panel view.
let Item = Object.freeze({
ARTICLE: "article",
IMAGE: "image"
});
// Valid item handlers for a panel view.
let ItemHandler = Object.freeze({
BROWSER: "browser",
INTENT: "intent"
});
let _generatePanel = function(id) {
let panel = new Panel(id, _registeredPanels[id]());
if (!panel.id || !panel.title) {
throw "Home.panels: Can't create a home panel without an id and title!";
}
if (!panel.layout) {
// Use FRAME layout by default
panel.layout = Layout.FRAME;
} else if (!_valueExists(Layout, panel.layout)) {
throw "Home.panels: Invalid layout for panel: panel.id = " + panel.id + ", panel.layout =" + panel.layout;
}
for (let view of panel.views) {
if (!_valueExists(View, view.type)) {
throw "Home.panels: Invalid view type: panel.id = " + panel.id + ", view.type = " + view.type;
}
if (!view.itemType) {
if (view.type == View.LIST) {
// Use ARTICLE item type by default in LIST views
view.itemType = Item.ARTICLE;
} else if (view.type == View.GRID) {
// Use IMAGE item type by default in GRID views
view.itemType = Item.IMAGE;
}
} else if (!_valueExists(Item, view.itemType)) {
throw "Home.panels: Invalid item type: panel.id = " + panel.id + ", view.itemType = " + view.itemType;
}
if (!view.itemHandler) {
// Use BROWSER item handler by default
view.itemHandler = ItemHandler.BROWSER;
} else if (!_valueExists(ItemHandler, view.itemHandler)) {
throw "Home.panels: Invalid item handler: panel.id = " + panel.id + ", view.itemHandler = " + view.itemHandler;
}
if (!view.dataset) {
throw "Home.panels: No dataset provided for view: panel.id = " + panel.id + ", view.type = " + view.type;
}
}
return panel;
};
let _handleGet = function(data) {
handlePanelsGet = function(data) {
let requestId = data.requestId;
let ids = data.ids || null;
let panels = [];
for (let id in _panels) {
let panel = _panels[id];
for (let id in _registeredPanels) {
// Null ids means we want to fetch all available panels
if (ids == null || ids.indexOf(panel.id) >= 0) {
panels.push(_panelToJSON(panel));
if (ids == null || ids.indexOf(id) >= 0) {
try {
panels.push(_generatePanel(id));
} catch(e) {
Cu.reportError("Home.panels: Invalid options, panel.id = " + id + ": " + e);
}
}
}
@ -215,87 +283,34 @@ let HomePanels = (function () {
};
let _assertPanelExists = function(id) {
if (!(id in _panels)) {
if (!(id in _registeredPanels)) {
throw "Home.panels: Panel doesn't exist: id = " + id;
}
};
return Object.freeze({
// Valid layouts for a panel.
Layout: Object.freeze({
FRAME: "frame"
}),
// Valid types of views for a dataset.
View: Object.freeze({
LIST: "list",
GRID: "grid"
}),
// Valid item types for a panel view.
Item: Object.freeze({
ARTICLE: "article",
IMAGE: "image"
}),
// Valid item handlers for a panel view.
ItemHandler: Object.freeze({
BROWSER: "browser",
INTENT: "intent"
}),
register: function(options) {
let panel = new Panel(options);
Layout: Layout,
View: View,
Item: Item,
ItemHandler: ItemHandler,
register: function(id, optionsCallback) {
// Bail if the panel already exists
if (panel.id in _panels) {
throw "Home.panels: Panel already exists: id = " + panel.id;
if (id in _registeredPanels) {
throw "Home.panels: Panel already exists: id = " + id;
}
if (!panel.id || !panel.title) {
throw "Home.panels: Can't create a home panel without an id and title!";
if (!optionsCallback || typeof optionsCallback !== "function") {
throw "Home.panels: Panel callback must be a function: id = " + id;
}
if (!_valueExists(this.Layout, panel.layout)) {
throw "Home.panels: Invalid layout for panel: panel.id = " + panel.id + ", panel.layout =" + panel.layout;
}
for (let view of panel.views) {
if (!_valueExists(this.View, view.type)) {
throw "Home.panels: Invalid view type: panel.id = " + panel.id + ", view.type = " + view.type;
}
if (!view.itemType) {
if (view.type == this.View.LIST) {
// Use ARTICLE item type by default in LIST views
view.itemType = this.Item.ARTICLE;
} else if (view.type == this.View.GRID) {
// Use IMAGE item type by default in GRID views
view.itemType = this.Item.IMAGE;
}
} else if (!_valueExists(this.Item, view.itemType)) {
throw "Home.panels: Invalid item type: panel.id = " + panel.id + ", view.itemType = " + view.itemType;
}
if (!view.itemHandler) {
// Use BROWSER item handler by default
view.itemHandler = this.ItemHandler.BROWSER;
} else if (!_valueExists(this.ItemHandler, view.itemHandler)) {
throw "Home.panels: Invalid item handler: panel.id = " + panel.id + ", view.itemHandler = " + view.itemHandler;
}
if (!view.dataset) {
throw "Home.panels: No dataset provided for view: panel.id = " + panel.id + ", view.type = " + view.type;
}
}
_panels[panel.id] = panel;
_registeredPanels[id] = optionsCallback;
},
unregister: function(id) {
_assertPanelExists(id);
delete _panels[id];
delete _registeredPanels[id];
},
install: function(id) {
@ -303,7 +318,7 @@ let HomePanels = (function () {
sendMessageToJava({
type: "HomePanels:Install",
panel: _panelToJSON(_panels[id])
panel: _generatePanel(id)
});
},
@ -321,7 +336,7 @@ let HomePanels = (function () {
sendMessageToJava({
type: "HomePanels:Update",
panel: _panelToJSON(_panels[id])
panel: _generatePanel(id)
});
}
});
@ -336,7 +351,7 @@ this.Home = Object.freeze({
observe: function(subject, topic, data) {
switch(topic) {
case "HomePanels:Get":
HomePanels._handleGet(JSON.parse(data));
handlePanelsGet(JSON.parse(data));
break;
}
}

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

@ -179,12 +179,11 @@ Prompt.prototype = {
if (item.disabled)
obj.disabled = true;
if (item.selected || hasSelected || this.msg.multiple) {
if (!this.msg.selected) {
this.msg.selected = new Array(this.msg.listitems.length);
hasSelected = true;
if (item.selected) {
if (!this.msg.choiceMode) {
this.msg.choiceMode = "single";
}
this.msg.selected[this.msg.listitems.length] = item.selected;
obj.selected = item.selected;
}
if (item.header)
@ -207,7 +206,7 @@ Prompt.prototype = {
},
setMultiChoiceItems: function(aItems) {
this.msg.multiple = true;
this.msg.choiceMode = "multiple";
return this._setListItems(aItems);
},

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

@ -7,7 +7,7 @@
android:versionName="1.0" >
<uses-sdk android:minSdkVersion="8"
android:targetSdkVersion="14" />
android:targetSdkVersion="16" />
<uses-permission android:name="@ANDROID_BACKGROUND_TARGET_PACKAGE_NAME@.permissions.BROWSER_PROVIDER"/>
<uses-permission android:name="@ANDROID_BACKGROUND_TARGET_PACKAGE_NAME@.permissions.FORMHISTORY_PROVIDER"/>

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

@ -2,18 +2,19 @@
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
ANDROID_APK_NAME := background-debug
ANDROID_APK_NAME := background-junit3-debug
PP_TARGETS += manifest
manifest := $(srcdir)/AndroidManifest.xml.in
manifest_TARGET := AndroidManifest.xml
manifest_FLAGS += \
-DANDROID_BACKGROUND_TARGET_PACKAGE_NAME='$(ANDROID_PACKAGE_NAME)' \
-DANDROID_BACKGROUND_TEST_PACKAGE_NAME='org.mozilla.background.test' \
-DANDROID_BACKGROUND_TEST_PACKAGE_NAME='org.mozilla.gecko.background.tests' \
-DANDROID_BACKGROUND_APP_DISPLAYNAME='$(MOZ_APP_DISPLAYNAME) Background Tests' \
-DMOZ_ANDROID_SHARED_ID='$(ANDROID_PACKAGE_NAME).sharedID' \
-DMOZ_ANDROID_SHARED_ACCOUNT_TYPE='$(ANDROID_PACKAGE_NAME)_sync' \
$(NULL)
ANDROID_MANIFEST_FILE := $(CURDIR)/AndroidManifest.xml
GARBAGE += AndroidManifest.xml

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

@ -4,14 +4,15 @@
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
DEFINES['ANDROID_PACKAGE_NAME'] = CONFIG['ANDROID_PACKAGE_NAME']
include('android-services.mozbuild')
main = add_android_eclipse_project('BackgroundInstrumentationTests', OBJDIR + '/AndroidManifest.xml')
main.package_name = 'org.mozilla.background.test'
main.package_name = 'org.mozilla.gecko.background.tests'
main.res = SRCDIR + '/res'
main.recursive_make_targets += [
OBJDIR + '/AndroidManifest.xml',
TOPOBJDIR + '/mobile/android/base/tests/TestConstants.java']
OBJDIR + '/AndroidManifest.xml']
main.referenced_projects += ['Fennec']
main.add_classpathentry('src', SRCDIR + '/src',

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

@ -0,0 +1,26 @@
#filter substitution
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="@ANDROID_BROWSER_TEST_PACKAGE_NAME@"
sharedUserId="@MOZ_ANDROID_SHARED_ID@"
android:versionCode="1"
android:versionName="1.0" >
<uses-sdk android:minSdkVersion="8"
android:targetSdkVersion="16" />
<uses-permission android:name="@ANDROID_BROWSER_TARGET_PACKAGE_NAME@.permissions.BROWSER_PROVIDER"/>
<uses-permission android:name="@ANDROID_BROWSER_TARGET_PACKAGE_NAME@.permissions.FORMHISTORY_PROVIDER"/>
<uses-permission android:name="@ANDROID_BROWSER_TARGET_PACKAGE_NAME@.permissions.PASSWORD_PROVIDER"/>
<application
android:icon="@drawable/icon"
android:label="@ANDROID_BROWSER_APP_DISPLAYNAME@">
<uses-library android:name="android.test.runner" />
</application>
<instrumentation
android:label="@string/app_name"
android:name="org.mozilla.browser.harness.BrowserInstrumentationTestRunner"
android:targetPackage="@ANDROID_BROWSER_TARGET_PACKAGE_NAME@" />
</manifest>

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

@ -0,0 +1,32 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
ANDROID_APK_NAME := browser-junit3-debug
ANDROID_EXTRA_JARS += \
browser-junit3.jar \
$(NULL)
PP_TARGETS += manifest
manifest := AndroidManifest.xml.in
manifest_FLAGS += \
-DANDROID_BROWSER_TARGET_PACKAGE_NAME='$(ANDROID_PACKAGE_NAME)' \
-DANDROID_BROWSER_TEST_PACKAGE_NAME='org.mozilla.gecko.browser.tests' \
-DANDROID_BROWSER_APP_DISPLAYNAME='$(MOZ_APP_DISPLAYNAME) Browser Tests' \
-DMOZ_ANDROID_SHARED_ID='$(ANDROID_PACKAGE_NAME).sharedID' \
$(NULL)
ANDROID_MANIFEST_FILE := $(CURDIR)/AndroidManifest.xml
include $(topsrcdir)/config/rules.mk
tools:: $(ANDROID_APK_NAME).apk
# The test APK needs to know the contents of the target APK while not
# being linked against them. This is a best effort to avoid getting
# out of sync with base's build config.
JARS_DIR := $(DEPTH)/mobile/android/base
JAVA_BOOTCLASSPATH := $(JAVA_BOOTCLASSPATH):$(subst $(NULL) ,:,$(wildcard $(JARS_DIR)/*.jar))
# We also want to re-compile classes.dex when the associated base
# content changes.
classes.dex: $(wildcard $(JARS_DIR)/*.jar)

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

@ -0,0 +1,33 @@
# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
# vim: set filetype=python:
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
DEFINES['ANDROID_PACKAGE_NAME'] = CONFIG['ANDROID_PACKAGE_NAME']
jar = add_java_jar('browser-junit3')
jar.sources += [
'src/harness/BrowserInstrumentationTestRunner.java',
'src/harness/BrowserTestListener.java',
'src/tests/BrowserTestCase.java',
'src/tests/TestJarReader.java',
]
jar.generated_sources = [] # None yet -- try to keep it this way.
jar.javac_flags += ['-Xlint:all,-unchecked']
# Android Eclipse project.
main = add_android_eclipse_project('BrowserInstrumentationTests', OBJDIR + '/AndroidManifest.xml')
# The package name doesn't really matter, but it looks nicest if the
# generated classes (org.mozilla.gecko.browser.tests.{BuildConfig,R})
# are in the same hierarchy as the rest of the source files.
main.package_name = 'org.mozilla.gecko.browser.tests'
main.res = 'res'
main.recursive_make_targets += [
OBJDIR + '/AndroidManifest.xml',
]
main.recursive_make_targets += [OBJDIR + '/generated/' + f for f in jar.generated_sources]
main.referenced_projects += ['Fennec']
main.add_classpathentry('src', SRCDIR + '/src',
dstdir='src/org/mozilla/gecko/browser')

Двоичные данные
mobile/android/tests/browser/junit3/res/drawable-hdpi/icon.png Normal file

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

После

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

Двоичные данные
mobile/android/tests/browser/junit3/res/drawable-ldpi/icon.png Normal file

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

После

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

Двоичные данные
mobile/android/tests/browser/junit3/res/drawable-mdpi/icon.png Normal file

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

После

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

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

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical" >
<TextView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="@string/app_name" />
</LinearLayout>

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

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Gecko Browser Tests</string>
</resources>

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

@ -0,0 +1,33 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
package org.mozilla.gecko.browser.harness;
import android.os.Bundle;
import android.test.AndroidTestRunner;
import android.test.InstrumentationTestRunner;
import android.util.Log;
/**
* A test runner that installs a special test listener.
* <p>
* In future, this listener will turn JUnit 3 test events into log messages in
* the format that Mochitest parsers understand.
*/
public class BrowserInstrumentationTestRunner extends InstrumentationTestRunner {
private static final String LOG_TAG = "BInstTestRunner";
@Override
public void onCreate(Bundle arguments) {
Log.d(LOG_TAG, "onCreate");
super.onCreate(arguments);
}
@Override
protected AndroidTestRunner getAndroidTestRunner() {
Log.d(LOG_TAG, "getAndroidTestRunner");
AndroidTestRunner testRunner = super.getAndroidTestRunner();
testRunner.addTestListener(new BrowserTestListener());
return testRunner;
}
}

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

@ -0,0 +1,42 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
package org.mozilla.gecko.browser.harness;
import junit.framework.AssertionFailedError;
import junit.framework.Test;
import junit.framework.TestListener;
import android.util.Log;
/**
* BrowserTestListener turns JUnit 3 test events into log messages in the format
* that Mochitest parsers understand.
* <p>
* The idea is that, on infrastructure, we'll be able to use the same test
* parsing code for Browser JUnit 3 tests as we do for Robocop tests.
* <p>
* In future, that is!
*/
public class BrowserTestListener implements TestListener {
public static final String LOG_TAG = "BTestListener";
@Override
public void startTest(Test test) {
Log.d(LOG_TAG, "startTest: " + test);
}
@Override
public void endTest(Test test) {
Log.d(LOG_TAG, "endTest: " + test);
}
@Override
public void addFailure(Test test, AssertionFailedError t) {
Log.d(LOG_TAG, "addFailure: " + test);
}
@Override
public void addError(Test test, Throwable t) {
Log.d(LOG_TAG, "addError: " + test);
}
}

Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше