Merge f-t to m-c

This commit is contained in:
Phil Ringnalda 2014-03-29 10:08:41 -07:00
commit 8d0a796f41
72 changed files with 1660 additions and 276 deletions

View File

@ -43,7 +43,8 @@
<button id="home-button"></button>
<button id="rotate-button"></button>
</footer>
#elifdef MOZ_WIDGET_COCOA
#endif
#ifdef MOZ_WIDGET_COCOA
<!--
If the document is empty at startup, we don't display the window
at all on Mac OS...

View File

@ -1226,7 +1226,7 @@ pref("devtools.webconsole.filter.cssparser", false);
pref("devtools.webconsole.filter.csslog", false);
pref("devtools.webconsole.filter.exception", true);
pref("devtools.webconsole.filter.jswarn", false);
pref("devtools.webconsole.filter.jslog", true);
pref("devtools.webconsole.filter.jslog", false);
pref("devtools.webconsole.filter.error", true);
pref("devtools.webconsole.filter.warn", true);
pref("devtools.webconsole.filter.info", true);
@ -1236,10 +1236,10 @@ pref("devtools.webconsole.filter.secwarn", true);
// Remember the Browser Console filters
pref("devtools.browserconsole.filter.network", true);
pref("devtools.browserconsole.filter.networkinfo", true);
pref("devtools.browserconsole.filter.networkinfo", false);
pref("devtools.browserconsole.filter.netwarn", true);
pref("devtools.browserconsole.filter.csserror", true);
pref("devtools.browserconsole.filter.cssparser", true);
pref("devtools.browserconsole.filter.cssparser", false);
pref("devtools.browserconsole.filter.csslog", false);
pref("devtools.browserconsole.filter.exception", true);
pref("devtools.browserconsole.filter.jswarn", true);

View File

@ -65,10 +65,13 @@ let gPage = {
/**
* Updates the whole page and the grid when the storage has changed.
* @param aOnlyIfHidden If true, the page is updated only if it's hidden in
* the preloader.
*/
update: function Page_update() {
update: function Page_update(aOnlyIfHidden=false) {
let skipUpdate = aOnlyIfHidden && this.allowBackgroundCaptures;
// The grid might not be ready yet as we initialize it asynchronously.
if (gGrid.ready) {
if (gGrid.ready && !skipUpdate) {
gGrid.refresh();
}
},

View File

@ -24,3 +24,4 @@ skip-if = os == "mac" # Intermittent failures, bug 898317
[browser_newtab_tabsync.js]
[browser_newtab_undo.js]
[browser_newtab_unpin.js]
[browser_newtab_update.js]

View File

@ -0,0 +1,52 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Checks that newtab is updated as its links change.
*/
function runTests() {
if (NewTabUtils.allPages.updateScheduledForHiddenPages) {
// Wait for dynamic updates triggered by the previous test to finish.
yield whenPagesUpdated(null, true);
}
// First, start with an empty page. setLinks will trigger a hidden page
// update because it calls clearHistory. We need to wait for that update to
// happen so that the next time we wait for a page update below, we catch the
// right update and not the one triggered by setLinks.
//
// Why this weird way of yielding? First, these two functions don't return
// promises, they call TestRunner.next when done. Second, the point at which
// setLinks is done is independent of when the page update will happen, so
// calling whenPagesUpdated cannot wait until that time.
setLinks([]);
whenPagesUpdated(null, true);
yield null;
yield null;
// Strategy: Add some visits, open a new page, check the grid, repeat.
fillHistory([link(1)]);
yield whenPagesUpdated(null, true);
yield addNewTabPageTab();
checkGrid("1,,,,,,,,");
fillHistory([link(2)]);
yield whenPagesUpdated(null, true);
yield addNewTabPageTab();
checkGrid("2,1,,,,,,,");
fillHistory([link(1)]);
yield whenPagesUpdated(null, true);
yield addNewTabPageTab();
checkGrid("1,2,,,,,,,");
fillHistory([link(2), link(3), link(4)]);
yield whenPagesUpdated(null, true);
yield addNewTabPageTab();
checkGrid("2,1,3,4,,,,,");
}
function link(id) {
return { url: "http://example.com/#" + id, title: "site#" + id };
}

View File

@ -159,20 +159,34 @@ function clearHistory(aCallback) {
function fillHistory(aLinks, aCallback) {
let numLinks = aLinks.length;
if (!numLinks) {
if (aCallback)
executeSoon(aCallback);
return;
}
let transitionLink = Ci.nsINavHistoryService.TRANSITION_LINK;
for (let link of aLinks.reverse()) {
// Important: To avoid test failures due to clock jitter on Windows XP, call
// Date.now() once here, not each time through the loop.
let now = Date.now() * 1000;
for (let i = 0; i < aLinks.length; i++) {
let link = aLinks[i];
let place = {
uri: makeURI(link.url),
title: link.title,
visits: [{visitDate: Date.now() * 1000, transitionType: transitionLink}]
// Links are secondarily sorted by visit date descending, so decrease the
// visit date as we progress through the array so that links appear in the
// grid in the order they're present in the array.
visits: [{visitDate: now - i, transitionType: transitionLink}]
};
PlacesUtils.asyncHistory.updatePlaces(place, {
handleError: function () ok(false, "couldn't add visit to history"),
handleResult: function () {},
handleCompletion: function () {
if (--numLinks == 0)
if (--numLinks == 0 && aCallback)
aCallback();
}
});
@ -503,12 +517,18 @@ function createDragEvent(aEventType, aData) {
/**
* Resumes testing when all pages have been updated.
* @param aCallback Called when done. If not specified, TestRunner.next is used.
* @param aOnlyIfHidden If true, this resumes testing only when an update that
* applies to pre-loaded, hidden pages is observed. If
* false, this resumes testing when any update is observed.
*/
function whenPagesUpdated(aCallback) {
function whenPagesUpdated(aCallback, aOnlyIfHidden=false) {
let page = {
update: function () {
NewTabUtils.allPages.unregister(this);
executeSoon(aCallback || TestRunner.next);
update: function (onlyIfHidden=false) {
if (onlyIfHidden == aOnlyIfHidden) {
NewTabUtils.allPages.unregister(this);
executeSoon(aCallback || TestRunner.next);
}
}
};

View File

@ -394,6 +394,9 @@ PlacesViewBase.prototype = {
// Add "Open (Feed Name)" menuitem.
aPopup._siteURIMenuitem = document.createElement("menuitem");
aPopup._siteURIMenuitem.className = "openlivemarksite-menuitem";
if (typeof this.options.extraClasses.entry == "string") {
aPopup._siteURIMenuitem.classList.add(this.options.extraClasses.entry);
}
aPopup._siteURIMenuitem.setAttribute("targetURI", siteUrl);
aPopup._siteURIMenuitem.setAttribute("oncommand",
"openUILink(this.getAttribute('targetURI'), event);");
@ -429,6 +432,9 @@ PlacesViewBase.prototype = {
// Create the status menuitem and cache it in the popup object.
statusMenuitem = document.createElement("menuitem");
statusMenuitem.className = "livemarkstatus-menuitem";
if (typeof this.options.extraClasses.entry == "string") {
statusMenuitem.classList.add(this.options.extraClasses.entry);
}
statusMenuitem.setAttribute("disabled", true);
aPopup._statusMenuitem = statusMenuitem;
}

View File

@ -30,9 +30,5 @@ function test() {
function newWindow(callback) {
let opts = "chrome,all,dialog=no,height=800,width=800";
let win = window.openDialog(getBrowserURL(), "_blank", opts);
win.addEventListener("load", function onLoad() {
win.removeEventListener("load", onLoad, false);
callback(win);
}, false);
whenDelayedStartupFinished(win, () => callback(win));
}

View File

@ -5,3 +5,4 @@ support-files =
manifest.webapp
[browser_manifest_editor.js]
skip-if = os == "linux"

View File

@ -110,6 +110,7 @@ support-files =
test-bug-952277-highlight-nodes-in-vview.html
test-bug-609872-cd-iframe-parent.html
test-bug-609872-cd-iframe-child.html
test-bug-989025-iframe-parent.html
[browser_bug664688_sandbox_update_after_navigation.js]
[browser_bug_638949_copy_link_location.js]
@ -277,3 +278,4 @@ run-if = os == "mac"
[browser_webconsole_start_netmon_first.js]
[browser_webconsole_console_trace_duplicates.js]
[browser_webconsole_cd_iframe.js]
[browser_webconsole_autocomplete_crossdomain_iframe.js]

View File

@ -11,74 +11,63 @@ const TEST_URI = "data:text/html;charset=utf8,<title>bug859756</title>\n" +
function test()
{
addTab(TEST_URI);
browser.addEventListener("load", function onLoad() {
browser.removeEventListener("load", onLoad, true);
const FILTER_PREF = "devtools.browserconsole.filter.jslog";
Services.prefs.setBoolPref(FILTER_PREF, true);
registerCleanupFunction(() => {
Services.prefs.clearUserPref(FILTER_PREF);
});
Task.spawn(function*() {
const {tab} = yield loadTab(TEST_URI);
// Test for cached nsIConsoleMessages.
Services.console.logStringMessage("test1 for bug859756");
info("open web console");
openConsole(null, consoleOpened);
}, true);
}
let hud = yield openConsole(tab);
function consoleOpened(hud)
{
ok(hud, "web console opened");
Services.console.logStringMessage("do-not-show-me");
content.console.log("foobarz");
ok(hud, "web console opened");
Services.console.logStringMessage("do-not-show-me");
content.console.log("foobarz");
waitForMessages({
webconsole: hud,
messages: [
{
yield waitForMessages({
webconsole: hud,
messages: [{
text: "foobarz",
category: CATEGORY_WEBDEV,
severity: SEVERITY_LOG,
},
],
}).then(() => {
}],
});
let text = hud.outputNode.textContent;
is(text.indexOf("do-not-show-me"), -1,
"nsIConsoleMessages are not displayed");
is(text.indexOf("test1 for bug859756"), -1,
"nsIConsoleMessages are not displayed (confirmed)");
closeConsole(null, onWebConsoleClose);
});
}
function onWebConsoleClose()
{
info("web console closed");
HUDService.toggleBrowserConsole().then(onBrowserConsoleOpen);
}
yield closeConsole(tab);
function onBrowserConsoleOpen(hud)
{
ok(hud, "browser console opened");
Services.console.logStringMessage("test2 for bug859756");
info("web console closed");
hud = yield HUDService.toggleBrowserConsole();
ok(hud, "browser console opened");
waitForMessages({
webconsole: hud,
messages: [
{
Services.console.logStringMessage("test2 for bug859756");
let results = yield waitForMessages({
webconsole: hud,
messages: [{
text: "test1 for bug859756",
category: CATEGORY_JS,
},
{
}, {
text: "test2 for bug859756",
category: CATEGORY_JS,
},
{
}, {
text: "do-not-show-me",
category: CATEGORY_JS,
},
],
}).then(testFiltering);
}],
});
function testFiltering(results)
{
let msg = [...results[2].matched][0];
ok(msg, "message element for do-not-show-me (nsIConsoleMessage)");
isnot(msg.textContent.indexOf("do-not-show"), -1, "element content is correct");
@ -87,9 +76,5 @@ function onBrowserConsoleOpen(hud)
hud.setFilterState("jslog", false);
ok(msg.classList.contains("filtered-by-type"), "element is filtered");
hud.setFilterState("jslog", true);
finishTest();
}
}).then(finishTest);
}

View File

@ -0,0 +1,59 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
// Test that autocomplete doesn't break when trying to reach into objects from
// a different domain, bug 989025.
function test() {
let hud;
const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-bug-989025-iframe-parent.html";
Task.spawn(function*() {
const {tab} = yield loadTab(TEST_URI);
hud = yield openConsole(tab);
hud.jsterm.execute('document.title');
yield waitForMessages({
webconsole: hud,
messages: [{
text: "989025 - iframe parent",
category: CATEGORY_OUTPUT,
}],
});
let autocompleteUpdated = hud.jsterm.once("autocomplete-updated");
hud.jsterm.setInputValue("window[0].document");
executeSoon(() => {
EventUtils.synthesizeKey(".", {});
});
yield autocompleteUpdated;
hud.jsterm.setInputValue("window[0].document.title");
EventUtils.synthesizeKey("VK_RETURN", {});
yield waitForMessages({
webconsole: hud,
messages: [{
text: "Permission denied",
category: CATEGORY_OUTPUT,
severity: SEVERITY_ERROR,
}],
});
hud.jsterm.execute("window.location");
yield waitForMessages({
webconsole: hud,
messages: [{
text: "test-bug-989025-iframe-parent.html",
category: CATEGORY_OUTPUT,
}],
});
yield closeConsole(tab);
}).then(finishTest);
}

View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>test for bug 989025 - iframe parent</title>
<!-- Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ -->
</head>
<body>
<p>test for bug 989025 - iframe parent</p>
<iframe src="http://mochi.test:8888/browser/browser/devtools/webconsole/test/test-bug-609872-cd-iframe-child.html"></iframe>
</body>
</html>

View File

@ -645,15 +645,15 @@ FunctionEnd
${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
; Don't add URLUpdateInfo 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
; Note: URLUpdateInfo is only defined in the official branding.nsi.
!ifdef URLUpdateInfo
!ifndef BETA_UPDATE_CHANNEL
${WriteRegStr2} $1 "$0" "URLInfoAbout" "${URLInfoAbout}" 0
!endif
!endif
${WriteRegStr2} $1 "$0" "URLUpdateInfo" "${URLUpdateInfo}" 0
!endif
!endif
${WriteRegStr2} $1 "$0" "URLInfoAbout" "${URLInfoAbout}" 0
${WriteRegDWORD2} $1 "$0" "NoModify" 1 0
${WriteRegDWORD2} $1 "$0" "NoRepair" 1 0

View File

@ -86,7 +86,10 @@ paste-button.tooltiptext2 = Paste (%S)
feed-button.label = Subscribe
feed-button.tooltiptext = Subscribe to this page…
characterencoding-button.label = Character Encoding
# LOCALIZATION NOTE (characterencoding-button.label): The \u00ad character at the beginning
# of the string is used to disable auto hyphenation on the button text when it is displayed
# in the menu panel.
characterencoding-button.label = \u00adCharacter Encoding
characterencoding-button.tooltiptext2 = Show Character Encoding options
email-link-button.label = Email Link

View File

@ -197,8 +197,7 @@ toolbarbutton.bookmark-item:not(.subviewbutton):not(#bookmarks-menu-button),
margin: 0 !important;
}
toolbarbutton.bookmark-item:not(.subviewbutton):not(#bookmarks-menu-button):hover,
toolbarbutton.bookmark-item:not(#bookmarks-menu-button)[open="true"] {
toolbarbutton.bookmark-item:not(.subviewbutton):not(#bookmarks-menu-button):hover {
background-color: rgba(0, 0, 0, .205);
}
@ -221,7 +220,7 @@ toolbarbutton.bookmark-item[open="true"]:not(.subviewbutton) {
}
toolbarbutton.bookmark-item:not(.subviewbutton):not(#bookmarks-menu-button):active:hover,
toolbarbutton.bookmark-item:not(#bookmarks-menu-button)[open="true"] {
toolbarbutton.bookmark-item:not(.subviewbutton):not(#bookmarks-menu-button)[open="true"] {
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.4), 0 1px rgba(255, 255, 255, 0.4);
background-color: rgba(0, 0, 0, .5);
}

View File

@ -147,19 +147,19 @@ menulist {
background-image: linear-gradient(#FFFFFF, rgba(255,255,255,0.1));
}
button:not([disabled]):hover,
menulist:not([disabled]):hover {
button:not([disabled="true"]):hover,
menulist:not([disabled="true"]):hover {
background-image: linear-gradient(#FFFFFF, rgba(255,255,255,0.6));
}
button:not([disabled]):hover:active,
menulist[open="true"]:not([disabled]) {
button:not([disabled="true"]):hover:active,
menulist[open="true"]:not([disabled="true"]) {
background-image: linear-gradient(rgba(255,255,255,0.1),
rgba(255,255,255,0.6));
}
button[disabled],
menulist[disabled] {
button[disabled="true"],
menulist[disabled="true"] {
background-image: linear-gradient(rgba(255,255,255,0.5),
rgba(255,255,255,0.1));
border-color: rgba(23,50,77,0.25);
@ -208,7 +208,7 @@ button[type="menu"] > .button-box > .button-menu-dropmarker {
list-style-image: url("chrome://global/skin/arrow/arrow-up.gif");
}
.spinbuttons-up[disabled] > .button-box > .button-icon {
.spinbuttons-up[disabled="true"] > .button-box > .button-icon {
list-style-image: url("chrome://global/skin/arrow/arrow-up-dis.gif");
}
@ -216,7 +216,7 @@ button[type="menu"] > .button-box > .button-menu-dropmarker {
list-style-image: url("chrome://global/skin/arrow/arrow-dn.gif");
}
.spinbuttons-down[disabled] > .button-box > .button-icon {
.spinbuttons-down[disabled="true"] > .button-box > .button-icon {
list-style-image: url("chrome://global/skin/arrow/arrow-dn-dis.gif");
}
@ -229,7 +229,7 @@ menulist:not([editable="true"]) > .menulist-dropmarker {
list-style-image: url("chrome://browser/skin/preferences/in-content/dropdown.png")
}
menulist[disabled]:not([editable="true"]) > .menulist-dropmarker {
menulist[disabled="true"]:not([editable="true"]) > .menulist-dropmarker {
list-style-image: url("chrome://browser/skin/preferences/in-content/dropdown-disabled.png")
}
@ -297,7 +297,7 @@ textbox[focused] {
box-shadow: 0 0 2px 2px rgba(0,150,220,0.35), inset 0 0 2px 0 #0096DC;
}
textbox[disabled] {
textbox[disabled="true"] {
color: rgba(115,121,128,0.5);
border-color: rgba(23,50,77,0.25);
background-image: linear-gradient(rgba(255,255,255,0.5), rgba(255,255,255,0.4));

View File

@ -270,18 +270,21 @@ case "$target" in
fi
# Get the api level from "$android_sdk"/source.properties.
android_api_level=`$AWK -F = changequote(<<, >>)'<<$>>1 == "AndroidVersion.ApiLevel" {print <<$>>2}'changequote([, ]) "$android_sdk"/source.properties`
ANDROID_TARGET_SDK=`$AWK -F = changequote(<<, >>)'<<$>>1 == "AndroidVersion.ApiLevel" {print <<$>>2}'changequote([, ]) "$android_sdk"/source.properties`
if test -z "$android_api_level" ; then
if test -z "$ANDROID_TARGET_SDK" ; then
AC_MSG_ERROR([Unexpected error: no AndroidVersion.ApiLevel field has been found in source.properties.])
fi
if ! test "$android_api_level" -eq "$android_api_level" ; then
AC_MSG_ERROR([Unexpected error: the found android api value isn't a number! (found $android_api_level)])
AC_DEFINE_UNQUOTED(ANDROID_TARGET_SDK,$ANDROID_TARGET_SDK)
AC_SUBST(ANDROID_TARGET_SDK)
if ! test "$ANDROID_TARGET_SDK" -eq "$ANDROID_TARGET_SDK" ; then
AC_MSG_ERROR([Unexpected error: the found android api value isn't a number! (found $ANDROID_TARGET_SDK)])
fi
if test $android_api_level -lt $1 ; then
AC_MSG_ERROR([The given Android SDK provides API level $android_api_level ($1 or higher required).])
if test $ANDROID_TARGET_SDK -lt $1 ; then
AC_MSG_ERROR([The given Android SDK provides API level $ANDROID_TARGET_SDK ($1 or higher required).])
fi
fi

View File

@ -6250,31 +6250,41 @@ dnl minimum minor version of Unicode NSIS isn't in the path
dnl (unless in case of cross compiling, for which Unicode
dnl is not yet sufficient).
if test "$OS_ARCH" = "WINNT"; then
REQ_NSIS_MAJOR_VER=2
MIN_NSIS_MAJOR_VER=2
MIN_NSIS_MINOR_VER=46
MOZ_PATH_PROGS(MAKENSISU, $MAKENSISU makensisu-2.46 makensis)
MOZ_PATH_PROGS(MAKENSISU, $MAKENSISU makensisu-3.0a2.exe makensisu-2.46.exe makensis)
if test -n "$MAKENSISU" -a "$MAKENSISU" != ":"; then
AC_MSG_RESULT([yes])
MAKENSISU_VER=`"$MAKENSISU" -version 2>/dev/null`
changequote(,)
MAKENSISU_VER=`"$MAKENSISU" -version 2>/dev/null | sed -e '/-Unicode/!s/.*//g' -e 's/^v\([0-9]\+\.[0-9]\+\).*\-Unicode$/\1/g'`
MAKENSISU_PARSED_VER=`echo "$MAKENSISU_VER" | sed -e '/-Unicode/!s/.*//g' -e 's/^v\([0-9]\+\.[0-9]\+\).*\-Unicode$/\1/g'`
changequote([,])
if test ! "$MAKENSISU_VER" = ""; then
MAKENSISU_MAJOR_VER=`echo $MAKENSISU_VER | $AWK -F\. '{ print $1 }'`
MAKENSISU_MINOR_VER=`echo $MAKENSISU_VER | $AWK -F\. '{ print $2 }'`
if test "$MAKENSISU_PARSED_VER" = ""; then
changequote(,)
MAKENSISU_PARSED_VER=`echo "$MAKENSISU_VER" | sed -e 's/^v\([0-9]\+\.[0-9]\+\).*$/\1/g'`
changequote([,])
fi
AC_MSG_CHECKING([for Unicode NSIS with major version == $REQ_NSIS_MAJOR_VER and minor version >= $MIN_NSIS_MINOR_VER])
if test "$MAKENSISU_VER" = "" || \
test ! "$MAKENSISU_MAJOR_VER" = "$REQ_NSIS_MAJOR_VER" -o \
! "$MAKENSISU_MINOR_VER" -ge $MIN_NSIS_MINOR_VER; then
MAKENSISU_MAJOR_VER=0
MAKENSISU_MINOR_VER=0
if test ! "$MAKENSISU_PARSED_VER" = ""; then
MAKENSISU_MAJOR_VER=`echo $MAKENSISU_PARSED_VER | $AWK -F\. '{ print $1 }'`
MAKENSISU_MINOR_VER=`echo $MAKENSISU_PARSED_VER | $AWK -F\. '{ print $2 }'`
fi
AC_MSG_CHECKING([for Unicode NSIS version $MIN_NSIS_MAJOR_VER.$MIN_NSIS_MINOR_VER or greater])
if test "$MAKENSISU_MAJOR_VER" -eq $MIN_NSIS_MAJOR_VER -a \
"$MAKENSISU_MINOR_VER" -ge $MIN_NSIS_MINOR_VER ||
test "$MAKENSISU_MAJOR_VER" -gt $MIN_NSIS_MAJOR_VER; then
AC_MSG_RESULT([yes])
else
AC_MSG_RESULT([no])
if test -z "$CROSS_COMPILE"; then
AC_MSG_ERROR([To build the installer you must have the latest MozillaBuild or Unicode NSIS with a major version of $REQ_NSIS_MAJOR_VER and a minimum minor version of $MIN_NSIS_MINOR_VER in your path.])
AC_MSG_ERROR([To build the installer you must have the latest MozillaBuild or Unicode NSIS version $REQ_NSIS_MAJOR_VER.$MIN_NSIS_MINOR_VER or greater in your path.])
else
MAKENSISU=
fi
fi
elif test -z "$CROSS_COMPILE"; then
AC_MSG_ERROR([To build the installer you must have the latest MozillaBuild or Unicode NSIS with a major version of $REQ_NSIS_MAJOR_VER and a minimum minor version of $MIN_NSIS_MINOR_VER in your path.])
AC_MSG_ERROR([To build the installer you must have the latest MozillaBuild or Unicode NSIS version $REQ_NSIS_MAJOR_VER.$MIN_NSIS_MINOR_VER or greater in your path.])
else
MAKENSISU=
fi

View File

@ -1,10 +1,11 @@
#filter substitution
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.mozilla.geckoviewexample"
android:versionCode="1"
android:versionName="1.0">
<uses-sdk android:minSdkVersion="8"
android:targetSdkVersion="16"/>
android:targetSdkVersion="@ANDROID_TARGET_SDK@"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.INTERNET"/>
<uses-feature android:glEsVersion="0x00020000" android:required="true" />

View File

@ -1,3 +1,7 @@
PP_TARGETS = properties manifest
manifest = AndroidManifest.xml.in
include $(topsrcdir)/config/rules.mk
GARBAGE = \
@ -22,7 +26,7 @@ GARBAGE_DIRS = \
ANDROID=$(ANDROID_SDK)/../../tools/android
TARGET= $(notdir $(ANDROID_SDK))
TARGET="android-$(ANDROID_TARGET_SDK)"
PACKAGE_DEPS = \
assets/libxul.so \
@ -35,22 +39,20 @@ PACKAGE_DEPS = \
$(CURDIR)/res/layout/main.xml: $(srcdir)/main.xml
$(NSINSTALL) $(srcdir)/main.xml res/layout/
$(CURDIR)/AndroidManifest.xml: $(srcdir)/AndroidManifest.xml
$(NSINSTALL) $(srcdir)/AndroidManifest.xml $(CURDIR)
src/org/mozilla/geckoviewexample/GeckoViewExample.java: $(srcdir)/GeckoViewExample.java
$(NSINSTALL) $(srcdir)/GeckoViewExample.java src/org/mozilla/geckoviewexample/
assets/libxul.so: $(DIST)/geckoview_library/geckoview_assets.zip FORCE
$(UNZIP) -o $(DIST)/geckoview_library/geckoview_assets.zip
build.xml:
build.xml: $(CURDIR)/AndroidManifest.xml
mv AndroidManifest.xml AndroidManifest.xml.save
$(ANDROID) create project --name GeckoViewExample --target $(TARGET) --path $(CURDIR) --activity GeckoViewExample --package org.mozilla.geckoviewexample
$(ANDROID) update project --target $(TARGET) --path $(CURDIR) --library $(DEPTH)/mobile/android/geckoview_library
$(RM) $(CURDIR)/res/layout/main.xml
$(NSINSTALL) $(srcdir)/main.xml res/layout/
$(RM) $(CURDIR)/AndroidManifest.xml
$(NSINSTALL) $(srcdir)/AndroidManifest.xml $(CURDIR)
$(RM) AndroidManifest.xml
mv AndroidManifest.xml.save AndroidManifest.xml
echo jar.libs.dir=libs >> project.properties
package: $(PACKAGE_DEPS) FORCE

View File

@ -10,7 +10,7 @@
#endif
>
<uses-sdk android:minSdkVersion="8"
android:targetSdkVersion="16"/>
android:targetSdkVersion="@ANDROID_TARGET_SDK@"/>
#include ../services/manifests/AnnouncementsAndroidManifest_permissions.xml.in
#include ../services/manifests/FxAccountAndroidManifest_permissions.xml.in

View File

@ -19,6 +19,8 @@ import java.util.concurrent.CopyOnWriteArrayList;
public final class EventDispatcher {
private static final String LOGTAG = "GeckoEventDispatcher";
private static final String GUID = "__guid__";
private static final String SUFFIX_RETURN = "Return";
private static final String SUFFIX_ERROR = "Error";
private final Map<String, CopyOnWriteArrayList<GeckoEventListener>> mEventListeners
= new HashMap<String, CopyOnWriteArrayList<GeckoEventListener>>();
@ -102,21 +104,22 @@ public final class EventDispatcher {
}
public static void sendResponse(JSONObject message, JSONObject response) {
try {
response.put(GUID, message.getString(GUID));
GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent(message.getString("type") + ":Return", response.toString()));
} catch (Exception ex) {
Log.e(LOGTAG, "Unable to send response", ex);
}
public static void sendResponse(JSONObject message, Object response) {
sendResponseHelper(SUFFIX_RETURN, message, response);
}
public static void sendError(JSONObject message, JSONObject error) {
public static void sendError(JSONObject message, Object response) {
sendResponseHelper(SUFFIX_ERROR, message, response);
}
private static void sendResponseHelper(String suffix, JSONObject message, Object response) {
try {
error.put(GUID, message.getString(GUID));
GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent(message.getString("type") + ":Error", error.toString()));
} catch (Exception ex) {
Log.e(LOGTAG, "Unable to send error", ex);
final JSONObject wrapper = new JSONObject();
wrapper.put(GUID, message.getString(GUID));
wrapper.put("response", response);
GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent(message.getString("type") + ":" + suffix, wrapper.toString()));
} catch (Exception e) {
Log.e(LOGTAG, "Unable to send " + suffix, e);
}
}
}

View File

@ -401,7 +401,7 @@ public class AndroidSubmissionClient implements SubmissionClient {
String generationProfilePath) throws JSONException {
final JSONObject document;
// If the given profilePath matches the one we cached for the tracker, use the cached env.
if (generationProfilePath == profilePath) {
if (profilePath != null && profilePath.equals(generationProfilePath)) {
final Environment environment = getCurrentEnvironment();
document = super.generateDocument(since, lastPingTime, environment);
} else {

View File

@ -51,15 +51,4 @@
<item name="android:paddingTop">30dp</item>
</style>
<!--
The content of the banner should align with the Grid/List views
in BookmarksPanel. BookmarksListView has a 120dp padding and
the TwoLinePageRows have a 50dp padding. Hence HomeBanner should
have 170dp padding.
-->
<style name="Widget.HomeBanner">
<item name="android:paddingLeft">170dp</item>
<item name="android:paddingRight">170dp</item>
</style>
</resources>

View File

@ -8,7 +8,6 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import org.mozilla.gecko.background.common.log.Logger;
@ -28,18 +27,17 @@ public class InfoCollections {
*/
final Map<String, Long> timestamps;
@SuppressWarnings("unchecked")
public InfoCollections() {
this(new ExtendedJSONObject());
}
public InfoCollections(final ExtendedJSONObject record) {
Logger.debug(LOG_TAG, "info/collections is " + record.toJSONString());
HashMap<String, Long> map = new HashMap<String, Long>();
Set<Entry<String, Object>> entrySet = record.object.entrySet();
String key;
Object value;
for (Entry<String, Object> entry : entrySet) {
key = entry.getKey();
value = entry.getValue();
for (Entry<String, Object> entry : record.entrySet()) {
final String key = entry.getKey();
final Object value = entry.getValue();
// These objects are most likely going to be Doubles. Regardless, we
// want to get them in a more sane time format.

View File

@ -161,4 +161,24 @@ public abstract class MiddlewareRepositorySession extends RepositorySession {
public void storeDone(long storeEnd) {
inner.storeDone(storeEnd);
}
}
@Override
public boolean shouldSkip() {
return inner.shouldSkip();
}
@Override
public boolean dataAvailable() {
return inner.dataAvailable();
}
@Override
public void unbundle(RepositorySessionBundle bundle) {
inner.unbundle(bundle);
}
@Override
public long getLastSyncTimestamp() {
return inner.getLastSyncTimestamp();
}
}

View File

@ -6,6 +6,7 @@ package org.mozilla.gecko.sync.repositories;
import java.net.URISyntaxException;
import org.mozilla.gecko.sync.InfoCollections;
import org.mozilla.gecko.sync.net.AuthHeaderProvider;
/**
@ -19,8 +20,8 @@ public class ConstrainedServer11Repository extends Server11Repository {
private String sort = null;
private long limit = -1;
public ConstrainedServer11Repository(String collection, String storageURL, AuthHeaderProvider authHeaderProvider, long limit, String sort) throws URISyntaxException {
super(collection, storageURL, authHeaderProvider);
public ConstrainedServer11Repository(String collection, String storageURL, AuthHeaderProvider authHeaderProvider, InfoCollections infoCollections, long limit, String sort) throws URISyntaxException {
super(collection, storageURL, authHeaderProvider, infoCollections);
this.limit = limit;
this.sort = sort;
}

View File

@ -70,7 +70,11 @@ public abstract class RepositorySession {
protected ExecutorService storeWorkQueue = Executors.newSingleThreadExecutor();
// The time that the last sync on this collection completed, in milliseconds since epoch.
public long lastSyncTimestamp;
private long lastSyncTimestamp = 0;
public long getLastSyncTimestamp() {
return lastSyncTimestamp;
}
public static long now() {
return System.currentTimeMillis();
@ -142,10 +146,6 @@ public abstract class RepositorySession {
public abstract void wipe(RepositorySessionWipeDelegate delegate);
public void unbundle(RepositorySessionBundle bundle) {
this.lastSyncTimestamp = bundle == null ? 0 : bundle.getTimestamp();
}
/**
* Synchronously perform the shared work of beginning. Throws on failure.
* @throws InvalidSessionTransitionException
@ -174,8 +174,8 @@ public abstract class RepositorySession {
delegate.deferredBeginDelegate(delegateQueue).onBeginSucceeded(this);
}
protected RepositorySessionBundle getBundle() {
return this.getBundle(null);
public void unbundle(RepositorySessionBundle bundle) {
this.lastSyncTimestamp = bundle == null ? 0 : bundle.getTimestamp();
}
/**
@ -187,10 +187,11 @@ public abstract class RepositorySession {
* The Synchronizer most likely wants to bump the bundle timestamp to be a value
* return from a fetch call.
*/
protected RepositorySessionBundle getBundle(RepositorySessionBundle optional) {
protected RepositorySessionBundle getBundle() {
// Why don't we just persist the old bundle?
RepositorySessionBundle bundle = (optional == null) ? new RepositorySessionBundle(this.lastSyncTimestamp) : optional;
Logger.debug(LOG_TAG, "Setting bundle timestamp to " + this.lastSyncTimestamp + ".");
long timestamp = getLastSyncTimestamp();
RepositorySessionBundle bundle = new RepositorySessionBundle(timestamp);
Logger.debug(LOG_TAG, "Setting bundle timestamp to " + timestamp + ".");
return bundle;
}
@ -201,7 +202,7 @@ public abstract class RepositorySession {
*/
public void abort(RepositorySessionFinishDelegate delegate) {
this.abort();
delegate.deferredFinishDelegate(delegateQueue).onFinishSucceeded(this, this.getBundle(null));
delegate.deferredFinishDelegate(delegateQueue).onFinishSucceeded(this, this.getBundle());
}
/**
@ -233,7 +234,7 @@ public abstract class RepositorySession {
public void finish(final RepositorySessionFinishDelegate delegate) throws InactiveSessionException {
try {
this.transitionFrom(SessionStatus.ACTIVE, SessionStatus.DONE);
delegate.deferredFinishDelegate(delegateQueue).onFinishSucceeded(this, this.getBundle(null));
delegate.deferredFinishDelegate(delegateQueue).onFinishSucceeded(this, this.getBundle());
} catch (InvalidSessionTransitionException e) {
Logger.error(LOG_TAG, "Tried to finish() an unstarted or already finished session");
throw new InactiveSessionException(e);

View File

@ -8,6 +8,7 @@ import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import org.mozilla.gecko.sync.InfoCollections;
import org.mozilla.gecko.sync.Utils;
import org.mozilla.gecko.sync.net.AuthHeaderProvider;
import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionCreationDelegate;
@ -24,19 +25,31 @@ public class Server11Repository extends Repository {
protected String collection;
protected URI collectionURI;
protected final AuthHeaderProvider authHeaderProvider;
protected final InfoCollections infoCollections;
/**
* Construct a new repository that fetches and stores against the Sync 1.1. API.
*
* @param collection name.
* @param storageURL full URL to storage endpoint.
* @param authHeaderProvider to use in requests.
* @param authHeaderProvider to use in requests; may be null.
* @param infoCollections instance; must not be null.
* @throws URISyntaxException
*/
public Server11Repository(String collection, String storageURL, AuthHeaderProvider authHeaderProvider) throws URISyntaxException {
public Server11Repository(String collection, String storageURL, AuthHeaderProvider authHeaderProvider, InfoCollections infoCollections) throws URISyntaxException {
if (collection == null) {
throw new IllegalArgumentException("collection must not be null");
}
if (storageURL == null) {
throw new IllegalArgumentException("storageURL must not be null");
}
if (infoCollections == null) {
throw new IllegalArgumentException("infoCollections must not be null");
}
this.collection = collection;
this.collectionURI = new URI(storageURL + (storageURL.endsWith("/") ? collection : "/" + collection));
this.authHeaderProvider = authHeaderProvider;
this.infoCollections = infoCollections;
}
@Override
@ -102,4 +115,8 @@ public class Server11Repository extends Repository {
public AuthHeaderProvider getAuthHeaderProvider() {
return authHeaderProvider;
}
public boolean updateNeeded(long lastSyncTimestamp) {
return infoCollections.updateNeeded(collection, lastSyncTimestamp);
}
}

View File

@ -411,6 +411,7 @@ public class Server11RepositorySession extends RepositorySession {
*/
protected volatile boolean recordUploadFailed;
@Override
public void begin(RepositorySessionBeginDelegate delegate) throws InvalidSessionTransitionException {
recordUploadFailed = false;
super.begin(delegate);
@ -608,4 +609,9 @@ public class Server11RepositorySession extends RepositorySession {
request.post(body);
}
}
@Override
public boolean dataAvailable() {
return serverRepository.updateNeeded(getLastSyncTimestamp());
}
}

View File

@ -40,6 +40,7 @@ public class WebViewActivity extends SyncActivity {
// Add a progress bar.
final Activity activity = this;
wv.setWebChromeClient(new WebChromeClient() {
@Override
public void onProgressChanged(WebView view, int progress) {
// Activities and WebViews measure progress with different scales.
// The progress meter will automatically disappear when we reach 100%

View File

@ -46,12 +46,13 @@ public class AndroidBrowserBookmarksServerSyncStage extends ServerSyncStage {
final JSONRecordFetcher countsFetcher = new JSONRecordFetcher(session.config.infoCollectionCountsURL(), authHeaderProvider);
String collection = getCollection();
return new SafeConstrainedServer11Repository(
collection,
session.config.storageURL(),
session.getAuthHeaderProvider(),
BOOKMARKS_REQUEST_LIMIT,
BOOKMARKS_SORT,
countsFetcher);
collection,
session.config.storageURL(),
session.getAuthHeaderProvider(),
session.config.infoCollections,
BOOKMARKS_REQUEST_LIMIT,
BOOKMARKS_SORT,
countsFetcher);
}
@Override

View File

@ -49,6 +49,7 @@ public class AndroidBrowserHistoryServerSyncStage extends ServerSyncStage {
collection,
session.config.storageURL(),
session.getAuthHeaderProvider(),
session.config.infoCollections,
HISTORY_REQUEST_LIMIT,
HISTORY_SORT);
}

View File

@ -212,12 +212,7 @@ public class EnsureClusterURLStage extends AbstractNonRepositorySyncStage {
callback.informNodeAssigned(session, oldClusterURL, url); // No matter what, we're getting a new node/weave clusterURL.
session.config.setClusterURL(url);
ThreadPool.run(new Runnable() {
@Override
public void run() {
session.advance();
}
});
session.advance();
}
@Override

View File

@ -41,11 +41,12 @@ public class FormHistoryServerSyncStage extends ServerSyncStage {
protected Repository getRemoteRepository() throws URISyntaxException {
String collection = getCollection();
return new ConstrainedServer11Repository(
collection,
session.config.storageURL(),
session.getAuthHeaderProvider(),
FORM_HISTORY_REQUEST_LIMIT,
FORM_HISTORY_SORT);
collection,
session.config.storageURL(),
session.getAuthHeaderProvider(),
session.config.infoCollections,
FORM_HISTORY_REQUEST_LIMIT,
FORM_HISTORY_SORT);
}
@Override

View File

@ -7,6 +7,7 @@ package org.mozilla.gecko.sync.stage;
import java.net.URISyntaxException;
import org.mozilla.gecko.background.common.log.Logger;
import org.mozilla.gecko.sync.InfoCollections;
import org.mozilla.gecko.sync.InfoCounts;
import org.mozilla.gecko.sync.JSONRecordFetcher;
import org.mozilla.gecko.sync.net.AuthHeaderProvider;
@ -35,11 +36,15 @@ public class SafeConstrainedServer11Repository extends ConstrainedServer11Reposi
public SafeConstrainedServer11Repository(String collection,
String storageURL,
AuthHeaderProvider authHeaderProvider,
InfoCollections infoCollections,
long limit,
String sort,
JSONRecordFetcher countFetcher)
throws URISyntaxException {
super(collection, storageURL, authHeaderProvider, limit, sort);
super(collection, storageURL, authHeaderProvider, infoCollections, limit, sort);
if (countFetcher == null) {
throw new IllegalArgumentException("countFetcher must not be null");
}
this.countFetcher = countFetcher;
}
@ -50,6 +55,8 @@ public class SafeConstrainedServer11Repository extends ConstrainedServer11Reposi
}
public class CountCheckingServer11RepositorySession extends Server11RepositorySession {
private static final String LOG_TAG = "CountCheckingServer11RepositorySession";
/**
* The session will report no data available if this is a first sync
* and the server has more data available than this limit.
@ -64,7 +71,13 @@ public class SafeConstrainedServer11Repository extends ConstrainedServer11Reposi
@Override
public boolean shouldSkip() {
// If this is a first sync, verify that we aren't going to blow through our limit.
if (this.lastSyncTimestamp <= 0) {
final long lastSyncTimestamp = getLastSyncTimestamp();
if (lastSyncTimestamp > 0) {
Logger.info(LOG_TAG, "Collection " + collection + " has already had a first sync: " +
"timestamp is " + lastSyncTimestamp + "; " +
"ignoring any updated counts and syncing as usual.");
} else {
Logger.info(LOG_TAG, "Collection " + collection + " is starting a first sync; checking counts.");
final InfoCounts counts;
try {
@ -77,6 +90,7 @@ public class SafeConstrainedServer11Repository extends ConstrainedServer11Reposi
Integer c = counts.getCount(collection);
if (c == null) {
Logger.info(LOG_TAG, "Fetched counts does not include collection " + collection + "; syncing as usual.");
return false;
}

View File

@ -148,7 +148,8 @@ public abstract class ServerSyncStage extends AbstractSessionManagingSyncStage i
String collection = getCollection();
return new Server11Repository(collection,
session.config.storageURL(),
session.getAuthHeaderProvider());
session.getAuthHeaderProvider(),
session.config.infoCollections);
}
/**

View File

@ -70,7 +70,6 @@ public class RecordsChannel implements
public RepositorySession source;
public RepositorySession sink;
private RecordsChannelDelegate delegate;
private long timestamp;
private long fetchEnd = -1;
protected final AtomicInteger numFetched = new AtomicInteger();
@ -82,7 +81,6 @@ public class RecordsChannel implements
this.source = source;
this.sink = sink;
this.delegate = delegate;
this.timestamp = source.lastSyncTimestamp;
}
/*
@ -155,6 +153,14 @@ public class RecordsChannel implements
this.delegate.onFlowBeginFailed(this, new SessionNotBegunException(failed));
return;
}
if (!source.dataAvailable()) {
Logger.info(LOG_TAG, "No data available: short-circuiting flow from source " + source);
long now = System.currentTimeMillis();
this.delegate.onFlowCompleted(this, now, now);
return;
}
sink.setStoreDelegate(this);
numFetched.set(0);
numFetchFailed.set(0);
@ -164,12 +170,12 @@ public class RecordsChannel implements
this.consumer = new ConcurrentRecordConsumer(this);
ThreadPool.run(this.consumer);
waitingForQueueDone = true;
source.fetchSince(timestamp, this);
source.fetchSince(source.getLastSyncTimestamp(), this);
}
/**
* Begin both sessions, invoking flow() when done.
* @throws InvalidSessionTransitionException
* @throws InvalidSessionTransitionException
*/
public void beginAndFlow() throws InvalidSessionTransitionException {
Logger.trace(LOG_TAG, "Beginning source.");

View File

@ -148,15 +148,6 @@ implements RecordsChannelDelegate,
return;
}
if (!sessionA.dataAvailable() &&
!sessionB.dataAvailable()) {
Logger.info(LOG_TAG, "Neither session reports data available. Short-circuiting sync.");
sessionA.abort();
sessionB.abort();
this.delegate.onSynchronizeSkipped(this);
return;
}
final SynchronizerSession session = this;
// TODO: failed record handling.

View File

@ -7254,7 +7254,7 @@ var WebappsUI = {
manifestURL: aData.app.manifestURL,
origin: origin
}, (data) => {
let profilePath = JSON.parse(data).profile;
let profilePath = data.profile;
if (!profilePath)
return;

View File

@ -1,3 +1,4 @@
#filter substitution
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.mozilla.gecko"
android:versionCode="1"
@ -7,6 +8,6 @@
<uses-permission android:name="android.permission.INTERNET"/>
<uses-sdk
android:minSdkVersion="8"
android:targetSdkVersion="17" />
android:targetSdkVersion="@ANDROID_TARGET_SDK@" />
</manifest>

View File

@ -7,18 +7,16 @@ GECKOVIEW_LIBRARY_DEST = $(CURDIR)
GECKOVIEW_LIBRARY_FILES := \
.classpath \
.project \
AndroidManifest.xml \
project.properties \
build.xml \
$(NULL)
PP_TARGETS = properties
PP_TARGETS = properties manifest project
properties = local.properties.in
properties_PATH = .
properties_deps := $(patsubst %.in,%,$(properties))
project = project.properties.in
manifest = AndroidManifest.xml.in
GARBAGE = $(GECKOVIEW_LIBRARY_FILES) $(properties_deps)
GARBAGE = $(GECKOVIEW_LIBRARY_FILES) local.properties project.properties AndroidManifest.xml
GARBAGE_DIRS = \
bin \
@ -33,7 +31,7 @@ include $(topsrcdir)/config/rules.mk
_ABS_DIST = $(abspath $(DIST))
package: $(properties_deps) FORCE
package: local.properties project.properties AndroidManifest.xml FORCE
# Make directory for the zips
$(MKDIR) -p $(_ABS_DIST)/geckoview_library

View File

@ -1,3 +1,4 @@
#filter substitution
# This file is automatically generated by Android Tools.
# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
#
@ -11,5 +12,5 @@
#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt
# Project target.
target=android-16
target=android-@ANDROID_TARGET_SDK@
android.library=true

View File

@ -42,7 +42,7 @@ let Accounts = Object.freeze({
if (error) {
deferred.reject(error);
} else {
deferred.resolve(JSON.parse(data).exists);
deferred.resolve(data.exists);
}
});

View File

@ -123,10 +123,10 @@ var HelperApps = {
let data = this._sendMessageSync(msg);
if (!data)
return [];
return parseData(JSON.parse(data));
return parseData(data);
} else {
sendMessageToJava(msg, function(data) {
callback(parseData(JSON.parse(data)));
callback(parseData(data));
});
}
},
@ -175,7 +175,7 @@ var HelperApps = {
});
sendMessageToJava(msg, function(data) {
callback(JSON.parse(data));
callback(data);
});
} else {
let msg = this._getMessage("Intent:Open", uri, {

View File

@ -27,8 +27,8 @@ function sendMessageToJava(aMessage, aCallback) {
Services.obs.removeObserver(obs, aMessage.type + ":Return", false);
Services.obs.removeObserver(obs, aMessage.type + ":Error", false);
aCallback(aTopic == aMessage.type + ":Return" ? aData : null,
aTopic == aMessage.type + ":Error" ? aData : null)
aCallback(aTopic == aMessage.type + ":Return" ? data.response : null,
aTopic == aMessage.type + ":Error" ? data.response : null);
}
}

View File

@ -159,17 +159,11 @@ Prompt.prototype = {
show: function(callback) {
this.callback = callback;
log("Sending message");
Services.obs.addObserver(this, "Prompt:Return", false);
this._innerShow();
},
_innerShow: function() {
sendMessageToJava(this.msg, (aData) => {
log("observe " + aData);
let data = JSON.parse(aData);
Services.obs.removeObserver(this, "Prompt:Return", false);
sendMessageToJava(this.msg, (data) => {
if (this.callback)
this.callback(data);
});

View File

@ -66,7 +66,7 @@ SharedPreferences.prototype = Object.freeze({
preferences: prefs,
branch: this._branch,
}, (data) => {
result = JSON.parse(data).values;
result = data.values;
});
let thread = Services.tm.currentThread;

View File

@ -338,7 +338,7 @@ this.WebappManager = {
sendMessageToJava({
type: "Webapps:GetApkVersions",
packageNames: packageNames
}, data => deferred.resolve(JSON.parse(data).versions));
}, data => deferred.resolve(data.versions));
return deferred.promise;
},

View File

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

View File

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

View File

@ -2338,6 +2338,22 @@ nsDownloadManager::OnTitleChanged(nsIURI *aURI,
return NS_OK;
}
NS_IMETHODIMP
nsDownloadManager::OnFrecencyChanged(nsIURI* aURI,
int32_t aNewFrecency,
const nsACString& aGUID,
bool aHidden,
PRTime aLastVisitDate)
{
return NS_OK;
}
NS_IMETHODIMP
nsDownloadManager::OnManyFrecenciesChanged()
{
return NS_OK;
}
NS_IMETHODIMP
nsDownloadManager::OnDeleteURI(nsIURI *aURI,
const nsACString& aGUID,

View File

@ -941,6 +941,8 @@ Database::InitFunctions()
NS_ENSURE_SUCCESS(rv, rv);
rv = FixupURLFunction::create(mMainConn);
NS_ENSURE_SUCCESS(rv, rv);
rv = FrecencyNotificationFunction::create(mMainConn);
NS_ENSURE_SUCCESS(rv, rv);
return NS_OK;
}

View File

@ -1185,7 +1185,10 @@ private:
if (aPlace.placeId) {
stmt = mHistory->GetStatement(
"UPDATE moz_places "
"SET frecency = CALCULATE_FRECENCY(:page_id) "
"SET frecency = NOTIFY_FRECENCY("
"CALCULATE_FRECENCY(:page_id), "
"url, guid, hidden, last_visit_date"
") "
"WHERE id = :page_id"
);
NS_ENSURE_STATE(stmt);
@ -1195,7 +1198,9 @@ private:
else {
stmt = mHistory->GetStatement(
"UPDATE moz_places "
"SET frecency = CALCULATE_FRECENCY(id) "
"SET frecency = NOTIFY_FRECENCY("
"CALCULATE_FRECENCY(id), url, guid, hidden, last_visit_date"
") "
"WHERE url = :page_url"
);
NS_ENSURE_STATE(stmt);
@ -2037,13 +2042,14 @@ History::InsertPlace(const VisitData& aPlace)
NS_ENSURE_SUCCESS(rv, rv);
rv = URIBinder::Bind(stmt, NS_LITERAL_CSTRING("url"), aPlace.spec);
NS_ENSURE_SUCCESS(rv, rv);
nsString title = aPlace.title;
// Empty strings should have no title, just like nsNavHistory::SetPageTitle.
if (aPlace.title.IsEmpty()) {
if (title.IsEmpty()) {
rv = stmt->BindNullByName(NS_LITERAL_CSTRING("title"));
}
else {
rv = stmt->BindStringByName(NS_LITERAL_CSTRING("title"),
StringHead(aPlace.title, TITLE_LENGTH_MAX));
title.Assign(StringHead(aPlace.title, TITLE_LENGTH_MAX));
rv = stmt->BindStringByName(NS_LITERAL_CSTRING("title"), title);
}
NS_ENSURE_SUCCESS(rv, rv);
rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("typed"), aPlace.typed);
@ -2065,6 +2071,13 @@ History::InsertPlace(const VisitData& aPlace)
rv = stmt->Execute();
NS_ENSURE_SUCCESS(rv, rv);
// Post an onFrecencyChanged observer notification.
const nsNavHistory* navHistory = nsNavHistory::GetConstHistoryService();
NS_ENSURE_STATE(navHistory);
navHistory->DispatchFrecencyChangedNotification(aPlace.spec, frecency, guid,
aPlace.hidden,
aPlace.visitTime);
return NS_OK;
}

View File

@ -749,5 +749,63 @@ namespace places {
return NS_OK;
}
////////////////////////////////////////////////////////////////////////////////
//// Frecency Changed Notification Function
/* static */
nsresult
FrecencyNotificationFunction::create(mozIStorageConnection *aDBConn)
{
nsRefPtr<FrecencyNotificationFunction> function =
new FrecencyNotificationFunction();
nsresult rv = aDBConn->CreateFunction(
NS_LITERAL_CSTRING("notify_frecency"), 5, function
);
NS_ENSURE_SUCCESS(rv, rv);
return NS_OK;
}
NS_IMPL_ISUPPORTS1(
FrecencyNotificationFunction,
mozIStorageFunction
)
NS_IMETHODIMP
FrecencyNotificationFunction::OnFunctionCall(mozIStorageValueArray *aArgs,
nsIVariant **_result)
{
uint32_t numArgs;
nsresult rv = aArgs->GetNumEntries(&numArgs);
NS_ENSURE_SUCCESS(rv, rv);
MOZ_ASSERT(numArgs == 5);
int32_t newFrecency = aArgs->AsInt32(0);
nsAutoCString spec;
rv = aArgs->GetUTF8String(1, spec);
NS_ENSURE_SUCCESS(rv, rv);
nsAutoCString guid;
rv = aArgs->GetUTF8String(2, guid);
NS_ENSURE_SUCCESS(rv, rv);
bool hidden = static_cast<bool>(aArgs->AsInt32(3));
PRTime lastVisitDate = static_cast<PRTime>(aArgs->AsInt64(4));
const nsNavHistory* navHistory = nsNavHistory::GetConstHistoryService();
NS_ENSURE_STATE(navHistory);
navHistory->DispatchFrecencyChangedNotification(spec, newFrecency, guid,
hidden, lastVisitDate);
nsCOMPtr<nsIWritableVariant> result =
do_CreateInstance("@mozilla.org/variant;1");
NS_ENSURE_STATE(result);
rv = result->SetAsInt32(newFrecency);
NS_ENSURE_SUCCESS(rv, rv);
NS_ADDREF(*_result = result);
return NS_OK;
}
} // namespace places
} // namespace mozilla

View File

@ -280,6 +280,43 @@ public:
static nsresult create(mozIStorageConnection *aDBConn);
};
////////////////////////////////////////////////////////////////////////////////
//// Frecency Changed Notification Function
/**
* For a given place, posts a runnable to the main thread that calls
* onFrecencyChanged on nsNavHistory's nsINavHistoryObservers. The passed-in
* newFrecency value is returned unchanged.
*
* @param newFrecency
* The place's new frecency.
* @param url
* The place's URL.
* @param guid
* The place's GUID.
* @param hidden
* The place's hidden boolean.
* @param lastVisitDate
* The place's last visit date.
* @return newFrecency
*/
class FrecencyNotificationFunction MOZ_FINAL : public mozIStorageFunction
{
public:
NS_DECL_THREADSAFE_ISUPPORTS
NS_DECL_MOZISTORAGEFUNCTION
/**
* Registers the function with the specified database connection.
*
* @param aDBConn
* The database connection to register with.
*/
static nsresult create(mozIStorageConnection *aDBConn);
};
} // namespace places
} // namespace storage

View File

@ -23,7 +23,7 @@ interface nsINavHistoryQueryOptions;
interface nsINavHistoryResult;
interface nsINavHistoryBatchCallback;
[scriptable, uuid(91d104bb-17ef-404b-9f9a-d9ed8de6824c)]
[scriptable, uuid(7daefb58-6989-4d22-a471-54f0b19b778a)]
interface nsINavHistoryResultNode : nsISupports
{
/**
@ -610,7 +610,7 @@ interface nsINavHistoryResult : nsISupports
* DANGER! If you are in the middle of a batch transaction, there may be a
* database transaction active. You can still access the DB, but be careful.
*/
[scriptable, uuid(45e2970b-9b00-4473-9938-39d6beaf4248)]
[scriptable, uuid(0f0f45b0-13a1-44ae-a0ab-c6046ec6d4da)]
interface nsINavHistoryObserver : nsISupports
{
/**
@ -675,6 +675,37 @@ interface nsINavHistoryObserver : nsISupports
in AString aPageTitle,
in ACString aGUID);
/**
* Called when an individual page's frecency has changed.
*
* This is not called for pages whose frecencies change as the result of some
* large operation where some large or unknown number of frecencies change at
* once. Use onManyFrecenciesChanged to detect such changes.
*
* @param aURI
* The page's URI.
* @param aNewFrecency
* The page's new frecency.
* @param aGUID
* The page's GUID.
* @param aHidden
* True if the page is marked as hidden.
* @param aVisitDate
* The page's last visit date.
*/
void onFrecencyChanged(in nsIURI aURI,
in long aNewFrecency,
in ACString aGUID,
in boolean aHidden,
in PRTime aVisitDate);
/**
* Called when the frecencies of many pages have changed at once.
*
* onFrecencyChanged is not called for each of those pages.
*/
void onManyFrecenciesChanged();
/**
* Removed by the user.
*/

View File

@ -2841,6 +2841,24 @@ nsNavBookmarks::OnTitleChanged(nsIURI* aURI,
}
NS_IMETHODIMP
nsNavBookmarks::OnFrecencyChanged(nsIURI* aURI,
int32_t aNewFrecency,
const nsACString& aGUID,
bool aHidden,
PRTime aLastVisitDate)
{
return NS_OK;
}
NS_IMETHODIMP
nsNavBookmarks::OnManyFrecenciesChanged()
{
return NS_OK;
}
NS_IMETHODIMP
nsNavBookmarks::OnPageChanged(nsIURI* aURI,
uint32_t aChangedAttribute,

View File

@ -535,6 +535,82 @@ nsNavHistory::NotifyTitleChange(nsIURI* aURI,
nsINavHistoryObserver, OnTitleChanged(aURI, aTitle, aGUID));
}
void
nsNavHistory::NotifyFrecencyChanged(nsIURI* aURI,
int32_t aNewFrecency,
const nsACString& aGUID,
bool aHidden,
PRTime aLastVisitDate)
{
MOZ_ASSERT(!aGUID.IsEmpty());
NOTIFY_OBSERVERS(mCanNotify, mCacheObservers, mObservers,
nsINavHistoryObserver,
OnFrecencyChanged(aURI, aNewFrecency, aGUID, aHidden,
aLastVisitDate));
}
void
nsNavHistory::NotifyManyFrecenciesChanged()
{
NOTIFY_OBSERVERS(mCanNotify, mCacheObservers, mObservers,
nsINavHistoryObserver,
OnManyFrecenciesChanged());
}
namespace {
class FrecencyNotification : public nsRunnable
{
public:
FrecencyNotification(const nsACString& aSpec,
int32_t aNewFrecency,
const nsACString& aGUID,
bool aHidden,
PRTime aLastVisitDate)
: mSpec(aSpec)
, mNewFrecency(aNewFrecency)
, mGUID(aGUID)
, mHidden(aHidden)
, mLastVisitDate(aLastVisitDate)
{
}
NS_IMETHOD Run()
{
MOZ_ASSERT(NS_IsMainThread(), "Must be called on the main thread");
nsNavHistory* navHistory = nsNavHistory::GetHistoryService();
if (navHistory) {
nsCOMPtr<nsIURI> uri;
(void)NS_NewURI(getter_AddRefs(uri), mSpec);
navHistory->NotifyFrecencyChanged(uri, mNewFrecency, mGUID, mHidden,
mLastVisitDate);
}
return NS_OK;
}
private:
nsCString mSpec;
int32_t mNewFrecency;
nsCString mGUID;
bool mHidden;
PRTime mLastVisitDate;
};
} // anonymous namespace
void
nsNavHistory::DispatchFrecencyChangedNotification(const nsACString& aSpec,
int32_t aNewFrecency,
const nsACString& aGUID,
bool aHidden,
PRTime aLastVisitDate) const
{
nsCOMPtr<nsIRunnable> notif = new FrecencyNotification(aSpec, aNewFrecency,
aGUID, aHidden,
aLastVisitDate);
(void)NS_DispatchToMainThread(notif);
}
int32_t
nsNavHistory::GetDaysOfHistory() {
MOZ_ASSERT(NS_IsMainThread(), "This can only be called on the main thread");
@ -928,32 +1004,68 @@ nsNavHistory::GetHasHistoryEntries(bool* aHasEntries)
}
namespace {
class InvalidateAllFrecenciesCallback : public AsyncStatementCallback
{
public:
InvalidateAllFrecenciesCallback()
{
}
NS_IMETHOD HandleCompletion(uint16_t aReason)
{
if (aReason == REASON_FINISHED) {
nsNavHistory *navHistory = nsNavHistory::GetHistoryService();
NS_ENSURE_STATE(navHistory);
navHistory->NotifyManyFrecenciesChanged();
}
return NS_OK;
}
};
} // anonymous namespace
nsresult
nsNavHistory::invalidateFrecencies(const nsCString& aPlaceIdsQueryString)
{
// Exclude place: queries by setting their frecency to zero.
nsAutoCString invalideFrecenciesSQLFragment(
"UPDATE moz_places SET frecency = (CASE "
"WHEN url BETWEEN 'place:' AND 'place;' "
"THEN 0 "
"ELSE -1 "
"END) "
nsCString invalidFrecenciesSQLFragment(
"UPDATE moz_places SET frecency = "
);
if (!aPlaceIdsQueryString.IsEmpty())
invalidFrecenciesSQLFragment.AppendLiteral("NOTIFY_FRECENCY(");
invalidFrecenciesSQLFragment.AppendLiteral(
"(CASE "
"WHEN url BETWEEN 'place:' AND 'place;' "
"THEN 0 "
"ELSE -1 "
"END) "
);
if (!aPlaceIdsQueryString.IsEmpty()) {
invalidFrecenciesSQLFragment.AppendLiteral(
", url, guid, hidden, last_visit_date) "
);
}
invalidFrecenciesSQLFragment.AppendLiteral(
"WHERE frecency > 0 "
);
if (!aPlaceIdsQueryString.IsEmpty()) {
invalideFrecenciesSQLFragment.AppendLiteral("AND id IN(");
invalideFrecenciesSQLFragment.Append(aPlaceIdsQueryString);
invalideFrecenciesSQLFragment.AppendLiteral(")");
invalidFrecenciesSQLFragment.AppendLiteral("AND id IN(");
invalidFrecenciesSQLFragment.Append(aPlaceIdsQueryString);
invalidFrecenciesSQLFragment.AppendLiteral(")");
}
nsRefPtr<InvalidateAllFrecenciesCallback> cb =
aPlaceIdsQueryString.IsEmpty() ? new InvalidateAllFrecenciesCallback()
: nullptr;
nsCOMPtr<mozIStorageAsyncStatement> stmt = mDB->GetAsyncStatement(
invalideFrecenciesSQLFragment
invalidFrecenciesSQLFragment
);
NS_ENSURE_STATE(stmt);
nsCOMPtr<mozIStoragePendingStatement> ps;
nsresult rv = stmt->ExecuteAsync(nullptr, getter_AddRefs(ps));
nsresult rv = stmt->ExecuteAsync(cb, getter_AddRefs(ps));
NS_ENSURE_SUCCESS(rv, rv);
return NS_OK;
@ -3078,6 +3190,30 @@ nsNavHistory::Observe(nsISupports *aSubject, const char *aTopic,
}
namespace {
class DecayFrecencyCallback : public AsyncStatementTelemetryTimer
{
public:
DecayFrecencyCallback()
: AsyncStatementTelemetryTimer(Telemetry::PLACES_IDLE_FRECENCY_DECAY_TIME_MS)
{
}
NS_IMETHOD HandleCompletion(uint16_t aReason)
{
(void)AsyncStatementTelemetryTimer::HandleCompletion(aReason);
if (aReason == REASON_FINISHED) {
nsNavHistory *navHistory = nsNavHistory::GetHistoryService();
NS_ENSURE_STATE(navHistory);
navHistory->NotifyManyFrecenciesChanged();
}
return NS_OK;
}
};
} // anonymous namespace
nsresult
nsNavHistory::DecayFrecency()
{
@ -3115,8 +3251,7 @@ nsNavHistory::DecayFrecency()
deleteAdaptive.get()
};
nsCOMPtr<mozIStoragePendingStatement> ps;
nsRefPtr<AsyncStatementTelemetryTimer> cb =
new AsyncStatementTelemetryTimer(Telemetry::PLACES_IDLE_FRECENCY_DECAY_TIME_MS);
nsRefPtr<DecayFrecencyCallback> cb = new DecayFrecencyCallback();
rv = mDB->MainConn()->ExecuteAsync(stmts, ArrayLength(stmts), cb,
getter_AddRefs(ps));
NS_ENSURE_SUCCESS(rv, rv);
@ -4312,7 +4447,9 @@ nsNavHistory::UpdateFrecency(int64_t aPlaceId)
{
nsCOMPtr<mozIStorageAsyncStatement> updateFrecencyStmt = mDB->GetAsyncStatement(
"UPDATE moz_places "
"SET frecency = CALCULATE_FRECENCY(:page_id) "
"SET frecency = NOTIFY_FRECENCY("
"CALCULATE_FRECENCY(:page_id), url, guid, hidden, last_visit_date"
") "
"WHERE id = :page_id"
);
NS_ENSURE_STATE(updateFrecencyStmt);
@ -4345,6 +4482,31 @@ nsNavHistory::UpdateFrecency(int64_t aPlaceId)
}
namespace {
class FixInvalidFrecenciesCallback : public AsyncStatementCallbackNotifier
{
public:
FixInvalidFrecenciesCallback()
: AsyncStatementCallbackNotifier(TOPIC_FRECENCY_UPDATED)
{
}
NS_IMETHOD HandleCompletion(uint16_t aReason)
{
nsresult rv = AsyncStatementCallbackNotifier::HandleCompletion(aReason);
NS_ENSURE_SUCCESS(rv, rv);
if (aReason == REASON_FINISHED) {
nsNavHistory *navHistory = nsNavHistory::GetHistoryService();
NS_ENSURE_STATE(navHistory);
navHistory->NotifyManyFrecenciesChanged();
}
return NS_OK;
}
};
} // anonymous namespace
nsresult
nsNavHistory::FixInvalidFrecencies()
{
@ -4355,8 +4517,8 @@ nsNavHistory::FixInvalidFrecencies()
);
NS_ENSURE_STATE(stmt);
nsRefPtr<AsyncStatementCallbackNotifier> callback =
new AsyncStatementCallbackNotifier(TOPIC_FRECENCY_UPDATED);
nsRefPtr<FixInvalidFrecenciesCallback> callback =
new FixInvalidFrecenciesCallback();
nsCOMPtr<mozIStoragePendingStatement> ps;
(void)stmt->ExecuteAsync(callback, getter_AddRefs(ps));

View File

@ -418,6 +418,29 @@ public:
const nsString& title,
const nsACString& aGUID);
/**
* Fires onFrecencyChanged event to nsINavHistoryService observers
*/
void NotifyFrecencyChanged(nsIURI* aURI,
int32_t aNewFrecency,
const nsACString& aGUID,
bool aHidden,
PRTime aLastVisitDate);
/**
* Fires onManyFrecenciesChanged event to nsINavHistoryService observers
*/
void NotifyManyFrecenciesChanged();
/**
* Posts a runnable to the main thread that calls NotifyFrecencyChanged.
*/
void DispatchFrecencyChangedNotification(const nsACString& aSpec,
int32_t aNewFrecency,
const nsACString& aGUID,
bool aHidden,
PRTime aLastVisitDate) const;
bool isBatching() {
return mBatchLevel > 0;
}

View File

@ -2614,6 +2614,24 @@ nsNavHistoryQueryResultNode::OnTitleChanged(nsIURI* aURI,
}
NS_IMETHODIMP
nsNavHistoryQueryResultNode::OnFrecencyChanged(nsIURI* aURI,
int32_t aNewFrecency,
const nsACString& aGUID,
bool aHidden,
PRTime aLastVisitDate)
{
return NS_OK;
}
NS_IMETHODIMP
nsNavHistoryQueryResultNode::OnManyFrecenciesChanged()
{
return NS_OK;
}
/**
* Here, we can always live update by just deleting all occurrences of
* the given URI.
@ -4662,6 +4680,24 @@ nsNavHistoryResult::OnTitleChanged(nsIURI* aURI,
}
NS_IMETHODIMP
nsNavHistoryResult::OnFrecencyChanged(nsIURI* aURI,
int32_t aNewFrecency,
const nsACString& aGUID,
bool aHidden,
PRTime aLastVisitDate)
{
return NS_OK;
}
NS_IMETHODIMP
nsNavHistoryResult::OnManyFrecenciesChanged()
{
return NS_OK;
}
NS_IMETHODIMP
nsNavHistoryResult::OnDeleteURI(nsIURI *aURI,
const nsACString& aGUID,

View File

@ -64,6 +64,10 @@ private:
NS_DECL_NSINAVBOOKMARKOBSERVER \
NS_IMETHOD OnTitleChanged(nsIURI* aURI, const nsAString& aPageTitle, \
const nsACString& aGUID); \
NS_IMETHOD OnFrecencyChanged(nsIURI* aURI, int32_t aNewFrecency, \
const nsACString& aGUID, bool aHidden, \
PRTime aLastVisitDate); \
NS_IMETHOD OnManyFrecenciesChanged(); \
NS_IMETHOD OnDeleteURI(nsIURI *aURI, const nsACString& aGUID, \
uint16_t aReason); \
NS_IMETHOD OnClearHistory(); \

View File

@ -0,0 +1,85 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
function run_test() {
run_next_test();
}
// Each of these tests a path that triggers a frecency update. Together they
// hit all sites that update a frecency.
// InsertVisitedURIs::UpdateFrecency and History::InsertPlace
add_task(function test_InsertVisitedURIs_UpdateFrecency_and_History_InsertPlace() {
// InsertPlace is at the end of a path that UpdateFrecency is also on, so kill
// two birds with one stone and expect two notifications. Trigger the path by
// adding a download.
let uri = NetUtil.newURI("http://example.com/a");
Cc["@mozilla.org/browser/download-history;1"].
getService(Ci.nsIDownloadHistory).
addDownload(uri);
yield Promise.all([onFrecencyChanged(uri), onFrecencyChanged(uri)]);
});
// nsNavHistory::UpdateFrecency
add_task(function test_nsNavHistory_UpdateFrecency() {
let bm = PlacesUtils.bookmarks;
let uri = NetUtil.newURI("http://example.com/b");
bm.insertBookmark(bm.unfiledBookmarksFolder, uri,
Ci.nsINavBookmarksService.DEFAULT_INDEX, "test");
yield onFrecencyChanged(uri);
});
// nsNavHistory::invalidateFrecencies for particular pages
add_task(function test_nsNavHistory_invalidateFrecencies_somePages() {
let uri = NetUtil.newURI("http://test-nsNavHistory-invalidateFrecencies-somePages.com/");
// Bookmarking the URI is enough to add it to moz_places, and importantly, it
// means that removePagesFromHost doesn't remove it from moz_places, so its
// frecency is able to be changed.
let bm = PlacesUtils.bookmarks;
bm.insertBookmark(bm.unfiledBookmarksFolder, uri,
Ci.nsINavBookmarksService.DEFAULT_INDEX, "test");
PlacesUtils.history.removePagesFromHost(uri.host, false);
yield onFrecencyChanged(uri);
});
// nsNavHistory::invalidateFrecencies for all pages
add_task(function test_nsNavHistory_invalidateFrecencies_allPages() {
PlacesUtils.history.removeAllPages();
yield onManyFrecenciesChanged();
});
// nsNavHistory::DecayFrecency and nsNavHistory::FixInvalidFrecencies
add_task(function test_nsNavHistory_DecayFrecency_and_nsNavHistory_FixInvalidFrecencies() {
// FixInvalidFrecencies is at the end of a path that DecayFrecency is also on,
// so expect two notifications. Trigger the path by making nsNavHistory
// observe the idle-daily notification.
PlacesUtils.history.QueryInterface(Ci.nsIObserver).
observe(null, "idle-daily", "");
yield Promise.all([onManyFrecenciesChanged(), onManyFrecenciesChanged()]);
});
function onFrecencyChanged(expectedURI) {
let deferred = Promise.defer();
let obs = new NavHistoryObserver();
obs.onFrecencyChanged =
(uri, newFrecency, guid, hidden, visitDate) => {
PlacesUtils.history.removeObserver(obs);
do_check_true(!!uri);
do_check_true(uri.equals(expectedURI));
deferred.resolve();
};
PlacesUtils.history.addObserver(obs, false);
return deferred.promise;
}
function onManyFrecenciesChanged() {
let deferred = Promise.defer();
let obs = new NavHistoryObserver();
obs.onManyFrecenciesChanged = () => {
PlacesUtils.history.removeObserver(obs);
do_check_true(true);
deferred.resolve();
};
PlacesUtils.history.addObserver(obs, false);
return deferred.promise;
}

View File

@ -113,6 +113,7 @@ skip-if = true
[test_null_interfaces.js]
[test_onItemChanged_tags.js]
[test_pageGuid_bookmarkGuid.js]
[test_frecency_observers.js]
[test_placeURIs.js]
[test_PlacesUtils_asyncGetBookmarkIds.js]
[test_PlacesUtils_lazyobservers.js]

View File

@ -1098,7 +1098,7 @@ let DebuggerEnvironmentSupport = {
};
exports.JSPropertyProvider = JSPropertyProvider;
exports.JSPropertyProvider = DevToolsUtils.makeInfallible(JSPropertyProvider);
})(WebConsoleUtils);
///////////////////////////////////////////////////////////////////////////////

View File

@ -0,0 +1,74 @@
/* 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";
this.EXPORTED_SYMBOLS = [
"BinarySearch",
];
this.BinarySearch = Object.freeze({
/**
* Returns the index of the given target in the given array or -1 if the
* target is not found.
*
* See search() for a description of this function's parameters.
*
* @return The index of `target` in `array` or -1 if `target` is not found.
*/
indexOf: function (array, target, comparator) {
let [found, idx] = this.search(array, target, comparator);
return found ? idx : -1;
},
/**
* Returns the index within the given array where the given target may be
* inserted to keep the array ordered.
*
* See search() for a description of this function's parameters.
*
* @return The index in `array` where `target` may be inserted to keep `array`
* ordered.
*/
insertionIndexOf: function (array, target, comparator) {
return this.search(array, target, comparator)[1];
},
/**
* Searches for the given target in the given array.
*
* @param array
* An array whose elements are ordered by `comparator`.
* @param target
* The value to search for.
* @param comparator
* A function that takes two arguments and compares them, returning a
* negative number if the first should be ordered before the second,
* zero if the first and second have the same ordering, or a positive
* number if the second should be ordered before the first. The first
* argument is always `target`, and the second argument is a value
* from the array.
* @return An array with two elements. If `target` is found, the first
* element is true, and the second element is its index in the array.
* If `target` is not found, the first element is false, and the
* second element is the index where it may be inserted to keep the
* array ordered.
*/
search: function (array, target, comparator) {
let low = 0;
let high = array.length - 1;
while (low <= high) {
let mid = Math.floor((low + high) / 2);
let cmp = comparator(target, array[mid]);
if (cmp == 0)
return [true, mid];
if (cmp < 0)
high = mid - 1;
else
low = mid + 1;
}
return [false, low];
},
});

View File

@ -19,6 +19,13 @@ XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
XPCOMUtils.defineLazyModuleGetter(this, "PageThumbs",
"resource://gre/modules/PageThumbs.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "BinarySearch",
"resource://gre/modules/BinarySearch.jsm");
XPCOMUtils.defineLazyGetter(this, "Timer", () => {
return Cu.import("resource://gre/modules/Timer.jsm", {});
});
XPCOMUtils.defineLazyGetter(this, "gPrincipal", function () {
let uri = Services.io.newURI("about:newtab", null, null);
return Services.scriptSecurityManager.getNoAppCodebasePrincipal(uri);
@ -44,12 +51,18 @@ const PREF_NEWTAB_ROWS = "browser.newtabpage.rows";
// The preference that tells the number of columns of the newtab grid.
const PREF_NEWTAB_COLUMNS = "browser.newtabpage.columns";
// The maximum number of results we want to retrieve from history.
// The maximum number of results PlacesProvider retrieves from history.
const HISTORY_RESULTS_LIMIT = 100;
// The maximum number of links Links.getLinks will return.
const LINKS_GET_LINKS_LIMIT = 100;
// The gather telemetry topic.
const TOPIC_GATHER_TELEMETRY = "gather-telemetry";
// The amount of time we wait while coalescing updates for hidden pages.
const SCHEDULE_UPDATE_TIMEOUT_MS = 1000;
/**
* Calculate the MD5 hash for a string.
* @param aValue
@ -244,14 +257,34 @@ let AllPages = {
/**
* Updates all currently active pages but the given one.
* @param aExceptPage The page to exclude from updating.
* @param aHiddenPagesOnly If true, only pages hidden in the preloader are
* updated.
*/
update: function AllPages_update(aExceptPage) {
update: function AllPages_update(aExceptPage, aHiddenPagesOnly=false) {
this._pages.forEach(function (aPage) {
if (aExceptPage != aPage)
aPage.update();
aPage.update(aHiddenPagesOnly);
});
},
/**
* Many individual link changes may happen in a small amount of time over
* multiple turns of the event loop. This method coalesces updates by waiting
* a small amount of time before updating hidden pages.
*/
scheduleUpdateForHiddenPages: function AllPages_scheduleUpdateForHiddenPages() {
if (!this._scheduleUpdateTimeout) {
this._scheduleUpdateTimeout = Timer.setTimeout(() => {
delete this._scheduleUpdateTimeout;
this.update(null, true);
}, SCHEDULE_UPDATE_TIMEOUT_MS);
}
},
get updateScheduledForHiddenPages() {
return !!this._scheduleUpdateTimeout;
},
/**
* Implements the nsIObserver interface to get notified when the preference
* value changes or when a new copy of a page thumbnail is available.
@ -504,13 +537,25 @@ let BlockedLinks = {
* the history to retrieve the most frequently visited sites.
*/
let PlacesProvider = {
/**
* Set this to change the maximum number of links the provider will provide.
*/
maxNumLinks: HISTORY_RESULTS_LIMIT,
/**
* Must be called before the provider is used.
*/
init: function PlacesProvider_init() {
PlacesUtils.history.addObserver(this, true);
},
/**
* Gets the current set of links delivered by this provider.
* @param aCallback The function that the array of links is passed to.
*/
getLinks: function PlacesProvider_getLinks(aCallback) {
let options = PlacesUtils.history.getNewQueryOptions();
options.maxResults = HISTORY_RESULTS_LIMIT;
options.maxResults = this.maxNumLinks;
// Sort by frecency, descending.
options.sortingMode = Ci.nsINavHistoryQueryOptions.SORT_BY_FRECENCY_DESCENDING
@ -525,7 +570,14 @@ let PlacesProvider = {
let url = row.getResultByIndex(1);
if (LinkChecker.checkLoadURI(url)) {
let title = row.getResultByIndex(2);
links.push({url: url, title: title});
let frecency = row.getResultByIndex(12);
let lastVisitDate = row.getResultByIndex(5);
links.push({
url: url,
title: title,
frecency: frecency,
lastVisitDate: lastVisitDate,
});
}
}
},
@ -536,6 +588,26 @@ let PlacesProvider = {
},
handleCompletion: function (aReason) {
// The Places query breaks ties in frecency by place ID descending, but
// that's different from how Links.compareLinks breaks ties, because
// compareLinks doesn't have access to place IDs. It's very important
// that the initial list of links is sorted in the same order imposed by
// compareLinks, because Links uses compareLinks to perform binary
// searches on the list. So, ensure the list is so ordered.
let i = 1;
let outOfOrder = [];
while (i < links.length) {
if (Links.compareLinks(links[i - 1], links[i]) > 0)
outOfOrder.push(links.splice(i, 1)[0]);
else
i++;
}
for (let link of outOfOrder) {
i = BinarySearch.insertionIndexOf(links, link,
Links.compareLinks.bind(Links));
links.splice(i, 0, link);
}
aCallback(links);
}
};
@ -544,28 +616,116 @@ let PlacesProvider = {
let query = PlacesUtils.history.getNewQuery();
let db = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase);
db.asyncExecuteLegacyQueries([query], 1, options, callback);
}
},
/**
* Registers an object that will be notified when the provider's links change.
* @param aObserver An object with the following optional properties:
* * onLinkChanged: A function that's called when a single link
* changes. It's passed the provider and the link object. Only the
* link's `url` property is guaranteed to be present. If its `title`
* property is present, then its title has changed, and the
* property's value is the new title. If any sort properties are
* present, then its position within the provider's list of links may
* have changed, and the properties' values are the new sort-related
* values. Note that this link may not necessarily have been present
* in the lists returned from any previous calls to getLinks.
* * onManyLinksChanged: A function that's called when many links
* change at once. It's passed the provider. You should call
* getLinks to get the provider's new list of links.
*/
addObserver: function PlacesProvider_addObserver(aObserver) {
this._observers.push(aObserver);
},
_observers: [],
/**
* Called by the history service.
*/
onFrecencyChanged: function PlacesProvider_onFrecencyChanged(aURI, aNewFrecency, aGUID, aHidden, aLastVisitDate) {
// The implementation of the query in getLinks excludes hidden and
// unvisited pages, so it's important to exclude them here, too.
if (!aHidden && aLastVisitDate) {
this._callObservers("onLinkChanged", {
url: aURI.spec,
frecency: aNewFrecency,
lastVisitDate: aLastVisitDate,
});
}
},
/**
* Called by the history service.
*/
onManyFrecenciesChanged: function PlacesProvider_onManyFrecenciesChanged() {
this._callObservers("onManyLinksChanged");
},
/**
* Called by the history service.
*/
onTitleChanged: function PlacesProvider_onTitleChanged(aURI, aNewTitle, aGUID) {
this._callObservers("onLinkChanged", {
url: aURI.spec,
title: aNewTitle
});
},
_callObservers: function PlacesProvider__callObservers(aMethodName, aArg) {
for (let obs of this._observers) {
if (obs[aMethodName]) {
try {
obs[aMethodName](this, aArg);
} catch (err) {
Cu.reportError(err);
}
}
}
},
QueryInterface: XPCOMUtils.generateQI([Ci.nsINavHistoryObserver,
Ci.nsISupportsWeakReference]),
};
/**
* Singleton that provides access to all links contained in the grid (including
* the ones that don't fit on the grid). A link is a plain object with title
* and url properties.
* the ones that don't fit on the grid). A link is a plain object that looks
* like this:
*
* Example:
*
* {url: "http://www.mozilla.org/", title: "Mozilla"}
* {
* url: "http://www.mozilla.org/",
* title: "Mozilla",
* frecency: 1337,
* lastVisitDate: 1394678824766431,
* }
*/
let Links = {
/**
* The links cache.
* The maximum number of links returned by getLinks.
*/
_links: null,
maxNumLinks: LINKS_GET_LINKS_LIMIT,
/**
* The default provider for links.
* The link providers.
*/
_provider: PlacesProvider,
_providers: new Set(),
/**
* A mapping from each provider to an object { sortedLinks, linkMap }.
* sortedLinks is the cached, sorted array of links for the provider. linkMap
* is a Map from link URLs to link objects.
*/
_providerLinks: new Map(),
/**
* The properties of link objects used to sort them.
*/
_sortProperties: [
"frecency",
"lastVisitDate",
"url",
],
/**
* List of callbacks waiting for the cache to be populated.
@ -573,7 +733,26 @@ let Links = {
_populateCallbacks: [],
/**
* Populates the cache with fresh links from the current provider.
* Adds a link provider.
* @param aProvider The link provider.
*/
addProvider: function Links_addProvider(aProvider) {
this._providers.add(aProvider);
aProvider.addObserver(this);
},
/**
* Removes a link provider.
* @param aProvider The link provider.
*/
removeProvider: function Links_removeProvider(aProvider) {
if (!this._providers.delete(aProvider))
throw new Error("Unknown provider");
this._providerLinks.delete(aProvider);
},
/**
* Populates the cache with fresh links from the providers.
* @param aCallback The callback to call when finished (optional).
* @param aForce When true, populates the cache even when it's already filled.
*/
@ -601,16 +780,15 @@ let Links = {
}
}
if (this._links && !aForce) {
executeCallbacks();
} else {
this._provider.getLinks(function (aLinks) {
this._links = aLinks;
executeCallbacks();
}.bind(this));
this._addObserver();
let numProvidersRemaining = this._providers.size;
for (let provider of this._providers) {
this._populateProviderCache(provider, () => {
if (--numProvidersRemaining == 0)
executeCallbacks();
}, aForce);
}
this._addObserver();
},
/**
@ -619,9 +797,10 @@ let Links = {
*/
getLinks: function Links_getLinks() {
let pinnedLinks = Array.slice(PinnedLinks.links);
let links = this._getMergedProviderLinks();
// Filter blocked and pinned links.
let links = (this._links || []).filter(function (link) {
links = links.filter(function (link) {
return !BlockedLinks.isBlocked(link) && !PinnedLinks.isPinned(link);
});
@ -641,7 +820,186 @@ let Links = {
* Resets the links cache.
*/
resetCache: function Links_resetCache() {
this._links = null;
this._providerLinks.clear();
},
/**
* Compares two links.
* @param aLink1 The first link.
* @param aLink2 The second link.
* @return A negative number if aLink1 is ordered before aLink2, zero if
* aLink1 and aLink2 have the same ordering, or a positive number if
* aLink1 is ordered after aLink2.
*/
compareLinks: function Links_compareLinks(aLink1, aLink2) {
for (let prop of this._sortProperties) {
if (!(prop in aLink1) || !(prop in aLink2))
throw new Error("Comparable link missing required property: " + prop);
}
return aLink2.frecency - aLink1.frecency ||
aLink2.lastVisitDate - aLink1.lastVisitDate ||
aLink1.url.localeCompare(aLink2.url);
},
/**
* Calls getLinks on the given provider and populates our cache for it.
* @param aProvider The provider whose cache will be populated.
* @param aCallback The callback to call when finished.
* @param aForce When true, populates the provider's cache even when it's
* already filled.
*/
_populateProviderCache: function Links_populateProviderCache(aProvider, aCallback, aForce) {
if (this._providerLinks.has(aProvider) && !aForce) {
aCallback();
} else {
aProvider.getLinks(links => {
// Filter out null and undefined links so we don't have to deal with
// them in getLinks when merging links from providers.
links = links.filter((link) => !!link);
this._providerLinks.set(aProvider, {
sortedLinks: links,
linkMap: links.reduce((map, link) => {
map.set(link.url, link);
return map;
}, new Map()),
});
aCallback();
});
}
},
/**
* Merges the cached lists of links from all providers whose lists are cached.
* @return The merged list.
*/
_getMergedProviderLinks: function Links__getMergedProviderLinks() {
// Build a list containing a copy of each provider's sortedLinks list.
let linkLists = [];
for (let links of this._providerLinks.values()) {
linkLists.push(links.sortedLinks.slice());
}
function getNextLink() {
let minLinks = null;
for (let links of linkLists) {
if (links.length &&
(!minLinks || Links.compareLinks(links[0], minLinks[0]) < 0))
minLinks = links;
}
return minLinks ? minLinks.shift() : null;
}
let finalLinks = [];
for (let nextLink = getNextLink();
nextLink && finalLinks.length < this.maxNumLinks;
nextLink = getNextLink()) {
finalLinks.push(nextLink);
}
return finalLinks;
},
/**
* Called by a provider to notify us when a single link changes.
* @param aProvider The provider whose link changed.
* @param aLink The link that changed. If the link is new, it must have all
* of the _sortProperties. Otherwise, it may have as few or as
* many as is convenient.
*/
onLinkChanged: function Links_onLinkChanged(aProvider, aLink) {
if (!("url" in aLink))
throw new Error("Changed links must have a url property");
let links = this._providerLinks.get(aProvider);
if (!links)
// This is not an error, it just means that between the time the provider
// was added and the future time we call getLinks on it, it notified us of
// a change.
return;
let { sortedLinks, linkMap } = links;
// Nothing to do if the list is full and the link isn't in it and shouldn't
// be in it.
if (!linkMap.has(aLink.url) &&
sortedLinks.length &&
sortedLinks.length == aProvider.maxNumLinks) {
let lastLink = sortedLinks[sortedLinks.length - 1];
if (this.compareLinks(lastLink, aLink) < 0)
return;
}
let updatePages = false;
// Update the title in O(1).
if ("title" in aLink) {
let link = linkMap.get(aLink.url);
if (link && link.title != aLink.title) {
link.title = aLink.title;
updatePages = true;
}
}
// Update the link's position in O(lg n).
if (this._sortProperties.some((prop) => prop in aLink)) {
let link = linkMap.get(aLink.url);
if (link) {
// The link is already in the list.
let idx = this._indexOf(sortedLinks, link);
if (idx < 0)
throw new Error("Link should be in _sortedLinks if in _linkMap");
sortedLinks.splice(idx, 1);
for (let prop of this._sortProperties) {
if (prop in aLink)
link[prop] = aLink[prop];
}
}
else {
// The link is new.
for (let prop of this._sortProperties) {
if (!(prop in aLink))
throw new Error("New link missing required sort property: " + prop);
}
// Copy the link object so that if the caller changes it, it doesn't
// screw up our bookkeeping.
link = {};
for (let [prop, val] of Iterator(aLink)) {
link[prop] = val;
}
linkMap.set(link.url, link);
}
let idx = this._insertionIndexOf(sortedLinks, link);
sortedLinks.splice(idx, 0, link);
if (sortedLinks.length > aProvider.maxNumLinks) {
let lastLink = sortedLinks.pop();
linkMap.delete(lastLink.url);
}
updatePages = true;
}
if (updatePages)
AllPages.scheduleUpdateForHiddenPages();
},
/**
* Called by a provider to notify us when many links change.
*/
onManyLinksChanged: function Links_onManyLinksChanged(aProvider) {
this._populateProviderCache(aProvider, () => {
AllPages.scheduleUpdateForHiddenPages();
}, true);
},
_indexOf: function Links__indexOf(aArray, aLink) {
return this._binsearch(aArray, aLink, "indexOf");
},
_insertionIndexOf: function Links__insertionIndexOf(aArray, aLink) {
return this._binsearch(aArray, aLink, "insertionIndexOf");
},
_binsearch: function Links__binsearch(aArray, aLink, aMethod) {
return BinarySearch[aMethod](aArray, aLink, this.compareLinks.bind(this));
},
/**
@ -654,7 +1012,7 @@ let Links = {
if (AllPages.length && AllPages.enabled)
this.populateCache(function () { AllPages.update() }, true);
else
this._links = null;
this.resetCache();
},
/**
@ -774,11 +1132,20 @@ this.NewTabUtils = {
_initialized: false,
init: function NewTabUtils_init() {
if (this.initWithoutProviders()) {
PlacesProvider.init();
Links.addProvider(PlacesProvider);
}
},
initWithoutProviders: function NewTabUtils_initWithoutProviders() {
if (!this._initialized) {
this._initialized = true;
ExpirationFilter.init();
Telemetry.init();
return true;
}
return false;
},
/**

View File

@ -11,6 +11,7 @@ MOCHITEST_CHROME_MANIFESTS += ['tests/chrome/chrome.ini']
EXTRA_JS_MODULES += [
'AsyncShutdown.jsm',
'BinarySearch.jsm',
'BrowserUtils.jsm',
'CharsetMenu.jsm',
'debug.js',

View File

@ -0,0 +1,81 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
Components.utils.import("resource://gre/modules/BinarySearch.jsm");
function run_test() {
// empty array
ok([], 1, false, 0);
// one-element array
ok([2], 2, true, 0);
ok([2], 1, false, 0);
ok([2], 3, false, 1);
// two-element array
ok([2, 4], 2, true, 0);
ok([2, 4], 4, true, 1);
ok([2, 4], 1, false, 0);
ok([2, 4], 3, false, 1);
ok([2, 4], 5, false, 2);
// three-element array
ok([2, 4, 6], 2, true, 0);
ok([2, 4, 6], 4, true, 1);
ok([2, 4, 6], 6, true, 2);
ok([2, 4, 6], 1, false, 0);
ok([2, 4, 6], 3, false, 1);
ok([2, 4, 6], 5, false, 2);
ok([2, 4, 6], 7, false, 3);
// duplicates
ok([2, 2], 2, true, 0);
ok([2, 2], 1, false, 0);
ok([2, 2], 3, false, 2);
// duplicates on the left
ok([2, 2, 4], 2, true, 1);
ok([2, 2, 4], 4, true, 2);
ok([2, 2, 4], 1, false, 0);
ok([2, 2, 4], 3, false, 2);
ok([2, 2, 4], 5, false, 3);
// duplicates on the right
ok([2, 4, 4], 2, true, 0);
ok([2, 4, 4], 4, true, 1);
ok([2, 4, 4], 1, false, 0);
ok([2, 4, 4], 3, false, 1);
ok([2, 4, 4], 5, false, 3);
// duplicates in the middle
ok([2, 4, 4, 6], 2, true, 0);
ok([2, 4, 4, 6], 4, true, 1);
ok([2, 4, 4, 6], 6, true, 3);
ok([2, 4, 4, 6], 1, false, 0);
ok([2, 4, 4, 6], 3, false, 1);
ok([2, 4, 4, 6], 5, false, 3);
ok([2, 4, 4, 6], 7, false, 4);
// duplicates all around
ok([2, 2, 4, 4, 6, 6], 2, true, 0);
ok([2, 2, 4, 4, 6, 6], 4, true, 2);
ok([2, 2, 4, 4, 6, 6], 6, true, 4);
ok([2, 2, 4, 4, 6, 6], 1, false, 0);
ok([2, 2, 4, 4, 6, 6], 3, false, 2);
ok([2, 2, 4, 4, 6, 6], 5, false, 4);
ok([2, 2, 4, 4, 6, 6], 7, false, 6);
}
function ok(array, target, expectedFound, expectedIdx) {
let [found, idx] = BinarySearch.search(array, target, cmp);
do_check_eq(found, expectedFound);
do_check_eq(idx, expectedIdx);
idx = expectedFound ? expectedIdx : -1;
do_check_eq(BinarySearch.indexOf(array, target, cmp), idx);
do_check_eq(BinarySearch.insertionIndexOf(array, target, cmp), expectedIdx);
}
function cmp(num1, num2) {
return num1 - num2;
}

View File

@ -0,0 +1,176 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
// See also browser/base/content/test/newtab/.
const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
Cu.import("resource://gre/modules/NewTabUtils.jsm");
Cu.import("resource://gre/modules/Promise.jsm");
function run_test() {
run_next_test();
}
add_test(function multipleProviders() {
// Make each provider generate NewTabUtils.links.maxNumLinks links to check
// that no more than maxNumLinks are actually returned in the merged list.
let evenLinks = makeLinks(0, 2 * NewTabUtils.links.maxNumLinks, 2);
let evenProvider = new TestProvider(done => done(evenLinks));
let oddLinks = makeLinks(0, 2 * NewTabUtils.links.maxNumLinks - 1, 2);
let oddProvider = new TestProvider(done => done(oddLinks));
NewTabUtils.initWithoutProviders();
NewTabUtils.links.addProvider(evenProvider);
NewTabUtils.links.addProvider(oddProvider);
// This is sync since the providers' getLinks are sync.
NewTabUtils.links.populateCache(function () {}, false);
let links = NewTabUtils.links.getLinks();
let expectedLinks = makeLinks(NewTabUtils.links.maxNumLinks,
2 * NewTabUtils.links.maxNumLinks,
1);
do_check_eq(links.length, NewTabUtils.links.maxNumLinks);
do_check_links(links, expectedLinks);
NewTabUtils.links.removeProvider(evenProvider);
NewTabUtils.links.removeProvider(oddProvider);
run_next_test();
});
add_test(function changeLinks() {
let expectedLinks = makeLinks(0, 20, 2);
let provider = new TestProvider(done => done(expectedLinks));
NewTabUtils.initWithoutProviders();
NewTabUtils.links.addProvider(provider);
// This is sync since the provider's getLinks is sync.
NewTabUtils.links.populateCache(function () {}, false);
do_check_links(NewTabUtils.links.getLinks(), expectedLinks);
// Notify of a new link.
let newLink = {
url: "http://example.com/19",
title: "My frecency is 19",
frecency: 19,
lastVisitDate: 0,
};
expectedLinks.splice(1, 0, newLink);
provider.notifyLinkChanged(newLink);
do_check_links(NewTabUtils.links.getLinks(), expectedLinks);
// Notify of a link that's changed sort criteria.
newLink.frecency = 17;
expectedLinks.splice(1, 1);
expectedLinks.splice(2, 0, newLink);
provider.notifyLinkChanged({
url: newLink.url,
frecency: 17,
});
do_check_links(NewTabUtils.links.getLinks(), expectedLinks);
// Notify of a link that's changed title.
newLink.title = "My frecency is now 17";
provider.notifyLinkChanged({
url: newLink.url,
title: newLink.title,
});
do_check_links(NewTabUtils.links.getLinks(), expectedLinks);
// Notify of a new link again, but this time make it overflow maxNumLinks.
provider.maxNumLinks = expectedLinks.length;
newLink = {
url: "http://example.com/21",
frecency: 21,
lastVisitDate: 0,
};
expectedLinks.unshift(newLink);
expectedLinks.pop();
do_check_eq(expectedLinks.length, provider.maxNumLinks); // Sanity check.
provider.notifyLinkChanged(newLink);
do_check_links(NewTabUtils.links.getLinks(), expectedLinks);
// Notify of many links changed.
expectedLinks = makeLinks(0, 3, 1);
provider.notifyManyLinksChanged();
// NewTabUtils.links will now repopulate its cache, which is sync since
// the provider's getLinks is sync.
do_check_links(NewTabUtils.links.getLinks(), expectedLinks);
NewTabUtils.links.removeProvider(provider);
run_next_test();
});
add_task(function oneProviderAlreadyCached() {
let links1 = makeLinks(0, 10, 1);
let provider1 = new TestProvider(done => done(links1));
NewTabUtils.initWithoutProviders();
NewTabUtils.links.addProvider(provider1);
// This is sync since the provider's getLinks is sync.
NewTabUtils.links.populateCache(function () {}, false);
do_check_links(NewTabUtils.links.getLinks(), links1);
let links2 = makeLinks(10, 20, 1);
let provider2 = new TestProvider(done => done(links2));
NewTabUtils.links.addProvider(provider2);
NewTabUtils.links.populateCache(function () {}, false);
do_check_links(NewTabUtils.links.getLinks(), links2.concat(links1));
NewTabUtils.links.removeProvider(provider1);
NewTabUtils.links.removeProvider(provider2);
});
function TestProvider(getLinksFn) {
this.getLinks = getLinksFn;
this._observers = new Set();
}
TestProvider.prototype = {
addObserver: function (observer) {
this._observers.add(observer);
},
notifyLinkChanged: function (link) {
this._notifyObservers("onLinkChanged", link);
},
notifyManyLinksChanged: function () {
this._notifyObservers("onManyLinksChanged");
},
_notifyObservers: function (observerMethodName, arg) {
for (let obs of this._observers) {
if (obs[observerMethodName])
obs[observerMethodName](this, arg);
}
},
};
function do_check_links(actualLinks, expectedLinks) {
do_check_true(Array.isArray(actualLinks));
do_check_eq(actualLinks.length, expectedLinks.length);
for (let i = 0; i < expectedLinks.length; i++) {
let expected = expectedLinks[i];
let actual = actualLinks[i];
do_check_eq(actual.url, expected.url);
do_check_eq(actual.title, expected.title);
do_check_eq(actual.frecency, expected.frecency);
do_check_eq(actual.lastVisitDate, expected.lastVisitDate);
}
}
function makeLinks(frecRangeStart, frecRangeEnd, step) {
let links = [];
// Remember, links are ordered by frecency descending.
for (let i = frecRangeEnd; i > frecRangeStart; i -= step) {
links.push({
url: "http://example.com/" + i,
title: "My frecency is " + i,
frecency: i,
lastVisitDate: 0,
});
}
return links;
}

View File

@ -8,12 +8,14 @@ support-files =
zips/zen.zip
[test_AsyncShutdown.js]
[test_BinarySearch.js]
[test_DeferredTask.js]
[test_dict.js]
[test_DirectoryLinksProvider.js]
[test_FileUtils.js]
[test_Http.js]
[test_Log.js]
[test_NewTabUtils.js]
[test_PermissionsUtils.js]
[test_Preferences.js]
[test_Promise.js]