author | Wes Kocher <wkocher@mozilla.com> |
Wed, 18 Sep 2013 18:00:34 -0700 | |
changeset 147851 | 803189f35921518c69410de472fb6a37038b2254 |
parent 147836 | 1907e00d462411cdb0faa96ebb3d2abee2f7b690 (current diff) |
parent 147850 | a0cad0d1d7fcf358ad41bf74752694242a9e95a0 (diff) |
child 147852 | 8baf25d5ed30bbfb70e3a352fd88ac8747505492 |
child 147907 | 121cd7e739989e99707974a1cd11fffaf36213ad |
child 147922 | c40bf17bbc630bab9d26531de27e4c5a2010e95d |
child 155816 | c6d3d1418c78d501386f2673d23302de4e02f3ae |
child 161522 | ef5453969fc767e4680b9594597ac7df788e9595 |
push id | 34007 |
push user | philringnalda@gmail.com |
push date | Thu, 19 Sep 2013 06:29:29 +0000 |
treeherder | mozilla-inbound@8baf25d5ed30 [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
milestone | 27.0a1 |
first release with | nightly linux32
803189f35921
/
27.0a1
/
20130919030202
/
files
nightly linux64
803189f35921
/
27.0a1
/
20130919030202
/
files
nightly mac
803189f35921
/
27.0a1
/
20130919030202
/
files
nightly win32
803189f35921
/
27.0a1
/
20130919030202
/
files
nightly win64
803189f35921
/
27.0a1
/
20130919030202
/
files
|
last release without | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
releases | nightly linux32
27.0a1
/
20130919030202
/
pushlog to previous
nightly linux64
27.0a1
/
20130919030202
/
pushlog to previous
nightly mac
27.0a1
/
20130919030202
/
pushlog to previous
nightly win32
27.0a1
/
20130919030202
/
pushlog to previous
nightly win64
27.0a1
/
20130919030202
/
pushlog to previous
|
mobile/android/base/home/FaviconsLoader.java | file | annotate | diff | comparison | revisions | |
mobile/android/base/home/HomeCursorLoaderCallbacks.java | file | annotate | diff | comparison | revisions |
--- a/browser/base/content/browser.js +++ b/browser/base/content/browser.js @@ -1114,28 +1114,35 @@ var gBrowserInit = { gPrefService.addObserver(ctrlTab.prefName, ctrlTab, false); // Initialize the download manager some time after the app starts so that // auto-resume downloads begin (such as after crashing or quitting with // active downloads) and speeds up the first-load of the download manager UI. // If the user manually opens the download manager before the timeout, the // downloads will start right away, and getting the service again won't hurt. setTimeout(function() { - let DownloadsCommon = - Cu.import("resource:///modules/DownloadsCommon.jsm", {}).DownloadsCommon; - if (DownloadsCommon.useJSTransfer) { - // Open the data link without initalizing nsIDownloadManager. - DownloadsCommon.initializeAllDataLinks(); - } else { - // Initalizing nsIDownloadManager will trigger the data link. - Services.downloads; + try { + let DownloadsCommon = + Cu.import("resource:///modules/DownloadsCommon.jsm", {}).DownloadsCommon; + if (DownloadsCommon.useJSTransfer) { + // Open the data link without initalizing nsIDownloadManager. + DownloadsCommon.initializeAllDataLinks(); + let DownloadsTaskbar = + Cu.import("resource:///modules/DownloadsTaskbar.jsm", {}).DownloadsTaskbar; + DownloadsTaskbar.registerIndicator(window); + } else { + // Initalizing nsIDownloadManager will trigger the data link. + Services.downloads; + let DownloadTaskbarProgress = + Cu.import("resource://gre/modules/DownloadTaskbarProgress.jsm", {}).DownloadTaskbarProgress; + DownloadTaskbarProgress.onBrowserWindowLoad(window); + } + } catch (ex) { + Cu.reportError(ex); } - let DownloadTaskbarProgress = - Cu.import("resource://gre/modules/DownloadTaskbarProgress.jsm", {}).DownloadTaskbarProgress; - DownloadTaskbarProgress.onBrowserWindowLoad(window); }, 10000); // The object handling the downloads indicator is also initialized here in the // delayed startup function, but the actual indicator element is not loaded // unless there are downloads to be displayed. DownloadsButton.initializeIndicator(); #ifndef XP_MACOSX
new file mode 100644 --- /dev/null +++ b/browser/components/downloads/src/DownloadsTaskbar.jsm @@ -0,0 +1,180 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80 filetype=javascript: */ +/* 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/. */ + +/** + * Handles the download progress indicator in the taskbar. + */ + +"use strict"; + +this.EXPORTED_SYMBOLS = [ + "DownloadsTaskbar", +]; + +//////////////////////////////////////////////////////////////////////////////// +//// Globals + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cu = Components.utils; +const Cr = Components.results; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "Downloads", + "resource://gre/modules/Downloads.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "RecentWindow", + "resource:///modules/RecentWindow.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Services", + "resource://gre/modules/Services.jsm"); + +XPCOMUtils.defineLazyGetter(this, "gWinTaskbar", function () { + if (!("@mozilla.org/windows-taskbar;1" in Cc)) { + return null; + } + let winTaskbar = Cc["@mozilla.org/windows-taskbar;1"] + .getService(Ci.nsIWinTaskbar); + return winTaskbar.available && winTaskbar; +}); + +XPCOMUtils.defineLazyGetter(this, "gMacTaskbarProgress", function () { + return ("@mozilla.org/widget/macdocksupport;1" in Cc) && + Cc["@mozilla.org/widget/macdocksupport;1"] + .getService(Ci.nsITaskbarProgress); +}); + +//////////////////////////////////////////////////////////////////////////////// +//// DownloadsTaskbar + +/** + * Handles the download progress indicator in the taskbar. + */ +this.DownloadsTaskbar = { + /** + * Underlying DownloadSummary providing the aggregate download information, or + * null if the indicator has never been initialized. + */ + _summary: null, + + /** + * nsITaskbarProgress object to which download information is dispatched. + * This can be null if the indicator has never been initialized or if the + * indicator is currently hidden on Windows. + */ + _taskbarProgress: null, + + /** + * This method is called after a new browser window is opened, and ensures + * that the download progress indicator is displayed in the taskbar. + * + * On Windows, the indicator is attached to the first browser window that + * calls this method. When the window is closed, the indicator is moved to + * another browser window, if available, in no particular order. When there + * are no browser windows visible, the indicator is hidden. + * + * On Mac OS X, the indicator is initialized globally when this method is + * called for the first time. Subsequent calls have no effect. + * + * @param aBrowserWindow + * nsIDOMWindow object of the newly opened browser window to which the + * indicator may be attached. + */ + registerIndicator: function (aBrowserWindow) + { + if (!this._taskbarProgress) { + if (gMacTaskbarProgress) { + // On Mac OS X, we have to register the global indicator only once. + this._taskbarProgress = gMacTaskbarProgress; + // Free the XPCOM reference on shutdown, to prevent detecting a leak. + Services.obs.addObserver(() => { + this._taskbarProgress = null; + gMacTaskbarProgress = null; + }, "quit-application-granted", false); + } else if (gWinTaskbar) { + // On Windows, the indicator is currently hidden because we have no + // previous browser window, thus we should attach the indicator now. + this._attachIndicator(aBrowserWindow); + } else { + // The taskbar indicator is not available on this platform. + return; + } + } + + // Ensure that the DownloadSummary object will be created asynchronously. + if (!this._summary) { + Downloads.getSummary(Downloads.ALL).then(summary => { + // In case the method is re-entered, we simply ignore redundant + // invocations of the callback, instead of keeping separate state. + if (this._summary) { + return; + } + this._summary = summary; + return this._summary.addView(this); + }).then(null, Cu.reportError); + } + }, + + /** + * On Windows, attaches the taskbar indicator to the specified browser window. + */ + _attachIndicator: function (aWindow) + { + // Activate the indicator on the specified window. + let docShell = aWindow.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShellTreeItem).treeOwner + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIXULWindow).docShell; + this._taskbarProgress = gWinTaskbar.getTaskbarProgress(docShell); + + // If the DownloadSummary object has already been created, we should update + // the state of the new indicator, otherwise it will be updated as soon as + // the DownloadSummary view is registered. + if (this._summary) { + this.onSummaryChanged(); + } + + aWindow.addEventListener("unload", () => { + // Locate another browser window, excluding the one being closed. + let browserWindow = RecentWindow.getMostRecentBrowserWindow(); + if (browserWindow) { + // Move the progress indicator to the other browser window. + this._attachIndicator(browserWindow); + } else { + // The last browser window has been closed. We remove the reference to + // the taskbar progress object so that the indicator will be registered + // again on the next browser window that is opened. + this._taskbarProgress = null; + } + }, false); + }, + + ////////////////////////////////////////////////////////////////////////////// + //// DownloadSummary view + + onSummaryChanged: function () + { + // If the last browser window has been closed, we have no indicator anymore. + if (!this._taskbarProgress) { + return; + } + + if (this._summary.allHaveStopped || this._summary.progressTotalBytes == 0) { + this._taskbarProgress.setProgressState( + Ci.nsITaskbarProgress.STATE_NO_PROGRESS, 0, 0); + } else { + // For a brief moment before completion, some download components may + // report more transferred bytes than the total number of bytes. Thus, + // ensure that we never break the expectations of the progress indicator. + let progressCurrentBytes = Math.min(this._summary.progressTotalBytes, + this._summary.progressCurrentBytes); + this._taskbarProgress.setProgressState( + Ci.nsITaskbarProgress.STATE_NORMAL, + progressCurrentBytes, + this._summary.progressTotalBytes); + } + }, +};
--- a/browser/components/downloads/src/moz.build +++ b/browser/components/downloads/src/moz.build @@ -8,10 +8,11 @@ EXTRA_COMPONENTS += [ 'BrowserDownloads.manifest', 'DownloadsStartup.js', 'DownloadsUI.js', ] EXTRA_JS_MODULES += [ 'DownloadsCommon.jsm', 'DownloadsLogger.jsm', + 'DownloadsTaskbar.jsm', ]
--- a/browser/components/preferences/in-content/main.xul +++ b/browser/components/preferences/in-content/main.xul @@ -210,39 +210,32 @@ <!-- Tab preferences --> <groupbox data-category="paneGeneral" hidden="true"> <caption label="&tabsGroup.label;"/> <checkbox id="linkTargeting" label="&newWindowsAsTabs.label;" accesskey="&newWindowsAsTabs.accesskey;" preference="browser.link.open_newwindow" onsyncfrompreference="return gMainPane.readLinkTarget();" - onsynctopreference="return gMainPane.writeLinkTarget();" - class="indent"/> + onsynctopreference="return gMainPane.writeLinkTarget();"/> <checkbox id="warnCloseMultiple" label="&warnCloseMultipleTabs.label;" accesskey="&warnCloseMultipleTabs.accesskey;" - preference="browser.tabs.warnOnClose" - class="indent"/> + preference="browser.tabs.warnOnClose"/> <checkbox id="warnOpenMany" label="&warnOpenManyTabs.label;" accesskey="&warnOpenManyTabs.accesskey;" - preference="browser.tabs.warnOnOpen" - class="indent"/> + preference="browser.tabs.warnOnOpen"/> <checkbox id="restoreOnDemand" label="&restoreTabsOnDemand.label;" accesskey="&restoreTabsOnDemand.accesskey;" - preference="browser.sessionstore.restore_on_demand" - class="indent"/> + preference="browser.sessionstore.restore_on_demand"/> <checkbox id="switchToNewTabs" label="&switchToNewTabs.label;" accesskey="&switchToNewTabs.accesskey;" - preference="browser.tabs.loadInBackground" - class="indent"/> + preference="browser.tabs.loadInBackground"/> #ifdef XP_WIN <checkbox id="showTabsInTaskbar" label="&showTabsInTaskbar.label;" accesskey="&showTabsInTaskbar.accesskey;" - preference="browser.taskbar.previews.enable" - class="indent"/> + preference="browser.taskbar.previews.enable"/> #endif - </groupbox>
--- a/browser/devtools/debugger/test/Makefile.in +++ b/browser/devtools/debugger/test/Makefile.in @@ -17,16 +17,17 @@ MOCHITEST_BROWSER_TESTS = \ browser_dbg_breakpoints-contextmenu.js \ browser_dbg_breakpoints-disabled-reload.js \ browser_dbg_breakpoints-editor.js \ browser_dbg_breakpoints-highlight.js \ browser_dbg_breakpoints-new-script.js \ browser_dbg_breakpoints-pane.js \ browser_dbg_chrome-debugging.js \ browser_dbg_clean-exit.js \ + browser_dbg_clean-exit-window.js \ browser_dbg_cmd-blackbox.js \ browser_dbg_cmd-break.js \ browser_dbg_cmd-dbg.js \ browser_dbg_conditional-breakpoints-01.js \ browser_dbg_conditional-breakpoints-02.js \ browser_dbg_debugger-statement.js \ browser_dbg_editor-contextmenu.js \ browser_dbg_editor-mode.js \
new file mode 100644 --- /dev/null +++ b/browser/devtools/debugger/test/browser_dbg_clean-exit-window.js @@ -0,0 +1,90 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test that closing a window with the debugger in a paused state exits cleanly. + */ + +let gDebuggee, gPanel, gDebugger, gWindow; + +const TAB_URL = EXAMPLE_URL + "doc_inline-debugger-statement.html"; + +function test() { + addWindow(TAB_URL) + .then(win => initDebugger(TAB_URL, win)) + .then(([aTab, aDebuggee, aPanel, aWindow]) => { + gDebuggee = aDebuggee; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + gWindow = aWindow; + + return testCleanExit(gWindow); + }) + .then(null, aError => { + ok(false, "Got an error: " + aError.message + "\n" + aError.stack); + }); +} + +function testCleanExit(aWindow) { + let deferred = promise.defer(); + + gWindow = aWindow; + ok(!!gWindow, "Second window created."); + + gWindow.focus(); + + let topWindow = Services.wm.getMostRecentWindow("navigator:browser"); + is(topWindow, gWindow, + "The second window is on top."); + + let isActive = promise.defer(); + let isLoaded = promise.defer(); + + promise.all([isActive.promise, isLoaded.promise]).then(() => { + gWindow.BrowserChromeTest.runWhenReady(() => { + waitForSourceAndCaretAndScopes(gPanel, ".html", 16).then(() => { + is(gDebugger.gThreadClient.paused, true, + "Should be paused after the debugger statement."); + gWindow.close(); + deferred.resolve(); + finish(); + }); + + gDebuggee.runDebuggerStatement(); + }); + }); + + let focusManager = Cc["@mozilla.org/focus-manager;1"].getService(Ci.nsIFocusManager); + if (focusManager.activeWindow != gWindow) { + gWindow.addEventListener("activate", function onActivate(aEvent) { + if (aEvent.target != gWindow) { + return; + } + gWindow.removeEventListener("activate", onActivate, true); + isActive.resolve(); + }, true); + } else { + isActive.resolve(); + } + + let contentLocation = gWindow.content.location.href; + if (contentLocation != TAB_URL) { + gWindow.document.addEventListener("load", function onLoad(aEvent) { + if (aEvent.target.documentURI != TAB_URL) { + return; + } + gWindow.document.removeEventListener("load", onLoad, true); + isLoaded.resolve(); + }, true); + } else { + isLoaded.resolve(); + } + return deferred.promise; +} + +registerCleanupFunction(function() { + gWindow = null; + gDebuggee = null; + gPanel = null; + gDebugger = null; +});
--- a/browser/devtools/debugger/test/head.js +++ b/browser/devtools/debugger/test/head.js @@ -411,28 +411,28 @@ function typeText(aElement, aText) { function backspaceText(aElement, aTimes) { info("Pressing backspace " + aTimes + " times."); for (let i = 0; i < aTimes; i++) { aElement.focus(); EventUtils.sendKey("BACK_SPACE", aElement.ownerDocument.defaultView); } } -function getTab(aTarget) { +function getTab(aTarget, aWindow) { if (aTarget instanceof XULElement) { return promise.resolve(aTarget); } else { - return addTab(aTarget); + return addTab(aTarget, aWindow); } } function initDebugger(aTarget, aWindow) { info("Initializing a debugger panel."); - return getTab(aTarget).then(aTab => { + return getTab(aTarget, aWindow).then(aTab => { info("Debugee tab added successfully: " + aTarget); let deferred = promise.defer(); let debuggee = aTab.linkedBrowser.contentWindow.wrappedJSObject; let target = TargetFactory.forTab(aTab); gDevTools.showToolbox(target, "jsdebugger").then(aToolbox => { info("Debugger panel shown successfully."); @@ -440,17 +440,17 @@ function initDebugger(aTarget, aWindow) let debuggerPanel = aToolbox.getCurrentPanel(); let panelWin = debuggerPanel.panelWin; // Wait for the initial resume... panelWin.gClient.addOneTimeListener("resumed", () => { info("Debugger client resumed successfully."); prepareDebugger(debuggerPanel); - deferred.resolve([aTab, debuggee, debuggerPanel]); + deferred.resolve([aTab, debuggee, debuggerPanel, aWindow]); }); }); return deferred.promise; }); } function initChromeDebugger(aOnClose) {
--- a/browser/devtools/styleinspector/computed-view.js +++ b/browser/devtools/styleinspector/computed-view.js @@ -663,81 +663,95 @@ PropertyView.prototype = { return false; } return true; }, /** * Returns the className that should be assigned to the propertyView. - * * @return string */ get propertyHeaderClassName() { if (this.visible) { - this.tree._darkStripe = !this.tree._darkStripe; - let darkValue = this.tree._darkStripe ? - "property-view theme-bg-darker" : "property-view"; - return darkValue; + let isDark = this.tree._darkStripe = !this.tree._darkStripe; + return isDark ? "property-view theme-bg-darker" : "property-view"; } return "property-view-hidden"; }, /** * Returns the className that should be assigned to the propertyView content * container. * @return string */ get propertyContentClassName() { if (this.visible) { - let darkValue = this.tree._darkStripe ? - "property-content theme-bg-darker" : "property-content"; - return darkValue; + let isDark = this.tree._darkStripe; + return isDark ? "property-content theme-bg-darker" : "property-content"; } return "property-content-hidden"; }, + /** + * Build the markup for on computed style + * @return Element + */ buildMain: function PropertyView_buildMain() { let doc = this.tree.styleDocument; + let onToggle = this.onStyleToggle.bind(this); + + // Build the container element this.element = doc.createElementNS(HTML_NS, "div"); this.element.setAttribute("class", this.propertyHeaderClassName); - this.matchedExpander = doc.createElementNS(HTML_NS, "div"); - this.matchedExpander.className = "expander theme-twisty"; - this.matchedExpander.setAttribute("tabindex", "0"); - this.matchedExpander.addEventListener("click", - this.matchedExpanderClick.bind(this), false); - this.matchedExpander.addEventListener("keydown", function(aEvent) { + // Make it keyboard navigable + this.element.setAttribute("tabindex", "0"); + this.element.addEventListener("keydown", function(aEvent) { let keyEvent = Ci.nsIDOMKeyEvent; if (aEvent.keyCode == keyEvent.DOM_VK_F1) { this.mdnLinkClick(); } if (aEvent.keyCode == keyEvent.DOM_VK_RETURN || aEvent.keyCode == keyEvent.DOM_VK_SPACE) { - this.matchedExpanderClick(aEvent); + onToggle(aEvent); } }.bind(this), false); + + // Build the twisty expand/collapse + this.matchedExpander = doc.createElementNS(HTML_NS, "div"); + this.matchedExpander.className = "expander theme-twisty"; + this.matchedExpander.addEventListener("click", onToggle, false); this.element.appendChild(this.matchedExpander); + // Build the style name element this.nameNode = doc.createElementNS(HTML_NS, "div"); - this.element.appendChild(this.nameNode); this.nameNode.setAttribute("class", "property-name theme-fg-color5"); + // Reset its tabindex attribute otherwise, if an ellipsis is applied + // it will be reachable via TABing + this.nameNode.setAttribute("tabindex", ""); this.nameNode.textContent = this.nameNode.title = this.name; - this.nameNode.addEventListener("click", function(aEvent) { - this.matchedExpander.focus(); - }.bind(this), false); + // Make it hand over the focus to the container + this.nameNode.addEventListener("click", () => this.element.focus(), false); + this.element.appendChild(this.nameNode); + // Build the style value element this.valueNode = doc.createElementNS(HTML_NS, "div"); - this.element.appendChild(this.valueNode); this.valueNode.setAttribute("class", "property-value theme-fg-color1"); + // Reset its tabindex attribute otherwise, if an ellipsis is applied + // it will be reachable via TABing + this.valueNode.setAttribute("tabindex", ""); this.valueNode.setAttribute("dir", "ltr"); this.valueNode.textContent = this.valueNode.title = this.value; + // Make it hand over the focus to the container + this.valueNode.addEventListener("click", () => this.element.focus(), false); + this.element.appendChild(this.valueNode); return this.element; }, buildSelectorContainer: function PropertyView_buildSelectorContainer() { let doc = this.tree.styleDocument; let element = doc.createElementNS(HTML_NS, "div"); @@ -831,17 +845,17 @@ PropertyView.prototype = { }, /** * The action when a user expands matched selectors. * * @param {Event} aEvent Used to determine the class name of the targets click * event. */ - matchedExpanderClick: function PropertyView_matchedExpanderClick(aEvent) + onStyleToggle: function PropertyView_onStyleToggle(aEvent) { this.matchedExpanded = !this.matchedExpanded; this.refreshMatchedSelectors(); aEvent.preventDefault(); }, /** * The action when a user clicks on the MDN help link for a property.
--- a/browser/devtools/styleinspector/test/Makefile.in +++ b/browser/devtools/styleinspector/test/Makefile.in @@ -31,16 +31,17 @@ MOCHITEST_BROWSER_FILES = \ browser_computedview_copy.js\ browser_styleinspector_bug_677930_urls_clickable.js \ browser_bug893965_css_property_completion_new_property.js \ browser_bug893965_css_property_completion_existing_property.js \ browser_bug894376_css_value_completion_new_property_value_pair.js \ browser_bug894376_css_value_completion_existing_property_value_pair.js \ browser_ruleview_bug_902966_revert_value_on_ESC.js \ browser_ruleview_pseudoelement.js \ + browser_computedview_bug835808_keyboard_nav.js \ head.js \ $(NULL) MOCHITEST_BROWSER_FILES += \ browser_bug683672.html \ browser_bug705707_is_content_stylesheet.html \ browser_bug705707_is_content_stylesheet_imported.css \ browser_bug705707_is_content_stylesheet_imported2.css \
new file mode 100644 --- /dev/null +++ b/browser/devtools/styleinspector/test/browser_computedview_bug835808_keyboard_nav.js @@ -0,0 +1,94 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests that the style inspector works properly + +let doc, computedView, inspector; + +function test() +{ + waitForExplicitFinish(); + + gBrowser.selectedTab = gBrowser.addTab(); + gBrowser.selectedBrowser.addEventListener("load", function onBrowserLoad(evt) { + gBrowser.selectedBrowser.removeEventListener("load", onBrowserLoad, true); + doc = content.document; + waitForFocus(createDocument, content); + }, true); + + content.location = "data:text/html,computed view context menu test"; +} + +function createDocument() +{ + doc.body.innerHTML = '<style type="text/css"> ' + + 'span { font-variant: small-caps; color: #000000; } ' + + '.nomatches {color: #ff0000;}</style> <div id="first" style="margin: 10em; ' + + 'font-size: 14pt; font-family: helvetica, sans-serif; color: #AAA">\n' + + '<h1>Some header text</h1>\n' + + '<p id="salutation" style="font-size: 12pt">hi.</p>\n' + + '<p id="body" style="font-size: 12pt">I am a test-case. This text exists ' + + 'solely to provide some things to <span style="color: yellow">' + + 'highlight</span> and <span style="font-weight: bold">count</span> ' + + 'style list-items in the box at right. If you are reading this, ' + + 'you should go do something else instead. Maybe read a book. Or better ' + + 'yet, write some test-cases for another bit of code. ' + + '<span style="font-style: italic">some text</span></p>\n' + + '<p id="closing">more text</p>\n' + + '<p>even more text</p>' + + '</div>'; + doc.title = "Computed view keyboard navigation test"; + + openComputedView(startTests); +} + +function startTests(aInspector, aComputedView) +{ + computedView = aComputedView; + inspector = aInspector; + testTabThrougStyles(); +} + +function endTests() +{ + computedView = inspector = doc = null; + gBrowser.removeCurrentTab(); + finish(); +} + +function testTabThrougStyles() +{ + let span = doc.querySelector("span"); + + inspector.once("computed-view-refreshed", () => { + // Selecting the first computed style in the list + let firstStyle = computedView.styleDocument.querySelector(".property-view"); + ok(firstStyle, "First computed style found in panel"); + firstStyle.focus(); + + // Tab to select the 2nd style, press return + EventUtils.synthesizeKey("VK_TAB", {}); + EventUtils.synthesizeKey("VK_RETURN", {}); + inspector.once("computed-view-property-expanded", () => { + // Verify the 2nd style has been expanded + let secondStyleSelectors = computedView.styleDocument.querySelectorAll( + ".property-content .matchedselectors")[1]; + ok(secondStyleSelectors.childNodes.length > 0, "Matched selectors expanded"); + + // Tab back up and test the same thing, with space + EventUtils.synthesizeKey("VK_TAB", {shiftKey: true}); + EventUtils.synthesizeKey("VK_SPACE", {}); + inspector.once("computed-view-property-expanded", () => { + // Verify the 1st style has been expanded too + let firstStyleSelectors = computedView.styleDocument.querySelectorAll( + ".property-content .matchedselectors")[0]; + ok(firstStyleSelectors.childNodes.length > 0, "Matched selectors expanded"); + + endTests(); + }); + }); + }); + + inspector.selection.setNode(span); +}
--- a/browser/themes/linux/devtools/computedview.css +++ b/browser/themes/linux/devtools/computedview.css @@ -40,29 +40,31 @@ body { vertical-align: middle; } .property-name { width: 50%; overflow-x: hidden; text-overflow: ellipsis; white-space: nowrap; + outline: 0; } .property-value { width: 50%; max-width: 100%; overflow-x: hidden; text-overflow: ellipsis; white-space: nowrap; background-image: url(arrow-e.png); background-repeat: no-repeat; background-size: 5px 8px; background-position: 2px center; padding-left: 10px; + outline: 0; } .other-property-value { background-image: url(arrow-e.png); background-repeat: no-repeat; background-size: 5px 8px; background-position: left center; padding-left: 8px;
--- a/browser/themes/osx/devtools/computedview.css +++ b/browser/themes/osx/devtools/computedview.css @@ -58,29 +58,31 @@ body { vertical-align: middle; } .property-name { width: 50%; overflow-x: hidden; text-overflow: ellipsis; white-space: nowrap; + outline: 0; } .property-value { width: 50%; max-width: 100%; overflow-x: hidden; text-overflow: ellipsis; white-space: nowrap; background-image: url(arrow-e.png); background-repeat: no-repeat; background-size: 5px 8px; background-position: 2px center; padding-left: 10px; + outline: 0; } .other-property-value { background-image: url(arrow-e.png); background-repeat: no-repeat; background-size: 5px 8px; background-position: left center; padding-left: 8px;
--- a/browser/themes/windows/devtools/computedview.css +++ b/browser/themes/windows/devtools/computedview.css @@ -58,29 +58,31 @@ body { vertical-align: middle; } .property-name { width: 50%; overflow-x: hidden; text-overflow: ellipsis; white-space: nowrap; + outline: 0; } .property-value { width: 50%; max-width: 100%; overflow-x: hidden; text-overflow: ellipsis; white-space: nowrap; background-image: url(arrow-e.png); background-repeat: no-repeat; background-size: 5px 8px; background-position: 2px center; padding-left: 10px; + outline: 0; } .other-property-value { background-image: url(arrow-e.png); background-repeat: no-repeat; background-size: 5px 8px; background-position: left center; padding-left: 8px;
--- a/mobile/android/base/GeckoAppShell.java +++ b/mobile/android/base/GeckoAppShell.java @@ -2252,17 +2252,17 @@ public class GeckoAppShell * will be invoked on future events of that type. * * This method is referenced by Robocop via reflection. */ public static void registerEventListener(String event, GeckoEventListener listener) { sEventDispatcher.registerEventListener(event, listener); } - static EventDispatcher getEventDispatcher() { + public static EventDispatcher getEventDispatcher() { return sEventDispatcher; } /** * Remove a previously-registered listener for a gecko event. * This method is thread-safe and may be called at any time. In particular, calling it * with an event that is currently being processed has the properly-defined behaviour that * any removed listeners will still be invoked on the event currently being processed, but
--- a/mobile/android/base/Makefile.in +++ b/mobile/android/base/Makefile.in @@ -215,24 +215,22 @@ FENNEC_JAVA_FILES = \ gfx/VirtualLayer.java \ home/BookmarksListAdapter.java \ home/BookmarksListView.java \ home/BookmarksPage.java \ home/BookmarkFolderView.java \ home/BookmarkThumbnailView.java \ home/BrowserSearch.java \ home/HistoryPage.java \ - home/HomeCursorLoaderCallbacks.java \ home/HomeFragment.java \ home/HomeListView.java \ home/HomePager.java \ home/HomePagerTabStrip.java \ home/HomeBanner.java \ home/FadedTextView.java \ - home/FaviconsLoader.java \ home/LastTabsPage.java \ home/MostRecentPage.java \ home/MostVisitedPage.java \ home/MultiTypeCursorAdapter.java \ home/PinBookmarkDialog.java \ home/ReadingListPage.java \ home/SearchEngine.java \ home/SearchEngineRow.java \
--- a/mobile/android/base/android-services-files.mk +++ b/mobile/android/base/android-services-files.mk @@ -20,16 +20,17 @@ SYNC_JAVA_FILES := \ background/announcements/AnnouncementsFetchResourceDelegate.java \ background/announcements/AnnouncementsService.java \ background/announcements/AnnouncementsStartReceiver.java \ background/BackgroundService.java \ background/bagheera/BagheeraClient.java \ background/bagheera/BagheeraRequestDelegate.java \ background/bagheera/BoundedByteArrayEntity.java \ background/bagheera/DeflateHelper.java \ + background/common/DateUtils.java \ background/common/log/Logger.java \ background/common/log/writers/AndroidLevelCachingLogWriter.java \ background/common/log/writers/AndroidLogWriter.java \ background/common/log/writers/LevelFilteringLogWriter.java \ background/common/log/writers/LogWriter.java \ background/common/log/writers/PrintLogWriter.java \ background/common/log/writers/SimpleTagLogWriter.java \ background/common/log/writers/StringLogWriter.java \
--- a/mobile/android/base/animation/PropertyAnimator.java +++ b/mobile/android/base/animation/PropertyAnimator.java @@ -155,22 +155,27 @@ public class PropertyAnimator implements final ViewTreeObserver treeObserver; if (mElementsList.size() > 0) { treeObserver = mElementsList.get(0).view.getViewTreeObserver(); } else { treeObserver = null; } // Try to start animation after any on-going layout round - // in the current view tree. - if (treeObserver != null && treeObserver.isAlive()) { + // in the current view tree. OnPreDrawListener seems broken + // on pre-Honeycomb devices, start animation immediatelly + // in this case. + if (Build.VERSION.SDK_INT >= 11 && treeObserver != null && treeObserver.isAlive()) { treeObserver.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { @Override public boolean onPreDraw() { - treeObserver.removeOnPreDrawListener(this); + if (treeObserver.isAlive()) { + treeObserver.removeOnPreDrawListener(this); + } + mFramePoster.postFirstAnimationFrame(); return true; } }); } else { mFramePoster.postFirstAnimationFrame(); }
new file mode 100644 --- /dev/null +++ b/mobile/android/base/background/common/DateUtils.java @@ -0,0 +1,43 @@ +/* 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.background.common; + +import java.util.Calendar; +import java.util.Formatter; +import java.util.TimeZone; + +public class DateUtils { + private static final TimeZone UTC = TimeZone.getTimeZone("UTC"); + + public static final class DateFormatter { + private final Calendar calendar; + private final Formatter formatter; + private final StringBuilder builder; + + public DateFormatter() { + this.calendar = Calendar.getInstance(UTC); + this.builder = new StringBuilder(); // So we can reset it. + this.formatter = new Formatter(this.builder, null); + } + + public String getDateString(long time) { + calendar.setTimeInMillis(time); + builder.setLength(0); + return formatter.format("%04d-%02d-%02d", + calendar.get(Calendar.YEAR), + calendar.get(Calendar.MONTH) + 1, // 0-indexed. + calendar.get(Calendar.DAY_OF_MONTH)) + .toString(); + } + + public String getDateStringForDay(long day) { + return getDateString(GlobalConstants.MILLISECONDS_PER_DAY * day); + } + } + + public static int getDay(final long time) { + return (int) Math.floor(time / GlobalConstants.MILLISECONDS_PER_DAY); + } +}
--- a/mobile/android/base/background/common/GlobalConstants.java.in +++ b/mobile/android/base/background/common/GlobalConstants.java.in @@ -42,9 +42,13 @@ public class GlobalConstants { // These are used to ask Fennec (via reflection) to send // us a pref notification. This avoids us having to guess // Fennec's prefs branch and pref name. // Eventually Fennec might listen to startup notifications and // do this automatically, but this will do for now. See Bug 800244. public static String GECKO_PREFERENCES_CLASS = "org.mozilla.gecko.GeckoPreferences"; public static String GECKO_BROADCAST_ANNOUNCEMENTS_PREF_METHOD = "broadcastAnnouncementsPref"; public static String GECKO_BROADCAST_HEALTHREPORT_UPLOAD_PREF_METHOD = "broadcastHealthReportUploadPref"; + + // Common time values. + public static final long MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000; + public static final long MILLISECONDS_PER_SIX_MONTHS = 180 * MILLISECONDS_PER_DAY; }
--- a/mobile/android/base/background/healthreport/EnvironmentBuilder.java +++ b/mobile/android/base/background/healthreport/EnvironmentBuilder.java @@ -4,16 +4,17 @@ package org.mozilla.gecko.background.healthreport; import java.util.Iterator; import org.json.JSONObject; import org.mozilla.gecko.AppConstants; import org.mozilla.gecko.SysInfo; +import org.mozilla.gecko.background.common.GlobalConstants; import org.mozilla.gecko.background.common.log.Logger; import android.content.ContentProvider; import android.content.ContentProviderClient; import android.content.ContentResolver; import android.content.Context; /** @@ -72,17 +73,17 @@ public class EnvironmentBuilder { e.platformVersion = AppConstants.MOZILLA_VERSION; e.platformBuildID = AppConstants.MOZ_APP_BUILDID; e.xpcomabi = AppConstants.TARGET_XPCOM_ABI; e.os = "Android"; e.architecture = SysInfo.getArchABI(); // Not just "arm". e.sysName = SysInfo.getName(); e.sysVersion = SysInfo.getReleaseVersion(); - e.profileCreation = (int) (info.getProfileCreationTime() / HealthReportConstants.MILLISECONDS_PER_DAY); + e.profileCreation = (int) (info.getProfileCreationTime() / GlobalConstants.MILLISECONDS_PER_DAY); // Corresponds to Gecko pref "extensions.blocklist.enabled". e.isBlocklistEnabled = (info.isBlocklistEnabled() ? 1 : 0); // Corresponds to one of two Gecko telemetry prefs. We reflect these into // GeckoPreferences as "datareporting.telemetry.enabled". e.isTelemetryEnabled = (info.isTelemetryEnabled() ? 1 : 0);
--- a/mobile/android/base/background/healthreport/HealthReportConstants.java.in +++ b/mobile/android/base/background/healthreport/HealthReportConstants.java.in @@ -1,48 +1,47 @@ #filter substitution /* 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.background.healthreport; +import org.mozilla.gecko.background.common.GlobalConstants; + public class HealthReportConstants { public static final String HEALTH_AUTHORITY = "@ANDROID_PACKAGE_NAME@.health"; public static final String GLOBAL_LOG_TAG = "GeckoHealth"; /** * The earliest allowable value for the last ping time, corresponding to May 2nd 2013. * Used for sanity checks. */ public static final long EARLIEST_LAST_PING = 1367500000000L; - public static final long MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000; - public static final long MILLISECONDS_PER_SIX_MONTHS = 180 * MILLISECONDS_PER_DAY; - // Not `final` so we have the option to turn this on at runtime with a magic addon. public static boolean UPLOAD_FEATURE_DISABLED = false; // Android SharedPreferences branch where global (not per-profile) uploader // settings are stored. public static final String PREFS_BRANCH = "background"; // How frequently the submission policy is ticked over. This is how frequently our // intent is scheduled to be called by the Android Alarm Manager, not how // frequently we actually submit. public static final String PREF_SUBMISSION_INTENT_INTERVAL_MSEC = "healthreport_submission_intent_interval_msec"; - public static final long DEFAULT_SUBMISSION_INTENT_INTERVAL_MSEC = MILLISECONDS_PER_DAY / 24; + public static final long DEFAULT_SUBMISSION_INTENT_INTERVAL_MSEC = GlobalConstants.MILLISECONDS_PER_DAY / 24; public static final String ACTION_HEALTHREPORT_UPLOAD_PREF = "@ANDROID_PACKAGE_NAME@.HEALTHREPORT_UPLOAD_PREF"; public static final String PREF_MINIMUM_TIME_BETWEEN_UPLOADS = "healthreport_time_between_uploads"; - public static final long DEFAULT_MINIMUM_TIME_BETWEEN_UPLOADS = MILLISECONDS_PER_DAY; + public static final long DEFAULT_MINIMUM_TIME_BETWEEN_UPLOADS = GlobalConstants.MILLISECONDS_PER_DAY; public static final String PREF_MINIMUM_TIME_BEFORE_FIRST_SUBMISSION = "healthreport_time_before_first_submission"; - public static final long DEFAULT_MINIMUM_TIME_BEFORE_FIRST_SUBMISSION = MILLISECONDS_PER_DAY; + public static final long DEFAULT_MINIMUM_TIME_BEFORE_FIRST_SUBMISSION = GlobalConstants.MILLISECONDS_PER_DAY; public static final String PREF_MINIMUM_TIME_AFTER_FAILURE = "healthreport_time_after_failure"; public static final long DEFAULT_MINIMUM_TIME_AFTER_FAILURE = DEFAULT_SUBMISSION_INTENT_INTERVAL_MSEC; public static final String PREF_MAXIMUM_FAILURES_PER_DAY = "healthreport_maximum_failures_per_day"; public static final long DEFAULT_MAXIMUM_FAILURES_PER_DAY = 2; // Authoritative.
--- a/mobile/android/base/background/healthreport/HealthReportDatabaseStorage.java +++ b/mobile/android/base/background/healthreport/HealthReportDatabaseStorage.java @@ -7,16 +7,17 @@ package org.mozilla.gecko.background.hea import java.io.File; import java.util.Collection; import java.util.HashMap; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executor; import java.util.concurrent.Executors; import org.json.JSONObject; +import org.mozilla.gecko.background.common.DateUtils; import org.mozilla.gecko.background.common.log.Logger; import org.mozilla.gecko.background.healthreport.HealthReportStorage.MeasurementFields.FieldSpec; import android.content.ContentValues; import android.content.Context; import android.content.ContextWrapper; import android.database.Cursor; import android.database.SQLException; @@ -1024,17 +1025,17 @@ public class HealthReportDatabaseStorage } public void abortInitialization() { this.helper.getWritableDatabase().endTransaction(); } @Override public int getDay(long time) { - return HealthReportUtils.getDay(time); + return DateUtils.getDay(time); } @Override public int getDay() { return this.getDay(System.currentTimeMillis()); } private void recordDailyLast(int env, int day, int field, Object value, String table) {
--- a/mobile/android/base/background/healthreport/HealthReportGenerator.java +++ b/mobile/android/base/background/healthreport/HealthReportGenerator.java @@ -5,31 +5,34 @@ package org.mozilla.gecko.background.healthreport; import java.util.HashSet; import java.util.Iterator; import java.util.Set; import org.json.JSONException; import org.json.JSONObject; +import org.mozilla.gecko.background.common.DateUtils.DateFormatter; import org.mozilla.gecko.background.common.log.Logger; import org.mozilla.gecko.background.healthreport.HealthReportStorage.Field; import android.database.Cursor; import android.util.SparseArray; public class HealthReportGenerator { private static final int PAYLOAD_VERSION = 3; private static final String LOG_TAG = "GeckoHealthGen"; private final HealthReportStorage storage; + private final DateFormatter dateFormatter; public HealthReportGenerator(HealthReportStorage storage) { this.storage = storage; + this.dateFormatter = new DateFormatter(); } @SuppressWarnings("static-method") protected long now() { return System.currentTimeMillis(); } /** @@ -71,20 +74,20 @@ public class HealthReportGenerator { } // We want to map field IDs to some strings as we go. SparseArray<Environment> envs = storage.getEnvironmentRecordsByID(); JSONObject document = new JSONObject(); if (lastPingTime >= HealthReportConstants.EARLIEST_LAST_PING) { - document.put("lastPingDate", HealthReportUtils.getDateString(lastPingTime)); + document.put("lastPingDate", dateFormatter.getDateString(lastPingTime)); } - document.put("thisPingDate", HealthReportUtils.getDateString(now())); + document.put("thisPingDate", dateFormatter.getDateString(now())); document.put("version", PAYLOAD_VERSION); document.put("environments", getEnvironmentsJSON(currentEnvironment, envs)); document.put("data", getDataJSON(currentEnvironment, envs, since)); return document; } @@ -142,17 +145,17 @@ public class HealthReportGenerator { int cField = cursor.getInt(2); Logger.trace(LOG_TAG, "Event row: " + cDate + ", " + cEnv + ", " + cField); boolean dateChanged = cDate != lastDate; boolean envChanged = cEnv != lastEnv; if (dateChanged) { if (dateObject != null) { - days.put(HealthReportUtils.getDateStringForDay(lastDate), dateObject); + days.put(dateFormatter.getDateStringForDay(lastDate), dateObject); } dateObject = new JSONObject(); lastDate = cDate; } if (dateChanged || envChanged) { envObject = new JSONObject(); // This is safe because we checked above that cEnv is valid. @@ -174,17 +177,17 @@ public class HealthReportGenerator { // How we record depends on the type of the field, so we // break this out into a separate method for clarity. recordMeasurementFromCursor(field, measurement, cursor); cursor.moveToNext(); continue; } - days.put(HealthReportUtils.getDateStringForDay(lastDate), dateObject); + days.put(dateFormatter.getDateStringForDay(lastDate), dateObject); } finally { cursor.close(); } return days; } /** * Return the {@link JSONObject} parsed from the provided index of the given
--- a/mobile/android/base/background/healthreport/HealthReportUtils.java +++ b/mobile/android/base/background/healthreport/HealthReportUtils.java @@ -1,53 +1,36 @@ /* 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.background.healthreport; -import java.text.SimpleDateFormat; import java.util.HashSet; import java.util.Iterator; -import java.util.Locale; import java.util.Set; import java.util.SortedSet; -import java.util.TimeZone; import java.util.TreeSet; import java.util.UUID; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import org.mozilla.apache.commons.codec.digest.DigestUtils; import android.content.ContentUris; import android.net.Uri; public class HealthReportUtils { public static final String LOG_TAG = HealthReportUtils.class.getSimpleName(); - public static int getDay(final long time) { - return (int) Math.floor(time / HealthReportConstants.MILLISECONDS_PER_DAY); - } - public static String getEnvironmentHash(final String input) { return DigestUtils.shaHex(input); } - public static String getDateStringForDay(long day) { - return getDateString(HealthReportConstants.MILLISECONDS_PER_DAY * day); - } - - public static String getDateString(long time) { - final SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd", Locale.US); - format.setTimeZone(TimeZone.getTimeZone("UTC")); - return format.format(time); - } - /** * Take an environment URI (one that identifies an environment) and produce an * event URI. * * That this is needed is tragic. * * @param environmentURI * the {@link Uri} returned by an environment operation.
--- a/mobile/android/base/background/healthreport/upload/AndroidSubmissionClient.java +++ b/mobile/android/base/background/healthreport/upload/AndroidSubmissionClient.java @@ -5,16 +5,17 @@ package org.mozilla.gecko.background.healthreport.upload; import java.io.IOException; import java.util.Collection; import org.json.JSONObject; import org.mozilla.gecko.background.bagheera.BagheeraClient; import org.mozilla.gecko.background.bagheera.BagheeraRequestDelegate; +import org.mozilla.gecko.background.common.GlobalConstants; import org.mozilla.gecko.background.common.log.Logger; import org.mozilla.gecko.background.healthreport.EnvironmentBuilder; import org.mozilla.gecko.background.healthreport.HealthReportConstants; import org.mozilla.gecko.background.healthreport.HealthReportDatabaseStorage; import org.mozilla.gecko.background.healthreport.HealthReportGenerator; import org.mozilla.gecko.sync.net.BaseResource; import android.content.ContentProviderClient; @@ -98,17 +99,17 @@ public class AndroidSubmissionClient imp // close it. It's worth noting that this call will fail if called // out-of-process. HealthReportDatabaseStorage storage = EnvironmentBuilder.getStorage(client, profilePath); if (storage == null) { delegate.onHardFailure(localTime, null, "No storage when generating report.", null); return; } - long since = localTime - HealthReportConstants.MILLISECONDS_PER_SIX_MONTHS; + long since = localTime - GlobalConstants.MILLISECONDS_PER_SIX_MONTHS; long last = Math.max(getLastUploadLocalTime(), HealthReportConstants.EARLIEST_LAST_PING); if (!storage.hasEventSince(last)) { delegate.onHardFailure(localTime, null, "No new events in storage.", null); return; } HealthReportGenerator generator = new HealthReportGenerator(storage);
--- a/mobile/android/base/health/BrowserHealthReporter.java +++ b/mobile/android/base/health/BrowserHealthReporter.java @@ -9,16 +9,17 @@ import android.content.ContentProviderCl import android.content.Context; import android.util.Log; import org.mozilla.gecko.GeckoAppShell; import org.mozilla.gecko.GeckoEvent; import org.mozilla.gecko.GeckoProfile; import org.mozilla.gecko.background.healthreport.EnvironmentBuilder; +import org.mozilla.gecko.background.common.GlobalConstants; import org.mozilla.gecko.background.healthreport.HealthReportConstants; import org.mozilla.gecko.background.healthreport.HealthReportDatabaseStorage; import org.mozilla.gecko.background.healthreport.HealthReportGenerator; import org.mozilla.gecko.util.GeckoEventListener; import org.mozilla.gecko.util.ThreadUtils; import org.json.JSONException; @@ -119,17 +120,17 @@ public class BrowserHealthReporter imple * @throws JSONException if JSON generation fails. * @throws IllegalStateException if the environment does not allow to generate a report. * @return non-null Health Report. */ public JSONObject generateReport() throws JSONException { GeckoProfile profile = GeckoAppShell.getGeckoInterface().getProfile(); String profilePath = profile.getDir().getAbsolutePath(); - long since = System.currentTimeMillis() - HealthReportConstants.MILLISECONDS_PER_SIX_MONTHS; + long since = System.currentTimeMillis() - GlobalConstants.MILLISECONDS_PER_SIX_MONTHS; long lastPingTime = Math.max(getLastUploadLocalTime(), HealthReportConstants.EARLIEST_LAST_PING); return generateReport(since, lastPingTime, profilePath); } @Override public void handleMessage(String event, JSONObject message) { try {
--- a/mobile/android/base/home/BookmarksPage.java +++ b/mobile/android/base/home/BookmarksPage.java @@ -174,17 +174,17 @@ public class BookmarksPage extends HomeF }); mList.setAdapter(mListAdapter); // Invalidate the cached value that keeps track of whether or // not desktop bookmarks (or reading list items) exist. BrowserDB.invalidateCachedState(); // Create callbacks before the initial loader is started. - mLoaderCallbacks = new CursorLoaderCallbacks(activity, getLoaderManager()); + mLoaderCallbacks = new CursorLoaderCallbacks(); mThumbnailsLoaderCallbacks = new ThumbnailsLoaderCallbacks(); loadIfVisible(); } @Override public void onDestroyView() { mList = null; mListAdapter = null; @@ -448,49 +448,42 @@ public class BookmarksPage extends HomeF final int max = getContext().getResources().getInteger(R.integer.number_of_top_sites); return BrowserDB.getTopBookmarks(getContext().getContentResolver(), max); } } /** * Loader callbacks for the LoaderManager of this fragment. */ - private class CursorLoaderCallbacks extends HomeCursorLoaderCallbacks { - public CursorLoaderCallbacks(Context context, LoaderManager loaderManager) { - super(context, loaderManager); - } - + private class CursorLoaderCallbacks implements LoaderCallbacks<Cursor> { @Override public Loader<Cursor> onCreateLoader(int id, Bundle args) { switch(id) { case LOADER_ID_BOOKMARKS_LIST: { if (args == null) { return new BookmarksLoader(getActivity()); } else { return new BookmarksLoader(getActivity(), args.getInt(BOOKMARKS_FOLDER_KEY)); } } case LOADER_ID_TOP_BOOKMARKS: { return new TopBookmarksLoader(getActivity()); } + } - default: { - return super.onCreateLoader(id, args); - } - } + return null; } @Override public void onLoadFinished(Loader<Cursor> loader, Cursor c) { final int loaderId = loader.getId(); switch(loaderId) { case LOADER_ID_BOOKMARKS_LIST: { mListAdapter.swapCursor(c); - loadFavicons(c); mList.setHeaderDividersEnabled(c != null && c.getCount() > 0); break; } case LOADER_ID_TOP_BOOKMARKS: { mTopBookmarksAdapter.swapCursor(c); // Load the thumbnails. @@ -504,21 +497,16 @@ public class BookmarksPage extends HomeF if (urls.size() > 0) { Bundle bundle = new Bundle(); bundle.putStringArrayList(THUMBNAILS_URLS_KEY, urls); getLoaderManager().restartLoader(LOADER_ID_THUMBNAILS, bundle, mThumbnailsLoaderCallbacks); } } break; } - - default: { - super.onLoadFinished(loader, c); - break; - } } } @Override public void onLoaderReset(Loader<Cursor> loader) { final int loaderId = loader.getId(); switch(loaderId) { case LOADER_ID_BOOKMARKS_LIST: { @@ -529,28 +517,18 @@ public class BookmarksPage extends HomeF } case LOADER_ID_TOP_BOOKMARKS: { if (mTopBookmarks != null) { mTopBookmarksAdapter.swapCursor(null); break; } } - - default: { - super.onLoaderReset(loader); - break; - } } } - - @Override - public void onFaviconsLoaded() { - mListAdapter.notifyDataSetChanged(); - } } /** * An AsyncTaskLoader to load the thumbnails from a cursor. */ private static class ThumbnailsLoader extends AsyncTaskLoader<Map<String, Thumbnail>> { private Map<String, Thumbnail> mThumbnails; private ArrayList<String> mUrls;
--- a/mobile/android/base/home/BrowserSearch.java +++ b/mobile/android/base/home/BrowserSearch.java @@ -99,17 +99,17 @@ public class BrowserSearch extends HomeF private volatile SuggestClient mSuggestClient; // List of search engines from gecko private ArrayList<SearchEngine> mSearchEngines; // Whether search suggestions are enabled or not private boolean mSuggestionsEnabled; - // Callbacks used for the search and favicon cursor loaders + // Callbacks used for the search loader private CursorLoaderCallbacks mCursorLoaderCallbacks; // Callbacks used for the search suggestion loader private SuggestionLoaderCallbacks mSuggestionLoaderCallbacks; // Inflater used by the adapter private LayoutInflater mInflater; @@ -281,27 +281,25 @@ public class BrowserSearch extends HomeF GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("SearchEngines:Get", null)); } @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); - final Activity activity = getActivity(); - // Intialize the search adapter - mAdapter = new SearchAdapter(activity); + mAdapter = new SearchAdapter(getActivity()); mList.setAdapter(mAdapter); // Only create an instance when we need it mSuggestionLoaderCallbacks = null; // Create callbacks before the initial loader is started - mCursorLoaderCallbacks = new CursorLoaderCallbacks(activity, getLoaderManager()); + mCursorLoaderCallbacks = new CursorLoaderCallbacks(); loadIfVisible(); } @Override public void handleMessage(String event, final JSONObject message) { if (event.equals("SearchEngines:Data")) { ThreadUtils.postToUiThread(new Runnable() { @Override @@ -767,59 +765,36 @@ public class BrowserSearch extends HomeF return -1; } // Return search engine index return position - resultCount; } } - private class CursorLoaderCallbacks extends HomeCursorLoaderCallbacks { - public CursorLoaderCallbacks(Context context, LoaderManager loaderManager) { - super(context, loaderManager); - } - + private class CursorLoaderCallbacks implements LoaderCallbacks<Cursor> { @Override public Loader<Cursor> onCreateLoader(int id, Bundle args) { - if (id == LOADER_ID_SEARCH) { - return SearchLoader.createInstance(getActivity(), args); - } else { - return super.onCreateLoader(id, args); - } + return SearchLoader.createInstance(getActivity(), args); } @Override public void onLoadFinished(Loader<Cursor> loader, Cursor c) { - if (loader.getId() == LOADER_ID_SEARCH) { - mAdapter.swapCursor(c); + mAdapter.swapCursor(c); - // We should handle autocompletion based on the search term - // associated with the currently loader that has just provided - // the results. - SearchCursorLoader searchLoader = (SearchCursorLoader) loader; - handleAutocomplete(searchLoader.getSearchTerm(), c); - - loadFavicons(c); - } else { - super.onLoadFinished(loader, c); - } + // We should handle autocompletion based on the search term + // associated with the currently loader that has just provided + // the results. + SearchCursorLoader searchLoader = (SearchCursorLoader) loader; + handleAutocomplete(searchLoader.getSearchTerm(), c); } @Override public void onLoaderReset(Loader<Cursor> loader) { - if (loader.getId() == LOADER_ID_SEARCH) { - mAdapter.swapCursor(null); - } else { - super.onLoaderReset(loader); - } - } - - @Override - public void onFaviconsLoaded() { - mAdapter.notifyDataSetChanged(); + mAdapter.swapCursor(null); } } private class SuggestionLoaderCallbacks implements LoaderCallbacks<ArrayList<String>> { @Override public Loader<ArrayList<String>> onCreateLoader(int id, Bundle args) { // mSuggestClient is set to null in onDestroyView(), so using it // safely here relies on the fact that onCreateLoader() is called
deleted file mode 100644 --- a/mobile/android/base/home/FaviconsLoader.java +++ /dev/null @@ -1,115 +0,0 @@ -/* -*- 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.home; - -import org.mozilla.gecko.favicons.Favicons; -import org.mozilla.gecko.db.BrowserDB; -import org.mozilla.gecko.db.BrowserDB.URLColumns; -import org.mozilla.gecko.gfx.BitmapUtils; - -import android.content.ContentResolver; -import android.content.Context; -import android.database.Cursor; -import android.graphics.Bitmap; -import android.os.Bundle; -import android.support.v4.app.LoaderManager; -import android.support.v4.app.LoaderManager.LoaderCallbacks; -import android.support.v4.content.Loader; - -import java.util.ArrayList; - -/** - * Encapsulates the implementation of the favicons cursorloader. - */ -class FaviconsLoader { - // Argument containing list of urls for the favicons loader - private static final String FAVICONS_LOADER_URLS_ARG = "urls"; - - private FaviconsLoader() { - } - - private static ArrayList<String> getUrlsWithoutFavicon(Cursor c) { - ArrayList<String> urls = new ArrayList<String>(); - - if (c == null || !c.moveToFirst()) { - return urls; - } - - do { - final String url = c.getString(c.getColumnIndexOrThrow(URLColumns.URL)); - - // We only want to load favicons from DB if they are not in the - // memory cache yet. The url is null for bookmark folders. - if (url == null || Favicons.getFaviconFromMemCache(url) != null) { - continue; - } - - urls.add(url); - } while (c.moveToNext()); - - return urls; - } - - private static void storeFaviconsInMemCache(Cursor c) { - if (c == null || !c.moveToFirst()) { - return; - } - - do { - final String url = c.getString(c.getColumnIndexOrThrow(URLColumns.URL)); - final byte[] b = c.getBlob(c.getColumnIndexOrThrow(URLColumns.FAVICON)); - - if (b == null) { - continue; - } - - Bitmap favicon = BitmapUtils.decodeByteArray(b); - if (favicon == null) { - continue; - } - - favicon = Favicons.scaleImage(favicon); - Favicons.putFaviconInMemCache(url, favicon); - } while (c.moveToNext()); - } - - public static void restartFromCursor(LoaderManager manager, int loaderId, - LoaderCallbacks<Cursor> callbacks, Cursor c) { - // If there urls without in-memory favicons, trigger a new loader - // to load the images from disk to memory. - ArrayList<String> urls = getUrlsWithoutFavicon(c); - if (urls.size() > 0) { - Bundle args = new Bundle(); - args.putStringArrayList(FAVICONS_LOADER_URLS_ARG, urls); - - manager.restartLoader(loaderId, args, callbacks); - } - } - - public static Loader<Cursor> createInstance(Context context, Bundle args) { - final ArrayList<String> urls = args.getStringArrayList(FAVICONS_LOADER_URLS_ARG); - return new FaviconsCursorLoader(context, urls); - } - - private static class FaviconsCursorLoader extends SimpleCursorLoader { - private final ArrayList<String> mUrls; - - public FaviconsCursorLoader(Context context, ArrayList<String> urls) { - super(context); - mUrls = urls; - } - - @Override - public Cursor loadCursor() { - final ContentResolver cr = getContext().getContentResolver(); - - Cursor c = BrowserDB.getFaviconsForUrls(cr, mUrls); - storeFaviconsInMemCache(c); - - return c; - } - } -}
--- a/mobile/android/base/home/HomeBanner.java +++ b/mobile/android/base/home/HomeBanner.java @@ -1,25 +1,40 @@ /* -*- 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.home; +import org.mozilla.gecko.GeckoAppShell; +import org.mozilla.gecko.GeckoEvent; import org.mozilla.gecko.R; +import org.mozilla.gecko.gfx.BitmapUtils; +import org.mozilla.gecko.util.GeckoEventListener; +import org.mozilla.gecko.util.ThreadUtils; + +import org.json.JSONException; +import org.json.JSONObject; import android.content.Context; +import android.graphics.drawable.Drawable; +import android.text.TextUtils; import android.util.AttributeSet; +import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.widget.ImageButton; +import android.widget.ImageView; import android.widget.LinearLayout; +import android.widget.TextView; -public class HomeBanner extends LinearLayout { +public class HomeBanner extends LinearLayout + implements GeckoEventListener { + private static final String LOGTAG = "GeckoHomeBanner"; public HomeBanner(Context context) { this(context, null); } public HomeBanner(Context context, AttributeSet attrs) { super(context, attrs); @@ -38,14 +53,83 @@ public class HomeBanner extends LinearLa closeButton.getDrawable().setAlpha(127); closeButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { HomeBanner.this.setVisibility(View.GONE); } }); + + setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + // Send the current message id back to JS. + GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("HomeBanner:Click", (String) getTag())); + } + }); + + GeckoAppShell.getEventDispatcher().registerEventListener("HomeBanner:Data", this); + GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("HomeBanner:Get", null)); } + @Override + public void onDetachedFromWindow() { + super.onDetachedFromWindow(); + + GeckoAppShell.getEventDispatcher().unregisterEventListener("HomeBanner:Data", this); + } + public boolean isDismissed() { return (getVisibility() == View.GONE); } + + @Override + public void handleMessage(String event, JSONObject message) { + try { + // Store the current message id to pass back to JS in the view's OnClickListener. + setTag(message.getString("id")); + + final String text = message.getString("text"); + final TextView textView = (TextView) findViewById(R.id.text); + + // Update the banner message on the UI thread. + ThreadUtils.postToUiThread(new Runnable() { + @Override + public void run() { + textView.setText(text); + setVisibility(View.VISIBLE); + } + }); + } catch (JSONException e) { + Log.e(LOGTAG, "Exception handling " + event + " message", e); + return; + } + + final String iconURI = message.optString("iconURI"); + final ImageView iconView = (ImageView) findViewById(R.id.icon); + + if (TextUtils.isEmpty(iconURI)) { + // Hide the image view if we don't have an icon to show. + iconView.setVisibility(View.GONE); + return; + } + + BitmapUtils.getDrawable(getContext(), iconURI, new BitmapUtils.BitmapLoader() { + @Override + public void onBitmapFound(final Drawable d) { + // Bail if getDrawable doesn't find anything. + if (d == null) { + iconView.setVisibility(View.GONE); + return; + } + + // Update the banner icon on the UI thread. + ThreadUtils.postToUiThread(new Runnable() { + @Override + public void run() { + iconView.setImageDrawable(d); + } + }); + } + }); + } }
deleted file mode 100644 --- a/mobile/android/base/home/HomeCursorLoaderCallbacks.java +++ /dev/null @@ -1,58 +0,0 @@ -/* -*- 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.home; - -import android.content.Context; -import android.database.Cursor; -import android.os.Bundle; -import android.support.v4.app.LoaderManager; -import android.support.v4.app.LoaderManager.LoaderCallbacks; -import android.support.v4.content.Loader; - -/** - * Cursor loader callbacks that takes care loading favicons into memory. - */ -abstract class HomeCursorLoaderCallbacks implements LoaderCallbacks<Cursor> { - - // Cursor loader ID for favicons query - private static final int LOADER_ID_FAVICONS = 100; - - private final Context mContext; - private final LoaderManager mLoaderManager; - - public HomeCursorLoaderCallbacks(Context context, LoaderManager loaderManager) { - mContext = context; - mLoaderManager = loaderManager; - } - - public void loadFavicons(Cursor cursor) { - FaviconsLoader.restartFromCursor(mLoaderManager, LOADER_ID_FAVICONS, this, cursor); - } - - @Override - public Loader<Cursor> onCreateLoader(int id, Bundle args) { - if (id == LOADER_ID_FAVICONS) { - return FaviconsLoader.createInstance(mContext, args); - } - - return null; - } - - @Override - public void onLoadFinished(Loader<Cursor> loader, Cursor c) { - if (loader.getId() == LOADER_ID_FAVICONS) { - onFaviconsLoaded(); - } - } - - @Override - public void onLoaderReset(Loader<Cursor> loader) { - // Do nothing by default. - } - - // Callback for favicons loaded in memory. - public abstract void onFaviconsLoaded(); -}
--- a/mobile/android/base/home/LastTabsPage.java +++ b/mobile/android/base/home/LastTabsPage.java @@ -137,24 +137,22 @@ public class LastTabsPage extends HomeFr mEmptyView = null; mRestoreButton = null; } @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); - final Activity activity = getActivity(); - // Intialize adapter - mAdapter = new LastTabsAdapter(activity); + mAdapter = new LastTabsAdapter(getActivity()); mList.setAdapter(mAdapter); // Create callbacks before the initial loader is started - mCursorLoaderCallbacks = new CursorLoaderCallbacks(activity, getLoaderManager()); + mCursorLoaderCallbacks = new CursorLoaderCallbacks(); loadIfVisible(); } private void updateUiFromCursor(Cursor c) { if (c != null && c.getCount() > 0) { if (mTitle != null) { mTitle.setVisibility(View.VISIBLE); } @@ -257,48 +255,26 @@ public class LastTabsPage extends HomeFr } @Override public View newView(Context context, Cursor cursor, ViewGroup parent) { return LayoutInflater.from(context).inflate(R.layout.home_item_row, parent, false); } } - private class CursorLoaderCallbacks extends HomeCursorLoaderCallbacks { - public CursorLoaderCallbacks(Context context, LoaderManager loaderManager) { - super(context, loaderManager); - } - + private class CursorLoaderCallbacks implements LoaderCallbacks<Cursor> { @Override public Loader<Cursor> onCreateLoader(int id, Bundle args) { - if (id == LOADER_ID_LAST_TABS) { - return new LastTabsCursorLoader(getActivity()); - } else { - return super.onCreateLoader(id, args); - } + return new LastTabsCursorLoader(getActivity()); } @Override public void onLoadFinished(Loader<Cursor> loader, Cursor c) { - if (loader.getId() == LOADER_ID_LAST_TABS) { - mAdapter.swapCursor(c); - updateUiFromCursor(c); - loadFavicons(c); - } else { - super.onLoadFinished(loader, c); - } + mAdapter.swapCursor(c); + updateUiFromCursor(c); } @Override public void onLoaderReset(Loader<Cursor> loader) { - if (loader.getId() == LOADER_ID_LAST_TABS) { - mAdapter.swapCursor(null); - } else { - super.onLoaderReset(loader); - } - } - - @Override - public void onFaviconsLoaded() { - mAdapter.notifyDataSetChanged(); + mAdapter.swapCursor(null); } } }
--- a/mobile/android/base/home/MostRecentPage.java +++ b/mobile/android/base/home/MostRecentPage.java @@ -12,16 +12,17 @@ import org.mozilla.gecko.home.HomePager. import org.mozilla.gecko.home.TwoLinePageRow; import android.app.Activity; import android.content.ContentResolver; import android.content.Context; import android.database.Cursor; import android.os.Bundle; import android.support.v4.app.LoaderManager; +import android.support.v4.app.LoaderManager.LoaderCallbacks; import android.support.v4.content.Loader; import android.util.SparseArray; import android.view.LayoutInflater; import android.view.View; import android.view.ViewStub; import android.view.ViewGroup; import android.widget.AdapterView; import android.widget.ImageView; @@ -122,24 +123,22 @@ public class MostRecentPage extends Home mTitle = null; mEmptyView = null; } @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); - final Activity activity = getActivity(); - // Intialize adapter - mAdapter = new MostRecentAdapter(activity); + mAdapter = new MostRecentAdapter(getActivity()); mList.setAdapter(mAdapter); // Create callbacks before the initial loader is started - mCursorLoaderCallbacks = new CursorLoaderCallbacks(activity, getLoaderManager()); + mCursorLoaderCallbacks = new CursorLoaderCallbacks(); loadIfVisible(); } @Override protected void load() { getLoaderManager().initLoader(LOADER_ID_HISTORY, null, mCursorLoaderCallbacks); } @@ -355,48 +354,26 @@ public class MostRecentPage extends Home // Reached the last section, no need to continue if (section == MostRecentSection.OLDER) { break; } } while (c.moveToNext()); } } - private class CursorLoaderCallbacks extends HomeCursorLoaderCallbacks { - public CursorLoaderCallbacks(Context context, LoaderManager loaderManager) { - super(context, loaderManager); - } - + private class CursorLoaderCallbacks implements LoaderCallbacks<Cursor> { @Override public Loader<Cursor> onCreateLoader(int id, Bundle args) { - if (id == LOADER_ID_HISTORY) { - return new MostRecentCursorLoader(getActivity()); - } else { - return super.onCreateLoader(id, args); - } + return new MostRecentCursorLoader(getActivity()); } @Override public void onLoadFinished(Loader<Cursor> loader, Cursor c) { - if (loader.getId() == LOADER_ID_HISTORY) { - mAdapter.swapCursor(c); - updateUiFromCursor(c); - loadFavicons(c); - } else { - super.onLoadFinished(loader, c); - } + mAdapter.swapCursor(c); + updateUiFromCursor(c); } @Override public void onLoaderReset(Loader<Cursor> loader) { - if (loader.getId() == LOADER_ID_HISTORY) { - mAdapter.swapCursor(null); - } else { - super.onLoaderReset(loader); - } - } - - @Override - public void onFaviconsLoaded() { - mAdapter.notifyDataSetChanged(); + mAdapter.swapCursor(null); } } }
--- a/mobile/android/base/home/MostVisitedPage.java +++ b/mobile/android/base/home/MostVisitedPage.java @@ -11,16 +11,17 @@ import org.mozilla.gecko.db.BrowserDB.UR import org.mozilla.gecko.home.HomePager.OnUrlOpenListener; import android.app.Activity; import android.content.ContentResolver; import android.content.Context; import android.database.Cursor; import android.os.Bundle; import android.support.v4.app.LoaderManager; +import android.support.v4.app.LoaderManager.LoaderCallbacks; import android.support.v4.content.Loader; import android.support.v4.widget.CursorAdapter; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.ViewStub; import android.widget.AdapterView; import android.widget.ImageView; @@ -124,24 +125,22 @@ public class MostVisitedPage extends Hom mTitle = null; mEmptyView = null; } @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); - final Activity activity = getActivity(); - // Intialize the search adapter - mAdapter = new VisitedAdapter(activity, null); + mAdapter = new VisitedAdapter(getActivity(), null); mList.setAdapter(mAdapter); // Create callbacks before the initial loader is started - mCursorLoaderCallbacks = new CursorLoaderCallbacks(activity, getLoaderManager()); + mCursorLoaderCallbacks = new CursorLoaderCallbacks(); loadIfVisible(); } @Override protected void load() { getLoaderManager().initLoader(LOADER_ID_FRECENCY, null, mCursorLoaderCallbacks); } @@ -201,48 +200,26 @@ public class MostVisitedPage extends Hom } @Override public View newView(Context context, Cursor cursor, ViewGroup parent) { return LayoutInflater.from(parent.getContext()).inflate(R.layout.home_item_row, parent, false); } } - private class CursorLoaderCallbacks extends HomeCursorLoaderCallbacks { - public CursorLoaderCallbacks(Context context, LoaderManager loaderManager) { - super(context, loaderManager); - } - + private class CursorLoaderCallbacks implements LoaderCallbacks<Cursor> { @Override public Loader<Cursor> onCreateLoader(int id, Bundle args) { - if (id == LOADER_ID_FRECENCY) { - return new FrecencyCursorLoader(getActivity()); - } else { - return super.onCreateLoader(id, args); - } + return new FrecencyCursorLoader(getActivity()); } @Override public void onLoadFinished(Loader<Cursor> loader, Cursor c) { - if (loader.getId() == LOADER_ID_FRECENCY) { - mAdapter.swapCursor(c); - updateUiFromCursor(c); - loadFavicons(c); - } else { - super.onLoadFinished(loader, c); - } + mAdapter.swapCursor(c); + updateUiFromCursor(c); } @Override public void onLoaderReset(Loader<Cursor> loader) { - if (loader.getId() == LOADER_ID_FRECENCY) { - mAdapter.swapCursor(null); - } else { - super.onLoaderReset(loader); - } - } - - @Override - public void onFaviconsLoaded() { - mAdapter.notifyDataSetChanged(); + mAdapter.swapCursor(null); } } }
--- a/mobile/android/base/home/PinBookmarkDialog.java +++ b/mobile/android/base/home/PinBookmarkDialog.java @@ -9,16 +9,17 @@ import org.mozilla.gecko.R; import org.mozilla.gecko.db.BrowserDB.URLColumns; import android.app.Activity; import android.content.Context; import android.database.Cursor; import android.os.Bundle; import android.support.v4.app.DialogFragment; import android.support.v4.app.LoaderManager; +import android.support.v4.app.LoaderManager.LoaderCallbacks; import android.support.v4.content.Loader; import android.support.v4.widget.CursorAdapter; import android.text.Editable; import android.text.TextUtils; import android.text.TextWatcher; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -33,32 +34,29 @@ class PinBookmarkDialog extends DialogFr // Listener for url selection public static interface OnBookmarkSelectedListener { public void onBookmarkSelected(String url, String title); } // Cursor loader ID for search query private static final int LOADER_ID_SEARCH = 0; - // Cursor loader ID for favicons query - private static final int LOADER_ID_FAVICONS = 1; - // Holds the current search term to use in the query private String mSearchTerm; // Adapter for the list of search results private SearchAdapter mAdapter; // Search entry private EditText mSearch; // Search results private ListView mList; - // Callbacks used for the search and favicon cursor loaders + // Callbacks used for the search loader private CursorLoaderCallbacks mLoaderCallbacks; // Bookmark selected listener private OnBookmarkSelectedListener mOnBookmarkSelectedListener; public static PinBookmarkDialog newInstance() { return new PinBookmarkDialog(); } @@ -120,25 +118,24 @@ class PinBookmarkDialog extends DialogFr } }); } @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); - final Activity activity = getActivity(); final LoaderManager manager = getLoaderManager(); // Initialize the search adapter - mAdapter = new SearchAdapter(activity); + mAdapter = new SearchAdapter(getActivity()); mList.setAdapter(mAdapter); // Create callbacks before the initial loader is started - mLoaderCallbacks = new CursorLoaderCallbacks(activity, manager); + mLoaderCallbacks = new CursorLoaderCallbacks(); // Reconnect to the loader only if present manager.initLoader(LOADER_ID_SEARCH, null, mLoaderCallbacks); // Default filter. filter(""); } @@ -174,47 +171,25 @@ class PinBookmarkDialog extends DialogFr } @Override public View newView(Context context, Cursor cursor, ViewGroup parent) { return (TwoLinePageRow) mInflater.inflate(R.layout.home_item_row, parent, false); } } - private class CursorLoaderCallbacks extends HomeCursorLoaderCallbacks { - public CursorLoaderCallbacks(Context context, LoaderManager loaderManager) { - super(context, loaderManager); - } - + private class CursorLoaderCallbacks implements LoaderCallbacks<Cursor> { @Override public Loader<Cursor> onCreateLoader(int id, Bundle args) { - if (id == LOADER_ID_SEARCH) { - return SearchLoader.createInstance(getActivity(), args); - } else { - return super.onCreateLoader(id, args); - } + return SearchLoader.createInstance(getActivity(), args); } @Override public void onLoadFinished(Loader<Cursor> loader, Cursor c) { - if (loader.getId() == LOADER_ID_SEARCH) { - mAdapter.swapCursor(c); - loadFavicons(c); - } else { - super.onLoadFinished(loader, c); - } + mAdapter.swapCursor(c); } @Override public void onLoaderReset(Loader<Cursor> loader) { - if (loader.getId() == LOADER_ID_SEARCH) { - mAdapter.swapCursor(null); - } else { - super.onLoaderReset(loader); - } - } - - @Override - public void onFaviconsLoaded() { - mAdapter.notifyDataSetChanged(); + mAdapter.swapCursor(null); } } }
--- a/mobile/android/base/home/ReadingListPage.java +++ b/mobile/android/base/home/ReadingListPage.java @@ -214,38 +214,23 @@ public class ReadingListPage extends Hom } /** * LoaderCallbacks implementation that interacts with the LoaderManager. */ private class CursorLoaderCallbacks implements LoaderCallbacks<Cursor> { @Override public Loader<Cursor> onCreateLoader(int id, Bundle args) { - switch(id) { - case LOADER_ID_READING_LIST: - return new ReadingListLoader(getActivity()); - } - return null; + return new ReadingListLoader(getActivity()); } @Override public void onLoadFinished(Loader<Cursor> loader, Cursor c) { - final int loaderId = loader.getId(); - switch(loaderId) { - case LOADER_ID_READING_LIST: - mAdapter.swapCursor(c); - break; - } - - updateUiFromCursor(c); + mAdapter.swapCursor(c); + updateUiFromCursor(c); } @Override public void onLoaderReset(Loader<Cursor> loader) { - final int loaderId = loader.getId(); - switch(loaderId) { - case LOADER_ID_READING_LIST: - mAdapter.swapCursor(null); - break; - } + mAdapter.swapCursor(null); } } }
--- a/mobile/android/base/home/TwoLinePageRow.java +++ b/mobile/android/base/home/TwoLinePageRow.java @@ -5,24 +5,29 @@ package org.mozilla.gecko.home; import org.mozilla.gecko.favicons.Favicons; import org.mozilla.gecko.R; import org.mozilla.gecko.Tab; import org.mozilla.gecko.Tabs; import org.mozilla.gecko.db.BrowserContract.Combined; +import org.mozilla.gecko.db.BrowserDB; import org.mozilla.gecko.db.BrowserDB.URLColumns; import org.mozilla.gecko.gfx.BitmapUtils; import org.mozilla.gecko.util.ThreadUtils; +import org.mozilla.gecko.util.UiAsyncTask; import org.mozilla.gecko.widget.FaviconView; +import android.content.ContentResolver; import android.content.Context; import android.database.Cursor; import android.graphics.Bitmap; +import android.os.AsyncTask; +import android.os.Build; import android.text.TextUtils; import android.util.AttributeSet; import android.view.Gravity; import android.view.LayoutInflater; import android.widget.LinearLayout; import android.widget.TextView; public class TwoLinePageRow extends LinearLayout @@ -35,16 +40,18 @@ public class TwoLinePageRow extends Line private int mUrlIconId; private int mBookmarkIconId; private boolean mShowIcons; // The URL for the page corresponding to this view. private String mPageUrl; + private LoadFaviconTask mLoadFaviconTask; + public TwoLinePageRow(Context context) { this(context, null); } public TwoLinePageRow(Context context, AttributeSet attrs) { super(context, attrs); setGravity(Gravity.CENTER_VERTICAL); @@ -69,16 +76,18 @@ public class TwoLinePageRow extends Line // Delay removing the listener to avoid modifying mTabsChangedListeners // while notifyListeners is iterating through the array. ThreadUtils.postToUiThread(new Runnable() { @Override public void run() { Tabs.unregisterOnTabsChangedListener(TwoLinePageRow.this); } }); + + cancelLoadFaviconTask(); } @Override public void onTabChanged(final Tab tab, final Tabs.TabEvents msg, final Object data) { switch(msg) { case ADDED: case CLOSED: case LOCATION_CHANGE: @@ -126,16 +135,26 @@ public class TwoLinePageRow extends Line * tab changes or is closed. */ private void updateDisplayedUrl(String url) { mPageUrl = url; updateDisplayedUrl(); } /** + * Cancels any pending favicon loading task associated with this view. + */ + private void cancelLoadFaviconTask() { + if (mLoadFaviconTask != null) { + mLoadFaviconTask.cancel(true); + mLoadFaviconTask = null; + } + } + + /** * Replaces the page URL with "Switch to tab" if there is already a tab open with that URL. */ private void updateDisplayedUrl() { int tabId = Tabs.getInstance().getTabIdForUrl(mPageUrl); if (!mShowIcons || tabId < 0) { setUrl(mPageUrl); setUrlIcon(NO_ICON); } else { @@ -158,34 +177,43 @@ public class TwoLinePageRow extends Line int urlIndex = cursor.getColumnIndexOrThrow(URLColumns.URL); final String url = cursor.getString(urlIndex); // Use the URL instead of an empty title for consistency with the normal URL // bar view - this is the equivalent of getDisplayTitle() in Tab.java setTitle(TextUtils.isEmpty(title) ? url : title); - updateDisplayedUrl(url); - - int faviconIndex = cursor.getColumnIndex(URLColumns.FAVICON); - if (faviconIndex != -1) { - byte[] b = cursor.getBlob(faviconIndex); + // No need to do extra work if the URL associated with this view + // hasn't changed. + if (TextUtils.equals(mPageUrl, url)) { + return; + } - Bitmap favicon = null; - if (b != null) { - Bitmap bitmap = BitmapUtils.decodeByteArray(b); - if (bitmap != null) { - favicon = Favicons.scaleImage(bitmap); - } - } + updateDisplayedUrl(url); + cancelLoadFaviconTask(); + // First, try to find the favicon in the memory cache. If it's not + // cached yet, try to load it from the database, off main thread. + final Bitmap favicon = Favicons.getFaviconFromMemCache(url); + if (favicon != null) { setFaviconWithUrl(favicon, url); } else { - // If favicons is not on the cursor, try to fetch it from the memory cache - setFaviconWithUrl(Favicons.getFaviconFromMemCache(url), url); + // Show blank image until the new favicon finishes loading + mFavicon.clearImage(); + + mLoadFaviconTask = new LoadFaviconTask(url); + + // Try to use a thread pool instead of serial execution of tasks + // to add more throughput to the favicon loading routines. + if (Build.VERSION.SDK_INT >= 11) { + mLoadFaviconTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } else { + mLoadFaviconTask.execute(); + } } // Don't show bookmark/reading list icon, if not needed. if (!mShowIcons) { return; } final int bookmarkIdIndex = cursor.getColumnIndex(Combined.BOOKMARK_ID); @@ -208,9 +236,42 @@ public class TwoLinePageRow extends Line setBookmarkIcon(R.drawable.ic_url_bar_reader); } else { setBookmarkIcon(R.drawable.ic_url_bar_star); } } else { setBookmarkIcon(NO_ICON); } } + + private class LoadFaviconTask extends AsyncTask<Void, Void, Bitmap> { + private final String mUrl; + + public LoadFaviconTask(String url) { + mUrl = url; + } + + @Override + public Bitmap doInBackground(Void... params) { + Bitmap favicon = Favicons.getFaviconFromMemCache(mUrl); + if (favicon == null) { + final ContentResolver cr = getContext().getContentResolver(); + + final Bitmap faviconFromDb = BrowserDB.getFaviconForUrl(cr, mUrl); + if (faviconFromDb != null) { + favicon = Favicons.scaleImage(faviconFromDb); + Favicons.putFaviconInMemCache(mUrl, favicon); + } + } + + return favicon; + } + + @Override + public void onPostExecute(Bitmap favicon) { + if (TextUtils.equals(mPageUrl, mUrl)) { + setFaviconWithUrl(favicon, mUrl); + } + + mLoadFaviconTask = null; + } + } }
--- a/mobile/android/base/resources/layout/home_banner.xml +++ b/mobile/android/base/resources/layout/home_banner.xml @@ -4,23 +4,23 @@ - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> <merge xmlns:android="http://schemas.android.com/apk/res/android"> <ImageView android:id="@+id/icon" android:layout_width="48dip" android:layout_height="48dip" android:layout_marginLeft="10dp" - android:layout_marginRight="10dp" android:scaleType="centerInside"/> <TextView android:id="@+id/text" android:layout_width="0dip" android:layout_height="fill_parent" android:layout_weight="1" + android:layout_marginLeft="10dp" android:paddingTop="7dp" android:paddingBottom="7dp" android:textAppearance="@style/TextAppearance.Widget.HomeBanner" android:layout_gravity="bottom" android:singleLine="false" android:maxLines="3" android:ellipsize="end" android:gravity="center_vertical"/>
--- a/mobile/android/base/widget/FaviconView.java +++ b/mobile/android/base/widget/FaviconView.java @@ -59,24 +59,25 @@ public class FaviconView extends ImageVi /** * Formats the image for display, if the prerequisite data are available. Upscales tiny Favicons to * normal sized ones, replaces null bitmaps with the default Favicon, and fills all remaining space * in this view with the coloured background. */ private void formatImage() { // If we're called before bitmap is set, just show the default. if (mIconBitmap == null) { - setImageResource(0); + setImageResource(R.drawable.favicon); hideBackground(); return; } // If we're called before size set, abort. if (mActualWidth == 0 || mActualHeight == 0) { hideBackground(); + setImageResource(R.drawable.favicon); return; } if (mScalingExpected && mActualWidth != mIconBitmap.getWidth()) { scaleBitmap(); // Don't scale the image every time something changes. mScalingExpected = false; } @@ -150,16 +151,24 @@ public class FaviconView extends ImageVi mIconKey = key; mScalingExpected = allowScaling; // Possibly update the display. formatImage(); } /** + * Clear image and background shown by this view. + */ + public void clearImage() { + setImageResource(0); + hideBackground(); + } + + /** * Update the displayed image and apply the scaling logic. * The scaling logic will attempt to resize the image to fit correctly inside the view in a way * that avoids unreasonable levels of loss of quality. * Scaling is necessary only when the icon being provided is not drawn from the Favicon cache * introduced in Bug 914296. * * Due to Bug 913746, icons bundled for search engines are not available to the cache, so must * always have the scaling logic applied here. At the time of writing, this is the only case in
new file mode 100644 --- /dev/null +++ b/mobile/android/modules/Home.jsm @@ -0,0 +1,132 @@ +// -*- Mode: js2; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*- +/* 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/. */ + +"use strict"; + +this.EXPORTED_SYMBOLS = ["Home"]; + +const { classes: Cc, interfaces: Ci, utils: Cu } = Components; + +Cu.import("resource://gre/modules/Services.jsm"); + +// See bug 915424 +function resolveGeckoURI(aURI) { + if (!aURI) + throw "Can't resolve an empty uri"; + + if (aURI.startsWith("chrome://")) { + let registry = Cc['@mozilla.org/chrome/chrome-registry;1'].getService(Ci["nsIChromeRegistry"]); + return registry.convertChromeURL(Services.io.newURI(aURI, null, null)).spec; + } else if (aURI.startsWith("resource://")) { + let handler = Services.io.getProtocolHandler("resource").QueryInterface(Ci.nsIResProtocolHandler); + return handler.resolveURI(Services.io.newURI(aURI, null, null)); + } + return aURI; +} + +function sendMessageToJava(message) { + return Services.androidBridge.handleGeckoMessage(JSON.stringify(message)); +} + +function BannerMessage(options) { + let uuidgen = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator); + this.id = uuidgen.generateUUID().toString(); + + if ("text" in options && options.text != null) + this.text = options.text; + + if ("icon" in options && options.icon != null) + this.iconURI = resolveGeckoURI(options.icon); + + if ("onclick" in options && typeof options.onclick === "function") + this.onclick = options.onclick; +} + +let HomeBanner = { + // Holds the messages that will rotate through the banner. + _messages: {}, + + // A queue used to keep track of which message to show next. + _queue: [], + + observe: function(subject, topic, data) { + switch(topic) { + case "HomeBanner:Get": + this._handleGet(); + break; + + case "HomeBanner:Click": + this._handleClick(data); + break; + } + }, + + _handleGet: function() { + // Get the message from the front of the queue, then add it back + // to the end of the queue to show it again later. + let id = this._queue.shift(); + this._queue.push(id); + + let message = this._messages[id]; + sendMessageToJava({ + type: "HomeBanner:Data", + id: message.id, + text: message.text, + iconURI: message.iconURI + }); + }, + + _handleClick: function(id) { + let message = this._messages[id]; + if (message.onclick) + message.onclick(); + }, + + /** + * Adds a new banner message to the rotation. + * + * @return id Unique identifer for the message. + */ + add: function(options) { + let message = new BannerMessage(options); + this._messages[message.id] = message; + + // Add the new message to the end of the queue. + this._queue.push(message.id); + + // If this is the first message we're adding, add + // observers to listen for requests from the Java UI. + if (Object.keys(this._messages).length == 1) { + Services.obs.addObserver(this, "HomeBanner:Get", false); + Services.obs.addObserver(this, "HomeBanner:Click", false); + } + + return message.id; + }, + + /** + * Removes a banner message from the rotation. + * + * @param id The id of the message to remove. + */ + remove: function(id) { + delete this._messages[id]; + + // Remove the message from the queue. + let index = this._queue.indexOf(id); + this._queue.splice(index, 1); + + // If there are no more messages, remove the observers. + if (Object.keys(this._messages).length == 0) { + Services.obs.removeObserver(this, "HomeBanner:Get"); + Services.obs.removeObserver(this, "HomeBanner:Click"); + } + } +}; + +// Public API +this.Home = { + banner: HomeBanner +}
--- a/mobile/android/modules/moz.build +++ b/mobile/android/modules/moz.build @@ -1,15 +1,16 @@ # -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*- # vim: set filetype=python: # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. EXTRA_JS_MODULES += [ 'ContactService.jsm', + 'Home.jsm', 'JNI.jsm', 'LightweightThemeConsumer.jsm', 'OrderedBroadcast.jsm', 'Prompt.jsm', 'Sanitizer.jsm', 'SharedPreferences.jsm', ]
--- a/mobile/android/services/java-sources.mn +++ b/mobile/android/services/java-sources.mn @@ -7,16 +7,17 @@ background/announcements/AnnouncementsFe background/announcements/AnnouncementsFetchResourceDelegate.java background/announcements/AnnouncementsService.java background/announcements/AnnouncementsStartReceiver.java background/BackgroundService.java background/bagheera/BagheeraClient.java background/bagheera/BagheeraRequestDelegate.java background/bagheera/BoundedByteArrayEntity.java background/bagheera/DeflateHelper.java +background/common/DateUtils.java background/common/log/Logger.java background/common/log/writers/AndroidLevelCachingLogWriter.java background/common/log/writers/AndroidLogWriter.java background/common/log/writers/LevelFilteringLogWriter.java background/common/log/writers/LogWriter.java background/common/log/writers/PrintLogWriter.java background/common/log/writers/SimpleTagLogWriter.java background/common/log/writers/StringLogWriter.java
--- a/toolkit/devtools/server/actors/script.js +++ b/toolkit/devtools/server/actors/script.js @@ -283,19 +283,27 @@ EventLoopStack.prototype = { get size() { return this._inspector.eventLoopNestLevel; }, /** * The URL of the debuggee who pushed the event loop on top of the stack. */ get lastPausedUrl() { - return this.size > 0 - ? this._inspector.lastNestRequestor.url - : null; + let url = null; + if (this.size > 0) { + try { + url = this._inspector.lastNestRequestor.url + } catch (e) { + // The tab's URL getter may throw if the tab is destroyed by the time + // this code runs, but we don't really care at this point. + dumpn(e); + } + } + return url; }, /** * Push a new nested event loop onto the stack. * * @returns EventLoop */ push: function () { @@ -931,17 +939,17 @@ ThreadActor.prototype = { message: "Can't resume when debuggee isn't paused. Current state is '" + this._state + "'" }; } // In case of multiple nested event loops (due to multiple debuggers open in // different tabs or multiple debugger clients connected to the same tab) // only allow resumption in a LIFO order. - if (this._nestedEventLoops.size + if (this._nestedEventLoops.size && this._nestedEventLoops.lastPausedUrl && this._nestedEventLoops.lastPausedUrl !== this._hooks.url) { return { error: "wrongOrder", message: "trying to resume in the wrong order.", lastPausedUrl: this._nestedEventLoops.lastPausedUrl }; }
--- a/widget/cocoa/nsLookAndFeel.mm +++ b/widget/cocoa/nsLookAndFeel.mm @@ -244,17 +244,17 @@ nsLookAndFeel::NativeGetColor(ColorID aI break; case eColorID__moz_mac_menutextdisable: aColor = NS_RGB(0x88,0x88,0x88); break; case eColorID__moz_mac_menutextselect: aColor = GetColorFromNSColor([NSColor selectedMenuItemTextColor]); break; case eColorID__moz_mac_disabledtoolbartext: - aColor = NS_RGB(0x3F,0x3F,0x3F); + aColor = GetColorFromNSColor([NSColor disabledControlTextColor]); break; case eColorID__moz_mac_menuselect: aColor = GetColorFromNSColor([NSColor alternateSelectedControlColor]); break; case eColorID__moz_buttondefault: aColor = NS_RGB(0xDC,0xDC,0xDC); break; case eColorID__moz_cellhighlight:
--- a/widget/tests/test_platform_colors.xul +++ b/widget/tests/test_platform_colors.xul @@ -70,17 +70,17 @@ var colors = { "-moz-html-cellhighlighttext": ["rgb(0, 0, 0)"], "-moz-mac-chrome-active": ["rgb(150, 150, 150)", "rgb(167, 167, 167)", "rgb(178, 178, 178)"], "-moz-mac-chrome-inactive": ["rgb(202, 202, 202)", "rgb(216, 216, 216)", "rgb(225, 225, 225)"], //"-moz-mac-focusring": ["rgb(83, 144, 210)", "rgb(95, 112, 130)", "rgb(63, 152, 221)", "rgb(108, 126, 141)"], "-moz-mac-menuselect": ["rgb(115, 132, 153)", "rgb(127, 127, 127)", "rgb(56, 117, 215)", "rgb(255, 193, 31)", "rgb(243, 70, 72)", "rgb(255, 138, 34)", "rgb(102, 197, 71)", "rgb(140, 78, 184)"], "-moz-mac-menushadow": ["rgb(163, 163, 163)"], "-moz-mac-menutextdisable": ["rgb(152, 152, 152)", "rgb(136, 136, 136)"], "-moz-mac-menutextselect": ["rgb(255, 255, 255)"], - "-moz-mac-disabledtoolbartext": ["rgb(63, 63, 63)"], + "-moz-mac-disabledtoolbartext": ["rgb(127, 127, 127)"], "-moz-mac-secondaryhighlight": ["rgb(212, 212, 212)"], "-moz-menuhover": ["rgb(115, 132, 153)", "rgb(127, 127, 127)", "rgb(56, 117, 215)", "rgb(255, 193, 31)", "rgb(243, 70, 72)", "rgb(255, 138, 34)", "rgb(102, 197, 71)", "rgb(140, 78, 184)"], "-moz-menuhovertext": ["rgb(255, 255, 255)", "rgb(255, 254, 254)", "rgb(254, 255, 254)"], "-moz-menubartext": ["rgb(0, 0, 0)"], //"-moz-menubarhovertext": ["rgb(255, 255, 255)"], "-moz-oddtreerow": ["rgb(236, 242, 254)", "rgb(240, 240, 240)", "rgb(243, 245, 250)", "rgb(243, 246, 250)"], "-moz-visitedhyperlinktext": ["rgb(85, 26, 139)"], "currentcolor": ["rgb(0, 0, 0)"],