Merge fx-team to m-c a=merge

This commit is contained in:
Wes Kocher 2015-02-25 17:39:55 -08:00
commit 6a521db2ac
65 changed files with 19116 additions and 17007 deletions

View File

@ -32,11 +32,7 @@ let SidebarUI = {
this._title = document.getElementById("sidebar-title");
this._splitter = document.getElementById("sidebar-splitter");
if (window.opener && !window.opener.closed &&
window.opener.document.documentURIObject.schemeIs("chrome") &&
PrivateBrowsingUtils.isWindowPrivate(window) == PrivateBrowsingUtils.isWindowPrivate(window.opener)) {
this.adoptFromWindow(window.opener);
} else {
if (!this.adoptFromWindow(window.opener)) {
let commandID = this._box.getAttribute("sidebarcommand");
if (commandID) {
let command = document.getElementById(commandID);
@ -67,24 +63,40 @@ let SidebarUI = {
},
/**
* Adopt the status of the sidebar from another window.
* Try and adopt the status of the sidebar from another window.
* @param {Window} sourceWindow - Window to use as a source for sidebar status.
* @return true if we adopted the state, or false if the caller should
* initialize the state itself.
*/
adoptFromWindow(sourceWindow) {
// No source window, or it being closed, or not chrome, or in a different
// private-browsing context means we can't adopt.
if (!sourceWindow || sourceWindow.closed ||
!sourceWindow.document.documentURIObject.schemeIs("chrome") ||
PrivateBrowsingUtils.isWindowPrivate(window) != PrivateBrowsingUtils.isWindowPrivate(sourceWindow)) {
return false;
}
// If the opener had a sidebar, open the same sidebar in our window.
// The opener can be the hidden window too, if we're coming from the state
// where no windows are open, and the hidden window has no sidebar box.
let sourceUI = sourceWindow.SidebarUI;
if (!sourceUI || sourceUI._box.hidden) {
return;
if (!sourceUI || !sourceUI._box) {
// no source UI or no _box means we also can't adopt the state.
return false;
}
if (sourceUI._box.hidden) {
// just hidden means we have adopted the hidden state.
return true;
}
let commandID = sourceUI._box.getAttribute("sidebarcommand");
let commandElem = document.getElementById(commandID);
// dynamically generated sidebars will fail this check.
// dynamically generated sidebars will fail this check, but we still
// consider it adopted.
if (!commandElem) {
return;
return true;
}
this._title.setAttribute("value",
@ -101,6 +113,7 @@ let SidebarUI = {
this._box.hidden = false;
this._splitter.hidden = false;
commandElem.setAttribute("checked", "true");
return true;
},
/**

View File

@ -16,5 +16,7 @@ FIREFOX_PREFERENCES = {
"loop.seenToS": "seen",
# this dialog is fragile, and likely to introduce intermittent failures
"media.navigator.permission.disabled": True
"media.navigator.permission.disabled": True,
# Use fake streams only
"media.navigator.streams.fake": True
}

View File

@ -3,12 +3,14 @@
* 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/. */
Components.utils.import("resource://gre/modules/Services.jsm");
const nsIPermissionManager = Components.interfaces.nsIPermissionManager;
const nsICookiePermission = Components.interfaces.nsICookiePermission;
const NOTIFICATION_FLUSH_PERMISSIONS = "flush-pending-permissions";
function Permission(host, rawHost, type, capability)
function Permission(host, rawHost, type, capability)
{
this.host = host;
this.rawHost = rawHost;
@ -17,18 +19,19 @@ function Permission(host, rawHost, type, capability)
}
var gPermissionManager = {
_type : "",
_permissions : [],
_pm : Components.classes["@mozilla.org/permissionmanager;1"]
.getService(Components.interfaces.nsIPermissionManager),
_bundle : null,
_tree : null,
_type : "",
_permissions : [],
_permissionsToAdd : new Map(),
_permissionsToDelete : new Map(),
_bundle : null,
_tree : null,
_observerRemoved : false,
_view: {
_rowCount: 0,
get rowCount()
{
return this._rowCount;
get rowCount()
{
return this._rowCount;
},
getCellText: function (aRow, aColumn)
{
@ -56,7 +59,7 @@ var gPermissionManager = {
return "";
}
},
_getCapabilityString: function (aCapability)
{
var stringKey = null;
@ -76,44 +79,47 @@ var gPermissionManager = {
}
return this._bundle.getString(stringKey);
},
addPermission: function (aCapability)
{
var textbox = document.getElementById("url");
var host = textbox.value.replace(/^\s*([-\w]*:\/+)?/, ""); // trim any leading space and scheme
try {
var ioService = Components.classes["@mozilla.org/network/io-service;1"]
.getService(Components.interfaces.nsIIOService);
var uri = ioService.newURI("http://"+host, null, null);
var uri = Services.io.newURI("http://"+host, null, null);
host = uri.host;
} catch(ex) {
var promptService = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
.getService(Components.interfaces.nsIPromptService);
var message = this._bundle.getString("invalidURI");
var title = this._bundle.getString("invalidURITitle");
promptService.alert(window, title, message);
Services.prompt.alert(window, title, message);
return;
}
var capabilityString = this._getCapabilityString(aCapability);
// check whether the permission already exists, if not, add it
var exists = false;
let hostExists = false;
let capabilityExists = false;
for (var i = 0; i < this._permissions.length; ++i) {
if (this._permissions[i].rawHost == host) {
// Avoid calling the permission manager if the capability settings are
// the same. Otherwise allow the call to the permissions manager to
// update the listbox for us.
exists = this._permissions[i].capability == capabilityString;
hostExists = true;
capabilityExists = this._permissions[i].capability == capabilityString;
if (!capabilityExists) {
this._permissions[i].capability = capabilityString;
}
break;
}
}
if (!exists) {
host = (host.charAt(0) == ".") ? host.substring(1,host.length) : host;
var uri = ioService.newURI("http://" + host, null, null);
this._pm.add(uri, this._type, aCapability);
let permissionParams = {host: host, type: this._type, capability: aCapability};
if (!hostExists) {
this._permissionsToAdd.set(host, permissionParams);
this._addPermission(permissionParams);
}
else if (!capabilityExists) {
this._permissionsToAdd.set(host, permissionParams);
this._handleCapabilityChange();
}
textbox.value = "";
textbox.focus();
@ -123,14 +129,57 @@ var gPermissionManager = {
// enable "remove all" button as needed
document.getElementById("removeAllPermissions").disabled = this._permissions.length == 0;
},
_removePermission: function(aPermission)
{
this._removePermissionFromList(aPermission.host);
// If this permission was added during this session, let's remove
// it from the pending adds list to prevent calls to the
// permission manager.
let isNewPermission = this._permissionsToAdd.delete(aPermission.host);
if (!isNewPermission) {
this._permissionsToDelete.set(aPermission.host, aPermission);
}
},
_handleCapabilityChange: function ()
{
// Re-do the sort, if the status changed from Block to Allow
// or vice versa, since if we're sorted on status, we may no
// longer be in order.
if (this._lastPermissionSortColumn.id == "statusCol") {
this._resortPermissions();
}
this._tree.treeBoxObject.invalidate();
},
_addPermission: function(aPermission)
{
this._addPermissionToList(aPermission);
++this._view._rowCount;
this._tree.treeBoxObject.rowCountChanged(this._view.rowCount - 1, 1);
// Re-do the sort, since we inserted this new item at the end.
this._resortPermissions();
},
_resortPermissions: function()
{
gTreeUtils.sort(this._tree, this._view, this._permissions,
this._permissionsComparator,
this._lastPermissionSortColumn,
this._lastPermissionSortAscending);
},
onHostInput: function (aSiteField)
{
document.getElementById("btnSession").disabled = !aSiteField.value;
document.getElementById("btnBlock").disabled = !aSiteField.value;
document.getElementById("btnAllow").disabled = !aSiteField.value;
},
onWindowKeyPress: function (aEvent)
{
if (aEvent.keyCode == KeyEvent.DOM_VK_ESCAPE)
@ -142,14 +191,14 @@ var gPermissionManager = {
if (aEvent.keyCode == KeyEvent.DOM_VK_RETURN)
document.getElementById("btnAllow").click();
},
onLoad: function ()
{
this._bundle = document.getElementById("bundlePreferences");
var params = window.arguments[0];
this.init(params);
},
init: function (aParams)
{
if (this._type) {
@ -159,14 +208,14 @@ var gPermissionManager = {
this._type = aParams.permissionType;
this._manageCapability = aParams.manageCapability;
var permissionsText = document.getElementById("permissionsText");
while (permissionsText.hasChildNodes())
permissionsText.removeChild(permissionsText.firstChild);
permissionsText.appendChild(document.createTextNode(aParams.introText));
document.title = aParams.windowTitle;
document.getElementById("btnBlock").hidden = !aParams.blockVisible;
document.getElementById("btnSession").hidden = !aParams.sessionVisible;
document.getElementById("btnAllow").hidden = !aParams.allowVisible;
@ -182,23 +231,23 @@ var gPermissionManager = {
var urlLabel = document.getElementById("urlLabel");
urlLabel.hidden = !urlFieldVisible;
var os = Components.classes["@mozilla.org/observer-service;1"]
.getService(Components.interfaces.nsIObserverService);
os.notifyObservers(null, NOTIFICATION_FLUSH_PERMISSIONS, this._type);
os.addObserver(this, "perm-changed", false);
Services.obs.notifyObservers(null, NOTIFICATION_FLUSH_PERMISSIONS, this._type);
Services.obs.addObserver(this, "perm-changed", false);
this._loadPermissions();
urlField.focus();
},
uninit: function ()
{
var os = Components.classes["@mozilla.org/observer-service;1"]
.getService(Components.interfaces.nsIObserverService);
os.removeObserver(this, "perm-changed");
if (!this._observerRemoved) {
Services.obs.removeObserver(this, "perm-changed");
this._observerRemoved = true;
}
},
observe: function (aSubject, aTopic, aData)
{
if (aTopic == "perm-changed") {
@ -209,14 +258,7 @@ var gPermissionManager = {
return;
if (aData == "added") {
this._addPermissionToList(permission);
++this._view._rowCount;
this._tree.treeBoxObject.rowCountChanged(this._view.rowCount - 1, 1);
// Re-do the sort, since we inserted this new item at the end.
gTreeUtils.sort(this._tree, this._view, this._permissions,
this._permissionsComparator,
this._lastPermissionSortColumn,
this._lastPermissionSortAscending);
this._addPermission(permission);
}
else if (aData == "changed") {
for (var i = 0; i < this._permissions.length; ++i) {
@ -225,31 +267,14 @@ var gPermissionManager = {
break;
}
}
// Re-do the sort, if the status changed from Block to Allow
// or vice versa, since if we're sorted on status, we may no
// longer be in order.
if (this._lastPermissionSortColumn.id == "statusCol") {
gTreeUtils.sort(this._tree, this._view, this._permissions,
this._permissionsComparator,
this._lastPermissionSortColumn,
this._lastPermissionSortAscending);
}
this._tree.treeBoxObject.invalidate();
this._handleCapabilityChange();
}
else if (aData == "deleted") {
for (var i = 0; i < this._permissions.length; i++) {
if (this._permissions[i].host == permission.host) {
this._permissions.splice(i, 1);
this._view._rowCount--;
this._tree.treeBoxObject.rowCountChanged(this._view.rowCount - 1, -1);
this._tree.treeBoxObject.invalidate();
break;
}
}
this._removePermissionFromList(permission);
}
}
},
onPermissionSelected: function ()
{
var hasSelection = this._tree.view.selection.count > 0;
@ -257,7 +282,7 @@ var gPermissionManager = {
document.getElementById("removePermission").disabled = !hasRows || !hasSelection;
document.getElementById("removeAllPermissions").disabled = !hasRows;
},
onPermissionDeleted: function ()
{
if (!this._view.rowCount)
@ -266,12 +291,12 @@ var gPermissionManager = {
gTreeUtils.deleteSelectedItems(this._tree, this._view, this._permissions, removedPermissions);
for (var i = 0; i < removedPermissions.length; ++i) {
var p = removedPermissions[i];
this._pm.remove(p.host, p.type);
}
this._removePermission(p);
}
document.getElementById("removePermission").disabled = !this._permissions.length;
document.getElementById("removeAllPermissions").disabled = !this._permissions.length;
},
onAllPermissionsDeleted: function ()
{
if (!this._view.rowCount)
@ -280,12 +305,12 @@ var gPermissionManager = {
gTreeUtils.deleteAll(this._tree, this._view, this._permissions, removedPermissions);
for (var i = 0; i < removedPermissions.length; ++i) {
var p = removedPermissions[i];
this._pm.remove(p.host, p.type);
}
this._removePermission(p);
}
document.getElementById("removePermission").disabled = true;
document.getElementById("removeAllPermissions").disabled = true;
},
onPermissionKeyPress: function (aEvent)
{
if (aEvent.keyCode == KeyEvent.DOM_VK_DELETE
@ -295,7 +320,7 @@ var gPermissionManager = {
)
this.onPermissionDeleted();
},
_lastPermissionSortColumn: "",
_lastPermissionSortAscending: false,
_permissionsComparator : function (a, b)
@ -303,19 +328,38 @@ var gPermissionManager = {
return a.toLowerCase().localeCompare(b.toLowerCase());
},
onPermissionSort: function (aColumn)
{
this._lastPermissionSortAscending = gTreeUtils.sort(this._tree,
this._view,
this._lastPermissionSortAscending = gTreeUtils.sort(this._tree,
this._view,
this._permissions,
aColumn,
this._permissionsComparator,
this._lastPermissionSortColumn,
this._lastPermissionSortColumn,
this._lastPermissionSortAscending);
this._lastPermissionSortColumn = aColumn;
},
onApplyChanges: function()
{
// Stop observing permission changes since we are about
// to write out the pending adds/deletes and don't need
// to update the UI
this.uninit();
for (let permissionParams of this._permissionsToAdd.values()) {
let uri = Services.io.newURI("http://" + permissionParams.host, null, null);
Services.perms.add(uri, permissionParams.type, permissionParams.capability);
}
for (let p of this._permissionsToDelete.values()) {
Services.perms.remove(p.host, p.type);
}
window.close();
},
_loadPermissions: function ()
{
this._tree = document.getElementById("permissionsTree");
@ -323,12 +367,12 @@ var gPermissionManager = {
// load permissions into a table
var count = 0;
var enumerator = this._pm.enumerator;
var enumerator = Services.perms.enumerator;
while (enumerator.hasMoreElements()) {
var nextPermission = enumerator.getNext().QueryInterface(Components.interfaces.nsIPermission);
this._addPermissionToList(nextPermission);
}
this._view._rowCount = this._permissions.length;
// sort and display the table
@ -338,7 +382,7 @@ var gPermissionManager = {
// disable "remove all" button if there are none
document.getElementById("removeAllPermissions").disabled = this._permissions.length == 0;
},
_addPermissionToList: function (aPermission)
{
if (aPermission.type == this._type &&
@ -352,9 +396,22 @@ var gPermissionManager = {
aPermission.type,
capabilityString);
this._permissions.push(p);
}
}
},
_removePermissionFromList: function (aHost)
{
for (let i = 0; i < this._permissions.length; ++i) {
if (this._permissions[i].host == aHost) {
this._permissions.splice(i, 1);
this._view._rowCount--;
this._tree.treeBoxObject.rowCountChanged(this._view.rowCount - 1, -1);
this._tree.treeBoxObject.invalidate();
break;
}
}
},
setHost: function (aHost)
{
document.getElementById("url").value = aHost;

View File

@ -61,8 +61,8 @@
<treechildren/>
</tree>
</vbox>
<hbox align="end">
<hbox class="actionButtons" flex="1">
<vbox>
<hbox class="actionButtons" align="left" flex="1">
<button id="removePermission" disabled="true"
accesskey="&removepermission.accesskey;"
icon="remove" label="&removepermission.label;"
@ -71,9 +71,13 @@
icon="clear" label="&removeallpermissions.label;"
accesskey="&removeallpermissions.accesskey;"
oncommand="gPermissionManager.onAllPermissionsDeleted();"/>
<spacer flex="1"/>
<button oncommand="close();" icon="close"
label="&button.close.label;" accesskey="&button.close.accesskey;"/>
</hbox>
</hbox>
<spacer flex="1"/>
<hbox class="actionButtons" align="right" flex="1">
<button oncommand="close();" icon="close"
label="&button.cancel.label;" accesskey="&button.cancel.accesskey;" />
<button id="btnApplyChanges" oncommand="gPermissionManager.onApplyChanges();" icon="save"
label="&button.ok.label;" accesskey="&button.ok.accesskey;"/>
</hbox>
</vbox>
</window>

View File

@ -4,182 +4,197 @@
function test() {
waitForExplicitFinish();
function prefWindowObserver(subject, topic, data) {
if (topic != "domwindowopened")
return;
Services.ww.unregisterNotification(this);
let win = subject.QueryInterface(Ci.nsIDOMEventTarget);
win.addEventListener("load", function(event) {
let historyMode = event.target.getElementById("historyMode");
historyMode.value = "custom";
historyMode.doCommand();
Services.ww.registerNotification(cookiesWindowObserver);
event.target.getElementById("cookieExceptions").doCommand();
}, false);
}
function cookiesWindowObserver(subject, topic, data) {
if (topic != "domwindowopened")
return;
Services.ww.unregisterNotification(this);
let win = subject.QueryInterface(Ci.nsIDOMEventTarget);
win.addEventListener("load", function(event) {
SimpleTest.executeSoon(function() windowLoad(event, win, dialog));
}, false);
}
Services.ww.registerNotification(prefWindowObserver);
let dialog = openDialog("chrome://browser/content/preferences/preferences.xul",
"Preferences", "chrome,titlebar,toolbar,centerscreen,dialog=no",
"panePrivacy");
testRunner.runTests();
}
function windowLoad(event, win, dialog) {
let doc = event.target;
let tree = doc.getElementById("permissionsTree");
let statusCol = tree.treeBoxObject.columns.getColumnAt(1);
let url = doc.getElementById("url");
let btnAllow = doc.getElementById("btnAllow");
let btnBlock = doc.getElementById("btnBlock");
let btnRemove = doc.getElementById("removePermission");
let pm = Cc["@mozilla.org/permissionmanager;1"]
.getService(Ci.nsIPermissionManager);
let ioService = Cc["@mozilla.org/network/io-service;1"]
.getService(Ci.nsIIOService);
const allowText = win.gPermissionManager._getCapabilityString(
Ci.nsIPermissionManager.ALLOW_ACTION);
const denyText = win.gPermissionManager._getCapabilityString(
Ci.nsIPermissionManager.DENY_ACTION);
const allow = Ci.nsIPermissionManager.ALLOW_ACTION;
const deny = Ci.nsIPermissionManager.DENY_ACTION;
var testRunner = {
is(tree.view.rowCount, 0, "no cookie exceptions");
tests:
[
{
test: function(params) {
params.url.value = "test.com";
params.btnAllow.doCommand();
is(params.tree.view.rowCount, 1, "added exception shows up in treeview");
is(params.tree.view.getCellText(0, params.statusCol), params.allowText,
"permission text should be set correctly");
params.btnApplyChanges.doCommand();
},
observances: [{ type: "cookie", host: "test.com", data: "added",
capability: Ci.nsIPermissionManager.ALLOW_ACTION }],
},
{
test: function(params) {
params.url.value = "test.com";
params.btnBlock.doCommand();
is(params.tree.view.getCellText(0, params.statusCol), params.denyText,
"permission should change to deny in UI");
params.btnApplyChanges.doCommand();
},
observances: [{ type: "cookie", host: "test.com", data: "changed",
capability: Ci.nsIPermissionManager.DENY_ACTION }],
},
{
test: function(params) {
params.url.value = "test.com";
params.btnAllow.doCommand();
is(params.tree.view.getCellText(0, params.statusCol), params.allowText,
"permission should revert back to allow");
params.btnApplyChanges.doCommand();
},
observances: [{ type: "cookie", host: "test.com", data: "changed",
capability: Ci.nsIPermissionManager.ALLOW_ACTION }],
},
{
test: function(params) {
params.url.value = "test.com";
params.btnRemove.doCommand();
is(params.tree.view.rowCount, 0, "exception should be removed");
params.btnApplyChanges.doCommand();
},
observances: [{ type: "cookie", host: "test.com", data: "deleted" }],
},
{
test: function(params) {
let uri = params.ioService.newURI("http://test.com", null, null);
params.pm.add(uri, "popup", Ci.nsIPermissionManager.DENY_ACTION);
is(params.tree.view.rowCount, 0, "adding unrelated permission should not change display");
params.btnApplyChanges.doCommand();
},
observances: [{ type: "popup", host: "test.com", data: "added",
capability: Ci.nsIPermissionManager.DENY_ACTION }],
cleanUp: function(params) {
params.pm.remove("test.com", "popup");
},
},
],
let tests = [
{
test: function() {
url.value = "test.com";
btnAllow.doCommand();
is(tree.view.rowCount, 1, "added exception shows up in treeview");
is(tree.view.getCellText(0, statusCol), allowText,
"permission text should be set correctly");
},
observances: [{ type: "cookie", host: "test.com", data: "added",
capability: allow }]
},
{
test: function() {
url.value = "test.com";
btnBlock.doCommand();
is(tree.view.getCellText(0, statusCol), denyText,
"permission should change to deny in UI");
},
observances: [{ type: "cookie", host: "test.com", data: "changed",
capability: deny }],
},
{
test: function() {
url.value = "test.com";
btnAllow.doCommand();
is(tree.view.getCellText(0, statusCol), allowText,
"permission should revert back to allow");
},
observances: [{ type: "cookie", host: "test.com", data: "changed",
capability: allow }],
},
{
test: function() {
url.value = "test.com";
btnRemove.doCommand();
is(tree.view.rowCount, 0, "exception should be removed");
},
observances: [{ type: "cookie", host: "test.com", data: "deleted" }],
},
{
test: function() {
let uri = ioService.newURI("http://test.com", null, null);
pm.add(uri, "popup", Ci.nsIPermissionManager.DENY_ACTION);
is(tree.view.rowCount, 0, "adding unrelated permission should not change display");
},
observances: [{ type: "popup", host: "test.com", data: "added",
capability: deny }],
cleanUp: function() {
pm.remove("test.com", "popup");
},
},
{
test: function() {
url.value = "test.com";
btnAllow.doCommand();
pm.remove("test.com", "cookie");
is(tree.view.rowCount, 0, "display should update when cookie permission is deleted");
},
observances: [{ type: "cookie", host: "test.com", data: "added",
capability: allow },
{ type: "cookie", host: "test.com", data: "deleted" }]
},
];
_currentTest: -1,
let permObserver = {
observe: function(aSubject, aTopic, aData) {
if (aTopic != "perm-changed")
return;
runTests: function() {
this._currentTest++;
if (tests[currentTest].observances.length == 0) {
// Should fail here as we are not expecting a notification, but we don't.
// See bug 1063410.
return;
info("Running test #" + (this._currentTest + 1) + "\n");
let that = this;
let p = this.runCurrentTest();
p.then(function() {
if (that._currentTest == that.tests.length - 1) {
finish();
}
let permission = aSubject.QueryInterface(Ci.nsIPermission);
let expected = tests[currentTest].observances.shift();
is(aData, expected.data, "type of message should be the same");
for each (let prop in ["type", "host", "capability"]) {
if (expected[prop])
is(permission[prop], expected[prop],
"property: \"" + prop + "\" should be equal");
else {
that.runTests();
}
});
},
if (tests[currentTest].observances.length == 0) {
SimpleTest.executeSoon(function() {
if (tests[currentTest].cleanUp)
tests[currentTest].cleanUp();
runCurrentTest: function() {
return new Promise(function(resolve, reject) {
runNextTest();
});
}
},
};
let helperFunctions = {
let os = Cc["@mozilla.org/observer-service;1"]
.getService(Ci.nsIObserverService);
prefWindowObserver: function(subject, topic, data) {
if (topic != "domwindowopened")
return;
os.addObserver(permObserver, "perm-changed", false);
Services.ww.unregisterNotification(helperFunctions.prefWindowObserver);
var currentTest = -1;
function runNextTest() {
currentTest++;
if (currentTest == tests.length) {
os.removeObserver(permObserver, "perm-changed");
win.close();
dialog.close();
finish();
return;
}
let win = subject.QueryInterface(Ci.nsIDOMEventTarget);
info("Running test #" + (currentTest + 1) + "\n");
tests[currentTest].test();
if (!tests[currentTest].observances)
runNextTest();
}
win.addEventListener("load", function(event) {
let historyMode = event.target.getElementById("historyMode");
historyMode.value = "custom";
historyMode.doCommand();
Services.ww.registerNotification(helperFunctions.cookiesWindowObserver);
event.target.getElementById("cookieExceptions").doCommand();
}, false);
},
runNextTest();
}
cookiesWindowObserver: function(subject, topic, data) {
if (topic != "domwindowopened")
return;
Services.ww.unregisterNotification(helperFunctions.cookiesWindowObserver);
let win = subject.QueryInterface(Ci.nsIDOMEventTarget);
win.addEventListener("load", function(event) {
SimpleTest.executeSoon(function() helperFunctions.windowLoad(event, win));
}, false);
},
windowLoad: function(event, win) {
let params = {
doc: event.target,
tree: event.target.getElementById("permissionsTree"),
statusCol: event.target.getElementById("permissionsTree").treeBoxObject.columns.getColumnAt(1),
url: event.target.getElementById("url"),
btnAllow: event.target.getElementById("btnAllow"),
btnBlock: event.target.getElementById("btnBlock"),
btnApplyChanges: event.target.getElementById("btnApplyChanges"),
btnRemove: event.target.getElementById("removePermission"),
pm: Cc["@mozilla.org/permissionmanager;1"]
.getService(Ci.nsIPermissionManager),
ioService: Cc["@mozilla.org/network/io-service;1"]
.getService(Ci.nsIIOService),
allowText: win.gPermissionManager._getCapabilityString(
Ci.nsIPermissionManager.ALLOW_ACTION),
denyText: win.gPermissionManager._getCapabilityString(
Ci.nsIPermissionManager.DENY_ACTION),
allow: Ci.nsIPermissionManager.ALLOW_ACTION,
deny: Ci.nsIPermissionManager.DENY_ACTION,
};
let permObserver = {
observe: function(aSubject, aTopic, aData) {
if (aTopic != "perm-changed")
return;
if (testRunner.tests[testRunner._currentTest].observances.length == 0) {
// Should fail here as we are not expecting a notification, but we don't.
// See bug 1063410.
return;
}
let permission = aSubject.QueryInterface(Ci.nsIPermission);
let expected = testRunner.tests[testRunner._currentTest].observances.shift();
is(aData, expected.data, "type of message should be the same");
for each (let prop in ["type", "host", "capability"]) {
if (expected[prop])
is(permission[prop], expected[prop],
"property: \"" + prop + "\" should be equal");
}
os.removeObserver(permObserver, "perm-changed");
if (testRunner.tests[testRunner._currentTest].cleanup) {
testRunner.tests[testRunner._currentTest].cleanup();
}
testRunner.dialog.close(params);
win.close();
resolve();
},
};
let os = Cc["@mozilla.org/observer-service;1"]
.getService(Ci.nsIObserverService);
os.addObserver(permObserver, "perm-changed", false);
if (testRunner._currentTest == 0) {
is(params.tree.view.rowCount, 0, "no cookie exceptions");
}
testRunner.tests[testRunner._currentTest].test(params);
},
};
Services.ww.registerNotification(helperFunctions.prefWindowObserver);
testRunner.dialog = openDialog("chrome://browser/content/preferences/preferences.xul",
"Preferences", "chrome,titlebar,toolbar,centerscreen,dialog=no",
"panePrivacy");
});
},
};

View File

@ -12,8 +12,9 @@ const {Task} = Cu.import("resource://gre/modules/Task.jsm", {});
let XMLHttpRequest = CC("@mozilla.org/xmlextras/xmlhttprequest;1");
let strings = Services.strings.createBundle("chrome://browser/locale/devtools/app-manager.properties");
function AppValidator(project) {
this.project = project;
function AppValidator({ type, location }) {
this.type = type;
this.location = location;
this.errors = [];
this.warnings = [];
}
@ -27,7 +28,7 @@ AppValidator.prototype.warning = function (message) {
};
AppValidator.prototype._getPackagedManifestFile = function () {
let manifestFile = FileUtils.File(this.project.location);
let manifestFile = FileUtils.File(this.location);
if (!manifestFile.exists()) {
this.error(strings.GetStringFromName("validator.nonExistingFolder"));
return null;
@ -149,12 +150,12 @@ AppValidator.prototype._fetchManifest = function (manifestURL) {
AppValidator.prototype._getManifest = function () {
let manifestURL;
if (this.project.type == "packaged") {
if (this.type == "packaged") {
manifestURL = this._getPackagedManifestURL();
if (!manifestURL)
return promise.resolve(null);
} else if (this.project.type == "hosted") {
manifestURL = this.project.location;
} else if (this.type == "hosted") {
manifestURL = this.location;
try {
Services.io.newURI(manifestURL, null, null);
} catch(e) {
@ -162,7 +163,7 @@ AppValidator.prototype._getManifest = function () {
return promise.resolve(null);
}
} else {
this.error(strings.formatStringFromName("validator.invalidProjectType", [this.project.type], 1));
this.error(strings.formatStringFromName("validator.invalidProjectType", [this.type], 1));
return promise.resolve(null);
}
return this._fetchManifest(manifestURL);
@ -181,11 +182,11 @@ AppValidator.prototype.validateManifest = function (manifest) {
};
AppValidator.prototype._getOriginURL = function () {
if (this.project.type == "packaged") {
if (this.type == "packaged") {
let manifestURL = Services.io.newURI(this.manifestURL, null, null);
return Services.io.newURI(".", null, manifestURL).spec;
} else if (this.project.type == "hosted") {
return Services.io.newURI(this.project.location, null, null).prePath;
} else if (this.type == "hosted") {
return Services.io.newURI(this.location, null, null).prePath;
}
};
@ -203,9 +204,9 @@ AppValidator.prototype.validateLaunchPath = function (manifest) {
}
let origin = this._getOriginURL();
let path;
if (this.project.type == "packaged") {
if (this.type == "packaged") {
path = "." + ( manifest.launch_path || "/index.html" );
} else if (this.project.type == "hosted") {
} else if (this.type == "hosted") {
path = manifest.launch_path || "/";
}
let indexURL;
@ -251,7 +252,7 @@ AppValidator.prototype.validateType = function (manifest) {
let appType = manifest.type || "web";
if (["web", "trusted", "privileged", "certified"].indexOf(appType) === -1) {
this.error(strings.formatStringFromName("validator.invalidAppType", [appType], 1));
} else if (this.project.type == "hosted" &&
} else if (this.type == "hosted" &&
["certified", "privileged"].indexOf(appType) !== -1) {
this.error(strings.formatStringFromName("validator.invalidHostedPriviledges", [appType], 1));
}

View File

@ -109,7 +109,7 @@
let validator = createPackaged("wrong-launch-path");
validator.validate().then(() => {
is(validator.errors.length, 1, "app with wrong path got an error");
let file = nsFile(validator.project.location);
let file = nsFile(validator.location);
file.append("wrong-path.html");
let url = Services.io.newFileURI(file);
is(validator.errors[0], strings.formatStringFromName("validator.accessFailedLaunchPath", [url.spec], 1),

View File

@ -9,8 +9,8 @@
// Expected values:
let res1 = [
{selector: "#element-size", value: "160" + "\u00D7" + "160"},
{selector: ".size > span", value: "100" + "\u00D7" + "100"},
{selector: "#element-size", value: "160" + "\u00D7" + "160.117"},
{selector: ".size > span", value: "100" + "\u00D7" + "100.117"},
{selector: ".margin.top > span", value: 30},
{selector: ".margin.left > span", value: "auto"},
{selector: ".margin.bottom > span", value: 30},
@ -43,7 +43,7 @@ let res2 = [
];
add_task(function*() {
let style = "div { position: absolute; top: 42px; left: 42px; height: 100px; width: 100px; border: 10px solid black; padding: 20px; margin: 30px auto;}";
let style = "div { position: absolute; top: 42px; left: 42px; height: 100.111px; width: 100px; border: 10px solid black; padding: 20px; margin: 30px auto;}";
let html = "<style>" + style + "</style><div></div>"
yield addTab("data:text/html," + encodeURIComponent(html));

View File

@ -422,7 +422,7 @@ LayoutView.prototype = {
// might be missing.
continue;
}
let parsedValue = parseInt(layout[property]);
let parsedValue = parseFloat(layout[property]);
if (Number.isNaN(parsedValue)) {
// Not a number. We use the raw string.
// Useful for "position" for example.
@ -451,9 +451,10 @@ LayoutView.prototype = {
width -= this.map.borderLeft.value + this.map.borderRight.value +
this.map.paddingLeft.value + this.map.paddingRight.value;
width = parseFloat(width.toPrecision(6));
height -= this.map.borderTop.value + this.map.borderBottom.value +
this.map.paddingTop.value + this.map.paddingBottom.value;
height = parseFloat(height.toPrecision(6));
let newValue = width + "\u00D7" + height;
if (this.sizeLabel.textContent != newValue) {

View File

@ -129,6 +129,8 @@ function updateUI() {
warningsNode.appendChild(li);
}
}
AppManager.update("details");
}
function showPrepackageLog() {

View File

@ -153,11 +153,11 @@ function doOK() {
target.remove(false);
AppProjects.addPackaged(folder).then((project) => {
window.arguments[0].location = project.location;
AppManager.validateProject(project).then(() => {
AppManager.validateAndUpdateProject(project).then(() => {
if (project.manifest) {
project.manifest.name = projectName;
AppManager.writeManifest(project).then(() => {
AppManager.validateProject(project).then(
AppManager.validateAndUpdateProject(project).then(
() => {window.close()}, bail)
}, bail)
} else {

View File

@ -134,7 +134,7 @@ let UI = {
AppManager.selectedProject.type != "mainProcess" &&
AppManager.selectedProject.type != "runtimeApp" &&
AppManager.selectedProject.type != "tab") {
AppManager.validateProject(AppManager.selectedProject);
AppManager.validateAndUpdateProject(AppManager.selectedProject);
}
// Hook to display promotional Developer Edition doorhanger. Only displayed once.
@ -567,7 +567,7 @@ let UI = {
menuindex: 1
});
this.projecteditor.on("onEditorSave", (editor, resource) => {
AppManager.validateProject(AppManager.selectedProject);
AppManager.validateAndUpdateProject(AppManager.selectedProject);
});
return this.projecteditor.loaded;
},
@ -667,9 +667,6 @@ let UI = {
}
}
// Validate project
yield AppManager.validateProject(project);
// Select project
AppManager.selectedProject = project;
}),
@ -1045,9 +1042,6 @@ let Cmds = {
// Retrieve added project
let project = AppProjects.get(ret.location);
// Validate project
yield AppManager.validateProject(project);
// Select project
AppManager.selectedProject = project;
@ -1106,7 +1100,7 @@ let Cmds = {
// The result of the validation process (storing names, icons, …) is not stored in
// the IndexedDB database when App Manager v1 is used.
// We need to run the validation again and update the name and icon of the app.
AppManager.validateProject(project).then(() => {
AppManager.validateAndUpdateProject(project).then(() => {
panelItemNode.setAttribute("label", project.name);
panelItemNode.setAttribute("image", project.icon);
});

View File

@ -278,36 +278,36 @@ let AppManager = exports.AppManager = {
},
_selectedProject: null,
set selectedProject(value) {
set selectedProject(project) {
// A regular comparison still sees a difference when equal in some cases
if (JSON.stringify(this._selectedProject) !==
JSON.stringify(value)) {
let cancelled = false;
this.update("before-project", { cancel: () => { cancelled = true; } });
if (cancelled) {
return;
}
this._selectedProject = value;
// Clear out tab store's selected state, if any
this.tabStore.selectedTab = null;
if (this.selectedProject) {
if (this.selectedProject.type == "packaged" ||
this.selectedProject.type == "hosted") {
this.validateProject(this.selectedProject);
}
if (this.selectedProject.type == "tab") {
this.tabStore.selectedTab = this.selectedProject.app;
}
}
this.update("project");
this.checkIfProjectIsRunning();
if (JSON.stringify(this._selectedProject) ===
JSON.stringify(project)) {
return;
}
let cancelled = false;
this.update("before-project", { cancel: () => { cancelled = true; } });
if (cancelled) {
return;
}
this._selectedProject = project;
// Clear out tab store's selected state, if any
this.tabStore.selectedTab = null;
if (project) {
if (project.type == "packaged" ||
project.type == "hosted") {
this.validateAndUpdateProject(project);
}
if (project.type == "tab") {
this.tabStore.selectedTab = project.app;
}
}
this.update("project");
this.checkIfProjectIsRunning();
},
get selectedProject() {
return this._selectedProject;
@ -323,6 +323,19 @@ let AppManager = exports.AppManager = {
return AppProjects.remove(location);
},
packageProject: Task.async(function*(project) {
if (!project) {
return;
}
if (project.type == "packaged" ||
project.type == "hosted") {
yield ProjectBuilding.build({
project: project,
logger: this.update.bind(this, "pre-package")
});
}
}),
_selectedRuntime: null,
set selectedRuntime(value) {
this._selectedRuntime = value;
@ -481,12 +494,9 @@ let AppManager = exports.AppManager = {
return Task.spawn(function* () {
let self = AppManager;
let packageDir = yield ProjectBuilding.build({
project: project,
logger: self.update.bind(self, "pre-package")
});
yield self.validateProject(project);
// Package and validate project
yield self.packageProject(project);
yield self.validateAndUpdateProject(project);
if (project.errorsCount > 0) {
self.reportError("error_cantInstallValidationErrors");
@ -501,7 +511,7 @@ let AppManager = exports.AppManager = {
let response;
if (project.type == "packaged") {
packageDir = packageDir || project.location;
let packageDir = yield ProjectBuilding.getPackageDir(project);
console.log("Installing app from " + packageDir);
response = yield self._appsFront.installPackaged(packageDir,
@ -557,14 +567,20 @@ let AppManager = exports.AppManager = {
/* PROJECT VALIDATION */
validateProject: function(project) {
validateAndUpdateProject: function(project) {
if (!project) {
return promise.reject();
}
return Task.spawn(function* () {
let validation = new AppValidator(project);
let packageDir = yield ProjectBuilding.getPackageDir(project);
let validation = new AppValidator({
type: project.type,
// Build process may place the manifest in a non-root directory
location: packageDir
});
yield validation.validate();
if (validation.manifest) {
@ -584,7 +600,7 @@ let AppManager = exports.AppManager = {
let origin = Services.io.newURI(manifestURL.prePath, null, null);
project.icon = Services.io.newURI(iconPath, null, origin).spec;
} else if (project.type == "packaged") {
let projectFolder = FileUtils.File(project.location);
let projectFolder = FileUtils.File(packageDir);
let folderURI = Services.io.newFileURI(projectFolder).spec;
project.icon = folderURI + iconPath.replace(/^\/|\\/, "");
}

View File

@ -14,10 +14,10 @@ const ProjectBuilding = exports.ProjectBuilding = {
let manifestPath = OS.Path.join(project.location, "package.json");
let exists = yield OS.File.exists(manifestPath);
if (!exists) {
return;
// No explicit manifest, try to generate one if possible
return this.generatePackageManifest(project);
}
let Decoder = new TextDecoder();
let data = yield OS.File.read(manifestPath);
data = new TextDecoder().decode(data);
let manifest;
@ -30,6 +30,31 @@ const ProjectBuilding = exports.ProjectBuilding = {
return manifest;
}),
/**
* For common frameworks in the community, attempt to detect the build
* settings if none are defined. This makes it much easier to get started
* with WebIDE. Later on, perhaps an add-on could define such things for
* different frameworks.
*/
generatePackageManifest: Task.async(function*(project) {
// Cordova
let cordovaConfigPath = OS.Path.join(project.location, "config.xml");
let exists = yield OS.File.exists(cordovaConfigPath);
if (!exists) {
return;
}
let data = yield OS.File.read(cordovaConfigPath);
data = new TextDecoder().decode(data);
if (data.contains("cordova.apache.org")) {
return {
"webide": {
"prepackage": "cordova prepare",
"packageDir": "./platforms/firefoxos/www"
}
};
}
}),
hasPrepackage: Task.async(function* (project) {
let manifest = yield ProjectBuilding.fetchPackageManifest(project);
return manifest && manifest.webide && "prepackage" in manifest.webide;
@ -37,22 +62,19 @@ const ProjectBuilding = exports.ProjectBuilding = {
// If the app depends on some build step, run it before pushing the app
build: Task.async(function* ({ project, logger }) {
if (!this.hasPrepackage(project)) {
if (!(yield this.hasPrepackage(project))) {
return;
}
let manifest = yield ProjectBuilding.fetchPackageManifest(project);
logger("start");
let packageDir;
try {
packageDir = yield this._build(project, manifest, logger);
yield this._build(project, manifest, logger);
logger("succeed");
} catch(e) {
logger("failed", e);
}
return packageDir;
}),
_build: Task.async(function* (project, manifest, logger) {
@ -61,6 +83,15 @@ const ProjectBuilding = exports.ProjectBuilding = {
let command, cwd, args = [], env = [];
// Copy frequently used env vars
let envService = Cc["@mozilla.org/process/environment;1"].getService(Ci.nsIEnvironment);
["HOME", "PATH"].forEach(key => {
let value = envService.get(key);
if (value) {
env.push(key + "=" + value);
}
});
if (typeof(manifest.prepackage) === "string") {
command = manifest.prepackage.replace(/%project%/g, project.location);
} else if (manifest.prepackage.command) {
@ -69,16 +100,9 @@ const ProjectBuilding = exports.ProjectBuilding = {
args = manifest.prepackage.args || [];
args = args.map(a => a.replace(/%project%/g, project.location));
env = manifest.prepackage.env || [];
env = env.concat(manifest.prepackage.env || []);
env = env.map(a => a.replace(/%project%/g, project.location));
// Gaia build system crashes if HOME env variable isn't set...
let envService = Cc["@mozilla.org/process/environment;1"].getService(Ci.nsIEnvironment);
let home = envService.get("HOME");
if (home) {
env.push("HOME=" + home);
}
if (manifest.prepackage.cwd) {
// Normalize path for Windows support (converts / to \)
let path = OS.Path.normalize(manifest.prepackage.cwd);
@ -110,7 +134,6 @@ const ProjectBuilding = exports.ProjectBuilding = {
// Otherwise, on Linux and Mac, SHELL env variable should refer to
// the user chosen shell program.
// (We do not check for OS, as on windows, with cygwin, ComSpec isn't set)
let envService = Cc["@mozilla.org/process/environment;1"].getService(Ci.nsIEnvironment);
let shell = envService.get("ComSpec") || envService.get("SHELL");
args.unshift(command);
@ -155,17 +178,22 @@ const ProjectBuilding = exports.ProjectBuilding = {
throw new Error("Unable to run pre-package command '" + command + "' " +
args.join(" ") + ":\n" + (e.message || e));
}
if (manifest.packageDir) {
let packageDir = OS.Path.join(project.location, manifest.packageDir);
// On Windows, replace / by \\
packageDir = OS.Path.normalize(packageDir);
let exists = yield OS.File.exists(packageDir);
if (exists) {
return packageDir;
}
throw new Error("Unable to resolve application package directory: '" + manifest.packageDir + "'");
}
}),
};
getPackageDir: Task.async(function*(project) {
let manifest = yield ProjectBuilding.fetchPackageManifest(project);
if (!manifest || !manifest.webide || !manifest.webide.packageDir) {
return project.location;
}
manifest = manifest.webide;
let packageDir = OS.Path.join(project.location, manifest.packageDir);
// On Windows, replace / by \\
packageDir = OS.Path.normalize(packageDir);
let exists = yield OS.File.exists(packageDir);
if (exists) {
return packageDir;
}
throw new Error("Unable to resolve application package directory: '" + manifest.packageDir + "'");
})
};

View File

@ -103,8 +103,10 @@ function nextTick() {
}
function waitForUpdate(win, update) {
info("Wait: " + update);
let deferred = promise.defer();
win.AppManager.on("app-manager-update", function onUpdate(e, what) {
info("Got: " + what);
if (what !== update) {
return;
}

View File

@ -26,7 +26,7 @@
let AppManager = win.AppManager;
function isProjectMarkedAsValid() {
let details = win.UI.projecteditor.window.frames[0];
let details = win.frames[0];
return !details.document.body.classList.contains("error");
}
@ -40,13 +40,10 @@
let packagedAppLocation = getTestFilePath("build_app" + testSuffix + "1");
yield win.Cmds.importPackagedApp(packagedAppLocation);
yield waitForUpdate(win, "details");
let project = win.AppManager.selectedProject;
let deferred = promise.defer();
win.UI.projecteditor.once("onEditorCreated", deferred.resolve);
yield deferred.promise;
ok(!project.manifest, "manifest includes name");
is(project.name, "--", "Display name uses manifest name");
@ -55,18 +52,19 @@
loggedMessages.push(msg);
}
let packageDir = yield ProjectBuilding.build({
yield ProjectBuilding.build({
project,
logger
});
ok(!packageDir, "no custom packagedir");
let packageDir = yield ProjectBuilding.getPackageDir(project);
is(packageDir, packagedAppLocation, "no custom packagedir");
is(loggedMessages[0], "start", "log messages are correct");
ok(loggedMessages[1].indexOf("Running pre-package hook") != -1, "log messages are correct");
is(loggedMessages[2], "Terminated with error code: 0", "log messages are correct");
is(loggedMessages[3], "succeed", "log messages are correct");
// Trigger validation
yield AppManager.validateProject(AppManager.selectedProject);
yield AppManager.validateAndUpdateProject(AppManager.selectedProject);
yield nextTick();
ok("name" in project.manifest, "manifest includes name");
@ -79,14 +77,16 @@
packagedAppLocation = getTestFilePath("build_app" + testSuffix + "2");
yield win.Cmds.importPackagedApp(packagedAppLocation);
yield waitForUpdate(win, "project-validated");
project = win.AppManager.selectedProject;
loggedMessages = [];
packageDir = yield ProjectBuilding.build({
yield ProjectBuilding.build({
project,
logger
});
packageDir = yield ProjectBuilding.getPackageDir(project);
is(OS.Path.normalize(packageDir),
OS.Path.join(packagedAppLocation, "stage"), "custom packagedir");
is(loggedMessages[0], "start", "log messages are correct");
@ -96,6 +96,7 @@
// Switch to the package dir in order to verify the generated webapp.manifest
yield win.Cmds.importPackagedApp(packageDir);
yield waitForUpdate(win, "project-validated");
project = win.AppManager.selectedProject;
@ -115,5 +116,3 @@
</script>
</body>
</html>

View File

@ -28,14 +28,17 @@
info("to call importPackagedApp(" + packagedAppLocation + ")");
yield win.Cmds.importPackagedApp(packagedAppLocation);
yield waitForUpdate(win, "project-validated");
yield nextTick();
info("to call importHostedApp(" + hostedAppManifest + ")");
yield win.Cmds.importHostedApp(hostedAppManifest);
yield waitForUpdate(win, "project-validated");
yield nextTick();
info("to call importPackagedApp(" + packagedAppLocation + ") again");
yield win.Cmds.importPackagedApp(packagedAppLocation);
yield waitForUpdate(win, "project-validated");
let project = win.AppManager.selectedProject;
is(project.location, packagedAppLocation, "Correctly reselected existing packaged app.");
@ -43,6 +46,7 @@
info("to call importHostedApp(" + hostedAppManifest + ") again");
yield win.Cmds.importHostedApp(hostedAppManifest);
yield waitForUpdate(win, "project-validated");
project = win.AppManager.selectedProject;
is(project.location, hostedAppManifest, "Correctly reselected existing hosted app.");
yield nextTick();
@ -71,4 +75,3 @@
</script>
</body>
</html>

View File

@ -29,6 +29,7 @@
ok(!win.UI._busyPromise, "UI is not busy");
yield win.Cmds.importPackagedApp(packagedAppLocation);
yield waitForUpdate(win, "project-validated");
let project = win.AppManager.selectedProject;
is(project.location, packagedAppLocation, "Location is valid");
@ -40,6 +41,7 @@
let hostedAppManifest = TEST_BASE + "hosted_app.manifest";
yield win.Cmds.importHostedApp(hostedAppManifest);
yield waitForUpdate(win, "project-validated");
project = win.AppManager.selectedProject;
is(project.location, hostedAppManifest, "Location is valid");
@ -49,6 +51,7 @@
hostedAppManifest = TEST_BASE + "/app";
yield win.Cmds.importHostedApp(hostedAppManifest);
yield waitForUpdate(win, "project-validated");
project = win.AppManager.selectedProject;
ok(project.location.endsWith('manifest.webapp'), "The manifest was found and the project was updated");

View File

@ -25,20 +25,17 @@
let AppManager = win.AppManager;
function isProjectMarkedAsValid() {
let details = win.UI.projecteditor.window.frames[0];
let details = win.frames[0];
return !details.document.body.classList.contains("error");
}
let packagedAppLocation = getTestFilePath("app");
yield win.Cmds.importPackagedApp(packagedAppLocation);
yield waitForUpdate(win, "details");
let project = win.AppManager.selectedProject;
let deferred = promise.defer();
win.UI.projecteditor.once("onEditorCreated", deferred.resolve);
yield deferred.promise;
ok("name" in project.manifest, "manifest includes name");
is(project.name, project.manifest.name, "Display name uses manifest name");
ok(isProjectMarkedAsValid(), "project is marked as valid");
@ -66,7 +63,7 @@
yield OS.File.writeAtomic(manifestPath, data , {tmpPath: manifestPath + ".tmp"});
// Trigger validation
yield AppManager.validateProject(AppManager.selectedProject);
yield AppManager.validateAndUpdateProject(AppManager.selectedProject);
yield nextTick();
ok(!("name" in project.manifest), "manifest has been updated");
@ -78,7 +75,7 @@
yield AppManager.writeManifest(project);
// Trigger validation
yield AppManager.validateProject(AppManager.selectedProject);
yield AppManager.validateAndUpdateProject(AppManager.selectedProject);
yield nextTick();
ok("name" in project.manifest, "manifest includes name");
@ -97,5 +94,3 @@
</script>
</body>
</html>

View File

@ -67,6 +67,7 @@
let packagedAppLocation = getTestFilePath("app");
yield win.Cmds.importPackagedApp(packagedAppLocation);
yield waitForUpdate(win, "project-validated");
let panelNode = win.document.querySelector("#runtime-panel");
let items = panelNode.querySelectorAll(".runtime-panel-item-usb");

View File

@ -113,6 +113,7 @@
return Task.spawn(function*() {
let packagedAppLocation = getTestFilePath("app");
yield win.Cmds.importPackagedApp(packagedAppLocation);
yield waitForUpdate(win, "project-validated");
});
}

View File

@ -46,6 +46,10 @@ function sendMessage(action, data, sync, callbackCookie) {
return Components.utils.cloneInto(result, content);
}
function enableDebug() {
sendAsyncMessage('Shumway:enableDebug', null);
}
addMessageListener('Shumway:init', function (message) {
sendAsyncMessage('Shumway:running', {}, {
externalInterface: externalInterfaceWrapper
@ -55,6 +59,7 @@ addMessageListener('Shumway:init', function (message) {
// up Xray wrappers.
shumwayComAdapter = Components.utils.createObjectIn(content, {defineAs: 'ShumwayCom'});
Components.utils.exportFunction(sendMessage, shumwayComAdapter, {defineAs: 'sendMessage'});
Components.utils.exportFunction(enableDebug, shumwayComAdapter, {defineAs: 'enableDebug'});
Object.defineProperties(shumwayComAdapter, {
onLoadFileCallback: { value: null, writable: true },
onExternalCallback: { value: null, writable: true },

View File

@ -0,0 +1,130 @@
/*
* Copyright 2015 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// Simple class for synchronous XHR communication.
// See also examples/inspector/debug/server.js.
var PingPongConnection = (function () {
function PingPongConnection(url, onlySend) {
this.url = url;
this.onData = null;
this.onError = null;
this.currentXhr = null;
this.closed = false;
if (!onlySend) {
this.idle();
}
}
PingPongConnection.prototype = {
idle: function () {
function requestIncoming(connection) {
var xhr = new XMLHttpRequest();
xhr.open('GET', connection.url + '?idle', true);
xhr.onload = function () {
if (xhr.status === 204 &&
xhr.getResponseHeader('X-PingPong-Error') === 'timeout') {
requestIncoming(connection);
return;
}
if (xhr.status === 200) {
var result;
if (connection.onData) {
var response = xhr.responseText;
result = connection.onData(response ? JSON.parse(response) : undefined);
}
if (xhr.getResponseHeader('X-PingPong-Async') === '1') {
requestIncoming(connection);
} else {
sendResponse(connection, result);
}
return;
}
if (connection.onError) {
connection.onError(xhr.statusText);
}
};
xhr.onerror = function () {
if (connection.onError) {
connection.onError(xhr.error);
}
};
xhr.send();
connection.currentXhr = xhr;
}
function sendResponse(connection, result) {
var xhr = new XMLHttpRequest();
xhr.open('POST', connection.url + '?response', false);
xhr.onload = function () {
if (xhr.status !== 204) {
if (connection.onError) {
connection.onError(xhr.statusText);
}
}
requestIncoming(connection);
};
xhr.onerror = function () {
if (connection.onError) {
connection.onError(xhr.error);
}
};
xhr.send(result === undefined ? '' : JSON.stringify(result));
connection.currentXhr = xhr;
}
requestIncoming(this);
},
send: function (data, async, timeout) {
if (this.closed) {
throw new Error('connection closed');
}
async = !!async;
timeout |= 0;
var encoded = data === undefined ? '' : JSON.stringify(data);
if (async) {
var xhr = new XMLHttpRequest();
xhr.open('POST', this.url + '?async', true);
xhr.send(encoded);
return;
} else {
var xhr = new XMLHttpRequest();
xhr.open('POST', this.url, false);
if (timeout > 0) {
xhr.setRequestHeader('X-PingPong-Timeout', timeout);
}
xhr.send(encoded);
if (xhr.status === 204 &&
xhr.getResponseHeader('X-PingPong-Error') === 'timeout') {
throw new Error('sync request timeout');
}
var response = xhr.responseText;
return response ? JSON.parse(response) : undefined;
}
},
close: function () {
if (this.currentXhr) {
this.currentXhr.abort();
this.currentXhr = null;
}
this.closed = true;
}
};
return PingPongConnection;
})();

View File

@ -36,11 +36,23 @@ limitations under the License.
line-height: 0;
border: 0px none;
}
body.remoteStopped {
background-color: red;
}
body.remoteDebug {
background-color: green;
}
body.remoteReload {
background-color: yellow;
}
</style>
</head>
<body>
<iframe id="viewer" src="resource://shumway/web/viewer.html" width="100%" height="100%" mozbrowser remote="true"></iframe>
<script src="chrome://shumway/content/pingpong.js"></script>
<script src="chrome://shumway/content/viewerDebugger.js"></script>
<script src="chrome://shumway/content/viewerWrapper.js"></script>
</body>
</html>

View File

@ -0,0 +1,90 @@
/*
* Copyright 2015 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
Components.utils.import('resource://gre/modules/Services.jsm');
var DebugUtils = (function () {
var baseUrl = null;
function getBaseUrl() {
if (baseUrl === null) {
try {
baseUrl = Services.prefs.getCharPref('shumway.debug.url');
} catch (e) {
baseUrl = 'http://localhost:8010';
}
}
return baseUrl;
}
var uniqueId = (Date.now() % 888888) * 2 + 1;
function getEnabledDebuggerId(swfUrl) {
return new Promise(function (resolve) {
var url = getBaseUrl() + '/debugController/' + uniqueId;
var connection = new PingPongConnection(url);
connection.onData = function (data) {
if (data.action === 'setDebugger' && data.swfUrl === swfUrl) {
resolve(data.debuggerId);
}
};
try {
connection.send({action: 'getDebugger', swfUrl: swfUrl, swfId: uniqueId}, true);
} catch (e) {
// ignoring failed send request
}
setTimeout(function () {
resolve(0);
connection.close();
}, 500);
});
}
function enableDebug(swfUrl) {
var url = getBaseUrl() + '/debugController/' + uniqueId;
var connection = new PingPongConnection(url, true);
try {
connection.send({action: 'enableDebugging', swfUrl: swfUrl}, true);
} catch (e) {
// ignoring failed send request
}
connection.close();
}
function createDebuggerConnection(swfUrl) {
return getEnabledDebuggerId(swfUrl).then(function (debuggerId) {
if (!debuggerId) {
return null;
}
var url = getBaseUrl() + '/debug/' + uniqueId + '/' + debuggerId;
console.log('Starting remote debugger with ' + url);
return new PingPongConnection(url);
});
}
return {
get isEnabled() {
try {
return Services.prefs.getBoolPref('shumway.debug.enabled');
} catch (e) {
return false;
}
},
enableDebug: enableDebug,
createDebuggerConnection: createDebuggerConnection
};
})();

View File

@ -53,6 +53,7 @@ function runViewer() {
// ShumwayStreamConverter.
var shumwayComAdapter = Components.utils.createObjectIn(childWindow, {defineAs: 'ShumwayCom'});
Components.utils.exportFunction(sendMessage, shumwayComAdapter, {defineAs: 'sendMessage'});
Components.utils.exportFunction(enableDebug, shumwayComAdapter, {defineAs: 'enableDebug'});
Object.defineProperties(shumwayComAdapter, {
onLoadFileCallback: { value: null, writable: true },
onExternalCallback: { value: null, writable: true },
@ -117,6 +118,10 @@ function runViewer() {
return window.notifyShumwayMessage(detail);
});
messageManager.addMessageListener('Shumway:enableDebug', function (message) {
enableDebug();
});
window.onExternalCallback = function (call) {
return externalInterface.callback(JSON.stringify(call));
};
@ -135,7 +140,76 @@ function runViewer() {
messageManager.sendAsyncMessage('Shumway:init', {});
}
function handleDebug(connection) {
viewer.parentNode.removeChild(viewer); // we don't need viewer anymore
document.body.className = 'remoteDebug';
function sendMessage(data) {
var detail = {
action: data.action,
data: data.data,
sync: data.sync
};
if (data.callback) {
detail.callback = true;
detail.cookie = data.cookie;
}
return window.notifyShumwayMessage(detail);
}
connection.onData = function (data) {
switch (data.action) {
case 'sendMessage':
return sendMessage(data.detail);
case 'reload':
document.body.className = 'remoteReload';
setTimeout(function () {
window.top.location.reload();
}, 1000);
return;
}
};
window.onExternalCallback = function (call) {
return connection.send({action: 'onExternalCallback', detail: call});
};
window.onMessageCallback = function (response) {
return connection.send({action: 'onMessageCallback', detail: response});
};
window.onLoadFileCallback = function (args) {
if (args.array) {
args.array = Array.prototype.slice.call(args.array, 0);
}
return connection.send({action: 'onLoadFileCallback', detail: args}, true);
};
connection.send({action: 'runViewer'}, true);
}
function enableDebug() {
DebugUtils.enableDebug(window.swfUrlLoading);
setTimeout(function () {
window.top.location.reload();
}, 1000);
}
promise.then(function (oop) {
if (DebugUtils.isEnabled) {
DebugUtils.createDebuggerConnection(window.swfUrlLoading).then(function (debuggerConnection) {
if (debuggerConnection) {
handleDebug(debuggerConnection);
} else if (oop) {
handlerOOP();
} else {
handler();
}
});
return;
}
if (oop) {
handlerOOP();
} else {

View File

@ -411,7 +411,7 @@ ChromeActions.prototype = {
var position = e.loaded;
var data = new Uint8Array(xhr.response);
notifyLoadFileListener({callback:"loadFile", sessionId: sessionId,
topic: "progress", array: data, loaded: e.loaded, total: e.total});
topic: "progress", array: data, loaded: position, total: e.total});
lastPosition = position;
if (limit && e.total >= limit) {
xhr.abort();
@ -755,6 +755,59 @@ function activateShumwayScripts(window, requestListener) {
return requestListener.receive.apply(requestListener, arguments);
};
window.wrappedJSObject.runViewer();
var parentWindow = window.parent;
var viewerWindow = window.viewer.contentWindow;
function activate(e) {
e.preventDefault();
viewerWindow.removeEventListener('mousedown', activate, true);
parentWindow.addEventListener('keydown', forwardKeyEvent, true);
parentWindow.addEventListener('keyup', forwardKeyEvent, true);
sendFocusEvent('focus');
parentWindow.addEventListener('blur', deactivate, true);
parentWindow.addEventListener('mousedown', deactivate, true);
viewerWindow.addEventListener('unload', deactivate, true);
}
function deactivate() {
parentWindow.removeEventListener('blur', deactivate, true);
parentWindow.removeEventListener('mousedown', deactivate, true);
viewerWindow.removeEventListener('unload', deactivate, true);
parentWindow.removeEventListener('keydown', forwardKeyEvent, true);
parentWindow.removeEventListener('keyup', forwardKeyEvent, true);
sendFocusEvent('blur');
viewerWindow.addEventListener('mousedown', activate, true);
}
function forwardKeyEvent(e) {
var event = viewerWindow.document.createEvent('KeyboardEvent');
event.initKeyEvent(e.type,
e.bubbles,
e.cancelable,
e.view,
e.ctrlKey,
e.altKey,
e.shiftKey,
e.metaKey,
e.keyCode,
e.charCode);
viewerWindow.dispatchEvent(event);
}
function sendFocusEvent(type) {
var event = viewerWindow.document.createEvent("UIEvent");
event.initEvent(type, false, true);
viewerWindow.dispatchEvent(event);
}
viewerWindow.addEventListener('mousedown', activate, true);
}
if (window.document.readyState === "interactive" ||
@ -1034,6 +1087,8 @@ ShumwayStreamConverterBase.prototype = {
return;
}
domWindow.swfUrlLoading = actions.url;
// Report telemetry on amount of swfs on the page
if (actions.isOverlay) {
// Looking for last actions with same baseUrl

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,2 +1,2 @@
0.9.3775
a82ac47
0.10.182
0195a96

Binary file not shown.

View File

@ -111,6 +111,7 @@ limitations under the License.
<menuitem label="Open in Inspector" id="inspectorMenu"></menuitem>
<menuitem label="Report Problems" id="reportMenu"></menuitem>
<menuitem label="Reload in Adobe Flash Player" id="fallbackMenu" hidden></menuitem>
<menuitem label="Debug this SWF" id="debugMenu"></menuitem>
<menuitem label="About Shumway %version%..." id="aboutMenu"></menuitem>
</menu>
</section>

View File

@ -93,7 +93,7 @@ var viewerPlayerglobalInfo = {
catalog: SHUMWAY_ROOT + "playerglobal/playerglobal.json"
};
var builtinPath = SHUMWAY_ROOT + "avm2/generated/builtin/builtin.abc";
var builtinPath = SHUMWAY_ROOT + "libs/builtin.abc";
var playerWindow;
var playerWindowLoaded = new Promise(function(resolve) {
@ -165,6 +165,13 @@ function runViewer() {
var version = Shumway.version || '';
document.getElementById('aboutMenu').label =
document.getElementById('aboutMenu').label.replace('%version%', version);
var debugMenuEnabled = FirefoxCom.requestSync('getBoolPref', {pref: 'shumway.debug.enabled', def: false});
if (debugMenuEnabled) {
document.getElementById('debugMenu').addEventListener('click', enableDebug);
} else {
document.getElementById('debugMenu').remove();
}
}
function showURL() {
@ -205,6 +212,10 @@ function showAbout() {
window.open('http://areweflashyet.com/');
}
function enableDebug() {
ShumwayCom.enableDebug();
}
var movieUrl, movieParams, objectParams;
window.addEventListener("message", function handlerMessage(e) {

View File

@ -22,8 +22,7 @@ var viewerPlayerglobalInfo = {
catalog: SHUMWAY_ROOT + "playerglobal/playerglobal.json"
};
var avm2Root = SHUMWAY_ROOT + "avm2/";
var builtinPath = avm2Root + "generated/builtin/builtin.abc";
var builtinPath = SHUMWAY_ROOT + "libs/builtin.abc";
window.print = function(msg) {
console.log(msg);

View File

@ -3,7 +3,7 @@
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<!ENTITY window.title "Exceptions">
<!ENTITY window.width "36em">
<!ENTITY window.width "45em">
<!ENTITY treehead.sitename.label "Site">
<!ENTITY treehead.status.label "Status">
@ -21,6 +21,8 @@
<!ENTITY allow.accesskey "A">
<!ENTITY windowClose.key "w">
<!ENTITY button.close.label "Close">
<!ENTITY button.close.accesskey "C">
<!ENTITY button.cancel.label "Cancel">
<!ENTITY button.cancel.accesskey "C">
<!ENTITY button.ok.label "Save Changes">
<!ENTITY button.ok.accesskey "S">

View File

@ -1328,14 +1328,15 @@ public:
already_AddRefed<nsIGetUserMediaDevicesSuccessCallback> aOnSuccess,
already_AddRefed<nsIDOMGetUserMediaErrorCallback> aOnFailure,
uint64_t aWindowId, nsACString& aAudioLoopbackDev,
nsACString& aVideoLoopbackDev)
nsACString& aVideoLoopbackDev, bool aUseFakeDevices)
: mConstraints(aConstraints)
, mOnSuccess(aOnSuccess)
, mOnFailure(aOnFailure)
, mManager(MediaManager::GetInstance())
, mWindowId(aWindowId)
, mLoopbackAudioDevice(aAudioLoopbackDev)
, mLoopbackVideoDevice(aVideoLoopbackDev) {}
, mLoopbackVideoDevice(aVideoLoopbackDev)
, mUseFakeDevices(aUseFakeDevices) {}
void // NS_IMETHOD
Run()
@ -1343,7 +1344,7 @@ public:
NS_ASSERTION(!NS_IsMainThread(), "Don't call on main thread");
nsRefPtr<MediaEngine> backend;
if (mConstraints.mFake)
if (mConstraints.mFake || mUseFakeDevices)
backend = new MediaEngineDefault(mConstraints.mFakeTracks);
else
backend = mManager->GetBackend(mWindowId);
@ -1387,6 +1388,7 @@ private:
// automated media tests only.
nsCString mLoopbackAudioDevice;
nsCString mLoopbackVideoDevice;
bool mUseFakeDevices;
};
MediaManager::MediaManager()
@ -1596,10 +1598,15 @@ MediaManager::GetUserMedia(
if (!Preferences::GetBool("media.navigator.video.enabled", true)) {
c.mVideo.SetAsBoolean() = false;
}
bool fake = true;
if (!c.mFake &&
!Preferences::GetBool("media.navigator.streams.fake", false)) {
fake = false;
}
// Pass callbacks and MediaStreamListener along to GetUserMediaTask.
nsAutoPtr<GetUserMediaTask> task;
if (c.mFake) {
if (fake) {
// Fake stream from default backend.
task = new GetUserMediaTask(c, onSuccess.forget(),
onFailure.forget(), windowID, listener, mPrefs, new MediaEngineDefault(c.mFakeTracks));
@ -1711,7 +1718,7 @@ MediaManager::GetUserMedia(
// XXX No full support for picture in Desktop yet (needs proper UI)
if (privileged ||
(c.mFake && !Preferences::GetBool("media.navigator.permission.fake"))) {
(fake && !Preferences::GetBool("media.navigator.permission.fake"))) {
MediaManager::GetMessageLoop()->PostTask(FROM_HERE, task.forget());
} else {
bool isHTTPS = false;
@ -1805,12 +1812,14 @@ MediaManager::GetUserMediaDevices(nsPIDOMWindow* aWindow,
Preferences::GetCString("media.audio_loopback_dev");
nsAdoptingCString loopbackVideoDevice =
Preferences::GetCString("media.video_loopback_dev");
bool useFakeStreams =
Preferences::GetBool("media.navigator.streams.fake", false);
MediaManager::GetMessageLoop()->PostTask(FROM_HERE,
new GetUserMediaDevicesTask(
aConstraints, onSuccess.forget(), onFailure.forget(),
(aInnerWindowID ? aInnerWindowID : aWindow->WindowID()),
loopbackAudioDevice, loopbackVideoDevice));
loopbackAudioDevice, loopbackVideoDevice, useFakeStreams));
return NS_OK;
}

12
intl/locales/Makefile.in Normal file
View File

@ -0,0 +1,12 @@
# 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/.
include $(topsrcdir)/config/rules.mk
PATTERN_FILES = $(strip $(wildcard $(srcdir)/*/hyphenation/*.dic))
ifneq (,$(PATTERN_FILES))
libs::
$(INSTALL) $(PATTERN_FILES) $(FINAL_TARGET)/hyphenation
endif

View File

@ -4,45 +4,3 @@
# 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/.
locales = [
'af',
'bg',
'ca',
'cy',
'da',
'de-1901',
'de-1996',
'de-CH',
# 'en-US', # en-US is renamed -- see below.
'eo',
'es',
'et',
'fi',
'fr',
'gl',
'hr',
'hsb',
'hu',
'ia',
'is',
'it',
'kmr',
'la',
'lt',
'mn',
'nb',
'nl',
'nn',
'pl',
'pt',
'ru',
'sh',
'sl',
'sv',
'tr',
'uk',
]
filename = '{locale}/hyphenation/hyph_{locale}.dic'
FINAL_TARGET_FILES.hyphenation += [filename.format(locale=locale) for locale in locales]
# en-US is a special case: the dic file is named like en_US.
FINAL_TARGET_FILES.hyphenation += ['en-US/hyphenation/hyph_en_US.dic']

View File

@ -133,15 +133,12 @@ import android.view.ViewGroup;
import android.view.ViewStub;
import android.view.ViewTreeObserver;
import android.view.Window;
import android.view.WindowManager;
import android.view.animation.Interpolator;
import android.widget.ListView;
import android.widget.RelativeLayout;
import android.widget.Toast;
import android.widget.ViewFlipper;
import com.readystatesoftware.systembartint.SystemBarTintManager;
public class BrowserApp extends GeckoApp
implements TabsPanel.TabsLayoutChangeListener,
PropertyAnimator.PropertyAnimationListener,
@ -245,8 +242,6 @@ public class BrowserApp extends GeckoApp
private ReadingListHelper mReadingListHelper;
private SystemBarTintManager mTintManager;
// The tab to be selected on editing mode exit.
private Integer mTargetTabForEditingMode;
@ -687,8 +682,6 @@ public class BrowserApp extends GeckoApp
final Context appContext = getApplicationContext();
setupSystemUITinting();
mBrowserChrome = (ViewGroup) findViewById(R.id.browser_chrome);
mActionBarFlipper = (ViewFlipper) findViewById(R.id.browser_actionbar);
mActionBar = (ActionModeCompatView) findViewById(R.id.actionbar);
@ -838,31 +831,6 @@ public class BrowserApp extends GeckoApp
}
}
private void setupSystemUITinting() {
if (!Versions.feature19Plus) {
return;
}
getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
mTintManager = new SystemBarTintManager(this);
mTintManager.setTintColor(getResources().getColor(R.color.background_tabs));
updateSystemUITinting(mRootLayout.getSystemUiVisibility());
mRootLayout.setOnSystemUiVisibilityChangeListener(new View.OnSystemUiVisibilityChangeListener() {
@Override
public void onSystemUiVisibilityChange(int visibility) {
updateSystemUITinting(visibility);
}
});
}
private void updateSystemUITinting(int visibility) {
final boolean shouldTint = (visibility & View.SYSTEM_UI_FLAG_HIDE_NAVIGATION) == 0 &&
(visibility & View.SYSTEM_UI_FLAG_FULLSCREEN) == 0;
mTintManager.setStatusBarTintEnabled(shouldTint);
}
/**
* Check and show the firstrun pane if the browser has never been launched and
* is not opening an external link from another application.
@ -2649,11 +2617,6 @@ public class BrowserApp extends GeckoApp
view.getHitRect(mTempRect);
mTempRect.offset(-view.getScrollX(), -view.getScrollY());
if (mTintManager != null) {
SystemBarTintManager.SystemBarConfig config = mTintManager.getConfig();
mTempRect.offset(0, -config.getPixelInsetTop(false));
}
int[] viewCoords = new int[2];
view.getLocationOnScreen(viewCoords);

View File

@ -571,7 +571,7 @@ final class GeckoEditable
}
@Override
public void setUpdateGecko(boolean update) {
public void setUpdateGecko(boolean update, boolean force) {
if (!onIcThread()) {
// Android may be holding an old InputConnection; ignore
if (DEBUG) {
@ -580,7 +580,7 @@ final class GeckoEditable
return;
}
if (update) {
icUpdateGecko(false);
icUpdateGecko(force);
}
mUpdateGecko = update;
}

View File

@ -14,7 +14,7 @@ import android.text.Editable;
interface GeckoEditableClient {
void sendEvent(GeckoEvent event);
Editable getEditable();
void setUpdateGecko(boolean update);
void setUpdateGecko(boolean update, boolean force);
void setSuppressKeyUp(boolean suppress);
Handler getInputConnectionHandler();
boolean setInputConnectionHandler(Handler handler);

View File

@ -250,7 +250,7 @@ class GeckoInputConnection
@Override
public synchronized boolean beginBatchEdit() {
mBatchEditCount++;
mEditableClient.setUpdateGecko(false);
mEditableClient.setUpdateGecko(false, false);
return true;
}
@ -259,6 +259,10 @@ class GeckoInputConnection
if (mBatchEditCount > 0) {
mBatchEditCount--;
if (mBatchEditCount == 0) {
// Force Gecko update for cancelled auto-correction of single-
// character words to prevent character duplication. (bug 1133802)
boolean forceUpdate = !mBatchTextChanged && !mBatchSelectionChanged;
if (mBatchTextChanged) {
notifyTextChange();
mBatchTextChanged = false;
@ -269,7 +273,7 @@ class GeckoInputConnection
Selection.getSelectionEnd(editable));
mBatchSelectionChanged = false;
}
mEditableClient.setUpdateGecko(true);
mEditableClient.setUpdateGecko(true, forceUpdate);
}
} else {
Log.w(LOGTAG, "endBatchEdit() called, but mBatchEditCount == 0?!");

View File

@ -43,6 +43,7 @@ class TextSelection extends Layer implements GeckoEventListener {
private final DrawListener mDrawListener;
private boolean mDraggingHandles;
private int selectionID; // Unique ID provided for each selection action.
private float mViewLeft;
private float mViewTop;
private float mViewZoom;
@ -131,6 +132,7 @@ class TextSelection extends Layer implements GeckoEventListener {
public void run() {
try {
if (event.equals("TextSelection:ShowHandles")) {
selectionID = message.getInt("selectionID");
final JSONArray handles = message.getJSONArray("handles");
for (int i=0; i < handles.length(); i++) {
String handle = handles.getString(i);
@ -315,7 +317,17 @@ class TextSelection extends Layer implements GeckoEventListener {
public void onDestroyActionMode(ActionModeCompat mode) {
mActionMode = null;
mCallback = null;
GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("TextSelection:End", null));
final JSONObject args = new JSONObject();
try {
args.put("selectionID", selectionID);
} catch (JSONException e) {
Log.e(LOGTAG, "Error building JSON arguments for TextSelection:End", e);
return;
}
final GeckoEvent event =
GeckoEvent.createBroadcastEvent("TextSelection:End", args.toString());
GeckoAppShell.sendEventToGecko(event);
}
}
}

View File

@ -628,7 +628,6 @@ gtjar.sources += [ thirdparty_source_dir + f for f in [
'com/nineoldandroids/view/ViewPropertyAnimatorHC.java',
'com/nineoldandroids/view/ViewPropertyAnimatorICS.java',
'com/nineoldandroids/view/ViewPropertyAnimatorPreHC.java',
'com/readystatesoftware/systembartint/SystemBarTintManager.java',
'com/squareup/picasso/Action.java',
'com/squareup/picasso/AssetBitmapHunter.java',
'com/squareup/picasso/BitmapHunter.java',

View File

@ -7,8 +7,7 @@
xmlns:gecko="http://schemas.android.com/apk/res-auto"
android:id="@+id/root_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true">
android:layout_height="match_parent">
<ViewStub android:id="@+id/tabs_panel"
android:layout="@layout/tabs_panel_view"

View File

@ -102,7 +102,8 @@ function testLTR_selectAll() {
]).then(function() {
// Close selection and complete test.
Services.obs.notifyObservers(null, "TextSelection:End", {});
Services.obs.notifyObservers(null, "TextSelection:End",
JSON.stringify({selectionID: sh._selectionID}));
return Promise.all([
ok(!sh.isSelectionActive(),
@ -171,7 +172,8 @@ function testRTL_selectAll() {
]).then(function() {
// Close selection and complete test.
Services.obs.notifyObservers(null, "TextSelection:End", {});
Services.obs.notifyObservers(null, "TextSelection:End",
JSON.stringify({selectionID: sh._selectionID}));
return Promise.all([
ok(!sh.isSelectionActive(),
@ -216,7 +218,8 @@ function testLTR_dragFocusHandleToSelf() {
let focusDragSelectionText = sh._getSelectedText();
// Complete test, and report.
Services.obs.notifyObservers(null, "TextSelection:End", {});
Services.obs.notifyObservers(null, "TextSelection:End",
JSON.stringify({selectionID: sh._selectionID}));
return Promise.all([
ok(true, "testLTR_dragFocusHandleToSelf - Test Starts."),
@ -275,7 +278,8 @@ function testLTR_dragAnchorHandleToSelf() {
let anchorDragSelectionText = sh._getSelectedText();
// Complete test, and report.
Services.obs.notifyObservers(null, "TextSelection:End", {});
Services.obs.notifyObservers(null, "TextSelection:End",
JSON.stringify({selectionID: sh._selectionID}));
return Promise.all([
ok(true, "testLTR_dragAnchorHandleToSelf - Test Starts."),
@ -333,7 +337,8 @@ function testRTL_dragFocusHandleToSelf() {
let focusDragSelectionText = sh._getSelectedText();
// Complete test, and report.
Services.obs.notifyObservers(null, "TextSelection:End", {});
Services.obs.notifyObservers(null, "TextSelection:End",
JSON.stringify({selectionID: sh._selectionID}));
return Promise.all([
ok(true, "testRTL_dragFocusHandleToSelf - Test Starts."),
@ -392,7 +397,8 @@ function testRTL_dragAnchorHandleToSelf() {
let anchorDragSelectionText = sh._getSelectedText();
// Complete test, and report.
Services.obs.notifyObservers(null, "TextSelection:End", {});
Services.obs.notifyObservers(null, "TextSelection:End",
JSON.stringify({selectionID: sh._selectionID}));
return Promise.all([
ok(true, "testRTL_dragAnchorHandleToSelf - Test Starts."),

View File

@ -208,7 +208,12 @@ function testCloseSelection() {
// Various ways to close an active selection.
]).then(function() {
sh.startSelection(inputNode);
Services.obs.notifyObservers(null, "TextSelection:End", {});
Services.obs.notifyObservers(null, "TextSelection:End",
JSON.stringify({selectionID: -1}));
return ok(sh.isSelectionActive(), "unrelated TextSelection:End should not close active selection");
}).then(function() {
Services.obs.notifyObservers(null, "TextSelection:End",
JSON.stringify({selectionID: sh._selectionID}));
return ok(!sh.isSelectionActive(), "TextSelection:End should close active selection");
}).then(function() {
@ -310,7 +315,8 @@ function testAttachCaret() {
]);
}).then(function() {
Services.obs.notifyObservers(null, "TextSelection:End", {});
Services.obs.notifyObservers(null, "TextSelection:End",
JSON.stringify({selectionID: sh._selectionID}));
return Promise.all([
ok(!sh.isSelectionActive(), "Selection should not be active at end of testAttachCaret"),

View File

@ -77,7 +77,8 @@ function testLTR_selectionPoints() {
let midpointSelText = sh._getSelectedText();
// Close selection and complete test.
Services.obs.notifyObservers(null, "TextSelection:End", {});
Services.obs.notifyObservers(null, "TextSelection:End",
JSON.stringify({selectionID: sh._selectionID}));
return Promise.all([
selectionExists(selection, "LTR Selection existed at points"),
@ -121,7 +122,8 @@ function testRTL_selectionPoints() {
let midpointSelText = sh._getSelectedText();
// Close selection and complete test.
Services.obs.notifyObservers(null, "TextSelection:End", {});
Services.obs.notifyObservers(null, "TextSelection:End",
JSON.stringify({selectionID: sh._selectionID}));
return Promise.all([
selectionExists(selection, "RTL Selection existed at points"),
@ -186,7 +188,8 @@ function test_selectionLineHeight() {
]).then(function() {
// Complete test, and report.
Services.obs.notifyObservers(null, "TextSelection:End", {});
Services.obs.notifyObservers(null, "TextSelection:End",
JSON.stringify({selectionID: sh._selectionID}));
return Promise.all([
greaterThan(selectionLineHeight, 0, "Distance from one line to another " +
@ -237,7 +240,8 @@ function testLTR_moveFocusHandleDown() {
focusPt : new Point(sh._cache.focusPt.x, sh._cache.focusPt.y) };
// Complete test, and report.
Services.obs.notifyObservers(null, "TextSelection:End", {});
Services.obs.notifyObservers(null, "TextSelection:End",
JSON.stringify({selectionID: sh._selectionID}));
return Promise.all([
ok(true, "testLTR_moveFocusHandleDown - Test Starts."),
@ -302,7 +306,8 @@ function testLTR_moveFocusHandleUp() {
focusPt : new Point(sh._cache.focusPt.x, sh._cache.focusPt.y) };
// Complete test, and report.
Services.obs.notifyObservers(null, "TextSelection:End", {});
Services.obs.notifyObservers(null, "TextSelection:End",
JSON.stringify({selectionID: sh._selectionID}));
return Promise.all([
ok(true, "testLTR_moveFocusHandleUp - Test Starts."),
@ -370,7 +375,8 @@ function testLTR_moveAnchorHandleUp() {
focusPt : new Point(sh._cache.focusPt.x, sh._cache.focusPt.y) };
// Complete test, and report.
Services.obs.notifyObservers(null, "TextSelection:End", {});
Services.obs.notifyObservers(null, "TextSelection:End",
JSON.stringify({selectionID: sh._selectionID}));
return Promise.all([
ok(true, "testLTR_moveAnchorHandleUp - Test Starts."),
@ -434,7 +440,8 @@ function testLTR_moveAnchorHandleDown() {
focusPt : new Point(sh._cache.focusPt.x, sh._cache.focusPt.y) };
// Complete test, and report.
Services.obs.notifyObservers(null, "TextSelection:End", {});
Services.obs.notifyObservers(null, "TextSelection:End",
JSON.stringify({selectionID: sh._selectionID}));
return Promise.all([
ok(true, "testLTR_moveAnchorHandleDown - Test Starts."),
@ -502,7 +509,8 @@ function testRTL_moveFocusHandleDown() {
focusPt : new Point(sh._cache.focusPt.x, sh._cache.focusPt.y) };
// Complete test, and report.
Services.obs.notifyObservers(null, "TextSelection:End", {});
Services.obs.notifyObservers(null, "TextSelection:End",
JSON.stringify({selectionID: sh._selectionID}));
return Promise.all([
ok(true, "testRTL_moveFocusHandleDown - Test Starts."),
@ -566,7 +574,8 @@ function testRTL_moveFocusHandleUp() {
focusPt : new Point(sh._cache.focusPt.x, sh._cache.focusPt.y) };
// Complete test, and report.
Services.obs.notifyObservers(null, "TextSelection:End", {});
Services.obs.notifyObservers(null, "TextSelection:End",
JSON.stringify({selectionID: sh._selectionID}));
return Promise.all([
ok(true, "testRTL_moveFocusHandleUp - Test Starts."),
@ -634,7 +643,8 @@ function testRTL_moveAnchorHandleUp() {
focusPt : new Point(sh._cache.focusPt.x, sh._cache.focusPt.y) };
// Complete test, and report.
Services.obs.notifyObservers(null, "TextSelection:End", {});
Services.obs.notifyObservers(null, "TextSelection:End",
JSON.stringify({selectionID: sh._selectionID}));
return Promise.all([
ok(true, "testRTL_moveAnchorHandleUp - Test Starts."),
@ -698,7 +708,8 @@ function testRTL_moveAnchorHandleDown() {
focusPt : new Point(sh._cache.focusPt.x, sh._cache.focusPt.y) };
// Complete test, and report.
Services.obs.notifyObservers(null, "TextSelection:End", {});
Services.obs.notifyObservers(null, "TextSelection:End",
JSON.stringify({selectionID: sh._selectionID}));
return Promise.all([
ok(true, "testRTL_moveAnchorHandleDown - Test Starts."),

View File

@ -44,6 +44,7 @@ var SelectionHandler = {
_focusIsRTL: false,
_activeType: 0, // TYPE_NONE
_selectionID: 0, // Unique Selection ID
_draggingHandles: false, // True while user drags text selection handles
_dragStartAnchorOffset: null, // Editables need initial pos during HandleMove events
@ -134,10 +135,19 @@ var SelectionHandler = {
}
break;
}
case "Tab:Selected":
case "TextSelection:End":
this._closeSelection();
break;
case "TextSelection:End":
let data = JSON.parse(aData);
// End the requested selection only.
if (this._selectionID === data.selectionID) {
this._closeSelection();
}
break;
case "TextSelection:Action":
for (let type in this.actions) {
if (this.actions[type].id == aData) {
@ -365,6 +375,7 @@ var SelectionHandler = {
// Determine position and show handles, open actionbar
this._positionHandles(positions);
Messaging.sendRequest({
selectionID: this._selectionID,
type: "TextSelection:ShowHandles",
handles: [this.HANDLE_TYPE_ANCHOR, this.HANDLE_TYPE_FOCUS]
});
@ -756,6 +767,7 @@ var SelectionHandler = {
// Determine position and show caret, open actionbar
this._positionHandles();
Messaging.sendRequest({
selectionID: this._selectionID,
type: "TextSelection:ShowHandles",
handles: [this.HANDLE_TYPE_CARET]
});
@ -777,6 +789,7 @@ var SelectionHandler = {
aElement.focus();
}
this._selectionID++;
this._stopDraggingHandles();
this._contentWindow = aElement.ownerDocument.defaultView;
this._targetIsRTL = (this._contentWindow.getComputedStyle(aElement, "").direction == "rtl");

View File

@ -1,565 +0,0 @@
/*
* Copyright (C) 2013 readyState Software Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.readystatesoftware.systembartint;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.app.Activity;
import android.content.Context;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.util.DisplayMetrics;
import android.util.TypedValue;
import android.view.Gravity;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.Window;
import android.view.WindowManager;
import android.widget.FrameLayout.LayoutParams;
import java.lang.reflect.Method;
/**
* Class to manage status and navigation bar tint effects when using KitKat
* translucent system UI modes.
*
*/
public class SystemBarTintManager {
static {
// Android allows a system property to override the presence of the navigation bar.
// Used by the emulator.
// See https://github.com/android/platform_frameworks_base/blob/master/policy/src/com/android/internal/policy/impl/PhoneWindowManager.java#L1076
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
try {
Class c = Class.forName("android.os.SystemProperties");
Method m = c.getDeclaredMethod("get", String.class);
m.setAccessible(true);
sNavBarOverride = (String) m.invoke(null, "qemu.hw.mainkeys");
} catch (Throwable e) {
sNavBarOverride = null;
}
}
}
/**
* The default system bar tint color value.
*/
public static final int DEFAULT_TINT_COLOR = 0x99000000;
private static String sNavBarOverride;
private final SystemBarConfig mConfig;
private boolean mStatusBarAvailable;
private boolean mNavBarAvailable;
private boolean mStatusBarTintEnabled;
private boolean mNavBarTintEnabled;
private View mStatusBarTintView;
private View mNavBarTintView;
/**
* Constructor. Call this in the host activity onCreate method after its
* content view has been set. You should always create new instances when
* the host activity is recreated.
*
* @param activity The host activity.
*/
@TargetApi(19)
public SystemBarTintManager(Activity activity) {
Window win = activity.getWindow();
ViewGroup decorViewGroup = (ViewGroup) win.getDecorView();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
// check theme attrs
int[] attrs = {android.R.attr.windowTranslucentStatus,
android.R.attr.windowTranslucentNavigation};
TypedArray a = activity.obtainStyledAttributes(attrs);
try {
mStatusBarAvailable = a.getBoolean(0, false);
mNavBarAvailable = a.getBoolean(1, false);
} finally {
a.recycle();
}
// check window flags
WindowManager.LayoutParams winParams = win.getAttributes();
int bits = WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS;
if ((winParams.flags & bits) != 0) {
mStatusBarAvailable = true;
}
bits = WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION;
if ((winParams.flags & bits) != 0) {
mNavBarAvailable = true;
}
}
mConfig = new SystemBarConfig(activity, mStatusBarAvailable, mNavBarAvailable);
// device might not have virtual navigation keys
if (!mConfig.hasNavigtionBar()) {
mNavBarAvailable = false;
}
if (mStatusBarAvailable) {
setupStatusBarView(activity, decorViewGroup);
}
if (mNavBarAvailable) {
setupNavBarView(activity, decorViewGroup);
}
}
/**
* Enable tinting of the system status bar.
*
* If the platform is running Jelly Bean or earlier, or translucent system
* UI modes have not been enabled in either the theme or via window flags,
* then this method does nothing.
*
* @param enabled True to enable tinting, false to disable it (default).
*/
public void setStatusBarTintEnabled(boolean enabled) {
mStatusBarTintEnabled = enabled;
if (mStatusBarAvailable) {
mStatusBarTintView.setVisibility(enabled ? View.VISIBLE : View.GONE);
}
}
/**
* Enable tinting of the system navigation bar.
*
* If the platform does not have soft navigation keys, is running Jelly Bean
* or earlier, or translucent system UI modes have not been enabled in either
* the theme or via window flags, then this method does nothing.
*
* @param enabled True to enable tinting, false to disable it (default).
*/
public void setNavigationBarTintEnabled(boolean enabled) {
mNavBarTintEnabled = enabled;
if (mNavBarAvailable) {
mNavBarTintView.setVisibility(enabled ? View.VISIBLE : View.GONE);
}
}
/**
* Apply the specified color tint to all system UI bars.
*
* @param color The color of the background tint.
*/
public void setTintColor(int color) {
setStatusBarTintColor(color);
setNavigationBarTintColor(color);
}
/**
* Apply the specified drawable or color resource to all system UI bars.
*
* @param res The identifier of the resource.
*/
public void setTintResource(int res) {
setStatusBarTintResource(res);
setNavigationBarTintResource(res);
}
/**
* Apply the specified drawable to all system UI bars.
*
* @param drawable The drawable to use as the background, or null to remove it.
*/
public void setTintDrawable(Drawable drawable) {
setStatusBarTintDrawable(drawable);
setNavigationBarTintDrawable(drawable);
}
/**
* Apply the specified alpha to all system UI bars.
*
* @param alpha The alpha to use
*/
public void setTintAlpha(float alpha) {
setStatusBarAlpha(alpha);
setNavigationBarAlpha(alpha);
}
/**
* Apply the specified color tint to the system status bar.
*
* @param color The color of the background tint.
*/
public void setStatusBarTintColor(int color) {
if (mStatusBarAvailable) {
mStatusBarTintView.setBackgroundColor(color);
}
}
/**
* Apply the specified drawable or color resource to the system status bar.
*
* @param res The identifier of the resource.
*/
public void setStatusBarTintResource(int res) {
if (mStatusBarAvailable) {
mStatusBarTintView.setBackgroundResource(res);
}
}
/**
* Apply the specified drawable to the system status bar.
*
* @param drawable The drawable to use as the background, or null to remove it.
*/
@SuppressWarnings("deprecation")
public void setStatusBarTintDrawable(Drawable drawable) {
if (mStatusBarAvailable) {
mStatusBarTintView.setBackgroundDrawable(drawable);
}
}
/**
* Apply the specified alpha to the system status bar.
*
* @param alpha The alpha to use
*/
@TargetApi(11)
public void setStatusBarAlpha(float alpha) {
if (mStatusBarAvailable && Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
mStatusBarTintView.setAlpha(alpha);
}
}
/**
* Apply the specified color tint to the system navigation bar.
*
* @param color The color of the background tint.
*/
public void setNavigationBarTintColor(int color) {
if (mNavBarAvailable) {
mNavBarTintView.setBackgroundColor(color);
}
}
/**
* Apply the specified drawable or color resource to the system navigation bar.
*
* @param res The identifier of the resource.
*/
public void setNavigationBarTintResource(int res) {
if (mNavBarAvailable) {
mNavBarTintView.setBackgroundResource(res);
}
}
/**
* Apply the specified drawable to the system navigation bar.
*
* @param drawable The drawable to use as the background, or null to remove it.
*/
@SuppressWarnings("deprecation")
public void setNavigationBarTintDrawable(Drawable drawable) {
if (mNavBarAvailable) {
mNavBarTintView.setBackgroundDrawable(drawable);
}
}
/**
* Apply the specified alpha to the system navigation bar.
*
* @param alpha The alpha to use
*/
@TargetApi(11)
public void setNavigationBarAlpha(float alpha) {
if (mNavBarAvailable && Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
mNavBarTintView.setAlpha(alpha);
}
}
/**
* Get the system bar configuration.
*
* @return The system bar configuration for the current device configuration.
*/
public SystemBarConfig getConfig() {
return mConfig;
}
/**
* Is tinting enabled for the system status bar?
*
* @return True if enabled, False otherwise.
*/
public boolean isStatusBarTintEnabled() {
return mStatusBarTintEnabled;
}
/**
* Is tinting enabled for the system navigation bar?
*
* @return True if enabled, False otherwise.
*/
public boolean isNavBarTintEnabled() {
return mNavBarTintEnabled;
}
private void setupStatusBarView(Context context, ViewGroup decorViewGroup) {
mStatusBarTintView = new View(context);
LayoutParams params = new LayoutParams(LayoutParams.MATCH_PARENT, mConfig.getStatusBarHeight());
params.gravity = Gravity.TOP;
if (mNavBarAvailable && !mConfig.isNavigationAtBottom()) {
params.rightMargin = mConfig.getNavigationBarWidth();
}
mStatusBarTintView.setLayoutParams(params);
mStatusBarTintView.setBackgroundColor(DEFAULT_TINT_COLOR);
mStatusBarTintView.setVisibility(View.GONE);
decorViewGroup.addView(mStatusBarTintView);
}
private void setupNavBarView(Context context, ViewGroup decorViewGroup) {
mNavBarTintView = new View(context);
LayoutParams params;
if (mConfig.isNavigationAtBottom()) {
params = new LayoutParams(LayoutParams.MATCH_PARENT, mConfig.getNavigationBarHeight());
params.gravity = Gravity.BOTTOM;
} else {
params = new LayoutParams(mConfig.getNavigationBarWidth(), LayoutParams.MATCH_PARENT);
params.gravity = Gravity.RIGHT;
}
mNavBarTintView.setLayoutParams(params);
mNavBarTintView.setBackgroundColor(DEFAULT_TINT_COLOR);
mNavBarTintView.setVisibility(View.GONE);
decorViewGroup.addView(mNavBarTintView);
}
/**
* Class which describes system bar sizing and other characteristics for the current
* device configuration.
*
*/
public static class SystemBarConfig {
private static final String STATUS_BAR_HEIGHT_RES_NAME = "status_bar_height";
private static final String NAV_BAR_HEIGHT_RES_NAME = "navigation_bar_height";
private static final String NAV_BAR_HEIGHT_LANDSCAPE_RES_NAME = "navigation_bar_height_landscape";
private static final String NAV_BAR_WIDTH_RES_NAME = "navigation_bar_width";
private static final String SHOW_NAV_BAR_RES_NAME = "config_showNavigationBar";
private final boolean mTranslucentStatusBar;
private final boolean mTranslucentNavBar;
private final int mStatusBarHeight;
private final int mActionBarHeight;
private final boolean mHasNavigationBar;
private final int mNavigationBarHeight;
private final int mNavigationBarWidth;
private final boolean mInPortrait;
private final float mSmallestWidthDp;
private SystemBarConfig(Activity activity, boolean translucentStatusBar, boolean traslucentNavBar) {
Resources res = activity.getResources();
mInPortrait = (res.getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT);
mSmallestWidthDp = getSmallestWidthDp(activity);
mStatusBarHeight = getInternalDimensionSize(res, STATUS_BAR_HEIGHT_RES_NAME);
mActionBarHeight = getActionBarHeight(activity);
mNavigationBarHeight = getNavigationBarHeight(activity);
mNavigationBarWidth = getNavigationBarWidth(activity);
mHasNavigationBar = (mNavigationBarHeight > 0);
mTranslucentStatusBar = translucentStatusBar;
mTranslucentNavBar = traslucentNavBar;
}
@TargetApi(14)
private int getActionBarHeight(Context context) {
int result = 0;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
TypedValue tv = new TypedValue();
context.getTheme().resolveAttribute(android.R.attr.actionBarSize, tv, true);
result = TypedValue.complexToDimensionPixelSize(tv.data, context.getResources().getDisplayMetrics());
}
return result;
}
@TargetApi(14)
private int getNavigationBarHeight(Context context) {
Resources res = context.getResources();
int result = 0;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
if (hasNavBar(context)) {
String key;
if (mInPortrait) {
key = NAV_BAR_HEIGHT_RES_NAME;
} else {
key = NAV_BAR_HEIGHT_LANDSCAPE_RES_NAME;
}
return getInternalDimensionSize(res, key);
}
}
return result;
}
@TargetApi(14)
private int getNavigationBarWidth(Context context) {
Resources res = context.getResources();
int result = 0;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
if (hasNavBar(context)) {
return getInternalDimensionSize(res, NAV_BAR_WIDTH_RES_NAME);
}
}
return result;
}
@TargetApi(14)
private boolean hasNavBar(Context context) {
Resources res = context.getResources();
int resourceId = res.getIdentifier(SHOW_NAV_BAR_RES_NAME, "bool", "android");
if (resourceId != 0) {
boolean hasNav = res.getBoolean(resourceId);
// check override flag (see static block)
if ("1".equals(sNavBarOverride)) {
hasNav = false;
} else if ("0".equals(sNavBarOverride)) {
hasNav = true;
}
return hasNav;
} else { // fallback
return !ViewConfiguration.get(context).hasPermanentMenuKey();
}
}
private int getInternalDimensionSize(Resources res, String key) {
int result = 0;
int resourceId = res.getIdentifier(key, "dimen", "android");
if (resourceId > 0) {
result = res.getDimensionPixelSize(resourceId);
}
return result;
}
@SuppressLint("NewApi")
private float getSmallestWidthDp(Activity activity) {
DisplayMetrics metrics = new DisplayMetrics();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
activity.getWindowManager().getDefaultDisplay().getRealMetrics(metrics);
} else {
// TODO this is not correct, but we don't really care pre-kitkat
activity.getWindowManager().getDefaultDisplay().getMetrics(metrics);
}
float widthDp = metrics.widthPixels / metrics.density;
float heightDp = metrics.heightPixels / metrics.density;
return Math.min(widthDp, heightDp);
}
/**
* Should a navigation bar appear at the bottom of the screen in the current
* device configuration? A navigation bar may appear on the right side of
* the screen in certain configurations.
*
* @return True if navigation should appear at the bottom of the screen, False otherwise.
*/
public boolean isNavigationAtBottom() {
return (mSmallestWidthDp >= 600 || mInPortrait);
}
/**
* Get the height of the system status bar.
*
* @return The height of the status bar (in pixels).
*/
public int getStatusBarHeight() {
return mStatusBarHeight;
}
/**
* Get the height of the action bar.
*
* @return The height of the action bar (in pixels).
*/
public int getActionBarHeight() {
return mActionBarHeight;
}
/**
* Does this device have a system navigation bar?
*
* @return True if this device uses soft key navigation, False otherwise.
*/
public boolean hasNavigtionBar() {
return mHasNavigationBar;
}
/**
* Get the height of the system navigation bar.
*
* @return The height of the navigation bar (in pixels). If the device does not have
* soft navigation keys, this will always return 0.
*/
public int getNavigationBarHeight() {
return mNavigationBarHeight;
}
/**
* Get the width of the system navigation bar when it is placed vertically on the screen.
*
* @return The width of the navigation bar (in pixels). If the device does not have
* soft navigation keys, this will always return 0.
*/
public int getNavigationBarWidth() {
return mNavigationBarWidth;
}
/**
* Get the layout inset for any system UI that appears at the top of the screen.
*
* @param withActionBar True to include the height of the action bar, False otherwise.
* @return The layout inset (in pixels).
*/
public int getPixelInsetTop(boolean withActionBar) {
return (mTranslucentStatusBar ? mStatusBarHeight : 0) + (withActionBar ? mActionBarHeight : 0);
}
/**
* Get the layout inset for any system UI that appears at the bottom of the screen.
*
* @return The layout inset (in pixels).
*/
public int getPixelInsetBottom() {
if (mTranslucentNavBar && isNavigationAtBottom()) {
return mNavigationBarHeight;
} else {
return 0;
}
}
/**
* Get the layout inset for any system UI that appears at the right of the screen.
*
* @return The layout inset (in pixels).
*/
public int getPixelInsetRight() {
if (mTranslucentNavBar && !isNavigationAtBottom()) {
return mNavigationBarWidth;
} else {
return 0;
}
}
}
}

View File

@ -0,0 +1,281 @@
/* 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/. */
const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, 'Services',
'resource://gre/modules/Services.jsm');
XPCOMUtils.defineLazyModuleGetter(this, 'Preferences',
'resource://gre/modules/Preferences.jsm');
XPCOMUtils.defineLazyModuleGetter(this, 'FileUtils',
'resource://gre/modules/FileUtils.jsm');
XPCOMUtils.defineLazyModuleGetter(this, 'Log',
'resource://gre/modules/Log.jsm');
XPCOMUtils.defineLazyModuleGetter(this, 'Task',
'resource://gre/modules/Task.jsm');
XPCOMUtils.defineLazyModuleGetter(this, 'OS',
'resource://gre/modules/osfile.jsm');
XPCOMUtils.defineLazyModuleGetter(this, 'CommonUtils',
'resource://services-common/utils.js');
this.EXPORTED_SYMBOLS = [
"LogManager",
];
const DEFAULT_MAX_ERROR_AGE = 20 * 24 * 60 * 60; // 20 days
// "shared" logs (ie, where the same log name is used by multiple LogManager
// instances) are a fact of life here - eg, FirefoxAccounts logs are used by
// both Sync and Reading-list.
// However, different instances have different pref branches, so we need to
// handle when one pref branch says "Debug" and the other says "Error"
// So we (a) keep singleton console and dump appenders and (b) keep track
// of the minimum (ie, most verbose) level and use that.
// This avoids (a) the most recent setter winning (as that is indeterminate)
// and (b) multiple dump/console appenders being added to the same log multiple
// times, which would cause messages to appear twice.
// Singletons used by each instance.
let formatter;
let dumpAppender;
let consoleAppender;
// A set of all preference roots used by all instances.
let allBranches = new Set();
// The public LogManager object.
function LogManager(prefRoot, logNames, logFilePrefix) {
this.init(prefRoot, logNames, logFilePrefix);
}
LogManager.prototype = {
REASON_SUCCESS: "success",
REASON_ERROR: "error",
_cleaningUpFileLogs: false,
_prefObservers: [],
init(prefRoot, logNames, logFilePrefix) {
if (prefRoot instanceof Preferences) {
this._prefs = prefRoot;
} else {
this._prefs = new Preferences(prefRoot);
}
this.logFilePrefix = logFilePrefix;
if (!formatter) {
// Create a formatter and various appenders to attach to the logs.
formatter = new Log.BasicFormatter();
consoleAppender = new Log.ConsoleAppender(formatter);
dumpAppender = new Log.DumpAppender(formatter);
}
allBranches.add(this._prefs._branchStr);
// We create a preference observer for all our prefs so they are magically
// reflected if the pref changes after creation.
let setupAppender = (appender, prefName, defaultLevel, findSmallest = false) => {
let observer = newVal => {
let level = Log.Level[newVal] || defaultLevel;
if (findSmallest) {
// We need to find the smallest value from all prefs controlling this appender.
for (let branch of allBranches) {
let lookPrefBranch = new Preferences(branch);
let lookVal = Log.Level[lookPrefBranch.get(prefName)];
if (lookVal && lookVal < level) {
level = lookVal;
}
}
}
appender.level = level;
}
this._prefs.observe(prefName, observer, this);
this._prefObservers.push([prefName, observer]);
// and call the observer now with the current pref value.
observer(this._prefs.get(prefName));
return observer;
}
this._observeConsolePref = setupAppender(consoleAppender, "log.appender.console", Log.Level.Error, true);
this._observeDumpPref = setupAppender(dumpAppender, "log.appender.dump", Log.Level.Error, true);
// The file appender doesn't get the special singleton behaviour.
let fapp = this._fileAppender = new Log.StorageStreamAppender(formatter);
// the stream gets a default of Debug as the user must go out of there way
// to see the stuff spewed to it.
this._observeStreamPref = setupAppender(fapp, "log.appender.file.level", Log.Level.Debug);
// now attach the appenders to all our logs.
for (let logName of logNames) {
let log = Log.repository.getLogger(logName);
for (let appender of [fapp, dumpAppender, consoleAppender]) {
log.addAppender(appender);
}
}
// and use the first specified log as a "root" for our log.
this._log = Log.repository.getLogger(logNames[0] + ".LogManager");
},
/**
* Cleanup this instance
*/
finalize() {
for (let [name, pref] of this._prefObservers) {
this._prefs.ignore(name, pref, this);
}
this._prefObservers = [];
try {
allBranches.delete(this._prefs._branchStr);
} catch (e) {}
this._prefs = null;
},
get _logFileDirectory() {
// At this point we don't allow a custom directory for the logs so
// about:sync-log can be used. We could fix this later if necessary.
return FileUtils.getDir("ProfD", ["weave", "logs"]);
},
/**
* Copy an input stream to the named file, doing everything off the main
* thread.
* outputFile is an nsIFile, but is used only for the name.
* Returns a promise that is resolved with the file modification date on
* completion or rejected if there is an error.
*/
_copyStreamToFile: Task.async(function* (inputStream, outputFile) {
// The log data could be large, so we don't want to pass it all in a single
// message, so use BUFFER_SIZE chunks.
const BUFFER_SIZE = 8192;
// get a binary stream
let binaryStream = Cc['@mozilla.org/binaryinputstream;1'].createInstance(Ci.nsIBinaryInputStream);
binaryStream.setInputStream(inputStream);
yield OS.File.makeDir(outputFile.parent.path, { ignoreExisting: true });
let output = yield OS.File.open(outputFile.path, { write: true} );
try {
while (true) {
let available = binaryStream.available();
if (!available) {
break;
}
let chunk = binaryStream.readByteArray(Math.min(available, BUFFER_SIZE));
yield output.write(new Uint8Array(chunk));
}
} finally {
inputStream.close();
binaryStream.close();
yield output.close();
}
this._log.trace("finished copy to", outputFile.path);
return (yield OS.File.stat(outputFile.path)).lastModificationDate;
}),
/**
* Possibly generate a log file for all accumulated log messages and refresh
* the input & output streams.
* Returns a promise that resolves on completion or rejects if the file could
* not be written.
*/
resetFileLog(reason) {
return new Promise((resolve, reject) => {
try {
let flushToFile;
let reasonPrefix;
switch (reason) {
case this.REASON_SUCCESS:
flushToFile = this._prefs.get("log.appender.file.logOnSuccess");
reasonPrefix = "success";
break;
case this.REASON_ERROR:
flushToFile = this._prefs.get("log.appender.file.logOnError");
reasonPrefix = "error";
break;
default:
return reject(new Error("Invalid reason"));
}
let inStream = this._fileAppender.getInputStream();
this._fileAppender.reset();
if (flushToFile && inStream) {
this._log.debug("Flushing file log");
let filename = this.logFilePrefix + "-" + reasonPrefix + "-" + Date.now() + ".txt";
let file = this._logFileDirectory;
file.append(filename);
this._log.trace("Beginning stream copy to " + file.leafName + ": " +
Date.now());
this._copyStreamToFile(inStream, file).then(
modDate => {
this._log.trace("onCopyComplete: " + Date.now());
this._log.trace("Output file timestamp: " + modDate + " (" + modDate.getTime() + ")");
},
err => {
this._log.error("Failed to copy log stream to file", err)
reject(err)
}
).then(
() => {
// It's not completely clear to markh why we only do log cleanups
// for errors, but for now the Sync semantics have been copied...
// (one theory is that only cleaning up on error makes it less
// likely old error logs would be removed, but that's not true if
// there are occasional errors - let's address this later!)
if (reason == this.REASON_ERROR &&
!this._cleaningUpFileLogs) {
this._log.trace("Scheduling cleanup.");
// Note we don't return or wait on this promise - it continues
// in the background
this.cleanupLogs().then(null, err => {
this._log.error("Failed to cleanup logs", err);
});
}
resolve();
}
);
} else {
resolve();
}
} catch (ex) {
this._log.error("Failed to resetFileLog", ex)
reject(ex);
}
})
},
/**
* Finds all logs older than maxErrorAge and deletes them using async I/O.
*/
cleanupLogs: Task.async(function* () {
this._cleaningUpFileLogs = true;
let iterator = new OS.File.DirectoryIterator(this._logFileDirectory.path);
let maxAge = this._prefs.get("log.appender.file.maxErrorAge", DEFAULT_MAX_ERROR_AGE);
let threshold = Date.now() - 1000 * maxAge;
this._log.debug("Log cleanup threshold time: " + threshold);
yield iterator.forEach(Task.async(function* (entry) {
if (!entry.name.startsWith(this.logFilePrefix + "-")) {
return;
}
try {
// need to call .stat() as the enumerator doesn't give that to us on *nix.
let info = yield OS.File.stat(entry.path);
if (info.lastModificationDate.getTime() >= threshold) {
return;
}
this._log.trace(" > Cleanup removing " + entry.name +
" (" + info.lastModificationDate.getTime() + ")");
yield OS.File.remove(entry.path);
this._log.trace("Deleted " + entry.name);
} catch (ex) {
this._log.debug("Encountered error trying to clean up old log file "
+ entry.name, ex);
}
}.bind(this)));
this._cleaningUpFileLogs = false;
this._log.debug("Done deleting files.");
// This notification is used only for tests.
Services.obs.notifyObservers(null, "services-tests:common:log-manager:cleanup-logs", null);
}),
}

View File

@ -13,6 +13,7 @@ EXTRA_COMPONENTS += [
EXTRA_JS_MODULES['services-common'] += [
'hawkclient.js',
'hawkrequest.js',
'logmanager.js',
'storageservice.js',
'stringbundle.js',
'tokenserverclient.js',

View File

@ -4,6 +4,7 @@
const modules = [
"async.js",
"bagheeraclient.js",
"logmanager.js",
"rest.js",
"storageservice.js",
"stringbundle.js",

View File

@ -0,0 +1,107 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
// NOTE: The sync test_errorhandler_* tests have quite good coverage for
// other aspects of this.
Cu.import("resource://services-common/logmanager.js");
Cu.import("resource://gre/modules/Log.jsm");
function run_test() {
run_next_test();
}
// Returns an array of [consoleAppender, dumpAppender, [fileAppenders]] for
// the specified log. Note that fileAppenders will usually have length=1
function getAppenders(log) {
let capps = log.appenders.filter(app => app instanceof Log.ConsoleAppender);
equal(capps.length, 1, "should only have one console appender");
let dapps = log.appenders.filter(app => app instanceof Log.DumpAppender);
equal(dapps.length, 1, "should only have one dump appender");
let fapps = log.appenders.filter(app => app instanceof Log.StorageStreamAppender);
return [capps[0], dapps[0], fapps];
}
// Test that the correct thing happens when no prefs exist for the log manager.
add_test(function test_noPrefs() {
// tell the log manager to init with a pref branch that doesn't exist.
let lm = new LogManager("no-such-branch.", ["TestLog"], "test");
let log = Log.repository.getLogger("TestLog");
let [capp, dapp, fapps] = getAppenders(log);
// the "dump" and "console" appenders should get Error level
equal(capp.level, Log.Level.Error);
equal(dapp.level, Log.Level.Error);
// and the file (stream) appender gets Dump by default
equal(fapps.length, 1, "only 1 file appender");
equal(fapps[0].level, Log.Level.Debug);
lm.finalize();
run_next_test();
});
// Test that changes to the prefs used by the log manager are updated dynamically.
add_test(function test_PrefChanges() {
Services.prefs.setCharPref("log-manager.test.log.appender.console", "Trace");
Services.prefs.setCharPref("log-manager.test.log.appender.dump", "Trace");
Services.prefs.setCharPref("log-manager.test.log.appender.file.level", "Trace");
let lm = new LogManager("log-manager.test.", ["TestLog2"], "test");
let log = Log.repository.getLogger("TestLog2");
let [capp, dapp, [fapp]] = getAppenders(log);
equal(capp.level, Log.Level.Trace);
equal(dapp.level, Log.Level.Trace);
equal(fapp.level, Log.Level.Trace);
// adjust the prefs and they should magically be reflected in the appenders.
Services.prefs.setCharPref("log-manager.test.log.appender.console", "Debug");
Services.prefs.setCharPref("log-manager.test.log.appender.dump", "Debug");
Services.prefs.setCharPref("log-manager.test.log.appender.file.level", "Debug");
equal(capp.level, Log.Level.Debug);
equal(dapp.level, Log.Level.Debug);
equal(fapp.level, Log.Level.Debug);
// and invalid values should cause them to fallback to their defaults.
Services.prefs.setCharPref("log-manager.test.log.appender.console", "xxx");
Services.prefs.setCharPref("log-manager.test.log.appender.dump", "xxx");
Services.prefs.setCharPref("log-manager.test.log.appender.file.level", "xxx");
equal(capp.level, Log.Level.Error);
equal(dapp.level, Log.Level.Error);
equal(fapp.level, Log.Level.Debug);
lm.finalize();
run_next_test();
});
// Test that the same log used by multiple log managers does the right thing.
add_test(function test_SharedLogs() {
// create the prefs for the first instance.
Services.prefs.setCharPref("log-manager-1.test.log.appender.console", "Trace");
Services.prefs.setCharPref("log-manager-1.test.log.appender.dump", "Trace");
Services.prefs.setCharPref("log-manager-1.test.log.appender.file.level", "Trace");
let lm1 = new LogManager("log-manager-1.test.", ["TestLog3"], "test");
// and the second.
Services.prefs.setCharPref("log-manager-2.test.log.appender.console", "Debug");
Services.prefs.setCharPref("log-manager-2.test.log.appender.dump", "Debug");
Services.prefs.setCharPref("log-manager-2.test.log.appender.file.level", "Debug");
let lm2 = new LogManager("log-manager-2.test.", ["TestLog3"], "test");
let log = Log.repository.getLogger("TestLog3");
let [capp, dapp, fapps] = getAppenders(log);
// console and dump appenders should be "trace" as it is more verbose than
// "debug"
equal(capp.level, Log.Level.Trace);
equal(dapp.level, Log.Level.Trace);
// Set the prefs on the -1 branch to "Error" - it should then end up with
// "Debug" from the -2 branch.
Services.prefs.setCharPref("log-manager-1.test.log.appender.console", "Error");
Services.prefs.setCharPref("log-manager-1.test.log.appender.dump", "Error");
Services.prefs.setCharPref("log-manager-1.test.log.appender.file.level", "Error");
equal(capp.level, Log.Level.Debug);
equal(dapp.level, Log.Level.Debug);
lm1.finalize();
lm2.finalize();
run_next_test();
});

View File

@ -29,6 +29,7 @@ skip-if = toolkit == 'gonk'
[test_bagheera_client.js]
[test_hawkclient.js]
[test_hawkrequest.js]
[test_logmanager.js]
[test_observers.js]
[test_restrequest.js]
[test_tokenauthenticatedrequest.js]

View File

@ -13,8 +13,7 @@ Cu.import("resource://gre/modules/Log.jsm");
Cu.import("resource://services-sync/constants.js");
Cu.import("resource://services-sync/engines.js");
Cu.import("resource://services-sync/util.js");
Cu.import("resource://gre/modules/FileUtils.jsm");
Cu.import("resource://gre/modules/NetUtil.jsm");
Cu.import("resource://services-common/logmanager.js");
XPCOMUtils.defineLazyModuleGetter(this, "Status",
"resource://services-sync/status.js");
@ -539,9 +538,6 @@ SyncScheduler.prototype = {
},
};
const LOG_PREFIX_SUCCESS = "success-";
const LOG_PREFIX_ERROR = "error-";
this.ErrorHandler = function ErrorHandler(service) {
this.service = service;
this.init();
@ -574,33 +570,14 @@ ErrorHandler.prototype = {
initLogs: function initLogs() {
this._log = Log.repository.getLogger("Sync.ErrorHandler");
this._log.level = Log.Level[Svc.Prefs.get("log.logger.service.main")];
this._cleaningUpFileLogs = false;
let root = Log.repository.getLogger("Sync");
root.level = Log.Level[Svc.Prefs.get("log.rootLogger")];
let formatter = new Log.BasicFormatter();
let capp = new Log.ConsoleAppender(formatter);
capp.level = Log.Level[Svc.Prefs.get("log.appender.console")];
root.addAppender(capp);
let logs = ["Sync", "FirefoxAccounts", "Hawk", "Common.TokenServerClient",
"Sync.SyncMigration"];
let dapp = new Log.DumpAppender(formatter);
dapp.level = Log.Level[Svc.Prefs.get("log.appender.dump")];
root.addAppender(dapp);
let fapp = this._logAppender = new Log.StorageStreamAppender(formatter);
fapp.level = Log.Level[Svc.Prefs.get("log.appender.file.level")];
root.addAppender(fapp);
// Arrange for a number of other sync-related logs to also go to our
// appenders.
for (let extra of ["FirefoxAccounts", "Hawk", "Common.TokenServerClient",
"Sync.SyncMigration"]) {
let log = Log.repository.getLogger(extra);
for (let appender of [fapp, dapp, capp]) {
log.addAppender(appender);
}
}
this._logManager = new LogManager(Svc.Prefs, logs, "sync");
},
observe: function observe(subject, topic, data) {
@ -625,8 +602,7 @@ ErrorHandler.prototype = {
this._log.debug(engine_name + " failed: " + Utils.exceptionStr(exception));
break;
case "weave:service:login:error":
this.resetFileLog(Svc.Prefs.get("log.appender.file.logOnError"),
LOG_PREFIX_ERROR);
this.resetFileLog(this._logManager.REASON_ERROR);
if (this.shouldReportError()) {
this.notifyOnNextTick("weave:ui:login:error");
@ -641,8 +617,7 @@ ErrorHandler.prototype = {
this.service.logout();
}
this.resetFileLog(Svc.Prefs.get("log.appender.file.logOnError"),
LOG_PREFIX_ERROR);
this.resetFileLog(this._logManager.REASON_ERROR);
if (this.shouldReportError()) {
this.notifyOnNextTick("weave:ui:sync:error");
@ -668,8 +643,7 @@ ErrorHandler.prototype = {
if (Status.service == SYNC_FAILED_PARTIAL) {
this._log.debug("Some engines did not sync correctly.");
this.resetFileLog(Svc.Prefs.get("log.appender.file.logOnError"),
LOG_PREFIX_ERROR);
this.resetFileLog(this._logManager.REASON_ERROR);
if (this.shouldReportError()) {
this.dontIgnoreErrors = false;
@ -677,8 +651,7 @@ ErrorHandler.prototype = {
break;
}
} else {
this.resetFileLog(Svc.Prefs.get("log.appender.file.logOnSuccess"),
LOG_PREFIX_SUCCESS);
this.resetFileLog(this._logManager.REASON_SUCCESS);
}
this.dontIgnoreErrors = false;
this.notifyOnNextTick("weave:ui:sync:finish");
@ -705,95 +678,22 @@ ErrorHandler.prototype = {
Utils.nextTick(this.service.sync, this.service);
},
/**
* Finds all logs older than maxErrorAge and deletes them without tying up I/O.
*/
cleanupLogs: function cleanupLogs() {
let direntries = FileUtils.getDir("ProfD", ["weave", "logs"]).directoryEntries;
let oldLogs = [];
let index = 0;
let threshold = Date.now() - 1000 * Svc.Prefs.get("log.appender.file.maxErrorAge");
this._log.debug("Log cleanup threshold time: " + threshold);
while (direntries.hasMoreElements()) {
let logFile = direntries.getNext().QueryInterface(Ci.nsIFile);
if (logFile.lastModifiedTime < threshold) {
this._log.trace(" > Noting " + logFile.leafName +
" for cleanup (" + logFile.lastModifiedTime + ")");
oldLogs.push(logFile);
}
}
// Deletes a file from oldLogs each tick until there are none left.
let errorHandler = this;
function deleteFile() {
if (index >= oldLogs.length) {
errorHandler._log.debug("Done deleting files.");
errorHandler._cleaningUpFileLogs = false;
Svc.Obs.notify("weave:service:cleanup-logs");
return;
}
try {
let file = oldLogs[index];
file.remove(false);
errorHandler._log.trace("Deleted " + file.leafName + ".");
} catch (ex) {
errorHandler._log._debug("Encountered error trying to clean up old log file '"
+ oldLogs[index].leafName + "':"
+ Utils.exceptionStr(ex));
}
index++;
Utils.nextTick(deleteFile);
}
if (oldLogs.length > 0) {
this._cleaningUpFileLogs = true;
Utils.nextTick(deleteFile);
} else {
this._log.debug("No logs to clean up.");
}
},
/**
* Generate a log file for the sync that just completed
* and refresh the input & output streams.
*
* @param flushToFile
* the log file to be flushed/reset
*
* @param filenamePrefix
* a value of either LOG_PREFIX_SUCCESS or LOG_PREFIX_ERROR
* to be used as the log filename prefix
* @param reason
* A constant from the LogManager that indicates the reason for the
* reset.
*/
resetFileLog: function resetFileLog(flushToFile, filenamePrefix) {
let inStream = this._logAppender.getInputStream();
this._logAppender.reset();
if (flushToFile && inStream) {
this._log.debug("Flushing file log.");
try {
let filename = filenamePrefix + Date.now() + ".txt";
let file = FileUtils.getFile("ProfD", ["weave", "logs", filename]);
let outStream = FileUtils.openFileOutputStream(file);
this._log.trace("Beginning stream copy to " + file.leafName + ": " +
Date.now());
NetUtil.asyncCopy(inStream, outStream, function onCopyComplete() {
this._log.trace("onCopyComplete: " + Date.now());
this._log.trace("Output file timestamp: " + file.lastModifiedTime);
Svc.Obs.notify("weave:service:reset-file-log");
this._log.trace("Notified: " + Date.now());
if (filenamePrefix == LOG_PREFIX_ERROR &&
!this._cleaningUpFileLogs) {
this._log.trace("Scheduling cleanup.");
Utils.nextTick(this.cleanupLogs, this);
}
}.bind(this));
} catch (ex) {
Svc.Obs.notify("weave:service:reset-file-log");
}
} else {
resetFileLog: function resetFileLog(reason) {
let onComplete = () => {
Svc.Obs.notify("weave:service:reset-file-log");
}
this._log.trace("Notified: " + Date.now());
};
// Note we do not return the promise here - the caller doesn't need to wait
// for this to complete.
this._logManager.resetFileLog(reason).then(onComplete, onComplete);
},
/**

View File

@ -14,8 +14,6 @@ Cu.import("resource://gre/modules/FileUtils.jsm");
const FAKE_SERVER_URL = "http://dummy:9000/";
const logsdir = FileUtils.getDir("ProfD", ["weave", "logs"], true);
const LOG_PREFIX_SUCCESS = "success-";
const LOG_PREFIX_ERROR = "error-";
const PROLONGED_ERROR_DURATION =
(Svc.Prefs.get('errorhandler.networkFailureReportTimeout') * 2) * 1000;
@ -1772,8 +1770,7 @@ add_task(function test_sync_engine_generic_fail() {
let entries = logsdir.directoryEntries;
do_check_true(entries.hasMoreElements());
let logfile = entries.getNext().QueryInterface(Ci.nsILocalFile);
do_check_eq(logfile.leafName.slice(0, LOG_PREFIX_ERROR.length),
LOG_PREFIX_ERROR);
do_check_true(logfile.leafName.startsWith("sync-error-"), logfile.leafName);
clean();
server.stop(deferred.resolve);
@ -1804,8 +1801,7 @@ add_test(function test_logs_on_sync_error_despite_shouldReportError() {
let entries = logsdir.directoryEntries;
do_check_true(entries.hasMoreElements());
let logfile = entries.getNext().QueryInterface(Ci.nsILocalFile);
do_check_eq(logfile.leafName.slice(0, LOG_PREFIX_ERROR.length),
LOG_PREFIX_ERROR);
do_check_true(logfile.leafName.startsWith("sync-error-"), logfile.leafName);
clean();
run_next_test();
@ -1832,8 +1828,7 @@ add_test(function test_logs_on_login_error_despite_shouldReportError() {
let entries = logsdir.directoryEntries;
do_check_true(entries.hasMoreElements());
let logfile = entries.getNext().QueryInterface(Ci.nsILocalFile);
do_check_eq(logfile.leafName.slice(0, LOG_PREFIX_ERROR.length),
LOG_PREFIX_ERROR);
do_check_true(logfile.leafName.startsWith("sync-error-"), logfile.leafName);
clean();
run_next_test();
@ -1867,8 +1862,7 @@ add_task(function test_engine_applyFailed() {
let entries = logsdir.directoryEntries;
do_check_true(entries.hasMoreElements());
let logfile = entries.getNext().QueryInterface(Ci.nsILocalFile);
do_check_eq(logfile.leafName.slice(0, LOG_PREFIX_ERROR.length),
LOG_PREFIX_ERROR);
do_check_true(logfile.leafName.startsWith("sync-error-"), logfile.leafName);
clean();
server.stop(deferred.resolve);

View File

@ -10,8 +10,6 @@ Cu.import("resource://gre/modules/NetUtil.jsm");
Cu.import("resource://gre/modules/Services.jsm");
const logsdir = FileUtils.getDir("ProfD", ["weave", "logs"], true);
const LOG_PREFIX_SUCCESS = "success-";
const LOG_PREFIX_ERROR = "error-";
// Delay to wait before cleanup, to allow files to age.
// This is so large because the file timestamp granularity is per-second, and
@ -32,6 +30,7 @@ function setLastSync(lastSyncValue) {
function run_test() {
initTestLogging("Trace");
Log.repository.getLogger("Sync.LogManager").level = Log.Level.Trace;
Log.repository.getLogger("Sync.Service").level = Log.Level.Trace;
Log.repository.getLogger("Sync.SyncScheduler").level = Log.Level.Trace;
Log.repository.getLogger("Sync.ErrorHandler").level = Log.Level.Trace;
@ -41,7 +40,7 @@ function run_test() {
add_test(function test_noOutput() {
// Ensure that the log appender won't print anything.
errorHandler._logAppender.level = Log.Level.Fatal + 1;
errorHandler._logManager._fileAppender.level = Log.Level.Fatal + 1;
// Clear log output from startup.
Svc.Prefs.set("log.appender.file.logOnSuccess", false);
@ -53,7 +52,7 @@ add_test(function test_noOutput() {
Svc.Obs.add("weave:service:reset-file-log", function onResetFileLog() {
Svc.Obs.remove("weave:service:reset-file-log", onResetFileLog);
errorHandler._logAppender.level = Log.Level.Trace;
errorHandler._logManager._fileAppender.level = Log.Level.Trace;
Svc.Prefs.resetBranch("");
run_next_test();
});
@ -109,8 +108,7 @@ add_test(function test_logOnSuccess_true() {
do_check_true(entries.hasMoreElements());
let logfile = entries.getNext().QueryInterface(Ci.nsILocalFile);
do_check_eq(logfile.leafName.slice(-4), ".txt");
do_check_eq(logfile.leafName.slice(0, LOG_PREFIX_SUCCESS.length),
LOG_PREFIX_SUCCESS);
do_check_true(logfile.leafName.startsWith("sync-success-"), logfile.leafName);
do_check_false(entries.hasMoreElements());
// Ensure the log message was actually written to file.
@ -162,6 +160,13 @@ add_test(function test_sync_error_logOnError_true() {
const MESSAGE = "this WILL show up";
log.info(MESSAGE);
// We need to wait until the log cleanup started by this test is complete
// or the next test will fail as it is ongoing.
Svc.Obs.add("services-tests:common:log-manager:cleanup-logs", function onCleanupLogs() {
Svc.Obs.remove("services-tests:common:log-manager:cleanup-logs", onCleanupLogs);
run_next_test();
});
Svc.Obs.add("weave:service:reset-file-log", function onResetFileLog() {
Svc.Obs.remove("weave:service:reset-file-log", onResetFileLog);
@ -170,8 +175,7 @@ add_test(function test_sync_error_logOnError_true() {
do_check_true(entries.hasMoreElements());
let logfile = entries.getNext().QueryInterface(Ci.nsILocalFile);
do_check_eq(logfile.leafName.slice(-4), ".txt");
do_check_eq(logfile.leafName.slice(0, LOG_PREFIX_ERROR.length),
LOG_PREFIX_ERROR);
do_check_true(logfile.leafName.startsWith("sync-error-"), logfile.leafName);
do_check_false(entries.hasMoreElements());
// Ensure the log message was actually written to file.
@ -188,7 +192,6 @@ add_test(function test_sync_error_logOnError_true() {
}
Svc.Prefs.resetBranch("");
run_next_test();
});
});
@ -224,6 +227,13 @@ add_test(function test_login_error_logOnError_true() {
const MESSAGE = "this WILL show up";
log.info(MESSAGE);
// We need to wait until the log cleanup started by this test is complete
// or the next test will fail as it is ongoing.
Svc.Obs.add("services-tests:common:log-manager:cleanup-logs", function onCleanupLogs() {
Svc.Obs.remove("services-tests:common:log-manager:cleanup-logs", onCleanupLogs);
run_next_test();
});
Svc.Obs.add("weave:service:reset-file-log", function onResetFileLog() {
Svc.Obs.remove("weave:service:reset-file-log", onResetFileLog);
@ -232,8 +242,7 @@ add_test(function test_login_error_logOnError_true() {
do_check_true(entries.hasMoreElements());
let logfile = entries.getNext().QueryInterface(Ci.nsILocalFile);
do_check_eq(logfile.leafName.slice(-4), ".txt");
do_check_eq(logfile.leafName.slice(0, LOG_PREFIX_ERROR.length),
LOG_PREFIX_ERROR);
do_check_true(logfile.leafName.startsWith("sync-error-"), logfile.leafName);
do_check_false(entries.hasMoreElements());
// Ensure the log message was actually written to file.
@ -250,7 +259,6 @@ add_test(function test_login_error_logOnError_true() {
}
Svc.Prefs.resetBranch("");
run_next_test();
});
});
@ -273,7 +281,7 @@ add_test(function test_logErrorCleanup_age() {
_("Making some files.");
for (let i = 0; i < numLogs; i++) {
let now = Date.now();
let filename = LOG_PREFIX_ERROR + now + "" + i + ".txt";
let filename = "sync-error-" + now + "" + i + ".txt";
let newLog = FileUtils.getFile("ProfD", ["weave", "logs", filename]);
let foStream = FileUtils.openFileOutputStream(newLog);
foStream.write(errString, errString.length);
@ -282,8 +290,8 @@ add_test(function test_logErrorCleanup_age() {
oldLogs.push(newLog.leafName);
}
Svc.Obs.add("weave:service:cleanup-logs", function onCleanupLogs() {
Svc.Obs.remove("weave:service:cleanup-logs", onCleanupLogs);
Svc.Obs.add("services-tests:common:log-manager:cleanup-logs", function onCleanupLogs() {
Svc.Obs.remove("services-tests:common:log-manager:cleanup-logs", onCleanupLogs);
// Only the newest created log file remains.
let entries = logsdir.directoryEntries;

View File

@ -1319,7 +1319,7 @@ BoxModelHighlighter.prototype = Heritage.extend(AutoRefreshHighlighter.prototype
}
let rect = this.currentQuads.border.bounds;
let dim = Math.ceil(rect.width) + " \u00D7 " + Math.ceil(rect.height);
let dim = parseFloat(rect.width.toPrecision(6)) + " \u00D7 " + parseFloat(rect.height.toPrecision(6));
let elementId = this.ID_CLASS_PREFIX + "nodeinfobar-";
this.markup.setTextContentForElement(elementId + "tagname", tagName);

View File

@ -43,7 +43,12 @@ types.addDictType("AllocationsRecordingOptions", {
// The probability we sample any given allocation when recording
// allocations. Must be between 0.0 and 1.0. Defaults to 1.0, or sampling
// every allocation.
probability: "number"
probability: "number",
// The maximum number of of allocation events to keep in the allocations
// log. If new allocations arrive, when we are already at capacity, the oldest
// allocation event is lost. This number must fit in a 32 bit signed integer.
maxLogLength: "number"
});
/**
@ -164,6 +169,9 @@ let MemoryActor = protocol.ActorClass({
this.dbg.memory.allocationSamplingProbability = options.probability != null
? options.probability
: 1.0;
if (options.maxLogLength != null) {
this.dbg.memory.maxAllocationsLogLength = options.maxLogLength;
}
this.dbg.memory.trackingAllocationSites = true;
return Date.now();
@ -253,6 +261,15 @@ let MemoryActor = protocol.ActorClass({
* profiling and done only when necessary.
*/
getAllocations: method(expectState("attached", function() {
if (this.dbg.memory.allocationsLogOverflowed) {
// Since the last time we drained the allocations log, there have been
// more allocations than the log's capacity, and we lost some data. There
// isn't anything actionable we can do about this, but put a message in
// the browser console so we at least know that it occurred.
reportException("MemoryActor.prototype.getAllocations",
"Warning: allocations log overflowed and lost some data.");
}
const allocations = this.dbg.memory.drainAllocationsLog()
const packet = {
allocations: [],

View File

@ -747,8 +747,8 @@ var PageStyleActor = protocol.ActorClass({
// the size of the element.
let clientRect = node.rawNode.getBoundingClientRect();
layout.width = Math.ceil(clientRect.width);
layout.height = Math.ceil(clientRect.height);
layout.width = parseFloat(clientRect.width.toPrecision(6));
layout.height = parseFloat(clientRect.height.toPrecision(6));
// We compute and update the values of margins & co.
let style = CssLogic.getComputedStyle(node.rawNode);
@ -776,7 +776,7 @@ var PageStyleActor = protocol.ActorClass({
for (let i in this.map) {
let property = this.map[i].property;
this.map[i].value = parseInt(style.getPropertyValue(property));
this.map[i].value = parseFloat(style.getPropertyValue(property));
}

View File

@ -67,6 +67,7 @@ skip-if = buildapp == 'mulet'
[test_memory_allocations_03.html]
[test_memory_allocations_04.html]
[test_memory_allocations_05.html]
[test_memory_allocations_06.html]
[test_memory_attach_01.html]
[test_memory_attach_02.html]
[test_memory_census.html]

View File

@ -0,0 +1,49 @@
<!DOCTYPE HTML>
<html>
<!--
Bug 1132764 - Test controlling the maximum allocations log length over the RDP.
-->
<head>
<meta charset="utf-8">
<title>Memory monitoring actor test</title>
<script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
</head>
<body>
<pre id="test">
<script src="memory-helpers.js" type="application/javascript;version=1.8"></script>
<script>
window.onload = function() {
SimpleTest.waitForExplicitFinish();
Task.spawn(function* () {
var { memory, client } = yield startServerAndGetSelectedTabMemory();
yield memory.attach();
var allocs = [];
function allocator() {
allocs.push(new Object);
}
yield memory.startRecordingAllocations({
maxLogLength: 1
});
allocator();
allocator();
allocator();
var response = yield memory.getAllocations();
yield memory.stopRecordingAllocations();
is(response.allocations.length, 1,
"There should only be one entry in the allocations log.");
yield memory.detach();
destroyServerAndFinish(client);
});
};
</script>
</pre>
</body>
</html>

View File

@ -548,6 +548,7 @@ xul|*#categories[keyboard-navigation="true"]:-moz-focusring > xul|*.category[cur
padding-bottom: 2px;
-moz-padding-start: 9px;
margin: 0;
-moz-user-select: none;
}
*|*.category-icon {
@ -569,6 +570,7 @@ xul|*#categories[keyboard-navigation="true"]:-moz-focusring > xul|*.category[cur
font-weight: normal;
line-height: 40px;
margin: 0;
-moz-user-select: none;
}
/* File fields */