This commit is contained in:
Phil Ringnalda 2013-10-13 10:29:45 -07:00
Родитель 1929e75948 c815374ea9
Коммит 05ae259d3c
64 изменённых файлов: 2426 добавлений и 602 удалений

Просмотреть файл

@ -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);
}

636
mobile/android/base/favicons/cache/FaviconCache.java поставляемый Normal file
Просмотреть файл

@ -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();
}
}
}

115
mobile/android/base/favicons/cache/FaviconCacheElement.java поставляемый Normal file
Просмотреть файл

@ -0,0 +1,115 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.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);
}
}
}

146
mobile/android/base/favicons/cache/FaviconsForURL.java поставляемый Normal file
Просмотреть файл

@ -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]