зеркало из 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/storage.xpt
|
||||
@BINPATH@/components/telemetry.xpt
|
||||
@BINPATH@/components/toolkit_finalizationwitness.xpt
|
||||
@BINPATH@/components/toolkitprofile.xpt
|
||||
#ifdef MOZ_ENABLE_XREMOTE
|
||||
@BINPATH@/components/toolkitremote.xpt
|
||||
|
|
|
@ -685,6 +685,11 @@ SourcesView.prototype = Heritage.extend(WidgetMethods, {
|
|||
}
|
||||
// The container is not empty and an actual item was selected.
|
||||
DebuggerView.setEditorLocation(sourceItem.value);
|
||||
|
||||
// Set window title.
|
||||
let script = sourceItem.value.split(" -> ").pop();
|
||||
document.title = L10N.getFormatStr("DebuggerWindowScriptTitle", script);
|
||||
|
||||
this.maybeShowBlackBoxMessage();
|
||||
},
|
||||
|
||||
|
|
|
@ -68,6 +68,7 @@ let DebuggerView = {
|
|||
this.GlobalSearch.initialize();
|
||||
this._initializeVariablesView();
|
||||
this._initializeEditor(deferred.resolve);
|
||||
document.title = L10N.getStr("DebuggerWindowTitle");
|
||||
|
||||
return deferred.promise;
|
||||
},
|
||||
|
|
|
@ -19,6 +19,9 @@ function test() {
|
|||
gEditor = gDebugger.DebuggerView.editor;
|
||||
gSources = gDebugger.DebuggerView.Sources;
|
||||
|
||||
ok(gDebugger.document.title.endsWith(EXAMPLE_URL + gLabel1),
|
||||
"Title with first source is correct.");
|
||||
|
||||
waitForSourceAndCaretAndScopes(gPanel, "-02.js", 6)
|
||||
.then(testSourcesDisplay)
|
||||
.then(testSwitchPaused1)
|
||||
|
@ -64,6 +67,9 @@ function testSourcesDisplay() {
|
|||
is(gEditor.getText().search(/debugger/), 172,
|
||||
"The second source is displayed.");
|
||||
|
||||
ok(gDebugger.document.title.endsWith(EXAMPLE_URL + gLabel2),
|
||||
"Title with second source is correct.");
|
||||
|
||||
ok(isCaretPos(gPanel, 6),
|
||||
"Editor caret location is correct.");
|
||||
|
||||
|
|
|
@ -305,6 +305,7 @@
|
|||
@BINPATH@/components/shistory.xpt
|
||||
@BINPATH@/components/spellchecker.xpt
|
||||
@BINPATH@/components/storage.xpt
|
||||
@BINPATH@/components/toolkit_finalizationwitness.xpt
|
||||
@BINPATH@/components/toolkitprofile.xpt
|
||||
#ifdef MOZ_ENABLE_XREMOTE
|
||||
@BINPATH@/components/toolkitremote.xpt
|
||||
|
|
|
@ -15,6 +15,14 @@
|
|||
# displayed inside the developer tools window and in the Developer Tools Menu.
|
||||
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):
|
||||
# This string is displayed in the tooltip of the tab when the debugger is
|
||||
# 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']++;
|
||||
}
|
||||
|
||||
var domBranch;
|
||||
var oldPrefVal;
|
||||
|
||||
var gEventSourceObj1 = null, gEventSourceObj1_e, gEventSourceObj1_f;
|
||||
var gEventSourceObj2 = 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) {
|
||||
// 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;
|
||||
xhr.open("GET", "/dynamic/getMyDirectory.sjs", false);
|
||||
xhr.send();
|
||||
|
@ -462,7 +465,7 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=338583
|
|||
function doTest5_c(test_id)
|
||||
{
|
||||
// credentials using the auth cache and cookies
|
||||
var xhr = SpecialPowers.createSystemXHR();
|
||||
var xhr = new XMLHttpRequest({mozAnon: false, mozSystem: true});
|
||||
xhr.withCredentials = true;
|
||||
// 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");
|
||||
|
@ -491,7 +494,7 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=338583
|
|||
|
||||
function doTest5_d(test_id)
|
||||
{
|
||||
var xhr = SpecialPowers.createSystemXHR();
|
||||
var xhr = new XMLHttpRequest({mozAnon: false, mozSystem: 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.send();
|
||||
|
@ -519,7 +522,7 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=338583
|
|||
function doTest5_e(test_id)
|
||||
{
|
||||
// credentials using the auth cache and cookies
|
||||
var xhr = SpecialPowers.createSystemXHR();
|
||||
var xhr = new XMLHttpRequest({mozAnon: false, mozSystem: 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.send();
|
||||
|
@ -547,7 +550,7 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=338583
|
|||
|
||||
function doTest5_f(test_id)
|
||||
{
|
||||
var xhr = SpecialPowers.createSystemXHR();
|
||||
var xhr = new XMLHttpRequest({mozAnon: false, mozSystem: 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.send();
|
||||
|
@ -614,7 +617,6 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=338583
|
|||
gEventSourceObj7.msg_received[1] == "delayed1" &&
|
||||
gEventSourceObj7.msg_received[2] == "delayed2", "Test 7 failed");
|
||||
|
||||
SpecialPowers.setBoolPref("dom.server-events.enabled", oldPrefVal);
|
||||
document.getElementById('waitSpan').innerHTML = '';
|
||||
setTestHasFinished(test_id);
|
||||
}, parseInt(8000*stress_factor));
|
||||
|
@ -623,13 +625,11 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=338583
|
|||
function doTest()
|
||||
{
|
||||
// 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()
|
||||
{
|
||||
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
|
||||
// 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";
|
||||
|
||||
var req = SpecialPowers.createSystemXHR();
|
||||
req.open("GET", SJS_URL + "?" + window.location.href, false);
|
||||
req.send(null);
|
||||
function startTest() {
|
||||
var req = new XMLHttpRequest({mozAnon: false, mozSystem: true});
|
||||
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>
|
||||
</pre>
|
||||
</body>
|
||||
|
|
|
@ -51,7 +51,7 @@ function createDoc() {
|
|||
function xhrDoc(idx) {
|
||||
return function() {
|
||||
// Defy same-origin restrictions!
|
||||
var xhr = SpecialPowers.createSystemXHR();
|
||||
var xhr = new XMLHttpRequest({mozAnon: false, mozSystem: true});
|
||||
xhr.open("GET", docSources[idx], false);
|
||||
xhr.send();
|
||||
return xhr.responseXML;
|
||||
|
@ -87,6 +87,10 @@ function doTest(idx) {
|
|||
}
|
||||
|
||||
addLoadEvent(function() {
|
||||
SpecialPowers.pushPermissions([{'type': 'systemXHR', 'allow': true, 'context': document}], startTest);
|
||||
});
|
||||
|
||||
function startTest() {
|
||||
// sanity check
|
||||
isnot("", null, "Shouldn't be equal!");
|
||||
|
||||
|
@ -104,7 +108,7 @@ addLoadEvent(function() {
|
|||
xhr.abort();
|
||||
|
||||
SimpleTest.finish();
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=804395
|
|||
<script type="application/javascript">
|
||||
|
||||
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.onreadystatechange = function() {
|
||||
if (xhr.readyState == 4) {
|
||||
|
@ -31,7 +31,7 @@ function test200() {
|
|||
}
|
||||
|
||||
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.onreadystatechange = function() {
|
||||
if (xhr.readyState == 4) {
|
||||
|
@ -43,7 +43,7 @@ function test404() {
|
|||
}
|
||||
|
||||
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);
|
||||
ok(xhr.status == 0, "Not Sent request must have status 0");
|
||||
runTests();
|
||||
|
@ -61,9 +61,11 @@ function runTests() {
|
|||
}
|
||||
|
||||
/** Test for Bug 804395 **/
|
||||
runTests();
|
||||
SimpleTest.waitForExplicitFinish();
|
||||
|
||||
addLoadEvent(function() {
|
||||
SpecialPowers.pushPermissions([{'type': 'systemXHR', 'allow': true, 'context': document}], runTests);
|
||||
});
|
||||
</script>
|
||||
</pre>
|
||||
</body>
|
||||
|
|
|
@ -49,37 +49,47 @@ var headers = [
|
|||
];
|
||||
var i, request;
|
||||
|
||||
// Try setting headers in unprivileged context
|
||||
request = new XMLHttpRequest();
|
||||
request.open("GET", window.location.href);
|
||||
for (i = 0; i < headers.length; i++)
|
||||
request.setRequestHeader(headers[i], "test" + i);
|
||||
function startTest() {
|
||||
// Try setting headers in unprivileged context
|
||||
request = new XMLHttpRequest();
|
||||
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++) {
|
||||
// Retrieving Content-Length will throw an exception
|
||||
var value = null;
|
||||
try {
|
||||
value = channel.getRequestHeader(headers[i]);
|
||||
// Read out headers
|
||||
var channel = SpecialPowers.wrap(request).channel.QueryInterface(SpecialPowers.Ci.nsIHttpChannel);
|
||||
for (i = 0; i < headers.length; i++) {
|
||||
// Retrieving Content-Length will throw an exception
|
||||
var value = null;
|
||||
try {
|
||||
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
|
||||
request = SpecialPowers.createSystemXHR();
|
||||
request.open("GET", window.location.href);
|
||||
for (i = 0; i < headers.length; i++)
|
||||
request.setRequestHeader(headers[i], "test" + i);
|
||||
SimpleTest.waitForExplicitFinish();
|
||||
|
||||
// Read out headers
|
||||
var channel = 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");
|
||||
}
|
||||
addLoadEvent(function() {
|
||||
SpecialPowers.pushPermissions([{'type': 'systemXHR', 'allow': true, 'context': document}], startTest);
|
||||
});
|
||||
</script>
|
||||
</pre>
|
||||
</body>
|
||||
|
|
|
@ -15,6 +15,10 @@
|
|||
#include "sys/stat.h"
|
||||
#endif // defined(XP_UNIX)
|
||||
|
||||
#if defined(XP_LINUX)
|
||||
#include <linux/fadvise.h>
|
||||
#endif // defined(XP_LINUX)
|
||||
|
||||
#if defined(XP_MACOSX)
|
||||
#include "copyfile.h"
|
||||
#endif // defined(XP_MACOSX)
|
||||
|
@ -377,6 +381,10 @@ static const dom::ConstantSpec gLibcProperties[] =
|
|||
INT_CONSTANT(AT_SYMLINK_NOFOLLOW),
|
||||
#endif //defined(AT_SYMLINK_NOFOLLOW)
|
||||
|
||||
#if defined(POSIX_FADV_SEQUENTIAL)
|
||||
INT_CONSTANT(POSIX_FADV_SEQUENTIAL),
|
||||
#endif //defined(POSIX_FADV_SEQUENTIAL)
|
||||
|
||||
// access
|
||||
#if defined(F_OK)
|
||||
INT_CONSTANT(F_OK),
|
||||
|
|
|
@ -57,7 +57,6 @@ import android.text.TextUtils;
|
|||
import android.util.Log;
|
||||
import android.view.InputDevice;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
|
@ -597,7 +596,7 @@ abstract public class BrowserApp extends GeckoApp
|
|||
String title = tab.getDisplayTitle();
|
||||
Bitmap favicon = tab.getFavicon();
|
||||
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;
|
||||
}
|
||||
|
||||
Favicons.loadFavicon(url, tab.getFaviconURL(), 0,
|
||||
Favicons.getFaviconForSize(url, tab.getFaviconURL(), Integer.MAX_VALUE, LoadFaviconTask.FLAG_PERSIST,
|
||||
new OnFaviconLoadedListener() {
|
||||
@Override
|
||||
public void onFaviconLoaded(String url, Bitmap favicon) {
|
||||
GeckoAppShell.createShortcut(title, url, url, favicon == null ? null : favicon, "");
|
||||
public void onFaviconLoaded(String pageUrl, String faviconURL, Bitmap 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 (tabs.isSelectedTab(tabs.getTab(tabId))) {
|
||||
if (tabs.isSelectedTabId(tabId)) {
|
||||
hideHomePager();
|
||||
} else {
|
||||
tabs.selectTab(tabId);
|
||||
|
@ -1330,28 +1329,30 @@ abstract public class BrowserApp extends GeckoApp
|
|||
private void loadFavicon(final Tab tab) {
|
||||
maybeCancelFaviconLoad(tab);
|
||||
|
||||
int flags = LoadFaviconTask.FLAG_SCALE | ( (tab.isPrivate() || tab.getErrorType() != Tab.ErrorType.NONE) ? 0 : LoadFaviconTask.FLAG_PERSIST);
|
||||
int id = Favicons.loadFavicon(tab.getURL(), tab.getFaviconURL(), flags,
|
||||
new OnFaviconLoadedListener() {
|
||||
final int tabFaviconSize = getResources().getDimensionPixelSize(R.dimen.browser_toolbar_favicon_size);
|
||||
|
||||
@Override
|
||||
public void onFaviconLoaded(String pageUrl, Bitmap favicon) {
|
||||
// Leave favicon UI untouched if we failed to load the image
|
||||
// for some reason.
|
||||
if (favicon == null)
|
||||
return;
|
||||
int flags = (tab.isPrivate() || tab.getErrorType() != Tab.ErrorType.NONE) ? 0 : LoadFaviconTask.FLAG_PERSIST;
|
||||
int id = Favicons.getFaviconForSize(tab.getURL(), tab.getFaviconURL(), tabFaviconSize, flags,
|
||||
new OnFaviconLoadedListener() {
|
||||
@Override
|
||||
public void onFaviconLoaded(String pageUrl, String faviconURL, Bitmap favicon) {
|
||||
// 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
|
||||
// favicon is finally loaded, in which case we simply ignore it.
|
||||
if (!tab.getURL().equals(pageUrl))
|
||||
return;
|
||||
// The tab might be pointing to another URL by the time the
|
||||
// favicon is finally loaded, in which case we simply ignore it.
|
||||
// See also: Bug 920331.
|
||||
if (!tab.getURL().equals(pageUrl)) {
|
||||
return;
|
||||
}
|
||||
|
||||
tab.updateFavicon(favicon);
|
||||
tab.setFaviconLoadId(Favicons.NOT_LOADING);
|
||||
|
||||
Tabs.getInstance().notifyListeners(tab, Tabs.TabEvents.FAVICON);
|
||||
}
|
||||
});
|
||||
tab.updateFavicon(favicon);
|
||||
tab.setFaviconLoadId(Favicons.NOT_LOADING);
|
||||
Tabs.getInstance().notifyListeners(tab, Tabs.TabEvents.FAVICON);
|
||||
}
|
||||
});
|
||||
|
||||
tab.setFaviconLoadId(id);
|
||||
}
|
||||
|
@ -1359,9 +1360,6 @@ abstract public class BrowserApp extends GeckoApp
|
|||
private void maybeCancelFaviconLoad(Tab tab) {
|
||||
int faviconLoadId = tab.getFaviconLoadId();
|
||||
|
||||
if (faviconLoadId == Favicons.NOT_LOADING)
|
||||
return;
|
||||
|
||||
// Cancel pending favicon load task
|
||||
Favicons.cancelFaviconLoad(faviconLoadId);
|
||||
|
||||
|
|
|
@ -1121,7 +1121,7 @@ public class BrowserToolbar extends GeckoRelativeLayout
|
|||
image = Bitmap.createScaledBitmap(image, mFaviconSize, mFaviconSize, false);
|
||||
mFavicon.setImageBitmap(image);
|
||||
} else {
|
||||
mFavicon.setImageResource(R.drawable.favicon);
|
||||
mFavicon.setImageBitmap(null);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1192,7 +1192,11 @@ abstract public class GeckoApp
|
|||
ThreadUtils.setUiThread(Thread.currentThread(), new Handler());
|
||||
|
||||
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
|
||||
// actually means restarting the entire application. This logic should
|
||||
|
|
|
@ -76,6 +76,9 @@ FENNEC_JAVA_FILES = \
|
|||
DoorHanger.java \
|
||||
DoorHangerPopup.java \
|
||||
EditBookmarkDialog.java \
|
||||
favicons/cache/FaviconCache.java \
|
||||
favicons/cache/FaviconCacheElement.java \
|
||||
favicons/cache/FaviconsForURL.java \
|
||||
favicons/Favicons.java \
|
||||
favicons/LoadFaviconTask.java \
|
||||
favicons/OnFaviconLoadedListener.java \
|
||||
|
|
|
@ -7,7 +7,6 @@ package org.mozilla.gecko;
|
|||
|
||||
import org.mozilla.gecko.db.BrowserDB;
|
||||
import org.mozilla.gecko.home.HomePager;
|
||||
import org.mozilla.gecko.ReaderModeUtils;
|
||||
import org.mozilla.gecko.sync.setup.SyncAccounts;
|
||||
import org.mozilla.gecko.util.GeckoEventListener;
|
||||
import org.mozilla.gecko.util.ThreadUtils;
|
||||
|
@ -17,7 +16,6 @@ import org.json.JSONObject;
|
|||
import android.accounts.Account;
|
||||
import android.accounts.AccountManager;
|
||||
import android.accounts.OnAccountsUpdateListener;
|
||||
import android.app.Activity;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.database.ContentObserver;
|
||||
|
@ -272,6 +270,11 @@ public class Tabs implements GeckoEventListener {
|
|||
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) {
|
||||
if (mTabs.size() == 0)
|
||||
return null;
|
||||
|
@ -607,6 +610,15 @@ public class Tabs implements GeckoEventListener {
|
|||
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.
|
||||
*
|
||||
|
|
|
@ -88,10 +88,6 @@ public class BrowserDB {
|
|||
|
||||
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 void updateFaviconForUrl(ContentResolver cr, String pageUri, Bitmap favicon, String faviconUri);
|
||||
|
@ -242,16 +238,8 @@ public class BrowserDB {
|
|||
sDb.removeReadingListItemWithURL(cr, uri);
|
||||
}
|
||||
|
||||
public static Bitmap getFaviconForUrl(ContentResolver cr, String uri) {
|
||||
return sDb.getFaviconForUrl(cr, uri);
|
||||
}
|
||||
|
||||
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 Bitmap getFaviconForFaviconUrl(ContentResolver cr, String faviconURL) {
|
||||
return sDb.getFaviconForUrl(cr, faviconURL);
|
||||
}
|
||||
|
||||
public static String getFaviconUrlForHistoryUrl(ContentResolver cr, String url) {
|
||||
|
|
|
@ -3015,6 +3015,11 @@ public class BrowserProvider extends ContentProvider {
|
|||
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();
|
||||
values.put(Favicons.DATE_CREATED, now);
|
||||
values.put(Favicons.DATE_MODIFIED, now);
|
||||
|
|
|
@ -696,33 +696,30 @@ public class LocalBrowserDB implements BrowserDB.BrowserDBIface {
|
|||
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
|
||||
public Bitmap getFaviconForUrl(ContentResolver cr, String uri) {
|
||||
final byte[] b = getFaviconBytesForUrl(cr, uri);
|
||||
if (b == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return BitmapUtils.decodeByteArray(b);
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getFaviconBytesForUrl(ContentResolver cr, String uri) {
|
||||
public Bitmap getFaviconForUrl(ContentResolver cr, String faviconURL) {
|
||||
Cursor c = null;
|
||||
byte[] b = null;
|
||||
|
||||
try {
|
||||
c = cr.query(mCombinedUriWithProfile,
|
||||
new String[] { Combined.FAVICON },
|
||||
Combined.URL + " = ?",
|
||||
new String[] { uri },
|
||||
c = cr.query(mFaviconsUriWithProfile,
|
||||
new String[] { Favicons.DATA },
|
||||
Favicons.URL + " = ?",
|
||||
new String[] { faviconURL },
|
||||
null);
|
||||
|
||||
if (!c.moveToFirst()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final int faviconIndex = c.getColumnIndexOrThrow(Combined.FAVICON);
|
||||
final int faviconIndex = c.getColumnIndexOrThrow(Favicons.DATA);
|
||||
b = c.getBlob(faviconIndex);
|
||||
} finally {
|
||||
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
|
||||
|
@ -754,28 +755,6 @@ public class LocalBrowserDB implements BrowserDB.BrowserDBIface {
|
|||
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
|
||||
public void updateFaviconForUrl(ContentResolver cr, String pageUri,
|
||||
Bitmap favicon, String faviconUri) {
|
||||
|
|
|
@ -5,8 +5,13 @@
|
|||
|
||||
package org.mozilla.gecko.favicons;
|
||||
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.text.TextUtils;
|
||||
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.favicons.cache.FaviconCache;
|
||||
import org.mozilla.gecko.gfx.BitmapUtils;
|
||||
import org.mozilla.gecko.util.ThreadUtils;
|
||||
|
||||
|
@ -15,6 +20,8 @@ import android.graphics.Bitmap;
|
|||
import android.support.v4.util.LruCache;
|
||||
import android.util.Log;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
|
@ -24,73 +31,212 @@ import java.util.Set;
|
|||
public class Favicons {
|
||||
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 FAILED_EXPIRY_NEVER = -1;
|
||||
public static final int FLAG_PERSIST = 1;
|
||||
public static final int FLAG_SCALE = 2;
|
||||
|
||||
private static int sFaviconSmallSize = -1;
|
||||
private static int sFaviconLargeSize = -1;
|
||||
|
||||
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 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
|
||||
// trying to download a Favicon when doing so is currently impossible.
|
||||
private static final LruCache<String, Long> sFailedCache = new LruCache<String, Long>(64);
|
||||
// Cache to hold mappings between page URLs and Favicon URLs. Used to avoid going to the DB when
|
||||
// doing so is not necessary.
|
||||
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
|
||||
// around a Favicon when it is asked to render a Favicon small than the view.
|
||||
private static final LruCache<String, Integer> sColorCache = new LruCache<String, Integer>(256);
|
||||
static void dispatchResult(final String pageUrl, final Bitmap image,
|
||||
public static String getFaviconURLForPageURLFromCache(String pageURL) {
|
||||
return sPageURLMappings.get(pageURL);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
if (pageUrl != null && image != null)
|
||||
putFaviconInMemCache(pageUrl, image);
|
||||
|
||||
// We want to always run the listener on UI thread
|
||||
ThreadUtils.postToUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (listener != null)
|
||||
listener.onFaviconLoaded(pageUrl, image);
|
||||
if (listener != null) {
|
||||
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) {
|
||||
dispatchResult(null, null, listener);
|
||||
return -1;
|
||||
/**
|
||||
* Attempts to find a Favicon for the provided page URL from either the mem cache or the database.
|
||||
* Does not need an explicit favicon URL, since, as we are accessing the database anyway, we
|
||||
* 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
|
||||
if (isFailedFavicon(pageUrl)) {
|
||||
dispatchResult(pageUrl, null, listener);
|
||||
return -1;
|
||||
// No joy using in-memory resources. Go to background thread and ask the database.
|
||||
LoadFaviconTask task = new LoadFaviconTask(ThreadUtils.getBackgroundHandler(), pageURL, targetURL, 0, callback, targetSize, true);
|
||||
int taskId = task.getId();
|
||||
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
|
||||
Bitmap image = getFaviconFromMemCache(pageUrl);
|
||||
if (image != null) {
|
||||
dispatchResult(pageUrl, image, listener);
|
||||
return -1;
|
||||
targetURL = BrowserDB.getFaviconUrlForHistoryUrl(sContext.getContentResolver(), pageURL);
|
||||
if (targetURL == null) {
|
||||
// Nothing in the history database. Fall back to the default URL and hope for the best.
|
||||
targetURL = guessDefaultFaviconURL(pageURL);
|
||||
}
|
||||
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();
|
||||
sLoadTasks.put(taskId, task);
|
||||
|
@ -100,44 +246,29 @@ public class Favicons {
|
|||
return taskId;
|
||||
}
|
||||
|
||||
public static Bitmap getFaviconFromMemCache(String pageUrl) {
|
||||
// If for some reason the key is null, simply return null
|
||||
// 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) {
|
||||
sFaviconsCache.putSingleFavicon(pageUrl, image);
|
||||
}
|
||||
|
||||
public static void putFaviconInMemCache(String pageUrl, Bitmap image) {
|
||||
sFaviconCache.put(pageUrl, image);
|
||||
public static void putFaviconsInMemCache(String pageUrl, Iterator<Bitmap> images) {
|
||||
sFaviconsCache.putFavicons(pageUrl, images);
|
||||
}
|
||||
|
||||
public static void clearMemCache() {
|
||||
sFaviconCache.evictAll();
|
||||
sFaviconsCache.evictAll();
|
||||
sPageURLMappings.evictAll();
|
||||
}
|
||||
|
||||
public static boolean isFailedFavicon(String pageUrl) {
|
||||
Long fetchTime = sFailedCache.get(pageUrl);
|
||||
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 void putFaviconInFailedCache(String faviconURL) {
|
||||
sFaviconsCache.putFailed(faviconURL);
|
||||
}
|
||||
|
||||
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) {
|
||||
if (!sLoadTasks.containsKey(taskId))
|
||||
return false;
|
||||
|
@ -161,47 +292,92 @@ public class Favicons {
|
|||
int taskId = iter.next();
|
||||
cancelFaviconLoad(taskId);
|
||||
}
|
||||
sLoadTasks.clear();
|
||||
}
|
||||
|
||||
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.
|
||||
// Otherwise, scale it to sFaviconSmallSize.
|
||||
if (isLargeFavicon(image)) {
|
||||
image = Bitmap.createScaledBitmap(image, sFaviconLargeSize, sFaviconLargeSize, false);
|
||||
} else {
|
||||
image = Bitmap.createScaledBitmap(image, sFaviconSmallSize, sFaviconSmallSize, false);
|
||||
}
|
||||
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) {
|
||||
/**
|
||||
* Called by GeckoApp on startup to pass this class a reference to the GeckoApp object used as
|
||||
* the application's Context.
|
||||
* Consider replacing with references to a staticly held reference to the GeckoApp object.
|
||||
*
|
||||
* @param context A reference to the GeckoApp instance.
|
||||
*/
|
||||
public static void attachToContext(Context context) throws Exception {
|
||||
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) {
|
||||
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.net.http.AndroidHttpClient;
|
||||
import android.os.Handler;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
import org.apache.http.Header;
|
||||
import org.apache.http.HttpEntity;
|
||||
import org.apache.http.HttpResponse;
|
||||
import org.apache.http.client.methods.HttpGet;
|
||||
|
@ -22,12 +24,15 @@ import org.mozilla.gecko.util.ThreadUtils;
|
|||
import org.mozilla.gecko.util.UiAsyncTask;
|
||||
import static org.mozilla.gecko.favicons.Favicons.sContext;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URI;
|
||||
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.AtomicReference;
|
||||
|
||||
/**
|
||||
* 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> {
|
||||
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_SCALE = 2;
|
||||
private static final int MAX_REDIRECTS_TO_FOLLOW = 5;
|
||||
|
||||
private static AtomicInteger mNextFaviconLoadId = new AtomicInteger(0);
|
||||
private int mId;
|
||||
|
@ -48,25 +58,39 @@ public class LoadFaviconTask extends UiAsyncTask<Void, Void, Bitmap> {
|
|||
private OnFaviconLoadedListener mListener;
|
||||
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());
|
||||
|
||||
public LoadFaviconTask(Handler backgroundThreadHandler,
|
||||
String aPageUrl, String aFaviconUrl, int aFlags,
|
||||
OnFaviconLoadedListener aListener) {
|
||||
String pageUrl, String faviconUrl, int flags,
|
||||
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);
|
||||
|
||||
mId = mNextFaviconLoadId.incrementAndGet();
|
||||
|
||||
mPageUrl = aPageUrl;
|
||||
mFaviconUrl = aFaviconUrl;
|
||||
mPageUrl = pageUrl;
|
||||
mFaviconUrl = faviconUrl;
|
||||
mListener = aListener;
|
||||
mFlags = aFlags;
|
||||
mFlags = flags;
|
||||
mTargetWidth = targetSize;
|
||||
mOnlyFromLocal = fromLocal;
|
||||
}
|
||||
|
||||
// Runs in background thread
|
||||
private Bitmap loadFaviconFromDb() {
|
||||
ContentResolver resolver = sContext.getContentResolver();
|
||||
return BrowserDB.getFaviconForUrl(resolver, mPageUrl);
|
||||
return BrowserDB.getFaviconForFaviconUrl(resolver, mFaviconUrl);
|
||||
}
|
||||
|
||||
// Runs in background thread
|
||||
|
@ -79,50 +103,87 @@ public class LoadFaviconTask extends UiAsyncTask<Void, Void, Bitmap> {
|
|||
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
|
||||
private Bitmap downloadFavicon(URL targetFaviconURL) {
|
||||
private Bitmap downloadFavicon(URI targetFaviconURI) {
|
||||
if (mFaviconUrl.startsWith("jar:jar:")) {
|
||||
return GeckoJarReader.getBitmap(sContext.getResources(), mFaviconUrl);
|
||||
}
|
||||
|
||||
URI uri;
|
||||
try {
|
||||
uri = targetFaviconURL.toURI();
|
||||
} catch (URISyntaxException e) {
|
||||
Log.d(LOGTAG, "Could not get URI for favicon");
|
||||
// only get favicons for HTTP/HTTPS
|
||||
String scheme = targetFaviconURI.getScheme();
|
||||
if (!"http".equals(scheme) && !"https".equals(scheme)) {
|
||||
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
|
||||
// http://groups.google.com/group/android-developers/browse_thread/thread/171b8bf35dbbed96/c3ec5f45436ceec8?lnk=raot
|
||||
Bitmap image = null;
|
||||
try {
|
||||
HttpGet request = new HttpGet(targetFaviconURL.toURI());
|
||||
HttpResponse response = sHttpClient.execute(request);
|
||||
if (response == null)
|
||||
// Try the URL we were given.
|
||||
HttpResponse response = tryDownload(targetFaviconURI);
|
||||
if (response == 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();
|
||||
if (entity == null)
|
||||
if (entity == 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);
|
||||
|
@ -145,69 +206,194 @@ public class LoadFaviconTask extends UiAsyncTask<Void, Void, Bitmap> {
|
|||
|
||||
@Override
|
||||
protected Bitmap doInBackground(Void... unused) {
|
||||
Bitmap image;
|
||||
|
||||
if (isCancelled())
|
||||
if (isCancelled()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
URL faviconURLToDownload;
|
||||
String storedFaviconUrl;
|
||||
boolean isUsingDefaultURL = false;
|
||||
|
||||
// Handle the case of malformed favicon URL
|
||||
try {
|
||||
// If favicon is empty, fallback to default favicon URI
|
||||
if (mFaviconUrl == null || mFaviconUrl.length() == 0) {
|
||||
// Handle the case of malformed URL
|
||||
URL targetPageURL = new URL(mPageUrl);
|
||||
// Handle the case of malformed favicon URL.
|
||||
// If favicon is empty, fall back to the stored one.
|
||||
if (TextUtils.isEmpty(mFaviconUrl)) {
|
||||
// Try to get the favicon URL from the memory cache.
|
||||
storedFaviconUrl = Favicons.getFaviconURLForPageURLFromCache(mPageUrl);
|
||||
|
||||
faviconURLToDownload = new URL(targetPageURL.getProtocol(), targetPageURL.getAuthority(), "/favicon.ico");
|
||||
mFaviconUrl = faviconURLToDownload.toString();
|
||||
} else {
|
||||
faviconURLToDownload = new URL(mFaviconUrl);
|
||||
// If that failed, try to get the URL from the database.
|
||||
if (storedFaviconUrl == null) {
|
||||
storedFaviconUrl = Favicons.getFaviconUrlForPageUrl(mPageUrl);
|
||||
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;
|
||||
}
|
||||
|
||||
if (isCancelled())
|
||||
if (isCancelled()) {
|
||||
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())
|
||||
return null;
|
||||
Bitmap image;
|
||||
// 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) {
|
||||
saveFaviconToDb(image);
|
||||
image = ((mFlags & FLAG_SCALE) != 0) ? Favicons.scaleImage(image) : image;
|
||||
} else {
|
||||
image = null;
|
||||
Favicons.putFaviconInFailedCache(mFaviconUrl);
|
||||
}
|
||||
|
||||
return image;
|
||||
}
|
||||
|
||||
@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.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
|
||||
protected void onCancelled() {
|
||||
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
|
||||
// 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() {
|
||||
return mId;
|
||||
}
|
||||
|
|
|
@ -10,5 +10,5 @@ import android.graphics.Bitmap;
|
|||
* Interface to be implemented by objects wishing to listen for favicon load completion events.
|
||||
*/
|
||||
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 */
|
||||
DisplayPortMetrics getDisplayPort(boolean pageSizeUpdate, boolean isBrowserContentDisplayed, int tabId, ImmutableViewportMetrics metrics) {
|
||||
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
|
||||
// displayed is different from the content document. In that case, just
|
||||
// calculate the display port.
|
||||
|
|
|
@ -5,29 +5,19 @@
|
|||
|
||||
package org.mozilla.gecko.home;
|
||||
|
||||
import org.mozilla.gecko.favicons.Favicons;
|
||||
import org.mozilla.gecko.R;
|
||||
import org.mozilla.gecko.Tabs;
|
||||
import org.mozilla.gecko.db.BrowserContract.Bookmarks;
|
||||
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.HomePager.OnUrlOpenListener;
|
||||
import org.mozilla.gecko.util.ThreadUtils;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.content.res.Configuration;
|
||||
import android.database.Cursor;
|
||||
import android.graphics.Bitmap;
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.app.LoaderManager;
|
||||
import android.support.v4.app.LoaderManager.LoaderCallbacks;
|
||||
import android.support.v4.content.Loader;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
|
|
@ -16,7 +16,6 @@ import org.mozilla.gecko.ReaderModeUtils;
|
|||
import org.mozilla.gecko.Tabs;
|
||||
import org.mozilla.gecko.db.BrowserContract.Combined;
|
||||
import org.mozilla.gecko.db.BrowserDB;
|
||||
import org.mozilla.gecko.gfx.BitmapUtils;
|
||||
import org.mozilla.gecko.home.HomeListView.HomeContextMenuInfo;
|
||||
import org.mozilla.gecko.util.ThreadUtils;
|
||||
import org.mozilla.gecko.util.UiAsyncTask;
|
||||
|
@ -114,7 +113,7 @@ abstract class HomeFragment extends Fragment {
|
|||
return false;
|
||||
}
|
||||
|
||||
HomeContextMenuInfo info = (HomeContextMenuInfo) menuInfo;
|
||||
final HomeContextMenuInfo info = (HomeContextMenuInfo) menuInfo;
|
||||
final Context context = getActivity().getApplicationContext();
|
||||
|
||||
final int itemId = item.getItemId();
|
||||
|
@ -133,7 +132,14 @@ abstract class HomeFragment extends Fragment {
|
|||
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;
|
||||
}
|
||||
|
||||
|
@ -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 final Context mContext;
|
||||
private final int mId;
|
||||
|
|
|
@ -37,12 +37,16 @@ public class TopSitesGridItemView extends RelativeLayout {
|
|||
// Data backing this view.
|
||||
private String mTitle;
|
||||
private String mUrl;
|
||||
private String mFaviconURL;
|
||||
|
||||
private Bitmap mThumbnail;
|
||||
|
||||
// Pinned state.
|
||||
private boolean mIsPinned = false;
|
||||
|
||||
// Empty state.
|
||||
private boolean mIsEmpty = true;
|
||||
private int mLoadId = Favicons.NOT_LOADING;
|
||||
|
||||
public TopSitesGridItemView(Context context) {
|
||||
this(context, null);
|
||||
|
@ -150,6 +154,8 @@ public class TopSitesGridItemView extends RelativeLayout {
|
|||
displayThumbnail(R.drawable.favicon);
|
||||
return;
|
||||
}
|
||||
mThumbnail = thumbnail;
|
||||
Favicons.cancelFaviconLoad(mLoadId);
|
||||
|
||||
mThumbnailView.setScaleType(ScaleType.CENTER_CROP);
|
||||
mThumbnailView.setImageBitmap(thumbnail);
|
||||
|
@ -161,16 +167,27 @@ public class TopSitesGridItemView extends RelativeLayout {
|
|||
*
|
||||
* @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) {
|
||||
// Should show default favicon.
|
||||
displayThumbnail(R.drawable.favicon);
|
||||
return;
|
||||
}
|
||||
|
||||
if (faviconURL != null) {
|
||||
mFaviconURL = faviconURL;
|
||||
}
|
||||
|
||||
mThumbnailView.setScaleType(ScaleType.CENTER);
|
||||
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.
|
||||
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.URLColumns;
|
||||
import org.mozilla.gecko.db.BrowserDB.TopSitesCursorWrapper;
|
||||
import org.mozilla.gecko.favicons.OnFaviconLoadedListener;
|
||||
import org.mozilla.gecko.gfx.BitmapUtils;
|
||||
import org.mozilla.gecko.home.HomeListView.HomeContextMenuInfo;
|
||||
import org.mozilla.gecko.home.HomePager.OnUrlOpenListener;
|
||||
|
@ -32,7 +33,6 @@ import android.graphics.Bitmap;
|
|||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.app.FragmentManager;
|
||||
import android.support.v4.app.LoaderManager;
|
||||
import android.support.v4.app.LoaderManager.LoaderCallbacks;
|
||||
import android.support.v4.content.AsyncTaskLoader;
|
||||
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.
|
||||
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() {
|
||||
return new TopSitesPage();
|
||||
}
|
||||
|
@ -531,7 +515,7 @@ public class TopSitesPage extends HomeFragment {
|
|||
|
||||
public class TopSitesGridAdapter extends CursorAdapter {
|
||||
// Cache to store the thumbnails.
|
||||
private Map<String, Thumbnail> mThumbnails;
|
||||
private Map<String, Bitmap> mThumbnails;
|
||||
|
||||
public TopSitesGridAdapter(Context context, Cursor cursor) {
|
||||
super(context, cursor);
|
||||
|
@ -554,7 +538,7 @@ public class TopSitesPage extends HomeFragment {
|
|||
*
|
||||
* @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;
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
@ -572,7 +556,7 @@ public class TopSitesPage extends HomeFragment {
|
|||
pinned = ((TopSitesCursorWrapper) cursor).isPinned();
|
||||
}
|
||||
|
||||
TopSitesGridItemView view = (TopSitesGridItemView) bindView;
|
||||
final TopSitesGridItemView view = (TopSitesGridItemView) bindView;
|
||||
view.setTitle(title);
|
||||
view.setUrl(url);
|
||||
view.setPinned(pinned);
|
||||
|
@ -581,14 +565,18 @@ public class TopSitesPage extends HomeFragment {
|
|||
if (TextUtils.isEmpty(url)) {
|
||||
view.displayThumbnail(R.drawable.top_site_add);
|
||||
} else {
|
||||
// Show the thumbnail.
|
||||
Thumbnail thumbnail = (mThumbnails != null ? mThumbnails.get(url) : null);
|
||||
if (thumbnail == null) {
|
||||
view.displayThumbnail(null);
|
||||
} else if (thumbnail.isThumbnail) {
|
||||
view.displayThumbnail(thumbnail.bitmap);
|
||||
// Show the thumbnail, if any.
|
||||
Bitmap thumbnail = (mThumbnails != null ? mThumbnails.get(url) : null);
|
||||
if (thumbnail != null) {
|
||||
view.displayThumbnail(thumbnail);
|
||||
} 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.
|
||||
*/
|
||||
private static class ThumbnailsLoader extends AsyncTaskLoader<Map<String, Thumbnail>> {
|
||||
private Map<String, Thumbnail> mThumbnails;
|
||||
private static class ThumbnailsLoader extends AsyncTaskLoader<Map<String, Bitmap>> {
|
||||
private Map<String, Bitmap> mThumbnails;
|
||||
private ArrayList<String> mUrls;
|
||||
|
||||
public ThumbnailsLoader(Context context, ArrayList<String> urls) {
|
||||
|
@ -675,7 +663,7 @@ public class TopSitesPage extends HomeFragment {
|
|||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Thumbnail> loadInBackground() {
|
||||
public Map<String, Bitmap> loadInBackground() {
|
||||
if (mUrls == null || mUrls.size() == 0) {
|
||||
return null;
|
||||
}
|
||||
|
@ -688,7 +676,7 @@ public class TopSitesPage extends HomeFragment {
|
|||
return null;
|
||||
}
|
||||
|
||||
final Map<String, Thumbnail> thumbnails = new HashMap<String, Thumbnail>();
|
||||
final Map<String, Bitmap> thumbnails = new HashMap<String, Bitmap>();
|
||||
|
||||
try {
|
||||
final int urlIndex = cursor.getColumnIndexOrThrow(Thumbnails.URL);
|
||||
|
@ -713,29 +701,17 @@ public class TopSitesPage extends HomeFragment {
|
|||
break;
|
||||
}
|
||||
|
||||
thumbnails.put(url, new Thumbnail(bitmap, true));
|
||||
thumbnails.put(url, bitmap);
|
||||
}
|
||||
} finally {
|
||||
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;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deliverResult(Map<String, Thumbnail> thumbnails) {
|
||||
public void deliverResult(Map<String, Bitmap> thumbnails) {
|
||||
if (isReset()) {
|
||||
mThumbnails = null;
|
||||
return;
|
||||
|
@ -765,7 +741,7 @@ public class TopSitesPage extends HomeFragment {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void onCanceled(Map<String, Thumbnail> thumbnails) {
|
||||
public void onCanceled(Map<String, Bitmap> thumbnails) {
|
||||
mThumbnails = null;
|
||||
}
|
||||
|
||||
|
@ -783,14 +759,14 @@ public class TopSitesPage extends HomeFragment {
|
|||
/**
|
||||
* Loader callbacks for the thumbnails on TopSitesGridView.
|
||||
*/
|
||||
private class ThumbnailsLoaderCallbacks implements LoaderCallbacks<Map<String, Thumbnail>> {
|
||||
private class ThumbnailsLoaderCallbacks implements LoaderCallbacks<Map<String, Bitmap>> {
|
||||
@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));
|
||||
}
|
||||
|
||||
@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) {
|
||||
mGridAdapter.updateThumbnails(thumbnails);
|
||||
}
|
||||
|
@ -801,7 +777,7 @@ public class TopSitesPage extends HomeFragment {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void onLoaderReset(Loader<Map<String, Thumbnail>> loader) {
|
||||
public void onLoaderReset(Loader<Map<String, Bitmap>> loader) {
|
||||
if (mGridAdapter != null) {
|
||||
mGridAdapter.updateThumbnails(null);
|
||||
}
|
||||
|
|
|
@ -5,24 +5,20 @@
|
|||
|
||||
package org.mozilla.gecko.home;
|
||||
|
||||
import android.util.Log;
|
||||
import org.mozilla.gecko.favicons.Favicons;
|
||||
import org.mozilla.gecko.R;
|
||||
import org.mozilla.gecko.Tab;
|
||||
import org.mozilla.gecko.Tabs;
|
||||
import org.mozilla.gecko.db.BrowserContract.Combined;
|
||||
import org.mozilla.gecko.db.BrowserDB;
|
||||
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.UiAsyncTask;
|
||||
import org.mozilla.gecko.widget.FaviconView;
|
||||
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.graphics.Bitmap;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Build;
|
||||
import android.text.TextUtils;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.Gravity;
|
||||
|
@ -41,12 +37,19 @@ public class TwoLinePageRow extends LinearLayout
|
|||
private int mUrlIconId;
|
||||
private int mBookmarkIconId;
|
||||
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.
|
||||
private String mPageUrl;
|
||||
|
||||
private LoadFaviconTask mLoadFaviconTask;
|
||||
|
||||
public TwoLinePageRow(Context context) {
|
||||
this(context, null);
|
||||
}
|
||||
|
@ -81,8 +84,6 @@ public class TwoLinePageRow extends LinearLayout
|
|||
Tabs.unregisterOnTabsChangedListener(TwoLinePageRow.this);
|
||||
}
|
||||
});
|
||||
|
||||
cancelLoadFaviconTask();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -118,7 +119,11 @@ public class TwoLinePageRow extends LinearLayout
|
|||
}
|
||||
|
||||
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) {
|
||||
|
@ -139,16 +144,6 @@ public class TwoLinePageRow extends LinearLayout
|
|||
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.
|
||||
* 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);
|
||||
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
|
||||
// bar view - this is the equivalent of getDisplayTitle() in Tab.java
|
||||
setTitle(TextUtils.isEmpty(title) ? url : title);
|
||||
|
||||
// No need to do extra work if the URL associated with this view
|
||||
// hasn't changed.
|
||||
if (TextUtils.equals(mPageUrl, url)) {
|
||||
return;
|
||||
}
|
||||
// Blank the Favicon, so we don't show the wrong Favicon if we scroll and miss DB.
|
||||
mFavicon.clearImage();
|
||||
mLoadFaviconJobId = Favicons.getSizedFaviconForPageFromLocal(url, mFaviconListener);
|
||||
|
||||
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:paddingLeft="4dip"
|
||||
android:paddingRight="4dip"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:src="@drawable/favicon"/>
|
||||
android:layout_gravity="center_vertical"/>
|
||||
|
||||
<ImageButton android:id="@+id/site_security"
|
||||
style="@style/UrlBar.ImageButton"
|
||||
|
|
|
@ -19,6 +19,13 @@
|
|||
<dimen name="favicon_size_large">32dp</dimen>
|
||||
<dimen name="favicon_bg">32dp</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 -->
|
||||
<dimen name="page_row_height">64dp</dimen>
|
||||
|
|
|
@ -98,7 +98,7 @@ abstract class AboutHomeTest extends BaseTest {
|
|||
protected View getDisplayedBookmark(String url) {
|
||||
openAboutHomeTab(AboutHomeTabs.BOOKMARKS);
|
||||
ListView bookmarksTabList = findListViewWithTag("bookmarks");
|
||||
waitForListToLoad(bookmarksTabList);
|
||||
waitForNonEmptyListToLoad(bookmarksTabList);
|
||||
ListAdapter adapter = bookmarksTabList.getAdapter();
|
||||
if (adapter != null) {
|
||||
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() {
|
||||
@Override
|
||||
public boolean isSatisfied() {
|
||||
|
@ -144,12 +146,16 @@ abstract class AboutHomeTest extends BaseTest {
|
|||
return false;
|
||||
}
|
||||
|
||||
return (adapter.getCount() > 0);
|
||||
return (listView.getCount() - listView.getHeaderViewsCount() >= minSize);
|
||||
}
|
||||
};
|
||||
return waitForCondition(listWaitCondition, MAX_WAIT_MS);
|
||||
}
|
||||
|
||||
protected boolean waitForNonEmptyListToLoad(final ListView listView) {
|
||||
return waitForListToLoad(listView, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an active ListView with the specified tag .
|
||||
*
|
||||
|
|
|
@ -44,7 +44,7 @@ public class testBookmarklets extends AboutHomeTest {
|
|||
openAboutHomeTab(AboutHomeTabs.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 height = mDriver.getGeckoHeight();
|
||||
|
|
|
@ -33,7 +33,7 @@ public class testHistory extends AboutHomeTest {
|
|||
openAboutHomeTab(AboutHomeTabs.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
|
||||
// wait for the history list to be populated
|
||||
|
|
|
@ -72,7 +72,7 @@ public class testShareLink extends AboutHomeTest {
|
|||
openAboutHomeTab(AboutHomeTabs.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());
|
||||
mSolo.clickLongOnView(bookmarksItem);
|
||||
|
@ -101,7 +101,7 @@ public class testShareLink extends AboutHomeTest {
|
|||
mActions.drag(width / 2, width / 2, height - 10, height / 2);
|
||||
|
||||
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());
|
||||
mSolo.clickLongOnView(mostVisitedItem);
|
||||
verifySharePopup(shareOptions,"top_sites");
|
||||
|
@ -110,7 +110,7 @@ public class testShareLink extends AboutHomeTest {
|
|||
openAboutHomeTab(AboutHomeTabs.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
|
||||
View mostRecentItem = mostRecentList.getChildAt(mostRecentList.getHeaderViewsCount() + 1);
|
||||
|
|
|
@ -106,7 +106,11 @@ public class FaviconView extends ImageView {
|
|||
* space.
|
||||
*/
|
||||
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));
|
||||
final Drawable drawable = getResources().getDrawable(R.drawable.favicon_bg);
|
||||
drawable.setColorFilter(color, Mode.SRC_ATOP);
|
||||
|
@ -152,7 +156,7 @@ public class FaviconView extends ImageView {
|
|||
formatImage();
|
||||
}
|
||||
|
||||
private void showDefaultFavicon() {
|
||||
public void showDefaultFavicon() {
|
||||
setImageResource(R.drawable.favicon);
|
||||
hideBackground();
|
||||
}
|
||||
|
|
|
@ -243,6 +243,7 @@
|
|||
@BINPATH@/components/spellchecker.xpt
|
||||
@BINPATH@/components/storage.xpt
|
||||
@BINPATH@/components/telemetry.xpt
|
||||
@BINPATH@/components/toolkit_finalizationwitness.xpt
|
||||
@BINPATH@/components/toolkitprofile.xpt
|
||||
#ifdef MOZ_ENABLE_XREMOTE
|
||||
@BINPATH@/components/toolkitremote.xpt
|
||||
|
|
|
@ -36,7 +36,6 @@
|
|||
"content/base/test/test_websocket_hello.html": "",
|
||||
"content/base/test/test_x-frame-options.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/websocket_hybi/test_receive-arraybuffer.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_x-frame-options.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/websocket_hybi/test_receive-arraybuffer.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_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_bug804395.html":"bug 901343, specialpowers.wrap issue createsystemxhr",
|
||||
"content/base/test/test_bug338583.html":"https not working, bug 907770",
|
||||
|
||||
"content/base/test/test_bug475156.html":"36 total - bug 902611",
|
||||
"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_bug424359-1.html":"",
|
||||
"content/base/test/test_bug424359-2.html":"",
|
||||
"content/base/test/test_bug426308.html":"",
|
||||
"content/base/test/test_mixed_content_blocker_bug803225.html":"",
|
||||
"content/html/document/test/test_non-ascii-cookie.html":"",
|
||||
|
||||
|
@ -321,7 +319,7 @@
|
|||
"dom/media/tests/mochitest/test_peerConnection_throwInCallbacks.html":"",
|
||||
|
||||
"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_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
|
||||
# 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 = \
|
||||
bootstrap.js \
|
||||
install.rdf \
|
||||
chrome.manifest \
|
||||
$(NULL)
|
||||
|
||||
TEST_EXTENSIONS_DIR = $(DEPTH)/_tests/testing/mochitest/extensions
|
||||
|
@ -15,4 +25,6 @@ include $(topsrcdir)/config/rules.mk
|
|||
libs:: $(_TEST_FILES)
|
||||
$(MKDIR) -p $(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
|
||||
is(SpecialPowers.DOMWindowUtils.getClassName(window), "Proxy");
|
||||
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
|
||||
is(SpecialPowers.can_QI(SpecialPowers), false);
|
||||
|
@ -99,7 +94,7 @@ function starttest(){
|
|||
|
||||
// Try some basic stuff with XHR.
|
||||
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']
|
||||
.createInstance(SpecialPowers.Ci.nsIURI);
|
||||
testURI.spec = "http://www.foobar.org/";
|
||||
|
|
|
@ -1178,10 +1178,6 @@ SpecialPowersAPI.prototype = {
|
|||
this._getMUDV(window).stopEmulatingMedium();
|
||||
},
|
||||
|
||||
createSystemXHR: function() {
|
||||
return this.wrap(Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance(Ci.nsIXMLHttpRequest));
|
||||
},
|
||||
|
||||
snapshotWindowWithOptions: function (win, rect, bgcolor, options) {
|
||||
var el = this.window.get().document.createElementNS("http://www.w3.org/1999/xhtml", "canvas");
|
||||
if (rect === undefined) {
|
||||
|
|
|
@ -28,6 +28,7 @@ SHARED_LIBRARY_LIBS = \
|
|||
../jsdownloads/src/$(LIB_PREFIX)jsdownloads_s.$(LIB_SUFFIX) \
|
||||
../protobuf/$(LIB_PREFIX)protobuf_s.$(LIB_SUFFIX) \
|
||||
../intl/$(LIB_PREFIX)intl_s.$(LIB_SUFFIX) \
|
||||
../finalizationwitness/$(LIB_PREFIX)finalizationwitness_s.$(LIB_SUFFIX) \
|
||||
$(NULL)
|
||||
|
||||
ifndef MOZ_DISABLE_PARENTAL_CONTROLS
|
||||
|
|
|
@ -34,6 +34,7 @@
|
|||
#endif
|
||||
|
||||
#include "nsBrowserStatusFilter.h"
|
||||
#include "mozilla/FinalizationWitnessService.h"
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
|
@ -85,6 +86,7 @@ NS_GENERIC_FACTORY_CONSTRUCTOR(nsBrowserStatusFilter)
|
|||
#if defined(USE_MOZ_UPDATER)
|
||||
NS_GENERIC_FACTORY_CONSTRUCTOR(nsUpdateProcessor)
|
||||
#endif
|
||||
NS_GENERIC_FACTORY_CONSTRUCTOR(FinalizationWitnessService)
|
||||
|
||||
NS_DEFINE_NAMED_CID(NS_TOOLKIT_APPSTARTUP_CID);
|
||||
NS_DEFINE_NAMED_CID(NS_USERINFO_CID);
|
||||
|
@ -109,6 +111,7 @@ NS_DEFINE_NAMED_CID(NS_CHARSETMENU_CID);
|
|||
#if defined(USE_MOZ_UPDATER)
|
||||
NS_DEFINE_NAMED_CID(NS_UPDATEPROCESSOR_CID);
|
||||
#endif
|
||||
NS_DEFINE_NAMED_CID(FINALIZATIONWITNESSSERVICE_CID);
|
||||
|
||||
static const mozilla::Module::CIDEntry kToolkitCIDs[] = {
|
||||
{ &kNS_TOOLKIT_APPSTARTUP_CID, false, nullptr, nsAppStartupConstructor },
|
||||
|
@ -134,6 +137,7 @@ static const mozilla::Module::CIDEntry kToolkitCIDs[] = {
|
|||
#if defined(USE_MOZ_UPDATER)
|
||||
{ &kNS_UPDATEPROCESSOR_CID, false, nullptr, nsUpdateProcessorConstructor },
|
||||
#endif
|
||||
{ &kFINALIZATIONWITNESSSERVICE_CID, false, NULL, FinalizationWitnessServiceConstructor },
|
||||
{ nullptr }
|
||||
};
|
||||
|
||||
|
@ -162,6 +166,7 @@ static const mozilla::Module::ContractIDEntry kToolkitContracts[] = {
|
|||
#if defined(USE_MOZ_UPDATER)
|
||||
{ NS_UPDATEPROCESSOR_CONTRACTID, &kNS_UPDATEPROCESSOR_CID },
|
||||
#endif
|
||||
{ FINALIZATIONWITNESSSERVICE_CONTRACTID, &kFINALIZATIONWITNESSSERVICE_CID },
|
||||
{ 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',
|
||||
'exthelper',
|
||||
'filepicker',
|
||||
'finalizationwitness',
|
||||
'find',
|
||||
'intl',
|
||||
'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.
|
||||
*
|
||||
* 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 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 clone = SharedAll.clone;
|
||||
|
||||
let worker = new PromiseWorker(
|
||||
"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
|
||||
// need to extract the |byteLength| now, as it will be lost by
|
||||
// communication
|
||||
if (isTypedArray(buffer) && (!options || !("bytes" in options))) {
|
||||
// Preserve the reference to |outExecutionDuration| option if it is
|
||||
// passed.
|
||||
if (isTypedArray(buffer) && !("bytes" in options)) {
|
||||
// Preserve reference to option |outExecutionDuration|, if it is passed.
|
||||
options = clone(options, ["outExecutionDuration"]);
|
||||
options.bytes = buffer.byteLength;
|
||||
}
|
||||
|
@ -459,9 +420,8 @@ File.prototype = {
|
|||
// 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 communication
|
||||
if (isTypedArray(buffer) && (!options || !("bytes" in options))) {
|
||||
// Preserve the reference to |outExecutionDuration| option if it is
|
||||
// passed.
|
||||
if (isTypedArray(buffer)) {
|
||||
// Preserve reference to option |outExecutionDuration|, if it is passed.
|
||||
options = clone(options, ["outExecutionDuration"]);
|
||||
options.bytes = buffer.byteLength;
|
||||
}
|
||||
|
@ -482,13 +442,14 @@ File.prototype = {
|
|||
* @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
|
||||
* contain that many bytes.
|
||||
* @param {JSON} options
|
||||
* @return {promise}
|
||||
* @resolves {Uint8Array} An array containing the bytes read.
|
||||
*/
|
||||
read: function read(nbytes) {
|
||||
read: function read(nbytes, options = {}) {
|
||||
let promise = Scheduler.post("File_prototype_read",
|
||||
[this._fdmsg,
|
||||
nbytes]);
|
||||
nbytes, options]);
|
||||
return promise.then(
|
||||
function onSuccess(data) {
|
||||
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
|
||||
* to read.
|
||||
* @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
|
||||
* read from the file.
|
||||
|
|
|
@ -271,7 +271,7 @@ if (this.Components) {
|
|||
});
|
||||
},
|
||||
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]);
|
||||
},
|
||||
exists: function exists(path) {
|
||||
|
|
|
@ -34,6 +34,7 @@ if (typeof Components != "undefined") {
|
|||
|
||||
let EXPORTED_SYMBOLS = [
|
||||
"LOG",
|
||||
"clone",
|
||||
"Config",
|
||||
"Constants",
|
||||
"Type",
|
||||
|
@ -162,6 +163,46 @@ let LOG = function (...args) {
|
|||
|
||||
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
|
||||
|
||||
/**
|
||||
|
@ -974,6 +1015,7 @@ exports.OS = {
|
|||
Constants: exports.Constants,
|
||||
Shared: {
|
||||
LOG: LOG,
|
||||
clone: clone,
|
||||
Type: Type,
|
||||
HollowStructure: HollowStructure,
|
||||
Error: OSError,
|
||||
|
@ -1015,4 +1057,3 @@ if (typeof Components != "undefined") {
|
|||
this[symbol] = exports[symbol];
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -17,6 +17,7 @@ if (typeof Components != "undefined") {
|
|||
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 clone = exports.OS.Shared.clone;
|
||||
|
||||
/**
|
||||
* Code shared by implementations of File.
|
||||
|
@ -44,17 +45,17 @@ AbstractFile.prototype = {
|
|||
* Read bytes from this file to a new buffer.
|
||||
*
|
||||
* @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.
|
||||
* @param {JSON} options
|
||||
* @return {Uint8Array} An array containing the bytes read.
|
||||
*/
|
||||
read: function read(bytes) {
|
||||
if (bytes == null) {
|
||||
bytes = this.stat().size;
|
||||
}
|
||||
let buffer = new Uint8Array(bytes);
|
||||
let size = this.readTo(buffer, {bytes: bytes});
|
||||
if (size == bytes) {
|
||||
read: function read(bytes, options = {}) {
|
||||
options = clone(options);
|
||||
options.bytes = bytes == null ? this.stat().size : bytes;
|
||||
let buffer = new Uint8Array(options.bytes);
|
||||
let size = this.readTo(buffer, options);
|
||||
if (size == options.bytes) {
|
||||
return buffer;
|
||||
} else {
|
||||
return buffer.subarray(0, size);
|
||||
|
@ -292,14 +293,15 @@ AbstractFile.normalizeOpenMode = function normalizeOpenMode(mode) {
|
|||
* @param {string} path The path to the file.
|
||||
* @param {number=} bytes Optionally, an upper bound to the number of bytes
|
||||
* to read.
|
||||
* @param {JSON} options Optionally contains additional options.
|
||||
*
|
||||
* @return {Uint8Array} A buffer holding the bytes
|
||||
* 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);
|
||||
try {
|
||||
return file.read(bytes);
|
||||
return file.read(bytes, options);
|
||||
} finally {
|
||||
file.close();
|
||||
}
|
||||
|
|
|
@ -447,6 +447,14 @@
|
|||
/*buf*/ Types.void_t.out_ptr,
|
||||
/*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) {
|
||||
// Special case for MacOS X 10.5+
|
||||
// Symbol name "readdir" still exists but is used for a
|
||||
|
|
|
@ -94,6 +94,13 @@
|
|||
* @throws {OS.File.Error} In case of I/O error.
|
||||
*/
|
||||
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",
|
||||
UnixFile.read(this.fd, buffer, nbytes)
|
||||
);
|
||||
|
|
|
@ -98,6 +98,7 @@ const Cu = Components.utils;
|
|||
const Cr = Components.results;
|
||||
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
|
||||
const STATUS_PENDING = 0;
|
||||
const STATUS_RESOLVED = 1;
|
||||
|
@ -113,7 +114,128 @@ const Name = (n) => "{private:" + n + ":" + salt + "}";
|
|||
const N_STATUS = Name("status");
|
||||
const N_VALUE = Name("value");
|
||||
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
|
||||
// reported (possibly redundantly) so as to let programmers fix their code.
|
||||
const ERRORS_TO_REPORT = ["EvalError", "RangeError", "ReferenceError", "TypeError"];
|
||||
|
@ -283,6 +405,13 @@ this.PromiseWalker = {
|
|||
aPromise[N_VALUE] = aValue;
|
||||
if (aPromise[N_HANDLERS].length > 0) {
|
||||
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: [] });
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
|
@ -511,6 +649,15 @@ PromiseImpl.prototype = {
|
|||
// Ensure the handler is scheduled for processing if this promise is already
|
||||
// resolved or rejected.
|
||||
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);
|
||||
}
|
||||
|
||||
|
@ -588,12 +735,15 @@ Handler.prototype = {
|
|||
// users to see it. Also, if the programmer handles errors correctly,
|
||||
// they will either treat the error or log them somewhere.
|
||||
|
||||
dump("*************************\n");
|
||||
dump("A coding exception was thrown in a Promise " +
|
||||
((nextStatus == STATUS_RESOLVED) ? "resolution":"rejection") +
|
||||
" callback.\n");
|
||||
" callback.\n\n");
|
||||
dump("Full message: " + ex + "\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("*************************\n");
|
||||
|
||||
}
|
||||
|
||||
// Additionally, reject the next promise.
|
||||
|
|
|
@ -291,9 +291,11 @@ TaskImpl.prototype = {
|
|||
// they will either treat the error or log them somewhere.
|
||||
|
||||
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 stack: " + stack + "\n");
|
||||
dump("*************************\n");
|
||||
}
|
||||
|
||||
this.deferred.reject(aException);
|
||||
|
|
|
@ -33,7 +33,7 @@ let run_promise_tests = function run_promise_tests(tests, cb) {
|
|||
|
||||
let make_promise_test = function(test) {
|
||||
return function runtest() {
|
||||
do_print("Test starting: " + test);
|
||||
do_print("Test starting: " + test.name);
|
||||
try {
|
||||
let result = test();
|
||||
if (result && "promise" in result) {
|
||||
|
@ -42,7 +42,7 @@ let make_promise_test = function(test) {
|
|||
if (!result || !("then" in result)) {
|
||||
let exn;
|
||||
try {
|
||||
do_throw("Test " + test + " did not return a promise: " + result);
|
||||
do_throw("Test " + test.name + " did not return a promise: " + result);
|
||||
} catch (x) {
|
||||
exn = x;
|
||||
}
|
||||
|
@ -52,7 +52,7 @@ let make_promise_test = function(test) {
|
|||
result = result.then(
|
||||
// Test complete
|
||||
function onResolve() {
|
||||
do_print("Test complete: " + test);
|
||||
do_print("Test complete: " + test.name);
|
||||
},
|
||||
// The test failed with an unexpected error
|
||||
function onReject(err) {
|
||||
|
@ -62,13 +62,13 @@ let make_promise_test = function(test) {
|
|||
} else {
|
||||
detail = "(no stack)";
|
||||
}
|
||||
do_throw("Test " + test + " rejected with the following reason: "
|
||||
do_throw("Test " + test.name + " rejected with the following reason: "
|
||||
+ err + detail);
|
||||
});
|
||||
return result;
|
||||
} catch (x) {
|
||||
// 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();
|
||||
}
|
||||
};
|
||||
|
@ -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()
|
||||
{
|
||||
|
|
|
@ -20,3 +20,4 @@ support-files =
|
|||
[test_task.js]
|
||||
[test_TelemetryTimestamps.js]
|
||||
[test_timer.js]
|
||||
|
||||
|
|
Загрузка…
Ссылка в новой задаче