Bug 697194 - Implement favicon caching (r=blassey)

This commit is contained in:
Lucas Rocha 2011-11-07 13:55:25 +00:00
parent 20d209feab
commit f9d12fc065
4 changed files with 371 additions and 60 deletions

View File

@ -0,0 +1,313 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
* ***** BEGIN LICENSE BLOCK *****
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
*
* The contents of this file are subject to the Mozilla Public License Version
* 1.1 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
* http://www.mozilla.org/MPL/
*
* Software distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
* for the specific language governing rights and limitations under the
* License.
*
* The Original Code is Mozilla Android code.
*
* The Initial Developer of the Original Code is Mozilla Foundation.
* Portions created by the Initial Developer are Copyright (C) 2011
* the Initial Developer. All Rights Reserved.
*
* Contributor(s):
* Lucas Rocha <lucasr@mozilla.com>
*
* Alternatively, the contents of this file may be used under the terms of
* either the GNU General Public License Version 2 or later (the "GPL"), or
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
* in which case the provisions of the GPL or the LGPL are applicable instead
* of those above. If you wish to allow use of your version of this file only
* under the terms of either the GPL or the LGPL, and not to allow others to
* use your version of this file under the terms of the MPL, indicate your
* decision by deleting the provisions above and replace them with the notice
* and other provisions required by the GPL or the LGPL. If you do not delete
* the provisions above, a recipient may use your version of this file under
* the terms of any one of the MPL, the GPL or the LGPL.
*
* ***** END LICENSE BLOCK ***** */
package org.mozilla.gecko;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.database.sqlite.SQLiteQueryBuilder;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.os.AsyncTask;
import android.provider.Browser;
import android.util.Log;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.HashMap;
public class Favicons {
private static final String LOG_NAME = "Favicons";
private Context mContext;
private DatabaseHelper mDbHelper;
public interface OnFaviconLoadedListener {
public void onFaviconLoaded(String url, Drawable favicon);
}
private class DatabaseHelper extends SQLiteOpenHelper {
private static final String DATABASE_NAME = "favicon_urls.db";
private static final String TABLE_NAME = "favicon_urls";
private static final int DATABASE_VERSION = 1;
private static final String COLUMN_ID = "_id";
private static final String COLUMN_FAVICON_URL = "favicon_url";
private static final String COLUMN_PAGE_URL = "page_url";
DatabaseHelper(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
Log.d(LOG_NAME, "Creating DatabaseHelper");
}
@Override
public void onCreate(SQLiteDatabase db) {
Log.d(LOG_NAME, "Creating database for favicon URLs");
db.execSQL("CREATE TABLE " + TABLE_NAME + " (" +
COLUMN_ID + " INTEGER PRIMARY KEY," +
COLUMN_FAVICON_URL + " TEXT NOT NULL," +
COLUMN_PAGE_URL + " TEXT UNIQUE NOT NULL" +
");");
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
Log.w(LOG_NAME, "Upgrading favicon URLs database from version " +
oldVersion + " to " + newVersion + ", which will destroy all old data");
// Drop table completely
db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME);
// Recreate database
onCreate(db);
}
public String getFaviconUrlForPageUrl(String pageUrl) {
Log.d(LOG_NAME, "Calling getFaviconUrlForPageUrl() for " + pageUrl);
SQLiteDatabase db = mDbHelper.getReadableDatabase();
SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
qb.setTables(TABLE_NAME);
Cursor c = qb.query(
db,
new String[] { COLUMN_FAVICON_URL },
COLUMN_PAGE_URL + " = ?",
new String[] { pageUrl },
null, null, null
);
if (!c.moveToFirst())
return null;
return c.getString(c.getColumnIndexOrThrow(COLUMN_FAVICON_URL));
}
public void setFaviconUrlForPageUrl(String pageUrl, String faviconUrl) {
Log.d(LOG_NAME, "Calling setFaviconUrlForPageUrl() for " + pageUrl);
SQLiteDatabase db = mDbHelper.getWritableDatabase();
ContentValues values = new ContentValues();
values.put(COLUMN_FAVICON_URL, faviconUrl);
values.put(COLUMN_PAGE_URL, pageUrl);
db.replace(TABLE_NAME, null, values);
}
}
public Favicons(Context context) {
Log.d(LOG_NAME, "Creating Favicons instance");
mContext = context;
mDbHelper = new DatabaseHelper(context);
}
public void loadFavicon(String pageUrl, String faviconUrl,
OnFaviconLoadedListener listener) {
Log.d(LOG_NAME, "Calling loadFavicon() with URL = " + pageUrl +
" and favicon URL = " + faviconUrl);
// Handle the case where page url is empty
if (pageUrl == null || pageUrl.length() == 0) {
if (listener != null)
listener.onFaviconLoaded(null, null);
}
new LoadFaviconTask(pageUrl, faviconUrl, listener).execute();
}
public void close() {
Log.d(LOG_NAME, "Closing Favicons database");
mDbHelper.close();
}
private class LoadFaviconTask extends AsyncTask<Void, Void, BitmapDrawable> {
private String mPageUrl;
private String mFaviconUrl;
private OnFaviconLoadedListener mListener;
public LoadFaviconTask(String pageUrl, String faviconUrl, OnFaviconLoadedListener listener) {
mPageUrl = pageUrl;
mFaviconUrl = faviconUrl;
mListener = listener;
Log.d(LOG_NAME, "Creating LoadFaviconTask with URL = " + pageUrl +
" and favicon URL = " + faviconUrl);
}
// Runs in background thread
private BitmapDrawable loadFaviconFromDb() {
Log.d(LOG_NAME, "Loading favicon from DB for URL = " + mPageUrl);
ContentResolver resolver = mContext.getContentResolver();
Cursor c = resolver.query(Browser.BOOKMARKS_URI,
new String[] { Browser.BookmarkColumns.FAVICON },
Browser.BookmarkColumns.URL + " = ?",
new String[] { mPageUrl },
null);
if (!c.moveToFirst())
return null;
int faviconIndex = c.getColumnIndexOrThrow(Browser.BookmarkColumns.FAVICON);
byte[] b = c.getBlob(faviconIndex);
if (b == null)
return null;
Bitmap bitmap = BitmapFactory.decodeByteArray(b, 0, b.length);
Log.d(LOG_NAME, "Loaded favicon from DB successfully for URL = " + mPageUrl);
return new BitmapDrawable(bitmap);
}
// Runs in background thread
private void saveFaviconToDb(BitmapDrawable favicon) {
Bitmap bitmap = favicon.getBitmap();
ByteArrayOutputStream stream = new ByteArrayOutputStream();
bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream);
ContentValues values = new ContentValues();
values.put(Browser.BookmarkColumns.FAVICON, stream.toByteArray());
ContentResolver resolver = mContext.getContentResolver();
Log.d(LOG_NAME, "Saving favicon on browser database for URL = " + mPageUrl);
resolver.update(Browser.BOOKMARKS_URI,
values,
Browser.BookmarkColumns.URL + " = ?",
new String[] { mPageUrl });
Log.d(LOG_NAME, "Saving favicon URL for URL = " + mPageUrl);
mDbHelper.setFaviconUrlForPageUrl(mPageUrl, mFaviconUrl);
}
// Runs in background thread
private BitmapDrawable downloadFavicon(URL faviconUrl) {
Log.d(LOG_NAME, "Downloading favicon for URL = " + mPageUrl +
" with favicon URL = " + mFaviconUrl);
BitmapDrawable image = null;
try {
InputStream is = (InputStream) faviconUrl.getContent();
image = (BitmapDrawable) Drawable.createFromStream(is, "src");
} catch (IOException e) {
Log.d(LOG_NAME, "Error downloading favicon: " + e);
}
if (image != null) {
Log.d(LOG_NAME, "Downloaded favicon successfully for URL = " + mPageUrl);
saveFaviconToDb(image);
}
return image;
}
@Override
protected BitmapDrawable doInBackground(Void... unused) {
BitmapDrawable image = null;
URL pageUrl = null;
// Handle the case of malformed URL
try {
pageUrl = new URL(mPageUrl);
} catch (MalformedURLException e) {
Log.d(LOG_NAME, "The provided URL is not valid: " + e);
return null;
}
URL faviconUrl = null;
// Handle the case of malformed favicon URL
try {
// If favicon is empty, fallback to default favicon URI
if (mFaviconUrl == null || mFaviconUrl.length() == 0) {
faviconUrl = new URL(pageUrl.getProtocol(), pageUrl.getAuthority(), "/favicon.ico");
mFaviconUrl = faviconUrl.toString();
} else {
faviconUrl = new URL(mFaviconUrl);
}
} catch (MalformedURLException e) {
Log.d(LOG_NAME, "The provided favicon URL is not valid: " + e);
return null;
}
Log.d(LOG_NAME, "Favicon URL is now: " + mFaviconUrl);
String storedFaviconUrl = mDbHelper.getFaviconUrlForPageUrl(mPageUrl);
if (storedFaviconUrl != null && storedFaviconUrl.equals(mFaviconUrl)) {
image = loadFaviconFromDb();
// If favicon URL is defined but the favicon image is not
// stored in the database for some reason, we force download.
if (image == null) {
image = downloadFavicon(faviconUrl);
}
} else {
image = downloadFavicon(faviconUrl);
}
return image;
}
@Override
protected void onPostExecute(BitmapDrawable image) {
Log.d(LOG_NAME, "LoadFaviconTask finished for URL = " + mPageUrl);
if (mListener != null)
mListener.onFaviconLoaded(mPageUrl, image);
}
}
}

View File

@ -99,6 +99,7 @@ abstract public class GeckoApp
private BroadcastReceiver mConnectivityReceiver;
private BrowserToolbar mBrowserToolbar;
public DoorHanger mDoorHanger;
public Favicons mFavicons;
private static boolean sIsGeckoReady = false;
private IntentFilter mBatteryFilter;
private BroadcastReceiver mBatteryReceiver;
@ -570,6 +571,35 @@ abstract public class GeckoApp
return true;
}
private void loadFavicon(final Tab tab) {
mFavicons.loadFavicon(tab.getURL(), tab.getFaviconURL(),
new Favicons.OnFaviconLoadedListener() {
public void onFaviconLoaded(String pageUrl, Drawable favicon) {
// Leave favicon UI untouched if we failed to load the image
// for some reason.
if (favicon == null)
return;
Log.i(LOG_NAME, "Favicon successfully loaded for URL = " + pageUrl);
// 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;
Log.i(LOG_NAME, "Favicon is for current URL = " + pageUrl);
tab.updateFavicon(favicon);
if (Tabs.getInstance().isSelectedTab(tab))
mBrowserToolbar.setFavicon(tab.getFavicon());
onTabsChanged();
}
});
}
void handleLocationChange(final int tabId, final String uri) {
final Tab tab = Tabs.getInstance().getTab(tabId);
if (tab == null)
@ -589,6 +619,7 @@ abstract public class GeckoApp
return;
tab.updateFavicon(null);
tab.updateFaviconURL(null);
tab.updateSecurityMode("unknown");
mMainHandler.post(new Runnable() {
@ -900,8 +931,7 @@ abstract public class GeckoApp
return;
tab.updateTitle(title);
if (tab.getFavicon() == null)
tab.downloadFavicon(null);
loadFavicon(tab);
mMainHandler.post(new Runnable() {
public void run() {
@ -931,15 +961,17 @@ abstract public class GeckoApp
void handleLinkAdded(final int tabId, String rel, final String href) {
if (rel.indexOf("icon") != -1) {
Tab tab = Tabs.getInstance().getTab(tabId);
if (tab != null)
tab.downloadFavicon(href);
}
}
if (tab != null) {
tab.updateFaviconURL(href);
void faviconUpdated(Tab tab) {
if (Tabs.getInstance().isSelectedTab(tab))
mBrowserToolbar.setFavicon(tab.getFavicon());
onTabsChanged();
// If tab is not loading and the favicon is updated, we
// want to load the image straight away. If tab is still
// loading, we only load the favicon once the page's content
// is fully loaded (see handleContentLoaded()).
if (!tab.isLoading())
loadFavicon(tab);
}
}
}
void addPluginView(final View view,
@ -1006,6 +1038,8 @@ abstract public class GeckoApp
setContentView(R.layout.gecko_app);
mAppContext = this;
mFavicons = new Favicons(this);
// setup gecko layout
mGeckoLayout = (RelativeLayout) findViewById(R.id.gecko_layout);
mMainLayout = (LinearLayout) findViewById(R.id.main_layout);
@ -1268,6 +1302,8 @@ abstract public class GeckoApp
GeckoAppShell.unregisterGeckoEventListener("Gecko:Ready", GeckoApp.mAppContext);
GeckoAppShell.unregisterGeckoEventListener("Toast:Show", GeckoApp.mAppContext);
mFavicons.close();
super.onDestroy();
}

View File

@ -53,6 +53,7 @@ JAVAFILES = \
ConfirmPreference.java \
DoorHanger.java \
DoorHangerPopup.java \
Favicons.java \
GeckoApp.java \
GeckoAppShell.java \
GeckoConnectivityReceiver.java \

View File

@ -63,13 +63,13 @@ public class Tab {
private String mUrl;
private String mTitle;
private Drawable mFavicon;
private String mFaviconUrl;
private String mSecurityMode;
private Drawable mThumbnail;
private List<HistoryEntry> mHistory;
private int mHistoryIndex;
private boolean mLoading;
private boolean mBookmark;
private DownloadFaviconTask mFaviconDownloader;
static class HistoryEntry {
public final String mUri;
@ -90,6 +90,7 @@ public class Tab {
mUrl = url;
mTitle = new String();
mFavicon = null;
mFaviconUrl = null;
mSecurityMode = "unknown";
mThumbnail = null;
mHistory = new ArrayList<HistoryEntry>();
@ -121,6 +122,10 @@ public class Tab {
return mFavicon;
}
public String getFaviconURL() {
return mFaviconUrl;
}
public String getSecurityMode() {
return mSecurityMode;
}
@ -182,6 +187,11 @@ public class Tab {
Log.i(LOG_NAME, "Updated favicon for tab with id: " + mId);
}
public void updateFaviconURL(String faviconUrl) {
mFaviconUrl = mFaviconUrl;
Log.i(LOG_NAME, "Updated favicon URL for tab with id: " + mId);
}
public void updateSecurityMode(String mode) {
mSecurityMode = mode;
}
@ -267,55 +277,6 @@ public class Tab {
}
}
void downloadFavicon(String url) {
if (url == null) {
try {
URL urlObj = new URL(mUrl);
url = urlObj.getProtocol() + "://" + urlObj.getAuthority() + "/favicon.ico";
} catch (MalformedURLException e) {
// Optional so not a real error
return;
}
}
try {
URL urlObj = new URL(url);
// note that the above line may throw a MalformedURLException,
// in which case we abort and don't cancel the old download task.
if (mFaviconDownloader != null) {
mFaviconDownloader.cancel(false);
Log.d(LOG_NAME, "Cancelled old favicon downloader");
}
mFaviconDownloader = new DownloadFaviconTask();
mFaviconDownloader.execute(urlObj);
} catch (MalformedURLException e) {
}
}
private class DownloadFaviconTask extends AsyncTask<URL, Void, Drawable> {
protected Drawable doInBackground(URL... args) {
Drawable image = null;
try {
URL url = args[0];
InputStream is = (InputStream) url.getContent();
image = Drawable.createFromStream(is, "src");
} catch (IOException e) {
Log.d(LOG_NAME, "Error loading favicon: " + e);
}
return image;
}
protected void onPostExecute(Drawable image) {
if (image == null)
return;
updateFavicon(image);
GeckoApp.mAppContext.faviconUpdated(Tab.this);
}
}
private class CheckBookmarkTask extends AsyncTask<Void, Void, Boolean> {
@Override
protected Boolean doInBackground(Void... unused) {