зеркало из https://github.com/mozilla/gecko-dev.git
Merge fx-team to m-c.
This commit is contained in:
Коммит
8333762d4c
|
@ -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,10 +2127,12 @@ let CustomizableUIInternal = {
|
|||
if (area.get("type") == CustomizableUI.TYPE_TOOLBAR) {
|
||||
let defaultCollapsed = area.get("defaultCollapsed");
|
||||
let win = areaNode.ownerDocument.defaultView;
|
||||
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();
|
||||
// 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();
|
||||
}
|
||||
},
|
||||
|
||||
_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.
|
||||
|
@ -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"),
|
||||
|
|
|
@ -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="µphone.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();
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 = [{
|
||||
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;
|
||||
|
@ -107,6 +108,17 @@ public class HomeBanner extends LinearLayout
|
|||
GeckoAppShell.getEventDispatcher().unregisterEventListener("HomeBanner:Data", this);
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
@ -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);
|
||||
|
||||
// If the button has already been filled in
|
||||
ArrayList<Integer> selectedItems = mAdapter.getSelected();
|
||||
for (Integer item : selectedItems) {
|
||||
selected.put(item);
|
||||
}
|
||||
}
|
||||
result.put("list", selected);
|
||||
} else {
|
||||
// Mirror the selected array from multi choice for consistency.
|
||||
JSONArray selected = new JSONArray();
|
||||
|
||||
// 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);
|
||||
// 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,25 +223,26 @@ 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 {
|
||||
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) {
|
||||
_launchApp: function launchApp(app, uri, callback) {
|
||||
if (callback) {
|
||||
let msg = this._getMessage("Intent:OpenForResult", uri, {
|
||||
packageName: app.packageName,
|
||||
className: app.activityName
|
||||
});
|
||||
|
||||
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,11 +156,8 @@ let HomeBanner = (function () {
|
|||
});
|
||||
})();
|
||||
|
||||
function Panel(options) {
|
||||
if ("id" in options)
|
||||
this.id = options.id;
|
||||
|
||||
if ("title" in options)
|
||||
function Panel(id, options) {
|
||||
this.id = id;
|
||||
this.title = options.title;
|
||||
|
||||
if ("layout" in options)
|
||||
|
@ -170,30 +167,101 @@ function Panel(options) {
|
|||
this.views = options.views;
|
||||
}
|
||||
|
||||
// We need this function to have access to the HomePanels
|
||||
// private members without leaking it outside Home.jsm.
|
||||
let handlePanelsGet;
|
||||
|
||||
let HomePanels = (function () {
|
||||
// Holds the currrent set of registered panels.
|
||||
let _panels = {};
|
||||
// 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 = {};
|
||||
|
||||
let _panelToJSON = function(panel) {
|
||||
return {
|
||||
id: panel.id,
|
||||
title: panel.title,
|
||||
layout: panel.layout,
|
||||
views: panel.views
|
||||
};
|
||||
// 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')
|
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 7.7 KiB |
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 3.2 KiB |
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 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);
|
||||
}
|
||||
}
|
Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше
Загрузка…
Ссылка в новой задаче