author | Carsten "Tomcat" Book <cbook@mozilla.com> |
Wed, 27 Nov 2013 11:01:25 +0100 | |
changeset 157732 | 6ecf0c4dfcbe6b04640713afa29eba044b6c0475 |
parent 157720 | aeaac4c8fbedd084564ccbf8ab5c9c25cff9c5b4 (current diff) |
parent 157731 | 9f446888b2d2318bde2761089bfc4a01a1c811c6 (diff) |
child 157764 | 6d9e485ede928e818504b5354bc5d838239c8a72 |
child 157814 | 61bf4acd28efa9aa7d71e3735ae1a77ea84faba1 |
push id | 25721 |
push user | cbook@mozilla.com |
push date | Wed, 27 Nov 2013 10:02:03 +0000 |
treeherder | mozilla-central@6ecf0c4dfcbe [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
milestone | 28.0a1 |
first release with | nightly linux32
6ecf0c4dfcbe
/
28.0a1
/
20131127030201
/
files
nightly linux64
6ecf0c4dfcbe
/
28.0a1
/
20131127030201
/
files
nightly mac
6ecf0c4dfcbe
/
28.0a1
/
20131127030201
/
files
nightly win32
6ecf0c4dfcbe
/
28.0a1
/
20131127030201
/
files
nightly win64
6ecf0c4dfcbe
/
28.0a1
/
20131127030201
/
files
|
last release without | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
releases | nightly linux32
28.0a1
/
20131127030201
/
pushlog to previous
nightly linux64
28.0a1
/
20131127030201
/
pushlog to previous
nightly mac
28.0a1
/
20131127030201
/
pushlog to previous
nightly win32
28.0a1
/
20131127030201
/
pushlog to previous
nightly win64
28.0a1
/
20131127030201
/
pushlog to previous
|
--- a/browser/base/content/browser-charsetmenu.inc +++ b/browser/base/content/browser-charsetmenu.inc @@ -1,41 +1,62 @@ # 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/. #filter substitution #expand <menu id="__ID_PREFIX__charsetMenu" label="&charsetMenu.label;" +#ifndef OMIT_ACCESSKEYS accesskey="&charsetMenu.accesskey;" +#endif oncommand="MultiplexHandler(event)" +#ifdef OMIT_ACCESSKEYS onpopupshowing="CharsetMenu.build(event);" +#else + onpopupshowing="CharsetMenu.build(event, true);" +#endif onpopupshown="UpdateMenus(event);"> <menupopup> <menu label="&charsetMenuAutodet.label;" - accesskey="&charsetMenuAutodet.accesskey;"> +#ifndef OMIT_ACCESSKEYS + accesskey="&charsetMenuAutodet.accesskey;" +#endif + > <menupopup> <menuitem type="radio" name="detectorGroup" - id="chardet.off" +#expand id="__ID_PREFIX__chardet.off" label="&charsetMenuAutodet.off.label;" - accesskey="&charsetMenuAutodet.off.accesskey;"/> +#ifndef OMIT_ACCESSKEYS + accesskey="&charsetMenuAutodet.off.accesskey;" +#endif + /> <menuitem type="radio" name="detectorGroup" - id="chardet.ja_parallel_state_machine" +#expand id="__ID_PREFIX__chardet.ja_parallel_state_machine" label="&charsetMenuAutodet.ja.label;" - accesskey="&charsetMenuAutodet.ja.accesskey;"/> +#ifndef OMIT_ACCESSKEYS + accesskey="&charsetMenuAutodet.ja.accesskey;" +#endif + /> <menuitem type="radio" name="detectorGroup" - id="chardet.ruprob" +#expand id="__ID_PREFIX__chardet.ruprob" label="&charsetMenuAutodet.ru.label;" - accesskey="&charsetMenuAutodet.ru.accesskey;"/> +#ifndef OMIT_ACCESSKEYS + accesskey="&charsetMenuAutodet.ru.accesskey;" +#endif + /> <menuitem type="radio" name="detectorGroup" - id="chardet.ukprob" +#expand id="__ID_PREFIX__chardet.ukprob" label="&charsetMenuAutodet.uk.label;" - accesskey="&charsetMenuAutodet.uk.accesskey;"/> +#ifndef OMIT_ACCESSKEYS + accesskey="&charsetMenuAutodet.uk.accesskey;" +#endif + /> </menupopup> </menu> <menuseparator/> </menupopup> </menu>
--- a/browser/components/customizableui/content/toolbar.xml +++ b/browser/components/customizableui/content/toolbar.xml @@ -373,16 +373,17 @@ <!-- This is a peculiar binding. It is here to deal with overlayed/inserted add-on content, and immediately direct such content elsewhere. --> <binding id="addonbar-delegating"> <implementation> <constructor><![CDATA[ // Reading these immediately so nobody messes with them anymore: this._delegatingToolbar = this.getAttribute("toolbar-delegate"); + this._wasCollapsed = this.getAttribute("collapsed"); // Leaving those in here to unbreak some code: if (document.readyState == "complete") { this._init(); } else { // Need to wait until XUL overlays are loaded. See bug 554279. let self = this; document.addEventListener("readystatechange", function onReadyStateChange() { if (document.readyState != "complete") @@ -404,18 +405,27 @@ // Hold on to the palette but remove it from the document. toolbox.palette = node; toolbox.removeChild(node); } } } // pass the current set of children for comparison with placements: - let children = [node.id for (node of this.childNodes) - if (node.getAttribute("skipintoolbarset") != "true" && node.id)]; + let children = []; + for (node of this.childNodes) { + if (node.getAttribute("skipintoolbarset") != "true" && node.id) { + // Force everything to be removable so that buildArea can chuck stuff + // out if the user has customized things / we've been here before: + if (!this._whiteListed.has(node.id)) { + node.setAttribute("removable", "true"); + } + children.push(node); + } + } CustomizableUI.registerToolbarNode(this, children); this.evictNodes(); // We can't easily use |this| or strong bindings for the observer fn here // because that creates leaky circular references when the node goes away, // and XBL destructors are unreliable. let mutationObserver = new MutationObserver(function(mutations) { if (!mutations.length) { return; @@ -450,30 +460,38 @@ <parameter name="aNode"/> <body> <![CDATA[ if (this._whiteListed.has(aNode.id) || CustomizableUI.isSpecialWidget(aNode.id)) { return; } const kItemMaxWidth = 100; let oldParent = aNode.parentNode; - - try { - aNode.setAttribute("removable", "true"); + aNode.setAttribute("removable", "true"); - let nodeWidth = aNode.getBoundingClientRect().width; - if (nodeWidth == 0 || nodeWidth > kItemMaxWidth) { - throw new Error(aNode.id + " is too big (" + nodeWidth + - "px wide), moving to the palette"); + let movedOut = false; + if (!this._wasCollapsed) { + try { + let nodeWidth = aNode.getBoundingClientRect().width; + if (nodeWidth == 0 || nodeWidth > kItemMaxWidth) { + throw new Error(aNode.id + " is too big (" + nodeWidth + + "px wide), moving to the palette"); + } + CustomizableUI.addWidgetToArea(aNode.id, this._delegatingToolbar); + movedOut = true; + } catch (ex) { + // This will throw if the node is too big, or can't be moved there for + // some reason. Report this: + Cu.reportError(ex); } - CustomizableUI.addWidgetToArea(aNode.id, this._delegatingToolbar); - } catch (ex) { - Cu.reportError(ex); - // This will throw if the node is too big, or can't be moved there for - // some reason. Try to remove it anyway: + } + + /* We won't have moved the widget if either the add-on bar was collapsed, + * or if it was too wide to be inserted into the navbar. */ + if (!movedOut) { try { CustomizableUI.removeWidgetFromArea(aNode.id); } catch (ex) { Cu.reportError(ex); aNode.remove(); } }
--- a/browser/devtools/sourceeditor/editor.js +++ b/browser/devtools/sourceeditor/editor.js @@ -233,19 +233,29 @@ Editor.prototype = { cm = win.CodeMirror(win.document.body, this.config); cm.getWrapperElement().addEventListener("contextmenu", (ev) => { ev.preventDefault(); this.showContextMenu(el.ownerDocument, ev.screenX, ev.screenY); }, false); cm.on("focus", () => this.emit("focus")); cm.on("change", () => this.emit("change")); - cm.on("gutterClick", (cm, line) => this.emit("gutterClick", line)); cm.on("cursorActivity", (cm) => this.emit("cursorActivity")); + cm.on("gutterClick", (cm, line, gutter, ev) => { + let head = { line: line, ch: 0 }; + let tail = { line: line, ch: this.getText(line).length }; + + // Shift-click on a gutter selects the whole line. + if (ev.shiftKey) + return void cm.setSelection(head, tail); + + this.emit("gutterClick", line); + }); + win.CodeMirror.defineExtension("l10n", (name) => { return L10N.GetStringFromName(name); }); cm.getInputField().controllers.insertControllerAt(0, controller(this)); this.container = env; editors.set(this, cm);
--- a/browser/devtools/sourceeditor/test/browser_editor_cursor.js +++ b/browser/devtools/sourceeditor/test/browser_editor_cursor.js @@ -26,11 +26,18 @@ function test() { is(ed.getSelection(), "Hello", "setSelection"); ed.extendSelection({ start: 0, length: 5 }); is(ed.getSelection(), ".\nHow", "extendSelection"); ed.dropSelection(); is(ed.getSelection(), "", "dropSelection"); + // Check that shift-click on a gutter selects the whole line (bug 919707) + let iframe = win.document.querySelector("iframe"); + let gutter = iframe.contentWindow.document.querySelector(".CodeMirror-gutters"); + + EventUtils.sendMouseEvent({ type: "mousedown", shiftKey: true }, gutter, iframe.contentWindow); + is(ed.getSelection(), "Hello.", "shift-click"); + teardown(ed, win); }); } \ No newline at end of file
--- a/browser/modules/CharsetMenu.jsm +++ b/browser/modules/CharsetMenu.jsm @@ -77,17 +77,17 @@ const kEncodings = new Set([ // Always at the start of the menu, in this order, followed by a separator. const kPinned = [ "UTF-8", "windows-1252" ]; this.CharsetMenu = Object.freeze({ - build: function BuildCharsetMenu(event) { + build: function BuildCharsetMenu(event, showAccessKeys) { let parent = event.target; if (parent.lastChild.localName != "menuseparator") { // Detector menu or charset menu already built return; } let doc = parent.ownerDocument; function createItem(encoding) { @@ -95,21 +95,23 @@ this.CharsetMenu = Object.freeze({ menuItem.setAttribute("type", "radio"); menuItem.setAttribute("name", "charsetGroup"); try { menuItem.setAttribute("label", gBundle.GetStringFromName(encoding)); } catch (e) { // Localization error but put *something* in the menu to recover. menuItem.setAttribute("label", encoding); } - try { - menuItem.setAttribute("accesskey", - gBundle.GetStringFromName(encoding + ".key")); - } catch (e) { - // Some items intentionally don't have an accesskey + if (showAccessKeys) { + try { + menuItem.setAttribute("accesskey", + gBundle.GetStringFromName(encoding + ".key")); + } catch (e) { + // Some items intentionally don't have an accesskey + } } menuItem.setAttribute("id", "charset." + encoding); return menuItem; } // Clone the set in order to be able to remove the pinned encodings from // the cloned set. let encodings = new Set(kEncodings);
--- 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 @@ -192,20 +192,34 @@ abstract public class BrowserApp extends if (mSiteIdentityPopup == null) mSiteIdentityPopup = new SiteIdentityPopup(this); return mSiteIdentityPopup; } @Override public void onTabChanged(Tab tab, Tabs.TabEvents msg, Object data) { + if (tab == null) { + // Only RESTORED is allowed a null tab: it's the only event that + // isn't tied to a specific tab. + if (msg != Tabs.TabEvents.RESTORED) { + throw new IllegalArgumentException("onTabChanged:" + msg + " must specify a tab."); + } + return; + } + + Log.d(LOGTAG, "BrowserApp.onTabChanged: " + tab.getId() + ": " + msg); switch(msg) { + // We don't get a LOCATION_CHANGE event for the first about:home + // load, because the previous and current URIs are the + // same. That means it's OK to trigger a new favicon load + // at this point. case LOCATION_CHANGE: if (Tabs.getInstance().isSelectedTab(tab)) { - maybeCancelFaviconLoad(tab); + loadFavicon(tab); } // fall through case SELECTED: if (Tabs.getInstance().isSelectedTab(tab)) { updateHomePagerForTab(tab); if (mSiteIdentityPopup != null) mSiteIdentityPopup.dismiss(); @@ -236,19 +250,16 @@ abstract public class BrowserApp extends break; case LOAD_ERROR: case STOP: case MENU_UPDATED: if (Tabs.getInstance().isSelectedTab(tab)) { invalidateOptionsMenu(); } break; - case PAGE_SHOW: - loadFavicon(tab); - break; case LINK_FAVICON: // 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. if (tab.getState() != Tab.STATE_LOADING) { loadFavicon(tab); } @@ -1224,17 +1235,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 +1407,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() @@ -1423,20 +1434,23 @@ abstract public class BrowserApp extends int flags = (tab.isPrivate() || tab.getErrorType() != Tab.ErrorType.NONE) ? 0 : LoadFaviconTask.FLAG_PERSIST; int id = Favicons.getFaviconForSize(tab.getURL(), tab.getFaviconURL(), tabFaviconSize, flags, sFaviconLoadedListener); tab.setFaviconLoadId(id); } private void maybeCancelFaviconLoad(Tab tab) { int faviconLoadId = tab.getFaviconLoadId(); - // Cancel pending favicon load task + if (Favicons.NOT_LOADING == faviconLoadId) { + return; + } + + // Cancel load task and reset favicon load state if it wasn't already + // in NOT_LOADING state. Favicons.cancelFaviconLoad(faviconLoadId); - - // Reset favicon load state tab.setFaviconLoadId(Favicons.NOT_LOADING); } /** * Enters editing mode with the current tab's URL. There might be no * tabs loaded by the time the user enters editing mode e.g. just after * the app starts. In this case, we simply fallback to an empty URL. */
--- 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/Tab.java +++ b/mobile/android/base/Tab.java @@ -622,29 +622,41 @@ public class Tab { mHistoryIndex = -1; } } void handleLocationChange(JSONObject message) throws JSONException { final String uri = message.getString("uri"); final String oldUrl = getURL(); mEnteringReaderMode = ReaderModeUtils.isEnteringReaderMode(oldUrl, uri); + + if (TextUtils.equals(oldUrl, uri)) { + Log.d(LOGTAG, "Ignoring location change event: URIs are the same."); + return; + } + updateURL(uri); updateUserSearch(message.getString("userSearch")); mBaseDomain = message.optString("baseDomain"); if (message.getBoolean("sameDocument")) { // We can get a location change event for the same document with an anchor tag // Notify listeners so that buttons like back or forward will update themselves Tabs.getInstance().notifyListeners(this, Tabs.TabEvents.LOCATION_CHANGE, oldUrl); return; } setContentType(message.getString("contentType")); + + // We can unconditionally clear the favicon here: we already + // short-circuited for both cases in which this was a (pseudo-) + // spurious location change, so we're definitely loading a new page. + // The same applies to all of the other fields we're wiping out. clearFavicon(); + setHasFeeds(false); updateTitle(null); updateIdentityData(null); setReaderEnabled(false); setZoomConstraints(new ZoomConstraints(true)); setHasTouchListeners(false); setBackgroundColor(DEFAULT_BACKGROUND_COLOR); setErrorType(ErrorType.NONE);
--- a/mobile/android/base/Tabs.java +++ b/mobile/android/base/Tabs.java @@ -377,16 +377,17 @@ public class Tabs implements GeckoEventL public static Tabs getInstance() { return Tabs.TabsInstanceHolder.INSTANCE; } // GeckoEventListener implementation @Override public void handleMessage(String event, JSONObject message) { + Log.d(LOGTAG, "handleMessage: " + event); try { if (event.equals("Session:RestoreEnd")) { notifyListeners(null, TabEvents.RESTORED); return; } // All other events handled below should contain a tabID property int id = message.getInt("tabID"); @@ -557,16 +558,21 @@ public class Tabs implements GeckoEventL } public void notifyListeners(Tab tab, TabEvents msg) { notifyListeners(tab, msg, ""); } // Throws if not initialized. public void notifyListeners(final Tab tab, final TabEvents msg, final Object data) { + if (tab == null && + msg != TabEvents.RESTORED) { + throw new IllegalArgumentException("onTabChanged:" + msg + " must specify a tab."); + } + ThreadUtils.postToUiThread(new Runnable() { @Override public void run() { onTabChanged(tab, msg, data); synchronized (mTabsChangedListeners) { if (mTabsChangedListeners.isEmpty()) { return; @@ -655,18 +661,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 +732,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); } @@ -257,18 +284,18 @@ public class Favicons { return taskId; } public static void putFaviconInMemCache(String pageUrl, Bitmap image) { sFaviconsCache.putSingleFavicon(pageUrl, image); } - public static void putFaviconsInMemCache(String pageUrl, Iterator<Bitmap> images) { - sFaviconsCache.putFavicons(pageUrl, images); + public static void putFaviconsInMemCache(String pageUrl, Iterator<Bitmap> images, boolean permanently) { + sFaviconsCache.putFavicons(pageUrl, images, permanently); } public static void clearMemCache() { sFaviconsCache.evictAll(); sPageURLMappings.evictAll(); } public static void putFaviconInFailedCache(String faviconURL) { @@ -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(), true); + } + + /** + * 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/favicons/cache/FaviconCache.java +++ b/mobile/android/base/favicons/cache/FaviconCache.java @@ -18,16 +18,19 @@ import java.util.concurrent.atomic.Atomi * 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. * + * A second map exists for permanent cache entries -- ones that are never expired. These entries + * are assumed to be disjoint from those in the normal cache, and this map is checked first. + * * 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 @@ -101,28 +104,31 @@ public class FaviconCache { 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>(); + // And the same, but never evicted. + private final ConcurrentHashMap<String, FaviconsForURL> mPermanentBackingMap = 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. + // Current size, in bytes, of the bitmap data present in the LRU 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); @@ -211,16 +217,18 @@ public class FaviconCache { startRead(); boolean isExpired = false; boolean isAborting = false; try { // If we don't have it in the cache, it certainly isn't a known failure. + // Non-evictable favicons are never failed, so we don't need to + // check mPermanentBackingMap. 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) { @@ -299,28 +307,34 @@ public class FaviconCache { 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; + boolean wasPermanent = false; + FaviconsForURL container; final Bitmap newBitmap; - final FaviconsForURL container; startRead(); try { - if (!mBackingMap.containsKey(faviconURL)) { - return null; + container = mPermanentBackingMap.get(faviconURL); + if (container == null) { + container = mBackingMap.get(faviconURL); + if (container == null) { + // We don't have it! + return null; + } + } else { + wasPermanent = true; } - 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. @@ -407,19 +421,20 @@ public class FaviconCache { } // 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()); + if (!wasPermanent) { + setMostRecentlyUsed(newElement); + mCurrentSize.addAndGet(newElement.sizeOf()); + } } finally { finishWrite(); } return newBitmap; } /** @@ -427,24 +442,28 @@ public class FaviconCache { * * @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)) { + FaviconsForURL element = mPermanentBackingMap.get(key); + if (element == null) { + element = mBackingMap.get(key); + } + + if (element == null) { Log.w(LOGTAG, "Cannot compute dominant color of non-cached favicon. Cache fullness " + mCurrentSize.get() + '/' + mMaxSizeBytes); finishRead(); return 0xFFFFFF; } - FaviconsForURL element = mBackingMap.get(key); return element.ensureDominantColor(); } finally { finishRead(); } } /** @@ -538,18 +557,19 @@ public class FaviconCache { 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. + * @param permanently If true, the added favicons are never subject to eviction. */ - public void putFavicons(String faviconURL, Iterator<Bitmap> favicons) { + public void putFavicons(String faviconURL, Iterator<Bitmap> favicons, boolean permanently) { // 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; @@ -562,38 +582,44 @@ public class FaviconCache { 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); + if (!permanently) { + 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); + if (permanently) { + mPermanentBackingMap.put(faviconURL, toInsert); + } else { + mCurrentSize.addAndGet(sizeGained); - // Update the value in the LruCache... - recordRemoved(mBackingMap.put(faviconURL, toInsert)); + // Update the value in the LruCache... + recordRemoved(mBackingMap.put(faviconURL, toInsert)); + } } finally { finishWrite(); } cullIfRequired(); } /** @@ -626,17 +652,19 @@ public class FaviconCache { } /** * Purge all elements from the FaviconCache. Handy if you want to reclaim some memory. */ public void evictAll() { startWrite(); + // Note that we neither clear, nor track the size of, the permanent map. try { mCurrentSize.set(0); mBackingMap.clear(); mOrdering.clear(); + } finally { finishWrite(); } } }
--- 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); } @@ -527,16 +531,17 @@ public class BrowserToolbar extends Geck mActivity.refreshToolbarHeight(); } }); } } @Override public void onTabChanged(Tab tab, Tabs.TabEvents msg, Object data) { + Log.d(LOGTAG, "onTabChanged: " + msg); final Tabs tabs = Tabs.getInstance(); // These conditions are split into three phases: // * Always do first // * Handling specific to the selected tab // * Always do afterwards. switch (msg) { @@ -578,17 +583,22 @@ public class BrowserToolbar extends Geck case SELECTED: case LOAD_ERROR: updateTitle(); // Fall through. case LOCATION_CHANGE: // A successful location change will cause Tab to notify // us of a title change, so we don't update the title here. - refresh(); + // And there's no point in refreshing the UI + // if the page is the same. + final String oldURL = (String) data; + if (!TextUtils.equals(oldURL, tab.getURL())) { + refresh(); + } break; case CLOSED: case ADDED: updateBackButton(canDoBack(tab)); updateForwardButton(canDoForward(tab)); break; @@ -736,33 +746,37 @@ 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 { Tab selectedTab = Tabs.getInstance().getSelectedTab(); - if (selectedTab != null) + if (selectedTab != null) { setFavicon(selectedTab.getFavicon()); + } if (mSpinnerVisible) { setPageActionVisibility(false); mFavicon.setAnimation(null); mProgressSpinner.cancel(); mSpinnerVisible = false; } Log.i(LOGTAG, "zerdatime " + SystemClock.uptimeMillis() + " - Throbber stop"); @@ -863,25 +877,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 +932,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); } @@ -1566,16 +1589,17 @@ public class BrowserToolbar extends Geck } protected void unregisterEventListener(String event) { GeckoAppShell.getEventDispatcher().unregisterEventListener(event, this); } @Override public void handleMessage(String event, JSONObject message) { + Log.d(LOGTAG, "handleMessage: " + event); if (event.equals("Reader:Click")) { Tab tab = Tabs.getInstance().getSelectedTab(); if (tab != null) { tab.toggleReaderMode(); } } else if (event.equals("Reader:LongClick")) { Tab tab = Tabs.getInstance().getSelectedTab(); if (tab != null) {
--- 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>
--- a/mobile/android/chrome/content/browser.js +++ b/mobile/android/chrome/content/browser.js @@ -4383,21 +4383,26 @@ var BrowserEventHandler = { try { let data = JSON.parse(aData); let [x, y] = [data.x, data.y]; if (ElementTouchHelper.isElementClickable(element)) { [x, y] = this._moveClickPoint(element, x, y); element = ElementTouchHelper.anyElementFromPoint(x, y); } + // Was the element already focused before it was clicked? + let isFocused = (element == BrowserApp.getFocusedInput(BrowserApp.selectedBrowser, true)); + this._sendMouseEvent("mousemove", element, x, y); this._sendMouseEvent("mousedown", element, x, y); this._sendMouseEvent("mouseup", element, x, y); - SelectionHandler.attachCaret(element); + // If the element was previously focused, show the caret attached to it. + if (isFocused) + SelectionHandler.attachCaret(element); // scrollToFocusedInput does its own checks to find out if an element should be zoomed into BrowserApp.scrollToFocusedInput(BrowserApp.selectedBrowser); } catch(e) { Cu.reportError(e); } } this._cancelTapHighlight();
--- a/mobile/android/themes/core/content.css +++ b/mobile/android/themes/core/content.css @@ -295,19 +295,19 @@ select[disabled] > button { /* -moz-touch-enabled? media elements */ :-moz-any(video, audio) > xul|videocontrols { -moz-binding: url("chrome://global/content/bindings/videocontrols.xml#touchControls"); } *:-moz-any-link:active, *[role=button]:active, button:not([disabled]):active, -input:not([disabled]):active, +input:not(:focus):not([disabled]):active, select:not([disabled]):active, -textarea:not([disabled]):active, +textarea:not(:focus):not([disabled]):active, option:active, label:active, xul|menulist:active { background-color: @color_background_highlight_overlay@; } input[type=number]::-moz-number-spin-box { display: none;