diff --git a/embedding/android/Favicons.java b/embedding/android/Favicons.java new file mode 100644 index 00000000000..b1c5899b352 --- /dev/null +++ b/embedding/android/Favicons.java @@ -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 + * + * 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 { + 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); + } + } +} diff --git a/embedding/android/GeckoApp.java b/embedding/android/GeckoApp.java index 0ab8c269183..439a959eaf3 100644 --- a/embedding/android/GeckoApp.java +++ b/embedding/android/GeckoApp.java @@ -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(); } diff --git a/embedding/android/Makefile.in b/embedding/android/Makefile.in index 2f611242b09..3ef451e72f8 100644 --- a/embedding/android/Makefile.in +++ b/embedding/android/Makefile.in @@ -53,6 +53,7 @@ JAVAFILES = \ ConfirmPreference.java \ DoorHanger.java \ DoorHangerPopup.java \ + Favicons.java \ GeckoApp.java \ GeckoAppShell.java \ GeckoConnectivityReceiver.java \ diff --git a/embedding/android/Tab.java b/embedding/android/Tab.java index 308f8f4facf..2c1d9ce0906 100644 --- a/embedding/android/Tab.java +++ b/embedding/android/Tab.java @@ -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 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(); @@ -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 { - 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 { @Override protected Boolean doInBackground(Void... unused) {