Bug 941868 - Part 2: load and cache certain preloaded favicons on launch, and remove favicon from about:home's HTML content. r=mcomella
authorRichard Newman <rnewman@mozilla.com>
Tue, 26 Nov 2013 19:48:30 -0800
changeset 157727 b5a3121b05c11057ba3e145789a786ec9d43a563
parent 157726 b8931ca74b85392e297c229bb44766a425d4faac
child 157728 cffd41d1d7e8226f4ff014108e6cf107b62becff
push id25721
push usercbook@mozilla.com
push dateWed, 27 Nov 2013 10:02:03 +0000
treeherdermozilla-central@6ecf0c4dfcbe [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmcomella
bugs941868
milestone28.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 941868 - Part 2: load and cache certain preloaded favicons on launch, and remove favicon from about:home's HTML content. r=mcomella
mobile/android/base/AboutPages.java
mobile/android/base/BrowserApp.java
mobile/android/base/GeckoApp.java
mobile/android/base/Tabs.java
mobile/android/base/favicons/Favicons.java
mobile/android/base/toolbar/BrowserToolbar.java
mobile/android/chrome/content/aboutHome.xhtml
--- a/mobile/android/base/AboutPages.java
+++ b/mobile/android/base/AboutPages.java
@@ -34,10 +34,43 @@ public class AboutPages {
     }
 
     public static final boolean isAboutReader(final String url) {
         if (url == null) {
             return false;
         }
         return url.startsWith(READER);
     }
+
+    private static final String[] DEFAULT_ICON_PAGES = new String[] {
+        HOME,
+
+        ADDONS,
+        CONFIG,
+        DOWNLOADS,
+        FIREFOX,
+        HEALTHREPORT,
+        UPDATER
+    };
+
+    /**
+     * Callers must not modify the returned array.
+     */
+    public static String[] getDefaultIconPages() {
+        return DEFAULT_ICON_PAGES;
+    }
+
+    public static boolean isDefaultIconPage(final String url) {
+        if (url == null ||
+            !url.startsWith("about:")) {
+            return false;
+        }
+
+        // TODO: it'd be quicker to not compare the "about:" part every time.
+        for (int i = 0; i < DEFAULT_ICON_PAGES.length; ++i) {
+            if (DEFAULT_ICON_PAGES[i].equals(url)) {
+                return true;
+            }
+        }
+        return false;
+    }
 }
 
--- a/mobile/android/base/BrowserApp.java
+++ b/mobile/android/base/BrowserApp.java
@@ -1224,17 +1224,17 @@ abstract public class BrowserApp extends
             }
         } catch (Exception e) {
             Log.e(LOGTAG, "Exception handling message \"" + event + "\":", e);
         }
     }
 
     @Override
     public void addTab() {
-        Tabs.getInstance().loadUrl(AboutPages.HOME, Tabs.LOADURL_NEW_TAB);
+        super.loadHomePage(Tabs.LOADURL_NEW_TAB);
     }
 
     @Override
     public void addPrivateTab() {
         Tabs.getInstance().loadUrl(AboutPages.PRIVATEBROWSING, Tabs.LOADURL_NEW_TAB | Tabs.LOADURL_PRIVATE);
     }
 
     @Override
@@ -1396,17 +1396,17 @@ abstract public class BrowserApp extends
         mBrowserToolbar.cancelEdit();
     }
 
     private boolean isHomePagerVisible() {
         return (mHomePager != null && mHomePager.isVisible());
     }
 
     private void openReadingList() {
-        Tabs.getInstance().loadUrl(AboutPages.HOME, Tabs.LOADURL_READING_LIST);
+        super.loadHomePage(Tabs.LOADURL_READING_LIST);
     }
 
     /* Favicon stuff. */
     private static OnFaviconLoadedListener sFaviconLoadedListener = 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.
             Tabs.getInstance()
--- a/mobile/android/base/GeckoApp.java
+++ b/mobile/android/base/GeckoApp.java
@@ -1349,26 +1349,34 @@ abstract public class GeckoApp
      * (about:home) be loaded.
      *
      * @param url External URL to load, or null to load the default URL
      */
     protected void loadStartupTab(String url) {
         if (url == null) {
             if (!mShouldRestore) {
                 // Show about:home if we aren't restoring previous session and
-                // there's no external URL
-                Tab tab = Tabs.getInstance().loadUrl(AboutPages.HOME, Tabs.LOADURL_NEW_TAB);
+                // there's no external URL.
+                loadHomePage(Tabs.LOADURL_NEW_TAB);
             }
         } else {
             // If given an external URL, load it
             int flags = Tabs.LOADURL_NEW_TAB | Tabs.LOADURL_USER_ENTERED | Tabs.LOADURL_EXTERNAL;
             Tabs.getInstance().loadUrl(url, flags);
         }
     }
 
+    protected Tab loadHomePage() {
+        return loadHomePage(Tabs.LOADURL_NONE);
+    }
+
+    protected Tab loadHomePage(int flags) {
+        return Tabs.getInstance().loadUrl(AboutPages.HOME, flags);
+    }
+
     private void initialize() {
         mInitialized = true;
 
         if (Build.VERSION.SDK_INT >= 11) {
             // Create the panel and inflate the custom menu.
             onCreatePanelMenu(Window.FEATURE_OPTIONS_PANEL, null);
         }
         invalidateOptionsMenu();
--- a/mobile/android/base/Tabs.java
+++ b/mobile/android/base/Tabs.java
@@ -655,18 +655,18 @@ public class Tabs implements GeckoEventL
         return getTab(tabId);
     }
 
     /**
      * Loads a tab with the given URL in the currently selected tab.
      *
      * @param url URL of page to load, or search term used if searchEngine is given
      */
-    public void loadUrl(String url) {
-        loadUrl(url, LOADURL_NONE);
+    public Tab loadUrl(String url) {
+        return loadUrl(url, LOADURL_NONE);
     }
 
     /**
      * Loads a tab with the given URL.
      *
      * @param url   URL of page to load, or search term used if searchEngine is given
      * @param flags flags used to load tab
      *
@@ -726,24 +726,45 @@ public class Tabs implements GeckoEventL
                 added.setDesktopMode(desktopMode);
             }
         } catch (Exception e) {
             Log.w(LOGTAG, "Error building JSON arguments for loadUrl.", e);
         }
 
         GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Tab:Load", args.toString()));
 
-        if ((added != null) && !delayLoad && !background) {
+        if (added == null) {
+            return null;
+        }
+
+        if (!delayLoad && !background) {
             selectTab(added.getId());
         }
 
+        // TODO: surely we could just fetch *any* cached icon?
+        if (AboutPages.isDefaultIconPage(url)) {
+            Log.d(LOGTAG, "Setting about: tab favicon inline.");
+            added.updateFavicon(getAboutPageFavicon(url));
+        }
+
         return added;
     }
 
     /**
+     * These favicons are only used for the URL bar, so
+     * we fetch with that size.
+     *
+     * This method completes on the calling thread.
+     */
+    private Bitmap getAboutPageFavicon(final String url) {
+        int faviconSize = Math.round(mAppContext.getResources().getDimension(R.dimen.browser_toolbar_favicon_size));
+        return Favicons.getCachedFaviconForSize(url, faviconSize);
+    }
+
+    /**
      * Open the url as a new tab, and mark the selected tab as its "parent".
      *
      * If the url is already open in a tab, the existing tab is selected.
      * Use this for tabs opened by the browser chrome, so users can press the
      * "Back" button to return to the previous tab.
      *
      * @param url URL of page to load
      */
--- a/mobile/android/base/favicons/Favicons.java
+++ b/mobile/android/base/favicons/Favicons.java
@@ -1,42 +1,50 @@
 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
  * 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;
 
 import org.mozilla.gecko.AboutPages;
+import org.mozilla.gecko.AppConstants;
 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.GeckoJarReader;
+import org.mozilla.gecko.util.NonEvictingLruCache;
 import org.mozilla.gecko.util.ThreadUtils;
-import org.mozilla.gecko.util.NonEvictingLruCache;
 
 import android.content.Context;
+import android.content.res.Resources;
 import android.graphics.Bitmap;
 import android.graphics.BitmapFactory;
 import android.text.TextUtils;
 import android.util.Log;
 
+import java.io.File;
 import java.net.URI;
 import java.net.URISyntaxException;
+import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.Iterator;
 import java.util.Map;
 import java.util.Set;
 
 public class Favicons {
     private static final String LOGTAG = "GeckoFavicons";
 
+    // A magic URL representing the app's own favicon, used for about: pages.
+    private static final String BUILT_IN_FAVICON_URL = "about:favicon";
+
     // 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 NUM_PAGE_URL_MAPPINGS_TO_STORE = 128;
 
     public static final int NOT_LOADING  = 0;
     public static final int LOADED       = 1;
@@ -93,33 +101,52 @@ public class Favicons {
             public void run() {
                 listener.onFaviconLoaded(pageUrl, faviconURL, image);
             }
         });
         return NOT_LOADING;
     }
 
     /**
+     * Only returns a non-null Bitmap if the entire path is cached -- the
+     * page URL to favicon URL, and the favicon URL to in-memory bitmaps.
+     *
+     * Returns null otherwise.
+     */
+    public static Bitmap getCachedFaviconForSize(final String pageURL, int targetSize) {
+        final String faviconURL = sPageURLMappings.get(pageURL);
+        if (faviconURL == null) {
+            return null;
+        }
+        return getSizedFaviconFromCache(faviconURL, targetSize);
+    }
+
+    /**
      * 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, or
      *         LOADED if the value could be dispatched on the current thread.
      */
     public static int getFaviconForSize(String pageURL, String faviconURL, int targetSize, int flags, OnFaviconLoadedListener listener) {
+        // Do we know the favicon URL for this page already?
+        String cacheURL = faviconURL;
+        if (cacheURL == null) {
+            cacheURL = sPageURLMappings.get(pageURL);
+        }
+
         // 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) {
             return dispatchResult(pageURL, null, sDefaultFavicon, listener);
         }
@@ -323,26 +350,59 @@ public class Favicons {
     /**
      * 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 {
+        final Resources res = context.getResources();
         sContext = context;
 
         // Decode the default Favicon ready for use.
-        sDefaultFavicon = BitmapFactory.decodeResource(context.getResources(), R.drawable.favicon);
+        sDefaultFavicon = BitmapFactory.decodeResource(res, R.drawable.favicon);
         if (sDefaultFavicon == null) {
             throw new Exception("Null default favicon was returned from the resources system!");
         }
 
-        sDefaultFaviconSize = context.getResources().getDimensionPixelSize(R.dimen.favicon_bg);
-        sFaviconsCache = new FaviconCache(FAVICON_CACHE_SIZE_BYTES, context.getResources().getDimensionPixelSize(R.dimen.favicon_largest_interesting_size));
+        sDefaultFaviconSize = res.getDimensionPixelSize(R.dimen.favicon_bg);
+        sFaviconsCache = new FaviconCache(FAVICON_CACHE_SIZE_BYTES, res.getDimensionPixelSize(R.dimen.favicon_largest_interesting_size));
+
+        // Initialize page mappings for each of our special pages.
+        for (String url : AboutPages.getDefaultIconPages()) {
+            sPageURLMappings.putWithoutEviction(url, BUILT_IN_FAVICON_URL);
+        }
+
+        // Load and cache the built-in favicon in each of its sizes.
+        // TODO: don't open the zip twice!
+        ArrayList<Bitmap> toInsert = new ArrayList<Bitmap>(2);
+        toInsert.add(loadBrandingBitmap(context, "favicon64.png"));
+        toInsert.add(loadBrandingBitmap(context, "favicon32.png"));
+        putFaviconsInMemCache(BUILT_IN_FAVICON_URL, toInsert.iterator());
+    }
+
+    /**
+     * Compute a string like:
+     * "jar:jar:file:///data/app/org.mozilla.firefox-1.apk!/assets/omni.ja!/chrome/chrome/content/branding/favicon64.png"
+     */
+    private static String getBrandingBitmapPath(Context context, String name) {
+        final String apkPath = context.getPackageResourcePath();
+        return "jar:jar:" + new File(apkPath).toURI() + "!/" +
+               AppConstants.OMNIJAR_NAME + "!/" +
+               "chrome/chrome/content/branding/" + name;
+    }
+
+    private static Bitmap loadBrandingBitmap(Context context, String name) {
+        Bitmap b = GeckoJarReader.getBitmap(context.getResources(),
+                                            getBrandingBitmapPath(context, name));
+        if (b == null) {
+            throw new IllegalStateException("Bitmap " + name + " missing from JAR!");
+        }
+        return b;
     }
 
     /**
      * 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.
      */
--- a/mobile/android/base/toolbar/BrowserToolbar.java
+++ b/mobile/android/base/toolbar/BrowserToolbar.java
@@ -110,18 +110,22 @@ public class BrowserToolbar extends Geck
     private ImageView mUrlBarRightEdge;
     private GeckoTextView mTitle;
     private int mTitlePadding;
     private boolean mSiteSecurityVisible;
     private boolean mSwitchingTabs;
     private ShapedButton mTabs;
     private ImageButton mBack;
     private ImageButton mForward;
+    private ImageButton mStop;
+
+    // To de-bounce sets.
+    private Bitmap mLastFavicon;
     private ImageButton mFavicon;
-    private ImageButton mStop;
+
     private ImageButton mSiteSecurity;
     private PageActionLayout mPageActionLayout;
     private Animation mProgressSpinner;
     private TabCounter mTabsCounter;
     private GeckoImageButton mMenu;
     private GeckoImageView mMenuIcon;
     private LinearLayout mActionItemBar;
     private MenuPopup mMenuPopup;
@@ -423,17 +427,17 @@ public class BrowserToolbar extends Geck
                 SiteIdentityPopup siteIdentityPopup = mActivity.getSiteIdentityPopup();
                 siteIdentityPopup.updateIdentity(identityData);
                 siteIdentityPopup.show();
             }
         };
 
         mFavicon.setOnClickListener(faviconListener);
         mSiteSecurity.setOnClickListener(faviconListener);
-        
+
         mStop.setOnClickListener(new Button.OnClickListener() {
             @Override
             public void onClick(View v) {
                 Tab tab = Tabs.getInstance().getSelectedTab();
                 if (tab != null)
                     tab.doStop();
                 setProgressVisibility(false);
             }
@@ -736,22 +740,25 @@ public class BrowserToolbar extends Geck
 
         // Update A11y information
         mTabs.setContentDescription((count > 1) ?
                                     mActivity.getString(R.string.num_tabs, count) :
                                     mActivity.getString(R.string.one_tab));
     }
 
     public void setProgressVisibility(boolean visible) {
+        Log.d(LOGTAG, "setProgressVisibility: " + visible);
         // The "Throbber start" and "Throbber stop" log messages in this method
         // are needed by S1/S2 tests (http://mrcote.info/phonedash/#).
         // See discussion in Bug 804457. Bug 805124 tracks paring these down.
         if (visible) {
             mFavicon.setImageResource(R.drawable.progress_spinner);
-            //To stop the glitch caused by mutiple start() calls.
+            mLastFavicon = null;
+
+            // To stop the glitch caused by multiple start() calls.
             if (!mSpinnerVisible) {
                 setPageActionVisibility(true);
                 mFavicon.setAnimation(mProgressSpinner);
                 mProgressSpinner.start();
                 mSpinnerVisible = true;
             }
             Log.i(LOGTAG, "zerdatime " + SystemClock.uptimeMillis() + " - Throbber start");
         } else {
@@ -863,25 +870,25 @@ public class BrowserToolbar extends Geck
         mUrlEditLayout.onEditSuggestion(suggestion);
     }
 
     public void setTitle(CharSequence title) {
         mTitle.setText(title);
         setContentDescription(title != null ? title : mTitle.getHint());
     }
 
-    // Sets the toolbar title according to the selected tab, obeying the mShowUrl prference.
+    // Sets the toolbar title according to the selected tab, obeying the mShowUrl preference.
     private void updateTitle() {
-        Tab tab = Tabs.getInstance().getSelectedTab();
+        final Tab tab = Tabs.getInstance().getSelectedTab();
         // Keep the title unchanged if there's no selected tab, or if the tab is entering reader mode.
         if (tab == null || tab.isEnteringReaderMode()) {
             return;
         }
 
-        String url = tab.getURL();
+        final String url = tab.getURL();
 
         if (!isEditing()) {
             mUrlEditLayout.setText(url);
         }
 
         // Setting a null title will ensure we just see the "Enter Search or Address" placeholder text.
         if (AboutPages.isTitlelessAboutPage(url)) {
             setTitle(null);
@@ -918,27 +925,36 @@ public class BrowserToolbar extends Geck
                 title = builder;
             }
         }
 
         setTitle(title);
     }
 
     private void setFavicon(Bitmap image) {
-        if (Tabs.getInstance().getSelectedTab().getState() == Tab.STATE_LOADING)
+        Log.d(LOGTAG, "setFavicon(" + image + ")");
+        if (Tabs.getInstance().getSelectedTab().getState() == Tab.STATE_LOADING) {
             return;
+        }
+
+        if (image == mLastFavicon) {
+            Log.d(LOGTAG, "Ignoring favicon set: new favicon is identical to previous favicon.");
+            return;
+        }
+
+        mLastFavicon = image;     // Cache the original so we can debounce without scaling.
 
         if (image != null) {
             image = Bitmap.createScaledBitmap(image, mFaviconSize, mFaviconSize, false);
             mFavicon.setImageBitmap(image);
         } else {
             mFavicon.setImageDrawable(null);
         }
     }
-    
+
     private void setSecurityMode(String mode) {
         int imageLevel = SiteIdentityPopup.getSecurityImageLevel(mode);
         mSiteSecurity.setImageLevel(imageLevel);
         mShowSiteSecurity = (imageLevel != SiteIdentityPopup.LEVEL_UKNOWN);
 
         setPageActionVisibility(mStop.getVisibility() == View.VISIBLE);
     }
 
--- a/mobile/android/chrome/content/aboutHome.xhtml
+++ b/mobile/android/chrome/content/aboutHome.xhtml
@@ -11,13 +11,12 @@
 
 <!-- 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/. -->
 
 <html xmlns="http://www.w3.org/1999/xhtml">
 <head>
   <title>&abouthome.title;</title>
-  <link rel="icon" type="image/png" sizes="64x64" href="chrome://branding/content/favicon64.png" />
 </head>
 <body>
 </body>
 </html>