Merge fx-team to m-c

This commit is contained in:
Wes Kocher 2014-03-28 16:48:17 -07:00
commit 7846a4d1f4
109 changed files with 2136 additions and 1143 deletions

View File

@ -1417,3 +1417,5 @@ pref("experiments.manifest.fetchIntervalSeconds", 86400);
pref("experiments.manifest.uri", "https://telemetry-experiment.cdn.mozilla.net/manifest/v1/firefox/%VERSION%/%CHANNEL%");
pref("experiments.manifest.certs.1.commonName", "*.cdn.mozilla.net");
pref("experiments.manifest.certs.1.issuerName", "CN=Cybertrust Public SureServer SV CA,O=Cybertrust Inc");
// Whether experiments are supported by the current application profile.
pref("experiments.supported", true);

View File

@ -1382,10 +1382,6 @@ let BookmarkingUI = {
dropmarkerAnimationNode.style.listStyleImage = dropmarkerStyle.listStyleImage;
}
let isInBookmarksToolbar = this.button.classList.contains("bookmark-item");
if (isInBookmarksToolbar)
this.notifier.setAttribute("in-bookmarks-toolbar", true);
let isInOverflowPanel = this.button.getAttribute("overflowedItem") == "true";
if (!isInOverflowPanel) {
this.notifier.setAttribute("notification", "finish");
@ -1394,7 +1390,6 @@ let BookmarkingUI = {
}
this._notificationTimeout = setTimeout( () => {
this.notifier.removeAttribute("in-bookmarks-toolbar");
this.notifier.removeAttribute("notification");
this.dropmarkerNotifier.removeAttribute("notification");
this.button.removeAttribute("notification");

View File

@ -31,7 +31,7 @@
label="&newtab.undo.restoreButton;"
class="newtab-undo-button" />
<xul:toolbarbutton id="newtab-undo-close-button" tabindex="-1"
class="close-icon"
class="close-icon tabbable"
tooltiptext="&newtab.undo.closeTooltip;" />
</div>
</div>

View File

@ -10,8 +10,8 @@
# instead of BrandFullName and typically should not be modified.
!define BrandFullNameInternal "Aurora"
!define CompanyName "mozilla.org"
!define URLInfoAbout "http://www.mozilla.org"
!define URLUpdateInfo "http://www.mozilla.org/projects/firefox"
!define URLInfoAbout "https://www.mozilla.org"
!define HelpLink "https://support.mozilla.org"
!define URLStubDownload "http://download.mozilla.org/?os=win&lang=${AB_CD}&product=firefox-aurora-latest"
!define URLManualDownload "https://www.mozilla.org/${AB_CD}/firefox/installer-help/?channel=aurora&installer_lang=${AB_CD}"

View File

@ -10,8 +10,8 @@
# instead of BrandFullName and typically should not be modified.
!define BrandFullNameInternal "Nightly"
!define CompanyName "mozilla.org"
!define URLInfoAbout "http://www.mozilla.org"
!define URLUpdateInfo "http://www.mozilla.org/projects/firefox"
!define URLInfoAbout "https://www.mozilla.org"
!define HelpLink "https://support.mozilla.org"
!define URLStubDownload "http://download.mozilla.org/?os=win&lang=${AB_CD}&product=firefox-nightly-latest"
!define URLManualDownload "https://www.mozilla.org/${AB_CD}/firefox/installer-help/?channel=nightly&installer_lang=${AB_CD}"

View File

@ -10,8 +10,9 @@
# instead of BrandFullName and typically should not be modified.
!define BrandFullNameInternal "Mozilla Firefox"
!define CompanyName "Mozilla Corporation"
!define URLInfoAbout "https://www.mozilla.org/${AB_CD}/"
!define URLUpdateInfo "https://www.mozilla.org/${AB_CD}/firefox/"
!define URLInfoAbout "https://www.mozilla.org"
!define URLUpdateInfo "https://www.mozilla.org/firefox/${AppVersion}/releasenotes"
!define HelpLink "https://support.mozilla.org"
; The OFFICIAL define is a workaround to support different urls for Release and
; Beta since they share the same branding when building with other branches that

View File

@ -10,8 +10,8 @@
# instead of BrandFullName and typically should not be modified.
!define BrandFullNameInternal "Mozilla Developer Preview"
!define CompanyName "mozilla.org"
!define URLInfoAbout "http://www.mozilla.org"
!define URLUpdateInfo "http://www.mozilla.org/projects/firefox"
!define URLInfoAbout "https://www.mozilla.org"
!define HelpLink "https://support.mozilla.org"
!define URLStubDownload "http://download.mozilla.org/?os=win&lang=${AB_CD}&product=firefox-latest"
!define URLManualDownload "https://www.mozilla.org/${AB_CD}/firefox/installer-help/?channel=release&installer_lang=${AB_CD}"

View File

@ -810,6 +810,8 @@ let CustomizableUIInternal = {
aWindow.addEventListener("unload", this);
aWindow.addEventListener("command", this, true);
this.notifyListeners("onWindowOpened", aWindow);
}
},
@ -850,6 +852,8 @@ let CustomizableUIInternal = {
areaMap.delete(toDelete);
}
}
this.notifyListeners("onWindowClosed", aWindow);
},
setLocationAttributes: function(aNode, aArea) {
@ -2476,6 +2480,18 @@ this.CustomizableUI = {
*/
get PANEL_COLUMN_COUNT() 3,
/**
* An iteratable property of windows managed by CustomizableUI.
* Note that this can *only* be used as an iterator. ie:
* for (let window of CustomizableUI.windows) { ... }
*/
windows: {
"@@iterator": function*() {
for (let [window,] of gBuildWindows)
yield window;
}
},
/**
* Add a listener object that will get fired for various events regarding
* customization.
@ -2559,6 +2575,12 @@ this.CustomizableUI = {
* - onWidgetUnderflow(aNode, aContainer)
* Fired when a widget's DOM node is *not* overflowing its container, a
* toolbar, anymore.
* - onWindowOpened(aWindow)
* Fired when a window has been opened that is managed by CustomizableUI,
* once all of the prerequisite setup has been done.
* - onWindowClosed(aWindow)
* Fired when a window that has been managed by CustomizableUI has been
* closed.
*/
addListener: function(aListener) {
CustomizableUIInternal.addListener(aListener);
@ -3274,7 +3296,7 @@ this.CustomizableUI = {
}
};
Object.freeze(this.CustomizableUI);
Object.freeze(this.CustomizableUI.windows);
/**
* All external consumers of widgets are really interacting with these wrappers

View File

@ -93,6 +93,55 @@ function addShortcut(aNode, aDocument, aItem) {
aItem.setAttribute("shortcut", ShortcutUtils.prettifyShortcut(shortcut));
}
function fillSubviewFromMenuItems(aMenuItems, aSubview) {
let attrs = ["oncommand", "onclick", "label", "key", "disabled",
"command", "observes", "hidden", "class", "origin",
"image", "checked"];
let doc = aSubview.ownerDocument;
let fragment = doc.createDocumentFragment();
for (let menuChild of aMenuItems) {
if (menuChild.hidden)
continue;
let subviewItem;
if (menuChild.localName == "menuseparator") {
// Don't insert duplicate or leading separators. This can happen if there are
// menus (which we don't copy) above the separator.
if (!fragment.lastChild || fragment.lastChild.localName == "menuseparator") {
continue;
}
subviewItem = doc.createElementNS(kNSXUL, "menuseparator");
} else if (menuChild.localName == "menuitem") {
subviewItem = doc.createElementNS(kNSXUL, "toolbarbutton");
subviewItem.setAttribute("class", "subviewbutton");
addShortcut(menuChild, doc, subviewItem);
} else {
continue;
}
for (let attr of attrs) {
let attrVal = menuChild.getAttribute(attr);
if (attrVal)
subviewItem.setAttribute(attr, attrVal);
}
fragment.appendChild(subviewItem);
}
aSubview.appendChild(fragment);
}
function clearSubview(aSubview) {
let parent = aSubview.parentNode;
// We'll take the container out of the document before cleaning it out
// to avoid reflowing each time we remove something.
parent.removeChild(aSubview);
while (aSubview.firstChild) {
aSubview.firstChild.remove();
}
parent.appendChild(aSubview);
}
const CustomizableWidgets = [{
id: "history-panelmenu",
type: "view",
@ -284,59 +333,18 @@ const CustomizableWidgets = [{
let doc = aEvent.target.ownerDocument;
let win = doc.defaultView;
let items = doc.getElementById("PanelUI-developerItems");
let menu = doc.getElementById("menuWebDeveloperPopup");
let attrs = ["oncommand", "onclick", "label", "key", "disabled",
"command", "observes"];
let fragment = doc.createDocumentFragment();
let itemsToDisplay = [...menu.children];
// Hardcode the addition of the "work offline" menuitem at the bottom:
itemsToDisplay.push({localName: "menuseparator", getAttribute: () => {}});
itemsToDisplay.push(doc.getElementById("goOfflineMenuitem"));
for (let node of itemsToDisplay) {
if (node.hidden)
continue;
let item;
if (node.localName == "menuseparator") {
// Don't insert duplicate or leading separators. This can happen if there are
// menus (which we don't copy) above the separator.
if (!fragment.lastChild || fragment.lastChild.localName == "menuseparator") {
continue;
}
item = doc.createElementNS(kNSXUL, "menuseparator");
} else if (node.localName == "menuitem") {
item = doc.createElementNS(kNSXUL, "toolbarbutton");
item.setAttribute("class", "subviewbutton");
addShortcut(node, doc, item);
} else {
continue;
}
for (let attr of attrs) {
let attrVal = node.getAttribute(attr);
if (attrVal)
item.setAttribute(attr, attrVal);
}
fragment.appendChild(item);
}
items.appendChild(fragment);
fillSubviewFromMenuItems(itemsToDisplay, doc.getElementById("PanelUI-developerItems"));
},
onViewHiding: function(aEvent) {
let doc = aEvent.target.ownerDocument;
let win = doc.defaultView;
let items = doc.getElementById("PanelUI-developerItems");
let parent = items.parentNode;
// We'll take the container out of the document before cleaning it out
// to avoid reflowing each time we remove something.
parent.removeChild(items);
while (items.firstChild) {
items.firstChild.remove();
}
parent.appendChild(items);
clearSubview(doc.getElementById("PanelUI-developerItems"));
}
}, {
id: "sidebar-button",
@ -350,8 +358,6 @@ const CustomizableWidgets = [{
// of dealing with those right now.
let doc = aEvent.target.ownerDocument;
let win = doc.defaultView;
let items = doc.getElementById("PanelUI-sidebarItems");
let menu = doc.getElementById("viewSidebarMenu");
// First clear any existing menuitems then populate. Social sidebar
@ -362,51 +368,11 @@ const CustomizableWidgets = [{
if (providerMenuSeps.length > 0)
win.SocialSidebar.populateProviderMenu(providerMenuSeps[0]);
let attrs = ["oncommand", "onclick", "label", "key", "disabled",
"command", "observes", "hidden", "class", "origin",
"image", "checked"];
let fragment = doc.createDocumentFragment();
let itemsToDisplay = [...menu.children];
for (let node of itemsToDisplay) {
if (node.hidden)
continue;
let item;
if (node.localName == "menuseparator") {
item = doc.createElementNS(kNSXUL, "menuseparator");
} else if (node.localName == "menuitem") {
item = doc.createElementNS(kNSXUL, "toolbarbutton");
} else {
continue;
}
for (let attr of attrs) {
let attrVal = node.getAttribute(attr);
if (attrVal)
item.setAttribute(attr, attrVal);
}
if (node.localName == "menuitem") {
item.classList.add("subviewbutton");
addShortcut(node, doc, item);
}
fragment.appendChild(item);
}
items.appendChild(fragment);
fillSubviewFromMenuItems([...menu.children], doc.getElementById("PanelUI-sidebarItems"));
},
onViewHiding: function(aEvent) {
let doc = aEvent.target.ownerDocument;
let items = doc.getElementById("PanelUI-sidebarItems");
let parent = items.parentNode;
// We'll take the container out of the document before cleaning it out
// to avoid reflowing each time we remove something.
parent.removeChild(items);
while (items.firstChild) {
items.firstChild.remove();
}
parent.appendChild(items);
clearSubview(doc.getElementById("PanelUI-sidebarItems"));
}
}, {
id: "add-ons-button",

View File

@ -93,4 +93,5 @@ skip-if = os == "linux"
[browser_981305_separator_insertion.js]
[browser_987177_destroyWidget_xul.js]
[browser_987177_xul_wrapper_updating.js]
[browser_987492_window_api.js]
[browser_panel_toggle.js]

View File

@ -5,43 +5,65 @@
"use strict";
let tempElements = [];
// Shouldn't insert multiple separators into the developer tools subview
add_task(function testMultipleDevtoolsSeparators() {
let devtoolsSubMenu = document.getElementById("menuWebDeveloperPopup");
function insertTempItemsIntoMenu(parentMenu) {
// Last element is null to insert at the end:
let beforeEls = [devtoolsSubMenu.firstChild, devtoolsSubMenu.lastChild, null];
let beforeEls = [parentMenu.firstChild, parentMenu.lastChild, null];
for (let i = 0; i < beforeEls.length; i++) {
let sep = document.createElement("menuseparator");
tempElements.push(sep);
devtoolsSubMenu.insertBefore(sep, beforeEls[i]);
parentMenu.insertBefore(sep, beforeEls[i]);
let menu = document.createElement("menu");
tempElements.push(menu);
devtoolsSubMenu.insertBefore(menu, beforeEls[i]);
parentMenu.insertBefore(menu, beforeEls[i]);
// And another separator for good measure:
sep = document.createElement("menuseparator");
tempElements.push(sep);
devtoolsSubMenu.insertBefore(sep, beforeEls[i]);
parentMenu.insertBefore(sep, beforeEls[i]);
}
yield PanelUI.show();
}
let devtoolsButton = document.getElementById("developer-button");
devtoolsButton.click();
yield waitForCondition(() => !PanelUI.multiView.hasAttribute("transitioning"));
let subview = document.getElementById("PanelUI-developerItems");
ok(subview.firstChild, "Subview should have a kid");
is(subview.firstChild.localName, "toolbarbutton", "There should be no separators to start with");
function checkSeparatorInsertion(menuId, buttonId, subviewId) {
return function() {
info("Checking for duplicate separators in " + buttonId + " widget");
let menu = document.getElementById(menuId);
insertTempItemsIntoMenu(menu);
for (let kid of subview.children) {
if (kid.localName == "menuseparator") {
ok(kid.previousSibling && kid.previousSibling.localName != "menuseparator",
"Separators should never have another separator next to them, and should never be the first node.");
let placement = CustomizableUI.getPlacementOfWidget(buttonId);
let changedPlacement = false;
if (!placement || placement.area != CustomizableUI.AREA_PANEL) {
CustomizableUI.addWidgetToArea(buttonId, CustomizableUI.AREA_PANEL);
changedPlacement = true;
}
}
yield PanelUI.show();
let panelHiddenPromise = promisePanelHidden(window);
PanelUI.hide();
yield panelHiddenPromise;
});
let button = document.getElementById(buttonId);
button.click();
yield waitForCondition(() => !PanelUI.multiView.hasAttribute("transitioning"));
let subview = document.getElementById(subviewId);
ok(subview.firstChild, "Subview should have a kid");
is(subview.firstChild.localName, "toolbarbutton", "There should be no separators to start with");
for (let kid of subview.children) {
if (kid.localName == "menuseparator") {
ok(kid.previousSibling && kid.previousSibling.localName != "menuseparator",
"Separators should never have another separator next to them, and should never be the first node.");
}
}
let panelHiddenPromise = promisePanelHidden(window);
PanelUI.hide();
yield panelHiddenPromise;
if (changedPlacement) {
CustomizableUI.reset();
}
};
}
add_task(checkSeparatorInsertion("menuWebDeveloperPopup", "developer-button", "PanelUI-developerItems"));
add_task(checkSeparatorInsertion("viewSidebarMenu", "sidebar-button", "PanelUI-sidebarItems"));
registerCleanupFunction(function() {
for (let el of tempElements) {

View File

@ -0,0 +1,54 @@
/* 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/. */
"use strict";
add_task(function* testOneWindow() {
let windows = [];
for (let win of CustomizableUI.windows)
windows.push(win);
is(windows.length, 1, "Should have one customizable window");
});
add_task(function* testOpenCloseWindow() {
let newWindow = null;
let openListener = {
onWindowOpened: function(window) {
newWindow = window;
}
}
CustomizableUI.addListener(openListener);
let win = yield openAndLoadWindow(null, true);
isnot(newWindow, null, "Should have gotten onWindowOpen event");
is(newWindow, win, "onWindowOpen event should have received expected window");
CustomizableUI.removeListener(openListener);
let windows = [];
for (let win of CustomizableUI.windows)
windows.push(win);
is(windows.length, 2, "Should have two customizable windows");
isnot(windows.indexOf(window), -1, "Current window should be in window collection.");
isnot(windows.indexOf(newWindow), -1, "New window should be in window collection.");
let closedWindow = null;
let closeListener = {
onWindowClosed: function(window) {
closedWindow = window;
}
}
CustomizableUI.addListener(closeListener);
yield promiseWindowClosed(newWindow);
isnot(closedWindow, null, "Should have gotten onWindowClosed event")
is(newWindow, closedWindow, "Closed window should match previously opened window");
CustomizableUI.removeListener(closeListener);
let windows = [];
for (let win of CustomizableUI.windows)
windows.push(win);
is(windows.length, 1, "Should have one customizable window");
isnot(windows.indexOf(window), -1, "Current window should be in window collection.");
is(windows.indexOf(closedWindow), -1, "Closed window should not be in window collection.");
});

View File

@ -85,6 +85,7 @@ support-files =
[browser_dbg_addonactor.js]
[browser_dbg_addon-sources.js]
[browser_dbg_addon-modules.js]
[browser_dbg_addon-panels.js]
[browser_dbg_auto-pretty-print-01.js]
[browser_dbg_auto-pretty-print-02.js]
[browser_dbg_bfcache.js]

View File

@ -0,0 +1,80 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
// Ensure that only panels that are relevant to the addon debugger
// display in the toolbox
const ADDON3_URL = EXAMPLE_URL + "addon3.xpi";
let gAddon, gClient, gThreadClient, gDebugger, gSources;
function test() {
Task.spawn(function () {
if (!DebuggerServer.initialized) {
DebuggerServer.init(() => true);
DebuggerServer.addBrowserActors();
}
gBrowser.selectedTab = gBrowser.addTab();
let iframe = document.createElement("iframe");
document.documentElement.appendChild(iframe);
let transport = DebuggerServer.connectPipe();
gClient = new DebuggerClient(transport);
let connected = promise.defer();
gClient.connect(connected.resolve);
yield connected.promise;
yield installAddon();
let debuggerPanel = yield initAddonDebugger(gClient, ADDON3_URL, iframe);
gDebugger = debuggerPanel.panelWin;
gThreadClient = gDebugger.gThreadClient;
gSources = gDebugger.DebuggerView.Sources;
testPanels(iframe);
yield uninstallAddon();
yield closeConnection();
yield debuggerPanel._toolbox.destroy();
iframe.remove();
finish();
});
}
function installAddon () {
return addAddon(ADDON3_URL).then(aAddon => {
gAddon = aAddon;
});
}
function testPanels(frame) {
let tabs = frame.contentDocument.getElementById("toolbox-tabs").children;
let expectedTabs = ["options", "jsdebugger"];
is(tabs.length, 2, "displaying only 2 tabs in addon debugger");
Array.forEach(tabs, (tab, i) => {
let toolName = expectedTabs[i];
is(tab.getAttribute("toolid"), toolName, "displaying " + toolName);
});
}
function uninstallAddon() {
return removeAddon(gAddon);
}
function closeConnection () {
let deferred = promise.defer();
gClient.close(deferred.resolve);
return deferred.promise;
}
registerCleanupFunction(function() {
gClient = null;
gAddon = null;
gThreadClient = null;
gDebugger = null;
gSources = null;
while (gBrowser.tabs.length > 1) {
gBrowser.removeCurrentTab();
}
});

View File

@ -242,6 +242,10 @@ TabTarget.prototype = {
return !this.isLocalTab;
},
get isAddon() {
return !!(this._form && this._form.addonActor);
},
get isLocalTab() {
return !!this._tab;
},

View File

@ -88,8 +88,8 @@
label="&options.disableJavaScript.label;"
tooltiptext="&options.disableJavaScript.tooltip;"/>
<hbox class="hidden-labels-box">
<checkbox label="&options.enableChrome.label3;"
tooltiptext="&options.enableChrome.tooltip;"
<checkbox label="&options.enableChrome.label4;"
tooltiptext="&options.enableChrome.tooltip2;"
data-pref="devtools.chrome.enabled"/>
</hbox>
<hbox class="hidden-labels-box">

View File

@ -97,7 +97,7 @@ Tools.webConsole = {
},
isTargetSupported: function(target) {
return true;
return !target.isAddon;
},
build: function(iframeWindow, toolbox) {
let panel = new WebConsolePanel(iframeWindow, toolbox);
@ -124,7 +124,7 @@ Tools.inspector = {
},
isTargetSupported: function(target) {
return true;
return !target.isAddon;
},
build: function(iframeWindow, toolbox) {
@ -171,7 +171,7 @@ Tools.styleEditor = {
inMenu: true,
isTargetSupported: function(target) {
return true;
return !target.isAddon;
},
build: function(iframeWindow, toolbox) {
@ -191,7 +191,7 @@ Tools.shaderEditor = {
tooltip: l10n("ToolboxShaderEditor.tooltip", shaderEditorStrings),
isTargetSupported: function(target) {
return true;
return !target.isAddon;
},
build: function(iframeWindow, toolbox) {
@ -215,7 +215,7 @@ Tools.jsprofiler = {
inMenu: true,
isTargetSupported: function (target) {
return true;
return !target.isAddon;
},
build: function (frame, target) {
@ -240,7 +240,7 @@ Tools.netMonitor = {
isTargetSupported: function(target) {
let root = target.client.mainRoot;
return root.traits.networkMonitor || !target.isApp;
return !target.isAddon && (root.traits.networkMonitor || !target.isApp);
},
build: function(iframeWindow, toolbox) {
@ -261,7 +261,7 @@ Tools.scratchpad = {
inMenu: false,
isTargetSupported: function(target) {
return target.isRemote;
return !target.isAddon && target.isRemote;
},
build: function(iframeWindow, toolbox) {

View File

@ -46,11 +46,13 @@ function setupAutoCompletion(ctx, walker) {
if (popup && popup.isOpen) {
if (!privates.get(ed).suggestionInsertedOnce) {
privates.get(ed).insertingSuggestion = true;
let {label, preLabel} = popup.getItemAtIndex(0);
let {label, preLabel, text} = popup.getItemAtIndex(0);
let cur = ed.getCursor();
ed.replaceText(label.slice(preLabel.length), cur, cur);
ed.replaceText(text.slice(preLabel.length), cur, cur);
}
popup.hidePopup();
// This event is used in tests
ed.emit("popup-hidden");
return;
}
@ -135,17 +137,17 @@ function cycleSuggestions(ed, reverse) {
}
if (popup.itemCount == 1)
popup.hidePopup();
ed.replaceText(firstItem.label.slice(firstItem.preLabel.length), cur, cur);
ed.replaceText(firstItem.text.slice(firstItem.preLabel.length), cur, cur);
} else {
let fromCur = {
line: cur.line,
ch : cur.ch - popup.selectedItem.label.length
ch : cur.ch - popup.selectedItem.text.length
};
if (reverse)
popup.selectPreviousItem();
else
popup.selectNextItem();
ed.replaceText(popup.selectedItem.label, fromCur, cur);
ed.replaceText(popup.selectedItem.text, fromCur, cur);
}
// This event is used in tests.
ed.emit("suggestion-entered");

View File

@ -126,12 +126,14 @@ CSSCompleter.prototype = {
if ("media".startsWith(this.completing)) {
return Promise.resolve([{
label: "media",
preLabel: this.completing
preLabel: this.completing,
text: "media"
}]);
} else if ("keyframes".startsWith(this.completing)) {
return Promise.resolve([{
label: "keyrames",
preLabel: this.completing
label: "keyframes",
preLabel: this.completing,
text: "keyframes"
}]);
}
}
@ -785,6 +787,7 @@ CSSCompleter.prototype = {
completion.push({
label: value[0],
preLabel: query,
text: value[0],
score: value[1]
});
if (completion.length > this.maxEntries - 1)
@ -808,9 +811,11 @@ CSSCompleter.prototype = {
for (; i < length && count < this.maxEntries; i++) {
if (propertyNames[i].startsWith(startProp)) {
count++;
let propName = propertyNames[i];
finalList.push({
preLabel: startProp,
label: propertyNames[i]
label: propName,
text: propName + ": "
});
} else if (propertyNames[i] > startProp) {
// We have crossed all possible matches alphabetically.
@ -840,9 +845,11 @@ CSSCompleter.prototype = {
for (; i < length && count < this.maxEntries; i++) {
if (list[i].startsWith(startValue)) {
count++;
let value = list[i];
finalList.push({
preLabel: startValue,
label: list[i]
label: value,
text: value
});
} else if (list[i] > startValue) {
// We have crossed all possible matches alphabetically.

View File

@ -13,60 +13,71 @@ const {CSSProperties, CSSValues} = getCSSKeywords();
// Test cases to test that autocompletion works correctly when enabled.
// Format:
// [
// -1 for pressing Ctrl + Space or the particular key to press,
// Number of suggestions in the popup (-1 if popup is closed),
// Index of selected suggestion,
// 1 to check whether the selected suggestion is inserted into the editor or not
// key,
// {
// total: Number of suggestions in the popup (-1 if popup is closed),
// current: Index of selected suggestion,
// inserted: 1 to check whether the selected suggestion is inserted into the editor or not,
// entered: 1 if the suggestion is inserted and finalized
// }
// ]
let TEST_CASES = [
['VK_RIGHT', -1],
['VK_RIGHT', -1],
['VK_RIGHT', -1],
['VK_RIGHT', -1],
[-1, 1, 0],
['VK_LEFT', -1],
['VK_RIGHT', -1],
['VK_DOWN', -1],
['VK_RIGHT', -1],
['VK_RIGHT', -1],
['VK_RIGHT', -1],
[-1, getSuggestionNumberFor("font"), 0],
['VK_END', -1],
['VK_RETURN', -1],
['b', getSuggestionNumberFor("b"), 0],
['a', getSuggestionNumberFor("ba"), 0],
['VK_DOWN', getSuggestionNumberFor("ba"), 0, 1],
['VK_TAB', getSuggestionNumberFor("ba"), 1, 1],
[':', getSuggestionNumberFor("background", ""), 0],
['b', getSuggestionNumberFor("background", "b"), 0],
['l', getSuggestionNumberFor("background", "bl"), 0],
['VK_TAB', getSuggestionNumberFor("background", "bl"), 0, 1],
['VK_DOWN', getSuggestionNumberFor("background", "bl"), 1, 1],
['VK_UP', getSuggestionNumberFor("background", "bl"), 0, 1],
['VK_TAB', getSuggestionNumberFor("background", "bl"), 1, 1],
['VK_TAB', getSuggestionNumberFor("background", "bl"), 2, 1],
['VK_LEFT', -1],
['VK_RIGHT', -1],
['VK_DOWN', -1],
['VK_RETURN', -1],
['b', 2, 0],
['u', 1, 0],
['VK_RETURN', -1, 0, 1],
['{', -1],
['VK_HOME', -1],
['VK_DOWN', -1],
['VK_DOWN', -1],
['VK_RIGHT', -1],
['VK_RIGHT', -1],
['VK_RIGHT', -1],
['VK_RIGHT', -1],
['VK_RIGHT', -1],
['VK_RIGHT', -1],
['VK_RIGHT', -1],
['VK_RIGHT', -1],
['VK_RIGHT', -1],
['VK_RIGHT', -1],
[-1, 1, 0],
['VK_RIGHT'],
['VK_RIGHT'],
['VK_RIGHT'],
['VK_RIGHT'],
['Ctrl+Space', {total: 1, current: 0}],
['VK_LEFT'],
['VK_RIGHT'],
['VK_DOWN'],
['VK_RIGHT'],
['VK_RIGHT'],
['VK_RIGHT'],
['Ctrl+Space', { total: getSuggestionNumberFor("font"), current: 0}],
['VK_END'],
['VK_RETURN'],
['b', {total: getSuggestionNumberFor("b"), current: 0}],
['a', {total: getSuggestionNumberFor("ba"), current: 0}],
['VK_DOWN', {total: getSuggestionNumberFor("ba"), current: 0, inserted: 1}],
['VK_TAB', {total: getSuggestionNumberFor("ba"), current: 1, inserted: 1}],
['VK_RETURN', {current: 1, inserted: 1, entered: 1}],
['b', {total: getSuggestionNumberFor("background", "b"), current: 0}],
['l', {total: getSuggestionNumberFor("background", "bl"), current: 0}],
['VK_TAB', {total: getSuggestionNumberFor("background", "bl"), current: 0, inserted: 1}],
['VK_DOWN', {total: getSuggestionNumberFor("background", "bl"), current: 1, inserted: 1}],
['VK_UP', {total: getSuggestionNumberFor("background", "bl"), current: 0, inserted: 1}],
['VK_TAB', {total: getSuggestionNumberFor("background", "bl"), current: 1, inserted: 1}],
['VK_TAB', {total: getSuggestionNumberFor("background", "bl"), current: 2, inserted: 1}],
[';'],
['VK_RETURN'],
['c', {total: getSuggestionNumberFor("c"), current: 0}],
['o', {total: getSuggestionNumberFor("co"), current: 0}],
['VK_RETURN', {current: 0, inserted: 1}],
['r', {total: getSuggestionNumberFor("color", "r"), current: 0}],
['VK_RETURN', {current: 0, inserted: 1}],
[';'],
['VK_LEFT'],
['VK_RIGHT'],
['VK_DOWN'],
['VK_RETURN'],
['b', {total: 2, current: 0}],
['u', {total: 1, current: 0}],
['VK_RETURN', {current: 0, inserted: 1}],
['{'],
['VK_HOME'],
['VK_DOWN'],
['VK_DOWN'],
['VK_RIGHT'],
['VK_RIGHT'],
['VK_RIGHT'],
['VK_RIGHT'],
['VK_RIGHT'],
['VK_RIGHT'],
['VK_RIGHT'],
['VK_RIGHT'],
['VK_RIGHT'],
['VK_RIGHT'],
['Ctrl+Space', {total: 1, current: 0}],
];
let gEditor;
@ -100,60 +111,64 @@ function testState() {
return;
}
let [key] = TEST_CASES[index];
let [key, details] = TEST_CASES[index];
let entered;
if (details) {
entered = details.entered;
}
let mods = {};
if (key == -1) {
info("pressing Ctrl + Space to get result: [" + TEST_CASES[index] +
"] for index " + index);
gEditor.once("after-suggest", checkState);
info("pressing key " + key + " to get result: " +
JSON.stringify(TEST_CASES[index]) + " for index " + index);
let evt = "after-suggest";
if (key == 'Ctrl+Space') {
key = " ";
mods.accelKey = true;
}
else if (key == "VK_RETURN" && entered) {
evt = "popup-hidden";
}
else if (/(left|right|return|home|end)/ig.test(key) ||
(key == "VK_DOWN" && !gPopup.isOpen)) {
info("pressing key " + key + " to get result: [" + TEST_CASES[index] +
"] for index " + index);
gEditor.once("cursorActivity", checkState);
evt = "cursorActivity";
}
else if (key == "VK_TAB" || key == "VK_UP" || key == "VK_DOWN") {
info("pressing key " + key + " to get result: [" + TEST_CASES[index] +
"] for index " + index);
gEditor.once("suggestion-entered", checkState);
}
else {
info("pressing key " + key + " to get result: [" + TEST_CASES[index] +
"] for index " + index);
gEditor.once("after-suggest", checkState);
evt = "suggestion-entered";
}
gEditor.once(evt, checkState);
EventUtils.synthesizeKey(key, mods, gPanelWindow);
}
function checkState() {
executeSoon(() => {
info("After keypress for index " + index);
let [key, total, current, inserted] = TEST_CASES[index];
if (total != -1) {
let [key, details] = TEST_CASES[index];
details = details || {};
let {total, current, inserted} = details;
if (total != undefined) {
ok(gPopup.isOpen, "Popup is open for index " + index);
is(total, gPopup.itemCount,
"Correct total suggestions for index " + index);
is(current, gPopup.selectedIndex,
"Correct index is selected for index " + index);
if (inserted) {
let { preLabel, label } = gPopup.getItemAtIndex(current);
let { preLabel, label, text } = gPopup.getItemAtIndex(current);
let { line, ch } = gEditor.getCursor();
let lineText = gEditor.getText(line);
is(lineText.substring(ch - label.length, ch), label,
is(lineText.substring(ch - text.length, ch), text,
"Current suggestion from the popup is inserted into the editor.");
}
}
else {
ok(!gPopup.isOpen, "Popup is closed for index " + index);
if (inserted) {
let { preLabel, label } = gPopup.getItemAtIndex(current);
let { preLabel, label, text } = gPopup.getItemAtIndex(current);
let { line, ch } = gEditor.getCursor();
let lineText = gEditor.getText(line);
is(lineText.substring(ch - label.length, ch), label,
is(lineText.substring(ch - text.length, ch), text,
"Current suggestion from the popup is inserted into the editor.");
}
}

View File

@ -168,6 +168,51 @@ function telemetryEnabled() {
gPrefsTelemetry.get(PREF_TELEMETRY_PRERELEASE, false);
}
// Returns a promise that is resolved with the AddonInstall for that URL.
function addonInstallForURL(url, hash) {
let deferred = Promise.defer();
AddonManager.getInstallForURL(url, install => deferred.resolve(install),
"application/x-xpinstall", hash);
return deferred.promise;
}
// Returns a promise that is resolved with an Array<Addon> of the installed
// experiment addons.
function installedExperimentAddons() {
let deferred = Promise.defer();
AddonManager.getAddonsByTypes(["experiment"],
addons => deferred.resolve(addons));
return deferred.promise;
}
// Takes an Array<Addon> and returns a promise that is resolved when the
// addons are uninstalled.
function uninstallAddons(addons) {
let ids = new Set([a.id for (a of addons)]);
let deferred = Promise.defer();
let listener = {};
listener.onUninstalled = addon => {
if (!ids.has(addon.id)) {
return;
}
ids.delete(addon.id);
if (ids.size == 0) {
AddonManager.removeAddonListener(listener);
deferred.resolve();
}
};
AddonManager.addAddonListener(listener);
for (let addon of addons) {
addon.uninstall();
}
return deferred.promise;
}
/**
* The experiments module.
*/
@ -1300,67 +1345,91 @@ Experiments.ExperimentEntry.prototype = {
*/
start: function () {
gLogger.trace("ExperimentEntry::start() for " + this.id);
return Task.spawn(function* ExperimentEntry_start_task() {
let addons = yield installedExperimentAddons();
if (addons.length > 0) {
gLogger.error("ExperimentEntry::start() - there are already "
+ addons.length + " experiment addons installed");
yield uninstallAddons(addons);
}
yield this._installAddon();
}.bind(this));
},
// Async install of the addon for this experiment, part of the start task above.
_installAddon: function* () {
let deferred = Promise.defer();
let installCallback = install => {
let failureHandler = (install, handler) => {
let message = "AddonInstall " + handler + " for " + this.id + ", state=" +
(install.state || "?") + ", error=" + install.error;
gLogger.error("ExperimentEntry::start() - " + message);
this._failedStart = true;
let install = yield addonInstallForURL(this._manifestData.xpiURL,
this._manifestData.xpiHash);
let failureHandler = (install, handler) => {
let message = "AddonInstall " + handler + " for " + this.id + ", state=" +
(install.state || "?") + ", error=" + install.error;
gLogger.error("ExperimentEntry::_installAddon() - " + message);
this._failedStart = true;
TelemetryLog.log(TELEMETRY_LOG.ACTIVATION_KEY,
[TELEMETRY_LOG.ACTIVATION.INSTALL_FAILURE, this.id]);
TelemetryLog.log(TELEMETRY_LOG.ACTIVATION_KEY,
[TELEMETRY_LOG.ACTIVATION.INSTALL_FAILURE, this.id]);
deferred.reject(new Error(message));
};
let listener = {
onDownloadEnded: install => {
gLogger.trace("ExperimentEntry::start() - onDownloadEnded for " + this.id);
},
onInstallStarted: install => {
gLogger.trace("ExperimentEntry::start() - onInstallStarted for " + this.id);
// TODO: this check still needs changes in the addon manager
//if (install.addon.type !== "experiment") {
// gLogger.error("ExperimentEntry::start() - wrong addon type");
// failureHandler({state: -1, error: -1}, "onInstallStarted");
//}
let addon = install.addon;
this._name = addon.name;
this._addonId = addon.id;
this._description = addon.description || "";
this._homepageURL = addon.homepageURL || "";
},
onInstallEnded: install => {
gLogger.trace("ExperimentEntry::start() - install ended for " + this.id);
this._lastChangedDate = this._policy.now();
this._startDate = this._policy.now();
this._enabled = true;
TelemetryLog.log(TELEMETRY_LOG.ACTIVATION_KEY,
[TELEMETRY_LOG.ACTIVATION.ACTIVATED, this.id]);
deferred.resolve();
},
};
["onDownloadCancelled", "onDownloadFailed", "onInstallCancelled", "onInstallFailed"]
.forEach(what => {
listener[what] = install => failureHandler(install, what)
});
install.addListener(listener);
install.install();
deferred.reject(new Error(message));
};
AddonManager.getInstallForURL(this._manifestData.xpiURL,
installCallback,
"application/x-xpinstall",
this._manifestData.xpiHash);
let listener = {
onDownloadEnded: install => {
gLogger.trace("ExperimentEntry::_installAddon() - onDownloadEnded for " + this.id);
if (install.existingAddon) {
gLogger.warn("ExperimentEntry::_installAddon() - onDownloadEnded, addon already installed");
}
if (install.addon.type !== "experiment") {
gLogger.error("ExperimentEntry::_installAddon() - onDownloadEnded, wrong addon type");
install.cancel();
}
},
onInstallStarted: install => {
gLogger.trace("ExperimentEntry::_installAddon() - onInstallStarted for " + this.id);
if (install.existingAddon) {
gLogger.warn("ExperimentEntry::_installAddon() - onInstallStarted, addon already installed");
}
if (install.addon.type !== "experiment") {
gLogger.error("ExperimentEntry::_installAddon() - onInstallStarted, wrong addon type");
return false;
}
},
onInstallEnded: install => {
gLogger.trace("ExperimentEntry::_installAddon() - install ended for " + this.id);
this._lastChangedDate = this._policy.now();
this._startDate = this._policy.now();
this._enabled = true;
TelemetryLog.log(TELEMETRY_LOG.ACTIVATION_KEY,
[TELEMETRY_LOG.ACTIVATION.ACTIVATED, this.id]);
let addon = install.addon;
this._name = addon.name;
this._addonId = addon.id;
this._description = addon.description || "";
this._homepageURL = addon.homepageURL || "";
deferred.resolve();
},
};
["onDownloadCancelled", "onDownloadFailed", "onInstallCancelled", "onInstallFailed"]
.forEach(what => {
listener[what] = install => failureHandler(install, what)
});
install.addListener(listener);
install.install();
return deferred.promise;
},
@ -1395,25 +1464,9 @@ Experiments.ExperimentEntry.prototype = {
return;
}
let listener = {};
let handler = addon => {
if (addon.id !== this._addonId) {
return;
}
updateDates();
this._logTermination(terminationKind, terminationReason);
AddonManager.removeAddonListener(listener);
deferred.resolve();
};
listener.onUninstalled = handler;
listener.onDisabled = handler;
AddonManager.addAddonListener(listener);
addon.uninstall();
updateDates();
this._logTermination(terminationKind, terminationReason);
deferred.resolve(uninstallAddons([addon]));
});
return deferred.promise;

View File

@ -13,16 +13,16 @@ Cu.import("resource://testing-common/services/healthreport/utils.jsm", this);
Cu.import("resource://gre/modules/services/healthreport/providers.jsm");
const EXPERIMENT1_ID = "test-experiment-1@tests.mozilla.org";
const EXPERIMENT1_XPI_SHA1 = "sha1:08c4d3ef1d0fc74faa455e85106ef0bc8cf8ca90";
const EXPERIMENT1_XPI_SHA1 = "sha1:0f15ee3677ffbf1e82367069fe4e8fe8e2ad838f";
const EXPERIMENT1_XPI_NAME = "experiment-1.xpi";
const EXPERIMENT1_NAME = "Test experiment 1";
const EXPERIMENT1A_XPI_SHA1 = "sha1:2b8d14e3e06a54d5ce628fe3598cbb364cff9e6b";
const EXPERIMENT1A_XPI_SHA1 = "sha1:b938f1b4f0bf466a67257aff26d4305ac24231eb";
const EXPERIMENT1A_XPI_NAME = "experiment-1a.xpi";
const EXPERIMENT1A_NAME = "Test experiment 1.1";
const EXPERIMENT2_ID = "test-experiment-2@tests.mozilla.org"
const EXPERIMENT2_XPI_SHA1 = "sha1:81877991ec70360fb48db84c34a9b2da7aa41d6a";
const EXPERIMENT2_XPI_SHA1 = "sha1:9d23425421941e1d1e2037232cf5aeae82dbd4e4";
const EXPERIMENT2_XPI_NAME = "experiment-2.xpi";
const EXPERIMENT3_ID = "test-experiment-3@tests.mozilla.org";
@ -160,11 +160,11 @@ function uninstallAddon(id) {
return deferred.promise;
}
function createAppInfo(options) {
function createAppInfo(optionsIn) {
const XULAPPINFO_CONTRACTID = "@mozilla.org/xre/app-info;1";
const XULAPPINFO_CID = Components.ID("{c763b610-9d49-455a-bbd2-ede71682a1ac}");
let options = options || {};
let options = optionsIn || {};
let id = options.id || "xpcshell@tests.mozilla.org";
let name = options.name || "XPCShell";
let version = options.version || "1.0";

View File

@ -263,6 +263,79 @@ add_task(function* test_getExperiments() {
yield removeCacheFile();
});
// Test that we handle the experiments addon already being
// installed properly.
// We should just pave over them.
add_task(function* test_addonAlreadyInstalled() {
const OBSERVER_TOPIC = "experiments-changed";
let observerFireCount = 0;
let expectedObserverFireCount = 0;
let observer = () => ++observerFireCount;
Services.obs.addObserver(observer, OBSERVER_TOPIC, false);
// Dates the following tests are based on.
let baseDate = new Date(2014, 5, 1, 12);
let startDate = futureDate(baseDate, 100 * MS_IN_ONE_DAY);
let endDate = futureDate(baseDate, 10000 * MS_IN_ONE_DAY);
// The manifest data we test with.
gManifestObject = {
"version": 1,
experiments: [
{
id: EXPERIMENT1_ID,
xpiURL: gDataRoot + EXPERIMENT1_XPI_NAME,
xpiHash: EXPERIMENT1_XPI_SHA1,
startTime: dateToSeconds(startDate),
endTime: dateToSeconds(endDate),
maxActiveSeconds: 10 * SEC_IN_ONE_DAY,
appName: ["XPCShell"],
channel: ["nightly"],
},
],
};
let experiments = new Experiments.Experiments(gPolicy);
// Trigger update, clock set to before any activation.
let now = baseDate;
defineNow(gPolicy, now);
yield experiments.updateManifest();
Assert.equal(observerFireCount, 0,
"Experiments observer should not have been called yet.");
let list = yield experiments.getExperiments();
Assert.equal(list.length, 0, "Experiment list should be empty.");
// Install conflicting addon.
let installed = yield installAddon(gDataRoot + EXPERIMENT1_XPI_NAME, EXPERIMENT1_XPI_SHA1);
Assert.ok(installed, "Addon should have been installed.");
// Trigger update, clock set for the experiment to start.
now = futureDate(startDate, 10 * MS_IN_ONE_DAY);
defineNow(gPolicy, now);
yield experiments.updateManifest();
Assert.equal(observerFireCount, ++expectedObserverFireCount,
"Experiments observer should have been called.");
list = yield experiments.getExperiments();
list = yield experiments.getExperiments();
Assert.equal(list.length, 1, "Experiment list should have 1 entry now.");
Assert.equal(list[0].id, EXPERIMENT1_ID, "Experiment 1 should be the sole entry.");
Assert.equal(list[0].active, true, "Experiment 1 should be active.");
// Cleanup.
Services.obs.removeObserver(observer, OBSERVER_TOPIC);
yield experiments.uninit();
yield removeCacheFile();
});
add_task(function* test_lastActiveToday() {
let experiments = new Experiments.Experiments(gPolicy);

View File

@ -640,10 +640,19 @@ FunctionEnd
${WriteRegStr2} $1 "$0" "DisplayIcon" "$8\${FileMainEXE},0" 0
${WriteRegStr2} $1 "$0" "DisplayName" "${BrandFullNameInternal} ${AppVersion}$3 (${ARCH} ${AB_CD})" 0
${WriteRegStr2} $1 "$0" "DisplayVersion" "${AppVersion}" 0
${WriteRegStr2} $1 "$0" "HelpLink" "${HelpLink}" 0
${WriteRegStr2} $1 "$0" "InstallLocation" "$8" 0
${WriteRegStr2} $1 "$0" "Publisher" "Mozilla" 0
${WriteRegStr2} $1 "$0" "UninstallString" "$\"$8\uninstall\helper.exe$\"" 0
DeleteRegValue SHCTX "$0" "URLInfoAbout"
; Don't add URLInfoAbout which is the release notes url except for the release
; and esr channels since nightly, aurora, and beta do not have release notes.
; Note: URLInfoAbout is only defined in the official branding.nsi.
!ifdef URLInfoAbout
!ifndef BETA_UPDATE_CHANNEL
${WriteRegStr2} $1 "$0" "URLInfoAbout" "${URLInfoAbout}" 0
!endif
!endif
${WriteRegStr2} $1 "$0" "URLUpdateInfo" "${URLUpdateInfo}" 0
${WriteRegDWORD2} $1 "$0" "NoModify" 1 0
${WriteRegDWORD2} $1 "$0" "NoRepair" 1 0

View File

@ -30,6 +30,7 @@ RequestExecutionLevel user
Var Dialog
Var Progressbar
Var ProgressbarMarqueeIntervalMS
Var LabelDownloading
Var LabelInstalling
Var LabelFreeSpace
@ -84,14 +85,13 @@ Var StartDownloadPhaseTickCount
; seconds spent on each of these pages is reported.
Var IntroPhaseSeconds
Var OptionsPhaseSeconds
; The tick count for the last download
; The tick count for the last download.
Var StartLastDownloadTickCount
; The number of seconds from the start of the download phase until the first
; bytes are received. This is only recorded for first request so it is possible
; to determine connection issues for the first request.
Var DownloadFirstTransferSeconds
; The last four tick counts are for the end of a phase in the installation page.
; the options phase when it isn't entered.
Var EndDownloadPhaseTickCount
Var EndPreInstallPhaseTickCount
Var EndInstallPhaseTickCount
@ -177,6 +177,10 @@ Var ControlRightPX
; immediate feedback is given to the user.
!define InstallProgressFirstStep 20
; The finish step size to quickly increment the progress bar after the
; installation has finished.
!define InstallProgressFinishStep 40
; Number of steps for the install progress.
; This might not be enough when installing on a slow network drive so it will
; fallback to downloading the full installer if it reaches this number. The size
@ -192,9 +196,6 @@ Var ControlRightPX
; InstallProgressFirstStep .
!define /math InstallPaveOverTotalSteps ${InstallProgressFirstStep} + 1800
; The interval in MS used for the progress bars set as marquee.
!define ProgressbarMarqueeIntervalMS 10
; On Vista and above attempt to elevate Standard Users in addition to users that
; are a member of the Administrators group.
!define NONADMIN_ELEVATE
@ -388,6 +389,13 @@ Function .onInit
StrCpy $CanSetAsDefault "true"
${EndIf}
; The interval in MS used for the progress bars set as marquee.
${If} ${AtLeastWinVista}
StrCpy $ProgressbarMarqueeIntervalMS "10"
${Else}
StrCpy $ProgressbarMarqueeIntervalMS "50"
${EndIf}
; Initialize the majority of variables except those that need to be reset
; when a page is displayed.
StrCpy $IntroPhaseSeconds "0"
@ -448,9 +456,9 @@ FunctionEnd
Function .onUserAbort
${NSD_KillTimer} StartDownload
${NSD_KillTimer} OnDownload
${NSD_KillTimer} StartInstall
${NSD_KillTimer} CheckInstall
${NSD_KillTimer} FinishInstall
${NSD_KillTimer} FinishProgressBar
${NSD_KillTimer} DisplayDownloadError
${If} "$IsDownloadFinished" != ""
@ -458,7 +466,7 @@ Function .onUserAbort
; Aborting the abort will allow SendPing which is called by
; DisplayDownloadError to hide the installer window and close the installer
; after it sends the metrics ping.
Abort
Abort
${EndIf}
FunctionEnd
@ -1167,7 +1175,7 @@ Function createInstall
Pop $Progressbar
${NSD_AddStyle} $Progressbar ${PBS_MARQUEE}
SendMessage $Progressbar ${PBM_SETMARQUEE} 1 \
${ProgressbarMarqueeIntervalMS} ; start=1|stop=0 interval(ms)=+N
$ProgressbarMarqueeIntervalMS ; start=1|stop=0 interval(ms)=+N
${NSD_CreateLabelCenter} 103u 180u 241u 20u "$(DOWNLOADING_LABEL)"
Pop $LabelDownloading
@ -1306,7 +1314,7 @@ Function OnDownload
StrCpy $DownloadedBytes "0"
${NSD_AddStyle} $Progressbar ${PBS_MARQUEE}
SendMessage $Progressbar ${PBM_SETMARQUEE} 1 \
${ProgressbarMarqueeIntervalMS} ; start=1|stop=0 interval(ms)=+N
$ProgressbarMarqueeIntervalMS ; start=1|stop=0 interval(ms)=+N
${EndIf}
InetBgDL::Get /RESET /END
StrCpy $DownloadSizeBytes ""
@ -1524,9 +1532,11 @@ Function OnDownload
; require an OS restart for the full installer.
Delete "$INSTDIR\${FileMainEXE}.moz-upgrade"
; Flicker happens less often if a timer is used between updates of the
; progress bar.
${NSD_CreateTimer} StartInstall ${InstallIntervalMS}
System::Call "kernel32::GetTickCount()l .s"
Pop $EndPreInstallPhaseTickCount
Exec "$\"$PLUGINSDIR\download.exe$\" /INI=$PLUGINSDIR\${CONFIG_INI}"
${NSD_CreateTimer} CheckInstall ${InstallIntervalMS}
${Else}
${If} $HalfOfDownload != "true"
${AndIf} $3 > $HalfOfDownload
@ -1566,21 +1576,6 @@ Function OnPing
${EndIf}
FunctionEnd
Function StartInstall
${NSD_KillTimer} StartInstall
System::Call "kernel32::GetTickCount()l .s"
Pop $EndPreInstallPhaseTickCount
IntOp $InstallCounterStep $InstallCounterStep + 1
LockWindow on
SendMessage $Progressbar ${PBM_STEPIT} 0 0
LockWindow off
Exec "$\"$PLUGINSDIR\download.exe$\" /INI=$PLUGINSDIR\${CONFIG_INI}"
${NSD_CreateTimer} CheckInstall ${InstallIntervalMS}
FunctionEnd
Function CheckInstall
IntOp $InstallCounterStep $InstallCounterStep + 1
${If} $InstallCounterStep >= $InstallTotalSteps
@ -1599,6 +1594,9 @@ Function CheckInstall
Delete "$INSTDIR\install.tmp"
CopyFiles /SILENT "$INSTDIR\install.log" "$INSTDIR\install.tmp"
; The unfocus and refocus that happens approximately here is caused by the
; installer calling SHChangeNotify to refresh the shortcut icons.
; When the full installer completes the installation the install.log will no
; longer be in use.
ClearErrors
@ -1612,7 +1610,7 @@ Function CheckInstall
Delete "$PLUGINSDIR\${CONFIG_INI}"
System::Call "kernel32::GetTickCount()l .s"
Pop $EndInstallPhaseTickCount
System::Int64Op $InstallStepSize * 20
System::Int64Op $InstallStepSize * ${InstallProgressFinishStep}
Pop $InstallStepSize
SendMessage $Progressbar ${PBM_SETSTEP} $InstallStepSize 0
${NSD_CreateTimer} FinishInstall ${InstallIntervalMS}
@ -1623,7 +1621,7 @@ FunctionEnd
Function FinishInstall
; The full installer has completed but the progress bar still needs to finish
; so increase the size of the step.
IntOp $InstallCounterStep $InstallCounterStep + 40
IntOp $InstallCounterStep $InstallCounterStep + ${InstallProgressFinishStep}
${If} $InstallTotalSteps < $InstallCounterStep
StrCpy $InstallCounterStep "$InstallTotalSteps"
${EndIf}
@ -1831,10 +1829,9 @@ Function CheckSpace
${GetLongPath} "$ExistingTopDir" $ExistingTopDir
; GetDiskFreeSpaceExW can require a backslash
; GetDiskFreeSpaceExW requires a backslash.
StrCpy $0 "$ExistingTopDir" "" -1 ; the last character
${If} "$0" != "\"
; A backslash is required for
StrCpy $0 "\"
${Else}
StrCpy $0 ""

View File

@ -57,11 +57,11 @@
- options panel and is used for settings that trigger page reload. -->
<!ENTITY options.context.triggersPageRefresh "* Current session only, reloads the page">
<!-- LOCALIZATION NOTE (options.enableChrome.label3): This is the label for the
<!-- LOCALIZATION NOTE (options.enableChrome.label4): This is the label for the
- checkbox that toggles chrome debugging, i.e. devtools.chrome.enabled
- boolean preference in about:config, in the options panel. -->
<!ENTITY options.enableChrome.label3 "Enable chrome debugging">
<!ENTITY options.enableChrome.tooltip "Turning this option on will allow you to use various developer tools in browser context">
<!ENTITY options.enableChrome.label4 "Enable chrome and addon debugging">
<!ENTITY options.enableChrome.tooltip2 "Turning this option on will allow you to use various developer tools in browser context and debug addons from the Add-On Manager">
<!-- LOCALIZATION NOTE (options.enableRemote.label3): This is the label for the
- checkbox that toggles remote debugging, i.e. devtools.debugger.remote-enabled

View File

@ -683,20 +683,20 @@ this.UITour = {
let targetQuery = targetObject.query;
aWindow.PanelUI.ensureReady().then(() => {
let node;
if (typeof targetQuery == "function") {
deferred.resolve({
addTargetListener: targetObject.addTargetListener,
node: targetQuery(aWindow.document),
removeTargetListener: targetObject.removeTargetListener,
targetName: aTargetName,
widgetName: targetObject.widgetName,
});
return;
try {
node = targetQuery(aWindow.document);
} catch (ex) {
node = null;
}
} else {
node = aWindow.document.querySelector(targetQuery);
}
deferred.resolve({
addTargetListener: targetObject.addTargetListener,
node: aWindow.document.querySelector(targetQuery),
node: node,
removeTargetListener: targetObject.removeTargetListener,
targetName: aTargetName,
widgetName: targetObject.widgetName,

View File

@ -64,6 +64,30 @@ let tests = [
done();
});
},
function test_availableTargets_exceptionFromGetTarget(done) {
// The query function for the "search" target will throw if it's not found.
// Make sure the callback still fires with the other available targets.
CustomizableUI.removeWidgetFromArea("search-container");
gContentAPI.getConfiguration("availableTargets", (data) => {
// Default minus "search" and "searchProvider"
ok_targets(data, [
"accountStatus",
"addons",
"appMenu",
"backForward",
"bookmarks",
"customize",
"help",
"home",
"pinnedTab",
"quit",
"urlbar",
]);
CustomizableUI.reset();
done();
});
},
];
function ok_targets(actualData, expectedTargets) {

View File

@ -66,8 +66,12 @@
-moz-image-region: rect(-5px, 12px, 11px, -4px);
}
/* This only has an effect when this element is placed on the bookmarks toolbar.
* It's 30px to make sure buttons with 18px icons fit along with the default 16px
* icons, without changing the size of the toolbar.
*/
#personal-bookmarks {
min-height: 29px;
min-height: 30px;
}
#browser-bottombox {
@ -95,7 +99,7 @@ toolbarbutton.bookmark-item[open="true"] {
-moz-padding-end: 2px;
}
.bookmark-item > .toolbarbutton-icon,
.bookmark-item:not(#home-button) > .toolbarbutton-icon,
#personal-bookmarks[cui-areatype="toolbar"] > #bookmarks-toolbar-placeholder > .toolbarbutton-icon {
width: 16px;
height: 16px;
@ -134,13 +138,6 @@ toolbarpaletteitem[place="palette"] > #personal-bookmarks > #bookmarks-toolbar-p
to { transform: rotate(180deg) translateX(-16px) rotate(-180deg) scale(1); opacity: 0; }
}
@keyframes animation-bookmarkAddedToBookmarksBar {
from { transform: rotate(0deg) translateX(-10px) rotate(0deg) scale(1); opacity: 0; }
60% { transform: rotate(180deg) translateX(-10px) rotate(-180deg) scale(2.2); opacity: 1; }
80% { opacity: 1; }
to { transform: rotate(180deg) translateX(-10px) rotate(-180deg) scale(1); opacity: 0; }
}
@keyframes animation-bookmarkPulse {
from { transform: scale(1); }
50% { transform: scale(1.3); }
@ -182,10 +179,6 @@ toolbarpaletteitem[place="palette"] > #personal-bookmarks > #bookmarks-toolbar-p
animation-timing-function: ease, ease, ease;
}
#bookmarked-notification-anchor[notification="finish"][in-bookmarks-toolbar=true] > #bookmarked-notification {
animation: animation-bookmarkAddedToBookmarksBar 800ms;
}
#bookmarks-menu-button[notification="finish"] > .toolbarbutton-menubutton-dropmarker > .dropmarker-icon {
list-style-image: none !important;
}
@ -839,12 +832,6 @@ toolbarbutton[sdk-button="true"][cui-areatype="toolbar"] > .toolbarbutton-icon {
.unified-nav-forward[_moz-menuactive]:-moz-locale-dir(rtl) {
list-style-image: url("moz-icon://stock/gtk-go-forward-rtl?size=menu") !important;
}
#home-button.bookmark-item {
list-style-image: url("moz-icon://stock/gtk-home?size=menu");
}
#home-button.bookmark-item[disabled="true"] {
list-style-image: url("moz-icon://stock/gtk-home?size=menu&state=disabled");
}
/* Menu panel buttons */
@ -1632,6 +1619,7 @@ richlistitem[type~="action"][actiontype="switchtab"] > .ac-url-box > .ac-action-
#bookmarks-menu-button[cui-areatype="toolbar"] > .toolbarbutton-menubutton-dropmarker {
-moz-appearance: none !important;
-moz-box-align: center;
}
#bookmarks-menu-button[cui-areatype="toolbar"] > .toolbarbutton-menubutton-dropmarker > .dropmarker-icon {
@ -1639,23 +1627,6 @@ richlistitem[type~="action"][actiontype="switchtab"] > .ac-url-box > .ac-action-
margin-bottom: 3px;
}
#bookmarks-menu-button[cui-areatype="toolbar"].bookmark-item > .toolbarbutton-menubutton-dropmarker > .dropmarker-icon,
#bookmarks-menu-button.bookmark-item {
list-style-image: url("chrome://browser/skin/Toolbar-small.png");
}
#bookmarks-menu-button.bookmark-item {
-moz-image-region: rect(0px 144px 16px 128px);
}
#bookmarks-menu-button.bookmark-item[starred] {
-moz-image-region: rect(16px 144px 32px 128px);
}
#bookmarks-menu-button[cui-areatype="toolbar"].bookmark-item > .toolbarbutton-menubutton-dropmarker > .dropmarker-icon {
-moz-image-region: rect(0px 160px 16px 144px);
}
#bookmarks-menu-button[disabled][cui-areatype="toolbar"] > .toolbarbutton-icon,
#bookmarks-menu-button[disabled][cui-areatype="toolbar"] > .toolbarbutton-menu-dropmarker,
#bookmarks-menu-button[disabled][cui-areatype="toolbar"] > .toolbarbutton-menubutton-dropmarker,
@ -1720,9 +1691,9 @@ richlistitem[type~="action"][actiontype="switchtab"] > .ac-url-box > .ac-action-
}
.panel-promo-closebutton {
list-style-image: url("moz-icon://stock/gtk-close?size=menu");
margin-top: 0;
margin-bottom: 0;
-moz-appearance: none;
height: 16px;
width: 16px;
}
.panel-promo-closebutton > .toolbarbutton-text {
@ -1817,11 +1788,27 @@ richlistitem[type~="action"][actiontype="switchtab"] > .ac-url-box > .ac-action-
margin: -4px;
}
/* Tabstrip close button */
.tabs-closebutton,
.tab-close-button {
list-style-image: url("moz-icon://stock/gtk-close?size=menu");
margin-top: 0;
margin-bottom: -1px;
-moz-margin-end: -4px;
-moz-appearance: none;
height: 16px;
width: 16px;
}
.tabs-closebutton:not([selected]):not(:hover),
.tab-close-button:not([selected]):not(:hover) {
background-image: -moz-image-rect(url("chrome://global/skin/icons/close.svg"), 0, 64, 16, 48);
}
.tabs-closebutton:not([selected]):not(:hover):-moz-lwtheme-brighttext,
.tab-close-button:not([selected]):not(:hover):-moz-lwtheme-brighttext {
background-image: -moz-image-rect(url("chrome://global/skin/icons/close.svg"), 0, 80, 16, 64);
}
.tabs-closebutton:not([selected]):not(:hover):-moz-lwtheme-darktext,
.tab-close-button:not([selected]):not(:hover):-moz-lwtheme-darktext {
background-image: -moz-image-rect(url("chrome://global/skin/icons/close.svg"), 0, 96, 16, 80);
}
/* Tabstrip new tab button */
@ -1832,10 +1819,15 @@ richlistitem[type~="action"][actiontype="switchtab"] > .ac-url-box > .ac-action-
-moz-image-region: auto;
}
/* Tabstrip close button */
.tabs-closebutton,
.customization-tipPanel-closeBox > .close-icon {
list-style-image: url("moz-icon://stock/gtk-close?size=menu");
-moz-appearance: none;
width: 16px;
height: 16px;
}
/* The :hover:active style from toolkit doesn't seem to work in this panel so just use :active. */
.customization-tipPanel-closeBox > .close-icon:active {
background-image: -moz-image-rect(url("chrome://global/skin/icons/close.svg"), 0, 48, 16, 32);
}
.tabs-closebutton > .toolbarbutton-icon {

View File

@ -49,14 +49,11 @@
}
#newtab-undo-close-button {
-moz-appearance: none;
padding: 0;
border: none;
list-style-image: url("moz-icon://stock/gtk-close?size=menu");
-moz-user-focus: normal;
}
#newtab-undo-close-button > .toolbarbutton-icon {
margin: -4px;
height: 16px;
width: 16px;
}
#newtab-undo-close-button > .toolbarbutton-text {

View File

@ -91,7 +91,17 @@ html[dir=rtl] .favicon {
width: 16px;
height: 16px;
opacity: 0.2;
background: url("moz-icon://stock/gtk-close?size=menu") no-repeat;
background-image: -moz-image-rect(url("chrome://global/skin/icons/close.svg"), 0, 16, 16, 0);
background-position: center center;
background-repeat: no-repeat;
}
.close:hover {
background-image: -moz-image-rect(url("chrome://global/skin/icons/close.svg"), 0, 32, 16, 16);
}
.close:hover:active {
background-image: -moz-image-rect(url("chrome://global/skin/icons/close.svg"), 0, 48, 16, 32);
}
html[dir=rtl] .close {

View File

@ -51,7 +51,6 @@
#newtab-undo-close-button {
padding: 0;
border: none;
-moz-user-focus: normal;
}
#newtab-undo-close-button > .toolbarbutton-text {

View File

@ -1,6 +1,8 @@
%filter substitution
%define primaryToolbarButtons #back-button, #forward-button, #home-button, #print-button, #downloads-button, #bookmarks-menu-button, #new-tab-button, #new-window-button, #cut-button, #copy-button, #paste-button, #fullscreen-button, #zoom-out-button, #zoom-reset-button, #zoom-in-button, #sync-button, #feed-button, #tabview-button, #webrtc-status-button, #social-share-button, #open-file-button, #find-button, #developer-button, #preferences-button, #privatebrowsing-button, #save-page-button, #switch-to-metro-button, #add-ons-button, #history-panelmenu, #nav-bar-overflow-button, #PanelUI-menu-button, #characterencoding-button, #email-link-button, #sidebar-button
% Note that zoom-reset-button is a bit different since it doesn't use an image and thus has the image with display: none.
%define nestedButtons #zoom-out-button, #zoom-reset-button, #zoom-in-button, #cut-button, #copy-button, #paste-button
%define primaryToolbarButtons #back-button, #forward-button, #home-button, #print-button, #downloads-button, #bookmarks-menu-button, #new-tab-button, #new-window-button, #fullscreen-button, #sync-button, #feed-button, #tabview-button, #webrtc-status-button, #social-share-button, #open-file-button, #find-button, #developer-button, #preferences-button, #privatebrowsing-button, #save-page-button, #switch-to-metro-button, #add-ons-button, #history-panelmenu, #nav-bar-overflow-button, #PanelUI-menu-button, #characterencoding-button, #email-link-button, #sidebar-button, @nestedButtons@
%ifdef XP_MACOSX
% Prior to 10.7 there wasn't a native fullscreen button so we use #restore-button to exit fullscreen
@ -9,4 +11,3 @@
%endif
%define inAnyPanel :-moz-any(:not([cui-areatype="toolbar"]), [overflowedItem=true])
%define nestedButtons #zoom-out-button, #zoom-in-button, #cut-button, #copy-button, #paste-button

View File

@ -1,3 +1,5 @@
/* Note that this file isn't used for HiDPI on OS X. */
:-moz-any(@primaryToolbarButtons@),
#bookmarks-menu-button > .toolbarbutton-menubutton-dropmarker > .dropmarker-icon {
list-style-image: url("chrome://browser/skin/Toolbar.png");

View File

@ -198,6 +198,12 @@
ThreeDHighlight 1px, ThreeDHighlight 2px,
ActiveBorder 2px, ActiveBorder 4px, transparent 4px);
}
/* End classic titlebar gradient */
#main-window[tabsintitlebar]:not([inFullscreen]) :-moz-any(#TabsToolbar, #toolbar-menubar) toolbarbutton:not(:-moz-lwtheme) {
color: inherit;
}
}
/* Render a window top border for lwthemes on WinXP modern themes: */

View File

@ -57,7 +57,6 @@
-moz-appearance: none;
padding: 0;
border: none;
-moz-user-focus: normal;
}
#newtab-undo-close-button > .toolbarbutton-text {

View File

@ -581,6 +581,7 @@ sync_java_files = [
'fxa/login/TokensAndKeysState.java',
'fxa/receivers/FxAccountDeletedReceiver.java',
'fxa/receivers/FxAccountDeletedService.java',
'fxa/receivers/FxAccountUpgradeReceiver.java',
'fxa/sync/FxAccountGlobalSession.java',
'fxa/sync/FxAccountNotificationManager.java',
'fxa/sync/FxAccountSchedulePolicy.java',

View File

@ -0,0 +1,79 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.db;
import org.mozilla.gecko.mozglue.RobocopTarget;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.net.Uri;
/**
* The base class for ContentProviders that wish to use a different DB
* for each profile.
*
* This class has logic shared between ordinary per-profile CPs and
* those that wish to share DB connections between CPs.
*/
public abstract class AbstractPerProfileDatabaseProvider extends AbstractTransactionalProvider {
/**
* Extend this to provide access to your own map of shared databases. This
* is a method so that your subclass doesn't collide with others!
*/
protected abstract PerProfileDatabases<? extends SQLiteOpenHelper> getDatabases();
/*
* Fetches a readable database based on the profile indicated in the
* passed URI. If the URI does not contain a profile param, the default profile
* is used.
*
* @param uri content URI optionally indicating the profile of the user
* @return instance of a readable SQLiteDatabase
*/
@Override
protected SQLiteDatabase getReadableDatabase(Uri uri) {
String profile = null;
if (uri != null) {
profile = uri.getQueryParameter(BrowserContract.PARAM_PROFILE);
}
return getDatabases().getDatabaseHelperForProfile(profile, isTest(uri)).getReadableDatabase();
}
/*
* Fetches a writable database based on the profile indicated in the
* passed URI. If the URI does not contain a profile param, the default profile
* is used
*
* @param uri content URI optionally indicating the profile of the user
* @return instance of a writable SQLiteDatabase
*/
@Override
protected SQLiteDatabase getWritableDatabase(Uri uri) {
String profile = null;
if (uri != null) {
profile = uri.getQueryParameter(BrowserContract.PARAM_PROFILE);
}
return getDatabases().getDatabaseHelperForProfile(profile, isTest(uri)).getWritableDatabase();
}
protected SQLiteDatabase getWritableDatabaseForProfile(String profile, boolean isTest) {
return getDatabases().getDatabaseHelperForProfile(profile, isTest).getWritableDatabase();
}
/**
* This method should ONLY be used for testing purposes.
*
* @param uri content URI optionally indicating the profile of the user
* @return instance of a writable SQLiteDatabase
*/
@Override
@RobocopTarget
public SQLiteDatabase getWritableDatabaseForTesting(Uri uri) {
return getWritableDatabase(uri);
}
}

View File

@ -1,199 +1,66 @@
/* 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/. */
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.db;
import org.mozilla.gecko.db.BrowserContract.CommonColumns;
import org.mozilla.gecko.db.BrowserContract.SyncColumns;
import org.mozilla.gecko.db.PerProfileDatabases.DatabaseHelperFactory;
import org.mozilla.gecko.mozglue.RobocopTarget;
import android.content.ContentProvider;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.SQLException;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.net.Uri;
import android.os.Build;
import android.text.TextUtils;
import android.util.Log;
/*
* Abstract class containing methods needed to make a SQLite-based content provider with a
* database helper of type T. Abstract methods insertInTransaction, deleteInTransaction and
* updateInTransaction all called within a DB transaction so failed modifications can be rolled-back.
/**
* This abstract class exists to capture some of the transaction-handling
* commonalities in Fennec's DB layer.
*
* In particular, this abstracts DB access, batching, and a particular
* transaction approach.
*
* That approach is: subclasses implement the abstract methods
* {@link #insertInTransaction(android.net.Uri, android.content.ContentValues)},
* {@link #deleteInTransaction(android.net.Uri, String, String[])}, and
* {@link #updateInTransaction(android.net.Uri, android.content.ContentValues, String, String[])}.
*
* These are all called expecting a transaction to be established, so failed
* modifications can be rolled-back, and work batched.
*
* If no transaction is established, that's not a problem. Transaction nesting
* can be avoided by using {@link #beginWrite(SQLiteDatabase)}.
*
* The decision of when to begin a transaction is left to the subclasses,
* primarily to avoid the pattern of a transaction being begun, a read occurring,
* and then a write being necessary. This lock upgrade can result in SQLITE_BUSY,
* which we don't handle well. Better to avoid starting a transaction too soon!
*
* You are probably interested in some subclasses:
*
* * {@link AbstractPerProfileDatabaseProvider} provides a simple abstraction for
* querying databases that are stored in the user's profile directory.
* * {@link PerProfileDatabaseProvider} is a simple version that only allows a
* single ContentProvider to access each per-profile database.
* * {@link SharedBrowserDatabaseProvider} is an example of a per-profile provider
* that allows for multiple providers to safely work with the same databases.
*/
public abstract class TransactionalProvider<T extends SQLiteOpenHelper> extends ContentProvider {
@SuppressWarnings("javadoc")
public abstract class AbstractTransactionalProvider extends ContentProvider {
private static final String LOGTAG = "GeckoTransProvider";
protected Context mContext;
protected PerProfileDatabases<T> mDatabases;
/*
* Returns the name of the database file. Used to get a path
* to the DB file.
*
* @return name of the database file
*/
abstract protected String getDatabaseName();
private static boolean logDebug = Log.isLoggable(LOGTAG, Log.DEBUG);
private static boolean logVerbose = Log.isLoggable(LOGTAG, Log.VERBOSE);
/*
* Creates and returns an instance of a DB helper. Given a
* context and a path to the DB file
*
* @param context to use to create the database helper
* @param databasePath path to the DB file
* @return instance of the database helper
*/
abstract protected T createDatabaseHelper(Context context, String databasePath);
protected abstract SQLiteDatabase getReadableDatabase(Uri uri);
protected abstract SQLiteDatabase getWritableDatabase(Uri uri);
/*
* Inserts an item into the database within a DB transaction.
*
* @param uri query URI
* @param values column values to be inserted
* @return a URI for the newly inserted item
*/
abstract protected Uri insertInTransaction(Uri uri, ContentValues values);
public abstract SQLiteDatabase getWritableDatabaseForTesting(Uri uri);
/*
* Deletes items from the database within a DB transaction.
*
* @param uri Query URI.
* @param selection An optional filter to match rows to delete.
* @param selectionArgs An array of arguments to substitute into the selection.
*
* @return number of rows impacted by the deletion.
*/
abstract protected int deleteInTransaction(Uri uri, String selection, String[] selectionArgs);
/*
* Updates the database within a DB transaction.
*
* @param uri Query URI.
* @param values A set of column_name/value pairs to add to the database.
* @param selection An optional filter to match rows to update.
* @param selectionArgs An array of arguments to substitute into the selection.
*
* @return number of rows impacted by the update.
*/
abstract protected int updateInTransaction(Uri uri, ContentValues values, String selection, String[] selectionArgs);
/*
* Fetches a readable database based on the profile indicated in the
* passed URI. If the URI does not contain a profile param, the default profile
* is used.
*
* @param uri content URI optionally indicating the profile of the user
* @return instance of a readable SQLiteDatabase
*/
protected SQLiteDatabase getReadableDatabase(Uri uri) {
String profile = null;
if (uri != null) {
profile = uri.getQueryParameter(BrowserContract.PARAM_PROFILE);
}
return mDatabases.getDatabaseHelperForProfile(profile, isTest(uri)).getReadableDatabase();
}
/*
* Fetches a writeable database based on the profile indicated in the
* passed URI. If the URI does not contain a profile param, the default profile
* is used
*
* @param uri content URI optionally indicating the profile of the user
* @return instance of a writeable SQLiteDatabase
*/
protected SQLiteDatabase getWritableDatabase(Uri uri) {
String profile = null;
if (uri != null) {
profile = uri.getQueryParameter(BrowserContract.PARAM_PROFILE);
}
return mDatabases.getDatabaseHelperForProfile(profile, isTest(uri)).getWritableDatabase();
}
/**
* Public version of {@link #getWritableDatabase(Uri) getWritableDatabase}.
* This method should ONLY be used for testing purposes.
*
* @param uri content URI optionally indicating the profile of the user
* @return instance of a writeable SQLiteDatabase
*/
@RobocopTarget
public SQLiteDatabase getWritableDatabaseForTesting(Uri uri) {
return getWritableDatabase(uri);
}
/**
* Return true of the query is from Firefox Sync.
* @param uri query URI
*/
public static boolean isCallerSync(Uri uri) {
String isSync = uri.getQueryParameter(BrowserContract.PARAM_IS_SYNC);
return !TextUtils.isEmpty(isSync);
}
/**
* Indicates whether a query should include deleted fields
* based on the URI.
* @param uri query URI
*/
public static boolean shouldShowDeleted(Uri uri) {
String showDeleted = uri.getQueryParameter(BrowserContract.PARAM_SHOW_DELETED);
return !TextUtils.isEmpty(showDeleted);
}
/**
* Indicates whether an insertion should be made if a record doesn't
* exist, based on the URI.
* @param uri query URI
*/
public static boolean shouldUpdateOrInsert(Uri uri) {
String insertIfNeeded = uri.getQueryParameter(BrowserContract.PARAM_INSERT_IF_NEEDED);
return Boolean.parseBoolean(insertIfNeeded);
}
/**
* Indicates whether query is a test based on the URI.
* @param uri query URI
*/
public static boolean isTest(Uri uri) {
String isTest = uri.getQueryParameter(BrowserContract.PARAM_IS_TEST);
return !TextUtils.isEmpty(isTest);
}
protected SQLiteDatabase getWritableDatabaseForProfile(String profile, boolean isTest) {
return mDatabases.getDatabaseHelperForProfile(profile, isTest).getWritableDatabase();
}
@Override
public boolean onCreate() {
synchronized (this) {
mContext = getContext();
mDatabases = new PerProfileDatabases<T>(
getContext(), getDatabaseName(), new DatabaseHelperFactory<T>() {
@Override
public T makeDatabaseHelper(Context context, String databasePath) {
return createDatabaseHelper(context, databasePath);
}
});
}
return true;
}
/**
* Return true if OS version and database parallelism support indicates
* that this provider should bundle writes into transactions.
*/
@SuppressWarnings("static-method")
protected boolean shouldUseTransactions() {
return Build.VERSION.SDK_INT >= 11;
}
protected abstract Uri insertInTransaction(Uri uri, ContentValues values);
protected abstract int deleteInTransaction(Uri uri, String selection, String[] selectionArgs);
protected abstract int updateInTransaction(Uri uri, ContentValues values, String selection, String[] selectionArgs);
/**
* Track whether we're in a batch operation.
@ -222,6 +89,29 @@ public abstract class TransactionalProvider<T extends SQLiteOpenHelper> extends
*/
final ThreadLocal<Boolean> isInBatchOperation = new ThreadLocal<Boolean>();
/**
* Return true if OS version and database parallelism support indicates
* that this provider should bundle writes into transactions.
*/
@SuppressWarnings("static-method")
protected boolean shouldUseTransactions() {
return Build.VERSION.SDK_INT >= 11;
}
protected static String computeSQLInClause(int items, String field) {
final StringBuilder builder = new StringBuilder(field);
builder.append(" IN (");
int i = 0;
for (; i < items - 1; ++i) {
builder.append("?, ");
}
if (i < items) {
builder.append("?");
}
builder.append(")");
return builder.toString();
}
private boolean isInBatch() {
final Boolean isInBatch = isInBatchOperation.get();
if (isInBatch == null) {
@ -265,7 +155,7 @@ public abstract class TransactionalProvider<T extends SQLiteOpenHelper> extends
* If we're not in a batch, but we are in a write transaction,
* end it.
*
* @see TransactionalProvider#markWriteSuccessful(SQLiteDatabase)
* @see PerProfileDatabaseProvider#markWriteSuccessful(SQLiteDatabase)
*/
protected void endWrite(final SQLiteDatabase db) {
if (isInBatch()) {
@ -301,23 +191,6 @@ public abstract class TransactionalProvider<T extends SQLiteOpenHelper> extends
isInBatchOperation.set(Boolean.FALSE);
}
/*
* This utility is replicated from RepoUtils, which is managed by android-sync.
*/
protected static String computeSQLInClause(int items, String field) {
final StringBuilder builder = new StringBuilder(field);
builder.append(" IN (");
int i = 0;
for (; i < items - 1; ++i) {
builder.append("?, ");
}
if (i < items) {
builder.append("?");
}
builder.append(")");
return builder.toString();
}
/**
* Turn a single-column cursor of longs into a single SQL "IN" clause.
* We can do this without using selection arguments because Long isn't
@ -385,10 +258,8 @@ public abstract class TransactionalProvider<T extends SQLiteOpenHelper> extends
return result;
}
@Override
public int update(Uri uri, ContentValues values, String selection,
String[] selectionArgs) {
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
trace("Calling update on URI: " + uri + ", " + selection + ", " + selectionArgs);
final SQLiteDatabase db = getWritableDatabase(uri);
@ -438,71 +309,53 @@ public abstract class TransactionalProvider<T extends SQLiteOpenHelper> extends
if (successes > 0) {
final boolean shouldSyncToNetwork = !isCallerSync(uri);
mContext.getContentResolver().notifyChange(uri, null, shouldSyncToNetwork);
getContext().getContentResolver().notifyChange(uri, null, shouldSyncToNetwork);
}
return successes;
}
/**
* Clean up some deleted records from the specified table.
*
* If called in an existing transaction, it is the caller's responsibility
* to ensure that the transaction is already upgraded to a writer, because
* this method issues a read followed by a write, and thus is potentially
* vulnerable to an unhandled SQLITE_BUSY failure during the upgrade.
*
* If not called in an existing transaction, no new explicit transaction
* will be begun.
* Indicates whether a query should include deleted fields
* based on the URI.
* @param uri query URI
*/
protected void cleanupSomeDeletedRecords(Uri fromUri, Uri targetUri, String tableName) {
Log.d(LOGTAG, "Cleaning up deleted records from " + tableName);
// We clean up records marked as deleted that are older than a
// predefined max age. It's important not be too greedy here and
// remove only a few old deleted records at a time.
// we cleanup records marked as deleted that are older than a
// predefined max age. It's important not be too greedy here and
// remove only a few old deleted records at a time.
// Maximum age of deleted records to be cleaned up (20 days in ms)
final long MAX_AGE_OF_DELETED_RECORDS = 86400000 * 20;
// Number of records marked as deleted to be removed
final long DELETED_RECORDS_PURGE_LIMIT = 5;
// Android SQLite doesn't have LIMIT on DELETE. Instead, query for the
// IDs of matching rows, then delete them in one go.
final long now = System.currentTimeMillis();
final String selection = SyncColumns.IS_DELETED + " = 1 AND " +
SyncColumns.DATE_MODIFIED + " <= " +
(now - MAX_AGE_OF_DELETED_RECORDS);
final String profile = fromUri.getQueryParameter(BrowserContract.PARAM_PROFILE);
final SQLiteDatabase db = getWritableDatabaseForProfile(profile, isTest(fromUri));
final String[] ids;
final String limit = Long.toString(DELETED_RECORDS_PURGE_LIMIT, 10);
final Cursor cursor = db.query(tableName, new String[] { CommonColumns._ID }, selection, null, null, null, null, limit);
try {
ids = new String[cursor.getCount()];
int i = 0;
while (cursor.moveToNext()) {
ids[i++] = Long.toString(cursor.getLong(0), 10);
}
} finally {
cursor.close();
}
final String inClause = computeSQLInClause(ids.length,
CommonColumns._ID);
db.delete(tableName, inClause, ids);
protected static boolean shouldShowDeleted(Uri uri) {
String showDeleted = uri.getQueryParameter(BrowserContract.PARAM_SHOW_DELETED);
return !TextUtils.isEmpty(showDeleted);
}
/**
* Indicates whether an insertion should be made if a record doesn't
* exist, based on the URI.
* @param uri query URI
*/
protected static boolean shouldUpdateOrInsert(Uri uri) {
String insertIfNeeded = uri.getQueryParameter(BrowserContract.PARAM_INSERT_IF_NEEDED);
return Boolean.parseBoolean(insertIfNeeded);
}
/**
* Indicates whether query is a test based on the URI.
* @param uri query URI
*/
protected static boolean isTest(Uri uri) {
if (uri == null) {
return false;
}
String isTest = uri.getQueryParameter(BrowserContract.PARAM_IS_TEST);
return !TextUtils.isEmpty(isTest);
}
/**
* Return true of the query is from Firefox Sync.
* @param uri query URI
*/
protected static boolean isCallerSync(Uri uri) {
String isSync = uri.getQueryParameter(BrowserContract.PARAM_IS_SYNC);
return !TextUtils.isEmpty(isSync);
}
// Calculate these once, at initialization. isLoggable is too expensive to
// have in-line in each log call.
private static boolean logDebug = Log.isLoggable(LOGTAG, Log.DEBUG);
private static boolean logVerbose = Log.isLoggable(LOGTAG, Log.VERBOSE);
protected static void trace(String message) {
if (logVerbose) {
Log.v(LOGTAG, message);
@ -514,4 +367,4 @@ public abstract class TransactionalProvider<T extends SQLiteOpenHelper> extends
Log.d(LOGTAG, message);
}
}
}
}

View File

@ -19,7 +19,6 @@ import org.mozilla.gecko.db.BrowserContract.History;
import org.mozilla.gecko.db.BrowserContract.Schema;
import org.mozilla.gecko.db.BrowserContract.SyncColumns;
import org.mozilla.gecko.db.BrowserContract.Thumbnails;
import org.mozilla.gecko.db.BrowserContract.URLColumns;
import org.mozilla.gecko.sync.Utils;
import android.app.SearchManager;
@ -27,7 +26,6 @@ import android.content.ContentProviderOperation;
import android.content.ContentProviderResult;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.OperationApplicationException;
import android.content.UriMatcher;
import android.database.Cursor;
@ -40,7 +38,7 @@ import android.net.Uri;
import android.text.TextUtils;
import android.util.Log;
public class BrowserProvider extends TransactionalProvider<BrowserDatabaseHelper> {
public class BrowserProvider extends SharedBrowserDatabaseProvider {
private static final String LOGTAG = "GeckoBrowserProvider";
// How many records to reposition in a single query.
@ -815,21 +813,6 @@ public class BrowserProvider extends TransactionalProvider<BrowserDatabaseHelper
return cursor;
}
private static int getUrlCount(SQLiteDatabase db, String table, String url) {
final Cursor c = db.query(table, new String[] { "COUNT(*)" },
URLColumns.URL + " = ?", new String[] { url },
null, null, null);
try {
if (c.moveToFirst()) {
return c.getInt(0);
}
} finally {
c.close();
}
return 0;
}
/**
* Update the positions of bookmarks in batches.
*
@ -1305,7 +1288,7 @@ public class BrowserProvider extends TransactionalProvider<BrowserDatabaseHelper
// it if we can.
final int updated = db.update(TABLE_HISTORY, values, selection, selectionArgs);
try {
cleanupSomeDeletedRecords(uri, History.CONTENT_URI, TABLE_HISTORY);
cleanUpSomeDeletedRecords(uri, TABLE_HISTORY);
} catch (Exception e) {
// We don't care.
Log.e(LOGTAG, "Unable to clean up deleted history records: ", e);
@ -1334,7 +1317,7 @@ public class BrowserProvider extends TransactionalProvider<BrowserDatabaseHelper
// require the transaction to be upgraded from a reader to a writer.
final int updated = updateBookmarks(uri, values, selection, selectionArgs);
try {
cleanupSomeDeletedRecords(uri, Bookmarks.CONTENT_URI, TABLE_BOOKMARKS);
cleanUpSomeDeletedRecords(uri, TABLE_BOOKMARKS);
} catch (Exception e) {
// We don't care.
Log.e(LOGTAG, "Unable to clean up deleted bookmark records: ", e);
@ -1461,15 +1444,4 @@ public class BrowserProvider extends TransactionalProvider<BrowserDatabaseHelper
return results;
}
@Override
protected BrowserDatabaseHelper createDatabaseHelper(
Context context, String databasePath) {
return new BrowserDatabaseHelper(context, databasePath);
}
@Override
protected String getDatabaseName() {
return BrowserDatabaseHelper.DATABASE_NAME;
}
}

View File

@ -0,0 +1,50 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.db;
import org.mozilla.gecko.db.PerProfileDatabases.DatabaseHelperFactory;
import android.content.Context;
import android.database.sqlite.SQLiteOpenHelper;
/**
* Abstract class containing methods needed to make a SQLite-based content
* provider with a database helper of type T, where one database helper is
* held per profile.
*/
public abstract class PerProfileDatabaseProvider<T extends SQLiteOpenHelper> extends AbstractPerProfileDatabaseProvider {
private PerProfileDatabases<T> databases;
@Override
protected PerProfileDatabases<T> getDatabases() {
return databases;
}
protected abstract String getDatabaseName();
/**
* Creates and returns an instance of the appropriate DB helper.
*
* @param context to use to create the database helper
* @param databasePath path to the DB file
* @return instance of the database helper
*/
protected abstract T createDatabaseHelper(Context context, String databasePath);
@Override
public boolean onCreate() {
synchronized (this) {
databases = new PerProfileDatabases<T>(
getContext(), getDatabaseName(), new DatabaseHelperFactory<T>() {
@Override
public T makeDatabaseHelper(Context context, String databasePath) {
return createDatabaseHelper(context, databasePath);
}
});
}
return true;
}
}

View File

@ -9,7 +9,6 @@ import org.mozilla.gecko.sync.Utils;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
@ -17,9 +16,7 @@ import android.database.sqlite.SQLiteQueryBuilder;
import android.net.Uri;
import android.text.TextUtils;
public class ReadingListProvider extends TransactionalProvider<BrowserDatabaseHelper> {
private static final String LOGTAG = "GeckoReadingListProv";
public class ReadingListProvider extends SharedBrowserDatabaseProvider {
static final String TABLE_READING_LIST = ReadingListItems.TABLE_NAME;
static final int ITEMS = 101;
@ -103,7 +100,7 @@ public class ReadingListProvider extends TransactionalProvider<BrowserDatabaseHe
ContentValues values = new ContentValues();
values.put(ReadingListItems.IS_DELETED, 1);
cleanupSomeDeletedRecords(uri, ReadingListItems.CONTENT_URI, TABLE_READING_LIST);
cleanUpSomeDeletedRecords(uri, TABLE_READING_LIST);
return updateItems(uri, values, selection, selectionArgs);
}
@ -247,15 +244,4 @@ public class ReadingListProvider extends TransactionalProvider<BrowserDatabaseHe
debug("URI has unrecognized type: " + uri);
return null;
}
@Override
protected BrowserDatabaseHelper createDatabaseHelper(Context context,
String databasePath) {
return new BrowserDatabaseHelper(context, databasePath);
}
@Override
protected String getDatabaseName() {
return BrowserDatabaseHelper.DATABASE_NAME;
}
}

View File

@ -0,0 +1,115 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.db;
import org.mozilla.gecko.db.BrowserContract.CommonColumns;
import org.mozilla.gecko.db.BrowserContract.SyncColumns;
import org.mozilla.gecko.db.PerProfileDatabases.DatabaseHelperFactory;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.net.Uri;
import android.util.Log;
/**
* A ContentProvider subclass that provides per-profile browser.db access
* that can be safely shared between multiple providers.
*
* If multiple ContentProvider classes wish to share a database, it's
* vitally important that they use the same SQLiteOpenHelpers for access.
*
* Failure to do so can cause accidental concurrent writes, with the result
* being unexpected SQLITE_BUSY errors.
*
* This class provides a static {@link PerProfileDatabases} instance, lazily
* initialized within {@link SharedBrowserDatabaseProvider#onCreate()}.
*/
public abstract class SharedBrowserDatabaseProvider extends AbstractPerProfileDatabaseProvider {
private static final String LOGTAG = SharedBrowserDatabaseProvider.class.getSimpleName();
private static PerProfileDatabases<BrowserDatabaseHelper> databases;
@Override
protected PerProfileDatabases<BrowserDatabaseHelper> getDatabases() {
return databases;
}
@Override
public boolean onCreate() {
// If necessary, do the shared DB work.
synchronized (SharedBrowserDatabaseProvider.class) {
if (databases != null) {
return true;
}
final DatabaseHelperFactory<BrowserDatabaseHelper> helperFactory = new DatabaseHelperFactory<BrowserDatabaseHelper>() {
@Override
public BrowserDatabaseHelper makeDatabaseHelper(Context context, String databasePath) {
return new BrowserDatabaseHelper(context, databasePath);
}
};
databases = new PerProfileDatabases<BrowserDatabaseHelper>(getContext(), BrowserDatabaseHelper.DATABASE_NAME, helperFactory);
}
return true;
}
/**
* Clean up some deleted records from the specified table.
*
* If called in an existing transaction, it is the caller's responsibility
* to ensure that the transaction is already upgraded to a writer, because
* this method issues a read followed by a write, and thus is potentially
* vulnerable to an unhandled SQLITE_BUSY failure during the upgrade.
*
* If not called in an existing transaction, no new explicit transaction
* will be begun.
*/
protected void cleanUpSomeDeletedRecords(Uri fromUri, String tableName) {
Log.d(LOGTAG, "Cleaning up deleted records from " + tableName);
// We clean up records marked as deleted that are older than a
// predefined max age. It's important not be too greedy here and
// remove only a few old deleted records at a time.
// we cleanup records marked as deleted that are older than a
// predefined max age. It's important not be too greedy here and
// remove only a few old deleted records at a time.
// Maximum age of deleted records to be cleaned up (20 days in ms)
final long MAX_AGE_OF_DELETED_RECORDS = 86400000 * 20;
// Number of records marked as deleted to be removed
final long DELETED_RECORDS_PURGE_LIMIT = 5;
// Android SQLite doesn't have LIMIT on DELETE. Instead, query for the
// IDs of matching rows, then delete them in one go.
final long now = System.currentTimeMillis();
final String selection = SyncColumns.IS_DELETED + " = 1 AND " +
SyncColumns.DATE_MODIFIED + " <= " +
(now - MAX_AGE_OF_DELETED_RECORDS);
final String profile = fromUri.getQueryParameter(BrowserContract.PARAM_PROFILE);
final SQLiteDatabase db = getWritableDatabaseForProfile(profile, isTest(fromUri));
final String[] ids;
final String limit = Long.toString(DELETED_RECORDS_PURGE_LIMIT, 10);
final Cursor cursor = db.query(tableName, new String[] { CommonColumns._ID }, selection, null, null, null, null, limit);
try {
ids = new String[cursor.getCount()];
int i = 0;
while (cursor.moveToNext()) {
ids[i++] = Long.toString(cursor.getLong(0), 10);
}
} finally {
cursor.close();
}
final String inClause = computeSQLInClause(ids.length,
CommonColumns._ID);
db.delete(tableName, inClause, ids);
}
}

View File

@ -4,37 +4,25 @@
package org.mozilla.gecko.db;
import java.io.File;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import org.mozilla.gecko.GeckoProfile;
import org.mozilla.gecko.db.BrowserContract.Clients;
import org.mozilla.gecko.db.BrowserContract.Tabs;
import org.mozilla.gecko.db.PerProfileDatabases.DatabaseHelperFactory;
import android.content.ContentProvider;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.SQLException;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.database.sqlite.SQLiteQueryBuilder;
import android.net.Uri;
import android.os.Build;
import android.text.TextUtils;
import android.util.Log;
public class TabsProvider extends ContentProvider {
private static final String LOGTAG = "GeckoTabsProvider";
private Context mContext;
private PerProfileDatabases<TabsDatabaseHelper> mDatabases;
public class TabsProvider extends PerProfileDatabaseProvider<TabsProvider.TabsDatabaseHelper> {
static final String DATABASE_NAME = "tabs.db";
static final int DATABASE_VERSION = 2;
@ -87,35 +75,10 @@ public class TabsProvider extends ContentProvider {
CLIENTS_PROJECTION_MAP = Collections.unmodifiableMap(map);
}
static final String selectColumn(String table, String column) {
private static final String selectColumn(String table, String column) {
return table + "." + column + " = ?";
}
// Calculate these once, at initialization. isLoggable is too expensive to
// have in-line in each log call.
private static boolean logDebug = Log.isLoggable(LOGTAG, Log.DEBUG);
private static boolean logVerbose = Log.isLoggable(LOGTAG, Log.VERBOSE);
protected static void trace(String message) {
if (logVerbose) {
Log.v(LOGTAG, message);
}
}
protected static void debug(String message) {
if (logDebug) {
Log.d(LOGTAG, message);
}
}
/**
* Return true of the query is from Firefox Sync.
* @param uri query URI
*/
public static boolean isCallerSync(Uri uri) {
String isSync = uri.getQueryParameter(BrowserContract.PARAM_IS_SYNC);
return !TextUtils.isEmpty(isSync);
}
final class TabsDatabaseHelper extends SQLiteOpenHelper {
public TabsDatabaseHelper(Context context, String databasePath) {
super(context, databasePath, null, DATABASE_VERSION);
@ -128,35 +91,34 @@ public class TabsProvider extends ContentProvider {
// Table for each tab on any client.
db.execSQL("CREATE TABLE " + TABLE_TABS + "(" +
Tabs._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
Tabs.CLIENT_GUID + " TEXT," +
Tabs.TITLE + " TEXT," +
Tabs.URL + " TEXT," +
Tabs.HISTORY + " TEXT," +
Tabs.FAVICON + " TEXT," +
Tabs.LAST_USED + " INTEGER," +
Tabs.POSITION + " INTEGER" +
");");
Tabs._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
Tabs.CLIENT_GUID + " TEXT," +
Tabs.TITLE + " TEXT," +
Tabs.URL + " TEXT," +
Tabs.HISTORY + " TEXT," +
Tabs.FAVICON + " TEXT," +
Tabs.LAST_USED + " INTEGER," +
Tabs.POSITION + " INTEGER" +
");");
// Indices on CLIENT_GUID and POSITION.
db.execSQL("CREATE INDEX " + INDEX_TABS_GUID + " ON " + TABLE_TABS + "("
+ Tabs.CLIENT_GUID + ")");
db.execSQL("CREATE INDEX " + INDEX_TABS_POSITION + " ON " + TABLE_TABS + "("
+ Tabs.POSITION + ")");
db.execSQL("CREATE INDEX " + INDEX_TABS_GUID +
" ON " + TABLE_TABS + "(" + Tabs.CLIENT_GUID + ")");
db.execSQL("CREATE INDEX " + INDEX_TABS_POSITION +
" ON " + TABLE_TABS + "(" + Tabs.POSITION + ")");
debug("Creating " + TABLE_CLIENTS + " table");
// Table for client's name-guid mapping.
db.execSQL("CREATE TABLE " + TABLE_CLIENTS + "(" +
Clients.GUID + " TEXT PRIMARY KEY," +
Clients.NAME + " TEXT," +
Clients.LAST_MODIFIED + " INTEGER" +
");");
Clients.GUID + " TEXT PRIMARY KEY," +
Clients.NAME + " TEXT," +
Clients.LAST_MODIFIED + " INTEGER" +
");");
// Index on GUID.
db.execSQL("CREATE INDEX " + INDEX_CLIENTS_GUID + " ON " + TABLE_CLIENTS + "("
+ Clients.GUID + ")");
db.execSQL("CREATE INDEX " + INDEX_CLIENTS_GUID +
" ON " + TABLE_CLIENTS + "(" + Clients.GUID + ")");
createLocalClient(db);
}
@ -173,7 +135,7 @@ public class TabsProvider extends ContentProvider {
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
debug("Upgrading tabs.db: " + db.getPath() + " from " +
oldVersion + " to " + newVersion);
oldVersion + " to " + newVersion);
// We have to do incremental upgrades until we reach the current
// database schema version.
@ -189,73 +151,20 @@ public class TabsProvider extends ContentProvider {
@Override
public void onOpen(SQLiteDatabase db) {
debug("Opening tabs.db: " + db.getPath());
db.rawQuery("PRAGMA synchronous=OFF", null).close();
Cursor cursor = null;
try {
cursor = db.rawQuery("PRAGMA synchronous=OFF", null);
} finally {
if (cursor != null)
cursor.close();
}
// From Honeycomb on, it's possible to run several db
// commands in parallel using multiple connections.
if (Build.VERSION.SDK_INT >= 11) {
if (shouldUseTransactions()) {
db.enableWriteAheadLogging();
db.setLockingEnabled(false);
} else {
// Pre-Honeycomb, we can do some lesser optimizations.
cursor = null;
try {
cursor = db.rawQuery("PRAGMA journal_mode=PERSIST", null);
} finally {
if (cursor != null)
cursor.close();
}
return;
}
// If we're not using transactions (in particular, prior to
// Honeycomb), then we can do some lesser optimizations.
db.rawQuery("PRAGMA journal_mode=PERSIST", null).close();
}
}
private SQLiteDatabase getReadableDatabase(Uri uri) {
trace("Getting readable database for URI: " + uri);
String profile = null;
if (uri != null)
profile = uri.getQueryParameter(BrowserContract.PARAM_PROFILE);
return mDatabases.getDatabaseHelperForProfile(profile).getReadableDatabase();
}
private SQLiteDatabase getWritableDatabase(Uri uri) {
trace("Getting writable database for URI: " + uri);
String profile = null;
if (uri != null)
profile = uri.getQueryParameter(BrowserContract.PARAM_PROFILE);
return mDatabases.getDatabaseHelperForProfile(profile).getWritableDatabase();
}
@Override
public boolean onCreate() {
debug("Creating TabsProvider");
synchronized (this) {
mContext = getContext();
mDatabases = new PerProfileDatabases<TabsDatabaseHelper>(
getContext(), DATABASE_NAME, new DatabaseHelperFactory<TabsDatabaseHelper>() {
@Override
public TabsDatabaseHelper makeDatabaseHelper(Context context, String databasePath) {
return new TabsDatabaseHelper(context, databasePath);
}
});
}
return true;
}
@Override
public String getType(Uri uri) {
final int match = URI_MATCHER.match(uri);
@ -285,35 +194,6 @@ public class TabsProvider extends ContentProvider {
return null;
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
trace("Calling delete on URI: " + uri);
final SQLiteDatabase db = getWritableDatabase(uri);
int deleted = 0;
if (Build.VERSION.SDK_INT >= 11) {
trace("Beginning delete transaction: " + uri);
db.beginTransaction();
try {
deleted = deleteInTransaction(uri, selection, selectionArgs);
db.setTransactionSuccessful();
trace("Successful delete transaction: " + uri);
} finally {
db.endTransaction();
}
} else {
deleted = deleteInTransaction(uri, selection, selectionArgs);
}
if (deleted > 0) {
final boolean shouldSyncToNetwork = !isCallerSync(uri);
getContext().getContentResolver().notifyChange(uri, null, shouldSyncToNetwork);
}
return deleted;
}
@SuppressWarnings("fallthrough")
public int deleteInTransaction(Uri uri, String selection, String[] selectionArgs) {
trace("Calling delete in transaction on URI: " + uri);
@ -355,35 +235,6 @@ public class TabsProvider extends ContentProvider {
return deleted;
}
@Override
public Uri insert(Uri uri, ContentValues values) {
trace("Calling insert on URI: " + uri);
final SQLiteDatabase db = getWritableDatabase(uri);
Uri result = null;
if (Build.VERSION.SDK_INT >= 11) {
trace("Beginning insert transaction: " + uri);
db.beginTransaction();
try {
result = insertInTransaction(uri, values);
db.setTransactionSuccessful();
trace("Successful insert transaction: " + uri);
} finally {
db.endTransaction();
}
} else {
result = insertInTransaction(uri, values);
}
if (result != null) {
final boolean shouldSyncToNetwork = !isCallerSync(uri);
getContext().getContentResolver().notifyChange(uri, null, shouldSyncToNetwork);
}
return result;
}
public Uri insertInTransaction(Uri uri, ContentValues values) {
trace("Calling insert in transaction on URI: " + uri);
@ -416,38 +267,7 @@ public class TabsProvider extends ContentProvider {
return null;
}
@Override
public int update(Uri uri, ContentValues values, String selection,
String[] selectionArgs) {
trace("Calling update on URI: " + uri);
final SQLiteDatabase db = getWritableDatabase(uri);
int updated = 0;
if (Build.VERSION.SDK_INT >= 11) {
trace("Beginning update transaction: " + uri);
db.beginTransaction();
try {
updated = updateInTransaction(uri, values, selection, selectionArgs);
db.setTransactionSuccessful();
trace("Successful update transaction: " + uri);
} finally {
db.endTransaction();
}
} else {
updated = updateInTransaction(uri, values, selection, selectionArgs);
}
if (updated > 0) {
final boolean shouldSyncToNetwork = !isCallerSync(uri);
getContext().getContentResolver().notifyChange(uri, null, shouldSyncToNetwork);
}
return updated;
}
public int updateInTransaction(Uri uri, ContentValues values, String selection,
String[] selectionArgs) {
public int updateInTransaction(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
trace("Calling update in transaction on URI: " + uri);
int match = URI_MATCHER.match(uri);
@ -537,10 +357,8 @@ public class TabsProvider extends ContentProvider {
}
trace("Running built query.");
Cursor cursor = qb.query(db, projection, selection, selectionArgs, null,
null, sortOrder, limit);
cursor.setNotificationUri(getContext().getContentResolver(),
BrowserContract.TABS_AUTHORITY_URI);
final Cursor cursor = qb.query(db, projection, selection, selectionArgs, null, null, sortOrder, limit);
cursor.setNotificationUri(getContext().getContentResolver(), BrowserContract.TABS_AUTHORITY_URI);
return cursor;
}
@ -549,7 +367,7 @@ public class TabsProvider extends ContentProvider {
trace("Updating tabs on URI: " + uri);
final SQLiteDatabase db = getWritableDatabase(uri);
beginWrite(db);
return db.update(table, values, selection, selectionArgs);
}
@ -557,46 +375,17 @@ public class TabsProvider extends ContentProvider {
debug("Deleting tabs for URI: " + uri);
final SQLiteDatabase db = getWritableDatabase(uri);
beginWrite(db);
return db.delete(table, selection, selectionArgs);
}
@Override
public int bulkInsert(Uri uri, ContentValues[] values) {
if (values == null)
return 0;
protected TabsDatabaseHelper createDatabaseHelper(Context context, String databasePath) {
return new TabsDatabaseHelper(context, databasePath);
}
int numValues = values.length;
int successes = 0;
final SQLiteDatabase db = getWritableDatabase(uri);
db.beginTransaction();
try {
for (int i = 0; i < numValues; i++) {
try {
insertInTransaction(uri, values[i]);
successes++;
} catch (SQLException e) {
Log.e(LOGTAG, "SQLException in bulkInsert", e);
// Restart the transaction to continue insertions.
db.setTransactionSuccessful();
db.endTransaction();
db.beginTransaction();
}
}
trace("Flushing DB bulkinsert...");
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
if (successes > 0) {
final boolean shouldSyncToNetwork = !isCallerSync(uri);
mContext.getContentResolver().notifyChange(uri, null, shouldSyncToNetwork);
}
return successes;
@Override
protected String getDatabaseName() {
return DATABASE_NAME;
}
}

View File

@ -115,7 +115,14 @@ public class FxAccountUpdateCredentialsActivity extends FxAccountAbstractSetupAc
@Override
public void handleFailure(FxAccountClientRemoteException e) {
// TODO On isUpgradeRequired, transition to Doghouse state.
if (e.isUpgradeRequired()) {
Logger.error(LOG_TAG, "Got upgrade required from remote server; transitioning Firefox Account to Doghouse state.");
final State state = fxAccount.getState();
fxAccount.setState(state.makeDoghouseState());
// The status activity will say that the user needs to upgrade.
redirectToActivity(FxAccountStatusActivity.class);
return;
}
showRemoteError(e, R.string.fxaccount_update_credentials_unknown_error);
}

View File

@ -398,7 +398,7 @@ public class AndroidFxAccount {
}
public void enableSyncing() {
Logger.info(LOG_TAG, "Enabling sync for account named like " + Utils.obfuscateEmail(getEmail()));
Logger.info(LOG_TAG, "Enabling sync for account named like " + getObfuscatedEmail());
for (String authority : new String[] { BrowserContract.AUTHORITY }) {
ContentResolver.setSyncAutomatically(account, authority, true);
ContentResolver.setIsSyncable(account, authority, 1);
@ -406,14 +406,14 @@ public class AndroidFxAccount {
}
public void disableSyncing() {
Logger.info(LOG_TAG, "Disabling sync for account named like " + Utils.obfuscateEmail(getEmail()));
Logger.info(LOG_TAG, "Disabling sync for account named like " + getObfuscatedEmail());
for (String authority : new String[] { BrowserContract.AUTHORITY }) {
ContentResolver.setSyncAutomatically(account, authority, false);
}
}
public void requestSync(Bundle extras) {
Logger.info(LOG_TAG, "Requesting sync for account named like " + Utils.obfuscateEmail(getEmail()) +
Logger.info(LOG_TAG, "Requesting sync for account named like " + getObfuscatedEmail() +
(extras.isEmpty() ? "." : "; has extras."));
for (String authority : new String[] { BrowserContract.AUTHORITY }) {
ContentResolver.requestSync(account, authority, extras);
@ -424,7 +424,7 @@ public class AndroidFxAccount {
if (state == null) {
throw new IllegalArgumentException("state must not be null");
}
Logger.info(LOG_TAG, "Moving account named like " + Utils.obfuscateEmail(getEmail()) +
Logger.info(LOG_TAG, "Moving account named like " + getObfuscatedEmail() +
" to state " + state.getStateLabel().toString());
updateBundleValue(BUNDLE_KEY_STATE_LABEL, state.getStateLabel().name());
updateBundleValue(BUNDLE_KEY_STATE, state.toJSONObject().toJSONString());
@ -475,6 +475,17 @@ public class AndroidFxAccount {
return account.name;
}
/**
* Return the Firefox Account's local email address, obfuscated.
* <p>
* Use this when logging.
*
* @return local email address, obfuscated.
*/
public String getObfuscatedEmail() {
return Utils.obfuscateEmail(account.name);
}
/**
* Create an intent announcing that a Firefox account will be deleted.
*

View File

@ -0,0 +1,132 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.fxa.receivers;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import org.mozilla.gecko.background.common.log.Logger;
import org.mozilla.gecko.fxa.FirefoxAccounts;
import org.mozilla.gecko.fxa.FxAccountConstants;
import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
import org.mozilla.gecko.fxa.login.State;
import org.mozilla.gecko.fxa.login.State.StateLabel;
import org.mozilla.gecko.sync.Utils;
import android.accounts.Account;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
/**
* A receiver that takes action when our Android package is upgraded (replaced).
*/
public class FxAccountUpgradeReceiver extends BroadcastReceiver {
private static final String LOG_TAG = FxAccountUpgradeReceiver.class.getSimpleName();
/**
* Produce a list of Runnable instances to be executed sequentially on
* upgrade.
* <p>
* Each Runnable will be executed sequentially on a background thread. Any
* unchecked Exception thrown will be caught and ignored.
*
* @param context Android context.
* @return list of Runnable instances.
*/
protected List<Runnable> onUpgradeRunnables(Context context) {
List<Runnable> runnables = new LinkedList<Runnable>();
runnables.add(new MaybeUnpickleRunnable(context));
// Recovering accounts that are in the Doghouse should happen *after* we
// unpickle any accounts saved to disk.
runnables.add(new AdvanceFromDoghouseRunnable(context));
return runnables;
}
@Override
public void onReceive(final Context context, Intent intent) {
Logger.setThreadLogTag(FxAccountConstants.GLOBAL_LOG_TAG);
Logger.info(LOG_TAG, "Upgrade broadcast received.");
// Iterate Runnable instances one at a time.
final Executor executor = Executors.newSingleThreadExecutor();
for (final Runnable runnable : onUpgradeRunnables(context)) {
executor.execute(new Runnable() {
@Override
public void run() {
try {
runnable.run();
} catch (Exception e) {
// We really don't want to throw on a background thread, so we
// catch, log, and move on.
Logger.error(LOG_TAG, "Got exception executing background upgrade Runnable; ignoring.", e);
}
}
});
}
}
/**
* A Runnable that tries to unpickle any pickled Firefox Accounts.
*/
protected static class MaybeUnpickleRunnable implements Runnable {
protected final Context context;
public MaybeUnpickleRunnable(Context context) {
this.context = context;
}
@Override
public void run() {
// Querying the accounts will unpickle any pickled Firefox Account.
Logger.info(LOG_TAG, "Trying to unpickle any pickled Firefox Account.");
FirefoxAccounts.getFirefoxAccounts(context);
}
}
/**
* A Runnable that tries to advance existing Firefox Accounts that are in the
* Doghouse state to the Separated state.
* <p>
* This is our main deprecation-and-upgrade mechanism: in some way, the
* Account gets moved to the Doghouse state. If possible, an upgraded version
* of the package advances to Separated, prompting the user to re-connect the
* Account.
*/
protected static class AdvanceFromDoghouseRunnable implements Runnable {
protected final Context context;
public AdvanceFromDoghouseRunnable(Context context) {
this.context = context;
}
@Override
public void run() {
final Account[] accounts = FirefoxAccounts.getFirefoxAccounts(context);
Logger.info(LOG_TAG, "Trying to advance " + accounts.length + " existing Firefox Accounts from the Doghouse to Separated (if necessary).");
for (Account account : accounts) {
try {
final AndroidFxAccount fxAccount = new AndroidFxAccount(context, account);
// For great debugging.
if (FxAccountConstants.LOG_PERSONAL_INFORMATION) {
fxAccount.dump();
}
State state = fxAccount.getState();
if (state == null || state.getStateLabel() != StateLabel.Doghouse) {
Logger.debug(LOG_TAG, "Account named like " + Utils.obfuscateEmail(account.name) + " is not in the Doghouse; skipping.");
continue;
}
Logger.debug(LOG_TAG, "Account named like " + Utils.obfuscateEmail(account.name) + " is in the Doghouse; advancing to Separated.");
fxAccount.setState(state.makeSeparatedState());
} catch (Exception e) {
Logger.warn(LOG_TAG, "Got exception trying to advance account named like " + Utils.obfuscateEmail(account.name) +
" from Doghouse to Separated state; ignoring.", e);
}
}
}
}
}

View File

@ -38,8 +38,8 @@ class FramePanelLayout extends PanelLayout {
Log.d(LOGTAG, "Loading");
if (mChildView instanceof DatasetBacked) {
// TODO: get filter from ViewEntry
DatasetRequest request = new DatasetRequest(mChildConfig.getDatasetId(), null);
final FilterDetail filter = new FilterDetail(mChildConfig.getFilter(), null);
final DatasetRequest request = new DatasetRequest(mChildConfig.getDatasetId(), filter);
Log.d(LOGTAG, "Requesting child request: " + request);
requestDataset(request);
}

View File

@ -608,12 +608,14 @@ public final class HomeConfig {
private final ItemType mItemType;
private final ItemHandler mItemHandler;
private final String mBackImageUrl;
private final String mFilter;
private static final String JSON_KEY_TYPE = "type";
private static final String JSON_KEY_DATASET = "dataset";
private static final String JSON_KEY_ITEM_TYPE = "itemType";
private static final String JSON_KEY_ITEM_HANDLER = "itemHandler";
private static final String JSON_KEY_BACK_IMAGE_URL = "backImageUrl";
private static final String JSON_KEY_FILTER = "filter";
public ViewConfig(JSONObject json) throws JSONException, IllegalArgumentException {
mType = ViewType.fromId(json.getString(JSON_KEY_TYPE));
@ -621,6 +623,7 @@ public final class HomeConfig {
mItemType = ItemType.fromId(json.getString(JSON_KEY_ITEM_TYPE));
mItemHandler = ItemHandler.fromId(json.getString(JSON_KEY_ITEM_HANDLER));
mBackImageUrl = json.optString(JSON_KEY_BACK_IMAGE_URL, null);
mFilter = json.optString(JSON_KEY_FILTER, null);
validate();
}
@ -632,6 +635,7 @@ public final class HomeConfig {
mItemType = (ItemType) in.readParcelable(getClass().getClassLoader());
mItemHandler = (ItemHandler) in.readParcelable(getClass().getClassLoader());
mBackImageUrl = in.readString();
mFilter = in.readString();
validate();
}
@ -642,17 +646,19 @@ public final class HomeConfig {
mItemType = viewConfig.mItemType;
mItemHandler = viewConfig.mItemHandler;
mBackImageUrl = viewConfig.mBackImageUrl;
mFilter = viewConfig.mFilter;
validate();
}
public ViewConfig(ViewType type, String datasetId, ItemType itemType,
ItemHandler itemHandler, String backImageUrl) {
ItemHandler itemHandler, String backImageUrl, String filter) {
mType = type;
mDatasetId = datasetId;
mItemType = itemType;
mItemHandler = itemHandler;
mBackImageUrl = backImageUrl;
mFilter = filter;
validate();
}
@ -695,6 +701,10 @@ public final class HomeConfig {
return mBackImageUrl;
}
public String getFilter() {
return mFilter;
}
public JSONObject toJSON() throws JSONException {
final JSONObject json = new JSONObject();
@ -707,6 +717,10 @@ public final class HomeConfig {
json.put(JSON_KEY_BACK_IMAGE_URL, mBackImageUrl);
}
if (!TextUtils.isEmpty(mFilter)) {
json.put(JSON_KEY_FILTER, mFilter);
}
return json;
}
@ -722,6 +736,7 @@ public final class HomeConfig {
dest.writeParcelable(mItemType, 0);
dest.writeParcelable(mItemHandler, 0);
dest.writeString(mBackImageUrl);
dest.writeString(mFilter);
}
public static final Creator<ViewConfig> CREATOR = new Creator<ViewConfig>() {

View File

@ -401,9 +401,9 @@ abstract class PanelLayout extends FrameLayout {
if (mFilterStack == null) {
mFilterStack = new LinkedList<FilterDetail>();
// Initialize with a null filter.
// TODO: use initial filter from ViewConfig
mFilterStack.push(new FilterDetail(null, mPanelConfig.getTitle()));
// Initialize with the initial filter.
mFilterStack.push(new FilterDetail(mViewConfig.getFilter(),
mPanelConfig.getTitle()));
}
mFilterStack.push(filter);

View File

@ -117,6 +117,8 @@ gbjar.sources += [
'ContextGetter.java',
'CustomEditText.java',
'DataReportingNotification.java',
'db/AbstractPerProfileDatabaseProvider.java',
'db/AbstractTransactionalProvider.java',
'db/BrowserContract.java',
'db/BrowserDatabaseHelper.java',
'db/BrowserDB.java',
@ -126,11 +128,12 @@ gbjar.sources += [
'db/HomeProvider.java',
'db/LocalBrowserDB.java',
'db/PasswordsProvider.java',
'db/PerProfileDatabaseProvider.java',
'db/PerProfileDatabases.java',
'db/ReadingListProvider.java',
'db/SharedBrowserDatabaseProvider.java',
'db/SQLiteBridgeContentProvider.java',
'db/TabsProvider.java',
'db/TransactionalProvider.java',
'Distribution.java',
'DoorHangerPopup.java',
'DynamicToolbar.java',

Binary file not shown.

Before

Width:  |  Height:  |  Size: 451 B

After

Width:  |  Height:  |  Size: 538 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 426 B

After

Width:  |  Height:  |  Size: 479 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 572 B

After

Width:  |  Height:  |  Size: 663 B

View File

@ -14,7 +14,9 @@ import org.mozilla.gecko.R;
import org.mozilla.gecko.background.common.log.Logger;
import org.mozilla.gecko.fxa.FxAccountConstants;
import org.mozilla.gecko.fxa.activities.FxAccountGetStartedActivity;
import org.mozilla.gecko.fxa.activities.FxAccountStatusActivity;
import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
import org.mozilla.gecko.fxa.login.State.Action;
import org.mozilla.gecko.sync.CommandProcessor;
import org.mozilla.gecko.sync.CommandRunner;
import org.mozilla.gecko.sync.GlobalSession;
@ -58,17 +60,17 @@ public class SendTabActivity extends Activity {
void syncClientsStage();
}
public class FxAccountTabSender implements TabSender {
private final AndroidFxAccount account;
private static class FxAccountTabSender implements TabSender {
private final AndroidFxAccount fxAccount;
public FxAccountTabSender(Context context, Account account) {
this.account = new AndroidFxAccount(context, account);
public FxAccountTabSender(Context context, AndroidFxAccount fxAccount) {
this.fxAccount = fxAccount;
}
@Override
public String getAccountGUID() {
try {
final SharedPreferences prefs = this.account.getSyncPrefs();
final SharedPreferences prefs = this.fxAccount.getSyncPrefs();
return prefs.getString(SyncConfiguration.PREF_ACCOUNT_GUID, null);
} catch (Exception e) {
Logger.warn(LOG_TAG, "Could not get Firefox Account parameters or preferences; aborting.");
@ -81,7 +83,7 @@ public class SendTabActivity extends Activity {
final Bundle extras = new Bundle();
Utils.putStageNamesToSync(extras, CLIENTS_STAGE, null);
extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true);
this.account.requestSync(extras);
this.fxAccount.requestSync(extras);
}
}
@ -89,6 +91,7 @@ public class SendTabActivity extends Activity {
private final Account account;
private final AccountManager accountManager;
private final Context context;
private Sync11TabSender(Context context, Account syncAccount, AccountManager accountManager) {
this.context = context;
this.account = syncAccount;
@ -224,7 +227,17 @@ public class SendTabActivity extends Activity {
final Account[] fxAccounts = accountManager.getAccountsByType(FxAccountConstants.ACCOUNT_TYPE);
if (fxAccounts.length > 0) {
this.tabSender = new FxAccountTabSender(applicationContext, fxAccounts[0]);
final AndroidFxAccount fxAccount = new AndroidFxAccount(applicationContext, fxAccounts[0]);
if (fxAccount.getState().getNeededAction() != Action.None) {
// We have a Firefox Account, but it's definitely not able to send a tab
// right now. Redirect to the status activity.
Logger.warn(LOG_TAG, "Firefox Account named like " + fxAccount.getObfuscatedEmail() +
" needs action before it can send a tab; redirecting to status activity.");
redirectToNewTask(FxAccountStatusActivity.class, false);
return;
}
this.tabSender = new FxAccountTabSender(applicationContext, fxAccount);
Logger.info(LOG_TAG, "Allowing tab send for Firefox Account.");
registerDisplayURICommand();
@ -241,10 +254,7 @@ public class SendTabActivity extends Activity {
}
// Offer to set up a Firefox Account, and finish this activity.
final Intent intent = new Intent(applicationContext, FxAccountGetStartedActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
finish();
redirectToNewTask(FxAccountGetStartedActivity.class, false);
}
private static void registerDisplayURICommand() {
@ -379,4 +389,15 @@ public class SendTabActivity extends Activity {
}
return out;
}
// Adapted from FxAccountAbstractActivity.
protected void redirectToNewTask(Class<? extends Activity> activityClass, boolean success) {
Intent intent = new Intent(this, activityClass);
// Per http://stackoverflow.com/a/8992365, this triggers a known bug with
// the soft keyboard not being shown for the started activity. Why, Android, why?
intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
notifyAndFinish(success);
}
}

View File

@ -33,6 +33,7 @@ public class WebViewActivity extends SyncActivity {
if (uri == null) {
Logger.debug(LOG_TAG, "No URI passed to display.");
finish();
return;
}
WebView wv = (WebView) findViewById(R.id.web_engine);

View File

@ -1,24 +1,23 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.tests;
import com.jayway.android.robotium.solo.Condition;
import org.mozilla.gecko.*;
import java.util.ArrayList;
import org.mozilla.gecko.Actions;
import android.content.ContentResolver;
import android.database.Cursor;
import android.net.Uri;
import android.support.v4.view.ViewPager;
import android.text.TextUtils;
import android.view.View;
import android.view.ViewGroup;
import android.widget.GridView;
import android.widget.LinearLayout;
import android.widget.TabWidget;
import android.widget.ListAdapter;
import android.widget.ListView;
import android.widget.TabWidget;
import android.widget.TextView;
import java.util.ArrayList;
import java.util.Arrays;
import com.jayway.android.robotium.solo.Condition;
/**
* This class is an extension of BaseTest that helps with interaction with about:home
@ -36,7 +35,7 @@ abstract class AboutHomeTest extends PixelTest {
@Override
protected void setUp() throws Exception {
public void setUp() throws Exception {
super.setUp();
if (aboutHomeTabs.size() < 4) {

View File

@ -0,0 +1,57 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.tests;
import java.util.Map;
import org.mozilla.gecko.Assert;
import org.mozilla.gecko.FennecInstrumentationTestRunner;
import org.mozilla.gecko.FennecMochitestAssert;
import org.mozilla.gecko.FennecNativeDriver;
import org.mozilla.gecko.FennecTalosAssert;
import android.app.Activity;
import android.test.ActivityInstrumentationTestCase2;
public abstract class BaseRobocopTest extends ActivityInstrumentationTestCase2<Activity> {
public static final int TEST_MOCHITEST = 0;
public static final int TEST_TALOS = 1;
protected static final String TARGET_PACKAGE_ID = "org.mozilla.gecko";
protected Assert mAsserter;
protected String mLogFile;
protected Map<?, ?> mConfig;
protected String mRootPath;
public BaseRobocopTest(Class<Activity> activityClass) {
super(activityClass);
}
@SuppressWarnings("deprecation")
public BaseRobocopTest(String targetPackageId, Class<Activity> activityClass) {
super(targetPackageId, activityClass);
}
protected abstract int getTestType();
@Override
protected void setUp() throws Exception {
// Load config file from root path (set up by Python script).
mRootPath = FennecInstrumentationTestRunner.getFennecArguments().getString("deviceroot");
String configFile = FennecNativeDriver.getFile(mRootPath + "/robotium.config");
mConfig = FennecNativeDriver.convertTextToTable(configFile);
mLogFile = (String) mConfig.get("logfile");
// Initialize the asserter.
if (getTestType() == TEST_TALOS) {
mAsserter = new FennecTalosAssert();
} else {
mAsserter = new FennecMochitestAssert();
}
mAsserter.setLogFile(mLogFile);
mAsserter.setTestName(this.getClass().getName());
}
}

View File

@ -1,63 +1,62 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.tests;
import com.jayway.android.robotium.solo.Condition;
import com.jayway.android.robotium.solo.Solo;
import org.mozilla.gecko.*;
import org.mozilla.gecko.GeckoThread.LaunchState;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.ArrayList;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.mozilla.gecko.Actions;
import org.mozilla.gecko.Driver;
import org.mozilla.gecko.Element;
import org.mozilla.gecko.FennecNativeActions;
import org.mozilla.gecko.FennecNativeDriver;
import org.mozilla.gecko.GeckoAppShell;
import org.mozilla.gecko.GeckoThread;
import org.mozilla.gecko.GeckoThread.LaunchState;
import org.mozilla.gecko.R;
import org.mozilla.gecko.RobocopUtils;
import org.mozilla.gecko.Tabs;
import android.app.Activity;
import android.app.Instrumentation;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.ContentUris;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.res.AssetManager;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.os.SystemClock;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentActivity;
import android.support.v4.app.FragmentManager;
import android.text.InputType;
import android.text.TextUtils;
import android.test.ActivityInstrumentationTestCase2;
import android.util.DisplayMetrics;
import android.view.inputmethod.InputMethodManager;
import android.view.KeyEvent;
import android.view.View;
import android.view.inputmethod.InputMethodManager;
import android.widget.AdapterView;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ListAdapter;
import android.widget.ListView;
import android.widget.TextView;
import java.io.File;
import java.io.InputStream;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.HashMap;
import com.jayway.android.robotium.solo.Condition;
import com.jayway.android.robotium.solo.Solo;
/**
* A convenient base class suitable for most Robocop tests.
*/
abstract class BaseTest extends ActivityInstrumentationTestCase2<Activity> {
public static final int TEST_MOCHITEST = 0;
public static final int TEST_TALOS = 1;
private static final String TARGET_PACKAGE_ID = "org.mozilla.gecko";
@SuppressWarnings("unchecked")
abstract class BaseTest extends BaseRobocopTest {
private static final String LAUNCH_ACTIVITY_FULL_CLASSNAME = TestConstants.ANDROID_PACKAGE_NAME + ".App";
private static final int VERIFY_URL_TIMEOUT = 2000;
private static final int MAX_LIST_ATTEMPTS = 3;
private static final int MAX_WAIT_ENABLED_TEXT_MS = 10000;
private static final int MAX_WAIT_HOME_PAGER_HIDDEN_MS = 15000;
public static final int MAX_WAIT_MS = 4500;
@ -70,11 +69,9 @@ abstract class BaseTest extends ActivityInstrumentationTestCase2<Activity> {
private int mPreferenceRequestID = 0;
protected Solo mSolo;
protected Driver mDriver;
protected Assert mAsserter;
protected Actions mActions;
protected String mBaseUrl;
protected String mRawBaseUrl;
private String mLogFile;
protected String mProfile;
public Device mDevice;
protected DatabaseHelper mDatabaseHelper;
@ -104,30 +101,17 @@ abstract class BaseTest extends ActivityInstrumentationTestCase2<Activity> {
super(TARGET_PACKAGE_ID, mLauncherActivityClass);
}
protected abstract int getTestType();
@Override
protected void setUp() throws Exception {
// Load config file from root path (setup by python script)
String rootPath = FennecInstrumentationTestRunner.getFennecArguments().getString("deviceroot");
String configFile = FennecNativeDriver.getFile(rootPath + "/robotium.config");
HashMap config = FennecNativeDriver.convertTextToTable(configFile);
mLogFile = (String)config.get("logfile");
mBaseUrl = ((String)config.get("host")).replaceAll("(/$)", "");
mRawBaseUrl = ((String)config.get("rawhost")).replaceAll("(/$)", "");
// Initialize the asserter
if (getTestType() == TEST_TALOS) {
mAsserter = new FennecTalosAssert();
} else {
mAsserter = new FennecMochitestAssert();
}
mAsserter.setLogFile(mLogFile);
mAsserter.setTestName(this.getClass().getName());
public void setUp() throws Exception {
super.setUp();
// Create the intent to be used with all the important arguments.
mBaseUrl = ((String) mConfig.get("host")).replaceAll("(/$)", "");
mRawBaseUrl = ((String) mConfig.get("rawhost")).replaceAll("(/$)", "");
Intent i = new Intent(Intent.ACTION_MAIN);
mProfile = (String)config.get("profile");
mProfile = (String) mConfig.get("profile");
i.putExtra("args", "-no-remote -profile " + mProfile);
String envString = (String)config.get("envvars");
String envString = (String) mConfig.get("envvars");
if (envString != "") {
String[] envStrings = envString.split(",");
for (int iter = 0; iter < envStrings.length; iter++) {
@ -139,7 +123,7 @@ abstract class BaseTest extends ActivityInstrumentationTestCase2<Activity> {
mActivity = getActivity();
// Set up Robotium.solo and Driver objects
mSolo = new Solo(getInstrumentation(), mActivity);
mDriver = new FennecNativeDriver(mActivity, mSolo, rootPath);
mDriver = new FennecNativeDriver(mActivity, mSolo, mRootPath);
mActions = new FennecNativeActions(mActivity, mSolo, getInstrumentation(), mAsserter);
mDevice = new Device();
mDatabaseHelper = new DatabaseHelper(mActivity, mAsserter);
@ -333,7 +317,6 @@ abstract class BaseTest extends ActivityInstrumentationTestCase2<Activity> {
public boolean test();
}
@SuppressWarnings({"unchecked", "non-varargs"})
public void SqliteCompare(String dbName, String sqlCommand, ContentValues[] cvs) {
File profile = new File(mProfile);
String dbPath = new File(profile, dbName).getPath();
@ -342,27 +325,6 @@ abstract class BaseTest extends ActivityInstrumentationTestCase2<Activity> {
SqliteCompare(c, cvs);
}
private boolean CursorMatches(Cursor c, String[] columns, ContentValues cv) {
for (int i = 0; i < columns.length; i++) {
String column = columns[i];
if (cv.containsKey(column)) {
mAsserter.info("Comparing", "Column values for: " + column);
Object value = cv.get(column);
if (value == null) {
if (!c.isNull(i)) {
return false;
}
} else {
if (c.isNull(i) || !value.toString().equals(c.getString(i))) {
return false;
}
}
}
}
return true;
}
@SuppressWarnings({"unchecked", "non-varargs"})
public void SqliteCompare(Cursor c, ContentValues[] cvs) {
mAsserter.is(c.getCount(), cvs.length, "List is correct length");
if (c.moveToFirst()) {
@ -374,7 +336,7 @@ abstract class BaseTest extends ActivityInstrumentationTestCase2<Activity> {
}
}
mAsserter.is(found, true, "Password was found");
} while(c.moveToNext());
} while (c.moveToNext());
}
}

View File

@ -1,3 +1,7 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.tests;
import android.content.ContentProvider;
@ -208,7 +212,7 @@ abstract class ContentProviderTest extends BaseTest {
mResolver.addProvider(mProviderAuthority, mProvider);
}
public Uri appendUriParam(Uri uri, String param, String value) {
public static Uri appendUriParam(Uri uri, String param, String value) {
return uri.buildUpon().appendQueryParameter(param, value).build();
}

View File

@ -1,22 +1,47 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.tests;
import java.io.File;
import java.util.Random;
import java.util.UUID;
import org.mozilla.gecko.db.BrowserContract;
import org.mozilla.gecko.db.BrowserProvider;
import org.mozilla.gecko.db.LocalBrowserDB;
import org.mozilla.gecko.util.FileUtils;
import android.app.Activity;
import android.content.ContentProvider;
import android.content.ContentProviderClient;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.net.Uri;
import android.os.SystemClock;
import android.util.Log;
import java.util.UUID;
import java.util.Random;
import org.mozilla.gecko.GeckoProfile;
import org.mozilla.gecko.db.BrowserContract;
import org.mozilla.gecko.db.BrowserDB;
import org.mozilla.gecko.db.BrowserProvider;
/*
* This test is meant to exercise the performance of Fennec's
* history and bookmarks content provider.
/**
* This test is meant to exercise the performance of Fennec's history and
* bookmarks content provider.
*
* It does not extend ContentProviderTest because that class is unable to
* accurately assess the performance of the ContentProvider -- it's a second
* instance of a class that's only supposed to exist once, wrapped in a bunch of
* junk.
*
* Instead, we directly use the existing ContentProvider, accessing a new
* profile directory that we initialize via BrowserDB.
*/
public class testBrowserProviderPerf extends ContentProviderTest {
@SuppressWarnings("unchecked")
public class testBrowserProviderPerf extends BaseRobocopTest {
private static final String LAUNCH_ACTIVITY_FULL_CLASSNAME = TestConstants.ANDROID_PACKAGE_NAME + ".App";
private static Class<Activity> mLauncherActivityClass;
private final int NUMBER_OF_BASIC_HISTORY_URLS = 10000;
private final int NUMBER_OF_BASIC_BOOKMARK_URLS = 500;
private final int NUMBER_OF_COMBINED_URLS = 500;
@ -25,13 +50,30 @@ public class testBrowserProviderPerf extends ContentProviderTest {
private final int BATCH_SIZE = 500;
// Include spaces in prefix to test performance querying with
// multiple constraint words
// multiple constraint words.
private final String KNOWN_PREFIX = "my mozilla test ";
private Random mGenerator;
private final String MOBILE_FOLDER_GUID = "mobile";
private long mMobileFolderId;
private ContentResolver mResolver;
private String mProfile;
private Uri mHistoryURI;
private Uri mBookmarksURI;
private Uri mFaviconsURI;
static {
try {
mLauncherActivityClass = (Class<Activity>) Class.forName(LAUNCH_ACTIVITY_FULL_CLASSNAME);
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
public testBrowserProviderPerf() {
super(TARGET_PACKAGE_ID, mLauncherActivityClass);
}
@Override
protected int getTestType() {
@ -39,7 +81,7 @@ public class testBrowserProviderPerf extends ContentProviderTest {
}
private void loadMobileFolderId() throws Exception {
Cursor c = mProvider.query(BrowserContract.Bookmarks.CONTENT_URI, null,
Cursor c = mResolver.query(mBookmarksURI, null,
BrowserContract.Bookmarks.GUID + " = ?",
new String[] { MOBILE_FOLDER_GUID },
null);
@ -121,7 +163,7 @@ public class testBrowserProviderPerf extends ContentProviderTest {
}
private void addTonsOfUrls() throws Exception {
// Create some random bookmark entries
// Create some random bookmark entries.
ContentValues[] bookmarkEntries = new ContentValues[BATCH_SIZE];
for (int i = 0; i < NUMBER_OF_BASIC_BOOKMARK_URLS / BATCH_SIZE; i++) {
@ -131,10 +173,10 @@ public class testBrowserProviderPerf extends ContentProviderTest {
bookmarkEntries[j] = createRandomBookmarkEntry();
}
mProvider.bulkInsert(BrowserContract.Bookmarks.CONTENT_URI, bookmarkEntries);
mResolver.bulkInsert(mBookmarksURI, bookmarkEntries);
}
// Create some random history entries
// Create some random history entries.
ContentValues[] historyEntries = new ContentValues[BATCH_SIZE];
ContentValues[] faviconEntries = new ContentValues[BATCH_SIZE];
@ -147,12 +189,12 @@ public class testBrowserProviderPerf extends ContentProviderTest {
faviconEntries[j] = createFaviconEntryWithUrl(historyEntries[j].getAsString(BrowserContract.History.URL));
}
mProvider.bulkInsert(BrowserContract.History.CONTENT_URI, historyEntries);
mProvider.bulkInsert(BrowserContract.Favicons.CONTENT_URI, faviconEntries);
mResolver.bulkInsert(mHistoryURI, historyEntries);
mResolver.bulkInsert(mFaviconsURI, faviconEntries);
}
// Create random bookmark/history entries with the same url
// Create random bookmark/history entries with the same URL.
for (int i = 0; i < NUMBER_OF_COMBINED_URLS / BATCH_SIZE; i++) {
bookmarkEntries = new ContentValues[BATCH_SIZE];
historyEntries = new ContentValues[BATCH_SIZE];
@ -164,12 +206,12 @@ public class testBrowserProviderPerf extends ContentProviderTest {
faviconEntries[j] = createFaviconEntryWithUrl(url);
}
mProvider.bulkInsert(BrowserContract.Bookmarks.CONTENT_URI, bookmarkEntries);
mProvider.bulkInsert(BrowserContract.History.CONTENT_URI, historyEntries);
mProvider.bulkInsert(BrowserContract.Favicons.CONTENT_URI, faviconEntries);
mResolver.bulkInsert(mBookmarksURI, bookmarkEntries);
mResolver.bulkInsert(mHistoryURI, historyEntries);
mResolver.bulkInsert(mFaviconsURI, faviconEntries);
}
// Create some history entries with a known prefix
// Create some history entries with a known prefix.
historyEntries = new ContentValues[NUMBER_OF_KNOWN_URLS];
faviconEntries = new ContentValues[NUMBER_OF_KNOWN_URLS];
for (int i = 0; i < NUMBER_OF_KNOWN_URLS; i++) {
@ -177,34 +219,112 @@ public class testBrowserProviderPerf extends ContentProviderTest {
faviconEntries[i] = createFaviconEntryWithUrl(historyEntries[i].getAsString(BrowserContract.History.URL));
}
mProvider.bulkInsert(BrowserContract.History.CONTENT_URI, historyEntries);
mProvider.bulkInsert(BrowserContract.Favicons.CONTENT_URI, faviconEntries);
mResolver.bulkInsert(mHistoryURI, historyEntries);
mResolver.bulkInsert(mFaviconsURI, faviconEntries);
}
@Override
public void setUp() throws Exception {
super.setUp(sBrowserProviderCallable, BrowserContract.AUTHORITY, "browser.db");
super.setUp();
mProfile = "prof" + System.currentTimeMillis();
mHistoryURI = prepUri(BrowserContract.History.CONTENT_URI);
mBookmarksURI = prepUri(BrowserContract.Bookmarks.CONTENT_URI);
mFaviconsURI = prepUri(BrowserContract.Favicons.CONTENT_URI);
mResolver = getActivity().getApplicationContext().getContentResolver();
mGenerator = new Random(19580427);
}
public void testBrowserProviderPerf() throws Exception {
BrowserDB.initialize(GeckoProfile.DEFAULT_PROFILE);
@Override
public void tearDown() {
final ContentProviderClient client = mResolver.acquireContentProviderClient(mBookmarksURI);
try {
final ContentProvider cp = client.getLocalContentProvider();
final BrowserProvider bp = ((BrowserProvider) cp);
// This will be the DB we were just testing.
final SQLiteDatabase db = bp.getWritableDatabaseForTesting(mBookmarksURI);
try {
db.close();
} catch (Throwable e) {
// Nothing we can do.
}
} finally {
try {
client.release();
} catch (Throwable e) {
// Still go ahead and try to delete the profile.
}
try {
FileUtils.delTree(new File(mProfile), null, true);
} catch (Exception e) {
Log.w("GeckoTest", "Unable to delete profile " + mProfile, e);
}
}
}
public Uri prepUri(Uri uri) {
return uri.buildUpon()
.appendQueryParameter(BrowserContract.PARAM_PROFILE, mProfile)
.appendQueryParameter(BrowserContract.PARAM_IS_SYNC, "1") // So we don't trigger a sync.
.build();
}
/**
* This method:
*
* * Adds a bunch of test data via the ContentProvider API.
* * Runs a single query against that test data via BrowserDB.
* * Reports timing for Talos.
*/
public void testBrowserProviderQueryPerf() throws Exception {
// We add at least this many results.
final int limit = 100;
// Make sure we're querying the right profile.
final LocalBrowserDB db = new LocalBrowserDB(mProfile);
final Cursor before = db.filter(mResolver, KNOWN_PREFIX, limit);
try {
mAsserter.is(before.getCount(), 0, "Starts empty");
} finally {
before.close();
}
// Add data.
loadMobileFolderId();
addTonsOfUrls();
long start = SystemClock.uptimeMillis();
// Wait for a little while after inserting data. We do this because
// this test launches about:home, and Top Sites watches for DB changes.
// We don't have a good way for it to only watch changes related to
// its current profile, nor is it convenient for us to launch a different
// activity that doesn't depend on the DB.
// We can fix this by:
// * Adjusting the provider interface to allow a "don't notify" param.
// * Adjusting the interface schema to include the profile in the path,
// and only observe the correct path.
// * Launching a different activity.
Thread.sleep(5000);
final Cursor c = BrowserDB.filter(mResolver, KNOWN_PREFIX, 100);
c.getCount(); // ensure query is not lazy loaded
// Time the query.
final long start = SystemClock.uptimeMillis();
final Cursor c = db.filter(mResolver, KNOWN_PREFIX, limit);
long end = SystemClock.uptimeMillis();
try {
final int count = c.getCount();
final long end = SystemClock.uptimeMillis();
mAsserter.dumpLog("__start_report" + Long.toString(end - start) + "__end_report");
mAsserter.dumpLog("__startTimestamp" + Long.toString(end - start) + "__endTimestamp");
c.close();
mAsserter.is(count, limit, "Retrieved results");
mAsserter.dumpLog("Results: " + count);
mAsserter.dumpLog("__start_report" + Long.toString(end - start) + "__end_report");
mAsserter.dumpLog("__startTimestamp" + Long.toString(end - start) + "__endTimestamp");
} finally {
c.close();
}
}
}

View File

@ -1,3 +1,7 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.tests;
import java.util.HashSet;
@ -7,7 +11,6 @@ import java.util.concurrent.Callable;
import org.mozilla.gecko.db.BrowserContract;
import org.mozilla.gecko.db.BrowserContract.ReadingListItems;
import org.mozilla.gecko.db.ReadingListProvider;
import org.mozilla.gecko.db.TransactionalProvider;
import android.content.ContentProvider;
import android.content.ContentUris;
@ -68,12 +71,13 @@ public class testReadingListProvider extends ContentProviderTest {
}
}
public void testReadingListProvider() throws Exception {
public void testReadingListProviderTests() throws Exception {
for (Runnable test : mTests) {
setTestName(test.getClass().getSimpleName());
ensureEmptyDatabase();
test.run();
}
// Ensure browser initialization is complete before completing test,
// so that the minidumps directory is consistently created.
blockForGeckoReady();

View File

@ -110,6 +110,7 @@ public class ToolbarEditText extends CustomEditText
super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
if (gainFocus) {
resetAutocompleteState();
return;
}
@ -152,6 +153,11 @@ public class ToolbarEditText extends CustomEditText
updateTextTypeFromText(getText().toString());
}
private void resetAutocompleteState() {
mAutoCompleteResult = "";
mAutoCompletePrefix = null;
}
private void updateKeyboardInputType() {
// If the user enters a space, then we know they are entering
// search terms, not a URL. We can then switch to text mode so,

View File

@ -10,6 +10,8 @@ import java.io.File;
import java.io.IOException;
import java.io.FilenameFilter;
import org.mozilla.gecko.mozglue.RobocopTarget;
public class FileUtils {
private static final String LOGTAG= "GeckoFileUtils";
/*
@ -38,6 +40,7 @@ public class FileUtils {
}
}
@RobocopTarget
public static void delTree(File dir, FilenameFilter filter, boolean recurse) {
String[] files = null;

View File

@ -74,3 +74,11 @@
<action android:name="@MOZ_ANDROID_SHARED_FXACCOUNT_TYPE@.accounts.ACCOUNT_DELETED_ACTION"/>
</intent-filter>
</receiver>
<receiver
android:name="org.mozilla.gecko.fxa.receivers.FxAccountUpgradeReceiver">
<intent-filter>
<action android:name="android.intent.action.PACKAGE_REPLACED" />
<data android:scheme="package"/>
</intent-filter>
</receiver>

View File

@ -61,7 +61,8 @@
<receiver
android:name="org.mozilla.gecko.sync.receivers.UpgradeReceiver">
<intent-filter>
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
<action android:name="android.intent.action.PACKAGE_REPLACED" />
<data android:scheme="package"/>
</intent-filter>
</receiver>

View File

@ -565,9 +565,6 @@ pref("devtools.debugger.prompt-connection", true);
// Block tools from seeing / interacting with certified apps
pref("devtools.debugger.forbid-certified-apps", true);
// Disable add-on debugging
pref("devtools.debugger.addon-enabled", false);
// DevTools default color unit
pref("devtools.defaultColorUnit", "hex");

View File

@ -49,6 +49,10 @@ user_pref("toolkit.telemetry.notifiedOptOut", 999);
user_pref("font.size.inflation.emPerLine", 0);
user_pref("font.size.inflation.minTwips", 0);
// AddonManager tests require that the experiments feature be enabled.
user_pref("experiments.enabled", true);
user_pref("experiments.supported", true);
// Only load extensions from the application and user profile
// AddonManager.SCOPE_PROFILE + AddonManager.SCOPE_APPLICATION
user_pref("extensions.enabledScopes", 5);

View File

@ -1377,7 +1377,7 @@ ThreadClient.prototype = {
after: function (aResponse) {
if (aResponse.error) {
// There was an error resuming, back to paused state.
self._state = "paused";
this._state = "paused";
}
return aResponse;
},

View File

@ -49,7 +49,7 @@ function runTests() {
is(prefs.intPref, localPref.intPref, "read/write int pref");
is(prefs.charPref, localPref.charPref, "read/write string pref");
for (var key in prefs.allPrefs) {
["test.all.bool", "test.all.int", "test.all.string"].forEach(function(key) {
var expectedValue;
switch(Services.prefs.getPrefType(key)) {
case Ci.nsIPrefBranch.PREF_STRING:
@ -68,9 +68,10 @@ function runTests() {
is(prefs.allPrefs[key].value, expectedValue, "valid preference value (" + key + ")");
is(prefs.allPrefs[key].hasUserValue, Services.prefs.prefHasUserValue(key), "valid hasUserValue (" + key + ")");
}
});
["test.bool", "test.int", "test.string"].forEach(function(key) {
ok(!prefs.allPrefs.hasOwnProperty(key), "expect no pref (" + key + ")");
is(Services.prefs.getPrefType(key), Ci.nsIPrefBranch.PREF_INVALID, "pref (" + key + ") is clear");
});
@ -102,6 +103,9 @@ window.onload = function () {
SpecialPowers.pushPrefEnv({
"set": [
["devtools.debugger.forbid-certified-apps", false],
["test.all.bool", true],
["test.all.int", 0x4321],
["test.all.string", "allizom"],
]
}, runTests);
}

View File

@ -217,3 +217,14 @@
<!ENTITY eula.accept "Accept and Install…">
<!ENTITY settings.path.button.label "Browse…">
<!-- LOCALIZATION NOTE (experiment.info.label): The strings related to
experiments are present on the "Experiments" tab of the add-ons manager.
This tab won't be displayed unless an Experiment add-on is installed.
Install https://people.mozilla.org/~gszorc/dummy-experiment-addon.xpi
to cause this tab to appear. -->
<!ENTITY experiment.info.label "What's this? Telemetry may install and run experiments from time to time.">
<!ENTITY experiment.info.learnmore "Learn More">
<!ENTITY experiment.info.learnmore.accesskey "L">
<!ENTITY experiment.info.changetelemetry "Telemetry Settings">
<!ENTITY experiment.info.changetelemetry.accesskey "T">

View File

@ -131,3 +131,4 @@ type.locale.name=Languages
type.plugin.name=Plugins
type.dictionary.name=Dictionaries
type.service.name=Services
type.experiment.name=Experiments

View File

@ -188,7 +188,11 @@ setting[type="menulist"] {
display: none;
}
#addons-page .view-pane:not([type="plugin"]) .global-info-container {
#addons-page .view-pane:not([type="plugin"]) .plugin-info-container {
display: none;
}
#addons-page .view-pane:not([type="experiment"]) .experiment-info-container {
display: none;
}
@ -221,3 +225,11 @@ richlistitem:not([selected]) * {
.discover-button[disabled="true"] {
display: none;
}
#experiments-learn-more[disabled="true"] {
display: none;
}
#experiments-change-telemetry[disabled="true"] {
display: none;
}

View File

@ -29,7 +29,7 @@ const PREF_GETADDONS_CACHE_ENABLED = "extensions.getAddons.cache.enabled";
const PREF_GETADDONS_CACHE_ID_ENABLED = "extensions.%ID%.getAddons.cache.enabled";
const PREF_UI_TYPE_HIDDEN = "extensions.ui.%TYPE%.hidden";
const PREF_UI_LASTCATEGORY = "extensions.ui.lastCategory";
const PREF_ADDON_DEBUGGING_ENABLED = "devtools.debugger.addon-enabled";
const PREF_ADDON_DEBUGGING_ENABLED = "devtools.chrome.enabled";
const PREF_REMOTE_DEBUGGING_ENABLED = "devtools.debugger.remote-enabled";
const LOADING_MSG_DELAY = 100;
@ -208,6 +208,36 @@ function isDiscoverEnabled() {
return true;
}
/**
* Obtain the main DOMWindow for the current context.
*/
function getMainWindow() {
return window.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebNavigation)
.QueryInterface(Ci.nsIDocShellTreeItem)
.rootTreeItem
.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindow);
}
/**
* Obtain the DOMWindow that can open a preferences pane.
*
* This is essentially "get the browser chrome window" with the added check
* that the supposed browser chrome window is capable of opening a preferences
* pane.
*
* This may return null if we can't find the browser chrome window.
*/
function getMainWindowWithPreferencesPane() {
let mainWindow = getMainWindow();
if (mainWindow && "openAdvancedPreferences" in mainWindow) {
return mainWindow;
} else {
return null;
}
}
/**
* A wrapper around the HTML5 session history service that allows the browser
* back/forward controls to work within the manager
@ -1245,6 +1275,27 @@ var gViewController = {
aAddon.userDisabled = true;
}
},
cmd_experimentsLearnMore: {
isEnabled: function cmd_experimentsLearnMore_isEnabled() {
let mainWindow = getMainWindow();
return mainWindow && "switchToTabHavingURI" in mainWindow;
},
doCommand: function cmd_experimentsLearnMore_doCommand() {
let url = Services.prefs.getCharPref("toolkit.telemetry.infoURL");
openOptionsInTab(url);
},
},
cmd_experimentsOpenTelemetryPreferences: {
isEnabled: function cmd_experimentsOpenTelemetryPreferences_isEnabled() {
return !!getMainWindowWithPreferencesPane();
},
doCommand: function cmd_experimentsOpenTelemetryPreferences_doCommand() {
let mainWindow = getMainWindowWithPreferencesPane();
mainWindow.openAdvancedPreferences("dataChoicesTab");
},
},
},
supportsCommand: function gVC_supportsCommand(aCommand) {
@ -1302,11 +1353,7 @@ function hasInlineOptions(aAddon) {
}
function openOptionsInTab(optionsURL) {
var mainWindow = window.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebNavigation)
.QueryInterface(Ci.nsIDocShellTreeItem)
.rootTreeItem.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindow);
let mainWindow = getMainWindow();
if ("switchToTabHavingURI" in mainWindow) {
mainWindow.switchToTabHavingURI(optionsURL, true);
return true;

View File

@ -1343,7 +1343,7 @@
this._showStatus(showProgress ? "progress" : "none");
let debuggable = this.mAddon.isDebuggable &&
Services.prefs.getBoolPref('devtools.debugger.addon-enabled') &&
Services.prefs.getBoolPref('devtools.chrome.enabled') &&
Services.prefs.getBoolPref('devtools.debugger.remote-enabled');
this._debugBtn.disabled = this._debugBtn.hidden = !debuggable

View File

@ -89,6 +89,8 @@
<command id="cmd_enableUpdateSecurity"/>
<command id="cmd_toggleAutoUpdateDefault"/>
<command id="cmd_resetAddonAutoUpdate"/>
<command id="cmd_experimentsLearnMore"/>
<command id="cmd_experimentsOpenTelemetryPreferences"/>
</commandset>
<!-- view commands - these act on the selected addon -->
@ -347,7 +349,7 @@
<spacer flex="5000"/> <!-- Necessary to allow the message to wrap -->
</hbox>
</hbox>
<hbox class="view-header global-info-container">
<hbox class="view-header global-info-container plugin-info-container">
<hbox class="global-info" flex="1" align="center">
<button class="button-link global-info-plugincheck"
label="&info.plugincheck.label;"
@ -356,6 +358,22 @@
<spacer flex="5000"/> <!-- Necessary to allow the message to wrap -->
</hbox>
</hbox>
<hbox class="view-header global-info-container experiment-info-container">
<hbox class="global-info" flex="1" align="center">
<label value="&experiment.info.label;"/>
<button id="experiments-learn-more"
label="&experiment.info.learnmore;"
tooltiptext="&experiment.info.learnmore;"
accesskey="&experiment.info.learnmore.accesskey;"
command="cmd_experimentsLearnMore"/>
<button id="experiments-change-telemetry"
label="&experiment.info.changetelemetry;"
tooltiptext="&experiment.info.changetelemetry;"
accesskey="&experiment.info.changetelemetry.accesskey;"
command="cmd_experimentsOpenTelemetryPreferences"/>
<spacer flex="5000"/> <!-- Necessary to allow the message to wrap. -->
</hbox>
</hbox>
<vbox id="addon-list-empty" class="alert-container"
flex="1" hidden="true">
<spacer class="alert-spacer-before"/>

View File

@ -168,9 +168,16 @@ const TYPES = {
theme: 4,
locale: 8,
multipackage: 32,
dictionary: 64
dictionary: 64,
experiment: 128,
};
const RESTARTLESS_TYPES = new Set([
"dictionary",
"experiment",
"locale",
]);
// Keep track of where we are in startup for telemetry
// event happened during XPIDatabase.startup()
const XPI_STARTING = "XPIStarting";
@ -819,9 +826,10 @@ function loadManifestFromRDF(aUri, aStream) {
}
}
else {
// spell check dictionaries and language packs never require a restart
if (addon.type == "dictionary" || addon.type == "locale")
// Some add-on types are always restartless.
if (RESTARTLESS_TYPES.has(addon.type)) {
addon.bootstrap = true;
}
// Only extensions are allowed to provide an optionsURL, optionsType or aboutURL. For
// all other types they are silently ignored
@ -7270,7 +7278,7 @@ WinRegInstallLocation.prototype = {
};
#endif
AddonManagerPrivate.registerProvider(XPIProvider, [
let addonTypes = [
new AddonManagerPrivate.AddonType("extension", URI_EXTENSION_STRINGS,
STRING_TYPE_NAME,
AddonManager.VIEW_TYPE_LIST, 4000),
@ -7284,5 +7292,20 @@ AddonManagerPrivate.registerProvider(XPIProvider, [
new AddonManagerPrivate.AddonType("locale", URI_EXTENSION_STRINGS,
STRING_TYPE_NAME,
AddonManager.VIEW_TYPE_LIST, 8000,
AddonManager.TYPE_UI_HIDE_EMPTY)
]);
AddonManager.TYPE_UI_HIDE_EMPTY),
];
// We only register experiments support if the application supports them.
// Ideally, we would install an observer to watch the pref. Installing
// an observer for this pref is not necessary here and may be buggy with
// regards to registering this XPIProvider twice.
if (Prefs.getBoolPref("experiments.supported", false)) {
addonTypes.push(
new AddonManagerPrivate.AddonType("experiment",
URI_EXTENSION_STRINGS,
STRING_TYPE_NAME,
AddonManager.VIEW_TYPE_LIST, 11000,
AddonManager.TYPE_UI_HIDE_EMPTY));
}
AddonManagerPrivate.registerProvider(XPIProvider, addonTypes);

View File

@ -0,0 +1,16 @@
<?xml version="1.0"?>
<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:em="http://www.mozilla.org/2004/em-rdf#">
<Description about="urn:mozilla:install-manifest">
<em:id>test-experiment1@experiments.mozilla.org</em:id>
<em:version>1.0</em:version>
<em:type>128</em:type>
<!-- Front End MetaData -->
<em:name>Test Experiment 1</em:name>
<em:description>Test Description</em:description>
</Description>
</RDF>

View File

@ -33,6 +33,7 @@ support-files =
[browser_details.js]
[browser_discovery.js]
[browser_dragdrop.js]
[browser_experiments.js]
[browser_list.js]
[browser_metadataTimeout.js]
[browser_searching.js]

View File

@ -12,7 +12,7 @@ let { Task } = Components.utils.import("resource://gre/modules/Task.jsm", {});
const getDebugButton = node =>
node.ownerDocument.getAnonymousElementByAttribute(node, "anonid", "debug-btn");
const addonDebuggingEnabled = bool =>
Services.prefs.setBoolPref("devtools.debugger.addon-enabled", !!bool);
Services.prefs.setBoolPref("devtools.chrome.enabled", !!bool);
const remoteDebuggingEnabled = bool =>
Services.prefs.setBoolPref("devtools.debugger.remote-enabled", !!bool);

View File

@ -0,0 +1,135 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/
*/
let gManagerWindow;
let gCategoryUtilities;
let gInstalledAddons = [];
function test() {
waitForExplicitFinish();
open_manager(null, (win) => {
gManagerWindow = win;
gCategoryUtilities = new CategoryUtilities(win);
run_next_test();
});
}
function end_test() {
for (let addon of gInstalledAddons) {
addon.uninstall();
}
close_manager(gManagerWindow, finish);
}
// On an empty profile with no experiments, the experiment category
// should be hidden.
add_test(function testInitialState() {
Assert.ok(gCategoryUtilities.get("experiment", false), "Experiment tab is defined.");
Assert.ok(!gCategoryUtilities.isTypeVisible("experiment"), "Experiment tab hidden by default.");
run_next_test();
});
add_test(function testExperimentInfoNotVisible() {
gCategoryUtilities.openType("extension", () => {
let el = gManagerWindow.document.getElementsByClassName("experiment-info-container")[0];
is_element_hidden(el, "Experiment info not visible on other types.");
run_next_test();
});
});
// If we have an active experiment, we should see the experiments tab
// and that tab should have some messages.
add_test(function testActiveExperiment() {
install_addon("addons/browser_experiment1.xpi", (addon) => {
gInstalledAddons.push(addon);
// This may change if we remove compatibility checking from experiments.
// Putting this check here so a test fails if preconditions change.
Assert.equal(addon.isActive, false, "Add-on is not active.");
Assert.ok(gCategoryUtilities.isTypeVisible("experiment"), "Experiment tab visible.");
gCategoryUtilities.openType("experiment", (win) => {
let el = gManagerWindow.document.getElementsByClassName("experiment-info-container")[0];
is_element_visible(el, "Experiment info is visible on experiment tab.");
run_next_test();
});
});
});
add_test(function testExperimentLearnMore() {
// Actual URL is irrelevant.
Services.prefs.setCharPref("toolkit.telemetry.infoURL",
"http://mochi.test:8888/server.js");
gCategoryUtilities.openType("experiment", (win) => {
let btn = gManagerWindow.document.getElementById("experiments-learn-more");
if (!gUseInContentUI) {
is_element_hidden(btn, "Learn more button hidden if not using in-content UI.");
Services.prefs.clearUserPref("toolkit.telemetry.infoURL");
run_next_test();
return;
} else {
is_element_visible(btn, "Learn more button visible.");
}
window.addEventListener("DOMContentLoaded", function onLoad(event) {
info("Telemetry privacy policy window opened.");
window.removeEventListener("DOMContentLoaded", onLoad, false);
let browser = gBrowser.selectedTab.linkedBrowser;
let expected = Services.prefs.getCharPref("toolkit.telemetry.infoURL");
Assert.equal(browser.currentURI.spec, expected, "New tab should have loaded privacy policy.");
browser.contentWindow.close();
Services.prefs.clearUserPref("toolkit.telemetry.infoURL");
run_next_test();
}, false);
info("Opening telemetry privacy policy.");
EventUtils.synthesizeMouseAtCenter(btn, {}, gManagerWindow);
});
});
add_test(function testOpenPreferences() {
gCategoryUtilities.openType("experiment", (win) => {
let btn = gManagerWindow.document.getElementById("experiments-change-telemetry");
if (!gUseInContentUI) {
is_element_hidden(btn, "Change telemetry button not enabled in out of window UI.");
info("Skipping preferences open test because not using in-content UI.");
run_next_test();
return;
}
is_element_visible(btn, "Change telemetry button visible in in-content UI.");
Services.obs.addObserver(function observer(prefWin, topic, data) {
Services.obs.removeObserver(observer, "advanced-pane-loaded");
info("Advanced preference pane opened.");
// We want this test to fail if the preferences pane changes.
let el = prefWin.document.getElementById("dataChoicesPanel");
is_element_visible(el);
prefWin.close();
info("Closed preferences pane.");
run_next_test();
}, "advanced-pane-loaded", false);
info("Loading preferences pane.");
EventUtils.synthesizeMouseAtCenter(btn, {}, gManagerWindow);
});
});

View File

@ -68,7 +68,7 @@ var gRestorePrefs = [{name: PREF_LOGGING_ENABLED},
{name: "extensions.getAddons.search.browseURL"},
{name: "extensions.getAddons.search.url"},
{name: "extensions.getAddons.cache.enabled"},
{name: "devtools.debugger.addon-enabled"},
{name: "devtools.chrome.enabled"},
{name: "devtools.debugger.remote-enabled"},
{name: PREF_SEARCH_MAXRESULTS},
{name: PREF_STRICT_COMPAT},
@ -418,6 +418,25 @@ function is_element_hidden(aElement, aMsg) {
ok(is_hidden(aElement), aMsg);
}
/**
* Install an add-on and call a callback when complete.
*
* The callback will receive the Addon for the installed add-on.
*/
function install_addon(path, cb, pathPrefix=TESTROOT) {
AddonManager.getInstallForURL(pathPrefix + path, (install) => {
install.addListener({
onInstallEnded: () => {
executeSoon(() => {
cb(install.addon);
});
},
});
install.install();
}, "application/x-xpinstall");
}
function CategoryUtilities(aManagerWindow) {
this.window = aManagerWindow;

View File

@ -62,11 +62,10 @@ label {
}
.alertCloseButton {
list-style-image: url("moz-icon://stock/gtk-close?size=button");
}
.alertCloseButton > .toolbarbutton-icon {
margin: -4px;
-moz-appearance: none;
height: 16px;
padding: 4px 2px;
width: 16px;
}
.alertCloseButton > .toolbarbutton-text {

View File

@ -29,8 +29,10 @@ findbar[hidden] {
}
.findbar-closebutton {
-moz-margin-start: 4px;
list-style-image: url("moz-icon://stock/gtk-close?size=menu");
-moz-appearance: none;
width: 16px;
height: 16px;
margin: 0 8px;
}
/* Search field */

View File

@ -306,3 +306,19 @@ notification > button {
.autoscroller[scrolldir="EW"] {
background-position: right bottom;
}
/* :::::: Close button icons ::::: */
.close-icon {
background-image: -moz-image-rect(url("chrome://global/skin/icons/close.svg"), 0, 16, 16, 0);
background-position: center center;
background-repeat: no-repeat;
}
.close-icon:hover {
background-image: -moz-image-rect(url("chrome://global/skin/icons/close.svg"), 0, 32, 16, 16);
}
.close-icon:hover:active {
background-image: -moz-image-rect(url("chrome://global/skin/icons/close.svg"), 0, 48, 16, 32);
}

View File

@ -0,0 +1,105 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1"
id="icon-close"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
x="0px"
y="0px"
width="96px"
height="16px"
viewBox="0 0 96 16">
<defs>
<style type="text/css"><![CDATA[
/* X Glyph Styles */
.glyphShape-style-foreground {
fill: ButtonText;
fill-opacity: .8;
}
.glyphShape-style-background {
fill: -moz-MenuBarText;
fill-opacity: .8;
}
.glyphShape-style-hover {
fill: #fff;
}
.glyphShape-style-hover-shadow {
fill: #b32c12;
}
.glyphShape-style-hover-active {
fill: #fff;
fill-opacity: .8;
}
.glyphShape-style-hover-active-shadow {
fill: #99260f;
}
.glyphShape-style-LWT-bright {
fill: #fff;
fill-opacity: .8;
}
.glyphShape-style-LWT-dark {
fill: #000;
fill-opacity: .8;
}
/* Close Button Background Styles */
.icon-background-hover {
fill: #d93616;
}
.icon-background-hover-active {
fill: #b32c12;
}
]]></style>
<polygon id="glyphShape-close" points="4,5.5 5.5,4 8,6.5 10.5,4 12,5.5 9.5,8 12,10.5 10.5,12 8,9.5 5.5,12 4,10.5 6.5,8"/>
<polygon id="glyphShape-close-topHighlight" points="4,5.5 5.5,4 8,6.5 10.5,4 12,5.5 9.5,8 11.5,6 10.5,5 8,7.5 5.5,5 4.5,6"/>
<rect id="glyphShape-background" x="2" y="2" rx="2" width="12" height="12"/>
</defs>
<g id="icon-closeForeground-default">
<use xlink:href="#glyphShape-close" class="glyphShape-style-foreground" />
<use xlink:href="#glyphShape-close-topHighlight" class="glyphShape-style-foreground" />
</g>
<g id="icon-close-hover" transform="translate(16)">
<use xlink:href="#glyphShape-background" class="icon-background-hover" />
<use xlink:href="#glyphShape-close" class="glyphShape-style-hover-shadow" transform="translate(0,1)" />
<use xlink:href="#glyphShape-close" class="glyphShape-style-hover" />
</g>
<g id="icon-close-hover-active" transform="translate(32)">
<use xlink:href="#glyphShape-background" class="icon-background-hover-active" />
<use xlink:href="#glyphShape-close" class="glyphShape-style-hover-active-shadow" transform="translate(0,1)" />
<use xlink:href="#glyphShape-close" class="glyphShape-style-hover-active" />
</g>
<g id="icon-closeBackground-default" transform="translate(48)">
<use xlink:href="#glyphShape-close" class="glyphShape-style-background" />
<use xlink:href="#glyphShape-close-topHighlight" class="glyphShape-style-background" />
</g>
<g id="icon-close-LWT-bright" transform="translate(64)">
<use xlink:href="#glyphShape-close" class="glyphShape-style-LWT-bright" />
<use xlink:href="#glyphShape-close-topHighlight" class="glyphShape-style-LWT-bright" />
</g>
<g id="icon-close-LWT-dark" transform="translate(80)">
<use xlink:href="#glyphShape-close" class="glyphShape-style-LWT-dark" />
<use xlink:href="#glyphShape-close-topHighlight" class="glyphShape-style-LWT-dark" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@ -43,6 +43,7 @@ toolkit.jar:
+ skin/classic/global/icons/autoscroll.png (icons/autoscroll.png)
+ skin/classic/global/icons/blacklist_favicon.png (icons/blacklist_favicon.png)
+ skin/classic/global/icons/blacklist_large.png (icons/blacklist_large.png)
+ skin/classic/global/icons/close.svg (icons/close.svg)
+ skin/classic/global/icons/find.png (icons/find.png)
+ skin/classic/global/icons/loading_16.png (icons/loading_16.png)
+ skin/classic/global/icons/panelarrow-horizontal.svg (icons/panelarrow-horizontal.svg)

View File

@ -52,9 +52,11 @@ notification[type="critical"] {
}
.messageCloseButton {
list-style-image: url("moz-icon://stock/gtk-close?size=menu");
margin-top: 0;
margin-bottom: 0;
-moz-appearance: none;
width: 16px;
height: 16px;
padding-left: 11px;
padding-right: 11px;
}
/* Popup notification */
@ -74,7 +76,3 @@ notification[type="critical"] {
.popup-notification-button-container {
margin-top: 17px;
}
.popup-notification-closeitem {
list-style-image: url("moz-icon://stock/gtk-close?size=menu");
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 822 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 822 B

View File

@ -226,6 +226,9 @@
#category-dictionary > .category-icon {
list-style-image: url("chrome://mozapps/skin/extensions/category-dictionaries.png");
}
#category-experiment > .category-icon {
list-style-image: url("chrome://mozapps/skin/extensions/category-experiments.png");
}
#category-availableUpdates > .category-icon {
list-style-image: url("chrome://mozapps/skin/extensions/category-available.png");
}
@ -417,6 +420,10 @@
list-style-image: url("chrome://mozapps/skin/extensions/dictionaryGeneric.png");
}
.addon-view[type="experiment"] .icon {
list-style-image: url("chrome://mozapps/skin/extensions/experimentGeneric.png");
}
.name-container {
font-size: 150%;
margin-bottom: 0;

View File

@ -15,12 +15,14 @@ toolkit.jar:
+ skin/classic/mozapps/extensions/category-plugins.png (extensions/category-plugins.png)
+ skin/classic/mozapps/extensions/category-service.png (extensions/category-service.png)
+ skin/classic/mozapps/extensions/category-dictionaries.png (extensions/category-dictionaries.png)
+ skin/classic/mozapps/extensions/category-experiments.png (extensions/category-experiments.png)
+ skin/classic/mozapps/extensions/category-recent.png (extensions/category-recent.png)
+ skin/classic/mozapps/extensions/category-available.png (extensions/category-available.png)
+ skin/classic/mozapps/extensions/extensionGeneric.png (extensions/extensionGeneric.png)
+ skin/classic/mozapps/extensions/extensionGeneric-16.png (extensions/extensionGeneric-16.png)
+ skin/classic/mozapps/extensions/dictionaryGeneric.png (extensions/dictionaryGeneric.png)
+ skin/classic/mozapps/extensions/dictionaryGeneric-16.png (extensions/dictionaryGeneric-16.png)
+ skin/classic/mozapps/extensions/experimentGeneric.png (extensions/experimentGeneric.png)
+ skin/classic/mozapps/extensions/themeGeneric.png (extensions/themeGeneric.png)
+ skin/classic/mozapps/extensions/themeGeneric-16.png (extensions/themeGeneric-16.png)
+ skin/classic/mozapps/extensions/localeGeneric.png (extensions/localeGeneric.png)

Binary file not shown.

After

Width:  |  Height:  |  Size: 822 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 822 B

View File

@ -259,6 +259,9 @@
#category-dictionary > .category-icon {
list-style-image: url("chrome://mozapps/skin/extensions/category-dictionaries.png");
}
#category-experiment > .category-icon {
list-style-image: url("chrome://mozapps/skin/extensions/category-experiments.png");
}
#category-availableUpdates > .category-icon {
list-style-image: url("chrome://mozapps/skin/extensions/category-available.png");
}
@ -483,6 +486,10 @@
list-style-image: url("chrome://mozapps/skin/extensions/dictionaryGeneric.png");
}
.addon-view[type="experiment"] .icon {
list-style-image: url("chrome://mozapps/skin/extensions/experimentGeneric.png");
}
.name-container {
font-size: 150%;
margin-bottom: 0;

View File

@ -17,6 +17,7 @@ toolkit.jar:
skin/classic/mozapps/extensions/category-plugins.png (extensions/category-plugins.png)
skin/classic/mozapps/extensions/category-service.png (extensions/category-service.png)
skin/classic/mozapps/extensions/category-dictionaries.png (extensions/category-dictionaries.png)
skin/classic/mozapps/extensions/category-experiments.png (extensions/category-experiments.png)
skin/classic/mozapps/extensions/category-recent.png (extensions/category-recent.png)
skin/classic/mozapps/extensions/category-available.png (extensions/category-available.png)
skin/classic/mozapps/extensions/discover-logo.png (extensions/discover-logo.png)
@ -26,6 +27,7 @@ toolkit.jar:
skin/classic/mozapps/extensions/themeGeneric-16.png (extensions/themeGeneric-16.png)
skin/classic/mozapps/extensions/dictionaryGeneric.png (extensions/dictionaryGeneric.png)
skin/classic/mozapps/extensions/dictionaryGeneric-16.png (extensions/dictionaryGeneric-16.png)
skin/classic/mozapps/extensions/experimentGeneric.png (extensions/experimentGeneric.png)
skin/classic/mozapps/extensions/localeGeneric.png (extensions/localeGeneric.png)
skin/classic/mozapps/extensions/rating-won.png (extensions/rating-won.png)
skin/classic/mozapps/extensions/rating-not-won.png (extensions/rating-not-won.png)

Binary file not shown.

After

Width:  |  Height:  |  Size: 822 B

Some files were not shown because too many files have changed in this diff Show More