зеркало из https://github.com/mozilla/gecko-dev.git
Merge f-t to m-c
This commit is contained in:
Коммит
d3f1570fbc
|
@ -299,6 +299,7 @@
|
||||||
@BINPATH@/components/spellchecker.xpt
|
@BINPATH@/components/spellchecker.xpt
|
||||||
@BINPATH@/components/storage.xpt
|
@BINPATH@/components/storage.xpt
|
||||||
@BINPATH@/components/telemetry.xpt
|
@BINPATH@/components/telemetry.xpt
|
||||||
|
@BINPATH@/components/toolkit_finalizationwitness.xpt
|
||||||
@BINPATH@/components/toolkitprofile.xpt
|
@BINPATH@/components/toolkitprofile.xpt
|
||||||
#ifdef MOZ_ENABLE_XREMOTE
|
#ifdef MOZ_ENABLE_XREMOTE
|
||||||
@BINPATH@/components/toolkitremote.xpt
|
@BINPATH@/components/toolkitremote.xpt
|
||||||
|
|
|
@ -685,6 +685,11 @@ SourcesView.prototype = Heritage.extend(WidgetMethods, {
|
||||||
}
|
}
|
||||||
// The container is not empty and an actual item was selected.
|
// The container is not empty and an actual item was selected.
|
||||||
DebuggerView.setEditorLocation(sourceItem.value);
|
DebuggerView.setEditorLocation(sourceItem.value);
|
||||||
|
|
||||||
|
// Set window title.
|
||||||
|
let script = sourceItem.value.split(" -> ").pop();
|
||||||
|
document.title = L10N.getFormatStr("DebuggerWindowScriptTitle", script);
|
||||||
|
|
||||||
this.maybeShowBlackBoxMessage();
|
this.maybeShowBlackBoxMessage();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -68,6 +68,7 @@ let DebuggerView = {
|
||||||
this.GlobalSearch.initialize();
|
this.GlobalSearch.initialize();
|
||||||
this._initializeVariablesView();
|
this._initializeVariablesView();
|
||||||
this._initializeEditor(deferred.resolve);
|
this._initializeEditor(deferred.resolve);
|
||||||
|
document.title = L10N.getStr("DebuggerWindowTitle");
|
||||||
|
|
||||||
return deferred.promise;
|
return deferred.promise;
|
||||||
},
|
},
|
||||||
|
|
|
@ -19,6 +19,9 @@ function test() {
|
||||||
gEditor = gDebugger.DebuggerView.editor;
|
gEditor = gDebugger.DebuggerView.editor;
|
||||||
gSources = gDebugger.DebuggerView.Sources;
|
gSources = gDebugger.DebuggerView.Sources;
|
||||||
|
|
||||||
|
ok(gDebugger.document.title.endsWith(EXAMPLE_URL + gLabel1),
|
||||||
|
"Title with first source is correct.");
|
||||||
|
|
||||||
waitForSourceAndCaretAndScopes(gPanel, "-02.js", 6)
|
waitForSourceAndCaretAndScopes(gPanel, "-02.js", 6)
|
||||||
.then(testSourcesDisplay)
|
.then(testSourcesDisplay)
|
||||||
.then(testSwitchPaused1)
|
.then(testSwitchPaused1)
|
||||||
|
@ -64,6 +67,9 @@ function testSourcesDisplay() {
|
||||||
is(gEditor.getText().search(/debugger/), 172,
|
is(gEditor.getText().search(/debugger/), 172,
|
||||||
"The second source is displayed.");
|
"The second source is displayed.");
|
||||||
|
|
||||||
|
ok(gDebugger.document.title.endsWith(EXAMPLE_URL + gLabel2),
|
||||||
|
"Title with second source is correct.");
|
||||||
|
|
||||||
ok(isCaretPos(gPanel, 6),
|
ok(isCaretPos(gPanel, 6),
|
||||||
"Editor caret location is correct.");
|
"Editor caret location is correct.");
|
||||||
|
|
||||||
|
|
|
@ -305,6 +305,7 @@
|
||||||
@BINPATH@/components/shistory.xpt
|
@BINPATH@/components/shistory.xpt
|
||||||
@BINPATH@/components/spellchecker.xpt
|
@BINPATH@/components/spellchecker.xpt
|
||||||
@BINPATH@/components/storage.xpt
|
@BINPATH@/components/storage.xpt
|
||||||
|
@BINPATH@/components/toolkit_finalizationwitness.xpt
|
||||||
@BINPATH@/components/toolkitprofile.xpt
|
@BINPATH@/components/toolkitprofile.xpt
|
||||||
#ifdef MOZ_ENABLE_XREMOTE
|
#ifdef MOZ_ENABLE_XREMOTE
|
||||||
@BINPATH@/components/toolkitremote.xpt
|
@BINPATH@/components/toolkitremote.xpt
|
||||||
|
|
|
@ -15,6 +15,14 @@
|
||||||
# displayed inside the developer tools window and in the Developer Tools Menu.
|
# displayed inside the developer tools window and in the Developer Tools Menu.
|
||||||
ToolboxDebugger.label=Debugger
|
ToolboxDebugger.label=Debugger
|
||||||
|
|
||||||
|
# LOCALIZATION NOTE (DebuggerWindowTitle):
|
||||||
|
# The title displayed for the debugger window.
|
||||||
|
DebuggerWindowTitle=Browser Debugger
|
||||||
|
|
||||||
|
# LOCALIZATION NOTE (DebuggerWindowScriptTitle):
|
||||||
|
# The title displayed for the debugger window when a script is selected.
|
||||||
|
DebuggerWindowScriptTitle=Browser Debugger - %S
|
||||||
|
|
||||||
# LOCALIZATION NOTE (ToolboxDebugger.tooltip):
|
# LOCALIZATION NOTE (ToolboxDebugger.tooltip):
|
||||||
# This string is displayed in the tooltip of the tab when the debugger is
|
# This string is displayed in the tooltip of the tab when the debugger is
|
||||||
# displayed inside the developer tools window..
|
# displayed inside the developer tools window..
|
||||||
|
|
|
@ -89,9 +89,6 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=338583
|
||||||
e.target.hits['fn_other_event_name']++;
|
e.target.hits['fn_other_event_name']++;
|
||||||
}
|
}
|
||||||
|
|
||||||
var domBranch;
|
|
||||||
var oldPrefVal;
|
|
||||||
|
|
||||||
var gEventSourceObj1 = null, gEventSourceObj1_e, gEventSourceObj1_f;
|
var gEventSourceObj1 = null, gEventSourceObj1_e, gEventSourceObj1_f;
|
||||||
var gEventSourceObj2 = null;
|
var gEventSourceObj2 = null;
|
||||||
var gEventSourceObj3_a = null, gEventSourceObj3_b = null,
|
var gEventSourceObj3_a = null, gEventSourceObj3_b = null,
|
||||||
|
@ -250,6 +247,12 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=338583
|
||||||
}
|
}
|
||||||
|
|
||||||
function doTest3_b(test_id) {
|
function doTest3_b(test_id) {
|
||||||
|
// currently no support yet for local files for b2g/Android mochitest, see bug 838726
|
||||||
|
if (navigator.appVersion.indexOf("Android") != -1 || SpecialPowers.Services.appinfo.name == "B2G") {
|
||||||
|
setTestHasFinished(test_id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var xhr = new XMLHttpRequest;
|
var xhr = new XMLHttpRequest;
|
||||||
xhr.open("GET", "/dynamic/getMyDirectory.sjs", false);
|
xhr.open("GET", "/dynamic/getMyDirectory.sjs", false);
|
||||||
xhr.send();
|
xhr.send();
|
||||||
|
@ -462,7 +465,7 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=338583
|
||||||
function doTest5_c(test_id)
|
function doTest5_c(test_id)
|
||||||
{
|
{
|
||||||
// credentials using the auth cache and cookies
|
// credentials using the auth cache and cookies
|
||||||
var xhr = SpecialPowers.createSystemXHR();
|
var xhr = new XMLHttpRequest({mozAnon: false, mozSystem: true});
|
||||||
xhr.withCredentials = true;
|
xhr.withCredentials = true;
|
||||||
// also, test mixed mode UI
|
// also, test mixed mode UI
|
||||||
xhr.open("GET", "https://example.com/tests/content/base/test/file_restrictedEventSource.sjs?test=user1_xhr", true, "user 1", "password 1");
|
xhr.open("GET", "https://example.com/tests/content/base/test/file_restrictedEventSource.sjs?test=user1_xhr", true, "user 1", "password 1");
|
||||||
|
@ -491,7 +494,7 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=338583
|
||||||
|
|
||||||
function doTest5_d(test_id)
|
function doTest5_d(test_id)
|
||||||
{
|
{
|
||||||
var xhr = SpecialPowers.createSystemXHR();
|
var xhr = new XMLHttpRequest({mozAnon: false, mozSystem: true});
|
||||||
xhr.withCredentials = true;
|
xhr.withCredentials = true;
|
||||||
xhr.open("GET", "https://example.com/tests/content/base/test/file_restrictedEventSource.sjs?test=user2_xhr", true, "user 2", "password 2");
|
xhr.open("GET", "https://example.com/tests/content/base/test/file_restrictedEventSource.sjs?test=user2_xhr", true, "user 2", "password 2");
|
||||||
xhr.send();
|
xhr.send();
|
||||||
|
@ -519,7 +522,7 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=338583
|
||||||
function doTest5_e(test_id)
|
function doTest5_e(test_id)
|
||||||
{
|
{
|
||||||
// credentials using the auth cache and cookies
|
// credentials using the auth cache and cookies
|
||||||
var xhr = SpecialPowers.createSystemXHR();
|
var xhr = new XMLHttpRequest({mozAnon: false, mozSystem: true});
|
||||||
xhr.withCredentials = true;
|
xhr.withCredentials = true;
|
||||||
xhr.open("GET", "http://example.org/tests/content/base/test/file_restrictedEventSource.sjs?test=user1_xhr", true, "user 1", "password 1");
|
xhr.open("GET", "http://example.org/tests/content/base/test/file_restrictedEventSource.sjs?test=user1_xhr", true, "user 1", "password 1");
|
||||||
xhr.send();
|
xhr.send();
|
||||||
|
@ -547,7 +550,7 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=338583
|
||||||
|
|
||||||
function doTest5_f(test_id)
|
function doTest5_f(test_id)
|
||||||
{
|
{
|
||||||
var xhr = SpecialPowers.createSystemXHR();
|
var xhr = new XMLHttpRequest({mozAnon: false, mozSystem: true});
|
||||||
xhr.withCredentials = true;
|
xhr.withCredentials = true;
|
||||||
xhr.open("GET", "http://example.org/tests/content/base/test/file_restrictedEventSource.sjs?test=user2_xhr", true, "user 2", "password 2");
|
xhr.open("GET", "http://example.org/tests/content/base/test/file_restrictedEventSource.sjs?test=user2_xhr", true, "user 2", "password 2");
|
||||||
xhr.send();
|
xhr.send();
|
||||||
|
@ -614,7 +617,6 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=338583
|
||||||
gEventSourceObj7.msg_received[1] == "delayed1" &&
|
gEventSourceObj7.msg_received[1] == "delayed1" &&
|
||||||
gEventSourceObj7.msg_received[2] == "delayed2", "Test 7 failed");
|
gEventSourceObj7.msg_received[2] == "delayed2", "Test 7 failed");
|
||||||
|
|
||||||
SpecialPowers.setBoolPref("dom.server-events.enabled", oldPrefVal);
|
|
||||||
document.getElementById('waitSpan').innerHTML = '';
|
document.getElementById('waitSpan').innerHTML = '';
|
||||||
setTestHasFinished(test_id);
|
setTestHasFinished(test_id);
|
||||||
}, parseInt(8000*stress_factor));
|
}, parseInt(8000*stress_factor));
|
||||||
|
@ -623,13 +625,11 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=338583
|
||||||
function doTest()
|
function doTest()
|
||||||
{
|
{
|
||||||
// Allow all cookies, then run the actual test
|
// Allow all cookies, then run the actual test
|
||||||
SpecialPowers.pushPrefEnv({"set": [["network.cookie.cookieBehavior", 0]]}, doTestCallback);
|
SpecialPowers.pushPrefEnv({"set": [["network.cookie.cookieBehavior", 0], ["dom.server-events.enabled", true]]}, function() { SpecialPowers.pushPermissions([{'type': 'systemXHR', 'allow': true, 'context': document}], doTestCallback);});
|
||||||
}
|
}
|
||||||
|
|
||||||
function doTestCallback()
|
function doTestCallback()
|
||||||
{
|
{
|
||||||
oldPrefVal = SpecialPowers.getBoolPref("dom.server-events.enabled");
|
|
||||||
SpecialPowers.setBoolPref("dom.server-events.enabled", true);
|
|
||||||
|
|
||||||
// we get a good stress_factor by testing 10 setTimeouts and some float
|
// we get a good stress_factor by testing 10 setTimeouts and some float
|
||||||
// arithmetic taking my machine as stress_factor==1 (time=589)
|
// arithmetic taking my machine as stress_factor==1 (time=589)
|
||||||
|
|
|
@ -21,12 +21,21 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=426308
|
||||||
|
|
||||||
const SJS_URL = "http://example.org:80/tests/content/base/test/bug426308-redirect.sjs";
|
const SJS_URL = "http://example.org:80/tests/content/base/test/bug426308-redirect.sjs";
|
||||||
|
|
||||||
var req = SpecialPowers.createSystemXHR();
|
function startTest() {
|
||||||
req.open("GET", SJS_URL + "?" + window.location.href, false);
|
var req = new XMLHttpRequest({mozAnon: false, mozSystem: true});
|
||||||
req.send(null);
|
req.open("GET", SJS_URL + "?" + window.location.href, false);
|
||||||
|
req.send(null);
|
||||||
|
|
||||||
is(req.status, 200, "Redirect did not happen");
|
is(req.status, 200, "Redirect did not happen");
|
||||||
|
|
||||||
|
SimpleTest.finish();
|
||||||
|
}
|
||||||
|
|
||||||
|
SimpleTest.waitForExplicitFinish();
|
||||||
|
|
||||||
|
addLoadEvent(function() {
|
||||||
|
SpecialPowers.pushPermissions([{'type': 'systemXHR', 'allow': true, 'context': document}], startTest);
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
</pre>
|
</pre>
|
||||||
</body>
|
</body>
|
||||||
|
|
|
@ -51,7 +51,7 @@ function createDoc() {
|
||||||
function xhrDoc(idx) {
|
function xhrDoc(idx) {
|
||||||
return function() {
|
return function() {
|
||||||
// Defy same-origin restrictions!
|
// Defy same-origin restrictions!
|
||||||
var xhr = SpecialPowers.createSystemXHR();
|
var xhr = new XMLHttpRequest({mozAnon: false, mozSystem: true});
|
||||||
xhr.open("GET", docSources[idx], false);
|
xhr.open("GET", docSources[idx], false);
|
||||||
xhr.send();
|
xhr.send();
|
||||||
return xhr.responseXML;
|
return xhr.responseXML;
|
||||||
|
@ -87,6 +87,10 @@ function doTest(idx) {
|
||||||
}
|
}
|
||||||
|
|
||||||
addLoadEvent(function() {
|
addLoadEvent(function() {
|
||||||
|
SpecialPowers.pushPermissions([{'type': 'systemXHR', 'allow': true, 'context': document}], startTest);
|
||||||
|
});
|
||||||
|
|
||||||
|
function startTest() {
|
||||||
// sanity check
|
// sanity check
|
||||||
isnot("", null, "Shouldn't be equal!");
|
isnot("", null, "Shouldn't be equal!");
|
||||||
|
|
||||||
|
@ -104,7 +108,7 @@ addLoadEvent(function() {
|
||||||
xhr.abort();
|
xhr.abort();
|
||||||
|
|
||||||
SimpleTest.finish();
|
SimpleTest.finish();
|
||||||
});
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -19,7 +19,7 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=804395
|
||||||
<script type="application/javascript">
|
<script type="application/javascript">
|
||||||
|
|
||||||
function test200() {
|
function test200() {
|
||||||
var xhr = SpecialPowers.createSystemXHR();
|
var xhr = new XMLHttpRequest({mozAnon: false, mozSystem: true});
|
||||||
xhr.open('GET', 'jar:http://example.org/tests/content/base/test/file_bug804395.jar!/foo.bar', true);
|
xhr.open('GET', 'jar:http://example.org/tests/content/base/test/file_bug804395.jar!/foo.bar', true);
|
||||||
xhr.onreadystatechange = function() {
|
xhr.onreadystatechange = function() {
|
||||||
if (xhr.readyState == 4) {
|
if (xhr.readyState == 4) {
|
||||||
|
@ -31,7 +31,7 @@ function test200() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function test404() {
|
function test404() {
|
||||||
var xhr = SpecialPowers.createSystemXHR();
|
var xhr = new XMLHttpRequest({mozAnon: false, mozSystem: true});
|
||||||
xhr.open('GET', 'jar:http://example.org/tests/content/base/test/file_bug804395.jar!/foo.do_not_exist', true);
|
xhr.open('GET', 'jar:http://example.org/tests/content/base/test/file_bug804395.jar!/foo.do_not_exist', true);
|
||||||
xhr.onreadystatechange = function() {
|
xhr.onreadystatechange = function() {
|
||||||
if (xhr.readyState == 4) {
|
if (xhr.readyState == 4) {
|
||||||
|
@ -43,7 +43,7 @@ function test404() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function test0() {
|
function test0() {
|
||||||
var xhr = SpecialPowers.createSystemXHR();
|
var xhr = new XMLHttpRequest({mozAnon: false, mozSystem: true});
|
||||||
xhr.open('GET', 'jar:http://example.org/tests/content/base/test/file_bug804395.jar!/foo.bar', true);
|
xhr.open('GET', 'jar:http://example.org/tests/content/base/test/file_bug804395.jar!/foo.bar', true);
|
||||||
ok(xhr.status == 0, "Not Sent request must have status 0");
|
ok(xhr.status == 0, "Not Sent request must have status 0");
|
||||||
runTests();
|
runTests();
|
||||||
|
@ -61,9 +61,11 @@ function runTests() {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Test for Bug 804395 **/
|
/** Test for Bug 804395 **/
|
||||||
runTests();
|
|
||||||
SimpleTest.waitForExplicitFinish();
|
SimpleTest.waitForExplicitFinish();
|
||||||
|
|
||||||
|
addLoadEvent(function() {
|
||||||
|
SpecialPowers.pushPermissions([{'type': 'systemXHR', 'allow': true, 'context': document}], runTests);
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
</pre>
|
</pre>
|
||||||
</body>
|
</body>
|
||||||
|
|
|
@ -49,37 +49,47 @@ var headers = [
|
||||||
];
|
];
|
||||||
var i, request;
|
var i, request;
|
||||||
|
|
||||||
// Try setting headers in unprivileged context
|
function startTest() {
|
||||||
request = new XMLHttpRequest();
|
// Try setting headers in unprivileged context
|
||||||
request.open("GET", window.location.href);
|
request = new XMLHttpRequest();
|
||||||
for (i = 0; i < headers.length; i++)
|
request.open("GET", window.location.href);
|
||||||
request.setRequestHeader(headers[i], "test" + i);
|
for (i = 0; i < headers.length; i++)
|
||||||
|
request.setRequestHeader(headers[i], "test" + i);
|
||||||
|
|
||||||
// Read out headers
|
// Read out headers
|
||||||
var channel = SpecialPowers.wrap(request).channel.QueryInterface(SpecialPowers.Ci.nsIHttpChannel);
|
var channel = SpecialPowers.wrap(request).channel.QueryInterface(SpecialPowers.Ci.nsIHttpChannel);
|
||||||
for (i = 0; i < headers.length; i++) {
|
for (i = 0; i < headers.length; i++) {
|
||||||
// Retrieving Content-Length will throw an exception
|
// Retrieving Content-Length will throw an exception
|
||||||
var value = null;
|
var value = null;
|
||||||
try {
|
try {
|
||||||
value = channel.getRequestHeader(headers[i]);
|
value = channel.getRequestHeader(headers[i]);
|
||||||
|
}
|
||||||
|
catch(e) {}
|
||||||
|
|
||||||
|
isnot(value, "test" + i, "Setting " + headers[i] + " header in unprivileged context");
|
||||||
}
|
}
|
||||||
catch(e) {}
|
|
||||||
|
|
||||||
isnot(value, "test" + i, "Setting " + headers[i] + " header in unprivileged context");
|
// Try setting headers in privileged context
|
||||||
|
request = new XMLHttpRequest({mozAnon: false, mozSystem: true});
|
||||||
|
request.open("GET", window.location.href);
|
||||||
|
for (i = 0; i < headers.length; i++)
|
||||||
|
request.setRequestHeader(headers[i], "test" + i);
|
||||||
|
|
||||||
|
// Read out headers
|
||||||
|
var channel = SpecialPowers.wrap(request).channel.QueryInterface(SpecialPowers.Ci.nsIHttpChannel);
|
||||||
|
for (i = 0; i < headers.length; i++) {
|
||||||
|
var value = channel.getRequestHeader(headers[i]);
|
||||||
|
is(value, "test" + i, "Setting " + headers[i] + " header in privileged context");
|
||||||
|
}
|
||||||
|
|
||||||
|
SimpleTest.finish();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try setting headers in privileged context
|
SimpleTest.waitForExplicitFinish();
|
||||||
request = SpecialPowers.createSystemXHR();
|
|
||||||
request.open("GET", window.location.href);
|
|
||||||
for (i = 0; i < headers.length; i++)
|
|
||||||
request.setRequestHeader(headers[i], "test" + i);
|
|
||||||
|
|
||||||
// Read out headers
|
addLoadEvent(function() {
|
||||||
var channel = request.channel.QueryInterface(SpecialPowers.Ci.nsIHttpChannel);
|
SpecialPowers.pushPermissions([{'type': 'systemXHR', 'allow': true, 'context': document}], startTest);
|
||||||
for (i = 0; i < headers.length; i++) {
|
});
|
||||||
var value = channel.getRequestHeader(headers[i]);
|
|
||||||
is(value, "test" + i, "Setting " + headers[i] + " header in privileged context");
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
</pre>
|
</pre>
|
||||||
</body>
|
</body>
|
||||||
|
|
|
@ -15,6 +15,10 @@
|
||||||
#include "sys/stat.h"
|
#include "sys/stat.h"
|
||||||
#endif // defined(XP_UNIX)
|
#endif // defined(XP_UNIX)
|
||||||
|
|
||||||
|
#if defined(XP_LINUX)
|
||||||
|
#include <linux/fadvise.h>
|
||||||
|
#endif // defined(XP_LINUX)
|
||||||
|
|
||||||
#if defined(XP_MACOSX)
|
#if defined(XP_MACOSX)
|
||||||
#include "copyfile.h"
|
#include "copyfile.h"
|
||||||
#endif // defined(XP_MACOSX)
|
#endif // defined(XP_MACOSX)
|
||||||
|
@ -377,6 +381,10 @@ static const dom::ConstantSpec gLibcProperties[] =
|
||||||
INT_CONSTANT(AT_SYMLINK_NOFOLLOW),
|
INT_CONSTANT(AT_SYMLINK_NOFOLLOW),
|
||||||
#endif //defined(AT_SYMLINK_NOFOLLOW)
|
#endif //defined(AT_SYMLINK_NOFOLLOW)
|
||||||
|
|
||||||
|
#if defined(POSIX_FADV_SEQUENTIAL)
|
||||||
|
INT_CONSTANT(POSIX_FADV_SEQUENTIAL),
|
||||||
|
#endif //defined(POSIX_FADV_SEQUENTIAL)
|
||||||
|
|
||||||
// access
|
// access
|
||||||
#if defined(F_OK)
|
#if defined(F_OK)
|
||||||
INT_CONSTANT(F_OK),
|
INT_CONSTANT(F_OK),
|
||||||
|
|
|
@ -57,7 +57,6 @@ import android.text.TextUtils;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.view.InputDevice;
|
import android.view.InputDevice;
|
||||||
import android.view.KeyEvent;
|
import android.view.KeyEvent;
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
import android.view.MenuInflater;
|
import android.view.MenuInflater;
|
||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
|
@ -597,7 +596,7 @@ abstract public class BrowserApp extends GeckoApp
|
||||||
String title = tab.getDisplayTitle();
|
String title = tab.getDisplayTitle();
|
||||||
Bitmap favicon = tab.getFavicon();
|
Bitmap favicon = tab.getFavicon();
|
||||||
if (url != null && title != null) {
|
if (url != null && title != null) {
|
||||||
GeckoAppShell.createShortcut(title, url, url, favicon == null ? null : favicon, "");
|
GeckoAppShell.createShortcut(title, url, url, favicon, "");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -710,11 +709,11 @@ abstract public class BrowserApp extends GeckoApp
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
Favicons.loadFavicon(url, tab.getFaviconURL(), 0,
|
Favicons.getFaviconForSize(url, tab.getFaviconURL(), Integer.MAX_VALUE, LoadFaviconTask.FLAG_PERSIST,
|
||||||
new OnFaviconLoadedListener() {
|
new OnFaviconLoadedListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onFaviconLoaded(String url, Bitmap favicon) {
|
public void onFaviconLoaded(String pageUrl, String faviconURL, Bitmap favicon) {
|
||||||
GeckoAppShell.createShortcut(title, url, url, favicon == null ? null : favicon, "");
|
GeckoAppShell.createShortcut(title, url, url, favicon, "");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -1280,7 +1279,7 @@ abstract public class BrowserApp extends GeckoApp
|
||||||
}
|
}
|
||||||
|
|
||||||
// If this tab is already selected, just hide the home pager.
|
// If this tab is already selected, just hide the home pager.
|
||||||
if (tabs.isSelectedTab(tabs.getTab(tabId))) {
|
if (tabs.isSelectedTabId(tabId)) {
|
||||||
hideHomePager();
|
hideHomePager();
|
||||||
} else {
|
} else {
|
||||||
tabs.selectTab(tabId);
|
tabs.selectTab(tabId);
|
||||||
|
@ -1330,28 +1329,30 @@ abstract public class BrowserApp extends GeckoApp
|
||||||
private void loadFavicon(final Tab tab) {
|
private void loadFavicon(final Tab tab) {
|
||||||
maybeCancelFaviconLoad(tab);
|
maybeCancelFaviconLoad(tab);
|
||||||
|
|
||||||
int flags = LoadFaviconTask.FLAG_SCALE | ( (tab.isPrivate() || tab.getErrorType() != Tab.ErrorType.NONE) ? 0 : LoadFaviconTask.FLAG_PERSIST);
|
final int tabFaviconSize = getResources().getDimensionPixelSize(R.dimen.browser_toolbar_favicon_size);
|
||||||
int id = Favicons.loadFavicon(tab.getURL(), tab.getFaviconURL(), flags,
|
|
||||||
new OnFaviconLoadedListener() {
|
|
||||||
|
|
||||||
@Override
|
int flags = (tab.isPrivate() || tab.getErrorType() != Tab.ErrorType.NONE) ? 0 : LoadFaviconTask.FLAG_PERSIST;
|
||||||
public void onFaviconLoaded(String pageUrl, Bitmap favicon) {
|
int id = Favicons.getFaviconForSize(tab.getURL(), tab.getFaviconURL(), tabFaviconSize, flags,
|
||||||
// Leave favicon UI untouched if we failed to load the image
|
new OnFaviconLoadedListener() {
|
||||||
// for some reason.
|
@Override
|
||||||
if (favicon == null)
|
public void onFaviconLoaded(String pageUrl, String faviconURL, Bitmap favicon) {
|
||||||
return;
|
// If we failed to load a favicon, we use the default favicon instead.
|
||||||
|
if (favicon == null) {
|
||||||
|
favicon = Favicons.sDefaultFavicon;
|
||||||
|
}
|
||||||
|
|
||||||
// The tab might be pointing to another URL by the time the
|
// The tab might be pointing to another URL by the time the
|
||||||
// favicon is finally loaded, in which case we simply ignore it.
|
// favicon is finally loaded, in which case we simply ignore it.
|
||||||
if (!tab.getURL().equals(pageUrl))
|
// See also: Bug 920331.
|
||||||
return;
|
if (!tab.getURL().equals(pageUrl)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
tab.updateFavicon(favicon);
|
tab.updateFavicon(favicon);
|
||||||
tab.setFaviconLoadId(Favicons.NOT_LOADING);
|
tab.setFaviconLoadId(Favicons.NOT_LOADING);
|
||||||
|
Tabs.getInstance().notifyListeners(tab, Tabs.TabEvents.FAVICON);
|
||||||
Tabs.getInstance().notifyListeners(tab, Tabs.TabEvents.FAVICON);
|
}
|
||||||
}
|
});
|
||||||
});
|
|
||||||
|
|
||||||
tab.setFaviconLoadId(id);
|
tab.setFaviconLoadId(id);
|
||||||
}
|
}
|
||||||
|
@ -1359,9 +1360,6 @@ abstract public class BrowserApp extends GeckoApp
|
||||||
private void maybeCancelFaviconLoad(Tab tab) {
|
private void maybeCancelFaviconLoad(Tab tab) {
|
||||||
int faviconLoadId = tab.getFaviconLoadId();
|
int faviconLoadId = tab.getFaviconLoadId();
|
||||||
|
|
||||||
if (faviconLoadId == Favicons.NOT_LOADING)
|
|
||||||
return;
|
|
||||||
|
|
||||||
// Cancel pending favicon load task
|
// Cancel pending favicon load task
|
||||||
Favicons.cancelFaviconLoad(faviconLoadId);
|
Favicons.cancelFaviconLoad(faviconLoadId);
|
||||||
|
|
||||||
|
|
|
@ -1121,7 +1121,7 @@ public class BrowserToolbar extends GeckoRelativeLayout
|
||||||
image = Bitmap.createScaledBitmap(image, mFaviconSize, mFaviconSize, false);
|
image = Bitmap.createScaledBitmap(image, mFaviconSize, mFaviconSize, false);
|
||||||
mFavicon.setImageBitmap(image);
|
mFavicon.setImageBitmap(image);
|
||||||
} else {
|
} else {
|
||||||
mFavicon.setImageResource(R.drawable.favicon);
|
mFavicon.setImageBitmap(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1192,7 +1192,11 @@ abstract public class GeckoApp
|
||||||
ThreadUtils.setUiThread(Thread.currentThread(), new Handler());
|
ThreadUtils.setUiThread(Thread.currentThread(), new Handler());
|
||||||
|
|
||||||
Tabs.getInstance().attachToContext(this);
|
Tabs.getInstance().attachToContext(this);
|
||||||
Favicons.attachToContext(this);
|
try {
|
||||||
|
Favicons.attachToContext(this);
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(LOGTAG, "Exception starting favicon cache. Corrupt resources?", e);
|
||||||
|
}
|
||||||
|
|
||||||
// When we detect a locale change, we need to restart Gecko, which
|
// When we detect a locale change, we need to restart Gecko, which
|
||||||
// actually means restarting the entire application. This logic should
|
// actually means restarting the entire application. This logic should
|
||||||
|
|
|
@ -76,6 +76,9 @@ FENNEC_JAVA_FILES = \
|
||||||
DoorHanger.java \
|
DoorHanger.java \
|
||||||
DoorHangerPopup.java \
|
DoorHangerPopup.java \
|
||||||
EditBookmarkDialog.java \
|
EditBookmarkDialog.java \
|
||||||
|
favicons/cache/FaviconCache.java \
|
||||||
|
favicons/cache/FaviconCacheElement.java \
|
||||||
|
favicons/cache/FaviconsForURL.java \
|
||||||
favicons/Favicons.java \
|
favicons/Favicons.java \
|
||||||
favicons/LoadFaviconTask.java \
|
favicons/LoadFaviconTask.java \
|
||||||
favicons/OnFaviconLoadedListener.java \
|
favicons/OnFaviconLoadedListener.java \
|
||||||
|
|
|
@ -7,7 +7,6 @@ package org.mozilla.gecko;
|
||||||
|
|
||||||
import org.mozilla.gecko.db.BrowserDB;
|
import org.mozilla.gecko.db.BrowserDB;
|
||||||
import org.mozilla.gecko.home.HomePager;
|
import org.mozilla.gecko.home.HomePager;
|
||||||
import org.mozilla.gecko.ReaderModeUtils;
|
|
||||||
import org.mozilla.gecko.sync.setup.SyncAccounts;
|
import org.mozilla.gecko.sync.setup.SyncAccounts;
|
||||||
import org.mozilla.gecko.util.GeckoEventListener;
|
import org.mozilla.gecko.util.GeckoEventListener;
|
||||||
import org.mozilla.gecko.util.ThreadUtils;
|
import org.mozilla.gecko.util.ThreadUtils;
|
||||||
|
@ -17,7 +16,6 @@ import org.json.JSONObject;
|
||||||
import android.accounts.Account;
|
import android.accounts.Account;
|
||||||
import android.accounts.AccountManager;
|
import android.accounts.AccountManager;
|
||||||
import android.accounts.OnAccountsUpdateListener;
|
import android.accounts.OnAccountsUpdateListener;
|
||||||
import android.app.Activity;
|
|
||||||
import android.content.ContentResolver;
|
import android.content.ContentResolver;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.database.ContentObserver;
|
import android.database.ContentObserver;
|
||||||
|
@ -272,6 +270,11 @@ public class Tabs implements GeckoEventListener {
|
||||||
return tab != null && tab == mSelectedTab;
|
return tab != null && tab == mSelectedTab;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isSelectedTabId(int tabId) {
|
||||||
|
final Tab selected = mSelectedTab;
|
||||||
|
return selected != null && selected.getId() == tabId;
|
||||||
|
}
|
||||||
|
|
||||||
public synchronized Tab getTab(int id) {
|
public synchronized Tab getTab(int id) {
|
||||||
if (mTabs.size() == 0)
|
if (mTabs.size() == 0)
|
||||||
return null;
|
return null;
|
||||||
|
@ -607,6 +610,15 @@ public class Tabs implements GeckoEventListener {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int getTabIdForUrl(String url) {
|
||||||
|
return getTabIdForUrl(url, Tabs.getInstance().getSelectedTab().isPrivate());
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized Tab getTabForUrl(String url) {
|
||||||
|
int tabId = getTabIdForUrl(url);
|
||||||
|
return getTab(tabId);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loads a tab with the given URL in the currently selected tab.
|
* Loads a tab with the given URL in the currently selected tab.
|
||||||
*
|
*
|
||||||
|
|
|
@ -88,10 +88,6 @@ public class BrowserDB {
|
||||||
|
|
||||||
public Bitmap getFaviconForUrl(ContentResolver cr, String uri);
|
public Bitmap getFaviconForUrl(ContentResolver cr, String uri);
|
||||||
|
|
||||||
public byte[] getFaviconBytesForUrl(ContentResolver cr, String uri);
|
|
||||||
|
|
||||||
public Cursor getFaviconsForUrls(ContentResolver cr, List<String> urls);
|
|
||||||
|
|
||||||
public String getFaviconUrlForHistoryUrl(ContentResolver cr, String url);
|
public String getFaviconUrlForHistoryUrl(ContentResolver cr, String url);
|
||||||
|
|
||||||
public void updateFaviconForUrl(ContentResolver cr, String pageUri, Bitmap favicon, String faviconUri);
|
public void updateFaviconForUrl(ContentResolver cr, String pageUri, Bitmap favicon, String faviconUri);
|
||||||
|
@ -242,16 +238,8 @@ public class BrowserDB {
|
||||||
sDb.removeReadingListItemWithURL(cr, uri);
|
sDb.removeReadingListItemWithURL(cr, uri);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Bitmap getFaviconForUrl(ContentResolver cr, String uri) {
|
public static Bitmap getFaviconForFaviconUrl(ContentResolver cr, String faviconURL) {
|
||||||
return sDb.getFaviconForUrl(cr, uri);
|
return sDb.getFaviconForUrl(cr, faviconURL);
|
||||||
}
|
|
||||||
|
|
||||||
public static byte[] getFaviconBytesForUrl(ContentResolver cr, String uri) {
|
|
||||||
return sDb.getFaviconBytesForUrl(cr, uri);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Cursor getFaviconsForUrls(ContentResolver cr, List<String> urls) {
|
|
||||||
return sDb.getFaviconsForUrls(cr, urls);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String getFaviconUrlForHistoryUrl(ContentResolver cr, String url) {
|
public static String getFaviconUrlForHistoryUrl(ContentResolver cr, String url) {
|
||||||
|
|
|
@ -3015,6 +3015,11 @@ public class BrowserProvider extends ContentProvider {
|
||||||
values.remove(Favicons.PAGE_URL);
|
values.remove(Favicons.PAGE_URL);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If no URL is provided, insert using the default one.
|
||||||
|
if (TextUtils.isEmpty(faviconUrl) && !TextUtils.isEmpty(pageUrl)) {
|
||||||
|
values.put(Favicons.URL, org.mozilla.gecko.favicons.Favicons.guessDefaultFaviconURL(pageUrl));
|
||||||
|
}
|
||||||
|
|
||||||
long now = System.currentTimeMillis();
|
long now = System.currentTimeMillis();
|
||||||
values.put(Favicons.DATE_CREATED, now);
|
values.put(Favicons.DATE_CREATED, now);
|
||||||
values.put(Favicons.DATE_MODIFIED, now);
|
values.put(Favicons.DATE_MODIFIED, now);
|
||||||
|
|
|
@ -696,33 +696,30 @@ public class LocalBrowserDB implements BrowserDB.BrowserDBIface {
|
||||||
new String[] { String.valueOf(id) });
|
new String[] { String.valueOf(id) });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the favicon from the database, if any, associated with the given favicon URL. (That is,
|
||||||
|
* the URL of the actual favicon image, not the URL of the page with which the favicon is associated.)
|
||||||
|
* @param cr The ContentResolver to use.
|
||||||
|
* @param faviconURL The URL of the favicon to fetch from the database.
|
||||||
|
* @return The decoded Bitmap from the database, if any. null if none is stored.
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
public Bitmap getFaviconForUrl(ContentResolver cr, String uri) {
|
public Bitmap getFaviconForUrl(ContentResolver cr, String faviconURL) {
|
||||||
final byte[] b = getFaviconBytesForUrl(cr, uri);
|
|
||||||
if (b == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return BitmapUtils.decodeByteArray(b);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public byte[] getFaviconBytesForUrl(ContentResolver cr, String uri) {
|
|
||||||
Cursor c = null;
|
Cursor c = null;
|
||||||
byte[] b = null;
|
byte[] b = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
c = cr.query(mCombinedUriWithProfile,
|
c = cr.query(mFaviconsUriWithProfile,
|
||||||
new String[] { Combined.FAVICON },
|
new String[] { Favicons.DATA },
|
||||||
Combined.URL + " = ?",
|
Favicons.URL + " = ?",
|
||||||
new String[] { uri },
|
new String[] { faviconURL },
|
||||||
null);
|
null);
|
||||||
|
|
||||||
if (!c.moveToFirst()) {
|
if (!c.moveToFirst()) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
final int faviconIndex = c.getColumnIndexOrThrow(Combined.FAVICON);
|
final int faviconIndex = c.getColumnIndexOrThrow(Favicons.DATA);
|
||||||
b = c.getBlob(faviconIndex);
|
b = c.getBlob(faviconIndex);
|
||||||
} finally {
|
} finally {
|
||||||
if (c != null) {
|
if (c != null) {
|
||||||
|
@ -730,7 +727,11 @@ public class LocalBrowserDB implements BrowserDB.BrowserDBIface {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return b;
|
if (b == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return BitmapUtils.decodeByteArray(b);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -754,28 +755,6 @@ public class LocalBrowserDB implements BrowserDB.BrowserDBIface {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public Cursor getFaviconsForUrls(ContentResolver cr, List<String> urls) {
|
|
||||||
StringBuilder selection = new StringBuilder();
|
|
||||||
selection.append(Favicons.URL + " IN (");
|
|
||||||
|
|
||||||
for (int i = 0; i < urls.size(); i++) {
|
|
||||||
final String url = urls.get(i);
|
|
||||||
|
|
||||||
if (i > 0)
|
|
||||||
selection.append(", ");
|
|
||||||
|
|
||||||
DatabaseUtils.appendEscapedSQLString(selection, url);
|
|
||||||
}
|
|
||||||
|
|
||||||
selection.append(")");
|
|
||||||
|
|
||||||
return cr.query(mCombinedUriWithProfile,
|
|
||||||
new String[] { Combined.URL, Combined.FAVICON },
|
|
||||||
selection.toString(),
|
|
||||||
null, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void updateFaviconForUrl(ContentResolver cr, String pageUri,
|
public void updateFaviconForUrl(ContentResolver cr, String pageUri,
|
||||||
Bitmap favicon, String faviconUri) {
|
Bitmap favicon, String faviconUri) {
|
||||||
|
|
|
@ -5,8 +5,13 @@
|
||||||
|
|
||||||
package org.mozilla.gecko.favicons;
|
package org.mozilla.gecko.favicons;
|
||||||
|
|
||||||
|
import android.graphics.BitmapFactory;
|
||||||
|
import android.text.TextUtils;
|
||||||
import org.mozilla.gecko.R;
|
import org.mozilla.gecko.R;
|
||||||
|
import org.mozilla.gecko.Tab;
|
||||||
|
import org.mozilla.gecko.Tabs;
|
||||||
import org.mozilla.gecko.db.BrowserDB;
|
import org.mozilla.gecko.db.BrowserDB;
|
||||||
|
import org.mozilla.gecko.favicons.cache.FaviconCache;
|
||||||
import org.mozilla.gecko.gfx.BitmapUtils;
|
import org.mozilla.gecko.gfx.BitmapUtils;
|
||||||
import org.mozilla.gecko.util.ThreadUtils;
|
import org.mozilla.gecko.util.ThreadUtils;
|
||||||
|
|
||||||
|
@ -15,6 +20,8 @@ import android.graphics.Bitmap;
|
||||||
import android.support.v4.util.LruCache;
|
import android.support.v4.util.LruCache;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.URISyntaxException;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
|
@ -24,73 +31,212 @@ import java.util.Set;
|
||||||
public class Favicons {
|
public class Favicons {
|
||||||
private static final String LOGTAG = "GeckoFavicons";
|
private static final String LOGTAG = "GeckoFavicons";
|
||||||
|
|
||||||
|
// Size of the favicon bitmap cache, in bytes (Counting payload only).
|
||||||
|
public static final int FAVICON_CACHE_SIZE_BYTES = 512 * 1024;
|
||||||
|
|
||||||
|
// Number of URL mappings from page URL to Favicon URL to cache in memory.
|
||||||
|
public static final int PAGE_URL_MAPPINGS_TO_STORE = 128;
|
||||||
|
|
||||||
public static final int NOT_LOADING = 0;
|
public static final int NOT_LOADING = 0;
|
||||||
public static final int FAILED_EXPIRY_NEVER = -1;
|
|
||||||
public static final int FLAG_PERSIST = 1;
|
public static final int FLAG_PERSIST = 1;
|
||||||
public static final int FLAG_SCALE = 2;
|
public static final int FLAG_SCALE = 2;
|
||||||
|
|
||||||
private static int sFaviconSmallSize = -1;
|
|
||||||
private static int sFaviconLargeSize = -1;
|
|
||||||
|
|
||||||
protected static Context sContext;
|
protected static Context sContext;
|
||||||
|
|
||||||
|
// The default Favicon to show if no other can be found.
|
||||||
|
public static Bitmap sDefaultFavicon;
|
||||||
|
|
||||||
|
// The density-adjusted default Favicon dimensions.
|
||||||
|
public static int sDefaultFaviconSize;
|
||||||
|
|
||||||
private static final Map<Integer, LoadFaviconTask> sLoadTasks = Collections.synchronizedMap(new HashMap<Integer, LoadFaviconTask>());
|
private static final Map<Integer, LoadFaviconTask> sLoadTasks = Collections.synchronizedMap(new HashMap<Integer, LoadFaviconTask>());
|
||||||
private static final LruCache<String, Bitmap> sFaviconCache = new LruCache<String, Bitmap>(1024 * 1024) {
|
|
||||||
@Override
|
|
||||||
protected int sizeOf(String url, Bitmap image) {
|
|
||||||
return image.getRowBytes() * image.getHeight();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// A cache of the Favicon which have recently failed to download - prevents us from repeatedly
|
// Cache to hold mappings between page URLs and Favicon URLs. Used to avoid going to the DB when
|
||||||
// trying to download a Favicon when doing so is currently impossible.
|
// doing so is not necessary.
|
||||||
private static final LruCache<String, Long> sFailedCache = new LruCache<String, Long>(64);
|
private static final LruCache<String, String> sPageURLMappings = new LruCache<String, String>(PAGE_URL_MAPPINGS_TO_STORE);
|
||||||
|
|
||||||
// A cache holding the dominant colours of favicons - used by FaviconView to fill the extra space
|
public static String getFaviconURLForPageURLFromCache(String pageURL) {
|
||||||
// around a Favicon when it is asked to render a Favicon small than the view.
|
return sPageURLMappings.get(pageURL);
|
||||||
private static final LruCache<String, Integer> sColorCache = new LruCache<String, Integer>(256);
|
}
|
||||||
static void dispatchResult(final String pageUrl, final Bitmap image,
|
|
||||||
|
/**
|
||||||
|
* Insert the given pageUrl->faviconUrl mapping into the memory cache of such mappings.
|
||||||
|
* Useful for short-circuiting local database access.
|
||||||
|
*/
|
||||||
|
public static void putFaviconURLForPageURLInCache(String pageURL, String faviconURL) {
|
||||||
|
sPageURLMappings.put(pageURL, faviconURL);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static FaviconCache sFaviconsCache;
|
||||||
|
static void dispatchResult(final String pageUrl, final String faviconURL, final Bitmap image,
|
||||||
final OnFaviconLoadedListener listener) {
|
final OnFaviconLoadedListener listener) {
|
||||||
if (pageUrl != null && image != null)
|
|
||||||
putFaviconInMemCache(pageUrl, image);
|
|
||||||
|
|
||||||
// We want to always run the listener on UI thread
|
// We want to always run the listener on UI thread
|
||||||
ThreadUtils.postToUiThread(new Runnable() {
|
ThreadUtils.postToUiThread(new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
if (listener != null)
|
if (listener != null) {
|
||||||
listener.onFaviconLoaded(pageUrl, image);
|
listener.onFaviconLoaded(pageUrl, faviconURL, image);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String getFaviconUrlForPageUrl(String pageUrl) {
|
/**
|
||||||
return BrowserDB.getFaviconUrlForHistoryUrl(sContext.getContentResolver(), pageUrl);
|
* Get a Favicon as close as possible to the target dimensions for the URL provided.
|
||||||
|
* If a result is instantly available from the cache, it is returned and the listener is invoked.
|
||||||
|
* Otherwise, the result is drawn from the database or network and the listener invoked when the
|
||||||
|
* result becomes available.
|
||||||
|
*
|
||||||
|
* @param pageURL Page URL for which a Favicon is desired.
|
||||||
|
* @param faviconURL URL of the Favicon to be downloaded, if known. If none provided, an educated
|
||||||
|
* guess is made by the system.
|
||||||
|
* @param targetSize Target size of the returned Favicon
|
||||||
|
* @param listener Listener to call with the result of the load operation, if the result is not
|
||||||
|
* immediately available.
|
||||||
|
* @return The id of the asynchronous task created, NOT_LOADING if none is created.
|
||||||
|
*/
|
||||||
|
public static int getFaviconForSize(String pageURL, String faviconURL, int targetSize, int flags, OnFaviconLoadedListener listener) {
|
||||||
|
// If there's no favicon URL given, try and hit the cache with the default one.
|
||||||
|
String cacheURL = faviconURL;
|
||||||
|
if (cacheURL == null) {
|
||||||
|
cacheURL = guessDefaultFaviconURL(pageURL);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it's something we can't even figure out a default URL for, just give up.
|
||||||
|
if (cacheURL == null) {
|
||||||
|
dispatchResult(pageURL, null, sDefaultFavicon, listener);
|
||||||
|
return NOT_LOADING;
|
||||||
|
}
|
||||||
|
|
||||||
|
Bitmap cachedIcon = getSizedFaviconFromCache(cacheURL, targetSize);
|
||||||
|
if (cachedIcon != null) {
|
||||||
|
dispatchResult(pageURL, cacheURL, cachedIcon, listener);
|
||||||
|
return NOT_LOADING;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if favicon has failed.
|
||||||
|
if (sFaviconsCache.isFailedFavicon(cacheURL)) {
|
||||||
|
dispatchResult(pageURL, cacheURL, sDefaultFavicon, listener);
|
||||||
|
return NOT_LOADING;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Failing that, try and get one from the database or internet.
|
||||||
|
return loadUncachedFavicon(pageURL, faviconURL, flags, targetSize, listener);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static int loadFavicon(String pageUrl, String faviconUrl, int flags,
|
/**
|
||||||
OnFaviconLoadedListener listener) {
|
* Returns the cached Favicon closest to the target size if any exists or is coercible. Returns
|
||||||
|
* null otherwise. Does not query the database or network for the Favicon is the result is not
|
||||||
|
* immediately available.
|
||||||
|
*
|
||||||
|
* @param faviconURL URL of the Favicon to query for.
|
||||||
|
* @param targetSize The desired size of the returned Favicon.
|
||||||
|
* @return The cached Favicon, rescaled to be as close as possible to the target size, if any exists.
|
||||||
|
* null if no applicable Favicon exists in the cache.
|
||||||
|
*/
|
||||||
|
public static Bitmap getSizedFaviconFromCache(String faviconURL, int targetSize) {
|
||||||
|
return sFaviconsCache.getFaviconForDimensions(faviconURL, targetSize);
|
||||||
|
}
|
||||||
|
|
||||||
// Handle the case where page url is empty
|
/**
|
||||||
if (pageUrl == null || pageUrl.length() == 0) {
|
* Attempts to find a Favicon for the provided page URL from either the mem cache or the database.
|
||||||
dispatchResult(null, null, listener);
|
* Does not need an explicit favicon URL, since, as we are accessing the database anyway, we
|
||||||
return -1;
|
* can query the history DB for the Favicon URL.
|
||||||
|
* Handy for easing the transition from caching with page URLs to caching with Favicon URLs.
|
||||||
|
*
|
||||||
|
* A null result is passed to the listener if no value is locally available. The Favicon is not
|
||||||
|
* added to the failure cache.
|
||||||
|
*
|
||||||
|
* @param pageURL Page URL for which a Favicon is wanted.
|
||||||
|
* @param targetSize Target size of the desired Favicon to pass to the cache query
|
||||||
|
* @param callback Callback to fire with the result.
|
||||||
|
* @return The job ID of the spawned async task, if any.
|
||||||
|
*/
|
||||||
|
public static int getSizedFaviconForPageFromLocal(final String pageURL, final int targetSize, final OnFaviconLoadedListener callback) {
|
||||||
|
// Firstly, try extremely hard to cheat.
|
||||||
|
// Have we cached this favicon URL? If we did, we can consult the memcache right away.
|
||||||
|
String targetURL = sPageURLMappings.get(pageURL);
|
||||||
|
if (targetURL != null) {
|
||||||
|
// Check if favicon has failed.
|
||||||
|
if (sFaviconsCache.isFailedFavicon(targetURL)) {
|
||||||
|
dispatchResult(pageURL, targetURL, null, callback);
|
||||||
|
return NOT_LOADING;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do we have a Favicon in the cache for this favicon URL?
|
||||||
|
Bitmap result = getSizedFaviconFromCache(targetURL, targetSize);
|
||||||
|
if (result != null) {
|
||||||
|
// Victory - immediate response!
|
||||||
|
dispatchResult(pageURL, targetURL, result, callback);
|
||||||
|
return NOT_LOADING;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if favicon has failed
|
// No joy using in-memory resources. Go to background thread and ask the database.
|
||||||
if (isFailedFavicon(pageUrl)) {
|
LoadFaviconTask task = new LoadFaviconTask(ThreadUtils.getBackgroundHandler(), pageURL, targetURL, 0, callback, targetSize, true);
|
||||||
dispatchResult(pageUrl, null, listener);
|
int taskId = task.getId();
|
||||||
return -1;
|
sLoadTasks.put(taskId, task);
|
||||||
|
task.execute();
|
||||||
|
return taskId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int getSizedFaviconForPageFromLocal(final String pageURL, final OnFaviconLoadedListener callback) {
|
||||||
|
return getSizedFaviconForPageFromLocal(pageURL, sDefaultFaviconSize, callback);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Helper method to determine the URL of the Favicon image for a given page URL by querying the
|
||||||
|
* history database. Should only be called from the background thread - does database access.
|
||||||
|
*
|
||||||
|
* @param pageURL The URL of a webpage with a Favicon.
|
||||||
|
* @return The URL of the Favicon used by that webpage, according to either the History database
|
||||||
|
* or a somewhat educated guess.
|
||||||
|
*/
|
||||||
|
public static String getFaviconUrlForPageUrl(String pageURL) {
|
||||||
|
// Attempt to determine the Favicon URL from the Tabs datastructure. Can dodge having to use
|
||||||
|
// the database sometimes by doing this.
|
||||||
|
String targetURL;
|
||||||
|
Tab theTab = Tabs.getInstance().getTabForUrl(pageURL);
|
||||||
|
if (theTab != null) {
|
||||||
|
targetURL = theTab.getFaviconURL();
|
||||||
|
if (targetURL != null) {
|
||||||
|
return targetURL;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if favicon is mem cached
|
targetURL = BrowserDB.getFaviconUrlForHistoryUrl(sContext.getContentResolver(), pageURL);
|
||||||
Bitmap image = getFaviconFromMemCache(pageUrl);
|
if (targetURL == null) {
|
||||||
if (image != null) {
|
// Nothing in the history database. Fall back to the default URL and hope for the best.
|
||||||
dispatchResult(pageUrl, image, listener);
|
targetURL = guessDefaultFaviconURL(pageURL);
|
||||||
return -1;
|
}
|
||||||
|
return targetURL;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function to create an async job to load a Favicon which does not exist in the memcache.
|
||||||
|
* Contains logic to prevent the repeated loading of Favicons which have previously failed.
|
||||||
|
* There is no support for recovery from transient failures.
|
||||||
|
*
|
||||||
|
* @param pageUrl URL of the page for which to load a Favicon. If null, no job is created.
|
||||||
|
* @param faviconUrl The URL of the Favicon to load. If null, an attempt to infer the value from
|
||||||
|
* the history database will be made, and ultimately an attempt to guess will
|
||||||
|
* be made.
|
||||||
|
* @param flags Flags to be used by the LoadFaviconTask while loading. Currently only one flag
|
||||||
|
* is supported, LoadFaviconTask.FLAG_PERSIST.
|
||||||
|
* If FLAG_PERSIST is set and the Favicon is ultimately loaded from the internet,
|
||||||
|
* the downloaded Favicon is subsequently stored in the local database.
|
||||||
|
* If FLAG_PERSIST is unset, the downloaded Favicon is stored only in the memcache.
|
||||||
|
* FLAG_PERSIST has no effect on loads which come from the database.
|
||||||
|
* @param listener The OnFaviconLoadedListener to invoke with the result of this Favicon load.
|
||||||
|
* @return The id of the LoadFaviconTask handling this job.
|
||||||
|
*/
|
||||||
|
private static int loadUncachedFavicon(String pageUrl, String faviconUrl, int flags, int targetSize, OnFaviconLoadedListener listener) {
|
||||||
|
// Handle the case where we have no page url.
|
||||||
|
if (TextUtils.isEmpty(pageUrl)) {
|
||||||
|
dispatchResult(null, null, null, listener);
|
||||||
|
return NOT_LOADING;
|
||||||
}
|
}
|
||||||
|
|
||||||
LoadFaviconTask task = new LoadFaviconTask(ThreadUtils.getBackgroundHandler(), pageUrl, faviconUrl, flags, listener);
|
LoadFaviconTask task = new LoadFaviconTask(ThreadUtils.getBackgroundHandler(), pageUrl, faviconUrl, flags, listener, targetSize, false);
|
||||||
|
|
||||||
int taskId = task.getId();
|
int taskId = task.getId();
|
||||||
sLoadTasks.put(taskId, task);
|
sLoadTasks.put(taskId, task);
|
||||||
|
@ -100,44 +246,29 @@ public class Favicons {
|
||||||
return taskId;
|
return taskId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Bitmap getFaviconFromMemCache(String pageUrl) {
|
public static void putFaviconInMemCache(String pageUrl, Bitmap image) {
|
||||||
// If for some reason the key is null, simply return null
|
sFaviconsCache.putSingleFavicon(pageUrl, image);
|
||||||
// and avoid an exception on the mem cache (see bug 813546)
|
|
||||||
if (pageUrl == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return sFaviconCache.get(pageUrl);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void putFaviconInMemCache(String pageUrl, Bitmap image) {
|
public static void putFaviconsInMemCache(String pageUrl, Iterator<Bitmap> images) {
|
||||||
sFaviconCache.put(pageUrl, image);
|
sFaviconsCache.putFavicons(pageUrl, images);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void clearMemCache() {
|
public static void clearMemCache() {
|
||||||
sFaviconCache.evictAll();
|
sFaviconsCache.evictAll();
|
||||||
|
sPageURLMappings.evictAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean isFailedFavicon(String pageUrl) {
|
public static void putFaviconInFailedCache(String faviconURL) {
|
||||||
Long fetchTime = sFailedCache.get(pageUrl);
|
sFaviconsCache.putFailed(faviconURL);
|
||||||
if (fetchTime == null)
|
|
||||||
return false;
|
|
||||||
// We don't have any other rules yet.
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void putFaviconInFailedCache(String pageUrl, long fetchTime) {
|
|
||||||
sFailedCache.put(pageUrl, fetchTime);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void clearFailedCache() {
|
|
||||||
sFailedCache.evictAll();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean cancelFaviconLoad(int taskId) {
|
public static boolean cancelFaviconLoad(int taskId) {
|
||||||
Log.d(LOGTAG, "Requesting cancelation of favicon load (" + taskId + ")");
|
if (taskId == NOT_LOADING) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
boolean cancelled = false;
|
boolean cancelled;
|
||||||
synchronized (sLoadTasks) {
|
synchronized (sLoadTasks) {
|
||||||
if (!sLoadTasks.containsKey(taskId))
|
if (!sLoadTasks.containsKey(taskId))
|
||||||
return false;
|
return false;
|
||||||
|
@ -161,47 +292,92 @@ public class Favicons {
|
||||||
int taskId = iter.next();
|
int taskId = iter.next();
|
||||||
cancelFaviconLoad(taskId);
|
cancelFaviconLoad(taskId);
|
||||||
}
|
}
|
||||||
|
sLoadTasks.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
LoadFaviconTask.closeHTTPClient();
|
LoadFaviconTask.closeHTTPClient();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean isLargeFavicon(Bitmap image) {
|
/**
|
||||||
return image.getWidth() > sFaviconSmallSize || image.getHeight() > sFaviconSmallSize;
|
* Get the dominant colour of the Favicon at the URL given, if any exists in the cache.
|
||||||
|
*
|
||||||
|
* @param url The URL of the Favicon, to be used as the cache key for the colour value.
|
||||||
|
* @return The dominant colour of the provided Favicon.
|
||||||
|
*/
|
||||||
|
public static int getFaviconColor(String url) {
|
||||||
|
return sFaviconsCache.getDominantColor(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Bitmap scaleImage(Bitmap image) {
|
/**
|
||||||
// If the icon is larger than 16px, scale it to sFaviconLargeSize.
|
* Called by GeckoApp on startup to pass this class a reference to the GeckoApp object used as
|
||||||
// Otherwise, scale it to sFaviconSmallSize.
|
* the application's Context.
|
||||||
if (isLargeFavicon(image)) {
|
* Consider replacing with references to a staticly held reference to the GeckoApp object.
|
||||||
image = Bitmap.createScaledBitmap(image, sFaviconLargeSize, sFaviconLargeSize, false);
|
*
|
||||||
} else {
|
* @param context A reference to the GeckoApp instance.
|
||||||
image = Bitmap.createScaledBitmap(image, sFaviconSmallSize, sFaviconSmallSize, false);
|
*/
|
||||||
}
|
public static void attachToContext(Context context) throws Exception {
|
||||||
return image;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static int getFaviconColor(Bitmap image, String key) {
|
|
||||||
Integer color = sColorCache.get(key);
|
|
||||||
if (color != null) {
|
|
||||||
return color;
|
|
||||||
}
|
|
||||||
|
|
||||||
color = BitmapUtils.getDominantColor(image);
|
|
||||||
sColorCache.put(key, color);
|
|
||||||
return color;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void attachToContext(Context context) {
|
|
||||||
sContext = context;
|
sContext = context;
|
||||||
if (sFaviconSmallSize < 0) {
|
|
||||||
sFaviconSmallSize = Math.round(sContext.getResources().getDimension(R.dimen.favicon_size_small));
|
// Decode the default Favicon ready for use.
|
||||||
|
sDefaultFavicon = BitmapFactory.decodeResource(context.getResources(), R.drawable.favicon);
|
||||||
|
if (sDefaultFavicon == null) {
|
||||||
|
throw new Exception("Null default favicon was returned from the resources system!");
|
||||||
}
|
}
|
||||||
if (sFaviconLargeSize < 0) {
|
|
||||||
sFaviconLargeSize = Math.round(sContext.getResources().getDimension(R.dimen.favicon_size_large));
|
sDefaultFaviconSize = context.getResources().getDimensionPixelSize(R.dimen.favicon_bg);
|
||||||
|
sFaviconsCache = new FaviconCache(FAVICON_CACHE_SIZE_BYTES, context.getResources().getDimensionPixelSize(R.dimen.favicon_largest_interesting_size));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper method to get the default Favicon URL for a given pageURL. Generally: somewhere.com/favicon.ico
|
||||||
|
*
|
||||||
|
* @param pageURL Page URL for which a default Favicon URL is requested
|
||||||
|
* @return The default Favicon URL.
|
||||||
|
*/
|
||||||
|
public static String guessDefaultFaviconURL(String pageURL) {
|
||||||
|
// Special-casing for about: pages. The favicon for about:pages which don't provide a link tag
|
||||||
|
// is bundled in the database, keyed only by page URL, hence the need to return the page URL
|
||||||
|
// here. If the database ever migrates to stop being silly in this way, this can plausibly
|
||||||
|
// be removed.
|
||||||
|
if (pageURL.startsWith("about:") || pageURL.startsWith("jar:")) {
|
||||||
|
return pageURL;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Fall back to trying "someScheme:someDomain.someExtension/favicon.ico".
|
||||||
|
URI u = new URI(pageURL);
|
||||||
|
return new URI(u.getScheme(),
|
||||||
|
u.getAuthority(),
|
||||||
|
"/favicon.ico", null,
|
||||||
|
null).toString();
|
||||||
|
} catch (URISyntaxException e) {
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void removeLoadTask(long taskId) {
|
public static void removeLoadTask(long taskId) {
|
||||||
sLoadTasks.remove(taskId);
|
sLoadTasks.remove(taskId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method to wrap FaviconCache.isFailedFavicon for use by LoadFaviconTask.
|
||||||
|
*
|
||||||
|
* @param faviconURL Favicon URL to check for failure.
|
||||||
|
*/
|
||||||
|
static boolean isFailedFavicon(String faviconURL) {
|
||||||
|
return sFaviconsCache.isFailedFavicon(faviconURL);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sidestep the cache and get, from either the database or the internet, the largest available
|
||||||
|
* Favicon for the given page URL. Useful for creating homescreen shortcuts without being limited
|
||||||
|
* by possibly low-resolution values in the cache.
|
||||||
|
* Deduces the favicon URL from the history database and, ultimately, guesses.
|
||||||
|
*
|
||||||
|
* @param url Page URL to get a large favicon image fro.
|
||||||
|
* @param onFaviconLoadedListener Listener to call back with the result.
|
||||||
|
*/
|
||||||
|
public static void getLargestFaviconForPage(String url, OnFaviconLoadedListener onFaviconLoadedListener) {
|
||||||
|
loadUncachedFavicon(url, null, 0, -1, onFaviconLoadedListener);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,9 @@ import android.content.ContentResolver;
|
||||||
import android.graphics.Bitmap;
|
import android.graphics.Bitmap;
|
||||||
import android.net.http.AndroidHttpClient;
|
import android.net.http.AndroidHttpClient;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
|
import android.text.TextUtils;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
import org.apache.http.Header;
|
||||||
import org.apache.http.HttpEntity;
|
import org.apache.http.HttpEntity;
|
||||||
import org.apache.http.HttpResponse;
|
import org.apache.http.HttpResponse;
|
||||||
import org.apache.http.client.methods.HttpGet;
|
import org.apache.http.client.methods.HttpGet;
|
||||||
|
@ -22,12 +24,15 @@ import org.mozilla.gecko.util.ThreadUtils;
|
||||||
import org.mozilla.gecko.util.UiAsyncTask;
|
import org.mozilla.gecko.util.UiAsyncTask;
|
||||||
import static org.mozilla.gecko.favicons.Favicons.sContext;
|
import static org.mozilla.gecko.favicons.Favicons.sContext;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.net.MalformedURLException;
|
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.net.URISyntaxException;
|
import java.net.URISyntaxException;
|
||||||
import java.net.URL;
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.LinkedList;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class representing the asynchronous task to load a Favicon which is not currently in the in-memory
|
* Class representing the asynchronous task to load a Favicon which is not currently in the in-memory
|
||||||
|
@ -38,8 +43,13 @@ import java.util.concurrent.atomic.AtomicInteger;
|
||||||
public class LoadFaviconTask extends UiAsyncTask<Void, Void, Bitmap> {
|
public class LoadFaviconTask extends UiAsyncTask<Void, Void, Bitmap> {
|
||||||
private static final String LOGTAG = "LoadFaviconTask";
|
private static final String LOGTAG = "LoadFaviconTask";
|
||||||
|
|
||||||
|
// Access to this map needs to be synchronized prevent multiple jobs loading the same favicon
|
||||||
|
// from executing concurrently.
|
||||||
|
private static final HashMap<String, LoadFaviconTask> loadsInFlight = new HashMap<String, LoadFaviconTask>();
|
||||||
|
|
||||||
public static final int FLAG_PERSIST = 1;
|
public static final int FLAG_PERSIST = 1;
|
||||||
public static final int FLAG_SCALE = 2;
|
public static final int FLAG_SCALE = 2;
|
||||||
|
private static final int MAX_REDIRECTS_TO_FOLLOW = 5;
|
||||||
|
|
||||||
private static AtomicInteger mNextFaviconLoadId = new AtomicInteger(0);
|
private static AtomicInteger mNextFaviconLoadId = new AtomicInteger(0);
|
||||||
private int mId;
|
private int mId;
|
||||||
|
@ -48,25 +58,39 @@ public class LoadFaviconTask extends UiAsyncTask<Void, Void, Bitmap> {
|
||||||
private OnFaviconLoadedListener mListener;
|
private OnFaviconLoadedListener mListener;
|
||||||
private int mFlags;
|
private int mFlags;
|
||||||
|
|
||||||
|
private final boolean mOnlyFromLocal;
|
||||||
|
|
||||||
|
// Assuming square favicons, judging by width only is acceptable.
|
||||||
|
protected int mTargetWidth;
|
||||||
|
private LinkedList<LoadFaviconTask> mChainees;
|
||||||
|
private boolean mIsChaining;
|
||||||
|
|
||||||
static AndroidHttpClient sHttpClient = AndroidHttpClient.newInstance(GeckoAppShell.getGeckoInterface().getDefaultUAString());
|
static AndroidHttpClient sHttpClient = AndroidHttpClient.newInstance(GeckoAppShell.getGeckoInterface().getDefaultUAString());
|
||||||
|
|
||||||
public LoadFaviconTask(Handler backgroundThreadHandler,
|
public LoadFaviconTask(Handler backgroundThreadHandler,
|
||||||
String aPageUrl, String aFaviconUrl, int aFlags,
|
String pageUrl, String faviconUrl, int flags,
|
||||||
OnFaviconLoadedListener aListener) {
|
OnFaviconLoadedListener listener) {
|
||||||
|
this(backgroundThreadHandler, pageUrl, faviconUrl, flags, listener, -1, false);
|
||||||
|
}
|
||||||
|
public LoadFaviconTask(Handler backgroundThreadHandler,
|
||||||
|
String pageUrl, String faviconUrl, int flags,
|
||||||
|
OnFaviconLoadedListener aListener, int targetSize, boolean fromLocal) {
|
||||||
super(backgroundThreadHandler);
|
super(backgroundThreadHandler);
|
||||||
|
|
||||||
mId = mNextFaviconLoadId.incrementAndGet();
|
mId = mNextFaviconLoadId.incrementAndGet();
|
||||||
|
|
||||||
mPageUrl = aPageUrl;
|
mPageUrl = pageUrl;
|
||||||
mFaviconUrl = aFaviconUrl;
|
mFaviconUrl = faviconUrl;
|
||||||
mListener = aListener;
|
mListener = aListener;
|
||||||
mFlags = aFlags;
|
mFlags = flags;
|
||||||
|
mTargetWidth = targetSize;
|
||||||
|
mOnlyFromLocal = fromLocal;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Runs in background thread
|
// Runs in background thread
|
||||||
private Bitmap loadFaviconFromDb() {
|
private Bitmap loadFaviconFromDb() {
|
||||||
ContentResolver resolver = sContext.getContentResolver();
|
ContentResolver resolver = sContext.getContentResolver();
|
||||||
return BrowserDB.getFaviconForUrl(resolver, mPageUrl);
|
return BrowserDB.getFaviconForFaviconUrl(resolver, mFaviconUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Runs in background thread
|
// Runs in background thread
|
||||||
|
@ -79,50 +103,87 @@ public class LoadFaviconTask extends UiAsyncTask<Void, Void, Bitmap> {
|
||||||
BrowserDB.updateFaviconForUrl(resolver, mPageUrl, favicon, mFaviconUrl);
|
BrowserDB.updateFaviconForUrl(resolver, mPageUrl, favicon, mFaviconUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper method for trying the download request to grab a Favicon.
|
||||||
|
* @param faviconURI URL of Favicon to try and download
|
||||||
|
* @return The HttpResponse containing the downloaded Favicon if successful, null otherwise.
|
||||||
|
*/
|
||||||
|
private HttpResponse tryDownload(URI faviconURI) throws URISyntaxException, IOException {
|
||||||
|
HashSet<String> visitedLinkSet = new HashSet<String>();
|
||||||
|
visitedLinkSet.add(faviconURI.toString());
|
||||||
|
return tryDownloadRecurse(faviconURI, visitedLinkSet);
|
||||||
|
}
|
||||||
|
private HttpResponse tryDownloadRecurse(URI faviconURI, HashSet<String> visited) throws URISyntaxException, IOException {
|
||||||
|
if (visited.size() == MAX_REDIRECTS_TO_FOLLOW) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
HttpGet request = new HttpGet(faviconURI);
|
||||||
|
HttpResponse response = sHttpClient.execute(request);
|
||||||
|
if (response == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.getStatusLine() != null) {
|
||||||
|
|
||||||
|
// Was the response a failure?
|
||||||
|
int status = response.getStatusLine().getStatusCode();
|
||||||
|
|
||||||
|
// Handle HTTP status codes requesting a redirect.
|
||||||
|
if (status >= 300 && status < 400) {
|
||||||
|
Header header = response.getFirstHeader("Location");
|
||||||
|
|
||||||
|
// Handle mad webservers.
|
||||||
|
if (header == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String newURI = header.getValue();
|
||||||
|
if (newURI == null || newURI.equals(faviconURI.toString())) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (visited.contains(newURI)) {
|
||||||
|
// Already been redirected here - abort.
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
visited.add(newURI);
|
||||||
|
return tryDownloadRecurse(new URI(newURI), visited);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status >= 400) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
// Runs in background thread
|
// Runs in background thread
|
||||||
private Bitmap downloadFavicon(URL targetFaviconURL) {
|
private Bitmap downloadFavicon(URI targetFaviconURI) {
|
||||||
if (mFaviconUrl.startsWith("jar:jar:")) {
|
if (mFaviconUrl.startsWith("jar:jar:")) {
|
||||||
return GeckoJarReader.getBitmap(sContext.getResources(), mFaviconUrl);
|
return GeckoJarReader.getBitmap(sContext.getResources(), mFaviconUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
URI uri;
|
// only get favicons for HTTP/HTTPS
|
||||||
try {
|
String scheme = targetFaviconURI.getScheme();
|
||||||
uri = targetFaviconURL.toURI();
|
if (!"http".equals(scheme) && !"https".equals(scheme)) {
|
||||||
} catch (URISyntaxException e) {
|
|
||||||
Log.d(LOGTAG, "Could not get URI for favicon");
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// only get favicons for HTTP/HTTPS
|
|
||||||
String scheme = uri.getScheme();
|
|
||||||
if (!"http".equals(scheme) && !"https".equals(scheme))
|
|
||||||
return null;
|
|
||||||
|
|
||||||
// skia decoder sometimes returns null; workaround is to use BufferedHttpEntity
|
// skia decoder sometimes returns null; workaround is to use BufferedHttpEntity
|
||||||
// http://groups.google.com/group/android-developers/browse_thread/thread/171b8bf35dbbed96/c3ec5f45436ceec8?lnk=raot
|
// http://groups.google.com/group/android-developers/browse_thread/thread/171b8bf35dbbed96/c3ec5f45436ceec8?lnk=raot
|
||||||
Bitmap image = null;
|
Bitmap image = null;
|
||||||
try {
|
try {
|
||||||
HttpGet request = new HttpGet(targetFaviconURL.toURI());
|
// Try the URL we were given.
|
||||||
HttpResponse response = sHttpClient.execute(request);
|
HttpResponse response = tryDownload(targetFaviconURI);
|
||||||
if (response == null)
|
if (response == null) {
|
||||||
return null;
|
return null;
|
||||||
if (response.getStatusLine() != null) {
|
|
||||||
// Was the response a failure?
|
|
||||||
int status = response.getStatusLine().getStatusCode();
|
|
||||||
if (status >= 400) {
|
|
||||||
Favicons.putFaviconInFailedCache(mPageUrl, Favicons.FAILED_EXPIRY_NEVER);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
HttpEntity entity = response.getEntity();
|
HttpEntity entity = response.getEntity();
|
||||||
if (entity == null)
|
if (entity == null) {
|
||||||
return null;
|
return null;
|
||||||
if (entity.getContentType() != null) {
|
|
||||||
// Is the content type valid? Might be a captive portal.
|
|
||||||
String contentType = entity.getContentType().getValue();
|
|
||||||
if (contentType.indexOf("image") == -1)
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
BufferedHttpEntity bufferedEntity = new BufferedHttpEntity(entity);
|
BufferedHttpEntity bufferedEntity = new BufferedHttpEntity(entity);
|
||||||
|
@ -145,69 +206,194 @@ public class LoadFaviconTask extends UiAsyncTask<Void, Void, Bitmap> {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Bitmap doInBackground(Void... unused) {
|
protected Bitmap doInBackground(Void... unused) {
|
||||||
Bitmap image;
|
if (isCancelled()) {
|
||||||
|
|
||||||
if (isCancelled())
|
|
||||||
return null;
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
URL faviconURLToDownload;
|
String storedFaviconUrl;
|
||||||
|
boolean isUsingDefaultURL = false;
|
||||||
|
|
||||||
// Handle the case of malformed favicon URL
|
// Handle the case of malformed favicon URL.
|
||||||
try {
|
// If favicon is empty, fall back to the stored one.
|
||||||
// If favicon is empty, fallback to default favicon URI
|
if (TextUtils.isEmpty(mFaviconUrl)) {
|
||||||
if (mFaviconUrl == null || mFaviconUrl.length() == 0) {
|
// Try to get the favicon URL from the memory cache.
|
||||||
// Handle the case of malformed URL
|
storedFaviconUrl = Favicons.getFaviconURLForPageURLFromCache(mPageUrl);
|
||||||
URL targetPageURL = new URL(mPageUrl);
|
|
||||||
|
|
||||||
faviconURLToDownload = new URL(targetPageURL.getProtocol(), targetPageURL.getAuthority(), "/favicon.ico");
|
// If that failed, try to get the URL from the database.
|
||||||
mFaviconUrl = faviconURLToDownload.toString();
|
if (storedFaviconUrl == null) {
|
||||||
} else {
|
storedFaviconUrl = Favicons.getFaviconUrlForPageUrl(mPageUrl);
|
||||||
faviconURLToDownload = new URL(mFaviconUrl);
|
if (storedFaviconUrl != null) {
|
||||||
|
// If that succeeded, cache the URL loaded from the database in memory.
|
||||||
|
Favicons.putFaviconURLForPageURLInCache(mPageUrl, storedFaviconUrl);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (MalformedURLException e) {
|
|
||||||
Log.d(LOGTAG, "The provided favicon URL is not valid");
|
// If we found a faviconURL - use it.
|
||||||
|
if (storedFaviconUrl != null) {
|
||||||
|
mFaviconUrl = storedFaviconUrl;
|
||||||
|
} else {
|
||||||
|
// If we don't have a stored one, fall back to the default.
|
||||||
|
mFaviconUrl = Favicons.guessDefaultFaviconURL(mPageUrl);
|
||||||
|
isUsingDefaultURL = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if favicon has failed - if so, give up. We need this check because, sometimes, we
|
||||||
|
// didn't know the real Favicon URL until we asked the database.
|
||||||
|
if (Favicons.isFailedFavicon(mFaviconUrl)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isCancelled())
|
if (isCancelled()) {
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
String storedFaviconUrl = Favicons.getFaviconUrlForPageUrl(mPageUrl);
|
|
||||||
if (storedFaviconUrl != null && storedFaviconUrl.equals(mFaviconUrl)) {
|
|
||||||
image = loadFaviconFromDb();
|
|
||||||
if (image != null && image.getWidth() > 0 && image.getHeight() > 0)
|
|
||||||
return ((mFlags & FLAG_SCALE) != 0) ? Favicons.scaleImage(image) : image;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isCancelled())
|
Bitmap image;
|
||||||
return null;
|
// Determine if there is already an ongoing task to fetch the Favicon we desire.
|
||||||
|
// If there is, just join the queue and wait for it to finish. If not, we carry on.
|
||||||
|
synchronized(loadsInFlight) {
|
||||||
|
// Another load of the current Favicon is already underway
|
||||||
|
LoadFaviconTask existingTask = loadsInFlight.get(mFaviconUrl);
|
||||||
|
if (existingTask != null && !existingTask.isCancelled()) {
|
||||||
|
existingTask.chainTasks(this);
|
||||||
|
mIsChaining = true;
|
||||||
|
|
||||||
image = downloadFavicon(faviconURLToDownload);
|
// If we are chaining, we want to keep the first task started to do this job as the one
|
||||||
|
// in the hashmap so subsequent tasks will add themselves to its chaining list.
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We do not want to update the hashmap if the task has chained - other tasks need to
|
||||||
|
// chain onto the same parent task.
|
||||||
|
loadsInFlight.put(mFaviconUrl, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isCancelled()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
image = loadFaviconFromDb();
|
||||||
|
if (image != null && image.getWidth() > 0 && image.getHeight() > 0) {
|
||||||
|
return image;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mOnlyFromLocal || isCancelled()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
image = downloadFavicon(new URI(mFaviconUrl));
|
||||||
|
} catch (URISyntaxException e) {
|
||||||
|
Log.e(LOGTAG, "The provided favicon URL is not valid");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we're not already trying the default URL, try it now.
|
||||||
|
if (image == null && !isUsingDefaultURL) {
|
||||||
|
try {
|
||||||
|
image = downloadFavicon(new URI(Favicons.guessDefaultFaviconURL(mPageUrl)));
|
||||||
|
} catch (URISyntaxException e){
|
||||||
|
// Not interesting. It was an educated guess, anyway.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (image != null && image.getWidth() > 0 && image.getHeight() > 0) {
|
if (image != null && image.getWidth() > 0 && image.getHeight() > 0) {
|
||||||
saveFaviconToDb(image);
|
saveFaviconToDb(image);
|
||||||
image = ((mFlags & FLAG_SCALE) != 0) ? Favicons.scaleImage(image) : image;
|
|
||||||
} else {
|
} else {
|
||||||
image = null;
|
Favicons.putFaviconInFailedCache(mFaviconUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
return image;
|
return image;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onPostExecute(final Bitmap image) {
|
protected void onPostExecute(Bitmap image) {
|
||||||
|
if (mIsChaining) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Put what we got in the memcache.
|
||||||
|
Favicons.putFaviconInMemCache(mFaviconUrl, image);
|
||||||
|
|
||||||
|
// Process the result, scale for the listener, etc.
|
||||||
|
processResult(image);
|
||||||
|
|
||||||
|
synchronized (loadsInFlight) {
|
||||||
|
// Prevent any other tasks from chaining on this one.
|
||||||
|
loadsInFlight.remove(mFaviconUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Since any update to mChainees is done while holding the loadsInFlight lock, once we reach
|
||||||
|
// this point no further updates to that list can possibly take place (As far as other tasks
|
||||||
|
// are concerned, there is no longer a task to chain from. The above block will have waited
|
||||||
|
// for any tasks that were adding themselves to the list before reaching this point.)
|
||||||
|
|
||||||
|
// As such, I believe we're safe to do the following without holding the lock.
|
||||||
|
// This is nice - we do not want to take the lock unless we have to anyway, and chaining rarely
|
||||||
|
// actually happens outside of the strange situations unit tests create.
|
||||||
|
|
||||||
|
// Share the result with all chained tasks.
|
||||||
|
if (mChainees != null) {
|
||||||
|
for (LoadFaviconTask t : mChainees) {
|
||||||
|
t.processResult(image);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void processResult(Bitmap image) {
|
||||||
Favicons.removeLoadTask(mId);
|
Favicons.removeLoadTask(mId);
|
||||||
Favicons.dispatchResult(mPageUrl, image, mListener);
|
|
||||||
|
Bitmap scaled = image;
|
||||||
|
|
||||||
|
// Notify listeners, scaling if required.
|
||||||
|
if (mTargetWidth != -1 && image != null && image.getWidth() != mTargetWidth) {
|
||||||
|
scaled = Favicons.getSizedFaviconFromCache(mFaviconUrl, mTargetWidth);
|
||||||
|
}
|
||||||
|
|
||||||
|
Favicons.dispatchResult(mPageUrl, mFaviconUrl, scaled, mListener);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCancelled() {
|
protected void onCancelled() {
|
||||||
Favicons.removeLoadTask(mId);
|
Favicons.removeLoadTask(mId);
|
||||||
|
|
||||||
|
synchronized(loadsInFlight) {
|
||||||
|
// Only remove from the hashmap if the task there is the one that's being canceled.
|
||||||
|
// Cancellation of a task that would have chained is not interesting to the hashmap.
|
||||||
|
final LoadFaviconTask primary = loadsInFlight.get(mFaviconUrl);
|
||||||
|
if (primary == this) {
|
||||||
|
loadsInFlight.remove(mFaviconUrl);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (primary == null) {
|
||||||
|
// This shouldn't happen.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (primary.mChainees != null) {
|
||||||
|
primary.mChainees.remove(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Note that we don't call the listener callback if the
|
// Note that we don't call the listener callback if the
|
||||||
// favicon load is cancelled.
|
// favicon load is cancelled.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When the result of this job is ready, also notify the chainee of the result.
|
||||||
|
* Used for aggregating concurrent requests for the same Favicon into a single actual request.
|
||||||
|
* (Don't want to download a hundred instances of Google's Favicon at once, for example).
|
||||||
|
* The loadsInFlight lock must be held when calling this function.
|
||||||
|
*
|
||||||
|
* @param aChainee LoadFaviconTask
|
||||||
|
*/
|
||||||
|
private void chainTasks(LoadFaviconTask aChainee) {
|
||||||
|
if (mChainees == null) {
|
||||||
|
mChainees = new LinkedList<LoadFaviconTask>();
|
||||||
|
}
|
||||||
|
|
||||||
|
mChainees.add(aChainee);
|
||||||
|
}
|
||||||
|
|
||||||
int getId() {
|
int getId() {
|
||||||
return mId;
|
return mId;
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,5 +10,5 @@ import android.graphics.Bitmap;
|
||||||
* Interface to be implemented by objects wishing to listen for favicon load completion events.
|
* Interface to be implemented by objects wishing to listen for favicon load completion events.
|
||||||
*/
|
*/
|
||||||
public interface OnFaviconLoadedListener {
|
public interface OnFaviconLoadedListener {
|
||||||
void onFaviconLoaded(String url, Bitmap favicon);
|
void onFaviconLoaded(String url, String faviconURL, Bitmap favicon);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,636 @@
|
||||||
|
/* 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.favicons.cache;
|
||||||
|
|
||||||
|
import android.graphics.Bitmap;
|
||||||
|
import android.util.Log;
|
||||||
|
import org.mozilla.gecko.favicons.Favicons;
|
||||||
|
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.Semaphore;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implements a Least-Recently-Used cache for Favicons, keyed by Favicon URL.
|
||||||
|
*
|
||||||
|
* When a favicon at a particular URL is decoded, it will yield one or more bitmaps.
|
||||||
|
* While in memory, these bitmaps are stored in a list, sorted in ascending order of size, in a
|
||||||
|
* FaviconsForURL object.
|
||||||
|
* The collection of FaviconsForURL objects currently in the cache is stored in mBackingMap, keyed
|
||||||
|
* by favicon URL.
|
||||||
|
*
|
||||||
|
* FaviconsForURL provides a method for obtaining the smallest icon larger than a given size - the
|
||||||
|
* most appropriate icon for a particular size.
|
||||||
|
* It also distinguishes between "primary" favicons (Ones that have merely been extracted from a
|
||||||
|
* file downloaded from the website) and "secondary" favicons (Ones that have been computed locally
|
||||||
|
* as resized versions of primary favicons.).
|
||||||
|
*
|
||||||
|
* FaviconsForURL is also responsible for storing URL-specific, as opposed to favicon-specific,
|
||||||
|
* information. For the purposes of this cache, the simplifying assumption that the dominant colour
|
||||||
|
* for all favicons served from a particular favicon URL shall be the same is made. (To violate this
|
||||||
|
* would mandate serving an ICO or similar file with multiple radically different images in it - an
|
||||||
|
* ill-advised and extremely uncommon use-case, for sure.)
|
||||||
|
* The dominant colour information is updated as the element is being added to the cache - typically
|
||||||
|
* on the background thread.
|
||||||
|
* Also present here are the download timestamp and isFailed flag. Upon failure, the flag is set.
|
||||||
|
* A constant exists in this file to specify the maximum time permitted between failures before
|
||||||
|
* a retry is again permitted.
|
||||||
|
*
|
||||||
|
* TODO: Expiry of Favicons from the favicon database cache is not implemented. (Bug 914296)
|
||||||
|
*
|
||||||
|
* A typical request to the cache will consist of a Favicon URL and a target size. The FaviconsForURL
|
||||||
|
* object for that URL will be obtained, queried for a favicon matching exactly the needed size, and
|
||||||
|
* if successful, the result is returned.
|
||||||
|
* If unsuccessful, the object is asked to return the smallest available primary favicon larger than
|
||||||
|
* the target size. If this step works, the result is downscaled to create a new secondary favicon,
|
||||||
|
* which is then stored (So subsequent requests will succeed at the first step) and returned.
|
||||||
|
* If that step fails, the object finally walks backwards through its sequence of favicons until it
|
||||||
|
* finds the largest primary favicon smaller than the target. This is then upscaled by a maximum of
|
||||||
|
* 2x towards the target size, and the result cached and returned as above.
|
||||||
|
*
|
||||||
|
* The bitmaps themselves are encapsulated inside FaviconCacheElement objects. These objects contain,
|
||||||
|
* as well as the bitmap, a pointer to the encapsulating FaviconsForURL object (Used by the LRU
|
||||||
|
* culler), the size of the encapsulated image, a flag indicating if this is a primary favicon, and
|
||||||
|
* a flag indicating if the entry is invalid.
|
||||||
|
* All FaviconCacheElement objects are tracked in the mOrdering LinkedList. This is used to record
|
||||||
|
* LRU information about FaviconCacheElements. In particular, the most recently used FaviconCacheElement
|
||||||
|
* will be at the start of the list, the least recently used at the end of the list.
|
||||||
|
*
|
||||||
|
* When the cache runs out of space, it removes FaviconCacheElements starting from the end of the list
|
||||||
|
* until a sufficient amount of space has been freed.
|
||||||
|
* When a secondary favicon is removed in this way, it is simply deleted from its parent FaviconsForURLs
|
||||||
|
* object's list of available favicons.
|
||||||
|
* The backpointer field on the FaviconCacheElement is used to remove the element from the encapsulating
|
||||||
|
* FaviconsForURL object, when this is required.
|
||||||
|
* When a primary favicon is removed, its invalid flag is set to true and its bitmap payload is set
|
||||||
|
* to null (So it is available for freeing by the garbage collector). This reduces the memory footprint
|
||||||
|
* of the icon to essentially zero, but keeps track of which primary favicons exist for this favicon
|
||||||
|
* URL.
|
||||||
|
* If a subsequent request comes in for that favicon URL, it is then known that a primary of those
|
||||||
|
* dimensions is available, just that it is not in the cache. The system is then able to load the
|
||||||
|
* primary back into the cache from the database (Where the entirety of the initially encapsulating
|
||||||
|
* container-formatted image file is stored).
|
||||||
|
* If this were not done, then when processing requests after the culling of primary favicons it would
|
||||||
|
* be impossible to distinguish between the nonexistence of a primary and the nonexistence of a primary
|
||||||
|
* in the cache without querying the database.
|
||||||
|
*
|
||||||
|
* The implementation is safe to use from multiple threads and, while is it not entirely strongly
|
||||||
|
* consistent all of the time, you almost certainly don't care.
|
||||||
|
* The thread-safety implementation used is approximately MRSW with semaphores. An extra semaphore
|
||||||
|
* is used to grant mutual exclusion over reordering operations from reader threads (Who thus gain
|
||||||
|
* a quasi-writer status to do such limited mutation as is necessary).
|
||||||
|
*
|
||||||
|
* Reads which race with writes are liable to not see the ongoing write. The cache may return a
|
||||||
|
* stale or now-removed value to the caller. Returned values are never invalid, even in the face
|
||||||
|
* of concurrent reading and culling.
|
||||||
|
*/
|
||||||
|
public class FaviconCache {
|
||||||
|
private static final String LOGTAG = "FaviconCache";
|
||||||
|
|
||||||
|
// The number of spaces to allocate for favicons in each node.
|
||||||
|
private static final int NUM_FAVICON_SIZES = 4;
|
||||||
|
|
||||||
|
// Dimensions of the largest favicon to store in the cache. Everything is downscaled to this.
|
||||||
|
public final int mMaxCachedWidth;
|
||||||
|
|
||||||
|
// Retry failed favicons after 20 minutes.
|
||||||
|
public static final long FAILURE_RETRY_MILLISECONDS = 1000 * 60 * 20;
|
||||||
|
|
||||||
|
// Map relating Favicon URLs with objects representing decoded favicons.
|
||||||
|
// Since favicons may be container formats holding multiple icons, the underlying type holds a
|
||||||
|
// sorted list of bitmap payloads in ascending order of size. The underlying type may be queried
|
||||||
|
// for the least larger payload currently present.
|
||||||
|
private final ConcurrentHashMap<String, FaviconsForURL> mBackingMap = new ConcurrentHashMap<String, FaviconsForURL>();
|
||||||
|
|
||||||
|
// A linked list used to implement a queue, defining the LRU properties of the cache. Elements
|
||||||
|
// contained within the various FaviconsForURL objects are held here, the least recently used
|
||||||
|
// of which at the end of the list. When space needs to be reclaimed, the appropriate bitmap is
|
||||||
|
// culled.
|
||||||
|
private final LinkedList<FaviconCacheElement> mOrdering = new LinkedList<FaviconCacheElement>();
|
||||||
|
|
||||||
|
// The above structures, if used correctly, enable this cache to exhibit LRU semantics across all
|
||||||
|
// favicon payloads in the system, as well as enabling the dynamic selection from the cache of
|
||||||
|
// the primary bitmap most suited to the requested size (in cases where multiple primary bitmaps
|
||||||
|
// are provided by the underlying file format).
|
||||||
|
|
||||||
|
// Current size, in bytes, of the bitmap data present in the cache.
|
||||||
|
private final AtomicInteger mCurrentSize = new AtomicInteger(0);
|
||||||
|
|
||||||
|
// The maximum quantity, in bytes, of bitmap data which may be stored in the cache.
|
||||||
|
private final int mMaxSizeBytes;
|
||||||
|
|
||||||
|
// Tracks the number of ongoing read operations. Enables the first one in to lock writers out and
|
||||||
|
// the last one out to let them in.
|
||||||
|
private final AtomicInteger mOngoingReads = new AtomicInteger(0);
|
||||||
|
|
||||||
|
// Used to ensure transaction fairness - each txn acquires and releases this as the first operation.
|
||||||
|
// The effect is an orderly, inexpensive ordering enforced on txns to prevent writer starvation.
|
||||||
|
private final Semaphore mTurnSemaphore = new Semaphore(1);
|
||||||
|
|
||||||
|
// A deviation from the usual MRSW solution - this semaphore is used to guard modification to the
|
||||||
|
// ordering map. This allows for read transactions to update the most-recently-used value without
|
||||||
|
// needing to take out the write lock.
|
||||||
|
private final Semaphore mReorderingSemaphore = new Semaphore(1);
|
||||||
|
|
||||||
|
// The semaphore one must acquire in order to perform a write.
|
||||||
|
private final Semaphore mWriteLock = new Semaphore(1);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called by txns performing only reads as they start. Prevents writer starvation with a turn
|
||||||
|
* semaphore and locks writers out if this is the first concurrent reader txn starting up.
|
||||||
|
*/
|
||||||
|
private void startRead() {
|
||||||
|
mTurnSemaphore.acquireUninterruptibly();
|
||||||
|
mTurnSemaphore.release();
|
||||||
|
|
||||||
|
if (mOngoingReads.incrementAndGet() == 1) {
|
||||||
|
// First one in. Wait for writers to finish and lock them out.
|
||||||
|
mWriteLock.acquireUninterruptibly();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An alternative to startWrite to be used when in a read transaction and wanting to upgrade it
|
||||||
|
* to a write transaction. Such a transaction should be terminated with finishWrite.
|
||||||
|
*/
|
||||||
|
private void upgradeReadToWrite() {
|
||||||
|
mTurnSemaphore.acquireUninterruptibly();
|
||||||
|
if (mOngoingReads.decrementAndGet() == 0) {
|
||||||
|
mWriteLock.release();
|
||||||
|
}
|
||||||
|
mWriteLock.acquireUninterruptibly();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called by transactions performing only reads as they finish. Ensures that if this is the last
|
||||||
|
* concluding read transaction then then writers are subsequently allowed in.
|
||||||
|
*/
|
||||||
|
private void finishRead() {
|
||||||
|
if (mOngoingReads.decrementAndGet() == 0) {
|
||||||
|
mWriteLock.release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called by writer transactions upon start. Ensures fairness and then obtains the write lock.
|
||||||
|
* Upon return, no other txns will be executing concurrently.
|
||||||
|
*/
|
||||||
|
private void startWrite() {
|
||||||
|
mTurnSemaphore.acquireUninterruptibly();
|
||||||
|
mWriteLock.acquireUninterruptibly();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called by a concluding write transaction - unlocks the structure.
|
||||||
|
*/
|
||||||
|
private void finishWrite() {
|
||||||
|
mTurnSemaphore.release();
|
||||||
|
mWriteLock.release();
|
||||||
|
}
|
||||||
|
|
||||||
|
public FaviconCache(int maxSize, int maxWidthToCache) {
|
||||||
|
mMaxSizeBytes = maxSize;
|
||||||
|
mMaxCachedWidth = maxWidthToCache;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if the provided favicon URL is marked as a failure (Has failed to load before -
|
||||||
|
* such icons get blacklisted for a time to prevent us endlessly retrying.)
|
||||||
|
*
|
||||||
|
* @param faviconURL Favicon URL to check if failed in memcache.
|
||||||
|
* @return true if this favicon is blacklisted, false otherwise.
|
||||||
|
*/
|
||||||
|
public boolean isFailedFavicon(String faviconURL) {
|
||||||
|
startRead();
|
||||||
|
|
||||||
|
boolean isExpired = false;
|
||||||
|
boolean isAborting = false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// If we don't have it in the cache, it certainly isn't a known failure.
|
||||||
|
if (!mBackingMap.containsKey(faviconURL)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
FaviconsForURL container = mBackingMap.get(faviconURL);
|
||||||
|
|
||||||
|
// If the has failed flag is not set, it's certainly not a known failure.
|
||||||
|
if (!container.mHasFailed) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final long failureTimestamp = container.mDownloadTimestamp;
|
||||||
|
|
||||||
|
// Calculate elapsed time since the failing download.
|
||||||
|
final long failureDiff = System.currentTimeMillis() - failureTimestamp;
|
||||||
|
|
||||||
|
// If long enough has passed, mark it as no longer a failure.
|
||||||
|
if (failureDiff > FAILURE_RETRY_MILLISECONDS) {
|
||||||
|
isExpired = true;
|
||||||
|
} else {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} catch (Exception unhandled) {
|
||||||
|
// Handle any exception thrown and return the locks to a sensible state.
|
||||||
|
finishRead();
|
||||||
|
|
||||||
|
// Flag to prevent finally from doubly-unlocking.
|
||||||
|
isAborting = true;
|
||||||
|
Log.e(LOGTAG, "FaviconCache exception!", unhandled);
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
if (!isAborting) {
|
||||||
|
if (isExpired) {
|
||||||
|
// No longer expired.
|
||||||
|
upgradeReadToWrite();
|
||||||
|
} else {
|
||||||
|
finishRead();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
recordRemoved(mBackingMap.get(faviconURL));
|
||||||
|
mBackingMap.remove(faviconURL);
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
finishWrite();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark the indicated page URL as a failed Favicon until the provided time.
|
||||||
|
*
|
||||||
|
* @param faviconURL Page URL for which a Favicon load has failed.
|
||||||
|
*/
|
||||||
|
public void putFailed(String faviconURL) {
|
||||||
|
startWrite();
|
||||||
|
|
||||||
|
if (mBackingMap.containsKey(faviconURL)) {
|
||||||
|
recordRemoved(mBackingMap.get(faviconURL));
|
||||||
|
}
|
||||||
|
|
||||||
|
FaviconsForURL container = new FaviconsForURL(0, true);
|
||||||
|
mBackingMap.put(faviconURL, container);
|
||||||
|
|
||||||
|
finishWrite();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch a Favicon for the given URL as close as possible to the size provided.
|
||||||
|
* If an icon of the given size is already in the cache, it is returned.
|
||||||
|
* If an icon of the given size is not in the cache but a larger unscaled image does exist in
|
||||||
|
* the cache, we downscale the larger image to the target size and cache the result.
|
||||||
|
* If there is no image of the required size, null is returned.
|
||||||
|
*
|
||||||
|
* @param faviconURL The URL for which a Favicon is desired. Must not be null.
|
||||||
|
* @param targetSize The size of the desired favicon.
|
||||||
|
* @return A favicon of the requested size for the requested URL, or null if none cached.
|
||||||
|
*/
|
||||||
|
public Bitmap getFaviconForDimensions(String faviconURL, int targetSize) {
|
||||||
|
if (faviconURL == null) {
|
||||||
|
Log.e(LOGTAG, "You passed a null faviconURL to getFaviconForDimensions. Don't.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean doingWrites = false;
|
||||||
|
boolean shouldComputeColour = false;
|
||||||
|
boolean isAborting = false;
|
||||||
|
final Bitmap newBitmap;
|
||||||
|
final FaviconsForURL container;
|
||||||
|
|
||||||
|
startRead();
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!mBackingMap.containsKey(faviconURL)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
container = mBackingMap.get(faviconURL);
|
||||||
|
|
||||||
|
FaviconCacheElement cacheElement;
|
||||||
|
|
||||||
|
int cacheElementIndex = container.getNextHighestIndex(targetSize);
|
||||||
|
|
||||||
|
// cacheElementIndex now holds either the index of the next least largest bitmap from
|
||||||
|
// targetSize, or -1 if targetSize > all bitmaps.
|
||||||
|
if (cacheElementIndex != -1) {
|
||||||
|
// If cacheElementIndex is not the sentinel value, then it is a valid index into mFavicons.
|
||||||
|
cacheElement = container.mFavicons.get(cacheElementIndex);
|
||||||
|
|
||||||
|
if (cacheElement.mInvalidated) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we found exactly what we wanted - we're done.
|
||||||
|
if (cacheElement.mImageSize == targetSize) {
|
||||||
|
setMostRecentlyUsed(cacheElement);
|
||||||
|
return cacheElement.mFaviconPayload;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// We requested an image larger than all primaries. Set the element to start the search
|
||||||
|
// from to the element beyond the end of the array, so the search runs backwards.
|
||||||
|
cacheElementIndex = container.mFavicons.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
// We did not find exactly what we wanted, but now have set cacheElementIndex to the index
|
||||||
|
// where what we want should live in the list. We now request the next least larger primary
|
||||||
|
// from the cache. We will downscale this to our target size.
|
||||||
|
|
||||||
|
// If there is no such primary, we'll upscale the next least smaller one instead.
|
||||||
|
cacheElement = container.getNextPrimary(cacheElementIndex);
|
||||||
|
|
||||||
|
|
||||||
|
if (cacheElement == null) {
|
||||||
|
// The primary has been invalidated! Fail! Need to get it back from the database.
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Having got this far, we'll be needing to write the new secondary to the cache, which
|
||||||
|
// involves us falling through to the next try block. This flag lets us do this (Other
|
||||||
|
// paths prior to this end in returns.)
|
||||||
|
doingWrites = true;
|
||||||
|
|
||||||
|
// Scaling logic...
|
||||||
|
Bitmap largestElementBitmap = cacheElement.mFaviconPayload;
|
||||||
|
int largestSize = cacheElement.mImageSize;
|
||||||
|
|
||||||
|
if (largestSize >= targetSize) {
|
||||||
|
// The largest we have is larger than the target - downsize to target.
|
||||||
|
newBitmap = Bitmap.createScaledBitmap(largestElementBitmap, targetSize, targetSize, true);
|
||||||
|
} else {
|
||||||
|
// Our largest primary is smaller than the desired size. Upscale by a maximum of 2x.
|
||||||
|
// largestSize now reflects the maximum size we can upscale to.
|
||||||
|
largestSize *= 2;
|
||||||
|
|
||||||
|
if (largestSize >= targetSize) {
|
||||||
|
// Perfect! We can upscale by less than 2x and reach the needed size. Do it.
|
||||||
|
newBitmap = Bitmap.createScaledBitmap(largestElementBitmap, targetSize, targetSize, true);
|
||||||
|
} else {
|
||||||
|
// We don't have enough information to make the target size look nonterrible. Best effort:
|
||||||
|
newBitmap = Bitmap.createScaledBitmap(largestElementBitmap, largestSize, largestSize, true);
|
||||||
|
|
||||||
|
shouldComputeColour = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception unhandled) {
|
||||||
|
isAborting = true;
|
||||||
|
|
||||||
|
// Handle any exception thrown and return the locks to a sensible state.
|
||||||
|
finishRead();
|
||||||
|
|
||||||
|
// Flag to prevent finally from doubly-unlocking.
|
||||||
|
Log.e(LOGTAG, "FaviconCache exception!", unhandled);
|
||||||
|
return null;
|
||||||
|
} finally {
|
||||||
|
if (!isAborting) {
|
||||||
|
if (doingWrites) {
|
||||||
|
upgradeReadToWrite();
|
||||||
|
} else {
|
||||||
|
finishRead();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (shouldComputeColour) {
|
||||||
|
// And since we failed, we'll need the dominant colour.
|
||||||
|
container.ensureDominantColor();
|
||||||
|
}
|
||||||
|
|
||||||
|
// While the image might not actually BE that size, we set the size field to the target
|
||||||
|
// because this is the best image you can get for a request of that size using the Favicon
|
||||||
|
// information provided by this website.
|
||||||
|
// This way, subsequent requests hit straight away.
|
||||||
|
FaviconCacheElement newElement = container.addSecondary(newBitmap, targetSize);
|
||||||
|
|
||||||
|
setMostRecentlyUsed(newElement);
|
||||||
|
|
||||||
|
mCurrentSize.addAndGet(newElement.sizeOf());
|
||||||
|
} finally {
|
||||||
|
finishWrite();
|
||||||
|
}
|
||||||
|
|
||||||
|
return newBitmap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Query the cache for the dominant colour stored for the Favicon URL provided, if any.
|
||||||
|
*
|
||||||
|
* @param key The URL of the Favicon for which a dominant colour is desired.
|
||||||
|
* @return The cached dominant colour, or null if none is cached.
|
||||||
|
*/
|
||||||
|
public int getDominantColor(String key) {
|
||||||
|
startRead();
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!mBackingMap.containsKey(key)) {
|
||||||
|
Log.w(LOGTAG, "Cannot compute dominant color of non-cached favicon " + key);
|
||||||
|
finishRead();
|
||||||
|
return 0xFFFFFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
FaviconsForURL element = mBackingMap.get(key);
|
||||||
|
|
||||||
|
return element.ensureDominantColor();
|
||||||
|
} finally {
|
||||||
|
finishRead();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove all payloads stored in the given container from the LRU cache. Must be called while
|
||||||
|
* holding the write lock.
|
||||||
|
*
|
||||||
|
* @param wasRemoved The container to purge from the cache.
|
||||||
|
*/
|
||||||
|
private void recordRemoved(FaviconsForURL wasRemoved) {
|
||||||
|
// If there was an existing value, strip it from the insertion-order cache.
|
||||||
|
if (wasRemoved == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int sizeRemoved = 0;
|
||||||
|
|
||||||
|
for (FaviconCacheElement e : wasRemoved.mFavicons) {
|
||||||
|
sizeRemoved += e.sizeOf();
|
||||||
|
mOrdering.remove(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
mCurrentSize.addAndGet(-sizeRemoved);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Bitmap produceCacheableBitmap(Bitmap favicon) {
|
||||||
|
// Never cache the default Favicon, or the null Favicon.
|
||||||
|
if (favicon == Favicons.sDefaultFavicon || favicon == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Some sites serve up insanely huge Favicons (Seen 512x512 ones...)
|
||||||
|
// While we want to cache nice big icons, we apply a limit based on screen density for the
|
||||||
|
// sake of space.
|
||||||
|
if (favicon.getWidth() > mMaxCachedWidth) {
|
||||||
|
return Bitmap.createScaledBitmap(favicon, mMaxCachedWidth, mMaxCachedWidth, true);
|
||||||
|
}
|
||||||
|
return favicon;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set an existing element as the most recently used element. May be called from either type of
|
||||||
|
* transaction.
|
||||||
|
*
|
||||||
|
* @param element The element that is to become the most recently used one.
|
||||||
|
*/
|
||||||
|
private void setMostRecentlyUsed(FaviconCacheElement element) {
|
||||||
|
mReorderingSemaphore.acquireUninterruptibly();
|
||||||
|
mOrdering.remove(element);
|
||||||
|
mOrdering.offer(element);
|
||||||
|
mReorderingSemaphore.release();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the provided bitmap to the cache as the only available primary for this URL.
|
||||||
|
* Should never be called with scaled Favicons. The input is assumed to be an unscaled Favicon.
|
||||||
|
*
|
||||||
|
* @param faviconURL The URL of the Favicon being stored.
|
||||||
|
* @param aFavicon The Favicon to store.
|
||||||
|
*/
|
||||||
|
public void putSingleFavicon(String faviconURL, Bitmap aFavicon) {
|
||||||
|
Bitmap favicon = produceCacheableBitmap(aFavicon);
|
||||||
|
if (favicon == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a fresh container for the favicons associated with this URL. Allocate extra slots
|
||||||
|
// in the underlying ArrayList in case multiple secondary favicons are later created.
|
||||||
|
// Currently set to the number of favicon sizes used in the UI, plus 1, at time of writing.
|
||||||
|
// Ought to be tuned as things change for maximal performance.
|
||||||
|
FaviconsForURL toInsert = new FaviconsForURL(NUM_FAVICON_SIZES);
|
||||||
|
|
||||||
|
// Create the cache element for the single element we are inserting, and configure it.
|
||||||
|
FaviconCacheElement newElement = toInsert.addPrimary(favicon);
|
||||||
|
|
||||||
|
startWrite();
|
||||||
|
try {
|
||||||
|
// Set the new element as the most recently used one.
|
||||||
|
setMostRecentlyUsed(newElement);
|
||||||
|
|
||||||
|
mCurrentSize.addAndGet(newElement.sizeOf());
|
||||||
|
|
||||||
|
// Update the value in the LruCache...
|
||||||
|
FaviconsForURL wasRemoved;
|
||||||
|
wasRemoved = mBackingMap.put(faviconURL, toInsert);
|
||||||
|
|
||||||
|
recordRemoved(wasRemoved);
|
||||||
|
} finally {
|
||||||
|
finishWrite();
|
||||||
|
}
|
||||||
|
|
||||||
|
cullIfRequired();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the collection of primary favicons for the given URL to the provided collection of bitmaps.
|
||||||
|
*
|
||||||
|
* @param faviconURL The URL from which the favicons originate.
|
||||||
|
* @param favicons A List of favicons decoded from this URL.
|
||||||
|
*/
|
||||||
|
public void putFavicons(String faviconURL, Iterator<Bitmap> favicons) {
|
||||||
|
// We don't know how many icons we'll have - let's just take a guess.
|
||||||
|
FaviconsForURL toInsert = new FaviconsForURL(5 * NUM_FAVICON_SIZES);
|
||||||
|
int sizeGained = 0;
|
||||||
|
|
||||||
|
while (favicons.hasNext()) {
|
||||||
|
Bitmap favicon = produceCacheableBitmap(favicons.next());
|
||||||
|
if (favicon == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
FaviconCacheElement newElement = toInsert.addPrimary(favicon);
|
||||||
|
sizeGained += newElement.sizeOf();
|
||||||
|
}
|
||||||
|
|
||||||
|
startRead();
|
||||||
|
|
||||||
|
boolean abortingRead = false;
|
||||||
|
|
||||||
|
// Not using setMostRecentlyUsed, because the elements are known to be new. This can be done
|
||||||
|
// without taking the write lock, via the magic of the reordering semaphore.
|
||||||
|
mReorderingSemaphore.acquireUninterruptibly();
|
||||||
|
try {
|
||||||
|
for (FaviconCacheElement newElement : toInsert.mFavicons) {
|
||||||
|
mOrdering.offer(newElement);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
abortingRead = true;
|
||||||
|
mReorderingSemaphore.release();
|
||||||
|
finishRead();
|
||||||
|
|
||||||
|
Log.e(LOGTAG, "Favicon cache exception!", e);
|
||||||
|
return;
|
||||||
|
} finally {
|
||||||
|
if (!abortingRead) {
|
||||||
|
mReorderingSemaphore.release();
|
||||||
|
upgradeReadToWrite();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
mCurrentSize.addAndGet(sizeGained);
|
||||||
|
|
||||||
|
// Update the value in the LruCache...
|
||||||
|
recordRemoved(mBackingMap.put(faviconURL, toInsert));
|
||||||
|
} finally {
|
||||||
|
finishWrite();
|
||||||
|
}
|
||||||
|
|
||||||
|
cullIfRequired();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If cache too large, drop stuff from the cache to get the size back into the acceptable range.
|
||||||
|
* Otherwise, do nothing.
|
||||||
|
*/
|
||||||
|
private void cullIfRequired() {
|
||||||
|
Log.d(LOGTAG, "Favicon cache fullness: " + mCurrentSize.get() + '/' + mMaxSizeBytes);
|
||||||
|
|
||||||
|
if (mCurrentSize.get() <= mMaxSizeBytes) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
startWrite();
|
||||||
|
try {
|
||||||
|
while (mCurrentSize.get() > mMaxSizeBytes) {
|
||||||
|
// Cull the least recently used element.
|
||||||
|
|
||||||
|
FaviconCacheElement victim;
|
||||||
|
victim = mOrdering.poll();
|
||||||
|
|
||||||
|
mCurrentSize.addAndGet(-victim.sizeOf());
|
||||||
|
victim.onEvictedFromCache();
|
||||||
|
|
||||||
|
Log.d(LOGTAG, "After cull: " + mCurrentSize.get() + '/' + mMaxSizeBytes);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
finishWrite();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Purge all elements from the FaviconCache. Handy if you want to reclaim some memory.
|
||||||
|
*/
|
||||||
|
public void evictAll() {
|
||||||
|
startWrite();
|
||||||
|
|
||||||
|
try {
|
||||||
|
mBackingMap.clear();
|
||||||
|
mOrdering.clear();
|
||||||
|
} finally {
|
||||||
|
finishWrite();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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.favicons.cache;
|
||||||
|
|
||||||
|
import android.graphics.Bitmap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Objects stored in the Favicon cache - allow for the bitmap to be tagged to indicate if it has
|
||||||
|
* been scaled. Unscaled bitmaps are not included in the scaled-bitmap cache's size calculation.
|
||||||
|
*/
|
||||||
|
public class FaviconCacheElement implements Comparable<FaviconCacheElement> {
|
||||||
|
// Was this Favicon computed via scaling another primary Favicon, or is this a primary Favicon?
|
||||||
|
final boolean mIsPrimary;
|
||||||
|
|
||||||
|
// The Favicon bitmap.
|
||||||
|
Bitmap mFaviconPayload;
|
||||||
|
|
||||||
|
// If set, mFaviconPayload is absent. Since the underlying ICO may contain multiple primary
|
||||||
|
// payloads, primary payloads are never truly deleted from the cache, but instead have their
|
||||||
|
// payload deleted and this flag set on their FaviconCacheElement. That way, the cache always
|
||||||
|
// has a record of the existence of a primary payload, even if it is no longer in the cache.
|
||||||
|
// This means that when a request comes in that will be best served using a primary that is in
|
||||||
|
// the database but no longer cached, we know that it exists and can go get it (Useful when ICO
|
||||||
|
// support is added).
|
||||||
|
volatile boolean mInvalidated;
|
||||||
|
|
||||||
|
final int mImageSize;
|
||||||
|
|
||||||
|
// Used for LRU pruning.
|
||||||
|
final FaviconsForURL mBackpointer;
|
||||||
|
|
||||||
|
public FaviconCacheElement(Bitmap payload, boolean isPrimary, int imageSize, FaviconsForURL backpointer) {
|
||||||
|
mFaviconPayload = payload;
|
||||||
|
mIsPrimary = isPrimary;
|
||||||
|
mImageSize = imageSize;
|
||||||
|
mBackpointer = backpointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public FaviconCacheElement(Bitmap payload, boolean isPrimary, FaviconsForURL backpointer) {
|
||||||
|
mFaviconPayload = payload;
|
||||||
|
mIsPrimary = isPrimary;
|
||||||
|
mBackpointer = backpointer;
|
||||||
|
|
||||||
|
if (payload != null) {
|
||||||
|
mImageSize = payload.getWidth();
|
||||||
|
} else {
|
||||||
|
mImageSize = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int sizeOf() {
|
||||||
|
if (mInvalidated) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return mFaviconPayload.getRowBytes() * mFaviconPayload.getHeight();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Establish an ordering on FaviconCacheElements based on size and validity. An element is
|
||||||
|
* considered "greater than" another if it is valid and the other is not, or if it contains a
|
||||||
|
* larger payload.
|
||||||
|
*
|
||||||
|
* @param another The FaviconCacheElement to compare to this one.
|
||||||
|
* @return -1 if this element is less than the given one, 1 if the other one is larger than this
|
||||||
|
* and 0 if both are of equal value.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public int compareTo(FaviconCacheElement another) {
|
||||||
|
if (mInvalidated && !another.mInvalidated) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!mInvalidated && another.mInvalidated) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mInvalidated) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
final int w1 = mImageSize;
|
||||||
|
final int w2 = another.mImageSize;
|
||||||
|
if (w1 > w2) {
|
||||||
|
return 1;
|
||||||
|
} else if (w2 > w1) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when this element is evicted from the cache.
|
||||||
|
*
|
||||||
|
* If primary, drop the payload and set invalid. If secondary, just unlink from parent node.
|
||||||
|
*/
|
||||||
|
public void onEvictedFromCache() {
|
||||||
|
if (mIsPrimary) {
|
||||||
|
// So we keep a record of which primaries exist in the database for this URL, we
|
||||||
|
// don't actually delete the entry for primaries. Instead, we delete their payload
|
||||||
|
// and flag them as invalid. This way, we can later figure out that what a request
|
||||||
|
// really want is one of the primaries that have been dropped from the cache, and we
|
||||||
|
// can go get it.
|
||||||
|
mInvalidated = true;
|
||||||
|
mFaviconPayload = null;
|
||||||
|
} else {
|
||||||
|
// Secondaries don't matter - just delete them.
|
||||||
|
if (mBackpointer == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
mBackpointer.mFavicons.remove(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,146 @@
|
||||||
|
/* 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.favicons.cache;
|
||||||
|
|
||||||
|
import android.graphics.Bitmap;
|
||||||
|
import android.util.Log;
|
||||||
|
import org.mozilla.gecko.gfx.BitmapUtils;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
|
||||||
|
public class FaviconsForURL {
|
||||||
|
private static final String LOGTAG = "FaviconForURL";
|
||||||
|
|
||||||
|
private volatile int mDominantColor = -1;
|
||||||
|
|
||||||
|
final long mDownloadTimestamp;
|
||||||
|
final ArrayList<FaviconCacheElement> mFavicons;
|
||||||
|
|
||||||
|
public final boolean mHasFailed;
|
||||||
|
|
||||||
|
public FaviconsForURL(int size) {
|
||||||
|
this(size, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public FaviconsForURL(int size, boolean hasFailed) {
|
||||||
|
mHasFailed = hasFailed;
|
||||||
|
mDownloadTimestamp = System.currentTimeMillis();
|
||||||
|
mFavicons = new ArrayList<FaviconCacheElement>(size);
|
||||||
|
}
|
||||||
|
|
||||||
|
public FaviconCacheElement addSecondary(Bitmap favicon, int imageSize) {
|
||||||
|
return addInternal(favicon, false, imageSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
public FaviconCacheElement addPrimary(Bitmap favicon) {
|
||||||
|
return addInternal(favicon, true, favicon.getWidth());
|
||||||
|
}
|
||||||
|
|
||||||
|
private FaviconCacheElement addInternal(Bitmap favicon, boolean isPrimary, int imageSize) {
|
||||||
|
FaviconCacheElement c = new FaviconCacheElement(favicon, isPrimary, imageSize, this);
|
||||||
|
|
||||||
|
int index = Collections.binarySearch(mFavicons, c);
|
||||||
|
if (index < 0) {
|
||||||
|
index = 0;
|
||||||
|
}
|
||||||
|
mFavicons.add(index, c);
|
||||||
|
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the index of the smallest image in this collection larger than or equal to
|
||||||
|
* the given target size.
|
||||||
|
*
|
||||||
|
* @param targetSize Minimum size for the desired result.
|
||||||
|
* @return The index of the smallest image larger than the target size, or -1 if none exists.
|
||||||
|
*/
|
||||||
|
public int getNextHighestIndex(int targetSize) {
|
||||||
|
// Create a dummy object to hold the target value for comparable.
|
||||||
|
FaviconCacheElement dummy = new FaviconCacheElement(null, false, targetSize, null);
|
||||||
|
|
||||||
|
int index = Collections.binarySearch(mFavicons, dummy);
|
||||||
|
|
||||||
|
// The search routine returns the index of an element equal to dummy, if present.
|
||||||
|
// Otherwise, it returns -x - 1, where x is the index in the ArrayList where dummy would be
|
||||||
|
// inserted if the list were to remain sorted.
|
||||||
|
if (index < 0) {
|
||||||
|
index++;
|
||||||
|
index = -index;
|
||||||
|
}
|
||||||
|
|
||||||
|
// index is now 'x', as described above.
|
||||||
|
|
||||||
|
// The routine will return mFavicons.size() as the index iff dummy is larger than all elements
|
||||||
|
// present (So the "index at which it should be inserted" is the index after the end.
|
||||||
|
// In this case, we set the sentinel value -1 to indicate that we just requested something
|
||||||
|
// larger than all primaries.
|
||||||
|
if (index == mFavicons.size()) {
|
||||||
|
index = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the next valid primary icon from this collection, starting at the given index.
|
||||||
|
* If the appropriate icon is found, but is invalid, we return null - the proper response is to
|
||||||
|
* reacquire the primary from the database.
|
||||||
|
* If no icon is found, the search is repeated going backwards from the start index to find any
|
||||||
|
* primary at all (The input index may be a secondary which is larger than the actual available
|
||||||
|
* primary.)
|
||||||
|
*
|
||||||
|
* @param fromIndex The index into mFavicons from which to start the search.
|
||||||
|
* @return The FaviconCacheElement of the next valid primary from the given index. If none exists,
|
||||||
|
* then returns the previous valid primary. If none exists, returns null (Insanity.).
|
||||||
|
*/
|
||||||
|
public FaviconCacheElement getNextPrimary(final int fromIndex) {
|
||||||
|
final int numIcons = mFavicons.size();
|
||||||
|
|
||||||
|
int searchIndex = fromIndex;
|
||||||
|
while (searchIndex < numIcons) {
|
||||||
|
FaviconCacheElement element = mFavicons.get(searchIndex);
|
||||||
|
|
||||||
|
if (element.mIsPrimary) {
|
||||||
|
if (element.mInvalidated) {
|
||||||
|
// TODO: Replace with `return null` when ICO decoder is introduced.
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return element;
|
||||||
|
}
|
||||||
|
searchIndex++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// No larger primary available. Let's look for smaller ones...
|
||||||
|
searchIndex = fromIndex - 1;
|
||||||
|
while (searchIndex >= 0) {
|
||||||
|
FaviconCacheElement element = mFavicons.get(searchIndex);
|
||||||
|
|
||||||
|
if (element.mIsPrimary) {
|
||||||
|
if (element.mInvalidated) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return element;
|
||||||
|
}
|
||||||
|
searchIndex--;
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.e(LOGTAG, "No primaries found in Favicon cache structure. This is madness!");
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure the dominant colour field is populated for this favicon.
|
||||||
|
*/
|
||||||
|
public int ensureDominantColor() {
|
||||||
|
if (mDominantColor == -1) {
|
||||||
|
mDominantColor = BitmapUtils.getDominantColor(getNextPrimary(0).mFaviconPayload);
|
||||||
|
}
|
||||||
|
|
||||||
|
return mDominantColor;
|
||||||
|
}
|
||||||
|
}
|
|
@ -416,7 +416,7 @@ public class GeckoLayerClient implements LayerView.Listener, PanZoomTarget
|
||||||
/* This is invoked by JNI on the gecko thread */
|
/* This is invoked by JNI on the gecko thread */
|
||||||
DisplayPortMetrics getDisplayPort(boolean pageSizeUpdate, boolean isBrowserContentDisplayed, int tabId, ImmutableViewportMetrics metrics) {
|
DisplayPortMetrics getDisplayPort(boolean pageSizeUpdate, boolean isBrowserContentDisplayed, int tabId, ImmutableViewportMetrics metrics) {
|
||||||
Tabs tabs = Tabs.getInstance();
|
Tabs tabs = Tabs.getInstance();
|
||||||
if (tabs.isSelectedTab(tabs.getTab(tabId)) && isBrowserContentDisplayed) {
|
if (isBrowserContentDisplayed && tabs.isSelectedTabId(tabId)) {
|
||||||
// for foreground tabs, send the viewport update unless the document
|
// for foreground tabs, send the viewport update unless the document
|
||||||
// displayed is different from the content document. In that case, just
|
// displayed is different from the content document. In that case, just
|
||||||
// calculate the display port.
|
// calculate the display port.
|
||||||
|
|
|
@ -5,29 +5,19 @@
|
||||||
|
|
||||||
package org.mozilla.gecko.home;
|
package org.mozilla.gecko.home;
|
||||||
|
|
||||||
import org.mozilla.gecko.favicons.Favicons;
|
|
||||||
import org.mozilla.gecko.R;
|
import org.mozilla.gecko.R;
|
||||||
import org.mozilla.gecko.Tabs;
|
|
||||||
import org.mozilla.gecko.db.BrowserContract.Bookmarks;
|
import org.mozilla.gecko.db.BrowserContract.Bookmarks;
|
||||||
import org.mozilla.gecko.db.BrowserDB;
|
import org.mozilla.gecko.db.BrowserDB;
|
||||||
import org.mozilla.gecko.db.BrowserDB.URLColumns;
|
|
||||||
import org.mozilla.gecko.gfx.BitmapUtils;
|
|
||||||
import org.mozilla.gecko.home.BookmarksListAdapter.OnRefreshFolderListener;
|
import org.mozilla.gecko.home.BookmarksListAdapter.OnRefreshFolderListener;
|
||||||
import org.mozilla.gecko.home.HomePager.OnUrlOpenListener;
|
import org.mozilla.gecko.home.HomePager.OnUrlOpenListener;
|
||||||
import org.mozilla.gecko.util.ThreadUtils;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.content.ContentResolver;
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.res.Configuration;
|
import android.content.res.Configuration;
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
import android.graphics.Bitmap;
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.support.v4.app.LoaderManager;
|
|
||||||
import android.support.v4.app.LoaderManager.LoaderCallbacks;
|
import android.support.v4.app.LoaderManager.LoaderCallbacks;
|
||||||
import android.support.v4.content.Loader;
|
import android.support.v4.content.Loader;
|
||||||
import android.text.TextUtils;
|
|
||||||
import android.util.Log;
|
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
|
|
@ -16,7 +16,6 @@ import org.mozilla.gecko.ReaderModeUtils;
|
||||||
import org.mozilla.gecko.Tabs;
|
import org.mozilla.gecko.Tabs;
|
||||||
import org.mozilla.gecko.db.BrowserContract.Combined;
|
import org.mozilla.gecko.db.BrowserContract.Combined;
|
||||||
import org.mozilla.gecko.db.BrowserDB;
|
import org.mozilla.gecko.db.BrowserDB;
|
||||||
import org.mozilla.gecko.gfx.BitmapUtils;
|
|
||||||
import org.mozilla.gecko.home.HomeListView.HomeContextMenuInfo;
|
import org.mozilla.gecko.home.HomeListView.HomeContextMenuInfo;
|
||||||
import org.mozilla.gecko.util.ThreadUtils;
|
import org.mozilla.gecko.util.ThreadUtils;
|
||||||
import org.mozilla.gecko.util.UiAsyncTask;
|
import org.mozilla.gecko.util.UiAsyncTask;
|
||||||
|
@ -114,7 +113,7 @@ abstract class HomeFragment extends Fragment {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
HomeContextMenuInfo info = (HomeContextMenuInfo) menuInfo;
|
final HomeContextMenuInfo info = (HomeContextMenuInfo) menuInfo;
|
||||||
final Context context = getActivity().getApplicationContext();
|
final Context context = getActivity().getApplicationContext();
|
||||||
|
|
||||||
final int itemId = item.getItemId();
|
final int itemId = item.getItemId();
|
||||||
|
@ -133,7 +132,14 @@ abstract class HomeFragment extends Fragment {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
new AddToLauncherTask(info.url, info.getDisplayTitle()).execute();
|
// Fetch the largest cacheable icon size.
|
||||||
|
Favicons.getLargestFaviconForPage(info.url, new OnFaviconLoadedListener() {
|
||||||
|
@Override
|
||||||
|
public void onFaviconLoaded(String url, String faviconURL, Bitmap favicon) {
|
||||||
|
GeckoAppShell.createShortcut(info.getDisplayTitle(), info.url, favicon, "");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -219,35 +225,6 @@ abstract class HomeFragment extends Fragment {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class AddToLauncherTask extends UiAsyncTask<Void, Void, String> {
|
|
||||||
private final String mUrl;
|
|
||||||
private final String mTitle;
|
|
||||||
|
|
||||||
public AddToLauncherTask(String url, String title) {
|
|
||||||
super(ThreadUtils.getBackgroundHandler());
|
|
||||||
|
|
||||||
mUrl = url;
|
|
||||||
mTitle = title;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String doInBackground(Void... params) {
|
|
||||||
return Favicons.getFaviconUrlForPageUrl(mUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onPostExecute(String faviconUrl) {
|
|
||||||
OnFaviconLoadedListener listener = new OnFaviconLoadedListener() {
|
|
||||||
@Override
|
|
||||||
public void onFaviconLoaded(String url, Bitmap favicon) {
|
|
||||||
GeckoAppShell.createShortcut(mTitle, mUrl, favicon, "");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Favicons.loadFavicon(mUrl, faviconUrl, 0, listener);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class RemoveBookmarkTask extends UiAsyncTask<Void, Void, Void> {
|
private static class RemoveBookmarkTask extends UiAsyncTask<Void, Void, Void> {
|
||||||
private final Context mContext;
|
private final Context mContext;
|
||||||
private final int mId;
|
private final int mId;
|
||||||
|
|
|
@ -37,12 +37,16 @@ public class TopSitesGridItemView extends RelativeLayout {
|
||||||
// Data backing this view.
|
// Data backing this view.
|
||||||
private String mTitle;
|
private String mTitle;
|
||||||
private String mUrl;
|
private String mUrl;
|
||||||
|
private String mFaviconURL;
|
||||||
|
|
||||||
|
private Bitmap mThumbnail;
|
||||||
|
|
||||||
// Pinned state.
|
// Pinned state.
|
||||||
private boolean mIsPinned = false;
|
private boolean mIsPinned = false;
|
||||||
|
|
||||||
// Empty state.
|
// Empty state.
|
||||||
private boolean mIsEmpty = true;
|
private boolean mIsEmpty = true;
|
||||||
|
private int mLoadId = Favicons.NOT_LOADING;
|
||||||
|
|
||||||
public TopSitesGridItemView(Context context) {
|
public TopSitesGridItemView(Context context) {
|
||||||
this(context, null);
|
this(context, null);
|
||||||
|
@ -150,6 +154,8 @@ public class TopSitesGridItemView extends RelativeLayout {
|
||||||
displayThumbnail(R.drawable.favicon);
|
displayThumbnail(R.drawable.favicon);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
mThumbnail = thumbnail;
|
||||||
|
Favicons.cancelFaviconLoad(mLoadId);
|
||||||
|
|
||||||
mThumbnailView.setScaleType(ScaleType.CENTER_CROP);
|
mThumbnailView.setScaleType(ScaleType.CENTER_CROP);
|
||||||
mThumbnailView.setImageBitmap(thumbnail);
|
mThumbnailView.setImageBitmap(thumbnail);
|
||||||
|
@ -161,16 +167,27 @@ public class TopSitesGridItemView extends RelativeLayout {
|
||||||
*
|
*
|
||||||
* @param favicon The favicon to show as thumbnail.
|
* @param favicon The favicon to show as thumbnail.
|
||||||
*/
|
*/
|
||||||
public void displayFavicon(Bitmap favicon) {
|
public void displayFavicon(Bitmap favicon, String faviconURL) {
|
||||||
|
if (mThumbnail != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (favicon == null) {
|
if (favicon == null) {
|
||||||
// Should show default favicon.
|
// Should show default favicon.
|
||||||
displayThumbnail(R.drawable.favicon);
|
displayThumbnail(R.drawable.favicon);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (faviconURL != null) {
|
||||||
|
mFaviconURL = faviconURL;
|
||||||
|
}
|
||||||
|
|
||||||
mThumbnailView.setScaleType(ScaleType.CENTER);
|
mThumbnailView.setScaleType(ScaleType.CENTER);
|
||||||
mThumbnailView.setImageBitmap(favicon);
|
mThumbnailView.setImageBitmap(favicon);
|
||||||
mThumbnailView.setBackgroundColor(Favicons.getFaviconColor(favicon, mUrl));
|
|
||||||
|
if (mFaviconURL != null) {
|
||||||
|
mThumbnailView.setBackgroundColor(Favicons.getFaviconColor(mFaviconURL));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -190,4 +207,9 @@ public class TopSitesGridItemView extends RelativeLayout {
|
||||||
// Refresh for state change.
|
// Refresh for state change.
|
||||||
refreshDrawableState();
|
refreshDrawableState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setLoadId(int aLoadId) {
|
||||||
|
Favicons.cancelFaviconLoad(mLoadId);
|
||||||
|
mLoadId = aLoadId;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,7 @@ import org.mozilla.gecko.db.BrowserContract.Thumbnails;
|
||||||
import org.mozilla.gecko.db.BrowserDB;
|
import org.mozilla.gecko.db.BrowserDB;
|
||||||
import org.mozilla.gecko.db.BrowserDB.URLColumns;
|
import org.mozilla.gecko.db.BrowserDB.URLColumns;
|
||||||
import org.mozilla.gecko.db.BrowserDB.TopSitesCursorWrapper;
|
import org.mozilla.gecko.db.BrowserDB.TopSitesCursorWrapper;
|
||||||
|
import org.mozilla.gecko.favicons.OnFaviconLoadedListener;
|
||||||
import org.mozilla.gecko.gfx.BitmapUtils;
|
import org.mozilla.gecko.gfx.BitmapUtils;
|
||||||
import org.mozilla.gecko.home.HomeListView.HomeContextMenuInfo;
|
import org.mozilla.gecko.home.HomeListView.HomeContextMenuInfo;
|
||||||
import org.mozilla.gecko.home.HomePager.OnUrlOpenListener;
|
import org.mozilla.gecko.home.HomePager.OnUrlOpenListener;
|
||||||
|
@ -32,7 +33,6 @@ import android.graphics.Bitmap;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.support.v4.app.FragmentManager;
|
import android.support.v4.app.FragmentManager;
|
||||||
import android.support.v4.app.LoaderManager;
|
|
||||||
import android.support.v4.app.LoaderManager.LoaderCallbacks;
|
import android.support.v4.app.LoaderManager.LoaderCallbacks;
|
||||||
import android.support.v4.content.AsyncTaskLoader;
|
import android.support.v4.content.AsyncTaskLoader;
|
||||||
import android.support.v4.content.Loader;
|
import android.support.v4.content.Loader;
|
||||||
|
@ -112,22 +112,6 @@ public class TopSitesPage extends HomeFragment {
|
||||||
// Time in ms until the Gecko thread is reset to normal priority.
|
// Time in ms until the Gecko thread is reset to normal priority.
|
||||||
private static final long PRIORITY_RESET_TIMEOUT = 10000;
|
private static final long PRIORITY_RESET_TIMEOUT = 10000;
|
||||||
|
|
||||||
/**
|
|
||||||
* Class to hold the bitmap of cached thumbnails/favicons.
|
|
||||||
*/
|
|
||||||
public static class Thumbnail {
|
|
||||||
// Thumbnail or favicon.
|
|
||||||
private final boolean isThumbnail;
|
|
||||||
|
|
||||||
// Bitmap of thumbnail/favicon.
|
|
||||||
private final Bitmap bitmap;
|
|
||||||
|
|
||||||
public Thumbnail(Bitmap bitmap, boolean isThumbnail) {
|
|
||||||
this.bitmap = bitmap;
|
|
||||||
this.isThumbnail = isThumbnail;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static TopSitesPage newInstance() {
|
public static TopSitesPage newInstance() {
|
||||||
return new TopSitesPage();
|
return new TopSitesPage();
|
||||||
}
|
}
|
||||||
|
@ -531,7 +515,7 @@ public class TopSitesPage extends HomeFragment {
|
||||||
|
|
||||||
public class TopSitesGridAdapter extends CursorAdapter {
|
public class TopSitesGridAdapter extends CursorAdapter {
|
||||||
// Cache to store the thumbnails.
|
// Cache to store the thumbnails.
|
||||||
private Map<String, Thumbnail> mThumbnails;
|
private Map<String, Bitmap> mThumbnails;
|
||||||
|
|
||||||
public TopSitesGridAdapter(Context context, Cursor cursor) {
|
public TopSitesGridAdapter(Context context, Cursor cursor) {
|
||||||
super(context, cursor);
|
super(context, cursor);
|
||||||
|
@ -554,7 +538,7 @@ public class TopSitesPage extends HomeFragment {
|
||||||
*
|
*
|
||||||
* @param thumbnails A map of urls and their thumbnail bitmaps.
|
* @param thumbnails A map of urls and their thumbnail bitmaps.
|
||||||
*/
|
*/
|
||||||
public void updateThumbnails(Map<String, Thumbnail> thumbnails) {
|
public void updateThumbnails(Map<String, Bitmap> thumbnails) {
|
||||||
mThumbnails = thumbnails;
|
mThumbnails = thumbnails;
|
||||||
notifyDataSetChanged();
|
notifyDataSetChanged();
|
||||||
}
|
}
|
||||||
|
@ -572,7 +556,7 @@ public class TopSitesPage extends HomeFragment {
|
||||||
pinned = ((TopSitesCursorWrapper) cursor).isPinned();
|
pinned = ((TopSitesCursorWrapper) cursor).isPinned();
|
||||||
}
|
}
|
||||||
|
|
||||||
TopSitesGridItemView view = (TopSitesGridItemView) bindView;
|
final TopSitesGridItemView view = (TopSitesGridItemView) bindView;
|
||||||
view.setTitle(title);
|
view.setTitle(title);
|
||||||
view.setUrl(url);
|
view.setUrl(url);
|
||||||
view.setPinned(pinned);
|
view.setPinned(pinned);
|
||||||
|
@ -581,14 +565,18 @@ public class TopSitesPage extends HomeFragment {
|
||||||
if (TextUtils.isEmpty(url)) {
|
if (TextUtils.isEmpty(url)) {
|
||||||
view.displayThumbnail(R.drawable.top_site_add);
|
view.displayThumbnail(R.drawable.top_site_add);
|
||||||
} else {
|
} else {
|
||||||
// Show the thumbnail.
|
// Show the thumbnail, if any.
|
||||||
Thumbnail thumbnail = (mThumbnails != null ? mThumbnails.get(url) : null);
|
Bitmap thumbnail = (mThumbnails != null ? mThumbnails.get(url) : null);
|
||||||
if (thumbnail == null) {
|
if (thumbnail != null) {
|
||||||
view.displayThumbnail(null);
|
view.displayThumbnail(thumbnail);
|
||||||
} else if (thumbnail.isThumbnail) {
|
|
||||||
view.displayThumbnail(thumbnail.bitmap);
|
|
||||||
} else {
|
} else {
|
||||||
view.displayFavicon(thumbnail.bitmap);
|
// If we have no thumbnail, attempt to show a Favicon instead.
|
||||||
|
view.setLoadId(Favicons.getSizedFaviconForPageFromLocal(url, new OnFaviconLoadedListener() {
|
||||||
|
@Override
|
||||||
|
public void onFaviconLoaded(String url, String faviconURL, Bitmap favicon) {
|
||||||
|
view.displayFavicon(favicon, faviconURL);
|
||||||
|
}
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -665,8 +653,8 @@ public class TopSitesPage extends HomeFragment {
|
||||||
/**
|
/**
|
||||||
* An AsyncTaskLoader to load the thumbnails from a cursor.
|
* An AsyncTaskLoader to load the thumbnails from a cursor.
|
||||||
*/
|
*/
|
||||||
private static class ThumbnailsLoader extends AsyncTaskLoader<Map<String, Thumbnail>> {
|
private static class ThumbnailsLoader extends AsyncTaskLoader<Map<String, Bitmap>> {
|
||||||
private Map<String, Thumbnail> mThumbnails;
|
private Map<String, Bitmap> mThumbnails;
|
||||||
private ArrayList<String> mUrls;
|
private ArrayList<String> mUrls;
|
||||||
|
|
||||||
public ThumbnailsLoader(Context context, ArrayList<String> urls) {
|
public ThumbnailsLoader(Context context, ArrayList<String> urls) {
|
||||||
|
@ -675,7 +663,7 @@ public class TopSitesPage extends HomeFragment {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Map<String, Thumbnail> loadInBackground() {
|
public Map<String, Bitmap> loadInBackground() {
|
||||||
if (mUrls == null || mUrls.size() == 0) {
|
if (mUrls == null || mUrls.size() == 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -688,7 +676,7 @@ public class TopSitesPage extends HomeFragment {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
final Map<String, Thumbnail> thumbnails = new HashMap<String, Thumbnail>();
|
final Map<String, Bitmap> thumbnails = new HashMap<String, Bitmap>();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final int urlIndex = cursor.getColumnIndexOrThrow(Thumbnails.URL);
|
final int urlIndex = cursor.getColumnIndexOrThrow(Thumbnails.URL);
|
||||||
|
@ -713,29 +701,17 @@ public class TopSitesPage extends HomeFragment {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
thumbnails.put(url, new Thumbnail(bitmap, true));
|
thumbnails.put(url, bitmap);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
cursor.close();
|
cursor.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Query the DB for favicons for the urls without thumbnails.
|
|
||||||
for (String url : mUrls) {
|
|
||||||
if (!thumbnails.containsKey(url)) {
|
|
||||||
final Bitmap bitmap = BrowserDB.getFaviconForUrl(cr, url);
|
|
||||||
if (bitmap != null) {
|
|
||||||
// Favicons.scaleImage can return several different size favicons,
|
|
||||||
// but will at least prevent this from being too large.
|
|
||||||
thumbnails.put(url, new Thumbnail(Favicons.scaleImage(bitmap), false));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return thumbnails;
|
return thumbnails;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void deliverResult(Map<String, Thumbnail> thumbnails) {
|
public void deliverResult(Map<String, Bitmap> thumbnails) {
|
||||||
if (isReset()) {
|
if (isReset()) {
|
||||||
mThumbnails = null;
|
mThumbnails = null;
|
||||||
return;
|
return;
|
||||||
|
@ -765,7 +741,7 @@ public class TopSitesPage extends HomeFragment {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCanceled(Map<String, Thumbnail> thumbnails) {
|
public void onCanceled(Map<String, Bitmap> thumbnails) {
|
||||||
mThumbnails = null;
|
mThumbnails = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -783,14 +759,14 @@ public class TopSitesPage extends HomeFragment {
|
||||||
/**
|
/**
|
||||||
* Loader callbacks for the thumbnails on TopSitesGridView.
|
* Loader callbacks for the thumbnails on TopSitesGridView.
|
||||||
*/
|
*/
|
||||||
private class ThumbnailsLoaderCallbacks implements LoaderCallbacks<Map<String, Thumbnail>> {
|
private class ThumbnailsLoaderCallbacks implements LoaderCallbacks<Map<String, Bitmap>> {
|
||||||
@Override
|
@Override
|
||||||
public Loader<Map<String, Thumbnail>> onCreateLoader(int id, Bundle args) {
|
public Loader<Map<String, Bitmap>> onCreateLoader(int id, Bundle args) {
|
||||||
return new ThumbnailsLoader(getActivity(), args.getStringArrayList(THUMBNAILS_URLS_KEY));
|
return new ThumbnailsLoader(getActivity(), args.getStringArrayList(THUMBNAILS_URLS_KEY));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onLoadFinished(Loader<Map<String, Thumbnail>> loader, Map<String, Thumbnail> thumbnails) {
|
public void onLoadFinished(Loader<Map<String, Bitmap>> loader, Map<String, Bitmap> thumbnails) {
|
||||||
if (mGridAdapter != null) {
|
if (mGridAdapter != null) {
|
||||||
mGridAdapter.updateThumbnails(thumbnails);
|
mGridAdapter.updateThumbnails(thumbnails);
|
||||||
}
|
}
|
||||||
|
@ -801,7 +777,7 @@ public class TopSitesPage extends HomeFragment {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onLoaderReset(Loader<Map<String, Thumbnail>> loader) {
|
public void onLoaderReset(Loader<Map<String, Bitmap>> loader) {
|
||||||
if (mGridAdapter != null) {
|
if (mGridAdapter != null) {
|
||||||
mGridAdapter.updateThumbnails(null);
|
mGridAdapter.updateThumbnails(null);
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,24 +5,20 @@
|
||||||
|
|
||||||
package org.mozilla.gecko.home;
|
package org.mozilla.gecko.home;
|
||||||
|
|
||||||
|
import android.util.Log;
|
||||||
import org.mozilla.gecko.favicons.Favicons;
|
import org.mozilla.gecko.favicons.Favicons;
|
||||||
import org.mozilla.gecko.R;
|
import org.mozilla.gecko.R;
|
||||||
import org.mozilla.gecko.Tab;
|
import org.mozilla.gecko.Tab;
|
||||||
import org.mozilla.gecko.Tabs;
|
import org.mozilla.gecko.Tabs;
|
||||||
import org.mozilla.gecko.db.BrowserContract.Combined;
|
import org.mozilla.gecko.db.BrowserContract.Combined;
|
||||||
import org.mozilla.gecko.db.BrowserDB;
|
|
||||||
import org.mozilla.gecko.db.BrowserDB.URLColumns;
|
import org.mozilla.gecko.db.BrowserDB.URLColumns;
|
||||||
import org.mozilla.gecko.gfx.BitmapUtils;
|
import org.mozilla.gecko.favicons.OnFaviconLoadedListener;
|
||||||
import org.mozilla.gecko.util.ThreadUtils;
|
import org.mozilla.gecko.util.ThreadUtils;
|
||||||
import org.mozilla.gecko.util.UiAsyncTask;
|
|
||||||
import org.mozilla.gecko.widget.FaviconView;
|
import org.mozilla.gecko.widget.FaviconView;
|
||||||
|
|
||||||
import android.content.ContentResolver;
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
import android.graphics.Bitmap;
|
import android.graphics.Bitmap;
|
||||||
import android.os.AsyncTask;
|
|
||||||
import android.os.Build;
|
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.util.AttributeSet;
|
import android.util.AttributeSet;
|
||||||
import android.view.Gravity;
|
import android.view.Gravity;
|
||||||
|
@ -41,12 +37,19 @@ public class TwoLinePageRow extends LinearLayout
|
||||||
private int mUrlIconId;
|
private int mUrlIconId;
|
||||||
private int mBookmarkIconId;
|
private int mBookmarkIconId;
|
||||||
private boolean mShowIcons;
|
private boolean mShowIcons;
|
||||||
|
private int mLoadFaviconJobId = Favicons.NOT_LOADING;
|
||||||
|
|
||||||
|
// Listener for handling Favicon loads.
|
||||||
|
private final OnFaviconLoadedListener mFaviconListener = new OnFaviconLoadedListener() {
|
||||||
|
@Override
|
||||||
|
public void onFaviconLoaded(String url, String faviconURL, Bitmap favicon) {
|
||||||
|
setFaviconWithUrl(favicon, faviconURL);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// The URL for the page corresponding to this view.
|
// The URL for the page corresponding to this view.
|
||||||
private String mPageUrl;
|
private String mPageUrl;
|
||||||
|
|
||||||
private LoadFaviconTask mLoadFaviconTask;
|
|
||||||
|
|
||||||
public TwoLinePageRow(Context context) {
|
public TwoLinePageRow(Context context) {
|
||||||
this(context, null);
|
this(context, null);
|
||||||
}
|
}
|
||||||
|
@ -81,8 +84,6 @@ public class TwoLinePageRow extends LinearLayout
|
||||||
Tabs.unregisterOnTabsChangedListener(TwoLinePageRow.this);
|
Tabs.unregisterOnTabsChangedListener(TwoLinePageRow.this);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
cancelLoadFaviconTask();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -118,7 +119,11 @@ public class TwoLinePageRow extends LinearLayout
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setFaviconWithUrl(Bitmap favicon, String url) {
|
private void setFaviconWithUrl(Bitmap favicon, String url) {
|
||||||
mFavicon.updateImage(favicon, url);
|
if (favicon == null) {
|
||||||
|
mFavicon.showDefaultFavicon();
|
||||||
|
} else {
|
||||||
|
mFavicon.updateImage(favicon, url);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setBookmarkIcon(int bookmarkIconId) {
|
private void setBookmarkIcon(int bookmarkIconId) {
|
||||||
|
@ -139,16 +144,6 @@ public class TwoLinePageRow extends LinearLayout
|
||||||
updateDisplayedUrl();
|
updateDisplayedUrl();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Cancels any pending favicon loading task associated with this view.
|
|
||||||
*/
|
|
||||||
private void cancelLoadFaviconTask() {
|
|
||||||
if (mLoadFaviconTask != null) {
|
|
||||||
mLoadFaviconTask.cancel(true);
|
|
||||||
mLoadFaviconTask = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Replaces the page URL with "Switch to tab" if there is already a tab open with that URL.
|
* Replaces the page URL with "Switch to tab" if there is already a tab open with that URL.
|
||||||
* Only looks for tabs that are either private or non-private, depending on the current
|
* Only looks for tabs that are either private or non-private, depending on the current
|
||||||
|
@ -181,106 +176,46 @@ public class TwoLinePageRow extends LinearLayout
|
||||||
int urlIndex = cursor.getColumnIndexOrThrow(URLColumns.URL);
|
int urlIndex = cursor.getColumnIndexOrThrow(URLColumns.URL);
|
||||||
final String url = cursor.getString(urlIndex);
|
final String url = cursor.getString(urlIndex);
|
||||||
|
|
||||||
|
if (mShowIcons) {
|
||||||
|
final int bookmarkIdIndex = cursor.getColumnIndex(Combined.BOOKMARK_ID);
|
||||||
|
if (bookmarkIdIndex != -1) {
|
||||||
|
final long bookmarkId = cursor.getLong(bookmarkIdIndex);
|
||||||
|
final int displayIndex = cursor.getColumnIndex(Combined.DISPLAY);
|
||||||
|
|
||||||
|
final int display;
|
||||||
|
if (displayIndex != -1) {
|
||||||
|
display = cursor.getInt(displayIndex);
|
||||||
|
} else {
|
||||||
|
display = Combined.DISPLAY_NORMAL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The bookmark id will be 0 (null in database) when the url
|
||||||
|
// is not a bookmark.
|
||||||
|
if (bookmarkId == 0) {
|
||||||
|
setBookmarkIcon(NO_ICON);
|
||||||
|
} else if (display == Combined.DISPLAY_READER) {
|
||||||
|
setBookmarkIcon(R.drawable.ic_url_bar_reader);
|
||||||
|
} else {
|
||||||
|
setBookmarkIcon(R.drawable.ic_url_bar_star);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setBookmarkIcon(NO_ICON);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// No point updating the below things if URL has not changed. Prevents evil Favicon flicker.
|
||||||
|
if (url.equals(mPageUrl)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Use the URL instead of an empty title for consistency with the normal URL
|
// Use the URL instead of an empty title for consistency with the normal URL
|
||||||
// bar view - this is the equivalent of getDisplayTitle() in Tab.java
|
// bar view - this is the equivalent of getDisplayTitle() in Tab.java
|
||||||
setTitle(TextUtils.isEmpty(title) ? url : title);
|
setTitle(TextUtils.isEmpty(title) ? url : title);
|
||||||
|
|
||||||
// No need to do extra work if the URL associated with this view
|
// Blank the Favicon, so we don't show the wrong Favicon if we scroll and miss DB.
|
||||||
// hasn't changed.
|
mFavicon.clearImage();
|
||||||
if (TextUtils.equals(mPageUrl, url)) {
|
mLoadFaviconJobId = Favicons.getSizedFaviconForPageFromLocal(url, mFaviconListener);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
updateDisplayedUrl(url);
|
updateDisplayedUrl(url);
|
||||||
cancelLoadFaviconTask();
|
|
||||||
|
|
||||||
// First, try to find the favicon in the memory cache. If it's not
|
|
||||||
// cached yet, try to load it from the database, off main thread.
|
|
||||||
final Bitmap favicon = Favicons.getFaviconFromMemCache(url);
|
|
||||||
if (favicon != null) {
|
|
||||||
setFaviconWithUrl(favicon, url);
|
|
||||||
} else {
|
|
||||||
// Show blank image until the new favicon finishes loading
|
|
||||||
mFavicon.clearImage();
|
|
||||||
|
|
||||||
mLoadFaviconTask = new LoadFaviconTask(TwoLinePageRow.this, url);
|
|
||||||
|
|
||||||
// Try to use a thread pool instead of serial execution of tasks
|
|
||||||
// to add more throughput to the favicon loading routines.
|
|
||||||
if (Build.VERSION.SDK_INT >= 11) {
|
|
||||||
mLoadFaviconTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
|
||||||
} else {
|
|
||||||
mLoadFaviconTask.execute();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Don't show bookmark/reading list icon, if not needed.
|
|
||||||
if (!mShowIcons) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final int bookmarkIdIndex = cursor.getColumnIndex(Combined.BOOKMARK_ID);
|
|
||||||
if (bookmarkIdIndex != -1) {
|
|
||||||
final long bookmarkId = cursor.getLong(bookmarkIdIndex);
|
|
||||||
final int displayIndex = cursor.getColumnIndex(Combined.DISPLAY);
|
|
||||||
|
|
||||||
final int display;
|
|
||||||
if (displayIndex != -1) {
|
|
||||||
display = cursor.getInt(displayIndex);
|
|
||||||
} else {
|
|
||||||
display = Combined.DISPLAY_NORMAL;
|
|
||||||
}
|
|
||||||
|
|
||||||
// The bookmark id will be 0 (null in database) when the url
|
|
||||||
// is not a bookmark.
|
|
||||||
if (bookmarkId == 0) {
|
|
||||||
setBookmarkIcon(NO_ICON);
|
|
||||||
} else if (display == Combined.DISPLAY_READER) {
|
|
||||||
setBookmarkIcon(R.drawable.ic_url_bar_reader);
|
|
||||||
} else {
|
|
||||||
setBookmarkIcon(R.drawable.ic_url_bar_star);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
setBookmarkIcon(NO_ICON);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void onFaviconLoaded(Bitmap favicon, String url) {
|
|
||||||
if (TextUtils.equals(mPageUrl, url)) {
|
|
||||||
setFaviconWithUrl(favicon, url);
|
|
||||||
}
|
|
||||||
|
|
||||||
mLoadFaviconTask = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class LoadFaviconTask extends AsyncTask<Void, Void, Bitmap> {
|
|
||||||
private final TwoLinePageRow mRow;
|
|
||||||
private final String mUrl;
|
|
||||||
|
|
||||||
public LoadFaviconTask(TwoLinePageRow row, String url) {
|
|
||||||
mRow = row;
|
|
||||||
mUrl = url;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Bitmap doInBackground(Void... params) {
|
|
||||||
Bitmap favicon = Favicons.getFaviconFromMemCache(mUrl);
|
|
||||||
if (favicon == null) {
|
|
||||||
final ContentResolver cr = mRow.getContext().getContentResolver();
|
|
||||||
|
|
||||||
final Bitmap faviconFromDb = BrowserDB.getFaviconForUrl(cr, mUrl);
|
|
||||||
if (faviconFromDb != null) {
|
|
||||||
favicon = Favicons.scaleImage(faviconFromDb);
|
|
||||||
Favicons.putFaviconInMemCache(mUrl, favicon);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return favicon;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onPostExecute(Bitmap favicon) {
|
|
||||||
mRow.onFaviconLoaded(favicon, mUrl);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -141,8 +141,7 @@
|
||||||
android:layout_marginLeft="8dip"
|
android:layout_marginLeft="8dip"
|
||||||
android:paddingLeft="4dip"
|
android:paddingLeft="4dip"
|
||||||
android:paddingRight="4dip"
|
android:paddingRight="4dip"
|
||||||
android:layout_gravity="center_vertical"
|
android:layout_gravity="center_vertical"/>
|
||||||
android:src="@drawable/favicon"/>
|
|
||||||
|
|
||||||
<ImageButton android:id="@+id/site_security"
|
<ImageButton android:id="@+id/site_security"
|
||||||
style="@style/UrlBar.ImageButton"
|
style="@style/UrlBar.ImageButton"
|
||||||
|
|
|
@ -19,6 +19,13 @@
|
||||||
<dimen name="favicon_size_large">32dp</dimen>
|
<dimen name="favicon_size_large">32dp</dimen>
|
||||||
<dimen name="favicon_bg">32dp</dimen>
|
<dimen name="favicon_bg">32dp</dimen>
|
||||||
<dimen name="favicon_bg_radius">1dp</dimen>
|
<dimen name="favicon_bg_radius">1dp</dimen>
|
||||||
|
<!-- Set the upper limit on the size of favicon that will be processed. Favicons larger than
|
||||||
|
this will be downscaled to this value. If you need to use larger Favicons (Due to a UI
|
||||||
|
redesign sometime after this is written) you should increase this value to the largest
|
||||||
|
commonly-used size of favicon and, performance permitting, fetch the remainder from the
|
||||||
|
database. The largest available size is always stored in the database, regardless of this
|
||||||
|
value.-->
|
||||||
|
<dimen name="favicon_largest_interesting_size">32dp</dimen>
|
||||||
|
|
||||||
<!-- Page Row height -->
|
<!-- Page Row height -->
|
||||||
<dimen name="page_row_height">64dp</dimen>
|
<dimen name="page_row_height">64dp</dimen>
|
||||||
|
|
|
@ -98,7 +98,7 @@ abstract class AboutHomeTest extends BaseTest {
|
||||||
protected View getDisplayedBookmark(String url) {
|
protected View getDisplayedBookmark(String url) {
|
||||||
openAboutHomeTab(AboutHomeTabs.BOOKMARKS);
|
openAboutHomeTab(AboutHomeTabs.BOOKMARKS);
|
||||||
ListView bookmarksTabList = findListViewWithTag("bookmarks");
|
ListView bookmarksTabList = findListViewWithTag("bookmarks");
|
||||||
waitForListToLoad(bookmarksTabList);
|
waitForNonEmptyListToLoad(bookmarksTabList);
|
||||||
ListAdapter adapter = bookmarksTabList.getAdapter();
|
ListAdapter adapter = bookmarksTabList.getAdapter();
|
||||||
if (adapter != null) {
|
if (adapter != null) {
|
||||||
for (int i = 0; i < adapter.getCount(); i++ ) {
|
for (int i = 0; i < adapter.getCount(); i++ ) {
|
||||||
|
@ -127,11 +127,13 @@ abstract class AboutHomeTest extends BaseTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Waits for the given ListView to have a non-empty adapter.
|
* Waits for the given ListView to have a non-empty adapter and be populated
|
||||||
|
* with a minimum number of items.
|
||||||
*
|
*
|
||||||
* This method will return false if the given ListView or its adapter are null.
|
* This method will return false if the given ListView or its adapter is null,
|
||||||
|
* or if the ListView does not have the minimum number of items.
|
||||||
*/
|
*/
|
||||||
protected boolean waitForListToLoad(final ListView listView) {
|
protected boolean waitForListToLoad(final ListView listView, final int minSize) {
|
||||||
Condition listWaitCondition = new Condition() {
|
Condition listWaitCondition = new Condition() {
|
||||||
@Override
|
@Override
|
||||||
public boolean isSatisfied() {
|
public boolean isSatisfied() {
|
||||||
|
@ -144,12 +146,16 @@ abstract class AboutHomeTest extends BaseTest {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (adapter.getCount() > 0);
|
return (listView.getCount() - listView.getHeaderViewsCount() >= minSize);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
return waitForCondition(listWaitCondition, MAX_WAIT_MS);
|
return waitForCondition(listWaitCondition, MAX_WAIT_MS);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected boolean waitForNonEmptyListToLoad(final ListView listView) {
|
||||||
|
return waitForListToLoad(listView, 1);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get an active ListView with the specified tag .
|
* Get an active ListView with the specified tag .
|
||||||
*
|
*
|
||||||
|
|
|
@ -44,7 +44,7 @@ public class testBookmarklets extends AboutHomeTest {
|
||||||
openAboutHomeTab(AboutHomeTabs.BOOKMARKS);
|
openAboutHomeTab(AboutHomeTabs.BOOKMARKS);
|
||||||
|
|
||||||
ListView bookmarks = findListViewWithTag("bookmarks");
|
ListView bookmarks = findListViewWithTag("bookmarks");
|
||||||
mAsserter.is(waitForListToLoad(bookmarks), true, "list is properly loaded");
|
mAsserter.is(waitForNonEmptyListToLoad(bookmarks), true, "list is properly loaded");
|
||||||
|
|
||||||
int width = mDriver.getGeckoWidth();
|
int width = mDriver.getGeckoWidth();
|
||||||
int height = mDriver.getGeckoHeight();
|
int height = mDriver.getGeckoHeight();
|
||||||
|
|
|
@ -33,7 +33,7 @@ public class testHistory extends AboutHomeTest {
|
||||||
openAboutHomeTab(AboutHomeTabs.MOST_RECENT);
|
openAboutHomeTab(AboutHomeTabs.MOST_RECENT);
|
||||||
|
|
||||||
final ListView hList = findListViewWithTag("most_recent");
|
final ListView hList = findListViewWithTag("most_recent");
|
||||||
mAsserter.is(waitForListToLoad(hList), true, "list is properly loaded");
|
mAsserter.is(waitForNonEmptyListToLoad(hList), true, "list is properly loaded");
|
||||||
|
|
||||||
// Click on the history item and wait for the page to load
|
// Click on the history item and wait for the page to load
|
||||||
// wait for the history list to be populated
|
// wait for the history list to be populated
|
||||||
|
|
|
@ -72,7 +72,7 @@ public class testShareLink extends AboutHomeTest {
|
||||||
openAboutHomeTab(AboutHomeTabs.BOOKMARKS);
|
openAboutHomeTab(AboutHomeTabs.BOOKMARKS);
|
||||||
|
|
||||||
ListView bookmarksList = findListViewWithTag("bookmarks");
|
ListView bookmarksList = findListViewWithTag("bookmarks");
|
||||||
mAsserter.is(waitForListToLoad(bookmarksList), true, "list is properly loaded");
|
mAsserter.is(waitForNonEmptyListToLoad(bookmarksList), true, "list is properly loaded");
|
||||||
|
|
||||||
View bookmarksItem = bookmarksList.getChildAt(bookmarksList.getHeaderViewsCount());
|
View bookmarksItem = bookmarksList.getChildAt(bookmarksList.getHeaderViewsCount());
|
||||||
mSolo.clickLongOnView(bookmarksItem);
|
mSolo.clickLongOnView(bookmarksItem);
|
||||||
|
@ -101,7 +101,7 @@ public class testShareLink extends AboutHomeTest {
|
||||||
mActions.drag(width / 2, width / 2, height - 10, height / 2);
|
mActions.drag(width / 2, width / 2, height - 10, height / 2);
|
||||||
|
|
||||||
ListView topSitesList = findListViewWithTag("top_sites");
|
ListView topSitesList = findListViewWithTag("top_sites");
|
||||||
mAsserter.is(waitForListToLoad(topSitesList), true, "list is properly loaded");
|
mAsserter.is(waitForNonEmptyListToLoad(topSitesList), true, "list is properly loaded");
|
||||||
View mostVisitedItem = topSitesList.getChildAt(topSitesList.getHeaderViewsCount());
|
View mostVisitedItem = topSitesList.getChildAt(topSitesList.getHeaderViewsCount());
|
||||||
mSolo.clickLongOnView(mostVisitedItem);
|
mSolo.clickLongOnView(mostVisitedItem);
|
||||||
verifySharePopup(shareOptions,"top_sites");
|
verifySharePopup(shareOptions,"top_sites");
|
||||||
|
@ -110,7 +110,7 @@ public class testShareLink extends AboutHomeTest {
|
||||||
openAboutHomeTab(AboutHomeTabs.MOST_RECENT);
|
openAboutHomeTab(AboutHomeTabs.MOST_RECENT);
|
||||||
|
|
||||||
ListView mostRecentList = findListViewWithTag("most_recent");
|
ListView mostRecentList = findListViewWithTag("most_recent");
|
||||||
mAsserter.is(waitForListToLoad(mostRecentList), true, "list is properly loaded");
|
mAsserter.is(waitForNonEmptyListToLoad(mostRecentList), true, "list is properly loaded");
|
||||||
|
|
||||||
// Getting second child after header views because the first is the "Today" label
|
// Getting second child after header views because the first is the "Today" label
|
||||||
View mostRecentItem = mostRecentList.getChildAt(mostRecentList.getHeaderViewsCount() + 1);
|
View mostRecentItem = mostRecentList.getChildAt(mostRecentList.getHeaderViewsCount() + 1);
|
||||||
|
|
|
@ -106,7 +106,11 @@ public class FaviconView extends ImageView {
|
||||||
* space.
|
* space.
|
||||||
*/
|
*/
|
||||||
private void showBackground() {
|
private void showBackground() {
|
||||||
int color = Favicons.getFaviconColor(mIconBitmap, mIconKey);
|
int color = Favicons.getFaviconColor(mIconKey);
|
||||||
|
if (color == -1) {
|
||||||
|
hideBackground();
|
||||||
|
return;
|
||||||
|
}
|
||||||
color = Color.argb(70, Color.red(color), Color.green(color), Color.blue(color));
|
color = Color.argb(70, Color.red(color), Color.green(color), Color.blue(color));
|
||||||
final Drawable drawable = getResources().getDrawable(R.drawable.favicon_bg);
|
final Drawable drawable = getResources().getDrawable(R.drawable.favicon_bg);
|
||||||
drawable.setColorFilter(color, Mode.SRC_ATOP);
|
drawable.setColorFilter(color, Mode.SRC_ATOP);
|
||||||
|
@ -152,7 +156,7 @@ public class FaviconView extends ImageView {
|
||||||
formatImage();
|
formatImage();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void showDefaultFavicon() {
|
public void showDefaultFavicon() {
|
||||||
setImageResource(R.drawable.favicon);
|
setImageResource(R.drawable.favicon);
|
||||||
hideBackground();
|
hideBackground();
|
||||||
}
|
}
|
||||||
|
|
|
@ -243,6 +243,7 @@
|
||||||
@BINPATH@/components/spellchecker.xpt
|
@BINPATH@/components/spellchecker.xpt
|
||||||
@BINPATH@/components/storage.xpt
|
@BINPATH@/components/storage.xpt
|
||||||
@BINPATH@/components/telemetry.xpt
|
@BINPATH@/components/telemetry.xpt
|
||||||
|
@BINPATH@/components/toolkit_finalizationwitness.xpt
|
||||||
@BINPATH@/components/toolkitprofile.xpt
|
@BINPATH@/components/toolkitprofile.xpt
|
||||||
#ifdef MOZ_ENABLE_XREMOTE
|
#ifdef MOZ_ENABLE_XREMOTE
|
||||||
@BINPATH@/components/toolkitremote.xpt
|
@BINPATH@/components/toolkitremote.xpt
|
||||||
|
|
|
@ -36,7 +36,6 @@
|
||||||
"content/base/test/test_websocket_hello.html": "",
|
"content/base/test/test_websocket_hello.html": "",
|
||||||
"content/base/test/test_x-frame-options.html": "",
|
"content/base/test/test_x-frame-options.html": "",
|
||||||
"content/base/test/test_xhr_abort_after_load.html": "",
|
"content/base/test/test_xhr_abort_after_load.html": "",
|
||||||
"content/base/test/test_xhr_forbidden_headers.html": "",
|
|
||||||
"content/base/test/test_xhr_progressevents.html": "",
|
"content/base/test/test_xhr_progressevents.html": "",
|
||||||
"content/base/test/websocket_hybi/test_receive-arraybuffer.html": "",
|
"content/base/test/websocket_hybi/test_receive-arraybuffer.html": "",
|
||||||
"content/base/test/websocket_hybi/test_receive-blob.html": "",
|
"content/base/test/websocket_hybi/test_receive-blob.html": "",
|
||||||
|
|
|
@ -37,7 +37,6 @@
|
||||||
"content/base/test/test_websocket_hello.html": "",
|
"content/base/test/test_websocket_hello.html": "",
|
||||||
"content/base/test/test_x-frame-options.html": "",
|
"content/base/test/test_x-frame-options.html": "",
|
||||||
"content/base/test/test_xhr_abort_after_load.html": "",
|
"content/base/test/test_xhr_abort_after_load.html": "",
|
||||||
"content/base/test/test_xhr_forbidden_headers.html": "",
|
|
||||||
"content/base/test/test_xhr_progressevents.html": "",
|
"content/base/test/test_xhr_progressevents.html": "",
|
||||||
"content/base/test/websocket_hybi/test_receive-arraybuffer.html": "",
|
"content/base/test/websocket_hybi/test_receive-arraybuffer.html": "",
|
||||||
"content/base/test/websocket_hybi/test_receive-blob.html": "",
|
"content/base/test/websocket_hybi/test_receive-blob.html": "",
|
||||||
|
|
|
@ -92,8 +92,7 @@
|
||||||
"content/base/test/test_bug431701.html":"xmlhttprequest causes crash, bug 902271",
|
"content/base/test/test_bug431701.html":"xmlhttprequest causes crash, bug 902271",
|
||||||
"content/base/test/test_bug422537.html":"xmlhttprequest causes crash, bug 902271",
|
"content/base/test/test_bug422537.html":"xmlhttprequest causes crash, bug 902271",
|
||||||
|
|
||||||
"content/base/test/test_bug338583.html":"43 total - bug 901343, specialpowers.wrap issue createsystemxhr",
|
"content/base/test/test_bug338583.html":"https not working, bug 907770",
|
||||||
"content/base/test/test_bug804395.html":"bug 901343, specialpowers.wrap issue createsystemxhr",
|
|
||||||
|
|
||||||
"content/base/test/test_bug475156.html":"36 total - bug 902611",
|
"content/base/test/test_bug475156.html":"36 total - bug 902611",
|
||||||
"content/base/test/test_bug422403-1.html":"bug 901343, specialpowers.wrap issue [nsIChannel.open]",
|
"content/base/test/test_bug422403-1.html":"bug 901343, specialpowers.wrap issue [nsIChannel.open]",
|
||||||
|
@ -249,7 +248,6 @@
|
||||||
"content/base/test/test_bug422403-2.xhtml":"",
|
"content/base/test/test_bug422403-2.xhtml":"",
|
||||||
"content/base/test/test_bug424359-1.html":"",
|
"content/base/test/test_bug424359-1.html":"",
|
||||||
"content/base/test/test_bug424359-2.html":"",
|
"content/base/test/test_bug424359-2.html":"",
|
||||||
"content/base/test/test_bug426308.html":"",
|
|
||||||
"content/base/test/test_mixed_content_blocker_bug803225.html":"",
|
"content/base/test/test_mixed_content_blocker_bug803225.html":"",
|
||||||
"content/html/document/test/test_non-ascii-cookie.html":"",
|
"content/html/document/test/test_non-ascii-cookie.html":"",
|
||||||
|
|
||||||
|
@ -321,7 +319,7 @@
|
||||||
"dom/media/tests/mochitest/test_peerConnection_throwInCallbacks.html":"",
|
"dom/media/tests/mochitest/test_peerConnection_throwInCallbacks.html":"",
|
||||||
|
|
||||||
"dom/network/tests/test_networkstats_basics.html":"Will be fixed in bug 858005",
|
"dom/network/tests/test_networkstats_basics.html":"Will be fixed in bug 858005",
|
||||||
"dom/permission/tests/test_permission_basics.html":"Bug 907770",
|
"dom/permission/tests/test_permission_basics.html":"https not working, bug 907770",
|
||||||
|
|
||||||
"dom/tests/mochitest/bugs/test_bug335976.xhtml":"",
|
"dom/tests/mochitest/bugs/test_bug335976.xhtml":"",
|
||||||
"dom/tests/mochitest/bugs/test_bug369306.html":"test timed out, can't focus back from popup window to opener?",
|
"dom/tests/mochitest/bugs/test_bug369306.html":"test timed out, can't focus back from popup window to opener?",
|
||||||
|
|
|
@ -3,9 +3,19 @@
|
||||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
# 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/.
|
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
DEPTH = ../../..
|
||||||
|
topsrcdir = @top_srcdir@
|
||||||
|
srcdir = @srcdir@
|
||||||
|
VPATH = @srcdir@
|
||||||
|
relativesrcdir = testing/mochitest/roboextender
|
||||||
|
TESTPATH = $(topsrcdir)/mobile/android/base/tests/roboextender
|
||||||
|
|
||||||
|
include $(DEPTH)/config/autoconf.mk
|
||||||
|
|
||||||
_TEST_FILES = \
|
_TEST_FILES = \
|
||||||
bootstrap.js \
|
bootstrap.js \
|
||||||
install.rdf \
|
install.rdf \
|
||||||
|
chrome.manifest \
|
||||||
$(NULL)
|
$(NULL)
|
||||||
|
|
||||||
TEST_EXTENSIONS_DIR = $(DEPTH)/_tests/testing/mochitest/extensions
|
TEST_EXTENSIONS_DIR = $(DEPTH)/_tests/testing/mochitest/extensions
|
||||||
|
@ -15,4 +25,6 @@ include $(topsrcdir)/config/rules.mk
|
||||||
libs:: $(_TEST_FILES)
|
libs:: $(_TEST_FILES)
|
||||||
$(MKDIR) -p $(TEST_EXTENSIONS_DIR)/roboextender@mozilla.org
|
$(MKDIR) -p $(TEST_EXTENSIONS_DIR)/roboextender@mozilla.org
|
||||||
$(INSTALL) $(foreach f,$^,"$f") $(TEST_EXTENSIONS_DIR)/roboextender@mozilla.org/
|
$(INSTALL) $(foreach f,$^,"$f") $(TEST_EXTENSIONS_DIR)/roboextender@mozilla.org/
|
||||||
|
$(MKDIR) -p $(TEST_EXTENSIONS_DIR)/roboextender@mozilla.org/base
|
||||||
|
$(MKDIR) -p $(TESTPATH)
|
||||||
|
-cp $(TESTPATH)/* $(TEST_EXTENSIONS_DIR)/roboextender@mozilla.org/base
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
content roboextender base/
|
|
@ -78,11 +78,6 @@ function starttest(){
|
||||||
// Test a DOMWindowUtils method and property
|
// Test a DOMWindowUtils method and property
|
||||||
is(SpecialPowers.DOMWindowUtils.getClassName(window), "Proxy");
|
is(SpecialPowers.DOMWindowUtils.getClassName(window), "Proxy");
|
||||||
is(SpecialPowers.DOMWindowUtils.docCharsetIsForced, false);
|
is(SpecialPowers.DOMWindowUtils.docCharsetIsForced, false);
|
||||||
|
|
||||||
//Run the createSystemXHR method
|
|
||||||
var xhr = SpecialPowers.createSystemXHR();
|
|
||||||
ok(xhr, "createSystemXHR should not return null");
|
|
||||||
is(xhr.readyState, XMLHttpRequest.UNSENT, "createSystemXHR should create an unsent XMLHttpRequest object");
|
|
||||||
|
|
||||||
// QueryInterface and getPrivilegedProps tests
|
// QueryInterface and getPrivilegedProps tests
|
||||||
is(SpecialPowers.can_QI(SpecialPowers), false);
|
is(SpecialPowers.can_QI(SpecialPowers), false);
|
||||||
|
@ -99,7 +94,7 @@ function starttest(){
|
||||||
|
|
||||||
// Try some basic stuff with XHR.
|
// Try some basic stuff with XHR.
|
||||||
var xhr2 = SpecialPowers.Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance(SpecialPowers.Ci.nsIXMLHttpRequest);
|
var xhr2 = SpecialPowers.Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance(SpecialPowers.Ci.nsIXMLHttpRequest);
|
||||||
is(xhr.readyState, XMLHttpRequest.UNSENT, "Should be able to get props off privileged objects");
|
is(xhr2.readyState, XMLHttpRequest.UNSENT, "Should be able to get props off privileged objects");
|
||||||
var testURI = SpecialPowers.Cc['@mozilla.org/network/standard-url;1']
|
var testURI = SpecialPowers.Cc['@mozilla.org/network/standard-url;1']
|
||||||
.createInstance(SpecialPowers.Ci.nsIURI);
|
.createInstance(SpecialPowers.Ci.nsIURI);
|
||||||
testURI.spec = "http://www.foobar.org/";
|
testURI.spec = "http://www.foobar.org/";
|
||||||
|
|
|
@ -1178,10 +1178,6 @@ SpecialPowersAPI.prototype = {
|
||||||
this._getMUDV(window).stopEmulatingMedium();
|
this._getMUDV(window).stopEmulatingMedium();
|
||||||
},
|
},
|
||||||
|
|
||||||
createSystemXHR: function() {
|
|
||||||
return this.wrap(Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance(Ci.nsIXMLHttpRequest));
|
|
||||||
},
|
|
||||||
|
|
||||||
snapshotWindowWithOptions: function (win, rect, bgcolor, options) {
|
snapshotWindowWithOptions: function (win, rect, bgcolor, options) {
|
||||||
var el = this.window.get().document.createElementNS("http://www.w3.org/1999/xhtml", "canvas");
|
var el = this.window.get().document.createElementNS("http://www.w3.org/1999/xhtml", "canvas");
|
||||||
if (rect === undefined) {
|
if (rect === undefined) {
|
||||||
|
|
|
@ -28,6 +28,7 @@ SHARED_LIBRARY_LIBS = \
|
||||||
../jsdownloads/src/$(LIB_PREFIX)jsdownloads_s.$(LIB_SUFFIX) \
|
../jsdownloads/src/$(LIB_PREFIX)jsdownloads_s.$(LIB_SUFFIX) \
|
||||||
../protobuf/$(LIB_PREFIX)protobuf_s.$(LIB_SUFFIX) \
|
../protobuf/$(LIB_PREFIX)protobuf_s.$(LIB_SUFFIX) \
|
||||||
../intl/$(LIB_PREFIX)intl_s.$(LIB_SUFFIX) \
|
../intl/$(LIB_PREFIX)intl_s.$(LIB_SUFFIX) \
|
||||||
|
../finalizationwitness/$(LIB_PREFIX)finalizationwitness_s.$(LIB_SUFFIX) \
|
||||||
$(NULL)
|
$(NULL)
|
||||||
|
|
||||||
ifndef MOZ_DISABLE_PARENTAL_CONTROLS
|
ifndef MOZ_DISABLE_PARENTAL_CONTROLS
|
||||||
|
|
|
@ -34,6 +34,7 @@
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#include "nsBrowserStatusFilter.h"
|
#include "nsBrowserStatusFilter.h"
|
||||||
|
#include "mozilla/FinalizationWitnessService.h"
|
||||||
|
|
||||||
/////////////////////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
@ -85,6 +86,7 @@ NS_GENERIC_FACTORY_CONSTRUCTOR(nsBrowserStatusFilter)
|
||||||
#if defined(USE_MOZ_UPDATER)
|
#if defined(USE_MOZ_UPDATER)
|
||||||
NS_GENERIC_FACTORY_CONSTRUCTOR(nsUpdateProcessor)
|
NS_GENERIC_FACTORY_CONSTRUCTOR(nsUpdateProcessor)
|
||||||
#endif
|
#endif
|
||||||
|
NS_GENERIC_FACTORY_CONSTRUCTOR(FinalizationWitnessService)
|
||||||
|
|
||||||
NS_DEFINE_NAMED_CID(NS_TOOLKIT_APPSTARTUP_CID);
|
NS_DEFINE_NAMED_CID(NS_TOOLKIT_APPSTARTUP_CID);
|
||||||
NS_DEFINE_NAMED_CID(NS_USERINFO_CID);
|
NS_DEFINE_NAMED_CID(NS_USERINFO_CID);
|
||||||
|
@ -109,6 +111,7 @@ NS_DEFINE_NAMED_CID(NS_CHARSETMENU_CID);
|
||||||
#if defined(USE_MOZ_UPDATER)
|
#if defined(USE_MOZ_UPDATER)
|
||||||
NS_DEFINE_NAMED_CID(NS_UPDATEPROCESSOR_CID);
|
NS_DEFINE_NAMED_CID(NS_UPDATEPROCESSOR_CID);
|
||||||
#endif
|
#endif
|
||||||
|
NS_DEFINE_NAMED_CID(FINALIZATIONWITNESSSERVICE_CID);
|
||||||
|
|
||||||
static const mozilla::Module::CIDEntry kToolkitCIDs[] = {
|
static const mozilla::Module::CIDEntry kToolkitCIDs[] = {
|
||||||
{ &kNS_TOOLKIT_APPSTARTUP_CID, false, nullptr, nsAppStartupConstructor },
|
{ &kNS_TOOLKIT_APPSTARTUP_CID, false, nullptr, nsAppStartupConstructor },
|
||||||
|
@ -134,6 +137,7 @@ static const mozilla::Module::CIDEntry kToolkitCIDs[] = {
|
||||||
#if defined(USE_MOZ_UPDATER)
|
#if defined(USE_MOZ_UPDATER)
|
||||||
{ &kNS_UPDATEPROCESSOR_CID, false, nullptr, nsUpdateProcessorConstructor },
|
{ &kNS_UPDATEPROCESSOR_CID, false, nullptr, nsUpdateProcessorConstructor },
|
||||||
#endif
|
#endif
|
||||||
|
{ &kFINALIZATIONWITNESSSERVICE_CID, false, NULL, FinalizationWitnessServiceConstructor },
|
||||||
{ nullptr }
|
{ nullptr }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -162,6 +166,7 @@ static const mozilla::Module::ContractIDEntry kToolkitContracts[] = {
|
||||||
#if defined(USE_MOZ_UPDATER)
|
#if defined(USE_MOZ_UPDATER)
|
||||||
{ NS_UPDATEPROCESSOR_CONTRACTID, &kNS_UPDATEPROCESSOR_CID },
|
{ NS_UPDATEPROCESSOR_CONTRACTID, &kNS_UPDATEPROCESSOR_CID },
|
||||||
#endif
|
#endif
|
||||||
|
{ FINALIZATIONWITNESSSERVICE_CONTRACTID, &kFINALIZATIONWITNESSSERVICE_CID },
|
||||||
{ nullptr }
|
{ nullptr }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,213 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||||
|
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
#include "FinalizationWitnessService.h"
|
||||||
|
|
||||||
|
#include "nsString.h"
|
||||||
|
#include "jsapi.h"
|
||||||
|
#include "js/CallNonGenericMethod.h"
|
||||||
|
#include "mozJSComponentLoader.h"
|
||||||
|
#include "nsZipArchive.h"
|
||||||
|
|
||||||
|
#include "mozilla/Scoped.h"
|
||||||
|
#include "mozilla/Services.h"
|
||||||
|
#include "mozilla/NullPtr.h"
|
||||||
|
#include "nsIObserverService.h"
|
||||||
|
#include "nsThreadUtils.h"
|
||||||
|
|
||||||
|
|
||||||
|
// Implementation of nsIFinalizationWitnessService
|
||||||
|
|
||||||
|
namespace mozilla {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An event meant to be dispatched to the main thread upon finalization
|
||||||
|
* of a FinalizationWitness, unless method |forget()| has been called.
|
||||||
|
*
|
||||||
|
* Held as private data by each instance of FinalizationWitness.
|
||||||
|
* Important note: we maintain the invariant that these private data
|
||||||
|
* slots are already addrefed.
|
||||||
|
*/
|
||||||
|
class FinalizationEvent MOZ_FINAL: public nsRunnable
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
FinalizationEvent(const char* aTopic,
|
||||||
|
const jschar* aValue)
|
||||||
|
: mTopic(aTopic)
|
||||||
|
, mValue(aValue)
|
||||||
|
{ }
|
||||||
|
|
||||||
|
NS_METHOD Run() {
|
||||||
|
nsCOMPtr<nsIObserverService> observerService =
|
||||||
|
mozilla::services::GetObserverService();
|
||||||
|
if (!observerService) {
|
||||||
|
// This is either too early or, more likely, too late for notifications.
|
||||||
|
// Bail out.
|
||||||
|
return NS_ERROR_NOT_AVAILABLE;
|
||||||
|
}
|
||||||
|
(void)observerService->
|
||||||
|
NotifyObservers(nullptr, mTopic.get(), mValue.get());
|
||||||
|
return NS_OK;
|
||||||
|
}
|
||||||
|
private:
|
||||||
|
/**
|
||||||
|
* The topic on which to broadcast the notification of finalization.
|
||||||
|
*
|
||||||
|
* Deallocated on the main thread.
|
||||||
|
*/
|
||||||
|
const nsCString mTopic;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The result of converting the exception to a string.
|
||||||
|
*
|
||||||
|
* Deallocated on the main thread.
|
||||||
|
*/
|
||||||
|
const nsString mValue;
|
||||||
|
};
|
||||||
|
|
||||||
|
enum {
|
||||||
|
WITNESS_SLOT_EVENT,
|
||||||
|
WITNESS_INSTANCES_SLOTS
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract the FinalizationEvent from an instance of FinalizationWitness
|
||||||
|
* and clear the slot containing the FinalizationEvent.
|
||||||
|
*/
|
||||||
|
already_AddRefed<FinalizationEvent>
|
||||||
|
ExtractFinalizationEvent(JSObject *objSelf)
|
||||||
|
{
|
||||||
|
JS::Value slotEvent = JS_GetReservedSlot(objSelf, WITNESS_SLOT_EVENT);
|
||||||
|
if (slotEvent.isUndefined()) {
|
||||||
|
// Forget() has been called
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
JS_SetReservedSlot(objSelf, WITNESS_SLOT_EVENT, JS::UndefinedValue());
|
||||||
|
|
||||||
|
return dont_AddRef(static_cast<FinalizationEvent*>(slotEvent.toPrivate()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finalizer for instances of FinalizationWitness.
|
||||||
|
*
|
||||||
|
* Unless method Forget() has been called, the finalizer displays an error
|
||||||
|
* message.
|
||||||
|
*/
|
||||||
|
void Finalize(JSFreeOp *fop, JSObject *objSelf)
|
||||||
|
{
|
||||||
|
nsRefPtr<FinalizationEvent> event = ExtractFinalizationEvent(objSelf);
|
||||||
|
if (event == nullptr) {
|
||||||
|
// Forget() has been called
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notify observers. Since we are executed during garbage-collection,
|
||||||
|
// we need to dispatch the notification to the main thread.
|
||||||
|
(void)NS_DispatchToMainThread(event);
|
||||||
|
// We may fail at dispatching to the main thread if we arrive too late
|
||||||
|
// during shutdown. In that case, there is not much we can do.
|
||||||
|
}
|
||||||
|
|
||||||
|
static const JSClass sWitnessClass = {
|
||||||
|
"FinalizationWitness",
|
||||||
|
JSCLASS_HAS_RESERVED_SLOTS(WITNESS_INSTANCES_SLOTS),
|
||||||
|
JS_PropertyStub /* addProperty */,
|
||||||
|
JS_DeletePropertyStub /* delProperty */,
|
||||||
|
JS_PropertyStub /* getProperty */,
|
||||||
|
JS_StrictPropertyStub /* setProperty */,
|
||||||
|
JS_EnumerateStub /* enumerate */,
|
||||||
|
JS_ResolveStub /* resolve */,
|
||||||
|
JS_ConvertStub /* convert */,
|
||||||
|
Finalize /* finalize */
|
||||||
|
};
|
||||||
|
|
||||||
|
bool IsWitness(JS::Handle<JS::Value> v)
|
||||||
|
{
|
||||||
|
return v.isObject() && JS_GetClass(&v.toObject()) == &sWitnessClass;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JS method |forget()|
|
||||||
|
*
|
||||||
|
* === JS documentation
|
||||||
|
*
|
||||||
|
* Neutralize the witness. Once this method is called, the witness will
|
||||||
|
* never report any error.
|
||||||
|
*/
|
||||||
|
bool ForgetImpl(JSContext* cx, JS::CallArgs args)
|
||||||
|
{
|
||||||
|
if (args.length() != 0) {
|
||||||
|
JS_ReportError(cx, "forget() takes no arguments");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
JS::Rooted<JS::Value> valSelf(cx, args.thisv());
|
||||||
|
JS::Rooted<JSObject*> objSelf(cx, &valSelf.toObject());
|
||||||
|
|
||||||
|
nsRefPtr<FinalizationEvent> event = ExtractFinalizationEvent(objSelf);
|
||||||
|
if (event == nullptr) {
|
||||||
|
JS_ReportError(cx, "forget() called twice");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
args.rval().setUndefined();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Forget(JSContext *cx, unsigned argc, JS::Value *vp)
|
||||||
|
{
|
||||||
|
JS::CallArgs args = CallArgsFromVp(argc, vp);
|
||||||
|
return JS::CallNonGenericMethod<IsWitness, ForgetImpl>(cx, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
static const JSFunctionSpec sWitnessClassFunctions[] = {
|
||||||
|
JS_FN("forget", Forget, 0, JSPROP_READONLY | JSPROP_PERMANENT),
|
||||||
|
JS_FS_END
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
NS_IMPL_ISUPPORTS1(FinalizationWitnessService, nsIFinalizationWitnessService)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new Finalization Witness.
|
||||||
|
*
|
||||||
|
* A finalization witness is an object whose sole role is to notify
|
||||||
|
* observers when it is gc-ed. Once the witness is created, call its
|
||||||
|
* method |forget()| to prevent the observers from being notified.
|
||||||
|
*
|
||||||
|
* @param aTopic The notification topic.
|
||||||
|
* @param aValue The notification value. Converted to a string.
|
||||||
|
*
|
||||||
|
* @constructor
|
||||||
|
*/
|
||||||
|
NS_IMETHODIMP
|
||||||
|
FinalizationWitnessService::Make(const char* aTopic,
|
||||||
|
const PRUnichar* aValue,
|
||||||
|
JSContext* aCx,
|
||||||
|
JS::Value *aRetval) {
|
||||||
|
MOZ_ASSERT(aRetval);
|
||||||
|
|
||||||
|
JS::Rooted<JSObject*> objResult(aCx, JS_NewObject(aCx, &sWitnessClass, nullptr, nullptr));
|
||||||
|
if (!objResult) {
|
||||||
|
return NS_ERROR_OUT_OF_MEMORY;
|
||||||
|
}
|
||||||
|
if (!JS_DefineFunctions(aCx, objResult, sWitnessClassFunctions)) {
|
||||||
|
return NS_ERROR_FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
nsRefPtr<FinalizationEvent> event = new FinalizationEvent(aTopic, aValue);
|
||||||
|
|
||||||
|
// Transfer ownership of the addrefed |event| to |objResult|.
|
||||||
|
JS_SetReservedSlot(objResult, WITNESS_SLOT_EVENT,
|
||||||
|
JS::PrivateValue(event.forget().get()));
|
||||||
|
|
||||||
|
aRetval->setObject(*objResult);
|
||||||
|
return NS_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace mozilla
|
|
@ -0,0 +1,26 @@
|
||||||
|
/* 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/. */
|
||||||
|
|
||||||
|
#ifndef mozilla_finalizationwitnessservice_h__
|
||||||
|
#define mozilla_finalizationwitnessservice_h__
|
||||||
|
|
||||||
|
#include "nsIFinalizationWitnessService.h"
|
||||||
|
|
||||||
|
namespace mozilla {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* XPConnect initializer, for use in the main thread.
|
||||||
|
*/
|
||||||
|
class FinalizationWitnessService MOZ_FINAL : public nsIFinalizationWitnessService
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
NS_DECL_ISUPPORTS
|
||||||
|
NS_DECL_NSIFINALIZATIONWITNESSSERVICE
|
||||||
|
private:
|
||||||
|
void operator=(const FinalizationWitnessService* other) MOZ_DELETE;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace mozilla
|
||||||
|
|
||||||
|
#endif // mozilla_finalizationwitnessservice_h__
|
|
@ -0,0 +1,28 @@
|
||||||
|
# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
|
||||||
|
# vim: set filetype=python:
|
||||||
|
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
MODULE = 'finalizationwitness'
|
||||||
|
|
||||||
|
CPP_SOURCES += [
|
||||||
|
'FinalizationWitnessService.cpp',
|
||||||
|
]
|
||||||
|
|
||||||
|
XPIDL_SOURCES += [
|
||||||
|
'nsIFinalizationWitnessService.idl',
|
||||||
|
]
|
||||||
|
|
||||||
|
XPIDL_MODULE = 'toolkit_finalizationwitness'
|
||||||
|
|
||||||
|
EXPORTS.mozilla += [
|
||||||
|
'FinalizationWitnessService.h',
|
||||||
|
]
|
||||||
|
|
||||||
|
LOCAL_INCLUDES += [
|
||||||
|
'/js/xpconnect/loader',
|
||||||
|
]
|
||||||
|
|
||||||
|
LIBRARY_NAME = 'finalizationwitness_s'
|
||||||
|
LIBXUL_LIBRARY = True
|
|
@ -0,0 +1,35 @@
|
||||||
|
/* -*- Mode: c++; c-basic-offset: 2; indent-tabs-mode: nil; tab-width: 40 -*- */
|
||||||
|
/* vim: set ts=2 et sw=2 tw=40: */
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
#include "nsISupports.idl"
|
||||||
|
|
||||||
|
|
||||||
|
[scriptable, uuid(15686f9d-483e-4361-98cd-37f1e8f1e61d)]
|
||||||
|
interface nsIFinalizationWitnessService: nsISupports
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Create a new Finalization Witness.
|
||||||
|
*
|
||||||
|
* A finalization witness is an object whose sole role is to
|
||||||
|
* broadcast when it is garbage-collected. Once the witness is
|
||||||
|
* created, call method its method |forget()| to prevent the
|
||||||
|
* broadcast.
|
||||||
|
*
|
||||||
|
* @param aTopic The topic that the witness will broadcast using
|
||||||
|
* Services.obs.
|
||||||
|
* @param aString The string that the witness will broadcast.
|
||||||
|
* @return An object with a single method |forget()|.
|
||||||
|
*/
|
||||||
|
[implicit_jscontext]
|
||||||
|
jsval make(in string aTopic, in wstring aString);
|
||||||
|
};
|
||||||
|
|
||||||
|
%{ C++
|
||||||
|
|
||||||
|
#define FINALIZATIONWITNESSSERVICE_CID {0x15686f9d,0x483e,0x4361,{0x98,0xcd,0x37,0xf1,0xe8,0xf1,0xe6,0x1d}}
|
||||||
|
#define FINALIZATIONWITNESSSERVICE_CONTRACTID "@mozilla.org/toolkit/finalizationwitness;1"
|
||||||
|
|
||||||
|
%}
|
|
@ -20,6 +20,7 @@ PARALLEL_DIRS += [
|
||||||
'downloads',
|
'downloads',
|
||||||
'exthelper',
|
'exthelper',
|
||||||
'filepicker',
|
'filepicker',
|
||||||
|
'finalizationwitness',
|
||||||
'find',
|
'find',
|
||||||
'intl',
|
'intl',
|
||||||
'jsdownloads',
|
'jsdownloads',
|
||||||
|
|
|
@ -98,48 +98,10 @@ if (!("localProfileDir" in OS.Constants.Path)) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* A global constant used as a default refs parameter value when cloning.
|
|
||||||
*/
|
|
||||||
const noRefs = [];
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return a shallow clone of the enumerable properties of an object.
|
* Return a shallow clone of the enumerable properties of an object.
|
||||||
*
|
|
||||||
* Utility used whenever normalizing options requires making (shallow)
|
|
||||||
* changes to an option object. The copy ensures that we do not modify
|
|
||||||
* a client-provided object by accident.
|
|
||||||
*
|
|
||||||
* Note: to reference and not copy specific fields, provide an optional
|
|
||||||
* |refs| argument containing their names.
|
|
||||||
*
|
|
||||||
* @param {JSON} object Options to be cloned.
|
|
||||||
* @param {Array} refs An optional array of field names to be passed by
|
|
||||||
* reference instead of copying.
|
|
||||||
*/
|
*/
|
||||||
let clone = function clone(object, refs = noRefs) {
|
let clone = SharedAll.clone;
|
||||||
let result = {};
|
|
||||||
// Make a reference between result[key] and object[key].
|
|
||||||
let refer = function refer(result, key, object) {
|
|
||||||
Object.defineProperty(result, key, {
|
|
||||||
enumerable: true,
|
|
||||||
get: function() {
|
|
||||||
return object[key];
|
|
||||||
},
|
|
||||||
set: function(value) {
|
|
||||||
object[key] = value;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
for (let k in object) {
|
|
||||||
if (refs.indexOf(k) < 0) {
|
|
||||||
result[k] = object[k];
|
|
||||||
} else {
|
|
||||||
refer(result, k, object);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
|
|
||||||
let worker = new PromiseWorker(
|
let worker = new PromiseWorker(
|
||||||
"resource://gre/modules/osfile/osfile_async_worker.js", LOG);
|
"resource://gre/modules/osfile/osfile_async_worker.js", LOG);
|
||||||
|
@ -421,9 +383,8 @@ File.prototype = {
|
||||||
// If |buffer| is a typed array and there is no |bytes| options, we
|
// If |buffer| is a typed array and there is no |bytes| options, we
|
||||||
// need to extract the |byteLength| now, as it will be lost by
|
// need to extract the |byteLength| now, as it will be lost by
|
||||||
// communication
|
// communication
|
||||||
if (isTypedArray(buffer) && (!options || !("bytes" in options))) {
|
if (isTypedArray(buffer) && !("bytes" in options)) {
|
||||||
// Preserve the reference to |outExecutionDuration| option if it is
|
// Preserve reference to option |outExecutionDuration|, if it is passed.
|
||||||
// passed.
|
|
||||||
options = clone(options, ["outExecutionDuration"]);
|
options = clone(options, ["outExecutionDuration"]);
|
||||||
options.bytes = buffer.byteLength;
|
options.bytes = buffer.byteLength;
|
||||||
}
|
}
|
||||||
|
@ -459,9 +420,8 @@ File.prototype = {
|
||||||
// If |buffer| is a typed array and there is no |bytes| options,
|
// If |buffer| is a typed array and there is no |bytes| options,
|
||||||
// we need to extract the |byteLength| now, as it will be lost
|
// we need to extract the |byteLength| now, as it will be lost
|
||||||
// by communication
|
// by communication
|
||||||
if (isTypedArray(buffer) && (!options || !("bytes" in options))) {
|
if (isTypedArray(buffer)) {
|
||||||
// Preserve the reference to |outExecutionDuration| option if it is
|
// Preserve reference to option |outExecutionDuration|, if it is passed.
|
||||||
// passed.
|
|
||||||
options = clone(options, ["outExecutionDuration"]);
|
options = clone(options, ["outExecutionDuration"]);
|
||||||
options.bytes = buffer.byteLength;
|
options.bytes = buffer.byteLength;
|
||||||
}
|
}
|
||||||
|
@ -482,13 +442,14 @@ File.prototype = {
|
||||||
* @param {number=} bytes If unspecified, read all the remaining bytes from
|
* @param {number=} bytes If unspecified, read all the remaining bytes from
|
||||||
* this file. If specified, read |bytes| bytes, or less if the file does not
|
* this file. If specified, read |bytes| bytes, or less if the file does not
|
||||||
* contain that many bytes.
|
* contain that many bytes.
|
||||||
|
* @param {JSON} options
|
||||||
* @return {promise}
|
* @return {promise}
|
||||||
* @resolves {Uint8Array} An array containing the bytes read.
|
* @resolves {Uint8Array} An array containing the bytes read.
|
||||||
*/
|
*/
|
||||||
read: function read(nbytes) {
|
read: function read(nbytes, options = {}) {
|
||||||
let promise = Scheduler.post("File_prototype_read",
|
let promise = Scheduler.post("File_prototype_read",
|
||||||
[this._fdmsg,
|
[this._fdmsg,
|
||||||
nbytes]);
|
nbytes, options]);
|
||||||
return promise.then(
|
return promise.then(
|
||||||
function onSuccess(data) {
|
function onSuccess(data) {
|
||||||
return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
|
return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
|
||||||
|
@ -693,6 +654,11 @@ File.makeDir = function makeDir(path, options) {
|
||||||
* @param {number=} bytes Optionally, an upper bound to the number of bytes
|
* @param {number=} bytes Optionally, an upper bound to the number of bytes
|
||||||
* to read.
|
* to read.
|
||||||
* @param {JSON} options Additional options.
|
* @param {JSON} options Additional options.
|
||||||
|
* - {boolean} sequential A flag that triggers a population of the page cache
|
||||||
|
* with data from a file so that subsequent reads from that file would not
|
||||||
|
* block on disk I/O. If |true| or unspecified, inform the system that the
|
||||||
|
* contents of the file will be read in order. Otherwise, make no such
|
||||||
|
* assumption. |true| by default.
|
||||||
*
|
*
|
||||||
* @resolves {Uint8Array} A buffer holding the bytes
|
* @resolves {Uint8Array} A buffer holding the bytes
|
||||||
* read from the file.
|
* read from the file.
|
||||||
|
|
|
@ -271,7 +271,7 @@ if (this.Components) {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
read: function read(path, bytes, options) {
|
read: function read(path, bytes, options) {
|
||||||
let data = File.read(Type.path.fromMsg(path), bytes);
|
let data = File.read(Type.path.fromMsg(path), bytes, options);
|
||||||
return new Transfer({buffer: data.buffer, byteOffset: data.byteOffset, byteLength: data.byteLength}, [data.buffer]);
|
return new Transfer({buffer: data.buffer, byteOffset: data.byteOffset, byteLength: data.byteLength}, [data.buffer]);
|
||||||
},
|
},
|
||||||
exists: function exists(path) {
|
exists: function exists(path) {
|
||||||
|
|
|
@ -34,6 +34,7 @@ if (typeof Components != "undefined") {
|
||||||
|
|
||||||
let EXPORTED_SYMBOLS = [
|
let EXPORTED_SYMBOLS = [
|
||||||
"LOG",
|
"LOG",
|
||||||
|
"clone",
|
||||||
"Config",
|
"Config",
|
||||||
"Constants",
|
"Constants",
|
||||||
"Type",
|
"Type",
|
||||||
|
@ -162,6 +163,46 @@ let LOG = function (...args) {
|
||||||
|
|
||||||
exports.LOG = LOG;
|
exports.LOG = LOG;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a shallow clone of the enumerable properties of an object.
|
||||||
|
*
|
||||||
|
* Utility used whenever normalizing options requires making (shallow)
|
||||||
|
* changes to an option object. The copy ensures that we do not modify
|
||||||
|
* a client-provided object by accident.
|
||||||
|
*
|
||||||
|
* Note: to reference and not copy specific fields, provide an optional
|
||||||
|
* |refs| argument containing their names.
|
||||||
|
*
|
||||||
|
* @param {JSON} object Options to be cloned.
|
||||||
|
* @param {Array} refs An optional array of field names to be passed by
|
||||||
|
* reference instead of copying.
|
||||||
|
*/
|
||||||
|
let clone = function (object, refs = []) {
|
||||||
|
let result = {};
|
||||||
|
// Make a reference between result[key] and object[key].
|
||||||
|
let refer = function refer(result, key, object) {
|
||||||
|
Object.defineProperty(result, key, {
|
||||||
|
enumerable: true,
|
||||||
|
get: function() {
|
||||||
|
return object[key];
|
||||||
|
},
|
||||||
|
set: function(value) {
|
||||||
|
object[key] = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
for (let k in object) {
|
||||||
|
if (refs.indexOf(k) < 0) {
|
||||||
|
result[k] = object[k];
|
||||||
|
} else {
|
||||||
|
refer(result, k, object);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.clone = clone;
|
||||||
|
|
||||||
///////////////////// Abstractions above js-ctypes
|
///////////////////// Abstractions above js-ctypes
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -974,6 +1015,7 @@ exports.OS = {
|
||||||
Constants: exports.Constants,
|
Constants: exports.Constants,
|
||||||
Shared: {
|
Shared: {
|
||||||
LOG: LOG,
|
LOG: LOG,
|
||||||
|
clone: clone,
|
||||||
Type: Type,
|
Type: Type,
|
||||||
HollowStructure: HollowStructure,
|
HollowStructure: HollowStructure,
|
||||||
Error: OSError,
|
Error: OSError,
|
||||||
|
@ -1015,4 +1057,3 @@ if (typeof Components != "undefined") {
|
||||||
this[symbol] = exports[symbol];
|
this[symbol] = exports[symbol];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,7 @@ if (typeof Components != "undefined") {
|
||||||
exports.OS = require("resource://gre/modules/osfile/osfile_shared_allthreads.jsm").OS;
|
exports.OS = require("resource://gre/modules/osfile/osfile_shared_allthreads.jsm").OS;
|
||||||
|
|
||||||
let LOG = exports.OS.Shared.LOG.bind(OS.Shared, "Shared front-end");
|
let LOG = exports.OS.Shared.LOG.bind(OS.Shared, "Shared front-end");
|
||||||
|
let clone = exports.OS.Shared.clone;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Code shared by implementations of File.
|
* Code shared by implementations of File.
|
||||||
|
@ -44,17 +45,17 @@ AbstractFile.prototype = {
|
||||||
* Read bytes from this file to a new buffer.
|
* Read bytes from this file to a new buffer.
|
||||||
*
|
*
|
||||||
* @param {number=} bytes If unspecified, read all the remaining bytes from
|
* @param {number=} bytes If unspecified, read all the remaining bytes from
|
||||||
* this file. If specified, read |bytes| bytes, or less if the file does not
|
* this file. If specified, read |bytes| bytes, or less if the file does notclone
|
||||||
* contain that many bytes.
|
* contain that many bytes.
|
||||||
|
* @param {JSON} options
|
||||||
* @return {Uint8Array} An array containing the bytes read.
|
* @return {Uint8Array} An array containing the bytes read.
|
||||||
*/
|
*/
|
||||||
read: function read(bytes) {
|
read: function read(bytes, options = {}) {
|
||||||
if (bytes == null) {
|
options = clone(options);
|
||||||
bytes = this.stat().size;
|
options.bytes = bytes == null ? this.stat().size : bytes;
|
||||||
}
|
let buffer = new Uint8Array(options.bytes);
|
||||||
let buffer = new Uint8Array(bytes);
|
let size = this.readTo(buffer, options);
|
||||||
let size = this.readTo(buffer, {bytes: bytes});
|
if (size == options.bytes) {
|
||||||
if (size == bytes) {
|
|
||||||
return buffer;
|
return buffer;
|
||||||
} else {
|
} else {
|
||||||
return buffer.subarray(0, size);
|
return buffer.subarray(0, size);
|
||||||
|
@ -292,14 +293,15 @@ AbstractFile.normalizeOpenMode = function normalizeOpenMode(mode) {
|
||||||
* @param {string} path The path to the file.
|
* @param {string} path The path to the file.
|
||||||
* @param {number=} bytes Optionally, an upper bound to the number of bytes
|
* @param {number=} bytes Optionally, an upper bound to the number of bytes
|
||||||
* to read.
|
* to read.
|
||||||
|
* @param {JSON} options Optionally contains additional options.
|
||||||
*
|
*
|
||||||
* @return {Uint8Array} A buffer holding the bytes
|
* @return {Uint8Array} A buffer holding the bytes
|
||||||
* and the number of bytes read from the file.
|
* and the number of bytes read from the file.
|
||||||
*/
|
*/
|
||||||
AbstractFile.read = function read(path, bytes) {
|
AbstractFile.read = function read(path, bytes, options = {}) {
|
||||||
let file = exports.OS.File.open(path);
|
let file = exports.OS.File.open(path);
|
||||||
try {
|
try {
|
||||||
return file.read(bytes);
|
return file.read(bytes, options);
|
||||||
} finally {
|
} finally {
|
||||||
file.close();
|
file.close();
|
||||||
}
|
}
|
||||||
|
|
|
@ -447,6 +447,14 @@
|
||||||
/*buf*/ Types.void_t.out_ptr,
|
/*buf*/ Types.void_t.out_ptr,
|
||||||
/*nbytes*/Types.size_t);
|
/*nbytes*/Types.size_t);
|
||||||
|
|
||||||
|
UnixFile.posix_fadvise =
|
||||||
|
declareFFI("posix_fadvise", ctypes.default_abi,
|
||||||
|
/*return*/ Types.int,
|
||||||
|
/*fd*/ Types.fd,
|
||||||
|
/*offset*/ Types.off_t,
|
||||||
|
/*len*/ Types.off_t,
|
||||||
|
/*advise*/ Types.int);
|
||||||
|
|
||||||
if (OS.Constants.libc._DARWIN_FEATURE_64_BIT_INODE) {
|
if (OS.Constants.libc._DARWIN_FEATURE_64_BIT_INODE) {
|
||||||
// Special case for MacOS X 10.5+
|
// Special case for MacOS X 10.5+
|
||||||
// Symbol name "readdir" still exists but is used for a
|
// Symbol name "readdir" still exists but is used for a
|
||||||
|
|
|
@ -94,6 +94,13 @@
|
||||||
* @throws {OS.File.Error} In case of I/O error.
|
* @throws {OS.File.Error} In case of I/O error.
|
||||||
*/
|
*/
|
||||||
File.prototype._read = function _read(buffer, nbytes, options) {
|
File.prototype._read = function _read(buffer, nbytes, options) {
|
||||||
|
// Populate the page cache with data from a file so the subsequent reads
|
||||||
|
// from that file will not block on disk I/O.
|
||||||
|
if (typeof(UnixFile.posix_fadvise) === 'function' &&
|
||||||
|
(options.sequential || !("sequential" in options))) {
|
||||||
|
UnixFile.posix_fadvise(this.fd, 0, nbytes,
|
||||||
|
OS.Constants.libc.POSIX_FADV_SEQUENTIAL);
|
||||||
|
}
|
||||||
return throw_on_negative("read",
|
return throw_on_negative("read",
|
||||||
UnixFile.read(this.fd, buffer, nbytes)
|
UnixFile.read(this.fd, buffer, nbytes)
|
||||||
);
|
);
|
||||||
|
|
|
@ -98,6 +98,7 @@ const Cu = Components.utils;
|
||||||
const Cr = Components.results;
|
const Cr = Components.results;
|
||||||
|
|
||||||
Cu.import("resource://gre/modules/Services.jsm");
|
Cu.import("resource://gre/modules/Services.jsm");
|
||||||
|
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||||
|
|
||||||
const STATUS_PENDING = 0;
|
const STATUS_PENDING = 0;
|
||||||
const STATUS_RESOLVED = 1;
|
const STATUS_RESOLVED = 1;
|
||||||
|
@ -113,7 +114,128 @@ const Name = (n) => "{private:" + n + ":" + salt + "}";
|
||||||
const N_STATUS = Name("status");
|
const N_STATUS = Name("status");
|
||||||
const N_VALUE = Name("value");
|
const N_VALUE = Name("value");
|
||||||
const N_HANDLERS = Name("handlers");
|
const N_HANDLERS = Name("handlers");
|
||||||
|
const N_WITNESS = Name("witness");
|
||||||
|
|
||||||
|
|
||||||
|
/////// Warn-upon-finalization mechanism
|
||||||
|
//
|
||||||
|
// One of the difficult problems with promises is locating uncaught
|
||||||
|
// rejections. We adopt the following strategy: if a promise is rejected
|
||||||
|
// at the time of its garbage-collection *and* if the promise is at the
|
||||||
|
// end of a promise chain (i.e. |thatPromise.then| has never been
|
||||||
|
// called), then we print a warning.
|
||||||
|
//
|
||||||
|
// let deferred = Promise.defer();
|
||||||
|
// let p = deferred.promise.then();
|
||||||
|
// deferred.reject(new Error("I am un uncaught error"));
|
||||||
|
// deferred = null;
|
||||||
|
// p = null;
|
||||||
|
//
|
||||||
|
// In this snippet, since |deferred.promise| is not the last in the
|
||||||
|
// chain, no error will be reported for that promise. However, since
|
||||||
|
// |p| is the last promise in the chain, the error will be reported
|
||||||
|
// for |p|.
|
||||||
|
//
|
||||||
|
// Note that this may, in some cases, cause an error to be reported more
|
||||||
|
// than once. For instance, consider:
|
||||||
|
//
|
||||||
|
// let deferred = Promise.defer();
|
||||||
|
// let p1 = deferred.promise.then();
|
||||||
|
// let p2 = deferred.promise.then();
|
||||||
|
// deferred.reject(new Error("I am an uncaught error"));
|
||||||
|
// p1 = p2 = deferred = null;
|
||||||
|
//
|
||||||
|
// In this snippet, the error is reported both by p1 and by p2.
|
||||||
|
//
|
||||||
|
|
||||||
|
XPCOMUtils.defineLazyServiceGetter(this, "FinalizationWitnessService",
|
||||||
|
"@mozilla.org/toolkit/finalizationwitness;1",
|
||||||
|
"nsIFinalizationWitnessService");
|
||||||
|
|
||||||
|
let PendingErrors = {
|
||||||
|
_counter: 0,
|
||||||
|
_map: new Map(),
|
||||||
|
register: function(error) {
|
||||||
|
let id = "pending-error-" + (this._counter++);
|
||||||
|
//
|
||||||
|
// At this stage, ideally, we would like to store the error itself
|
||||||
|
// and delay any treatment until we are certain that we will need
|
||||||
|
// to report that error. However, in the (unlikely but possible)
|
||||||
|
// case the error holds a reference to the promise itself, doing so
|
||||||
|
// would prevent the promise from being garbabe-collected, which
|
||||||
|
// would both cause a memory leak and ensure that we cannot report
|
||||||
|
// the uncaught error.
|
||||||
|
//
|
||||||
|
// To avoid this situation, we rather extract relevant data from
|
||||||
|
// the error and further normalize it to strings.
|
||||||
|
//
|
||||||
|
let value = {
|
||||||
|
date: new Date(),
|
||||||
|
message: "" + error,
|
||||||
|
fileName: null,
|
||||||
|
stack: null,
|
||||||
|
lineNumber: null
|
||||||
|
};
|
||||||
|
try { // Defend against non-enumerable values
|
||||||
|
if (typeof error == "object" && error) {
|
||||||
|
for (let k of ["fileName", "stack", "lineNumber"]) {
|
||||||
|
try { // Defend against fallible getters and string conversions
|
||||||
|
let v = error[k];
|
||||||
|
value[k] = v ? ("" + v):null;
|
||||||
|
} catch (ex) {
|
||||||
|
// Ignore field
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (ex) {
|
||||||
|
// Ignore value
|
||||||
|
}
|
||||||
|
this._map.set(id, value);
|
||||||
|
return id;
|
||||||
|
},
|
||||||
|
extract: function(id) {
|
||||||
|
let value = this._map.get(id);
|
||||||
|
this._map.delete(id);
|
||||||
|
return value;
|
||||||
|
},
|
||||||
|
unregister: function(id) {
|
||||||
|
this._map.delete(id);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Actually print the finalization warning.
|
||||||
|
Services.obs.addObserver(function observe(aSubject, aTopic, aValue) {
|
||||||
|
let error = PendingErrors.extract(aValue);
|
||||||
|
let {message, date, fileName, stack, lineNumber} = error;
|
||||||
|
let error = Cc['@mozilla.org/scripterror;1'].createInstance(Ci.nsIScriptError);
|
||||||
|
if (!error || !Services.console) {
|
||||||
|
// Too late during shutdown to use the nsIConsole
|
||||||
|
dump("*************************\n");
|
||||||
|
dump("A promise chain failed to handle a rejection\n\n");
|
||||||
|
dump("On: " + date + "\n");
|
||||||
|
dump("Full message: " + message + "\n");
|
||||||
|
dump("See https://developer.mozilla.org/Mozilla/JavaScript_code_modules/Promise.jsm/Promise\n");
|
||||||
|
dump("Full stack: " + (stack||"not available") + "\n");
|
||||||
|
dump("*************************\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (stack) {
|
||||||
|
message += " at " + stack;
|
||||||
|
}
|
||||||
|
error.init(
|
||||||
|
/*message*/"A promise chain failed to handle a rejection: on " +
|
||||||
|
date + ", " + message,
|
||||||
|
/*sourceName*/ fileName,
|
||||||
|
/*sourceLine*/ lineNumber?("" + lineNumber):0,
|
||||||
|
/*lineNumber*/ lineNumber || 0,
|
||||||
|
/*columnNumber*/ 0,
|
||||||
|
/*flags*/ Ci.nsIScriptError.errorFlag,
|
||||||
|
/*category*/ "chrome javascript");
|
||||||
|
Services.console.logMessage(error);
|
||||||
|
}, "promise-finalization-witness", false);
|
||||||
|
|
||||||
|
///////// Additional warnings for developers
|
||||||
|
//
|
||||||
// The following error types are considered programmer errors, which should be
|
// The following error types are considered programmer errors, which should be
|
||||||
// reported (possibly redundantly) so as to let programmers fix their code.
|
// reported (possibly redundantly) so as to let programmers fix their code.
|
||||||
const ERRORS_TO_REPORT = ["EvalError", "RangeError", "ReferenceError", "TypeError"];
|
const ERRORS_TO_REPORT = ["EvalError", "RangeError", "ReferenceError", "TypeError"];
|
||||||
|
@ -283,6 +405,13 @@ this.PromiseWalker = {
|
||||||
aPromise[N_VALUE] = aValue;
|
aPromise[N_VALUE] = aValue;
|
||||||
if (aPromise[N_HANDLERS].length > 0) {
|
if (aPromise[N_HANDLERS].length > 0) {
|
||||||
this.schedulePromise(aPromise);
|
this.schedulePromise(aPromise);
|
||||||
|
} else if (aStatus == STATUS_REJECTED) {
|
||||||
|
// This is a rejection and the promise is the last in the chain.
|
||||||
|
// For the time being we therefore have an uncaught error.
|
||||||
|
let id = PendingErrors.register(aValue);
|
||||||
|
let witness =
|
||||||
|
FinalizationWitnessService.make("promise-finalization-witness", id);
|
||||||
|
aPromise[N_WITNESS] = [id, witness];
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -456,6 +585,15 @@ function PromiseImpl()
|
||||||
*/
|
*/
|
||||||
Object.defineProperty(this, N_HANDLERS, { value: [] });
|
Object.defineProperty(this, N_HANDLERS, { value: [] });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When the N_STATUS property is STATUS_REJECTED and until there is
|
||||||
|
* a rejection callback, this contains an array
|
||||||
|
* - {string} id An id for use with |PendingErrors|;
|
||||||
|
* - {FinalizationWitness} witness A witness broadcasting |id| on
|
||||||
|
* notification "promise-finalization-witness".
|
||||||
|
*/
|
||||||
|
Object.defineProperty(this, N_WITNESS, { writable: true });
|
||||||
|
|
||||||
Object.seal(this);
|
Object.seal(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -511,6 +649,15 @@ PromiseImpl.prototype = {
|
||||||
// Ensure the handler is scheduled for processing if this promise is already
|
// Ensure the handler is scheduled for processing if this promise is already
|
||||||
// resolved or rejected.
|
// resolved or rejected.
|
||||||
if (this[N_STATUS] != STATUS_PENDING) {
|
if (this[N_STATUS] != STATUS_PENDING) {
|
||||||
|
|
||||||
|
// This promise is not the last in the chain anymore. Remove any watchdog.
|
||||||
|
if (this[N_WITNESS] != null) {
|
||||||
|
let [id, witness] = this[N_WITNESS];
|
||||||
|
this[N_WITNESS] = null;
|
||||||
|
witness.forget();
|
||||||
|
PendingErrors.unregister(id);
|
||||||
|
}
|
||||||
|
|
||||||
PromiseWalker.schedulePromise(this);
|
PromiseWalker.schedulePromise(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -588,12 +735,15 @@ Handler.prototype = {
|
||||||
// users to see it. Also, if the programmer handles errors correctly,
|
// users to see it. Also, if the programmer handles errors correctly,
|
||||||
// they will either treat the error or log them somewhere.
|
// they will either treat the error or log them somewhere.
|
||||||
|
|
||||||
|
dump("*************************\n");
|
||||||
dump("A coding exception was thrown in a Promise " +
|
dump("A coding exception was thrown in a Promise " +
|
||||||
((nextStatus == STATUS_RESOLVED) ? "resolution":"rejection") +
|
((nextStatus == STATUS_RESOLVED) ? "resolution":"rejection") +
|
||||||
" callback.\n");
|
" callback.\n\n");
|
||||||
dump("Full message: " + ex + "\n");
|
dump("Full message: " + ex + "\n");
|
||||||
dump("See https://developer.mozilla.org/Mozilla/JavaScript_code_modules/Promise.jsm/Promise\n");
|
dump("See https://developer.mozilla.org/Mozilla/JavaScript_code_modules/Promise.jsm/Promise\n");
|
||||||
dump("Full stack: " + (("stack" in ex)?ex.stack:"not available") + "\n");
|
dump("Full stack: " + (("stack" in ex)?ex.stack:"not available") + "\n");
|
||||||
|
dump("*************************\n");
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Additionally, reject the next promise.
|
// Additionally, reject the next promise.
|
||||||
|
|
|
@ -291,9 +291,11 @@ TaskImpl.prototype = {
|
||||||
// they will either treat the error or log them somewhere.
|
// they will either treat the error or log them somewhere.
|
||||||
|
|
||||||
let stack = ("stack" in aException) ? aException.stack : "not available";
|
let stack = ("stack" in aException) ? aException.stack : "not available";
|
||||||
dump("A coding exception was thrown and uncaught in a Task.\n");
|
dump("*************************\n");
|
||||||
|
dump("A coding exception was thrown and uncaught in a Task.\n\n");
|
||||||
dump("Full message: " + aException + "\n");
|
dump("Full message: " + aException + "\n");
|
||||||
dump("Full stack: " + stack + "\n");
|
dump("Full stack: " + stack + "\n");
|
||||||
|
dump("*************************\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
this.deferred.reject(aException);
|
this.deferred.reject(aException);
|
||||||
|
|
|
@ -33,7 +33,7 @@ let run_promise_tests = function run_promise_tests(tests, cb) {
|
||||||
|
|
||||||
let make_promise_test = function(test) {
|
let make_promise_test = function(test) {
|
||||||
return function runtest() {
|
return function runtest() {
|
||||||
do_print("Test starting: " + test);
|
do_print("Test starting: " + test.name);
|
||||||
try {
|
try {
|
||||||
let result = test();
|
let result = test();
|
||||||
if (result && "promise" in result) {
|
if (result && "promise" in result) {
|
||||||
|
@ -42,7 +42,7 @@ let make_promise_test = function(test) {
|
||||||
if (!result || !("then" in result)) {
|
if (!result || !("then" in result)) {
|
||||||
let exn;
|
let exn;
|
||||||
try {
|
try {
|
||||||
do_throw("Test " + test + " did not return a promise: " + result);
|
do_throw("Test " + test.name + " did not return a promise: " + result);
|
||||||
} catch (x) {
|
} catch (x) {
|
||||||
exn = x;
|
exn = x;
|
||||||
}
|
}
|
||||||
|
@ -52,7 +52,7 @@ let make_promise_test = function(test) {
|
||||||
result = result.then(
|
result = result.then(
|
||||||
// Test complete
|
// Test complete
|
||||||
function onResolve() {
|
function onResolve() {
|
||||||
do_print("Test complete: " + test);
|
do_print("Test complete: " + test.name);
|
||||||
},
|
},
|
||||||
// The test failed with an unexpected error
|
// The test failed with an unexpected error
|
||||||
function onReject(err) {
|
function onReject(err) {
|
||||||
|
@ -62,13 +62,13 @@ let make_promise_test = function(test) {
|
||||||
} else {
|
} else {
|
||||||
detail = "(no stack)";
|
detail = "(no stack)";
|
||||||
}
|
}
|
||||||
do_throw("Test " + test + " rejected with the following reason: "
|
do_throw("Test " + test.name + " rejected with the following reason: "
|
||||||
+ err + detail);
|
+ err + detail);
|
||||||
});
|
});
|
||||||
return result;
|
return result;
|
||||||
} catch (x) {
|
} catch (x) {
|
||||||
// The test failed because of an error outside of a promise
|
// The test failed because of an error outside of a promise
|
||||||
do_throw("Error in body of test " + test + ": " + x + " at " + x.stack);
|
do_throw("Error in body of test " + test.name + ": " + x + " at " + x.stack);
|
||||||
return Promise.reject();
|
return Promise.reject();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -714,6 +714,124 @@ tests.push(
|
||||||
});
|
});
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
function wait_for_uncaught(aMustAppear, aTimeout = undefined) {
|
||||||
|
let remaining = new Set();
|
||||||
|
for (let k of aMustAppear) {
|
||||||
|
remaining.add(k);
|
||||||
|
}
|
||||||
|
let deferred = Promise.defer();
|
||||||
|
let print = do_print;
|
||||||
|
let execute_soon = do_execute_soon;
|
||||||
|
let observer = function(aMessage) {
|
||||||
|
execute_soon(function() {
|
||||||
|
let message = aMessage.message;
|
||||||
|
print("Observing " + message);
|
||||||
|
for (let expected of remaining) {
|
||||||
|
if (message.indexOf(expected) != -1) {
|
||||||
|
print("I found " + expected);
|
||||||
|
remaining.delete(expected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (remaining.size == 0 && observer) {
|
||||||
|
Services.console.unregisterListener(observer);
|
||||||
|
observer = null;
|
||||||
|
deferred.resolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
Services.console.registerListener(observer);
|
||||||
|
if (aTimeout) {
|
||||||
|
do_timeout(aTimeout, function timeout() {
|
||||||
|
if (observer) {
|
||||||
|
Services.console.unregisterListener(observer);
|
||||||
|
observer = null;
|
||||||
|
}
|
||||||
|
deferred.reject(new Error("Timeout"));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return deferred.promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test that uncaught errors are reported as uncaught
|
||||||
|
(function() {
|
||||||
|
let make_string_rejection = function make_string_rejection() {
|
||||||
|
let salt = (Math.random() * ( Math.pow(2, 24) - 1 ));
|
||||||
|
let string = "This is an uncaught rejection " + salt;
|
||||||
|
return {mustFind: [string], error: string};
|
||||||
|
};
|
||||||
|
let make_num_rejection = function make_num_rejection() {
|
||||||
|
let salt = (Math.random() * ( Math.pow(2, 24) - 1 ));
|
||||||
|
return {mustFind: [salt], error: salt};
|
||||||
|
};
|
||||||
|
let make_undefined_rejection = function make_undefined_rejection() {
|
||||||
|
return {mustFind: [], error: undefined};
|
||||||
|
};
|
||||||
|
let make_error_rejection = function make_error_rejection() {
|
||||||
|
let salt = (Math.random() * ( Math.pow(2, 24) - 1 ));
|
||||||
|
let error = new Error("This is an uncaught error " + salt);
|
||||||
|
return {
|
||||||
|
mustFind: [error.message, error.fileName, error.lineNumber, error.stack],
|
||||||
|
error: error
|
||||||
|
};
|
||||||
|
};
|
||||||
|
for (let make_rejection of [make_string_rejection,
|
||||||
|
make_num_rejection,
|
||||||
|
make_undefined_rejection,
|
||||||
|
make_error_rejection]) {
|
||||||
|
let {mustFind, error} = make_rejection();
|
||||||
|
let name = make_rejection.name;
|
||||||
|
tests.push(make_promise_test(function test_uncaught_is_reported() {
|
||||||
|
do_print("Testing with rejection " + name);
|
||||||
|
let promise = wait_for_uncaught(mustFind);
|
||||||
|
(function() {
|
||||||
|
// For the moment, we cannot be absolutely certain that a value is
|
||||||
|
// garbage-collected, even if it is not referenced anymore, due to
|
||||||
|
// the conservative stack-scanning algorithm.
|
||||||
|
//
|
||||||
|
// To be _almost_ certain that a value will be garbage-collected, we
|
||||||
|
// 1. isolate that value in an anonymous closure;
|
||||||
|
// 2. allocate 100 values instead of 1 (gc-ing a single value from
|
||||||
|
// these is sufficient for the test);
|
||||||
|
// 3. place everything in a loop, as the JIT typically reuses memory;
|
||||||
|
// 4. call all the GC methods we can.
|
||||||
|
//
|
||||||
|
// Unfortunately, we might still have intermittent failures,
|
||||||
|
// materialized as timeouts.
|
||||||
|
//
|
||||||
|
for (let i = 0; i < 100; ++i) {
|
||||||
|
Promise.reject(error);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
Components.utils.forceGC();
|
||||||
|
Components.utils.forceCC();
|
||||||
|
Components.utils.forceShrinkingGC();
|
||||||
|
return promise;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
|
||||||
|
// Test that caught errors are not reported as uncaught
|
||||||
|
tests.push(
|
||||||
|
make_promise_test(function test_caught_is_not_reported() {
|
||||||
|
let salt = (Math.random() * ( Math.pow(2, 24) - 1 ));
|
||||||
|
let promise = wait_for_uncaught([salt], 500);
|
||||||
|
(function() {
|
||||||
|
let uncaught = Promise.reject("This error, on the other hand, is caught " + salt);
|
||||||
|
uncaught.then(null, function() { /* ignore rejection */});
|
||||||
|
uncaught = null;
|
||||||
|
})();
|
||||||
|
// Isolate this in a function to increase likelihood that the gc will
|
||||||
|
// realise that |uncaught| has remained uncaught.
|
||||||
|
Components.utils.forceGC();
|
||||||
|
|
||||||
|
return promise.then(function onSuccess() {
|
||||||
|
throw new Error("This error was caught and should not have been reported");
|
||||||
|
}, function onError() {
|
||||||
|
do_print("The caught error was not reported, all is fine");
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}));
|
||||||
|
|
||||||
function run_test()
|
function run_test()
|
||||||
{
|
{
|
||||||
|
|
|
@ -20,3 +20,4 @@ support-files =
|
||||||
[test_task.js]
|
[test_task.js]
|
||||||
[test_TelemetryTimestamps.js]
|
[test_TelemetryTimestamps.js]
|
||||||
[test_timer.js]
|
[test_timer.js]
|
||||||
|
|
||||||
|
|
Загрузка…
Ссылка в новой задаче