Merge fx-team to m-c
@ -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);
|
||||
|
@ -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");
|
||||
|
@ -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>
|
||||
|
@ -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}"
|
||||
|
@ -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}"
|
||||
|
@ -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
|
||||
|
@ -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}"
|
||||
|
@ -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
|
||||
|
@ -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",
|
||||
|
@ -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]
|
||||
|
@ -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) {
|
||||
|
@ -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.");
|
||||
});
|
@ -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]
|
||||
|
80
browser/devtools/debugger/test/browser_dbg_addon-panels.js
Normal 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();
|
||||
}
|
||||
});
|
@ -242,6 +242,10 @@ TabTarget.prototype = {
|
||||
return !this.isLocalTab;
|
||||
},
|
||||
|
||||
get isAddon() {
|
||||
return !!(this._form && this._form.addonActor);
|
||||
},
|
||||
|
||||
get isLocalTab() {
|
||||
return !!this._tab;
|
||||
},
|
||||
|
@ -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">
|
||||
|
@ -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) {
|
||||
|
@ -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");
|
||||
|
@ -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.
|
||||
|
@ -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.");
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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";
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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
|
||||
|
@ -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 ""
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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) {
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -51,7 +51,6 @@
|
||||
#newtab-undo-close-button {
|
||||
padding: 0;
|
||||
border: none;
|
||||
-moz-user-focus: normal;
|
||||
}
|
||||
|
||||
#newtab-undo-close-button > .toolbarbutton-text {
|
||||
|
@ -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
|
||||
|
@ -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");
|
||||
|
@ -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: */
|
||||
|
@ -57,7 +57,6 @@
|
||||
-moz-appearance: none;
|
||||
padding: 0;
|
||||
border: none;
|
||||
-moz-user-focus: normal;
|
||||
}
|
||||
|
||||
#newtab-undo-close-button > .toolbarbutton-text {
|
||||
|
@ -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',
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
50
mobile/android/base/db/PerProfileDatabaseProvider.java
Normal 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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
115
mobile/android/base/db/SharedBrowserDatabaseProvider.java
Normal 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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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.
|
||||
*
|
||||
|
132
mobile/android/base/fxa/receivers/FxAccountUpgradeReceiver.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
@ -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>() {
|
||||
|
@ -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);
|
||||
|
@ -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',
|
||||
|
Before Width: | Height: | Size: 451 B After Width: | Height: | Size: 538 B |
Before Width: | Height: | Size: 426 B After Width: | Height: | Size: 479 B |
Before Width: | Height: | Size: 572 B After Width: | Height: | Size: 663 B |
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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) {
|
||||
|
57
mobile/android/base/tests/BaseRobocopTest.java
Normal 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());
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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");
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
},
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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">
|
||||
|
@ -131,3 +131,4 @@ type.locale.name=Languages
|
||||
type.plugin.name=Plugins
|
||||
type.dictionary.name=Dictionaries
|
||||
type.service.name=Services
|
||||
type.experiment.name=Experiments
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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"/>
|
||||
|
@ -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);
|
||||
|
@ -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>
|
@ -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]
|
||||
|
@ -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);
|
||||
|
||||
|
135
toolkit/mozapps/extensions/test/browser/browser_experiments.js
Normal 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);
|
||||
});
|
||||
});
|
@ -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;
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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 */
|
||||
|
@ -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);
|
||||
}
|
||||
|
105
toolkit/themes/linux/global/icons/close.svg
Normal 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 |
@ -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)
|
||||
|
@ -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");
|
||||
}
|
||||
|
BIN
toolkit/themes/linux/mozapps/extensions/category-experiments.png
Normal file
After Width: | Height: | Size: 822 B |
BIN
toolkit/themes/linux/mozapps/extensions/experimentGeneric.png
Normal file
After Width: | Height: | Size: 822 B |
@ -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;
|
||||
|
@ -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)
|
||||
|
BIN
toolkit/themes/osx/mozapps/extensions/category-experiments.png
Normal file
After Width: | Height: | Size: 822 B |
BIN
toolkit/themes/osx/mozapps/extensions/experimentGeneric.png
Normal file
After Width: | Height: | Size: 822 B |
@ -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;
|
||||
|
@ -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)
|
||||
|
After Width: | Height: | Size: 822 B |