From 74e02af5af32f165f8a139038f305ec0f51faa72 Mon Sep 17 00:00:00 2001 From: Ascrod <32915892+Ascrod@users.noreply.github.com> Date: Mon, 8 Apr 2019 18:12:40 -0400 Subject: [PATCH 01/30] Issue #991 Part 1: Pale Moon --- .../palemoon/base/content/browser-syncui.js | 11 ++--------- .../palemoon/base/content/tabbrowser.xml | 6 +----- .../palemoon/components/distribution.js | 14 ++------------ .../palemoon/components/feeds/FeedWriter.js | 19 +++---------------- .../components/feeds/WebContentConverter.js | 9 ++------- .../components/nsBrowserContentHandler.js | 15 +++------------ .../palemoon/components/nsBrowserGlue.js | 15 +++------------ 7 files changed, 16 insertions(+), 73 deletions(-) diff --git a/application/palemoon/base/content/browser-syncui.js b/application/palemoon/base/content/browser-syncui.js index 67056e2210..6fad03cddf 100644 --- a/application/palemoon/base/content/browser-syncui.js +++ b/application/palemoon/base/content/browser-syncui.js @@ -83,10 +83,7 @@ var gSyncUI = { _wasDelayed: false, _needsSetup: function SUI__needsSetup() { - let firstSync = ""; - try { - firstSync = Services.prefs.getCharPref("services.sync.firstSync"); - } catch (e) { } + let firstSync = Services.prefs.getCharPref("services.sync.firstSync", ""); return Weave.Status.checkSetup() == Weave.CLIENT_NOT_CONFIGURED || firstSync == "notReady"; }, @@ -285,11 +282,7 @@ var gSyncUI = { if (!syncButton) return; - let lastSync; - try { - lastSync = Services.prefs.getCharPref("services.sync.lastSync"); - } - catch (e) { }; + let lastSync = Services.prefs.getCharPref("services.sync.lastSync"); if (!lastSync || this._needsSetup()) { syncButton.removeAttribute("tooltiptext"); return; diff --git a/application/palemoon/base/content/tabbrowser.xml b/application/palemoon/base/content/tabbrowser.xml index 868179b5d5..d9366f488c 100644 --- a/application/palemoon/base/content/tabbrowser.xml +++ b/application/palemoon/base/content/tabbrowser.xml @@ -3628,11 +3628,7 @@ window.addEventListener("resize", this, false); window.addEventListener("load", this, false); - try { - this._tabAnimationLoggingEnabled = Services.prefs.getBoolPref("browser.tabs.animationLogging.enabled"); - } catch (ex) { - this._tabAnimationLoggingEnabled = false; - } + this._tabAnimationLoggingEnabled = Services.prefs.getBoolPref("browser.tabs.animationLogging.enabled", false); this._browserNewtabpageEnabled = Services.prefs.getBoolPref("browser.newtabpage.enabled"); ]]> diff --git a/application/palemoon/components/distribution.js b/application/palemoon/components/distribution.js index d3300604ef..121e55b1bd 100644 --- a/application/palemoon/components/distribution.js +++ b/application/palemoon/components/distribution.js @@ -38,13 +38,7 @@ DistributionCustomizer.prototype = { }, get _locale() { - let locale; - try { - locale = this._prefs.getCharPref("general.useragent.locale"); - } - catch (e) { - locale = "en-US"; - } + let locale = this._prefs.getCharPref("general.useragent.locale", "en-US"); this.__defineGetter__("_locale", function() locale); return this._locale; }, @@ -221,11 +215,7 @@ DistributionCustomizer.prototype = { this._ini.getString("Global", "id") + ".bookmarksProcessed"; } - let bmProcessed = false; - try { - bmProcessed = this._prefs.getBoolPref(bmProcessedPref); - } - catch (e) {} + let bmProcessed = this._prefs.getBoolPref(bmProcessedPref, false); if (!bmProcessed) { if (sections["BookmarksMenu"]) diff --git a/application/palemoon/components/feeds/FeedWriter.js b/application/palemoon/components/feeds/FeedWriter.js index d704835bb7..facde5815b 100644 --- a/application/palemoon/components/feeds/FeedWriter.js +++ b/application/palemoon/components/feeds/FeedWriter.js @@ -18,12 +18,7 @@ function LOG(str) { var prefB = Cc["@mozilla.org/preferences-service;1"]. getService(Ci.nsIPrefBranch); - var shouldLog = false; - try { - shouldLog = prefB.getBoolPref("feeds.log"); - } - catch (ex) { - } + var shouldLog = prefB.getBoolPref("feeds.log", false); if (shouldLog) dump("*** Feeds: " + str + "\n"); @@ -874,11 +869,7 @@ FeedWriter.prototype = { Cc["@mozilla.org/preferences-service;1"]. getService(Ci.nsIPrefBranch); - var handler = "bookmarks"; - try { - handler = prefs.getCharPref(getPrefReaderForType(feedType)); - } - catch (ex) { } + var handler = prefs.getCharPref(getPrefReaderForType(feedType), "bookmarks"); switch (handler) { case "web": { @@ -1076,11 +1067,7 @@ FeedWriter.prototype = { .addEventListener("command", this, false); // first-run ui - var showFirstRunUI = true; - try { - showFirstRunUI = prefs.getBoolPref(PREF_SHOW_FIRST_RUN_UI); - } - catch (ex) { } + var showFirstRunUI = prefs.getBoolPref(PREF_SHOW_FIRST_RUN_UI, true); if (showFirstRunUI) { var textfeedinfo1, textfeedinfo2; switch (feedType) { diff --git a/application/palemoon/components/feeds/WebContentConverter.js b/application/palemoon/components/feeds/WebContentConverter.js index 41679b0281..42e2edee03 100644 --- a/application/palemoon/components/feeds/WebContentConverter.js +++ b/application/palemoon/components/feeds/WebContentConverter.js @@ -436,13 +436,8 @@ WebContentConverterRegistrar.prototype = { // check if it is in the black list var pb = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch); - var allowed; - try { - allowed = pb.getBoolPref(PREF_HANDLER_EXTERNAL_PREFIX + "." + aProtocol); - } - catch (e) { - allowed = pb.getBoolPref(PREF_HANDLER_EXTERNAL_PREFIX + "-default"); - } + var allowed = pb.getBoolPref(PREF_HANDLER_EXTERNAL_PREFIX + "." + aProtocol, + pb.getBoolPref(PREF_HANDLER_EXTERNAL_PREFIX + "-default")); if (!allowed) { // XXX this should be a "security exception" according to spec throw("Not allowed to register a protocol handler for " + aProtocol); diff --git a/application/palemoon/components/nsBrowserContentHandler.js b/application/palemoon/components/nsBrowserContentHandler.js index 6a75b40f28..e7f1414c2d 100644 --- a/application/palemoon/components/nsBrowserContentHandler.js +++ b/application/palemoon/components/nsBrowserContentHandler.js @@ -100,20 +100,14 @@ const OVERRIDE_NEW_BUILD_ID = 3; * OVERRIDE_NONE otherwise. */ function needHomepageOverride(prefb) { - var savedmstone = null; - try { - savedmstone = prefb.getCharPref("browser.startup.homepage_override.mstone"); - } catch (e) {} + var savedmstone = prefb.getCharPref("browser.startup.homepage_override.mstone", ""); if (savedmstone == "ignore") return OVERRIDE_NONE; var mstone = Services.appinfo.platformVersion; - var savedBuildID = null; - try { - savedBuildID = prefb.getCharPref("browser.startup.homepage_override.buildID"); - } catch (e) {} + var savedBuildID = prefb.getCharPref("browser.startup.homepage_override.buildID", ""); var buildID = Services.appinfo.platformBuildID; @@ -550,10 +544,7 @@ nsBrowserContentHandler.prototype = { // URL if we do end up showing an overridePage. This makes it possible // to have the overridePage's content vary depending on the version we're // upgrading from. - let old_mstone = "unknown"; - try { - old_mstone = Services.prefs.getCharPref("browser.startup.homepage_override.mstone"); - } catch (ex) {} + let old_mstone = Services.prefs.getCharPref("browser.startup.homepage_override.mstone", "unknown"); let override = needHomepageOverride(prefb); if (override != OVERRIDE_NONE) { switch (override) { diff --git a/application/palemoon/components/nsBrowserGlue.js b/application/palemoon/components/nsBrowserGlue.js index ad48dab29d..01a133833d 100644 --- a/application/palemoon/components/nsBrowserGlue.js +++ b/application/palemoon/components/nsBrowserGlue.js @@ -990,14 +990,8 @@ BrowserGlue.prototype = { // An import operation is about to run. // Don't try to recreate smart bookmarks if autoExportHTML is true or // smart bookmarks are disabled. - var autoExportHTML = false; - try { - autoExportHTML = Services.prefs.getBoolPref("browser.bookmarks.autoExportHTML"); - } catch(ex) {} - var smartBookmarksVersion = 0; - try { - smartBookmarksVersion = Services.prefs.getIntPref("browser.places.smartBookmarksVersion"); - } catch(ex) {} + var autoExportHTML = Services.prefs.getBoolPref("browser.bookmarks.autoExportHTML", false); + var smartBookmarksVersion = Services.prefs.getIntPref("browser.places.smartBookmarksVersion", 0); if (!autoExportHTML && smartBookmarksVersion != -1) Services.prefs.setIntPref("browser.places.smartBookmarksVersion", 0); @@ -1556,10 +1550,7 @@ BrowserGlue.prototype = { const MAX_RESULTS = 10; // Get current smart bookmarks version. If not set, create them. - let smartBookmarksCurrentVersion = 0; - try { - smartBookmarksCurrentVersion = Services.prefs.getIntPref(SMART_BOOKMARKS_PREF); - } catch(ex) {} + let smartBookmarksCurrentVersion = Services.prefs.getIntPref(SMART_BOOKMARKS_PREF, 0); // If version is current or smart bookmarks are disabled, just bail out. if (smartBookmarksCurrentVersion == -1 || From 5d978c380c6b1b7fa0d593dd5f3aa1f731128968 Mon Sep 17 00:00:00 2001 From: Ascrod <32915892+Ascrod@users.noreply.github.com> Date: Mon, 8 Apr 2019 19:58:00 -0400 Subject: [PATCH 02/30] Issue #991 Part 2: Basilisk --- .../basilisk/base/content/browser-syncui.js | 1 - application/basilisk/base/content/browser.js | 10 ++---- .../basilisk/base/content/tabbrowser.xml | 12 ++----- .../customizableui/CustomizableUI.jsm | 22 +++---------- .../customizableui/CustomizableWidgets.jsm | 5 +-- .../customizableui/CustomizeMode.jsm | 9 ++--- .../basilisk/components/distribution.js | 19 ++--------- .../basilisk/components/feeds/FeedWriter.js | 7 +--- .../components/feeds/WebContentConverter.js | 9 ++--- .../components/nsBrowserContentHandler.js | 15 ++------- .../basilisk/components/nsBrowserGlue.js | 33 ++++--------------- .../basilisk/components/sync/aboutSyncTabs.js | 8 +---- .../modules/DirectoryLinksProvider.jsm | 6 +--- 13 files changed, 30 insertions(+), 126 deletions(-) diff --git a/application/basilisk/base/content/browser-syncui.js b/application/basilisk/base/content/browser-syncui.js index 67056e2210..57e43b96db 100644 --- a/application/basilisk/base/content/browser-syncui.js +++ b/application/basilisk/base/content/browser-syncui.js @@ -275,7 +275,6 @@ var gSyncUI = { openPreferences("paneSync"); }, - // Helpers _updateLastSyncTime: function SUI__updateLastSyncTime() { if (!gBrowser) diff --git a/application/basilisk/base/content/browser.js b/application/basilisk/base/content/browser.js index d564187252..771c4a501c 100644 --- a/application/basilisk/base/content/browser.js +++ b/application/basilisk/base/content/browser.js @@ -2594,15 +2594,9 @@ var gMenuButtonUpdateBadge = { cancelObserverRegistered: false, init: function () { - try { - this.enabled = Services.prefs.getBoolPref("app.update.badge"); - } catch (e) {} + this.enabled = Services.prefs.getBoolPref("app.update.badge", false); if (this.enabled) { - try { - this.badgeWaitTime = Services.prefs.getIntPref("app.update.badgeWaitTime"); - } catch (e) { - this.badgeWaitTime = 345600; // 4 days - } + this.badgeWaitTime = Services.prefs.getIntPref("app.update.badgeWaitTime", 345600); // 4 days Services.obs.addObserver(this, "update-staged", false); Services.obs.addObserver(this, "update-downloaded", false); } diff --git a/application/basilisk/base/content/tabbrowser.xml b/application/basilisk/base/content/tabbrowser.xml index e4f0579e3e..8f98a5af0f 100644 --- a/application/basilisk/base/content/tabbrowser.xml +++ b/application/basilisk/base/content/tabbrowser.xml @@ -4151,11 +4151,7 @@ return true; if (this._logInit) return this._shouldLog; - let result = false; - try { - result = Services.prefs.getBoolPref("browser.tabs.remote.logSwitchTiming"); - } catch (ex) { - } + let result = Services.prefs.getBoolPref("browser.tabs.remote.logSwitchTiming", false); this._shouldLog = result; this._logInit = true; return this._shouldLog; @@ -5225,11 +5221,7 @@ window.addEventListener("resize", this, false); window.addEventListener("load", this, false); - try { - this._tabAnimationLoggingEnabled = Services.prefs.getBoolPref("browser.tabs.animationLogging.enabled"); - } catch (ex) { - this._tabAnimationLoggingEnabled = false; - } + this._tabAnimationLoggingEnabled = Services.prefs.getBoolPref("browser.tabs.animationLogging.enabled", false); this._browserNewtabpageEnabled = Services.prefs.getBoolPref("browser.newtabpage.enabled"); ]]> diff --git a/application/basilisk/components/customizableui/CustomizableUI.jsm b/application/basilisk/components/customizableui/CustomizableUI.jsm index 46c989f537..df4dae753a 100644 --- a/application/basilisk/components/customizableui/CustomizableUI.jsm +++ b/application/basilisk/components/customizableui/CustomizableUI.jsm @@ -158,10 +158,7 @@ var gUIStateBeforeReset = { XPCOMUtils.defineLazyGetter(this, "log", () => { let scope = {}; Cu.import("resource://gre/modules/Console.jsm", scope); - let debug; - try { - debug = Services.prefs.getBoolPref(kPrefCustomizationDebug); - } catch (ex) {} + let debug = Services.prefs.getBoolPref(kPrefCustomizationDebug, false); let consoleOptions = { maxLogLevel: debug ? "all" : "log", prefix: "CustomizableUI", @@ -1924,16 +1921,10 @@ var CustomizableUIInternal = { // state immediately when a browser window opens, which is important for // other consumers of this API. loadSavedState: function() { - let state = null; - try { - state = Services.prefs.getCharPref(kPrefCustomizationState); - } catch (e) { - log.debug("No saved state found"); - // This will fail if nothing has been customized, so silently fall back to - // the defaults. - } - + let state = Services.prefs.getCharPref(kPrefCustomizationState, ""); if (!state) { + log.debug("No saved state found"); + // Nothing has been customized, so silently fall back to the defaults. return; } try { @@ -2218,10 +2209,7 @@ var CustomizableUIInternal = { this.notifyListeners("onWidgetAdded", widget.id, widget.currentArea, widget.currentPosition); } else if (widgetMightNeedAutoAdding) { - let autoAdd = true; - try { - autoAdd = Services.prefs.getBoolPref(kPrefCustomizationAutoAdd); - } catch (e) {} + let autoAdd = Services.prefs.getBoolPref(kPrefCustomizationAutoAdd, true); // If the widget doesn't have an existing placement, and it hasn't been // seen before, then add it to its default area so it can be used. diff --git a/application/basilisk/components/customizableui/CustomizableWidgets.jsm b/application/basilisk/components/customizableui/CustomizableWidgets.jsm index 9e8f0ec78a..d4a191a971 100644 --- a/application/basilisk/components/customizableui/CustomizableWidgets.jsm +++ b/application/basilisk/components/customizableui/CustomizableWidgets.jsm @@ -40,10 +40,7 @@ const kWidePanelItemClass = "panel-wide-item"; XPCOMUtils.defineLazyGetter(this, "log", () => { let scope = {}; Cu.import("resource://gre/modules/Console.jsm", scope); - let debug; - try { - debug = Services.prefs.getBoolPref(kPrefCustomizationDebug); - } catch (ex) {} + let debug = Services.prefs.getBoolPref(kPrefCustomizationDebug, false); let consoleOptions = { maxLogLevel: debug ? "all" : "log", prefix: "CustomizableWidgets", diff --git a/application/basilisk/components/customizableui/CustomizeMode.jsm b/application/basilisk/components/customizableui/CustomizeMode.jsm index 4365ddfbce..2958655d2e 100644 --- a/application/basilisk/components/customizableui/CustomizeMode.jsm +++ b/application/basilisk/components/customizableui/CustomizeMode.jsm @@ -40,9 +40,7 @@ let gDebug; XPCOMUtils.defineLazyGetter(this, "log", () => { let scope = {}; Cu.import("resource://gre/modules/Console.jsm", scope); - try { - gDebug = Services.prefs.getBoolPref(kPrefCustomizationDebug); - } catch (ex) {} + gDebug = Services.prefs.getBoolPref(kPrefCustomizationDebug, false); let consoleOptions = { maxLogLevel: gDebug ? "all" : "log", prefix: "CustomizeMode", @@ -1503,10 +1501,7 @@ CustomizeMode.prototype = { if (!AppConstants.CAN_DRAW_IN_TITLEBAR) { return; } - let drawInTitlebar = true; - try { - drawInTitlebar = Services.prefs.getBoolPref(kDrawInTitlebarPref); - } catch (ex) { } + let drawInTitlebar = Services.prefs.getBoolPref(kDrawInTitlebarPref, true); let button = this.document.getElementById("customization-titlebar-visibility-button"); // Drawing in the titlebar means 'hiding' the titlebar: if (drawInTitlebar) { diff --git a/application/basilisk/components/distribution.js b/application/basilisk/components/distribution.js index 589129a5af..c30e4dcfea 100644 --- a/application/basilisk/components/distribution.js +++ b/application/basilisk/components/distribution.js @@ -22,10 +22,7 @@ XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils", this.DistributionCustomizer = function DistributionCustomizer() { // For parallel xpcshell testing purposes allow loading the distribution.ini // file from the profile folder through an hidden pref. - let loadFromProfile = false; - try { - loadFromProfile = Services.prefs.getBoolPref("distribution.testing.loadFromProfile"); - } catch (ex) {} + loadFromProfile = Services.prefs.getBoolPref("distribution.testing.loadFromProfile", false); let dirSvc = Cc["@mozilla.org/file/directory_service;1"]. getService(Ci.nsIProperties); try { @@ -60,13 +57,7 @@ DistributionCustomizer.prototype = { }, get _locale() { - let locale; - try { - locale = this._prefs.getCharPref("general.useragent.locale"); - } - catch (e) { - locale = "en-US"; - } + let locale = this._prefs.getCharPref("general.useragent.locale", "en-US"); this.__defineGetter__("_locale", () => locale); return this._locale; }, @@ -291,11 +282,7 @@ DistributionCustomizer.prototype = { this._ini.getString("Global", "id") + ".bookmarksProcessed"; } - let bmProcessed = false; - try { - bmProcessed = this._prefs.getBoolPref(bmProcessedPref); - } - catch (e) {} + let bmProcessed = this._prefs.getBoolPref(bmProcessedPref, false); if (!bmProcessed) { if (sections["BookmarksMenu"]) diff --git a/application/basilisk/components/feeds/FeedWriter.js b/application/basilisk/components/feeds/FeedWriter.js index 20f1399b0c..ceb2a7e2f6 100644 --- a/application/basilisk/components/feeds/FeedWriter.js +++ b/application/basilisk/components/feeds/FeedWriter.js @@ -19,12 +19,7 @@ function LOG(str) { let prefB = Cc["@mozilla.org/preferences-service;1"]. getService(Ci.nsIPrefBranch); - let shouldLog = false; - try { - shouldLog = prefB.getBoolPref("feeds.log"); - } - catch (ex) { - } + let shouldLog = prefB.getBoolPref("feeds.log", false); if (shouldLog) dump("*** Feeds: " + str + "\n"); diff --git a/application/basilisk/components/feeds/WebContentConverter.js b/application/basilisk/components/feeds/WebContentConverter.js index 2cb5cd1451..159eca7c24 100644 --- a/application/basilisk/components/feeds/WebContentConverter.js +++ b/application/basilisk/components/feeds/WebContentConverter.js @@ -187,13 +187,8 @@ const Utils = { // check if it is in the black list let pb = Services.prefs; - let allowed; - try { - allowed = pb.getBoolPref(PREF_HANDLER_EXTERNAL_PREFIX + "." + aProtocol); - } - catch (e) { - allowed = pb.getBoolPref(PREF_HANDLER_EXTERNAL_PREFIX + "-default"); - } + let allowed = pb.getBoolPref(PREF_HANDLER_EXTERNAL_PREFIX + "." + aProtocol, + pb.getBoolPref(PREF_HANDLER_EXTERNAL_PREFIX + "-default")); if (!allowed) { throw this.getSecurityError( `Not allowed to register a protocol handler for ${aProtocol}`, diff --git a/application/basilisk/components/nsBrowserContentHandler.js b/application/basilisk/components/nsBrowserContentHandler.js index 74144fc1b3..d65e52594a 100644 --- a/application/basilisk/components/nsBrowserContentHandler.js +++ b/application/basilisk/components/nsBrowserContentHandler.js @@ -100,20 +100,14 @@ const OVERRIDE_NEW_BUILD_ID = 3; * OVERRIDE_NONE otherwise. */ function needHomepageOverride(prefb) { - var savedmstone = null; - try { - savedmstone = prefb.getCharPref("browser.startup.homepage_override.mstone"); - } catch (e) {} + var savedmstone = prefb.getCharPref("browser.startup.homepage_override.mstone", ""); if (savedmstone == "ignore") return OVERRIDE_NONE; var mstone = Services.appinfo.platformVersion; - var savedBuildID = null; - try { - savedBuildID = prefb.getCharPref("browser.startup.homepage_override.buildID"); - } catch (e) {} + var savedBuildID = prefb.getCharPref("browser.startup.homepage_override.buildID", ""); var buildID = Services.appinfo.platformBuildID; @@ -489,10 +483,7 @@ nsBrowserContentHandler.prototype = { // URL if we do end up showing an overridePage. This makes it possible // to have the overridePage's content vary depending on the version we're // upgrading from. - let old_mstone = "unknown"; - try { - old_mstone = Services.prefs.getCharPref("browser.startup.homepage_override.mstone"); - } catch (ex) {} + let old_mstone = Services.prefs.getCharPref("browser.startup.homepage_override.mstone", "unknown"); override = needHomepageOverride(prefb); if (override != OVERRIDE_NONE) { switch (override) { diff --git a/application/basilisk/components/nsBrowserGlue.js b/application/basilisk/components/nsBrowserGlue.js index 82de33240c..b4256523ff 100644 --- a/application/basilisk/components/nsBrowserGlue.js +++ b/application/basilisk/components/nsBrowserGlue.js @@ -961,10 +961,7 @@ BrowserGlue.prototype = { // Offer to reset a user's profile if it hasn't been used for 60 days. const OFFER_PROFILE_RESET_INTERVAL_MS = 60 * 24 * 60 * 60 * 1000; let lastUse = Services.appinfo.replacedLockTime; - let disableResetPrompt = false; - try { - disableResetPrompt = Services.prefs.getBoolPref("browser.disableResetPrompt"); - } catch (e) {} + let disableResetPrompt = Services.prefs.getBoolPref("browser.disableResetPrompt", false); if (!disableResetPrompt && lastUse && Date.now() - lastUse >= OFFER_PROFILE_RESET_INTERVAL_MS) { @@ -1505,10 +1502,7 @@ BrowserGlue.prototype = { } catch (ex) {} // Support legacy bookmarks.html format for apps that depend on that format. - let autoExportHTML = false; - try { - autoExportHTML = Services.prefs.getBoolPref("browser.bookmarks.autoExportHTML"); - } catch (ex) {} // Do not export. + let autoExportHTML = Services.prefs.getBoolPref("browser.bookmarks.autoExportHTML", false); // Do not export. if (autoExportHTML) { // Sqlite.jsm and Places shutdown happen at profile-before-change, thus, // to be on the safe side, this should run earlier. @@ -1578,10 +1572,7 @@ BrowserGlue.prototype = { // An import operation is about to run. // Don't try to recreate smart bookmarks if autoExportHTML is true or // smart bookmarks are disabled. - let smartBookmarksVersion = 0; - try { - smartBookmarksVersion = Services.prefs.getIntPref("browser.places.smartBookmarksVersion"); - } catch (ex) {} + let smartBookmarksVersion = Services.prefs.getIntPref("browser.places.smartBookmarksVersion", 0); if (!autoExportHTML && smartBookmarksVersion != -1) Services.prefs.setIntPref("browser.places.smartBookmarksVersion", 0); @@ -1932,10 +1923,7 @@ BrowserGlue.prototype = { // Refactor urlbar suggestion preferences to make it extendable and // allow new suggestion types (e.g: search suggestions). let types = ["history", "bookmark", "openpage"]; - let defaultBehavior = 0; - try { - defaultBehavior = Services.prefs.getIntPref("browser.urlbar.default.behavior"); - } catch (ex) {} + let defaultBehavior = Services.prefs.getIntPref("browser.urlbar.default.behavior", 0); try { let autocompleteEnabled = Services.prefs.getBoolPref("browser.urlbar.autocomplete.enabled"); if (!autocompleteEnabled) { @@ -1988,12 +1976,8 @@ BrowserGlue.prototype = { if (currentUIVersion < 30) { // Convert old devedition theme pref to lightweight theme storage - let lightweightThemeSelected = false; - let selectedThemeID = null; - try { - lightweightThemeSelected = Services.prefs.prefHasUserValue("lightweightThemes.selectedThemeID"); - selectedThemeID = Services.prefs.getCharPref("lightweightThemes.selectedThemeID"); - } catch (e) {} + let lightweightThemeSelected = Services.prefs.prefHasUserValue("lightweightThemes.selectedThemeID", false); + let selectedThemeID = Services.prefs.getCharPref("lightweightThemes.selectedThemeID", ""); let defaultThemeSelected = false; try { @@ -2143,10 +2127,7 @@ BrowserGlue.prototype = { const MAX_RESULTS = 10; // Get current smart bookmarks version. If not set, create them. - let smartBookmarksCurrentVersion = 0; - try { - smartBookmarksCurrentVersion = Services.prefs.getIntPref(SMART_BOOKMARKS_PREF); - } catch (ex) {} + let smartBookmarksCurrentVersion = Services.prefs.getIntPref(SMART_BOOKMARKS_PREF, 0); // If version is current, or smart bookmarks are disabled, bail out. if (smartBookmarksCurrentVersion == -1 || diff --git a/application/basilisk/components/sync/aboutSyncTabs.js b/application/basilisk/components/sync/aboutSyncTabs.js index 410494b5bd..4808c052fc 100644 --- a/application/basilisk/components/sync/aboutSyncTabs.js +++ b/application/basilisk/components/sync/aboutSyncTabs.js @@ -254,13 +254,7 @@ var RemoteTabViewer = { _refetchTabs: function(force) { if (!force) { // Don't bother refetching tabs if we already did so recently - let lastFetch = 0; - try { - lastFetch = Services.prefs.getIntPref("services.sync.lastTabFetch"); - } - catch (e) { - /* Just use the default value of 0 */ - } + lastFetch = Services.prefs.getIntPref("services.sync.lastTabFetch", 0); let now = Math.floor(Date.now() / 1000); if (now - lastFetch < 30) { diff --git a/application/basilisk/modules/DirectoryLinksProvider.jsm b/application/basilisk/modules/DirectoryLinksProvider.jsm index 117564099d..85d8b296bd 100644 --- a/application/basilisk/modules/DirectoryLinksProvider.jsm +++ b/application/basilisk/modules/DirectoryLinksProvider.jsm @@ -189,11 +189,7 @@ var DirectoryLinksProvider = { * @return the selected locale or "en-US" if none is selected */ get locale() { - let matchOS; - try { - matchOS = Services.prefs.getBoolPref(PREF_MATCH_OS_LOCALE); - } - catch (e) {} + let matchOS = Services.prefs.getBoolPref(PREF_MATCH_OS_LOCALE, false); if (matchOS) { return Services.locale.getLocaleComponentForUserAgent(); From e2e42f0e1a351331b60f3bd627890fae9cee13aa Mon Sep 17 00:00:00 2001 From: Ascrod <32915892+Ascrod@users.noreply.github.com> Date: Mon, 8 Apr 2019 20:05:53 -0400 Subject: [PATCH 03/30] Issue #991 Part 3: Devtools --- devtools/client/framework/devtools.js | 7 +------ .../client/framework/toolbox-process-window.js | 17 ++++++----------- devtools/client/framework/toolbox.js | 7 +------ devtools/client/inspector/markup/markup.js | 7 ++----- devtools/shared/touch/simulator-core.js | 15 ++------------- 5 files changed, 12 insertions(+), 41 deletions(-) diff --git a/devtools/client/framework/devtools.js b/devtools/client/framework/devtools.js index 90f88023b1..976a4b56db 100644 --- a/devtools/client/framework/devtools.js +++ b/devtools/client/framework/devtools.js @@ -195,12 +195,7 @@ DevTools.prototype = { return tool; } - let enabled; - try { - enabled = Services.prefs.getBoolPref(tool.visibilityswitch); - } catch (e) { - enabled = true; - } + let enabled = Services.prefs.getBoolPref(tool.visibilityswitch, true); return enabled ? tool : null; }, diff --git a/devtools/client/framework/toolbox-process-window.js b/devtools/client/framework/toolbox-process-window.js index 8ead718b3d..c34fe33559 100644 --- a/devtools/client/framework/toolbox-process-window.js +++ b/devtools/client/framework/toolbox-process-window.js @@ -100,17 +100,12 @@ function openToolbox({ form, chrome, isTabActor }) { }; TargetFactory.forRemoteTab(options).then(target => { let frame = document.getElementById("toolbox-iframe"); - let selectedTool = "jsdebugger"; - - try { - // Remember the last panel that was used inside of this profile. - selectedTool = Services.prefs.getCharPref("devtools.toolbox.selectedTool"); - } catch(e) {} - - try { - // But if we are testing, then it should always open the debugger panel. - selectedTool = Services.prefs.getCharPref("devtools.browsertoolbox.panel"); - } catch(e) {} + // Remember the last panel that was used inside of this profile. + // But if we are testing, then it should always open the debugger panel. + let selectedTool = + Services.prefs.getCharPref("devtools.toolbox.selectedTool", + Services.prefs.getCharPref("devtools.browsertoolbox.panel", + "jsdebugger")); let options = { customIframe: frame }; gDevTools.showToolbox(target, diff --git a/devtools/client/framework/toolbox.js b/devtools/client/framework/toolbox.js index 926e306477..cde7de0e41 100644 --- a/devtools/client/framework/toolbox.js +++ b/devtools/client/framework/toolbox.js @@ -1131,12 +1131,7 @@ Toolbox.prototype = { setToolboxButtonsVisibility: function () { this.toolboxButtons.forEach(buttonSpec => { let { visibilityswitch, button, isTargetSupported } = buttonSpec; - let on = true; - try { - on = Services.prefs.getBoolPref(visibilityswitch); - } catch (ex) { - // Do nothing. - } + let on = Services.prefs.getBoolPref(visibilityswitch, true); on = on && isTargetSupported(this.target); diff --git a/devtools/client/inspector/markup/markup.js b/devtools/client/inspector/markup/markup.js index d6e9f8c115..a4b65c899a 100644 --- a/devtools/client/inspector/markup/markup.js +++ b/devtools/client/inspector/markup/markup.js @@ -73,11 +73,8 @@ function MarkupView(inspector, frame, controllerWindow) { this._elt = this.doc.querySelector("#root"); this.htmlEditor = new HTMLEditor(this.doc); - try { - this.maxChildren = Services.prefs.getIntPref("devtools.markup.pagesize"); - } catch (ex) { - this.maxChildren = DEFAULT_MAX_CHILDREN; - } + this.maxChildren = Services.prefs.getIntPref("devtools.markup.pagesize", + DEFAULT_MAX_CHILDREN); this.collapseAttributes = Services.prefs.getBoolPref(ATTR_COLLAPSE_ENABLED_PREF); diff --git a/devtools/shared/touch/simulator-core.js b/devtools/shared/touch/simulator-core.js index 6933f9207c..fff7d2e788 100644 --- a/devtools/shared/touch/simulator-core.js +++ b/devtools/shared/touch/simulator-core.js @@ -18,19 +18,8 @@ var systemAppOrigin = (function () { return systemOrigin; })(); -var threshold = 25; -try { - threshold = Services.prefs.getIntPref("ui.dragThresholdX"); -} catch (e) { - // Fall back to default value -} - -var delay = 500; -try { - delay = Services.prefs.getIntPref("ui.click_hold_context_menus.delay"); -} catch (e) { - // Fall back to default value -} +var threshold = Services.prefs.getIntPref("ui.dragThresholdX", 25); +var delay = Services.prefs.getIntPref("ui.click_hold_context_menus.delay", 500); function SimulatorCore(simulatorTarget) { this.simulatorTarget = simulatorTarget; From 3da26ba9e338bde6d502d7e483d0033ce383ee5b Mon Sep 17 00:00:00 2001 From: Ascrod <32915892+Ascrod@users.noreply.github.com> Date: Mon, 8 Apr 2019 20:36:18 -0400 Subject: [PATCH 04/30] Issue #991 Part 4: Network and URILoader --- netwerk/test/unit/test_bug650995.js | 14 ++------------ netwerk/test/unit/test_idn_blacklist.js | 7 +------ netwerk/test/unit/test_idn_urls.js | 7 +------ uriloader/exthandler/nsHandlerService.js | 16 +++++----------- 4 files changed, 9 insertions(+), 35 deletions(-) diff --git a/netwerk/test/unit/test_bug650995.js b/netwerk/test/unit/test_bug650995.js index 3c1ea8d67e..bf2e9fa7d4 100644 --- a/netwerk/test/unit/test_bug650995.js +++ b/netwerk/test/unit/test_bug650995.js @@ -78,24 +78,14 @@ function InitializeCacheDevices(memDevice, diskDevice) { this.start = function() { prefService.setBoolPref("browser.cache.memory.enable", memDevice); if (memDevice) { - try { - cap = prefService.getIntPref("browser.cache.memory.capacity"); - } - catch(ex) { - cap = 0; - } + cap = prefService.getIntPref("browser.cache.memory.capacity", 0); if (cap == 0) { prefService.setIntPref("browser.cache.memory.capacity", 1024); } } prefService.setBoolPref("browser.cache.disk.enable", diskDevice); if (diskDevice) { - try { - cap = prefService.getIntPref("browser.cache.disk.capacity"); - } - catch(ex) { - cap = 0; - } + cap = prefService.getIntPref("browser.cache.disk.capacity", 0); if (cap == 0) { prefService.setIntPref("browser.cache.disk.capacity", 1024); } diff --git a/netwerk/test/unit/test_idn_blacklist.js b/netwerk/test/unit/test_idn_blacklist.js index 5ca0173bb2..aea45d7060 100644 --- a/netwerk/test/unit/test_idn_blacklist.js +++ b/netwerk/test/unit/test_idn_blacklist.js @@ -121,12 +121,7 @@ const testcases = [ function run_test() { var pbi = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch); var oldProfile = pbi.getCharPref("network.IDN.restriction_profile", "moderate"); - var oldWhiteListCom; - try { - oldWhitelistCom = pbi.getBoolPref("network.IDN.whitelist.com"); - } catch(e) { - oldWhitelistCom = false; - } + var oldWhitelistCom = pbi.getBoolPref("network.IDN.whitelist.com", false); var idnService = Cc["@mozilla.org/network/idn-service;1"].getService(Ci.nsIIDNService); pbi.setCharPref("network.IDN.restriction_profile", "moderate"); diff --git a/netwerk/test/unit/test_idn_urls.js b/netwerk/test/unit/test_idn_urls.js index 06a53032b4..358854093f 100644 --- a/netwerk/test/unit/test_idn_urls.js +++ b/netwerk/test/unit/test_idn_urls.js @@ -295,12 +295,7 @@ const profiles = ["ASCII", "high", "moderate"]; function run_test() { var pbi = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch); var oldProfile = pbi.getCharPref("network.IDN.restriction_profile", "moderate"); - var oldWhiteListCom; - try { - oldWhitelistCom = pbi.getBoolPref("network.IDN.whitelist.com"); - } catch(e) { - oldWhitelistCom = false; - } + var oldWhitelistCom = pbi.getBoolPref("network.IDN.whitelist.com", false); var idnService = Cc["@mozilla.org/network/idn-service;1"].getService(Ci.nsIIDNService); for (var i = 0; i < profiles.length; ++i) { diff --git a/uriloader/exthandler/nsHandlerService.js b/uriloader/exthandler/nsHandlerService.js index c932f9f5dc..5e6356ac2e 100644 --- a/uriloader/exthandler/nsHandlerService.js +++ b/uriloader/exthandler/nsHandlerService.js @@ -352,17 +352,11 @@ HandlerService.prototype = { var prefSvc = Cc["@mozilla.org/preferences-service;1"]. getService(Ci.nsIPrefService); var prefBranch = prefSvc.getBranch("network.protocol-handler."); - try { - alwaysAsk = prefBranch.getBoolPref("warn-external." + type); - } catch (e) { - // will throw if pref didn't exist. - try { - alwaysAsk = prefBranch.getBoolPref("warn-external-default"); - } catch (e) { - // Nothing to tell us what to do, so be paranoid and prompt. - alwaysAsk = true; - } - } + // If neither of the prefs exists, be paranoid and prompt. + alwaysAsk = + prefBranch.getBoolPref("warn-external." + type, + prefBranch.getBoolPref("warn-external-default", + true)); } aHandlerInfo.alwaysAskBeforeHandling = alwaysAsk; From f1d39b624edf00c98169e352b7ed5133c3f7c57c Mon Sep 17 00:00:00 2001 From: Ascrod <32915892+Ascrod@users.noreply.github.com> Date: Mon, 8 Apr 2019 20:42:10 -0400 Subject: [PATCH 05/30] Issue #991 Part 5: DOM and layout --- .../BrowserElementChildPreload.js | 11 +-- .../test/test_globalObjects_other.xul | 10 +- ...PresentationDataChannelSessionTransport.js | 8 +- ...orageQuotaPrivateBrowsing_perwindowpb.html | 8 +- layout/tools/reftest/reftest.jsm | 94 +++---------------- 5 files changed, 21 insertions(+), 110 deletions(-) diff --git a/dom/browser-element/BrowserElementChildPreload.js b/dom/browser-element/BrowserElementChildPreload.js index 780dfa80e8..5adff2caca 100644 --- a/dom/browser-element/BrowserElementChildPreload.js +++ b/dom/browser-element/BrowserElementChildPreload.js @@ -1010,11 +1010,7 @@ BrowserElementChild.prototype = { self._takeScreenshot(maxWidth, maxHeight, mimeType, domRequestID); }; - let maxDelayMS = 2000; - try { - maxDelayMS = Services.prefs.getIntPref('dom.browserElement.maxScreenshotDelayMS'); - } - catch(e) {} + let maxDelayMS = Services.prefs.getIntPref('dom.browserElement.maxScreenshotDelayMS', 2000); // Try to wait for the event loop to go idle before we take the screenshot, // but once we've waited maxDelayMS milliseconds, go ahead and take it @@ -1720,10 +1716,7 @@ BrowserElementChild.prototype = { // certerror? If yes, maybe we should add a property to the // event to to indicate whether there is a custom page. That would // let the embedder have more control over the desired behavior. - let errorPage = null; - try { - errorPage = Services.prefs.getCharPref(CERTIFICATE_ERROR_PAGE_PREF); - } catch (e) {} + let errorPage = Services.prefs.getCharPref(CERTIFICATE_ERROR_PAGE_PREF, ""); if (errorPage == 'certerror') { sendAsyncMsg('error', { type: 'certerror' }); diff --git a/dom/indexedDB/test/test_globalObjects_other.xul b/dom/indexedDB/test/test_globalObjects_other.xul index eb180a9b4a..b527d66bd5 100644 --- a/dom/indexedDB/test/test_globalObjects_other.xul +++ b/dom/indexedDB/test/test_globalObjects_other.xul @@ -31,14 +31,8 @@ Cu.import("resource://gre/modules/Services.jsm"); for (var stage of [ "install", "startup", "shutdown", "uninstall" ]) { for (var symbol of [ "IDBKeyRange", "indexedDB" ]) { - let pref; - try { - pref = Services.prefs.getBoolPref("indexeddbtest.bootstrap." + stage + - "." + symbol); - } - catch(ex) { - pref = false; - } + let pref = Services.prefs.getBoolPref("indexeddbtest.bootstrap." + stage + + "." + symbol, false); ok(pref, "Symbol '" + symbol + "' present during '" + stage + "'"); } } diff --git a/dom/presentation/PresentationDataChannelSessionTransport.js b/dom/presentation/PresentationDataChannelSessionTransport.js index 461e4f2cb6..9af6213cb0 100644 --- a/dom/presentation/PresentationDataChannelSessionTransport.js +++ b/dom/presentation/PresentationDataChannelSessionTransport.js @@ -109,13 +109,7 @@ PresentationTransportBuilder.prototype = { // TODO bug 1228235 we should have a way to let device providers customize // the time-out duration. - let timeout; - try { - timeout = Services.prefs.getIntPref("presentation.receiver.loading.timeout"); - } catch (e) { - // This happens if the pref doesn't exist, so we have a default value. - timeout = 10000; - } + let timeout = Services.prefs.getIntPref("presentation.receiver.loading.timeout", 10000); // The timer is to check if the negotiation finishes on time. this._timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); diff --git a/dom/tests/mochitest/localstorage/test_localStorageQuotaPrivateBrowsing_perwindowpb.html b/dom/tests/mochitest/localstorage/test_localStorageQuotaPrivateBrowsing_perwindowpb.html index 24d3e4ba33..4dcc361c17 100644 --- a/dom/tests/mochitest/localstorage/test_localStorageQuotaPrivateBrowsing_perwindowpb.html +++ b/dom/tests/mochitest/localstorage/test_localStorageQuotaPrivateBrowsing_perwindowpb.html @@ -14,13 +14,7 @@ const Ci = Components.interfaces; const CONTENT_PAGE = "http://mochi.test:8888/chrome/dom/tests/mochitest/localstorage/page_blank.html"; const slavePath = "/chrome/dom/tests/mochitest/localstorage/"; var currentTest = 1; -var quota; - -try { - quota = Services.prefs.getIntPref("dom.storage.default_quota"); -} catch (ex) { - quota = 5 * 1024; -} +var quota = Services.prefs.getIntPref("dom.storage.default_quota", 5 * 1024); Services.prefs.setIntPref("browser.startup.page", 0); Services.prefs.setIntPref("dom.storage.default_quota", 1); diff --git a/layout/tools/reftest/reftest.jsm b/layout/tools/reftest/reftest.jsm index bd9976b0fc..c885150a49 100644 --- a/layout/tools/reftest/reftest.jsm +++ b/layout/tools/reftest/reftest.jsm @@ -277,29 +277,10 @@ this.OnRefTestLoad = function OnRefTestLoad(win) var prefs = Components.classes["@mozilla.org/preferences-service;1"]. getService(Components.interfaces.nsIPrefBranch); - try { - gBrowserIsRemote = prefs.getBoolPref("browser.tabs.remote.autostart"); - } catch (e) { - gBrowserIsRemote = false; - } - - try { - gB2GisMulet = prefs.getBoolPref("b2g.is_mulet"); - } catch (e) { - gB2GisMulet = false; - } - - try { - gBrowserIsIframe = prefs.getBoolPref("reftest.browser.iframe.enabled"); - } catch (e) { - gBrowserIsIframe = false; - } - - try { - gLogLevel = prefs.getCharPref("reftest.logLevel"); - } catch (e) { - gLogLevel ='info'; - } + gBrowserIsRemote = prefs.getBoolPref("browser.tabs.remote.autostart", false); + gB2GisMulet = prefs.getBoolPref("b2g.is_mulet", false); + gBrowserIsIframe = prefs.getBoolPref("reftest.browser.iframe.enabled", false); + gLogLevel = prefs.getCharPref("reftest.logLevel", "info"); if (win === undefined || win == null) { win = window; @@ -366,11 +347,7 @@ function InitAndStartRefTests() } catch (e) {} /* set the gLoadTimeout */ - try { - gLoadTimeout = prefs.getIntPref("reftest.timeout"); - } catch(e) { - gLoadTimeout = 5 * 60 * 1000; //5 minutes as per bug 479518 - } + gLoadTimeout = prefs.getIntPref("reftest.timeout", 5 * 60 * 1000); //5 minutes as per bug 479518 /* Get the logfile for android tests */ try { @@ -381,27 +358,12 @@ function InitAndStartRefTests() } } catch(e) {} - try { - gRemote = prefs.getBoolPref("reftest.remote"); - } catch(e) { - gRemote = false; - } - - try { - gIgnoreWindowSize = prefs.getBoolPref("reftest.ignoreWindowSize"); - } catch(e) { - gIgnoreWindowSize = false; - } + gRemote = prefs.getBoolPref("reftest.remote", false); + gIgnoreWindowSize = prefs.getBoolPref("reftest.ignoreWindowSize", false); /* Support for running a chunk (subset) of tests. In separate try as this is optional */ - try { - gTotalChunks = prefs.getIntPref("reftest.totalChunks"); - gThisChunk = prefs.getIntPref("reftest.thisChunk"); - } - catch(e) { - gTotalChunks = 0; - gThisChunk = 0; - } + gTotalChunks = prefs.getIntPref("reftest.totalChunks", 0); + gThisChunk = prefs.getIntPref("reftest.thisChunk", 0); try { gFocusFilterMode = prefs.getCharPref("reftest.focusFilterMode"); @@ -468,39 +430,17 @@ function StartTests() logger.error("EXCEPTION: " + e); } - try { - gNoCanvasCache = prefs.getIntPref("reftest.nocache"); - } catch(e) { - gNoCanvasCache = false; - } - - try { - gShuffle = prefs.getBoolPref("reftest.shuffle"); - } catch (e) { - gShuffle = false; - } - - try { - gRunUntilFailure = prefs.getBoolPref("reftest.runUntilFailure"); - } catch (e) { - gRunUntilFailure = false; - } + gNoCanvasCache = prefs.getIntPref("reftest.nocache", false); + gShuffle = prefs.getBoolPref("reftest.shuffle", false); + gRunUntilFailure = prefs.getBoolPref("reftest.runUntilFailure", false); // When we repeat this function is called again, so really only want to set // gRepeat once. if (gRepeat == null) { - try { - gRepeat = prefs.getIntPref("reftest.repeat"); - } catch (e) { - gRepeat = 0; - } + gRepeat = prefs.getIntPref("reftest.repeat", 0); } - try { - gRunSlowTests = prefs.getIntPref("reftest.skipslowtests"); - } catch(e) { - gRunSlowTests = false; - } + gRunSlowTests = prefs.getIntPref("reftest.skipslowtests", false); if (gShuffle) { gNoCanvasCache = true; @@ -737,11 +677,7 @@ function BuildConditionSandbox(aURL) { var prefs = CC["@mozilla.org/preferences-service;1"]. getService(CI.nsIPrefBranch); - try { - sandbox.nativeThemePref = !prefs.getBoolPref("mozilla.widget.disable-native-theme"); - } catch (e) { - sandbox.nativeThemePref = true; - } + sandbox.nativeThemePref = !prefs.getBoolPref("mozilla.widget.disable-native-theme", false); sandbox.prefs = CU.cloneInto({ getBoolPref: function(p) { return prefs.getBoolPref(p); }, From c54c6d3bd2f86619ed72d475481e2b0ce25b749d Mon Sep 17 00:00:00 2001 From: Ascrod <32915892+Ascrod@users.noreply.github.com> Date: Mon, 8 Apr 2019 21:06:29 -0400 Subject: [PATCH 06/30] Issue #991 Part 6: Services --- services/common/tokenserverclient.js | 5 +---- services/crypto/modules/WeaveCrypto.js | 6 +----- services/sync/modules/status.js | 7 +------ 3 files changed, 3 insertions(+), 15 deletions(-) diff --git a/services/common/tokenserverclient.js b/services/common/tokenserverclient.js index b220ab586c..ca40f7d931 100644 --- a/services/common/tokenserverclient.js +++ b/services/common/tokenserverclient.js @@ -149,10 +149,7 @@ TokenServerClientServerError.prototype._toStringFields = function() { */ this.TokenServerClient = function TokenServerClient() { this._log = Log.repository.getLogger("Common.TokenServerClient"); - let level = "Debug"; - try { - level = Services.prefs.getCharPref(PREF_LOG_LEVEL); - } catch (ex) {} + let level = Services.prefs.getCharPref(PREF_LOG_LEVEL, "Debug"); this._log.level = Log.Level[level]; } TokenServerClient.prototype = { diff --git a/services/crypto/modules/WeaveCrypto.js b/services/crypto/modules/WeaveCrypto.js index c040c4f6fc..d0d5bdb186 100644 --- a/services/crypto/modules/WeaveCrypto.js +++ b/services/crypto/modules/WeaveCrypto.js @@ -51,11 +51,7 @@ WeaveCrypto.prototype = { this.prefBranch = Services.prefs.getBranch("services.sync.log."); this.prefBranch.addObserver("cryptoDebug", this.observer, false); this.observer._self = this; - try { - this.debug = this.prefBranch.getBoolPref("cryptoDebug"); - } catch (x) { - this.debug = false; - } + this.debug = this.prefBranch.getBoolPref("cryptoDebug", false); XPCOMUtils.defineLazyGetter(this, 'encoder', () => new TextEncoder(UTF_LABEL)); XPCOMUtils.defineLazyGetter(this, 'decoder', () => new TextDecoder(UTF_LABEL, { fatal: true })); }, diff --git a/services/sync/modules/status.js b/services/sync/modules/status.js index a5b8305b41..1233cbe143 100644 --- a/services/sync/modules/status.js +++ b/services/sync/modules/status.js @@ -125,12 +125,7 @@ this.Status = { resetSync: function resetSync() { // Logger setup. let logPref = PREFS_BRANCH + "log.logger.status"; - let logLevel = "Trace"; - try { - logLevel = Services.prefs.getCharPref(logPref); - } catch (ex) { - // Use default. - } + let logLevel = Services.prefs.getCharPref(logPref, "Trace"); this._log.level = Log.Level[logLevel]; this._log.info("Resetting Status."); From e309515e66cfaf365046c7b3496db1d147611165 Mon Sep 17 00:00:00 2001 From: Ascrod <32915892+Ascrod@users.noreply.github.com> Date: Sat, 13 Apr 2019 10:53:58 -0400 Subject: [PATCH 07/30] Issue #991 Part 7: Toolkit Components --- .../asyncshutdown/AsyncShutdown.jsm | 29 +++----------- .../blocklist/nsBlocklistService.js | 11 +---- .../jsdownloads/src/DownloadIntegration.jsm | 6 +-- toolkit/components/nsDefaultCLH.js | 8 +--- .../osfile/modules/osfile_async_front.jsm | 40 +++++-------------- .../places/PlacesCategoriesStarter.js | 7 +--- .../PlacesRemoteTabsAutocompleteProvider.jsm | 6 +-- toolkit/components/places/UnifiedComplete.js | 5 +-- .../components/places/nsPlacesExpiration.js | 14 ++----- .../places/tests/unit/test_000_frecency.js | 10 +---- .../printing/content/printPageSetup.js | 9 +---- .../printing/content/printdialog.js | 10 +---- .../printing/content/printjoboptions.js | 9 +---- .../search/current/nsSearchService.js | 38 +++++------------- .../search/orginal/nsSearchService.js | 38 +++++------------- toolkit/components/telemetry/UITelemetry.jsm | 6 +-- .../nsUrlClassifierHashCompleter.js | 7 +--- .../tests/unit/test_urlformatter.js | 6 +-- 18 files changed, 55 insertions(+), 204 deletions(-) diff --git a/toolkit/components/asyncshutdown/AsyncShutdown.jsm b/toolkit/components/asyncshutdown/AsyncShutdown.jsm index 62ac36f42c..9cdf9e126d 100644 --- a/toolkit/components/asyncshutdown/AsyncShutdown.jsm +++ b/toolkit/components/asyncshutdown/AsyncShutdown.jsm @@ -79,12 +79,8 @@ const DELAY_WARNING_MS = 10 * 1000; // Crash the process if shutdown is really too long // (allowing for sleep). const PREF_DELAY_CRASH_MS = "toolkit.asyncshutdown.crash_timeout"; -var DELAY_CRASH_MS = 60 * 1000; // One minute -try { - DELAY_CRASH_MS = Services.prefs.getIntPref(PREF_DELAY_CRASH_MS); -} catch (ex) { - // Ignore errors -} +var DELAY_CRASH_MS = Services.prefs.getIntPref(PREF_DELAY_CRASH_MS, + 60 * 1000); // One minute Services.prefs.addObserver(PREF_DELAY_CRASH_MS, function() { DELAY_CRASH_MS = Services.prefs.getIntPref(PREF_DELAY_CRASH_MS); }, false); @@ -207,12 +203,7 @@ function log(msg, prefix = "", error = null) { } } const PREF_DEBUG_LOG = "toolkit.asyncshutdown.log"; -var DEBUG_LOG = false; -try { - DEBUG_LOG = Services.prefs.getBoolPref(PREF_DEBUG_LOG); -} catch (ex) { - // Ignore errors -} +var DEBUG_LOG = Services.prefs.getBoolPref(PREF_DEBUG_LOG, false); Services.prefs.addObserver(PREF_DEBUG_LOG, function() { DEBUG_LOG = Services.prefs.getBoolPref(PREF_DEBUG_LOG); }, false); @@ -360,12 +351,7 @@ this.AsyncShutdown = { * Access function getPhase. For testing purposes only. */ get _getPhase() { - let accepted = false; - try { - accepted = Services.prefs.getBoolPref("toolkit.asyncshutdown.testing"); - } catch (ex) { - // Ignore errors - } + let accepted = Services.prefs.getBoolPref("toolkit.asyncshutdown.testing", false); if (accepted) { return getPhase; } @@ -464,12 +450,7 @@ function getPhase(topic) { * notification. For testing purposes only. */ get _trigger() { - let accepted = false; - try { - accepted = Services.prefs.getBoolPref("toolkit.asyncshutdown.testing"); - } catch (ex) { - // Ignore errors - } + let accepted = Services.prefs.getBoolPref("toolkit.asyncshutdown.testing", false); if (accepted) { return () => spinner.observe(); } diff --git a/toolkit/components/blocklist/nsBlocklistService.js b/toolkit/components/blocklist/nsBlocklistService.js index 28bfa71bf8..f764497ed4 100644 --- a/toolkit/components/blocklist/nsBlocklistService.js +++ b/toolkit/components/blocklist/nsBlocklistService.js @@ -243,16 +243,7 @@ function getLocale() { /* Get the distribution pref values, from defaults only */ function getDistributionPrefValue(aPrefName) { - var prefValue = "default"; - - var defaults = gPref.getDefaultBranch(null); - try { - prefValue = defaults.getCharPref(aPrefName); - } catch (e) { - // use default when pref not found - } - - return prefValue; + return gPref.getDefaultBranch(null).getCharPref(aPrefName, "default"); } /** diff --git a/toolkit/components/jsdownloads/src/DownloadIntegration.jsm b/toolkit/components/jsdownloads/src/DownloadIntegration.jsm index 3052847497..a6875ec48f 100644 --- a/toolkit/components/jsdownloads/src/DownloadIntegration.jsm +++ b/toolkit/components/jsdownloads/src/DownloadIntegration.jsm @@ -368,11 +368,7 @@ this.DownloadIntegration = { */ getPreferredDownloadsDirectory: Task.async(function* () { let directoryPath = null; - let prefValue = 1; - - try { - prefValue = Services.prefs.getIntPref("browser.download.folderList"); - } catch(e) {} + let prefValue = Services.prefs.getIntPref("browser.download.folderList", 1); switch(prefValue) { case 0: // Desktop diff --git a/toolkit/components/nsDefaultCLH.js b/toolkit/components/nsDefaultCLH.js index a081bae498..affbf75ee4 100644 --- a/toolkit/components/nsDefaultCLH.js +++ b/toolkit/components/nsDefaultCLH.js @@ -104,13 +104,7 @@ nsDefaultCLH.prototype = { // if the pref is missing, ignore the exception try { var chromeURI = prefs.getCharPref("toolkit.defaultChromeURI"); - - var flags = "chrome,dialog=no,all"; - try { - flags = prefs.getCharPref("toolkit.defaultChromeFeatures"); - } - catch (e) { } - + var flags = prefs.getCharPref("toolkit.defaultChromeFeatures", "chrome,dialog=no,all"); var wwatch = Components.classes["@mozilla.org/embedcomp/window-watcher;1"] .getService(nsIWindowWatcher); wwatch.openWindow(null, chromeURI, "_blank", diff --git a/toolkit/components/osfile/modules/osfile_async_front.jsm b/toolkit/components/osfile/modules/osfile_async_front.jsm index 93c7e66d6c..1d1d7c73fd 100644 --- a/toolkit/components/osfile/modules/osfile_async_front.jsm +++ b/toolkit/components/osfile/modules/osfile_async_front.jsm @@ -239,10 +239,9 @@ var Scheduler = this.Scheduler = { * Prepare to kill the OS.File worker after a few seconds. */ restartTimer: function(arg) { - let delay; - try { - delay = Services.prefs.getIntPref("osfile.reset_worker_delay"); - } catch(e) { + let delay = Services.prefs.getIntPref("osfile.reset_worker_delay", 0); + + if (!delay) { // Don't auto-shutdown if we don't have a delay preference set. return; } @@ -468,43 +467,25 @@ var Scheduler = this.Scheduler = { const PREF_OSFILE_LOG = "toolkit.osfile.log"; const PREF_OSFILE_LOG_REDIRECT = "toolkit.osfile.log.redirect"; -/** - * Safely read a PREF_OSFILE_LOG preference. - * Returns a value read or, in case of an error, oldPref or false. - * - * @param bool oldPref - * An optional value that the DEBUG flag was set to previously. - */ -function readDebugPref(prefName, oldPref = false) { - let pref = oldPref; - try { - pref = Services.prefs.getBoolPref(prefName); - } catch (x) { - // In case of an error when reading a pref keep it as is. - } - // If neither pref nor oldPref were set, default it to false. - return pref; -}; - /** * Listen to PREF_OSFILE_LOG changes and update gShouldLog flag * appropriately. */ Services.prefs.addObserver(PREF_OSFILE_LOG, function prefObserver(aSubject, aTopic, aData) { - SharedAll.Config.DEBUG = readDebugPref(PREF_OSFILE_LOG, SharedAll.Config.DEBUG); + SharedAll.Config.DEBUG = Services.prefs.getBoolPref(PREF_OSFILE_LOG, SharedAll.Config.DEBUG); if (Scheduler.launched) { // Don't start the worker just to set this preference. Scheduler.post("SET_DEBUG", [SharedAll.Config.DEBUG]); } }, false); -SharedAll.Config.DEBUG = readDebugPref(PREF_OSFILE_LOG, false); +SharedAll.Config.DEBUG = Services.prefs.getBoolPref(PREF_OSFILE_LOG, false); Services.prefs.addObserver(PREF_OSFILE_LOG_REDIRECT, function prefObserver(aSubject, aTopic, aData) { - SharedAll.Config.TEST = readDebugPref(PREF_OSFILE_LOG_REDIRECT, OS.Shared.TEST); + SharedAll.Config.TEST = Services.prefs.getBoolPref(PREF_OSFILE_LOG_REDIRECT, OS.Shared.TEST); }, false); -SharedAll.Config.TEST = readDebugPref(PREF_OSFILE_LOG_REDIRECT, false); +SharedAll.Config.TEST = Services.prefs.getBoolPref(PREF_OSFILE_LOG_REDIRECT, false); /** @@ -515,7 +496,7 @@ var nativeWheneverAvailable = true; const PREF_OSFILE_NATIVE = "toolkit.osfile.native"; Services.prefs.addObserver(PREF_OSFILE_NATIVE, function prefObserver(aSubject, aTopic, aData) { - nativeWheneverAvailable = readDebugPref(PREF_OSFILE_NATIVE, nativeWheneverAvailable); + nativeWheneverAvailable = Services.prefs.getBoolPref(PREF_OSFILE_NATIVE, nativeWheneverAvailable); }, false); @@ -557,10 +538,9 @@ Services.prefs.addObserver(PREF_OSFILE_TEST_SHUTDOWN_OBSERVER, function prefObserver() { // The temporary phase topic used to trigger the unclosed // phase warning. - let TOPIC = null; + let TOPIC = Services.prefs.getCharPref(PREF_OSFILE_TEST_SHUTDOWN_OBSERVER, + ""); try { - TOPIC = Services.prefs.getCharPref( - PREF_OSFILE_TEST_SHUTDOWN_OBSERVER); } catch (x) { } if (TOPIC) { diff --git a/toolkit/components/places/PlacesCategoriesStarter.js b/toolkit/components/places/PlacesCategoriesStarter.js index 560bd486a8..bab14db52d 100644 --- a/toolkit/components/places/PlacesCategoriesStarter.js +++ b/toolkit/components/places/PlacesCategoriesStarter.js @@ -77,11 +77,8 @@ PlacesCategoriesStarter.prototype = { break; case "idle-daily": // Once a week run places.sqlite maintenance tasks. - let lastMaintenance = 0; - try { - lastMaintenance = - Services.prefs.getIntPref("places.database.lastMaintenance"); - } catch (ex) {} + let lastMaintenance = + Services.prefs.getIntPref("places.database.lastMaintenance", 0); let nowSeconds = parseInt(Date.now() / 1000); if (lastMaintenance < nowSeconds - MAINTENANCE_INTERVAL_SECONDS) { PlacesDBUtils.maintenanceOnIdle(); diff --git a/toolkit/components/places/PlacesRemoteTabsAutocompleteProvider.jsm b/toolkit/components/places/PlacesRemoteTabsAutocompleteProvider.jsm index d23d5bc6e7..c3bc501eb2 100644 --- a/toolkit/components/places/PlacesRemoteTabsAutocompleteProvider.jsm +++ b/toolkit/components/places/PlacesRemoteTabsAutocompleteProvider.jsm @@ -95,11 +95,7 @@ function observe(subject, topic, data) { case "nsPref:changed": if (data == PREF_SHOW_REMOTE_ICONS) { - try { - showRemoteIcons = Services.prefs.getBoolPref(PREF_SHOW_REMOTE_ICONS); - } catch (_) { - showRemoteIcons = true; // no such pref - default is to show the icons. - } + showRemoteIcons = Services.prefs.getBoolPref(PREF_SHOW_REMOTE_ICONS, true); } break; diff --git a/toolkit/components/places/UnifiedComplete.js b/toolkit/components/places/UnifiedComplete.js index ec5f911115..7b63e29bb4 100644 --- a/toolkit/components/places/UnifiedComplete.js +++ b/toolkit/components/places/UnifiedComplete.js @@ -437,10 +437,7 @@ XPCOMUtils.defineLazyGetter(this, "Prefs", () => { store.suggestTyped = prefs.get(...PREF_SUGGEST_HISTORY_ONLYTYPED); store.suggestSearches = prefs.get(...PREF_SUGGEST_SEARCHES); store.maxCharsForSearchSuggestions = prefs.get(...PREF_MAX_CHARS_FOR_SUGGEST); - store.keywordEnabled = true; - try { - store.keywordEnabled = Services.prefs.getBoolPref("keyword.enabled"); - } catch (ex) {} + store.keywordEnabled = Services.prefs.getBoolPref("keyword.enabled", true); // If history is not set, onlyTyped value should be ignored. if (!store.suggestHistory) { diff --git a/toolkit/components/places/nsPlacesExpiration.js b/toolkit/components/places/nsPlacesExpiration.js index 499934362d..767a4d3458 100644 --- a/toolkit/components/places/nsPlacesExpiration.js +++ b/toolkit/components/places/nsPlacesExpiration.js @@ -812,11 +812,8 @@ nsPlacesExpiration.prototype = { _loadPrefs: Task.async(function* () { // Get the user's limit, if it was set. - try { - // We want to silently fail since getIntPref throws if it does not exist, - // and use a default to fallback to. - this._urisLimit = this._prefBranch.getIntPref(PREF_MAX_URIS); - } catch (ex) { /* User limit not set */ } + this._urisLimit = this._prefBranch.getIntPref(PREF_MAX_URIS, + PREF_MAX_URIS_NOTSET); if (this._urisLimit < 0) { // Some testing code expects a pref change to be synchronous, so @@ -874,11 +871,8 @@ nsPlacesExpiration.prototype = { this._urisLimit); // Get the expiration interval value. - try { - // We want to silently fail since getIntPref throws if it does not exist, - // and use a default to fallback to. - this._interval = this._prefBranch.getIntPref(PREF_INTERVAL_SECONDS); - } catch (ex) { /* User interval not set */ } + this._interval = this._prefBranch.getIntPref(PREF_INTERVAL_SECONDS, + PREF_INTERVAL_SECONDS_NOTSET); if (this._interval <= 0) { this._interval = PREF_INTERVAL_SECONDS_NOTSET; } diff --git a/toolkit/components/places/tests/unit/test_000_frecency.js b/toolkit/components/places/tests/unit/test_000_frecency.js index 0a7347a026..64ee86b593 100644 --- a/toolkit/components/places/tests/unit/test_000_frecency.js +++ b/toolkit/components/places/tests/unit/test_000_frecency.js @@ -56,14 +56,8 @@ var prefPrefix = "places.frecency."; function* task_initializeBucket(bucket) { let [cutoffName, weightName] = bucket; // get pref values - var weight = 0, cutoff = 0; - try { - weight = prefs.getIntPref(prefPrefix + weightName); - } catch (ex) {} - try { - cutoff = prefs.getIntPref(prefPrefix + cutoffName); - } catch (ex) {} - + var weight = prefs.getIntPref(prefPrefix + weightName, 0); + var cutoff = prefs.getIntPref(prefPrefix + cutoffName, 0); if (cutoff < 1) return; diff --git a/toolkit/components/printing/content/printPageSetup.js b/toolkit/components/printing/content/printPageSetup.js index 31eb1bdd34..6bcd972076 100644 --- a/toolkit/components/printing/content/printPageSetup.js +++ b/toolkit/components/printing/content/printPageSetup.js @@ -65,14 +65,7 @@ function initDialog() // --------------------------------------------------- function isListOfPrinterFeaturesAvailable() { - var has_printerfeatures = false; - - try { - has_printerfeatures = gPrefs.getBoolPref("print.tmp.printerfeatures." + gPrintSettings.printerName + ".has_special_printerfeatures"); - } catch (ex) { - } - - return has_printerfeatures; + return gPrefs.getBoolPref("print.tmp.printerfeatures." + gPrintSettings.printerName + ".has_special_printerfeatures", false); } // --------------------------------------------------- diff --git a/toolkit/components/printing/content/printdialog.js b/toolkit/components/printing/content/printdialog.js index e5a38ddced..f9ed94badd 100644 --- a/toolkit/components/printing/content/printdialog.js +++ b/toolkit/components/printing/content/printdialog.js @@ -79,15 +79,7 @@ function stripTrailingWhitespace(element) // --------------------------------------------------- function getPrinterDescription(printerName) { - var s = ""; - - try { - /* This may not work with non-ASCII test (see bug 235763 comment #16) */ - s = gPrefs.getCharPref("print.printer_" + printerName + ".printer_description") - } catch (e) { - } - - return s; + return gPrefs.getCharPref("print.printer_" + printerName + ".printer_description", "") } // --------------------------------------------------- diff --git a/toolkit/components/printing/content/printjoboptions.js b/toolkit/components/printing/content/printjoboptions.js index 26f3ffd854..098358940e 100644 --- a/toolkit/components/printing/content/printjoboptions.js +++ b/toolkit/components/printing/content/printjoboptions.js @@ -33,14 +33,7 @@ function checkDouble(element, maxVal) // --------------------------------------------------- function isListOfPrinterFeaturesAvailable() { - var has_printerfeatures = false; - - try { - has_printerfeatures = gPrefs.getBoolPref("print.tmp.printerfeatures." + gPrintSettings.printerName + ".has_special_printerfeatures"); - } catch (ex) { - } - - return has_printerfeatures; + return gPrefs.getBoolPref("print.tmp.printerfeatures." + gPrintSettings.printerName + ".has_special_printerfeatures", false); } // --------------------------------------------------- diff --git a/toolkit/components/search/current/nsSearchService.js b/toolkit/components/search/current/nsSearchService.js index 2a86010780..66866540a7 100644 --- a/toolkit/components/search/current/nsSearchService.js +++ b/toolkit/components/search/current/nsSearchService.js @@ -390,12 +390,7 @@ function isPartnerBuild() { // Method to determine if we should be using geo-specific defaults function geoSpecificDefaultsEnabled() { - let geoSpecificDefaults = false; - try { - geoSpecificDefaults = Services.prefs.getBoolPref("browser.search.geoSpecificDefaults"); - } catch (e) {} - - return geoSpecificDefaults; + return Services.prefs.getBoolPref("browser.search.geoSpecificDefaults", false); } // Some notes on countryCode and region prefs: @@ -500,11 +495,7 @@ function isUSTimezone() { // If it fails we don't touch that pref so isUS() does its normal thing. var ensureKnownCountryCode = Task.async(function* (ss) { // If we have a country-code already stored in our prefs we trust it. - let countryCode; - try { - countryCode = Services.prefs.getCharPref("browser.search.countryCode"); - } catch (e) {} - + let countryCode = Services.prefs.getCharPref("browser.search.countryCode", ""); if (!countryCode) { // We don't have it cached, so fetch it. fetchCountryCode() will call // storeCountryCode if it gets a result (even if that happens after the @@ -726,10 +717,7 @@ var fetchRegionDefault = (ss) => new Promise(resolve => { // Append the optional cohort value. const cohortPref = "browser.search.cohort"; - let cohort; - try { - cohort = Services.prefs.getCharPref(cohortPref); - } catch (e) {} + let cohort = Services.prefs.getCharPref(cohortPref, ""); if (cohort) endpoint += "/" + cohort; @@ -993,19 +981,13 @@ function QueryParameter(aName, aValue, aPurpose) { function ParamSubstitution(aParamValue, aSearchTerms, aEngine) { var value = aParamValue; - var distributionID = Services.appinfo.distributionID; - try { - distributionID = Services.prefs.getCharPref(BROWSER_SEARCH_PREF + "distributionID"); - } - catch (ex) { } - var official = MOZ_OFFICIAL; - try { - if (Services.prefs.getBoolPref(BROWSER_SEARCH_PREF + "official")) - official = "official"; - else - official = "unofficial"; - } - catch (ex) { } + var distributionID = Services.prefs.getCharPref(BROWSER_SEARCH_PREF + "distributionID", + Services.appinfo.distributionID || ""); + var official; + if (Services.prefs.getBoolPref(BROWSER_SEARCH_PREF + "official", MOZ_OFFICIAL)) + official = "official"; + else + official = "unofficial"; // Custom search parameters. These are only available to default search // engines. diff --git a/toolkit/components/search/orginal/nsSearchService.js b/toolkit/components/search/orginal/nsSearchService.js index 5b5ce7a577..c4481df5f3 100644 --- a/toolkit/components/search/orginal/nsSearchService.js +++ b/toolkit/components/search/orginal/nsSearchService.js @@ -418,12 +418,7 @@ function isPartnerBuild() { // Method to determine if we should be using geo-specific defaults function geoSpecificDefaultsEnabled() { - let geoSpecificDefaults = false; - try { - geoSpecificDefaults = Services.prefs.getBoolPref("browser.search.geoSpecificDefaults"); - } catch(e) {} - - return geoSpecificDefaults; + return Services.prefs.getBoolPref("browser.search.geoSpecificDefaults", false); } // Some notes on countryCode and region prefs: @@ -528,11 +523,7 @@ function isUSTimezone() { // If it fails we don't touch that pref so isUS() does its normal thing. var ensureKnownCountryCode = Task.async(function* () { // If we have a country-code already stored in our prefs we trust it. - let countryCode; - try { - countryCode = Services.prefs.getCharPref("browser.search.countryCode"); - } catch(e) {} - + let countryCode = Services.prefs.getCharPref("browser.search.countryCode", ""); if (!countryCode) { // We don't have it cached, so fetch it. fetchCountryCode() will call // storeCountryCode if it gets a result (even if that happens after the @@ -732,10 +723,7 @@ var fetchRegionDefault = () => new Promise(resolve => { // Append the optional cohort value. const cohortPref = "browser.search.cohort"; - let cohort; - try { - cohort = Services.prefs.getCharPref(cohortPref); - } catch(e) {} + let cohort = Services.prefs.getCharPref(cohortPref, ""); if (cohort) endpoint += "/" + cohort; @@ -1034,19 +1022,13 @@ function QueryParameter(aName, aValue, aPurpose) { function ParamSubstitution(aParamValue, aSearchTerms, aEngine) { var value = aParamValue; - var distributionID = MOZ_DISTRIBUTION_ID; - try { - distributionID = Services.prefs.getCharPref(BROWSER_SEARCH_PREF + "distributionID"); - } - catch (ex) { } - var official = MOZ_OFFICIAL; - try { - if (Services.prefs.getBoolPref(BROWSER_SEARCH_PREF + "official")) - official = "official"; - else - official = "unofficial"; - } - catch (ex) { } + var distributionID = Services.prefs.getCharPref(BROWSER_SEARCH_PREF + "distributionID", + MOZ_DISTRIBUTION_ID || ""); + var official; + if (Services.prefs.getBoolPref(BROWSER_SEARCH_PREF + "official", MOZ_OFFICIAL)) + official = "official"; + else + official = "unofficial"; // Custom search parameters. These are only available to default search // engines. diff --git a/toolkit/components/telemetry/UITelemetry.jsm b/toolkit/components/telemetry/UITelemetry.jsm index bd7a34b725..231b52e23c 100644 --- a/toolkit/components/telemetry/UITelemetry.jsm +++ b/toolkit/components/telemetry/UITelemetry.jsm @@ -36,11 +36,7 @@ this.UITelemetry = { Services.obs.addObserver(this, "profile-before-change", false); // Pick up the current value. - try { - this._enabled = Services.prefs.getBoolPref(PREF_ENABLED); - } catch (e) { - this._enabled = false; - } + this._enabled = Services.prefs.getBoolPref(PREF_ENABLED, false); return this._enabled; }, diff --git a/toolkit/components/url-classifier/nsUrlClassifierHashCompleter.js b/toolkit/components/url-classifier/nsUrlClassifierHashCompleter.js index ba4bf225a6..5d462c90d4 100644 --- a/toolkit/components/url-classifier/nsUrlClassifierHashCompleter.js +++ b/toolkit/components/url-classifier/nsUrlClassifierHashCompleter.js @@ -20,12 +20,7 @@ Cu.import("resource://gre/modules/NetUtil.jsm"); // Log only if browser.safebrowsing.debug is true function log(...stuff) { - let logging = null; - try { - logging = Services.prefs.getBoolPref("browser.safebrowsing.debug"); - } catch(e) { - return; - } + let logging = Services.prefs.getBoolPref("browser.safebrowsing.debug", false); if (!logging) { return; } diff --git a/toolkit/components/urlformatter/tests/unit/test_urlformatter.js b/toolkit/components/urlformatter/tests/unit/test_urlformatter.js index 393b0dc33b..6f218822ae 100644 --- a/toolkit/components/urlformatter/tests/unit/test_urlformatter.js +++ b/toolkit/components/urlformatter/tests/unit/test_urlformatter.js @@ -27,11 +27,9 @@ function run_test() { QueryInterface(Ci.nsIXULRuntime); var abi = macutils && macutils.isUniversalBinary ? "Universal-gcc3" : appInfo.XPCOMABI; - let channel = "default"; let defaults = prefs.QueryInterface(Ci.nsIPrefService).getDefaultBranch(null); - try { - channel = defaults.getCharPref("app.update.channel"); - } catch (e) {} + let channel = defaults.getCharPref("app.update.channel", "default"); + // Set distribution values. defaults.setCharPref("distribution.id", "bacon"); defaults.setCharPref("distribution.version", "1.0"); From a24456a76d824414384f0736a3e95242718e4946 Mon Sep 17 00:00:00 2001 From: Ascrod <32915892+Ascrod@users.noreply.github.com> Date: Sat, 13 Apr 2019 11:22:15 -0400 Subject: [PATCH 08/30] Issue #991 Part 8: Toolkit everything else --- toolkit/content/widgets/browser.xml | 23 +----- toolkit/content/widgets/preferences.xml | 8 +- toolkit/content/widgets/scrollbox.xml | 27 ++----- toolkit/modules/NewTabUtils.jsm | 20 ++--- toolkit/modules/PermissionsUtils.jsm | 5 +- toolkit/modules/UpdateUtils.jsm | 18 +---- .../secondscreen/SimpleServiceDiscovery.jsm | 5 +- toolkit/mozapps/downloads/nsHelperAppDlg.js | 5 +- toolkit/mozapps/extensions/AddonManager.jsm | 79 +++++-------------- toolkit/mozapps/extensions/DeferredSave.jsm | 7 +- toolkit/mozapps/extensions/GMPUtils.jsm | 8 +- .../extensions/LightweightThemeManager.jsm | 15 +--- .../extensions/internal/AddonLogging.jsm | 7 +- .../extensions/internal/AddonRepository.jsm | 32 ++------ .../internal/AddonUpdateChecker.jsm | 14 +--- .../extensions/internal/XPIProviderUtils.js | 5 +- 16 files changed, 62 insertions(+), 216 deletions(-) diff --git a/toolkit/content/widgets/browser.xml b/toolkit/content/widgets/browser.xml index 9c72e86d88..11ed998cca 100644 --- a/toolkit/content/widgets/browser.xml +++ b/toolkit/content/widgets/browser.xml @@ -19,14 +19,7 @@ if (this.getAttribute("autoscroll") == "false") return false; - var enabled = true; - try { - enabled = this.mPrefs.getBoolPref("general.autoScroll"); - } - catch (ex) { - } - - return enabled; + return this.mPrefs.getBoolPref("general.autoScroll", true); ]]> @@ -1427,18 +1420,8 @@ return; // Toggle browse with caret mode - var browseWithCaretOn = false; - var warn = true; - - try { - warn = this.mPrefs.getBoolPref(kPrefWarnOnEnable); - } catch (ex) { - } - - try { - browseWithCaretOn = this.mPrefs.getBoolPref(kPrefCaretBrowsingOn); - } catch (ex) { - } + var browseWithCaretOn = this.mPrefs.getBoolPref(kPrefCaretBrowsingOn, false); + var warn = this.mPrefs.getBoolPref(kPrefWarnOnEnable, true); if (warn && !browseWithCaretOn) { var checkValue = {value:false}; var promptService = Components.classes["@mozilla.org/embedcomp/prompt-service;1"] diff --git a/toolkit/content/widgets/preferences.xml b/toolkit/content/widgets/preferences.xml index 9fb2ac7ea1..11de032462 100644 --- a/toolkit/content/widgets/preferences.xml +++ b/toolkit/content/widgets/preferences.xml @@ -893,12 +893,8 @@ diff --git a/toolkit/content/widgets/scrollbox.xml b/toolkit/content/widgets/scrollbox.xml index ff57a59114..219b7cb5e4 100644 --- a/toolkit/content/widgets/scrollbox.xml +++ b/toolkit/content/widgets/scrollbox.xml @@ -80,13 +80,8 @@ @@ -99,13 +94,8 @@ if (this.hasAttribute("smoothscroll")) { this._smoothScroll = (this.getAttribute("smoothscroll") == "true"); } else { - try { - this._smoothScroll = this._prefBranch - .getBoolPref("toolkit.scrollbox.smoothScroll"); - } - catch (ex) { - this._smoothScroll = true; - } + this._smoothScroll = this._prefBranch + .getBoolPref("toolkit.scrollbox.smoothScroll", true); } } return this._smoothScroll; @@ -737,12 +727,9 @@ threshold); }, @@ -1543,10 +1530,8 @@ this.AddonRepository = { // Create url from preference, returning null if preference does not exist _formatURLPref: function AddonRepo_formatURLPref(aPreference, aSubstitutions) { - let url = null; - try { - url = Services.prefs.getCharPref(aPreference); - } catch(e) { + let url = Services.prefs.getCharPref(aPreference, ""); + if (!url) { logger.warn("_formatURLPref: Couldn't get pref: " + aPreference); return null; } @@ -1639,10 +1624,7 @@ var AddonDatabase = { // Create a blank addons.json file this._saveDBToDisk(); - let dbSchema = 0; - try { - dbSchema = Services.prefs.getIntPref(PREF_GETADDONS_DB_SCHEMA); - } catch (e) {} + let dbSchema = Services.prefs.getIntPref(PREF_GETADDONS_DB_SCHEMA, 0); if (dbSchema < DB_MIN_JSON_SCHEMA) { let results = yield new Promise((resolve, reject) => { diff --git a/toolkit/mozapps/extensions/internal/AddonUpdateChecker.jsm b/toolkit/mozapps/extensions/internal/AddonUpdateChecker.jsm index c06dca1d53..a475f7f1fa 100644 --- a/toolkit/mozapps/extensions/internal/AddonUpdateChecker.jsm +++ b/toolkit/mozapps/extensions/internal/AddonUpdateChecker.jsm @@ -608,12 +608,7 @@ function UpdateParser(aId, aUpdateKey, aUrl, aObserver) { this.observer = aObserver; this.url = aUrl; - let requireBuiltIn = true; - try { - requireBuiltIn = Services.prefs.getBoolPref(PREF_UPDATE_REQUIREBUILTINCERTS); - } - catch (e) { - } + let requireBuiltIn = Services.prefs.getBoolPref(PREF_UPDATE_REQUIREBUILTINCERTS, true); logger.debug("Requesting " + aUrl); try { @@ -651,12 +646,7 @@ UpdateParser.prototype = { this.request = null; this._doneAt = new Error("place holder"); - let requireBuiltIn = true; - try { - requireBuiltIn = Services.prefs.getBoolPref(PREF_UPDATE_REQUIREBUILTINCERTS); - } - catch (e) { - } + let requireBuiltIn = Services.prefs.getBoolPref(PREF_UPDATE_REQUIREBUILTINCERTS, true); try { CertUtils.checkCert(request.channel, !requireBuiltIn); diff --git a/toolkit/mozapps/extensions/internal/XPIProviderUtils.js b/toolkit/mozapps/extensions/internal/XPIProviderUtils.js index f2420bd1ac..69d774e4a0 100644 --- a/toolkit/mozapps/extensions/internal/XPIProviderUtils.js +++ b/toolkit/mozapps/extensions/internal/XPIProviderUtils.js @@ -1410,10 +1410,7 @@ this.XPIDatabase = { // when a lightweight theme is applied for example) text += "\r\n[ThemeDirs]\r\n"; - let dssEnabled = false; - try { - dssEnabled = Services.prefs.getBoolPref(PREF_EM_DSS_ENABLED); - } catch (e) {} + let dssEnabled = Services.prefs.getBoolPref(PREF_EM_DSS_ENABLED, false); let themes = []; if (dssEnabled) { From 09bcb1f83410f479e55f4e776dc3abaa9477b003 Mon Sep 17 00:00:00 2001 From: Ascrod <32915892+Ascrod@users.noreply.github.com> Date: Sat, 13 Apr 2019 17:27:59 -0400 Subject: [PATCH 09/30] Pref try/catch block fixup. Tag #991. --- devtools/client/framework/toolbox-process-window.js | 4 ++-- .../components/search/current/nsSearchService.js | 13 ++++++++----- .../components/search/orginal/nsSearchService.js | 13 ++++++++----- 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/devtools/client/framework/toolbox-process-window.js b/devtools/client/framework/toolbox-process-window.js index c34fe33559..8f75e3e24d 100644 --- a/devtools/client/framework/toolbox-process-window.js +++ b/devtools/client/framework/toolbox-process-window.js @@ -103,8 +103,8 @@ function openToolbox({ form, chrome, isTabActor }) { // Remember the last panel that was used inside of this profile. // But if we are testing, then it should always open the debugger panel. let selectedTool = - Services.prefs.getCharPref("devtools.toolbox.selectedTool", - Services.prefs.getCharPref("devtools.browsertoolbox.panel", + Services.prefs.getCharPref("devtools.browsertoolbox.panel", + Services.prefs.getCharPref("devtools.toolbox.selectedTool", "jsdebugger")); let options = { customIframe: frame }; diff --git a/toolkit/components/search/current/nsSearchService.js b/toolkit/components/search/current/nsSearchService.js index 66866540a7..db90e5150e 100644 --- a/toolkit/components/search/current/nsSearchService.js +++ b/toolkit/components/search/current/nsSearchService.js @@ -983,11 +983,14 @@ function ParamSubstitution(aParamValue, aSearchTerms, aEngine) { var distributionID = Services.prefs.getCharPref(BROWSER_SEARCH_PREF + "distributionID", Services.appinfo.distributionID || ""); - var official; - if (Services.prefs.getBoolPref(BROWSER_SEARCH_PREF + "official", MOZ_OFFICIAL)) - official = "official"; - else - official = "unofficial"; + var official = MOZ_OFFICIAL; + try { + if (Services.prefs.getBoolPref(BROWSER_SEARCH_PREF + "official")) + official = "official"; + else + official = "unofficial"; + } + catch (ex) { } // Custom search parameters. These are only available to default search // engines. diff --git a/toolkit/components/search/orginal/nsSearchService.js b/toolkit/components/search/orginal/nsSearchService.js index c4481df5f3..1cec5f966e 100644 --- a/toolkit/components/search/orginal/nsSearchService.js +++ b/toolkit/components/search/orginal/nsSearchService.js @@ -1024,11 +1024,14 @@ function ParamSubstitution(aParamValue, aSearchTerms, aEngine) { var distributionID = Services.prefs.getCharPref(BROWSER_SEARCH_PREF + "distributionID", MOZ_DISTRIBUTION_ID || ""); - var official; - if (Services.prefs.getBoolPref(BROWSER_SEARCH_PREF + "official", MOZ_OFFICIAL)) - official = "official"; - else - official = "unofficial"; + var official = MOZ_OFFICIAL; + try { + if (Services.prefs.getBoolPref(BROWSER_SEARCH_PREF + "official")) + official = "official"; + else + official = "unofficial"; + } + catch (ex) { } // Custom search parameters. These are only available to default search // engines. From 0e3dbe8f549096b97438988f4f51c26f14b2fefd Mon Sep 17 00:00:00 2001 From: Ascrod <32915892+Ascrod@users.noreply.github.com> Date: Thu, 18 Apr 2019 20:37:23 -0400 Subject: [PATCH 10/30] Remove unused try/catch block. Tag #991. --- toolkit/components/osfile/modules/osfile_async_front.jsm | 3 --- 1 file changed, 3 deletions(-) diff --git a/toolkit/components/osfile/modules/osfile_async_front.jsm b/toolkit/components/osfile/modules/osfile_async_front.jsm index 1d1d7c73fd..9f3b7fb211 100644 --- a/toolkit/components/osfile/modules/osfile_async_front.jsm +++ b/toolkit/components/osfile/modules/osfile_async_front.jsm @@ -540,9 +540,6 @@ Services.prefs.addObserver(PREF_OSFILE_TEST_SHUTDOWN_OBSERVER, // phase warning. let TOPIC = Services.prefs.getCharPref(PREF_OSFILE_TEST_SHUTDOWN_OBSERVER, ""); - try { - } catch (x) { - } if (TOPIC) { // Generate a phase, add a blocker. // Note that this can work only if AsyncShutdown itself has been From 56ad09f64a7552bff99aa833bf2d6128370e0946 Mon Sep 17 00:00:00 2001 From: JustOff Date: Sat, 20 Apr 2019 13:53:46 +0300 Subject: [PATCH 11/30] Handle URL token in a closer way to the CSS3 spec --- devtools/shared/css/lexer.js | 21 ++++++++++++++++++ devtools/shared/tests/unit/test_csslexer.js | 3 +-- .../css-parsing/invalid-url-handling.xhtml | 22 +++++++++---------- layout/style/nsCSSScanner.cpp | 22 ++++++++++++++++++- layout/style/test/test_csslexer.js | 3 +-- 5 files changed, 55 insertions(+), 16 deletions(-) diff --git a/devtools/shared/css/lexer.js b/devtools/shared/css/lexer.js index 9013b63ea5..192fa95b34 100644 --- a/devtools/shared/css/lexer.js +++ b/devtools/shared/css/lexer.js @@ -1067,6 +1067,7 @@ Scanner.prototype = { // aToken.mIdent may be "url" at this point; clear that out aToken.mIdent.length = 0; + let hasString = false; let ch = this.Peek(); // Do we have a string? if (ch == QUOTATION_MARK || ch == APOSTROPHE) { @@ -1075,6 +1076,7 @@ Scanner.prototype = { aToken.mType = eCSSToken_Bad_URL; return; } + hasString = true; } else { // Otherwise, this is the start of a non-quoted url (which may be empty). aToken.mSymbol = 0; @@ -1093,6 +1095,25 @@ Scanner.prototype = { } } else { aToken.mType = eCSSToken_Bad_URL; + if (!hasString) { + // Consume until before the next right parenthesis, which follows + // how is consumed in CSS Syntax 3 spec. + // Note that, we only do this when "url(" is not followed by a + // string, because in the spec, "url(" followed by a string is + // handled as a url function rather than a , so the + // rest of content before ")" should be consumed in balance, + // which will be done by the parser. + // The closing ")" is not consumed here. It is left to the parser + // so that the parser can handle both cases. + do { + if (IsVertSpace(ch)) { + this.AdvanceLine(); + } else { + this.Advance(); + } + ch = this.Peek(); + } while (ch >= 0 && ch != RIGHT_PARENTHESIS); + } } }, diff --git a/devtools/shared/tests/unit/test_csslexer.js b/devtools/shared/tests/unit/test_csslexer.js index 35855640be..b2dfdf5aa5 100644 --- a/devtools/shared/tests/unit/test_csslexer.js +++ b/devtools/shared/tests/unit/test_csslexer.js @@ -128,8 +128,7 @@ var LEX_TESTS = [ ["url:http://example.com"]], // In CSS Level 3, this is an ordinary URL, not a BAD_URL. ["url(http://example.com", ["url:http://example.com"]], - // See bug 1153981 to understand why this gets a SYMBOL token. - ["url(http://example.com @", ["bad_url:http://example.com", "symbol:@"]], + ["url(http://example.com @", ["bad_url:http://example.com"]], ["quo\\ting", ["ident:quoting"]], ["'bad string\n", ["bad_string:bad string", "whitespace"]], ["~=", ["includes"]], diff --git a/layout/reftests/css-parsing/invalid-url-handling.xhtml b/layout/reftests/css-parsing/invalid-url-handling.xhtml index da1709b01b..e6b85a81c9 100644 --- a/layout/reftests/css-parsing/invalid-url-handling.xhtml +++ b/layout/reftests/css-parsing/invalid-url-handling.xhtml @@ -22,17 +22,16 @@ #two { background-color: green; } - diff --git a/mobile/android/base/geckoview.ddf b/mobile/android/base/geckoview.ddf deleted file mode 100644 index 015a0d3e72..0000000000 --- a/mobile/android/base/geckoview.ddf +++ /dev/null @@ -1,75 +0,0 @@ -# This is a Classycle dependency definition file that asserts that the contents -# of the GeckoView library (Classycle set [lib]) is a dependency (but does not -# depend) on Fennec (Classycle set [main]). The additional Classycle set -# [middle] consists of classes referenced by GeckoView that probably should not -# be referenced. We want this middle set to shrink over time. - -show allResults - -[lib] = \ - org.mozilla.gecko.gfx.* \ - org.mozilla.gecko.mozglue.* \ - org.mozilla.gecko.sqlite.* \ - org.mozilla.gecko.util.* \ - org.mozilla.gecko.AndroidGamepadManager \ - org.mozilla.gecko.AppConstants \ - org.mozilla.gecko.BaseGeckoInterface \ - org.mozilla.gecko.ContextGetter \ - org.mozilla.gecko.CrashHandler \ - org.mozilla.gecko.EventDispatcher \ - org.mozilla.gecko.GeckoAccessibility \ - org.mozilla.gecko.GeckoAppShell \ - org.mozilla.gecko.GeckoBatteryManager \ - org.mozilla.gecko.GeckoEditable \ - org.mozilla.gecko.GeckoEditableClient \ - org.mozilla.gecko.GeckoEditableListener \ - org.mozilla.gecko.GeckoEvent \ - org.mozilla.gecko.GeckoInputConnection \ - org.mozilla.gecko.GeckoJavaSampler \ - org.mozilla.gecko.GeckoNetworkManager \ - org.mozilla.gecko.GeckoProfile \ - org.mozilla.gecko.GeckoScreenOrientation \ - org.mozilla.gecko.GeckoSharedPrefs \ - org.mozilla.gecko.GeckoThread \ - org.mozilla.gecko.GeckoView \ - org.mozilla.gecko.GlobalHistory \ - org.mozilla.gecko.InputMethods \ - org.mozilla.gecko.NSSBridge \ - org.mozilla.gecko.NotificationClient \ - org.mozilla.gecko.NotificationHandler \ - org.mozilla.gecko.PrefsHelper \ - org.mozilla.gecko.SysInfo \ - org.mozilla.gecko.TouchEventInterceptor \ - org.mozilla.gecko.ZoomConstraints - -[middle] = \ - org.mozilla.gecko.prompts.* \ - org.mozilla.gecko.FormAssistPopup \ - org.mozilla.gecko.GeckoActivity \ - org.mozilla.gecko.GeckoApp \ - org.mozilla.gecko.GeckoProfileDirectories \ - org.mozilla.gecko.GuestSession \ - org.mozilla.gecko.R \ - org.mozilla.gecko.Tab \ - org.mozilla.gecko.Tabs \ - org.mozilla.gecko.Telemetry \ - org.mozilla.gecko.TelemetryContract \ - org.mozilla.gecko.ThumbnailHelper \ - org.mozilla.gecko.db.BrowserDB \ - org.mozilla.gecko.db.LocalBrowserDB \ - org.mozilla.gecko.distribution.Distribution \ - org.mozilla.gecko.icons.* - -[main] = org.mozilla.gecko.* excluding [lib] [middle] - -check sets [lib] [middle] [main] - -# Bug 1107134: it appears that Classycle can be fooled if the Java -# compiler inlines a constant from [main] into [lib]. That is, [main] -# really does depend on [lib] but Classycle only sees the dependency -# with some javac versions. For now, disable the check. Yes, this -# processing is useless without this check. -# check [lib] directlyIndependentOf [main] - -# This fails; if this passed, GeckoView would be ready to extract from Fennec. -# check [lib] independentOf [middle] diff --git a/mobile/android/base/java/org/mozilla/gecko/ANRReporter.java b/mobile/android/base/java/org/mozilla/gecko/ANRReporter.java deleted file mode 100644 index 3c29edef35..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/ANRReporter.java +++ /dev/null @@ -1,596 +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; - -import java.io.BufferedOutputStream; -import java.io.BufferedReader; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.FileReader; -import java.io.IOException; -import java.io.InputStreamReader; -import java.io.OutputStream; -import java.io.Reader; -import java.util.Locale; -import java.util.UUID; -import java.util.regex.Pattern; - -import org.json.JSONObject; -import org.mozilla.gecko.annotation.WrapForJNI; -import org.mozilla.gecko.AppConstants.Versions; -import org.mozilla.gecko.util.ThreadUtils; - -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.os.Handler; -import android.os.Looper; -import android.util.Log; - -public final class ANRReporter extends BroadcastReceiver -{ - private static final boolean DEBUG = false; - private static final String LOGTAG = "GeckoANRReporter"; - - private static final String ANR_ACTION = "android.intent.action.ANR"; - // Number of lines to search traces.txt to decide whether it's a Gecko ANR - private static final int LINES_TO_IDENTIFY_TRACES = 10; - // ANRs may happen because of memory pressure, - // so don't use up too much memory here - // Size of buffer to hold one line of text - private static final int TRACES_LINE_SIZE = 100; - // Size of block to use when processing traces.txt - private static final int TRACES_BLOCK_SIZE = 2000; - private static final String TRACES_CHARSET = "utf-8"; - private static final String PING_CHARSET = "utf-8"; - - private static final ANRReporter sInstance = new ANRReporter(); - private static int sRegisteredCount; - private Handler mHandler; - private volatile boolean mPendingANR; - - @WrapForJNI - private static native boolean requestNativeStack(boolean unwind); - @WrapForJNI - private static native String getNativeStack(); - @WrapForJNI - private static native void releaseNativeStack(); - - public static void register(Context context) { - if (sRegisteredCount++ != 0) { - // Already registered - return; - } - sInstance.start(context); - } - - public static void unregister() { - if (sRegisteredCount == 0) { - Log.w(LOGTAG, "register/unregister mismatch"); - return; - } - if (--sRegisteredCount != 0) { - // Should still be registered - return; - } - sInstance.stop(); - } - - private void start(final Context context) { - - Thread receiverThread = new Thread(new Runnable() { - @Override - public void run() { - Looper.prepare(); - synchronized (ANRReporter.this) { - mHandler = new Handler(); - ANRReporter.this.notify(); - } - if (DEBUG) { - Log.d(LOGTAG, "registering receiver"); - } - context.registerReceiver(ANRReporter.this, - new IntentFilter(ANR_ACTION), - null, - mHandler); - Looper.loop(); - - if (DEBUG) { - Log.d(LOGTAG, "unregistering receiver"); - } - context.unregisterReceiver(ANRReporter.this); - mHandler = null; - } - }, LOGTAG); - - receiverThread.setDaemon(true); - receiverThread.start(); - } - - private void stop() { - synchronized (this) { - while (mHandler == null) { - try { - wait(1000); - if (mHandler == null) { - // We timed out; just give up. The process is probably - // quitting anyways, so we let the OS do the clean up - Log.w(LOGTAG, "timed out waiting for handler"); - return; - } - } catch (InterruptedException e) { - } - } - } - Looper looper = mHandler.getLooper(); - looper.quit(); - try { - looper.getThread().join(); - } catch (InterruptedException e) { - } - } - - private ANRReporter() { - } - - // Return the "traces.txt" file, or null if there is no such file - private static File getTracesFile() { - // Check most common location first. - File tracesFile = new File("/data/anr/traces.txt"); - if (tracesFile.isFile() && tracesFile.canRead()) { - return tracesFile; - } - - // Find the traces file name if we can. - try { - // getprop [prop-name [default-value]] - Process propProc = (new ProcessBuilder()) - .command("/system/bin/getprop", "dalvik.vm.stack-trace-file") - .redirectErrorStream(true) - .start(); - try { - BufferedReader buf = new BufferedReader( - new InputStreamReader(propProc.getInputStream()), TRACES_LINE_SIZE); - String propVal = buf.readLine(); - if (DEBUG) { - Log.d(LOGTAG, "getprop returned " + String.valueOf(propVal)); - } - // getprop can return empty string when the prop value is empty - // or prop is undefined, treat both cases the same way - if (propVal != null && propVal.length() != 0) { - tracesFile = new File(propVal); - if (tracesFile.isFile() && tracesFile.canRead()) { - return tracesFile; - } else if (DEBUG) { - Log.d(LOGTAG, "cannot access traces file"); - } - } else if (DEBUG) { - Log.d(LOGTAG, "empty getprop result"); - } - } finally { - propProc.destroy(); - } - } catch (IOException e) { - Log.w(LOGTAG, e); - } catch (ClassCastException e) { - Log.w(LOGTAG, e); // Bug 975436 - } - return null; - } - - private static File getPingFile() { - if (GeckoAppShell.getContext() == null) { - return null; - } - GeckoProfile profile = GeckoAppShell.getGeckoInterface().getProfile(); - if (profile == null) { - return null; - } - File profDir = profile.getDir(); - if (profDir == null) { - return null; - } - File pingDir = new File(profDir, "saved-telemetry-pings"); - pingDir.mkdirs(); - if (!(pingDir.exists() && pingDir.isDirectory())) { - return null; - } - return new File(pingDir, UUID.randomUUID().toString()); - } - - // Return true if the traces file corresponds to a Gecko ANR - private static boolean isGeckoTraces(String pkgName, File tracesFile) { - try { - final String END_OF_PACKAGE_NAME = "([^a-zA-Z0-9_]|$)"; - // Regex for finding our package name in the traces file - Pattern pkgPattern = Pattern.compile(Pattern.quote(pkgName) + END_OF_PACKAGE_NAME); - Pattern mangledPattern = null; - if (!AppConstants.MANGLED_ANDROID_PACKAGE_NAME.equals(pkgName)) { - mangledPattern = Pattern.compile(Pattern.quote( - AppConstants.MANGLED_ANDROID_PACKAGE_NAME) + END_OF_PACKAGE_NAME); - } - if (DEBUG) { - Log.d(LOGTAG, "trying to match package: " + pkgName); - } - BufferedReader traces = new BufferedReader( - new FileReader(tracesFile), TRACES_BLOCK_SIZE); - try { - for (int count = 0; count < LINES_TO_IDENTIFY_TRACES; count++) { - String line = traces.readLine(); - if (DEBUG) { - Log.d(LOGTAG, "identifying line: " + String.valueOf(line)); - } - if (line == null) { - if (DEBUG) { - Log.d(LOGTAG, "reached end of traces file"); - } - return false; - } - if (pkgPattern.matcher(line).find()) { - // traces.txt file contains our package - return true; - } - if (mangledPattern != null && mangledPattern.matcher(line).find()) { - // traces.txt file contains our alternate package - return true; - } - } - } finally { - traces.close(); - } - } catch (IOException e) { - // meh, can't even read from it right. just return false - } - return false; - } - - private static long getUptimeMins() { - - long uptimeMins = (new File("/proc/self/stat")).lastModified(); - if (uptimeMins != 0L) { - uptimeMins = (System.currentTimeMillis() - uptimeMins) / 1000L / 60L; - if (DEBUG) { - Log.d(LOGTAG, "uptime " + String.valueOf(uptimeMins)); - } - return uptimeMins; - } - if (DEBUG) { - Log.d(LOGTAG, "could not get uptime"); - } - return 0L; - } - - /* - a saved telemetry ping file consists of JSON in the following format, - { - "reason": "android-anr-report", - "slug": "", - "payload": - } - for Android ANR, our JSON payload should look like, - { - "ver": 1, - "simpleMeasurements": { - "uptime": - }, - "info": { - "reason": "android-anr-report", - "OS": "Android", - ... - }, - "androidANR": "...", - "androidLogcat": "..." - } - */ - - private static int writePingPayload(OutputStream ping, - String payload) throws IOException { - byte [] data = payload.getBytes(PING_CHARSET); - ping.write(data); - return data.length; - } - - private static void fillPingHeader(OutputStream ping, String slug) - throws IOException { - - // ping file header - byte [] data = ("{" + - "\"reason\":\"android-anr-report\"," + - "\"slug\":" + JSONObject.quote(slug) + "," + - "\"payload\":").getBytes(PING_CHARSET); - ping.write(data); - if (DEBUG) { - Log.d(LOGTAG, "wrote ping header, size = " + String.valueOf(data.length)); - } - - // payload start - int size = writePingPayload(ping, ("{" + - "\"ver\":1," + - "\"simpleMeasurements\":{" + - "\"uptime\":" + String.valueOf(getUptimeMins()) + - "}," + - "\"info\":{" + - "\"reason\":\"android-anr-report\"," + - "\"OS\":" + JSONObject.quote(SysInfo.getName()) + "," + - "\"version\":\"" + String.valueOf(SysInfo.getVersion()) + "\"," + - "\"appID\":" + JSONObject.quote(AppConstants.MOZ_APP_ID) + "," + - "\"appVersion\":" + JSONObject.quote(AppConstants.MOZ_APP_VERSION) + "," + - "\"appName\":" + JSONObject.quote(AppConstants.MOZ_APP_BASENAME) + "," + - "\"appBuildID\":" + JSONObject.quote(AppConstants.MOZ_APP_BUILDID) + "," + - "\"appUpdateChannel\":" + JSONObject.quote(AppConstants.MOZ_UPDATE_CHANNEL) + "," + - // Technically the platform build ID may be different, but we'll never know - "\"platformBuildID\":" + JSONObject.quote(AppConstants.MOZ_APP_BUILDID) + "," + - "\"locale\":" + JSONObject.quote(Locales.getLanguageTag(Locale.getDefault())) + "," + - "\"cpucount\":" + String.valueOf(SysInfo.getCPUCount()) + "," + - "\"memsize\":" + String.valueOf(SysInfo.getMemSize()) + "," + - "\"arch\":" + JSONObject.quote(SysInfo.getArchABI()) + "," + - "\"kernel_version\":" + JSONObject.quote(SysInfo.getKernelVersion()) + "," + - "\"device\":" + JSONObject.quote(SysInfo.getDevice()) + "," + - "\"manufacturer\":" + JSONObject.quote(SysInfo.getManufacturer()) + "," + - "\"hardware\":" + JSONObject.quote(SysInfo.getHardware()) + - "}," + - "\"androidANR\":\"")); - if (DEBUG) { - Log.d(LOGTAG, "wrote metadata, size = " + String.valueOf(size)); - } - - // We are at the start of ANR data - } - - // Block is a section of the larger input stream, and we want to find pattern within - // the stream. This is straightforward if the entire pattern is within one block; - // however, if the pattern spans across two blocks, we have to match both the start of - // the pattern in the first block and the end of the pattern in the second block. - // * If pattern is found in block, this method returns the index at the end of the - // found pattern, which must always be > 0. - // * If pattern is not found, it returns 0. - // * If the start of the pattern matches the end of the block, it returns a number - // < 0, which equals the negated value of how many characters in pattern are already - // matched; when processing the next block, this number is passed in through - // prevIndex, and the rest of the characters in pattern are matched against the - // start of this second block. The method returns value > 0 if the rest of the - // characters match, or 0 if they do not. - private static int getEndPatternIndex(String block, String pattern, int prevIndex) { - if (pattern == null || block.length() < pattern.length()) { - // Nothing to do - return 0; - } - if (prevIndex < 0) { - // Last block ended with a partial start; now match start of block to rest of pattern - if (block.startsWith(pattern.substring(-prevIndex, pattern.length()))) { - // Rest of pattern matches; return index at end of pattern - return pattern.length() + prevIndex; - } - // Not a match; continue with normal search - } - // Did not find pattern in last block; see if entire pattern is inside this block - int index = block.indexOf(pattern); - if (index >= 0) { - // Found pattern; return index at end of the pattern - return index + pattern.length(); - } - // Block does not contain the entire pattern, but see if the end of the block - // contains the start of pattern. To do that, we see if block ends with the - // first n-1 characters of pattern, the first n-2 characters of pattern, etc. - for (index = block.length() - pattern.length() + 1; index < block.length(); index++) { - // Using index as a start, see if the rest of block contains the start of pattern - if (block.charAt(index) == pattern.charAt(0) && - block.endsWith(pattern.substring(0, block.length() - index))) { - // Found partial match; return -(number of characters matched), - // i.e. -1 for 1 character matched, -2 for 2 characters matched, etc. - return index - block.length(); - } - } - return 0; - } - - // Copy the content of reader to ping; - // copying stops when endPattern is found in the input stream - private static int fillPingBlock(OutputStream ping, - Reader reader, String endPattern) - throws IOException { - - int total = 0; - int endIndex = 0; - char [] block = new char[TRACES_BLOCK_SIZE]; - for (int size = reader.read(block); size >= 0; size = reader.read(block)) { - String stringBlock = new String(block, 0, size); - endIndex = getEndPatternIndex(stringBlock, endPattern, endIndex); - if (endIndex > 0) { - // Found end pattern; clip the string - stringBlock = stringBlock.substring(0, endIndex); - } - String quoted = JSONObject.quote(stringBlock); - total += writePingPayload(ping, quoted.substring(1, quoted.length() - 1)); - if (endIndex > 0) { - // End pattern already found; return now - break; - } - } - return total; - } - - private static void fillLogcat(final OutputStream ping) { - if (Versions.preJB) { - // Logcat retrieval is not supported on pre-JB devices. - return; - } - - try { - // get the last 200 lines of logcat - Process proc = (new ProcessBuilder()) - .command("/system/bin/logcat", "-v", "threadtime", "-t", "200", "-d", "*:D") - .redirectErrorStream(true) - .start(); - try { - Reader procOut = new InputStreamReader(proc.getInputStream(), TRACES_CHARSET); - int size = fillPingBlock(ping, procOut, null); - if (DEBUG) { - Log.d(LOGTAG, "wrote logcat, size = " + String.valueOf(size)); - } - } finally { - proc.destroy(); - } - } catch (IOException e) { - // ignore because logcat is not essential - Log.w(LOGTAG, e); - } - } - - private static void fillPingFooter(OutputStream ping, - boolean haveNativeStack) - throws IOException { - - // We are at the end of ANR data - - int total = writePingPayload(ping, ("\"," + - "\"androidLogcat\":\"")); - fillLogcat(ping); - - if (haveNativeStack) { - total += writePingPayload(ping, ("\"," + - "\"androidNativeStack\":")); - - String nativeStack = String.valueOf(getNativeStack()); - int size = writePingPayload(ping, nativeStack); - if (DEBUG) { - Log.d(LOGTAG, "wrote native stack, size = " + String.valueOf(size)); - } - total += size + writePingPayload(ping, "}"); - } else { - total += writePingPayload(ping, "\"}"); - } - - byte [] data = ( - "}").getBytes(PING_CHARSET); - ping.write(data); - if (DEBUG) { - Log.d(LOGTAG, "wrote ping footer, size = " + String.valueOf(data.length + total)); - } - } - - private static void processTraces(Reader traces, File pingFile) { - - // Only get native stack if Gecko is running. - // Also, unwinding is memory intensive, so only unwind if we have enough memory. - final boolean haveNativeStack = - GeckoThread.isRunning() ? - requestNativeStack(/* unwind */ SysInfo.getMemSize() >= 640) : false; - - try { - OutputStream ping = new BufferedOutputStream( - new FileOutputStream(pingFile), TRACES_BLOCK_SIZE); - try { - fillPingHeader(ping, pingFile.getName()); - // Traces file has the format - // ----- pid xxx at xxx ----- - // Cmd line: org.mozilla.xxx - // * stack trace * - // ----- end xxx ----- - // ----- pid xxx at xxx ----- - // Cmd line: com.android.xxx - // * stack trace * - // ... - // If we end the stack dump at the first end marker, - // only Fennec stacks will be dumped - int size = fillPingBlock(ping, traces, "\n----- end"); - if (DEBUG) { - Log.d(LOGTAG, "wrote traces, size = " + String.valueOf(size)); - } - fillPingFooter(ping, haveNativeStack); - if (DEBUG) { - Log.d(LOGTAG, "finished creating ping file"); - } - return; - } finally { - ping.close(); - if (haveNativeStack) { - releaseNativeStack(); - } - } - } catch (IOException e) { - Log.w(LOGTAG, e); - } - // exception; delete ping file - if (pingFile.exists()) { - pingFile.delete(); - } - } - - private static void processTraces(File tracesFile, File pingFile) { - try { - Reader traces = new InputStreamReader( - new FileInputStream(tracesFile), TRACES_CHARSET); - try { - processTraces(traces, pingFile); - } finally { - traces.close(); - } - } catch (IOException e) { - Log.w(LOGTAG, e); - } - } - - @Override - public void onReceive(Context context, Intent intent) { - if (mPendingANR) { - // we already processed an ANR without getting unstuck; skip this one - if (DEBUG) { - Log.d(LOGTAG, "skipping duplicate ANR"); - } - return; - } - if (ThreadUtils.getUiHandler() != null) { - mPendingANR = true; - // detect when the main thread gets unstuck - ThreadUtils.postToUiThread(new Runnable() { - @Override - public void run() { - // okay to reset mPendingANR on main thread - mPendingANR = false; - if (DEBUG) { - Log.d(LOGTAG, "yay we got unstuck!"); - } - } - }); - } - if (DEBUG) { - Log.d(LOGTAG, "receiving " + String.valueOf(intent)); - } - if (!ANR_ACTION.equals(intent.getAction())) { - return; - } - - // make sure we have a good save location first - File pingFile = getPingFile(); - if (DEBUG) { - Log.d(LOGTAG, "using ping file: " + String.valueOf(pingFile)); - } - if (pingFile == null) { - return; - } - - File tracesFile = getTracesFile(); - if (DEBUG) { - Log.d(LOGTAG, "using traces file: " + String.valueOf(tracesFile)); - } - if (tracesFile == null) { - return; - } - - // We get ANR intents from all ANRs in the system, but we only want Gecko ANRs - if (!isGeckoTraces(context.getPackageName(), tracesFile)) { - if (DEBUG) { - Log.d(LOGTAG, "traces is not Gecko ANR"); - } - return; - } - Log.i(LOGTAG, "processing Gecko ANR"); - processTraces(tracesFile, pingFile); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/AboutPages.java b/mobile/android/base/java/org/mozilla/gecko/AboutPages.java deleted file mode 100644 index 705d700af1..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/AboutPages.java +++ /dev/null @@ -1,117 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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; - -import org.mozilla.gecko.annotation.RobocopTarget; -import org.mozilla.gecko.home.HomeConfig; -import org.mozilla.gecko.home.HomeConfig.PanelType; -import org.mozilla.gecko.util.StringUtils; - -public class AboutPages { - // All of our special pages. - public static final String ACCOUNTS = "about:accounts"; - public static final String ADDONS = "about:addons"; - public static final String CONFIG = "about:config"; - public static final String DOWNLOADS = "about:downloads"; - public static final String FIREFOX = "about:firefox"; - public static final String HEALTHREPORT = "about:healthreport"; - public static final String HOME = "about:home"; - public static final String LOGINS = "about:logins"; - public static final String PRIVATEBROWSING = "about:privatebrowsing"; - public static final String READER = "about:reader"; - public static final String UPDATER = "about:"; - - public static final String URL_FILTER = "about:%"; - - public static final String PANEL_PARAM = "panel"; - - public static final boolean isAboutPage(final String url) { - return url != null && url.startsWith("about:"); - } - - public static final boolean isTitlelessAboutPage(final String url) { - return isAboutHome(url) || - PRIVATEBROWSING.equals(url); - } - - public static final boolean isAboutHome(final String url) { - if (url == null || !url.startsWith(HOME)) { - return false; - } - // We sometimes append a parameter to "about:home" to specify which page to - // show when we open the home pager. Discard this parameter when checking - // whether or not this URL is "about:home". - return HOME.equals(url.split("\\?")[0]); - } - - public static final String getPanelIdFromAboutHomeUrl(String aboutHomeUrl) { - return StringUtils.getQueryParameter(aboutHomeUrl, PANEL_PARAM); - } - - public static boolean isAboutReader(final String url) { - return isAboutPage(READER, url); - } - - public static boolean isAboutConfig(final String url) { - return isAboutPage(CONFIG, url); - } - - public static boolean isAboutAddons(final String url) { - return isAboutPage(ADDONS, url); - } - - public static boolean isAboutPrivateBrowsing(final String url) { - return isAboutPage(PRIVATEBROWSING, url); - } - - public static boolean isAboutPage(String page, String url) { - return url != null && url.toLowerCase().startsWith(page); - - } - - public static final String[] DEFAULT_ICON_PAGES = new String[] { - HOME, - ACCOUNTS, - ADDONS, - CONFIG, - DOWNLOADS, - FIREFOX, - HEALTHREPORT, - UPDATER - }; - - public static boolean isBuiltinIconPage(final String url) { - if (url == null || - !url.startsWith("about:")) { - return false; - } - - // about:home uses a separate search built-in icon. - if (isAboutHome(url)) { - return true; - } - - // TODO: it'd be quicker to not compare the "about:" part every time. - for (int i = 0; i < DEFAULT_ICON_PAGES.length; ++i) { - if (DEFAULT_ICON_PAGES[i].equals(url)) { - return true; - } - } - return false; - } - - /** - * Get a URL that navigates to the specified built-in Home Panel. - * - * @param panelType to navigate to. - * @return URL. - * @throws IllegalArgumentException if the built-in panel type is not a built-in panel. - */ - @RobocopTarget - public static String getURLForBuiltinPanelType(PanelType panelType) throws IllegalArgumentException { - return HOME + "?panel=" + HomeConfig.getIdForBuiltinPanelType(panelType); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/AccountsHelper.java b/mobile/android/base/java/org/mozilla/gecko/AccountsHelper.java deleted file mode 100644 index 5892c16b61..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/AccountsHelper.java +++ /dev/null @@ -1,318 +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; - -import android.accounts.Account; -import android.accounts.AccountManager; -import android.accounts.AccountManagerCallback; -import android.accounts.AccountManagerFuture; -import android.accounts.AuthenticatorException; -import android.accounts.OperationCanceledException; -import android.content.Context; -import android.content.Intent; -import android.util.Log; - -import org.json.JSONException; -import org.json.JSONObject; -import org.mozilla.gecko.background.fxa.FxAccountUtils; -import org.mozilla.gecko.fxa.FirefoxAccounts; -import org.mozilla.gecko.fxa.FxAccountConstants; -import org.mozilla.gecko.fxa.FxAccountDeviceRegistrator; -import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount; -import org.mozilla.gecko.fxa.login.Engaged; -import org.mozilla.gecko.fxa.login.State; -import org.mozilla.gecko.restrictions.Restrictable; -import org.mozilla.gecko.restrictions.Restrictions; -import org.mozilla.gecko.sync.SyncConfiguration; -import org.mozilla.gecko.sync.Utils; -import org.mozilla.gecko.util.EventCallback; -import org.mozilla.gecko.util.NativeEventListener; -import org.mozilla.gecko.util.NativeJSObject; - -import java.io.IOException; -import java.io.UnsupportedEncodingException; -import java.net.URISyntaxException; -import java.security.GeneralSecurityException; -import java.util.HashMap; -import java.util.Map; - -/** - * Helper class to manage Android Accounts corresponding to Firefox Accounts. - */ -public class AccountsHelper implements NativeEventListener { - public static final String LOGTAG = "GeckoAccounts"; - - protected final Context mContext; - protected final GeckoProfile mProfile; - - public AccountsHelper(Context context, GeckoProfile profile) { - mContext = context; - mProfile = profile; - - EventDispatcher dispatcher = GeckoApp.getEventDispatcher(); - if (dispatcher == null) { - Log.e(LOGTAG, "Gecko event dispatcher must not be null", new RuntimeException()); - return; - } - dispatcher.registerGeckoThreadListener(this, - "Accounts:CreateFirefoxAccountFromJSON", - "Accounts:UpdateFirefoxAccountFromJSON", - "Accounts:Create", - "Accounts:DeleteFirefoxAccount", - "Accounts:Exist", - "Accounts:ProfileUpdated", - "Accounts:ShowSyncPreferences"); - } - - public synchronized void uninit() { - EventDispatcher dispatcher = GeckoApp.getEventDispatcher(); - if (dispatcher == null) { - Log.e(LOGTAG, "Gecko event dispatcher must not be null", new RuntimeException()); - return; - } - dispatcher.unregisterGeckoThreadListener(this, - "Accounts:CreateFirefoxAccountFromJSON", - "Accounts:UpdateFirefoxAccountFromJSON", - "Accounts:Create", - "Accounts:DeleteFirefoxAccount", - "Accounts:Exist", - "Accounts:ProfileUpdated", - "Accounts:ShowSyncPreferences"); - } - - @Override - public void handleMessage(String event, NativeJSObject message, final EventCallback callback) { - if (!Restrictions.isAllowed(mContext, Restrictable.MODIFY_ACCOUNTS)) { - // We register for messages in all contexts; we drop, with a log and an error to JavaScript, - // when the profile is restricted. It's better to return errors than silently ignore messages. - Log.e(LOGTAG, "Profile is not allowed to modify accounts! Ignoring event: " + event); - if (callback != null) { - callback.sendError("Profile is not allowed to modify accounts!"); - } - return; - } - - if ("Accounts:CreateFirefoxAccountFromJSON".equals(event)) { - // As we are about to create a new account, let's ensure our in-memory accounts cache - // is empty so that there are no undesired side-effects. - AndroidFxAccount.invalidateCaches(); - - AndroidFxAccount fxAccount = null; - try { - final NativeJSObject json = message.getObject("json"); - final String email = json.getString("email"); - final String uid = json.getString("uid"); - final boolean verified = json.optBoolean("verified", false); - final byte[] unwrapkB = Utils.hex2Byte(json.getString("unwrapBKey")); - final byte[] sessionToken = Utils.hex2Byte(json.getString("sessionToken")); - final byte[] keyFetchToken = Utils.hex2Byte(json.getString("keyFetchToken")); - final String authServerEndpoint = - json.optString("authServerEndpoint", FxAccountConstants.DEFAULT_AUTH_SERVER_ENDPOINT); - final String tokenServerEndpoint = - json.optString("tokenServerEndpoint", FxAccountConstants.DEFAULT_TOKEN_SERVER_ENDPOINT); - final String profileServerEndpoint = - json.optString("profileServerEndpoint", FxAccountConstants.DEFAULT_PROFILE_SERVER_ENDPOINT); - // TODO: handle choose what to Sync. - State state = new Engaged(email, uid, verified, unwrapkB, sessionToken, keyFetchToken); - fxAccount = AndroidFxAccount.addAndroidAccount(mContext, - email, - mProfile.getName(), - authServerEndpoint, - tokenServerEndpoint, - profileServerEndpoint, - state, - AndroidFxAccount.DEFAULT_AUTHORITIES_TO_SYNC_AUTOMATICALLY_MAP); - - final String[] declinedSyncEngines = json.optStringArray("declinedSyncEngines", null); - if (declinedSyncEngines != null) { - Log.i(LOGTAG, "User has selected engines; storing to prefs."); - final Map selectedEngines = new HashMap(); - for (String enabledSyncEngine : SyncConfiguration.validEngineNames()) { - selectedEngines.put(enabledSyncEngine, true); - } - for (String declinedSyncEngine : declinedSyncEngines) { - selectedEngines.put(declinedSyncEngine, false); - } - // The "forms" engine has the same state as the "history" engine. - selectedEngines.put("forms", selectedEngines.get("history")); - FxAccountUtils.pii(LOGTAG, "User selected engines: " + selectedEngines.toString()); - try { - SyncConfiguration.storeSelectedEnginesToPrefs(fxAccount.getSyncPrefs(), selectedEngines); - } catch (UnsupportedEncodingException | GeneralSecurityException e) { - Log.e(LOGTAG, "Got exception storing selected engines; ignoring.", e); - } - } - } catch (URISyntaxException | GeneralSecurityException | UnsupportedEncodingException e) { - Log.w(LOGTAG, "Got exception creating Firefox Account from JSON; ignoring.", e); - if (callback != null) { - callback.sendError("Could not create Firefox Account from JSON: " + e.toString()); - return; - } - } - if (callback != null) { - callback.sendSuccess(fxAccount != null); - } - - } else if ("Accounts:UpdateFirefoxAccountFromJSON".equals(event)) { - // We might be significantly changing state of the account; let's ensure our in-memory - // accounts cache is empty so that there are no undesired side-effects. - AndroidFxAccount.invalidateCaches(); - - try { - final Account account = FirefoxAccounts.getFirefoxAccount(mContext); - if (account == null) { - if (callback != null) { - callback.sendError("Could not update Firefox Account since none exists"); - } - return; - } - - final NativeJSObject json = message.getObject("json"); - final String email = json.getString("email"); - final String uid = json.getString("uid"); - - // Protect against cross-connecting accounts. - if (account.name == null || !account.name.equals(email)) { - final String errorMessage = "Cannot update Firefox Account from JSON: datum has different email address!"; - Log.e(LOGTAG, errorMessage); - if (callback != null) { - callback.sendError(errorMessage); - } - return; - } - - final boolean verified = json.optBoolean("verified", false); - final byte[] unwrapkB = Utils.hex2Byte(json.getString("unwrapBKey")); - final byte[] sessionToken = Utils.hex2Byte(json.getString("sessionToken")); - final byte[] keyFetchToken = Utils.hex2Byte(json.getString("keyFetchToken")); - final State state = new Engaged(email, uid, verified, unwrapkB, sessionToken, keyFetchToken); - - final AndroidFxAccount fxAccount = new AndroidFxAccount(mContext, account); - fxAccount.setState(state); - - if (callback != null) { - callback.sendSuccess(true); - } - } catch (NativeJSObject.InvalidPropertyException e) { - Log.w(LOGTAG, "Got exception updating Firefox Account from JSON; ignoring.", e); - if (callback != null) { - callback.sendError("Could not update Firefox Account from JSON: " + e.toString()); - return; - } - } - - } else if ("Accounts:Create".equals(event)) { - // Do exactly the same thing as if you tapped 'Sync' in Settings. - final Intent intent = new Intent(FxAccountConstants.ACTION_FXA_GET_STARTED); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - final NativeJSObject extras = message.optObject("extras", null); - if (extras != null) { - intent.putExtra("extras", extras.toString()); - } - mContext.startActivity(intent); - - } else if ("Accounts:DeleteFirefoxAccount".equals(event)) { - try { - final Account account = FirefoxAccounts.getFirefoxAccount(mContext); - if (account == null) { - Log.w(LOGTAG, "Could not delete Firefox Account since none exists!"); - if (callback != null) { - callback.sendError("Could not delete Firefox Account since none exists"); - } - return; - } - - final AccountManagerCallback accountManagerCallback = new AccountManagerCallback() { - @Override - public void run(AccountManagerFuture future) { - try { - final boolean result = future.getResult(); - Log.i(LOGTAG, "Account named like " + Utils.obfuscateEmail(account.name) + " removed: " + result); - if (callback != null) { - callback.sendSuccess(result); - } - } catch (OperationCanceledException | IOException | AuthenticatorException e) { - if (callback != null) { - callback.sendError("Could not delete Firefox Account: " + e.toString()); - } - } - } - }; - - AccountManager.get(mContext).removeAccount(account, accountManagerCallback, null); - } catch (Exception e) { - Log.w(LOGTAG, "Got exception updating Firefox Account from JSON; ignoring.", e); - if (callback != null) { - callback.sendError("Could not update Firefox Account from JSON: " + e.toString()); - return; - } - } - - } else if ("Accounts:Exist".equals(event)) { - if (callback == null) { - Log.w(LOGTAG, "Accounts:Exist requires a callback"); - return; - } - - final String kind = message.optString("kind", null); - final JSONObject response = new JSONObject(); - - try { - if ("any".equals(kind)) { - response.put("exists", FirefoxAccounts.firefoxAccountsExist(mContext)); - callback.sendSuccess(response); - } else if ("fxa".equals(kind)) { - final Account account = FirefoxAccounts.getFirefoxAccount(mContext); - response.put("exists", account != null); - if (account != null) { - response.put("email", account.name); - // We should always be able to extract the server endpoints. - final AndroidFxAccount fxAccount = new AndroidFxAccount(mContext, account); - response.put("authServerEndpoint", fxAccount.getAccountServerURI()); - response.put("profileServerEndpoint", fxAccount.getProfileServerURI()); - response.put("tokenServerEndpoint", fxAccount.getTokenServerURI()); - try { - // It is possible for the state fetch to fail and us to not be able to provide a UID. - // Long term, the UID (and verification flag) will be attached to the Android account - // user data and not the internal state representation. - final State state = fxAccount.getState(); - response.put("uid", state.uid); - } catch (Exception e) { - Log.w(LOGTAG, "Got exception extracting account UID; ignoring.", e); - } - } - - callback.sendSuccess(response); - } else { - callback.sendError("Could not query account existence: unknown kind."); - } - } catch (JSONException e) { - Log.w(LOGTAG, "Got exception querying account existence; ignoring.", e); - callback.sendError("Could not query account existence: " + e.toString()); - return; - } - } else if ("Accounts:ProfileUpdated".equals(event)) { - final Account account = FirefoxAccounts.getFirefoxAccount(mContext); - if (account == null) { - Log.w(LOGTAG, "Can't change profile of non-existent Firefox Account!; ignored"); - return; - } - final AndroidFxAccount androidFxAccount = new AndroidFxAccount(mContext, account); - androidFxAccount.fetchProfileJSON(); - } else if ("Accounts:ShowSyncPreferences".equals(event)) { - final Account account = FirefoxAccounts.getFirefoxAccount(mContext); - if (account == null) { - Log.w(LOGTAG, "Can't change show Sync preferences of non-existent Firefox Account!; ignored"); - return; - } - // We don't necessarily have an Activity context here, so we always start in a new task. - final Intent intent = new Intent(FxAccountConstants.ACTION_FXA_STATUS); - intent.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - mContext.startActivity(intent); - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/ActionBarTextSelection.java b/mobile/android/base/java/org/mozilla/gecko/ActionBarTextSelection.java deleted file mode 100644 index 7f2eb219e6..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/ActionBarTextSelection.java +++ /dev/null @@ -1,256 +0,0 @@ -/* 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; - -import org.mozilla.gecko.menu.GeckoMenu; -import org.mozilla.gecko.menu.GeckoMenuItem; -import org.mozilla.gecko.util.ResourceDrawableUtils; -import org.mozilla.gecko.text.TextSelection; -import org.mozilla.gecko.util.GeckoEventListener; -import org.mozilla.gecko.util.ThreadUtils; -import org.mozilla.gecko.ActionModeCompat.Callback; - -import android.content.Context; -import android.graphics.drawable.Drawable; -import android.view.MenuItem; - -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - -import java.util.Timer; -import java.util.TimerTask; - -import android.util.Log; - -class ActionBarTextSelection implements TextSelection, GeckoEventListener { - private static final String LOGTAG = "GeckoTextSelection"; - private static final int SHUTDOWN_DELAY_MS = 250; - - private final Context context; - - private boolean mDraggingHandles; - - private String selectionID; // Unique ID provided for each selection action. - - private String mCurrentItems; - - private TextSelectionActionModeCallback mCallback; - - // These timers are used to avoid flicker caused by selection handles showing/hiding quickly. - // For instance when moving between single handle caret mode and two handle selection mode. - private final Timer mActionModeTimer = new Timer("actionMode"); - private class ActionModeTimerTask extends TimerTask { - @Override - public void run() { - ThreadUtils.postToUiThread(new Runnable() { - @Override - public void run() { - endActionMode(); - } - }); - } - }; - private ActionModeTimerTask mActionModeTimerTask; - - ActionBarTextSelection(Context context) { - this.context = context; - } - - @Override - public void create() { - // Only register listeners if we have valid start/middle/end handles - if (context == null) { - Log.e(LOGTAG, "Failed to initialize text selection because at least one context is null"); - } else { - GeckoApp.getEventDispatcher().registerGeckoThreadListener(this, - "TextSelection:ActionbarInit", - "TextSelection:ActionbarStatus", - "TextSelection:ActionbarUninit", - "TextSelection:Update"); - } - } - - @Override - public boolean dismiss() { - // We do not call endActionMode() here because this is already handled by the activity. - return false; - } - - @Override - public void destroy() { - if (context == null) { - Log.e(LOGTAG, "Do not unregister TextSelection:* listeners since context is null"); - } else { - GeckoApp.getEventDispatcher().unregisterGeckoThreadListener(this, - "TextSelection:ActionbarInit", - "TextSelection:ActionbarStatus", - "TextSelection:ActionbarUninit", - "TextSelection:Update"); - } - } - - @Override - public void handleMessage(final String event, final JSONObject message) { - ThreadUtils.postToUiThread(new Runnable() { - @Override - public void run() { - try { - if (event.equals("TextSelection:Update")) { - if (mActionModeTimerTask != null) - mActionModeTimerTask.cancel(); - showActionMode(message.getJSONArray("actions")); - } else if (event.equals("TextSelection:ActionbarInit")) { - // Init / Open the action bar. Note the current selectionID, - // cancel any pending actionBar close. - Telemetry.sendUIEvent(TelemetryContract.Event.SHOW, - TelemetryContract.Method.CONTENT, "text_selection"); - - selectionID = message.getString("selectionID"); - mCurrentItems = null; - if (mActionModeTimerTask != null) { - mActionModeTimerTask.cancel(); - } - - } else if (event.equals("TextSelection:ActionbarStatus")) { - // Ensure async updates from SearchService for example are valid. - if (selectionID != message.optString("selectionID")) { - return; - } - - // Update the actionBar actions as provided by Gecko. - showActionMode(message.getJSONArray("actions")); - - } else if (event.equals("TextSelection:ActionbarUninit")) { - // Uninit the actionbar. Schedule a cancellable close - // action to avoid UI jank. (During SelectionAll for ex). - mCurrentItems = null; - mActionModeTimerTask = new ActionModeTimerTask(); - mActionModeTimer.schedule(mActionModeTimerTask, SHUTDOWN_DELAY_MS); - } - - } catch (JSONException e) { - Log.e(LOGTAG, "JSON exception", e); - } - } - }); - } - - private void showActionMode(final JSONArray items) { - String itemsString = items.toString(); - if (itemsString.equals(mCurrentItems)) { - return; - } - mCurrentItems = itemsString; - - if (mCallback != null) { - mCallback.updateItems(items); - return; - } - - if (context instanceof ActionModeCompat.Presenter) { - final ActionModeCompat.Presenter presenter = (ActionModeCompat.Presenter) context; - mCallback = new TextSelectionActionModeCallback(items); - presenter.startActionModeCompat(mCallback); - mCallback.animateIn(); - } - } - - private void endActionMode() { - if (context instanceof ActionModeCompat.Presenter) { - final ActionModeCompat.Presenter presenter = (ActionModeCompat.Presenter) context; - presenter.endActionModeCompat(); - } - mCurrentItems = null; - } - - private class TextSelectionActionModeCallback implements Callback { - private JSONArray mItems; - private ActionModeCompat mActionMode; - - public TextSelectionActionModeCallback(JSONArray items) { - mItems = items; - } - - public void updateItems(JSONArray items) { - mItems = items; - if (mActionMode != null) { - mActionMode.invalidate(); - } - } - - public void animateIn() { - if (mActionMode != null) { - mActionMode.animateIn(); - } - } - - @Override - public boolean onPrepareActionMode(final ActionModeCompat mode, final GeckoMenu menu) { - // Android would normally expect us to only update the state of menu items here - // To make the js-java interaction a bit simpler, we just wipe out the menu here and recreate all - // the javascript menu items in onPrepare instead. This will be called any time invalidate() is called on the - // action mode. - menu.clear(); - - int length = mItems.length(); - for (int i = 0; i < length; i++) { - try { - final JSONObject obj = mItems.getJSONObject(i); - final GeckoMenuItem menuitem = (GeckoMenuItem) menu.add(0, i, 0, obj.optString("label")); - final int actionEnum = obj.optBoolean("showAsAction") ? GeckoMenuItem.SHOW_AS_ACTION_ALWAYS : GeckoMenuItem.SHOW_AS_ACTION_NEVER; - menuitem.setShowAsAction(actionEnum, R.attr.menuItemActionModeStyle); - - final String iconString = obj.optString("icon"); - ResourceDrawableUtils.getDrawable(context, iconString, new ResourceDrawableUtils.BitmapLoader() { - @Override - public void onBitmapFound(Drawable d) { - if (d != null) { - menuitem.setIcon(d); - } - } - }); - } catch (Exception ex) { - Log.i(LOGTAG, "Exception building menu", ex); - } - } - return true; - } - - @Override - public boolean onCreateActionMode(ActionModeCompat mode, GeckoMenu unused) { - mActionMode = mode; - return true; - } - - @Override - public boolean onActionItemClicked(ActionModeCompat mode, MenuItem item) { - try { - final JSONObject obj = mItems.getJSONObject(item.getItemId()); - GeckoAppShell.notifyObservers("TextSelection:Action", obj.optString("id")); - return true; - } catch (Exception ex) { - Log.i(LOGTAG, "Exception calling action", ex); - } - return false; - } - - // Called when the user exits the action mode - @Override - public void onDestroyActionMode(ActionModeCompat mode) { - mActionMode = null; - mCallback = null; - final JSONObject args = new JSONObject(); - try { - args.put("selectionID", selectionID); - } catch (JSONException e) { - Log.e(LOGTAG, "Error building JSON arguments for TextSelection:End", e); - return; - } - - GeckoAppShell.notifyObservers("TextSelection:End", args.toString()); - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/ActionModeCompat.java b/mobile/android/base/java/org/mozilla/gecko/ActionModeCompat.java deleted file mode 100644 index 709c0056fd..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/ActionModeCompat.java +++ /dev/null @@ -1,135 +0,0 @@ -/* 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; - -import org.mozilla.gecko.menu.GeckoMenu; -import org.mozilla.gecko.menu.GeckoMenuItem; -import org.mozilla.gecko.widget.GeckoPopupMenu; - -import android.view.Gravity; -import android.view.Menu; -import android.view.MenuItem; -import android.view.View; -import android.widget.Toast; - -class ActionModeCompat implements GeckoPopupMenu.OnMenuItemClickListener, - GeckoPopupMenu.OnMenuItemLongClickListener, - View.OnClickListener { - private final String LOGTAG = "GeckoActionModeCompat"; - - private final Callback mCallback; - private final ActionModeCompatView mView; - private final Presenter mPresenter; - - /* A set of callbacks to be called during this ActionMode's lifecycle. These will control the - * creation, interaction with, and destruction of menuitems for the view */ - public static interface Callback { - /* Called when action mode is first created. Implementors should use this to inflate menu resources. */ - public boolean onCreateActionMode(ActionModeCompat mode, GeckoMenu menu); - - /* Called to refresh an action mode's action menu. Called whenever the mode is invalidated. Implementors - * should use this to enable/disable/show/hide menu items. */ - public boolean onPrepareActionMode(ActionModeCompat mode, GeckoMenu menu); - - /* Called to report a user click on an action button. */ - public boolean onActionItemClicked(ActionModeCompat mode, MenuItem item); - - /* Called when an action mode is about to be exited and destroyed. */ - public void onDestroyActionMode(ActionModeCompat mode); - } - - /* Presenters handle the actual showing/hiding of the action mode UI in the app. Its their responsibility - * to create an action mode, and assign it Callbacks and ActionModeCompatView's. */ - public static interface Presenter { - /* Called when an action mode should be shown */ - public void startActionModeCompat(final Callback callback); - - /* Called when whatever action mode is showing should be hidden */ - public void endActionModeCompat(); - } - - public ActionModeCompat(Presenter presenter, Callback callback, ActionModeCompatView view) { - mPresenter = presenter; - mCallback = callback; - - mView = view; - mView.initForMode(this); - } - - public void finish() { - // Clearing the menu will also clear the ActionItemBar - final GeckoMenu menu = mView.getMenu(); - menu.clear(); - menu.close(); - - if (mCallback != null) { - mCallback.onDestroyActionMode(this); - } - } - - public CharSequence getTitle() { - return mView.getTitle(); - } - - public void setTitle(CharSequence title) { - mView.setTitle(title); - } - - public void setTitle(int resId) { - mView.setTitle(resId); - } - - public GeckoMenu getMenu() { - return mView.getMenu(); - } - - public void invalidate() { - if (mCallback != null) { - mCallback.onPrepareActionMode(this, mView.getMenu()); - } - mView.invalidate(); - } - - public void animateIn() { - mView.animateIn(); - } - - /* GeckoPopupMenu.OnMenuItemClickListener */ - @Override - public boolean onMenuItemClick(MenuItem item) { - if (mCallback != null) { - return mCallback.onActionItemClicked(this, item); - } - return false; - } - - /* GeckoPopupMenu.onMenuItemLongClickListener */ - @Override - public boolean onMenuItemLongClick(MenuItem item) { - showTooltip((GeckoMenuItem) item); - return true; - } - - /* View.OnClickListener*/ - @Override - public void onClick(View v) { - mPresenter.endActionModeCompat(); - } - - private void showTooltip(GeckoMenuItem item) { - // Computes the tooltip toast screen position (shown when long-tapping the menu item) with regards to the - // menu item's position (i.e below the item and slightly to the left) - int[] location = new int[2]; - final View view = item.getActionView(); - view.getLocationOnScreen(location); - - int xOffset = location[0] - view.getWidth(); - int yOffset = location[1] + view.getHeight() / 2; - - Toast toast = Toast.makeText(view.getContext(), item.getTitle(), Toast.LENGTH_SHORT); - toast.setGravity(Gravity.TOP | Gravity.LEFT, xOffset, yOffset); - toast.show(); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/ActionModeCompatView.java b/mobile/android/base/java/org/mozilla/gecko/ActionModeCompatView.java deleted file mode 100644 index c9021b7105..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/ActionModeCompatView.java +++ /dev/null @@ -1,202 +0,0 @@ -/* 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; - -import android.content.res.TypedArray; -import android.graphics.Canvas; -import android.graphics.Paint; -import org.mozilla.gecko.animation.AnimationUtils; -import org.mozilla.gecko.menu.GeckoMenu; -import org.mozilla.gecko.widget.GeckoPopupMenu; - -import android.content.Context; -import android.util.AttributeSet; -import android.view.LayoutInflater; -import android.view.Menu; -import android.view.View; -import android.view.ViewGroup; -import android.view.animation.Animation; -import android.view.animation.ScaleAnimation; -import android.view.animation.TranslateAnimation; -import android.widget.Button; -import android.widget.ImageButton; -import android.widget.LinearLayout; - -class ActionModeCompatView extends LinearLayout implements GeckoMenu.ActionItemBarPresenter { - private final String LOGTAG = "GeckoActionModeCompatPresenter"; - - private static final int SPEC = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED); - - private Button mTitleView; - private ImageButton mMenuButton; - private ViewGroup mActionButtonBar; - private GeckoPopupMenu mPopupMenu; - - // Maximum number of items to show as actions - private static final int MAX_ACTION_ITEMS = 4; - - private int mActionButtonsWidth; - - private Paint mBottomDividerPaint; - private int mBottomDividerOffset; - - public ActionModeCompatView(Context context, AttributeSet attrs) { - super(context, attrs); - init(context, attrs, 0); - } - - public ActionModeCompatView(Context context, AttributeSet attrs, int style) { - super(context, attrs, style); - init(context, attrs, style); - } - - public void init(final Context context, final AttributeSet attrs, final int defStyle) { - LayoutInflater.from(context).inflate(R.layout.actionbar, this); - - mTitleView = (Button) findViewById(R.id.actionmode_title); - mMenuButton = (ImageButton) findViewById(R.id.actionbar_menu); - mActionButtonBar = (ViewGroup) findViewById(R.id.actionbar_buttons); - - mPopupMenu = new GeckoPopupMenu(getContext(), mMenuButton); - mPopupMenu.getMenu().setActionItemBarPresenter(this); - - mMenuButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - openMenu(); - } - }); - - // The built-in action bar uses colorAccent for the divider so we duplicate that here. - final TypedArray arr = context.obtainStyledAttributes(attrs, new int[] { R.attr.colorAccent }, defStyle, 0); - final int bottomDividerColor = arr.getColor(0, 0); - arr.recycle(); - - mBottomDividerPaint = new Paint(); - mBottomDividerPaint.setColor(bottomDividerColor); - mBottomDividerOffset = getResources().getDimensionPixelSize(R.dimen.action_bar_divider_height); - } - - public void initForMode(final ActionModeCompat mode) { - mTitleView.setOnClickListener(mode); - mPopupMenu.setOnMenuItemClickListener(mode); - mPopupMenu.setOnMenuItemLongClickListener(mode); - } - - public CharSequence getTitle() { - return mTitleView.getText(); - } - - public void setTitle(CharSequence title) { - mTitleView.setText(title); - } - - public void setTitle(int resId) { - mTitleView.setText(resId); - } - - public GeckoMenu getMenu() { - return mPopupMenu.getMenu(); - } - - @Override - public void invalidate() { - // onFinishInflate may not have been called yet on some versions of Android - if (mPopupMenu != null && mMenuButton != null) { - mMenuButton.setVisibility(mPopupMenu.getMenu().hasVisibleItems() ? View.VISIBLE : View.GONE); - } - super.invalidate(); - } - - /* GeckoMenu.ActionItemBarPresenter */ - @Override - public boolean addActionItem(View actionItem) { - final int count = mActionButtonBar.getChildCount(); - if (count >= MAX_ACTION_ITEMS) { - return false; - } - - int maxWidth = mActionButtonBar.getMeasuredWidth(); - if (maxWidth == 0) { - mActionButtonBar.measure(SPEC, SPEC); - maxWidth = mActionButtonBar.getMeasuredWidth(); - } - - // If the menu button is already visible, no need to account for it - if (mMenuButton.getVisibility() == View.GONE) { - // Since we don't know how many items will be added, we always reserve space for the overflow menu - mMenuButton.measure(SPEC, SPEC); - maxWidth -= mMenuButton.getMeasuredWidth(); - } - - if (mActionButtonsWidth <= 0) { - mActionButtonsWidth = 0; - - // Loop over child views, measure them, and add their width to the taken width - for (int i = 0; i < count; i++) { - View v = mActionButtonBar.getChildAt(i); - v.measure(SPEC, SPEC); - mActionButtonsWidth += v.getMeasuredWidth(); - } - } - - actionItem.measure(SPEC, SPEC); - int w = actionItem.getMeasuredWidth(); - if (mActionButtonsWidth + w < maxWidth) { - // We cache the new width of our children. - mActionButtonsWidth += w; - mActionButtonBar.addView(actionItem); - return true; - } - - return false; - } - - /* GeckoMenu.ActionItemBarPresenter */ - @Override - public void removeActionItem(View actionItem) { - actionItem.measure(SPEC, SPEC); - mActionButtonsWidth -= actionItem.getMeasuredWidth(); - mActionButtonBar.removeView(actionItem); - } - - public void openMenu() { - mPopupMenu.openMenu(); - } - - public void closeMenu() { - mPopupMenu.dismiss(); - } - - public void animateIn() { - long duration = AnimationUtils.getShortDuration(getContext()); - TranslateAnimation t = new TranslateAnimation(Animation.RELATIVE_TO_SELF, -0.5f, Animation.RELATIVE_TO_SELF, 0f, - Animation.RELATIVE_TO_SELF, 0f, Animation.RELATIVE_TO_SELF, 0f); - t.setDuration(duration); - - ScaleAnimation s = new ScaleAnimation(1f, 1f, 0f, 1f, - Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f); - s.setDuration((long) (duration * 1.5f)); - - mTitleView.startAnimation(t); - mActionButtonBar.startAnimation(s); - - if ((mMenuButton.getVisibility() == View.VISIBLE) && - (mPopupMenu.getMenu().size() > 0)) { - mMenuButton.startAnimation(s); - } - } - - @Override - protected void onDraw(Canvas canvas) { - super.onDraw(canvas); - - // Draw the divider at the bottom of the screen. We could do this with a layer-list - // but then we'd have overdraw (http://stackoverflow.com/a/13509472). - final int bottom = getHeight(); - final int top = bottom - mBottomDividerOffset; - canvas.drawRect(0, top, getWidth(), bottom, mBottomDividerPaint); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/ActivityHandlerHelper.java b/mobile/android/base/java/org/mozilla/gecko/ActivityHandlerHelper.java deleted file mode 100644 index 7174c65807..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/ActivityHandlerHelper.java +++ /dev/null @@ -1,61 +0,0 @@ -/* 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; - -import org.mozilla.gecko.util.ActivityResultHandler; -import org.mozilla.gecko.util.ActivityResultHandlerMap; - -import android.app.Activity; -import android.content.ActivityNotFoundException; -import android.content.Context; -import android.content.Intent; -import android.util.Log; - -public class ActivityHandlerHelper { - private static final String LOGTAG = "GeckoActivityHandlerHelper"; - private static final ActivityResultHandlerMap mActivityResultHandlerMap = new ActivityResultHandlerMap(); - - private static int makeRequestCode(ActivityResultHandler aHandler) { - return mActivityResultHandlerMap.put(aHandler); - } - - public static void startIntent(Intent intent, ActivityResultHandler activityResultHandler) { - startIntentForActivity(GeckoAppShell.getGeckoInterface().getActivity(), intent, activityResultHandler); - } - - /** - * Starts the Activity, catching & logging if the Activity fails to start. - * - * We catch to prevent callers from passing in invalid Intents and crashing the browser. - * - * @return true if the Activity is successfully started, false otherwise. - */ - public static boolean startIntentAndCatch(final String logtag, final Context context, final Intent intent) { - try { - context.startActivity(intent); - return true; - } catch (final ActivityNotFoundException e) { - Log.w(logtag, "Activity not found.", e); - return false; - } catch (final SecurityException e) { - Log.w(logtag, "Forbidden to launch activity.", e); - return false; - } - } - - public static void startIntentForActivity(Activity activity, Intent intent, ActivityResultHandler activityResultHandler) { - activity.startActivityForResult(intent, mActivityResultHandlerMap.put(activityResultHandler)); - } - - - public static boolean handleActivityResult(int requestCode, int resultCode, Intent data) { - ActivityResultHandler handler = mActivityResultHandlerMap.getAndRemove(requestCode); - if (handler != null) { - handler.onActivityResult(resultCode, data); - return true; - } - return false; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/BootReceiver.java b/mobile/android/base/java/org/mozilla/gecko/BootReceiver.java deleted file mode 100644 index 39ca25b679..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/BootReceiver.java +++ /dev/null @@ -1,27 +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; - -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; - -import org.mozilla.gecko.feeds.FeedService; - -/** - * This broadcast receiver receives ACTION_BOOT_COMPLETED broadcasts and starts components that should - * run after the device has booted. - */ -public class BootReceiver extends BroadcastReceiver { - @Override - public void onReceive(Context context, Intent intent) { - if (intent == null || !intent.getAction().equals(Intent.ACTION_BOOT_COMPLETED)) { - return; // This is not the broadcast you are looking for. - } - - FeedService.setup(context); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java b/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java deleted file mode 100644 index 5eddca3cf7..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java +++ /dev/null @@ -1,4261 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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; - -import android.Manifest; -import android.annotation.TargetApi; -import android.app.DownloadManager; -import android.content.ContentProviderClient; -import android.os.Environment; -import android.os.Process; -import android.support.annotation.NonNull; - -import android.graphics.Rect; - -import org.json.JSONArray; -import org.mozilla.gecko.activitystream.ActivityStream; -import org.mozilla.gecko.adjust.AdjustBrowserAppDelegate; -import org.mozilla.gecko.annotation.RobocopTarget; -import org.mozilla.gecko.AppConstants.Versions; -import org.mozilla.gecko.DynamicToolbar.VisibilityTransition; -import org.mozilla.gecko.Tabs.TabEvents; -import org.mozilla.gecko.animation.PropertyAnimator; -import org.mozilla.gecko.animation.ViewHelper; -import org.mozilla.gecko.cleanup.FileCleanupController; -import org.mozilla.gecko.db.BrowserContract; -import org.mozilla.gecko.db.BrowserDB; -import org.mozilla.gecko.db.SuggestedSites; -import org.mozilla.gecko.delegates.BrowserAppDelegate; -import org.mozilla.gecko.delegates.OfflineTabStatusDelegate; -import org.mozilla.gecko.delegates.ScreenshotDelegate; -import org.mozilla.gecko.distribution.Distribution; -import org.mozilla.gecko.distribution.DistributionStoreCallback; -import org.mozilla.gecko.distribution.PartnerBrowserCustomizationsClient; -import org.mozilla.gecko.dlc.DownloadContentService; -import org.mozilla.gecko.icons.decoders.IconDirectoryEntry; -import org.mozilla.gecko.feeds.ContentNotificationsDelegate; -import org.mozilla.gecko.feeds.FeedService; -import org.mozilla.gecko.firstrun.FirstrunAnimationContainer; -import org.mozilla.gecko.gfx.DynamicToolbarAnimator; -import org.mozilla.gecko.gfx.DynamicToolbarAnimator.PinReason; -import org.mozilla.gecko.gfx.ImmutableViewportMetrics; -import org.mozilla.gecko.gfx.LayerView; -import org.mozilla.gecko.home.BrowserSearch; -import org.mozilla.gecko.home.HomeBanner; -import org.mozilla.gecko.home.HomeConfig; -import org.mozilla.gecko.home.HomeConfig.PanelType; -import org.mozilla.gecko.home.HomeConfigPrefsBackend; -import org.mozilla.gecko.home.HomeFragment; -import org.mozilla.gecko.home.HomePager.OnUrlOpenInBackgroundListener; -import org.mozilla.gecko.home.HomePager.OnUrlOpenListener; -import org.mozilla.gecko.home.HomePanelsManager; -import org.mozilla.gecko.home.HomeScreen; -import org.mozilla.gecko.home.SearchEngine; -import org.mozilla.gecko.icons.IconCallback; -import org.mozilla.gecko.icons.IconResponse; -import org.mozilla.gecko.icons.Icons; -import org.mozilla.gecko.javaaddons.JavaAddonManager; -import org.mozilla.gecko.media.VideoPlayer; -import org.mozilla.gecko.menu.GeckoMenu; -import org.mozilla.gecko.menu.GeckoMenuItem; -import org.mozilla.gecko.mozglue.SafeIntent; -import org.mozilla.gecko.notifications.NotificationHelper; -import org.mozilla.gecko.overlays.ui.ShareDialog; -import org.mozilla.gecko.permissions.Permissions; -import org.mozilla.gecko.preferences.ClearOnShutdownPref; -import org.mozilla.gecko.preferences.GeckoPreferences; -import org.mozilla.gecko.promotion.AddToHomeScreenPromotion; -import org.mozilla.gecko.delegates.BookmarkStateChangeDelegate; -import org.mozilla.gecko.promotion.ReaderViewBookmarkPromotion; -import org.mozilla.gecko.prompts.Prompt; -import org.mozilla.gecko.reader.SavedReaderViewHelper; -import org.mozilla.gecko.reader.ReaderModeUtils; -import org.mozilla.gecko.reader.ReadingListHelper; -import org.mozilla.gecko.restrictions.Restrictable; -import org.mozilla.gecko.restrictions.RestrictedProfileConfiguration; -import org.mozilla.gecko.restrictions.Restrictions; -import org.mozilla.gecko.search.SearchEngineManager; -import org.mozilla.gecko.sync.repositories.android.FennecTabsRepository; -import org.mozilla.gecko.tabqueue.TabQueueHelper; -import org.mozilla.gecko.tabqueue.TabQueuePrompt; -import org.mozilla.gecko.tabs.TabHistoryController; -import org.mozilla.gecko.tabs.TabHistoryController.OnShowTabHistory; -import org.mozilla.gecko.tabs.TabHistoryFragment; -import org.mozilla.gecko.tabs.TabHistoryPage; -import org.mozilla.gecko.tabs.TabsPanel; -import org.mozilla.gecko.telemetry.TelemetryUploadService; -import org.mozilla.gecko.telemetry.TelemetryCorePingDelegate; -import org.mozilla.gecko.telemetry.measurements.SearchCountMeasurements; -import org.mozilla.gecko.toolbar.AutocompleteHandler; -import org.mozilla.gecko.toolbar.BrowserToolbar; -import org.mozilla.gecko.toolbar.BrowserToolbar.TabEditingState; -import org.mozilla.gecko.toolbar.ToolbarProgressView; -import org.mozilla.gecko.trackingprotection.TrackingProtectionPrompt; -import org.mozilla.gecko.updater.PostUpdateHandler; -import org.mozilla.gecko.updater.UpdateServiceHelper; -import org.mozilla.gecko.util.ActivityUtils; -import org.mozilla.gecko.util.Clipboard; -import org.mozilla.gecko.util.ContextUtils; -import org.mozilla.gecko.util.EventCallback; -import org.mozilla.gecko.util.FloatUtils; -import org.mozilla.gecko.util.GamepadUtils; -import org.mozilla.gecko.util.GeckoEventListener; -import org.mozilla.gecko.util.HardwareUtils; -import org.mozilla.gecko.util.IntentUtils; -import org.mozilla.gecko.util.MenuUtils; -import org.mozilla.gecko.util.NativeEventListener; -import org.mozilla.gecko.util.NativeJSObject; -import org.mozilla.gecko.util.PrefUtils; -import org.mozilla.gecko.util.StringUtils; -import org.mozilla.gecko.util.ThreadUtils; -import org.mozilla.gecko.widget.AnchoredPopup; - -import org.mozilla.gecko.widget.GeckoActionProvider; - -import android.app.Activity; -import android.app.AlertDialog; -import android.app.Notification; -import android.app.NotificationManager; -import android.app.PendingIntent; -import android.content.ContentResolver; -import android.content.Context; -import android.content.DialogInterface; -import android.content.Intent; -import android.content.SharedPreferences; -import android.content.pm.PackageManager; -import android.content.pm.ResolveInfo; -import android.content.res.Resources; -import android.graphics.Bitmap; -import android.graphics.drawable.BitmapDrawable; -import android.net.Uri; -import android.nfc.NdefMessage; -import android.nfc.NdefRecord; -import android.nfc.NfcAdapter; -import android.nfc.NfcEvent; -import android.os.Build; -import android.os.Bundle; -import android.os.StrictMode; -import android.support.design.widget.Snackbar; -import android.support.v4.app.Fragment; -import android.support.v4.app.FragmentManager; -import android.support.v4.app.NotificationCompat; -import android.support.v4.view.MenuItemCompat; -import android.text.TextUtils; -import android.util.AttributeSet; -import android.util.Base64; -import android.util.Base64OutputStream; -import android.util.Log; -import android.view.InputDevice; -import android.view.KeyEvent; -import android.view.LayoutInflater; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.view.MotionEvent; -import android.view.SubMenu; -import android.view.View; -import android.view.ViewConfiguration; -import android.view.ViewGroup; -import android.view.ViewStub; -import android.view.ViewTreeObserver; -import android.view.Window; -import android.view.animation.Interpolator; -import android.widget.Button; -import android.widget.ListView; -import android.widget.RelativeLayout; -import android.widget.ViewFlipper; -import com.keepsafe.switchboard.AsyncConfigLoader; -import com.keepsafe.switchboard.SwitchBoard; -import android.animation.Animator; -import android.animation.ObjectAnimator; -import org.json.JSONException; -import org.json.JSONObject; - -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.lang.reflect.Method; -import java.net.URLEncoder; -import java.util.Arrays; -import java.util.Collections; -import java.util.EnumSet; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Locale; -import java.util.Vector; -import java.util.regex.Pattern; - -public class BrowserApp extends GeckoApp - implements TabsPanel.TabsLayoutChangeListener, - PropertyAnimator.PropertyAnimationListener, - View.OnKeyListener, - LayerView.DynamicToolbarListener, - BrowserSearch.OnSearchListener, - BrowserSearch.OnEditSuggestionListener, - OnUrlOpenListener, - OnUrlOpenInBackgroundListener, - AnchoredPopup.OnVisibilityChangeListener, - ActionModeCompat.Presenter, - LayoutInflater.Factory { - private static final String LOGTAG = "GeckoBrowserApp"; - - private static final int TABS_ANIMATION_DURATION = 450; - - // Intent String extras used to specify custom Switchboard configurations. - private static final String INTENT_KEY_SWITCHBOARD_SERVER = "switchboard-server"; - - // TODO: Replace with kinto endpoint. - private static final String SWITCHBOARD_SERVER = "https://firefox.settings.services.mozilla.com/v1/buckets/fennec/collections/experiments/records"; - - private static final String STATE_ABOUT_HOME_TOP_PADDING = "abouthome_top_padding"; - - private static final String BROWSER_SEARCH_TAG = "browser_search"; - - // Request ID for startActivityForResult. - private static final int ACTIVITY_REQUEST_PREFERENCES = 1001; - private static final int ACTIVITY_REQUEST_TAB_QUEUE = 2001; - public static final int ACTIVITY_REQUEST_FIRST_READERVIEW_BOOKMARK = 3001; - public static final int ACTIVITY_RESULT_FIRST_READERVIEW_BOOKMARKS_GOTO_BOOKMARKS = 3002; - public static final int ACTIVITY_RESULT_FIRST_READERVIEW_BOOKMARKS_IGNORE = 3003; - public static final int ACTIVITY_REQUEST_TRIPLE_READERVIEW = 4001; - public static final int ACTIVITY_RESULT_TRIPLE_READERVIEW_ADD_BOOKMARK = 4002; - public static final int ACTIVITY_RESULT_TRIPLE_READERVIEW_IGNORE = 4003; - - public static final String ACTION_VIEW_MULTIPLE = AppConstants.ANDROID_PACKAGE_NAME + ".action.VIEW_MULTIPLE"; - - @RobocopTarget - public static final String EXTRA_SKIP_STARTPANE = "skipstartpane"; - private static final String EOL_NOTIFIED = "eol_notified"; - - private BrowserSearch mBrowserSearch; - private View mBrowserSearchContainer; - - public ViewGroup mBrowserChrome; - public ViewFlipper mActionBarFlipper; - public ActionModeCompatView mActionBar; - private VideoPlayer mVideoPlayer; - private BrowserToolbar mBrowserToolbar; - private View mDoorhangerOverlay; - // We can't name the TabStrip class because it's not included on API 9. - private TabStripInterface mTabStrip; - private ToolbarProgressView mProgressView; - private FirstrunAnimationContainer mFirstrunAnimationContainer; - private HomeScreen mHomeScreen; - private TabsPanel mTabsPanel; - /** - * Container for the home screen implementation. This will be populated with any valid - * home screen implementation (currently that is just the HomePager, but that will be extended - * to permit further experimental replacement panels such as the activity-stream panel). - */ - private ViewGroup mHomeScreenContainer; - private int mCachedRecentTabsCount; - private ActionModeCompat mActionMode; - private TabHistoryController tabHistoryController; - private ZoomedView mZoomedView; - - private static final int GECKO_TOOLS_MENU = -1; - private static final int ADDON_MENU_OFFSET = 1000; - public static final String TAB_HISTORY_FRAGMENT_TAG = "tabHistoryFragment"; - - private static class MenuItemInfo { - public int id; - public String label; - public boolean checkable; - public boolean checked; - public boolean enabled = true; - public boolean visible = true; - public int parent; - public boolean added; // So we can re-add after a locale change. - } - - // The types of guest mode dialogs we show. - public static enum GuestModeDialog { - ENTERING, - LEAVING - } - - private Vector mAddonMenuItemsCache; - private PropertyAnimator mMainLayoutAnimator; - - private static final Interpolator sTabsInterpolator = new Interpolator() { - @Override - public float getInterpolation(float t) { - t -= 1.0f; - return t * t * t * t * t + 1.0f; - } - }; - - private FindInPageBar mFindInPageBar; - private MediaCastingBar mMediaCastingBar; - - // We'll ask for feedback after the user launches the app this many times. - private static final int FEEDBACK_LAUNCH_COUNT = 15; - - // Stored value of the toolbar height, so we know when it's changed. - private int mToolbarHeight; - - private SharedPreferencesHelper mSharedPreferencesHelper; - - private ReadingListHelper mReadingListHelper; - - private AccountsHelper mAccountsHelper; - - // The tab to be selected on editing mode exit. - private Integer mTargetTabForEditingMode; - - private final TabEditingState mLastTabEditingState = new TabEditingState(); - - // The animator used to toggle HomePager visibility has a race where if the HomePager is shown - // (starting the animation), the HomePager is hidden, and the HomePager animation completes, - // both the web content and the HomePager will be hidden. This flag is used to prevent the - // race by determining if the web content should be hidden at the animation's end. - private boolean mHideWebContentOnAnimationEnd; - - private final DynamicToolbar mDynamicToolbar = new DynamicToolbar(); - - private final TelemetryCorePingDelegate mTelemetryCorePingDelegate = new TelemetryCorePingDelegate(); - - private final List delegates = Collections.unmodifiableList(Arrays.asList( - new AddToHomeScreenPromotion(), - new ScreenshotDelegate(), - new BookmarkStateChangeDelegate(), - new ReaderViewBookmarkPromotion(), - new ContentNotificationsDelegate(), - new PostUpdateHandler(), - mTelemetryCorePingDelegate, - new OfflineTabStatusDelegate(), - new AdjustBrowserAppDelegate(mTelemetryCorePingDelegate) - )); - - @NonNull - private SearchEngineManager mSearchEngineManager; // Contains reference to Context - DO NOT LEAK! - - private boolean mHasResumed; - - @Override - public View onCreateView(final String name, final Context context, final AttributeSet attrs) { - final View view; - if (BrowserToolbar.class.getName().equals(name)) { - view = BrowserToolbar.create(context, attrs); - } else if (TabsPanel.TabsLayout.class.getName().equals(name)) { - view = TabsPanel.createTabsLayout(context, attrs); - } else { - view = super.onCreateView(name, context, attrs); - } - return view; - } - - @Override - public void onTabChanged(Tab tab, TabEvents msg, String data) { - if (tab == null) { - // Only RESTORED is allowed a null tab: it's the only event that - // isn't tied to a specific tab. - if (msg != Tabs.TabEvents.RESTORED) { - throw new IllegalArgumentException("onTabChanged:" + msg + " must specify a tab."); - } - - final Tab selectedTab = Tabs.getInstance().getSelectedTab(); - if (selectedTab != null) { - // After restoring the tabs we want to update the home pager immediately. Otherwise we - // might wait for an event coming from Gecko and this can take several seconds. (Bug 1283627) - updateHomePagerForTab(selectedTab); - } - - return; - } - - Log.d(LOGTAG, "BrowserApp.onTabChanged: " + tab.getId() + ": " + msg); - switch (msg) { - case SELECTED: - if (mVideoPlayer.isPlaying()) { - mVideoPlayer.stop(); - } - - if (Tabs.getInstance().isSelectedTab(tab) && mDynamicToolbar.isEnabled()) { - final VisibilityTransition transition = (tab.getShouldShowToolbarWithoutAnimationOnFirstSelection()) ? - VisibilityTransition.IMMEDIATE : VisibilityTransition.ANIMATE; - mDynamicToolbar.setVisible(true, transition); - - // The first selection has happened - reset the state. - tab.setShouldShowToolbarWithoutAnimationOnFirstSelection(false); - } - // fall through - case LOCATION_CHANGE: - if (mZoomedView != null) { - mZoomedView.stopZoomDisplay(false); - } - if (Tabs.getInstance().isSelectedTab(tab)) { - updateHomePagerForTab(tab); - } - - mDynamicToolbar.persistTemporaryVisibility(); - break; - case START: - if (Tabs.getInstance().isSelectedTab(tab)) { - invalidateOptionsMenu(); - - if (mDynamicToolbar.isEnabled()) { - mDynamicToolbar.setVisible(true, VisibilityTransition.ANIMATE); - } - } - break; - case LOAD_ERROR: - case STOP: - case MENU_UPDATED: - if (Tabs.getInstance().isSelectedTab(tab)) { - invalidateOptionsMenu(); - } - break; - case PAGE_SHOW: - tab.loadFavicon(); - break; - - case UNSELECTED: - // We receive UNSELECTED immediately after the SELECTED listeners run - // so we are ensured that the unselectedTabEditingText has not changed. - if (tab.isEditing()) { - // Copy to avoid constructing new objects. - tab.getEditingState().copyFrom(mLastTabEditingState); - } - break; - } - - if (HardwareUtils.isTablet() && msg == TabEvents.SELECTED) { - updateEditingModeForTab(tab); - } - - super.onTabChanged(tab, msg, data); - } - - private void updateEditingModeForTab(final Tab selectedTab) { - // (bug 1086983 comment 11) Because the tab may be selected from the gecko thread and we're - // running this code on the UI thread, the selected tab argument may not still refer to the - // selected tab. However, that means this code should be run again and the initial state - // changes will be overridden. As an optimization, we can skip this update, but it may have - // unknown side-effects so we don't. - if (!Tabs.getInstance().isSelectedTab(selectedTab)) { - Log.w(LOGTAG, "updateEditingModeForTab: Given tab is expected to be selected tab"); - } - - saveTabEditingState(mLastTabEditingState); - - if (selectedTab.isEditing()) { - enterEditingMode(); - restoreTabEditingState(selectedTab.getEditingState()); - } else { - mBrowserToolbar.cancelEdit(); - } - } - - private void saveTabEditingState(final TabEditingState editingState) { - mBrowserToolbar.saveTabEditingState(editingState); - editingState.setIsBrowserSearchShown(mBrowserSearch.getUserVisibleHint()); - } - - private void restoreTabEditingState(final TabEditingState editingState) { - mBrowserToolbar.restoreTabEditingState(editingState); - - // Since changing the editing text will show/hide browser search, this - // must be called after we restore the editing state in the edit text View. - if (editingState.isBrowserSearchShown()) { - showBrowserSearch(); - } else { - hideBrowserSearch(); - } - } - - @Override - public boolean onKey(View v, int keyCode, KeyEvent event) { - if (AndroidGamepadManager.handleKeyEvent(event)) { - return true; - } - - // Global onKey handler. This is called if the focused UI doesn't - // handle the key event, and before Gecko swallows the events. - if (event.getAction() != KeyEvent.ACTION_DOWN) { - return false; - } - - if ((event.getSource() & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD) { - switch (keyCode) { - case KeyEvent.KEYCODE_BUTTON_Y: - // Toggle/focus the address bar on gamepad-y button. - if (mBrowserChrome.getVisibility() == View.VISIBLE) { - if (mDynamicToolbar.isEnabled() && !isHomePagerVisible()) { - mDynamicToolbar.setVisible(false, VisibilityTransition.ANIMATE); - if (mLayerView != null) { - mLayerView.requestFocus(); - } - } else { - // Just focus the address bar when about:home is visible - // or when the dynamic toolbar isn't enabled. - mBrowserToolbar.requestFocusFromTouch(); - } - } else { - mDynamicToolbar.setVisible(true, VisibilityTransition.ANIMATE); - mBrowserToolbar.requestFocusFromTouch(); - } - return true; - case KeyEvent.KEYCODE_BUTTON_L1: - // Go back on L1 - Tabs.getInstance().getSelectedTab().doBack(); - return true; - case KeyEvent.KEYCODE_BUTTON_R1: - // Go forward on R1 - Tabs.getInstance().getSelectedTab().doForward(); - return true; - } - } - - // Check if this was a shortcut. Meta keys exists only on 11+. - final Tab tab = Tabs.getInstance().getSelectedTab(); - if (tab != null && event.isCtrlPressed()) { - switch (keyCode) { - case KeyEvent.KEYCODE_LEFT_BRACKET: - tab.doBack(); - return true; - - case KeyEvent.KEYCODE_RIGHT_BRACKET: - tab.doForward(); - return true; - - case KeyEvent.KEYCODE_R: - tab.doReload(false); - return true; - - case KeyEvent.KEYCODE_PERIOD: - tab.doStop(); - return true; - - case KeyEvent.KEYCODE_T: - addTab(); - return true; - - case KeyEvent.KEYCODE_W: - Tabs.getInstance().closeTab(tab); - return true; - - case KeyEvent.KEYCODE_F: - mFindInPageBar.show(); - return true; - } - } - - return false; - } - - private Runnable mCheckLongPress; - { - // Only initialise the runnable if we are >= N. - // See onKeyDown() for more details of the back-button long-press workaround - if (!Versions.preN) { - mCheckLongPress = new Runnable() { - public void run() { - handleBackLongPress(); - } - }; - } - } - - @Override - public boolean onKeyDown(int keyCode, KeyEvent event) { - // Bug 1304688: Android N has broken passing onKeyLongPress events for the back button, so we - // instead copy the long-press-handler technique from Android's KeyButtonView. - // - For short presses, we cancel the callback in onKeyUp - // - For long presses, the normal keypress is marked as cancelled, hence won't be handled elsewhere - // (but Android still provides the haptic feedback), and the runnable is run. - if (!Versions.preN && - keyCode == KeyEvent.KEYCODE_BACK) { - ThreadUtils.getUiHandler().removeCallbacks(mCheckLongPress); - ThreadUtils.getUiHandler().postDelayed(mCheckLongPress, ViewConfiguration.getLongPressTimeout()); - } - - if (!mBrowserToolbar.isEditing() && onKey(null, keyCode, event)) { - return true; - } - return super.onKeyDown(keyCode, event); - } - - @Override - public boolean onKeyUp(int keyCode, KeyEvent event) { - if (!Versions.preN && - keyCode == KeyEvent.KEYCODE_BACK) { - ThreadUtils.getUiHandler().removeCallbacks(mCheckLongPress); - } - - if (AndroidGamepadManager.handleKeyEvent(event)) { - return true; - } - return super.onKeyUp(keyCode, event); - } - - @Override - public void onCreate(Bundle savedInstanceState) { - if (!HardwareUtils.isSupportedSystem()) { - // This build does not support the Android version of the device; Exit early. - super.onCreate(savedInstanceState); - return; - } - - final SafeIntent intent = new SafeIntent(getIntent()); - final boolean isInAutomation = IntentUtils.getIsInAutomationFromEnvironment(intent); - - // This has to be prepared prior to calling GeckoApp.onCreate, because - // widget code and BrowserToolbar need it, and they're created by the - // layout, which GeckoApp takes care of. - ((GeckoApplication) getApplication()).prepareLightweightTheme(); - - super.onCreate(savedInstanceState); - - final Context appContext = getApplicationContext(); - - initSwitchboard(this, intent, isInAutomation); - initTelemetryUploader(isInAutomation); - - mBrowserChrome = (ViewGroup) findViewById(R.id.browser_chrome); - mActionBarFlipper = (ViewFlipper) findViewById(R.id.browser_actionbar); - mActionBar = (ActionModeCompatView) findViewById(R.id.actionbar); - - mVideoPlayer = (VideoPlayer) findViewById(R.id.video_player); - mVideoPlayer.setFullScreenListener(new VideoPlayer.FullScreenListener() { - @Override - public void onFullScreenChanged(boolean fullScreen) { - mVideoPlayer.setFullScreen(fullScreen); - setFullScreen(fullScreen); - } - }); - - mBrowserToolbar = (BrowserToolbar) findViewById(R.id.browser_toolbar); - mBrowserToolbar.setTouchEventInterceptor(new TouchEventInterceptor() { - @Override - public boolean onInterceptTouchEvent(View view, MotionEvent event) { - // Manually dismiss text selection bar if it's not overlaying the toolbar. - mTextSelection.dismiss(); - return false; - } - - @Override - public boolean onTouch(View v, MotionEvent event) { - return false; - } - }); - - mProgressView = (ToolbarProgressView) findViewById(R.id.progress); - mBrowserToolbar.setProgressBar(mProgressView); - - // Initialize Tab History Controller. - tabHistoryController = new TabHistoryController(new OnShowTabHistory() { - @Override - public void onShowHistory(final List historyPageList, final int toIndex) { - runOnUiThread(new Runnable() { - @Override - public void run() { - if (BrowserApp.this.isFinishing()) { - // TabHistoryController is rather slow - and involves calling into Gecko - // to retrieve tab history. That means there can be a significant - // delay between the back-button long-press, and onShowHistory() - // being called. Hence we need to guard against the Activity being - // shut down (in which case trying to perform UI changes, such as showing - // fragments below, will crash). - return; - } - - final TabHistoryFragment fragment = TabHistoryFragment.newInstance(historyPageList, toIndex); - final FragmentManager fragmentManager = getSupportFragmentManager(); - GeckoAppShell.vibrateOnHapticFeedbackEnabled(getResources().getIntArray(R.array.long_press_vibrate_msec)); - fragment.show(R.id.tab_history_panel, fragmentManager.beginTransaction(), TAB_HISTORY_FRAGMENT_TAG); - } - }); - } - }); - mBrowserToolbar.setTabHistoryController(tabHistoryController); - - final String action = intent.getAction(); - if (Intent.ACTION_VIEW.equals(action)) { - // Show the target URL immediately in the toolbar. - mBrowserToolbar.setTitle(intent.getDataString()); - - showTabQueuePromptIfApplicable(intent); - } else if (ACTION_VIEW_MULTIPLE.equals(action) && savedInstanceState == null) { - // We only want to handle this intent if savedInstanceState is null. In the case where - // savedInstanceState is not null this activity is being re-created and we already - // opened tabs for the URLs the last time. Our session store will take care of restoring - // them. - openMultipleTabsFromIntent(intent); - } else if (GuestSession.NOTIFICATION_INTENT.equals(action)) { - GuestSession.onNotificationIntentReceived(this); - } else if (TabQueueHelper.LOAD_URLS_ACTION.equals(action)) { - Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.NOTIFICATION, "tabqueue"); - } - - if (HardwareUtils.isTablet()) { - mTabStrip = (TabStripInterface) (((ViewStub) findViewById(R.id.tablet_tab_strip)).inflate()); - } - - ((GeckoApp.MainLayout) mMainLayout).setTouchEventInterceptor(new HideOnTouchListener()); - ((GeckoApp.MainLayout) mMainLayout).setMotionEventInterceptor(new MotionEventInterceptor() { - @Override - public boolean onInterceptMotionEvent(View view, MotionEvent event) { - // If we get a gamepad panning MotionEvent while the focus is not on the layerview, - // put the focus on the layerview and carry on - if (mLayerView != null && !mLayerView.hasFocus() && GamepadUtils.isPanningControl(event)) { - if (mHomeScreen == null) { - return false; - } - - if (isHomePagerVisible()) { - mLayerView.requestFocus(); - } else { - mHomeScreen.requestFocus(); - } - } - return false; - } - }); - - mHomeScreenContainer = (ViewGroup) findViewById(R.id.home_screen_container); - - mBrowserSearchContainer = findViewById(R.id.search_container); - mBrowserSearch = (BrowserSearch) getSupportFragmentManager().findFragmentByTag(BROWSER_SEARCH_TAG); - if (mBrowserSearch == null) { - mBrowserSearch = BrowserSearch.newInstance(); - mBrowserSearch.setUserVisibleHint(false); - } - - setBrowserToolbarListeners(); - - mFindInPageBar = (FindInPageBar) findViewById(R.id.find_in_page); - mMediaCastingBar = (MediaCastingBar) findViewById(R.id.media_casting); - - mDoorhangerOverlay = findViewById(R.id.doorhanger_overlay); - - EventDispatcher.getInstance().registerGeckoThreadListener((GeckoEventListener)this, - "Gecko:DelayedStartup", - "Menu:Open", - "Menu:Update", - "LightweightTheme:Update", - "Search:Keyword", - "Prompt:ShowTop", - "Tab:Added", - "Video:Play"); - - EventDispatcher.getInstance().registerGeckoThreadListener((NativeEventListener)this, - "CharEncoding:Data", - "CharEncoding:State", - "Download:AndroidDownloadManager", - "Experiments:GetActive", - "Experiments:SetOverride", - "Experiments:ClearOverride", - "Favicon:CacheLoad", - "Feedback:MaybeLater", - "Menu:Add", - "Menu:Remove", - "Sanitize:ClearHistory", - "Sanitize:ClearSyncedTabs", - "Settings:Show", - "Telemetry:Gather", - "Updater:Launch", - "Website:Metadata"); - - final GeckoProfile profile = getProfile(); - - // We want to upload the telemetry core ping as soon after startup as possible. It relies on the - // Distribution being initialized. If you move this initialization, ensure it plays well with telemetry. - final Distribution distribution = Distribution.init(getApplicationContext()); - distribution.addOnDistributionReadyCallback( - new DistributionStoreCallback(getApplicationContext(), profile.getName())); - - mSearchEngineManager = new SearchEngineManager(this, distribution); - - // Init suggested sites engine in BrowserDB. - final SuggestedSites suggestedSites = new SuggestedSites(appContext, distribution); - final BrowserDB db = BrowserDB.from(profile); - db.setSuggestedSites(suggestedSites); - - JavaAddonManager.getInstance().init(appContext); - mSharedPreferencesHelper = new SharedPreferencesHelper(appContext); - mReadingListHelper = new ReadingListHelper(appContext, profile); - mAccountsHelper = new AccountsHelper(appContext, profile); - - if (AppConstants.MOZ_ANDROID_BEAM) { - NfcAdapter nfc = NfcAdapter.getDefaultAdapter(this); - if (nfc != null) { - nfc.setNdefPushMessageCallback(new NfcAdapter.CreateNdefMessageCallback() { - @Override - public NdefMessage createNdefMessage(NfcEvent event) { - Tab tab = Tabs.getInstance().getSelectedTab(); - if (tab == null || tab.isPrivate()) { - return null; - } - return new NdefMessage(new NdefRecord[] { NdefRecord.createUri(tab.getURL()) }); - } - }, this); - } - } - - if (savedInstanceState != null) { - mDynamicToolbar.onRestoreInstanceState(savedInstanceState); - mHomeScreenContainer.setPadding(0, savedInstanceState.getInt(STATE_ABOUT_HOME_TOP_PADDING), 0, 0); - } - - mDynamicToolbar.setEnabledChangedListener(new DynamicToolbar.OnEnabledChangedListener() { - @Override - public void onEnabledChanged(boolean enabled) { - setDynamicToolbarEnabled(enabled); - } - }); - - // Set the maximum bits-per-pixel the favicon system cares about. - IconDirectoryEntry.setMaxBPP(GeckoAppShell.getScreenDepth()); - - // The update service is enabled for RELEASE_OR_BETA, which includes the release and beta channels. - // However, no updates are served. Therefore, we don't trust the update service directly, and - // try to avoid prompting unnecessarily. See Bug 1232798. - if (!AppConstants.RELEASE_OR_BETA && UpdateServiceHelper.isUpdaterEnabled(this)) { - Permissions.from(this) - .withPermissions(Manifest.permission.WRITE_EXTERNAL_STORAGE) - .doNotPrompt() - .andFallback(new Runnable() { - @Override - public void run() { - showUpdaterPermissionSnackbar(); - } - }) - .run(); - } - - for (final BrowserAppDelegate delegate : delegates) { - delegate.onCreate(this, savedInstanceState); - } - - // We want to get an understanding of how our user base is spread (bug 1221646). - final String installerPackageName = getPackageManager().getInstallerPackageName(getPackageName()); - Telemetry.sendUIEvent(TelemetryContract.Event.LAUNCH, TelemetryContract.Method.SYSTEM, "installer_" + installerPackageName); - } - - /** - * Initializes the default Switchboard URLs the first time. - * @param intent - */ - private static void initSwitchboard(final Context context, final SafeIntent intent, final boolean isInAutomation) { - if (isInAutomation) { - Log.d(LOGTAG, "Switchboard disabled - in automation"); - return; - } else if (!AppConstants.MOZ_SWITCHBOARD) { - Log.d(LOGTAG, "Switchboard compile-time disabled"); - return; - } - - final String serverExtra = intent.getStringExtra(INTENT_KEY_SWITCHBOARD_SERVER); - final String serverUrl = TextUtils.isEmpty(serverExtra) ? SWITCHBOARD_SERVER : serverExtra; - new AsyncConfigLoader(context, serverUrl).execute(); - } - - private static void initTelemetryUploader(final boolean isInAutomation) { - TelemetryUploadService.setDisabled(isInAutomation); - } - - private void showUpdaterPermissionSnackbar() { - SnackbarBuilder.SnackbarCallback allowCallback = new SnackbarBuilder.SnackbarCallback() { - @Override - public void onClick(View v) { - Permissions.from(BrowserApp.this) - .withPermissions(Manifest.permission.WRITE_EXTERNAL_STORAGE) - .run(); - } - }; - - SnackbarBuilder.builder(this) - .message(R.string.updater_permission_text) - .duration(Snackbar.LENGTH_INDEFINITE) - .action(R.string.updater_permission_allow) - .callback(allowCallback) - .buildAndShow(); - } - - private void conditionallyNotifyEOL() { - final StrictMode.ThreadPolicy savedPolicy = StrictMode.allowThreadDiskReads(); - try { - final SharedPreferences prefs = GeckoSharedPrefs.forProfile(this); - if (!prefs.contains(EOL_NOTIFIED)) { - - // Launch main App to load SUMO url on EOL notification. - final String link = getString(R.string.eol_notification_url, - AppConstants.MOZ_APP_VERSION, - AppConstants.OS_TARGET, - Locales.getLanguageTag(Locale.getDefault())); - - final Intent intent = new Intent(Intent.ACTION_VIEW); - intent.setClassName(AppConstants.ANDROID_PACKAGE_NAME, AppConstants.MOZ_ANDROID_BROWSER_INTENT_CLASS); - intent.setData(Uri.parse(link)); - final PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); - - final Notification notification = new NotificationCompat.Builder(this) - .setContentTitle(getString(R.string.eol_notification_title)) - .setContentText(getString(R.string.eol_notification_summary)) - .setSmallIcon(R.drawable.ic_status_logo) - .setAutoCancel(true) - .setContentIntent(pendingIntent) - .build(); - - final NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); - final int notificationID = EOL_NOTIFIED.hashCode(); - notificationManager.notify(notificationID, notification); - - GeckoSharedPrefs.forProfile(this) - .edit() - .putBoolean(EOL_NOTIFIED, true) - .apply(); - } - } finally { - StrictMode.setThreadPolicy(savedPolicy); - } - } - - /** - * Check and show the firstrun pane if the browser has never been launched and - * is not opening an external link from another application. - * - * @param context Context of application; used to show firstrun pane if appropriate - * @param intent Intent that launched this activity - */ - private void checkFirstrun(Context context, SafeIntent intent) { - if (getProfile().inGuestMode()) { - // We do not want to show any first run tour for guest profiles. - return; - } - - if (intent.getBooleanExtra(EXTRA_SKIP_STARTPANE, false)) { - // Note that we don't set the pref, so subsequent launches can result - // in the firstrun pane being shown. - return; - } - final StrictMode.ThreadPolicy savedPolicy = StrictMode.allowThreadDiskReads(); - - try { - final SharedPreferences prefs = GeckoSharedPrefs.forProfile(this); - - if (prefs.getBoolean(FirstrunAnimationContainer.PREF_FIRSTRUN_ENABLED, true)) { - if (!Intent.ACTION_VIEW.equals(intent.getAction())) { - showFirstrunPager(); - - if (HardwareUtils.isTablet()) { - mTabStrip.setOnTabChangedListener(new TabStripInterface.OnTabAddedOrRemovedListener() { - @Override - public void onTabChanged() { - hideFirstrunPager(TelemetryContract.Method.BUTTON); - mTabStrip.setOnTabChangedListener(null); - } - }); - } - } - - // Don't bother trying again to show the v1 minimal first run. - prefs.edit().putBoolean(FirstrunAnimationContainer.PREF_FIRSTRUN_ENABLED, false).apply(); - - // We have no intention of stopping this session. The FIRSTRUN session - // ends when the browsing session/activity has ended. All events - // during firstrun will be tagged as FIRSTRUN. - Telemetry.startUISession(TelemetryContract.Session.FIRSTRUN); - } - } finally { - StrictMode.setThreadPolicy(savedPolicy); - } - } - - private Class getMediaPlayerManager() { - if (AppConstants.MOZ_MEDIA_PLAYER) { - try { - return Class.forName("org.mozilla.gecko.MediaPlayerManager"); - } catch (Exception ex) { - // Ignore failures - Log.e(LOGTAG, "No native casting support", ex); - } - } - - return null; - } - - @Override - public void onBackPressed() { - if (mTextSelection.dismiss()) { - return; - } - - if (getSupportFragmentManager().getBackStackEntryCount() > 0) { - super.onBackPressed(); - return; - } - - if (mBrowserToolbar.onBackPressed()) { - return; - } - - if (mActionMode != null) { - endActionModeCompat(); - return; - } - - if (hideFirstrunPager(TelemetryContract.Method.BACK)) { - return; - } - - if (mVideoPlayer.isFullScreen()) { - mVideoPlayer.setFullScreen(false); - setFullScreen(false); - return; - } - - if (mVideoPlayer.isPlaying()) { - mVideoPlayer.stop(); - return; - } - - super.onBackPressed(); - } - - @Override - public void onAttachedToWindow() { - // We can't show the first run experience until Gecko has finished initialization (bug 1077583). - checkFirstrun(this, new SafeIntent(getIntent())); - } - - @Override - protected void processTabQueue() { - if (TabQueueHelper.TAB_QUEUE_ENABLED && mInitialized) { - ThreadUtils.postToBackgroundThread(new Runnable() { - @Override - public void run() { - if (TabQueueHelper.shouldOpenTabQueueUrls(BrowserApp.this)) { - openQueuedTabs(); - } - } - }); - } - } - - @Override - protected void openQueuedTabs() { - ThreadUtils.assertNotOnUiThread(); - - int queuedTabCount = TabQueueHelper.getTabQueueLength(BrowserApp.this); - - Telemetry.addToHistogram("FENNEC_TABQUEUE_QUEUESIZE", queuedTabCount); - Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.INTENT, "tabqueue-delayed"); - - TabQueueHelper.openQueuedUrls(BrowserApp.this, getProfile(), TabQueueHelper.FILE_NAME, false); - - // If there's more than one tab then also show the tabs panel. - if (queuedTabCount > 1) { - ThreadUtils.postToUiThread(new Runnable() { - @Override - public void run() { - showNormalTabs(); - } - }); - } - } - - private void openMultipleTabsFromIntent(final SafeIntent intent) { - final List urls = intent.getStringArrayListExtra("urls"); - if (urls != null) { - openUrls(urls); - } - } - - @Override - public void onResume() { - super.onResume(); - - if (mIsAbortingAppLaunch) { - return; - } - - if (!mHasResumed) { - EventDispatcher.getInstance().unregisterGeckoThreadListener((GeckoEventListener) this, - "Prompt:ShowTop"); - mHasResumed = true; - } - - processTabQueue(); - - for (BrowserAppDelegate delegate : delegates) { - delegate.onResume(this); - } - } - - @Override - public void onPause() { - super.onPause(); - if (mIsAbortingAppLaunch) { - return; - } - - if (mHasResumed) { - // Register for Prompt:ShowTop so we can foreground this activity even if it's hidden. - EventDispatcher.getInstance().registerGeckoThreadListener((GeckoEventListener) this, - "Prompt:ShowTop"); - mHasResumed = false; - } - - for (BrowserAppDelegate delegate : delegates) { - delegate.onPause(this); - } - } - - @Override - public void onRestart() { - super.onRestart(); - if (mIsAbortingAppLaunch) { - return; - } - - for (final BrowserAppDelegate delegate : delegates) { - delegate.onRestart(this); - } - } - - @Override - public void onStart() { - super.onStart(); - if (mIsAbortingAppLaunch) { - return; - } - - // Queue this work so that the first launch of the activity doesn't - // trigger profile init too early. - ThreadUtils.postToBackgroundThread(new Runnable() { - @Override - public void run() { - final GeckoProfile profile = getProfile(); - if (profile.inGuestMode()) { - GuestSession.showNotification(BrowserApp.this); - } else { - // If we're restarting, we won't destroy the activity. - // Make sure we remove any guest notifications that might - // have been shown. - GuestSession.hideNotification(BrowserApp.this); - } - - // It'd be better to launch this once, in onCreate, but there's ambiguity for when the - // profile is created so we run here instead. Don't worry, call start short-circuits pretty fast. - final SharedPreferences sharedPrefs = GeckoSharedPrefs.forProfileName(BrowserApp.this, profile.getName()); - FileCleanupController.startIfReady(BrowserApp.this, sharedPrefs, profile.getDir().getAbsolutePath()); - } - }); - - for (final BrowserAppDelegate delegate : delegates) { - delegate.onStart(this); - } - } - - @Override - public void onStop() { - super.onStop(); - if (mIsAbortingAppLaunch) { - return; - } - - // We only show the guest mode notification when our activity is in the foreground. - GuestSession.hideNotification(this); - - for (final BrowserAppDelegate delegate : delegates) { - delegate.onStop(this); - } - } - - @Override - public void onWindowFocusChanged(boolean hasFocus) { - super.onWindowFocusChanged(hasFocus); - - // Sending a message to the toolbar when the browser window gains focus - // This is needed for qr code input - if (hasFocus) { - mBrowserToolbar.onParentFocus(); - } - } - - private void setBrowserToolbarListeners() { - mBrowserToolbar.setOnActivateListener(new BrowserToolbar.OnActivateListener() { - @Override - public void onActivate() { - enterEditingMode(); - } - }); - - mBrowserToolbar.setOnCommitListener(new BrowserToolbar.OnCommitListener() { - @Override - public void onCommit() { - commitEditingMode(); - } - }); - - mBrowserToolbar.setOnDismissListener(new BrowserToolbar.OnDismissListener() { - @Override - public void onDismiss() { - mBrowserToolbar.cancelEdit(); - } - }); - - mBrowserToolbar.setOnFilterListener(new BrowserToolbar.OnFilterListener() { - @Override - public void onFilter(String searchText, AutocompleteHandler handler) { - filterEditingMode(searchText, handler); - } - }); - - mBrowserToolbar.setOnFocusChangeListener(new View.OnFocusChangeListener() { - @Override - public void onFocusChange(View v, boolean hasFocus) { - if (isHomePagerVisible()) { - mHomeScreen.onToolbarFocusChange(hasFocus); - } - } - }); - - mBrowserToolbar.setOnStartEditingListener(new BrowserToolbar.OnStartEditingListener() { - @Override - public void onStartEditing() { - final Tab selectedTab = Tabs.getInstance().getSelectedTab(); - if (selectedTab != null) { - selectedTab.setIsEditing(true); - } - - // Temporarily disable doorhanger notifications. - if (mDoorHangerPopup != null) { - mDoorHangerPopup.disable(); - } - } - }); - - mBrowserToolbar.setOnStopEditingListener(new BrowserToolbar.OnStopEditingListener() { - @Override - public void onStopEditing() { - final Tab selectedTab = Tabs.getInstance().getSelectedTab(); - if (selectedTab != null) { - selectedTab.setIsEditing(false); - } - - selectTargetTabForEditingMode(); - - // Since the underlying LayerView is set visible in hideHomePager, we would - // ordinarily want to call it first. However, hideBrowserSearch changes the - // visibility of the HomePager and hideHomePager will take no action if the - // HomePager is hidden, so we want to call hideBrowserSearch to restore the - // HomePager visibility first. - hideBrowserSearch(); - hideHomePager(); - - // Re-enable doorhanger notifications. They may trigger on the selected tab above. - if (mDoorHangerPopup != null) { - mDoorHangerPopup.enable(); - } - } - }); - - // Intercept key events for gamepad shortcuts - mBrowserToolbar.setOnKeyListener(this); - } - - private void setDynamicToolbarEnabled(boolean enabled) { - ThreadUtils.assertOnUiThread(); - - if (enabled) { - if (mLayerView != null) { - mLayerView.getDynamicToolbarAnimator().addTranslationListener(this); - } - setToolbarMargin(0); - mHomeScreenContainer.setPadding(0, mBrowserChrome.getHeight(), 0, 0); - } else { - // Immediately show the toolbar when disabling the dynamic - // toolbar. - if (mLayerView != null) { - mLayerView.getDynamicToolbarAnimator().removeTranslationListener(this); - } - mHomeScreenContainer.setPadding(0, 0, 0, 0); - if (mBrowserChrome != null) { - ViewHelper.setTranslationY(mBrowserChrome, 0); - } - if (mLayerView != null) { - mLayerView.setSurfaceTranslation(0); - } - } - - refreshToolbarHeight(); - } - - private static boolean isAboutHome(final Tab tab) { - return AboutPages.isAboutHome(tab.getURL()); - } - - @Override - public boolean onSearchRequested() { - enterEditingMode(); - return true; - } - - @Override - public boolean onContextItemSelected(MenuItem item) { - final int itemId = item.getItemId(); - if (itemId == R.id.pasteandgo) { - hideFirstrunPager(TelemetryContract.Method.CONTEXT_MENU); - - String text = Clipboard.getText(); - if (!TextUtils.isEmpty(text)) { - loadUrlOrKeywordSearch(text); - Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.CONTEXT_MENU); - Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.CONTEXT_MENU, "pasteandgo"); - } - return true; - } - - if (itemId == R.id.paste) { - String text = Clipboard.getText(); - if (!TextUtils.isEmpty(text)) { - enterEditingMode(text); - showBrowserSearch(); - mBrowserSearch.filter(text, null); - Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.CONTEXT_MENU, "paste"); - } - return true; - } - - if (itemId == R.id.subscribe) { - // This can be selected from either the browser menu or the contextmenu, depending on the size and version (v11+) of the phone. - Tab tab = Tabs.getInstance().getSelectedTab(); - if (tab != null && tab.hasFeeds()) { - JSONObject args = new JSONObject(); - try { - args.put("tabId", tab.getId()); - } catch (JSONException e) { - Log.e(LOGTAG, "error building json arguments", e); - } - GeckoAppShell.notifyObservers("Feeds:Subscribe", args.toString()); - } - return true; - } - - if (itemId == R.id.add_search_engine) { - // This can be selected from either the browser menu or the contextmenu, depending on the size and version (v11+) of the phone. - Tab tab = Tabs.getInstance().getSelectedTab(); - if (tab != null && tab.hasOpenSearch()) { - JSONObject args = new JSONObject(); - try { - args.put("tabId", tab.getId()); - } catch (JSONException e) { - Log.e(LOGTAG, "error building json arguments", e); - return true; - } - GeckoAppShell.notifyObservers("SearchEngines:Add", args.toString()); - } - return true; - } - - if (itemId == R.id.copyurl) { - Tab tab = Tabs.getInstance().getSelectedTab(); - if (tab != null) { - String url = ReaderModeUtils.stripAboutReaderUrl(tab.getURL()); - if (url != null) { - Clipboard.setText(url); - Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.CONTEXT_MENU, "copyurl"); - } - } - return true; - } - - if (itemId == R.id.add_to_launcher) { - final Tab tab = Tabs.getInstance().getSelectedTab(); - if (tab == null) { - return true; - } - - final String url = tab.getURL(); - final String title = tab.getDisplayTitle(); - if (url == null || title == null) { - return true; - } - - ThreadUtils.postToBackgroundThread(new Runnable() { - @Override - public void run() { - GeckoAppShell.createShortcut(title, url); - - } - }); - - Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.CONTEXT_MENU, - getResources().getResourceEntryName(itemId)); - return true; - } - - return false; - } - - @Override - public void setAccessibilityEnabled(boolean enabled) { - super.setAccessibilityEnabled(enabled); - mDynamicToolbar.setAccessibilityEnabled(enabled); - } - - @Override - public void onDestroy() { - if (mIsAbortingAppLaunch) { - super.onDestroy(); - return; - } - - mDynamicToolbar.destroy(); - - if (mBrowserToolbar != null) - mBrowserToolbar.onDestroy(); - - if (mFindInPageBar != null) { - mFindInPageBar.onDestroy(); - mFindInPageBar = null; - } - - if (mMediaCastingBar != null) { - mMediaCastingBar.onDestroy(); - mMediaCastingBar = null; - } - - if (mSharedPreferencesHelper != null) { - mSharedPreferencesHelper.uninit(); - mSharedPreferencesHelper = null; - } - - if (mReadingListHelper != null) { - mReadingListHelper.uninit(); - mReadingListHelper = null; - } - - if (mAccountsHelper != null) { - mAccountsHelper.uninit(); - mAccountsHelper = null; - } - - if (mZoomedView != null) { - mZoomedView.destroy(); - } - - mSearchEngineManager.unregisterListeners(); - - EventDispatcher.getInstance().unregisterGeckoThreadListener((GeckoEventListener) this, - "Gecko:DelayedStartup", - "Menu:Open", - "Menu:Update", - "LightweightTheme:Update", - "Search:Keyword", - "Prompt:ShowTop", - "Tab:Added", - "Video:Play"); - - EventDispatcher.getInstance().unregisterGeckoThreadListener((NativeEventListener) this, - "CharEncoding:Data", - "CharEncoding:State", - "Download:AndroidDownloadManager", - "Experiments:GetActive", - "Experiments:SetOverride", - "Experiments:ClearOverride", - "Favicon:CacheLoad", - "Feedback:MaybeLater", - "Menu:Add", - "Menu:Remove", - "Sanitize:ClearHistory", - "Sanitize:ClearSyncedTabs", - "Settings:Show", - "Telemetry:Gather", - "Updater:Launch", - "Website:Metadata"); - - if (AppConstants.MOZ_ANDROID_BEAM) { - NfcAdapter nfc = NfcAdapter.getDefaultAdapter(this); - if (nfc != null) { - // null this out even though the docs say it's not needed, - // because the source code looks like it will only do this - // automatically on API 14+ - nfc.setNdefPushMessageCallback(null, this); - } - } - - for (final BrowserAppDelegate delegate : delegates) { - delegate.onDestroy(this); - } - - deleteTempFiles(); - - if (mDoorHangerPopup != null) - mDoorHangerPopup.destroy(); - if (mFormAssistPopup != null) - mFormAssistPopup.destroy(); - if (mTextSelection != null) - mTextSelection.destroy(); - NotificationHelper.destroy(); - IntentHelper.destroy(); - GeckoNetworkManager.destroy(); - - super.onDestroy(); - - if (!isFinishing()) { - // GeckoApp was not intentionally destroyed, so keep our process alive. - return; - } - - // Wait for Gecko to handle our pause event sent in onPause. - if (GeckoThread.isStateAtLeast(GeckoThread.State.PROFILE_READY)) { - GeckoThread.waitOnGecko(); - } - - if (mRestartIntent != null) { - // Restarting, so let Restarter kill us. - final Intent intent = new Intent(); - intent.setClass(getApplicationContext(), Restarter.class) - .putExtra("pid", Process.myPid()) - .putExtra(Intent.EXTRA_INTENT, mRestartIntent); - startService(intent); - } else { - // Exiting, so kill our own process. - Process.killProcess(Process.myPid()); - } - } - - @Override - protected void initializeChrome() { - super.initializeChrome(); - - mDoorHangerPopup.setAnchor(mBrowserToolbar.getDoorHangerAnchor()); - mDoorHangerPopup.setOnVisibilityChangeListener(this); - - mDynamicToolbar.setLayerView(mLayerView); - setDynamicToolbarEnabled(mDynamicToolbar.isEnabled()); - - // Intercept key events for gamepad shortcuts - mLayerView.setOnKeyListener(this); - - // Initialize the actionbar menu items on startup for both large and small tablets - if (HardwareUtils.isTablet()) { - onCreatePanelMenu(Window.FEATURE_OPTIONS_PANEL, null); - invalidateOptionsMenu(); - } - } - - @Override - public void onDoorHangerShow() { - mDynamicToolbar.setVisible(true, VisibilityTransition.ANIMATE); - - final Animator alphaAnimator = ObjectAnimator.ofFloat(mDoorhangerOverlay, "alpha", 1); - alphaAnimator.setDuration(250); - - alphaAnimator.start(); - } - - @Override - public void onDoorHangerHide() { - final Animator alphaAnimator = ObjectAnimator.ofFloat(mDoorhangerOverlay, "alpha", 0); - alphaAnimator.setDuration(200); - - alphaAnimator.start(); - } - - private void handleClearHistory(final boolean clearSearchHistory) { - final BrowserDB db = BrowserDB.from(getProfile()); - ThreadUtils.postToBackgroundThread(new Runnable() { - @Override - public void run() { - db.clearHistory(getContentResolver(), clearSearchHistory); - } - }); - } - - private void handleClearSyncedTabs() { - ThreadUtils.postToBackgroundThread(new Runnable() { - @Override - public void run() { - FennecTabsRepository.deleteNonLocalClientsAndTabs(getContext()); - } - }); - } - - private void setToolbarMargin(int margin) { - ((RelativeLayout.LayoutParams) mGeckoLayout.getLayoutParams()).topMargin = margin; - mGeckoLayout.requestLayout(); - } - - @Override - public void onTranslationChanged(float aToolbarTranslation, float aLayerViewTranslation) { - if (mBrowserChrome == null) { - return; - } - - final View browserChrome = mBrowserChrome; - final ToolbarProgressView progressView = mProgressView; - - ViewHelper.setTranslationY(browserChrome, -aToolbarTranslation); - mLayerView.setSurfaceTranslation(mToolbarHeight - aLayerViewTranslation); - - // Stop the progressView from moving all the way up so that we can still see a good chunk of it - // when the chrome is offscreen. - final float offset = getResources().getDimensionPixelOffset(R.dimen.progress_bar_scroll_offset); - final float progressTranslationY = Math.min(aToolbarTranslation, mToolbarHeight - offset); - ViewHelper.setTranslationY(progressView, -progressTranslationY); - - if (mFormAssistPopup != null) { - mFormAssistPopup.onTranslationChanged(); - } - } - - @Override - public void onMetricsChanged(ImmutableViewportMetrics aMetrics) { - if (isHomePagerVisible() || mBrowserChrome == null) { - return; - } - - if (mFormAssistPopup != null) { - mFormAssistPopup.onMetricsChanged(aMetrics); - } - } - - @Override - public void onPanZoomStopped() { - if (!mDynamicToolbar.isEnabled() || isHomePagerVisible() || - mBrowserChrome.getVisibility() != View.VISIBLE) { - return; - } - - // Make sure the toolbar is fully hidden or fully shown when the user - // lifts their finger, depending on various conditions. - ImmutableViewportMetrics metrics = mLayerView.getViewportMetrics(); - float toolbarTranslation = mLayerView.getDynamicToolbarAnimator().getToolbarTranslation(); - - boolean shortPage = metrics.getPageHeight() < metrics.getHeight(); - boolean atBottomOfLongPage = - FloatUtils.fuzzyEquals(metrics.pageRectBottom, metrics.viewportRectBottom()) - && (metrics.pageRectBottom > 2 * metrics.getHeight()); - Log.v(LOGTAG, "On pan/zoom stopped, short page: " + shortPage - + "; atBottomOfLongPage: " + atBottomOfLongPage); - if (shortPage || atBottomOfLongPage) { - mDynamicToolbar.setVisible(true, VisibilityTransition.ANIMATE); - } - } - - public void refreshToolbarHeight() { - ThreadUtils.assertOnUiThread(); - - int height = 0; - if (mBrowserChrome != null) { - height = mBrowserChrome.getHeight(); - } - - if (!mDynamicToolbar.isEnabled() || isHomePagerVisible()) { - // Use aVisibleHeight here so that when the dynamic toolbar is - // enabled, the padding will animate with the toolbar becoming - // visible. - if (mDynamicToolbar.isEnabled()) { - // When the dynamic toolbar is enabled, set the padding on the - // about:home widget directly - this is to avoid resizing the - // LayerView, which can cause visible artifacts. - mHomeScreenContainer.setPadding(0, height, 0, 0); - } else { - setToolbarMargin(height); - height = 0; - } - } else { - setToolbarMargin(0); - } - - if (mLayerView != null && height != mToolbarHeight) { - mToolbarHeight = height; - mLayerView.setMaxTranslation(height); - mDynamicToolbar.setVisible(true, VisibilityTransition.IMMEDIATE); - } - } - - @Override - void toggleChrome(final boolean aShow) { - ThreadUtils.postToUiThread(new Runnable() { - @Override - public void run() { - if (aShow) { - mBrowserChrome.setVisibility(View.VISIBLE); - } else { - mBrowserChrome.setVisibility(View.GONE); - } - } - }); - - super.toggleChrome(aShow); - } - - @Override - void focusChrome() { - ThreadUtils.postToUiThread(new Runnable() { - @Override - public void run() { - mBrowserChrome.setVisibility(View.VISIBLE); - mActionBarFlipper.requestFocusFromTouch(); - } - }); - } - - @Override - public void refreshChrome() { - invalidateOptionsMenu(); - - if (mTabsPanel != null) { - mTabsPanel.refresh(); - } - - if (mTabStrip != null) { - mTabStrip.refresh(); - } - - mBrowserToolbar.refresh(); - } - - @Override - public void handleMessage(final String event, final NativeJSObject message, - final EventCallback callback) { - switch (event) { - case "CharEncoding:Data": - final NativeJSObject[] charsets = message.getObjectArray("charsets"); - final int selected = message.getInt("selected"); - - final String[] titleArray = new String[charsets.length]; - final String[] codeArray = new String[charsets.length]; - for (int i = 0; i < charsets.length; i++) { - final NativeJSObject charset = charsets[i]; - titleArray[i] = charset.getString("title"); - codeArray[i] = charset.getString("code"); - } - - final AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this); - dialogBuilder.setSingleChoiceItems(titleArray, selected, - new AlertDialog.OnClickListener() { - @Override - public void onClick(final DialogInterface dialog, final int which) { - GeckoAppShell.notifyObservers("CharEncoding:Set", codeArray[which]); - dialog.dismiss(); - } - }); - dialogBuilder.setNegativeButton(R.string.button_cancel, - new AlertDialog.OnClickListener() { - @Override - public void onClick(final DialogInterface dialog, final int which) { - dialog.dismiss(); - } - }); - ThreadUtils.postToUiThread(new Runnable() { - @Override - public void run() { - dialogBuilder.show(); - } - }); - break; - - case "CharEncoding:State": - final boolean visible = message.getString("visible").equals("true"); - GeckoPreferences.setCharEncodingState(visible); - final Menu menu = mMenu; - ThreadUtils.postToUiThread(new Runnable() { - @Override - public void run() { - if (menu != null) { - menu.findItem(R.id.char_encoding).setVisible(visible); - } - } - }); - break; - - case "Experiments:GetActive": - final List experiments = SwitchBoard.getActiveExperiments(this); - final JSONArray json = new JSONArray(experiments); - callback.sendSuccess(json.toString()); - break; - - case "Experiments:SetOverride": - Experiments.setOverride(getContext(), message.getString("name"), message.getBoolean("isEnabled")); - break; - - case "Experiments:ClearOverride": - Experiments.clearOverride(getContext(), message.getString("name")); - break; - - case "Favicon:CacheLoad": - final String url = message.getString("url"); - getFaviconFromCache(callback, url); - break; - - case "Feedback:MaybeLater": - resetFeedbackLaunchCount(); - break; - - case "Menu:Add": - final MenuItemInfo info = new MenuItemInfo(); - info.label = message.getString("name"); - info.id = message.getInt("id") + ADDON_MENU_OFFSET; - info.checked = message.optBoolean("checked", false); - info.enabled = message.optBoolean("enabled", true); - info.visible = message.optBoolean("visible", true); - info.checkable = message.optBoolean("checkable", false); - final int parent = message.optInt("parent", 0); - info.parent = parent <= 0 ? parent : parent + ADDON_MENU_OFFSET; - final MenuItemInfo menuItemInfo = info; - ThreadUtils.postToUiThread(new Runnable() { - @Override - public void run() { - addAddonMenuItem(menuItemInfo); - } - }); - break; - - case "Menu:Remove": - final int id = message.getInt("id") + ADDON_MENU_OFFSET; - ThreadUtils.postToUiThread(new Runnable() { - @Override - public void run() { - removeAddonMenuItem(id); - } - }); - break; - - case "Sanitize:ClearHistory": - handleClearHistory(message.optBoolean("clearSearchHistory", false)); - callback.sendSuccess(true); - break; - - case "Sanitize:ClearSyncedTabs": - handleClearSyncedTabs(); - callback.sendSuccess(true); - break; - - case "Settings:Show": - final String resource = - message.optString(GeckoPreferences.INTENT_EXTRA_RESOURCES, null); - final Intent settingsIntent = new Intent(this, GeckoPreferences.class); - GeckoPreferences.setResourceToOpen(settingsIntent, resource); - startActivityForResult(settingsIntent, ACTIVITY_REQUEST_PREFERENCES); - - // Don't use a transition to settings if we're on a device where that - // would look bad. - if (HardwareUtils.IS_KINDLE_DEVICE) { - overridePendingTransition(0, 0); - } - break; - - case "Telemetry:Gather": - final BrowserDB db = BrowserDB.from(getProfile()); - final ContentResolver cr = getContentResolver(); - Telemetry.addToHistogram("PLACES_PAGES_COUNT", db.getCount(cr, "history")); - Telemetry.addToHistogram("FENNEC_BOOKMARKS_COUNT", db.getCount(cr, "bookmarks")); - Telemetry.addToHistogram("BROWSER_IS_USER_DEFAULT", (isDefaultBrowser(Intent.ACTION_VIEW) ? 1 : 0)); - Telemetry.addToHistogram("FENNEC_CUSTOM_HOMEPAGE", (TextUtils.isEmpty(getHomepage()) ? 0 : 1)); - final SharedPreferences prefs = GeckoSharedPrefs.forProfile(getContext()); - final boolean hasCustomHomepanels = - prefs.contains(HomeConfigPrefsBackend.PREFS_CONFIG_KEY) || prefs.contains(HomeConfigPrefsBackend.PREFS_CONFIG_KEY_OLD); - Telemetry.addToHistogram("FENNEC_HOMEPANELS_CUSTOM", hasCustomHomepanels ? 1 : 0); - - Telemetry.addToHistogram("FENNEC_READER_VIEW_CACHE_SIZE", - SavedReaderViewHelper.getSavedReaderViewHelper(getContext()).getDiskSpacedUsedKB()); - - if (Versions.feature16Plus) { - Telemetry.addToHistogram("BROWSER_IS_ASSIST_DEFAULT", (isDefaultBrowser(Intent.ACTION_ASSIST) ? 1 : 0)); - } - - Telemetry.addToHistogram("FENNEC_ORBOT_INSTALLED", - ContextUtils.isPackageInstalled(getContext(), "org.torproject.android") ? 1 : 0); - break; - - case "Updater:Launch": - handleUpdaterLaunch(); - break; - - case "Download:AndroidDownloadManager": - // Downloading via Android's download manager - - final String uri = message.getString("uri"); - final String filename = message.getString("filename"); - final String mimeType = message.getString("mimeType"); - - final DownloadManager.Request request = new DownloadManager.Request(Uri.parse(uri)); - request.setMimeType(mimeType); - - try { - request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, filename); - } catch (IllegalStateException e) { - Log.e(LOGTAG, "Cannot create download directory"); - return; - } - - request.allowScanningByMediaScanner(); - request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED); - request.addRequestHeader("User-Agent", HardwareUtils.isTablet() ? - AppConstants.USER_AGENT_FENNEC_TABLET : - AppConstants.USER_AGENT_FENNEC_MOBILE); - - try { - DownloadManager manager = (DownloadManager) getSystemService(Context.DOWNLOAD_SERVICE); - manager.enqueue(request); - - Log.d(LOGTAG, "Enqueued download (Download Manager)"); - } catch (RuntimeException e) { - Log.e(LOGTAG, "Download failed: " + e); - } - break; - - case "Website:Metadata": - final NativeJSObject metadata = message.getObject("metadata"); - final String location = message.getString("location"); - - final boolean hasImage = !TextUtils.isEmpty(metadata.optString("image_url", null)); - final String metadataJSON = metadata.toString(); - - ThreadUtils.postToBackgroundThread(new Runnable() { - @Override - public void run() { - final ContentProviderClient contentProviderClient = getContentResolver() - .acquireContentProviderClient(BrowserContract.PageMetadata.CONTENT_URI); - if (contentProviderClient == null) { - Log.w(LOGTAG, "Failed to obtain content provider client for: " + BrowserContract.PageMetadata.CONTENT_URI); - return; - } - try { - GlobalPageMetadata.getInstance().add( - BrowserDB.from(getProfile()), - contentProviderClient, - location, hasImage, metadataJSON); - } finally { - contentProviderClient.release(); - } - } - }); - - break; - - default: - super.handleMessage(event, message, callback); - break; - } - } - - private void getFaviconFromCache(final EventCallback callback, final String url) { - Icons.with(this) - .pageUrl(url) - .skipNetwork() - .executeCallbackOnBackgroundThread() - .build() - .execute(new IconCallback() { - @Override - public void onIconResponse(IconResponse response) { - ByteArrayOutputStream out = null; - Base64OutputStream b64 = null; - - try { - out = new ByteArrayOutputStream(); - out.write("data:image/png;base64,".getBytes()); - b64 = new Base64OutputStream(out, Base64.NO_WRAP); - response.getBitmap().compress(Bitmap.CompressFormat.PNG, 100, b64); - callback.sendSuccess(new String(out.toByteArray())); - } catch (IOException e) { - Log.w(LOGTAG, "Failed to convert to base64 data URI"); - callback.sendError("Failed to convert favicon to a base64 data URI"); - } finally { - try { - if (out != null) { - out.close(); - } - if (b64 != null) { - b64.close(); - } - } catch (IOException e) { - Log.w(LOGTAG, "Failed to close the streams"); - } - } - } - }); - } - - /** - * Use a dummy Intent to do a default browser check. - * - * @return true if this package is the default browser on this device, false otherwise. - */ - private boolean isDefaultBrowser(String action) { - final Intent viewIntent = new Intent(action, Uri.parse("http://www.mozilla.org")); - final ResolveInfo info = getPackageManager().resolveActivity(viewIntent, PackageManager.MATCH_DEFAULT_ONLY); - if (info == null) { - // No default is set - return false; - } - - final String packageName = info.activityInfo.packageName; - return (TextUtils.equals(packageName, getPackageName())); - } - - @Override - public void handleMessage(String event, JSONObject message) { - try { - switch (event) { - case "Menu:Open": - if (mBrowserToolbar.isEditing()) { - mBrowserToolbar.cancelEdit(); - } - - openOptionsMenu(); - break; - - case "Menu:Update": - final int id = message.getInt("id") + ADDON_MENU_OFFSET; - final JSONObject options = message.getJSONObject("options"); - ThreadUtils.postToUiThread(new Runnable() { - @Override - public void run() { - updateAddonMenuItem(id, options); - } - }); - break; - - case "Gecko:DelayedStartup": - ThreadUtils.postToUiThread(new Runnable() { - @Override - public void run() { - // Force tabs panel inflation once the initial - // pageload is finished. - ensureTabsPanelExists(); - if (AppConstants.NIGHTLY_BUILD && mZoomedView == null) { - ViewStub stub = (ViewStub) findViewById(R.id.zoomed_view_stub); - mZoomedView = (ZoomedView) stub.inflate(); - } - } - }); - - if (AppConstants.MOZ_MEDIA_PLAYER) { - // Check if the fragment is already added. This should never be true here, but this is - // a nice safety check. - // If casting is disabled, these classes aren't built. We use reflection to initialize them. - final Class mediaManagerClass = getMediaPlayerManager(); - - if (mediaManagerClass != null) { - try { - final String tag = ""; - mediaManagerClass.getDeclaredField("MEDIA_PLAYER_TAG").get(tag); - Log.i(LOGTAG, "Found tag " + tag); - final Fragment frag = getSupportFragmentManager().findFragmentByTag(tag); - if (frag == null) { - final Method getInstance = mediaManagerClass.getMethod("getInstance", (Class[]) null); - final Fragment mpm = (Fragment) getInstance.invoke(null); - getSupportFragmentManager().beginTransaction().disallowAddToBackStack().add(mpm, tag).commit(); - } - } catch (Exception ex) { - Log.e(LOGTAG, "Error initializing media manager", ex); - } - } - } - - if (AppConstants.MOZ_STUMBLER_BUILD_TIME_ENABLED && Restrictions.isAllowed(this, Restrictable.DATA_CHOICES)) { - // Start (this acts as ping if started already) the stumbler lib; if the stumbler has queued data it will upload it. - // Stumbler operates on its own thread, and startup impact is further minimized by delaying work (such as upload) a few seconds. - // Avoid any potential startup CPU/thread contention by delaying the pref broadcast. - final long oneSecondInMillis = 1000; - ThreadUtils.getBackgroundHandler().postDelayed(new Runnable() { - @Override - public void run() { - GeckoPreferences.broadcastStumblerPref(BrowserApp.this); - } - }, oneSecondInMillis); - } - - if (AppConstants.MOZ_ANDROID_DOWNLOAD_CONTENT_SERVICE) { - // TODO: Better scheduling of sync action (Bug 1257492) - DownloadContentService.startSync(this); - - DownloadContentService.startVerification(this); - } - - FeedService.setup(this); - - super.handleMessage(event, message); - break; - - case "Gecko:Ready": - // Handle this message in GeckoApp, but also enable the Settings - // menuitem, which is specific to BrowserApp. - super.handleMessage(event, message); - final Menu menu = mMenu; - ThreadUtils.postToUiThread(new Runnable() { - @Override - public void run() { - if (menu != null) { - menu.findItem(R.id.settings).setEnabled(true); - menu.findItem(R.id.help).setEnabled(true); - } - } - }); - - // Display notification for Mozilla data reporting, if data should be collected. - if (AppConstants.MOZ_DATA_REPORTING && Restrictions.isAllowed(this, Restrictable.DATA_CHOICES)) { - DataReportingNotification.checkAndNotifyPolicy(GeckoAppShell.getContext()); - } - break; - - case "Search:Keyword": - storeSearchQuery(message.getString("query")); - recordSearch(GeckoSharedPrefs.forProfile(this), message.getString("identifier"), - TelemetryContract.Method.ACTIONBAR); - break; - - case "LightweightTheme:Update": - ThreadUtils.postToUiThread(new Runnable() { - @Override - public void run() { - mDynamicToolbar.setVisible(true, VisibilityTransition.ANIMATE); - } - }); - break; - - case "Video:Play": - if (SwitchBoard.isInExperiment(this, Experiments.HLS_VIDEO_PLAYBACK)) { - final String uri = message.getString("uri"); - final String uuid = message.getString("uuid"); - ThreadUtils.postToUiThread(new Runnable() { - @Override - public void run() { - mVideoPlayer.start(Uri.parse(uri)); - Telemetry.sendUIEvent(TelemetryContract.Event.SHOW, TelemetryContract.Method.CONTENT, "playhls"); - } - }); - } - break; - - case "Prompt:ShowTop": - // Bring this activity to front so the prompt is visible.. - Intent bringToFrontIntent = new Intent(); - bringToFrontIntent.setClassName(AppConstants.ANDROID_PACKAGE_NAME, AppConstants.MOZ_ANDROID_BROWSER_INTENT_CLASS); - bringToFrontIntent.setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT); - startActivity(bringToFrontIntent); - break; - - case "Tab:Added": - if (message.getBoolean("cancelEditMode")) { - ThreadUtils.postToUiThread(new Runnable() { - @Override - public void run() { - // Set the target tab to null so it does not get selected (on editing - // mode exit) in lieu of the tab that we're going to open and select. - mTargetTabForEditingMode = null; - mBrowserToolbar.cancelEdit(); - } - }); - } - break; - - default: - super.handleMessage(event, message); - break; - } - } catch (Exception e) { - Log.e(LOGTAG, "Exception handling message \"" + event + "\":", e); - } - } - - @Override - public void addTab() { - Tabs.getInstance().addTab(); - } - - @Override - public void addPrivateTab() { - Tabs.getInstance().addPrivateTab(); - } - - public void showTrackingProtectionPromptIfApplicable() { - final SharedPreferences prefs = getSharedPreferences(); - - final boolean hasTrackingProtectionPromptBeShownBefore = prefs.getBoolean(GeckoPreferences.PREFS_TRACKING_PROTECTION_PROMPT_SHOWN, false); - - if (hasTrackingProtectionPromptBeShownBefore) { - return; - } - - prefs.edit().putBoolean(GeckoPreferences.PREFS_TRACKING_PROTECTION_PROMPT_SHOWN, true).apply(); - - startActivity(new Intent(BrowserApp.this, TrackingProtectionPrompt.class)); - } - - @Override - public void showNormalTabs() { - showTabs(TabsPanel.Panel.NORMAL_TABS); - } - - @Override - public void showPrivateTabs() { - showTabs(TabsPanel.Panel.PRIVATE_TABS); - } - /** - * Ensure the TabsPanel view is properly inflated and returns - * true when the view has been inflated, false otherwise. - */ - private boolean ensureTabsPanelExists() { - if (mTabsPanel != null) { - return false; - } - - ViewStub tabsPanelStub = (ViewStub) findViewById(R.id.tabs_panel); - mTabsPanel = (TabsPanel) tabsPanelStub.inflate(); - - mTabsPanel.setTabsLayoutChangeListener(this); - - return true; - } - - private void showTabs(final TabsPanel.Panel panel) { - if (Tabs.getInstance().getDisplayCount() == 0) - return; - - hideFirstrunPager(TelemetryContract.Method.BUTTON); - - if (ensureTabsPanelExists()) { - // If we've just inflated the tabs panel, only show it once the current - // layout pass is done to avoid displayed temporary UI states during - // relayout. - ViewTreeObserver vto = mTabsPanel.getViewTreeObserver(); - if (vto.isAlive()) { - vto.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { - @Override - public void onGlobalLayout() { - mTabsPanel.getViewTreeObserver().removeGlobalOnLayoutListener(this); - showTabs(panel); - } - }); - } - } else { - if (mDoorHangerPopup != null) { - mDoorHangerPopup.disable(); - } - mTabsPanel.show(panel); - - // Hide potentially visible "find in page" bar (Bug 1177338) - mFindInPageBar.hide(); - - for (final BrowserAppDelegate delegate : delegates) { - delegate.onTabsTrayShown(this, mTabsPanel); - } - } - } - - @Override - public void hideTabs() { - mTabsPanel.hide(); - if (mDoorHangerPopup != null) { - mDoorHangerPopup.enable(); - } - - for (final BrowserAppDelegate delegate : delegates) { - delegate.onTabsTrayHidden(this, mTabsPanel); - } - } - - @Override - public boolean autoHideTabs() { - if (areTabsShown()) { - hideTabs(); - return true; - } - return false; - } - - @Override - public boolean areTabsShown() { - return (mTabsPanel != null && mTabsPanel.isShown()); - } - - @Override - public String getHomepage() { - final SharedPreferences preferences = GeckoSharedPrefs.forProfile(this); - final String homepagePreference = preferences.getString(GeckoPreferences.PREFS_HOMEPAGE, null); - - final boolean readFromPartnerProvider = preferences.getBoolean( - GeckoPreferences.PREFS_READ_PARTNER_CUSTOMIZATIONS_PROVIDER, false); - - if (!readFromPartnerProvider) { - // Just return homepage as set by the user (or null). - return homepagePreference; - } - - - final String homepagePrevious = preferences.getString(GeckoPreferences.PREFS_HOMEPAGE_PARTNER_COPY, null); - if (homepagePrevious != null && !homepagePrevious.equals(homepagePreference)) { - // We have read the homepage once and the user has changed it since then. Just use the - // value the user has set. - return homepagePreference; - } - - // This is the first time we read the partner provider or the value has not been altered by the user - final String homepagePartner = PartnerBrowserCustomizationsClient.getHomepage(this); - - if (homepagePartner == null) { - // We didn't get anything from the provider. Let's just use what we have locally. - return homepagePreference; - } - - if (!homepagePartner.equals(homepagePrevious)) { - // We have a new value. Update the preferences. - preferences.edit() - .putString(GeckoPreferences.PREFS_HOMEPAGE, homepagePartner) - .putString(GeckoPreferences.PREFS_HOMEPAGE_PARTNER_COPY, homepagePartner) - .apply(); - } - - return homepagePartner; - } - - @TargetApi(Build.VERSION_CODES.JELLY_BEAN) - @Override - public void onTabsLayoutChange(int width, int height) { - int animationLength = TABS_ANIMATION_DURATION; - - if (mMainLayoutAnimator != null) { - animationLength = Math.max(1, animationLength - (int)mMainLayoutAnimator.getRemainingTime()); - mMainLayoutAnimator.stop(false); - } - - if (areTabsShown()) { - mTabsPanel.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS); - // Hide the web content from accessibility tools even though it's visible - // so that you can't examine it as long as the tabs are being shown. - if (Versions.feature16Plus) { - mLayerView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO); - } - } else { - if (Versions.feature16Plus) { - mLayerView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES); - } - } - - mMainLayoutAnimator = new PropertyAnimator(animationLength, sTabsInterpolator); - mMainLayoutAnimator.addPropertyAnimationListener(this); - mMainLayoutAnimator.attach(mMainLayout, - PropertyAnimator.Property.SCROLL_Y, - -height); - - mTabsPanel.prepareTabsAnimation(mMainLayoutAnimator); - mBrowserToolbar.triggerTabsPanelTransition(mMainLayoutAnimator, areTabsShown()); - - // If the tabs panel is animating onto the screen, pin the dynamic - // toolbar. - if (mDynamicToolbar.isEnabled()) { - if (width > 0 && height > 0) { - mDynamicToolbar.setPinned(true, PinReason.RELAYOUT); - mDynamicToolbar.setVisible(true, VisibilityTransition.ANIMATE); - } else { - mDynamicToolbar.setPinned(false, PinReason.RELAYOUT); - } - } - - mMainLayoutAnimator.start(); - } - - @Override - public void onPropertyAnimationStart() { - } - - @Override - public void onPropertyAnimationEnd() { - if (!areTabsShown()) { - mTabsPanel.setVisibility(View.INVISIBLE); - mTabsPanel.setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS); - } else { - // Cancel editing mode to return to page content when the TabsPanel closes. We cancel - // it here because there are graphical glitches if it's canceled while it's visible. - mBrowserToolbar.cancelEdit(); - } - - mTabsPanel.finishTabsAnimation(); - - mMainLayoutAnimator = null; - } - - @Override - public void onSaveInstanceState(Bundle outState) { - super.onSaveInstanceState(outState); - mDynamicToolbar.onSaveInstanceState(outState); - outState.putInt(STATE_ABOUT_HOME_TOP_PADDING, mHomeScreenContainer.getPaddingTop()); - } - - /** - * Attempts to switch to an open tab with the given URL. - *

- * If the tab exists, this method cancels any in-progress editing as well as - * calling {@link Tabs#selectTab(int)}. - * - * @param url of tab to switch to. - * @param flags to obey: if {@link OnUrlOpenListener.Flags#ALLOW_SWITCH_TO_TAB} - * is not present, return false. - * @return true if we successfully switched to a tab, false otherwise. - */ - private boolean maybeSwitchToTab(String url, EnumSet flags) { - if (!flags.contains(OnUrlOpenListener.Flags.ALLOW_SWITCH_TO_TAB)) { - return false; - } - - final Tabs tabs = Tabs.getInstance(); - final Tab tab; - - if (AboutPages.isAboutReader(url)) { - tab = tabs.getFirstReaderTabForUrl(url, tabs.getSelectedTab().isPrivate()); - } else { - tab = tabs.getFirstTabForUrl(url, tabs.getSelectedTab().isPrivate()); - } - - if (tab == null) { - return false; - } - - return maybeSwitchToTab(tab.getId()); - } - - /** - * Attempts to switch to an open tab with the given unique tab ID. - *

- * If the tab exists, this method cancels any in-progress editing as well as - * calling {@link Tabs#selectTab(int)}. - * - * @param id of tab to switch to. - * @return true if we successfully switched to the tab, false otherwise. - */ - private boolean maybeSwitchToTab(int id) { - final Tabs tabs = Tabs.getInstance(); - final Tab tab = tabs.getTab(id); - - if (tab == null) { - return false; - } - - final Tab oldTab = tabs.getSelectedTab(); - if (oldTab != null) { - oldTab.setIsEditing(false); - } - - // Set the target tab to null so it does not get selected (on editing - // mode exit) in lieu of the tab we are about to select. - mTargetTabForEditingMode = null; - tabs.selectTab(tab.getId()); - - mBrowserToolbar.cancelEdit(); - - return true; - } - - public void openUrlAndStopEditing(String url) { - openUrlAndStopEditing(url, null, false); - } - - private void openUrlAndStopEditing(String url, boolean newTab) { - openUrlAndStopEditing(url, null, newTab); - } - - private void openUrlAndStopEditing(String url, String searchEngine) { - openUrlAndStopEditing(url, searchEngine, false); - } - - private void openUrlAndStopEditing(String url, String searchEngine, boolean newTab) { - int flags = Tabs.LOADURL_NONE; - if (newTab) { - flags |= Tabs.LOADURL_NEW_TAB; - if (Tabs.getInstance().getSelectedTab().isPrivate()) { - flags |= Tabs.LOADURL_PRIVATE; - } - } - - Tabs.getInstance().loadUrl(url, searchEngine, -1, flags); - - mBrowserToolbar.cancelEdit(); - } - - private boolean isHomePagerVisible() { - return (mHomeScreen != null && mHomeScreen.isVisible() - && mHomeScreenContainer != null && mHomeScreenContainer.getVisibility() == View.VISIBLE); - } - - private boolean isFirstrunVisible() { - return (mFirstrunAnimationContainer != null && mFirstrunAnimationContainer.isVisible() - && mHomeScreenContainer != null && mHomeScreenContainer.getVisibility() == View.VISIBLE); - } - - /** - * Enters editing mode with the current tab's URL. There might be no - * tabs loaded by the time the user enters editing mode e.g. just after - * the app starts. In this case, we simply fallback to an empty URL. - */ - private void enterEditingMode() { - String url = ""; - String telemetryMsg = "urlbar-empty"; - - final Tab tab = Tabs.getInstance().getSelectedTab(); - if (tab != null) { - final String userSearchTerm = tab.getUserRequested(); - final String tabURL = tab.getURL(); - - // Check to see if there's a user-entered search term, - // which we save whenever the user performs a search. - if (!TextUtils.isEmpty(userSearchTerm)) { - url = userSearchTerm; - telemetryMsg = "urlbar-userentered"; - } else if (!TextUtils.isEmpty(tabURL)) { - url = tabURL; - telemetryMsg = "urlbar-url"; - } - } - - enterEditingMode(url); - Telemetry.sendUIEvent(TelemetryContract.Event.SHOW, TelemetryContract.Method.ACTIONBAR, telemetryMsg); - } - - /** - * Enters editing mode with the specified URL. If a null - * url is given, the empty String will be used instead. - */ - private void enterEditingMode(@NonNull String url) { - hideFirstrunPager(TelemetryContract.Method.ACTIONBAR); - - if (mBrowserToolbar.isEditing() || mBrowserToolbar.isAnimating()) { - return; - } - - final Tab selectedTab = Tabs.getInstance().getSelectedTab(); - final String panelId; - if (selectedTab != null) { - mTargetTabForEditingMode = selectedTab.getId(); - panelId = selectedTab.getMostRecentHomePanel(); - } else { - mTargetTabForEditingMode = null; - panelId = null; - } - - final PropertyAnimator animator = new PropertyAnimator(250); - animator.setUseHardwareLayer(false); - - mBrowserToolbar.startEditing(url, animator); - - showHomePagerWithAnimator(panelId, null, animator); - - animator.start(); - Telemetry.startUISession(TelemetryContract.Session.AWESOMESCREEN); - } - - private void commitEditingMode() { - if (!mBrowserToolbar.isEditing()) { - return; - } - - Telemetry.stopUISession(TelemetryContract.Session.AWESOMESCREEN, - TelemetryContract.Reason.COMMIT); - - final String url = mBrowserToolbar.commitEdit(); - - // HACK: We don't know the url that will be loaded when hideHomePager is initially called - // in BrowserToolbar's onStopEditing listener so on the awesomescreen, hideHomePager will - // use the url "about:home" and return without taking any action. hideBrowserSearch is - // then called, but since hideHomePager changes both HomePager and LayerView visibility - // and exited without taking an action, no Views are displayed and graphical corruption is - // visible instead. - // - // Here we call hideHomePager for the second time with the URL to be loaded so that - // hideHomePager is called with the correct state for the upcoming page load. - // - // Expected to be fixed by bug 915825. - hideHomePager(url); - loadUrlOrKeywordSearch(url); - clearSelectedTabApplicationId(); - } - - private void clearSelectedTabApplicationId() { - final Tab selected = Tabs.getInstance().getSelectedTab(); - if (selected != null) { - selected.setApplicationId(null); - } - } - - private void loadUrlOrKeywordSearch(final String url) { - // Don't do anything if the user entered an empty URL. - if (TextUtils.isEmpty(url)) { - return; - } - - // If the URL doesn't look like a search query, just load it. - if (!StringUtils.isSearchQuery(url, true)) { - Tabs.getInstance().loadUrl(url, Tabs.LOADURL_USER_ENTERED); - Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.ACTIONBAR, "user"); - return; - } - - // Otherwise, check for a bookmark keyword. - final SharedPreferences sharedPrefs = GeckoSharedPrefs.forProfile(this); - final BrowserDB db = BrowserDB.from(getProfile()); - ThreadUtils.postToBackgroundThread(new Runnable() { - @Override - public void run() { - final String keyword; - final String keywordSearch; - - final int index = url.indexOf(" "); - if (index == -1) { - keyword = url; - keywordSearch = ""; - } else { - keyword = url.substring(0, index); - keywordSearch = url.substring(index + 1); - } - - final String keywordUrl = db.getUrlForKeyword(getContentResolver(), keyword); - - // If there isn't a bookmark keyword, load the url. This may result in a query - // using the default search engine. - if (TextUtils.isEmpty(keywordUrl)) { - Tabs.getInstance().loadUrl(url, Tabs.LOADURL_USER_ENTERED); - Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.ACTIONBAR, "user"); - return; - } - - // Otherwise, construct a search query from the bookmark keyword. - // Replace lower case bookmark keywords with URLencoded search query or - // replace upper case bookmark keywords with un-encoded search query. - // This makes it match the same behaviour as on Firefox for the desktop. - final String searchUrl = keywordUrl.replace("%s", URLEncoder.encode(keywordSearch)).replace("%S", keywordSearch); - - Tabs.getInstance().loadUrl(searchUrl, Tabs.LOADURL_USER_ENTERED); - Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, - TelemetryContract.Method.ACTIONBAR, - "keyword"); - } - }); - } - - /** - * Records in telemetry that a search has occurred. - * - * @param where where the search was started from - */ - private static void recordSearch(@NonNull final SharedPreferences prefs, @NonNull final String engineIdentifier, - @NonNull final TelemetryContract.Method where) { - // We could include the engine identifier as an extra but we'll - // just capture that with core ping telemetry (bug 1253319). - Telemetry.sendUIEvent(TelemetryContract.Event.SEARCH, where); - SearchCountMeasurements.incrementSearch(prefs, engineIdentifier, where.toString()); - } - - /** - * Store search query in SearchHistoryProvider. - * - * @param query - * a search query to store. We won't store empty queries. - */ - private void storeSearchQuery(final String query) { - if (TextUtils.isEmpty(query)) { - return; - } - - // Filter out URLs and long suggestions - if (query.length() > 50 || Pattern.matches("^(https?|ftp|file)://.*", query)) { - return; - } - - final GeckoProfile profile = getProfile(); - // Don't bother storing search queries in guest mode - if (profile.inGuestMode()) { - return; - } - - final BrowserDB db = BrowserDB.from(profile); - ThreadUtils.postToBackgroundThread(new Runnable() { - @Override - public void run() { - db.getSearches().insert(getContentResolver(), query); - } - }); - } - - void filterEditingMode(String searchTerm, AutocompleteHandler handler) { - if (TextUtils.isEmpty(searchTerm)) { - hideBrowserSearch(); - } else { - showBrowserSearch(); - mBrowserSearch.filter(searchTerm, handler); - } - } - - /** - * Selects the target tab for editing mode. This is expected to be the tab selected on editing - * mode entry, unless it is subsequently overridden. - * - * A background tab may be selected while editing mode is active (e.g. popups), causing the - * new url to load in the newly selected tab. Call this method on editing mode exit to - * mitigate this. - * - * Note that this method is disabled for new tablets because we can see the selected tab in the - * tab strip and, when the selected tab changes during editing mode as in this hack, the - * temporarily selected tab is visible to users. - */ - private void selectTargetTabForEditingMode() { - if (HardwareUtils.isTablet()) { - return; - } - - if (mTargetTabForEditingMode != null) { - Tabs.getInstance().selectTab(mTargetTabForEditingMode); - } - - mTargetTabForEditingMode = null; - } - - /** - * Shows or hides the home pager for the given tab. - */ - private void updateHomePagerForTab(Tab tab) { - // Don't change the visibility of the home pager if we're in editing mode. - if (mBrowserToolbar.isEditing()) { - return; - } - - // History will only store that we were visiting about:home, however the specific panel - // isn't stored. (We are able to navigate directly to homepanels using an about:home?panel=... - // URL, but the reverse doesn't apply: manually switching panels doesn't update the URL.) - // Hence we need to restore the panel, in addition to panel state, here. - if (isAboutHome(tab)) { - String panelId = AboutPages.getPanelIdFromAboutHomeUrl(tab.getURL()); - Bundle panelRestoreData = null; - if (panelId == null) { - // No panel was specified in the URL. Try loading the most recent - // home panel for this tab. - // Note: this isn't necessarily correct. We don't update the URL when we switch tabs. - // If a user explicitly navigated to about:reader?panel=FOO, and then switches - // to panel BAR, the history URL still contains FOO, and we restore to FOO. In most - // cases however we aren't supplying a panel ID in the URL so this code still works - // for most cases. - // We can't fix this directly since we can't ignore the panelId if we're explicitly - // loading a specific panel, and we currently can't distinguish between loading - // history, and loading new pages, see Bug 1268887 - panelId = tab.getMostRecentHomePanel(); - panelRestoreData = tab.getMostRecentHomePanelData(); - } else if (panelId.equals(HomeConfig.getIdForBuiltinPanelType(PanelType.DEPRECATED_RECENT_TABS))) { - // Redirect to the Combined History panel. - panelId = HomeConfig.getIdForBuiltinPanelType(PanelType.COMBINED_HISTORY); - panelRestoreData = new Bundle(); - // Jump directly to the Recent Tabs subview of the Combined History panel. - panelRestoreData.putBoolean("goToRecentTabs", true); - } - showHomePager(panelId, panelRestoreData); - - if (mDynamicToolbar.isEnabled()) { - mDynamicToolbar.setVisible(true, VisibilityTransition.ANIMATE); - } - } else { - hideHomePager(); - } - } - - @Override - public void onLocaleReady(final String locale) { - Log.d(LOGTAG, "onLocaleReady: " + locale); - super.onLocaleReady(locale); - - HomePanelsManager.getInstance().onLocaleReady(locale); - - if (mMenu != null) { - mMenu.clear(); - onCreateOptionsMenu(mMenu); - } - } - - @Override - public void onActivityResult(int requestCode, int resultCode, Intent data) { - Log.d(LOGTAG, "onActivityResult: " + requestCode + ", " + resultCode + ", " + data); - switch (requestCode) { - case ACTIVITY_REQUEST_PREFERENCES: - // We just returned from preferences. If our locale changed, - // we need to redisplay at this point, and do any other browser-level - // bookkeeping that we associate with a locale change. - if (resultCode != GeckoPreferences.RESULT_CODE_LOCALE_DID_CHANGE) { - Log.d(LOGTAG, "No locale change returning from preferences; nothing to do."); - return; - } - - ThreadUtils.postToBackgroundThread(new Runnable() { - @Override - public void run() { - final LocaleManager localeManager = BrowserLocaleManager.getInstance(); - final Locale locale = localeManager.getCurrentLocale(getApplicationContext()); - Log.d(LOGTAG, "Read persisted locale " + locale); - if (locale == null) { - return; - } - onLocaleChanged(Locales.getLanguageTag(locale)); - } - }); - break; - - case ACTIVITY_REQUEST_TAB_QUEUE: - TabQueueHelper.processTabQueuePromptResponse(resultCode, this); - break; - - default: - for (final BrowserAppDelegate delegate : delegates) { - delegate.onActivityResult(this, requestCode, resultCode, data); - } - - super.onActivityResult(requestCode, resultCode, data); - } - } - - private void showFirstrunPager() { - if (Experiments.isInExperimentLocal(getContext(), Experiments.ONBOARDING3_A)) { - Telemetry.startUISession(TelemetryContract.Session.EXPERIMENT, Experiments.ONBOARDING3_A); - GeckoSharedPrefs.forProfile(getContext()).edit().putString(Experiments.PREF_ONBOARDING_VERSION, Experiments.ONBOARDING3_A).apply(); - Telemetry.stopUISession(TelemetryContract.Session.EXPERIMENT, Experiments.ONBOARDING3_A); - return; - } - - if (mFirstrunAnimationContainer == null) { - final ViewStub firstrunPagerStub = (ViewStub) findViewById(R.id.firstrun_pager_stub); - mFirstrunAnimationContainer = (FirstrunAnimationContainer) firstrunPagerStub.inflate(); - mFirstrunAnimationContainer.load(getApplicationContext(), getSupportFragmentManager()); - mFirstrunAnimationContainer.registerOnFinishListener(new FirstrunAnimationContainer.OnFinishListener() { - @Override - public void onFinish() { - if (mFirstrunAnimationContainer.showBrowserHint() && - TextUtils.isEmpty(getHomepage())) { - enterEditingMode(); - } - } - }); - } - - mHomeScreenContainer.setVisibility(View.VISIBLE); - } - - private void showHomePager(String panelId, Bundle panelRestoreData) { - showHomePagerWithAnimator(panelId, panelRestoreData, null); - } - - private void showHomePagerWithAnimator(String panelId, Bundle panelRestoreData, PropertyAnimator animator) { - if (isHomePagerVisible()) { - // Home pager already visible, make sure it shows the correct panel. - mHomeScreen.showPanel(panelId, panelRestoreData); - return; - } - - // This must be called before the dynamic toolbar is set visible because it calls - // FormAssistPopup.onMetricsChanged, which queues a runnable that undoes the effect of hide. - // With hide first, onMetricsChanged will return early instead. - mFormAssistPopup.hide(); - mFindInPageBar.hide(); - - // Refresh toolbar height to possibly restore the toolbar padding - refreshToolbarHeight(); - - // Show the toolbar before hiding about:home so the - // onMetricsChanged callback still works. - if (mDynamicToolbar.isEnabled()) { - mDynamicToolbar.setVisible(true, VisibilityTransition.IMMEDIATE); - } - - if (mHomeScreen == null) { - if (ActivityStream.isEnabled(this) && - !ActivityStream.isHomePanel()) { - final ViewStub asStub = (ViewStub) findViewById(R.id.activity_stream_stub); - mHomeScreen = (HomeScreen) asStub.inflate(); - } else { - final ViewStub homePagerStub = (ViewStub) findViewById(R.id.home_pager_stub); - mHomeScreen = (HomeScreen) homePagerStub.inflate(); - - // For now these listeners are HomePager specific. In future we might want - // to have a more abstracted data storage, with one Bundle containing all - // relevant restore data. - mHomeScreen.setOnPanelChangeListener(new HomeScreen.OnPanelChangeListener() { - @Override - public void onPanelSelected(String panelId) { - final Tab currentTab = Tabs.getInstance().getSelectedTab(); - if (currentTab != null) { - currentTab.setMostRecentHomePanel(panelId); - } - } - }); - - // Set this listener to persist restore data (via the Tab) every time panel state changes. - mHomeScreen.setPanelStateChangeListener(new HomeFragment.PanelStateChangeListener() { - @Override - public void onStateChanged(Bundle bundle) { - final Tab currentTab = Tabs.getInstance().getSelectedTab(); - if (currentTab != null) { - currentTab.setMostRecentHomePanelData(bundle); - } - } - - @Override - public void setCachedRecentTabsCount(int count) { - mCachedRecentTabsCount = count; - } - - @Override - public int getCachedRecentTabsCount() { - return mCachedRecentTabsCount; - } - }); - } - - // Don't show the banner in guest mode. - if (!Restrictions.isUserRestricted()) { - final ViewStub homeBannerStub = (ViewStub) findViewById(R.id.home_banner_stub); - final HomeBanner homeBanner = (HomeBanner) homeBannerStub.inflate(); - mHomeScreen.setBanner(homeBanner); - - // Remove the banner from the view hierarchy if it is dismissed. - homeBanner.setOnDismissListener(new HomeBanner.OnDismissListener() { - @Override - public void onDismiss() { - mHomeScreen.setBanner(null); - mHomeScreenContainer.removeView(homeBanner); - } - }); - } - } - - mHomeScreenContainer.setVisibility(View.VISIBLE); - mHomeScreen.load(getSupportLoaderManager(), - getSupportFragmentManager(), - panelId, - panelRestoreData, - animator); - - // Hide the web content so it cannot be focused by screen readers. - hideWebContentOnPropertyAnimationEnd(animator); - } - - private void hideWebContentOnPropertyAnimationEnd(final PropertyAnimator animator) { - if (animator == null) { - hideWebContent(); - return; - } - - animator.addPropertyAnimationListener(new PropertyAnimator.PropertyAnimationListener() { - @Override - public void onPropertyAnimationStart() { - mHideWebContentOnAnimationEnd = true; - } - - @Override - public void onPropertyAnimationEnd() { - if (mHideWebContentOnAnimationEnd) { - hideWebContent(); - } - } - }); - } - - private void hideWebContent() { - // The view is set to INVISIBLE, rather than GONE, to avoid - // the additional requestLayout() call. - mLayerView.setVisibility(View.INVISIBLE); - } - - /** - * Hide the Onboarding pager on user action, and don't show any onFinish hints. - * @param method TelemetryContract method by which action was taken - * @return boolean of whether pager was visible - */ - private boolean hideFirstrunPager(TelemetryContract.Method method) { - if (!isFirstrunVisible()) { - return false; - } - - Telemetry.sendUIEvent(TelemetryContract.Event.CANCEL, method, "firstrun-pane"); - - // Don't show any onFinish actions when hiding from this Activity. - mFirstrunAnimationContainer.registerOnFinishListener(null); - mFirstrunAnimationContainer.hide(); - return true; - } - - /** - * Hides the HomePager, using the url of the currently selected tab as the url to be - * loaded. - */ - private void hideHomePager() { - final Tab selectedTab = Tabs.getInstance().getSelectedTab(); - final String url = (selectedTab != null) ? selectedTab.getURL() : null; - - hideHomePager(url); - } - - /** - * Hides the HomePager. The given url should be the url of the page to be loaded, or null - * if a new page is not being loaded. - */ - private void hideHomePager(final String url) { - if (!isHomePagerVisible() || AboutPages.isAboutHome(url)) { - return; - } - - // Prevent race in hiding web content - see declaration for more info. - mHideWebContentOnAnimationEnd = false; - - // Display the previously hidden web content (which prevented screen reader access). - mLayerView.setVisibility(View.VISIBLE); - mHomeScreenContainer.setVisibility(View.GONE); - - if (mHomeScreen != null) { - mHomeScreen.unload(); - } - - mBrowserToolbar.setNextFocusDownId(R.id.layer_view); - - // Refresh toolbar height to possibly restore the toolbar padding - refreshToolbarHeight(); - } - - private void showBrowserSearchAfterAnimation(PropertyAnimator animator) { - if (animator == null) { - showBrowserSearch(); - return; - } - - animator.addPropertyAnimationListener(new PropertyAnimator.PropertyAnimationListener() { - @Override - public void onPropertyAnimationStart() { - } - - @Override - public void onPropertyAnimationEnd() { - showBrowserSearch(); - } - }); - } - - private void showBrowserSearch() { - if (mBrowserSearch.getUserVisibleHint()) { - return; - } - - mBrowserSearchContainer.setVisibility(View.VISIBLE); - - // Prevent overdraw by hiding the underlying web content and HomePager View - hideWebContent(); - mHomeScreenContainer.setVisibility(View.INVISIBLE); - - final FragmentManager fm = getSupportFragmentManager(); - - // In certain situations, showBrowserSearch() can be called immediately after hideBrowserSearch() - // (see bug 925012). Because of an Android bug (http://code.google.com/p/android/issues/detail?id=61179), - // calling FragmentTransaction#add immediately after FragmentTransaction#remove won't add the fragment's - // view to the layout. Calling FragmentManager#executePendingTransactions before re-adding the fragment - // prevents this issue. - fm.executePendingTransactions(); - - Fragment f = fm.findFragmentById(R.id.search_container); - - // checking if fragment is already present - if (f != null) { - fm.beginTransaction().show(f).commitAllowingStateLoss(); - mBrowserSearch.resetScrollState(); - } else { - // add fragment if not already present - fm.beginTransaction().add(R.id.search_container, mBrowserSearch, BROWSER_SEARCH_TAG).commitAllowingStateLoss(); - } - mBrowserSearch.setUserVisibleHint(true); - - // We want to adjust the window size when the keyboard appears to bring the - // SearchEngineBar above the keyboard. However, adjusting the window size - // when hiding the keyboard results in graphical glitches where the keyboard was - // because nothing was being drawn underneath (bug 933422). This can be - // prevented drawing content under the keyboard (i.e. in the Window). - // - // We do this here because there are glitches when unlocking a device with - // BrowserSearch in the foreground if we use BrowserSearch.onStart/Stop. - getActivity().getWindow().setBackgroundDrawableResource(android.R.color.white); - } - - private void hideBrowserSearch() { - if (!mBrowserSearch.getUserVisibleHint()) { - return; - } - - // To prevent overdraw, the HomePager is hidden when BrowserSearch is displayed: - // reverse that. - showHomePager(Tabs.getInstance().getSelectedTab().getMostRecentHomePanel(), - Tabs.getInstance().getSelectedTab().getMostRecentHomePanelData()); - - mBrowserSearchContainer.setVisibility(View.INVISIBLE); - - getSupportFragmentManager().beginTransaction() - .hide(mBrowserSearch).commitAllowingStateLoss(); - mBrowserSearch.setUserVisibleHint(false); - - getWindow().setBackgroundDrawable(null); - } - - /** - * Hides certain UI elements (e.g. button toast, tabs panel) when the - * user touches the main layout. - */ - private class HideOnTouchListener implements TouchEventInterceptor { - private boolean mIsHidingTabs; - private final Rect mTempRect = new Rect(); - - @Override - public boolean onInterceptTouchEvent(View view, MotionEvent event) { - if (event.getActionMasked() == MotionEvent.ACTION_DOWN) { - SnackbarBuilder.dismissCurrentSnackbar(); - } - - - - // We need to account for scroll state for the touched view otherwise - // tapping on an "empty" part of the view will still be considered a - // valid touch event. - if (view.getScrollX() != 0 || view.getScrollY() != 0) { - view.getHitRect(mTempRect); - mTempRect.offset(-view.getScrollX(), -view.getScrollY()); - - int[] viewCoords = new int[2]; - view.getLocationOnScreen(viewCoords); - - int x = (int) event.getRawX() - viewCoords[0]; - int y = (int) event.getRawY() - viewCoords[1]; - - if (!mTempRect.contains(x, y)) - return false; - } - - // If the tabs panel is showing, hide the tab panel and don't send the event to content. - if (event.getActionMasked() == MotionEvent.ACTION_DOWN && autoHideTabs()) { - mIsHidingTabs = true; - return true; - } - return false; - } - - @Override - public boolean onTouch(View view, MotionEvent event) { - if (mIsHidingTabs) { - // Keep consuming events until the gesture finishes. - int action = event.getActionMasked(); - if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) { - mIsHidingTabs = false; - } - return true; - } - return false; - } - } - - private static Menu findParentMenu(Menu menu, MenuItem item) { - final int itemId = item.getItemId(); - - final int count = (menu != null) ? menu.size() : 0; - for (int i = 0; i < count; i++) { - MenuItem menuItem = menu.getItem(i); - if (menuItem.getItemId() == itemId) { - return menu; - } - if (menuItem.hasSubMenu()) { - Menu parent = findParentMenu(menuItem.getSubMenu(), item); - if (parent != null) { - return parent; - } - } - } - - return null; - } - - /** - * Add the provided item to the provided menu, which should be - * the root (mMenu). - */ - private void addAddonMenuItemToMenu(final Menu menu, final MenuItemInfo info) { - info.added = true; - - final Menu destination; - if (info.parent == 0) { - destination = menu; - } else if (info.parent == GECKO_TOOLS_MENU) { - // The tools menu only exists in our -v11 resources. - final MenuItem tools = menu.findItem(R.id.tools); - destination = tools != null ? tools.getSubMenu() : menu; - } else { - final MenuItem parent = menu.findItem(info.parent); - if (parent == null) { - return; - } - - Menu parentMenu = findParentMenu(menu, parent); - - if (!parent.hasSubMenu()) { - parentMenu.removeItem(parent.getItemId()); - destination = parentMenu.addSubMenu(Menu.NONE, parent.getItemId(), Menu.NONE, parent.getTitle()); - if (parent.getIcon() != null) { - ((SubMenu) destination).getItem().setIcon(parent.getIcon()); - } - } else { - destination = parent.getSubMenu(); - } - } - - final MenuItem item = destination.add(Menu.NONE, info.id, Menu.NONE, info.label); - - item.setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() { - @Override - public boolean onMenuItemClick(MenuItem item) { - GeckoAppShell.notifyObservers("Menu:Clicked", Integer.toString(info.id - ADDON_MENU_OFFSET)); - return true; - } - }); - - item.setCheckable(info.checkable); - item.setChecked(info.checked); - item.setEnabled(info.enabled); - item.setVisible(info.visible); - } - - private void addAddonMenuItem(final MenuItemInfo info) { - if (mAddonMenuItemsCache == null) { - mAddonMenuItemsCache = new Vector(); - } - - // Mark it as added if the menu was ready. - info.added = (mMenu != null); - - // Always cache so we can rebuild after a locale switch. - mAddonMenuItemsCache.add(info); - - if (mMenu == null) { - return; - } - - addAddonMenuItemToMenu(mMenu, info); - } - - private void removeAddonMenuItem(int id) { - // Remove add-on menu item from cache, if available. - if (mAddonMenuItemsCache != null && !mAddonMenuItemsCache.isEmpty()) { - for (MenuItemInfo item : mAddonMenuItemsCache) { - if (item.id == id) { - mAddonMenuItemsCache.remove(item); - break; - } - } - } - - if (mMenu == null) - return; - - final MenuItem menuItem = mMenu.findItem(id); - if (menuItem != null) - mMenu.removeItem(id); - } - - private void updateAddonMenuItem(int id, JSONObject options) { - // Set attribute for the menu item in cache, if available - if (mAddonMenuItemsCache != null && !mAddonMenuItemsCache.isEmpty()) { - for (MenuItemInfo item : mAddonMenuItemsCache) { - if (item.id == id) { - item.label = options.optString("name", item.label); - item.checkable = options.optBoolean("checkable", item.checkable); - item.checked = options.optBoolean("checked", item.checked); - item.enabled = options.optBoolean("enabled", item.enabled); - item.visible = options.optBoolean("visible", item.visible); - item.added = (mMenu != null); - break; - } - } - } - - if (mMenu == null) { - return; - } - - final MenuItem menuItem = mMenu.findItem(id); - if (menuItem != null) { - menuItem.setTitle(options.optString("name", menuItem.getTitle().toString())); - menuItem.setCheckable(options.optBoolean("checkable", menuItem.isCheckable())); - menuItem.setChecked(options.optBoolean("checked", menuItem.isChecked())); - menuItem.setEnabled(options.optBoolean("enabled", menuItem.isEnabled())); - menuItem.setVisible(options.optBoolean("visible", menuItem.isVisible())); - } - } - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - // Sets mMenu = menu. - super.onCreateOptionsMenu(menu); - - // Inform the menu about the action-items bar. - if (menu instanceof GeckoMenu && - HardwareUtils.isTablet()) { - ((GeckoMenu) menu).setActionItemBarPresenter(mBrowserToolbar); - } - - MenuInflater inflater = getMenuInflater(); - inflater.inflate(R.menu.browser_app_menu, mMenu); - - // Add add-on menu items, if any exist. - if (mAddonMenuItemsCache != null && !mAddonMenuItemsCache.isEmpty()) { - for (MenuItemInfo item : mAddonMenuItemsCache) { - addAddonMenuItemToMenu(mMenu, item); - } - } - - // Action providers are available only ICS+. - GeckoMenuItem share = (GeckoMenuItem) mMenu.findItem(R.id.share); - - GeckoActionProvider provider = GeckoActionProvider.getForType(GeckoActionProvider.DEFAULT_MIME_TYPE, this); - - share.setActionProvider(provider); - - return true; - } - - @Override - public void openOptionsMenu() { - hideFirstrunPager(TelemetryContract.Method.MENU); - - // Disable menu access (for hardware buttons) when the software menu button is inaccessible. - // Note that the software button is always accessible on new tablet. - if (mBrowserToolbar.isEditing() && !HardwareUtils.isTablet()) { - return; - } - - if (ActivityUtils.isFullScreen(this)) { - return; - } - - if (areTabsShown()) { - mTabsPanel.showMenu(); - return; - } - - // Scroll custom menu to the top - if (mMenuPanel != null) - mMenuPanel.scrollTo(0, 0); - - // Scroll menu ListView (potentially in MenuPanel ViewGroup) to top. - if (mMenu instanceof GeckoMenu) { - ((GeckoMenu) mMenu).setSelection(0); - } - - if (!mBrowserToolbar.openOptionsMenu()) - super.openOptionsMenu(); - - if (mDynamicToolbar.isEnabled()) { - mDynamicToolbar.setVisible(true, VisibilityTransition.ANIMATE); - } - } - - @Override - public void closeOptionsMenu() { - if (!mBrowserToolbar.closeOptionsMenu()) - super.closeOptionsMenu(); - } - - @Override - public void setFullScreen(final boolean fullscreen) { - super.setFullScreen(fullscreen); - ThreadUtils.postToUiThread(new Runnable() { - @Override - public void run() { - if (fullscreen) { - if (mDynamicToolbar.isEnabled()) { - mDynamicToolbar.setVisible(false, VisibilityTransition.IMMEDIATE); - mDynamicToolbar.setPinned(true, PinReason.FULL_SCREEN); - } else { - setToolbarMargin(0); - } - mBrowserChrome.setVisibility(View.GONE); - } else { - mBrowserChrome.setVisibility(View.VISIBLE); - if (mDynamicToolbar.isEnabled()) { - mDynamicToolbar.setPinned(false, PinReason.FULL_SCREEN); - mDynamicToolbar.setVisible(true, VisibilityTransition.IMMEDIATE); - } else { - setToolbarMargin(mBrowserChrome.getHeight()); - } - } - } - }); - } - - @Override - public boolean onPrepareOptionsMenu(Menu aMenu) { - if (aMenu == null) - return false; - - // Hide the tab history panel when hardware menu button is pressed. - TabHistoryFragment frag = (TabHistoryFragment) getSupportFragmentManager().findFragmentByTag(TAB_HISTORY_FRAGMENT_TAG); - if (frag != null) { - frag.dismiss(); - } - - if (!GeckoThread.isRunning()) { - aMenu.findItem(R.id.settings).setEnabled(false); - aMenu.findItem(R.id.help).setEnabled(false); - } - - Tab tab = Tabs.getInstance().getSelectedTab(); - // Unlike other menu items, the bookmark star is not tinted. See {@link ThemedImageButton#setTintedDrawable}. - final MenuItem bookmark = aMenu.findItem(R.id.bookmark); - final MenuItem back = aMenu.findItem(R.id.back); - final MenuItem forward = aMenu.findItem(R.id.forward); - final MenuItem share = aMenu.findItem(R.id.share); - final MenuItem bookmarksList = aMenu.findItem(R.id.bookmarks_list); - final MenuItem historyList = aMenu.findItem(R.id.history_list); - final MenuItem saveAsPDF = aMenu.findItem(R.id.save_as_pdf); - final MenuItem print = aMenu.findItem(R.id.print); - final MenuItem charEncoding = aMenu.findItem(R.id.char_encoding); - final MenuItem findInPage = aMenu.findItem(R.id.find_in_page); - final MenuItem desktopMode = aMenu.findItem(R.id.desktop_mode); - final MenuItem enterGuestMode = aMenu.findItem(R.id.new_guest_session); - final MenuItem exitGuestMode = aMenu.findItem(R.id.exit_guest_session); - - // Only show the "Quit" menu item on pre-ICS, television devices, - // or if the user has explicitly enabled the clear on shutdown pref. - // (We check the pref last to save the pref read.) - // In ICS+, it's easy to kill an app through the task switcher. - final boolean visible = HardwareUtils.isTelevision() || - !PrefUtils.getStringSet(GeckoSharedPrefs.forProfile(this), - ClearOnShutdownPref.PREF, - new HashSet()).isEmpty(); - aMenu.findItem(R.id.quit).setVisible(visible); - - // If tab data is unavailable we disable most of the context menu and related items and - // return early. - if (tab == null || tab.getURL() == null) { - bookmark.setEnabled(false); - back.setEnabled(false); - forward.setEnabled(false); - share.setEnabled(false); - saveAsPDF.setEnabled(false); - print.setEnabled(false); - findInPage.setEnabled(false); - - // NOTE: Use MenuUtils.safeSetEnabled because some actions might - // be on the BrowserToolbar context menu. - MenuUtils.safeSetEnabled(aMenu, R.id.page, false); - MenuUtils.safeSetEnabled(aMenu, R.id.subscribe, false); - MenuUtils.safeSetEnabled(aMenu, R.id.add_search_engine, false); - MenuUtils.safeSetEnabled(aMenu, R.id.add_to_launcher, false); - - return true; - } - - // If tab data IS available we need to manually enable items as necessary. They may have - // been disabled if returning early above, hence every item must be toggled, even if it's - // always expected to be enabled (e.g. the bookmark star is always enabled, except when - // we don't have tab data). - - final boolean inGuestMode = GeckoProfile.get(this).inGuestMode(); - - bookmark.setEnabled(true); // Might have been disabled above, ensure it's reenabled - bookmark.setVisible(!inGuestMode); - bookmark.setCheckable(true); - bookmark.setChecked(tab.isBookmark()); - bookmark.setTitle(resolveBookmarkTitleID(tab.isBookmark())); - - // We don't use icons on GB builds so not resolving icons might conserve resources. - bookmark.setIcon(resolveBookmarkIconID(tab.isBookmark())); - - back.setEnabled(tab.canDoBack()); - forward.setEnabled(tab.canDoForward()); - desktopMode.setChecked(tab.getDesktopMode()); - - View backButtonView = MenuItemCompat.getActionView(back); - - if (backButtonView != null) { - backButtonView.setOnLongClickListener(new Button.OnLongClickListener() { - @Override - public boolean onLongClick(View view) { - Tab tab = Tabs.getInstance().getSelectedTab(); - if (tab != null) { - closeOptionsMenu(); - return tabHistoryController.showTabHistory(tab, - TabHistoryController.HistoryAction.BACK); - } - return false; - } - }); - } - - View forwardButtonView = MenuItemCompat.getActionView(forward); - - if (forwardButtonView != null) { - forwardButtonView.setOnLongClickListener(new Button.OnLongClickListener() { - @Override - public boolean onLongClick(View view) { - Tab tab = Tabs.getInstance().getSelectedTab(); - if (tab != null) { - closeOptionsMenu(); - return tabHistoryController.showTabHistory(tab, - TabHistoryController.HistoryAction.FORWARD); - } - return false; - } - }); - } - - String url = tab.getURL(); - if (AboutPages.isAboutReader(url)) { - url = ReaderModeUtils.stripAboutReaderUrl(url); - } - - // Disable share menuitem for about:, chrome:, file:, and resource: URIs - final boolean shareVisible = Restrictions.isAllowed(this, Restrictable.SHARE); - share.setVisible(shareVisible); - final boolean shareEnabled = StringUtils.isShareableUrl(url) && shareVisible; - share.setEnabled(shareEnabled); - MenuUtils.safeSetEnabled(aMenu, R.id.downloads, Restrictions.isAllowed(this, Restrictable.DOWNLOAD)); - - // NOTE: Use MenuUtils.safeSetEnabled because some actions might - // be on the BrowserToolbar context menu. - MenuUtils.safeSetEnabled(aMenu, R.id.page, !isAboutHome(tab)); - MenuUtils.safeSetEnabled(aMenu, R.id.subscribe, tab.hasFeeds()); - MenuUtils.safeSetEnabled(aMenu, R.id.add_search_engine, tab.hasOpenSearch()); - MenuUtils.safeSetEnabled(aMenu, R.id.add_to_launcher, !isAboutHome(tab)); - - // This provider also applies to the quick share menu item. - final GeckoActionProvider provider = ((GeckoMenuItem) share).getGeckoActionProvider(); - if (provider != null) { - Intent shareIntent = provider.getIntent(); - - // For efficiency, the provider's intent is only set once - if (shareIntent == null) { - shareIntent = new Intent(Intent.ACTION_SEND); - shareIntent.setType("text/plain"); - provider.setIntent(shareIntent); - } - - // Replace the existing intent's extras - shareIntent.putExtra(Intent.EXTRA_TEXT, url); - shareIntent.putExtra(Intent.EXTRA_SUBJECT, tab.getDisplayTitle()); - shareIntent.putExtra(Intent.EXTRA_TITLE, tab.getDisplayTitle()); - shareIntent.putExtra(ShareDialog.INTENT_EXTRA_DEVICES_ONLY, true); - - // Clear the existing thumbnail extras so we don't share an old thumbnail. - shareIntent.removeExtra("share_screenshot_uri"); - - // Include the thumbnail of the page being shared. - BitmapDrawable drawable = tab.getThumbnail(); - if (drawable != null) { - Bitmap thumbnail = drawable.getBitmap(); - - // Kobo uses a custom intent extra for sharing thumbnails. - if (Build.MANUFACTURER.equals("Kobo") && thumbnail != null) { - File cacheDir = getExternalCacheDir(); - - if (cacheDir != null) { - File outFile = new File(cacheDir, "thumbnail.png"); - - try { - final java.io.FileOutputStream out = new java.io.FileOutputStream(outFile); - try { - thumbnail.compress(Bitmap.CompressFormat.PNG, 90, out); - } finally { - try { - out.close(); - } catch (final IOException e) { /* Nothing to do here. */ } - } - } catch (FileNotFoundException e) { - Log.e(LOGTAG, "File not found", e); - } - - shareIntent.putExtra("share_screenshot_uri", Uri.parse(outFile.getPath())); - } - } - } - } - - final boolean privateTabVisible = Restrictions.isAllowed(this, Restrictable.PRIVATE_BROWSING); - MenuUtils.safeSetVisible(aMenu, R.id.new_private_tab, privateTabVisible); - - // Disable PDF generation (save and print) for about:home and xul pages. - boolean allowPDF = (!(isAboutHome(tab) || - tab.getContentType().equals("application/vnd.mozilla.xul+xml") || - tab.getContentType().startsWith("video/"))); - saveAsPDF.setEnabled(allowPDF); - print.setEnabled(allowPDF); - print.setVisible(Versions.feature19Plus); - - // Disable find in page for about:home, since it won't work on Java content. - findInPage.setEnabled(!isAboutHome(tab)); - - charEncoding.setVisible(GeckoPreferences.getCharEncodingState()); - - if (getProfile().inGuestMode()) { - exitGuestMode.setVisible(true); - } else { - enterGuestMode.setVisible(true); - } - - if (!Restrictions.isAllowed(this, Restrictable.GUEST_BROWSING)) { - MenuUtils.safeSetVisible(aMenu, R.id.new_guest_session, false); - } - - if (!Restrictions.isAllowed(this, Restrictable.INSTALL_EXTENSION)) { - MenuUtils.safeSetVisible(aMenu, R.id.addons, false); - } - - // Hide panel menu items if the panels themselves are hidden. - // If we don't know whether the panels are hidden, just show the menu items. - final SharedPreferences prefs = GeckoSharedPrefs.forProfile(getContext()); - bookmarksList.setVisible(prefs.getBoolean(HomeConfig.PREF_KEY_BOOKMARKS_PANEL_ENABLED, true)); - historyList.setVisible(prefs.getBoolean(HomeConfig.PREF_KEY_HISTORY_PANEL_ENABLED, true)); - - return true; - } - - private int resolveBookmarkIconID(final boolean isBookmark) { - if (isBookmark) { - return R.drawable.star_blue; - } else { - return R.drawable.ic_menu_bookmark_add; - } - } - - private int resolveBookmarkTitleID(final boolean isBookmark) { - return (isBookmark ? R.string.bookmark_remove : R.string.bookmark); - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - Tab tab = null; - Intent intent = null; - - final int itemId = item.getItemId(); - - // Track the menu action. We don't know much about the context, but we can use this to determine - // the frequency of use for various actions. - String extras = getResources().getResourceEntryName(itemId); - if (TextUtils.equals(extras, "new_private_tab")) { - // Mask private browsing - extras = "new_tab"; - } - Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.MENU, extras); - - mBrowserToolbar.cancelEdit(); - - if (itemId == R.id.bookmark) { - tab = Tabs.getInstance().getSelectedTab(); - if (tab != null) { - final String extra; - if (AboutPages.isAboutReader(tab.getURL())) { - extra = "bookmark_reader"; - } else { - extra = "bookmark"; - } - - if (item.isChecked()) { - Telemetry.sendUIEvent(TelemetryContract.Event.UNSAVE, TelemetryContract.Method.MENU, extra); - tab.removeBookmark(); - item.setTitle(resolveBookmarkTitleID(false)); - item.setIcon(resolveBookmarkIconID(false)); - } else { - Telemetry.sendUIEvent(TelemetryContract.Event.SAVE, TelemetryContract.Method.MENU, extra); - tab.addBookmark(); - item.setTitle(resolveBookmarkTitleID(true)); - item.setIcon(resolveBookmarkIconID(true)); - } - } - return true; - } - - if (itemId == R.id.share) { - tab = Tabs.getInstance().getSelectedTab(); - if (tab != null) { - String url = tab.getURL(); - if (url != null) { - url = ReaderModeUtils.stripAboutReaderUrl(url); - - // Context: Sharing via chrome list (no explicit session is active) - Telemetry.sendUIEvent(TelemetryContract.Event.SHARE, TelemetryContract.Method.LIST, "menu"); - - IntentHelper.openUriExternal(url, "text/plain", "", "", Intent.ACTION_SEND, tab.getDisplayTitle(), false); - } - } - return true; - } - - if (itemId == R.id.reload) { - tab = Tabs.getInstance().getSelectedTab(); - if (tab != null) - tab.doReload(false); - return true; - } - - if (itemId == R.id.back) { - tab = Tabs.getInstance().getSelectedTab(); - if (tab != null) - tab.doBack(); - return true; - } - - if (itemId == R.id.forward) { - tab = Tabs.getInstance().getSelectedTab(); - if (tab != null) - tab.doForward(); - return true; - } - - if (itemId == R.id.bookmarks_list) { - final String url = AboutPages.getURLForBuiltinPanelType(PanelType.BOOKMARKS); - Tabs.getInstance().loadUrl(url); - return true; - } - - if (itemId == R.id.history_list) { - final String url = AboutPages.getURLForBuiltinPanelType(PanelType.COMBINED_HISTORY); - Tabs.getInstance().loadUrl(url); - return true; - } - - if (itemId == R.id.save_as_pdf) { - Telemetry.sendUIEvent(TelemetryContract.Event.SAVE, TelemetryContract.Method.MENU, "pdf"); - GeckoAppShell.notifyObservers("SaveAs:PDF", null); - return true; - } - - if (itemId == R.id.print) { - Telemetry.sendUIEvent(TelemetryContract.Event.SAVE, TelemetryContract.Method.MENU, "print"); - PrintHelper.printPDF(this); - return true; - } - - if (itemId == R.id.settings) { - intent = new Intent(this, GeckoPreferences.class); - - // We want to know when the Settings activity returns, because - // we might need to redisplay based on a locale change. - startActivityForResult(intent, ACTIVITY_REQUEST_PREFERENCES); - return true; - } - - if (itemId == R.id.help) { - final String VERSION = AppConstants.MOZ_APP_VERSION; - final String OS = AppConstants.OS_TARGET; - final String LOCALE = Locales.getLanguageTag(Locale.getDefault()); - - final String URL = getResources().getString(R.string.help_link, VERSION, OS, LOCALE); - Tabs.getInstance().loadUrlInTab(URL); - return true; - } - - if (itemId == R.id.addons) { - Tabs.getInstance().loadUrlInTab(AboutPages.ADDONS); - return true; - } - - if (itemId == R.id.logins) { - Tabs.getInstance().loadUrlInTab(AboutPages.LOGINS); - return true; - } - - if (itemId == R.id.downloads) { - Tabs.getInstance().loadUrlInTab(AboutPages.DOWNLOADS); - return true; - } - - if (itemId == R.id.char_encoding) { - GeckoAppShell.notifyObservers("CharEncoding:Get", null); - return true; - } - - if (itemId == R.id.find_in_page) { - mFindInPageBar.show(); - return true; - } - - if (itemId == R.id.desktop_mode) { - Tab selectedTab = Tabs.getInstance().getSelectedTab(); - if (selectedTab == null) - return true; - JSONObject args = new JSONObject(); - try { - args.put("desktopMode", !item.isChecked()); - args.put("tabId", selectedTab.getId()); - } catch (JSONException e) { - Log.e(LOGTAG, "error building json arguments", e); - } - GeckoAppShell.notifyObservers("DesktopMode:Change", args.toString()); - return true; - } - - if (itemId == R.id.new_tab) { - addTab(); - return true; - } - - if (itemId == R.id.new_private_tab) { - addPrivateTab(); - return true; - } - - if (itemId == R.id.new_guest_session) { - showGuestModeDialog(GuestModeDialog.ENTERING); - return true; - } - - if (itemId == R.id.exit_guest_session) { - showGuestModeDialog(GuestModeDialog.LEAVING); - return true; - } - - // We have a few menu items that can also be in the context menu. If - // we have not already handled the item, give the context menu handler - // a chance. - if (onContextItemSelected(item)) { - return true; - } - - return super.onOptionsItemSelected(item); - } - - @Override - public boolean onMenuItemLongClick(MenuItem item) { - if (item.getItemId() == R.id.reload) { - Tab tab = Tabs.getInstance().getSelectedTab(); - if (tab != null) { - tab.doReload(true); - - Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.MENU, "reload_force"); - } - return true; - } - - return super.onMenuItemLongClick(item); - } - - public void showGuestModeDialog(final GuestModeDialog type) { - if ((type == GuestModeDialog.ENTERING) == getProfile().inGuestMode()) { - // Don't show enter dialog if we are already in guest mode; same with leaving. - return; - } - - final Prompt ps = new Prompt(this, new Prompt.PromptCallback() { - @Override - public void onPromptFinished(String result) { - try { - int itemId = new JSONObject(result).getInt("button"); - if (itemId == 0) { - final Context context = GeckoAppShell.getApplicationContext(); - if (type == GuestModeDialog.ENTERING) { - GeckoProfile.enterGuestMode(context); - } else { - GeckoProfile.leaveGuestMode(context); - // Now's a good time to make sure we're not displaying the - // Guest Browsing notification. - GuestSession.hideNotification(context); - } - doRestart(); - } - } catch (JSONException ex) { - Log.e(LOGTAG, "Exception reading guest mode prompt result", ex); - } - } - }); - - Resources res = getResources(); - ps.setButtons(new String[] { - res.getString(R.string.guest_session_dialog_continue), - res.getString(R.string.guest_session_dialog_cancel) - }); - - int titleString = 0; - int msgString = 0; - if (type == GuestModeDialog.ENTERING) { - titleString = R.string.new_guest_session_title; - msgString = R.string.new_guest_session_text; - } else { - titleString = R.string.exit_guest_session_title; - msgString = R.string.exit_guest_session_text; - } - - ps.show(res.getString(titleString), res.getString(msgString), null, ListView.CHOICE_MODE_NONE); - } - - /** - * Handle a long press on the back button - */ - private boolean handleBackLongPress() { - // If the tab search history is already shown, do nothing. - TabHistoryFragment frag = (TabHistoryFragment) getSupportFragmentManager().findFragmentByTag(TAB_HISTORY_FRAGMENT_TAG); - if (frag != null) { - return true; - } - - Tab tab = Tabs.getInstance().getSelectedTab(); - if (tab != null && !tab.isEditing()) { - return tabHistoryController.showTabHistory(tab, TabHistoryController.HistoryAction.ALL); - } - - return false; - } - - /** - * This will detect if the key pressed is back. If so, will show the history. - */ - @Override - public boolean onKeyLongPress(int keyCode, KeyEvent event) { - // onKeyLongPress is broken in Android N, see onKeyDown() for more information. We add a version - // check here to match our fallback code in order to avoid handling a long press twice (which - // could happen if newer versions of android and/or other vendors were to fix this problem). - if (Versions.preN && - keyCode == KeyEvent.KEYCODE_BACK) { - if (handleBackLongPress()) { - return true; - } - - } - return super.onKeyLongPress(keyCode, event); - } - - /* - * If the app has been launched a certain number of times, and we haven't asked for feedback before, - * open a new tab with about:feedback when launching the app from the icon shortcut. - */ - @Override - protected void onNewIntent(Intent externalIntent) { - final SafeIntent intent = new SafeIntent(externalIntent); - String action = intent.getAction(); - - final boolean isViewAction = Intent.ACTION_VIEW.equals(action); - final boolean isBookmarkAction = GeckoApp.ACTION_HOMESCREEN_SHORTCUT.equals(action); - final boolean isTabQueueAction = TabQueueHelper.LOAD_URLS_ACTION.equals(action); - final boolean isViewMultipleAction = ACTION_VIEW_MULTIPLE.equals(action); - - if (mInitialized && (isViewAction || isBookmarkAction)) { - // Dismiss editing mode if the user is loading a URL from an external app. - mBrowserToolbar.cancelEdit(); - - // Hide firstrun-pane if the user is loading a URL from an external app. - hideFirstrunPager(TelemetryContract.Method.NONE); - - if (isBookmarkAction) { - // GeckoApp.ACTION_HOMESCREEN_SHORTCUT means we're opening a bookmark that - // was added to Android's homescreen. - Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.HOMESCREEN); - } - } - - showTabQueuePromptIfApplicable(intent); - - // GeckoApp will wrap this unsafe external intent in a SafeIntent. - super.onNewIntent(externalIntent); - - if (AppConstants.MOZ_ANDROID_BEAM && NfcAdapter.ACTION_NDEF_DISCOVERED.equals(action)) { - String uri = intent.getDataString(); - mLayerView.loadUri(uri, GeckoView.LOAD_NEW_TAB); - } - - // Only solicit feedback when the app has been launched from the icon shortcut. - if (GuestSession.NOTIFICATION_INTENT.equals(action)) { - GuestSession.onNotificationIntentReceived(this); - } - - // If the user has clicked the tab queue notification then load the tabs. - if (TabQueueHelper.TAB_QUEUE_ENABLED && mInitialized && isTabQueueAction) { - Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.NOTIFICATION, "tabqueue"); - ThreadUtils.postToBackgroundThread(new Runnable() { - @Override - public void run() { - openQueuedTabs(); - } - }); - } - - // Custom intent action for opening multiple URLs at once - if (isViewMultipleAction) { - openMultipleTabsFromIntent(intent); - } - - for (final BrowserAppDelegate delegate : delegates) { - delegate.onNewIntent(this, intent); - } - - if (!mInitialized || !Intent.ACTION_MAIN.equals(action)) { - return; - } - - // Check to see how many times the app has been launched. - final String keyName = getPackageName() + ".feedback_launch_count"; - final StrictMode.ThreadPolicy savedPolicy = StrictMode.allowThreadDiskReads(); - - // Faster on main thread with an async apply(). - try { - SharedPreferences settings = getPreferences(Activity.MODE_PRIVATE); - int launchCount = settings.getInt(keyName, 0); - if (launchCount < FEEDBACK_LAUNCH_COUNT) { - // Increment the launch count and store the new value. - launchCount++; - settings.edit().putInt(keyName, launchCount).apply(); - - // If we've reached our magic number, show the feedback page. - if (launchCount == FEEDBACK_LAUNCH_COUNT) { - GeckoAppShell.notifyObservers("Feedback:Show", null); - } - } - } finally { - StrictMode.setThreadPolicy(savedPolicy); - } - } - - public void openUrls(List urls) { - try { - JSONArray array = new JSONArray(); - for (String url : urls) { - array.put(url); - } - - JSONObject object = new JSONObject(); - object.put("urls", array); - - GeckoAppShell.notifyObservers("Tabs:OpenMultiple", object.toString()); - } catch (JSONException e) { - Log.e(LOGTAG, "Unable to create JSON for opening multiple URLs"); - } - } - - private void showTabQueuePromptIfApplicable(final SafeIntent intent) { - ThreadUtils.postToBackgroundThread(new Runnable() { - @Override - public void run() { - // We only want to show the prompt if the browser has been opened from an external url - if (TabQueueHelper.TAB_QUEUE_ENABLED && mInitialized - && Intent.ACTION_VIEW.equals(intent.getAction()) - && !intent.getBooleanExtra(BrowserContract.SKIP_TAB_QUEUE_FLAG, false) - && TabQueueHelper.shouldShowTabQueuePrompt(BrowserApp.this)) { - Intent promptIntent = new Intent(BrowserApp.this, TabQueuePrompt.class); - startActivityForResult(promptIntent, ACTIVITY_REQUEST_TAB_QUEUE); - } - } - }); - } - - private void resetFeedbackLaunchCount() { - SharedPreferences settings = getPreferences(Activity.MODE_PRIVATE); - settings.edit().putInt(getPackageName() + ".feedback_launch_count", 0).apply(); - } - - // HomePager.OnUrlOpenListener - @Override - public void onUrlOpen(String url, EnumSet flags) { - if (flags.contains(OnUrlOpenListener.Flags.OPEN_WITH_INTENT)) { - Intent intent = new Intent(Intent.ACTION_VIEW); - intent.setData(Uri.parse(url)); - startActivity(intent); - } else { - // By default this listener is used for lists where the offline reader-view icon - // is shown - hence we need to redirect to the reader-view page by default. - // However there are some cases where we might not want to use this, e.g. - // for topsites where we do not indicate that a page is an offline reader-view bookmark too. - final String pageURL; - if (!flags.contains(OnUrlOpenListener.Flags.NO_READER_VIEW)) { - pageURL = SavedReaderViewHelper.getReaderURLIfCached(getContext(), url); - } else { - pageURL = url; - } - - if (!maybeSwitchToTab(pageURL, flags)) { - openUrlAndStopEditing(pageURL); - clearSelectedTabApplicationId(); - } - } - } - - // HomePager.OnUrlOpenInBackgroundListener - @Override - public void onUrlOpenInBackground(final String url, EnumSet flags) { - if (url == null) { - throw new IllegalArgumentException("url must not be null"); - } - if (flags == null) { - throw new IllegalArgumentException("flags must not be null"); - } - - // We only use onUrlOpenInBackgroundListener for the homepanel context menus, hence - // we should always be checking whether we want the readermode version - final String pageURL = SavedReaderViewHelper.getReaderURLIfCached(getContext(), url); - - final boolean isPrivate = flags.contains(OnUrlOpenInBackgroundListener.Flags.PRIVATE); - - int loadFlags = Tabs.LOADURL_NEW_TAB | Tabs.LOADURL_BACKGROUND; - if (isPrivate) { - loadFlags |= Tabs.LOADURL_PRIVATE; - } - - final Tab newTab = Tabs.getInstance().loadUrl(pageURL, loadFlags); - - // We switch to the desired tab by unique ID, which closes any window - // for a race between opening the tab and closing it, and switching to - // it. We could also switch to the Tab explicitly, but we don't want to - // hold a reference to the Tab itself in the anonymous listener class. - final int newTabId = newTab.getId(); - - final SnackbarBuilder.SnackbarCallback callback = new SnackbarBuilder.SnackbarCallback() { - @Override - public void onClick(View v) { - Telemetry.sendUIEvent(TelemetryContract.Event.SHOW, TelemetryContract.Method.TOAST, "switchtab"); - - maybeSwitchToTab(newTabId); - } - }; - - final String message = isPrivate ? - getResources().getString(R.string.new_private_tab_opened) : - getResources().getString(R.string.new_tab_opened); - final String buttonMessage = getResources().getString(R.string.switch_button_message); - - SnackbarBuilder.builder(this) - .message(message) - .duration(Snackbar.LENGTH_LONG) - .action(buttonMessage) - .callback(callback) - .buildAndShow(); - } - - // BrowserSearch.OnSearchListener - @Override - public void onSearch(SearchEngine engine, final String text, final TelemetryContract.Method method) { - // Don't store searches that happen in private tabs. This assumes the user can only - // perform a search inside the currently selected tab, which is true for searches - // that come from SearchEngineRow. - if (!Tabs.getInstance().getSelectedTab().isPrivate()) { - storeSearchQuery(text); - } - - // We don't use SearchEngine.getEngineIdentifier because it can - // return a custom search engine name, which is a privacy concern. - final String identifierToRecord = (engine.identifier != null) ? engine.identifier : "other"; - recordSearch(GeckoSharedPrefs.forProfile(this), identifierToRecord, method); - openUrlAndStopEditing(text, engine.name); - } - - // BrowserSearch.OnEditSuggestionListener - @Override - public void onEditSuggestion(String suggestion) { - mBrowserToolbar.onEditSuggestion(suggestion); - } - - @Override - public int getLayout() { return R.layout.gecko_app; } - - public SearchEngineManager getSearchEngineManager() { - return mSearchEngineManager; - } - - // For use from tests only. - @RobocopTarget - public ReadingListHelper getReadingListHelper() { - return mReadingListHelper; - } - - /** - * Launch UI that lets the user update Firefox. - * - * This depends on the current channel: Release and Beta both direct to the - * Google Play Store. If updating is enabled, Aurora, Nightly, and custom - * builds open about:, which provides an update interface. - * - * If updating is not enabled, this simply logs an error. - * - * @return true if update UI was launched. - */ - protected boolean handleUpdaterLaunch() { - if (AppConstants.RELEASE_OR_BETA) { - Intent intent = new Intent(Intent.ACTION_VIEW); - intent.setData(Uri.parse("market://details?id=" + getPackageName())); - startActivity(intent); - return true; - } - - if (AppConstants.MOZ_UPDATER) { - Tabs.getInstance().loadUrlInTab(AboutPages.UPDATER); - return true; - } - - Log.w(LOGTAG, "No candidate updater found; ignoring launch request."); - return false; - } - - /* Implementing ActionModeCompat.Presenter */ - @Override - public void startActionModeCompat(final ActionModeCompat.Callback callback) { - // If actionMode is null, we're not currently showing one. Flip to the action mode view - if (mActionMode == null) { - mActionBarFlipper.showNext(); - DynamicToolbarAnimator toolbar = mLayerView.getDynamicToolbarAnimator(); - - // If the toolbar is dynamic and not currently showing, just slide it in - if (mDynamicToolbar.isEnabled() && toolbar.getToolbarTranslation() != 0) { - mDynamicToolbar.setTemporarilyVisible(true, VisibilityTransition.ANIMATE); - } - mDynamicToolbar.setPinned(true, PinReason.ACTION_MODE); - - } else { - // Otherwise, we're already showing an action mode. Just finish it and show the new one - mActionMode.finish(); - } - - mActionMode = new ActionModeCompat(BrowserApp.this, callback, mActionBar); - if (callback.onCreateActionMode(mActionMode, mActionMode.getMenu())) { - mActionMode.invalidate(); - } - } - - /* Implementing ActionModeCompat.Presenter */ - @Override - public void endActionModeCompat() { - if (mActionMode == null) { - return; - } - - mActionMode.finish(); - mActionMode = null; - mDynamicToolbar.setPinned(false, PinReason.ACTION_MODE); - - mActionBarFlipper.showPrevious(); - - // Only slide the urlbar out if it was hidden when the action mode started - // Don't animate hiding it so that there's no flash as we switch back to url mode - mDynamicToolbar.setTemporarilyVisible(false, VisibilityTransition.IMMEDIATE); - } - - public static interface TabStripInterface { - public void refresh(); - void setOnTabChangedListener(OnTabAddedOrRemovedListener listener); - interface OnTabAddedOrRemovedListener { - void onTabChanged(); - } - } - - @Override - protected void recordStartupActionTelemetry(final String passedURL, final String action) { - final TelemetryContract.Method method; - if (ACTION_HOMESCREEN_SHORTCUT.equals(action)) { - // This action is also recorded via "loadurl.1" > "homescreen". - method = TelemetryContract.Method.HOMESCREEN; - } else if (passedURL == null) { - Telemetry.sendUIEvent(TelemetryContract.Event.LAUNCH, TelemetryContract.Method.HOMESCREEN, "launcher"); - method = TelemetryContract.Method.HOMESCREEN; - } else { - // This is action is also recorded via "loadurl.1" > "intent". - method = TelemetryContract.Method.INTENT; - } - - if (GeckoProfile.get(this).inGuestMode()) { - Telemetry.sendUIEvent(TelemetryContract.Event.LAUNCH, method, "guest"); - } else if (Restrictions.isRestrictedProfile(this)) { - Telemetry.sendUIEvent(TelemetryContract.Event.LAUNCH, method, "restricted"); - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/BrowserLocaleManager.java b/mobile/android/base/java/org/mozilla/gecko/BrowserLocaleManager.java deleted file mode 100644 index c5c041c7ac..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/BrowserLocaleManager.java +++ /dev/null @@ -1,439 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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; - -import java.io.File; -import java.util.Collection; -import java.util.HashSet; -import java.util.Locale; -import java.util.Set; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicReference; - -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; -import org.mozilla.gecko.annotation.ReflectionTarget; -import org.mozilla.gecko.util.GeckoJarReader; - -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.SharedPreferences; -import android.content.res.Configuration; -import android.content.res.Resources; -import android.util.Log; - -/** - * This class manages persistence, application, and otherwise handling of - * user-specified locales. - * - * Of note: - * - * * It's a singleton, because its scope extends to that of the application, - * and definitionally all changes to the locale of the app must go through - * this. - * * It's lazy. - * * It has ties into the Gecko event system, because it has to tell Gecko when - * to switch locale. - * * It relies on using the SharedPreferences file owned by the browser (in - * Fennec's case, "GeckoApp") for performance. - */ -public class BrowserLocaleManager implements LocaleManager { - private static final String LOG_TAG = "GeckoLocales"; - - private static final String EVENT_LOCALE_CHANGED = "Locale:Changed"; - private static final String PREF_LOCALE = "locale"; - - private static final String FALLBACK_LOCALE_TAG = "en-US"; - - // These are volatile because we don't impose restrictions - // over which thread calls our methods. - private volatile Locale currentLocale; - private volatile Locale systemLocale = Locale.getDefault(); - - private final AtomicBoolean inited = new AtomicBoolean(false); - private boolean systemLocaleDidChange; - private BroadcastReceiver receiver; - - private static final AtomicReference instance = new AtomicReference(); - - @ReflectionTarget - public static LocaleManager getInstance() { - LocaleManager localeManager = instance.get(); - if (localeManager != null) { - return localeManager; - } - - localeManager = new BrowserLocaleManager(); - if (instance.compareAndSet(null, localeManager)) { - return localeManager; - } else { - return instance.get(); - } - } - - @Override - public boolean isEnabled() { - return AppConstants.MOZ_LOCALE_SWITCHER; - } - - /** - * Ensure that you call this early in your application startup, - * and with a context that's sufficiently long-lived (typically - * the application context). - * - * Calling multiple times is harmless. - */ - @Override - public void initialize(final Context context) { - if (!inited.compareAndSet(false, true)) { - return; - } - - receiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - final Locale current = systemLocale; - - // We don't trust Locale.getDefault() here, because we make a - // habit of mutating it! Use the one Android supplies, because - // that gets regularly reset. - // The default value of systemLocale is fine, because we haven't - // yet swizzled Locale during static initialization. - systemLocale = context.getResources().getConfiguration().locale; - systemLocaleDidChange = true; - - Log.d(LOG_TAG, "System locale changed from " + current + " to " + systemLocale); - } - }; - context.registerReceiver(receiver, new IntentFilter(Intent.ACTION_LOCALE_CHANGED)); - } - - @Override - public boolean systemLocaleDidChange() { - return systemLocaleDidChange; - } - - /** - * Every time the system gives us a new configuration, it - * carries the external locale. Fix it. - */ - @Override - public void correctLocale(Context context, Resources res, Configuration config) { - final Locale current = getCurrentLocale(context); - if (current == null) { - Log.d(LOG_TAG, "No selected locale. No correction needed."); - return; - } - - // I know it's tempting to short-circuit here if the config seems to be - // up-to-date, but the rest is necessary. - - config.locale = current; - - // The following two lines are heavily commented in case someone - // decides to chase down performance improvements and decides to - // question what's going on here. - // Both lines should be cheap, *but*... - - // This is unnecessary for basic string choice, but it almost - // certainly comes into play when rendering numbers, deciding on RTL, - // etc. Take it out if you can prove that's not the case. - Locale.setDefault(current); - - // This seems to be a no-op, but every piece of documentation under the - // sun suggests that it's necessary, and it certainly makes sense. - res.updateConfiguration(config, null); - } - - /** - * We can be in one of two states. - * - * If the user has not explicitly chosen a Firefox-specific locale, we say - * we are "mirroring" the system locale. - * - * When we are not mirroring, system locale changes do not impact Firefox - * and are essentially ignored; the user's locale selection is the only - * thing we care about, and we actively correct incoming configuration - * changes to reflect the user's chosen locale. - * - * By contrast, when we are mirroring, system locale changes cause Firefox - * to reflect the new system locale, as if the user picked the new locale. - * - * If we're currently mirroring the system locale, this method returns the - * supplied configuration's locale, unless the current activity locale is - * correct. If we're not currently mirroring, this method updates the - * configuration object to match the user's currently selected locale, and - * returns that, unless the current activity locale is correct. - * - * If the current activity locale is correct, returns null. - * - * The caller is expected to redisplay themselves accordingly. - * - * This method is intended to be called from inside - * onConfigurationChanged(Configuration) as part of a strategy - * to detect and either apply or undo system locale changes. - */ - @Override - public Locale onSystemConfigurationChanged(final Context context, final Resources resources, final Configuration configuration, final Locale currentActivityLocale) { - if (!isMirroringSystemLocale(context)) { - correctLocale(context, resources, configuration); - } - - final Locale changed = configuration.locale; - if (changed.equals(currentActivityLocale)) { - return null; - } - - return changed; - } - - /** - * Gecko needs to know the OS locale to compute a useful Accept-Language - * header. If it changed since last time, send a message to Gecko and - * persist the new value. If unchanged, returns immediately. - * - * @param prefs the SharedPreferences instance to use. Cannot be null. - * @param osLocale the new locale instance. Safe if null. - */ - public static void storeAndNotifyOSLocale(final SharedPreferences prefs, - final Locale osLocale) { - if (osLocale == null) { - return; - } - - final String lastOSLocale = prefs.getString("osLocale", null); - final String osLocaleString = osLocale.toString(); - - if (osLocaleString.equals(lastOSLocale)) { - return; - } - - // Store the Java-native form. - prefs.edit().putString("osLocale", osLocaleString).apply(); - - // The value we send to Gecko should be a language tag, not - // a Java locale string. - final String osLanguageTag = Locales.getLanguageTag(osLocale); - GeckoAppShell.notifyObservers("Locale:OS", osLanguageTag); - } - - @Override - public String getAndApplyPersistedLocale(Context context) { - initialize(context); - - final long t1 = android.os.SystemClock.uptimeMillis(); - final String localeCode = getPersistedLocale(context); - if (localeCode == null) { - return null; - } - - // Note that we don't tell Gecko about this. We notify Gecko when the - // locale is set, not when we update Java. - final String resultant = updateLocale(context, localeCode); - - if (resultant == null) { - // Update the configuration anyway. - updateConfiguration(context, currentLocale); - } - - final long t2 = android.os.SystemClock.uptimeMillis(); - Log.i(LOG_TAG, "Locale read and update took: " + (t2 - t1) + "ms."); - return resultant; - } - - /** - * Returns the set locale if it changed. - * - * Always persists and notifies Gecko. - */ - @Override - public String setSelectedLocale(Context context, String localeCode) { - final String resultant = updateLocale(context, localeCode); - - // We always persist and notify Gecko, even if nothing seemed to - // change. This might happen if you're picking a locale that's the same - // as the current OS locale. The OS locale might change next time we - // launch, and we need the Gecko pref and persisted locale to have been - // set by the time that happens. - persistLocale(context, localeCode); - - // Tell Gecko. - GeckoAppShell.notifyObservers(EVENT_LOCALE_CHANGED, Locales.getLanguageTag(getCurrentLocale(context))); - - return resultant; - } - - @Override - public void resetToSystemLocale(Context context) { - // Wipe the pref. - final SharedPreferences settings = getSharedPreferences(context); - settings.edit().remove(PREF_LOCALE).apply(); - - // Apply the system locale. - updateLocale(context, systemLocale); - - // Tell Gecko. - GeckoAppShell.notifyObservers(EVENT_LOCALE_CHANGED, ""); - } - - /** - * This is public to allow for an activity to force the - * current locale to be applied if necessary (e.g., when - * a new activity launches). - */ - @Override - public void updateConfiguration(Context context, Locale locale) { - Resources res = context.getResources(); - Configuration config = res.getConfiguration(); - - // We should use setLocale, but it's unexpectedly missing - // on real devices. - config.locale = locale; - res.updateConfiguration(config, null); - } - - private SharedPreferences getSharedPreferences(Context context) { - return GeckoSharedPrefs.forApp(context); - } - - /** - * @return the persisted locale in Java format: "en_US". - */ - private String getPersistedLocale(Context context) { - final SharedPreferences settings = getSharedPreferences(context); - final String locale = settings.getString(PREF_LOCALE, ""); - - if ("".equals(locale)) { - return null; - } - return locale; - } - - private void persistLocale(Context context, String localeCode) { - final SharedPreferences settings = getSharedPreferences(context); - settings.edit().putString(PREF_LOCALE, localeCode).apply(); - } - - @Override - public Locale getCurrentLocale(Context context) { - if (currentLocale != null) { - return currentLocale; - } - - final String current = getPersistedLocale(context); - if (current == null) { - return null; - } - return currentLocale = Locales.parseLocaleCode(current); - } - - /** - * Updates the Java locale and the Android configuration. - * - * Returns the persisted locale if it differed. - * - * Does not notify Gecko. - * - * @param localeCode a locale string in Java format: "en_US". - * @return if it differed, a locale string in Java format: "en_US". - */ - private String updateLocale(Context context, String localeCode) { - // Fast path. - final Locale defaultLocale = Locale.getDefault(); - if (defaultLocale.toString().equals(localeCode)) { - return null; - } - - final Locale locale = Locales.parseLocaleCode(localeCode); - - return updateLocale(context, locale); - } - - /** - * @return the Java locale string: e.g., "en_US". - */ - private String updateLocale(Context context, final Locale locale) { - // Fast path. - if (Locale.getDefault().equals(locale)) { - return null; - } - - Locale.setDefault(locale); - currentLocale = locale; - - // Update resources. - updateConfiguration(context, locale); - - return locale.toString(); - } - - private boolean isMirroringSystemLocale(final Context context) { - return getPersistedLocale(context) == null; - } - - /** - * Examines multilocale.json, returning the included list of - * locale codes. - * - * If multilocale.json is not present, returns - * null. In that case, consider {@link #getFallbackLocaleTag()}. - * - * multilocale.json currently looks like this: - * - * - * {"locales": ["en-US", "be", "ca", "cs", "da", "de", "en-GB", - * "en-ZA", "es-AR", "es-ES", "es-MX", "et", "fi", - * "fr", "ga-IE", "hu", "id", "it", "ja", "ko", - * "lt", "lv", "nb-NO", "nl", "pl", "pt-BR", - * "pt-PT", "ro", "ru", "sk", "sl", "sv-SE", "th", - * "tr", "uk", "zh-CN", "zh-TW", "en-US"]} - * - */ - public static Collection getPackagedLocaleTags(final Context context) { - final String resPath = "res/multilocale.json"; - final String jarURL = GeckoJarReader.getJarURL(context, resPath); - - final String contents = GeckoJarReader.getText(context, jarURL); - if (contents == null) { - // GeckoJarReader logs and swallows exceptions. - return null; - } - - try { - final JSONObject multilocale = new JSONObject(contents); - final JSONArray locales = multilocale.getJSONArray("locales"); - if (locales == null) { - Log.e(LOG_TAG, "No 'locales' array in multilocales.json!"); - return null; - } - - final Set out = new HashSet(locales.length()); - for (int i = 0; i < locales.length(); ++i) { - // If any item in the array is invalid, this will throw, - // and the entire clause will fail, being caught below - // and returning null. - out.add(locales.getString(i)); - } - - return out; - } catch (JSONException e) { - Log.e(LOG_TAG, "Unable to parse multilocale.json.", e); - return null; - } - } - - /** - * @return the single default locale baked into this application. - * Applicable when there is no multilocale.json present. - */ - @SuppressWarnings("static-method") - public String getFallbackLocaleTag() { - return FALLBACK_LOCALE_TAG; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/ChromeCastDisplay.java b/mobile/android/base/java/org/mozilla/gecko/ChromeCastDisplay.java deleted file mode 100644 index cff6ea6439..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/ChromeCastDisplay.java +++ /dev/null @@ -1,112 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- - * vim: ts=4 sw=4 expandtab: - * 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; - -import org.json.JSONObject; -import org.json.JSONException; - -import org.mozilla.gecko.R; -import org.mozilla.gecko.util.EventCallback; - -import com.google.android.gms.cast.CastDevice; -import com.google.android.gms.cast.CastRemoteDisplayLocalService; -import com.google.android.gms.common.ConnectionResult; -import com.google.android.gms.common.GooglePlayServicesUtil; -import com.google.android.gms.common.api.Status; - -import android.app.PendingIntent; -import android.content.Context; -import android.content.Intent; -import android.support.v7.media.MediaRouter.RouteInfo; -import android.util.Log; - -public class ChromeCastDisplay implements GeckoPresentationDisplay { - - static final String REMOTE_DISPLAY_APP_ID = "4574A331"; - - private static final String LOGTAG = "GeckoChromeCastDisplay"; - private final Context context; - private final RouteInfo route; - private CastDevice castDevice; - - public ChromeCastDisplay(Context context, RouteInfo route) { - int status = GooglePlayServicesUtil.isGooglePlayServicesAvailable(context); - if (status != ConnectionResult.SUCCESS) { - throw new IllegalStateException("Play services are required for Chromecast support (got status code " + status + ")"); - } - - this.context = context; - this.route = route; - this.castDevice = CastDevice.getFromBundle(route.getExtras()); - } - - public JSONObject toJSON() { - final JSONObject obj = new JSONObject(); - try { - if (castDevice == null) { - return null; - } - obj.put("uuid", route.getId()); - obj.put("friendlyName", castDevice.getFriendlyName()); - obj.put("type", "chromecast"); - } catch (JSONException ex) { - Log.d(LOGTAG, "Error building route", ex); - } - - return obj; - } - - @Override - public void start(final EventCallback callback) { - - if (CastRemoteDisplayLocalService.getInstance() != null) { - Log.d(LOGTAG, "CastRemoteDisplayLocalService already existed."); - GeckoAppShell.notifyObservers("presentation-view-ready", route.getId()); - callback.sendSuccess("Succeed to start presentation."); - return; - } - - Intent intent = new Intent(context, RemotePresentationService.class); - intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP); - PendingIntent notificationPendingIntent = PendingIntent.getActivity(context, 0, intent, 0); - - CastRemoteDisplayLocalService.NotificationSettings settings = - new CastRemoteDisplayLocalService.NotificationSettings.Builder() - .setNotificationPendingIntent(notificationPendingIntent).build(); - - CastRemoteDisplayLocalService.startService( - context, - RemotePresentationService.class, - REMOTE_DISPLAY_APP_ID, - castDevice, - settings, - new CastRemoteDisplayLocalService.Callbacks() { - @Override - public void onServiceCreated(CastRemoteDisplayLocalService service) { - ((RemotePresentationService) service).setDeviceId(route.getId()); - } - - @Override - public void onRemoteDisplaySessionStarted(CastRemoteDisplayLocalService service) { - Log.d(LOGTAG, "Remote presentation launched!"); - callback.sendSuccess("Succeed to start presentation."); - } - - @Override - public void onRemoteDisplaySessionError(Status errorReason) { - int code = errorReason.getStatusCode(); - callback.sendError("Fail to start presentation. Error code: " + code); - } - }); - } - - @Override - public void stop(EventCallback callback) { - CastRemoteDisplayLocalService.stopService(); - callback.sendSuccess("Succeed to stop presentation."); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/ChromeCastPlayer.java b/mobile/android/base/java/org/mozilla/gecko/ChromeCastPlayer.java deleted file mode 100644 index c531b8c377..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/ChromeCastPlayer.java +++ /dev/null @@ -1,509 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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; - -import java.io.IOException; - -import org.mozilla.gecko.util.EventCallback; -import org.json.JSONObject; -import org.json.JSONException; - -import com.google.android.gms.cast.Cast.MessageReceivedCallback; -import com.google.android.gms.cast.ApplicationMetadata; -import com.google.android.gms.cast.Cast; -import com.google.android.gms.cast.Cast.ApplicationConnectionResult; -import com.google.android.gms.cast.CastDevice; -import com.google.android.gms.cast.CastMediaControlIntent; -import com.google.android.gms.cast.MediaInfo; -import com.google.android.gms.cast.MediaMetadata; -import com.google.android.gms.cast.MediaStatus; -import com.google.android.gms.cast.RemoteMediaPlayer; -import com.google.android.gms.cast.RemoteMediaPlayer.MediaChannelResult; -import com.google.android.gms.common.ConnectionResult; -import com.google.android.gms.common.api.GoogleApiClient; -import com.google.android.gms.common.api.ResultCallback; -import com.google.android.gms.common.api.Status; -import com.google.android.gms.common.GooglePlayServicesUtil; - -import android.content.Context; -import android.os.Bundle; -import android.support.v7.media.MediaRouter.RouteInfo; -import android.util.Log; - -/* Implementation of GeckoMediaPlayer for talking to ChromeCast devices */ -class ChromeCastPlayer implements GeckoMediaPlayer { - private static final boolean SHOW_DEBUG = false; - - static final String MIRROR_RECEIVER_APP_ID = "08FF1091"; - - private final Context context; - private final RouteInfo route; - private GoogleApiClient apiClient; - private RemoteMediaPlayer remoteMediaPlayer; - private final boolean canMirror; - private String mSessionId; - private MirrorChannel mMirrorChannel; - private boolean mApplicationStarted = false; - - // EventCallback which is actually a GeckoEventCallback is sometimes being invoked more - // than once. That causes the IllegalStateException to be thrown. To prevent a crash, - // catch the exception and report it as an error to the log. - private static void sendSuccess(final EventCallback callback, final String msg) { - try { - callback.sendSuccess(msg); - } catch (final IllegalStateException e) { - Log.e(LOGTAG, "Attempting to invoke callback.sendSuccess more than once.", e); - } - } - - private static void sendError(final EventCallback callback, final String msg) { - try { - callback.sendError(msg); - } catch (final IllegalStateException e) { - Log.e(LOGTAG, "Attempting to invoke callback.sendError more than once.", e); - } - } - - // Callback to start playback of a url on a remote device - private class VideoPlayCallback implements ResultCallback, - RemoteMediaPlayer.OnStatusUpdatedListener, - RemoteMediaPlayer.OnMetadataUpdatedListener { - private final String url; - private final String type; - private final String title; - private final EventCallback callback; - - public VideoPlayCallback(String url, String type, String title, EventCallback callback) { - this.url = url; - this.type = type; - this.title = title; - this.callback = callback; - } - - @Override - public void onStatusUpdated() { - MediaStatus mediaStatus = remoteMediaPlayer.getMediaStatus(); - - switch (mediaStatus.getPlayerState()) { - case MediaStatus.PLAYER_STATE_PLAYING: - GeckoAppShell.notifyObservers("MediaPlayer:Playing", null); - break; - case MediaStatus.PLAYER_STATE_PAUSED: - GeckoAppShell.notifyObservers("MediaPlayer:Paused", null); - break; - case MediaStatus.PLAYER_STATE_IDLE: - // TODO: Do we want to shutdown when there are errors? - if (mediaStatus.getIdleReason() == MediaStatus.IDLE_REASON_FINISHED) { - GeckoAppShell.notifyObservers("Casting:Stop", null); - } - break; - default: - // TODO: Do we need to handle other status such as buffering / unknown? - break; - } - } - - @Override - public void onMetadataUpdated() { } - - @Override - public void onResult(ApplicationConnectionResult result) { - Status status = result.getStatus(); - debug("ApplicationConnectionResultCallback.onResult: statusCode" + status.getStatusCode()); - if (status.isSuccess()) { - remoteMediaPlayer = new RemoteMediaPlayer(); - remoteMediaPlayer.setOnStatusUpdatedListener(this); - remoteMediaPlayer.setOnMetadataUpdatedListener(this); - mSessionId = result.getSessionId(); - if (!verifySession(callback)) { - return; - } - - try { - Cast.CastApi.setMessageReceivedCallbacks(apiClient, remoteMediaPlayer.getNamespace(), remoteMediaPlayer); - } catch (IOException e) { - debug("Exception while creating media channel", e); - } - - startPlayback(); - } else { - sendError(callback, status.toString()); - } - } - - private void startPlayback() { - MediaMetadata mediaMetadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE); - mediaMetadata.putString(MediaMetadata.KEY_TITLE, title); - MediaInfo mediaInfo = new MediaInfo.Builder(url) - .setContentType(type) - .setStreamType(MediaInfo.STREAM_TYPE_BUFFERED) - .setMetadata(mediaMetadata) - .build(); - try { - remoteMediaPlayer.load(apiClient, mediaInfo, true).setResultCallback(new ResultCallback() { - @Override - public void onResult(MediaChannelResult result) { - if (result.getStatus().isSuccess()) { - sendSuccess(callback, null); - debug("Media loaded successfully"); - return; - } - - debug("Media load failed " + result.getStatus()); - sendError(callback, result.getStatus().toString()); - } - }); - - return; - } catch (IllegalStateException e) { - debug("Problem occurred with media during loading", e); - } catch (Exception e) { - debug("Problem opening media during loading", e); - } - - sendError(callback, ""); - } - } - - public ChromeCastPlayer(Context context, RouteInfo route) { - int status = GooglePlayServicesUtil.isGooglePlayServicesAvailable(context); - if (status != ConnectionResult.SUCCESS) { - throw new IllegalStateException("Play services are required for Chromecast support (got status code " + status + ")"); - } - - this.context = context; - this.route = route; - this.canMirror = route.supportsControlCategory(CastMediaControlIntent.categoryForCast(MIRROR_RECEIVER_APP_ID)); - } - - /** - * This dumps everything we can find about the device into JSON. This will hopefully make it - * easier to filter out duplicate devices from different sources in JS. - * Returns null if the device can't be found. - */ - @Override - public JSONObject toJSON() { - final JSONObject obj = new JSONObject(); - try { - final CastDevice device = CastDevice.getFromBundle(route.getExtras()); - if (device == null) { - return null; - } - - obj.put("uuid", route.getId()); - obj.put("version", device.getDeviceVersion()); - obj.put("friendlyName", device.getFriendlyName()); - obj.put("location", device.getIpAddress().toString()); - obj.put("modelName", device.getModelName()); - obj.put("mirror", canMirror); - // For now we just assume all of these are Google devices - obj.put("manufacturer", "Google Inc."); - } catch (JSONException ex) { - debug("Error building route", ex); - } - - return obj; - } - - @Override - public void load(final String title, final String url, final String type, final EventCallback callback) { - final CastDevice device = CastDevice.getFromBundle(route.getExtras()); - Cast.CastOptions.Builder apiOptionsBuilder = Cast.CastOptions.builder(device, new Cast.Listener() { - @Override - public void onApplicationStatusChanged() { } - - @Override - public void onVolumeChanged() { } - - @Override - public void onApplicationDisconnected(int errorCode) { } - }); - - apiClient = new GoogleApiClient.Builder(context) - .addApi(Cast.API, apiOptionsBuilder.build()) - .addConnectionCallbacks(new GoogleApiClient.ConnectionCallbacks() { - @Override - public void onConnected(Bundle connectionHint) { - // Sometimes apiClient is null here. See bug 1061032 - if (apiClient != null && !apiClient.isConnected()) { - debug("Connection failed"); - sendError(callback, "Not connected"); - return; - } - - // Launch the media player app and launch this url once its loaded - try { - Cast.CastApi.launchApplication(apiClient, CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID, true) - .setResultCallback(new VideoPlayCallback(url, type, title, callback)); - } catch (Exception e) { - debug("Failed to launch application", e); - } - } - - @Override - public void onConnectionSuspended(int cause) { - debug("suspended"); - } - }).build(); - - apiClient.connect(); - } - - @Override - public void start(final EventCallback callback) { - // Nothing to be done here - sendSuccess(callback, null); - } - - @Override - public void stop(final EventCallback callback) { - // Nothing to be done here - sendSuccess(callback, null); - } - - public boolean verifySession(final EventCallback callback) { - String msg = null; - if (apiClient == null || !apiClient.isConnected()) { - msg = "Not connected"; - } - - if (mSessionId == null) { - msg = "No session"; - } - - if (msg != null) { - debug(msg); - if (callback != null) { - sendError(callback, msg); - } - return false; - } - - return true; - } - - @Override - public void play(final EventCallback callback) { - if (!verifySession(callback)) { - return; - } - - try { - remoteMediaPlayer.play(apiClient).setResultCallback(new ResultCallback() { - @Override - public void onResult(MediaChannelResult result) { - Status status = result.getStatus(); - if (!status.isSuccess()) { - debug("Unable to play: " + status.getStatusCode()); - sendError(callback, status.toString()); - } else { - sendSuccess(callback, null); - } - } - }); - } catch (IllegalStateException ex) { - // The media player may throw if the session has been killed. For now, we're just catching this here. - sendError(callback, "Error playing"); - } - } - - @Override - public void pause(final EventCallback callback) { - if (!verifySession(callback)) { - return; - } - - try { - remoteMediaPlayer.pause(apiClient).setResultCallback(new ResultCallback() { - @Override - public void onResult(MediaChannelResult result) { - Status status = result.getStatus(); - if (!status.isSuccess()) { - debug("Unable to pause: " + status.getStatusCode()); - sendError(callback, status.toString()); - } else { - sendSuccess(callback, null); - } - } - }); - } catch (IllegalStateException ex) { - // The media player may throw if the session has been killed. For now, we're just catching this here. - sendError(callback, "Error pausing"); - } - } - - @Override - public void end(final EventCallback callback) { - if (!verifySession(callback)) { - return; - } - - try { - Cast.CastApi.stopApplication(apiClient).setResultCallback(new ResultCallback() { - @Override - public void onResult(Status result) { - if (result.isSuccess()) { - try { - Cast.CastApi.removeMessageReceivedCallbacks(apiClient, remoteMediaPlayer.getNamespace()); - remoteMediaPlayer = null; - mSessionId = null; - apiClient.disconnect(); - apiClient = null; - - if (callback != null) { - sendSuccess(callback, null); - } - - return; - } catch (Exception ex) { - debug("Error ending", ex); - } - } - - if (callback != null) { - sendError(callback, result.getStatus().toString()); - } - } - }); - } catch (IllegalStateException ex) { - // The media player may throw if the session has been killed. For now, we're just catching this here. - sendError(callback, "Error stopping"); - } - } - - class MirrorChannel implements MessageReceivedCallback { - /** - * @return custom namespace - */ - public String getNamespace() { - return "urn:x-cast:org.mozilla.mirror"; - } - - /* - * Receive message from the receiver app - */ - @Override - public void onMessageReceived(CastDevice castDevice, String namespace, - String message) { - GeckoAppShell.notifyObservers("MediaPlayer:Response", message); - } - - public void sendMessage(String message) { - if (apiClient != null && mMirrorChannel != null) { - try { - Cast.CastApi.sendMessage(apiClient, mMirrorChannel.getNamespace(), message) - .setResultCallback( - new ResultCallback() { - @Override - public void onResult(Status result) { - } - }); - } catch (Exception e) { - Log.e(LOGTAG, "Exception while sending message", e); - } - } - } - } - private class MirrorCallback implements ResultCallback { - final EventCallback callback; - MirrorCallback(final EventCallback callback) { - this.callback = callback; - } - - - @Override - public void onResult(ApplicationConnectionResult result) { - Status status = result.getStatus(); - if (status.isSuccess()) { - ApplicationMetadata applicationMetadata = result.getApplicationMetadata(); - mSessionId = result.getSessionId(); - String applicationStatus = result.getApplicationStatus(); - boolean wasLaunched = result.getWasLaunched(); - mApplicationStarted = true; - - // Create the custom message - // channel - mMirrorChannel = new MirrorChannel(); - try { - Cast.CastApi.setMessageReceivedCallbacks(apiClient, - mMirrorChannel - .getNamespace(), - mMirrorChannel); - sendSuccess(callback, null); - } catch (IOException e) { - Log.e(LOGTAG, "Exception while creating channel", e); - } - - GeckoAppShell.notifyObservers("Casting:Mirror", route.getId()); - } else { - sendError(callback, status.toString()); - } - } - } - - @Override - public void message(String msg, final EventCallback callback) { - if (mMirrorChannel != null) { - mMirrorChannel.sendMessage(msg); - } - } - - @Override - public void mirror(final EventCallback callback) { - final CastDevice device = CastDevice.getFromBundle(route.getExtras()); - Cast.CastOptions.Builder apiOptionsBuilder = Cast.CastOptions.builder(device, new Cast.Listener() { - @Override - public void onApplicationStatusChanged() { } - - @Override - public void onVolumeChanged() { } - - @Override - public void onApplicationDisconnected(int errorCode) { } - }); - - apiClient = new GoogleApiClient.Builder(context) - .addApi(Cast.API, apiOptionsBuilder.build()) - .addConnectionCallbacks(new GoogleApiClient.ConnectionCallbacks() { - @Override - public void onConnected(Bundle connectionHint) { - // Sometimes apiClient is null here. See bug 1061032 - if (apiClient == null || !apiClient.isConnected()) { - return; - } - - // Launch the media player app and launch this url once its loaded - try { - Cast.CastApi.launchApplication(apiClient, MIRROR_RECEIVER_APP_ID, true) - .setResultCallback(new MirrorCallback(callback)); - } catch (Exception e) { - debug("Failed to launch application", e); - } - } - - @Override - public void onConnectionSuspended(int cause) { - debug("suspended"); - } - }).build(); - - apiClient.connect(); - } - - private static final String LOGTAG = "GeckoChromeCastPlayer"; - private void debug(String msg, Exception e) { - if (SHOW_DEBUG) { - Log.e(LOGTAG, msg, e); - } - } - - private void debug(String msg) { - if (SHOW_DEBUG) { - Log.d(LOGTAG, msg); - } - } - -} diff --git a/mobile/android/base/java/org/mozilla/gecko/CrashReporter.java b/mobile/android/base/java/org/mozilla/gecko/CrashReporter.java deleted file mode 100644 index ce2384a4d4..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/CrashReporter.java +++ /dev/null @@ -1,480 +0,0 @@ -/* -*- Mode: Java; 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; - -import java.util.HashMap; -import java.util.Map; -import java.io.BufferedReader; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.FileReader; -import java.io.InputStreamReader; -import java.io.IOException; -import java.io.OutputStream; -import java.net.HttpURLConnection; -import java.net.URL; -import java.nio.channels.Channels; -import java.nio.channels.FileChannel; -import java.util.zip.GZIPOutputStream; - -import org.mozilla.gecko.AppConstants.Versions; - -import android.annotation.SuppressLint; -import android.app.AlertDialog; -import android.app.ProgressDialog; -import android.content.DialogInterface; -import android.content.Intent; -import android.content.SharedPreferences; -import android.os.Build; -import android.os.Bundle; -import android.os.Handler; -import android.support.v7.app.AppCompatActivity; -import android.text.TextUtils; -import android.util.Log; -import android.view.View; -import android.widget.CheckBox; -import android.widget.CompoundButton; -import android.widget.EditText; - -@SuppressLint("Registered") // This activity is only registered in the manifest if MOZ_CRASHREPORTER is set -public class CrashReporter extends AppCompatActivity -{ - private static final String LOGTAG = "GeckoCrashReporter"; - - private static final String PASSED_MINI_DUMP_KEY = "minidumpPath"; - private static final String PASSED_MINI_DUMP_SUCCESS_KEY = "minidumpSuccess"; - private static final String MINI_DUMP_PATH_KEY = "upload_file_minidump"; - private static final String PAGE_URL_KEY = "URL"; - private static final String NOTES_KEY = "Notes"; - private static final String SERVER_URL_KEY = "ServerURL"; - - private static final String CRASH_REPORT_SUFFIX = "/mozilla/Crash Reports/"; - private static final String PENDING_SUFFIX = CRASH_REPORT_SUFFIX + "pending"; - private static final String SUBMITTED_SUFFIX = CRASH_REPORT_SUFFIX + "submitted"; - - private static final String PREFS_SEND_REPORT = "sendReport"; - private static final String PREFS_INCLUDE_URL = "includeUrl"; - private static final String PREFS_ALLOW_CONTACT = "allowContact"; - private static final String PREFS_CONTACT_EMAIL = "contactEmail"; - - private Handler mHandler; - private ProgressDialog mProgressDialog; - private File mPendingMinidumpFile; - private File mPendingExtrasFile; - private HashMap mExtrasStringMap; - private boolean mMinidumpSucceeded; - - private boolean moveFile(File inFile, File outFile) { - Log.i(LOGTAG, "moving " + inFile + " to " + outFile); - if (inFile.renameTo(outFile)) - return true; - try { - outFile.createNewFile(); - Log.i(LOGTAG, "couldn't rename minidump file"); - // so copy it instead - FileChannel inChannel = new FileInputStream(inFile).getChannel(); - FileChannel outChannel = new FileOutputStream(outFile).getChannel(); - long transferred = inChannel.transferTo(0, inChannel.size(), outChannel); - inChannel.close(); - outChannel.close(); - - if (transferred > 0) - inFile.delete(); - } catch (Exception e) { - Log.e(LOGTAG, "exception while copying minidump file: ", e); - return false; - } - return true; - } - - private void doFinish() { - if (mHandler != null) { - mHandler.post(new Runnable() { - @Override - public void run() { - finish(); - } - }); - } - } - - @Override - public void finish() { - try { - if (mProgressDialog.isShowing()) { - mProgressDialog.dismiss(); - } - } catch (Exception e) { - Log.e(LOGTAG, "exception while closing progress dialog: ", e); - } - super.finish(); - } - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - // mHandler is created here so runnables can be run on the main thread - mHandler = new Handler(); - setContentView(R.layout.crash_reporter); - mProgressDialog = new ProgressDialog(this); - mProgressDialog.setMessage(getString(R.string.sending_crash_report)); - - mMinidumpSucceeded = getIntent().getBooleanExtra(PASSED_MINI_DUMP_SUCCESS_KEY, false); - if (!mMinidumpSucceeded) { - Log.i(LOGTAG, "Failed to get minidump."); - } - String passedMinidumpPath = getIntent().getStringExtra(PASSED_MINI_DUMP_KEY); - File passedMinidumpFile = new File(passedMinidumpPath); - File pendingDir = new File(getFilesDir(), PENDING_SUFFIX); - pendingDir.mkdirs(); - mPendingMinidumpFile = new File(pendingDir, passedMinidumpFile.getName()); - moveFile(passedMinidumpFile, mPendingMinidumpFile); - - File extrasFile = new File(passedMinidumpPath.replaceAll("\\.dmp", ".extra")); - mPendingExtrasFile = new File(pendingDir, extrasFile.getName()); - moveFile(extrasFile, mPendingExtrasFile); - - mExtrasStringMap = new HashMap(); - readStringsFromFile(mPendingExtrasFile.getPath(), mExtrasStringMap); - - // Notify GeckoApp that we've crashed, so it can react appropriately during the next start. - try { - File crashFlag = new File(GeckoProfileDirectories.getMozillaDirectory(this), "CRASHED"); - crashFlag.createNewFile(); - } catch (GeckoProfileDirectories.NoMozillaDirectoryException | IOException e) { - Log.e(LOGTAG, "Cannot set crash flag: ", e); - } - - final CheckBox allowContactCheckBox = (CheckBox) findViewById(R.id.allow_contact); - final CheckBox includeUrlCheckBox = (CheckBox) findViewById(R.id.include_url); - final CheckBox sendReportCheckBox = (CheckBox) findViewById(R.id.send_report); - final EditText commentsEditText = (EditText) findViewById(R.id.comment); - final EditText emailEditText = (EditText) findViewById(R.id.email); - - // Load CrashReporter preferences to avoid redundant user input. - SharedPreferences prefs = GeckoSharedPrefs.forCrashReporter(this); - final boolean sendReport = prefs.getBoolean(PREFS_SEND_REPORT, true); - final boolean includeUrl = prefs.getBoolean(PREFS_INCLUDE_URL, false); - final boolean allowContact = prefs.getBoolean(PREFS_ALLOW_CONTACT, false); - final String contactEmail = prefs.getString(PREFS_CONTACT_EMAIL, ""); - - allowContactCheckBox.setChecked(allowContact); - includeUrlCheckBox.setChecked(includeUrl); - sendReportCheckBox.setChecked(sendReport); - emailEditText.setText(contactEmail); - - sendReportCheckBox.setOnCheckedChangeListener(new CheckBox.OnCheckedChangeListener() { - @Override - public void onCheckedChanged(CompoundButton checkbox, boolean isChecked) { - commentsEditText.setEnabled(isChecked); - commentsEditText.requestFocus(); - - includeUrlCheckBox.setEnabled(isChecked); - allowContactCheckBox.setEnabled(isChecked); - emailEditText.setEnabled(isChecked && allowContactCheckBox.isChecked()); - } - }); - - allowContactCheckBox.setOnCheckedChangeListener(new CheckBox.OnCheckedChangeListener() { - @Override - public void onCheckedChanged(CompoundButton checkbox, boolean isChecked) { - // We need to check isEnabled() here because this listener is - // fired on rotation -- even when the checkbox is disabled. - emailEditText.setEnabled(checkbox.isEnabled() && isChecked); - emailEditText.requestFocus(); - } - }); - - emailEditText.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - // Even if the email EditText is disabled, allow it to be - // clicked and focused. - if (sendReportCheckBox.isChecked() && !v.isEnabled()) { - allowContactCheckBox.setChecked(true); - v.setEnabled(true); - v.requestFocus(); - } - } - }); - } - - @Override - public void onBackPressed() { - AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setMessage(R.string.crash_closing_alert); - builder.setNegativeButton(R.string.button_cancel, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - dialog.dismiss(); - } - }); - builder.setPositiveButton(R.string.button_ok, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - CrashReporter.this.finish(); - } - }); - builder.show(); - } - - private void backgroundSendReport() { - final CheckBox sendReportCheckbox = (CheckBox) findViewById(R.id.send_report); - if (!sendReportCheckbox.isChecked()) { - doFinish(); - return; - } - - // Persist settings to avoid redundant user input. - savePrefs(); - - mProgressDialog.show(); - new Thread(new Runnable() { - @Override - public void run() { - sendReport(mPendingMinidumpFile, mExtrasStringMap, mPendingExtrasFile); - } - }, "CrashReporter Thread").start(); - } - - private void savePrefs() { - SharedPreferences.Editor editor = GeckoSharedPrefs.forCrashReporter(this).edit(); - - final boolean allowContact = ((CheckBox) findViewById(R.id.allow_contact)).isChecked(); - final boolean includeUrl = ((CheckBox) findViewById(R.id.include_url)).isChecked(); - final boolean sendReport = ((CheckBox) findViewById(R.id.send_report)).isChecked(); - final String contactEmail = ((EditText) findViewById(R.id.email)).getText().toString(); - - editor.putBoolean(PREFS_ALLOW_CONTACT, allowContact); - editor.putBoolean(PREFS_INCLUDE_URL, includeUrl); - editor.putBoolean(PREFS_SEND_REPORT, sendReport); - editor.putString(PREFS_CONTACT_EMAIL, contactEmail); - - // A slight performance improvement via async apply() vs. blocking on commit(). - editor.apply(); - } - - public void onCloseClick(View v) { // bound via crash_reporter.xml - backgroundSendReport(); - } - - public void onRestartClick(View v) { // bound via crash_reporter.xml - doRestart(); - backgroundSendReport(); - } - - private boolean readStringsFromFile(String filePath, Map stringMap) { - try { - BufferedReader reader = new BufferedReader(new FileReader(filePath)); - return readStringsFromReader(reader, stringMap); - } catch (Exception e) { - Log.e(LOGTAG, "exception while reading strings: ", e); - return false; - } - } - - private boolean readStringsFromReader(BufferedReader reader, Map stringMap) throws IOException { - String line; - while ((line = reader.readLine()) != null) { - int equalsPos = -1; - if ((equalsPos = line.indexOf('=')) != -1) { - String key = line.substring(0, equalsPos); - String val = unescape(line.substring(equalsPos + 1)); - stringMap.put(key, val); - } - } - reader.close(); - return true; - } - - private String generateBoundary() { - // Generate some random numbers to fill out the boundary - int r0 = (int)(Integer.MAX_VALUE * Math.random()); - int r1 = (int)(Integer.MAX_VALUE * Math.random()); - return String.format("---------------------------%08X%08X", r0, r1); - } - - private void sendPart(OutputStream os, String boundary, String name, String data) { - try { - os.write(("--" + boundary + "\r\n" + - "Content-Disposition: form-data; name=\"" + name + "\"\r\n" + - "\r\n" + - data + "\r\n" - ).getBytes()); - } catch (Exception ex) { - Log.e(LOGTAG, "Exception when sending \"" + name + "\"", ex); - } - } - - private void sendFile(OutputStream os, String boundary, String name, File file) throws IOException { - os.write(("--" + boundary + "\r\n" + - "Content-Disposition: form-data; name=\"" + name + "\"; " + - "filename=\"" + file.getName() + "\"\r\n" + - "Content-Type: application/octet-stream\r\n" + - "\r\n" - ).getBytes()); - FileChannel fc = new FileInputStream(file).getChannel(); - fc.transferTo(0, fc.size(), Channels.newChannel(os)); - fc.close(); - } - - private String readLogcat() { - final String crashReporterProc = " " + android.os.Process.myPid() + ' '; - BufferedReader br = null; - try { - // get at most the last 400 lines of logcat - Process proc = Runtime.getRuntime().exec(new String[] { - "logcat", "-v", "threadtime", "-t", "400", "-d", "*:D" - }); - StringBuilder sb = new StringBuilder(); - br = new BufferedReader(new InputStreamReader(proc.getInputStream())); - for (String s = br.readLine(); s != null; s = br.readLine()) { - if (s.contains(crashReporterProc)) { - // Don't include logs from the crash reporter's process. - break; - } - sb.append(s).append('\n'); - } - return sb.toString(); - } catch (Exception e) { - return "Unable to get logcat: " + e.toString(); - } finally { - if (br != null) { - try { - br.close(); - } catch (Exception e) { - // ignore - } - } - } - } - - private void sendReport(File minidumpFile, Map extras, File extrasFile) { - Log.i(LOGTAG, "sendReport: " + minidumpFile.getPath()); - final CheckBox includeURLCheckbox = (CheckBox) findViewById(R.id.include_url); - - String spec = extras.get(SERVER_URL_KEY); - if (spec == null) { - doFinish(); - return; - } - - Log.i(LOGTAG, "server url: " + spec); - try { - URL url = new URL(spec); - HttpURLConnection conn = (HttpURLConnection)url.openConnection(); - conn.setRequestMethod("POST"); - String boundary = generateBoundary(); - conn.setDoOutput(true); - conn.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + boundary); - conn.setRequestProperty("Content-Encoding", "gzip"); - - OutputStream os = new GZIPOutputStream(conn.getOutputStream()); - for (String key : extras.keySet()) { - if (key.equals(PAGE_URL_KEY)) { - if (includeURLCheckbox.isChecked()) - sendPart(os, boundary, key, extras.get(key)); - } else if (!key.equals(SERVER_URL_KEY) && !key.equals(NOTES_KEY)) { - sendPart(os, boundary, key, extras.get(key)); - } - } - - // Add some extra information to notes so its displayed by - // crash-stats.mozilla.org. Remove this when bug 607942 is fixed. - StringBuilder sb = new StringBuilder(); - sb.append(extras.containsKey(NOTES_KEY) ? extras.get(NOTES_KEY) + "\n" : ""); - if (AppConstants.MOZ_MIN_CPU_VERSION < 7) { - sb.append("nothumb Build\n"); - } - sb.append(Build.MANUFACTURER).append(' ') - .append(Build.MODEL).append('\n') - .append(Build.FINGERPRINT); - sendPart(os, boundary, NOTES_KEY, sb.toString()); - - sendPart(os, boundary, "Min_ARM_Version", Integer.toString(AppConstants.MOZ_MIN_CPU_VERSION)); - sendPart(os, boundary, "Android_Manufacturer", Build.MANUFACTURER); - sendPart(os, boundary, "Android_Model", Build.MODEL); - sendPart(os, boundary, "Android_Board", Build.BOARD); - sendPart(os, boundary, "Android_Brand", Build.BRAND); - sendPart(os, boundary, "Android_Device", Build.DEVICE); - sendPart(os, boundary, "Android_Display", Build.DISPLAY); - sendPart(os, boundary, "Android_Fingerprint", Build.FINGERPRINT); - sendPart(os, boundary, "Android_APP_ABI", AppConstants.MOZ_APP_ABI); - sendPart(os, boundary, "Android_CPU_ABI", Build.CPU_ABI); - sendPart(os, boundary, "Android_MIN_SDK", Integer.toString(AppConstants.Versions.MIN_SDK_VERSION)); - sendPart(os, boundary, "Android_MAX_SDK", Integer.toString(AppConstants.Versions.MAX_SDK_VERSION)); - try { - sendPart(os, boundary, "Android_CPU_ABI2", Build.CPU_ABI2); - sendPart(os, boundary, "Android_Hardware", Build.HARDWARE); - } catch (Exception ex) { - Log.e(LOGTAG, "Exception while sending SDK version 8 keys", ex); - } - sendPart(os, boundary, "Android_Version", Build.VERSION.SDK_INT + " (" + Build.VERSION.CODENAME + ")"); - if (Versions.feature16Plus && includeURLCheckbox.isChecked()) { - sendPart(os, boundary, "Android_Logcat", readLogcat()); - } - - String comment = ((EditText) findViewById(R.id.comment)).getText().toString(); - if (!TextUtils.isEmpty(comment)) { - sendPart(os, boundary, "Comments", comment); - } - - if (((CheckBox) findViewById(R.id.allow_contact)).isChecked()) { - String email = ((EditText) findViewById(R.id.email)).getText().toString(); - sendPart(os, boundary, "Email", email); - } - - sendPart(os, boundary, PASSED_MINI_DUMP_SUCCESS_KEY, mMinidumpSucceeded ? "True" : "False"); - sendFile(os, boundary, MINI_DUMP_PATH_KEY, minidumpFile); - os.write(("\r\n--" + boundary + "--\r\n").getBytes()); - os.flush(); - os.close(); - BufferedReader br = new BufferedReader( - new InputStreamReader(conn.getInputStream())); - HashMap responseMap = new HashMap(); - readStringsFromReader(br, responseMap); - - if (conn.getResponseCode() == HttpURLConnection.HTTP_OK) { - File submittedDir = new File(getFilesDir(), - SUBMITTED_SUFFIX); - submittedDir.mkdirs(); - minidumpFile.delete(); - extrasFile.delete(); - String crashid = responseMap.get("CrashID"); - File file = new File(submittedDir, crashid + ".txt"); - FileOutputStream fos = new FileOutputStream(file); - fos.write("Crash ID: ".getBytes()); - fos.write(crashid.getBytes()); - fos.close(); - } else { - Log.i(LOGTAG, "Received failure HTTP response code from server: " + conn.getResponseCode()); - } - } catch (IOException e) { - Log.e(LOGTAG, "exception during send: ", e); - } - - doFinish(); - } - - private void doRestart() { - try { - String action = "android.intent.action.MAIN"; - Intent intent = new Intent(action); - intent.setClassName(AppConstants.ANDROID_PACKAGE_NAME, - AppConstants.MOZ_ANDROID_BROWSER_INTENT_CLASS); - intent.putExtra("didRestart", true); - Log.i(LOGTAG, intent.toString()); - startActivity(intent); - } catch (Exception e) { - Log.e(LOGTAG, "error while trying to restart", e); - } - } - - private String unescape(String string) { - return string.replaceAll("\\\\\\\\", "\\").replaceAll("\\\\n", "\n").replaceAll("\\\\t", "\t"); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/CustomEditText.java b/mobile/android/base/java/org/mozilla/gecko/CustomEditText.java deleted file mode 100644 index 98274b752b..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/CustomEditText.java +++ /dev/null @@ -1,89 +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; - -import android.support.v4.content.ContextCompat; -import org.mozilla.gecko.widget.themed.ThemedEditText; - -import android.content.Context; -import android.util.AttributeSet; -import android.view.KeyEvent; -import android.view.View; - -public class CustomEditText extends ThemedEditText { - private OnKeyPreImeListener mOnKeyPreImeListener; - private OnSelectionChangedListener mOnSelectionChangedListener; - private OnWindowFocusChangeListener mOnWindowFocusChangeListener; - private int mHighlightColor; - - public CustomEditText(Context context, AttributeSet attrs) { - super(context, attrs); - setPrivateMode(false); // Initialize mHighlightColor. - } - - public interface OnKeyPreImeListener { - public boolean onKeyPreIme(View v, int keyCode, KeyEvent event); - } - - public void setOnKeyPreImeListener(OnKeyPreImeListener listener) { - mOnKeyPreImeListener = listener; - } - - @Override - public boolean onKeyPreIme(int keyCode, KeyEvent event) { - if (mOnKeyPreImeListener != null) - return mOnKeyPreImeListener.onKeyPreIme(this, keyCode, event); - - return false; - } - - public interface OnSelectionChangedListener { - public void onSelectionChanged(int selStart, int selEnd); - } - - public void setOnSelectionChangedListener(OnSelectionChangedListener listener) { - mOnSelectionChangedListener = listener; - } - - @Override - protected void onSelectionChanged(int selStart, int selEnd) { - if (mOnSelectionChangedListener != null) - mOnSelectionChangedListener.onSelectionChanged(selStart, selEnd); - - super.onSelectionChanged(selStart, selEnd); - } - - public interface OnWindowFocusChangeListener { - public void onWindowFocusChanged(boolean hasFocus); - } - - public void setOnWindowFocusChangeListener(OnWindowFocusChangeListener listener) { - mOnWindowFocusChangeListener = listener; - } - - @Override - public void onWindowFocusChanged(boolean hasFocus) { - super.onWindowFocusChanged(hasFocus); - if (mOnWindowFocusChangeListener != null) - mOnWindowFocusChangeListener.onWindowFocusChanged(hasFocus); - } - - // Provide a getHighlightColor implementation for API level < 16. - @Override - public int getHighlightColor() { - return mHighlightColor; - } - - @Override - public void setPrivateMode(boolean isPrivate) { - super.setPrivateMode(isPrivate); - - mHighlightColor = ContextCompat.getColor(getContext(), isPrivate - ? R.color.url_bar_text_highlight_pb : R.color.fennec_ui_orange); - // android:textColorHighlight cannot support a ColorStateList. - setHighlightColor(mHighlightColor); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/DataReportingNotification.java b/mobile/android/base/java/org/mozilla/gecko/DataReportingNotification.java deleted file mode 100644 index 725c25d6e8..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/DataReportingNotification.java +++ /dev/null @@ -1,133 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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; - -import org.mozilla.gecko.AppConstants.Versions; -import org.mozilla.gecko.preferences.GeckoPreferences; - -import android.app.Notification; -import android.app.NotificationManager; -import android.app.PendingIntent; -import android.content.Context; -import android.content.Intent; -import android.content.SharedPreferences; -import android.content.res.Resources; -import android.graphics.Typeface; -import android.support.v4.app.NotificationCompat; -import android.text.Spannable; -import android.text.SpannableString; -import android.text.TextUtils; -import android.text.style.StyleSpan; - -public class DataReportingNotification { - - private static final String LOGTAG = "DataReportNotification"; - - public static final String ALERT_NAME_DATAREPORTING_NOTIFICATION = "datareporting-notification"; - - private static final String PREFS_POLICY_NOTIFIED_TIME = "datareporting.policy.dataSubmissionPolicyNotifiedTime"; - private static final String PREFS_POLICY_VERSION = "datareporting.policy.dataSubmissionPolicyVersion"; - private static final int DATA_REPORTING_VERSION = 2; - - public static void checkAndNotifyPolicy(Context context) { - SharedPreferences dataPrefs = GeckoSharedPrefs.forApp(context); - final int currentVersion = dataPrefs.getInt(PREFS_POLICY_VERSION, -1); - - if (currentVersion < 1) { - // This is a first run, so notify user about data policy. - notifyDataPolicy(context, dataPrefs); - - // If healthreport is enabled, set default preference value. - if (AppConstants.MOZ_SERVICES_HEALTHREPORT) { - SharedPreferences.Editor editor = dataPrefs.edit(); - editor.putBoolean(GeckoPreferences.PREFS_HEALTHREPORT_UPLOAD_ENABLED, true); - editor.apply(); - } - return; - } - - if (currentVersion == 1) { - // Redisplay notification only for Beta because version 2 updates Beta policy and update version. - if (TextUtils.equals("beta", AppConstants.MOZ_UPDATE_CHANNEL)) { - notifyDataPolicy(context, dataPrefs); - } else { - // Silently update the version. - SharedPreferences.Editor editor = dataPrefs.edit(); - editor.putInt(PREFS_POLICY_VERSION, DATA_REPORTING_VERSION); - editor.apply(); - } - return; - } - - if (currentVersion >= DATA_REPORTING_VERSION) { - // Do nothing, we're at a current (or future) version. - return; - } - } - - /** - * Launch a notification of the data policy, and record notification time and version. - */ - public static void notifyDataPolicy(Context context, SharedPreferences sharedPrefs) { - boolean result = false; - try { - // Launch main App to launch Data choices when notification is clicked. - Intent prefIntent = new Intent(GeckoApp.ACTION_LAUNCH_SETTINGS); - prefIntent.setClassName(AppConstants.ANDROID_PACKAGE_NAME, AppConstants.MOZ_ANDROID_BROWSER_INTENT_CLASS); - - GeckoPreferences.setResourceToOpen(prefIntent, "preferences_privacy"); - prefIntent.putExtra(ALERT_NAME_DATAREPORTING_NOTIFICATION, true); - - PendingIntent contentIntent = PendingIntent.getActivity(context, 0, prefIntent, PendingIntent.FLAG_UPDATE_CURRENT); - final Resources resources = context.getResources(); - - // Create and send notification. - String notificationTitle = resources.getString(R.string.datareporting_notification_title); - String notificationSummary; - if (Versions.preJB) { - notificationSummary = resources.getString(R.string.datareporting_notification_action); - } else { - // Display partial version of Big Style notification for supporting devices. - notificationSummary = resources.getString(R.string.datareporting_notification_summary); - } - String notificationAction = resources.getString(R.string.datareporting_notification_action); - String notificationBigSummary = resources.getString(R.string.datareporting_notification_summary); - - // Make styled ticker text for display in notification bar. - String tickerString = resources.getString(R.string.datareporting_notification_ticker_text); - SpannableString tickerText = new SpannableString(tickerString); - // Bold the notification title of the ticker text, which is the same string as notificationTitle. - tickerText.setSpan(new StyleSpan(Typeface.BOLD), 0, notificationTitle.length(), Spannable.SPAN_INCLUSIVE_EXCLUSIVE); - - Notification notification = new NotificationCompat.Builder(context) - .setContentTitle(notificationTitle) - .setContentText(notificationSummary) - .setSmallIcon(R.drawable.ic_status_logo) - .setAutoCancel(true) - .setContentIntent(contentIntent) - .setStyle(new NotificationCompat.BigTextStyle() - .bigText(notificationBigSummary)) - .addAction(R.drawable.firefox_settings_alert, notificationAction, contentIntent) - .setTicker(tickerText) - .build(); - - NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); - int notificationID = ALERT_NAME_DATAREPORTING_NOTIFICATION.hashCode(); - notificationManager.notify(notificationID, notification); - - // Record version and notification time. - SharedPreferences.Editor editor = sharedPrefs.edit(); - long now = System.currentTimeMillis(); - editor.putLong(PREFS_POLICY_NOTIFIED_TIME, now); - editor.putInt(PREFS_POLICY_VERSION, DATA_REPORTING_VERSION); - editor.apply(); - result = true; - } finally { - // We want to track any errors, so record notification outcome. - Telemetry.sendUIEvent(TelemetryContract.Event.POLICY_NOTIFICATION_SUCCESS, result); - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/DevToolsAuthHelper.java b/mobile/android/base/java/org/mozilla/gecko/DevToolsAuthHelper.java deleted file mode 100644 index 44aaa14a0a..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/DevToolsAuthHelper.java +++ /dev/null @@ -1,52 +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; - -import android.app.Activity; -import android.content.Context; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.util.Log; -import org.mozilla.gecko.util.ActivityResultHandler; -import org.mozilla.gecko.util.EventCallback; -import org.mozilla.gecko.util.InputOptionsUtils; - -/** - * Supports the DevTools WiFi debugging authentication flow by invoking a QR decoder. - */ -public class DevToolsAuthHelper { - - private static final String LOGTAG = "GeckoDevToolsAuthHelper"; - - public static void scan(Context context, final EventCallback callback) { - final Intent intent = InputOptionsUtils.createQRCodeReaderIntent(); - - intent.putExtra("PROMPT_MESSAGE", context.getString(R.string.devtools_auth_scan_header)); - - // Check ahead of time if an activity exists for the intent. This - // avoids a case where we get both an ActivityNotFoundException *and* - // an activity result when the activity is missing. - PackageManager pm = context.getPackageManager(); - if (pm.resolveActivity(intent, 0) == null) { - Log.w(LOGTAG, "PackageManager can't resolve the activity."); - callback.sendError("PackageManager can't resolve the activity."); - return; - } - - ActivityHandlerHelper.startIntent(intent, new ActivityResultHandler() { - @Override - public void onActivityResult(int resultCode, Intent intent) { - if (resultCode == Activity.RESULT_OK) { - String text = intent.getStringExtra("SCAN_RESULT"); - callback.sendSuccess(text); - } else { - callback.sendError(resultCode); - } - } - }); - } - -} diff --git a/mobile/android/base/java/org/mozilla/gecko/DoorHangerPopup.java b/mobile/android/base/java/org/mozilla/gecko/DoorHangerPopup.java deleted file mode 100644 index 9aa3f96a44..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/DoorHangerPopup.java +++ /dev/null @@ -1,361 +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; - -import java.util.HashSet; - -import android.text.TextUtils; -import android.widget.PopupWindow; -import org.json.JSONException; -import org.json.JSONObject; -import org.json.JSONArray; -import org.mozilla.gecko.AppConstants.Versions; -import org.mozilla.gecko.util.GeckoEventListener; -import org.mozilla.gecko.util.ThreadUtils; -import org.mozilla.gecko.widget.AnchoredPopup; -import org.mozilla.gecko.widget.DoorHanger; - -import android.content.Context; -import android.util.Log; -import android.view.View; -import org.mozilla.gecko.widget.DoorhangerConfig; - -public class DoorHangerPopup extends AnchoredPopup - implements GeckoEventListener, - Tabs.OnTabsChangedListener, - PopupWindow.OnDismissListener, - DoorHanger.OnButtonClickListener { - private static final String LOGTAG = "GeckoDoorHangerPopup"; - - // Stores a set of all active DoorHanger notifications. A DoorHanger is - // uniquely identified by its tabId and value. - private final HashSet mDoorHangers; - - // Whether or not the doorhanger popup is disabled. - private boolean mDisabled; - - public DoorHangerPopup(Context context) { - super(context); - - mDoorHangers = new HashSet(); - - GeckoApp.getEventDispatcher().registerGeckoThreadListener(this, - "Doorhanger:Add", - "Doorhanger:Remove"); - Tabs.registerOnTabsChangedListener(this); - - setOnDismissListener(this); - } - - void destroy() { - GeckoApp.getEventDispatcher().unregisterGeckoThreadListener(this, - "Doorhanger:Add", - "Doorhanger:Remove"); - Tabs.unregisterOnTabsChangedListener(this); - } - - /** - * Temporarily disables the doorhanger popup. If the popup is disabled, - * it will not be shown to the user, but it will continue to process - * calls to add/remove doorhanger notifications. - */ - void disable() { - mDisabled = true; - updatePopup(); - } - - /** - * Re-enables the doorhanger popup. - */ - void enable() { - mDisabled = false; - updatePopup(); - } - - @Override - public void handleMessage(String event, JSONObject geckoObject) { - try { - if (event.equals("Doorhanger:Add")) { - final DoorhangerConfig config = makeConfigFromJSON(geckoObject); - - ThreadUtils.postToUiThread(new Runnable() { - @Override - public void run() { - addDoorHanger(config); - } - }); - } else if (event.equals("Doorhanger:Remove")) { - final int tabId = geckoObject.getInt("tabID"); - final String value = geckoObject.getString("value"); - - ThreadUtils.postToUiThread(new Runnable() { - @Override - public void run() { - DoorHanger doorHanger = getDoorHanger(tabId, value); - if (doorHanger == null) - return; - - removeDoorHanger(doorHanger); - updatePopup(); - } - }); - } - } catch (Exception e) { - Log.e(LOGTAG, "Exception handling message \"" + event + "\":", e); - } - } - - private DoorhangerConfig makeConfigFromJSON(JSONObject json) throws JSONException { - final int tabId = json.getInt("tabID"); - final String id = json.getString("value"); - - final String typeString = json.optString("category"); - DoorHanger.Type doorhangerType = DoorHanger.Type.DEFAULT; - if (DoorHanger.Type.LOGIN.toString().equals(typeString)) { - doorhangerType = DoorHanger.Type.LOGIN; - } else if (DoorHanger.Type.GEOLOCATION.toString().equals(typeString)) { - doorhangerType = DoorHanger.Type.GEOLOCATION; - } else if (DoorHanger.Type.DESKTOPNOTIFICATION2.toString().equals(typeString)) { - doorhangerType = DoorHanger.Type.DESKTOPNOTIFICATION2; - } else if (DoorHanger.Type.WEBRTC.toString().equals(typeString)) { - doorhangerType = DoorHanger.Type.WEBRTC; - } else if (DoorHanger.Type.VIBRATION.toString().equals(typeString)) { - doorhangerType = DoorHanger.Type.VIBRATION; - } - - final DoorhangerConfig config = new DoorhangerConfig(tabId, id, doorhangerType, this); - - config.setMessage(json.getString("message")); - config.setOptions(json.getJSONObject("options")); - - final JSONArray buttonArray = json.getJSONArray("buttons"); - int numButtons = buttonArray.length(); - if (numButtons > 2) { - Log.e(LOGTAG, "Doorhanger can have a maximum of two buttons!"); - numButtons = 2; - } - - for (int i = 0; i < numButtons; i++) { - final JSONObject buttonJSON = buttonArray.getJSONObject(i); - final boolean isPositive = buttonJSON.optBoolean("positive", false); - config.setButton(buttonJSON.getString("label"), buttonJSON.getInt("callback"), isPositive); - } - - return config; - } - - // This callback is automatically executed on the UI thread. - @Override - public void onTabChanged(final Tab tab, final Tabs.TabEvents msg, final String data) { - switch (msg) { - case CLOSED: - // Remove any doorhangers for a tab when it's closed (make - // a temporary set to avoid a ConcurrentModificationException) - removeTabDoorHangers(tab.getId(), true); - break; - - case LOCATION_CHANGE: - // Only remove doorhangers if the popup is hidden or if we're navigating to a new URL - if (!isShowing() || !data.equals(tab.getURL())) - removeTabDoorHangers(tab.getId(), false); - - // Update the popup if the location change was on the current tab - if (Tabs.getInstance().isSelectedTab(tab)) - updatePopup(); - break; - - case SELECTED: - // Always update the popup when a new tab is selected. This will cover cases - // where a different tab was closed, since we always need to select a new tab. - updatePopup(); - break; - } - } - - /** - * Adds a doorhanger. - * - * This method must be called on the UI thread. - */ - void addDoorHanger(DoorhangerConfig config) { - final int tabId = config.getTabId(); - // Don't add a doorhanger for a tab that doesn't exist - if (Tabs.getInstance().getTab(tabId) == null) { - return; - } - - // Replace the doorhanger if it already exists - DoorHanger oldDoorHanger = getDoorHanger(tabId, config.getId()); - if (oldDoorHanger != null) { - removeDoorHanger(oldDoorHanger); - } - - if (!mInflated) { - init(); - } - - final DoorHanger newDoorHanger = DoorHanger.Get(mContext, config); - - mDoorHangers.add(newDoorHanger); - mContent.addView(newDoorHanger); - - // Only update the popup if we're adding a notification to the selected tab - if (tabId == Tabs.getInstance().getSelectedTab().getId()) - updatePopup(); - } - - - /* - * DoorHanger.OnButtonClickListener implementation - */ - @Override - public void onButtonClick(JSONObject response, DoorHanger doorhanger) { - GeckoAppShell.notifyObservers("Doorhanger:Reply", response.toString()); - removeDoorHanger(doorhanger); - updatePopup(); - } - - /** - * Gets a doorhanger. - * - * This method must be called on the UI thread. - */ - DoorHanger getDoorHanger(int tabId, String value) { - for (DoorHanger dh : mDoorHangers) { - if (dh.getTabId() == tabId && dh.getIdentifier().equals(value)) - return dh; - } - - // If there's no doorhanger for the given tabId and value, return null - return null; - } - - /** - * Removes a doorhanger. - * - * This method must be called on the UI thread. - */ - void removeDoorHanger(final DoorHanger doorHanger) { - mDoorHangers.remove(doorHanger); - mContent.removeView(doorHanger); - } - - /** - * Removes doorhangers for a given tab. - * @param tabId identifier of the tab to remove doorhangers from - * @param forceRemove boolean for force-removing tabs. If true, all doorhangers associated - * with the tab specified are removed; if false, only remove the doorhangers - * that are not persistent, as specified by the doorhanger options. - * - * This method must be called on the UI thread. - */ - void removeTabDoorHangers(int tabId, boolean forceRemove) { - // Make a temporary set to avoid a ConcurrentModificationException - HashSet doorHangersToRemove = new HashSet(); - for (DoorHanger dh : mDoorHangers) { - // Only remove transient doorhangers for the given tab - if (dh.getTabId() == tabId - && (forceRemove || (!forceRemove && dh.shouldRemove(isShowing())))) { - doorHangersToRemove.add(dh); - } - } - - for (DoorHanger dh : doorHangersToRemove) { - removeDoorHanger(dh); - } - } - - /** - * Updates the popup state. - * - * This method must be called on the UI thread. - */ - void updatePopup() { - // Bail if the selected tab is null, if there are no active doorhangers, - // if we haven't inflated the layout yet (this can happen if updatePopup() - // is called before the runnable from addDoorHanger() runs), or if the - // doorhanger popup is temporarily disabled. - Tab tab = Tabs.getInstance().getSelectedTab(); - if (tab == null || mDoorHangers.size() == 0 || !mInflated || mDisabled) { - dismiss(); - return; - } - - // Show doorhangers for the selected tab - int tabId = tab.getId(); - boolean shouldShowPopup = false; - DoorHanger firstDoorhanger = null; - for (DoorHanger dh : mDoorHangers) { - if (dh.getTabId() == tabId) { - dh.setVisibility(View.VISIBLE); - shouldShowPopup = true; - if (firstDoorhanger == null) { - firstDoorhanger = dh; - } else { - dh.hideTitle(); - } - } else { - dh.setVisibility(View.GONE); - } - } - - // Dismiss the popup if there are no doorhangers to show for this tab - if (!shouldShowPopup) { - dismiss(); - return; - } - - showDividers(); - - final String baseDomain = tab.getBaseDomain(); - - if (TextUtils.isEmpty(baseDomain)) { - firstDoorhanger.hideTitle(); - } else { - firstDoorhanger.showTitle(tab.getFavicon(), baseDomain); - } - - if (isShowing()) { - show(); - return; - } - - setFocusable(true); - - show(); - } - - //Show all inter-DoorHanger dividers (ie. Dividers on all visible DoorHangers except the last one) - private void showDividers() { - int count = mContent.getChildCount(); - DoorHanger lastVisibleDoorHanger = null; - - for (int i = 0; i < count; i++) { - DoorHanger dh = (DoorHanger) mContent.getChildAt(i); - dh.showDivider(); - if (dh.getVisibility() == View.VISIBLE) { - lastVisibleDoorHanger = dh; - } - } - if (lastVisibleDoorHanger != null) { - lastVisibleDoorHanger.hideDivider(); - } - } - - @Override - public void onDismiss() { - final int tabId = Tabs.getInstance().getSelectedTab().getId(); - removeTabDoorHangers(tabId, true); - } - - @Override - public void dismiss() { - // If the popup is focusable while it is hidden, we run into crashes - // on pre-ICS devices when the popup gets focus before it is shown. - setFocusable(false); - super.dismiss(); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/DownloadsIntegration.java b/mobile/android/base/java/org/mozilla/gecko/DownloadsIntegration.java deleted file mode 100644 index ff3ac6110a..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/DownloadsIntegration.java +++ /dev/null @@ -1,235 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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; - -import org.mozilla.gecko.annotation.WrapForJNI; -import org.mozilla.gecko.AppConstants.Versions; -import org.mozilla.gecko.permissions.Permissions; -import org.mozilla.gecko.util.NativeEventListener; -import org.mozilla.gecko.util.NativeJSObject; -import org.mozilla.gecko.util.EventCallback; - -import java.io.File; -import java.lang.IllegalArgumentException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -import android.app.DownloadManager; -import android.content.Context; -import android.content.pm.PackageManager; -import android.database.Cursor; -import android.media.MediaScannerConnection; -import android.media.MediaScannerConnection.MediaScannerConnectionClient; -import android.net.Uri; -import android.os.Environment; -import android.text.TextUtils; -import android.util.Log; - -public class DownloadsIntegration implements NativeEventListener -{ - private static final String LOGTAG = "GeckoDownloadsIntegration"; - - private static final List UNKNOWN_MIME_TYPES; - static { - final ArrayList tempTypes = new ArrayList<>(3); - tempTypes.add("unknown/unknown"); // This will be used as a default mime type for unknown files - tempTypes.add("application/unknown"); - tempTypes.add("application/octet-stream"); // Github uses this for APK files - UNKNOWN_MIME_TYPES = Collections.unmodifiableList(tempTypes); - } - - private static final String DOWNLOAD_REMOVE = "Download:Remove"; - - private DownloadsIntegration() { - EventDispatcher.getInstance().registerGeckoThreadListener((NativeEventListener)this, DOWNLOAD_REMOVE); - } - - private static DownloadsIntegration sInstance; - - private static class Download { - final File file; - final long id; - - final private static int UNKNOWN_ID = -1; - - public Download(final String path) { - this(path, UNKNOWN_ID); - } - - public Download(final String path, final long id) { - file = new File(path); - this.id = id; - } - - public static Download fromJSON(final NativeJSObject obj) { - final String path = obj.getString("path"); - return new Download(path); - } - - public static Download fromCursor(final Cursor c) { - final String path = c.getString(c.getColumnIndexOrThrow(DownloadManager.COLUMN_LOCAL_FILENAME)); - final long id = c.getLong(c.getColumnIndexOrThrow(DownloadManager.COLUMN_ID)); - return new Download(path, id); - } - - public boolean equals(final Download download) { - return file.equals(download.file); - } - } - - public static void init() { - if (sInstance == null) { - sInstance = new DownloadsIntegration(); - } - } - - @Override - public void handleMessage(final String event, final NativeJSObject message, - final EventCallback callback) { - if (DOWNLOAD_REMOVE.equals(event)) { - final Download d = Download.fromJSON(message); - removeDownload(d); - } - } - - private static boolean useSystemDownloadManager() { - if (!AppConstants.ANDROID_DOWNLOADS_INTEGRATION) { - return false; - } - - int state = PackageManager.COMPONENT_ENABLED_STATE_DEFAULT; - try { - state = GeckoAppShell.getContext().getPackageManager().getApplicationEnabledSetting("com.android.providers.downloads"); - } catch (IllegalArgumentException e) { - // Download Manager package does not exist - return false; - } - - return (PackageManager.COMPONENT_ENABLED_STATE_ENABLED == state || - PackageManager.COMPONENT_ENABLED_STATE_DEFAULT == state); - } - - @WrapForJNI(calledFrom = "gecko") - public static String getTemporaryDownloadDirectory() { - Context context = GeckoAppShell.getApplicationContext(); - - if (Permissions.has(context, android.Manifest.permission.WRITE_EXTERNAL_STORAGE)) { - // We do have the STORAGE permission, so we can save the file directly to the public - // downloads directory. - return Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) - .getAbsolutePath(); - } else { - // Without the permission we are going to start to download the file to the cache - // directory. Later in the process we will ask for the permission and the download - // process will move the file to the actual downloads directory. If we do not get the - // permission then the download will be cancelled. - return context.getCacheDir().getAbsolutePath(); - } - } - - - @WrapForJNI(calledFrom = "gecko") - public static void scanMedia(final String aFile, String aMimeType) { - String mimeType = aMimeType; - if (UNKNOWN_MIME_TYPES.contains(mimeType)) { - // If this is a generic undefined mimetype, erase it so that we can try to determine - // one from the file extension below. - mimeType = ""; - } - - // If the platform didn't give us a mimetype, try to guess one from the filename - if (TextUtils.isEmpty(mimeType)) { - final int extPosition = aFile.lastIndexOf("."); - if (extPosition > 0 && extPosition < aFile.length() - 1) { - mimeType = GeckoAppShell.getMimeTypeFromExtension(aFile.substring(extPosition + 1)); - } - } - - // addCompletedDownload will throw if it received any null parameters. Use aMimeType or a default - // if we still don't have one. - if (TextUtils.isEmpty(mimeType)) { - if (TextUtils.isEmpty(aMimeType)) { - mimeType = UNKNOWN_MIME_TYPES.get(0); - } else { - mimeType = aMimeType; - } - } - - if (useSystemDownloadManager()) { - final File f = new File(aFile); - final DownloadManager dm = (DownloadManager) GeckoAppShell.getContext().getSystemService(Context.DOWNLOAD_SERVICE); - dm.addCompletedDownload(f.getName(), - f.getName(), - true, // Media scanner should scan this - mimeType, - f.getAbsolutePath(), - Math.max(1, f.length()), // Some versions of Android require downloads to be at least length 1 - false); // Don't show a notification. - } else { - final Context context = GeckoAppShell.getContext(); - final GeckoMediaScannerClient client = new GeckoMediaScannerClient(context, aFile, mimeType); - client.connect(); - } - } - - public static void removeDownload(final Download download) { - if (!useSystemDownloadManager()) { - return; - } - - final DownloadManager dm = (DownloadManager) GeckoAppShell.getContext().getSystemService(Context.DOWNLOAD_SERVICE); - - Cursor c = null; - try { - c = dm.query((new DownloadManager.Query()).setFilterByStatus(DownloadManager.STATUS_SUCCESSFUL)); - if (c == null || !c.moveToFirst()) { - return; - } - - do { - final Download d = Download.fromCursor(c); - // Try hard as we can to verify this download is the one we think it is - if (download.equals(d)) { - dm.remove(d.id); - } - } while (c.moveToNext()); - } finally { - if (c != null) { - c.close(); - } - } - } - - private static final class GeckoMediaScannerClient implements MediaScannerConnectionClient { - private final String mFile; - private final String mMimeType; - private MediaScannerConnection mScanner; - - public GeckoMediaScannerClient(Context context, String file, String mimeType) { - mFile = file; - mMimeType = mimeType; - mScanner = new MediaScannerConnection(context, this); - } - - public void connect() { - mScanner.connect(); - } - - @Override - public void onMediaScannerConnected() { - mScanner.scanFile(mFile, mMimeType); - } - - @Override - public void onScanCompleted(String path, Uri uri) { - if (path.equals(mFile)) { - mScanner.disconnect(); - mScanner = null; - } - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/DynamicToolbar.java b/mobile/android/base/java/org/mozilla/gecko/DynamicToolbar.java deleted file mode 100644 index 28f542d5c8..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/DynamicToolbar.java +++ /dev/null @@ -1,218 +0,0 @@ -package org.mozilla.gecko; - -import org.mozilla.gecko.PrefsHelper.PrefHandlerBase; -import org.mozilla.gecko.gfx.DynamicToolbarAnimator.PinReason; -import org.mozilla.gecko.gfx.LayerView; -import org.mozilla.gecko.util.ThreadUtils; - -import android.os.Build; -import android.os.Bundle; -import android.util.Log; - -public class DynamicToolbar { - private static final String LOGTAG = "DynamicToolbar"; - - private static final String STATE_ENABLED = "dynamic_toolbar"; - private static final String CHROME_PREF = "browser.chrome.dynamictoolbar"; - - // DynamicToolbar is enabled iff prefEnabled is true *and* accessibilityEnabled is false, - // so it is disabled by default on startup. We do not enable it until we explicitly get - // the pref from Gecko telling us to turn it on. - private volatile boolean prefEnabled; - private boolean accessibilityEnabled; - // On some device we have to force-disable the dynamic toolbar because of - // bugs in the Android code. See bug 1231554. - private final boolean forceDisabled; - - private final PrefsHelper.PrefHandler prefObserver; - private LayerView layerView; - private OnEnabledChangedListener enabledChangedListener; - private boolean temporarilyVisible; - - public enum VisibilityTransition { - IMMEDIATE, - ANIMATE - } - - /** - * Listener for changes to the dynamic toolbar's enabled state. - */ - public interface OnEnabledChangedListener { - /** - * This callback is executed on the UI thread. - */ - public void onEnabledChanged(boolean enabled); - } - - public DynamicToolbar() { - // Listen to the dynamic toolbar pref - prefObserver = new PrefHandler(); - PrefsHelper.addObserver(new String[] { CHROME_PREF }, prefObserver); - forceDisabled = isForceDisabled(); - if (forceDisabled) { - Log.i(LOGTAG, "Force-disabling dynamic toolbar for " + Build.MODEL + " (" + Build.DEVICE + "/" + Build.PRODUCT + ")"); - } - } - - public static boolean isForceDisabled() { - // Force-disable dynamic toolbar on the variants of the Galaxy Note 10.1 - // and Note 8.0 running Android 4.1.2. (Bug 1231554). This includes - // the following model numbers: - // GT-N8000, GT-N8005, GT-N8010, GT-N8013, GT-N8020 - // GT-N5100, GT-N5110, GT-N5120 - if (Build.VERSION.SDK_INT == Build.VERSION_CODES.JELLY_BEAN - && (Build.MODEL.startsWith("GT-N80") || - Build.MODEL.startsWith("GT-N51"))) { - return true; - } - // Also disable variants of the Galaxy Note 4 on Android 5.0.1 (Bug 1301593) - if (Build.VERSION.SDK_INT == Build.VERSION_CODES.LOLLIPOP - && (Build.MODEL.startsWith("SM-N910"))) { - return true; - } - return false; - } - - public void destroy() { - PrefsHelper.removeObserver(prefObserver); - } - - public void setLayerView(LayerView layerView) { - ThreadUtils.assertOnUiThread(); - - this.layerView = layerView; - } - - public void setEnabledChangedListener(OnEnabledChangedListener listener) { - ThreadUtils.assertOnUiThread(); - - enabledChangedListener = listener; - } - - public void onSaveInstanceState(Bundle outState) { - ThreadUtils.assertOnUiThread(); - - outState.putBoolean(STATE_ENABLED, prefEnabled); - } - - public void onRestoreInstanceState(Bundle savedInstanceState) { - ThreadUtils.assertOnUiThread(); - - if (savedInstanceState != null) { - prefEnabled = savedInstanceState.getBoolean(STATE_ENABLED); - } - } - - public boolean isEnabled() { - ThreadUtils.assertOnUiThread(); - - if (forceDisabled) { - return false; - } - - return prefEnabled && !accessibilityEnabled; - } - - public void setAccessibilityEnabled(boolean enabled) { - ThreadUtils.assertOnUiThread(); - - if (accessibilityEnabled == enabled) { - return; - } - - // Disable the dynamic toolbar when accessibility features are enabled, - // and re-read the preference when they're disabled. - accessibilityEnabled = enabled; - if (prefEnabled) { - triggerEnabledListener(); - } - } - - public void setVisible(boolean visible, VisibilityTransition transition) { - ThreadUtils.assertOnUiThread(); - - if (layerView == null) { - return; - } - - // Don't hide the ActionBar/Toolbar, if it's pinned open by TextSelection. - if (visible == false && - layerView.getDynamicToolbarAnimator().isPinnedBy(PinReason.ACTION_MODE)) { - return; - } - - final boolean isImmediate = transition == VisibilityTransition.IMMEDIATE; - if (visible) { - layerView.getDynamicToolbarAnimator().showToolbar(isImmediate); - } else { - layerView.getDynamicToolbarAnimator().hideToolbar(isImmediate); - } - } - - public void setTemporarilyVisible(boolean visible, VisibilityTransition transition) { - ThreadUtils.assertOnUiThread(); - - if (layerView == null) { - return; - } - - if (visible == temporarilyVisible) { - // nothing to do - return; - } - - temporarilyVisible = visible; - final boolean isImmediate = transition == VisibilityTransition.IMMEDIATE; - if (visible) { - layerView.getDynamicToolbarAnimator().showToolbar(isImmediate); - } else { - layerView.getDynamicToolbarAnimator().hideToolbar(isImmediate); - } - } - - public void persistTemporaryVisibility() { - ThreadUtils.assertOnUiThread(); - - if (temporarilyVisible) { - temporarilyVisible = false; - setVisible(true, VisibilityTransition.IMMEDIATE); - } - } - - public void setPinned(boolean pinned, PinReason reason) { - ThreadUtils.assertOnUiThread(); - if (layerView == null) { - return; - } - - layerView.getDynamicToolbarAnimator().setPinned(pinned, reason); - } - - private void triggerEnabledListener() { - if (enabledChangedListener != null) { - enabledChangedListener.onEnabledChanged(isEnabled()); - } - } - - private class PrefHandler extends PrefHandlerBase { - @Override - public void prefValue(String pref, boolean value) { - if (value == prefEnabled) { - return; - } - - prefEnabled = value; - - ThreadUtils.postToUiThread(new Runnable() { - @Override - public void run() { - // If accessibility is enabled, the dynamic toolbar is - // forced to be off. - if (!accessibilityEnabled) { - triggerEnabledListener(); - } - } - }); - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/EditBookmarkDialog.java b/mobile/android/base/java/org/mozilla/gecko/EditBookmarkDialog.java deleted file mode 100644 index 38c38a9eba..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/EditBookmarkDialog.java +++ /dev/null @@ -1,252 +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; - -import org.mozilla.gecko.db.BrowserDB; -import org.mozilla.gecko.db.BrowserContract.Bookmarks; -import org.mozilla.gecko.util.ThreadUtils; -import org.mozilla.gecko.util.UIAsyncTask; - -import android.app.Activity; -import android.content.ContentResolver; -import android.content.Context; -import android.app.AlertDialog; -import android.content.DialogInterface; -import android.database.Cursor; -import android.support.design.widget.Snackbar; -import android.text.Editable; -import android.text.TextWatcher; -import android.view.LayoutInflater; -import android.view.View; -import android.widget.EditText; - -/** - * A dialog that allows editing a bookmarks url, title, or keywords - *

- * Invoked by calling one of the {@link org.mozilla.gecko.EditBookmarkDialog#show(String)} - * methods. - */ -public class EditBookmarkDialog { - private final Context mContext; - - public EditBookmarkDialog(Context context) { - mContext = context; - } - - /** - * A private struct to make it easier to pass bookmark data across threads - */ - private class Bookmark { - final int id; - final String title; - final String url; - final String keyword; - - public Bookmark(int aId, String aTitle, String aUrl, String aKeyword) { - id = aId; - title = aTitle; - url = aUrl; - keyword = aKeyword; - } - } - - /** - * This text watcher to enable or disable the OK button if the dialog contains - * valid information. This class is overridden to do data checking on different fields. - * By itself, it always enables the button. - * - * Callers can also assign a paired partner to the TextWatcher, and callers will check - * that both are enabled before enabling the ok button. - */ - private class EditBookmarkTextWatcher implements TextWatcher { - // A stored reference to the dialog containing the text field being watched - protected AlertDialog mDialog; - - // A stored text watcher to do the real verification of a field - protected EditBookmarkTextWatcher mPairedTextWatcher; - - // Whether or not the ok button should be enabled. - protected boolean mEnabled = true; - - public EditBookmarkTextWatcher(AlertDialog aDialog) { - mDialog = aDialog; - } - - public void setPairedTextWatcher(EditBookmarkTextWatcher aTextWatcher) { - mPairedTextWatcher = aTextWatcher; - } - - public boolean isEnabled() { - return mEnabled; - } - - // Textwatcher interface - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) { - // Disable if the we're disabled or the paired partner is disabled - boolean enabled = mEnabled && (mPairedTextWatcher == null || mPairedTextWatcher.isEnabled()); - mDialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(enabled); - } - - @Override - public void afterTextChanged(Editable s) {} - @Override - public void beforeTextChanged(CharSequence s, int start, int count, int after) {} - } - - /** - * A version of the EditBookmarkTextWatcher for the url field of the dialog. - * Only checks if the field is empty or not. - */ - private class LocationTextWatcher extends EditBookmarkTextWatcher { - public LocationTextWatcher(AlertDialog aDialog) { - super(aDialog); - } - - // Disables the ok button if the location field is empty. - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) { - mEnabled = (s.toString().trim().length() > 0); - super.onTextChanged(s, start, before, count); - } - } - - /** - * A version of the EditBookmarkTextWatcher for the keyword field of the dialog. - * Checks if the field has any (non leading or trailing) spaces. - */ - private class KeywordTextWatcher extends EditBookmarkTextWatcher { - public KeywordTextWatcher(AlertDialog aDialog) { - super(aDialog); - } - - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) { - // Disable if the keyword contains spaces - mEnabled = (s.toString().trim().indexOf(' ') == -1); - super.onTextChanged(s, start, before, count); - } - } - - /** - * Show the Edit bookmark dialog for a particular url. If the url is bookmarked multiple times - * this will just edit the first instance it finds. - * - * @param url The url of the bookmark to edit. The dialog will look up other information like the id, - * current title, or keywords associated with this url. If the url isn't bookmarked, the - * dialog will fail silently. If the url is bookmarked multiple times, this will only show - * information about the first it finds. - */ - public void show(final String url) { - final ContentResolver cr = mContext.getContentResolver(); - final BrowserDB db = BrowserDB.from(mContext); - (new UIAsyncTask.WithoutParams(ThreadUtils.getBackgroundHandler()) { - @Override - public Bookmark doInBackground() { - final Cursor cursor = db.getBookmarkForUrl(cr, url); - if (cursor == null) { - return null; - } - - Bookmark bookmark = null; - try { - cursor.moveToFirst(); - bookmark = new Bookmark(cursor.getInt(cursor.getColumnIndexOrThrow(Bookmarks._ID)), - cursor.getString(cursor.getColumnIndexOrThrow(Bookmarks.TITLE)), - cursor.getString(cursor.getColumnIndexOrThrow(Bookmarks.URL)), - cursor.getString(cursor.getColumnIndexOrThrow(Bookmarks.KEYWORD))); - } finally { - cursor.close(); - } - return bookmark; - } - - @Override - public void onPostExecute(Bookmark bookmark) { - if (bookmark == null) { - return; - } - - show(bookmark.id, bookmark.title, bookmark.url, bookmark.keyword); - } - }).execute(); - } - - /** - * Show the Edit bookmark dialog for a set of data. This will show the dialog whether - * a bookmark with this url exists or not, but the results will NOT be saved if the id - * is not a valid bookmark id. - * - * @param id The id of the bookmark to change. If there is no bookmark with this ID, the dialog - * will fail silently. - * @param title The initial title to show in the dialog - * @param url The initial url to show in the dialog - * @param keyword The initial keyword to show in the dialog - */ - public void show(final int id, final String title, final String url, final String keyword) { - final Context context = mContext; - - AlertDialog.Builder editPrompt = new AlertDialog.Builder(context); - final View editView = LayoutInflater.from(context).inflate(R.layout.bookmark_edit, null); - editPrompt.setTitle(R.string.bookmark_edit_title); - editPrompt.setView(editView); - - final EditText nameText = ((EditText) editView.findViewById(R.id.edit_bookmark_name)); - final EditText locationText = ((EditText) editView.findViewById(R.id.edit_bookmark_location)); - final EditText keywordText = ((EditText) editView.findViewById(R.id.edit_bookmark_keyword)); - nameText.setText(title); - locationText.setText(url); - keywordText.setText(keyword); - - final BrowserDB db = BrowserDB.from(mContext); - editPrompt.setPositiveButton(R.string.button_ok, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int whichButton) { - (new UIAsyncTask.WithoutParams(ThreadUtils.getBackgroundHandler()) { - @Override - public Void doInBackground() { - String newUrl = locationText.getText().toString().trim(); - String newKeyword = keywordText.getText().toString().trim(); - - db.updateBookmark(context.getContentResolver(), id, newUrl, nameText.getText().toString(), newKeyword); - return null; - } - - @Override - public void onPostExecute(Void result) { - SnackbarBuilder.builder((Activity) context) - .message(R.string.bookmark_updated) - .duration(Snackbar.LENGTH_LONG) - .buildAndShow(); - } - }).execute(); - } - }); - - editPrompt.setNegativeButton(R.string.button_cancel, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int whichButton) { - // do nothing - } - }); - - final AlertDialog dialog = editPrompt.create(); - - // Create our TextWatchers - LocationTextWatcher locationTextWatcher = new LocationTextWatcher(dialog); - KeywordTextWatcher keywordTextWatcher = new KeywordTextWatcher(dialog); - - // Cross reference the TextWatchers - locationTextWatcher.setPairedTextWatcher(keywordTextWatcher); - keywordTextWatcher.setPairedTextWatcher(locationTextWatcher); - - // Add the TextWatcher Listeners - locationText.addTextChangedListener(locationTextWatcher); - keywordText.addTextChangedListener(keywordTextWatcher); - - dialog.show(); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/Experiments.java b/mobile/android/base/java/org/mozilla/gecko/Experiments.java deleted file mode 100644 index e71bb4c52f..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/Experiments.java +++ /dev/null @@ -1,119 +0,0 @@ -/* 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; - -import android.content.Context; - -import android.util.Log; -import android.text.TextUtils; - -import com.keepsafe.switchboard.Preferences; -import com.keepsafe.switchboard.SwitchBoard; - -import java.util.LinkedList; -import java.util.List; - -/** - * This class should reflect the experiment names found in the Switchboard experiments config here: - * https://github.com/mozilla-services/switchboard-experiments - */ -public class Experiments { - private static final String LOGTAG = "GeckoExperiments"; - - // Show a system notification linking to a "What's New" page on app update. - public static final String WHATSNEW_NOTIFICATION = "whatsnew-notification"; - - // Subscribe to known, bookmarked sites and show a notification if new content is available. - public static final String CONTENT_NOTIFICATIONS_12HRS = "content-notifications-12hrs"; - public static final String CONTENT_NOTIFICATIONS_8AM = "content-notifications-8am"; - public static final String CONTENT_NOTIFICATIONS_5PM = "content-notifications-5pm"; - - // Onboarding: "Features and Story". These experiments are determined - // on the client, they are not part of the server config. - public static final String ONBOARDING3_A = "onboarding3-a"; // Control: No first run - public static final String ONBOARDING3_B = "onboarding3-b"; // 4 static Feature + 1 dynamic slides - public static final String ONBOARDING3_C = "onboarding3-c"; // Differentiating features slides - - // Synchronizing the catalog of downloadable content from Kinto - public static final String DOWNLOAD_CONTENT_CATALOG_SYNC = "download-content-catalog-sync"; - - // Promotion for "Add to homescreen" - public static final String PROMOTE_ADD_TO_HOMESCREEN = "promote-add-to-homescreen"; - - public static final String PREF_ONBOARDING_VERSION = "onboarding_version"; - - // Promotion to bookmark reader-view items after entering reader view three times (Bug 1247689) - public static final String TRIPLE_READERVIEW_BOOKMARK_PROMPT = "triple-readerview-bookmark-prompt"; - - // Only show origin in URL bar instead of full URL (Bug 1236431) - public static final String URLBAR_SHOW_ORIGIN_ONLY = "urlbar-show-origin-only"; - - // Show name of organization (EV cert) instead of full URL in URL bar (Bug 1249594). - public static final String URLBAR_SHOW_EV_CERT_OWNER = "urlbar-show-ev-cert-owner"; - - // Play HLS videos in a VideoView (Bug 1313391) - public static final String HLS_VIDEO_PLAYBACK = "hls-video-playback"; - - // Make new activity stream panel available (to replace top sites) (Bug 1313316) - public static final String ACTIVITY_STREAM = "activity-stream"; - - /** - * Returns if a user is in certain local experiment. - * @param experiment Name of experiment to look up - * @return returns value for experiment or false if experiment does not exist. - */ - public static boolean isInExperimentLocal(Context context, String experiment) { - if (SwitchBoard.isInBucket(context, 0, 20)) { - return Experiments.ONBOARDING3_A.equals(experiment); - } else if (SwitchBoard.isInBucket(context, 20, 60)) { - return Experiments.ONBOARDING3_B.equals(experiment); - } else if (SwitchBoard.isInBucket(context, 60, 100)) { - return Experiments.ONBOARDING3_C.equals(experiment); - } else { - return false; - } - } - - /** - * Returns list of all active experiments, remote and local. - * @return List of experiment names Strings - */ - public static List getActiveExperiments(Context c) { - final List experiments = new LinkedList<>(); - experiments.addAll(SwitchBoard.getActiveExperiments(c)); - - // Add onboarding version. - final String onboardingExperiment = GeckoSharedPrefs.forProfile(c).getString(Experiments.PREF_ONBOARDING_VERSION, null); - if (!TextUtils.isEmpty(onboardingExperiment)) { - experiments.add(onboardingExperiment); - } - - return experiments; - } - - /** - * Sets an override to force an experiment to be enabled or disabled. This value - * will be read and used before reading the switchboard server configuration. - * - * @param c Context - * @param experimentName Experiment name - * @param isEnabled Whether or not the experiment should be enabled - */ - public static void setOverride(Context c, String experimentName, boolean isEnabled) { - Log.d(LOGTAG, "setOverride: " + experimentName + " = " + isEnabled); - Preferences.setOverrideValue(c, experimentName, isEnabled); - } - - /** - * Clears the override value for an experiment. - * - * @param c Context - * @param experimentName Experiment name - */ - public static void clearOverride(Context c, String experimentName) { - Log.d(LOGTAG, "clearOverride: " + experimentName); - Preferences.clearOverrideValue(c, experimentName); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/FilePicker.java b/mobile/android/base/java/org/mozilla/gecko/FilePicker.java deleted file mode 100644 index 8ac5428a44..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/FilePicker.java +++ /dev/null @@ -1,227 +0,0 @@ -/* 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; - -import org.mozilla.gecko.GeckoAppShell; -import org.mozilla.gecko.util.GeckoEventListener; - -import org.json.JSONException; -import org.json.JSONObject; - -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.content.pm.ResolveInfo; -import android.net.Uri; -import android.os.Environment; -import android.os.Parcelable; -import android.provider.MediaStore; -import android.text.TextUtils; -import android.util.Log; - -import java.io.File; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; - -public class FilePicker implements GeckoEventListener { - private static final String LOGTAG = "GeckoFilePicker"; - private static FilePicker sFilePicker; - private final Context context; - - public interface ResultHandler { - public void gotFile(String filename); - } - - public static void init(Context context) { - if (sFilePicker == null) { - sFilePicker = new FilePicker(context.getApplicationContext()); - } - } - - protected FilePicker(Context context) { - this.context = context; - EventDispatcher.getInstance().registerGeckoThreadListener(this, "FilePicker:Show"); - } - - @Override - public void handleMessage(String event, final JSONObject message) { - if (event.equals("FilePicker:Show")) { - String mimeType = "*/*"; - final String mode = message.optString("mode"); - final int tabId = message.optInt("tabId", -1); - final String title = message.optString("title"); - - if ("mimeType".equals(mode)) - mimeType = message.optString("mimeType"); - else if ("extension".equals(mode)) - mimeType = GeckoAppShell.getMimeTypeFromExtensions(message.optString("extensions")); - - showFilePickerAsync(title, mimeType, new ResultHandler() { - @Override - public void gotFile(String filename) { - try { - message.put("file", filename); - } catch (JSONException ex) { - Log.i(LOGTAG, "Can't add filename to message " + filename); - } - - - GeckoAppShell.notifyObservers("FilePicker:Result", message.toString()); - } - }, tabId); - } - } - - private void addActivities(Intent intent, HashMap intents, HashMap filters) { - PackageManager pm = context.getPackageManager(); - List lri = pm.queryIntentActivities(intent, 0); - for (ResolveInfo ri : lri) { - ComponentName cn = new ComponentName(ri.activityInfo.applicationInfo.packageName, ri.activityInfo.name); - if (filters != null && !filters.containsKey(cn.toString())) { - Intent rintent = new Intent(intent); - rintent.setComponent(cn); - intents.put(cn.toString(), rintent); - } - } - } - - private Intent getIntent(String mimeType) { - Intent intent = new Intent(Intent.ACTION_GET_CONTENT); - intent.setType(mimeType); - intent.addCategory(Intent.CATEGORY_OPENABLE); - return intent; - } - - private List getIntentsForFilePicker(final String mimeType, - final FilePickerResultHandler fileHandler) { - // The base intent to use for the file picker. Even if this is an implicit intent, Android will - // still show a list of Activities that match this action/type. - Intent baseIntent; - // A HashMap of Activities the base intent will show in the chooser. This is used - // to filter activities from other intents so that we don't show duplicates. - HashMap baseIntents = new HashMap(); - // A list of other activities to shwo in the picker (and the intents to launch them). - HashMap intents = new HashMap (); - - if ("audio/*".equals(mimeType)) { - // For audio the only intent is the mimetype - baseIntent = getIntent(mimeType); - addActivities(baseIntent, baseIntents, null); - } else if ("image/*".equals(mimeType)) { - // For images the base is a capture intent - baseIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); - baseIntent.putExtra(MediaStore.EXTRA_OUTPUT, - Uri.fromFile(new File(Environment.getExternalStorageDirectory(), - fileHandler.generateImageName()))); - addActivities(baseIntent, baseIntents, null); - - // We also add the mimetype intent - addActivities(getIntent(mimeType), intents, baseIntents); - } else if ("video/*".equals(mimeType)) { - // For videos the base is a capture intent - baseIntent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE); - addActivities(baseIntent, baseIntents, null); - - // We also add the mimetype intent - addActivities(getIntent(mimeType), intents, baseIntents); - } else { - baseIntent = getIntent("*/*"); - addActivities(baseIntent, baseIntents, null); - - Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); - intent.putExtra(MediaStore.EXTRA_OUTPUT, - Uri.fromFile(new File(Environment.getExternalStorageDirectory(), - fileHandler.generateImageName()))); - addActivities(intent, intents, baseIntents); - intent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE); - addActivities(intent, intents, baseIntents); - } - - // If we didn't find any activities, we fall back to the */* mimetype intent - if (baseIntents.size() == 0 && intents.size() == 0) { - intents.clear(); - - baseIntent = getIntent("*/*"); - addActivities(baseIntent, baseIntents, null); - } - - ArrayList vals = new ArrayList(intents.values()); - vals.add(0, baseIntent); - return vals; - } - - private String getFilePickerTitle(String mimeType) { - if (mimeType.equals("audio/*")) { - return context.getString(R.string.filepicker_audio_title); - } else if (mimeType.equals("image/*")) { - return context.getString(R.string.filepicker_image_title); - } else if (mimeType.equals("video/*")) { - return context.getString(R.string.filepicker_video_title); - } else { - return context.getString(R.string.filepicker_title); - } - } - - private interface IntentHandler { - public void gotIntent(Intent intent); - } - - /* Gets an intent that can open a particular mimetype. Will show a prompt with a list - * of Activities that can handle the mietype. Asynchronously calls the handler when - * one of the intents is selected. If the caller passes in null for the handler, will still - * prompt for the activity, but will throw away the result. - */ - private void getFilePickerIntentAsync(String title, - final String mimeType, - final FilePickerResultHandler fileHandler, - final IntentHandler handler) { - List intents = getIntentsForFilePicker(mimeType, fileHandler); - - if (intents.size() == 0) { - Log.i(LOGTAG, "no activities for the file picker!"); - handler.gotIntent(null); - return; - } - - Intent base = intents.remove(0); - - if (intents.size() == 0) { - handler.gotIntent(base); - return; - } - - if (TextUtils.isEmpty(title)) { - title = getFilePickerTitle(mimeType); - } - Intent chooser = Intent.createChooser(base, title); - chooser.putExtra(Intent.EXTRA_INITIAL_INTENTS, intents.toArray(new Parcelable[intents.size()])); - handler.gotIntent(chooser); - } - - /* Allows the user to pick an activity to load files from using a list prompt. Then opens the activity and - * sends the file returned to the passed in handler. If a null handler is passed in, will still - * pick and launch the file picker, but will throw away the result. - */ - protected void showFilePickerAsync(final String title, final String mimeType, final ResultHandler handler, final int tabId) { - final FilePickerResultHandler fileHandler = new FilePickerResultHandler(handler, context, tabId); - getFilePickerIntentAsync(title, mimeType, fileHandler, new IntentHandler() { - @Override - public void gotIntent(Intent intent) { - if (handler == null) { - return; - } - - if (intent == null) { - handler.gotFile(""); - return; - } - - ActivityHandlerHelper.startIntent(intent, fileHandler); - } - }); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/FilePickerResultHandler.java b/mobile/android/base/java/org/mozilla/gecko/FilePickerResultHandler.java deleted file mode 100644 index 7629ea546e..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/FilePickerResultHandler.java +++ /dev/null @@ -1,282 +0,0 @@ -/* 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; - -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; - -import org.mozilla.gecko.util.ActivityResultHandler; -import org.mozilla.gecko.util.ThreadUtils; - -import android.app.Activity; -import android.content.ContentResolver; -import android.content.Context; -import android.content.Intent; -import android.database.Cursor; -import android.net.Uri; -import android.os.Bundle; -import android.os.Environment; -import android.os.Process; -import android.provider.MediaStore; -import android.provider.OpenableColumns; -import android.support.v4.app.FragmentActivity; -import android.support.v4.app.LoaderManager; -import android.support.v4.app.LoaderManager.LoaderCallbacks; -import android.support.v4.content.CursorLoader; -import android.support.v4.content.Loader; -import android.text.TextUtils; -import android.text.format.Time; -import android.util.Log; - -class FilePickerResultHandler implements ActivityResultHandler { - private static final String LOGTAG = "GeckoFilePickerResultHandler"; - private static final String UPLOADS_DIR = "uploads"; - - private final FilePicker.ResultHandler handler; - private final int tabId; - private final File cacheDir; - - // this code is really hacky and doesn't belong anywhere so I'm putting it here for now - // until I can come up with a better solution. - private String mImageName = ""; - - /* Use this constructor to asynchronously listen for results */ - public FilePickerResultHandler(final FilePicker.ResultHandler handler, final Context context, final int tabId) { - this.tabId = tabId; - this.cacheDir = new File(context.getCacheDir(), UPLOADS_DIR); - this.handler = handler; - } - - void sendResult(String res) { - if (handler != null) { - handler.gotFile(res); - } - } - - @Override - public void onActivityResult(int resultCode, Intent intent) { - if (resultCode != Activity.RESULT_OK) { - sendResult(""); - return; - } - - // Camera results won't return an Intent. Use the file name we passed to the original intent. - // In Android M, camera results return an empty Intent rather than null. - if (intent == null || (intent.getAction() == null && intent.getData() == null)) { - if (mImageName != null) { - File file = new File(Environment.getExternalStorageDirectory(), mImageName); - sendResult(file.getAbsolutePath()); - } else { - sendResult(""); - } - return; - } - - Uri uri = intent.getData(); - if (uri == null) { - sendResult(""); - return; - } - - // Some file pickers may return a file uri - if ("file".equals(uri.getScheme())) { - String path = uri.getPath(); - sendResult(path == null ? "" : path); - return; - } - - final FragmentActivity fa = (FragmentActivity) GeckoAppShell.getGeckoInterface().getActivity(); - final LoaderManager lm = fa.getSupportLoaderManager(); - - // Finally, Video pickers and some file pickers may return a content provider. - final ContentResolver cr = fa.getContentResolver(); - final Cursor cursor = cr.query(uri, new String[] { MediaStore.Video.Media.DATA }, null, null, null); - if (cursor != null) { - try { - // Try a query to make sure the expected columns exist - int index = cursor.getColumnIndex(MediaStore.Video.Media.DATA); - if (index >= 0) { - lm.initLoader(intent.hashCode(), null, new VideoLoaderCallbacks(uri)); - return; - } - } catch (Exception ex) { - // We'll try a different loader below - } finally { - cursor.close(); - } - } - - lm.initLoader(uri.hashCode(), null, new FileLoaderCallbacks(uri, cacheDir, tabId)); - } - - public String generateImageName() { - Time now = new Time(); - now.setToNow(); - mImageName = now.format("%Y-%m-%d %H.%M.%S") + ".jpg"; - return mImageName; - } - - private class VideoLoaderCallbacks implements LoaderCallbacks { - final private Uri uri; - public VideoLoaderCallbacks(Uri uri) { - this.uri = uri; - } - - @Override - public Loader onCreateLoader(int id, Bundle args) { - final FragmentActivity fa = (FragmentActivity) GeckoAppShell.getGeckoInterface().getActivity(); - return new CursorLoader(fa, - uri, - new String[] { MediaStore.Video.Media.DATA }, - null, // selection - null, // selectionArgs - null); // sortOrder - } - - @Override - public void onLoadFinished(Loader loader, Cursor cursor) { - if (cursor.moveToFirst()) { - String res = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DATA)); - - // Some pickers (the KitKat Documents one for instance) won't return a temporary file here. - // Fall back to the normal FileLoader if we didn't find anything. - if (TextUtils.isEmpty(res)) { - tryFileLoaderCallback(); - return; - } - - sendResult(res); - } else { - tryFileLoaderCallback(); - } - } - - private void tryFileLoaderCallback() { - final FragmentActivity fa = (FragmentActivity) GeckoAppShell.getGeckoInterface().getActivity(); - final LoaderManager lm = fa.getSupportLoaderManager(); - lm.initLoader(uri.hashCode(), null, new FileLoaderCallbacks(uri, cacheDir, tabId)); - } - - @Override - public void onLoaderReset(Loader loader) { } - } - - /** - * This class's only dependency on FilePickerResultHandler is sendResult. - */ - private class FileLoaderCallbacks implements LoaderCallbacks, - Tabs.OnTabsChangedListener { - private final Uri uri; - private final File cacheDir; - private final int tabId; - String tempFile; - - public FileLoaderCallbacks(Uri uri, File cacheDir, int tabId) { - this.uri = uri; - this.cacheDir = cacheDir; - this.tabId = tabId; - } - - @Override - public Loader onCreateLoader(int id, Bundle args) { - final FragmentActivity fa = (FragmentActivity) GeckoAppShell.getGeckoInterface().getActivity(); - return new CursorLoader(fa, - uri, - new String[] { OpenableColumns.DISPLAY_NAME }, - null, // selection - null, // selectionArgs - null); // sortOrder - } - - @Override - public void onLoadFinished(Loader loader, Cursor cursor) { - if (cursor.moveToFirst()) { - String name = cursor.getString(0); - // tmp filenames must be at least 3 characters long. Add a prefix to make sure that happens - String fileName = "tmp_" + Process.myPid() + "-"; - String fileExt; - int period; - - final FragmentActivity fa = (FragmentActivity) GeckoAppShell.getGeckoInterface().getActivity(); - final ContentResolver cr = fa.getContentResolver(); - - // Generate an extension if we don't already have one - if (name == null || (period = name.lastIndexOf('.')) == -1) { - String mimeType = cr.getType(uri); - fileExt = "." + GeckoAppShell.getExtensionFromMimeType(mimeType); - } else { - fileExt = name.substring(period); - fileName += name.substring(0, period); - } - - // Now write the data to the temp file - FileOutputStream fos = null; - try { - cacheDir.mkdir(); - - File file = File.createTempFile(fileName, fileExt, cacheDir); - fos = new FileOutputStream(file); - InputStream is = cr.openInputStream(uri); - byte[] buf = new byte[4096]; - int len = is.read(buf); - while (len != -1) { - fos.write(buf, 0, len); - len = is.read(buf); - } - fos.close(); - is.close(); - tempFile = file.getAbsolutePath(); - sendResult((tempFile == null) ? "" : tempFile); - - if (tabId > -1 && !TextUtils.isEmpty(tempFile)) { - Tabs.registerOnTabsChangedListener(this); - } - } catch (IOException ex) { - Log.i(LOGTAG, "Error writing file", ex); - } finally { - if (fos != null) { - try { - fos.close(); - } catch (IOException e) { /* not much to do here */ } - } - } - } else { - sendResult(""); - } - } - - @Override - public void onLoaderReset(Loader loader) { } - - /*Tabs.OnTabsChangedListener*/ - // This cleans up our temp file. If it doesn't run, we just hope that Android - // will eventually does the cleanup for us. - @Override - public void onTabChanged(Tab tab, Tabs.TabEvents msg, String data) { - if ((tab == null) || (tab.getId() != tabId)) { - return; - } - - if (msg == Tabs.TabEvents.LOCATION_CHANGE || - msg == Tabs.TabEvents.CLOSED) { - ThreadUtils.postToBackgroundThread(new Runnable() { - @Override - public void run() { - File f = new File(tempFile); - f.delete(); - } - }); - - // Tabs' listener array is safe to modify during use: its - // iteration pattern is based on snapshots. - Tabs.unregisterOnTabsChangedListener(this); - } - } - } - -} - diff --git a/mobile/android/base/java/org/mozilla/gecko/FindInPageBar.java b/mobile/android/base/java/org/mozilla/gecko/FindInPageBar.java deleted file mode 100644 index efa04a04e1..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/FindInPageBar.java +++ /dev/null @@ -1,256 +0,0 @@ -/* 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; - -import org.mozilla.gecko.util.GeckoEventListener; -import org.mozilla.gecko.util.GeckoRequest; -import org.mozilla.gecko.util.NativeJSObject; -import org.mozilla.gecko.util.ThreadUtils; - -import org.json.JSONObject; - -import android.content.Context; -import android.text.Editable; -import android.text.TextUtils; -import android.text.TextWatcher; -import android.util.AttributeSet; -import android.util.Log; -import android.view.KeyEvent; -import android.view.LayoutInflater; -import android.view.View; -import android.view.inputmethod.InputMethodManager; -import android.widget.LinearLayout; -import android.widget.TextView; - -public class FindInPageBar extends LinearLayout implements TextWatcher, View.OnClickListener, GeckoEventListener { - private static final String LOGTAG = "GeckoFindInPageBar"; - private static final String REQUEST_ID = "FindInPageBar"; - - private final Context mContext; - private CustomEditText mFindText; - private TextView mStatusText; - private boolean mInflated; - - public FindInPageBar(Context context, AttributeSet attrs) { - super(context, attrs); - mContext = context; - setFocusable(true); - } - - public void inflateContent() { - LayoutInflater inflater = LayoutInflater.from(mContext); - View content = inflater.inflate(R.layout.find_in_page_content, this); - - content.findViewById(R.id.find_prev).setOnClickListener(this); - content.findViewById(R.id.find_next).setOnClickListener(this); - content.findViewById(R.id.find_close).setOnClickListener(this); - - // Capture clicks on the rest of the view to prevent them from - // leaking into other views positioned below. - content.setOnClickListener(this); - - mFindText = (CustomEditText) content.findViewById(R.id.find_text); - mFindText.addTextChangedListener(this); - mFindText.setOnKeyPreImeListener(new CustomEditText.OnKeyPreImeListener() { - @Override - public boolean onKeyPreIme(View v, int keyCode, KeyEvent event) { - if (keyCode == KeyEvent.KEYCODE_BACK) { - hide(); - return true; - } - return false; - } - }); - - mStatusText = (TextView) content.findViewById(R.id.find_status); - - mInflated = true; - GeckoApp.getEventDispatcher().registerGeckoThreadListener(this, - "FindInPage:MatchesCountResult", - "TextSelection:Data"); - } - - public void show() { - if (!mInflated) - inflateContent(); - - setVisibility(VISIBLE); - mFindText.requestFocus(); - - // handleMessage() receives response message and determines initial state of softInput - GeckoAppShell.notifyObservers("TextSelection:Get", REQUEST_ID); - GeckoAppShell.notifyObservers("FindInPage:Opened", null); - } - - public void hide() { - if (!mInflated || getVisibility() == View.GONE) { - // There's nothing to hide yet. - return; - } - - // Always clear the Find string, primarily for privacy. - mFindText.setText(""); - - // Only close the IMM if its EditText is the one with focus. - if (mFindText.isFocused()) { - getInputMethodManager(mFindText).hideSoftInputFromWindow(mFindText.getWindowToken(), 0); - } - - // Close the FIPB / FindHelper state. - setVisibility(GONE); - GeckoAppShell.notifyObservers("FindInPage:Closed", null); - } - - private InputMethodManager getInputMethodManager(View view) { - Context context = view.getContext(); - return (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE); - } - - public void onDestroy() { - if (!mInflated) { - return; - } - GeckoApp.getEventDispatcher().unregisterGeckoThreadListener(this, - "FindInPage:MatchesCountResult", - "TextSelection:Data"); - } - - private void onMatchesCountResult(final int total, final int current, final int limit, final String searchString) { - if (total == -1) { - updateResult(Integer.toString(limit) + "+"); - } else if (total > 0) { - updateResult(Integer.toString(current) + "/" + Integer.toString(total)); - } else if (TextUtils.isEmpty(searchString)) { - updateResult(""); - } else { - // We display 0/0, when there were no - // matches found, or if matching has been turned off by setting - // pref accessibility.typeaheadfind.matchesCountLimit to 0. - updateResult("0/0"); - } - } - - private void updateResult(final String statusText) { - ThreadUtils.postToUiThread(new Runnable() { - @Override - public void run() { - mStatusText.setVisibility(statusText.isEmpty() ? View.GONE : View.VISIBLE); - mStatusText.setText(statusText); - } - }); - } - - // TextWatcher implementation - - @Override - public void afterTextChanged(Editable s) { - sendRequestToFinderHelper("FindInPage:Find", s.toString()); - } - - @Override - public void beforeTextChanged(CharSequence s, int start, int count, int after) { - // ignore - } - - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) { - // ignore - } - - // View.OnClickListener implementation - - @Override - public void onClick(View v) { - final int viewId = v.getId(); - - String extras = getResources().getResourceEntryName(viewId); - Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.BUTTON, extras); - - if (viewId == R.id.find_prev) { - sendRequestToFinderHelper("FindInPage:Prev", mFindText.getText().toString()); - getInputMethodManager(mFindText).hideSoftInputFromWindow(mFindText.getWindowToken(), 0); - return; - } - - if (viewId == R.id.find_next) { - sendRequestToFinderHelper("FindInPage:Next", mFindText.getText().toString()); - getInputMethodManager(mFindText).hideSoftInputFromWindow(mFindText.getWindowToken(), 0); - return; - } - - if (viewId == R.id.find_close) { - hide(); - } - } - - // GeckoEventListener implementation - - @Override - public void handleMessage(String event, JSONObject message) { - if (event.equals("FindInPage:MatchesCountResult")) { - onMatchesCountResult(message.optInt("total", 0), - message.optInt("current", 0), - message.optInt("limit", 0), - message.optString("searchString")); - return; - } - - if (!event.equals("TextSelection:Data") || !REQUEST_ID.equals(message.optString("requestId"))) { - return; - } - - final String text = message.optString("text"); - - // Populate an initial find string, virtual keyboard not required. - if (!TextUtils.isEmpty(text)) { - // Populate initial selection - ThreadUtils.postToUiThread(new Runnable() { - @Override - public void run() { - mFindText.setText(text); - } - }); - return; - } - - // Show the virtual keyboard. - if (mFindText.hasWindowFocus()) { - getInputMethodManager(mFindText).showSoftInput(mFindText, 0); - } else { - // showSoftInput won't work until after the window is focused. - mFindText.setOnWindowFocusChangeListener(new CustomEditText.OnWindowFocusChangeListener() { - @Override - public void onWindowFocusChanged(boolean hasFocus) { - if (!hasFocus) - return; - - mFindText.setOnWindowFocusChangeListener(null); - getInputMethodManager(mFindText).showSoftInput(mFindText, 0); - } - }); - } - } - - /** - * Request find operation, and update matchCount results (current count and total). - */ - private void sendRequestToFinderHelper(final String request, final String searchString) { - GeckoAppShell.sendRequestToGecko(new GeckoRequest(request, searchString) { - @Override - public void onResponse(NativeJSObject nativeJSObject) { - // We don't care about the return value, because `onMatchesCountResult` - // does the heavy lifting. - } - - @Override - public void onError(NativeJSObject error) { - // Gecko didn't respond due to state change, javascript error, etc. - Log.d(LOGTAG, "No response from Gecko on request to match string: [" + - searchString + "]"); - updateResult(""); - } - }); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/FormAssistPopup.java b/mobile/android/base/java/org/mozilla/gecko/FormAssistPopup.java deleted file mode 100644 index 5c7f932c00..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/FormAssistPopup.java +++ /dev/null @@ -1,459 +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; - -import org.mozilla.gecko.animation.ViewHelper; -import org.mozilla.gecko.gfx.FloatSize; -import org.mozilla.gecko.gfx.ImmutableViewportMetrics; -import org.mozilla.gecko.util.GeckoEventListener; -import org.mozilla.gecko.util.ThreadUtils; -import org.mozilla.gecko.widget.SwipeDismissListViewTouchListener; -import org.mozilla.gecko.widget.SwipeDismissListViewTouchListener.OnDismissCallback; - -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - -import android.content.Context; -import android.content.res.Resources; -import android.graphics.PointF; -import android.util.AttributeSet; -import android.util.Log; -import android.util.Pair; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.view.animation.Animation; -import android.view.animation.AnimationUtils; -import android.view.inputmethod.InputMethodManager; -import android.widget.AdapterView; -import android.widget.AdapterView.OnItemClickListener; -import android.widget.ArrayAdapter; -import android.widget.ImageView; -import android.widget.ListView; -import android.widget.RelativeLayout; -import android.widget.RelativeLayout.LayoutParams; -import android.widget.TextView; - -import java.util.Arrays; -import java.util.Collection; - -public class FormAssistPopup extends RelativeLayout implements GeckoEventListener { - private final Context mContext; - private final Animation mAnimation; - - private ListView mAutoCompleteList; - private RelativeLayout mValidationMessage; - private TextView mValidationMessageText; - private ImageView mValidationMessageArrow; - private ImageView mValidationMessageArrowInverted; - - private double mX; - private double mY; - private double mW; - private double mH; - - private enum PopupType { - AUTOCOMPLETE, - VALIDATIONMESSAGE; - } - private PopupType mPopupType; - - private static final int MAX_VISIBLE_ROWS = 5; - - private static int sAutoCompleteMinWidth; - private static int sAutoCompleteRowHeight; - private static int sValidationMessageHeight; - private static int sValidationTextMarginTop; - private static LayoutParams sValidationTextLayoutNormal; - private static LayoutParams sValidationTextLayoutInverted; - - private static final String LOGTAG = "GeckoFormAssistPopup"; - - // The blocklist is so short that ArrayList is probably cheaper than HashSet. - private static final Collection sInputMethodBlocklist = Arrays.asList( - InputMethods.METHOD_GOOGLE_JAPANESE_INPUT, // bug 775850 - InputMethods.METHOD_OPENWNN_PLUS, // bug 768108 - InputMethods.METHOD_SIMEJI, // bug 768108 - InputMethods.METHOD_SWYPE, // bug 755909 - InputMethods.METHOD_SWYPE_BETA // bug 755909 - ); - - public FormAssistPopup(Context context, AttributeSet attrs) { - super(context, attrs); - mContext = context; - - mAnimation = AnimationUtils.loadAnimation(context, R.anim.grow_fade_in); - mAnimation.setDuration(75); - - setFocusable(false); - - GeckoApp.getEventDispatcher().registerGeckoThreadListener(this, - "FormAssist:AutoComplete", - "FormAssist:ValidationMessage", - "FormAssist:Hide"); - } - - void destroy() { - GeckoApp.getEventDispatcher().unregisterGeckoThreadListener(this, - "FormAssist:AutoComplete", - "FormAssist:ValidationMessage", - "FormAssist:Hide"); - } - - @Override - public void handleMessage(String event, JSONObject message) { - try { - if (event.equals("FormAssist:AutoComplete")) { - handleAutoCompleteMessage(message); - } else if (event.equals("FormAssist:ValidationMessage")) { - handleValidationMessage(message); - } else if (event.equals("FormAssist:Hide")) { - handleHideMessage(message); - } - } catch (Exception e) { - Log.e(LOGTAG, "Exception handling message \"" + event + "\":", e); - } - } - - private void handleAutoCompleteMessage(JSONObject message) throws JSONException { - final JSONArray suggestions = message.getJSONArray("suggestions"); - final JSONObject rect = message.getJSONObject("rect"); - final boolean isEmpty = message.getBoolean("isEmpty"); - ThreadUtils.postToUiThread(new Runnable() { - @Override - public void run() { - showAutoCompleteSuggestions(suggestions, rect, isEmpty); - } - }); - } - - private void handleValidationMessage(JSONObject message) throws JSONException { - final String validationMessage = message.getString("validationMessage"); - final JSONObject rect = message.getJSONObject("rect"); - ThreadUtils.postToUiThread(new Runnable() { - @Override - public void run() { - showValidationMessage(validationMessage, rect); - } - }); - } - - private void handleHideMessage(JSONObject message) { - ThreadUtils.postToUiThread(new Runnable() { - @Override - public void run() { - hide(); - } - }); - } - - private void showAutoCompleteSuggestions(JSONArray suggestions, JSONObject rect, boolean isEmpty) { - final String inputMethod = InputMethods.getCurrentInputMethod(mContext); - if (!isEmpty && sInputMethodBlocklist.contains(inputMethod)) { - // Don't display the form auto-complete popup after the user starts typing - // to avoid confusing somes IME. See bug 758820 and bug 632744. - hide(); - return; - } - - if (mAutoCompleteList == null) { - LayoutInflater inflater = LayoutInflater.from(mContext); - mAutoCompleteList = (ListView) inflater.inflate(R.layout.autocomplete_list, null); - - mAutoCompleteList.setOnItemClickListener(new OnItemClickListener() { - @Override - public void onItemClick(AdapterView parentView, View view, int position, long id) { - // Use the value stored with the autocomplete view, not the label text, - // since they can be different. - TextView textView = (TextView) view; - String value = (String) textView.getTag(); - broadcastGeckoEvent("FormAssist:AutoComplete", value); - hide(); - } - }); - - // Create a ListView-specific touch listener. ListViews are given special treatment because - // by default they handle touches for their list items... i.e. they're in charge of drawing - // the pressed state (the list selector), handling list item clicks, etc. - final SwipeDismissListViewTouchListener touchListener = new SwipeDismissListViewTouchListener(mAutoCompleteList, new OnDismissCallback() { - @Override - public void onDismiss(ListView listView, final int position) { - // Use the value stored with the autocomplete view, not the label text, - // since they can be different. - AutoCompleteListAdapter adapter = (AutoCompleteListAdapter) listView.getAdapter(); - Pair item = adapter.getItem(position); - - // Remove the item from form history. - broadcastGeckoEvent("FormAssist:Remove", item.second); - - // Update the list - adapter.remove(item); - adapter.notifyDataSetChanged(); - positionAndShowPopup(); - } - }); - mAutoCompleteList.setOnTouchListener(touchListener); - - // Setting this scroll listener is required to ensure that during ListView scrolling, - // we don't look for swipes. - mAutoCompleteList.setOnScrollListener(touchListener.makeScrollListener()); - - // Setting this recycler listener is required to make sure animated views are reset. - mAutoCompleteList.setRecyclerListener(touchListener.makeRecyclerListener()); - - addView(mAutoCompleteList); - } - - AutoCompleteListAdapter adapter = new AutoCompleteListAdapter(mContext, R.layout.autocomplete_list_item); - adapter.populateSuggestionsList(suggestions); - mAutoCompleteList.setAdapter(adapter); - - if (setGeckoPositionData(rect, true)) { - positionAndShowPopup(); - } - } - - private void showValidationMessage(String validationMessage, JSONObject rect) { - if (mValidationMessage == null) { - LayoutInflater inflater = LayoutInflater.from(mContext); - mValidationMessage = (RelativeLayout) inflater.inflate(R.layout.validation_message, null); - - addView(mValidationMessage); - mValidationMessageText = (TextView) mValidationMessage.findViewById(R.id.validation_message_text); - - sValidationTextMarginTop = (int) (mContext.getResources().getDimension(R.dimen.validation_message_margin_top)); - - sValidationTextLayoutNormal = new LayoutParams(mValidationMessageText.getLayoutParams()); - sValidationTextLayoutNormal.setMargins(0, sValidationTextMarginTop, 0, 0); - - sValidationTextLayoutInverted = new LayoutParams((ViewGroup.MarginLayoutParams) sValidationTextLayoutNormal); - sValidationTextLayoutInverted.setMargins(0, 0, 0, 0); - - mValidationMessageArrow = (ImageView) mValidationMessage.findViewById(R.id.validation_message_arrow); - mValidationMessageArrowInverted = (ImageView) mValidationMessage.findViewById(R.id.validation_message_arrow_inverted); - } - - mValidationMessageText.setText(validationMessage); - - // We need to set the text as selected for the marquee text to work. - mValidationMessageText.setSelected(true); - - if (setGeckoPositionData(rect, false)) { - positionAndShowPopup(); - } - } - - private boolean setGeckoPositionData(JSONObject rect, boolean isAutoComplete) { - try { - mX = rect.getDouble("x"); - mY = rect.getDouble("y"); - mW = rect.getDouble("w"); - mH = rect.getDouble("h"); - } catch (JSONException e) { - // Bail if we can't get the correct dimensions for the popup. - Log.e(LOGTAG, "Error getting FormAssistPopup dimensions", e); - return false; - } - - mPopupType = (isAutoComplete ? - PopupType.AUTOCOMPLETE : PopupType.VALIDATIONMESSAGE); - return true; - } - - private void positionAndShowPopup() { - positionAndShowPopup(GeckoAppShell.getLayerView().getViewportMetrics()); - } - - private void positionAndShowPopup(ImmutableViewportMetrics aMetrics) { - ThreadUtils.assertOnUiThread(); - - // Don't show the form assist popup when using fullscreen VKB - InputMethodManager imm = - (InputMethodManager) mContext.getSystemService(Context.INPUT_METHOD_SERVICE); - if (imm.isFullscreenMode()) { - return; - } - - // Hide/show the appropriate popup contents - if (mAutoCompleteList != null) { - mAutoCompleteList.setVisibility((mPopupType == PopupType.AUTOCOMPLETE) ? VISIBLE : GONE); - } - if (mValidationMessage != null) { - mValidationMessage.setVisibility((mPopupType == PopupType.AUTOCOMPLETE) ? GONE : VISIBLE); - } - - if (sAutoCompleteMinWidth == 0) { - Resources res = mContext.getResources(); - sAutoCompleteMinWidth = (int) (res.getDimension(R.dimen.autocomplete_min_width)); - sAutoCompleteRowHeight = (int) (res.getDimension(R.dimen.autocomplete_row_height)); - sValidationMessageHeight = (int) (res.getDimension(R.dimen.validation_message_height)); - } - - float zoom = aMetrics.zoomFactor; - - // These values correspond to the input box for which we want to - // display the FormAssistPopup. - int left = (int) (mX * zoom - aMetrics.viewportRectLeft); - int top = (int) (mY * zoom - aMetrics.viewportRectTop + GeckoAppShell.getLayerView().getSurfaceTranslation()); - int width = (int) (mW * zoom); - int height = (int) (mH * zoom); - - int popupWidth = LayoutParams.MATCH_PARENT; - int popupLeft = left < 0 ? 0 : left; - - FloatSize viewport = aMetrics.getSize(); - - // For autocomplete suggestions, if the input is smaller than the screen-width, - // shrink the popup's width. Otherwise, keep it as MATCH_PARENT. - if ((mPopupType == PopupType.AUTOCOMPLETE) && (left + width) < viewport.width) { - popupWidth = left < 0 ? left + width : width; - - // Ensure the popup has a minimum width. - if (popupWidth < sAutoCompleteMinWidth) { - popupWidth = sAutoCompleteMinWidth; - - // Move the popup to the left if there isn't enough room for it. - if ((popupLeft + popupWidth) > viewport.width) { - popupLeft = (int) (viewport.width - popupWidth); - } - } - } - - int popupHeight; - if (mPopupType == PopupType.AUTOCOMPLETE) { - // Limit the amount of visible rows. - int rows = mAutoCompleteList.getAdapter().getCount(); - if (rows > MAX_VISIBLE_ROWS) { - rows = MAX_VISIBLE_ROWS; - } - - popupHeight = sAutoCompleteRowHeight * rows; - } else { - popupHeight = sValidationMessageHeight; - } - - int popupTop = top + height; - - if (mPopupType == PopupType.VALIDATIONMESSAGE) { - mValidationMessageText.setLayoutParams(sValidationTextLayoutNormal); - mValidationMessageArrow.setVisibility(VISIBLE); - mValidationMessageArrowInverted.setVisibility(GONE); - } - - // If the popup doesn't fit below the input box, shrink its height, or - // see if we can place it above the input instead. - if ((popupTop + popupHeight) > viewport.height) { - // Find where the maximum space is, and put the popup there. - if ((viewport.height - popupTop) > top) { - // Shrink the height to fit it below the input box. - popupHeight = (int) (viewport.height - popupTop); - } else { - if (popupHeight < top) { - // No shrinking needed to fit on top. - popupTop = (top - popupHeight); - } else { - // Shrink to available space on top. - popupTop = 0; - popupHeight = top; - } - - if (mPopupType == PopupType.VALIDATIONMESSAGE) { - mValidationMessageText.setLayoutParams(sValidationTextLayoutInverted); - mValidationMessageArrow.setVisibility(GONE); - mValidationMessageArrowInverted.setVisibility(VISIBLE); - } - } - } - - LayoutParams layoutParams = new LayoutParams(popupWidth, popupHeight); - layoutParams.setMargins(popupLeft, popupTop, 0, 0); - setLayoutParams(layoutParams); - requestLayout(); - - if (!isShown()) { - setVisibility(VISIBLE); - startAnimation(mAnimation); - } - } - - public void hide() { - if (isShown()) { - setVisibility(GONE); - broadcastGeckoEvent("FormAssist:Hidden", null); - } - } - - void onTranslationChanged() { - ThreadUtils.assertOnUiThread(); - if (!isShown()) { - return; - } - positionAndShowPopup(); - } - - void onMetricsChanged(final ImmutableViewportMetrics aMetrics) { - if (!isShown()) { - return; - } - - ThreadUtils.postToUiThread(new Runnable() { - @Override - public void run() { - positionAndShowPopup(aMetrics); - } - }); - } - - private static void broadcastGeckoEvent(String eventName, String eventData) { - GeckoAppShell.notifyObservers(eventName, eventData); - } - - private class AutoCompleteListAdapter extends ArrayAdapter> { - private final LayoutInflater mInflater; - private final int mTextViewResourceId; - - public AutoCompleteListAdapter(Context context, int textViewResourceId) { - super(context, textViewResourceId); - - mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); - mTextViewResourceId = textViewResourceId; - } - - // This method takes an array of autocomplete suggestions with label/value properties - // and adds label/value Pair objects to the array that backs the adapter. - public void populateSuggestionsList(JSONArray suggestions) { - try { - for (int i = 0; i < suggestions.length(); i++) { - JSONObject suggestion = suggestions.getJSONObject(i); - String label = suggestion.getString("label"); - String value = suggestion.getString("value"); - add(new Pair(label, value)); - } - } catch (JSONException e) { - Log.e(LOGTAG, "JSONException", e); - } - } - - @Override - public View getView(int position, View convertView, ViewGroup parent) { - if (convertView == null) { - convertView = mInflater.inflate(mTextViewResourceId, null); - } - - Pair item = getItem(position); - TextView itemView = (TextView) convertView; - - // Set the text with the suggestion label - itemView.setText(item.first); - - // Set a tag with the suggestion value - itemView.setTag(item.second); - - return convertView; - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/GeckoActivity.java b/mobile/android/base/java/org/mozilla/gecko/GeckoActivity.java deleted file mode 100644 index 774ca60249..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/GeckoActivity.java +++ /dev/null @@ -1,100 +0,0 @@ -/* 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; - -import android.content.ComponentName; -import android.content.Intent; -import android.support.v7.app.AppCompatActivity; - -public abstract class GeckoActivity extends AppCompatActivity implements GeckoActivityStatus { - // has this activity recently started another Gecko activity? - private boolean mGeckoActivityOpened; - - /** - * Display any resources that show strings or encompass locale-specific - * representations. - * - * onLocaleReady must always be called on the UI thread. - */ - public void onLocaleReady(final String locale) { - } - - @Override - public void onPause() { - super.onPause(); - - if (getApplication() instanceof GeckoApplication) { - ((GeckoApplication) getApplication()).onActivityPause(this); - } - } - - @Override - public void onResume() { - super.onResume(); - - if (getApplication() instanceof GeckoApplication) { - ((GeckoApplication) getApplication()).onActivityResume(this); - mGeckoActivityOpened = false; - } - } - - @Override - public void onCreate(android.os.Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - if (AppConstants.MOZ_ANDROID_ANR_REPORTER) { - ANRReporter.register(getApplicationContext()); - } - } - - @Override - public void onDestroy() { - if (AppConstants.MOZ_ANDROID_ANR_REPORTER) { - ANRReporter.unregister(); - } - super.onDestroy(); - } - - @Override - public void startActivity(Intent intent) { - mGeckoActivityOpened = checkIfGeckoActivity(intent); - super.startActivity(intent); - } - - @Override - public void startActivityForResult(Intent intent, int request) { - mGeckoActivityOpened = checkIfGeckoActivity(intent); - super.startActivityForResult(intent, request); - } - - private static boolean checkIfGeckoActivity(Intent intent) { - // Whenever we call our own activity, the component and its package name is set. - // If we call an activity from another package, or an open intent (leaving android to resolve) - // component has a different package name or it is null. - ComponentName component = intent.getComponent(); - return (component != null && - AppConstants.ANDROID_PACKAGE_NAME.equals(component.getPackageName())); - } - - @Override - public boolean isGeckoActivityOpened() { - return mGeckoActivityOpened; - } - - public boolean isApplicationInBackground() { - return ((GeckoApplication) getApplication()).isApplicationInBackground(); - } - - @Override - public void onLowMemory() { - MemoryMonitor.getInstance().onLowMemory(); - super.onLowMemory(); - } - - @Override - public void onTrimMemory(int level) { - MemoryMonitor.getInstance().onTrimMemory(level); - super.onTrimMemory(level); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/GeckoActivityStatus.java b/mobile/android/base/java/org/mozilla/gecko/GeckoActivityStatus.java deleted file mode 100644 index ce6b8abd03..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/GeckoActivityStatus.java +++ /dev/null @@ -1,10 +0,0 @@ -/* 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; - -public interface GeckoActivityStatus { - public boolean isGeckoActivityOpened(); - public boolean isFinishing(); // typically from android.app.Activity -}; diff --git a/mobile/android/base/java/org/mozilla/gecko/GeckoApp.java b/mobile/android/base/java/org/mozilla/gecko/GeckoApp.java deleted file mode 100644 index 05fa2bbf81..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/GeckoApp.java +++ /dev/null @@ -1,2878 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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; - -import org.mozilla.gecko.AppConstants.Versions; -import org.mozilla.gecko.GeckoProfileDirectories.NoMozillaDirectoryException; -import org.mozilla.gecko.db.BrowserDB; -import org.mozilla.gecko.db.UrlAnnotations; -import org.mozilla.gecko.gfx.BitmapUtils; -import org.mozilla.gecko.gfx.FullScreenState; -import org.mozilla.gecko.gfx.LayerView; -import org.mozilla.gecko.health.HealthRecorder; -import org.mozilla.gecko.health.SessionInformation; -import org.mozilla.gecko.health.StubbedHealthRecorder; -import org.mozilla.gecko.home.HomeConfig.PanelType; -import org.mozilla.gecko.icons.IconCallback; -import org.mozilla.gecko.icons.IconResponse; -import org.mozilla.gecko.icons.Icons; -import org.mozilla.gecko.menu.GeckoMenu; -import org.mozilla.gecko.menu.GeckoMenuInflater; -import org.mozilla.gecko.menu.MenuPanel; -import org.mozilla.gecko.notifications.NotificationClient; -import org.mozilla.gecko.notifications.NotificationHelper; -import org.mozilla.gecko.util.IntentUtils; -import org.mozilla.gecko.mozglue.SafeIntent; -import org.mozilla.gecko.mozglue.GeckoLoader; -import org.mozilla.gecko.permissions.Permissions; -import org.mozilla.gecko.preferences.ClearOnShutdownPref; -import org.mozilla.gecko.preferences.GeckoPreferences; -import org.mozilla.gecko.prompts.PromptService; -import org.mozilla.gecko.restrictions.Restrictions; -import org.mozilla.gecko.tabqueue.TabQueueHelper; -import org.mozilla.gecko.text.FloatingToolbarTextSelection; -import org.mozilla.gecko.text.TextSelection; -import org.mozilla.gecko.updater.UpdateServiceHelper; -import org.mozilla.gecko.util.ActivityResultHandler; -import org.mozilla.gecko.util.ActivityUtils; -import org.mozilla.gecko.util.EventCallback; -import org.mozilla.gecko.util.FileUtils; -import org.mozilla.gecko.util.GeckoEventListener; -import org.mozilla.gecko.util.GeckoRequest; -import org.mozilla.gecko.util.HardwareUtils; -import org.mozilla.gecko.util.NativeEventListener; -import org.mozilla.gecko.util.NativeJSObject; -import org.mozilla.gecko.util.PrefUtils; -import org.mozilla.gecko.util.ThreadUtils; - -import android.annotation.SuppressLint; -import android.annotation.TargetApi; -import android.app.Activity; -import android.app.AlertDialog; -import android.content.Context; -import android.content.DialogInterface; -import android.content.Intent; -import android.content.SharedPreferences; -import android.content.pm.PackageManager.NameNotFoundException; -import android.content.res.Configuration; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.graphics.Canvas; -import android.graphics.Color; -import android.graphics.Paint; -import android.graphics.Rect; -import android.graphics.RectF; -import android.hardware.Sensor; -import android.net.Uri; -import android.os.Build; -import android.os.Bundle; -import android.os.Environment; -import android.os.Handler; -import android.os.PowerManager; -import android.os.Process; -import android.os.StrictMode; -import android.provider.ContactsContract; -import android.provider.MediaStore.Images.Media; -import android.support.annotation.WorkerThread; -import android.support.design.widget.Snackbar; -import android.text.TextUtils; -import android.util.AttributeSet; -import android.util.Base64; -import android.util.Log; -import android.util.SparseBooleanArray; -import android.view.Gravity; -import android.view.KeyEvent; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.view.MotionEvent; -import android.view.OrientationEventListener; -import android.view.SurfaceView; -import android.view.View; -import android.view.ViewGroup; -import android.view.ViewTreeObserver; -import android.view.Window; -import android.widget.AbsoluteLayout; -import android.widget.AdapterView; -import android.widget.Button; -import android.widget.FrameLayout; -import android.widget.ListView; -import android.widget.RelativeLayout; -import android.widget.SimpleAdapter; -import android.widget.TextView; -import android.widget.Toast; - -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.lang.ref.WeakReference; -import java.net.URL; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.LinkedList; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.TimeUnit; - -public abstract class GeckoApp - extends GeckoActivity - implements - ContextGetter, - GeckoAppShell.GeckoInterface, - GeckoEventListener, - GeckoMenu.Callback, - GeckoMenu.MenuPresenter, - NativeEventListener, - Tabs.OnTabsChangedListener, - ViewTreeObserver.OnGlobalLayoutListener { - - private static final String LOGTAG = "GeckoApp"; - private static final long ONE_DAY_MS = TimeUnit.MILLISECONDS.convert(1, TimeUnit.DAYS); - - public static final String ACTION_ALERT_CALLBACK = "org.mozilla.gecko.ALERT_CALLBACK"; - public static final String ACTION_HOMESCREEN_SHORTCUT = "org.mozilla.gecko.BOOKMARK"; - public static final String ACTION_DEBUG = "org.mozilla.gecko.DEBUG"; - public static final String ACTION_LAUNCH_SETTINGS = "org.mozilla.gecko.SETTINGS"; - public static final String ACTION_LOAD = "org.mozilla.gecko.LOAD"; - public static final String ACTION_INIT_PW = "org.mozilla.gecko.INIT_PW"; - public static final String ACTION_SWITCH_TAB = "org.mozilla.gecko.SWITCH_TAB"; - - public static final String INTENT_REGISTER_STUMBLER_LISTENER = "org.mozilla.gecko.STUMBLER_REGISTER_LOCAL_LISTENER"; - - public static final String EXTRA_STATE_BUNDLE = "stateBundle"; - - public static final String LAST_SELECTED_TAB = "lastSelectedTab"; - - public static final String PREFS_ALLOW_STATE_BUNDLE = "allowStateBundle"; - public static final String PREFS_VERSION_CODE = "versionCode"; - public static final String PREFS_WAS_STOPPED = "wasStopped"; - public static final String PREFS_CRASHED_COUNT = "crashedCount"; - public static final String PREFS_CLEANUP_TEMP_FILES = "cleanupTempFiles"; - - public static final String SAVED_STATE_IN_BACKGROUND = "inBackground"; - public static final String SAVED_STATE_PRIVATE_SESSION = "privateSession"; - - // Delay before running one-time "cleanup" tasks that may be needed - // after a version upgrade. - private static final int CLEANUP_DEFERRAL_SECONDS = 15; - - private static boolean sAlreadyLoaded; - - private static WeakReference lastActiveGeckoApp; - - protected RelativeLayout mRootLayout; - protected RelativeLayout mMainLayout; - - protected RelativeLayout mGeckoLayout; - private OrientationEventListener mCameraOrientationEventListener; - public List mAppStateListeners = new LinkedList(); - protected MenuPanel mMenuPanel; - protected Menu mMenu; - protected boolean mIsRestoringActivity; - - /** Tells if we're aborting app launch, e.g. if this is an unsupported device configuration. */ - protected boolean mIsAbortingAppLaunch; - - private PromptService mPromptService; - protected TextSelection mTextSelection; - - protected DoorHangerPopup mDoorHangerPopup; - protected FormAssistPopup mFormAssistPopup; - - - protected GeckoView mLayerView; - private AbsoluteLayout mPluginContainer; - - private FullScreenHolder mFullScreenPluginContainer; - private View mFullScreenPluginView; - - private final HashMap mWakeLocks = new HashMap(); - - protected boolean mLastSessionCrashed; - protected boolean mShouldRestore; - private boolean mSessionRestoreParsingFinished = false; - - private EventDispatcher eventDispatcher; - - private int lastSelectedTabId = -1; - - private static final class LastSessionParser extends SessionParser { - private JSONArray tabs; - private JSONObject windowObject; - private boolean isExternalURL; - - private boolean selectNextTab; - private boolean tabsWereSkipped; - private boolean tabsWereProcessed; - - public LastSessionParser(JSONArray tabs, JSONObject windowObject, boolean isExternalURL) { - this.tabs = tabs; - this.windowObject = windowObject; - this.isExternalURL = isExternalURL; - } - - public boolean allTabsSkipped() { - return tabsWereSkipped && !tabsWereProcessed; - } - - @Override - public void onTabRead(final SessionTab sessionTab) { - if (sessionTab.isAboutHomeWithoutHistory()) { - // This is a tab pointing to about:home with no history. We won't restore - // this tab. If we end up restoring no tabs then the browser will decide - // whether it needs to open about:home or a different 'homepage'. If we'd - // always restore about:home only tabs then we'd never open the homepage. - // See bug 1261008. - - if (sessionTab.isSelected()) { - // Unfortunately this tab is the selected tab. Let's just try to select - // the first tab. If we haven't restored any tabs so far then remember - // to select the next tab that gets restored. - - if (!Tabs.getInstance().selectLastTab()) { - selectNextTab = true; - } - } - - // Do not restore this tab. - tabsWereSkipped = true; - return; - } - - tabsWereProcessed = true; - - JSONObject tabObject = sessionTab.getTabObject(); - - int flags = Tabs.LOADURL_NEW_TAB; - flags |= ((isExternalURL || !sessionTab.isSelected()) ? Tabs.LOADURL_DELAY_LOAD : 0); - flags |= (tabObject.optBoolean("desktopMode") ? Tabs.LOADURL_DESKTOP : 0); - flags |= (tabObject.optBoolean("isPrivate") ? Tabs.LOADURL_PRIVATE : 0); - - final Tab tab = Tabs.getInstance().loadUrl(sessionTab.getUrl(), flags); - - if (selectNextTab) { - // We did not restore the selected tab previously. Now let's select this tab. - Tabs.getInstance().selectTab(tab.getId()); - selectNextTab = false; - } - - ThreadUtils.postToUiThread(new Runnable() { - @Override - public void run() { - tab.updateTitle(sessionTab.getTitle()); - } - }); - - try { - tabObject.put("tabId", tab.getId()); - } catch (JSONException e) { - Log.e(LOGTAG, "JSON error", e); - } - tabs.put(tabObject); - } - - @Override - public void onClosedTabsRead(final JSONArray closedTabData) throws JSONException { - windowObject.put("closedTabs", closedTabData); - } - }; - - protected boolean mInitialized; - protected boolean mWindowFocusInitialized; - private Telemetry.Timer mJavaUiStartupTimer; - private Telemetry.Timer mGeckoReadyStartupTimer; - - private String mPrivateBrowsingSession; - - private volatile HealthRecorder mHealthRecorder; - private volatile Locale mLastLocale; - - protected Intent mRestartIntent; - - private boolean mWasFirstTabShownAfterActivityUnhidden; - - abstract public int getLayout(); - - protected void processTabQueue() {}; - - protected void openQueuedTabs() {}; - - @SuppressWarnings("serial") - class SessionRestoreException extends Exception { - public SessionRestoreException(Exception e) { - super(e); - } - - public SessionRestoreException(String message) { - super(message); - } - } - - void toggleChrome(final boolean aShow) { } - - void focusChrome() { } - - @Override - public Context getContext() { - return this; - } - - @Override - public SharedPreferences getSharedPreferences() { - return GeckoSharedPrefs.forApp(this); - } - - @Override - public Activity getActivity() { - return this; - } - - @Override - public void addAppStateListener(GeckoAppShell.AppStateListener listener) { - mAppStateListeners.add(listener); - } - - @Override - public void removeAppStateListener(GeckoAppShell.AppStateListener listener) { - mAppStateListeners.remove(listener); - } - - @Override - public void onTabChanged(Tab tab, Tabs.TabEvents msg, String data) { - // When a tab is closed, it is always unselected first. - // When a tab is unselected, another tab is always selected first. - switch (msg) { - case UNSELECTED: - break; - - case LOCATION_CHANGE: - // We only care about location change for the selected tab. - if (!Tabs.getInstance().isSelectedTab(tab)) - break; - // Fall through... - case SELECTED: - invalidateOptionsMenu(); - if (mFormAssistPopup != null) - mFormAssistPopup.hide(); - break; - - case DESKTOP_MODE_CHANGE: - if (Tabs.getInstance().isSelectedTab(tab)) - invalidateOptionsMenu(); - break; - } - } - - public void refreshChrome() { } - - @Override - public void invalidateOptionsMenu() { - if (mMenu == null) { - return; - } - - onPrepareOptionsMenu(mMenu); - - super.invalidateOptionsMenu(); - } - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - mMenu = menu; - - MenuInflater inflater = getMenuInflater(); - inflater.inflate(R.menu.gecko_app_menu, mMenu); - return true; - } - - @Override - public MenuInflater getMenuInflater() { - return new GeckoMenuInflater(this); - } - - public MenuPanel getMenuPanel() { - if (mMenuPanel == null) { - onCreatePanelMenu(Window.FEATURE_OPTIONS_PANEL, null); - invalidateOptionsMenu(); - } - return mMenuPanel; - } - - @Override - public boolean onMenuItemClick(MenuItem item) { - return onOptionsItemSelected(item); - } - - @Override - public boolean onMenuItemLongClick(MenuItem item) { - return false; - } - - @Override - public void openMenu() { - openOptionsMenu(); - } - - @Override - public void showMenu(final View menu) { - // On devices using the custom menu, focus is cleared from the menu when its tapped. - // Close and then reshow it to avoid these issues. See bug 794581 and bug 968182. - closeMenu(); - - // Post the reshow code back to the UI thread to avoid some optimizations Android - // has put in place for menus that hide/show themselves quickly. See bug 985400. - ThreadUtils.postToUiThread(new Runnable() { - @Override - public void run() { - mMenuPanel.removeAllViews(); - mMenuPanel.addView(menu); - openOptionsMenu(); - } - }); - } - - @Override - public void closeMenu() { - closeOptionsMenu(); - } - - @Override - public View onCreatePanelView(int featureId) { - if (featureId == Window.FEATURE_OPTIONS_PANEL) { - if (mMenuPanel == null) { - mMenuPanel = new MenuPanel(this, null); - } else { - // Prepare the panel every time before showing the menu. - onPreparePanel(featureId, mMenuPanel, mMenu); - } - - return mMenuPanel; - } - - return super.onCreatePanelView(featureId); - } - - @Override - public boolean onCreatePanelMenu(int featureId, Menu menu) { - if (featureId == Window.FEATURE_OPTIONS_PANEL) { - if (mMenuPanel == null) { - mMenuPanel = (MenuPanel) onCreatePanelView(featureId); - } - - GeckoMenu gMenu = new GeckoMenu(this, null); - gMenu.setCallback(this); - gMenu.setMenuPresenter(this); - menu = gMenu; - mMenuPanel.addView(gMenu); - - return onCreateOptionsMenu(menu); - } - - return super.onCreatePanelMenu(featureId, menu); - } - - @Override - public boolean onPreparePanel(int featureId, View view, Menu menu) { - if (featureId == Window.FEATURE_OPTIONS_PANEL) { - return onPrepareOptionsMenu(menu); - } - - return super.onPreparePanel(featureId, view, menu); - } - - @Override - public boolean onMenuOpened(int featureId, Menu menu) { - // exit full-screen mode whenever the menu is opened - if (mLayerView != null && mLayerView.isFullScreen()) { - GeckoAppShell.notifyObservers("FullScreen:Exit", null); - } - - if (featureId == Window.FEATURE_OPTIONS_PANEL) { - if (mMenu == null) { - // getMenuPanel() will force the creation of the menu as well - MenuPanel panel = getMenuPanel(); - onPreparePanel(featureId, panel, mMenu); - } - - // Scroll custom menu to the top - if (mMenuPanel != null) - mMenuPanel.scrollTo(0, 0); - - return true; - } - - return super.onMenuOpened(featureId, menu); - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - if (item.getItemId() == R.id.quit) { - // Make sure the Guest Browsing notification goes away when we quit. - GuestSession.hideNotification(this); - - final SharedPreferences prefs = GeckoSharedPrefs.forProfile(this); - final Set clearSet = - PrefUtils.getStringSet(prefs, ClearOnShutdownPref.PREF, new HashSet()); - - final JSONObject clearObj = new JSONObject(); - for (String clear : clearSet) { - try { - clearObj.put(clear, true); - } catch (JSONException ex) { - Log.e(LOGTAG, "Error adding clear object " + clear, ex); - } - } - - final JSONObject res = new JSONObject(); - try { - res.put("sanitize", clearObj); - } catch (JSONException ex) { - Log.e(LOGTAG, "Error adding sanitize object", ex); - } - - // If the user has opted out of session restore, and does want to clear history - // we also want to prevent the current session info from being saved. - if (clearObj.has("private.data.history")) { - final String sessionRestore = getSessionRestorePreference(getSharedPreferences()); - try { - res.put("dontSaveSession", "quit".equals(sessionRestore)); - } catch (JSONException ex) { - Log.e(LOGTAG, "Error adding session restore data", ex); - } - } - - GeckoAppShell.notifyObservers("Browser:Quit", res.toString()); - // We don't call doShutdown() here because this creates a race condition which can - // cause the clearing of private data to fail. Instead, we shut down the UI only after - // we're done sanitizing. - return true; - } - - return super.onOptionsItemSelected(item); - } - - @Override - public void onOptionsMenuClosed(Menu menu) { - mMenuPanel.removeAllViews(); - mMenuPanel.addView((GeckoMenu) mMenu); - } - - @Override - public boolean onKeyDown(int keyCode, KeyEvent event) { - // Handle hardware menu key presses separately so that we can show a custom menu in some cases. - if (keyCode == KeyEvent.KEYCODE_MENU) { - openOptionsMenu(); - return true; - } - - return super.onKeyDown(keyCode, event); - } - - @Override - protected void onSaveInstanceState(Bundle outState) { - super.onSaveInstanceState(outState); - - outState.putBoolean(SAVED_STATE_IN_BACKGROUND, isApplicationInBackground()); - outState.putString(SAVED_STATE_PRIVATE_SESSION, mPrivateBrowsingSession); - outState.putInt(LAST_SELECTED_TAB, lastSelectedTabId); - } - - @Override - protected void onRestoreInstanceState(final Bundle inState) { - lastSelectedTabId = inState.getInt(LAST_SELECTED_TAB); - } - - public void addTab() { } - - public void addPrivateTab() { } - - public void showNormalTabs() { } - - public void showPrivateTabs() { } - - public void hideTabs() { } - - /** - * Close the tab UI indirectly (not as the result of a direct user - * action). This does not force the UI to close; for example in Firefox - * tablet mode it will remain open unless the user explicitly closes it. - * - * @return True if the tab UI was hidden. - */ - public boolean autoHideTabs() { return false; } - - @Override - public boolean areTabsShown() { return false; } - - @Override - public void handleMessage(final String event, final NativeJSObject message, - final EventCallback callback) { - if ("Accessibility:Ready".equals(event)) { - GeckoAccessibility.updateAccessibilitySettings(this); - - } else if ("Bookmark:Insert".equals(event)) { - final String url = message.getString("url"); - final String title = message.getString("title"); - final Context context = this; - final BrowserDB db = BrowserDB.from(getProfile()); - ThreadUtils.postToBackgroundThread(new Runnable() { - @Override - public void run() { - final boolean bookmarkAdded = db.addBookmark(getContentResolver(), title, url); - final int resId = bookmarkAdded ? R.string.bookmark_added : R.string.bookmark_already_added; - ThreadUtils.postToUiThread(new Runnable() { - @Override - public void run() { - SnackbarBuilder.builder(GeckoApp.this) - .message(resId) - .duration(Snackbar.LENGTH_LONG) - .buildAndShow(); - } - }); - } - }); - - } else if ("Contact:Add".equals(event)) { - final String email = message.optString("email", null); - final String phone = message.optString("phone", null); - if (email != null) { - Uri contactUri = Uri.parse(email); - Intent i = new Intent(ContactsContract.Intents.SHOW_OR_CREATE_CONTACT, contactUri); - startActivity(i); - } else if (phone != null) { - Uri contactUri = Uri.parse(phone); - Intent i = new Intent(ContactsContract.Intents.SHOW_OR_CREATE_CONTACT, contactUri); - startActivity(i); - } else { - // something went wrong. - Log.e(LOGTAG, "Received Contact:Add message with no email nor phone number"); - } - - } else if ("DevToolsAuth:Scan".equals(event)) { - DevToolsAuthHelper.scan(this, callback); - - } else if ("DOMFullScreen:Start".equals(event)) { - // Local ref to layerView for thread safety - LayerView layerView = mLayerView; - if (layerView != null) { - layerView.setFullScreenState(message.getBoolean("rootElement") - ? FullScreenState.ROOT_ELEMENT : FullScreenState.NON_ROOT_ELEMENT); - } - - } else if ("DOMFullScreen:Stop".equals(event)) { - // Local ref to layerView for thread safety - LayerView layerView = mLayerView; - if (layerView != null) { - layerView.setFullScreenState(FullScreenState.NONE); - } - - } else if ("Image:SetAs".equals(event)) { - String src = message.getString("url"); - setImageAs(src); - - } else if ("Locale:Set".equals(event)) { - setLocale(message.getString("locale")); - - } else if ("Permissions:Data".equals(event)) { - final NativeJSObject[] permissions = message.getObjectArray("permissions"); - showSiteSettingsDialog(permissions); - - } else if ("PrivateBrowsing:Data".equals(event)) { - mPrivateBrowsingSession = message.optString("session", null); - - } else if ("Session:StatePurged".equals(event)) { - onStatePurged(); - - } else if ("Sanitize:Finished".equals(event)) { - if (message.getBoolean("shutdown")) { - // Gecko is shutting down and has called our sanitize handlers, - // so we can start exiting, too. - doShutdown(); - } - - } else if ("Share:Text".equals(event)) { - final String text = message.getString("text"); - final Tab tab = Tabs.getInstance().getSelectedTab(); - String title = ""; - if (tab != null) { - title = tab.getDisplayTitle(); - } - IntentHelper.openUriExternal(text, "text/plain", "", "", Intent.ACTION_SEND, title, false); - - // Context: Sharing via chrome list (no explicit session is active) - Telemetry.sendUIEvent(TelemetryContract.Event.SHARE, TelemetryContract.Method.LIST, "text"); - - } else if ("Snackbar:Show".equals(event)) { - SnackbarBuilder.builder(this) - .fromEvent(message) - .callback(callback) - .buildAndShow(); - - } else if ("SystemUI:Visibility".equals(event)) { - setSystemUiVisible(message.getBoolean("visible")); - - } else if ("ToggleChrome:Focus".equals(event)) { - focusChrome(); - - } else if ("ToggleChrome:Hide".equals(event)) { - toggleChrome(false); - - } else if ("ToggleChrome:Show".equals(event)) { - toggleChrome(true); - - } else if ("Update:Check".equals(event)) { - UpdateServiceHelper.checkForUpdate(this); - } else if ("Update:Download".equals(event)) { - UpdateServiceHelper.downloadUpdate(this); - } else if ("Update:Install".equals(event)) { - UpdateServiceHelper.applyUpdate(this); - } else if ("RuntimePermissions:Prompt".equals(event)) { - String[] permissions = message.getStringArray("permissions"); - if (callback == null || permissions == null) { - return; - } - - Permissions.from(this) - .withPermissions(permissions) - .andFallback(new Runnable() { - @Override - public void run() { - callback.sendSuccess(false); - } - }) - .run(new Runnable() { - @Override - public void run() { - callback.sendSuccess(true); - } - }); - } - } - - @Override - public void handleMessage(String event, JSONObject message) { - try { - if (event.equals("Gecko:Ready")) { - mGeckoReadyStartupTimer.stop(); - geckoConnected(); - - // This method is already running on the background thread, so we - // know that mHealthRecorder will exist. That doesn't stop us being - // paranoid. - // This method is cheap, so don't spawn a new runnable. - final HealthRecorder rec = mHealthRecorder; - if (rec != null) { - rec.recordGeckoStartupTime(mGeckoReadyStartupTimer.getElapsed()); - } - - GeckoApplication.get().onDelayedStartup(); - - } else if (event.equals("Gecko:Exited")) { - // Gecko thread exited first; let GeckoApp die too. - doShutdown(); - return; - - } else if (event.equals("Accessibility:Event")) { - GeckoAccessibility.sendAccessibilityEvent(message); - } - } catch (Exception e) { - Log.e(LOGTAG, "Exception handling message \"" + event + "\":", e); - } - } - - void onStatePurged() { } - - /** - * @param permissions - * Array of JSON objects to represent site permissions. - * Example: { type: "offline-app", setting: "Store Offline Data", value: "Allow" } - */ - private void showSiteSettingsDialog(final NativeJSObject[] permissions) { - final AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setTitle(R.string.site_settings_title); - - final ArrayList> itemList = - new ArrayList>(); - for (final NativeJSObject permObj : permissions) { - final HashMap map = new HashMap(); - map.put("setting", permObj.getString("setting")); - map.put("value", permObj.getString("value")); - itemList.add(map); - } - - // setMultiChoiceItems doesn't support using an adapter, so we're creating a hack with - // setSingleChoiceItems and changing the choiceMode below when we create the dialog - builder.setSingleChoiceItems(new SimpleAdapter( - GeckoApp.this, - itemList, - R.layout.site_setting_item, - new String[] { "setting", "value" }, - new int[] { R.id.setting, R.id.value } - ), -1, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int id) { } - }); - - builder.setPositiveButton(R.string.site_settings_clear, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int id) { - ListView listView = ((AlertDialog) dialog).getListView(); - SparseBooleanArray checkedItemPositions = listView.getCheckedItemPositions(); - - // An array of the indices of the permissions we want to clear - JSONArray permissionsToClear = new JSONArray(); - for (int i = 0; i < checkedItemPositions.size(); i++) - if (checkedItemPositions.get(i)) - permissionsToClear.put(i); - - GeckoAppShell.notifyObservers("Permissions:Clear", permissionsToClear.toString()); - } - }); - - builder.setNegativeButton(R.string.site_settings_cancel, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int id) { - dialog.cancel(); - } - }); - - ThreadUtils.postToUiThread(new Runnable() { - @Override - public void run() { - AlertDialog dialog = builder.create(); - dialog.show(); - - final ListView listView = dialog.getListView(); - if (listView != null) { - listView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE); - } - - final Button clearButton = dialog.getButton(DialogInterface.BUTTON_POSITIVE); - clearButton.setEnabled(false); - - dialog.getListView().setOnItemClickListener(new AdapterView.OnItemClickListener() { - @Override - public void onItemClick(AdapterView adapterView, View view, int i, long l) { - if (listView.getCheckedItemCount() == 0) { - clearButton.setEnabled(false); - } else { - clearButton.setEnabled(true); - } - } - }); - } - }); - } - - - - /* package */ void addFullScreenPluginView(View view) { - if (mFullScreenPluginView != null) { - Log.w(LOGTAG, "Already have a fullscreen plugin view"); - return; - } - - setFullScreen(true); - - view.setWillNotDraw(false); - if (view instanceof SurfaceView) { - ((SurfaceView) view).setZOrderOnTop(true); - } - - mFullScreenPluginContainer = new FullScreenHolder(this); - - FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.MATCH_PARENT, - Gravity.CENTER); - mFullScreenPluginContainer.addView(view, layoutParams); - - - FrameLayout decor = (FrameLayout)getWindow().getDecorView(); - decor.addView(mFullScreenPluginContainer, layoutParams); - - mFullScreenPluginView = view; - } - - @Override - public void addPluginView(final View view) { - - if (ThreadUtils.isOnUiThread()) { - addFullScreenPluginView(view); - } else { - ThreadUtils.postToUiThread(new Runnable() { - @Override - public void run() { - addFullScreenPluginView(view); - } - }); - } - } - - /* package */ void removeFullScreenPluginView(View view) { - if (mFullScreenPluginView == null) { - Log.w(LOGTAG, "Don't have a fullscreen plugin view"); - return; - } - - if (mFullScreenPluginView != view) { - Log.w(LOGTAG, "Passed view is not the current full screen view"); - return; - } - - mFullScreenPluginContainer.removeView(mFullScreenPluginView); - - // We need do do this on the next iteration in order to avoid - // a deadlock, see comment below in FullScreenHolder - ThreadUtils.postToUiThread(new Runnable() { - @Override - public void run() { - mLayerView.showSurface(); - } - }); - - FrameLayout decor = (FrameLayout)getWindow().getDecorView(); - decor.removeView(mFullScreenPluginContainer); - - mFullScreenPluginView = null; - - GeckoScreenOrientation.getInstance().unlock(); - setFullScreen(false); - } - - @Override - public void removePluginView(final View view) { - if (ThreadUtils.isOnUiThread()) { - removePluginView(view); - } else { - ThreadUtils.postToUiThread(new Runnable() { - @Override - public void run() { - removeFullScreenPluginView(view); - } - }); - } - } - - // This method starts downloading an image synchronously and displays the Chooser activity to set the image as wallpaper. - private void setImageAs(final String aSrc) { - boolean isDataURI = aSrc.startsWith("data:"); - Bitmap image = null; - InputStream is = null; - ByteArrayOutputStream os = null; - try { - if (isDataURI) { - int dataStart = aSrc.indexOf(","); - byte[] buf = Base64.decode(aSrc.substring(dataStart + 1), Base64.DEFAULT); - image = BitmapUtils.decodeByteArray(buf); - } else { - int byteRead; - byte[] buf = new byte[4192]; - os = new ByteArrayOutputStream(); - URL url = new URL(aSrc); - is = url.openStream(); - - // Cannot read from same stream twice. Also, InputStream from - // URL does not support reset. So converting to byte array. - - while ((byteRead = is.read(buf)) != -1) { - os.write(buf, 0, byteRead); - } - byte[] imgBuffer = os.toByteArray(); - image = BitmapUtils.decodeByteArray(imgBuffer); - } - if (image != null) { - // Some devices don't have a DCIM folder and the Media.insertImage call will fail. - File dcimDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES); - - if (!dcimDir.mkdirs() && !dcimDir.isDirectory()) { - SnackbarBuilder.builder(this) - .message(R.string.set_image_path_fail) - .duration(Snackbar.LENGTH_LONG) - .buildAndShow(); - return; - } - String path = Media.insertImage(getContentResolver(), image, null, null); - if (path == null) { - SnackbarBuilder.builder(this) - .message(R.string.set_image_path_fail) - .duration(Snackbar.LENGTH_LONG) - .buildAndShow(); - return; - } - final Intent intent = new Intent(Intent.ACTION_ATTACH_DATA); - intent.addCategory(Intent.CATEGORY_DEFAULT); - intent.setData(Uri.parse(path)); - - // Removes the image from storage once the chooser activity ends. - Intent chooser = Intent.createChooser(intent, getString(R.string.set_image_chooser_title)); - ActivityResultHandler handler = new ActivityResultHandler() { - @Override - public void onActivityResult (int resultCode, Intent data) { - getContentResolver().delete(intent.getData(), null, null); - } - }; - ActivityHandlerHelper.startIntentForActivity(this, chooser, handler); - } else { - SnackbarBuilder.builder(this) - .message(R.string.set_image_fail) - .duration(Snackbar.LENGTH_LONG) - .buildAndShow(); - } - } catch (OutOfMemoryError ome) { - Log.e(LOGTAG, "Out of Memory when converting to byte array", ome); - } catch (IOException ioe) { - Log.e(LOGTAG, "I/O Exception while setting wallpaper", ioe); - } finally { - if (is != null) { - try { - is.close(); - } catch (IOException ioe) { - Log.w(LOGTAG, "I/O Exception while closing stream", ioe); - } - } - if (os != null) { - try { - os.close(); - } catch (IOException ioe) { - Log.w(LOGTAG, "I/O Exception while closing stream", ioe); - } - } - } - } - - private int getBitmapSampleSize(BitmapFactory.Options options, int idealWidth, int idealHeight) { - int width = options.outWidth; - int height = options.outHeight; - int inSampleSize = 1; - if (height > idealHeight || width > idealWidth) { - if (width > height) { - inSampleSize = Math.round((float)height / idealHeight); - } else { - inSampleSize = Math.round((float)width / idealWidth); - } - } - return inSampleSize; - } - - public void requestRender() { - mLayerView.requestRender(); - } - - @Override - public void setFullScreen(final boolean fullscreen) { - ThreadUtils.postToUiThread(new Runnable() { - @Override - public void run() { - ActivityUtils.setFullScreen(GeckoApp.this, fullscreen); - } - }); - } - - /** - * Check and start the Java profiler if MOZ_PROFILER_STARTUP env var is specified. - **/ - protected static void earlyStartJavaSampler(SafeIntent intent) { - String env = intent.getStringExtra("env0"); - for (int i = 1; env != null; i++) { - if (env.startsWith("MOZ_PROFILER_STARTUP=")) { - if (!env.endsWith("=")) { - GeckoJavaSampler.start(10, 1000); - Log.d(LOGTAG, "Profiling Java on startup"); - } - break; - } - env = intent.getStringExtra("env" + i); - } - } - - /** - * Called when the activity is first created. - * - * Here we initialize all of our profile settings, Firefox Health Report, - * and other one-shot constructions. - **/ - @Override - public void onCreate(Bundle savedInstanceState) { - GeckoAppShell.ensureCrashHandling(); - - eventDispatcher = new EventDispatcher(); - - // Enable Android Strict Mode for developers' local builds (the "default" channel). - if ("default".equals(AppConstants.MOZ_UPDATE_CHANNEL)) { - enableStrictMode(); - } - - if (!HardwareUtils.isSupportedSystem()) { - // This build does not support the Android version of the device: Show an error and finish the app. - mIsAbortingAppLaunch = true; - super.onCreate(savedInstanceState); - showSDKVersionError(); - finish(); - return; - } - - // The clock starts...now. Better hurry! - mJavaUiStartupTimer = new Telemetry.UptimeTimer("FENNEC_STARTUP_TIME_JAVAUI"); - mGeckoReadyStartupTimer = new Telemetry.UptimeTimer("FENNEC_STARTUP_TIME_GECKOREADY"); - - final SafeIntent intent = new SafeIntent(getIntent()); - - earlyStartJavaSampler(intent); - - // GeckoLoader wants to dig some environment variables out of the - // incoming intent, so pass it in here. GeckoLoader will do its - // business later and dispose of the reference. - GeckoLoader.setLastIntent(intent); - - // Workaround for . - try { - Class.forName("android.os.AsyncTask"); - } catch (ClassNotFoundException e) { } - - MemoryMonitor.getInstance().init(getApplicationContext()); - - // GeckoAppShell is tightly coupled to us, rather than - // the app context, because various parts of Fennec (e.g., - // GeckoScreenOrientation) use GAS to access the Activity in - // the guise of fetching a Context. - // When that's fixed, `this` can change to - // `(GeckoApplication) getApplication()` here. - GeckoAppShell.setContextGetter(this); - GeckoAppShell.setGeckoInterface(this); - - // Tell Stumbler to register a local broadcast listener to listen for preference intents. - // We do this via intents since we can't easily access Stumbler directly, - // as it might be compiled outside of Fennec. - getApplicationContext().sendBroadcast( - new Intent(INTENT_REGISTER_STUMBLER_LISTENER) - ); - - // Did the OS locale change while we were backgrounded? If so, - // we need to die so that Gecko will re-init add-ons that touch - // the UI. - // This is using a sledgehammer to crack a nut, but it'll do for - // now. - // Our OS locale pref will be detected as invalid after the - // restart, and will be propagated to Gecko accordingly, so there's - // no need to touch that here. - if (BrowserLocaleManager.getInstance().systemLocaleDidChange()) { - Log.i(LOGTAG, "System locale changed. Restarting."); - doRestart(); - return; - } - - if (sAlreadyLoaded) { - // This happens when the GeckoApp activity is destroyed by Android - // without killing the entire application (see Bug 769269). - mIsRestoringActivity = true; - Telemetry.addToHistogram("FENNEC_RESTORING_ACTIVITY", 1); - - } else { - final String action = intent.getAction(); - final String args = intent.getStringExtra("args"); - - sAlreadyLoaded = true; - GeckoThread.init(/* profile */ null, args, action, - /* debugging */ ACTION_DEBUG.equals(action)); - - // Speculatively pre-fetch the profile in the background. - ThreadUtils.postToBackgroundThread(new Runnable() { - @Override - public void run() { - getProfile(); - } - }); - - final String uri = getURIFromIntent(intent); - if (!TextUtils.isEmpty(uri)) { - // Start a speculative connection as soon as Gecko loads. - GeckoThread.speculativeConnect(uri); - } - } - - // GeckoThread has to register for "Gecko:Ready" first, so GeckoApp registers - // for events after initializing GeckoThread but before launching it. - - getAppEventDispatcher().registerGeckoThreadListener((GeckoEventListener)this, - "Gecko:Ready", - "Gecko:Exited", - "Accessibility:Event"); - - getAppEventDispatcher().registerGeckoThreadListener((NativeEventListener)this, - "Accessibility:Ready", - "Bookmark:Insert", - "Contact:Add", - "DevToolsAuth:Scan", - "DOMFullScreen:Start", - "DOMFullScreen:Stop", - "Image:SetAs", - "Locale:Set", - "Permissions:Data", - "PrivateBrowsing:Data", - "RuntimePermissions:Prompt", - "Sanitize:Finished", - "Session:StatePurged", - "Share:Text", - "Snackbar:Show", - "SystemUI:Visibility", - "ToggleChrome:Focus", - "ToggleChrome:Hide", - "ToggleChrome:Show", - "Update:Check", - "Update:Download", - "Update:Install"); - - GeckoThread.launch(); - - Bundle stateBundle = IntentUtils.getBundleExtraSafe(getIntent(), EXTRA_STATE_BUNDLE); - if (stateBundle != null) { - // Use the state bundle if it was given as an intent extra. This is - // only intended to be used internally via Robocop, so a boolean - // is read from a private shared pref to prevent other apps from - // injecting states. - final SharedPreferences prefs = getSharedPreferences(); - if (prefs.getBoolean(PREFS_ALLOW_STATE_BUNDLE, false)) { - prefs.edit().remove(PREFS_ALLOW_STATE_BUNDLE).apply(); - savedInstanceState = stateBundle; - } - } else if (savedInstanceState != null) { - // Bug 896992 - This intent has already been handled; reset the intent. - setIntent(new Intent(Intent.ACTION_MAIN)); - } - - super.onCreate(savedInstanceState); - - GeckoScreenOrientation.getInstance().update(getResources().getConfiguration().orientation); - - setContentView(getLayout()); - - // Set up Gecko layout. - mRootLayout = (RelativeLayout) findViewById(R.id.root_layout); - mGeckoLayout = (RelativeLayout) findViewById(R.id.gecko_layout); - mMainLayout = (RelativeLayout) findViewById(R.id.main_layout); - mLayerView = (GeckoView) findViewById(R.id.layer_view); - - Tabs.getInstance().attachToContext(this, mLayerView); - - // Use global layout state change to kick off additional initialization - mMainLayout.getViewTreeObserver().addOnGlobalLayoutListener(this); - - if (Versions.preMarshmallow) { - mTextSelection = new ActionBarTextSelection(this); - } else { - mTextSelection = new FloatingToolbarTextSelection(this, mLayerView); - } - mTextSelection.create(); - - // Determine whether we should restore tabs. - mLastSessionCrashed = updateCrashedState(); - mShouldRestore = getSessionRestoreState(savedInstanceState); - if (mShouldRestore && savedInstanceState != null) { - boolean wasInBackground = - savedInstanceState.getBoolean(SAVED_STATE_IN_BACKGROUND, false); - - // Don't log OOM-kills if only one activity was destroyed. (For example - // from "Don't keep activities" on ICS) - if (!wasInBackground && !mIsRestoringActivity) { - Telemetry.addToHistogram("FENNEC_WAS_KILLED", 1); - } - - mPrivateBrowsingSession = savedInstanceState.getString(SAVED_STATE_PRIVATE_SESSION); - } - - ThreadUtils.postToBackgroundThread(new Runnable() { - @Override - public void run() { - // If we are doing a restore, read the session data so we can send it to Gecko later. - String restoreMessage = null; - if (!mIsRestoringActivity && mShouldRestore) { - final boolean isExternalURL = invokedWithExternalURL(getIntentURI(new SafeIntent(getIntent()))); - try { - // restoreSessionTabs() will create simple tab stubs with the - // URL and title for each page, but we also need to restore - // session history. restoreSessionTabs() will inject the IDs - // of the tab stubs into the JSON data (which holds the session - // history). This JSON data is then sent to Gecko so session - // history can be restored for each tab. - restoreMessage = restoreSessionTabs(isExternalURL, false); - } catch (SessionRestoreException e) { - // If mShouldRestore was set to false in restoreSessionTabs(), this means - // either that we intentionally skipped all tabs read from the session file, - // or else that the file was syntactically valid, but didn't contain any - // tabs (e.g. because the user cleared history), therefore we don't need - // to switch to the backup copy. - if (mShouldRestore) { - Log.e(LOGTAG, "An error occurred during restore, switching to backup file", e); - // To be on the safe side, we will always attempt to restore from the backup - // copy if we end up here. - // Since we will also hit this situation regularly during first run though, - // we'll only report it in telemetry if we failed to restore despite the - // file existing, which means it's very probably damaged. - if (getProfile().sessionFileExists()) { - Telemetry.addToHistogram("FENNEC_SESSIONSTORE_DAMAGED_SESSION_FILE", 1); - } - try { - restoreMessage = restoreSessionTabs(isExternalURL, true); - Telemetry.addToHistogram("FENNEC_SESSIONSTORE_RESTORING_FROM_BACKUP", 1); - } catch (SessionRestoreException ex) { - if (!mShouldRestore) { - // Restoring only "failed" because the backup copy was deliberately empty, too. - Telemetry.addToHistogram("FENNEC_SESSIONSTORE_RESTORING_FROM_BACKUP", 1); - } else { - // Restoring the backup failed, too, so do a normal startup. - Log.e(LOGTAG, "An error occurred during restore", ex); - mShouldRestore = false; - } - } - } - } - } - - synchronized (GeckoApp.this) { - mSessionRestoreParsingFinished = true; - GeckoApp.this.notifyAll(); - } - - // If we are doing a restore, send the parsed session data to Gecko. - if (!mIsRestoringActivity) { - GeckoAppShell.notifyObservers("Session:Restore", restoreMessage); - } - - // Make sure sessionstore.old is either updated or deleted as necessary. - getProfile().updateSessionFile(mShouldRestore); - } - }); - - // Perform background initialization. - ThreadUtils.postToBackgroundThread(new Runnable() { - @Override - public void run() { - final SharedPreferences prefs = GeckoApp.this.getSharedPreferences(); - - // Wait until now to set this, because we'd rather throw an exception than - // have a caller of BrowserLocaleManager regress startup. - final LocaleManager localeManager = BrowserLocaleManager.getInstance(); - localeManager.initialize(getApplicationContext()); - - SessionInformation previousSession = SessionInformation.fromSharedPrefs(prefs); - if (previousSession.wasKilled()) { - Telemetry.addToHistogram("FENNEC_WAS_KILLED", 1); - } - - SharedPreferences.Editor editor = prefs.edit(); - editor.putBoolean(GeckoAppShell.PREFS_OOM_EXCEPTION, false); - - // Put a flag to check if we got a normal `onSaveInstanceState` - // on exit, or if we were suddenly killed (crash or native OOM). - editor.putBoolean(GeckoApp.PREFS_WAS_STOPPED, false); - - editor.apply(); - - // The lifecycle of mHealthRecorder is "shortly after onCreate" - // through "onDestroy" -- essentially the same as the lifecycle - // of the activity itself. - final String profilePath = getProfile().getDir().getAbsolutePath(); - final EventDispatcher dispatcher = getAppEventDispatcher(); - - // This is the locale prior to fixing it up. - final Locale osLocale = Locale.getDefault(); - - // Both of these are Java-format locale strings: "en_US", not "en-US". - final String osLocaleString = osLocale.toString(); - String appLocaleString = localeManager.getAndApplyPersistedLocale(GeckoApp.this); - Log.d(LOGTAG, "OS locale is " + osLocaleString + ", app locale is " + appLocaleString); - - if (appLocaleString == null) { - appLocaleString = osLocaleString; - } - - mHealthRecorder = GeckoApp.this.createHealthRecorder(GeckoApp.this, - profilePath, - dispatcher, - osLocaleString, - appLocaleString, - previousSession); - - final String uiLocale = appLocaleString; - ThreadUtils.postToUiThread(new Runnable() { - @Override - public void run() { - GeckoApp.this.onLocaleReady(uiLocale); - } - }); - - // We use per-profile prefs here, because we're tracking against - // a Gecko pref. The same applies to the locale switcher! - BrowserLocaleManager.storeAndNotifyOSLocale(GeckoSharedPrefs.forProfile(GeckoApp.this), osLocale); - } - }); - - IntentHelper.init(this); - } - - @Override - public void onStart() { - super.onStart(); - if (mIsAbortingAppLaunch) { - return; - } - - mWasFirstTabShownAfterActivityUnhidden = false; // onStart indicates we were hidden. - } - - @Override - protected void onStop() { - super.onStop(); - // Overriding here is not necessary, but we do this so we don't - // forget to add the abort if we override this method later. - if (mIsAbortingAppLaunch) { - return; - } - } - - /** - * At this point, the resource system and the rest of the browser are - * aware of the locale. - * - * Now we can display strings! - * - * You can think of this as being something like a second phase of onCreate, - * where you can do string-related operations. Use this in place of embedding - * strings in view XML. - * - * By contrast, onConfigurationChanged does some locale operations, but is in - * response to device changes. - */ - @Override - public void onLocaleReady(final String locale) { - if (!ThreadUtils.isOnUiThread()) { - throw new RuntimeException("onLocaleReady must always be called from the UI thread."); - } - - final Locale loc = Locales.parseLocaleCode(locale); - if (loc.equals(mLastLocale)) { - Log.d(LOGTAG, "New locale same as old; onLocaleReady has nothing to do."); - } - - // The URL bar hint needs to be populated. - TextView urlBar = (TextView) findViewById(R.id.url_bar_title); - if (urlBar != null) { - final String hint = getResources().getString(R.string.url_bar_default_text); - urlBar.setHint(hint); - } else { - Log.d(LOGTAG, "No URL bar in GeckoApp. Not loading localized hint string."); - } - - mLastLocale = loc; - - // Allow onConfigurationChanged to take care of the rest. - // We don't call this.onConfigurationChanged, because (a) that does - // work that's unnecessary after this locale action, and (b) it can - // cause a loop! See Bug 1011008, Comment 12. - super.onConfigurationChanged(getResources().getConfiguration()); - } - - protected void initializeChrome() { - mDoorHangerPopup = new DoorHangerPopup(this); - mPluginContainer = (AbsoluteLayout) findViewById(R.id.plugin_container); - mFormAssistPopup = (FormAssistPopup) findViewById(R.id.form_assist_popup); - } - - /** - * Loads the initial tab at Fennec startup. If we don't restore tabs, this - * tab will be about:home, or the homepage if the user has set one. - * If we've temporarily disabled restoring to break out of a crash loop, we'll show - * the Recent Tabs folder of the Combined History panel, so the user can manually - * restore tabs as needed. - * If we restore tabs, we don't need to create a new tab. - */ - protected void loadStartupTab(final int flags) { - if (!mShouldRestore) { - if (mLastSessionCrashed) { - // The Recent Tabs panel no longer exists, but BrowserApp will redirect us - // to the Recent Tabs folder of the Combined History panel. - Tabs.getInstance().loadUrl(AboutPages.getURLForBuiltinPanelType(PanelType.DEPRECATED_RECENT_TABS), flags); - } else { - final String homepage = getHomepage(); - Tabs.getInstance().loadUrl(!TextUtils.isEmpty(homepage) ? homepage : AboutPages.HOME, flags); - } - } - } - - /** - * Loads the initial tab at Fennec startup. This tab will load with the given - * external URL. If that URL is invalid, a startup tab will be loaded. - * - * @param url External URL to load. - * @param intent External intent whose extras modify the request - * @param flags Flags used to load the load - */ - protected void loadStartupTab(final String url, final SafeIntent intent, final int flags) { - // Invalid url - if (url == null) { - loadStartupTab(flags); - return; - } - - Tabs.getInstance().loadUrlWithIntentExtras(url, intent, flags); - } - - public String getHomepage() { - return null; - } - - private String getIntentURI(SafeIntent intent) { - final String passedUri; - final String uri = getURIFromIntent(intent); - - if (!TextUtils.isEmpty(uri)) { - passedUri = uri; - } else { - passedUri = null; - } - return passedUri; - } - - private boolean invokedWithExternalURL(String uri) { - return uri != null && !AboutPages.isAboutHome(uri); - } - - private void initialize() { - mInitialized = true; - - final boolean isFirstTab = !mWasFirstTabShownAfterActivityUnhidden; - mWasFirstTabShownAfterActivityUnhidden = true; // Reset since we'll be loading a tab. - - final SafeIntent intent = new SafeIntent(getIntent()); - final String action = intent.getAction(); - - final String passedUri = getIntentURI(intent); - - final boolean isExternalURL = invokedWithExternalURL(passedUri); - - // Start migrating as early as possible, can do this in - // parallel with Gecko load. - checkMigrateProfile(); - - Tabs.registerOnTabsChangedListener(this); - - initializeChrome(); - - // We need to wait here because mShouldRestore can revert back to - // false if a parsing error occurs and the startup tab we load - // depends on whether we restore tabs or not. - synchronized (this) { - while (!mSessionRestoreParsingFinished) { - try { - wait(); - } catch (final InterruptedException e) { - // Ignore and wait again. - } - } - } - - // External URLs should always be loaded regardless of whether Gecko is - // already running. - if (isExternalURL) { - // Restore tabs before opening an external URL so that the new tab - // is animated properly. - Tabs.getInstance().notifyListeners(null, Tabs.TabEvents.RESTORED); - processActionViewIntent(new Runnable() { - @Override - public void run() { - int flags = Tabs.LOADURL_NEW_TAB | Tabs.LOADURL_USER_ENTERED | Tabs.LOADURL_EXTERNAL; - if (ACTION_HOMESCREEN_SHORTCUT.equals(action)) { - flags |= Tabs.LOADURL_PINNED; - } - if (isFirstTab) { - flags |= Tabs.LOADURL_FIRST_AFTER_ACTIVITY_UNHIDDEN; - } - loadStartupTab(passedUri, intent, flags); - } - }); - } else { - if (!mIsRestoringActivity) { - loadStartupTab(Tabs.LOADURL_NEW_TAB); - } - - Tabs.getInstance().notifyListeners(null, Tabs.TabEvents.RESTORED); - - processTabQueue(); - } - - recordStartupActionTelemetry(passedUri, action); - - // Check if launched from data reporting notification. - if (ACTION_LAUNCH_SETTINGS.equals(action)) { - Intent settingsIntent = new Intent(GeckoApp.this, GeckoPreferences.class); - // Copy extras. - settingsIntent.putExtras(intent.getUnsafe()); - startActivity(settingsIntent); - } - - //app state callbacks - mAppStateListeners = new LinkedList(); - - mPromptService = new PromptService(this); - - // Trigger the completion of the telemetry timer that wraps activity startup, - // then grab the duration to give to FHR. - mJavaUiStartupTimer.stop(); - final long javaDuration = mJavaUiStartupTimer.getElapsed(); - - ThreadUtils.getBackgroundHandler().postDelayed(new Runnable() { - @Override - public void run() { - final HealthRecorder rec = mHealthRecorder; - if (rec != null) { - rec.recordJavaStartupTime(javaDuration); - } - } - }, 50); - - final int updateServiceDelay = 30 * 1000; - ThreadUtils.getBackgroundHandler().postDelayed(new Runnable() { - @Override - public void run() { - UpdateServiceHelper.registerForUpdates(GeckoAppShell.getApplicationContext()); - } - }, updateServiceDelay); - - if (mIsRestoringActivity) { - Tab selectedTab = Tabs.getInstance().getSelectedTab(); - if (selectedTab != null) { - Tabs.getInstance().notifyListeners(selectedTab, Tabs.TabEvents.SELECTED); - } - - if (GeckoThread.isRunning()) { - geckoConnected(); - if (mLayerView != null) { - mLayerView.setPaintState(LayerView.PAINT_BEFORE_FIRST); - } - } - } - } - - @TargetApi(Build.VERSION_CODES.JELLY_BEAN) - @Override - public void onGlobalLayout() { - if (Versions.preJB) { - mMainLayout.getViewTreeObserver().removeGlobalOnLayoutListener(this); - } else { - mMainLayout.getViewTreeObserver().removeOnGlobalLayoutListener(this); - } - if (!mInitialized) { - initialize(); - } - } - - protected void processActionViewIntent(final Runnable openTabsRunnable) { - // We need to ensure that if we receive a VIEW action and there are tabs queued then the - // site loaded from the intent is on top (last loaded) and selected with all other tabs - // being opened behind it. We process the tab queue first and request a callback from the JS - the - // listener will open the url from the intent as normal when the tab queue has been processed. - ThreadUtils.postToBackgroundThread(new Runnable() { - @Override - public void run() { - if (TabQueueHelper.TAB_QUEUE_ENABLED && TabQueueHelper.shouldOpenTabQueueUrls(GeckoApp.this)) { - - getAppEventDispatcher().registerGeckoThreadListener(new NativeEventListener() { - @Override - public void handleMessage(String event, NativeJSObject message, EventCallback callback) { - if ("Tabs:TabsOpened".equals(event)) { - getAppEventDispatcher().unregisterGeckoThreadListener(this, "Tabs:TabsOpened"); - openTabsRunnable.run(); - } - } - }, "Tabs:TabsOpened"); - TabQueueHelper.openQueuedUrls(GeckoApp.this, getProfile(), TabQueueHelper.FILE_NAME, true); - } else { - openTabsRunnable.run(); - } - } - }); - } - - @WorkerThread - private String restoreSessionTabs(final boolean isExternalURL, boolean useBackup) throws SessionRestoreException { - try { - String sessionString = getProfile().readSessionFile(useBackup); - if (sessionString == null) { - throw new SessionRestoreException("Could not read from session file"); - } - - // If we are doing an OOM restore, parse the session data and - // stub the restored tabs immediately. This allows the UI to be - // updated before Gecko has restored. - final JSONArray tabs = new JSONArray(); - final JSONObject windowObject = new JSONObject(); - final boolean sessionDataValid; - - LastSessionParser parser = new LastSessionParser(tabs, windowObject, isExternalURL); - - if (mPrivateBrowsingSession == null) { - sessionDataValid = parser.parse(sessionString); - } else { - sessionDataValid = parser.parse(sessionString, mPrivateBrowsingSession); - } - - if (tabs.length() > 0) { - windowObject.put("tabs", tabs); - sessionString = new JSONObject().put("windows", new JSONArray().put(windowObject)).toString(); - } else { - if (parser.allTabsSkipped() || sessionDataValid) { - // If we intentionally skipped all tabs we've read from the session file, we - // set mShouldRestore back to false at this point already, so the calling code - // can infer that the exception wasn't due to a damaged session store file. - // The same applies if the session file was syntactically valid and - // simply didn't contain any tabs. - mShouldRestore = false; - } - throw new SessionRestoreException("No tabs could be read from session file"); - } - - JSONObject restoreData = new JSONObject(); - restoreData.put("sessionString", sessionString); - return restoreData.toString(); - } catch (JSONException e) { - throw new SessionRestoreException(e); - } - } - - public static EventDispatcher getEventDispatcher() { - final GeckoApp geckoApp = (GeckoApp) GeckoAppShell.getGeckoInterface(); - return geckoApp.getAppEventDispatcher(); - } - - @Override - public EventDispatcher getAppEventDispatcher() { - return eventDispatcher; - } - - @Override - public GeckoProfile getProfile() { - return GeckoThread.getActiveProfile(); - } - - /** - * Check whether we've crashed during the last browsing session. - * - * @return True if the crash reporter ran after the last session. - */ - protected boolean updateCrashedState() { - try { - File crashFlag = new File(GeckoProfileDirectories.getMozillaDirectory(this), "CRASHED"); - if (crashFlag.exists() && crashFlag.delete()) { - // Set the flag that indicates we were stopped as expected, as - // the crash reporter has run, so it is not a silent OOM crash. - getSharedPreferences().edit().putBoolean(PREFS_WAS_STOPPED, true).apply(); - return true; - } - } catch (NoMozillaDirectoryException e) { - // If we can't access the Mozilla directory, we're in trouble anyway. - Log.e(LOGTAG, "Cannot read crash flag: ", e); - } - return false; - } - - /** - * Determine whether the session should be restored. - * - * @param savedInstanceState Saved instance state given to the activity - * @return Whether to restore - */ - protected boolean getSessionRestoreState(Bundle savedInstanceState) { - final SharedPreferences prefs = getSharedPreferences(); - boolean shouldRestore = false; - - final int versionCode = getVersionCode(); - if (mLastSessionCrashed) { - if (incrementCrashCount(prefs) <= getSessionStoreMaxCrashResumes(prefs) && - getSessionRestoreAfterCrashPreference(prefs)) { - shouldRestore = true; - } else { - shouldRestore = false; - } - } else if (prefs.getInt(PREFS_VERSION_CODE, 0) != versionCode) { - // If the version has changed, the user has done an upgrade, so restore - // previous tabs. - prefs.edit().putInt(PREFS_VERSION_CODE, versionCode).apply(); - shouldRestore = true; - } else if (savedInstanceState != null || - getSessionRestorePreference(prefs).equals("always") || - getRestartFromIntent()) { - // We're coming back from a background kill by the OS, the user - // has chosen to always restore, or we restarted. - shouldRestore = true; - } - - return shouldRestore; - } - - private int incrementCrashCount(SharedPreferences prefs) { - final int crashCount = getSuccessiveCrashesCount(prefs) + 1; - prefs.edit().putInt(PREFS_CRASHED_COUNT, crashCount).apply(); - return crashCount; - } - - private int getSuccessiveCrashesCount(SharedPreferences prefs) { - return prefs.getInt(PREFS_CRASHED_COUNT, 0); - } - - private int getSessionStoreMaxCrashResumes(SharedPreferences prefs) { - return prefs.getInt(GeckoPreferences.PREFS_RESTORE_SESSION_MAX_CRASH_RESUMES, 1); - } - - private boolean getSessionRestoreAfterCrashPreference(SharedPreferences prefs) { - return prefs.getBoolean(GeckoPreferences.PREFS_RESTORE_SESSION_FROM_CRASH, true); - } - - private String getSessionRestorePreference(SharedPreferences prefs) { - return prefs.getString(GeckoPreferences.PREFS_RESTORE_SESSION, "always"); - } - - private boolean getRestartFromIntent() { - return IntentUtils.getBooleanExtraSafe(getIntent(), "didRestart", false); - } - - /** - * Enable Android StrictMode checks (for supported OS versions). - * http://developer.android.com/reference/android/os/StrictMode.html - */ - private void enableStrictMode() { - Log.d(LOGTAG, "Enabling Android StrictMode"); - - StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder() - .detectAll() - .penaltyLog() - .build()); - - StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder() - .detectAll() - .penaltyLog() - .build()); - } - - @Override - public void enableOrientationListener() { - // Start listening for orientation events - mCameraOrientationEventListener = new OrientationEventListener(this) { - @Override - public void onOrientationChanged(int orientation) { - if (mAppStateListeners != null) { - for (GeckoAppShell.AppStateListener listener: mAppStateListeners) { - listener.onOrientationChanged(); - } - } - } - }; - mCameraOrientationEventListener.enable(); - } - - @Override - public void disableOrientationListener() { - if (mCameraOrientationEventListener != null) { - mCameraOrientationEventListener.disable(); - mCameraOrientationEventListener = null; - } - } - - @Override - public String getDefaultUAString() { - return HardwareUtils.isTablet() ? AppConstants.USER_AGENT_FENNEC_TABLET : - AppConstants.USER_AGENT_FENNEC_MOBILE; - } - - @Override - public void createShortcut(final String title, final String url) { - Icons.with(this) - .pageUrl(url) - .skipNetwork() - .skipMemory() - .forLauncherIcon() - .build() - .execute(new IconCallback() { - @Override - public void onIconResponse(IconResponse response) { - doCreateShortcut(title, url, response.getBitmap()); - } - }); - } - - private void doCreateShortcut(final String aTitle, final String aURI, final Bitmap aIcon) { - // The intent to be launched by the shortcut. - Intent shortcutIntent = new Intent(); - shortcutIntent.setAction(GeckoApp.ACTION_HOMESCREEN_SHORTCUT); - shortcutIntent.setData(Uri.parse(aURI)); - shortcutIntent.setClassName(AppConstants.ANDROID_PACKAGE_NAME, - AppConstants.MOZ_ANDROID_BROWSER_INTENT_CLASS); - - Intent intent = new Intent(); - intent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcutIntent); - intent.putExtra(Intent.EXTRA_SHORTCUT_ICON, getLauncherIcon(aIcon, GeckoAppShell.getPreferredIconSize())); - - if (aTitle != null) { - intent.putExtra(Intent.EXTRA_SHORTCUT_NAME, aTitle); - } else { - intent.putExtra(Intent.EXTRA_SHORTCUT_NAME, aURI); - } - - // Do not allow duplicate items. - intent.putExtra("duplicate", false); - - intent.setAction("com.android.launcher.action.INSTALL_SHORTCUT"); - getApplicationContext().sendBroadcast(intent); - - // Remember interaction - final UrlAnnotations urlAnnotations = BrowserDB.from(getApplicationContext()).getUrlAnnotations(); - urlAnnotations.insertHomeScreenShortcut(getContentResolver(), aURI, true); - - // After shortcut is created, show the mobile desktop. - ActivityUtils.goToHomeScreen(this); - } - - private Bitmap getLauncherIcon(Bitmap aSource, int size) { - final float[] DEFAULT_LAUNCHER_ICON_HSV = { 32.0f, 1.0f, 1.0f }; - final int kOffset = 6; - final int kRadius = 5; - - int insetSize = aSource != null ? size * 2 / 3 : size; - - Bitmap bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888); - Canvas canvas = new Canvas(bitmap); - - // draw a base color - Paint paint = new Paint(); - if (aSource == null) { - // If we aren't drawing a favicon, just use an orange color. - paint.setColor(Color.HSVToColor(DEFAULT_LAUNCHER_ICON_HSV)); - canvas.drawRoundRect(new RectF(kOffset, kOffset, size - kOffset, size - kOffset), kRadius, kRadius, paint); - } else if (aSource.getWidth() >= insetSize || aSource.getHeight() >= insetSize) { - // Otherwise, if the icon is large enough, just draw it. - Rect iconBounds = new Rect(0, 0, size, size); - canvas.drawBitmap(aSource, null, iconBounds, null); - return bitmap; - } else { - // otherwise use the dominant color from the icon + a layer of transparent white to lighten it somewhat - int color = BitmapUtils.getDominantColor(aSource); - paint.setColor(color); - canvas.drawRoundRect(new RectF(kOffset, kOffset, size - kOffset, size - kOffset), kRadius, kRadius, paint); - paint.setColor(Color.argb(100, 255, 255, 255)); - canvas.drawRoundRect(new RectF(kOffset, kOffset, size - kOffset, size - kOffset), kRadius, kRadius, paint); - } - - // draw the overlay - Bitmap overlay = BitmapUtils.decodeResource(this, R.drawable.home_bg); - canvas.drawBitmap(overlay, null, new Rect(0, 0, size, size), null); - - // draw the favicon - if (aSource == null) - aSource = BitmapUtils.decodeResource(this, R.drawable.home_star); - - // by default, we scale the icon to this size - int sWidth = insetSize / 2; - int sHeight = sWidth; - - int halfSize = size / 2; - canvas.drawBitmap(aSource, - null, - new Rect(halfSize - sWidth, - halfSize - sHeight, - halfSize + sWidth, - halfSize + sHeight), - null); - - return bitmap; - } - - @Override - protected void onNewIntent(Intent externalIntent) { - final SafeIntent intent = new SafeIntent(externalIntent); - - final boolean isFirstTab = !mWasFirstTabShownAfterActivityUnhidden; - mWasFirstTabShownAfterActivityUnhidden = true; // Reset since we'll be loading a tab. - - // if we were previously OOM killed, we can end up here when launching - // from external shortcuts, so set this as the intent for initialization - if (!mInitialized) { - setIntent(externalIntent); - return; - } - - final String action = intent.getAction(); - - final String uri = getURIFromIntent(intent); - final String passedUri; - if (!TextUtils.isEmpty(uri)) { - passedUri = uri; - } else { - passedUri = null; - } - - if (ACTION_LOAD.equals(action)) { - Tabs.getInstance().loadUrl(intent.getDataString()); - lastSelectedTabId = -1; - } else if (Intent.ACTION_VIEW.equals(action)) { - processActionViewIntent(new Runnable() { - @Override - public void run() { - final String url = intent.getDataString(); - int flags = Tabs.LOADURL_NEW_TAB | Tabs.LOADURL_USER_ENTERED | Tabs.LOADURL_EXTERNAL; - if (isFirstTab) { - flags |= Tabs.LOADURL_FIRST_AFTER_ACTIVITY_UNHIDDEN; - } - Tabs.getInstance().loadUrlWithIntentExtras(url, intent, flags); - } - }); - lastSelectedTabId = -1; - } else if (ACTION_HOMESCREEN_SHORTCUT.equals(action)) { - mLayerView.loadUri(uri, GeckoView.LOAD_SWITCH_TAB); - } else if (Intent.ACTION_SEARCH.equals(action)) { - mLayerView.loadUri(uri, GeckoView.LOAD_NEW_TAB); - } else if (NotificationHelper.HELPER_BROADCAST_ACTION.equals(action)) { - NotificationHelper.getInstance(getApplicationContext()).handleNotificationIntent(intent); - } else if (ACTION_LAUNCH_SETTINGS.equals(action)) { - // Check if launched from data reporting notification. - Intent settingsIntent = new Intent(GeckoApp.this, GeckoPreferences.class); - // Copy extras. - settingsIntent.putExtras(intent.getUnsafe()); - startActivity(settingsIntent); - } else if (ACTION_SWITCH_TAB.equals(action)) { - final int tabId = intent.getIntExtra("TabId", -1); - Tabs.getInstance().selectTab(tabId); - lastSelectedTabId = -1; - } - - recordStartupActionTelemetry(passedUri, action); - } - - /** - * Handles getting a URI from an intent in a way that is backwards- - * compatible with our previous implementations. - */ - protected String getURIFromIntent(SafeIntent intent) { - final String action = intent.getAction(); - if (ACTION_ALERT_CALLBACK.equals(action) || - NotificationHelper.HELPER_BROADCAST_ACTION.equals(action)) { - return null; - } - - return intent.getDataString(); - } - - protected int getOrientation() { - return GeckoScreenOrientation.getInstance().getAndroidOrientation(); - } - - @Override - public void onResume() - { - // After an onPause, the activity is back in the foreground. - // Undo whatever we did in onPause. - super.onResume(); - if (mIsAbortingAppLaunch) { - return; - } - - GeckoAppShell.setGeckoInterface(this); - - if (lastSelectedTabId >= 0 && (lastActiveGeckoApp == null || lastActiveGeckoApp.get() != this)) { - Tabs.getInstance().selectTab(lastSelectedTabId); - } - - int newOrientation = getResources().getConfiguration().orientation; - if (GeckoScreenOrientation.getInstance().update(newOrientation)) { - refreshChrome(); - } - - if (mAppStateListeners != null) { - for (GeckoAppShell.AppStateListener listener : mAppStateListeners) { - listener.onResume(); - } - } - - // We use two times: a pseudo-unique wall-clock time to identify the - // current session across power cycles, and the elapsed realtime to - // track the duration of the session. - final long now = System.currentTimeMillis(); - final long realTime = android.os.SystemClock.elapsedRealtime(); - - ThreadUtils.postToBackgroundThread(new Runnable() { - @Override - public void run() { - // Now construct the new session on HealthRecorder's behalf. We do this here - // so it can benefit from a single near-startup prefs commit. - SessionInformation currentSession = new SessionInformation(now, realTime); - - SharedPreferences prefs = GeckoApp.this.getSharedPreferences(); - SharedPreferences.Editor editor = prefs.edit(); - editor.putBoolean(GeckoApp.PREFS_WAS_STOPPED, false); - - if (!mLastSessionCrashed) { - // The last session terminated normally, - // so we can reset the count of successive crashes. - editor.putInt(GeckoApp.PREFS_CRASHED_COUNT, 0); - } - - currentSession.recordBegin(editor); - editor.apply(); - - final HealthRecorder rec = mHealthRecorder; - if (rec != null) { - rec.setCurrentSession(currentSession); - rec.processDelayed(); - } else { - Log.w(LOGTAG, "Can't record session: rec is null."); - } - } - }); - - Restrictions.update(this); - } - - @Override - public void onWindowFocusChanged(boolean hasFocus) { - super.onWindowFocusChanged(hasFocus); - - if (!mWindowFocusInitialized && hasFocus) { - mWindowFocusInitialized = true; - // XXX our editor tests require the GeckoView to have focus to pass, so we have to - // manually shift focus to the GeckoView. requestFocus apparently doesn't work at - // this stage of starting up, so we have to unset and reset the focusability. - mLayerView.setFocusable(false); - mLayerView.setFocusable(true); - mLayerView.setFocusableInTouchMode(true); - getWindow().setBackgroundDrawable(null); - } - } - - @Override - public void onPause() - { - if (mIsAbortingAppLaunch) { - super.onPause(); - return; - } - - final Tab selectedTab = Tabs.getInstance().getSelectedTab(); - if (selectedTab != null) { - lastSelectedTabId = selectedTab.getId(); - } - lastActiveGeckoApp = new WeakReference(this); - - final HealthRecorder rec = mHealthRecorder; - final Context context = this; - - // In some way it's sad that Android will trigger StrictMode warnings - // here as the whole point is to save to disk while the activity is not - // interacting with the user. - ThreadUtils.postToBackgroundThread(new Runnable() { - @Override - public void run() { - SharedPreferences prefs = GeckoApp.this.getSharedPreferences(); - SharedPreferences.Editor editor = prefs.edit(); - editor.putBoolean(GeckoApp.PREFS_WAS_STOPPED, true); - if (rec != null) { - rec.recordSessionEnd("P", editor); - } - - // onPause might in fact be called even after a crash, but in that case the - // crash reporter will record this fact for us and we'll pick it up in onCreate. - mLastSessionCrashed = false; - - // If we haven't done it before, cleanup any old files in our old temp dir - if (prefs.getBoolean(GeckoApp.PREFS_CLEANUP_TEMP_FILES, true)) { - File tempDir = GeckoLoader.getGREDir(GeckoApp.this); - FileUtils.delTree(tempDir, new FileUtils.NameAndAgeFilter(null, ONE_DAY_MS), false); - - editor.putBoolean(GeckoApp.PREFS_CLEANUP_TEMP_FILES, false); - } - - editor.apply(); - } - }); - - if (mAppStateListeners != null) { - for (GeckoAppShell.AppStateListener listener : mAppStateListeners) { - listener.onPause(); - } - } - - super.onPause(); - } - - @Override - public void onRestart() { - if (mIsAbortingAppLaunch) { - super.onRestart(); - return; - } - - // Faster on main thread with an async apply(). - final StrictMode.ThreadPolicy savedPolicy = StrictMode.allowThreadDiskReads(); - try { - SharedPreferences.Editor editor = GeckoApp.this.getSharedPreferences().edit(); - editor.putBoolean(GeckoApp.PREFS_WAS_STOPPED, false); - editor.apply(); - } finally { - StrictMode.setThreadPolicy(savedPolicy); - } - - super.onRestart(); - } - - @Override - public void onDestroy() { - if (mIsAbortingAppLaunch) { - // This build does not support the Android version of the device: - // We did not initialize anything, so skip cleaning up. - super.onDestroy(); - return; - } - - getAppEventDispatcher().unregisterGeckoThreadListener((GeckoEventListener)this, - "Gecko:Ready", - "Gecko:Exited", - "Accessibility:Event"); - - getAppEventDispatcher().unregisterGeckoThreadListener((NativeEventListener)this, - "Accessibility:Ready", - "Bookmark:Insert", - "Contact:Add", - "DevToolsAuth:Scan", - "DOMFullScreen:Start", - "DOMFullScreen:Stop", - "Image:SetAs", - "Locale:Set", - "Permissions:Data", - "PrivateBrowsing:Data", - "RuntimePermissions:Prompt", - "Sanitize:Finished", - "Session:StatePurged", - "Share:Text", - "Snackbar:Show", - "SystemUI:Visibility", - "ToggleChrome:Focus", - "ToggleChrome:Hide", - "ToggleChrome:Show", - "Update:Check", - "Update:Download", - "Update:Install"); - - if (mPromptService != null) - mPromptService.destroy(); - - final HealthRecorder rec = mHealthRecorder; - mHealthRecorder = null; - if (rec != null && rec.isEnabled()) { - // Closing a HealthRecorder could incur a write. - ThreadUtils.postToBackgroundThread(new Runnable() { - @Override - public void run() { - rec.close(GeckoApp.this); - } - }); - } - - super.onDestroy(); - - Tabs.unregisterOnTabsChangedListener(this); - } - - public void showSDKVersionError() { - final String message = getString(R.string.unsupported_sdk_version, Build.CPU_ABI, Build.VERSION.SDK_INT); - Toast.makeText(this, message, Toast.LENGTH_LONG).show(); - } - - // Get a temporary directory, may return null - public static File getTempDirectory() { - File dir = GeckoApplication.get().getExternalFilesDir("temp"); - return dir; - } - - // Delete any files in our temporary directory - public static void deleteTempFiles() { - File dir = getTempDirectory(); - if (dir == null) - return; - File[] files = dir.listFiles(); - if (files == null) - return; - for (File file : files) { - file.delete(); - } - } - - @Override - public void onConfigurationChanged(Configuration newConfig) { - Log.d(LOGTAG, "onConfigurationChanged: " + newConfig.locale); - - final LocaleManager localeManager = BrowserLocaleManager.getInstance(); - final Locale changed = localeManager.onSystemConfigurationChanged(this, getResources(), newConfig, mLastLocale); - if (changed != null) { - onLocaleChanged(Locales.getLanguageTag(changed)); - } - - // onConfigurationChanged is not called for 180 degree orientation changes, - // we will miss such rotations and the screen orientation will not be - // updated. - if (GeckoScreenOrientation.getInstance().update(newConfig.orientation)) { - if (mFormAssistPopup != null) - mFormAssistPopup.hide(); - refreshChrome(); - } - super.onConfigurationChanged(newConfig); - } - - public String getContentProcessName() { - return AppConstants.MOZ_CHILD_PROCESS_NAME; - } - - public void addEnvToIntent(Intent intent) { - Map envMap = System.getenv(); - Set> envSet = envMap.entrySet(); - Iterator> envIter = envSet.iterator(); - int c = 0; - while (envIter.hasNext()) { - Map.Entry entry = envIter.next(); - intent.putExtra("env" + c, entry.getKey() + "=" - + entry.getValue()); - c++; - } - } - - @Override - public void doRestart() { - doRestart(null, null); - } - - public void doRestart(String args) { - doRestart(args, null); - } - - public void doRestart(Intent intent) { - doRestart(null, intent); - } - - public void doRestart(String args, Intent restartIntent) { - if (restartIntent == null) { - restartIntent = new Intent(Intent.ACTION_MAIN); - } - - if (args != null) { - restartIntent.putExtra("args", args); - } - - mRestartIntent = restartIntent; - Log.d(LOGTAG, "doRestart(\"" + restartIntent + "\")"); - - doShutdown(); - } - - private void doShutdown() { - // Shut down GeckoApp activity. - runOnUiThread(new Runnable() { - @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) - @Override public void run() { - if (!isFinishing() && (Versions.preJBMR1 || !isDestroyed())) { - finish(); - } - } - }); - } - - private void checkMigrateProfile() { - final File profileDir = getProfile().getDir(); - - if (profileDir != null) { - ThreadUtils.postToBackgroundThread(new Runnable() { - @Override - public void run() { - Handler handler = new Handler(); - handler.postDelayed(new DeferredCleanupTask(), CLEANUP_DEFERRAL_SECONDS * 1000); - } - }); - } - } - - private static class DeferredCleanupTask implements Runnable { - // The cleanup-version setting is recorded to avoid repeating the same - // tasks on subsequent startups; CURRENT_CLEANUP_VERSION may be updated - // if we need to do additional cleanup for future Gecko versions. - - private static final String CLEANUP_VERSION = "cleanup-version"; - private static final int CURRENT_CLEANUP_VERSION = 1; - - @Override - public void run() { - final Context context = GeckoAppShell.getApplicationContext(); - long cleanupVersion = GeckoSharedPrefs.forApp(context).getInt(CLEANUP_VERSION, 0); - - if (cleanupVersion < 1) { - // Reduce device storage footprint by removing .ttf files from - // the res/fonts directory: we no longer need to copy our - // bundled fonts out of the APK in order to use them. - // See https://bugzilla.mozilla.org/show_bug.cgi?id=878674. - File dir = new File("res/fonts"); - if (dir.exists() && dir.isDirectory()) { - for (File file : dir.listFiles()) { - if (file.isFile() && file.getName().endsWith(".ttf")) { - file.delete(); - } - } - if (!dir.delete()) { - Log.w(LOGTAG, "unable to delete res/fonts directory (not empty?)"); - } - } - } - - // Additional cleanup needed for future versions would go here - - if (cleanupVersion != CURRENT_CLEANUP_VERSION) { - SharedPreferences.Editor editor = GeckoSharedPrefs.forApp(context).edit(); - editor.putInt(CLEANUP_VERSION, CURRENT_CLEANUP_VERSION); - editor.apply(); - } - } - } - - protected void onDone() { - moveTaskToBack(true); - } - - @Override - public void onBackPressed() { - if (getSupportFragmentManager().getBackStackEntryCount() > 0) { - super.onBackPressed(); - return; - } - - if (autoHideTabs()) { - return; - } - - if (mDoorHangerPopup != null && mDoorHangerPopup.isShowing()) { - mDoorHangerPopup.dismiss(); - return; - } - - if (mFullScreenPluginView != null) { - GeckoAppShell.onFullScreenPluginHidden(mFullScreenPluginView); - removeFullScreenPluginView(mFullScreenPluginView); - return; - } - - if (mLayerView != null && mLayerView.isFullScreen()) { - GeckoAppShell.notifyObservers("FullScreen:Exit", null); - return; - } - - final Tabs tabs = Tabs.getInstance(); - final Tab tab = tabs.getSelectedTab(); - if (tab == null) { - onDone(); - return; - } - - // Give Gecko a chance to handle the back press first, then fallback to the Java UI. - GeckoAppShell.sendRequestToGecko(new GeckoRequest("Browser:OnBackPressed", null) { - @Override - public void onResponse(NativeJSObject nativeJSObject) { - if (!nativeJSObject.getBoolean("handled")) { - // Default behavior is Gecko didn't prevent. - onDefault(); - } - } - - @Override - public void onError(NativeJSObject error) { - // Default behavior is Gecko didn't prevent, via failure. - onDefault(); - } - - // Return from Gecko thread, then back-press through the Java UI. - private void onDefault() { - ThreadUtils.postToUiThread(new Runnable() { - @Override - public void run() { - if (tab.doBack()) { - return; - } - - if (tab.isExternal()) { - onDone(); - Tab nextSelectedTab = Tabs.getInstance().getNextTab(tab); - if (nextSelectedTab != null) { - int nextSelectedTabId = nextSelectedTab.getId(); - GeckoAppShell.notifyObservers("Tab:KeepZombified", Integer.toString(nextSelectedTabId)); - } - tabs.closeTab(tab); - return; - } - - final int parentId = tab.getParentId(); - final Tab parent = tabs.getTab(parentId); - if (parent != null) { - // The back button should always return to the parent (not a sibling). - tabs.closeTab(tab, parent); - return; - } - - onDone(); - } - }); - } - }); - } - - @Override - protected void onActivityResult(int requestCode, int resultCode, Intent data) { - if (!ActivityHandlerHelper.handleActivityResult(requestCode, resultCode, data)) { - super.onActivityResult(requestCode, resultCode, data); - } - } - - @Override - public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { - Permissions.onRequestPermissionsResult(this, permissions, grantResults); - } - - @Override - public AbsoluteLayout getPluginContainer() { return mPluginContainer; } - - private static final String CPU = "cpu"; - private static final String SCREEN = "screen"; - - // Called when a Gecko Hal WakeLock is changed - @Override - // We keep the wake lock independent from the function scope, so we need to - // suppress the linter warning. - @SuppressLint("Wakelock") - public void notifyWakeLockChanged(String topic, String state) { - PowerManager.WakeLock wl = mWakeLocks.get(topic); - if (state.equals("locked-foreground") && wl == null) { - PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); - - if (CPU.equals(topic)) { - wl = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, topic); - } else if (SCREEN.equals(topic)) { - // ON_AFTER_RELEASE is set, the user activity timer will be reset when the - // WakeLock is released, causing the illumination to remain on a bit longer. - wl = pm.newWakeLock(PowerManager.SCREEN_BRIGHT_WAKE_LOCK | PowerManager.ON_AFTER_RELEASE, topic); - } - - if (wl != null) { - wl.acquire(); - mWakeLocks.put(topic, wl); - } - } else if (!state.equals("locked-foreground") && wl != null) { - wl.release(); - mWakeLocks.remove(topic); - } - } - - @Override - public void notifyCheckUpdateResult(String result) { - GeckoAppShell.notifyObservers("Update:CheckResult", result); - } - - private void geckoConnected() { - mLayerView.setOverScrollMode(View.OVER_SCROLL_NEVER); - } - - @Override - public void setAccessibilityEnabled(boolean enabled) { - } - - @Override - public boolean openUriExternal(String targetURI, String mimeType, String packageName, String className, String action, String title) { - // Default to showing prompt in private browsing to be safe. - return IntentHelper.openUriExternal(targetURI, mimeType, packageName, className, action, title, true); - } - - public static class MainLayout extends RelativeLayout { - private TouchEventInterceptor mTouchEventInterceptor; - private MotionEventInterceptor mMotionEventInterceptor; - - public MainLayout(Context context, AttributeSet attrs) { - super(context, attrs); - } - - @Override - protected void onLayout(boolean changed, int left, int top, int right, int bottom) { - super.onLayout(changed, left, top, right, bottom); - } - - public void setTouchEventInterceptor(TouchEventInterceptor interceptor) { - mTouchEventInterceptor = interceptor; - } - - public void setMotionEventInterceptor(MotionEventInterceptor interceptor) { - mMotionEventInterceptor = interceptor; - } - - @Override - public boolean onInterceptTouchEvent(MotionEvent event) { - if (mTouchEventInterceptor != null && mTouchEventInterceptor.onInterceptTouchEvent(this, event)) { - return true; - } - return super.onInterceptTouchEvent(event); - } - - @Override - public boolean onTouchEvent(MotionEvent event) { - if (mTouchEventInterceptor != null && mTouchEventInterceptor.onTouch(this, event)) { - return true; - } - return super.onTouchEvent(event); - } - - @Override - public boolean onGenericMotionEvent(MotionEvent event) { - if (mMotionEventInterceptor != null && mMotionEventInterceptor.onInterceptMotionEvent(this, event)) { - return true; - } - return super.onGenericMotionEvent(event); - } - - @Override - public void setDrawingCacheEnabled(boolean enabled) { - // Instead of setting drawing cache in the view itself, we simply - // enable drawing caching on its children. This is mainly used in - // animations (see PropertyAnimator) - super.setChildrenDrawnWithCacheEnabled(enabled); - } - } - - private class FullScreenHolder extends FrameLayout { - - public FullScreenHolder(Context ctx) { - super(ctx); - setBackgroundColor(0xff000000); - } - - @Override - public void addView(View view, int index) { - /** - * This normally gets called when Flash adds a separate SurfaceView - * for the video. It is unhappy if we have the LayerView underneath - * it for some reason so we need to hide that. Hiding the LayerView causes - * its surface to be destroyed, which causes a pause composition - * event to be sent to Gecko. We synchronously wait for that to be - * processed. Simultaneously, however, Flash is waiting on a mutex so - * the post() below is an attempt to avoid a deadlock. - */ - super.addView(view, index); - - ThreadUtils.postToUiThread(new Runnable() { - @Override - public void run() { - mLayerView.hideSurface(); - } - }); - } - - /** - * The methods below are simply copied from what Android WebKit does. - * It wasn't ever called in my testing, but might as well - * keep it in case it is for some reason. The methods - * all return true because we don't want any events - * leaking out from the fullscreen view. - */ - @Override - public boolean onKeyDown(int keyCode, KeyEvent event) { - if (event.isSystem()) { - return super.onKeyDown(keyCode, event); - } - mFullScreenPluginView.onKeyDown(keyCode, event); - return true; - } - - @Override - public boolean onKeyUp(int keyCode, KeyEvent event) { - if (event.isSystem()) { - return super.onKeyUp(keyCode, event); - } - mFullScreenPluginView.onKeyUp(keyCode, event); - return true; - } - - @Override - public boolean onTouchEvent(MotionEvent event) { - return true; - } - - @Override - public boolean onTrackballEvent(MotionEvent event) { - mFullScreenPluginView.onTrackballEvent(event); - return true; - } - } - - private int getVersionCode() { - int versionCode = 0; - try { - versionCode = getPackageManager().getPackageInfo(getPackageName(), 0).versionCode; - } catch (NameNotFoundException e) { - Log.wtf(LOGTAG, getPackageName() + " not found", e); - } - return versionCode; - } - - // FHR reason code for a session end prior to a restart for a - // locale change. - private static final String SESSION_END_LOCALE_CHANGED = "L"; - - /** - * This exists so that a locale can be applied in two places: when saved - * in a nested activity, and then again when we get back up to GeckoApp. - * - * GeckoApp needs to do a bunch more stuff than, say, GeckoPreferences. - */ - protected void onLocaleChanged(final String locale) { - final boolean startNewSession = true; - final boolean shouldRestart = false; - - // If the HealthRecorder is not yet initialized (unlikely), the locale change won't - // trigger a session transition and subsequent events will be recorded in an environment - // with the wrong locale. - final HealthRecorder rec = mHealthRecorder; - if (rec != null) { - rec.onAppLocaleChanged(locale); - rec.onEnvironmentChanged(startNewSession, SESSION_END_LOCALE_CHANGED); - } - - if (!shouldRestart) { - ThreadUtils.postToUiThread(new Runnable() { - @Override - public void run() { - GeckoApp.this.onLocaleReady(locale); - } - }); - return; - } - - // Do this in the background so that the health recorder has its - // time to finish. - ThreadUtils.postToBackgroundThread(new Runnable() { - @Override - public void run() { - GeckoApp.this.doRestart(); - } - }); - } - - /** - * Use BrowserLocaleManager to change our persisted and current locales, - * and poke the system to tell it of our changed state. - */ - protected void setLocale(final String locale) { - if (locale == null) { - return; - } - - final String resultant = BrowserLocaleManager.getInstance().setSelectedLocale(this, locale); - if (resultant == null) { - return; - } - - onLocaleChanged(resultant); - } - - private void setSystemUiVisible(final boolean visible) { - ThreadUtils.postToUiThread(new Runnable() { - @Override - public void run() { - if (visible) { - mMainLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_VISIBLE); - } else { - mMainLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LOW_PROFILE); - } - } - }); - } - - protected HealthRecorder createHealthRecorder(final Context context, - final String profilePath, - final EventDispatcher dispatcher, - final String osLocale, - final String appLocale, - final SessionInformation previousSession) { - // GeckoApp does not need to record any health information - return a stub. - return new StubbedHealthRecorder(); - } - - protected void recordStartupActionTelemetry(final String passedURL, final String action) { - } - - @Override - public void checkUriVisited(String uri) { - GlobalHistory.getInstance().checkUriVisited(uri); - } - - @Override - public void markUriVisited(final String uri) { - final Context context = getApplicationContext(); - final BrowserDB db = BrowserDB.from(context); - ThreadUtils.postToBackgroundThread(new Runnable() { - @Override - public void run() { - GlobalHistory.getInstance().add(context, db, uri); - } - }); - } - - @Override - public void setUriTitle(final String uri, final String title) { - final Context context = getApplicationContext(); - final BrowserDB db = BrowserDB.from(context); - ThreadUtils.postToBackgroundThread(new Runnable() { - @Override - public void run() { - GlobalHistory.getInstance().update(context.getContentResolver(), db, uri, title); - } - }); - } - - @Override - public String[] getHandlersForMimeType(String mimeType, String action) { - Intent intent = IntentHelper.getIntentForActionString(action); - if (mimeType != null && mimeType.length() > 0) - intent.setType(mimeType); - return IntentHelper.getHandlersForIntent(intent); - } - - @Override - public String[] getHandlersForURL(String url, String action) { - // May contain the whole URL or just the protocol. - Uri uri = url.indexOf(':') >= 0 ? Uri.parse(url) : new Uri.Builder().scheme(url).build(); - - Intent intent = IntentHelper.getOpenURIIntent(getApplicationContext(), uri.toString(), "", - TextUtils.isEmpty(action) ? Intent.ACTION_VIEW : action, ""); - - return IntentHelper.getHandlersForIntent(intent); - } - - @Override - public String getDefaultChromeURI() { - // Use the chrome URI specified by Gecko's defaultChromeURI pref. - return null; - } - - public GeckoView getGeckoView() { - return mLayerView; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/GeckoApplication.java b/mobile/android/base/java/org/mozilla/gecko/GeckoApplication.java deleted file mode 100644 index 18a6e6535c..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/GeckoApplication.java +++ /dev/null @@ -1,314 +0,0 @@ -/* 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; - -import android.app.Application; -import android.content.ContentResolver; -import android.content.Context; -import android.content.SharedPreferences; -import android.content.res.Configuration; -import android.os.Bundle; -import android.os.SystemClock; -import android.util.Log; - -import com.squareup.leakcanary.LeakCanary; -import com.squareup.leakcanary.RefWatcher; - -import org.mozilla.gecko.db.BrowserContract; -import org.mozilla.gecko.db.BrowserDB; -import org.mozilla.gecko.db.LocalBrowserDB; -import org.mozilla.gecko.distribution.Distribution; -import org.mozilla.gecko.dlc.DownloadContentService; -import org.mozilla.gecko.home.HomePanelsManager; -import org.mozilla.gecko.lwt.LightweightTheme; -import org.mozilla.gecko.mdns.MulticastDNSManager; -import org.mozilla.gecko.media.AudioFocusAgent; -import org.mozilla.gecko.notifications.NotificationClient; -import org.mozilla.gecko.notifications.NotificationHelper; -import org.mozilla.gecko.preferences.DistroSharedPrefsImport; -import org.mozilla.gecko.util.BundleEventListener; -import org.mozilla.gecko.util.Clipboard; -import org.mozilla.gecko.util.EventCallback; -import org.mozilla.gecko.util.HardwareUtils; -import org.mozilla.gecko.util.ThreadUtils; - -import java.io.File; -import java.lang.reflect.Method; - -public class GeckoApplication extends Application - implements ContextGetter { - private static final String LOG_TAG = "GeckoApplication"; - - private static volatile GeckoApplication instance; - - private boolean mInBackground; - private boolean mPausedGecko; - - private LightweightTheme mLightweightTheme; - - private RefWatcher mRefWatcher; - - public GeckoApplication() { - super(); - instance = this; - } - - public static GeckoApplication get() { - return instance; - } - - public static RefWatcher getRefWatcher(Context context) { - GeckoApplication app = (GeckoApplication) context.getApplicationContext(); - return app.mRefWatcher; - } - - public static void watchReference(Context context, Object object) { - if (context == null) { - return; - } - - getRefWatcher(context).watch(object); - } - - @Override - public Context getContext() { - return this; - } - - @Override - public SharedPreferences getSharedPreferences() { - return GeckoSharedPrefs.forApp(this); - } - - /** - * We need to do locale work here, because we need to intercept - * each hit to onConfigurationChanged. - */ - @Override - public void onConfigurationChanged(Configuration config) { - Log.d(LOG_TAG, "onConfigurationChanged: " + config.locale + - ", background: " + mInBackground); - - // Do nothing if we're in the background. It'll simply cause a loop - // (Bug 936756 Comment 11), and it's not necessary. - if (mInBackground) { - super.onConfigurationChanged(config); - return; - } - - // Otherwise, correct the locale. This catches some cases that GeckoApp - // doesn't get a chance to. - try { - BrowserLocaleManager.getInstance().correctLocale(this, getResources(), config); - } catch (IllegalStateException ex) { - // GeckoApp hasn't started, so we have no ContextGetter in BrowserLocaleManager. - Log.w(LOG_TAG, "Couldn't correct locale.", ex); - } - - super.onConfigurationChanged(config); - } - - public void onActivityPause(GeckoActivityStatus activity) { - mInBackground = true; - - if ((activity.isFinishing() == false) && - (activity.isGeckoActivityOpened() == false)) { - // Notify Gecko that we are pausing; the cache service will be - // shutdown, closing the disk cache cleanly. If the android - // low memory killer subsequently kills us, the disk cache will - // be left in a consistent state, avoiding costly cleanup and - // re-creation. - GeckoThread.onPause(); - mPausedGecko = true; - - final BrowserDB db = BrowserDB.from(this); - ThreadUtils.postToBackgroundThread(new Runnable() { - @Override - public void run() { - db.expireHistory(getContentResolver(), BrowserContract.ExpirePriority.NORMAL); - } - }); - } - GeckoNetworkManager.getInstance().stop(); - } - - public void onActivityResume(GeckoActivityStatus activity) { - if (mPausedGecko) { - GeckoThread.onResume(); - mPausedGecko = false; - } - - GeckoBatteryManager.getInstance().start(this); - GeckoNetworkManager.getInstance().start(this); - - mInBackground = false; - } - - @Override - protected void attachBaseContext(Context base) { - super.attachBaseContext(base); - AppConstants.maybeInstallMultiDex(base); - } - - @Override - public void onCreate() { - Log.i(LOG_TAG, "zerdatime " + SystemClock.uptimeMillis() + " - Fennec application start"); - - mRefWatcher = LeakCanary.install(this); - - final Context context = getApplicationContext(); - GeckoAppShell.setApplicationContext(context); - HardwareUtils.init(context); - Clipboard.init(context); - FilePicker.init(context); - DownloadsIntegration.init(); - HomePanelsManager.getInstance().init(context); - - GlobalPageMetadata.getInstance().init(); - - // We need to set the notification client before launching Gecko, since Gecko could start - // sending notifications immediately after startup, which we don't want to lose/crash on. - GeckoAppShell.setNotificationListener(new NotificationClient(context)); - // This getInstance call will force initialization of the NotificationHelper, but does nothing with the result - NotificationHelper.getInstance(context).init(); - - MulticastDNSManager.getInstance(context).init(); - - GeckoService.register(); - - EventDispatcher.getInstance().registerBackgroundThreadListener(new EventListener(), - "Profile:Create"); - - super.onCreate(); - } - - public void onDelayedStartup() { - if (AppConstants.MOZ_ANDROID_GCM) { - // TODO: only run in main process. - ThreadUtils.postToBackgroundThread(new Runnable() { - @Override - public void run() { - // It's fine to throw GCM initialization onto a background thread; the registration process requires - // network access, so is naturally asynchronous. This, of course, races against Gecko page load of - // content requiring GCM-backed services, like Web Push. There's nothing to be done here. - try { - final Class clazz = Class.forName("org.mozilla.gecko.push.PushService"); - final Method onCreate = clazz.getMethod("onCreate", Context.class); - onCreate.invoke(null, getApplicationContext()); // Method is static. - } catch (Exception e) { - Log.e(LOG_TAG, "Got exception during startup; ignoring.", e); - return; - } - } - }); - } - - if (AppConstants.MOZ_ANDROID_DOWNLOAD_CONTENT_SERVICE) { - DownloadContentService.startStudy(this); - } - - GeckoAccessibility.setAccessibilityManagerListeners(this); - - AudioFocusAgent.getInstance().attachToContext(this); - } - - private class EventListener implements BundleEventListener - { - private void onProfileCreate(final String name, final String path) { - // Add everything when we're done loading the distribution. - final Context context = GeckoApplication.this; - final GeckoProfile profile = GeckoProfile.get(context, name); - final Distribution distribution = Distribution.getInstance(context); - - distribution.addOnDistributionReadyCallback(new Distribution.ReadyCallback() { - @Override - public void distributionNotFound() { - this.distributionFound(null); - } - - @Override - public void distributionFound(final Distribution distribution) { - Log.d(LOG_TAG, "Running post-distribution task: bookmarks."); - // Because we are running in the background, we want to synchronize on the - // GeckoProfile instance so that we don't race with main thread operations - // such as locking/unlocking/removing the profile. - synchronized (profile.getLock()) { - distributionFoundLocked(distribution); - } - } - - @Override - public void distributionArrivedLate(final Distribution distribution) { - Log.d(LOG_TAG, "Running late distribution task: bookmarks."); - // Recover as best we can. - synchronized (profile.getLock()) { - distributionArrivedLateLocked(distribution); - } - } - - private void distributionFoundLocked(final Distribution distribution) { - // Skip initialization if the profile directory has been removed. - if (!(new File(path)).exists()) { - return; - } - - final ContentResolver cr = context.getContentResolver(); - final LocalBrowserDB db = new LocalBrowserDB(profile.getName()); - - // We pass the number of added bookmarks to ensure that the - // indices of the distribution and default bookmarks are - // contiguous. Because there are always at least as many - // bookmarks as there are favicons, we can also guarantee that - // the favicon IDs won't overlap. - final int offset = distribution == null ? 0 : - db.addDistributionBookmarks(cr, distribution, 0); - db.addDefaultBookmarks(context, cr, offset); - - Log.d(LOG_TAG, "Running post-distribution task: android preferences."); - DistroSharedPrefsImport.importPreferences(context, distribution); - } - - private void distributionArrivedLateLocked(final Distribution distribution) { - // Skip initialization if the profile directory has been removed. - if (!(new File(path)).exists()) { - return; - } - - final ContentResolver cr = context.getContentResolver(); - final LocalBrowserDB db = new LocalBrowserDB(profile.getName()); - - // We assume we've been called very soon after startup, and so our offset - // into "Mobile Bookmarks" is the number of bookmarks in the DB. - final int offset = db.getCount(cr, "bookmarks"); - db.addDistributionBookmarks(cr, distribution, offset); - - Log.d(LOG_TAG, "Running late distribution task: android preferences."); - DistroSharedPrefsImport.importPreferences(context, distribution); - } - }); - } - - @Override // BundleEventListener - public void handleMessage(final String event, final Bundle message, - final EventCallback callback) { - if ("Profile:Create".equals(event)) { - onProfileCreate(message.getCharSequence("name").toString(), - message.getCharSequence("path").toString()); - } - } - } - - public boolean isApplicationInBackground() { - return mInBackground; - } - - public LightweightTheme getLightweightTheme() { - return mLightweightTheme; - } - - public void prepareLightweightTheme() { - mLightweightTheme = new LightweightTheme(this); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/GeckoJavaSampler.java b/mobile/android/base/java/org/mozilla/gecko/GeckoJavaSampler.java deleted file mode 100644 index 319eccec11..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/GeckoJavaSampler.java +++ /dev/null @@ -1,211 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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; - -import android.os.SystemClock; -import android.util.Log; -import android.util.SparseArray; - -import org.mozilla.gecko.annotation.WrapForJNI; - -import java.lang.Thread; -import java.util.Set; - -public class GeckoJavaSampler { - private static final String LOGTAG = "JavaSampler"; - private static Thread sSamplingThread; - private static SamplingThread sSamplingRunnable; - private static Thread sMainThread; - - // Use the same timer primitive as the profiler - // to get a perfect sample syncing. - @WrapForJNI - private static native double getProfilerTime(); - - private static class Sample { - public Frame[] mFrames; - public double mTime; - public long mJavaTime; // non-zero if Android system time is used - public Sample(StackTraceElement[] aStack) { - mFrames = new Frame[aStack.length]; - if (GeckoThread.isStateAtLeast(GeckoThread.State.LIBS_READY)) { - mTime = getProfilerTime(); - } - if (mTime == 0.0d) { - // getProfilerTime is not available yet; either libs are not loaded, - // or profiling hasn't started on the Gecko side yet - mJavaTime = SystemClock.elapsedRealtime(); - } - for (int i = 0; i < aStack.length; i++) { - mFrames[aStack.length - 1 - i] = new Frame(); - mFrames[aStack.length - 1 - i].fileName = aStack[i].getFileName(); - mFrames[aStack.length - 1 - i].lineNo = aStack[i].getLineNumber(); - mFrames[aStack.length - 1 - i].methodName = aStack[i].getMethodName(); - mFrames[aStack.length - 1 - i].className = aStack[i].getClassName(); - } - } - } - private static class Frame { - public String fileName; - public int lineNo; - public String methodName; - public String className; - } - - private static class SamplingThread implements Runnable { - private final int mInterval; - private final int mSampleCount; - - private boolean mPauseSampler; - private boolean mStopSampler; - - private final SparseArray mSamples = new SparseArray(); - private int mSamplePos; - - public SamplingThread(final int aInterval, final int aSampleCount) { - // If we sample faster then 10ms we get to many missed samples - mInterval = Math.max(10, aInterval); - mSampleCount = aSampleCount; - } - - @Override - public void run() { - synchronized (GeckoJavaSampler.class) { - mSamples.put(0, new Sample[mSampleCount]); - mSamplePos = 0; - - // Find the main thread - Set threadSet = Thread.getAllStackTraces().keySet(); - for (Thread t : threadSet) { - if (t.getName().compareToIgnoreCase("main") == 0) { - sMainThread = t; - break; - } - } - - if (sMainThread == null) { - Log.e(LOGTAG, "Main thread not found"); - return; - } - } - - while (true) { - try { - Thread.sleep(mInterval); - } catch (InterruptedException e) { - e.printStackTrace(); - } - synchronized (GeckoJavaSampler.class) { - if (!mPauseSampler) { - StackTraceElement[] bt = sMainThread.getStackTrace(); - mSamples.get(0)[mSamplePos] = new Sample(bt); - mSamplePos = (mSamplePos + 1) % mSamples.get(0).length; - } - if (mStopSampler) { - break; - } - } - } - } - - private Sample getSample(int aThreadId, int aSampleId) { - if (aThreadId < mSamples.size() && aSampleId < mSamples.get(aThreadId).length && - mSamples.get(aThreadId)[aSampleId] != null) { - int startPos = 0; - if (mSamples.get(aThreadId)[mSamplePos] != null) { - startPos = mSamplePos; - } - int readPos = (startPos + aSampleId) % mSamples.get(aThreadId).length; - return mSamples.get(aThreadId)[readPos]; - } - return null; - } - } - - - @WrapForJNI - public synchronized static String getThreadName(int aThreadId) { - if (aThreadId == 0 && sMainThread != null) { - return sMainThread.getName(); - } - return null; - } - - private synchronized static Sample getSample(int aThreadId, int aSampleId) { - return sSamplingRunnable.getSample(aThreadId, aSampleId); - } - - @WrapForJNI - public synchronized static double getSampleTime(int aThreadId, int aSampleId) { - Sample sample = getSample(aThreadId, aSampleId); - if (sample != null) { - if (sample.mJavaTime != 0) { - return (sample.mJavaTime - - SystemClock.elapsedRealtime()) + getProfilerTime(); - } - System.out.println("Sample: " + sample.mTime); - return sample.mTime; - } - return 0; - } - - @WrapForJNI - public synchronized static String getFrameName(int aThreadId, int aSampleId, int aFrameId) { - Sample sample = getSample(aThreadId, aSampleId); - if (sample != null && aFrameId < sample.mFrames.length) { - Frame frame = sample.mFrames[aFrameId]; - if (frame == null) { - return null; - } - return frame.className + "." + frame.methodName + "()"; - } - return null; - } - - @WrapForJNI - public static void start(int aInterval, int aSamples) { - synchronized (GeckoJavaSampler.class) { - if (sSamplingRunnable != null) { - return; - } - sSamplingRunnable = new SamplingThread(aInterval, aSamples); - sSamplingThread = new Thread(sSamplingRunnable, "Java Sampler"); - sSamplingThread.start(); - } - } - - @WrapForJNI - public static void pause() { - synchronized (GeckoJavaSampler.class) { - sSamplingRunnable.mPauseSampler = true; - } - } - - @WrapForJNI - public static void unpause() { - synchronized (GeckoJavaSampler.class) { - sSamplingRunnable.mPauseSampler = false; - } - } - - @WrapForJNI - public static void stop() { - synchronized (GeckoJavaSampler.class) { - if (sSamplingThread == null) { - return; - } - - sSamplingRunnable.mStopSampler = true; - try { - sSamplingThread.join(); - } catch (InterruptedException e) { - e.printStackTrace(); - } - sSamplingThread = null; - sSamplingRunnable = null; - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/GeckoMediaPlayer.java b/mobile/android/base/java/org/mozilla/gecko/GeckoMediaPlayer.java deleted file mode 100644 index c199aad554..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/GeckoMediaPlayer.java +++ /dev/null @@ -1,27 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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; - -import org.json.JSONObject; -import org.mozilla.gecko.util.EventCallback; - -/** - * Wrapper for MediaRouter types supported by Android, such as Chromecast, Miracast, etc. - */ -interface GeckoMediaPlayer { - /** - * Can return null. - */ - JSONObject toJSON(); - void load(String title, String url, String type, EventCallback callback); - void play(EventCallback callback); - void pause(EventCallback callback); - void stop(EventCallback callback); - void start(EventCallback callback); - void end(EventCallback callback); - void mirror(EventCallback callback); - void message(String message, EventCallback callback); -} diff --git a/mobile/android/base/java/org/mozilla/gecko/GeckoMessageReceiver.java b/mobile/android/base/java/org/mozilla/gecko/GeckoMessageReceiver.java deleted file mode 100644 index b7f4870c21..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/GeckoMessageReceiver.java +++ /dev/null @@ -1,19 +0,0 @@ -/* 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; - -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; - -public class GeckoMessageReceiver extends BroadcastReceiver { - @Override - public void onReceive(Context context, Intent intent) { - final String action = intent.getAction(); - if (GeckoApp.ACTION_INIT_PW.equals(action)) { - GeckoAppShell.notifyObservers("Passwords:Init", null); - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/GeckoPresentationDisplay.java b/mobile/android/base/java/org/mozilla/gecko/GeckoPresentationDisplay.java deleted file mode 100644 index df9844d7b0..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/GeckoPresentationDisplay.java +++ /dev/null @@ -1,22 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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; - -import org.json.JSONObject; -import org.mozilla.gecko.util.EventCallback; - -/** - * Wrapper for MediaRouter types supported by Android to use for - * Presentation API, such as Chromecast, Miracast, etc. - */ -interface GeckoPresentationDisplay { - /** - * Can return null. - */ - JSONObject toJSON(); - void start(EventCallback callback); - void stop(EventCallback callback); -} diff --git a/mobile/android/base/java/org/mozilla/gecko/GeckoProfilesProvider.java b/mobile/android/base/java/org/mozilla/gecko/GeckoProfilesProvider.java deleted file mode 100644 index 8a9c461c54..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/GeckoProfilesProvider.java +++ /dev/null @@ -1,149 +0,0 @@ -/* 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; - -import java.io.File; -import java.util.Map; -import java.util.Map.Entry; - -import org.mozilla.gecko.GeckoProfileDirectories.NoMozillaDirectoryException; -import org.mozilla.gecko.db.BrowserContract; - -import android.content.ContentProvider; -import android.content.ContentValues; -import android.content.UriMatcher; -import android.database.Cursor; -import android.database.MatrixCursor; -import android.net.Uri; -import android.util.Log; - -/** - * This is not a per-profile provider. This provider allows read-only, - * restricted access to certain attributes of Fennec profiles. - */ -public class GeckoProfilesProvider extends ContentProvider { - private static final String LOG_TAG = "GeckoProfilesProvider"; - - private static final UriMatcher URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH); - - private static final int PROFILES = 100; - private static final int PROFILES_NAME = 101; - private static final int PROFILES_DEFAULT = 200; - - private static final String[] DEFAULT_ARGS = { - BrowserContract.Profiles.NAME, - BrowserContract.Profiles.PATH, - }; - - static { - URI_MATCHER.addURI(BrowserContract.PROFILES_AUTHORITY, "profiles", PROFILES); - URI_MATCHER.addURI(BrowserContract.PROFILES_AUTHORITY, "profiles/*", PROFILES_NAME); - URI_MATCHER.addURI(BrowserContract.PROFILES_AUTHORITY, "default", PROFILES_DEFAULT); - } - - @Override - public String getType(Uri uri) { - return null; - } - - @Override - public boolean onCreate() { - // Successfully loaded. - return true; - } - - private String[] profileValues(final String name, final String path, int len, int nameIndex, int pathIndex) { - final String[] values = new String[len]; - if (nameIndex >= 0) { - values[nameIndex] = name; - } - if (pathIndex >= 0) { - values[pathIndex] = path; - } - return values; - } - - protected void addRowForProfile(final MatrixCursor cursor, final int len, final int nameIndex, final int pathIndex, final String name, final String path) { - if (path == null || name == null) { - return; - } - - cursor.addRow(profileValues(name, path, len, nameIndex, pathIndex)); - } - - protected Cursor getCursorForProfiles(final String[] args, Map profiles) { - // Compute the projection. - int nameIndex = -1; - int pathIndex = -1; - for (int i = 0; i < args.length; ++i) { - if (BrowserContract.Profiles.NAME.equals(args[i])) { - nameIndex = i; - } else if (BrowserContract.Profiles.PATH.equals(args[i])) { - pathIndex = i; - } - } - - final MatrixCursor cursor = new MatrixCursor(args); - for (Entry entry : profiles.entrySet()) { - addRowForProfile(cursor, args.length, nameIndex, pathIndex, entry.getKey(), entry.getValue()); - } - return cursor; - } - - @Override - public Cursor query(Uri uri, String[] projection, String selection, - String[] selectionArgs, String sortOrder) { - - final String[] args = (projection == null) ? DEFAULT_ARGS : projection; - - final File mozillaDir; - try { - mozillaDir = GeckoProfileDirectories.getMozillaDirectory(getContext()); - } catch (NoMozillaDirectoryException e) { - Log.d(LOG_TAG, "No Mozilla directory; cannot query for profiles. Assuming there are none."); - return new MatrixCursor(projection); - } - - final Map matchingProfiles; - - final int match = URI_MATCHER.match(uri); - switch (match) { - case PROFILES: - // Return all profiles. - matchingProfiles = GeckoProfileDirectories.getAllProfiles(mozillaDir); - break; - case PROFILES_NAME: - // Return data about the specified profile. - final String name = uri.getLastPathSegment(); - matchingProfiles = GeckoProfileDirectories.getProfilesNamed(mozillaDir, - name); - break; - case PROFILES_DEFAULT: - matchingProfiles = GeckoProfileDirectories.getDefaultProfile(mozillaDir); - break; - default: - throw new UnsupportedOperationException("Unknown query URI " + uri); - } - - return getCursorForProfiles(args, matchingProfiles); - } - - @Override - public Uri insert(Uri uri, ContentValues values) { - throw new IllegalStateException("Inserts not supported."); - } - - @Override - public int delete(Uri uri, String selection, String[] selectionArgs) { - throw new IllegalStateException("Deletes not supported."); - } - - @Override - public int update(Uri uri, ContentValues values, String selection, - String[] selectionArgs) { - throw new IllegalStateException("Updates not supported."); - } - -} diff --git a/mobile/android/base/java/org/mozilla/gecko/GeckoService.java b/mobile/android/base/java/org/mozilla/gecko/GeckoService.java deleted file mode 100644 index 3a99fd2a14..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/GeckoService.java +++ /dev/null @@ -1,236 +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; - -import android.app.AlarmManager; -import android.app.Service; -import android.app.PendingIntent; -import android.content.Context; -import android.content.Intent; -import android.os.IBinder; -import android.os.Handler; -import android.os.Looper; -import android.util.Log; - -import java.io.File; - -import org.mozilla.gecko.util.NativeEventListener; -import org.mozilla.gecko.util.NativeJSObject; -import org.mozilla.gecko.util.EventCallback; - -public class GeckoService extends Service { - - private static final String LOGTAG = "GeckoService"; - private static final boolean DEBUG = false; - - private static final String INTENT_PROFILE_NAME = "org.mozilla.gecko.intent.PROFILE_NAME"; - private static final String INTENT_PROFILE_DIR = "org.mozilla.gecko.intent.PROFILE_DIR"; - - private static final String INTENT_ACTION_UPDATE_ADDONS = "update-addons"; - private static final String INTENT_ACTION_CREATE_SERVICES = "create-services"; - - private static final String INTENT_SERVICE_CATEGORY = "category"; - private static final String INTENT_SERVICE_DATA = "data"; - - private static class EventListener implements NativeEventListener { - @Override // NativeEventListener - public void handleMessage(final String event, - final NativeJSObject message, - final EventCallback callback) { - final Context context = GeckoAppShell.getApplicationContext(); - switch (event) { - case "Gecko:ScheduleRun": - if (DEBUG) { - Log.d(LOGTAG, "Scheduling " + message.getString("action") + - " @ " + message.getInt("interval") + "ms"); - } - - final Intent intent = getIntentForAction(context, message.getString("action")); - final PendingIntent pendingIntent = PendingIntent.getService( - context, /* requestCode */ 0, intent, PendingIntent.FLAG_CANCEL_CURRENT); - - final AlarmManager am = (AlarmManager) - context.getSystemService(Context.ALARM_SERVICE); - // Cancel any previous alarm and schedule a new one. - am.setInexactRepeating(AlarmManager.ELAPSED_REALTIME, - message.getInt("trigger"), - message.getInt("interval"), - pendingIntent); - break; - - default: - throw new UnsupportedOperationException(event); - } - } - } - - private static final EventListener EVENT_LISTENER = new EventListener(); - - public static void register() { - if (DEBUG) { - Log.d(LOGTAG, "Registered listener"); - } - EventDispatcher.getInstance().registerGeckoThreadListener(EVENT_LISTENER, - "Gecko:ScheduleRun"); - } - - public static void unregister() { - if (DEBUG) { - Log.d(LOGTAG, "Unregistered listener"); - } - EventDispatcher.getInstance().unregisterGeckoThreadListener(EVENT_LISTENER, - "Gecko:ScheduleRun"); - } - - @Override // Service - public void onCreate() { - GeckoAppShell.ensureCrashHandling(); - GeckoThread.onResume(); - super.onCreate(); - - if (DEBUG) { - Log.d(LOGTAG, "Created"); - } - } - - @Override // Service - public void onDestroy() { - GeckoThread.onPause(); - - // We want to block here if we can, so we don't get killed when Gecko is in the - // middle of handling onPause(). - if (GeckoThread.isStateAtLeast(GeckoThread.State.PROFILE_READY)) { - GeckoThread.waitOnGecko(); - } - - if (DEBUG) { - Log.d(LOGTAG, "Destroyed"); - } - super.onDestroy(); - } - - private static Intent getIntentForAction(final Context context, final String action) { - final Intent intent = new Intent(action, /* uri */ null, context, GeckoService.class); - final GeckoProfile profile = GeckoThread.getActiveProfile(); - if (profile != null) { - setIntentProfile(intent, profile.getName(), profile.getDir().getAbsolutePath()); - } - return intent; - } - - public static Intent getIntentToCreateServices(final Context context, final String category, final String data) { - final Intent intent = getIntentForAction(context, INTENT_ACTION_CREATE_SERVICES); - intent.putExtra(INTENT_SERVICE_CATEGORY, category); - intent.putExtra(INTENT_SERVICE_DATA, data); - return intent; - } - - public static Intent getIntentToCreateServices(final Context context, final String category) { - return getIntentToCreateServices(context, category, /* data */ null); - } - - public static void setIntentProfile(final Intent intent, final String profileName, - final String profileDir) { - intent.putExtra(INTENT_PROFILE_NAME, profileName); - intent.putExtra(INTENT_PROFILE_DIR, profileDir); - } - - private int handleIntent(final Intent intent, final int startId) { - if (DEBUG) { - Log.d(LOGTAG, "Handling " + intent.getAction()); - } - - final String profileName = intent.getStringExtra(INTENT_PROFILE_NAME); - final String profileDir = intent.getStringExtra(INTENT_PROFILE_DIR); - - if (profileName == null) { - throw new IllegalArgumentException("Intent must specify profile."); - } - - if (!GeckoThread.initWithProfile(profileName != null ? profileName : "", - profileDir != null ? new File(profileDir) : null)) { - Log.w(LOGTAG, "Ignoring due to profile mismatch: " + - profileName + " [" + profileDir + ']'); - - final GeckoProfile profile = GeckoThread.getActiveProfile(); - if (profile != null) { - Log.w(LOGTAG, "Current profile is " + profile.getName() + - " [" + profile.getDir().getAbsolutePath() + ']'); - } - stopSelf(startId); - return Service.START_NOT_STICKY; - } - - GeckoThread.launch(); - - switch (intent.getAction()) { - case INTENT_ACTION_UPDATE_ADDONS: - // Run the add-on update service. Because the service is automatically invoked - // when loading Gecko, we don't have to do anything else here. - break; - - case INTENT_ACTION_CREATE_SERVICES: - final String category = intent.getStringExtra(INTENT_SERVICE_CATEGORY); - final String data = intent.getStringExtra(INTENT_SERVICE_DATA); - - if (category == null) { - break; - } - GeckoThread.createServices(category, data); - break; - - default: - Log.w(LOGTAG, "Unknown request: " + intent); - } - - stopSelf(startId); - return Service.START_NOT_STICKY; - } - - @Override // Service - public int onStartCommand(final Intent intent, final int flags, final int startId) { - if (intent == null) { - return Service.START_NOT_STICKY; - } - try { - return handleIntent(intent, startId); - } catch (final Throwable e) { - Log.e(LOGTAG, "Cannot handle intent: " + intent, e); - return Service.START_NOT_STICKY; - } - } - - @Override // Service - public IBinder onBind(final Intent intent) { - return null; - } - - public static void startGecko(final GeckoProfile profile, final String args, final Context context) { - if (GeckoThread.isLaunched()) { - if (DEBUG) { - Log.v(LOGTAG, "already launched"); - } - return; - } - - Handler handler = new Handler(Looper.getMainLooper()); - handler.post(new Runnable() { - @Override - public void run() { - GeckoAppShell.ensureCrashHandling(); - GeckoAppShell.setApplicationContext(context); - GeckoThread.onResume(); - - GeckoThread.init(profile, args, null, false); - GeckoThread.launch(); - - if (DEBUG) { - Log.v(LOGTAG, "warmed up (launched)"); - } - } - }); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/GeckoUpdateReceiver.java b/mobile/android/base/java/org/mozilla/gecko/GeckoUpdateReceiver.java deleted file mode 100644 index f73c42e405..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/GeckoUpdateReceiver.java +++ /dev/null @@ -1,25 +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; - -import org.mozilla.gecko.updater.UpdateServiceHelper; - -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; - -public class GeckoUpdateReceiver extends BroadcastReceiver -{ - @Override - public void onReceive(Context context, Intent intent) { - if (UpdateServiceHelper.ACTION_CHECK_UPDATE_RESULT.equals(intent.getAction())) { - String result = intent.getStringExtra("result"); - if (GeckoAppShell.getGeckoInterface() != null && result != null) { - GeckoAppShell.getGeckoInterface().notifyCheckUpdateResult(result); - } - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/GlobalHistory.java b/mobile/android/base/java/org/mozilla/gecko/GlobalHistory.java deleted file mode 100644 index c1d9c4939a..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/GlobalHistory.java +++ /dev/null @@ -1,178 +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; - -import java.lang.ref.SoftReference; -import java.util.HashSet; -import java.util.LinkedList; -import java.util.Queue; -import java.util.Set; - -import org.mozilla.gecko.db.BrowserDB; -import org.mozilla.gecko.reader.ReaderModeUtils; -import org.mozilla.gecko.util.ThreadUtils; - -import android.content.ContentResolver; -import android.content.Context; -import android.database.Cursor; -import android.os.Bundle; -import android.os.Handler; -import android.os.SystemClock; -import android.util.Log; - -class GlobalHistory { - private static final String LOGTAG = "GeckoGlobalHistory"; - - public static final String EVENT_URI_AVAILABLE_IN_HISTORY = "URI_INSERTED_TO_HISTORY"; - public static final String EVENT_PARAM_URI = "uri"; - - private static final String TELEMETRY_HISTOGRAM_ADD = "FENNEC_GLOBALHISTORY_ADD_MS"; - private static final String TELEMETRY_HISTOGRAM_UPDATE = "FENNEC_GLOBALHISTORY_UPDATE_MS"; - private static final String TELEMETRY_HISTOGRAM_BUILD_VISITED_LINK = "FENNEC_GLOBALHISTORY_VISITED_BUILD_MS"; - - private static final GlobalHistory sInstance = new GlobalHistory(); - - static GlobalHistory getInstance() { - return sInstance; - } - - // this is the delay between receiving a URI check request and processing it. - // this allows batching together multiple requests and processing them together, - // which is more efficient. - private static final long BATCHING_DELAY_MS = 100; - - private final Handler mHandler; // a background thread on which we can process requests - - // Note: These fields are accessed through the NotificationRunnable inner class. - final Queue mPendingUris; // URIs that need to be checked - SoftReference> mVisitedCache; // cache of the visited URI list - boolean mProcessing; // = false // whether or not the runnable is queued/working - - private class NotifierRunnable implements Runnable { - private final ContentResolver mContentResolver; - private final BrowserDB mDB; - - public NotifierRunnable(final Context context) { - mContentResolver = context.getContentResolver(); - mDB = BrowserDB.from(context); - } - - @Override - public void run() { - Set visitedSet = mVisitedCache.get(); - if (visitedSet == null) { - // The cache was wiped. Repopulate it. - Log.w(LOGTAG, "Rebuilding visited link set..."); - final long start = SystemClock.uptimeMillis(); - final Cursor c = mDB.getAllVisitedHistory(mContentResolver); - if (c == null) { - return; - } - - try { - visitedSet = new HashSet(); - if (c.moveToFirst()) { - do { - visitedSet.add(c.getString(0)); - } while (c.moveToNext()); - } - mVisitedCache = new SoftReference>(visitedSet); - final long end = SystemClock.uptimeMillis(); - final long took = end - start; - Telemetry.addToHistogram(TELEMETRY_HISTOGRAM_BUILD_VISITED_LINK, (int) Math.min(took, Integer.MAX_VALUE)); - } finally { - c.close(); - } - } - - // This runs on the same handler thread as the checkUriVisited code, - // so no synchronization is needed. - while (true) { - final String uri = mPendingUris.poll(); - if (uri == null) { - break; - } - - if (visitedSet.contains(uri)) { - GeckoAppShell.notifyUriVisited(uri); - } - } - - mProcessing = false; - } - }; - - private GlobalHistory() { - mHandler = ThreadUtils.getBackgroundHandler(); - mPendingUris = new LinkedList(); - mVisitedCache = new SoftReference>(null); - } - - public void addToGeckoOnly(String uri) { - Set visitedSet = mVisitedCache.get(); - if (visitedSet != null) { - visitedSet.add(uri); - } - GeckoAppShell.notifyUriVisited(uri); - } - - public void add(final Context context, final BrowserDB db, String uri) { - ThreadUtils.assertOnBackgroundThread(); - final long start = SystemClock.uptimeMillis(); - - // stripAboutReaderUrl only removes about:reader if present, in all other cases the original string is returned - final String uriToStore = ReaderModeUtils.stripAboutReaderUrl(uri); - - db.updateVisitedHistory(context.getContentResolver(), uriToStore); - - final long end = SystemClock.uptimeMillis(); - final long took = end - start; - Telemetry.addToHistogram(TELEMETRY_HISTOGRAM_ADD, (int) Math.min(took, Integer.MAX_VALUE)); - addToGeckoOnly(uriToStore); - dispatchUriAvailableMessage(uri); - } - - @SuppressWarnings("static-method") - public void update(final ContentResolver cr, final BrowserDB db, String uri, String title) { - ThreadUtils.assertOnBackgroundThread(); - final long start = SystemClock.uptimeMillis(); - - final String uriToStore = ReaderModeUtils.stripAboutReaderUrl(uri); - - db.updateHistoryTitle(cr, uriToStore, title); - - final long end = SystemClock.uptimeMillis(); - final long took = end - start; - Telemetry.addToHistogram(TELEMETRY_HISTOGRAM_UPDATE, (int) Math.min(took, Integer.MAX_VALUE)); - } - - public void checkUriVisited(final String uri) { - final String storedURI = ReaderModeUtils.stripAboutReaderUrl(uri); - - final NotifierRunnable runnable = new NotifierRunnable(GeckoAppShell.getContext()); - mHandler.post(new Runnable() { - @Override - public void run() { - // this runs on the same handler thread as the processing loop, - // so no synchronization needed - mPendingUris.add(storedURI); - if (mProcessing) { - // there's already a runnable queued up or working away, so - // no need to post another - return; - } - mProcessing = true; - mHandler.postDelayed(runnable, BATCHING_DELAY_MS); - } - }); - } - - private void dispatchUriAvailableMessage(String uri) { - final Bundle message = new Bundle(); - message.putString(EVENT_PARAM_URI, uri); - EventDispatcher.getInstance().dispatch(EVENT_URI_AVAILABLE_IN_HISTORY, message); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/GlobalPageMetadata.java b/mobile/android/base/java/org/mozilla/gecko/GlobalPageMetadata.java deleted file mode 100644 index d9d12962ca..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/GlobalPageMetadata.java +++ /dev/null @@ -1,182 +0,0 @@ -/* 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; - -import android.content.ContentProviderClient; -import android.os.Bundle; -import android.support.annotation.NonNull; -import android.support.annotation.VisibleForTesting; -import android.text.TextUtils; -import android.util.Log; - -import org.json.JSONException; -import org.json.JSONObject; -import org.mozilla.gecko.db.BrowserContract; -import org.mozilla.gecko.db.BrowserDB; -import org.mozilla.gecko.util.BundleEventListener; -import org.mozilla.gecko.util.EventCallback; -import org.mozilla.gecko.util.ThreadUtils; - -import java.util.Collections; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.Map; - -/** - * Provides access to metadata information about websites. - * - * While storing, in case of timing issues preventing us from looking up History GUID by a given uri, - * we queue up metadata and wait for GlobalHistory to let us know history record is now available. - * - * TODO Bug 1313515: selection of metadata for a given uri/history_GUID - * - * @author grisha - */ -/* package-local */ class GlobalPageMetadata implements BundleEventListener { - private static final String LOG_TAG = "GeckoGlobalPageMetadata"; - - private static final GlobalPageMetadata instance = new GlobalPageMetadata(); - - private static final String KEY_HAS_IMAGE = "hasImage"; - private static final String KEY_METADATA_JSON = "metadataJSON"; - - private static final int MAX_METADATA_QUEUE_SIZE = 15; - - private final Map queuedMetadata = Collections.synchronizedMap(new LimitedLinkedHashMap()); - - public static GlobalPageMetadata getInstance() { - return instance; - } - - private static class LimitedLinkedHashMap extends LinkedHashMap { - private static final long serialVersionUID = 6359725112736360244L; - - @Override - protected boolean removeEldestEntry(Entry eldest) { - if (size() > MAX_METADATA_QUEUE_SIZE) { - Log.w(LOG_TAG, "Page metadata queue is full. Dropping oldest metadata."); - return true; - } - return false; - } - } - - private GlobalPageMetadata() {} - - public void init() { - EventDispatcher - .getInstance() - .registerBackgroundThreadListener(this, GlobalHistory.EVENT_URI_AVAILABLE_IN_HISTORY); - } - - public void add(BrowserDB db, ContentProviderClient contentProviderClient, String uri, boolean hasImage, @NonNull String metadataJSON) { - ThreadUtils.assertOnBackgroundThread(); - - // NB: Other than checking that JSON is valid and trimming it, - // we do not process metadataJSON in any way, trusting our source. - doAddOrQueue(db, contentProviderClient, uri, hasImage, metadataJSON); - } - - @VisibleForTesting - /*package-local */ void doAddOrQueue(BrowserDB db, ContentProviderClient contentProviderClient, String uri, boolean hasImage, @NonNull String metadataJSON) { - final String preparedMetadataJSON; - try { - preparedMetadataJSON = prepareJSON(metadataJSON); - } catch (JSONException e) { - Log.e(LOG_TAG, "Couldn't process metadata JSON", e); - return; - } - - // Don't bother queuing this if deletions fails to find a corresponding history record. - // If we can't delete metadata because it didn't exist yet, that's OK. - if (preparedMetadataJSON.equals("{}")) { - final int deleted = db.deletePageMetadata(contentProviderClient, uri); - // We could delete none if history record for uri isn't present. - // We must delete one if history record for uri is present. - if (deleted != 0 && deleted != 1) { - throw new IllegalStateException("Deleted unexpected number of page metadata records: " + deleted); - } - return; - } - - // If we could insert page metadata, we're done. - if (db.insertPageMetadata(contentProviderClient, uri, hasImage, preparedMetadataJSON)) { - return; - } - - // Otherwise, we need to queue it for future insertion when history record is available. - Bundle bundledMetadata = new Bundle(); - bundledMetadata.putBoolean(KEY_HAS_IMAGE, hasImage); - bundledMetadata.putString(KEY_METADATA_JSON, preparedMetadataJSON); - queuedMetadata.put(uri, bundledMetadata); - } - - @VisibleForTesting - /* package-local */ int getMetadataQueueSize() { - return queuedMetadata.size(); - } - - @Override - public void handleMessage(String event, Bundle message, EventCallback callback) { - ThreadUtils.assertOnBackgroundThread(); - - if (!GlobalHistory.EVENT_URI_AVAILABLE_IN_HISTORY.equals(event)) { - return; - } - - final String uri = message.getString(GlobalHistory.EVENT_PARAM_URI); - if (TextUtils.isEmpty(uri)) { - return; - } - - final Bundle bundledMetadata; - synchronized (queuedMetadata) { - if (!queuedMetadata.containsKey(uri)) { - return; - } - - bundledMetadata = queuedMetadata.get(uri); - queuedMetadata.remove(uri); - } - - insertMetadataBundleForUri(uri, bundledMetadata); - } - - private void insertMetadataBundleForUri(String uri, Bundle bundledMetadata) { - final boolean hasImage = bundledMetadata.getBoolean(KEY_HAS_IMAGE); - final String metadataJSON = bundledMetadata.getString(KEY_METADATA_JSON); - - // Acquire CPC, must be released in this function. - final ContentProviderClient contentProviderClient = GeckoAppShell.getApplicationContext() - .getContentResolver() - .acquireContentProviderClient(BrowserContract.PageMetadata.CONTENT_URI); - - // Pre-conditions... - if (contentProviderClient == null) { - Log.e(LOG_TAG, "Couldn't acquire content provider client"); - return; - } - - if (TextUtils.isEmpty(metadataJSON)) { - Log.e(LOG_TAG, "Metadata bundle contained empty metadata json"); - return; - } - - // Insert! - try { - add( - BrowserDB.from(GeckoThread.getActiveProfile()), - contentProviderClient, - uri, hasImage, metadataJSON - ); - } finally { - contentProviderClient.release(); - } - } - - private String prepareJSON(String json) throws JSONException { - return (new JSONObject(json)).toString(); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/GuestSession.java b/mobile/android/base/java/org/mozilla/gecko/GuestSession.java deleted file mode 100644 index 69502f44a2..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/GuestSession.java +++ /dev/null @@ -1,51 +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; - -import android.app.KeyguardManager; -import android.app.NotificationManager; -import android.app.PendingIntent; -import android.content.Context; -import android.content.Intent; -import android.content.res.Resources; -import android.support.v4.app.NotificationCompat; -import android.view.Window; -import android.view.WindowManager; - -// Utility methods for entering/exiting guest mode. -public final class GuestSession { - private static final String LOGTAG = "GeckoGuestSession"; - - public static final String NOTIFICATION_INTENT = "org.mozilla.gecko.GUEST_SESSION_INPROGRESS"; - - private static PendingIntent getNotificationIntent(Context context) { - Intent intent = new Intent(NOTIFICATION_INTENT); - intent.setClassName(context, AppConstants.MOZ_ANDROID_BROWSER_INTENT_CLASS); - return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); - } - - public static void showNotification(Context context) { - final NotificationCompat.Builder builder = new NotificationCompat.Builder(context); - final Resources res = context.getResources(); - builder.setContentTitle(res.getString(R.string.guest_browsing_notification_title)) - .setContentText(res.getString(R.string.guest_browsing_notification_text)) - .setSmallIcon(R.drawable.alert_guest) - .setOngoing(true) - .setContentIntent(getNotificationIntent(context)); - - final NotificationManager manager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); - manager.notify(R.id.guestNotification, builder.build()); - } - - public static void hideNotification(Context context) { - final NotificationManager manager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); - manager.cancel(R.id.guestNotification); - } - - public static void onNotificationIntentReceived(BrowserApp context) { - context.showGuestModeDialog(BrowserApp.GuestModeDialog.LEAVING); - } - -} diff --git a/mobile/android/base/java/org/mozilla/gecko/IntentHelper.java b/mobile/android/base/java/org/mozilla/gecko/IntentHelper.java deleted file mode 100644 index e2f34f926b..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/IntentHelper.java +++ /dev/null @@ -1,599 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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; - -import org.mozilla.gecko.overlays.ui.ShareDialog; -import org.mozilla.gecko.util.ActivityResultHandler; -import org.mozilla.gecko.util.EventCallback; -import org.mozilla.gecko.util.GeckoEventListener; -import org.mozilla.gecko.util.JSONUtils; -import org.mozilla.gecko.util.NativeEventListener; -import org.mozilla.gecko.util.NativeJSObject; -import org.mozilla.gecko.widget.ExternalIntentDuringPrivateBrowsingPromptFragment; - -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - -import android.annotation.TargetApi; -import android.content.Context; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.content.pm.ResolveInfo; -import android.net.Uri; -import android.provider.Browser; -import android.support.annotation.Nullable; -import android.support.v4.app.FragmentActivity; -import android.text.TextUtils; -import android.util.Log; -import android.webkit.MimeTypeMap; - -import java.io.UnsupportedEncodingException; -import java.net.URI; -import java.net.URISyntaxException; -import java.net.URLEncoder; -import java.util.Arrays; -import java.util.List; -import java.util.Locale; - -public final class IntentHelper implements GeckoEventListener, - NativeEventListener { - - private static final String LOGTAG = "GeckoIntentHelper"; - private static final String[] EVENTS = { - "Intent:GetHandlers", - "Intent:Open", - "Intent:OpenForResult", - }; - - private static final String[] NATIVE_EVENTS = { - "Intent:OpenNoHandler", - }; - - // via http://developer.android.com/distribute/tools/promote/linking.html - private static String MARKET_INTENT_URI_PACKAGE_PREFIX = "market://details?id="; - private static String EXTRA_BROWSER_FALLBACK_URL = "browser_fallback_url"; - - /** A partial URI to an error page - the encoded error URI should be appended before loading. */ - private static String UNKNOWN_PROTOCOL_URI_PREFIX = "about:neterror?e=unknownProtocolFound&u="; - - private static IntentHelper instance; - - private final FragmentActivity activity; - - private IntentHelper(final FragmentActivity activity) { - this.activity = activity; - EventDispatcher.getInstance().registerGeckoThreadListener((GeckoEventListener) this, EVENTS); - EventDispatcher.getInstance().registerGeckoThreadListener((NativeEventListener) this, NATIVE_EVENTS); - } - - public static IntentHelper init(final FragmentActivity activity) { - if (instance == null) { - instance = new IntentHelper(activity); - } else { - Log.w(LOGTAG, "IntentHelper.init() called twice, ignoring."); - } - - return instance; - } - - public static void destroy() { - if (instance != null) { - EventDispatcher.getInstance().unregisterGeckoThreadListener((GeckoEventListener) instance, EVENTS); - EventDispatcher.getInstance().unregisterGeckoThreadListener((NativeEventListener) instance, NATIVE_EVENTS); - instance = null; - } - } - - /** - * Given the inputs to getOpenURIIntent, plus an optional - * package name and class name, create and fire an intent to open the - * provided URI. If a class name is specified but a package name is not, - * we will default to using the current fennec package. - * - * @param targetURI the string spec of the URI to open. - * @param mimeType an optional MIME type string. - * @param packageName an optional app package name. - * @param className an optional intent class name. - * @param action an Android action specifier, such as - * Intent.ACTION_SEND. - * @param title the title to use in ACTION_SEND intents. - * @param showPromptInPrivateBrowsing whether or not the user should be prompted when opening - * this uri from private browsing. This should be true - * when the user doesn't explicitly choose to open an an - * external app (e.g. just clicked a link). - * @return true if the activity started successfully or the user was prompted to open the - * application; false otherwise. - */ - public static boolean openUriExternal(String targetURI, - String mimeType, - String packageName, - String className, - String action, - String title, - final boolean showPromptInPrivateBrowsing) { - final GeckoAppShell.GeckoInterface gi = GeckoAppShell.getGeckoInterface(); - final Context activityContext = gi != null ? gi.getActivity() : null; - final Context context = activityContext != null ? activityContext : GeckoAppShell.getApplicationContext(); - final Intent intent = getOpenURIIntent(context, targetURI, - mimeType, action, title); - - if (intent == null) { - return false; - } - - if (!TextUtils.isEmpty(className)) { - if (!TextUtils.isEmpty(packageName)) { - intent.setClassName(packageName, className); - } else { - // Default to using the fennec app context. - intent.setClassName(context, className); - } - } - - if (!showPromptInPrivateBrowsing || activityContext == null) { - if (activityContext == null) { - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - } - return ActivityHandlerHelper.startIntentAndCatch(LOGTAG, context, intent); - } else { - // Ideally we retrieve the Activity from the calling args, rather than - // statically, but since this method is called from Gecko and I'm - // unfamiliar with that code, this is a simpler solution. - final FragmentActivity fragmentActivity = (FragmentActivity) activityContext; - return ExternalIntentDuringPrivateBrowsingPromptFragment.showDialogOrAndroidChooser( - context, fragmentActivity.getSupportFragmentManager(), intent); - } - } - - public static boolean hasHandlersForIntent(Intent intent) { - try { - return !GeckoAppShell.queryIntentActivities(intent).isEmpty(); - } catch (Exception ex) { - Log.e(LOGTAG, "Exception in hasHandlersForIntent"); - return false; - } - } - - public static String[] getHandlersForIntent(Intent intent) { - final PackageManager pm = GeckoAppShell.getApplicationContext().getPackageManager(); - try { - final List list = GeckoAppShell.queryIntentActivities(intent); - - int numAttr = 4; - final String[] ret = new String[list.size() * numAttr]; - for (int i = 0; i < list.size(); i++) { - ResolveInfo resolveInfo = list.get(i); - ret[i * numAttr] = resolveInfo.loadLabel(pm).toString(); - if (resolveInfo.isDefault) - ret[i * numAttr + 1] = "default"; - else - ret[i * numAttr + 1] = ""; - ret[i * numAttr + 2] = resolveInfo.activityInfo.applicationInfo.packageName; - ret[i * numAttr + 3] = resolveInfo.activityInfo.name; - } - return ret; - } catch (Exception ex) { - Log.e(LOGTAG, "Exception in getHandlersForIntent"); - return new String[0]; - } - } - - public static Intent getIntentForActionString(String aAction) { - // Default to the view action if no other action as been specified. - if (TextUtils.isEmpty(aAction)) { - return new Intent(Intent.ACTION_VIEW); - } - return new Intent(aAction); - } - - /** - * Given a URI, a MIME type, and a title, - * produce a share intent which can be used to query all activities - * than can open the specified URI. - * - * @param context a Context instance. - * @param targetURI the string spec of the URI to open. - * @param mimeType an optional MIME type string. - * @param title the title to use in ACTION_SEND intents. - * @return an Intent, or null if none could be - * produced. - */ - public static Intent getShareIntent(final Context context, - final String targetURI, - final String mimeType, - final String title) { - Intent shareIntent = getIntentForActionString(Intent.ACTION_SEND); - shareIntent.putExtra(Intent.EXTRA_TEXT, targetURI); - shareIntent.putExtra(Intent.EXTRA_SUBJECT, title); - shareIntent.putExtra(ShareDialog.INTENT_EXTRA_DEVICES_ONLY, true); - - // Note that EXTRA_TITLE is intended to be used for share dialog - // titles. Common usage (e.g., Pocket) suggests that it's sometimes - // interpreted as an alternate to EXTRA_SUBJECT, so we include it. - shareIntent.putExtra(Intent.EXTRA_TITLE, title); - - if (mimeType != null && mimeType.length() > 0) { - shareIntent.setType(mimeType); - } - - return shareIntent; - } - - /** - * Given a URI, a MIME type, an Android intent "action", and a title, - * produce an intent which can be used to start an activity to open - * the specified URI. - * - * @param context a Context instance. - * @param targetURI the string spec of the URI to open. - * @param mimeType an optional MIME type string. - * @param action an Android action specifier, such as - * Intent.ACTION_SEND. - * @param title the title to use in ACTION_SEND intents. - * @return an Intent, or null if none could be - * produced. - */ - static Intent getOpenURIIntent(final Context context, - final String targetURI, - final String mimeType, - final String action, - final String title) { - - // The resultant chooser can return non-exported activities in 4.1 and earlier. - // https://code.google.com/p/android/issues/detail?id=29535 - final Intent intent = getOpenURIIntentInner(context, targetURI, mimeType, action, title); - - if (intent != null) { - // Some applications use this field to return to the same browser after processing the - // Intent. While there is some danger (e.g. denial of service), other major browsers already - // use it and so it's the norm. - intent.putExtra(Browser.EXTRA_APPLICATION_ID, AppConstants.ANDROID_PACKAGE_NAME); - } - - return intent; - } - - private static Intent getOpenURIIntentInner(final Context context, final String targetURI, - final String mimeType, final String action, final String title) { - - if (action.equalsIgnoreCase(Intent.ACTION_SEND)) { - Intent shareIntent = getShareIntent(context, targetURI, mimeType, title); - return Intent.createChooser(shareIntent, - context.getResources().getString(R.string.share_title)); - } - - Uri uri = normalizeUriScheme(targetURI.indexOf(':') >= 0 ? Uri.parse(targetURI) : new Uri.Builder().scheme(targetURI).build()); - if (!TextUtils.isEmpty(mimeType)) { - Intent intent = getIntentForActionString(action); - intent.setDataAndType(uri, mimeType); - return intent; - } - - if (!GeckoAppShell.isUriSafeForScheme(uri)) { - return null; - } - - final String scheme = uri.getScheme(); - if ("intent".equals(scheme) || "android-app".equals(scheme)) { - final Intent intent; - try { - intent = Intent.parseUri(targetURI, 0); - } catch (final URISyntaxException e) { - Log.e(LOGTAG, "Unable to parse URI - " + e); - return null; - } - - final Uri data = intent.getData(); - if (data != null && "file".equals(data.normalizeScheme().getScheme())) { - Log.w(LOGTAG, "Blocked intent with \"file://\" data scheme."); - return null; - } - - // Only open applications which can accept arbitrary data from a browser. - intent.addCategory(Intent.CATEGORY_BROWSABLE); - - // Prevent site from explicitly opening our internal activities, which can leak data. - intent.setComponent(null); - nullIntentSelector(intent); - - return intent; - } - - // Compute our most likely intent, then check to see if there are any - // custom handlers that would apply. - // Start with the original URI. If we end up modifying it, we'll - // overwrite it. - final String extension = MimeTypeMap.getFileExtensionFromUrl(targetURI); - final Intent intent = getIntentForActionString(action); - intent.setData(uri); - - if ("file".equals(scheme)) { - // Only set explicit mimeTypes on file://. - final String mimeType2 = GeckoAppShell.getMimeTypeFromExtension(extension); - intent.setType(mimeType2); - return intent; - } - - // Have a special handling for SMS based schemes, as the query parameters - // are not extracted from the URI automatically. - if (!"sms".equals(scheme) && !"smsto".equals(scheme) && !"mms".equals(scheme) && !"mmsto".equals(scheme)) { - return intent; - } - - final String query = uri.getEncodedQuery(); - if (TextUtils.isEmpty(query)) { - return intent; - } - - // It is common to see sms*/mms* uris on the web without '//', it is W3C standard not to have the slashes, - // but android's Uri builder & Uri require the slashes and will interpret those without as malformed. - String currentUri = uri.toString(); - String correctlyFormattedDataURIScheme = scheme + "://"; - if (!currentUri.contains(correctlyFormattedDataURIScheme)) { - uri = Uri.parse(currentUri.replaceFirst(scheme + ":", correctlyFormattedDataURIScheme)); - } - - final String[] fields = query.split("&"); - boolean shouldUpdateIntent = false; - String resultQuery = ""; - for (String field : fields) { - if (field.startsWith("body=")) { - final String body = Uri.decode(field.substring(5)); - intent.putExtra("sms_body", body); - shouldUpdateIntent = true; - } else if (field.startsWith("subject=")) { - final String subject = Uri.decode(field.substring(8)); - intent.putExtra("subject", subject); - shouldUpdateIntent = true; - } else if (field.startsWith("cc=")) { - final String ccNumber = Uri.decode(field.substring(3)); - String phoneNumber = uri.getAuthority(); - if (phoneNumber != null) { - uri = uri.buildUpon().encodedAuthority(phoneNumber + ";" + ccNumber).build(); - } - shouldUpdateIntent = true; - } else { - resultQuery = resultQuery.concat(resultQuery.length() > 0 ? "&" + field : field); - } - } - - if (!shouldUpdateIntent) { - // No need to rewrite the URI, then. - return intent; - } - - // Form a new URI without the extracted fields in the query part, and - // push that into the new Intent. - final String newQuery = resultQuery.length() > 0 ? "?" + resultQuery : ""; - final Uri pruned = uri.buildUpon().encodedQuery(newQuery).build(); - intent.setData(pruned); - - return intent; - } - - // We create a separate method to better encapsulate the @TargetApi use. - @TargetApi(15) - private static void nullIntentSelector(final Intent intent) { - intent.setSelector(null); - } - - /** - * Return a Uri instance which is equivalent to u, - * but with a guaranteed-lowercase scheme as if the API level 16 method - * u.normalizeScheme had been called. - * - * @param u the Uri to normalize. - * @return a Uri, which might be u. - */ - private static Uri normalizeUriScheme(final Uri u) { - final String scheme = u.getScheme(); - final String lower = scheme.toLowerCase(Locale.US); - if (lower.equals(scheme)) { - return u; - } - - // Otherwise, return a new URI with a normalized scheme. - return u.buildUpon().scheme(lower).build(); - } - - @Override - public void handleMessage(final String event, final NativeJSObject message, final EventCallback callback) { - if (event.equals("Intent:OpenNoHandler")) { - openNoHandler(message, callback); - } - } - - @Override - public void handleMessage(String event, JSONObject message) { - try { - if (event.equals("Intent:GetHandlers")) { - getHandlers(message); - } else if (event.equals("Intent:Open")) { - open(message); - } else if (event.equals("Intent:OpenForResult")) { - openForResult(message); - } - } catch (JSONException e) { - Log.e(LOGTAG, "Exception handling message \"" + event + "\":", e); - } - } - - private void getHandlers(JSONObject message) throws JSONException { - final Intent intent = getOpenURIIntent(activity, - message.optString("url"), - message.optString("mime"), - message.optString("action"), - message.optString("title")); - final List appList = Arrays.asList(getHandlersForIntent(intent)); - - final JSONObject response = new JSONObject(); - response.put("apps", new JSONArray(appList)); - EventDispatcher.sendResponse(message, response); - } - - private void open(JSONObject message) throws JSONException { - openUriExternal(message.optString("url"), - message.optString("mime"), - message.optString("packageName"), - message.optString("className"), - message.optString("action"), - message.optString("title"), false); - } - - private void openForResult(final JSONObject message) throws JSONException { - Intent intent = getOpenURIIntent(activity, - message.optString("url"), - message.optString("mime"), - message.optString("action"), - message.optString("title")); - intent.setClassName(message.optString("packageName"), message.optString("className")); - intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); - - final ResultHandler handler = new ResultHandler(message); - try { - ActivityHandlerHelper.startIntentForActivity(activity, intent, handler); - } catch (SecurityException e) { - Log.w(LOGTAG, "Forbidden to launch activity.", e); - } - } - - /** - * Opens a URI without any valid handlers on device. In the best case, a package is specified - * and we can bring the user directly to the application page in an app market. If a package is - * not specified and there is a fallback url in the intent extras, we open that url. If neither - * is present, we alert the user that we were unable to open the link. - * - * @param msg A message with the uri with no handlers as the value for the "uri" key - * @param callback A callback that will be called with success & no params if Java loads a page, or with error and - * the uri to load if Java does not load a page - */ - private void openNoHandler(final NativeJSObject msg, final EventCallback callback) { - final String uri = msg.getString("uri"); - - if (TextUtils.isEmpty(uri)) { - Log.w(LOGTAG, "Received empty URL - loading about:neterror"); - callback.sendError(getUnknownProtocolErrorPageUri("")); - return; - } - - final Intent intent; - try { - // TODO (bug 1173626): This will not handle android-app uris on non 5.1 devices. - intent = Intent.parseUri(uri, 0); - } catch (final URISyntaxException e) { - String errorUri; - try { - errorUri = getUnknownProtocolErrorPageUri(URLEncoder.encode(uri, "UTF-8")); - } catch (final UnsupportedEncodingException encodingE) { - errorUri = getUnknownProtocolErrorPageUri(""); - } - - // Don't log the exception to prevent leaking URIs. - Log.w(LOGTAG, "Unable to parse Intent URI - loading about:neterror"); - callback.sendError(errorUri); - return; - } - - // For this flow, we follow Chrome's lead: - // https://developer.chrome.com/multidevice/android/intents - final String fallbackUrl = intent.getStringExtra(EXTRA_BROWSER_FALLBACK_URL); - if (isFallbackUrlValid(fallbackUrl)) { - // Opens the page in JS. - callback.sendError(fallbackUrl); - - } else if (intent.getPackage() != null) { - // Note on alternative flows: we could get the intent package from a component, however, for - // security reasons, components are ignored when opening URIs (bug 1168998) so we should - // ignore it here too. - // - // Our old flow used to prompt the user to search for their app in the market by scheme and - // while this could help the user find a new app, there is not always a correlation in - // scheme to application name and we could end up steering the user wrong (potentially to - // malicious software). Better to leave that one alone. - final String marketUri = MARKET_INTENT_URI_PACKAGE_PREFIX + intent.getPackage(); - final Intent marketIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(marketUri)); - marketIntent.addCategory(Intent.CATEGORY_BROWSABLE); - marketIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - - // (Bug 1192436) We don't know if marketIntent matches any Activities (e.g. non-Play - // Store devices). If it doesn't, clicking the link will cause no action to occur. - ExternalIntentDuringPrivateBrowsingPromptFragment.showDialogOrAndroidChooser( - activity, activity.getSupportFragmentManager(), marketIntent); - callback.sendSuccess(null); - - } else { - // We return the error page here, but it will only be shown if we think the load did - // not come from clicking a link. Chrome does not show error pages in that case, and - // many websites have catered to this behavior. For example, the site might set a timeout and load a play - // store url for their app if the intent link fails to load, i.e. the app is not installed. - // These work-arounds would often end with our users seeing about:neterror instead of the intended experience. - // While I feel showing about:neterror is a better solution for users (when not hacked around), - // we should match the status quo for the good of our users. - // - // Don't log the URI to prevent leaking it. - Log.w(LOGTAG, "Unable to open URI, maybe showing neterror"); - callback.sendError(getUnknownProtocolErrorPageUri(intent.getData().toString())); - } - } - - private static boolean isFallbackUrlValid(@Nullable final String fallbackUrl) { - if (fallbackUrl == null) { - return false; - } - - try { - final String anyCaseScheme = new URI(fallbackUrl).getScheme(); - final String scheme = (anyCaseScheme == null) ? null : anyCaseScheme.toLowerCase(Locale.US); - if ("http".equals(scheme) || "https".equals(scheme)) { - return true; - } else { - Log.w(LOGTAG, "Fallback URI uses unsupported scheme: " + scheme + ". Try http or https."); - } - } catch (final URISyntaxException e) { - // Do not include Exception to avoid leaking uris. - Log.w(LOGTAG, "URISyntaxException parsing fallback URI"); - } - return false; - } - - /** - * Returns an about:neterror uri with the unknownProtocolFound text as a parameter. - * @param encodedUri The encoded uri. While the page does not open correctly without specifying - * a uri parameter, it happily accepts the empty String so this argument may - * be the empty String. - */ - private String getUnknownProtocolErrorPageUri(final String encodedUri) { - return UNKNOWN_PROTOCOL_URI_PREFIX + encodedUri; - } - - private static class ResultHandler implements ActivityResultHandler { - private final JSONObject message; - - public ResultHandler(JSONObject message) { - this.message = message; - } - - @Override - public void onActivityResult(int resultCode, Intent data) { - JSONObject response = new JSONObject(); - try { - if (data != null) { - if (data.getExtras() != null) { - response.put("extras", JSONUtils.bundleToJSON(data.getExtras())); - } - if (data.getData() != null) { - response.put("uri", data.getData().toString()); - } - } - response.put("resultCode", resultCode); - } catch (JSONException e) { - Log.w(LOGTAG, "Error building JSON response.", e); - } - EventDispatcher.sendResponse(message, response); - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/LauncherActivity.java b/mobile/android/base/java/org/mozilla/gecko/LauncherActivity.java deleted file mode 100644 index 4de8fa423c..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/LauncherActivity.java +++ /dev/null @@ -1,110 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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; - -import android.app.Activity; -import android.content.Intent; -import android.os.Bundle; -import android.support.annotation.NonNull; -import android.support.customtabs.CustomTabsIntent; - -import org.mozilla.gecko.customtabs.CustomTabsActivity; -import org.mozilla.gecko.db.BrowserContract; -import org.mozilla.gecko.mozglue.SafeIntent; -import org.mozilla.gecko.preferences.GeckoPreferences; -import org.mozilla.gecko.tabqueue.TabQueueHelper; -import org.mozilla.gecko.tabqueue.TabQueueService; - -/** - * Activity that receives incoming Intents and dispatches them to the appropriate activities (e.g. browser, custom tabs, web app). - */ -public class LauncherActivity extends Activity { - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - GeckoAppShell.ensureCrashHandling(); - - final SafeIntent safeIntent = new SafeIntent(getIntent()); - - // If it's not a view intent, it won't be a custom tabs intent either. Just launch! - if (!isViewIntentWithURL(safeIntent)) { - dispatchNormalIntent(); - - // Is this a custom tabs intent, and are custom tabs enabled? - } else if (AppConstants.MOZ_ANDROID_CUSTOM_TABS && isCustomTabsIntent(safeIntent) - && isCustomTabsEnabled()) { - dispatchCustomTabsIntent(); - - // Can we dispatch this VIEW action intent to the tab queue service? - } else if (!safeIntent.getBooleanExtra(BrowserContract.SKIP_TAB_QUEUE_FLAG, false) - && TabQueueHelper.TAB_QUEUE_ENABLED - && TabQueueHelper.isTabQueueEnabled(this)) { - dispatchTabQueueIntent(); - - // Dispatch this VIEW action intent to the browser. - } else { - dispatchNormalIntent(); - } - - finish(); - } - - /** - * Launch tab queue service to display overlay. - */ - private void dispatchTabQueueIntent() { - Intent intent = new Intent(getIntent()); - intent.setClass(getApplicationContext(), TabQueueService.class); - startService(intent); - } - - /** - * Launch the browser activity. - */ - private void dispatchNormalIntent() { - Intent intent = new Intent(getIntent()); - intent.setClassName(getApplicationContext(), AppConstants.MOZ_ANDROID_BROWSER_INTENT_CLASS); - - filterFlags(intent); - - startActivity(intent); - } - - private void dispatchCustomTabsIntent() { - Intent intent = new Intent(getIntent()); - intent.setClassName(getApplicationContext(), CustomTabsActivity.class.getName()); - - filterFlags(intent); - - startActivity(intent); - } - - private static void filterFlags(Intent intent) { - // Explicitly remove the new task and clear task flags (Our browser activity is a single - // task activity and we never want to start a second task here). See bug 1280112. - intent.setFlags(intent.getFlags() & ~Intent.FLAG_ACTIVITY_NEW_TASK); - intent.setFlags(intent.getFlags() & ~Intent.FLAG_ACTIVITY_CLEAR_TASK); - - // LauncherActivity is started with the "exclude from recents" flag (set in manifest). We do - // not want to propagate this flag from the launcher activity to the browser. - intent.setFlags(intent.getFlags() & ~Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS); - } - - private static boolean isViewIntentWithURL(@NonNull final SafeIntent safeIntent) { - return Intent.ACTION_VIEW.equals(safeIntent.getAction()) - && safeIntent.getDataString() != null; - } - - private static boolean isCustomTabsIntent(@NonNull final SafeIntent safeIntent) { - return isViewIntentWithURL(safeIntent) - && safeIntent.hasExtra(CustomTabsIntent.EXTRA_SESSION); - } - - private boolean isCustomTabsEnabled() { - return GeckoSharedPrefs.forApp(this).getBoolean(GeckoPreferences.PREFS_CUSTOM_TABS, false); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/LocaleManager.java b/mobile/android/base/java/org/mozilla/gecko/LocaleManager.java deleted file mode 100644 index 795caa925a..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/LocaleManager.java +++ /dev/null @@ -1,42 +0,0 @@ -/* 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; - -import java.util.Locale; - -import android.content.Context; -import android.content.res.Configuration; -import android.content.res.Resources; - -/** - * Implement this interface to provide Fennec's locale switching functionality. - * - * The LocaleManager is responsible for persisting and applying selected locales, - * and correcting configurations after Android has changed them. - */ -public interface LocaleManager { - void initialize(Context context); - - /** - * @return true if locale switching is enabled. - */ - boolean isEnabled(); - Locale getCurrentLocale(Context context); - String getAndApplyPersistedLocale(Context context); - void correctLocale(Context context, Resources resources, Configuration newConfig); - void updateConfiguration(Context context, Locale locale); - String setSelectedLocale(Context context, String localeCode); - boolean systemLocaleDidChange(); - void resetToSystemLocale(Context context); - - /** - * Call this in your onConfigurationChanged handler. This method is expected - * to do the appropriate thing: if the user has selected a locale, it - * corrects the incoming configuration; if not, it signals the new locale to - * use. - */ - Locale onSystemConfigurationChanged(Context context, Resources resources, Configuration configuration, Locale currentActivityLocale); - String getFallbackLocaleTag(); -} diff --git a/mobile/android/base/java/org/mozilla/gecko/Locales.java b/mobile/android/base/java/org/mozilla/gecko/Locales.java deleted file mode 100644 index e030b95e9e..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/Locales.java +++ /dev/null @@ -1,136 +0,0 @@ -/* 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; - -import java.lang.reflect.Method; -import java.util.Locale; - -import org.mozilla.gecko.LocaleManager; - -import android.app.Activity; -import android.content.Context; -import android.os.Bundle; -import android.os.StrictMode; -import android.support.v4.app.FragmentActivity; -import android.support.v7.app.AppCompatActivity; - -/** - * This is a helper class to do typical locale switching operations without - * hitting StrictMode errors or adding boilerplate to common activity - * subclasses. - * - * Either call {@link Locales#initializeLocale(Context)} in your - * onCreate method, or inherit from - * LocaleAwareFragmentActivity or LocaleAwareActivity. - */ -public class Locales { - public static LocaleManager getLocaleManager() { - try { - final Class clazz = Class.forName("org.mozilla.gecko.BrowserLocaleManager"); - final Method getInstance = clazz.getMethod("getInstance"); - final LocaleManager localeManager = (LocaleManager) getInstance.invoke(null); - return localeManager; - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - public static void initializeLocale(Context context) { - final LocaleManager localeManager = getLocaleManager(); - final StrictMode.ThreadPolicy savedPolicy = StrictMode.allowThreadDiskReads(); - StrictMode.allowThreadDiskWrites(); - try { - localeManager.getAndApplyPersistedLocale(context); - } finally { - StrictMode.setThreadPolicy(savedPolicy); - } - } - - public static abstract class LocaleAwareAppCompatActivity extends AppCompatActivity { - @Override - protected void onCreate(Bundle savedInstanceState) { - Locales.initializeLocale(getApplicationContext()); - super.onCreate(savedInstanceState); - } - - } - public static abstract class LocaleAwareFragmentActivity extends FragmentActivity { - @Override - protected void onCreate(Bundle savedInstanceState) { - Locales.initializeLocale(getApplicationContext()); - super.onCreate(savedInstanceState); - } - } - - public static abstract class LocaleAwareActivity extends Activity { - @Override - protected void onCreate(Bundle savedInstanceState) { - Locales.initializeLocale(getApplicationContext()); - super.onCreate(savedInstanceState); - } - } - - /** - * Sometimes we want just the language for a locale, not the entire language - * tag. But Java's .getLanguage method is wrong. - * - * This method is equivalent to the first part of - * {@link Locales#getLanguageTag(Locale)}. - * - * @return a language string, such as "he" for the Hebrew locales. - */ - public static String getLanguage(final Locale locale) { - // Can, but should never be, an empty string. - final String language = locale.getLanguage(); - - // Modernize certain language codes. - if (language.equals("iw")) { - return "he"; - } - - if (language.equals("in")) { - return "id"; - } - - if (language.equals("ji")) { - return "yi"; - } - - return language; - } - - /** - * Gecko uses locale codes like "es-ES", whereas a Java {@link Locale} - * stringifies as "es_ES". - * - * This method approximates the Java 7 method - * Locale#toLanguageTag(). - * - * @return a locale string suitable for passing to Gecko. - */ - public static String getLanguageTag(final Locale locale) { - // If this were Java 7: - // return locale.toLanguageTag(); - - final String language = getLanguage(locale); - final String country = locale.getCountry(); // Can be an empty string. - if (country.equals("")) { - return language; - } - return language + "-" + country; - } - - public static Locale parseLocaleCode(final String localeCode) { - int index; - if ((index = localeCode.indexOf('-')) != -1 || - (index = localeCode.indexOf('_')) != -1) { - final String langCode = localeCode.substring(0, index); - final String countryCode = localeCode.substring(index + 1); - return new Locale(langCode, countryCode); - } - - return new Locale(localeCode); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/MediaCastingBar.java b/mobile/android/base/java/org/mozilla/gecko/MediaCastingBar.java deleted file mode 100644 index bd109058cd..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/MediaCastingBar.java +++ /dev/null @@ -1,131 +0,0 @@ -/* 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; - -import org.mozilla.gecko.GeckoApp; -import org.mozilla.gecko.util.GeckoEventListener; -import org.mozilla.gecko.util.ThreadUtils; - -import org.json.JSONObject; - -import android.content.Context; -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.RelativeLayout; -import android.widget.TextView; - -public class MediaCastingBar extends RelativeLayout implements View.OnClickListener, GeckoEventListener { - private static final String LOGTAG = "GeckoMediaCastingBar"; - - private TextView mCastingTo; - private ImageButton mMediaPlay; - private ImageButton mMediaPause; - private ImageButton mMediaStop; - - private boolean mInflated; - - public MediaCastingBar(Context context, AttributeSet attrs) { - super(context, attrs); - - GeckoApp.getEventDispatcher().registerGeckoThreadListener(this, - "Casting:Started", - "Casting:Paused", - "Casting:Playing", - "Casting:Stopped"); - } - - public void inflateContent() { - LayoutInflater inflater = LayoutInflater.from(getContext()); - View content = inflater.inflate(R.layout.media_casting, this); - - mMediaPlay = (ImageButton) content.findViewById(R.id.media_play); - mMediaPlay.setOnClickListener(this); - mMediaPause = (ImageButton) content.findViewById(R.id.media_pause); - mMediaPause.setOnClickListener(this); - mMediaStop = (ImageButton) content.findViewById(R.id.media_stop); - mMediaStop.setOnClickListener(this); - - mCastingTo = (TextView) content.findViewById(R.id.media_sending_to); - - // Capture clicks on the rest of the view to prevent them from - // leaking into other views positioned below. - content.setOnClickListener(this); - - mInflated = true; - } - - public void show() { - if (!mInflated) - inflateContent(); - - setVisibility(VISIBLE); - } - - public void hide() { - setVisibility(GONE); - } - - public void onDestroy() { - GeckoApp.getEventDispatcher().unregisterGeckoThreadListener(this, - "Casting:Started", - "Casting:Paused", - "Casting:Playing", - "Casting:Stopped"); - } - - // View.OnClickListener implementation - @Override - public void onClick(View v) { - final int viewId = v.getId(); - - if (viewId == R.id.media_play) { - GeckoAppShell.notifyObservers("Casting:Play", ""); - mMediaPlay.setVisibility(GONE); - mMediaPause.setVisibility(VISIBLE); - } else if (viewId == R.id.media_pause) { - GeckoAppShell.notifyObservers("Casting:Pause", ""); - mMediaPause.setVisibility(GONE); - mMediaPlay.setVisibility(VISIBLE); - } else if (viewId == R.id.media_stop) { - GeckoAppShell.notifyObservers("Casting:Stop", ""); - } - } - - // GeckoEventListener implementation - @Override - public void handleMessage(final String event, final JSONObject message) { - final String device = message.optString("device"); - - ThreadUtils.postToUiThread(new Runnable() { - @Override - public void run() { - if (event.equals("Casting:Started")) { - show(); - if (!TextUtils.isEmpty(device)) { - mCastingTo.setText(device); - } else { - // Should not happen - mCastingTo.setText(""); - Log.d(LOGTAG, "Device name is empty."); - } - mMediaPlay.setVisibility(GONE); - mMediaPause.setVisibility(VISIBLE); - } else if (event.equals("Casting:Paused")) { - mMediaPause.setVisibility(GONE); - mMediaPlay.setVisibility(VISIBLE); - } else if (event.equals("Casting:Playing")) { - mMediaPlay.setVisibility(GONE); - mMediaPause.setVisibility(VISIBLE); - } else if (event.equals("Casting:Stopped")) { - hide(); - } - } - }); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/MediaPlayerManager.java b/mobile/android/base/java/org/mozilla/gecko/MediaPlayerManager.java deleted file mode 100644 index fc0ce82cfd..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/MediaPlayerManager.java +++ /dev/null @@ -1,323 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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; - -import android.os.Bundle; -import android.support.v4.app.Fragment; -import android.support.v7.media.MediaControlIntent; -import android.support.v7.media.MediaRouteSelector; -import android.support.v7.media.MediaRouter; -import android.support.v7.media.MediaRouter.RouteInfo; -import android.util.Log; - -import com.google.android.gms.cast.CastMediaControlIntent; - -import org.json.JSONObject; -import org.mozilla.gecko.annotation.JNITarget; -import org.mozilla.gecko.annotation.ReflectionTarget; -import org.mozilla.gecko.AppConstants.Versions; -import org.mozilla.gecko.util.EventCallback; -import org.mozilla.gecko.util.NativeEventListener; -import org.mozilla.gecko.util.NativeJSObject; - -import java.util.HashMap; -import java.util.Iterator; -import java.util.Map; - -/** - * Manages a list of GeckoMediaPlayers methods (i.e. Chromecast/Miracast). Routes messages - * from Gecko to the correct caster based on the id of the display - */ -public class MediaPlayerManager extends Fragment implements NativeEventListener { - /** - * Create a new instance of DetailsFragment, initialized to - * show the text at 'index'. - */ - - private static MediaPlayerManager instance = null; - - @ReflectionTarget - public static MediaPlayerManager getInstance() { - if (instance != null) { - return instance; - } - if (Versions.feature17Plus) { - instance = (MediaPlayerManager) new PresentationMediaPlayerManager(); - } else { - instance = new MediaPlayerManager(); - } - return instance; - } - - private static final String LOGTAG = "GeckoMediaPlayerManager"; - protected boolean isPresentationMode = false; // Used to prevent mirroring when Presentation API is used. - - @ReflectionTarget - public static final String MEDIA_PLAYER_TAG = "MPManagerFragment"; - - private static final boolean SHOW_DEBUG = false; - // Simplified debugging interfaces - private static void debug(String msg, Exception e) { - if (SHOW_DEBUG) { - Log.e(LOGTAG, msg, e); - } - } - - private static void debug(String msg) { - if (SHOW_DEBUG) { - Log.d(LOGTAG, msg); - } - } - - protected MediaRouter mediaRouter = null; - protected final Map players = new HashMap(); - protected final Map displays = new HashMap(); // used for Presentation API - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - GeckoApp.getEventDispatcher().registerGeckoThreadListener(this, - "MediaPlayer:Load", - "MediaPlayer:Start", - "MediaPlayer:Stop", - "MediaPlayer:Play", - "MediaPlayer:Pause", - "MediaPlayer:End", - "MediaPlayer:Mirror", - "MediaPlayer:Message", - "AndroidCastDevice:Start", - "AndroidCastDevice:Stop", - "AndroidCastDevice:SyncDevice"); - } - - @Override - @JNITarget - public void onDestroy() { - super.onDestroy(); - GeckoApp.getEventDispatcher().unregisterGeckoThreadListener(this, - "MediaPlayer:Load", - "MediaPlayer:Start", - "MediaPlayer:Stop", - "MediaPlayer:Play", - "MediaPlayer:Pause", - "MediaPlayer:End", - "MediaPlayer:Mirror", - "MediaPlayer:Message", - "AndroidCastDevice:Start", - "AndroidCastDevice:Stop", - "AndroidCastDevice:SyncDevice"); - } - - // GeckoEventListener implementation - @Override - public void handleMessage(String event, final NativeJSObject message, final EventCallback callback) { - debug(event); - if (event.startsWith("MediaPlayer:")) { - final GeckoMediaPlayer player = players.get(message.getString("id")); - if (player == null) { - Log.e(LOGTAG, "Couldn't find a player for this id: " + message.getString("id") + " for message: " + event); - if (callback != null) { - callback.sendError(null); - } - return; - } - - if ("MediaPlayer:Play".equals(event)) { - player.play(callback); - } else if ("MediaPlayer:Start".equals(event)) { - player.start(callback); - } else if ("MediaPlayer:Stop".equals(event)) { - player.stop(callback); - } else if ("MediaPlayer:Pause".equals(event)) { - player.pause(callback); - } else if ("MediaPlayer:End".equals(event)) { - player.end(callback); - } else if ("MediaPlayer:Mirror".equals(event)) { - player.mirror(callback); - } else if ("MediaPlayer:Message".equals(event) && message.has("data")) { - player.message(message.getString("data"), callback); - } else if ("MediaPlayer:Load".equals(event)) { - final String url = message.optString("source", ""); - final String type = message.optString("type", "video/mp4"); - final String title = message.optString("title", ""); - player.load(title, url, type, callback); - } - } - - if (event.startsWith("AndroidCastDevice:")) { - if ("AndroidCastDevice:Start".equals(event)) { - final GeckoPresentationDisplay display = displays.get(message.getString("id")); - if (display == null) { - Log.e(LOGTAG, "Couldn't find a display for this id: " + message.getString("id") + " for message: " + event); - return; - } - display.start(callback); - } else if ("AndroidCastDevice:Stop".equals(event)) { - final GeckoPresentationDisplay display = displays.get(message.getString("id")); - if (display == null) { - Log.e(LOGTAG, "Couldn't find a display for this id: " + message.getString("id") + " for message: " + event); - return; - } - display.stop(callback); - } else if ("AndroidCastDevice:SyncDevice".equals(event)) { - for (Map.Entry entry : displays.entrySet()) { - GeckoPresentationDisplay display = entry.getValue(); - JSONObject json = display.toJSON(); - if (json == null) { - break; - } - GeckoAppShell.notifyObservers("AndroidCastDevice:Added", json.toString()); - } - } - } - } - - private final MediaRouter.Callback callback = - new MediaRouter.Callback() { - @Override - public void onRouteRemoved(MediaRouter router, RouteInfo route) { - debug("onRouteRemoved: route=" + route); - - // Remove from media player list. - players.remove(route.getId()); - GeckoAppShell.notifyObservers("MediaPlayer:Removed", route.getId()); - updatePresentation(); - - // Remove from presentation display list. - displays.remove(route.getId()); - GeckoAppShell.notifyObservers("AndroidCastDevice:Removed", route.getId()); - } - - @SuppressWarnings("unused") - public void onRouteSelected(MediaRouter router, int type, MediaRouter.RouteInfo route) { - updatePresentation(); - } - - // These methods aren't used by the support version Media Router - @SuppressWarnings("unused") - public void onRouteUnselected(MediaRouter router, int type, RouteInfo route) { - updatePresentation(); - } - - @Override - public void onRoutePresentationDisplayChanged(MediaRouter router, RouteInfo route) { - updatePresentation(); - } - - @Override - public void onRouteVolumeChanged(MediaRouter router, RouteInfo route) { - } - - @Override - public void onRouteAdded(MediaRouter router, MediaRouter.RouteInfo route) { - debug("onRouteAdded: route=" + route); - final GeckoMediaPlayer player = getMediaPlayerForRoute(route); - saveAndNotifyOfPlayer("MediaPlayer:Added", route, player); - updatePresentation(); - - final GeckoPresentationDisplay display = getPresentationDisplayForRoute(route); - saveAndNotifyOfDisplay("AndroidCastDevice:Added", route, display); - } - - @Override - public void onRouteChanged(MediaRouter router, MediaRouter.RouteInfo route) { - debug("onRouteChanged: route=" + route); - final GeckoMediaPlayer player = players.get(route.getId()); - saveAndNotifyOfPlayer("MediaPlayer:Changed", route, player); - updatePresentation(); - - final GeckoPresentationDisplay display = displays.get(route.getId()); - saveAndNotifyOfDisplay("AndroidCastDevice:Changed", route, display); - } - - private void saveAndNotifyOfPlayer(final String eventName, - MediaRouter.RouteInfo route, - final GeckoMediaPlayer player) { - if (player == null) { - return; - } - - final JSONObject json = player.toJSON(); - if (json == null) { - return; - } - - players.put(route.getId(), player); - GeckoAppShell.notifyObservers(eventName, json.toString()); - } - - private void saveAndNotifyOfDisplay(final String eventName, - MediaRouter.RouteInfo route, - final GeckoPresentationDisplay display) { - if (display == null) { - return; - } - - final JSONObject json = display.toJSON(); - if (json == null) { - return; - } - - displays.put(route.getId(), display); - GeckoAppShell.notifyObservers(eventName, json.toString()); - } - }; - - private GeckoMediaPlayer getMediaPlayerForRoute(MediaRouter.RouteInfo route) { - try { - if (route.supportsControlCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK)) { - return new ChromeCastPlayer(getActivity(), route); - } - } catch (Exception ex) { - debug("Error handling presentation", ex); - } - - return null; - } - - private GeckoPresentationDisplay getPresentationDisplayForRoute(MediaRouter.RouteInfo route) { - try { - if (route.supportsControlCategory(CastMediaControlIntent.categoryForCast(ChromeCastDisplay.REMOTE_DISPLAY_APP_ID))) { - return new ChromeCastDisplay(getActivity(), route); - } - } catch (Exception ex) { - debug("Error handling presentation", ex); - } - return null; - } - - @Override - public void onPause() { - super.onPause(); - mediaRouter.removeCallback(callback); - mediaRouter = null; - } - - @Override - public void onResume() { - super.onResume(); - - // The mediaRouter shouldn't exist here, but this is a nice safety check. - if (mediaRouter != null) { - return; - } - - mediaRouter = MediaRouter.getInstance(getActivity()); - final MediaRouteSelector selectorBuilder = new MediaRouteSelector.Builder() - .addControlCategory(MediaControlIntent.CATEGORY_LIVE_VIDEO) - .addControlCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK) - .addControlCategory(CastMediaControlIntent.categoryForCast(ChromeCastPlayer.MIRROR_RECEIVER_APP_ID)) - .addControlCategory(CastMediaControlIntent.categoryForCast(ChromeCastDisplay.REMOTE_DISPLAY_APP_ID)) - .build(); - mediaRouter.addCallback(selectorBuilder, callback, MediaRouter.CALLBACK_FLAG_REQUEST_DISCOVERY); - } - - public void setPresentationMode(boolean isPresentationMode) { - this.isPresentationMode = isPresentationMode; - } - - protected void updatePresentation() { /* Overridden in sub-classes. */ } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/MemoryMonitor.java b/mobile/android/base/java/org/mozilla/gecko/MemoryMonitor.java deleted file mode 100644 index 94ca761b96..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/MemoryMonitor.java +++ /dev/null @@ -1,279 +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; - -import org.mozilla.gecko.annotation.WrapForJNI; -import org.mozilla.gecko.db.BrowserDB; -import org.mozilla.gecko.db.BrowserContract; -import org.mozilla.gecko.db.BrowserProvider; -import org.mozilla.gecko.home.ImageLoader; -import org.mozilla.gecko.icons.storage.MemoryStorage; -import org.mozilla.gecko.util.ThreadUtils; - -import android.content.BroadcastReceiver; -import android.content.ComponentCallbacks2; -import android.content.ContentResolver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.support.v4.content.LocalBroadcastManager; -import android.util.Log; - -/** - * This is a utility class to keep track of how much memory and disk-space pressure - * the system is under. It receives input from GeckoActivity via the onLowMemory() and - * onTrimMemory() functions, and also listens for some system intents related to - * disk-space notifications. Internally it will track how much memory and disk pressure - * the system is under, and perform various actions to help alleviate the pressure. - * - * Note that since there is no notification for when the system has lots of free memory - * again, this class also assumes that, over time, the system will free up memory. This - * assumption is implemented using a timer that slowly lowers the internal memory - * pressure state if no new low-memory notifications are received. - * - * Synchronization note: MemoryMonitor contains an inner class PressureDecrementer. Both - * of these classes may be accessed from various threads, and have both been designed to - * be thread-safe. In terms of lock ordering, code holding the PressureDecrementer lock - * is allowed to pick up the MemoryMonitor lock, but not vice-versa. - */ -class MemoryMonitor extends BroadcastReceiver { - private static final String LOGTAG = "GeckoMemoryMonitor"; - private static final String ACTION_MEMORY_DUMP = "org.mozilla.gecko.MEMORY_DUMP"; - private static final String ACTION_FORCE_PRESSURE = "org.mozilla.gecko.FORCE_MEMORY_PRESSURE"; - - // Memory pressure levels. Keep these in sync with those in AndroidJavaWrappers.h - private static final int MEMORY_PRESSURE_NONE = 0; - private static final int MEMORY_PRESSURE_CLEANUP = 1; - private static final int MEMORY_PRESSURE_LOW = 2; - private static final int MEMORY_PRESSURE_MEDIUM = 3; - private static final int MEMORY_PRESSURE_HIGH = 4; - - private static final MemoryMonitor sInstance = new MemoryMonitor(); - - static MemoryMonitor getInstance() { - return sInstance; - } - - private Context mAppContext; - private final PressureDecrementer mPressureDecrementer; - private int mMemoryPressure; // Synchronized access only. - private volatile boolean mStoragePressure; // Accessed via UI thread intent, background runnables. - private boolean mInited; - - private MemoryMonitor() { - mPressureDecrementer = new PressureDecrementer(); - mMemoryPressure = MEMORY_PRESSURE_NONE; - } - - public void init(final Context context) { - if (mInited) { - return; - } - - mAppContext = context.getApplicationContext(); - IntentFilter filter = new IntentFilter(); - filter.addAction(Intent.ACTION_DEVICE_STORAGE_LOW); - filter.addAction(Intent.ACTION_DEVICE_STORAGE_OK); - filter.addAction(ACTION_MEMORY_DUMP); - filter.addAction(ACTION_FORCE_PRESSURE); - mAppContext.registerReceiver(this, filter); - mInited = true; - } - - public void onLowMemory() { - Log.d(LOGTAG, "onLowMemory() notification received"); - if (increaseMemoryPressure(MEMORY_PRESSURE_HIGH)) { - // We need to wait on Gecko here, because if we haven't reduced - // memory usage enough when we return from this, Android will kill us. - if (GeckoThread.isStateAtLeast(GeckoThread.State.PROFILE_READY)) { - GeckoThread.waitOnGecko(); - } - } - } - - public void onTrimMemory(int level) { - Log.d(LOGTAG, "onTrimMemory() notification received with level " + level); - if (level == ComponentCallbacks2.TRIM_MEMORY_COMPLETE) { - // We seem to get this just by entering the task switcher or hitting the home button. - // Seems bogus, because we are the foreground app, or at least not at the end of the LRU list. - // Just ignore it, and if there is a real memory pressure event (CRITICAL, MODERATE, etc), - // we'll respond appropriately. - return; - } - - switch (level) { - case ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL: - case ComponentCallbacks2.TRIM_MEMORY_MODERATE: - // TRIM_MEMORY_MODERATE is the highest level we'll respond to while backgrounded - increaseMemoryPressure(MEMORY_PRESSURE_HIGH); - break; - case ComponentCallbacks2.TRIM_MEMORY_RUNNING_MODERATE: - increaseMemoryPressure(MEMORY_PRESSURE_MEDIUM); - break; - case ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW: - increaseMemoryPressure(MEMORY_PRESSURE_LOW); - break; - case ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN: - case ComponentCallbacks2.TRIM_MEMORY_BACKGROUND: - increaseMemoryPressure(MEMORY_PRESSURE_CLEANUP); - break; - default: - Log.d(LOGTAG, "Unhandled onTrimMemory() level " + level); - break; - } - } - - @Override - public void onReceive(Context context, Intent intent) { - if (Intent.ACTION_DEVICE_STORAGE_LOW.equals(intent.getAction())) { - Log.d(LOGTAG, "Device storage is low"); - mStoragePressure = true; - ThreadUtils.postToBackgroundThread(new StorageReducer(context)); - } else if (Intent.ACTION_DEVICE_STORAGE_OK.equals(intent.getAction())) { - Log.d(LOGTAG, "Device storage is ok"); - mStoragePressure = false; - } else if (ACTION_MEMORY_DUMP.equals(intent.getAction())) { - String label = intent.getStringExtra("label"); - if (label == null) { - label = "default"; - } - GeckoAppShell.notifyObservers("Memory:Dump", label); - } else if (ACTION_FORCE_PRESSURE.equals(intent.getAction())) { - increaseMemoryPressure(MEMORY_PRESSURE_HIGH); - } - } - - @WrapForJNI(calledFrom = "ui") - private static native void dispatchMemoryPressure(); - - private boolean increaseMemoryPressure(int level) { - int oldLevel; - synchronized (this) { - // bump up our level if we're not already higher - if (mMemoryPressure > level) { - return false; - } - oldLevel = mMemoryPressure; - mMemoryPressure = level; - } - - Log.d(LOGTAG, "increasing memory pressure to " + level); - - // since we don't get notifications for when memory pressure is off, - // we schedule our own timer to slowly back off the memory pressure level. - // note that this will reset the time to next decrement if the decrementer - // is already running, which is the desired behaviour because we just got - // a new low-mem notification. - mPressureDecrementer.start(); - - if (oldLevel == level) { - // if we're not going to a higher level we probably don't - // need to run another round of the same memory reductions - // we did on the last memory pressure increase. - return false; - } - - // TODO hook in memory-reduction stuff for different levels here - if (level >= MEMORY_PRESSURE_MEDIUM) { - //Only send medium or higher events because that's all that is used right now - if (GeckoThread.isRunning()) { - dispatchMemoryPressure(); - } - - MemoryStorage.get().evictAll(); - ImageLoader.clearLruCache(); - LocalBroadcastManager.getInstance(mAppContext) - .sendBroadcast(new Intent(BrowserProvider.ACTION_SHRINK_MEMORY)); - } - return true; - } - - /** - * Thread-safe due to mStoragePressure's volatility. - */ - boolean isUnderStoragePressure() { - return mStoragePressure; - } - - private boolean decreaseMemoryPressure() { - int newLevel; - synchronized (this) { - if (mMemoryPressure <= 0) { - return false; - } - - newLevel = --mMemoryPressure; - } - Log.d(LOGTAG, "Decreased memory pressure to " + newLevel); - - return true; - } - - class PressureDecrementer implements Runnable { - private static final int DECREMENT_DELAY = 5 * 60 * 1000; // 5 minutes - - private boolean mPosted; - - synchronized void start() { - if (mPosted) { - // cancel the old one before scheduling a new one - ThreadUtils.getBackgroundHandler().removeCallbacks(this); - } - ThreadUtils.getBackgroundHandler().postDelayed(this, DECREMENT_DELAY); - mPosted = true; - } - - @Override - public synchronized void run() { - if (!decreaseMemoryPressure()) { - // done decrementing, bail out - mPosted = false; - return; - } - - // need to keep decrementing - ThreadUtils.getBackgroundHandler().postDelayed(this, DECREMENT_DELAY); - } - } - - private static class StorageReducer implements Runnable { - private final Context mContext; - private final BrowserDB mDB; - - public StorageReducer(final Context context) { - this.mContext = context; - // Since this may be called while Fennec is in the background, we don't want to risk accidentally - // using the wrong context. If the profile we get is a guest profile, use the default profile instead. - GeckoProfile profile = GeckoProfile.get(mContext); - if (profile.inGuestMode()) { - // If it was the guest profile, switch to the default one. - profile = GeckoProfile.get(mContext, GeckoProfile.DEFAULT_PROFILE); - } - - mDB = BrowserDB.from(profile); - } - - @Override - public void run() { - // this might get run right on startup, if so wait 10 seconds and try again - if (!GeckoThread.isRunning()) { - ThreadUtils.getBackgroundHandler().postDelayed(this, 10000); - return; - } - - if (!MemoryMonitor.getInstance().isUnderStoragePressure()) { - // Pressure is off, so we can abort. - return; - } - - final ContentResolver cr = mContext.getContentResolver(); - mDB.expireHistory(cr, BrowserContract.ExpirePriority.AGGRESSIVE); - mDB.removeThumbnails(cr); - - // TODO: drop or shrink disk caches - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/MotionEventInterceptor.java b/mobile/android/base/java/org/mozilla/gecko/MotionEventInterceptor.java deleted file mode 100644 index 814c09995c..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/MotionEventInterceptor.java +++ /dev/null @@ -1,13 +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; - -import android.view.MotionEvent; -import android.view.View; - -public interface MotionEventInterceptor { - public boolean onInterceptMotionEvent(View view, MotionEvent event); -} diff --git a/mobile/android/base/java/org/mozilla/gecko/PackageReplacedReceiver.java b/mobile/android/base/java/org/mozilla/gecko/PackageReplacedReceiver.java deleted file mode 100644 index 37dd8c304d..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/PackageReplacedReceiver.java +++ /dev/null @@ -1,38 +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; - -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.util.Log; - -import org.mozilla.gecko.mozglue.GeckoLoader; - -/** - * This broadcast receiver receives ACTION_MY_PACKAGE_REPLACED broadcasts and - * starts procedures that should run after the APK has been updated. - */ -public class PackageReplacedReceiver extends BroadcastReceiver { - public static final String ACTION_MY_PACKAGE_REPLACED = "android.intent.action.MY_PACKAGE_REPLACED"; - - @Override - public void onReceive(Context context, Intent intent) { - if (intent == null || !ACTION_MY_PACKAGE_REPLACED.equals(intent.getAction())) { - // This is not the broadcast we are looking for. - return; - } - - // Extract Gecko libs to allow them to be loaded from cache on startup. - extractGeckoLibs(context); - } - - private static void extractGeckoLibs(final Context context) { - final String resourcePath = context.getPackageResourcePath(); - GeckoLoader.loadMozGlue(context); - GeckoLoader.extractGeckoLibs(context, resourcePath); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/PresentationMediaPlayerManager.java b/mobile/android/base/java/org/mozilla/gecko/PresentationMediaPlayerManager.java deleted file mode 100644 index e44096489b..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/PresentationMediaPlayerManager.java +++ /dev/null @@ -1,149 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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; - -import android.annotation.TargetApi; -import android.app.Presentation; -import android.content.Context; -import android.os.Bundle; -import android.support.v7.media.MediaRouter; -import android.util.Log; -import android.view.Display; -import android.view.Surface; -import android.view.SurfaceHolder; -import android.view.SurfaceView; -import android.view.ViewGroup; -import android.view.WindowManager; - -import org.mozilla.gecko.AppConstants.Versions; - -import org.mozilla.gecko.annotation.WrapForJNI; - -/** - * A MediaPlayerManager with API 17+ Presentation support. - */ -@TargetApi(17) -public class PresentationMediaPlayerManager extends MediaPlayerManager { - - private static final String LOGTAG = "Gecko" + PresentationMediaPlayerManager.class.getSimpleName(); - - private GeckoPresentation presentation; - - public PresentationMediaPlayerManager() { - if (!Versions.feature17Plus) { - throw new IllegalStateException(PresentationMediaPlayerManager.class.getSimpleName() + - " does not support < API 17"); - } - } - - @Override - public void onStop() { - super.onStop(); - if (presentation != null) { - presentation.dismiss(); - presentation = null; - } - } - - @Override - protected void updatePresentation() { - if (mediaRouter == null) { - return; - } - - if (isPresentationMode) { - return; - } - - MediaRouter.RouteInfo route = mediaRouter.getSelectedRoute(); - Display display = route != null ? route.getPresentationDisplay() : null; - - if (display != null) { - if ((presentation != null) && (presentation.getDisplay() != display)) { - presentation.dismiss(); - presentation = null; - } - - if (presentation == null) { - final GeckoView geckoView = (GeckoView) getActivity().findViewById(R.id.layer_view); - presentation = new GeckoPresentation(getActivity(), display, geckoView); - - try { - presentation.show(); - } catch (WindowManager.InvalidDisplayException ex) { - Log.w(LOGTAG, "Couldn't show presentation! Display was removed in " - + "the meantime.", ex); - presentation = null; - } - } - } else if (presentation != null) { - presentation.dismiss(); - presentation = null; - } - } - - @WrapForJNI(calledFrom = "ui") - /* protected */ static native void invalidateAndScheduleComposite(GeckoView geckoView); - - @WrapForJNI(calledFrom = "ui") - /* protected */ static native void addPresentationSurface(GeckoView geckoView, Surface surface); - - @WrapForJNI(calledFrom = "ui") - /* protected */ static native void removePresentationSurface(); - - private static final class GeckoPresentation extends Presentation { - private SurfaceView mView; - private GeckoView mGeckoView; - - public GeckoPresentation(Context context, Display display, GeckoView geckoView) { - super(context, display); - - mGeckoView = geckoView; - } - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - mView = new SurfaceView(getContext()); - setContentView(mView, new ViewGroup.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.MATCH_PARENT)); - mView.getHolder().addCallback(new SurfaceListener(mGeckoView)); - } - } - - private static final class SurfaceListener implements SurfaceHolder.Callback { - private GeckoView mGeckoView; - - public SurfaceListener(GeckoView geckoView) { - mGeckoView = geckoView; - } - - @Override - public void surfaceChanged(SurfaceHolder holder, int format, int width, - int height) { - // Surface changed so force a composite - if (GeckoThread.isStateAtLeast(GeckoThread.State.PROFILE_READY)) { - invalidateAndScheduleComposite(mGeckoView); - } - } - - @Override - public void surfaceCreated(SurfaceHolder holder) { - if (GeckoThread.isStateAtLeast(GeckoThread.State.PROFILE_READY)) { - addPresentationSurface(mGeckoView, holder.getSurface()); - } - } - - @Override - public void surfaceDestroyed(SurfaceHolder holder) { - if (GeckoThread.isStateAtLeast(GeckoThread.State.PROFILE_READY)) { - removePresentationSurface(); - } - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/PresentationView.java b/mobile/android/base/java/org/mozilla/gecko/PresentationView.java deleted file mode 100644 index 3e5b5ffb30..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/PresentationView.java +++ /dev/null @@ -1,27 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- - * vim: ts=4 sw=4 expandtab: - * 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; - -import org.mozilla.gecko.annotation.WrapForJNI; -import org.mozilla.gecko.GeckoThread; -import org.mozilla.gecko.GeckoView; -import org.mozilla.gecko.ScreenManagerHelper; - -import android.content.Context; -import android.util.AttributeSet; -import android.util.DisplayMetrics; - -public class PresentationView extends GeckoView { - private static final String LOGTAG = "PresentationView"; - private static final String presentationViewURI = "chrome://browser/content/PresentationView.xul"; - - public PresentationView(Context context, String deviceId, int screenId) { - super(context); - this.chromeURI = presentationViewURI + "#" + deviceId; - this.screenId = screenId; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/PrintHelper.java b/mobile/android/base/java/org/mozilla/gecko/PrintHelper.java deleted file mode 100644 index 077b2d29b4..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/PrintHelper.java +++ /dev/null @@ -1,124 +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; - -import org.mozilla.gecko.AppConstants.Versions; -import org.mozilla.gecko.GeckoAppShell; -import org.mozilla.gecko.util.GeckoRequest; -import org.mozilla.gecko.util.IOUtils; -import org.mozilla.gecko.util.NativeJSObject; -import org.mozilla.gecko.util.ThreadUtils; - -import org.json.JSONException; -import org.json.JSONObject; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.InputStream; -import java.io.IOException; -import java.io.OutputStream; - -import android.content.Context; -import android.os.Bundle; -import android.os.CancellationSignal; -import android.os.ParcelFileDescriptor; -import android.print.PrintAttributes; -import android.print.PrintDocumentAdapter; -import android.print.PrintDocumentAdapter.LayoutResultCallback; -import android.print.PrintDocumentAdapter.WriteResultCallback; -import android.print.PrintDocumentInfo; -import android.print.PrintManager; -import android.print.PageRange; -import android.util.Log; - -public class PrintHelper { - private static final String LOGTAG = "GeckoPrintUtils"; - - public static void printPDF(final Context context) { - GeckoAppShell.sendRequestToGecko(new GeckoRequest("Print:PDF", new JSONObject()) { - @Override - public void onResponse(NativeJSObject nativeJSObject) { - final String filePath = nativeJSObject.getString("file"); - final String title = nativeJSObject.getString("title"); - finish(context, filePath, title); - } - - @Override - public void onError(NativeJSObject error) { - // Gecko didn't respond due to state change, javascript error, etc. - Log.d(LOGTAG, "No response from Gecko on request to generate a PDF"); - } - - private void finish(final Context context, final String filePath, final String title) { - PrintManager printManager = (PrintManager) context.getSystemService(Context.PRINT_SERVICE); - String jobName = title; - - // The adapter methods are all called on the UI thread by the PrintManager. Put the heavyweight code - // in onWrite on the background thread. - PrintDocumentAdapter pda = new PrintDocumentAdapter() { - @Override - public void onWrite(final PageRange[] pages, final ParcelFileDescriptor destination, final CancellationSignal cancellationSignal, final WriteResultCallback callback) { - ThreadUtils.postToBackgroundThread(new Runnable() { - @Override - public void run() { - InputStream input = null; - OutputStream output = null; - - try { - File pdfFile = new File(filePath); - input = new FileInputStream(pdfFile); - output = new FileOutputStream(destination.getFileDescriptor()); - - byte[] buf = new byte[8192]; - int bytesRead; - while ((bytesRead = input.read(buf)) > 0) { - output.write(buf, 0, bytesRead); - } - - callback.onWriteFinished(new PageRange[] { PageRange.ALL_PAGES }); - } catch (FileNotFoundException ee) { - Log.d(LOGTAG, "Unable to find the temporary PDF file."); - } catch (IOException ioe) { - Log.e(LOGTAG, "IOException while transferring temporary PDF file: ", ioe); - } finally { - IOUtils.safeStreamClose(input); - IOUtils.safeStreamClose(output); - } - } - }); - } - - @Override - public void onLayout(PrintAttributes oldAttributes, PrintAttributes newAttributes, CancellationSignal cancellationSignal, LayoutResultCallback callback, Bundle extras) { - if (cancellationSignal.isCanceled()) { - callback.onLayoutCancelled(); - return; - } - - PrintDocumentInfo pdi = new PrintDocumentInfo.Builder(filePath).setContentType(PrintDocumentInfo.CONTENT_TYPE_DOCUMENT).build(); - callback.onLayoutFinished(pdi, true); - } - - @Override - public void onFinish() { - // Remove the temporary file when the printing system is finished. - try { - File pdfFile = new File(filePath); - pdfFile.delete(); - } catch (NullPointerException npe) { - // Silence the exception. We only want to delete a real file. We don't - // care if the file doesn't exist. - } - } - }; - - printManager.print(jobName, pda, null); - } - }); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/PrivateTab.java b/mobile/android/base/java/org/mozilla/gecko/PrivateTab.java deleted file mode 100644 index 39b6899d3f..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/PrivateTab.java +++ /dev/null @@ -1,28 +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; - -import android.content.Context; - -import org.json.JSONObject; -import org.mozilla.gecko.db.BrowserDB; - -public class PrivateTab extends Tab { - public PrivateTab(Context context, int id, String url, boolean external, int parentId, String title) { - super(context, id, url, external, parentId, title); - } - - @Override - protected void saveThumbnailToDB(final BrowserDB db) {} - - @Override - public void setMetadata(JSONObject metadata) {} - - @Override - public boolean isPrivate() { - return true; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/RemoteClientsDialogFragment.java b/mobile/android/base/java/org/mozilla/gecko/RemoteClientsDialogFragment.java deleted file mode 100644 index b4aee9370a..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/RemoteClientsDialogFragment.java +++ /dev/null @@ -1,133 +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; - -import java.util.ArrayList; -import java.util.List; - -import org.mozilla.gecko.db.RemoteClient; - -import android.app.AlertDialog; -import android.app.AlertDialog.Builder; -import android.app.Dialog; -import android.content.DialogInterface; -import android.os.Bundle; -import android.support.v4.app.DialogFragment; -import android.support.v4.app.Fragment; -import android.util.SparseBooleanArray; - -/** - * A dialog fragment that displays a list of remote clients. - *

- * The dialog allows both single (one tap) and multiple (checkbox) selection. - * The dialog's results are communicated via the {@link RemoteClientsListener} - * interface. Either the dialog fragment's target fragment (see - * {@link Fragment#setTargetFragment(Fragment, int)}), or the containing - * activity, must implement that interface. See - * {@link #notifyListener(List)} for details. - */ -public class RemoteClientsDialogFragment extends DialogFragment { - private static final String KEY_TITLE = "title"; - private static final String KEY_CHOICE_MODE = "choice_mode"; - private static final String KEY_POSITIVE_BUTTON_TEXT = "positive_button_text"; - private static final String KEY_CLIENTS = "clients"; - - public interface RemoteClientsListener { - // Always called on the main UI thread. - public void onClients(List clients); - } - - public enum ChoiceMode { - SINGLE, - MULTIPLE, - } - - public static RemoteClientsDialogFragment newInstance(String title, String positiveButtonText, ChoiceMode choiceMode, ArrayList clients) { - final RemoteClientsDialogFragment dialog = new RemoteClientsDialogFragment(); - final Bundle args = new Bundle(); - args.putString(KEY_TITLE, title); - args.putString(KEY_POSITIVE_BUTTON_TEXT, positiveButtonText); - args.putInt(KEY_CHOICE_MODE, choiceMode.ordinal()); - args.putParcelableArrayList(KEY_CLIENTS, clients); - dialog.setArguments(args); - return dialog; - } - - public RemoteClientsDialogFragment() { - // Empty constructor is required for DialogFragment. - } - - @Override - public void onDestroy() { - super.onDestroy(); - - GeckoApplication.watchReference(getActivity(), this); - } - - protected void notifyListener(List clients) { - RemoteClientsListener listener; - try { - listener = (RemoteClientsListener) getTargetFragment(); - } catch (ClassCastException e) { - try { - listener = (RemoteClientsListener) getActivity(); - } catch (ClassCastException f) { - throw new ClassCastException(getTargetFragment() + " or " + getActivity() - + " must implement RemoteClientsListener"); - } - } - listener.onClients(clients); - } - - @Override - public Dialog onCreateDialog(Bundle savedInstanceState) { - final String title = getArguments().getString(KEY_TITLE); - final String positiveButtonText = getArguments().getString(KEY_POSITIVE_BUTTON_TEXT); - final ChoiceMode choiceMode = ChoiceMode.values()[getArguments().getInt(KEY_CHOICE_MODE)]; - final ArrayList clients = getArguments().getParcelableArrayList(KEY_CLIENTS); - - final Builder builder = new AlertDialog.Builder(getActivity()); - builder.setTitle(title); - - final String[] clientNames = new String[clients.size()]; - for (int i = 0; i < clients.size(); i++) { - clientNames[i] = clients.get(i).name; - } - - if (choiceMode == ChoiceMode.MULTIPLE) { - builder.setMultiChoiceItems(clientNames, null, null); - builder.setPositiveButton(positiveButtonText, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialogInterface, int which) { - if (which != Dialog.BUTTON_POSITIVE) { - return; - } - - final AlertDialog dialog = (AlertDialog) dialogInterface; - final SparseBooleanArray checkedItemPositions = dialog.getListView().getCheckedItemPositions(); - final ArrayList checked = new ArrayList(); - for (int i = 0; i < clients.size(); i++) { - if (checkedItemPositions.get(i)) { - checked.add(clients.get(i)); - } - } - notifyListener(checked); - } - }); - } else { - builder.setItems(clientNames, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int index) { - final ArrayList checked = new ArrayList(); - checked.add(clients.get(index)); - notifyListener(checked); - } - }); - } - - return builder.create(); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/RemotePresentationService.java b/mobile/android/base/java/org/mozilla/gecko/RemotePresentationService.java deleted file mode 100644 index b5a5527c9e..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/RemotePresentationService.java +++ /dev/null @@ -1,150 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- - * vim: ts=4 sw=4 expandtab: - * 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; - -import org.json.JSONObject; -import org.json.JSONException; - -import org.json.JSONObject; -import org.mozilla.gecko.AppConstants.Versions; -import org.mozilla.gecko.GeckoAppShell; -import org.mozilla.gecko.PresentationView; -import org.mozilla.gecko.R; -import org.mozilla.gecko.ScreenManagerHelper; -import org.mozilla.gecko.annotation.JNITarget; -import org.mozilla.gecko.annotation.ReflectionTarget; -import org.mozilla.gecko.annotation.WrapForJNI; -import org.mozilla.gecko.gfx.LayerView; -import org.mozilla.gecko.util.EventCallback; -import org.mozilla.gecko.util.NativeEventListener; -import org.mozilla.gecko.util.NativeJSObject; - -import com.google.android.gms.cast.CastMediaControlIntent; -import com.google.android.gms.cast.CastPresentation; -import com.google.android.gms.cast.CastRemoteDisplayLocalService; -import com.google.android.gms.common.ConnectionResult; -import com.google.android.gms.common.GooglePlayServicesUtil; - -import android.app.Activity; -import android.content.Context; -import android.os.Bundle; -import android.support.v4.app.Fragment; -import android.support.v7.media.MediaControlIntent; -import android.support.v7.media.MediaRouteSelector; -import android.support.v7.media.MediaRouter.RouteInfo; -import android.support.v7.media.MediaRouter; -import android.util.DisplayMetrics; -import android.util.Log; -import android.view.Display; -import android.view.ViewGroup.LayoutParams; -import android.view.WindowManager; -import android.widget.RelativeLayout; - -import java.util.HashMap; -import java.util.Map; - -/* - * Service to keep the remote display running even when the app goes into the background - */ -public class RemotePresentationService extends CastRemoteDisplayLocalService { - - private static final String LOGTAG = "RemotePresentationService"; - private CastPresentation presentation; - private String deviceId; - private int screenId; - - public void setDeviceId(String deviceId) { - this.deviceId = deviceId; - } - - public String getDeviceId() { - return deviceId; - } - - @Override - public void onCreatePresentation(Display display) { - createPresentation(); - } - - @Override - public void onDismissPresentation() { - dismissPresentation(); - } - - private void dismissPresentation() { - if (presentation != null) { - presentation.dismiss(); - presentation = null; - ScreenManagerHelper.removeDisplay(screenId); - MediaPlayerManager.getInstance().setPresentationMode(false); - } - } - - private void createPresentation() { - dismissPresentation(); - - MediaPlayerManager.getInstance().setPresentationMode(true); - - DisplayMetrics metrics = new DisplayMetrics(); - getDisplay().getMetrics(metrics); - screenId = ScreenManagerHelper.addDisplay(ScreenManagerHelper.DISPLAY_VIRTUAL, - metrics.widthPixels, - metrics.heightPixels, - metrics.density); - - VirtualPresentation virtualPresentation = new VirtualPresentation(this, getDisplay()); - virtualPresentation.setDeviceId(deviceId); - virtualPresentation.setScreenId(screenId); - presentation = (CastPresentation) virtualPresentation; - - try { - presentation.show(); - } catch (WindowManager.InvalidDisplayException ex) { - Log.e(LOGTAG, "Unable to show presentation, display was removed.", ex); - dismissPresentation(); - } - } -} - -class VirtualPresentation extends CastPresentation { - private final String LOGTAG = "VirtualPresentation"; - private RelativeLayout layout; - private PresentationView view; - private String deviceId; - private int screenId; - - public VirtualPresentation(Context context, Display display) { - super(context, display); - } - - public void setDeviceId(String deviceId) { this.deviceId = deviceId; } - public void setScreenId(int screenId) { this.screenId = screenId; } - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - /* - * NOTICE: The context get from getContext() is different to the context - * of the application. Presentaion has its own context to get correct - * resources. - */ - - // Create new PresentationView - view = new PresentationView(getContext(), deviceId, screenId); - view.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, - LayoutParams.MATCH_PARENT)); - - // Create new layout to put the GeckoView - layout = new RelativeLayout(getContext()); - layout.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, - LayoutParams.MATCH_PARENT)); - layout.addView(view); - - setContentView(layout); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/Restarter.java b/mobile/android/base/java/org/mozilla/gecko/Restarter.java deleted file mode 100644 index b049f76278..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/Restarter.java +++ /dev/null @@ -1,50 +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; - -import android.app.Service; -import android.content.Intent; -import android.os.IBinder; -import android.os.Process; -import android.util.Log; - -public class Restarter extends Service { - private static final String LOGTAG = "GeckoRestarter"; - - private void doRestart(Intent intent) { - final int oldProc = intent.getIntExtra("pid", -1); - if (oldProc < 0) { - return; - } - - Process.killProcess(oldProc); - Log.d(LOGTAG, "Killed " + oldProc); - try { - Thread.sleep(100); - } catch (final InterruptedException e) { - } - - final Intent restartIntent = (Intent)intent.getParcelableExtra(Intent.EXTRA_INTENT); - restartIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - .putExtra("didRestart", true) - .setClassName(getApplicationContext(), - AppConstants.MOZ_ANDROID_BROWSER_INTENT_CLASS); - startActivity(restartIntent); - Log.d(LOGTAG, "Launched " + restartIntent); - } - - @Override - public int onStartCommand(Intent intent, int flags, int startId) { - doRestart(intent); - stopSelf(startId); - return Service.START_NOT_STICKY; - } - - @Override - public IBinder onBind(Intent intent) { - return null; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/ScreenManagerHelper.java b/mobile/android/base/java/org/mozilla/gecko/ScreenManagerHelper.java deleted file mode 100644 index 5cb404ce8a..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/ScreenManagerHelper.java +++ /dev/null @@ -1,43 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- - * vim: ts=4 sw=4 expandtab: - * 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; - -import org.mozilla.gecko.annotation.WrapForJNI; - -class ScreenManagerHelper { - - /** - * The following display types use the same definition in nsIScreen.idl - */ - final static int DISPLAY_PRIMARY = 0; // primary screen - final static int DISPLAY_EXTERNAL = 1; // wired displays, such as HDMI, DisplayPort, etc. - final static int DISPLAY_VIRTUAL = 2; // wireless displays, such as Chromecast, WiFi-Display, etc. - - /** - * Add a new nsScreen when a new display in Android is available. - * - * @param displayType the display type of the nsScreen would be added - * @param width the width of the new nsScreen - * @param height the height of the new nsScreen - * @param density the density of the new nsScreen - * - * @return return the ID of the added nsScreen - */ - @WrapForJNI - public native static int addDisplay(int displayType, - int width, - int height, - float density); - - /** - * Remove the nsScreen by the specific screen ID. - * - * @param screenId the ID of the screen would be removed. - */ - @WrapForJNI - public native static void removeDisplay(int screenId); -} diff --git a/mobile/android/base/java/org/mozilla/gecko/ScreenshotObserver.java b/mobile/android/base/java/org/mozilla/gecko/ScreenshotObserver.java deleted file mode 100644 index 64f101e515..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/ScreenshotObserver.java +++ /dev/null @@ -1,146 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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; - -import org.mozilla.gecko.AppConstants.Versions; -import org.mozilla.gecko.permissions.Permissions; -import org.mozilla.gecko.util.ThreadUtils; - -import android.Manifest; -import android.content.ContentResolver; -import android.content.Context; -import android.database.ContentObserver; -import android.database.Cursor; -import android.net.Uri; -import android.provider.MediaStore; -import android.util.Log; - -public class ScreenshotObserver { - private static final String LOGTAG = "GeckoScreenshotObserver"; - public Context context; - - /** - * Listener for screenshot changes. - */ - public interface OnScreenshotListener { - /** - * This callback is executed on the UI thread. - */ - public void onScreenshotTaken(String data, String title); - } - - private OnScreenshotListener listener; - - public ScreenshotObserver() { - } - - public void setListener(Context context, OnScreenshotListener listener) { - this.context = context; - this.listener = listener; - } - - private MediaObserver mediaObserver; - private String[] mediaProjections = new String[] { - MediaStore.Images.ImageColumns.DATA, - MediaStore.Images.ImageColumns.DISPLAY_NAME, - MediaStore.Images.ImageColumns.BUCKET_DISPLAY_NAME, - MediaStore.Images.ImageColumns.DATE_TAKEN, - MediaStore.Images.ImageColumns.TITLE - }; - - /** - * Start ScreenshotObserver if this device is supported and all required runtime permissions - * have been granted by the user. Calling this method will not prompt for permissions. - */ - public void start() { - Permissions.from(context) - .withPermissions(Manifest.permission.WRITE_EXTERNAL_STORAGE) - .doNotPrompt() - .run(startObserverRunnable()); - } - - private Runnable startObserverRunnable() { - return new Runnable() { - @Override - public void run() { - try { - if (mediaObserver == null) { - mediaObserver = new MediaObserver(); - context.getContentResolver().registerContentObserver(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, false, mediaObserver); - } - } catch (Exception e) { - Log.e(LOGTAG, "Failure to start watching media: ", e); - } - } - }; - } - - public void stop() { - if (mediaObserver == null) { - return; - } - - try { - context.getContentResolver().unregisterContentObserver(mediaObserver); - mediaObserver = null; - } catch (Exception e) { - Log.e(LOGTAG, "Failure to stop watching media: ", e); - } - } - - public void onMediaChange(final Uri uri) { - // Make sure we are on not on the main thread. - final ContentResolver cr = context.getContentResolver(); - ThreadUtils.postToBackgroundThread(new Runnable() { - @Override - public void run() { - // Find the most recent image added to the MediaStore and see if it's a screenshot. - final Cursor cursor = cr.query(uri, mediaProjections, null, null, MediaStore.Images.ImageColumns.DATE_ADDED + " DESC LIMIT 1"); - try { - if (cursor == null) { - return; - } - - while (cursor.moveToNext()) { - String data = cursor.getString(0); - Log.i(LOGTAG, "data: " + data); - String display = cursor.getString(1); - Log.i(LOGTAG, "display: " + display); - String album = cursor.getString(2); - Log.i(LOGTAG, "album: " + album); - long date = cursor.getLong(3); - String title = cursor.getString(4); - Log.i(LOGTAG, "title: " + title); - if (album != null && album.toLowerCase().contains("screenshot")) { - if (listener != null) { - listener.onScreenshotTaken(data, title); - break; - } - } - } - } catch (Exception e) { - Log.e(LOGTAG, "Failure to process media change: ", e); - } finally { - if (cursor != null) { - cursor.close(); - } - } - } - }); - } - - private class MediaObserver extends ContentObserver { - public MediaObserver() { - super(null); - } - - @Override - public void onChange(boolean selfChange) { - super.onChange(selfChange); - onMediaChange(MediaStore.Images.Media.EXTERNAL_CONTENT_URI); - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/SessionParser.java b/mobile/android/base/java/org/mozilla/gecko/SessionParser.java deleted file mode 100644 index d29aaadc73..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/SessionParser.java +++ /dev/null @@ -1,140 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- - * ***** BEGIN LICENSE BLOCK ***** - * - * 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/. - * - * ***** END LICENSE BLOCK ***** */ - -package org.mozilla.gecko; - -import java.util.LinkedList; - -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - -import android.util.Log; - -public abstract class SessionParser { - private static final String LOGTAG = "GeckoSessionParser"; - - public class SessionTab { - final private String mTitle; - final private String mUrl; - final private JSONObject mTabObject; - private boolean mIsSelected; - - private SessionTab(String title, String url, boolean isSelected, JSONObject tabObject) { - mTitle = title; - mUrl = url; - mIsSelected = isSelected; - mTabObject = tabObject; - } - - public String getTitle() { - return mTitle; - } - - public String getUrl() { - return mUrl; - } - - public boolean isSelected() { - return mIsSelected; - } - - public JSONObject getTabObject() { - return mTabObject; - } - - /** - * Is this tab pointing to about:home and does not contain any other history? - */ - public boolean isAboutHomeWithoutHistory() { - JSONArray entries = mTabObject.optJSONArray("entries"); - return entries != null && entries.length() == 1 && AboutPages.isAboutHome(mUrl); - } - }; - - abstract public void onTabRead(SessionTab tab); - - /** - * Placeholder method that must be overloaded to handle closedTabs while parsing session data. - * - * @param closedTabs, JSONArray of recently closed tab entries. - * @throws JSONException - */ - public void onClosedTabsRead(final JSONArray closedTabs) throws JSONException { - } - - /** - * Parses the provided session store data and calls onTabRead for each tab that has been found. - * - * @param sessionStrings One or more strings containing session store data. - * @return False if any of the session strings provided didn't contain valid session store data. - */ - public boolean parse(String... sessionStrings) { - final LinkedList sessionTabs = new LinkedList(); - int totalCount = 0; - int selectedIndex = -1; - try { - for (String sessionString : sessionStrings) { - final JSONArray windowsArray = new JSONObject(sessionString).getJSONArray("windows"); - if (windowsArray.length() == 0) { - // Session json can be empty if the user has opted out of session restore. - Log.d(LOGTAG, "Session restore file is empty, no session entries found."); - continue; - } - - final JSONObject window = windowsArray.getJSONObject(0); - final JSONArray tabs = window.getJSONArray("tabs"); - final int optSelected = window.optInt("selected", -1); - final JSONArray closedTabs = window.optJSONArray("closedTabs"); - if (closedTabs != null) { - onClosedTabsRead(closedTabs); - } - - for (int i = 0; i < tabs.length(); i++) { - final JSONObject tab = tabs.getJSONObject(i); - final int index = tab.getInt("index"); - final JSONArray entries = tab.getJSONArray("entries"); - if (index < 1 || entries.length() < index) { - Log.w(LOGTAG, "Session entries and index don't agree."); - continue; - } - final JSONObject entry = entries.getJSONObject(index - 1); - final String url = entry.getString("url"); - - String title = entry.optString("title"); - if (title.length() == 0) { - title = url; - } - - totalCount++; - boolean selected = false; - if (optSelected == i + 1) { - selected = true; - selectedIndex = totalCount; - } - sessionTabs.add(new SessionTab(title, url, selected, tab)); - } - } - } catch (JSONException e) { - Log.e(LOGTAG, "JSON error", e); - return false; - } - - // If no selected index was found, select the first tab. - if (selectedIndex == -1 && sessionTabs.size() > 0) { - sessionTabs.getFirst().mIsSelected = true; - } - - for (SessionTab tab : sessionTabs) { - onTabRead(tab); - } - - return true; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/SharedPreferencesHelper.java b/mobile/android/base/java/org/mozilla/gecko/SharedPreferencesHelper.java deleted file mode 100644 index 1066da0799..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/SharedPreferencesHelper.java +++ /dev/null @@ -1,311 +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; - -import org.mozilla.gecko.EventDispatcher; -import org.mozilla.gecko.util.GeckoEventListener; -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - -import android.content.Context; -import android.content.SharedPreferences; -import android.preference.PreferenceManager; -import android.util.Log; - -import java.util.Map; -import java.util.HashMap; - -/** - * Helper class to get, set, and observe Android Shared Preferences. - */ -public final class SharedPreferencesHelper - implements GeckoEventListener -{ - public static final String LOGTAG = "GeckoAndSharedPrefs"; - - // Calculate this once, at initialization. isLoggable is too expensive to - // have in-line in each log call. - private static final boolean logVerbose = Log.isLoggable(LOGTAG, Log.VERBOSE); - - private enum Scope { - APP("app"), - PROFILE("profile"), - GLOBAL("global"); - - public final String key; - - private Scope(String key) { - this.key = key; - } - - public static Scope forKey(String key) { - for (Scope scope : values()) { - if (scope.key.equals(key)) { - return scope; - } - } - - throw new IllegalStateException("SharedPreferences scope must be valid."); - } - } - - protected final Context mContext; - - // mListeners is not synchronized because it is only updated in - // handleObserve, which is called from Gecko serially. - protected final Map mListeners; - - public SharedPreferencesHelper(Context context) { - mContext = context; - - mListeners = new HashMap(); - - EventDispatcher dispatcher = GeckoApp.getEventDispatcher(); - if (dispatcher == null) { - Log.e(LOGTAG, "Gecko event dispatcher must not be null", new RuntimeException()); - return; - } - dispatcher.registerGeckoThreadListener(this, - "SharedPreferences:Set", - "SharedPreferences:Get", - "SharedPreferences:Observe"); - } - - public synchronized void uninit() { - EventDispatcher dispatcher = GeckoApp.getEventDispatcher(); - if (dispatcher == null) { - Log.e(LOGTAG, "Gecko event dispatcher must not be null", new RuntimeException()); - return; - } - dispatcher.unregisterGeckoThreadListener(this, - "SharedPreferences:Set", - "SharedPreferences:Get", - "SharedPreferences:Observe"); - } - - private SharedPreferences getSharedPreferences(JSONObject message) throws JSONException { - final Scope scope = Scope.forKey(message.getString("scope")); - switch (scope) { - case APP: - return GeckoSharedPrefs.forApp(mContext); - case PROFILE: - final String profileName = message.optString("profileName", null); - if (profileName == null) { - return GeckoSharedPrefs.forProfile(mContext); - } else { - return GeckoSharedPrefs.forProfileName(mContext, profileName); - } - case GLOBAL: - final String branch = message.optString("branch", null); - if (branch == null) { - return PreferenceManager.getDefaultSharedPreferences(mContext); - } else { - return mContext.getSharedPreferences(branch, Context.MODE_PRIVATE); - } - } - - return null; - } - - private String getBranch(Scope scope, String profileName, String branch) { - switch (scope) { - case APP: - return GeckoSharedPrefs.APP_PREFS_NAME; - case PROFILE: - if (profileName == null) { - profileName = GeckoProfile.get(mContext).getName(); - } - - return GeckoSharedPrefs.PROFILE_PREFS_NAME_PREFIX + profileName; - case GLOBAL: - return branch; - } - - return null; - } - - /** - * Set many SharedPreferences in Android. - * - * message.branch must exist, and should be a String SharedPreferences - * branch name, or null for the default branch. - * message.preferences should be an array of preferences. Each preference - * must include a String name, a String type in ["bool", "int", "string"], - * and an Object value. - */ - private void handleSet(JSONObject message) throws JSONException { - SharedPreferences.Editor editor = getSharedPreferences(message).edit(); - - JSONArray jsonPrefs = message.getJSONArray("preferences"); - - for (int i = 0; i < jsonPrefs.length(); i++) { - JSONObject pref = jsonPrefs.getJSONObject(i); - String name = pref.getString("name"); - String type = pref.getString("type"); - if ("bool".equals(type)) { - editor.putBoolean(name, pref.getBoolean("value")); - } else if ("int".equals(type)) { - editor.putInt(name, pref.getInt("value")); - } else if ("string".equals(type)) { - editor.putString(name, pref.getString("value")); - } else { - Log.w(LOGTAG, "Unknown pref value type [" + type + "] for pref [" + name + "]"); - } - editor.apply(); - } - } - - /** - * Get many SharedPreferences from Android. - * - * message.branch must exist, and should be a String SharedPreferences - * branch name, or null for the default branch. - * message.preferences should be an array of preferences. Each preference - * must include a String name, and a String type in ["bool", "int", - * "string"]. - */ - private JSONArray handleGet(JSONObject message) throws JSONException { - SharedPreferences prefs = getSharedPreferences(message); - JSONArray jsonPrefs = message.getJSONArray("preferences"); - JSONArray jsonValues = new JSONArray(); - - for (int i = 0; i < jsonPrefs.length(); i++) { - JSONObject pref = jsonPrefs.getJSONObject(i); - String name = pref.getString("name"); - String type = pref.getString("type"); - JSONObject jsonValue = new JSONObject(); - jsonValue.put("name", name); - jsonValue.put("type", type); - try { - if ("bool".equals(type)) { - boolean value = prefs.getBoolean(name, false); - jsonValue.put("value", value); - } else if ("int".equals(type)) { - int value = prefs.getInt(name, 0); - jsonValue.put("value", value); - } else if ("string".equals(type)) { - String value = prefs.getString(name, ""); - jsonValue.put("value", value); - } else { - Log.w(LOGTAG, "Unknown pref value type [" + type + "] for pref [" + name + "]"); - } - } catch (ClassCastException e) { - // Thrown if there is a preference with the given name that is - // not the right type. - Log.w(LOGTAG, "Wrong pref value type [" + type + "] for pref [" + name + "]"); - } - jsonValues.put(jsonValue); - } - - return jsonValues; - } - - private static class ChangeListener - implements SharedPreferences.OnSharedPreferenceChangeListener { - public final Scope scope; - public final String branch; - public final String profileName; - - public ChangeListener(final Scope scope, final String branch, final String profileName) { - this.scope = scope; - this.branch = branch; - this.profileName = profileName; - } - - @Override - public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { - if (logVerbose) { - Log.v(LOGTAG, "Got onSharedPreferenceChanged"); - } - try { - final JSONObject msg = new JSONObject(); - msg.put("scope", this.scope.key); - msg.put("branch", this.branch); - msg.put("profileName", this.profileName); - msg.put("key", key); - - // Truly, this is awful, but the API impedance is strong: there - // is no way to get a single untyped value from a - // SharedPreferences instance. - msg.put("value", sharedPreferences.getAll().get(key)); - - GeckoAppShell.notifyObservers("SharedPreferences:Changed", msg.toString()); - } catch (JSONException e) { - Log.e(LOGTAG, "Got exception creating JSON object", e); - return; - } - } - } - - /** - * Register or unregister a SharedPreferences.OnSharedPreferenceChangeListener. - * - * message.branch must exist, and should be a String SharedPreferences - * branch name, or null for the default branch. - * message.enable should be a boolean: true to enable listening, false to - * disable listening. - */ - private void handleObserve(JSONObject message) throws JSONException { - final SharedPreferences prefs = getSharedPreferences(message); - final boolean enable = message.getBoolean("enable"); - - final Scope scope = Scope.forKey(message.getString("scope")); - final String profileName = message.optString("profileName", null); - final String branch = getBranch(scope, profileName, message.optString("branch", null)); - - if (branch == null) { - Log.e(LOGTAG, "No branch specified for SharedPreference:Observe; aborting."); - return; - } - - // mListeners is only modified in this one observer, which is called - // from Gecko serially. - if (enable && !this.mListeners.containsKey(branch)) { - SharedPreferences.OnSharedPreferenceChangeListener listener - = new ChangeListener(scope, branch, profileName); - this.mListeners.put(branch, listener); - prefs.registerOnSharedPreferenceChangeListener(listener); - } - if (!enable && this.mListeners.containsKey(branch)) { - SharedPreferences.OnSharedPreferenceChangeListener listener - = this.mListeners.remove(branch); - prefs.unregisterOnSharedPreferenceChangeListener(listener); - } - } - - @Override - public void handleMessage(String event, JSONObject message) { - // Everything here is synchronous and serial, so we need not worry about - // overwriting an in-progress response. - try { - if (event.equals("SharedPreferences:Set")) { - if (logVerbose) { - Log.v(LOGTAG, "Got SharedPreferences:Set message."); - } - handleSet(message); - } else if (event.equals("SharedPreferences:Get")) { - if (logVerbose) { - Log.v(LOGTAG, "Got SharedPreferences:Get message."); - } - JSONObject obj = new JSONObject(); - obj.put("values", handleGet(message)); - EventDispatcher.sendResponse(message, obj); - } else if (event.equals("SharedPreferences:Observe")) { - if (logVerbose) { - Log.v(LOGTAG, "Got SharedPreferences:Observe message."); - } - handleObserve(message); - } else { - Log.e(LOGTAG, "SharedPreferencesHelper got unexpected message " + event); - return; - } - } catch (JSONException e) { - Log.e(LOGTAG, "Got exception in handleMessage handling event " + event, e); - return; - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/SiteIdentity.java b/mobile/android/base/java/org/mozilla/gecko/SiteIdentity.java deleted file mode 100644 index e39d25dd87..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/SiteIdentity.java +++ /dev/null @@ -1,249 +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; - -import org.json.JSONObject; - -import android.text.TextUtils; - -public class SiteIdentity { - private final String LOGTAG = "GeckoSiteIdentity"; - private SecurityMode mSecurityMode; - private boolean mSecure; - private MixedMode mMixedModeActive; - private MixedMode mMixedModeDisplay; - private TrackingMode mTrackingMode; - private String mHost; - private String mOwner; - private String mSupplemental; - private String mCountry; - private String mVerifier; - private String mOrigin; - - // The order of the items here relate to image levels in - // site_security_level.xml - public enum SecurityMode { - UNKNOWN("unknown"), - IDENTIFIED("identified"), - VERIFIED("verified"), - CHROMEUI("chromeUI"); - - private final String mId; - - private SecurityMode(String id) { - mId = id; - } - - public static SecurityMode fromString(String id) { - if (id == null) { - throw new IllegalArgumentException("Can't convert null String to SiteIdentity"); - } - - for (SecurityMode mode : SecurityMode.values()) { - if (TextUtils.equals(mode.mId, id)) { - return mode; - } - } - - throw new IllegalArgumentException("Could not convert String id to SiteIdentity"); - } - - @Override - public String toString() { - return mId; - } - } - - // The order of the items here relate to image levels in - // site_security_level.xml - public enum MixedMode { - UNKNOWN("unknown"), - MIXED_CONTENT_BLOCKED("blocked"), - MIXED_CONTENT_LOADED("loaded"); - - private final String mId; - - private MixedMode(String id) { - mId = id; - } - - public static MixedMode fromString(String id) { - if (id == null) { - throw new IllegalArgumentException("Can't convert null String to MixedMode"); - } - - for (MixedMode mode : MixedMode.values()) { - if (TextUtils.equals(mode.mId, id.toLowerCase())) { - return mode; - } - } - - throw new IllegalArgumentException("Could not convert String id to MixedMode"); - } - - @Override - public String toString() { - return mId; - } - } - - // The order of the items here relate to image levels in - // site_security_level.xml - public enum TrackingMode { - UNKNOWN("unknown"), - TRACKING_CONTENT_BLOCKED("tracking_content_blocked"), - TRACKING_CONTENT_LOADED("tracking_content_loaded"); - - private final String mId; - - private TrackingMode(String id) { - mId = id; - } - - public static TrackingMode fromString(String id) { - if (id == null) { - throw new IllegalArgumentException("Can't convert null String to TrackingMode"); - } - - for (TrackingMode mode : TrackingMode.values()) { - if (TextUtils.equals(mode.mId, id.toLowerCase())) { - return mode; - } - } - - throw new IllegalArgumentException("Could not convert String id to TrackingMode"); - } - - @Override - public String toString() { - return mId; - } - } - - public SiteIdentity() { - reset(); - } - - public void resetIdentity() { - mSecurityMode = SecurityMode.UNKNOWN; - mOrigin = null; - mHost = null; - mOwner = null; - mSupplemental = null; - mCountry = null; - mVerifier = null; - mSecure = false; - } - - public void reset() { - resetIdentity(); - mMixedModeActive = MixedMode.UNKNOWN; - mMixedModeDisplay = MixedMode.UNKNOWN; - mTrackingMode = TrackingMode.UNKNOWN; - } - - void update(JSONObject identityData) { - if (identityData == null) { - reset(); - return; - } - - try { - JSONObject mode = identityData.getJSONObject("mode"); - - try { - mMixedModeDisplay = MixedMode.fromString(mode.getString("mixed_display")); - } catch (Exception e) { - mMixedModeDisplay = MixedMode.UNKNOWN; - } - - try { - mMixedModeActive = MixedMode.fromString(mode.getString("mixed_active")); - } catch (Exception e) { - mMixedModeActive = MixedMode.UNKNOWN; - } - - try { - mTrackingMode = TrackingMode.fromString(mode.getString("tracking")); - } catch (Exception e) { - mTrackingMode = TrackingMode.UNKNOWN; - } - - try { - mSecurityMode = SecurityMode.fromString(mode.getString("identity")); - } catch (Exception e) { - resetIdentity(); - return; - } - - try { - mOrigin = identityData.getString("origin"); - mHost = identityData.optString("host", null); - mOwner = identityData.optString("owner", null); - mSupplemental = identityData.optString("supplemental", null); - mCountry = identityData.optString("country", null); - mVerifier = identityData.optString("verifier", null); - mSecure = identityData.optBoolean("secure", false); - } catch (Exception e) { - resetIdentity(); - } - } catch (Exception e) { - reset(); - } - } - - public SecurityMode getSecurityMode() { - return mSecurityMode; - } - - public String getOrigin() { - return mOrigin; - } - - public String getHost() { - return mHost; - } - - public String getOwner() { - return mOwner; - } - - public boolean hasOwner() { - return !TextUtils.isEmpty(mOwner); - } - - public String getSupplemental() { - return mSupplemental; - } - - public String getCountry() { - return mCountry; - } - - public boolean hasCountry() { - return !TextUtils.isEmpty(mCountry); - } - - public String getVerifier() { - return mVerifier; - } - - public boolean isSecure() { - return mSecure; - } - - public MixedMode getMixedModeActive() { - return mMixedModeActive; - } - - public MixedMode getMixedModeDisplay() { - return mMixedModeDisplay; - } - - public TrackingMode getTrackingMode() { - return mTrackingMode; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/SnackbarBuilder.java b/mobile/android/base/java/org/mozilla/gecko/SnackbarBuilder.java deleted file mode 100644 index 3283e7c375..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/SnackbarBuilder.java +++ /dev/null @@ -1,257 +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; - -import org.mozilla.gecko.util.EventCallback; -import org.mozilla.gecko.util.NativeJSObject; - -import android.app.Activity; -import android.graphics.Color; -import android.graphics.drawable.Drawable; -import android.graphics.drawable.InsetDrawable; -import android.support.annotation.StringRes; -import android.support.design.widget.Snackbar; -import android.support.v4.content.ContextCompat; -import android.text.TextUtils; -import android.util.Log; -import android.util.TypedValue; -import android.view.View; -import android.widget.TextView; - -import java.lang.ref.WeakReference; - -/** - * Helper class for creating and dismissing snackbars. Use this class to guarantee a consistent style and behavior - * across the app. - */ -public class SnackbarBuilder { - /** - * Combined interface for handling all callbacks from a snackbar because anonymous classes can only extend one - * interface or class. - */ - public static abstract class SnackbarCallback extends Snackbar.Callback implements View.OnClickListener {} - public static final String LOGTAG = "GeckoSnackbarBuilder"; - - /** - * SnackbarCallback implementation for delegating snackbar events to an EventCallback. - */ - private static class SnackbarEventCallback extends SnackbarCallback { - private EventCallback callback; - - public SnackbarEventCallback(EventCallback callback) { - this.callback = callback; - } - - @Override - public synchronized void onClick(View view) { - if (callback == null) { - return; - } - - callback.sendSuccess(null); - callback = null; // Releasing reference. We only want to execute the callback once. - } - - @Override - public synchronized void onDismissed(Snackbar snackbar, int event) { - if (callback == null || event == Snackbar.Callback.DISMISS_EVENT_ACTION) { - return; - } - - callback.sendError(null); - callback = null; // Releasing reference. We only want to execute the callback once. - } - } - - private static final Object currentSnackbarLock = new Object(); - private static WeakReference currentSnackbar = new WeakReference<>(null); // Guarded by 'currentSnackbarLock' - - private final Activity activity; - private String message; - private int duration; - private String action; - private SnackbarCallback callback; - private Drawable icon; - private Integer backgroundColor; - private Integer actionColor; - - /** - * @param activity Activity to show the snackbar in. - */ - private SnackbarBuilder(final Activity activity) { - this.activity = activity; - } - - public static SnackbarBuilder builder(final Activity activity) { - return new SnackbarBuilder(activity); - } - - /** - * @param message The text to show. Can be formatted text. - */ - public SnackbarBuilder message(final String message) { - this.message = message; - return this; - } - - /** - * @param id The id of the string resource to show. Can be formatted text. - */ - public SnackbarBuilder message(@StringRes final int id) { - message = activity.getResources().getString(id); - return this; - } - - /** - * @param duration How long to display the message. - */ - public SnackbarBuilder duration(final int duration) { - this.duration = duration; - return this; - } - - /** - * @param action Action text to display. - */ - public SnackbarBuilder action(final String action) { - this.action = action; - return this; - } - - /** - * @param id The id of the string resource for the action text to display. - */ - public SnackbarBuilder action(@StringRes final int id) { - action = activity.getResources().getString(id); - return this; - } - - /** - * @param callback Callback to be invoked when the action is clicked or the snackbar is dismissed. - */ - public SnackbarBuilder callback(final SnackbarCallback callback) { - this.callback = callback; - return this; - } - - /** - * @param callback Callback to be invoked when the action is clicked or the snackbar is dismissed. - */ - public SnackbarBuilder callback(final EventCallback callback) { - this.callback = new SnackbarEventCallback(callback); - return this; - } - - /** - * @param icon Icon to be displayed with the snackbar text. - */ - public SnackbarBuilder icon(final Drawable icon) { - this.icon = icon; - return this; - } - - /** - * @param backgroundColor Snackbar background color. - */ - public SnackbarBuilder backgroundColor(final Integer backgroundColor) { - this.backgroundColor = backgroundColor; - return this; - } - - /** - * @param actionColor Action text color. - */ - public SnackbarBuilder actionColor(final Integer actionColor) { - this.actionColor = actionColor; - return this; - } - - /** - * @param object Populate the builder with data from a Gecko Snackbar:Show event. - */ - public SnackbarBuilder fromEvent(final NativeJSObject object) { - message = object.getString("message"); - duration = object.getInt("duration"); - - if (object.has("backgroundColor")) { - final String providedColor = object.getString("backgroundColor"); - try { - backgroundColor = Color.parseColor(providedColor); - } catch (IllegalArgumentException e) { - Log.w(LOGTAG, "Failed to parse color string: " + providedColor); - } - } - - NativeJSObject actionObject = object.optObject("action", null); - if (actionObject != null) { - action = actionObject.optString("label", null); - } - return this; - } - - public void buildAndShow() { - final View parentView = findBestParentView(activity); - final Snackbar snackbar = Snackbar.make(parentView, message, duration); - - if (callback != null && !TextUtils.isEmpty(action)) { - snackbar.setAction(action, callback); - if (actionColor == null) { - snackbar.setActionTextColor(ContextCompat.getColor(activity, R.color.fennec_ui_orange)); - } else { - snackbar.setActionTextColor(actionColor); - } - snackbar.setCallback(callback); - } - - if (icon != null) { - int leftPadding = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 10, activity.getResources().getDisplayMetrics()); - - final InsetDrawable paddedIcon = new InsetDrawable(icon, 0, 0, leftPadding, 0); - - paddedIcon.setBounds(0, 0, leftPadding + icon.getIntrinsicWidth(), icon.getIntrinsicHeight()); - - TextView textView = (TextView) snackbar.getView().findViewById(android.support.design.R.id.snackbar_text); - textView.setCompoundDrawables(paddedIcon, null, null, null); - } - - if (backgroundColor != null) { - snackbar.getView().setBackgroundColor(backgroundColor); - } - - snackbar.show(); - - synchronized (currentSnackbarLock) { - currentSnackbar = new WeakReference<>(snackbar); - } - } - - /** - * Dismiss the currently visible snackbar. - */ - public static void dismissCurrentSnackbar() { - synchronized (currentSnackbarLock) { - final Snackbar snackbar = currentSnackbar.get(); - if (snackbar != null && snackbar.isShown()) { - snackbar.dismiss(); - } - } - } - - /** - * Find the best parent view to hold the Snackbar's view. The Snackbar implementation of the support - * library will use this view to walk up the view tree to find an actual suitable parent (if needed). - */ - private static View findBestParentView(Activity activity) { - if (activity instanceof GeckoApp) { - final View view = activity.findViewById(R.id.root_layout); - if (view != null) { - return view; - } - } - - return activity.findViewById(android.R.id.content); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/SuggestClient.java b/mobile/android/base/java/org/mozilla/gecko/SuggestClient.java deleted file mode 100644 index e43bbef1f4..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/SuggestClient.java +++ /dev/null @@ -1,142 +0,0 @@ -/* 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; - -import java.io.BufferedInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.net.HttpURLConnection; -import java.net.URL; -import java.net.URLEncoder; -import java.util.ArrayList; - -import org.json.JSONArray; -import org.mozilla.gecko.annotation.RobocopTarget; -import org.mozilla.gecko.util.HardwareUtils; - -import android.content.Context; -import android.text.TextUtils; -import android.util.Log; -import org.mozilla.gecko.util.NetworkUtils; - -/** - * Use network-based search suggestions. - */ -public class SuggestClient { - private static final String LOGTAG = "GeckoSuggestClient"; - - // This should go through GeckoInterface to get the UA, but the search activity - // doesn't use a GeckoView yet. Until it does, get the UA directly. - private static final String USER_AGENT = HardwareUtils.isTablet() ? - AppConstants.USER_AGENT_FENNEC_TABLET : AppConstants.USER_AGENT_FENNEC_MOBILE; - - private final Context mContext; - private final int mTimeout; - - // should contain the string "__searchTerms__", which is replaced with the query - private final String mSuggestTemplate; - - // the maximum number of suggestions to return - private final int mMaxResults; - - // used by robocop for testing - private final boolean mCheckNetwork; - - // used to make suggestions appear instantly after opt-in - private String mPrevQuery; - private ArrayList mPrevResults; - - @RobocopTarget - public SuggestClient(Context context, String suggestTemplate, int timeout, int maxResults, boolean checkNetwork) { - mContext = context; - mMaxResults = maxResults; - mSuggestTemplate = suggestTemplate; - mTimeout = timeout; - mCheckNetwork = checkNetwork; - } - - public String getSuggestTemplate() { - return mSuggestTemplate; - } - - /** - * Queries for a given search term and returns an ArrayList of suggestions. - */ - public ArrayList query(String query) { - if (query.equals(mPrevQuery)) - return mPrevResults; - - ArrayList suggestions = new ArrayList(); - if (TextUtils.isEmpty(mSuggestTemplate) || TextUtils.isEmpty(query)) { - return suggestions; - } - - if (!NetworkUtils.isConnected(mContext) && mCheckNetwork) { - Log.i(LOGTAG, "Not connected to network"); - return suggestions; - } - - try { - String encoded = URLEncoder.encode(query, "UTF-8"); - String suggestUri = mSuggestTemplate.replace("__searchTerms__", encoded); - - URL url = new URL(suggestUri); - String json = null; - HttpURLConnection urlConnection = null; - InputStream in = null; - try { - urlConnection = (HttpURLConnection) url.openConnection(); - urlConnection.setConnectTimeout(mTimeout); - urlConnection.setRequestProperty("User-Agent", USER_AGENT); - in = new BufferedInputStream(urlConnection.getInputStream()); - json = convertStreamToString(in); - } finally { - if (urlConnection != null) - urlConnection.disconnect(); - if (in != null) { - try { - in.close(); - } catch (IOException e) { - Log.e(LOGTAG, "error", e); - } - } - } - - if (json != null) { - /* - * Sample result: - * ["foo",["food network","foothill college","foot locker",...]] - */ - JSONArray results = new JSONArray(json); - JSONArray jsonSuggestions = results.getJSONArray(1); - - int added = 0; - for (int i = 0; (i < jsonSuggestions.length()) && (added < mMaxResults); i++) { - String suggestion = jsonSuggestions.getString(i); - if (!suggestion.equalsIgnoreCase(query)) { - suggestions.add(suggestion); - added++; - } - } - } else { - Log.e(LOGTAG, "Suggestion query failed"); - } - } catch (Exception e) { - Log.e(LOGTAG, "Error", e); - } - - mPrevQuery = query; - mPrevResults = suggestions; - return suggestions; - } - - private String convertStreamToString(java.io.InputStream is) { - try { - return new java.util.Scanner(is).useDelimiter("\\A").next(); - } catch (java.util.NoSuchElementException e) { - return ""; - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/Tab.java b/mobile/android/base/java/org/mozilla/gecko/Tab.java deleted file mode 100644 index 6010a3dd94..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/Tab.java +++ /dev/null @@ -1,843 +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; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashMap; -import java.util.Map; -import java.util.concurrent.Future; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import org.json.JSONException; -import org.json.JSONObject; -import org.mozilla.gecko.annotation.RobocopTarget; -import org.mozilla.gecko.db.BrowserDB; -import org.mozilla.gecko.db.URLMetadata; -import org.mozilla.gecko.gfx.BitmapUtils; -import org.mozilla.gecko.icons.IconCallback; -import org.mozilla.gecko.icons.IconDescriptor; -import org.mozilla.gecko.icons.IconRequestBuilder; -import org.mozilla.gecko.icons.IconResponse; -import org.mozilla.gecko.icons.Icons; -import org.mozilla.gecko.reader.ReaderModeUtils; -import org.mozilla.gecko.reader.ReadingListHelper; -import org.mozilla.gecko.toolbar.BrowserToolbar.TabEditingState; -import org.mozilla.gecko.util.ThreadUtils; -import org.mozilla.gecko.widget.SiteLogins; - -import android.content.ContentResolver; -import android.content.Context; -import android.graphics.Bitmap; -import android.graphics.Color; -import android.graphics.drawable.BitmapDrawable; -import android.os.Build; -import android.os.Bundle; -import android.text.TextUtils; -import android.util.Log; -import android.view.View; - -public class Tab { - private static final String LOGTAG = "GeckoTab"; - - private static Pattern sColorPattern; - private final int mId; - private final BrowserDB mDB; - private long mLastUsed; - private String mUrl; - private String mBaseDomain; - private String mUserRequested; // The original url requested. May be typed by the user or sent by an extneral app for example. - private String mTitle; - private Bitmap mFavicon; - private String mFaviconUrl; - private String mApplicationId; // Intended to be null after explicit user action. - - private IconRequestBuilder mIconRequestBuilder; - private Future mRunningIconRequest; - - private boolean mHasFeeds; - private boolean mHasOpenSearch; - private final SiteIdentity mSiteIdentity; - private SiteLogins mSiteLogins; - private BitmapDrawable mThumbnail; - private final int mParentId; - // Indicates the url was loaded from a source external to the app. This will be cleared - // when the user explicitly loads a new url (e.g. clicking a link is not explicit). - private final boolean mExternal; - private boolean mBookmark; - private int mFaviconLoadId; - private String mContentType; - private boolean mHasTouchListeners; - private final ArrayList mPluginViews; - private int mState; - private Bitmap mThumbnailBitmap; - private boolean mDesktopMode; - private boolean mEnteringReaderMode; - private final Context mAppContext; - private ErrorType mErrorType = ErrorType.NONE; - private volatile int mLoadProgress; - private volatile int mRecordingCount; - private volatile boolean mIsAudioPlaying; - private volatile boolean mIsMediaPlaying; - private String mMostRecentHomePanel; - private boolean mShouldShowToolbarWithoutAnimationOnFirstSelection; - - /* - * Bundle containing restore data for the panel referenced in mMostRecentHomePanel. This can be - * e.g. the most recent folder for the bookmarks panel, or any other state that should be - * persisted. This is then used e.g. when returning to homepanels via history. - */ - private Bundle mMostRecentHomePanelData; - - private int mHistoryIndex; - private int mHistorySize; - private boolean mCanDoBack; - private boolean mCanDoForward; - - private boolean mIsEditing; - private final TabEditingState mEditingState = new TabEditingState(); - - // Will be true when tab is loaded from cache while device was offline. - private boolean mLoadedFromCache; - - public static final int STATE_DELAYED = 0; - public static final int STATE_LOADING = 1; - public static final int STATE_SUCCESS = 2; - public static final int STATE_ERROR = 3; - - public static final int LOAD_PROGRESS_INIT = 10; - public static final int LOAD_PROGRESS_START = 20; - public static final int LOAD_PROGRESS_LOCATION_CHANGE = 60; - public static final int LOAD_PROGRESS_LOADED = 80; - public static final int LOAD_PROGRESS_STOP = 100; - - public enum ErrorType { - CERT_ERROR, // Pages with certificate problems - BLOCKED, // Pages blocked for phishing or malware warnings - NET_ERROR, // All other types of error - NONE // Non error pages - } - - public Tab(Context context, int id, String url, boolean external, int parentId, String title) { - mAppContext = context.getApplicationContext(); - mDB = BrowserDB.from(context); - mId = id; - mUrl = url; - mBaseDomain = ""; - mUserRequested = ""; - mExternal = external; - mParentId = parentId; - mTitle = title == null ? "" : title; - mSiteIdentity = new SiteIdentity(); - mHistoryIndex = -1; - mContentType = ""; - mPluginViews = new ArrayList(); - mState = shouldShowProgress(url) ? STATE_LOADING : STATE_SUCCESS; - mLoadProgress = LOAD_PROGRESS_INIT; - mIconRequestBuilder = Icons.with(mAppContext).pageUrl(mUrl); - - updateBookmark(); - } - - private ContentResolver getContentResolver() { - return mAppContext.getContentResolver(); - } - - public void onDestroy() { - Tabs.getInstance().notifyListeners(this, Tabs.TabEvents.CLOSED); - } - - @RobocopTarget - public int getId() { - return mId; - } - - public synchronized void onChange() { - mLastUsed = System.currentTimeMillis(); - } - - public synchronized long getLastUsed() { - return mLastUsed; - } - - public int getParentId() { - return mParentId; - } - - // may be null if user-entered query hasn't yet been resolved to a URI - public synchronized String getURL() { - return mUrl; - } - - // mUserRequested should never be null, but it may be an empty string - public synchronized String getUserRequested() { - return mUserRequested; - } - - // mTitle should never be null, but it may be an empty string - public synchronized String getTitle() { - return mTitle; - } - - public String getDisplayTitle() { - if (mTitle != null && mTitle.length() > 0) { - return mTitle; - } - - return mUrl; - } - - /** - * Returns the base domain of the loaded uri. Note that if the page is - * a Reader mode uri, the base domain returned is that of the original uri. - */ - public String getBaseDomain() { - return mBaseDomain; - } - - public Bitmap getFavicon() { - return mFavicon; - } - - protected String getApplicationId() { - return mApplicationId; - } - - protected void setApplicationId(final String applicationId) { - mApplicationId = applicationId; - } - - public BitmapDrawable getThumbnail() { - return mThumbnail; - } - - public String getMostRecentHomePanel() { - return mMostRecentHomePanel; - } - - public Bundle getMostRecentHomePanelData() { - return mMostRecentHomePanelData; - } - - public void setMostRecentHomePanel(String panelId) { - mMostRecentHomePanel = panelId; - mMostRecentHomePanelData = null; - } - - public void setMostRecentHomePanelData(Bundle data) { - mMostRecentHomePanelData = data; - } - - public Bitmap getThumbnailBitmap(int width, int height) { - if (mThumbnailBitmap != null) { - // Bug 787318 - Honeycomb has a bug with bitmap caching, we can't - // reuse the bitmap there. - boolean honeycomb = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB - && Build.VERSION.SDK_INT <= Build.VERSION_CODES.HONEYCOMB_MR2); - boolean sizeChange = mThumbnailBitmap.getWidth() != width - || mThumbnailBitmap.getHeight() != height; - if (honeycomb || sizeChange) { - mThumbnailBitmap = null; - } - } - - if (mThumbnailBitmap == null) { - Bitmap.Config config = (GeckoAppShell.getScreenDepth() == 24) ? - Bitmap.Config.ARGB_8888 : Bitmap.Config.RGB_565; - mThumbnailBitmap = Bitmap.createBitmap(width, height, config); - } - - return mThumbnailBitmap; - } - - public void updateThumbnail(final Bitmap b, final ThumbnailHelper.CachePolicy cachePolicy) { - ThreadUtils.postToBackgroundThread(new Runnable() { - @Override - public void run() { - if (b != null) { - try { - mThumbnail = new BitmapDrawable(mAppContext.getResources(), b); - if (mState == Tab.STATE_SUCCESS && cachePolicy == ThumbnailHelper.CachePolicy.STORE) { - saveThumbnailToDB(mDB); - } else { - // If the page failed to load, or requested that we not cache info about it, clear any previous - // thumbnails we've stored. - clearThumbnailFromDB(mDB); - } - } catch (OutOfMemoryError oom) { - Log.w(LOGTAG, "Unable to create/scale bitmap.", oom); - mThumbnail = null; - } - } else { - mThumbnail = null; - } - - Tabs.getInstance().notifyListeners(Tab.this, Tabs.TabEvents.THUMBNAIL); - } - }); - } - - public synchronized String getFaviconURL() { - return mFaviconUrl; - } - - public boolean hasFeeds() { - return mHasFeeds; - } - - public boolean hasOpenSearch() { - return mHasOpenSearch; - } - - public boolean hasLoadedFromCache() { - return mLoadedFromCache; - } - - public SiteIdentity getSiteIdentity() { - return mSiteIdentity; - } - - public void resetSiteIdentity() { - if (mSiteIdentity != null) { - mSiteIdentity.reset(); - Tabs.getInstance().notifyListeners(this, Tabs.TabEvents.SECURITY_CHANGE); - } - } - - public SiteLogins getSiteLogins() { - return mSiteLogins; - } - - public boolean isBookmark() { - return mBookmark; - } - - public boolean isExternal() { - return mExternal; - } - - public synchronized void updateURL(String url) { - if (url != null && url.length() > 0) { - mUrl = url; - } - } - - public synchronized void updateUserRequested(String userRequested) { - mUserRequested = userRequested; - } - - public void setErrorType(String type) { - if ("blocked".equals(type)) - setErrorType(ErrorType.BLOCKED); - else if ("certerror".equals(type)) - setErrorType(ErrorType.CERT_ERROR); - else if ("neterror".equals(type)) - setErrorType(ErrorType.NET_ERROR); - else - setErrorType(ErrorType.NONE); - } - - public void setErrorType(ErrorType type) { - mErrorType = type; - } - - public void setMetadata(JSONObject metadata) { - if (metadata == null) { - return; - } - - final ContentResolver cr = mAppContext.getContentResolver(); - final URLMetadata urlMetadata = mDB.getURLMetadata(); - - final Map data = urlMetadata.fromJSON(metadata); - ThreadUtils.postToBackgroundThread(new Runnable() { - @Override - public void run() { - urlMetadata.save(cr, data); - } - }); - } - - public ErrorType getErrorType() { - return mErrorType; - } - - public void setContentType(String contentType) { - mContentType = (contentType == null) ? "" : contentType; - } - - public String getContentType() { - return mContentType; - } - - public int getHistoryIndex() { - return mHistoryIndex; - } - - public int getHistorySize() { - return mHistorySize; - } - - public synchronized void updateTitle(String title) { - // Keep the title unchanged while entering reader mode. - if (mEnteringReaderMode) { - return; - } - - // If there was a title, but it hasn't changed, do nothing. - if (mTitle != null && - TextUtils.equals(mTitle, title)) { - return; - } - - mTitle = (title == null ? "" : title); - Tabs.getInstance().notifyListeners(this, Tabs.TabEvents.TITLE); - } - - public void setState(int state) { - mState = state; - - if (mState != Tab.STATE_LOADING) - mEnteringReaderMode = false; - } - - public int getState() { - return mState; - } - - public void setHasTouchListeners(boolean aValue) { - mHasTouchListeners = aValue; - } - - public boolean getHasTouchListeners() { - return mHasTouchListeners; - } - - public synchronized void addFavicon(String faviconURL, int faviconSize, String mimeType) { - mIconRequestBuilder - .icon(IconDescriptor.createFavicon(faviconURL, faviconSize, mimeType)) - .deferBuild(); - } - - public synchronized void addTouchicon(String iconUrl, int faviconSize, String mimeType) { - mIconRequestBuilder - .icon(IconDescriptor.createTouchicon(iconUrl, faviconSize, mimeType)) - .deferBuild(); - } - - public void loadFavicon() { - // Static Favicons never change - if (AboutPages.isBuiltinIconPage(mUrl) && mFavicon != null) { - return; - } - - mRunningIconRequest = mIconRequestBuilder - .build() - .execute(new IconCallback() { - @Override - public void onIconResponse(IconResponse response) { - mFavicon = response.getBitmap(); - - Tabs.getInstance().notifyListeners(Tab.this, Tabs.TabEvents.FAVICON); - } - }); - } - - public synchronized void clearFavicon() { - // Cancel any ongoing favicon load (if we never finished downloading the old favicon before - // we changed page). - if (mRunningIconRequest != null) { - mRunningIconRequest.cancel(true); - } - - // Keep the favicon unchanged while entering reader mode - if (mEnteringReaderMode) - return; - - mFavicon = null; - mFaviconUrl = null; - } - - public void setHasFeeds(boolean hasFeeds) { - mHasFeeds = hasFeeds; - } - - public void setHasOpenSearch(boolean hasOpenSearch) { - mHasOpenSearch = hasOpenSearch; - } - - public void setLoadedFromCache(boolean loadedFromCache) { - mLoadedFromCache = loadedFromCache; - } - - public void updateIdentityData(JSONObject identityData) { - mSiteIdentity.update(identityData); - } - - public void setSiteLogins(SiteLogins siteLogins) { - mSiteLogins = siteLogins; - } - - void updateBookmark() { - if (getURL() == null) { - return; - } - - ThreadUtils.postToBackgroundThread(new Runnable() { - @Override - public void run() { - final String url = getURL(); - if (url == null) { - return; - } - final String pageUrl = ReaderModeUtils.stripAboutReaderUrl(url); - - mBookmark = mDB.isBookmark(getContentResolver(), pageUrl); - Tabs.getInstance().notifyListeners(Tab.this, Tabs.TabEvents.MENU_UPDATED); - } - }); - } - - public void addBookmark() { - final String url = getURL(); - if (url == null) { - return; - } - - final String pageUrl = ReaderModeUtils.stripAboutReaderUrl(getURL()); - - ThreadUtils.postToBackgroundThread(new Runnable() { - @Override - public void run() { - mDB.addBookmark(getContentResolver(), mTitle, pageUrl); - Tabs.getInstance().notifyListeners(Tab.this, Tabs.TabEvents.BOOKMARK_ADDED); - } - }); - - if (AboutPages.isAboutReader(url)) { - ReadingListHelper.cacheReaderItem(pageUrl, mId, mAppContext); - } - } - - public void removeBookmark() { - final String url = getURL(); - if (url == null) { - return; - } - - final String pageUrl = ReaderModeUtils.stripAboutReaderUrl(getURL()); - - ThreadUtils.postToBackgroundThread(new Runnable() { - @Override - public void run() { - mDB.removeBookmarksWithURL(getContentResolver(), pageUrl); - Tabs.getInstance().notifyListeners(Tab.this, Tabs.TabEvents.BOOKMARK_REMOVED); - } - }); - - // We need to ensure we remove readercached items here - we could have switched out of readermode - // before unbookmarking, so we don't necessarily have an about:reader URL here. - ReadingListHelper.removeCachedReaderItem(pageUrl, mAppContext); - } - - public boolean isEnteringReaderMode() { - return mEnteringReaderMode; - } - - public void doReload(boolean bypassCache) { - GeckoAppShell.notifyObservers("Session:Reload", "{\"bypassCache\":" + String.valueOf(bypassCache) + "}"); - } - - // Our version of nsSHistory::GetCanGoBack - public boolean canDoBack() { - return mCanDoBack; - } - - public boolean doBack() { - if (!canDoBack()) - return false; - - GeckoAppShell.notifyObservers("Session:Back", ""); - return true; - } - - public void doStop() { - GeckoAppShell.notifyObservers("Session:Stop", ""); - } - - // Our version of nsSHistory::GetCanGoForward - public boolean canDoForward() { - return mCanDoForward; - } - - public boolean doForward() { - if (!canDoForward()) - return false; - - GeckoAppShell.notifyObservers("Session:Forward", ""); - return true; - } - - void handleLocationChange(JSONObject message) throws JSONException { - final String uri = message.getString("uri"); - final String oldUrl = getURL(); - final boolean sameDocument = message.getBoolean("sameDocument"); - mEnteringReaderMode = ReaderModeUtils.isEnteringReaderMode(oldUrl, uri); - mHistoryIndex = message.getInt("historyIndex"); - mHistorySize = message.getInt("historySize"); - mCanDoBack = message.getBoolean("canGoBack"); - mCanDoForward = message.getBoolean("canGoForward"); - - if (!TextUtils.equals(oldUrl, uri)) { - updateURL(uri); - updateBookmark(); - if (!sameDocument) { - // We can unconditionally clear the favicon and title here: we - // already filtered both cases in which this was a (pseudo-) - // spurious location change, so we're definitely loading a new - // page. - clearFavicon(); - - // Start to build a new request to load a favicon. - mIconRequestBuilder = Icons.with(mAppContext) - .pageUrl(uri); - - // Load local static Favicons immediately - if (AboutPages.isBuiltinIconPage(uri)) { - loadFavicon(); - } - - updateTitle(null); - } - } - - if (sameDocument) { - // We can get a location change event for the same document with an anchor tag - // Notify listeners so that buttons like back or forward will update themselves - Tabs.getInstance().notifyListeners(this, Tabs.TabEvents.LOCATION_CHANGE, oldUrl); - return; - } - - setContentType(message.getString("contentType")); - updateUserRequested(message.getString("userRequested")); - mBaseDomain = message.optString("baseDomain"); - - setHasFeeds(false); - setHasOpenSearch(false); - mSiteIdentity.reset(); - setSiteLogins(null); - setHasTouchListeners(false); - setErrorType(ErrorType.NONE); - setLoadProgressIfLoading(LOAD_PROGRESS_LOCATION_CHANGE); - - Tabs.getInstance().notifyListeners(this, Tabs.TabEvents.LOCATION_CHANGE, oldUrl); - } - - private static boolean shouldShowProgress(final String url) { - return !AboutPages.isAboutPage(url); - } - - void handleDocumentStart(boolean restoring, String url) { - setLoadProgress(LOAD_PROGRESS_START); - setState((!restoring && shouldShowProgress(url)) ? STATE_LOADING : STATE_SUCCESS); - mSiteIdentity.reset(); - } - - void handleDocumentStop(boolean success) { - setState(success ? STATE_SUCCESS : STATE_ERROR); - - final String oldURL = getURL(); - final Tab tab = this; - tab.setLoadProgress(LOAD_PROGRESS_STOP); - - ThreadUtils.getBackgroundHandler().postDelayed(new Runnable() { - @Override - public void run() { - // tab.getURL() may return null - if (!TextUtils.equals(oldURL, getURL())) - return; - - ThumbnailHelper.getInstance().getAndProcessThumbnailFor(tab); - } - }, 500); - } - - void handleContentLoaded() { - setLoadProgressIfLoading(LOAD_PROGRESS_LOADED); - } - - protected void saveThumbnailToDB(final BrowserDB db) { - final BitmapDrawable thumbnail = mThumbnail; - if (thumbnail == null) { - return; - } - - try { - final String url = getURL(); - if (url == null) { - return; - } - - db.updateThumbnailForUrl(getContentResolver(), url, thumbnail); - } catch (Exception e) { - // ignore - } - } - - public void loadThumbnailFromDB(final BrowserDB db) { - try { - final String url = getURL(); - if (url == null) { - return; - } - - byte[] thumbnail = db.getThumbnailForUrl(getContentResolver(), url); - if (thumbnail == null) { - return; - } - - Bitmap bitmap = BitmapUtils.decodeByteArray(thumbnail); - mThumbnail = new BitmapDrawable(mAppContext.getResources(), bitmap); - - Tabs.getInstance().notifyListeners(Tab.this, Tabs.TabEvents.THUMBNAIL); - } catch (Exception e) { - // ignore - } - } - - private void clearThumbnailFromDB(final BrowserDB db) { - try { - final String url = getURL(); - if (url == null) { - return; - } - - // Passing in a null thumbnail will delete the stored thumbnail for this url - db.updateThumbnailForUrl(getContentResolver(), url, null); - } catch (Exception e) { - // ignore - } - } - - public void addPluginView(View view) { - mPluginViews.add(view); - } - - public void removePluginView(View view) { - mPluginViews.remove(view); - } - - public View[] getPluginViews() { - return mPluginViews.toArray(new View[mPluginViews.size()]); - } - - public void setDesktopMode(boolean enabled) { - mDesktopMode = enabled; - } - - public boolean getDesktopMode() { - return mDesktopMode; - } - - public boolean isPrivate() { - return false; - } - - /** - * Sets the tab load progress to the given percentage. - * - * @param progressPercentage Percentage to set progress to (0-100) - */ - void setLoadProgress(int progressPercentage) { - mLoadProgress = progressPercentage; - } - - /** - * Sets the tab load progress to the given percentage only if the tab is - * currently loading. - * - * about:neterror can trigger a STOP before other page load events (bug - * 976426), so any post-START events should make sure the page is loading - * before updating progress. - * - * @param progressPercentage Percentage to set progress to (0-100) - */ - void setLoadProgressIfLoading(int progressPercentage) { - if (getState() == STATE_LOADING) { - setLoadProgress(progressPercentage); - } - } - - /** - * Gets the tab load progress percentage. - * - * @return Current progress percentage - */ - public int getLoadProgress() { - return mLoadProgress; - } - - public void setRecording(boolean isRecording) { - if (isRecording) { - mRecordingCount++; - } else { - mRecordingCount--; - } - } - - public boolean isRecording() { - return mRecordingCount > 0; - } - - /** - * The "MediaPlaying" is used for controling media control interface and - * means the tab has playing media. - * - * @param isMediaPlaying the tab has any playing media or not - */ - public void setIsMediaPlaying(boolean isMediaPlaying) { - mIsMediaPlaying = isMediaPlaying; - } - - public boolean isMediaPlaying() { - return mIsMediaPlaying; - } - - /** - * The "AudioPlaying" is used for showing the tab sound indicator and means - * the tab has playing media and the media is audible. - * - * @param isAudioPlaying the tab has any audible playing media or not - */ - public void setIsAudioPlaying(boolean isAudioPlaying) { - mIsAudioPlaying = isAudioPlaying; - } - - public boolean isAudioPlaying() { - return mIsAudioPlaying; - } - - public boolean isEditing() { - return mIsEditing; - } - - public void setIsEditing(final boolean isEditing) { - this.mIsEditing = isEditing; - } - - public TabEditingState getEditingState() { - return mEditingState; - } - - public void setShouldShowToolbarWithoutAnimationOnFirstSelection(final boolean shouldShowWithoutAnimation) { - mShouldShowToolbarWithoutAnimationOnFirstSelection = shouldShowWithoutAnimation; - } - - public boolean getShouldShowToolbarWithoutAnimationOnFirstSelection() { - return mShouldShowToolbarWithoutAnimationOnFirstSelection; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/Tabs.java b/mobile/android/base/java/org/mozilla/gecko/Tabs.java deleted file mode 100644 index c7e024fe03..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/Tabs.java +++ /dev/null @@ -1,1021 +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; - -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; -import java.util.concurrent.CopyOnWriteArrayList; -import java.util.concurrent.atomic.AtomicInteger; - -import android.support.annotation.Nullable; -import org.json.JSONException; -import org.json.JSONObject; - -import org.mozilla.gecko.annotation.JNITarget; -import org.mozilla.gecko.annotation.RobocopTarget; -import org.mozilla.gecko.AppConstants.Versions; -import org.mozilla.gecko.db.BrowserDB; -import org.mozilla.gecko.gfx.LayerView; -import org.mozilla.gecko.mozglue.SafeIntent; -import org.mozilla.gecko.notifications.WhatsNewReceiver; -import org.mozilla.gecko.reader.ReaderModeUtils; -import org.mozilla.gecko.util.GeckoEventListener; -import org.mozilla.gecko.util.ThreadUtils; - -import android.accounts.Account; -import android.accounts.AccountManager; -import android.accounts.OnAccountsUpdateListener; -import android.content.ContentResolver; -import android.content.Context; -import android.database.ContentObserver; -import android.database.sqlite.SQLiteException; -import android.graphics.Color; -import android.net.Uri; -import android.os.Handler; -import android.provider.Browser; -import android.support.v4.content.ContextCompat; -import android.util.Log; - -public class Tabs implements GeckoEventListener { - private static final String LOGTAG = "GeckoTabs"; - - // mOrder and mTabs are always of the same cardinality, and contain the same values. - private final CopyOnWriteArrayList mOrder = new CopyOnWriteArrayList(); - - // All writes to mSelectedTab must be synchronized on the Tabs instance. - // In general, it's preferred to always use selectTab()). - private volatile Tab mSelectedTab; - - // All accesses to mTabs must be synchronized on the Tabs instance. - private final HashMap mTabs = new HashMap(); - - private AccountManager mAccountManager; - private OnAccountsUpdateListener mAccountListener; - - public static final int LOADURL_NONE = 0; - public static final int LOADURL_NEW_TAB = 1 << 0; - public static final int LOADURL_USER_ENTERED = 1 << 1; - public static final int LOADURL_PRIVATE = 1 << 2; - public static final int LOADURL_PINNED = 1 << 3; - public static final int LOADURL_DELAY_LOAD = 1 << 4; - public static final int LOADURL_DESKTOP = 1 << 5; - public static final int LOADURL_BACKGROUND = 1 << 6; - /** Indicates the url has been specified by a source external to the app. */ - public static final int LOADURL_EXTERNAL = 1 << 7; - /** Indicates the tab is the first shown after Firefox is hidden and restored. */ - public static final int LOADURL_FIRST_AFTER_ACTIVITY_UNHIDDEN = 1 << 8; - - private static final long PERSIST_TABS_AFTER_MILLISECONDS = 1000 * 2; - - public static final int INVALID_TAB_ID = -1; - - private static final AtomicInteger sTabId = new AtomicInteger(0); - private volatile boolean mInitialTabsAdded; - - private Context mAppContext; - private LayerView mLayerView; - private ContentObserver mBookmarksContentObserver; - private PersistTabsRunnable mPersistTabsRunnable; - private int mPrivateClearColor; - - private static class PersistTabsRunnable implements Runnable { - private final BrowserDB db; - private final Context context; - private final Iterable tabs; - - public PersistTabsRunnable(final Context context, Iterable tabsInOrder) { - this.context = context; - this.db = BrowserDB.from(context); - this.tabs = tabsInOrder; - } - - @Override - public void run() { - try { - db.getTabsAccessor().persistLocalTabs(context.getContentResolver(), tabs); - } catch (SQLiteException e) { - Log.w(LOGTAG, "Error persisting local tabs", e); - } - } - }; - - private Tabs() { - EventDispatcher.getInstance().registerGeckoThreadListener(this, - "Tab:Added", - "Tab:Close", - "Tab:Select", - "Content:LocationChange", - "Content:SecurityChange", - "Content:StateChange", - "Content:LoadError", - "Content:PageShow", - "DOMTitleChanged", - "Link:Favicon", - "Link:Touchicon", - "Link:Feed", - "Link:OpenSearch", - "DesktopMode:Changed", - "Tab:StreamStart", - "Tab:StreamStop", - "Tab:AudioPlayingChange", - "Tab:MediaPlaybackChange"); - - mPrivateClearColor = Color.RED; - - } - - public synchronized void attachToContext(Context context, LayerView layerView) { - final Context appContext = context.getApplicationContext(); - if (mAppContext == appContext) { - return; - } - - if (mAppContext != null) { - // This should never happen. - Log.w(LOGTAG, "The application context has changed!"); - } - - mAppContext = appContext; - mLayerView = layerView; - mPrivateClearColor = ContextCompat.getColor(context, R.color.tabs_tray_grey_pressed); - mAccountManager = AccountManager.get(appContext); - - mAccountListener = new OnAccountsUpdateListener() { - @Override - public void onAccountsUpdated(Account[] accounts) { - queuePersistAllTabs(); - } - }; - - // The listener will run on the background thread (see 2nd argument). - mAccountManager.addOnAccountsUpdatedListener(mAccountListener, ThreadUtils.getBackgroundHandler(), false); - - if (mBookmarksContentObserver != null) { - // It's safe to use the db here since we aren't doing any I/O. - final GeckoProfile profile = GeckoProfile.get(context); - BrowserDB.from(profile).registerBookmarkObserver(getContentResolver(), mBookmarksContentObserver); - } - } - - /** - * Gets the tab count corresponding to the private state of the selected - * tab. - * - * If the selected tab is a non-private tab, this will return the number of - * non-private tabs; likewise, if this is a private tab, this will return - * the number of private tabs. - * - * @return the number of tabs in the current private state - */ - public synchronized int getDisplayCount() { - // Once mSelectedTab is non-null, it cannot be null for the remainder - // of the object's lifetime. - boolean getPrivate = mSelectedTab != null && mSelectedTab.isPrivate(); - int count = 0; - for (Tab tab : mOrder) { - if (tab.isPrivate() == getPrivate) { - count++; - } - } - return count; - } - - public int isOpen(String url) { - for (Tab tab : mOrder) { - if (tab.getURL().equals(url)) { - return tab.getId(); - } - } - return -1; - } - - // Must be synchronized to avoid racing on mBookmarksContentObserver. - private void lazyRegisterBookmarkObserver() { - if (mBookmarksContentObserver == null) { - mBookmarksContentObserver = new ContentObserver(null) { - @Override - public void onChange(boolean selfChange) { - for (Tab tab : mOrder) { - tab.updateBookmark(); - } - } - }; - - // It's safe to use the db here since we aren't doing any I/O. - final GeckoProfile profile = GeckoProfile.get(mAppContext); - BrowserDB.from(profile).registerBookmarkObserver(getContentResolver(), mBookmarksContentObserver); - } - } - - private Tab addTab(int id, String url, boolean external, int parentId, String title, boolean isPrivate, int tabIndex) { - final Tab tab = isPrivate ? new PrivateTab(mAppContext, id, url, external, parentId, title) : - new Tab(mAppContext, id, url, external, parentId, title); - synchronized (this) { - lazyRegisterBookmarkObserver(); - mTabs.put(id, tab); - - if (tabIndex > -1) { - mOrder.add(tabIndex, tab); - } else { - mOrder.add(tab); - } - } - - // Suppress the ADDED event to prevent animation of tabs created via session restore. - if (mInitialTabsAdded) { - notifyListeners(tab, TabEvents.ADDED, - Integer.toString(getPrivacySpecificTabIndex(tabIndex, isPrivate))); - } - - return tab; - } - - // Return the index, among those tabs whose privacy setting matches isPrivate, of the tab at - // position index in mOrder. Returns -1, for "new last tab", when index is -1. - private int getPrivacySpecificTabIndex(int index, boolean isPrivate) { - int privacySpecificIndex = -1; - for (int i = 0; i <= index; i++) { - final Tab tab = mOrder.get(i); - if (tab.isPrivate() == isPrivate) { - privacySpecificIndex++; - } - } - return privacySpecificIndex; - } - - public synchronized void removeTab(int id) { - if (mTabs.containsKey(id)) { - Tab tab = getTab(id); - mOrder.remove(tab); - mTabs.remove(id); - } - } - - public synchronized Tab selectTab(int id) { - if (!mTabs.containsKey(id)) - return null; - - final Tab oldTab = getSelectedTab(); - final Tab tab = mTabs.get(id); - - // This avoids a NPE below, but callers need to be careful to - // handle this case. - if (tab == null || oldTab == tab) { - return tab; - } - - mSelectedTab = tab; - notifyListeners(tab, TabEvents.SELECTED); - - if (mLayerView != null) { - mLayerView.setClearColor(getTabColor(tab)); - } - - if (oldTab != null) { - notifyListeners(oldTab, TabEvents.UNSELECTED); - } - - // Pass a message to Gecko to update tab state in BrowserApp. - GeckoAppShell.notifyObservers("Tab:Selected", String.valueOf(tab.getId())); - return tab; - } - - public synchronized boolean selectLastTab() { - if (mOrder.isEmpty()) { - return false; - } - - selectTab(mOrder.get(mOrder.size() - 1).getId()); - return true; - } - - private int getIndexOf(Tab tab) { - return mOrder.lastIndexOf(tab); - } - - private Tab getNextTabFrom(Tab tab, boolean getPrivate) { - int numTabs = mOrder.size(); - int index = getIndexOf(tab); - for (int i = index + 1; i < numTabs; i++) { - Tab next = mOrder.get(i); - if (next.isPrivate() == getPrivate) { - return next; - } - } - return null; - } - - private Tab getPreviousTabFrom(Tab tab, boolean getPrivate) { - int index = getIndexOf(tab); - for (int i = index - 1; i >= 0; i--) { - Tab prev = mOrder.get(i); - if (prev.isPrivate() == getPrivate) { - return prev; - } - } - return null; - } - - /** - * Gets the selected tab. - * - * The selected tab can be null if we're doing a session restore after a - * crash and Gecko isn't ready yet. - * - * @return the selected tab, or null if no tabs exist - */ - @Nullable - public Tab getSelectedTab() { - return mSelectedTab; - } - - public boolean isSelectedTab(Tab tab) { - return tab != null && tab == mSelectedTab; - } - - public boolean isSelectedTabId(int tabId) { - final Tab selected = mSelectedTab; - return selected != null && selected.getId() == tabId; - } - - @RobocopTarget - public synchronized Tab getTab(int id) { - if (id == -1) - return null; - - if (mTabs.size() == 0) - return null; - - if (!mTabs.containsKey(id)) - return null; - - return mTabs.get(id); - } - - public synchronized Tab getTabForApplicationId(final String applicationId) { - if (applicationId == null) { - return null; - } - - for (final Tab tab : mOrder) { - if (applicationId.equals(tab.getApplicationId())) { - return tab; - } - } - - return null; - } - - /** Close tab and then select the default next tab */ - @RobocopTarget - public synchronized void closeTab(Tab tab) { - closeTab(tab, getNextTab(tab)); - } - - public synchronized void closeTab(Tab tab, Tab nextTab) { - closeTab(tab, nextTab, false); - } - - public synchronized void closeTab(Tab tab, boolean showUndoToast) { - closeTab(tab, getNextTab(tab), showUndoToast); - } - - /** Close tab and then select nextTab */ - public synchronized void closeTab(final Tab tab, Tab nextTab, boolean showUndoToast) { - if (tab == null) - return; - - int tabId = tab.getId(); - removeTab(tabId); - - if (nextTab == null) { - nextTab = loadUrl(AboutPages.HOME, LOADURL_NEW_TAB); - } - - selectTab(nextTab.getId()); - - tab.onDestroy(); - - final JSONObject args = new JSONObject(); - try { - args.put("tabId", String.valueOf(tabId)); - args.put("showUndoToast", showUndoToast); - } catch (JSONException e) { - Log.e(LOGTAG, "Error building Tab:Closed arguments: " + e); - } - - // Pass a message to Gecko to update tab state in BrowserApp - GeckoAppShell.notifyObservers("Tab:Closed", args.toString()); - } - - /** Return the tab that will be selected by default after this one is closed */ - public Tab getNextTab(Tab tab) { - Tab selectedTab = getSelectedTab(); - if (selectedTab != tab) - return selectedTab; - - boolean getPrivate = tab.isPrivate(); - Tab nextTab = getNextTabFrom(tab, getPrivate); - if (nextTab == null) - nextTab = getPreviousTabFrom(tab, getPrivate); - if (nextTab == null && getPrivate) { - // If there are no private tabs remaining, get the last normal tab - Tab lastTab = mOrder.get(mOrder.size() - 1); - if (!lastTab.isPrivate()) { - nextTab = lastTab; - } else { - nextTab = getPreviousTabFrom(lastTab, false); - } - } - - Tab parent = getTab(tab.getParentId()); - if (parent != null) { - // If the next tab is a sibling, switch to it. Otherwise go back to the parent. - if (nextTab != null && nextTab.getParentId() == tab.getParentId()) - return nextTab; - else - return parent; - } - return nextTab; - } - - public Iterable getTabsInOrder() { - return mOrder; - } - - /** - * @return the current GeckoApp instance, or throws if - * we aren't correctly initialized. - */ - private synchronized Context getAppContext() { - if (mAppContext == null) { - throw new IllegalStateException("Tabs not initialized with a GeckoApp instance."); - } - return mAppContext; - } - - public ContentResolver getContentResolver() { - return getAppContext().getContentResolver(); - } - - // Make Tabs a singleton class. - private static class TabsInstanceHolder { - private static final Tabs INSTANCE = new Tabs(); - } - - @RobocopTarget - public static Tabs getInstance() { - return Tabs.TabsInstanceHolder.INSTANCE; - } - - // GeckoEventListener implementation - @Override - public void handleMessage(String event, JSONObject message) { - Log.d(LOGTAG, "handleMessage: " + event); - try { - // All other events handled below should contain a tabID property - int id = message.getInt("tabID"); - Tab tab = getTab(id); - - // "Tab:Added" is a special case because tab will be null if the tab was just added - if (event.equals("Tab:Added")) { - String url = message.isNull("uri") ? null : message.getString("uri"); - - if (message.getBoolean("cancelEditMode")) { - final Tab oldTab = getSelectedTab(); - if (oldTab != null) { - oldTab.setIsEditing(false); - } - } - - if (message.getBoolean("stub")) { - if (tab == null) { - // Tab was already closed; abort - return; - } - } else { - tab = addTab(id, url, message.getBoolean("external"), - message.getInt("parentId"), - message.getString("title"), - message.getBoolean("isPrivate"), - message.getInt("tabIndex")); - // If we added the tab as a stub, we should have already - // selected it, so ignore this flag for stubbed tabs. - if (message.getBoolean("selected")) - selectTab(id); - } - - if (message.getBoolean("delayLoad")) - tab.setState(Tab.STATE_DELAYED); - if (message.getBoolean("desktopMode")) - tab.setDesktopMode(true); - return; - } - - // Tab was already closed; abort - if (tab == null) - return; - - if (event.equals("Tab:Close")) { - closeTab(tab); - } else if (event.equals("Tab:Select")) { - selectTab(tab.getId()); - } else if (event.equals("Content:LocationChange")) { - tab.handleLocationChange(message); - } else if (event.equals("Content:SecurityChange")) { - tab.updateIdentityData(message.getJSONObject("identity")); - notifyListeners(tab, TabEvents.SECURITY_CHANGE); - } else if (event.equals("Content:StateChange")) { - int state = message.getInt("state"); - if ((state & GeckoAppShell.WPL_STATE_IS_NETWORK) != 0) { - if ((state & GeckoAppShell.WPL_STATE_START) != 0) { - boolean restoring = message.getBoolean("restoring"); - tab.handleDocumentStart(restoring, message.getString("uri")); - notifyListeners(tab, Tabs.TabEvents.START); - } else if ((state & GeckoAppShell.WPL_STATE_STOP) != 0) { - tab.handleDocumentStop(message.getBoolean("success")); - notifyListeners(tab, Tabs.TabEvents.STOP); - } - } - } else if (event.equals("Content:LoadError")) { - tab.handleContentLoaded(); - notifyListeners(tab, Tabs.TabEvents.LOAD_ERROR); - } else if (event.equals("Content:PageShow")) { - tab.setLoadedFromCache(message.getBoolean("fromCache")); - tab.updateUserRequested(message.getString("userRequested")); - notifyListeners(tab, TabEvents.PAGE_SHOW); - } else if (event.equals("DOMTitleChanged")) { - tab.updateTitle(message.getString("title")); - } else if (event.equals("Link:Favicon")) { - // Add the favicon to the set of available icons for this tab. - - tab.addFavicon(message.getString("href"), message.getInt("size"), message.getString("mime")); - - // Load the favicon. If the tab is still loading, we actually do the load once the - // page has loaded, in an attempt to prevent the favicon load from having a - // detrimental effect on page load time. - if (tab.getState() != Tab.STATE_LOADING) { - tab.loadFavicon(); - } - } else if (event.equals("Link:Touchicon")) { - tab.addTouchicon(message.getString("href"), message.getInt("size"), message.getString("mime")); - } else if (event.equals("Link:Feed")) { - tab.setHasFeeds(true); - notifyListeners(tab, TabEvents.LINK_FEED); - } else if (event.equals("Link:OpenSearch")) { - boolean visible = message.getBoolean("visible"); - tab.setHasOpenSearch(visible); - } else if (event.equals("DesktopMode:Changed")) { - tab.setDesktopMode(message.getBoolean("desktopMode")); - notifyListeners(tab, TabEvents.DESKTOP_MODE_CHANGE); - } else if (event.equals("Tab:StreamStart")) { - tab.setRecording(true); - notifyListeners(tab, TabEvents.RECORDING_CHANGE); - } else if (event.equals("Tab:StreamStop")) { - tab.setRecording(false); - notifyListeners(tab, TabEvents.RECORDING_CHANGE); - } else if (event.equals("Tab:AudioPlayingChange")) { - tab.setIsAudioPlaying(message.getBoolean("isAudioPlaying")); - notifyListeners(tab, TabEvents.AUDIO_PLAYING_CHANGE); - } else if (event.equals("Tab:MediaPlaybackChange")) { - final String status = message.getString("status"); - if (status.equals("resume")) { - notifyListeners(tab, TabEvents.MEDIA_PLAYING_RESUME); - } else { - tab.setIsMediaPlaying(status.equals("start")); - notifyListeners(tab, TabEvents.MEDIA_PLAYING_CHANGE); - } - } - - } catch (Exception e) { - Log.w(LOGTAG, "handleMessage threw for " + event, e); - } - } - - public void refreshThumbnails() { - final BrowserDB db = BrowserDB.from(mAppContext); - ThreadUtils.postToBackgroundThread(new Runnable() { - @Override - public void run() { - for (final Tab tab : mOrder) { - if (tab.getThumbnail() == null) { - tab.loadThumbnailFromDB(db); - } - } - } - }); - } - - public interface OnTabsChangedListener { - void onTabChanged(Tab tab, TabEvents msg, String data); - } - - private static final List TABS_CHANGED_LISTENERS = new CopyOnWriteArrayList(); - - public static void registerOnTabsChangedListener(OnTabsChangedListener listener) { - TABS_CHANGED_LISTENERS.add(listener); - } - - public static void unregisterOnTabsChangedListener(OnTabsChangedListener listener) { - TABS_CHANGED_LISTENERS.remove(listener); - } - - public enum TabEvents { - CLOSED, - START, - LOADED, - LOAD_ERROR, - STOP, - FAVICON, - THUMBNAIL, - TITLE, - SELECTED, - UNSELECTED, - ADDED, - RESTORED, - LOCATION_CHANGE, - MENU_UPDATED, - PAGE_SHOW, - LINK_FEED, - SECURITY_CHANGE, - DESKTOP_MODE_CHANGE, - RECORDING_CHANGE, - BOOKMARK_ADDED, - BOOKMARK_REMOVED, - AUDIO_PLAYING_CHANGE, - OPENED_FROM_TABS_TRAY, - MEDIA_PLAYING_CHANGE, - MEDIA_PLAYING_RESUME - } - - public void notifyListeners(Tab tab, TabEvents msg) { - notifyListeners(tab, msg, ""); - } - - public void notifyListeners(final Tab tab, final TabEvents msg, final String data) { - if (tab == null && - msg != TabEvents.RESTORED) { - throw new IllegalArgumentException("onTabChanged:" + msg + " must specify a tab."); - } - - ThreadUtils.postToUiThread(new Runnable() { - @Override - public void run() { - onTabChanged(tab, msg, data); - - if (TABS_CHANGED_LISTENERS.isEmpty()) { - return; - } - - Iterator items = TABS_CHANGED_LISTENERS.iterator(); - while (items.hasNext()) { - items.next().onTabChanged(tab, msg, data); - } - } - }); - } - - private void onTabChanged(Tab tab, Tabs.TabEvents msg, Object data) { - switch (msg) { - // We want the tab record to have an accurate favicon, so queue - // the persisting of tabs when it changes. - case FAVICON: - case LOCATION_CHANGE: - queuePersistAllTabs(); - break; - case RESTORED: - mInitialTabsAdded = true; - break; - - // When one tab is deselected, another one is always selected, so only - // queue a single persist operation. When tabs are added/closed, they - // are also selected/unselected, so it would be redundant to also listen - // for ADDED/CLOSED events. - case SELECTED: - if (mLayerView != null) { - mLayerView.setSurfaceBackgroundColor(getTabColor(tab)); - mLayerView.setPaintState(LayerView.PAINT_START); - } - queuePersistAllTabs(); - case UNSELECTED: - tab.onChange(); - break; - default: - break; - } - } - - /** - * Queues a request to persist tabs after PERSIST_TABS_AFTER_MILLISECONDS - * milliseconds have elapsed. If any existing requests are already queued then - * those requests are removed. - */ - private void queuePersistAllTabs() { - final Handler backgroundHandler = ThreadUtils.getBackgroundHandler(); - - // Note: Its safe to modify the runnable here because all of the callers are on the same thread. - if (mPersistTabsRunnable != null) { - backgroundHandler.removeCallbacks(mPersistTabsRunnable); - mPersistTabsRunnable = null; - } - - mPersistTabsRunnable = new PersistTabsRunnable(mAppContext, getTabsInOrder()); - backgroundHandler.postDelayed(mPersistTabsRunnable, PERSIST_TABS_AFTER_MILLISECONDS); - } - - /** - * Looks for an open tab with the given URL. - * @param url the URL of the tab we're looking for - * - * @return first Tab with the given URL, or null if there is no such tab. - */ - public Tab getFirstTabForUrl(String url) { - return getFirstTabForUrlHelper(url, null); - } - - /** - * Looks for an open tab with the given URL and private state. - * @param url the URL of the tab we're looking for - * @param isPrivate if true, only look for tabs that are private. if false, - * only look for tabs that are non-private. - * - * @return first Tab with the given URL, or null if there is no such tab. - */ - public Tab getFirstTabForUrl(String url, boolean isPrivate) { - return getFirstTabForUrlHelper(url, isPrivate); - } - - private Tab getFirstTabForUrlHelper(String url, Boolean isPrivate) { - if (url == null) { - return null; - } - - for (Tab tab : mOrder) { - if (isPrivate != null && isPrivate != tab.isPrivate()) { - continue; - } - if (url.equals(tab.getURL())) { - return tab; - } - } - - return null; - } - - /** - * Looks for a reader mode enabled open tab with the given URL and private - * state. - * - * @param url - * The URL of the tab we're looking for. The url parameter can be - * the actual article URL or the reader mode article URL. - * @param isPrivate - * If true, only look for tabs that are private. If false, only - * look for tabs that are not private. - * - * @return The first Tab with the given URL, or null if there is no such - * tab. - */ - public Tab getFirstReaderTabForUrl(String url, boolean isPrivate) { - if (url == null) { - return null; - } - - url = ReaderModeUtils.stripAboutReaderUrl(url); - - for (Tab tab : mOrder) { - if (isPrivate != tab.isPrivate()) { - continue; - } - String tabUrl = tab.getURL(); - if (AboutPages.isAboutReader(tabUrl)) { - tabUrl = ReaderModeUtils.stripAboutReaderUrl(tabUrl); - if (url.equals(tabUrl)) { - return tab; - } - } - } - - return null; - } - - /** - * Loads a tab with the given URL in the currently selected tab. - * - * @param url URL of page to load, or search term used if searchEngine is given - */ - @RobocopTarget - public Tab loadUrl(String url) { - return loadUrl(url, LOADURL_NONE); - } - - /** - * Loads a tab with the given URL. - * - * @param url URL of page to load, or search term used if searchEngine is given - * @param flags flags used to load tab - * - * @return the Tab if a new one was created; null otherwise - */ - @RobocopTarget - public Tab loadUrl(String url, int flags) { - return loadUrl(url, null, -1, null, flags); - } - - public Tab loadUrlWithIntentExtras(final String url, final SafeIntent intent, final int flags) { - // We can't directly create a listener to tell when the user taps on the "What's new" - // notification, so we use this intent handling as a signal that they tapped the notification. - if (intent.getBooleanExtra(WhatsNewReceiver.EXTRA_WHATSNEW_NOTIFICATION, false)) { - Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.NOTIFICATION, - WhatsNewReceiver.EXTRA_WHATSNEW_NOTIFICATION); - } - - // Note: we don't get the URL from the intent so the calling - // method has the opportunity to change the URL if applicable. - return loadUrl(url, null, -1, intent, flags); - } - - public Tab loadUrl(final String url, final String searchEngine, final int parentId, final int flags) { - return loadUrl(url, searchEngine, parentId, null, flags); - } - - /** - * Loads a tab with the given URL. - * - * @param url URL of page to load, or search term used if searchEngine is given - * @param searchEngine if given, the search engine with this name is used - * to search for the url string; if null, the URL is loaded directly - * @param parentId ID of this tab's parent, or -1 if it has no parent - * @param intent an intent whose extras are used to modify the request - * @param flags flags used to load tab - * - * @return the Tab if a new one was created; null otherwise - */ - public Tab loadUrl(final String url, final String searchEngine, final int parentId, - final SafeIntent intent, final int flags) { - JSONObject args = new JSONObject(); - Tab tabToSelect = null; - boolean delayLoad = (flags & LOADURL_DELAY_LOAD) != 0; - - // delayLoad implies background tab - boolean background = delayLoad || (flags & LOADURL_BACKGROUND) != 0; - - try { - boolean isPrivate = (flags & LOADURL_PRIVATE) != 0; - boolean userEntered = (flags & LOADURL_USER_ENTERED) != 0; - boolean desktopMode = (flags & LOADURL_DESKTOP) != 0; - boolean external = (flags & LOADURL_EXTERNAL) != 0; - final boolean isFirstShownAfterActivityUnhidden = (flags & LOADURL_FIRST_AFTER_ACTIVITY_UNHIDDEN) != 0; - - args.put("url", url); - args.put("engine", searchEngine); - args.put("parentId", parentId); - args.put("userEntered", userEntered); - args.put("isPrivate", isPrivate); - args.put("pinned", (flags & LOADURL_PINNED) != 0); - args.put("desktopMode", desktopMode); - - final boolean needsNewTab; - final String applicationId = (intent == null) ? null : - intent.getStringExtra(Browser.EXTRA_APPLICATION_ID); - if (applicationId == null) { - needsNewTab = (flags & LOADURL_NEW_TAB) != 0; - } else { - // If you modify this code, be careful that intent != null. - final boolean extraCreateNewTab = intent.getBooleanExtra(Browser.EXTRA_CREATE_NEW_TAB, false); - final Tab applicationTab = getTabForApplicationId(applicationId); - if (applicationTab == null || extraCreateNewTab) { - needsNewTab = true; - } else { - needsNewTab = false; - delayLoad = false; - background = false; - - tabToSelect = applicationTab; - final int tabToSelectId = tabToSelect.getId(); - args.put("tabID", tabToSelectId); - - // This must be called before the "Tab:Load" event is sent. I think addTab gets - // away with it because having "newTab" == true causes the selected tab to be - // updated in JS for the "Tab:Load" event but "newTab" is false in our case. - // This makes me think the other selectTab is not necessary (bug 1160673). - // - // Note: that makes the later call redundant but selectTab exits early so I'm - // fine not adding the complex logic to avoid calling it again. - selectTab(tabToSelect.getId()); - } - } - - args.put("newTab", needsNewTab); - args.put("delayLoad", delayLoad); - args.put("selected", !background); - - if (needsNewTab) { - int tabId = getNextTabId(); - args.put("tabID", tabId); - - // The URL is updated for the tab once Gecko responds with the - // Tab:Added message. We can preliminarily set the tab's URL as - // long as it's a valid URI. - String tabUrl = (url != null && Uri.parse(url).getScheme() != null) ? url : null; - - // Add the new tab to the end of the tab order. - final int tabIndex = -1; - - tabToSelect = addTab(tabId, tabUrl, external, parentId, url, isPrivate, tabIndex); - tabToSelect.setDesktopMode(desktopMode); - tabToSelect.setApplicationId(applicationId); - if (isFirstShownAfterActivityUnhidden) { - // We just opened Firefox so we want to show - // the toolbar but not animate it to avoid jank. - tabToSelect.setShouldShowToolbarWithoutAnimationOnFirstSelection(true); - } - } - } catch (Exception e) { - Log.w(LOGTAG, "Error building JSON arguments for loadUrl.", e); - } - - GeckoAppShell.notifyObservers("Tab:Load", args.toString()); - - if (tabToSelect == null) { - return null; - } - - if (!delayLoad && !background) { - selectTab(tabToSelect.getId()); - } - - // Load favicon instantly for about:home page because it's already cached - if (AboutPages.isBuiltinIconPage(url)) { - tabToSelect.loadFavicon(); - } - - return tabToSelect; - } - - public Tab addTab() { - return loadUrl(AboutPages.HOME, Tabs.LOADURL_NEW_TAB); - } - - public Tab addPrivateTab() { - return loadUrl(AboutPages.PRIVATEBROWSING, Tabs.LOADURL_NEW_TAB | Tabs.LOADURL_PRIVATE); - } - - /** - * Open the url as a new tab, and mark the selected tab as its "parent". - * - * If the url is already open in a tab, the existing tab is selected. - * Use this for tabs opened by the browser chrome, so users can press the - * "Back" button to return to the previous tab. - * - * This method will open a new private tab if the currently selected tab - * is also private. - * - * @param url URL of page to load - */ - public void loadUrlInTab(String url) { - Iterable tabs = getTabsInOrder(); - for (Tab tab : tabs) { - if (url.equals(tab.getURL())) { - selectTab(tab.getId()); - return; - } - } - - // getSelectedTab() can return null if no tab has been created yet - // (i.e., we're restoring a session after a crash). In these cases, - // don't mark any tabs as a parent. - int parentId = -1; - int flags = LOADURL_NEW_TAB; - - final Tab selectedTab = getSelectedTab(); - if (selectedTab != null) { - parentId = selectedTab.getId(); - if (selectedTab.isPrivate()) { - flags = flags | LOADURL_PRIVATE; - } - } - - loadUrl(url, null, parentId, flags); - } - - /** - * Gets the next tab ID. - */ - @JNITarget - public static int getNextTabId() { - return sTabId.getAndIncrement(); - } - - private int getTabColor(Tab tab) { - if (tab != null) { - return tab.isPrivate() ? mPrivateClearColor : Color.WHITE; - } - - return Color.WHITE; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/Telemetry.java b/mobile/android/base/java/org/mozilla/gecko/Telemetry.java deleted file mode 100644 index 342445bf21..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/Telemetry.java +++ /dev/null @@ -1,246 +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; - -import org.mozilla.gecko.annotation.RobocopTarget; -import org.mozilla.gecko.annotation.WrapForJNI; -import org.mozilla.gecko.TelemetryContract.Event; -import org.mozilla.gecko.TelemetryContract.Method; -import org.mozilla.gecko.TelemetryContract.Reason; -import org.mozilla.gecko.TelemetryContract.Session; - -import android.os.SystemClock; -import android.util.Log; - -/** - * All telemetry times are relative to one of two clocks: - * - * * Real time since the device was booted, including deep sleep. Use this - * as a substitute for wall clock. - * * Uptime since the device was booted, excluding deep sleep. Use this to - * avoid timing a user activity when their phone is in their pocket! - * - * The majority of methods in this class are defined in terms of real time. - */ -@RobocopTarget -public class Telemetry { - private static final String LOGTAG = "Telemetry"; - - @WrapForJNI(stubName = "AddHistogram", dispatchTo = "gecko") - private static native void nativeAddHistogram(String name, int value); - @WrapForJNI(stubName = "AddKeyedHistogram", dispatchTo = "gecko") - private static native void nativeAddKeyedHistogram(String name, String key, int value); - @WrapForJNI(stubName = "StartUISession", dispatchTo = "gecko") - private static native void nativeStartUiSession(String name, long timestamp); - @WrapForJNI(stubName = "StopUISession", dispatchTo = "gecko") - private static native void nativeStopUiSession(String name, String reason, long timestamp); - @WrapForJNI(stubName = "AddUIEvent", dispatchTo = "gecko") - private static native void nativeAddUiEvent(String action, String method, - long timestamp, String extras); - - public static long uptime() { - return SystemClock.uptimeMillis(); - } - - public static long realtime() { - return SystemClock.elapsedRealtime(); - } - - // Define new histograms in: - // toolkit/components/telemetry/Histograms.json - public static void addToHistogram(String name, int value) { - if (GeckoThread.isRunning()) { - nativeAddHistogram(name, value); - } else { - GeckoThread.queueNativeCall(Telemetry.class, "nativeAddHistogram", - String.class, name, value); - } - } - - public static void addToKeyedHistogram(String name, String key, int value) { - if (GeckoThread.isRunning()) { - nativeAddKeyedHistogram(name, key, value); - } else { - GeckoThread.queueNativeCall(Telemetry.class, "nativeAddKeyedHistogram", - String.class, name, String.class, key, value); - } - } - - public abstract static class Timer { - private final long mStartTime; - private final String mName; - - private volatile boolean mHasFinished; - private volatile long mElapsed = -1; - - protected abstract long now(); - - public Timer(String name) { - mName = name; - mStartTime = now(); - } - - public void cancel() { - mHasFinished = true; - } - - public long getElapsed() { - return mElapsed; - } - - public void stop() { - // Only the first stop counts. - if (mHasFinished) { - return; - } - - mHasFinished = true; - - final long elapsed = now() - mStartTime; - if (elapsed < 0) { - Log.e(LOGTAG, "Current time less than start time -- clock shenanigans?"); - return; - } - - mElapsed = elapsed; - if (elapsed > Integer.MAX_VALUE) { - Log.e(LOGTAG, "Duration of " + elapsed + "ms is too great to add to histogram."); - return; - } - - addToHistogram(mName, (int) (elapsed)); - } - } - - public static class RealtimeTimer extends Timer { - public RealtimeTimer(String name) { - super(name); - } - - @Override - protected long now() { - return Telemetry.realtime(); - } - } - - public static class UptimeTimer extends Timer { - public UptimeTimer(String name) { - super(name); - } - - @Override - protected long now() { - return Telemetry.uptime(); - } - } - - public static void startUISession(final Session session, final String sessionNameSuffix) { - final String sessionName = getSessionName(session, sessionNameSuffix); - - Log.d(LOGTAG, "StartUISession: " + sessionName); - if (GeckoThread.isRunning()) { - nativeStartUiSession(sessionName, realtime()); - } else { - GeckoThread.queueNativeCall(Telemetry.class, "nativeStartUiSession", - String.class, sessionName, realtime()); - } - } - - public static void startUISession(final Session session) { - startUISession(session, null); - } - - public static void stopUISession(final Session session, final String sessionNameSuffix, - final Reason reason) { - final String sessionName = getSessionName(session, sessionNameSuffix); - - Log.d(LOGTAG, "StopUISession: " + sessionName + ", reason=" + reason); - if (GeckoThread.isRunning()) { - nativeStopUiSession(sessionName, reason.toString(), realtime()); - } else { - GeckoThread.queueNativeCall(Telemetry.class, "nativeStopUiSession", - String.class, sessionName, - String.class, reason.toString(), realtime()); - } - } - - public static void stopUISession(final Session session, final Reason reason) { - stopUISession(session, null, reason); - } - - public static void stopUISession(final Session session, final String sessionNameSuffix) { - stopUISession(session, sessionNameSuffix, Reason.NONE); - } - - public static void stopUISession(final Session session) { - stopUISession(session, null, Reason.NONE); - } - - private static String getSessionName(final Session session, final String sessionNameSuffix) { - if (sessionNameSuffix != null) { - return session.toString() + ":" + sessionNameSuffix; - } else { - return session.toString(); - } - } - - /** - * @param method A non-null method (if null is desired, consider using Method.NONE) - */ - private static void sendUIEvent(final String eventName, final Method method, - final long timestamp, final String extras) { - if (method == null) { - throw new IllegalArgumentException("Expected non-null method - use Method.NONE?"); - } - - if (!AppConstants.RELEASE_OR_BETA) { - final String logString = "SendUIEvent: event = " + eventName + " method = " + method + " timestamp = " + - timestamp + " extras = " + extras; - Log.d(LOGTAG, logString); - } - if (GeckoThread.isRunning()) { - nativeAddUiEvent(eventName, method.toString(), timestamp, extras); - } else { - GeckoThread.queueNativeCall(Telemetry.class, "nativeAddUiEvent", - String.class, eventName, String.class, method.toString(), - timestamp, String.class, extras); - } - } - - public static void sendUIEvent(final Event event, final Method method, final long timestamp, - final String extras) { - sendUIEvent(event.toString(), method, timestamp, extras); - } - - public static void sendUIEvent(final Event event, final Method method, final long timestamp) { - sendUIEvent(event, method, timestamp, null); - } - - public static void sendUIEvent(final Event event, final Method method, final String extras) { - sendUIEvent(event, method, realtime(), extras); - } - - public static void sendUIEvent(final Event event, final Method method) { - sendUIEvent(event, method, realtime(), null); - } - - public static void sendUIEvent(final Event event) { - sendUIEvent(event, Method.NONE, realtime(), null); - } - - /** - * Sends a UIEvent with the given status appended to the event name. - * - * This method is a slight bend of the Telemetry framework so chances - * are that you don't want to use this: please think really hard before you do. - * - * Intended for use with data policy notifications. - */ - public static void sendUIEvent(final Event event, final boolean eventStatus) { - final String eventName = event + ":" + eventStatus; - sendUIEvent(eventName, Method.NONE, realtime(), null); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/TelemetryContract.java b/mobile/android/base/java/org/mozilla/gecko/TelemetryContract.java deleted file mode 100644 index 0c2051a9da..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/TelemetryContract.java +++ /dev/null @@ -1,307 +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; - -import org.mozilla.gecko.annotation.RobocopTarget; - -/** - * Holds data definitions for our UI Telemetry implementation. - * - * Note that enum values of "_TEST*" are reserved for testing and - * should not be changed without changing the associated tests. - * - * See mobile/android/base/docs/index.rst for a full dictionary. - */ -@RobocopTarget -public interface TelemetryContract { - - /** - * Holds event names. Intended for use with - * Telemetry.sendUIEvent() as the "action" parameter. - * - * Please keep this list sorted. - */ - public enum Event { - // Generic action, usually for tracking menu and toolbar actions. - ACTION("action.1"), - - // Cancel a state, action, etc. - CANCEL("cancel.1"), - - // Start casting a video. - // Note: Only used in JavaScript for now, but here for completeness. - CAST("cast.1"), - - // Editing an item. - EDIT("edit.1"), - - // Launching (opening) an external application. - // Note: Only used in JavaScript for now, but here for completeness. - LAUNCH("launch.1"), - - // Loading a URL. - LOAD_URL("loadurl.1"), - - LOCALE_BROWSER_RESET("locale.browser.reset.1"), - LOCALE_BROWSER_SELECTED("locale.browser.selected.1"), - LOCALE_BROWSER_UNSELECTED("locale.browser.unselected.1"), - - // Hide a built-in home panel. - PANEL_HIDE("panel.hide.1"), - - // Move a home panel up or down. - PANEL_MOVE("panel.move.1"), - - // Remove a custom home panel. - PANEL_REMOVE("panel.remove.1"), - - // Set default home panel. - PANEL_SET_DEFAULT("panel.setdefault.1"), - - // Show a hidden built-in home panel. - PANEL_SHOW("panel.show.1"), - - // Pinning an item. - PIN("pin.1"), - - // Outcome of data policy notification: can be true or false. - POLICY_NOTIFICATION_SUCCESS("policynotification.success.1"), - - // Sanitizing private data. - SANITIZE("sanitize.1"), - - // Saving a resource (reader, bookmark, etc) for viewing later. - SAVE("save.1"), - - // Perform a search -- currently used when starting a search in the search activity. - SEARCH("search.1"), - - // Remove a search engine. - SEARCH_REMOVE("search.remove.1"), - - // Restore default search engines. - SEARCH_RESTORE_DEFAULTS("search.restoredefaults.1"), - - // Set default search engine. - SEARCH_SET_DEFAULT("search.setdefault.1"), - - // Sharing content. - SHARE("share.1"), - - // Show a UI element. - SHOW("show.1"), - - // Undoing a user action. - // Note: Only used in JavaScript for now, but here for completeness. - UNDO("undo.1"), - - // Unpinning an item. - UNPIN("unpin.1"), - - // Stop holding a resource (reader, bookmark, etc) for viewing later. - UNSAVE("unsave.1"), - - // When the user performs actions on the in-content network error page. - NETERROR("neterror.1"), - - // VALUES BELOW THIS LINE ARE EXCLUSIVE TO TESTING. - _TEST1("_test_event_1.1"), - _TEST2("_test_event_2.1"), - _TEST3("_test_event_3.1"), - _TEST4("_test_event_4.1"), - ; - - private final String string; - - Event(final String string) { - this.string = string; - } - - @Override - public String toString() { - return string; - } - } - - /** - * Holds event methods. Intended for use in - * Telemetry.sendUIEvent() as the "method" parameter. - * - * Please keep this list sorted. - */ - public enum Method { - // Action triggered from the action bar (including the toolbar). - ACTIONBAR("actionbar"), - - // Action triggered by hitting the Android back button. - BACK("back"), - - // Action triggered from a button. - BUTTON("button"), - - // Action taken from a content page -- for example, a search results web page. - CONTENT("content"), - - // Action occurred via a context menu. - CONTEXT_MENU("contextmenu"), - - // Action triggered from a dialog. - DIALOG("dialog"), - - // Action triggered from a doorhanger popup prompt. - DOORHANGER("doorhanger"), - - // Action triggered from a view grid item, like a thumbnail. - GRID_ITEM("griditem"), - - // Action occurred via an intent. - INTENT("intent"), - - // Action occurred via a homescreen launcher. - HOMESCREEN("homescreen"), - - // Action triggered from a list. - LIST("list"), - - // Action triggered from a view list item, like a row of a list. - LIST_ITEM("listitem"), - - // Action occurred via the main menu. - MENU("menu"), - - // No method is specified. - NONE(null), - - // Action triggered from a notification in the Android notification bar. - NOTIFICATION("notification"), - - // Action triggered from a pageaction in the URLBar. - // Note: Only used in JavaScript for now, but here for completeness. - PAGEACTION("pageaction"), - - // Action triggered from one of a series of views, such as ViewPager. - PANEL("panel"), - - // Action triggered by a background service / automatic system making a decision. - SERVICE("service"), - - // Action triggered from a settings screen. - SETTINGS("settings"), - - // Actions triggered from the share overlay. - SHARE_OVERLAY("shareoverlay"), - - // Action triggered from a suggestion provided to the user. - SUGGESTION("suggestion"), - - // Action triggered from an OS system action. - SYSTEM("system"), - - // Action triggered from a SuperToast. - // Note: Only used in JavaScript for now, but here for completeness. - TOAST("toast"), - - // Action triggerred by pressing a SearchWidget button - WIDGET("widget"), - - // VALUES BELOW THIS LINE ARE EXCLUSIVE TO TESTING. - _TEST1("_test_method_1"), - _TEST2("_test_method_2"), - ; - - private final String string; - - Method(final String string) { - this.string = string; - } - - @Override - public String toString() { - return string; - } - } - - /** - * Holds session names. Intended for use with - * Telemetry.startUISession() as the "sessionName" parameter. - * - * Please keep this list sorted. - */ - public enum Session { - // Awesomescreen (including frecency search) is active. - AWESOMESCREEN("awesomescreen.1"), - - // Used to tag experiments being run. - EXPERIMENT("experiment.1"), - - // Started the very first time we believe the application has been launched. - FIRSTRUN("firstrun.1"), - - // Awesomescreen frecency search is active. - FRECENCY("frecency.1"), - - // Started when a user enters a given home panel. - // Session name is dynamic, encoded as "homepanel.1:" - HOME_PANEL("homepanel.1"), - - // Started when a Reader viewer becomes active in the foreground. - // Note: Only used in JavaScript for now, but here for completeness. - READER("reader.1"), - - // Started when the search activity launches. - SEARCH_ACTIVITY("searchactivity.1"), - - // Settings activity is active. - SETTINGS("settings.1"), - - // VALUES BELOW THIS LINE ARE EXCLUSIVE TO TESTING. - _TEST_STARTED_TWICE("_test_session_started_twice.1"), - _TEST_STOPPED_TWICE("_test_session_stopped_twice.1"), - ; - - private final String string; - - Session(final String string) { - this.string = string; - } - - @Override - public String toString() { - return string; - } - } - - /** - * Holds reasons for stopping a session. Intended for use in - * Telemetry.stopUISession() as the "reason" parameter. - * - * Please keep this list sorted. - */ - public enum Reason { - // Changes were committed. - COMMIT("commit"), - - // No reason is specified. - NONE(null), - - // VALUES BELOW THIS LINE ARE EXCLUSIVE TO TESTING. - _TEST1("_test_reason_1"), - _TEST2("_test_reason_2"), - _TEST_IGNORED("_test_reason_ignored"), - ; - - private final String string; - - Reason(final String string) { - this.string = string; - } - - @Override - public String toString() { - return string; - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/ThumbnailHelper.java b/mobile/android/base/java/org/mozilla/gecko/ThumbnailHelper.java deleted file mode 100644 index 3a70124316..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/ThumbnailHelper.java +++ /dev/null @@ -1,246 +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; - -import org.mozilla.gecko.annotation.WrapForJNI; -import org.mozilla.gecko.gfx.BitmapUtils; -import org.mozilla.gecko.util.ResourceDrawableUtils; -import org.mozilla.gecko.mozglue.DirectBufferAllocator; - -import android.content.res.Resources; -import android.graphics.Bitmap; -import android.util.Log; - -import java.nio.ByteBuffer; -import java.util.ArrayList; - -/** - * Helper class to generate thumbnails for tabs. - * Internally, a queue of pending thumbnails is maintained in mPendingThumbnails. - * The head of the queue is the thumbnail that is currently being processed; upon - * completion of the current thumbnail the next one is automatically processed. - * Changes to the thumbnail width are stashed in mPendingWidth and the change is - * applied between thumbnail processing. This allows a single thumbnail buffer to - * be used for all thumbnails. - */ -public final class ThumbnailHelper { - private static final boolean DEBUG = false; - private static final String LOGTAG = "GeckoThumbnailHelper"; - - public static final float TABS_PANEL_THUMBNAIL_ASPECT_RATIO = 0.8333333f; - public static final float TOP_SITES_THUMBNAIL_ASPECT_RATIO = 0.571428571f; // this is a 4:7 ratio (as per UX decision) - public static final float THUMBNAIL_ASPECT_RATIO; - - static { - // As we only want to generate one thumbnail for each tab, we calculate the - // largest aspect ratio required and create the thumbnail based off that. - // Any views with a smaller aspect ratio will use a cropped version of the - // same image. - THUMBNAIL_ASPECT_RATIO = Math.max(TABS_PANEL_THUMBNAIL_ASPECT_RATIO, TOP_SITES_THUMBNAIL_ASPECT_RATIO); - } - - public enum CachePolicy { - STORE, - NO_STORE - } - - // static singleton stuff - - private static ThumbnailHelper sInstance; - - public static synchronized ThumbnailHelper getInstance() { - if (sInstance == null) { - sInstance = new ThumbnailHelper(); - } - return sInstance; - } - - // instance stuff - - private final ArrayList mPendingThumbnails; // synchronized access only - private volatile int mPendingWidth; - private int mWidth; - private int mHeight; - private ByteBuffer mBuffer; - - private ThumbnailHelper() { - final Resources res = GeckoAppShell.getContext().getResources(); - - mPendingThumbnails = new ArrayList<>(); - try { - mPendingWidth = (int) res.getDimension(R.dimen.tab_thumbnail_width); - } catch (Resources.NotFoundException nfe) { - } - mWidth = -1; - mHeight = -1; - } - - public void getAndProcessThumbnailFor(final int tabId, final ResourceDrawableUtils.BitmapLoader loader) { - final Tab tab = Tabs.getInstance().getTab(tabId); - if (tab != null) { - getAndProcessThumbnailFor(tab, loader); - } - } - - public void getAndProcessThumbnailFor(final Tab tab, final ResourceDrawableUtils.BitmapLoader loader) { - ResourceDrawableUtils.runOnBitmapFoundOnUiThread(loader, tab.getThumbnail()); - - Tabs.registerOnTabsChangedListener(new Tabs.OnTabsChangedListener() { - @Override - public void onTabChanged(final Tab t, final Tabs.TabEvents msg, final String data) { - if (tab != t || msg != Tabs.TabEvents.THUMBNAIL) { - return; - } - Tabs.unregisterOnTabsChangedListener(this); - ResourceDrawableUtils.runOnBitmapFoundOnUiThread(loader, t.getThumbnail()); - } - }); - getAndProcessThumbnailFor(tab); - } - - public void getAndProcessThumbnailFor(Tab tab) { - if (AboutPages.isAboutHome(tab.getURL()) || AboutPages.isAboutPrivateBrowsing(tab.getURL())) { - tab.updateThumbnail(null, CachePolicy.NO_STORE); - return; - } - - synchronized (mPendingThumbnails) { - if (mPendingThumbnails.lastIndexOf(tab) > 0) { - // This tab is already in the queue, so don't add it again. - // Note that if this tab is only at the *head* of the queue, - // (i.e. mPendingThumbnails.lastIndexOf(tab) == 0) then we do - // add it again because it may have already been thumbnailed - // and now we need to do it again. - return; - } - - mPendingThumbnails.add(tab); - if (mPendingThumbnails.size() > 1) { - // Some thumbnail was already being processed, so wait - // for that to be done. - return; - } - - requestThumbnailLocked(tab); - } - } - - public void setThumbnailWidth(int width) { - // Check inverted for safety: Bug 803299 Comment 34. - if (GeckoAppShell.getScreenDepth() == 24) { - mPendingWidth = width; - } else { - // Bug 776906: on 16-bit screens we need to ensure an even width. - mPendingWidth = (width + 1) & (~1); - } - } - - private void updateThumbnailSizeLocked() { - // Apply any pending width updates. - mWidth = mPendingWidth; - mHeight = Math.round(mWidth * THUMBNAIL_ASPECT_RATIO); - - int pixelSize = (GeckoAppShell.getScreenDepth() == 24) ? 4 : 2; - int capacity = mWidth * mHeight * pixelSize; - if (DEBUG) { - Log.d(LOGTAG, "Using new thumbnail size: " + capacity + - " (width " + mWidth + " - height " + mHeight + ")"); - } - if (mBuffer == null || mBuffer.capacity() != capacity) { - if (mBuffer != null) { - mBuffer = DirectBufferAllocator.free(mBuffer); - } - try { - mBuffer = DirectBufferAllocator.allocate(capacity); - } catch (IllegalArgumentException iae) { - Log.w(LOGTAG, iae.toString()); - } catch (OutOfMemoryError oom) { - Log.w(LOGTAG, "Unable to allocate thumbnail buffer of capacity " + capacity); - } - // If we hit an error above, mBuffer will be pointing to null, so we are in a sane state. - } - } - - private void requestThumbnailLocked(Tab tab) { - updateThumbnailSizeLocked(); - - if (mBuffer == null) { - // Buffer allocation may have failed. In this case we can't send the - // event requesting the screenshot which means we won't get back a response - // and so our queue will grow unboundedly. Handle this scenario by clearing - // the queue (no point trying more thumbnailing right now since we're likely - // low on memory). We will try again normally on the next call to - // getAndProcessThumbnailFor which will hopefully be when we have more free memory. - mPendingThumbnails.clear(); - return; - } - - if (DEBUG) { - Log.d(LOGTAG, "Sending thumbnail event: " + mWidth + ", " + mHeight); - } - requestThumbnailLocked(mBuffer, tab, tab.getId(), mWidth, mHeight); - } - - @WrapForJNI(stubName = "RequestThumbnail", dispatchTo = "proxy") - private static native void requestThumbnailLocked(ByteBuffer data, Tab tab, int tabId, - int width, int height); - - /* This method is invoked by JNI once the thumbnail data is ready. */ - @WrapForJNI(calledFrom = "gecko") - private static void notifyThumbnail(final ByteBuffer data, final Tab tab, - final boolean success, final boolean shouldStore) { - final ThumbnailHelper helper = ThumbnailHelper.getInstance(); - if (success) { - helper.handleThumbnailData( - tab, data, shouldStore ? CachePolicy.STORE : CachePolicy.NO_STORE); - } - helper.processNextThumbnail(); - } - - private void processNextThumbnail() { - synchronized (mPendingThumbnails) { - if (mPendingThumbnails.isEmpty()) { - return; - } - - mPendingThumbnails.remove(0); - - if (!mPendingThumbnails.isEmpty()) { - requestThumbnailLocked(mPendingThumbnails.get(0)); - } - } - } - - private void handleThumbnailData(Tab tab, ByteBuffer data, CachePolicy cachePolicy) { - if (DEBUG) { - Log.d(LOGTAG, "handleThumbnailData: " + data.capacity()); - } - if (data != mBuffer) { - // This should never happen, but log it and recover gracefully - Log.e(LOGTAG, "handleThumbnailData called with an unexpected ByteBuffer!"); - } - - processThumbnailData(tab, data, cachePolicy); - } - - private void processThumbnailData(Tab tab, ByteBuffer data, CachePolicy cachePolicy) { - Bitmap b = tab.getThumbnailBitmap(mWidth, mHeight); - data.position(0); - b.copyPixelsFromBuffer(data); - setTabThumbnail(tab, b, null, cachePolicy); - } - - private void setTabThumbnail(Tab tab, Bitmap bitmap, byte[] compressed, CachePolicy cachePolicy) { - if (bitmap == null) { - if (compressed == null) { - Log.w(LOGTAG, "setTabThumbnail: one of bitmap or compressed must be non-null!"); - return; - } - bitmap = BitmapUtils.decodeByteArray(compressed); - } - tab.updateThumbnail(bitmap, cachePolicy); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/ZoomedView.java b/mobile/android/base/java/org/mozilla/gecko/ZoomedView.java deleted file mode 100644 index c0c9307dcf..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/ZoomedView.java +++ /dev/null @@ -1,838 +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; - -import org.mozilla.gecko.animation.ViewHelper; -import org.mozilla.gecko.annotation.WrapForJNI; -import org.mozilla.gecko.gfx.ImmutableViewportMetrics; -import org.mozilla.gecko.gfx.LayerView; -import org.mozilla.gecko.gfx.PanZoomController; -import org.mozilla.gecko.gfx.PointUtils; -import org.mozilla.gecko.mozglue.DirectBufferAllocator; -import org.mozilla.gecko.PrefsHelper; -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.content.res.Resources; -import android.content.res.Configuration; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.graphics.BitmapShader; -import android.graphics.Canvas; -import android.graphics.drawable.BitmapDrawable; -import android.graphics.Matrix; -import android.graphics.Paint; -import android.graphics.Point; -import android.graphics.PointF; -import android.graphics.RectF; -import android.graphics.Shader; -import android.util.AttributeSet; -import android.util.Log; -import android.view.MotionEvent; -import android.view.View; -import android.view.ViewTreeObserver; -import android.view.animation.Animation; -import android.view.animation.Animation.AnimationListener; -import android.view.animation.OvershootInterpolator; -import android.view.animation.ScaleAnimation; -import android.widget.FrameLayout; -import android.widget.ImageView; -import android.widget.RelativeLayout; -import android.widget.TextView; - -import java.nio.ByteBuffer; -import java.text.DecimalFormat; - -public class ZoomedView extends FrameLayout implements LayerView.DynamicToolbarListener, - LayerView.ZoomedViewListener, GeckoEventListener { - private static final String LOGTAG = "Gecko" + ZoomedView.class.getSimpleName(); - - private static final float[] ZOOM_FACTORS_LIST = {2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f, 8.0f, 9.0f, 10.0f, 1.5f}; - private static final int W_CAPTURED_VIEW_IN_PERCENT = 50; - private static final int H_CAPTURED_VIEW_IN_PERCENT = 50; - private static final int MINIMUM_DELAY_BETWEEN_TWO_RENDER_CALLS_NS = 1000000; - private static final int DELAY_BEFORE_NEXT_RENDER_REQUEST_MS = 2000; - private static final int OPENING_ANIMATION_DURATION_MS = 250; - private static final int CLOSING_ANIMATION_DURATION_MS = 150; - private static final float OVERSHOOT_INTERPOLATOR_TENSION = 1.5f; - - private float zoomFactor; - private int currentZoomFactorIndex; - private boolean isSimplifiedUI; - private int defaultZoomFactor; - private PrefsHelper.PrefHandler prefObserver; - - private ImageView zoomedImageView; - private LayerView layerView; - private int viewWidth; - private int viewHeight; // Only the zoomed view height, no toolbar, no shadow ... - private int viewContainerWidth; - private int viewContainerHeight; // Zoomed view height with toolbar and other elements like shadow, ... - private int containterSize; // shadow, margin, ... - private Point lastPosition; - private boolean shouldSetVisibleOnUpdate; - private boolean isBlockedFromAppearing; // Prevent the display of the zoomedview while FormAssistantPopup is visible - private PointF returnValue; - private final PointF animationStart; - private ImageView closeButton; - private TextView changeZoomFactorButton; - private boolean toolbarOnTop; - private float offsetDueToToolBarPosition; - private int toolbarHeight; - private int cornerRadius; - private float dynamicToolbarOverlap; - - private boolean stopUpdateView; - - private int lastOrientation; - - private ByteBuffer buffer; - private Runnable requestRenderRunnable; - private long startTimeReRender; - private long lastStartTimeReRender; - - private ZoomedViewTouchListener touchListener; - - private enum StartPointUpdate { - GECKO_POSITION, CENTER, NO_CHANGE - } - - private class RoundedBitmapDrawable extends BitmapDrawable { - private Paint paint = new Paint(Paint.FILTER_BITMAP_FLAG | Paint.DITHER_FLAG); - final float cornerRadius; - final boolean squareOnTopOfDrawable; - - RoundedBitmapDrawable(Resources res, Bitmap bitmap, boolean squareOnTop, int radius) { - super(res, bitmap); - squareOnTopOfDrawable = squareOnTop; - final BitmapShader shader = new BitmapShader(bitmap, Shader.TileMode.CLAMP, - Shader.TileMode.CLAMP); - paint.setAntiAlias(true); - paint.setShader(shader); - cornerRadius = radius; - } - - @Override - public void draw(Canvas canvas) { - int height = getBounds().height(); - int width = getBounds().width(); - RectF rect = new RectF(0.0f, 0.0f, width, height); - canvas.drawRoundRect(rect, cornerRadius, cornerRadius, paint); - - //draw rectangles over the corners we want to be square - if (squareOnTopOfDrawable) { - canvas.drawRect(0, 0, cornerRadius, cornerRadius, paint); - canvas.drawRect(width - cornerRadius, 0, width, cornerRadius, paint); - } else { - canvas.drawRect(0, height - cornerRadius, cornerRadius, height, paint); - canvas.drawRect(width - cornerRadius, height - cornerRadius, width, height, paint); - } - } - } - - private class ZoomedViewTouchListener implements View.OnTouchListener { - private float originRawX; - private float originRawY; - private boolean dragged; - private MotionEvent actionDownEvent; - - @Override - public boolean onTouch(View view, MotionEvent event) { - if (layerView == null) { - return false; - } - - switch (event.getAction()) { - case MotionEvent.ACTION_MOVE: - if (moveZoomedView(event)) { - dragged = true; - } - break; - - case MotionEvent.ACTION_UP: - if (dragged) { - dragged = false; - } else { - if (isClickInZoomedView(event.getY())) { - GeckoAppShell.notifyObservers("Gesture:ClickInZoomedView", ""); - layerView.dispatchTouchEvent(actionDownEvent); - actionDownEvent.recycle(); - PointF convertedPosition = getUnzoomedPositionFromPointInZoomedView(event.getX(), event.getY()); - // the LayerView expects the coordinates relative to the window, not the surface, so we need - // to adjust that here. - convertedPosition.y += layerView.getSurfaceTranslation(); - MotionEvent e = MotionEvent.obtain(event.getDownTime(), event.getEventTime(), - MotionEvent.ACTION_UP, convertedPosition.x, convertedPosition.y, - event.getMetaState()); - layerView.dispatchTouchEvent(e); - e.recycle(); - } - } - break; - - case MotionEvent.ACTION_DOWN: - dragged = false; - originRawX = event.getRawX(); - originRawY = event.getRawY(); - PointF convertedPosition = getUnzoomedPositionFromPointInZoomedView(event.getX(), event.getY()); - // the LayerView expects the coordinates relative to the window, not the surface, so we need - // to adjust that here. - convertedPosition.y += layerView.getSurfaceTranslation(); - actionDownEvent = MotionEvent.obtain(event.getDownTime(), event.getEventTime(), - MotionEvent.ACTION_DOWN, convertedPosition.x, convertedPosition.y, - event.getMetaState()); - break; - } - return true; - } - - private boolean isClickInZoomedView(float y) { - return ((toolbarOnTop && y > toolbarHeight) || - (!toolbarOnTop && y < ZoomedView.this.viewHeight)); - } - - private boolean moveZoomedView(MotionEvent event) { - RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) ZoomedView.this.getLayoutParams(); - if ((!dragged) && (Math.abs((int) (event.getRawX() - originRawX)) < PanZoomController.CLICK_THRESHOLD) - && (Math.abs((int) (event.getRawY() - originRawY)) < PanZoomController.CLICK_THRESHOLD)) { - // When the user just touches the screen ACTION_MOVE can be detected for a very small delta on position. - // In this case, the move is ignored if the delta is lower than 1 unit. - return false; - } - - float newLeftMargin = params.leftMargin + event.getRawX() - originRawX; - float newTopMargin = params.topMargin + event.getRawY() - originRawY; - ImmutableViewportMetrics metrics = layerView.getViewportMetrics(); - ZoomedView.this.moveZoomedView(metrics, newLeftMargin, newTopMargin, StartPointUpdate.CENTER); - originRawX = event.getRawX(); - originRawY = event.getRawY(); - return true; - } - } - - public ZoomedView(Context context) { - this(context, null, 0); - } - - public ZoomedView(Context context, AttributeSet attrs) { - this(context, attrs, 0); - } - - public ZoomedView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - isSimplifiedUI = true; - isBlockedFromAppearing = false; - getPrefs(); - currentZoomFactorIndex = 0; - returnValue = new PointF(); - animationStart = new PointF(); - requestRenderRunnable = new Runnable() { - @Override - public void run() { - requestZoomedViewRender(); - } - }; - touchListener = new ZoomedViewTouchListener(); - GeckoApp.getEventDispatcher().registerGeckoThreadListener(this, - "Gesture:clusteredLinksClicked", "Window:Resize", "Content:LocationChange", - "Gesture:CloseZoomedView", "Browser:ZoomToPageWidth", "Browser:ZoomToRect", - "FormAssist:AutoComplete", "FormAssist:Hide"); - } - - void destroy() { - if (prefObserver != null) { - PrefsHelper.removeObserver(prefObserver); - prefObserver = null; - } - ThreadUtils.removeCallbacksFromUiThread(requestRenderRunnable); - GeckoApp.getEventDispatcher().unregisterGeckoThreadListener(this, - "Gesture:clusteredLinksClicked", "Window:Resize", "Content:LocationChange", - "Gesture:CloseZoomedView", "Browser:ZoomToPageWidth", "Browser:ZoomToRect", - "FormAssist:AutoComplete", "FormAssist:Hide"); - } - - // This method (onFinishInflate) is called only when the zoomed view class is used inside - // an xml structure = (changeZoomFactorButton.getLeft() + changeZoomFactorButton.getWidth() / 2)) { - changeZoomFactor(true); - } else { - changeZoomFactor(false); - } - } - return true; - } - }); - - setOnTouchListener(touchListener); - } - - private void removeListeners() { - closeButton.setOnClickListener(null); - - changeZoomFactorButton.setOnTouchListener(null); - - setOnTouchListener(null); - } - /* - * Convert a click from ZoomedView. Return the position of the click in the - * LayerView - */ - private PointF getUnzoomedPositionFromPointInZoomedView(float x, float y) { - ImmutableViewportMetrics metrics = layerView.getViewportMetrics(); - final float parentWidth = metrics.getWidth(); - final float parentHeight = metrics.getHeight(); - RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) getLayoutParams(); - - // The number of unzoomed content pixels that can be displayed in the - // zoomed area. - float visibleContentPixels = viewWidth / zoomFactor; - // The offset in content pixels of the leftmost zoomed pixel from the - // layerview's left edge when the zoomed view is moved to the right as - // far as it can go. - float maxContentOffset = parentWidth - visibleContentPixels; - // The maximum offset in screen pixels that the zoomed view can have - float maxZoomedViewOffset = parentWidth - viewContainerWidth; - - // The above values allow us to compute the term - // maxContentOffset / maxZoomedViewOffset - // which is the number of content pixels that we should move over by - // for every screen pixel that the zoomed view is moved over by. - // This allows a smooth transition from when the zoomed view is at the - // leftmost extent to when it is at the rightmost extent. - - // This is the offset in content pixels of the leftmost zoomed pixel - // visible in the zoomed view. This value is relative to the layerview - // edge. - float zoomedContentOffset = ((float)params.leftMargin) * maxContentOffset / maxZoomedViewOffset; - returnValue.x = (int)(zoomedContentOffset + (x / zoomFactor)); - - // Same comments here vertically - visibleContentPixels = viewHeight / zoomFactor; - maxContentOffset = parentHeight - visibleContentPixels; - maxZoomedViewOffset = parentHeight - (viewContainerHeight - toolbarHeight); - float zoomedAreaOffset = (float)params.topMargin + offsetDueToToolBarPosition - layerView.getSurfaceTranslation(); - zoomedContentOffset = zoomedAreaOffset * maxContentOffset / maxZoomedViewOffset; - returnValue.y = (int)(zoomedContentOffset + ((y - offsetDueToToolBarPosition) / zoomFactor)); - - return returnValue; - } - - /* - * A touch point (x,y) occurs in LayerView, this point should be displayed - * in the center of the zoomed view. The returned point is the position of - * the Top-Left zoomed view point on the screen device - */ - private PointF getZoomedViewTopLeftPositionFromTouchPosition(float x, float y) { - ImmutableViewportMetrics metrics = layerView.getViewportMetrics(); - final float parentWidth = metrics.getWidth(); - final float parentHeight = metrics.getHeight(); - - // See comments in getUnzoomedPositionFromPointInZoomedView, but the - // transformations here are largely the reverse of that function. - - float visibleContentPixels = viewWidth / zoomFactor; - float maxContentOffset = parentWidth - visibleContentPixels; - float maxZoomedViewOffset = parentWidth - viewContainerWidth; - float contentPixelOffset = x - (visibleContentPixels / 2.0f); - returnValue.x = (int)(contentPixelOffset * (maxZoomedViewOffset / maxContentOffset)); - - visibleContentPixels = viewHeight / zoomFactor; - maxContentOffset = parentHeight - visibleContentPixels; - maxZoomedViewOffset = parentHeight - (viewContainerHeight - toolbarHeight); - contentPixelOffset = y - (visibleContentPixels / 2.0f); - float unscaledViewOffset = layerView.getSurfaceTranslation() - offsetDueToToolBarPosition; - returnValue.y = (int)((contentPixelOffset * (maxZoomedViewOffset / maxContentOffset)) + unscaledViewOffset); - - return returnValue; - } - - private void moveZoomedView(ImmutableViewportMetrics metrics, float newLeftMargin, float newTopMargin, - StartPointUpdate animateStartPoint) { - RelativeLayout.LayoutParams newLayoutParams = (RelativeLayout.LayoutParams) getLayoutParams(); - newLayoutParams.leftMargin = (int) newLeftMargin; - newLayoutParams.topMargin = (int) newTopMargin; - int topMarginMin = (int)(layerView.getSurfaceTranslation() + dynamicToolbarOverlap); - int topMarginMax = layerView.getHeight() - viewContainerHeight; - int leftMarginMin = 0; - int leftMarginMax = layerView.getWidth() - viewContainerWidth; - - if (newTopMargin < topMarginMin) { - newLayoutParams.topMargin = topMarginMin; - } else if (newTopMargin > topMarginMax) { - newLayoutParams.topMargin = topMarginMax; - } - - if (newLeftMargin < leftMarginMin) { - newLayoutParams.leftMargin = leftMarginMin; - } else if (newLeftMargin > leftMarginMax) { - newLayoutParams.leftMargin = leftMarginMax; - } - - if (newLayoutParams.topMargin < topMarginMin + 1) { - moveToolbar(false); - } else if (newLayoutParams.topMargin > topMarginMax - 1) { - moveToolbar(true); - } - - if (animateStartPoint == StartPointUpdate.GECKO_POSITION) { - // Before this point, the animationStart point is relative to the layerView. - // The value is initialized in startZoomDisplay using the click point position coming from Gecko. - // The position of the zoomed view is now calculated, so the position of the animation - // can now be correctly set relative to the zoomed view - animationStart.x = animationStart.x - newLayoutParams.leftMargin; - animationStart.y = animationStart.y - newLayoutParams.topMargin; - } else if (animateStartPoint == StartPointUpdate.CENTER) { - // At this point, the animationStart point is no more valid probably because - // the zoomed view has been moved by the user. - // In this case, the animationStart point is set to the center point of the zoomed view. - PointF convertedPosition = getUnzoomedPositionFromPointInZoomedView(viewContainerWidth / 2, viewContainerHeight / 2); - animationStart.x = convertedPosition.x - newLayoutParams.leftMargin; - animationStart.y = convertedPosition.y - newLayoutParams.topMargin; - } - - setLayoutParams(newLayoutParams); - PointF convertedPosition = getUnzoomedPositionFromPointInZoomedView(0, offsetDueToToolBarPosition); - lastPosition = PointUtils.round(convertedPosition); - requestZoomedViewRender(); - } - - private void moveToolbar(boolean moveTop) { - if (toolbarOnTop == moveTop) { - return; - } - toolbarOnTop = moveTop; - if (toolbarOnTop) { - offsetDueToToolBarPosition = toolbarHeight; - } else { - offsetDueToToolBarPosition = 0; - } - - RelativeLayout.LayoutParams p = (RelativeLayout.LayoutParams) zoomedImageView.getLayoutParams(); - RelativeLayout.LayoutParams pChangeZoomFactorButton = (RelativeLayout.LayoutParams) changeZoomFactorButton.getLayoutParams(); - RelativeLayout.LayoutParams pCloseButton = (RelativeLayout.LayoutParams) closeButton.getLayoutParams(); - - if (moveTop) { - p.addRule(RelativeLayout.BELOW, R.id.change_zoom_factor); - pChangeZoomFactorButton.addRule(RelativeLayout.BELOW, 0); - pCloseButton.addRule(RelativeLayout.BELOW, 0); - } else { - p.addRule(RelativeLayout.BELOW, 0); - pChangeZoomFactorButton.addRule(RelativeLayout.BELOW, R.id.zoomed_image_view); - pCloseButton.addRule(RelativeLayout.BELOW, R.id.zoomed_image_view); - } - pChangeZoomFactorButton.addRule(RelativeLayout.ALIGN_LEFT, R.id.zoomed_image_view); - pCloseButton.addRule(RelativeLayout.ALIGN_RIGHT, R.id.zoomed_image_view); - zoomedImageView.setLayoutParams(p); - changeZoomFactorButton.setLayoutParams(pChangeZoomFactorButton); - closeButton.setLayoutParams(pCloseButton); - } - - @Override - public void onConfigurationChanged(Configuration newConfig) { - super.onConfigurationChanged(newConfig); - // In case of orientation change, the zoomed view update is stopped until the orientation change - // is completed. At this time, the function onMetricsChanged is called and the - // zoomed view update is restarted again. - if (lastOrientation != newConfig.orientation) { - shouldBlockUpdate(true); - lastOrientation = newConfig.orientation; - } - } - - private void refreshZoomedViewSize(ImmutableViewportMetrics viewport) { - if (layerView == null) { - return; - } - - RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) getLayoutParams(); - setCapturedSize(viewport); - moveZoomedView(viewport, params.leftMargin, params.topMargin, StartPointUpdate.NO_CHANGE); - } - - private void setCapturedSize(ImmutableViewportMetrics metrics) { - float parentMinSize = Math.min(metrics.getWidth(), metrics.getHeight()); - viewWidth = (int) ((parentMinSize * W_CAPTURED_VIEW_IN_PERCENT / (zoomFactor * 100.0)) * zoomFactor); - viewHeight = (int) ((parentMinSize * H_CAPTURED_VIEW_IN_PERCENT / (zoomFactor * 100.0)) * zoomFactor); - viewContainerHeight = viewHeight + toolbarHeight + - 2 * containterSize; // Top and bottom shadows - viewContainerWidth = viewWidth + - 2 * containterSize; // Right and left shadows - // Display in zoomedview is corrupted when width is an odd number - // More details about this issue here: bug 776906 comment 11 - viewWidth &= ~0x1; - } - - private void shouldBlockUpdate(boolean shouldBlockUpdate) { - stopUpdateView = shouldBlockUpdate; - } - - private Bitmap.Config getBitmapConfig() { - return (GeckoAppShell.getScreenDepth() == 24) ? Bitmap.Config.ARGB_8888 : Bitmap.Config.RGB_565; - } - - private void updateUI() { - // onFinishInflate is not yet completed, the update of the UI will be done later - if (changeZoomFactorButton == null) { - return; - } - if (isSimplifiedUI) { - changeZoomFactorButton.setVisibility(View.INVISIBLE); - } else { - setTextInZoomFactorButton(zoomFactor); - changeZoomFactorButton.setVisibility(View.VISIBLE); - } - } - - private void getPrefs() { - prefObserver = new PrefsHelper.PrefHandlerBase() { - @Override - public void prefValue(String pref, boolean simplified) { - isSimplifiedUI = simplified; - if (simplified) { - zoomFactor = (float) defaultZoomFactor; - } else { - zoomFactor = ZOOM_FACTORS_LIST[currentZoomFactorIndex]; - } - updateUI(); - } - - @Override - public void prefValue(String pref, int defaultZoomFactorFromSettings) { - defaultZoomFactor = defaultZoomFactorFromSettings; - if (isSimplifiedUI) { - zoomFactor = (float) defaultZoomFactor; - } else { - zoomFactor = ZOOM_FACTORS_LIST[currentZoomFactorIndex]; - } - updateUI(); - } - }; - PrefsHelper.addObserver(new String[] { "ui.zoomedview.simplified", - "ui.zoomedview.defaultZoomFactor" }, - prefObserver); - } - - private void startZoomDisplay(LayerView aLayerView, final int leftFromGecko, final int topFromGecko) { - if (isBlockedFromAppearing) { - return; - } - if (layerView == null) { - layerView = aLayerView; - layerView.addZoomedViewListener(this); - layerView.getDynamicToolbarAnimator().addTranslationListener(this); - ImmutableViewportMetrics metrics = layerView.getViewportMetrics(); - setCapturedSize(metrics); - } - startTimeReRender = 0; - shouldSetVisibleOnUpdate = true; - - ImmutableViewportMetrics metrics = layerView.getViewportMetrics(); - // At this point, the start point is relative to the layerView. - // Later, it will be converted relative to the zoomed view as soon as - // the position of the zoomed view will be calculated. - animationStart.x = (float) leftFromGecko * metrics.zoomFactor; - animationStart.y = (float) topFromGecko * metrics.zoomFactor + layerView.getSurfaceTranslation(); - - moveUsingGeckoPosition(leftFromGecko, topFromGecko); - } - - public void stopZoomDisplay(boolean withAnimation) { - // If "startZoomDisplay" is running and not totally completed (Gecko thread is still - // running and "showZoomedView" has not yet been called), the zoomed view will be - // displayed after this call and it should not. - // Force the stop of the zoomed view, changing the shouldSetVisibleOnUpdate flag - // before the test of the visibility. - shouldSetVisibleOnUpdate = false; - if (getVisibility() == View.VISIBLE) { - hideZoomedView(withAnimation); - ThreadUtils.removeCallbacksFromUiThread(requestRenderRunnable); - if (layerView != null) { - layerView.getDynamicToolbarAnimator().removeTranslationListener(this); - layerView.removeZoomedViewListener(this); - layerView = null; - } - } - } - - private void changeZoomFactor(boolean zoomIn) { - if (zoomIn && currentZoomFactorIndex < ZOOM_FACTORS_LIST.length - 1) { - currentZoomFactorIndex++; - } else if (zoomIn && currentZoomFactorIndex >= ZOOM_FACTORS_LIST.length - 1) { - currentZoomFactorIndex = 0; - } else if (!zoomIn && currentZoomFactorIndex > 0) { - currentZoomFactorIndex--; - } else { - currentZoomFactorIndex = ZOOM_FACTORS_LIST.length - 1; - } - zoomFactor = ZOOM_FACTORS_LIST[currentZoomFactorIndex]; - - ImmutableViewportMetrics metrics = layerView.getViewportMetrics(); - refreshZoomedViewSize(metrics); - setTextInZoomFactorButton(zoomFactor); - } - - private void setTextInZoomFactorButton(float zoom) { - final String percentageValue = Integer.toString((int) (100 * zoom)); - changeZoomFactorButton.setText("- " + getResources().getString(R.string.percent, percentageValue) + " +"); - } - - @Override - public void handleMessage(final String event, final JSONObject message) { - ThreadUtils.postToUiThread(new Runnable() { - @Override - public void run() { - try { - if (event.equals("Gesture:clusteredLinksClicked")) { - final JSONObject clickPosition = message.getJSONObject("clickPosition"); - int left = clickPosition.getInt("x"); - int top = clickPosition.getInt("y"); - // Start to display inside the zoomedView - LayerView geckoAppLayerView = GeckoAppShell.getLayerView(); - if (geckoAppLayerView != null) { - startZoomDisplay(geckoAppLayerView, left, top); - } - } else if (event.equals("Window:Resize")) { - ImmutableViewportMetrics metrics = layerView.getViewportMetrics(); - refreshZoomedViewSize(metrics); - } else if (event.equals("Content:LocationChange")) { - stopZoomDisplay(false); - } else if (event.equals("Gesture:CloseZoomedView") || - event.equals("Browser:ZoomToPageWidth") || - event.equals("Browser:ZoomToRect")) { - stopZoomDisplay(true); - } else if (event.equals("FormAssist:AutoComplete")) { - isBlockedFromAppearing = true; - stopZoomDisplay(true); - } else if (event.equals("FormAssist:Hide")) { - isBlockedFromAppearing = false; - } - } catch (JSONException e) { - Log.e(LOGTAG, "JSON exception", e); - } - } - }); - } - - private void moveUsingGeckoPosition(int leftFromGecko, int topFromGecko) { - ImmutableViewportMetrics metrics = layerView.getViewportMetrics(); - final float parentHeight = metrics.getHeight(); - // moveToolbar is called before getZoomedViewTopLeftPositionFromTouchPosition in order to - // correctly center vertically the zoomed area - moveToolbar((topFromGecko * metrics.zoomFactor > parentHeight / 2)); - PointF convertedPosition = getZoomedViewTopLeftPositionFromTouchPosition((leftFromGecko * metrics.zoomFactor), - (topFromGecko * metrics.zoomFactor)); - moveZoomedView(metrics, convertedPosition.x, convertedPosition.y, StartPointUpdate.GECKO_POSITION); - } - - @Override - public void onTranslationChanged(float aToolbarTranslation, float aLayerViewTranslation) { - ThreadUtils.assertOnUiThread(); - if (layerView != null) { - dynamicToolbarOverlap = aLayerViewTranslation - aToolbarTranslation; - refreshZoomedViewSize(layerView.getViewportMetrics()); - } - } - - @Override - public void onMetricsChanged(final ImmutableViewportMetrics viewport) { - // It can be called from a Gecko thread (forceViewportMetrics in GeckoLayerClient). - // Post to UI Thread to avoid Exception: - // "Only the original thread that created a view hierarchy can touch its views." - ThreadUtils.postToUiThread(new Runnable() { - @Override - public void run() { - shouldBlockUpdate(false); - refreshZoomedViewSize(viewport); - } - }); - } - - @Override - public void onPanZoomStopped() { - } - - @Override - public void updateView(ByteBuffer data) { - final Bitmap sb3 = Bitmap.createBitmap(viewWidth, viewHeight, getBitmapConfig()); - if (sb3 != null) { - data.rewind(); - try { - sb3.copyPixelsFromBuffer(data); - } catch (Exception iae) { - Log.w(LOGTAG, iae.toString()); - } - if (zoomedImageView != null) { - RoundedBitmapDrawable ob3 = new RoundedBitmapDrawable(getResources(), sb3, toolbarOnTop, cornerRadius); - zoomedImageView.setImageDrawable(ob3); - } - } - if (shouldSetVisibleOnUpdate) { - this.showZoomedView(); - } - lastStartTimeReRender = startTimeReRender; - startTimeReRender = 0; - } - - private void showZoomedView() { - // no animation if the zoomed view is already visible - if (getVisibility() != View.VISIBLE) { - final Animation anim = new ScaleAnimation( - 0f, 1f, // Start and end values for the X axis scaling - 0f, 1f, // Start and end values for the Y axis scaling - Animation.ABSOLUTE, animationStart.x, // Pivot point of X scaling - Animation.ABSOLUTE, animationStart.y); // Pivot point of Y scaling - anim.setFillAfter(true); // Needed to keep the result of the animation - anim.setDuration(OPENING_ANIMATION_DURATION_MS); - anim.setInterpolator(new OvershootInterpolator(OVERSHOOT_INTERPOLATOR_TENSION)); - anim.setAnimationListener(new AnimationListener() { - public void onAnimationEnd(Animation animation) { - setListeners(); - } - public void onAnimationRepeat(Animation animation) { - } - public void onAnimationStart(Animation animation) { - removeListeners(); - } - }); - setAnimation(anim); - } - setVisibility(View.VISIBLE); - shouldSetVisibleOnUpdate = false; - } - - private void hideZoomedView(boolean withAnimation) { - if (withAnimation) { - final Animation anim = new ScaleAnimation( - 1f, 0f, // Start and end values for the X axis scaling - 1f, 0f, // Start and end values for the Y axis scaling - Animation.ABSOLUTE, animationStart.x, // Pivot point of X scaling - Animation.ABSOLUTE, animationStart.y); // Pivot point of Y scaling - anim.setFillAfter(true); // Needed to keep the result of the animation - anim.setDuration(CLOSING_ANIMATION_DURATION_MS); - anim.setAnimationListener(new AnimationListener() { - public void onAnimationEnd(Animation animation) { - } - public void onAnimationRepeat(Animation animation) { - } - public void onAnimationStart(Animation animation) { - removeListeners(); - } - }); - setAnimation(anim); - } else { - removeListeners(); - setAnimation(null); - } - setVisibility(View.GONE); - shouldSetVisibleOnUpdate = false; - } - - private void updateBufferSize() { - int pixelSize = (GeckoAppShell.getScreenDepth() == 24) ? 4 : 2; - int capacity = viewWidth * viewHeight * pixelSize; - if (buffer == null || buffer.capacity() != capacity) { - buffer = DirectBufferAllocator.free(buffer); - buffer = DirectBufferAllocator.allocate(capacity); - } - } - - private boolean isRendering() { - return (startTimeReRender != 0); - } - - private boolean renderFrequencyTooHigh() { - return ((System.nanoTime() - lastStartTimeReRender) < MINIMUM_DELAY_BETWEEN_TWO_RENDER_CALLS_NS); - } - - @WrapForJNI(dispatchTo = "gecko") - private static native void requestZoomedViewData(ByteBuffer buffer, int tabId, - int xPos, int yPos, int width, - int height, float scale); - - @Override - public void requestZoomedViewRender() { - if (stopUpdateView) { - return; - } - // remove pending runnable - ThreadUtils.removeCallbacksFromUiThread(requestRenderRunnable); - - // "requestZoomedViewRender" can be called very often by Gecko (endDrawing in LayerRender) without - // any thing changed in the zoomed area (useless calls from the "zoomed area" point of view). - // "requestZoomedViewRender" can take time to re-render the zoomed view, it depends of the complexity - // of the html on this area. - // To avoid to slow down the application, the 2 following cases are tested: - - // 1- Last render is still running, plan another render later. - if (isRendering()) { - // post a new runnable DELAY_BEFORE_NEXT_RENDER_REQUEST_MS later - // We need to post with a delay to be sure that the last call to requestZoomedViewRender will be done. - // For a static html page WITHOUT any animation/video, there is a last call to endDrawing and we need to make - // the zoomed render on this last call. - ThreadUtils.postDelayedToUiThread(requestRenderRunnable, DELAY_BEFORE_NEXT_RENDER_REQUEST_MS); - return; - } - - // 2- Current render occurs too early, plan another render later. - if (renderFrequencyTooHigh()) { - // post a new runnable DELAY_BEFORE_NEXT_RENDER_REQUEST_MS later - // We need to post with a delay to be sure that the last call to requestZoomedViewRender will be done. - // For a page WITH animation/video, the animation/video can be stopped, and we need to make - // the zoomed render on this last call. - ThreadUtils.postDelayedToUiThread(requestRenderRunnable, DELAY_BEFORE_NEXT_RENDER_REQUEST_MS); - return; - } - - startTimeReRender = System.nanoTime(); - // Allocate the buffer if it's the first call. - // Change the buffer size if it's not the right size. - updateBufferSize(); - - int tabId = Tabs.getInstance().getSelectedTab().getId(); - - ImmutableViewportMetrics metrics = layerView.getViewportMetrics(); - PointF origin = metrics.getOrigin(); - - final int xPos = (int)origin.x + lastPosition.x; - final int yPos = (int)origin.y + lastPosition.y; - - requestZoomedViewData(buffer, tabId, xPos, yPos, viewWidth, viewHeight, - zoomFactor * metrics.zoomFactor); - } - -} diff --git a/mobile/android/base/java/org/mozilla/gecko/activitystream/ActivityStream.java b/mobile/android/base/java/org/mozilla/gecko/activitystream/ActivityStream.java deleted file mode 100644 index d1c3f59169..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/activitystream/ActivityStream.java +++ /dev/null @@ -1,149 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.activitystream; - -import android.content.Context; -import android.net.Uri; -import android.os.AsyncTask; -import android.text.TextUtils; - -import com.keepsafe.switchboard.SwitchBoard; - -import org.mozilla.gecko.AppConstants; -import org.mozilla.gecko.Experiments; -import org.mozilla.gecko.GeckoSharedPrefs; -import org.mozilla.gecko.preferences.GeckoPreferences; -import org.mozilla.gecko.util.StringUtils; -import org.mozilla.gecko.util.publicsuffix.PublicSuffix; - -import java.util.Arrays; -import java.util.List; - -public class ActivityStream { - /** - * List of undesired prefixes for labels based on a URL. - * - * This list is by no means complete and is based on those sources: - * - https://gist.github.com/nchapman/36502ad115e8825d522a66549971a3f0 - * - https://github.com/mozilla/activity-stream/issues/1311 - */ - private static final List UNDESIRED_LABEL_PREFIXES = Arrays.asList( - "index.", - "home." - ); - - /** - * Undesired labels for labels based on a URL. - * - * This list is by no means complete and is based on those sources: - * - https://gist.github.com/nchapman/36502ad115e8825d522a66549971a3f0 - * - https://github.com/mozilla/activity-stream/issues/1311 - */ - private static final List UNDESIRED_LABELS = Arrays.asList( - "render", - "login", - "edit" - ); - - public static boolean isEnabled(Context context) { - if (!isUserEligible(context)) { - // If the user is not eligible then disable activity stream. Even if it has been - // enabled before. - return false; - } - - return GeckoSharedPrefs.forApp(context) - .getBoolean(GeckoPreferences.PREFS_ACTIVITY_STREAM, false); - } - - /** - * Is the user eligible to use activity stream or should we hide it from settings etc.? - */ - public static boolean isUserEligible(Context context) { - if (AppConstants.MOZ_ANDROID_ACTIVITY_STREAM) { - // If the build flag is enabled then just show the option to the user. - return true; - } - - if (AppConstants.NIGHTLY_BUILD && SwitchBoard.isInExperiment(context, Experiments.ACTIVITY_STREAM)) { - // If this is a nightly build and the user is part of the activity stream experiment then - // the option should be visible too. The experiment is limited to Nightly too but I want - // to make really sure that this isn't riding the trains accidentally. - return true; - } - - // For everyone else activity stream is not available yet. - return false; - } - - /** - * Query whether we want to display Activity Stream as a Home Panel (within the HomePager), - * or as a HomePager replacement. - */ - public static boolean isHomePanel() { - return true; - } - - /** - * Extract a label from a URL to use in Activity Stream. - * - * This method implements the proposal from this desktop AS issue: - * https://github.com/mozilla/activity-stream/issues/1311 - * - * @param usePath Use the path of the URL to extract a label (if suitable) - */ - public static void extractLabel(final Context context, final String url, final boolean usePath, final LabelCallback callback) { - new AsyncTask() { - @Override - protected String doInBackground(Void... params) { - if (TextUtils.isEmpty(url)) { - return ""; - } - - final Uri uri = Uri.parse(url); - - // Use last path segment if suitable - if (usePath) { - final String segment = uri.getLastPathSegment(); - if (!TextUtils.isEmpty(segment) - && !UNDESIRED_LABELS.contains(segment) - && !segment.matches("^[0-9]+$")) { - - boolean hasUndesiredPrefix = false; - for (int i = 0; i < UNDESIRED_LABEL_PREFIXES.size(); i++) { - if (segment.startsWith(UNDESIRED_LABEL_PREFIXES.get(i))) { - hasUndesiredPrefix = true; - break; - } - } - - if (!hasUndesiredPrefix) { - return segment; - } - } - } - - // If no usable path segment was found then use the host without public suffix and common subdomains - final String host = uri.getHost(); - if (TextUtils.isEmpty(host)) { - return url; - } - - return StringUtils.stripCommonSubdomains( - PublicSuffix.stripPublicSuffix(context, host)); - } - - @Override - protected void onPostExecute(String label) { - callback.onLabelExtracted(label); - } - }.execute(); - } - - public abstract static class LabelCallback { - public abstract void onLabelExtracted(String label); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/adjust/AdjustBrowserAppDelegate.java b/mobile/android/base/java/org/mozilla/gecko/adjust/AdjustBrowserAppDelegate.java deleted file mode 100644 index aee0bba63a..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/adjust/AdjustBrowserAppDelegate.java +++ /dev/null @@ -1,52 +0,0 @@ -package org.mozilla.gecko.adjust; - -import android.content.SharedPreferences; -import android.os.Bundle; - -import org.mozilla.gecko.AdjustConstants; -import org.mozilla.gecko.BrowserApp; -import org.mozilla.gecko.GeckoSharedPrefs; -import org.mozilla.gecko.delegates.BrowserAppDelegate; -import org.mozilla.gecko.mozglue.SafeIntent; -import org.mozilla.gecko.preferences.GeckoPreferences; -import org.mozilla.gecko.util.IntentUtils; - -public class AdjustBrowserAppDelegate extends BrowserAppDelegate { - private final AdjustHelperInterface adjustHelper; - private final AttributionHelperListener attributionHelperListener; - - public AdjustBrowserAppDelegate(AttributionHelperListener attributionHelperListener) { - this.adjustHelper = AdjustConstants.getAdjustHelper(); - this.attributionHelperListener = attributionHelperListener; - } - - @Override - public void onCreate(BrowserApp browserApp, Bundle savedInstanceState) { - adjustHelper.onCreate(browserApp, - AdjustConstants.MOZ_INSTALL_TRACKING_ADJUST_SDK_APP_TOKEN, - attributionHelperListener); - - final boolean isInAutomation = IntentUtils.getIsInAutomationFromEnvironment( - new SafeIntent(browserApp.getIntent())); - - final SharedPreferences prefs = GeckoSharedPrefs.forApp(browserApp); - - // Adjust stores enabled state so this is only necessary because users may have set - // their data preferences before this feature was implemented and we need to respect - // those before upload can occur in Adjust.onResume. - adjustHelper.setEnabled(!isInAutomation - && prefs.getBoolean(GeckoPreferences.PREFS_HEALTHREPORT_UPLOAD_ENABLED, true)); - } - - @Override - public void onResume(BrowserApp browserApp) { - // Needed for Adjust to get accurate session measurements - adjustHelper.onResume(); - } - - @Override - public void onPause(BrowserApp browserApp) { - // Needed for Adjust to get accurate session measurements - adjustHelper.onPause(); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/adjust/AdjustHelper.java b/mobile/android/base/java/org/mozilla/gecko/adjust/AdjustHelper.java deleted file mode 100644 index 19399e735b..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/adjust/AdjustHelper.java +++ /dev/null @@ -1,75 +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.adjust; - -import android.content.Context; -import android.content.Intent; -import android.util.Log; - -import com.adjust.sdk.Adjust; -import com.adjust.sdk.AdjustAttribution; -import com.adjust.sdk.AdjustConfig; -import com.adjust.sdk.AdjustReferrerReceiver; -import com.adjust.sdk.LogLevel; -import com.adjust.sdk.OnAttributionChangedListener; - -import org.mozilla.gecko.AppConstants; - -public class AdjustHelper implements AdjustHelperInterface, OnAttributionChangedListener { - - private static final String LOGTAG = AdjustHelper.class.getSimpleName(); - private AttributionHelperListener attributionListener; - - public void onCreate(final Context context, final String maybeAppToken, final AttributionHelperListener listener) { - final String environment; - final LogLevel logLevel; - if (AppConstants.MOZILLA_OFFICIAL) { - environment = AdjustConfig.ENVIRONMENT_PRODUCTION; - logLevel = LogLevel.WARN; - } else { - environment = AdjustConfig.ENVIRONMENT_SANDBOX; - logLevel = LogLevel.VERBOSE; - } - if (maybeAppToken == null) { - // We've got install tracking turned on -- we better have a token! - throw new IllegalArgumentException("maybeAppToken must not be null"); - } - attributionListener = listener; - AdjustConfig config = new AdjustConfig(context, maybeAppToken, environment); - config.setLogLevel(logLevel); - config.setOnAttributionChangedListener(this); - Adjust.onCreate(config); - } - - public void onPause() { - Adjust.onPause(); - } - - public void onResume() { - Adjust.onResume(); - } - - public void setEnabled(final boolean isEnabled) { - Adjust.setEnabled(isEnabled); - } - - public void onReceive(final Context context, final Intent intent) { - new AdjustReferrerReceiver().onReceive(context, intent); - } - - @Override - public void onAttributionChanged(AdjustAttribution attribution) { - if (attributionListener == null) { - throw new IllegalStateException("Expected non-null attribution listener."); - } - - if (attribution == null) { - Log.e(LOGTAG, "Adjust attribution is null; skipping campaign id retrieval."); - return; - } - attributionListener.onCampaignIdChanged(attribution.campaign); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/adjust/AdjustHelperInterface.java b/mobile/android/base/java/org/mozilla/gecko/adjust/AdjustHelperInterface.java deleted file mode 100644 index aeb7b4334e..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/adjust/AdjustHelperInterface.java +++ /dev/null @@ -1,22 +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.adjust; - -import android.content.Context; -import android.content.Intent; - -public interface AdjustHelperInterface { - /** - * Register the Application with the Adjust SDK. - * @param appToken the (secret!) Adjust SDK per-application token to register with; may be null. - */ - void onCreate(final Context context, final String appToken, final AttributionHelperListener listener); - void onPause(); - void onResume(); - - void setEnabled(final boolean isEnabled); - void onReceive(final Context context, final Intent intent); -} diff --git a/mobile/android/base/java/org/mozilla/gecko/adjust/AttributionHelperListener.java b/mobile/android/base/java/org/mozilla/gecko/adjust/AttributionHelperListener.java deleted file mode 100644 index 6dadd2261d..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/adjust/AttributionHelperListener.java +++ /dev/null @@ -1,17 +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.adjust; - -/** - * Because of how our build module dependencies are structured, we aren't able to use - * the {@link com.adjust.sdk.OnAttributionChangedListener} directly outside of {@link AdjustHelper}. - * If the Adjust SDK is enabled, this listener should be notified when {@link com.adjust.sdk.OnAttributionChangedListener} - * is fired (i.e. this listener would be daisy-chained to the Adjust one). The listener also - * inherits thread-safety from GeckoSharedPrefs which is used to store the campaign ID. - */ -public interface AttributionHelperListener { - void onCampaignIdChanged(String campaignId); -} diff --git a/mobile/android/base/java/org/mozilla/gecko/adjust/StubAdjustHelper.java b/mobile/android/base/java/org/mozilla/gecko/adjust/StubAdjustHelper.java deleted file mode 100644 index ddfed84bd7..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/adjust/StubAdjustHelper.java +++ /dev/null @@ -1,31 +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.adjust; - -import android.content.Context; -import android.content.Intent; - -public class StubAdjustHelper implements AdjustHelperInterface { - public void onCreate(final Context context, final String appToken, final AttributionHelperListener listener) { - // Do nothing. - } - - public void onPause() { - // Do nothing. - } - - public void onResume() { - // Do nothing. - } - - public void setEnabled(final boolean isEnabled) { - // Do nothing. - } - - public void onReceive(final Context context, final Intent intent) { - // Do nothing. - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/animation/AnimationUtils.java b/mobile/android/base/java/org/mozilla/gecko/animation/AnimationUtils.java deleted file mode 100644 index 63e8e168ec..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/animation/AnimationUtils.java +++ /dev/null @@ -1,21 +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.animation; - -import android.content.Context; - -public class AnimationUtils { - private static long mShortDuration = -1; - - public static long getShortDuration(Context context) { - if (mShortDuration < 0) { - mShortDuration = context.getResources().getInteger(android.R.integer.config_shortAnimTime); - } - return mShortDuration; - } -} - diff --git a/mobile/android/base/java/org/mozilla/gecko/animation/HeightChangeAnimation.java b/mobile/android/base/java/org/mozilla/gecko/animation/HeightChangeAnimation.java deleted file mode 100644 index bf8007bbfa..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/animation/HeightChangeAnimation.java +++ /dev/null @@ -1,27 +0,0 @@ -/* 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.animation; - -import android.view.View; -import android.view.animation.Animation; -import android.view.animation.Transformation; - -public class HeightChangeAnimation extends Animation { - int mFromHeight; - int mToHeight; - View mView; - - public HeightChangeAnimation(View view, int fromHeight, int toHeight) { - mView = view; - mFromHeight = fromHeight; - mToHeight = toHeight; - } - - @Override - protected void applyTransformation(float interpolatedTime, Transformation t) { - mView.getLayoutParams().height = Math.round((mFromHeight * (1 - interpolatedTime)) + (mToHeight * interpolatedTime)); - mView.requestLayout(); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/animation/PropertyAnimator.java b/mobile/android/base/java/org/mozilla/gecko/animation/PropertyAnimator.java deleted file mode 100644 index dc2403bbd1..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/animation/PropertyAnimator.java +++ /dev/null @@ -1,342 +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.animation; - -import java.util.ArrayList; -import java.util.List; - -import org.mozilla.gecko.AppConstants.Versions; - -import android.os.Handler; -import android.support.v4.view.ViewCompat; -import android.view.Choreographer; -import android.view.View; -import android.view.ViewGroup; -import android.view.ViewTreeObserver; -import android.view.animation.AnimationUtils; -import android.view.animation.DecelerateInterpolator; -import android.view.animation.Interpolator; - -public class PropertyAnimator implements Runnable { - private static final String LOGTAG = "GeckoPropertyAnimator"; - - public static enum Property { - ALPHA, - TRANSLATION_X, - TRANSLATION_Y, - SCROLL_X, - SCROLL_Y, - WIDTH, - HEIGHT - } - - private class ElementHolder { - View view; - Property property; - float from; - float to; - } - - public static interface PropertyAnimationListener { - public void onPropertyAnimationStart(); - public void onPropertyAnimationEnd(); - } - - private final Interpolator mInterpolator; - private long mStartTime; - private final long mDuration; - private final float mDurationReciprocal; - private final List mElementsList; - private List mListeners; - FramePoster mFramePoster; - private boolean mUseHardwareLayer; - - public PropertyAnimator(long duration) { - this(duration, new DecelerateInterpolator()); - } - - public PropertyAnimator(long duration, Interpolator interpolator) { - mDuration = duration; - mDurationReciprocal = 1.0f / mDuration; - mInterpolator = interpolator; - mElementsList = new ArrayList(); - mFramePoster = FramePoster.create(this); - mUseHardwareLayer = true; - } - - public void setUseHardwareLayer(boolean useHardwareLayer) { - mUseHardwareLayer = useHardwareLayer; - } - - public void attach(View view, Property property, float to) { - ElementHolder element = new ElementHolder(); - - element.view = view; - element.property = property; - element.to = to; - - mElementsList.add(element); - } - - public void addPropertyAnimationListener(PropertyAnimationListener listener) { - if (mListeners == null) { - mListeners = new ArrayList(); - } - - mListeners.add(listener); - } - - public long getDuration() { - return mDuration; - } - - public long getRemainingTime() { - int timePassed = (int) (AnimationUtils.currentAnimationTimeMillis() - mStartTime); - return mDuration - timePassed; - } - - @Override - public void run() { - int timePassed = (int) (AnimationUtils.currentAnimationTimeMillis() - mStartTime); - if (timePassed >= mDuration) { - stop(); - return; - } - - float interpolation = mInterpolator.getInterpolation(timePassed * mDurationReciprocal); - - for (ElementHolder element : mElementsList) { - float delta = element.from + ((element.to - element.from) * interpolation); - invalidate(element, delta); - } - - mFramePoster.postNextAnimationFrame(); - } - - public void start() { - if (mDuration == 0) { - return; - } - - mStartTime = AnimationUtils.currentAnimationTimeMillis(); - - // Fix the from value based on current position and property - for (ElementHolder element : mElementsList) { - if (element.property == Property.ALPHA) - element.from = ViewHelper.getAlpha(element.view); - else if (element.property == Property.TRANSLATION_Y) - element.from = ViewHelper.getTranslationY(element.view); - else if (element.property == Property.TRANSLATION_X) - element.from = ViewHelper.getTranslationX(element.view); - else if (element.property == Property.SCROLL_Y) - element.from = ViewHelper.getScrollY(element.view); - else if (element.property == Property.SCROLL_X) - element.from = ViewHelper.getScrollX(element.view); - else if (element.property == Property.WIDTH) - element.from = ViewHelper.getWidth(element.view); - else if (element.property == Property.HEIGHT) - element.from = ViewHelper.getHeight(element.view); - - ViewCompat.setHasTransientState(element.view, true); - - if (shouldEnableHardwareLayer(element)) - element.view.setLayerType(View.LAYER_TYPE_HARDWARE, null); - else - element.view.setDrawingCacheEnabled(true); - } - - // Get ViewTreeObserver from any of the participant views - // in the animation. - final ViewTreeObserver treeObserver; - if (mElementsList.size() > 0) { - treeObserver = mElementsList.get(0).view.getViewTreeObserver(); - } else { - treeObserver = null; - } - - final ViewTreeObserver.OnPreDrawListener preDrawListener = new ViewTreeObserver.OnPreDrawListener() { - @Override - public boolean onPreDraw() { - if (treeObserver.isAlive()) { - treeObserver.removeOnPreDrawListener(this); - } - - mFramePoster.postFirstAnimationFrame(); - return true; - } - }; - - // Try to start animation after any on-going layout round - // in the current view tree. OnPreDrawListener seems broken - // on pre-Honeycomb devices, start animation immediatelly - // in this case. - if (treeObserver != null && treeObserver.isAlive()) { - treeObserver.addOnPreDrawListener(preDrawListener); - } else { - mFramePoster.postFirstAnimationFrame(); - } - - if (mListeners != null) { - for (PropertyAnimationListener listener : mListeners) { - listener.onPropertyAnimationStart(); - } - } - } - - /** - * Stop the animation, optionally snapping to the end position. - * onPropertyAnimationEnd is only called when snapping to the end position. - */ - public void stop(boolean snapToEndPosition) { - mFramePoster.cancelAnimationFrame(); - - // Make sure to snap to the end position. - for (ElementHolder element : mElementsList) { - if (snapToEndPosition) - invalidate(element, element.to); - - ViewCompat.setHasTransientState(element.view, false); - - if (shouldEnableHardwareLayer(element)) { - element.view.setLayerType(View.LAYER_TYPE_NONE, null); - } else { - element.view.setDrawingCacheEnabled(false); - } - } - - mElementsList.clear(); - - if (mListeners != null) { - if (snapToEndPosition) { - for (PropertyAnimationListener listener : mListeners) { - listener.onPropertyAnimationEnd(); - } - } - - mListeners.clear(); - mListeners = null; - } - } - - public void stop() { - stop(true); - } - - private boolean shouldEnableHardwareLayer(ElementHolder element) { - if (!mUseHardwareLayer) { - return false; - } - - if (!(element.view instanceof ViewGroup)) { - return false; - } - - if (element.property == Property.ALPHA || - element.property == Property.TRANSLATION_Y || - element.property == Property.TRANSLATION_X) { - return true; - } - - return false; - } - - private void invalidate(final ElementHolder element, final float delta) { - final View view = element.view; - - // check to see if the view was detached between the check above and this code - // getting run on the UI thread. - if (view.getHandler() == null) - return; - - if (element.property == Property.ALPHA) - ViewHelper.setAlpha(element.view, delta); - else if (element.property == Property.TRANSLATION_Y) - ViewHelper.setTranslationY(element.view, delta); - else if (element.property == Property.TRANSLATION_X) - ViewHelper.setTranslationX(element.view, delta); - else if (element.property == Property.SCROLL_Y) - ViewHelper.scrollTo(element.view, ViewHelper.getScrollX(element.view), (int) delta); - else if (element.property == Property.SCROLL_X) - ViewHelper.scrollTo(element.view, (int) delta, ViewHelper.getScrollY(element.view)); - else if (element.property == Property.WIDTH) - ViewHelper.setWidth(element.view, (int) delta); - else if (element.property == Property.HEIGHT) - ViewHelper.setHeight(element.view, (int) delta); - } - - private static abstract class FramePoster { - public static FramePoster create(Runnable r) { - if (Versions.feature16Plus) { - return new FramePosterPostJB(r); - } - - return new FramePosterPreJB(r); - } - - public abstract void postFirstAnimationFrame(); - public abstract void postNextAnimationFrame(); - public abstract void cancelAnimationFrame(); - } - - private static class FramePosterPreJB extends FramePoster { - // Default refresh rate in ms. - private static final int INTERVAL = 10; - - private final Handler mHandler; - private final Runnable mRunnable; - - public FramePosterPreJB(Runnable r) { - mHandler = new Handler(); - mRunnable = r; - } - - @Override - public void postFirstAnimationFrame() { - mHandler.post(mRunnable); - } - - @Override - public void postNextAnimationFrame() { - mHandler.postDelayed(mRunnable, INTERVAL); - } - - @Override - public void cancelAnimationFrame() { - mHandler.removeCallbacks(mRunnable); - } - } - - private static class FramePosterPostJB extends FramePoster { - private final Choreographer mChoreographer; - private final Choreographer.FrameCallback mCallback; - - public FramePosterPostJB(final Runnable r) { - mChoreographer = Choreographer.getInstance(); - - mCallback = new Choreographer.FrameCallback() { - @Override - public void doFrame(long frameTimeNanos) { - r.run(); - } - }; - } - - @Override - public void postFirstAnimationFrame() { - postNextAnimationFrame(); - } - - @Override - public void postNextAnimationFrame() { - mChoreographer.postFrameCallback(mCallback); - } - - @Override - public void cancelAnimationFrame() { - mChoreographer.removeFrameCallback(mCallback); - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/animation/Rotate3DAnimation.java b/mobile/android/base/java/org/mozilla/gecko/animation/Rotate3DAnimation.java deleted file mode 100644 index 7e8377f55f..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/animation/Rotate3DAnimation.java +++ /dev/null @@ -1,97 +0,0 @@ -/* - * Copyright (C) 2007 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.mozilla.gecko.animation; - -import android.view.animation.Animation; -import android.view.animation.Transformation; - -import android.graphics.Camera; -import android.graphics.Matrix; - -/** - * An animation that rotates the view on the Y axis between two specified angles. - * This animation also adds a translation on the Z axis (depth) to improve the effect. - */ -public class Rotate3DAnimation extends Animation { - private final float mFromDegrees; - private final float mToDegrees; - - private final float mCenterX; - private final float mCenterY; - - private final float mDepthZ; - private final boolean mReverse; - private Camera mCamera; - - private int mWidth = 1; - private int mHeight = 1; - - /** - * Creates a new 3D rotation on the Y axis. The rotation is defined by its - * start angle and its end angle. Both angles are in degrees. The rotation - * is performed around a center point on the 2D space, defined by a pair - * of X and Y coordinates, called centerX and centerY. When the animation - * starts, a translation on the Z axis (depth) is performed. The length - * of the translation can be specified, as well as whether the translation - * should be reversed in time. - * - * @param fromDegrees the start angle of the 3D rotation - * @param toDegrees the end angle of the 3D rotation - * @param centerX the X center of the 3D rotation - * @param centerY the Y center of the 3D rotation - * @param reverse true if the translation should be reversed, false otherwise - */ - public Rotate3DAnimation(float fromDegrees, float toDegrees, - float centerX, float centerY, float depthZ, boolean reverse) { - mFromDegrees = fromDegrees; - mToDegrees = toDegrees; - mCenterX = centerX; - mCenterY = centerY; - mDepthZ = depthZ; - mReverse = reverse; - } - - @Override - public void initialize(int width, int height, int parentWidth, int parentHeight) { - super.initialize(width, height, parentWidth, parentHeight); - mCamera = new Camera(); - mWidth = width; - mHeight = height; - } - - @Override - protected void applyTransformation(float interpolatedTime, Transformation t) { - final float fromDegrees = mFromDegrees; - float degrees = fromDegrees + ((mToDegrees - fromDegrees) * interpolatedTime); - - final Camera camera = mCamera; - final Matrix matrix = t.getMatrix(); - - camera.save(); - if (mReverse) { - camera.translate(0.0f, 0.0f, mDepthZ * interpolatedTime); - } else { - camera.translate(0.0f, 0.0f, mDepthZ * (1.0f - interpolatedTime)); - } - camera.rotateX(degrees); - camera.getMatrix(matrix); - camera.restore(); - - matrix.preTranslate(-mCenterX * mWidth, -mCenterY * mHeight); - matrix.postTranslate(mCenterX * mWidth, mCenterY * mHeight); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/animation/ViewHelper.java b/mobile/android/base/java/org/mozilla/gecko/animation/ViewHelper.java deleted file mode 100644 index 3ea2e84373..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/animation/ViewHelper.java +++ /dev/null @@ -1,109 +0,0 @@ -/* 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.animation; - -import android.view.View; -import android.view.ViewGroup; - -public final class ViewHelper { - private ViewHelper() { - } - - public static float getTranslationX(View view) { - if (view != null) { - return view.getTranslationX(); - } - - return 0; - } - - public static void setTranslationX(View view, float translationX) { - if (view != null) { - view.setTranslationX(translationX); - } - } - - public static float getTranslationY(View view) { - if (view != null) { - return view.getTranslationY(); - } - - return 0; - } - - public static void setTranslationY(View view, float translationY) { - if (view != null) { - view.setTranslationY(translationY); - } - } - - public static float getAlpha(View view) { - if (view != null) { - return view.getAlpha(); - } - - return 1; - } - - public static void setAlpha(View view, float alpha) { - if (view != null) { - view.setAlpha(alpha); - } - } - - public static int getWidth(View view) { - if (view != null) { - return view.getWidth(); - } - - return 0; - } - - public static void setWidth(View view, int width) { - if (view != null) { - ViewGroup.LayoutParams lp = view.getLayoutParams(); - lp.width = width; - view.setLayoutParams(lp); - } - } - - public static int getHeight(View view) { - if (view != null) { - return view.getHeight(); - } - - return 0; - } - - public static void setHeight(View view, int height) { - if (view != null) { - ViewGroup.LayoutParams lp = view.getLayoutParams(); - lp.height = height; - view.setLayoutParams(lp); - } - } - - public static int getScrollX(View view) { - if (view != null) { - return view.getScrollX(); - } - - return 0; - } - - public static int getScrollY(View view) { - if (view != null) { - return view.getScrollY(); - } - - return 0; - } - - public static void scrollTo(View view, int scrollX, int scrollY) { - if (view != null) { - view.scrollTo(scrollX, scrollY); - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/cleanup/FileCleanupController.java b/mobile/android/base/java/org/mozilla/gecko/cleanup/FileCleanupController.java deleted file mode 100644 index 447b837e86..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/cleanup/FileCleanupController.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * 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.cleanup; - -import android.content.Context; -import android.content.Intent; -import android.content.SharedPreferences; -import android.support.annotation.VisibleForTesting; - -import java.io.File; -import java.util.ArrayList; -import java.util.concurrent.TimeUnit; - -/** - * Encapsulates the code to run the {@link FileCleanupService}. Call - * {@link #startIfReady(Context, SharedPreferences, String)} to start the clean-up. - * - * Note: for simplicity, the current implementation does not cache which - * files have been cleaned up and will attempt to delete the same files - * each time it is run. If the file deletion list grows large, consider - * keeping a cache. - */ -public class FileCleanupController { - - private static final long MILLIS_BETWEEN_CLEANUPS = TimeUnit.DAYS.toMillis(7); - @VisibleForTesting static final String PREF_LAST_CLEANUP_MILLIS = "cleanup.lastFileCleanupMillis"; - - // These will be prepended with the path of the profile we're cleaning up. - private static final String[] PROFILE_FILES_TO_CLEANUP = new String[] { - "health.db", - "health.db-journal", - "health.db-shm", - "health.db-wal", - }; - - /** - * Starts the clean-up if it's time to clean-up, otherwise returns. For simplicity, - * it does not schedule the cleanup for some point in the future - this method will - * have to be called again (i.e. polled) in order to run the clean-up service. - * - * @param context Context of the calling {@link android.app.Activity} - * @param sharedPrefs The {@link SharedPreferences} instance to store the controller state to - * @param profilePath The path to the profile the service should clean-up files from - */ - public static void startIfReady(final Context context, final SharedPreferences sharedPrefs, final String profilePath) { - if (!isCleanupReady(sharedPrefs)) { - return; - } - - recordCleanupScheduled(sharedPrefs); - - final Intent fileCleanupIntent = new Intent(context, FileCleanupService.class); - fileCleanupIntent.setAction(FileCleanupService.ACTION_DELETE_FILES); - fileCleanupIntent.putExtra(FileCleanupService.EXTRA_FILE_PATHS_TO_DELETE, getFilesToCleanup(profilePath + "/")); - context.startService(fileCleanupIntent); - } - - private static boolean isCleanupReady(final SharedPreferences sharedPrefs) { - final long lastCleanupMillis = sharedPrefs.getLong(PREF_LAST_CLEANUP_MILLIS, -1); - return lastCleanupMillis + MILLIS_BETWEEN_CLEANUPS < System.currentTimeMillis(); - } - - private static void recordCleanupScheduled(final SharedPreferences sharedPrefs) { - final SharedPreferences.Editor editor = sharedPrefs.edit(); - editor.putLong(PREF_LAST_CLEANUP_MILLIS, System.currentTimeMillis()).apply(); - } - - @VisibleForTesting - static ArrayList getFilesToCleanup(final String profilePath) { - final ArrayList out = new ArrayList<>(PROFILE_FILES_TO_CLEANUP.length); - for (final String path : PROFILE_FILES_TO_CLEANUP) { - // Append a file separator, just in-case the caller didn't include one. - out.add(profilePath + File.separator + path); - } - return out; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/cleanup/FileCleanupService.java b/mobile/android/base/java/org/mozilla/gecko/cleanup/FileCleanupService.java deleted file mode 100644 index 76aff733a6..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/cleanup/FileCleanupService.java +++ /dev/null @@ -1,80 +0,0 @@ -/* - * 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.cleanup; - -import android.app.IntentService; -import android.content.Intent; -import android.util.Log; - -import java.io.File; -import java.util.ArrayList; - -/** - * An IntentService to delete files. - * - * It takes an {@link ArrayList} of String file paths to delete via the extra - * {@link #EXTRA_FILE_PATHS_TO_DELETE}. If these file paths are directories, they will - * not be traversed recursively and will only be deleted if empty. This is to avoid accidentally - * trashing a users' profile if a folder is accidentally listed. - * - * An IntentService was chosen because: - * * It generally won't be killed when the Activity is - * * (unlike HandlerThread) The system handles scheduling, prioritizing, - * and shutting down the underlying background thread - * * (unlike an existing background thread) We don't block our background operations - * for this, which doesn't directly affect the user. - * - * The major trade-off is that this Service is very dangerous if it's exported... so don't do that! - */ -public class FileCleanupService extends IntentService { - private static final String LOGTAG = "Gecko" + FileCleanupService.class.getSimpleName(); - private static final String WORKER_THREAD_NAME = LOGTAG + "Worker"; - - public static final String ACTION_DELETE_FILES = "org.mozilla.gecko.intent.action.DELETE_FILES"; - public static final String EXTRA_FILE_PATHS_TO_DELETE = "org.mozilla.gecko.file_paths_to_delete"; - - public FileCleanupService() { - super(WORKER_THREAD_NAME); - - // We're likely to get scheduled again - let's wait until then in order to avoid: - // * The coding complexity of re-running this - // * Consuming system resources: we were probably killed for resource conservation purposes - setIntentRedelivery(false); - } - - @Override - protected void onHandleIntent(final Intent intent) { - if (!isIntentValid(intent)) { - return; - } - - final ArrayList filesToDelete = intent.getStringArrayListExtra(EXTRA_FILE_PATHS_TO_DELETE); - for (final String path : filesToDelete) { - final File file = new File(path); - file.delete(); - } - } - - private static boolean isIntentValid(final Intent intent) { - if (intent == null) { - Log.w(LOGTAG, "Received null intent"); - return false; - } - - if (!intent.getAction().equals(ACTION_DELETE_FILES)) { - Log.w(LOGTAG, "Received unknown intent action: " + intent.getAction()); - return false; - } - - if (!intent.hasExtra(EXTRA_FILE_PATHS_TO_DELETE)) { - Log.w(LOGTAG, "Received intent with no files extra"); - return false; - } - - return true; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/customtabs/CustomTabsActivity.java b/mobile/android/base/java/org/mozilla/gecko/customtabs/CustomTabsActivity.java deleted file mode 100644 index b1bf567b0c..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/customtabs/CustomTabsActivity.java +++ /dev/null @@ -1,177 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.customtabs; - -import android.net.Uri; -import android.os.Build; -import android.os.Bundle; -import android.support.v7.app.ActionBar; -import android.support.v7.widget.Toolbar; -import android.text.TextUtils; -import android.util.Log; -import android.view.MenuItem; -import android.view.View; -import android.view.Window; -import android.view.WindowManager; -import android.widget.TextView; - -import org.mozilla.gecko.AppConstants; -import org.mozilla.gecko.GeckoApp; -import org.mozilla.gecko.GeckoAppShell; -import org.mozilla.gecko.R; -import org.mozilla.gecko.Tab; -import org.mozilla.gecko.Tabs; -import org.mozilla.gecko.util.ColorUtil; -import org.mozilla.gecko.util.GeckoRequest; -import org.mozilla.gecko.util.NativeJSObject; -import org.mozilla.gecko.util.ThreadUtils; - -import java.lang.reflect.Field; - -import static android.support.customtabs.CustomTabsIntent.EXTRA_TOOLBAR_COLOR; - -public class CustomTabsActivity extends GeckoApp implements Tabs.OnTabsChangedListener { - private static final String LOGTAG = "CustomTabsActivity"; - private static final String SAVED_TOOLBAR_COLOR = "SavedToolbarColor"; - private static final String SAVED_TOOLBAR_TITLE = "SavedToolbarTitle"; - private static final int NO_COLOR = -1; - private Toolbar toolbar; - - private ActionBar actionBar; - private int tabId = -1; - private boolean useDomainTitle = true; - - private int toolbarColor; - private String toolbarTitle; - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - if (savedInstanceState != null) { - toolbarColor = savedInstanceState.getInt(SAVED_TOOLBAR_COLOR, NO_COLOR); - toolbarTitle = savedInstanceState.getString(SAVED_TOOLBAR_TITLE, AppConstants.MOZ_APP_BASENAME); - } else { - toolbarColor = NO_COLOR; - toolbarTitle = AppConstants.MOZ_APP_BASENAME; - } - - Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); - updateActionBarWithToolbar(toolbar); - try { - // Since we don't create the Toolbar's TextView ourselves, this seems - // to be the only way of changing the ellipsize setting. - Field f = toolbar.getClass().getDeclaredField("mTitleTextView"); - f.setAccessible(true); - TextView textView = (TextView) f.get(toolbar); - textView.setEllipsize(TextUtils.TruncateAt.START); - } catch (Exception e) { - // If we can't ellipsize at the start of the title, we shouldn't display the host - // so as to avoid displaying a misleadingly truncated host. - Log.w(LOGTAG, "Failed to get Toolbar TextView, using default title."); - useDomainTitle = false; - } - actionBar = getSupportActionBar(); - actionBar.setTitle(toolbarTitle); - updateToolbarColor(toolbar); - - toolbar.setNavigationOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - onBackPressed(); - } - }); - - Tabs.registerOnTabsChangedListener(this); - } - - @Override - public void onDestroy() { - super.onDestroy(); - Tabs.unregisterOnTabsChangedListener(this); - } - - @Override - public int getLayout() { - return R.layout.customtabs_activity; - } - - @Override - protected void onDone() { - finish(); - } - - @Override - public void onTabChanged(Tab tab, Tabs.TabEvents msg, String data) { - if (tab == null) { - return; - } - - if (tabId >= 0 && tab.getId() != tabId) { - return; - } - - if (msg == Tabs.TabEvents.LOCATION_CHANGE) { - tabId = tab.getId(); - final Uri uri = Uri.parse(tab.getURL()); - String title = null; - if (uri != null) { - title = uri.getHost(); - } - if (!useDomainTitle || title == null || title.isEmpty()) { - toolbarTitle = AppConstants.MOZ_APP_BASENAME; - } else { - toolbarTitle = title; - } - actionBar.setTitle(toolbarTitle); - } - } - - @Override - protected void onSaveInstanceState(Bundle outState) { - super.onSaveInstanceState(outState); - - outState.putInt(SAVED_TOOLBAR_COLOR, toolbarColor); - outState.putString(SAVED_TOOLBAR_TITLE, toolbarTitle); - } - - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case android.R.id.home: - finish(); - return true; - } - return super.onOptionsItemSelected(item); - } - - private void updateActionBarWithToolbar(final Toolbar toolbar) { - setSupportActionBar(toolbar); - final ActionBar ab = getSupportActionBar(); - if (ab != null) { - ab.setDisplayHomeAsUpEnabled(true); - } - } - - private void updateToolbarColor(final Toolbar toolbar) { - if (toolbarColor == NO_COLOR) { - final int color = getIntent().getIntExtra(EXTRA_TOOLBAR_COLOR, NO_COLOR); - if (color == NO_COLOR) { - return; - } - toolbarColor = color; - } - - final int titleTextColor = ColorUtil.getReadableTextColor(toolbarColor); - - toolbar.setBackgroundColor(toolbarColor); - toolbar.setTitleTextColor(titleTextColor); - final Window window = getWindow(); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS); - window.setStatusBarColor(ColorUtil.darken(toolbarColor, 0.25)); - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/customtabs/GeckoCustomTabsService.java b/mobile/android/base/java/org/mozilla/gecko/customtabs/GeckoCustomTabsService.java deleted file mode 100644 index 7960f78324..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/customtabs/GeckoCustomTabsService.java +++ /dev/null @@ -1,65 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.customtabs; - -import android.net.Uri; -import android.os.Bundle; -import android.support.customtabs.CustomTabsService; -import android.support.customtabs.CustomTabsSessionToken; -import android.util.Log; - -import org.mozilla.gecko.GeckoProfile; -import org.mozilla.gecko.GeckoService; - -import java.util.List; - -/** - * Custom tabs service external, third-party apps connect to. - */ -public class GeckoCustomTabsService extends CustomTabsService { - private static final String LOGTAG = "GeckoCustomTabsService"; - private static final boolean DEBUG = false; - - @Override - protected boolean updateVisuals(CustomTabsSessionToken sessionToken, Bundle bundle) { - Log.v(LOGTAG, "updateVisuals()"); - - return false; - } - - @Override - protected boolean warmup(long flags) { - if (DEBUG) { - Log.v(LOGTAG, "warming up..."); - } - - GeckoService.startGecko(GeckoProfile.initFromArgs(this, null), null, getApplicationContext()); - - return true; - } - - @Override - protected boolean newSession(CustomTabsSessionToken sessionToken) { - Log.v(LOGTAG, "newSession()"); - - // Pretend session has been started - return true; - } - - @Override - protected boolean mayLaunchUrl(CustomTabsSessionToken sessionToken, Uri uri, Bundle bundle, List list) { - Log.v(LOGTAG, "mayLaunchUrl()"); - - return false; - } - - @Override - protected Bundle extraCommand(String commandName, Bundle bundle) { - Log.v(LOGTAG, "extraCommand()"); - - return null; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/db/AbstractPerProfileDatabaseProvider.java b/mobile/android/base/java/org/mozilla/gecko/db/AbstractPerProfileDatabaseProvider.java deleted file mode 100644 index 2e056cc1ea..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/db/AbstractPerProfileDatabaseProvider.java +++ /dev/null @@ -1,79 +0,0 @@ -/* 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.db; - -import org.mozilla.gecko.annotation.RobocopTarget; - -import android.database.sqlite.SQLiteDatabase; -import android.database.sqlite.SQLiteOpenHelper; -import android.net.Uri; - -/** - * The base class for ContentProviders that wish to use a different DB - * for each profile. - * - * This class has logic shared between ordinary per-profile CPs and - * those that wish to share DB connections between CPs. - */ -public abstract class AbstractPerProfileDatabaseProvider extends AbstractTransactionalProvider { - - /** - * Extend this to provide access to your own map of shared databases. This - * is a method so that your subclass doesn't collide with others! - */ - protected abstract PerProfileDatabases getDatabases(); - - /* - * Fetches a readable database based on the profile indicated in the - * passed URI. If the URI does not contain a profile param, the default profile - * is used. - * - * @param uri content URI optionally indicating the profile of the user - * @return instance of a readable SQLiteDatabase - */ - @Override - protected SQLiteDatabase getReadableDatabase(Uri uri) { - String profile = null; - if (uri != null) { - profile = uri.getQueryParameter(BrowserContract.PARAM_PROFILE); - } - - return getDatabases().getDatabaseHelperForProfile(profile, isTest(uri)).getReadableDatabase(); - } - - /* - * Fetches a writable database based on the profile indicated in the - * passed URI. If the URI does not contain a profile param, the default profile - * is used - * - * @param uri content URI optionally indicating the profile of the user - * @return instance of a writable SQLiteDatabase - */ - @Override - protected SQLiteDatabase getWritableDatabase(Uri uri) { - String profile = null; - if (uri != null) { - profile = uri.getQueryParameter(BrowserContract.PARAM_PROFILE); - } - - return getDatabases().getDatabaseHelperForProfile(profile, isTest(uri)).getWritableDatabase(); - } - - protected SQLiteDatabase getWritableDatabaseForProfile(String profile, boolean isTest) { - return getDatabases().getDatabaseHelperForProfile(profile, isTest).getWritableDatabase(); - } - - /** - * This method should ONLY be used for testing purposes. - * - * @param uri content URI optionally indicating the profile of the user - * @return instance of a writable SQLiteDatabase - */ - @Override - @RobocopTarget - public SQLiteDatabase getWritableDatabaseForTesting(Uri uri) { - return getWritableDatabase(uri); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/db/AbstractTransactionalProvider.java b/mobile/android/base/java/org/mozilla/gecko/db/AbstractTransactionalProvider.java deleted file mode 100644 index 7e289b76fd..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/db/AbstractTransactionalProvider.java +++ /dev/null @@ -1,328 +0,0 @@ -/* 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.db; - -import org.mozilla.gecko.AppConstants.Versions; - -import android.content.ContentProvider; -import android.content.ContentValues; -import android.database.SQLException; -import android.database.sqlite.SQLiteDatabase; -import android.net.Uri; -import android.text.TextUtils; -import android.util.Log; - -/** - * This abstract class exists to capture some of the transaction-handling - * commonalities in Fennec's DB layer. - * - * In particular, this abstracts DB access, batching, and a particular - * transaction approach. - * - * That approach is: subclasses implement the abstract methods - * {@link #insertInTransaction(android.net.Uri, android.content.ContentValues)}, - * {@link #deleteInTransaction(android.net.Uri, String, String[])}, and - * {@link #updateInTransaction(android.net.Uri, android.content.ContentValues, String, String[])}. - * - * These are all called expecting a transaction to be established, so failed - * modifications can be rolled-back, and work batched. - * - * If no transaction is established, that's not a problem. Transaction nesting - * can be avoided by using {@link #beginWrite(SQLiteDatabase)}. - * - * The decision of when to begin a transaction is left to the subclasses, - * primarily to avoid the pattern of a transaction being begun, a read occurring, - * and then a write being necessary. This lock upgrade can result in SQLITE_BUSY, - * which we don't handle well. Better to avoid starting a transaction too soon! - * - * You are probably interested in some subclasses: - * - * * {@link AbstractPerProfileDatabaseProvider} provides a simple abstraction for - * querying databases that are stored in the user's profile directory. - * * {@link PerProfileDatabaseProvider} is a simple version that only allows a - * single ContentProvider to access each per-profile database. - * * {@link SharedBrowserDatabaseProvider} is an example of a per-profile provider - * that allows for multiple providers to safely work with the same databases. - */ -@SuppressWarnings("javadoc") -public abstract class AbstractTransactionalProvider extends ContentProvider { - private static final String LOGTAG = "GeckoTransProvider"; - - private static final boolean logDebug = Log.isLoggable(LOGTAG, Log.DEBUG); - private static final boolean logVerbose = Log.isLoggable(LOGTAG, Log.VERBOSE); - - protected abstract SQLiteDatabase getReadableDatabase(Uri uri); - protected abstract SQLiteDatabase getWritableDatabase(Uri uri); - - public abstract SQLiteDatabase getWritableDatabaseForTesting(Uri uri); - - protected abstract Uri insertInTransaction(Uri uri, ContentValues values); - protected abstract int deleteInTransaction(Uri uri, String selection, String[] selectionArgs); - protected abstract int updateInTransaction(Uri uri, ContentValues values, String selection, String[] selectionArgs); - - /** - * Track whether we're in a batch operation. - * - * When we're in a batch operation, individual write steps won't even try - * to start a transaction... and neither will they attempt to finish one. - * - * Set this to Boolean.TRUE when you're entering a batch -- - * a section of code in which {@link ContentProvider} methods will be - * called, but nested transactions should not be started. Callers are - * responsible for beginning and ending the enclosing transaction, and - * for setting this to Boolean.FALSE when done. - * - * This is a ThreadLocal separate from `db.inTransaction` because batched - * operations start transactions independent of individual ContentProvider - * operations. This doesn't work well with the entire concept of this - * abstract class -- that is, automatically beginning and ending transactions - * for each insert/delete/update operation -- and doing so without - * causing arbitrary nesting requires external tracking. - * - * Note that beginWrite takes a DB argument, but we don't differentiate - * between databases in this tracking flag. If your ContentProvider manages - * multiple database transactions within the same thread, you'll need to - * amend this scheme -- but then, you're already doing some serious wizardry, - * so rock on. - */ - final ThreadLocal isInBatchOperation = new ThreadLocal(); - - private boolean isInBatch() { - final Boolean isInBatch = isInBatchOperation.get(); - if (isInBatch == null) { - return false; - } - - return isInBatch; - } - - /** - * If we're not currently in a transaction, and we should be, start one. - */ - protected void beginWrite(final SQLiteDatabase db) { - if (isInBatch()) { - trace("Not bothering with an intermediate write transaction: inside batch operation."); - return; - } - - if (!db.inTransaction()) { - trace("beginWrite: beginning transaction."); - db.beginTransaction(); - } - } - - /** - * If we're not in a batch, but we are in a write transaction, mark it as - * successful. - */ - protected void markWriteSuccessful(final SQLiteDatabase db) { - if (isInBatch()) { - trace("Not marking write successful: inside batch operation."); - return; - } - - if (db.inTransaction()) { - trace("Marking write transaction successful."); - db.setTransactionSuccessful(); - } - } - - /** - * If we're not in a batch, but we are in a write transaction, - * end it. - * - * @see PerProfileDatabaseProvider#markWriteSuccessful(SQLiteDatabase) - */ - protected void endWrite(final SQLiteDatabase db) { - if (isInBatch()) { - trace("Not ending write: inside batch operation."); - return; - } - - if (db.inTransaction()) { - trace("endWrite: ending transaction."); - db.endTransaction(); - } - } - - protected void beginBatch(final SQLiteDatabase db) { - trace("Beginning batch."); - isInBatchOperation.set(Boolean.TRUE); - db.beginTransaction(); - } - - protected void markBatchSuccessful(final SQLiteDatabase db) { - if (isInBatch()) { - trace("Marking batch successful."); - db.setTransactionSuccessful(); - return; - } - Log.w(LOGTAG, "Unexpectedly asked to mark batch successful, but not in batch!"); - throw new IllegalStateException("Not in batch."); - } - - protected void endBatch(final SQLiteDatabase db) { - trace("Ending batch."); - db.endTransaction(); - isInBatchOperation.set(Boolean.FALSE); - } - - @Override - public int delete(Uri uri, String selection, String[] selectionArgs) { - trace("Calling delete on URI: " + uri + ", " + selection + ", " + selectionArgs); - - final SQLiteDatabase db = getWritableDatabase(uri); - int deleted = 0; - - try { - deleted = deleteInTransaction(uri, selection, selectionArgs); - markWriteSuccessful(db); - } finally { - endWrite(db); - } - - if (deleted > 0) { - final boolean shouldSyncToNetwork = !isCallerSync(uri); - getContext().getContentResolver().notifyChange(uri, null, shouldSyncToNetwork); - } - - return deleted; - } - - @Override - public Uri insert(Uri uri, ContentValues values) { - trace("Calling insert on URI: " + uri); - - final SQLiteDatabase db = getWritableDatabase(uri); - Uri result = null; - try { - result = insertInTransaction(uri, values); - markWriteSuccessful(db); - } catch (SQLException sqle) { - Log.e(LOGTAG, "exception in DB operation", sqle); - } catch (UnsupportedOperationException uoe) { - Log.e(LOGTAG, "don't know how to perform that insert", uoe); - } finally { - endWrite(db); - } - - if (result != null) { - final boolean shouldSyncToNetwork = !isCallerSync(uri); - getContext().getContentResolver().notifyChange(uri, null, shouldSyncToNetwork); - } - - return result; - } - - @Override - public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { - trace("Calling update on URI: " + uri + ", " + selection + ", " + selectionArgs); - - final SQLiteDatabase db = getWritableDatabase(uri); - int updated = 0; - - try { - updated = updateInTransaction(uri, values, selection, - selectionArgs); - markWriteSuccessful(db); - } finally { - endWrite(db); - } - - if (updated > 0) { - final boolean shouldSyncToNetwork = !isCallerSync(uri); - getContext().getContentResolver().notifyChange(uri, null, shouldSyncToNetwork); - } - - return updated; - } - - @Override - public int bulkInsert(Uri uri, ContentValues[] values) { - if (values == null) { - return 0; - } - - int numValues = values.length; - int successes = 0; - - final SQLiteDatabase db = getWritableDatabase(uri); - - debug("bulkInsert: explicitly starting transaction."); - beginBatch(db); - - try { - for (int i = 0; i < numValues; i++) { - insertInTransaction(uri, values[i]); - successes++; - } - trace("Flushing DB bulkinsert..."); - markBatchSuccessful(db); - } finally { - debug("bulkInsert: explicitly ending transaction."); - endBatch(db); - } - - if (successes > 0) { - final boolean shouldSyncToNetwork = !isCallerSync(uri); - getContext().getContentResolver().notifyChange(uri, null, shouldSyncToNetwork); - } - - return successes; - } - - /** - * Indicates whether a query should include deleted fields - * based on the URI. - * @param uri query URI - */ - protected static boolean shouldShowDeleted(Uri uri) { - String showDeleted = uri.getQueryParameter(BrowserContract.PARAM_SHOW_DELETED); - return !TextUtils.isEmpty(showDeleted); - } - - /** - * Indicates whether an insertion should be made if a record doesn't - * exist, based on the URI. - * @param uri query URI - */ - protected static boolean shouldUpdateOrInsert(Uri uri) { - String insertIfNeeded = uri.getQueryParameter(BrowserContract.PARAM_INSERT_IF_NEEDED); - return Boolean.parseBoolean(insertIfNeeded); - } - - /** - * Indicates whether query is a test based on the URI. - * @param uri query URI - */ - protected static boolean isTest(Uri uri) { - if (uri == null) { - return false; - } - String isTest = uri.getQueryParameter(BrowserContract.PARAM_IS_TEST); - return !TextUtils.isEmpty(isTest); - } - - /** - * Return true of the query is from Firefox Sync. - * @param uri query URI - */ - protected static boolean isCallerSync(Uri uri) { - String isSync = uri.getQueryParameter(BrowserContract.PARAM_IS_SYNC); - return !TextUtils.isEmpty(isSync); - } - - protected static void trace(String message) { - if (logVerbose) { - Log.v(LOGTAG, message); - } - } - - protected static void debug(String message) { - if (logDebug) { - Log.d(LOGTAG, message); - } - } -} \ No newline at end of file diff --git a/mobile/android/base/java/org/mozilla/gecko/db/BaseTable.java b/mobile/android/base/java/org/mozilla/gecko/db/BaseTable.java deleted file mode 100644 index 418d547ed4..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/db/BaseTable.java +++ /dev/null @@ -1,64 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.db; - -import android.content.ContentValues; -import android.database.Cursor; -import android.database.sqlite.SQLiteDatabase; -import android.net.Uri; -import android.util.Log; - -// BaseTable provides a basic implementation of a Table for tables that don't require advanced operations during -// insert, delete, update, or query operations. Implementors must still provide onCreate and onUpgrade operations. -public abstract class BaseTable implements Table { - private static final String LOGTAG = "GeckoBaseTable"; - - private static final boolean DEBUG = false; - - protected static void log(String msg) { - if (DEBUG) { - Log.i(LOGTAG, msg); - } - } - - // Table implementation - @Override - public Table.ContentProviderInfo[] getContentProviderInfo() { - return new Table.ContentProviderInfo[0]; - } - - // Returns the name of the table to modify/query - protected abstract String getTable(); - - // Table implementation - @Override - public Cursor query(SQLiteDatabase db, Uri uri, int dbId, String[] columns, String selection, String[] selectionArgs, String sortOrder, String groupBy, String limit) { - Cursor c = db.query(getTable(), columns, selection, selectionArgs, groupBy, null, sortOrder, limit); - log("query " + columns + " in " + selection + " = " + c); - return c; - } - - @Override - public int update(SQLiteDatabase db, Uri uri, int dbId, ContentValues values, String selection, String[] selectionArgs) { - int updated = db.updateWithOnConflict(getTable(), values, selection, selectionArgs, SQLiteDatabase.CONFLICT_REPLACE); - log("update " + values + " in " + selection + " = " + updated); - return updated; - } - - @Override - public long insert(SQLiteDatabase db, Uri uri, int dbId, ContentValues values) { - long inserted = db.insertOrThrow(getTable(), null, values); - log("insert " + values + " = " + inserted); - return inserted; - } - - @Override - public int delete(SQLiteDatabase db, Uri uri, int dbId, String selection, String[] selectionArgs) { - int deleted = db.delete(getTable(), selection, selectionArgs); - log("delete " + selection + " = " + deleted); - return deleted; - } -}; diff --git a/mobile/android/base/java/org/mozilla/gecko/db/BrowserContract.java b/mobile/android/base/java/org/mozilla/gecko/db/BrowserContract.java deleted file mode 100644 index 51c8d964fb..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/db/BrowserContract.java +++ /dev/null @@ -1,785 +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.db; - -import org.mozilla.gecko.AppConstants; - -import android.net.Uri; -import android.support.annotation.NonNull; - -import org.mozilla.gecko.annotation.RobocopTarget; - -@RobocopTarget -public class BrowserContract { - public static final String AUTHORITY = AppConstants.ANDROID_PACKAGE_NAME + ".db.browser"; - public static final Uri AUTHORITY_URI = Uri.parse("content://" + AUTHORITY); - - public static final String PASSWORDS_AUTHORITY = AppConstants.ANDROID_PACKAGE_NAME + ".db.passwords"; - public static final Uri PASSWORDS_AUTHORITY_URI = Uri.parse("content://" + PASSWORDS_AUTHORITY); - - public static final String FORM_HISTORY_AUTHORITY = AppConstants.ANDROID_PACKAGE_NAME + ".db.formhistory"; - public static final Uri FORM_HISTORY_AUTHORITY_URI = Uri.parse("content://" + FORM_HISTORY_AUTHORITY); - - public static final String TABS_AUTHORITY = AppConstants.ANDROID_PACKAGE_NAME + ".db.tabs"; - public static final Uri TABS_AUTHORITY_URI = Uri.parse("content://" + TABS_AUTHORITY); - - public static final String HOME_AUTHORITY = AppConstants.ANDROID_PACKAGE_NAME + ".db.home"; - public static final Uri HOME_AUTHORITY_URI = Uri.parse("content://" + HOME_AUTHORITY); - - public static final String PROFILES_AUTHORITY = AppConstants.ANDROID_PACKAGE_NAME + ".profiles"; - public static final Uri PROFILES_AUTHORITY_URI = Uri.parse("content://" + PROFILES_AUTHORITY); - - public static final String READING_LIST_AUTHORITY = AppConstants.ANDROID_PACKAGE_NAME + ".db.readinglist"; - public static final Uri READING_LIST_AUTHORITY_URI = Uri.parse("content://" + READING_LIST_AUTHORITY); - - public static final String SEARCH_HISTORY_AUTHORITY = AppConstants.ANDROID_PACKAGE_NAME + ".db.searchhistory"; - public static final Uri SEARCH_HISTORY_AUTHORITY_URI = Uri.parse("content://" + SEARCH_HISTORY_AUTHORITY); - - public static final String LOGINS_AUTHORITY = AppConstants.ANDROID_PACKAGE_NAME + ".db.logins"; - public static final Uri LOGINS_AUTHORITY_URI = Uri.parse("content://" + LOGINS_AUTHORITY); - - public static final String PARAM_PROFILE = "profile"; - public static final String PARAM_PROFILE_PATH = "profilePath"; - public static final String PARAM_LIMIT = "limit"; - public static final String PARAM_SUGGESTEDSITES_LIMIT = "suggestedsites_limit"; - public static final String PARAM_TOPSITES_DISABLE_PINNED = "topsites_disable_pinned"; - public static final String PARAM_IS_SYNC = "sync"; - public static final String PARAM_SHOW_DELETED = "show_deleted"; - public static final String PARAM_IS_TEST = "test"; - public static final String PARAM_INSERT_IF_NEEDED = "insert_if_needed"; - public static final String PARAM_INCREMENT_VISITS = "increment_visits"; - public static final String PARAM_INCREMENT_REMOTE_AGGREGATES = "increment_remote_aggregates"; - public static final String PARAM_EXPIRE_PRIORITY = "priority"; - public static final String PARAM_DATASET_ID = "dataset_id"; - public static final String PARAM_GROUP_BY = "group_by"; - - static public enum ExpirePriority { - NORMAL, - AGGRESSIVE - } - - /** - * Produces a SQL expression used for sorting results of the "combined" view by frecency. - * Combines remote and local frecency calculations, weighting local visits much heavier. - * - * @param includesBookmarks When URL is bookmarked, should we give it bonus frecency points? - * @param ascending Indicates if sorting order ascending - * @return Combined frecency sorting expression - */ - static public String getCombinedFrecencySortOrder(boolean includesBookmarks, boolean ascending) { - final long now = System.currentTimeMillis(); - StringBuilder order = new StringBuilder(getRemoteFrecencySQL(now) + " + " + getLocalFrecencySQL(now)); - - if (includesBookmarks) { - order.insert(0, "(CASE WHEN " + Combined.BOOKMARK_ID + " > -1 THEN 100 ELSE 0 END) + "); - } - - order.append(ascending ? " ASC" : " DESC"); - return order.toString(); - } - - /** - * See Bug 1265525 for details (explanation + graphs) on how Remote frecency compares to Local frecency for different - * combinations of visits count and age. - * - * @param now Base time in milliseconds for age calculation - * @return remote frecency SQL calculation - */ - static public String getRemoteFrecencySQL(final long now) { - return getFrecencyCalculation(now, 1, 110, Combined.REMOTE_VISITS_COUNT, Combined.REMOTE_DATE_LAST_VISITED); - } - - /** - * Local frecency SQL calculation. Note higher scale factor and squared visit count which achieve - * visits generated locally being much preferred over remote visits. - * See Bug 1265525 for details (explanation + comparison graphs). - * - * @param now Base time in milliseconds for age calculation - * @return local frecency SQL calculation - */ - static public String getLocalFrecencySQL(final long now) { - String visitCountExpr = "(" + Combined.LOCAL_VISITS_COUNT + " + 2)"; - visitCountExpr = visitCountExpr + " * " + visitCountExpr; - - return getFrecencyCalculation(now, 2, 225, visitCountExpr, Combined.LOCAL_DATE_LAST_VISITED); - } - - /** - * Our version of frecency is computed by scaling the number of visits by a multiplier - * that approximates Gaussian decay, based on how long ago the entry was last visited. - * Since we're limited by the math we can do with sqlite, we're calculating this - * approximation using the Cauchy distribution: multiplier = scale_const / (age^2 + scale_const). - * For example, with 15 as our scale parameter, we get a scale constant 15^2 = 225. Then: - * frecencyScore = numVisits * max(1, 100 * 225 / (age*age + 225)). (See bug 704977) - * - * @param now Base time in milliseconds for age calculation - * @param minFrecency Minimum allowed frecency value - * @param multiplier Scale constant - * @param visitCountExpr Expression which will produce a visit count - * @param lastVisitExpr Expression which will produce "last-visited" timestamp - * @return Frecency SQL calculation - */ - static public String getFrecencyCalculation(final long now, final int minFrecency, final int multiplier, @NonNull final String visitCountExpr, @NonNull final String lastVisitExpr) { - final long nowInMicroseconds = now * 1000; - final long microsecondsPerDay = 86400000000L; - final String ageExpr = "(" + nowInMicroseconds + " - " + lastVisitExpr + ") / " + microsecondsPerDay; - - return visitCountExpr + " * MAX(" + minFrecency + ", 100 * " + multiplier + " / (" + ageExpr + " * " + ageExpr + " + " + multiplier + "))"; - } - - @RobocopTarget - public interface CommonColumns { - public static final String _ID = "_id"; - } - - @RobocopTarget - public interface DateSyncColumns { - public static final String DATE_CREATED = "created"; - public static final String DATE_MODIFIED = "modified"; - } - - @RobocopTarget - public interface SyncColumns extends DateSyncColumns { - public static final String GUID = "guid"; - public static final String IS_DELETED = "deleted"; - } - - @RobocopTarget - public interface URLColumns { - public static final String URL = "url"; - public static final String TITLE = "title"; - } - - @RobocopTarget - public interface FaviconColumns { - public static final String FAVICON = "favicon"; - public static final String FAVICON_ID = "favicon_id"; - public static final String FAVICON_URL = "favicon_url"; - } - - @RobocopTarget - public interface HistoryColumns { - public static final String DATE_LAST_VISITED = "date"; - public static final String VISITS = "visits"; - // Aggregates used to speed up top sites and search frecency-powered queries - public static final String LOCAL_VISITS = "visits_local"; - public static final String REMOTE_VISITS = "visits_remote"; - public static final String LOCAL_DATE_LAST_VISITED = "date_local"; - public static final String REMOTE_DATE_LAST_VISITED = "date_remote"; - } - - @RobocopTarget - public interface VisitsColumns { - public static final String HISTORY_GUID = "history_guid"; - public static final String VISIT_TYPE = "visit_type"; - public static final String DATE_VISITED = "date"; - // Used to distinguish between visits that were generated locally vs those that came in from Sync. - // Since we don't track "origin clientID" for visits, this is the best we can do for now. - public static final String IS_LOCAL = "is_local"; - } - - public interface PageMetadataColumns { - public static final String HISTORY_GUID = "history_guid"; - public static final String DATE_CREATED = "created"; - public static final String HAS_IMAGE = "has_image"; - public static final String JSON = "json"; - } - - public interface DeletedColumns { - public static final String ID = "id"; - public static final String GUID = "guid"; - public static final String TIME_DELETED = "timeDeleted"; - } - - @RobocopTarget - public static final class Favicons implements CommonColumns, DateSyncColumns { - private Favicons() {} - - public static final String TABLE_NAME = "favicons"; - - public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, "favicons"); - - public static final String URL = "url"; - public static final String DATA = "data"; - public static final String PAGE_URL = "page_url"; - } - - @RobocopTarget - public static final class Thumbnails implements CommonColumns { - private Thumbnails() {} - - public static final String TABLE_NAME = "thumbnails"; - - public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, "thumbnails"); - - public static final String URL = "url"; - public static final String DATA = "data"; - } - - public static final class Profiles { - private Profiles() {} - public static final String NAME = "name"; - public static final String PATH = "path"; - } - - @RobocopTarget - public static final class Bookmarks implements CommonColumns, URLColumns, FaviconColumns, SyncColumns { - private Bookmarks() {} - - public static final String TABLE_NAME = "bookmarks"; - - public static final String VIEW_WITH_FAVICONS = "bookmarks_with_favicons"; - - public static final String VIEW_WITH_ANNOTATIONS = "bookmarks_with_annotations"; - - public static final int FIXED_ROOT_ID = 0; - public static final int FAKE_DESKTOP_FOLDER_ID = -1; - public static final int FIXED_READING_LIST_ID = -2; - public static final int FIXED_PINNED_LIST_ID = -3; - public static final int FIXED_SCREENSHOT_FOLDER_ID = -4; - public static final int FAKE_READINGLIST_SMARTFOLDER_ID = -5; - - /** - * This ID and the following negative IDs are reserved for bookmarks from Android's partner - * bookmark provider. - */ - public static final long FAKE_PARTNER_BOOKMARKS_START = -1000; - - public static final String MOBILE_FOLDER_GUID = "mobile"; - public static final String PLACES_FOLDER_GUID = "places"; - public static final String MENU_FOLDER_GUID = "menu"; - public static final String TAGS_FOLDER_GUID = "tags"; - public static final String TOOLBAR_FOLDER_GUID = "toolbar"; - public static final String UNFILED_FOLDER_GUID = "unfiled"; - public static final String FAKE_DESKTOP_FOLDER_GUID = "desktop"; - public static final String PINNED_FOLDER_GUID = "pinned"; - public static final String SCREENSHOT_FOLDER_GUID = "screenshots"; - public static final String FAKE_READINGLIST_SMARTFOLDER_GUID = "readinglist"; - - public static final int TYPE_FOLDER = 0; - public static final int TYPE_BOOKMARK = 1; - public static final int TYPE_SEPARATOR = 2; - public static final int TYPE_LIVEMARK = 3; - public static final int TYPE_QUERY = 4; - - public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, "bookmarks"); - public static final Uri PARENTS_CONTENT_URI = Uri.withAppendedPath(CONTENT_URI, "parents"); - // Hacky API for bulk-updating positions. Bug 728783. - public static final Uri POSITIONS_CONTENT_URI = Uri.withAppendedPath(CONTENT_URI, "positions"); - public static final long DEFAULT_POSITION = Long.MIN_VALUE; - - public static final String CONTENT_TYPE = "vnd.android.cursor.dir/bookmark"; - public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/bookmark"; - public static final String TYPE = "type"; - public static final String PARENT = "parent"; - public static final String POSITION = "position"; - public static final String TAGS = "tags"; - public static final String DESCRIPTION = "description"; - public static final String KEYWORD = "keyword"; - - public static final String ANNOTATION_KEY = "annotation_key"; - public static final String ANNOTATION_VALUE = "annotation_value"; - } - - @RobocopTarget - public static final class History implements CommonColumns, URLColumns, HistoryColumns, FaviconColumns, SyncColumns { - private History() {} - - public static final String TABLE_NAME = "history"; - - public static final String VIEW_WITH_FAVICONS = "history_with_favicons"; - - public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, "history"); - public static final Uri CONTENT_OLD_URI = Uri.withAppendedPath(AUTHORITY_URI, "history/old"); - public static final String CONTENT_TYPE = "vnd.android.cursor.dir/browser-history"; - public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/browser-history"; - } - - @RobocopTarget - public static final class Visits implements CommonColumns, VisitsColumns { - private Visits() {} - - public static final String TABLE_NAME = "visits"; - - public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, "visits"); - - public static final int VISIT_IS_LOCAL = 1; - public static final int VISIT_IS_REMOTE = 0; - } - - // Combined bookmarks and history - @RobocopTarget - public static final class Combined implements CommonColumns, URLColumns, HistoryColumns, FaviconColumns { - private Combined() {} - - public static final String VIEW_NAME = "combined"; - - public static final String VIEW_WITH_FAVICONS = "combined_with_favicons"; - - public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, "combined"); - - public static final String BOOKMARK_ID = "bookmark_id"; - public static final String HISTORY_ID = "history_id"; - - public static final String REMOTE_VISITS_COUNT = "remoteVisitCount"; - public static final String REMOTE_DATE_LAST_VISITED = "remoteDateLastVisited"; - - public static final String LOCAL_VISITS_COUNT = "localVisitCount"; - public static final String LOCAL_DATE_LAST_VISITED = "localDateLastVisited"; - } - - public static final class Schema { - private Schema() {} - public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, "schema"); - - public static final String VERSION = "version"; - } - - public static final class Passwords { - private Passwords() {} - public static final Uri CONTENT_URI = Uri.withAppendedPath(PASSWORDS_AUTHORITY_URI, "passwords"); - public static final String CONTENT_TYPE = "vnd.android.cursor.dir/passwords"; - - public static final String ID = "id"; - public static final String HOSTNAME = "hostname"; - public static final String HTTP_REALM = "httpRealm"; - public static final String FORM_SUBMIT_URL = "formSubmitURL"; - public static final String USERNAME_FIELD = "usernameField"; - public static final String PASSWORD_FIELD = "passwordField"; - public static final String ENCRYPTED_USERNAME = "encryptedUsername"; - public static final String ENCRYPTED_PASSWORD = "encryptedPassword"; - public static final String ENC_TYPE = "encType"; - public static final String TIME_CREATED = "timeCreated"; - public static final String TIME_LAST_USED = "timeLastUsed"; - public static final String TIME_PASSWORD_CHANGED = "timePasswordChanged"; - public static final String TIMES_USED = "timesUsed"; - public static final String GUID = "guid"; - - // This needs to be kept in sync with the types defined in toolkit/components/passwordmgr/nsILoginManagerCrypto.idl#45 - public static final int ENCTYPE_SDR = 1; - } - - public static final class DeletedPasswords implements DeletedColumns { - private DeletedPasswords() {} - public static final String CONTENT_TYPE = "vnd.android.cursor.dir/deleted-passwords"; - public static final Uri CONTENT_URI = Uri.withAppendedPath(PASSWORDS_AUTHORITY_URI, "deleted-passwords"); - } - - @RobocopTarget - public static final class GeckoDisabledHosts { - private GeckoDisabledHosts() {} - public static final String CONTENT_TYPE = "vnd.android.cursor.dir/disabled-hosts"; - public static final Uri CONTENT_URI = Uri.withAppendedPath(PASSWORDS_AUTHORITY_URI, "disabled-hosts"); - - public static final String HOSTNAME = "hostname"; - } - - public static final class FormHistory { - private FormHistory() {} - public static final Uri CONTENT_URI = Uri.withAppendedPath(FORM_HISTORY_AUTHORITY_URI, "formhistory"); - public static final String CONTENT_TYPE = "vnd.android.cursor.dir/formhistory"; - - public static final String ID = "id"; - public static final String FIELD_NAME = "fieldname"; - public static final String VALUE = "value"; - public static final String TIMES_USED = "timesUsed"; - public static final String FIRST_USED = "firstUsed"; - public static final String LAST_USED = "lastUsed"; - public static final String GUID = "guid"; - } - - public static final class DeletedFormHistory implements DeletedColumns { - private DeletedFormHistory() {} - public static final Uri CONTENT_URI = Uri.withAppendedPath(FORM_HISTORY_AUTHORITY_URI, "deleted-formhistory"); - public static final String CONTENT_TYPE = "vnd.android.cursor.dir/deleted-formhistory"; - } - - @RobocopTarget - public static final class Tabs implements CommonColumns { - private Tabs() {} - public static final String TABLE_NAME = "tabs"; - - public static final Uri CONTENT_URI = Uri.withAppendedPath(TABS_AUTHORITY_URI, "tabs"); - public static final String CONTENT_TYPE = "vnd.android.cursor.dir/tab"; - public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/tab"; - - // Title of the tab. - public static final String TITLE = "title"; - - // Topmost URL from the history array. Allows processing of this tab without - // parsing that array. - public static final String URL = "url"; - - // Sync-assigned GUID for client device. NULL for local tabs. - public static final String CLIENT_GUID = "client_guid"; - - // JSON-encoded array of history URL strings, from most recent to least recent. - public static final String HISTORY = "history"; - - // Favicon URL for the tab's topmost history entry. - public static final String FAVICON = "favicon"; - - // Last used time of the tab. - public static final String LAST_USED = "last_used"; - - // Position of the tab. 0 represents foreground. - public static final String POSITION = "position"; - } - - public static final class Clients implements CommonColumns { - private Clients() {} - public static final Uri CONTENT_RECENCY_URI = Uri.withAppendedPath(TABS_AUTHORITY_URI, "clients_recency"); - public static final Uri CONTENT_URI = Uri.withAppendedPath(TABS_AUTHORITY_URI, "clients"); - public static final String CONTENT_TYPE = "vnd.android.cursor.dir/client"; - public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/client"; - - // Client-provided name string. Could conceivably be null. - public static final String NAME = "name"; - - // Sync-assigned GUID for client device. NULL for local tabs. - public static final String GUID = "guid"; - - // Last modified time for the client's tab record. For remote records, a server - // timestamp provided by Sync during insertion. - public static final String LAST_MODIFIED = "last_modified"; - - public static final String DEVICE_TYPE = "device_type"; - } - - // Data storage for dynamic panels on about:home - @RobocopTarget - public static final class HomeItems implements CommonColumns { - private HomeItems() {} - public static final Uri CONTENT_FAKE_URI = Uri.withAppendedPath(HOME_AUTHORITY_URI, "items/fake"); - public static final Uri CONTENT_URI = Uri.withAppendedPath(HOME_AUTHORITY_URI, "items"); - - public static final String CONTENT_TYPE = "vnd.android.cursor.dir/homeitem"; - public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/homeitem"; - - public static final String DATASET_ID = "dataset_id"; - public static final String URL = "url"; - public static final String TITLE = "title"; - public static final String DESCRIPTION = "description"; - public static final String IMAGE_URL = "image_url"; - public static final String BACKGROUND_COLOR = "background_color"; - public static final String BACKGROUND_URL = "background_url"; - public static final String CREATED = "created"; - public static final String FILTER = "filter"; - - public static final String[] DEFAULT_PROJECTION = - new String[] { _ID, DATASET_ID, URL, TITLE, DESCRIPTION, IMAGE_URL, BACKGROUND_COLOR, BACKGROUND_URL, FILTER }; - } - - @RobocopTarget - public static final class ReadingListItems implements CommonColumns, URLColumns { - public static final String EXCERPT = "excerpt"; - public static final String CLIENT_LAST_MODIFIED = "client_last_modified"; - public static final String GUID = "guid"; - public static final String SERVER_LAST_MODIFIED = "last_modified"; - public static final String SERVER_STORED_ON = "stored_on"; - public static final String ADDED_ON = "added_on"; - public static final String MARKED_READ_ON = "marked_read_on"; - public static final String IS_DELETED = "is_deleted"; - public static final String IS_ARCHIVED = "is_archived"; - public static final String IS_UNREAD = "is_unread"; - public static final String IS_ARTICLE = "is_article"; - public static final String IS_FAVORITE = "is_favorite"; - public static final String RESOLVED_URL = "resolved_url"; - public static final String RESOLVED_TITLE = "resolved_title"; - public static final String ADDED_BY = "added_by"; - public static final String MARKED_READ_BY = "marked_read_by"; - public static final String WORD_COUNT = "word_count"; - public static final String READ_POSITION = "read_position"; - public static final String CONTENT_STATUS = "content_status"; - - public static final String SYNC_STATUS = "sync_status"; - public static final String SYNC_CHANGE_FLAGS = "sync_change_flags"; - - private ReadingListItems() {} - public static final Uri CONTENT_URI = Uri.withAppendedPath(READING_LIST_AUTHORITY_URI, "items"); - - public static final String CONTENT_TYPE = "vnd.android.cursor.dir/readinglistitem"; - public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/readinglistitem"; - - // CONTENT_STATUS represents the result of an attempt to fetch content for the reading list item. - public static final int STATUS_UNFETCHED = 0; - public static final int STATUS_FETCH_FAILED_TEMPORARY = 1; - public static final int STATUS_FETCH_FAILED_PERMANENT = 2; - public static final int STATUS_FETCH_FAILED_UNSUPPORTED_FORMAT = 3; - public static final int STATUS_FETCHED_ARTICLE = 4; - - // See https://github.com/mozilla-services/readinglist/wiki/Client-phases for how this is expected to work. - // - // If an item is SYNCED, it doesn't need to be uploaded. - // - // If its status is NEW, the entire record should be uploaded. - // - // If DELETED, the record should be deleted. A record can only move into this state from SYNCED; NEW records - // are deleted immediately. - // - - public static final int SYNC_STATUS_SYNCED = 0; - public static final int SYNC_STATUS_NEW = 1; // Upload everything. - public static final int SYNC_STATUS_DELETED = 2; // Delete the record from the server. - public static final int SYNC_STATUS_MODIFIED = 3; // Consult SYNC_CHANGE_FLAGS. - - // SYNC_CHANGE_FLAG represents the sets of fields that need to be uploaded. - // If its status is only UNREAD_CHANGED (and maybe FAVORITE_CHANGED?), then it can easily be uploaded - // in a fire-and-forget manner. This change can never conflict. - // - // If its status is RESOLVED, then one or more of the content-oriented fields has changed, and a full - // upload of those fields should occur. These can result in conflicts. - // - // Note that these are flags; they should be considered together when deciding on a course of action. - // - // These flags are meaningless for records in any state other than SYNCED. They can be safely altered in - // other states (to avoid having to query to pre-fill a ContentValues), but should be ignored. - public static final int SYNC_CHANGE_NONE = 0; - public static final int SYNC_CHANGE_UNREAD_CHANGED = 1 << 0; // => marked_read_{on,by}, is_unread - public static final int SYNC_CHANGE_FAVORITE_CHANGED = 1 << 1; // => is_favorite - public static final int SYNC_CHANGE_RESOLVED = 1 << 2; // => is_article, resolved_{url,title}, excerpt, word_count - - - public static final String DEFAULT_SORT_ORDER = CLIENT_LAST_MODIFIED + " DESC"; - public static final String[] DEFAULT_PROJECTION = new String[] { _ID, URL, TITLE, EXCERPT, WORD_COUNT, IS_UNREAD }; - - // Minimum fields required to create a reading list item. - public static final String[] REQUIRED_FIELDS = { ReadingListItems.URL, ReadingListItems.TITLE }; - - // All fields that might be mapped from the DB into a record object. - public static final String[] ALL_FIELDS = { - CommonColumns._ID, - URLColumns.URL, - URLColumns.TITLE, - EXCERPT, - CLIENT_LAST_MODIFIED, - GUID, - SERVER_LAST_MODIFIED, - SERVER_STORED_ON, - ADDED_ON, - MARKED_READ_ON, - IS_DELETED, - IS_ARCHIVED, - IS_UNREAD, - IS_ARTICLE, - IS_FAVORITE, - RESOLVED_URL, - RESOLVED_TITLE, - ADDED_BY, - MARKED_READ_BY, - WORD_COUNT, - READ_POSITION, - CONTENT_STATUS, - - SYNC_STATUS, - SYNC_CHANGE_FLAGS, - }; - - public static final String TABLE_NAME = "reading_list"; - } - - @RobocopTarget - public static final class TopSites implements CommonColumns, URLColumns { - private TopSites() {} - - public static final int TYPE_BLANK = 0; - public static final int TYPE_TOP = 1; - public static final int TYPE_PINNED = 2; - public static final int TYPE_SUGGESTED = 3; - - public static final String BOOKMARK_ID = "bookmark_id"; - public static final String HISTORY_ID = "history_id"; - public static final String TYPE = "type"; - - public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, "topsites"); - } - - public static final class Highlights { - public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, "highlights"); - - public static final String DATE = "date"; - } - - @RobocopTarget - public static final class SearchHistory implements CommonColumns, HistoryColumns { - private SearchHistory() {} - - public static final String CONTENT_TYPE = "vnd.android.cursor.dir/searchhistory"; - public static final String QUERY = "query"; - public static final String DATE = "date"; - public static final String TABLE_NAME = "searchhistory"; - - public static final Uri CONTENT_URI = Uri.withAppendedPath(SEARCH_HISTORY_AUTHORITY_URI, "searchhistory"); - } - - @RobocopTarget - public static final class SuggestedSites implements CommonColumns, URLColumns { - private SuggestedSites() {} - - public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, "suggestedsites"); - } - - public static final class ActivityStreamBlocklist implements CommonColumns { - private ActivityStreamBlocklist() {} - - public static final String TABLE_NAME = "activity_stream_blocklist"; - public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, TABLE_NAME); - - public static final String URL = "url"; - public static final String CREATED = "created"; - } - - @RobocopTarget - public static final class UrlAnnotations implements CommonColumns, DateSyncColumns { - private UrlAnnotations() {} - - public static final String TABLE_NAME = "urlannotations"; - public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, TABLE_NAME); - - public static final String URL = "url"; - public static final String KEY = "key"; - public static final String VALUE = "value"; - public static final String SYNC_STATUS = "sync_status"; - - public enum Key { - // We use a parameter, rather than name(), as defensive coding: we can't let the - // enum name change because we've already stored values into the DB. - SCREENSHOT ("screenshot"), - - /** - * This key maps URLs to its feeds. - * - * Key: feed - * Value: URL of feed - */ - FEED("feed"), - - /** - * This key maps URLs of feeds to an object describing the feed. - * - * Key: feed_subscription - * Value: JSON object describing feed - */ - FEED_SUBSCRIPTION("feed_subscription"), - - /** - * Indicates that this URL (if stored as a bookmark) should be opened into reader view. - * - * Key: reader_view - * Value: String "true" to indicate that we would like to open into reader view. - */ - READER_VIEW("reader_view"), - - /** - * Indicator that the user interacted with the URL in regards to home screen shortcuts. - * - * Key: home_screen_shortcut - * Value: True: User created an home screen shortcut for this URL - * False: User declined to create a shortcut for this URL - */ - HOME_SCREEN_SHORTCUT("home_screen_shortcut"); - - private final String dbValue; - - Key(final String dbValue) { this.dbValue = dbValue; } - public String getDbValue() { return dbValue; } - } - - public enum SyncStatus { - // We use a parameter, rather than ordinal(), as defensive coding: we can't let the - // ordinal values change because we've already stored values into the DB. - NEW (0); - - // Value stored into the database for this column. - private final int dbValue; - - SyncStatus(final int dbValue) { - this.dbValue = dbValue; - } - - public int getDBValue() { return dbValue; } - } - - /** - * Value used to indicate that a reader view item is saved. We use the - */ - public static final String READER_VIEW_SAVED_VALUE = "true"; - } - - public static final class Numbers { - private Numbers() {} - - public static final String TABLE_NAME = "numbers"; - - public static final String POSITION = "position"; - - public static final int MAX_VALUE = 50; - } - - @RobocopTarget - public static final class Logins implements CommonColumns { - private Logins() {} - - public static final Uri CONTENT_URI = Uri.withAppendedPath(LOGINS_AUTHORITY_URI, "logins"); - public static final String CONTENT_TYPE = "vnd.android.cursor.dir/logins"; - public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/logins"; - public static final String TABLE_LOGINS = "logins"; - - public static final String HOSTNAME = "hostname"; - public static final String HTTP_REALM = "httpRealm"; - public static final String FORM_SUBMIT_URL = "formSubmitURL"; - public static final String USERNAME_FIELD = "usernameField"; - public static final String PASSWORD_FIELD = "passwordField"; - public static final String ENCRYPTED_USERNAME = "encryptedUsername"; - public static final String ENCRYPTED_PASSWORD = "encryptedPassword"; - public static final String ENC_TYPE = "encType"; - public static final String TIME_CREATED = "timeCreated"; - public static final String TIME_LAST_USED = "timeLastUsed"; - public static final String TIME_PASSWORD_CHANGED = "timePasswordChanged"; - public static final String TIMES_USED = "timesUsed"; - public static final String GUID = "guid"; - } - - @RobocopTarget - public static final class DeletedLogins implements CommonColumns { - private DeletedLogins() {} - - public static final Uri CONTENT_URI = Uri.withAppendedPath(LOGINS_AUTHORITY_URI, "deleted-logins"); - public static final String CONTENT_TYPE = "vnd.android.cursor.dir/deleted-logins"; - public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/deleted-logins"; - public static final String TABLE_DELETED_LOGINS = "deleted_logins"; - - public static final String GUID = "guid"; - public static final String TIME_DELETED = "timeDeleted"; - } - - @RobocopTarget - public static final class LoginsDisabledHosts implements CommonColumns { - private LoginsDisabledHosts() {} - - public static final Uri CONTENT_URI = Uri.withAppendedPath(LOGINS_AUTHORITY_URI, "logins-disabled-hosts"); - public static final String CONTENT_TYPE = "vnd.android.cursor.dir/logins-disabled-hosts"; - public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/logins-disabled-hosts"; - public static final String TABLE_DISABLED_HOSTS = "logins_disabled_hosts"; - - public static final String HOSTNAME = "hostname"; - } - - @RobocopTarget - public static final class PageMetadata implements CommonColumns, PageMetadataColumns { - private PageMetadata() {} - - public static final String TABLE_NAME = "page_metadata"; - public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, "page_metadata"); - } - - // We refer to the service by name to decouple services from the rest of the code base. - public static final String TAB_RECEIVED_SERVICE_CLASS_NAME = "org.mozilla.gecko.tabqueue.TabReceivedService"; - - public static final String SKIP_TAB_QUEUE_FLAG = "skip_tab_queue"; - - public static final String EXTRA_CLIENT_GUID = "org.mozilla.gecko.extra.CLIENT_ID"; -} diff --git a/mobile/android/base/java/org/mozilla/gecko/db/BrowserDB.java b/mobile/android/base/java/org/mozilla/gecko/db/BrowserDB.java deleted file mode 100644 index 4219e45b17..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/db/BrowserDB.java +++ /dev/null @@ -1,205 +0,0 @@ -/* 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.db; - -import java.io.File; -import java.util.Collection; -import java.util.EnumSet; -import java.util.List; - -import org.mozilla.gecko.GeckoProfile; -import org.mozilla.gecko.annotation.RobocopTarget; -import org.mozilla.gecko.db.BrowserContract.ExpirePriority; -import org.mozilla.gecko.distribution.Distribution; -import org.mozilla.gecko.icons.decoders.LoadFaviconResult; - -import android.content.ContentProviderClient; -import android.content.ContentProviderOperation; -import android.content.ContentResolver; -import android.content.Context; -import android.database.ContentObserver; -import android.database.Cursor; -import android.graphics.drawable.BitmapDrawable; -import android.support.v4.content.CursorLoader; - -/** - * Interface for interactions with all databases. If you want an instance - * that implements this, you should go through GeckoProfile. E.g., - * BrowserDB.from(context). - */ -public abstract class BrowserDB { - public static enum FilterFlags { - EXCLUDE_PINNED_SITES - } - - public abstract Searches getSearches(); - public abstract TabsAccessor getTabsAccessor(); - public abstract URLMetadata getURLMetadata(); - @RobocopTarget public abstract UrlAnnotations getUrlAnnotations(); - - /** - * Add default bookmarks to the database. - * Takes an offset; returns a new offset. - */ - public abstract int addDefaultBookmarks(Context context, ContentResolver cr, int offset); - - /** - * Add bookmarks from the provided distribution. - * Takes an offset; returns a new offset. - */ - public abstract int addDistributionBookmarks(ContentResolver cr, Distribution distribution, int offset); - - /** - * Invalidate cached data. - */ - public abstract void invalidate(); - - public abstract int getCount(ContentResolver cr, String database); - - /** - * @return a cursor representing the contents of the DB filtered according to the arguments. - * Can return null. CursorLoader will handle this correctly. - */ - public abstract Cursor filter(ContentResolver cr, CharSequence constraint, - int limit, EnumSet flags); - - /** - * @return a cursor over top sites (high-ranking bookmarks and history). - * Can return null. - * Returns no more than limit results. - * Suggested sites will be limited to being within the first suggestedRangeLimit results. - */ - public abstract Cursor getTopSites(ContentResolver cr, int suggestedRangeLimit, int limit); - - public abstract CursorLoader getActivityStreamTopSites(Context context, int limit); - - public abstract void updateVisitedHistory(ContentResolver cr, String uri); - - public abstract void updateHistoryTitle(ContentResolver cr, String uri, String title); - - /** - * Can return null. - */ - public abstract Cursor getAllVisitedHistory(ContentResolver cr); - - /** - * Can return null. - */ - public abstract Cursor getRecentHistory(ContentResolver cr, int limit); - - public abstract Cursor getHistoryForURL(ContentResolver cr, String uri); - - public abstract Cursor getRecentHistoryBetweenTime(ContentResolver cr, int historyLimit, long start, long end); - - public abstract long getPrePathLastVisitedTimeMilliseconds(ContentResolver cr, String prePath); - - public abstract void expireHistory(ContentResolver cr, ExpirePriority priority); - - public abstract void removeHistoryEntry(ContentResolver cr, String url); - - public abstract void clearHistory(ContentResolver cr, boolean clearSearchHistory); - - - public abstract String getUrlForKeyword(ContentResolver cr, String keyword); - - public abstract boolean isBookmark(ContentResolver cr, String uri); - public abstract boolean addBookmark(ContentResolver cr, String title, String uri); - public abstract Cursor getBookmarkForUrl(ContentResolver cr, String url); - public abstract Cursor getBookmarksForPartialUrl(ContentResolver cr, String partialUrl); - public abstract void removeBookmarksWithURL(ContentResolver cr, String uri); - public abstract void registerBookmarkObserver(ContentResolver cr, ContentObserver observer); - public abstract void updateBookmark(ContentResolver cr, int id, String uri, String title, String keyword); - public abstract boolean hasBookmarkWithGuid(ContentResolver cr, String guid); - - public abstract boolean insertPageMetadata(ContentProviderClient contentProviderClient, String pageUrl, boolean hasImage, String metadataJSON); - public abstract int deletePageMetadata(ContentProviderClient contentProviderClient, String pageUrl); - /** - * Can return null. - */ - public abstract Cursor getBookmarksInFolder(ContentResolver cr, long folderId); - - public abstract int getBookmarkCountForFolder(ContentResolver cr, long folderId); - - /** - * Get the favicon from the database, if any, associated with the given favicon URL. (That is, - * the URL of the actual favicon image, not the URL of the page with which the favicon is associated.) - * @param cr The ContentResolver to use. - * @param faviconURL The URL of the favicon to fetch from the database. - * @return The decoded Bitmap from the database, if any. null if none is stored. - */ - public abstract LoadFaviconResult getFaviconForUrl(Context context, ContentResolver cr, String faviconURL); - - /** - * Try to find a usable favicon URL in the history or bookmarks table. - */ - public abstract String getFaviconURLFromPageURL(ContentResolver cr, String uri); - - public abstract byte[] getThumbnailForUrl(ContentResolver cr, String uri); - public abstract void updateThumbnailForUrl(ContentResolver cr, String uri, BitmapDrawable thumbnail); - - /** - * Query for non-null thumbnails matching the provided urls. - * The returned cursor will have no more than, but possibly fewer than, - * the requested number of thumbnails. - * - * Returns null if the provided list of URLs is empty or null. - */ - public abstract Cursor getThumbnailsForUrls(ContentResolver cr, - List urls); - - public abstract void removeThumbnails(ContentResolver cr); - - // Utility function for updating existing history using batch operations - public abstract void updateHistoryInBatch(ContentResolver cr, - Collection operations, String url, - String title, long date, int visits); - - public abstract void updateBookmarkInBatch(ContentResolver cr, - Collection operations, String url, - String title, String guid, long parent, long added, long modified, - long position, String keyword, int type); - - public abstract void pinSite(ContentResolver cr, String url, String title, int position); - public abstract void unpinSite(ContentResolver cr, int position); - - public abstract boolean hideSuggestedSite(String url); - public abstract void setSuggestedSites(SuggestedSites suggestedSites); - public abstract SuggestedSites getSuggestedSites(); - public abstract boolean hasSuggestedImageUrl(String url); - public abstract String getSuggestedImageUrlForUrl(String url); - public abstract int getSuggestedBackgroundColorForUrl(String url); - - /** - * Obtain a set of links for highlights from bookmarks and history. - * - * @param context The context to load the cursor. - * @param limit Maximum number of results to return. - */ - public abstract CursorLoader getHighlights(Context context, int limit); - - /** - * Block a page from the highlights list. - * - * @param url The page URL. Only pages exactly matching this URL will be blocked. - */ - public abstract void blockActivityStreamSite(ContentResolver cr, String url); - - public static BrowserDB from(final Context context) { - return from(GeckoProfile.get(context)); - } - - public static BrowserDB from(final GeckoProfile profile) { - synchronized (profile.getLock()) { - BrowserDB db = (BrowserDB) profile.getData(); - if (db != null) { - return db; - } - - db = new LocalBrowserDB(profile.getName()); - profile.setData(db); - return db; - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/db/BrowserDatabaseHelper.java b/mobile/android/base/java/org/mozilla/gecko/db/BrowserDatabaseHelper.java deleted file mode 100644 index f823d90609..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/db/BrowserDatabaseHelper.java +++ /dev/null @@ -1,2237 +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.db; - -import java.io.File; -import java.io.UnsupportedEncodingException; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import org.mozilla.apache.commons.codec.binary.Base32; -import org.json.simple.JSONArray; -import org.json.simple.JSONObject; -import org.mozilla.gecko.GeckoProfile; -import org.mozilla.gecko.R; -import org.mozilla.gecko.annotation.RobocopTarget; -import org.mozilla.gecko.db.BrowserContract.ActivityStreamBlocklist; -import org.mozilla.gecko.db.BrowserContract.Bookmarks; -import org.mozilla.gecko.db.BrowserContract.Combined; -import org.mozilla.gecko.db.BrowserContract.Favicons; -import org.mozilla.gecko.db.BrowserContract.History; -import org.mozilla.gecko.db.BrowserContract.Visits; -import org.mozilla.gecko.db.BrowserContract.PageMetadata; -import org.mozilla.gecko.db.BrowserContract.Numbers; -import org.mozilla.gecko.db.BrowserContract.ReadingListItems; -import org.mozilla.gecko.db.BrowserContract.SearchHistory; -import org.mozilla.gecko.db.BrowserContract.Thumbnails; -import org.mozilla.gecko.db.BrowserContract.UrlAnnotations; -import org.mozilla.gecko.fxa.FirefoxAccounts; -import org.mozilla.gecko.reader.SavedReaderViewHelper; -import org.mozilla.gecko.sync.Utils; -import org.mozilla.gecko.sync.repositories.android.RepoUtils; -import org.mozilla.gecko.util.FileUtils; - -import static org.mozilla.gecko.db.DBUtils.qualifyColumn; - -import android.content.ContentValues; -import android.content.Context; -import android.database.Cursor; -import android.database.DatabaseUtils; -import android.database.SQLException; -import android.database.sqlite.SQLiteDatabase; -import android.database.sqlite.SQLiteException; -import android.database.sqlite.SQLiteOpenHelper; -import android.database.sqlite.SQLiteStatement; -import android.net.Uri; -import android.os.Build; -import android.util.Log; - - -// public for robocop testing -public final class BrowserDatabaseHelper extends SQLiteOpenHelper { - private static final String LOGTAG = "GeckoBrowserDBHelper"; - - // Replace the Bug number below with your Bug that is conducting a DB upgrade, as to force a merge conflict with any - // other patches that require a DB upgrade. - public static final int DATABASE_VERSION = 36; // Bug 1301717 - public static final String DATABASE_NAME = "browser.db"; - - final protected Context mContext; - - static final String TABLE_BOOKMARKS = Bookmarks.TABLE_NAME; - static final String TABLE_HISTORY = History.TABLE_NAME; - static final String TABLE_VISITS = Visits.TABLE_NAME; - static final String TABLE_PAGE_METADATA = PageMetadata.TABLE_NAME; - static final String TABLE_FAVICONS = Favicons.TABLE_NAME; - static final String TABLE_THUMBNAILS = Thumbnails.TABLE_NAME; - static final String TABLE_READING_LIST = ReadingListItems.TABLE_NAME; - static final String TABLE_TABS = TabsProvider.TABLE_TABS; - static final String TABLE_CLIENTS = TabsProvider.TABLE_CLIENTS; - static final String TABLE_LOGINS = BrowserContract.Logins.TABLE_LOGINS; - static final String TABLE_DELETED_LOGINS = BrowserContract.DeletedLogins.TABLE_DELETED_LOGINS; - static final String TABLE_DISABLED_HOSTS = BrowserContract.LoginsDisabledHosts.TABLE_DISABLED_HOSTS; - static final String TABLE_ANNOTATIONS = UrlAnnotations.TABLE_NAME; - - static final String VIEW_COMBINED = Combined.VIEW_NAME; - static final String VIEW_BOOKMARKS_WITH_FAVICONS = Bookmarks.VIEW_WITH_FAVICONS; - static final String VIEW_BOOKMARKS_WITH_ANNOTATIONS = Bookmarks.VIEW_WITH_ANNOTATIONS; - static final String VIEW_HISTORY_WITH_FAVICONS = History.VIEW_WITH_FAVICONS; - static final String VIEW_COMBINED_WITH_FAVICONS = Combined.VIEW_WITH_FAVICONS; - - static final String TABLE_BOOKMARKS_JOIN_FAVICONS = TABLE_BOOKMARKS + " LEFT OUTER JOIN " + - TABLE_FAVICONS + " ON " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.FAVICON_ID) + " = " + - qualifyColumn(TABLE_FAVICONS, Favicons._ID); - - static final String TABLE_BOOKMARKS_JOIN_ANNOTATIONS = TABLE_BOOKMARKS + " JOIN " + - TABLE_ANNOTATIONS + " ON " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.URL) + " = " + - qualifyColumn(TABLE_ANNOTATIONS, UrlAnnotations.URL); - - static final String TABLE_HISTORY_JOIN_FAVICONS = TABLE_HISTORY + " LEFT OUTER JOIN " + - TABLE_FAVICONS + " ON " + qualifyColumn(TABLE_HISTORY, History.FAVICON_ID) + " = " + - qualifyColumn(TABLE_FAVICONS, Favicons._ID); - - static final String TABLE_BOOKMARKS_TMP = TABLE_BOOKMARKS + "_tmp"; - static final String TABLE_HISTORY_TMP = TABLE_HISTORY + "_tmp"; - - private static final String[] mobileIdColumns = new String[] { Bookmarks._ID }; - private static final String[] mobileIdSelectionArgs = new String[] { Bookmarks.MOBILE_FOLDER_GUID }; - - private boolean didCreateTabsTable = false; - private boolean didCreateCurrentReadingListTable = false; - - public BrowserDatabaseHelper(Context context, String databasePath) { - super(context, databasePath, null, DATABASE_VERSION); - mContext = context; - } - - private void createBookmarksTable(SQLiteDatabase db) { - debug("Creating " + TABLE_BOOKMARKS + " table"); - - db.execSQL("CREATE TABLE " + TABLE_BOOKMARKS + "(" + - Bookmarks._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," + - Bookmarks.TITLE + " TEXT," + - Bookmarks.URL + " TEXT," + - Bookmarks.TYPE + " INTEGER NOT NULL DEFAULT " + Bookmarks.TYPE_BOOKMARK + "," + - Bookmarks.PARENT + " INTEGER," + - Bookmarks.POSITION + " INTEGER NOT NULL," + - Bookmarks.KEYWORD + " TEXT," + - Bookmarks.DESCRIPTION + " TEXT," + - Bookmarks.TAGS + " TEXT," + - Bookmarks.FAVICON_ID + " INTEGER," + - Bookmarks.DATE_CREATED + " INTEGER," + - Bookmarks.DATE_MODIFIED + " INTEGER," + - Bookmarks.GUID + " TEXT NOT NULL," + - Bookmarks.IS_DELETED + " INTEGER NOT NULL DEFAULT 0, " + - "FOREIGN KEY (" + Bookmarks.PARENT + ") REFERENCES " + - TABLE_BOOKMARKS + "(" + Bookmarks._ID + ")" + - ");"); - - db.execSQL("CREATE INDEX bookmarks_url_index ON " + TABLE_BOOKMARKS + "(" - + Bookmarks.URL + ")"); - db.execSQL("CREATE INDEX bookmarks_type_deleted_index ON " + TABLE_BOOKMARKS + "(" - + Bookmarks.TYPE + ", " + Bookmarks.IS_DELETED + ")"); - db.execSQL("CREATE UNIQUE INDEX bookmarks_guid_index ON " + TABLE_BOOKMARKS + "(" - + Bookmarks.GUID + ")"); - db.execSQL("CREATE INDEX bookmarks_modified_index ON " + TABLE_BOOKMARKS + "(" - + Bookmarks.DATE_MODIFIED + ")"); - } - - private void createHistoryTable(SQLiteDatabase db) { - debug("Creating " + TABLE_HISTORY + " table"); - db.execSQL("CREATE TABLE " + TABLE_HISTORY + "(" + - History._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," + - History.TITLE + " TEXT," + - History.URL + " TEXT NOT NULL," + - // Can we drop VISITS count? Can we calculate it in the Combined view as a sum? - // See Bug 1277329. - History.VISITS + " INTEGER NOT NULL DEFAULT 0," + - History.LOCAL_VISITS + " INTEGER NOT NULL DEFAULT 0," + - History.REMOTE_VISITS + " INTEGER NOT NULL DEFAULT 0," + - History.FAVICON_ID + " INTEGER," + - History.DATE_LAST_VISITED + " INTEGER," + - History.LOCAL_DATE_LAST_VISITED + " INTEGER NOT NULL DEFAULT 0," + - History.REMOTE_DATE_LAST_VISITED + " INTEGER NOT NULL DEFAULT 0," + - History.DATE_CREATED + " INTEGER," + - History.DATE_MODIFIED + " INTEGER," + - History.GUID + " TEXT NOT NULL," + - History.IS_DELETED + " INTEGER NOT NULL DEFAULT 0" + - ");"); - - db.execSQL("CREATE INDEX history_url_index ON " + TABLE_HISTORY + '(' - + History.URL + ')'); - db.execSQL("CREATE UNIQUE INDEX history_guid_index ON " + TABLE_HISTORY + '(' - + History.GUID + ')'); - db.execSQL("CREATE INDEX history_modified_index ON " + TABLE_HISTORY + '(' - + History.DATE_MODIFIED + ')'); - db.execSQL("CREATE INDEX history_visited_index ON " + TABLE_HISTORY + '(' - + History.DATE_LAST_VISITED + ')'); - } - - private void createVisitsTable(SQLiteDatabase db) { - debug("Creating " + TABLE_VISITS + " table"); - db.execSQL("CREATE TABLE " + TABLE_VISITS + "(" + - Visits._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," + - Visits.HISTORY_GUID + " TEXT NOT NULL," + - Visits.VISIT_TYPE + " TINYINT NOT NULL DEFAULT 1," + - Visits.DATE_VISITED + " INTEGER NOT NULL, " + - Visits.IS_LOCAL + " TINYINT NOT NULL DEFAULT 1, " + - - "FOREIGN KEY (" + Visits.HISTORY_GUID + ") REFERENCES " + - TABLE_HISTORY + "(" + History.GUID + ") ON DELETE CASCADE ON UPDATE CASCADE" + - ");"); - - db.execSQL("CREATE UNIQUE INDEX visits_history_guid_and_date_visited_index ON " + TABLE_VISITS + "(" - + Visits.HISTORY_GUID + "," + Visits.DATE_VISITED + ")"); - db.execSQL("CREATE INDEX visits_history_guid_index ON " + TABLE_VISITS + "(" + Visits.HISTORY_GUID + ")"); - } - - private void createFaviconsTable(SQLiteDatabase db) { - debug("Creating " + TABLE_FAVICONS + " table"); - db.execSQL("CREATE TABLE " + TABLE_FAVICONS + " (" + - Favicons._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," + - Favicons.URL + " TEXT UNIQUE," + - Favicons.DATA + " BLOB," + - Favicons.DATE_CREATED + " INTEGER," + - Favicons.DATE_MODIFIED + " INTEGER" + - ");"); - - db.execSQL("CREATE INDEX favicons_modified_index ON " + TABLE_FAVICONS + "(" - + Favicons.DATE_MODIFIED + ")"); - } - - private void createThumbnailsTable(SQLiteDatabase db) { - debug("Creating " + TABLE_THUMBNAILS + " table"); - db.execSQL("CREATE TABLE " + TABLE_THUMBNAILS + " (" + - Thumbnails._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," + - Thumbnails.URL + " TEXT UNIQUE," + - Thumbnails.DATA + " BLOB" + - ");"); - } - - private void createPageMetadataTable(SQLiteDatabase db) { - debug("Creating " + TABLE_PAGE_METADATA + " table"); - db.execSQL("CREATE TABLE " + TABLE_PAGE_METADATA + "(" + - PageMetadata._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," + - PageMetadata.HISTORY_GUID + " TEXT NOT NULL," + - PageMetadata.DATE_CREATED + " INTEGER NOT NULL, " + - PageMetadata.HAS_IMAGE + " TINYINT NOT NULL DEFAULT 0, " + - PageMetadata.JSON + " TEXT NOT NULL, " + - - "FOREIGN KEY (" + Visits.HISTORY_GUID + ") REFERENCES " + - TABLE_HISTORY + "(" + History.GUID + ") ON DELETE CASCADE ON UPDATE CASCADE" + - ");"); - - // Establish a 1-to-1 relationship with History table. - db.execSQL("CREATE UNIQUE INDEX page_metadata_history_guid ON " + TABLE_PAGE_METADATA + "(" - + PageMetadata.HISTORY_GUID + ")"); - // Improve performance of commonly occurring selections. - db.execSQL("CREATE INDEX page_metadata_history_guid_and_has_image ON " + TABLE_PAGE_METADATA + "(" - + PageMetadata.HISTORY_GUID + ", " + PageMetadata.HAS_IMAGE + ")"); - } - - private void createBookmarksWithFaviconsView(SQLiteDatabase db) { - debug("Creating " + VIEW_BOOKMARKS_WITH_FAVICONS + " view"); - - db.execSQL("CREATE VIEW IF NOT EXISTS " + VIEW_BOOKMARKS_WITH_FAVICONS + " AS " + - "SELECT " + qualifyColumn(TABLE_BOOKMARKS, "*") + - ", " + qualifyColumn(TABLE_FAVICONS, Favicons.DATA) + " AS " + Bookmarks.FAVICON + - ", " + qualifyColumn(TABLE_FAVICONS, Favicons.URL) + " AS " + Bookmarks.FAVICON_URL + - " FROM " + TABLE_BOOKMARKS_JOIN_FAVICONS); - } - - private void createBookmarksWithAnnotationsView(SQLiteDatabase db) { - debug("Creating " + VIEW_BOOKMARKS_WITH_ANNOTATIONS + " view"); - - db.execSQL("CREATE VIEW IF NOT EXISTS " + VIEW_BOOKMARKS_WITH_ANNOTATIONS + " AS " + - "SELECT " + qualifyColumn(TABLE_BOOKMARKS, "*") + - ", " + qualifyColumn(TABLE_ANNOTATIONS, UrlAnnotations.KEY) + " AS " + Bookmarks.ANNOTATION_KEY + - ", " + qualifyColumn(TABLE_ANNOTATIONS, UrlAnnotations.VALUE) + " AS " + Bookmarks.ANNOTATION_VALUE + - " FROM " + TABLE_BOOKMARKS_JOIN_ANNOTATIONS); - } - - private void createHistoryWithFaviconsView(SQLiteDatabase db) { - debug("Creating " + VIEW_HISTORY_WITH_FAVICONS + " view"); - - db.execSQL("CREATE VIEW IF NOT EXISTS " + VIEW_HISTORY_WITH_FAVICONS + " AS " + - "SELECT " + qualifyColumn(TABLE_HISTORY, "*") + - ", " + qualifyColumn(TABLE_FAVICONS, Favicons.DATA) + " AS " + History.FAVICON + - ", " + qualifyColumn(TABLE_FAVICONS, Favicons.URL) + " AS " + History.FAVICON_URL + - " FROM " + TABLE_HISTORY_JOIN_FAVICONS); - } - - private void createClientsTable(SQLiteDatabase db) { - debug("Creating " + TABLE_CLIENTS + " table"); - - // Table for client's name-guid mapping. - db.execSQL("CREATE TABLE " + TABLE_CLIENTS + "(" + - BrowserContract.Clients.GUID + " TEXT PRIMARY KEY," + - BrowserContract.Clients.NAME + " TEXT," + - BrowserContract.Clients.LAST_MODIFIED + " INTEGER," + - BrowserContract.Clients.DEVICE_TYPE + " TEXT" + - ");"); - } - - private void createTabsTable(SQLiteDatabase db, final String tableName) { - debug("Creating tabs.db: " + db.getPath()); - debug("Creating " + tableName + " table"); - - // Table for each tab on any client. - db.execSQL("CREATE TABLE " + tableName + "(" + - BrowserContract.Tabs._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," + - BrowserContract.Tabs.CLIENT_GUID + " TEXT," + - BrowserContract.Tabs.TITLE + " TEXT," + - BrowserContract.Tabs.URL + " TEXT," + - BrowserContract.Tabs.HISTORY + " TEXT," + - BrowserContract.Tabs.FAVICON + " TEXT," + - BrowserContract.Tabs.LAST_USED + " INTEGER," + - BrowserContract.Tabs.POSITION + " INTEGER, " + - "FOREIGN KEY (" + BrowserContract.Tabs.CLIENT_GUID + ") REFERENCES " + - TABLE_CLIENTS + "(" + BrowserContract.Clients.GUID + ") ON DELETE CASCADE" + - ");"); - - didCreateTabsTable = true; - } - - private void createTabsTableIndices(SQLiteDatabase db, final String tableName) { - // Indices on CLIENT_GUID and POSITION. - db.execSQL("CREATE INDEX " + TabsProvider.INDEX_TABS_GUID + - " ON " + tableName + "(" + BrowserContract.Tabs.CLIENT_GUID + ")"); - db.execSQL("CREATE INDEX " + TabsProvider.INDEX_TABS_POSITION + - " ON " + tableName + "(" + BrowserContract.Tabs.POSITION + ")"); - } - - // Insert a client row for our local Fennec client. - private void createLocalClient(SQLiteDatabase db) { - debug("Inserting local Fennec client into " + TABLE_CLIENTS + " table"); - - ContentValues values = new ContentValues(); - values.put(BrowserContract.Clients.LAST_MODIFIED, System.currentTimeMillis()); - db.insertOrThrow(TABLE_CLIENTS, null, values); - } - - private void createCombinedViewOn19(SQLiteDatabase db) { - /* - The v19 combined view removes the redundant subquery from the v16 - combined view and reorders the columns as necessary to prevent this - from breaking any code that might be referencing columns by index. - - The rows in the ensuing view are, in order: - - Combined.BOOKMARK_ID - Combined.HISTORY_ID - Combined._ID (always 0) - Combined.URL - Combined.TITLE - Combined.VISITS - Combined.DATE_LAST_VISITED - Combined.FAVICON_ID - - We need to return an _id column because CursorAdapter requires it for its - default implementation for the getItemId() method. However, since - we're not using this feature in the parts of the UI using this view, - we can just use 0 for all rows. - */ - - db.execSQL("CREATE VIEW IF NOT EXISTS " + VIEW_COMBINED + " AS" + - - // Bookmarks without history. - " SELECT " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks._ID) + " AS " + Combined.BOOKMARK_ID + "," + - "-1 AS " + Combined.HISTORY_ID + "," + - "0 AS " + Combined._ID + "," + - qualifyColumn(TABLE_BOOKMARKS, Bookmarks.URL) + " AS " + Combined.URL + ", " + - qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TITLE) + " AS " + Combined.TITLE + ", " + - "-1 AS " + Combined.VISITS + ", " + - "-1 AS " + Combined.DATE_LAST_VISITED + "," + - qualifyColumn(TABLE_BOOKMARKS, Bookmarks.FAVICON_ID) + " AS " + Combined.FAVICON_ID + - " FROM " + TABLE_BOOKMARKS + - " WHERE " + - qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TYPE) + " = " + Bookmarks.TYPE_BOOKMARK + " AND " + - // Ignore pinned bookmarks. - qualifyColumn(TABLE_BOOKMARKS, Bookmarks.PARENT) + " <> " + Bookmarks.FIXED_PINNED_LIST_ID + " AND " + - qualifyColumn(TABLE_BOOKMARKS, Bookmarks.IS_DELETED) + " = 0 AND " + - qualifyColumn(TABLE_BOOKMARKS, Bookmarks.URL) + - " NOT IN (SELECT " + History.URL + " FROM " + TABLE_HISTORY + ")" + - " UNION ALL" + - - // History with and without bookmark. - " SELECT " + - "CASE " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.IS_DELETED) + - - // Give pinned bookmarks a NULL ID so that they're not treated as bookmarks. We can't - // completely ignore them here because they're joined with history entries we care about. - " WHEN 0 THEN " + - "CASE " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.PARENT) + - " WHEN " + Bookmarks.FIXED_PINNED_LIST_ID + " THEN " + - "NULL " + - "ELSE " + - qualifyColumn(TABLE_BOOKMARKS, Bookmarks._ID) + - " END " + - "ELSE " + - "NULL " + - "END AS " + Combined.BOOKMARK_ID + "," + - qualifyColumn(TABLE_HISTORY, History._ID) + " AS " + Combined.HISTORY_ID + "," + - "0 AS " + Combined._ID + "," + - qualifyColumn(TABLE_HISTORY, History.URL) + " AS " + Combined.URL + "," + - - // Prioritize bookmark titles over history titles, since the user may have - // customized the title for a bookmark. - "COALESCE(" + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TITLE) + ", " + - qualifyColumn(TABLE_HISTORY, History.TITLE) + - ") AS " + Combined.TITLE + "," + - qualifyColumn(TABLE_HISTORY, History.VISITS) + " AS " + Combined.VISITS + "," + - qualifyColumn(TABLE_HISTORY, History.DATE_LAST_VISITED) + " AS " + Combined.DATE_LAST_VISITED + "," + - qualifyColumn(TABLE_HISTORY, History.FAVICON_ID) + " AS " + Combined.FAVICON_ID + - - // We really shouldn't be selecting deleted bookmarks, but oh well. - " FROM " + TABLE_HISTORY + " LEFT OUTER JOIN " + TABLE_BOOKMARKS + - " ON " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.URL) + " = " + qualifyColumn(TABLE_HISTORY, History.URL) + - " WHERE " + - qualifyColumn(TABLE_HISTORY, History.IS_DELETED) + " = 0 AND " + - "(" + - // The left outer join didn't match... - qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TYPE) + " IS NULL OR " + - - // ... or it's a bookmark. This is less efficient than filtering prior - // to the join if you have lots of folders. - qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TYPE) + " = " + Bookmarks.TYPE_BOOKMARK + - ")" - ); - - debug("Creating " + VIEW_COMBINED_WITH_FAVICONS + " view"); - - db.execSQL("CREATE VIEW IF NOT EXISTS " + VIEW_COMBINED_WITH_FAVICONS + " AS" + - " SELECT " + qualifyColumn(VIEW_COMBINED, "*") + ", " + - qualifyColumn(TABLE_FAVICONS, Favicons.URL) + " AS " + Combined.FAVICON_URL + ", " + - qualifyColumn(TABLE_FAVICONS, Favicons.DATA) + " AS " + Combined.FAVICON + - " FROM " + VIEW_COMBINED + " LEFT OUTER JOIN " + TABLE_FAVICONS + - " ON " + Combined.FAVICON_ID + " = " + qualifyColumn(TABLE_FAVICONS, Favicons._ID)); - - } - - private void createCombinedViewOn33(final SQLiteDatabase db) { - /* - Builds on top of v19 combined view, and adds the following aggregates: - - Combined.LOCAL_DATE_LAST_VISITED - last date visited for all local visits - - Combined.REMOTE_DATE_LAST_VISITED - last date visited for all remote visits - - Combined.LOCAL_VISITS_COUNT - total number of local visits - - Combined.REMOTE_VISITS_COUNT - total number of remote visits - - Any code written prior to v33 referencing columns by index directly remains intact - (yet must die a fiery death), as new columns were added to the end of the list. - - The rows in the ensuing view are, in order: - Combined.BOOKMARK_ID - Combined.HISTORY_ID - Combined._ID (always 0) - Combined.URL - Combined.TITLE - Combined.VISITS - Combined.DATE_LAST_VISITED - Combined.FAVICON_ID - Combined.LOCAL_DATE_LAST_VISITED - Combined.REMOTE_DATE_LAST_VISITED - Combined.LOCAL_VISITS_COUNT - Combined.REMOTE_VISITS_COUNT - */ - db.execSQL("CREATE VIEW IF NOT EXISTS " + VIEW_COMBINED + " AS" + - - // Bookmarks without history. - " SELECT " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks._ID) + " AS " + Combined.BOOKMARK_ID + "," + - "-1 AS " + Combined.HISTORY_ID + "," + - "0 AS " + Combined._ID + "," + - qualifyColumn(TABLE_BOOKMARKS, Bookmarks.URL) + " AS " + Combined.URL + ", " + - qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TITLE) + " AS " + Combined.TITLE + ", " + - "-1 AS " + Combined.VISITS + ", " + - "-1 AS " + Combined.DATE_LAST_VISITED + "," + - qualifyColumn(TABLE_BOOKMARKS, Bookmarks.FAVICON_ID) + " AS " + Combined.FAVICON_ID + "," + - "0 AS " + Combined.LOCAL_DATE_LAST_VISITED + ", " + - "0 AS " + Combined.REMOTE_DATE_LAST_VISITED + ", " + - "0 AS " + Combined.LOCAL_VISITS_COUNT + ", " + - "0 AS " + Combined.REMOTE_VISITS_COUNT + - " FROM " + TABLE_BOOKMARKS + - " WHERE " + - qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TYPE) + " = " + Bookmarks.TYPE_BOOKMARK + " AND " + - // Ignore pinned bookmarks. - qualifyColumn(TABLE_BOOKMARKS, Bookmarks.PARENT) + " <> " + Bookmarks.FIXED_PINNED_LIST_ID + " AND " + - qualifyColumn(TABLE_BOOKMARKS, Bookmarks.IS_DELETED) + " = 0 AND " + - qualifyColumn(TABLE_BOOKMARKS, Bookmarks.URL) + - " NOT IN (SELECT " + History.URL + " FROM " + TABLE_HISTORY + ")" + - " UNION ALL" + - - // History with and without bookmark. - " SELECT " + - "CASE " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.IS_DELETED) + - - // Give pinned bookmarks a NULL ID so that they're not treated as bookmarks. We can't - // completely ignore them here because they're joined with history entries we care about. - " WHEN 0 THEN " + - "CASE " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.PARENT) + - " WHEN " + Bookmarks.FIXED_PINNED_LIST_ID + " THEN " + - "NULL " + - "ELSE " + - qualifyColumn(TABLE_BOOKMARKS, Bookmarks._ID) + - " END " + - "ELSE " + - "NULL " + - "END AS " + Combined.BOOKMARK_ID + "," + - qualifyColumn(TABLE_HISTORY, History._ID) + " AS " + Combined.HISTORY_ID + "," + - "0 AS " + Combined._ID + "," + - qualifyColumn(TABLE_HISTORY, History.URL) + " AS " + Combined.URL + "," + - - // Prioritize bookmark titles over history titles, since the user may have - // customized the title for a bookmark. - "COALESCE(" + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TITLE) + ", " + - qualifyColumn(TABLE_HISTORY, History.TITLE) + - ") AS " + Combined.TITLE + "," + - qualifyColumn(TABLE_HISTORY, History.VISITS) + " AS " + Combined.VISITS + "," + - qualifyColumn(TABLE_HISTORY, History.DATE_LAST_VISITED) + " AS " + Combined.DATE_LAST_VISITED + "," + - qualifyColumn(TABLE_HISTORY, History.FAVICON_ID) + " AS " + Combined.FAVICON_ID + "," + - - // Figure out "last visited" days using MAX values for visit timestamps. - // We use CASE statements here to separate local from remote visits. - "COALESCE(MAX(CASE " + qualifyColumn(TABLE_VISITS, Visits.IS_LOCAL) + " " + - "WHEN 1 THEN " + qualifyColumn(TABLE_VISITS, Visits.DATE_VISITED) + " " + - "ELSE 0 END" + - "), 0) AS " + Combined.LOCAL_DATE_LAST_VISITED + ", " + - - "COALESCE(MAX(CASE " + qualifyColumn(TABLE_VISITS, Visits.IS_LOCAL) + " " + - "WHEN 0 THEN " + qualifyColumn(TABLE_VISITS, Visits.DATE_VISITED) + " " + - "ELSE 0 END" + - "), 0) AS " + Combined.REMOTE_DATE_LAST_VISITED + ", " + - - // Sum up visit counts for local and remote visit types. Again, use CASE to separate the two. - "COALESCE(SUM(" + qualifyColumn(TABLE_VISITS, Visits.IS_LOCAL) + "), 0) AS " + Combined.LOCAL_VISITS_COUNT + ", " + - "COALESCE(SUM(CASE " + qualifyColumn(TABLE_VISITS, Visits.IS_LOCAL) + " WHEN 0 THEN 1 ELSE 0 END), 0) AS " + Combined.REMOTE_VISITS_COUNT + - - // We need to JOIN on Visits in order to compute visit counts - " FROM " + TABLE_HISTORY + " " + - "LEFT OUTER JOIN " + TABLE_VISITS + - " ON " + qualifyColumn(TABLE_HISTORY, History.GUID) + " = " + qualifyColumn(TABLE_VISITS, Visits.HISTORY_GUID) + " " + - - // We really shouldn't be selecting deleted bookmarks, but oh well. - "LEFT OUTER JOIN " + TABLE_BOOKMARKS + - " ON " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.URL) + " = " + qualifyColumn(TABLE_HISTORY, History.URL) + - " WHERE " + - qualifyColumn(TABLE_HISTORY, History.IS_DELETED) + " = 0 AND " + - "(" + - // The left outer join didn't match... - qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TYPE) + " IS NULL OR " + - - // ... or it's a bookmark. This is less efficient than filtering prior - // to the join if you have lots of folders. - qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TYPE) + " = " + Bookmarks.TYPE_BOOKMARK + - - ") GROUP BY " + qualifyColumn(TABLE_HISTORY, History.GUID) - ); - - debug("Creating " + VIEW_COMBINED_WITH_FAVICONS + " view"); - - db.execSQL("CREATE VIEW IF NOT EXISTS " + VIEW_COMBINED_WITH_FAVICONS + " AS" + - " SELECT " + qualifyColumn(VIEW_COMBINED, "*") + ", " + - qualifyColumn(TABLE_FAVICONS, Favicons.URL) + " AS " + Combined.FAVICON_URL + ", " + - qualifyColumn(TABLE_FAVICONS, Favicons.DATA) + " AS " + Combined.FAVICON + - " FROM " + VIEW_COMBINED + " LEFT OUTER JOIN " + TABLE_FAVICONS + - " ON " + Combined.FAVICON_ID + " = " + qualifyColumn(TABLE_FAVICONS, Favicons._ID)); - } - - private void createCombinedViewOn34(final SQLiteDatabase db) { - /* - Builds on top of v33 combined view, and instead of calculating the following aggregates, gets them - from the history table: - - Combined.LOCAL_DATE_LAST_VISITED - last date visited for all local visits - - Combined.REMOTE_DATE_LAST_VISITED - last date visited for all remote visits - - Combined.LOCAL_VISITS_COUNT - total number of local visits - - Combined.REMOTE_VISITS_COUNT - total number of remote visits - - Any code written prior to v33 referencing columns by index directly remains intact - (yet must die a fiery death), as new columns were added to the end of the list. - - The rows in the ensuing view are, in order: - Combined.BOOKMARK_ID - Combined.HISTORY_ID - Combined._ID (always 0) - Combined.URL - Combined.TITLE - Combined.VISITS - Combined.DATE_LAST_VISITED - Combined.FAVICON_ID - Combined.LOCAL_DATE_LAST_VISITED - Combined.REMOTE_DATE_LAST_VISITED - Combined.LOCAL_VISITS_COUNT - Combined.REMOTE_VISITS_COUNT - */ - db.execSQL("CREATE VIEW IF NOT EXISTS " + VIEW_COMBINED + " AS" + - - // Bookmarks without history. - " SELECT " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks._ID) + " AS " + Combined.BOOKMARK_ID + "," + - "-1 AS " + Combined.HISTORY_ID + "," + - "0 AS " + Combined._ID + "," + - qualifyColumn(TABLE_BOOKMARKS, Bookmarks.URL) + " AS " + Combined.URL + ", " + - qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TITLE) + " AS " + Combined.TITLE + ", " + - "-1 AS " + Combined.VISITS + ", " + - "-1 AS " + Combined.DATE_LAST_VISITED + "," + - qualifyColumn(TABLE_BOOKMARKS, Bookmarks.FAVICON_ID) + " AS " + Combined.FAVICON_ID + "," + - "0 AS " + Combined.LOCAL_DATE_LAST_VISITED + ", " + - "0 AS " + Combined.REMOTE_DATE_LAST_VISITED + ", " + - "0 AS " + Combined.LOCAL_VISITS_COUNT + ", " + - "0 AS " + Combined.REMOTE_VISITS_COUNT + - " FROM " + TABLE_BOOKMARKS + - " WHERE " + - qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TYPE) + " = " + Bookmarks.TYPE_BOOKMARK + " AND " + - // Ignore pinned bookmarks. - qualifyColumn(TABLE_BOOKMARKS, Bookmarks.PARENT) + " <> " + Bookmarks.FIXED_PINNED_LIST_ID + " AND " + - qualifyColumn(TABLE_BOOKMARKS, Bookmarks.IS_DELETED) + " = 0 AND " + - qualifyColumn(TABLE_BOOKMARKS, Bookmarks.URL) + - " NOT IN (SELECT " + History.URL + " FROM " + TABLE_HISTORY + ")" + - " UNION ALL" + - - // History with and without bookmark. - " SELECT " + - "CASE " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.IS_DELETED) + - - // Give pinned bookmarks a NULL ID so that they're not treated as bookmarks. We can't - // completely ignore them here because they're joined with history entries we care about. - " WHEN 0 THEN " + - "CASE " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.PARENT) + - " WHEN " + Bookmarks.FIXED_PINNED_LIST_ID + " THEN " + - "NULL " + - "ELSE " + - qualifyColumn(TABLE_BOOKMARKS, Bookmarks._ID) + - " END " + - "ELSE " + - "NULL " + - "END AS " + Combined.BOOKMARK_ID + "," + - qualifyColumn(TABLE_HISTORY, History._ID) + " AS " + Combined.HISTORY_ID + "," + - "0 AS " + Combined._ID + "," + - qualifyColumn(TABLE_HISTORY, History.URL) + " AS " + Combined.URL + "," + - - // Prioritize bookmark titles over history titles, since the user may have - // customized the title for a bookmark. - "COALESCE(" + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TITLE) + ", " + - qualifyColumn(TABLE_HISTORY, History.TITLE) + - ") AS " + Combined.TITLE + "," + - qualifyColumn(TABLE_HISTORY, History.VISITS) + " AS " + Combined.VISITS + "," + - qualifyColumn(TABLE_HISTORY, History.DATE_LAST_VISITED) + " AS " + Combined.DATE_LAST_VISITED + "," + - qualifyColumn(TABLE_HISTORY, History.FAVICON_ID) + " AS " + Combined.FAVICON_ID + "," + - - qualifyColumn(TABLE_HISTORY, History.LOCAL_DATE_LAST_VISITED) + " AS " + Combined.LOCAL_DATE_LAST_VISITED + "," + - qualifyColumn(TABLE_HISTORY, History.REMOTE_DATE_LAST_VISITED) + " AS " + Combined.REMOTE_DATE_LAST_VISITED + "," + - qualifyColumn(TABLE_HISTORY, History.LOCAL_VISITS) + " AS " + Combined.LOCAL_VISITS_COUNT + "," + - qualifyColumn(TABLE_HISTORY, History.REMOTE_VISITS) + " AS " + Combined.REMOTE_VISITS_COUNT + - - // We need to JOIN on Visits in order to compute visit counts - " FROM " + TABLE_HISTORY + " " + - - // We really shouldn't be selecting deleted bookmarks, but oh well. - "LEFT OUTER JOIN " + TABLE_BOOKMARKS + - " ON " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.URL) + " = " + qualifyColumn(TABLE_HISTORY, History.URL) + - " WHERE " + - qualifyColumn(TABLE_HISTORY, History.IS_DELETED) + " = 0 AND " + - "(" + - // The left outer join didn't match... - qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TYPE) + " IS NULL OR " + - - // ... or it's a bookmark. This is less efficient than filtering prior - // to the join if you have lots of folders. - qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TYPE) + " = " + Bookmarks.TYPE_BOOKMARK + ")" - ); - - debug("Creating " + VIEW_COMBINED_WITH_FAVICONS + " view"); - - db.execSQL("CREATE VIEW IF NOT EXISTS " + VIEW_COMBINED_WITH_FAVICONS + " AS" + - " SELECT " + qualifyColumn(VIEW_COMBINED, "*") + ", " + - qualifyColumn(TABLE_FAVICONS, Favicons.URL) + " AS " + Combined.FAVICON_URL + ", " + - qualifyColumn(TABLE_FAVICONS, Favicons.DATA) + " AS " + Combined.FAVICON + - " FROM " + VIEW_COMBINED + " LEFT OUTER JOIN " + TABLE_FAVICONS + - " ON " + Combined.FAVICON_ID + " = " + qualifyColumn(TABLE_FAVICONS, Favicons._ID)); - } - - private void createLoginsTable(SQLiteDatabase db, final String tableName) { - debug("Creating logins.db: " + db.getPath()); - debug("Creating " + tableName + " table"); - - // Table for each login. - db.execSQL("CREATE TABLE " + tableName + "(" + - BrowserContract.Logins._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," + - BrowserContract.Logins.HOSTNAME + " TEXT NOT NULL," + - BrowserContract.Logins.HTTP_REALM + " TEXT," + - BrowserContract.Logins.FORM_SUBMIT_URL + " TEXT," + - BrowserContract.Logins.USERNAME_FIELD + " TEXT NOT NULL," + - BrowserContract.Logins.PASSWORD_FIELD + " TEXT NOT NULL," + - BrowserContract.Logins.ENCRYPTED_USERNAME + " TEXT NOT NULL," + - BrowserContract.Logins.ENCRYPTED_PASSWORD + " TEXT NOT NULL," + - BrowserContract.Logins.GUID + " TEXT UNIQUE NOT NULL," + - BrowserContract.Logins.ENC_TYPE + " INTEGER NOT NULL, " + - BrowserContract.Logins.TIME_CREATED + " INTEGER," + - BrowserContract.Logins.TIME_LAST_USED + " INTEGER," + - BrowserContract.Logins.TIME_PASSWORD_CHANGED + " INTEGER," + - BrowserContract.Logins.TIMES_USED + " INTEGER" + - ");"); - } - - private void createLoginsTableIndices(SQLiteDatabase db, final String tableName) { - // No need to create an index on GUID, it is an unique column. - db.execSQL("CREATE INDEX " + LoginsProvider.INDEX_LOGINS_HOSTNAME + - " ON " + tableName + "(" + BrowserContract.Logins.HOSTNAME + ")"); - db.execSQL("CREATE INDEX " + LoginsProvider.INDEX_LOGINS_HOSTNAME_FORM_SUBMIT_URL + - " ON " + tableName + "(" + BrowserContract.Logins.HOSTNAME + "," + BrowserContract.Logins.FORM_SUBMIT_URL + ")"); - db.execSQL("CREATE INDEX " + LoginsProvider.INDEX_LOGINS_HOSTNAME_HTTP_REALM + - " ON " + tableName + "(" + BrowserContract.Logins.HOSTNAME + "," + BrowserContract.Logins.HTTP_REALM + ")"); - } - - private void createDeletedLoginsTable(SQLiteDatabase db, final String tableName) { - debug("Creating deleted_logins.db: " + db.getPath()); - debug("Creating " + tableName + " table"); - - // Table for each deleted login. - db.execSQL("CREATE TABLE " + tableName + "(" + - BrowserContract.DeletedLogins._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," + - BrowserContract.DeletedLogins.GUID + " TEXT UNIQUE NOT NULL," + - BrowserContract.DeletedLogins.TIME_DELETED + " INTEGER NOT NULL" + - ");"); - } - - private void createDisabledHostsTable(SQLiteDatabase db, final String tableName) { - debug("Creating disabled_hosts.db: " + db.getPath()); - debug("Creating " + tableName + " table"); - - // Table for each disabled host. - db.execSQL("CREATE TABLE " + tableName + "(" + - BrowserContract.LoginsDisabledHosts._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," + - BrowserContract.LoginsDisabledHosts.HOSTNAME + " TEXT UNIQUE NOT NULL ON CONFLICT REPLACE" + - ");"); - } - - @Override - public void onCreate(SQLiteDatabase db) { - debug("Creating browser.db: " + db.getPath()); - - for (Table table : BrowserProvider.sTables) { - table.onCreate(db); - } - - createBookmarksTable(db); - createHistoryTable(db); - createFaviconsTable(db); - createThumbnailsTable(db); - createClientsTable(db); - createLocalClient(db); - createTabsTable(db, TABLE_TABS); - createTabsTableIndices(db, TABLE_TABS); - - - createBookmarksWithFaviconsView(db); - createHistoryWithFaviconsView(db); - - createOrUpdateSpecialFolder(db, Bookmarks.PLACES_FOLDER_GUID, - R.string.bookmarks_folder_places, 0); - - createOrUpdateAllSpecialFolders(db); - createSearchHistoryTable(db); - createUrlAnnotationsTable(db); - createNumbersTable(db); - - createDeletedLoginsTable(db, TABLE_DELETED_LOGINS); - createDisabledHostsTable(db, TABLE_DISABLED_HOSTS); - createLoginsTable(db, TABLE_LOGINS); - createLoginsTableIndices(db, TABLE_LOGINS); - - createBookmarksWithAnnotationsView(db); - - createVisitsTable(db); - createCombinedViewOn34(db); - - createActivityStreamBlocklistTable(db); - - createPageMetadataTable(db); - } - - /** - * Copies the tabs and clients tables out of the given tabs.db file and into the destinationDB. - * - * @param tabsDBFile Path to existing tabs.db. - * @param destinationDB The destination database. - */ - public void copyTabsDB(File tabsDBFile, SQLiteDatabase destinationDB) { - createClientsTable(destinationDB); - createTabsTable(destinationDB, TABLE_TABS); - createTabsTableIndices(destinationDB, TABLE_TABS); - - SQLiteDatabase oldTabsDB = null; - try { - oldTabsDB = SQLiteDatabase.openDatabase(tabsDBFile.getPath(), null, SQLiteDatabase.OPEN_READONLY); - - if (!DBUtils.copyTable(oldTabsDB, TABLE_CLIENTS, destinationDB, TABLE_CLIENTS)) { - Log.e(LOGTAG, "Failed to migrate table clients; ignoring."); - } - if (!DBUtils.copyTable(oldTabsDB, TABLE_TABS, destinationDB, TABLE_TABS)) { - Log.e(LOGTAG, "Failed to migrate table tabs; ignoring."); - } - } catch (Exception e) { - Log.e(LOGTAG, "Exception occurred while trying to copy from " + tabsDBFile.getPath() + - " to " + destinationDB.getPath() + "; ignoring.", e); - } finally { - if (oldTabsDB != null) { - oldTabsDB.close(); - } - } - } - - /** - * We used to have a separate history extensions database which was used by Sync to store arrays - * of visits for individual History GUIDs. It was only used by Sync. - * This function migrates contents of that database over to the Visits table. - * - * Warning to callers: this method might throw IllegalStateException if we fail to allocate a - * cursor to read HistoryExtensionsDB data for whatever reason. See Bug 1280409. - * - * @param historyExtensionDb Source History Extensions database - * @param db Destination database - */ - private void copyHistoryExtensionDataToVisitsTable(final SQLiteDatabase historyExtensionDb, final SQLiteDatabase db) { - final String historyExtensionTable = "HistoryExtension"; - final String columnGuid = "guid"; - final String columnVisits = "visits"; - - final Cursor historyExtensionCursor = historyExtensionDb.query(historyExtensionTable, - new String[] {columnGuid, columnVisits}, - null, null, null, null, null); - // Ignore null or empty cursor, we can't (or have nothing to) copy at this point. - if (historyExtensionCursor == null) { - return; - } - try { - if (!historyExtensionCursor.moveToFirst()) { - return; - } - - final int guidCol = historyExtensionCursor.getColumnIndexOrThrow(columnGuid); - - // Use prepared (aka "compiled") SQL statements because they are much faster when we're inserting - // lots of data. We avoid GC churn and recompilation of SQL statements on every insert. - // NB #1: OR IGNORE clause applies to UNIQUE, NOT NULL, CHECK, and PRIMARY KEY constraints. - // It does not apply to Foreign Key constraints, but in our case, at this point in time, foreign key - // constraints are disabled anyway. - // We care about OR IGNORE because we want to ensure that in case of (GUID,DATE) - // clash (the UNIQUE constraint), we will not fail the transaction, and just skip conflicting row. - // Clash might occur if visits array we got from Sync has duplicate (guid,date) records. - // NB #2: IS_LOCAL is always 0, since we consider all visits coming from Sync to be remote. - final String insertSqlStatement = "INSERT OR IGNORE INTO " + Visits.TABLE_NAME + " (" + - Visits.DATE_VISITED + "," + - Visits.VISIT_TYPE + "," + - Visits.HISTORY_GUID + "," + - Visits.IS_LOCAL + ") VALUES (?, ?, ?, " + Visits.VISIT_IS_REMOTE + ")"; - final SQLiteStatement compiledInsertStatement = db.compileStatement(insertSqlStatement); - - do { - final String guid = historyExtensionCursor.getString(guidCol); - - // Sanity check, let's not risk a bad incoming GUID. - if (guid == null || guid.isEmpty()) { - continue; - } - - // First, check if history with given GUID exists in the History table. - // We might have a lot of entries in the HistoryExtensionDatabase whose GUID doesn't - // match one in the History table. Let's avoid doing unnecessary work by first checking if - // GUID exists locally. - // Note that we don't have foreign key constraints enabled at this point. - // See Bug 1266232 for details. - if (!isGUIDPresentInHistoryTable(db, guid)) { - continue; - } - - final JSONArray visitsInHistoryExtensionDB = RepoUtils.getJSONArrayFromCursor(historyExtensionCursor, columnVisits); - - if (visitsInHistoryExtensionDB == null) { - continue; - } - - final int histExtVisitCount = visitsInHistoryExtensionDB.size(); - - debug("Inserting " + histExtVisitCount + " visits from history extension db for GUID: " + guid); - for (int i = 0; i < histExtVisitCount; i++) { - final JSONObject visit = (JSONObject) visitsInHistoryExtensionDB.get(i); - - // Sanity check. - if (visit == null) { - continue; - } - - // Let's not rely on underlying data being correct, and guard against casting failures. - // Since we can't recover from this (other than ignoring this visit), let's not fail user's migration. - final Long date; - final Long visitType; - try { - date = (Long) visit.get("date"); - visitType = (Long) visit.get("type"); - } catch (ClassCastException e) { - continue; - } - // Sanity check our incoming data. - if (date == null || visitType == null) { - continue; - } - - // Bind parameters use a 1-based index. - compiledInsertStatement.clearBindings(); - compiledInsertStatement.bindLong(1, date); - compiledInsertStatement.bindLong(2, visitType); - compiledInsertStatement.bindString(3, guid); - compiledInsertStatement.executeInsert(); - } - } while (historyExtensionCursor.moveToNext()); - } finally { - // We return on a null cursor, so don't have to check it here. - historyExtensionCursor.close(); - } - } - - private boolean isGUIDPresentInHistoryTable(final SQLiteDatabase db, String guid) { - final Cursor historyCursor = db.query( - History.TABLE_NAME, - new String[] {History.GUID}, History.GUID + " = ?", new String[] {guid}, - null, null, null); - if (historyCursor == null) { - return false; - } - try { - // No history record found for given GUID - if (!historyCursor.moveToFirst()) { - return false; - } - } finally { - historyCursor.close(); - } - - return true; - } - - private void createSearchHistoryTable(SQLiteDatabase db) { - debug("Creating " + SearchHistory.TABLE_NAME + " table"); - - db.execSQL("CREATE TABLE " + SearchHistory.TABLE_NAME + "(" + - SearchHistory._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " + - SearchHistory.QUERY + " TEXT UNIQUE NOT NULL, " + - SearchHistory.DATE_LAST_VISITED + " INTEGER, " + - SearchHistory.VISITS + " INTEGER ) "); - - db.execSQL("CREATE INDEX idx_search_history_last_visited ON " + - SearchHistory.TABLE_NAME + "(" + SearchHistory.DATE_LAST_VISITED + ")"); - } - - private void createActivityStreamBlocklistTable(final SQLiteDatabase db) { - debug("Creating " + ActivityStreamBlocklist.TABLE_NAME + " table"); - - db.execSQL("CREATE TABLE " + ActivityStreamBlocklist.TABLE_NAME + "(" + - ActivityStreamBlocklist._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " + - ActivityStreamBlocklist.URL + " TEXT UNIQUE NOT NULL, " + - ActivityStreamBlocklist.CREATED + " INTEGER NOT NULL)"); - } - - private void createReadingListTable(final SQLiteDatabase db, final String tableName) { - debug("Creating " + TABLE_READING_LIST + " table"); - - db.execSQL("CREATE TABLE " + tableName + "(" + - ReadingListItems._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " + - ReadingListItems.GUID + " TEXT UNIQUE, " + // Server-assigned. - - ReadingListItems.CONTENT_STATUS + " TINYINT NOT NULL DEFAULT " + ReadingListItems.STATUS_UNFETCHED + ", " + - ReadingListItems.SYNC_STATUS + " TINYINT NOT NULL DEFAULT " + ReadingListItems.SYNC_STATUS_NEW + ", " + - ReadingListItems.SYNC_CHANGE_FLAGS + " TINYINT NOT NULL DEFAULT " + ReadingListItems.SYNC_CHANGE_NONE + ", " + - - ReadingListItems.CLIENT_LAST_MODIFIED + " INTEGER NOT NULL, " + // Client time. - ReadingListItems.SERVER_LAST_MODIFIED + " INTEGER, " + // Server-assigned. - - // Server-assigned. - ReadingListItems.SERVER_STORED_ON + " INTEGER, " + - ReadingListItems.ADDED_ON + " INTEGER, " + // Client time. Shouldn't be null, but not enforced. Formerly DATE_CREATED. - ReadingListItems.MARKED_READ_ON + " INTEGER, " + - - // These boolean flags represent the server 'status', 'unread', 'is_article', and 'favorite' fields. - ReadingListItems.IS_DELETED + " TINYINT NOT NULL DEFAULT 0, " + - ReadingListItems.IS_ARCHIVED + " TINYINT NOT NULL DEFAULT 0, " + - ReadingListItems.IS_UNREAD + " TINYINT NOT NULL DEFAULT 1, " + - ReadingListItems.IS_ARTICLE + " TINYINT NOT NULL DEFAULT 0, " + - ReadingListItems.IS_FAVORITE + " TINYINT NOT NULL DEFAULT 0, " + - - ReadingListItems.URL + " TEXT NOT NULL, " + - ReadingListItems.TITLE + " TEXT, " + - ReadingListItems.RESOLVED_URL + " TEXT, " + - ReadingListItems.RESOLVED_TITLE + " TEXT, " + - - ReadingListItems.EXCERPT + " TEXT, " + - - ReadingListItems.ADDED_BY + " TEXT, " + - ReadingListItems.MARKED_READ_BY + " TEXT, " + - - ReadingListItems.WORD_COUNT + " INTEGER DEFAULT 0, " + - ReadingListItems.READ_POSITION + " INTEGER DEFAULT 0 " + - "); "); - - didCreateCurrentReadingListTable = true; // Mostly correct, in the absence of transactions. - } - - private void createReadingListIndices(final SQLiteDatabase db, final String tableName) { - // No need to create an index on GUID; it's a UNIQUE column. - db.execSQL("CREATE INDEX reading_list_url ON " + tableName + "(" - + ReadingListItems.URL + ")"); - db.execSQL("CREATE INDEX reading_list_content_status ON " + tableName + "(" - + ReadingListItems.CONTENT_STATUS + ")"); - } - - private void createUrlAnnotationsTable(final SQLiteDatabase db) { - debug("Creating " + UrlAnnotations.TABLE_NAME + " table"); - - db.execSQL("CREATE TABLE " + UrlAnnotations.TABLE_NAME + "(" + - UrlAnnotations._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " + - UrlAnnotations.URL + " TEXT NOT NULL, " + - UrlAnnotations.KEY + " TEXT NOT NULL, " + - UrlAnnotations.VALUE + " TEXT, " + - UrlAnnotations.DATE_CREATED + " INTEGER NOT NULL, " + - UrlAnnotations.DATE_MODIFIED + " INTEGER NOT NULL, " + - UrlAnnotations.SYNC_STATUS + " TINYINT NOT NULL DEFAULT " + UrlAnnotations.SyncStatus.NEW.getDBValue() + - " );"); - - db.execSQL("CREATE INDEX idx_url_annotations_url_key ON " + - UrlAnnotations.TABLE_NAME + "(" + UrlAnnotations.URL + ", " + UrlAnnotations.KEY + ")"); - } - - private void createOrUpdateAllSpecialFolders(SQLiteDatabase db) { - createOrUpdateSpecialFolder(db, Bookmarks.MOBILE_FOLDER_GUID, - R.string.bookmarks_folder_mobile, 0); - createOrUpdateSpecialFolder(db, Bookmarks.TOOLBAR_FOLDER_GUID, - R.string.bookmarks_folder_toolbar, 1); - createOrUpdateSpecialFolder(db, Bookmarks.MENU_FOLDER_GUID, - R.string.bookmarks_folder_menu, 2); - createOrUpdateSpecialFolder(db, Bookmarks.TAGS_FOLDER_GUID, - R.string.bookmarks_folder_tags, 3); - createOrUpdateSpecialFolder(db, Bookmarks.UNFILED_FOLDER_GUID, - R.string.bookmarks_folder_unfiled, 4); - createOrUpdateSpecialFolder(db, Bookmarks.PINNED_FOLDER_GUID, - R.string.bookmarks_folder_pinned, 5); - } - - private void createOrUpdateSpecialFolder(SQLiteDatabase db, - String guid, int titleId, int position) { - ContentValues values = new ContentValues(); - values.put(Bookmarks.GUID, guid); - values.put(Bookmarks.TYPE, Bookmarks.TYPE_FOLDER); - values.put(Bookmarks.POSITION, position); - - if (guid.equals(Bookmarks.PLACES_FOLDER_GUID)) { - values.put(Bookmarks._ID, Bookmarks.FIXED_ROOT_ID); - } else if (guid.equals(Bookmarks.PINNED_FOLDER_GUID)) { - values.put(Bookmarks._ID, Bookmarks.FIXED_PINNED_LIST_ID); - } - - // Set the parent to 0, which sync assumes is the root - values.put(Bookmarks.PARENT, Bookmarks.FIXED_ROOT_ID); - - String title = mContext.getResources().getString(titleId); - values.put(Bookmarks.TITLE, title); - - long now = System.currentTimeMillis(); - values.put(Bookmarks.DATE_CREATED, now); - values.put(Bookmarks.DATE_MODIFIED, now); - - int updated = db.update(TABLE_BOOKMARKS, values, - Bookmarks.GUID + " = ?", - new String[] { guid }); - - if (updated == 0) { - db.insert(TABLE_BOOKMARKS, Bookmarks.GUID, values); - debug("Inserted special folder: " + guid); - } else { - debug("Updated special folder: " + guid); - } - } - - private void createNumbersTable(SQLiteDatabase db) { - db.execSQL("CREATE TABLE " + Numbers.TABLE_NAME + " (" + Numbers.POSITION + " INTEGER PRIMARY KEY AUTOINCREMENT)"); - - if (db.getVersion() >= 3007011) { // SQLite 3.7.11 - // This is only available in SQLite >= 3.7.11, see release notes: - // "Enhance the INSERT syntax to allow multiple rows to be inserted via the VALUES clause" - final String numbers = "(0),(1),(2),(3),(4),(5),(6),(7),(8),(9)," + - "(10),(11),(12),(13),(14),(15),(16),(17),(18),(19)," + - "(20),(21),(22),(23),(24),(25),(26),(27),(28),(29)," + - "(30),(31),(32),(33),(34),(35),(36),(37),(38),(39)," + - "(40),(41),(42),(43),(44),(45),(46),(47),(48),(49)," + - "(50)"; - - db.execSQL("INSERT INTO " + Numbers.TABLE_NAME + " (" + Numbers.POSITION + ") VALUES " + numbers); - } else { - final SQLiteStatement statement = db.compileStatement("INSERT INTO " + Numbers.TABLE_NAME + " (" + Numbers.POSITION + ") VALUES (?)"); - - for (int i = 0; i <= Numbers.MAX_VALUE; i++) { - statement.bindLong(1, i); - statement.executeInsert(); - } - } - } - - private boolean isSpecialFolder(ContentValues values) { - String guid = values.getAsString(Bookmarks.GUID); - if (guid == null) { - return false; - } - - return guid.equals(Bookmarks.MOBILE_FOLDER_GUID) || - guid.equals(Bookmarks.MENU_FOLDER_GUID) || - guid.equals(Bookmarks.TOOLBAR_FOLDER_GUID) || - guid.equals(Bookmarks.UNFILED_FOLDER_GUID) || - guid.equals(Bookmarks.TAGS_FOLDER_GUID); - } - - private void migrateBookmarkFolder(SQLiteDatabase db, int folderId, - BookmarkMigrator migrator) { - Cursor c = null; - - debug("Migrating bookmark folder with id = " + folderId); - - String selection = Bookmarks.PARENT + " = " + folderId; - String[] selectionArgs = null; - - boolean isRootFolder = (folderId == Bookmarks.FIXED_ROOT_ID); - - // If we're loading the root folder, we have to account for - // any previously created special folder that was created without - // setting a parent id (e.g. mobile folder) and making sure we're - // not adding any infinite recursion as root's parent is root itself. - if (isRootFolder) { - selection = Bookmarks.GUID + " != ?" + " AND (" + - selection + " OR " + Bookmarks.PARENT + " = NULL)"; - selectionArgs = new String[] { Bookmarks.PLACES_FOLDER_GUID }; - } - - List subFolders = new ArrayList(); - List invalidSpecialEntries = new ArrayList(); - - try { - c = db.query(TABLE_BOOKMARKS_TMP, - null, - selection, - selectionArgs, - null, null, null); - - // The key point here is that bookmarks should be added in - // parent order to avoid any problems with the foreign key - // in Bookmarks.PARENT. - while (c.moveToNext()) { - ContentValues values = new ContentValues(); - - // We're using a null projection in the query which - // means we're getting all columns from the table. - // It's safe to simply transform the row into the - // values to be inserted on the new table. - DatabaseUtils.cursorRowToContentValues(c, values); - - boolean isSpecialFolder = isSpecialFolder(values); - - // The mobile folder used to be created with PARENT = NULL. - // We want fix that here. - if (values.getAsLong(Bookmarks.PARENT) == null && isSpecialFolder) - values.put(Bookmarks.PARENT, Bookmarks.FIXED_ROOT_ID); - - if (isRootFolder && !isSpecialFolder) { - invalidSpecialEntries.add(values); - continue; - } - - if (migrator != null) - migrator.updateForNewTable(values); - - debug("Migrating bookmark: " + values.getAsString(Bookmarks.TITLE)); - db.insert(TABLE_BOOKMARKS, Bookmarks.URL, values); - - Integer type = values.getAsInteger(Bookmarks.TYPE); - if (type != null && type == Bookmarks.TYPE_FOLDER) - subFolders.add(values.getAsInteger(Bookmarks._ID)); - } - } finally { - if (c != null) - c.close(); - } - - // At this point is safe to assume that the mobile folder is - // in the new table given that we've always created it on - // database creation time. - final int nInvalidSpecialEntries = invalidSpecialEntries.size(); - if (nInvalidSpecialEntries > 0) { - Integer mobileFolderId = getMobileFolderId(db); - if (mobileFolderId == null) { - Log.e(LOGTAG, "Error migrating invalid special folder entries: mobile folder id is null"); - return; - } - - debug("Found " + nInvalidSpecialEntries + " invalid special folder entries"); - for (int i = 0; i < nInvalidSpecialEntries; i++) { - ContentValues values = invalidSpecialEntries.get(i); - values.put(Bookmarks.PARENT, mobileFolderId); - - db.insert(TABLE_BOOKMARKS, Bookmarks.URL, values); - } - } - - final int nSubFolders = subFolders.size(); - for (int i = 0; i < nSubFolders; i++) { - int subFolderId = subFolders.get(i); - migrateBookmarkFolder(db, subFolderId, migrator); - } - } - - private void migrateBookmarksTable(SQLiteDatabase db) { - migrateBookmarksTable(db, null); - } - - private void migrateBookmarksTable(SQLiteDatabase db, BookmarkMigrator migrator) { - debug("Renaming bookmarks table to " + TABLE_BOOKMARKS_TMP); - db.execSQL("ALTER TABLE " + TABLE_BOOKMARKS + - " RENAME TO " + TABLE_BOOKMARKS_TMP); - - debug("Dropping views and indexes related to " + TABLE_BOOKMARKS); - - db.execSQL("DROP INDEX IF EXISTS bookmarks_url_index"); - db.execSQL("DROP INDEX IF EXISTS bookmarks_type_deleted_index"); - db.execSQL("DROP INDEX IF EXISTS bookmarks_guid_index"); - db.execSQL("DROP INDEX IF EXISTS bookmarks_modified_index"); - - createBookmarksTable(db); - - createOrUpdateSpecialFolder(db, Bookmarks.PLACES_FOLDER_GUID, - R.string.bookmarks_folder_places, 0); - - migrateBookmarkFolder(db, Bookmarks.FIXED_ROOT_ID, migrator); - - // Ensure all special folders exist and have the - // right folder hierarchy. - createOrUpdateAllSpecialFolders(db); - - debug("Dropping bookmarks temporary table"); - db.execSQL("DROP TABLE IF EXISTS " + TABLE_BOOKMARKS_TMP); - } - - /** - * Migrate a history table from some old version to the newest one by creating the new table and - * copying all the data over. - */ - private void migrateHistoryTable(SQLiteDatabase db) { - debug("Renaming history table to " + TABLE_HISTORY_TMP); - db.execSQL("ALTER TABLE " + TABLE_HISTORY + - " RENAME TO " + TABLE_HISTORY_TMP); - - debug("Dropping views and indexes related to " + TABLE_HISTORY); - - db.execSQL("DROP INDEX IF EXISTS history_url_index"); - db.execSQL("DROP INDEX IF EXISTS history_guid_index"); - db.execSQL("DROP INDEX IF EXISTS history_modified_index"); - db.execSQL("DROP INDEX IF EXISTS history_visited_index"); - - createHistoryTable(db); - - db.execSQL("INSERT INTO " + TABLE_HISTORY + " SELECT * FROM " + TABLE_HISTORY_TMP); - - debug("Dropping history temporary table"); - db.execSQL("DROP TABLE IF EXISTS " + TABLE_HISTORY_TMP); - } - - private void upgradeDatabaseFrom3to4(SQLiteDatabase db) { - migrateBookmarksTable(db, new BookmarkMigrator3to4()); - } - - private void upgradeDatabaseFrom6to7(SQLiteDatabase db) { - debug("Removing history visits with NULL GUIDs"); - db.execSQL("DELETE FROM " + TABLE_HISTORY + " WHERE " + History.GUID + " IS NULL"); - - migrateBookmarksTable(db); - migrateHistoryTable(db); - } - - private void upgradeDatabaseFrom7to8(SQLiteDatabase db) { - debug("Combining history entries with the same URL"); - - final String TABLE_DUPES = "duped_urls"; - final String TOTAL = "total"; - final String LATEST = "latest"; - final String WINNER = "winner"; - - db.execSQL("CREATE TEMP TABLE " + TABLE_DUPES + " AS" + - " SELECT " + History.URL + ", " + - "SUM(" + History.VISITS + ") AS " + TOTAL + ", " + - "MAX(" + History.DATE_MODIFIED + ") AS " + LATEST + ", " + - "MAX(" + History._ID + ") AS " + WINNER + - " FROM " + TABLE_HISTORY + - " GROUP BY " + History.URL + - " HAVING count(" + History.URL + ") > 1"); - - db.execSQL("CREATE UNIQUE INDEX " + TABLE_DUPES + "_url_index ON " + - TABLE_DUPES + " (" + History.URL + ")"); - - final String fromClause = " FROM " + TABLE_DUPES + " WHERE " + - qualifyColumn(TABLE_DUPES, History.URL) + " = " + - qualifyColumn(TABLE_HISTORY, History.URL); - - db.execSQL("UPDATE " + TABLE_HISTORY + - " SET " + History.VISITS + " = (SELECT " + TOTAL + fromClause + "), " + - History.DATE_MODIFIED + " = (SELECT " + LATEST + fromClause + "), " + - History.IS_DELETED + " = " + - "(" + History._ID + " <> (SELECT " + WINNER + fromClause + "))" + - " WHERE " + History.URL + " IN (SELECT " + History.URL + " FROM " + TABLE_DUPES + ")"); - - db.execSQL("DROP TABLE " + TABLE_DUPES); - } - - private void upgradeDatabaseFrom10to11(SQLiteDatabase db) { - db.execSQL("CREATE INDEX bookmarks_type_deleted_index ON " + TABLE_BOOKMARKS + "(" - + Bookmarks.TYPE + ", " + Bookmarks.IS_DELETED + ")"); - } - - private void upgradeDatabaseFrom12to13(SQLiteDatabase db) { - createFaviconsTable(db); - - // Add favicon_id column to the history/bookmarks tables. We wrap this in a try-catch - // because the column *may* already exist at this point (depending on how many upgrade - // steps have been performed in this operation). In which case these queries will throw, - // but we don't care. - try { - db.execSQL("ALTER TABLE " + TABLE_HISTORY + - " ADD COLUMN " + History.FAVICON_ID + " INTEGER"); - db.execSQL("ALTER TABLE " + TABLE_BOOKMARKS + - " ADD COLUMN " + Bookmarks.FAVICON_ID + " INTEGER"); - } catch (SQLException e) { - // Don't care. - debug("Exception adding favicon_id column. We're probably fine." + e); - } - - createThumbnailsTable(db); - - db.execSQL("DROP VIEW IF EXISTS bookmarks_with_images"); - db.execSQL("DROP VIEW IF EXISTS history_with_images"); - db.execSQL("DROP VIEW IF EXISTS combined_with_images"); - - createBookmarksWithFaviconsView(db); - createHistoryWithFaviconsView(db); - - db.execSQL("DROP TABLE IF EXISTS images"); - } - - private void upgradeDatabaseFrom13to14(SQLiteDatabase db) { - createOrUpdateSpecialFolder(db, Bookmarks.PINNED_FOLDER_GUID, - R.string.bookmarks_folder_pinned, 6); - } - - private void upgradeDatabaseFrom14to15(SQLiteDatabase db) { - Cursor c = null; - try { - // Get all the pinned bookmarks - c = db.query(TABLE_BOOKMARKS, - new String[] { Bookmarks._ID, Bookmarks.URL }, - Bookmarks.PARENT + " = ?", - new String[] { Integer.toString(Bookmarks.FIXED_PINNED_LIST_ID) }, - null, null, null); - - while (c.moveToNext()) { - // Check if this URL can be parsed as a URI with a valid scheme. - String url = c.getString(c.getColumnIndexOrThrow(Bookmarks.URL)); - if (Uri.parse(url).getScheme() != null) { - continue; - } - - // If it can't, update the URL to be an encoded "user-entered" value. - ContentValues values = new ContentValues(1); - String newUrl = Uri.fromParts("user-entered", url, null).toString(); - values.put(Bookmarks.URL, newUrl); - db.update(TABLE_BOOKMARKS, values, Bookmarks._ID + " = ?", - new String[] { Integer.toString(c.getInt(c.getColumnIndexOrThrow(Bookmarks._ID))) }); - } - } finally { - if (c != null) { - c.close(); - } - } - } - - private void upgradeDatabaseFrom15to16(SQLiteDatabase db) { - // No harm in creating the v19 combined view here: means we don't need two almost-identical - // functions to define both the v16 and v19 ones. The upgrade path will redundantly drop - // and recreate the view again. *shrug* - createV19CombinedView(db); - } - - private void upgradeDatabaseFrom16to17(SQLiteDatabase db) { - // Purge any 0-byte favicons/thumbnails - try { - db.execSQL("DELETE FROM " + TABLE_FAVICONS + - " WHERE length(" + Favicons.DATA + ") = 0"); - db.execSQL("DELETE FROM " + TABLE_THUMBNAILS + - " WHERE length(" + Thumbnails.DATA + ") = 0"); - } catch (SQLException e) { - Log.e(LOGTAG, "Error purging invalid favicons or thumbnails", e); - } - } - - /* - * Moves reading list items from 'bookmarks' table to 'reading_list' table. - */ - private void upgradeDatabaseFrom17to18(SQLiteDatabase db) { - debug("Moving reading list items from 'bookmarks' table to 'reading_list' table"); - - final String selection = Bookmarks.PARENT + " = ? AND " + Bookmarks.IS_DELETED + " = ? "; - final String[] selectionArgs = { String.valueOf(Bookmarks.FIXED_READING_LIST_ID), "0" }; - final String[] projection = { Bookmarks._ID, - Bookmarks.GUID, - Bookmarks.URL, - Bookmarks.DATE_MODIFIED, - Bookmarks.DATE_CREATED, - Bookmarks.TITLE }; - - try { - db.beginTransaction(); - - // Create 'reading_list' table. - createReadingListTable(db, TABLE_READING_LIST); - - // Get all the reading list items from bookmarks table. - final Cursor cursor = db.query(TABLE_BOOKMARKS, projection, selection, selectionArgs, null, null, null); - - if (cursor == null) { - // This should never happen. - db.setTransactionSuccessful(); - return; - } - - try { - // Insert reading list items into reading_list table. - while (cursor.moveToNext()) { - debug(DatabaseUtils.dumpCurrentRowToString(cursor)); - final ContentValues values = new ContentValues(); - - // We don't preserve bookmark GUIDs. - DatabaseUtils.cursorStringToContentValues(cursor, Bookmarks.URL, values, ReadingListItems.URL); - DatabaseUtils.cursorStringToContentValues(cursor, Bookmarks.TITLE, values, ReadingListItems.TITLE); - DatabaseUtils.cursorLongToContentValues(cursor, Bookmarks.DATE_CREATED, values, ReadingListItems.ADDED_ON); - DatabaseUtils.cursorLongToContentValues(cursor, Bookmarks.DATE_MODIFIED, values, ReadingListItems.CLIENT_LAST_MODIFIED); - - db.insertOrThrow(TABLE_READING_LIST, null, values); - } - } finally { - cursor.close(); - } - - // Delete reading list items from bookmarks table. - db.delete(TABLE_BOOKMARKS, - Bookmarks.PARENT + " = ? ", - new String[] { String.valueOf(Bookmarks.FIXED_READING_LIST_ID) }); - - // Delete reading list special folder. - db.delete(TABLE_BOOKMARKS, - Bookmarks._ID + " = ? ", - new String[] { String.valueOf(Bookmarks.FIXED_READING_LIST_ID) }); - - // Create indices. - createReadingListIndices(db, TABLE_READING_LIST); - - // Done. - db.setTransactionSuccessful(); - } catch (SQLException e) { - Log.e(LOGTAG, "Error migrating reading list items", e); - } finally { - db.endTransaction(); - } - } - - private void upgradeDatabaseFrom18to19(SQLiteDatabase db) { - // Redefine the "combined" view... - createV19CombinedView(db); - - // Kill any history entries with NULL URL. This ostensibly can't happen... - db.execSQL("DELETE FROM " + TABLE_HISTORY + " WHERE " + History.URL + " IS NULL"); - - // Similar for bookmark types. Replaces logic from the combined view, also shouldn't happen. - db.execSQL("UPDATE " + TABLE_BOOKMARKS + " SET " + - Bookmarks.TYPE + " = " + Bookmarks.TYPE_BOOKMARK + - " WHERE " + Bookmarks.TYPE + " IS NULL"); - } - - private void upgradeDatabaseFrom19to20(SQLiteDatabase db) { - createSearchHistoryTable(db); - } - - private void upgradeDatabaseFrom21to22(SQLiteDatabase db) { - if (didCreateCurrentReadingListTable) { - debug("No need to add CONTENT_STATUS to reading list; we just created with the current schema."); - return; - } - - debug("Adding CONTENT_STATUS column to reading list table."); - - try { - db.execSQL("ALTER TABLE " + TABLE_READING_LIST + - " ADD COLUMN " + ReadingListItems.CONTENT_STATUS + - " TINYINT DEFAULT " + ReadingListItems.STATUS_UNFETCHED); - - db.execSQL("CREATE INDEX reading_list_content_status ON " + TABLE_READING_LIST + "(" - + ReadingListItems.CONTENT_STATUS + ")"); - } catch (SQLiteException e) { - // We're betting that an error here means that the table already has the column, - // so we're failing due to the duplicate column name. - Log.e(LOGTAG, "Error upgrading database from 21 to 22", e); - } - } - - private void upgradeDatabaseFrom22to23(SQLiteDatabase db) { - if (didCreateCurrentReadingListTable) { - // If we just created this table it is already in the expected >= 23 schema. Trying - // to run this migration will crash because columns that were in the <= 22 schema - // no longer exist. - debug("No need to rev reading list schema; we just created with the current schema."); - return; - } - - debug("Rewriting reading list table."); - createReadingListTable(db, "tmp_rl"); - - // Remove indexes. We don't need them now, and we'll be throwing away the table. - db.execSQL("DROP INDEX IF EXISTS reading_list_url"); - db.execSQL("DROP INDEX IF EXISTS reading_list_guid"); - db.execSQL("DROP INDEX IF EXISTS reading_list_content_status"); - - // This used to be a part of the no longer existing ReadingListProvider, since we're deleting - // this table later in the second migration, and since sync for this table never existed, - // we don't care about the device name here. - final String thisDevice = "_fake_device_name_that_will_be_discarded_in_the_next_migration_"; - db.execSQL("INSERT INTO tmp_rl (" + - // Here are the columns we can preserve. - ReadingListItems._ID + ", " + - ReadingListItems.URL + ", " + - ReadingListItems.TITLE + ", " + - ReadingListItems.RESOLVED_TITLE + ", " + // = TITLE (if CONTENT_STATUS = STATUS_FETCHED_ARTICLE) - ReadingListItems.RESOLVED_URL + ", " + // = URL (if CONTENT_STATUS = STATUS_FETCHED_ARTICLE) - ReadingListItems.EXCERPT + ", " + - ReadingListItems.IS_UNREAD + ", " + // = !READ - ReadingListItems.IS_DELETED + ", " + // = 0 - ReadingListItems.GUID + ", " + // = NULL - ReadingListItems.CLIENT_LAST_MODIFIED + ", " + // = DATE_MODIFIED - ReadingListItems.ADDED_ON + ", " + // = DATE_CREATED - ReadingListItems.CONTENT_STATUS + ", " + - ReadingListItems.MARKED_READ_BY + ", " + // if READ + ", = this device - ReadingListItems.ADDED_BY + // = this device - ") " + - "SELECT " + - "_id, url, title, " + - "CASE content_status WHEN " + ReadingListItems.STATUS_FETCHED_ARTICLE + " THEN title ELSE NULL END, " + // RESOLVED_TITLE. - "CASE content_status WHEN " + ReadingListItems.STATUS_FETCHED_ARTICLE + " THEN url ELSE NULL END, " + // RESOLVED_URL. - "excerpt, " + - "CASE read WHEN 1 THEN 0 ELSE 1 END, " + // IS_UNREAD. - "0, " + // IS_DELETED. - "NULL, modified, created, content_status, " + - "CASE read WHEN 1 THEN ? ELSE NULL END, " + // MARKED_READ_BY. - "?" + // ADDED_BY. - " FROM " + TABLE_READING_LIST + - " WHERE deleted = 0", - new String[] {thisDevice, thisDevice}); - - // Now switch these tables over and recreate the indices. - db.execSQL("DROP TABLE " + TABLE_READING_LIST); - db.execSQL("ALTER TABLE tmp_rl RENAME TO " + TABLE_READING_LIST); - - createReadingListIndices(db, TABLE_READING_LIST); - } - - private void upgradeDatabaseFrom23to24(SQLiteDatabase db) { - // Version 24 consolidates the tabs and clients table into browser.db. Before, they lived in tabs.db. - // It's easier to copy the existing data than to arrange for Sync to re-populate it. - try { - final File oldTabsDBFile = new File(GeckoProfile.get(mContext).getDir(), "tabs.db"); - copyTabsDB(oldTabsDBFile, db); - } catch (Exception e) { - Log.e(LOGTAG, "Got exception copying tabs and clients data from tabs.db to browser.db; ignoring.", e); - } - - // Delete the database, the shared memory, and the log. - for (String filename : new String[] { "tabs.db", "tabs.db-shm", "tabs.db-wal" }) { - final File file = new File(GeckoProfile.get(mContext).getDir(), filename); - try { - FileUtils.delete(file); - } catch (Exception e) { - Log.e(LOGTAG, "Exception occurred while trying to delete " + file.getPath() + "; ignoring.", e); - } - } - } - - private void upgradeDatabaseFrom24to25(SQLiteDatabase db) { - if (didCreateTabsTable) { - // This migration adds a foreign key constraint (the table scheme stays identical, except - // for the new constraint) - hence it is safe to run this migration on a newly created tabs - // table - but it's unnecessary hence we should avoid doing so. - debug("No need to rev tabs schema; foreign key constraint exists."); - return; - } - - debug("Rewriting tabs table."); - createTabsTable(db, "tmp_tabs"); - - // Remove indexes. We don't need them now, and we'll be throwing away the table. - db.execSQL("DROP INDEX IF EXISTS " + TabsProvider.INDEX_TABS_GUID); - db.execSQL("DROP INDEX IF EXISTS " + TabsProvider.INDEX_TABS_POSITION); - - db.execSQL("INSERT INTO tmp_tabs (" + - // Here are the columns we can preserve. - BrowserContract.Tabs._ID + ", " + - BrowserContract.Tabs.CLIENT_GUID + ", " + - BrowserContract.Tabs.TITLE + ", " + - BrowserContract.Tabs.URL + ", " + - BrowserContract.Tabs.HISTORY + ", " + - BrowserContract.Tabs.FAVICON + ", " + - BrowserContract.Tabs.LAST_USED + ", " + - BrowserContract.Tabs.POSITION + - ") " + - "SELECT " + - "_id, client_guid, title, url, history, favicon, last_used, position" + - " FROM " + TABLE_TABS); - - // Now switch these tables over and recreate the indices. - db.execSQL("DROP TABLE " + TABLE_TABS); - db.execSQL("ALTER TABLE tmp_tabs RENAME TO " + TABLE_TABS); - createTabsTableIndices(db, TABLE_TABS); - didCreateTabsTable = true; - } - - private void upgradeDatabaseFrom25to26(SQLiteDatabase db) { - debug("Dropping unnecessary indices"); - db.execSQL("DROP INDEX IF EXISTS clients_guid_index"); - db.execSQL("DROP INDEX IF EXISTS thumbnails_url_index"); - db.execSQL("DROP INDEX IF EXISTS favicons_url_index"); - } - - private void upgradeDatabaseFrom27to28(final SQLiteDatabase db) { - debug("Adding url annotations table"); - createUrlAnnotationsTable(db); - } - - private void upgradeDatabaseFrom28to29(SQLiteDatabase db) { - debug("Adding numbers table"); - createNumbersTable(db); - } - - private void upgradeDatabaseFrom29to30(final SQLiteDatabase db) { - debug("creating logins table"); - createDeletedLoginsTable(db, TABLE_DELETED_LOGINS); - createDisabledHostsTable(db, TABLE_DISABLED_HOSTS); - createLoginsTable(db, TABLE_LOGINS); - createLoginsTableIndices(db, TABLE_LOGINS); - } - - // Get the cache path for a URL, based on the storage format in place during the 27to28 transition. - // This is a reimplementation of _toHashedPath from ReaderMode.jsm - given that we're likely - // to migrate the SavedReaderViewHelper implementation at some point, it seems safest to have a local - // implementation here - moreover this is probably faster than calling into JS. - // This is public only to allow for testing. - @RobocopTarget - public static String getReaderCacheFileNameForURL(String url) { - try { - // On KitKat and above we can use java.nio.charset.StandardCharsets.UTF_8 in place of "UTF8" - // which avoids having to handle UnsupportedCodingException - byte[] utf8 = url.getBytes("UTF8"); - - final MessageDigest digester = MessageDigest.getInstance("MD5"); - byte[] hash = digester.digest(utf8); - - final String hashString = new Base32().encodeAsString(hash); - return hashString.substring(0, hashString.indexOf('=')) + ".json"; - } catch (UnsupportedEncodingException e) { - // This should never happen - throw new IllegalStateException("UTF8 encoding not available - can't process readercache filename"); - } catch (NoSuchAlgorithmException e) { - // This should also never happen - throw new IllegalStateException("MD5 digester unavailable - can't process readercache filename"); - } - } - - /* - * Moves reading list items from the 'reading_list' table back into the 'bookmarks' table. This time the - * reading list items are placed into a "Reading List" folder, which is a subfolder of the mobile-bookmarks table. - */ - private void upgradeDatabaseFrom30to31(SQLiteDatabase db) { - // We only need to do the migration if reading-list items already exist. We could do a query of count(*) on - // TABLE_READING_LIST, however if we are doing the migration, we'll need to query all items in the reading-list, - // hence we might as well just query all items, and proceed with the migration if cursor.count > 0. - - // We try to retain the original ordering below. Our LocalReadingListAccessor actually coalesced - // SERVER_STORED_ON with ADDED_ON to determine positioning, however reading list syncing was never - // implemented hence SERVER_STORED will have always been null. - final Cursor readingListCursor = db.query(TABLE_READING_LIST, - new String[] { - ReadingListItems.URL, - ReadingListItems.TITLE, - ReadingListItems.ADDED_ON, - ReadingListItems.CLIENT_LAST_MODIFIED - }, - ReadingListItems.IS_DELETED + " = 0", - null, - null, - null, - ReadingListItems.ADDED_ON + " DESC"); - - // We'll want to walk the cache directory, so that we can (A) bookkeep readercache items - // that we want and (B) delete unneeded readercache items. (B) shouldn't actually happen, but - // is possible if there were bugs in our reader-caching code. - // We need to construct this here since we populate this map while walking the DB cursor, - // and use the map later when walking the cache. - final Map fileToURLMap = new HashMap<>(); - - - try { - if (!readingListCursor.moveToFirst()) { - return; - } - - final Integer mobileBookmarksID = getMobileFolderId(db); - - if (mobileBookmarksID == null) { - // This folder is created either on DB creation or during the 3-4 or 6-7 migrations. - throw new IllegalStateException("mobile bookmarks folder must already exist"); - } - - final long now = System.currentTimeMillis(); - - // We try to retain the same order as the reading-list would show. We should hopefully be reading the - // items in the order they are displayed on screen (final param of db.query above), by providing - // a position we should obtain the same ordering in the bookmark folder. - long position = 0; - - final int titleColumnID = readingListCursor.getColumnIndexOrThrow(ReadingListItems.TITLE); - final int createdColumnID = readingListCursor.getColumnIndexOrThrow(ReadingListItems.ADDED_ON); - - // This isn't the most efficient implementation, but the migration is one-off, and this - // also more maintainable than the SQL equivalent (generating the guids correctly is - // difficult in SQLite). - do { - final ContentValues readingListItemValues = new ContentValues(); - - final String url = readingListCursor.getString(readingListCursor.getColumnIndexOrThrow(ReadingListItems.URL)); - - readingListItemValues.put(Bookmarks.PARENT, mobileBookmarksID); - readingListItemValues.put(Bookmarks.GUID, Utils.generateGuid()); - readingListItemValues.put(Bookmarks.URL, url); - // Title may be null, however we're expecting a String - we can generate an empty string if needed: - if (!readingListCursor.isNull(titleColumnID)) { - readingListItemValues.put(Bookmarks.TITLE, readingListCursor.getString(titleColumnID)); - } else { - readingListItemValues.put(Bookmarks.TITLE, ""); - } - readingListItemValues.put(Bookmarks.DATE_CREATED, readingListCursor.getLong(createdColumnID)); - readingListItemValues.put(Bookmarks.DATE_MODIFIED, now); - readingListItemValues.put(Bookmarks.POSITION, position); - - db.insert(TABLE_BOOKMARKS, - null, - readingListItemValues); - - final String cacheFileName = getReaderCacheFileNameForURL(url); - fileToURLMap.put(cacheFileName, url); - - position++; - } while (readingListCursor.moveToNext()); - - } finally { - readingListCursor.close(); - // We need to do this work here since we might be returning (we return early if the - // reading-list table is empty). - db.execSQL("DROP TABLE IF EXISTS " + TABLE_READING_LIST); - createBookmarksWithAnnotationsView(db); - } - - final File profileDir = GeckoProfile.get(mContext).getDir(); - final File cacheDir = new File(profileDir, "readercache"); - - // At the time of this migration the SavedReaderViewHelper becomes a 1:1 mirror of reader view - // url-annotations. This may change in future implementations, however currently we only need to care - // about standard bookmarks (untouched during this migration) and bookmarks with a reader - // view annotation (which we're creating here, and which are guaranteed to be saved offline). - // - // This is why we have to migrate the cache items (instead of cleaning the cache - // and rebuilding it). We simply don't support uncached reader view bookmarks, and we would - // break existing reading list items (they would convert into plain bookmarks without - // reader view). This helps ensure that offline content isn't lost during the migration. - if (cacheDir.exists() && cacheDir.isDirectory()) { - SavedReaderViewHelper savedReaderViewHelper = SavedReaderViewHelper.getSavedReaderViewHelper(mContext); - - // Usually we initialise the helper during onOpen(). However onUpgrade() is run before - // onOpen() hence we need to manually initialise it at this stage. - savedReaderViewHelper.loadItems(); - - for (File cacheFile : cacheDir.listFiles()) { - if (fileToURLMap.containsKey(cacheFile.getName())) { - final String url = fileToURLMap.get(cacheFile.getName()); - final String path = cacheFile.getAbsolutePath(); - long size = cacheFile.length(); - - savedReaderViewHelper.put(url, path, size); - } else { - // This should never happen, but we don't actually know whether or not orphaned - // items happened in the wild. - boolean deleted = cacheFile.delete(); - - if (!deleted) { - Log.w(LOGTAG, "Failed to delete orphaned saved reader view file."); - } - } - } - } - } - - private void upgradeDatabaseFrom31to32(final SQLiteDatabase db) { - debug("Adding visits table"); - createVisitsTable(db); - - debug("Migrating visits from history extension db into visits table"); - String historyExtensionDbName = "history_extension_database"; - - SQLiteDatabase historyExtensionDb = null; - final File historyExtensionsDatabase = mContext.getDatabasePath(historyExtensionDbName); - - // Primary goal of this migration is to improve Top Sites experience by distinguishing between - // local and remote visits. If Sync is enabled, we rely on visit data from Sync and treat it as remote. - // However, if Sync is disabled but we detect evidence that it was enabled at some point (HistoryExtensionsDB is present) - // then we synthesize visits from the History table, but we mark them all as "remote". This will ensure - // that once user starts browsing around, their Top Sites will reflect their local browsing history. - // Otherwise, we risk overwhelming their Top Sites with remote history, just as we did before this migration. - try { - // If FxAccount exists (Sync is enabled) then port data over to the Visits table. - if (FirefoxAccounts.firefoxAccountsExist(mContext)) { - try { - historyExtensionDb = SQLiteDatabase.openDatabase(historyExtensionsDatabase.getPath(), null, - SQLiteDatabase.OPEN_READONLY); - - if (historyExtensionDb != null) { - copyHistoryExtensionDataToVisitsTable(historyExtensionDb, db); - } - - // If we fail to open HistoryExtensionDatabase, then synthesize visits marking them as remote - } catch (SQLiteException e) { - Log.w(LOGTAG, "Couldn't open history extension database; synthesizing visits instead", e); - synthesizeAndInsertVisits(db, false); - - // It's possible that we might fail to copy over visit data from the HistoryExtensionsDB, - // so let's synthesize visits marking them as remote. See Bug 1280409. - } catch (IllegalStateException e) { - Log.w(LOGTAG, "Couldn't copy over history extension data; synthesizing visits instead", e); - synthesizeAndInsertVisits(db, false); - } - - // FxAccount doesn't exist, but there's evidence Sync was enabled at some point. - // Synthesize visits from History table marking them all as remote. - } else if (historyExtensionsDatabase.exists()) { - synthesizeAndInsertVisits(db, false); - - // FxAccount doesn't exist and there's no evidence sync was ever enabled. - // Synthesize visits from History table marking them all as local. - } else { - synthesizeAndInsertVisits(db, true); - } - } finally { - if (historyExtensionDb != null) { - historyExtensionDb.close(); - } - } - - // Delete history extensions database if it's present. - if (historyExtensionsDatabase.exists()) { - if (!mContext.deleteDatabase(historyExtensionDbName)) { - Log.e(LOGTAG, "Couldn't remove history extension database"); - } - } - } - - private void synthesizeAndInsertVisits(final SQLiteDatabase db, boolean markAsLocal) { - final Cursor cursor = db.query( - History.TABLE_NAME, - new String[] {History.GUID, History.VISITS, History.DATE_LAST_VISITED}, - null, null, null, null, null); - if (cursor == null) { - Log.e(LOGTAG, "Null cursor while selecting all history records"); - return; - } - - try { - if (!cursor.moveToFirst()) { - Log.e(LOGTAG, "No history records to synthesize visits for."); - return; - } - - int guidCol = cursor.getColumnIndexOrThrow(History.GUID); - int visitsCol = cursor.getColumnIndexOrThrow(History.VISITS); - int dateCol = cursor.getColumnIndexOrThrow(History.DATE_LAST_VISITED); - - // Re-use compiled SQL statements for faster inserts. - // Visit Type is going to be 1, which is the column's default value. - final String insertSqlStatement = "INSERT OR IGNORE INTO " + Visits.TABLE_NAME + "(" + - Visits.DATE_VISITED + "," + - Visits.HISTORY_GUID + "," + - Visits.IS_LOCAL + - ") VALUES (?, ?, ?)"; - final SQLiteStatement compiledInsertStatement = db.compileStatement(insertSqlStatement); - - // For each history record, insert as many visits as there are recorded in the VISITS column. - do { - final int numberOfVisits = cursor.getInt(visitsCol); - final String guid = cursor.getString(guidCol); - final long lastVisitedDate = cursor.getLong(dateCol); - - // Sanity check. - if (guid == null) { - continue; - } - - // In a strange case that lastVisitedDate is a very low number, let's not introduce - // negative timestamps into our data. - if (lastVisitedDate - numberOfVisits < 0) { - continue; - } - - for (int i = 0; i < numberOfVisits; i++) { - final long offsetVisitedDate = lastVisitedDate - i; - compiledInsertStatement.clearBindings(); - compiledInsertStatement.bindLong(1, offsetVisitedDate); - compiledInsertStatement.bindString(2, guid); - // Very old school, 1 is true and 0 is false :) - if (markAsLocal) { - compiledInsertStatement.bindLong(3, Visits.VISIT_IS_LOCAL); - } else { - compiledInsertStatement.bindLong(3, Visits.VISIT_IS_REMOTE); - } - compiledInsertStatement.executeInsert(); - } - } while (cursor.moveToNext()); - } catch (Exception e) { - Log.e(LOGTAG, "Error while synthesizing visits for history record", e); - } finally { - cursor.close(); - } - } - - private void updateHistoryTableAddVisitAggregates(final SQLiteDatabase db) { - db.execSQL("ALTER TABLE " + TABLE_HISTORY + - " ADD COLUMN " + History.LOCAL_VISITS + " INTEGER NOT NULL DEFAULT 0"); - db.execSQL("ALTER TABLE " + TABLE_HISTORY + - " ADD COLUMN " + History.REMOTE_VISITS + " INTEGER NOT NULL DEFAULT 0"); - db.execSQL("ALTER TABLE " + TABLE_HISTORY + - " ADD COLUMN " + History.LOCAL_DATE_LAST_VISITED + " INTEGER NOT NULL DEFAULT 0"); - db.execSQL("ALTER TABLE " + TABLE_HISTORY + - " ADD COLUMN " + History.REMOTE_DATE_LAST_VISITED + " INTEGER NOT NULL DEFAULT 0"); - } - - private void calculateHistoryTableVisitAggregates(final SQLiteDatabase db) { - // Note that we convert from microseconds (timestamps in the visits table) to milliseconds - // (timestamps in the history table). Sync works in microseconds, so for visits Fennec stores - // timestamps in microseconds as well - but the rest of the timestamps are stored in milliseconds. - db.execSQL("UPDATE " + TABLE_HISTORY + " SET " + - History.LOCAL_VISITS + " = (" + - "SELECT COALESCE(SUM(" + qualifyColumn(TABLE_VISITS, Visits.IS_LOCAL) + "), 0)" + - " FROM " + TABLE_VISITS + - " WHERE " + qualifyColumn(TABLE_VISITS, Visits.HISTORY_GUID) + " = " + qualifyColumn(TABLE_HISTORY, History.GUID) + - "), " + - History.REMOTE_VISITS + " = (" + - "SELECT COALESCE(SUM(CASE " + Visits.IS_LOCAL + " WHEN 0 THEN 1 ELSE 0 END), 0)" + - " FROM " + TABLE_VISITS + - " WHERE " + qualifyColumn(TABLE_VISITS, Visits.HISTORY_GUID) + " = " + qualifyColumn(TABLE_HISTORY, History.GUID) + - "), " + - History.LOCAL_DATE_LAST_VISITED + " = (" + - "SELECT COALESCE(MAX(CASE " + Visits.IS_LOCAL + " WHEN 1 THEN " + Visits.DATE_VISITED + " ELSE 0 END), 0) / 1000" + - " FROM " + TABLE_VISITS + - " WHERE " + qualifyColumn(TABLE_VISITS, Visits.HISTORY_GUID) + " = " + qualifyColumn(TABLE_HISTORY, History.GUID) + - "), " + - History.REMOTE_DATE_LAST_VISITED + " = (" + - "SELECT COALESCE(MAX(CASE " + Visits.IS_LOCAL + " WHEN 0 THEN " + Visits.DATE_VISITED + " ELSE 0 END), 0) / 1000" + - " FROM " + TABLE_VISITS + - " WHERE " + qualifyColumn(TABLE_VISITS, Visits.HISTORY_GUID) + " = " + qualifyColumn(TABLE_HISTORY, History.GUID) + - ") " + - "WHERE EXISTS " + - "(SELECT " + Visits._ID + - " FROM " + TABLE_VISITS + - " WHERE " + qualifyColumn(TABLE_VISITS, Visits.HISTORY_GUID) + " = " + qualifyColumn(TABLE_HISTORY, History.GUID) + ")" - ); - } - - private void upgradeDatabaseFrom32to33(final SQLiteDatabase db) { - createV33CombinedView(db); - } - - private void upgradeDatabaseFrom33to34(final SQLiteDatabase db) { - updateHistoryTableAddVisitAggregates(db); - calculateHistoryTableVisitAggregates(db); - createV34CombinedView(db); - } - - private void upgradeDatabaseFrom34to35(final SQLiteDatabase db) { - createActivityStreamBlocklistTable(db); - } - - private void upgradeDatabaseFrom35to36(final SQLiteDatabase db) { - createPageMetadataTable(db); - } - - private void createV33CombinedView(final SQLiteDatabase db) { - db.execSQL("DROP VIEW IF EXISTS " + VIEW_COMBINED); - db.execSQL("DROP VIEW IF EXISTS " + VIEW_COMBINED_WITH_FAVICONS); - - createCombinedViewOn33(db); - } - - private void createV34CombinedView(final SQLiteDatabase db) { - db.execSQL("DROP VIEW IF EXISTS " + VIEW_COMBINED); - db.execSQL("DROP VIEW IF EXISTS " + VIEW_COMBINED_WITH_FAVICONS); - - createCombinedViewOn34(db); - } - - private void createV19CombinedView(SQLiteDatabase db) { - db.execSQL("DROP VIEW IF EXISTS " + VIEW_COMBINED); - db.execSQL("DROP VIEW IF EXISTS " + VIEW_COMBINED_WITH_FAVICONS); - - createCombinedViewOn19(db); - } - - @Override - public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { - debug("Upgrading browser.db: " + db.getPath() + " from " + - oldVersion + " to " + newVersion); - - // We have to do incremental upgrades until we reach the current - // database schema version. - for (int v = oldVersion + 1; v <= newVersion; v++) { - switch (v) { - case 4: - upgradeDatabaseFrom3to4(db); - break; - - case 7: - upgradeDatabaseFrom6to7(db); - break; - - case 8: - upgradeDatabaseFrom7to8(db); - break; - - case 11: - upgradeDatabaseFrom10to11(db); - break; - - case 13: - upgradeDatabaseFrom12to13(db); - break; - - case 14: - upgradeDatabaseFrom13to14(db); - break; - - case 15: - upgradeDatabaseFrom14to15(db); - break; - - case 16: - upgradeDatabaseFrom15to16(db); - break; - - case 17: - upgradeDatabaseFrom16to17(db); - break; - - case 18: - upgradeDatabaseFrom17to18(db); - break; - - case 19: - upgradeDatabaseFrom18to19(db); - break; - - case 20: - upgradeDatabaseFrom19to20(db); - break; - - case 22: - upgradeDatabaseFrom21to22(db); - break; - - case 23: - upgradeDatabaseFrom22to23(db); - break; - - case 24: - upgradeDatabaseFrom23to24(db); - break; - - case 25: - upgradeDatabaseFrom24to25(db); - break; - - case 26: - upgradeDatabaseFrom25to26(db); - break; - - // case 27 occurs in UrlMetadataTable.onUpgrade - - case 28: - upgradeDatabaseFrom27to28(db); - break; - - case 29: - upgradeDatabaseFrom28to29(db); - break; - - case 30: - upgradeDatabaseFrom29to30(db); - break; - - case 31: - upgradeDatabaseFrom30to31(db); - break; - - case 32: - upgradeDatabaseFrom31to32(db); - break; - - case 33: - upgradeDatabaseFrom32to33(db); - break; - - case 34: - upgradeDatabaseFrom33to34(db); - break; - - case 35: - upgradeDatabaseFrom34to35(db); - break; - - case 36: - upgradeDatabaseFrom35to36(db); - break; - } - } - - for (Table table : BrowserProvider.sTables) { - table.onUpgrade(db, oldVersion, newVersion); - } - - // Delete the obsolete favicon database after all other upgrades complete. - // This can probably equivalently be moved into upgradeDatabaseFrom12to13. - if (oldVersion < 13 && newVersion >= 13) { - if (mContext.getDatabasePath("favicon_urls.db").exists()) { - mContext.deleteDatabase("favicon_urls.db"); - } - } - } - - @Override - public void onOpen(SQLiteDatabase db) { - debug("Opening browser.db: " + db.getPath()); - - // Force explicit readercache loading - we won't access readercache state for bookmarks - // until we actually know what our bookmarks are. Bookmarks are stored in the DB, hence - // it is sufficient to ensure that the readercache is loaded before the DB can be accessed. - // Note, this takes ~4-6ms to load on an N4 (compared to 20-50ms for most DB queries), and - // is only done once, hence this shouldn't have noticeable impact on performance. Moreover - // this is run on a background thread and therefore won't block UI code during startup. - SavedReaderViewHelper.getSavedReaderViewHelper(mContext).loadItems(); - - Cursor cursor = null; - try { - cursor = db.rawQuery("PRAGMA foreign_keys=ON", null); - } finally { - if (cursor != null) - cursor.close(); - } - cursor = null; - try { - cursor = db.rawQuery("PRAGMA synchronous=NORMAL", null); - } finally { - if (cursor != null) - cursor.close(); - } - - // From Honeycomb on, it's possible to run several db - // commands in parallel using multiple connections. - if (Build.VERSION.SDK_INT >= 11) { - // Modern Android allows WAL to be enabled through a mode flag. - if (Build.VERSION.SDK_INT < 16) { - db.enableWriteAheadLogging(); - - // This does nothing on 16+. - db.setLockingEnabled(false); - } - } else { - // Pre-Honeycomb, we can do some lesser optimizations. - cursor = null; - try { - cursor = db.rawQuery("PRAGMA journal_mode=PERSIST", null); - } finally { - if (cursor != null) - cursor.close(); - } - } - } - - // Calculate these once, at initialization. isLoggable is too expensive to - // have in-line in each log call. - private static final boolean logDebug = Log.isLoggable(LOGTAG, Log.DEBUG); - private static final boolean logVerbose = Log.isLoggable(LOGTAG, Log.VERBOSE); - protected static void trace(String message) { - if (logVerbose) { - Log.v(LOGTAG, message); - } - } - - protected static void debug(String message) { - if (logDebug) { - Log.d(LOGTAG, message); - } - } - - private Integer getMobileFolderId(SQLiteDatabase db) { - Cursor c = null; - - try { - c = db.query(TABLE_BOOKMARKS, - mobileIdColumns, - Bookmarks.GUID + " = ?", - mobileIdSelectionArgs, - null, null, null); - - if (c == null || !c.moveToFirst()) - return null; - - return c.getInt(c.getColumnIndex(Bookmarks._ID)); - } finally { - if (c != null) - c.close(); - } - } - - private interface BookmarkMigrator { - public void updateForNewTable(ContentValues bookmark); - } - - private class BookmarkMigrator3to4 implements BookmarkMigrator { - @Override - public void updateForNewTable(ContentValues bookmark) { - Integer isFolder = bookmark.getAsInteger("folder"); - if (isFolder == null || isFolder != 1) { - bookmark.put(Bookmarks.TYPE, Bookmarks.TYPE_BOOKMARK); - } else { - bookmark.put(Bookmarks.TYPE, Bookmarks.TYPE_FOLDER); - } - - bookmark.remove("folder"); - } - } -} - diff --git a/mobile/android/base/java/org/mozilla/gecko/db/BrowserProvider.java b/mobile/android/base/java/org/mozilla/gecko/db/BrowserProvider.java deleted file mode 100644 index eb75d0be96..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/db/BrowserProvider.java +++ /dev/null @@ -1,2340 +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.db; - -import java.lang.ref.WeakReference; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import org.mozilla.gecko.AboutPages; -import org.mozilla.gecko.GeckoProfile; -import org.mozilla.gecko.R; -import org.mozilla.gecko.db.BrowserContract.ActivityStreamBlocklist; -import org.mozilla.gecko.db.BrowserContract.Bookmarks; -import org.mozilla.gecko.db.BrowserContract.Combined; -import org.mozilla.gecko.db.BrowserContract.FaviconColumns; -import org.mozilla.gecko.db.BrowserContract.Favicons; -import org.mozilla.gecko.db.BrowserContract.Highlights; -import org.mozilla.gecko.db.BrowserContract.History; -import org.mozilla.gecko.db.BrowserContract.Visits; -import org.mozilla.gecko.db.BrowserContract.Schema; -import org.mozilla.gecko.db.BrowserContract.Tabs; -import org.mozilla.gecko.db.BrowserContract.Thumbnails; -import org.mozilla.gecko.db.BrowserContract.TopSites; -import org.mozilla.gecko.db.BrowserContract.UrlAnnotations; -import org.mozilla.gecko.db.BrowserContract.PageMetadata; -import org.mozilla.gecko.db.DBUtils.UpdateOperation; -import org.mozilla.gecko.icons.IconsHelper; -import org.mozilla.gecko.sync.Utils; -import org.mozilla.gecko.util.ThreadUtils; - -import android.content.BroadcastReceiver; -import android.content.ContentProviderOperation; -import android.content.ContentProviderResult; -import android.content.ContentUris; -import android.content.ContentValues; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.OperationApplicationException; -import android.content.UriMatcher; -import android.database.Cursor; -import android.database.DatabaseUtils; -import android.database.MatrixCursor; -import android.database.MergeCursor; -import android.database.SQLException; -import android.database.sqlite.SQLiteCursor; -import android.database.sqlite.SQLiteDatabase; -import android.database.sqlite.SQLiteQueryBuilder; -import android.net.Uri; -import android.support.v4.content.LocalBroadcastManager; -import android.text.TextUtils; -import android.util.Log; - -public class BrowserProvider extends SharedBrowserDatabaseProvider { - public static final String ACTION_SHRINK_MEMORY = "org.mozilla.gecko.db.intent.action.SHRINK_MEMORY"; - - private static final String LOGTAG = "GeckoBrowserProvider"; - - // How many records to reposition in a single query. - // This should be less than the SQLite maximum number of query variables - // (currently 999) divided by the number of variables used per positioning - // query (currently 3). - static final int MAX_POSITION_UPDATES_PER_QUERY = 100; - - // Minimum number of records to keep when expiring history. - static final int DEFAULT_EXPIRY_RETAIN_COUNT = 2000; - static final int AGGRESSIVE_EXPIRY_RETAIN_COUNT = 500; - - // Factor used to determine the minimum number of records to keep when expiring the activity stream blocklist - static final int ACTIVITYSTREAM_BLOCKLIST_EXPIRY_FACTOR = 5; - - // Minimum duration to keep when expiring. - static final long DEFAULT_EXPIRY_PRESERVE_WINDOW = 1000L * 60L * 60L * 24L * 28L; // Four weeks. - // Minimum number of thumbnails to keep around. - static final int DEFAULT_EXPIRY_THUMBNAIL_COUNT = 15; - - static final String TABLE_BOOKMARKS = Bookmarks.TABLE_NAME; - static final String TABLE_HISTORY = History.TABLE_NAME; - static final String TABLE_VISITS = Visits.TABLE_NAME; - static final String TABLE_FAVICONS = Favicons.TABLE_NAME; - static final String TABLE_THUMBNAILS = Thumbnails.TABLE_NAME; - static final String TABLE_TABS = Tabs.TABLE_NAME; - static final String TABLE_URL_ANNOTATIONS = UrlAnnotations.TABLE_NAME; - static final String TABLE_ACTIVITY_STREAM_BLOCKLIST = ActivityStreamBlocklist.TABLE_NAME; - static final String TABLE_PAGE_METADATA = PageMetadata.TABLE_NAME; - - static final String VIEW_COMBINED = Combined.VIEW_NAME; - static final String VIEW_BOOKMARKS_WITH_FAVICONS = Bookmarks.VIEW_WITH_FAVICONS; - static final String VIEW_BOOKMARKS_WITH_ANNOTATIONS = Bookmarks.VIEW_WITH_ANNOTATIONS; - static final String VIEW_HISTORY_WITH_FAVICONS = History.VIEW_WITH_FAVICONS; - static final String VIEW_COMBINED_WITH_FAVICONS = Combined.VIEW_WITH_FAVICONS; - - // Bookmark matches - static final int BOOKMARKS = 100; - static final int BOOKMARKS_ID = 101; - static final int BOOKMARKS_FOLDER_ID = 102; - static final int BOOKMARKS_PARENT = 103; - static final int BOOKMARKS_POSITIONS = 104; - - // History matches - static final int HISTORY = 200; - static final int HISTORY_ID = 201; - static final int HISTORY_OLD = 202; - - // Favicon matches - static final int FAVICONS = 300; - static final int FAVICON_ID = 301; - - // Schema matches - static final int SCHEMA = 400; - - // Combined bookmarks and history matches - static final int COMBINED = 500; - - // Control matches - static final int CONTROL = 600; - - // Search Suggest matches. Obsolete. - static final int SEARCH_SUGGEST = 700; - - // Thumbnail matches - static final int THUMBNAILS = 800; - static final int THUMBNAIL_ID = 801; - - static final int URL_ANNOTATIONS = 900; - - static final int TOPSITES = 1000; - - static final int VISITS = 1100; - - static final int METADATA = 1200; - - static final int HIGHLIGHTS = 1300; - - static final int ACTIVITY_STREAM_BLOCKLIST = 1400; - - static final int PAGE_METADATA = 1500; - - static final String DEFAULT_BOOKMARKS_SORT_ORDER = Bookmarks.TYPE - + " ASC, " + Bookmarks.POSITION + " ASC, " + Bookmarks._ID - + " ASC"; - - static final String DEFAULT_HISTORY_SORT_ORDER = History.DATE_LAST_VISITED + " DESC"; - static final String DEFAULT_VISITS_SORT_ORDER = Visits.DATE_VISITED + " DESC"; - - static final UriMatcher URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH); - - static final Map BOOKMARKS_PROJECTION_MAP; - static final Map HISTORY_PROJECTION_MAP; - static final Map COMBINED_PROJECTION_MAP; - static final Map SCHEMA_PROJECTION_MAP; - static final Map FAVICONS_PROJECTION_MAP; - static final Map THUMBNAILS_PROJECTION_MAP; - static final Map URL_ANNOTATIONS_PROJECTION_MAP; - static final Map VISIT_PROJECTION_MAP; - static final Map PAGE_METADATA_PROJECTION_MAP; - static final Table[] sTables; - - static { - sTables = new Table[] { - // See awful shortcut assumption hack in getURLMetadataTable. - new URLMetadataTable() - }; - // We will reuse this. - HashMap map; - - // Bookmarks - URI_MATCHER.addURI(BrowserContract.AUTHORITY, "bookmarks", BOOKMARKS); - URI_MATCHER.addURI(BrowserContract.AUTHORITY, "bookmarks/#", BOOKMARKS_ID); - URI_MATCHER.addURI(BrowserContract.AUTHORITY, "bookmarks/parents", BOOKMARKS_PARENT); - URI_MATCHER.addURI(BrowserContract.AUTHORITY, "bookmarks/positions", BOOKMARKS_POSITIONS); - URI_MATCHER.addURI(BrowserContract.AUTHORITY, "bookmarks/folder/#", BOOKMARKS_FOLDER_ID); - - map = new HashMap(); - map.put(Bookmarks._ID, Bookmarks._ID); - map.put(Bookmarks.TITLE, Bookmarks.TITLE); - map.put(Bookmarks.URL, Bookmarks.URL); - map.put(Bookmarks.FAVICON, Bookmarks.FAVICON); - map.put(Bookmarks.FAVICON_ID, Bookmarks.FAVICON_ID); - map.put(Bookmarks.FAVICON_URL, Bookmarks.FAVICON_URL); - map.put(Bookmarks.TYPE, Bookmarks.TYPE); - map.put(Bookmarks.PARENT, Bookmarks.PARENT); - map.put(Bookmarks.POSITION, Bookmarks.POSITION); - map.put(Bookmarks.TAGS, Bookmarks.TAGS); - map.put(Bookmarks.DESCRIPTION, Bookmarks.DESCRIPTION); - map.put(Bookmarks.KEYWORD, Bookmarks.KEYWORD); - map.put(Bookmarks.DATE_CREATED, Bookmarks.DATE_CREATED); - map.put(Bookmarks.DATE_MODIFIED, Bookmarks.DATE_MODIFIED); - map.put(Bookmarks.GUID, Bookmarks.GUID); - map.put(Bookmarks.IS_DELETED, Bookmarks.IS_DELETED); - BOOKMARKS_PROJECTION_MAP = Collections.unmodifiableMap(map); - - // History - URI_MATCHER.addURI(BrowserContract.AUTHORITY, "history", HISTORY); - URI_MATCHER.addURI(BrowserContract.AUTHORITY, "history/#", HISTORY_ID); - URI_MATCHER.addURI(BrowserContract.AUTHORITY, "history/old", HISTORY_OLD); - - map = new HashMap(); - map.put(History._ID, History._ID); - map.put(History.TITLE, History.TITLE); - map.put(History.URL, History.URL); - map.put(History.FAVICON, History.FAVICON); - map.put(History.FAVICON_ID, History.FAVICON_ID); - map.put(History.FAVICON_URL, History.FAVICON_URL); - map.put(History.VISITS, History.VISITS); - map.put(History.LOCAL_VISITS, History.LOCAL_VISITS); - map.put(History.REMOTE_VISITS, History.REMOTE_VISITS); - map.put(History.DATE_LAST_VISITED, History.DATE_LAST_VISITED); - map.put(History.LOCAL_DATE_LAST_VISITED, History.LOCAL_DATE_LAST_VISITED); - map.put(History.REMOTE_DATE_LAST_VISITED, History.REMOTE_DATE_LAST_VISITED); - map.put(History.DATE_CREATED, History.DATE_CREATED); - map.put(History.DATE_MODIFIED, History.DATE_MODIFIED); - map.put(History.GUID, History.GUID); - map.put(History.IS_DELETED, History.IS_DELETED); - HISTORY_PROJECTION_MAP = Collections.unmodifiableMap(map); - - // Visits - URI_MATCHER.addURI(BrowserContract.AUTHORITY, "visits", VISITS); - - map = new HashMap(); - map.put(Visits._ID, Visits._ID); - map.put(Visits.HISTORY_GUID, Visits.HISTORY_GUID); - map.put(Visits.VISIT_TYPE, Visits.VISIT_TYPE); - map.put(Visits.DATE_VISITED, Visits.DATE_VISITED); - map.put(Visits.IS_LOCAL, Visits.IS_LOCAL); - VISIT_PROJECTION_MAP = Collections.unmodifiableMap(map); - - // Favicons - URI_MATCHER.addURI(BrowserContract.AUTHORITY, "favicons", FAVICONS); - URI_MATCHER.addURI(BrowserContract.AUTHORITY, "favicons/#", FAVICON_ID); - - map = new HashMap(); - map.put(Favicons._ID, Favicons._ID); - map.put(Favicons.URL, Favicons.URL); - map.put(Favicons.DATA, Favicons.DATA); - map.put(Favicons.DATE_CREATED, Favicons.DATE_CREATED); - map.put(Favicons.DATE_MODIFIED, Favicons.DATE_MODIFIED); - FAVICONS_PROJECTION_MAP = Collections.unmodifiableMap(map); - - // Thumbnails - URI_MATCHER.addURI(BrowserContract.AUTHORITY, "thumbnails", THUMBNAILS); - URI_MATCHER.addURI(BrowserContract.AUTHORITY, "thumbnails/#", THUMBNAIL_ID); - - map = new HashMap(); - map.put(Thumbnails._ID, Thumbnails._ID); - map.put(Thumbnails.URL, Thumbnails.URL); - map.put(Thumbnails.DATA, Thumbnails.DATA); - THUMBNAILS_PROJECTION_MAP = Collections.unmodifiableMap(map); - - // Url annotations - URI_MATCHER.addURI(BrowserContract.AUTHORITY, TABLE_URL_ANNOTATIONS, URL_ANNOTATIONS); - - map = new HashMap<>(); - map.put(UrlAnnotations._ID, UrlAnnotations._ID); - map.put(UrlAnnotations.URL, UrlAnnotations.URL); - map.put(UrlAnnotations.KEY, UrlAnnotations.KEY); - map.put(UrlAnnotations.VALUE, UrlAnnotations.VALUE); - map.put(UrlAnnotations.DATE_CREATED, UrlAnnotations.DATE_CREATED); - map.put(UrlAnnotations.DATE_MODIFIED, UrlAnnotations.DATE_MODIFIED); - map.put(UrlAnnotations.SYNC_STATUS, UrlAnnotations.SYNC_STATUS); - URL_ANNOTATIONS_PROJECTION_MAP = Collections.unmodifiableMap(map); - - // Combined bookmarks and history - URI_MATCHER.addURI(BrowserContract.AUTHORITY, "combined", COMBINED); - - map = new HashMap(); - map.put(Combined._ID, Combined._ID); - map.put(Combined.BOOKMARK_ID, Combined.BOOKMARK_ID); - map.put(Combined.HISTORY_ID, Combined.HISTORY_ID); - map.put(Combined.URL, Combined.URL); - map.put(Combined.TITLE, Combined.TITLE); - map.put(Combined.VISITS, Combined.VISITS); - map.put(Combined.DATE_LAST_VISITED, Combined.DATE_LAST_VISITED); - map.put(Combined.FAVICON, Combined.FAVICON); - map.put(Combined.FAVICON_ID, Combined.FAVICON_ID); - map.put(Combined.FAVICON_URL, Combined.FAVICON_URL); - map.put(Combined.LOCAL_DATE_LAST_VISITED, Combined.LOCAL_DATE_LAST_VISITED); - map.put(Combined.REMOTE_DATE_LAST_VISITED, Combined.REMOTE_DATE_LAST_VISITED); - map.put(Combined.LOCAL_VISITS_COUNT, Combined.LOCAL_VISITS_COUNT); - map.put(Combined.REMOTE_VISITS_COUNT, Combined.REMOTE_VISITS_COUNT); - COMBINED_PROJECTION_MAP = Collections.unmodifiableMap(map); - - map = new HashMap<>(); - map.put(PageMetadata._ID, PageMetadata._ID); - map.put(PageMetadata.HISTORY_GUID, PageMetadata.HISTORY_GUID); - map.put(PageMetadata.DATE_CREATED, PageMetadata.DATE_CREATED); - map.put(PageMetadata.HAS_IMAGE, PageMetadata.HAS_IMAGE); - map.put(PageMetadata.JSON, PageMetadata.JSON); - PAGE_METADATA_PROJECTION_MAP = Collections.unmodifiableMap(map); - - URI_MATCHER.addURI(BrowserContract.AUTHORITY, "page_metadata", PAGE_METADATA); - - // Schema - URI_MATCHER.addURI(BrowserContract.AUTHORITY, "schema", SCHEMA); - - map = new HashMap(); - map.put(Schema.VERSION, Schema.VERSION); - SCHEMA_PROJECTION_MAP = Collections.unmodifiableMap(map); - - - // Control - URI_MATCHER.addURI(BrowserContract.AUTHORITY, "control", CONTROL); - - for (Table table : sTables) { - for (Table.ContentProviderInfo type : table.getContentProviderInfo()) { - URI_MATCHER.addURI(BrowserContract.AUTHORITY, type.name, type.id); - } - } - - // Combined pinned sites, top visited sites, and suggested sites - URI_MATCHER.addURI(BrowserContract.AUTHORITY, "topsites", TOPSITES); - - URI_MATCHER.addURI(BrowserContract.AUTHORITY, "highlights", HIGHLIGHTS); - - URI_MATCHER.addURI(BrowserContract.AUTHORITY, ActivityStreamBlocklist.TABLE_NAME, ACTIVITY_STREAM_BLOCKLIST); - } - - private static class ShrinkMemoryReceiver extends BroadcastReceiver { - private final WeakReference mBrowserProviderWeakReference; - - public ShrinkMemoryReceiver(final BrowserProvider browserProvider) { - mBrowserProviderWeakReference = new WeakReference<>(browserProvider); - } - - @Override - public void onReceive(Context context, Intent intent) { - final BrowserProvider browserProvider = mBrowserProviderWeakReference.get(); - if (browserProvider == null) { - return; - } - final PerProfileDatabases databases = browserProvider.getDatabases(); - if (databases == null) { - return; - } - ThreadUtils.postToBackgroundThread(new Runnable() { - @Override - public void run() { - databases.shrinkMemory(); - } - }); - } - } - - private final ShrinkMemoryReceiver mShrinkMemoryReceiver = new ShrinkMemoryReceiver(this); - - @Override - public boolean onCreate() { - if (!super.onCreate()) { - return false; - } - - LocalBroadcastManager.getInstance(getContext()).registerReceiver(mShrinkMemoryReceiver, - new IntentFilter(ACTION_SHRINK_MEMORY)); - - return true; - } - - @Override - public void shutdown() { - LocalBroadcastManager.getInstance(getContext()).unregisterReceiver(mShrinkMemoryReceiver); - - super.shutdown(); - } - - // Convenience accessor. - // Assumes structure of sTables! - private URLMetadataTable getURLMetadataTable() { - return (URLMetadataTable) sTables[0]; - } - - private static boolean hasFaviconsInProjection(String[] projection) { - if (projection == null) return true; - for (int i = 0; i < projection.length; ++i) { - if (projection[i].equals(FaviconColumns.FAVICON) || - projection[i].equals(FaviconColumns.FAVICON_URL)) - return true; - } - - return false; - } - - // Calculate these once, at initialization. isLoggable is too expensive to - // have in-line in each log call. - private static final boolean logDebug = Log.isLoggable(LOGTAG, Log.DEBUG); - private static final boolean logVerbose = Log.isLoggable(LOGTAG, Log.VERBOSE); - protected static void trace(String message) { - if (logVerbose) { - Log.v(LOGTAG, message); - } - } - - protected static void debug(String message) { - if (logDebug) { - Log.d(LOGTAG, message); - } - } - - /** - * Remove enough activity stream blocklist items to bring the database count below retain. - * - * Items will be removed according to their creation date, oldest being removed first. - */ - private void expireActivityStreamBlocklist(final SQLiteDatabase db, final int retain) { - Log.d(LOGTAG, "Expiring highlights blocklist."); - final long rows = DatabaseUtils.queryNumEntries(db, TABLE_ACTIVITY_STREAM_BLOCKLIST); - - if (retain >= rows) { - debug("Not expiring highlights blocklist: only have " + rows + " rows."); - return; - } - - final long toRemove = rows - retain; - - final String statement = "DELETE FROM " + TABLE_ACTIVITY_STREAM_BLOCKLIST + " WHERE " + ActivityStreamBlocklist._ID + " IN " + - " ( SELECT " + ActivityStreamBlocklist._ID + " FROM " + TABLE_ACTIVITY_STREAM_BLOCKLIST + " " + - "ORDER BY " + ActivityStreamBlocklist.CREATED + " ASC LIMIT " + toRemove + ")"; - - beginWrite(db); - db.execSQL(statement); - } - - /** - * Remove enough history items to bring the database count below retain, - * removing no items with a modified time after keepAfter. - * - * Provide keepAfter less than or equal to zero to skip that check. - * - * Items will be removed according to last visited date. - */ - private void expireHistory(final SQLiteDatabase db, final int retain, final long keepAfter) { - Log.d(LOGTAG, "Expiring history."); - final long rows = DatabaseUtils.queryNumEntries(db, TABLE_HISTORY); - - if (retain >= rows) { - debug("Not expiring history: only have " + rows + " rows."); - return; - } - - final long toRemove = rows - retain; - debug("Expiring at most " + toRemove + " rows earlier than " + keepAfter + "."); - - final String sql; - if (keepAfter > 0) { - sql = "DELETE FROM " + TABLE_HISTORY + " " + - "WHERE MAX(" + History.DATE_LAST_VISITED + ", " + History.DATE_MODIFIED + ") < " + keepAfter + " " + - " AND " + History._ID + " IN ( SELECT " + - History._ID + " FROM " + TABLE_HISTORY + " " + - "ORDER BY " + History.DATE_LAST_VISITED + " ASC LIMIT " + toRemove + - ")"; - } else { - sql = "DELETE FROM " + TABLE_HISTORY + " WHERE " + History._ID + " " + - "IN ( SELECT " + History._ID + " FROM " + TABLE_HISTORY + " " + - "ORDER BY " + History.DATE_LAST_VISITED + " ASC LIMIT " + toRemove + ")"; - } - trace("Deleting using query: " + sql); - - beginWrite(db); - db.execSQL(sql); - } - - /** - * Remove any thumbnails that for sites that aren't likely to be ever shown. - * Items will be removed according to a frecency calculation and only if they are not pinned - * - * Call this method within a transaction. - */ - private void expireThumbnails(final SQLiteDatabase db) { - Log.d(LOGTAG, "Expiring thumbnails."); - final String sortOrder = BrowserContract.getCombinedFrecencySortOrder(true, false); - final String sql = "DELETE FROM " + TABLE_THUMBNAILS + - " WHERE " + Thumbnails.URL + " NOT IN ( " + - " SELECT " + Combined.URL + - " FROM " + Combined.VIEW_NAME + - " ORDER BY " + sortOrder + - " LIMIT " + DEFAULT_EXPIRY_THUMBNAIL_COUNT + - ") AND " + Thumbnails.URL + " NOT IN ( " + - " SELECT " + Bookmarks.URL + - " FROM " + TABLE_BOOKMARKS + - " WHERE " + Bookmarks.PARENT + " = " + Bookmarks.FIXED_PINNED_LIST_ID + - ") AND " + Thumbnails.URL + " NOT IN ( " + - " SELECT " + Tabs.URL + - " FROM " + TABLE_TABS + - ")"; - trace("Clear thumbs using query: " + sql); - db.execSQL(sql); - } - - private boolean shouldIncrementVisits(Uri uri) { - String incrementVisits = uri.getQueryParameter(BrowserContract.PARAM_INCREMENT_VISITS); - return Boolean.parseBoolean(incrementVisits); - } - - private boolean shouldIncrementRemoteAggregates(Uri uri) { - final String incrementRemoteAggregates = uri.getQueryParameter(BrowserContract.PARAM_INCREMENT_REMOTE_AGGREGATES); - return Boolean.parseBoolean(incrementRemoteAggregates); - } - - @Override - public String getType(Uri uri) { - final int match = URI_MATCHER.match(uri); - - trace("Getting URI type: " + uri); - - switch (match) { - case BOOKMARKS: - trace("URI is BOOKMARKS: " + uri); - return Bookmarks.CONTENT_TYPE; - case BOOKMARKS_ID: - trace("URI is BOOKMARKS_ID: " + uri); - return Bookmarks.CONTENT_ITEM_TYPE; - case HISTORY: - trace("URI is HISTORY: " + uri); - return History.CONTENT_TYPE; - case HISTORY_ID: - trace("URI is HISTORY_ID: " + uri); - return History.CONTENT_ITEM_TYPE; - default: - String type = getContentItemType(match); - if (type != null) { - trace("URI is " + type); - return type; - } - - debug("URI has unrecognized type: " + uri); - return null; - } - } - - @SuppressWarnings("fallthrough") - @Override - public int deleteInTransaction(Uri uri, String selection, String[] selectionArgs) { - trace("Calling delete in transaction on URI: " + uri); - final SQLiteDatabase db = getWritableDatabase(uri); - - final int match = URI_MATCHER.match(uri); - int deleted = 0; - - switch (match) { - case BOOKMARKS_ID: - trace("Delete on BOOKMARKS_ID: " + uri); - - selection = DBUtils.concatenateWhere(selection, TABLE_BOOKMARKS + "._id = ?"); - selectionArgs = DBUtils.appendSelectionArgs(selectionArgs, - new String[] { Long.toString(ContentUris.parseId(uri)) }); - // fall through - case BOOKMARKS: { - trace("Deleting bookmarks: " + uri); - deleted = deleteBookmarks(uri, selection, selectionArgs); - deleteUnusedImages(uri); - break; - } - - case HISTORY_ID: - trace("Delete on HISTORY_ID: " + uri); - - selection = DBUtils.concatenateWhere(selection, TABLE_HISTORY + "._id = ?"); - selectionArgs = DBUtils.appendSelectionArgs(selectionArgs, - new String[] { Long.toString(ContentUris.parseId(uri)) }); - // fall through - case HISTORY: { - trace("Deleting history: " + uri); - beginWrite(db); - /** - * Deletes from Sync are actual DELETE statements, which will cascade delete relevant visits. - * Fennec's deletes mark records as deleted and wipe out all information (except for GUID). - * Eventually, Fennec will purge history records that were marked as deleted for longer than some - * period of time (e.g. 20 days). - * See {@link SharedBrowserDatabaseProvider#cleanUpSomeDeletedRecords(Uri, String)}. - */ - final ArrayList historyGUIDs = getHistoryGUIDsFromSelection(db, uri, selection, selectionArgs); - - if (!isCallerSync(uri)) { - deleteVisitsForHistory(db, historyGUIDs); - } - deletePageMetadataForHistory(db, historyGUIDs); - deleted = deleteHistory(db, uri, selection, selectionArgs); - deleteUnusedImages(uri); - break; - } - - case VISITS: - trace("Deleting visits: " + uri); - beginWrite(db); - deleted = deleteVisits(uri, selection, selectionArgs); - break; - - case HISTORY_OLD: { - String priority = uri.getQueryParameter(BrowserContract.PARAM_EXPIRE_PRIORITY); - long keepAfter = System.currentTimeMillis() - DEFAULT_EXPIRY_PRESERVE_WINDOW; - int retainCount = DEFAULT_EXPIRY_RETAIN_COUNT; - - if (BrowserContract.ExpirePriority.AGGRESSIVE.toString().equals(priority)) { - keepAfter = 0; - retainCount = AGGRESSIVE_EXPIRY_RETAIN_COUNT; - } - expireHistory(db, retainCount, keepAfter); - expireActivityStreamBlocklist(db, retainCount / ACTIVITYSTREAM_BLOCKLIST_EXPIRY_FACTOR); - expireThumbnails(db); - deleteUnusedImages(uri); - break; - } - - case FAVICON_ID: - debug("Delete on FAVICON_ID: " + uri); - - selection = DBUtils.concatenateWhere(selection, TABLE_FAVICONS + "._id = ?"); - selectionArgs = DBUtils.appendSelectionArgs(selectionArgs, - new String[] { Long.toString(ContentUris.parseId(uri)) }); - // fall through - case FAVICONS: { - trace("Deleting favicons: " + uri); - beginWrite(db); - deleted = deleteFavicons(uri, selection, selectionArgs); - break; - } - - case THUMBNAIL_ID: - debug("Delete on THUMBNAIL_ID: " + uri); - - selection = DBUtils.concatenateWhere(selection, TABLE_THUMBNAILS + "._id = ?"); - selectionArgs = DBUtils.appendSelectionArgs(selectionArgs, - new String[] { Long.toString(ContentUris.parseId(uri)) }); - // fall through - case THUMBNAILS: { - trace("Deleting thumbnails: " + uri); - beginWrite(db); - deleted = deleteThumbnails(uri, selection, selectionArgs); - break; - } - - case URL_ANNOTATIONS: - trace("Delete on URL_ANNOTATIONS: " + uri); - deleteUrlAnnotation(uri, selection, selectionArgs); - break; - - case PAGE_METADATA: - trace("Delete on PAGE_METADATA: " + uri); - deleted = deletePageMetadata(uri, selection, selectionArgs); - break; - - default: { - Table table = findTableFor(match); - if (table == null) { - throw new UnsupportedOperationException("Unknown delete URI " + uri); - } - trace("Deleting TABLE: " + uri); - beginWrite(db); - deleted = table.delete(db, uri, match, selection, selectionArgs); - } - } - - debug("Deleted " + deleted + " rows for URI: " + uri); - - return deleted; - } - - @Override - public Uri insertInTransaction(Uri uri, ContentValues values) { - trace("Calling insert in transaction on URI: " + uri); - - int match = URI_MATCHER.match(uri); - long id = -1; - - switch (match) { - case BOOKMARKS: { - trace("Insert on BOOKMARKS: " + uri); - id = insertBookmark(uri, values); - break; - } - - case HISTORY: { - trace("Insert on HISTORY: " + uri); - id = insertHistory(uri, values); - break; - } - - case VISITS: { - trace("Insert on VISITS: " + uri); - id = insertVisit(uri, values); - break; - } - - case FAVICONS: { - trace("Insert on FAVICONS: " + uri); - id = insertFavicon(uri, values); - break; - } - - case THUMBNAILS: { - trace("Insert on THUMBNAILS: " + uri); - id = insertThumbnail(uri, values); - break; - } - - case URL_ANNOTATIONS: { - trace("Insert on URL_ANNOTATIONS: " + uri); - id = insertUrlAnnotation(uri, values); - break; - } - - case ACTIVITY_STREAM_BLOCKLIST: { - trace("Insert on ACTIVITY_STREAM_BLOCKLIST: " + uri); - id = insertActivityStreamBlocklistSite(uri, values); - break; - } - - case PAGE_METADATA: { - trace("Insert on PAGE_METADATA: " + uri); - id = insertPageMetadata(uri, values); - break; - } - - default: { - Table table = findTableFor(match); - if (table == null) { - throw new UnsupportedOperationException("Unknown insert URI " + uri); - } - - trace("Insert on TABLE: " + uri); - final SQLiteDatabase db = getWritableDatabase(uri); - beginWrite(db); - id = table.insert(db, uri, match, values); - } - } - - debug("Inserted ID in database: " + id); - - if (id >= 0) - return ContentUris.withAppendedId(uri, id); - - return null; - } - - @SuppressWarnings("fallthrough") - @Override - public int updateInTransaction(Uri uri, ContentValues values, String selection, - String[] selectionArgs) { - trace("Calling update in transaction on URI: " + uri); - - int match = URI_MATCHER.match(uri); - int updated = 0; - - final SQLiteDatabase db = getWritableDatabase(uri); - switch (match) { - // We provide a dedicated (hacky) API for callers to bulk-update the positions of - // folder children by passing an array of GUID strings as `selectionArgs`. - // Each child will have its position column set to its index in the provided array. - // - // This avoids callers having to issue a large number of UPDATE queries through - // the usual channels. See Bug 728783. - // - // Note that this is decidedly not a general-purpose API; use at your own risk. - // `values` and `selection` are ignored. - case BOOKMARKS_POSITIONS: { - debug("Update on BOOKMARKS_POSITIONS: " + uri); - - // This already starts and finishes its own transaction. - updated = updateBookmarkPositions(uri, selectionArgs); - break; - } - - case BOOKMARKS_PARENT: { - debug("Update on BOOKMARKS_PARENT: " + uri); - beginWrite(db); - updated = updateBookmarkParents(db, values, selection, selectionArgs); - break; - } - - case BOOKMARKS_ID: - debug("Update on BOOKMARKS_ID: " + uri); - - selection = DBUtils.concatenateWhere(selection, TABLE_BOOKMARKS + "._id = ?"); - selectionArgs = DBUtils.appendSelectionArgs(selectionArgs, - new String[] { Long.toString(ContentUris.parseId(uri)) }); - // fall through - case BOOKMARKS: { - debug("Updating bookmark: " + uri); - if (shouldUpdateOrInsert(uri)) { - updated = updateOrInsertBookmark(uri, values, selection, selectionArgs); - } else { - updated = updateBookmarks(uri, values, selection, selectionArgs); - } - break; - } - - case HISTORY_ID: - debug("Update on HISTORY_ID: " + uri); - - selection = DBUtils.concatenateWhere(selection, TABLE_HISTORY + "._id = ?"); - selectionArgs = DBUtils.appendSelectionArgs(selectionArgs, - new String[] { Long.toString(ContentUris.parseId(uri)) }); - // fall through - case HISTORY: { - debug("Updating history: " + uri); - if (shouldUpdateOrInsert(uri)) { - updated = updateOrInsertHistory(uri, values, selection, selectionArgs); - } else { - updated = updateHistory(uri, values, selection, selectionArgs); - } - if (shouldIncrementVisits(uri)) { - insertVisitForHistory(uri, values, selection, selectionArgs); - } - break; - } - - case FAVICONS: { - debug("Update on FAVICONS: " + uri); - - String url = values.getAsString(Favicons.URL); - String faviconSelection = null; - String[] faviconSelectionArgs = null; - - if (!TextUtils.isEmpty(url)) { - faviconSelection = Favicons.URL + " = ?"; - faviconSelectionArgs = new String[] { url }; - } - - if (shouldUpdateOrInsert(uri)) { - updated = updateOrInsertFavicon(uri, values, faviconSelection, faviconSelectionArgs); - } else { - updated = updateExistingFavicon(uri, values, faviconSelection, faviconSelectionArgs); - } - break; - } - - case THUMBNAILS: { - debug("Update on THUMBNAILS: " + uri); - - String url = values.getAsString(Thumbnails.URL); - - // if no URL is provided, update all of the entries - if (TextUtils.isEmpty(values.getAsString(Thumbnails.URL))) { - updated = updateExistingThumbnail(uri, values, null, null); - } else if (shouldUpdateOrInsert(uri)) { - updated = updateOrInsertThumbnail(uri, values, Thumbnails.URL + " = ?", - new String[] { url }); - } else { - updated = updateExistingThumbnail(uri, values, Thumbnails.URL + " = ?", - new String[] { url }); - } - break; - } - - case URL_ANNOTATIONS: - updateUrlAnnotation(uri, values, selection, selectionArgs); - break; - - default: { - Table table = findTableFor(match); - if (table == null) { - throw new UnsupportedOperationException("Unknown update URI " + uri); - } - trace("Update TABLE: " + uri); - - beginWrite(db); - updated = table.update(db, uri, match, values, selection, selectionArgs); - if (shouldUpdateOrInsert(uri) && updated == 0) { - trace("No update, inserting for URL: " + uri); - table.insert(db, uri, match, values); - updated = 1; - } - } - } - - debug("Updated " + updated + " rows for URI: " + uri); - return updated; - } - - /** - * Get topsites by themselves, without the inclusion of pinned sites. Suggested sites - * will be appended (if necessary) to the end of the list in order to provide up to PARAM_LIMIT items. - */ - private Cursor getPlainTopSites(final Uri uri) { - final SQLiteDatabase db = getReadableDatabase(uri); - - final String limitParam = uri.getQueryParameter(BrowserContract.PARAM_LIMIT); - final int limit; - if (limitParam != null) { - limit = Integer.parseInt(limitParam); - } else { - limit = 12; - } - - // Filter out: unvisited pages (history_id == -1) pinned (and other special) sites, deleted sites, - // and about: pages. - final String ignoreForTopSitesWhereClause = - "(" + Combined.HISTORY_ID + " IS NOT -1)" + - " AND " + - Combined.URL + " NOT IN (SELECT " + - Bookmarks.URL + " FROM " + TABLE_BOOKMARKS + " WHERE " + - DBUtils.qualifyColumn(TABLE_BOOKMARKS, Bookmarks.PARENT) + " < " + Bookmarks.FIXED_ROOT_ID + " AND " + - DBUtils.qualifyColumn(TABLE_BOOKMARKS, Bookmarks.IS_DELETED) + " == 0)" + - " AND " + - "(" + Combined.URL + " NOT LIKE ?)"; - - final String[] ignoreForTopSitesArgs = new String[] { - AboutPages.URL_FILTER - }; - - final Cursor c = db.rawQuery("SELECT " + - Bookmarks._ID + ", " + - Combined.BOOKMARK_ID + ", " + - Combined.HISTORY_ID + ", " + - Bookmarks.URL + ", " + - Bookmarks.TITLE + ", " + - Combined.HISTORY_ID + ", " + - TopSites.TYPE_TOP + " AS " + TopSites.TYPE + - " FROM " + Combined.VIEW_NAME + - " WHERE " + ignoreForTopSitesWhereClause + - " ORDER BY " + BrowserContract.getCombinedFrecencySortOrder(true, false) + - " LIMIT " + limit, - ignoreForTopSitesArgs); - - c.setNotificationUri(getContext().getContentResolver(), - BrowserContract.AUTHORITY_URI); - - if (c.getCount() == limit) { - return c; - } - - // If we don't have enough data: get suggested sites too - final SuggestedSites suggestedSites = BrowserDB.from(GeckoProfile.get( - getContext(), uri.getQueryParameter(BrowserContract.PARAM_PROFILE))).getSuggestedSites(); - - final Cursor suggestedSitesCursor = suggestedSites.get(limit - c.getCount()); - - return new MergeCursor(new Cursor[]{ - c, - suggestedSitesCursor - }); - } - - private Cursor getTopSites(final Uri uri) { - // In order to correctly merge the top and pinned sites we: - // - // 1. Generate a list of free ids for topsites - this is the positions that are NOT used by pinned sites. - // We do this using a subquery with a self-join in order to generate rowids, that allow us to join with - // the list of topsites. - // 2. Generate the list of topsites in order of frecency. - // 3. Join these, so that each topsite is given its resulting position - // 4. UNION all with the pinned sites, and order by position - // - // Suggested sites are placed after the topsites, but might still be interspersed with the suggested sites, - // hence we append these to the topsite list, and treat these identically to topsites from this point on. - // - // We require rowids to join the two lists, however subqueries aren't given rowids - hence we use two different - // tricks to generate these: - // 1. The list of free ids is small, hence we can do a self-join to generate rowids. - // 2. The topsites list is larger, hence we use a temporary table, which automatically provides rowids. - - final SQLiteDatabase db = getWritableDatabase(uri); - - final String TABLE_TOPSITES = "topsites"; - - final String limitParam = uri.getQueryParameter(BrowserContract.PARAM_LIMIT); - final String gridLimitParam = uri.getQueryParameter(BrowserContract.PARAM_SUGGESTEDSITES_LIMIT); - - final int totalLimit; - final int suggestedGridLimit; - - if (limitParam == null) { - totalLimit = 50; - } else { - totalLimit = Integer.parseInt(limitParam, 10); - } - - if (gridLimitParam == null) { - suggestedGridLimit = getContext().getResources().getInteger(R.integer.number_of_top_sites); - } else { - suggestedGridLimit = Integer.parseInt(gridLimitParam, 10); - } - - final String pinnedSitesFromClause = "FROM " + TABLE_BOOKMARKS + " WHERE " + - Bookmarks.PARENT + " == " + Bookmarks.FIXED_PINNED_LIST_ID + - " AND " + Bookmarks.IS_DELETED + " IS NOT 1"; - - // Ideally we'd use a recursive CTE to generate our sequence, e.g. something like this worked at one point: - // " WITH RECURSIVE" + - // " cnt(x) AS (VALUES(1) UNION ALL SELECT x+1 FROM cnt WHERE x < 6)" + - // However that requires SQLite >= 3.8.3 (available on Android >= 5.0), so in the meantime - // we use a temporary numbers table. - // Note: SQLite rowids are 1-indexed, whereas we're expecting 0-indexed values for the position. Our numbers - // table starts at position = 0, which ensures the correct results here. - final String freeIDSubquery = - " SELECT count(free_ids.position) + 1 AS rowid, numbers.position AS " + Bookmarks.POSITION + - " FROM (SELECT position FROM numbers WHERE position NOT IN (SELECT " + Bookmarks.POSITION + " " + pinnedSitesFromClause + ")) AS numbers" + - " LEFT OUTER JOIN " + - " (SELECT position FROM numbers WHERE position NOT IN (SELECT " + Bookmarks.POSITION + " " + pinnedSitesFromClause + ")) AS free_ids" + - " ON numbers.position > free_ids.position" + - " GROUP BY numbers.position" + - " ORDER BY numbers.position ASC" + - " LIMIT " + suggestedGridLimit; - - // Filter out: unvisited pages (history_id == -1) pinned (and other special) sites, deleted sites, - // and about: pages. - final String ignoreForTopSitesWhereClause = - "(" + Combined.HISTORY_ID + " IS NOT -1)" + - " AND " + - Combined.URL + " NOT IN (SELECT " + - Bookmarks.URL + " FROM bookmarks WHERE " + - DBUtils.qualifyColumn("bookmarks", Bookmarks.PARENT) + " < " + Bookmarks.FIXED_ROOT_ID + " AND " + - DBUtils.qualifyColumn("bookmarks", Bookmarks.IS_DELETED) + " == 0)" + - " AND " + - "(" + Combined.URL + " NOT LIKE ?)"; - - final String[] ignoreForTopSitesArgs = new String[] { - AboutPages.URL_FILTER - }; - - // Stuff the suggested sites into SQL: this allows us to filter pinned and topsites out of the suggested - // sites list as part of the final query (as opposed to walking cursors in java) - final SuggestedSites suggestedSites = BrowserDB.from(GeckoProfile.get( - getContext(), uri.getQueryParameter(BrowserContract.PARAM_PROFILE))).getSuggestedSites(); - - StringBuilder suggestedSitesBuilder = new StringBuilder(); - // We could access the underlying data here, however SuggestedSites also performs filtering on the suggested - // sites list, which means we'd need to process the lists within SuggestedSites in any case. If we're doing - // that processing, there is little real between us using a MatrixCursor, or a Map (or List) instead of the - // MatrixCursor. - final Cursor suggestedSitesCursor = suggestedSites.get(suggestedGridLimit); - - String[] suggestedSiteArgs = new String[0]; - - boolean hasProcessedAnySuggestedSites = false; - - final int idColumnIndex = suggestedSitesCursor.getColumnIndexOrThrow(Bookmarks._ID); - final int urlColumnIndex = suggestedSitesCursor.getColumnIndexOrThrow(Bookmarks.URL); - final int titleColumnIndex = suggestedSitesCursor.getColumnIndexOrThrow(Bookmarks.TITLE); - - while (suggestedSitesCursor.moveToNext()) { - // We'll be using this as a subquery, hence we need to avoid the preceding UNION ALL - if (hasProcessedAnySuggestedSites) { - suggestedSitesBuilder.append(" UNION ALL"); - } else { - hasProcessedAnySuggestedSites = true; - } - suggestedSitesBuilder.append(" SELECT" + - " ? AS " + Bookmarks._ID + "," + - " ? AS " + Bookmarks.URL + "," + - " ? AS " + Bookmarks.TITLE); - - suggestedSiteArgs = DBUtils.appendSelectionArgs(suggestedSiteArgs, - new String[] { - suggestedSitesCursor.getString(idColumnIndex), - suggestedSitesCursor.getString(urlColumnIndex), - suggestedSitesCursor.getString(titleColumnIndex) - }); - } - suggestedSitesCursor.close(); - - boolean hasPreparedBlankTiles = false; - - // We can somewhat reduce the number of blanks we produce by eliminating suggested sites. - // We do the actual limit calculation in SQL (since we need to take into account the number - // of pinned sites too), but this might avoid producing 5 or so additional blank tiles - // that would then need to be filtered out. - final int maxBlanksNeeded = suggestedGridLimit - suggestedSitesCursor.getCount(); - - final StringBuilder blanksBuilder = new StringBuilder(); - for (int i = 0; i < maxBlanksNeeded; i++) { - if (hasPreparedBlankTiles) { - blanksBuilder.append(" UNION ALL"); - } else { - hasPreparedBlankTiles = true; - } - - blanksBuilder.append(" SELECT" + - " -1 AS " + Bookmarks._ID + "," + - " '' AS " + Bookmarks.URL + "," + - " '' AS " + Bookmarks.TITLE); - } - - - - // To restrict suggested sites to the grid, we simply subtract the number of topsites (which have already had - // the pinned sites filtered out), and the number of pinned sites. - // SQLite completely ignores negative limits, hence we need to manually limit to 0 in this case. - final String suggestedLimitClause = " LIMIT MAX(0, (" + suggestedGridLimit + " - (SELECT COUNT(*) FROM " + TABLE_TOPSITES + ") - (SELECT COUNT(*) " + pinnedSitesFromClause + "))) "; - - // Pinned site positions are zero indexed, but we need to get the maximum 1-indexed position. - // Hence to correctly calculate the largest pinned position (which should be 0 if there are - // no sites, or 1-6 if we have at least one pinned site), we coalesce the DB position (0-5) - // with -1 to represent no-sites, which allows us to directly add 1 to obtain the expected value - // regardless of whether a position was actually retrieved. - final String blanksLimitClause = " LIMIT MAX(0, " + - "COALESCE((SELECT " + Bookmarks.POSITION + " " + pinnedSitesFromClause + "), -1) + 1" + - " - (SELECT COUNT(*) " + pinnedSitesFromClause + ")" + - " - (SELECT COUNT(*) FROM " + TABLE_TOPSITES + ")" + - ")"; - - db.beginTransaction(); - try { - db.execSQL("DROP TABLE IF EXISTS " + TABLE_TOPSITES); - - db.execSQL("CREATE TEMP TABLE " + TABLE_TOPSITES + " AS" + - " SELECT " + - Bookmarks._ID + ", " + - Combined.BOOKMARK_ID + ", " + - Combined.HISTORY_ID + ", " + - Bookmarks.URL + ", " + - Bookmarks.TITLE + ", " + - Combined.HISTORY_ID + ", " + - TopSites.TYPE_TOP + " AS " + TopSites.TYPE + - " FROM " + Combined.VIEW_NAME + - " WHERE " + ignoreForTopSitesWhereClause + - " ORDER BY " + BrowserContract.getCombinedFrecencySortOrder(true, false) + - " LIMIT " + totalLimit, - - ignoreForTopSitesArgs); - - if (hasProcessedAnySuggestedSites) { - db.execSQL("INSERT INTO " + TABLE_TOPSITES + - // We need to LIMIT _after_ selecting the relevant suggested sites, which requires us to - // use an additional internal subquery, since we cannot LIMIT a subquery that is part of UNION ALL. - // Hence the weird SELECT * FROM (SELECT ...relevant suggested sites... LIMIT ?) - " SELECT * FROM (SELECT " + - Bookmarks._ID + ", " + - Bookmarks._ID + " AS " + Combined.BOOKMARK_ID + ", " + - " -1 AS " + Combined.HISTORY_ID + ", " + - Bookmarks.URL + ", " + - Bookmarks.TITLE + ", " + - "NULL AS " + Combined.HISTORY_ID + ", " + - TopSites.TYPE_SUGGESTED + " as " + TopSites.TYPE + - " FROM ( " + suggestedSitesBuilder.toString() + " )" + - " WHERE " + - Bookmarks.URL + " NOT IN (SELECT url FROM " + TABLE_TOPSITES + ")" + - " AND " + - Bookmarks.URL + " NOT IN (SELECT url " + pinnedSitesFromClause + ")" + - suggestedLimitClause + " )", - - suggestedSiteArgs); - } - - if (hasPreparedBlankTiles) { - db.execSQL("INSERT INTO " + TABLE_TOPSITES + - // We need to LIMIT _after_ selecting the relevant suggested sites, which requires us to - // use an additional internal subquery, since we cannot LIMIT a subquery that is part of UNION ALL. - // Hence the weird SELECT * FROM (SELECT ...relevant suggested sites... LIMIT ?) - " SELECT * FROM (SELECT " + - Bookmarks._ID + ", " + - Bookmarks._ID + " AS " + Combined.BOOKMARK_ID + ", " + - " -1 AS " + Combined.HISTORY_ID + ", " + - Bookmarks.URL + ", " + - Bookmarks.TITLE + ", " + - "NULL AS " + Combined.HISTORY_ID + ", " + - TopSites.TYPE_BLANK + " as " + TopSites.TYPE + - " FROM ( " + blanksBuilder.toString() + " )" + - blanksLimitClause + " )"); - } - - // If we retrieve more topsites than we have free positions for in the freeIdSubquery, - // we will have topsites that don't receive a position when joining TABLE_TOPSITES - // with freeIdSubquery. Hence we need to coalesce the position with a generated position. - // We know that the difference in positions will be at most suggestedGridLimit, hence we - // can add that to the rowid to generate a safe position. - // I.e. if we have 6 pinned sites then positions 0..5 are filled, the JOIN results in - // the first N rows having positions 6..(N+6), so row N+1 should receive a position that is at - // least N+1+6, which is equal to rowid + 6. - final SQLiteCursor c = (SQLiteCursor) db.rawQuery( - "SELECT " + - Bookmarks._ID + ", " + - TopSites.BOOKMARK_ID + ", " + - TopSites.HISTORY_ID + ", " + - Bookmarks.URL + ", " + - Bookmarks.TITLE + ", " + - "COALESCE(" + Bookmarks.POSITION + ", " + - DBUtils.qualifyColumn(TABLE_TOPSITES, "rowid") + " + " + suggestedGridLimit + - ")" + " AS " + Bookmarks.POSITION + ", " + - Combined.HISTORY_ID + ", " + - TopSites.TYPE + - " FROM " + TABLE_TOPSITES + - " LEFT OUTER JOIN " + // TABLE_IDS + - "(" + freeIDSubquery + ") AS id_results" + - " ON " + DBUtils.qualifyColumn(TABLE_TOPSITES, "rowid") + - " = " + DBUtils.qualifyColumn("id_results", "rowid") + - - " UNION ALL " + - - "SELECT " + - Bookmarks._ID + ", " + - Bookmarks._ID + " AS " + TopSites.BOOKMARK_ID + ", " + - " -1 AS " + TopSites.HISTORY_ID + ", " + - Bookmarks.URL + ", " + - Bookmarks.TITLE + ", " + - Bookmarks.POSITION + ", " + - "NULL AS " + Combined.HISTORY_ID + ", " + - TopSites.TYPE_PINNED + " as " + TopSites.TYPE + - " " + pinnedSitesFromClause + - - " ORDER BY " + Bookmarks.POSITION, - - null); - - c.setNotificationUri(getContext().getContentResolver(), - BrowserContract.AUTHORITY_URI); - - // Force the cursor to be compiled and the cursor-window filled now: - // (A) without compiling the cursor now we won't have access to the TEMP table which - // is removed as soon as we close our connection. - // (B) this might also mitigate the situation causing this crash where we're accessing - // a cursor and crashing in fillWindow. - c.moveToFirst(); - - db.setTransactionSuccessful(); - return c; - } finally { - db.endTransaction(); - } - } - - /** - * Obtain a set of links for highlights (from bookmarks and history). - * - * Based on the query for Activity^ Stream (desktop): - * https://github.com/mozilla/activity-stream/blob/9eb9f451b553bb62ae9b8d6b41a8ef94a2e020ea/addon/PlacesProvider.js#L578 - */ - public Cursor getHighlights(final SQLiteDatabase db, String limit) { - final int totalLimit = limit == null ? 20 : Integer.parseInt(limit); - - final long threeDaysAgo = System.currentTimeMillis() - (1000 * 60 * 60 * 24 * 3); - final long bookmarkLimit = 1; - - // Select recent bookmarks that have not been visited much - final String bookmarksQuery = "SELECT * FROM (SELECT " + - "-1 AS " + Combined.HISTORY_ID + ", " + - DBUtils.qualifyColumn(Bookmarks.TABLE_NAME, Bookmarks._ID) + " AS " + Combined.BOOKMARK_ID + ", " + - DBUtils.qualifyColumn(Bookmarks.TABLE_NAME, Bookmarks.URL) + ", " + - DBUtils.qualifyColumn(Bookmarks.TABLE_NAME, Bookmarks.TITLE) + ", " + - DBUtils.qualifyColumn(Bookmarks.TABLE_NAME, Bookmarks.DATE_CREATED) + " AS " + Highlights.DATE + " " + - "FROM " + Bookmarks.TABLE_NAME + " " + - "LEFT JOIN " + History.TABLE_NAME + " ON " + - DBUtils.qualifyColumn(Bookmarks.TABLE_NAME, Bookmarks.URL) + " = " + - DBUtils.qualifyColumn(History.TABLE_NAME, History.URL) + " " + - "WHERE " + DBUtils.qualifyColumn(Bookmarks.TABLE_NAME, Bookmarks.DATE_CREATED) + " > " + threeDaysAgo + " " + - "AND (" + DBUtils.qualifyColumn(History.TABLE_NAME, History.VISITS) + " <= 3 " + - "OR " + DBUtils.qualifyColumn(History.TABLE_NAME, History.VISITS) + " IS NULL) " + - "AND " + DBUtils.qualifyColumn(Bookmarks.TABLE_NAME, Bookmarks.IS_DELETED) + " = 0 " + - "AND " + DBUtils.qualifyColumn(Bookmarks.TABLE_NAME, Bookmarks.TYPE) + " = " + Bookmarks.TYPE_BOOKMARK + " " + - "AND " + DBUtils.qualifyColumn(Bookmarks.TABLE_NAME, Bookmarks.URL) + " NOT IN (SELECT " + ActivityStreamBlocklist.URL + " FROM " + ActivityStreamBlocklist.TABLE_NAME + " )" + - "ORDER BY " + DBUtils.qualifyColumn(Bookmarks.TABLE_NAME, Bookmarks.DATE_CREATED) + " DESC " + - "LIMIT " + bookmarkLimit + ")"; - - final long last30Minutes = System.currentTimeMillis() - (1000 * 60 * 30); - final long historyLimit = totalLimit - bookmarkLimit; - - // Select recent history that has not been visited much. - final String historyQuery = "SELECT * FROM (SELECT " + - History._ID + " AS " + Combined.HISTORY_ID + ", " + - "-1 AS " + Combined.BOOKMARK_ID + ", " + - History.URL + ", " + - History.TITLE + ", " + - History.DATE_LAST_VISITED + " AS " + Highlights.DATE + " " + - "FROM " + History.TABLE_NAME + " " + - "WHERE " + History.DATE_LAST_VISITED + " < " + last30Minutes + " " + - "AND " + History.VISITS + " <= 3 " + - "AND " + History.TITLE + " NOT NULL AND " + History.TITLE + " != '' " + - "AND " + History.IS_DELETED + " = 0 " + - "AND " + History.URL + " NOT IN (SELECT " + ActivityStreamBlocklist.URL + " FROM " + ActivityStreamBlocklist.TABLE_NAME + " )" + - // TODO: Implement domain black list (bug 1298786) - // TODO: Group by host (bug 1298785) - "ORDER BY " + History.DATE_LAST_VISITED + " DESC " + - "LIMIT " + historyLimit + ")"; - - final String query = "SELECT DISTINCT * " + - "FROM (" + bookmarksQuery + " " + - "UNION ALL " + historyQuery + ") " + - "GROUP BY " + Combined.URL + ";"; - - final Cursor cursor = db.rawQuery(query, null); - - cursor.setNotificationUri(getContext().getContentResolver(), - BrowserContract.AUTHORITY_URI); - - return cursor; - } - - @Override - public Cursor query(Uri uri, String[] projection, String selection, - String[] selectionArgs, String sortOrder) { - final int match = URI_MATCHER.match(uri); - - // Handle only queries requiring a writable DB connection here: most queries need only a readable - // connection, hence we can get a readable DB once, and then handle most queries within a switch. - // TopSites requires a writable connection (because of the temporary tables it uses), hence - // we handle that separately, i.e. before retrieving a readable connection. - if (match == TOPSITES) { - if (uri.getBooleanQueryParameter(BrowserContract.PARAM_TOPSITES_DISABLE_PINNED, false)) { - return getPlainTopSites(uri); - } else { - return getTopSites(uri); - } - } - - SQLiteDatabase db = getReadableDatabase(uri); - - SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); - String limit = uri.getQueryParameter(BrowserContract.PARAM_LIMIT); - String groupBy = null; - - switch (match) { - case BOOKMARKS_FOLDER_ID: - case BOOKMARKS_ID: - case BOOKMARKS: { - debug("Query is on bookmarks: " + uri); - - if (match == BOOKMARKS_ID) { - selection = DBUtils.concatenateWhere(selection, Bookmarks._ID + " = ?"); - selectionArgs = DBUtils.appendSelectionArgs(selectionArgs, - new String[] { Long.toString(ContentUris.parseId(uri)) }); - } else if (match == BOOKMARKS_FOLDER_ID) { - selection = DBUtils.concatenateWhere(selection, Bookmarks.PARENT + " = ?"); - selectionArgs = DBUtils.appendSelectionArgs(selectionArgs, - new String[] { Long.toString(ContentUris.parseId(uri)) }); - } - - if (!shouldShowDeleted(uri)) - selection = DBUtils.concatenateWhere(Bookmarks.IS_DELETED + " = 0", selection); - - if (TextUtils.isEmpty(sortOrder)) { - sortOrder = DEFAULT_BOOKMARKS_SORT_ORDER; - } else { - debug("Using sort order " + sortOrder + "."); - } - - qb.setProjectionMap(BOOKMARKS_PROJECTION_MAP); - - if (hasFaviconsInProjection(projection)) { - qb.setTables(VIEW_BOOKMARKS_WITH_FAVICONS); - } else if (selection != null && selection.contains(Bookmarks.ANNOTATION_KEY)) { - qb.setTables(VIEW_BOOKMARKS_WITH_ANNOTATIONS); - - groupBy = uri.getQueryParameter(BrowserContract.PARAM_GROUP_BY); - } else { - qb.setTables(TABLE_BOOKMARKS); - } - - break; - } - - case HISTORY_ID: - selection = DBUtils.concatenateWhere(selection, History._ID + " = ?"); - selectionArgs = DBUtils.appendSelectionArgs(selectionArgs, - new String[] { Long.toString(ContentUris.parseId(uri)) }); - // fall through - case HISTORY: { - debug("Query is on history: " + uri); - - if (!shouldShowDeleted(uri)) - selection = DBUtils.concatenateWhere(History.IS_DELETED + " = 0", selection); - - if (TextUtils.isEmpty(sortOrder)) - sortOrder = DEFAULT_HISTORY_SORT_ORDER; - - qb.setProjectionMap(HISTORY_PROJECTION_MAP); - - if (hasFaviconsInProjection(projection)) - qb.setTables(VIEW_HISTORY_WITH_FAVICONS); - else - qb.setTables(TABLE_HISTORY); - - break; - } - - case VISITS: - debug("Query is on visits: " + uri); - qb.setProjectionMap(VISIT_PROJECTION_MAP); - qb.setTables(TABLE_VISITS); - - if (TextUtils.isEmpty(sortOrder)) { - sortOrder = DEFAULT_VISITS_SORT_ORDER; - } - break; - - case FAVICON_ID: - selection = DBUtils.concatenateWhere(selection, Favicons._ID + " = ?"); - selectionArgs = DBUtils.appendSelectionArgs(selectionArgs, - new String[] { Long.toString(ContentUris.parseId(uri)) }); - // fall through - case FAVICONS: { - debug("Query is on favicons: " + uri); - - qb.setProjectionMap(FAVICONS_PROJECTION_MAP); - qb.setTables(TABLE_FAVICONS); - - break; - } - - case THUMBNAIL_ID: - selection = DBUtils.concatenateWhere(selection, Thumbnails._ID + " = ?"); - selectionArgs = DBUtils.appendSelectionArgs(selectionArgs, - new String[] { Long.toString(ContentUris.parseId(uri)) }); - // fall through - case THUMBNAILS: { - debug("Query is on thumbnails: " + uri); - - qb.setProjectionMap(THUMBNAILS_PROJECTION_MAP); - qb.setTables(TABLE_THUMBNAILS); - - break; - } - - case URL_ANNOTATIONS: - debug("Query is on url annotations: " + uri); - - qb.setProjectionMap(URL_ANNOTATIONS_PROJECTION_MAP); - qb.setTables(TABLE_URL_ANNOTATIONS); - break; - - case SCHEMA: { - debug("Query is on schema."); - MatrixCursor schemaCursor = new MatrixCursor(new String[] { Schema.VERSION }); - schemaCursor.newRow().add(BrowserDatabaseHelper.DATABASE_VERSION); - - return schemaCursor; - } - - case COMBINED: { - debug("Query is on combined: " + uri); - - if (TextUtils.isEmpty(sortOrder)) - sortOrder = DEFAULT_HISTORY_SORT_ORDER; - - // This will avoid duplicate entries in the awesomebar - // results when a history entry has multiple bookmarks. - groupBy = Combined.URL; - - qb.setProjectionMap(COMBINED_PROJECTION_MAP); - - if (hasFaviconsInProjection(projection)) - qb.setTables(VIEW_COMBINED_WITH_FAVICONS); - else - qb.setTables(Combined.VIEW_NAME); - - break; - } - - case HIGHLIGHTS: { - debug("Highlights query: " + uri); - - return getHighlights(db, limit); - } - - case PAGE_METADATA: { - debug("PageMetadata query: " + uri); - - qb.setProjectionMap(PAGE_METADATA_PROJECTION_MAP); - qb.setTables(TABLE_PAGE_METADATA); - break; - } - - default: { - Table table = findTableFor(match); - if (table == null) { - throw new UnsupportedOperationException("Unknown query URI " + uri); - } - trace("Update TABLE: " + uri); - return table.query(db, uri, match, projection, selection, selectionArgs, sortOrder, groupBy, limit); - } - } - - trace("Running built query."); - Cursor cursor = qb.query(db, projection, selection, selectionArgs, groupBy, - null, sortOrder, limit); - cursor.setNotificationUri(getContext().getContentResolver(), - BrowserContract.AUTHORITY_URI); - - return cursor; - } - - /** - * Update the positions of bookmarks in batches. - * - * Begins and ends its own transactions. - * - * @see #updateBookmarkPositionsInTransaction(SQLiteDatabase, String[], int, int) - */ - private int updateBookmarkPositions(Uri uri, String[] guids) { - if (guids == null) { - return 0; - } - - int guidsCount = guids.length; - if (guidsCount == 0) { - return 0; - } - - int offset = 0; - int updated = 0; - - final SQLiteDatabase db = getWritableDatabase(uri); - db.beginTransaction(); - - while (offset < guidsCount) { - try { - updated += updateBookmarkPositionsInTransaction(db, guids, offset, - MAX_POSITION_UPDATES_PER_QUERY); - } catch (SQLException e) { - Log.e(LOGTAG, "Got SQLite exception updating bookmark positions at offset " + offset, e); - - // Need to restart the transaction. - // The only way a caller knows that anything failed is that the - // returned update count will be smaller than the requested - // number of records. - db.setTransactionSuccessful(); - db.endTransaction(); - - db.beginTransaction(); - } - - offset += MAX_POSITION_UPDATES_PER_QUERY; - } - - db.setTransactionSuccessful(); - db.endTransaction(); - - return updated; - } - - /** - * Construct and execute an update expression that will modify the positions - * of records in-place. - */ - private static int updateBookmarkPositionsInTransaction(final SQLiteDatabase db, final String[] guids, - final int offset, final int max) { - int guidsCount = guids.length; - int processCount = Math.min(max, guidsCount - offset); - - // Each must appear twice: once in a CASE, and once in the IN clause. - String[] args = new String[processCount * 2]; - System.arraycopy(guids, offset, args, 0, processCount); - System.arraycopy(guids, offset, args, processCount, processCount); - - StringBuilder b = new StringBuilder("UPDATE " + TABLE_BOOKMARKS + - " SET " + Bookmarks.POSITION + - " = CASE guid"); - - // Build the CASE statement body for GUID/index pairs from offset up to - // the computed limit. - final int end = offset + processCount; - int i = offset; - for (; i < end; ++i) { - if (guids[i] == null) { - // We don't want to issue the query if not every GUID is specified. - debug("updateBookmarkPositions called with null GUID at index " + i); - return 0; - } - b.append(" WHEN ? THEN " + i); - } - - b.append(" END WHERE " + DBUtils.computeSQLInClause(processCount, Bookmarks.GUID)); - db.execSQL(b.toString(), args); - - // We can't easily get a modified count without calling something like changes(). - return processCount; - } - - /** - * Construct an update expression that will modify the parents of any records - * that match. - */ - private int updateBookmarkParents(SQLiteDatabase db, ContentValues values, String selection, String[] selectionArgs) { - trace("Updating bookmark parents of " + selection + " (" + selectionArgs[0] + ")"); - String where = Bookmarks._ID + " IN (" + - " SELECT DISTINCT " + Bookmarks.PARENT + - " FROM " + TABLE_BOOKMARKS + - " WHERE " + selection + " )"; - return db.update(TABLE_BOOKMARKS, values, where, selectionArgs); - } - - private long insertBookmark(Uri uri, ContentValues values) { - // Generate values if not specified. Don't overwrite - // if specified by caller. - long now = System.currentTimeMillis(); - if (!values.containsKey(Bookmarks.DATE_CREATED)) { - values.put(Bookmarks.DATE_CREATED, now); - } - - if (!values.containsKey(Bookmarks.DATE_MODIFIED)) { - values.put(Bookmarks.DATE_MODIFIED, now); - } - - if (!values.containsKey(Bookmarks.GUID)) { - values.put(Bookmarks.GUID, Utils.generateGuid()); - } - - if (!values.containsKey(Bookmarks.POSITION)) { - debug("Inserting bookmark with no position for URI"); - values.put(Bookmarks.POSITION, - Long.toString(BrowserContract.Bookmarks.DEFAULT_POSITION)); - } - - if (!values.containsKey(Bookmarks.TITLE)) { - // Desktop Places barfs on insertion of a bookmark with no title, - // so we don't store them that way. - values.put(Bookmarks.TITLE, ""); - } - - String url = values.getAsString(Bookmarks.URL); - - debug("Inserting bookmark in database with URL: " + url); - final SQLiteDatabase db = getWritableDatabase(uri); - beginWrite(db); - return db.insertOrThrow(TABLE_BOOKMARKS, Bookmarks.TITLE, values); - } - - - private int updateOrInsertBookmark(Uri uri, ContentValues values, String selection, - String[] selectionArgs) { - int updated = updateBookmarks(uri, values, selection, selectionArgs); - if (updated > 0) { - return updated; - } - - // Transaction already begun by updateBookmarks. - if (0 <= insertBookmark(uri, values)) { - // We 'updated' one row. - return 1; - } - - // If something went wrong, then we updated zero rows. - return 0; - } - - private int updateBookmarks(Uri uri, ContentValues values, String selection, - String[] selectionArgs) { - trace("Updating bookmarks on URI: " + uri); - - final String[] bookmarksProjection = new String[] { - Bookmarks._ID, // 0 - }; - - if (!values.containsKey(Bookmarks.DATE_MODIFIED)) { - values.put(Bookmarks.DATE_MODIFIED, System.currentTimeMillis()); - } - - trace("Querying bookmarks to update on URI: " + uri); - final SQLiteDatabase db = getWritableDatabase(uri); - - // Compute matching IDs. - final Cursor cursor = db.query(TABLE_BOOKMARKS, bookmarksProjection, - selection, selectionArgs, null, null, null); - - // Now that we're done reading, open a transaction. - final String inClause; - try { - inClause = DBUtils.computeSQLInClauseFromLongs(cursor, Bookmarks._ID); - } finally { - cursor.close(); - } - - beginWrite(db); - return db.update(TABLE_BOOKMARKS, values, inClause, null); - } - - private long insertHistory(Uri uri, ContentValues values) { - final long now = System.currentTimeMillis(); - values.put(History.DATE_CREATED, now); - values.put(History.DATE_MODIFIED, now); - - // Generate GUID for new history entry. Don't override specified GUIDs. - if (!values.containsKey(History.GUID)) { - values.put(History.GUID, Utils.generateGuid()); - } - - String url = values.getAsString(History.URL); - - debug("Inserting history in database with URL: " + url); - final SQLiteDatabase db = getWritableDatabase(uri); - beginWrite(db); - return db.insertOrThrow(TABLE_HISTORY, History.VISITS, values); - } - - private int updateOrInsertHistory(Uri uri, ContentValues values, String selection, - String[] selectionArgs) { - final int updated = updateHistory(uri, values, selection, selectionArgs); - if (updated > 0) { - return updated; - } - - // Insert a new entry if necessary, setting visit and date aggregate values. - if (!values.containsKey(History.VISITS)) { - values.put(History.VISITS, 1); - values.put(History.LOCAL_VISITS, 1); - } else { - values.put(History.LOCAL_VISITS, values.getAsInteger(History.VISITS)); - } - if (values.containsKey(History.DATE_LAST_VISITED)) { - values.put(History.LOCAL_DATE_LAST_VISITED, values.getAsLong(History.DATE_LAST_VISITED)); - } - if (!values.containsKey(History.TITLE)) { - values.put(History.TITLE, values.getAsString(History.URL)); - } - - if (0 <= insertHistory(uri, values)) { - return 1; - } - - return 0; - } - - private int updateHistory(Uri uri, ContentValues values, String selection, - String[] selectionArgs) { - trace("Updating history on URI: " + uri); - - final SQLiteDatabase db = getWritableDatabase(uri); - - if (!values.containsKey(History.DATE_MODIFIED)) { - values.put(History.DATE_MODIFIED, System.currentTimeMillis()); - } - - // Use the simple code path for easy updates. - if (!shouldIncrementVisits(uri) && !shouldIncrementRemoteAggregates(uri)) { - trace("Updating history meta data only"); - return db.update(TABLE_HISTORY, values, selection, selectionArgs); - } - - trace("Updating history meta data and incrementing visits"); - - if (values.containsKey(History.DATE_LAST_VISITED)) { - values.put(History.LOCAL_DATE_LAST_VISITED, values.getAsLong(History.DATE_LAST_VISITED)); - } - - // Create a separate set of values that will be updated as an expression. - final ContentValues visits = new ContentValues(); - if (shouldIncrementVisits(uri)) { - // Update data and increment visits by 1. - final long incVisits = 1; - - visits.put(History.VISITS, History.VISITS + " + " + incVisits); - visits.put(History.LOCAL_VISITS, History.LOCAL_VISITS + " + " + incVisits); - } - - if (shouldIncrementRemoteAggregates(uri)) { - // Let's fail loudly instead of trying to assume what users of this API meant to do. - if (!values.containsKey(History.REMOTE_VISITS)) { - throw new IllegalArgumentException( - "Tried incrementing History.REMOTE_VISITS by unknown value"); - } - visits.put( - History.REMOTE_VISITS, - History.REMOTE_VISITS + " + " + values.getAsInteger(History.REMOTE_VISITS) - ); - // Need to remove passed in value, so that we increment REMOTE_VISITS, and not just set it. - values.remove(History.REMOTE_VISITS); - } - - final ContentValues[] valuesAndVisits = { values, visits }; - final UpdateOperation[] ops = { UpdateOperation.ASSIGN, UpdateOperation.EXPRESSION }; - - return DBUtils.updateArrays(db, TABLE_HISTORY, valuesAndVisits, ops, selection, selectionArgs); - } - - private long insertVisitForHistory(Uri uri, ContentValues values, String selection, String[] selectionArgs) { - trace("Inserting visit for history on URI: " + uri); - - final SQLiteDatabase db = getReadableDatabase(uri); - - final Cursor cursor = db.query( - History.TABLE_NAME, new String[] {History.GUID}, selection, selectionArgs, - null, null, null); - if (cursor == null) { - Log.e(LOGTAG, "Null cursor while trying to insert visit for history URI: " + uri); - return 0; - } - final ContentValues[] visitValues; - try { - visitValues = new ContentValues[cursor.getCount()]; - - if (!cursor.moveToFirst()) { - Log.e(LOGTAG, "No history records found while inserting visit(s) for history URI: " + uri); - return 0; - } - - // Sync works in microseconds, so we store visit timestamps in microseconds as well. - // History timestamps are in milliseconds. - // This is the conversion point for locally generated visits. - final long visitDate; - if (values.containsKey(History.DATE_LAST_VISITED)) { - visitDate = values.getAsLong(History.DATE_LAST_VISITED) * 1000; - } else { - visitDate = System.currentTimeMillis() * 1000; - } - - final int guidColumn = cursor.getColumnIndexOrThrow(History.GUID); - while (!cursor.isAfterLast()) { - final ContentValues visit = new ContentValues(); - visit.put(Visits.HISTORY_GUID, cursor.getString(guidColumn)); - visit.put(Visits.DATE_VISITED, visitDate); - visitValues[cursor.getPosition()] = visit; - cursor.moveToNext(); - } - } finally { - cursor.close(); - } - - if (visitValues.length == 1) { - return insertVisit(Visits.CONTENT_URI, visitValues[0]); - } else { - return bulkInsert(Visits.CONTENT_URI, visitValues); - } - } - - private long insertVisit(Uri uri, ContentValues values) { - final SQLiteDatabase db = getWritableDatabase(uri); - - debug("Inserting history in database with URL: " + uri); - beginWrite(db); - - // We ignore insert conflicts here to simplify inserting visits records coming in from Sync. - // Visits table has a unique index on (history_guid,date), so a conflict might arise when we're - // trying to insert history record visits coming in from sync which are already present locally - // as a result of previous sync operations. - // An alternative to doing this is to filter out already present records when we're doing history inserts - // from Sync, which is a costly operation to do en masse. - return db.insertWithOnConflict( - TABLE_VISITS, null, values, SQLiteDatabase.CONFLICT_IGNORE); - } - - private void updateFaviconIdsForUrl(SQLiteDatabase db, String pageUrl, Long faviconId) { - ContentValues updateValues = new ContentValues(1); - updateValues.put(FaviconColumns.FAVICON_ID, faviconId); - db.update(TABLE_HISTORY, - updateValues, - History.URL + " = ?", - new String[] { pageUrl }); - db.update(TABLE_BOOKMARKS, - updateValues, - Bookmarks.URL + " = ?", - new String[] { pageUrl }); - } - - private long insertFavicon(Uri uri, ContentValues values) { - return insertFavicon(getWritableDatabase(uri), values); - } - - private long insertFavicon(SQLiteDatabase db, ContentValues values) { - String faviconUrl = values.getAsString(Favicons.URL); - String pageUrl = null; - - trace("Inserting favicon for URL: " + faviconUrl); - - DBUtils.stripEmptyByteArray(values, Favicons.DATA); - - // Extract the page URL from the ContentValues - if (values.containsKey(Favicons.PAGE_URL)) { - pageUrl = values.getAsString(Favicons.PAGE_URL); - values.remove(Favicons.PAGE_URL); - } - - // If no URL is provided, insert using the default one. - if (TextUtils.isEmpty(faviconUrl) && !TextUtils.isEmpty(pageUrl)) { - values.put(Favicons.URL, IconsHelper.guessDefaultFaviconURL(pageUrl)); - } - - final long now = System.currentTimeMillis(); - values.put(Favicons.DATE_CREATED, now); - values.put(Favicons.DATE_MODIFIED, now); - - beginWrite(db); - final long faviconId = db.insertOrThrow(TABLE_FAVICONS, null, values); - - if (pageUrl != null) { - updateFaviconIdsForUrl(db, pageUrl, faviconId); - } - return faviconId; - } - - private int updateOrInsertFavicon(Uri uri, ContentValues values, String selection, - String[] selectionArgs) { - return updateFavicon(uri, values, selection, selectionArgs, - true /* insert if needed */); - } - - private int updateExistingFavicon(Uri uri, ContentValues values, String selection, - String[] selectionArgs) { - return updateFavicon(uri, values, selection, selectionArgs, - false /* only update, no insert */); - } - - private int updateFavicon(Uri uri, ContentValues values, String selection, - String[] selectionArgs, boolean insertIfNeeded) { - String faviconUrl = values.getAsString(Favicons.URL); - String pageUrl = null; - int updated = 0; - Long faviconId = null; - long now = System.currentTimeMillis(); - - trace("Updating favicon for URL: " + faviconUrl); - - DBUtils.stripEmptyByteArray(values, Favicons.DATA); - - // Extract the page URL from the ContentValues - if (values.containsKey(Favicons.PAGE_URL)) { - pageUrl = values.getAsString(Favicons.PAGE_URL); - values.remove(Favicons.PAGE_URL); - } - - values.put(Favicons.DATE_MODIFIED, now); - - final SQLiteDatabase db = getWritableDatabase(uri); - - // If there's no favicon URL given and we're inserting if needed, skip - // the update and only do an insert (otherwise all rows would be - // updated). - if (!(insertIfNeeded && (faviconUrl == null))) { - updated = db.update(TABLE_FAVICONS, values, selection, selectionArgs); - } - - if (updated > 0) { - if ((faviconUrl != null) && (pageUrl != null)) { - final Cursor cursor = db.query(TABLE_FAVICONS, - new String[] { Favicons._ID }, - Favicons.URL + " = ?", - new String[] { faviconUrl }, - null, null, null); - try { - if (cursor.moveToFirst()) { - faviconId = cursor.getLong(cursor.getColumnIndexOrThrow(Favicons._ID)); - } - } finally { - cursor.close(); - } - } - if (pageUrl != null) { - beginWrite(db); - } - } else if (insertIfNeeded) { - values.put(Favicons.DATE_CREATED, now); - - trace("No update, inserting favicon for URL: " + faviconUrl); - beginWrite(db); - faviconId = db.insert(TABLE_FAVICONS, null, values); - updated = 1; - } - - if (pageUrl != null) { - updateFaviconIdsForUrl(db, pageUrl, faviconId); - } - - return updated; - } - - private long insertThumbnail(Uri uri, ContentValues values) { - final String url = values.getAsString(Thumbnails.URL); - - trace("Inserting thumbnail for URL: " + url); - - DBUtils.stripEmptyByteArray(values, Thumbnails.DATA); - - final SQLiteDatabase db = getWritableDatabase(uri); - beginWrite(db); - return db.insertOrThrow(TABLE_THUMBNAILS, null, values); - } - - private long insertActivityStreamBlocklistSite(final Uri uri, final ContentValues values) { - final String url = values.getAsString(ActivityStreamBlocklist.URL); - trace("Inserting url into highlights blocklist, URL: " + url); - - final SQLiteDatabase db = getWritableDatabase(uri); - values.put(ActivityStreamBlocklist.CREATED, System.currentTimeMillis()); - - beginWrite(db); - return db.insertOrThrow(TABLE_ACTIVITY_STREAM_BLOCKLIST, null, values); - } - - private long insertPageMetadata(final Uri uri, final ContentValues values) { - final SQLiteDatabase db = getWritableDatabase(uri); - - if (!values.containsKey(PageMetadata.DATE_CREATED)) { - values.put(PageMetadata.DATE_CREATED, System.currentTimeMillis()); - } - - beginWrite(db); - - // Perform INSERT OR REPLACE, there might be page metadata present and we want to replace it. - // Depends on a conflict arising from unique foreign key (history_guid) constraint violation. - return db.insertWithOnConflict( - TABLE_PAGE_METADATA, null, values, SQLiteDatabase.CONFLICT_REPLACE); - } - - private long insertUrlAnnotation(final Uri uri, final ContentValues values) { - final String url = values.getAsString(UrlAnnotations.URL); - trace("Inserting url annotations for URL: " + url); - - final SQLiteDatabase db = getWritableDatabase(uri); - beginWrite(db); - return db.insertOrThrow(TABLE_URL_ANNOTATIONS, null, values); - } - - private void deleteUrlAnnotation(final Uri uri, final String selection, final String[] selectionArgs) { - trace("Deleting url annotation for URI: " + uri); - - final SQLiteDatabase db = getWritableDatabase(uri); - db.delete(TABLE_URL_ANNOTATIONS, selection, selectionArgs); - } - - private int deletePageMetadata(final Uri uri, final String selection, final String[] selectionArgs) { - trace("Deleting page metadata for URI: " + uri); - - final SQLiteDatabase db = getWritableDatabase(uri); - return db.delete(TABLE_PAGE_METADATA, selection, selectionArgs); - } - - private void updateUrlAnnotation(final Uri uri, final ContentValues values, final String selection, final String[] selectionArgs) { - trace("Updating url annotation for URI: " + uri); - - final SQLiteDatabase db = getWritableDatabase(uri); - db.update(TABLE_URL_ANNOTATIONS, values, selection, selectionArgs); - } - - private int updateOrInsertThumbnail(Uri uri, ContentValues values, String selection, - String[] selectionArgs) { - return updateThumbnail(uri, values, selection, selectionArgs, - true /* insert if needed */); - } - - private int updateExistingThumbnail(Uri uri, ContentValues values, String selection, - String[] selectionArgs) { - return updateThumbnail(uri, values, selection, selectionArgs, - false /* only update, no insert */); - } - - private int updateThumbnail(Uri uri, ContentValues values, String selection, - String[] selectionArgs, boolean insertIfNeeded) { - final String url = values.getAsString(Thumbnails.URL); - DBUtils.stripEmptyByteArray(values, Thumbnails.DATA); - - trace("Updating thumbnail for URL: " + url); - - final SQLiteDatabase db = getWritableDatabase(uri); - beginWrite(db); - int updated = db.update(TABLE_THUMBNAILS, values, selection, selectionArgs); - - if (updated == 0 && insertIfNeeded) { - trace("No update, inserting thumbnail for URL: " + url); - db.insert(TABLE_THUMBNAILS, null, values); - updated = 1; - } - - return updated; - } - - /** - * This method does not create a new transaction. Its first operation is - * guaranteed to be a write, which in the case of a new enclosing - * transaction will guarantee that a read does not need to be upgraded to - * a write. - */ - private int deleteHistory(SQLiteDatabase db, Uri uri, String selection, String[] selectionArgs) { - debug("Deleting history entry for URI: " + uri); - - if (isCallerSync(uri)) { - return db.delete(TABLE_HISTORY, selection, selectionArgs); - } - - debug("Marking history entry as deleted for URI: " + uri); - - ContentValues values = new ContentValues(); - values.put(History.IS_DELETED, 1); - - // Wipe sensitive data. - values.putNull(History.TITLE); - values.put(History.URL, ""); // Column is NOT NULL. - values.put(History.DATE_CREATED, 0); - values.put(History.DATE_LAST_VISITED, 0); - values.put(History.VISITS, 0); - values.put(History.DATE_MODIFIED, System.currentTimeMillis()); - - // Doing this UPDATE (or the DELETE above) first ensures that the - // first operation within a new enclosing transaction is a write. - // The cleanup call below will do a SELECT first, and thus would - // require the transaction to be upgraded from a reader to a writer. - // In some cases that upgrade can fail (SQLITE_BUSY), so we avoid - // it if we can. - final int updated = db.update(TABLE_HISTORY, values, selection, selectionArgs); - try { - cleanUpSomeDeletedRecords(uri, TABLE_HISTORY); - } catch (Exception e) { - // We don't care. - Log.e(LOGTAG, "Unable to clean up deleted history records: ", e); - } - return updated; - } - - private ArrayList getHistoryGUIDsFromSelection(SQLiteDatabase db, Uri uri, String selection, String[] selectionArgs) { - final ArrayList historyGUIDs = new ArrayList<>(); - - final Cursor cursor = db.query( - History.TABLE_NAME, new String[] {History.GUID}, selection, selectionArgs, - null, null, null); - if (cursor == null) { - Log.e(LOGTAG, "Null cursor while trying to delete visits for history URI: " + uri); - return historyGUIDs; - } - - try { - if (!cursor.moveToFirst()) { - trace("No history items for which to remove visits matched for URI: " + uri); - return historyGUIDs; - } - final int historyColumn = cursor.getColumnIndexOrThrow(History.GUID); - while (!cursor.isAfterLast()) { - historyGUIDs.add(cursor.getString(historyColumn)); - cursor.moveToNext(); - } - } finally { - cursor.close(); - } - - return historyGUIDs; - } - - private int deletePageMetadataForHistory(SQLiteDatabase db, ArrayList historyGUIDs) { - return bulkDeleteByHistoryGUID(db, historyGUIDs, PageMetadata.TABLE_NAME, PageMetadata.HISTORY_GUID); - } - - private int deleteVisitsForHistory(SQLiteDatabase db, ArrayList historyGUIDs) { - return bulkDeleteByHistoryGUID(db, historyGUIDs, Visits.TABLE_NAME, Visits.HISTORY_GUID); - } - - private int bulkDeleteByHistoryGUID(SQLiteDatabase db, ArrayList historyGUIDs, String table, String historyGUIDColumn) { - // Due to SQLite's maximum variable limitation, we need to chunk our delete statements. - // For example, if there were 1200 GUIDs, this will perform 2 delete statements. - int deleted = 0; - for (int chunk = 0; chunk <= historyGUIDs.size() / DBUtils.SQLITE_MAX_VARIABLE_NUMBER; chunk++) { - final int chunkStart = chunk * DBUtils.SQLITE_MAX_VARIABLE_NUMBER; - int chunkEnd = (chunk + 1) * DBUtils.SQLITE_MAX_VARIABLE_NUMBER; - if (chunkEnd > historyGUIDs.size()) { - chunkEnd = historyGUIDs.size(); - } - final List chunkGUIDs = historyGUIDs.subList(chunkStart, chunkEnd); - deleted += db.delete( - table, - DBUtils.computeSQLInClause(chunkGUIDs.size(), historyGUIDColumn), - chunkGUIDs.toArray(new String[chunkGUIDs.size()]) - ); - } - - return deleted; - } - - private int deleteVisits(Uri uri, String selection, String[] selectionArgs) { - debug("Deleting visits for URI: " + uri); - - final SQLiteDatabase db = getWritableDatabase(uri); - - beginWrite(db); - return db.delete(TABLE_VISITS, selection, selectionArgs); - } - - private int deleteBookmarks(Uri uri, String selection, String[] selectionArgs) { - debug("Deleting bookmarks for URI: " + uri); - - final SQLiteDatabase db = getWritableDatabase(uri); - - if (isCallerSync(uri)) { - beginWrite(db); - return db.delete(TABLE_BOOKMARKS, selection, selectionArgs); - } - - debug("Marking bookmarks as deleted for URI: " + uri); - - ContentValues values = new ContentValues(); - values.put(Bookmarks.IS_DELETED, 1); - values.put(Bookmarks.POSITION, 0); - values.putNull(Bookmarks.PARENT); - values.putNull(Bookmarks.URL); - values.putNull(Bookmarks.TITLE); - values.putNull(Bookmarks.DESCRIPTION); - values.putNull(Bookmarks.KEYWORD); - values.putNull(Bookmarks.TAGS); - values.putNull(Bookmarks.FAVICON_ID); - - // Doing this UPDATE (or the DELETE above) first ensures that the - // first operation within this transaction is a write. - // The cleanup call below will do a SELECT first, and thus would - // require the transaction to be upgraded from a reader to a writer. - final int updated = updateBookmarks(uri, values, selection, selectionArgs); - try { - cleanUpSomeDeletedRecords(uri, TABLE_BOOKMARKS); - } catch (Exception e) { - // We don't care. - Log.e(LOGTAG, "Unable to clean up deleted bookmark records: ", e); - } - return updated; - } - - private int deleteFavicons(Uri uri, String selection, String[] selectionArgs) { - debug("Deleting favicons for URI: " + uri); - - final SQLiteDatabase db = getWritableDatabase(uri); - - return db.delete(TABLE_FAVICONS, selection, selectionArgs); - } - - private int deleteThumbnails(Uri uri, String selection, String[] selectionArgs) { - debug("Deleting thumbnails for URI: " + uri); - - final SQLiteDatabase db = getWritableDatabase(uri); - - return db.delete(TABLE_THUMBNAILS, selection, selectionArgs); - } - - private int deleteUnusedImages(Uri uri) { - debug("Deleting all unused favicons and thumbnails for URI: " + uri); - - String faviconSelection = Favicons._ID + " NOT IN " - + "(SELECT " + History.FAVICON_ID - + " FROM " + TABLE_HISTORY - + " WHERE " + History.IS_DELETED + " = 0" - + " AND " + History.FAVICON_ID + " IS NOT NULL" - + " UNION ALL SELECT " + Bookmarks.FAVICON_ID - + " FROM " + TABLE_BOOKMARKS - + " WHERE " + Bookmarks.IS_DELETED + " = 0" - + " AND " + Bookmarks.FAVICON_ID + " IS NOT NULL)"; - - String thumbnailSelection = Thumbnails.URL + " NOT IN " - + "(SELECT " + History.URL - + " FROM " + TABLE_HISTORY - + " WHERE " + History.IS_DELETED + " = 0" - + " AND " + History.URL + " IS NOT NULL" - + " UNION ALL SELECT " + Bookmarks.URL - + " FROM " + TABLE_BOOKMARKS - + " WHERE " + Bookmarks.IS_DELETED + " = 0" - + " AND " + Bookmarks.URL + " IS NOT NULL)"; - - return deleteFavicons(uri, faviconSelection, null) + - deleteThumbnails(uri, thumbnailSelection, null) + - getURLMetadataTable().deleteUnused(getWritableDatabase(uri)); - } - - @Override - public ContentProviderResult[] applyBatch (ArrayList operations) - throws OperationApplicationException { - final int numOperations = operations.size(); - final ContentProviderResult[] results = new ContentProviderResult[numOperations]; - - if (numOperations < 1) { - debug("applyBatch: no operations; returning immediately."); - // The original Android implementation returns a zero-length - // array in this case. We do the same. - return results; - } - - boolean failures = false; - - // We only have 1 database for all Uris that we can get. - SQLiteDatabase db = getWritableDatabase(operations.get(0).getUri()); - - // Note that the apply() call may cause us to generate - // additional transactions for the individual operations. - // But Android's wrapper for SQLite supports nested transactions, - // so this will do the right thing. - // - // Note further that in some circumstances this can result in - // exceptions: if this transaction is first involved in reading, - // and then (naturally) tries to perform writes, SQLITE_BUSY can - // be raised. See Bug 947939 and friends. - beginBatch(db); - - for (int i = 0; i < numOperations; i++) { - try { - final ContentProviderOperation operation = operations.get(i); - results[i] = operation.apply(this, results, i); - } catch (SQLException e) { - Log.w(LOGTAG, "SQLite Exception during applyBatch.", e); - // The Android API makes it implementation-defined whether - // the failure of a single operation makes all others abort - // or not. For our use cases, best-effort operation makes - // more sense. Rolling back and forcing the caller to retry - // after it figures out what went wrong isn't very convenient - // anyway. - // Signal failed operation back, so the caller knows what - // went through and what didn't. - results[i] = new ContentProviderResult(0); - failures = true; - // http://www.sqlite.org/lang_conflict.html - // Note that we need a new transaction, subsequent operations - // on this one will fail (we're in ABORT by default, which - // isn't IGNORE). We still need to set it as successful to let - // everything before the failed op go through. - // We can't set conflict resolution on API level < 8, and even - // above 8 it requires splitting the call per operation - // (insert/update/delete). - db.setTransactionSuccessful(); - db.endTransaction(); - db.beginTransaction(); - } catch (OperationApplicationException e) { - // Repeat of above. - results[i] = new ContentProviderResult(0); - failures = true; - db.setTransactionSuccessful(); - db.endTransaction(); - db.beginTransaction(); - } - } - - trace("Flushing DB applyBatch..."); - markBatchSuccessful(db); - endBatch(db); - - if (failures) { - throw new OperationApplicationException(); - } - - return results; - } - - private static Table findTableFor(int id) { - for (Table table : sTables) { - for (Table.ContentProviderInfo type : table.getContentProviderInfo()) { - if (type.id == id) { - return table; - } - } - } - return null; - } - - private static void addTablesToMatcher(Table[] tables, final UriMatcher matcher) { - } - - private static String getContentItemType(final int match) { - for (Table table : sTables) { - for (Table.ContentProviderInfo type : table.getContentProviderInfo()) { - if (type.id == match) { - return "vnd.android.cursor.item/" + type.name; - } - } - } - - return null; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/db/DBUtils.java b/mobile/android/base/java/org/mozilla/gecko/db/DBUtils.java deleted file mode 100644 index cfa2f870fb..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/db/DBUtils.java +++ /dev/null @@ -1,450 +0,0 @@ -/* 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.db; - -import android.annotation.TargetApi; -import android.database.DatabaseUtils; -import android.database.sqlite.SQLiteDatabase; -import android.database.sqlite.SQLiteStatement; -import android.os.Build; -import org.mozilla.gecko.AppConstants; -import org.mozilla.gecko.GeckoAppShell; -import org.mozilla.gecko.GeckoProfile; - -import android.content.ContentValues; -import android.database.Cursor; -import android.database.sqlite.SQLiteOpenHelper; -import android.net.Uri; -import android.text.TextUtils; -import android.util.Log; -import org.mozilla.gecko.annotation.RobocopTarget; -import org.mozilla.gecko.Telemetry; - -import java.util.Map; - -public class DBUtils { - private static final String LOGTAG = "GeckoDBUtils"; - - public static final int SQLITE_MAX_VARIABLE_NUMBER = 999; - - public static final String qualifyColumn(String table, String column) { - return table + "." + column; - } - - // This is available in Android >= 11. Implemented locally to be - // compatible with older versions. - public static String concatenateWhere(String a, String b) { - if (TextUtils.isEmpty(a)) { - return b; - } - - if (TextUtils.isEmpty(b)) { - return a; - } - - return "(" + a + ") AND (" + b + ")"; - } - - // This is available in Android >= 11. Implemented locally to be - // compatible with older versions. - public static String[] appendSelectionArgs(String[] originalValues, String[] newValues) { - if (originalValues == null || originalValues.length == 0) { - return newValues; - } - - if (newValues == null || newValues.length == 0) { - return originalValues; - } - - String[] result = new String[originalValues.length + newValues.length]; - System.arraycopy(originalValues, 0, result, 0, originalValues.length); - System.arraycopy(newValues, 0, result, originalValues.length, newValues.length); - - return result; - } - - /** - * Concatenate multiple lists of selection arguments. values may be null. - */ - public static String[] concatenateSelectionArgs(String[]... values) { - // Since we're most likely to be concatenating a few arrays of many values, it is most - // efficient to iterate over the arrays once to obtain their lengths, allowing us to create one target array - // (as opposed to copying arrays on every iteration, which would result in many more copies). - int totalLength = 0; - for (String[] v : values) { - if (v != null) { - totalLength += v.length; - } - } - - String[] result = new String[totalLength]; - - int position = 0; - for (String[] v: values) { - if (v != null) { - int currentLength = v.length; - System.arraycopy(v, 0, result, position, currentLength); - position += currentLength; - } - } - - return result; - } - - public static void replaceKey(ContentValues aValues, String aOriginalKey, - String aNewKey, String aDefault) { - String value = aDefault; - if (aOriginalKey != null && aValues.containsKey(aOriginalKey)) { - value = aValues.get(aOriginalKey).toString(); - aValues.remove(aOriginalKey); - } - - if (!aValues.containsKey(aNewKey)) { - aValues.put(aNewKey, value); - } - } - - private static String HISTOGRAM_DATABASE_LOCKED = "DATABASE_LOCKED_EXCEPTION"; - private static String HISTOGRAM_DATABASE_UNLOCKED = "DATABASE_SUCCESSFUL_UNLOCK"; - public static void ensureDatabaseIsNotLocked(SQLiteOpenHelper dbHelper, String databasePath) { - final int maxAttempts = 5; - int attempt = 0; - SQLiteDatabase db = null; - for (; attempt < maxAttempts; attempt++) { - try { - // Try a simple test and exit the loop. - db = dbHelper.getWritableDatabase(); - break; - } catch (Exception e) { - // We assume that this is a android.database.sqlite.SQLiteDatabaseLockedException. - // That class is only available on API 11+. - Telemetry.addToHistogram(HISTOGRAM_DATABASE_LOCKED, attempt); - - // Things could get very bad if we don't find a way to unlock the DB. - Log.d(LOGTAG, "Database is locked, trying to kill any zombie processes: " + databasePath); - GeckoAppShell.killAnyZombies(); - try { - Thread.sleep(attempt * 100); - } catch (InterruptedException ie) { - } - } - } - - if (db == null) { - Log.w(LOGTAG, "Failed to unlock database."); - GeckoAppShell.listOfOpenFiles(); - return; - } - - // If we needed to retry, but we succeeded, report that in telemetry. - // Failures are indicated by a lower frequency of UNLOCKED than LOCKED. - if (attempt > 1) { - Telemetry.addToHistogram(HISTOGRAM_DATABASE_UNLOCKED, attempt - 1); - } - } - - /** - * Copies a table between database files. - * - * This method assumes that the source table and destination table already exist in the - * source and destination databases, respectively. - * - * The table is copied row-by-row in a single transaction. - * - * @param source The source database that the table will be copied from. - * @param sourceTableName The name of the source table. - * @param destination The destination database that the table will be copied to. - * @param destinationTableName The name of the destination table. - * @return true if all rows were copied; false otherwise. - */ - public static boolean copyTable(SQLiteDatabase source, String sourceTableName, - SQLiteDatabase destination, String destinationTableName) { - Cursor cursor = null; - try { - destination.beginTransaction(); - - cursor = source.query(sourceTableName, null, null, null, null, null, null); - Log.d(LOGTAG, "Trying to copy " + cursor.getCount() + " rows from " + sourceTableName + " to " + destinationTableName); - - final ContentValues contentValues = new ContentValues(); - while (cursor.moveToNext()) { - contentValues.clear(); - DatabaseUtils.cursorRowToContentValues(cursor, contentValues); - destination.insert(destinationTableName, null, contentValues); - } - - destination.setTransactionSuccessful(); - Log.d(LOGTAG, "Successfully copied " + cursor.getCount() + " rows from " + sourceTableName + " to " + destinationTableName); - return true; - } catch (Exception e) { - Log.w(LOGTAG, "Got exception copying rows from " + sourceTableName + " to " + destinationTableName + "; ignoring.", e); - return false; - } finally { - destination.endTransaction(); - if (cursor != null) { - cursor.close(); - } - } - } - - /** - * Verifies that 0-byte arrays aren't added as favicon or thumbnail data. - * @param values ContentValues of query - * @param columnName Name of data column to verify - */ - public static void stripEmptyByteArray(ContentValues values, String columnName) { - if (values.containsKey(columnName)) { - byte[] data = values.getAsByteArray(columnName); - if (data == null || data.length == 0) { - Log.w(LOGTAG, "Tried to insert an empty or non-byte-array image. Ignoring."); - values.putNull(columnName); - } - } - } - - /** - * Builds a selection string that searches for a list of arguments in a particular column. - * For example URL in (?,?,?). Callers should pass the actual arguments into their query - * as selection args. - * @para columnName The column to search in - * @para size The number of arguments to search for - */ - public static String computeSQLInClause(int items, String field) { - final StringBuilder builder = new StringBuilder(field); - builder.append(" IN ("); - int i = 0; - for (; i < items - 1; ++i) { - builder.append("?, "); - } - if (i < items) { - builder.append("?"); - } - builder.append(")"); - return builder.toString(); - } - - /** - * Turn a single-column cursor of longs into a single SQL "IN" clause. - * We can do this without using selection arguments because Long isn't - * vulnerable to injection. - */ - public static String computeSQLInClauseFromLongs(final Cursor cursor, String field) { - final StringBuilder builder = new StringBuilder(field); - builder.append(" IN ("); - final int commaLimit = cursor.getCount() - 1; - int i = 0; - while (cursor.moveToNext()) { - builder.append(cursor.getLong(0)); - if (i++ < commaLimit) { - builder.append(", "); - } - } - builder.append(")"); - return builder.toString(); - } - - public static Uri appendProfile(final String profile, final Uri uri) { - return uri.buildUpon().appendQueryParameter(BrowserContract.PARAM_PROFILE, profile).build(); - } - - public static Uri appendProfileWithDefault(final String profile, final Uri uri) { - if (profile == null) { - return appendProfile(GeckoProfile.DEFAULT_PROFILE, uri); - } - return appendProfile(profile, uri); - } - - /** - * Use the following when no conflict action is specified. - */ - private static final int CONFLICT_NONE = 0; - private static final String[] CONFLICT_VALUES = new String[] {"", " OR ROLLBACK ", " OR ABORT ", " OR FAIL ", " OR IGNORE ", " OR REPLACE "}; - - /** - * Convenience method for updating rows in the database. - * - * @param table the table to update in - * @param values a map from column names to new column values. null is a - * valid value that will be translated to NULL. - * @param whereClause the optional WHERE clause to apply when updating. - * Passing null will update all rows. - * @param whereArgs You may include ?s in the where clause, which - * will be replaced by the values from whereArgs. The values - * will be bound as Strings. - * @return the number of rows affected - */ - @RobocopTarget - public static int updateArrays(SQLiteDatabase db, String table, ContentValues[] values, UpdateOperation[] ops, String whereClause, String[] whereArgs) { - return updateArraysWithOnConflict(db, table, values, ops, whereClause, whereArgs, CONFLICT_NONE, true); - } - - public static void updateArraysBlindly(SQLiteDatabase db, String table, ContentValues[] values, UpdateOperation[] ops, String whereClause, String[] whereArgs) { - updateArraysWithOnConflict(db, table, values, ops, whereClause, whereArgs, CONFLICT_NONE, false); - } - - @RobocopTarget - public enum UpdateOperation { - /** - * ASSIGN is the usual update: replaces the value in the named column with the provided value. - * - * foo = ? - */ - ASSIGN, - - /** - * BITWISE_OR applies the provided value to the existing value with a bitwise OR. This is useful for adding to flags. - * - * foo |= ? - */ - BITWISE_OR, - - /** - * EXPRESSION is an end-run around the API: it allows callers to specify a fragment of SQL to splice into the - * SET part of the query. - * - * foo = $value - * - * Be very careful not to use user input in this. - */ - EXPRESSION, - } - - /** - * This is an evil reimplementation of SQLiteDatabase's methods to allow for - * smarter updating. - * - * Each ContentValues has an associated enum that describes how to unify input values with the existing column values. - */ - private static int updateArraysWithOnConflict(SQLiteDatabase db, String table, - ContentValues[] values, - UpdateOperation[] ops, - String whereClause, - String[] whereArgs, - int conflictAlgorithm, - boolean returnChangedRows) { - if (values == null || values.length == 0) { - throw new IllegalArgumentException("Empty values"); - } - - if (ops == null || ops.length != values.length) { - throw new IllegalArgumentException("ops and values don't match"); - } - - StringBuilder sql = new StringBuilder(120); - sql.append("UPDATE "); - sql.append(CONFLICT_VALUES[conflictAlgorithm]); - sql.append(table); - sql.append(" SET "); - - // move all bind args to one array - int setValuesSize = 0; - for (int i = 0; i < values.length; i++) { - // EXPRESSION types don't contribute any placeholders. - if (ops[i] != UpdateOperation.EXPRESSION) { - setValuesSize += values[i].size(); - } - } - - int bindArgsSize = (whereArgs == null) ? setValuesSize : (setValuesSize + whereArgs.length); - Object[] bindArgs = new Object[bindArgsSize]; - - int arg = 0; - for (int i = 0; i < values.length; i++) { - final ContentValues v = values[i]; - final UpdateOperation op = ops[i]; - - // Alas, code duplication. - switch (op) { - case ASSIGN: - for (Map.Entry entry : v.valueSet()) { - final String colName = entry.getKey(); - sql.append((arg > 0) ? "," : ""); - sql.append(colName); - bindArgs[arg++] = entry.getValue(); - sql.append("= ?"); - } - break; - case BITWISE_OR: - for (Map.Entry entry : v.valueSet()) { - final String colName = entry.getKey(); - sql.append((arg > 0) ? "," : ""); - sql.append(colName); - bindArgs[arg++] = entry.getValue(); - sql.append("= ? | "); - sql.append(colName); - } - break; - case EXPRESSION: - // Treat each value as a literal SQL string. - for (Map.Entry entry : v.valueSet()) { - final String colName = entry.getKey(); - sql.append((arg > 0) ? "," : ""); - sql.append(colName); - sql.append(" = "); - sql.append(entry.getValue()); - } - break; - } - } - - if (whereArgs != null) { - for (arg = setValuesSize; arg < bindArgsSize; arg++) { - bindArgs[arg] = whereArgs[arg - setValuesSize]; - } - } - if (!TextUtils.isEmpty(whereClause)) { - sql.append(" WHERE "); - sql.append(whereClause); - } - - // What a huge pain in the ass, all because SQLiteDatabase doesn't expose .executeSql, - // and we can't get a DB handle. Nor can we easily construct a statement with arguments - // already bound. - final SQLiteStatement statement = db.compileStatement(sql.toString()); - try { - bindAllArgs(statement, bindArgs); - if (!returnChangedRows) { - statement.execute(); - return 0; - } - // This is a separate method so we can annotate it with @TargetApi. - return executeStatementReturningChangedRows(statement); - } finally { - statement.close(); - } - } - - @TargetApi(Build.VERSION_CODES.HONEYCOMB) - private static int executeStatementReturningChangedRows(SQLiteStatement statement) { - return statement.executeUpdateDelete(); - } - - // All because {@link SQLiteProgram#bind(integer, Object)} is private. - private static void bindAllArgs(SQLiteStatement statement, Object[] bindArgs) { - if (bindArgs == null) { - return; - } - for (int i = bindArgs.length; i != 0; i--) { - Object v = bindArgs[i - 1]; - if (v == null) { - statement.bindNull(i); - } else if (v instanceof String) { - statement.bindString(i, (String) v); - } else if (v instanceof Double) { - statement.bindDouble(i, (Double) v); - } else if (v instanceof Float) { - statement.bindDouble(i, (Float) v); - } else if (v instanceof Long) { - statement.bindLong(i, (Long) v); - } else if (v instanceof Integer) { - statement.bindLong(i, (Integer) v); - } else if (v instanceof Byte) { - statement.bindLong(i, (Byte) v); - } else if (v instanceof byte[]) { - statement.bindBlob(i, (byte[]) v); - } - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/db/FormHistoryProvider.java b/mobile/android/base/java/org/mozilla/gecko/db/FormHistoryProvider.java deleted file mode 100644 index ff2f5238e6..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/db/FormHistoryProvider.java +++ /dev/null @@ -1,166 +0,0 @@ -/* 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.db; - -import java.lang.IllegalArgumentException; -import java.util.HashMap; - -import org.mozilla.gecko.GeckoAppShell; -import org.mozilla.gecko.db.BrowserContract.FormHistory; -import org.mozilla.gecko.db.BrowserContract.DeletedFormHistory; -import org.mozilla.gecko.db.BrowserContract; -import org.mozilla.gecko.sqlite.SQLiteBridge; -import org.mozilla.gecko.sync.Utils; - -import android.content.ContentValues; -import android.content.UriMatcher; -import android.database.Cursor; -import android.net.Uri; -import android.text.TextUtils; - -public class FormHistoryProvider extends SQLiteBridgeContentProvider { - static final String TABLE_FORM_HISTORY = "moz_formhistory"; - static final String TABLE_DELETED_FORM_HISTORY = "moz_deleted_formhistory"; - - private static final int FORM_HISTORY = 100; - private static final int DELETED_FORM_HISTORY = 101; - - private static final UriMatcher URI_MATCHER; - - - // This should be kept in sync with the db version in toolkit/components/satchel/nsFormHistory.js - private static final int DB_VERSION = 4; - private static final String DB_FILENAME = "formhistory.sqlite"; - private static final String TELEMETRY_TAG = "SQLITEBRIDGE_PROVIDER_FORMS"; - - private static final String WHERE_GUID_IS_NULL = BrowserContract.DeletedFormHistory.GUID + " IS NULL"; - private static final String WHERE_GUID_IS_VALUE = BrowserContract.DeletedFormHistory.GUID + " = ?"; - - private static final String LOG_TAG = "FormHistoryProvider"; - - static { - URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH); - URI_MATCHER.addURI(BrowserContract.FORM_HISTORY_AUTHORITY, "formhistory", FORM_HISTORY); - URI_MATCHER.addURI(BrowserContract.FORM_HISTORY_AUTHORITY, "deleted-formhistory", DELETED_FORM_HISTORY); - } - - public FormHistoryProvider() { - super(LOG_TAG); - } - - - @Override - public String getType(Uri uri) { - final int match = URI_MATCHER.match(uri); - - switch (match) { - case FORM_HISTORY: - return FormHistory.CONTENT_TYPE; - - case DELETED_FORM_HISTORY: - return DeletedFormHistory.CONTENT_TYPE; - - default: - throw new UnsupportedOperationException("Unknown type " + uri); - } - } - - @Override - public String getTable(Uri uri) { - String table = null; - final int match = URI_MATCHER.match(uri); - switch (match) { - case DELETED_FORM_HISTORY: - table = TABLE_DELETED_FORM_HISTORY; - break; - - case FORM_HISTORY: - table = TABLE_FORM_HISTORY; - break; - - default: - throw new UnsupportedOperationException("Unknown table " + uri); - } - return table; - } - - @Override - public String getSortOrder(Uri uri, String aRequested) { - if (!TextUtils.isEmpty(aRequested)) { - return aRequested; - } - - return null; - } - - @Override - public void setupDefaults(Uri uri, ContentValues values) { - int match = URI_MATCHER.match(uri); - long now = System.currentTimeMillis(); - - switch (match) { - case DELETED_FORM_HISTORY: - values.put(DeletedFormHistory.TIME_DELETED, now); - - // Deleted entries must contain a guid - if (!values.containsKey(FormHistory.GUID)) { - throw new IllegalArgumentException("Must provide a GUID for a deleted form history"); - } - break; - - case FORM_HISTORY: - // Generate GUID for new entry. Don't override specified GUIDs. - if (!values.containsKey(FormHistory.GUID)) { - String guid = Utils.generateGuid(); - values.put(FormHistory.GUID, guid); - } - break; - - default: - throw new UnsupportedOperationException("Unknown insert URI " + uri); - } - } - - @Override - public void initGecko() { - GeckoAppShell.notifyObservers("FormHistory:Init", null); - } - - @Override - public void onPreInsert(ContentValues values, Uri uri, SQLiteBridge db) { - if (!values.containsKey(FormHistory.GUID)) { - return; - } - - String guid = values.getAsString(FormHistory.GUID); - if (guid == null) { - db.delete(TABLE_DELETED_FORM_HISTORY, WHERE_GUID_IS_NULL, null); - return; - } - String[] args = new String[] { guid }; - db.delete(TABLE_DELETED_FORM_HISTORY, WHERE_GUID_IS_VALUE, args); - } - - @Override - public void onPreUpdate(ContentValues values, Uri uri, SQLiteBridge db) { } - - @Override - public void onPostQuery(Cursor cursor, Uri uri, SQLiteBridge db) { } - - @Override - protected String getDBName() { - return DB_FILENAME; - } - - @Override - protected String getTelemetryPrefix() { - return TELEMETRY_TAG; - } - - @Override - protected int getDBVersion() { - return DB_VERSION; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/db/HomeProvider.java b/mobile/android/base/java/org/mozilla/gecko/db/HomeProvider.java deleted file mode 100644 index 1a241f9dac..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/db/HomeProvider.java +++ /dev/null @@ -1,194 +0,0 @@ -/* 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.db; - -import java.io.IOException; - -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; -import org.mozilla.gecko.R; -import org.mozilla.gecko.db.BrowserContract.HomeItems; -import org.mozilla.gecko.db.DBUtils; -import org.mozilla.gecko.sqlite.SQLiteBridge; -import org.mozilla.gecko.util.RawResource; - -import android.content.ContentResolver; -import android.content.ContentValues; -import android.content.UriMatcher; -import android.database.Cursor; -import android.database.MatrixCursor; -import android.net.Uri; -import android.util.Log; - -public class HomeProvider extends SQLiteBridgeContentProvider { - private static final String LOGTAG = "GeckoHomeProvider"; - - // This should be kept in sync with the db version in mobile/android/modules/HomeProvider.jsm - private static final int DB_VERSION = 3; - private static final String DB_FILENAME = "home.sqlite"; - private static final String TELEMETRY_TAG = "SQLITEBRIDGE_PROVIDER_HOME"; - - private static final String TABLE_ITEMS = "items"; - - // Endpoint to return static fake data. - static final int ITEMS_FAKE = 100; - static final int ITEMS = 101; - static final int ITEMS_ID = 102; - - static final UriMatcher URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH); - - static { - URI_MATCHER.addURI(BrowserContract.HOME_AUTHORITY, "items/fake", ITEMS_FAKE); - URI_MATCHER.addURI(BrowserContract.HOME_AUTHORITY, "items", ITEMS); - URI_MATCHER.addURI(BrowserContract.HOME_AUTHORITY, "items/#", ITEMS_ID); - } - - public HomeProvider() { - super(LOGTAG); - } - - @Override - public String getType(Uri uri) { - final int match = URI_MATCHER.match(uri); - - switch (match) { - case ITEMS_FAKE: { - return HomeItems.CONTENT_TYPE; - } - case ITEMS: { - return HomeItems.CONTENT_TYPE; - } - default: { - throw new UnsupportedOperationException("Unknown type " + uri); - } - } - } - - @Override - public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { - final int match = URI_MATCHER.match(uri); - - // If we're querying the fake items, don't try to get the database. - if (match == ITEMS_FAKE) { - return queryFakeItems(uri, projection, selection, selectionArgs, sortOrder); - } - - final String datasetId = uri.getQueryParameter(BrowserContract.PARAM_DATASET_ID); - if (datasetId == null) { - throw new IllegalArgumentException("All queries should contain a dataset ID parameter"); - } - - selection = DBUtils.concatenateWhere(selection, HomeItems.DATASET_ID + " = ?"); - selectionArgs = DBUtils.appendSelectionArgs(selectionArgs, - new String[] { datasetId }); - - // Otherwise, let the SQLiteContentProvider implementation take care of this query for us! - Cursor c = super.query(uri, projection, selection, selectionArgs, sortOrder); - - // SQLiteBridgeContentProvider may return a null Cursor if the database hasn't been created yet. - // However, we need a non-null cursor in order to listen for notifications. - if (c == null) { - c = new MatrixCursor(projection != null ? projection : HomeItems.DEFAULT_PROJECTION); - } - - final ContentResolver cr = getContext().getContentResolver(); - c.setNotificationUri(cr, getDatasetNotificationUri(datasetId)); - - return c; - } - - /** - * Returns a cursor populated with static fake data. - */ - private Cursor queryFakeItems(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { - JSONArray items = null; - try { - final String jsonString = RawResource.getAsString(getContext(), R.raw.fake_home_items); - items = new JSONArray(jsonString); - } catch (IOException e) { - Log.e(LOGTAG, "Error getting fake home items", e); - return null; - } catch (JSONException e) { - Log.e(LOGTAG, "Error parsing fake_home_items.json", e); - return null; - } - - final MatrixCursor c = new MatrixCursor(HomeItems.DEFAULT_PROJECTION); - for (int i = 0; i < items.length(); i++) { - try { - final JSONObject item = items.getJSONObject(i); - c.addRow(new Object[] { - item.getInt("id"), - item.getString("dataset_id"), - item.getString("url"), - item.getString("title"), - item.getString("description"), - item.getString("image_url"), - item.getString("filter") - }); - } catch (JSONException e) { - Log.e(LOGTAG, "Error creating cursor row for fake home item", e); - } - } - return c; - } - - /** - * SQLiteBridgeContentProvider implementation - */ - - @Override - protected String getDBName() { - return DB_FILENAME; - } - - @Override - protected String getTelemetryPrefix() { - return TELEMETRY_TAG; - } - - @Override - protected int getDBVersion() { - return DB_VERSION; - } - - @Override - public String getTable(Uri uri) { - final int match = URI_MATCHER.match(uri); - switch (match) { - case ITEMS: { - return TABLE_ITEMS; - } - default: { - throw new UnsupportedOperationException("Unknown table " + uri); - } - } - } - - @Override - public String getSortOrder(Uri uri, String aRequested) { - return null; - } - - @Override - public void setupDefaults(Uri uri, ContentValues values) { } - - @Override - public void initGecko() { } - - @Override - public void onPreInsert(ContentValues values, Uri uri, SQLiteBridge db) { } - - @Override - public void onPreUpdate(ContentValues values, Uri uri, SQLiteBridge db) { } - - @Override - public void onPostQuery(Cursor cursor, Uri uri, SQLiteBridge db) { } - - public static Uri getDatasetNotificationUri(String datasetId) { - return Uri.withAppendedPath(HomeItems.CONTENT_URI, datasetId); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/db/LocalBrowserDB.java b/mobile/android/base/java/org/mozilla/gecko/db/LocalBrowserDB.java deleted file mode 100644 index 8c219282fc..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/db/LocalBrowserDB.java +++ /dev/null @@ -1,1938 +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.db; - -import java.io.ByteArrayOutputStream; -import java.io.InputStream; -import java.lang.IllegalAccessException; -import java.lang.NoSuchFieldException; -import java.lang.reflect.Array; -import java.lang.reflect.Field; -import java.util.ArrayList; -import java.util.Collection; -import java.util.EnumSet; -import java.util.HashMap; -import java.util.List; -import java.util.Locale; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; -import org.mozilla.gecko.AppConstants; -import org.mozilla.gecko.Telemetry; -import org.mozilla.gecko.annotation.RobocopTarget; -import org.mozilla.gecko.R; -import org.mozilla.gecko.db.BrowserContract.ActivityStreamBlocklist; -import org.mozilla.gecko.db.BrowserContract.Bookmarks; -import org.mozilla.gecko.db.BrowserContract.Combined; -import org.mozilla.gecko.db.BrowserContract.ExpirePriority; -import org.mozilla.gecko.db.BrowserContract.Favicons; -import org.mozilla.gecko.db.BrowserContract.History; -import org.mozilla.gecko.db.BrowserContract.SyncColumns; -import org.mozilla.gecko.db.BrowserContract.Thumbnails; -import org.mozilla.gecko.db.BrowserContract.TopSites; -import org.mozilla.gecko.db.BrowserContract.Highlights; -import org.mozilla.gecko.db.BrowserContract.PageMetadata; -import org.mozilla.gecko.distribution.Distribution; -import org.mozilla.gecko.icons.decoders.FaviconDecoder; -import org.mozilla.gecko.icons.decoders.LoadFaviconResult; -import org.mozilla.gecko.gfx.BitmapUtils; -import org.mozilla.gecko.restrictions.Restrictions; -import org.mozilla.gecko.sync.Utils; -import org.mozilla.gecko.util.GeckoJarReader; -import org.mozilla.gecko.util.StringUtils; - -import android.content.ContentProviderClient; -import android.content.ContentProviderOperation; -import android.content.ContentResolver; -import android.content.ContentValues; -import android.content.Context; -import android.database.ContentObserver; -import android.database.Cursor; -import android.database.MatrixCursor; -import android.database.MergeCursor; -import android.graphics.Bitmap; -import android.graphics.Color; -import android.graphics.drawable.BitmapDrawable; -import android.net.Uri; -import android.os.RemoteException; -import android.os.SystemClock; -import android.support.annotation.CheckResult; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.support.v4.content.CursorLoader; -import android.text.TextUtils; -import android.util.Log; -import org.mozilla.gecko.util.IOUtils; - -import static org.mozilla.gecko.util.IOUtils.ConsumedInputStream; - -public class LocalBrowserDB extends BrowserDB { - // The default size of the buffer to use for downloading Favicons in the event no size is given - // by the server. - public static final int DEFAULT_FAVICON_BUFFER_SIZE_BYTES = 25000; - - private static final String LOGTAG = "GeckoLocalBrowserDB"; - - // Calculate this once, at initialization. isLoggable is too expensive to - // have in-line in each log call. - private static final boolean logDebug = Log.isLoggable(LOGTAG, Log.DEBUG); - protected static void debug(String message) { - if (logDebug) { - Log.d(LOGTAG, message); - } - } - - // Sentinel value used to indicate a failure to locate an ID for a default favicon. - private static final int FAVICON_ID_NOT_FOUND = Integer.MIN_VALUE; - - // Constant used to indicate that no folder was found for particular GUID. - private static final long FOLDER_NOT_FOUND = -1L; - - private final String mProfile; - - // Map of folder GUIDs to IDs. Used for caching. - private final HashMap mFolderIdMap; - - // Use wrapped Boolean so that we can have a null state - private volatile Boolean mDesktopBookmarksExist; - - private volatile SuggestedSites mSuggestedSites; - - // Constants used when importing history data from legacy browser. - public static String HISTORY_VISITS_DATE = "date"; - public static String HISTORY_VISITS_COUNT = "visits"; - public static String HISTORY_VISITS_URL = "url"; - - private static final String TELEMETRY_HISTOGRAM_ACITIVITY_STREAM_TOPSITES = "FENNEC_ACTIVITY_STREAM_TOPSITES_LOADER_TIME_MS"; - - private final Uri mBookmarksUriWithProfile; - private final Uri mParentsUriWithProfile; - private final Uri mHistoryUriWithProfile; - private final Uri mHistoryExpireUriWithProfile; - private final Uri mCombinedUriWithProfile; - private final Uri mUpdateHistoryUriWithProfile; - private final Uri mFaviconsUriWithProfile; - private final Uri mThumbnailsUriWithProfile; - private final Uri mTopSitesUriWithProfile; - private final Uri mHighlightsUriWithProfile; - private final Uri mSearchHistoryUri; - private final Uri mActivityStreamBlockedUriWithProfile; - private final Uri mPageMetadataWithProfile; - - private LocalSearches searches; - private LocalTabsAccessor tabsAccessor; - private LocalURLMetadata urlMetadata; - private LocalUrlAnnotations urlAnnotations; - - private static final String[] DEFAULT_BOOKMARK_COLUMNS = - new String[] { Bookmarks._ID, - Bookmarks.GUID, - Bookmarks.URL, - Bookmarks.TITLE, - Bookmarks.TYPE, - Bookmarks.PARENT }; - - public LocalBrowserDB(String profile) { - mProfile = profile; - mFolderIdMap = new HashMap(); - - mBookmarksUriWithProfile = DBUtils.appendProfile(profile, Bookmarks.CONTENT_URI); - mParentsUriWithProfile = DBUtils.appendProfile(profile, Bookmarks.PARENTS_CONTENT_URI); - mHistoryUriWithProfile = DBUtils.appendProfile(profile, History.CONTENT_URI); - mHistoryExpireUriWithProfile = DBUtils.appendProfile(profile, History.CONTENT_OLD_URI); - mCombinedUriWithProfile = DBUtils.appendProfile(profile, Combined.CONTENT_URI); - mFaviconsUriWithProfile = DBUtils.appendProfile(profile, Favicons.CONTENT_URI); - mTopSitesUriWithProfile = DBUtils.appendProfile(profile, TopSites.CONTENT_URI); - mHighlightsUriWithProfile = DBUtils.appendProfile(profile, Highlights.CONTENT_URI); - mThumbnailsUriWithProfile = DBUtils.appendProfile(profile, Thumbnails.CONTENT_URI); - mActivityStreamBlockedUriWithProfile = DBUtils.appendProfile(profile, ActivityStreamBlocklist.CONTENT_URI); - - mPageMetadataWithProfile = DBUtils.appendProfile(profile, PageMetadata.CONTENT_URI); - - mSearchHistoryUri = BrowserContract.SearchHistory.CONTENT_URI; - - mUpdateHistoryUriWithProfile = - mHistoryUriWithProfile.buildUpon() - .appendQueryParameter(BrowserContract.PARAM_INCREMENT_VISITS, "true") - .appendQueryParameter(BrowserContract.PARAM_INSERT_IF_NEEDED, "true") - .build(); - - searches = new LocalSearches(mProfile); - tabsAccessor = new LocalTabsAccessor(mProfile); - urlMetadata = new LocalURLMetadata(mProfile); - urlAnnotations = new LocalUrlAnnotations(mProfile); - } - - @Override - public Searches getSearches() { - return searches; - } - - @Override - public TabsAccessor getTabsAccessor() { - return tabsAccessor; - } - - @Override - public URLMetadata getURLMetadata() { - return urlMetadata; - } - - @RobocopTarget - @Override - public UrlAnnotations getUrlAnnotations() { - return urlAnnotations; - } - - /** - * Not thread safe. A helper to allocate new IDs for arbitrary strings. - */ - private static class NameCounter { - private final HashMap names = new HashMap(); - private int counter; - private final int increment; - - public NameCounter(int start, int increment) { - this.counter = start; - this.increment = increment; - } - - public int get(final String name) { - Integer mapping = names.get(name); - if (mapping == null) { - int ours = counter; - counter += increment; - names.put(name, ours); - return ours; - } - - return mapping; - } - - public boolean has(final String name) { - return names.containsKey(name); - } - } - - /** - * Add default bookmarks to the database. - * Takes an offset; returns a new offset. - */ - @Override - public int addDefaultBookmarks(Context context, ContentResolver cr, final int offset) { - final long folderID = getFolderIdFromGuid(cr, Bookmarks.MOBILE_FOLDER_GUID); - if (folderID == FOLDER_NOT_FOUND) { - Log.e(LOGTAG, "No mobile folder: cannot add default bookmarks."); - return offset; - } - - // Use reflection to walk the set of bookmark defaults. - // This is horrible. - final Class stringsClass = R.string.class; - final Field[] fields = stringsClass.getFields(); - final Pattern p = Pattern.compile("^bookmarkdefaults_title_"); - - int pos = offset; - final long now = System.currentTimeMillis(); - - final ArrayList bookmarkValues = new ArrayList(); - final ArrayList faviconValues = new ArrayList(); - - // Count down from -offset into negative values to get new favicon IDs. - final NameCounter faviconIDs = new NameCounter((-1 - offset), -1); - - for (int i = 0; i < fields.length; i++) { - final String name = fields[i].getName(); - final Matcher m = p.matcher(name); - if (!m.find()) { - continue; - } - - try { - if (Restrictions.isRestrictedProfile(context)) { - // matching on variable name from strings.xml.in - final String addons = "bookmarkdefaults_title_addons"; - final String regularSumo = "bookmarkdefaults_title_support"; - if (name.equals(addons) || name.equals(regularSumo)) { - continue; - } - } - if (!Restrictions.isRestrictedProfile(context)) { - // if we're not in kidfox, skip the kidfox specific bookmark(s) - if (name.startsWith("bookmarkdefaults_title_restricted")) { - continue; - } - } - final int titleID = fields[i].getInt(null); - final String title = context.getString(titleID); - - final Field urlField = stringsClass.getField(name.replace("_title_", "_url_")); - final int urlID = urlField.getInt(null); - final String url = context.getString(urlID); - - final ContentValues bookmarkValue = createBookmark(now, title, url, pos++, folderID); - bookmarkValues.add(bookmarkValue); - - ConsumedInputStream faviconStream = getDefaultFaviconFromDrawable(context, name); - if (faviconStream == null) { - faviconStream = getDefaultFaviconFromPath(context, name); - } - - if (faviconStream == null) { - continue; - } - - // In the event that truncating the buffer fails, give up and move on. - byte[] icon; - try { - icon = faviconStream.getTruncatedData(); - } catch (OutOfMemoryError e) { - continue; - } - - final ContentValues iconValue = createFavicon(url, icon); - - // Assign a reserved negative _id to each new favicon. - // For now, each name is expected to be unique, and duplicate - // icons will be duplicated in the DB. See Bug 1040806 Comment 8. - if (iconValue != null) { - final int faviconID = faviconIDs.get(name); - iconValue.put("_id", faviconID); - bookmarkValue.put(Bookmarks.FAVICON_ID, faviconID); - faviconValues.add(iconValue); - } - } catch (IllegalAccessException | IllegalArgumentException | NoSuchFieldException e) { - Log.wtf(LOGTAG, "Reflection failure.", e); - } - } - - if (!faviconValues.isEmpty()) { - try { - cr.bulkInsert(mFaviconsUriWithProfile, faviconValues.toArray(new ContentValues[faviconValues.size()])); - } catch (Exception e) { - Log.e(LOGTAG, "Error bulk-inserting default favicons.", e); - } - } - - if (!bookmarkValues.isEmpty()) { - try { - final int inserted = cr.bulkInsert(mBookmarksUriWithProfile, bookmarkValues.toArray(new ContentValues[bookmarkValues.size()])); - return offset + inserted; - } catch (Exception e) { - Log.e(LOGTAG, "Error bulk-inserting default bookmarks.", e); - } - } - - return offset; - } - - /** - * Add bookmarks from the provided distribution. - * Takes an offset; returns a new offset. - */ - @Override - public int addDistributionBookmarks(ContentResolver cr, Distribution distribution, int offset) { - if (!distribution.exists()) { - Log.d(LOGTAG, "No distribution from which to add bookmarks."); - return offset; - } - - final JSONArray bookmarks = distribution.getBookmarks(); - if (bookmarks == null) { - Log.d(LOGTAG, "No distribution bookmarks."); - return offset; - } - - final long folderID = getFolderIdFromGuid(cr, Bookmarks.MOBILE_FOLDER_GUID); - if (folderID == FOLDER_NOT_FOUND) { - Log.e(LOGTAG, "No mobile folder: cannot add distribution bookmarks."); - return offset; - } - - final Locale locale = Locale.getDefault(); - final long now = System.currentTimeMillis(); - int mobilePos = offset; - int pinnedPos = 0; // Assume nobody has pinned anything yet. - - final ArrayList bookmarkValues = new ArrayList(); - final ArrayList faviconValues = new ArrayList(); - - // Count down from -offset into negative values to get new favicon IDs. - final NameCounter faviconIDs = new NameCounter((-1 - offset), -1); - - for (int i = 0; i < bookmarks.length(); i++) { - try { - final JSONObject bookmark = bookmarks.getJSONObject(i); - - final String title = getLocalizedProperty(bookmark, "title", locale); - final String url = getLocalizedProperty(bookmark, "url", locale); - final long parent; - final int pos; - if (bookmark.has("pinned")) { - parent = Bookmarks.FIXED_PINNED_LIST_ID; - pos = pinnedPos++; - } else { - parent = folderID; - pos = mobilePos++; - } - - final ContentValues bookmarkValue = createBookmark(now, title, url, pos, parent); - bookmarkValues.add(bookmarkValue); - - // Return early if there is no icon for this bookmark. - if (!bookmark.has("icon")) { - continue; - } - - try { - final String iconData = bookmark.getString("icon"); - - byte[] icon = BitmapUtils.getBytesFromDataURI(iconData); - if (icon == null) { - continue; - } - - final ContentValues iconValue = createFavicon(url, icon); - if (iconValue == null) { - continue; - } - - /* - * Find out if this icon is a duplicate. If it is, don't try - * to insert it again, but reuse the shared ID. - * Otherwise, assign a new reserved negative _id. - * Duplicates won't be detected in default bookmarks, or - * those already in the database. - */ - final boolean seen = faviconIDs.has(iconData); - final int faviconID = faviconIDs.get(iconData); - - iconValue.put("_id", faviconID); - bookmarkValue.put(Bookmarks.FAVICON_ID, faviconID); - - if (!seen) { - faviconValues.add(iconValue); - } - } catch (JSONException e) { - Log.e(LOGTAG, "Error creating distribution bookmark icon.", e); - } - } catch (JSONException e) { - Log.e(LOGTAG, "Error creating distribution bookmark.", e); - } - } - - if (!faviconValues.isEmpty()) { - try { - cr.bulkInsert(mFaviconsUriWithProfile, faviconValues.toArray(new ContentValues[faviconValues.size()])); - } catch (Exception e) { - Log.e(LOGTAG, "Error bulk-inserting distribution favicons.", e); - } - } - - if (!bookmarkValues.isEmpty()) { - try { - final int inserted = cr.bulkInsert(mBookmarksUriWithProfile, bookmarkValues.toArray(new ContentValues[bookmarkValues.size()])); - return offset + inserted; - } catch (Exception e) { - Log.e(LOGTAG, "Error bulk-inserting distribution bookmarks.", e); - } - } - - return offset; - } - - private static ContentValues createBookmark(final long timestamp, final String title, final String url, final int pos, final long parent) { - final ContentValues v = new ContentValues(); - - v.put(Bookmarks.DATE_CREATED, timestamp); - v.put(Bookmarks.DATE_MODIFIED, timestamp); - v.put(Bookmarks.GUID, Utils.generateGuid()); - - v.put(Bookmarks.PARENT, parent); - v.put(Bookmarks.POSITION, pos); - v.put(Bookmarks.TITLE, title); - v.put(Bookmarks.URL, url); - return v; - } - - private static ContentValues createFavicon(final String url, final byte[] icon) { - ContentValues iconValues = new ContentValues(); - iconValues.put(Favicons.PAGE_URL, url); - iconValues.put(Favicons.DATA, icon); - - return iconValues; - } - - private static String getLocalizedProperty(final JSONObject bookmark, final String property, final Locale locale) throws JSONException { - // Try the full locale. - final String fullLocale = property + "." + locale.toString(); - if (bookmark.has(fullLocale)) { - return bookmark.getString(fullLocale); - } - - // Try without a variant. - if (!TextUtils.isEmpty(locale.getVariant())) { - String noVariant = fullLocale.substring(0, fullLocale.lastIndexOf("_")); - if (bookmark.has(noVariant)) { - return bookmark.getString(noVariant); - } - } - - // Try just the language. - String lang = property + "." + locale.getLanguage(); - if (bookmark.has(lang)) { - return bookmark.getString(lang); - } - - // Default to the non-localized property name. - return bookmark.getString(property); - } - - private static int getFaviconId(String name) { - try { - Class drawablesClass = R.raw.class; - - // Look for a favicon with the id R.raw.bookmarkdefaults_favicon_*. - Field faviconField = drawablesClass.getField(name.replace("_title_", "_favicon_")); - faviconField.setAccessible(true); - - return faviconField.getInt(null); - } catch (IllegalAccessException | NoSuchFieldException e) { - // We'll end up here for any default bookmark that doesn't have a favicon in - // resources/raw/ (i.e., about:firefox). When this happens, the Favicons service will - // fall back to the default branding icon for about pages. Non-about pages should always - // specify an icon; otherwise, the placeholder globe favicon will be used. - Log.d(LOGTAG, "No raw favicon resource found for " + name); - } - - Log.e(LOGTAG, "Failed to find favicon resource ID for " + name); - return FAVICON_ID_NOT_FOUND; - } - - @Override - public boolean insertPageMetadata(ContentProviderClient contentProviderClient, String pageUrl, boolean hasImage, String metadataJSON) { - final String historyGUID = lookupHistoryGUIDByPageUri(contentProviderClient, pageUrl); - - if (historyGUID == null) { - return false; - } - - // We have the GUID, insert the metadata. - final ContentValues cv = new ContentValues(); - cv.put(PageMetadata.HISTORY_GUID, historyGUID); - cv.put(PageMetadata.HAS_IMAGE, hasImage); - cv.put(PageMetadata.JSON, metadataJSON); - - try { - contentProviderClient.insert(mPageMetadataWithProfile, cv); - } catch (RemoteException e) { - throw new IllegalStateException("Unexpected RemoteException", e); - } - - return true; - } - - @Override - public int deletePageMetadata(ContentProviderClient contentProviderClient, String pageUrl) { - final String historyGUID = lookupHistoryGUIDByPageUri(contentProviderClient, pageUrl); - - if (historyGUID == null) { - return 0; - } - - try { - return contentProviderClient.delete(mPageMetadataWithProfile, PageMetadata.HISTORY_GUID + " = ?", new String[]{historyGUID}); - } catch (RemoteException e) { - throw new IllegalStateException("Unexpected RemoteException", e); - } - } - - @Nullable - private String lookupHistoryGUIDByPageUri(ContentProviderClient contentProviderClient, String uri) { - // Unfortunately we might have duplicate history records for the same URL. - final Cursor cursor; - try { - cursor = contentProviderClient.query( - mHistoryUriWithProfile - .buildUpon() - .appendQueryParameter(BrowserContract.PARAM_LIMIT, "1") - .build(), - new String[]{ - History.GUID, - }, - History.URL + "= ?", - new String[]{uri}, History.DATE_LAST_VISITED + " DESC" - ); - } catch (RemoteException e) { - // Won't happen, we control the implementation. - throw new IllegalStateException("Unexpected RemoteException", e); - } - - if (cursor == null) { - return null; - } - - try { - if (!cursor.moveToFirst()) { - return null; - } - - final int historyGUIDCol = cursor.getColumnIndexOrThrow(History.GUID); - return cursor.getString(historyGUIDCol); - } finally { - cursor.close(); - } - } - - /** - * Load a favicon from the omnijar. - * @return A ConsumedInputStream containing the bytes loaded from omnijar. This must be a format - * compatible with the favicon decoder (most probably a PNG or ICO file). - */ - private static ConsumedInputStream getDefaultFaviconFromPath(Context context, String name) { - final int faviconId = getFaviconId(name); - if (faviconId == FAVICON_ID_NOT_FOUND) { - return null; - } - - final String bitmapPath = GeckoJarReader.getJarURL(context, context.getString(faviconId)); - final InputStream iStream = GeckoJarReader.getStream(context, bitmapPath); - - return IOUtils.readFully(iStream, DEFAULT_FAVICON_BUFFER_SIZE_BYTES); - } - - private static ConsumedInputStream getDefaultFaviconFromDrawable(Context context, String name) { - int faviconId = getFaviconId(name); - if (faviconId == FAVICON_ID_NOT_FOUND) { - return null; - } - - InputStream iStream = context.getResources().openRawResource(faviconId); - return IOUtils.readFully(iStream, DEFAULT_FAVICON_BUFFER_SIZE_BYTES); - } - - // Invalidate cached data - @Override - public void invalidate() { - mDesktopBookmarksExist = null; - } - - private Uri bookmarksUriWithLimit(int limit) { - return mBookmarksUriWithProfile.buildUpon() - .appendQueryParameter(BrowserContract.PARAM_LIMIT, - String.valueOf(limit)) - .build(); - } - - private Uri combinedUriWithLimit(int limit) { - return mCombinedUriWithProfile.buildUpon() - .appendQueryParameter(BrowserContract.PARAM_LIMIT, - String.valueOf(limit)) - .build(); - } - - private static Uri withDeleted(final Uri uri) { - return uri.buildUpon() - .appendQueryParameter(BrowserContract.PARAM_SHOW_DELETED, "1") - .build(); - } - - private Cursor filterAllSites(ContentResolver cr, String[] projection, CharSequence constraint, - int limit, CharSequence urlFilter, String selection, String[] selectionArgs) { - // The combined history/bookmarks selection queries for sites with a URL or title containing - // the constraint string(s), treating space-separated words as separate constraints - if (!TextUtils.isEmpty(constraint)) { - final String[] constraintWords = constraint.toString().split(" "); - - // Only create a filter query with a maximum of 10 constraint words. - final int constraintCount = Math.min(constraintWords.length, 10); - for (int i = 0; i < constraintCount; i++) { - selection = DBUtils.concatenateWhere(selection, "(" + Combined.URL + " LIKE ? OR " + - Combined.TITLE + " LIKE ?)"); - String constraintWord = "%" + constraintWords[i] + "%"; - selectionArgs = DBUtils.appendSelectionArgs(selectionArgs, - new String[] { constraintWord, constraintWord }); - } - } - - if (urlFilter != null) { - selection = DBUtils.concatenateWhere(selection, "(" + Combined.URL + " NOT LIKE ?)"); - selectionArgs = DBUtils.appendSelectionArgs(selectionArgs, new String[] { urlFilter.toString() }); - } - - // Order by combined remote+local frecency score. - // Local visits are preferred, so they will by far outweigh remote visits. - // Bookmarked history items get extra frecency points. - final String sortOrder = BrowserContract.getCombinedFrecencySortOrder(true, false); - - return cr.query(combinedUriWithLimit(limit), - projection, - selection, - selectionArgs, - sortOrder); - } - - @Override - public int getCount(ContentResolver cr, String database) { - int count = 0; - String[] columns = null; - String constraint = null; - Uri uri = null; - - if ("history".equals(database)) { - uri = mHistoryUriWithProfile; - columns = new String[] { History._ID }; - constraint = Combined.VISITS + " > 0"; - } else if ("bookmarks".equals(database)) { - uri = mBookmarksUriWithProfile; - columns = new String[] { Bookmarks._ID }; - // ignore folders, tags, keywords, separators, etc. - constraint = Bookmarks.TYPE + " = " + Bookmarks.TYPE_BOOKMARK; - } else if ("thumbnails".equals(database)) { - uri = mThumbnailsUriWithProfile; - columns = new String[] { Thumbnails._ID }; - } else if ("favicons".equals(database)) { - uri = mFaviconsUriWithProfile; - columns = new String[] { Favicons._ID }; - } - - if (uri != null) { - final Cursor cursor = cr.query(uri, columns, constraint, null, null); - - try { - count = cursor.getCount(); - } finally { - cursor.close(); - } - } - - debug("Got count " + count + " for " + database); - return count; - } - - @Override - @RobocopTarget - public Cursor filter(ContentResolver cr, CharSequence constraint, int limit, - EnumSet flags) { - String selection = ""; - String[] selectionArgs = null; - - if (flags.contains(FilterFlags.EXCLUDE_PINNED_SITES)) { - selection = Combined.URL + " NOT IN (SELECT " + - Bookmarks.URL + " FROM bookmarks WHERE " + - DBUtils.qualifyColumn("bookmarks", Bookmarks.PARENT) + " = ? AND " + - DBUtils.qualifyColumn("bookmarks", Bookmarks.IS_DELETED) + " == 0)"; - selectionArgs = new String[] { String.valueOf(Bookmarks.FIXED_PINNED_LIST_ID) }; - } - - return filterAllSites(cr, - new String[] { Combined._ID, - Combined.URL, - Combined.TITLE, - Combined.BOOKMARK_ID, - Combined.HISTORY_ID }, - constraint, - limit, - null, - selection, selectionArgs); - } - - @Override - public void updateVisitedHistory(ContentResolver cr, String uri) { - ContentValues values = new ContentValues(); - - values.put(History.URL, uri); - values.put(History.DATE_LAST_VISITED, System.currentTimeMillis()); - values.put(History.IS_DELETED, 0); - - // This will insert a new history entry if one for this URL - // doesn't already exist - cr.update(mUpdateHistoryUriWithProfile, - values, - History.URL + " = ?", - new String[] { uri }); - } - - @Override - public void updateHistoryTitle(ContentResolver cr, String uri, String title) { - ContentValues values = new ContentValues(); - values.put(History.TITLE, title); - - cr.update(mHistoryUriWithProfile, - values, - History.URL + " = ?", - new String[] { uri }); - } - - @Override - @RobocopTarget - public Cursor getAllVisitedHistory(ContentResolver cr) { - return cr.query(mHistoryUriWithProfile, - new String[] { History.URL }, - History.VISITS + " > 0", - null, - null); - } - - @Override - public Cursor getRecentHistory(ContentResolver cr, int limit) { - return cr.query(combinedUriWithLimit(limit), - new String[] { Combined._ID, - Combined.BOOKMARK_ID, - Combined.HISTORY_ID, - Combined.URL, - Combined.TITLE, - Combined.DATE_LAST_VISITED, - Combined.VISITS }, - History.DATE_LAST_VISITED + " > 0", - null, - History.DATE_LAST_VISITED + " DESC"); - } - - @Override - public Cursor getRecentHistoryBetweenTime(ContentResolver cr, int limit, long start, long end) { - return cr.query(combinedUriWithLimit(limit), - new String[] { Combined._ID, - Combined.BOOKMARK_ID, - Combined.HISTORY_ID, - Combined.URL, - Combined.TITLE, - Combined.DATE_LAST_VISITED, - Combined.VISITS }, - History.DATE_LAST_VISITED + " >= " + start + " AND " + History.DATE_LAST_VISITED + " < " + end, - null, - History.DATE_LAST_VISITED + " DESC"); - } - - public Cursor getHistoryForURL(ContentResolver cr, String uri) { - return cr.query(mHistoryUriWithProfile, - new String[] { - History.VISITS, - History.DATE_LAST_VISITED - }, - History.URL + "= ?", - new String[] { uri }, - History.DATE_LAST_VISITED + " DESC" - ); - } - - @Override - public long getPrePathLastVisitedTimeMilliseconds(ContentResolver cr, String prePath) { - if (prePath == null) { - return 0; - } - // If we don't end with a trailing slash, then both https://foo.com and https://foo.company.biz will match. - if (!prePath.endsWith("/")) { - prePath = prePath + "/"; - } - final Cursor cursor = cr.query(BrowserContract.History.CONTENT_URI, - new String[] { "MAX(" + BrowserContract.HistoryColumns.DATE_LAST_VISITED + ") AS date" }, - BrowserContract.URLColumns.URL + " BETWEEN ? AND ?", new String[] { prePath, prePath + "\u007f" }, null); - try { - cursor.moveToFirst(); - if (cursor.isAfterLast()) { - return 0; - } - return cursor.getLong(0); - } finally { - cursor.close(); - } - } - - @Override - public void expireHistory(ContentResolver cr, ExpirePriority priority) { - Uri url = mHistoryExpireUriWithProfile; - url = url.buildUpon().appendQueryParameter(BrowserContract.PARAM_EXPIRE_PRIORITY, priority.toString()).build(); - cr.delete(url, null, null); - } - - @Override - @RobocopTarget - public void removeHistoryEntry(ContentResolver cr, String url) { - cr.delete(mHistoryUriWithProfile, - History.URL + " = ?", - new String[] { url }); - } - - @Override - public void clearHistory(ContentResolver cr, boolean clearSearchHistory) { - if (clearSearchHistory) { - cr.delete(mSearchHistoryUri, null, null); - } else { - cr.delete(mHistoryUriWithProfile, null, null); - } - } - - private void assertDefaultBookmarkColumnOrdering() { - // We need to insert MatrixCursor values in a specific order - in order to protect against changes - // in DEFAULT_BOOKMARK_COLUMNS we can just assert that we're using the correct ordering. - // Alternatively we could use RowBuilder.add(columnName, value) but that needs api >= 19, - // or we could iterate over DEFAULT_BOOKMARK_COLUMNS, but that gets messy once we need - // to add more than one artificial folder. - if (!((DEFAULT_BOOKMARK_COLUMNS[0].equals(Bookmarks._ID)) && - (DEFAULT_BOOKMARK_COLUMNS[1].equals(Bookmarks.GUID)) && - (DEFAULT_BOOKMARK_COLUMNS[2].equals(Bookmarks.URL)) && - (DEFAULT_BOOKMARK_COLUMNS[3].equals(Bookmarks.TITLE)) && - (DEFAULT_BOOKMARK_COLUMNS[4].equals(Bookmarks.TYPE)) && - (DEFAULT_BOOKMARK_COLUMNS[5].equals(Bookmarks.PARENT)) && - (DEFAULT_BOOKMARK_COLUMNS.length == 6))) { - // If DEFAULT_BOOKMARK_COLUMNS changes we need to update all the MatrixCursor rows - // to contain appropriate data. - throw new IllegalStateException("Fake folder MatrixCursor creation code must be updated to match DEFAULT_BOOKMARK_COLUMNS"); - } - } - - /** - * Retrieve the list of reader-view bookmarks, i.e. the equivalent of the former reading-list. - * This is the result of a join of bookmarks with reader-view annotations (as stored in - * UrlAnnotations). - */ - private Cursor getReadingListBookmarks(ContentResolver cr) { - // group by URL to avoid having duplicate bookmarks listed. It's possible to have multiple - // bookmarks pointing to the same URL (this would most commonly happen by manually - // copying bookmarks on desktop, followed by syncing with mobile), and we don't want - // to show the same URL multiple times in the reading list folder. - final Uri bookmarksGroupedByUri = mBookmarksUriWithProfile.buildUpon() - .appendQueryParameter(BrowserContract.PARAM_GROUP_BY, Bookmarks.URL) - .build(); - - return cr.query(bookmarksGroupedByUri, - DEFAULT_BOOKMARK_COLUMNS, - Bookmarks.ANNOTATION_KEY + " == ? AND " + - Bookmarks.ANNOTATION_VALUE + " == ? AND " + - "(" + Bookmarks.TYPE + " = ? AND " + Bookmarks.URL + " IS NOT NULL)", - new String[] { - BrowserContract.UrlAnnotations.Key.READER_VIEW.getDbValue(), - BrowserContract.UrlAnnotations.READER_VIEW_SAVED_VALUE, - String.valueOf(Bookmarks.TYPE_BOOKMARK) }, - null); - } - - @Override - @RobocopTarget - public Cursor getBookmarksInFolder(ContentResolver cr, long folderId) { - final boolean addDesktopFolder; - final boolean addScreenshotsFolder; - final boolean addReadingListFolder; - - // We always want to show mobile bookmarks in the root view. - if (folderId == Bookmarks.FIXED_ROOT_ID) { - folderId = getFolderIdFromGuid(cr, Bookmarks.MOBILE_FOLDER_GUID); - - // We'll add a fake "Desktop Bookmarks" folder to the root view if desktop - // bookmarks exist, so that the user can still access non-mobile bookmarks. - addDesktopFolder = desktopBookmarksExist(cr); - addScreenshotsFolder = AppConstants.SCREENSHOTS_IN_BOOKMARKS_ENABLED; - - final int readingListItemCount = getBookmarkCountForFolder(cr, Bookmarks.FAKE_READINGLIST_SMARTFOLDER_ID); - addReadingListFolder = (readingListItemCount > 0); - } else { - addDesktopFolder = false; - addScreenshotsFolder = false; - addReadingListFolder = false; - } - - final Cursor c; - - // (You can't switch on a long in Java, hence the if statements) - if (folderId == Bookmarks.FAKE_DESKTOP_FOLDER_ID) { - // Since the "Desktop Bookmarks" folder doesn't actually exist, we - // just fake it by querying specifically certain known desktop folders. - c = cr.query(mBookmarksUriWithProfile, - DEFAULT_BOOKMARK_COLUMNS, - Bookmarks.GUID + " = ? OR " + - Bookmarks.GUID + " = ? OR " + - Bookmarks.GUID + " = ?", - new String[] { Bookmarks.TOOLBAR_FOLDER_GUID, - Bookmarks.MENU_FOLDER_GUID, - Bookmarks.UNFILED_FOLDER_GUID }, - null); - } else if (folderId == Bookmarks.FIXED_SCREENSHOT_FOLDER_ID) { - c = getUrlAnnotations().getScreenshots(cr); - } else if (folderId == Bookmarks.FAKE_READINGLIST_SMARTFOLDER_ID) { - c = getReadingListBookmarks(cr); - } else { - // Right now, we only support showing folder and bookmark type of - // entries. We should add support for other types though (bug 737024) - c = cr.query(mBookmarksUriWithProfile, - DEFAULT_BOOKMARK_COLUMNS, - Bookmarks.PARENT + " = ? AND " + - "(" + Bookmarks.TYPE + " = ? OR " + - "(" + Bookmarks.TYPE + " = ? AND " + Bookmarks.URL + " IS NOT NULL))", - new String[] { String.valueOf(folderId), - String.valueOf(Bookmarks.TYPE_FOLDER), - String.valueOf(Bookmarks.TYPE_BOOKMARK) }, - null); - } - - final List cursorsToMerge = getSpecialFoldersCursorList(addDesktopFolder, addScreenshotsFolder, addReadingListFolder); - if (cursorsToMerge.size() >= 1) { - cursorsToMerge.add(c); - final Cursor[] arr = (Cursor[]) Array.newInstance(Cursor.class, cursorsToMerge.size()); - return new MergeCursor(cursorsToMerge.toArray(arr)); - } else { - return c; - } - } - - @Override - public int getBookmarkCountForFolder(ContentResolver cr, long folderID) { - if (folderID == Bookmarks.FAKE_READINGLIST_SMARTFOLDER_ID) { - return getUrlAnnotations().getAnnotationCount(cr, BrowserContract.UrlAnnotations.Key.READER_VIEW); - } else { - throw new IllegalArgumentException("Retrieving bookmark count for folder with ID=" + folderID + " not supported yet"); - } - } - - @CheckResult - private ArrayList getSpecialFoldersCursorList(final boolean addDesktopFolder, - final boolean addScreenshotsFolder, final boolean addReadingListFolder) { - if (addDesktopFolder || addScreenshotsFolder || addReadingListFolder) { - // Avoid calling this twice. - assertDefaultBookmarkColumnOrdering(); - } - - // Capacity is number of cursors added below plus one for non-special data. - final ArrayList out = new ArrayList<>(4); - if (addDesktopFolder) { - out.add(getSpecialFolderCursor(Bookmarks.FAKE_DESKTOP_FOLDER_ID, Bookmarks.FAKE_DESKTOP_FOLDER_GUID)); - } - - if (addScreenshotsFolder) { - out.add(getSpecialFolderCursor(Bookmarks.FIXED_SCREENSHOT_FOLDER_ID, Bookmarks.SCREENSHOT_FOLDER_GUID)); - } - - if (addReadingListFolder) { - out.add(getSpecialFolderCursor(Bookmarks.FAKE_READINGLIST_SMARTFOLDER_ID, Bookmarks.FAKE_READINGLIST_SMARTFOLDER_GUID)); - } - - return out; - } - - @CheckResult - private MatrixCursor getSpecialFolderCursor(final int folderId, final String folderGuid) { - final MatrixCursor out = new MatrixCursor(DEFAULT_BOOKMARK_COLUMNS); - out.addRow(new Object[] { - folderId, - folderGuid, - "", - "", // Title localisation is done later, in the UI layer (BookmarksListAdapter) - Bookmarks.TYPE_FOLDER, - Bookmarks.FIXED_ROOT_ID - }); - return out; - } - - // Returns true if any desktop bookmarks exist, which will be true if the user - // has set up sync at one point, or done a profile migration from XUL fennec. - private boolean desktopBookmarksExist(ContentResolver cr) { - if (mDesktopBookmarksExist != null) { - return mDesktopBookmarksExist; - } - - // Check to see if there are any bookmarks in one of our three - // fixed "Desktop Bookmarks" folders. - final Cursor c = cr.query(bookmarksUriWithLimit(1), - new String[] { Bookmarks._ID }, - Bookmarks.PARENT + " = ? OR " + - Bookmarks.PARENT + " = ? OR " + - Bookmarks.PARENT + " = ?", - new String[] { String.valueOf(getFolderIdFromGuid(cr, Bookmarks.TOOLBAR_FOLDER_GUID)), - String.valueOf(getFolderIdFromGuid(cr, Bookmarks.MENU_FOLDER_GUID)), - String.valueOf(getFolderIdFromGuid(cr, Bookmarks.UNFILED_FOLDER_GUID)) }, - null); - - try { - // Don't read back out of the cache to avoid races with invalidation. - final boolean e = c.getCount() > 0; - mDesktopBookmarksExist = e; - return e; - } finally { - c.close(); - } - } - - @Override - @RobocopTarget - public boolean isBookmark(ContentResolver cr, String uri) { - final Cursor c = cr.query(bookmarksUriWithLimit(1), - new String[] { Bookmarks._ID }, - Bookmarks.URL + " = ? AND " + Bookmarks.PARENT + " != ?", - new String[] { uri, String.valueOf(Bookmarks.FIXED_PINNED_LIST_ID) }, - Bookmarks.URL); - - if (c == null) { - Log.e(LOGTAG, "Null cursor in isBookmark"); - return false; - } - - try { - return c.getCount() > 0; - } finally { - c.close(); - } - } - - @Override - public String getUrlForKeyword(ContentResolver cr, String keyword) { - final Cursor c = cr.query(mBookmarksUriWithProfile, - new String[] { Bookmarks.URL }, - Bookmarks.KEYWORD + " = ?", - new String[] { keyword }, - null); - try { - if (!c.moveToFirst()) { - return null; - } - - return c.getString(c.getColumnIndexOrThrow(Bookmarks.URL)); - } finally { - c.close(); - } - } - - private synchronized long getFolderIdFromGuid(final ContentResolver cr, final String guid) { - if (mFolderIdMap.containsKey(guid)) { - return mFolderIdMap.get(guid); - } - - final Cursor c = cr.query(mBookmarksUriWithProfile, - new String[] { Bookmarks._ID }, - Bookmarks.GUID + " = ?", - new String[] { guid }, - null); - try { - final int col = c.getColumnIndexOrThrow(Bookmarks._ID); - if (!c.moveToFirst() || c.isNull(col)) { - return FOLDER_NOT_FOUND; - } - - final long id = c.getLong(col); - mFolderIdMap.put(guid, id); - return id; - } finally { - c.close(); - } - } - - /** - * Find parents of records that match the provided criteria, and bump their - * modified timestamp. - */ - protected void bumpParents(ContentResolver cr, String param, String value) { - ContentValues values = new ContentValues(); - values.put(Bookmarks.DATE_MODIFIED, System.currentTimeMillis()); - - String where = param + " = ?"; - String[] args = new String[] { value }; - int updated = cr.update(mParentsUriWithProfile, values, where, args); - debug("Updated " + updated + " rows to new modified time."); - } - - private void addBookmarkItem(ContentResolver cr, String title, String uri, long folderId) { - final long now = System.currentTimeMillis(); - ContentValues values = new ContentValues(); - if (title != null) { - values.put(Bookmarks.TITLE, title); - } - - values.put(Bookmarks.URL, uri); - values.put(Bookmarks.PARENT, folderId); - values.put(Bookmarks.DATE_MODIFIED, now); - - // Get the page's favicon ID from the history table - final Cursor c = cr.query(mHistoryUriWithProfile, - new String[] { History.FAVICON_ID }, - History.URL + " = ?", - new String[] { uri }, - null); - try { - if (c.moveToFirst()) { - int columnIndex = c.getColumnIndexOrThrow(History.FAVICON_ID); - if (!c.isNull(columnIndex)) { - values.put(Bookmarks.FAVICON_ID, c.getLong(columnIndex)); - } - } - } finally { - c.close(); - } - - // Restore deleted record if possible - values.put(Bookmarks.IS_DELETED, 0); - - final Uri bookmarksWithInsert = mBookmarksUriWithProfile.buildUpon() - .appendQueryParameter(BrowserContract.PARAM_INSERT_IF_NEEDED, "true") - .build(); - cr.update(bookmarksWithInsert, - values, - Bookmarks.URL + " = ? AND " + - Bookmarks.PARENT + " = " + folderId, - new String[] { uri }); - - // Bump parent modified time using its ID. - debug("Bumping parent modified time for addition to: " + folderId); - final String where = Bookmarks._ID + " = ?"; - final String[] args = new String[] { String.valueOf(folderId) }; - - ContentValues bumped = new ContentValues(); - bumped.put(Bookmarks.DATE_MODIFIED, now); - - final int updated = cr.update(mBookmarksUriWithProfile, bumped, where, args); - debug("Updated " + updated + " rows to new modified time."); - } - - @Override - @RobocopTarget - public boolean addBookmark(ContentResolver cr, String title, String uri) { - long folderId = getFolderIdFromGuid(cr, Bookmarks.MOBILE_FOLDER_GUID); - if (isBookmarkForUrlInFolder(cr, uri, folderId)) { - // Bookmark added already. - return false; - } - - // Add a new bookmark. - addBookmarkItem(cr, title, uri, folderId); - return true; - } - - private boolean isBookmarkForUrlInFolder(ContentResolver cr, String uri, long folderId) { - final Cursor c = cr.query(bookmarksUriWithLimit(1), - new String[] { Bookmarks._ID }, - Bookmarks.URL + " = ? AND " + Bookmarks.PARENT + " = ? AND " + Bookmarks.IS_DELETED + " == 0", - new String[] { uri, String.valueOf(folderId) }, - Bookmarks.URL); - - if (c == null) { - return false; - } - - try { - return c.getCount() > 0; - } finally { - c.close(); - } - } - - @Override - @RobocopTarget - public void removeBookmarksWithURL(ContentResolver cr, String uri) { - Uri contentUri = mBookmarksUriWithProfile; - - // Do this now so that the items still exist! - bumpParents(cr, Bookmarks.URL, uri); - - final String[] urlArgs = new String[] { uri, String.valueOf(Bookmarks.FIXED_PINNED_LIST_ID) }; - final String urlEquals = Bookmarks.URL + " = ? AND " + Bookmarks.PARENT + " != ? "; - - cr.delete(contentUri, urlEquals, urlArgs); - } - - @Override - public void registerBookmarkObserver(ContentResolver cr, ContentObserver observer) { - cr.registerContentObserver(mBookmarksUriWithProfile, false, observer); - } - - @Override - @RobocopTarget - public void updateBookmark(ContentResolver cr, int id, String uri, String title, String keyword) { - ContentValues values = new ContentValues(); - values.put(Bookmarks.TITLE, title); - values.put(Bookmarks.URL, uri); - values.put(Bookmarks.KEYWORD, keyword); - values.put(Bookmarks.DATE_MODIFIED, System.currentTimeMillis()); - - cr.update(mBookmarksUriWithProfile, - values, - Bookmarks._ID + " = ?", - new String[] { String.valueOf(id) }); - } - - @Override - public boolean hasBookmarkWithGuid(ContentResolver cr, String guid) { - Cursor c = cr.query(bookmarksUriWithLimit(1), - new String[] { Bookmarks.GUID }, - Bookmarks.GUID + " = ?", - new String[] { guid }, - null); - - try { - return c != null && c.getCount() > 0; - } finally { - if (c != null) { - c.close(); - } - } - } - - /** - * Get the favicon from the database, if any, associated with the given favicon URL. (That is, - * the URL of the actual favicon image, not the URL of the page with which the favicon is associated.) - * @param cr The ContentResolver to use. - * @param faviconURL The URL of the favicon to fetch from the database. - * @return The decoded Bitmap from the database, if any. null if none is stored. - */ - @Override - public LoadFaviconResult getFaviconForUrl(Context context, ContentResolver cr, String faviconURL) { - final Cursor c = cr.query(mFaviconsUriWithProfile, - new String[] { Favicons.DATA }, - Favicons.URL + " = ? AND " + Favicons.DATA + " IS NOT NULL", - new String[] { faviconURL }, - null); - - boolean shouldDelete = false; - byte[] b = null; - try { - if (!c.moveToFirst()) { - return null; - } - - final int faviconIndex = c.getColumnIndexOrThrow(Favicons.DATA); - try { - b = c.getBlob(faviconIndex); - } catch (IllegalStateException e) { - // This happens when the blob is more than 1MB: Bug 1106347. - // Delete that row. - shouldDelete = true; - } - } finally { - c.close(); - } - - if (shouldDelete) { - try { - Log.d(LOGTAG, "Deleting invalid favicon."); - cr.delete(mFaviconsUriWithProfile, - Favicons.URL + " = ?", - new String[] { faviconURL }); - } catch (Exception e) { - // Do nothing. - } - } - - if (b == null) { - return null; - } - - return FaviconDecoder.decodeFavicon(context, b); - } - - /** - * Try to find a usable favicon URL in the history or bookmarks table. - */ - @Override - public String getFaviconURLFromPageURL(ContentResolver cr, String uri) { - // Check first in the history table. - Cursor c = cr.query(mHistoryUriWithProfile, - new String[] { History.FAVICON_URL }, - Combined.URL + " = ?", - new String[] { uri }, - null); - - try { - if (c.moveToFirst()) { - // Interrupted page loads can leave History items without a valid favicon_id. - final int columnIndex = c.getColumnIndexOrThrow(History.FAVICON_URL); - if (!c.isNull(columnIndex)) { - final String faviconURL = c.getString(columnIndex); - if (faviconURL != null) { - return faviconURL; - } - } - } - } finally { - c.close(); - } - - // If that fails, check in the bookmarks table. - c = cr.query(mBookmarksUriWithProfile, - new String[] { Bookmarks.FAVICON_URL }, - Bookmarks.URL + " = ?", - new String[] { uri }, - null); - - try { - if (c.moveToFirst()) { - return c.getString(c.getColumnIndexOrThrow(Bookmarks.FAVICON_URL)); - } - - return null; - } finally { - c.close(); - } - } - - @Override - public boolean hideSuggestedSite(String url) { - if (mSuggestedSites == null) { - return false; - } - - return mSuggestedSites.hideSite(url); - } - - @Override - public void updateThumbnailForUrl(ContentResolver cr, String uri, - BitmapDrawable thumbnail) { - // If a null thumbnail was passed in, delete the stored thumbnail for this url. - if (thumbnail == null) { - cr.delete(mThumbnailsUriWithProfile, Thumbnails.URL + " == ?", new String[] { uri }); - return; - } - - Bitmap bitmap = thumbnail.getBitmap(); - - byte[] data = null; - ByteArrayOutputStream stream = new ByteArrayOutputStream(); - if (bitmap.compress(Bitmap.CompressFormat.PNG, 0, stream)) { - data = stream.toByteArray(); - } else { - Log.w(LOGTAG, "Favicon compression failed."); - } - - ContentValues values = new ContentValues(); - values.put(Thumbnails.URL, uri); - values.put(Thumbnails.DATA, data); - - Uri thumbnailsUri = mThumbnailsUriWithProfile.buildUpon(). - appendQueryParameter(BrowserContract.PARAM_INSERT_IF_NEEDED, "true").build(); - cr.update(thumbnailsUri, - values, - Thumbnails.URL + " = ?", - new String[] { uri }); - } - - @Override - @RobocopTarget - public byte[] getThumbnailForUrl(ContentResolver cr, String uri) { - final Cursor c = cr.query(mThumbnailsUriWithProfile, - new String[]{ Thumbnails.DATA }, - Thumbnails.URL + " = ? AND " + Thumbnails.DATA + " IS NOT NULL", - new String[]{ uri }, - null); - try { - if (!c.moveToFirst()) { - return null; - } - - int thumbnailIndex = c.getColumnIndexOrThrow(Thumbnails.DATA); - - return c.getBlob(thumbnailIndex); - } finally { - c.close(); - } - - } - - /** - * Query for non-null thumbnails matching the provided urls. - * The returned cursor will have no more than, but possibly fewer than, - * the requested number of thumbnails. - * - * Returns null if the provided list of URLs is empty or null. - */ - @Override - public Cursor getThumbnailsForUrls(ContentResolver cr, List urls) { - final int urlCount = urls.size(); - if (urlCount == 0) { - return null; - } - - // Don't match against null thumbnails. - final String selection = Thumbnails.DATA + " IS NOT NULL AND " + - DBUtils.computeSQLInClause(urlCount, Thumbnails.URL); - final String[] selectionArgs = urls.toArray(new String[urlCount]); - - return cr.query(mThumbnailsUriWithProfile, - new String[] { Thumbnails.URL, Thumbnails.DATA }, - selection, - selectionArgs, - null); - } - - @Override - @RobocopTarget - public void removeThumbnails(ContentResolver cr) { - cr.delete(mThumbnailsUriWithProfile, null, null); - } - - /** - * Utility method used by AndroidImport for updating existing history record using batch operations. - * - * @param cr ContentResolver used for querying information about existing history records. - * @param operations Collection of operations for queueing record updates. - * @param url URL used for querying history records to update. - * @param title Optional new title. - * @param date New last visited date. Will be used if newer than current last visited date. - * @param visits Will increment existing visit counts by this number. - */ - @Override - public void updateHistoryInBatch(@NonNull ContentResolver cr, - @NonNull Collection operations, - @NonNull String url, @Nullable String title, - long date, int visits) { - final String[] projection = { - History._ID, - History.VISITS, - History.LOCAL_VISITS, - History.DATE_LAST_VISITED, - History.LOCAL_DATE_LAST_VISITED - }; - - // We need to get the old visit and date aggregates. - final Cursor cursor = cr.query(withDeleted(mHistoryUriWithProfile), - projection, - History.URL + " = ?", - new String[] { url }, - null); - if (cursor == null) { - Log.w(LOGTAG, "Null cursor while querying for old visit and date aggregates"); - return; - } - - try { - final ContentValues values = new ContentValues(); - - // Restore deleted record if possible - values.put(History.IS_DELETED, 0); - - if (cursor.moveToFirst()) { - final int visitsCol = cursor.getColumnIndexOrThrow(History.VISITS); - final int localVisitsCol = cursor.getColumnIndexOrThrow(History.LOCAL_VISITS); - final int dateCol = cursor.getColumnIndexOrThrow(History.DATE_LAST_VISITED); - final int localDateCol = cursor.getColumnIndexOrThrow(History.LOCAL_DATE_LAST_VISITED); - - final int oldVisits = cursor.getInt(visitsCol); - final int oldLocalVisits = cursor.getInt(localVisitsCol); - final long oldDate = cursor.getLong(dateCol); - final long oldLocalDate = cursor.getLong(localDateCol); - - // NB: This will increment visit counts even if subsequent "insert visits" operations - // insert no new visits (see insertVisitsFromImportHistoryInBatch). - // So, we're doing a wrong thing here if user imports history more than once. - // See Bug 1277330. - values.put(History.VISITS, oldVisits + visits); - values.put(History.LOCAL_VISITS, oldLocalVisits + visits); - // Only update last visited if newer. - if (date > oldDate) { - values.put(History.DATE_LAST_VISITED, date); - } - if (date > oldLocalDate) { - values.put(History.LOCAL_DATE_LAST_VISITED, date); - } - } else { - values.put(History.VISITS, visits); - values.put(History.LOCAL_VISITS, visits); - values.put(History.DATE_LAST_VISITED, date); - values.put(History.LOCAL_DATE_LAST_VISITED, date); - } - if (title != null) { - values.put(History.TITLE, title); - } - values.put(History.URL, url); - - final Uri historyUri = withDeleted(mHistoryUriWithProfile).buildUpon(). - appendQueryParameter(BrowserContract.PARAM_INSERT_IF_NEEDED, "true").build(); - - // Update or insert - final ContentProviderOperation.Builder builder = - ContentProviderOperation.newUpdate(historyUri); - builder.withSelection(History.URL + " = ?", new String[] { url }); - builder.withValues(values); - - // Queue the operation - operations.add(builder.build()); - } finally { - cursor.close(); - } - } - - /** - * Utility method used by AndroidImport to insert visit data for history records that were just imported. - * Uses batch operations. - * - * @param cr ContentResolver used to query history table and bulkInsert visit records - * @param operations Collection of operations for queueing inserts - * @param visitsToSynthesize List of ContentValues describing visit information for each history record: - * (History URL, LAST DATE VISITED, VISIT COUNT) - */ - public void insertVisitsFromImportHistoryInBatch(ContentResolver cr, - Collection operations, - ArrayList visitsToSynthesize) { - // If for any reason we fail to obtain history GUID for a tuple we're processing, - // let's just ignore it. It's possible that the "best-effort" history import - // did not fully succeed, so we could be missing some of the records. - int historyGUIDCol = -1; - for (ContentValues visitsInformation : visitsToSynthesize) { - final Cursor cursor = cr.query(mHistoryUriWithProfile, - new String[] {History.GUID}, - History.URL + " = ?", - new String[] {visitsInformation.getAsString(HISTORY_VISITS_URL)}, - null); - if (cursor == null) { - continue; - } - - final String historyGUID; - - try { - if (!cursor.moveToFirst()) { - continue; - } - if (historyGUIDCol == -1) { - historyGUIDCol = cursor.getColumnIndexOrThrow(History.GUID); - } - - historyGUID = cursor.getString(historyGUIDCol); - } finally { - // We "continue" on a null cursor above, so it's safe to act upon it without checking. - cursor.close(); - } - if (historyGUID == null) { - continue; - } - - // This fakes the individual visit records, using last visited date as the starting point. - for (int i = 0; i < visitsInformation.getAsInteger(HISTORY_VISITS_COUNT); i++) { - // We rely on database defaults for IS_LOCAL and VISIT_TYPE. - final ContentValues visitToInsert = new ContentValues(); - visitToInsert.put(BrowserContract.Visits.HISTORY_GUID, historyGUID); - - // Visit timestamps are stored in microseconds, while Android Browser visit timestmaps - // are in milliseconds. This is the conversion point for imports. - visitToInsert.put(BrowserContract.Visits.DATE_VISITED, - (visitsInformation.getAsLong(HISTORY_VISITS_DATE) - i) * 1000); - - final ContentProviderOperation.Builder builder = - ContentProviderOperation.newInsert(BrowserContract.Visits.CONTENT_URI); - builder.withValues(visitToInsert); - - // Queue the insert operation - operations.add(builder.build()); - } - } - } - - @Override - public void updateBookmarkInBatch(ContentResolver cr, - Collection operations, - String url, String title, String guid, - long parent, long added, - long modified, long position, - String keyword, int type) { - ContentValues values = new ContentValues(); - if (title == null && url != null) { - title = url; - } - if (title != null) { - values.put(Bookmarks.TITLE, title); - } - if (url != null) { - values.put(Bookmarks.URL, url); - } - if (guid != null) { - values.put(SyncColumns.GUID, guid); - } - if (keyword != null) { - values.put(Bookmarks.KEYWORD, keyword); - } - if (added > 0) { - values.put(SyncColumns.DATE_CREATED, added); - } - if (modified > 0) { - values.put(SyncColumns.DATE_MODIFIED, modified); - } - values.put(Bookmarks.POSITION, position); - // Restore deleted record if possible - values.put(Bookmarks.IS_DELETED, 0); - - // This assumes no "real" folder has a negative ID. Only - // things like the reading list folder do. - if (parent < 0) { - parent = getFolderIdFromGuid(cr, Bookmarks.MOBILE_FOLDER_GUID); - } - values.put(Bookmarks.PARENT, parent); - values.put(Bookmarks.TYPE, type); - - Uri bookmarkUri = withDeleted(mBookmarksUriWithProfile).buildUpon(). - appendQueryParameter(BrowserContract.PARAM_INSERT_IF_NEEDED, "true").build(); - // Update or insert - ContentProviderOperation.Builder builder = - ContentProviderOperation.newUpdate(bookmarkUri); - if (url != null) { - // Bookmarks are defined by their URL and Folder. - builder.withSelection(Bookmarks.URL + " = ? AND " - + Bookmarks.PARENT + " = ?", - new String[] { url, - Long.toString(parent) - }); - } else if (title != null) { - // Or their title and parent folder. (Folders!) - builder.withSelection(Bookmarks.TITLE + " = ? AND " - + Bookmarks.PARENT + " = ?", - new String[]{ title, - Long.toString(parent) - }); - } else if (type == Bookmarks.TYPE_SEPARATOR) { - // Or their their position (separators) - builder.withSelection(Bookmarks.POSITION + " = ? AND " - + Bookmarks.PARENT + " = ?", - new String[] { Long.toString(position), - Long.toString(parent) - }); - } else { - Log.e(LOGTAG, "Bookmark entry without url or title and not a separator, not added."); - } - builder.withValues(values); - - // Queue the operation - operations.add(builder.build()); - } - - @Override - public void pinSite(ContentResolver cr, String url, String title, int position) { - ContentValues values = new ContentValues(); - final long now = System.currentTimeMillis(); - values.put(Bookmarks.TITLE, title); - values.put(Bookmarks.URL, url); - values.put(Bookmarks.PARENT, Bookmarks.FIXED_PINNED_LIST_ID); - values.put(Bookmarks.DATE_MODIFIED, now); - values.put(Bookmarks.POSITION, position); - values.put(Bookmarks.IS_DELETED, 0); - - // We do an update-and-replace here without deleting any existing pins for the given URL. - // That means if the user pins a URL, then edits another thumbnail to use the same URL, - // we'll end up with two pins for that site. This is the intended behavior, which - // incidentally saves us a delete query. - Uri uri = mBookmarksUriWithProfile.buildUpon() - .appendQueryParameter(BrowserContract.PARAM_INSERT_IF_NEEDED, "true").build(); - cr.update(uri, - values, - Bookmarks.POSITION + " = ? AND " + - Bookmarks.PARENT + " = ?", - new String[] { Integer.toString(position), - String.valueOf(Bookmarks.FIXED_PINNED_LIST_ID) }); - } - - @Override - public void unpinSite(ContentResolver cr, int position) { - cr.delete(mBookmarksUriWithProfile, - Bookmarks.PARENT + " == ? AND " + Bookmarks.POSITION + " = ?", - new String[] { - String.valueOf(Bookmarks.FIXED_PINNED_LIST_ID), - Integer.toString(position) - }); - } - - @Override - @RobocopTarget - public Cursor getBookmarkForUrl(ContentResolver cr, String url) { - Cursor c = cr.query(bookmarksUriWithLimit(1), - new String[] { Bookmarks._ID, - Bookmarks.URL, - Bookmarks.TITLE, - Bookmarks.KEYWORD }, - Bookmarks.URL + " = ?", - new String[] { url }, - null); - - if (c != null && c.getCount() == 0) { - c.close(); - c = null; - } - - return c; - } - - @Override - public Cursor getBookmarksForPartialUrl(ContentResolver cr, String partialUrl) { - Cursor c = cr.query(mBookmarksUriWithProfile, - new String[] { Bookmarks.GUID, Bookmarks._ID, Bookmarks.URL }, - Bookmarks.URL + " LIKE '%" + partialUrl + "%'", // TODO: Escaping! - null, - null); - - if (c != null && c.getCount() == 0) { - c.close(); - c = null; - } - - return c; - } - - @Override - public void setSuggestedSites(SuggestedSites suggestedSites) { - mSuggestedSites = suggestedSites; - } - - @Override - public SuggestedSites getSuggestedSites() { - return mSuggestedSites; - } - - @Override - public boolean hasSuggestedImageUrl(String url) { - if (mSuggestedSites == null) { - return false; - } - return mSuggestedSites.contains(url); - } - - @Override - public String getSuggestedImageUrlForUrl(String url) { - if (mSuggestedSites == null) { - return null; - } - return mSuggestedSites.getImageUrlForUrl(url); - } - - @Override - public int getSuggestedBackgroundColorForUrl(String url) { - if (mSuggestedSites == null) { - return 0; - } - final String bgColor = mSuggestedSites.getBackgroundColorForUrl(url); - if (bgColor != null) { - return Color.parseColor(bgColor); - } - - return 0; - } - - private static void appendUrlsFromCursor(List urls, Cursor c) { - if (!c.moveToFirst()) { - return; - } - - do { - String url = c.getString(c.getColumnIndex(History.URL)); - - // Do a simpler check before decoding to avoid parsing - // all URLs unnecessarily. - if (StringUtils.isUserEnteredUrl(url)) { - url = StringUtils.decodeUserEnteredUrl(url); - } - - urls.add(url); - } while (c.moveToNext()); - } - - - /** - * Internal CursorLoader that extends the framework CursorLoader in order to measure - * performance for telemetry purposes. - */ - private static final class TelemetrisedCursorLoader extends CursorLoader { - final String mHistogramName; - - public TelemetrisedCursorLoader(Context context, Uri uri, String[] projection, String selection, - String[] selectionArgs, String sortOrder, - final String histogramName) { - super(context, uri, projection, selection, selectionArgs, sortOrder); - mHistogramName = histogramName; - } - - @Override - public Cursor loadInBackground() { - final long start = SystemClock.uptimeMillis(); - - final Cursor cursor = super.loadInBackground(); - - final long end = SystemClock.uptimeMillis(); - final long took = end - start; - - Telemetry.addToHistogram(mHistogramName, (int) Math.min(took, Integer.MAX_VALUE)); - return cursor; - } - } - - public CursorLoader getActivityStreamTopSites(Context context, int limit) { - final Uri uri = mTopSitesUriWithProfile.buildUpon() - .appendQueryParameter(BrowserContract.PARAM_LIMIT, - String.valueOf(limit)) - .appendQueryParameter(BrowserContract.PARAM_TOPSITES_DISABLE_PINNED, Boolean.TRUE.toString()) - .build(); - - return new TelemetrisedCursorLoader(context, - uri, - new String[]{ Combined._ID, - Combined.URL, - Combined.TITLE, - Combined.BOOKMARK_ID, - Combined.HISTORY_ID }, - null, - null, - null, - TELEMETRY_HISTOGRAM_ACITIVITY_STREAM_TOPSITES); - } - - @Override - public Cursor getTopSites(ContentResolver cr, int suggestedRangeLimit, int limit) { - final Uri uri = mTopSitesUriWithProfile.buildUpon() - .appendQueryParameter(BrowserContract.PARAM_LIMIT, - String.valueOf(limit)) - .appendQueryParameter(BrowserContract.PARAM_SUGGESTEDSITES_LIMIT, - String.valueOf(suggestedRangeLimit)) - .build(); - - Cursor topSitesCursor = cr.query(uri, - new String[] { Combined._ID, - Combined.URL, - Combined.TITLE, - Combined.BOOKMARK_ID, - Combined.HISTORY_ID }, - null, - null, - null); - - // It's possible that we will retrieve fewer sites than are required to fill the top-sites panel - in this case - // we need to add "blank" tiles. It's much easier to add these here (as opposed to SQL), since we don't care - // about their ordering (they go after all the other sites), but we do care about their number (and calculating - // that inside out topsites SQL query would be difficult given the other processing we're already doing there). - final int blanksRequired = suggestedRangeLimit - topSitesCursor.getCount(); - - if (blanksRequired <= 0) { - return topSitesCursor; - } - - MatrixCursor blanksCursor = new MatrixCursor(new String[] { - TopSites._ID, - TopSites.BOOKMARK_ID, - TopSites.HISTORY_ID, - TopSites.URL, - TopSites.TITLE, - TopSites.TYPE}); - - final MatrixCursor.RowBuilder rb = blanksCursor.newRow(); - rb.add(-1); - rb.add(-1); - rb.add(-1); - rb.add(""); - rb.add(""); - rb.add(TopSites.TYPE_BLANK); - - return new MergeCursor(new Cursor[] {topSitesCursor, blanksCursor}); - } - - @Override - public CursorLoader getHighlights(Context context, int limit) { - final Uri uri = mHighlightsUriWithProfile.buildUpon() - .appendQueryParameter(BrowserContract.PARAM_LIMIT, String.valueOf(limit)) - .build(); - - return new CursorLoader(context, uri, null, null, null, null); - } - - @Override - public void blockActivityStreamSite(ContentResolver cr, String url) { - final ContentValues values = new ContentValues(); - values.put(ActivityStreamBlocklist.URL, url); - cr.insert(mActivityStreamBlockedUriWithProfile, values); - } - -} diff --git a/mobile/android/base/java/org/mozilla/gecko/db/LocalSearches.java b/mobile/android/base/java/org/mozilla/gecko/db/LocalSearches.java deleted file mode 100644 index a9a55e51d0..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/db/LocalSearches.java +++ /dev/null @@ -1,28 +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.db; - -import android.content.ContentResolver; -import android.content.ContentValues; -import android.net.Uri; - -/** - * Helper class for dealing with the search provider inside Fennec. - */ -public class LocalSearches implements Searches { - private final Uri uriWithProfile; - - public LocalSearches(String mProfile) { - uriWithProfile = DBUtils.appendProfileWithDefault(mProfile, BrowserContract.SearchHistory.CONTENT_URI); - } - - @Override - public void insert(ContentResolver cr, String query) { - final ContentValues values = new ContentValues(); - values.put(BrowserContract.SearchHistory.QUERY, query); - cr.insert(uriWithProfile, values); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/db/LocalTabsAccessor.java b/mobile/android/base/java/org/mozilla/gecko/db/LocalTabsAccessor.java deleted file mode 100644 index c7bd9475c0..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/db/LocalTabsAccessor.java +++ /dev/null @@ -1,320 +0,0 @@ -/* 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.db; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.concurrent.TimeUnit; -import java.util.regex.Pattern; - -import org.json.JSONArray; -import org.json.JSONException; -import org.mozilla.gecko.Tab; -import org.mozilla.gecko.util.ThreadUtils; -import org.mozilla.gecko.util.UIAsyncTask; - -import android.content.ContentResolver; -import android.content.ContentValues; -import android.content.Context; -import android.database.Cursor; -import android.net.Uri; -import android.text.TextUtils; -import android.util.Log; - -public class LocalTabsAccessor implements TabsAccessor { - private static final String LOGTAG = "GeckoTabsAccessor"; - private static final long THREE_WEEKS_IN_MILLISECONDS = TimeUnit.MILLISECONDS.convert(21L, TimeUnit.DAYS); - - public static final String[] TABS_PROJECTION_COLUMNS = new String[] { - BrowserContract.Tabs.TITLE, - BrowserContract.Tabs.URL, - BrowserContract.Clients.GUID, - BrowserContract.Clients.NAME, - BrowserContract.Tabs.LAST_USED, - BrowserContract.Clients.LAST_MODIFIED, - BrowserContract.Clients.DEVICE_TYPE, - }; - - public static final String[] CLIENTS_PROJECTION_COLUMNS = new String[] { - BrowserContract.Clients.GUID, - BrowserContract.Clients.NAME, - BrowserContract.Clients.LAST_MODIFIED, - BrowserContract.Clients.DEVICE_TYPE - }; - - private static final String REMOTE_CLIENTS_SELECTION = BrowserContract.Clients.GUID + " IS NOT NULL"; - private static final String LOCAL_TABS_SELECTION = BrowserContract.Tabs.CLIENT_GUID + " IS NULL"; - private static final String REMOTE_TABS_SELECTION = BrowserContract.Tabs.CLIENT_GUID + " IS NOT NULL"; - private static final String REMOTE_TABS_SELECTION_CLIENT_RECENCY = REMOTE_TABS_SELECTION + - " AND " + BrowserContract.Clients.LAST_MODIFIED + " > ?"; - - private static final String REMOTE_TABS_SORT_ORDER = - // Most recently synced clients first. - BrowserContract.Clients.LAST_MODIFIED + " DESC, " + - // If two clients somehow had the same last modified time, this will - // group them (arbitrarily). - BrowserContract.Clients.GUID + " DESC, " + - // Within a single client, most recently used tabs first. - BrowserContract.Tabs.LAST_USED + " DESC"; - - private static final String LOCAL_CLIENT_SELECTION = BrowserContract.Clients.GUID + " IS NULL"; - - private static final Pattern FILTERED_URL_PATTERN = Pattern.compile("^(about|chrome|wyciwyg|file):"); - - private final Uri clientsRecencyUriWithProfile; - private final Uri tabsUriWithProfile; - private final Uri clientsUriWithProfile; - - public LocalTabsAccessor(String profileName) { - tabsUriWithProfile = DBUtils.appendProfileWithDefault(profileName, BrowserContract.Tabs.CONTENT_URI); - clientsUriWithProfile = DBUtils.appendProfileWithDefault(profileName, BrowserContract.Clients.CONTENT_URI); - clientsRecencyUriWithProfile = DBUtils.appendProfileWithDefault(profileName, BrowserContract.Clients.CONTENT_RECENCY_URI); - } - - /** - * Extracts a List of just RemoteClients from a cursor. - * The supplied cursor should be grouped by guid and sorted by most recently used. - */ - @Override - public List getClientsWithoutTabsByRecencyFromCursor(Cursor cursor) { - final ArrayList clients = new ArrayList<>(cursor.getCount()); - - final int originalPosition = cursor.getPosition(); - try { - if (!cursor.moveToFirst()) { - return clients; - } - - final int clientGuidIndex = cursor.getColumnIndex(BrowserContract.Clients.GUID); - final int clientNameIndex = cursor.getColumnIndex(BrowserContract.Clients.NAME); - final int clientLastModifiedIndex = cursor.getColumnIndex(BrowserContract.Clients.LAST_MODIFIED); - final int clientDeviceTypeIndex = cursor.getColumnIndex(BrowserContract.Clients.DEVICE_TYPE); - - while (!cursor.isAfterLast()) { - final String clientGuid = cursor.getString(clientGuidIndex); - final String clientName = cursor.getString(clientNameIndex); - final String deviceType = cursor.getString(clientDeviceTypeIndex); - final long lastModified = cursor.getLong(clientLastModifiedIndex); - - clients.add(new RemoteClient(clientGuid, clientName, lastModified, deviceType)); - - cursor.moveToNext(); - } - } finally { - cursor.moveToPosition(originalPosition); - } - return clients; - } - - /** - * Extract client and tab records from a cursor. - *

- * The position of the cursor is moved to before the first record before - * reading. The cursor is advanced until there are no more records to be - * read. The position of the cursor is restored before returning. - * - * @param cursor - * to extract records from. The records should already be grouped - * by client GUID. - * @return list of clients, each containing list of tabs. - */ - @Override - public List getClientsFromCursor(final Cursor cursor) { - final ArrayList clients = new ArrayList(); - - final int originalPosition = cursor.getPosition(); - try { - if (!cursor.moveToFirst()) { - return clients; - } - - final int tabTitleIndex = cursor.getColumnIndex(BrowserContract.Tabs.TITLE); - final int tabUrlIndex = cursor.getColumnIndex(BrowserContract.Tabs.URL); - final int tabLastUsedIndex = cursor.getColumnIndex(BrowserContract.Tabs.LAST_USED); - final int clientGuidIndex = cursor.getColumnIndex(BrowserContract.Clients.GUID); - final int clientNameIndex = cursor.getColumnIndex(BrowserContract.Clients.NAME); - final int clientLastModifiedIndex = cursor.getColumnIndex(BrowserContract.Clients.LAST_MODIFIED); - final int clientDeviceTypeIndex = cursor.getColumnIndex(BrowserContract.Clients.DEVICE_TYPE); - - // A walking partition, chunking by client GUID. We assume the - // cursor records are already grouped by client GUID; see the query - // sort order. - RemoteClient lastClient = null; - while (!cursor.isAfterLast()) { - final String clientGuid = cursor.getString(clientGuidIndex); - if (lastClient == null || !TextUtils.equals(lastClient.guid, clientGuid)) { - final String clientName = cursor.getString(clientNameIndex); - final long lastModified = cursor.getLong(clientLastModifiedIndex); - final String deviceType = cursor.getString(clientDeviceTypeIndex); - lastClient = new RemoteClient(clientGuid, clientName, lastModified, deviceType); - clients.add(lastClient); - } - - final String tabTitle = cursor.getString(tabTitleIndex); - final String tabUrl = cursor.getString(tabUrlIndex); - final long tabLastUsed = cursor.getLong(tabLastUsedIndex); - lastClient.tabs.add(new RemoteTab(tabTitle, tabUrl, tabLastUsed)); - - cursor.moveToNext(); - } - } finally { - cursor.moveToPosition(originalPosition); - } - - return clients; - } - - @Override - public Cursor getRemoteClientsByRecencyCursor(Context context) { - final Uri uri = clientsRecencyUriWithProfile; - return context.getContentResolver().query(uri, CLIENTS_PROJECTION_COLUMNS, - REMOTE_CLIENTS_SELECTION, null, null); - } - - @Override - public Cursor getRemoteTabsCursor(Context context) { - return getRemoteTabsCursor(context, -1); - } - - @Override - public Cursor getRemoteTabsCursor(Context context, int limit) { - Uri uri = tabsUriWithProfile; - - if (limit > 0) { - uri = uri.buildUpon() - .appendQueryParameter(BrowserContract.PARAM_LIMIT, String.valueOf(limit)) - .build(); - } - - final String threeWeeksAgoTimestampMillis = Long.valueOf( - System.currentTimeMillis() - THREE_WEEKS_IN_MILLISECONDS).toString(); - return context.getContentResolver().query(uri, - TABS_PROJECTION_COLUMNS, - REMOTE_TABS_SELECTION_CLIENT_RECENCY, - new String[] {threeWeeksAgoTimestampMillis}, - REMOTE_TABS_SORT_ORDER); - } - - // This method returns all tabs from all remote clients, - // ordered by most recent client first, most recent tab first - @Override - public void getTabs(final Context context, final OnQueryTabsCompleteListener listener) { - getTabs(context, 0, listener); - } - - // This method returns limited number of tabs from all remote clients, - // ordered by most recent client first, most recent tab first - @Override - public void getTabs(final Context context, final int limit, final OnQueryTabsCompleteListener listener) { - // If there is no listener, no point in doing work. - if (listener == null) - return; - - (new UIAsyncTask.WithoutParams>(ThreadUtils.getBackgroundHandler()) { - @Override - protected List doInBackground() { - final Cursor cursor = getRemoteTabsCursor(context, limit); - if (cursor == null) - return null; - - try { - return Collections.unmodifiableList(getClientsFromCursor(cursor)); - } finally { - cursor.close(); - } - } - - @Override - protected void onPostExecute(List clients) { - listener.onQueryTabsComplete(clients); - } - }).execute(); - } - - // Updates the modified time of the local client with the current time. - private void updateLocalClient(final ContentResolver cr) { - ContentValues values = new ContentValues(); - values.put(BrowserContract.Clients.LAST_MODIFIED, System.currentTimeMillis()); - - cr.update(clientsUriWithProfile, values, LOCAL_CLIENT_SELECTION, null); - } - - // Deletes all local tabs. - private void deleteLocalTabs(final ContentResolver cr) { - cr.delete(tabsUriWithProfile, LOCAL_TABS_SELECTION, null); - } - - /** - * Tabs are positioned in the DB in the same order that they appear in the tabs param. - * - URL should never empty or null. Skip this tab if there's no URL. - * - TITLE should always a string, either a page title or empty. - * - LAST_USED should always be numeric. - * - FAVICON should be a URL or null. - * - HISTORY should be serialized JSON array of URLs. - * - POSITION should always be numeric. - * - CLIENT_GUID should always be null to represent the local client. - */ - private void insertLocalTabs(final ContentResolver cr, final Iterable tabs) { - // Reuse this for serializing individual history URLs as JSON. - JSONArray history = new JSONArray(); - ArrayList valuesToInsert = new ArrayList(); - - int position = 0; - for (Tab tab : tabs) { - // Skip this tab if it has a null URL or is in private browsing mode, or is a filtered URL. - String url = tab.getURL(); - if (url == null || tab.isPrivate() || isFilteredURL(url)) - continue; - - ContentValues values = new ContentValues(); - values.put(BrowserContract.Tabs.URL, url); - values.put(BrowserContract.Tabs.TITLE, tab.getTitle()); - values.put(BrowserContract.Tabs.LAST_USED, tab.getLastUsed()); - - String favicon = tab.getFaviconURL(); - if (favicon != null) - values.put(BrowserContract.Tabs.FAVICON, favicon); - else - values.putNull(BrowserContract.Tabs.FAVICON); - - // We don't have access to session history in Java, so for now, we'll - // just use a JSONArray that holds most recent history item. - try { - history.put(0, tab.getURL()); - values.put(BrowserContract.Tabs.HISTORY, history.toString()); - } catch (JSONException e) { - Log.w(LOGTAG, "JSONException adding URL to tab history array.", e); - } - - values.put(BrowserContract.Tabs.POSITION, position++); - - // A null client guid corresponds to the local client. - values.putNull(BrowserContract.Tabs.CLIENT_GUID); - - valuesToInsert.add(values); - } - - ContentValues[] valuesToInsertArray = valuesToInsert.toArray(new ContentValues[valuesToInsert.size()]); - cr.bulkInsert(tabsUriWithProfile, valuesToInsertArray); - } - - // Deletes all local tabs and replaces them with a new list of tabs. - @Override - public synchronized void persistLocalTabs(final ContentResolver cr, final Iterable tabs) { - deleteLocalTabs(cr); - insertLocalTabs(cr, tabs); - updateLocalClient(cr); - } - - /** - * Matches the supplied URL string against the set of URLs to filter. - * - * @return true if the supplied URL should be skipped; false otherwise. - */ - private boolean isFilteredURL(String url) { - return FILTERED_URL_PATTERN.matcher(url).lookingAt(); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/db/LocalURLMetadata.java b/mobile/android/base/java/org/mozilla/gecko/db/LocalURLMetadata.java deleted file mode 100644 index 7f2c4a7367..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/db/LocalURLMetadata.java +++ /dev/null @@ -1,240 +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.db; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.Set; - -import org.json.JSONException; -import org.json.JSONObject; -import org.mozilla.gecko.GeckoAppShell; -import org.mozilla.gecko.icons.decoders.LoadFaviconResult; -import org.mozilla.gecko.util.ThreadUtils; - -import android.content.ContentResolver; -import android.content.ContentValues; -import android.database.Cursor; -import android.net.Uri; -import android.util.Log; -import android.util.LruCache; - -// Holds metadata info about URLs. Supports some helper functions for getting back a HashMap of key value data. -public class LocalURLMetadata implements URLMetadata { - private static final String LOGTAG = "GeckoURLMetadata"; - private final Uri uriWithProfile; - - public LocalURLMetadata(String mProfile) { - uriWithProfile = DBUtils.appendProfileWithDefault(mProfile, URLMetadataTable.CONTENT_URI); - } - - // A list of columns in the table. It's used to simplify some loops for reading/writing data. - private static final Set COLUMNS; - static { - final HashSet tempModel = new HashSet<>(4); - tempModel.add(URLMetadataTable.URL_COLUMN); - tempModel.add(URLMetadataTable.TILE_IMAGE_URL_COLUMN); - tempModel.add(URLMetadataTable.TILE_COLOR_COLUMN); - tempModel.add(URLMetadataTable.TOUCH_ICON_COLUMN); - COLUMNS = Collections.unmodifiableSet(tempModel); - } - - // Store a cache of recent results. This number is chosen to match the max number of tiles on about:home - private static final int CACHE_SIZE = 9; - // Note: Members of this cache are unmodifiable. - private final LruCache> cache = new LruCache>(CACHE_SIZE); - - /** - * Converts a JSON object into a unmodifiable Map of known metadata properties. - * Will throw away any properties that aren't stored in the database. - * - * Incoming data can include a list like: {touchIconList:{56:"http://x.com/56.png", 76:"http://x.com/76.png"}}. - * This will then be filtered to find the most appropriate touchIcon, i.e. the closest icon size that is larger - * than (or equal to) the preferred homescreen launcher icon size, which is then stored in the "touchIcon" property. - */ - @Override - public Map fromJSON(JSONObject obj) { - Map data = new HashMap(); - - for (String key : COLUMNS) { - if (obj.has(key)) { - data.put(key, obj.optString(key)); - } - } - - - try { - JSONObject icons; - if (obj.has("touchIconList") && - (icons = obj.getJSONObject("touchIconList")).length() > 0) { - int preferredSize = GeckoAppShell.getPreferredIconSize(); - - Iterator keys = icons.keys(); - - ArrayList sizes = new ArrayList(icons.length()); - while (keys.hasNext()) { - sizes.add(new Integer(keys.next())); - } - - final int bestSize = LoadFaviconResult.selectBestSizeFromList(sizes, preferredSize); - final String iconURL = icons.getString(Integer.toString(bestSize)); - - data.put(URLMetadataTable.TOUCH_ICON_COLUMN, iconURL); - } - } catch (JSONException e) { - Log.w(LOGTAG, "Exception processing touchIconList for LocalURLMetadata; ignoring.", e); - } - - return Collections.unmodifiableMap(data); - } - - /** - * Converts a Cursor into a unmodifiable Map of known metadata properties. - * Will throw away any properties that aren't stored in the database. - * Will also not iterate through multiple rows in the cursor. - */ - private Map fromCursor(Cursor c) { - Map data = new HashMap(); - - String[] columns = c.getColumnNames(); - for (String column : columns) { - if (COLUMNS.contains(column)) { - try { - data.put(column, c.getString(c.getColumnIndexOrThrow(column))); - } catch (Exception ex) { - Log.i(LOGTAG, "Error getting data for " + column, ex); - } - } - } - - return Collections.unmodifiableMap(data); - } - - /** - * Returns an unmodifiable Map of url->Metadata (i.e. A second HashMap) for a list of urls. - * Must not be called from UI or Gecko threads. - */ - @Override - public Map> getForURLs(final ContentResolver cr, - final Collection urls, - final List requestedColumns) { - ThreadUtils.assertNotOnUiThread(); - ThreadUtils.assertNotOnGeckoThread(); - - final Map> data = new HashMap>(); - - // Nothing to query for - if (urls.isEmpty() || requestedColumns.isEmpty()) { - Log.e(LOGTAG, "Queried metadata for nothing"); - return data; - } - - // Search the cache for any of these urls - List urlsToQuery = new ArrayList(); - for (String url : urls) { - final Map hit = cache.get(url); - if (hit != null) { - // Cache hit: we've found the URL in the cache, however we may not have cached the desired columns - // for that URL. Hence we need to check whether our cache hit contains those columns, and directly - // retrieve the desired data if not. (E.g. the top sites panel retrieves the tile, and tilecolor. If - // we later try to retrieve the touchIcon for a top-site the cache hit will only point to - // tile+tilecolor, and not the required touchIcon. In this case we don't want to use the cache.) - boolean useCache = true; - for (String c: requestedColumns) { - if (!hit.containsKey(c)) { - useCache = false; - } - } - if (useCache) { - data.put(url, hit); - } else { - urlsToQuery.add(url); - } - } else { - urlsToQuery.add(url); - } - } - - // If everything was in the cache, we're done! - if (urlsToQuery.size() == 0) { - return Collections.unmodifiableMap(data); - } - - final String selection = DBUtils.computeSQLInClause(urlsToQuery.size(), URLMetadataTable.URL_COLUMN); - List columns = requestedColumns; - // We need the url to build our final HashMap, so we force it to be included in the query. - if (!columns.contains(URLMetadataTable.URL_COLUMN)) { - // The requestedColumns may be immutable (e.g. if the caller used Collections.singletonList), hence - // we have to create a copy. - columns = new ArrayList(columns); - columns.add(URLMetadataTable.URL_COLUMN); - } - - final Cursor cursor = cr.query(uriWithProfile, - columns.toArray(new String[columns.size()]), // columns, - selection, // selection - urlsToQuery.toArray(new String[urlsToQuery.size()]), // selectionargs - null); - try { - if (!cursor.moveToFirst()) { - return Collections.unmodifiableMap(data); - } - - do { - final Map metadata = fromCursor(cursor); - final String url = cursor.getString(cursor.getColumnIndexOrThrow(URLMetadataTable.URL_COLUMN)); - - data.put(url, metadata); - cache.put(url, metadata); - } while (cursor.moveToNext()); - - } finally { - cursor.close(); - } - - return Collections.unmodifiableMap(data); - } - - /** - * Saves a HashMap of metadata into the database. Will iterate through columns - * in the Database and only save rows with matching keys in the HashMap. - * Must not be called from UI or Gecko threads. - */ - @Override - public void save(final ContentResolver cr, final Map data) { - ThreadUtils.assertNotOnUiThread(); - ThreadUtils.assertNotOnGeckoThread(); - - try { - ContentValues values = new ContentValues(); - - for (String key : COLUMNS) { - if (data.containsKey(key)) { - values.put(key, (String) data.get(key)); - } - } - - if (values.size() == 0) { - return; - } - - Uri uri = uriWithProfile.buildUpon() - .appendQueryParameter(BrowserContract.PARAM_INSERT_IF_NEEDED, "true") - .build(); - cr.update(uri, values, URLMetadataTable.URL_COLUMN + "=?", new String[] { - (String) data.get(URLMetadataTable.URL_COLUMN) - }); - } catch (Exception ex) { - Log.e(LOGTAG, "error saving", ex); - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/db/LocalUrlAnnotations.java b/mobile/android/base/java/org/mozilla/gecko/db/LocalUrlAnnotations.java deleted file mode 100644 index 9df41a1691..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/db/LocalUrlAnnotations.java +++ /dev/null @@ -1,253 +0,0 @@ -/* 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.db; - -import android.content.ContentResolver; -import android.content.ContentValues; -import android.database.Cursor; -import android.net.Uri; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.util.Log; - -import org.json.JSONException; -import org.mozilla.gecko.annotation.RobocopTarget; -import org.mozilla.gecko.db.BrowserContract.UrlAnnotations.Key; -import org.mozilla.gecko.feeds.subscriptions.FeedSubscription; - -public class LocalUrlAnnotations implements UrlAnnotations { - private static final String LOGTAG = "LocalUrlAnnotations"; - - private Uri urlAnnotationsTableWithProfile; - - public LocalUrlAnnotations(final String profile) { - urlAnnotationsTableWithProfile = DBUtils.appendProfile(profile, BrowserContract.UrlAnnotations.CONTENT_URI); - } - - /** - * Get all feed subscriptions. - */ - @Override - public Cursor getFeedSubscriptions(ContentResolver cr) { - return queryByKey(cr, - Key.FEED_SUBSCRIPTION, - new String[] { BrowserContract.UrlAnnotations.URL, BrowserContract.UrlAnnotations.VALUE }, - null); - } - - /** - * Insert mapping from website URL to URL of the feed. - */ - @Override - public void insertFeedUrl(ContentResolver cr, String originUrl, String feedUrl) { - insertAnnotation(cr, originUrl, Key.FEED, feedUrl); - } - - @Override - public boolean hasAcceptedOrDeclinedHomeScreenShortcut(ContentResolver cr, String url) { - return hasResultsForSelection(cr, - BrowserContract.UrlAnnotations.URL + " = ?", - new String[]{url}); - } - - @Override - public void insertHomeScreenShortcut(ContentResolver cr, String url, boolean hasCreatedShortCut) { - insertAnnotation(cr, url, Key.HOME_SCREEN_SHORTCUT, String.valueOf(hasCreatedShortCut)); - } - - /** - * Returns true if there's a mapping from the given website URL to a feed URL. False otherwise. - */ - @Override - public boolean hasFeedUrlForWebsite(ContentResolver cr, String websiteUrl) { - return hasResultsForSelection(cr, - BrowserContract.UrlAnnotations.URL + " = ? AND " + BrowserContract.UrlAnnotations.KEY + " = ?", - new String[]{websiteUrl, Key.FEED.getDbValue()}); - } - - /** - * Returns true if there's a website URL with this feed URL. False otherwise. - */ - @Override - public boolean hasWebsiteForFeedUrl(ContentResolver cr, String feedUrl) { - return hasResultsForSelection(cr, - BrowserContract.UrlAnnotations.VALUE + " = ? AND " + BrowserContract.UrlAnnotations.KEY + " = ?", - new String[]{feedUrl, Key.FEED.getDbValue()}); - } - - /** - * Delete the feed URL mapping for this website URL. - */ - @Override - public void deleteFeedUrl(ContentResolver cr, String websiteUrl) { - deleteAnnotation(cr, websiteUrl, Key.FEED); - } - - /** - * Get website URLs that are mapped to the given feed URL. - */ - @Override - public Cursor getWebsitesWithFeedUrl(ContentResolver cr) { - return cr.query(urlAnnotationsTableWithProfile, - new String[] { BrowserContract.UrlAnnotations.URL }, - BrowserContract.UrlAnnotations.KEY + " = ?", - new String[] { Key.FEED.getDbValue() }, - null); - } - - /** - * Returns true if there's a subscription for this feed URL. False otherwise. - */ - @Override - public boolean hasFeedSubscription(ContentResolver cr, String feedUrl) { - return hasResultsForSelection(cr, - BrowserContract.UrlAnnotations.URL + " = ? AND " + BrowserContract.UrlAnnotations.KEY + " = ?", - new String[]{feedUrl, Key.FEED_SUBSCRIPTION.getDbValue()}); - } - - /** - * Insert the given feed subscription (Mapping from feed URL to the subscription object). - */ - @Override - public void insertFeedSubscription(ContentResolver cr, FeedSubscription subscription) { - try { - insertAnnotation(cr, subscription.getFeedUrl(), Key.FEED_SUBSCRIPTION, subscription.toJSON().toString()); - } catch (JSONException e) { - Log.w(LOGTAG, "Could not serialize subscription"); - } - } - - /** - * Update the feed subscription with new values. - */ - @Override - public void updateFeedSubscription(ContentResolver cr, FeedSubscription subscription) { - try { - updateAnnotation(cr, subscription.getFeedUrl(), Key.FEED_SUBSCRIPTION, subscription.toJSON().toString()); - } catch (JSONException e) { - Log.w(LOGTAG, "Could not serialize subscription"); - } - } - - /** - * Delete the subscription for the feed URL. - */ - @Override - public void deleteFeedSubscription(ContentResolver cr, FeedSubscription subscription) { - deleteAnnotation(cr, subscription.getFeedUrl(), Key.FEED_SUBSCRIPTION); - } - - private int deleteAnnotation(final ContentResolver cr, final String url, final Key key) { - return cr.delete(urlAnnotationsTableWithProfile, - BrowserContract.UrlAnnotations.KEY + " = ? AND " + BrowserContract.UrlAnnotations.URL + " = ?", - new String[] { key.getDbValue(), url }); - } - - private int updateAnnotation(final ContentResolver cr, final String url, final Key key, final String value) { - ContentValues values = new ContentValues(); - values.put(BrowserContract.UrlAnnotations.VALUE, value); - values.put(BrowserContract.UrlAnnotations.DATE_MODIFIED, System.currentTimeMillis()); - - return cr.update(urlAnnotationsTableWithProfile, - values, - BrowserContract.UrlAnnotations.KEY + " = ? AND " + BrowserContract.UrlAnnotations.URL + " = ?", - new String[]{key.getDbValue(), url}); - } - - private void insertAnnotation(final ContentResolver cr, final String url, final Key key, final String value) { - insertAnnotation(cr, url, key.getDbValue(), value); - } - - @RobocopTarget - @Override - public void insertAnnotation(final ContentResolver cr, final String url, final String key, final String value) { - final long creationTime = System.currentTimeMillis(); - final ContentValues values = new ContentValues(5); - values.put(BrowserContract.UrlAnnotations.URL, url); - values.put(BrowserContract.UrlAnnotations.KEY, key); - values.put(BrowserContract.UrlAnnotations.VALUE, value); - values.put(BrowserContract.UrlAnnotations.DATE_CREATED, creationTime); - values.put(BrowserContract.UrlAnnotations.DATE_MODIFIED, creationTime); - cr.insert(urlAnnotationsTableWithProfile, values); - } - - /** - * @return true if the table contains rows for the given selection. - */ - private boolean hasResultsForSelection(ContentResolver cr, String selection, String[] selectionArgs) { - Cursor cursor = cr.query(urlAnnotationsTableWithProfile, - new String[] { BrowserContract.UrlAnnotations._ID }, - selection, - selectionArgs, - null); - if (cursor == null) { - return false; - } - - try { - return cursor.getCount() > 0; - } finally { - cursor.close(); - } - } - - private Cursor queryByKey(final ContentResolver cr, @NonNull final Key key, @Nullable final String[] projections, - @Nullable final String sortOrder) { - return cr.query(urlAnnotationsTableWithProfile, - projections, - BrowserContract.UrlAnnotations.KEY + " = ?", new String[] { key.getDbValue() }, - sortOrder); - } - - @Override - public Cursor getScreenshots(ContentResolver cr) { - return queryByKey(cr, - Key.SCREENSHOT, - new String[] { - BrowserContract.UrlAnnotations._ID, - BrowserContract.UrlAnnotations.URL, - BrowserContract.UrlAnnotations.KEY, - BrowserContract.UrlAnnotations.VALUE, - BrowserContract.UrlAnnotations.DATE_CREATED, - }, - BrowserContract.UrlAnnotations.DATE_CREATED + " DESC"); - } - - public void insertScreenshot(final ContentResolver cr, final String pageUrl, final String screenshotPath) { - insertAnnotation(cr, pageUrl, Key.SCREENSHOT.getDbValue(), screenshotPath); - } - - @Override - public void insertReaderViewUrl(final ContentResolver cr, final String pageUrl) { - insertAnnotation(cr, pageUrl, Key.READER_VIEW.getDbValue(), BrowserContract.UrlAnnotations.READER_VIEW_SAVED_VALUE); - } - - @Override - public void deleteReaderViewUrl(ContentResolver cr, String pageURL) { - deleteAnnotation(cr, pageURL, Key.READER_VIEW); - } - - public int getAnnotationCount(ContentResolver cr, Key key) { - final String countColumnname = "count"; - final Cursor c = queryByKey(cr, - key, - new String[] { - "COUNT(*) AS " + countColumnname - }, - null); - - try { - if (c != null && c.moveToFirst()) { - return c.getInt(c.getColumnIndexOrThrow(countColumnname)); - } else { - return 0; - } - } finally { - if (c != null) { - c.close(); - } - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/db/LoginsProvider.java b/mobile/android/base/java/org/mozilla/gecko/db/LoginsProvider.java deleted file mode 100644 index d2d5048510..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/db/LoginsProvider.java +++ /dev/null @@ -1,520 +0,0 @@ -/* 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.db; - -import android.content.ContentUris; -import android.content.ContentValues; -import android.content.UriMatcher; -import android.database.Cursor; -import android.database.DatabaseUtils; -import android.database.MatrixCursor; -import android.database.sqlite.SQLiteDatabase; -import android.database.sqlite.SQLiteQueryBuilder; -import android.net.Uri; -import android.support.annotation.NonNull; -import android.text.TextUtils; -import android.util.Base64; - -import org.mozilla.gecko.db.BrowserContract.DeletedLogins; -import org.mozilla.gecko.db.BrowserContract.Logins; -import org.mozilla.gecko.db.BrowserContract.LoginsDisabledHosts; -import org.mozilla.gecko.sync.Utils; - -import java.io.UnsupportedEncodingException; -import java.security.GeneralSecurityException; -import java.util.HashMap; - -import javax.crypto.Cipher; -import javax.crypto.NullCipher; - -import static org.mozilla.gecko.db.BrowserContract.DeletedLogins.TABLE_DELETED_LOGINS; -import static org.mozilla.gecko.db.BrowserContract.Logins.TABLE_LOGINS; -import static org.mozilla.gecko.db.BrowserContract.LoginsDisabledHosts.TABLE_DISABLED_HOSTS; - -public class LoginsProvider extends SharedBrowserDatabaseProvider { - - private static final int LOGINS = 100; - private static final int LOGINS_ID = 101; - private static final int DELETED_LOGINS = 102; - private static final int DELETED_LOGINS_ID = 103; - private static final int DISABLED_HOSTS = 104; - private static final int DISABLED_HOSTS_HOSTNAME = 105; - private static final UriMatcher URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH); - - private static final HashMap LOGIN_PROJECTION_MAP; - private static final HashMap DELETED_LOGIN_PROJECTION_MAP; - private static final HashMap DISABLED_HOSTS_PROJECTION_MAP; - - private static final String DEFAULT_LOGINS_SORT_ORDER = Logins.HOSTNAME + " ASC"; - private static final String DEFAULT_DELETED_LOGINS_SORT_ORDER = DeletedLogins.TIME_DELETED + " ASC"; - private static final String DEFAULT_DISABLED_HOSTS_SORT_ORDER = LoginsDisabledHosts.HOSTNAME + " ASC"; - private static final String WHERE_GUID_IS_NULL = DeletedLogins.GUID + " IS NULL"; - private static final String WHERE_GUID_IS_VALUE = DeletedLogins.GUID + " = ?"; - - protected static final String INDEX_LOGINS_HOSTNAME = "login_hostname_index"; - protected static final String INDEX_LOGINS_HOSTNAME_FORM_SUBMIT_URL = "login_hostname_formSubmitURL_index"; - protected static final String INDEX_LOGINS_HOSTNAME_HTTP_REALM = "login_hostname_httpRealm_index"; - - static { - URI_MATCHER.addURI(BrowserContract.LOGINS_AUTHORITY, "logins", LOGINS); - URI_MATCHER.addURI(BrowserContract.LOGINS_AUTHORITY, "logins/#", LOGINS_ID); - URI_MATCHER.addURI(BrowserContract.LOGINS_AUTHORITY, "deleted-logins", DELETED_LOGINS); - URI_MATCHER.addURI(BrowserContract.LOGINS_AUTHORITY, "deleted-logins/#", DELETED_LOGINS_ID); - URI_MATCHER.addURI(BrowserContract.LOGINS_AUTHORITY, "logins-disabled-hosts", DISABLED_HOSTS); - URI_MATCHER.addURI(BrowserContract.LOGINS_AUTHORITY, "logins-disabled-hosts/hostname/*", DISABLED_HOSTS_HOSTNAME); - - LOGIN_PROJECTION_MAP = new HashMap<>(); - LOGIN_PROJECTION_MAP.put(Logins._ID, Logins._ID); - LOGIN_PROJECTION_MAP.put(Logins.HOSTNAME, Logins.HOSTNAME); - LOGIN_PROJECTION_MAP.put(Logins.HTTP_REALM, Logins.HTTP_REALM); - LOGIN_PROJECTION_MAP.put(Logins.FORM_SUBMIT_URL, Logins.FORM_SUBMIT_URL); - LOGIN_PROJECTION_MAP.put(Logins.USERNAME_FIELD, Logins.USERNAME_FIELD); - LOGIN_PROJECTION_MAP.put(Logins.PASSWORD_FIELD, Logins.PASSWORD_FIELD); - LOGIN_PROJECTION_MAP.put(Logins.ENCRYPTED_USERNAME, Logins.ENCRYPTED_USERNAME); - LOGIN_PROJECTION_MAP.put(Logins.ENCRYPTED_PASSWORD, Logins.ENCRYPTED_PASSWORD); - LOGIN_PROJECTION_MAP.put(Logins.GUID, Logins.GUID); - LOGIN_PROJECTION_MAP.put(Logins.ENC_TYPE, Logins.ENC_TYPE); - LOGIN_PROJECTION_MAP.put(Logins.TIME_CREATED, Logins.TIME_CREATED); - LOGIN_PROJECTION_MAP.put(Logins.TIME_LAST_USED, Logins.TIME_LAST_USED); - LOGIN_PROJECTION_MAP.put(Logins.TIME_PASSWORD_CHANGED, Logins.TIME_PASSWORD_CHANGED); - LOGIN_PROJECTION_MAP.put(Logins.TIMES_USED, Logins.TIMES_USED); - - DELETED_LOGIN_PROJECTION_MAP = new HashMap<>(); - DELETED_LOGIN_PROJECTION_MAP.put(DeletedLogins._ID, DeletedLogins._ID); - DELETED_LOGIN_PROJECTION_MAP.put(DeletedLogins.GUID, DeletedLogins.GUID); - DELETED_LOGIN_PROJECTION_MAP.put(DeletedLogins.TIME_DELETED, DeletedLogins.TIME_DELETED); - - DISABLED_HOSTS_PROJECTION_MAP = new HashMap<>(); - DISABLED_HOSTS_PROJECTION_MAP.put(LoginsDisabledHosts._ID, LoginsDisabledHosts._ID); - DISABLED_HOSTS_PROJECTION_MAP.put(LoginsDisabledHosts.HOSTNAME, LoginsDisabledHosts.HOSTNAME); - } - - private static String projectColumn(String table, String column) { - return table + "." + column; - } - - private static String selectColumn(String table, String column) { - return projectColumn(table, column) + " = ?"; - } - - @Override - protected Uri insertInTransaction(Uri uri, ContentValues values) { - trace("Calling insert in transaction on URI: " + uri); - - final int match = URI_MATCHER.match(uri); - final SQLiteDatabase db = getWritableDatabase(uri); - final long id; - String guid; - - setupDefaultValues(values, uri); - switch (match) { - case LOGINS: - removeDeletedLoginsByGUIDInTransaction(values, db); - // Encrypt sensitive data. - encryptContentValueFields(values); - guid = values.getAsString(Logins.GUID); - debug("Inserting login in database with GUID: " + guid); - id = db.insertOrThrow(TABLE_LOGINS, Logins.GUID, values); - break; - - case DELETED_LOGINS: - guid = values.getAsString(DeletedLogins.GUID); - debug("Inserting deleted-login in database with GUID: " + guid); - id = db.insertOrThrow(TABLE_DELETED_LOGINS, DeletedLogins.GUID, values); - break; - - case DISABLED_HOSTS: - String hostname = values.getAsString(LoginsDisabledHosts.HOSTNAME); - debug("Inserting disabled-host in database with hostname: " + hostname); - id = db.insertOrThrow(TABLE_DISABLED_HOSTS, LoginsDisabledHosts.HOSTNAME, values); - break; - - default: - throw new UnsupportedOperationException("Unknown insert URI " + uri); - } - - debug("Inserted ID in database: " + id); - - if (id >= 0) { - return ContentUris.withAppendedId(uri, id); - } - - return null; - } - - @Override - @SuppressWarnings("fallthrough") - protected int deleteInTransaction(Uri uri, String selection, String[] selectionArgs) { - trace("Calling delete in transaction on URI: " + uri); - - final int match = URI_MATCHER.match(uri); - final String table; - final SQLiteDatabase db = getWritableDatabase(uri); - - beginWrite(db); - switch (match) { - case LOGINS_ID: - trace("Delete on LOGINS_ID: " + uri); - selection = DBUtils.concatenateWhere(selection, selectColumn(TABLE_LOGINS, Logins._ID)); - selectionArgs = DBUtils.appendSelectionArgs(selectionArgs, - new String[]{Long.toString(ContentUris.parseId(uri))}); - // Store the deleted client in deleted-logins table. - final String guid = getLoginGUIDByID(selection, selectionArgs, db); - if (guid == null) { - // No matching logins found for the id. - return 0; - } - boolean isInsertSuccessful = storeDeletedLoginForGUIDInTransaction(guid, db); - if (!isInsertSuccessful) { - // Failed to insert into deleted-logins, return early. - return 0; - } - // fall through - case LOGINS: - trace("Delete on LOGINS: " + uri); - table = TABLE_LOGINS; - break; - - case DELETED_LOGINS_ID: - trace("Delete on DELETED_LOGINS_ID: " + uri); - selection = DBUtils.concatenateWhere(selection, selectColumn(TABLE_DELETED_LOGINS, DeletedLogins._ID)); - selectionArgs = DBUtils.appendSelectionArgs(selectionArgs, - new String[]{Long.toString(ContentUris.parseId(uri))}); - // fall through - case DELETED_LOGINS: - trace("Delete on DELETED_LOGINS_ID: " + uri); - table = TABLE_DELETED_LOGINS; - break; - - case DISABLED_HOSTS_HOSTNAME: - trace("Delete on DISABLED_HOSTS_HOSTNAME: " + uri); - selection = DBUtils.concatenateWhere(selection, selectColumn(TABLE_DISABLED_HOSTS, LoginsDisabledHosts.HOSTNAME)); - selectionArgs = DBUtils.appendSelectionArgs(selectionArgs, - new String[]{uri.getLastPathSegment()}); - // fall through - case DISABLED_HOSTS: - trace("Delete on DISABLED_HOSTS: " + uri); - table = TABLE_DISABLED_HOSTS; - break; - - default: - throw new UnsupportedOperationException("Unknown delete URI " + uri); - } - - debug("Deleting " + table + " for URI: " + uri); - return db.delete(table, selection, selectionArgs); - } - - @Override - @SuppressWarnings("fallthrough") - protected int updateInTransaction(Uri uri, ContentValues values, String selection, String[] selectionArgs) { - trace("Calling update in transaction on URI: " + uri); - - final int match = URI_MATCHER.match(uri); - final SQLiteDatabase db = getWritableDatabase(uri); - final String table; - - beginWrite(db); - switch (match) { - case LOGINS_ID: - trace("Update on LOGINS_ID: " + uri); - selection = DBUtils.concatenateWhere(selection, selectColumn(TABLE_LOGINS, Logins._ID)); - selectionArgs = DBUtils.appendSelectionArgs(selectionArgs, - new String[]{Long.toString(ContentUris.parseId(uri))}); - - case LOGINS: - trace("Update on LOGINS: " + uri); - table = TABLE_LOGINS; - // Encrypt sensitive data. - encryptContentValueFields(values); - break; - - default: - throw new UnsupportedOperationException("Unknown update URI " + uri); - } - - trace("Updating " + table + " on URI: " + uri); - return db.update(table, values, selection, selectionArgs); - - } - - @Override - @SuppressWarnings("fallthrough") - public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { - trace("Calling query on URI: " + uri); - - final SQLiteDatabase db = getReadableDatabase(uri); - final int match = URI_MATCHER.match(uri); - final String groupBy = null; - final SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); - final String limit = uri.getQueryParameter(BrowserContract.PARAM_LIMIT); - - switch (match) { - case LOGINS_ID: - trace("Query is on LOGINS_ID: " + uri); - selection = DBUtils.concatenateWhere(selection, selectColumn(TABLE_LOGINS, Logins._ID)); - selectionArgs = DBUtils.appendSelectionArgs(selectionArgs, - new String[] { Long.toString(ContentUris.parseId(uri)) }); - - // fall through - case LOGINS: - trace("Query is on LOGINS: " + uri); - if (TextUtils.isEmpty(sortOrder)) { - sortOrder = DEFAULT_LOGINS_SORT_ORDER; - } else { - debug("Using sort order " + sortOrder + "."); - } - - qb.setProjectionMap(LOGIN_PROJECTION_MAP); - qb.setTables(TABLE_LOGINS); - break; - - case DELETED_LOGINS_ID: - trace("Query is on DELETED_LOGINS_ID: " + uri); - selection = DBUtils.concatenateWhere(selection, selectColumn(TABLE_DELETED_LOGINS, DeletedLogins._ID)); - selectionArgs = DBUtils.appendSelectionArgs(selectionArgs, - new String[] { Long.toString(ContentUris.parseId(uri)) }); - - // fall through - case DELETED_LOGINS: - trace("Query is on DELETED_LOGINS: " + uri); - if (TextUtils.isEmpty(sortOrder)) { - sortOrder = DEFAULT_DELETED_LOGINS_SORT_ORDER; - } else { - debug("Using sort order " + sortOrder + "."); - } - - qb.setProjectionMap(DELETED_LOGIN_PROJECTION_MAP); - qb.setTables(TABLE_DELETED_LOGINS); - break; - - case DISABLED_HOSTS_HOSTNAME: - trace("Query is on DISABLED_HOSTS_HOSTNAME: " + uri); - selection = DBUtils.concatenateWhere(selection, selectColumn(TABLE_DISABLED_HOSTS, LoginsDisabledHosts.HOSTNAME)); - selectionArgs = DBUtils.appendSelectionArgs(selectionArgs, - new String[] { uri.getLastPathSegment() }); - - // fall through - case DISABLED_HOSTS: - trace("Query is on DISABLED_HOSTS: " + uri); - if (TextUtils.isEmpty(sortOrder)) { - sortOrder = DEFAULT_DISABLED_HOSTS_SORT_ORDER; - } else { - debug("Using sort order " + sortOrder + "."); - } - - qb.setProjectionMap(DISABLED_HOSTS_PROJECTION_MAP); - qb.setTables(TABLE_DISABLED_HOSTS); - break; - - default: - throw new UnsupportedOperationException("Unknown query URI " + uri); - } - - trace("Running built query."); - Cursor cursor = qb.query(db, projection, selection, selectionArgs, groupBy, null, sortOrder, limit); - // If decryptManyCursorRows does not return the original cursor, it closes it, so there's - // no need to close here. - cursor = decryptManyCursorRows(cursor); - cursor.setNotificationUri(getContext().getContentResolver(), BrowserContract.LOGINS_AUTHORITY_URI); - return cursor; - } - - @Override - public String getType(@NonNull Uri uri) { - final int match = URI_MATCHER.match(uri); - - switch (match) { - case LOGINS: - return Logins.CONTENT_TYPE; - - case LOGINS_ID: - return Logins.CONTENT_ITEM_TYPE; - - case DELETED_LOGINS: - return DeletedLogins.CONTENT_TYPE; - - case DELETED_LOGINS_ID: - return DeletedLogins.CONTENT_ITEM_TYPE; - - case DISABLED_HOSTS: - return LoginsDisabledHosts.CONTENT_TYPE; - - case DISABLED_HOSTS_HOSTNAME: - return LoginsDisabledHosts.CONTENT_ITEM_TYPE; - - default: - throw new UnsupportedOperationException("Unknown type " + uri); - } - } - - /** - * Caller is responsible for invoking this method inside a transaction. - */ - private String getLoginGUIDByID(final String selection, final String[] selectionArgs, final SQLiteDatabase db) { - final Cursor cursor = db.query(Logins.TABLE_LOGINS, new String[]{Logins.GUID}, selection, selectionArgs, null, null, DEFAULT_LOGINS_SORT_ORDER); - try { - if (!cursor.moveToFirst()) { - return null; - } - return cursor.getString(cursor.getColumnIndexOrThrow(Logins.GUID)); - } finally { - cursor.close(); - } - } - - /** - * Caller is responsible for invoking this method inside a transaction. - */ - private boolean storeDeletedLoginForGUIDInTransaction(final String guid, final SQLiteDatabase db) { - if (guid == null) { - return false; - } - final ContentValues values = new ContentValues(); - values.put(DeletedLogins.GUID, guid); - values.put(DeletedLogins.TIME_DELETED, System.currentTimeMillis()); - return db.insert(TABLE_DELETED_LOGINS, DeletedLogins.GUID, values) > 0; - } - - /** - * Caller is responsible for invoking this method inside a transaction. - */ - private void removeDeletedLoginsByGUIDInTransaction(ContentValues values, SQLiteDatabase db) { - if (values.containsKey(Logins.GUID)) { - final String guid = values.getAsString(Logins.GUID); - if (guid == null) { - db.delete(TABLE_DELETED_LOGINS, WHERE_GUID_IS_NULL, null); - } else { - String[] args = new String[]{guid}; - db.delete(TABLE_DELETED_LOGINS, WHERE_GUID_IS_VALUE, args); - } - } - } - - private void setupDefaultValues(ContentValues values, Uri uri) throws IllegalArgumentException { - final int match = URI_MATCHER.match(uri); - final long now = System.currentTimeMillis(); - switch (match) { - case DELETED_LOGINS: - values.put(DeletedLogins.TIME_DELETED, now); - // deleted-logins must contain a guid - if (!values.containsKey(DeletedLogins.GUID)) { - throw new IllegalArgumentException("Must provide GUID for deleted-login"); - } - break; - - case LOGINS: - values.put(Logins.TIME_CREATED, now); - // Generate GUID for new login. Don't override specified GUIDs. - if (!values.containsKey(Logins.GUID)) { - final String guid = Utils.generateGuid(); - values.put(Logins.GUID, guid); - } - // The database happily accepts strings for long values; this just lets us re-use - // the existing helper method. - String nowString = Long.toString(now); - DBUtils.replaceKey(values, null, Logins.HTTP_REALM, null); - DBUtils.replaceKey(values, null, Logins.FORM_SUBMIT_URL, null); - DBUtils.replaceKey(values, null, Logins.ENC_TYPE, "0"); - DBUtils.replaceKey(values, null, Logins.TIME_LAST_USED, nowString); - DBUtils.replaceKey(values, null, Logins.TIME_PASSWORD_CHANGED, nowString); - DBUtils.replaceKey(values, null, Logins.TIMES_USED, "0"); - break; - - case DISABLED_HOSTS: - if (!values.containsKey(LoginsDisabledHosts.HOSTNAME)) { - throw new IllegalArgumentException("Must provide hostname for disabled-host"); - } - break; - - default: - throw new UnsupportedOperationException("Unknown URI in setupDefaultValues " + uri); - } - } - - private void encryptContentValueFields(final ContentValues values) { - if (values.containsKey(Logins.ENCRYPTED_PASSWORD)) { - final String res = encrypt(values.getAsString(Logins.ENCRYPTED_PASSWORD)); - values.put(Logins.ENCRYPTED_PASSWORD, res); - } - - if (values.containsKey(Logins.ENCRYPTED_USERNAME)) { - final String res = encrypt(values.getAsString(Logins.ENCRYPTED_USERNAME)); - values.put(Logins.ENCRYPTED_USERNAME, res); - } - } - - /** - * Replace each password and username encrypted ciphertext with its equivalent decrypted - * plaintext in the given cursor. - *

- * The encryption algorithm used to protect logins is unspecified; and further, a consumer of - * consumers should never have access to encrypted ciphertext. - * - * @param cursor containing at least one of password and username encrypted ciphertexts. - * @return a new {@link Cursor} with password and username decrypted plaintexts. - */ - private Cursor decryptManyCursorRows(final Cursor cursor) { - final int passwordIndex = cursor.getColumnIndex(Logins.ENCRYPTED_PASSWORD); - final int usernameIndex = cursor.getColumnIndex(Logins.ENCRYPTED_USERNAME); - - if (passwordIndex == -1 && usernameIndex == -1) { - return cursor; - } - - // Special case, decrypt the encrypted username or password before returning the cursor. - final MatrixCursor newCursor = new MatrixCursor(cursor.getColumnNames(), cursor.getColumnCount()); - try { - for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) { - final ContentValues values = new ContentValues(); - DatabaseUtils.cursorRowToContentValues(cursor, values); - - if (passwordIndex > -1) { - String decrypted = decrypt(values.getAsString(Logins.ENCRYPTED_PASSWORD)); - values.put(Logins.ENCRYPTED_PASSWORD, decrypted); - } - - if (usernameIndex > -1) { - String decrypted = decrypt(values.getAsString(Logins.ENCRYPTED_USERNAME)); - values.put(Logins.ENCRYPTED_USERNAME, decrypted); - } - - final MatrixCursor.RowBuilder rowBuilder = newCursor.newRow(); - for (String key : cursor.getColumnNames()) { - rowBuilder.add(values.get(key)); - } - } - } finally { - // Close the old cursor before returning the new one. - cursor.close(); - } - - return newCursor; - } - - private String encrypt(@NonNull String initialValue) { - try { - final Cipher cipher = getCipher(Cipher.ENCRYPT_MODE); - return Base64.encodeToString(cipher.doFinal(initialValue.getBytes("UTF-8")), Base64.URL_SAFE); - } catch (Exception e) { - debug("encryption failed : " + e); - throw new IllegalStateException("Logins encryption failed", e); - } - } - - private String decrypt(@NonNull String initialValue) { - try { - final Cipher cipher = getCipher(Cipher.DECRYPT_MODE); - return new String(cipher.doFinal(Base64.decode(initialValue.getBytes("UTF-8"), Base64.URL_SAFE))); - } catch (Exception e) { - debug("Decryption failed : " + e); - throw new IllegalStateException("Logins decryption failed", e); - } - } - - private Cipher getCipher(int mode) throws UnsupportedEncodingException, GeneralSecurityException { - return new NullCipher(); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/db/PasswordsProvider.java b/mobile/android/base/java/org/mozilla/gecko/db/PasswordsProvider.java deleted file mode 100644 index 2f5e11ed46..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/db/PasswordsProvider.java +++ /dev/null @@ -1,348 +0,0 @@ -/* 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.db; - -import java.util.HashMap; - -import org.mozilla.gecko.CrashHandler; -import org.mozilla.gecko.GeckoApp; -import org.mozilla.gecko.GeckoAppShell; -import org.mozilla.gecko.GeckoMessageReceiver; -import org.mozilla.gecko.NSSBridge; -import org.mozilla.gecko.db.BrowserContract.DeletedPasswords; -import org.mozilla.gecko.db.BrowserContract.GeckoDisabledHosts; -import org.mozilla.gecko.db.BrowserContract.Passwords; -import org.mozilla.gecko.mozglue.GeckoLoader; -import org.mozilla.gecko.sqlite.MatrixBlobCursor; -import org.mozilla.gecko.sqlite.SQLiteBridge; -import org.mozilla.gecko.sync.Utils; - -import android.content.ContentValues; -import android.content.Intent; -import android.content.UriMatcher; -import android.database.Cursor; -import android.net.Uri; -import android.text.TextUtils; -import android.util.Log; - -public class PasswordsProvider extends SQLiteBridgeContentProvider { - static final String TABLE_PASSWORDS = "moz_logins"; - static final String TABLE_DELETED_PASSWORDS = "moz_deleted_logins"; - static final String TABLE_DISABLED_HOSTS = "moz_disabledHosts"; - - private static final String TELEMETRY_TAG = "SQLITEBRIDGE_PROVIDER_PASSWORDS"; - - private static final int PASSWORDS = 100; - private static final int DELETED_PASSWORDS = 101; - private static final int DISABLED_HOSTS = 102; - - static final String DEFAULT_PASSWORDS_SORT_ORDER = Passwords.HOSTNAME + " ASC"; - static final String DEFAULT_DELETED_PASSWORDS_SORT_ORDER = DeletedPasswords.TIME_DELETED + " ASC"; - - private static final UriMatcher URI_MATCHER; - - private static final HashMap PASSWORDS_PROJECTION_MAP; - private static final HashMap DELETED_PASSWORDS_PROJECTION_MAP; - private static final HashMap DISABLED_HOSTS_PROJECTION_MAP; - - // this should be kept in sync with the version in toolkit/components/passwordmgr/storage-mozStorage.js - private static final int DB_VERSION = 6; - private static final String DB_FILENAME = "signons.sqlite"; - private static final String WHERE_GUID_IS_NULL = BrowserContract.DeletedPasswords.GUID + " IS NULL"; - private static final String WHERE_GUID_IS_VALUE = BrowserContract.DeletedPasswords.GUID + " = ?"; - - private static final String LOG_TAG = "GeckoPasswordsProvider"; - - private CrashHandler mCrashHandler; - - static { - URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH); - - // content://org.mozilla.gecko.providers.browser/passwords/# - URI_MATCHER.addURI(BrowserContract.PASSWORDS_AUTHORITY, "passwords", PASSWORDS); - - PASSWORDS_PROJECTION_MAP = new HashMap(); - PASSWORDS_PROJECTION_MAP.put(Passwords.ID, Passwords.ID); - PASSWORDS_PROJECTION_MAP.put(Passwords.HOSTNAME, Passwords.HOSTNAME); - PASSWORDS_PROJECTION_MAP.put(Passwords.HTTP_REALM, Passwords.HTTP_REALM); - PASSWORDS_PROJECTION_MAP.put(Passwords.FORM_SUBMIT_URL, Passwords.FORM_SUBMIT_URL); - PASSWORDS_PROJECTION_MAP.put(Passwords.USERNAME_FIELD, Passwords.USERNAME_FIELD); - PASSWORDS_PROJECTION_MAP.put(Passwords.PASSWORD_FIELD, Passwords.PASSWORD_FIELD); - PASSWORDS_PROJECTION_MAP.put(Passwords.ENCRYPTED_USERNAME, Passwords.ENCRYPTED_USERNAME); - PASSWORDS_PROJECTION_MAP.put(Passwords.ENCRYPTED_PASSWORD, Passwords.ENCRYPTED_PASSWORD); - PASSWORDS_PROJECTION_MAP.put(Passwords.GUID, Passwords.GUID); - PASSWORDS_PROJECTION_MAP.put(Passwords.ENC_TYPE, Passwords.ENC_TYPE); - PASSWORDS_PROJECTION_MAP.put(Passwords.TIME_CREATED, Passwords.TIME_CREATED); - PASSWORDS_PROJECTION_MAP.put(Passwords.TIME_LAST_USED, Passwords.TIME_LAST_USED); - PASSWORDS_PROJECTION_MAP.put(Passwords.TIME_PASSWORD_CHANGED, Passwords.TIME_PASSWORD_CHANGED); - PASSWORDS_PROJECTION_MAP.put(Passwords.TIMES_USED, Passwords.TIMES_USED); - - URI_MATCHER.addURI(BrowserContract.PASSWORDS_AUTHORITY, "deleted-passwords", DELETED_PASSWORDS); - - DELETED_PASSWORDS_PROJECTION_MAP = new HashMap(); - DELETED_PASSWORDS_PROJECTION_MAP.put(DeletedPasswords.ID, DeletedPasswords.ID); - DELETED_PASSWORDS_PROJECTION_MAP.put(DeletedPasswords.GUID, DeletedPasswords.GUID); - DELETED_PASSWORDS_PROJECTION_MAP.put(DeletedPasswords.TIME_DELETED, DeletedPasswords.TIME_DELETED); - - URI_MATCHER.addURI(BrowserContract.PASSWORDS_AUTHORITY, "disabled-hosts", DISABLED_HOSTS); - - DISABLED_HOSTS_PROJECTION_MAP = new HashMap(); - DISABLED_HOSTS_PROJECTION_MAP.put(GeckoDisabledHosts.HOSTNAME, GeckoDisabledHosts.HOSTNAME); - } - - public PasswordsProvider() { - super(LOG_TAG); - } - - @Override - public boolean onCreate() { - mCrashHandler = CrashHandler.createDefaultCrashHandler(getContext()); - - // We don't use .loadMozGlue because we're in a different process, - // and we just want to reuse code rather than use the loader lock etc. - GeckoLoader.doLoadLibrary(getContext(), "mozglue"); - return super.onCreate(); - } - - @Override - public void shutdown() { - super.shutdown(); - - if (mCrashHandler != null) { - mCrashHandler.unregister(); - mCrashHandler = null; - } - } - - @Override - protected String getDBName() { - return DB_FILENAME; - } - - @Override - protected String getTelemetryPrefix() { - return TELEMETRY_TAG; - } - - @Override - protected int getDBVersion() { - return DB_VERSION; - } - - @Override - public String getType(Uri uri) { - final int match = URI_MATCHER.match(uri); - - switch (match) { - case PASSWORDS: - return Passwords.CONTENT_TYPE; - - case DELETED_PASSWORDS: - return DeletedPasswords.CONTENT_TYPE; - - case DISABLED_HOSTS: - return GeckoDisabledHosts.CONTENT_TYPE; - - default: - throw new UnsupportedOperationException("Unknown type " + uri); - } - } - - @Override - public String getTable(Uri uri) { - final int match = URI_MATCHER.match(uri); - switch (match) { - case DELETED_PASSWORDS: - return TABLE_DELETED_PASSWORDS; - - case PASSWORDS: - return TABLE_PASSWORDS; - - case DISABLED_HOSTS: - return TABLE_DISABLED_HOSTS; - - default: - throw new UnsupportedOperationException("Unknown table " + uri); - } - } - - @Override - public String getSortOrder(Uri uri, String aRequested) { - if (!TextUtils.isEmpty(aRequested)) { - return aRequested; - } - - final int match = URI_MATCHER.match(uri); - switch (match) { - case DELETED_PASSWORDS: - return DEFAULT_DELETED_PASSWORDS_SORT_ORDER; - - case PASSWORDS: - return DEFAULT_PASSWORDS_SORT_ORDER; - - case DISABLED_HOSTS: - return null; - - default: - throw new UnsupportedOperationException("Unknown URI " + uri); - } - } - - @Override - public void setupDefaults(Uri uri, ContentValues values) - throws IllegalArgumentException { - int match = URI_MATCHER.match(uri); - long now = System.currentTimeMillis(); - switch (match) { - case DELETED_PASSWORDS: - values.put(DeletedPasswords.TIME_DELETED, now); - - // Deleted passwords must contain a guid - if (!values.containsKey(Passwords.GUID)) { - throw new IllegalArgumentException("Must provide a GUID for a deleted password"); - } - break; - - case PASSWORDS: - values.put(Passwords.TIME_CREATED, now); - - // Generate GUID for new password. Don't override specified GUIDs. - if (!values.containsKey(Passwords.GUID)) { - String guid = Utils.generateGuid(); - values.put(Passwords.GUID, guid); - } - String nowString = Long.toString(now); - DBUtils.replaceKey(values, null, Passwords.HOSTNAME, ""); - DBUtils.replaceKey(values, null, Passwords.HTTP_REALM, ""); - DBUtils.replaceKey(values, null, Passwords.FORM_SUBMIT_URL, ""); - DBUtils.replaceKey(values, null, Passwords.USERNAME_FIELD, ""); - DBUtils.replaceKey(values, null, Passwords.PASSWORD_FIELD, ""); - DBUtils.replaceKey(values, null, Passwords.ENCRYPTED_USERNAME, ""); - DBUtils.replaceKey(values, null, Passwords.ENCRYPTED_PASSWORD, ""); - DBUtils.replaceKey(values, null, Passwords.ENC_TYPE, "0"); - DBUtils.replaceKey(values, null, Passwords.TIME_LAST_USED, nowString); - DBUtils.replaceKey(values, null, Passwords.TIME_PASSWORD_CHANGED, nowString); - DBUtils.replaceKey(values, null, Passwords.TIMES_USED, "0"); - break; - - case DISABLED_HOSTS: - if (!values.containsKey(GeckoDisabledHosts.HOSTNAME)) { - throw new IllegalArgumentException("Must provide a hostname for a disabled host"); - } - break; - - default: - throw new UnsupportedOperationException("Unknown URI " + uri); - } - } - - @Override - public void initGecko() { - // We're not in the main process. The receiver of this Intent can - // communicate with Gecko in the main process. - Intent initIntent = new Intent(getContext(), GeckoMessageReceiver.class); - initIntent.setAction(GeckoApp.ACTION_INIT_PW); - mContext.sendBroadcast(initIntent); - } - - private String doCrypto(String initialValue, Uri uri, Boolean encrypt) { - String profilePath = null; - if (uri != null) { - profilePath = uri.getQueryParameter(BrowserContract.PARAM_PROFILE_PATH); - } - - String result = ""; - try { - if (encrypt) { - if (profilePath != null) { - result = NSSBridge.encrypt(mContext, profilePath, initialValue); - } else { - result = NSSBridge.encrypt(mContext, initialValue); - } - } else { - if (profilePath != null) { - result = NSSBridge.decrypt(mContext, profilePath, initialValue); - } else { - result = NSSBridge.decrypt(mContext, initialValue); - } - } - } catch (Exception ex) { - Log.e(LOG_TAG, "Error in NSSBridge"); - throw new RuntimeException(ex); - } - return result; - } - - @Override - public void onPreInsert(ContentValues values, Uri uri, SQLiteBridge db) { - if (values.containsKey(Passwords.GUID)) { - String guid = values.getAsString(Passwords.GUID); - if (guid == null) { - db.delete(TABLE_DELETED_PASSWORDS, WHERE_GUID_IS_NULL, null); - return; - } - String[] args = new String[] { guid }; - db.delete(TABLE_DELETED_PASSWORDS, WHERE_GUID_IS_VALUE, args); - } - - if (values.containsKey(Passwords.ENCRYPTED_PASSWORD)) { - String res = doCrypto(values.getAsString(Passwords.ENCRYPTED_PASSWORD), uri, true); - values.put(Passwords.ENCRYPTED_PASSWORD, res); - values.put(Passwords.ENC_TYPE, Passwords.ENCTYPE_SDR); - } - - if (values.containsKey(Passwords.ENCRYPTED_USERNAME)) { - String res = doCrypto(values.getAsString(Passwords.ENCRYPTED_USERNAME), uri, true); - values.put(Passwords.ENCRYPTED_USERNAME, res); - values.put(Passwords.ENC_TYPE, Passwords.ENCTYPE_SDR); - } - } - - @Override - public void onPreUpdate(ContentValues values, Uri uri, SQLiteBridge db) { - if (values.containsKey(Passwords.ENCRYPTED_PASSWORD)) { - String res = doCrypto(values.getAsString(Passwords.ENCRYPTED_PASSWORD), uri, true); - values.put(Passwords.ENCRYPTED_PASSWORD, res); - values.put(Passwords.ENC_TYPE, Passwords.ENCTYPE_SDR); - } - - if (values.containsKey(Passwords.ENCRYPTED_USERNAME)) { - String res = doCrypto(values.getAsString(Passwords.ENCRYPTED_USERNAME), uri, true); - values.put(Passwords.ENCRYPTED_USERNAME, res); - values.put(Passwords.ENC_TYPE, Passwords.ENCTYPE_SDR); - } - } - - @Override - public void onPostQuery(Cursor cursor, Uri uri, SQLiteBridge db) { - int passwordIndex = -1; - int usernameIndex = -1; - String profilePath = null; - - try { - passwordIndex = cursor.getColumnIndexOrThrow(Passwords.ENCRYPTED_PASSWORD); - } catch (Exception ex) { } - try { - usernameIndex = cursor.getColumnIndexOrThrow(Passwords.ENCRYPTED_USERNAME); - } catch (Exception ex) { } - - if (passwordIndex > -1 || usernameIndex > -1) { - MatrixBlobCursor m = (MatrixBlobCursor)cursor; - if (cursor.moveToFirst()) { - do { - if (passwordIndex > -1) { - String decrypted = doCrypto(cursor.getString(passwordIndex), uri, false);; - m.set(passwordIndex, decrypted); - } - - if (usernameIndex > -1) { - String decrypted = doCrypto(cursor.getString(usernameIndex), uri, false); - m.set(usernameIndex, decrypted); - } - } while (cursor.moveToNext()); - } - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/db/PerProfileDatabaseProvider.java b/mobile/android/base/java/org/mozilla/gecko/db/PerProfileDatabaseProvider.java deleted file mode 100644 index 7075c6e8ac..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/db/PerProfileDatabaseProvider.java +++ /dev/null @@ -1,55 +0,0 @@ -/* 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.db; - -import org.mozilla.gecko.AppConstants.Versions; -import org.mozilla.gecko.db.PerProfileDatabases.DatabaseHelperFactory; - -import android.content.Context; -import android.database.sqlite.SQLiteOpenHelper; - -/** - * Abstract class containing methods needed to make a SQLite-based content - * provider with a database helper of type T, where one database helper is - * held per profile. - */ -public abstract class PerProfileDatabaseProvider extends AbstractPerProfileDatabaseProvider { - private PerProfileDatabases databases; - - @Override - protected PerProfileDatabases getDatabases() { - return databases; - } - - protected abstract String getDatabaseName(); - - /** - * Creates and returns an instance of the appropriate DB helper. - * - * @param context to use to create the database helper - * @param databasePath path to the DB file - * @return instance of the database helper - */ - protected abstract T createDatabaseHelper(Context context, String databasePath); - - @Override - public boolean onCreate() { - synchronized (this) { - databases = new PerProfileDatabases( - getContext(), getDatabaseName(), new DatabaseHelperFactory() { - @Override - public T makeDatabaseHelper(Context context, String databasePath) { - final T helper = createDatabaseHelper(context, databasePath); - if (Versions.feature16Plus) { - helper.setWriteAheadLoggingEnabled(true); - } - return helper; - } - }); - } - - return true; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/db/PerProfileDatabases.java b/mobile/android/base/java/org/mozilla/gecko/db/PerProfileDatabases.java deleted file mode 100644 index 288d9cae7d..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/db/PerProfileDatabases.java +++ /dev/null @@ -1,94 +0,0 @@ -/* 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.db; - -import java.io.File; -import java.util.HashMap; - -import org.mozilla.gecko.GeckoProfile; - -import android.content.Context; -import android.database.sqlite.SQLiteDatabase; -import android.database.sqlite.SQLiteOpenHelper; -import android.text.TextUtils; - -/** - * Manages a set of per-profile database storage helpers. - */ -public class PerProfileDatabases { - - private final HashMap mStorages = new HashMap(); - - private final Context mContext; - private final String mDatabaseName; - private final DatabaseHelperFactory mHelperFactory; - - // Only used during tests. - public void shutdown() { - synchronized (this) { - for (T t : mStorages.values()) { - try { - t.close(); - } catch (Throwable e) { - // Never mind. - } - } - } - } - - public interface DatabaseHelperFactory { - public T makeDatabaseHelper(Context context, String databasePath); - } - - public PerProfileDatabases(final Context context, final String databaseName, final DatabaseHelperFactory helperFactory) { - mContext = context; - mDatabaseName = databaseName; - mHelperFactory = helperFactory; - } - - public String getDatabasePathForProfile(String profile) { - final File profileDir = GeckoProfile.get(mContext, profile).getDir(); - if (profileDir == null) { - return null; - } - - return new File(profileDir, mDatabaseName).getAbsolutePath(); - } - - public T getDatabaseHelperForProfile(String profile) { - return getDatabaseHelperForProfile(profile, false); - } - - public T getDatabaseHelperForProfile(String profile, boolean isTest) { - // Always fall back to default profile if none has been provided. - if (profile == null) { - profile = GeckoProfile.get(mContext).getName(); - } - - synchronized (this) { - if (mStorages.containsKey(profile)) { - return mStorages.get(profile); - } - - final String databasePath = isTest ? mDatabaseName : getDatabasePathForProfile(profile); - if (databasePath == null) { - throw new IllegalStateException("Database path is null for profile: " + profile); - } - - final T helper = mHelperFactory.makeDatabaseHelper(mContext, databasePath); - DBUtils.ensureDatabaseIsNotLocked(helper, databasePath); - - mStorages.put(profile, helper); - return helper; - } - } - - public synchronized void shrinkMemory() { - for (T t : mStorages.values()) { - final SQLiteDatabase db = t.getWritableDatabase(); - db.execSQL("PRAGMA shrink_memory"); - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/db/RemoteClient.java b/mobile/android/base/java/org/mozilla/gecko/db/RemoteClient.java deleted file mode 100644 index 07f057c117..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/db/RemoteClient.java +++ /dev/null @@ -1,69 +0,0 @@ -/* 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.db; - -import java.util.ArrayList; - -import android.os.Parcel; -import android.os.Parcelable; - -/** - * A thin representation of a remote client. - *

- * We use the hash of the client's GUID as the ID elsewhere. - */ -public class RemoteClient implements Parcelable { - public final String guid; - public final String name; - public final long lastModified; - public final String deviceType; - public final ArrayList tabs; - - public RemoteClient(String guid, String name, long lastModified, String deviceType) { - this.guid = guid; - this.name = name; - this.lastModified = lastModified; - this.deviceType = deviceType; - this.tabs = new ArrayList<>(); - } - - @Override - public int describeContents() { - return 0; - } - - @Override - public void writeToParcel(Parcel parcel, int flags) { - parcel.writeString(guid); - parcel.writeString(name); - parcel.writeLong(lastModified); - parcel.writeString(deviceType); - parcel.writeTypedList(tabs); - } - - public static final Creator CREATOR = new Creator() { - @Override - public RemoteClient createFromParcel(final Parcel source) { - final String guid = source.readString(); - final String name = source.readString(); - final long lastModified = source.readLong(); - final String deviceType = source.readString(); - - final RemoteClient client = new RemoteClient(guid, name, lastModified, deviceType); - source.readTypedList(client.tabs, RemoteTab.CREATOR); - - return client; - } - - @Override - public RemoteClient[] newArray(final int size) { - return new RemoteClient[size]; - } - }; - - public boolean isDesktop() { - return "desktop".equals(deviceType); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/db/RemoteTab.java b/mobile/android/base/java/org/mozilla/gecko/db/RemoteTab.java deleted file mode 100644 index f7660c1f7d..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/db/RemoteTab.java +++ /dev/null @@ -1,90 +0,0 @@ -/* 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.db; - -import android.os.Parcel; -import android.os.Parcelable; - -/** - * A thin representation of a remote tab. - *

- * These are generated functions. - */ -public class RemoteTab implements Parcelable { - public final String title; - public final String url; - public final long lastUsed; - - public RemoteTab(String title, String url, long lastUsed) { - this.title = title; - this.url = url; - this.lastUsed = lastUsed; - } - - @Override - public int describeContents() { - return 0; - } - - @Override - public void writeToParcel(Parcel parcel, int flags) { - parcel.writeString(title); - parcel.writeString(url); - parcel.writeLong(lastUsed); - } - - public static final Creator CREATOR = new Creator() { - @Override - public RemoteTab createFromParcel(final Parcel source) { - final String title = source.readString(); - final String url = source.readString(); - final long lastUsed = source.readLong(); - return new RemoteTab(title, url, lastUsed); - } - - @Override - public RemoteTab[] newArray(final int size) { - return new RemoteTab[size]; - } - }; - - @Override - public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + ((title == null) ? 0 : title.hashCode()); - result = prime * result + ((url == null) ? 0 : url.hashCode()); - return result; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (obj == null) { - return false; - } - if (getClass() != obj.getClass()) { - return false; - } - RemoteTab other = (RemoteTab) obj; - if (title == null) { - if (other.title != null) { - return false; - } - } else if (!title.equals(other.title)) { - return false; - } - if (url == null) { - if (other.url != null) { - return false; - } - } else if (!url.equals(other.url)) { - return false; - } - return true; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/db/SQLiteBridgeContentProvider.java b/mobile/android/base/java/org/mozilla/gecko/db/SQLiteBridgeContentProvider.java deleted file mode 100644 index d48604f036..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/db/SQLiteBridgeContentProvider.java +++ /dev/null @@ -1,471 +0,0 @@ -/* 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.db; - -import java.io.File; -import java.util.HashMap; - -import org.mozilla.gecko.AppConstants; -import org.mozilla.gecko.GeckoProfile; -import org.mozilla.gecko.GeckoThread; -import org.mozilla.gecko.Telemetry; -import org.mozilla.gecko.mozglue.GeckoLoader; -import org.mozilla.gecko.sqlite.SQLiteBridge; -import org.mozilla.gecko.sqlite.SQLiteBridgeException; - -import android.content.ContentProvider; -import android.content.ContentUris; -import android.content.ContentValues; -import android.content.Context; -import android.database.Cursor; -import android.net.Uri; -import android.text.TextUtils; -import android.util.Log; - -/* - * Provides a basic ContentProvider that sets up and sends queries through - * SQLiteBridge. Content providers should extend this by setting the appropriate - * table and version numbers in onCreate, and implementing the abstract methods: - * - * public abstract String getTable(Uri uri); - * public abstract String getSortOrder(Uri uri, String aRequested); - * public abstract void setupDefaults(Uri uri, ContentValues values); - * public abstract void initGecko(); - */ - -public abstract class SQLiteBridgeContentProvider extends ContentProvider { - private static final String ERROR_MESSAGE_DATABASE_IS_LOCKED = "Can't step statement: (5) database is locked"; - - private HashMap mDatabasePerProfile; - protected Context mContext; - private final String mLogTag; - - protected SQLiteBridgeContentProvider(String logTag) { - mLogTag = logTag; - } - - /** - * Subclasses must override this to allow error reporting code to compose - * the correct histogram name. - * - * Ensure that you define the new histograms if you define a new class! - */ - protected abstract String getTelemetryPrefix(); - - /** - * Errors are recorded in telemetry using an enumerated histogram. - * - * - * - * These are the allowable enumeration values. Keep these in sync with the - * histogram definition! - * - */ - private static enum TelemetryErrorOp { - BULKINSERT (0), - DELETE (1), - INSERT (2), - QUERY (3), - UPDATE (4); - - private final int bucket; - - TelemetryErrorOp(final int bucket) { - this.bucket = bucket; - } - - public int getBucket() { - return bucket; - } - } - - @Override - public void shutdown() { - if (mDatabasePerProfile == null) { - return; - } - - synchronized (this) { - for (SQLiteBridge bridge : mDatabasePerProfile.values()) { - if (bridge != null) { - try { - bridge.close(); - } catch (Exception ex) { } - } - } - mDatabasePerProfile = null; - } - super.shutdown(); - } - - @Override - public void finalize() { - shutdown(); - } - - /** - * Return true of the query is from Firefox Sync. - * @param uri query URI - */ - public static boolean isCallerSync(Uri uri) { - String isSync = uri.getQueryParameter(BrowserContract.PARAM_IS_SYNC); - return !TextUtils.isEmpty(isSync); - } - - private SQLiteBridge getDB(Context context, final String databasePath) { - SQLiteBridge bridge = null; - - boolean dbNeedsSetup = true; - try { - String resourcePath = context.getPackageResourcePath(); - GeckoLoader.loadSQLiteLibs(context, resourcePath); - GeckoLoader.loadNSSLibs(context, resourcePath); - bridge = SQLiteBridge.openDatabase(databasePath, null, 0); - int version = bridge.getVersion(); - dbNeedsSetup = version != getDBVersion(); - } catch (SQLiteBridgeException ex) { - // close the database - if (bridge != null) { - bridge.close(); - } - - // this will throw if the database can't be found - // we should attempt to set it up if Gecko is running - dbNeedsSetup = true; - Log.e(mLogTag, "Error getting version ", ex); - - // if Gecko is not running, we should bail out. Otherwise we try to - // let Gecko build the database for us - if (!GeckoThread.isRunning()) { - Log.e(mLogTag, "Can not set up database. Gecko is not running"); - return null; - } - } - - // If the database is not set up yet, or is the wrong schema version, we send an initialize - // call to Gecko. Gecko will handle building the database file correctly, as well as any - // migrations that are necessary - if (dbNeedsSetup) { - bridge = null; - initGecko(); - } - return bridge; - } - - /** - * Returns the absolute path of a database file depending on the specified profile and dbName. - * @param profile - * the profile whose dbPath must be returned - * @param dbName - * the name of the db file whose absolute path must be returned - * @return the absolute path of the db file or null if it was not possible to retrieve a valid path - * - */ - private String getDatabasePathForProfile(String profile, String dbName) { - // Depends on the vagaries of GeckoProfile.get, so null check for safety. - File profileDir = GeckoProfile.get(mContext, profile).getDir(); - if (profileDir == null) { - return null; - } - - String databasePath = new File(profileDir, dbName).getAbsolutePath(); - return databasePath; - } - - /** - * Returns a SQLiteBridge object according to the specified profile id and to the name of db related to the - * current provider instance. - * @param profile - * the id of the profile to be used to retrieve the related SQLiteBridge - * @return the SQLiteBridge related to the specified profile id or null if it was - * not possible to retrieve a valid SQLiteBridge - */ - private SQLiteBridge getDatabaseForProfile(String profile) { - if (profile == null) { - profile = GeckoProfile.get(mContext).getName(); - Log.d(mLogTag, "No profile provided, using '" + profile + "'"); - } - - final String dbName = getDBName(); - String mapKey = profile + "/" + dbName; - - SQLiteBridge db = null; - synchronized (this) { - db = mDatabasePerProfile.get(mapKey); - if (db != null) { - return db; - } - final String dbPath = getDatabasePathForProfile(profile, dbName); - if (dbPath == null) { - Log.e(mLogTag, "Failed to get a valid db path for profile '" + profile + "'' dbName '" + dbName + "'"); - return null; - } - db = getDB(mContext, dbPath); - if (db != null) { - mDatabasePerProfile.put(mapKey, db); - } - } - return db; - } - - /** - * Returns a SQLiteBridge object according to the specified profile path and to the name of db related to the - * current provider instance. - * @param profilePath - * the profilePath to be used to retrieve the related SQLiteBridge - * @return the SQLiteBridge related to the specified profile path or null if it was - * not possible to retrieve a valid SQLiteBridge - */ - private SQLiteBridge getDatabaseForProfilePath(String profilePath) { - File profileDir = new File(profilePath, getDBName()); - final String dbPath = profileDir.getPath(); - return getDatabaseForDBPath(dbPath); - } - - /** - * Returns a SQLiteBridge object according to the specified file path. - * @param dbPath - * the path of the file to be used to retrieve the related SQLiteBridge - * @return the SQLiteBridge related to the specified file path or null if it was - * not possible to retrieve a valid SQLiteBridge - * - */ - private SQLiteBridge getDatabaseForDBPath(String dbPath) { - SQLiteBridge db = null; - synchronized (this) { - db = mDatabasePerProfile.get(dbPath); - if (db != null) { - return db; - } - db = getDB(mContext, dbPath); - if (db != null) { - mDatabasePerProfile.put(dbPath, db); - } - } - return db; - } - - /** - * Returns a SQLiteBridge object to be used to perform operations on the given Uri. - * @param uri - * the Uri to be used to retrieve the related SQLiteBridge - * @return a SQLiteBridge object to be used on the given uri or null if it was - * not possible to retrieve a valid SQLiteBridge - * - */ - private SQLiteBridge getDatabase(Uri uri) { - String profile = null; - String profilePath = null; - - profile = uri.getQueryParameter(BrowserContract.PARAM_PROFILE); - profilePath = uri.getQueryParameter(BrowserContract.PARAM_PROFILE_PATH); - - // Testing will specify the absolute profile path - if (profilePath != null) { - return getDatabaseForProfilePath(profilePath); - } - return getDatabaseForProfile(profile); - } - - @Override - public boolean onCreate() { - mContext = getContext(); - synchronized (this) { - mDatabasePerProfile = new HashMap(); - } - return true; - } - - @Override - public String getType(Uri uri) { - return null; - } - - @Override - public int delete(Uri uri, String selection, String[] selectionArgs) { - int deleted = 0; - final SQLiteBridge db = getDatabase(uri); - if (db == null) { - return deleted; - } - - try { - deleted = db.delete(getTable(uri), selection, selectionArgs); - } catch (SQLiteBridgeException ex) { - reportError(ex, TelemetryErrorOp.DELETE); - throw ex; - } - - return deleted; - } - - @Override - public Uri insert(Uri uri, ContentValues values) { - long id = -1; - final SQLiteBridge db = getDatabase(uri); - - // If we can not get a SQLiteBridge instance, its likely that the database - // has not been set up and Gecko is not running. We return null and expect - // callers to try again later - if (db == null) { - return null; - } - - setupDefaults(uri, values); - - boolean useTransaction = !db.inTransaction(); - try { - if (useTransaction) { - db.beginTransaction(); - } - - // onPreInsert does a check for the item in the deleted table in some cases - // so we put it inside this transaction - onPreInsert(values, uri, db); - id = db.insert(getTable(uri), null, values); - - if (useTransaction) { - db.setTransactionSuccessful(); - } - } catch (SQLiteBridgeException ex) { - reportError(ex, TelemetryErrorOp.INSERT); - throw ex; - } finally { - if (useTransaction) { - db.endTransaction(); - } - } - - return ContentUris.withAppendedId(uri, id); - } - - @Override - public int bulkInsert(Uri uri, ContentValues[] allValues) { - final SQLiteBridge db = getDatabase(uri); - // If we can not get a SQLiteBridge instance, its likely that the database - // has not been set up and Gecko is not running. We return 0 and expect - // callers to try again later - if (db == null) { - return 0; - } - - int rowsAdded = 0; - - String table = getTable(uri); - - try { - db.beginTransaction(); - for (ContentValues initialValues : allValues) { - ContentValues values = new ContentValues(initialValues); - setupDefaults(uri, values); - onPreInsert(values, uri, db); - db.insert(table, null, values); - rowsAdded++; - } - db.setTransactionSuccessful(); - } catch (SQLiteBridgeException ex) { - reportError(ex, TelemetryErrorOp.BULKINSERT); - throw ex; - } finally { - db.endTransaction(); - } - - if (rowsAdded > 0) { - final boolean shouldSyncToNetwork = !isCallerSync(uri); - mContext.getContentResolver().notifyChange(uri, null, shouldSyncToNetwork); - } - - return rowsAdded; - } - - @Override - public int update(Uri uri, ContentValues values, String selection, - String[] selectionArgs) { - int updated = 0; - final SQLiteBridge db = getDatabase(uri); - - // If we can not get a SQLiteBridge instance, its likely that the database - // has not been set up and Gecko is not running. We return null and expect - // callers to try again later - if (db == null) { - return updated; - } - - onPreUpdate(values, uri, db); - - try { - updated = db.update(getTable(uri), values, selection, selectionArgs); - } catch (SQLiteBridgeException ex) { - reportError(ex, TelemetryErrorOp.UPDATE); - throw ex; - } - - return updated; - } - - @Override - public Cursor query(Uri uri, String[] projection, String selection, - String[] selectionArgs, String sortOrder) { - Cursor cursor = null; - final SQLiteBridge db = getDatabase(uri); - - // If we can not get a SQLiteBridge instance, its likely that the database - // has not been set up and Gecko is not running. We return null and expect - // callers to try again later - if (db == null) { - return cursor; - } - - sortOrder = getSortOrder(uri, sortOrder); - - try { - cursor = db.query(getTable(uri), projection, selection, selectionArgs, null, null, sortOrder, null); - onPostQuery(cursor, uri, db); - } catch (SQLiteBridgeException ex) { - reportError(ex, TelemetryErrorOp.QUERY); - throw ex; - } - - return cursor; - } - - private String getHistogram(SQLiteBridgeException e) { - // If you add values here, make sure to update - // toolkit/components/telemetry/Histograms.json. - if (ERROR_MESSAGE_DATABASE_IS_LOCKED.equals(e.getMessage())) { - return getTelemetryPrefix() + "_LOCKED"; - } - return null; - } - - protected void reportError(SQLiteBridgeException e, TelemetryErrorOp op) { - Log.e(mLogTag, "Error in database " + op.name(), e); - final String histogram = getHistogram(e); - if (histogram == null) { - return; - } - - Telemetry.addToHistogram(histogram, op.getBucket()); - } - - protected abstract String getDBName(); - - protected abstract int getDBVersion(); - - protected abstract String getTable(Uri uri); - - protected abstract String getSortOrder(Uri uri, String aRequested); - - protected abstract void setupDefaults(Uri uri, ContentValues values); - - protected abstract void initGecko(); - - protected abstract void onPreInsert(ContentValues values, Uri uri, SQLiteBridge db); - - protected abstract void onPreUpdate(ContentValues values, Uri uri, SQLiteBridge db); - - protected abstract void onPostQuery(Cursor cursor, Uri uri, SQLiteBridge db); -} diff --git a/mobile/android/base/java/org/mozilla/gecko/db/SearchHistoryProvider.java b/mobile/android/base/java/org/mozilla/gecko/db/SearchHistoryProvider.java deleted file mode 100644 index 05d31fefd6..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/db/SearchHistoryProvider.java +++ /dev/null @@ -1,127 +0,0 @@ -/* 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.db; - -import org.mozilla.gecko.db.BrowserContract; -import org.mozilla.gecko.db.BrowserContract.SearchHistory; - -import android.content.ContentUris; -import android.content.ContentValues; -import android.database.Cursor; -import android.database.SQLException; -import android.database.sqlite.SQLiteDatabase; -import android.net.Uri; -import android.text.TextUtils; -import android.util.Log; - -public class SearchHistoryProvider extends SharedBrowserDatabaseProvider { - private static final String LOG_TAG = "GeckoSearchProvider"; - private static final boolean DEBUG_ENABLED = false; - - /** - * Collapse whitespace. - */ - private String stripWhitespace(String query) { - if (TextUtils.isEmpty(query)) { - return ""; - } - - // Collapse whitespace - return query.trim().replaceAll("\\s+", " "); - } - - - @Override - public Uri insertInTransaction(Uri uri, ContentValues cv) { - final String query = stripWhitespace(cv.getAsString(SearchHistory.QUERY)); - - // We don't support inserting empty search queries. - if (TextUtils.isEmpty(query)) { - return null; - } - - final SQLiteDatabase db = getWritableDatabase(uri); - long id = -1; - - /* - * Attempt to insert the query. The catch block handles the case when - * the query already exists in the DB. - */ - try { - cv.put(SearchHistory.QUERY, query); - cv.put(SearchHistory.VISITS, 1); - cv.put(SearchHistory.DATE_LAST_VISITED, System.currentTimeMillis()); - - id = db.insertOrThrow(SearchHistory.TABLE_NAME, null, cv); - - if (id > 0) { - return ContentUris.withAppendedId(uri, id); - } - } catch (SQLException e) { - // This happens when the column already exists for this term. - if (DEBUG_ENABLED) { - Log.w(LOG_TAG, String.format("Query `%s` already in db", query)); - } - } - - /* - * Increment the VISITS counter and update the DATE_LAST_VISITED. - */ - final String sql = "UPDATE " + SearchHistory.TABLE_NAME + " SET " + - SearchHistory.VISITS + " = " + SearchHistory.VISITS + " + 1, " + - SearchHistory.DATE_LAST_VISITED + " = " + System.currentTimeMillis() + - " WHERE " + SearchHistory.QUERY + " = ?"; - - final Cursor c = db.rawQuery(sql, new String[] { query }); - - try { - if (c.getCount() > 1) { - // There is a UNIQUE constraint on the QUERY column, - // so there should only be one match. - return null; - } - if (c.moveToFirst()) { - return ContentUris.withAppendedId(uri, c.getInt(c.getColumnIndex(SearchHistory._ID))); - } - } finally { - c.close(); - } - - return null; - } - - @Override - public int deleteInTransaction(Uri uri, String selection, String[] selectionArgs) { - return getWritableDatabase(uri).delete(SearchHistory.TABLE_NAME, - selection, selectionArgs); - } - - /** - * Since we are managing counts and the full-text db, an update - * could mangle the internal state. So we disable it. - */ - @Override - public int updateInTransaction(Uri uri, ContentValues values, String selection, - String[] selectionArgs) { - throw new UnsupportedOperationException("This content provider does not support updating items"); - } - - @Override - public Cursor query(Uri uri, String[] projection, String selection, - String[] selectionArgs, String sortOrder) { - final String groupBy = null; - final String having = null; - final String limit = uri.getQueryParameter(BrowserContract.PARAM_LIMIT); - final Cursor cursor = getReadableDatabase(uri).query(SearchHistory.TABLE_NAME, projection, - selection, selectionArgs, groupBy, having, sortOrder, limit); - cursor.setNotificationUri(getContext().getContentResolver(), uri); - return cursor; - } - - @Override - public String getType(Uri uri) { - return SearchHistory.CONTENT_TYPE; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/db/Searches.java b/mobile/android/base/java/org/mozilla/gecko/db/Searches.java deleted file mode 100644 index e050a4f932..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/db/Searches.java +++ /dev/null @@ -1,12 +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.db; - -import android.content.ContentResolver; - -public interface Searches { - public void insert(ContentResolver cr, String query); -} diff --git a/mobile/android/base/java/org/mozilla/gecko/db/SharedBrowserDatabaseProvider.java b/mobile/android/base/java/org/mozilla/gecko/db/SharedBrowserDatabaseProvider.java deleted file mode 100644 index 8be18c089e..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/db/SharedBrowserDatabaseProvider.java +++ /dev/null @@ -1,128 +0,0 @@ -/* 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.db; - -import org.mozilla.gecko.AppConstants.Versions; -import org.mozilla.gecko.db.BrowserContract.CommonColumns; -import org.mozilla.gecko.db.BrowserContract.SyncColumns; -import org.mozilla.gecko.db.PerProfileDatabases.DatabaseHelperFactory; - -import android.content.Context; -import android.database.Cursor; -import android.database.sqlite.SQLiteDatabase; -import android.net.Uri; -import android.util.Log; - -/** - * A ContentProvider subclass that provides per-profile browser.db access - * that can be safely shared between multiple providers. - * - * If multiple ContentProvider classes wish to share a database, it's - * vitally important that they use the same SQLiteOpenHelpers for access. - * - * Failure to do so can cause accidental concurrent writes, with the result - * being unexpected SQLITE_BUSY errors. - * - * This class provides a static {@link PerProfileDatabases} instance, lazily - * initialized within {@link SharedBrowserDatabaseProvider#onCreate()}. - */ -public abstract class SharedBrowserDatabaseProvider extends AbstractPerProfileDatabaseProvider { - private static final String LOGTAG = SharedBrowserDatabaseProvider.class.getSimpleName(); - - private static PerProfileDatabases databases; - - @Override - protected PerProfileDatabases getDatabases() { - return databases; - } - - @Override - public void shutdown() { - synchronized (SharedBrowserDatabaseProvider.class) { - databases.shutdown(); - databases = null; - } - } - - @Override - public boolean onCreate() { - // If necessary, do the shared DB work. - synchronized (SharedBrowserDatabaseProvider.class) { - if (databases != null) { - return true; - } - - final DatabaseHelperFactory helperFactory = new DatabaseHelperFactory() { - @Override - public BrowserDatabaseHelper makeDatabaseHelper(Context context, String databasePath) { - final BrowserDatabaseHelper helper = new BrowserDatabaseHelper(context, databasePath); - if (Versions.feature16Plus) { - helper.setWriteAheadLoggingEnabled(true); - } - return helper; - } - }; - - databases = new PerProfileDatabases(getContext(), BrowserDatabaseHelper.DATABASE_NAME, helperFactory); - } - - return true; - } - - /** - * Clean up some deleted records from the specified table. - * - * If called in an existing transaction, it is the caller's responsibility - * to ensure that the transaction is already upgraded to a writer, because - * this method issues a read followed by a write, and thus is potentially - * vulnerable to an unhandled SQLITE_BUSY failure during the upgrade. - * - * If not called in an existing transaction, no new explicit transaction - * will be begun. - */ - protected void cleanUpSomeDeletedRecords(Uri fromUri, String tableName) { - Log.d(LOGTAG, "Cleaning up deleted records from " + tableName); - - // We clean up records marked as deleted that are older than a - // predefined max age. It's important not be too greedy here and - // remove only a few old deleted records at a time. - - // we cleanup records marked as deleted that are older than a - // predefined max age. It's important not be too greedy here and - // remove only a few old deleted records at a time. - - // Maximum age of deleted records to be cleaned up (20 days in ms) - final long MAX_AGE_OF_DELETED_RECORDS = 86400000 * 20; - - // Number of records marked as deleted to be removed - final long DELETED_RECORDS_PURGE_LIMIT = 5; - - // Android SQLite doesn't have LIMIT on DELETE. Instead, query for the - // IDs of matching rows, then delete them in one go. - final long now = System.currentTimeMillis(); - final String selection = getDeletedItemSelection(now - MAX_AGE_OF_DELETED_RECORDS); - - final String profile = fromUri.getQueryParameter(BrowserContract.PARAM_PROFILE); - final SQLiteDatabase db = getWritableDatabaseForProfile(profile, isTest(fromUri)); - final String limit = Long.toString(DELETED_RECORDS_PURGE_LIMIT, 10); - final Cursor cursor = db.query(tableName, new String[] { CommonColumns._ID }, selection, null, null, null, null, limit); - final String inClause; - try { - inClause = DBUtils.computeSQLInClauseFromLongs(cursor, CommonColumns._ID); - } finally { - cursor.close(); - } - - db.delete(tableName, inClause, null); - } - - // Override this, or override cleanUpSomeDeletedRecords. - protected String getDeletedItemSelection(long earlierThan) { - if (earlierThan == -1L) { - return SyncColumns.IS_DELETED + " = 1"; - } - return SyncColumns.IS_DELETED + " = 1 AND " + SyncColumns.DATE_MODIFIED + " <= " + earlierThan; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/db/SuggestedSites.java b/mobile/android/base/java/org/mozilla/gecko/db/SuggestedSites.java deleted file mode 100644 index 89b12904bb..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/db/SuggestedSites.java +++ /dev/null @@ -1,629 +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.db; - -import android.content.Context; -import android.content.ContentResolver; -import android.content.SharedPreferences; -import android.content.SharedPreferences.Editor; -import android.database.Cursor; -import android.database.MatrixCursor; -import android.database.MatrixCursor.RowBuilder; -import android.net.Uri; -import android.text.TextUtils; -import android.util.Log; - -import java.io.File; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.OutputStreamWriter; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashSet; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Scanner; -import java.util.Set; - -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; -import org.mozilla.gecko.annotation.RobocopTarget; -import org.mozilla.gecko.GeckoSharedPrefs; -import org.mozilla.gecko.GeckoProfile; -import org.mozilla.gecko.Locales; -import org.mozilla.gecko.R; -import org.mozilla.gecko.distribution.Distribution; -import org.mozilla.gecko.restrictions.Restrictions; -import org.mozilla.gecko.util.RawResource; -import org.mozilla.gecko.util.ThreadUtils; -import org.mozilla.gecko.preferences.GeckoPreferences; - -/** - * {@code SuggestedSites} provides API to get a list of locale-specific - * suggested sites to be used in Fennec's top sites panel. It provides - * only a single method to fetch the list as a {@code Cursor}. This cursor - * will then be wrapped by {@code TopSitesCursorWrapper} to blend top, - * pinned, and suggested sites in the UI. The returned {@code Cursor} - * uses its own schema defined in {@code BrowserContract.SuggestedSites} - * for clarity. - * - * Under the hood, {@code SuggestedSites} keeps reference to the - * parsed list of sites to avoid reparsing the JSON file on every - * {@code get()} call. - * - * The default list of suggested sites is stored in a raw Android - * resource ({@code R.raw.suggestedsites}) which is dynamically - * generated at build time for each target locale. - * - * Changes to the list of suggested sites are saved in SharedPreferences. - */ -@RobocopTarget -public class SuggestedSites { - private static final String LOGTAG = "GeckoSuggestedSites"; - - // SharedPreference key for suggested sites that should be hidden. - public static final String PREF_SUGGESTED_SITES_HIDDEN = GeckoPreferences.NON_PREF_PREFIX + "suggestedSites.hidden"; - public static final String PREF_SUGGESTED_SITES_HIDDEN_OLD = "suggestedSites.hidden"; - - // Locale used to generate the current suggested sites. - public static final String PREF_SUGGESTED_SITES_LOCALE = GeckoPreferences.NON_PREF_PREFIX + "suggestedSites.locale"; - public static final String PREF_SUGGESTED_SITES_LOCALE_OLD = "suggestedSites.locale"; - - // File in profile dir with the list of suggested sites. - private static final String FILENAME = "suggestedsites.json"; - - private static final String[] COLUMNS = new String[] { - BrowserContract.SuggestedSites._ID, - BrowserContract.SuggestedSites.URL, - BrowserContract.SuggestedSites.TITLE, - BrowserContract.Combined.HISTORY_ID - }; - - private static final String JSON_KEY_URL = "url"; - private static final String JSON_KEY_TITLE = "title"; - private static final String JSON_KEY_IMAGE_URL = "imageurl"; - private static final String JSON_KEY_BG_COLOR = "bgcolor"; - private static final String JSON_KEY_RESTRICTED = "restricted"; - - private static class Site { - public final String url; - public final String title; - public final String imageUrl; - public final String bgColor; - public final boolean restricted; - - public Site(JSONObject json) throws JSONException { - this.restricted = !json.isNull(JSON_KEY_RESTRICTED); - this.url = json.getString(JSON_KEY_URL); - this.title = json.getString(JSON_KEY_TITLE); - this.imageUrl = json.getString(JSON_KEY_IMAGE_URL); - this.bgColor = json.getString(JSON_KEY_BG_COLOR); - - validate(); - } - - public Site(String url, String title, String imageUrl, String bgColor) { - this.url = url; - this.title = title; - this.imageUrl = imageUrl; - this.bgColor = bgColor; - this.restricted = false; - - validate(); - } - - private void validate() { - // Site instances must have non-empty values for all properties except IDs. - if (TextUtils.isEmpty(url) || - TextUtils.isEmpty(title) || - TextUtils.isEmpty(imageUrl) || - TextUtils.isEmpty(bgColor)) { - throw new IllegalStateException("Suggested sites must have a URL, title, " + - "image URL, and background color."); - } - } - - @Override - public String toString() { - return "{ url = " + url + "\n" + - "restricted = " + restricted + "\n" + - "title = " + title + "\n" + - "imageUrl = " + imageUrl + "\n" + - "bgColor = " + bgColor + " }"; - } - - public JSONObject toJSON() throws JSONException { - final JSONObject json = new JSONObject(); - - if (restricted) { - json.put(JSON_KEY_RESTRICTED, true); - } - - json.put(JSON_KEY_URL, url); - json.put(JSON_KEY_TITLE, title); - json.put(JSON_KEY_IMAGE_URL, imageUrl); - json.put(JSON_KEY_BG_COLOR, bgColor); - - return json; - } - } - - final Context context; - final Distribution distribution; - private File cachedFile; - private Map cachedSites; - private Set cachedBlacklist; - - public SuggestedSites(Context appContext) { - this(appContext, null); - } - - public SuggestedSites(Context appContext, Distribution distribution) { - this(appContext, distribution, null); - } - - public SuggestedSites(Context appContext, Distribution distribution, File file) { - this.context = appContext; - this.distribution = distribution; - this.cachedFile = file; - } - - synchronized File getFile() { - if (cachedFile == null) { - cachedFile = GeckoProfile.get(context).getFile(FILENAME); - } - return cachedFile; - } - - private static boolean isNewLocale(Context context, Locale requestedLocale) { - final SharedPreferences prefs = GeckoSharedPrefs.forProfile(context); - - String locale = prefs.getString(PREF_SUGGESTED_SITES_LOCALE_OLD, null); - if (locale != null) { - // Migrate the old pref and remove it - final Editor editor = prefs.edit(); - editor.remove(PREF_SUGGESTED_SITES_LOCALE_OLD); - editor.putString(PREF_SUGGESTED_SITES_LOCALE, locale); - editor.apply(); - } else { - locale = prefs.getString(PREF_SUGGESTED_SITES_LOCALE, null); - } - if (locale == null) { - // Initialize config with the current locale - updateSuggestedSitesLocale(context); - return true; - } - - return !TextUtils.equals(requestedLocale.toString(), locale); - } - - /** - * Return the current locale and its fallback (en_US) in order. - */ - private static List getAcceptableLocales() { - final List locales = new ArrayList(); - - final Locale defaultLocale = Locale.getDefault(); - locales.add(defaultLocale); - - if (!defaultLocale.equals(Locale.US)) { - locales.add(Locale.US); - } - - return locales; - } - - private static Map loadSites(File f) throws IOException { - Scanner scanner = null; - - try { - scanner = new Scanner(f, "UTF-8"); - return loadSites(scanner.useDelimiter("\\A").next()); - } finally { - if (scanner != null) { - scanner.close(); - } - } - } - - private static Map loadSites(String jsonString) { - if (TextUtils.isEmpty(jsonString)) { - return null; - } - - Map sites = null; - - try { - final JSONArray jsonSites = new JSONArray(jsonString); - sites = new LinkedHashMap(jsonSites.length()); - - final int count = jsonSites.length(); - for (int i = 0; i < count; i++) { - final Site site = new Site(jsonSites.getJSONObject(i)); - sites.put(site.url, site); - } - } catch (Exception e) { - Log.e(LOGTAG, "Failed to refresh suggested sites", e); - return null; - } - - return sites; - } - - /** - * Saves suggested sites file to disk. Access to this method should - * be synchronized on 'file'. - */ - static void saveSites(File f, Map sites) { - ThreadUtils.assertNotOnUiThread(); - - if (sites == null || sites.isEmpty()) { - return; - } - - OutputStreamWriter osw = null; - - try { - final JSONArray jsonSites = new JSONArray(); - for (Site site : sites.values()) { - jsonSites.put(site.toJSON()); - } - - osw = new OutputStreamWriter(new FileOutputStream(f), "UTF-8"); - - final String jsonString = jsonSites.toString(); - osw.write(jsonString, 0, jsonString.length()); - } catch (Exception e) { - Log.e(LOGTAG, "Failed to save suggested sites", e); - } finally { - if (osw != null) { - try { - osw.close(); - } catch (IOException e) { - // Ignore. - } - } - } - } - - private void maybeWaitForDistribution() { - if (distribution == null) { - return; - } - - distribution.addOnDistributionReadyCallback(new Distribution.ReadyCallback() { - @Override - public void distributionNotFound() { - // If distribution doesn't exist, simply continue to load - // suggested sites directly from resources. See refresh(). - } - - @Override - public void distributionFound(Distribution distribution) { - Log.d(LOGTAG, "Running post-distribution task: suggested sites."); - // Merge suggested sites from distribution with the - // default ones. Distribution takes precedence. - Map sites = loadFromDistribution(distribution); - if (sites == null) { - sites = new LinkedHashMap(); - } - sites.putAll(loadFromResource()); - - // Update cached list of sites. - setCachedSites(sites); - - // Save the result to disk. - final File file = getFile(); - synchronized (file) { - saveSites(file, sites); - } - - // Then notify any active loaders about the changes. - final ContentResolver cr = context.getContentResolver(); - cr.notifyChange(BrowserContract.SuggestedSites.CONTENT_URI, null); - } - - @Override - public void distributionArrivedLate(Distribution distribution) { - distributionFound(distribution); - } - }); - } - - /** - * Loads suggested sites from a distribution file either matching the - * current locale or with the fallback locale (en-US). - * - * It's assumed that the given distribution instance is ready to be - * used and exists. - */ - static Map loadFromDistribution(Distribution dist) { - for (Locale locale : getAcceptableLocales()) { - try { - final String languageTag = Locales.getLanguageTag(locale); - final String path = String.format("suggestedsites/locales/%s/%s", - languageTag, FILENAME); - - final File f = dist.getDistributionFile(path); - if (f == null) { - Log.d(LOGTAG, "No suggested sites for locale: " + languageTag); - continue; - } - - return loadSites(f); - } catch (Exception e) { - Log.e(LOGTAG, "Failed to open suggested sites for locale " + - locale + " in distribution.", e); - } - } - - return null; - } - - private Map loadFromProfile() { - try { - final File file = getFile(); - synchronized (file) { - return loadSites(file); - } - } catch (FileNotFoundException e) { - maybeWaitForDistribution(); - } catch (IOException e) { - // Fall through, return null. - } - - return null; - } - - Map loadFromResource() { - try { - return loadSites(RawResource.getAsString(context, R.raw.suggestedsites)); - } catch (IOException e) { - return null; - } - } - - private synchronized void setCachedSites(Map sites) { - cachedSites = Collections.unmodifiableMap(sites); - updateSuggestedSitesLocale(context); - } - - /** - * Refreshes the cached list of sites either from the default raw - * source or standard file location. This will be called on every - * cache miss during a {@code get()} call. - */ - private void refresh() { - Log.d(LOGTAG, "Refreshing suggested sites from file"); - - Map sites = loadFromProfile(); - if (sites == null) { - sites = loadFromResource(); - } - - // Update cached list of sites. - if (sites != null) { - setCachedSites(sites); - } - } - - private static void updateSuggestedSitesLocale(Context context) { - final Editor editor = GeckoSharedPrefs.forProfile(context).edit(); - editor.putString(PREF_SUGGESTED_SITES_LOCALE, Locale.getDefault().toString()); - editor.apply(); - } - - private synchronized Site getSiteForUrl(String url) { - if (cachedSites == null) { - return null; - } - - return cachedSites.get(url); - } - - /** - * Returns a {@code Cursor} with the list of suggested websites. - * - * @param limit maximum number of suggested sites. - */ - public Cursor get(int limit) { - return get(limit, Locale.getDefault()); - } - - /** - * Returns a {@code Cursor} with the list of suggested websites. - * - * @param limit maximum number of suggested sites. - * @param locale the target locale. - */ - public Cursor get(int limit, Locale locale) { - return get(limit, locale, null); - } - - /** - * Returns a {@code Cursor} with the list of suggested websites. - * - * @param limit maximum number of suggested sites. - * @param excludeUrls list of URLs to be excluded from the list. - */ - public Cursor get(int limit, List excludeUrls) { - return get(limit, Locale.getDefault(), excludeUrls); - } - - /** - * Returns a {@code Cursor} with the list of suggested websites. - * - * @param limit maximum number of suggested sites. - * @param locale the target locale. - * @param excludeUrls list of URLs to be excluded from the list. - */ - public synchronized Cursor get(int limit, Locale locale, List excludeUrls) { - final MatrixCursor cursor = new MatrixCursor(COLUMNS); - final boolean isNewLocale = isNewLocale(context, locale); - - // Force the suggested sites file in profile dir to be re-generated - // if the locale has changed. - if (isNewLocale) { - getFile().delete(); - } - - if (cachedSites == null || isNewLocale) { - Log.d(LOGTAG, "No cached sites, refreshing."); - refresh(); - } - - // Return empty cursor if there was an error when - // loading the suggested sites or the list is empty. - if (cachedSites == null || cachedSites.isEmpty()) { - return cursor; - } - - excludeUrls = includeBlacklist(excludeUrls); - - final int sitesCount = cachedSites.size(); - Log.d(LOGTAG, "Number of suggested sites: " + sitesCount); - - final int maxCount = Math.min(limit, sitesCount); - // History IDS: real history is positive, -1 is no history id in the combined table - // hence we can start at -2 for suggested sites - int id = -1; - for (Site site : cachedSites.values()) { - // Decrement ID here: this ensure we have a consistent ID to URL mapping, even if items - // are removed. If we instead decremented at the point of insertion we'd end up with - // ID conflicts when a suggested site is removed. (note that cachedSites does not change - // while we're already showing topsites) - --id; - if (cursor.getCount() == maxCount) { - break; - } - - if (excludeUrls != null && excludeUrls.contains(site.url)) { - continue; - } - - final boolean restrictedProfile = Restrictions.isRestrictedProfile(context); - - if (restrictedProfile == site.restricted) { - final RowBuilder row = cursor.newRow(); - row.add(id); - row.add(site.url); - row.add(site.title); - row.add(id); - } - } - - cursor.setNotificationUri(context.getContentResolver(), - BrowserContract.SuggestedSites.CONTENT_URI); - - return cursor; - } - - public boolean contains(String url) { - return (getSiteForUrl(url) != null); - } - - public String getImageUrlForUrl(String url) { - final Site site = getSiteForUrl(url); - return (site != null ? site.imageUrl : null); - } - - public String getBackgroundColorForUrl(String url) { - final Site site = getSiteForUrl(url); - return (site != null ? site.bgColor : null); - } - - private Set loadBlacklist() { - Log.d(LOGTAG, "Loading blacklisted suggested sites from SharedPreferences."); - final Set blacklist = new HashSet(); - - final SharedPreferences prefs = GeckoSharedPrefs.forProfile(context); - String sitesString = prefs.getString(PREF_SUGGESTED_SITES_HIDDEN_OLD, null); - if (sitesString != null) { - // Migrate the old pref and remove it - final Editor editor = prefs.edit(); - editor.remove(PREF_SUGGESTED_SITES_HIDDEN_OLD); - editor.putString(PREF_SUGGESTED_SITES_HIDDEN, sitesString); - editor.apply(); - } else { - sitesString = prefs.getString(PREF_SUGGESTED_SITES_HIDDEN, null); - } - - if (sitesString != null) { - for (String site : sitesString.trim().split(" ")) { - blacklist.add(Uri.decode(site)); - } - } - - return blacklist; - } - - private List includeBlacklist(List originalList) { - if (cachedBlacklist == null) { - cachedBlacklist = loadBlacklist(); - } - - if (cachedBlacklist.isEmpty()) { - return originalList; - } - - if (originalList == null) { - originalList = new ArrayList(); - } - - originalList.addAll(cachedBlacklist); - return originalList; - } - - /** - * Blacklist a suggested site so it will no longer be returned as a suggested site. - * This method should only be called from a background thread because it may write - * to SharedPreferences. - * - * Urls that are not Suggested Sites are ignored. - * - * @param url String url of site to blacklist - * @return true is blacklisted, false otherwise - */ - public synchronized boolean hideSite(String url) { - ThreadUtils.assertNotOnUiThread(); - - if (cachedSites == null) { - refresh(); - if (cachedSites == null) { - Log.w(LOGTAG, "Could not load suggested sites!"); - return false; - } - } - - if (cachedSites.containsKey(url)) { - if (cachedBlacklist == null) { - cachedBlacklist = loadBlacklist(); - } - - // Check if site has already been blacklisted, just in case. - if (!cachedBlacklist.contains(url)) { - - saveToBlacklist(url); - cachedBlacklist.add(url); - - return true; - } - } - - return false; - } - - private void saveToBlacklist(String url) { - final SharedPreferences prefs = GeckoSharedPrefs.forProfile(context); - final String prefString = prefs.getString(PREF_SUGGESTED_SITES_HIDDEN, ""); - final String siteString = prefString.concat(" " + Uri.encode(url)); - prefs.edit().putString(PREF_SUGGESTED_SITES_HIDDEN, siteString).apply(); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/db/Table.java b/mobile/android/base/java/org/mozilla/gecko/db/Table.java deleted file mode 100644 index 37a605ee1b..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/db/Table.java +++ /dev/null @@ -1,47 +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.db; - -import android.content.ContentValues; -import android.database.Cursor; -import android.database.sqlite.SQLiteDatabase; -import android.net.Uri; - -// Tables provide a basic wrapper around ContentProvider methods to make it simpler to add new tables into storage. -// If you create a new Table type, make sure to add it to the sTables list in BrowserProvider to ensure it is queried. -interface Table { - // Provides information to BrowserProvider about the type of URIs this Table can handle. - public static class ContentProviderInfo { - public final int id; // A number of ID for this table. Used by the UriMatcher in BrowserProvider - public final String name; // A name for this table. Will be appended onto uris querying this table - // This is also used to define the mimetype of data returned from this db, i.e. - // BrowserProvider will return "vnd.android.cursor.item/" + name - - public ContentProviderInfo(int id, String name) { - if (name == null) { - throw new IllegalArgumentException("Content provider info must specify a name"); - } - this.id = id; - this.name = name; - } - } - - // Return a list of Info about the ContentProvider URIs this will match - ContentProviderInfo[] getContentProviderInfo(); - - // Called by BrowserDBHelper whenever the database is created or upgraded. - // Order in which tables are created/upgraded isn't guaranteed (yet), so be careful if your Table depends on something in a - // separate table. - void onCreate(SQLiteDatabase db); - void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion); - - // Called by BrowserProvider when this database queried/modified - // The dbId here should match the dbId's you returned in your getContentProviderInfo() call - Cursor query(SQLiteDatabase db, Uri uri, int dbId, String[] projection, String selection, String[] selectionArgs, String sortOrder, String groupBy, String limit); - int update(SQLiteDatabase db, Uri uri, int dbId, ContentValues values, String selection, String[] selectionArgs); - long insert(SQLiteDatabase db, Uri uri, int dbId, ContentValues values); - int delete(SQLiteDatabase db, Uri uri, int dbId, String selection, String[] selectionArgs); -}; diff --git a/mobile/android/base/java/org/mozilla/gecko/db/TabsAccessor.java b/mobile/android/base/java/org/mozilla/gecko/db/TabsAccessor.java deleted file mode 100644 index 1be004ca7f..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/db/TabsAccessor.java +++ /dev/null @@ -1,28 +0,0 @@ -/* 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.db; - -import android.content.ContentResolver; -import android.content.Context; -import android.database.Cursor; - -import org.mozilla.gecko.Tab; - -import java.util.List; - -public interface TabsAccessor { - public interface OnQueryTabsCompleteListener { - public void onQueryTabsComplete(List clients); - } - - public Cursor getRemoteClientsByRecencyCursor(Context context); - public Cursor getRemoteTabsCursor(Context context); - public Cursor getRemoteTabsCursor(Context context, int limit); - public List getClientsWithoutTabsByRecencyFromCursor(final Cursor cursor); - public List getClientsFromCursor(final Cursor cursor); - public void getTabs(final Context context, final OnQueryTabsCompleteListener listener); - public void getTabs(final Context context, final int limit, final OnQueryTabsCompleteListener listener); - public void persistLocalTabs(final ContentResolver cr, final Iterable tabs); -} diff --git a/mobile/android/base/java/org/mozilla/gecko/db/TabsProvider.java b/mobile/android/base/java/org/mozilla/gecko/db/TabsProvider.java deleted file mode 100644 index 09e4d9cf5e..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/db/TabsProvider.java +++ /dev/null @@ -1,361 +0,0 @@ -/* 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.db; - -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; - -import org.mozilla.gecko.db.BrowserContract.Clients; -import org.mozilla.gecko.db.BrowserContract.Tabs; - -import android.content.ContentUris; -import android.content.ContentValues; -import android.content.UriMatcher; -import android.database.Cursor; -import android.database.sqlite.SQLiteDatabase; -import android.database.sqlite.SQLiteQueryBuilder; -import android.net.Uri; -import android.text.TextUtils; - -public class TabsProvider extends SharedBrowserDatabaseProvider { - private static final long ONE_DAY_IN_MILLISECONDS = 1000 * 60 * 60 * 24; - private static final long ONE_WEEK_IN_MILLISECONDS = 7 * ONE_DAY_IN_MILLISECONDS; - private static final long THREE_WEEKS_IN_MILLISECONDS = 3 * ONE_WEEK_IN_MILLISECONDS; - - static final String TABLE_TABS = "tabs"; - static final String TABLE_CLIENTS = "clients"; - - static final int TABS = 600; - static final int TABS_ID = 601; - static final int CLIENTS = 602; - static final int CLIENTS_ID = 603; - static final int CLIENTS_RECENCY = 604; - - // Exclude clients that are more than three weeks old and also any duplicates that are older than one week old. - static final String EXCLUDE_STALE_CLIENTS_SUBQUERY = - "(SELECT " + Clients.GUID + - ", " + Clients.NAME + - ", " + Clients.LAST_MODIFIED + - ", " + Clients.DEVICE_TYPE + - " FROM " + TABLE_CLIENTS + - " WHERE " + Clients.LAST_MODIFIED + " > %1$s " + - " GROUP BY " + Clients.NAME + - " UNION ALL " + - " SELECT c." + Clients.GUID + " AS " + Clients.GUID + - ", c." + Clients.NAME + " AS " + Clients.NAME + - ", c." + Clients.LAST_MODIFIED + " AS " + Clients.LAST_MODIFIED + - ", c." + Clients.DEVICE_TYPE + " AS " + Clients.DEVICE_TYPE + - " FROM " + TABLE_CLIENTS + " AS c " + - " JOIN (" + - " SELECT " + Clients.GUID + - ", " + "MAX( " + Clients.LAST_MODIFIED + ") AS " + Clients.LAST_MODIFIED + - " FROM " + TABLE_CLIENTS + - " WHERE (" + Clients.LAST_MODIFIED + " < %1$s" + " AND " + Clients.LAST_MODIFIED + " > %2$s) AND " + - Clients.NAME + " NOT IN " + "( SELECT " + Clients.NAME + " FROM " + TABLE_CLIENTS + " WHERE " + Clients.LAST_MODIFIED + " > %1$s)" + - " GROUP BY " + Clients.NAME + - ") AS c2" + - " ON c." + Clients.GUID + " = c2." + Clients.GUID + ")"; - - static final String DEFAULT_TABS_SORT_ORDER = Clients.LAST_MODIFIED + " DESC, " + Tabs.LAST_USED + " DESC"; - static final String DEFAULT_CLIENTS_SORT_ORDER = Clients.LAST_MODIFIED + " DESC"; - static final String DEFAULT_CLIENTS_RECENCY_SORT_ORDER = "COALESCE(MAX(" + Tabs.LAST_USED + "), " + Clients.LAST_MODIFIED + ") DESC"; - - static final String INDEX_TABS_GUID = "tabs_guid_index"; - static final String INDEX_TABS_POSITION = "tabs_position_index"; - - static final UriMatcher URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH); - - static final Map TABS_PROJECTION_MAP; - static final Map CLIENTS_PROJECTION_MAP; - static final Map CLIENTS_RECENCY_PROJECTION_MAP; - - static { - URI_MATCHER.addURI(BrowserContract.TABS_AUTHORITY, "tabs", TABS); - URI_MATCHER.addURI(BrowserContract.TABS_AUTHORITY, "tabs/#", TABS_ID); - URI_MATCHER.addURI(BrowserContract.TABS_AUTHORITY, "clients", CLIENTS); - URI_MATCHER.addURI(BrowserContract.TABS_AUTHORITY, "clients/#", CLIENTS_ID); - URI_MATCHER.addURI(BrowserContract.TABS_AUTHORITY, "clients_recency", CLIENTS_RECENCY); - - HashMap map; - - map = new HashMap(); - map.put(Tabs._ID, Tabs._ID); - map.put(Tabs.TITLE, Tabs.TITLE); - map.put(Tabs.URL, Tabs.URL); - map.put(Tabs.HISTORY, Tabs.HISTORY); - map.put(Tabs.FAVICON, Tabs.FAVICON); - map.put(Tabs.LAST_USED, Tabs.LAST_USED); - map.put(Tabs.POSITION, Tabs.POSITION); - map.put(Clients.GUID, Clients.GUID); - map.put(Clients.NAME, Clients.NAME); - map.put(Clients.LAST_MODIFIED, Clients.LAST_MODIFIED); - map.put(Clients.DEVICE_TYPE, Clients.DEVICE_TYPE); - TABS_PROJECTION_MAP = Collections.unmodifiableMap(map); - - map = new HashMap(); - map.put(Clients.GUID, Clients.GUID); - map.put(Clients.NAME, Clients.NAME); - map.put(Clients.LAST_MODIFIED, Clients.LAST_MODIFIED); - map.put(Clients.DEVICE_TYPE, Clients.DEVICE_TYPE); - CLIENTS_PROJECTION_MAP = Collections.unmodifiableMap(map); - - map = new HashMap<>(); - map.put(Clients.GUID, projectColumn(TABLE_CLIENTS, Clients.GUID) + " AS guid"); - map.put(Clients.NAME, projectColumn(TABLE_CLIENTS, Clients.NAME) + " AS name"); - map.put(Clients.LAST_MODIFIED, projectColumn(TABLE_CLIENTS, Clients.LAST_MODIFIED) + " AS last_modified"); - map.put(Clients.DEVICE_TYPE, projectColumn(TABLE_CLIENTS, Clients.DEVICE_TYPE) + " AS device_type"); - // last_used is the max of the tab last_used times, or if there are no tabs, - // the client's last_modified time. - map.put(Tabs.LAST_USED, "COALESCE(MAX(" + projectColumn(TABLE_TABS, Tabs.LAST_USED) + "), " + projectColumn(TABLE_CLIENTS, Clients.LAST_MODIFIED) + ") AS last_used"); - CLIENTS_RECENCY_PROJECTION_MAP = Collections.unmodifiableMap(map); - } - - private static final String projectColumn(String table, String column) { - return table + "." + column; - } - - private static final String selectColumn(String table, String column) { - return projectColumn(table, column) + " = ?"; - } - - @Override - public String getType(Uri uri) { - final int match = URI_MATCHER.match(uri); - - trace("Getting URI type: " + uri); - - switch (match) { - case TABS: - trace("URI is TABS: " + uri); - return Tabs.CONTENT_TYPE; - - case TABS_ID: - trace("URI is TABS_ID: " + uri); - return Tabs.CONTENT_ITEM_TYPE; - - case CLIENTS: - trace("URI is CLIENTS: " + uri); - return Clients.CONTENT_TYPE; - - case CLIENTS_ID: - trace("URI is CLIENTS_ID: " + uri); - return Clients.CONTENT_ITEM_TYPE; - } - - debug("URI has unrecognized type: " + uri); - - return null; - } - - @Override - @SuppressWarnings("fallthrough") - public int deleteInTransaction(Uri uri, String selection, String[] selectionArgs) { - trace("Calling delete in transaction on URI: " + uri); - - final int match = URI_MATCHER.match(uri); - int deleted = 0; - - switch (match) { - case CLIENTS_ID: - trace("Delete on CLIENTS_ID: " + uri); - selection = DBUtils.concatenateWhere(selection, selectColumn(TABLE_CLIENTS, Clients._ID)); - selectionArgs = DBUtils.appendSelectionArgs(selectionArgs, - new String[] { Long.toString(ContentUris.parseId(uri)) }); - // fall through - case CLIENTS: - trace("Delete on CLIENTS: " + uri); - deleted = deleteValues(uri, selection, selectionArgs, TABLE_CLIENTS); - break; - - case TABS_ID: - trace("Delete on TABS_ID: " + uri); - selection = DBUtils.concatenateWhere(selection, selectColumn(TABLE_TABS, Tabs._ID)); - selectionArgs = DBUtils.appendSelectionArgs(selectionArgs, - new String[] { Long.toString(ContentUris.parseId(uri)) }); - // fall through - case TABS: - trace("Deleting on TABS: " + uri); - deleted = deleteValues(uri, selection, selectionArgs, TABLE_TABS); - break; - - default: - throw new UnsupportedOperationException("Unknown delete URI " + uri); - } - - debug("Deleted " + deleted + " rows for URI: " + uri); - - return deleted; - } - - @Override - public Uri insertInTransaction(Uri uri, ContentValues values) { - trace("Calling insert in transaction on URI: " + uri); - - final SQLiteDatabase db = getWritableDatabase(uri); - int match = URI_MATCHER.match(uri); - long id = -1; - - switch (match) { - case CLIENTS: - String guid = values.getAsString(Clients.GUID); - debug("Inserting client in database with GUID: " + guid); - id = db.insertOrThrow(TABLE_CLIENTS, Clients.GUID, values); - break; - - case TABS: - String url = values.getAsString(Tabs.URL); - debug("Inserting tab in database with URL: " + url); - id = db.insertOrThrow(TABLE_TABS, Tabs.TITLE, values); - break; - - default: - throw new UnsupportedOperationException("Unknown insert URI " + uri); - } - - debug("Inserted ID in database: " + id); - - if (id >= 0) - return ContentUris.withAppendedId(uri, id); - - return null; - } - - @Override - public int updateInTransaction(Uri uri, ContentValues values, String selection, String[] selectionArgs) { - trace("Calling update in transaction on URI: " + uri); - - int match = URI_MATCHER.match(uri); - int updated = 0; - - switch (match) { - case CLIENTS_ID: - trace("Update on CLIENTS_ID: " + uri); - selection = DBUtils.concatenateWhere(selection, selectColumn(TABLE_CLIENTS, Clients._ID)); - selectionArgs = DBUtils.appendSelectionArgs(selectionArgs, - new String[] { Long.toString(ContentUris.parseId(uri)) }); - // fall through - case CLIENTS: - trace("Update on CLIENTS: " + uri); - updated = updateValues(uri, values, selection, selectionArgs, TABLE_CLIENTS); - break; - - case TABS_ID: - trace("Update on TABS_ID: " + uri); - selection = DBUtils.concatenateWhere(selection, selectColumn(TABLE_TABS, Tabs._ID)); - selectionArgs = DBUtils.appendSelectionArgs(selectionArgs, - new String[] { Long.toString(ContentUris.parseId(uri)) }); - // fall through - case TABS: - trace("Update on TABS: " + uri); - updated = updateValues(uri, values, selection, selectionArgs, TABLE_TABS); - break; - - default: - throw new UnsupportedOperationException("Unknown update URI " + uri); - } - - debug("Updated " + updated + " rows for URI: " + uri); - - return updated; - } - - @Override - @SuppressWarnings("fallthrough") - public Cursor query(Uri uri, String[] projection, String selection, - String[] selectionArgs, String sortOrder) { - SQLiteDatabase db = getReadableDatabase(uri); - final int match = URI_MATCHER.match(uri); - - String groupBy = null; - SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); - String limit = uri.getQueryParameter(BrowserContract.PARAM_LIMIT); - - switch (match) { - case TABS_ID: - trace("Query is on TABS_ID: " + uri); - selection = DBUtils.concatenateWhere(selection, selectColumn(TABLE_TABS, Tabs._ID)); - selectionArgs = DBUtils.appendSelectionArgs(selectionArgs, - new String[] { Long.toString(ContentUris.parseId(uri)) }); - // fall through - case TABS: - trace("Query is on TABS: " + uri); - if (TextUtils.isEmpty(sortOrder)) { - sortOrder = DEFAULT_TABS_SORT_ORDER; - } else { - debug("Using sort order " + sortOrder + "."); - } - - qb.setProjectionMap(TABS_PROJECTION_MAP); - qb.setTables(TABLE_TABS + " LEFT OUTER JOIN " + TABLE_CLIENTS + " ON (" + TABLE_TABS + "." + Tabs.CLIENT_GUID + " = " + TABLE_CLIENTS + "." + Clients.GUID + ")"); - break; - - case CLIENTS_ID: - trace("Query is on CLIENTS_ID: " + uri); - selection = DBUtils.concatenateWhere(selection, selectColumn(TABLE_CLIENTS, Clients._ID)); - selectionArgs = DBUtils.appendSelectionArgs(selectionArgs, - new String[] { Long.toString(ContentUris.parseId(uri)) }); - // fall through - case CLIENTS: - trace("Query is on CLIENTS: " + uri); - if (TextUtils.isEmpty(sortOrder)) { - sortOrder = DEFAULT_CLIENTS_SORT_ORDER; - } else { - debug("Using sort order " + sortOrder + "."); - } - - qb.setProjectionMap(CLIENTS_PROJECTION_MAP); - qb.setTables(TABLE_CLIENTS); - break; - - case CLIENTS_RECENCY: - trace("Query is on CLIENTS_RECENCY: " + uri); - if (TextUtils.isEmpty(sortOrder)) { - sortOrder = DEFAULT_CLIENTS_RECENCY_SORT_ORDER; - } else { - debug("Using sort order " + sortOrder + "."); - } - - final long oneWeekAgo = System.currentTimeMillis() - ONE_WEEK_IN_MILLISECONDS; - final long threeWeeksAgo = System.currentTimeMillis() - THREE_WEEKS_IN_MILLISECONDS; - - final String excludeStaleClientsTable = String.format(EXCLUDE_STALE_CLIENTS_SUBQUERY, oneWeekAgo, threeWeeksAgo); - - qb.setProjectionMap(CLIENTS_RECENCY_PROJECTION_MAP); - - // Use a subquery to quietly exclude stale duplicate client records. - qb.setTables(excludeStaleClientsTable + " AS " + TABLE_CLIENTS + " LEFT OUTER JOIN " + TABLE_TABS + - " ON (" + projectColumn(TABLE_CLIENTS, Clients.GUID) + - " = " + projectColumn(TABLE_TABS, Tabs.CLIENT_GUID) + ")"); - groupBy = projectColumn(TABLE_CLIENTS, Clients.GUID); - break; - - default: - throw new UnsupportedOperationException("Unknown query URI " + uri); - } - - trace("Running built query."); - final Cursor cursor = qb.query(db, projection, selection, selectionArgs, groupBy, null, sortOrder, limit); - cursor.setNotificationUri(getContext().getContentResolver(), BrowserContract.TABS_AUTHORITY_URI); - - return cursor; - } - - int updateValues(Uri uri, ContentValues values, String selection, String[] selectionArgs, String table) { - trace("Updating tabs on URI: " + uri); - - final SQLiteDatabase db = getWritableDatabase(uri); - beginWrite(db); - return db.update(table, values, selection, selectionArgs); - } - - int deleteValues(Uri uri, String selection, String[] selectionArgs, String table) { - debug("Deleting tabs for URI: " + uri); - - final SQLiteDatabase db = getWritableDatabase(uri); - beginWrite(db); - return db.delete(table, selection, selectionArgs); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/db/URLMetadata.java b/mobile/android/base/java/org/mozilla/gecko/db/URLMetadata.java deleted file mode 100644 index 7973839e26..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/db/URLMetadata.java +++ /dev/null @@ -1,25 +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.db; - -import java.util.Collection; -import java.util.List; -import java.util.Map; - -import org.json.JSONObject; - -import org.mozilla.gecko.annotation.RobocopTarget; - -import android.content.ContentResolver; - -@RobocopTarget -public interface URLMetadata { - public Map fromJSON(JSONObject obj); - public Map> getForURLs(final ContentResolver cr, - final Collection urls, - final List columns); - public void save(final ContentResolver cr, final Map data); -} diff --git a/mobile/android/base/java/org/mozilla/gecko/db/URLMetadataTable.java b/mobile/android/base/java/org/mozilla/gecko/db/URLMetadataTable.java deleted file mode 100644 index 49bbb74e7e..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/db/URLMetadataTable.java +++ /dev/null @@ -1,92 +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.db; - -import org.mozilla.gecko.db.BrowserContract.Bookmarks; -import org.mozilla.gecko.db.BrowserContract.History; - -import android.database.sqlite.SQLiteDatabase; -import android.net.Uri; - -// Holds metadata info about urls. Supports some helper functions for getting back a HashMap of key value data. -public class URLMetadataTable extends BaseTable { - private static final String LOGTAG = "GeckoURLMetadataTable"; - - private static final String TABLE = "metadata"; // Name of the table in the db - private static final int TABLE_ID_NUMBER = BrowserProvider.METADATA; - - // Uri for querying this table - public static final Uri CONTENT_URI = Uri.withAppendedPath(BrowserContract.AUTHORITY_URI, "metadata"); - - // Columns in the table - public static final String ID_COLUMN = "id"; - public static final String URL_COLUMN = "url"; - public static final String TILE_IMAGE_URL_COLUMN = "tileImage"; - public static final String TILE_COLOR_COLUMN = "tileColor"; - public static final String TOUCH_ICON_COLUMN = "touchIcon"; - - URLMetadataTable() { } - - @Override - protected String getTable() { - return TABLE; - } - - @Override - public void onCreate(SQLiteDatabase db) { - String create = "CREATE TABLE " + TABLE + " (" + - ID_COLUMN + " INTEGER PRIMARY KEY, " + - URL_COLUMN + " TEXT NON NULL UNIQUE, " + - TILE_IMAGE_URL_COLUMN + " STRING, " + - TILE_COLOR_COLUMN + " STRING, " + - TOUCH_ICON_COLUMN + " STRING);"; - db.execSQL(create); - } - - private void upgradeDatabaseFrom26To27(SQLiteDatabase db) { - db.execSQL("ALTER TABLE " + TABLE + - " ADD COLUMN " + TOUCH_ICON_COLUMN + " STRING"); - } - - @Override - public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { - // This table was added in v21 of the db. Force its creation if we're coming from an earlier version - if (newVersion >= 21 && oldVersion < 21) { - onCreate(db); - return; - } - - // Removed the redundant metadata_url_idx index in version 26 - if (newVersion >= 26 && oldVersion < 26) { - db.execSQL("DROP INDEX IF EXISTS metadata_url_idx"); - } - if (newVersion >= 27 && oldVersion < 27) { - upgradeDatabaseFrom26To27(db); - } - } - - @Override - public Table.ContentProviderInfo[] getContentProviderInfo() { - return new Table.ContentProviderInfo[] { - new Table.ContentProviderInfo(TABLE_ID_NUMBER, TABLE) - }; - } - - public int deleteUnused(final SQLiteDatabase db) { - final String selection = URL_COLUMN + " NOT IN " + - "(SELECT " + History.URL + - " FROM " + History.TABLE_NAME + - " WHERE " + History.IS_DELETED + " = 0" + - " UNION " + - " SELECT " + Bookmarks.URL + - " FROM " + Bookmarks.TABLE_NAME + - " WHERE " + Bookmarks.IS_DELETED + " = 0 " + - " AND " + Bookmarks.URL + " IS NOT NULL)"; - - return db.delete(getTable(), selection, null); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/db/UrlAnnotations.java b/mobile/android/base/java/org/mozilla/gecko/db/UrlAnnotations.java deleted file mode 100644 index dcae6ee79a..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/db/UrlAnnotations.java +++ /dev/null @@ -1,51 +0,0 @@ -/* 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.db; - -import android.content.ContentResolver; -import android.database.Cursor; -import org.mozilla.gecko.annotation.RobocopTarget; -import org.mozilla.gecko.feeds.subscriptions.FeedSubscription; - -public interface UrlAnnotations { - @RobocopTarget void insertAnnotation(ContentResolver cr, String url, String key, String value); - - Cursor getScreenshots(ContentResolver cr); - void insertScreenshot(ContentResolver cr, String pageUrl, String screenshotPath); - - Cursor getFeedSubscriptions(ContentResolver cr); - Cursor getWebsitesWithFeedUrl(ContentResolver cr); - void deleteFeedUrl(ContentResolver cr, String websiteUrl); - boolean hasWebsiteForFeedUrl(ContentResolver cr, String feedUrl); - void deleteFeedSubscription(ContentResolver cr, FeedSubscription subscription); - void updateFeedSubscription(ContentResolver cr, FeedSubscription subscription); - boolean hasFeedSubscription(ContentResolver cr, String feedUrl); - void insertFeedSubscription(ContentResolver cr, FeedSubscription subscription); - boolean hasFeedUrlForWebsite(ContentResolver cr, String websiteUrl); - void insertFeedUrl(ContentResolver cr, String originUrl, String feedUrl); - - void insertReaderViewUrl(ContentResolver cr, String pageURL); - void deleteReaderViewUrl(ContentResolver cr, String pageURL); - - /** - * Did the user ever interact with this URL in regards to home screen shortcuts? - * - * @return true if the user has created a home screen shortcut or declined to create one in the - * past. This method will still return true if the shortcut has been removed from the - * home screen by the user. - */ - boolean hasAcceptedOrDeclinedHomeScreenShortcut(ContentResolver cr, String url); - - /** - * Insert an indication that the user has interacted with this URL in regards to home screen - * shortcuts. - * - * @param hasCreatedShortCut True if a home screen shortcut has been created for this URL. False - * if the user has actively declined to create a shortcut for this URL. - */ - void insertHomeScreenShortcut(ContentResolver cr, String url, boolean hasCreatedShortCut); - - int getAnnotationCount(ContentResolver cr, BrowserContract.UrlAnnotations.Key key); -} diff --git a/mobile/android/base/java/org/mozilla/gecko/delegates/BookmarkStateChangeDelegate.java b/mobile/android/base/java/org/mozilla/gecko/delegates/BookmarkStateChangeDelegate.java deleted file mode 100644 index a1c54d3c33..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/delegates/BookmarkStateChangeDelegate.java +++ /dev/null @@ -1,237 +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.delegates; - -import android.content.Intent; -import android.content.SharedPreferences; -import android.content.res.Resources; -import android.graphics.Color; -import android.graphics.drawable.Drawable; -import android.os.Bundle; -import android.support.design.widget.Snackbar; -import android.support.v4.content.ContextCompat; -import android.util.Log; -import android.view.View; -import android.widget.ListView; - -import org.json.JSONException; -import org.json.JSONObject; -import org.mozilla.gecko.AboutPages; -import org.mozilla.gecko.BrowserApp; -import org.mozilla.gecko.EditBookmarkDialog; -import org.mozilla.gecko.GeckoAppShell; -import org.mozilla.gecko.GeckoSharedPrefs; -import org.mozilla.gecko.R; -import org.mozilla.gecko.SnackbarBuilder; -import org.mozilla.gecko.Tab; -import org.mozilla.gecko.Tabs; -import org.mozilla.gecko.Telemetry; -import org.mozilla.gecko.TelemetryContract; -import org.mozilla.gecko.home.HomeConfig; -import org.mozilla.gecko.promotion.SimpleHelperUI; -import org.mozilla.gecko.prompts.Prompt; -import org.mozilla.gecko.prompts.PromptListItem; -import org.mozilla.gecko.util.DrawableUtil; -import org.mozilla.gecko.util.ThreadUtils; - -import java.lang.ref.WeakReference; - -/** - * Delegate to watch for bookmark state changes. - * - * This is responsible for showing snackbars and helper UIs related to the addition/removal - * of bookmarks, or reader view bookmarks. - */ -public class BookmarkStateChangeDelegate extends BrowserAppDelegateWithReference implements Tabs.OnTabsChangedListener { - private static final String LOGTAG = "BookmarkDelegate"; - - @Override - public void onResume(BrowserApp browserApp) { - Tabs.registerOnTabsChangedListener(this); - } - - @Override - public void onPause(BrowserApp browserApp) { - Tabs.unregisterOnTabsChangedListener(this); - } - - @Override - public void onTabChanged(Tab tab, Tabs.TabEvents msg, String data) { - switch (msg) { - case BOOKMARK_ADDED: - // We always show the special offline snackbar whenever we bookmark a reader page. - // It's possible that the page is already stored offline, however this is highly - // unlikely, and even so it is probably nicer to show the same offline notification - // every time we bookmark an about:reader page. - if (!AboutPages.isAboutReader(tab.getURL())) { - showBookmarkAddedSnackbar(); - } else { - if (!promoteReaderViewBookmarkAdded()) { - showReaderModeBookmarkAddedSnackbar(); - } - } - break; - - case BOOKMARK_REMOVED: - showBookmarkRemovedSnackbar(); - break; - } - } - - @Override - public void onActivityResult(BrowserApp browserApp, int requestCode, int resultCode, Intent data) { - if (requestCode == BrowserApp.ACTIVITY_REQUEST_FIRST_READERVIEW_BOOKMARK) { - if (resultCode == BrowserApp.ACTIVITY_RESULT_FIRST_READERVIEW_BOOKMARKS_GOTO_BOOKMARKS) { - browserApp.openUrlAndStopEditing("about:home?panel=" + HomeConfig.getIdForBuiltinPanelType(HomeConfig.PanelType.BOOKMARKS)); - } else if (resultCode == BrowserApp.ACTIVITY_RESULT_FIRST_READERVIEW_BOOKMARKS_IGNORE) { - showReaderModeBookmarkAddedSnackbar(); - } - } - } - - private boolean promoteReaderViewBookmarkAdded() { - final BrowserApp browserApp = getBrowserApp(); - if (browserApp == null) { - return false; - } - - final SharedPreferences prefs = GeckoSharedPrefs.forProfile(browserApp); - - final boolean hasFirstReaderViewPromptBeenShownBefore = prefs.getBoolean(SimpleHelperUI.PREF_FIRST_RVBP_SHOWN, false); - - if (hasFirstReaderViewPromptBeenShownBefore) { - return false; - } - - SimpleHelperUI.show(browserApp, - SimpleHelperUI.FIRST_RVBP_SHOWN_TELEMETRYEXTRA, - BrowserApp.ACTIVITY_REQUEST_FIRST_READERVIEW_BOOKMARK, - R.string.helper_first_offline_bookmark_title, R.string.helper_first_offline_bookmark_message, - R.drawable.helper_readerview_bookmark, R.string.helper_first_offline_bookmark_button, - BrowserApp.ACTIVITY_RESULT_FIRST_READERVIEW_BOOKMARKS_GOTO_BOOKMARKS, - BrowserApp.ACTIVITY_RESULT_FIRST_READERVIEW_BOOKMARKS_IGNORE); - - GeckoSharedPrefs.forProfile(browserApp) - .edit() - .putBoolean(SimpleHelperUI.PREF_FIRST_RVBP_SHOWN, true) - .apply(); - - return true; - } - - private void showBookmarkAddedSnackbar() { - final BrowserApp browserApp = getBrowserApp(); - if (browserApp == null) { - return; - } - - // This flow is from the option menu which has check to see if a bookmark was already added. - // So, it is safe here to show the snackbar that bookmark_added without any checks. - final SnackbarBuilder.SnackbarCallback callback = new SnackbarBuilder.SnackbarCallback() { - @Override - public void onClick(View v) { - Telemetry.sendUIEvent(TelemetryContract.Event.SHOW, TelemetryContract.Method.TOAST, "bookmark_options"); - showBookmarkDialog(browserApp); - } - }; - - SnackbarBuilder.builder(browserApp) - .message(R.string.bookmark_added) - .duration(Snackbar.LENGTH_LONG) - .action(R.string.bookmark_options) - .callback(callback) - .buildAndShow(); - } - - private void showBookmarkRemovedSnackbar() { - final BrowserApp browserApp = getBrowserApp(); - if (browserApp == null) { - return; - } - - SnackbarBuilder.builder(browserApp) - .message(R.string.bookmark_removed) - .duration(Snackbar.LENGTH_LONG) - .buildAndShow(); - } - - private static void showBookmarkDialog(final BrowserApp browserApp) { - final Resources res = browserApp.getResources(); - final Tab tab = Tabs.getInstance().getSelectedTab(); - - final Prompt ps = new Prompt(browserApp, new Prompt.PromptCallback() { - @Override - public void onPromptFinished(String result) { - int itemId = -1; - try { - itemId = new JSONObject(result).getInt("button"); - } catch (JSONException ex) { - Log.e(LOGTAG, "Exception reading bookmark prompt result", ex); - } - - if (tab == null) { - return; - } - - if (itemId == 0) { - final String extrasId = res.getResourceEntryName(R.string.contextmenu_edit_bookmark); - Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, - TelemetryContract.Method.DIALOG, extrasId); - - new EditBookmarkDialog(browserApp).show(tab.getURL()); - } else if (itemId == 1) { - final String extrasId = res.getResourceEntryName(R.string.contextmenu_add_to_launcher); - Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, - TelemetryContract.Method.DIALOG, extrasId); - - final String url = tab.getURL(); - final String title = tab.getDisplayTitle(); - - if (url != null && title != null) { - ThreadUtils.postToBackgroundThread(new Runnable() { - @Override - public void run() { - GeckoAppShell.createShortcut(title, url); - } - }); - } - } - } - }); - - final PromptListItem[] items = new PromptListItem[2]; - items[0] = new PromptListItem(res.getString(R.string.contextmenu_edit_bookmark)); - items[1] = new PromptListItem(res.getString(R.string.contextmenu_add_to_launcher)); - - ps.show("", "", items, ListView.CHOICE_MODE_NONE); - } - - private void showReaderModeBookmarkAddedSnackbar() { - final BrowserApp browserApp = getBrowserApp(); - if (browserApp == null) { - return; - } - - final Drawable iconDownloaded = DrawableUtil.tintDrawable(browserApp, R.drawable.status_icon_readercache, Color.WHITE); - - final SnackbarBuilder.SnackbarCallback callback = new SnackbarBuilder.SnackbarCallback() { - @Override - public void onClick(View v) { - browserApp.openUrlAndStopEditing("about:home?panel=" + HomeConfig.getIdForBuiltinPanelType(HomeConfig.PanelType.BOOKMARKS)); - } - }; - - SnackbarBuilder.builder(browserApp) - .message(R.string.reader_saved_offline) - .duration(Snackbar.LENGTH_LONG) - .action(R.string.reader_switch_to_bookmarks) - .callback(callback) - .icon(iconDownloaded) - .backgroundColor(ContextCompat.getColor(browserApp, R.color.link_blue)) - .actionColor(Color.WHITE) - .buildAndShow(); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/delegates/BrowserAppDelegate.java b/mobile/android/base/java/org/mozilla/gecko/delegates/BrowserAppDelegate.java deleted file mode 100644 index 70b1349927..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/delegates/BrowserAppDelegate.java +++ /dev/null @@ -1,78 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.delegates; - -import android.content.Intent; -import android.os.Bundle; - -import org.mozilla.gecko.BrowserApp; -import org.mozilla.gecko.mozglue.SafeIntent; -import org.mozilla.gecko.tabs.TabsPanel; - -/** - * Abstract class for extending the behavior of BrowserApp without adding additional code to the - * already huge class. - */ -public abstract class BrowserAppDelegate { - /** - * Called when the BrowserApp activity is first created. - */ - public void onCreate(BrowserApp browserApp, Bundle savedInstanceState) {} - - /** - * Called after the BrowserApp activity has been stopped, prior to it being started again. - */ - public void onRestart(BrowserApp browserApp) {} - - /** - * Called when the BrowserApp activity is becoming visible to the user. - */ - public void onStart(BrowserApp browserApp) {} - - /** - * Called when the BrowserApp activity will start interacting with the user. - */ - public void onResume(BrowserApp browserApp) {} - - /** - * Called when the system is about to start resuming a previous activity. - */ - public void onPause(BrowserApp browserApp) {} - - /** - * Called when BrowserApp activity is no longer visible to the user. - */ - public void onStop(BrowserApp browserApp) {} - - /** - * The final call before the BrowserApp activity is destroyed. - */ - public void onDestroy(BrowserApp browserApp) {} - - /** - * Called when BrowserApp already exists and a new Intent to re-launch it was fired. - */ - public void onNewIntent(BrowserApp browserApp, SafeIntent intent) {} - - /** - * Called when the tabs tray is opened. - */ - public void onTabsTrayShown(BrowserApp browserApp, TabsPanel tabsPanel) {} - - /** - * Called when the tabs tray is closed. - */ - public void onTabsTrayHidden(BrowserApp browserApp, TabsPanel tabsPanel) {} - - /** - * Called when an activity started using startActivityForResult() returns. - * - * Delegates should only use request and result codes declared in BrowserApp itself (as opposed - * to declarations in the delegate), in order to avoid conflicts. - */ - public void onActivityResult(BrowserApp browserApp, int requestCode, int resultCode, Intent data) {} -} - diff --git a/mobile/android/base/java/org/mozilla/gecko/delegates/BrowserAppDelegateWithReference.java b/mobile/android/base/java/org/mozilla/gecko/delegates/BrowserAppDelegateWithReference.java deleted file mode 100644 index c67b8a18a4..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/delegates/BrowserAppDelegateWithReference.java +++ /dev/null @@ -1,29 +0,0 @@ -package org.mozilla.gecko.delegates; - -import android.os.Bundle; -import android.support.annotation.CallSuper; - -import org.mozilla.gecko.BrowserApp; - -import java.lang.ref.WeakReference; - -/** - * BrowserAppDelegate that stores a reference to the parent BrowserApp. - */ -public abstract class BrowserAppDelegateWithReference extends BrowserAppDelegate { - private WeakReference browserApp; - - @Override - @CallSuper - public void onCreate(BrowserApp browserApp, Bundle savedInstanceState) { - this.browserApp = new WeakReference<>(browserApp); - } - - /** - * Obtain the referenced BrowserApp. May return null if the BrowserApp no longer - * exists. - */ - protected BrowserApp getBrowserApp() { - return browserApp.get(); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/delegates/OfflineTabStatusDelegate.java b/mobile/android/base/java/org/mozilla/gecko/delegates/OfflineTabStatusDelegate.java deleted file mode 100644 index 5f3aa9c597..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/delegates/OfflineTabStatusDelegate.java +++ /dev/null @@ -1,119 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.delegates; - -import android.app.Activity; -import android.os.Bundle; -import android.support.annotation.CallSuper; -import android.support.design.widget.Snackbar; -import android.support.v4.content.ContextCompat; - -import org.mozilla.gecko.AboutPages; -import org.mozilla.gecko.BrowserApp; -import org.mozilla.gecko.R; -import org.mozilla.gecko.SnackbarBuilder; -import org.mozilla.gecko.Tab; -import org.mozilla.gecko.Tabs; -import org.mozilla.gecko.Telemetry; -import org.mozilla.gecko.TelemetryContract; - -import java.lang.ref.WeakReference; -import java.util.ArrayList; -import java.util.WeakHashMap; - -/** - * Displays "Showing offline version" message when tabs are loaded from cache while offline. - */ -public class OfflineTabStatusDelegate extends TabsTrayVisibilityAwareDelegate implements Tabs.OnTabsChangedListener { - private WeakReference activityReference; - private WeakHashMap tabsQueuedForOfflineSnackbar = new WeakHashMap<>(); - - @CallSuper - @Override - public void onCreate(BrowserApp browserApp, Bundle savedInstanceState) { - super.onCreate(browserApp, savedInstanceState); - activityReference = new WeakReference(browserApp); - } - - @Override - public void onResume(BrowserApp browserApp) { - Tabs.registerOnTabsChangedListener(this); - } - - @Override - public void onPause(BrowserApp browserApp) { - Tabs.unregisterOnTabsChangedListener(this); - } - - public void onTabChanged(final Tab tab, Tabs.TabEvents event, String data) { - if (tab == null) { - return; - } - - // Ignore tabs loaded regularly. - if (!tab.hasLoadedFromCache()) { - return; - } - - // Ignore tabs displaying about pages - if (AboutPages.isAboutPage(tab.getURL())) { - return; - } - - // We only want to show these notifications for tabs that were loaded successfully. - if (tab.getState() != Tab.STATE_SUCCESS) { - return; - } - - switch (event) { - // We listen specifically for the STOP event (as opposed to PAGE_SHOW), because we need - // to know if page load actually succeeded. When STOP is triggered, tab.getState() - // will return definitive STATE_SUCCESS or STATE_ERROR. When PAGE_SHOW is triggered, - // tab.getState() will return STATE_LOADING, which is ambiguous for our purposes. - // We don't want to show these notifications for 404 pages, for example. See Bug 1304914. - case STOP: - // Show offline notification if tab is visible, or queue it for display later. - if (!isTabsTrayVisible() && Tabs.getInstance().isSelectedTab(tab)) { - showLoadedOfflineSnackbar(activityReference.get()); - } else { - tabsQueuedForOfflineSnackbar.put(tab, null); - } - break; - // Fallthrough; see Bug 1278980 for details on why this event is here. - case OPENED_FROM_TABS_TRAY: - // When tab is selected and offline notification was queued, display it if possible. - // SELECTED event might also fire when we're on a TabStrip, so check first. - case SELECTED: - if (isTabsTrayVisible()) { - break; - } - if (tabsQueuedForOfflineSnackbar.containsKey(tab)) { - showLoadedOfflineSnackbar(activityReference.get()); - tabsQueuedForOfflineSnackbar.remove(tab); - } - break; - } - } - - /** - * Displays the notification snackbar and logs a telemetry event. - * - * @param activity which will be used for displaying the snackbar. - */ - private static void showLoadedOfflineSnackbar(final Activity activity) { - if (activity == null) { - return; - } - - Telemetry.sendUIEvent(TelemetryContract.Event.NETERROR, TelemetryContract.Method.TOAST, "usecache"); - - SnackbarBuilder.builder(activity) - .message(R.string.tab_offline_version) - .duration(Snackbar.LENGTH_INDEFINITE) - .backgroundColor(ContextCompat.getColor(activity, R.color.link_blue)) - .buildAndShow(); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/delegates/ScreenshotDelegate.java b/mobile/android/base/java/org/mozilla/gecko/delegates/ScreenshotDelegate.java deleted file mode 100644 index f048372f73..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/delegates/ScreenshotDelegate.java +++ /dev/null @@ -1,80 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.delegates; - -import android.app.Activity; -import android.os.Bundle; -import android.support.design.widget.Snackbar; -import android.util.Log; - -import org.mozilla.gecko.AppConstants; -import org.mozilla.gecko.BrowserApp; -import org.mozilla.gecko.GeckoProfile; -import org.mozilla.gecko.R; -import org.mozilla.gecko.ScreenshotObserver; -import org.mozilla.gecko.SnackbarBuilder; -import org.mozilla.gecko.Tab; -import org.mozilla.gecko.Tabs; -import org.mozilla.gecko.Telemetry; -import org.mozilla.gecko.TelemetryContract; -import org.mozilla.gecko.db.BrowserDB; - -import java.lang.ref.WeakReference; - -/** - * Delegate for observing screenshots being taken. - */ -public class ScreenshotDelegate extends BrowserAppDelegateWithReference implements ScreenshotObserver.OnScreenshotListener { - private static final String LOGTAG = "GeckoScreenshotDelegate"; - - private final ScreenshotObserver mScreenshotObserver = new ScreenshotObserver(); - - @Override - public void onCreate(BrowserApp browserApp, Bundle savedInstanceState) { - super.onCreate(browserApp, savedInstanceState); - - mScreenshotObserver.setListener(browserApp, this); - } - - @Override - public void onScreenshotTaken(String screenshotPath, String title) { - // Treat screenshots as a sharing method. - Telemetry.sendUIEvent(TelemetryContract.Event.SHARE, TelemetryContract.Method.BUTTON, "screenshot"); - - if (!AppConstants.SCREENSHOTS_IN_BOOKMARKS_ENABLED) { - return; - } - - final Tab selectedTab = Tabs.getInstance().getSelectedTab(); - if (selectedTab == null) { - Log.w(LOGTAG, "Selected tab is null: could not page info to store screenshot."); - return; - } - - final Activity activity = getBrowserApp(); - if (activity == null) { - return; - } - - BrowserDB.from(activity).getUrlAnnotations().insertScreenshot( - activity.getContentResolver(), selectedTab.getURL(), screenshotPath); - - SnackbarBuilder.builder(activity) - .message(R.string.screenshot_added_to_bookmarks) - .duration(Snackbar.LENGTH_SHORT) - .buildAndShow(); - } - - @Override - public void onResume(BrowserApp browserApp) { - mScreenshotObserver.start(); - } - - @Override - public void onPause(BrowserApp browserApp) { - mScreenshotObserver.stop(); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/delegates/TabsTrayVisibilityAwareDelegate.java b/mobile/android/base/java/org/mozilla/gecko/delegates/TabsTrayVisibilityAwareDelegate.java deleted file mode 100644 index ebd3991ea7..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/delegates/TabsTrayVisibilityAwareDelegate.java +++ /dev/null @@ -1,38 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.delegates; - -import android.os.Bundle; -import android.support.annotation.CallSuper; - -import org.mozilla.gecko.BrowserApp; -import org.mozilla.gecko.tabs.TabsPanel; - -public abstract class TabsTrayVisibilityAwareDelegate extends BrowserAppDelegate { - private boolean tabsTrayVisible; - - @Override - @CallSuper - public void onCreate(BrowserApp browserApp, Bundle savedInstanceState) { - tabsTrayVisible = false; - } - - @Override - @CallSuper - public void onTabsTrayShown(BrowserApp browserApp, TabsPanel tabsPanel) { - tabsTrayVisible = true; - } - - @Override - @CallSuper - public void onTabsTrayHidden(BrowserApp browserApp, TabsPanel tabsPanel) { - tabsTrayVisible = false; - } - - protected boolean isTabsTrayVisible() { - return tabsTrayVisible; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/distribution/Distribution.java b/mobile/android/base/java/org/mozilla/gecko/distribution/Distribution.java deleted file mode 100644 index a7b0fe32d3..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/distribution/Distribution.java +++ /dev/null @@ -1,1046 +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.distribution; - -import java.io.BufferedInputStream; -import java.io.File; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.net.HttpURLConnection; -import java.net.ProtocolException; -import java.net.SocketException; -import java.net.URI; -import java.net.URISyntaxException; -import java.net.UnknownHostException; -import java.util.Collections; -import java.util.Enumeration; -import java.util.HashMap; -import java.util.Iterator; -import java.util.Map; -import java.util.Queue; -import java.util.concurrent.ConcurrentLinkedQueue; -import java.util.jar.JarEntry; -import java.util.jar.JarInputStream; -import java.util.zip.ZipEntry; -import java.util.zip.ZipFile; - -import javax.net.ssl.SSLException; - -import ch.boye.httpclientandroidlib.protocol.HTTP; -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; -import org.mozilla.gecko.annotation.RobocopTarget; -import org.mozilla.gecko.AppConstants; -import org.mozilla.gecko.GeckoAppShell; -import org.mozilla.gecko.GeckoSharedPrefs; -import org.mozilla.gecko.Telemetry; -import org.mozilla.gecko.annotation.JNITarget; -import org.mozilla.gecko.util.FileUtils; -import org.mozilla.gecko.util.HardwareUtils; -import org.mozilla.gecko.util.ThreadUtils; - -import android.app.Activity; -import android.content.Context; -import android.content.SharedPreferences; -import android.os.SystemClock; -import android.support.annotation.WorkerThread; -import android.telephony.TelephonyManager; -import android.util.Log; - -/** - * Handles distribution file loading and fetching, - * and the corresponding hand-offs to Gecko. - */ -@RobocopTarget -public class Distribution { - private static final String LOGTAG = "GeckoDistribution"; - - private static final int STATE_UNKNOWN = 0; - private static final int STATE_NONE = 1; - private static final int STATE_SET = 2; - - private static final String FETCH_PROTOCOL = "https"; - private static final String FETCH_HOSTNAME = "mobile.cdn.mozilla.net"; - private static final String FETCH_PATH = "/distributions/1/"; - private static final String FETCH_EXTENSION = ".jar"; - - private static final String EXPECTED_CONTENT_TYPE = "application/java-archive"; - - private static final String DISTRIBUTION_PATH = "distribution/"; - - /** - * Telemetry constants. - */ - private static final String HISTOGRAM_REFERRER_INVALID = "FENNEC_DISTRIBUTION_REFERRER_INVALID"; - private static final String HISTOGRAM_DOWNLOAD_TIME_MS = "FENNEC_DISTRIBUTION_DOWNLOAD_TIME_MS"; - private static final String HISTOGRAM_CODE_CATEGORY = "FENNEC_DISTRIBUTION_CODE_CATEGORY"; - - /** - * Success/failure codes. Don't exceed the maximum listed in Histograms.json. - */ - private static final int CODE_CATEGORY_STATUS_OUT_OF_RANGE = 0; - // HTTP status 'codes' run from 1 to 5. - private static final int CODE_CATEGORY_OFFLINE = 6; - private static final int CODE_CATEGORY_FETCH_EXCEPTION = 7; - - // It's a post-fetch exception if we were able to download, but not - // able to extract. - private static final int CODE_CATEGORY_POST_FETCH_EXCEPTION = 8; - private static final int CODE_CATEGORY_POST_FETCH_SECURITY_EXCEPTION = 9; - - // It's a malformed distribution if we could extract, but couldn't - // process the contents. - private static final int CODE_CATEGORY_MALFORMED_DISTRIBUTION = 10; - - // Specific fetch errors. - private static final int CODE_CATEGORY_FETCH_SOCKET_ERROR = 11; - private static final int CODE_CATEGORY_FETCH_SSL_ERROR = 12; - private static final int CODE_CATEGORY_FETCH_NON_SUCCESS_RESPONSE = 13; - private static final int CODE_CATEGORY_FETCH_INVALID_CONTENT_TYPE = 14; - - // Corresponds to the high value in Histograms.json. - private static final long MAX_DOWNLOAD_TIME_MSEC = 40000; // 40 seconds. - - // If this is true, ready callbacks that arrive after our state is initially determined - // will be queued for delayed running. - // This should only be the case on first run, when we're in STATE_NONE. - // Implicitly accessed from any non-UI threads via Distribution.doInit, but in practice only one - // will actually perform initialization, and "non-UI thread" really means "background thread". - private volatile boolean shouldDelayLateCallbacks = false; - - /** - * These tasks can be queued to run when a distribution is available. - * - * If distributionFound is called, it will be the only call. - * If distributionNotFound is called, it might be followed by - * a call to distributionArrivedLate. - * - * When distributionNotFound is called, - * {@link org.mozilla.gecko.distribution.Distribution#exists()} will return - * false. In the other two callbacks, it will return true. - */ - public interface ReadyCallback { - @WorkerThread - void distributionNotFound(); - - @WorkerThread - void distributionFound(Distribution distribution); - - @WorkerThread - void distributionArrivedLate(Distribution distribution); - } - - /** - * Used as a drop-off point for ReferrerReceiver. Checked when we process - * first-run distribution. - * - * This is `protected` so that test code can clear it between runs. - */ - @RobocopTarget - protected static volatile ReferrerDescriptor referrer; - - private static Distribution instance; - - private final Context context; - private final String packagePath; - private final String prefsBranch; - - volatile int state = STATE_UNKNOWN; - private File distributionDir; - - private final Queue onDistributionReady = new ConcurrentLinkedQueue<>(); - - // Callbacks in this queue have been invoked once as distributionNotFound. - // If they're invoked again, it'll be with distributionArrivedLate. - private final Queue onLateReady = new ConcurrentLinkedQueue<>(); - - /** - * This is a little bit of a bad singleton, because in principle a Distribution - * can be created with arbitrary paths. So we only have one path to get here, and - * it uses the default arguments. Watch out if you're creating your own instances! - */ - public static synchronized Distribution getInstance(Context context) { - if (instance == null) { - instance = new Distribution(context); - } - return instance; - } - - @RobocopTarget - public static class DistributionDescriptor { - public final boolean valid; - public final String id; - public final String version; // Example uses a float, but that's a crazy idea. - - // Default UI-visible description of the distribution. - public final String about; - - // Each distribution file can include multiple localized versions of - // the 'about' string. These are represented as, e.g., "about.en-US" - // keys in the Global object. - // Here we map locale to description. - public final Map localizedAbout; - - @SuppressWarnings("unchecked") - public DistributionDescriptor(JSONObject obj) { - this.id = obj.optString("id"); - this.version = obj.optString("version"); - this.about = obj.optString("about"); - Map loc = new HashMap(); - try { - Iterator keys = obj.keys(); - while (keys.hasNext()) { - String key = keys.next(); - if (key.startsWith("about.")) { - String locale = key.substring(6); - if (!obj.isNull(locale)) { - loc.put(locale, obj.getString(key)); - } - } - } - } catch (JSONException ex) { - Log.w(LOGTAG, "Unable to completely process distribution JSON.", ex); - } - - this.localizedAbout = Collections.unmodifiableMap(loc); - this.valid = (null != this.id) && - (null != this.version) && - (null != this.about); - } - } - - private static Distribution init(final Distribution distribution) { - // Read/write preferences and files on the background thread. - ThreadUtils.postToBackgroundThread(new Runnable() { - @Override - public void run() { - boolean distributionSet = distribution.doInit(); - if (distributionSet) { - String preferencesJSON = ""; - try { - final File descFile = distribution.getDistributionFile("preferences.json"); - preferencesJSON = FileUtils.readStringFromFile(descFile); - } catch (IOException e) { - Log.e(LOGTAG, "Error getting distribution descriptor file.", e); - } - GeckoAppShell.notifyObservers("Distribution:Set", preferencesJSON); - } - } - }); - - return distribution; - } - - /** - * Initializes distribution if it hasn't already been initialized. Sends - * messages to Gecko as appropriate. - * - * @param packagePath where to look for the distribution directory. - */ - @RobocopTarget - public static Distribution init(final Context context, final String packagePath, final String prefsPath) { - return init(new Distribution(context, packagePath, prefsPath)); - } - - /** - * Use Context.getPackageResourcePath to find an implicit - * package path. Reuses the existing Distribution if one exists. - */ - @RobocopTarget - public static Distribution init(final Context context) { - return init(Distribution.getInstance(context)); - } - - /** - * Returns parsed contents of bookmarks.json. - * This method should only be called from a background thread. - */ - public static JSONArray getBookmarks(final Context context) { - Distribution dist = new Distribution(context); - return dist.getBookmarks(); - } - - /** - * @param packagePath where to look for the distribution directory. - */ - public Distribution(final Context context, final String packagePath, final String prefsBranch) { - this.context = context; - this.packagePath = packagePath; - this.prefsBranch = prefsBranch; - } - - public Distribution(final Context context) { - this(context, context.getPackageResourcePath(), null); - } - - /** - * This method is called by ReferrerReceiver when we receive a post-install - * notification from Google Play. - * - * @param ref a parsed referrer value from the store-supplied intent. - */ - public static void onReceivedReferrer(final Context context, final ReferrerDescriptor ref) { - // Track the referrer object for distribution handling. - referrer = ref; - - ThreadUtils.postToBackgroundThread(new Runnable() { - @Override - public void run() { - final Distribution distribution = Distribution.getInstance(context); - - // This will bail if we aren't delayed, or we already have a distribution. - distribution.processDelayedReferrer(ref); - - // On Android 5+ we might receive the referrer intent - // and never actually launch the browser, which is the usual signal - // for the distribution init process to complete. - // Attempt to init here to handle that case. - // Profile setup that relies on the distribution will occur - // when the browser is eventually launched, via `addOnDistributionReadyCallback`. - distribution.doInit(); - } - }); - } - - /** - * Handle a referrer intent that arrives after first use of the distribution. - */ - private void processDelayedReferrer(final ReferrerDescriptor ref) { - ThreadUtils.assertOnBackgroundThread(); - if (state != STATE_NONE) { - return; - } - - Log.i(LOGTAG, "Processing delayed referrer."); - - if (!checkIntentDistribution(ref)) { - // Oh well. No sense keeping these tasks around. - this.onLateReady.clear(); - return; - } - - // Persist our new state. - this.state = STATE_SET; - getSharedPreferences().edit().putInt(getKeyName(), this.state).apply(); - - // Just in case this isn't empty but doInit has finished. - runReadyQueue(); - - // Now process any tasks that already ran while we were in STATE_NONE - // to tell them of our good news. - runLateReadyQueue(); - - // Make sure that changes to search defaults are applied immediately. - GeckoAppShell.notifyObservers("Distribution:Changed", ""); - } - - /** - * Helper to grab a file in the distribution directory. - * - * Returns null if there is no distribution directory or the file - * doesn't exist. Ensures init first. - */ - public File getDistributionFile(String name) { - Log.d(LOGTAG, "Getting file from distribution."); - - if (this.state == STATE_UNKNOWN) { - if (!this.doInit()) { - return null; - } - } - - File dist = ensureDistributionDir(); - if (dist == null) { - return null; - } - - File descFile = new File(dist, name); - if (!descFile.exists()) { - Log.e(LOGTAG, "Distribution directory exists, but no file named " + name); - return null; - } - - return descFile; - } - - public DistributionDescriptor getDescriptor() { - File descFile = getDistributionFile("preferences.json"); - if (descFile == null) { - // Logging and existence checks are handled in getDistributionFile. - return null; - } - - try { - JSONObject all = FileUtils.readJSONObjectFromFile(descFile); - - if (!all.has("Global")) { - Log.e(LOGTAG, "Distribution preferences.json has no Global entry!"); - return null; - } - - return new DistributionDescriptor(all.getJSONObject("Global")); - - } catch (IOException e) { - Log.e(LOGTAG, "Error getting distribution descriptor file.", e); - Telemetry.addToHistogram(HISTOGRAM_CODE_CATEGORY, CODE_CATEGORY_MALFORMED_DISTRIBUTION); - return null; - } catch (JSONException e) { - Log.e(LOGTAG, "Error parsing preferences.json", e); - Telemetry.addToHistogram(HISTOGRAM_CODE_CATEGORY, CODE_CATEGORY_MALFORMED_DISTRIBUTION); - return null; - } - } - - /** - * Get the Android preferences from the preferences.json file, if any exist. - * @return The preferences in a JSONObject, or an empty JSONObject if no preferences are defined. - */ - public JSONObject getAndroidPreferences() { - final File descFile = getDistributionFile("preferences.json"); - if (descFile == null) { - // Logging and existence checks are handled in getDistributionFile. - return new JSONObject(); - } - - try { - final JSONObject all = FileUtils.readJSONObjectFromFile(descFile); - - if (!all.has("AndroidPreferences")) { - return new JSONObject(); - } - - return all.getJSONObject("AndroidPreferences"); - - } catch (IOException e) { - Log.e(LOGTAG, "Error getting distribution descriptor file.", e); - Telemetry.addToHistogram(HISTOGRAM_CODE_CATEGORY, CODE_CATEGORY_MALFORMED_DISTRIBUTION); - return new JSONObject(); - } catch (JSONException e) { - Log.e(LOGTAG, "Error parsing preferences.json", e); - Telemetry.addToHistogram(HISTOGRAM_CODE_CATEGORY, CODE_CATEGORY_MALFORMED_DISTRIBUTION); - return new JSONObject(); - } - } - - public JSONArray getBookmarks() { - File bookmarks = getDistributionFile("bookmarks.json"); - if (bookmarks == null) { - // Logging and existence checks are handled in getDistributionFile. - return null; - } - - try { - return new JSONArray(FileUtils.readStringFromFile(bookmarks)); - } catch (IOException e) { - Log.e(LOGTAG, "Error getting bookmarks", e); - Telemetry.addToHistogram(HISTOGRAM_CODE_CATEGORY, CODE_CATEGORY_MALFORMED_DISTRIBUTION); - return null; - } catch (JSONException e) { - Log.e(LOGTAG, "Error parsing bookmarks.json", e); - Telemetry.addToHistogram(HISTOGRAM_CODE_CATEGORY, CODE_CATEGORY_MALFORMED_DISTRIBUTION); - return null; - } - } - - /** - * Don't call from the main thread. - * - * Postcondition: if this returns true, distributionDir will have been - * set and populated. - * - * This method is *only* protected for use from testDistribution. - * - * @return true if we've set a distribution. - */ - @RobocopTarget - protected boolean doInit() { - ThreadUtils.assertNotOnUiThread(); - - // Bail if we've already tried to initialize the distribution, and - // there wasn't one. - final SharedPreferences settings = getSharedPreferences(); - - final String keyName = getKeyName(); - this.state = settings.getInt(keyName, STATE_UNKNOWN); - - if (this.state == STATE_NONE) { - runReadyQueue(); - return false; - } - - // We've done the work once; don't do it again. - if (this.state == STATE_SET) { - // Note that we don't compute the distribution directory. - // Call `ensureDistributionDir` if you need it. - runReadyQueue(); - return true; - } - - // We try to find the install intent, then the APK, then the system directory, and finally - // an already copied distribution. Already copied might originate from the bouncer APK. - final boolean distributionSet = - checkIntentDistribution(referrer) || - copyAndCheckAPKDistribution() || - checkSystemDistribution() || - checkDataDistribution(); - - // If this is our first run -- and thus we weren't already in STATE_NONE or STATE_SET above -- - // and we didn't find a distribution already, then we should hold on to callbacks in case we - // get a late distribution. - this.shouldDelayLateCallbacks = !distributionSet; - this.state = distributionSet ? STATE_SET : STATE_NONE; - settings.edit().putInt(keyName, this.state).apply(); - - runReadyQueue(); - return distributionSet; - } - - /** - * If applicable, download and select the distribution specified in - * the referrer intent. - * - * @return true if a referrer-supplied distribution was selected. - */ - private boolean checkIntentDistribution(final ReferrerDescriptor referrer) { - if (referrer == null) { - return false; - } - - URI uri = getReferredDistribution(referrer); - if (uri == null) { - return false; - } - - long start = SystemClock.uptimeMillis(); - Log.v(LOGTAG, "Downloading referred distribution: " + uri); - - try { - final HttpURLConnection connection = (HttpURLConnection) uri.toURL().openConnection(); - - // If the Search Activity starts, and we handle the referrer intent, this'll return - // null. Recover gracefully in this case. - final GeckoAppShell.GeckoInterface geckoInterface = GeckoAppShell.getGeckoInterface(); - final String ua; - if (geckoInterface == null) { - // Fall back to GeckoApp's default implementation. - ua = HardwareUtils.isTablet() ? AppConstants.USER_AGENT_FENNEC_TABLET : - AppConstants.USER_AGENT_FENNEC_MOBILE; - } else { - ua = geckoInterface.getDefaultUAString(); - } - - connection.setRequestProperty(HTTP.USER_AGENT, ua); - connection.setRequestProperty("Accept", EXPECTED_CONTENT_TYPE); - - try { - final JarInputStream distro; - try { - distro = fetchDistribution(uri, connection); - } catch (Exception e) { - Log.e(LOGTAG, "Error fetching distribution from network.", e); - recordFetchTelemetry(e); - return false; - } - - long end = SystemClock.uptimeMillis(); - final long duration = end - start; - Log.d(LOGTAG, "Distro fetch took " + duration + "ms; result? " + (distro != null)); - Telemetry.addToHistogram(HISTOGRAM_DOWNLOAD_TIME_MS, clamp(MAX_DOWNLOAD_TIME_MSEC, duration)); - - if (distro == null) { - // Nothing to do. - return false; - } - - // Try to copy distribution files from the fetched stream. - try { - Log.d(LOGTAG, "Copying files from fetched zip."); - if (copyFilesFromStream(distro)) { - // We always copy to the data dir, and we only copy files from - // a 'distribution' subdirectory. Now determine our actual distribution directory. - return checkDataDistribution(); - } - } catch (SecurityException e) { - Log.e(LOGTAG, "Security exception copying files. Corrupt or malicious?", e); - Telemetry.addToHistogram(HISTOGRAM_CODE_CATEGORY, CODE_CATEGORY_POST_FETCH_SECURITY_EXCEPTION); - } catch (Exception e) { - Log.e(LOGTAG, "Error copying files from distribution.", e); - Telemetry.addToHistogram(HISTOGRAM_CODE_CATEGORY, CODE_CATEGORY_POST_FETCH_EXCEPTION); - } finally { - distro.close(); - } - } finally { - connection.disconnect(); - } - } catch (IOException e) { - Log.e(LOGTAG, "Error copying distribution files from network.", e); - recordFetchTelemetry(e); - } - - return false; - } - - private static final int clamp(long v, long c) { - return (int) Math.min(c, v); - } - - /** - * Fetch the provided URI, returning a {@link JarInputStream} if the response body - * is appropriate. - * - * Protected to allow for mocking. - * - * @return the entity body as a stream, or null on failure. - */ - @SuppressWarnings("static-method") - @RobocopTarget - protected JarInputStream fetchDistribution(URI uri, HttpURLConnection connection) throws IOException { - final int status = connection.getResponseCode(); - - Log.d(LOGTAG, "Distribution fetch: " + status); - // We record HTTP statuses as 2xx, 3xx, 4xx, 5xx => 2, 3, 4, 5. - final int value; - if (status > 599 || status < 100) { - Log.wtf(LOGTAG, "Unexpected HTTP status code: " + status); - value = CODE_CATEGORY_STATUS_OUT_OF_RANGE; - } else { - value = status / 100; - } - - Telemetry.addToHistogram(HISTOGRAM_CODE_CATEGORY, value); - - if (status != 200) { - Log.w(LOGTAG, "Got status " + status + " fetching distribution."); - Telemetry.addToHistogram(HISTOGRAM_CODE_CATEGORY, CODE_CATEGORY_FETCH_NON_SUCCESS_RESPONSE); - return null; - } - - final String contentType = connection.getContentType(); - if (contentType == null || !contentType.startsWith(EXPECTED_CONTENT_TYPE)) { - Log.w(LOGTAG, "Malformed response: invalid Content-Type."); - Telemetry.addToHistogram(HISTOGRAM_CODE_CATEGORY, CODE_CATEGORY_FETCH_INVALID_CONTENT_TYPE); - return null; - } - - return new JarInputStream(new BufferedInputStream(connection.getInputStream()), true); - } - - private static void recordFetchTelemetry(final Exception exception) { - if (exception == null) { - // Should never happen. - Telemetry.addToHistogram(HISTOGRAM_CODE_CATEGORY, CODE_CATEGORY_FETCH_EXCEPTION); - return; - } - - if (exception instanceof UnknownHostException) { - // Unknown host => we're offline. - Telemetry.addToHistogram(HISTOGRAM_CODE_CATEGORY, CODE_CATEGORY_OFFLINE); - return; - } - - if (exception instanceof SSLException) { - Telemetry.addToHistogram(HISTOGRAM_CODE_CATEGORY, CODE_CATEGORY_FETCH_SSL_ERROR); - return; - } - - if (exception instanceof ProtocolException || - exception instanceof SocketException) { - Telemetry.addToHistogram(HISTOGRAM_CODE_CATEGORY, CODE_CATEGORY_FETCH_SOCKET_ERROR); - return; - } - - Telemetry.addToHistogram(HISTOGRAM_CODE_CATEGORY, CODE_CATEGORY_FETCH_EXCEPTION); - } - - /** - * @return true if we copied files out of the APK. Sets distributionDir in that case. - */ - private boolean copyAndCheckAPKDistribution() { - try { - // First, try copying distribution files out of the APK. - if (copyFilesFromPackagedAssets()) { - // We always copy to the data dir, and we only copy files from - // a 'distribution' subdirectory. Now determine our actual distribution directory. - return checkDataDistribution(); - } - } catch (IOException e) { - Log.e(LOGTAG, "Error copying distribution files from APK.", e); - } - return false; - } - - /** - * @return true if we found a data distribution (copied from APK or OTA). Sets distributionDir in that case. - */ - private boolean checkDataDistribution() { - return checkDirectories(getDataDistributionDirectories(context)); - } - - /** - * @return true if we found a system distribution. Sets distributionDir in that case. - */ - private boolean checkSystemDistribution() { - return checkDirectories(getSystemDistributionDirectories(context)); - } - - /** - * @return true if one of the specified distribution directories exists. Sets distributionDir in that case. - */ - private boolean checkDirectories(String[] directories) { - for (String path : directories) { - File directory = new File(path); - if (directory.exists()) { - distributionDir = directory; - return true; - } - } - return false; - } - - /** - * Unpack distribution files from a downloaded jar stream. - * - * The caller is responsible for closing the provided stream. - */ - private boolean copyFilesFromStream(JarInputStream jar) throws FileNotFoundException, IOException { - final byte[] buffer = new byte[1024]; - boolean distributionSet = false; - JarEntry entry; - while ((entry = jar.getNextJarEntry()) != null) { - final String name = entry.getName(); - - if (entry.isDirectory()) { - // We'll let getDataFile deal with creating the directory hierarchy. - // Yes, we can do better, but it can wait. - continue; - } - - if (!name.startsWith(DISTRIBUTION_PATH)) { - continue; - } - - File outFile = getDataFile(name); - if (outFile == null) { - continue; - } - - distributionSet = true; - - writeStream(jar, outFile, entry.getTime(), buffer); - } - - return distributionSet; - } - - /** - * Copies the /assets/distribution folder out of the APK and into the app's data directory. - * Returns true if distribution files were found and copied. - */ - private boolean copyFilesFromPackagedAssets() throws IOException { - final File applicationPackage = new File(packagePath); - final ZipFile zip = new ZipFile(applicationPackage); - - final String assetsPrefix = "assets/"; - final String fullPrefix = assetsPrefix + DISTRIBUTION_PATH; - - boolean distributionSet = false; - try { - final byte[] buffer = new byte[1024]; - - final Enumeration zipEntries = zip.entries(); - while (zipEntries.hasMoreElements()) { - final ZipEntry fileEntry = zipEntries.nextElement(); - final String name = fileEntry.getName(); - - if (fileEntry.isDirectory()) { - // We'll let getDataFile deal with creating the directory hierarchy. - continue; - } - - // Read from "assets/distribution/**". - if (!name.startsWith(fullPrefix)) { - continue; - } - - // Write to "distribution/**". - final String nameWithoutPrefix = name.substring(assetsPrefix.length()); - final File outFile = getDataFile(nameWithoutPrefix); - if (outFile == null) { - continue; - } - - distributionSet = true; - - final InputStream fileStream = zip.getInputStream(fileEntry); - try { - writeStream(fileStream, outFile, fileEntry.getTime(), buffer); - } finally { - fileStream.close(); - } - } - } finally { - zip.close(); - } - - return distributionSet; - } - - private void writeStream(InputStream fileStream, File outFile, final long modifiedTime, byte[] buffer) - throws FileNotFoundException, IOException { - final OutputStream outStream = new FileOutputStream(outFile); - try { - int count; - while ((count = fileStream.read(buffer)) > 0) { - outStream.write(buffer, 0, count); - } - - outFile.setLastModified(modifiedTime); - } finally { - outStream.close(); - } - } - - /** - * Return a File instance in the data directory, ensuring - * that the parent exists. - * - * @return null if the parents could not be created. - */ - private File getDataFile(final String name) { - File outFile = new File(getDataDir(), name); - File dir = outFile.getParentFile(); - - if (!dir.exists()) { - Log.d(LOGTAG, "Creating " + dir.getAbsolutePath()); - if (!dir.mkdirs()) { - Log.e(LOGTAG, "Unable to create directories: " + dir.getAbsolutePath()); - return null; - } - } - - return outFile; - } - - private URI getReferredDistribution(ReferrerDescriptor descriptor) { - final String content = descriptor.content; - if (content == null) { - return null; - } - - // We restrict here to avoid injection attacks. After all, - // we're downloading a distribution payload based on intent input. - if (!content.matches("^[a-zA-Z0-9]+$")) { - Log.e(LOGTAG, "Invalid referrer content: " + content); - Telemetry.addToHistogram(HISTOGRAM_REFERRER_INVALID, 1); - return null; - } - - try { - return new URI(FETCH_PROTOCOL, FETCH_HOSTNAME, FETCH_PATH + content + FETCH_EXTENSION, null); - } catch (URISyntaxException e) { - // This should never occur. - Log.wtf(LOGTAG, "Invalid URI with content " + content + "!"); - return null; - } - } - - /** - * After calling this method, either distributionDir - * will be set, or there is no distribution in use. - * - * Only call after init. - */ - private File ensureDistributionDir() { - if (this.distributionDir != null) { - return this.distributionDir; - } - - if (this.state != STATE_SET) { - return null; - } - - // After init, we know that either we've copied a distribution out of - // the APK, or it exists in /system/. - // Look in each location in turn. - // (This could be optimized by caching the path in shared prefs.) - if (checkDataDistribution() || checkSystemDistribution()) { - return distributionDir; - } - - return null; - } - - private String getDataDir() { - return context.getApplicationInfo().dataDir; - } - - @JNITarget - public static String[] getDistributionDirectories() { - final Context context = GeckoAppShell.getApplicationContext(); - - final String[] dataDirectories = getDataDistributionDirectories(context); - final String[] systemDirectories = getSystemDistributionDirectories(context); - - final String[] directories = new String[dataDirectories.length + systemDirectories.length]; - - System.arraycopy(dataDirectories, 0, directories, 0, dataDirectories.length); - System.arraycopy(systemDirectories, 0, directories, dataDirectories.length, systemDirectories.length); - - return directories; - } - - /** - * Get a list of system distribution folder candidates. - * - * /system//distribution// - For bundled distributions for specific network providers - * /system//distribution/ - For bundled distributions for specific countries - * /system//distribution/default - For bundled distributions with no matching mcc/mnc - * /system//distribution - Default non-bundled system distribution - */ - private static String[] getSystemDistributionDirectories(Context context) { - final String baseDirectory = "/system/" + context.getPackageName() + "/distribution"; - return getDistributionDirectoriesFromBaseDirectory(context, baseDirectory); - } - - /** - * Get a list of data distribution folder candidates. - * - * /distribution// - For bundled distributions for specific network providers - * /distribution/ - For bundled distributions for specific countries - * /distribution/default - For bundled distributions with no matching mcc/mnc - * /distribution - Default non-bundled system distribution - */ - private static String[] getDataDistributionDirectories(Context context) { - final String baseDirectory = new File(context.getApplicationInfo().dataDir, DISTRIBUTION_PATH).getAbsolutePath(); - return getDistributionDirectoriesFromBaseDirectory(context, baseDirectory); - } - - /** - * Get a list of distribution folder candidates inside the specified base directory. - */ - private static String[] getDistributionDirectoriesFromBaseDirectory(Context context, String baseDirectory) { - final TelephonyManager telephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); - if (telephonyManager != null && telephonyManager.getSimState() == TelephonyManager.SIM_STATE_READY) { - final String simOperator = telephonyManager.getSimOperator(); - - if (simOperator != null && simOperator.length() >= 5) { - final String mcc = simOperator.substring(0, 3); - final String mnc = simOperator.substring(3); - - return new String[] { - baseDirectory + "/" + mcc + "/" + mnc, - baseDirectory + "/" + mcc, - baseDirectory + "/default", - baseDirectory - }; - } - } - - return new String[] { - baseDirectory + "/default", - baseDirectory - }; - } - - /** - * The provided ReadyCallback will be queued for execution after - * the distribution is ready, or queued for immediate execution if the - * distribution has already been processed. - * - * Each ReadyCallback will be executed on the background thread. - */ - public void addOnDistributionReadyCallback(final ReadyCallback callback) { - if (state == STATE_UNKNOWN) { - // Queue for later. - onDistributionReady.add(callback); - } else { - invokeCallbackDelayed(callback); - } - } - - /** - * Run our delayed queue, after a delayed distribution arrives. - */ - private void runLateReadyQueue() { - ReadyCallback task; - while ((task = onLateReady.poll()) != null) { - invokeLateCallbackDelayed(task); - } - } - - /** - * Execute tasks that wanted to run when we were done loading - * the distribution. - */ - private void runReadyQueue() { - ReadyCallback task; - while ((task = onDistributionReady.poll()) != null) { - invokeCallbackDelayed(task); - } - } - - private void invokeLateCallbackDelayed(final ReadyCallback callback) { - ThreadUtils.postToBackgroundThread(new Runnable() { - @Override - public void run() { - // Sanity. - if (state != STATE_SET) { - Log.w(LOGTAG, "Refusing to invoke late distro callback in state " + state); - return; - } - callback.distributionArrivedLate(Distribution.this); - } - }); - } - - private void invokeCallbackDelayed(final ReadyCallback callback) { - ThreadUtils.postToBackgroundThread(new Runnable() { - @WorkerThread - @Override - public void run() { - switch (state) { - case STATE_SET: - callback.distributionFound(Distribution.this); - break; - case STATE_NONE: - callback.distributionNotFound(); - if (shouldDelayLateCallbacks) { - onLateReady.add(callback); - } - break; - default: - throw new IllegalStateException("Expected STATE_NONE or STATE_SET, got " + state); - } - } - }); - } - - /** - * A safe way for callers to determine if this Distribution instance - * represents a real live distribution. - */ - public boolean exists() { - return state == STATE_SET; - } - - private String getKeyName() { - return context.getPackageName() + ".distribution_state"; - } - - private SharedPreferences getSharedPreferences() { - final SharedPreferences settings; - if (prefsBranch == null) { - settings = GeckoSharedPrefs.forApp(context); - } else { - settings = context.getSharedPreferences(prefsBranch, Activity.MODE_PRIVATE); - } - return settings; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/distribution/DistributionStoreCallback.java b/mobile/android/base/java/org/mozilla/gecko/distribution/DistributionStoreCallback.java deleted file mode 100644 index 11ed4811f2..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/distribution/DistributionStoreCallback.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * 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.distribution; - -import android.content.Context; -import android.content.SharedPreferences; -import android.util.Log; -import org.mozilla.gecko.GeckoSharedPrefs; - -import java.lang.ref.WeakReference; - -/** - * A distribution ready callback that will store the distribution ID to profile-specific shared preferences. - */ -public class DistributionStoreCallback implements Distribution.ReadyCallback { - private static final String LOGTAG = "Gecko" + DistributionStoreCallback.class.getSimpleName(); - - public static final String PREF_DISTRIBUTION_ID = "distribution.id"; - - private final WeakReference contextReference; - private final String profileName; - - public DistributionStoreCallback(final Context context, final String profileName) { - this.contextReference = new WeakReference<>(context); - this.profileName = profileName; - } - - public void distributionNotFound() { /* nothing to do here */ } - - @Override - public void distributionFound(final Distribution distribution) { - storeDistribution(distribution); - } - - @Override - public void distributionArrivedLate(final Distribution distribution) { - storeDistribution(distribution); - } - - private void storeDistribution(final Distribution distribution) { - final Context context = contextReference.get(); - if (context == null) { - Log.w(LOGTAG, "Context is no longer alive, could retrieve shared prefs to store distribution"); - return; - } - - // While the distribution preferences are per install and not per profile, it's okay to use the - // profile-specific prefs because: - // 1) We don't really support mulitple profiles for end-users - // 2) The TelemetryUploadService already accesses profile-specific shared prefs so this keeps things simple. - final SharedPreferences sharedPrefs = GeckoSharedPrefs.forProfileName(context, profileName); - final Distribution.DistributionDescriptor desc = distribution.getDescriptor(); - if (desc != null) { - sharedPrefs.edit().putString(PREF_DISTRIBUTION_ID, desc.id).apply(); - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/distribution/PartnerBookmarksProviderProxy.java b/mobile/android/base/java/org/mozilla/gecko/distribution/PartnerBookmarksProviderProxy.java deleted file mode 100644 index 78a77221d0..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/distribution/PartnerBookmarksProviderProxy.java +++ /dev/null @@ -1,322 +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.distribution; - -import android.content.ContentProvider; -import android.content.ContentResolver; -import android.content.ContentUris; -import android.content.ContentValues; -import android.content.Context; -import android.content.SharedPreferences; -import android.content.UriMatcher; -import android.database.Cursor; -import android.database.CursorWrapper; -import android.net.Uri; -import android.support.annotation.NonNull; -import android.text.TextUtils; - -import org.mozilla.gecko.GeckoSharedPrefs; -import org.mozilla.gecko.db.BrowserContract; - -import java.util.HashSet; -import java.util.Set; - -/** - * A proxy for the partner bookmarks provider. Bookmark and folder ids of the partner bookmarks providers - * will be transformed so that they do not overlap with the ids from the local database. - * - * Bookmarks in folder: - * content://{PACKAGE_ID}.partnerbookmarks/bookmarks/{folderId} - * Icon of bookmark: - * content://{PACKAGE_ID}.partnerbookmarks/icons/{bookmarkId} - */ -public class PartnerBookmarksProviderProxy extends ContentProvider { - /** - * The contract between the partner bookmarks provider and applications. Contains the definition - * for the supported URIs and columns. - */ - public static class PartnerContract { - public static final Uri CONTENT_URI = Uri.parse("content://com.android.partnerbookmarks/bookmarks"); - - public static final int TYPE_BOOKMARK = 1; - public static final int TYPE_FOLDER = 2; - - public static final int PARENT_ROOT_ID = 0; - - public static final String ID = "_id"; - public static final String TYPE = "type"; - public static final String URL = "url"; - public static final String TITLE = "title"; - public static final String FAVICON = "favicon"; - public static final String TOUCHICON = "touchicon"; - public static final String PARENT = "parent"; - } - - private static final String AUTHORITY_PREFIX = ".partnerbookmarks"; - - private static final int URI_MATCH_BOOKMARKS = 1000; - private static final int URI_MATCH_ICON = 1001; - private static final int URI_MATCH_BOOKMARK = 1002; - - private static final String PREF_DELETED_PARTNER_BOOKMARKS = "distribution.partner.bookmark.deleted"; - - /** - * Cursor wrapper for filtering empty folders. - */ - private static class FilteredCursor extends CursorWrapper { - private HashSet emptyFolderPositions; - private int count; - - public FilteredCursor(PartnerBookmarksProviderProxy proxy, Cursor cursor) { - super(cursor); - - emptyFolderPositions = new HashSet<>(); - count = cursor.getCount(); - - for (int i = 0; i < cursor.getCount(); i++) { - cursor.moveToPosition(i); - - final long id = cursor.getLong(cursor.getColumnIndexOrThrow(BrowserContract.Bookmarks._ID)); - final int type = cursor.getInt(cursor.getColumnIndexOrThrow(BrowserContract.Bookmarks.TYPE)); - - if (type == BrowserContract.Bookmarks.TYPE_FOLDER && proxy.isFolderEmpty(id)) { - // We do not support deleting folders. So at least hide partner folders that are - // empty because all bookmarks inside it are deleted/hidden. - // Note that this will still show folders with empty folders in them. But multi-level - // partner bookmarks are very unlikely. - - count--; - emptyFolderPositions.add(i); - } - } - } - - @Override - public int getCount() { - return count; - } - - @Override - public boolean moveToPosition(int position) { - final Cursor cursor = getWrappedCursor(); - final int actualCount = cursor.getCount(); - - // Find the next position pointing to a bookmark or a non-empty folder - while (position < actualCount && emptyFolderPositions.contains(position)) { - position++; - } - - return position < actualCount && cursor.moveToPosition(position); - } - } - - private static String getAuthority(Context context) { - return context.getPackageName() + AUTHORITY_PREFIX; - } - - public static Uri getUriForBookmarks(Context context, long folderId) { - return new Uri.Builder() - .scheme("content") - .authority(getAuthority(context)) - .appendPath("bookmarks") - .appendPath(String.valueOf(folderId)) - .build(); - } - - public static Uri getUriForIcon(Context context, long bookmarkId) { - return new Uri.Builder() - .scheme("content") - .authority(getAuthority(context)) - .appendPath("icons") - .appendPath(String.valueOf(bookmarkId)) - .build(); - } - - public static Uri getUriForBookmark(Context context, long bookmarkId) { - return new Uri.Builder() - .scheme("content") - .authority(getAuthority(context)) - .appendPath("bookmark") - .appendPath(String.valueOf(bookmarkId)) - .build(); - } - - private final UriMatcher uriMatcher = new UriMatcher(UriMatcher.NO_MATCH); - - @Override - public boolean onCreate() { - String authority = getAuthority(assertAndGetContext()); - - uriMatcher.addURI(authority, "bookmarks/*", URI_MATCH_BOOKMARKS); - uriMatcher.addURI(authority, "icons/*", URI_MATCH_ICON); - uriMatcher.addURI(authority, "bookmark/*", URI_MATCH_BOOKMARK); - - return true; - } - - @Override - public Cursor query(@NonNull Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { - final Context context = assertAndGetContext(); - final int match = uriMatcher.match(uri); - - final ContentResolver contentResolver = context.getContentResolver(); - - switch (match) { - case URI_MATCH_BOOKMARKS: - final long bookmarkId = ContentUris.parseId(uri); - if (bookmarkId == -1) { - throw new IllegalArgumentException("Bookmark id is not a number"); - } - final Cursor cursor = getBookmarksInFolder(contentResolver, bookmarkId); - cursor.setNotificationUri(context.getContentResolver(), uri); - return new FilteredCursor(this, cursor); - - case URI_MATCH_ICON: - return getIcon(contentResolver, ContentUris.parseId(uri)); - - default: - throw new UnsupportedOperationException("Unknown URI " + uri.toString()); - } - } - - @Override - public int delete(@NonNull Uri uri, String selection, String[] selectionArgs) { - final int match = uriMatcher.match(uri); - - switch (match) { - case URI_MATCH_BOOKMARK: - rememberRemovedBookmark(ContentUris.parseId(uri)); - notifyBookmarkChange(); - return 1; - - default: - throw new UnsupportedOperationException("Unknown URI " + uri.toString()); - } - } - - private void notifyBookmarkChange() { - final Context context = assertAndGetContext(); - - context.getContentResolver().notifyChange( - new Uri.Builder() - .scheme("content") - .authority(getAuthority(context)) - .appendPath("bookmarks") - .build(), - null); - } - - private synchronized void rememberRemovedBookmark(long bookmarkId) { - Set deletedIds = getRemovedBookmarkIds(); - - deletedIds.add(String.valueOf(bookmarkId)); - - GeckoSharedPrefs.forProfile(assertAndGetContext()) - .edit() - .putStringSet(PREF_DELETED_PARTNER_BOOKMARKS, deletedIds) - .apply(); - } - - private synchronized Set getRemovedBookmarkIds() { - SharedPreferences preferences = GeckoSharedPrefs.forProfile(assertAndGetContext()); - return preferences.getStringSet(PREF_DELETED_PARTNER_BOOKMARKS, new HashSet()); - } - - private Cursor getBookmarksInFolder(ContentResolver contentResolver, long folderId) { - // Use root folder id or transform negative id into actual (positive) folder id. - final long actualFolderId = folderId == BrowserContract.Bookmarks.FIXED_ROOT_ID - ? PartnerContract.PARENT_ROOT_ID - : BrowserContract.Bookmarks.FAKE_PARTNER_BOOKMARKS_START - folderId; - - final String removedBookmarkIds = TextUtils.join(",", getRemovedBookmarkIds()); - - return contentResolver.query( - PartnerContract.CONTENT_URI, - new String[] { - // Transform ids into negative values starting with FAKE_PARTNER_BOOKMARKS_START. - "(" + BrowserContract.Bookmarks.FAKE_PARTNER_BOOKMARKS_START + " - " + PartnerContract.ID + ") as " + BrowserContract.Bookmarks._ID, - "(" + BrowserContract.Bookmarks.FAKE_PARTNER_BOOKMARKS_START + " - " + PartnerContract.ID + ") as " + BrowserContract.Combined.BOOKMARK_ID, - PartnerContract.TITLE + " as " + BrowserContract.Bookmarks.TITLE, - PartnerContract.URL + " as " + BrowserContract.Bookmarks.URL, - // Transform parent ids to negative ids as well - "(" + BrowserContract.Bookmarks.FAKE_PARTNER_BOOKMARKS_START + " - " + PartnerContract.PARENT + ") as " + BrowserContract.Bookmarks.PARENT, - // Convert types (we use 0-1 and the partner provider 1-2) - "(2 - " + PartnerContract.TYPE + ") as " + BrowserContract.Bookmarks.TYPE, - // Use the ID of the entry as GUID - PartnerContract.ID + " as " + BrowserContract.Bookmarks.GUID - }, - PartnerContract.PARENT + " = ?" - // We only want to read bookmarks or folders from the content provider - + " AND " + BrowserContract.Bookmarks.TYPE + " IN (?,?)" - // Only select entries with non empty title - + " AND " + BrowserContract.Bookmarks.TITLE + " <> ''" - // Filter all "deleted" ids - + " AND " + BrowserContract.Combined.BOOKMARK_ID + " NOT IN (" + removedBookmarkIds + ")", - new String[] { - String.valueOf(actualFolderId), - String.valueOf(PartnerContract.TYPE_BOOKMARK), - String.valueOf(PartnerContract.TYPE_FOLDER) - }, - // Same order we use in our content provider (without position) - BrowserContract.Bookmarks.TYPE + " ASC, " + BrowserContract.Bookmarks._ID + " ASC"); - } - - private boolean isFolderEmpty(long folderId) { - final Context context = assertAndGetContext(); - final Cursor cursor = getBookmarksInFolder(context.getContentResolver(), folderId); - - if (cursor == null) { - return true; - } - - try { - return cursor.getCount() == 0; - } finally { - cursor.close(); - } - } - - private Cursor getIcon(ContentResolver contentResolver, long bookmarkId) { - final long actualId = BrowserContract.Bookmarks.FAKE_PARTNER_BOOKMARKS_START - bookmarkId; - - return contentResolver.query( - PartnerContract.CONTENT_URI, - new String[] { - PartnerContract.TOUCHICON, - PartnerContract.FAVICON - }, - PartnerContract.ID + " = ?", - new String[] { - String.valueOf(actualId) - }, - null); - } - - private Context assertAndGetContext() { - final Context context = super.getContext(); - - if (context == null) { - throw new AssertionError("Context is null"); - } - - return context; - } - - @Override - public String getType(@NonNull Uri uri) { - throw new UnsupportedOperationException(); - } - - @Override - public Uri insert(@NonNull Uri uri, ContentValues values) { - throw new UnsupportedOperationException(); - } - - @Override - public int update(@NonNull Uri uri, ContentValues values, String selection, String[] selectionArgs) { - throw new UnsupportedOperationException(); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/distribution/PartnerBrowserCustomizationsClient.java b/mobile/android/base/java/org/mozilla/gecko/distribution/PartnerBrowserCustomizationsClient.java deleted file mode 100644 index 2dad21a48f..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/distribution/PartnerBrowserCustomizationsClient.java +++ /dev/null @@ -1,43 +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.distribution; - -import android.content.Context; -import android.database.Cursor; -import android.net.Uri; - -/** - * Client for accessing data from Android's "partner browser customizations" content provider. - */ -public class PartnerBrowserCustomizationsClient { - private static final Uri CONTENT_URI = Uri.parse("content://com.android.partnerbrowsercustomizations"); - - private static final Uri HOMEPAGE_URI = CONTENT_URI.buildUpon().path("homepage").build(); - - private static final String COLUMN_HOMEPAGE = "homepage"; - - /** - * Returns the partner homepage or null if it could not be read from the content provider. - */ - public static String getHomepage(Context context) { - Cursor cursor = context.getContentResolver().query( - HOMEPAGE_URI, new String[] { COLUMN_HOMEPAGE }, null, null, null); - - if (cursor == null) { - return null; - } - - try { - if (!cursor.moveToFirst()) { - return null; - } - - return cursor.getString(cursor.getColumnIndexOrThrow(COLUMN_HOMEPAGE)); - } finally { - cursor.close(); - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/distribution/ReferrerDescriptor.java b/mobile/android/base/java/org/mozilla/gecko/distribution/ReferrerDescriptor.java deleted file mode 100644 index 4a1be656b2..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/distribution/ReferrerDescriptor.java +++ /dev/null @@ -1,64 +0,0 @@ -/* 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.distribution; - -import org.mozilla.gecko.annotation.RobocopTarget; - -import android.net.Uri; - -import java.net.URLDecoder; -import java.io.UnsupportedEncodingException; - -/** - * Encapsulates access to values encoded in the "referrer" extra of an install intent. - * - * This object is immutable. - * - * Example input: - * - * "utm_source=campsource&utm_medium=campmed&utm_term=term%2Bhere&utm_content=content&utm_campaign=name" - */ -@RobocopTarget -public class ReferrerDescriptor { - public final String source; - public final String medium; - public final String term; - public final String content; - public final String campaign; - - public ReferrerDescriptor(String referrer) { - if (referrer == null) { - source = null; - medium = null; - term = null; - content = null; - campaign = null; - return; - } - - try { - referrer = URLDecoder.decode(referrer, "UTF-8"); - } catch (UnsupportedEncodingException e) { - // UTF-8 is always supported - } - - final Uri u = new Uri.Builder() - .scheme("http") - .authority("local") - .path("/") - .encodedQuery(referrer).build(); - - source = u.getQueryParameter("utm_source"); - medium = u.getQueryParameter("utm_medium"); - term = u.getQueryParameter("utm_term"); - content = u.getQueryParameter("utm_content"); - campaign = u.getQueryParameter("utm_campaign"); - } - - @Override - public String toString() { - return "{s: " + source + ", m: " + medium + ", t: " + term + ", c: " + content + ", c: " + campaign + "}"; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/distribution/ReferrerReceiver.java b/mobile/android/base/java/org/mozilla/gecko/distribution/ReferrerReceiver.java deleted file mode 100644 index 3651d6068f..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/distribution/ReferrerReceiver.java +++ /dev/null @@ -1,107 +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.distribution; - -import org.mozilla.gecko.AdjustConstants; -import org.mozilla.gecko.annotation.RobocopTarget; -import org.mozilla.gecko.AppConstants; -import org.mozilla.gecko.GeckoAppShell; - -import org.json.JSONException; -import org.json.JSONObject; - -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.support.v4.content.LocalBroadcastManager; -import android.text.TextUtils; -import android.util.Log; - -public class ReferrerReceiver extends BroadcastReceiver { - private static final String LOGTAG = "GeckoReferrerReceiver"; - - private static final String ACTION_INSTALL_REFERRER = "com.android.vending.INSTALL_REFERRER"; - - // Sent when we're done. - @RobocopTarget - public static final String ACTION_REFERRER_RECEIVED = "org.mozilla.fennec.REFERRER_RECEIVED"; - - /** - * If the install intent has this source, it is a Mozilla specific or over - * the air distribution referral. We'll track the campaign ID using - * Mozilla's metrics systems. - * - * If the install intent has a source different than this one, it is a - * referral from an advertising network. We may track these campaigns using - * third-party tracking and metrics systems. - */ - private static final String MOZILLA_UTM_SOURCE = "mozilla"; - - /** - * If the install intent has this campaign, we'll load the specified distribution. - */ - private static final String DISTRIBUTION_UTM_CAMPAIGN = "distribution"; - - @Override - public void onReceive(Context context, Intent intent) { - Log.v(LOGTAG, "Received intent " + intent); - if (!ACTION_INSTALL_REFERRER.equals(intent.getAction())) { - // This should never happen. - return; - } - - // Track the referrer object for distribution handling. - ReferrerDescriptor referrer = new ReferrerDescriptor(intent.getStringExtra("referrer")); - - if (!TextUtils.equals(referrer.source, MOZILLA_UTM_SOURCE)) { - // Allow the Adjust handler to process the intent. - try { - AdjustConstants.getAdjustHelper().onReceive(context, intent); - } catch (Exception e) { - Log.e(LOGTAG, "Got exception in Adjust's onReceive; ignoring referrer intent.", e); - } - return; - } - - if (TextUtils.equals(referrer.campaign, DISTRIBUTION_UTM_CAMPAIGN)) { - Distribution.onReceivedReferrer(context, referrer); - // We want Adjust information for OTA distributions as well - try { - AdjustConstants.getAdjustHelper().onReceive(context, intent); - } catch (Exception e) { - Log.e(LOGTAG, "Got exception in Adjust's onReceive for distribution.", e); - } - } else { - Log.d(LOGTAG, "Not downloading distribution: non-matching campaign."); - // If this is a Mozilla campaign, pass the campaign along to Gecko. - // It'll pretend to be a "playstore" distribution for BLP purposes. - propagateMozillaCampaign(referrer); - } - - // Broadcast a secondary, local intent to allow test code to respond. - final Intent receivedIntent = new Intent(ACTION_REFERRER_RECEIVED); - LocalBroadcastManager.getInstance(context).sendBroadcast(receivedIntent); - } - - - private void propagateMozillaCampaign(ReferrerDescriptor referrer) { - if (referrer.campaign == null) { - return; - } - - try { - final JSONObject data = new JSONObject(); - data.put("id", "playstore"); - data.put("version", referrer.campaign); - String payload = data.toString(); - - // Try to make sure the prefs are written as a group. - GeckoAppShell.notifyObservers("Campaign:Set", payload); - } catch (JSONException e) { - Log.e(LOGTAG, "Error propagating campaign identifier.", e); - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/dlc/BaseAction.java b/mobile/android/base/java/org/mozilla/gecko/dlc/BaseAction.java deleted file mode 100644 index 28d6b238df..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/dlc/BaseAction.java +++ /dev/null @@ -1,166 +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.dlc; - -import android.content.Context; -import android.support.annotation.IntDef; -import android.util.Log; - -import org.mozilla.gecko.AppConstants; -import org.mozilla.gecko.background.nativecode.NativeCrypto; -import org.mozilla.gecko.dlc.catalog.DownloadContent; -import org.mozilla.gecko.dlc.catalog.DownloadContentCatalog; -import org.mozilla.gecko.sync.Utils; -import org.mozilla.gecko.util.HardwareUtils; -import org.mozilla.gecko.util.IOUtils; -import org.mozilla.gecko.util.ProxySelector; - -import java.io.BufferedInputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.net.HttpURLConnection; -import java.net.MalformedURLException; -import java.net.URI; -import java.net.URISyntaxException; - -public abstract class BaseAction { - private static final String LOGTAG = "GeckoDLCBaseAction"; - - /** - * Exception indicating a recoverable error has happened. Download of the content will be retried later. - */ - /* package-private */ static class RecoverableDownloadContentException extends Exception { - private static final long serialVersionUID = -2246772819507370734L; - - @IntDef({MEMORY, DISK_IO, SERVER, NETWORK}) - public @interface ErrorType {} - public static final int MEMORY = 1; - public static final int DISK_IO = 2; - public static final int SERVER = 3; - public static final int NETWORK = 4; - - private int errorType; - - public RecoverableDownloadContentException(@ErrorType int errorType, String message) { - super(message); - this.errorType = errorType; - } - - public RecoverableDownloadContentException(@ErrorType int errorType, Throwable cause) { - super(cause); - this.errorType = errorType; - } - - @ErrorType - public int getErrorType() { - return errorType; - } - - /** - * Should this error be counted as failure? If this type of error will happen multiple times in a row then this - * error will be treated as permanently and the operation will not be tried again until the content changes. - */ - public boolean shouldBeCountedAsFailure() { - if (NETWORK == errorType) { - return false; // Always retry after network errors - } - - return true; - } - } - - /** - * If this exception is thrown the content will be marked as unrecoverable, permanently failed and we will not try - * downloading it again - until a newer version of the content is available. - */ - /* package-private */ static class UnrecoverableDownloadContentException extends Exception { - private static final long serialVersionUID = 8956080754787367105L; - - public UnrecoverableDownloadContentException(String message) { - super(message); - } - - public UnrecoverableDownloadContentException(Throwable cause) { - super(cause); - } - } - - public abstract void perform(Context context, DownloadContentCatalog catalog); - - protected File getDestinationFile(Context context, DownloadContent content) - throws UnrecoverableDownloadContentException, RecoverableDownloadContentException { - if (content.isFont()) { - File destinationDirectory = new File(context.getApplicationInfo().dataDir, "fonts"); - - if (!destinationDirectory.exists() && !destinationDirectory.mkdirs()) { - throw new RecoverableDownloadContentException(RecoverableDownloadContentException.DISK_IO, - "Destination directory does not exist and cannot be created"); - } - - return new File(destinationDirectory, content.getFilename()); - } - - // Unrecoverable: We downloaded a file and we don't know what to do with it (Should not happen) - throw new UnrecoverableDownloadContentException("Can't determine destination for kind: " + content.getKind()); - } - - protected boolean verify(File file, String expectedChecksum) - throws RecoverableDownloadContentException, UnrecoverableDownloadContentException { - InputStream inputStream = null; - - try { - inputStream = new BufferedInputStream(new FileInputStream(file)); - - byte[] ctx = NativeCrypto.sha256init(); - if (ctx == null) { - throw new RecoverableDownloadContentException(RecoverableDownloadContentException.MEMORY, - "Could not create SHA-256 context"); - } - - byte[] buffer = new byte[4096]; - int read; - - while ((read = inputStream.read(buffer)) != -1) { - NativeCrypto.sha256update(ctx, buffer, read); - } - - String actualChecksum = Utils.byte2Hex(NativeCrypto.sha256finalize(ctx)); - - if (!actualChecksum.equalsIgnoreCase(expectedChecksum)) { - Log.w(LOGTAG, "Checksums do not match. Expected=" + expectedChecksum + ", Actual=" + actualChecksum); - return false; - } - - return true; - } catch (IOException e) { - // Recoverable: Just I/O discontinuation - throw new RecoverableDownloadContentException(RecoverableDownloadContentException.DISK_IO, e); - } finally { - IOUtils.safeStreamClose(inputStream); - } - } - - protected HttpURLConnection buildHttpURLConnection(String url) - throws UnrecoverableDownloadContentException, IOException { - try { - System.setProperty("http.keepAlive", "true"); - - HttpURLConnection connection = (HttpURLConnection) ProxySelector.openConnectionWithProxy(new URI(url)); - connection.setRequestProperty("User-Agent", HardwareUtils.isTablet() ? - AppConstants.USER_AGENT_FENNEC_TABLET : - AppConstants.USER_AGENT_FENNEC_MOBILE); - connection.setRequestMethod("GET"); - connection.setInstanceFollowRedirects(true); - return connection; - } catch (MalformedURLException e) { - throw new UnrecoverableDownloadContentException(e); - } catch (URISyntaxException e) { - throw new UnrecoverableDownloadContentException(e); - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/dlc/CleanupAction.java b/mobile/android/base/java/org/mozilla/gecko/dlc/CleanupAction.java deleted file mode 100644 index e44704c6c1..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/dlc/CleanupAction.java +++ /dev/null @@ -1,49 +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.dlc; - -import android.content.Context; - -import org.mozilla.gecko.dlc.catalog.DownloadContent; -import org.mozilla.gecko.dlc.catalog.DownloadContentCatalog; - -import java.io.File; - -/** - * CleanupAction: Remove content that is no longer needed. - */ -public class CleanupAction extends BaseAction { - @Override - public void perform(Context context, DownloadContentCatalog catalog) { - for (DownloadContent content : catalog.getContentToDelete()) { - if (!content.isAssetArchive()) { - continue; // We do not know how to clean up this content. But this means we didn't - // download it anyways. - } - - try { - File file = getDestinationFile(context, content); - - if (!file.exists()) { - // File does not exist. As good as deleting. - catalog.remove(content); - return; - } - - if (file.delete()) { - // File has been deleted. Now remove it from the catalog. - catalog.remove(content); - } - } catch (UnrecoverableDownloadContentException e) { - // We can't recover. Pretend the content is removed. It probably never existed in - // the first place. - catalog.remove(content); - } catch (RecoverableDownloadContentException e) { - // Try again next time. - } - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/dlc/DownloadAction.java b/mobile/android/base/java/org/mozilla/gecko/dlc/DownloadAction.java deleted file mode 100644 index 8618d4699c..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/dlc/DownloadAction.java +++ /dev/null @@ -1,325 +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.dlc; - -import android.content.Context; -import android.net.ConnectivityManager; -import android.net.NetworkInfo; -import android.support.v4.net.ConnectivityManagerCompat; -import android.util.Log; - -import org.mozilla.gecko.AppConstants; -import org.mozilla.gecko.dlc.catalog.DownloadContent; -import org.mozilla.gecko.dlc.catalog.DownloadContentCatalog; -import org.mozilla.gecko.util.HardwareUtils; -import org.mozilla.gecko.util.IOUtils; - -import java.io.BufferedInputStream; -import java.io.BufferedOutputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.net.HttpURLConnection; -import java.net.MalformedURLException; -import java.net.URL; -import java.util.zip.GZIPInputStream; - -/** - * Download content that has been scheduled during "study" or "verify". - */ -public class DownloadAction extends BaseAction { - private static final String LOGTAG = "DLCDownloadAction"; - - private static final String CACHE_DIRECTORY = "downloadContent"; - - private static final String CDN_BASE_URL = "https://fennec-catalog.cdn.mozilla.net/"; - - public interface Callback { - void onContentDownloaded(DownloadContent content); - } - - private Callback callback; - - public DownloadAction(Callback callback) { - this.callback = callback; - } - - public void perform(Context context, DownloadContentCatalog catalog) { - Log.d(LOGTAG, "Downloading content.."); - - if (!isConnectedToNetwork(context)) { - Log.d(LOGTAG, "No connected network available. Postponing download."); - // TODO: Reschedule download (bug 1209498) - return; - } - - if (isActiveNetworkMetered(context)) { - Log.d(LOGTAG, "Network is metered. Postponing download."); - // TODO: Reschedule download (bug 1209498) - return; - } - - for (DownloadContent content : catalog.getScheduledDownloads()) { - Log.d(LOGTAG, "Downloading: " + content); - - File temporaryFile = null; - - try { - File destinationFile = getDestinationFile(context, content); - if (destinationFile.exists() && verify(destinationFile, content.getChecksum())) { - Log.d(LOGTAG, "Content already exists and is up-to-date."); - catalog.markAsDownloaded(content); - continue; - } - - temporaryFile = createTemporaryFile(context, content); - - if (!hasEnoughDiskSpace(content, destinationFile, temporaryFile)) { - Log.d(LOGTAG, "Not enough disk space to save content. Skipping download."); - continue; - } - - // TODO: Check space on disk before downloading content (bug 1220145) - final String url = createDownloadURL(content); - - if (!temporaryFile.exists() || temporaryFile.length() < content.getSize()) { - download(url, temporaryFile); - } - - if (!verify(temporaryFile, content.getDownloadChecksum())) { - Log.w(LOGTAG, "Wrong checksum after download, content=" + content.getId()); - temporaryFile.delete(); - continue; - } - - if (!content.isAssetArchive()) { - Log.e(LOGTAG, "Downloaded content is not of type 'asset-archive': " + content.getType()); - temporaryFile.delete(); - continue; - } - - extract(temporaryFile, destinationFile, content.getChecksum()); - - catalog.markAsDownloaded(content); - - Log.d(LOGTAG, "Successfully downloaded: " + content); - - if (callback != null) { - callback.onContentDownloaded(content); - } - - if (temporaryFile != null && temporaryFile.exists()) { - temporaryFile.delete(); - } - } catch (RecoverableDownloadContentException e) { - Log.w(LOGTAG, "Downloading content failed (Recoverable): " + content, e); - - if (e.shouldBeCountedAsFailure()) { - catalog.rememberFailure(content, e.getErrorType()); - } - - // TODO: Reschedule download (bug 1209498) - } catch (UnrecoverableDownloadContentException e) { - Log.w(LOGTAG, "Downloading content failed (Unrecoverable): " + content, e); - - catalog.markAsPermanentlyFailed(content); - - if (temporaryFile != null && temporaryFile.exists()) { - temporaryFile.delete(); - } - } - } - - Log.v(LOGTAG, "Done"); - } - - protected void download(String source, File temporaryFile) - throws RecoverableDownloadContentException, UnrecoverableDownloadContentException { - InputStream inputStream = null; - OutputStream outputStream = null; - - HttpURLConnection connection = null; - - try { - connection = buildHttpURLConnection(source); - - final long offset = temporaryFile.exists() ? temporaryFile.length() : 0; - if (offset > 0) { - connection.setRequestProperty("Range", "bytes=" + offset + "-"); - } - - final int status = connection.getResponseCode(); - if (status != HttpURLConnection.HTTP_OK && status != HttpURLConnection.HTTP_PARTIAL) { - // We are trying to be smart and only retry if this is an error that might resolve in the future. - // TODO: This is guesstimating at best. We want to implement failure counters (Bug 1215106). - if (status >= 500) { - // Recoverable: Server errors 5xx - throw new RecoverableDownloadContentException(RecoverableDownloadContentException.SERVER, - "(Recoverable) Download failed. Status code: " + status); - } else if (status >= 400) { - // Unrecoverable: Client errors 4xx - Unlikely that this version of the client will ever succeed. - throw new UnrecoverableDownloadContentException("(Unrecoverable) Download failed. Status code: " + status); - } else { - // HttpsUrlConnection: -1 (No valid response code) - // Informational 1xx: They have no meaning to us. - // Successful 2xx: We don't know how to handle anything but 200. - // Redirection 3xx: HttpClient should have followed redirects if possible. We should not see those errors here. - throw new UnrecoverableDownloadContentException("(Unrecoverable) Download failed. Status code: " + status); - } - } - - inputStream = new BufferedInputStream(connection.getInputStream()); - outputStream = openFile(temporaryFile, status == HttpURLConnection.HTTP_PARTIAL); - - IOUtils.copy(inputStream, outputStream); - - inputStream.close(); - outputStream.close(); - } catch (IOException e) { - // Recoverable: Just I/O discontinuation - throw new RecoverableDownloadContentException(RecoverableDownloadContentException.NETWORK, e); - } finally { - IOUtils.safeStreamClose(inputStream); - IOUtils.safeStreamClose(outputStream); - - if (connection != null) { - connection.disconnect(); - } - } - } - - protected OutputStream openFile(File file, boolean append) throws FileNotFoundException { - return new BufferedOutputStream(new FileOutputStream(file, append)); - } - - protected void extract(File sourceFile, File destinationFile, String checksum) - throws UnrecoverableDownloadContentException, RecoverableDownloadContentException { - InputStream inputStream = null; - OutputStream outputStream = null; - File temporaryFile = null; - - try { - File destinationDirectory = destinationFile.getParentFile(); - if (!destinationDirectory.exists() && !destinationDirectory.mkdirs()) { - throw new IOException("Destination directory does not exist and cannot be created"); - } - - temporaryFile = new File(destinationDirectory, destinationFile.getName() + ".tmp"); - - inputStream = new GZIPInputStream(new BufferedInputStream(new FileInputStream(sourceFile))); - outputStream = new BufferedOutputStream(new FileOutputStream(temporaryFile)); - - IOUtils.copy(inputStream, outputStream); - - inputStream.close(); - outputStream.close(); - - if (!verify(temporaryFile, checksum)) { - Log.w(LOGTAG, "Checksum of extracted file does not match."); - return; - } - - move(temporaryFile, destinationFile); - } catch (IOException e) { - // We could not extract to the destination: Keep temporary file and try again next time we run. - throw new RecoverableDownloadContentException(RecoverableDownloadContentException.DISK_IO, e); - } finally { - IOUtils.safeStreamClose(inputStream); - IOUtils.safeStreamClose(outputStream); - - if (temporaryFile != null && temporaryFile.exists()) { - temporaryFile.delete(); - } - } - } - - protected boolean isConnectedToNetwork(Context context) { - ConnectivityManager manager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); - NetworkInfo networkInfo = manager.getActiveNetworkInfo(); - - return networkInfo != null && networkInfo.isConnected(); - } - - protected boolean isActiveNetworkMetered(Context context) { - return ConnectivityManagerCompat.isActiveNetworkMetered( - (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE)); - } - - protected String createDownloadURL(DownloadContent content) { - final String location = content.getLocation(); - - return CDN_BASE_URL + content.getLocation(); - } - - protected File createTemporaryFile(Context context, DownloadContent content) - throws RecoverableDownloadContentException { - File cacheDirectory = new File(context.getCacheDir(), CACHE_DIRECTORY); - - if (!cacheDirectory.exists() && !cacheDirectory.mkdirs()) { - // Recoverable: File system might not be mounted NOW and we didn't download anything yet anyways. - throw new RecoverableDownloadContentException(RecoverableDownloadContentException.DISK_IO, - "Could not create cache directory: " + cacheDirectory); - } - - return new File(cacheDirectory, content.getDownloadChecksum() + "-" + content.getId()); - } - - protected void move(File temporaryFile, File destinationFile) - throws RecoverableDownloadContentException, UnrecoverableDownloadContentException { - if (!temporaryFile.renameTo(destinationFile)) { - Log.d(LOGTAG, "Could not move temporary file to destination. Trying to copy.."); - copy(temporaryFile, destinationFile); - temporaryFile.delete(); - } - } - - protected void copy(File temporaryFile, File destinationFile) - throws RecoverableDownloadContentException, UnrecoverableDownloadContentException { - InputStream inputStream = null; - OutputStream outputStream = null; - - try { - File destinationDirectory = destinationFile.getParentFile(); - if (!destinationDirectory.exists() && !destinationDirectory.mkdirs()) { - throw new IOException("Destination directory does not exist and cannot be created"); - } - - inputStream = new BufferedInputStream(new FileInputStream(temporaryFile)); - outputStream = new BufferedOutputStream(new FileOutputStream(destinationFile)); - - IOUtils.copy(inputStream, outputStream); - - inputStream.close(); - outputStream.close(); - } catch (IOException e) { - // We could not copy the temporary file to its destination: Keep the temporary file and - // try again the next time we run. - throw new RecoverableDownloadContentException(RecoverableDownloadContentException.DISK_IO, e); - } finally { - IOUtils.safeStreamClose(inputStream); - IOUtils.safeStreamClose(outputStream); - } - } - - protected boolean hasEnoughDiskSpace(DownloadContent content, File destinationFile, File temporaryFile) { - final File temporaryDirectory = temporaryFile.getParentFile(); - if (temporaryDirectory.getUsableSpace() < content.getSize()) { - return false; - } - - final File destinationDirectory = destinationFile.getParentFile(); - // We need some more space to extract the file (getSize() returns the uncompressed size) - if (destinationDirectory.getUsableSpace() < content.getSize() * 2) { - return false; - } - - return true; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/dlc/DownloadContentService.java b/mobile/android/base/java/org/mozilla/gecko/dlc/DownloadContentService.java deleted file mode 100644 index 3729cf2e06..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/dlc/DownloadContentService.java +++ /dev/null @@ -1,144 +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.dlc; - -import org.mozilla.gecko.AppConstants; -import org.mozilla.gecko.GeckoAppShell; -import org.mozilla.gecko.dlc.catalog.DownloadContent; -import org.mozilla.gecko.dlc.catalog.DownloadContentCatalog; -import org.mozilla.gecko.util.HardwareUtils; - -import android.app.IntentService; -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.util.Log; - -/** - * Service to handle downloadable content that did not ship with the APK. - */ -public class DownloadContentService extends IntentService { - private static final String LOGTAG = "GeckoDLCService"; - - /** - * Study: Scan the catalog for "new" content available for download. - */ - private static final String ACTION_STUDY_CATALOG = AppConstants.ANDROID_PACKAGE_NAME + ".DLC.STUDY"; - - /** - * Verify: Validate downloaded content. Does it still exist and does it have the correct checksum? - */ - private static final String ACTION_VERIFY_CONTENT = AppConstants.ANDROID_PACKAGE_NAME + ".DLC.VERIFY"; - - /** - * Download content that has been scheduled during "study" or "verify". - */ - private static final String ACTION_DOWNLOAD_CONTENT = AppConstants.ANDROID_PACKAGE_NAME + ".DLC.DOWNLOAD"; - - /** - * Sync: Synchronize catalog from a Kinto instance. - */ - private static final String ACTION_SYNCHRONIZE_CATALOG = AppConstants.ANDROID_PACKAGE_NAME + ".DLC.SYNC"; - - /** - * CleanupAction: Remove content that is no longer needed (e.g. Removed from the catalog after a sync). - */ - private static final String ACTION_CLEANUP_FILES = AppConstants.ANDROID_PACKAGE_NAME + ".DLC.CLEANUP"; - - public static void startStudy(Context context) { - Intent intent = new Intent(ACTION_STUDY_CATALOG); - intent.setComponent(new ComponentName(context, DownloadContentService.class)); - context.startService(intent); - } - - public static void startVerification(Context context) { - Intent intent = new Intent(ACTION_VERIFY_CONTENT); - intent.setComponent(new ComponentName(context, DownloadContentService.class)); - context.startService(intent); - } - - public static void startDownloads(Context context) { - Intent intent = new Intent(ACTION_DOWNLOAD_CONTENT); - intent.setComponent(new ComponentName(context, DownloadContentService.class)); - context.startService(intent); - } - - public static void startSync(Context context) { - Intent intent = new Intent(ACTION_SYNCHRONIZE_CATALOG); - intent.setComponent(new ComponentName(context, DownloadContentService.class)); - context.startService(intent); - } - - public static void startCleanup(Context context) { - Intent intent = new Intent(ACTION_CLEANUP_FILES); - intent.setComponent(new ComponentName(context, DownloadContentService.class)); - context.startService(intent); - } - - private DownloadContentCatalog catalog; - - public DownloadContentService() { - super(LOGTAG); - } - - @Override - public void onCreate() { - super.onCreate(); - - catalog = new DownloadContentCatalog(this); - } - - protected void onHandleIntent(Intent intent) { - if (!AppConstants.MOZ_ANDROID_DOWNLOAD_CONTENT_SERVICE) { - Log.w(LOGTAG, "Download content is not enabled. Stop."); - return; - } - - if (!HardwareUtils.isSupportedSystem()) { - // This service is running very early before checks in BrowserApp can prevent us from running. - Log.w(LOGTAG, "System is not supported. Stop."); - return; - } - - if (intent == null) { - return; - } - - final BaseAction action; - - switch (intent.getAction()) { - case ACTION_STUDY_CATALOG: - action = new StudyAction(); - break; - - case ACTION_DOWNLOAD_CONTENT: - action = new DownloadAction(new DownloadAction.Callback() { - @Override - public void onContentDownloaded(DownloadContent content) { - if (content.isFont()) { - GeckoAppShell.notifyObservers("Fonts:Reload", ""); - } - } - }); - break; - - case ACTION_VERIFY_CONTENT: - action = new VerifyAction(); - break; - - case ACTION_SYNCHRONIZE_CATALOG: - action = new SyncAction(); - break; - - default: - Log.e(LOGTAG, "Unknown action: " + intent.getAction()); - return; - } - - action.perform(this, catalog); - catalog.persistChanges(); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/dlc/StudyAction.java b/mobile/android/base/java/org/mozilla/gecko/dlc/StudyAction.java deleted file mode 100644 index e15a17bbee..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/dlc/StudyAction.java +++ /dev/null @@ -1,81 +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.dlc; - -import android.content.Context; -import android.os.Build; -import android.text.TextUtils; -import android.util.Log; - -import org.mozilla.gecko.dlc.catalog.DownloadContent; -import org.mozilla.gecko.dlc.catalog.DownloadContentCatalog; -import org.mozilla.gecko.util.ContextUtils; - -/** - * Study: Scan the catalog for "new" content available for download. - */ -public class StudyAction extends BaseAction { - private static final String LOGTAG = "DLCStudyAction"; - - public void perform(Context context, DownloadContentCatalog catalog) { - Log.d(LOGTAG, "Studying catalog.."); - - for (DownloadContent content : catalog.getContentToStudy()) { - if (!isMatching(context, content)) { - // This content is not for this particular version of the application or system - continue; - } - - if (content.isAssetArchive() && content.isFont()) { - catalog.scheduleDownload(content); - - Log.d(LOGTAG, "Scheduled download: " + content); - } - } - - if (catalog.hasScheduledDownloads()) { - startDownloads(context); - } - - Log.v(LOGTAG, "Done"); - } - - protected boolean isMatching(Context context, DownloadContent content) { - final String androidApiPattern = content.getAndroidApiPattern(); - if (!TextUtils.isEmpty(androidApiPattern)) { - final String apiVersion = String.valueOf(Build.VERSION.SDK_INT); - if (apiVersion.matches(androidApiPattern)) { - Log.d(LOGTAG, String.format("Android API (%s) does not match pattern: %s", apiVersion, androidApiPattern)); - return false; - } - } - - final String appIdPattern = content.getAppIdPattern(); - if (!TextUtils.isEmpty(appIdPattern)) { - final String appId = context.getPackageName(); - if (!appId.matches(appIdPattern)) { - Log.d(LOGTAG, String.format("App ID (%s) does not match pattern: %s", appId, appIdPattern)); - return false; - } - } - - final String appVersionPattern = content.getAppVersionPattern(); - if (!TextUtils.isEmpty(appVersionPattern)) { - final String appVersion = ContextUtils.getCurrentPackageInfo(context).versionName; - if (!appVersion.matches(appVersionPattern)) { - Log.d(LOGTAG, String.format("App version (%s) does not match pattern: %s", appVersion, appVersionPattern)); - return false; - } - } - - // There are no patterns or all patterns have matched. - return true; - } - - protected void startDownloads(Context context) { - DownloadContentService.startDownloads(context); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/dlc/SyncAction.java b/mobile/android/base/java/org/mozilla/gecko/dlc/SyncAction.java deleted file mode 100644 index 104bdad183..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/dlc/SyncAction.java +++ /dev/null @@ -1,263 +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.dlc; - -import android.content.Context; -import android.net.Uri; -import android.util.Log; - -import com.keepsafe.switchboard.SwitchBoard; -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; -import org.mozilla.gecko.dlc.catalog.DownloadContent; -import org.mozilla.gecko.dlc.catalog.DownloadContentBuilder; -import org.mozilla.gecko.dlc.catalog.DownloadContentCatalog; -import org.mozilla.gecko.Experiments; -import org.mozilla.gecko.util.IOUtils; - -import java.io.BufferedInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.net.HttpURLConnection; - -/** - * Sync: Synchronize catalog from a Kinto instance. - */ -public class SyncAction extends BaseAction { - private static final String LOGTAG = "DLCSyncAction"; - - private static final String KINTO_KEY_ID = "id"; - private static final String KINTO_KEY_DELETED = "deleted"; - private static final String KINTO_KEY_DATA = "data"; - private static final String KINTO_KEY_ATTACHMENT = "attachment"; - private static final String KINTO_KEY_ORIGINAL = "original"; - - private static final String KINTO_PARAMETER_SINCE = "_since"; - private static final String KINTO_PARAMETER_FIELDS = "_fields"; - private static final String KINTO_PARAMETER_SORT = "_sort"; - - /** - * Kinto endpoint with online version of downloadable content catalog - * - * Dev instance: - * https://kinto-ota.dev.mozaws.net/v1/buckets/dlc/collections/catalog/records - */ - private static final String CATALOG_ENDPOINT = "https://firefox.settings.services.mozilla.com/v1/buckets/fennec/collections/catalog/records"; - - @Override - public void perform(Context context, DownloadContentCatalog catalog) { - Log.d(LOGTAG, "Synchronizing catalog."); - - if (!isSyncEnabledForClient(context)) { - Log.d(LOGTAG, "Sync is not enabled for client. Skipping."); - return; - } - - boolean cleanupRequired = false; - boolean studyRequired = false; - - try { - long lastModified = catalog.getLastModified(); - - // TODO: Consider using ETag here (Bug 1257459) - JSONArray rawCatalog = fetchRawCatalog(lastModified); - - Log.d(LOGTAG, "Server returned " + rawCatalog.length() + " records (since " + lastModified + ")"); - - for (int i = 0; i < rawCatalog.length(); i++) { - JSONObject object = rawCatalog.getJSONObject(i); - String id = object.getString(KINTO_KEY_ID); - - final boolean isDeleted = object.optBoolean(KINTO_KEY_DELETED, false); - - if (!isDeleted) { - JSONObject attachment = object.getJSONObject(KINTO_KEY_ATTACHMENT); - if (attachment.isNull(KINTO_KEY_ORIGINAL)) - throw new JSONException(String.format("Old Attachment Format")); - } - - DownloadContent existingContent = catalog.getContentById(id); - - if (isDeleted) { - cleanupRequired |= deleteContent(catalog, id); - } else if (existingContent != null) { - studyRequired |= updateContent(catalog, object, existingContent); - } else { - studyRequired |= createContent(catalog, object); - } - } - } catch (UnrecoverableDownloadContentException e) { - Log.e(LOGTAG, "UnrecoverableDownloadContentException", e); - } catch (RecoverableDownloadContentException e) { - Log.e(LOGTAG, "RecoverableDownloadContentException"); - } catch (JSONException e) { - Log.e(LOGTAG, "JSONException", e); - } - - if (studyRequired) { - startStudyAction(context); - } - - if (cleanupRequired) { - startCleanupAction(context); - } - - Log.v(LOGTAG, "Done"); - } - - protected void startStudyAction(Context context) { - DownloadContentService.startStudy(context); - } - - protected void startCleanupAction(Context context) { - DownloadContentService.startCleanup(context); - } - - protected JSONArray fetchRawCatalog(long lastModified) - throws RecoverableDownloadContentException, UnrecoverableDownloadContentException { - HttpURLConnection connection = null; - - try { - Uri.Builder builder = Uri.parse(CATALOG_ENDPOINT).buildUpon(); - - if (lastModified > 0) { - builder.appendQueryParameter(KINTO_PARAMETER_SINCE, String.valueOf(lastModified)); - } - // Only select the fields we are actually going to read. - builder.appendQueryParameter(KINTO_PARAMETER_FIELDS, - "attachment.location,attachment.original.filename,attachment.original.hash,attachment.hash,type,kind,attachment.original.size,match"); - - // We want to process items in the order they have been modified. This is to ensure that - // our last_modified values are correct if we processing is interrupted and not all items - // have been processed. - builder.appendQueryParameter(KINTO_PARAMETER_SORT, "last_modified"); - - connection = buildHttpURLConnection(builder.build().toString()); - - // TODO: Read 'Alert' header and EOL message if existing (Bug 1249248) - - // TODO: Read and use 'Backoff' header if available (Bug 1249251) - - // TODO: Add support for Next-Page header (Bug 1257495) - - final int responseCode = connection.getResponseCode(); - - if (responseCode != HttpURLConnection.HTTP_OK) { - if (responseCode >= 500) { - // A Retry-After header will be added to error responses (>=500), telling the - // client how many seconds it should wait before trying again. - - // TODO: Read and obey value in "Retry-After" header (Bug 1249249) - throw new RecoverableDownloadContentException(RecoverableDownloadContentException.SERVER, "Server error (" + responseCode + ")"); - } else if (responseCode == 410) { - // A 410 Gone error response can be returned if the client version is too old, - // or the service had been replaced with a new and better service using a new - // protocol version. - - // TODO: The server is gone. Stop synchronizing the catalog from this server (Bug 1249248). - throw new UnrecoverableDownloadContentException("Server is gone (410)"); - } else if (responseCode >= 400) { - // If the HTTP status is >=400 the response contains a JSON response. - logErrorResponse(connection); - - // Unrecoverable: Client errors 4xx - Unlikely that this version of the client will ever succeed. - throw new UnrecoverableDownloadContentException("(Unrecoverable) Catalog sync failed. Status code: " + responseCode); - } else if (responseCode < 200) { - // If the HTTP status is <200 the response contains a JSON response. - logErrorResponse(connection); - - throw new RecoverableDownloadContentException(RecoverableDownloadContentException.SERVER, "Response code: " + responseCode); - } else { - // HttpsUrlConnection: -1 (No valid response code) - // Successful 2xx: We don't know how to handle anything but 200. - // Redirection 3xx: We should have followed redirects if possible. We should not see those errors here. - - throw new UnrecoverableDownloadContentException("(Unrecoverable) Download failed. Response code: " + responseCode); - } - } - - return fetchJSONResponse(connection).getJSONArray(KINTO_KEY_DATA); - } catch (JSONException | IOException e) { - throw new RecoverableDownloadContentException(RecoverableDownloadContentException.NETWORK, e); - } finally { - if (connection != null) { - connection.disconnect(); - } - } - } - - private JSONObject fetchJSONResponse(HttpURLConnection connection) throws IOException, JSONException { - InputStream inputStream = null; - - try { - inputStream = new BufferedInputStream(connection.getInputStream()); - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - IOUtils.copy(inputStream, outputStream); - return new JSONObject(outputStream.toString("UTF-8")); - } finally { - IOUtils.safeStreamClose(inputStream); - } - } - - protected boolean updateContent(DownloadContentCatalog catalog, JSONObject object, DownloadContent existingContent) - throws JSONException { - DownloadContent content = existingContent.buildUpon() - .updateFromKinto(object) - .build(); - - if (existingContent.getLastModified() >= content.getLastModified()) { - Log.d(LOGTAG, "Item has not changed: " + content); - return false; - } - - catalog.update(content); - - return true; - } - - protected boolean createContent(DownloadContentCatalog catalog, JSONObject object) throws JSONException { - DownloadContent content = new DownloadContentBuilder() - .updateFromKinto(object) - .build(); - - catalog.add(content); - - return true; - } - - protected boolean deleteContent(DownloadContentCatalog catalog, String id) { - DownloadContent content = catalog.getContentById(id); - if (content == null) { - return false; - } - - catalog.markAsDeleted(content); - - return true; - } - - protected boolean isSyncEnabledForClient(Context context) { - // Sync action is behind a switchboard flag for staged rollout. - return SwitchBoard.isInExperiment(context, Experiments.DOWNLOAD_CONTENT_CATALOG_SYNC); - } - - private void logErrorResponse(HttpURLConnection connection) { - try { - JSONObject error = fetchJSONResponse(connection); - - Log.w(LOGTAG, "Server returned error response:"); - Log.w(LOGTAG, "- Code: " + error.getInt("code")); - Log.w(LOGTAG, "- Errno: " + error.getInt("errno")); - Log.w(LOGTAG, "- Error: " + error.optString("error", "-")); - Log.w(LOGTAG, "- Message: " + error.optString("message", "-")); - Log.w(LOGTAG, "- Info: " + error.optString("info", "-")); - } catch (JSONException | IOException e) { - Log.w(LOGTAG, "Could not fetch error response", e); - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/dlc/VerifyAction.java b/mobile/android/base/java/org/mozilla/gecko/dlc/VerifyAction.java deleted file mode 100644 index e96a62eaea..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/dlc/VerifyAction.java +++ /dev/null @@ -1,63 +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.dlc; - -import android.content.Context; -import android.util.Log; - -import org.mozilla.gecko.dlc.catalog.DownloadContent; -import org.mozilla.gecko.dlc.catalog.DownloadContentCatalog; - -import java.io.File; - -/** - * Verify: Validate downloaded content. Does it still exist and does it have the correct checksum? - */ -public class VerifyAction extends BaseAction { - private static final String LOGTAG = "DLCVerifyAction"; - - @Override - public void perform(Context context, DownloadContentCatalog catalog) { - Log.d(LOGTAG, "Verifying catalog.."); - - for (DownloadContent content : catalog.getDownloadedContent()) { - try { - File destinationFile = getDestinationFile(context, content); - - if (!destinationFile.exists()) { - Log.d(LOGTAG, "Downloaded content does not exist anymore: " + content); - - // This file does not exist even though it is marked as downloaded in the catalog. Scheduling a - // download to fetch it again. - catalog.scheduleDownload(content); - continue; - } - - if (!verify(destinationFile, content.getChecksum())) { - catalog.scheduleDownload(content); - Log.d(LOGTAG, "Wrong checksum. Scheduling download: " + content); - continue; - } - - Log.v(LOGTAG, "Content okay: " + content); - } catch (UnrecoverableDownloadContentException e) { - Log.w(LOGTAG, "Unrecoverable exception while verifying downloaded file", e); - } catch (RecoverableDownloadContentException e) { - // That's okay, we are just verifying already existing content. No log. - } - } - - if (catalog.hasScheduledDownloads()) { - startDownloads(context); - } - - Log.v(LOGTAG, "Done"); - } - - protected void startDownloads(Context context) { - DownloadContentService.startDownloads(context); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/dlc/catalog/DownloadContent.java b/mobile/android/base/java/org/mozilla/gecko/dlc/catalog/DownloadContent.java deleted file mode 100644 index 61f7992ca8..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/dlc/catalog/DownloadContent.java +++ /dev/null @@ -1,189 +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.dlc.catalog; - -import android.support.annotation.IntDef; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.support.annotation.StringDef; - -public class DownloadContent { - @IntDef({STATE_NONE, STATE_SCHEDULED, STATE_DOWNLOADED, STATE_FAILED, STATE_UPDATED, STATE_DELETED}) - public @interface State {} - public static final int STATE_NONE = 0; - public static final int STATE_SCHEDULED = 1; - public static final int STATE_DOWNLOADED = 2; - public static final int STATE_FAILED = 3; // Permanently failed for this version of the content - public static final int STATE_UPDATED = 4; - public static final int STATE_DELETED = 5; - - @StringDef({TYPE_ASSET_ARCHIVE}) - public @interface Type {} - public static final String TYPE_ASSET_ARCHIVE = "asset-archive"; - - @StringDef({KIND_FONT, KIND_HYPHENATION_DICTIONARY}) - public @interface Kind {} - public static final String KIND_FONT = "font"; - public static final String KIND_HYPHENATION_DICTIONARY = "hyphenation"; - - private final String id; - private final String location; - private final String filename; - private final String checksum; - private final String downloadChecksum; - private final long lastModified; - private final String type; - private final String kind; - private final long size; - private final String appVersionPattern; - private final String androidApiPattern; - private final String appIdPattern; - private int state; - private int failures; - private int lastFailureType; - - /* package-private */ DownloadContent(@NonNull String id, @NonNull String location, @NonNull String filename, - @NonNull String checksum, @NonNull String downloadChecksum, @NonNull long lastModified, - @NonNull String type, @NonNull String kind, long size, int failures, int lastFailureType, - @Nullable String appVersionPattern, @Nullable String androidApiPattern, @Nullable String appIdPattern) { - this.id = id; - this.location = location; - this.filename = filename; - this.checksum = checksum; - this.downloadChecksum = downloadChecksum; - this.lastModified = lastModified; - this.type = type; - this.kind = kind; - this.size = size; - this.state = STATE_NONE; - this.failures = failures; - this.lastFailureType = lastFailureType; - this.appVersionPattern = appVersionPattern; - this.androidApiPattern = androidApiPattern; - this.appIdPattern = appIdPattern; - } - - public String getId() { - return id; - } - - /* package-private */ void setState(@State int state) { - this.state = state; - } - - @State - public int getState() { - return state; - } - - public boolean isStateIn(@State int... states) { - for (int state : states) { - if (this.state == state) { - return true; - } - } - - return false; - } - - @Kind - public String getKind() { - return kind; - } - - @Type - public String getType() { - return type; - } - - public String getLocation() { - return location; - } - - public long getLastModified() { - return lastModified; - } - - public String getFilename() { - return filename; - } - - public String getChecksum() { - return checksum; - } - - public String getDownloadChecksum() { - return downloadChecksum; - } - - public long getSize() { - return size; - } - - public boolean isFont() { - return KIND_FONT.equals(kind); - } - - public boolean isHyphenationDictionary() { - return KIND_HYPHENATION_DICTIONARY.equals(kind); - } - - /** - *Checks whether the content to be downloaded is a known content. - *Currently it checks whether the type is "Asset Archive" and is of kind - *"Font" or "Hyphenation Dictionary". - */ - public boolean isKnownContent() { - return ((isFont() || isHyphenationDictionary()) && isAssetArchive()); - } - - public boolean isAssetArchive() { - return TYPE_ASSET_ARCHIVE.equals(type); - } - - /* package-private */ int getFailures() { - return failures; - } - - /* package-private */ int getLastFailureType() { - return lastFailureType; - } - - /* package-private */ void rememberFailure(int failureType) { - if (lastFailureType != failureType) { - lastFailureType = failureType; - failures = 1; - } else { - failures++; - } - } - - /* package-private */ void resetFailures() { - failures = 0; - lastFailureType = 0; - } - - public String getAppVersionPattern() { - return appVersionPattern; - } - - public String getAndroidApiPattern() { - return androidApiPattern; - } - - public String getAppIdPattern() { - return appIdPattern; - } - - public DownloadContentBuilder buildUpon() { - return DownloadContentBuilder.buildUpon(this); - } - - - public String toString() { - return String.format("[%s,%s] %s (%d bytes) %s", getType(), getKind(), getId(), getSize(), getChecksum()); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/dlc/catalog/DownloadContentBootstrap.java b/mobile/android/base/java/org/mozilla/gecko/dlc/catalog/DownloadContentBootstrap.java deleted file mode 100644 index 40c8045730..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/dlc/catalog/DownloadContentBootstrap.java +++ /dev/null @@ -1,161 +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.dlc.catalog; - -import android.support.v4.util.ArrayMap; - -import org.mozilla.gecko.AppConstants; - -import java.util.Arrays; -import java.util.List; - -/* package-private */ class DownloadContentBootstrap { - public static ArrayMap createInitialDownloadContentList() { - if (!AppConstants.MOZ_ANDROID_EXCLUDE_FONTS) { - // We are packaging fonts. There's nothing we want to download; - return new ArrayMap<>(); - } - - List initialList = Arrays.asList( - new DownloadContentBuilder() - .setId("c40929cf-7f4c-fa72-3dc9-12cadf56905d") - .setLocation("fennec/catalog/f63e5f92-793c-4574-a2d7-fbc50242b8cb.gz") - .setFilename("CharisSILCompact-B.ttf") - .setChecksum("699d958b492eda0cc2823535f8567d0393090e3842f6df3c36dbe7239cb80b6d") - .setDownloadChecksum("a9f9b34fed353169a88cc159b8f298cb285cce0b8b0f979c22a7d85de46f0532") - .setSize(1676072) - .setKind("font") - .setType("asset-archive") - .build(), - - new DownloadContentBuilder() - .setId("6d265876-85ed-0917-fdc8-baf583ca2cba") - .setLocation("fennec/catalog/19af6c88-09d9-4d6c-805e-cfebb8699a6c.gz") - .setFilename("CharisSILCompact-BI.ttf") - .setChecksum("82465e747b4f41471dbfd942842b2ee810749217d44b55dbc43623b89f9c7d9b") - .setDownloadChecksum("2be26671039a5e2e4d0360a948b4fa42048171133076a3bb6173d93d4b9cd55b") - .setSize(1667812) - .setKind("font") - .setType("asset-archive") - .build(), - - new DownloadContentBuilder() - .setId("8460dc6d-d129-fd1a-24b6-343dbf6531dd") - .setLocation("fennec/catalog/f35a384a-90ea-41c6-a957-bb1845de97eb.gz") - .setFilename("CharisSILCompact-I.ttf") - .setChecksum("ab3ed6f2a4d4c2095b78227bd33155d7ccd05a879c107a291912640d4d64f767") - .setDownloadChecksum("38a6469041c02624d43dfd41d2dd745e3e3211655e616188f65789a90952a1e9") - .setSize(1693988) - .setKind("font") - .setType("asset-archive") - .build(), - - new DownloadContentBuilder() - .setId("c906275c-3747-fe27-426f-6187526a6f06") - .setLocation("fennec/catalog/8c3bec92-d2df-4789-8c4a-0f523f026d96.gz") - .setFilename("CharisSILCompact-R.ttf") - .setChecksum("4ed509317f1bb441b185ea13bf1c9d19d1a0b396962efa3b5dc3190ad88f2067") - .setDownloadChecksum("7c2ec1f550c2005b75383b878f737266b5f0b1c82679dd886c8bbe30c82e340e") - .setSize(1727656) - .setKind("font") - .setType("asset-archive") - .build(), - - new DownloadContentBuilder() - .setId("ff5deecc-6ecc-d816-bb51-65face460119") - .setLocation("fennec/catalog/ea115d71-e2ac-4609-853e-c978780776b1.gz") - .setFilename("ClearSans-Bold.ttf") - .setChecksum("385d0a293c1714770e198f7c762ab32f7905a0ed9d2993f69d640bd7232b4b70") - .setDownloadChecksum("0d3c22bef90e7096f75b331bb7391de3aa43017e10d61041cd3085816db4919a") - .setSize(140136) - .setKind("font") - .setType("asset-archive") - .build(), - - new DownloadContentBuilder() - .setId("a173d1db-373b-ce42-1335-6b3285cfdebd") - .setLocation("fennec/catalog/0838e513-2d99-4e53-b58f-6b970f6548c6.gz") - .setFilename("ClearSans-BoldItalic.ttf") - .setChecksum("7bce66864e38eecd7c94b6657b9b38c35ebfacf8046bfb1247e08f07fe933198") - .setDownloadChecksum("de0903164dde1ad3768d0bd6dec949871d6ab7be08f573d9d70f38c138a22e37") - .setSize(156124) - .setKind("font") - .setType("asset-archive") - .build(), - - new DownloadContentBuilder() - .setId("e65c66df-0088-940d-ca5c-207c22118c0e") - .setLocation("fennec/catalog/7550fa42-0947-478c-a5f0-5ea1bbb6ba27.gz") - .setFilename("ClearSans-Italic.ttf") - .setChecksum("87c13c5fbae832e4f85c3bd46fcbc175978234f39be5fe51c4937be4cbff3b68") - .setDownloadChecksum("6e323db3115005dd0e96d2422db87a520f9ae426de28a342cd6cc87b55601d87") - .setSize(155672) - .setKind("font") - .setType("asset-archive") - .build(), - - new DownloadContentBuilder() - .setId("25610abb-5dc8-fd75-40e7-990507f010c4") - .setLocation("fennec/catalog/dd9bee7d-d784-476b-a3dd-69af8e516487.gz") - .setFilename("ClearSans-Light.ttf") - .setChecksum("e4885f6188e7a8587f5621c077c6c1f5e8d3739dffc8f4d055c2ba87568c750a") - .setDownloadChecksum("19d4f7c67176e9e254c61420da9c7363d9fe5e6b4bb9d61afa4b3b574280714f") - .setSize(145976) - .setKind("font") - .setType("asset-archive") - .build(), - - new DownloadContentBuilder() - .setId("ffe40339-a096-2262-c3f8-54af75c81fe6") - .setLocation("fennec/catalog/bc5ada8c-8cfc-443d-93d7-dc5f98138a07.gz") - .setFilename("ClearSans-Medium.ttf") - .setChecksum("5d0e0115f3a3ed4be3eda6d7eabb899bb9a361292802e763d53c72e00f629da1") - .setDownloadChecksum("edec86dab3ad2a97561cb41b584670262a48bed008c57bb587ee05ca47fb067f") - .setSize(148892) - .setKind("font") - .setType("asset-archive") - .build(), - - new DownloadContentBuilder() - .setId("139a94be-ac69-0264-c9cc-8f2d071fd29d") - .setLocation("fennec/catalog/0490c768-6178-49c2-af88-9f8769ff3167.gz") - .setFilename("ClearSans-MediumItalic.ttf") - .setChecksum("937dda88b26469306258527d38e42c95e27e7ebb9f05bd1d7c5d706a3c9541d7") - .setDownloadChecksum("34edbd1b325dbffe7791fba8dd2d19852eb3c2fe00cff517ea2161ddc424ee22") - .setSize(155228) - .setKind("font") - .setType("asset-archive") - .build(), - - new DownloadContentBuilder() - .setId("b887012a-01e1-7c94-fdcb-ca44d5b974a2") - .setLocation("fennec/catalog/78205bf8-c668-41b1-b68f-afd54f98713b.gz") - .setFilename("ClearSans-Regular.ttf") - .setChecksum("9b91bbdb95ffa6663da24fdaa8ee06060cd0a4d2dceaf1ffbdda00e04915ee5b") - .setDownloadChecksum("a72f1420b4da1ba9e6797adac34f08e72f94128a85e56542d5e6a8080af5f08a") - .setSize(142572) - .setKind("font") - .setType("asset-archive") - .build(), - - new DownloadContentBuilder() - .setId("c8703652-d317-0356-0bf8-95441a5b2c9b") - .setLocation("fennec/catalog/3570f44f-9440-4aa0-abd0-642eaf2a1aa0.gz") - .setFilename("ClearSans-Thin.ttf") - .setChecksum("07b0db85a3ad99afeb803f0f35631436a7b4c67ac66d0c7f77d26a47357c592a") - .setDownloadChecksum("d9f23fd8687d6743f5c281c33539fb16f163304795039959b8caf159e6d62822") - .setSize(147004) - .setKind("font") - .setType("asset-archive") - .build()); - - ArrayMap content = new ArrayMap<>(); - for (DownloadContent currentContent : initialList) { - content.put(currentContent.getId(), currentContent); - } - return content; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/dlc/catalog/DownloadContentBuilder.java b/mobile/android/base/java/org/mozilla/gecko/dlc/catalog/DownloadContentBuilder.java deleted file mode 100644 index 243e2d4eb2..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/dlc/catalog/DownloadContentBuilder.java +++ /dev/null @@ -1,238 +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.dlc.catalog; - -import android.text.TextUtils; - -import org.json.JSONException; -import org.json.JSONObject; - -public class DownloadContentBuilder { - private static final String LOCAL_KEY_ID = "id"; - private static final String LOCAL_KEY_LOCATION = "location"; - private static final String LOCAL_KEY_FILENAME = "filename"; - private static final String LOCAL_KEY_CHECKSUM = "checksum"; - private static final String LOCAL_KEY_DOWNLOAD_CHECKSUM = "download_checksum"; - private static final String LOCAL_KEY_LAST_MODIFIED = "last_modified"; - private static final String LOCAL_KEY_TYPE = "type"; - private static final String LOCAL_KEY_KIND = "kind"; - private static final String LOCAL_KEY_SIZE = "size"; - private static final String LOCAL_KEY_STATE = "state"; - private static final String LOCAL_KEY_FAILURES = "failures"; - private static final String LOCAL_KEY_LAST_FAILURE_TYPE = "last_failure_type"; - private static final String LOCAL_KEY_PATTERN_APP_ID = "pattern_app_id"; - private static final String LOCAL_KEY_PATTERN_ANDROID_API = "pattern_android_api"; - private static final String LOCAL_KEY_PATTERN_APP_VERSION = "pattern_app_version"; - - private static final String KINTO_KEY_ID = "id"; - private static final String KINTO_KEY_ATTACHMENT = "attachment"; - private static final String KINTO_KEY_ORIGINAL = "original"; - private static final String KINTO_KEY_LOCATION = "location"; - private static final String KINTO_KEY_FILENAME = "filename"; - private static final String KINTO_KEY_HASH = "hash"; - private static final String KINTO_KEY_LAST_MODIFIED = "last_modified"; - private static final String KINTO_KEY_TYPE = "type"; - private static final String KINTO_KEY_KIND = "kind"; - private static final String KINTO_KEY_SIZE = "size"; - private static final String KINTO_KEY_MATCH = "match"; - private static final String KINTO_KEY_APP_ID = "appId"; - private static final String KINTO_KEY_ANDROID_API = "androidApi"; - private static final String KINTO_KEY_APP_VERSION = "appVersion"; - - private String id; - private String location; - private String filename; - private String checksum; - private String downloadChecksum; - private long lastModified; - private String type; - private String kind; - private long size; - private int state; - private int failures; - private int lastFailureType; - private String appVersionPattern; - private String androidApiPattern; - private String appIdPattern; - - public static DownloadContentBuilder buildUpon(DownloadContent content) { - DownloadContentBuilder builder = new DownloadContentBuilder(); - - builder.id = content.getId(); - builder.location = content.getLocation(); - builder.filename = content.getFilename(); - builder.checksum = content.getChecksum(); - builder.downloadChecksum = content.getDownloadChecksum(); - builder.lastModified = content.getLastModified(); - builder.type = content.getType(); - builder.kind = content.getKind(); - builder.size = content.getSize(); - builder.state = content.getState(); - builder.failures = content.getFailures(); - builder.lastFailureType = content.getLastFailureType(); - - return builder; - } - - public static DownloadContent fromJSON(JSONObject object) throws JSONException { - return new DownloadContentBuilder() - .setId(object.getString(LOCAL_KEY_ID)) - .setLocation(object.getString(LOCAL_KEY_LOCATION)) - .setFilename(object.getString(LOCAL_KEY_FILENAME)) - .setChecksum(object.getString(LOCAL_KEY_CHECKSUM)) - .setDownloadChecksum(object.getString(LOCAL_KEY_DOWNLOAD_CHECKSUM)) - .setLastModified(object.getLong(LOCAL_KEY_LAST_MODIFIED)) - .setType(object.getString(LOCAL_KEY_TYPE)) - .setKind(object.getString(LOCAL_KEY_KIND)) - .setSize(object.getLong(LOCAL_KEY_SIZE)) - .setState(object.getInt(LOCAL_KEY_STATE)) - .setFailures(object.optInt(LOCAL_KEY_FAILURES), object.optInt(LOCAL_KEY_LAST_FAILURE_TYPE)) - .setAppVersionPattern(object.optString(LOCAL_KEY_PATTERN_APP_VERSION)) - .setAppIdPattern(object.optString(LOCAL_KEY_PATTERN_APP_ID)) - .setAndroidApiPattern(object.optString(LOCAL_KEY_PATTERN_ANDROID_API)) - .build(); - } - - public static JSONObject toJSON(DownloadContent content) throws JSONException { - final JSONObject object = new JSONObject(); - object.put(LOCAL_KEY_ID, content.getId()); - object.put(LOCAL_KEY_LOCATION, content.getLocation()); - object.put(LOCAL_KEY_FILENAME, content.getFilename()); - object.put(LOCAL_KEY_CHECKSUM, content.getChecksum()); - object.put(LOCAL_KEY_DOWNLOAD_CHECKSUM, content.getDownloadChecksum()); - object.put(LOCAL_KEY_LAST_MODIFIED, content.getLastModified()); - object.put(LOCAL_KEY_TYPE, content.getType()); - object.put(LOCAL_KEY_KIND, content.getKind()); - object.put(LOCAL_KEY_SIZE, content.getSize()); - object.put(LOCAL_KEY_STATE, content.getState()); - object.put(LOCAL_KEY_PATTERN_APP_VERSION, content.getAppVersionPattern()); - object.put(LOCAL_KEY_PATTERN_APP_ID, content.getAppIdPattern()); - object.put(LOCAL_KEY_PATTERN_ANDROID_API, content.getAndroidApiPattern()); - - final int failures = content.getFailures(); - if (failures > 0) { - object.put(LOCAL_KEY_FAILURES, failures); - object.put(LOCAL_KEY_LAST_FAILURE_TYPE, content.getLastFailureType()); - } - - return object; - } - - public DownloadContent build() { - DownloadContent content = new DownloadContent(id, location, filename, checksum, - downloadChecksum, lastModified, type, kind, size, failures, lastFailureType, - appVersionPattern, androidApiPattern, appIdPattern); - content.setState(state); - - return content; - } - - public DownloadContentBuilder setId(String id) { - this.id = id; - return this; - } - - public DownloadContentBuilder setLocation(String location) { - this.location = location; - return this; - } - - public DownloadContentBuilder setFilename(String filename) { - this.filename = filename; - return this; - } - - public DownloadContentBuilder setChecksum(String checksum) { - this.checksum = checksum; - return this; - } - - public DownloadContentBuilder setDownloadChecksum(String downloadChecksum) { - this.downloadChecksum = downloadChecksum; - return this; - } - - public DownloadContentBuilder setLastModified(long lastModified) { - this.lastModified = lastModified; - return this; - } - - public DownloadContentBuilder setType(String type) { - this.type = type; - return this; - } - - public DownloadContentBuilder setKind(String kind) { - this.kind = kind; - return this; - } - - public DownloadContentBuilder setSize(long size) { - this.size = size; - return this; - } - - public DownloadContentBuilder setState(int state) { - this.state = state; - return this; - } - - /* package-private */ DownloadContentBuilder setFailures(int failures, int lastFailureType) { - this.failures = failures; - this.lastFailureType = lastFailureType; - - return this; - } - - public DownloadContentBuilder setAppVersionPattern(String appVersionPattern) { - this.appVersionPattern = appVersionPattern; - return this; - } - - public DownloadContentBuilder setAndroidApiPattern(String androidApiPattern) { - this.androidApiPattern = androidApiPattern; - return this; - } - - public DownloadContentBuilder setAppIdPattern(String appIdPattern) { - this.appIdPattern = appIdPattern; - return this; - } - - public DownloadContentBuilder updateFromKinto(JSONObject object) throws JSONException { - final String objectId = object.getString(KINTO_KEY_ID); - - if (TextUtils.isEmpty(id)) { - // New object without an id yet - id = objectId; - } else if (!id.equals(objectId)) { - throw new JSONException(String.format("Record ids do not match: Expected=%s, Actual=%s", id, objectId)); - } - - setType(object.getString(KINTO_KEY_TYPE)); - setKind(object.getString(KINTO_KEY_KIND)); - setLastModified(object.getLong(KINTO_KEY_LAST_MODIFIED)); - - JSONObject attachment = object.getJSONObject(KINTO_KEY_ATTACHMENT); - JSONObject original = attachment.getJSONObject(KINTO_KEY_ORIGINAL); - - setFilename(original.getString(KINTO_KEY_FILENAME)); - setChecksum(original.getString(KINTO_KEY_HASH)); - setSize(original.getLong(KINTO_KEY_SIZE)); - - setLocation(attachment.getString(KINTO_KEY_LOCATION)); - setDownloadChecksum(attachment.getString(KINTO_KEY_HASH)); - - JSONObject match = object.optJSONObject(KINTO_KEY_MATCH); - if (match != null) { - setAndroidApiPattern(match.optString(KINTO_KEY_ANDROID_API)); - setAppIdPattern(match.optString(KINTO_KEY_APP_ID)); - setAppVersionPattern(match.optString(KINTO_KEY_APP_VERSION)); - } - - return this; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/dlc/catalog/DownloadContentCatalog.java b/mobile/android/base/java/org/mozilla/gecko/dlc/catalog/DownloadContentCatalog.java deleted file mode 100644 index 43ba4e82e0..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/dlc/catalog/DownloadContentCatalog.java +++ /dev/null @@ -1,303 +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.dlc.catalog; - -import android.content.Context; -import android.support.annotation.Nullable; -import android.support.v4.util.ArrayMap; -import android.support.v4.util.AtomicFile; -import android.util.Log; - -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - -import java.io.File; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.UnsupportedEncodingException; -import java.util.ArrayList; -import java.util.List; - -/** - * Catalog of downloadable content (DLC). - * - * Changing elements returned by the catalog should be guarded by the catalog instance to guarantee visibility when - * persisting changes. - */ -public class DownloadContentCatalog { - private static final String LOGTAG = "GeckoDLCCatalog"; - private static final String FILE_NAME = "download_content_catalog"; - - private static final String JSON_KEY_CONTENT = "content"; - - private static final int MAX_FAILURES_UNTIL_PERMANENTLY_FAILED = 10; - - private final AtomicFile file; // Guarded by 'file' - - private ArrayMap content; // Guarded by 'this' - private boolean hasLoadedCatalog; // Guarded by 'this - private boolean hasCatalogChanged; // Guarded by 'this' - - public DownloadContentCatalog(Context context) { - this(new AtomicFile(new File(context.getApplicationInfo().dataDir, FILE_NAME))); - - startLoadFromDisk(); - } - - // For injecting mocked AtomicFile objects during test - protected DownloadContentCatalog(AtomicFile file) { - this.content = new ArrayMap<>(); - this.file = file; - } - - public List getContentToStudy() { - return filterByState(DownloadContent.STATE_NONE, DownloadContent.STATE_UPDATED); - } - - public List getContentToDelete() { - return filterByState(DownloadContent.STATE_DELETED); - } - - public List getDownloadedContent() { - return filterByState(DownloadContent.STATE_DOWNLOADED); - } - - public List getScheduledDownloads() { - return filterByState(DownloadContent.STATE_SCHEDULED); - } - - private synchronized List filterByState(@DownloadContent.State int... filterStates) { - awaitLoadingCatalogLocked(); - - List filteredContent = new ArrayList<>(); - - for (DownloadContent currentContent : content.values()) { - if (currentContent.isStateIn(filterStates)) { - filteredContent.add(currentContent); - } - } - - return filteredContent; - } - - public boolean hasScheduledDownloads() { - return !filterByState(DownloadContent.STATE_SCHEDULED).isEmpty(); - } - - public synchronized void add(DownloadContent newContent) { - awaitLoadingCatalogLocked(); - - content.put(newContent.getId(), newContent); - hasCatalogChanged = true; - } - - public synchronized void update(DownloadContent changedContent) { - awaitLoadingCatalogLocked(); - - if (!content.containsKey(changedContent.getId())) { - Log.w(LOGTAG, "Did not find content with matching id (" + changedContent.getId() + ") to update"); - return; - } - - changedContent.setState(DownloadContent.STATE_UPDATED); - changedContent.resetFailures(); - - content.put(changedContent.getId(), changedContent); - hasCatalogChanged = true; - } - - public synchronized void remove(DownloadContent removedContent) { - awaitLoadingCatalogLocked(); - - if (!content.containsKey(removedContent.getId())) { - Log.w(LOGTAG, "Did not find content with matching id (" + removedContent.getId() + ") to remove"); - return; - } - - content.remove(removedContent.getId()); - } - - @Nullable - public synchronized DownloadContent getContentById(String id) { - return content.get(id); - } - - public synchronized long getLastModified() { - awaitLoadingCatalogLocked(); - - long lastModified = 0; - - for (DownloadContent currentContent : content.values()) { - if (currentContent.getLastModified() > lastModified) { - lastModified = currentContent.getLastModified(); - } - } - - return lastModified; - } - - public synchronized void scheduleDownload(DownloadContent content) { - content.setState(DownloadContent.STATE_SCHEDULED); - hasCatalogChanged = true; - } - - public synchronized void markAsDownloaded(DownloadContent content) { - content.setState(DownloadContent.STATE_DOWNLOADED); - content.resetFailures(); - hasCatalogChanged = true; - } - - public synchronized void markAsPermanentlyFailed(DownloadContent content) { - content.setState(DownloadContent.STATE_FAILED); - hasCatalogChanged = true; - } - - public synchronized void markAsDeleted(DownloadContent content) { - content.setState(DownloadContent.STATE_DELETED); - hasCatalogChanged = true; - } - - public synchronized void rememberFailure(DownloadContent content, int failureType) { - if (content.getFailures() >= MAX_FAILURES_UNTIL_PERMANENTLY_FAILED) { - Log.d(LOGTAG, "Maximum number of failures reached. Marking content has permanently failed."); - - markAsPermanentlyFailed(content); - } else { - content.rememberFailure(failureType); - hasCatalogChanged = true; - } - } - - public void persistChanges() { - new Thread(LOGTAG + "-Persist") { - public void run() { - writeToDisk(); - } - }.start(); - } - - private void startLoadFromDisk() { - new Thread(LOGTAG + "-Load") { - public void run() { - loadFromDisk(); - } - }.start(); - } - - private void awaitLoadingCatalogLocked() { - while (!hasLoadedCatalog) { - try { - Log.v(LOGTAG, "Waiting for catalog to be loaded"); - - wait(); - } catch (InterruptedException e) { - // Ignore - } - } - } - - protected synchronized boolean hasCatalogChanged() { - return hasCatalogChanged; - } - - protected synchronized void loadFromDisk() { - Log.d(LOGTAG, "Loading from disk"); - - if (hasLoadedCatalog) { - return; - } - - ArrayMap loadedContent = new ArrayMap<>(); - - try { - JSONObject catalog; - - synchronized (file) { - catalog = new JSONObject(new String(file.readFully(), "UTF-8")); - } - - JSONArray array = catalog.getJSONArray(JSON_KEY_CONTENT); - for (int i = 0; i < array.length(); i++) { - DownloadContent currentContent = DownloadContentBuilder.fromJSON(array.getJSONObject(i)); - loadedContent.put(currentContent.getId(), currentContent); - } - } catch (FileNotFoundException e) { - Log.d(LOGTAG, "Catalog file does not exist: Bootstrapping initial catalog"); - loadedContent = DownloadContentBootstrap.createInitialDownloadContentList(); - } catch (JSONException e) { - Log.w(LOGTAG, "Unable to parse catalog JSON. Re-creating catalog.", e); - // Catalog seems to be broken. Re-create catalog: - loadedContent = DownloadContentBootstrap.createInitialDownloadContentList(); - hasCatalogChanged = true; // Indicate that we want to persist the new catalog - } catch (NullPointerException e) { - // Bad content can produce an NPE in JSON code -- bug 1300139 - Log.w(LOGTAG, "Unable to parse catalog JSON. Re-creating catalog.", e); - // Catalog seems to be broken. Re-create catalog: - loadedContent = DownloadContentBootstrap.createInitialDownloadContentList(); - hasCatalogChanged = true; // Indicate that we want to persist the new catalog - } catch (UnsupportedEncodingException e) { - AssertionError error = new AssertionError("Should not happen: This device does not speak UTF-8"); - error.initCause(e); - throw error; - } catch (IOException e) { - Log.d(LOGTAG, "Can't read catalog due to IOException", e); - } - - onCatalogLoaded(loadedContent); - - notifyAll(); - - Log.d(LOGTAG, "Loaded " + content.size() + " elements"); - } - - protected void onCatalogLoaded(ArrayMap content) { - this.content = content; - this.hasLoadedCatalog = true; - } - - protected synchronized void writeToDisk() { - if (!hasCatalogChanged) { - Log.v(LOGTAG, "Not persisting: Catalog has not changed"); - return; - } - - Log.d(LOGTAG, "Writing to disk"); - - FileOutputStream outputStream = null; - - synchronized (file) { - try { - outputStream = file.startWrite(); - - JSONArray array = new JSONArray(); - for (DownloadContent currentContent : content.values()) { - array.put(DownloadContentBuilder.toJSON(currentContent)); - } - - JSONObject catalog = new JSONObject(); - catalog.put(JSON_KEY_CONTENT, array); - - outputStream.write(catalog.toString().getBytes("UTF-8")); - - file.finishWrite(outputStream); - - hasCatalogChanged = false; - } catch (UnsupportedEncodingException e) { - AssertionError error = new AssertionError("Should not happen: This device does not speak UTF-8"); - error.initCause(e); - throw error; - } catch (IOException | JSONException e) { - Log.e(LOGTAG, "IOException during writing catalog", e); - - if (outputStream != null) { - file.failWrite(outputStream); - } - } - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/feeds/ContentNotificationsDelegate.java b/mobile/android/base/java/org/mozilla/gecko/feeds/ContentNotificationsDelegate.java deleted file mode 100644 index d317a21eea..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/feeds/ContentNotificationsDelegate.java +++ /dev/null @@ -1,89 +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.feeds; - -import android.content.Intent; -import android.os.Bundle; -import android.support.annotation.NonNull; -import android.support.v4.app.NotificationManagerCompat; - -import org.mozilla.gecko.AppConstants; -import org.mozilla.gecko.BrowserApp; -import org.mozilla.gecko.R; -import org.mozilla.gecko.Telemetry; -import org.mozilla.gecko.TelemetryContract; -import org.mozilla.gecko.delegates.BrowserAppDelegate; -import org.mozilla.gecko.mozglue.SafeIntent; - -import java.util.List; - -/** - * BrowserAppDelegate implementation that takes care of handling intents from content notifications. - */ -public class ContentNotificationsDelegate extends BrowserAppDelegate { - // The application is opened from a content notification - public static final String ACTION_CONTENT_NOTIFICATION = AppConstants.ANDROID_PACKAGE_NAME + ".action.CONTENT_NOTIFICATION"; - - public static final String EXTRA_READ_BUTTON = "read_button"; - public static final String EXTRA_URLS = "urls"; - - private static final String TELEMETRY_EXTRA_CONTENT_UPDATE = "content_update"; - private static final String TELEMETRY_EXTRA_READ_NOW_BUTTON = TELEMETRY_EXTRA_CONTENT_UPDATE + "_read_now"; - - @Override - public void onCreate(BrowserApp browserApp, Bundle savedInstanceState) { - if (savedInstanceState != null) { - // This activity is getting restored: We do not want to handle the URLs in the Intent again. The browser - // will take care of restoring the tabs we already created. - return; - } - - - final Intent unsafeIntent = browserApp.getIntent(); - - // Nothing to do. - if (unsafeIntent == null) { - return; - } - - final SafeIntent intent = new SafeIntent(unsafeIntent); - - if (ACTION_CONTENT_NOTIFICATION.equals(intent.getAction())) { - openURLsFromIntent(browserApp, intent); - } - } - - @Override - public void onNewIntent(BrowserApp browserApp, @NonNull final SafeIntent intent) { - if (ACTION_CONTENT_NOTIFICATION.equals(intent.getAction())) { - openURLsFromIntent(browserApp, intent); - } - } - - private void openURLsFromIntent(BrowserApp browserApp, @NonNull final SafeIntent intent) { - final List urls = intent.getStringArrayListExtra(EXTRA_URLS); - if (urls != null) { - browserApp.openUrls(urls); - } - - Telemetry.startUISession(TelemetryContract.Session.EXPERIMENT, FeedService.getEnabledExperiment(browserApp)); - - Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.INTENT, TELEMETRY_EXTRA_CONTENT_UPDATE); - - if (intent.getBooleanExtra(EXTRA_READ_BUTTON, false)) { - // "READ NOW" button in notification was clicked - Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.NOTIFICATION, TELEMETRY_EXTRA_READ_NOW_BUTTON); - - // Android's "auto cancel" won't remove the notification when an action button is pressed. So we do it ourselves here. - NotificationManagerCompat.from(browserApp).cancel(R.id.websiteContentNotification); - } else { - // Notification was clicked - Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.NOTIFICATION, TELEMETRY_EXTRA_CONTENT_UPDATE); - } - - Telemetry.stopUISession(TelemetryContract.Session.EXPERIMENT, FeedService.getEnabledExperiment(browserApp)); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/feeds/FeedAlarmReceiver.java b/mobile/android/base/java/org/mozilla/gecko/feeds/FeedAlarmReceiver.java deleted file mode 100644 index d943b4f81c..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/feeds/FeedAlarmReceiver.java +++ /dev/null @@ -1,31 +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.feeds; - -import android.content.Context; -import android.content.Intent; -import android.support.v4.content.WakefulBroadcastReceiver; -import android.util.Log; - -/** - * Broadcast receiver that will receive broadcasts from the AlarmManager and start the FeedService - * with the given action. - */ -public class FeedAlarmReceiver extends WakefulBroadcastReceiver { - private static final String LOGTAG = "FeedCheckAction"; - - @Override - public void onReceive(Context context, Intent intent) { - final String action = intent.getAction(); - - Log.d(LOGTAG, "Received alarm with action: " + action); - - final Intent serviceIntent = new Intent(context, FeedService.class); - serviceIntent.setAction(action); - - startWakefulService(context, serviceIntent); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/feeds/FeedFetcher.java b/mobile/android/base/java/org/mozilla/gecko/feeds/FeedFetcher.java deleted file mode 100644 index 76c1b7e309..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/feeds/FeedFetcher.java +++ /dev/null @@ -1,110 +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.feeds; - -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; - -import org.mozilla.gecko.feeds.parser.Feed; -import org.mozilla.gecko.feeds.parser.SimpleFeedParser; -import org.mozilla.gecko.util.IOUtils; - -import java.io.BufferedInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.net.HttpURLConnection; -import java.net.URL; - -import ch.boye.httpclientandroidlib.util.TextUtils; - -/** - * Helper class for fetching and parsing a feed. - */ -public class FeedFetcher { - private static final int CONNECT_TIMEOUT = 15000; - private static final int READ_TIMEOUT = 15000; - - public static class FeedResponse { - public final Feed feed; - public final String etag; - public final String lastModified; - - public FeedResponse(Feed feed, String etag, String lastModified) { - this.feed = feed; - this.etag = etag; - this.lastModified = lastModified; - } - } - - /** - * Fetch and parse a feed from the given URL. Will return null if fetching or parsing failed. - */ - public static FeedResponse fetchAndParseFeed(String url) { - return fetchAndParseFeedIfModified(url, null, null); - } - - /** - * Fetch and parse a feed from the given URL using the given ETag and "Last modified" value. - * - * Will return null if fetching or parsing failed. Will also return null if the feed has not - * changed (ETag / Last-Modified-Since). - * - * @param eTag The ETag from the last fetch or null if no ETag is available (will always fetch feed) - * @param lastModified The "Last modified" header from the last time the feed has been fetch or - * null if no value is available (will always fetch feed) - * @return A FeedResponse or null if no feed could be fetched (error or no new version available) - */ - @Nullable - public static FeedResponse fetchAndParseFeedIfModified(@NonNull String url, @Nullable String eTag, @Nullable String lastModified) { - HttpURLConnection connection = null; - InputStream stream = null; - - try { - connection = (HttpURLConnection) new URL(url).openConnection(); - connection.setInstanceFollowRedirects(true); - connection.setConnectTimeout(CONNECT_TIMEOUT); - connection.setReadTimeout(READ_TIMEOUT); - - if (!TextUtils.isEmpty(eTag)) { - connection.setRequestProperty("If-None-Match", eTag); - } - - if (!TextUtils.isEmpty(lastModified)) { - connection.setRequestProperty("If-Modified-Since", lastModified); - } - - final int statusCode = connection.getResponseCode(); - - if (statusCode != HttpURLConnection.HTTP_OK) { - return null; - } - - String responseEtag = connection.getHeaderField("ETag"); - if (!TextUtils.isEmpty(responseEtag) && responseEtag.startsWith("W/")) { - // Weak ETag, get actual ETag value - responseEtag = responseEtag.substring(2); - } - - final String updatedLastModified = connection.getHeaderField("Last-Modified"); - - stream = new BufferedInputStream(connection.getInputStream()); - - final SimpleFeedParser parser = new SimpleFeedParser(); - final Feed feed = parser.parse(stream); - - return new FeedResponse(feed, responseEtag, updatedLastModified); - } catch (IOException e) { - return null; - } catch (SimpleFeedParser.ParserException e) { - return null; - } finally { - if (connection != null) { - connection.disconnect(); - } - IOUtils.safeStreamClose(stream); - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/feeds/FeedService.java b/mobile/android/base/java/org/mozilla/gecko/feeds/FeedService.java deleted file mode 100644 index 3744862153..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/feeds/FeedService.java +++ /dev/null @@ -1,168 +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.feeds; - -import android.app.IntentService; -import android.content.Context; -import android.content.Intent; -import android.net.ConnectivityManager; -import android.net.NetworkInfo; -import android.support.annotation.Nullable; -import android.support.v4.net.ConnectivityManagerCompat; -import android.util.Log; - -import com.keepsafe.switchboard.SwitchBoard; - -import org.mozilla.gecko.AppConstants; -import org.mozilla.gecko.GeckoProfile; -import org.mozilla.gecko.GeckoSharedPrefs; -import org.mozilla.gecko.db.BrowserDB; -import org.mozilla.gecko.feeds.action.FeedAction; -import org.mozilla.gecko.feeds.action.CheckForUpdatesAction; -import org.mozilla.gecko.feeds.action.EnrollSubscriptionsAction; -import org.mozilla.gecko.feeds.action.SetupAlarmsAction; -import org.mozilla.gecko.feeds.action.SubscribeToFeedAction; -import org.mozilla.gecko.feeds.action.WithdrawSubscriptionsAction; -import org.mozilla.gecko.preferences.GeckoPreferences; -import org.mozilla.gecko.Experiments; - -/** - * Background service for subscribing to and checking website feeds to notify the user about updates. - */ -public class FeedService extends IntentService { - private static final String LOGTAG = "GeckoFeedService"; - - public static final String ACTION_SETUP = AppConstants.ANDROID_PACKAGE_NAME + ".FEEDS.SETUP"; - public static final String ACTION_SUBSCRIBE = AppConstants.ANDROID_PACKAGE_NAME + ".FEEDS.SUBSCRIBE"; - public static final String ACTION_CHECK = AppConstants.ANDROID_PACKAGE_NAME + ".FEEDS.CHECK"; - public static final String ACTION_ENROLL = AppConstants.ANDROID_PACKAGE_NAME + ".FEEDS.ENROLL"; - public static final String ACTION_WITHDRAW = AppConstants.ANDROID_PACKAGE_NAME + ".FEEDS.WITHDRAW"; - - public static void setup(Context context) { - Intent intent = new Intent(context, FeedService.class); - intent.setAction(ACTION_SETUP); - context.startService(intent); - } - - public static void subscribe(Context context, String feedUrl) { - Intent intent = new Intent(context, FeedService.class); - intent.setAction(ACTION_SUBSCRIBE); - intent.putExtra(SubscribeToFeedAction.EXTRA_FEED_URL, feedUrl); - context.startService(intent); - } - - public FeedService() { - super(LOGTAG); - } - - private BrowserDB browserDB; - - @Override - public void onCreate() { - super.onCreate(); - - browserDB = BrowserDB.from(this); - } - - @Override - protected void onHandleIntent(Intent intent) { - try { - if (intent == null) { - return; - } - - Log.d(LOGTAG, "Service started with action: " + intent.getAction()); - - if (!isInExperiment(this)) { - Log.d(LOGTAG, "Not in content notifications experiment. Skipping."); - return; - } - - FeedAction action = createActionForIntent(intent); - if (action == null) { - Log.d(LOGTAG, "No action to process"); - return; - } - - if (action.requiresPreferenceEnabled() && !isPreferenceEnabled()) { - Log.d(LOGTAG, "Preference is disabled. Skipping."); - return; - } - - if (action.requiresNetwork() && !isConnectedToUnmeteredNetwork()) { - // For now just skip if we are not connected or the network is metered. We do not want - // to use precious mobile traffic. - Log.d(LOGTAG, "Not connected to a network or network is metered. Skipping."); - return; - } - - action.perform(browserDB, intent); - } finally { - FeedAlarmReceiver.completeWakefulIntent(intent); - } - - Log.d(LOGTAG, "Done."); - } - - @Nullable - private FeedAction createActionForIntent(Intent intent) { - final Context context = getApplicationContext(); - - switch (intent.getAction()) { - case ACTION_SETUP: - return new SetupAlarmsAction(context); - - case ACTION_SUBSCRIBE: - return new SubscribeToFeedAction(context); - - case ACTION_CHECK: - return new CheckForUpdatesAction(context); - - case ACTION_ENROLL: - return new EnrollSubscriptionsAction(context); - - case ACTION_WITHDRAW: - return new WithdrawSubscriptionsAction(context); - - default: - throw new AssertionError("Unknown action: " + intent.getAction()); - } - } - - private boolean isConnectedToUnmeteredNetwork() { - ConnectivityManager manager = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); - NetworkInfo networkInfo = manager.getActiveNetworkInfo(); - if (networkInfo == null || !networkInfo.isConnected()) { - return false; - } - - return !ConnectivityManagerCompat.isActiveNetworkMetered(manager); - } - - public static boolean isInExperiment(Context context) { - return SwitchBoard.isInExperiment(context, Experiments.CONTENT_NOTIFICATIONS_12HRS) || - SwitchBoard.isInExperiment(context, Experiments.CONTENT_NOTIFICATIONS_5PM) || - SwitchBoard.isInExperiment(context, Experiments.CONTENT_NOTIFICATIONS_8AM); - } - - public static String getEnabledExperiment(Context context) { - String experiment = null; - - if (SwitchBoard.isInExperiment(context, Experiments.CONTENT_NOTIFICATIONS_12HRS)) { - experiment = Experiments.CONTENT_NOTIFICATIONS_12HRS; - } else if (SwitchBoard.isInExperiment(context, Experiments.CONTENT_NOTIFICATIONS_8AM)) { - experiment = Experiments.CONTENT_NOTIFICATIONS_8AM; - } else if (SwitchBoard.isInExperiment(context, Experiments.CONTENT_NOTIFICATIONS_5PM)) { - experiment = Experiments.CONTENT_NOTIFICATIONS_5PM; - } - - return experiment; - } - - private boolean isPreferenceEnabled() { - return GeckoSharedPrefs.forApp(this).getBoolean(GeckoPreferences.PREFS_NOTIFICATIONS_CONTENT, true); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/feeds/action/CheckForUpdatesAction.java b/mobile/android/base/java/org/mozilla/gecko/feeds/action/CheckForUpdatesAction.java deleted file mode 100644 index 09a2b12b6c..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/feeds/action/CheckForUpdatesAction.java +++ /dev/null @@ -1,281 +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.feeds.action; - -import android.app.Notification; -import android.app.PendingIntent; -import android.content.ContentResolver; -import android.content.Context; -import android.content.Intent; -import android.database.Cursor; -import android.support.v4.app.NotificationCompat; -import android.support.v4.app.NotificationManagerCompat; -import android.support.v4.content.ContextCompat; -import android.text.format.DateFormat; - -import org.json.JSONException; -import org.mozilla.gecko.AppConstants; -import org.mozilla.gecko.BrowserApp; -import org.mozilla.gecko.GeckoApp; -import org.mozilla.gecko.R; -import org.mozilla.gecko.Telemetry; -import org.mozilla.gecko.TelemetryContract; -import org.mozilla.gecko.db.BrowserContract; -import org.mozilla.gecko.db.BrowserDB; -import org.mozilla.gecko.db.UrlAnnotations; -import org.mozilla.gecko.feeds.ContentNotificationsDelegate; -import org.mozilla.gecko.feeds.FeedFetcher; -import org.mozilla.gecko.feeds.FeedService; -import org.mozilla.gecko.feeds.parser.Feed; -import org.mozilla.gecko.feeds.subscriptions.FeedSubscription; -import org.mozilla.gecko.preferences.GeckoPreferences; -import org.mozilla.gecko.util.StringUtils; - -import java.util.ArrayList; -import java.util.Date; -import java.util.List; - -/** - * CheckForUpdatesAction: Check if feeds we subscribed to have new content available. - */ -public class CheckForUpdatesAction extends FeedAction { - /** - * This extra will be added to Intents fired by the notification. - */ - public static final String EXTRA_CONTENT_NOTIFICATION = "content-notification"; - - private final Context context; - - public CheckForUpdatesAction(Context context) { - this.context = context; - } - - @Override - public void perform(BrowserDB browserDB, Intent intent) { - final UrlAnnotations urlAnnotations = browserDB.getUrlAnnotations(); - final ContentResolver resolver = context.getContentResolver(); - final List updatedFeeds = new ArrayList<>(); - - log("Checking feeds for updates.."); - - Cursor cursor = urlAnnotations.getFeedSubscriptions(resolver); - if (cursor == null) { - return; - } - - try { - while (cursor.moveToNext()) { - FeedSubscription subscription = FeedSubscription.fromCursor(cursor); - - FeedFetcher.FeedResponse response = checkFeedForUpdates(subscription); - if (response != null) { - final Feed feed = response.feed; - - if (!hasBeenVisited(browserDB, feed.getLastItem().getURL())) { - // Only notify about this update if the last item hasn't been visited yet. - updatedFeeds.add(feed); - } else { - Telemetry.startUISession(TelemetryContract.Session.EXPERIMENT, FeedService.getEnabledExperiment(context)); - Telemetry.sendUIEvent(TelemetryContract.Event.CANCEL, - TelemetryContract.Method.SERVICE, - "content_update"); - Telemetry.stopUISession(TelemetryContract.Session.EXPERIMENT, FeedService.getEnabledExperiment(context)); - } - - urlAnnotations.updateFeedSubscription(resolver, subscription); - } - } - } catch (JSONException e) { - log("Could not deserialize subscription", e); - } finally { - cursor.close(); - } - - showNotification(updatedFeeds); - } - - private FeedFetcher.FeedResponse checkFeedForUpdates(FeedSubscription subscription) { - log("Checking feed: " + subscription.getFeedTitle()); - - FeedFetcher.FeedResponse response = fetchFeed(subscription); - if (response == null) { - return null; - } - - if (subscription.hasBeenUpdated(response)) { - log("* Feed has changed. New item: " + response.feed.getLastItem().getTitle()); - - subscription.update(response); - - return response; - - } - - return null; - } - - /** - * Returns true if this URL has been visited before. - * - * We do an exact match. So this can fail if the feed uses a different URL and redirects to - * content. But it's better than no checks at all. - */ - private boolean hasBeenVisited(final BrowserDB browserDB, final String url) { - final Cursor cursor = browserDB.getHistoryForURL(context.getContentResolver(), url); - if (cursor == null) { - return false; - } - - try { - if (cursor.moveToFirst()) { - return cursor.getInt(cursor.getColumnIndex(BrowserContract.History.VISITS)) > 0; - } - } finally { - cursor.close(); - } - - return false; - } - - private void showNotification(List updatedFeeds) { - final int feedCount = updatedFeeds.size(); - if (feedCount == 0) { - return; - } - - if (feedCount == 1) { - showNotificationForSingleUpdate(updatedFeeds.get(0)); - } else { - showNotificationForMultipleUpdates(updatedFeeds); - } - - Telemetry.startUISession(TelemetryContract.Session.EXPERIMENT, FeedService.getEnabledExperiment(context)); - Telemetry.sendUIEvent(TelemetryContract.Event.SHOW, TelemetryContract.Method.NOTIFICATION, "content_update"); - Telemetry.stopUISession(TelemetryContract.Session.EXPERIMENT, FeedService.getEnabledExperiment(context)); - } - - private void showNotificationForSingleUpdate(Feed feed) { - final String date = DateFormat.getMediumDateFormat(context).format(new Date(feed.getLastItem().getTimestamp())); - - final NotificationCompat.BigTextStyle style = new NotificationCompat.BigTextStyle() - .bigText(feed.getLastItem().getTitle()) - .setBigContentTitle(feed.getTitle()) - .setSummaryText(context.getString(R.string.content_notification_updated_on, date)); - - final PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, createOpenIntent(feed), PendingIntent.FLAG_UPDATE_CURRENT); - - final Notification notification = new NotificationCompat.Builder(context) - .setSmallIcon(R.drawable.ic_status_logo) - .setContentTitle(feed.getTitle()) - .setContentText(feed.getLastItem().getTitle()) - .setStyle(style) - .setColor(ContextCompat.getColor(context, R.color.fennec_ui_orange)) - .setContentIntent(pendingIntent) - .setAutoCancel(true) - .addAction(createOpenAction(feed)) - .addAction(createNotificationSettingsAction()) - .build(); - - NotificationManagerCompat.from(context).notify(R.id.websiteContentNotification, notification); - } - - private void showNotificationForMultipleUpdates(List feeds) { - final NotificationCompat.InboxStyle inboxStyle = new NotificationCompat.InboxStyle(); - for (Feed feed : feeds) { - inboxStyle.addLine(StringUtils.stripScheme(feed.getLastItem().getURL(), StringUtils.UrlFlags.STRIP_HTTPS)); - } - inboxStyle.setSummaryText(context.getString(R.string.content_notification_summary)); - - final PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, createOpenIntent(feeds), PendingIntent.FLAG_UPDATE_CURRENT); - - Notification notification = new NotificationCompat.Builder(context) - .setSmallIcon(R.drawable.ic_status_logo) - .setContentTitle(context.getString(R.string.content_notification_title_plural, feeds.size())) - .setContentText(context.getString(R.string.content_notification_summary)) - .setStyle(inboxStyle) - .setColor(ContextCompat.getColor(context, R.color.fennec_ui_orange)) - .setContentIntent(pendingIntent) - .setAutoCancel(true) - .addAction(createOpenAction(feeds)) - .setNumber(feeds.size()) - .addAction(createNotificationSettingsAction()) - .build(); - - NotificationManagerCompat.from(context).notify(R.id.websiteContentNotification, notification); - } - - private Intent createOpenIntent(Feed feed) { - final List feeds = new ArrayList<>(); - feeds.add(feed); - - return createOpenIntent(feeds); - } - - private Intent createOpenIntent(List feeds) { - final ArrayList urls = new ArrayList<>(); - for (Feed feed : feeds) { - urls.add(feed.getLastItem().getURL()); - } - - final Intent intent = new Intent(context, BrowserApp.class); - intent.setAction(ContentNotificationsDelegate.ACTION_CONTENT_NOTIFICATION); - intent.putStringArrayListExtra(ContentNotificationsDelegate.EXTRA_URLS, urls); - - return intent; - } - - private NotificationCompat.Action createOpenAction(Feed feed) { - final List feeds = new ArrayList<>(); - feeds.add(feed); - - return createOpenAction(feeds); - } - - private NotificationCompat.Action createOpenAction(List feeds) { - Intent intent = createOpenIntent(feeds); - intent.putExtra(ContentNotificationsDelegate.EXTRA_READ_BUTTON, true); - - PendingIntent pendingIntent = PendingIntent.getActivity(context, 1, intent, PendingIntent.FLAG_UPDATE_CURRENT); - - return new NotificationCompat.Action( - R.drawable.open_in_browser, - context.getString(R.string.content_notification_action_read_now), - pendingIntent); - } - - private NotificationCompat.Action createNotificationSettingsAction() { - final Intent intent = new Intent(GeckoApp.ACTION_LAUNCH_SETTINGS); - intent.setClassName(AppConstants.ANDROID_PACKAGE_NAME, AppConstants.MOZ_ANDROID_BROWSER_INTENT_CLASS); - intent.putExtra(EXTRA_CONTENT_NOTIFICATION, true); - - GeckoPreferences.setResourceToOpen(intent, "preferences_notifications"); - - PendingIntent settingsIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); - - return new NotificationCompat.Action( - R.drawable.settings_notifications, - context.getString(R.string.content_notification_action_settings), - settingsIntent); - } - - private FeedFetcher.FeedResponse fetchFeed(FeedSubscription subscription) { - return FeedFetcher.fetchAndParseFeedIfModified( - subscription.getFeedUrl(), - subscription.getETag(), - subscription.getLastModified() - ); - } - - @Override - public boolean requiresNetwork() { - return true; - } - - @Override - public boolean requiresPreferenceEnabled() { - return true; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/feeds/action/EnrollSubscriptionsAction.java b/mobile/android/base/java/org/mozilla/gecko/feeds/action/EnrollSubscriptionsAction.java deleted file mode 100644 index b778938fdd..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/feeds/action/EnrollSubscriptionsAction.java +++ /dev/null @@ -1,101 +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.feeds.action; - -import android.content.ContentResolver; -import android.content.Context; -import android.content.Intent; -import android.database.Cursor; -import android.text.TextUtils; - -import org.mozilla.gecko.db.BrowserContract; -import org.mozilla.gecko.db.BrowserDB; -import org.mozilla.gecko.db.UrlAnnotations; -import org.mozilla.gecko.feeds.FeedService; -import org.mozilla.gecko.feeds.knownsites.KnownSiteBlogger; -import org.mozilla.gecko.feeds.knownsites.KnownSite; -import org.mozilla.gecko.feeds.knownsites.KnownSiteMedium; -import org.mozilla.gecko.feeds.knownsites.KnownSiteTumblr; -import org.mozilla.gecko.feeds.knownsites.KnownSiteWordpress; - -/** - * EnrollSubscriptionsAction: Search for bookmarks of known sites we can subscribe to. - */ -public class EnrollSubscriptionsAction extends FeedAction { - private static final String LOGTAG = "FeedEnrollAction"; - - private static final KnownSite[] knownSites = { - new KnownSiteMedium(), - new KnownSiteBlogger(), - new KnownSiteWordpress(), - new KnownSiteTumblr(), - }; - - private Context context; - - public EnrollSubscriptionsAction(Context context) { - this.context = context; - } - - @Override - public void perform(BrowserDB db, Intent intent) { - log("Searching for bookmarks to enroll in updates"); - - final ContentResolver contentResolver = context.getContentResolver(); - - for (KnownSite knownSite : knownSites) { - searchFor(db, contentResolver, knownSite); - } - } - - @Override - public boolean requiresNetwork() { - return false; - } - - @Override - public boolean requiresPreferenceEnabled() { - return true; - } - - private void searchFor(BrowserDB db, ContentResolver contentResolver, KnownSite knownSite) { - final UrlAnnotations urlAnnotations = db.getUrlAnnotations(); - - final Cursor cursor = db.getBookmarksForPartialUrl(contentResolver, knownSite.getURLSearchString()); - if (cursor == null) { - log("Nothing found (" + knownSite.getClass().getSimpleName() + ")"); - return; - } - - try { - log("Found " + cursor.getCount() + " websites"); - - while (cursor.moveToNext()) { - - final String url = cursor.getString(cursor.getColumnIndex(BrowserContract.Bookmarks.URL)); - - log(" URL: " + url); - - String feedUrl = knownSite.getFeedFromURL(url); - if (TextUtils.isEmpty(feedUrl)) { - log("Could not determine feed for URL: " + url); - return; - } - - if (!urlAnnotations.hasFeedUrlForWebsite(contentResolver, url)) { - urlAnnotations.insertFeedUrl(contentResolver, url, feedUrl); - } - - if (!urlAnnotations.hasFeedSubscription(contentResolver, feedUrl)) { - FeedService.subscribe(context, feedUrl); - } - } - } finally { - cursor.close(); - } - } - -} diff --git a/mobile/android/base/java/org/mozilla/gecko/feeds/action/FeedAction.java b/mobile/android/base/java/org/mozilla/gecko/feeds/action/FeedAction.java deleted file mode 100644 index acfaa8b4d6..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/feeds/action/FeedAction.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.feeds.action; - -import android.content.Intent; -import android.util.Log; - -import org.mozilla.gecko.db.BrowserDB; - -/** - * Interface for actions run by FeedService. - */ -public abstract class FeedAction { - public static final boolean DEBUG_LOG = false; - - /** - * Perform this action. - * - * @param browserDB database instance to perform the action. - * @param intent used to start the service. - */ - public abstract void perform(BrowserDB browserDB, Intent intent); - - /** - * Does this action require an active network connection? - */ - public abstract boolean requiresNetwork(); - - /** - * Should this action only run if the preference is enabled? - */ - public abstract boolean requiresPreferenceEnabled(); - - /** - * This method will swallow all log messages to avoid logging potential personal information. - * - * For debugging purposes set {@code DEBUG_LOG} to true. - */ - public void log(String message) { - if (DEBUG_LOG) { - Log.d("Gecko" + getClass().getSimpleName(), message); - } - } - - /** - * This method will swallow all log messages to avoid logging potential personal information. - * - * For debugging purposes set {@code DEBUG_LOG} to true. - */ - public void log(String message, Throwable throwable) { - if (DEBUG_LOG) { - Log.d("Gecko" + getClass().getSimpleName(), message, throwable); - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/feeds/action/SetupAlarmsAction.java b/mobile/android/base/java/org/mozilla/gecko/feeds/action/SetupAlarmsAction.java deleted file mode 100644 index f5bf39997d..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/feeds/action/SetupAlarmsAction.java +++ /dev/null @@ -1,146 +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.feeds.action; - -import android.app.AlarmManager; -import android.app.PendingIntent; -import android.content.Context; -import android.content.Intent; -import android.os.SystemClock; - -import com.keepsafe.switchboard.SwitchBoard; - -import org.mozilla.gecko.db.BrowserDB; -import org.mozilla.gecko.feeds.FeedAlarmReceiver; -import org.mozilla.gecko.feeds.FeedService; -import org.mozilla.gecko.Experiments; - -import java.text.DateFormat; -import java.util.Calendar; - -/** - * SetupAlarmsAction: Set up alarms to run various actions every now and then. - */ -public class SetupAlarmsAction extends FeedAction { - private static final String LOGTAG = "FeedSetupAction"; - - private Context context; - - public SetupAlarmsAction(Context context) { - this.context = context; - } - - @Override - public void perform(BrowserDB browserDB, Intent intent) { - final AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); - - cancelPreviousAlarms(alarmManager); - scheduleAlarms(alarmManager); - } - - @Override - public boolean requiresNetwork() { - return false; - } - - @Override - public boolean requiresPreferenceEnabled() { - return false; - } - - private void cancelPreviousAlarms(AlarmManager alarmManager) { - final PendingIntent withdrawIntent = getWithdrawPendingIntent(); - alarmManager.cancel(withdrawIntent); - - final PendingIntent enrollIntent = getEnrollPendingIntent(); - alarmManager.cancel(enrollIntent); - - final PendingIntent checkIntent = getCheckPendingIntent(); - alarmManager.cancel(checkIntent); - - log("Cancelled previous alarms"); - } - - private void scheduleAlarms(AlarmManager alarmManager) { - alarmManager.setInexactRepeating( - AlarmManager.ELAPSED_REALTIME, - SystemClock.elapsedRealtime() + AlarmManager.INTERVAL_FIFTEEN_MINUTES, - AlarmManager.INTERVAL_DAY, - getWithdrawPendingIntent()); - - alarmManager.setInexactRepeating( - AlarmManager.ELAPSED_REALTIME, - SystemClock.elapsedRealtime() + AlarmManager.INTERVAL_HALF_HOUR, - AlarmManager.INTERVAL_DAY, - getEnrollPendingIntent() - ); - - if (SwitchBoard.isInExperiment(context, Experiments.CONTENT_NOTIFICATIONS_12HRS)) { - scheduleUpdateCheckEvery12Hours(alarmManager); - } - - if (SwitchBoard.isInExperiment(context, Experiments.CONTENT_NOTIFICATIONS_8AM)) { - scheduleUpdateAtFullHour(alarmManager, 8); - } - - if (SwitchBoard.isInExperiment(context, Experiments.CONTENT_NOTIFICATIONS_5PM)) { - scheduleUpdateAtFullHour(alarmManager, 17); - } - - - log("Scheduled alarms"); - } - - private void scheduleUpdateCheckEvery12Hours(AlarmManager alarmManager) { - alarmManager.setInexactRepeating( - AlarmManager.ELAPSED_REALTIME, - SystemClock.elapsedRealtime() + AlarmManager.INTERVAL_HOUR, - AlarmManager.INTERVAL_HALF_DAY, - getCheckPendingIntent() - ); - } - - private void scheduleUpdateAtFullHour(AlarmManager alarmManager, int hourOfDay) { - final Calendar calendar = Calendar.getInstance(); - - if (calendar.get(Calendar.HOUR_OF_DAY) >= hourOfDay) { - // This time has already passed today. Try again tomorrow. - calendar.add(Calendar.DAY_OF_MONTH, 1); - } - - calendar.set(Calendar.HOUR_OF_DAY, hourOfDay); - calendar.set(Calendar.MINUTE, 0); - calendar.set(Calendar.SECOND, 0); - calendar.set(Calendar.MILLISECOND, 0); - - alarmManager.setInexactRepeating( - AlarmManager.RTC, - calendar.getTimeInMillis(), - AlarmManager.INTERVAL_DAY, - getCheckPendingIntent() - ); - - log("Scheduled update alarm at " + DateFormat.getDateTimeInstance().format(calendar.getTime())); - } - - private PendingIntent getWithdrawPendingIntent() { - Intent intent = new Intent(context, FeedAlarmReceiver.class); - intent.setAction(FeedService.ACTION_WITHDRAW); - return PendingIntent.getBroadcast(context, 0, intent, 0); - } - - private PendingIntent getEnrollPendingIntent() { - Intent intent = new Intent(context, FeedAlarmReceiver.class); - intent.setAction(FeedService.ACTION_ENROLL); - return PendingIntent.getBroadcast(context, 0, intent, 0); - } - - private PendingIntent getCheckPendingIntent() { - Intent intent = new Intent(context, FeedAlarmReceiver.class); - intent.setAction(FeedService.ACTION_CHECK); - return PendingIntent.getBroadcast(context, 0, intent, 0); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/feeds/action/SubscribeToFeedAction.java b/mobile/android/base/java/org/mozilla/gecko/feeds/action/SubscribeToFeedAction.java deleted file mode 100644 index fbfce1af2f..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/feeds/action/SubscribeToFeedAction.java +++ /dev/null @@ -1,79 +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.feeds.action; - -import android.content.Context; -import android.content.Intent; -import android.os.Bundle; - -import org.mozilla.gecko.Telemetry; -import org.mozilla.gecko.TelemetryContract; -import org.mozilla.gecko.db.BrowserDB; -import org.mozilla.gecko.db.UrlAnnotations; -import org.mozilla.gecko.feeds.FeedFetcher; -import org.mozilla.gecko.feeds.FeedService; -import org.mozilla.gecko.feeds.subscriptions.FeedSubscription; - -/** - * SubscribeToFeedAction: Try to fetch a feed and create a subscription if successful. - */ -public class SubscribeToFeedAction extends FeedAction { - private static final String LOGTAG = "FeedSubscribeAction"; - - public static final String EXTRA_FEED_URL = "feed_url"; - - private Context context; - - public SubscribeToFeedAction(Context context) { - this.context = context; - } - - @Override - public void perform(BrowserDB browserDB, Intent intent) { - final UrlAnnotations urlAnnotations = browserDB.getUrlAnnotations(); - - final Bundle extras = intent.getExtras(); - final String feedUrl = extras.getString(EXTRA_FEED_URL); - - if (urlAnnotations.hasFeedSubscription(context.getContentResolver(), feedUrl)) { - log("Already subscribed to " + feedUrl + ". Skipping."); - return; - } - - log("Subscribing to feed: " + feedUrl); - - subscribe(urlAnnotations, feedUrl); - } - - @Override - public boolean requiresNetwork() { - return true; - } - - @Override - public boolean requiresPreferenceEnabled() { - return true; - } - - private void subscribe(UrlAnnotations urlAnnotations, String feedUrl) { - FeedFetcher.FeedResponse response = FeedFetcher.fetchAndParseFeed(feedUrl); - if (response == null) { - log(String.format("Could not fetch feed (%s). Not subscribing for now.", feedUrl)); - return; - } - - log("Subscribing to feed: " + response.feed.getTitle()); - log(" Last item: " + response.feed.getLastItem().getTitle()); - - final FeedSubscription subscription = FeedSubscription.create(feedUrl, response); - - urlAnnotations.insertFeedSubscription(context.getContentResolver(), subscription); - - Telemetry.startUISession(TelemetryContract.Session.EXPERIMENT, FeedService.getEnabledExperiment(context)); - Telemetry.sendUIEvent(TelemetryContract.Event.SAVE, TelemetryContract.Method.SERVICE, "content_update"); - Telemetry.stopUISession(TelemetryContract.Session.EXPERIMENT, FeedService.getEnabledExperiment(context)); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/feeds/action/WithdrawSubscriptionsAction.java b/mobile/android/base/java/org/mozilla/gecko/feeds/action/WithdrawSubscriptionsAction.java deleted file mode 100644 index 6f955c1857..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/feeds/action/WithdrawSubscriptionsAction.java +++ /dev/null @@ -1,109 +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.feeds.action; - -import android.content.ContentResolver; -import android.content.Context; -import android.content.Intent; -import android.database.Cursor; - -import org.json.JSONException; -import org.mozilla.gecko.Telemetry; -import org.mozilla.gecko.TelemetryContract; -import org.mozilla.gecko.db.BrowserContract; -import org.mozilla.gecko.db.BrowserDB; -import org.mozilla.gecko.db.UrlAnnotations; -import org.mozilla.gecko.feeds.FeedService; -import org.mozilla.gecko.feeds.subscriptions.FeedSubscription; - -/** - * WithdrawSubscriptionsAction: Look for feeds to unsubscribe from. - */ -public class WithdrawSubscriptionsAction extends FeedAction { - private static final String LOGTAG = "FeedWithdrawAction"; - - private Context context; - - public WithdrawSubscriptionsAction(Context context) { - this.context = context; - } - - @Override - public void perform(BrowserDB browserDB, Intent intent) { - log("Searching for subscriptions to remove.."); - - final UrlAnnotations urlAnnotations = browserDB.getUrlAnnotations(); - final ContentResolver resolver = context.getContentResolver(); - - removeFeedsOfUnknownUrls(browserDB, urlAnnotations, resolver); - removeSubscriptionsOfRemovedFeeds(urlAnnotations, resolver); - } - - /** - * Search for website URLs with a feed assigned. Remove entry if website URL is not known anymore: - * For now this means the website is not bookmarked. - */ - private void removeFeedsOfUnknownUrls(BrowserDB browserDB, UrlAnnotations urlAnnotations, ContentResolver resolver) { - Cursor cursor = urlAnnotations.getWebsitesWithFeedUrl(resolver); - if (cursor == null) { - return; - } - - try { - while (cursor.moveToNext()) { - final String url = cursor.getString(cursor.getColumnIndex(BrowserContract.UrlAnnotations.URL)); - - if (!browserDB.isBookmark(resolver, url)) { - log("Removing feed for unknown URL: " + url); - - urlAnnotations.deleteFeedUrl(resolver, url); - } - } - } finally { - cursor.close(); - } - } - - /** - * Remove subscriptions of feed URLs that are not assigned to a website URL (anymore). - */ - private void removeSubscriptionsOfRemovedFeeds(UrlAnnotations urlAnnotations, ContentResolver resolver) { - Cursor cursor = urlAnnotations.getFeedSubscriptions(resolver); - if (cursor == null) { - return; - } - - try { - while (cursor.moveToNext()) { - final FeedSubscription subscription = FeedSubscription.fromCursor(cursor); - - if (!urlAnnotations.hasWebsiteForFeedUrl(resolver, subscription.getFeedUrl())) { - log("Removing subscription for feed: " + subscription.getFeedUrl()); - - urlAnnotations.deleteFeedSubscription(resolver, subscription); - - Telemetry.startUISession(TelemetryContract.Session.EXPERIMENT, FeedService.getEnabledExperiment(context)); - Telemetry.sendUIEvent(TelemetryContract.Event.UNSAVE, TelemetryContract.Method.SERVICE, "content_update"); - Telemetry.stopUISession(TelemetryContract.Session.EXPERIMENT, FeedService.getEnabledExperiment(context)); - } - } - } catch (JSONException e) { - log("Could not deserialize subscription", e); - } finally { - cursor.close(); - } - } - - @Override - public boolean requiresNetwork() { - return false; - } - - @Override - public boolean requiresPreferenceEnabled() { - return true; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/feeds/knownsites/KnownSite.java b/mobile/android/base/java/org/mozilla/gecko/feeds/knownsites/KnownSite.java deleted file mode 100644 index febfbb0c7d..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/feeds/knownsites/KnownSite.java +++ /dev/null @@ -1,38 +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.feeds.knownsites; - -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; - -/** - * A site we know and for which we can guess the feed URL from an arbitrary URL. - */ -public interface KnownSite { - /** - * Get a search string to find URLs of this site in our database. This search string is usually - * a partial domain / URL. - * - * For example we could return "medium.com" to find all URLs that contain this string. This could - * obviously find URLs that are not actually medium.com sites. This is acceptable as long as - * getFeedFromURL() can handle these inputs and either returns a feed for valid URLs or null for - * other matches that are not related to this site. - */ - @NonNull String getURLSearchString(); - - /** - * Get the Feed URL for this URL. For a known site we can "guess" the feed URL from an URL - * pointing to any page. The input URL will be a result from the database found with the value - * returned by getURLSearchString(). - * - * Example: - * - Input: https://medium.com/@antlam/ux-thoughts-for-2016-1fc1d6e515e8 - * - Output: https://medium.com/feed/@antlam - * - * @return the url representing a feed, or null if a feed could not be determined. - */ - @Nullable String getFeedFromURL(String url); -} diff --git a/mobile/android/base/java/org/mozilla/gecko/feeds/knownsites/KnownSiteBlogger.java b/mobile/android/base/java/org/mozilla/gecko/feeds/knownsites/KnownSiteBlogger.java deleted file mode 100644 index 6bb3629bf1..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/feeds/knownsites/KnownSiteBlogger.java +++ /dev/null @@ -1,29 +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.feeds.knownsites; - -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -/** - * Blogger.com - */ -public class KnownSiteBlogger implements KnownSite { - @Override - public String getURLSearchString() { - return ".blogspot.com"; - } - - @Override - public String getFeedFromURL(String url) { - Pattern pattern = Pattern.compile("https?://(www\\.)?(.*?)\\.blogspot\\.com(/.*)?"); - Matcher matcher = pattern.matcher(url); - if (matcher.matches()) { - return String.format("https://%s.blogspot.com/feeds/posts/default", matcher.group(2)); - } - return null; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/feeds/knownsites/KnownSiteMedium.java b/mobile/android/base/java/org/mozilla/gecko/feeds/knownsites/KnownSiteMedium.java deleted file mode 100644 index a96e83fcdb..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/feeds/knownsites/KnownSiteMedium.java +++ /dev/null @@ -1,29 +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.feeds.knownsites; - -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -/** - * Medium.com - */ -public class KnownSiteMedium implements KnownSite { - @Override - public String getURLSearchString() { - return "://medium.com/"; - } - - @Override - public String getFeedFromURL(String url) { - Pattern pattern = Pattern.compile("https?://medium.com/([^/]+)(/.*)?"); - Matcher matcher = pattern.matcher(url); - if (matcher.matches()) { - return String.format("https://medium.com/feed/%s", matcher.group(1)); - } - return null; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/feeds/knownsites/KnownSiteTumblr.java b/mobile/android/base/java/org/mozilla/gecko/feeds/knownsites/KnownSiteTumblr.java deleted file mode 100644 index c9f480013c..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/feeds/knownsites/KnownSiteTumblr.java +++ /dev/null @@ -1,33 +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.feeds.knownsites; - -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -/** - * Tumblr.com - */ -public class KnownSiteTumblr implements KnownSite { - @Override - public String getURLSearchString() { - return ".tumblr.com"; - } - - @Override - public String getFeedFromURL(String url) { - final Pattern pattern = Pattern.compile("https?://(.*?).tumblr.com(/.*)?"); - final Matcher matcher = pattern.matcher(url); - if (matcher.matches()) { - final String username = matcher.group(1); - if (username.equals("www")) { - return null; - } - return "http://" + username + ".tumblr.com/rss"; - } - return null; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/feeds/knownsites/KnownSiteWordpress.java b/mobile/android/base/java/org/mozilla/gecko/feeds/knownsites/KnownSiteWordpress.java deleted file mode 100644 index a74b41a749..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/feeds/knownsites/KnownSiteWordpress.java +++ /dev/null @@ -1,26 +0,0 @@ -package org.mozilla.gecko.feeds.knownsites; - -import android.support.annotation.NonNull; - -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -/** - * Wordpress.com - */ -public class KnownSiteWordpress implements KnownSite { - @Override - public String getURLSearchString() { - return ".wordpress.com"; - } - - @Override - public String getFeedFromURL(String url) { - Pattern pattern = Pattern.compile("https?://(.*?).wordpress.com(/.*)?"); - Matcher matcher = pattern.matcher(url); - if (matcher.matches()) { - return "https://" + matcher.group(1) + ".wordpress.com/feed/"; - } - return null; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/feeds/parser/Feed.java b/mobile/android/base/java/org/mozilla/gecko/feeds/parser/Feed.java deleted file mode 100644 index aefc72aa71..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/feeds/parser/Feed.java +++ /dev/null @@ -1,70 +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.feeds.parser; - -import ch.boye.httpclientandroidlib.util.TextUtils; - -public class Feed { - private String title; - private String websiteURL; - private String feedURL; - private Item lastItem; - - public static Feed create(String title, String websiteURL, String feedURL, Item lastItem) { - Feed feed = new Feed(); - - feed.setTitle(title); - feed.setWebsiteURL(websiteURL); - feed.setFeedURL(feedURL); - feed.setLastItem(lastItem); - - return feed; - } - - /* package-private */ Feed() {} - - /* package-private */ void setTitle(String title) { - this.title = title; - } - - /* package-private */ void setWebsiteURL(String websiteURL) { - this.websiteURL = websiteURL; - } - - /* package-private */ void setFeedURL(String feedURL) { - this.feedURL = feedURL; - } - - /* package-private */ void setLastItem(Item lastItem) { - this.lastItem = lastItem; - } - - /** - * Is this feed object sufficiently complete so that we can use it? - */ - /* package-private */ boolean isSufficientlyComplete() { - return !TextUtils.isEmpty(title) && - lastItem != null && - !TextUtils.isEmpty(lastItem.getURL()) && - !TextUtils.isEmpty(lastItem.getTitle()); - } - - public String getTitle() { - return title; - } - - public String getWebsiteURL() { - return websiteURL; - } - - public String getFeedURL() { - return feedURL; - } - - public Item getLastItem() { - return lastItem; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/feeds/parser/Item.java b/mobile/android/base/java/org/mozilla/gecko/feeds/parser/Item.java deleted file mode 100644 index 8d8f6d44ea..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/feeds/parser/Item.java +++ /dev/null @@ -1,49 +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.feeds.parser; - -public class Item { - private String title; - private String url; - private long timestamp; - - public static Item create(String title, String url, long timestamp) { - Item item = new Item(); - - item.setTitle(title); - item.setURL(url); - item.setTimestamp(timestamp); - - return item; - } - - /* package-private */ void setTitle(String title) { - this.title = title; - } - - /* package-private */ void setURL(String url) { - this.url = url; - } - - /* package-private */ void setTimestamp(long timestamp) { - this.timestamp = timestamp; - } - - public String getTitle() { - return title; - } - - public String getURL() { - return url; - } - - /** - * @return the number of milliseconds since Jan. 1, 1970, midnight GMT. - */ - public long getTimestamp() { - return timestamp; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/feeds/parser/SimpleFeedParser.java b/mobile/android/base/java/org/mozilla/gecko/feeds/parser/SimpleFeedParser.java deleted file mode 100644 index afb1b7cb20..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/feeds/parser/SimpleFeedParser.java +++ /dev/null @@ -1,367 +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.feeds.parser; - -import android.util.Log; - -import org.xmlpull.v1.XmlPullParser; -import org.xmlpull.v1.XmlPullParserException; -import org.xmlpull.v1.XmlPullParserFactory; - -import java.io.IOException; -import java.io.InputStream; -import java.text.ParseException; -import java.text.SimpleDateFormat; -import java.util.Date; -import java.util.HashMap; -import java.util.Locale; -import java.util.Map; - -import ch.boye.httpclientandroidlib.util.TextUtils; - -/** - * A super simple feed parser written for implementing "content notifications". This XML Pull Parser - * can read ATOM and RSS feeds and returns an object describing the feed and the latest entry. - */ -public class SimpleFeedParser { - /** - * Generic exception that's thrown by the parser whenever a stream cannot be parsed. - */ - public static class ParserException extends Exception { - private static final long serialVersionUID = -6119538440219805603L; - - public ParserException(Throwable cause) { - super(cause); - } - - public ParserException(String message) { - super(message); - } - } - - private static final String LOGTAG = "Gecko/FeedParser"; - - private static final String TAG_RSS = "rss"; - private static final String TAG_FEED = "feed"; - private static final String TAG_RDF = "RDF"; - private static final String TAG_TITLE = "title"; - private static final String TAG_ITEM = "item"; - private static final String TAG_LINK = "link"; - private static final String TAG_ENTRY = "entry"; - private static final String TAG_PUBDATE = "pubDate"; - private static final String TAG_UPDATED = "updated"; - private static final String TAG_DATE = "date"; - private static final String TAG_SOURCE = "source"; - private static final String TAG_IMAGE = "image"; - private static final String TAG_CONTENT = "content"; - - private class ParserState { - public Feed feed; - public Item currentItem; - public boolean isRSS; - public boolean isATOM; - public boolean inSource; - public boolean inImage; - public boolean inContent; - } - - public Feed parse(InputStream in) throws ParserException, IOException { - final ParserState state = new ParserState(); - - try { - final XmlPullParserFactory factory = XmlPullParserFactory.newInstance(); - factory.setNamespaceAware(true); - - XmlPullParser parser = factory.newPullParser(); - parser.setInput(in, null); - - int eventType = parser.getEventType(); - - while (eventType != XmlPullParser.END_DOCUMENT) { - switch (eventType) { - case XmlPullParser.START_DOCUMENT: - handleStartDocument(state); - break; - - case XmlPullParser.START_TAG: - handleStartTag(parser, state); - break; - - case XmlPullParser.END_TAG: - handleEndTag(parser, state); - break; - } - - eventType = parser.next(); - } - } catch (XmlPullParserException e) { - throw new ParserException(e); - } - - if (!state.feed.isSufficientlyComplete()) { - throw new ParserException("Feed is not sufficiently complete"); - } - - return state.feed; - } - - private void handleStartDocument(ParserState state) { - state.feed = new Feed(); - } - - private void handleStartTag(XmlPullParser parser, ParserState state) throws IOException, XmlPullParserException { - switch (parser.getName()) { - case TAG_RSS: - state.isRSS = true; - break; - - case TAG_FEED: - state.isATOM = true; - break; - - case TAG_RDF: - // This is a RSS 1.0 feed - state.isRSS = true; - break; - - case TAG_ITEM: - case TAG_ENTRY: - state.currentItem = new Item(); - break; - - case TAG_TITLE: - handleTitleStartTag(parser, state); - break; - - case TAG_LINK: - handleLinkStartTag(parser, state); - break; - - case TAG_PUBDATE: - handlePubDateStartTag(parser, state); - break; - - case TAG_UPDATED: - handleUpdatedStartTag(parser, state); - break; - - case TAG_DATE: - handleDateStartTag(parser, state); - break; - - case TAG_SOURCE: - state.inSource = true; - break; - - case TAG_IMAGE: - state.inImage = true; - break; - - case TAG_CONTENT: - state.inContent = true; - break; - } - } - - private void handleEndTag(XmlPullParser parser, ParserState state) { - switch (parser.getName()) { - case TAG_ITEM: - case TAG_ENTRY: - handleItemOrEntryREndTag(state); - break; - - case TAG_SOURCE: - state.inSource = false; - break; - - case TAG_IMAGE: - state.inImage = false; - break; - - case TAG_CONTENT: - state.inContent = false; - break; - } - } - - private void handleTitleStartTag(XmlPullParser parser, ParserState state) throws IOException, XmlPullParserException { - if (state.inSource || state.inImage || state.inContent) { - // We do not care about titles in , or tags. - return; - } - - String title = getTextUntilEndTag(parser, TAG_TITLE); - - title = title.replaceAll("[\r\n]", " "); - title = title.replaceAll(" +", " "); - - if (state.currentItem != null) { - state.currentItem.setTitle(title); - } else { - state.feed.setTitle(title); - } - } - - private void handleLinkStartTag(XmlPullParser parser, ParserState state) throws IOException, XmlPullParserException { - if (state.inSource || state.inImage) { - // We do not care about links in or tags. - return; - } - - Map attributes = fetchAttributes(parser); - - if (attributes.size() > 0) { - String rel = attributes.get("rel"); - - if (state.currentItem == null && "self".equals(rel)) { - state.feed.setFeedURL(attributes.get("href")); - return; - } - - if (rel == null || "alternate".equals(rel)) { - String type = attributes.get("type"); - if (type == null || type.equals("text/html")) { - String link = attributes.get("href"); - if (TextUtils.isEmpty(link)) { - return; - } - - if (state.currentItem != null) { - state.currentItem.setURL(link); - } else { - state.feed.setWebsiteURL(link); - } - - return; - } - } - } - - if (state.isRSS) { - String link = getTextUntilEndTag(parser, TAG_LINK); - if (TextUtils.isEmpty(link)) { - return; - } - - if (state.currentItem != null) { - state.currentItem.setURL(link); - } else { - state.feed.setWebsiteURL(link); - } - } - } - - private void handleItemOrEntryREndTag(ParserState state) { - if (state.feed.getLastItem() == null || state.feed.getLastItem().getTimestamp() < state.currentItem.getTimestamp()) { - // Only set this item as "last item" if we do not have an item yet or this item is newer. - state.feed.setLastItem(state.currentItem); - } - - state.currentItem = null; - } - - private void handlePubDateStartTag(XmlPullParser parser, ParserState state) throws IOException, XmlPullParserException { - if (state.currentItem == null) { - return; - } - - String pubDate = getTextUntilEndTag(parser, TAG_PUBDATE); - if (TextUtils.isEmpty(pubDate)) { - return; - } - - // RFC-822 - SimpleDateFormat format = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss Z", Locale.US); - - updateCurrentItemTimestamp(state, pubDate, format); - } - - private void handleUpdatedStartTag(XmlPullParser parser, ParserState state) throws IOException, XmlPullParserException { - if (state.inSource) { - // We do not care about stuff in tags. - return; - } - - if (state.currentItem == null) { - // We are only interested in values of feed items. - return; - } - - String updated = getTextUntilEndTag(parser, TAG_UPDATED); - if (TextUtils.isEmpty(updated)) { - return; - } - - SimpleDateFormat[] formats = new SimpleDateFormat[] { - new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.US), - new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ", Locale.US) - }; - - // Fix timezones SimpleDateFormat can't parse: - // 2016-01-26T18:56:54Z -> 2016-01-26T18:56:54+0000 (Timezone: Z -> +0000) - updated = updated.replaceFirst("Z$", "+0000"); - // 2016-01-26T18:56:54+01:00 -> 2016-01-26T18:56:54+0100 (Timezone: +01:00 -> +0100) - updated = updated.replaceFirst("([0-9]{2})([\\+\\-])([0-9]{2}):([0-9]{2})$", "$1$2$3$4"); - - updateCurrentItemTimestamp(state, updated, formats); - } - - private void handleDateStartTag(XmlPullParser parser, ParserState state) throws IOException, XmlPullParserException { - if (state.currentItem == null) { - // We are only interested in values of feed items. - return; - } - - String text = getTextUntilEndTag(parser, TAG_DATE); - if (TextUtils.isEmpty(text)) { - return; - } - - // Fix timezones SimpleDateFormat can't parse: - // 2016-01-26T18:56:54+00:00 -> 2016-01-26T18:56:54+0000 - text = text.replaceFirst("([0-9]{2})([\\+\\-])([0-9]{2}):([0-9]{2})$", "$1$2$3$4"); - - SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.US); - - updateCurrentItemTimestamp(state, text, format); - } - - private void updateCurrentItemTimestamp(ParserState state, String text, SimpleDateFormat... formats) { - for (SimpleDateFormat format : formats) { - try { - Date date = format.parse(text); - state.currentItem.setTimestamp(date.getTime()); - return; - } catch (ParseException e) { - Log.w(LOGTAG, "Could not parse 'updated': " + text); - } - } - } - - private Map fetchAttributes(XmlPullParser parser) { - Map attributes = new HashMap<>(); - - for (int i = 0; i < parser.getAttributeCount(); i++) { - attributes.put(parser.getAttributeName(i), parser.getAttributeValue(i)); - } - - return attributes; - } - - private String getTextUntilEndTag(XmlPullParser parser, String tag) throws IOException, XmlPullParserException { - StringBuilder builder = new StringBuilder(); - - while (parser.next() != XmlPullParser.END_DOCUMENT) { - if (parser.getEventType() == XmlPullParser.TEXT) { - builder.append(parser.getText()); - } else if (parser.getEventType() == XmlPullParser.END_TAG && tag.equals(parser.getName())) { - break; - } - } - - return builder.toString().trim(); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/feeds/subscriptions/FeedSubscription.java b/mobile/android/base/java/org/mozilla/gecko/feeds/subscriptions/FeedSubscription.java deleted file mode 100644 index 7ce7f193fd..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/feeds/subscriptions/FeedSubscription.java +++ /dev/null @@ -1,130 +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.feeds.subscriptions; - -import android.database.Cursor; -import android.text.TextUtils; - -import org.json.JSONException; -import org.json.JSONObject; -import org.mozilla.gecko.db.BrowserContract; -import org.mozilla.gecko.feeds.FeedFetcher; -import org.mozilla.gecko.feeds.parser.Item; - -/** - * An object describing a subscription and containing some meta data about the last time we fetched - * the feed. - */ -public class FeedSubscription { - private static final String JSON_KEY_FEED_TITLE = "feed_title"; - private static final String JSON_KEY_LAST_ITEM_TITLE = "last_item_title"; - private static final String JSON_KEY_LAST_ITEM_URL = "last_item_url"; - private static final String JSON_KEY_LAST_ITEM_TIMESTAMP = "last_item_timestamp"; - private static final String JSON_KEY_ETAG = "etag"; - private static final String JSON_KEY_LAST_MODIFIED = "last_modified"; - - private String feedUrl; - private String feedTitle; - private String lastItemTitle; - private String lastItemUrl; - private long lastItemTimestamp; - private String etag; - private String lastModified; - - public static FeedSubscription create(String feedUrl, FeedFetcher.FeedResponse response) { - FeedSubscription subscription = new FeedSubscription(); - subscription.feedUrl = feedUrl; - - subscription.update(response); - - return subscription; - } - - public static FeedSubscription fromCursor(Cursor cursor) throws JSONException { - final FeedSubscription subscription = new FeedSubscription(); - subscription.feedUrl = cursor.getString(cursor.getColumnIndex(BrowserContract.UrlAnnotations.URL)); - - final String value = cursor.getString(cursor.getColumnIndex(BrowserContract.UrlAnnotations.VALUE)); - subscription.fromJSON(new JSONObject(value)); - - return subscription; - } - - private void fromJSON(JSONObject object) throws JSONException { - feedTitle = object.getString(JSON_KEY_FEED_TITLE); - lastItemTitle = object.getString(JSON_KEY_LAST_ITEM_TITLE); - lastItemUrl = object.getString(JSON_KEY_LAST_ITEM_URL); - lastItemTimestamp = object.getLong(JSON_KEY_LAST_ITEM_TIMESTAMP); - etag = object.optString(JSON_KEY_ETAG); - lastModified = object.optString(JSON_KEY_LAST_MODIFIED); - } - - public void update(FeedFetcher.FeedResponse response) { - feedTitle = response.feed.getTitle(); - lastItemTitle = response.feed.getLastItem().getTitle(); - lastItemUrl = response.feed.getLastItem().getURL(); - lastItemTimestamp = response.feed.getLastItem().getTimestamp(); - etag = response.etag; - lastModified = response.lastModified; - } - - /** - * Guesstimate if this response is a newer representation of the feed. - */ - public boolean hasBeenUpdated(FeedFetcher.FeedResponse response) { - final Item responseItem = response.feed.getLastItem(); - - if (responseItem.getTimestamp() > lastItemTimestamp) { - // The timestamp is from a newer date so we expect that this item is a new item. But this - // could also mean that the timestamp of an already existing item has been updated. We - // accept that and assume that the content will have changed too in this case. - return true; - } - - if (responseItem.getTimestamp() == lastItemTimestamp && responseItem.getTimestamp() != 0) { - // We have a timestamp that is not zero and this item has still the timestamp: It's very - // likely that we are looking at the same item. We assume this is not new content. - return false; - } - - if (!responseItem.getURL().equals(lastItemUrl)) { - // The URL changed: It is very likely that this is a new item. At least it has been updated - // in a way that we just treat it as new content here. - return true; - } - - return false; - } - - public String getFeedUrl() { - return feedUrl; - } - - public String getFeedTitle() { - return feedTitle; - } - - public String getETag() { - return etag; - } - - public String getLastModified() { - return lastModified; - } - - public JSONObject toJSON() throws JSONException { - JSONObject object = new JSONObject(); - - object.put(JSON_KEY_FEED_TITLE, feedTitle); - object.put(JSON_KEY_LAST_ITEM_TITLE, lastItemTitle); - object.put(JSON_KEY_LAST_ITEM_URL, lastItemUrl); - object.put(JSON_KEY_LAST_ITEM_TIMESTAMP, lastItemTimestamp); - object.put(JSON_KEY_ETAG, etag); - object.put(JSON_KEY_LAST_MODIFIED, lastModified); - - return object; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/firstrun/DataPanel.java b/mobile/android/base/java/org/mozilla/gecko/firstrun/DataPanel.java deleted file mode 100644 index d5940d7581..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/firstrun/DataPanel.java +++ /dev/null @@ -1,47 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.firstrun; - -import android.os.Bundle; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ImageView; -import org.mozilla.gecko.GeckoSharedPrefs; -import org.mozilla.gecko.PrefsHelper; -import org.mozilla.gecko.R; -import org.mozilla.gecko.Telemetry; -import org.mozilla.gecko.TelemetryContract; - -public class DataPanel extends FirstrunPanel { - private boolean isEnabled = false; - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstance) { - final View root = super.onCreateView(inflater, container, savedInstance); - final ImageView clickableImage = (ImageView) root.findViewById(R.id.firstrun_image); - clickableImage.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - // Set new state. - isEnabled = !isEnabled; - int newResource = isEnabled ? R.drawable.firstrun_data_on : R.drawable.firstrun_data_off; - ((ImageView) view).setImageResource(newResource); - if (isEnabled) { - // Always block images. - PrefsHelper.setPref("browser.image_blocking", 0); - } else { - // Default: always load images. - PrefsHelper.setPref("browser.image_blocking", 1); - } - Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.BUTTON, "firstrun-datasaving-" + isEnabled); - } - }); - - return root; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/firstrun/FirstrunAnimationContainer.java b/mobile/android/base/java/org/mozilla/gecko/firstrun/FirstrunAnimationContainer.java deleted file mode 100644 index 93dd0c254e..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/firstrun/FirstrunAnimationContainer.java +++ /dev/null @@ -1,94 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.firstrun; - -import android.content.Context; -import android.support.v4.app.FragmentManager; -import android.util.AttributeSet; - -import android.view.View; -import android.widget.LinearLayout; -import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; -import android.animation.ObjectAnimator; -import org.mozilla.gecko.R; -import org.mozilla.gecko.Telemetry; -import org.mozilla.gecko.TelemetryContract; -import org.mozilla.gecko.Experiments; - -/** - * A container for the pager and the entire first run experience. - * This is used for animation purposes. - */ -public class FirstrunAnimationContainer extends LinearLayout { - public static final String PREF_FIRSTRUN_ENABLED = "startpane_enabled"; - - public static interface OnFinishListener { - public void onFinish(); - } - - private FirstrunPager pager; - private boolean visible; - private OnFinishListener onFinishListener; - - public FirstrunAnimationContainer(Context context) { - this(context, null); - } - public FirstrunAnimationContainer(Context context, AttributeSet attrs) { - super(context, attrs); - } - - public void load(Context appContext, FragmentManager fm) { - visible = true; - pager = (FirstrunPager) findViewById(R.id.firstrun_pager); - pager.load(appContext, fm, new OnFinishListener() { - @Override - public void onFinish() { - hide(); - } - }); - } - - public boolean isVisible() { - return visible; - } - - public void hide() { - visible = false; - if (onFinishListener != null) { - onFinishListener.onFinish(); - } - animateHide(); - - // Stop all versions of firstrun A/B sessions. - Telemetry.stopUISession(TelemetryContract.Session.EXPERIMENT, Experiments.ONBOARDING3_B); - Telemetry.stopUISession(TelemetryContract.Session.EXPERIMENT, Experiments.ONBOARDING3_C); - } - - private void animateHide() { - final Animator alphaAnimator = ObjectAnimator.ofFloat(this, "alpha", 0); - alphaAnimator.setDuration(150); - alphaAnimator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - FirstrunAnimationContainer.this.setVisibility(View.GONE); - } - }); - - alphaAnimator.start(); - } - - public boolean showBrowserHint() { - final int currentPage = pager.getCurrentItem(); - FirstrunPanel currentPanel = (FirstrunPanel) ((FirstrunPager.ViewPagerAdapter) pager.getAdapter()).getItem(currentPage); - pager.cleanup(); - return currentPanel.shouldShowBrowserHint(); - } - - public void registerOnFinishListener(OnFinishListener listener) { - this.onFinishListener = listener; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/firstrun/FirstrunPager.java b/mobile/android/base/java/org/mozilla/gecko/firstrun/FirstrunPager.java deleted file mode 100644 index c2838ee3e0..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/firstrun/FirstrunPager.java +++ /dev/null @@ -1,174 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.firstrun; - -import android.content.Context; -import android.support.v4.app.Fragment; -import android.support.v4.app.FragmentManager; -import android.support.v4.app.FragmentPagerAdapter; -import android.support.v4.view.ViewPager; -import android.util.AttributeSet; -import android.view.View; -import android.view.ViewGroup; -import android.animation.Animator; -import android.animation.AnimatorSet; -import android.animation.ObjectAnimator; - -import org.mozilla.gecko.Telemetry; -import org.mozilla.gecko.TelemetryContract; -import org.mozilla.gecko.home.HomePager.Decor; -import org.mozilla.gecko.home.TabMenuStrip; -import org.mozilla.gecko.restrictions.Restrictions; - -import java.util.List; - -/** - * ViewPager containing for our first run pages. - * - * @see FirstrunPanel for the first run pages that are used in this pager. - */ -public class FirstrunPager extends ViewPager { - - private Context context; - protected FirstrunPanel.PagerNavigation pagerNavigation; - private Decor mDecor; - - public FirstrunPager(Context context) { - this(context, null); - } - - public FirstrunPager(Context context, AttributeSet attrs) { - super(context, attrs); - this.context = context; - } - - @Override - public void addView(View child, int index, ViewGroup.LayoutParams params) { - if (child instanceof Decor) { - ((ViewPager.LayoutParams) params).isDecor = true; - mDecor = (Decor) child; - mDecor.setOnTitleClickListener(new TabMenuStrip.OnTitleClickListener() { - @Override - public void onTitleClicked(int index) { - setCurrentItem(index, true); - } - }); - } - - super.addView(child, index, params); - } - - public void load(Context appContext, FragmentManager fm, final FirstrunAnimationContainer.OnFinishListener onFinishListener) { - final List panels; - - if (Restrictions.isRestrictedProfile(context)) { - panels = FirstrunPagerConfig.getRestricted(); - } else { - panels = FirstrunPagerConfig.getDefault(appContext); - } - - setAdapter(new ViewPagerAdapter(fm, panels)); - this.pagerNavigation = new FirstrunPanel.PagerNavigation() { - @Override - public void next() { - final int currentPage = FirstrunPager.this.getCurrentItem(); - if (currentPage < FirstrunPager.this.getAdapter().getCount() - 1) { - FirstrunPager.this.setCurrentItem(currentPage + 1); - } - } - - @Override - public void finish() { - if (onFinishListener != null) { - onFinishListener.onFinish(); - } - } - }; - addOnPageChangeListener(new OnPageChangeListener() { - @Override - public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { - mDecor.onPageScrolled(position, positionOffset, positionOffsetPixels); - } - - @Override - public void onPageSelected(int i) { - mDecor.onPageSelected(i); - Telemetry.sendUIEvent(TelemetryContract.Event.SHOW, TelemetryContract.Method.PANEL, "onboarding." + i); - } - - @Override - public void onPageScrollStateChanged(int i) {} - }); - - animateLoad(); - - // Record telemetry for first onboarding panel, for baseline. - Telemetry.sendUIEvent(TelemetryContract.Event.SHOW, TelemetryContract.Method.PANEL, "onboarding.0"); - } - - public void cleanup() { - setAdapter(null); - } - - private void animateLoad() { - setTranslationY(500); - setAlpha(0); - - final Animator translateAnimator = ObjectAnimator.ofFloat(this, "translationY", 0); - translateAnimator.setDuration(400); - - final Animator alphaAnimator = ObjectAnimator.ofFloat(this, "alpha", 1); - alphaAnimator.setStartDelay(200); - alphaAnimator.setDuration(600); - - final AnimatorSet set = new AnimatorSet(); - set.playTogether(alphaAnimator, translateAnimator); - set.setStartDelay(400); - - set.start(); - } - - protected class ViewPagerAdapter extends FragmentPagerAdapter { - private final List panels; - private final Fragment[] fragments; - - public ViewPagerAdapter(FragmentManager fm, List panels) { - super(fm); - this.panels = panels; - this.fragments = new Fragment[panels.size()]; - for (FirstrunPagerConfig.FirstrunPanelConfig panel : panels) { - mDecor.onAddPagerView(context.getString(panel.getTitleRes())); - } - - if (panels.size() > 0) { - mDecor.onPageSelected(0); - } - } - - @Override - public Fragment getItem(int i) { - Fragment fragment = this.fragments[i]; - if (fragment == null) { - FirstrunPagerConfig.FirstrunPanelConfig panelConfig = panels.get(i); - fragment = Fragment.instantiate(context, panelConfig.getClassname(), panelConfig.getArgs()); - ((FirstrunPanel) fragment).setPagerNavigation(pagerNavigation); - fragments[i] = fragment; - } - return fragment; - } - - @Override - public int getCount() { - return panels.size(); - } - - @Override - public CharSequence getPageTitle(int i) { - // Unused now that we use TabMenuStrip. - return context.getString(panels.get(i).getTitleRes()).toUpperCase(); - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/firstrun/FirstrunPagerConfig.java b/mobile/android/base/java/org/mozilla/gecko/firstrun/FirstrunPagerConfig.java deleted file mode 100644 index 3f901d07b0..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/firstrun/FirstrunPagerConfig.java +++ /dev/null @@ -1,107 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.firstrun; - -import android.content.Context; -import android.os.Bundle; -import android.util.Log; -import org.mozilla.gecko.GeckoSharedPrefs; -import org.mozilla.gecko.R; -import org.mozilla.gecko.Telemetry; -import org.mozilla.gecko.TelemetryContract; -import org.mozilla.gecko.Experiments; - -import java.util.LinkedList; -import java.util.List; - -public class FirstrunPagerConfig { - public static final String LOGTAG = "FirstrunPagerConfig"; - - public static final String KEY_IMAGE = "imageRes"; - public static final String KEY_TEXT = "textRes"; - public static final String KEY_SUBTEXT = "subtextRes"; - - public static List getDefault(Context context) { - final List panels = new LinkedList<>(); - - if (Experiments.isInExperimentLocal(context, Experiments.ONBOARDING3_B)) { - panels.add(SimplePanelConfigs.urlbarPanelConfig); - panels.add(SimplePanelConfigs.bookmarksPanelConfig); - panels.add(SimplePanelConfigs.dataPanelConfig); - panels.add(SimplePanelConfigs.syncPanelConfig); - panels.add(SimplePanelConfigs.signInPanelConfig); - Telemetry.startUISession(TelemetryContract.Session.EXPERIMENT, Experiments.ONBOARDING3_B); - GeckoSharedPrefs.forProfile(context).edit().putString(Experiments.PREF_ONBOARDING_VERSION, Experiments.ONBOARDING3_B).apply(); - } else if (Experiments.isInExperimentLocal(context, Experiments.ONBOARDING3_C)) { - panels.add(SimplePanelConfigs.tabqueuePanelConfig); - panels.add(SimplePanelConfigs.readerviewPanelConfig); - panels.add(SimplePanelConfigs.accountPanelConfig); - Telemetry.startUISession(TelemetryContract.Session.EXPERIMENT, Experiments.ONBOARDING3_C); - GeckoSharedPrefs.forProfile(context).edit().putString(Experiments.PREF_ONBOARDING_VERSION, Experiments.ONBOARDING3_C).apply(); - } else { - Log.e(LOGTAG, "Not in an experiment!"); - panels.add(SimplePanelConfigs.signInPanelConfig); - } - return panels; - } - - public static List getRestricted() { - final List panels = new LinkedList<>(); - panels.add(new FirstrunPanelConfig(RestrictedWelcomePanel.class.getName(), RestrictedWelcomePanel.TITLE_RES)); - return panels; - } - - public static class FirstrunPanelConfig { - - private String classname; - private int titleRes; - private Bundle args; - - public FirstrunPanelConfig(String resource, int titleRes) { - this(resource, titleRes, -1, -1, -1, true); - } - - public FirstrunPanelConfig(String classname, int titleRes, int imageRes, int textRes, int subtextRes) { - this(classname, titleRes, imageRes, textRes, subtextRes, false); - } - - private FirstrunPanelConfig(String classname, int titleRes, int imageRes, int textRes, int subtextRes, boolean isCustom) { - this.classname = classname; - this.titleRes = titleRes; - - if (!isCustom) { - this.args = new Bundle(); - this.args.putInt(KEY_IMAGE, imageRes); - this.args.putInt(KEY_TEXT, textRes); - this.args.putInt(KEY_SUBTEXT, subtextRes); - } - } - - public String getClassname() { - return this.classname; - } - - public int getTitleRes() { - return this.titleRes; - } - - public Bundle getArgs() { - return args; - } - } - - private static class SimplePanelConfigs { - public static final FirstrunPanelConfig urlbarPanelConfig = new FirstrunPanelConfig(FirstrunPanel.class.getName(), R.string.firstrun_panel_title_welcome, R.drawable.firstrun_urlbar, R.string.firstrun_urlbar_message, R.string.firstrun_urlbar_subtext); - public static final FirstrunPanelConfig bookmarksPanelConfig = new FirstrunPanelConfig(FirstrunPanel.class.getName(), R.string.firstrun_bookmarks_title, R.drawable.firstrun_bookmarks, R.string.firstrun_bookmarks_message, R.string.firstrun_bookmarks_subtext); - public static final FirstrunPanelConfig dataPanelConfig = new FirstrunPanelConfig(DataPanel.class.getName(), R.string.firstrun_data_title, R.drawable.firstrun_data_off, R.string.firstrun_data_message, R.string.firstrun_data_subtext); - public static final FirstrunPanelConfig syncPanelConfig = new FirstrunPanelConfig(FirstrunPanel.class.getName(), R.string.firstrun_sync_title, R.drawable.firstrun_sync, R.string.firstrun_sync_message, R.string.firstrun_sync_subtext); - public static final FirstrunPanelConfig signInPanelConfig = new FirstrunPanelConfig(SyncPanel.class.getName(), R.string.pref_sync, R.drawable.firstrun_signin, R.string.firstrun_signin_message, R.string.firstrun_welcome_button_browser); - - public static final FirstrunPanelConfig tabqueuePanelConfig = new FirstrunPanelConfig(TabQueuePanel.class.getName(), R.string.firstrun_tabqueue_title, R.drawable.firstrun_tabqueue_off, R.string.firstrun_tabqueue_message_off, R.string.firstrun_tabqueue_subtext_off); - public static final FirstrunPanelConfig readerviewPanelConfig = new FirstrunPanelConfig(FirstrunPanel.class.getName(), R.string.firstrun_readerview_title, R.drawable.firstrun_readerview, R.string.firstrun_readerview_message, R.string.firstrun_readerview_subtext); - public static final FirstrunPanelConfig accountPanelConfig = new FirstrunPanelConfig(SyncPanel.class.getName(), R.string.firstrun_account_title, R.drawable.firstrun_account, R.string.firstrun_account_message, R.string.firstrun_button_notnow); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/firstrun/FirstrunPanel.java b/mobile/android/base/java/org/mozilla/gecko/firstrun/FirstrunPanel.java deleted file mode 100644 index 4b27dbc734..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/firstrun/FirstrunPanel.java +++ /dev/null @@ -1,80 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.firstrun; - -import android.os.Bundle; -import android.support.v4.app.Fragment; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ImageView; -import android.widget.TextView; -import org.mozilla.gecko.R; -import org.mozilla.gecko.Telemetry; -import org.mozilla.gecko.TelemetryContract; - -/** - * Base class for our first run pages. We call these FirstrunPanel for consistency - * with HomePager/HomePanel. - * - * @see FirstrunPager for the containing pager. - */ -public class FirstrunPanel extends Fragment { - - public static final int TITLE_RES = -1; - protected boolean showBrowserHint = true; - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstance) { - final ViewGroup root = (ViewGroup) inflater.inflate(R.layout.firstrun_basepanel_checkable_fragment, container, false); - Bundle args = getArguments(); - if (args != null) { - final int imageRes = args.getInt(FirstrunPagerConfig.KEY_IMAGE); - final int textRes = args.getInt(FirstrunPagerConfig.KEY_TEXT); - final int subtextRes = args.getInt(FirstrunPagerConfig.KEY_SUBTEXT); - - ((ImageView) root.findViewById(R.id.firstrun_image)).setImageResource(imageRes); - ((TextView) root.findViewById(R.id.firstrun_text)).setText(textRes); - ((TextView) root.findViewById(R.id.firstrun_subtext)).setText(subtextRes); - } - - root.findViewById(R.id.firstrun_link).setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.BUTTON, "firstrun-next"); - pagerNavigation.next(); - } - }); - - return root; - } - - public interface PagerNavigation { - void next(); - void finish(); - } - protected PagerNavigation pagerNavigation; - - public void setPagerNavigation(PagerNavigation listener) { - this.pagerNavigation = listener; - } - - protected void next() { - if (pagerNavigation != null) { - pagerNavigation.next(); - } - } - - protected void close() { - if (pagerNavigation != null) { - pagerNavigation.finish(); - } - } - - protected boolean shouldShowBrowserHint() { - return showBrowserHint; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/firstrun/RestrictedWelcomePanel.java b/mobile/android/base/java/org/mozilla/gecko/firstrun/RestrictedWelcomePanel.java deleted file mode 100644 index efc91d20f4..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/firstrun/RestrictedWelcomePanel.java +++ /dev/null @@ -1,61 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.firstrun; - -import org.mozilla.gecko.R; -import org.mozilla.gecko.Telemetry; -import org.mozilla.gecko.TelemetryContract; -import org.mozilla.gecko.home.HomePager; - -import android.app.Activity; -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; - -import java.util.EnumSet; - -public class RestrictedWelcomePanel extends FirstrunPanel { - public static final int TITLE_RES = R.string.firstrun_panel_title_welcome; - - private static final String LEARN_MORE_URL = "https://support.mozilla.org/kb/controlledaccess"; - - private HomePager.OnUrlOpenListener onUrlOpenListener; - - @Override - public void onAttach(Activity activity) { - super.onAttach(activity); - - try { - onUrlOpenListener = (HomePager.OnUrlOpenListener) activity; - } catch (ClassCastException e) { - throw new ClassCastException(activity.toString() + " must implement HomePager.OnUrlOpenListener"); - } - } - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstance) { - final ViewGroup root = (ViewGroup) inflater.inflate(R.layout.restricted_firstrun_welcome_fragment, container, false); - - root.findViewById(R.id.welcome_browse).setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - close(); - } - }); - - root.findViewById(R.id.learn_more_link).setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - onUrlOpenListener.onUrlOpen(LEARN_MORE_URL, EnumSet.of(HomePager.OnUrlOpenListener.Flags.ALLOW_SWITCH_TO_TAB)); - - close(); - } - }); - - return root; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/firstrun/SyncPanel.java b/mobile/android/base/java/org/mozilla/gecko/firstrun/SyncPanel.java deleted file mode 100644 index 2f489c84ed..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/firstrun/SyncPanel.java +++ /dev/null @@ -1,61 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.firstrun; - -import android.content.Intent; -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ImageView; -import android.widget.TextView; -import org.mozilla.gecko.R; -import org.mozilla.gecko.Telemetry; -import org.mozilla.gecko.TelemetryContract; -import org.mozilla.gecko.fxa.FxAccountConstants; -import org.mozilla.gecko.fxa.activities.FxAccountWebFlowActivity; - -public class SyncPanel extends FirstrunPanel { - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstance) { - final ViewGroup root = (ViewGroup) inflater.inflate(R.layout.firstrun_sync_fragment, container, false); - final Bundle args = getArguments(); - if (args != null) { - final int imageRes = args.getInt(FirstrunPagerConfig.KEY_IMAGE); - final int textRes = args.getInt(FirstrunPagerConfig.KEY_TEXT); - final int subtextRes = args.getInt(FirstrunPagerConfig.KEY_SUBTEXT); - - ((ImageView) root.findViewById(R.id.firstrun_image)).setImageResource(imageRes); - ((TextView) root.findViewById(R.id.firstrun_text)).setText(textRes); - ((TextView) root.findViewById(R.id.welcome_browse)).setText(subtextRes); - } - - root.findViewById(R.id.welcome_account).setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.BUTTON, "firstrun-sync"); - showBrowserHint = false; - - final Intent intent = new Intent(FxAccountConstants.ACTION_FXA_GET_STARTED); - intent.putExtra(FxAccountWebFlowActivity.EXTRA_ENDPOINT, FxAccountConstants.ENDPOINT_FIRSTRUN); - intent.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION); - startActivity(intent); - - close(); - } - }); - - root.findViewById(R.id.welcome_browse).setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.BUTTON, "firstrun-browser"); - close(); - } - }); - - return root; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/firstrun/TabQueuePanel.java b/mobile/android/base/java/org/mozilla/gecko/firstrun/TabQueuePanel.java deleted file mode 100644 index 3c2ed83124..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/firstrun/TabQueuePanel.java +++ /dev/null @@ -1,92 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.firstrun; - -import android.content.Context; -import android.content.Intent; -import android.content.SharedPreferences; -import android.graphics.Typeface; -import android.os.Bundle; -import android.support.v4.content.ContextCompat; -import android.support.v7.widget.SwitchCompat; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.CompoundButton; -import android.widget.ImageView; -import android.widget.TextView; -import org.mozilla.gecko.GeckoSharedPrefs; -import org.mozilla.gecko.R; -import org.mozilla.gecko.Telemetry; -import org.mozilla.gecko.TelemetryContract; -import org.mozilla.gecko.preferences.GeckoPreferences; -import org.mozilla.gecko.tabqueue.TabQueueHelper; -import org.mozilla.gecko.tabqueue.TabQueuePrompt; - -public class TabQueuePanel extends FirstrunPanel { - private static final int REQUEST_CODE_TAB_QUEUE = 1; - private SwitchCompat toggleSwitch; - private ImageView imageView; - private TextView messageTextView; - private TextView subtextTextView; - private Context context; - - @Override - public View onCreateView(LayoutInflater inflater, final ViewGroup container, Bundle savedInstance) { - context = getContext(); - final View root = super.onCreateView(inflater, container, savedInstance); - - imageView = (ImageView) root.findViewById(R.id.firstrun_image); - messageTextView = (TextView) root.findViewById(R.id.firstrun_text); - subtextTextView = (TextView) root.findViewById(R.id.firstrun_subtext); - - toggleSwitch = (SwitchCompat) root.findViewById(R.id.firstrun_switch); - toggleSwitch.setVisibility(View.VISIBLE); - toggleSwitch.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { - @Override - public void onCheckedChanged(CompoundButton compoundButton, boolean b) { - Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.DIALOG, "firstrun_tabqueue-permissions"); - if (b && !TabQueueHelper.canDrawOverlays(context)) { - Intent promptIntent = new Intent(context, TabQueuePrompt.class); - startActivityForResult(promptIntent, REQUEST_CODE_TAB_QUEUE); - return; - } - - Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.BUTTON, "firstrun-tabqueue-" + b); - - final SharedPreferences prefs = GeckoSharedPrefs.forApp(context); - final SharedPreferences.Editor editor = prefs.edit(); - editor.putBoolean(GeckoPreferences.PREFS_TAB_QUEUE, b).apply(); - - // Set image, text, and typeface changes. - imageView.setImageResource(b ? R.drawable.firstrun_tabqueue_on : R.drawable.firstrun_tabqueue_off); - messageTextView.setText(b ? R.string.firstrun_tabqueue_message_on : R.string.firstrun_tabqueue_message_off); - messageTextView.setTypeface(b ? Typeface.DEFAULT_BOLD : Typeface.DEFAULT); - subtextTextView.setText(b ? R.string.firstrun_tabqueue_subtext_on : R.string.firstrun_tabqueue_subtext_off); - subtextTextView.setTypeface(b ? Typeface.defaultFromStyle(Typeface.ITALIC) : Typeface.DEFAULT); - subtextTextView.setTextColor(b ? ContextCompat.getColor(context, R.color.fennec_ui_orange) : ContextCompat.getColor(context, R.color.placeholder_grey)); - } - }); - - return root; - } - - @Override - public void onActivityResult(int requestCode, int resultCode, Intent data) { - switch (requestCode) { - case REQUEST_CODE_TAB_QUEUE: - final boolean accepted = TabQueueHelper.processTabQueuePromptResponse(resultCode, context); - if (accepted) { - Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.DIALOG, "firstrun_tabqueue-permissions-yes"); - toggleSwitch.setChecked(true); - Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.BUTTON, "firstrun-tabqueue-true"); - } - Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.DIALOG, "firstrun_tabqueue-permissions-" + (accepted ? "accepted" : "rejected")); - break; - } - } - -} diff --git a/mobile/android/base/java/org/mozilla/gecko/gcm/GcmInstanceIDListenerService.java b/mobile/android/base/java/org/mozilla/gecko/gcm/GcmInstanceIDListenerService.java deleted file mode 100644 index 0616cd229b..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/gcm/GcmInstanceIDListenerService.java +++ /dev/null @@ -1,35 +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.gcm; - -import android.util.Log; - -import com.google.android.gms.iid.InstanceIDListenerService; - -import org.mozilla.gecko.push.PushService; -import org.mozilla.gecko.util.ThreadUtils; - -/** - * This service is notified by the on-device Google Play Services library if an - * in-use token needs to be updated. We simply pass through to AndroidPushService. - */ -public class GcmInstanceIDListenerService extends InstanceIDListenerService { - /** - * Called if InstanceID token is updated. This may occur if the security of - * the previous token had been compromised. This call is initiated by the - * InstanceID provider. - */ - @Override - public void onTokenRefresh() { - Log.d("GeckoPushGCM", "Token refresh request received. Processing on background thread."); - ThreadUtils.postToBackgroundThread(new Runnable() { - @Override - public void run() { - PushService.getInstance(GcmInstanceIDListenerService.this).onRefresh(); - } - }); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/gcm/GcmMessageListenerService.java b/mobile/android/base/java/org/mozilla/gecko/gcm/GcmMessageListenerService.java deleted file mode 100644 index 7962d7dc31..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/gcm/GcmMessageListenerService.java +++ /dev/null @@ -1,38 +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.gcm; - -import android.os.Bundle; -import android.util.Log; - -import com.google.android.gms.gcm.GcmListenerService; - -import org.mozilla.gecko.push.PushService; -import org.mozilla.gecko.util.ThreadUtils; - -/** - * This service actually handles messages directed from the on-device Google - * Play Services package. We simply route them to the AndroidPushService. - */ -public class GcmMessageListenerService extends GcmListenerService { - /** - * Called when message is received. - * - * @param from SenderID of the sender. - * @param bundle Data bundle containing message data as key/value pairs. - */ - @Override - public void onMessageReceived(final String from, final Bundle bundle) { - Log.d("GeckoPushGCM", "Message received. Processing on background thread."); - ThreadUtils.postToBackgroundThread(new Runnable() { - @Override - public void run() { - PushService.getInstance(GcmMessageListenerService.this).onMessageReceived( - GcmMessageListenerService.this, bundle); - } - }); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/gcm/GcmTokenClient.java b/mobile/android/base/java/org/mozilla/gecko/gcm/GcmTokenClient.java deleted file mode 100644 index 024905eb09..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/gcm/GcmTokenClient.java +++ /dev/null @@ -1,131 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.gcm; - -import android.content.Context; -import android.content.SharedPreferences; -import android.support.annotation.NonNull; -import android.util.Log; - -import com.google.android.gms.common.ConnectionResult; -import com.google.android.gms.common.GoogleApiAvailability; -import com.google.android.gms.gcm.GoogleCloudMessaging; -import com.google.android.gms.iid.InstanceID; - -import org.mozilla.gecko.GeckoSharedPrefs; -import org.mozilla.gecko.push.Fetched; - -import java.io.IOException; - -/** - * Fetch and cache GCM tokens. - *

- * GCM tokens are stable and long lived. Google Play Services will periodically request that - * they are rotated, however: see - * https://developers.google.com/instance-id/guides/android-implementation. - *

- * The GCM token is cached in the App-wide shared preferences. There's no particular harm in - * requesting new tokens, so if the user clears the App data, that's fine -- we'll get a fresh - * token and Push will react accordingly. - */ -public class GcmTokenClient { - private static final String LOG_TAG = "GeckoPushGCM"; - - private static final String KEY_GCM_TOKEN = "gcm_token"; - private static final String KEY_GCM_TOKEN_TIMESTAMP = "gcm_token_timestamp"; - - private final Context context; - - public GcmTokenClient(Context context) { - this.context = context; - } - - /** - * Check the device to make sure it has the Google Play Services APK. - * @param context Android context. - */ - protected void ensurePlayServices(Context context) throws NeedsGooglePlayServicesException { - final GoogleApiAvailability apiAvailability = GoogleApiAvailability.getInstance(); - int resultCode = apiAvailability.isGooglePlayServicesAvailable(context); - if (resultCode != ConnectionResult.SUCCESS) { - Log.w(LOG_TAG, "This device does not support GCM! isGooglePlayServicesAvailable returned: " + resultCode); - Log.w(LOG_TAG, "isGooglePlayServicesAvailable message: " + apiAvailability.getErrorString(resultCode)); - throw new NeedsGooglePlayServicesException(resultCode); - } - } - - /** - * Get a GCM token (possibly cached). - * - * @param senderID to request token for. - * @param debug whether to log debug details. - * @return token and timestamp. - * @throws NeedsGooglePlayServicesException if user action is needed to use Google Play Services. - * @throws IOException if the token fetch failed. - */ - public @NonNull Fetched getToken(@NonNull String senderID, boolean debug) throws NeedsGooglePlayServicesException, IOException { - ensurePlayServices(this.context); - - final SharedPreferences sharedPrefs = GeckoSharedPrefs.forApp(context); - String token = sharedPrefs.getString(KEY_GCM_TOKEN, null); - long timestamp = sharedPrefs.getLong(KEY_GCM_TOKEN_TIMESTAMP, 0L); - if (token != null && timestamp > 0L) { - if (debug) { - Log.i(LOG_TAG, "Cached GCM token exists: " + token); - } else { - Log.i(LOG_TAG, "Cached GCM token exists."); - } - return new Fetched(token, timestamp); - } - - Log.i(LOG_TAG, "Cached GCM token does not exist; requesting new token with sender ID: " + senderID); - - final InstanceID instanceID = InstanceID.getInstance(context); - token = instanceID.getToken(senderID, GoogleCloudMessaging.INSTANCE_ID_SCOPE, null); - timestamp = System.currentTimeMillis(); - - if (debug) { - Log.i(LOG_TAG, "Got fresh GCM token; caching: " + token); - } else { - Log.i(LOG_TAG, "Got fresh GCM token; caching."); - } - sharedPrefs - .edit() - .putString(KEY_GCM_TOKEN, token) - .putLong(KEY_GCM_TOKEN_TIMESTAMP, timestamp) - .apply(); - - return new Fetched(token, timestamp); - } - - /** - * Remove any cached GCM token. - */ - public void invalidateToken() { - final SharedPreferences sharedPrefs = GeckoSharedPrefs.forApp(context); - sharedPrefs - .edit() - .remove(KEY_GCM_TOKEN) - .remove(KEY_GCM_TOKEN_TIMESTAMP) - .apply(); - } - - public class NeedsGooglePlayServicesException extends Exception { - private static final long serialVersionUID = 4132853166L; - - private final int resultCode; - - NeedsGooglePlayServicesException(int resultCode) { - super(); - this.resultCode = resultCode; - } - - public void showErrorNotification() { - final GoogleApiAvailability apiAvailability = GoogleApiAvailability.getInstance(); - apiAvailability.showErrorNotification(context, resultCode); - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/health/HealthRecorder.java b/mobile/android/base/java/org/mozilla/gecko/health/HealthRecorder.java deleted file mode 100644 index a9f5b72f3b..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/health/HealthRecorder.java +++ /dev/null @@ -1,40 +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.health; - -import android.content.Context; -import android.content.SharedPreferences; - -import org.json.JSONObject; - -/** - * HealthRecorder is an interface into the Firefox Health Report storage system. - */ -public interface HealthRecorder { - /** - * Returns whether the Health Recorder is actively recording events. - */ - public boolean isEnabled(); - - public void setCurrentSession(SessionInformation session); - public void checkForOrphanSessions(); - - public void recordGeckoStartupTime(long duration); - public void recordJavaStartupTime(long duration); - public void recordSearch(final String engineID, final String location); - public void recordSessionEnd(String reason, SharedPreferences.Editor editor); - public void recordSessionEnd(String reason, SharedPreferences.Editor editor, final int environment); - - public void onAppLocaleChanged(String to); - public void onAddonChanged(String id, JSONObject json); - public void onAddonUninstalling(String id); - public void onEnvironmentChanged(); - public void onEnvironmentChanged(final boolean startNewSession, final String sessionEndReason); - - public void close(final Context context); - - public void processDelayed(); -} diff --git a/mobile/android/base/java/org/mozilla/gecko/health/SessionInformation.java b/mobile/android/base/java/org/mozilla/gecko/health/SessionInformation.java deleted file mode 100644 index ad65918e1e..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/health/SessionInformation.java +++ /dev/null @@ -1,138 +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.health; - -import android.content.SharedPreferences; -import android.util.Log; - -import org.mozilla.gecko.GeckoApp; -import org.mozilla.gecko.GeckoAppShell; - -import org.json.JSONException; -import org.json.JSONObject; - -public class SessionInformation { - private static final String LOG_TAG = "GeckoSessInfo"; - - public static final String PREFS_SESSION_START = "sessionStart"; - - public final long wallStartTime; // System wall clock. - public final long realStartTime; // Realtime clock. - - private final boolean wasOOM; - private final boolean wasStopped; - - private volatile long timedGeckoStartup = -1; - private volatile long timedJavaStartup = -1; - - // Current sessions don't (right now) care about wasOOM/wasStopped. - // Eventually we might want to lift that logic out of GeckoApp. - public SessionInformation(long wallTime, long realTime) { - this(wallTime, realTime, false, false); - } - - // Previous sessions do... - public SessionInformation(long wallTime, long realTime, boolean wasOOM, boolean wasStopped) { - this.wallStartTime = wallTime; - this.realStartTime = realTime; - this.wasOOM = wasOOM; - this.wasStopped = wasStopped; - } - - /** - * Initialize a new SessionInformation instance from the supplied prefs object. - * - * This includes retrieving OOM/crash data, as well as timings. - * - * If no wallStartTime was found, that implies that the previous - * session was correctly recorded, and an object with a zero - * wallStartTime is returned. - */ - public static SessionInformation fromSharedPrefs(SharedPreferences prefs) { - boolean wasOOM = prefs.getBoolean(GeckoAppShell.PREFS_OOM_EXCEPTION, false); - boolean wasStopped = prefs.getBoolean(GeckoApp.PREFS_WAS_STOPPED, true); - long wallStartTime = prefs.getLong(PREFS_SESSION_START, 0L); - long realStartTime = 0L; - Log.d(LOG_TAG, "Building SessionInformation from prefs: " + - wallStartTime + ", " + realStartTime + ", " + - wasStopped + ", " + wasOOM); - return new SessionInformation(wallStartTime, realStartTime, wasOOM, wasStopped); - } - - /** - * Initialize a new SessionInformation instance to 'split' the current - * session. - */ - public static SessionInformation forRuntimeTransition() { - final boolean wasOOM = false; - final boolean wasStopped = true; - final long wallStartTime = System.currentTimeMillis(); - final long realStartTime = android.os.SystemClock.elapsedRealtime(); - Log.v(LOG_TAG, "Recording runtime session transition: " + - wallStartTime + ", " + realStartTime); - return new SessionInformation(wallStartTime, realStartTime, wasOOM, wasStopped); - } - - public boolean wasKilled() { - return wasOOM || !wasStopped; - } - - /** - * Record the beginning of this session to SharedPreferences by - * recording our start time. If a session was already recorded, it is - * overwritten (there can only be one running session at a time). Does - * not commit the editor. - */ - public void recordBegin(SharedPreferences.Editor editor) { - Log.d(LOG_TAG, "Recording start of session: " + this.wallStartTime); - editor.putLong(PREFS_SESSION_START, this.wallStartTime); - } - - /** - * Record the completion of this session to SharedPreferences by - * deleting our start time. Does not commit the editor. - */ - public void recordCompletion(SharedPreferences.Editor editor) { - Log.d(LOG_TAG, "Recording session done: " + this.wallStartTime); - editor.remove(PREFS_SESSION_START); - } - - /** - * Return the JSON that we'll put in the DB for this session. - */ - public JSONObject getCompletionJSON(String reason, long realEndTime) throws JSONException { - long durationSecs = (realEndTime - this.realStartTime) / 1000; - JSONObject out = new JSONObject(); - out.put("r", reason); - out.put("d", durationSecs); - if (this.timedGeckoStartup > 0) { - out.put("sg", this.timedGeckoStartup); - } - if (this.timedJavaStartup > 0) { - out.put("sj", this.timedJavaStartup); - } - return out; - } - - public JSONObject getCrashedJSON() throws JSONException { - JSONObject out = new JSONObject(); - // We use ints here instead of booleans, because we're packing - // stuff into JSON, and saving bytes in the DB is a worthwhile - // goal. - out.put("oom", this.wasOOM ? 1 : 0); - out.put("stopped", this.wasStopped ? 1 : 0); - out.put("r", "A"); - return out; - } - - public void setTimedGeckoStartup(final long duration) { - timedGeckoStartup = duration; - } - - public void setTimedJavaStartup(final long duration) { - timedJavaStartup = duration; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/health/StubbedHealthRecorder.java b/mobile/android/base/java/org/mozilla/gecko/health/StubbedHealthRecorder.java deleted file mode 100644 index 65a9729859..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/health/StubbedHealthRecorder.java +++ /dev/null @@ -1,53 +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.health; - -import android.content.Context; -import android.content.SharedPreferences; - -import org.json.JSONObject; - -/** - * StubbedHealthRecorder is an implementation of HealthRecorder that does (you guessed it!) - * nothing. - */ -public class StubbedHealthRecorder implements HealthRecorder { - @Override - public boolean isEnabled() { return false; } - - @Override - public void setCurrentSession(SessionInformation session) { } - @Override - public void checkForOrphanSessions() { } - - @Override - public void recordGeckoStartupTime(long duration) { } - @Override - public void recordJavaStartupTime(long duration) { } - @Override - public void recordSearch(final String engineID, final String location) { } - @Override - public void recordSessionEnd(String reason, SharedPreferences.Editor editor) { } - @Override - public void recordSessionEnd(String reason, SharedPreferences.Editor editor, final int environment) { } - - @Override - public void onAppLocaleChanged(String to) { } - @Override - public void onAddonChanged(String id, JSONObject json) { } - @Override - public void onAddonUninstalling(String id) { } - @Override - public void onEnvironmentChanged() { } - @Override - public void onEnvironmentChanged(final boolean startNewSession, final String sessionEndReason) { } - - @Override - public void close(final Context context) { } - - @Override - public void processDelayed() { } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/BookmarkFolderView.java b/mobile/android/base/java/org/mozilla/gecko/home/BookmarkFolderView.java deleted file mode 100644 index 566422faff..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/BookmarkFolderView.java +++ /dev/null @@ -1,147 +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.GeckoProfile; -import org.mozilla.gecko.R; -import org.mozilla.gecko.db.BrowserContract; -import org.mozilla.gecko.db.BrowserDB; -import org.mozilla.gecko.util.ThreadUtils; -import org.mozilla.gecko.util.UIAsyncTask; - -import android.content.Context; -import android.support.annotation.NonNull; -import android.util.AttributeSet; -import android.view.LayoutInflater; -import android.view.View; -import android.widget.ImageView; -import android.widget.LinearLayout; -import android.widget.TextView; - -import java.lang.ref.WeakReference; -import java.util.Collections; -import java.util.Set; -import java.util.TreeSet; - -public class BookmarkFolderView extends LinearLayout { - private static final Set FOLDERS_WITH_COUNT; - - static { - final Set folders = new TreeSet<>(); - folders.add(BrowserContract.Bookmarks.FAKE_READINGLIST_SMARTFOLDER_ID); - - FOLDERS_WITH_COUNT = Collections.unmodifiableSet(folders); - } - - public enum FolderState { - /** - * A standard folder, i.e. a folder in a list of bookmarks and folders. - */ - FOLDER(R.drawable.folder_closed), - - /** - * The parent folder: this indicates that you are able to return to the previous - * folder ("Back to {name}"). - */ - PARENT(R.drawable.bookmark_folder_arrow_up), - - /** - * The reading list smartfolder: this displays a reading list icon instead of the - * normal folder icon. - */ - READING_LIST(R.drawable.reading_list_folder); - - public final int image; - - FolderState(final int image) { this.image = image; } - } - - private final TextView mTitle; - private final TextView mSubtitle; - - private final ImageView mIcon; - - public BookmarkFolderView(Context context) { - this(context, null); - } - - public BookmarkFolderView(Context context, AttributeSet attrs) { - super(context, attrs); - - LayoutInflater.from(context).inflate(R.layout.two_line_folder_row, this); - - mTitle = (TextView) findViewById(R.id.title); - mSubtitle = (TextView) findViewById(R.id.subtitle); - mIcon = (ImageView) findViewById(R.id.icon); - } - - public void update(String title, int folderID) { - setTitle(title); - setID(folderID); - } - - private void setTitle(String title) { - mTitle.setText(title); - } - - private static class ItemCountUpdateTask extends UIAsyncTask.WithoutParams { - private final WeakReference mTextViewReference; - private final int mFolderID; - - public ItemCountUpdateTask(final WeakReference textViewReference, - final int folderID) { - super(ThreadUtils.getBackgroundHandler()); - - mTextViewReference = textViewReference; - mFolderID = folderID; - } - - @Override - protected Integer doInBackground() { - final TextView textView = mTextViewReference.get(); - - if (textView == null) { - return null; - } - - final BrowserDB db = BrowserDB.from(textView.getContext()); - return db.getBookmarkCountForFolder(textView.getContext().getContentResolver(), mFolderID); - } - - @Override - protected void onPostExecute(Integer count) { - final TextView textView = mTextViewReference.get(); - - if (textView == null) { - return; - } - - final String text; - if (count == 1) { - text = textView.getContext().getResources().getString(R.string.bookmark_folder_one_item); - } else { - text = textView.getContext().getResources().getString(R.string.bookmark_folder_items, count); - } - - textView.setText(text); - textView.setVisibility(View.VISIBLE); - } - } - - private void setID(final int folderID) { - if (FOLDERS_WITH_COUNT.contains(folderID)) { - final WeakReference subTitleReference = new WeakReference(mSubtitle); - - new ItemCountUpdateTask(subTitleReference, folderID).execute(); - } else { - mSubtitle.setVisibility(View.GONE); - } - } - - public void setState(@NonNull FolderState state) { - mIcon.setImageResource(state.image); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/BookmarkScreenshotRow.java b/mobile/android/base/java/org/mozilla/gecko/home/BookmarkScreenshotRow.java deleted file mode 100644 index a1efff0491..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/BookmarkScreenshotRow.java +++ /dev/null @@ -1,67 +0,0 @@ -/* 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.util.AttributeSet; -import android.widget.LinearLayout; -import android.widget.TextView; - -import org.mozilla.gecko.R; -import org.mozilla.gecko.db.BrowserContract.UrlAnnotations; - -import java.text.DateFormat; -import java.text.FieldPosition; -import java.util.Date; - -/** - * An entry of the screenshot list in the bookmarks panel. - */ -class BookmarkScreenshotRow extends LinearLayout { - private TextView titleView; - private TextView dateView; - - // This DateFormat uses the current locale at instantiation time, which won't get updated if the locale is changed. - // Since it's just a date, it's probably not worth the code complexity to fix that. - private static final DateFormat dateFormat = DateFormat.getDateInstance(DateFormat.LONG); - private static final DateFormat timeFormat = DateFormat.getTimeInstance(DateFormat.SHORT); - - // This parameter to DateFormat.format has no impact on the result but rather gets mutated by the method to - // identify where a certain field starts and ends (by index). This is useful if you want to later modify the String; - // I'm not sure why this argument isn't optional. - private static final FieldPosition dummyFieldPosition = new FieldPosition(-1); - - public BookmarkScreenshotRow(Context context, AttributeSet attrs) { - super(context, attrs); - } - - @Override - public void onFinishInflate() { - super.onFinishInflate(); - titleView = (TextView) findViewById(R.id.title); - dateView = (TextView) findViewById(R.id.date); - } - - public void updateFromCursor(final Cursor c) { - titleView.setText(getTitleFromCursor(c)); - dateView.setText(getDateFromCursor(c)); - } - - private static String getTitleFromCursor(final Cursor c) { - final int index = c.getColumnIndexOrThrow(UrlAnnotations.URL); - return c.getString(index); - } - - private static String getDateFromCursor(final Cursor c) { - final long timestamp = c.getLong(c.getColumnIndexOrThrow(UrlAnnotations.DATE_CREATED)); - final Date date = new Date(timestamp); - final StringBuffer sb = new StringBuffer(); - dateFormat.format(date, sb, dummyFieldPosition) - .append(" - "); - timeFormat.format(date, sb, dummyFieldPosition); - return sb.toString(); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/BookmarksListAdapter.java b/mobile/android/base/java/org/mozilla/gecko/home/BookmarksListAdapter.java deleted file mode 100644 index b311166933..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/BookmarksListAdapter.java +++ /dev/null @@ -1,352 +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 java.util.Collections; -import java.util.LinkedList; -import java.util.List; - -import org.mozilla.gecko.R; -import org.mozilla.gecko.db.BrowserContract.Bookmarks; -import org.mozilla.gecko.home.BookmarkFolderView.FolderState; - -import android.content.Context; -import android.content.res.Resources; -import android.database.Cursor; -import android.os.Parcel; -import android.os.Parcelable; -import android.view.View; - -/** - * Adapter to back the BookmarksListView with a list of bookmarks. - */ -class BookmarksListAdapter extends MultiTypeCursorAdapter { - private static final int VIEW_TYPE_BOOKMARK_ITEM = 0; - private static final int VIEW_TYPE_FOLDER = 1; - private static final int VIEW_TYPE_SCREENSHOT = 2; - - private static final int[] VIEW_TYPES = new int[] { VIEW_TYPE_BOOKMARK_ITEM, VIEW_TYPE_FOLDER, VIEW_TYPE_SCREENSHOT }; - private static final int[] LAYOUT_TYPES = - new int[] { R.layout.bookmark_item_row, R.layout.bookmark_folder_row, R.layout.bookmark_screenshot_row }; - - public enum RefreshType implements Parcelable { - PARENT, - CHILD; - - @Override - public int describeContents() { - return 0; - } - - @Override - public void writeToParcel(Parcel dest, int flags) { - dest.writeInt(ordinal()); - } - - public static final Creator CREATOR = new Creator() { - @Override - public RefreshType createFromParcel(final Parcel source) { - return RefreshType.values()[source.readInt()]; - } - - @Override - public RefreshType[] newArray(final int size) { - return new RefreshType[size]; - } - }; - } - - public static class FolderInfo implements Parcelable { - public final int id; - public final String title; - - public FolderInfo(int id) { - this(id, ""); - } - - public FolderInfo(Parcel in) { - this(in.readInt(), in.readString()); - } - - public FolderInfo(int id, String title) { - this.id = id; - this.title = title; - } - - @Override - public int describeContents() { - return 0; - } - - @Override - public void writeToParcel(Parcel dest, int flags) { - dest.writeInt(id); - dest.writeString(title); - } - - public static final Creator CREATOR = new Creator() { - @Override - public FolderInfo createFromParcel(Parcel in) { - return new FolderInfo(in); - } - - @Override - public FolderInfo[] newArray(int size) { - return new FolderInfo[size]; - } - }; - } - - // A listener that knows how to refresh the list for a given folder id. - // This is usually implemented by the enclosing fragment/activity. - public static interface OnRefreshFolderListener { - // The folder id to refresh the list with. - public void onRefreshFolder(FolderInfo folderInfo, RefreshType refreshType); - } - - /** - * The type of data a bookmarks folder can display. This can be used to - * distinguish bookmark folders from "smart folders" that contain non-bookmark - * entries but still appear in the Bookmarks panel. - */ - public enum FolderType { - BOOKMARKS, - SCREENSHOTS, - } - - // mParentStack holds folder info instances (id + title) that allow - // us to navigate back up the folder hierarchy. - private LinkedList mParentStack; - - // Refresh folder listener. - private OnRefreshFolderListener mListener; - - private FolderType openFolderType = FolderType.BOOKMARKS; - - public BookmarksListAdapter(Context context, Cursor cursor, List parentStack) { - // Initializing with a null cursor. - super(context, cursor, VIEW_TYPES, LAYOUT_TYPES); - - if (parentStack == null) { - mParentStack = new LinkedList(); - } else { - mParentStack = new LinkedList(parentStack); - } - } - - public void restoreData(List parentStack) { - mParentStack = new LinkedList(parentStack); - notifyDataSetChanged(); - } - - public List getParentStack() { - return Collections.unmodifiableList(mParentStack); - } - - public FolderType getOpenFolderType() { - return openFolderType; - } - - /** - * Moves to parent folder, if one exists. - * - * @return Whether the adapter successfully moved to a parent folder. - */ - public boolean moveToParentFolder() { - // If we're already at the root, we can't move to a parent folder. - // An empty parent stack here means we're still waiting for the - // initial list of bookmarks and can't go to a parent folder. - if (mParentStack.size() <= 1) { - return false; - } - - if (mListener != null) { - // We pick the second folder in the stack as it represents - // the parent folder. - mListener.onRefreshFolder(mParentStack.get(1), RefreshType.PARENT); - } - - return true; - } - - /** - * Moves to child folder, given a folderId. - * - * @param folderId The id of the folder to show. - * @param folderTitle The title of the folder to show. - */ - public void moveToChildFolder(int folderId, String folderTitle) { - FolderInfo folderInfo = new FolderInfo(folderId, folderTitle); - - if (mListener != null) { - mListener.onRefreshFolder(folderInfo, RefreshType.CHILD); - } - } - - /** - * Set a listener that can refresh this adapter. - * - * @param listener The listener that can refresh the adapter. - */ - public void setOnRefreshFolderListener(OnRefreshFolderListener listener) { - mListener = listener; - } - - private boolean isCurrentFolder(FolderInfo folderInfo) { - return (mParentStack.size() > 0 && - mParentStack.peek().id == folderInfo.id); - } - - public void swapCursor(Cursor c, FolderInfo folderInfo, RefreshType refreshType) { - updateOpenFolderType(folderInfo); - switch (refreshType) { - case PARENT: - if (!isCurrentFolder(folderInfo)) { - mParentStack.removeFirst(); - } - break; - - case CHILD: - if (!isCurrentFolder(folderInfo)) { - mParentStack.addFirst(folderInfo); - } - break; - - default: - // Do nothing; - } - - swapCursor(c); - } - - private void updateOpenFolderType(final FolderInfo folderInfo) { - if (folderInfo.id == Bookmarks.FIXED_SCREENSHOT_FOLDER_ID) { - openFolderType = FolderType.SCREENSHOTS; - } else { - openFolderType = FolderType.BOOKMARKS; - } - } - - @Override - public int getItemViewType(int position) { - // The position also reflects the opened child folder row. - if (isShowingChildFolder()) { - if (position == 0) { - return VIEW_TYPE_FOLDER; - } - - // Accounting for the folder view. - position--; - } - - if (openFolderType == FolderType.SCREENSHOTS) { - return VIEW_TYPE_SCREENSHOT; - } - - final Cursor c = getCursor(position); - if (c.getInt(c.getColumnIndexOrThrow(Bookmarks.TYPE)) == Bookmarks.TYPE_FOLDER) { - return VIEW_TYPE_FOLDER; - } - - // Default to returning normal item type. - return VIEW_TYPE_BOOKMARK_ITEM; - } - - /** - * Get the title of the folder given a cursor moved to the position. - * - * @param context The context of the view. - * @param cursor A cursor moved to the required position. - * @return The title of the folder at the position. - */ - public String getFolderTitle(Context context, Cursor c) { - String guid = c.getString(c.getColumnIndexOrThrow(Bookmarks.GUID)); - - // If we don't have a special GUID, just return the folder title from the DB. - if (guid == null || guid.length() == 12) { - return c.getString(c.getColumnIndexOrThrow(Bookmarks.TITLE)); - } - - Resources res = context.getResources(); - - // Use localized strings for special folder names. - if (guid.equals(Bookmarks.FAKE_DESKTOP_FOLDER_GUID)) { - return res.getString(R.string.bookmarks_folder_desktop); - } else if (guid.equals(Bookmarks.MENU_FOLDER_GUID)) { - return res.getString(R.string.bookmarks_folder_menu); - } else if (guid.equals(Bookmarks.TOOLBAR_FOLDER_GUID)) { - return res.getString(R.string.bookmarks_folder_toolbar); - } else if (guid.equals(Bookmarks.UNFILED_FOLDER_GUID)) { - return res.getString(R.string.bookmarks_folder_unfiled); - } else if (guid.equals(Bookmarks.SCREENSHOT_FOLDER_GUID)) { - return res.getString(R.string.screenshot_folder_label_in_bookmarks); - } else if (guid.equals(Bookmarks.FAKE_READINGLIST_SMARTFOLDER_GUID)) { - return res.getString(R.string.readinglist_smartfolder_label_in_bookmarks); - } - - // If for some reason we have a folder with a special GUID, but it's not one of - // the special folders we expect in the UI, just return the title from the DB. - return c.getString(c.getColumnIndexOrThrow(Bookmarks.TITLE)); - } - - /** - * @return true, if currently showing a child folder, false otherwise. - */ - public boolean isShowingChildFolder() { - if (mParentStack.size() == 0) { - return false; - } - - return (mParentStack.peek().id != Bookmarks.FIXED_ROOT_ID); - } - - @Override - public int getCount() { - return super.getCount() + (isShowingChildFolder() ? 1 : 0); - } - - @Override - public void bindView(View view, Context context, int position) { - final int viewType = getItemViewType(position); - - final Cursor cursor; - if (isShowingChildFolder()) { - if (position == 0) { - cursor = null; - } else { - // Accounting for the folder view. - position--; - cursor = getCursor(position); - } - } else { - cursor = getCursor(position); - } - - if (viewType == VIEW_TYPE_SCREENSHOT) { - ((BookmarkScreenshotRow) view).updateFromCursor(cursor); - } else if (viewType == VIEW_TYPE_BOOKMARK_ITEM) { - final TwoLinePageRow row = (TwoLinePageRow) view; - row.updateFromCursor(cursor); - } else { - final BookmarkFolderView row = (BookmarkFolderView) view; - if (cursor == null) { - final Resources res = context.getResources(); - row.update(res.getString(R.string.home_move_back_to_filter, mParentStack.get(1).title), -1); - row.setState(FolderState.PARENT); - } else { - int id = cursor.getInt(cursor.getColumnIndexOrThrow(Bookmarks._ID)); - - row.update(getFolderTitle(context, cursor), id); - - if (id == Bookmarks.FAKE_READINGLIST_SMARTFOLDER_ID) { - row.setState(FolderState.READING_LIST); - } else { - row.setState(FolderState.FOLDER); - } - } - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/BookmarksListView.java b/mobile/android/base/java/org/mozilla/gecko/home/BookmarksListView.java deleted file mode 100644 index 94157be105..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/BookmarksListView.java +++ /dev/null @@ -1,218 +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 java.util.EnumSet; -import java.util.List; - -import android.util.Log; -import org.mozilla.gecko.R; -import org.mozilla.gecko.Telemetry; -import org.mozilla.gecko.TelemetryContract; -import org.mozilla.gecko.db.BrowserContract; -import org.mozilla.gecko.db.BrowserContract.Bookmarks; -import org.mozilla.gecko.home.HomePager.OnUrlOpenListener; - -import android.content.Context; -import android.database.Cursor; -import android.util.AttributeSet; -import android.view.KeyEvent; -import android.view.View; -import android.widget.AdapterView; -import android.widget.HeaderViewListAdapter; -import android.widget.ListAdapter; - -import org.mozilla.gecko.reader.SavedReaderViewHelper; -import org.mozilla.gecko.util.NetworkUtils; - -/** - * A ListView of bookmarks. - */ -public class BookmarksListView extends HomeListView - implements AdapterView.OnItemClickListener { - public static final String LOGTAG = "GeckoBookmarksListView"; - - public BookmarksListView(Context context) { - this(context, null); - } - - public BookmarksListView(Context context, AttributeSet attrs) { - this(context, attrs, R.attr.bookmarksListViewStyle); - } - - public BookmarksListView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - } - - @Override - public void onAttachedToWindow() { - super.onAttachedToWindow(); - - setOnItemClickListener(this); - - setOnKeyListener(new View.OnKeyListener() { - @Override - public boolean onKey(View v, int keyCode, KeyEvent event) { - final int action = event.getAction(); - - // If the user hit the BACK key, try to move to the parent folder. - if (action == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) { - return getBookmarksListAdapter().moveToParentFolder(); - } - return false; - } - }); - } - - /** - * Get the appropriate telemetry extra for a given folder. - * - * baseFolderID is the ID of the first-level folder in the parent stack, i.e. the first folder - * that was selected from the root hierarchy (e.g. Desktop, Reading List, or any mobile first-level - * subfolder). If the current folder is a first-level folder, then the fixed root ID may be used - * instead. - * - * We use baseFolderID only to distinguish whether or not we're currently in a desktop subfolder. - * If it isn't equal to FAKE_DESKTOP_FOLDER_ID we know we're in a mobile subfolder, or one - * of the smartfolders. - */ - private String getTelemetryExtraForFolder(int folderID, int baseFolderID) { - if (folderID == Bookmarks.FAKE_DESKTOP_FOLDER_ID) { - return "folder_desktop"; - } else if (folderID == Bookmarks.FIXED_SCREENSHOT_FOLDER_ID) { - return "folder_screenshots"; - } else if (folderID == Bookmarks.FAKE_READINGLIST_SMARTFOLDER_ID) { - return "folder_reading_list"; - } else { - // The stack depth is 2 for either the fake desktop folder, or any subfolder of mobile - // bookmarks, we subtract these offsets so that any direct subfolder of mobile - // has a level equal to 1. (Desktop folders will be one level deeper due to the - // fake desktop folder, hence subtract 2.) - if (baseFolderID == Bookmarks.FAKE_DESKTOP_FOLDER_ID) { - return "folder_desktop_subfolder"; - } else { - return "folder_mobile_subfolder"; - } - } - } - - @Override - public void onItemClick(AdapterView parent, View view, int position, long id) { - final BookmarksListAdapter adapter = getBookmarksListAdapter(); - if (adapter.isShowingChildFolder()) { - if (position == 0) { - // If we tap on an opened folder, move back to parent folder. - - final List parentStack = ((BookmarksListAdapter) getAdapter()).getParentStack(); - if (parentStack.size() < 2) { - throw new IllegalStateException("Cannot move to parent folder if we are already in the root folder"); - } - - // The first item (top of stack) is the current folder, we're returning to the next one - BookmarksListAdapter.FolderInfo folder = parentStack.get(1); - final int parentID = folder.id; - final int baseFolderID; - if (parentStack.size() > 2) { - baseFolderID = parentStack.get(parentStack.size() - 2).id; - } else { - baseFolderID = Bookmarks.FIXED_ROOT_ID; - } - - final String extra = getTelemetryExtraForFolder(parentID, baseFolderID); - - // Move to parent _after_ retrieving stack information - adapter.moveToParentFolder(); - - Telemetry.sendUIEvent(TelemetryContract.Event.SHOW, TelemetryContract.Method.LIST_ITEM, extra); - return; - } - - // Accounting for the folder view. - position--; - } - - final Cursor cursor = adapter.getCursor(); - if (cursor == null) { - return; - } - - cursor.moveToPosition(position); - - if (adapter.getOpenFolderType() == BookmarksListAdapter.FolderType.SCREENSHOTS) { - Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.LIST_ITEM, "bookmarks-screenshot"); - - final String fileUrl = "file://" + cursor.getString(cursor.getColumnIndex(BrowserContract.UrlAnnotations.VALUE)); - getOnUrlOpenListener().onUrlOpen(fileUrl, EnumSet.of(OnUrlOpenListener.Flags.ALLOW_SWITCH_TO_TAB)); - return; - } - - int type = cursor.getInt(cursor.getColumnIndexOrThrow(Bookmarks.TYPE)); - if (type == Bookmarks.TYPE_FOLDER) { - // If we're clicking on a folder, update adapter to move to that folder - final int folderId = cursor.getInt(cursor.getColumnIndexOrThrow(Bookmarks._ID)); - final String folderTitle = adapter.getFolderTitle(parent.getContext(), cursor); - adapter.moveToChildFolder(folderId, folderTitle); - - final List parentStack = ((BookmarksListAdapter) getAdapter()).getParentStack(); - - final int baseFolderID; - if (parentStack.size() > 2) { - baseFolderID = parentStack.get(parentStack.size() - 2).id; - } else { - baseFolderID = Bookmarks.FIXED_ROOT_ID; - } - - final String extra = getTelemetryExtraForFolder(folderId, baseFolderID); - Telemetry.sendUIEvent(TelemetryContract.Event.SHOW, TelemetryContract.Method.LIST_ITEM, extra); - } else { - // Otherwise, just open the URL - final String url = cursor.getString(cursor.getColumnIndexOrThrow(Bookmarks.URL)); - - final SavedReaderViewHelper rvh = SavedReaderViewHelper.getSavedReaderViewHelper(getContext()); - - final String extra; - if (rvh.isURLCached(url)) { - extra = "bookmarks-reader"; - } else { - extra = "bookmarks"; - } - - Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.LIST_ITEM, extra); - Telemetry.addToHistogram("FENNEC_LOAD_SAVED_PAGE", NetworkUtils.isConnected(getContext()) ? 2 : 3); - - // This item is a TwoLinePageRow, so we allow switch-to-tab. - getOnUrlOpenListener().onUrlOpen(url, EnumSet.of(OnUrlOpenListener.Flags.ALLOW_SWITCH_TO_TAB)); - } - } - - @Override - public boolean onItemLongClick(AdapterView parent, View view, int position, long id) { - // Adjust the item position to account for the parent folder row that is inserted - // at the top of the list when viewing the contents of a folder. - final BookmarksListAdapter adapter = getBookmarksListAdapter(); - if (adapter.isShowingChildFolder()) { - position--; - } - - // Temporarily prevent crashes until we figure out what we actually want to do here (bug 1252316). - if (adapter.getOpenFolderType() == BookmarksListAdapter.FolderType.SCREENSHOTS) { - return false; - } - - return super.onItemLongClick(parent, view, position, id); - } - - private BookmarksListAdapter getBookmarksListAdapter() { - BookmarksListAdapter adapter; - ListAdapter listAdapter = getAdapter(); - if (listAdapter instanceof HeaderViewListAdapter) { - adapter = (BookmarksListAdapter) ((HeaderViewListAdapter) listAdapter).getWrappedAdapter(); - } else { - adapter = (BookmarksListAdapter) listAdapter; - } - return adapter; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/BookmarksPanel.java b/mobile/android/base/java/org/mozilla/gecko/home/BookmarksPanel.java deleted file mode 100644 index 4b47819966..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/BookmarksPanel.java +++ /dev/null @@ -1,316 +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 java.util.ArrayList; -import java.util.LinkedList; -import java.util.List; - -import org.mozilla.gecko.GeckoProfile; -import org.mozilla.gecko.GeckoSharedPrefs; -import org.mozilla.gecko.R; -import org.mozilla.gecko.db.BrowserContract; -import org.mozilla.gecko.db.BrowserContract.Bookmarks; -import org.mozilla.gecko.db.BrowserDB; -import org.mozilla.gecko.distribution.PartnerBookmarksProviderProxy; -import org.mozilla.gecko.home.BookmarksListAdapter.FolderInfo; -import org.mozilla.gecko.home.BookmarksListAdapter.OnRefreshFolderListener; -import org.mozilla.gecko.home.BookmarksListAdapter.RefreshType; -import org.mozilla.gecko.home.HomeContextMenuInfo.RemoveItemType; -import org.mozilla.gecko.home.HomePager.OnUrlOpenListener; -import org.mozilla.gecko.preferences.GeckoPreferences; - -import android.app.Activity; -import android.content.ContentResolver; -import android.content.Context; -import android.content.res.Configuration; -import android.database.Cursor; -import android.database.MergeCursor; -import android.os.Bundle; -import android.os.Parcelable; -import android.support.annotation.NonNull; -import android.support.v4.app.LoaderManager; -import android.support.v4.content.Loader; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.view.ViewStub; -import android.widget.ImageView; -import android.widget.TextView; - -/** - * A page in about:home that displays a ListView of bookmarks. - */ -public class BookmarksPanel extends HomeFragment { - public static final String LOGTAG = "GeckoBookmarksPanel"; - - // Cursor loader ID for list of bookmarks. - private static final int LOADER_ID_BOOKMARKS_LIST = 0; - - // Information about the target bookmarks folder. - private static final String BOOKMARKS_FOLDER_INFO = "folder_info"; - - // Refresh type for folder refreshing loader. - private static final String BOOKMARKS_REFRESH_TYPE = "refresh_type"; - - // List of bookmarks. - private BookmarksListView mList; - - // Adapter for list of bookmarks. - private BookmarksListAdapter mListAdapter; - - // Adapter's parent stack. - private List mSavedParentStack; - - // Reference to the View to display when there are no results. - private View mEmptyView; - - // Callback for cursor loaders. - private CursorLoaderCallbacks mLoaderCallbacks; - - @Override - public void restoreData(@NonNull Bundle data) { - final ArrayList stack = data.getParcelableArrayList("parentStack"); - if (stack == null) { - return; - } - - if (mListAdapter == null) { - mSavedParentStack = new LinkedList(stack); - } else { - mListAdapter.restoreData(stack); - } - } - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - final View view = inflater.inflate(R.layout.home_bookmarks_panel, container, false); - - mList = (BookmarksListView) view.findViewById(R.id.bookmarks_list); - - mList.setContextMenuInfoFactory(new HomeContextMenuInfo.Factory() { - @Override - public HomeContextMenuInfo makeInfoForCursor(View view, int position, long id, Cursor cursor) { - final int type = cursor.getInt(cursor.getColumnIndexOrThrow(Bookmarks.TYPE)); - if (type == Bookmarks.TYPE_FOLDER) { - // We don't show a context menu for folders - return null; - } - final HomeContextMenuInfo info = new HomeContextMenuInfo(view, position, id); - info.url = cursor.getString(cursor.getColumnIndexOrThrow(Bookmarks.URL)); - info.title = cursor.getString(cursor.getColumnIndexOrThrow(Bookmarks.TITLE)); - info.bookmarkId = cursor.getInt(cursor.getColumnIndexOrThrow(Bookmarks._ID)); - info.itemType = RemoveItemType.BOOKMARKS; - return info; - } - }); - - return view; - } - - @Override - public void onViewCreated(View view, Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - - OnUrlOpenListener listener = null; - try { - listener = (OnUrlOpenListener) getActivity(); - } catch (ClassCastException e) { - throw new ClassCastException(getActivity().toString() - + " must implement HomePager.OnUrlOpenListener"); - } - - mList.setTag(HomePager.LIST_TAG_BOOKMARKS); - mList.setOnUrlOpenListener(listener); - - registerForContextMenu(mList); - } - - @Override - public void onActivityCreated(Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - - final Activity activity = getActivity(); - - // Setup the list adapter. - mListAdapter = new BookmarksListAdapter(activity, null, mSavedParentStack); - mListAdapter.setOnRefreshFolderListener(new OnRefreshFolderListener() { - @Override - public void onRefreshFolder(FolderInfo folderInfo, RefreshType refreshType) { - // Restart the loader with folder as the argument. - Bundle bundle = new Bundle(); - bundle.putParcelable(BOOKMARKS_FOLDER_INFO, folderInfo); - bundle.putParcelable(BOOKMARKS_REFRESH_TYPE, refreshType); - getLoaderManager().restartLoader(LOADER_ID_BOOKMARKS_LIST, bundle, mLoaderCallbacks); - } - }); - mList.setAdapter(mListAdapter); - - // Create callbacks before the initial loader is started. - mLoaderCallbacks = new CursorLoaderCallbacks(); - loadIfVisible(); - } - - @Override - public void onDestroyView() { - mList = null; - mListAdapter = null; - mEmptyView = null; - super.onDestroyView(); - } - - @Override - public void onConfigurationChanged(Configuration newConfig) { - super.onConfigurationChanged(newConfig); - - if (isVisible()) { - // The parent stack is saved just so that the folder state can be - // restored on rotation. - mSavedParentStack = mListAdapter.getParentStack(); - } - } - - @Override - protected void load() { - final Bundle bundle; - if (mSavedParentStack != null && mSavedParentStack.size() > 1) { - bundle = new Bundle(); - bundle.putParcelable(BOOKMARKS_FOLDER_INFO, mSavedParentStack.get(0)); - bundle.putParcelable(BOOKMARKS_REFRESH_TYPE, RefreshType.CHILD); - } else { - bundle = null; - } - - getLoaderManager().initLoader(LOADER_ID_BOOKMARKS_LIST, bundle, mLoaderCallbacks); - } - - private void updateUiFromCursor(Cursor c) { - if ((c == null || c.getCount() == 0) && mEmptyView == null) { - // Set empty page view. We delay this so that the empty view won't flash. - final ViewStub emptyViewStub = (ViewStub) getView().findViewById(R.id.home_empty_view_stub); - mEmptyView = emptyViewStub.inflate(); - - final ImageView emptyIcon = (ImageView) mEmptyView.findViewById(R.id.home_empty_image); - emptyIcon.setImageResource(R.drawable.icon_bookmarks_empty); - - final TextView emptyText = (TextView) mEmptyView.findViewById(R.id.home_empty_text); - emptyText.setText(R.string.home_bookmarks_empty); - - mList.setEmptyView(mEmptyView); - } - } - - /** - * Loader for the list for bookmarks. - */ - private static class BookmarksLoader extends SimpleCursorLoader { - private final FolderInfo mFolderInfo; - private final RefreshType mRefreshType; - private final BrowserDB mDB; - - public BookmarksLoader(Context context) { - this(context, - new FolderInfo(Bookmarks.FIXED_ROOT_ID, context.getResources().getString(R.string.bookmarks_title)), - RefreshType.CHILD); - } - - public BookmarksLoader(Context context, FolderInfo folderInfo, RefreshType refreshType) { - super(context); - mFolderInfo = folderInfo; - mRefreshType = refreshType; - mDB = BrowserDB.from(context); - } - - @Override - public Cursor loadCursor() { - final boolean isRootFolder = mFolderInfo.id == BrowserContract.Bookmarks.FIXED_ROOT_ID; - - final ContentResolver contentResolver = getContext().getContentResolver(); - - Cursor partnerCursor = null; - Cursor userCursor = null; - - if (GeckoSharedPrefs.forProfile(getContext()).getBoolean(GeckoPreferences.PREFS_READ_PARTNER_BOOKMARKS_PROVIDER, false) - && (isRootFolder || mFolderInfo.id <= Bookmarks.FAKE_PARTNER_BOOKMARKS_START)) { - partnerCursor = contentResolver.query(PartnerBookmarksProviderProxy.getUriForBookmarks(getContext(), mFolderInfo.id), null, null, null, null, null); - } - - if (isRootFolder || mFolderInfo.id > Bookmarks.FAKE_PARTNER_BOOKMARKS_START) { - userCursor = mDB.getBookmarksInFolder(contentResolver, mFolderInfo.id); - } - - - if (partnerCursor == null && userCursor == null) { - return null; - } else if (partnerCursor == null) { - return userCursor; - } else if (userCursor == null) { - return partnerCursor; - } else { - return new MergeCursor(new Cursor[] { partnerCursor, userCursor }); - } - } - - @Override - public void onContentChanged() { - // Invalidate the cached value that keeps track of whether or - // not desktop bookmarks exist. - mDB.invalidate(); - super.onContentChanged(); - } - - public FolderInfo getFolderInfo() { - return mFolderInfo; - } - - public RefreshType getRefreshType() { - return mRefreshType; - } - } - - /** - * Loader callbacks for the LoaderManager of this fragment. - */ - private class CursorLoaderCallbacks implements LoaderManager.LoaderCallbacks { - @Override - public Loader onCreateLoader(int id, Bundle args) { - if (args == null) { - return new BookmarksLoader(getActivity()); - } else { - FolderInfo folderInfo = (FolderInfo) args.getParcelable(BOOKMARKS_FOLDER_INFO); - RefreshType refreshType = (RefreshType) args.getParcelable(BOOKMARKS_REFRESH_TYPE); - return new BookmarksLoader(getActivity(), folderInfo, refreshType); - } - } - - @Override - public void onLoadFinished(Loader loader, Cursor c) { - BookmarksLoader bl = (BookmarksLoader) loader; - mListAdapter.swapCursor(c, bl.getFolderInfo(), bl.getRefreshType()); - - if (mPanelStateChangeListener != null) { - final List parentStack = mListAdapter.getParentStack(); - final Bundle bundle = new Bundle(); - - // Bundle likes to store ArrayLists or Arrays, but we've got a generic List (which - // is actually an unmodifiable wrapper around a LinkedList). We'll need to do a - // LinkedList conversion at the other end, when saving we need to use this awkward - // syntax to create an Array. - bundle.putParcelableArrayList("parentStack", new ArrayList(parentStack)); - - mPanelStateChangeListener.onStateChanged(bundle); - } - updateUiFromCursor(c); - } - - @Override - public void onLoaderReset(Loader loader) { - if (mList != null) { - mListAdapter.swapCursor(null); - } - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/BrowserSearch.java b/mobile/android/base/java/org/mozilla/gecko/home/BrowserSearch.java deleted file mode 100644 index 7732932fe3..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/BrowserSearch.java +++ /dev/null @@ -1,1316 +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 java.io.BufferedReader; -import java.io.InputStreamReader; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.EnumSet; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Locale; - -import android.content.SharedPreferences; -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - -import org.mozilla.gecko.EventDispatcher; -import org.mozilla.gecko.GeckoApp; -import org.mozilla.gecko.GeckoAppShell; -import org.mozilla.gecko.GeckoSharedPrefs; -import org.mozilla.gecko.PrefsHelper; -import org.mozilla.gecko.R; -import org.mozilla.gecko.SuggestClient; -import org.mozilla.gecko.Tab; -import org.mozilla.gecko.Tabs; -import org.mozilla.gecko.Telemetry; -import org.mozilla.gecko.TelemetryContract; -import org.mozilla.gecko.annotation.RobocopTarget; -import org.mozilla.gecko.db.BrowserContract; -import org.mozilla.gecko.db.BrowserContract.History; -import org.mozilla.gecko.db.BrowserContract.URLColumns; -import org.mozilla.gecko.home.HomePager.OnUrlOpenListener; -import org.mozilla.gecko.home.SearchLoader.SearchCursorLoader; -import org.mozilla.gecko.preferences.GeckoPreferences; -import org.mozilla.gecko.toolbar.AutocompleteHandler; -import org.mozilla.gecko.util.GeckoEventListener; -import org.mozilla.gecko.util.StringUtils; -import org.mozilla.gecko.util.ThreadUtils; - -import android.app.Activity; -import android.content.ContentResolver; -import android.content.Context; -import android.view.ContextMenu.ContextMenuInfo; -import android.database.Cursor; -import android.net.Uri; -import android.os.Bundle; -import android.support.v4.app.LoaderManager.LoaderCallbacks; -import android.support.v4.content.AsyncTaskLoader; -import android.support.v4.content.Loader; -import android.text.TextUtils; -import android.util.AttributeSet; -import android.util.Log; -import android.view.ContextMenu; -import android.view.LayoutInflater; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.view.MotionEvent; -import android.view.View; -import android.view.View.OnClickListener; -import android.view.ViewGroup; -import android.view.ViewStub; -import android.view.WindowManager.LayoutParams; -import android.view.animation.AccelerateInterpolator; -import android.view.animation.Animation; -import android.view.animation.TranslateAnimation; -import android.widget.AdapterView; -import android.widget.LinearLayout; -import android.widget.ListView; -import android.widget.TextView; - -/** - * Fragment that displays frecency search results in a ListView. - */ -public class BrowserSearch extends HomeFragment - implements GeckoEventListener, - SearchEngineBar.OnSearchBarClickListener { - - @RobocopTarget - public interface SuggestClientFactory { - public SuggestClient getSuggestClient(Context context, String template, int timeout, int max); - } - - @RobocopTarget - public static class DefaultSuggestClientFactory implements SuggestClientFactory { - @Override - public SuggestClient getSuggestClient(Context context, String template, int timeout, int max) { - return new SuggestClient(context, template, timeout, max, true); - } - } - - /** - * Set this to mock the suggestion mechanism. Public for access from tests. - */ - @RobocopTarget - public static volatile SuggestClientFactory sSuggestClientFactory = new DefaultSuggestClientFactory(); - - // Logging tag name - private static final String LOGTAG = "GeckoBrowserSearch"; - - // Cursor loader ID for search query - private static final int LOADER_ID_SEARCH = 0; - - // AsyncTask loader ID for suggestion query - private static final int LOADER_ID_SUGGESTION = 1; - private static final int LOADER_ID_SAVED_SUGGESTION = 2; - - // Timeout for the suggestion client to respond - private static final int SUGGESTION_TIMEOUT = 3000; - - // Maximum number of suggestions from the search engine's suggestion client. This impacts network traffic and device - // data consumption whereas R.integer.max_saved_suggestions controls how many suggestion to show in the UI. - private static final int NETWORK_SUGGESTION_MAX = 3; - - // The maximum number of rows deep in a search we'll dig - // for an autocomplete result - private static final int MAX_AUTOCOMPLETE_SEARCH = 20; - - // Length of https:// + 1 required to make autocomplete - // fill in the domain, for both http:// and https:// - private static final int HTTPS_PREFIX_LENGTH = 9; - - // Duration for fade-in animation - private static final int ANIMATION_DURATION = 250; - - // Holds the current search term to use in the query - private volatile String mSearchTerm; - - // Adapter for the list of search results - private SearchAdapter mAdapter; - - // The view shown by the fragment - private LinearLayout mView; - - // The list showing search results - private HomeListView mList; - - // The bar on the bottom of the screen displaying search engine options. - private SearchEngineBar mSearchEngineBar; - - // Client that performs search suggestion queries. - // Public for testing. - @RobocopTarget - public volatile SuggestClient mSuggestClient; - - // List of search engines from Gecko. - // Do not mutate this list. - // Access to this member must only occur from the UI thread. - private List mSearchEngines; - - // Search history suggestions - private ArrayList mSearchHistorySuggestions; - - // Track the locale that was last in use when we filled mSearchEngines. - // Access to this member must only occur from the UI thread. - private Locale mLastLocale; - - // Whether search suggestions are enabled or not - private boolean mSuggestionsEnabled; - - // Whether history suggestions are enabled or not - private boolean mSavedSearchesEnabled; - - // Callbacks used for the search loader - private CursorLoaderCallbacks mCursorLoaderCallbacks; - - // Callbacks used for the search suggestion loader - private SearchEngineSuggestionLoaderCallbacks mSearchEngineSuggestionLoaderCallbacks; - private SearchHistorySuggestionLoaderCallbacks mSearchHistorySuggestionLoaderCallback; - - // Autocomplete handler used when filtering results - private AutocompleteHandler mAutocompleteHandler; - - // On search listener - private OnSearchListener mSearchListener; - - // On edit suggestion listener - private OnEditSuggestionListener mEditSuggestionListener; - - // Whether the suggestions will fade in when shown - private boolean mAnimateSuggestions; - - // Opt-in prompt view for search suggestions - private View mSuggestionsOptInPrompt; - - public interface OnSearchListener { - void onSearch(SearchEngine engine, String text, TelemetryContract.Method method); - } - - public interface OnEditSuggestionListener { - public void onEditSuggestion(String suggestion); - } - - public static BrowserSearch newInstance() { - BrowserSearch browserSearch = new BrowserSearch(); - - final Bundle args = new Bundle(); - args.putBoolean(HomePager.CAN_LOAD_ARG, true); - browserSearch.setArguments(args); - - return browserSearch; - } - - public BrowserSearch() { - mSearchTerm = ""; - } - - @Override - public void onAttach(Activity activity) { - super.onAttach(activity); - - try { - mSearchListener = (OnSearchListener) activity; - } catch (ClassCastException e) { - throw new ClassCastException(activity.toString() - + " must implement BrowserSearch.OnSearchListener"); - } - - try { - mEditSuggestionListener = (OnEditSuggestionListener) activity; - } catch (ClassCastException e) { - throw new ClassCastException(activity.toString() - + " must implement BrowserSearch.OnEditSuggestionListener"); - } - } - - @Override - public void onDetach() { - super.onDetach(); - - mAutocompleteHandler = null; - mSearchListener = null; - mEditSuggestionListener = null; - } - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - mSearchEngines = new ArrayList(); - mSearchHistorySuggestions = new ArrayList<>(); - } - - @Override - public void onDestroy() { - super.onDestroy(); - - mSearchEngines = null; - } - - @Override - public void onHiddenChanged(boolean hidden) { - if (!hidden) { - final Tab tab = Tabs.getInstance().getSelectedTab(); - final boolean isPrivate = (tab != null && tab.isPrivate()); - - // Removes Search Suggestions Loader if in private browsing mode - // Loader may have been inserted when browsing in normal tab - if (isPrivate) { - getLoaderManager().destroyLoader(LOADER_ID_SUGGESTION); - } - - GeckoAppShell.notifyObservers("SearchEngines:GetVisible", null); - } - super.onHiddenChanged(hidden); - } - - @Override - public void onResume() { - super.onResume(); - - final SharedPreferences prefs = GeckoSharedPrefs.forApp(getContext()); - mSavedSearchesEnabled = prefs.getBoolean(GeckoPreferences.PREFS_HISTORY_SAVED_SEARCH, true); - - // Fetch engines if we need to. - if (mSearchEngines.isEmpty() || !Locale.getDefault().equals(mLastLocale)) { - GeckoAppShell.notifyObservers("SearchEngines:GetVisible", null); - } else { - updateSearchEngineBar(); - } - - Telemetry.startUISession(TelemetryContract.Session.FRECENCY); - } - - @Override - public void onPause() { - super.onPause(); - - Telemetry.stopUISession(TelemetryContract.Session.FRECENCY); - } - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - // All list views are styled to look the same with a global activity theme. - // If the style of the list changes, inflate it from an XML. - mView = (LinearLayout) inflater.inflate(R.layout.browser_search, container, false); - mList = (HomeListView) mView.findViewById(R.id.home_list_view); - mSearchEngineBar = (SearchEngineBar) mView.findViewById(R.id.search_engine_bar); - - return mView; - } - - @Override - public void onDestroyView() { - super.onDestroyView(); - - GeckoApp.getEventDispatcher().unregisterGeckoThreadListener(this, - "SearchEngines:Data"); - - mSearchEngineBar.setAdapter(null); - mSearchEngineBar = null; - - mList.setAdapter(null); - mList = null; - - mView = null; - mSuggestionsOptInPrompt = null; - mSuggestClient = null; - } - - @Override - public void onViewCreated(View view, Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - mList.setTag(HomePager.LIST_TAG_BROWSER_SEARCH); - - mList.setOnItemClickListener(new AdapterView.OnItemClickListener() { - @Override - public void onItemClick(AdapterView parent, View view, int position, long id) { - // Perform the user-entered search if the user clicks on a search engine row. - // This row will be disabled if suggestions (in addition to the user-entered term) are showing. - if (view instanceof SearchEngineRow) { - ((SearchEngineRow) view).performUserEnteredSearch(); - return; - } - - // Account for the search engine rows. - position -= getPrimaryEngineCount(); - final Cursor c = mAdapter.getCursor(position); - final String url = c.getString(c.getColumnIndexOrThrow(URLColumns.URL)); - - Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.LIST_ITEM, "frecency"); - - // This item is a TwoLinePageRow, so we allow switch-to-tab. - mUrlOpenListener.onUrlOpen(url, EnumSet.of(OnUrlOpenListener.Flags.ALLOW_SWITCH_TO_TAB)); - } - }); - - mList.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() { - @Override - public boolean onItemLongClick(AdapterView parent, View view, int position, long id) { - // Don't do anything when the user long-clicks on a search engine row. - if (view instanceof SearchEngineRow) { - return true; - } - - // Account for the search engine rows. - position -= getPrimaryEngineCount(); - return mList.onItemLongClick(parent, view, position, id); - } - }); - - final ListSelectionListener listener = new ListSelectionListener(); - mList.setOnItemSelectedListener(listener); - mList.setOnFocusChangeListener(listener); - - mList.setContextMenuInfoFactory(new HomeContextMenuInfo.Factory() { - @Override - public HomeContextMenuInfo makeInfoForCursor(View view, int position, long id, Cursor cursor) { - final HomeContextMenuInfo info = new HomeContextMenuInfo(view, position, id); - info.url = cursor.getString(cursor.getColumnIndexOrThrow(BrowserContract.Combined.URL)); - info.title = cursor.getString(cursor.getColumnIndexOrThrow(BrowserContract.Combined.TITLE)); - - int bookmarkId = cursor.getInt(cursor.getColumnIndexOrThrow(BrowserContract.Combined.BOOKMARK_ID)); - info.bookmarkId = bookmarkId; - - int historyId = cursor.getInt(cursor.getColumnIndexOrThrow(BrowserContract.Combined.HISTORY_ID)); - info.historyId = historyId; - - boolean isBookmark = bookmarkId != -1; - boolean isHistory = historyId != -1; - - if (isBookmark && isHistory) { - info.itemType = HomeContextMenuInfo.RemoveItemType.COMBINED; - } else if (isBookmark) { - info.itemType = HomeContextMenuInfo.RemoveItemType.BOOKMARKS; - } else if (isHistory) { - info.itemType = HomeContextMenuInfo.RemoveItemType.HISTORY; - } - - return info; - } - }); - - mList.setOnKeyListener(new View.OnKeyListener() { - @Override - public boolean onKey(View v, int keyCode, android.view.KeyEvent event) { - final View selected = mList.getSelectedView(); - - if (selected instanceof SearchEngineRow) { - return selected.onKeyDown(keyCode, event); - } - return false; - } - }); - - registerForContextMenu(mList); - GeckoApp.getEventDispatcher().registerGeckoThreadListener(this, - "SearchEngines:Data"); - - mSearchEngineBar.setOnSearchBarClickListener(this); - } - - @Override - public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfo) { - if (!(menuInfo instanceof HomeContextMenuInfo)) { - return; - } - - HomeContextMenuInfo info = (HomeContextMenuInfo) menuInfo; - - MenuInflater inflater = new MenuInflater(view.getContext()); - inflater.inflate(R.menu.browsersearch_contextmenu, menu); - - menu.setHeaderTitle(info.getDisplayTitle()); - } - - @Override - public boolean onContextItemSelected(MenuItem item) { - ContextMenuInfo menuInfo = item.getMenuInfo(); - if (!(menuInfo instanceof HomeContextMenuInfo)) { - return false; - } - - final HomeContextMenuInfo info = (HomeContextMenuInfo) menuInfo; - final Context context = getActivity(); - - final int itemId = item.getItemId(); - - if (itemId == R.id.browsersearch_remove) { - // Position for Top Sites grid items, but will always be -1 since this is only for BrowserSearch result - final int position = -1; - - new RemoveItemByUrlTask(context, info.url, info.itemType, position).execute(); - return true; - } - - return false; - } - - @Override - public void onActivityCreated(Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - - // Initialize the search adapter - mAdapter = new SearchAdapter(getActivity()); - mList.setAdapter(mAdapter); - - // Only create an instance when we need it - mSearchEngineSuggestionLoaderCallbacks = null; - mSearchHistorySuggestionLoaderCallback = null; - - // Create callbacks before the initial loader is started - mCursorLoaderCallbacks = new CursorLoaderCallbacks(); - loadIfVisible(); - } - - @Override - public void handleMessage(String event, final JSONObject message) { - if (event.equals("SearchEngines:Data")) { - ThreadUtils.postToUiThread(new Runnable() { - @Override - public void run() { - setSearchEngines(message); - } - }); - } - } - - @Override - protected void load() { - SearchLoader.init(getLoaderManager(), LOADER_ID_SEARCH, mCursorLoaderCallbacks, mSearchTerm); - } - - private void handleAutocomplete(String searchTerm, Cursor c) { - if (c == null || - mAutocompleteHandler == null || - TextUtils.isEmpty(searchTerm)) { - return; - } - - // Avoid searching the path if we don't have to. Currently just - // decided by whether there is a '/' character in the string. - final boolean searchPath = searchTerm.indexOf('/') > 0; - final String autocompletion = findAutocompletion(searchTerm, c, searchPath); - - if (autocompletion == null || mAutocompleteHandler == null) { - return; - } - - // Prefetch auto-completed domain since it's a likely target - GeckoAppShell.notifyObservers("Session:Prefetch", "http://" + autocompletion); - - mAutocompleteHandler.onAutocomplete(autocompletion); - mAutocompleteHandler = null; - } - - /** - * Returns the substring of a provided URI, starting at the given offset, - * and extending up to the end of the path segment in which the provided - * index is found. - * - * For example, given - * - * "www.reddit.com/r/boop/abcdef", 0, ? - * - * this method returns - * - * ?=2: "www.reddit.com/" - * ?=17: "www.reddit.com/r/boop/" - * ?=21: "www.reddit.com/r/boop/" - * ?=22: "www.reddit.com/r/boop/abcdef" - * - */ - private static String uriSubstringUpToMatchedPath(final String url, final int offset, final int begin) { - final int afterEnd = url.length(); - - // We want to include the trailing slash, but not other characters. - int chop = url.indexOf('/', begin); - if (chop != -1) { - ++chop; - if (chop < offset) { - // This isn't supposed to happen. Fall back to returning the whole damn thing. - return url; - } - } else { - chop = url.indexOf('?', begin); - if (chop == -1) { - chop = url.indexOf('#', begin); - } - if (chop == -1) { - chop = afterEnd; - } - } - - return url.substring(offset, chop); - } - - LinkedHashSet domains = null; - private LinkedHashSet getDomains() { - if (domains == null) { - domains = new LinkedHashSet(500); - BufferedReader buf = null; - try { - buf = new BufferedReader(new InputStreamReader(getResources().openRawResource(R.raw.topdomains))); - String res = null; - - do { - res = buf.readLine(); - if (res != null) { - domains.add(res); - } - } while (res != null); - } catch (IOException e) { - Log.e(LOGTAG, "Error reading domains", e); - } finally { - if (buf != null) { - try { - buf.close(); - } catch (IOException e) { } - } - } - } - return domains; - } - - private String searchDomains(String search) { - for (String domain : getDomains()) { - if (domain.startsWith(search)) { - return domain; - } - } - return null; - } - - private String findAutocompletion(String searchTerm, Cursor c, boolean searchPath) { - if (!c.moveToFirst()) { - // No cursor probably means no history, so let's try the fallback list. - return searchDomains(searchTerm); - } - - final int searchLength = searchTerm.length(); - final int urlIndex = c.getColumnIndexOrThrow(History.URL); - int searchCount = 0; - - do { - final String url = c.getString(urlIndex); - - if (searchCount == 0) { - // Prefetch the first item in the list since it's weighted the highest - GeckoAppShell.notifyObservers("Session:Prefetch", url); - } - - // Does the completion match against the whole URL? This will match - // about: pages, as well as user input including "http://...". - if (url.startsWith(searchTerm)) { - return uriSubstringUpToMatchedPath(url, 0, - (searchLength > HTTPS_PREFIX_LENGTH) ? searchLength : HTTPS_PREFIX_LENGTH); - } - - final Uri uri = Uri.parse(url); - final String host = uri.getHost(); - - // Host may be null for about pages. - if (host == null) { - continue; - } - - if (host.startsWith(searchTerm)) { - return host + "/"; - } - - final String strippedHost = StringUtils.stripCommonSubdomains(host); - if (strippedHost.startsWith(searchTerm)) { - return strippedHost + "/"; - } - - ++searchCount; - - if (!searchPath) { - continue; - } - - // Otherwise, if we're matching paths, let's compare against the string itself. - final int hostOffset = url.indexOf(strippedHost); - if (hostOffset == -1) { - // This was a URL string that parsed to a different host (normalized?). - // Give up. - continue; - } - - // We already matched the non-stripped host, so now we're - // substring-searching in the part of the URL without the common - // subdomains. - if (url.startsWith(searchTerm, hostOffset)) { - // Great! Return including the rest of the path segment. - return uriSubstringUpToMatchedPath(url, hostOffset, hostOffset + searchLength); - } - } while (searchCount < MAX_AUTOCOMPLETE_SEARCH && c.moveToNext()); - - // If we can't find an autocompletion domain from history, let's try using the fallback list. - return searchDomains(searchTerm); - } - - public void resetScrollState() { - mSearchEngineBar.scrollToPosition(0); - } - - private void filterSuggestions() { - Tab tab = Tabs.getInstance().getSelectedTab(); - final boolean isPrivate = (tab != null && tab.isPrivate()); - - // mSuggestClient may be null if we haven't received our search engine list yet - hence - // we need to exit here in that case. - if (isPrivate || mSuggestClient == null || (!mSuggestionsEnabled && !mSavedSearchesEnabled)) { - mSearchHistorySuggestions.clear(); - return; - } - - // Suggestions from search engine - if (mSearchEngineSuggestionLoaderCallbacks == null) { - mSearchEngineSuggestionLoaderCallbacks = new SearchEngineSuggestionLoaderCallbacks(); - } - getLoaderManager().restartLoader(LOADER_ID_SUGGESTION, null, mSearchEngineSuggestionLoaderCallbacks); - - // Saved suggestions - if (mSearchHistorySuggestionLoaderCallback == null) { - mSearchHistorySuggestionLoaderCallback = new SearchHistorySuggestionLoaderCallbacks(); - } - getLoaderManager().restartLoader(LOADER_ID_SAVED_SUGGESTION, null, mSearchHistorySuggestionLoaderCallback); - } - - private void setSuggestions(ArrayList suggestions) { - ThreadUtils.assertOnUiThread(); - - // mSearchEngines may be null if the setSuggestions calls after onDestroy (bug 1310621). - // So drop the suggestions if search engines are not available - if (mSearchEngines != null && !mSearchEngines.isEmpty()) { - mSearchEngines.get(0).setSuggestions(suggestions); - mAdapter.notifyDataSetChanged(); - } - - } - - private void setSavedSuggestions(ArrayList savedSuggestions) { - ThreadUtils.assertOnUiThread(); - - mSearchHistorySuggestions = savedSuggestions; - mAdapter.notifyDataSetChanged(); - } - - private boolean shouldUpdateSearchEngine(ArrayList searchEngines) { - if (searchEngines.size() != mSearchEngines.size()) { - return true; - } - - int size = searchEngines.size(); - - for (int i = 0; i < size; i++) { - if (!mSearchEngines.get(i).name.equals(searchEngines.get(i).name)) { - return true; - } - } - - return false; - } - - private void setSearchEngines(JSONObject data) { - ThreadUtils.assertOnUiThread(); - - // This method is called via a Runnable posted from the Gecko thread, so - // it's possible the fragment and/or its view has been destroyed by the - // time we get here. If so, just abort. - if (mView == null) { - return; - } - - try { - final JSONObject suggest = data.getJSONObject("suggest"); - final String suggestEngine = suggest.optString("engine", null); - final String suggestTemplate = suggest.optString("template", null); - final boolean suggestionsPrompted = suggest.getBoolean("prompted"); - final JSONArray engines = data.getJSONArray("searchEngines"); - - mSuggestionsEnabled = suggest.getBoolean("enabled"); - - ArrayList searchEngines = new ArrayList(); - for (int i = 0; i < engines.length(); i++) { - final JSONObject engineJSON = engines.getJSONObject(i); - final SearchEngine engine = new SearchEngine((Context) getActivity(), engineJSON); - - if (engine.name.equals(suggestEngine) && suggestTemplate != null) { - // Suggest engine should be at the front of the list. - // We're baking in an assumption here that the suggest engine - // is also the default engine. - searchEngines.add(0, engine); - - ensureSuggestClientIsSet(suggestTemplate); - } else { - searchEngines.add(engine); - } - } - - // checking if the new searchEngine is different from mSearchEngine, will have to re-layout if yes - boolean change = shouldUpdateSearchEngine(searchEngines); - - if (mAdapter != null && change) { - mSearchEngines = Collections.unmodifiableList(searchEngines); - mLastLocale = Locale.getDefault(); - updateSearchEngineBar(); - - mAdapter.notifyDataSetChanged(); - } - - final Tab tab = Tabs.getInstance().getSelectedTab(); - final boolean isPrivate = (tab != null && tab.isPrivate()); - - // Show suggestions opt-in prompt only if suggestions are not enabled yet, - // user hasn't been prompted and we're not on a private browsing tab. - // The prompt might have been inflated already when this view was previously called. - // Remove the opt-in prompt if it has been inflated in the view and dealt with by the user, - // or if we're on a private browsing tab - if (!mSuggestionsEnabled && !suggestionsPrompted && !isPrivate) { - showSuggestionsOptIn(); - } else { - removeSuggestionsOptIn(); - } - - } catch (JSONException e) { - Log.e(LOGTAG, "Error getting search engine JSON", e); - } - - filterSuggestions(); - } - - private void updateSearchEngineBar() { - final int primaryEngineCount = getPrimaryEngineCount(); - - if (primaryEngineCount < mSearchEngines.size()) { - mSearchEngineBar.setSearchEngines( - mSearchEngines.subList(primaryEngineCount, mSearchEngines.size()) - ); - mSearchEngineBar.setVisibility(View.VISIBLE); - } else { - mSearchEngineBar.setVisibility(View.GONE); - } - } - - @Override - public void onSearchBarClickListener(final SearchEngine searchEngine) { - final TelemetryContract.Method method = TelemetryContract.Method.LIST_ITEM; - Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, method, "searchenginebar"); - mSearchListener.onSearch(searchEngine, mSearchTerm, method); - } - - private void ensureSuggestClientIsSet(final String suggestTemplate) { - // Don't update the suggestClient if we already have a client with the correct template - if (mSuggestClient != null && suggestTemplate.equals(mSuggestClient.getSuggestTemplate())) { - return; - } - - mSuggestClient = sSuggestClientFactory.getSuggestClient(getActivity(), suggestTemplate, SUGGESTION_TIMEOUT, NETWORK_SUGGESTION_MAX); - } - - private void showSuggestionsOptIn() { - // Only make the ViewStub visible again if it has already previously been shown. - // (An inflated ViewStub is removed from the View hierarchy so a second call to findViewById will return null, - // which also further necessitates handling this separately.) - if (mSuggestionsOptInPrompt != null) { - mSuggestionsOptInPrompt.setVisibility(View.VISIBLE); - return; - } - - mSuggestionsOptInPrompt = ((ViewStub) mView.findViewById(R.id.suggestions_opt_in_prompt)).inflate(); - - TextView promptText = (TextView) mSuggestionsOptInPrompt.findViewById(R.id.suggestions_prompt_title); - promptText.setText(getResources().getString(R.string.suggestions_prompt)); - - final View yesButton = mSuggestionsOptInPrompt.findViewById(R.id.suggestions_prompt_yes); - final View noButton = mSuggestionsOptInPrompt.findViewById(R.id.suggestions_prompt_no); - - final OnClickListener listener = new OnClickListener() { - @Override - public void onClick(View v) { - // Prevent the buttons from being clicked multiple times (bug 816902) - yesButton.setOnClickListener(null); - noButton.setOnClickListener(null); - - setSuggestionsEnabled(v == yesButton); - } - }; - - yesButton.setOnClickListener(listener); - noButton.setOnClickListener(listener); - - // If the prompt gains focus, automatically pass focus to the - // yes button in the prompt. - final View prompt = mSuggestionsOptInPrompt.findViewById(R.id.prompt); - prompt.setOnFocusChangeListener(new View.OnFocusChangeListener() { - @Override - public void onFocusChange(View v, boolean hasFocus) { - if (hasFocus) { - yesButton.requestFocus(); - } - } - }); - } - - private void removeSuggestionsOptIn() { - if (mSuggestionsOptInPrompt == null) { - return; - } - - mSuggestionsOptInPrompt.setVisibility(View.GONE); - } - - private void setSuggestionsEnabled(final boolean enabled) { - // Clicking the yes/no buttons quickly can cause the click events be - // queued before the listeners are removed above, so it's possible - // setSuggestionsEnabled() can be called twice. mSuggestionsOptInPrompt - // can be null if this happens (bug 828480). - if (mSuggestionsOptInPrompt == null) { - return; - } - - // Make suggestions appear immediately after the user opts in - ThreadUtils.postToBackgroundThread(new Runnable() { - @Override - public void run() { - SuggestClient client = mSuggestClient; - if (client != null) { - client.query(mSearchTerm); - } - } - }); - - PrefsHelper.setPref("browser.search.suggest.prompted", true); - PrefsHelper.setPref("browser.search.suggest.enabled", enabled); - - Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.BUTTON, (enabled ? "suggestions_optin_yes" : "suggestions_optin_no")); - - TranslateAnimation slideAnimation = new TranslateAnimation(0, mSuggestionsOptInPrompt.getWidth(), 0, 0); - slideAnimation.setDuration(ANIMATION_DURATION); - slideAnimation.setInterpolator(new AccelerateInterpolator()); - slideAnimation.setFillAfter(true); - final View prompt = mSuggestionsOptInPrompt.findViewById(R.id.prompt); - - TranslateAnimation shrinkAnimation = new TranslateAnimation(0, 0, 0, -1 * mSuggestionsOptInPrompt.getHeight()); - shrinkAnimation.setDuration(ANIMATION_DURATION); - shrinkAnimation.setFillAfter(true); - shrinkAnimation.setStartOffset(slideAnimation.getDuration()); - shrinkAnimation.setAnimationListener(new Animation.AnimationListener() { - @Override - public void onAnimationStart(Animation a) { - // Increase the height of the view so a gap isn't shown during animation - mView.getLayoutParams().height = mView.getHeight() + - mSuggestionsOptInPrompt.getHeight(); - mView.requestLayout(); - } - - @Override - public void onAnimationRepeat(Animation a) {} - - @Override - public void onAnimationEnd(Animation a) { - // Removing the view immediately results in a NPE in - // dispatchDraw(), possibly because this callback executes - // before drawing is finished. Posting this as a Runnable fixes - // the issue. - mView.post(new Runnable() { - @Override - public void run() { - mView.removeView(mSuggestionsOptInPrompt); - mList.clearAnimation(); - mSuggestionsOptInPrompt = null; - - // Reset the view height - mView.getLayoutParams().height = LayoutParams.MATCH_PARENT; - - // Show search suggestions and update them - if (enabled) { - mSuggestionsEnabled = enabled; - mAnimateSuggestions = true; - mAdapter.notifyDataSetChanged(); - filterSuggestions(); - } - } - }); - } - }); - - prompt.startAnimation(slideAnimation); - mSuggestionsOptInPrompt.startAnimation(shrinkAnimation); - mList.startAnimation(shrinkAnimation); - } - - private int getPrimaryEngineCount() { - return mSearchEngines.size() > 0 ? 1 : 0; - } - - private void restartSearchLoader() { - SearchLoader.restart(getLoaderManager(), LOADER_ID_SEARCH, mCursorLoaderCallbacks, mSearchTerm); - } - - private void initSearchLoader() { - SearchLoader.init(getLoaderManager(), LOADER_ID_SEARCH, mCursorLoaderCallbacks, mSearchTerm); - } - - public void filter(String searchTerm, AutocompleteHandler handler) { - if (TextUtils.isEmpty(searchTerm)) { - return; - } - - final boolean isNewFilter = !TextUtils.equals(mSearchTerm, searchTerm); - - mSearchTerm = searchTerm; - mAutocompleteHandler = handler; - - if (mAdapter != null) { - if (isNewFilter) { - // The adapter depends on the search term to determine its number - // of items. Make it we notify the view about it. - mAdapter.notifyDataSetChanged(); - - // Restart loaders with the new search term - restartSearchLoader(); - filterSuggestions(); - } else { - // The search term hasn't changed, simply reuse any existing - // loader for the current search term. This will ensure autocompletion - // is consistently triggered (see bug 933739). - initSearchLoader(); - } - } - } - - abstract private static class SuggestionAsyncLoader extends AsyncTaskLoader> { - protected final String mSearchTerm; - private ArrayList mSuggestions; - - public SuggestionAsyncLoader(Context context, String searchTerm) { - super(context); - mSearchTerm = searchTerm; - } - - @Override - public void deliverResult(ArrayList suggestions) { - mSuggestions = suggestions; - - if (isStarted()) { - super.deliverResult(mSuggestions); - } - } - - @Override - protected void onStartLoading() { - if (mSuggestions != null) { - deliverResult(mSuggestions); - } - - if (takeContentChanged() || mSuggestions == null) { - forceLoad(); - } - } - - @Override - protected void onStopLoading() { - cancelLoad(); - } - - @Override - protected void onReset() { - super.onReset(); - - onStopLoading(); - mSuggestions = null; - } - } - - private static class SearchEngineSuggestionAsyncLoader extends SuggestionAsyncLoader { - private final SuggestClient mSuggestClient; - - public SearchEngineSuggestionAsyncLoader(Context context, SuggestClient suggestClient, String searchTerm) { - super(context, searchTerm); - mSuggestClient = suggestClient; - } - - @Override - public ArrayList loadInBackground() { - return mSuggestClient.query(mSearchTerm); - } - } - - private static class SearchHistorySuggestionAsyncLoader extends SuggestionAsyncLoader { - public SearchHistorySuggestionAsyncLoader(Context context, String searchTerm) { - super(context, searchTerm); - } - - @Override - public ArrayList loadInBackground() { - final ContentResolver cr = getContext().getContentResolver(); - - String[] columns = new String[] { BrowserContract.SearchHistory.QUERY }; - String actualQuery = BrowserContract.SearchHistory.QUERY + " LIKE ?"; - String[] queryArgs = new String[] { '%' + mSearchTerm + '%' }; - - // For deduplication, the worst case is that all the first NETWORK_SUGGESTION_MAX history suggestions are duplicates - // of search engine suggestions, and the there is a duplicate for the search term itself. A duplicate of the - // search term can occur if the user has previously searched for the same thing. - final int maxSavedSuggestions = NETWORK_SUGGESTION_MAX + 1 + getContext().getResources().getInteger(R.integer.max_saved_suggestions); - - final String sortOrderAndLimit = BrowserContract.SearchHistory.DATE + " DESC LIMIT " + maxSavedSuggestions; - final Cursor result = cr.query(BrowserContract.SearchHistory.CONTENT_URI, columns, actualQuery, queryArgs, sortOrderAndLimit); - - if (result == null) { - return new ArrayList<>(); - } - - final ArrayList savedSuggestions = new ArrayList<>(); - try { - if (result.moveToFirst()) { - final int searchColumn = result.getColumnIndexOrThrow(BrowserContract.SearchHistory.QUERY); - do { - final String savedSearch = result.getString(searchColumn); - savedSuggestions.add(savedSearch); - } while (result.moveToNext()); - } - } finally { - result.close(); - } - - return savedSuggestions; - } - } - - private class SearchAdapter extends MultiTypeCursorAdapter { - private static final int ROW_SEARCH = 0; - private static final int ROW_STANDARD = 1; - private static final int ROW_SUGGEST = 2; - - public SearchAdapter(Context context) { - super(context, null, new int[] { ROW_STANDARD, - ROW_SEARCH, - ROW_SUGGEST }, - new int[] { R.layout.home_item_row, - R.layout.home_search_item_row, - R.layout.home_search_item_row }); - } - - @Override - public int getItemViewType(int position) { - if (position < getPrimaryEngineCount()) { - if (mSuggestionsEnabled && mSearchEngines.get(position).hasSuggestions()) { - // Give suggestion views their own type to prevent them from - // sharing other recycled search result views. Using other - // recycled views for the suggestion row can break animations - // (bug 815937). - - return ROW_SUGGEST; - } else { - return ROW_SEARCH; - } - } - - return ROW_STANDARD; - } - - @Override - public boolean isEnabled(int position) { - // If we're using a gamepad or keyboard, allow the row to be - // focused so it can pass the focus to its child suggestion views. - if (!mList.isInTouchMode()) { - return true; - } - - // If the suggestion row only contains one item (the user-entered - // query), allow the entire row to be clickable; clicking the row - // has the same effect as clicking the single suggestion. If the - // row contains multiple items, clicking the row will do nothing. - - if (position < getPrimaryEngineCount()) { - return !mSearchEngines.get(position).hasSuggestions(); - } - - return true; - } - - // Add the search engines to the number of reported results. - @Override - public int getCount() { - final int resultCount = super.getCount(); - - // Don't show search engines or suggestions if search field is empty - if (TextUtils.isEmpty(mSearchTerm)) { - return resultCount; - } - - return resultCount + getPrimaryEngineCount(); - } - - @Override - public void bindView(View view, Context context, int position) { - final int type = getItemViewType(position); - - if (type == ROW_SEARCH || type == ROW_SUGGEST) { - final SearchEngineRow row = (SearchEngineRow) view; - row.setOnUrlOpenListener(mUrlOpenListener); - row.setOnSearchListener(mSearchListener); - row.setOnEditSuggestionListener(mEditSuggestionListener); - row.setSearchTerm(mSearchTerm); - - final SearchEngine engine = mSearchEngines.get(position); - final boolean haveSuggestions = (engine.hasSuggestions() || !mSearchHistorySuggestions.isEmpty()); - final boolean animate = (mAnimateSuggestions && haveSuggestions); - row.updateSuggestions(mSuggestionsEnabled, engine, mSearchHistorySuggestions, animate); - if (animate) { - // Only animate suggestions the first time they are shown - mAnimateSuggestions = false; - } - } else { - // Account for the search engines - position -= getPrimaryEngineCount(); - - final Cursor c = getCursor(position); - final TwoLinePageRow row = (TwoLinePageRow) view; - row.updateFromCursor(c); - } - } - } - - private class CursorLoaderCallbacks implements LoaderCallbacks { - @Override - public Loader onCreateLoader(int id, Bundle args) { - return SearchLoader.createInstance(getActivity(), args); - } - - @Override - public void onLoadFinished(Loader loader, Cursor c) { - if (mAdapter != null) { - mAdapter.swapCursor(c); - - // We should handle autocompletion based on the search term - // associated with the loader that has just provided - // the results. - SearchCursorLoader searchLoader = (SearchCursorLoader) loader; - handleAutocomplete(searchLoader.getSearchTerm(), c); - } - } - - @Override - public void onLoaderReset(Loader loader) { - if (mAdapter != null) { - mAdapter.swapCursor(null); - } - } - } - - private class SearchEngineSuggestionLoaderCallbacks implements LoaderCallbacks> { - @Override - public Loader> 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 - // synchronously in restartLoader(). - return new SearchEngineSuggestionAsyncLoader(getActivity(), mSuggestClient, mSearchTerm); - } - - @Override - public void onLoadFinished(Loader> loader, ArrayList suggestions) { - setSuggestions(suggestions); - } - - @Override - public void onLoaderReset(Loader> loader) { - setSuggestions(new ArrayList()); - } - } - - private class SearchHistorySuggestionLoaderCallbacks implements LoaderCallbacks> { - @Override - public Loader> 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 - // synchronously in restartLoader(). - return new SearchHistorySuggestionAsyncLoader(getActivity(), mSearchTerm); - } - - @Override - public void onLoadFinished(Loader> loader, ArrayList suggestions) { - setSavedSuggestions(suggestions); - } - - @Override - public void onLoaderReset(Loader> loader) { - setSavedSuggestions(new ArrayList()); - } - } - - private static class ListSelectionListener implements View.OnFocusChangeListener, - AdapterView.OnItemSelectedListener { - private SearchEngineRow mSelectedEngineRow; - - @Override - public void onFocusChange(View v, boolean hasFocus) { - if (hasFocus) { - View selectedRow = ((ListView) v).getSelectedView(); - if (selectedRow != null) { - selectRow(selectedRow); - } - } else { - deselectRow(); - } - } - - @Override - public void onItemSelected(AdapterView parent, View view, int position, long id) { - deselectRow(); - selectRow(view); - } - - @Override - public void onNothingSelected(AdapterView parent) { - deselectRow(); - } - - private void selectRow(View row) { - if (row instanceof SearchEngineRow) { - mSelectedEngineRow = (SearchEngineRow) row; - mSelectedEngineRow.onSelected(); - } - } - - private void deselectRow() { - if (mSelectedEngineRow != null) { - mSelectedEngineRow.onDeselected(); - mSelectedEngineRow = null; - } - } - } - - /** - * HomeSearchListView is a list view for displaying search engine results on the awesome screen. - */ - public static class HomeSearchListView extends HomeListView { - public HomeSearchListView(Context context, AttributeSet attrs) { - this(context, attrs, R.attr.homeListViewStyle); - } - - public HomeSearchListView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - } - - @Override - public boolean onTouchEvent(MotionEvent event) { - if (event.getActionMasked() == MotionEvent.ACTION_DOWN) { - // Dismiss the soft keyboard. - requestFocus(); - } - - return super.onTouchEvent(event); - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/ClientsAdapter.java b/mobile/android/base/java/org/mozilla/gecko/home/ClientsAdapter.java deleted file mode 100644 index f288a27456..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/ClientsAdapter.java +++ /dev/null @@ -1,373 +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.support.annotation.UiThread; -import android.support.v4.util.Pair; -import android.support.v7.widget.RecyclerView; -import android.text.format.DateUtils; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.TextView; -import org.mozilla.gecko.GeckoSharedPrefs; -import org.mozilla.gecko.R; -import org.mozilla.gecko.db.RemoteClient; -import org.mozilla.gecko.db.RemoteTab; - -import java.util.ArrayList; -import java.util.Calendar; -import java.util.Date; -import java.util.GregorianCalendar; -import java.util.HashMap; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; - -import static org.mozilla.gecko.home.CombinedHistoryItem.ItemType.*; - -public class ClientsAdapter extends RecyclerView.Adapter implements CombinedHistoryRecyclerView.AdapterContextMenuBuilder { - public static final String LOGTAG = "GeckoClientsAdapter"; - - /** - * If a device claims to have synced before this date, we will assume it has never synced. - */ - public static final Date EARLIEST_VALID_SYNCED_DATE; - static { - final Calendar c = GregorianCalendar.getInstance(); - c.set(2000, Calendar.JANUARY, 1, 0, 0, 0); - EARLIEST_VALID_SYNCED_DATE = c.getTime(); - } - - List> adapterList = new LinkedList<>(); - - // List of hidden remote clients. - // Only accessed from the UI thread. - protected final List hiddenClients = new ArrayList<>(); - private Map visibleClients = new HashMap<>(); - - // Maintain group collapsed and hidden state. Only accessed from the UI thread. - protected static RemoteTabsExpandableListState sState; - - private final Context context; - - public ClientsAdapter(Context context) { - this.context = context; - - // This races when multiple Fragments are created. That's okay: one - // will win, and thereafter, all will be okay. If we create and then - // drop an instance the shared SharedPreferences backing all the - // instances will maintain the state for us. Since everything happens on - // the UI thread, this doesn't even need to be volatile. - if (sState == null) { - sState = new RemoteTabsExpandableListState(GeckoSharedPrefs.forProfile(context)); - } - - this.setHasStableIds(true); - } - - @Override - public CombinedHistoryItem onCreateViewHolder(ViewGroup parent, int viewType) { - final LayoutInflater inflater = LayoutInflater.from(parent.getContext()); - final View view; - - final CombinedHistoryItem.ItemType itemType = CombinedHistoryItem.ItemType.viewTypeToItemType(viewType); - - switch (itemType) { - case NAVIGATION_BACK: - view = inflater.inflate(R.layout.home_combined_back_item, parent, false); - return new CombinedHistoryItem.HistoryItem(view); - - case CLIENT: - view = inflater.inflate(R.layout.home_remote_tabs_group, parent, false); - return new CombinedHistoryItem.ClientItem(view); - - case CHILD: - view = inflater.inflate(R.layout.home_item_row, parent, false); - return new CombinedHistoryItem.HistoryItem(view); - - case HIDDEN_DEVICES: - view = inflater.inflate(R.layout.home_remote_tabs_hidden_devices, parent, false); - return new CombinedHistoryItem.BasicItem(view); - } - return null; - } - - @Override - public void onBindViewHolder(CombinedHistoryItem holder, final int position) { - final CombinedHistoryItem.ItemType itemType = getItemTypeForPosition(position); - - switch (itemType) { - case CLIENT: - final CombinedHistoryItem.ClientItem clientItem = (CombinedHistoryItem.ClientItem) holder; - final String clientGuid = adapterList.get(position).first; - final RemoteClient client = visibleClients.get(clientGuid); - clientItem.bind(context, client, sState.isClientCollapsed(clientGuid)); - break; - - case CHILD: - final Pair pair = adapterList.get(position); - RemoteTab remoteTab = visibleClients.get(pair.first).tabs.get(pair.second); - ((CombinedHistoryItem.HistoryItem) holder).bind(remoteTab); - break; - - case HIDDEN_DEVICES: - final String hiddenDevicesLabel = context.getResources().getString(R.string.home_remote_tabs_many_hidden_devices, hiddenClients.size()); - ((TextView) holder.itemView).setText(hiddenDevicesLabel); - break; - } - } - - @Override - public int getItemCount () { - return adapterList.size(); - } - - private CombinedHistoryItem.ItemType getItemTypeForPosition(int position) { - if (position == 0) { - return NAVIGATION_BACK; - } - - final Pair pair = adapterList.get(position); - if (pair == null) { - return HIDDEN_DEVICES; - } else if (pair.second == -1) { - return CLIENT; - } else { - return CHILD; - } - } - - @Override - public int getItemViewType(int position) { - return CombinedHistoryItem.ItemType.itemTypeToViewType(getItemTypeForPosition(position)); - } - - @Override - public long getItemId(int position) { - // RecyclerView.NO_ID is -1, so start our hard-coded IDs at -2. - final int NAVIGATION_BACK_ID = -2; - final int HIDDEN_DEVICES_ID = -3; - - final String clientGuid; - // adapterList is a list of tuples (clientGuid, tabId). - final Pair pair = adapterList.get(position); - - switch (getItemTypeForPosition(position)) { - case NAVIGATION_BACK: - return NAVIGATION_BACK_ID; - - case HIDDEN_DEVICES: - return HIDDEN_DEVICES_ID; - - // For Clients, return hashCode of their GUIDs. - case CLIENT: - clientGuid = pair.first; - return clientGuid.hashCode(); - - // For Tabs, return hashCode of their URLs. - case CHILD: - clientGuid = pair.first; - final Integer tabId = pair.second; - - final RemoteClient remoteClient = visibleClients.get(clientGuid); - if (remoteClient == null) { - return RecyclerView.NO_ID; - } - - final RemoteTab remoteTab = remoteClient.tabs.get(tabId); - if (remoteTab == null) { - return RecyclerView.NO_ID; - } - - return remoteTab.url.hashCode(); - - default: - throw new IllegalStateException("Unexpected Home Panel item type"); - } - } - - public int getClientsCount() { - return hiddenClients.size() + visibleClients.size(); - } - - @UiThread - public void setClients(List clients) { - adapterList.clear(); - adapterList.add(null); - - hiddenClients.clear(); - visibleClients.clear(); - - for (RemoteClient client : clients) { - final String guid = client.guid; - if (sState.isClientHidden(guid)) { - hiddenClients.add(client); - } else { - visibleClients.put(guid, client); - adapterList.addAll(getVisibleItems(client)); - } - } - - // Add item for unhiding clients. - if (!hiddenClients.isEmpty()) { - adapterList.add(null); - } - - notifyDataSetChanged(); - } - - private static List> getVisibleItems(RemoteClient client) { - List> list = new LinkedList<>(); - final String guid = client.guid; - list.add(new Pair<>(guid, -1)); - if (!sState.isClientCollapsed(client.guid)) { - for (int i = 0; i < client.tabs.size(); i++) { - list.add(new Pair<>(guid, i)); - } - } - return list; - } - - public List getHiddenClients() { - return hiddenClients; - } - - public void toggleClient(int position) { - final Pair pair = adapterList.get(position); - if (pair.second != -1) { - return; - } - - final String clientGuid = pair.first; - final RemoteClient client = visibleClients.get(clientGuid); - - final boolean isCollapsed = sState.isClientCollapsed(clientGuid); - - sState.setClientCollapsed(clientGuid, !isCollapsed); - notifyItemChanged(position); - - if (isCollapsed) { - for (int i = client.tabs.size() - 1; i > -1; i--) { - // Insert child tabs at the index right after the client item that was clicked. - adapterList.add(position + 1, new Pair<>(clientGuid, i)); - } - notifyItemRangeInserted(position + 1, client.tabs.size()); - } else { - int i = client.tabs.size(); - while (i > 0) { - adapterList.remove(position + 1); - i--; - } - notifyItemRangeRemoved(position + 1, client.tabs.size()); - } - } - - public void unhideClients(List selectedClients) { - final int numClients = selectedClients.size(); - if (numClients == 0) { - return; - } - - final int insertionIndex = adapterList.size() - 1; - int itemCount = numClients; - - for (RemoteClient client : selectedClients) { - final String clientGuid = client.guid; - - sState.setClientHidden(clientGuid, false); - hiddenClients.remove(client); - - visibleClients.put(clientGuid, client); - sState.setClientCollapsed(clientGuid, false); - adapterList.addAll(adapterList.size() - 1, getVisibleItems(client)); - - itemCount += client.tabs.size(); - } - - notifyItemRangeInserted(insertionIndex, itemCount); - - final int hiddenDevicesIndex = adapterList.size() - 1; - if (hiddenClients.isEmpty()) { - // No more hidden clients, remove "unhide" item. - adapterList.remove(hiddenDevicesIndex); - notifyItemRemoved(hiddenDevicesIndex); - } else { - // Update "hidden clients" item because number of hidden clients changed. - notifyItemChanged(hiddenDevicesIndex); - } - } - - public void removeItem(int position) { - final CombinedHistoryItem.ItemType itemType = getItemTypeForPosition(position); - switch (itemType) { - case CLIENT: - final String clientGuid = adapterList.get(position).first; - final RemoteClient client = visibleClients.remove(clientGuid); - final boolean hadHiddenClients = !hiddenClients.isEmpty(); - - int removeCount = sState.isClientCollapsed(clientGuid) ? 1 : client.tabs.size() + 1; - int c = removeCount; - while (c > 0) { - adapterList.remove(position); - c--; - } - notifyItemRangeRemoved(position, removeCount); - - sState.setClientHidden(clientGuid, true); - hiddenClients.add(client); - - if (!hadHiddenClients) { - // Add item for unhiding clients; - adapterList.add(null); - notifyItemInserted(adapterList.size() - 1); - } else { - // Update "hidden clients" item because number of hidden clients changed. - notifyItemChanged(adapterList.size() - 1); - } - break; - } - } - - @Override - public HomeContextMenuInfo makeContextMenuInfoFromPosition(View view, int position) { - final CombinedHistoryItem.ItemType itemType = getItemTypeForPosition(position); - HomeContextMenuInfo info; - final Pair pair = adapterList.get(position); - switch (itemType) { - case CHILD: - info = new HomeContextMenuInfo(view, position, -1); - return populateChildInfoFromTab(info, visibleClients.get(pair.first).tabs.get(pair.second)); - - case CLIENT: - info = new CombinedHistoryPanel.RemoteTabsClientContextMenuInfo(view, position, -1, visibleClients.get(pair.first)); - return info; - } - return null; - } - - protected static HomeContextMenuInfo populateChildInfoFromTab(HomeContextMenuInfo info, RemoteTab tab) { - info.url = tab.url; - info.title = tab.title; - return info; - } - - /** - * Return a relative "Last synced" time span for the given tab record. - * - * @param now local time. - * @param time to format string for. - * @return string describing time span - */ - public static String getLastSyncedString(Context context, long now, long time) { - if (new Date(time).before(EARLIEST_VALID_SYNCED_DATE)) { - return context.getString(R.string.remote_tabs_never_synced); - } - final CharSequence relativeTimeSpanString = DateUtils.getRelativeTimeSpanString(time, now, DateUtils.MINUTE_IN_MILLIS); - return context.getResources().getString(R.string.remote_tabs_last_synced, relativeTimeSpanString); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/CombinedHistoryAdapter.java b/mobile/android/base/java/org/mozilla/gecko/home/CombinedHistoryAdapter.java deleted file mode 100644 index 402ed26e7b..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/CombinedHistoryAdapter.java +++ /dev/null @@ -1,433 +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.res.Resources; -import android.support.annotation.UiThread; -import android.support.v7.widget.LinearLayoutManager; -import android.support.v7.widget.RecyclerView; - -import android.database.Cursor; -import android.util.SparseArray; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.TextView; -import org.mozilla.gecko.R; -import org.mozilla.gecko.db.BrowserContract; -import org.mozilla.gecko.util.ThreadUtils; - -public class CombinedHistoryAdapter extends RecyclerView.Adapter implements CombinedHistoryRecyclerView.AdapterContextMenuBuilder { - private static final int RECENT_TABS_SMARTFOLDER_INDEX = 0; - - // Array for the time ranges in milliseconds covered by each section. - static final HistorySectionsHelper.SectionDateRange[] sectionDateRangeArray = new HistorySectionsHelper.SectionDateRange[SectionHeader.values().length]; - - // Semantic names for the time covered by each section - public enum SectionHeader { - TODAY, - YESTERDAY, - WEEK, - THIS_MONTH, - MONTH_AGO, - TWO_MONTHS_AGO, - THREE_MONTHS_AGO, - FOUR_MONTHS_AGO, - FIVE_MONTHS_AGO, - OLDER_THAN_SIX_MONTHS - } - - private HomeFragment.PanelStateChangeListener panelStateChangeListener; - - private Cursor historyCursor; - private DevicesUpdateHandler devicesUpdateHandler; - private int deviceCount = 0; - private RecentTabsUpdateHandler recentTabsUpdateHandler; - private int recentTabsCount = 0; - - private LinearLayoutManager linearLayoutManager; // Only used on the UI thread, so no need to be volatile. - - // We use a sparse array to store each section header's position in the panel [more cheaply than a HashMap]. - private final SparseArray sectionHeaders; - - public CombinedHistoryAdapter(Resources resources, int cachedRecentTabsCount) { - super(); - recentTabsCount = cachedRecentTabsCount; - sectionHeaders = new SparseArray<>(); - HistorySectionsHelper.updateRecentSectionOffset(resources, sectionDateRangeArray); - this.setHasStableIds(true); - } - - public void setPanelStateChangeListener( - HomeFragment.PanelStateChangeListener panelStateChangeListener) { - this.panelStateChangeListener = panelStateChangeListener; - } - - @UiThread - public void setLinearLayoutManager(LinearLayoutManager linearLayoutManager) { - this.linearLayoutManager = linearLayoutManager; - } - - public void setHistory(Cursor history) { - historyCursor = history; - populateSectionHeaders(historyCursor, sectionHeaders); - notifyDataSetChanged(); - } - - public interface DevicesUpdateHandler { - void onDeviceCountUpdated(int count); - } - - public DevicesUpdateHandler getDeviceUpdateHandler() { - if (devicesUpdateHandler == null) { - devicesUpdateHandler = new DevicesUpdateHandler() { - @Override - public void onDeviceCountUpdated(int count) { - deviceCount = count; - notifyItemChanged(getSyncedDevicesSmartFolderIndex()); - } - }; - } - return devicesUpdateHandler; - } - - public interface RecentTabsUpdateHandler { - void onRecentTabsCountUpdated(int count, boolean countReliable); - } - - public RecentTabsUpdateHandler getRecentTabsUpdateHandler() { - if (recentTabsUpdateHandler != null) { - return recentTabsUpdateHandler; - } - - recentTabsUpdateHandler = new RecentTabsUpdateHandler() { - @Override - public void onRecentTabsCountUpdated(final int count, final boolean countReliable) { - // Now that other items can move around depending on the visibility of the - // Recent Tabs folder, only update the recentTabsCount on the UI thread. - ThreadUtils.postToUiThread(new Runnable() { - @UiThread - @Override - public void run() { - if (!countReliable && count <= recentTabsCount) { - // The final tab count (where countReliable = true) is normally >= than - // previous values with countReliable = false. Hence we only want to - // update the displayed tab count with a preliminary value if it's larger - // than the previous count, so as to avoid the displayed count jumping - // downwards and then back up, as well as unnecessary folder animations. - return; - } - - final boolean prevFolderVisibility = isRecentTabsFolderVisible(); - recentTabsCount = count; - final boolean folderVisible = isRecentTabsFolderVisible(); - - if (prevFolderVisibility == folderVisible) { - if (prevFolderVisibility) { - notifyItemChanged(RECENT_TABS_SMARTFOLDER_INDEX); - } - return; - } - - // If the Recent Tabs smart folder has become hidden/unhidden, - // we need to recalculate the history section header positions. - populateSectionHeaders(historyCursor, sectionHeaders); - - if (folderVisible) { - int scrollPos = -1; - if (linearLayoutManager != null) { - scrollPos = linearLayoutManager.findFirstCompletelyVisibleItemPosition(); - } - - notifyItemInserted(RECENT_TABS_SMARTFOLDER_INDEX); - // If the list exceeds the display height and we want to show the new - // item inserted at position 0, we need to scroll up manually - // (see https://code.google.com/p/android/issues/detail?id=174227#c2). - // However we only do this if our current scroll position is at the - // top of the list. - if (linearLayoutManager != null && scrollPos == 0) { - linearLayoutManager.scrollToPosition(0); - } - } else { - notifyItemRemoved(RECENT_TABS_SMARTFOLDER_INDEX); - } - - if (countReliable && panelStateChangeListener != null) { - panelStateChangeListener.setCachedRecentTabsCount(recentTabsCount); - } - } - }); - } - }; - return recentTabsUpdateHandler; - } - - @UiThread - private boolean isRecentTabsFolderVisible() { - return recentTabsCount > 0; - } - - @UiThread - // Number of smart folders for determining practical empty state. - public int getNumVisibleSmartFolders() { - int visibleFolders = 1; // Synced devices folder is always visible. - - if (isRecentTabsFolderVisible()) { - visibleFolders += 1; - } - - return visibleFolders; - } - - @UiThread - private int getSyncedDevicesSmartFolderIndex() { - return isRecentTabsFolderVisible() ? - RECENT_TABS_SMARTFOLDER_INDEX + 1 : - RECENT_TABS_SMARTFOLDER_INDEX; - } - - @Override - public CombinedHistoryItem onCreateViewHolder(ViewGroup viewGroup, int viewType) { - final LayoutInflater inflater = LayoutInflater.from(viewGroup.getContext()); - final View view; - - final CombinedHistoryItem.ItemType itemType = CombinedHistoryItem.ItemType.viewTypeToItemType(viewType); - - switch (itemType) { - case RECENT_TABS: - case SYNCED_DEVICES: - view = inflater.inflate(R.layout.home_smartfolder, viewGroup, false); - return new CombinedHistoryItem.SmartFolder(view); - - case SECTION_HEADER: - view = inflater.inflate(R.layout.home_header_row, viewGroup, false); - return new CombinedHistoryItem.BasicItem(view); - - case HISTORY: - view = inflater.inflate(R.layout.home_item_row, viewGroup, false); - return new CombinedHistoryItem.HistoryItem(view); - default: - throw new IllegalArgumentException("Unexpected Home Panel item type"); - } - } - - @Override - public void onBindViewHolder(CombinedHistoryItem viewHolder, int position) { - final CombinedHistoryItem.ItemType itemType = getItemTypeForPosition(position); - final int localPosition = transformAdapterPositionForDataStructure(itemType, position); - - switch (itemType) { - case RECENT_TABS: - ((CombinedHistoryItem.SmartFolder) viewHolder).bind(R.drawable.icon_recent, R.string.home_closed_tabs_title2, R.string.home_closed_tabs_one, R.string.home_closed_tabs_number, recentTabsCount); - break; - - case SYNCED_DEVICES: - ((CombinedHistoryItem.SmartFolder) viewHolder).bind(R.drawable.cloud, R.string.home_synced_devices_smartfolder, R.string.home_synced_devices_one, R.string.home_synced_devices_number, deviceCount); - break; - - case SECTION_HEADER: - ((TextView) viewHolder.itemView).setText(getSectionHeaderTitle(sectionHeaders.get(localPosition))); - break; - - case HISTORY: - if (historyCursor == null || !historyCursor.moveToPosition(localPosition)) { - throw new IllegalStateException("Couldn't move cursor to position " + localPosition); - } - ((CombinedHistoryItem.HistoryItem) viewHolder).bind(historyCursor); - break; - } - } - - /** - * Transform an adapter position to the position for the data structure backing the item type. - * - * The type is not strictly necessary and could be fetched from getItemTypeForPosition, - * but is used for explicitness. - * - * @param type ItemType of the item - * @param position position in the adapter - * @return position of the item in the data structure - */ - @UiThread - private int transformAdapterPositionForDataStructure(CombinedHistoryItem.ItemType type, int position) { - if (type == CombinedHistoryItem.ItemType.SECTION_HEADER) { - return position; - } else if (type == CombinedHistoryItem.ItemType.HISTORY) { - return position - getHeadersBefore(position) - getNumVisibleSmartFolders(); - } else { - return position; - } - } - - @UiThread - private CombinedHistoryItem.ItemType getItemTypeForPosition(int position) { - if (position == RECENT_TABS_SMARTFOLDER_INDEX && isRecentTabsFolderVisible()) { - return CombinedHistoryItem.ItemType.RECENT_TABS; - } - if (position == getSyncedDevicesSmartFolderIndex()) { - return CombinedHistoryItem.ItemType.SYNCED_DEVICES; - } - final int sectionPosition = transformAdapterPositionForDataStructure(CombinedHistoryItem.ItemType.SECTION_HEADER, position); - if (sectionHeaders.get(sectionPosition) != null) { - return CombinedHistoryItem.ItemType.SECTION_HEADER; - } - return CombinedHistoryItem.ItemType.HISTORY; - } - - @UiThread - @Override - public int getItemViewType(int position) { - return CombinedHistoryItem.ItemType.itemTypeToViewType(getItemTypeForPosition(position)); - } - - @UiThread - @Override - public int getItemCount() { - final int historySize = historyCursor == null ? 0 : historyCursor.getCount(); - return historySize + sectionHeaders.size() + getNumVisibleSmartFolders(); - } - - /** - * Returns stable ID for each position. Data behind historyCursor is a sorted Combined view. - * - * @param position view item position for which to generate a stable ID - * @return stable ID for given position - */ - @UiThread - @Override - public long getItemId(int position) { - // Two randomly selected large primes used to generate non-clashing IDs. - final long PRIME_BOOKMARKS = 32416189867L; - final long PRIME_SECTION_HEADERS = 32416187737L; - - // RecyclerView.NO_ID is -1, so let's start from -2 for our hard-coded IDs. - final int RECENT_TABS_ID = -2; - final int SYNCED_DEVICES_ID = -3; - - switch (getItemTypeForPosition(position)) { - case RECENT_TABS: - return RECENT_TABS_ID; - case SYNCED_DEVICES: - return SYNCED_DEVICES_ID; - case SECTION_HEADER: - // We might have multiple section headers, so we try get unique IDs for them. - return position * PRIME_SECTION_HEADERS; - case HISTORY: - final int historyPosition = transformAdapterPositionForDataStructure( - CombinedHistoryItem.ItemType.HISTORY, position); - if (!historyCursor.moveToPosition(historyPosition)) { - return RecyclerView.NO_ID; - } - - final int historyIdCol = historyCursor.getColumnIndexOrThrow(BrowserContract.Combined.HISTORY_ID); - final long historyId = historyCursor.getLong(historyIdCol); - - if (historyId != -1) { - return historyId; - } - - final int bookmarkIdCol = historyCursor.getColumnIndexOrThrow(BrowserContract.Combined.BOOKMARK_ID); - final long bookmarkId = historyCursor.getLong(bookmarkIdCol); - - // Avoid clashing with historyId. - return bookmarkId * PRIME_BOOKMARKS; - default: - throw new IllegalStateException("Unexpected Home Panel item type"); - } - } - - /** - * Add only the SectionHeaders that have history items within their range to a SparseArray, where the - * array index is the position of the header in the history-only (no clients) ordering. - * @param c data Cursor - * @param sparseArray SparseArray to populate - */ - @UiThread - private void populateSectionHeaders(Cursor c, SparseArray sparseArray) { - ThreadUtils.assertOnUiThread(); - - sparseArray.clear(); - - if (c == null || !c.moveToFirst()) { - return; - } - - SectionHeader section = null; - - do { - final int historyPosition = c.getPosition(); - final long visitTime = c.getLong(c.getColumnIndexOrThrow(BrowserContract.History.DATE_LAST_VISITED)); - final SectionHeader itemSection = getSectionFromTime(visitTime); - - if (section != itemSection) { - section = itemSection; - sparseArray.append(historyPosition + sparseArray.size() + getNumVisibleSmartFolders(), section); - } - - if (section == SectionHeader.OLDER_THAN_SIX_MONTHS) { - break; - } - } while (c.moveToNext()); - } - - private static String getSectionHeaderTitle(SectionHeader section) { - return sectionDateRangeArray[section.ordinal()].displayName; - } - - private static SectionHeader getSectionFromTime(long time) { - for (int i = 0; i < SectionHeader.OLDER_THAN_SIX_MONTHS.ordinal(); i++) { - if (time > sectionDateRangeArray[i].start) { - return SectionHeader.values()[i]; - } - } - - return SectionHeader.OLDER_THAN_SIX_MONTHS; - } - - /** - * Returns the number of section headers before the given history item at the adapter position. - * @param position position in the adapter - */ - private int getHeadersBefore(int position) { - // Skip the first header case because there will always be a header. - for (int i = 1; i < sectionHeaders.size(); i++) { - // If the position of the header is greater than the history position, - // return the number of headers tested. - if (sectionHeaders.keyAt(i) > position) { - return i; - } - } - return sectionHeaders.size(); - } - - @Override - public HomeContextMenuInfo makeContextMenuInfoFromPosition(View view, int position) { - final CombinedHistoryItem.ItemType itemType = getItemTypeForPosition(position); - if (itemType == CombinedHistoryItem.ItemType.HISTORY) { - final HomeContextMenuInfo info = new HomeContextMenuInfo(view, position, -1); - - historyCursor.moveToPosition(transformAdapterPositionForDataStructure(CombinedHistoryItem.ItemType.HISTORY, position)); - return populateHistoryInfoFromCursor(info, historyCursor); - } - return null; - } - - protected static HomeContextMenuInfo populateHistoryInfoFromCursor(HomeContextMenuInfo info, Cursor cursor) { - info.url = cursor.getString(cursor.getColumnIndexOrThrow(BrowserContract.Combined.URL)); - info.title = cursor.getString(cursor.getColumnIndexOrThrow(BrowserContract.Combined.TITLE)); - info.historyId = cursor.getInt(cursor.getColumnIndexOrThrow(BrowserContract.Combined.HISTORY_ID)); - info.itemType = HomeContextMenuInfo.RemoveItemType.HISTORY; - final int bookmarkIdCol = cursor.getColumnIndexOrThrow(BrowserContract.Combined.BOOKMARK_ID); - if (cursor.isNull(bookmarkIdCol)) { - // If this is a combined cursor, we may get a history item without a - // bookmark, in which case the bookmarks ID column value will be null. - info.bookmarkId = -1; - } else { - info.bookmarkId = cursor.getInt(bookmarkIdCol); - } - return info; - } - -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/CombinedHistoryItem.java b/mobile/android/base/java/org/mozilla/gecko/home/CombinedHistoryItem.java deleted file mode 100644 index a2c1b72c24..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/CombinedHistoryItem.java +++ /dev/null @@ -1,127 +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.support.v4.content.ContextCompat; -import android.support.v7.widget.RecyclerView; -import android.util.Log; -import android.view.View; -import android.widget.ImageView; -import android.widget.TextView; -import org.mozilla.gecko.R; -import org.mozilla.gecko.db.RemoteClient; -import org.mozilla.gecko.db.RemoteTab; -import org.mozilla.gecko.home.RecentTabsAdapter.ClosedTab; - -public abstract class CombinedHistoryItem extends RecyclerView.ViewHolder { - private static final String LOGTAG = "CombinedHistoryItem"; - - public CombinedHistoryItem(View view) { - super(view); - } - - public enum ItemType { - CLIENT, HIDDEN_DEVICES, SECTION_HEADER, HISTORY, NAVIGATION_BACK, CHILD, SYNCED_DEVICES, - RECENT_TABS, CLOSED_TAB; - - public static ItemType viewTypeToItemType(int viewType) { - if (viewType >= ItemType.values().length) { - Log.e(LOGTAG, "No corresponding ItemType!"); - } - return ItemType.values()[viewType]; - } - - public static int itemTypeToViewType(ItemType itemType) { - return itemType.ordinal(); - } - } - - public static class BasicItem extends CombinedHistoryItem { - public BasicItem(View view) { - super(view); - } - } - - public static class SmartFolder extends CombinedHistoryItem { - final Context context; - final ImageView icon; - final TextView title; - final TextView subtext; - - public SmartFolder(View view) { - super(view); - context = view.getContext(); - - icon = (ImageView) view.findViewById(R.id.device_type); - title = (TextView) view.findViewById(R.id.title); - subtext = (TextView) view.findViewById(R.id.subtext); - } - - public void bind(int drawableRes, int titleRes, int singleDeviceRes, int multiDeviceRes, int numDevices) { - icon.setImageResource(drawableRes); - title.setText(titleRes); - final String subtitle = numDevices == 1 ? context.getString(singleDeviceRes) : context.getString(multiDeviceRes, numDevices); - subtext.setText(subtitle); - } - } - - public static class HistoryItem extends CombinedHistoryItem { - public HistoryItem(View view) { - super(view); - } - - public void bind(Cursor historyCursor) { - final TwoLinePageRow pageRow = (TwoLinePageRow) this.itemView; - pageRow.setShowIcons(true); - pageRow.updateFromCursor(historyCursor); - } - - public void bind(RemoteTab remoteTab) { - final TwoLinePageRow childPageRow = (TwoLinePageRow) this.itemView; - childPageRow.setShowIcons(true); - childPageRow.update(remoteTab.title, remoteTab.url); - } - - public void bind(ClosedTab closedTab) { - final TwoLinePageRow childPageRow = (TwoLinePageRow) this.itemView; - childPageRow.setShowIcons(false); - childPageRow.update(closedTab.title, closedTab.url); - } - } - - public static class ClientItem extends CombinedHistoryItem { - final TextView nameView; - final ImageView deviceTypeView; - final TextView lastModifiedView; - final ImageView deviceExpanded; - - public ClientItem(View view) { - super(view); - nameView = (TextView) view.findViewById(R.id.client); - deviceTypeView = (ImageView) view.findViewById(R.id.device_type); - lastModifiedView = (TextView) view.findViewById(R.id.last_synced); - deviceExpanded = (ImageView) view.findViewById(R.id.device_expanded); - } - - public void bind(Context context, RemoteClient client, boolean isCollapsed) { - this.nameView.setText(client.name); - final long now = System.currentTimeMillis(); - this.lastModifiedView.setText(ClientsAdapter.getLastSyncedString(context, now, client.lastModified)); - - if (client.isDesktop()) { - deviceTypeView.setImageResource(isCollapsed ? R.drawable.sync_desktop_inactive : R.drawable.sync_desktop); - } else { - deviceTypeView.setImageResource(isCollapsed ? R.drawable.sync_mobile_inactive : R.drawable.sync_mobile); - } - - nameView.setTextColor(ContextCompat.getColor(context, isCollapsed ? R.color.tabs_tray_icon_grey : R.color.placeholder_active_grey)); - if (client.tabs.size() > 0) { - deviceExpanded.setImageResource(isCollapsed ? R.drawable.home_group_collapsed : R.drawable.arrow_down); - } - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/CombinedHistoryPanel.java b/mobile/android/base/java/org/mozilla/gecko/home/CombinedHistoryPanel.java deleted file mode 100644 index c9afecd636..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/CombinedHistoryPanel.java +++ /dev/null @@ -1,697 +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.accounts.Account; -import android.app.AlertDialog; -import android.content.ContentResolver; -import android.content.Context; -import android.content.DialogInterface; -import android.content.Intent; -import android.database.Cursor; -import android.os.Bundle; -import android.support.annotation.UiThread; -import android.support.v4.app.LoaderManager; -import android.support.v4.content.Loader; -import android.support.v4.widget.SwipeRefreshLayout; -import android.support.v7.widget.DefaultItemAnimator; -import android.support.v7.widget.LinearLayoutManager; -import android.support.v7.widget.RecyclerView; -import android.text.SpannableStringBuilder; -import android.text.TextPaint; -import android.text.method.LinkMovementMethod; -import android.text.style.ClickableSpan; -import android.text.style.UnderlineSpan; -import android.util.Log; -import android.view.ContextMenu; -import android.view.LayoutInflater; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; -import android.widget.Button; -import android.widget.ImageView; -import android.widget.TextView; -import org.json.JSONException; -import org.json.JSONObject; -import org.mozilla.gecko.EventDispatcher; -import org.mozilla.gecko.GeckoApp; -import org.mozilla.gecko.GeckoAppShell; -import org.mozilla.gecko.GeckoProfile; -import org.mozilla.gecko.R; -import org.mozilla.gecko.RemoteClientsDialogFragment; -import org.mozilla.gecko.fxa.FirefoxAccounts; -import org.mozilla.gecko.fxa.FxAccountConstants; -import org.mozilla.gecko.fxa.SyncStatusListener; -import org.mozilla.gecko.home.CombinedHistoryPanel.OnPanelLevelChangeListener.PanelLevel; -import org.mozilla.gecko.restrictions.Restrictions; -import org.mozilla.gecko.Telemetry; -import org.mozilla.gecko.TelemetryContract; -import org.mozilla.gecko.db.BrowserDB; -import org.mozilla.gecko.db.RemoteClient; -import org.mozilla.gecko.restrictions.Restrictable; -import org.mozilla.gecko.widget.HistoryDividerItemDecoration; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -import static org.mozilla.gecko.home.CombinedHistoryPanel.OnPanelLevelChangeListener.PanelLevel.PARENT; -import static org.mozilla.gecko.home.CombinedHistoryPanel.OnPanelLevelChangeListener.PanelLevel.CHILD_SYNC; -import static org.mozilla.gecko.home.CombinedHistoryPanel.OnPanelLevelChangeListener.PanelLevel.CHILD_RECENT_TABS; - -public class CombinedHistoryPanel extends HomeFragment implements RemoteClientsDialogFragment.RemoteClientsListener { - private static final String LOGTAG = "GeckoCombinedHistoryPnl"; - - private static final String[] STAGES_TO_SYNC_ON_REFRESH = new String[] { "clients", "tabs" }; - private final int LOADER_ID_HISTORY = 0; - private final int LOADER_ID_REMOTE = 1; - - // String placeholders to mark formatting. - private final static String FORMAT_S1 = "%1$s"; - private final static String FORMAT_S2 = "%2$s"; - - private CombinedHistoryRecyclerView mRecyclerView; - private CombinedHistoryAdapter mHistoryAdapter; - private ClientsAdapter mClientsAdapter; - private RecentTabsAdapter mRecentTabsAdapter; - private CursorLoaderCallbacks mCursorLoaderCallbacks; - - private Bundle mSavedRestoreBundle; - - private PanelLevel mPanelLevel; - private Button mPanelFooterButton; - - private PanelStateUpdateHandler mPanelStateUpdateHandler; - - // Child refresh layout view. - protected SwipeRefreshLayout mRefreshLayout; - - // Sync listener that stops refreshing when a sync is completed. - protected RemoteTabsSyncListener mSyncStatusListener; - - // Reference to the View to display when there are no results. - private View mHistoryEmptyView; - private View mClientsEmptyView; - private View mRecentTabsEmptyView; - - public interface OnPanelLevelChangeListener { - enum PanelLevel { - PARENT, CHILD_SYNC, CHILD_RECENT_TABS - } - - /** - * Propagates level changes. - * @param level - * @return true if level changed, false otherwise. - */ - boolean changeLevel(PanelLevel level); - } - - @Override - public void onCreate(Bundle savedInstance) { - super.onCreate(savedInstance); - - int cachedRecentTabsCount = 0; - if (mPanelStateChangeListener != null ) { - cachedRecentTabsCount = mPanelStateChangeListener.getCachedRecentTabsCount(); - } - mHistoryAdapter = new CombinedHistoryAdapter(getResources(), cachedRecentTabsCount); - if (mPanelStateChangeListener != null) { - mHistoryAdapter.setPanelStateChangeListener(mPanelStateChangeListener); - } - - mClientsAdapter = new ClientsAdapter(getContext()); - // The RecentTabsAdapter doesn't use a cursor and therefore can't use the CursorLoader's - // onLoadFinished() callback for updating the panel state when the closed tab count changes. - // Instead, we provide it with independent callbacks as necessary. - mRecentTabsAdapter = new RecentTabsAdapter(getContext(), - mHistoryAdapter.getRecentTabsUpdateHandler(), getPanelStateUpdateHandler()); - - mSyncStatusListener = new RemoteTabsSyncListener(); - FirefoxAccounts.addSyncStatusListener(mSyncStatusListener); - } - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - return inflater.inflate(R.layout.home_combined_history_panel, container, false); - } - - @UiThread - @Override - public void onViewCreated(View view, Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - - mRecyclerView = (CombinedHistoryRecyclerView) view.findViewById(R.id.combined_recycler_view); - setUpRecyclerView(); - - mRefreshLayout = (SwipeRefreshLayout) view.findViewById(R.id.refresh_layout); - setUpRefreshLayout(); - - mClientsEmptyView = view.findViewById(R.id.home_clients_empty_view); - mHistoryEmptyView = view.findViewById(R.id.home_history_empty_view); - mRecentTabsEmptyView = view.findViewById(R.id.home_recent_tabs_empty_view); - setUpEmptyViews(); - - mPanelFooterButton = (Button) view.findViewById(R.id.history_panel_footer_button); - mPanelFooterButton.setText(R.string.home_clear_history_button); - mPanelFooterButton.setOnClickListener(new OnFooterButtonClickListener()); - - mRecentTabsAdapter.startListeningForClosedTabs(); - mRecentTabsAdapter.startListeningForHistorySanitize(); - - if (mSavedRestoreBundle != null) { - setPanelStateFromBundle(mSavedRestoreBundle); - mSavedRestoreBundle = null; - } - } - - @UiThread - private void setUpRecyclerView() { - if (mPanelLevel == null) { - mPanelLevel = PARENT; - } - - mRecyclerView.setAdapter(mPanelLevel == PARENT ? mHistoryAdapter : - mPanelLevel == CHILD_SYNC ? mClientsAdapter : mRecentTabsAdapter); - - final RecyclerView.ItemAnimator animator = new DefaultItemAnimator(); - animator.setAddDuration(100); - animator.setChangeDuration(100); - animator.setMoveDuration(100); - animator.setRemoveDuration(100); - mRecyclerView.setLayoutManager(new LinearLayoutManager(getContext())); - mHistoryAdapter.setLinearLayoutManager((LinearLayoutManager) mRecyclerView.getLayoutManager()); - mRecyclerView.setItemAnimator(animator); - mRecyclerView.addItemDecoration(new HistoryDividerItemDecoration(getContext())); - mRecyclerView.setOnHistoryClickedListener(mUrlOpenListener); - mRecyclerView.setOnPanelLevelChangeListener(new OnLevelChangeListener()); - mRecyclerView.setHiddenClientsDialogBuilder(new HiddenClientsHelper()); - mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { - @Override - public void onScrolled(RecyclerView recyclerView, int dx, int dy) { - super.onScrolled(recyclerView, dx, dy); - final LinearLayoutManager llm = (LinearLayoutManager) recyclerView.getLayoutManager(); - if ((mPanelLevel == PARENT) && (llm.findLastCompletelyVisibleItemPosition() == HistoryCursorLoader.HISTORY_LIMIT)) { - Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.LIST, "history_scroll_max"); - } - - } - }); - registerForContextMenu(mRecyclerView); - } - - private void setUpRefreshLayout() { - mRefreshLayout.setColorSchemeResources(R.color.fennec_ui_orange, R.color.action_orange); - mRefreshLayout.setOnRefreshListener(new RemoteTabsRefreshListener()); - mRefreshLayout.setEnabled(false); - } - - private void setUpEmptyViews() { - // Set up history empty view. - final ImageView historyIcon = (ImageView) mHistoryEmptyView.findViewById(R.id.home_empty_image); - historyIcon.setVisibility(View.GONE); - - final TextView historyText = (TextView) mHistoryEmptyView.findViewById(R.id.home_empty_text); - historyText.setText(R.string.home_most_recent_empty); - - final TextView historyHint = (TextView) mHistoryEmptyView.findViewById(R.id.home_empty_hint); - - if (!Restrictions.isAllowed(getActivity(), Restrictable.PRIVATE_BROWSING)) { - historyHint.setVisibility(View.GONE); - } else { - final String hintText = getResources().getString(R.string.home_most_recent_emptyhint); - final SpannableStringBuilder hintBuilder = formatHintText(hintText); - if (hintBuilder != null) { - historyHint.setText(hintBuilder); - historyHint.setMovementMethod(LinkMovementMethod.getInstance()); - historyHint.setVisibility(View.VISIBLE); - } - } - - // Set up Clients empty view. - final Button syncSetupButton = (Button) mClientsEmptyView.findViewById(R.id.sync_setup_button); - syncSetupButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.BUTTON, "history_syncsetup"); - // This Activity will redirect to the correct Activity as needed. - final Intent intent = new Intent(FxAccountConstants.ACTION_FXA_GET_STARTED); - startActivity(intent); - } - }); - - // Set up Recent Tabs empty view. - final ImageView recentTabsIcon = (ImageView) mRecentTabsEmptyView.findViewById(R.id.home_empty_image); - recentTabsIcon.setImageResource(R.drawable.icon_remote_tabs_empty); - - final TextView recentTabsText = (TextView) mRecentTabsEmptyView.findViewById(R.id.home_empty_text); - recentTabsText.setText(R.string.home_last_tabs_empty); - } - - @Override - public void setPanelStateChangeListener( - PanelStateChangeListener panelStateChangeListener) { - super.setPanelStateChangeListener(panelStateChangeListener); - if (mHistoryAdapter != null) { - mHistoryAdapter.setPanelStateChangeListener(panelStateChangeListener); - } - } - - @Override - public void restoreData(Bundle data) { - if (mRecyclerView != null) { - setPanelStateFromBundle(data); - } else { - mSavedRestoreBundle = data; - } - } - - private void setPanelStateFromBundle(Bundle data) { - if (data != null && data.getBoolean("goToRecentTabs", false) && mPanelLevel != CHILD_RECENT_TABS) { - mPanelLevel = CHILD_RECENT_TABS; - mRecyclerView.swapAdapter(mRecentTabsAdapter, true); - updateEmptyView(CHILD_RECENT_TABS); - updateButtonFromLevel(); - } - } - - @Override - public void onActivityCreated(Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - mCursorLoaderCallbacks = new CursorLoaderCallbacks(); - } - - @Override - protected void load() { - getLoaderManager().initLoader(LOADER_ID_HISTORY, null, mCursorLoaderCallbacks); - getLoaderManager().initLoader(LOADER_ID_REMOTE, null, mCursorLoaderCallbacks); - } - - private static class RemoteTabsCursorLoader extends SimpleCursorLoader { - private final GeckoProfile mProfile; - - public RemoteTabsCursorLoader(Context context) { - super(context); - mProfile = GeckoProfile.get(context); - } - - @Override - public Cursor loadCursor() { - return BrowserDB.from(mProfile).getTabsAccessor().getRemoteTabsCursor(getContext()); - } - } - - private static class HistoryCursorLoader extends SimpleCursorLoader { - // Max number of history results - public static final int HISTORY_LIMIT = 100; - private final BrowserDB mDB; - - public HistoryCursorLoader(Context context) { - super(context); - mDB = BrowserDB.from(context); - } - - @Override - public Cursor loadCursor() { - final ContentResolver cr = getContext().getContentResolver(); - return mDB.getRecentHistory(cr, HISTORY_LIMIT); - } - } - - private class CursorLoaderCallbacks implements LoaderManager.LoaderCallbacks { - private BrowserDB mDB; // Pseudo-final: set in onCreateLoader. - - @Override - public Loader onCreateLoader(int id, Bundle args) { - if (mDB == null) { - mDB = BrowserDB.from(getActivity()); - } - - switch (id) { - case LOADER_ID_HISTORY: - return new HistoryCursorLoader(getContext()); - case LOADER_ID_REMOTE: - return new RemoteTabsCursorLoader(getContext()); - default: - Log.e(LOGTAG, "Unknown loader id!"); - return null; - } - } - - @Override - public void onLoadFinished(Loader loader, Cursor c) { - final int loaderId = loader.getId(); - switch (loaderId) { - case LOADER_ID_HISTORY: - mHistoryAdapter.setHistory(c); - updateEmptyView(PARENT); - break; - - case LOADER_ID_REMOTE: - final List clients = mDB.getTabsAccessor().getClientsFromCursor(c); - mHistoryAdapter.getDeviceUpdateHandler().onDeviceCountUpdated(clients.size()); - mClientsAdapter.setClients(clients); - updateEmptyView(CHILD_SYNC); - break; - } - - updateButtonFromLevel(); - } - - @Override - public void onLoaderReset(Loader loader) { - mClientsAdapter.setClients(Collections.emptyList()); - mHistoryAdapter.setHistory(null); - } - } - - public interface PanelStateUpdateHandler { - void onPanelStateUpdated(PanelLevel level); - } - - public PanelStateUpdateHandler getPanelStateUpdateHandler() { - if (mPanelStateUpdateHandler == null) { - mPanelStateUpdateHandler = new PanelStateUpdateHandler() { - @Override - public void onPanelStateUpdated(PanelLevel level) { - updateEmptyView(level); - updateButtonFromLevel(); - } - }; - } - return mPanelStateUpdateHandler; - } - - protected class OnLevelChangeListener implements OnPanelLevelChangeListener { - @Override - public boolean changeLevel(PanelLevel level) { - if (level == mPanelLevel) { - return false; - } - - mPanelLevel = level; - switch (level) { - case PARENT: - mRecyclerView.swapAdapter(mHistoryAdapter, true); - mRefreshLayout.setEnabled(false); - break; - case CHILD_SYNC: - mRecyclerView.swapAdapter(mClientsAdapter, true); - mRefreshLayout.setEnabled(mClientsAdapter.getClientsCount() > 0); - break; - case CHILD_RECENT_TABS: - mRecyclerView.swapAdapter(mRecentTabsAdapter, true); - break; - } - - updateEmptyView(level); - updateButtonFromLevel(); - return true; - } - } - - private void updateButtonFromLevel() { - switch (mPanelLevel) { - case PARENT: - final boolean historyRestricted = !Restrictions.isAllowed(getActivity(), Restrictable.CLEAR_HISTORY); - if (historyRestricted || mHistoryAdapter.getItemCount() == mHistoryAdapter.getNumVisibleSmartFolders()) { - mPanelFooterButton.setVisibility(View.GONE); - } else { - mPanelFooterButton.setText(R.string.home_clear_history_button); - mPanelFooterButton.setVisibility(View.VISIBLE); - } - break; - case CHILD_RECENT_TABS: - if (mRecentTabsAdapter.getClosedTabsCount() > 1) { - mPanelFooterButton.setText(R.string.home_restore_all); - mPanelFooterButton.setVisibility(View.VISIBLE); - } else { - mPanelFooterButton.setVisibility(View.GONE); - } - break; - case CHILD_SYNC: - mPanelFooterButton.setVisibility(View.GONE); - break; - } - } - - private class OnFooterButtonClickListener implements View.OnClickListener { - @Override - public void onClick(View view) { - switch (mPanelLevel) { - case PARENT: - final AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(getActivity()); - dialogBuilder.setMessage(R.string.home_clear_history_confirm); - dialogBuilder.setNegativeButton(R.string.button_cancel, new AlertDialog.OnClickListener() { - @Override - public void onClick(final DialogInterface dialog, final int which) { - dialog.dismiss(); - } - }); - - dialogBuilder.setPositiveButton(R.string.button_ok, new AlertDialog.OnClickListener() { - @Override - public void onClick(final DialogInterface dialog, final int which) { - dialog.dismiss(); - - // Send message to Java to clear history. - final JSONObject json = new JSONObject(); - try { - json.put("history", true); - } catch (JSONException e) { - Log.e(LOGTAG, "JSON error", e); - } - - GeckoAppShell.notifyObservers("Sanitize:ClearData", json.toString()); - Telemetry.sendUIEvent(TelemetryContract.Event.SANITIZE, TelemetryContract.Method.BUTTON, "history"); - } - }); - - dialogBuilder.show(); - break; - case CHILD_RECENT_TABS: - final String telemetryExtra = mRecentTabsAdapter.restoreAllTabs(); - if (telemetryExtra != null) { - Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.BUTTON, telemetryExtra); - } - break; - } - } - } - - private void updateEmptyView(PanelLevel level) { - boolean showEmptyHistoryView = (mPanelLevel == PARENT && mHistoryEmptyView.isShown()); - boolean showEmptyClientsView = (mPanelLevel == CHILD_SYNC && mClientsEmptyView.isShown()); - boolean showEmptyRecentTabsView = (mPanelLevel == CHILD_RECENT_TABS && mRecentTabsEmptyView.isShown()); - - if (mPanelLevel == level) { - switch (mPanelLevel) { - case PARENT: - showEmptyHistoryView = mHistoryAdapter.getItemCount() == mHistoryAdapter.getNumVisibleSmartFolders(); - break; - - case CHILD_SYNC: - showEmptyClientsView = mClientsAdapter.getItemCount() == 1; - break; - - case CHILD_RECENT_TABS: - showEmptyRecentTabsView = mRecentTabsAdapter.getClosedTabsCount() == 0; - break; - } - } - - final boolean showEmptyView = showEmptyClientsView || showEmptyHistoryView || showEmptyRecentTabsView; - mRecyclerView.setOverScrollMode(showEmptyView ? View.OVER_SCROLL_NEVER : View.OVER_SCROLL_IF_CONTENT_SCROLLS); - - mHistoryEmptyView.setVisibility(showEmptyHistoryView ? View.VISIBLE : View.GONE); - mClientsEmptyView.setVisibility(showEmptyClientsView ? View.VISIBLE : View.GONE); - mRecentTabsEmptyView.setVisibility(showEmptyRecentTabsView ? View.VISIBLE : View.GONE); - } - - /** - * Make Span that is clickable, and underlined - * between the string markers FORMAT_S1 and - * FORMAT_S2. - * - * @param text String to format - * @return formatted SpannableStringBuilder, or null if there - * is not any text to format. - */ - private SpannableStringBuilder formatHintText(String text) { - // Set formatting as marked by string placeholders. - final int underlineStart = text.indexOf(FORMAT_S1); - final int underlineEnd = text.indexOf(FORMAT_S2); - - // Check that there is text to be formatted. - if (underlineStart >= underlineEnd) { - return null; - } - - final SpannableStringBuilder ssb = new SpannableStringBuilder(text); - - // Set clickable text. - final ClickableSpan clickableSpan = new ClickableSpan() { - @Override - public void onClick(View widget) { - Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.BUTTON, "hint_private_browsing"); - try { - final JSONObject json = new JSONObject(); - json.put("type", "Menu:Open"); - GeckoApp.getEventDispatcher().dispatchEvent(json, null); - EventDispatcher.getInstance().dispatchEvent(json, null); - } catch (JSONException e) { - Log.e(LOGTAG, "Error forming JSON for Private Browsing contextual hint", e); - } - } - }; - - ssb.setSpan(clickableSpan, 0, text.length(), 0); - - // Remove underlining set by ClickableSpan. - final UnderlineSpan noUnderlineSpan = new UnderlineSpan() { - @Override - public void updateDrawState(TextPaint textPaint) { - textPaint.setUnderlineText(false); - } - }; - - ssb.setSpan(noUnderlineSpan, 0, text.length(), 0); - - // Add underlining for "Private Browsing". - ssb.setSpan(new UnderlineSpan(), underlineStart, underlineEnd, 0); - - ssb.delete(underlineEnd, underlineEnd + FORMAT_S2.length()); - ssb.delete(underlineStart, underlineStart + FORMAT_S1.length()); - - return ssb; - } - - @Override - public void onCreateContextMenu(ContextMenu menu, View view, ContextMenu.ContextMenuInfo menuInfo) { - if (!(menuInfo instanceof RemoteTabsClientContextMenuInfo)) { - // Long pressed item was not a RemoteTabsGroup item. Superclass - // can handle this. - super.onCreateContextMenu(menu, view, menuInfo); - return; - } - - // Long pressed item was a remote client; provide the appropriate menu. - final MenuInflater inflater = new MenuInflater(view.getContext()); - inflater.inflate(R.menu.home_remote_tabs_client_contextmenu, menu); - - final RemoteTabsClientContextMenuInfo info = (RemoteTabsClientContextMenuInfo) menuInfo; - menu.setHeaderTitle(info.client.name); - } - - @Override - public boolean onContextItemSelected(MenuItem item) { - if (super.onContextItemSelected(item)) { - // HomeFragment was able to handle to selected item. - return true; - } - - final ContextMenu.ContextMenuInfo menuInfo = item.getMenuInfo(); - if (!(menuInfo instanceof RemoteTabsClientContextMenuInfo)) { - return false; - } - - final RemoteTabsClientContextMenuInfo info = (RemoteTabsClientContextMenuInfo) menuInfo; - - final int itemId = item.getItemId(); - if (itemId == R.id.home_remote_tabs_hide_client) { - mClientsAdapter.removeItem(info.position); - return true; - } - - return false; - } - - interface DialogBuilder { - void createAndShowDialog(List items); - } - - protected class HiddenClientsHelper implements DialogBuilder { - @Override - public void createAndShowDialog(List clientsList) { - final RemoteClientsDialogFragment dialog = RemoteClientsDialogFragment.newInstance( - getResources().getString(R.string.home_remote_tabs_hidden_devices_title), - getResources().getString(R.string.home_remote_tabs_unhide_selected_devices), - RemoteClientsDialogFragment.ChoiceMode.MULTIPLE, new ArrayList<>(clientsList)); - dialog.setTargetFragment(CombinedHistoryPanel.this, 0); - dialog.show(getActivity().getSupportFragmentManager(), "show-clients"); - } - } - - @Override - public void onClients(List clients) { - mClientsAdapter.unhideClients(clients); - } - - /** - * Stores information regarding the creation of the context menu for a remote client. - */ - protected static class RemoteTabsClientContextMenuInfo extends HomeContextMenuInfo { - protected final RemoteClient client; - - public RemoteTabsClientContextMenuInfo(View targetView, int position, long id, RemoteClient client) { - super(targetView, position, id); - this.client = client; - } - } - - protected class RemoteTabsRefreshListener implements SwipeRefreshLayout.OnRefreshListener { - @Override - public void onRefresh() { - if (FirefoxAccounts.firefoxAccountsExist(getActivity())) { - final Account account = FirefoxAccounts.getFirefoxAccount(getActivity()); - FirefoxAccounts.requestImmediateSync(account, STAGES_TO_SYNC_ON_REFRESH, null); - } else { - Log.wtf(LOGTAG, "No Firefox Account found; this should never happen. Ignoring."); - mRefreshLayout.setRefreshing(false); - } - } - } - - protected class RemoteTabsSyncListener implements SyncStatusListener { - @Override - public Context getContext() { - return getActivity(); - } - - @Override - public Account getAccount() { - return FirefoxAccounts.getFirefoxAccount(getContext()); - } - - @Override - public void onSyncStarted() { - } - - @Override - public void onSyncFinished() { - mRefreshLayout.setRefreshing(false); - } - } - - @Override - public void onDestroyView() { - super.onDestroyView(); - - mRecentTabsAdapter.stopListeningForClosedTabs(); - mRecentTabsAdapter.stopListeningForHistorySanitize(); - } - - @Override - public void onDestroy() { - super.onDestroy(); - - if (mSyncStatusListener != null) { - FirefoxAccounts.removeSyncStatusListener(mSyncStatusListener); - mSyncStatusListener = null; - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/CombinedHistoryRecyclerView.java b/mobile/android/base/java/org/mozilla/gecko/home/CombinedHistoryRecyclerView.java deleted file mode 100644 index e813e4c44a..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/CombinedHistoryRecyclerView.java +++ /dev/null @@ -1,145 +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.support.v7.widget.LinearLayoutManager; -import android.support.v7.widget.RecyclerView; -import android.util.AttributeSet; -import android.view.KeyEvent; -import android.view.View; -import org.mozilla.gecko.db.RemoteClient; -import org.mozilla.gecko.home.CombinedHistoryPanel.OnPanelLevelChangeListener; -import org.mozilla.gecko.Telemetry; -import org.mozilla.gecko.TelemetryContract; -import org.mozilla.gecko.widget.RecyclerViewClickSupport; - -import java.util.EnumSet; - -import static org.mozilla.gecko.home.CombinedHistoryPanel.OnPanelLevelChangeListener.PanelLevel.CHILD_RECENT_TABS; -import static org.mozilla.gecko.home.CombinedHistoryPanel.OnPanelLevelChangeListener.PanelLevel.CHILD_SYNC; -import static org.mozilla.gecko.home.CombinedHistoryPanel.OnPanelLevelChangeListener.PanelLevel.PARENT; - -public class CombinedHistoryRecyclerView extends RecyclerView - implements RecyclerViewClickSupport.OnItemClickListener, RecyclerViewClickSupport.OnItemLongClickListener { - public static String LOGTAG = "CombinedHistoryRecycView"; - - protected interface AdapterContextMenuBuilder { - HomeContextMenuInfo makeContextMenuInfoFromPosition(View view, int position); - } - - protected HomePager.OnUrlOpenListener mOnUrlOpenListener; - protected OnPanelLevelChangeListener mOnPanelLevelChangeListener; - protected CombinedHistoryPanel.DialogBuilder mDialogBuilder; - protected HomeContextMenuInfo mContextMenuInfo; - - public CombinedHistoryRecyclerView(Context context) { - super(context); - init(context); - } - - public CombinedHistoryRecyclerView(Context context, AttributeSet attributeSet) { - super(context, attributeSet); - init(context); - } - - public CombinedHistoryRecyclerView(Context context, AttributeSet attributeSet, int defStyle) { - super(context, attributeSet, defStyle); - init(context); - } - - private void init(Context context) { - LinearLayoutManager layoutManager = new LinearLayoutManager(context); - layoutManager.setOrientation(LinearLayoutManager.VERTICAL); - setLayoutManager(layoutManager); - - RecyclerViewClickSupport.addTo(this) - .setOnItemClickListener(this) - .setOnItemLongClickListener(this); - - setOnKeyListener(new View.OnKeyListener() { - @Override - public boolean onKey(View v, int keyCode, KeyEvent event) { - final int action = event.getAction(); - - // If the user hit the BACK key, try to move to the parent folder. - if (action == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) { - return mOnPanelLevelChangeListener.changeLevel(PARENT); - } - return false; - } - }); - } - - public void setOnHistoryClickedListener(HomePager.OnUrlOpenListener listener) { - this.mOnUrlOpenListener = listener; - } - - public void setOnPanelLevelChangeListener(OnPanelLevelChangeListener listener) { - this.mOnPanelLevelChangeListener = listener; - } - - public void setHiddenClientsDialogBuilder(CombinedHistoryPanel.DialogBuilder builder) { - mDialogBuilder = builder; - } - - @Override - public void onItemClicked(RecyclerView recyclerView, int position, View v) { - final int viewType = getAdapter().getItemViewType(position); - final CombinedHistoryItem.ItemType itemType = CombinedHistoryItem.ItemType.viewTypeToItemType(viewType); - final String telemetryExtra; - - switch (itemType) { - case RECENT_TABS: - mOnPanelLevelChangeListener.changeLevel(CHILD_RECENT_TABS); - break; - - case SYNCED_DEVICES: - mOnPanelLevelChangeListener.changeLevel(CHILD_SYNC); - break; - - case CLIENT: - ((ClientsAdapter) getAdapter()).toggleClient(position); - break; - - case HIDDEN_DEVICES: - if (mDialogBuilder != null) { - mDialogBuilder.createAndShowDialog(((ClientsAdapter) getAdapter()).getHiddenClients()); - } - break; - - case NAVIGATION_BACK: - mOnPanelLevelChangeListener.changeLevel(PARENT); - break; - - case CHILD: - case HISTORY: - if (mOnUrlOpenListener != null) { - final TwoLinePageRow historyItem = (TwoLinePageRow) v; - Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.LIST_ITEM, "history"); - mOnUrlOpenListener.onUrlOpen(historyItem.getUrl(), EnumSet.of(HomePager.OnUrlOpenListener.Flags.ALLOW_SWITCH_TO_TAB)); - } - break; - - case CLOSED_TAB: - telemetryExtra = ((RecentTabsAdapter) getAdapter()).restoreTabFromPosition(position); - Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.LIST_ITEM, telemetryExtra); - break; - } - } - - @Override - public boolean onItemLongClicked(RecyclerView recyclerView, int position, View v) { - mContextMenuInfo = ((AdapterContextMenuBuilder) getAdapter()).makeContextMenuInfoFromPosition(v, position); - return showContextMenuForChild(this); - } - - @Override - public HomeContextMenuInfo getContextMenuInfo() { - return mContextMenuInfo; - } - -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/DynamicPanel.java b/mobile/android/base/java/org/mozilla/gecko/home/DynamicPanel.java deleted file mode 100644 index d2c136219f..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/DynamicPanel.java +++ /dev/null @@ -1,393 +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.db.BrowserContract; -import org.mozilla.gecko.db.BrowserContract.HomeItems; -import org.mozilla.gecko.home.HomeConfig.PanelConfig; -import org.mozilla.gecko.home.PanelLayout.ContextMenuRegistry; -import org.mozilla.gecko.home.PanelLayout.DatasetHandler; -import org.mozilla.gecko.home.PanelLayout.DatasetRequest; -import org.mozilla.gecko.util.ThreadUtils; -import org.mozilla.gecko.util.UIAsyncTask; - -import android.content.ContentResolver; -import android.content.Context; -import android.database.Cursor; -import android.net.Uri; -import android.os.Bundle; -import android.support.v4.app.LoaderManager; -import android.support.v4.content.Loader; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.FrameLayout; - -/** - * Fragment that displays dynamic content specified by a {@code PanelConfig}. - * The {@code DynamicPanel} UI is built based on the given {@code LayoutType} - * and its associated list of {@code ViewConfig}. - * - * {@code DynamicPanel} manages all necessary Loaders to load panel datasets - * from their respective content providers. Each panel dataset has its own - * associated Loader. This is enforced by defining the Loader IDs based on - * their associated dataset IDs. - * - * The {@code PanelLayout} can make load and reset requests on datasets via - * the provided {@code DatasetHandler}. This way it doesn't need to know the - * details of how datasets are loaded and reset. Each time a dataset is - * requested, {@code DynamicPanel} restarts a Loader with the respective ID (see - * {@code PanelDatasetHandler}). - * - * See {@code PanelLayout} for more details on how {@code DynamicPanel} - * receives dataset requests and delivers them back to the {@code PanelLayout}. - */ -public class DynamicPanel extends HomeFragment { - private static final String LOGTAG = "GeckoDynamicPanel"; - - // Dataset ID to be used by the loader - private static final String DATASET_REQUEST = "dataset_request"; - - // Max number of items to display in the panel - private static final int RESULT_LIMIT = 100; - - // The main view for this fragment. This contains the PanelLayout and PanelAuthLayout. - private FrameLayout mView; - - // The panel layout associated with this panel - private PanelLayout mPanelLayout; - - // The layout used to show authentication UI for this panel - private PanelAuthLayout mPanelAuthLayout; - - // Cache used to keep track of whether or not the user has been authenticated. - private PanelAuthCache mPanelAuthCache; - - // Hold a reference to the UiAsyncTask we use to check the state of the - // PanelAuthCache, so that we can cancel it if necessary. - private UIAsyncTask.WithoutParams mAuthStateTask; - - // The configuration associated with this panel - private PanelConfig mPanelConfig; - - // Callbacks used for the loader - private PanelLoaderCallbacks mLoaderCallbacks; - - // The current UI mode in the fragment - private UIMode mUIMode; - - /* - * Different UI modes to display depending on the authentication state. - * - * PANEL: Layout to display panel data. - * AUTH: Authentication UI. - */ - private enum UIMode { - PANEL, - AUTH - } - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - final Bundle args = getArguments(); - if (args != null) { - mPanelConfig = (PanelConfig) args.getParcelable(HomePager.PANEL_CONFIG_ARG); - } - - if (mPanelConfig == null) { - throw new IllegalStateException("Can't create a DynamicPanel without a PanelConfig"); - } - - mPanelAuthCache = new PanelAuthCache(getActivity()); - } - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - mView = new FrameLayout(getActivity()); - return mView; - } - - @Override - public void onViewCreated(View view, Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - - // Restore whatever the UI mode the fragment had before - // a device rotation. - if (mUIMode != null) { - setUIMode(mUIMode); - } - - mPanelAuthCache.setOnChangeListener(new PanelAuthChangeListener()); - } - - @Override - public void onDestroyView() { - super.onDestroyView(); - mView = null; - mPanelLayout = null; - mPanelAuthLayout = null; - - mPanelAuthCache.setOnChangeListener(null); - - if (mAuthStateTask != null) { - mAuthStateTask.cancel(); - mAuthStateTask = null; - } - } - - @Override - public void onActivityCreated(Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - - // Create callbacks before the initial loader is started. - mLoaderCallbacks = new PanelLoaderCallbacks(); - loadIfVisible(); - } - - @Override - protected void load() { - Log.d(LOGTAG, "Loading layout"); - - if (requiresAuth()) { - mAuthStateTask = new UIAsyncTask.WithoutParams(ThreadUtils.getBackgroundHandler()) { - @Override - public synchronized Boolean doInBackground() { - return mPanelAuthCache.isAuthenticated(mPanelConfig.getId()); - } - - @Override - public void onPostExecute(Boolean isAuthenticated) { - mAuthStateTask = null; - setUIMode(isAuthenticated ? UIMode.PANEL : UIMode.AUTH); - } - }; - mAuthStateTask.execute(); - } else { - setUIMode(UIMode.PANEL); - } - } - - /** - * @return true if this panel requires authentication. - */ - private boolean requiresAuth() { - return mPanelConfig.getAuthConfig() != null; - } - - /** - * Lazily creates layout for panel data. - */ - private void createPanelLayout() { - final ContextMenuRegistry contextMenuRegistry = new ContextMenuRegistry() { - @Override - public void register(View view) { - registerForContextMenu(view); - } - }; - - switch (mPanelConfig.getLayoutType()) { - case FRAME: - final PanelDatasetHandler datasetHandler = new PanelDatasetHandler(); - mPanelLayout = new FramePanelLayout(getActivity(), mPanelConfig, datasetHandler, - mUrlOpenListener, contextMenuRegistry); - break; - - default: - throw new IllegalStateException("Unrecognized layout type in DynamicPanel"); - } - - Log.d(LOGTAG, "Created layout of type: " + mPanelConfig.getLayoutType()); - mView.addView(mPanelLayout); - } - - /** - * Lazily creates layout for authentication UI. - */ - private void createPanelAuthLayout() { - mPanelAuthLayout = new PanelAuthLayout(getActivity(), mPanelConfig); - mView.addView(mPanelAuthLayout, 0); - } - - private void setUIMode(UIMode mode) { - switch (mode) { - case PANEL: - if (mPanelAuthLayout != null) { - mPanelAuthLayout.setVisibility(View.GONE); - } - if (mPanelLayout == null) { - createPanelLayout(); - } - mPanelLayout.setVisibility(View.VISIBLE); - - // Only trigger a reload if the UI mode has changed - // (e.g. auth cache changes) and the fragment is allowed - // to load its contents. Any loaders associated with the - // panel layout will be automatically re-bound after a - // device rotation, no need to explicitly load it again. - if (mUIMode != mode && canLoad()) { - mPanelLayout.load(); - } - break; - - case AUTH: - if (mPanelLayout != null) { - mPanelLayout.setVisibility(View.GONE); - } - if (mPanelAuthLayout == null) { - createPanelAuthLayout(); - } - mPanelAuthLayout.setVisibility(View.VISIBLE); - break; - - default: - throw new IllegalStateException("Unrecognized UIMode in DynamicPanel"); - } - - mUIMode = mode; - } - - /** - * Used by the PanelLayout to make load and reset requests to - * the holding fragment. - */ - private class PanelDatasetHandler implements DatasetHandler { - @Override - public void requestDataset(DatasetRequest request) { - Log.d(LOGTAG, "Requesting request: " + request); - - final Bundle bundle = new Bundle(); - bundle.putParcelable(DATASET_REQUEST, request); - - getLoaderManager().restartLoader(request.getViewIndex(), - bundle, mLoaderCallbacks); - } - - @Override - public void resetDataset(int viewIndex) { - Log.d(LOGTAG, "Resetting dataset: " + viewIndex); - - final LoaderManager lm = getLoaderManager(); - - // Release any resources associated with the dataset if - // it's currently loaded in memory. - final Loader datasetLoader = lm.getLoader(viewIndex); - if (datasetLoader != null) { - datasetLoader.reset(); - } - } - } - - /** - * Cursor loader for the panel datasets. - */ - private static class PanelDatasetLoader extends SimpleCursorLoader { - private DatasetRequest mRequest; - - public PanelDatasetLoader(Context context, DatasetRequest request) { - super(context); - mRequest = request; - } - - public DatasetRequest getRequest() { - return mRequest; - } - - @Override - public void onContentChanged() { - // Ensure the refresh request doesn't affect the view's filter - // stack (i.e. use DATASET_LOAD type) but keep the current - // dataset ID and filter. - final DatasetRequest newRequest = - new DatasetRequest(mRequest.getViewIndex(), - DatasetRequest.Type.DATASET_LOAD, - mRequest.getDatasetId(), - mRequest.getFilterDetail()); - - mRequest = newRequest; - super.onContentChanged(); - } - - @Override - public Cursor loadCursor() { - final ContentResolver cr = getContext().getContentResolver(); - - final String selection; - final String[] selectionArgs; - - // Null represents the root filter - if (mRequest.getFilter() == null) { - selection = HomeItems.FILTER + " IS NULL"; - selectionArgs = null; - } else { - selection = HomeItems.FILTER + " = ?"; - selectionArgs = new String[] { mRequest.getFilter() }; - } - - final Uri queryUri = HomeItems.CONTENT_URI.buildUpon() - .appendQueryParameter(BrowserContract.PARAM_DATASET_ID, - mRequest.getDatasetId()) - .appendQueryParameter(BrowserContract.PARAM_LIMIT, - String.valueOf(RESULT_LIMIT)) - .build(); - - // XXX: You can use HomeItems.CONTENT_FAKE_URI for development - // to pull items from fake_home_items.json. - return cr.query(queryUri, null, selection, selectionArgs, null); - } - } - - /** - * LoaderCallbacks implementation that interacts with the LoaderManager. - */ - private class PanelLoaderCallbacks implements LoaderManager.LoaderCallbacks { - @Override - public Loader onCreateLoader(int id, Bundle args) { - final DatasetRequest request = (DatasetRequest) args.getParcelable(DATASET_REQUEST); - - Log.d(LOGTAG, "Creating loader for request: " + request); - return new PanelDatasetLoader(getActivity(), request); - } - - @Override - public void onLoadFinished(Loader loader, Cursor cursor) { - final DatasetRequest request = getRequestFromLoader(loader); - Log.d(LOGTAG, "Finished loader for request: " + request); - - if (mPanelLayout != null) { - mPanelLayout.deliverDataset(request, cursor); - } - } - - @Override - public void onLoaderReset(Loader loader) { - final DatasetRequest request = getRequestFromLoader(loader); - Log.d(LOGTAG, "Resetting loader for request: " + request); - - if (mPanelLayout != null) { - mPanelLayout.releaseDataset(request.getViewIndex()); - } - } - - private DatasetRequest getRequestFromLoader(Loader loader) { - final PanelDatasetLoader datasetLoader = (PanelDatasetLoader) loader; - return datasetLoader.getRequest(); - } - } - - private class PanelAuthChangeListener implements PanelAuthCache.OnChangeListener { - @Override - public void onChange(String panelId, boolean isAuthenticated) { - if (!mPanelConfig.getId().equals(panelId)) { - return; - } - - setUIMode(isAuthenticated ? UIMode.PANEL : UIMode.AUTH); - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/FramePanelLayout.java b/mobile/android/base/java/org/mozilla/gecko/home/FramePanelLayout.java deleted file mode 100644 index 7168c1576a..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/FramePanelLayout.java +++ /dev/null @@ -1,52 +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.home.HomePager.OnUrlOpenListener; -import org.mozilla.gecko.home.HomeConfig.PanelConfig; -import org.mozilla.gecko.home.HomeConfig.ViewConfig; - -import android.content.Context; -import android.util.Log; -import android.view.View; - -class FramePanelLayout extends PanelLayout { - private static final String LOGTAG = "GeckoFramePanelLayout"; - - private final View mChildView; - private final ViewConfig mChildConfig; - - public FramePanelLayout(Context context, PanelConfig panelConfig, DatasetHandler datasetHandler, - OnUrlOpenListener urlOpenListener, ContextMenuRegistry contextMenuRegistry) { - super(context, panelConfig, datasetHandler, urlOpenListener, contextMenuRegistry); - - // This layout can only hold one view so we simply - // take the first defined view from PanelConfig. - mChildConfig = panelConfig.getViewAt(0); - if (mChildConfig == null) { - throw new IllegalStateException("FramePanelLayout requires a view in PanelConfig"); - } - - mChildView = createPanelView(mChildConfig); - addView(mChildView); - } - - @Override - public void load() { - Log.d(LOGTAG, "Loading"); - - if (mChildView instanceof DatasetBacked) { - final FilterDetail filter = new FilterDetail(mChildConfig.getFilter(), null); - - final DatasetRequest request = new DatasetRequest(mChildConfig.getIndex(), - mChildConfig.getDatasetId(), - filter); - - Log.d(LOGTAG, "Requesting child request: " + request); - requestDataset(request); - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/HistorySectionsHelper.java b/mobile/android/base/java/org/mozilla/gecko/home/HistorySectionsHelper.java deleted file mode 100644 index 7a49559f67..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/HistorySectionsHelper.java +++ /dev/null @@ -1,80 +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.res.Resources; - -import org.mozilla.gecko.home.CombinedHistoryAdapter.SectionHeader; -import org.mozilla.gecko.R; - -import java.util.Calendar; -import java.util.Locale; - - -public class HistorySectionsHelper { - - // Constants for different time sections. - private static final long MS_PER_DAY = 86400000; - private static final long MS_PER_WEEK = MS_PER_DAY * 7; - - public static class SectionDateRange { - public final long start; - public final long end; - public final String displayName; - - private SectionDateRange(long start, long end, String displayName) { - this.start = start; - this.end = end; - this.displayName = displayName; - } - } - - /** - * Updates the time range in milliseconds covered by each section header and sets the title. - * @param res Resources for fetching string names - * @param sectionsArray Array of section bookkeeping objects - */ - public static void updateRecentSectionOffset(final Resources res, SectionDateRange[] sectionsArray) { - final long now = System.currentTimeMillis(); - final Calendar cal = Calendar.getInstance(); - - // Update calendar to this day. - cal.set(Calendar.HOUR_OF_DAY, 0); - cal.set(Calendar.MINUTE, 0); - cal.set(Calendar.SECOND, 0); - cal.set(Calendar.MILLISECOND, 1); - final long currentDayMS = cal.getTimeInMillis(); - - // Calculate the start and end time for each section header and set its display text. - sectionsArray[SectionHeader.TODAY.ordinal()] = - new SectionDateRange(currentDayMS, now, res.getString(R.string.history_today_section)); - - sectionsArray[SectionHeader.YESTERDAY.ordinal()] = - new SectionDateRange(currentDayMS - MS_PER_DAY, currentDayMS, res.getString(R.string.history_yesterday_section)); - - sectionsArray[SectionHeader.WEEK.ordinal()] = - new SectionDateRange(currentDayMS - MS_PER_WEEK, now, res.getString(R.string.history_week_section)); - - // Update the calendar to beginning of next month to avoid problems calculating the last day of this month. - cal.add(Calendar.MONTH, 1); - cal.set(Calendar.DAY_OF_MONTH, cal.getMinimum(Calendar.DAY_OF_MONTH)); - - // Iterate over the remaining history sections, moving backwards in time. - for (int i = SectionHeader.THIS_MONTH.ordinal(); i < SectionHeader.OLDER_THAN_SIX_MONTHS.ordinal(); i++) { - final long end = cal.getTimeInMillis(); - - cal.add(Calendar.MONTH, -1); - final long start = cal.getTimeInMillis(); - - final String displayName = cal.getDisplayName(Calendar.MONTH, Calendar.LONG, Locale.getDefault()); - - sectionsArray[i] = new SectionDateRange(start, end, displayName); - } - - sectionsArray[SectionHeader.OLDER_THAN_SIX_MONTHS.ordinal()] = - new SectionDateRange(0L, cal.getTimeInMillis(), res.getString(R.string.history_older_section)); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/HomeAdapter.java b/mobile/android/base/java/org/mozilla/gecko/home/HomeAdapter.java deleted file mode 100644 index 98d1ae6d8b..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/HomeAdapter.java +++ /dev/null @@ -1,224 +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.activitystream.ActivityStream; -import org.mozilla.gecko.home.HomeConfig.PanelConfig; -import org.mozilla.gecko.home.HomeConfig.PanelType; -import org.mozilla.gecko.home.activitystream.ActivityStreamHomeFragment; - -import android.content.Context; -import android.os.Bundle; -import android.support.v4.app.Fragment; -import android.support.v4.app.FragmentManager; -import android.support.v4.app.FragmentStatePagerAdapter; -import android.view.ViewGroup; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -public class HomeAdapter extends FragmentStatePagerAdapter { - - private final Context mContext; - private final ArrayList mPanelInfos; - private final Map mPanels; - private final Map mRestoreBundles; - - private boolean mCanLoadHint; - - private OnAddPanelListener mAddPanelListener; - - private HomeFragment.PanelStateChangeListener mPanelStateChangeListener = null; - - public interface OnAddPanelListener { - void onAddPanel(String title); - } - - public void setPanelStateChangeListener(HomeFragment.PanelStateChangeListener listener) { - mPanelStateChangeListener = listener; - - for (Fragment fragment : mPanels.values()) { - ((HomeFragment) fragment).setPanelStateChangeListener(listener); - } - } - - public HomeAdapter(Context context, FragmentManager fm) { - super(fm); - - mContext = context; - mCanLoadHint = HomeFragment.DEFAULT_CAN_LOAD_HINT; - - mPanelInfos = new ArrayList<>(); - mPanels = new HashMap<>(); - mRestoreBundles = new HashMap<>(); - } - - @Override - public int getCount() { - return mPanelInfos.size(); - } - - @Override - public Fragment getItem(int position) { - PanelInfo info = mPanelInfos.get(position); - return Fragment.instantiate(mContext, info.getClassName(mContext), info.getArgs()); - } - - @Override - public CharSequence getPageTitle(int position) { - if (mPanelInfos.size() > 0) { - PanelInfo info = mPanelInfos.get(position); - return info.getTitle().toUpperCase(); - } - - return null; - } - - @Override - public Object instantiateItem(ViewGroup container, int position) { - final HomeFragment fragment = (HomeFragment) super.instantiateItem(container, position); - fragment.setPanelStateChangeListener(mPanelStateChangeListener); - - final String id = mPanelInfos.get(position).getId(); - mPanels.put(id, fragment); - - if (mRestoreBundles.containsKey(id)) { - fragment.restoreData(mRestoreBundles.get(id)); - mRestoreBundles.remove(id); - } - - return fragment; - } - - public void setRestoreData(int position, Bundle data) { - final String id = mPanelInfos.get(position).getId(); - final HomeFragment fragment = mPanels.get(id); - - // We have no guarantees as to whether our desired fragment is instantiated yet: therefore - // we might need to either pass data to the fragment, or store it for later. - if (fragment != null) { - fragment.restoreData(data); - } else { - mRestoreBundles.put(id, data); - } - - } - - @Override - public void destroyItem(ViewGroup container, int position, Object object) { - final String id = mPanelInfos.get(position).getId(); - - super.destroyItem(container, position, object); - mPanels.remove(id); - } - - public void setOnAddPanelListener(OnAddPanelListener listener) { - mAddPanelListener = listener; - } - - public int getItemPosition(String panelId) { - for (int i = 0; i < mPanelInfos.size(); i++) { - final String id = mPanelInfos.get(i).getId(); - if (id.equals(panelId)) { - return i; - } - } - - return -1; - } - - public String getPanelIdAtPosition(int position) { - // getPanelIdAtPosition() might be called before HomeAdapter - // has got its initial list of PanelConfigs. Just bail. - if (mPanelInfos.isEmpty()) { - return null; - } - - return mPanelInfos.get(position).getId(); - } - - private void addPanel(PanelInfo info) { - mPanelInfos.add(info); - - if (mAddPanelListener != null) { - mAddPanelListener.onAddPanel(info.getTitle()); - } - } - - public void update(List panelConfigs) { - mPanels.clear(); - mPanelInfos.clear(); - - if (panelConfigs != null) { - for (PanelConfig panelConfig : panelConfigs) { - final PanelInfo info = new PanelInfo(panelConfig); - addPanel(info); - } - } - - notifyDataSetChanged(); - } - - public boolean getCanLoadHint() { - return mCanLoadHint; - } - - public void setCanLoadHint(boolean canLoadHint) { - // We cache the last hint value so that we can use it when - // creating new panels. See PanelInfo.getArgs(). - mCanLoadHint = canLoadHint; - - // Enable/disable loading on all existing panels - for (Fragment panelFragment : mPanels.values()) { - final HomeFragment panel = (HomeFragment) panelFragment; - panel.setCanLoadHint(canLoadHint); - } - } - - private final class PanelInfo { - private final PanelConfig mPanelConfig; - - PanelInfo(PanelConfig panelConfig) { - mPanelConfig = panelConfig; - } - - public String getId() { - return mPanelConfig.getId(); - } - - public String getTitle() { - return mPanelConfig.getTitle(); - } - - public String getClassName(Context context) { - final PanelType type = mPanelConfig.getType(); - - // Override top_sites with ActivityStream panel when enabled - // PanelType.toString() returns the panel id - if (type.toString() == "top_sites" && - ActivityStream.isEnabled(context) && - ActivityStream.isHomePanel()) { - return ActivityStreamHomeFragment.class.getName(); - } - return type.getPanelClass().getName(); - } - - public Bundle getArgs() { - final Bundle args = new Bundle(); - - args.putBoolean(HomePager.CAN_LOAD_ARG, mCanLoadHint); - - // Only DynamicPanels need the PanelConfig argument - if (mPanelConfig.isDynamic()) { - args.putParcelable(HomePager.PANEL_CONFIG_ARG, mPanelConfig); - } - - return args; - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/HomeBanner.java b/mobile/android/base/java/org/mozilla/gecko/home/HomeBanner.java deleted file mode 100644 index 10f5db39e5..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/HomeBanner.java +++ /dev/null @@ -1,315 +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.json.JSONObject; -import org.mozilla.gecko.GeckoApp; -import org.mozilla.gecko.GeckoAppShell; -import org.mozilla.gecko.R; -import org.mozilla.gecko.animation.PropertyAnimator; -import org.mozilla.gecko.animation.PropertyAnimator.Property; -import org.mozilla.gecko.animation.ViewHelper; -import org.mozilla.gecko.util.ResourceDrawableUtils; -import org.mozilla.gecko.util.GeckoEventListener; -import org.mozilla.gecko.util.ThreadUtils; -import org.mozilla.gecko.widget.EllipsisTextView; - -import android.content.Context; -import android.graphics.drawable.Drawable; -import android.text.Html; -import android.text.TextUtils; -import android.util.AttributeSet; -import android.view.LayoutInflater; -import android.view.MotionEvent; -import android.view.View; -import android.widget.ImageButton; -import android.widget.ImageView; -import android.widget.LinearLayout; - -public class HomeBanner extends LinearLayout - implements GeckoEventListener { - private static final String LOGTAG = "GeckoHomeBanner"; - - // Used for tracking scroll length - private float mTouchY = -1; - - // Used to detect for upwards scroll to push banner all the way up - private boolean mSnapBannerToTop; - - // Tracks whether or not the banner should be shown on the current panel. - private boolean mActive; - - // The user is currently swiping between HomePager pages - private boolean mScrollingPages; - - // Tracks whether the user swiped the banner down, preventing us from autoshowing when the user - // switches back to the default page. - private boolean mUserSwipedDown; - - // We must use this custom TextView to address an issue on 2.3 and lower where ellipsized text - // will not wrap more than 2 lines. - private final EllipsisTextView mTextView; - private final ImageView mIconView; - - // The height of the banner view. - private final float mHeight; - - // Listener that gets called when the banner is dismissed from the close button. - private OnDismissListener mOnDismissListener; - - public interface OnDismissListener { - public void onDismiss(); - } - - public HomeBanner(Context context) { - this(context, null); - } - - public HomeBanner(Context context, AttributeSet attrs) { - super(context, attrs); - - LayoutInflater.from(context).inflate(R.layout.home_banner_content, this); - - mTextView = (EllipsisTextView) findViewById(R.id.text); - mIconView = (ImageView) findViewById(R.id.icon); - - mHeight = getResources().getDimensionPixelSize(R.dimen.home_banner_height); - - // Disable the banner until a message is set. - setEnabled(false); - } - - @Override - public void onAttachedToWindow() { - super.onAttachedToWindow(); - - // Tapping on the close button will ensure that the banner is never - // showed again on this session. - final ImageButton closeButton = (ImageButton) findViewById(R.id.close); - - // The drawable should have 50% opacity. - closeButton.getDrawable().setAlpha(127); - - closeButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - HomeBanner.this.dismiss(); - - // Send the current message id back to JS. - GeckoAppShell.notifyObservers("HomeBanner:Dismiss", (String) getTag()); - } - }); - - setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - HomeBanner.this.dismiss(); - - // Send the current message id back to JS. - GeckoAppShell.notifyObservers("HomeBanner:Click", (String) getTag()); - } - }); - - GeckoApp.getEventDispatcher().registerGeckoThreadListener(this, "HomeBanner:Data"); - } - - @Override - public void onDetachedFromWindow() { - super.onDetachedFromWindow(); - - GeckoApp.getEventDispatcher().unregisterGeckoThreadListener(this, "HomeBanner:Data"); - } - - public void setScrollingPages(boolean scrollingPages) { - mScrollingPages = scrollingPages; - } - - public void setOnDismissListener(OnDismissListener listener) { - mOnDismissListener = listener; - } - - /** - * Hides and disables the banner. - */ - private void dismiss() { - setVisibility(View.GONE); - setEnabled(false); - - if (mOnDismissListener != null) { - mOnDismissListener.onDismiss(); - } - } - - /** - * Sends a message to gecko to request a new banner message. UI is updated in handleMessage. - */ - public void update() { - GeckoAppShell.notifyObservers("HomeBanner:Get", null); - } - - @Override - public void handleMessage(String event, JSONObject message) { - final String id = message.optString("id"); - final String text = message.optString("text"); - final String iconURI = message.optString("iconURI"); - - // Don't update the banner if the message doesn't have valid id and text. - if (TextUtils.isEmpty(id) || TextUtils.isEmpty(text)) { - return; - } - - // Update the banner message on the UI thread. - ThreadUtils.postToUiThread(new Runnable() { - @Override - public void run() { - // Store the current message id to pass back to JS in the view's OnClickListener. - setTag(id); - mTextView.setOriginalText(Html.fromHtml(text)); - - ResourceDrawableUtils.getDrawable(getContext(), iconURI, new ResourceDrawableUtils.BitmapLoader() { - @Override - public void onBitmapFound(final Drawable d) { - // Hide the image view if we don't have an icon to show. - if (d == null) { - mIconView.setVisibility(View.GONE); - } else { - mIconView.setImageDrawable(d); - mIconView.setVisibility(View.VISIBLE); - } - } - }); - - GeckoAppShell.notifyObservers("HomeBanner:Shown", id); - - // Enable the banner after a message is set. - setEnabled(true); - - // Animate the banner if it is currently active. - if (mActive) { - animateUp(); - } - } - }); - } - - public void setActive(boolean active) { - // No need to animate if not changing - if (mActive == active) { - return; - } - - mActive = active; - - // Don't animate if the banner isn't enabled. - if (!isEnabled()) { - return; - } - - if (active) { - animateUp(); - } else { - animateDown(); - } - } - - private void ensureVisible() { - // The banner visibility is set to GONE after it is animated off screen, - // so we need to make it visible again. - if (getVisibility() == View.GONE) { - // Translate the banner off screen before setting it to VISIBLE. - ViewHelper.setTranslationY(this, mHeight); - setVisibility(View.VISIBLE); - } - } - - private void animateUp() { - // Don't try to animate if the user swiped the banner down previously to hide it. - if (mUserSwipedDown) { - return; - } - - ensureVisible(); - - final PropertyAnimator animator = new PropertyAnimator(100); - animator.attach(this, Property.TRANSLATION_Y, 0); - animator.start(); - } - - private void animateDown() { - if (ViewHelper.getTranslationY(this) == mHeight) { - // Hide the banner to avoid intercepting clicks on pre-honeycomb devices. - setVisibility(View.GONE); - return; - } - - final PropertyAnimator animator = new PropertyAnimator(100); - animator.attach(this, Property.TRANSLATION_Y, mHeight); - animator.addPropertyAnimationListener(new PropertyAnimator.PropertyAnimationListener() { - @Override - public void onPropertyAnimationStart() { - } - - @Override - public void onPropertyAnimationEnd() { - // Hide the banner to avoid intercepting clicks on pre-honeycomb devices. - setVisibility(View.GONE); - } - }); - animator.start(); - } - - public void handleHomeTouch(MotionEvent event) { - if (!mActive || !isEnabled() || mScrollingPages) { - return; - } - - ensureVisible(); - - switch (event.getActionMasked()) { - case MotionEvent.ACTION_DOWN: { - // Track the beginning of the touch - mTouchY = event.getRawY(); - break; - } - - case MotionEvent.ACTION_MOVE: { - final float curY = event.getRawY(); - final float delta = mTouchY - curY; - mSnapBannerToTop = delta <= 0.0f; - - float newTranslationY = ViewHelper.getTranslationY(this) + delta; - - // Clamp the values to be between 0 and height. - if (newTranslationY < 0.0f) { - newTranslationY = 0.0f; - } else if (newTranslationY > mHeight) { - newTranslationY = mHeight; - } - - // Don't change this value if it wasn't a significant movement - if (delta >= 10 || delta <= -10) { - mUserSwipedDown = (newTranslationY == mHeight); - } - - ViewHelper.setTranslationY(this, newTranslationY); - mTouchY = curY; - break; - } - - case MotionEvent.ACTION_UP: - case MotionEvent.ACTION_CANCEL: { - mTouchY = -1; - if (mSnapBannerToTop) { - animateUp(); - } else { - animateDown(); - mUserSwipedDown = true; - } - break; - } - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/HomeConfig.java b/mobile/android/base/java/org/mozilla/gecko/home/HomeConfig.java deleted file mode 100644 index 08e79be3a3..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/HomeConfig.java +++ /dev/null @@ -1,1694 +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 java.util.ArrayList; -import java.util.Collections; -import java.util.EnumSet; -import java.util.HashMap; -import java.util.Iterator; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; - -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; -import org.mozilla.gecko.annotation.RobocopTarget; -import org.mozilla.gecko.GeckoAppShell; -import org.mozilla.gecko.R; -import org.mozilla.gecko.util.ThreadUtils; - -import android.content.Context; -import android.os.Parcel; -import android.os.Parcelable; -import android.text.TextUtils; -import android.util.Pair; - -public final class HomeConfig { - public static final String PREF_KEY_BOOKMARKS_PANEL_ENABLED = "bookmarksPanelEnabled"; - public static final String PREF_KEY_HISTORY_PANEL_ENABLED = "combinedHistoryPanelEnabled"; - - /** - * Used to determine what type of HomeFragment subclass to use when creating - * a given panel. With the exception of DYNAMIC, all of these types correspond - * to a default set of built-in panels. The DYNAMIC panel type is used by - * third-party services to create panels with varying types of content. - */ - @RobocopTarget - public static enum PanelType implements Parcelable { - TOP_SITES("top_sites", TopSitesPanel.class), - BOOKMARKS("bookmarks", BookmarksPanel.class), - COMBINED_HISTORY("combined_history", CombinedHistoryPanel.class), - DYNAMIC("dynamic", DynamicPanel.class), - // Deprecated panels that should no longer exist but are kept around for - // migration code. Class references have been replaced with new version of the panel. - DEPRECATED_REMOTE_TABS("remote_tabs", CombinedHistoryPanel.class), - DEPRECATED_HISTORY("history", CombinedHistoryPanel.class), - DEPRECATED_READING_LIST("reading_list", BookmarksPanel.class), - DEPRECATED_RECENT_TABS("recent_tabs", CombinedHistoryPanel.class); - - private final String mId; - private final Class mPanelClass; - - PanelType(String id, Class panelClass) { - mId = id; - mPanelClass = panelClass; - } - - public static PanelType fromId(String id) { - if (id == null) { - throw new IllegalArgumentException("Could not convert null String to PanelType"); - } - - for (PanelType panelType : PanelType.values()) { - if (TextUtils.equals(panelType.mId, id.toLowerCase())) { - return panelType; - } - } - - throw new IllegalArgumentException("Could not convert String id to PanelType"); - } - - @Override - public String toString() { - return mId; - } - - public Class getPanelClass() { - return mPanelClass; - } - - @Override - public int describeContents() { - return 0; - } - - @Override - public void writeToParcel(Parcel dest, int flags) { - dest.writeInt(ordinal()); - } - - public static final Creator CREATOR = new Creator() { - @Override - public PanelType createFromParcel(final Parcel source) { - return PanelType.values()[source.readInt()]; - } - - @Override - public PanelType[] newArray(final int size) { - return new PanelType[size]; - } - }; - } - - public static class PanelConfig implements Parcelable { - private final PanelType mType; - private final String mTitle; - private final String mId; - private final LayoutType mLayoutType; - private final List mViews; - private final AuthConfig mAuthConfig; - private final EnumSet mFlags; - private final int mPosition; - - static final String JSON_KEY_TYPE = "type"; - static final String JSON_KEY_TITLE = "title"; - static final String JSON_KEY_ID = "id"; - static final String JSON_KEY_LAYOUT = "layout"; - static final String JSON_KEY_VIEWS = "views"; - static final String JSON_KEY_AUTH_CONFIG = "authConfig"; - static final String JSON_KEY_DEFAULT = "default"; - static final String JSON_KEY_DISABLED = "disabled"; - static final String JSON_KEY_POSITION = "position"; - - public enum Flags { - DEFAULT_PANEL, - DISABLED_PANEL - } - - public PanelConfig(JSONObject json) throws JSONException, IllegalArgumentException { - final String panelType = json.optString(JSON_KEY_TYPE, null); - if (TextUtils.isEmpty(panelType)) { - mType = PanelType.DYNAMIC; - } else { - mType = PanelType.fromId(panelType); - } - - mTitle = json.getString(JSON_KEY_TITLE); - mId = json.getString(JSON_KEY_ID); - - final String layoutTypeId = json.optString(JSON_KEY_LAYOUT, null); - if (layoutTypeId != null) { - mLayoutType = LayoutType.fromId(layoutTypeId); - } else { - mLayoutType = null; - } - - final JSONArray jsonViews = json.optJSONArray(JSON_KEY_VIEWS); - if (jsonViews != null) { - mViews = new ArrayList(); - - final int viewCount = jsonViews.length(); - for (int i = 0; i < viewCount; i++) { - final JSONObject jsonViewConfig = (JSONObject) jsonViews.get(i); - final ViewConfig viewConfig = new ViewConfig(i, jsonViewConfig); - mViews.add(viewConfig); - } - } else { - mViews = null; - } - - final JSONObject jsonAuthConfig = json.optJSONObject(JSON_KEY_AUTH_CONFIG); - if (jsonAuthConfig != null) { - mAuthConfig = new AuthConfig(jsonAuthConfig); - } else { - mAuthConfig = null; - } - - mFlags = EnumSet.noneOf(Flags.class); - - if (json.optBoolean(JSON_KEY_DEFAULT, false)) { - mFlags.add(Flags.DEFAULT_PANEL); - } - - if (json.optBoolean(JSON_KEY_DISABLED, false)) { - mFlags.add(Flags.DISABLED_PANEL); - } - - mPosition = json.optInt(JSON_KEY_POSITION, -1); - - validate(); - } - - @SuppressWarnings("unchecked") - public PanelConfig(Parcel in) { - mType = (PanelType) in.readParcelable(getClass().getClassLoader()); - mTitle = in.readString(); - mId = in.readString(); - mLayoutType = (LayoutType) in.readParcelable(getClass().getClassLoader()); - - mViews = new ArrayList(); - in.readTypedList(mViews, ViewConfig.CREATOR); - - mAuthConfig = (AuthConfig) in.readParcelable(getClass().getClassLoader()); - - mFlags = (EnumSet) in.readSerializable(); - mPosition = in.readInt(); - - validate(); - } - - public PanelConfig(PanelConfig panelConfig) { - mType = panelConfig.mType; - mTitle = panelConfig.mTitle; - mId = panelConfig.mId; - mLayoutType = panelConfig.mLayoutType; - - mViews = new ArrayList(); - List viewConfigs = panelConfig.mViews; - if (viewConfigs != null) { - for (ViewConfig viewConfig : viewConfigs) { - mViews.add(new ViewConfig(viewConfig)); - } - } - - mAuthConfig = panelConfig.mAuthConfig; - mFlags = panelConfig.mFlags.clone(); - mPosition = panelConfig.mPosition; - - validate(); - } - - public PanelConfig(PanelType type, String title, String id) { - this(type, title, id, EnumSet.noneOf(Flags.class)); - } - - public PanelConfig(PanelType type, String title, String id, EnumSet flags) { - this(type, title, id, null, null, null, flags, -1); - } - - public PanelConfig(PanelType type, String title, String id, LayoutType layoutType, - List views, AuthConfig authConfig, EnumSet flags, int position) { - mType = type; - mTitle = title; - mId = id; - mLayoutType = layoutType; - mViews = views; - mAuthConfig = authConfig; - mFlags = flags; - mPosition = position; - - validate(); - } - - private void validate() { - if (mType == null) { - throw new IllegalArgumentException("Can't create PanelConfig with null type"); - } - - if (TextUtils.isEmpty(mTitle)) { - throw new IllegalArgumentException("Can't create PanelConfig with empty title"); - } - - if (TextUtils.isEmpty(mId)) { - throw new IllegalArgumentException("Can't create PanelConfig with empty id"); - } - - if (mLayoutType == null && mType == PanelType.DYNAMIC) { - throw new IllegalArgumentException("Can't create a dynamic PanelConfig with null layout type"); - } - - if ((mViews == null || mViews.size() == 0) && mType == PanelType.DYNAMIC) { - throw new IllegalArgumentException("Can't create a dynamic PanelConfig with no views"); - } - - if (mFlags == null) { - throw new IllegalArgumentException("Can't create PanelConfig with null flags"); - } - } - - public PanelType getType() { - return mType; - } - - public String getTitle() { - return mTitle; - } - - public String getId() { - return mId; - } - - public LayoutType getLayoutType() { - return mLayoutType; - } - - public int getViewCount() { - return (mViews != null ? mViews.size() : 0); - } - - public ViewConfig getViewAt(int index) { - return (mViews != null ? mViews.get(index) : null); - } - - public EnumSet getFlags() { - return mFlags.clone(); - } - - public boolean isDynamic() { - return (mType == PanelType.DYNAMIC); - } - - public boolean isDefault() { - return mFlags.contains(Flags.DEFAULT_PANEL); - } - - private void setIsDefault(boolean isDefault) { - if (isDefault) { - mFlags.add(Flags.DEFAULT_PANEL); - } else { - mFlags.remove(Flags.DEFAULT_PANEL); - } - } - - public boolean isDisabled() { - return mFlags.contains(Flags.DISABLED_PANEL); - } - - private void setIsDisabled(boolean isDisabled) { - if (isDisabled) { - mFlags.add(Flags.DISABLED_PANEL); - } else { - mFlags.remove(Flags.DISABLED_PANEL); - } - } - - public AuthConfig getAuthConfig() { - return mAuthConfig; - } - - public int getPosition() { - return mPosition; - } - - public JSONObject toJSON() throws JSONException { - final JSONObject json = new JSONObject(); - - json.put(JSON_KEY_TYPE, mType.toString()); - json.put(JSON_KEY_TITLE, mTitle); - json.put(JSON_KEY_ID, mId); - - if (mLayoutType != null) { - json.put(JSON_KEY_LAYOUT, mLayoutType.toString()); - } - - if (mViews != null) { - final JSONArray jsonViews = new JSONArray(); - - final int viewCount = mViews.size(); - for (int i = 0; i < viewCount; i++) { - final ViewConfig viewConfig = mViews.get(i); - final JSONObject jsonViewConfig = viewConfig.toJSON(); - jsonViews.put(jsonViewConfig); - } - - json.put(JSON_KEY_VIEWS, jsonViews); - } - - if (mAuthConfig != null) { - json.put(JSON_KEY_AUTH_CONFIG, mAuthConfig.toJSON()); - } - - if (mFlags.contains(Flags.DEFAULT_PANEL)) { - json.put(JSON_KEY_DEFAULT, true); - } - - if (mFlags.contains(Flags.DISABLED_PANEL)) { - json.put(JSON_KEY_DISABLED, true); - } - - json.put(JSON_KEY_POSITION, mPosition); - - return json; - } - - @Override - public boolean equals(Object o) { - if (o == null) { - return false; - } - - if (this == o) { - return true; - } - - if (!(o instanceof PanelConfig)) { - return false; - } - - final PanelConfig other = (PanelConfig) o; - return mId.equals(other.mId); - } - - @Override - public int hashCode() { - return super.hashCode(); - } - - @Override - public int describeContents() { - return 0; - } - - @Override - public void writeToParcel(Parcel dest, int flags) { - dest.writeParcelable(mType, 0); - dest.writeString(mTitle); - dest.writeString(mId); - dest.writeParcelable(mLayoutType, 0); - dest.writeTypedList(mViews); - dest.writeParcelable(mAuthConfig, 0); - dest.writeSerializable(mFlags); - dest.writeInt(mPosition); - } - - public static final Creator CREATOR = new Creator() { - @Override - public PanelConfig createFromParcel(final Parcel in) { - return new PanelConfig(in); - } - - @Override - public PanelConfig[] newArray(final int size) { - return new PanelConfig[size]; - } - }; - } - - public static enum LayoutType implements Parcelable { - FRAME("frame"); - - private final String mId; - - LayoutType(String id) { - mId = id; - } - - public static LayoutType fromId(String id) { - if (id == null) { - throw new IllegalArgumentException("Could not convert null String to LayoutType"); - } - - for (LayoutType layoutType : LayoutType.values()) { - if (TextUtils.equals(layoutType.mId, id.toLowerCase())) { - return layoutType; - } - } - - throw new IllegalArgumentException("Could not convert String id to LayoutType"); - } - - @Override - public String toString() { - return mId; - } - - @Override - public int describeContents() { - return 0; - } - - @Override - public void writeToParcel(Parcel dest, int flags) { - dest.writeInt(ordinal()); - } - - public static final Creator CREATOR = new Creator() { - @Override - public LayoutType createFromParcel(final Parcel source) { - return LayoutType.values()[source.readInt()]; - } - - @Override - public LayoutType[] newArray(final int size) { - return new LayoutType[size]; - } - }; - } - - public static enum ViewType implements Parcelable { - LIST("list"), - GRID("grid"); - - private final String mId; - - ViewType(String id) { - mId = id; - } - - public static ViewType fromId(String id) { - if (id == null) { - throw new IllegalArgumentException("Could not convert null String to ViewType"); - } - - for (ViewType viewType : ViewType.values()) { - if (TextUtils.equals(viewType.mId, id.toLowerCase())) { - return viewType; - } - } - - throw new IllegalArgumentException("Could not convert String id to ViewType"); - } - - @Override - public String toString() { - return mId; - } - - @Override - public int describeContents() { - return 0; - } - - @Override - public void writeToParcel(Parcel dest, int flags) { - dest.writeInt(ordinal()); - } - - public static final Creator CREATOR = new Creator() { - @Override - public ViewType createFromParcel(final Parcel source) { - return ViewType.values()[source.readInt()]; - } - - @Override - public ViewType[] newArray(final int size) { - return new ViewType[size]; - } - }; - } - - public static enum ItemType implements Parcelable { - ARTICLE("article"), - IMAGE("image"), - ICON("icon"); - - private final String mId; - - ItemType(String id) { - mId = id; - } - - public static ItemType fromId(String id) { - if (id == null) { - throw new IllegalArgumentException("Could not convert null String to ItemType"); - } - - for (ItemType itemType : ItemType.values()) { - if (TextUtils.equals(itemType.mId, id.toLowerCase())) { - return itemType; - } - } - - throw new IllegalArgumentException("Could not convert String id to ItemType"); - } - - @Override - public String toString() { - return mId; - } - - @Override - public int describeContents() { - return 0; - } - - @Override - public void writeToParcel(Parcel dest, int flags) { - dest.writeInt(ordinal()); - } - - public static final Creator CREATOR = new Creator() { - @Override - public ItemType createFromParcel(final Parcel source) { - return ItemType.values()[source.readInt()]; - } - - @Override - public ItemType[] newArray(final int size) { - return new ItemType[size]; - } - }; - } - - public static enum ItemHandler implements Parcelable { - BROWSER("browser"), - INTENT("intent"); - - private final String mId; - - ItemHandler(String id) { - mId = id; - } - - public static ItemHandler fromId(String id) { - if (id == null) { - throw new IllegalArgumentException("Could not convert null String to ItemHandler"); - } - - for (ItemHandler itemHandler : ItemHandler.values()) { - if (TextUtils.equals(itemHandler.mId, id.toLowerCase())) { - return itemHandler; - } - } - - throw new IllegalArgumentException("Could not convert String id to ItemHandler"); - } - - @Override - public String toString() { - return mId; - } - - @Override - public int describeContents() { - return 0; - } - - @Override - public void writeToParcel(Parcel dest, int flags) { - dest.writeInt(ordinal()); - } - - public static final Creator CREATOR = new Creator() { - @Override - public ItemHandler createFromParcel(final Parcel source) { - return ItemHandler.values()[source.readInt()]; - } - - @Override - public ItemHandler[] newArray(final int size) { - return new ItemHandler[size]; - } - }; - } - - public static class ViewConfig implements Parcelable { - private final int mIndex; - private final ViewType mType; - private final String mDatasetId; - private final ItemType mItemType; - private final ItemHandler mItemHandler; - private final String mBackImageUrl; - private final String mFilter; - private final EmptyViewConfig mEmptyViewConfig; - private final HeaderConfig mHeaderConfig; - private final EnumSet mFlags; - - static final String JSON_KEY_TYPE = "type"; - static final String JSON_KEY_DATASET = "dataset"; - static final String JSON_KEY_ITEM_TYPE = "itemType"; - static final String JSON_KEY_ITEM_HANDLER = "itemHandler"; - static final String JSON_KEY_BACK_IMAGE_URL = "backImageUrl"; - static final String JSON_KEY_FILTER = "filter"; - static final String JSON_KEY_EMPTY = "empty"; - static final String JSON_KEY_HEADER = "header"; - static final String JSON_KEY_REFRESH_ENABLED = "refreshEnabled"; - - public enum Flags { - REFRESH_ENABLED - } - - public ViewConfig(int index, JSONObject json) throws JSONException, IllegalArgumentException { - mIndex = index; - mType = ViewType.fromId(json.getString(JSON_KEY_TYPE)); - mDatasetId = json.getString(JSON_KEY_DATASET); - mItemType = ItemType.fromId(json.getString(JSON_KEY_ITEM_TYPE)); - mItemHandler = ItemHandler.fromId(json.getString(JSON_KEY_ITEM_HANDLER)); - mBackImageUrl = json.optString(JSON_KEY_BACK_IMAGE_URL, null); - mFilter = json.optString(JSON_KEY_FILTER, null); - - final JSONObject jsonEmptyViewConfig = json.optJSONObject(JSON_KEY_EMPTY); - if (jsonEmptyViewConfig != null) { - mEmptyViewConfig = new EmptyViewConfig(jsonEmptyViewConfig); - } else { - mEmptyViewConfig = null; - } - - final JSONObject jsonHeaderConfig = json.optJSONObject(JSON_KEY_HEADER); - mHeaderConfig = jsonHeaderConfig != null ? new HeaderConfig(jsonHeaderConfig) : null; - - mFlags = EnumSet.noneOf(Flags.class); - if (json.optBoolean(JSON_KEY_REFRESH_ENABLED, false)) { - mFlags.add(Flags.REFRESH_ENABLED); - } - - validate(); - } - - @SuppressWarnings("unchecked") - public ViewConfig(Parcel in) { - mIndex = in.readInt(); - mType = (ViewType) in.readParcelable(getClass().getClassLoader()); - mDatasetId = in.readString(); - mItemType = (ItemType) in.readParcelable(getClass().getClassLoader()); - mItemHandler = (ItemHandler) in.readParcelable(getClass().getClassLoader()); - mBackImageUrl = in.readString(); - mFilter = in.readString(); - mEmptyViewConfig = (EmptyViewConfig) in.readParcelable(getClass().getClassLoader()); - mHeaderConfig = (HeaderConfig) in.readParcelable(getClass().getClassLoader()); - mFlags = (EnumSet) in.readSerializable(); - - validate(); - } - - public ViewConfig(ViewConfig viewConfig) { - mIndex = viewConfig.mIndex; - mType = viewConfig.mType; - mDatasetId = viewConfig.mDatasetId; - mItemType = viewConfig.mItemType; - mItemHandler = viewConfig.mItemHandler; - mBackImageUrl = viewConfig.mBackImageUrl; - mFilter = viewConfig.mFilter; - mEmptyViewConfig = viewConfig.mEmptyViewConfig; - mHeaderConfig = viewConfig.mHeaderConfig; - mFlags = viewConfig.mFlags.clone(); - - validate(); - } - - private void validate() { - if (mType == null) { - throw new IllegalArgumentException("Can't create ViewConfig with null type"); - } - - if (TextUtils.isEmpty(mDatasetId)) { - throw new IllegalArgumentException("Can't create ViewConfig with empty dataset ID"); - } - - if (mItemType == null) { - throw new IllegalArgumentException("Can't create ViewConfig with null item type"); - } - - if (mItemHandler == null) { - throw new IllegalArgumentException("Can't create ViewConfig with null item handler"); - } - - if (mFlags == null) { - throw new IllegalArgumentException("Can't create ViewConfig with null flags"); - } - } - - public int getIndex() { - return mIndex; - } - - public ViewType getType() { - return mType; - } - - public String getDatasetId() { - return mDatasetId; - } - - public ItemType getItemType() { - return mItemType; - } - - public ItemHandler getItemHandler() { - return mItemHandler; - } - - public String getBackImageUrl() { - return mBackImageUrl; - } - - public String getFilter() { - return mFilter; - } - - public EmptyViewConfig getEmptyViewConfig() { - return mEmptyViewConfig; - } - - public HeaderConfig getHeaderConfig() { - return mHeaderConfig; - } - - public boolean hasHeaderConfig() { - return mHeaderConfig != null; - } - - public boolean isRefreshEnabled() { - return mFlags.contains(Flags.REFRESH_ENABLED); - } - - public JSONObject toJSON() throws JSONException { - final JSONObject json = new JSONObject(); - - json.put(JSON_KEY_TYPE, mType.toString()); - json.put(JSON_KEY_DATASET, mDatasetId); - json.put(JSON_KEY_ITEM_TYPE, mItemType.toString()); - json.put(JSON_KEY_ITEM_HANDLER, mItemHandler.toString()); - - if (!TextUtils.isEmpty(mBackImageUrl)) { - json.put(JSON_KEY_BACK_IMAGE_URL, mBackImageUrl); - } - - if (!TextUtils.isEmpty(mFilter)) { - json.put(JSON_KEY_FILTER, mFilter); - } - - if (mEmptyViewConfig != null) { - json.put(JSON_KEY_EMPTY, mEmptyViewConfig.toJSON()); - } - - if (mHeaderConfig != null) { - json.put(JSON_KEY_HEADER, mHeaderConfig.toJSON()); - } - - if (mFlags.contains(Flags.REFRESH_ENABLED)) { - json.put(JSON_KEY_REFRESH_ENABLED, true); - } - - return json; - } - - @Override - public int describeContents() { - return 0; - } - - @Override - public void writeToParcel(Parcel dest, int flags) { - dest.writeInt(mIndex); - dest.writeParcelable(mType, 0); - dest.writeString(mDatasetId); - dest.writeParcelable(mItemType, 0); - dest.writeParcelable(mItemHandler, 0); - dest.writeString(mBackImageUrl); - dest.writeString(mFilter); - dest.writeParcelable(mEmptyViewConfig, 0); - dest.writeParcelable(mHeaderConfig, 0); - dest.writeSerializable(mFlags); - } - - public static final Creator CREATOR = new Creator() { - @Override - public ViewConfig createFromParcel(final Parcel in) { - return new ViewConfig(in); - } - - @Override - public ViewConfig[] newArray(final int size) { - return new ViewConfig[size]; - } - }; - } - - public static class EmptyViewConfig implements Parcelable { - private final String mText; - private final String mImageUrl; - - static final String JSON_KEY_TEXT = "text"; - static final String JSON_KEY_IMAGE_URL = "imageUrl"; - - public EmptyViewConfig(JSONObject json) throws JSONException, IllegalArgumentException { - mText = json.optString(JSON_KEY_TEXT, null); - mImageUrl = json.optString(JSON_KEY_IMAGE_URL, null); - } - - public EmptyViewConfig(Parcel in) { - mText = in.readString(); - mImageUrl = in.readString(); - } - - public EmptyViewConfig(EmptyViewConfig emptyViewConfig) { - mText = emptyViewConfig.mText; - mImageUrl = emptyViewConfig.mImageUrl; - } - - public EmptyViewConfig(String text, String imageUrl) { - mText = text; - mImageUrl = imageUrl; - } - - public String getText() { - return mText; - } - - public String getImageUrl() { - return mImageUrl; - } - - public JSONObject toJSON() throws JSONException { - final JSONObject json = new JSONObject(); - - json.put(JSON_KEY_TEXT, mText); - json.put(JSON_KEY_IMAGE_URL, mImageUrl); - - return json; - } - - @Override - public int describeContents() { - return 0; - } - - @Override - public void writeToParcel(Parcel dest, int flags) { - dest.writeString(mText); - dest.writeString(mImageUrl); - } - - public static final Creator CREATOR = new Creator() { - @Override - public EmptyViewConfig createFromParcel(final Parcel in) { - return new EmptyViewConfig(in); - } - - @Override - public EmptyViewConfig[] newArray(final int size) { - return new EmptyViewConfig[size]; - } - }; - } - - public static class HeaderConfig implements Parcelable { - static final String JSON_KEY_IMAGE_URL = "image_url"; - static final String JSON_KEY_URL = "url"; - - private final String mImageUrl; - private final String mUrl; - - public HeaderConfig(JSONObject json) { - mImageUrl = json.optString(JSON_KEY_IMAGE_URL); - mUrl = json.optString(JSON_KEY_URL); - } - - public HeaderConfig(Parcel in) { - mImageUrl = in.readString(); - mUrl = in.readString(); - } - - public String getImageUrl() { - return mImageUrl; - } - - public String getUrl() { - return mUrl; - } - - public JSONObject toJSON() throws JSONException { - JSONObject json = new JSONObject(); - - json.put(JSON_KEY_IMAGE_URL, mImageUrl); - json.put(JSON_KEY_URL, mUrl); - - return json; - } - - @Override - public int describeContents() { - return 0; - } - - @Override - public void writeToParcel(Parcel dest, int flags) { - dest.writeString(mImageUrl); - dest.writeString(mUrl); - } - - public static final Creator CREATOR = new Creator() { - @Override - public HeaderConfig createFromParcel(Parcel source) { - return new HeaderConfig(source); - } - - @Override - public HeaderConfig[] newArray(int size) { - return new HeaderConfig[size]; - } - }; - } - - public static class AuthConfig implements Parcelable { - private final String mMessageText; - private final String mButtonText; - private final String mImageUrl; - - static final String JSON_KEY_MESSAGE_TEXT = "messageText"; - static final String JSON_KEY_BUTTON_TEXT = "buttonText"; - static final String JSON_KEY_IMAGE_URL = "imageUrl"; - - public AuthConfig(JSONObject json) throws JSONException, IllegalArgumentException { - mMessageText = json.optString(JSON_KEY_MESSAGE_TEXT); - mButtonText = json.optString(JSON_KEY_BUTTON_TEXT); - mImageUrl = json.optString(JSON_KEY_IMAGE_URL, null); - } - - public AuthConfig(Parcel in) { - mMessageText = in.readString(); - mButtonText = in.readString(); - mImageUrl = in.readString(); - - validate(); - } - - public AuthConfig(AuthConfig authConfig) { - mMessageText = authConfig.mMessageText; - mButtonText = authConfig.mButtonText; - mImageUrl = authConfig.mImageUrl; - - validate(); - } - - public AuthConfig(String messageText, String buttonText, String imageUrl) { - mMessageText = messageText; - mButtonText = buttonText; - mImageUrl = imageUrl; - - validate(); - } - - private void validate() { - if (mMessageText == null) { - throw new IllegalArgumentException("Can't create AuthConfig with null message text"); - } - - if (mButtonText == null) { - throw new IllegalArgumentException("Can't create AuthConfig with null button text"); - } - } - - public String getMessageText() { - return mMessageText; - } - - public String getButtonText() { - return mButtonText; - } - - public String getImageUrl() { - return mImageUrl; - } - - public JSONObject toJSON() throws JSONException { - final JSONObject json = new JSONObject(); - - json.put(JSON_KEY_MESSAGE_TEXT, mMessageText); - json.put(JSON_KEY_BUTTON_TEXT, mButtonText); - json.put(JSON_KEY_IMAGE_URL, mImageUrl); - - return json; - } - - @Override - public int describeContents() { - return 0; - } - - @Override - public void writeToParcel(Parcel dest, int flags) { - dest.writeString(mMessageText); - dest.writeString(mButtonText); - dest.writeString(mImageUrl); - } - - public static final Creator CREATOR = new Creator() { - @Override - public AuthConfig createFromParcel(final Parcel in) { - return new AuthConfig(in); - } - - @Override - public AuthConfig[] newArray(final int size) { - return new AuthConfig[size]; - } - }; - } - /** - * Immutable representation of the current state of {@code HomeConfig}. - * This is what HomeConfig returns from a load() call and takes as - * input to save a new state. - * - * Users of {@code State} should use an {@code Iterator} to iterate - * through the contained {@code PanelConfig} instances. - * - * {@code State} is immutable i.e. you can't add, remove, or update - * contained elements directly. You have to use an {@code Editor} to - * change the state, which can be created through the {@code edit()} - * method. - */ - public static class State implements Iterable { - private HomeConfig mHomeConfig; - private final List mPanelConfigs; - private final boolean mIsDefault; - - State(List panelConfigs, boolean isDefault) { - this(null, panelConfigs, isDefault); - } - - private State(HomeConfig homeConfig, List panelConfigs, boolean isDefault) { - mHomeConfig = homeConfig; - mPanelConfigs = Collections.unmodifiableList(panelConfigs); - mIsDefault = isDefault; - } - - private void setHomeConfig(HomeConfig homeConfig) { - if (mHomeConfig != null) { - throw new IllegalStateException("Can't set HomeConfig more than once"); - } - - mHomeConfig = homeConfig; - } - - @Override - public Iterator iterator() { - return mPanelConfigs.iterator(); - } - - /** - * Returns whether this {@code State} instance represents the default - * {@code HomeConfig} configuration or not. - */ - public boolean isDefault() { - return mIsDefault; - } - - /** - * Creates an {@code Editor} for this state. - */ - public Editor edit() { - return new Editor(mHomeConfig, this); - } - } - - /** - * {@code Editor} allows you to make changes to a {@code State}. You - * can create {@code Editor} by calling {@code edit()} on the target - * {@code State} instance. - * - * {@code Editor} works on a copy of the {@code State} that originated - * it. This means that adding, removing, or updating panels in an - * {@code Editor} will never change the {@code State} which you - * created the {@code Editor} from. Calling {@code commit()} or - * {@code apply()} will cause the new {@code State} instance to be - * created and saved using the {@code HomeConfig} instance that - * created the source {@code State}. - * - * {@code Editor} is *not* thread-safe. You can only make calls on it - * from the thread where it was originally created. It will throw an - * exception if you don't follow this invariant. - */ - public static class Editor implements Iterable { - private final HomeConfig mHomeConfig; - private final Map mConfigMap; - private final List mConfigOrder; - private final Thread mOriginalThread; - - // Each Pair represents parameters to a GeckoAppShell.notifyObservers call; - // the first String is the observer topic and the second string is the notification data. - private List> mNotificationQueue; - private PanelConfig mDefaultPanel; - private int mEnabledCount; - - private boolean mHasChanged; - private final boolean mIsFromDefault; - - private Editor(HomeConfig homeConfig, State configState) { - mHomeConfig = homeConfig; - mOriginalThread = Thread.currentThread(); - mConfigMap = new HashMap(); - mConfigOrder = new LinkedList(); - mNotificationQueue = new ArrayList<>(); - - mIsFromDefault = configState.isDefault(); - - initFromState(configState); - } - - /** - * Initialize the initial state of the editor from the given - * {@sode State}. A HashMap is used to represent the list of - * panels as it provides fast access, and a LinkedList is used to - * keep track of order. We keep a reference to the default panel - * and the number of enabled panels to avoid iterating through the - * map every time we need those. - * - * @param configState The source State to load the editor from. - */ - private void initFromState(State configState) { - for (PanelConfig panelConfig : configState) { - final PanelConfig panelCopy = new PanelConfig(panelConfig); - - if (!panelCopy.isDisabled()) { - mEnabledCount++; - } - - if (panelCopy.isDefault()) { - if (mDefaultPanel == null) { - mDefaultPanel = panelCopy; - } else { - throw new IllegalStateException("Multiple default panels in HomeConfig state"); - } - } - - final String panelId = panelConfig.getId(); - mConfigOrder.add(panelId); - mConfigMap.put(panelId, panelCopy); - } - - // We should always have a defined default panel if there's - // at least one enabled panel around. - if (mEnabledCount > 0 && mDefaultPanel == null) { - throw new IllegalStateException("Default panel in HomeConfig state is undefined"); - } - } - - private PanelConfig getPanelOrThrow(String panelId) { - final PanelConfig panelConfig = mConfigMap.get(panelId); - if (panelConfig == null) { - throw new IllegalStateException("Tried to access non-existing panel: " + panelId); - } - - return panelConfig; - } - - private boolean isCurrentDefaultPanel(PanelConfig panelConfig) { - if (mDefaultPanel == null) { - return false; - } - - return mDefaultPanel.equals(panelConfig); - } - - private void findNewDefault() { - // Pick the first panel that is neither disabled nor currently - // set as default. - for (PanelConfig panelConfig : makeOrderedCopy(false)) { - if (!panelConfig.isDefault() && !panelConfig.isDisabled()) { - setDefault(panelConfig.getId()); - return; - } - } - - mDefaultPanel = null; - } - - /** - * Makes an ordered list of PanelConfigs that can be references - * or deep copied objects. - * - * @param deepCopy true to make deep-copied objects - * @return ordered List of PanelConfigs - */ - private List makeOrderedCopy(boolean deepCopy) { - final List copiedList = new ArrayList(mConfigOrder.size()); - for (String panelId : mConfigOrder) { - PanelConfig panelConfig = mConfigMap.get(panelId); - if (deepCopy) { - panelConfig = new PanelConfig(panelConfig); - } - copiedList.add(panelConfig); - } - - return copiedList; - } - - private void setPanelIsDisabled(PanelConfig panelConfig, boolean disabled) { - if (panelConfig.isDisabled() == disabled) { - return; - } - - panelConfig.setIsDisabled(disabled); - mEnabledCount += (disabled ? -1 : 1); - } - - /** - * Gets the ID of the current default panel. - */ - public String getDefaultPanelId() { - ThreadUtils.assertOnThread(mOriginalThread); - - if (mDefaultPanel == null) { - return null; - } - - return mDefaultPanel.getId(); - } - - /** - * Set a new default panel. - * - * @param panelId the ID of the new default panel. - */ - public void setDefault(String panelId) { - ThreadUtils.assertOnThread(mOriginalThread); - - final PanelConfig panelConfig = getPanelOrThrow(panelId); - if (isCurrentDefaultPanel(panelConfig)) { - return; - } - - if (mDefaultPanel != null) { - mDefaultPanel.setIsDefault(false); - } - - panelConfig.setIsDefault(true); - setPanelIsDisabled(panelConfig, false); - - mDefaultPanel = panelConfig; - mHasChanged = true; - } - - /** - * Toggles disabled state for a panel. - * - * @param panelId the ID of the target panel. - * @param disabled true to disable the panel. - */ - public void setDisabled(String panelId, boolean disabled) { - ThreadUtils.assertOnThread(mOriginalThread); - - final PanelConfig panelConfig = getPanelOrThrow(panelId); - if (panelConfig.isDisabled() == disabled) { - return; - } - - setPanelIsDisabled(panelConfig, disabled); - - if (disabled) { - if (isCurrentDefaultPanel(panelConfig)) { - panelConfig.setIsDefault(false); - findNewDefault(); - } - } else if (mEnabledCount == 1) { - setDefault(panelId); - } - - mHasChanged = true; - } - - /** - * Adds a new {@code PanelConfig}. It will do nothing if the - * {@code Editor} already contains a panel with the same ID. - * - * @param panelConfig the {@code PanelConfig} instance to be added. - * @return true if the item has been added. - */ - public boolean install(PanelConfig panelConfig) { - ThreadUtils.assertOnThread(mOriginalThread); - - if (panelConfig == null) { - throw new IllegalStateException("Can't install a null panel"); - } - - if (!panelConfig.isDynamic()) { - throw new IllegalStateException("Can't install a built-in panel: " + panelConfig.getId()); - } - - if (panelConfig.isDisabled()) { - throw new IllegalStateException("Can't install a disabled panel: " + panelConfig.getId()); - } - - boolean installed = false; - - final String id = panelConfig.getId(); - if (!mConfigMap.containsKey(id)) { - mConfigMap.put(id, panelConfig); - - final int position = panelConfig.getPosition(); - if (position < 0 || position >= mConfigOrder.size()) { - mConfigOrder.add(id); - } else { - mConfigOrder.add(position, id); - } - - mEnabledCount++; - if (mEnabledCount == 1 || panelConfig.isDefault()) { - setDefault(panelConfig.getId()); - } - - installed = true; - - // Add an event to the queue if a new panel is successfully installed. - mNotificationQueue.add(new Pair( - "HomePanels:Installed", panelConfig.getId())); - } - - mHasChanged = true; - return installed; - } - - /** - * Removes an existing panel. - * - * @return true if the item has been removed. - */ - public boolean uninstall(String panelId) { - ThreadUtils.assertOnThread(mOriginalThread); - - final PanelConfig panelConfig = mConfigMap.get(panelId); - if (panelConfig == null) { - return false; - } - - if (!panelConfig.isDynamic()) { - throw new IllegalStateException("Can't uninstall a built-in panel: " + panelConfig.getId()); - } - - mConfigMap.remove(panelId); - mConfigOrder.remove(panelId); - - if (!panelConfig.isDisabled()) { - mEnabledCount--; - } - - if (isCurrentDefaultPanel(panelConfig)) { - findNewDefault(); - } - - // Add an event to the queue if a panel is successfully uninstalled. - mNotificationQueue.add(new Pair("HomePanels:Uninstalled", panelId)); - - mHasChanged = true; - return true; - } - - /** - * Moves panel associated with panelId to the specified position. - * - * @param panelId Id of panel - * @param destIndex Destination position - * @return true if move succeeded - */ - public boolean moveTo(String panelId, int destIndex) { - ThreadUtils.assertOnThread(mOriginalThread); - - if (!mConfigOrder.contains(panelId)) { - return false; - } - - mConfigOrder.remove(panelId); - mConfigOrder.add(destIndex, panelId); - mHasChanged = true; - - return true; - } - - /** - * Replaces an existing panel with a new {@code PanelConfig} instance. - * - * @return true if the item has been updated. - */ - public boolean update(PanelConfig panelConfig) { - ThreadUtils.assertOnThread(mOriginalThread); - - if (panelConfig == null) { - throw new IllegalStateException("Can't update a null panel"); - } - - boolean updated = false; - - final String id = panelConfig.getId(); - if (mConfigMap.containsKey(id)) { - final PanelConfig oldPanelConfig = mConfigMap.put(id, panelConfig); - - // The disabled and default states can't never be - // changed by an update operation. - panelConfig.setIsDefault(oldPanelConfig.isDefault()); - panelConfig.setIsDisabled(oldPanelConfig.isDisabled()); - - updated = true; - } - - mHasChanged = true; - return updated; - } - - /** - * Saves the current {@code Editor} state asynchronously in the - * background thread. - * - * @return the resulting {@code State} instance. - */ - public State apply() { - ThreadUtils.assertOnThread(mOriginalThread); - - // We're about to save the current state in the background thread - // so we should use a deep copy of the PanelConfig instances to - // avoid saving corrupted state. - final State newConfigState = - new State(mHomeConfig, makeOrderedCopy(true), isDefault()); - - // Copy the event queue to a new list, so that we only modify mNotificationQueue on - // the original thread where it was created. - final List> copiedQueue = mNotificationQueue; - mNotificationQueue = new ArrayList<>(); - - ThreadUtils.getBackgroundHandler().post(new Runnable() { - @Override - public void run() { - mHomeConfig.save(newConfigState); - - // Send pending events after the new config is saved. - sendNotificationsToGecko(copiedQueue); - } - }); - - return newConfigState; - } - - /** - * Saves the current {@code Editor} state synchronously in the - * current thread. - * - * @return the resulting {@code State} instance. - */ - public State commit() { - ThreadUtils.assertOnThread(mOriginalThread); - - final State newConfigState = - new State(mHomeConfig, makeOrderedCopy(false), isDefault()); - - // This is a synchronous blocking operation, hence no - // need to deep copy the current PanelConfig instances. - mHomeConfig.save(newConfigState); - - // Send pending events after the new config is saved. - sendNotificationsToGecko(mNotificationQueue); - mNotificationQueue.clear(); - - return newConfigState; - } - - /** - * Returns whether the {@code Editor} represents the default - * {@code HomeConfig} configuration without any unsaved changes. - */ - public boolean isDefault() { - ThreadUtils.assertOnThread(mOriginalThread); - - return (!mHasChanged && mIsFromDefault); - } - - public boolean isEmpty() { - return mConfigMap.isEmpty(); - } - - private void sendNotificationsToGecko(List> notifications) { - for (Pair p : notifications) { - GeckoAppShell.notifyObservers(p.first, p.second); - } - } - - private class EditorIterator implements Iterator { - private final Iterator mOrderIterator; - - public EditorIterator() { - mOrderIterator = mConfigOrder.iterator(); - } - - @Override - public boolean hasNext() { - return mOrderIterator.hasNext(); - } - - @Override - public PanelConfig next() { - final String panelId = mOrderIterator.next(); - return mConfigMap.get(panelId); - } - - @Override - public void remove() { - throw new UnsupportedOperationException("Can't 'remove' from on Editor iterator."); - } - } - - @Override - public Iterator iterator() { - ThreadUtils.assertOnThread(mOriginalThread); - - return new EditorIterator(); - } - } - - public interface OnReloadListener { - public void onReload(); - } - - public interface HomeConfigBackend { - public State load(); - public void save(State configState); - public String getLocale(); - public void setOnReloadListener(OnReloadListener listener); - } - - // UUIDs used to create PanelConfigs for default built-in panels. These are - // public because they can be used in "about:home?panel=UUID" query strings - // to open specific panels without querying the active Home Panel - // configuration. Because they don't consider the active configuration, it - // is only sensible to do this for built-in panels (and not for dynamic - // panels). - private static final String TOP_SITES_PANEL_ID = "4becc86b-41eb-429a-a042-88fe8b5a094e"; - private static final String BOOKMARKS_PANEL_ID = "7f6d419a-cd6c-4e34-b26f-f68b1b551907"; - private static final String HISTORY_PANEL_ID = "f134bf20-11f7-4867-ab8b-e8e705d7fbe8"; - private static final String COMBINED_HISTORY_PANEL_ID = "4d716ce2-e063-486d-9e7c-b190d7b04dc6"; - private static final String RECENT_TABS_PANEL_ID = "5c2601a5-eedc-4477-b297-ce4cef52adf8"; - private static final String REMOTE_TABS_PANEL_ID = "72429afd-8d8b-43d8-9189-14b779c563d0"; - private static final String DEPRECATED_READING_LIST_PANEL_ID = "20f4549a-64ad-4c32-93e4-1dcef792733b"; - - private final HomeConfigBackend mBackend; - - public HomeConfig(HomeConfigBackend backend) { - mBackend = backend; - } - - public State load() { - final State configState = mBackend.load(); - configState.setHomeConfig(this); - - return configState; - } - - public String getLocale() { - return mBackend.getLocale(); - } - - public void save(State configState) { - mBackend.save(configState); - } - - public void setOnReloadListener(OnReloadListener listener) { - mBackend.setOnReloadListener(listener); - } - - public static PanelConfig createBuiltinPanelConfig(Context context, PanelType panelType) { - return createBuiltinPanelConfig(context, panelType, EnumSet.noneOf(PanelConfig.Flags.class)); - } - - public static int getTitleResourceIdForBuiltinPanelType(PanelType panelType) { - switch (panelType) { - case TOP_SITES: - return R.string.home_top_sites_title; - - case BOOKMARKS: - case DEPRECATED_READING_LIST: - return R.string.bookmarks_title; - - case DEPRECATED_HISTORY: - case DEPRECATED_REMOTE_TABS: - case DEPRECATED_RECENT_TABS: - case COMBINED_HISTORY: - return R.string.home_history_title; - - default: - throw new IllegalArgumentException("Only for built-in panel types: " + panelType); - } - } - - public static String getIdForBuiltinPanelType(PanelType panelType) { - switch (panelType) { - case TOP_SITES: - return TOP_SITES_PANEL_ID; - - case BOOKMARKS: - return BOOKMARKS_PANEL_ID; - - case DEPRECATED_HISTORY: - return HISTORY_PANEL_ID; - - case COMBINED_HISTORY: - return COMBINED_HISTORY_PANEL_ID; - - case DEPRECATED_REMOTE_TABS: - return REMOTE_TABS_PANEL_ID; - - case DEPRECATED_READING_LIST: - return DEPRECATED_READING_LIST_PANEL_ID; - - case DEPRECATED_RECENT_TABS: - return RECENT_TABS_PANEL_ID; - - default: - throw new IllegalArgumentException("Only for built-in panel types: " + panelType); - } - } - - public static PanelConfig createBuiltinPanelConfig(Context context, PanelType panelType, EnumSet flags) { - final int titleId = getTitleResourceIdForBuiltinPanelType(panelType); - final String id = getIdForBuiltinPanelType(panelType); - - return new PanelConfig(panelType, context.getString(titleId), id, flags); - } - - public static HomeConfig getDefault(Context context) { - return new HomeConfig(new HomeConfigPrefsBackend(context)); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/HomeConfigLoader.java b/mobile/android/base/java/org/mozilla/gecko/home/HomeConfigLoader.java deleted file mode 100644 index 914d0fdd19..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/HomeConfigLoader.java +++ /dev/null @@ -1,83 +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.home.HomeConfig.OnReloadListener; - -import android.content.Context; -import android.support.v4.content.AsyncTaskLoader; - -public class HomeConfigLoader extends AsyncTaskLoader { - private final HomeConfig mConfig; - private HomeConfig.State mConfigState; - - private final Context mContext; - - public HomeConfigLoader(Context context, HomeConfig homeConfig) { - super(context); - mContext = context; - mConfig = homeConfig; - } - - @Override - public HomeConfig.State loadInBackground() { - return mConfig.load(); - } - - @Override - public void deliverResult(HomeConfig.State configState) { - if (isReset()) { - mConfigState = null; - return; - } - - mConfigState = configState; - mConfig.setOnReloadListener(new ForceReloadListener()); - - if (isStarted()) { - super.deliverResult(configState); - } - } - - @Override - protected void onStartLoading() { - if (mConfigState != null) { - deliverResult(mConfigState); - } - - if (takeContentChanged() || mConfigState == null) { - forceLoad(); - } - } - - @Override - protected void onStopLoading() { - cancelLoad(); - } - - @Override - public void onCanceled(HomeConfig.State configState) { - mConfigState = null; - } - - @Override - protected void onReset() { - super.onReset(); - - // Ensure the loader is stopped. - onStopLoading(); - - mConfigState = null; - mConfig.setOnReloadListener(null); - } - - private class ForceReloadListener implements OnReloadListener { - @Override - public void onReload() { - onContentChanged(); - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/HomeConfigPrefsBackend.java b/mobile/android/base/java/org/mozilla/gecko/home/HomeConfigPrefsBackend.java deleted file mode 100644 index a2d80788c2..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/HomeConfigPrefsBackend.java +++ /dev/null @@ -1,663 +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 static org.mozilla.gecko.home.HomeConfig.createBuiltinPanelConfig; - -import java.util.ArrayList; -import java.util.EnumSet; -import java.util.Locale; - -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; -import org.mozilla.gecko.GeckoSharedPrefs; -import org.mozilla.gecko.home.HomeConfig.HomeConfigBackend; -import org.mozilla.gecko.home.HomeConfig.OnReloadListener; -import org.mozilla.gecko.home.HomeConfig.PanelConfig; -import org.mozilla.gecko.home.HomeConfig.PanelType; -import org.mozilla.gecko.home.HomeConfig.State; -import org.mozilla.gecko.util.HardwareUtils; - -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.SharedPreferences; -import android.support.annotation.CheckResult; -import android.support.annotation.VisibleForTesting; -import android.support.v4.content.LocalBroadcastManager; -import android.text.TextUtils; -import android.util.Log; - -public class HomeConfigPrefsBackend implements HomeConfigBackend { - private static final String LOGTAG = "GeckoHomeConfigBackend"; - - // Increment this to trigger a migration. - @VisibleForTesting - static final int VERSION = 8; - - // This key was originally used to store only an array of panel configs. - public static final String PREFS_CONFIG_KEY_OLD = "home_panels"; - - // This key is now used to store a version number with the array of panel configs. - public static final String PREFS_CONFIG_KEY = "home_panels_with_version"; - - // Keys used with JSON object stored in prefs. - private static final String JSON_KEY_PANELS = "panels"; - private static final String JSON_KEY_VERSION = "version"; - - private static final String PREFS_LOCALE_KEY = "home_locale"; - - private static final String RELOAD_BROADCAST = "HomeConfigPrefsBackend:Reload"; - - private final Context mContext; - private ReloadBroadcastReceiver mReloadBroadcastReceiver; - private OnReloadListener mReloadListener; - - private static boolean sMigrationDone; - - public HomeConfigPrefsBackend(Context context) { - mContext = context; - } - - private SharedPreferences getSharedPreferences() { - return GeckoSharedPrefs.forProfile(mContext); - } - - private State loadDefaultConfig() { - final ArrayList panelConfigs = new ArrayList(); - - panelConfigs.add(createBuiltinPanelConfig(mContext, PanelType.TOP_SITES, - EnumSet.of(PanelConfig.Flags.DEFAULT_PANEL))); - - panelConfigs.add(createBuiltinPanelConfig(mContext, PanelType.BOOKMARKS)); - panelConfigs.add(createBuiltinPanelConfig(mContext, PanelType.COMBINED_HISTORY)); - - - return new State(panelConfigs, true); - } - - /** - * Iterate through the panels to check if they are all disabled. - */ - private static boolean allPanelsAreDisabled(JSONArray jsonPanels) throws JSONException { - final int count = jsonPanels.length(); - for (int i = 0; i < count; i++) { - final JSONObject jsonPanelConfig = jsonPanels.getJSONObject(i); - - if (!jsonPanelConfig.optBoolean(PanelConfig.JSON_KEY_DISABLED, false)) { - return false; - } - } - - return true; - } - - protected enum Position { - NONE, // Not present. - FRONT, // At the front of the list of panels. - BACK, // At the back of the list of panels. - } - - /** - * Create and insert a built-in panel configuration. - * - * @param context Android context. - * @param jsonPanels array of JSON panels to update in place. - * @param panelType to add. - * @param positionOnPhones where to place the new panel on phones. - * @param positionOnTablets where to place the new panel on tablets. - * @throws JSONException - */ - protected static void addBuiltinPanelConfig(Context context, JSONArray jsonPanels, - PanelType panelType, Position positionOnPhones, Position positionOnTablets) throws JSONException { - // Add the new panel. - final JSONObject jsonPanelConfig = - createBuiltinPanelConfig(context, panelType).toJSON(); - - // If any panel is enabled, then we should make the new panel enabled. - jsonPanelConfig.put(PanelConfig.JSON_KEY_DISABLED, - allPanelsAreDisabled(jsonPanels)); - - final boolean isTablet = HardwareUtils.isTablet(); - final boolean isPhone = !isTablet; - - // Maybe add the new panel to the front of the array. - if ((isPhone && positionOnPhones == Position.FRONT) || - (isTablet && positionOnTablets == Position.FRONT)) { - // This is an inefficient way to stretch [a, b, c] to [a, a, b, c]. - for (int i = jsonPanels.length(); i >= 1; i--) { - jsonPanels.put(i, jsonPanels.get(i - 1)); - } - // And this inserts [d, a, b, c]. - jsonPanels.put(0, jsonPanelConfig); - } - - // Maybe add the new panel to the back of the array. - if ((isPhone && positionOnPhones == Position.BACK) || - (isTablet && positionOnTablets == Position.BACK)) { - jsonPanels.put(jsonPanelConfig); - } - } - - /** - * Updates the panels to combine the History and Sync panels into the (Combined) History panel. - * - * Tries to replace the History panel with the Combined History panel if visible, or falls back to - * replacing the Sync panel if it's visible. That way, we minimize panel reordering during a migration. - * @param context Android context - * @param jsonPanels array of original JSON panels - * @return new array of updated JSON panels - * @throws JSONException - */ - private static JSONArray combineHistoryAndSyncPanels(Context context, JSONArray jsonPanels) throws JSONException { - EnumSet historyFlags = null; - EnumSet syncFlags = null; - - int historyIndex = -1; - int syncIndex = -1; - - // Determine state and location of History and Sync panels. - for (int i = 0; i < jsonPanels.length(); i++) { - JSONObject panelObj = jsonPanels.getJSONObject(i); - final PanelConfig panelConfig = new PanelConfig(panelObj); - final PanelType type = panelConfig.getType(); - if (type == PanelType.DEPRECATED_HISTORY) { - historyIndex = i; - historyFlags = panelConfig.getFlags(); - } else if (type == PanelType.DEPRECATED_REMOTE_TABS) { - syncIndex = i; - syncFlags = panelConfig.getFlags(); - } else if (type == PanelType.COMBINED_HISTORY) { - // Partial landing of bug 1220928 combined the History and Sync panels of users who didn't - // have home panel customizations (including new users), thus they don't this migration. - return jsonPanels; - } - } - - if (historyIndex == -1 || syncIndex == -1) { - throw new IllegalArgumentException("Expected both History and Sync panels to be present prior to Combined History."); - } - - PanelConfig newPanel; - int replaceIndex; - int removeIndex; - - if (historyFlags.contains(PanelConfig.Flags.DISABLED_PANEL) && !syncFlags.contains(PanelConfig.Flags.DISABLED_PANEL)) { - // Replace the Sync panel if it's visible and the History panel is disabled. - replaceIndex = syncIndex; - removeIndex = historyIndex; - newPanel = createBuiltinPanelConfig(context, PanelType.COMBINED_HISTORY, syncFlags); - } else { - // Otherwise, just replace the History panel. - replaceIndex = historyIndex; - removeIndex = syncIndex; - newPanel = createBuiltinPanelConfig(context, PanelType.COMBINED_HISTORY, historyFlags); - } - - // Copy the array with updated panel and removed panel. - final JSONArray newArray = new JSONArray(); - for (int i = 0; i < jsonPanels.length(); i++) { - if (i == replaceIndex) { - newArray.put(newPanel.toJSON()); - } else if (i == removeIndex) { - continue; - } else { - newArray.put(jsonPanels.get(i)); - } - } - - return newArray; - } - - /** - * Iterate over all homepanels to verify that there is at least one default panel. If there is - * no default panel, set History as the default panel. (This is only relevant for two botched - * migrations where the history panel should have been made the default panel, but wasn't.) - */ - private static void ensureDefaultPanelForV5orV8(Context context, JSONArray jsonPanels) throws JSONException { - int historyIndex = -1; - - // If all panels are disabled, there is no default panel - this is the only valid state - // that has no default. We can use this flag to track whether any visible panels have been - // found. - boolean enabledPanelsFound = false; - - for (int i = 0; i < jsonPanels.length(); i++) { - final PanelConfig panelConfig = new PanelConfig(jsonPanels.getJSONObject(i)); - if (panelConfig.isDefault()) { - return; - } - - if (!panelConfig.isDisabled()) { - enabledPanelsFound = true; - } - - if (panelConfig.getType() == PanelType.COMBINED_HISTORY) { - historyIndex = i; - } - } - - if (!enabledPanelsFound) { - // No panels are enabled, hence there can be no default (see noEnabledPanelsFound declaration - // for more information). - return; - } - - // Make the History panel default. We can't modify existing PanelConfigs, so make a new one. - final PanelConfig historyPanelConfig = createBuiltinPanelConfig(context, PanelType.COMBINED_HISTORY, EnumSet.of(PanelConfig.Flags.DEFAULT_PANEL)); - jsonPanels.put(historyIndex, historyPanelConfig.toJSON()); - } - - /** - * Removes a panel from the home panel config. - * If the removed panel was set as the default home panel, we provide a replacement for it. - * - * @param context Android context - * @param jsonPanels array of original JSON panels - * @param panelToRemove The home panel to be removed. - * @param replacementPanel The panel which will replace it if the removed panel - * was the default home panel. - * @param alwaysUnhide If true, the replacement panel will always be unhidden, - * otherwise only if we turn it into the new default panel. - * @return new array of updated JSON panels - * @throws JSONException - */ - private static JSONArray removePanel(Context context, JSONArray jsonPanels, - PanelType panelToRemove, PanelType replacementPanel, boolean alwaysUnhide) throws JSONException { - boolean wasDefault = false; - boolean wasDisabled = false; - int replacementPanelIndex = -1; - boolean replacementWasDefault = false; - - // JSONArrary doesn't provide remove() for API < 19, therefore we need to manually copy all - // the items we don't want deleted into a new array. - final JSONArray newJSONPanels = new JSONArray(); - - for (int i = 0; i < jsonPanels.length(); i++) { - final JSONObject panelJSON = jsonPanels.getJSONObject(i); - final PanelConfig panelConfig = new PanelConfig(panelJSON); - - if (panelConfig.getType() == panelToRemove) { - // If this panel was the default we'll need to assign a new default: - wasDefault = panelConfig.isDefault(); - wasDisabled = panelConfig.isDisabled(); - } else { - if (panelConfig.getType() == replacementPanel) { - replacementPanelIndex = newJSONPanels.length(); - if (panelConfig.isDefault()) { - replacementWasDefault = true; - } - } - - newJSONPanels.put(panelJSON); - } - } - - // Unless alwaysUnhide is true, we make the replacement panel visible only if it is going - // to be the new default panel, since a hidden default panel doesn't make sense. - // This is to allow preserving the behaviour of the original reading list migration function. - if ((wasDefault || alwaysUnhide) && !wasDisabled) { - final JSONObject replacementPanelConfig; - if (wasDefault) { - // If the removed panel was the default, the replacement has to be made the new default - replacementPanelConfig = createBuiltinPanelConfig(context, replacementPanel, EnumSet.of(PanelConfig.Flags.DEFAULT_PANEL)).toJSON(); - } else { - final EnumSet flags; - if (replacementWasDefault) { - // However if the replacement panel was already default, we need to preserve it's default status - // (By rewriting the PanelConfig, we lose all existing flags, so we need to make sure desired - // flags are retained - in this case there's only DEFAULT_PANEL, which is mutually - // exclusive with the DISABLE_PANEL case). - flags = EnumSet.of(PanelConfig.Flags.DEFAULT_PANEL); - } else { - flags = EnumSet.noneOf(PanelConfig.Flags.class); - } - - // The panel is visible since we don't set Flags.DISABLED_PANEL. - replacementPanelConfig = createBuiltinPanelConfig(context, replacementPanel, flags).toJSON(); - } - - if (replacementPanelIndex != -1) { - newJSONPanels.put(replacementPanelIndex, replacementPanelConfig); - } else { - newJSONPanels.put(replacementPanelConfig); - } - } - - return newJSONPanels; - } - - /** - * Checks to see if the reading list panel already exists. - * - * @param jsonPanels JSONArray array representing the curent set of panel configs. - * - * @return boolean Whether or not the reading list panel exists. - */ - private static boolean readingListPanelExists(JSONArray jsonPanels) { - final int count = jsonPanels.length(); - for (int i = 0; i < count; i++) { - try { - final JSONObject jsonPanelConfig = jsonPanels.getJSONObject(i); - final PanelConfig panelConfig = new PanelConfig(jsonPanelConfig); - if (panelConfig.getType() == PanelType.DEPRECATED_READING_LIST) { - return true; - } - } catch (Exception e) { - // It's okay to ignore this exception, since an invalid reading list - // panel config is equivalent to no reading list panel. - Log.e(LOGTAG, "Exception loading PanelConfig from JSON", e); - } - } - return false; - } - - @CheckResult - static synchronized JSONArray migratePrefsFromVersionToVersion(final Context context, final int currentVersion, final int newVersion, - final JSONArray jsonPanelsIn, final SharedPreferences.Editor prefsEditor) throws JSONException { - - JSONArray jsonPanels = jsonPanelsIn; - - for (int v = currentVersion + 1; v <= newVersion; v++) { - Log.d(LOGTAG, "Migrating to version = " + v); - - switch (v) { - case 1: - // Add "Recent Tabs" panel. - addBuiltinPanelConfig(context, jsonPanels, - PanelType.DEPRECATED_RECENT_TABS, Position.FRONT, Position.BACK); - - // Remove the old pref key. - prefsEditor.remove(PREFS_CONFIG_KEY_OLD); - break; - - case 2: - // Add "Remote Tabs"/"Synced Tabs" panel. - addBuiltinPanelConfig(context, jsonPanels, - PanelType.DEPRECATED_REMOTE_TABS, Position.FRONT, Position.BACK); - break; - - case 3: - // Add the "Reading List" panel if it does not exist. At one time, - // the Reading List panel was shown only to devices that were not - // considered "low memory". Now, we expose the panel to all devices. - // This migration should only occur for "low memory" devices. - // Note: This will not agree with the default configuration, which - // has DEPRECATED_REMOTE_TABS after DEPRECATED_READING_LIST on some devices. - if (!readingListPanelExists(jsonPanels)) { - addBuiltinPanelConfig(context, jsonPanels, - PanelType.DEPRECATED_READING_LIST, Position.BACK, Position.BACK); - } - break; - - case 4: - // Combine the History and Sync panels. In order to minimize an unexpected reordering - // of panels, we try to replace the History panel if it's visible, and fall back to - // the Sync panel if that's visible. - jsonPanels = combineHistoryAndSyncPanels(context, jsonPanels); - break; - - case 5: - // This is the fix for bug 1264136 where we lost track of the default panel during some migrations. - ensureDefaultPanelForV5orV8(context, jsonPanels); - break; - - case 6: - jsonPanels = removePanel(context, jsonPanels, - PanelType.DEPRECATED_READING_LIST, PanelType.BOOKMARKS, false); - break; - - case 7: - jsonPanels = removePanel(context, jsonPanels, - PanelType.DEPRECATED_RECENT_TABS, PanelType.COMBINED_HISTORY, true); - break; - - case 8: - // Similar to "case 5" above, this time 1304777 - once again we lost track - // of the history panel - ensureDefaultPanelForV5orV8(context, jsonPanels); - break; - } - } - - return jsonPanels; - } - - /** - * Migrates JSON config data storage. - * - * @param context Context used to get shared preferences and create built-in panel. - * @param jsonString String currently stored in preferences. - * - * @return JSONArray array representing new set of panel configs. - */ - private static synchronized JSONArray maybePerformMigration(Context context, String jsonString) throws JSONException { - // If the migration is already done, we're at the current version. - if (sMigrationDone) { - final JSONObject json = new JSONObject(jsonString); - return json.getJSONArray(JSON_KEY_PANELS); - } - - // Make sure we only do this version check once. - sMigrationDone = true; - - JSONArray jsonPanels; - final int version; - - final SharedPreferences prefs = GeckoSharedPrefs.forProfile(context); - if (prefs.contains(PREFS_CONFIG_KEY_OLD)) { - // Our original implementation did not contain versioning, so this is implicitly version 0. - jsonPanels = new JSONArray(jsonString); - version = 0; - } else { - final JSONObject json = new JSONObject(jsonString); - jsonPanels = json.getJSONArray(JSON_KEY_PANELS); - version = json.getInt(JSON_KEY_VERSION); - } - - if (version == VERSION) { - return jsonPanels; - } - - Log.d(LOGTAG, "Performing migration"); - - final SharedPreferences.Editor prefsEditor = prefs.edit(); - - jsonPanels = migratePrefsFromVersionToVersion(context, version, VERSION, jsonPanels, prefsEditor); - - // Save the new panel config and the new version number. - final JSONObject newJson = new JSONObject(); - newJson.put(JSON_KEY_PANELS, jsonPanels); - newJson.put(JSON_KEY_VERSION, VERSION); - - prefsEditor.putString(PREFS_CONFIG_KEY, newJson.toString()); - prefsEditor.apply(); - - return jsonPanels; - } - - private State loadConfigFromString(String jsonString) { - final JSONArray jsonPanelConfigs; - try { - jsonPanelConfigs = maybePerformMigration(mContext, jsonString); - updatePrefsFromConfig(jsonPanelConfigs); - } catch (JSONException e) { - Log.e(LOGTAG, "Error loading the list of home panels from JSON prefs", e); - - // Fallback to default config - return loadDefaultConfig(); - } - - final ArrayList panelConfigs = new ArrayList(); - - final int count = jsonPanelConfigs.length(); - for (int i = 0; i < count; i++) { - try { - final JSONObject jsonPanelConfig = jsonPanelConfigs.getJSONObject(i); - final PanelConfig panelConfig = new PanelConfig(jsonPanelConfig); - panelConfigs.add(panelConfig); - } catch (Exception e) { - Log.e(LOGTAG, "Exception loading PanelConfig from JSON", e); - } - } - - return new State(panelConfigs, false); - } - - @Override - public State load() { - final SharedPreferences prefs = getSharedPreferences(); - - final String key = (prefs.contains(PREFS_CONFIG_KEY_OLD) ? PREFS_CONFIG_KEY_OLD : PREFS_CONFIG_KEY); - final String jsonString = prefs.getString(key, null); - - final State configState; - if (TextUtils.isEmpty(jsonString)) { - configState = loadDefaultConfig(); - } else { - configState = loadConfigFromString(jsonString); - } - - return configState; - } - - @Override - public void save(State configState) { - final SharedPreferences prefs = getSharedPreferences(); - final SharedPreferences.Editor editor = prefs.edit(); - - // No need to save the state to disk if it represents the default - // HomeConfig configuration. Simply force all existing HomeConfigLoader - // instances to refresh their contents. - if (!configState.isDefault()) { - final JSONArray jsonPanelConfigs = new JSONArray(); - - for (PanelConfig panelConfig : configState) { - try { - final JSONObject jsonPanelConfig = panelConfig.toJSON(); - jsonPanelConfigs.put(jsonPanelConfig); - } catch (Exception e) { - Log.e(LOGTAG, "Exception converting PanelConfig to JSON", e); - } - } - - try { - final JSONObject json = new JSONObject(); - json.put(JSON_KEY_PANELS, jsonPanelConfigs); - json.put(JSON_KEY_VERSION, VERSION); - - editor.putString(PREFS_CONFIG_KEY, json.toString()); - } catch (JSONException e) { - Log.e(LOGTAG, "Exception saving PanelConfig state", e); - } - } - - editor.putString(PREFS_LOCALE_KEY, Locale.getDefault().toString()); - editor.apply(); - - // Trigger reload listeners on all live backend instances - sendReloadBroadcast(); - } - - @Override - public String getLocale() { - final SharedPreferences prefs = getSharedPreferences(); - - String locale = prefs.getString(PREFS_LOCALE_KEY, null); - if (locale == null) { - // Initialize config with the current locale - final String currentLocale = Locale.getDefault().toString(); - - final SharedPreferences.Editor editor = prefs.edit(); - editor.putString(PREFS_LOCALE_KEY, currentLocale); - editor.apply(); - - // If the user has saved HomeConfig before, return null this - // one time to trigger a refresh and ensure we use the - // correct locale for the saved state. For more context, - // see HomePanelsManager.onLocaleReady(). - if (!prefs.contains(PREFS_CONFIG_KEY)) { - locale = currentLocale; - } - } - - return locale; - } - - @Override - public void setOnReloadListener(OnReloadListener listener) { - if (mReloadListener != null) { - unregisterReloadReceiver(); - mReloadBroadcastReceiver = null; - } - - mReloadListener = listener; - - if (mReloadListener != null) { - mReloadBroadcastReceiver = new ReloadBroadcastReceiver(); - registerReloadReceiver(); - } - } - - /** - * Update prefs that depend on home panels state. - * - * This includes the prefs that keep track of whether bookmarks or history are enabled, which are - * used to control the visibility of the corresponding menu items. - */ - private void updatePrefsFromConfig(JSONArray panelsArray) { - final SharedPreferences prefs = GeckoSharedPrefs.forProfile(mContext); - if (!prefs.contains(HomeConfig.PREF_KEY_BOOKMARKS_PANEL_ENABLED) - || !prefs.contains(HomeConfig.PREF_KEY_HISTORY_PANEL_ENABLED)) { - - final String bookmarkType = PanelType.BOOKMARKS.toString(); - final String historyType = PanelType.COMBINED_HISTORY.toString(); - try { - for (int i = 0; i < panelsArray.length(); i++) { - final JSONObject panelObj = panelsArray.getJSONObject(i); - final String panelType = panelObj.optString(PanelConfig.JSON_KEY_TYPE, null); - if (panelType == null) { - break; - } - final boolean isDisabled = panelObj.optBoolean(PanelConfig.JSON_KEY_DISABLED, false); - if (bookmarkType.equals(panelType)) { - prefs.edit().putBoolean(HomeConfig.PREF_KEY_BOOKMARKS_PANEL_ENABLED, !isDisabled).apply(); - } else if (historyType.equals(panelType)) { - prefs.edit().putBoolean(HomeConfig.PREF_KEY_HISTORY_PANEL_ENABLED, !isDisabled).apply(); - } - } - } catch (JSONException e) { - Log.e(LOGTAG, "Error fetching panel from config to update prefs"); - } - } - } - - - private void sendReloadBroadcast() { - final LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(mContext); - final Intent reloadIntent = new Intent(RELOAD_BROADCAST); - lbm.sendBroadcast(reloadIntent); - } - - private void registerReloadReceiver() { - final LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(mContext); - lbm.registerReceiver(mReloadBroadcastReceiver, new IntentFilter(RELOAD_BROADCAST)); - } - - private void unregisterReloadReceiver() { - final LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(mContext); - lbm.unregisterReceiver(mReloadBroadcastReceiver); - } - - private class ReloadBroadcastReceiver extends BroadcastReceiver { - @Override - public void onReceive(Context context, Intent intent) { - mReloadListener.onReload(); - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/HomeContextMenuInfo.java b/mobile/android/base/java/org/mozilla/gecko/home/HomeContextMenuInfo.java deleted file mode 100644 index cefa0329da..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/HomeContextMenuInfo.java +++ /dev/null @@ -1,82 +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.db.BrowserContract; -import org.mozilla.gecko.util.StringUtils; - -import android.database.Cursor; -import android.text.TextUtils; -import android.view.View; -import android.widget.AdapterView.AdapterContextMenuInfo; -import android.widget.ExpandableListAdapter; -import android.widget.ListAdapter; - -/** - * A ContextMenuInfo for HomeListView - */ -public class HomeContextMenuInfo extends AdapterContextMenuInfo { - - public String url; - public String title; - public boolean isFolder; - public int historyId = -1; - public int bookmarkId = -1; - public RemoveItemType itemType = null; - - // Item type to be handled with "Remove" selection. - public static enum RemoveItemType { - BOOKMARKS, COMBINED, HISTORY - } - - public HomeContextMenuInfo(View targetView, int position, long id) { - super(targetView, position, id); - } - - public boolean hasBookmarkId() { - return bookmarkId > -1; - } - - public boolean hasHistoryId() { - return historyId > -1; - } - - public boolean hasPartnerBookmarkId() { - return bookmarkId <= BrowserContract.Bookmarks.FAKE_PARTNER_BOOKMARKS_START; - } - - public boolean canRemove() { - return hasBookmarkId() || hasHistoryId() || hasPartnerBookmarkId(); - } - - public String getDisplayTitle() { - if (!TextUtils.isEmpty(title)) { - return title; - } - return StringUtils.stripCommonSubdomains(StringUtils.stripScheme(url, StringUtils.UrlFlags.STRIP_HTTPS)); - } - - /** - * Interface for creating ContextMenuInfo instances from cursors. - */ - public interface Factory { - public HomeContextMenuInfo makeInfoForCursor(View view, int position, long id, Cursor cursor); - } - - /** - * Interface for creating ContextMenuInfo instances from ListAdapters. - */ - public interface ListFactory extends Factory { - public HomeContextMenuInfo makeInfoForAdapter(View view, int position, long id, ListAdapter adapter); - } - - /** - * Interface for creating ContextMenuInfo instances from ExpandableListAdapters. - */ - public interface ExpandableFactory { - public HomeContextMenuInfo makeInfoForAdapter(View view, int position, long id, ExpandableListAdapter adapter); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/HomeExpandableListView.java b/mobile/android/base/java/org/mozilla/gecko/home/HomeExpandableListView.java deleted file mode 100644 index 7badd69290..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/HomeExpandableListView.java +++ /dev/null @@ -1,68 +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.util.AttributeSet; -import android.view.ContextMenu.ContextMenuInfo; -import android.view.View; -import android.widget.AdapterView; -import android.widget.AdapterView.OnItemLongClickListener; -import android.widget.ExpandableListView; - -/** - * HomeExpandableListView is a custom extension of - * ExpandableListView, that packs a HomeContextMenuInfo - * when any of its rows is long pressed. - *

- * This is the ExpandableListView equivalent of - * HomeListView. - */ -public class HomeExpandableListView extends ExpandableListView - implements OnItemLongClickListener { - - // ContextMenuInfo associated with the currently long pressed list item. - private HomeContextMenuInfo mContextMenuInfo; - - // ContextMenuInfo factory. - private HomeContextMenuInfo.ExpandableFactory mContextMenuInfoFactory; - - public HomeExpandableListView(Context context) { - this(context, null); - } - - public HomeExpandableListView(Context context, AttributeSet attrs) { - this(context, attrs, 0); - } - - public HomeExpandableListView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - - setOnItemLongClickListener(this); - } - - @Override - public boolean onItemLongClick(AdapterView parent, View view, int position, long id) { - if (mContextMenuInfoFactory == null) { - return false; - } - - // HomeExpandableListView items can correspond to groups and children. - // The factory can determine whether to add context menu for either, - // both, or none by unpacking the given position. - mContextMenuInfo = mContextMenuInfoFactory.makeInfoForAdapter(view, position, id, getExpandableListAdapter()); - return showContextMenuForChild(HomeExpandableListView.this); - } - - @Override - public ContextMenuInfo getContextMenuInfo() { - return mContextMenuInfo; - } - - public void setContextMenuInfoFactory(final HomeContextMenuInfo.ExpandableFactory factory) { - mContextMenuInfoFactory = factory; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/HomeFragment.java b/mobile/android/base/java/org/mozilla/gecko/home/HomeFragment.java deleted file mode 100644 index da6e9b7038..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/HomeFragment.java +++ /dev/null @@ -1,498 +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 java.util.EnumSet; - -import org.mozilla.gecko.EditBookmarkDialog; -import org.mozilla.gecko.GeckoAppShell; -import org.mozilla.gecko.GeckoApplication; -import org.mozilla.gecko.GeckoProfile; -import org.mozilla.gecko.IntentHelper; -import org.mozilla.gecko.R; -import org.mozilla.gecko.SnackbarBuilder; -import org.mozilla.gecko.Telemetry; -import org.mozilla.gecko.TelemetryContract; -import org.mozilla.gecko.db.BrowserContract; -import org.mozilla.gecko.db.BrowserDB; -import org.mozilla.gecko.db.BrowserContract.SuggestedSites; -import org.mozilla.gecko.distribution.PartnerBookmarksProviderProxy; -import org.mozilla.gecko.home.HomeContextMenuInfo.RemoveItemType; -import org.mozilla.gecko.home.HomePager.OnUrlOpenInBackgroundListener; -import org.mozilla.gecko.home.HomePager.OnUrlOpenListener; -import org.mozilla.gecko.home.TopSitesGridView.TopSitesGridContextMenuInfo; -import org.mozilla.gecko.reader.SavedReaderViewHelper; -import org.mozilla.gecko.reader.ReadingListHelper; -import org.mozilla.gecko.restrictions.Restrictable; -import org.mozilla.gecko.restrictions.Restrictions; -import org.mozilla.gecko.util.Clipboard; -import org.mozilla.gecko.util.StringUtils; -import org.mozilla.gecko.util.ThreadUtils; -import org.mozilla.gecko.util.UIAsyncTask; - -import android.app.Activity; -import android.content.ContentResolver; -import android.content.Context; -import android.content.Intent; -import android.content.res.Configuration; -import android.os.Bundle; -import android.support.design.widget.Snackbar; -import android.support.v4.app.Fragment; -import android.text.TextUtils; -import android.util.Log; -import android.view.ContextMenu; -import android.view.ContextMenu.ContextMenuInfo; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.view.View; - -/** - * HomeFragment is an empty fragment that can be added to the HomePager. - * Subclasses can add their own views. - *

- * The containing activity must implement {@link OnUrlOpenListener}. - */ -public abstract class HomeFragment extends Fragment { - // Log Tag. - private static final String LOGTAG = "GeckoHomeFragment"; - - // Share MIME type. - protected static final String SHARE_MIME_TYPE = "text/plain"; - - // Default value for "can load" hint - static final boolean DEFAULT_CAN_LOAD_HINT = false; - - // Whether the fragment can load its content or not - // This is used to defer data loading until the editing - // mode animation ends. - private boolean mCanLoadHint; - - // Whether the fragment has loaded its content - private boolean mIsLoaded; - - // On URL open listener - protected OnUrlOpenListener mUrlOpenListener; - - // Helper for opening a tab in the background. - protected OnUrlOpenInBackgroundListener mUrlOpenInBackgroundListener; - - protected PanelStateChangeListener mPanelStateChangeListener = null; - - /** - * Listener to notify when a home panels' state has changed in a way that needs to be stored - * for history/restoration. E.g. when a folder is opened/closed in bookmarks. - */ - public interface PanelStateChangeListener { - - /** - * @param bundle Data that should be persisted, and passed to this panel if restored at a later - * stage. - */ - void onStateChanged(Bundle bundle); - - void setCachedRecentTabsCount(int count); - - int getCachedRecentTabsCount(); - } - - public void restoreData(Bundle data) { - // Do nothing - } - - public void setPanelStateChangeListener( - PanelStateChangeListener mPanelStateChangeListener) { - this.mPanelStateChangeListener = mPanelStateChangeListener; - } - - @Override - public void onAttach(Activity activity) { - super.onAttach(activity); - - try { - mUrlOpenListener = (OnUrlOpenListener) activity; - } catch (ClassCastException e) { - throw new ClassCastException(activity.toString() - + " must implement HomePager.OnUrlOpenListener"); - } - - try { - mUrlOpenInBackgroundListener = (OnUrlOpenInBackgroundListener) activity; - } catch (ClassCastException e) { - throw new ClassCastException(activity.toString() - + " must implement HomePager.OnUrlOpenInBackgroundListener"); - } - } - - @Override - public void onDetach() { - super.onDetach(); - mUrlOpenListener = null; - mUrlOpenInBackgroundListener = null; - } - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - final Bundle args = getArguments(); - if (args != null) { - mCanLoadHint = args.getBoolean(HomePager.CAN_LOAD_ARG, DEFAULT_CAN_LOAD_HINT); - } else { - mCanLoadHint = DEFAULT_CAN_LOAD_HINT; - } - - mIsLoaded = false; - } - - @Override - public void onDestroy() { - super.onDestroy(); - - GeckoApplication.watchReference(getActivity(), this); - } - - @Override - public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfo) { - if (!(menuInfo instanceof HomeContextMenuInfo)) { - return; - } - - HomeContextMenuInfo info = (HomeContextMenuInfo) menuInfo; - - // Don't show the context menu for folders. - if (info.isFolder) { - return; - } - - MenuInflater inflater = new MenuInflater(view.getContext()); - inflater.inflate(R.menu.home_contextmenu, menu); - - menu.setHeaderTitle(info.getDisplayTitle()); - - // Hide unused menu items. - menu.findItem(R.id.top_sites_edit).setVisible(false); - menu.findItem(R.id.top_sites_pin).setVisible(false); - menu.findItem(R.id.top_sites_unpin).setVisible(false); - - // Hide the "Edit" menuitem if this item isn't a bookmark, - // or if this is a reading list item. - if (!info.hasBookmarkId()) { - menu.findItem(R.id.home_edit_bookmark).setVisible(false); - } - - // Hide the "Remove" menuitem if this item not removable. - if (!info.canRemove()) { - menu.findItem(R.id.home_remove).setVisible(false); - } - - if (!StringUtils.isShareableUrl(info.url) || GeckoProfile.get(getActivity()).inGuestMode()) { - menu.findItem(R.id.home_share).setVisible(false); - } - - if (!Restrictions.isAllowed(view.getContext(), Restrictable.PRIVATE_BROWSING)) { - menu.findItem(R.id.home_open_private_tab).setVisible(false); - } - } - - @Override - public boolean onContextItemSelected(MenuItem item) { - // onContextItemSelected() is first dispatched to the activity and - // then dispatched to its fragments. Since fragments cannot "override" - // menu item selection handling, it's better to avoid menu id collisions - // between the activity and its fragments. - - ContextMenuInfo menuInfo = item.getMenuInfo(); - if (!(menuInfo instanceof HomeContextMenuInfo)) { - return false; - } - - final HomeContextMenuInfo info = (HomeContextMenuInfo) menuInfo; - final Context context = getActivity(); - - final int itemId = item.getItemId(); - - // Track the menu action. We don't know much about the context, but we can use this to determine - // the frequency of use for various actions. - String extras = getResources().getResourceEntryName(itemId); - if (TextUtils.equals(extras, "home_open_private_tab")) { - // Mask private browsing - extras = "home_open_new_tab"; - } - Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.CONTEXT_MENU, extras); - - if (itemId == R.id.home_copyurl) { - if (info.url == null) { - Log.e(LOGTAG, "Can't copy address because URL is null"); - return false; - } - - Clipboard.setText(info.url); - return true; - } - - if (itemId == R.id.home_share) { - if (info.url == null) { - Log.e(LOGTAG, "Can't share because URL is null"); - return false; - } else { - IntentHelper.openUriExternal(info.url, SHARE_MIME_TYPE, "", "", - Intent.ACTION_SEND, info.getDisplayTitle(), false); - - // Context: Sharing via chrome homepage contextmenu list (home session should be active) - Telemetry.sendUIEvent(TelemetryContract.Event.SHARE, TelemetryContract.Method.LIST, "home_contextmenu"); - return true; - } - } - - if (itemId == R.id.home_add_to_launcher) { - if (info.url == null) { - Log.e(LOGTAG, "Can't add to home screen because URL is null"); - return false; - } - - // Fetch an icon big enough for use as a home screen icon. - final String displayTitle = info.getDisplayTitle(); - ThreadUtils.postToBackgroundThread(new Runnable() { - @Override - public void run() { - GeckoAppShell.createShortcut(displayTitle, info.url); - - } - }); - - return true; - } - - if (itemId == R.id.home_open_private_tab || itemId == R.id.home_open_new_tab) { - if (info.url == null) { - Log.e(LOGTAG, "Can't open in new tab because URL is null"); - return false; - } - - // Some pinned site items have "user-entered" urls. URLs entered in - // the PinSiteDialog are wrapped in a special URI until we can get a - // valid URL. If the url is a user-entered url, decode the URL - // before loading it. - final String url = StringUtils.decodeUserEnteredUrl(info.url); - - final EnumSet flags = EnumSet.noneOf(OnUrlOpenInBackgroundListener.Flags.class); - if (item.getItemId() == R.id.home_open_private_tab) { - flags.add(OnUrlOpenInBackgroundListener.Flags.PRIVATE); - } - - mUrlOpenInBackgroundListener.onUrlOpenInBackground(url, flags); - - Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.CONTEXT_MENU); - - return true; - } - - if (itemId == R.id.home_edit_bookmark) { - // UI Dialog associates to the activity context, not the applications'. - new EditBookmarkDialog(context).show(info.url); - return true; - } - - if (itemId == R.id.home_remove) { - // For Top Sites grid items, position is required in case item is Pinned. - final int position = info instanceof TopSitesGridContextMenuInfo ? info.position : -1; - - if (info.hasPartnerBookmarkId()) { - new RemovePartnerBookmarkTask(context, info.bookmarkId).execute(); - } else { - new RemoveItemByUrlTask(context, info.url, info.itemType, position).execute(); - } - return true; - } - - return false; - } - - @Override - public void setUserVisibleHint (boolean isVisibleToUser) { - if (isVisibleToUser == getUserVisibleHint()) { - return; - } - - super.setUserVisibleHint(isVisibleToUser); - loadIfVisible(); - } - - /** - * Handle a configuration change by detaching and re-attaching. - *

- * A HomeFragment only needs to handle onConfiguration change (i.e., - * re-attach) if its UI needs to change (i.e., re-inflate layouts, use - * different styles, etc) for different device orientations. Handling - * configuration changes in all HomeFragments will simply cause some - * redundant re-inflations on device rotation. This slight inefficiency - * avoids potentially not handling a needed onConfigurationChanged in a - * subclass. - */ - @Override - public void onConfigurationChanged(Configuration newConfig) { - super.onConfigurationChanged(newConfig); - - // Reattach the fragment, forcing a re-inflation of its view. - // We use commitAllowingStateLoss() instead of commit() here to avoid - // an IllegalStateException. If the phone is rotated while Fennec - // is in the background, onConfigurationChanged() is fired. - // onConfigurationChanged() is called before onResume(), so - // using commit() would throw an IllegalStateException since it can't - // be used between the Activity's onSaveInstanceState() and - // onResume(). - if (isVisible()) { - getFragmentManager().beginTransaction() - .detach(this) - .attach(this) - .commitAllowingStateLoss(); - } - } - - void setCanLoadHint(boolean canLoadHint) { - if (mCanLoadHint == canLoadHint) { - return; - } - - mCanLoadHint = canLoadHint; - loadIfVisible(); - } - - boolean getCanLoadHint() { - return mCanLoadHint; - } - - protected abstract void load(); - - protected boolean canLoad() { - return (mCanLoadHint && isVisible() && getUserVisibleHint()); - } - - protected void loadIfVisible() { - if (!canLoad() || mIsLoaded) { - return; - } - - load(); - mIsLoaded = true; - } - - protected static class RemoveItemByUrlTask extends UIAsyncTask.WithoutParams { - private final Context mContext; - private final String mUrl; - private final RemoveItemType mType; - private final int mPosition; - private final BrowserDB mDB; - - /** - * Remove bookmark/history/reading list type item by url, and also unpin the - * Top Sites grid item at index position. - */ - public RemoveItemByUrlTask(Context context, String url, RemoveItemType type, int position) { - super(ThreadUtils.getBackgroundHandler()); - - mContext = context; - mUrl = url; - mType = type; - mPosition = position; - mDB = BrowserDB.from(context); - } - - @Override - public Void doInBackground() { - ContentResolver cr = mContext.getContentResolver(); - - if (mPosition > -1) { - mDB.unpinSite(cr, mPosition); - if (mDB.hideSuggestedSite(mUrl)) { - cr.notifyChange(SuggestedSites.CONTENT_URI, null); - } - } - - switch (mType) { - case BOOKMARKS: - removeBookmark(cr); - break; - - case HISTORY: - removeHistory(cr); - break; - - case COMBINED: - removeBookmark(cr); - removeHistory(cr); - break; - - default: - Log.e(LOGTAG, "Can't remove item type " + mType.toString()); - break; - } - return null; - } - - @Override - public void onPostExecute(Void result) { - SnackbarBuilder.builder((Activity) mContext) - .message(R.string.page_removed) - .duration(Snackbar.LENGTH_LONG) - .buildAndShow(); - } - - private void removeBookmark(ContentResolver cr) { - SavedReaderViewHelper rch = SavedReaderViewHelper.getSavedReaderViewHelper(mContext); - final boolean isReaderViewPage = rch.isURLCached(mUrl); - - final String extra; - if (isReaderViewPage) { - extra = "bookmark_reader"; - } else { - extra = "bookmark"; - } - - Telemetry.sendUIEvent(TelemetryContract.Event.UNSAVE, TelemetryContract.Method.CONTEXT_MENU, extra); - mDB.removeBookmarksWithURL(cr, mUrl); - - if (isReaderViewPage) { - ReadingListHelper.removeCachedReaderItem(mUrl, mContext); - } - } - - private void removeHistory(ContentResolver cr) { - mDB.removeHistoryEntry(cr, mUrl); - } - } - - private static class RemovePartnerBookmarkTask extends UIAsyncTask.WithoutParams { - private Context context; - private long bookmarkId; - - public RemovePartnerBookmarkTask(Context context, long bookmarkId) { - super(ThreadUtils.getBackgroundHandler()); - - this.context = context; - this.bookmarkId = bookmarkId; - } - - @Override - protected Void doInBackground() { - context.getContentResolver().delete( - PartnerBookmarksProviderProxy.getUriForBookmark(context, bookmarkId), - null, - null - ); - - return null; - } - - @Override - protected void onPostExecute(Void aVoid) { - SnackbarBuilder.builder((Activity) context) - .message(R.string.page_removed) - .duration(Snackbar.LENGTH_LONG) - .buildAndShow(); - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/HomeListView.java b/mobile/android/base/java/org/mozilla/gecko/home/HomeListView.java deleted file mode 100644 index d179a27ce0..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/HomeListView.java +++ /dev/null @@ -1,138 +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.R; -import org.mozilla.gecko.home.HomePager.OnUrlOpenListener; - -import android.content.Context; -import android.content.res.TypedArray; -import android.database.Cursor; -import android.graphics.drawable.Drawable; -import android.util.AttributeSet; -import android.view.ContextMenu.ContextMenuInfo; -import android.view.View; -import android.widget.AdapterView; -import android.widget.AdapterView.OnItemLongClickListener; -import android.widget.ListView; - -/** - * HomeListView is a custom extension of ListView, that packs a HomeContextMenuInfo - * when any of its rows is long pressed. - */ -public class HomeListView extends ListView - implements OnItemLongClickListener { - - // ContextMenuInfo associated with the currently long pressed list item. - private HomeContextMenuInfo mContextMenuInfo; - - // On URL open listener - protected OnUrlOpenListener mUrlOpenListener; - - // Top divider - private final boolean mShowTopDivider; - - // ContextMenuInfo maker - private HomeContextMenuInfo.Factory mContextMenuInfoFactory; - - public HomeListView(Context context) { - this(context, null); - } - - public HomeListView(Context context, AttributeSet attrs) { - this(context, attrs, R.attr.homeListViewStyle); - } - - public HomeListView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - - TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.HomeListView, defStyle, 0); - mShowTopDivider = a.getBoolean(R.styleable.HomeListView_topDivider, false); - a.recycle(); - - setOnItemLongClickListener(this); - } - - @Override - public void onAttachedToWindow() { - super.onAttachedToWindow(); - - final Drawable divider = getDivider(); - if (mShowTopDivider && divider != null) { - final int dividerHeight = getDividerHeight(); - final View view = new View(getContext()); - view.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, dividerHeight)); - addHeaderView(view); - } - } - - @Override - public void onDetachedFromWindow() { - super.onDetachedFromWindow(); - - mUrlOpenListener = null; - } - - @Override - public boolean onItemLongClick(AdapterView parent, View view, int position, long id) { - Object item = parent.getItemAtPosition(position); - - // HomeListView could hold headers too. Add a context menu info only for its children. - if (item instanceof Cursor) { - Cursor cursor = (Cursor) item; - if (cursor == null || mContextMenuInfoFactory == null) { - mContextMenuInfo = null; - return false; - } - - mContextMenuInfo = mContextMenuInfoFactory.makeInfoForCursor(view, position, id, cursor); - return showContextMenuForChild(HomeListView.this); - - } else if (mContextMenuInfoFactory instanceof HomeContextMenuInfo.ListFactory) { - mContextMenuInfo = ((HomeContextMenuInfo.ListFactory) mContextMenuInfoFactory).makeInfoForAdapter(view, position, id, getAdapter()); - return showContextMenuForChild(HomeListView.this); - } else { - mContextMenuInfo = null; - return false; - } - } - - @Override - public ContextMenuInfo getContextMenuInfo() { - return mContextMenuInfo; - } - - @Override - public void setOnItemClickListener(final AdapterView.OnItemClickListener listener) { - if (listener == null) { - super.setOnItemClickListener(null); - return; - } - - super.setOnItemClickListener(new AdapterView.OnItemClickListener() { - @Override - public void onItemClick(AdapterView parent, View view, int position, long id) { - if (mShowTopDivider) { - position--; - } - - listener.onItemClick(parent, view, position, id); - } - }); - } - - public void setContextMenuInfoFactory(final HomeContextMenuInfo.Factory factory) { - mContextMenuInfoFactory = factory; - } - - public OnUrlOpenListener getOnUrlOpenListener() { - return mUrlOpenListener; - } - - public void setOnUrlOpenListener(OnUrlOpenListener listener) { - mUrlOpenListener = listener; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/HomePager.java b/mobile/android/base/java/org/mozilla/gecko/home/HomePager.java deleted file mode 100644 index 4915f0c919..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/HomePager.java +++ /dev/null @@ -1,564 +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 java.util.ArrayList; -import java.util.EnumSet; -import java.util.List; - -import org.mozilla.gecko.AppConstants.Versions; -import org.mozilla.gecko.R; -import org.mozilla.gecko.Telemetry; -import org.mozilla.gecko.TelemetryContract; -import org.mozilla.gecko.animation.PropertyAnimator; -import org.mozilla.gecko.animation.ViewHelper; -import org.mozilla.gecko.home.HomeAdapter.OnAddPanelListener; -import org.mozilla.gecko.home.HomeConfig.PanelConfig; -import org.mozilla.gecko.util.ThreadUtils; - -import android.content.Context; -import android.graphics.Rect; -import android.graphics.drawable.Drawable; -import android.os.Bundle; -import android.support.v4.app.FragmentManager; -import android.support.v4.app.LoaderManager; -import android.support.v4.app.LoaderManager.LoaderCallbacks; -import android.support.v4.content.Loader; -import android.support.v4.view.ViewPager; -import android.util.AttributeSet; -import android.view.MotionEvent; -import android.view.View; -import android.view.ViewGroup; - -public class HomePager extends ViewPager implements HomeScreen { - - @Override - public boolean requestFocus(int direction, Rect previouslyFocusedRect) { - return super.requestFocus(direction, previouslyFocusedRect); - } - - private static final int LOADER_ID_CONFIG = 0; - - private final Context mContext; - private volatile boolean mVisible; - private Decor mDecor; - private View mTabStrip; - private HomeBanner mHomeBanner; - private int mDefaultPageIndex = -1; - - private final OnAddPanelListener mAddPanelListener; - - private final HomeConfig mConfig; - private final ConfigLoaderCallbacks mConfigLoaderCallbacks; - - private String mInitialPanelId; - private Bundle mRestoreData; - - // Cached original ViewPager background. - private final Drawable mOriginalBackground; - - // Telemetry session for current panel. - private TelemetryContract.Session mCurrentPanelSession; - private String mCurrentPanelSessionSuffix; - - // Current load state of HomePager. - private LoadState mLoadState; - - // Listens for when the current panel changes. - private OnPanelChangeListener mPanelChangedListener; - - private HomeFragment.PanelStateChangeListener mPanelStateChangeListener; - - // This is mostly used by UI tests to easily fetch - // specific list views at runtime. - public static final String LIST_TAG_HISTORY = "history"; - public static final String LIST_TAG_BOOKMARKS = "bookmarks"; - public static final String LIST_TAG_TOP_SITES = "top_sites"; - public static final String LIST_TAG_RECENT_TABS = "recent_tabs"; - public static final String LIST_TAG_BROWSER_SEARCH = "browser_search"; - public static final String LIST_TAG_REMOTE_TABS = "remote_tabs"; - - public interface OnUrlOpenListener { - public enum Flags { - ALLOW_SWITCH_TO_TAB, - OPEN_WITH_INTENT, - /** - * Ensure that the raw URL is opened. If not set, then the reader view version of the page - * might be opened if the URL is stored as an offline reader-view bookmark. - */ - NO_READER_VIEW - } - - public void onUrlOpen(String url, EnumSet flags); - } - - /** - * Interface for requesting a new tab be opened in the background. - *

- * This is the HomeFragment equivalent of opening a new tab by - * long clicking a link and selecting the "Open new [private] tab" context - * menu option. - */ - public interface OnUrlOpenInBackgroundListener { - public enum Flags { - PRIVATE, - } - - /** - * Open a new tab with the given URL - * - * @param url to open. - * @param flags to open new tab with. - */ - public void onUrlOpenInBackground(String url, EnumSet flags); - } - - /** - * Special type of child views that could be added as pager decorations by default. - */ - public interface Decor { - void onAddPagerView(String title); - void removeAllPagerViews(); - void onPageSelected(int position); - void onPageScrolled(int position, float positionOffset, int positionOffsetPixels); - void setOnTitleClickListener(TabMenuStrip.OnTitleClickListener onTitleClickListener); - } - - /** - * State of HomePager with respect to loading its configuration. - */ - private enum LoadState { - UNLOADED, - LOADING, - LOADED - } - - public static final String CAN_LOAD_ARG = "canLoad"; - public static final String PANEL_CONFIG_ARG = "panelConfig"; - - public HomePager(Context context) { - this(context, null); - } - - public HomePager(Context context, AttributeSet attrs) { - super(context, attrs); - mContext = context; - - mConfig = HomeConfig.getDefault(mContext); - mConfigLoaderCallbacks = new ConfigLoaderCallbacks(); - - mAddPanelListener = new OnAddPanelListener() { - @Override - public void onAddPanel(String title) { - if (mDecor != null) { - mDecor.onAddPagerView(title); - } - } - }; - - // This is to keep all 4 panels in memory after they are - // selected in the pager. - setOffscreenPageLimit(3); - - // We can call HomePager.requestFocus to steal focus from the URL bar and drop the soft - // keyboard. However, if there are no focusable views (e.g. an empty reading list), the - // URL bar will be refocused. Therefore, we make the HomePager container focusable to - // ensure there is always a focusable view. This would ordinarily be done via an XML - // attribute, but it is not working properly. - setFocusableInTouchMode(true); - - mOriginalBackground = getBackground(); - setOnPageChangeListener(new PageChangeListener()); - - mLoadState = LoadState.UNLOADED; - } - - @Override - public void addView(View child, int index, ViewGroup.LayoutParams params) { - if (child instanceof Decor) { - ((ViewPager.LayoutParams) params).isDecor = true; - mDecor = (Decor) child; - mTabStrip = child; - - mDecor.setOnTitleClickListener(new TabMenuStrip.OnTitleClickListener() { - @Override - public void onTitleClicked(int index) { - setCurrentItem(index, true); - } - }); - } - - super.addView(child, index, params); - } - - /** - * Loads and initializes the pager. - * - * @param fm FragmentManager for the adapter - */ - @Override - public void load(LoaderManager lm, FragmentManager fm, String panelId, Bundle restoreData, PropertyAnimator animator) { - mLoadState = LoadState.LOADING; - - mVisible = true; - mInitialPanelId = panelId; - mRestoreData = restoreData; - - // Update the home banner message each time the HomePager is loaded. - if (mHomeBanner != null) { - mHomeBanner.update(); - } - - // Only animate on post-HC devices, when a non-null animator is given - final boolean shouldAnimate = animator != null; - - final HomeAdapter adapter = new HomeAdapter(mContext, fm); - adapter.setOnAddPanelListener(mAddPanelListener); - adapter.setPanelStateChangeListener(mPanelStateChangeListener); - adapter.setCanLoadHint(true); - setAdapter(adapter); - - // Don't show the tabs strip until we have the - // list of panels in place. - mTabStrip.setVisibility(View.INVISIBLE); - - // Load list of panels from configuration - lm.initLoader(LOADER_ID_CONFIG, null, mConfigLoaderCallbacks); - - if (shouldAnimate) { - animator.addPropertyAnimationListener(new PropertyAnimator.PropertyAnimationListener() { - @Override - public void onPropertyAnimationStart() { - setLayerType(View.LAYER_TYPE_HARDWARE, null); - } - - @Override - public void onPropertyAnimationEnd() { - setLayerType(View.LAYER_TYPE_NONE, null); - } - }); - - ViewHelper.setAlpha(this, 0.0f); - - animator.attach(this, - PropertyAnimator.Property.ALPHA, - 1.0f); - } - } - - /** - * Removes all child fragments to free memory. - */ - @Override - public void unload() { - mVisible = false; - setAdapter(null); - mLoadState = LoadState.UNLOADED; - - // Stop UI Telemetry sessions. - stopCurrentPanelTelemetrySession(); - } - - /** - * Determines whether the pager is visible. - * - * Unlike getVisibility(), this method does not need to be called on the UI - * thread. - * - * @return Whether the pager and its fragments are loaded - */ - public boolean isVisible() { - return mVisible; - } - - @Override - public void setCurrentItem(int item, boolean smoothScroll) { - super.setCurrentItem(item, smoothScroll); - - if (mDecor != null) { - mDecor.onPageSelected(item); - } - - if (mHomeBanner != null) { - mHomeBanner.setActive(item == mDefaultPageIndex); - } - } - - private void restorePanelData(int item, Bundle data) { - ((HomeAdapter) getAdapter()).setRestoreData(item, data); - } - - /** - * Shows a home panel. If the given panelId is null, - * the default panel will be shown. No action will be taken if: - * * HomePager has not loaded yet - * * Panel with the given panelId cannot be found - * - * If you're trying to open a built-in panel, consider loading the panel url directly with - * {@link org.mozilla.gecko.AboutPages#getURLForBuiltinPanelType(HomeConfig.PanelType)}. - * - * @param panelId of the home panel to be shown. - */ - @Override - public void showPanel(String panelId, Bundle restoreData) { - if (!mVisible) { - return; - } - - switch (mLoadState) { - case LOADING: - mInitialPanelId = panelId; - mRestoreData = restoreData; - break; - - case LOADED: - int position = mDefaultPageIndex; - if (panelId != null) { - position = ((HomeAdapter) getAdapter()).getItemPosition(panelId); - } - - if (position > -1) { - setCurrentItem(position); - if (restoreData != null) { - restorePanelData(position, restoreData); - } - } - break; - - default: - // Do nothing. - } - } - - @Override - public boolean onInterceptTouchEvent(MotionEvent event) { - if (event.getActionMasked() == MotionEvent.ACTION_DOWN) { - // Drop the soft keyboard by stealing focus from the URL bar. - requestFocus(); - } - - return super.onInterceptTouchEvent(event); - } - - public void setBanner(HomeBanner banner) { - mHomeBanner = banner; - } - - @Override - public boolean dispatchTouchEvent(MotionEvent event) { - if (mHomeBanner != null) { - mHomeBanner.handleHomeTouch(event); - } - - return super.dispatchTouchEvent(event); - } - - @Override - public void onToolbarFocusChange(boolean hasFocus) { - if (mHomeBanner == null) { - return; - } - - // We should only make the banner active if the toolbar is not focused and we are on the default page - final boolean active = !hasFocus && getCurrentItem() == mDefaultPageIndex; - mHomeBanner.setActive(active); - } - - private void updateUiFromConfigState(HomeConfig.State configState) { - // We only care about the adapter if HomePager is currently - // loaded, which means it's visible in the activity. - if (!mVisible) { - return; - } - - if (mDecor != null) { - mDecor.removeAllPagerViews(); - } - - final HomeAdapter adapter = (HomeAdapter) getAdapter(); - - // Disable any fragment loading until we have the initial - // panel selection done. - adapter.setCanLoadHint(false); - - // Destroy any existing panels currently loaded - // in the pager. - setAdapter(null); - - // Only keep enabled panels. - final List enabledPanels = new ArrayList(); - - for (PanelConfig panelConfig : configState) { - if (!panelConfig.isDisabled()) { - enabledPanels.add(panelConfig); - } - } - - // Update the adapter with the new panel configs - adapter.update(enabledPanels); - - final int count = enabledPanels.size(); - if (count == 0) { - // Set firefox watermark as background. - setBackgroundResource(R.drawable.home_pager_empty_state); - // Hide the tab strip as there are no panels. - mTabStrip.setVisibility(View.INVISIBLE); - } else { - mTabStrip.setVisibility(View.VISIBLE); - // Restore original background. - setBackgroundDrawable(mOriginalBackground); - } - - // Re-install the adapter with the final state - // in the pager. - setAdapter(adapter); - - if (count == 0) { - mDefaultPageIndex = -1; - - // Hide the banner if there are no enabled panels. - if (mHomeBanner != null) { - mHomeBanner.setActive(false); - } - } else { - for (int i = 0; i < count; i++) { - if (enabledPanels.get(i).isDefault()) { - mDefaultPageIndex = i; - break; - } - } - - // Use the default panel if the initial panel wasn't explicitly set by the - // load() caller, or if the initial panel is not found in the adapter. - final int itemPosition = (mInitialPanelId == null) ? -1 : adapter.getItemPosition(mInitialPanelId); - if (itemPosition > -1) { - setCurrentItem(itemPosition, false); - if (mRestoreData != null) { - restorePanelData(itemPosition, mRestoreData); - mRestoreData = null; // Release data since it's no longer needed - } - mInitialPanelId = null; - } else { - setCurrentItem(mDefaultPageIndex, false); - } - } - - // The selection is updated asynchronously so we need to post to - // UI thread to give the pager time to commit the new page selection - // internally and load the right initial panel. - ThreadUtils.getUiHandler().post(new Runnable() { - @Override - public void run() { - adapter.setCanLoadHint(true); - } - }); - } - - @Override - public void setOnPanelChangeListener(OnPanelChangeListener listener) { - mPanelChangedListener = listener; - } - - @Override - public void setPanelStateChangeListener(HomeFragment.PanelStateChangeListener listener) { - mPanelStateChangeListener = listener; - - HomeAdapter adapter = (HomeAdapter) getAdapter(); - if (adapter != null) { - adapter.setPanelStateChangeListener(listener); - } - } - - /** - * Notify listeners of newly selected panel. - * - * @param position of the newly selected panel - */ - private void notifyPanelSelected(int position) { - if (mDecor != null) { - mDecor.onPageSelected(position); - } - - if (mPanelChangedListener != null) { - final String panelId = ((HomeAdapter) getAdapter()).getPanelIdAtPosition(position); - mPanelChangedListener.onPanelSelected(panelId); - } - } - - private class ConfigLoaderCallbacks implements LoaderCallbacks { - @Override - public Loader onCreateLoader(int id, Bundle args) { - return new HomeConfigLoader(mContext, mConfig); - } - - @Override - public void onLoadFinished(Loader loader, HomeConfig.State configState) { - mLoadState = LoadState.LOADED; - updateUiFromConfigState(configState); - } - - @Override - public void onLoaderReset(Loader loader) { - mLoadState = LoadState.UNLOADED; - } - } - - private class PageChangeListener implements ViewPager.OnPageChangeListener { - @Override - public void onPageSelected(int position) { - notifyPanelSelected(position); - - if (mHomeBanner != null) { - mHomeBanner.setActive(position == mDefaultPageIndex); - } - - // Start a UI telemetry session for the newly selected panel. - final String newPanelId = ((HomeAdapter) getAdapter()).getPanelIdAtPosition(position); - startNewPanelTelemetrySession(newPanelId); - } - - @Override - public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { - if (mDecor != null) { - mDecor.onPageScrolled(position, positionOffset, positionOffsetPixels); - } - - if (mHomeBanner != null) { - mHomeBanner.setScrollingPages(positionOffsetPixels != 0); - } - } - - @Override - public void onPageScrollStateChanged(int state) { } - } - - /** - * Start UI telemetry session for the a panel. - * If there is currently a session open for a panel, - * it will be stopped before a new one is started. - * - * @param panelId of panel to start a session for - */ - private void startNewPanelTelemetrySession(String panelId) { - // Stop the current panel's session if we have one. - stopCurrentPanelTelemetrySession(); - - mCurrentPanelSession = TelemetryContract.Session.HOME_PANEL; - mCurrentPanelSessionSuffix = panelId; - Telemetry.startUISession(mCurrentPanelSession, mCurrentPanelSessionSuffix); - } - - /** - * Stop the current panel telemetry session if one exists. - */ - private void stopCurrentPanelTelemetrySession() { - if (mCurrentPanelSession != null) { - Telemetry.stopUISession(mCurrentPanelSession, mCurrentPanelSessionSuffix); - mCurrentPanelSession = null; - mCurrentPanelSessionSuffix = null; - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/HomePanelsManager.java b/mobile/android/base/java/org/mozilla/gecko/home/HomePanelsManager.java deleted file mode 100644 index bfd6c5624a..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/HomePanelsManager.java +++ /dev/null @@ -1,368 +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 static org.mozilla.gecko.home.HomeConfig.createBuiltinPanelConfig; - -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Queue; -import java.util.Set; -import java.util.concurrent.ConcurrentLinkedQueue; - -import org.json.JSONException; -import org.json.JSONObject; -import org.mozilla.gecko.db.HomeProvider; -import org.mozilla.gecko.EventDispatcher; -import org.mozilla.gecko.home.HomeConfig.PanelConfig; -import org.mozilla.gecko.home.PanelInfoManager.PanelInfo; -import org.mozilla.gecko.home.PanelInfoManager.RequestCallback; -import org.mozilla.gecko.util.GeckoEventListener; -import org.mozilla.gecko.util.ThreadUtils; - -import android.content.ContentResolver; -import android.content.Context; -import android.os.Handler; -import android.util.Log; - -public class HomePanelsManager implements GeckoEventListener { - public static final String LOGTAG = "HomePanelsManager"; - - private static final HomePanelsManager sInstance = new HomePanelsManager(); - - private static final int INVALIDATION_DELAY_MSEC = 500; - private static final int PANEL_INFO_TIMEOUT_MSEC = 1000; - - private static final String EVENT_HOMEPANELS_INSTALL = "HomePanels:Install"; - private static final String EVENT_HOMEPANELS_UNINSTALL = "HomePanels:Uninstall"; - private static final String EVENT_HOMEPANELS_UPDATE = "HomePanels:Update"; - private static final String EVENT_HOMEPANELS_REFRESH = "HomePanels:RefreshDataset"; - - private static final String JSON_KEY_PANEL = "panel"; - private static final String JSON_KEY_PANEL_ID = "id"; - - private enum ChangeType { - UNINSTALL, - INSTALL, - UPDATE, - REFRESH - } - - private enum InvalidationMode { - DELAYED, - IMMEDIATE - } - - private static class ConfigChange { - private final ChangeType type; - private final Object target; - - public ConfigChange(ChangeType type) { - this(type, null); - } - - public ConfigChange(ChangeType type, Object target) { - this.type = type; - this.target = target; - } - } - - private Context mContext; - private HomeConfig mHomeConfig; - private boolean mInitialized; - - private final Queue mPendingChanges = new ConcurrentLinkedQueue(); - private final Runnable mInvalidationRunnable = new InvalidationRunnable(); - - public static HomePanelsManager getInstance() { - return sInstance; - } - - public void init(Context context) { - if (mInitialized) { - return; - } - - mContext = context; - mHomeConfig = HomeConfig.getDefault(context); - - EventDispatcher.getInstance().registerGeckoThreadListener(this, - EVENT_HOMEPANELS_INSTALL, - EVENT_HOMEPANELS_UNINSTALL, - EVENT_HOMEPANELS_UPDATE, - EVENT_HOMEPANELS_REFRESH); - - mInitialized = true; - } - - public void onLocaleReady(final String locale) { - ThreadUtils.getBackgroundHandler().post(new Runnable() { - @Override - public void run() { - final String configLocale = mHomeConfig.getLocale(); - if (configLocale == null || !configLocale.equals(locale)) { - handleLocaleChange(); - } - } - }); - } - - @Override - public void handleMessage(String event, JSONObject message) { - try { - if (event.equals(EVENT_HOMEPANELS_INSTALL)) { - Log.d(LOGTAG, EVENT_HOMEPANELS_INSTALL); - handlePanelInstall(createPanelConfigFromMessage(message), InvalidationMode.DELAYED); - } else if (event.equals(EVENT_HOMEPANELS_UNINSTALL)) { - Log.d(LOGTAG, EVENT_HOMEPANELS_UNINSTALL); - final String panelId = message.getString(JSON_KEY_PANEL_ID); - handlePanelUninstall(panelId); - } else if (event.equals(EVENT_HOMEPANELS_UPDATE)) { - Log.d(LOGTAG, EVENT_HOMEPANELS_UPDATE); - handlePanelUpdate(createPanelConfigFromMessage(message)); - } else if (event.equals(EVENT_HOMEPANELS_REFRESH)) { - Log.d(LOGTAG, EVENT_HOMEPANELS_REFRESH); - handleDatasetRefresh(message); - } - } catch (Exception e) { - Log.e(LOGTAG, "Failed to handle event " + event, e); - } - } - - private PanelConfig createPanelConfigFromMessage(JSONObject message) throws JSONException { - final JSONObject json = message.getJSONObject(JSON_KEY_PANEL); - return new PanelConfig(json); - } - - /** - * Adds a new PanelConfig to the HomeConfig. - * - * This posts the invalidation of HomeConfig immediately. - * - * @param panelConfig panel to add - */ - public void installPanel(PanelConfig panelConfig) { - Log.d(LOGTAG, "installPanel: " + panelConfig.getTitle()); - handlePanelInstall(panelConfig, InvalidationMode.IMMEDIATE); - } - - /** - * Runs in the gecko thread. - */ - private void handlePanelInstall(PanelConfig panelConfig, InvalidationMode mode) { - mPendingChanges.offer(new ConfigChange(ChangeType.INSTALL, panelConfig)); - Log.d(LOGTAG, "handlePanelInstall: " + mPendingChanges.size()); - - scheduleInvalidation(mode); - } - - /** - * Runs in the gecko thread. - */ - private void handlePanelUninstall(String panelId) { - mPendingChanges.offer(new ConfigChange(ChangeType.UNINSTALL, panelId)); - Log.d(LOGTAG, "handlePanelUninstall: " + mPendingChanges.size()); - - scheduleInvalidation(InvalidationMode.DELAYED); - } - - /** - * Runs in the gecko thread. - */ - private void handlePanelUpdate(PanelConfig panelConfig) { - mPendingChanges.offer(new ConfigChange(ChangeType.UPDATE, panelConfig)); - Log.d(LOGTAG, "handlePanelUpdate: " + mPendingChanges.size()); - - scheduleInvalidation(InvalidationMode.DELAYED); - } - - /** - * Runs in the background thread. - */ - private void handleLocaleChange() { - mPendingChanges.offer(new ConfigChange(ChangeType.REFRESH)); - Log.d(LOGTAG, "handleLocaleChange: " + mPendingChanges.size()); - - scheduleInvalidation(InvalidationMode.IMMEDIATE); - } - - - /** - * Handles a dataset refresh request from Gecko. This is usually - * triggered by a HomeStorage.save() call in an add-on. - * - * Runs in the gecko thread. - */ - private void handleDatasetRefresh(JSONObject message) { - final String datasetId; - try { - datasetId = message.getString("datasetId"); - } catch (JSONException e) { - Log.e(LOGTAG, "Failed to handle dataset refresh", e); - return; - } - - Log.d(LOGTAG, "Refresh request for dataset: " + datasetId); - - final ContentResolver cr = mContext.getContentResolver(); - cr.notifyChange(HomeProvider.getDatasetNotificationUri(datasetId), null); - } - - /** - * Runs in the gecko or main thread. - */ - private void scheduleInvalidation(InvalidationMode mode) { - final Handler handler = ThreadUtils.getBackgroundHandler(); - - handler.removeCallbacks(mInvalidationRunnable); - - if (mode == InvalidationMode.IMMEDIATE) { - handler.post(mInvalidationRunnable); - } else { - handler.postDelayed(mInvalidationRunnable, INVALIDATION_DELAY_MSEC); - } - - Log.d(LOGTAG, "scheduleInvalidation: scheduled new invalidation: " + mode); - } - - /** - * Runs in the background thread. - */ - private void executePendingChanges(HomeConfig.Editor editor) { - boolean shouldRefresh = false; - - while (!mPendingChanges.isEmpty()) { - final ConfigChange pendingChange = mPendingChanges.poll(); - - switch (pendingChange.type) { - case UNINSTALL: { - final String panelId = (String) pendingChange.target; - if (editor.uninstall(panelId)) { - Log.d(LOGTAG, "executePendingChanges: uninstalled panel " + panelId); - } - break; - } - - case INSTALL: { - final PanelConfig panelConfig = (PanelConfig) pendingChange.target; - if (editor.install(panelConfig)) { - Log.d(LOGTAG, "executePendingChanges: added panel " + panelConfig.getId()); - } - break; - } - - case UPDATE: { - final PanelConfig panelConfig = (PanelConfig) pendingChange.target; - if (editor.update(panelConfig)) { - Log.w(LOGTAG, "executePendingChanges: updated panel " + panelConfig.getId()); - } - break; - } - - case REFRESH: { - shouldRefresh = true; - } - } - } - - // The editor still represents the default HomeConfig - // configuration and hasn't been changed by any operation - // above. No need to refresh as the HomeConfig backend will - // take of forcing all existing HomeConfigLoader instances to - // refresh their contents. - if (shouldRefresh && !editor.isDefault()) { - executeRefresh(editor); - } - } - - /** - * Runs in the background thread. - */ - private void refreshFromPanelInfos(HomeConfig.Editor editor, List panelInfos) { - Log.d(LOGTAG, "refreshFromPanelInfos"); - - for (PanelConfig panelConfig : editor) { - PanelConfig refreshedPanelConfig = null; - - if (panelConfig.isDynamic()) { - for (PanelInfo panelInfo : panelInfos) { - if (panelInfo.getId().equals(panelConfig.getId())) { - refreshedPanelConfig = panelInfo.toPanelConfig(); - Log.d(LOGTAG, "refreshFromPanelInfos: refreshing from panel info: " + panelInfo.getId()); - break; - } - } - } else { - refreshedPanelConfig = createBuiltinPanelConfig(mContext, panelConfig.getType()); - Log.d(LOGTAG, "refreshFromPanelInfos: refreshing built-in panel: " + panelConfig.getId()); - } - - if (refreshedPanelConfig == null) { - Log.d(LOGTAG, "refreshFromPanelInfos: no refreshed panel, falling back: " + panelConfig.getId()); - continue; - } - - Log.d(LOGTAG, "refreshFromPanelInfos: refreshed panel " + refreshedPanelConfig.getId()); - editor.update(refreshedPanelConfig); - } - } - - /** - * Runs in the background thread. - */ - private void executeRefresh(HomeConfig.Editor editor) { - if (editor.isEmpty()) { - return; - } - - Log.d(LOGTAG, "executeRefresh"); - - final Set ids = new HashSet(); - for (PanelConfig panelConfig : editor) { - ids.add(panelConfig.getId()); - } - - final Object panelRequestLock = new Object(); - final List latestPanelInfos = new ArrayList(); - - final PanelInfoManager pm = new PanelInfoManager(); - pm.requestPanelsById(ids, new RequestCallback() { - @Override - public void onComplete(List panelInfos) { - synchronized (panelRequestLock) { - latestPanelInfos.addAll(panelInfos); - Log.d(LOGTAG, "executeRefresh: fetched panel infos: " + panelInfos.size()); - - panelRequestLock.notifyAll(); - } - } - }); - - try { - synchronized (panelRequestLock) { - panelRequestLock.wait(PANEL_INFO_TIMEOUT_MSEC); - - Log.d(LOGTAG, "executeRefresh: done fetching panel infos"); - refreshFromPanelInfos(editor, latestPanelInfos); - } - } catch (InterruptedException e) { - Log.e(LOGTAG, "Failed to fetch panels from gecko", e); - } - } - - /** - * Runs in the background thread. - */ - private class InvalidationRunnable implements Runnable { - @Override - public void run() { - final HomeConfig.Editor editor = mHomeConfig.load().edit(); - executePendingChanges(editor); - editor.apply(); - } - }; -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/HomeScreen.java b/mobile/android/base/java/org/mozilla/gecko/home/HomeScreen.java deleted file mode 100644 index 1525969a00..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/HomeScreen.java +++ /dev/null @@ -1,57 +0,0 @@ -package org.mozilla.gecko.home; - -import android.os.Bundle; -import android.support.v4.app.FragmentManager; -import android.support.v4.app.LoaderManager; -import android.view.View; - -import org.mozilla.gecko.animation.PropertyAnimator; - -/** - * Generic interface for any View that can be used as the homescreen. - * - * In the past we had the HomePager, which contained the usual homepanels (multiple panels: TopSites, - * bookmarks, history, etc.), which could be swiped between. - * - * This interface allows easily switching between different homepanel implementations. For example - * the prototype activity-stream panel (which will be a single panel combining the functionality - * of the previous panels). - */ -public interface HomeScreen { - /** - * Interface for listening into ViewPager panel changes - */ - public interface OnPanelChangeListener { - /** - * Called when a new panel is selected. - * - * @param panelId of the newly selected panel - */ - public void onPanelSelected(String panelId); - } - - // The following two methods are actually methods of View. Since there is no View interface - // we're forced to do this instead of "extending" View. Any class implementing HomeScreen - // will have to implement these and pass them through to the underlying View. - boolean isVisible(); - boolean requestFocus(); - - void onToolbarFocusChange(boolean hasFocus); - - // The following three methods are HomePager specific. The persistence framework might need - // refactoring/generalising at some point, but it isn't entirely clear what other panels - // might need so we can leave these as is for now. - void showPanel(String panelId, Bundle restoreData); - void setOnPanelChangeListener(OnPanelChangeListener listener); - void setPanelStateChangeListener(HomeFragment.PanelStateChangeListener listener); - - /** - * Set a banner that may be displayed at the bottom of the HomeScreen. This can be used - * e.g. to show snippets. - */ - void setBanner(HomeBanner banner); - - void load(LoaderManager lm, FragmentManager fm, String panelId, Bundle restoreData, PropertyAnimator animator); - - void unload(); -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/ImageLoader.java b/mobile/android/base/java/org/mozilla/gecko/home/ImageLoader.java deleted file mode 100644 index 2bbd82a8df..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/ImageLoader.java +++ /dev/null @@ -1,164 +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.net.Uri; -import android.util.DisplayMetrics; -import android.util.Log; - -import com.squareup.picasso.LruCache; -import com.squareup.picasso.Picasso; -import com.squareup.picasso.Downloader.Response; -import com.squareup.picasso.UrlConnectionDownloader; - -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.util.EnumSet; -import java.util.Set; - -import org.mozilla.gecko.distribution.Distribution; - -public class ImageLoader { - private static final String LOGTAG = "GeckoImageLoader"; - - private static final String DISTRIBUTION_SCHEME = "gecko.distribution"; - private static final String SUGGESTED_SITES_AUTHORITY = "suggestedsites"; - - // The order of density factors to try when looking for an image resource - // in the distribution directory. It looks for an exact match first (1.0) then - // tries to find images with higher density (2.0 and 1.5). If no image is found, - // try a lower density (0.5). See loadDistributionImage(). - private static final float[] densityFactors = new float[] { 1.0f, 2.0f, 1.5f, 0.5f }; - - private static enum Density { - MDPI, - HDPI, - XHDPI, - XXHDPI; - - @Override - public String toString() { - return super.toString().toLowerCase(); - } - } - - // Picasso instance and LruCache lrucache are protected by synchronization. - private static Picasso instance; - private static LruCache lrucache; - - public static synchronized Picasso with(Context context) { - if (instance == null) { - lrucache = new LruCache(context); - Picasso.Builder builder = new Picasso.Builder(context).memoryCache(lrucache); - - final Distribution distribution = Distribution.getInstance(context.getApplicationContext()); - builder.downloader(new ImageDownloader(context, distribution)); - instance = builder.build(); - } - - return instance; - } - - public static synchronized void clearLruCache() { - if (lrucache != null) { - lrucache.evictAll(); - } - } - - /** - * Custom Downloader built on top of Picasso's UrlConnectionDownloader - * that supports loading images from custom URIs. - */ - public static class ImageDownloader extends UrlConnectionDownloader { - private final Context context; - private final Distribution distribution; - - public ImageDownloader(Context context, Distribution distribution) { - super(context); - this.context = context; - this.distribution = distribution; - } - - private Density getDensity(float factor) { - final DisplayMetrics dm = context.getResources().getDisplayMetrics(); - final float densityDpi = dm.densityDpi * factor; - - if (densityDpi >= DisplayMetrics.DENSITY_XXHIGH) { - return Density.XXHDPI; - } else if (densityDpi >= DisplayMetrics.DENSITY_XHIGH) { - return Density.XHDPI; - } else if (densityDpi >= DisplayMetrics.DENSITY_HIGH) { - return Density.HDPI; - } - - // Fallback to mdpi, no need to handle ldpi. - return Density.MDPI; - } - - @Override - public Response load(Uri uri, boolean localCacheOnly) throws IOException { - final String scheme = uri.getScheme(); - if (DISTRIBUTION_SCHEME.equals(scheme)) { - return loadDistributionImage(uri); - } - - return super.load(uri, localCacheOnly); - } - - private static String getPathForDensity(String basePath, Density density, - String filename) { - final File dir = new File(basePath, density.toString()); - return String.format("%s/%s.png", dir.toString(), filename); - } - - /** - * Handle distribution URIs in Picasso. The expected format is: - * - * gecko.distribution:/// - * - * Which will look for the following file in the distribution: - * - * ///.png - */ - private Response loadDistributionImage(Uri uri) throws IOException { - // Eliminate the leading '//' - final String ssp = uri.getSchemeSpecificPart().substring(2); - - final String filename; - final String basePath; - - final int slashIndex = ssp.lastIndexOf('/'); - if (slashIndex == -1) { - filename = ssp; - basePath = ""; - } else { - filename = ssp.substring(slashIndex + 1); - basePath = ssp.substring(0, slashIndex); - } - - Set triedDensities = EnumSet.noneOf(Density.class); - - for (int i = 0; i < densityFactors.length; i++) { - final Density density = getDensity(densityFactors[i]); - if (!triedDensities.add(density)) { - continue; - } - - final String path = getPathForDensity(basePath, density, filename); - Log.d(LOGTAG, "Trying to load image from distribution " + path); - - final File f = distribution.getDistributionFile(path); - if (f != null) { - return new Response(new FileInputStream(f), true); - } - } - - throw new ResponseException("Couldn't find suggested site image in distribution"); - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/MultiTypeCursorAdapter.java b/mobile/android/base/java/org/mozilla/gecko/home/MultiTypeCursorAdapter.java deleted file mode 100644 index 26edf13ff9..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/MultiTypeCursorAdapter.java +++ /dev/null @@ -1,100 +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.support.v4.widget.CursorAdapter; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; - -/** - * MultiTypeCursorAdapter wraps a cursor and any meta data associated with it. - * A set of view types (corresponding to the cursor and its meta data) - * are mapped to a set of layouts. - */ -abstract class MultiTypeCursorAdapter extends CursorAdapter { - private final int[] mViewTypes; - private final int[] mLayouts; - - // Bind the view for the given position. - abstract public void bindView(View view, Context context, int position); - - public MultiTypeCursorAdapter(Context context, Cursor cursor, int[] viewTypes, int[] layouts) { - super(context, cursor, 0); - - if (viewTypes.length != layouts.length) { - throw new IllegalStateException("The view types and the layouts should be of same size"); - } - - mViewTypes = viewTypes; - mLayouts = layouts; - } - - @Override - public final int getViewTypeCount() { - return mViewTypes.length; - } - - /** - * @return Cursor for the given position. - */ - public final Cursor getCursor(int position) { - final Cursor cursor = getCursor(); - if (cursor == null || !cursor.moveToPosition(position)) { - throw new IllegalStateException("Couldn't move cursor to position " + position); - } - - return cursor; - } - - @Override - public final View getView(int position, View convertView, ViewGroup parent) { - final Context context = parent.getContext(); - if (convertView == null) { - convertView = newView(context, position, parent); - } - - bindView(convertView, context, position); - return convertView; - } - - @Override - public final void bindView(View view, Context context, Cursor cursor) { - // Do nothing. - } - - @Override - public boolean hasStableIds() { - return false; - } - - @Override - public final View newView(Context context, Cursor cursor, ViewGroup parent) { - return null; - } - - /** - * Inflate a new view from a set of view types and layouts based on the position. - * - * @param context Context for inflating the view. - * @param position Position of the view. - * @param parent Parent view group that will hold this view. - */ - private View newView(Context context, int position, ViewGroup parent) { - final int type = getItemViewType(position); - final int count = mViewTypes.length; - - for (int i = 0; i < count; i++) { - if (mViewTypes[i] == type) { - return LayoutInflater.from(context).inflate(mLayouts[i], parent, false); - } - } - - return null; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/PanelAuthCache.java b/mobile/android/base/java/org/mozilla/gecko/home/PanelAuthCache.java deleted file mode 100644 index d66919344f..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/PanelAuthCache.java +++ /dev/null @@ -1,82 +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.content.SharedPreferences; -import android.content.SharedPreferences.OnSharedPreferenceChangeListener; -import android.util.Log; - -import org.mozilla.gecko.GeckoSharedPrefs; - -/** - * Cache used to store authentication state of dynamic panels. The values - * in this cache are set in JS through the Home.panels API. - * - * {@code DynamicPanel} uses this cache to determine whether or not to - * show authentication UI for dynamic panels, including listening for - * changes in authentication state. - */ -class PanelAuthCache { - private static final String LOGTAG = "GeckoPanelAuthCache"; - - // Keep this in sync with the constant defined in Home.jsm - private static final String PREFS_PANEL_AUTH_PREFIX = "home_panels_auth_"; - - private final Context mContext; - private SharedPrefsListener mSharedPrefsListener; - private OnChangeListener mChangeListener; - - public interface OnChangeListener { - public void onChange(String panelId, boolean isAuthenticated); - } - - public PanelAuthCache(Context context) { - mContext = context; - } - - private SharedPreferences getSharedPreferences() { - return GeckoSharedPrefs.forProfile(mContext); - } - - private String getPanelAuthKey(String panelId) { - return PREFS_PANEL_AUTH_PREFIX + panelId; - } - - public boolean isAuthenticated(String panelId) { - final SharedPreferences prefs = getSharedPreferences(); - return prefs.getBoolean(getPanelAuthKey(panelId), false); - } - - public void setOnChangeListener(OnChangeListener listener) { - final SharedPreferences prefs = getSharedPreferences(); - - if (mChangeListener != null) { - prefs.unregisterOnSharedPreferenceChangeListener(mSharedPrefsListener); - mSharedPrefsListener = null; - } - - mChangeListener = listener; - - if (mChangeListener != null) { - mSharedPrefsListener = new SharedPrefsListener(); - prefs.registerOnSharedPreferenceChangeListener(mSharedPrefsListener); - } - } - - private class SharedPrefsListener implements OnSharedPreferenceChangeListener { - @Override - public void onSharedPreferenceChanged(SharedPreferences prefs, String key) { - if (key.startsWith(PREFS_PANEL_AUTH_PREFIX)) { - final String panelId = key.substring(PREFS_PANEL_AUTH_PREFIX.length()); - final boolean isAuthenticated = prefs.getBoolean(key, false); - - Log.d(LOGTAG, "Auth state changed: panelId=" + panelId + ", isAuthenticated=" + isAuthenticated); - mChangeListener.onChange(panelId, isAuthenticated); - } - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/PanelAuthLayout.java b/mobile/android/base/java/org/mozilla/gecko/home/PanelAuthLayout.java deleted file mode 100644 index 1ad91b7caf..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/PanelAuthLayout.java +++ /dev/null @@ -1,63 +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.GeckoAppShell; -import org.mozilla.gecko.R; -import org.mozilla.gecko.home.HomeConfig.AuthConfig; -import org.mozilla.gecko.home.HomeConfig.PanelConfig; - -import android.content.Context; -import android.text.TextUtils; -import android.view.LayoutInflater; -import android.view.View; -import android.widget.Button; -import android.widget.ImageView; -import android.widget.LinearLayout; -import android.widget.TextView; - -import com.squareup.picasso.Picasso; - -class PanelAuthLayout extends LinearLayout { - - public PanelAuthLayout(Context context, PanelConfig panelConfig) { - super(context); - - final AuthConfig authConfig = panelConfig.getAuthConfig(); - if (authConfig == null) { - throw new IllegalStateException("Can't create PanelAuthLayout without a valid AuthConfig"); - } - - setOrientation(LinearLayout.VERTICAL); - LayoutInflater.from(context).inflate(R.layout.panel_auth_layout, this); - - final TextView messageView = (TextView) findViewById(R.id.message); - messageView.setText(authConfig.getMessageText()); - - final Button buttonView = (Button) findViewById(R.id.button); - buttonView.setText(authConfig.getButtonText()); - - final String panelId = panelConfig.getId(); - buttonView.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - GeckoAppShell.notifyObservers("HomePanels:Authenticate", panelId); - } - }); - - final ImageView imageView = (ImageView) findViewById(R.id.image); - final String imageUrl = authConfig.getImageUrl(); - - if (TextUtils.isEmpty(imageUrl)) { - // Use a default image if an image URL isn't specified. - imageView.setImageResource(R.drawable.icon_home_empty_firefox); - } else { - ImageLoader.with(getContext()) - .load(imageUrl) - .into(imageView); - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/PanelBackItemView.java b/mobile/android/base/java/org/mozilla/gecko/home/PanelBackItemView.java deleted file mode 100644 index 4772e08abb..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/PanelBackItemView.java +++ /dev/null @@ -1,48 +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.R; -import org.mozilla.gecko.home.PanelLayout.FilterDetail; - -import android.content.Context; -import android.text.TextUtils; -import android.view.LayoutInflater; -import android.widget.ImageView; -import android.widget.LinearLayout; -import android.widget.TextView; - -import com.squareup.picasso.Picasso; - -class PanelBackItemView extends LinearLayout { - private final TextView title; - - public PanelBackItemView(Context context, String backImageUrl) { - super(context); - - LayoutInflater.from(context).inflate(R.layout.panel_back_item, this); - setOrientation(HORIZONTAL); - - title = (TextView) findViewById(R.id.title); - - final ImageView image = (ImageView) findViewById(R.id.image); - - if (TextUtils.isEmpty(backImageUrl)) { - image.setImageResource(R.drawable.arrow_up); - } else { - ImageLoader.with(getContext()) - .load(backImageUrl) - .placeholder(R.drawable.arrow_up) - .into(image); - } - } - - public void updateFromFilter(FilterDetail filter) { - final String backText = getResources() - .getString(R.string.home_move_back_to_filter, filter.title); - title.setText(backText); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/PanelHeaderView.java b/mobile/android/base/java/org/mozilla/gecko/home/PanelHeaderView.java deleted file mode 100644 index 50c4dbc075..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/PanelHeaderView.java +++ /dev/null @@ -1,28 +0,0 @@ -package org.mozilla.gecko.home; - -import android.annotation.SuppressLint; -import android.content.Context; -import android.widget.ImageView; - -@SuppressLint("ViewConstructor") // View is only created from code -public class PanelHeaderView extends ImageView { - public PanelHeaderView(Context context, HomeConfig.HeaderConfig config) { - super(context); - - setAdjustViewBounds(true); - - ImageLoader.with(context) - .load(config.getImageUrl()) - .into(this); - } - - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - int width = MeasureSpec.getSize(widthMeasureSpec); - - // Always span the whole width and adjust height as needed. - widthMeasureSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY); - - super.onMeasure(widthMeasureSpec, heightMeasureSpec); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/PanelInfoManager.java b/mobile/android/base/java/org/mozilla/gecko/home/PanelInfoManager.java deleted file mode 100644 index 089e178376..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/PanelInfoManager.java +++ /dev/null @@ -1,162 +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 java.util.ArrayList; -import java.util.List; -import java.util.Set; -import java.util.concurrent.atomic.AtomicInteger; - -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; -import org.mozilla.gecko.EventDispatcher; -import org.mozilla.gecko.GeckoApp; -import org.mozilla.gecko.GeckoAppShell; -import org.mozilla.gecko.home.HomeConfig.PanelConfig; -import org.mozilla.gecko.util.GeckoEventListener; -import org.mozilla.gecko.util.ThreadUtils; - -import android.util.Log; -import android.util.SparseArray; - -public class PanelInfoManager implements GeckoEventListener { - private static final String LOGTAG = "GeckoPanelInfoManager"; - - public class PanelInfo { - private final String mId; - private final String mTitle; - private final JSONObject mJSONData; - - public PanelInfo(String id, String title, JSONObject jsonData) { - mId = id; - mTitle = title; - mJSONData = jsonData; - } - - public String getId() { - return mId; - } - - public String getTitle() { - return mTitle; - } - - public PanelConfig toPanelConfig() { - try { - return new PanelConfig(mJSONData); - } catch (Exception e) { - Log.e(LOGTAG, "Failed to convert PanelInfo to PanelConfig", e); - return null; - } - } - } - - public interface RequestCallback { - public void onComplete(List panelInfos); - } - - private static final AtomicInteger sRequestId = new AtomicInteger(0); - - // Stores set of pending request callbacks. - private static final SparseArray sCallbacks = new SparseArray(); - - /** - * Asynchronously fetches list of available panels from Gecko - * for the given IDs. - * - * @param ids list of panel ids to be fetched. A null value will fetch all - * available panels. - * @param callback onComplete will be called on the UI thread. - */ - public void requestPanelsById(Set ids, RequestCallback callback) { - final int requestId = sRequestId.getAndIncrement(); - - synchronized (sCallbacks) { - // If there are no pending callbacks, register the event listener. - if (sCallbacks.size() == 0) { - GeckoApp.getEventDispatcher().registerGeckoThreadListener(this, - "HomePanels:Data"); - } - sCallbacks.put(requestId, callback); - } - - final JSONObject message = new JSONObject(); - try { - message.put("requestId", requestId); - - if (ids != null && ids.size() > 0) { - JSONArray idsArray = new JSONArray(); - for (String id : ids) { - idsArray.put(id); - } - - message.put("ids", idsArray); - } - } catch (JSONException e) { - Log.e(LOGTAG, "Failed to build event to request panels by id", e); - return; - } - - GeckoAppShell.notifyObservers("HomePanels:Get", message.toString()); - } - - /** - * Asynchronously fetches list of available panels from Gecko. - * - * @param callback onComplete will be called on the UI thread. - */ - public void requestAvailablePanels(RequestCallback callback) { - requestPanelsById(null, callback); - } - - /** - * Handles "HomePanels:Data" events. - */ - @Override - public void handleMessage(String event, JSONObject message) { - final ArrayList panelInfos = new ArrayList(); - - try { - final JSONArray panels = message.getJSONArray("panels"); - final int count = panels.length(); - for (int i = 0; i < count; i++) { - final PanelInfo panelInfo = getPanelInfoFromJSON(panels.getJSONObject(i)); - panelInfos.add(panelInfo); - } - - final RequestCallback callback; - final int requestId = message.getInt("requestId"); - - synchronized (sCallbacks) { - callback = sCallbacks.get(requestId); - sCallbacks.delete(requestId); - - // Unregister the event listener if there are no more pending callbacks. - if (sCallbacks.size() == 0) { - GeckoApp.getEventDispatcher().unregisterGeckoThreadListener(this, - "HomePanels:Data"); - } - } - - ThreadUtils.postToUiThread(new Runnable() { - @Override - public void run() { - callback.onComplete(panelInfos); - } - }); - } catch (JSONException e) { - Log.e(LOGTAG, "Exception handling " + event + " message", e); - } - } - - private PanelInfo getPanelInfoFromJSON(JSONObject jsonPanelInfo) throws JSONException { - final String id = jsonPanelInfo.getString("id"); - final String title = jsonPanelInfo.getString("title"); - - return new PanelInfo(id, title, jsonPanelInfo); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/PanelItemView.java b/mobile/android/base/java/org/mozilla/gecko/home/PanelItemView.java deleted file mode 100644 index 2a97d42bc8..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/PanelItemView.java +++ /dev/null @@ -1,136 +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.R; -import org.mozilla.gecko.db.BrowserContract.HomeItems; -import org.mozilla.gecko.home.HomeConfig.ItemType; - -import android.content.Context; -import android.database.Cursor; -import android.graphics.Color; -import android.text.TextUtils; -import android.view.LayoutInflater; -import android.view.View; -import android.widget.ImageView; -import android.widget.LinearLayout; -import android.widget.TextView; - -class PanelItemView extends LinearLayout { - private final TextView titleView; - private final TextView descriptionView; - private final ImageView imageView; - private final LinearLayout titleDescContainerView; - private final ImageView backgroundView; - - private PanelItemView(Context context, int layoutId) { - super(context); - - LayoutInflater.from(context).inflate(layoutId, this); - titleView = (TextView) findViewById(R.id.title); - descriptionView = (TextView) findViewById(R.id.description); - imageView = (ImageView) findViewById(R.id.image); - backgroundView = (ImageView) findViewById(R.id.background); - titleDescContainerView = (LinearLayout) findViewById(R.id.title_desc_container); - } - - public void updateFromCursor(Cursor cursor) { - int titleIndex = cursor.getColumnIndexOrThrow(HomeItems.TITLE); - final String titleText = cursor.getString(titleIndex); - - // Only show title if the item has one - final boolean hasTitle = !TextUtils.isEmpty(titleText); - titleView.setVisibility(hasTitle ? View.VISIBLE : View.GONE); - if (hasTitle) { - titleView.setText(titleText); - } - - int descriptionIndex = cursor.getColumnIndexOrThrow(HomeItems.DESCRIPTION); - final String descriptionText = cursor.getString(descriptionIndex); - - // Only show description if the item has one - // Descriptions are not supported for IconItemView objects (Bug 1157539) - final boolean hasDescription = !TextUtils.isEmpty(descriptionText); - if (descriptionView != null) { - descriptionView.setVisibility(hasDescription ? View.VISIBLE : View.GONE); - if (hasDescription) { - descriptionView.setText(descriptionText); - } - } - if (titleDescContainerView != null) { - titleDescContainerView.setVisibility(hasTitle || hasDescription ? View.VISIBLE : View.GONE); - } - - int imageIndex = cursor.getColumnIndexOrThrow(HomeItems.IMAGE_URL); - final String imageUrl = cursor.getString(imageIndex); - - // Only try to load the image if the item has define image URL - final boolean hasImageUrl = !TextUtils.isEmpty(imageUrl); - imageView.setVisibility(hasImageUrl ? View.VISIBLE : View.GONE); - - if (hasImageUrl) { - ImageLoader.with(getContext()) - .load(imageUrl) - .into(imageView); - } - - final int columnIndexBackgroundColor = cursor.getColumnIndex(HomeItems.BACKGROUND_COLOR); - if (columnIndexBackgroundColor != -1) { - final String color = cursor.getString(columnIndexBackgroundColor); - if (!TextUtils.isEmpty(color)) { - setBackgroundColor(Color.parseColor(color)); - } - } - - // Backgrounds are only supported for IconItemView objects (Bug 1157539) - final int columnIndexBackgroundUrl = cursor.getColumnIndex(HomeItems.BACKGROUND_URL); - if (columnIndexBackgroundUrl != -1) { - final String backgroundUrl = cursor.getString(columnIndexBackgroundUrl); - if (backgroundView != null && !TextUtils.isEmpty(backgroundUrl)) { - ImageLoader.with(getContext()) - .load(backgroundUrl) - .fit() - .into(backgroundView); - } - } - } - - private static class ArticleItemView extends PanelItemView { - private ArticleItemView(Context context) { - super(context, R.layout.panel_article_item); - setOrientation(LinearLayout.HORIZONTAL); - } - } - - private static class ImageItemView extends PanelItemView { - private ImageItemView(Context context) { - super(context, R.layout.panel_image_item); - setOrientation(LinearLayout.VERTICAL); - } - } - - private static class IconItemView extends PanelItemView { - private IconItemView(Context context) { - super(context, R.layout.panel_icon_item); - } - } - - public static PanelItemView create(Context context, ItemType itemType) { - switch (itemType) { - case ARTICLE: - return new ArticleItemView(context); - - case IMAGE: - return new ImageItemView(context); - - case ICON: - return new IconItemView(context); - - default: - throw new IllegalArgumentException("Could not create panel item view from " + itemType); - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/PanelLayout.java b/mobile/android/base/java/org/mozilla/gecko/home/PanelLayout.java deleted file mode 100644 index 2c2d89ae04..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/PanelLayout.java +++ /dev/null @@ -1,747 +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.R; -import org.mozilla.gecko.db.BrowserContract.HomeItems; -import org.mozilla.gecko.home.HomePager.OnUrlOpenListener; -import org.mozilla.gecko.home.HomeConfig.EmptyViewConfig; -import org.mozilla.gecko.home.HomeConfig.ItemHandler; -import org.mozilla.gecko.home.HomeConfig.PanelConfig; -import org.mozilla.gecko.home.HomeConfig.ViewConfig; -import org.mozilla.gecko.util.StringUtils; - -import android.content.Context; -import android.database.Cursor; -import android.os.Parcel; -import android.os.Parcelable; -import android.text.TextUtils; -import android.util.Log; -import android.util.SparseArray; -import android.view.KeyEvent; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.FrameLayout; -import android.widget.ImageView; -import android.widget.LinearLayout; -import android.widget.TextView; - -import java.lang.ref.SoftReference; -import java.util.EnumSet; -import java.util.LinkedList; -import java.util.Map; -import java.util.WeakHashMap; - -import com.squareup.picasso.Picasso; - -/** - * {@code PanelLayout} is the base class for custom layouts to be - * used in {@code DynamicPanel}. It provides the basic framework - * that enables custom layouts to request and reset datasets and - * create panel views. Furthermore, it automates most of the process - * of binding panel views with their respective datasets. - * - * {@code PanelLayout} abstracts the implemention details of how - * datasets are actually loaded through the {@DatasetHandler} interface. - * {@code DatasetHandler} provides two operations: request and reset. - * The results of the dataset requests done via the {@code DatasetHandler} - * are delivered to the {@code PanelLayout} with the {@code deliverDataset()} - * method. - * - * Subclasses of {@code PanelLayout} should simply use the utilities - * provided by {@code PanelLayout}. Namely: - * - * {@code requestDataset()} - To fetch datasets and auto-bind them to - * the existing panel views backed by them. - * - * {@code resetDataset()} - To release any resources associated with a - * previously loaded dataset. - * - * {@code createPanelView()} - To create a panel view for a ViewConfig - * associated with the panel. - * - * {@code disposePanelView()} - To dispose any dataset references associated - * with the given view. - * - * {@code PanelLayout} subclasses should always use {@code createPanelView()} - * to create the views dynamically created based on {@code ViewConfig}. This - * allows {@code PanelLayout} to auto-bind datasets with panel views. - * {@code PanelLayout} subclasses are free to have any type of views to arrange - * the panel views in different ways. - */ -abstract class PanelLayout extends FrameLayout { - private static final String LOGTAG = "GeckoPanelLayout"; - - protected final SparseArray mViewStates; - private final PanelConfig mPanelConfig; - private final DatasetHandler mDatasetHandler; - private final OnUrlOpenListener mUrlOpenListener; - private final ContextMenuRegistry mContextMenuRegistry; - - /** - * To be used by panel views to express that they are - * backed by datasets. - */ - public interface DatasetBacked { - public void setDataset(Cursor cursor); - public void setFilterManager(FilterManager manager); - } - - /** - * To be used by requests made to {@code DatasetHandler}s to couple dataset ID with current - * filter for queries on the database. - */ - public static class DatasetRequest implements Parcelable { - public enum Type implements Parcelable { - DATASET_LOAD, - FILTER_PUSH, - FILTER_POP; - - @Override - public int describeContents() { - return 0; - } - - @Override - public void writeToParcel(Parcel dest, int flags) { - dest.writeInt(ordinal()); - } - - public static final Creator CREATOR = new Creator() { - @Override - public Type createFromParcel(final Parcel source) { - return Type.values()[source.readInt()]; - } - - @Override - public Type[] newArray(final int size) { - return new Type[size]; - } - }; - } - - private final int mViewIndex; - private final Type mType; - private final String mDatasetId; - private final FilterDetail mFilterDetail; - - private DatasetRequest(Parcel in) { - this.mViewIndex = in.readInt(); - this.mType = (Type) in.readParcelable(getClass().getClassLoader()); - this.mDatasetId = in.readString(); - this.mFilterDetail = (FilterDetail) in.readParcelable(getClass().getClassLoader()); - } - - public DatasetRequest(int index, String datasetId, FilterDetail filterDetail) { - this(index, Type.DATASET_LOAD, datasetId, filterDetail); - } - - public DatasetRequest(int index, Type type, String datasetId, FilterDetail filterDetail) { - this.mViewIndex = index; - this.mType = type; - this.mDatasetId = datasetId; - this.mFilterDetail = filterDetail; - } - - public int getViewIndex() { - return mViewIndex; - } - - public Type getType() { - return mType; - } - - public String getDatasetId() { - return mDatasetId; - } - - public String getFilter() { - return (mFilterDetail != null ? mFilterDetail.filter : null); - } - - public FilterDetail getFilterDetail() { - return mFilterDetail; - } - - @Override - public int describeContents() { - return 0; - } - - @Override - public void writeToParcel(Parcel dest, int flags) { - dest.writeInt(mViewIndex); - dest.writeParcelable(mType, 0); - dest.writeString(mDatasetId); - dest.writeParcelable(mFilterDetail, 0); - } - - public String toString() { - return "{ index: " + mViewIndex + - ", type: " + mType + - ", dataset: " + mDatasetId + - ", filter: " + mFilterDetail + - " }"; - } - - public static final Creator CREATOR = new Creator() { - @Override - public DatasetRequest createFromParcel(Parcel in) { - return new DatasetRequest(in); - } - - @Override - public DatasetRequest[] newArray(int size) { - return new DatasetRequest[size]; - } - }; - } - - /** - * Defines the contract with the component that is responsible - * for handling datasets requests. - */ - public interface DatasetHandler { - /** - * Requests a dataset to be fetched and auto-bound to the - * panel views backed by it. - */ - public void requestDataset(DatasetRequest request); - - /** - * Releases any resources associated with a panel view. It will - * do nothing if the view with the given index been created - * before. - */ - public void resetDataset(int viewIndex); - } - - public interface PanelView { - public void setOnItemOpenListener(OnItemOpenListener listener); - public void setOnKeyListener(OnKeyListener listener); - public void setContextMenuInfoFactory(HomeContextMenuInfo.Factory factory); - } - - public interface FilterManager { - public FilterDetail getPreviousFilter(); - public boolean canGoBack(); - public void goBack(); - } - - public interface ContextMenuRegistry { - public void register(View view); - } - - public PanelLayout(Context context, PanelConfig panelConfig, DatasetHandler datasetHandler, - OnUrlOpenListener urlOpenListener, ContextMenuRegistry contextMenuRegistry) { - super(context); - mViewStates = new SparseArray(); - mPanelConfig = panelConfig; - mDatasetHandler = datasetHandler; - mUrlOpenListener = urlOpenListener; - mContextMenuRegistry = contextMenuRegistry; - } - - @Override - public void onDetachedFromWindow() { - super.onDetachedFromWindow(); - - final int count = mViewStates.size(); - for (int i = 0; i < count; i++) { - final ViewState viewState = mViewStates.valueAt(i); - - final View view = viewState.getView(); - if (view != null) { - maybeSetDataset(view, null); - } - } - mViewStates.clear(); - } - - /** - * Delivers the dataset as a {@code Cursor} to be bound to the - * panel view backed by it. This is used by the {@code DatasetHandler} - * in response to a dataset request. - */ - public final void deliverDataset(DatasetRequest request, Cursor cursor) { - Log.d(LOGTAG, "Delivering request: " + request); - final ViewState viewState = mViewStates.get(request.getViewIndex()); - if (viewState == null) { - return; - } - - switch (request.getType()) { - case FILTER_PUSH: - viewState.pushFilter(request.getFilterDetail()); - break; - case FILTER_POP: - viewState.popFilter(); - break; - } - - final View activeView = viewState.getActiveView(); - if (activeView == null) { - throw new IllegalStateException("No active view for view state: " + viewState.getIndex()); - } - - final ViewConfig viewConfig = viewState.getViewConfig(); - - final View newView; - if (cursor == null || cursor.getCount() == 0) { - newView = createEmptyView(viewConfig); - maybeSetDataset(activeView, null); - } else { - newView = createPanelView(viewConfig); - maybeSetDataset(newView, cursor); - } - - if (activeView != newView) { - replacePanelView(activeView, newView); - } - } - - /** - * Releases any references to the given dataset from all - * existing panel views. - */ - public final void releaseDataset(int viewIndex) { - Log.d(LOGTAG, "Releasing dataset: " + viewIndex); - final ViewState viewState = mViewStates.get(viewIndex); - if (viewState == null) { - return; - } - - final View view = viewState.getView(); - if (view != null) { - maybeSetDataset(view, null); - } - } - - /** - * Requests a dataset to be loaded and bound to any existing - * panel view backed by it. - */ - protected final void requestDataset(DatasetRequest request) { - Log.d(LOGTAG, "Requesting request: " + request); - if (mViewStates.get(request.getViewIndex()) == null) { - return; - } - - mDatasetHandler.requestDataset(request); - } - - /** - * Releases any resources associated with a panel view. - * e.g. close any associated {@code Cursor}. - */ - protected final void resetDataset(int viewIndex) { - Log.d(LOGTAG, "Resetting view with index: " + viewIndex); - if (mViewStates.get(viewIndex) == null) { - return; - } - - mDatasetHandler.resetDataset(viewIndex); - } - - /** - * Factory method to create instance of panels from a given - * {@code ViewConfig}. All panel views defined in {@code PanelConfig} - * should be created using this method so that {@PanelLayout} can - * keep track of panel views and their associated datasets. - */ - protected final View createPanelView(ViewConfig viewConfig) { - Log.d(LOGTAG, "Creating panel view: " + viewConfig.getType()); - - ViewState viewState = mViewStates.get(viewConfig.getIndex()); - if (viewState == null) { - viewState = new ViewState(viewConfig); - mViewStates.put(viewConfig.getIndex(), viewState); - } - - View view = viewState.getView(); - if (view == null) { - switch (viewConfig.getType()) { - case LIST: - view = new PanelListView(getContext(), viewConfig); - break; - - case GRID: - view = new PanelRecyclerView(getContext(), viewConfig); - break; - - default: - throw new IllegalStateException("Unrecognized view type in " + getClass().getSimpleName()); - } - - PanelView panelView = (PanelView) view; - panelView.setOnItemOpenListener(new PanelOnItemOpenListener(viewState)); - panelView.setOnKeyListener(new PanelKeyListener(viewState)); - panelView.setContextMenuInfoFactory(new HomeContextMenuInfo.Factory() { - @Override - public HomeContextMenuInfo makeInfoForCursor(View view, int position, long id, Cursor cursor) { - final HomeContextMenuInfo info = new HomeContextMenuInfo(view, position, id); - info.url = cursor.getString(cursor.getColumnIndexOrThrow(HomeItems.URL)); - info.title = cursor.getString(cursor.getColumnIndexOrThrow(HomeItems.TITLE)); - return info; - } - }); - - mContextMenuRegistry.register(view); - - if (view instanceof DatasetBacked) { - DatasetBacked datasetBacked = (DatasetBacked) view; - datasetBacked.setFilterManager(new PanelFilterManager(viewState)); - - if (viewConfig.isRefreshEnabled()) { - view = new PanelRefreshLayout(getContext(), view, - mPanelConfig.getId(), viewConfig.getIndex()); - } - } - - viewState.setView(view); - } - - return view; - } - - /** - * Dispose any dataset references associated with the - * given view. - */ - protected final void disposePanelView(View view) { - Log.d(LOGTAG, "Disposing panel view"); - final int count = mViewStates.size(); - for (int i = 0; i < count; i++) { - final ViewState viewState = mViewStates.valueAt(i); - - if (viewState.getView() == view) { - maybeSetDataset(view, null); - mViewStates.remove(viewState.getIndex()); - break; - } - } - } - - private void maybeSetDataset(View view, Cursor cursor) { - if (view instanceof DatasetBacked) { - final DatasetBacked dsb = (DatasetBacked) view; - dsb.setDataset(cursor); - } - } - - private View createEmptyView(ViewConfig viewConfig) { - Log.d(LOGTAG, "Creating empty view: " + viewConfig.getType()); - - ViewState viewState = mViewStates.get(viewConfig.getIndex()); - if (viewState == null) { - throw new IllegalStateException("No view state found for view index: " + viewConfig.getIndex()); - } - - View view = viewState.getEmptyView(); - if (view == null) { - view = LayoutInflater.from(getContext()).inflate(R.layout.home_empty_panel, null); - - final EmptyViewConfig emptyViewConfig = viewConfig.getEmptyViewConfig(); - - // XXX: Refactor this into a custom view (bug 985134) - final String text = (emptyViewConfig == null) ? null : emptyViewConfig.getText(); - final TextView textView = (TextView) view.findViewById(R.id.home_empty_text); - if (TextUtils.isEmpty(text)) { - textView.setText(R.string.home_default_empty); - } else { - textView.setText(text); - } - - final String imageUrl = (emptyViewConfig == null) ? null : emptyViewConfig.getImageUrl(); - final ImageView imageView = (ImageView) view.findViewById(R.id.home_empty_image); - - if (TextUtils.isEmpty(imageUrl)) { - imageView.setImageResource(R.drawable.icon_home_empty_firefox); - } else { - ImageLoader.with(getContext()) - .load(imageUrl) - .error(R.drawable.icon_home_empty_firefox) - .into(imageView); - } - - viewState.setEmptyView(view); - } - - return view; - } - - private void replacePanelView(View currentView, View newView) { - final ViewGroup parent = (ViewGroup) currentView.getParent(); - parent.addView(newView, parent.indexOfChild(currentView), currentView.getLayoutParams()); - parent.removeView(currentView); - } - - /** - * Must be implemented by {@code PanelLayout} subclasses to define - * what happens then the layout is first loaded. Should set initial - * UI state and request any necessary datasets. - */ - public abstract void load(); - - /** - * Represents a 'live' instance of a panel view associated with - * the {@code PanelLayout}. Is responsible for tracking the history stack of filters. - */ - protected class ViewState { - private final ViewConfig mViewConfig; - private SoftReference mView; - private SoftReference mEmptyView; - private LinkedList mFilterStack; - - public ViewState(ViewConfig viewConfig) { - mViewConfig = viewConfig; - mView = new SoftReference(null); - mEmptyView = new SoftReference(null); - } - - public ViewConfig getViewConfig() { - return mViewConfig; - } - - public int getIndex() { - return mViewConfig.getIndex(); - } - - public View getView() { - return mView.get(); - } - - public void setView(View view) { - mView = new SoftReference(view); - } - - public View getEmptyView() { - return mEmptyView.get(); - } - - public void setEmptyView(View view) { - mEmptyView = new SoftReference(view); - } - - public View getActiveView() { - final View view = getView(); - if (view != null && view.getParent() != null) { - return view; - } - - final View emptyView = getEmptyView(); - if (emptyView != null && emptyView.getParent() != null) { - return emptyView; - } - - return null; - } - - public String getDatasetId() { - return mViewConfig.getDatasetId(); - } - - public ItemHandler getItemHandler() { - return mViewConfig.getItemHandler(); - } - - /** - * Get the current filter that this view is displaying, or null if none. - */ - public FilterDetail getCurrentFilter() { - if (mFilterStack == null) { - return null; - } else { - return mFilterStack.peek(); - } - } - - /** - * Get the previous filter that this view was displaying, or null if none. - */ - public FilterDetail getPreviousFilter() { - if (!canPopFilter()) { - return null; - } - - return mFilterStack.get(1); - } - - /** - * Adds a filter to the history stack for this view. - */ - public void pushFilter(FilterDetail filter) { - if (mFilterStack == null) { - mFilterStack = new LinkedList(); - - // Initialize with the initial filter. - mFilterStack.push(new FilterDetail(mViewConfig.getFilter(), - mPanelConfig.getTitle())); - } - - mFilterStack.push(filter); - } - - /** - * Remove the most recent filter from the stack. - * - * @return whether the filter was popped - */ - public boolean popFilter() { - if (!canPopFilter()) { - return false; - } - - mFilterStack.pop(); - return true; - } - - public boolean canPopFilter() { - return (mFilterStack != null && mFilterStack.size() > 1); - } - } - - static class FilterDetail implements Parcelable { - final String filter; - final String title; - - private FilterDetail(Parcel in) { - this.filter = in.readString(); - this.title = in.readString(); - } - - public FilterDetail(String filter, String title) { - this.filter = filter; - this.title = title; - } - - @Override - public int describeContents() { - return 0; - } - - @Override - public void writeToParcel(Parcel dest, int flags) { - dest.writeString(filter); - dest.writeString(title); - } - - public static final Creator CREATOR = new Creator() { - @Override - public FilterDetail createFromParcel(Parcel in) { - return new FilterDetail(in); - } - - @Override - public FilterDetail[] newArray(int size) { - return new FilterDetail[size]; - } - }; - } - - /** - * Pushes filter to {@code ViewState}'s stack and makes request for new filter value. - */ - private void pushFilterOnView(ViewState viewState, FilterDetail filterDetail) { - final int index = viewState.getIndex(); - final String datasetId = viewState.getDatasetId(); - - mDatasetHandler.requestDataset(new DatasetRequest(index, - DatasetRequest.Type.FILTER_PUSH, - datasetId, - filterDetail)); - } - - /** - * Pops filter from {@code ViewState}'s stack and makes request for previous filter value. - * - * @return whether the filter has changed - */ - private boolean popFilterOnView(ViewState viewState) { - if (viewState.canPopFilter()) { - final int index = viewState.getIndex(); - final String datasetId = viewState.getDatasetId(); - final FilterDetail filterDetail = viewState.getPreviousFilter(); - - mDatasetHandler.requestDataset(new DatasetRequest(index, - DatasetRequest.Type.FILTER_POP, - datasetId, - filterDetail)); - - return true; - } else { - return false; - } - } - - public interface OnItemOpenListener { - public void onItemOpen(String url, String title); - } - - private class PanelOnItemOpenListener implements OnItemOpenListener { - private final ViewState mViewState; - - public PanelOnItemOpenListener(ViewState viewState) { - mViewState = viewState; - } - - @Override - public void onItemOpen(String url, String title) { - if (StringUtils.isFilterUrl(url)) { - FilterDetail filterDetail = new FilterDetail(StringUtils.getFilterFromUrl(url), title); - pushFilterOnView(mViewState, filterDetail); - } else { - EnumSet flags = EnumSet.noneOf(OnUrlOpenListener.Flags.class); - if (mViewState.getItemHandler() == ItemHandler.INTENT) { - flags.add(OnUrlOpenListener.Flags.OPEN_WITH_INTENT); - } - - mUrlOpenListener.onUrlOpen(url, flags); - } - } - } - - private class PanelKeyListener implements View.OnKeyListener { - private final ViewState mViewState; - - public PanelKeyListener(ViewState viewState) { - mViewState = viewState; - } - - @Override - public boolean onKey(View v, int keyCode, KeyEvent event) { - if (event.getAction() == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) { - return popFilterOnView(mViewState); - } - - return false; - } - } - - private class PanelFilterManager implements FilterManager { - private final ViewState mViewState; - - public PanelFilterManager(ViewState viewState) { - mViewState = viewState; - } - - @Override - public FilterDetail getPreviousFilter() { - return mViewState.getPreviousFilter(); - } - - @Override - public boolean canGoBack() { - return mViewState.canPopFilter(); - } - - @Override - public void goBack() { - popFilterOnView(mViewState); - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/PanelListView.java b/mobile/android/base/java/org/mozilla/gecko/home/PanelListView.java deleted file mode 100644 index 505fb9b0df..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/PanelListView.java +++ /dev/null @@ -1,83 +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 java.util.EnumSet; - -import org.mozilla.gecko.db.BrowserContract.HomeItems; -import org.mozilla.gecko.home.HomeConfig.ItemHandler; -import org.mozilla.gecko.home.HomeConfig.ViewConfig; -import org.mozilla.gecko.home.HomePager.OnUrlOpenListener; -import org.mozilla.gecko.home.PanelLayout.DatasetBacked; -import org.mozilla.gecko.home.PanelLayout.FilterManager; -import org.mozilla.gecko.home.PanelLayout.OnItemOpenListener; -import org.mozilla.gecko.home.PanelLayout.PanelView; - -import android.content.Context; -import android.database.Cursor; -import android.util.Log; -import android.view.View; -import android.widget.AdapterView; - -public class PanelListView extends HomeListView - implements DatasetBacked, PanelView { - - private static final String LOGTAG = "GeckoPanelListView"; - - private final ViewConfig viewConfig; - private final PanelViewAdapter adapter; - private final PanelViewItemHandler itemHandler; - private OnItemOpenListener itemOpenListener; - - public PanelListView(Context context, ViewConfig viewConfig) { - super(context); - - this.viewConfig = viewConfig; - itemHandler = new PanelViewItemHandler(); - - adapter = new PanelViewAdapter(context, viewConfig); - setAdapter(adapter); - - setOnItemClickListener(new PanelListItemClickListener()); - } - - @Override - public void onAttachedToWindow() { - super.onAttachedToWindow(); - itemHandler.setOnItemOpenListener(itemOpenListener); - } - - @Override - public void onDetachedFromWindow() { - super.onDetachedFromWindow(); - itemHandler.setOnItemOpenListener(null); - } - - @Override - public void setDataset(Cursor cursor) { - Log.d(LOGTAG, "Setting dataset: " + viewConfig.getDatasetId()); - adapter.swapCursor(cursor); - } - - @Override - public void setOnItemOpenListener(OnItemOpenListener listener) { - itemHandler.setOnItemOpenListener(listener); - itemOpenListener = listener; - } - - @Override - public void setFilterManager(FilterManager filterManager) { - adapter.setFilterManager(filterManager); - itemHandler.setFilterManager(filterManager); - } - - private class PanelListItemClickListener implements AdapterView.OnItemClickListener { - @Override - public void onItemClick(AdapterView parent, View view, int position, long id) { - itemHandler.openItemAtPosition(adapter.getCursor(), position); - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/PanelRecyclerView.java b/mobile/android/base/java/org/mozilla/gecko/home/PanelRecyclerView.java deleted file mode 100644 index 9145ab1e1c..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/PanelRecyclerView.java +++ /dev/null @@ -1,178 +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.R; -import org.mozilla.gecko.home.PanelLayout.DatasetBacked; -import org.mozilla.gecko.home.PanelLayout.PanelView; -import org.mozilla.gecko.widget.RecyclerViewClickSupport; -import org.mozilla.gecko.widget.RecyclerViewClickSupport.OnItemClickListener; -import org.mozilla.gecko.widget.RecyclerViewClickSupport.OnItemLongClickListener; - -import android.annotation.SuppressLint; -import android.content.Context; -import android.content.res.Resources; -import android.database.Cursor; -import android.support.v7.widget.GridLayoutManager; -import android.support.v7.widget.RecyclerView; -import android.view.View; - -/** - * RecyclerView implementation for grid home panels. - */ -@SuppressLint("ViewConstructor") // View is only created from code -public class PanelRecyclerView extends RecyclerView - implements DatasetBacked, PanelView, OnItemClickListener, OnItemLongClickListener { - private final PanelRecyclerViewAdapter adapter; - private final GridLayoutManager layoutManager; - private final PanelViewItemHandler itemHandler; - private final float columnWidth; - private final boolean autoFit; - private final HomeConfig.ViewConfig viewConfig; - - private PanelLayout.OnItemOpenListener itemOpenListener; - private HomeContextMenuInfo contextMenuInfo; - private HomeContextMenuInfo.Factory contextMenuInfoFactory; - - public PanelRecyclerView(Context context, HomeConfig.ViewConfig viewConfig) { - super(context); - - this.viewConfig = viewConfig; - - final Resources resources = context.getResources(); - - int spanCount; - if (viewConfig.getItemType() == HomeConfig.ItemType.ICON) { - autoFit = false; - spanCount = getResources().getInteger(R.integer.panel_icon_grid_view_columns); - } else { - autoFit = true; - spanCount = 1; - } - - columnWidth = resources.getDimension(R.dimen.panel_grid_view_column_width); - layoutManager = new GridLayoutManager(context, spanCount); - adapter = new PanelRecyclerViewAdapter(context, viewConfig); - itemHandler = new PanelViewItemHandler(); - - layoutManager.setSpanSizeLookup(new PanelSpanSizeLookup()); - - setLayoutManager(layoutManager); - setAdapter(adapter); - - int horizontalSpacing = (int) resources.getDimension(R.dimen.panel_grid_view_horizontal_spacing); - int verticalSpacing = (int) resources.getDimension(R.dimen.panel_grid_view_vertical_spacing); - int outerSpacing = (int) resources.getDimension(R.dimen.panel_grid_view_outer_spacing); - - addItemDecoration(new SpacingDecoration(horizontalSpacing, verticalSpacing)); - - setPadding(outerSpacing, outerSpacing, outerSpacing, outerSpacing); - setClipToPadding(false); - - RecyclerViewClickSupport.addTo(this) - .setOnItemClickListener(this) - .setOnItemLongClickListener(this); - } - - @Override - protected void onMeasure(int widthSpec, int heightSpec) { - super.onMeasure(widthSpec, heightSpec); - - if (autoFit) { - // Adjust span based on space available (What GridView does when you say numColumns="auto_fit") - final int spanCount = (int) Math.max(1, getMeasuredWidth() / columnWidth); - layoutManager.setSpanCount(spanCount); - } - } - - @Override - public void onAttachedToWindow() { - super.onAttachedToWindow(); - itemHandler.setOnItemOpenListener(itemOpenListener); - } - - @Override - public void onDetachedFromWindow() { - super.onDetachedFromWindow(); - itemHandler.setOnItemOpenListener(null); - } - - @Override - public void setDataset(Cursor cursor) { - adapter.swapCursor(cursor); - } - - @Override - public void setFilterManager(PanelLayout.FilterManager manager) { - adapter.setFilterManager(manager); - itemHandler.setFilterManager(manager); - } - - @Override - public void setOnItemOpenListener(PanelLayout.OnItemOpenListener listener) { - itemOpenListener = listener; - itemHandler.setOnItemOpenListener(listener); - } - - @Override - public HomeContextMenuInfo getContextMenuInfo() { - return contextMenuInfo; - } - - @Override - public void setContextMenuInfoFactory(HomeContextMenuInfo.Factory factory) { - contextMenuInfoFactory = factory; - } - - @Override - public void onItemClicked(RecyclerView recyclerView, int position, View v) { - if (viewConfig.hasHeaderConfig()) { - if (position == 0) { - itemOpenListener.onItemOpen(viewConfig.getHeaderConfig().getUrl(), null); - return; - } - - position--; - } - - itemHandler.openItemAtPosition(adapter.getCursor(), position); - } - - @Override - public boolean onItemLongClicked(RecyclerView recyclerView, int position, View v) { - if (viewConfig.hasHeaderConfig()) { - if (position == 0) { - final HomeConfig.HeaderConfig headerConfig = viewConfig.getHeaderConfig(); - - final HomeContextMenuInfo info = new HomeContextMenuInfo(v, position, -1); - info.url = headerConfig.getUrl(); - info.title = headerConfig.getUrl(); - - contextMenuInfo = info; - return showContextMenuForChild(this); - } - - position--; - } - - Cursor cursor = adapter.getCursor(); - cursor.moveToPosition(position); - - contextMenuInfo = contextMenuInfoFactory.makeInfoForCursor(recyclerView, position, -1, cursor); - return showContextMenuForChild(this); - } - - private class PanelSpanSizeLookup extends GridLayoutManager.SpanSizeLookup { - @Override - public int getSpanSize(int position) { - if (position == 0 && viewConfig.hasHeaderConfig()) { - return layoutManager.getSpanCount(); - } - - return 1; - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/PanelRecyclerViewAdapter.java b/mobile/android/base/java/org/mozilla/gecko/home/PanelRecyclerViewAdapter.java deleted file mode 100644 index fa632bccd0..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/PanelRecyclerViewAdapter.java +++ /dev/null @@ -1,137 +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.R; - -import android.content.Context; -import android.database.Cursor; -import android.support.v7.widget.RecyclerView; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.FrameLayout; - -public class PanelRecyclerViewAdapter extends RecyclerView.Adapter { - private static final int VIEW_TYPE_ITEM = 0; - private static final int VIEW_TYPE_BACK = 1; - private static final int VIEW_TYPE_HEADER = 2; - - public static class PanelViewHolder extends RecyclerView.ViewHolder { - public static PanelViewHolder create(View itemView) { - - // Wrap in a FrameLayout that will handle the highlight on touch - FrameLayout frameLayout = (FrameLayout) LayoutInflater.from(itemView.getContext()) - .inflate(R.layout.panel_item_container, null); - - frameLayout.addView(itemView, 0, new FrameLayout.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); - - return new PanelViewHolder(frameLayout); - } - - private PanelViewHolder(View itemView) { - super(itemView); - } - } - - private final Context context; - private final HomeConfig.ViewConfig viewConfig; - private PanelLayout.FilterManager filterManager; - private Cursor cursor; - - public PanelRecyclerViewAdapter(Context context, HomeConfig.ViewConfig viewConfig) { - this.context = context; - this.viewConfig = viewConfig; - } - - public void setFilterManager(PanelLayout.FilterManager filterManager) { - this.filterManager = filterManager; - } - - private boolean isShowingBack() { - return filterManager != null && filterManager.canGoBack(); - } - - public void swapCursor(Cursor cursor) { - this.cursor = cursor; - - notifyDataSetChanged(); - } - - public Cursor getCursor() { - return cursor; - } - - @Override - public int getItemViewType(int position) { - if (viewConfig.hasHeaderConfig() && position == 0) { - return VIEW_TYPE_HEADER; - } else if (isShowingBack() && position == getBackPosition()) { - return VIEW_TYPE_BACK; - } else { - return VIEW_TYPE_ITEM; - } - } - - @Override - public PanelViewHolder onCreateViewHolder(ViewGroup viewGroup, int viewType) { - switch (viewType) { - case VIEW_TYPE_HEADER: - return PanelViewHolder.create(new PanelHeaderView(context, viewConfig.getHeaderConfig())); - case VIEW_TYPE_BACK: - return PanelViewHolder.create(new PanelBackItemView(context, viewConfig.getBackImageUrl())); - case VIEW_TYPE_ITEM: - return PanelViewHolder.create(PanelItemView.create(context, viewConfig.getItemType())); - default: - throw new IllegalArgumentException("Unknown view type: " + viewType); - } - } - - @Override - public void onBindViewHolder(PanelViewHolder panelViewHolder, int position) { - final View view = ((FrameLayout) panelViewHolder.itemView).getChildAt(0); - - if (viewConfig.hasHeaderConfig()) { - if (position == 0) { - // Nothing to do here, the header is static - return; - } - } - - if (isShowingBack()) { - if (position == getBackPosition()) { - final PanelBackItemView item = (PanelBackItemView) view; - item.updateFromFilter(filterManager.getPreviousFilter()); - return; - } - } - - int actualPosition = position - - (isShowingBack() ? 1 : 0) - - (viewConfig.hasHeaderConfig() ? 1 : 0); - - cursor.moveToPosition(actualPosition); - - final PanelItemView panelItemView = (PanelItemView) view; - panelItemView.updateFromCursor(cursor); - } - - private int getBackPosition() { - return viewConfig.hasHeaderConfig() ? 1 : 0; - } - - @Override - public int getItemCount() { - if (cursor == null) { - return 0; - } - - return cursor.getCount() - + (isShowingBack() ? 1 : 0) - + (viewConfig.hasHeaderConfig() ? 1 : 0); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/PanelRefreshLayout.java b/mobile/android/base/java/org/mozilla/gecko/home/PanelRefreshLayout.java deleted file mode 100644 index d43a97f31a..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/PanelRefreshLayout.java +++ /dev/null @@ -1,90 +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.R; - -import org.mozilla.gecko.GeckoAppShell; -import org.mozilla.gecko.home.PanelLayout.DatasetBacked; -import org.mozilla.gecko.home.PanelLayout.FilterManager; - -import org.json.JSONException; -import org.json.JSONObject; - -import android.content.Context; -import android.database.Cursor; -import android.support.v4.widget.SwipeRefreshLayout; -import android.util.Log; -import android.view.View; - -/** - * Used to wrap a {@code DatasetBacked} ListView or GridView to give the child view swipe-to-refresh - * capabilities. - * - * This view acts as a decorator to forward the {@code DatasetBacked} methods to the child view - * while providing the refresh gesture support on top of it. - */ -class PanelRefreshLayout extends SwipeRefreshLayout implements DatasetBacked { - private static final String LOGTAG = "GeckoPanelRefreshLayout"; - - private static final String JSON_KEY_PANEL_ID = "panelId"; - private static final String JSON_KEY_VIEW_INDEX = "viewIndex"; - - private final String panelId; - private final int viewIndex; - private final DatasetBacked datasetBacked; - - /** - * @param context Android context. - * @param childView ListView or GridView. Must implement {@code DatasetBacked}. - * @param panelId The ID from the {@code PanelConfig}. - * @param viewIndex The index from the {@code ViewConfig}. - */ - public PanelRefreshLayout(Context context, View childView, String panelId, int viewIndex) { - super(context); - - if (!(childView instanceof DatasetBacked)) { - throw new IllegalArgumentException("View must implement DatasetBacked to be refreshed"); - } - - this.panelId = panelId; - this.viewIndex = viewIndex; - this.datasetBacked = (DatasetBacked) childView; - - setOnRefreshListener(new RefreshListener()); - addView(childView); - - // Must be called after the child view has been added. - setColorSchemeResources(R.color.fennec_ui_orange, R.color.action_orange); - } - - @Override - public void setDataset(Cursor cursor) { - datasetBacked.setDataset(cursor); - setRefreshing(false); - } - - @Override - public void setFilterManager(FilterManager manager) { - datasetBacked.setFilterManager(manager); - } - - private class RefreshListener implements OnRefreshListener { - @Override - public void onRefresh() { - final JSONObject response = new JSONObject(); - try { - response.put(JSON_KEY_PANEL_ID, panelId); - response.put(JSON_KEY_VIEW_INDEX, viewIndex); - } catch (JSONException e) { - Log.e(LOGTAG, "Could not create refresh message", e); - return; - } - - GeckoAppShell.notifyObservers("HomePanels:RefreshView", response.toString()); - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/PanelViewAdapter.java b/mobile/android/base/java/org/mozilla/gecko/home/PanelViewAdapter.java deleted file mode 100644 index cf03c50c0b..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/PanelViewAdapter.java +++ /dev/null @@ -1,113 +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.home.HomeConfig.ItemType; -import org.mozilla.gecko.home.HomeConfig.ViewConfig; -import org.mozilla.gecko.home.PanelLayout.FilterManager; - -import org.mozilla.gecko.R; - -import android.content.Context; -import android.database.Cursor; -import android.support.v4.widget.CursorAdapter; -import android.view.View; -import android.view.ViewGroup; - -class PanelViewAdapter extends CursorAdapter { - private static final int VIEW_TYPE_ITEM = 0; - private static final int VIEW_TYPE_BACK = 1; - - private final ViewConfig viewConfig; - private FilterManager filterManager; - private final Context context; - - public PanelViewAdapter(Context context, ViewConfig viewConfig) { - super(context, null, 0); - this.context = context; - this.viewConfig = viewConfig; - } - - public void setFilterManager(FilterManager manager) { - this.filterManager = manager; - } - - @Override - public final int getViewTypeCount() { - return 2; - } - - @Override - public int getCount() { - return super.getCount() + (isShowingBack() ? 1 : 0); - } - - @Override - public int getItemViewType(int position) { - if (isShowingBack() && position == 0) { - return VIEW_TYPE_BACK; - } else { - return VIEW_TYPE_ITEM; - } - } - - @Override - public final View getView(int position, View convertView, ViewGroup parent) { - if (convertView == null) { - convertView = newView(parent.getContext(), position, parent); - } - - bindView(convertView, position); - return convertView; - } - - private View newView(Context context, int position, ViewGroup parent) { - if (getItemViewType(position) == VIEW_TYPE_BACK) { - return new PanelBackItemView(context, viewConfig.getBackImageUrl()); - } else { - return PanelItemView.create(context, viewConfig.getItemType()); - } - } - - private void bindView(View view, int position) { - if (isShowingBack()) { - if (position == 0) { - final PanelBackItemView item = (PanelBackItemView) view; - item.updateFromFilter(filterManager.getPreviousFilter()); - return; - } - - position--; - } - - final Cursor cursor = getCursor(position); - final PanelItemView item = (PanelItemView) view; - item.updateFromCursor(cursor); - } - - private boolean isShowingBack() { - return filterManager != null && filterManager.canGoBack(); - } - - private final Cursor getCursor(int position) { - final Cursor cursor = getCursor(); - if (cursor == null || !cursor.moveToPosition(position)) { - throw new IllegalStateException("Couldn't move cursor to position " + position); - } - - return cursor; - } - - @Override - public final void bindView(View view, Context context, Cursor cursor) { - // Do nothing. - } - - @Override - public final View newView(Context context, Cursor cursor, ViewGroup parent) { - return null; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/PanelViewItemHandler.java b/mobile/android/base/java/org/mozilla/gecko/home/PanelViewItemHandler.java deleted file mode 100644 index a69db0b418..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/PanelViewItemHandler.java +++ /dev/null @@ -1,59 +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.db.BrowserContract.HomeItems; -import org.mozilla.gecko.home.HomeConfig.ViewConfig; -import org.mozilla.gecko.home.HomePager.OnUrlOpenListener; -import org.mozilla.gecko.home.PanelLayout.FilterManager; -import org.mozilla.gecko.home.PanelLayout.OnItemOpenListener; - -import android.database.Cursor; - -import java.util.EnumSet; - -class PanelViewItemHandler { - private OnItemOpenListener mItemOpenListener; - private FilterManager mFilterManager; - - public void setOnItemOpenListener(OnItemOpenListener listener) { - mItemOpenListener = listener; - } - - public void setFilterManager(FilterManager manager) { - mFilterManager = manager; - } - - /** - * If item at this position is a back item, perform the go back action via the - * {@code FilterManager}. Otherwise, prepare the url to be opened by the - * {@code OnUrlOpenListener}. - */ - public void openItemAtPosition(Cursor cursor, int position) { - if (mFilterManager != null && mFilterManager.canGoBack()) { - if (position == 0) { - mFilterManager.goBack(); - return; - } - - position--; - } - - if (cursor == null || !cursor.moveToPosition(position)) { - throw new IllegalStateException("Couldn't move cursor to position " + position); - } - - int urlIndex = cursor.getColumnIndexOrThrow(HomeItems.URL); - final String url = cursor.getString(urlIndex); - - int titleIndex = cursor.getColumnIndexOrThrow(HomeItems.TITLE); - final String title = cursor.getString(titleIndex); - - if (mItemOpenListener != null) { - mItemOpenListener.onItemOpen(url, title); - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/PinSiteDialog.java b/mobile/android/base/java/org/mozilla/gecko/home/PinSiteDialog.java deleted file mode 100644 index 230b1d3290..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/PinSiteDialog.java +++ /dev/null @@ -1,256 +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 java.util.EnumSet; - -import org.mozilla.gecko.R; -import org.mozilla.gecko.db.BrowserContract.URLColumns; -import org.mozilla.gecko.db.BrowserDB.FilterFlags; -import org.mozilla.gecko.util.StringUtils; - -import android.app.Dialog; -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.KeyEvent; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.view.Window; -import android.view.WindowManager; -import android.widget.AdapterView; -import android.widget.EditText; -import android.widget.ListView; - -/** - * Dialog fragment that displays frecency search results, for pinning a site, in a GridView. - */ -class PinSiteDialog extends DialogFragment { - // Listener for url selection - public static interface OnSiteSelectedListener { - public void onSiteSelected(String url, String title); - } - - // Cursor loader ID for search query - private static final int LOADER_ID_SEARCH = 0; - - // 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 loader - private CursorLoaderCallbacks mLoaderCallbacks; - - // Bookmark selected listener - private OnSiteSelectedListener mOnSiteSelectedListener; - - public static PinSiteDialog newInstance() { - return new PinSiteDialog(); - } - - private PinSiteDialog() { - } - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - setStyle(DialogFragment.STYLE_NO_TITLE, android.R.style.Theme_Holo_Light_Dialog); - } - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - // All list views are styled to look the same with a global activity theme. - // If the style of the list changes, inflate it from an XML. - return inflater.inflate(R.layout.pin_site_dialog, container, false); - } - - @Override - public void onViewCreated(View view, Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - - mSearch = (EditText) view.findViewById(R.id.search); - mSearch.addTextChangedListener(new TextWatcher() { - @Override - public void afterTextChanged(Editable s) { - } - - @Override - public void beforeTextChanged(CharSequence s, int start, int count, int after) { - } - - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) { - setSearchTerm(mSearch.getText().toString()); - filter(mSearchTerm); - } - }); - - mSearch.setOnKeyListener(new View.OnKeyListener() { - @Override - public boolean onKey(View v, int keyCode, KeyEvent event) { - if (keyCode != KeyEvent.KEYCODE_ENTER || mOnSiteSelectedListener == null) { - return false; - } - - // If the user manually entered a search term or URL, wrap the value in - // a special URI until we can get a valid URL for this bookmark. - final String text = mSearch.getText().toString().trim(); - if (!TextUtils.isEmpty(text)) { - final String url = StringUtils.encodeUserEnteredUrl(text); - mOnSiteSelectedListener.onSiteSelected(url, text); - dismiss(); - } - - return true; - } - }); - - mSearch.setOnFocusChangeListener(new View.OnFocusChangeListener() { - @Override - public void onFocusChange(View v, boolean hasFocus) { - if (hasFocus) { - // On rotation, the view gets destroyed and we could be in a race to get the dialog - // and window (see bug 1072959). - Dialog dialog = getDialog(); - if (dialog == null) { - return; - } - Window window = dialog.getWindow(); - if (window == null) { - return; - } - window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE); - } - } - }); - - mList = (HomeListView) view.findViewById(R.id.list); - mList.setOnItemClickListener(new AdapterView.OnItemClickListener() { - @Override - public void onItemClick(AdapterView parent, View view, int position, long id) { - if (mOnSiteSelectedListener != null) { - final Cursor c = mAdapter.getCursor(); - if (c == null || !c.moveToPosition(position)) { - return; - } - - final String url = c.getString(c.getColumnIndexOrThrow(URLColumns.URL)); - final String title = c.getString(c.getColumnIndexOrThrow(URLColumns.TITLE)); - mOnSiteSelectedListener.onSiteSelected(url, title); - } - - // Dismiss the fragment and the dialog. - dismiss(); - } - }); - } - - @Override - public void onActivityCreated(Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - - final LoaderManager manager = getLoaderManager(); - - // Initialize the search adapter - mAdapter = new SearchAdapter(getActivity()); - mList.setAdapter(mAdapter); - - // Create callbacks before the initial loader is started - mLoaderCallbacks = new CursorLoaderCallbacks(); - - // Reconnect to the loader only if present - manager.initLoader(LOADER_ID_SEARCH, null, mLoaderCallbacks); - - // If there is a search term, put it in the text field - if (!TextUtils.isEmpty(mSearchTerm)) { - mSearch.setText(mSearchTerm); - mSearch.selectAll(); - } - - // Always start with an empty filter - filter(""); - } - - @Override - public void onDestroyView() { - super.onDestroyView(); - - // Discard any additional site selection as the dialog - // is getting destroyed (see bug 935542). - setOnSiteSelectedListener(null); - } - - public void setSearchTerm(String searchTerm) { - mSearchTerm = searchTerm; - } - - private void filter(String searchTerm) { - // Restart loaders with the new search term - SearchLoader.restart(getLoaderManager(), LOADER_ID_SEARCH, - mLoaderCallbacks, searchTerm, - EnumSet.of(FilterFlags.EXCLUDE_PINNED_SITES)); - } - - public void setOnSiteSelectedListener(OnSiteSelectedListener listener) { - mOnSiteSelectedListener = listener; - } - - private static class SearchAdapter extends CursorAdapter { - private final LayoutInflater mInflater; - - public SearchAdapter(Context context) { - super(context, null, 0); - mInflater = LayoutInflater.from(context); - } - - @Override - public void bindView(View view, Context context, Cursor cursor) { - TwoLinePageRow row = (TwoLinePageRow) view; - row.setShowIcons(false); - row.updateFromCursor(cursor); - } - - @Override - public View newView(Context context, Cursor cursor, ViewGroup parent) { - return (TwoLinePageRow) mInflater.inflate(R.layout.home_item_row, parent, false); - } - } - - private class CursorLoaderCallbacks implements LoaderCallbacks { - @Override - public Loader onCreateLoader(int id, Bundle args) { - return SearchLoader.createInstance(getActivity(), args); - } - - @Override - public void onLoadFinished(Loader loader, Cursor c) { - mAdapter.swapCursor(c); - } - - @Override - public void onLoaderReset(Loader loader) { - mAdapter.swapCursor(null); - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/RecentTabsAdapter.java b/mobile/android/base/java/org/mozilla/gecko/home/RecentTabsAdapter.java deleted file mode 100644 index 3091f77da9..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/RecentTabsAdapter.java +++ /dev/null @@ -1,454 +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.support.v7.widget.RecyclerView; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.TextView; - -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; -import org.mozilla.gecko.AboutPages; -import org.mozilla.gecko.EventDispatcher; -import org.mozilla.gecko.GeckoApp; -import org.mozilla.gecko.GeckoAppShell; -import org.mozilla.gecko.GeckoProfile; -import org.mozilla.gecko.R; -import org.mozilla.gecko.SessionParser; -import org.mozilla.gecko.home.CombinedHistoryAdapter.RecentTabsUpdateHandler; -import org.mozilla.gecko.home.CombinedHistoryPanel.PanelStateUpdateHandler; -import org.mozilla.gecko.util.EventCallback; -import org.mozilla.gecko.util.NativeEventListener; -import org.mozilla.gecko.util.NativeJSObject; -import org.mozilla.gecko.util.ThreadUtils; - -import java.util.ArrayList; -import java.util.List; - -import static org.mozilla.gecko.home.CombinedHistoryItem.ItemType; -import static org.mozilla.gecko.home.CombinedHistoryPanel.OnPanelLevelChangeListener.PanelLevel.CHILD_RECENT_TABS; - -public class RecentTabsAdapter extends RecyclerView.Adapter - implements CombinedHistoryRecyclerView.AdapterContextMenuBuilder, NativeEventListener { - private static final String LOGTAG = "GeckoRecentTabsAdapter"; - - private static final int NAVIGATION_BACK_BUTTON_INDEX = 0; - - private static final String TELEMETRY_EXTRA_LAST_TIME = "recent_tabs_last_time"; - private static final String TELEMETRY_EXTRA_RECENTLY_CLOSED = "recent_closed_tabs"; - private static final String TELEMETRY_EXTRA_MIXED = "recent_tabs_mixed"; - - // Recently closed tabs from Gecko. - private ClosedTab[] recentlyClosedTabs; - private boolean recentlyClosedTabsReceived = false; - - // "Tabs from last time". - private ClosedTab[] lastSessionTabs; - - public static final class ClosedTab { - public final String url; - public final String title; - public final String data; - - public ClosedTab(String url, String title, String data) { - this.url = url; - this.title = title; - this.data = data; - } - } - - private final Context context; - private final RecentTabsUpdateHandler recentTabsUpdateHandler; - private final PanelStateUpdateHandler panelStateUpdateHandler; - - public RecentTabsAdapter(Context context, - RecentTabsUpdateHandler recentTabsUpdateHandler, - PanelStateUpdateHandler panelStateUpdateHandler) { - this.context = context; - this.recentTabsUpdateHandler = recentTabsUpdateHandler; - this.panelStateUpdateHandler = panelStateUpdateHandler; - recentlyClosedTabs = new ClosedTab[0]; - lastSessionTabs = new ClosedTab[0]; - - readPreviousSessionData(); - } - - public void startListeningForClosedTabs() { - EventDispatcher.getInstance().registerGeckoThreadListener(this, "ClosedTabs:Data"); - GeckoAppShell.notifyObservers("ClosedTabs:StartNotifications", null); - } - - public void stopListeningForClosedTabs() { - GeckoAppShell.notifyObservers("ClosedTabs:StopNotifications", null); - EventDispatcher.getInstance().unregisterGeckoThreadListener(this, "ClosedTabs:Data"); - recentlyClosedTabsReceived = false; - } - - public void startListeningForHistorySanitize() { - EventDispatcher.getInstance().registerGeckoThreadListener(this, "Sanitize:Finished"); - } - - public void stopListeningForHistorySanitize() { - EventDispatcher.getInstance().unregisterGeckoThreadListener(this, "Sanitize:Finished"); - } - - @Override - public void handleMessage(String event, NativeJSObject message, EventCallback callback) { - switch (event) { - case "ClosedTabs:Data": - updateRecentlyClosedTabs(message); - break; - case "Sanitize:Finished": - clearLastSessionData(); - break; - } - } - - private void updateRecentlyClosedTabs(NativeJSObject message) { - final NativeJSObject[] tabs = message.getObjectArray("tabs"); - final int length = tabs.length; - - final ClosedTab[] closedTabs = new ClosedTab[length]; - for (int i = 0; i < length; i++) { - final NativeJSObject tab = tabs[i]; - closedTabs[i] = new ClosedTab(tab.getString("url"), tab.getString("title"), tab.getObject("data").toString()); - } - - // Only modify recentlyClosedTabs on the UI thread. - ThreadUtils.postToUiThread(new Runnable() { - @Override - public void run() { - // Save some data about the old panel state, so we can be - // smarter about notifying the recycler view which bits changed. - int prevClosedTabsCount = recentlyClosedTabs.length; - boolean prevSectionHeaderVisibility = isSectionHeaderVisible(); - int prevSectionHeaderIndex = getSectionHeaderIndex(); - - recentlyClosedTabs = closedTabs; - recentlyClosedTabsReceived = true; - recentTabsUpdateHandler.onRecentTabsCountUpdated( - getClosedTabsCount(), recentlyClosedTabsReceived); - panelStateUpdateHandler.onPanelStateUpdated(CHILD_RECENT_TABS); - - // Handle the section header hiding/unhiding. - updateHeaderVisibility(prevSectionHeaderVisibility, prevSectionHeaderIndex); - - // Update the "Recently closed" part of the tab list. - updateTabsList(prevClosedTabsCount, recentlyClosedTabs.length, getFirstRecentTabIndex(), getLastRecentTabIndex()); - } - }); - } - - private void readPreviousSessionData() { - // If we happen to initialise before GeckoApp, waiting on either the main or the background - // thread can lead to a deadlock, so we have to run on a separate thread instead. - final Thread parseThread = new Thread(new Runnable() { - @Override - public void run() { - // Make sure that the start up code has had a chance to update sessionstore.old as necessary. - GeckoProfile.get(context).waitForOldSessionDataProcessing(); - - final String jsonString = GeckoProfile.get(context).readPreviousSessionFile(); - if (jsonString == null) { - // No previous session data. - return; - } - - final List parsedTabs = new ArrayList<>(); - - new SessionParser() { - @Override - public void onTabRead(SessionTab tab) { - final String url = tab.getUrl(); - - // Don't show last tabs for about:home - if (AboutPages.isAboutHome(url)) { - return; - } - - parsedTabs.add(new ClosedTab(url, tab.getTitle(), tab.getTabObject().toString())); - } - }.parse(jsonString); - - final ClosedTab[] closedTabs = parsedTabs.toArray(new ClosedTab[parsedTabs.size()]); - - // Only modify lastSessionTabs on the UI thread. - ThreadUtils.postToUiThread(new Runnable() { - @Override - public void run() { - // Save some data about the old panel state, so we can be - // smarter about notifying the recycler view which bits changed. - int prevClosedTabsCount = lastSessionTabs.length; - boolean prevSectionHeaderVisibility = isSectionHeaderVisible(); - int prevSectionHeaderIndex = getSectionHeaderIndex(); - - lastSessionTabs = closedTabs; - recentTabsUpdateHandler.onRecentTabsCountUpdated( - getClosedTabsCount(), recentlyClosedTabsReceived); - panelStateUpdateHandler.onPanelStateUpdated(CHILD_RECENT_TABS); - - // Handle the section header hiding/unhiding. - updateHeaderVisibility(prevSectionHeaderVisibility, prevSectionHeaderIndex); - - // Update the "Tabs from last time" part of the tab list. - updateTabsList(prevClosedTabsCount, lastSessionTabs.length, getFirstLastSessionTabIndex(), getLastLastSessionTabIndex()); - } - }); - } - }, "LastSessionTabsThread"); - - parseThread.start(); - } - - private void clearLastSessionData() { - final ClosedTab[] emptyLastSessionTabs = new ClosedTab[0]; - - // Only modify mLastSessionTabs on the UI thread. - ThreadUtils.postToUiThread(new Runnable() { - @Override - public void run() { - // Save some data about the old panel state, so we can be - // smarter about notifying the recycler view which bits changed. - int prevClosedTabsCount = lastSessionTabs.length; - boolean prevSectionHeaderVisibility = isSectionHeaderVisible(); - int prevSectionHeaderIndex = getSectionHeaderIndex(); - - lastSessionTabs = emptyLastSessionTabs; - recentTabsUpdateHandler.onRecentTabsCountUpdated( - getClosedTabsCount(), recentlyClosedTabsReceived); - panelStateUpdateHandler.onPanelStateUpdated(CHILD_RECENT_TABS); - - // Handle the section header hiding. - updateHeaderVisibility(prevSectionHeaderVisibility, prevSectionHeaderIndex); - - // Handle the "tabs from last time" being cleared. - if (prevClosedTabsCount > 0) { - notifyItemRangeRemoved(getFirstLastSessionTabIndex(), prevClosedTabsCount); - } - } - }); - } - - private void updateHeaderVisibility(boolean prevSectionHeaderVisibility, int prevSectionHeaderIndex) { - if (prevSectionHeaderVisibility && !isSectionHeaderVisible()) { - notifyItemRemoved(prevSectionHeaderIndex); - } else if (!prevSectionHeaderVisibility && isSectionHeaderVisible()) { - notifyItemInserted(getSectionHeaderIndex()); - } - } - - /** - * Updates the tab list as necessary to account for any changes in tab count in a particular data source. - * - * Since the session store only sends out full updates, we don't know for sure what has changed compared - * to the last data set, so we can only animate if the tab count actually changes. - * - * @param prevClosedTabsCount The previous number of closed tabs from that data source. - * @param closedTabsCount The current number of closed tabs contained in that data source. - * @param firstTabListIndex The current position of that data source's first item in the RecyclerView. - * @param lastTabListIndex The current position of that data source's last item in the RecyclerView. - */ - private void updateTabsList(int prevClosedTabsCount, int closedTabsCount, int firstTabListIndex, int lastTabListIndex) { - final int closedTabsCountChange = closedTabsCount - prevClosedTabsCount; - - if (closedTabsCountChange <= 0) { - notifyItemRangeRemoved(lastTabListIndex + 1, -closedTabsCountChange); // Remove tabs from the bottom of the list. - notifyItemRangeChanged(firstTabListIndex, closedTabsCount); // Update the contents of the remaining items. - } else { // closedTabsCountChange > 0 - notifyItemRangeInserted(firstTabListIndex, closedTabsCountChange); // Add additional tabs at the top of the list. - notifyItemRangeChanged(firstTabListIndex + closedTabsCountChange, prevClosedTabsCount); // Update any previous list items. - } - } - - public String restoreTabFromPosition(int position) { - final List dataList = new ArrayList<>(1); - dataList.add(getClosedTabForPosition(position).data); - - final String telemetryExtra = - position > getLastRecentTabIndex() ? TELEMETRY_EXTRA_LAST_TIME : TELEMETRY_EXTRA_RECENTLY_CLOSED; - - restoreSessionWithHistory(dataList); - - return telemetryExtra; - } - - public String restoreAllTabs() { - if (recentlyClosedTabs.length == 0 && lastSessionTabs.length == 0) { - return null; - } - - final List dataList = new ArrayList<>(getClosedTabsCount()); - addTabDataToList(dataList, recentlyClosedTabs); - addTabDataToList(dataList, lastSessionTabs); - - final String telemetryExtra = recentlyClosedTabs.length > 0 && lastSessionTabs.length > 0 ? TELEMETRY_EXTRA_MIXED : - recentlyClosedTabs.length > 0 ? TELEMETRY_EXTRA_RECENTLY_CLOSED : TELEMETRY_EXTRA_LAST_TIME; - - restoreSessionWithHistory(dataList); - - return telemetryExtra; - } - - private void addTabDataToList(List dataList, ClosedTab[] closedTabs) { - for (ClosedTab closedTab : closedTabs) { - dataList.add(closedTab.data); - } - } - - private static void restoreSessionWithHistory(List dataList) { - final JSONObject json = new JSONObject(); - try { - json.put("tabs", new JSONArray(dataList)); - } catch (JSONException e) { - Log.e(LOGTAG, "JSON error", e); - } - - GeckoAppShell.notifyObservers("Session:RestoreRecentTabs", json.toString()); - } - - @Override - public CombinedHistoryItem onCreateViewHolder(ViewGroup parent, int viewType) { - final LayoutInflater inflater = LayoutInflater.from(parent.getContext()); - final View view; - - final CombinedHistoryItem.ItemType itemType = CombinedHistoryItem.ItemType.viewTypeToItemType(viewType); - - switch (itemType) { - case NAVIGATION_BACK: - view = inflater.inflate(R.layout.home_combined_back_item, parent, false); - return new CombinedHistoryItem.HistoryItem(view); - - case SECTION_HEADER: - view = inflater.inflate(R.layout.home_header_row, parent, false); - return new CombinedHistoryItem.BasicItem(view); - - case CLOSED_TAB: - view = inflater.inflate(R.layout.home_item_row, parent, false); - return new CombinedHistoryItem.HistoryItem(view); - } - return null; - } - - @Override - public void onBindViewHolder(CombinedHistoryItem holder, final int position) { - final CombinedHistoryItem.ItemType itemType = getItemTypeForPosition(position); - - switch (itemType) { - case SECTION_HEADER: - ((TextView) holder.itemView).setText(context.getString(R.string.home_closed_tabs_title2)); - break; - - case CLOSED_TAB: - final ClosedTab closedTab = getClosedTabForPosition(position); - ((CombinedHistoryItem.HistoryItem) holder).bind(closedTab); - break; - } - } - - @Override - public int getItemCount() { - int itemCount = 1; // NAVIGATION_BACK button is always visible. - - if (isSectionHeaderVisible()) { - itemCount += 1; - } - - itemCount += getClosedTabsCount(); - - return itemCount; - } - - private CombinedHistoryItem.ItemType getItemTypeForPosition(int position) { - if (position == NAVIGATION_BACK_BUTTON_INDEX) { - return ItemType.NAVIGATION_BACK; - } - - if (position == getSectionHeaderIndex() && isSectionHeaderVisible()) { - return ItemType.SECTION_HEADER; - } - - return ItemType.CLOSED_TAB; - } - - @Override - public int getItemViewType(int position) { - return CombinedHistoryItem.ItemType.itemTypeToViewType(getItemTypeForPosition(position)); - } - - public int getClosedTabsCount() { - return recentlyClosedTabs.length + lastSessionTabs.length; - } - - private boolean isSectionHeaderVisible() { - return recentlyClosedTabs.length > 0 || lastSessionTabs.length > 0; - } - - private int getSectionHeaderIndex() { - return isSectionHeaderVisible() ? - NAVIGATION_BACK_BUTTON_INDEX + 1 : - NAVIGATION_BACK_BUTTON_INDEX; - } - - private int getFirstRecentTabIndex() { - return getSectionHeaderIndex() + 1; - } - - private int getLastRecentTabIndex() { - return getSectionHeaderIndex() + recentlyClosedTabs.length; - } - - private int getFirstLastSessionTabIndex() { - return getLastRecentTabIndex() + 1; - } - - private int getLastLastSessionTabIndex() { - return getLastRecentTabIndex() + lastSessionTabs.length; - } - - /** - * Get the closed tab corresponding to a RecyclerView list item. - * - * The Recent Tab folder combines two data sources, so if we want to get the ClosedTab object - * behind a certain list item, we need to route this request to the corresponding data source - * and also transform the global list position into a local array index. - */ - private ClosedTab getClosedTabForPosition(int position) { - final ClosedTab closedTab; - if (position <= getLastRecentTabIndex()) { // Upper part of the list is "Recently closed tabs". - closedTab = recentlyClosedTabs[position - getFirstRecentTabIndex()]; - } else { // Lower part is "Tabs from last time". - closedTab = lastSessionTabs[position - getFirstLastSessionTabIndex()]; - } - - return closedTab; - } - - @Override - public HomeContextMenuInfo makeContextMenuInfoFromPosition(View view, int position) { - final CombinedHistoryItem.ItemType itemType = getItemTypeForPosition(position); - final HomeContextMenuInfo info; - - switch (itemType) { - case CLOSED_TAB: - info = new HomeContextMenuInfo(view, position, -1); - ClosedTab closedTab = getClosedTabForPosition(position); - return populateChildInfoFromTab(info, closedTab); - } - - return null; - } - - protected static HomeContextMenuInfo populateChildInfoFromTab(HomeContextMenuInfo info, ClosedTab tab) { - info.url = tab.url; - info.title = tab.title; - return info; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/RemoteTabsExpandableListState.java b/mobile/android/base/java/org/mozilla/gecko/home/RemoteTabsExpandableListState.java deleted file mode 100644 index 43497ae6ca..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/RemoteTabsExpandableListState.java +++ /dev/null @@ -1,163 +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 java.util.HashSet; -import java.util.Set; - -import org.mozilla.gecko.util.PrefUtils; - -import android.content.SharedPreferences; -import android.content.SharedPreferences.Editor; - -/** - * Encapsulate visual state maintained by the Remote Tabs home panel. - *

- * This state should persist across database updates by Sync and the like. This - * state could be stored in a separate "clients_metadata" table and served by - * the Tabs provider, but that is heavy-weight for what we want to achieve. Such - * a scheme would require either an expensive table join, or a tricky - * co-ordination between multiple cursors. In contrast, this is easy and cheap - * enough to do on the main thread. - *

- * This state is "per SharedPreferences" object. In practice, there should exist - * one state object per Gecko Profile; since we can't change profiles without - * killing our process, this can be a static singleton. - */ -public class RemoteTabsExpandableListState { - private static final String PREF_COLLAPSED_CLIENT_GUIDS = "remote_tabs_collapsed_client_guids"; - private static final String PREF_HIDDEN_CLIENT_GUIDS = "remote_tabs_hidden_client_guids"; - private static final String PREF_SELECTED_CLIENT_GUID = "remote_tabs_selected_client_guid"; - - protected final SharedPreferences sharedPrefs; - - // Synchronized by the state instance. The default is to expand a clients - // tabs, so "not present" means "expanded". - // Only accessed from the UI thread. - protected final Set collapsedClients; - - // Synchronized by the state instance. The default is to show a client, so - // "not present" means "shown". - // Only accessed from the UI thread. - protected final Set hiddenClients; - - // Synchronized by the state instance. The last user selected client guid. - // The selectedClient may be invalid or null. - protected String selectedClient; - - public RemoteTabsExpandableListState(SharedPreferences sharedPrefs) { - if (null == sharedPrefs) { - throw new IllegalArgumentException("sharedPrefs must not be null"); - } - this.sharedPrefs = sharedPrefs; - - this.collapsedClients = getStringSet(PREF_COLLAPSED_CLIENT_GUIDS); - this.hiddenClients = getStringSet(PREF_HIDDEN_CLIENT_GUIDS); - this.selectedClient = sharedPrefs.getString(PREF_SELECTED_CLIENT_GUID, null); - } - - /** - * Extract a string set from shared preferences. - *

- * Nota bene: it is not OK to modify the set returned by {@link SharedPreferences#getStringSet(String, Set)}. - * - * @param pref to read from. - * @returns string set; never null. - */ - protected Set getStringSet(String pref) { - final Set loaded = PrefUtils.getStringSet(sharedPrefs, pref, null); - if (loaded != null) { - return new HashSet(loaded); - } else { - return new HashSet(); - } - } - - /** - * Update client membership in a set. - * - * @param pref - * to write updated set to. - * @param clients - * set to update membership in. - * @param clientGuid - * to update membership of. - * @param isMember - * whether the client is a member of the set. - * @return true if the set of clients was modified. - */ - protected boolean updateClientMembership(String pref, Set clients, String clientGuid, boolean isMember) { - final boolean modified; - if (isMember) { - modified = clients.add(clientGuid); - } else { - modified = clients.remove(clientGuid); - } - - if (modified) { - // This starts an asynchronous write. We don't care if we drop the - // write, and we don't really care if we race between writes, since - // we will return results from our in-memory cache. - final Editor editor = sharedPrefs.edit(); - PrefUtils.putStringSet(editor, pref, clients); - editor.apply(); - } - - return modified; - } - - /** - * Mark a client as collapsed. - * - * @param clientGuid - * to update. - * @param collapsed - * whether the client is collapsed. - * @return true if the set of collapsed clients was modified. - */ - protected synchronized boolean setClientCollapsed(String clientGuid, boolean collapsed) { - return updateClientMembership(PREF_COLLAPSED_CLIENT_GUIDS, collapsedClients, clientGuid, collapsed); - } - - /** - * Mark a client as the selected. - * - * @param clientGuid - * to update. - */ - protected synchronized void setClientAsSelected(String clientGuid) { - if (hiddenClients.contains(clientGuid)) { - selectedClient = null; - } else { - selectedClient = clientGuid; - } - - final Editor editor = sharedPrefs.edit(); - editor.putString(PREF_SELECTED_CLIENT_GUID, selectedClient); - editor.apply(); - } - - public synchronized boolean isClientCollapsed(String clientGuid) { - return collapsedClients.contains(clientGuid); - } - - /** - * Mark a client as hidden. - * - * @param clientGuid - * to update. - * @param hidden - * whether the client is hidden. - * @return true if the set of hidden clients was modified. - */ - protected synchronized boolean setClientHidden(String clientGuid, boolean hidden) { - return updateClientMembership(PREF_HIDDEN_CLIENT_GUIDS, hiddenClients, clientGuid, hidden); - } - - public synchronized boolean isClientHidden(String clientGuid) { - return hiddenClients.contains(clientGuid); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/SearchEngine.java b/mobile/android/base/java/org/mozilla/gecko/home/SearchEngine.java deleted file mode 100644 index 9b2d2746a7..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/SearchEngine.java +++ /dev/null @@ -1,102 +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.support.annotation.NonNull; -import org.mozilla.gecko.gfx.BitmapUtils; -import org.mozilla.gecko.R; - -import org.json.JSONException; -import org.json.JSONObject; - -import android.content.Context; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.util.Log; - -import java.util.ArrayList; -import java.util.List; - -public class SearchEngine { - public static final String LOG_TAG = "GeckoSearchEngine"; - - public final String name; // Never null. - public final String identifier; // Can be null. - - private final Bitmap icon; - private volatile List suggestions = new ArrayList(); // Never null. - - public SearchEngine(final Context context, final JSONObject engineJSON) throws JSONException { - if (engineJSON == null) { - throw new IllegalArgumentException("Can't instantiate SearchEngine from null JSON."); - } - - this.name = getString(engineJSON, "name"); - if (this.name == null) { - throw new IllegalArgumentException("Cannot have an unnamed search engine."); - } - - this.identifier = getString(engineJSON, "identifier"); - - final String iconURI = getString(engineJSON, "iconURI"); - if (iconURI == null) { - Log.w(LOG_TAG, "iconURI is null for search engine " + this.name); - } - final Bitmap tempIcon = BitmapUtils.getBitmapFromDataURI(iconURI); - - this.icon = (tempIcon != null) ? tempIcon : getDefaultFavicon(context); - } - - private Bitmap getDefaultFavicon(final Context context) { - return BitmapFactory.decodeResource(context.getResources(), R.drawable.search_icon_inactive); - } - - private static String getString(JSONObject data, String key) throws JSONException { - if (data.isNull(key)) { - return null; - } - return data.getString(key); - } - - /** - * @return a non-null string suitable for use by FHR. - */ - @NonNull - public String getEngineIdentifier() { - if (this.identifier != null) { - return this.identifier; - } - if (this.name != null) { - return "other-" + this.name; - } - return "other"; - } - - public boolean hasSuggestions() { - return !this.suggestions.isEmpty(); - } - - public int getSuggestionsCount() { - return this.suggestions.size(); - } - - public Iterable getSuggestions() { - return this.suggestions; - } - - public void setSuggestions(List suggestions) { - if (suggestions == null) { - this.suggestions = new ArrayList(); - return; - } - this.suggestions = suggestions; - } - - public Bitmap getIcon() { - return this.icon; - } -} - diff --git a/mobile/android/base/java/org/mozilla/gecko/home/SearchEngineAdapter.java b/mobile/android/base/java/org/mozilla/gecko/home/SearchEngineAdapter.java deleted file mode 100644 index be5b3b461f..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/SearchEngineAdapter.java +++ /dev/null @@ -1,122 +0,0 @@ -package org.mozilla.gecko.home; - -import android.content.Context; -import android.graphics.drawable.Drawable; -import android.support.v4.content.ContextCompat; -import android.support.v4.graphics.drawable.DrawableCompat; -import android.support.v7.widget.RecyclerView; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ImageView; - -import org.mozilla.gecko.R; - -import java.util.Collections; -import java.util.List; - -public class SearchEngineAdapter - extends RecyclerView.Adapter { - - private static final String LOGTAG = SearchEngineAdapter.class.getSimpleName(); - - private static final int VIEW_TYPE_SEARCH_ENGINE = 0; - private static final int VIEW_TYPE_LABEL = 1; - private final Context mContext; - - private int mContainerWidth; - private List mSearchEngines = Collections.emptyList(); - - public void setSearchEngines(List searchEngines) { - mSearchEngines = searchEngines; - notifyDataSetChanged(); - } - - /** - * The container width is used for setting the appropriate calculated amount of width that - * a search engine icon can have. This varies depending on the space available in the - * {@link SearchEngineBar}. The setter exists for this attribute, in creating the view in the - * adapter after said calculation is done when the search bar is created. - * @param iconContainerWidth Width of each search icon. - */ - void setIconContainerWidth(int iconContainerWidth) { - mContainerWidth = iconContainerWidth; - } - - public static class SearchEngineViewHolder extends RecyclerView.ViewHolder { - final private ImageView faviconView; - - public void bindItem(SearchEngine searchEngine) { - faviconView.setImageBitmap(searchEngine.getIcon()); - final String desc = itemView.getResources().getString(R.string.search_bar_item_desc, - searchEngine.getEngineIdentifier()); - itemView.setContentDescription(desc); - } - - public SearchEngineViewHolder(View itemView) { - super(itemView); - faviconView = (ImageView) itemView.findViewById(R.id.search_engine_icon); - } - } - - public SearchEngineAdapter(Context context) { - mContext = context; - } - - @Override - public int getItemViewType(int position) { - return position == 0 ? VIEW_TYPE_LABEL : VIEW_TYPE_SEARCH_ENGINE; - } - - public SearchEngine getItem(int position) { - // We omit the first position which is where the label currently is. - return position == 0 ? null : mSearchEngines.get(position - 1); - } - - @Override - public SearchEngineViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { - switch (viewType) { - case VIEW_TYPE_LABEL: - return new SearchEngineViewHolder(createLabelView(parent)); - case VIEW_TYPE_SEARCH_ENGINE: - return new SearchEngineViewHolder(createSearchEngineView(parent)); - default: - throw new IllegalArgumentException("Unknown view type: " + viewType); - } - } - - @Override - public void onBindViewHolder(SearchEngineViewHolder holder, int position) { - if (position != 0) { - holder.bindItem(getItem(position)); - } - } - - @Override - public int getItemCount() { - return mSearchEngines.size() + 1; - } - - private View createLabelView(ViewGroup parent) { - View view = LayoutInflater.from(mContext) - .inflate(R.layout.search_engine_bar_label, parent, false); - final Drawable icon = DrawableCompat.wrap( - ContextCompat.getDrawable(mContext, R.drawable.search_icon_active).mutate()); - DrawableCompat.setTint(icon, ContextCompat.getColor(mContext, R.color.disabled_grey)); - - final ImageView iconView = (ImageView) view.findViewById(R.id.search_engine_label); - iconView.setImageDrawable(icon); - return view; - } - - private View createSearchEngineView(ViewGroup parent) { - View view = LayoutInflater.from(mContext) - .inflate(R.layout.search_engine_bar_item, parent, false); - - ViewGroup.LayoutParams params = view.getLayoutParams(); - params.width = mContainerWidth; - view.setLayoutParams(params); - - return view; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/SearchEngineBar.java b/mobile/android/base/java/org/mozilla/gecko/home/SearchEngineBar.java deleted file mode 100644 index 6a6509bcbe..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/SearchEngineBar.java +++ /dev/null @@ -1,148 +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.content.Intent; -import android.graphics.Canvas; -import android.graphics.Paint; -import android.support.v4.content.ContextCompat; -import android.support.v7.widget.LinearLayoutManager; -import android.support.v7.widget.RecyclerView; -import android.util.AttributeSet; -import android.util.DisplayMetrics; -import android.util.TypedValue; -import android.view.View; - -import org.mozilla.gecko.annotation.RobocopTarget; -import org.mozilla.gecko.R; -import org.mozilla.gecko.Telemetry; -import org.mozilla.gecko.TelemetryContract; -import org.mozilla.gecko.preferences.GeckoPreferences; -import org.mozilla.gecko.widget.RecyclerViewClickSupport; - -import java.util.List; - -public class SearchEngineBar extends RecyclerView - implements RecyclerViewClickSupport.OnItemClickListener { - private static final String LOGTAG = SearchEngineBar.class.getSimpleName(); - - private static final float ICON_CONTAINER_MIN_WIDTH_DP = 72; - private static final float LABEL_CONTAINER_WIDTH_DP = 48; - - public interface OnSearchBarClickListener { - void onSearchBarClickListener(SearchEngine searchEngine); - } - - private final SearchEngineAdapter mAdapter; - private final LinearLayoutManager mLayoutManager; - private final Paint mDividerPaint; - private final float mMinIconContainerWidth; - private final float mDividerHeight; - private final int mLabelContainerWidth; - - private int mIconContainerWidth; - private OnSearchBarClickListener mOnSearchBarClickListener; - - public SearchEngineBar(final Context context, final AttributeSet attrs) { - super(context, attrs); - - mDividerPaint = new Paint(); - mDividerPaint.setColor(ContextCompat.getColor(context, R.color.toolbar_divider_grey)); - mDividerPaint.setStyle(Paint.Style.FILL_AND_STROKE); - - final DisplayMetrics displayMetrics = getResources().getDisplayMetrics(); - mMinIconContainerWidth = TypedValue.applyDimension( - TypedValue.COMPLEX_UNIT_DIP, ICON_CONTAINER_MIN_WIDTH_DP, displayMetrics); - mDividerHeight = context.getResources().getDimension(R.dimen.page_row_divider_height); - mLabelContainerWidth = Math.round(TypedValue.applyDimension( - TypedValue.COMPLEX_UNIT_DIP, LABEL_CONTAINER_WIDTH_DP, displayMetrics)); - - mIconContainerWidth = Math.round(mMinIconContainerWidth); - - mAdapter = new SearchEngineAdapter(context); - mAdapter.setIconContainerWidth(mIconContainerWidth); - mLayoutManager = new LinearLayoutManager(context); - mLayoutManager.setOrientation(LinearLayoutManager.HORIZONTAL); - - setAdapter(mAdapter); - setLayoutManager(mLayoutManager); - - RecyclerViewClickSupport.addTo(this) - .setOnItemClickListener(this); - } - - public void setSearchEngines(List searchEngines) { - mAdapter.setSearchEngines(searchEngines); - } - - public void setOnSearchBarClickListener(OnSearchBarClickListener listener) { - mOnSearchBarClickListener = listener; - } - - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - super.onMeasure(widthMeasureSpec, heightMeasureSpec); - - final int searchEngineCount = mAdapter.getItemCount() - 1; - - if (searchEngineCount > 0) { - final int availableWidth = getMeasuredWidth() - mLabelContainerWidth; - - if (searchEngineCount * mMinIconContainerWidth <= availableWidth) { - // All search engines fit int: So let's just display all. - mIconContainerWidth = (int) mMinIconContainerWidth; - } else { - // If only (n) search engines fit into the available space then display only (x) - // search engines with (x) picked so that the last search engine will be cut-off - // (we only display half of it) to show the ability to scroll this view. - - final double searchEnginesToDisplay = Math.floor((availableWidth / mMinIconContainerWidth) - 0.5) + 0.5; - // Use all available width and spread search engine icons - mIconContainerWidth = (int) (availableWidth / searchEnginesToDisplay); - } - - mAdapter.setIconContainerWidth(mIconContainerWidth); - } - } - - @Override - public void onDraw(Canvas canvas) { - super.onDraw(canvas); - - canvas.drawRect(0, 0, getWidth(), mDividerHeight, mDividerPaint); - } - - @Override - public void onItemClicked(RecyclerView recyclerView, int position, View v) { - if (mOnSearchBarClickListener == null) { - throw new IllegalStateException( - OnSearchBarClickListener.class.getSimpleName() + " is not initializer." - ); - } - - if (position == 0) { - final Intent settingsIntent = new Intent(getContext(), GeckoPreferences.class); - GeckoPreferences.setResourceToOpen(settingsIntent, "preferences_search"); - getContext().startActivity(settingsIntent); - Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.BUTTON, "searchenginebar-settings"); - return; - } - - final SearchEngine searchEngine = mAdapter.getItem(position); - mOnSearchBarClickListener.onSearchBarClickListener(searchEngine); - } - - /** - * We manually add the override for getAdapter because we see this method getting stripped - * out during compile time by aggressive proguard rules. - */ - @RobocopTarget - @Override - public SearchEngineAdapter getAdapter() { - return mAdapter; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/SearchEngineRow.java b/mobile/android/base/java/org/mozilla/gecko/home/SearchEngineRow.java deleted file mode 100644 index 5b97a8f5f7..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/SearchEngineRow.java +++ /dev/null @@ -1,494 +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.GeckoSharedPrefs; -import org.mozilla.gecko.R; -import org.mozilla.gecko.Telemetry; -import org.mozilla.gecko.TelemetryContract; -import org.mozilla.gecko.home.BrowserSearch.OnEditSuggestionListener; -import org.mozilla.gecko.home.BrowserSearch.OnSearchListener; -import org.mozilla.gecko.home.HomePager.OnUrlOpenListener; -import org.mozilla.gecko.icons.IconResponse; -import org.mozilla.gecko.preferences.GeckoPreferences; -import org.mozilla.gecko.util.DrawableUtil; -import org.mozilla.gecko.util.StringUtils; -import org.mozilla.gecko.util.HardwareUtils; -import org.mozilla.gecko.widget.AnimatedHeightLayout; -import org.mozilla.gecko.widget.FaviconView; -import org.mozilla.gecko.widget.FlowLayout; - -import android.content.Context; -import android.content.SharedPreferences; -import android.graphics.drawable.Drawable; -import android.graphics.Typeface; -import android.support.annotation.Nullable; -import android.text.TextUtils; -import android.text.style.StyleSpan; -import android.text.Spannable; -import android.text.SpannableStringBuilder; -import android.util.AttributeSet; -import android.view.KeyEvent; -import android.view.LayoutInflater; -import android.view.View; -import android.view.animation.AlphaAnimation; -import android.widget.ImageView; -import android.widget.LinearLayout; -import android.widget.TextView; - -import java.util.ArrayList; -import java.util.EnumSet; -import java.util.Iterator; -import java.util.List; -import java.util.regex.Pattern; - -class SearchEngineRow extends AnimatedHeightLayout { - // Duration for fade-in animation - private static final int ANIMATION_DURATION = 250; - - // Inner views - private final FlowLayout mSuggestionView; - private final FaviconView mIconView; - private final LinearLayout mUserEnteredView; - private final TextView mUserEnteredTextView; - - // Inflater used when updating from suggestions - private final LayoutInflater mInflater; - - // Search engine associated with this view - private SearchEngine mSearchEngine; - - // Event listeners for suggestion views - private final OnClickListener mClickListener; - private final OnLongClickListener mLongClickListener; - - // On URL open listener - private OnUrlOpenListener mUrlOpenListener; - - // On search listener - private OnSearchListener mSearchListener; - - // On edit suggestion listener - private OnEditSuggestionListener mEditSuggestionListener; - - // Selected suggestion view - private int mSelectedView; - - // android:backgroundTint only works in Android 21 and higher so we can't do this statically in the xml - private Drawable mSearchHistorySuggestionIcon; - - // Maximums for suggestions - private int mMaxSavedSuggestions; - private int mMaxSearchSuggestions; - - private final List mOccurrences = new ArrayList(); - - public SearchEngineRow(Context context) { - this(context, null); - } - - public SearchEngineRow(Context context, AttributeSet attrs) { - this(context, attrs, 0); - } - - public SearchEngineRow(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - - mClickListener = new OnClickListener() { - @Override - public void onClick(View v) { - final String suggestion = getSuggestionTextFromView(v); - - // If we're not clicking the user-entered view (the first suggestion item) - // and the search matches a URL pattern, go to that URL. Otherwise, do a - // search for the term. - if (v != mUserEnteredView && !StringUtils.isSearchQuery(suggestion, true)) { - if (mUrlOpenListener != null) { - Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.SUGGESTION, "url"); - - mUrlOpenListener.onUrlOpen(suggestion, EnumSet.noneOf(OnUrlOpenListener.Flags.class)); - } - } else if (mSearchListener != null) { - if (v == mUserEnteredView) { - Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.SUGGESTION, "user"); - } else { - Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.SUGGESTION, (String) v.getTag()); - } - mSearchListener.onSearch(mSearchEngine, suggestion, TelemetryContract.Method.SUGGESTION); - } - } - }; - - mLongClickListener = new OnLongClickListener() { - @Override - public boolean onLongClick(View v) { - if (mEditSuggestionListener != null) { - final String suggestion = getSuggestionTextFromView(v); - mEditSuggestionListener.onEditSuggestion(suggestion); - return true; - } - - return false; - } - }; - - mInflater = LayoutInflater.from(context); - mInflater.inflate(R.layout.search_engine_row, this); - - mSuggestionView = (FlowLayout) findViewById(R.id.suggestion_layout); - mIconView = (FaviconView) findViewById(R.id.suggestion_icon); - - // User-entered search term is first suggestion - mUserEnteredView = (LinearLayout) findViewById(R.id.suggestion_user_entered); - mUserEnteredView.setOnClickListener(mClickListener); - - mUserEnteredTextView = (TextView) findViewById(R.id.suggestion_text); - mSearchHistorySuggestionIcon = DrawableUtil.tintDrawableWithColorRes(getContext(), R.drawable.icon_most_recent_empty, R.color.tabs_tray_icon_grey); - - // Suggestion limits - mMaxSavedSuggestions = getResources().getInteger(R.integer.max_saved_suggestions); - mMaxSearchSuggestions = getResources().getInteger(R.integer.max_search_suggestions); - } - - private void setDescriptionOnSuggestion(View v, String suggestion) { - v.setContentDescription(getResources().getString(R.string.suggestion_for_engine, - mSearchEngine.name, suggestion)); - } - - private String getSuggestionTextFromView(View v) { - final TextView suggestionText = (TextView) v.findViewById(R.id.suggestion_text); - return suggestionText.getText().toString(); - } - - /** - * Finds all occurrences of pattern in string and returns a list of the starting indices - * of each occurrence. - * - * @param pattern The pattern that is searched for - * @param string The string where we search for the pattern - */ - private void refreshOccurrencesWith(String pattern, String string) { - mOccurrences.clear(); - - // Don't try to search for an empty string - String.indexOf will return 0, which would result - // in us iterating with lastIndexOfMatch = 0, which eventually results in an OOM. - if (TextUtils.isEmpty(pattern)) { - return; - } - - final int patternLength = pattern.length(); - - int indexOfMatch = 0; - int lastIndexOfMatch = 0; - while (indexOfMatch != -1) { - indexOfMatch = string.indexOf(pattern, lastIndexOfMatch); - lastIndexOfMatch = indexOfMatch + patternLength; - if (indexOfMatch != -1) { - mOccurrences.add(indexOfMatch); - } - } - } - - /** - * Sets the content for the suggestion view. - * - * If the suggestion doesn't contain mUserSearchTerm, nothing is made bold. - * All instances of mUserSearchTerm in the suggestion are not bold. - * - * @param v The View that needs to be populated - * @param suggestion The suggestion text that will be placed in the view - * @param isUserSavedSearch whether the suggestion is from history or not - */ - private void setSuggestionOnView(View v, String suggestion, boolean isUserSavedSearch) { - final ImageView historyIcon = (ImageView) v.findViewById(R.id.suggestion_item_icon); - if (isUserSavedSearch) { - historyIcon.setImageDrawable(mSearchHistorySuggestionIcon); - historyIcon.setVisibility(View.VISIBLE); - } else { - historyIcon.setVisibility(View.GONE); - } - - final TextView suggestionText = (TextView) v.findViewById(R.id.suggestion_text); - final String searchTerm = getSuggestionTextFromView(mUserEnteredView); - final int searchTermLength = searchTerm.length(); - refreshOccurrencesWith(searchTerm, suggestion); - if (mOccurrences.size() > 0) { - final SpannableStringBuilder sb = new SpannableStringBuilder(suggestion); - int nextStartSpanIndex = 0; - // Done to make sure that the stretch of text after the last occurrence, till the end of the suggestion, is made bold - mOccurrences.add(suggestion.length()); - for (int occurrence : mOccurrences) { - // Even though they're the same style, SpannableStringBuilder will interpret there as being only one Span present if we re-use a StyleSpan - StyleSpan boldSpan = new StyleSpan(Typeface.BOLD); - sb.setSpan(boldSpan, nextStartSpanIndex, occurrence, Spannable.SPAN_INCLUSIVE_INCLUSIVE); - nextStartSpanIndex = occurrence + searchTermLength; - } - mOccurrences.clear(); - suggestionText.setText(sb); - } else { - suggestionText.setText(suggestion); - } - - setDescriptionOnSuggestion(suggestionText, suggestion); - } - - /** - * Perform a search for the user-entered term. - */ - public void performUserEnteredSearch() { - String searchTerm = getSuggestionTextFromView(mUserEnteredView); - if (mSearchListener != null) { - Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.SUGGESTION, "user"); - mSearchListener.onSearch(mSearchEngine, searchTerm, TelemetryContract.Method.SUGGESTION); - } - } - - public void setSearchTerm(String searchTerm) { - mUserEnteredTextView.setText(searchTerm); - - // mSearchEngine is not set in the first call to this method; the content description - // is instead initially set in updateSuggestions(). - if (mSearchEngine != null) { - setDescriptionOnSuggestion(mUserEnteredTextView, searchTerm); - } - } - - public void setOnUrlOpenListener(OnUrlOpenListener listener) { - mUrlOpenListener = listener; - } - - public void setOnSearchListener(OnSearchListener listener) { - mSearchListener = listener; - } - - public void setOnEditSuggestionListener(OnEditSuggestionListener listener) { - mEditSuggestionListener = listener; - } - - private void bindSuggestionView(String suggestion, boolean animate, int recycledSuggestionCount, Integer previousSuggestionChildIndex, boolean isUserSavedSearch, String telemetryTag) { - final View suggestionItem; - - // Reuse suggestion views from recycled view, if possible. - if (previousSuggestionChildIndex + 1 < recycledSuggestionCount) { - suggestionItem = mSuggestionView.getChildAt(previousSuggestionChildIndex + 1); - suggestionItem.setVisibility(View.VISIBLE); - } else { - suggestionItem = mInflater.inflate(R.layout.suggestion_item, null); - - suggestionItem.setOnClickListener(mClickListener); - suggestionItem.setOnLongClickListener(mLongClickListener); - - suggestionItem.setTag(telemetryTag); - - mSuggestionView.addView(suggestionItem); - } - - setSuggestionOnView(suggestionItem, suggestion, isUserSavedSearch); - - if (animate) { - AlphaAnimation anim = new AlphaAnimation(0, 1); - anim.setDuration(ANIMATION_DURATION); - anim.setStartOffset(previousSuggestionChildIndex * ANIMATION_DURATION); - suggestionItem.startAnimation(anim); - } - } - - private void hideRecycledSuggestions(int lastVisibleChildIndex, int recycledSuggestionCount) { - // Hide extra suggestions that have been recycled. - for (int i = lastVisibleChildIndex + 1; i < recycledSuggestionCount; ++i) { - mSuggestionView.getChildAt(i).setVisibility(View.GONE); - } - } - - /** - * Displays search suggestions from previous searches. - * - * @param savedSuggestions The List to iterate over for saved search suggestions to display. This function does not - * enforce a ui maximum or filter. It will show all the suggestions in this list. - * @param suggestionStartIndex global index of where to start adding suggestion "buttons" in the search engine row. Also - * acts as a counter for total number of suggestions visible. - * @param animate whether or not to animate suggestions for visual polish - * @param recycledSuggestionCount How many suggestion "button" views we could recycle from previous calls - */ - private void updateFromSavedSearches(List savedSuggestions, boolean animate, int suggestionStartIndex, int recycledSuggestionCount) { - if (savedSuggestions == null || savedSuggestions.isEmpty()) { - hideRecycledSuggestions(suggestionStartIndex, recycledSuggestionCount); - return; - } - - final int numSavedSearches = savedSuggestions.size(); - int indexOfPreviousSuggestion = 0; - for (int i = 0; i < numSavedSearches; i++) { - String telemetryTag = "history." + i; - final String suggestion = savedSuggestions.get(i); - indexOfPreviousSuggestion = suggestionStartIndex + i; - bindSuggestionView(suggestion, animate, recycledSuggestionCount, indexOfPreviousSuggestion, true, telemetryTag); - } - - hideRecycledSuggestions(indexOfPreviousSuggestion + 1, recycledSuggestionCount); - } - - /** - * Displays suggestions supplied by the search engine, relative to number of suggestions from search history. - * - * @param animate whether or not to animate suggestions for visual polish - * @param recycledSuggestionCount How many suggestion "button" views we could recycle from previous calls - * @param savedSuggestionCount how many saved searches this searchTerm has - * @return the global count of how many suggestions have been bound/shown in the search engine row - */ - private int updateFromSearchEngine(boolean animate, List searchEngineSuggestions, int recycledSuggestionCount, int savedSuggestionCount) { - int maxSuggestions = mMaxSearchSuggestions; - // If there are less than max saved searches on phones, fill the space with more search engine suggestions - if (!HardwareUtils.isTablet() && savedSuggestionCount < mMaxSavedSuggestions) { - maxSuggestions += mMaxSavedSuggestions - savedSuggestionCount; - } - - final int numSearchEngineSuggestions = searchEngineSuggestions.size(); - int relativeIndex; - for (relativeIndex = 0; relativeIndex < numSearchEngineSuggestions; relativeIndex++) { - if (relativeIndex == maxSuggestions) { - break; - } - - // Since the search engine suggestions are listed first, their relative index is their global index - String telemetryTag = "engine." + relativeIndex; - final String suggestion = searchEngineSuggestions.get(relativeIndex); - bindSuggestionView(suggestion, animate, recycledSuggestionCount, relativeIndex, false, telemetryTag); - } - - hideRecycledSuggestions(relativeIndex + 1, recycledSuggestionCount); - - // Make sure mSelectedView is still valid. - if (mSelectedView >= mSuggestionView.getChildCount()) { - mSelectedView = mSuggestionView.getChildCount() - 1; - } - - return relativeIndex; - } - - /** - * Updates the whole suggestions UI, the search engine UI, suggestions from the default search engine, - * and suggestions from search history. - * - * This can be called before the opt-in permission prompt is shown or set. - * Even if both suggestion types are disabled, we need to update the search engine, its image, and the content description. - * - * @param searchSuggestionsEnabled whether or not suggestions from the default search engine are enabled - * @param searchEngine the search engine to use throughout the SearchEngineRow class - * @param rawSearchHistorySuggestions search history suggestions - * @param animate whether or not to use animations - **/ - public void updateSuggestions(boolean searchSuggestionsEnabled, SearchEngine searchEngine, @Nullable List rawSearchHistorySuggestions, boolean animate) { - mSearchEngine = searchEngine; - // Set the search engine icon (e.g., Google) for the row. - - mIconView.updateAndScaleImage(IconResponse.create(mSearchEngine.getIcon())); - // Set the initial content description. - setDescriptionOnSuggestion(mUserEnteredTextView, mUserEnteredTextView.getText().toString()); - - final int recycledSuggestionCount = mSuggestionView.getChildCount(); - final SharedPreferences prefs = GeckoSharedPrefs.forApp(getContext()); - final boolean savedSearchesEnabled = prefs.getBoolean(GeckoPreferences.PREFS_HISTORY_SAVED_SEARCH, true); - - // Remove duplicates of search engine suggestions from saved searches. - List searchHistorySuggestions = (rawSearchHistorySuggestions != null) ? rawSearchHistorySuggestions : new ArrayList(); - - // Filter out URLs and long search suggestions - Iterator searchHistoryIterator = searchHistorySuggestions.iterator(); - while (searchHistoryIterator.hasNext()) { - final String currentSearchHistory = searchHistoryIterator.next(); - - if (currentSearchHistory.length() > 50 || Pattern.matches("^(https?|ftp|file)://.*", currentSearchHistory)) { - searchHistoryIterator.remove(); - } - } - - - List searchEngineSuggestions = new ArrayList(); - for (String suggestion : searchEngine.getSuggestions()) { - searchHistorySuggestions.remove(suggestion); - searchEngineSuggestions.add(suggestion); - } - // Make sure the search term itself isn't duplicated. This is more important on phones than tablets where screen - // space is more precious. - searchHistorySuggestions.remove(getSuggestionTextFromView(mUserEnteredView)); - - // Trim the history suggestions down to the maximum allowed. - if (searchHistorySuggestions.size() >= mMaxSavedSuggestions) { - // The second index to subList() is exclusive, so this looks like an off by one error but it is not. - searchHistorySuggestions = searchHistorySuggestions.subList(0, mMaxSavedSuggestions); - } - final int searchHistoryCount = searchHistorySuggestions.size(); - - if (searchSuggestionsEnabled && savedSearchesEnabled) { - final int suggestionViewCount = updateFromSearchEngine(animate, searchEngineSuggestions, recycledSuggestionCount, searchHistoryCount); - updateFromSavedSearches(searchHistorySuggestions, animate, suggestionViewCount, recycledSuggestionCount); - } else if (savedSearchesEnabled) { - updateFromSavedSearches(searchHistorySuggestions, animate, 0, recycledSuggestionCount); - } else if (searchSuggestionsEnabled) { - updateFromSearchEngine(animate, searchEngineSuggestions, recycledSuggestionCount, 0); - } else { - // The current search term is treated separately from the suggestions list, hence we can - // recycle ALL suggestion items here. (We always show the current search term, i.e. 1 item, - // in front of the search engine suggestions and/or the search history.) - hideRecycledSuggestions(0, recycledSuggestionCount); - } - } - - @Override - public boolean onKeyDown(int keyCode, android.view.KeyEvent event) { - final View suggestion = mSuggestionView.getChildAt(mSelectedView); - - if (event.getAction() != android.view.KeyEvent.ACTION_DOWN) { - return false; - } - - switch (event.getKeyCode()) { - case KeyEvent.KEYCODE_DPAD_RIGHT: - final View nextSuggestion = mSuggestionView.getChildAt(mSelectedView + 1); - if (nextSuggestion != null) { - changeSelectedSuggestion(suggestion, nextSuggestion); - mSelectedView++; - return true; - } - break; - - case KeyEvent.KEYCODE_DPAD_LEFT: - final View prevSuggestion = mSuggestionView.getChildAt(mSelectedView - 1); - if (prevSuggestion != null) { - changeSelectedSuggestion(suggestion, prevSuggestion); - mSelectedView--; - return true; - } - break; - - case KeyEvent.KEYCODE_BUTTON_A: - // TODO: handle long pressing for editing suggestions - return suggestion.performClick(); - } - - return false; - } - - private void changeSelectedSuggestion(View oldSuggestion, View newSuggestion) { - oldSuggestion.setDuplicateParentStateEnabled(false); - newSuggestion.setDuplicateParentStateEnabled(true); - oldSuggestion.refreshDrawableState(); - newSuggestion.refreshDrawableState(); - } - - public void onSelected() { - mSelectedView = 0; - mUserEnteredView.setDuplicateParentStateEnabled(true); - mUserEnteredView.refreshDrawableState(); - } - - public void onDeselected() { - final View suggestion = mSuggestionView.getChildAt(mSelectedView); - suggestion.setDuplicateParentStateEnabled(false); - suggestion.refreshDrawableState(); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/SearchLoader.java b/mobile/android/base/java/org/mozilla/gecko/home/SearchLoader.java deleted file mode 100644 index f7b5b65866..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/SearchLoader.java +++ /dev/null @@ -1,114 +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 java.util.EnumSet; - -import org.mozilla.gecko.GeckoProfile; -import org.mozilla.gecko.Telemetry; -import org.mozilla.gecko.db.BrowserDB; -import org.mozilla.gecko.db.BrowserDB.FilterFlags; - -import android.content.Context; -import android.database.Cursor; -import android.os.Bundle; -import android.os.SystemClock; -import android.support.v4.app.LoaderManager; -import android.support.v4.app.LoaderManager.LoaderCallbacks; -import android.support.v4.content.Loader; - -/** - * Encapsulates the implementation of the search cursor loader. - */ -class SearchLoader { - public static final String LOGTAG = "GeckoSearchLoader"; - - private static final String KEY_SEARCH_TERM = "search_term"; - private static final String KEY_FILTER_FLAGS = "flags"; - - private SearchLoader() { - } - - @SuppressWarnings("unchecked") - public static Loader createInstance(Context context, Bundle args) { - if (args != null) { - final String searchTerm = args.getString(KEY_SEARCH_TERM); - final EnumSet flags = - (EnumSet) args.getSerializable(KEY_FILTER_FLAGS); - return new SearchCursorLoader(context, searchTerm, flags); - } else { - return new SearchCursorLoader(context, "", EnumSet.noneOf(FilterFlags.class)); - } - } - - private static Bundle createArgs(String searchTerm, EnumSet flags) { - Bundle args = new Bundle(); - args.putString(SearchLoader.KEY_SEARCH_TERM, searchTerm); - args.putSerializable(SearchLoader.KEY_FILTER_FLAGS, flags); - - return args; - } - - public static void init(LoaderManager manager, int loaderId, - LoaderCallbacks callbacks, String searchTerm) { - init(manager, loaderId, callbacks, searchTerm, EnumSet.noneOf(FilterFlags.class)); - } - - public static void init(LoaderManager manager, int loaderId, - LoaderCallbacks callbacks, String searchTerm, - EnumSet flags) { - final Bundle args = createArgs(searchTerm, flags); - manager.initLoader(loaderId, args, callbacks); - } - - public static void restart(LoaderManager manager, int loaderId, - LoaderCallbacks callbacks, String searchTerm) { - restart(manager, loaderId, callbacks, searchTerm, EnumSet.noneOf(FilterFlags.class)); - } - - public static void restart(LoaderManager manager, int loaderId, - LoaderCallbacks callbacks, String searchTerm, - EnumSet flags) { - final Bundle args = createArgs(searchTerm, flags); - manager.restartLoader(loaderId, args, callbacks); - } - - public static class SearchCursorLoader extends SimpleCursorLoader { - private static final String TELEMETRY_HISTOGRAM_LOAD_CURSOR = "FENNEC_SEARCH_LOADER_TIME_MS"; - - // Max number of search results. - private static final int SEARCH_LIMIT = 100; - - // The target search term associated with the loader. - private final String mSearchTerm; - - // The filter flags associated with the loader. - private final EnumSet mFlags; - private final GeckoProfile mProfile; - - public SearchCursorLoader(Context context, String searchTerm, EnumSet flags) { - super(context); - mSearchTerm = searchTerm; - mFlags = flags; - mProfile = GeckoProfile.get(context); - } - - @Override - public Cursor loadCursor() { - final long start = SystemClock.uptimeMillis(); - final Cursor cursor = BrowserDB.from(mProfile).filter(getContext().getContentResolver(), mSearchTerm, SEARCH_LIMIT, mFlags); - final long end = SystemClock.uptimeMillis(); - final long took = end - start; - Telemetry.addToHistogram(TELEMETRY_HISTOGRAM_LOAD_CURSOR, (int) Math.min(took, Integer.MAX_VALUE)); - return cursor; - } - - public String getSearchTerm() { - return mSearchTerm; - } - } - -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/SimpleCursorLoader.java b/mobile/android/base/java/org/mozilla/gecko/home/SimpleCursorLoader.java deleted file mode 100644 index b8889c0331..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/SimpleCursorLoader.java +++ /dev/null @@ -1,147 +0,0 @@ -/* - * This is an adapted version of Android's original CursorLoader - * without all the ContentProvider-specific bits. - * - * Copyright (C) 2011 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.mozilla.gecko.home; - -import android.content.Context; -import android.database.Cursor; -import android.support.v4.content.AsyncTaskLoader; - -import org.mozilla.gecko.GeckoApplication; - -/** - * A copy of the framework's {@link android.content.CursorLoader} that - * instead allows the caller to load the Cursor themselves via the abstract - * {@link #loadCursor()} method, rather than calling out to a ContentProvider via - * class methods. - * - * For new code, prefer {@link android.content.CursorLoader} (see @deprecated). - * - * This was originally created to re-use existing code which loaded Cursors manually. - * - * @deprecated since the framework provides an implementation, we'd like to eventually remove - * this class to reduce maintenance burden. Originally planned for bug 1239491, but - * it'd be more efficient to do this over time, rather than all at once. - */ -@Deprecated -public abstract class SimpleCursorLoader extends AsyncTaskLoader { - final ForceLoadContentObserver mObserver; - Cursor mCursor; - - public SimpleCursorLoader(Context context) { - super(context); - mObserver = new ForceLoadContentObserver(); - } - - /** - * Loads the target cursor for this loader. This method is called - * on a worker thread. - */ - protected abstract Cursor loadCursor(); - - /* Runs on a worker thread */ - @Override - public Cursor loadInBackground() { - Cursor cursor = loadCursor(); - - if (cursor != null) { - // Ensure the cursor window is filled - cursor.getCount(); - cursor.registerContentObserver(mObserver); - } - - return cursor; - } - - /* Runs on the UI thread */ - @Override - public void deliverResult(Cursor cursor) { - if (isReset()) { - // An async query came in while the loader is stopped - if (cursor != null) { - cursor.close(); - } - - return; - } - - Cursor oldCursor = mCursor; - mCursor = cursor; - - if (isStarted()) { - super.deliverResult(cursor); - } - - if (oldCursor != null && oldCursor != cursor && !oldCursor.isClosed()) { - oldCursor.close(); - - // Trying to read from the closed cursor will cause crashes, hence we should make - // sure that no adapters/LoaderCallbacks are holding onto this cursor. - GeckoApplication.getRefWatcher(getContext()).watch(oldCursor); - } - } - - /** - * Starts an asynchronous load of the list data. When the result is ready the callbacks - * will be called on the UI thread. If a previous load has been completed and is still valid - * the result may be passed to the callbacks immediately. - * - * Must be called from the UI thread - */ - @Override - protected void onStartLoading() { - if (mCursor != null) { - deliverResult(mCursor); - } - - if (takeContentChanged() || mCursor == null) { - forceLoad(); - } - } - - /** - * Must be called from the UI thread - */ - @Override - protected void onStopLoading() { - // Attempt to cancel the current load task if possible. - cancelLoad(); - } - - @Override - public void onCanceled(Cursor cursor) { - if (cursor != null && !cursor.isClosed()) { - cursor.close(); - } - } - - @Override - protected void onReset() { - super.onReset(); - - // Ensure the loader is stopped - onStopLoading(); - - if (mCursor != null && !mCursor.isClosed()) { - mCursor.close(); - } - - mCursor = null; - } -} \ No newline at end of file diff --git a/mobile/android/base/java/org/mozilla/gecko/home/SpacingDecoration.java b/mobile/android/base/java/org/mozilla/gecko/home/SpacingDecoration.java deleted file mode 100644 index 039b65e829..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/SpacingDecoration.java +++ /dev/null @@ -1,20 +0,0 @@ -package org.mozilla.gecko.home; - -import android.graphics.Rect; -import android.support.v7.widget.RecyclerView; -import android.view.View; - -public class SpacingDecoration extends RecyclerView.ItemDecoration { - private final int horizontalSpacing; - private final int verticalSpacing; - - public SpacingDecoration(int horizontalSpacing, int verticalSpacing) { - this.horizontalSpacing = horizontalSpacing; - this.verticalSpacing = verticalSpacing; - } - - @Override - public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) { - outRect.set(horizontalSpacing, verticalSpacing, horizontalSpacing, verticalSpacing); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/TabMenuStrip.java b/mobile/android/base/java/org/mozilla/gecko/home/TabMenuStrip.java deleted file mode 100644 index b302d35225..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/TabMenuStrip.java +++ /dev/null @@ -1,127 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.support.v4.content.ContextCompat; -import org.mozilla.gecko.R; - -import android.content.Context; -import android.content.res.Resources; -import android.content.res.TypedArray; -import android.graphics.Canvas; -import android.graphics.Paint; -import android.graphics.drawable.Drawable; -import android.util.AttributeSet; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewTreeObserver; -import android.view.accessibility.AccessibilityEvent; -import android.widget.HorizontalScrollView; -import android.widget.LinearLayout; -import android.widget.TextView; - -/** - * {@code TabMenuStrip} is the view used to display {@code HomePager} tabs - * on tablets. See {@code TabMenuStripLayout} for details about how the - * tabs are created and updated. - */ -public class TabMenuStrip extends HorizontalScrollView - implements HomePager.Decor { - - // Offset between the selected tab title and the edge of the screen, - // except for the first and last tab in the tab strip. - private static final int TITLE_OFFSET_DIPS = 24; - - private final int titleOffset; - private final TabMenuStripLayout layout; - - private final Paint shadowPaint; - private final int shadowSize; - - public interface OnTitleClickListener { - void onTitleClicked(int index); - } - - public TabMenuStrip(Context context, AttributeSet attrs) { - super(context, attrs); - - // Disable the scroll bar. - setHorizontalScrollBarEnabled(false); - setFillViewport(true); - - final Resources res = getResources(); - - titleOffset = (int) (TITLE_OFFSET_DIPS * res.getDisplayMetrics().density); - - layout = new TabMenuStripLayout(context, attrs); - addView(layout, LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); - - shadowSize = res.getDimensionPixelSize(R.dimen.tabs_strip_shadow_size); - - shadowPaint = new Paint(); - shadowPaint.setColor(ContextCompat.getColor(context, R.color.url_bar_shadow)); - shadowPaint.setStrokeWidth(0.0f); - } - - @Override - public void draw(Canvas canvas) { - super.draw(canvas); - - final int height = getHeight(); - canvas.drawRect(0, height - shadowSize, layout.getWidth(), height, shadowPaint); - } - - @Override - public void onAddPagerView(String title) { - layout.onAddPagerView(title); - } - - @Override - public void removeAllPagerViews() { - layout.removeAllViews(); - } - - @Override - public void onPageSelected(final int position) { - layout.onPageSelected(position); - } - - @Override - public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { - layout.onPageScrolled(position, positionOffset, positionOffsetPixels); - - final View selectedTitle = layout.getChildAt(position); - if (selectedTitle == null) { - return; - } - - final int selectedTitleOffset = (int) (positionOffset * selectedTitle.getWidth()); - - int titleLeft = selectedTitle.getLeft() + selectedTitleOffset; - if (position > 0) { - titleLeft -= titleOffset; - } - - int titleRight = selectedTitle.getRight() + selectedTitleOffset; - if (position < layout.getChildCount() - 1) { - titleRight += titleOffset; - } - - final int scrollX = getScrollX(); - if (titleLeft < scrollX) { - // Tab strip overflows to the left. - scrollTo(titleLeft, 0); - } else if (titleRight > scrollX + getWidth()) { - // Tab strip overflows to the right. - scrollTo(titleRight - getWidth(), 0); - } - } - - @Override - public void setOnTitleClickListener(OnTitleClickListener onTitleClickListener) { - layout.setOnTitleClickListener(onTitleClickListener); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/TabMenuStripLayout.java b/mobile/android/base/java/org/mozilla/gecko/home/TabMenuStripLayout.java deleted file mode 100644 index a09add80b3..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/TabMenuStripLayout.java +++ /dev/null @@ -1,246 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.view.ViewGroup; -import android.widget.LinearLayout; - -import android.content.res.ColorStateList; -import org.mozilla.gecko.R; - -import android.content.Context; -import android.content.res.TypedArray; -import android.graphics.Canvas; -import android.graphics.drawable.Drawable; -import android.util.AttributeSet; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewTreeObserver; -import android.view.accessibility.AccessibilityEvent; -import android.widget.TextView; - -/** - * {@code TabMenuStripLayout} is the view that draws the {@code HomePager} - * tabs that are displayed in {@code TabMenuStrip}. - */ -class TabMenuStripLayout extends LinearLayout - implements View.OnFocusChangeListener { - - private TabMenuStrip.OnTitleClickListener onTitleClickListener; - private Drawable strip; - private TextView selectedView; - - // Data associated with the scrolling of the strip drawable. - private View toTab; - private View fromTab; - private int fromPosition; - private int toPosition; - private float progress; - - // This variable is used to predict the direction of scroll. - private float prevProgress; - private int tabContentStart; - private boolean titlebarFill; - private int activeTextColor; - private ColorStateList inactiveTextColor; - - TabMenuStripLayout(Context context, AttributeSet attrs) { - super(context, attrs); - - TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TabMenuStrip); - final int stripResId = a.getResourceId(R.styleable.TabMenuStrip_strip, -1); - - titlebarFill = a.getBoolean(R.styleable.TabMenuStrip_titlebarFill, false); - tabContentStart = a.getDimensionPixelSize(R.styleable.TabMenuStrip_tabsMarginLeft, 0); - activeTextColor = a.getColor(R.styleable.TabMenuStrip_activeTextColor, R.color.text_and_tabs_tray_grey); - inactiveTextColor = a.getColorStateList(R.styleable.TabMenuStrip_inactiveTextColor); - a.recycle(); - - if (stripResId != -1) { - strip = getResources().getDrawable(stripResId); - } - - setWillNotDraw(false); - } - - void onAddPagerView(String title) { - final TextView button = (TextView) LayoutInflater.from(getContext()).inflate(R.layout.tab_menu_strip, this, false); - button.setText(title.toUpperCase()); - button.setTextColor(inactiveTextColor); - - // Set titles width to weight, or wrap text width. - if (titlebarFill) { - button.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT, 1.0f)); - } else { - button.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT)); - } - - if (getChildCount() == 0) { - button.setPadding(button.getPaddingLeft() + tabContentStart, - button.getPaddingTop(), - button.getPaddingRight(), - button.getPaddingBottom()); - } - - addView(button); - button.setOnClickListener(new ViewClickListener(getChildCount() - 1)); - button.setOnFocusChangeListener(this); - } - - void onPageSelected(final int position) { - if (selectedView != null) { - selectedView.setTextColor(inactiveTextColor); - } - - selectedView = (TextView) getChildAt(position); - selectedView.setTextColor(activeTextColor); - - // Callback to measure and draw the strip after the view is visible. - ViewTreeObserver vto = selectedView.getViewTreeObserver(); - if (vto.isAlive()) { - vto.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { - @Override - public void onGlobalLayout() { - selectedView.getViewTreeObserver().removeGlobalOnLayoutListener(this); - - if (strip != null) { - strip.setBounds(selectedView.getLeft() + (position == 0 ? tabContentStart : 0), - selectedView.getTop(), - selectedView.getRight(), - selectedView.getBottom()); - } - - prevProgress = position; - } - }); - } - } - - // Page scroll animates the drawable and its bounds from the previous to next child view. - void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { - if (strip == null) { - return; - } - - setScrollingData(position, positionOffset); - - if (fromTab == null || toTab == null) { - return; - } - - final int fromTabLeft = fromTab.getLeft(); - final int fromTabRight = fromTab.getRight(); - - final int toTabLeft = toTab.getLeft(); - final int toTabRight = toTab.getRight(); - - // The first tab has a padding applied (tabContentStart). We don't want the 'strip' to jump around so we remove - // this padding slowly (modifier) when scrolling to or from the first tab. - final int modifier; - - if (fromPosition == 0 && toPosition == 1) { - // Slowly remove extra padding (tabContentStart) based on scroll progress - modifier = (int) (tabContentStart * (1 - progress)); - } else if (fromPosition == 1 && toPosition == 0) { - // Slowly add extra padding (tabContentStart) based on scroll progress - modifier = (int) (tabContentStart * progress); - } else { - // We are not scrolling tab 0 in any way, no modifier needed - modifier = 0; - } - - strip.setBounds((int) (fromTabLeft + ((toTabLeft - fromTabLeft) * progress)) + modifier, - 0, - (int) (fromTabRight + ((toTabRight - fromTabRight) * progress)), - getHeight()); - invalidate(); - } - - /* - * position + positionOffset goes from 0 to 2 as we scroll from page 1 to 3. - * Normalized progress is relative to the the direction the page is being scrolled towards. - * For this, we maintain direction of scroll with a state, and the child view we are moving towards and away from. - */ - void setScrollingData(int position, float positionOffset) { - if (position >= getChildCount() - 1) { - return; - } - - final float currProgress = position + positionOffset; - - if (prevProgress > currProgress) { - toPosition = position; - fromPosition = position + 1; - progress = 1 - positionOffset; - } else { - toPosition = position + 1; - fromPosition = position; - progress = positionOffset; - } - - toTab = getChildAt(toPosition); - fromTab = getChildAt(fromPosition); - - prevProgress = currProgress; - } - - @Override - public void onDraw(Canvas canvas) { - super.onDraw(canvas); - - if (strip != null) { - strip.draw(canvas); - } - } - - @Override - public void onFocusChange(View v, boolean hasFocus) { - if (v == this && hasFocus && getChildCount() > 0) { - selectedView.requestFocus(); - return; - } - - if (!hasFocus) { - return; - } - - int i = 0; - final int numTabs = getChildCount(); - - while (i < numTabs) { - View view = getChildAt(i); - if (view == v) { - view.requestFocus(); - if (isShown()) { - // A view is focused so send an event to announce the menu strip state. - sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED); - } - break; - } - - i++; - } - } - - void setOnTitleClickListener(TabMenuStrip.OnTitleClickListener onTitleClickListener) { - this.onTitleClickListener = onTitleClickListener; - } - - private class ViewClickListener implements OnClickListener { - private final int mIndex; - - public ViewClickListener(int index) { - mIndex = index; - } - - @Override - public void onClick(View view) { - if (onTitleClickListener != null) { - onTitleClickListener.onTitleClicked(mIndex); - } - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/TopSitesGridItemView.java b/mobile/android/base/java/org/mozilla/gecko/home/TopSitesGridItemView.java deleted file mode 100644 index c17aff209a..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/TopSitesGridItemView.java +++ /dev/null @@ -1,312 +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.graphics.Bitmap; -import android.text.TextUtils; -import android.util.AttributeSet; -import android.view.LayoutInflater; -import android.widget.ImageView.ScaleType; -import android.widget.RelativeLayout; -import android.widget.TextView; - -import org.mozilla.gecko.R; -import org.mozilla.gecko.db.BrowserContract.TopSites; -import org.mozilla.gecko.icons.IconCallback; -import org.mozilla.gecko.icons.IconResponse; -import org.mozilla.gecko.icons.Icons; - -import java.util.concurrent.Future; - -/** - * A view that displays the thumbnail and the title/url for a top/pinned site. - * If the title/url is longer than the width of the view, they are faded out. - * If there is no valid url, a default string is shown at 50% opacity. - * This is denoted by the empty state. - */ -public class TopSitesGridItemView extends RelativeLayout implements IconCallback { - private static final String LOGTAG = "GeckoTopSitesGridItemView"; - - // Empty state, to denote there is no valid url. - private static final int[] STATE_EMPTY = { android.R.attr.state_empty }; - - private static final ScaleType SCALE_TYPE_FAVICON = ScaleType.CENTER; - private static final ScaleType SCALE_TYPE_RESOURCE = ScaleType.CENTER; - private static final ScaleType SCALE_TYPE_THUMBNAIL = ScaleType.CENTER_CROP; - private static final ScaleType SCALE_TYPE_URL = ScaleType.CENTER_INSIDE; - - // Child views. - private final TextView mTitleView; - private final TopSitesThumbnailView mThumbnailView; - - // Data backing this view. - private String mTitle; - private String mUrl; - - private boolean mThumbnailSet; - - // Matches BrowserContract.TopSites row types - private int mType = -1; - - // Dirty state. - private boolean mIsDirty; - - private Future mOngoingIconRequest; - - public TopSitesGridItemView(Context context) { - this(context, null); - } - - public TopSitesGridItemView(Context context, AttributeSet attrs) { - this(context, attrs, R.attr.topSitesGridItemViewStyle); - } - - public TopSitesGridItemView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - - LayoutInflater.from(context).inflate(R.layout.top_sites_grid_item_view, this); - - mTitleView = (TextView) findViewById(R.id.title); - mThumbnailView = (TopSitesThumbnailView) findViewById(R.id.thumbnail); - } - - /** - * {@inheritDoc} - */ - @Override - public int[] onCreateDrawableState(int extraSpace) { - final int[] drawableState = super.onCreateDrawableState(extraSpace + 1); - - if (mType == TopSites.TYPE_BLANK) { - mergeDrawableStates(drawableState, STATE_EMPTY); - } - - return drawableState; - } - - /** - * @return The title shown by this view. - */ - public String getTitle() { - return (!TextUtils.isEmpty(mTitle) ? mTitle : mUrl); - } - - /** - * @return The url shown by this view. - */ - public String getUrl() { - return mUrl; - } - - /** - * @return The site type associated with this view. - */ - public int getType() { - return mType; - } - - /** - * @param title The title for this view. - */ - public void setTitle(String title) { - if (mTitle != null && mTitle.equals(title)) { - return; - } - - mTitle = title; - updateTitleView(); - } - - /** - * @param url The url for this view. - */ - public void setUrl(String url) { - if (mUrl != null && mUrl.equals(url)) { - return; - } - - mUrl = url; - updateTitleView(); - } - - public void blankOut() { - mUrl = ""; - mTitle = ""; - updateType(TopSites.TYPE_BLANK); - updateTitleView(); - cancelIconLoading(); - ImageLoader.with(getContext()).cancelRequest(mThumbnailView); - displayThumbnail(R.drawable.top_site_add); - - } - - public void markAsDirty() { - mIsDirty = true; - } - - /** - * Updates the title, URL, and pinned state of this view. - * - * Also resets our loadId to NOT_LOADING. - * - * Returns true if any fields changed. - */ - public boolean updateState(final String title, final String url, final int type, final TopSitesPanel.ThumbnailInfo thumbnail) { - boolean changed = false; - if (mUrl == null || !mUrl.equals(url)) { - mUrl = url; - changed = true; - } - - if (mTitle == null || !mTitle.equals(title)) { - mTitle = title; - changed = true; - } - - if (thumbnail != null) { - if (thumbnail.imageUrl != null) { - displayThumbnail(thumbnail.imageUrl, thumbnail.bgColor); - } else if (thumbnail.bitmap != null) { - displayThumbnail(thumbnail.bitmap); - } - } else if (changed) { - // Because we'll have a new favicon or thumbnail arriving shortly, and - // we need to not reject it because we already had a thumbnail. - mThumbnailSet = false; - } - - if (changed) { - updateTitleView(); - cancelIconLoading(); - ImageLoader.with(getContext()).cancelRequest(mThumbnailView); - } - - if (updateType(type)) { - changed = true; - } - - // The dirty state forces the state update to return true - // so that the adapter loads favicons once the thumbnails - // are loaded in TopSitesPanel/TopSitesGridAdapter. - changed = (changed || mIsDirty); - mIsDirty = false; - - return changed; - } - - /** - * Try to load an icon for the given page URL. - */ - public void loadFavicon(String pageUrl) { - mOngoingIconRequest = Icons.with(getContext()) - .pageUrl(pageUrl) - .skipNetwork() - .build() - .execute(this); - } - - private void cancelIconLoading() { - if (mOngoingIconRequest != null) { - mOngoingIconRequest.cancel(true); - } - } - - /** - * Display the thumbnail from a resource. - * - * @param resId Resource ID of the drawable to show. - */ - public void displayThumbnail(int resId) { - mThumbnailView.setScaleType(SCALE_TYPE_RESOURCE); - mThumbnailView.setImageResource(resId); - mThumbnailView.setBackgroundColor(0x0); - mThumbnailSet = false; - } - - /** - * Display the thumbnail from a bitmap. - * - * @param thumbnail The bitmap to show as thumbnail. - */ - public void displayThumbnail(Bitmap thumbnail) { - if (thumbnail == null) { - return; - } - - mThumbnailSet = true; - - cancelIconLoading(); - ImageLoader.with(getContext()).cancelRequest(mThumbnailView); - - mThumbnailView.setScaleType(SCALE_TYPE_THUMBNAIL); - mThumbnailView.setImageBitmap(thumbnail, true); - mThumbnailView.setBackgroundDrawable(null); - } - - /** - * Display the thumbnail from a URL. - * - * @param imageUrl URL of the image to show. - * @param bgColor background color to use in the view. - */ - public void displayThumbnail(final String imageUrl, final int bgColor) { - mThumbnailView.setScaleType(SCALE_TYPE_URL); - mThumbnailView.setBackgroundColor(bgColor); - mThumbnailSet = true; - - ImageLoader.with(getContext()) - .load(imageUrl) - .noFade() - .into(mThumbnailView); - } - - /** - * Update the item type associated with this view. Returns true if - * the type has changed, false otherwise. - */ - private boolean updateType(int type) { - if (mType == type) { - return false; - } - - mType = type; - refreshDrawableState(); - - int pinResourceId = (type == TopSites.TYPE_PINNED ? R.drawable.pin : 0); - mTitleView.setCompoundDrawablesWithIntrinsicBounds(pinResourceId, 0, 0, 0); - - return true; - } - - /** - * Update the title shown by this view. If both title and url - * are empty, mark the state as STATE_EMPTY and show a default text. - */ - private void updateTitleView() { - String title = getTitle(); - if (!TextUtils.isEmpty(title)) { - mTitleView.setText(title); - } else { - mTitleView.setText(R.string.home_top_sites_add); - } - } - - /** - * Display the loaded icon (if no thumbnail is set). - */ - @Override - public void onIconResponse(IconResponse response) { - if (mThumbnailSet) { - // Already showing a thumbnail; do nothing. - return; - } - - mThumbnailView.setScaleType(SCALE_TYPE_FAVICON); - mThumbnailView.setImageBitmap(response.getBitmap(), false); - mThumbnailView.setBackgroundColorWithOpacityFilter(response.getColor()); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/TopSitesGridView.java b/mobile/android/base/java/org/mozilla/gecko/home/TopSitesGridView.java deleted file mode 100644 index 58a05b1986..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/TopSitesGridView.java +++ /dev/null @@ -1,169 +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.R; -import org.mozilla.gecko.ThumbnailHelper; -import android.content.Context; -import android.content.res.TypedArray; -import android.graphics.Rect; -import android.util.AttributeSet; -import android.view.ContextMenu.ContextMenuInfo; -import android.view.View; -import android.widget.AbsListView; -import android.widget.GridView; - -/** - * A grid view of top and pinned sites. - * Each cell in the grid is a TopSitesGridItemView. - */ -public class TopSitesGridView extends GridView { - private static final String LOGTAG = "GeckoTopSitesGridView"; - - // Listener for editing pinned sites. - public static interface OnEditPinnedSiteListener { - public void onEditPinnedSite(int position, String searchTerm); - } - - // Max number of top sites that needs to be shown. - private final int mMaxSites; - - // Number of columns to show. - private final int mNumColumns; - - // Horizontal spacing in between the rows. - private final int mHorizontalSpacing; - - // Vertical spacing in between the rows. - private final int mVerticalSpacing; - - // Measured width of this view. - private int mMeasuredWidth; - - // Measured height of this view. - private int mMeasuredHeight; - - // A dummy View used to measure the required size of the child Views. - private final TopSitesGridItemView dummyChildView; - - // Context menu info. - private TopSitesGridContextMenuInfo mContextMenuInfo; - - // Whether we're handling focus changes or not. This is used - // to avoid infinite re-layouts when using this GridView as - // a ListView header view (see bug 918044). - private boolean mIsHandlingFocusChange; - - public TopSitesGridView(Context context) { - this(context, null); - } - - public TopSitesGridView(Context context, AttributeSet attrs) { - this(context, attrs, R.attr.topSitesGridViewStyle); - } - - public TopSitesGridView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - mMaxSites = getResources().getInteger(R.integer.number_of_top_sites); - mNumColumns = getResources().getInteger(R.integer.number_of_top_sites_cols); - setNumColumns(mNumColumns); - - TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TopSitesGridView, defStyle, 0); - mHorizontalSpacing = a.getDimensionPixelOffset(R.styleable.TopSitesGridView_android_horizontalSpacing, 0x00); - mVerticalSpacing = a.getDimensionPixelOffset(R.styleable.TopSitesGridView_android_verticalSpacing, 0x00); - a.recycle(); - - dummyChildView = new TopSitesGridItemView(context); - // Set a default LayoutParams on the child, if it doesn't have one on its own. - AbsListView.LayoutParams params = (AbsListView.LayoutParams) dummyChildView.getLayoutParams(); - if (params == null) { - params = new AbsListView.LayoutParams(AbsListView.LayoutParams.WRAP_CONTENT, - AbsListView.LayoutParams.WRAP_CONTENT); - dummyChildView.setLayoutParams(params); - } - } - - @Override - protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) { - mIsHandlingFocusChange = true; - super.onFocusChanged(gainFocus, direction, previouslyFocusedRect); - mIsHandlingFocusChange = false; - } - - @Override - public void requestLayout() { - if (!mIsHandlingFocusChange) { - super.requestLayout(); - } - } - - @Override - public int getColumnWidth() { - // This method will be called from onMeasure() too. - // It's better to use getMeasuredWidth(), as it is safe in this case. - final int totalHorizontalSpacing = mNumColumns > 0 ? (mNumColumns - 1) * mHorizontalSpacing : 0; - return (getMeasuredWidth() - getPaddingLeft() - getPaddingRight() - totalHorizontalSpacing) / mNumColumns; - } - - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - // Sets the padding for this view. - super.onMeasure(widthMeasureSpec, heightMeasureSpec); - - final int measuredWidth = getMeasuredWidth(); - if (measuredWidth == mMeasuredWidth) { - // Return the cached values as the width is the same. - setMeasuredDimension(mMeasuredWidth, mMeasuredHeight); - return; - } - - final int columnWidth = getColumnWidth(); - - // Measure the exact width of the child, and the height based on the width. - // Note: the child (and TopSitesThumbnailView) takes care of calculating its height. - int childWidthSpec = MeasureSpec.makeMeasureSpec(columnWidth, MeasureSpec.EXACTLY); - int childHeightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); - dummyChildView.measure(childWidthSpec, childHeightSpec); - final int childHeight = dummyChildView.getMeasuredHeight(); - - // This is the maximum width of the contents of each child in the grid. - // Use this as the target width for thumbnails. - final int thumbnailWidth = dummyChildView.getMeasuredWidth() - dummyChildView.getPaddingLeft() - dummyChildView.getPaddingRight(); - ThumbnailHelper.getInstance().setThumbnailWidth(thumbnailWidth); - - // Number of rows required to show these top sites. - final int rows = (int) Math.ceil((double) mMaxSites / mNumColumns); - final int childrenHeight = childHeight * rows; - final int totalVerticalSpacing = rows > 0 ? (rows - 1) * mVerticalSpacing : 0; - - // Total height of this view. - final int measuredHeight = childrenHeight + getPaddingTop() + getPaddingBottom() + totalVerticalSpacing; - setMeasuredDimension(measuredWidth, measuredHeight); - mMeasuredWidth = measuredWidth; - mMeasuredHeight = measuredHeight; - } - - @Override - public ContextMenuInfo getContextMenuInfo() { - return mContextMenuInfo; - } - - public void setContextMenuInfo(TopSitesGridContextMenuInfo contextMenuInfo) { - mContextMenuInfo = contextMenuInfo; - } - - /** - * Stores information regarding the creation of the context menu for a GridView item. - */ - public static class TopSitesGridContextMenuInfo extends HomeContextMenuInfo { - public int type = -1; - - public TopSitesGridContextMenuInfo(View targetView, int position, long id) { - super(targetView, position, id); - this.itemType = RemoveItemType.HISTORY; - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/TopSitesPanel.java b/mobile/android/base/java/org/mozilla/gecko/home/TopSitesPanel.java deleted file mode 100644 index f39e51ac5e..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/TopSitesPanel.java +++ /dev/null @@ -1,968 +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 static org.mozilla.gecko.db.URLMetadataTable.TILE_COLOR_COLUMN; -import static org.mozilla.gecko.db.URLMetadataTable.TILE_IMAGE_URL_COLUMN; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.EnumSet; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.concurrent.Future; - -import org.mozilla.gecko.GeckoProfile; -import org.mozilla.gecko.R; -import org.mozilla.gecko.Telemetry; -import org.mozilla.gecko.TelemetryContract; -import org.mozilla.gecko.db.BrowserContract.Thumbnails; -import org.mozilla.gecko.db.BrowserContract.TopSites; -import org.mozilla.gecko.db.BrowserDB; -import org.mozilla.gecko.gfx.BitmapUtils; -import org.mozilla.gecko.home.HomeContextMenuInfo.RemoveItemType; -import org.mozilla.gecko.home.HomePager.OnUrlOpenListener; -import org.mozilla.gecko.home.PinSiteDialog.OnSiteSelectedListener; -import org.mozilla.gecko.home.TopSitesGridView.OnEditPinnedSiteListener; -import org.mozilla.gecko.home.TopSitesGridView.TopSitesGridContextMenuInfo; -import org.mozilla.gecko.icons.IconCallback; -import org.mozilla.gecko.icons.IconResponse; -import org.mozilla.gecko.icons.Icons; -import org.mozilla.gecko.restrictions.Restrictable; -import org.mozilla.gecko.restrictions.Restrictions; -import org.mozilla.gecko.util.StringUtils; -import org.mozilla.gecko.util.ThreadUtils; - -import android.app.Activity; -import android.content.ContentResolver; -import android.content.Context; -import android.database.Cursor; -import android.graphics.Bitmap; -import android.graphics.Color; -import android.os.Bundle; -import android.os.SystemClock; -import android.support.v4.app.FragmentManager; -import android.support.v4.app.LoaderManager.LoaderCallbacks; -import android.support.v4.content.AsyncTaskLoader; -import android.support.v4.content.Loader; -import android.support.v4.widget.CursorAdapter; -import android.text.TextUtils; -import android.util.Log; -import android.view.ContextMenu; -import android.view.ContextMenu.ContextMenuInfo; -import android.view.LayoutInflater; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; -import android.widget.AdapterView; -import android.widget.ListView; - -/** - * Fragment that displays frecency search results in a ListView. - */ -public class TopSitesPanel extends HomeFragment { - // Logging tag name - private static final String LOGTAG = "GeckoTopSitesPanel"; - - // Cursor loader ID for the top sites - private static final int LOADER_ID_TOP_SITES = 0; - - // Loader ID for thumbnails - private static final int LOADER_ID_THUMBNAILS = 1; - - // Key for thumbnail urls - private static final String THUMBNAILS_URLS_KEY = "urls"; - - // Adapter for the list of top sites - private VisitedAdapter mListAdapter; - - // Adapter for the grid of top sites - private TopSitesGridAdapter mGridAdapter; - - // List of top sites - private HomeListView mList; - - // Grid of top sites - private TopSitesGridView mGrid; - - // Callbacks used for the search and favicon cursor loaders - private CursorLoaderCallbacks mCursorLoaderCallbacks; - - // Callback for thumbnail loader - private ThumbnailsLoaderCallbacks mThumbnailsLoaderCallbacks; - - // Listener for editing pinned sites. - private EditPinnedSiteListener mEditPinnedSiteListener; - - // Max number of entries shown in the grid from the cursor. - private int mMaxGridEntries; - - // Time in ms until the Gecko thread is reset to normal priority. - private static final long PRIORITY_RESET_TIMEOUT = 10000; - - public static TopSitesPanel newInstance() { - return new TopSitesPanel(); - } - - private static final boolean logDebug = Log.isLoggable(LOGTAG, Log.DEBUG); - private static final boolean logVerbose = Log.isLoggable(LOGTAG, Log.VERBOSE); - - private static void debug(final String message) { - if (logDebug) { - Log.d(LOGTAG, message); - } - } - - private static void trace(final String message) { - if (logVerbose) { - Log.v(LOGTAG, message); - } - } - - @Override - public void onAttach(Activity activity) { - super.onAttach(activity); - - mMaxGridEntries = activity.getResources().getInteger(R.integer.number_of_top_sites); - } - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - final View view = inflater.inflate(R.layout.home_top_sites_panel, container, false); - - mList = (HomeListView) view.findViewById(R.id.list); - - mGrid = new TopSitesGridView(getActivity()); - mList.addHeaderView(mGrid); - - return view; - } - - @Override - public void onViewCreated(View view, Bundle savedInstanceState) { - mEditPinnedSiteListener = new EditPinnedSiteListener(); - - mList.setTag(HomePager.LIST_TAG_TOP_SITES); - mList.setHeaderDividersEnabled(false); - - mList.setOnItemClickListener(new AdapterView.OnItemClickListener() { - @Override - public void onItemClick(AdapterView parent, View view, int position, long id) { - final ListView list = (ListView) parent; - final int headerCount = list.getHeaderViewsCount(); - if (position < headerCount) { - // The click is on a header, don't do anything. - return; - } - - // Absolute position for the adapter. - position += (mGridAdapter.getCount() - headerCount); - - final Cursor c = mListAdapter.getCursor(); - if (c == null || !c.moveToPosition(position)) { - return; - } - - final String url = c.getString(c.getColumnIndexOrThrow(TopSites.URL)); - - Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.LIST_ITEM, "top_sites"); - - // This item is a TwoLinePageRow, so we allow switch-to-tab. - mUrlOpenListener.onUrlOpen(url, EnumSet.of(OnUrlOpenListener.Flags.ALLOW_SWITCH_TO_TAB)); - } - }); - - mList.setContextMenuInfoFactory(new HomeContextMenuInfo.Factory() { - @Override - public HomeContextMenuInfo makeInfoForCursor(View view, int position, long id, Cursor cursor) { - final HomeContextMenuInfo info = new HomeContextMenuInfo(view, position, id); - info.url = cursor.getString(cursor.getColumnIndexOrThrow(TopSites.URL)); - info.title = cursor.getString(cursor.getColumnIndexOrThrow(TopSites.TITLE)); - info.historyId = cursor.getInt(cursor.getColumnIndexOrThrow(TopSites.HISTORY_ID)); - info.itemType = RemoveItemType.HISTORY; - final int bookmarkIdCol = cursor.getColumnIndexOrThrow(TopSites.BOOKMARK_ID); - if (cursor.isNull(bookmarkIdCol)) { - // If this is a combined cursor, we may get a history item without a - // bookmark, in which case the bookmarks ID column value will be null. - info.bookmarkId = -1; - } else { - info.bookmarkId = cursor.getInt(bookmarkIdCol); - } - return info; - } - }); - - mGrid.setOnItemClickListener(new AdapterView.OnItemClickListener() { - @Override - public void onItemClick(AdapterView parent, View view, int position, long id) { - TopSitesGridItemView item = (TopSitesGridItemView) view; - - // Decode "user-entered" URLs before loading them. - String url = StringUtils.decodeUserEnteredUrl(item.getUrl()); - int type = item.getType(); - - // If the url is empty, the user can pin a site. - // If not, navigate to the page given by the url. - if (type != TopSites.TYPE_BLANK) { - if (mUrlOpenListener != null) { - final TelemetryContract.Method method; - if (type == TopSites.TYPE_SUGGESTED) { - method = TelemetryContract.Method.SUGGESTION; - } else { - method = TelemetryContract.Method.GRID_ITEM; - } - - String extra = Integer.toString(position); - if (type == TopSites.TYPE_PINNED) { - extra += "-pinned"; - } - - Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, method, extra); - - mUrlOpenListener.onUrlOpen(url, EnumSet.of(OnUrlOpenListener.Flags.NO_READER_VIEW)); - } - } else { - if (mEditPinnedSiteListener != null) { - mEditPinnedSiteListener.onEditPinnedSite(position, ""); - } - } - } - }); - - mGrid.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() { - @Override - public boolean onItemLongClick(AdapterView parent, View view, int position, long id) { - - Cursor cursor = (Cursor) parent.getItemAtPosition(position); - - TopSitesGridItemView item = (TopSitesGridItemView) view; - if (cursor == null || item.getType() == TopSites.TYPE_BLANK) { - mGrid.setContextMenuInfo(null); - return false; - } - - TopSitesGridContextMenuInfo contextMenuInfo = new TopSitesGridContextMenuInfo(view, position, id); - updateContextMenuFromCursor(contextMenuInfo, cursor); - mGrid.setContextMenuInfo(contextMenuInfo); - return mGrid.showContextMenuForChild(mGrid); - } - - /* - * Update the fields of a TopSitesGridContextMenuInfo object - * from a cursor. - * - * @param info context menu info object to be updated - * @param cursor used to update the context menu info object - */ - private void updateContextMenuFromCursor(TopSitesGridContextMenuInfo info, Cursor cursor) { - info.url = cursor.getString(cursor.getColumnIndexOrThrow(TopSites.URL)); - info.title = cursor.getString(cursor.getColumnIndexOrThrow(TopSites.TITLE)); - info.type = cursor.getInt(cursor.getColumnIndexOrThrow(TopSites.TYPE)); - info.historyId = cursor.getInt(cursor.getColumnIndexOrThrow(TopSites.HISTORY_ID)); - } - }); - - registerForContextMenu(mList); - registerForContextMenu(mGrid); - } - - @Override - public void onDestroyView() { - super.onDestroyView(); - - // Discard any additional item clicks on the list as the - // panel is getting destroyed (see bugs 930160 & 1096958). - mList.setOnItemClickListener(null); - mGrid.setOnItemClickListener(null); - - mList = null; - mGrid = null; - mListAdapter = null; - mGridAdapter = null; - } - - @Override - public void onActivityCreated(Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - - final Activity activity = getActivity(); - - // Setup the top sites grid adapter. - mGridAdapter = new TopSitesGridAdapter(activity, null); - mGrid.setAdapter(mGridAdapter); - - // Setup the top sites list adapter. - mListAdapter = new VisitedAdapter(activity, null); - mList.setAdapter(mListAdapter); - - // Create callbacks before the initial loader is started - mCursorLoaderCallbacks = new CursorLoaderCallbacks(); - mThumbnailsLoaderCallbacks = new ThumbnailsLoaderCallbacks(); - loadIfVisible(); - } - - @Override - public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfo) { - if (menuInfo == null) { - return; - } - - if (!(menuInfo instanceof TopSitesGridContextMenuInfo)) { - // Long pressed item was not a Top Sites GridView item. Superclass - // can handle this. - super.onCreateContextMenu(menu, view, menuInfo); - - if (!Restrictions.isAllowed(view.getContext(), Restrictable.CLEAR_HISTORY)) { - menu.findItem(R.id.home_remove).setVisible(false); - } - - return; - } - - final Context context = view.getContext(); - - // Long pressed item was a Top Sites GridView item, handle it. - MenuInflater inflater = new MenuInflater(context); - inflater.inflate(R.menu.home_contextmenu, menu); - - // Hide unused menu items. - menu.findItem(R.id.home_edit_bookmark).setVisible(false); - - menu.findItem(R.id.home_remove).setVisible(Restrictions.isAllowed(context, Restrictable.CLEAR_HISTORY)); - - TopSitesGridContextMenuInfo info = (TopSitesGridContextMenuInfo) menuInfo; - menu.setHeaderTitle(info.getDisplayTitle()); - - if (info.type != TopSites.TYPE_BLANK) { - if (info.type == TopSites.TYPE_PINNED) { - menu.findItem(R.id.top_sites_pin).setVisible(false); - } else { - menu.findItem(R.id.top_sites_unpin).setVisible(false); - } - } else { - menu.findItem(R.id.home_open_new_tab).setVisible(false); - menu.findItem(R.id.home_open_private_tab).setVisible(false); - menu.findItem(R.id.top_sites_pin).setVisible(false); - menu.findItem(R.id.top_sites_unpin).setVisible(false); - } - - if (!StringUtils.isShareableUrl(info.url) || GeckoProfile.get(getActivity()).inGuestMode()) { - menu.findItem(R.id.home_share).setVisible(false); - } - - if (!Restrictions.isAllowed(context, Restrictable.PRIVATE_BROWSING)) { - menu.findItem(R.id.home_open_private_tab).setVisible(false); - } - } - - @Override - public boolean onContextItemSelected(MenuItem item) { - if (super.onContextItemSelected(item)) { - // HomeFragment was able to handle to selected item. - return true; - } - - ContextMenuInfo menuInfo = item.getMenuInfo(); - - if (!(menuInfo instanceof TopSitesGridContextMenuInfo)) { - return false; - } - - TopSitesGridContextMenuInfo info = (TopSitesGridContextMenuInfo) menuInfo; - - final int itemId = item.getItemId(); - final BrowserDB db = BrowserDB.from(getActivity()); - - if (itemId == R.id.top_sites_pin) { - final String url = info.url; - final String title = info.title; - final int position = info.position; - final Context context = getActivity().getApplicationContext(); - - ThreadUtils.postToBackgroundThread(new Runnable() { - @Override - public void run() { - db.pinSite(context.getContentResolver(), url, title, position); - } - }); - - Telemetry.sendUIEvent(TelemetryContract.Event.PIN); - return true; - } - - if (itemId == R.id.top_sites_unpin) { - final int position = info.position; - final Context context = getActivity().getApplicationContext(); - - ThreadUtils.postToBackgroundThread(new Runnable() { - @Override - public void run() { - db.unpinSite(context.getContentResolver(), position); - } - }); - - Telemetry.sendUIEvent(TelemetryContract.Event.UNPIN); - - return true; - } - - if (itemId == R.id.top_sites_edit) { - // Decode "user-entered" URLs before showing them. - mEditPinnedSiteListener.onEditPinnedSite(info.position, - StringUtils.decodeUserEnteredUrl(info.url)); - - Telemetry.sendUIEvent(TelemetryContract.Event.EDIT); - return true; - } - - return false; - } - - @Override - protected void load() { - getLoaderManager().initLoader(LOADER_ID_TOP_SITES, null, mCursorLoaderCallbacks); - - // Since this is the primary fragment that loads whenever about:home is - // visited, we want to load it as quickly as possible. Heavy load on - // the Gecko thread can slow down the time it takes for thumbnails to - // appear, especially during startup (bug 897162). By minimizing the - // Gecko thread priority, we ensure that the UI appears quickly. The - // priority is reset to normal once thumbnails are loaded. - ThreadUtils.reduceGeckoPriority(PRIORITY_RESET_TIMEOUT); - } - - /** - * Listener for editing pinned sites. - */ - private class EditPinnedSiteListener implements OnEditPinnedSiteListener, - OnSiteSelectedListener { - // Tag for the PinSiteDialog fragment. - private static final String TAG_PIN_SITE = "pin_site"; - - // Position of the pin. - private int mPosition; - - @Override - public void onEditPinnedSite(int position, String searchTerm) { - final FragmentManager manager = getChildFragmentManager(); - PinSiteDialog dialog = (PinSiteDialog) manager.findFragmentByTag(TAG_PIN_SITE); - if (dialog == null) { - mPosition = position; - - dialog = PinSiteDialog.newInstance(); - dialog.setOnSiteSelectedListener(this); - dialog.setSearchTerm(searchTerm); - dialog.show(manager, TAG_PIN_SITE); - } - } - - @Override - public void onSiteSelected(final String url, final String title) { - final int position = mPosition; - final Context context = getActivity().getApplicationContext(); - final BrowserDB db = BrowserDB.from(getActivity()); - ThreadUtils.postToBackgroundThread(new Runnable() { - @Override - public void run() { - db.pinSite(context.getContentResolver(), url, title, position); - } - }); - } - } - - private void updateUiFromCursor(Cursor c) { - mList.setHeaderDividersEnabled(c != null && c.getCount() > mMaxGridEntries); - } - - private void updateUiWithThumbnails(Map thumbnails) { - if (mGridAdapter != null) { - mGridAdapter.updateThumbnails(thumbnails); - } - - // Once thumbnails have finished loading, the UI is ready. Reset - // Gecko to normal priority. - ThreadUtils.resetGeckoPriority(); - } - - private static class TopSitesLoader extends SimpleCursorLoader { - // Max number of search results. - private static final int SEARCH_LIMIT = 30; - private static final String TELEMETRY_HISTOGRAM_LOAD_CURSOR = "FENNEC_TOPSITES_LOADER_TIME_MS"; - private final BrowserDB mDB; - private final int mMaxGridEntries; - - public TopSitesLoader(Context context) { - super(context); - mMaxGridEntries = context.getResources().getInteger(R.integer.number_of_top_sites); - mDB = BrowserDB.from(context); - } - - @Override - public Cursor loadCursor() { - final long start = SystemClock.uptimeMillis(); - final Cursor cursor = mDB.getTopSites(getContext().getContentResolver(), mMaxGridEntries, SEARCH_LIMIT); - final long end = SystemClock.uptimeMillis(); - final long took = end - start; - Telemetry.addToHistogram(TELEMETRY_HISTOGRAM_LOAD_CURSOR, (int) Math.min(took, Integer.MAX_VALUE)); - return cursor; - } - } - - private class VisitedAdapter extends CursorAdapter { - public VisitedAdapter(Context context, Cursor cursor) { - super(context, cursor, 0); - } - - @Override - public int getCount() { - return Math.max(0, super.getCount() - mMaxGridEntries); - } - - @Override - public Object getItem(int position) { - return super.getItem(position + mMaxGridEntries); - } - - /** - * We have to override default getItemId implementation, since for a given position, it returns - * value of the _id column. In our case _id is always 0 (see Combined view). - */ - @Override - public long getItemId(int position) { - final int adjustedPosition = position + mMaxGridEntries; - final Cursor cursor = getCursor(); - - cursor.moveToPosition(adjustedPosition); - return getItemIdForTopSitesCursor(cursor); - } - - @Override - public void bindView(View view, Context context, Cursor cursor) { - final int position = cursor.getPosition(); - cursor.moveToPosition(position + mMaxGridEntries); - - final TwoLinePageRow row = (TwoLinePageRow) view; - row.updateFromCursor(cursor); - } - - @Override - public View newView(Context context, Cursor cursor, ViewGroup parent) { - return LayoutInflater.from(context).inflate(R.layout.bookmark_item_row, parent, false); - } - } - - public class TopSitesGridAdapter extends CursorAdapter { - private final BrowserDB mDB; - // Cache to store the thumbnails. - // Ensure that this is only accessed from the UI thread. - private Map mThumbnailInfos; - - public TopSitesGridAdapter(Context context, Cursor cursor) { - super(context, cursor, 0); - mDB = BrowserDB.from(context); - } - - @Override - public int getCount() { - return Math.min(mMaxGridEntries, super.getCount()); - } - - @Override - protected void onContentChanged() { - // Don't do anything. We don't want to regenerate every time - // our database is updated. - return; - } - - /** - * Update the thumbnails returned by the db. - * - * @param thumbnails A map of urls and their thumbnail bitmaps. - */ - public void updateThumbnails(Map thumbnails) { - mThumbnailInfos = thumbnails; - - final int count = mGrid.getChildCount(); - for (int i = 0; i < count; i++) { - TopSitesGridItemView gridItem = (TopSitesGridItemView) mGrid.getChildAt(i); - - // All the views have already got their initial state at this point. - // This will force each view to load favicons for the missing - // thumbnails if necessary. - gridItem.markAsDirty(); - } - - notifyDataSetChanged(); - } - - /** - * We have to override default getItemId implementation, since for a given position, it returns - * value of the _id column. In our case _id is always 0 (see Combined view). - */ - @Override - public long getItemId(int position) { - final Cursor cursor = getCursor(); - cursor.moveToPosition(position); - - return getItemIdForTopSitesCursor(cursor); - } - - @Override - public void bindView(View bindView, Context context, Cursor cursor) { - final String url = cursor.getString(cursor.getColumnIndexOrThrow(TopSites.URL)); - final String title = cursor.getString(cursor.getColumnIndexOrThrow(TopSites.TITLE)); - final int type = cursor.getInt(cursor.getColumnIndexOrThrow(TopSites.TYPE)); - - final TopSitesGridItemView view = (TopSitesGridItemView) bindView; - - // If there is no url, then show "add bookmark". - if (type == TopSites.TYPE_BLANK) { - view.blankOut(); - return; - } - - // Show the thumbnail, if any. - ThumbnailInfo thumbnail = (mThumbnailInfos != null ? mThumbnailInfos.get(url) : null); - - // Debounce bindView calls to avoid redundant redraws and favicon - // fetches. - final boolean updated = view.updateState(title, url, type, thumbnail); - - // Thumbnails are delivered late, so we can't short-circuit any - // sooner than this. But we can avoid a duplicate favicon - // fetch... - if (!updated) { - debug("bindView called twice for same values; short-circuiting."); - return; - } - - // Make sure we query suggested images without the user-entered wrapper. - final String decodedUrl = StringUtils.decodeUserEnteredUrl(url); - - // Suggested images have precedence over thumbnails, no need to wait - // for them to be loaded. See: CursorLoaderCallbacks.onLoadFinished() - final String imageUrl = mDB.getSuggestedImageUrlForUrl(decodedUrl); - if (!TextUtils.isEmpty(imageUrl)) { - final int bgColor = mDB.getSuggestedBackgroundColorForUrl(decodedUrl); - view.displayThumbnail(imageUrl, bgColor); - return; - } - - // If thumbnails are still being loaded, don't try to load favicons - // just yet. If we sent in a thumbnail, we're done now. - if (mThumbnailInfos == null || thumbnail != null) { - return; - } - - view.loadFavicon(url); - } - - @Override - public View newView(Context context, Cursor cursor, ViewGroup parent) { - return new TopSitesGridItemView(context); - } - } - - private class CursorLoaderCallbacks implements LoaderCallbacks { - @Override - public Loader onCreateLoader(int id, Bundle args) { - trace("Creating TopSitesLoader: " + id); - return new TopSitesLoader(getActivity()); - } - - /** - * This method is called *twice* in some circumstances. - * - * If you try to avoid that through some kind of boolean flag, - * sometimes (e.g., returning to the activity) you'll *not* be called - * twice, and thus you'll never draw thumbnails. - * - * The root cause is TopSitesLoader.loadCursor being called twice. - * Why that is... dunno. - */ - public void onLoadFinished(Loader loader, Cursor c) { - debug("onLoadFinished: " + c.getCount() + " rows."); - - mListAdapter.swapCursor(c); - mGridAdapter.swapCursor(c); - updateUiFromCursor(c); - - final int col = c.getColumnIndexOrThrow(TopSites.URL); - - // Load the thumbnails. - // Even though the cursor we're given is supposed to be fresh, - // we getIcon a bad first value unless we reset its position. - // Using move(-1) and moveToNext() doesn't work correctly under - // rotation, so we use moveToFirst. - if (!c.moveToFirst()) { - return; - } - - final ArrayList urls = new ArrayList(); - int i = 1; - do { - final String url = c.getString(col); - - // Only try to fetch thumbnails for non-empty URLs that - // don't have an associated suggested image URL. - final GeckoProfile profile = GeckoProfile.get(getActivity()); - if (TextUtils.isEmpty(url) || BrowserDB.from(profile).hasSuggestedImageUrl(url)) { - continue; - } - - urls.add(url); - } while (i++ < mMaxGridEntries && c.moveToNext()); - - if (urls.isEmpty()) { - // Short-circuit empty results to the UI. - updateUiWithThumbnails(new HashMap()); - return; - } - - Bundle bundle = new Bundle(); - bundle.putStringArrayList(THUMBNAILS_URLS_KEY, urls); - getLoaderManager().restartLoader(LOADER_ID_THUMBNAILS, bundle, mThumbnailsLoaderCallbacks); - } - - @Override - public void onLoaderReset(Loader loader) { - if (mListAdapter != null) { - mListAdapter.swapCursor(null); - } - - if (mGridAdapter != null) { - mGridAdapter.swapCursor(null); - } - } - } - - static class ThumbnailInfo { - public final Bitmap bitmap; - public final String imageUrl; - public final int bgColor; - - public ThumbnailInfo(final Bitmap bitmap) { - this.bitmap = bitmap; - this.imageUrl = null; - this.bgColor = Color.TRANSPARENT; - } - - public ThumbnailInfo(final String imageUrl, final int bgColor) { - this.bitmap = null; - this.imageUrl = imageUrl; - this.bgColor = bgColor; - } - - public static ThumbnailInfo fromMetadata(final Map data) { - if (data == null) { - return null; - } - - final String imageUrl = (String) data.get(TILE_IMAGE_URL_COLUMN); - if (imageUrl == null) { - return null; - } - - int bgColor = Color.WHITE; - final String colorString = (String) data.get(TILE_COLOR_COLUMN); - try { - bgColor = Color.parseColor(colorString); - } catch (Exception ex) { - } - - return new ThumbnailInfo(imageUrl, bgColor); - } - } - - /** - * An AsyncTaskLoader to load the thumbnails from a cursor. - */ - static class ThumbnailsLoader extends AsyncTaskLoader> { - private final BrowserDB mDB; - private Map mThumbnailInfos; - private final ArrayList mUrls; - - private static final List COLUMNS; - static { - final ArrayList tempColumns = new ArrayList<>(2); - tempColumns.add(TILE_IMAGE_URL_COLUMN); - tempColumns.add(TILE_COLOR_COLUMN); - COLUMNS = Collections.unmodifiableList(tempColumns); - } - - public ThumbnailsLoader(Context context, ArrayList urls) { - super(context); - mUrls = urls; - mDB = BrowserDB.from(context); - } - - @Override - public Map loadInBackground() { - final Map thumbnails = new HashMap(); - if (mUrls == null || mUrls.size() == 0) { - return thumbnails; - } - - // We need to query metadata based on the URL without any refs, hence we create a new - // mapping and list of these URLs (we need to preserve the original URL for display purposes) - final Map queryURLs = new HashMap<>(); - for (final String pageURL : mUrls) { - queryURLs.put(pageURL, StringUtils.stripRef(pageURL)); - } - - // Query the DB for tile images. - final ContentResolver cr = getContext().getContentResolver(); - // Use the stripped URLs for querying the DB - final Map> metadata = mDB.getURLMetadata().getForURLs(cr, queryURLs.values(), COLUMNS); - - // Keep a list of urls that don't have tiles images. We'll use thumbnails for them instead. - final List thumbnailUrls = new ArrayList(); - for (final String pageURL : mUrls) { - final String queryURL = queryURLs.get(pageURL); - - ThumbnailInfo info = ThumbnailInfo.fromMetadata(metadata.get(queryURL)); - if (info == null) { - // If we didn't find metadata, we'll look for a thumbnail for this url. - thumbnailUrls.add(pageURL); - continue; - } - - thumbnails.put(pageURL, info); - } - - if (thumbnailUrls.size() == 0) { - return thumbnails; - } - - // Query the DB for tile thumbnails. - final Cursor cursor = mDB.getThumbnailsForUrls(cr, thumbnailUrls); - if (cursor == null) { - return thumbnails; - } - - try { - final int urlIndex = cursor.getColumnIndexOrThrow(Thumbnails.URL); - final int dataIndex = cursor.getColumnIndexOrThrow(Thumbnails.DATA); - - while (cursor.moveToNext()) { - String url = cursor.getString(urlIndex); - - // This should never be null, but if it is... - final byte[] b = cursor.getBlob(dataIndex); - if (b == null) { - continue; - } - - final Bitmap bitmap = BitmapUtils.decodeByteArray(b); - - // Our thumbnails are never null, so if we getIcon a null decoded - // bitmap, it's because we hit an OOM or some other disaster. - // Give up immediately rather than hammering on. - if (bitmap == null) { - Log.w(LOGTAG, "Aborting thumbnail load; decode failed."); - break; - } - - thumbnails.put(url, new ThumbnailInfo(bitmap)); - } - } finally { - cursor.close(); - } - - return thumbnails; - } - - @Override - public void deliverResult(Map thumbnails) { - if (isReset()) { - mThumbnailInfos = null; - return; - } - - mThumbnailInfos = thumbnails; - - if (isStarted()) { - super.deliverResult(thumbnails); - } - } - - @Override - protected void onStartLoading() { - if (mThumbnailInfos != null) { - deliverResult(mThumbnailInfos); - } - - if (takeContentChanged() || mThumbnailInfos == null) { - forceLoad(); - } - } - - @Override - protected void onStopLoading() { - cancelLoad(); - } - - @Override - public void onCanceled(Map thumbnails) { - mThumbnailInfos = null; - } - - @Override - protected void onReset() { - super.onReset(); - - // Ensure the loader is stopped. - onStopLoading(); - - mThumbnailInfos = null; - } - } - - /** - * Loader callbacks for the thumbnails on TopSitesGridView. - */ - private class ThumbnailsLoaderCallbacks implements LoaderCallbacks> { - @Override - public Loader> onCreateLoader(int id, Bundle args) { - return new ThumbnailsLoader(getActivity(), args.getStringArrayList(THUMBNAILS_URLS_KEY)); - } - - @Override - public void onLoadFinished(Loader> loader, Map thumbnails) { - updateUiWithThumbnails(thumbnails); - } - - @Override - public void onLoaderReset(Loader> loader) { - if (mGridAdapter != null) { - mGridAdapter.updateThumbnails(null); - } - } - } - - /** - * We are trying to return stable IDs so that Android can recycle views appropriately: - * - If we have a history ID then we return it - * - If we only have a bookmark ID then we negate it and return it. We negate it in order - * to avoid clashing/conflicting with history IDs. - * - * @param cursorInPosition Cursor already moved to position for which we're getting a stable ID - * @return Stable ID for a given cursor - */ - private static long getItemIdForTopSitesCursor(final Cursor cursorInPosition) { - final int historyIdCol = cursorInPosition.getColumnIndexOrThrow(TopSites.HISTORY_ID); - final long historyId = cursorInPosition.getLong(historyIdCol); - if (historyId != 0) { - return historyId; - } - - final int bookmarkIdCol = cursorInPosition.getColumnIndexOrThrow(TopSites.BOOKMARK_ID); - final long bookmarkId = cursorInPosition.getLong(bookmarkIdCol); - return -1 * bookmarkId; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/TopSitesThumbnailView.java b/mobile/android/base/java/org/mozilla/gecko/home/TopSitesThumbnailView.java deleted file mode 100644 index dd45014b0a..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/TopSitesThumbnailView.java +++ /dev/null @@ -1,102 +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.support.v4.content.ContextCompat; -import org.mozilla.gecko.R; -import org.mozilla.gecko.ThumbnailHelper; -import org.mozilla.gecko.widget.CropImageView; - -import android.content.Context; -import android.content.res.Resources; -import android.graphics.Canvas; -import android.graphics.Paint; -import android.graphics.PorterDuff.Mode; -import android.graphics.drawable.Drawable; -import android.util.AttributeSet; - -/** - * A width constrained ImageView to show thumbnails of top and pinned sites. - */ -public class TopSitesThumbnailView extends CropImageView { - private static final String LOGTAG = "GeckoTopSitesThumbnailView"; - - // 27.34% opacity filter for the dominant color. - private static final int COLOR_FILTER = 0x46FFFFFF; - - // Default filter color for "Add a bookmark" views. - private final int mDefaultColor = ContextCompat.getColor(getContext(), R.color.top_site_default); - - // Stroke width for the border. - private final float mStrokeWidth = getResources().getDisplayMetrics().density * 2; - - // Paint for drawing the border. - private final Paint mBorderPaint; - - public TopSitesThumbnailView(Context context) { - this(context, null); - - // A border will be drawn if needed. - setWillNotDraw(false); - } - - public TopSitesThumbnailView(Context context, AttributeSet attrs) { - this(context, attrs, R.attr.topSitesThumbnailViewStyle); - } - - public TopSitesThumbnailView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - - // Initialize the border paint. - final Resources res = getResources(); - mBorderPaint = new Paint(Paint.ANTI_ALIAS_FLAG); - mBorderPaint.setColor(ContextCompat.getColor(context, R.color.top_site_border)); - mBorderPaint.setStyle(Paint.Style.STROKE); - } - - @Override - protected float getAspectRatio() { - return ThumbnailHelper.TOP_SITES_THUMBNAIL_ASPECT_RATIO; - } - - /** - * {@inheritDoc} - */ - @Override - public void onDraw(Canvas canvas) { - super.onDraw(canvas); - - if (getBackground() == null) { - mBorderPaint.setStrokeWidth(mStrokeWidth); - canvas.drawRect(0, 0, getWidth(), getHeight(), mBorderPaint); - } - } - - /** - * Sets the background color with a filter to reduce the color opacity. - * - * @param color the color filter to apply over the drawable. - */ - public void setBackgroundColorWithOpacityFilter(int color) { - setBackgroundColor(color & COLOR_FILTER); - } - - /** - * Sets the background to a Drawable by applying the specified color as a filter. - * - * @param color the color filter to apply over the drawable. - */ - @Override - public void setBackgroundColor(int color) { - if (color == 0) { - color = mDefaultColor; - } - - Drawable drawable = getResources().getDrawable(R.drawable.top_sites_thumbnail_bg); - drawable.setColorFilter(color, Mode.SRC_ATOP); - setBackgroundDrawable(drawable); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/TwoLinePageRow.java b/mobile/android/base/java/org/mozilla/gecko/home/TwoLinePageRow.java deleted file mode 100644 index 68eb8daa5c..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/TwoLinePageRow.java +++ /dev/null @@ -1,324 +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 java.lang.ref.WeakReference; -import java.util.concurrent.Future; - -import android.content.Context; -import android.database.Cursor; -import android.text.TextUtils; -import android.util.AttributeSet; -import android.view.Gravity; -import android.view.LayoutInflater; -import android.view.View; -import android.widget.ImageView; -import android.widget.LinearLayout; -import android.widget.TextView; - -import org.mozilla.gecko.AboutPages; -import org.mozilla.gecko.R; -import org.mozilla.gecko.Tab; -import org.mozilla.gecko.Tabs; -import org.mozilla.gecko.db.BrowserContract; -import org.mozilla.gecko.db.BrowserContract.Combined; -import org.mozilla.gecko.db.BrowserContract.URLColumns; -import org.mozilla.gecko.distribution.PartnerBookmarksProviderProxy; -import org.mozilla.gecko.icons.IconDescriptor; -import org.mozilla.gecko.icons.IconResponse; -import org.mozilla.gecko.icons.Icons; -import org.mozilla.gecko.reader.ReaderModeUtils; -import org.mozilla.gecko.reader.SavedReaderViewHelper; -import org.mozilla.gecko.widget.FaviconView; - -public class TwoLinePageRow extends LinearLayout - implements Tabs.OnTabsChangedListener { - - protected static final int NO_ICON = 0; - - private final TextView mTitle; - private final TextView mUrl; - private final ImageView mStatusIcon; - - private int mSwitchToTabIconId; - - private final FaviconView mFavicon; - private Future mOngoingIconLoad; - - private boolean mShowIcons; - - // The URL for the page corresponding to this view. - private String mPageUrl; - - private boolean mHasReaderCacheItem; - - public TwoLinePageRow(Context context) { - this(context, null); - } - - public TwoLinePageRow(Context context, AttributeSet attrs) { - super(context, attrs); - - setGravity(Gravity.CENTER_VERTICAL); - - LayoutInflater.from(context).inflate(R.layout.two_line_page_row, this); - // Merge layouts lose their padding, so set it dynamically. - setPadding(0, 0, (int) getResources().getDimension(R.dimen.page_row_edge_padding), 0); - - mTitle = (TextView) findViewById(R.id.title); - mUrl = (TextView) findViewById(R.id.url); - mStatusIcon = (ImageView) findViewById(R.id.status_icon_bookmark); - - mSwitchToTabIconId = NO_ICON; - mShowIcons = true; - - mFavicon = (FaviconView) findViewById(R.id.icon); - } - - @Override - protected void onAttachedToWindow() { - super.onAttachedToWindow(); - - Tabs.registerOnTabsChangedListener(this); - } - - @Override - protected void onDetachedFromWindow() { - super.onDetachedFromWindow(); - - // Tabs' listener array is safe to modify during use: its - // iteration pattern is based on snapshots. - Tabs.unregisterOnTabsChangedListener(this); - } - - /** - * Update the row in response to a tab change event. - *

- * This method is always invoked on the UI thread. - */ - @Override - public void onTabChanged(final Tab tab, final Tabs.TabEvents msg, final String data) { - // Carefully check if this tab event is relevant to this row. - final String pageUrl = mPageUrl; - if (pageUrl == null) { - return; - } - if (tab == null) { - return; - } - - // Return early if the page URL doesn't match the current tab URL, - // or the old tab URL. - // data is an empty String for ADDED/CLOSED, and contains the previous/old URL during - // LOCATION_CHANGE (the new URL is retrieved using tab.getURL()). - // tabURL and data may be about:reader URLs if the current or old tab page was a reader view - // page, however pageUrl will always be a plain URL (i.e. we only add about:reader when opening - // a reader view bookmark, at all other times it's a normal bookmark with normal URL). - final String tabUrl = tab.getURL(); - if (!pageUrl.equals(ReaderModeUtils.stripAboutReaderUrl(tabUrl)) && - !pageUrl.equals(ReaderModeUtils.stripAboutReaderUrl(data))) { - return; - } - - // Note: we *might* need to update the display status (i.e. switch-to-tab icon/label) if - // a matching tab has been opened/closed/switched to a different page. updateDisplayedUrl() will - // determine the changes (if any) that actually need to be made. A tab change with a matching URL - // does not imply that any changes are needed - e.g. if a given URL is already open in one tab, and - // is also opened in a second tab, the switch-to-tab status doesn't change, closing 1 of 2 tabs with a URL - // similarly doesn't change the switch-to-tab display, etc. (However closing the last tab for - // a given URL does require a status change, as does opening the first tab with that URL.) - switch (msg) { - case ADDED: - case CLOSED: - case LOCATION_CHANGE: - updateDisplayedUrl(); - break; - default: - break; - } - } - - private void setTitle(String text) { - mTitle.setText(text); - } - - protected void setUrl(String text) { - mUrl.setText(text); - } - - protected void setUrl(int stringId) { - mUrl.setText(stringId); - } - - protected String getUrl() { - return mPageUrl; - } - - protected void setSwitchToTabIcon(int iconId) { - if (mSwitchToTabIconId == iconId) { - return; - } - - mSwitchToTabIconId = iconId; - mUrl.setCompoundDrawablesWithIntrinsicBounds(mSwitchToTabIconId, 0, 0, 0); - } - - private void updateStatusIcon(boolean isBookmark, boolean isReaderItem) { - if (isReaderItem) { - mStatusIcon.setImageResource(R.drawable.status_icon_readercache); - } else if (isBookmark) { - mStatusIcon.setImageResource(R.drawable.star_blue); - } - - if (mShowIcons && (isBookmark || isReaderItem)) { - mStatusIcon.setVisibility(View.VISIBLE); - } else if (mShowIcons) { - // We use INVISIBLE to have consistent padding for our items. This means text/URLs - // fade consistently in the same location, regardless of them being bookmarked. - mStatusIcon.setVisibility(View.INVISIBLE); - } else { - mStatusIcon.setVisibility(View.GONE); - } - - } - - /** - * Stores the page URL, so that we can use it to replace "Switch to tab" if the open - * tab changes or is closed. - */ - private void updateDisplayedUrl(String url, boolean hasReaderCacheItem) { - mPageUrl = url; - mHasReaderCacheItem = hasReaderCacheItem; - updateDisplayedUrl(); - } - - /** - * Replaces the page URL with "Switch to tab" if there is already a tab open with that URL. - * Only looks for tabs that are either private or non-private, depending on the current - * selected tab. - */ - protected void updateDisplayedUrl() { - final Tab selectedTab = Tabs.getInstance().getSelectedTab(); - final boolean isPrivate = (selectedTab != null) && (selectedTab.isPrivate()); - - // We always want to display the underlying page url, however for readermode pages - // we navigate to the about:reader equivalent, hence we need to use that url when finding - // existing tabs - final String navigationUrl = mHasReaderCacheItem ? ReaderModeUtils.getAboutReaderForUrl(mPageUrl) : mPageUrl; - Tab tab = Tabs.getInstance().getFirstTabForUrl(navigationUrl, isPrivate); - - - if (!mShowIcons || tab == null) { - setUrl(mPageUrl); - setSwitchToTabIcon(NO_ICON); - } else { - setUrl(R.string.switch_to_tab); - setSwitchToTabIcon(R.drawable.ic_url_bar_tab); - } - } - - public void setShowIcons(boolean showIcons) { - mShowIcons = showIcons; - } - - /** - * Update the data displayed by this row. - *

- * This method must be invoked on the UI thread. - * - * @param title to display. - * @param url to display. - */ - public void update(String title, String url) { - update(title, url, 0, false); - } - - protected void update(String title, String url, long bookmarkId, boolean hasReaderCacheItem) { - if (mShowIcons) { - // The bookmark id will be 0 (null in database) when the url - // is not a bookmark and negative for 'fake' bookmarks. - final boolean isBookmark = bookmarkId > 0; - - updateStatusIcon(isBookmark, hasReaderCacheItem); - } else { - updateStatusIcon(false, false); - } - - // 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); - - // No point updating the below things if URL has not changed. Prevents evil Favicon flicker. - if (url.equals(mPageUrl)) { - return; - } - - // Blank the Favicon, so we don't show the wrong Favicon if we scroll and miss DB. - mFavicon.clearImage(); - - if (mOngoingIconLoad != null) { - mOngoingIconLoad.cancel(true); - } - - // Displayed RecentTabsPanel URLs may refer to pages opened in reader mode, so we - // remove the about:reader prefix to ensure the Favicon loads properly. - final String pageURL = ReaderModeUtils.stripAboutReaderUrl(url); - - if (TextUtils.isEmpty(pageURL)) { - // If url is empty, display the item as-is but do not load an icon if we do not have a page URL (bug 1310622) - } else if (bookmarkId < BrowserContract.Bookmarks.FAKE_PARTNER_BOOKMARKS_START) { - mOngoingIconLoad = Icons.with(getContext()) - .pageUrl(pageURL) - .skipNetwork() - .privileged(true) - .icon(IconDescriptor.createGenericIcon( - PartnerBookmarksProviderProxy.getUriForIcon(getContext(), bookmarkId).toString())) - .build() - .execute(mFavicon.createIconCallback()); - } else { - mOngoingIconLoad = Icons.with(getContext()) - .pageUrl(pageURL) - .skipNetwork() - .build() - .execute(mFavicon.createIconCallback()); - - } - - updateDisplayedUrl(url, hasReaderCacheItem); - } - - /** - * Update the data displayed by this row. - *

- * This method must be invoked on the UI thread. - * - * @param cursor to extract data from. - */ - public void updateFromCursor(Cursor cursor) { - if (cursor == null) { - return; - } - - int titleIndex = cursor.getColumnIndexOrThrow(URLColumns.TITLE); - final String title = cursor.getString(titleIndex); - - int urlIndex = cursor.getColumnIndexOrThrow(URLColumns.URL); - final String url = cursor.getString(urlIndex); - - final long bookmarkId; - final int bookmarkIdIndex = cursor.getColumnIndex(Combined.BOOKMARK_ID); - if (bookmarkIdIndex != -1) { - bookmarkId = cursor.getLong(bookmarkIdIndex); - } else { - bookmarkId = 0; - } - - SavedReaderViewHelper rch = SavedReaderViewHelper.getSavedReaderViewHelper(getContext()); - final boolean hasReaderCacheItem = rch.isURLCached(url); - - update(title, url, bookmarkId, hasReaderCacheItem); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/activitystream/ActivityStream.java b/mobile/android/base/java/org/mozilla/gecko/home/activitystream/ActivityStream.java deleted file mode 100644 index ef0c105d30..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/activitystream/ActivityStream.java +++ /dev/null @@ -1,145 +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.activitystream; - -import android.content.Context; -import android.content.res.Resources; -import android.database.Cursor; -import android.os.Bundle; -import android.support.v4.app.LoaderManager; -import android.support.v4.content.ContextCompat; -import android.support.v4.content.Loader; -import android.support.v4.graphics.ColorUtils; -import android.support.v7.widget.LinearLayoutManager; -import android.support.v7.widget.RecyclerView; -import android.util.AttributeSet; -import android.util.DisplayMetrics; -import android.util.Log; -import android.util.TypedValue; -import android.widget.FrameLayout; - -import org.mozilla.gecko.R; -import org.mozilla.gecko.db.BrowserDB; -import org.mozilla.gecko.home.HomePager; -import org.mozilla.gecko.home.activitystream.topsites.TopSitesPagerAdapter; -import org.mozilla.gecko.util.ContextUtils; -import org.mozilla.gecko.widget.RecyclerViewClickSupport; - -public class ActivityStream extends FrameLayout { - private final StreamRecyclerAdapter adapter; - - private static final int LOADER_ID_HIGHLIGHTS = 0; - private static final int LOADER_ID_TOPSITES = 1; - - private static final int MINIMUM_TILES = 4; - private static final int MAXIMUM_TILES = 6; - - private int desiredTileWidth; - private int desiredTilesHeight; - private int tileMargin; - - public ActivityStream(Context context, AttributeSet attrs) { - super(context, attrs); - - setBackgroundColor(ContextCompat.getColor(context, R.color.about_page_header_grey)); - - inflate(context, R.layout.as_content, this); - - adapter = new StreamRecyclerAdapter(); - - RecyclerView rv = (RecyclerView) findViewById(R.id.activity_stream_main_recyclerview); - - rv.setAdapter(adapter); - rv.setLayoutManager(new LinearLayoutManager(getContext())); - rv.setHasFixedSize(true); - - RecyclerViewClickSupport.addTo(rv) - .setOnItemClickListener(adapter); - - final Resources resources = getResources(); - desiredTileWidth = resources.getDimensionPixelSize(R.dimen.activity_stream_desired_tile_width); - desiredTilesHeight = resources.getDimensionPixelSize(R.dimen.activity_stream_desired_tile_height); - tileMargin = resources.getDimensionPixelSize(R.dimen.activity_stream_base_margin); - } - - void setOnUrlOpenListeners(HomePager.OnUrlOpenListener onUrlOpenListener, HomePager.OnUrlOpenInBackgroundListener onUrlOpenInBackgroundListener) { - adapter.setOnUrlOpenListeners(onUrlOpenListener, onUrlOpenInBackgroundListener); - } - - public void load(LoaderManager lm) { - CursorLoaderCallbacks callbacks = new CursorLoaderCallbacks(); - - lm.initLoader(LOADER_ID_HIGHLIGHTS, null, callbacks); - lm.initLoader(LOADER_ID_TOPSITES, null, callbacks); - } - - public void unload() { - adapter.swapHighlightsCursor(null); - adapter.swapTopSitesCursor(null); - } - - @Override - protected void onSizeChanged(int w, int h, int oldw, int oldh) { - super.onSizeChanged(w, h, oldw, oldh); - - int tiles = (w - tileMargin) / (desiredTileWidth + tileMargin); - - if (tiles < MINIMUM_TILES) { - tiles = MINIMUM_TILES; - - setPadding(0, 0, 0, 0); - } else if (tiles > MAXIMUM_TILES) { - tiles = MAXIMUM_TILES; - - // Use the remaining space as padding - int needed = tiles * (desiredTileWidth + tileMargin) + tileMargin; - int padding = (w - needed) / 2; - w = needed; - - setPadding(padding, 0, padding, 0); - } else { - setPadding(0, 0, 0, 0); - } - - final float ratio = (float) desiredTilesHeight / (float) desiredTileWidth; - final int tilesWidth = (w - (tiles * tileMargin) - tileMargin) / tiles; - final int tilesHeight = (int) (ratio * tilesWidth); - - adapter.setTileSize(tiles, tilesWidth, tilesHeight); - } - - private class CursorLoaderCallbacks implements LoaderManager.LoaderCallbacks { - @Override - public Loader onCreateLoader(int id, Bundle args) { - final Context context = getContext(); - if (id == LOADER_ID_HIGHLIGHTS) { - return BrowserDB.from(context).getHighlights(context, 10); - } else if (id == LOADER_ID_TOPSITES) { - return BrowserDB.from(context).getActivityStreamTopSites( - context, TopSitesPagerAdapter.PAGES * MAXIMUM_TILES); - } else { - throw new IllegalArgumentException("Can't handle loader id " + id); - } - } - - @Override - public void onLoadFinished(Loader loader, Cursor data) { - if (loader.getId() == LOADER_ID_HIGHLIGHTS) { - adapter.swapHighlightsCursor(data); - } else if (loader.getId() == LOADER_ID_TOPSITES) { - adapter.swapTopSitesCursor(data); - } - } - - @Override - public void onLoaderReset(Loader loader) { - if (loader.getId() == LOADER_ID_HIGHLIGHTS) { - adapter.swapHighlightsCursor(null); - } else if (loader.getId() == LOADER_ID_TOPSITES) { - adapter.swapTopSitesCursor(null); - } - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/activitystream/ActivityStreamHomeFragment.java b/mobile/android/base/java/org/mozilla/gecko/home/activitystream/ActivityStreamHomeFragment.java deleted file mode 100644 index 09f6705d78..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/activitystream/ActivityStreamHomeFragment.java +++ /dev/null @@ -1,39 +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.activitystream; - -import android.os.Bundle; -import android.support.annotation.Nullable; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; - -import org.mozilla.gecko.R; -import org.mozilla.gecko.home.HomeFragment; - -/** - * Simple wrapper around the ActivityStream view that allows embedding as a HomePager panel. - */ -public class ActivityStreamHomeFragment - extends HomeFragment { - private ActivityStream activityStream; - - @Override - protected void load() { - activityStream.load(getLoaderManager()); - } - - @Nullable - @Override - public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, - @Nullable Bundle savedInstanceState) { - if (activityStream == null) { - activityStream = (ActivityStream) inflater.inflate(R.layout.activity_stream, container, false); - activityStream.setOnUrlOpenListeners(mUrlOpenListener, mUrlOpenInBackgroundListener); - } - - return activityStream; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/activitystream/ActivityStreamHomeScreen.java b/mobile/android/base/java/org/mozilla/gecko/home/activitystream/ActivityStreamHomeScreen.java deleted file mode 100644 index 4decc82180..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/activitystream/ActivityStreamHomeScreen.java +++ /dev/null @@ -1,73 +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.activitystream; - -import android.content.Context; -import android.os.Bundle; -import android.support.v4.app.FragmentManager; -import android.support.v4.app.LoaderManager; -import android.util.AttributeSet; - -import org.mozilla.gecko.animation.PropertyAnimator; -import org.mozilla.gecko.home.HomeBanner; -import org.mozilla.gecko.home.HomeFragment; -import org.mozilla.gecko.home.HomeScreen; - -/** - * HomeScreen implementation that displays ActivityStream. - */ -public class ActivityStreamHomeScreen - extends ActivityStream - implements HomeScreen { - - private boolean visible = false; - - public ActivityStreamHomeScreen(Context context, AttributeSet attrs) { - super(context, attrs); - } - - @Override - public boolean isVisible() { - return visible; - } - - @Override - public void onToolbarFocusChange(boolean hasFocus) { - - } - - @Override - public void showPanel(String panelId, Bundle restoreData) { - - } - - @Override - public void setOnPanelChangeListener(OnPanelChangeListener listener) { - - } - - @Override - public void setPanelStateChangeListener(HomeFragment.PanelStateChangeListener listener) { - - } - - @Override - public void setBanner(HomeBanner banner) { - - } - - @Override - public void load(LoaderManager lm, FragmentManager fm, String panelId, Bundle restoreData, - PropertyAnimator animator) { - super.load(lm); - visible = true; - } - - @Override - public void unload() { - super.unload(); - visible = false; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/activitystream/StreamItem.java b/mobile/android/base/java/org/mozilla/gecko/home/activitystream/StreamItem.java deleted file mode 100644 index 24348dfe09..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/activitystream/StreamItem.java +++ /dev/null @@ -1,196 +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.activitystream; - -import android.content.res.Resources; -import android.database.Cursor; -import android.graphics.Color; -import android.support.v4.view.ViewPager; -import android.support.v7.widget.RecyclerView; -import android.text.TextUtils; -import android.text.format.DateUtils; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ImageView; -import android.widget.TextView; - -import org.mozilla.gecko.R; -import org.mozilla.gecko.activitystream.ActivityStream.LabelCallback; -import org.mozilla.gecko.db.BrowserContract; -import org.mozilla.gecko.home.HomePager; -import org.mozilla.gecko.home.activitystream.menu.ActivityStreamContextMenu; -import org.mozilla.gecko.home.activitystream.topsites.CirclePageIndicator; -import org.mozilla.gecko.home.activitystream.topsites.TopSitesPagerAdapter; -import org.mozilla.gecko.icons.IconCallback; -import org.mozilla.gecko.icons.IconResponse; -import org.mozilla.gecko.icons.Icons; -import org.mozilla.gecko.util.DrawableUtil; -import org.mozilla.gecko.util.ViewUtil; -import org.mozilla.gecko.util.TouchTargetUtil; -import org.mozilla.gecko.widget.FaviconView; - -import java.util.concurrent.Future; - -import static org.mozilla.gecko.activitystream.ActivityStream.extractLabel; - -public abstract class StreamItem extends RecyclerView.ViewHolder { - public StreamItem(View itemView) { - super(itemView); - } - - public static class HighlightsTitle extends StreamItem { - public static final int LAYOUT_ID = R.layout.activity_stream_main_highlightstitle; - - public HighlightsTitle(View itemView) { - super(itemView); - } - } - - public static class TopPanel extends StreamItem { - public static final int LAYOUT_ID = R.layout.activity_stream_main_toppanel; - - private final ViewPager topSitesPager; - - public TopPanel(View itemView, HomePager.OnUrlOpenListener onUrlOpenListener, HomePager.OnUrlOpenInBackgroundListener onUrlOpenInBackgroundListener) { - super(itemView); - - topSitesPager = (ViewPager) itemView.findViewById(R.id.topsites_pager); - topSitesPager.setAdapter(new TopSitesPagerAdapter(itemView.getContext(), onUrlOpenListener, onUrlOpenInBackgroundListener)); - - CirclePageIndicator indicator = (CirclePageIndicator) itemView.findViewById(R.id.topsites_indicator); - indicator.setViewPager(topSitesPager); - } - - public void bind(Cursor cursor, int tiles, int tilesWidth, int tilesHeight) { - final TopSitesPagerAdapter adapter = (TopSitesPagerAdapter) topSitesPager.getAdapter(); - adapter.setTilesSize(tiles, tilesWidth, tilesHeight); - adapter.swapCursor(cursor); - - final Resources resources = itemView.getResources(); - final int tilesMargin = resources.getDimensionPixelSize(R.dimen.activity_stream_base_margin); - final int textHeight = resources.getDimensionPixelSize(R.dimen.activity_stream_top_sites_text_height); - - ViewGroup.LayoutParams layoutParams = topSitesPager.getLayoutParams(); - layoutParams.height = tilesHeight + tilesMargin + textHeight; - topSitesPager.setLayoutParams(layoutParams); - } - } - - public static class HighlightItem extends StreamItem implements IconCallback { - public static final int LAYOUT_ID = R.layout.activity_stream_card_history_item; - - String title; - String url; - - final FaviconView vIconView; - final TextView vLabel; - final TextView vTimeSince; - final TextView vSourceView; - final TextView vPageView; - final ImageView vSourceIconView; - - private Future ongoingIconLoad; - private int tilesMargin; - - public HighlightItem(final View itemView, - final HomePager.OnUrlOpenListener onUrlOpenListener, - final HomePager.OnUrlOpenInBackgroundListener onUrlOpenInBackgroundListener) { - super(itemView); - - tilesMargin = itemView.getResources().getDimensionPixelSize(R.dimen.activity_stream_base_margin); - - vLabel = (TextView) itemView.findViewById(R.id.card_history_label); - vTimeSince = (TextView) itemView.findViewById(R.id.card_history_time_since); - vIconView = (FaviconView) itemView.findViewById(R.id.icon); - vSourceView = (TextView) itemView.findViewById(R.id.card_history_source); - vPageView = (TextView) itemView.findViewById(R.id.page); - vSourceIconView = (ImageView) itemView.findViewById(R.id.source_icon); - - final ImageView menuButton = (ImageView) itemView.findViewById(R.id.menu); - - menuButton.setImageDrawable( - DrawableUtil.tintDrawable(menuButton.getContext(), R.drawable.menu, Color.LTGRAY)); - - TouchTargetUtil.ensureTargetHitArea(menuButton, itemView); - - menuButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - ActivityStreamContextMenu.show(v.getContext(), - menuButton, - ActivityStreamContextMenu.MenuMode.HIGHLIGHT, - title, url, onUrlOpenListener, onUrlOpenInBackgroundListener, - vIconView.getWidth(), vIconView.getHeight()); - } - }); - - ViewUtil.enableTouchRipple(menuButton); - } - - public void bind(Cursor cursor, int tilesWidth, int tilesHeight) { - - final long time = cursor.getLong(cursor.getColumnIndexOrThrow(BrowserContract.Highlights.DATE)); - final String ago = DateUtils.getRelativeTimeSpanString(time, System.currentTimeMillis(), DateUtils.MINUTE_IN_MILLIS, 0).toString(); - - title = cursor.getString(cursor.getColumnIndexOrThrow(BrowserContract.History.TITLE)); - url = cursor.getString(cursor.getColumnIndexOrThrow(BrowserContract.Combined.URL)); - - vLabel.setText(title); - vTimeSince.setText(ago); - - ViewGroup.LayoutParams layoutParams = vIconView.getLayoutParams(); - layoutParams.width = tilesWidth - tilesMargin; - layoutParams.height = tilesHeight; - vIconView.setLayoutParams(layoutParams); - - updateSource(cursor); - updatePage(url); - - if (ongoingIconLoad != null) { - ongoingIconLoad.cancel(true); - } - - ongoingIconLoad = Icons.with(itemView.getContext()) - .pageUrl(url) - .skipNetwork() - .build() - .execute(this); - } - - private void updateSource(final Cursor cursor) { - final boolean isBookmark = -1 != cursor.getLong(cursor.getColumnIndexOrThrow(BrowserContract.Combined.BOOKMARK_ID)); - final boolean isHistory = -1 != cursor.getLong(cursor.getColumnIndexOrThrow(BrowserContract.Combined.HISTORY_ID)); - - if (isBookmark) { - vSourceView.setText(R.string.activity_stream_highlight_label_bookmarked); - vSourceView.setVisibility(View.VISIBLE); - vSourceIconView.setImageResource(R.drawable.ic_as_bookmarked); - } else if (isHistory) { - vSourceView.setText(R.string.activity_stream_highlight_label_visited); - vSourceView.setVisibility(View.VISIBLE); - vSourceIconView.setImageResource(R.drawable.ic_as_visited); - } else { - vSourceView.setVisibility(View.INVISIBLE); - vSourceIconView.setImageResource(0); - } - - vSourceView.setText(vSourceView.getText()); - } - - private void updatePage(final String url) { - extractLabel(itemView.getContext(), url, false, new LabelCallback() { - @Override - public void onLabelExtracted(String label) { - vPageView.setText(TextUtils.isEmpty(label) ? url : label); - } - }); - } - - @Override - public void onIconResponse(IconResponse response) { - vIconView.updateImage(response); - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/activitystream/StreamRecyclerAdapter.java b/mobile/android/base/java/org/mozilla/gecko/home/activitystream/StreamRecyclerAdapter.java deleted file mode 100644 index f7cda2e7fd..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/activitystream/StreamRecyclerAdapter.java +++ /dev/null @@ -1,135 +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.activitystream; - -import android.database.Cursor; -import android.support.v7.widget.RecyclerView; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; - -import org.mozilla.gecko.db.BrowserContract; -import org.mozilla.gecko.home.HomePager; -import org.mozilla.gecko.home.activitystream.StreamItem.HighlightItem; -import org.mozilla.gecko.home.activitystream.StreamItem.TopPanel; -import org.mozilla.gecko.widget.RecyclerViewClickSupport; - -import java.util.EnumSet; - -public class StreamRecyclerAdapter extends RecyclerView.Adapter implements RecyclerViewClickSupport.OnItemClickListener { - private Cursor highlightsCursor; - private Cursor topSitesCursor; - - private HomePager.OnUrlOpenListener onUrlOpenListener; - private HomePager.OnUrlOpenInBackgroundListener onUrlOpenInBackgroundListener; - - private int tiles; - private int tilesWidth; - private int tilesHeight; - - void setOnUrlOpenListeners(HomePager.OnUrlOpenListener onUrlOpenListener, HomePager.OnUrlOpenInBackgroundListener onUrlOpenInBackgroundListener) { - this.onUrlOpenListener = onUrlOpenListener; - this.onUrlOpenInBackgroundListener = onUrlOpenInBackgroundListener; - } - - public void setTileSize(int tiles, int tilesWidth, int tilesHeight) { - this.tilesWidth = tilesWidth; - this.tilesHeight = tilesHeight; - this.tiles = tiles; - - notifyDataSetChanged(); - } - - @Override - public int getItemViewType(int position) { - if (position == 0) { - return TopPanel.LAYOUT_ID; - } else if (position == 1) { - return StreamItem.HighlightsTitle.LAYOUT_ID; - } else { - return HighlightItem.LAYOUT_ID; - } - } - - @Override - public StreamItem onCreateViewHolder(ViewGroup parent, final int type) { - final LayoutInflater inflater = LayoutInflater.from(parent.getContext()); - - if (type == TopPanel.LAYOUT_ID) { - return new TopPanel(inflater.inflate(type, parent, false), onUrlOpenListener, onUrlOpenInBackgroundListener); - } else if (type == StreamItem.HighlightsTitle.LAYOUT_ID) { - return new StreamItem.HighlightsTitle(inflater.inflate(type, parent, false)); - } else if (type == HighlightItem.LAYOUT_ID) { - return new HighlightItem(inflater.inflate(type, parent, false), onUrlOpenListener, onUrlOpenInBackgroundListener); - } else { - throw new IllegalStateException("Missing inflation for ViewType " + type); - } - } - - private int translatePositionToCursor(int position) { - if (position == 0) { - throw new IllegalArgumentException("Requested cursor position for invalid item"); - } - - // We have two blank panels at the top, hence remove that to obtain the cursor position - return position - 2; - } - - @Override - public void onBindViewHolder(StreamItem holder, int position) { - int type = getItemViewType(position); - - if (type == HighlightItem.LAYOUT_ID) { - final int cursorPosition = translatePositionToCursor(position); - - highlightsCursor.moveToPosition(cursorPosition); - ((HighlightItem) holder).bind(highlightsCursor, tilesWidth, tilesHeight); - } else if (type == TopPanel.LAYOUT_ID) { - ((TopPanel) holder).bind(topSitesCursor, tiles, tilesWidth, tilesHeight); - } - } - - @Override - public void onItemClicked(RecyclerView recyclerView, int position, View v) { - if (position < 1) { - // The header contains top sites and has its own click handling. - return; - } - - highlightsCursor.moveToPosition( - translatePositionToCursor(position)); - - final String url = highlightsCursor.getString( - highlightsCursor.getColumnIndexOrThrow(BrowserContract.Combined.URL)); - - onUrlOpenListener.onUrlOpen(url, EnumSet.of(HomePager.OnUrlOpenListener.Flags.ALLOW_SWITCH_TO_TAB)); - } - - @Override - public int getItemCount() { - final int highlightsCount; - - if (highlightsCursor != null) { - highlightsCount = highlightsCursor.getCount(); - } else { - highlightsCount = 0; - } - - return highlightsCount + 2; - } - - public void swapHighlightsCursor(Cursor cursor) { - highlightsCursor = cursor; - - notifyDataSetChanged(); - } - - public void swapTopSitesCursor(Cursor cursor) { - this.topSitesCursor = cursor; - - notifyItemChanged(0); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/activitystream/menu/ActivityStreamContextMenu.java b/mobile/android/base/java/org/mozilla/gecko/home/activitystream/menu/ActivityStreamContextMenu.java deleted file mode 100644 index 525d3b426b..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/activitystream/menu/ActivityStreamContextMenu.java +++ /dev/null @@ -1,239 +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.activitystream.menu; - -import android.content.Context; -import android.content.Intent; -import android.database.Cursor; -import android.support.annotation.NonNull; -import android.support.design.widget.NavigationView; -import android.view.MenuItem; -import android.view.View; - -import org.mozilla.gecko.GeckoAppShell; -import org.mozilla.gecko.IntentHelper; -import org.mozilla.gecko.R; -import org.mozilla.gecko.Telemetry; -import org.mozilla.gecko.TelemetryContract; -import org.mozilla.gecko.annotation.RobocopTarget; -import org.mozilla.gecko.db.BrowserDB; -import org.mozilla.gecko.home.HomePager; -import org.mozilla.gecko.util.Clipboard; -import org.mozilla.gecko.util.HardwareUtils; -import org.mozilla.gecko.util.ThreadUtils; -import org.mozilla.gecko.util.UIAsyncTask; - -import java.util.EnumSet; - -@RobocopTarget -public abstract class ActivityStreamContextMenu - implements NavigationView.OnNavigationItemSelectedListener { - - public enum MenuMode { - HIGHLIGHT, - TOPSITE - } - - final Context context; - - final String title; - final String url; - - final HomePager.OnUrlOpenListener onUrlOpenListener; - final HomePager.OnUrlOpenInBackgroundListener onUrlOpenInBackgroundListener; - - boolean isAlreadyBookmarked; // default false; - - public abstract MenuItem getItemByID(int id); - - public abstract void show(); - - public abstract void dismiss(); - - final MenuMode mode; - - /* package-private */ ActivityStreamContextMenu(final Context context, - final MenuMode mode, - final String title, @NonNull final String url, - HomePager.OnUrlOpenListener onUrlOpenListener, - HomePager.OnUrlOpenInBackgroundListener onUrlOpenInBackgroundListener) { - this.context = context; - - this.mode = mode; - - this.title = title; - this.url = url; - this.onUrlOpenListener = onUrlOpenListener; - this.onUrlOpenInBackgroundListener = onUrlOpenInBackgroundListener; - } - - /** - * Must be called before the menu is shown. - *

- * Your implementation must be ready to return items from getItemByID() before postInit() is - * called, i.e. you should probably inflate your menu items before this call. - */ - protected void postInit() { - // Disable "dismiss" for topsites until we have decided on its behaviour for topsites - // (currently "dismiss" adds the URL to a highlights-specific blocklist, which the topsites - // query has no knowledge of). - if (mode == MenuMode.TOPSITE) { - final MenuItem dismissItem = getItemByID(R.id.dismiss); - dismissItem.setVisible(false); - } - - // Disable the bookmark item until we know its bookmark state - final MenuItem bookmarkItem = getItemByID(R.id.bookmark); - bookmarkItem.setEnabled(false); - - (new UIAsyncTask.WithoutParams(ThreadUtils.getBackgroundHandler()) { - @Override - protected Void doInBackground() { - isAlreadyBookmarked = BrowserDB.from(context).isBookmark(context.getContentResolver(), url); - return null; - } - - @Override - protected void onPostExecute(Void aVoid) { - if (isAlreadyBookmarked) { - bookmarkItem.setTitle(R.string.bookmark_remove); - } - - bookmarkItem.setEnabled(true); - } - }).execute(); - - // Only show the "remove from history" item if a page actually has history - final MenuItem deleteHistoryItem = getItemByID(R.id.delete); - deleteHistoryItem.setVisible(false); - - (new UIAsyncTask.WithoutParams(ThreadUtils.getBackgroundHandler()) { - boolean hasHistory; - - @Override - protected Void doInBackground() { - final Cursor cursor = BrowserDB.from(context).getHistoryForURL(context.getContentResolver(), url); - try { - if (cursor != null && - cursor.getCount() == 1) { - hasHistory = true; - } else { - hasHistory = false; - } - } finally { - cursor.close(); - } - return null; - } - - @Override - protected void onPostExecute(Void aVoid) { - if (hasHistory) { - deleteHistoryItem.setVisible(true); - } - } - }).execute(); - } - - - @Override - public boolean onNavigationItemSelected(MenuItem item) { - switch (item.getItemId()) { - case R.id.share: - Telemetry.sendUIEvent(TelemetryContract.Event.SHARE, TelemetryContract.Method.LIST, "menu"); - IntentHelper.openUriExternal(url, "text/plain", "", "", Intent.ACTION_SEND, title, false); - break; - - case R.id.bookmark: - ThreadUtils.postToBackgroundThread(new Runnable() { - @Override - public void run() { - final BrowserDB db = BrowserDB.from(context); - - if (isAlreadyBookmarked) { - db.removeBookmarksWithURL(context.getContentResolver(), url); - } else { - db.addBookmark(context.getContentResolver(), title, url); - } - - } - }); - break; - - case R.id.copy_url: - Clipboard.setText(url); - break; - - case R.id.add_homescreen: - GeckoAppShell.createShortcut(title, url); - break; - - case R.id.open_new_tab: - onUrlOpenInBackgroundListener.onUrlOpenInBackground(url, EnumSet.noneOf(HomePager.OnUrlOpenInBackgroundListener.Flags.class)); - break; - - case R.id.open_new_private_tab: - onUrlOpenInBackgroundListener.onUrlOpenInBackground(url, EnumSet.of(HomePager.OnUrlOpenInBackgroundListener.Flags.PRIVATE)); - break; - - case R.id.dismiss: - ThreadUtils.postToBackgroundThread(new Runnable() { - @Override - public void run() { - BrowserDB.from(context) - .blockActivityStreamSite(context.getContentResolver(), - url); - } - }); - break; - - case R.id.delete: - ThreadUtils.postToBackgroundThread(new Runnable() { - @Override - public void run() { - BrowserDB.from(context) - .removeHistoryEntry(context.getContentResolver(), - url); - } - }); - break; - - default: - throw new IllegalArgumentException("Menu item with ID=" + item.getItemId() + " not handled"); - } - - dismiss(); - return true; - } - - - @RobocopTarget - public static ActivityStreamContextMenu show(Context context, - View anchor, - final MenuMode menuMode, - final String title, @NonNull final String url, - HomePager.OnUrlOpenListener onUrlOpenListener, - HomePager.OnUrlOpenInBackgroundListener onUrlOpenInBackgroundListener, - final int tilesWidth, final int tilesHeight) { - final ActivityStreamContextMenu menu; - - if (!HardwareUtils.isTablet()) { - menu = new BottomSheetContextMenu(context, - menuMode, - title, url, - onUrlOpenListener, onUrlOpenInBackgroundListener, - tilesWidth, tilesHeight); - } else { - menu = new PopupContextMenu(context, - anchor, - menuMode, - title, url, - onUrlOpenListener, onUrlOpenInBackgroundListener); - } - - menu.show(); - return menu; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/activitystream/menu/BottomSheetContextMenu.java b/mobile/android/base/java/org/mozilla/gecko/home/activitystream/menu/BottomSheetContextMenu.java deleted file mode 100644 index e95867c369..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/activitystream/menu/BottomSheetContextMenu.java +++ /dev/null @@ -1,102 +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.activitystream.menu; - -import android.content.Context; -import android.support.annotation.NonNull; -import android.support.design.widget.BottomSheetBehavior; -import android.support.design.widget.BottomSheetDialog; -import android.support.design.widget.NavigationView; -import android.view.LayoutInflater; -import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; -import android.widget.TextView; - -import org.mozilla.gecko.R; -import org.mozilla.gecko.activitystream.ActivityStream; -import org.mozilla.gecko.home.HomePager; -import org.mozilla.gecko.icons.IconCallback; -import org.mozilla.gecko.icons.IconResponse; -import org.mozilla.gecko.icons.Icons; -import org.mozilla.gecko.widget.FaviconView; - -import static org.mozilla.gecko.activitystream.ActivityStream.extractLabel; - -/* package-private */ class BottomSheetContextMenu - extends ActivityStreamContextMenu { - - - private final BottomSheetDialog bottomSheetDialog; - - private final NavigationView navigationView; - - public BottomSheetContextMenu(final Context context, - final MenuMode mode, - final String title, @NonNull final String url, - HomePager.OnUrlOpenListener onUrlOpenListener, - HomePager.OnUrlOpenInBackgroundListener onUrlOpenInBackgroundListener, - final int tilesWidth, final int tilesHeight) { - - super(context, - mode, - title, - url, - onUrlOpenListener, - onUrlOpenInBackgroundListener); - - final LayoutInflater inflater = LayoutInflater.from(context); - final View content = inflater.inflate(R.layout.activity_stream_contextmenu_bottomsheet, null); - - bottomSheetDialog = new BottomSheetDialog(context); - bottomSheetDialog.setContentView(content); - - ((TextView) content.findViewById(R.id.title)).setText(title); - - extractLabel(context, url, false, new ActivityStream.LabelCallback() { - public void onLabelExtracted(String label) { - ((TextView) content.findViewById(R.id.url)).setText(label); - } - }); - - // Copy layouted parameters from the Highlights / TopSites items to ensure consistency - final FaviconView faviconView = (FaviconView) content.findViewById(R.id.icon); - ViewGroup.LayoutParams layoutParams = faviconView.getLayoutParams(); - layoutParams.width = tilesWidth; - layoutParams.height = tilesHeight; - faviconView.setLayoutParams(layoutParams); - - Icons.with(context) - .pageUrl(url) - .skipNetwork() - .build() - .execute(new IconCallback() { - @Override - public void onIconResponse(IconResponse response) { - faviconView.updateImage(response); - } - }); - - navigationView = (NavigationView) content.findViewById(R.id.menu); - navigationView.setNavigationItemSelectedListener(this); - - super.postInit(); - } - - @Override - public MenuItem getItemByID(int id) { - return navigationView.getMenu().findItem(id); - } - - @Override - public void show() { - bottomSheetDialog.show(); - } - - public void dismiss() { - bottomSheetDialog.dismiss(); - } - -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/activitystream/menu/PopupContextMenu.java b/mobile/android/base/java/org/mozilla/gecko/home/activitystream/menu/PopupContextMenu.java deleted file mode 100644 index 56615937bd..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/activitystream/menu/PopupContextMenu.java +++ /dev/null @@ -1,76 +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.activitystream.menu; - -import android.content.Context; -import android.content.Intent; -import android.graphics.Color; -import android.graphics.drawable.ColorDrawable; -import android.support.annotation.NonNull; -import android.support.design.widget.NavigationView; -import android.view.LayoutInflater; -import android.view.MenuItem; -import android.view.View; -import android.widget.PopupWindow; - -import org.mozilla.gecko.R; -import org.mozilla.gecko.home.HomePager; - -/* package-private */ class PopupContextMenu - extends ActivityStreamContextMenu { - - private final PopupWindow popupWindow; - private final NavigationView navigationView; - - private final View anchor; - - public PopupContextMenu(final Context context, - View anchor, - final MenuMode mode, - final String title, - @NonNull final String url, - HomePager.OnUrlOpenListener onUrlOpenListener, - HomePager.OnUrlOpenInBackgroundListener onUrlOpenInBackgroundListener) { - super(context, - mode, - title, - url, - onUrlOpenListener, - onUrlOpenInBackgroundListener); - - this.anchor = anchor; - - final LayoutInflater inflater = LayoutInflater.from(context); - - View card = inflater.inflate(R.layout.activity_stream_contextmenu_popupmenu, null); - navigationView = (NavigationView) card.findViewById(R.id.menu); - navigationView.setNavigationItemSelectedListener(this); - - popupWindow = new PopupWindow(context); - popupWindow.setContentView(card); - popupWindow.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT)); - popupWindow.setFocusable(true); - - super.postInit(); - } - - @Override - public MenuItem getItemByID(int id) { - return navigationView.getMenu().findItem(id); - } - - @Override - public void show() { - // By default popupWindow follows the pre-material convention of displaying the popup - // below a View. We need to shift it over the view: - popupWindow.showAsDropDown(anchor, - 0, - -(anchor.getHeight() + anchor.getPaddingBottom())); - } - - public void dismiss() { - popupWindow.dismiss(); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/activitystream/topsites/CirclePageIndicator.java b/mobile/android/base/java/org/mozilla/gecko/home/activitystream/topsites/CirclePageIndicator.java deleted file mode 100644 index 096f0c597e..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/activitystream/topsites/CirclePageIndicator.java +++ /dev/null @@ -1,568 +0,0 @@ -/* - * Copyright (C) 2011 Patrik Akerfeldt - * Copyright (C) 2011 Jake Wharton - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.mozilla.gecko.home.activitystream.topsites; - -import android.content.Context; -import android.content.res.Resources; -import android.content.res.TypedArray; -import android.graphics.Canvas; -import android.graphics.Paint; -import android.graphics.Paint.Style; -import android.graphics.drawable.Drawable; -import android.os.Parcel; -import android.os.Parcelable; -import android.support.v4.view.MotionEventCompat; -import android.support.v4.view.ViewConfigurationCompat; -import android.support.v4.view.ViewPager; -import android.util.AttributeSet; -import android.view.MotionEvent; -import android.view.View; -import android.view.ViewConfiguration; - -import org.mozilla.gecko.R; - -import static android.graphics.Paint.ANTI_ALIAS_FLAG; -import static android.widget.LinearLayout.HORIZONTAL; -import static android.widget.LinearLayout.VERTICAL; - -/** - * Draws circles (one for each view). The current view position is filled and - * others are only stroked. - * - * This file was imported from Jake Wharton's ViewPagerIndicator library: - * https://github.com/JakeWharton/ViewPagerIndicator - * It was modified to not extend the PageIndicator interface (as we only use one single Indicator) - * implementation, and has had some minor appearance related modifications added alter. - */ -public class CirclePageIndicator - extends View - implements ViewPager.OnPageChangeListener { - - /** - * Separation between circles, as a factor of the circle radius. By default CirclePageIndicator - * shipped with a separation factor of 3, however we want to be able to tweak this for - * ActivityStream. - * - * If/when we reuse this indicator elsewhere, this should probably become a configurable property. - */ - private static final int SEPARATION_FACTOR = 7; - - private static final int INVALID_POINTER = -1; - - private float mRadius; - private final Paint mPaintPageFill = new Paint(ANTI_ALIAS_FLAG); - private final Paint mPaintStroke = new Paint(ANTI_ALIAS_FLAG); - private final Paint mPaintFill = new Paint(ANTI_ALIAS_FLAG); - private ViewPager mViewPager; - private ViewPager.OnPageChangeListener mListener; - private int mCurrentPage; - private int mSnapPage; - private float mPageOffset; - private int mScrollState; - private int mOrientation; - private boolean mCentered; - private boolean mSnap; - - private int mTouchSlop; - private float mLastMotionX = -1; - private int mActivePointerId = INVALID_POINTER; - private boolean mIsDragging; - - - public CirclePageIndicator(Context context) { - this(context, null); - } - - public CirclePageIndicator(Context context, AttributeSet attrs) { - this(context, attrs, R.attr.vpiCirclePageIndicatorStyle); - } - - public CirclePageIndicator(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - if (isInEditMode()) return; - - //Load defaults from resources - final Resources res = getResources(); - final int defaultPageColor = res.getColor(R.color.default_circle_indicator_page_color); - final int defaultFillColor = res.getColor(R.color.default_circle_indicator_fill_color); - final int defaultOrientation = res.getInteger(R.integer.default_circle_indicator_orientation); - final int defaultStrokeColor = res.getColor(R.color.default_circle_indicator_stroke_color); - final float defaultStrokeWidth = res.getDimension(R.dimen.default_circle_indicator_stroke_width); - final float defaultRadius = res.getDimension(R.dimen.default_circle_indicator_radius); - final boolean defaultCentered = res.getBoolean(R.bool.default_circle_indicator_centered); - final boolean defaultSnap = res.getBoolean(R.bool.default_circle_indicator_snap); - - //Retrieve styles attributes - TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CirclePageIndicator, defStyle, 0); - - mCentered = a.getBoolean(R.styleable.CirclePageIndicator_centered, defaultCentered); - mOrientation = a.getInt(R.styleable.CirclePageIndicator_android_orientation, defaultOrientation); - mPaintPageFill.setStyle(Style.FILL); - mPaintPageFill.setColor(a.getColor(R.styleable.CirclePageIndicator_pageColor, defaultPageColor)); - mPaintStroke.setStyle(Style.STROKE); - mPaintStroke.setColor(a.getColor(R.styleable.CirclePageIndicator_strokeColor, defaultStrokeColor)); - mPaintStroke.setStrokeWidth(a.getDimension(R.styleable.CirclePageIndicator_strokeWidth, defaultStrokeWidth)); - mPaintFill.setStyle(Style.FILL); - mPaintFill.setColor(a.getColor(R.styleable.CirclePageIndicator_fillColor, defaultFillColor)); - mRadius = a.getDimension(R.styleable.CirclePageIndicator_radius, defaultRadius); - mSnap = a.getBoolean(R.styleable.CirclePageIndicator_snap, defaultSnap); - - Drawable background = a.getDrawable(R.styleable.CirclePageIndicator_android_background); - if (background != null) { - setBackgroundDrawable(background); - } - - a.recycle(); - - final ViewConfiguration configuration = ViewConfiguration.get(context); - mTouchSlop = ViewConfigurationCompat.getScaledPagingTouchSlop(configuration); - } - - - public void setCentered(boolean centered) { - mCentered = centered; - invalidate(); - } - - public boolean isCentered() { - return mCentered; - } - - public void setPageColor(int pageColor) { - mPaintPageFill.setColor(pageColor); - invalidate(); - } - - public int getPageColor() { - return mPaintPageFill.getColor(); - } - - public void setFillColor(int fillColor) { - mPaintFill.setColor(fillColor); - invalidate(); - } - - public int getFillColor() { - return mPaintFill.getColor(); - } - - public void setOrientation(int orientation) { - switch (orientation) { - case HORIZONTAL: - case VERTICAL: - mOrientation = orientation; - requestLayout(); - break; - - default: - throw new IllegalArgumentException("Orientation must be either HORIZONTAL or VERTICAL."); - } - } - - public int getOrientation() { - return mOrientation; - } - - public void setStrokeColor(int strokeColor) { - mPaintStroke.setColor(strokeColor); - invalidate(); - } - - public int getStrokeColor() { - return mPaintStroke.getColor(); - } - - public void setStrokeWidth(float strokeWidth) { - mPaintStroke.setStrokeWidth(strokeWidth); - invalidate(); - } - - public float getStrokeWidth() { - return mPaintStroke.getStrokeWidth(); - } - - public void setRadius(float radius) { - mRadius = radius; - invalidate(); - } - - public float getRadius() { - return mRadius; - } - - public void setSnap(boolean snap) { - mSnap = snap; - invalidate(); - } - - public boolean isSnap() { - return mSnap; - } - - @Override - protected void onDraw(Canvas canvas) { - super.onDraw(canvas); - - if (mViewPager == null) { - return; - } - final int count = mViewPager.getAdapter().getCount(); - if (count == 0) { - return; - } - - if (mCurrentPage >= count) { - setCurrentItem(count - 1); - return; - } - - int longSize; - int longPaddingBefore; - int longPaddingAfter; - int shortPaddingBefore; - if (mOrientation == HORIZONTAL) { - longSize = getWidth(); - longPaddingBefore = getPaddingLeft(); - longPaddingAfter = getPaddingRight(); - shortPaddingBefore = getPaddingTop(); - } else { - longSize = getHeight(); - longPaddingBefore = getPaddingTop(); - longPaddingAfter = getPaddingBottom(); - shortPaddingBefore = getPaddingLeft(); - } - - final float threeRadius = mRadius * SEPARATION_FACTOR; - final float shortOffset = shortPaddingBefore + mRadius; - float longOffset = longPaddingBefore + mRadius; - if (mCentered) { - longOffset += ((longSize - longPaddingBefore - longPaddingAfter) / 2.0f) - ((count * threeRadius) / 2.0f); - } - - float dX; - float dY; - - float pageFillRadius = mRadius; - if (mPaintStroke.getStrokeWidth() > 0) { - pageFillRadius -= mPaintStroke.getStrokeWidth() / 2.0f; - } - - //Draw stroked circles - for (int iLoop = 0; iLoop < count; iLoop++) { - float drawLong = longOffset + (iLoop * threeRadius); - if (mOrientation == HORIZONTAL) { - dX = drawLong; - dY = shortOffset; - } else { - dX = shortOffset; - dY = drawLong; - } - // Only paint fill if not completely transparent - if (mPaintPageFill.getAlpha() > 0) { - canvas.drawCircle(dX, dY, pageFillRadius, mPaintPageFill); - } - - // Only paint stroke if a stroke width was non-zero - if (pageFillRadius != mRadius) { - canvas.drawCircle(dX, dY, mRadius, mPaintStroke); - } - } - - //Draw the filled circle according to the current scroll - float cx = (mSnap ? mSnapPage : mCurrentPage) * threeRadius; - if (!mSnap) { - cx += mPageOffset * threeRadius; - } - if (mOrientation == HORIZONTAL) { - dX = longOffset + cx; - dY = shortOffset; - } else { - dX = shortOffset; - dY = longOffset + cx; - } - canvas.drawCircle(dX, dY, mRadius, mPaintFill); - } - - public boolean onTouchEvent(android.view.MotionEvent ev) { - if (super.onTouchEvent(ev)) { - return true; - } - if ((mViewPager == null) || (mViewPager.getAdapter().getCount() == 0)) { - return false; - } - - final int action = ev.getAction() & MotionEventCompat.ACTION_MASK; - switch (action) { - case MotionEvent.ACTION_DOWN: - mActivePointerId = MotionEventCompat.getPointerId(ev, 0); - mLastMotionX = ev.getX(); - break; - - case MotionEvent.ACTION_MOVE: { - final int activePointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId); - final float x = MotionEventCompat.getX(ev, activePointerIndex); - final float deltaX = x - mLastMotionX; - - if (!mIsDragging) { - if (Math.abs(deltaX) > mTouchSlop) { - mIsDragging = true; - } - } - - if (mIsDragging) { - mLastMotionX = x; - if (mViewPager.isFakeDragging() || mViewPager.beginFakeDrag()) { - mViewPager.fakeDragBy(deltaX); - } - } - - break; - } - - case MotionEvent.ACTION_CANCEL: - case MotionEvent.ACTION_UP: - if (!mIsDragging) { - final int count = mViewPager.getAdapter().getCount(); - final int width = getWidth(); - final float halfWidth = width / 2f; - final float sixthWidth = width / 6f; - - if ((mCurrentPage > 0) && (ev.getX() < halfWidth - sixthWidth)) { - if (action != MotionEvent.ACTION_CANCEL) { - mViewPager.setCurrentItem(mCurrentPage - 1); - } - return true; - } else if ((mCurrentPage < count - 1) && (ev.getX() > halfWidth + sixthWidth)) { - if (action != MotionEvent.ACTION_CANCEL) { - mViewPager.setCurrentItem(mCurrentPage + 1); - } - return true; - } - } - - mIsDragging = false; - mActivePointerId = INVALID_POINTER; - if (mViewPager.isFakeDragging()) mViewPager.endFakeDrag(); - break; - - case MotionEventCompat.ACTION_POINTER_DOWN: { - final int index = MotionEventCompat.getActionIndex(ev); - mLastMotionX = MotionEventCompat.getX(ev, index); - mActivePointerId = MotionEventCompat.getPointerId(ev, index); - break; - } - - case MotionEventCompat.ACTION_POINTER_UP: - final int pointerIndex = MotionEventCompat.getActionIndex(ev); - final int pointerId = MotionEventCompat.getPointerId(ev, pointerIndex); - if (pointerId == mActivePointerId) { - final int newPointerIndex = pointerIndex == 0 ? 1 : 0; - mActivePointerId = MotionEventCompat.getPointerId(ev, newPointerIndex); - } - mLastMotionX = MotionEventCompat.getX(ev, MotionEventCompat.findPointerIndex(ev, mActivePointerId)); - break; - } - - return true; - } - - public void setViewPager(ViewPager view) { - if (mViewPager == view) { - return; - } - if (mViewPager != null) { - mViewPager.setOnPageChangeListener(null); - } - if (view.getAdapter() == null) { - throw new IllegalStateException("ViewPager does not have adapter instance."); - } - mViewPager = view; - mViewPager.setOnPageChangeListener(this); - invalidate(); - } - - public void setViewPager(ViewPager view, int initialPosition) { - setViewPager(view); - setCurrentItem(initialPosition); - } - - public void setCurrentItem(int item) { - if (mViewPager == null) { - throw new IllegalStateException("ViewPager has not been bound."); - } - mViewPager.setCurrentItem(item); - mCurrentPage = item; - invalidate(); - } - - public void notifyDataSetChanged() { - invalidate(); - } - - @Override - public void onPageScrollStateChanged(int state) { - mScrollState = state; - - if (mListener != null) { - mListener.onPageScrollStateChanged(state); - } - } - - public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { - mCurrentPage = position; - mPageOffset = positionOffset; - invalidate(); - - if (mListener != null) { - mListener.onPageScrolled(position, positionOffset, positionOffsetPixels); - } - } - - @Override - public void onPageSelected(int position) { - if (mSnap || mScrollState == ViewPager.SCROLL_STATE_IDLE) { - mCurrentPage = position; - mSnapPage = position; - invalidate(); - } - - if (mListener != null) { - mListener.onPageSelected(position); - } - } - - public void setOnPageChangeListener(ViewPager.OnPageChangeListener listener) { - mListener = listener; - } - - /* - * (non-Javadoc) - * - * @see android.view.View#onMeasure(int, int) - */ - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - if (mOrientation == HORIZONTAL) { - setMeasuredDimension(measureLong(widthMeasureSpec), measureShort(heightMeasureSpec)); - } else { - setMeasuredDimension(measureShort(widthMeasureSpec), measureLong(heightMeasureSpec)); - } - } - - /** - * Determines the width of this view - * - * @param measureSpec - * A measureSpec packed into an int - * @return The width of the view, honoring constraints from measureSpec - */ - private int measureLong(int measureSpec) { - int result; - int specMode = MeasureSpec.getMode(measureSpec); - int specSize = MeasureSpec.getSize(measureSpec); - - if ((specMode == MeasureSpec.EXACTLY) || (mViewPager == null)) { - //We were told how big to be - result = specSize; - } else { - //Calculate the width according the views count - final int count = mViewPager.getAdapter().getCount(); - result = (int)(getPaddingLeft() + getPaddingRight() - + (count * 2 * mRadius) + (count - 1) * mRadius + 1); - //Respect AT_MOST value if that was what is called for by measureSpec - if (specMode == MeasureSpec.AT_MOST) { - result = Math.min(result, specSize); - } - } - return result; - } - - /** - * Determines the height of this view - * - * @param measureSpec - * A measureSpec packed into an int - * @return The height of the view, honoring constraints from measureSpec - */ - private int measureShort(int measureSpec) { - int result; - int specMode = MeasureSpec.getMode(measureSpec); - int specSize = MeasureSpec.getSize(measureSpec); - - if (specMode == MeasureSpec.EXACTLY) { - //We were told how big to be - result = specSize; - } else { - //Measure the height - result = (int)(2 * mRadius + getPaddingTop() + getPaddingBottom() + 1); - //Respect AT_MOST value if that was what is called for by measureSpec - if (specMode == MeasureSpec.AT_MOST) { - result = Math.min(result, specSize); - } - } - return result; - } - - @Override - public void onRestoreInstanceState(Parcelable state) { - SavedState savedState = (SavedState)state; - super.onRestoreInstanceState(savedState.getSuperState()); - mCurrentPage = savedState.currentPage; - mSnapPage = savedState.currentPage; - requestLayout(); - } - - @Override - public Parcelable onSaveInstanceState() { - Parcelable superState = super.onSaveInstanceState(); - SavedState savedState = new SavedState(superState); - savedState.currentPage = mCurrentPage; - return savedState; - } - - static class SavedState extends BaseSavedState { - int currentPage; - - public SavedState(Parcelable superState) { - super(superState); - } - - private SavedState(Parcel in) { - super(in); - currentPage = in.readInt(); - } - - @Override - public void writeToParcel(Parcel dest, int flags) { - super.writeToParcel(dest, flags); - dest.writeInt(currentPage); - } - - @SuppressWarnings("UnusedDeclaration") - public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { - @Override - public SavedState createFromParcel(Parcel in) { - return new SavedState(in); - } - - @Override - public SavedState[] newArray(int size) { - return new SavedState[size]; - } - }; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/activitystream/topsites/TopSitesCard.java b/mobile/android/base/java/org/mozilla/gecko/home/activitystream/topsites/TopSitesCard.java deleted file mode 100644 index b436a466f7..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/activitystream/topsites/TopSitesCard.java +++ /dev/null @@ -1,105 +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.activitystream.topsites; - -import android.graphics.Color; -import android.support.v7.widget.RecyclerView; -import android.view.View; -import android.widget.FrameLayout; -import android.widget.ImageView; -import android.widget.TextView; - -import org.mozilla.gecko.R; -import org.mozilla.gecko.activitystream.ActivityStream; -import org.mozilla.gecko.home.HomePager; -import org.mozilla.gecko.home.activitystream.menu.ActivityStreamContextMenu; -import org.mozilla.gecko.icons.IconCallback; -import org.mozilla.gecko.icons.IconResponse; -import org.mozilla.gecko.icons.Icons; -import org.mozilla.gecko.util.DrawableUtil; -import org.mozilla.gecko.util.ViewUtil; -import org.mozilla.gecko.util.TouchTargetUtil; -import org.mozilla.gecko.widget.FaviconView; - -import java.util.EnumSet; -import java.util.concurrent.Future; - -class TopSitesCard extends RecyclerView.ViewHolder - implements IconCallback, View.OnClickListener { - private final FaviconView faviconView; - - private final TextView title; - private final ImageView menuButton; - private Future ongoingIconLoad; - - private String url; - - private final HomePager.OnUrlOpenListener onUrlOpenListener; - private final HomePager.OnUrlOpenInBackgroundListener onUrlOpenInBackgroundListener; - - public TopSitesCard(FrameLayout card, final HomePager.OnUrlOpenListener onUrlOpenListener, final HomePager.OnUrlOpenInBackgroundListener onUrlOpenInBackgroundListener) { - super(card); - - faviconView = (FaviconView) card.findViewById(R.id.favicon); - - title = (TextView) card.findViewById(R.id.title); - menuButton = (ImageView) card.findViewById(R.id.menu); - - this.onUrlOpenListener = onUrlOpenListener; - this.onUrlOpenInBackgroundListener = onUrlOpenInBackgroundListener; - - card.setOnClickListener(this); - - TouchTargetUtil.ensureTargetHitArea(menuButton, card); - menuButton.setOnClickListener(this); - - ViewUtil.enableTouchRipple(menuButton); - } - - void bind(final TopSitesPageAdapter.TopSite topSite) { - ActivityStream.extractLabel(itemView.getContext(), topSite.url, true, new ActivityStream.LabelCallback() { - @Override - public void onLabelExtracted(String label) { - title.setText(label); - } - }); - - this.url = topSite.url; - - if (ongoingIconLoad != null) { - ongoingIconLoad.cancel(true); - } - - ongoingIconLoad = Icons.with(itemView.getContext()) - .pageUrl(topSite.url) - .skipNetwork() - .build() - .execute(this); - } - - @Override - public void onIconResponse(IconResponse response) { - faviconView.updateImage(response); - - final int tintColor = !response.hasColor() || response.getColor() == Color.WHITE ? Color.LTGRAY : Color.WHITE; - - menuButton.setImageDrawable( - DrawableUtil.tintDrawable(menuButton.getContext(), R.drawable.menu, tintColor)); - } - - @Override - public void onClick(View clickedView) { - if (clickedView == itemView) { - onUrlOpenListener.onUrlOpen(url, EnumSet.noneOf(HomePager.OnUrlOpenListener.Flags.class)); - } else if (clickedView == menuButton) { - ActivityStreamContextMenu.show(clickedView.getContext(), - menuButton, - ActivityStreamContextMenu.MenuMode.TOPSITE, - title.getText().toString(), url, - onUrlOpenListener, onUrlOpenInBackgroundListener, - faviconView.getWidth(), faviconView.getHeight()); - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/activitystream/topsites/TopSitesPage.java b/mobile/android/base/java/org/mozilla/gecko/home/activitystream/topsites/TopSitesPage.java deleted file mode 100644 index 45fdc0d1a2..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/activitystream/topsites/TopSitesPage.java +++ /dev/null @@ -1,38 +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.activitystream.topsites; - -import android.content.Context; -import android.support.annotation.Nullable; -import android.support.v7.widget.GridLayoutManager; -import android.support.v7.widget.RecyclerView; -import android.util.AttributeSet; -import android.view.View; - -import org.mozilla.gecko.home.HomePager; -import org.mozilla.gecko.widget.RecyclerViewClickSupport; - -import java.util.EnumSet; - -public class TopSitesPage - extends RecyclerView { - public TopSitesPage(Context context, - @Nullable AttributeSet attrs) { - super(context, attrs); - - setLayoutManager(new GridLayoutManager(context, 1)); - } - - public void setTiles(int tiles) { - setLayoutManager(new GridLayoutManager(getContext(), tiles)); - } - - private HomePager.OnUrlOpenListener onUrlOpenListener; - private HomePager.OnUrlOpenInBackgroundListener onUrlOpenInBackgroundListener; - - public TopSitesPageAdapter getAdapter() { - return (TopSitesPageAdapter) super.getAdapter(); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/activitystream/topsites/TopSitesPageAdapter.java b/mobile/android/base/java/org/mozilla/gecko/home/activitystream/topsites/TopSitesPageAdapter.java deleted file mode 100644 index 29e6aca3dd..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/activitystream/topsites/TopSitesPageAdapter.java +++ /dev/null @@ -1,117 +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.activitystream.topsites; - -import android.content.Context; -import android.database.Cursor; -import android.support.annotation.UiThread; -import android.support.v7.widget.RecyclerView; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.FrameLayout; - -import org.mozilla.gecko.R; -import org.mozilla.gecko.db.BrowserContract; -import org.mozilla.gecko.home.HomePager; - -import java.util.ArrayList; -import java.util.List; - -public class TopSitesPageAdapter extends RecyclerView.Adapter { - static final class TopSite { - public final long id; - public final String url; - public final String title; - - TopSite(long id, String url, String title) { - this.id = id; - this.url = url; - this.title = title; - } - } - - private List topSites; - private int tiles; - private int tilesWidth; - private int tilesHeight; - private int textHeight; - - private final HomePager.OnUrlOpenListener onUrlOpenListener; - private final HomePager.OnUrlOpenInBackgroundListener onUrlOpenInBackgroundListener; - - public TopSitesPageAdapter(Context context, int tiles, int tilesWidth, int tilesHeight, - HomePager.OnUrlOpenListener onUrlOpenListener, HomePager.OnUrlOpenInBackgroundListener onUrlOpenInBackgroundListener) { - setHasStableIds(true); - - this.topSites = new ArrayList<>(); - this.tiles = tiles; - this.tilesWidth = tilesWidth; - this.tilesHeight = tilesHeight; - this.textHeight = context.getResources().getDimensionPixelSize(R.dimen.activity_stream_top_sites_text_height); - - this.onUrlOpenListener = onUrlOpenListener; - this.onUrlOpenInBackgroundListener = onUrlOpenInBackgroundListener; - } - - /** - * - * @param cursor - * @param startIndex The first item that this topsites group should show. This item, and the following - * 3 items will be displayed by this adapter. - */ - public void swapCursor(Cursor cursor, int startIndex) { - topSites.clear(); - - if (cursor == null) { - return; - } - - for (int i = 0; i < tiles && startIndex + i < cursor.getCount(); i++) { - cursor.moveToPosition(startIndex + i); - - // The Combined View only contains pages that have been visited at least once, i.e. any - // page in the TopSites query will contain a HISTORY_ID. _ID however will be 0 for all rows. - final long id = cursor.getLong(cursor.getColumnIndexOrThrow(BrowserContract.Combined.HISTORY_ID)); - final String url = cursor.getString(cursor.getColumnIndexOrThrow(BrowserContract.Combined.URL)); - final String title = cursor.getString(cursor.getColumnIndexOrThrow(BrowserContract.Combined.TITLE)); - - topSites.add(new TopSite(id, url, title)); - } - - notifyDataSetChanged(); - } - - @Override - public void onBindViewHolder(TopSitesCard holder, int position) { - holder.bind(topSites.get(position)); - } - - @Override - public TopSitesCard onCreateViewHolder(ViewGroup parent, int viewType) { - final LayoutInflater inflater = LayoutInflater.from(parent.getContext()); - - final FrameLayout card = (FrameLayout) inflater.inflate(R.layout.activity_stream_topsites_card, parent, false); - final View content = card.findViewById(R.id.content); - - ViewGroup.LayoutParams layoutParams = content.getLayoutParams(); - layoutParams.width = tilesWidth; - layoutParams.height = tilesHeight + textHeight; - content.setLayoutParams(layoutParams); - - return new TopSitesCard(card, onUrlOpenListener, onUrlOpenInBackgroundListener); - } - - @Override - public int getItemCount() { - return topSites.size(); - } - - @Override - @UiThread - public long getItemId(int position) { - return topSites.get(position).id; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/activitystream/topsites/TopSitesPagerAdapter.java b/mobile/android/base/java/org/mozilla/gecko/home/activitystream/topsites/TopSitesPagerAdapter.java deleted file mode 100644 index dc824d9029..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/activitystream/topsites/TopSitesPagerAdapter.java +++ /dev/null @@ -1,124 +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.activitystream.topsites; - -import android.content.Context; -import android.database.Cursor; -import android.support.v4.view.PagerAdapter; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; - -import org.mozilla.gecko.R; -import org.mozilla.gecko.home.HomePager; - -import java.util.LinkedList; - -/** - * The primary / top-level TopSites adapter: it handles the ViewPager, and also handles - * all lower-level Adapters that populate the individual topsite items. - */ -public class TopSitesPagerAdapter extends PagerAdapter { - public static final int PAGES = 4; - - private int tiles; - private int tilesWidth; - private int tilesHeight; - - private LinkedList pages = new LinkedList<>(); - - private final Context context; - private final HomePager.OnUrlOpenListener onUrlOpenListener; - private final HomePager.OnUrlOpenInBackgroundListener onUrlOpenInBackgroundListener; - - private int count = 0; - - public TopSitesPagerAdapter(Context context, - HomePager.OnUrlOpenListener onUrlOpenListener, - HomePager.OnUrlOpenInBackgroundListener onUrlOpenInBackgroundListener) { - this.context = context; - this.onUrlOpenListener = onUrlOpenListener; - this.onUrlOpenInBackgroundListener = onUrlOpenInBackgroundListener; - } - - public void setTilesSize(int tiles, int tilesWidth, int tilesHeight) { - this.tilesWidth = tilesWidth; - this.tilesHeight = tilesHeight; - this.tiles = tiles; - } - - @Override - public int getCount() { - return Math.min(count, 4); - } - - @Override - public boolean isViewFromObject(View view, Object object) { - return view == object; - } - - @Override - public Object instantiateItem(ViewGroup container, int position) { - TopSitesPage page = pages.get(position); - - container.addView(page); - - return page; - } - - @Override - public int getItemPosition(Object object) { - return PagerAdapter.POSITION_NONE; - } - - @Override - public void destroyItem(ViewGroup container, int position, Object object) { - container.removeView((View) object); - } - - public void swapCursor(Cursor cursor) { - // Divide while rounding up: 0 items = 0 pages, 1-ITEMS_PER_PAGE items = 1 page, etc. - if (cursor != null) { - count = (cursor.getCount() - 1) / tiles + 1; - } else { - count = 0; - } - - pages.clear(); - final int pageDelta = count; - - if (pageDelta > 0) { - final LayoutInflater inflater = LayoutInflater.from(context); - for (int i = 0; i < pageDelta; i++) { - final TopSitesPage page = (TopSitesPage) inflater.inflate(R.layout.activity_stream_topsites_page, null, false); - - page.setTiles(tiles); - final TopSitesPageAdapter adapter = new TopSitesPageAdapter(context, tiles, tilesWidth, tilesHeight, - onUrlOpenListener, onUrlOpenInBackgroundListener); - page.setAdapter(adapter); - pages.add(page); - } - } else if (pageDelta < 0) { - for (int i = 0; i > pageDelta; i--) { - final TopSitesPage page = pages.getLast(); - - // Ensure the page doesn't use the old/invalid cursor anymore - page.getAdapter().swapCursor(null, 0); - - pages.removeLast(); - } - } else { - // do nothing: we will be updating all the pages below - } - - int startIndex = 0; - for (TopSitesPage page : pages) { - page.getAdapter().swapCursor(cursor, startIndex); - startIndex += tiles; - } - - notifyDataSetChanged(); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/icons/IconCallback.java b/mobile/android/base/java/org/mozilla/gecko/icons/IconCallback.java deleted file mode 100644 index 0232a4ea6f..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/icons/IconCallback.java +++ /dev/null @@ -1,13 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.icons; - -/** - * Interface for a callback that will be executed once an icon has been loaded successfully. - */ -public interface IconCallback { - void onIconResponse(IconResponse response); -} diff --git a/mobile/android/base/java/org/mozilla/gecko/icons/IconDescriptor.java b/mobile/android/base/java/org/mozilla/gecko/icons/IconDescriptor.java deleted file mode 100644 index 359c47e53a..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/icons/IconDescriptor.java +++ /dev/null @@ -1,96 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.icons; - -import android.support.annotation.IntDef; -import android.support.annotation.Nullable; -import android.support.annotation.VisibleForTesting; - -/** - * A class describing the location and properties of an icon that can be loaded. - */ -public class IconDescriptor { - @IntDef({ TYPE_GENERIC, TYPE_FAVICON, TYPE_TOUCHICON, TYPE_LOOKUP }) - @interface IconType {} - - // The type values are used for ranking icons (higher values = try to load first). - @VisibleForTesting static final int TYPE_GENERIC = 0; - @VisibleForTesting static final int TYPE_LOOKUP = 1; - @VisibleForTesting static final int TYPE_FAVICON = 5; - @VisibleForTesting static final int TYPE_TOUCHICON = 10; - - private final String url; - private final int size; - private final String mimeType; - private final int type; - - /** - * Create a generic icon located at the given URL. No MIME type or size is known. - */ - public static IconDescriptor createGenericIcon(String url) { - return new IconDescriptor(TYPE_GENERIC, url, 0, null); - } - - /** - * Create a favicon located at the given URL and with a known size and MIME type. - */ - public static IconDescriptor createFavicon(String url, int size, String mimeType) { - return new IconDescriptor(TYPE_FAVICON, url, size, mimeType); - } - - /** - * Create a touch icon located at the given URL and with a known MIME type and size. - */ - public static IconDescriptor createTouchicon(String url, int size, String mimeType) { - return new IconDescriptor(TYPE_TOUCHICON, url, size, mimeType); - } - - /** - * Create an icon located at an URL that has been returned from a disk or memory storage. This - * is an icon with an URL we loaded an icon from previously. Therefore we give it a little higher - * ranking than a generic icon - even though we do not know the MIME type or size of the icon. - */ - public static IconDescriptor createLookupIcon(String url) { - return new IconDescriptor(TYPE_LOOKUP, url, 0, null); - } - - private IconDescriptor(@IconType int type, String url, int size, String mimeType) { - this.type = type; - this.url = url; - this.size = size; - this.mimeType = mimeType; - } - - /** - * Get the URL of the icon. - */ - public String getUrl() { - return url; - } - - /** - * Get the (assumed) size of the icon. Returns 0 if no size is known. - */ - public int getSize() { - return size; - } - - /** - * Get the type of the icon (favicon, touch icon, generic, lookup). - */ - @IconType - public int getType() { - return type; - } - - /** - * Get the (assumed) MIME type of the icon. Returns null if no MIME type is known. - */ - @Nullable - public String getMimeType() { - return mimeType; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/icons/IconDescriptorComparator.java b/mobile/android/base/java/org/mozilla/gecko/icons/IconDescriptorComparator.java deleted file mode 100644 index 3c6064825a..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/icons/IconDescriptorComparator.java +++ /dev/null @@ -1,67 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.icons; - -import java.util.Comparator; - -/** - * This comparator implementation compares IconDescriptor objects in order to determine which icon - * to load first. - * - * In general this comparator will try touch icons before favicons (they usually have a higher resolution) - * and prefers larger icons over smaller ones. - */ -/* package-private */ class IconDescriptorComparator implements Comparator { - @Override - public int compare(final IconDescriptor lhs, final IconDescriptor rhs) { - if (lhs.getUrl().equals(rhs.getUrl())) { - // Two descriptors pointing to the same URL are always referencing the same icon. So treat - // them as equal. - return 0; - } - - // First compare the types. We prefer touch icons because they tend to have a higher resolution - // than ordinary favicons. - if (lhs.getType() != rhs.getType()) { - return compareType(lhs, rhs); - } - - // If one of them is larger than pick the larger icon. - if (lhs.getSize() != rhs.getSize()) { - return compareSizes(lhs, rhs); - } - - // If there's no other way to choose, we prefer container types. They *might* contain - // an image larger than the size given in the tag. - final boolean lhsContainer = IconsHelper.isContainerType(lhs.getMimeType()); - final boolean rhsContainer = IconsHelper.isContainerType(rhs.getMimeType()); - - if (lhsContainer != rhsContainer) { - return lhsContainer ? -1 : 1; - } - - // There's no way to know which icon might be better. However we need to pick a consistent - // one to avoid breaking the TreeSet implementation (See Bug 1331808). Therefore we are - // picking one by just comparing the URLs. - return lhs.getUrl().compareTo(rhs.getUrl()); - } - - private int compareType(IconDescriptor lhs, IconDescriptor rhs) { - if (lhs.getType() > rhs.getType()) { - return -1; - } else { - return 1; - } - } - - private int compareSizes(IconDescriptor lhs, IconDescriptor rhs) { - if (lhs.getSize() > rhs.getSize()) { - return -1; - } else { - return 1; - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/icons/IconRequest.java b/mobile/android/base/java/org/mozilla/gecko/icons/IconRequest.java deleted file mode 100644 index be000642ed..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/icons/IconRequest.java +++ /dev/null @@ -1,181 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.icons; - -import android.content.Context; -import android.support.annotation.VisibleForTesting; - -import org.mozilla.gecko.R; - -import java.util.Iterator; -import java.util.TreeSet; -import java.util.concurrent.Future; - -/** - * A class describing a request to load an icon for a website. - */ -public class IconRequest { - private Context context; - - // Those values are written by the IconRequestBuilder class. - /* package-private */ String pageUrl; - /* package-private */ boolean privileged; - /* package-private */ TreeSet icons; - /* package-private */ boolean skipNetwork; - /* package-private */ boolean backgroundThread; - /* package-private */ boolean skipDisk; - /* package-private */ boolean skipMemory; - /* package-private */ int targetSize; - /* package-private */ boolean prepareOnly; - private IconCallback callback; - - /* package-private */ IconRequest(Context context) { - this.context = context.getApplicationContext(); - this.icons = new TreeSet<>(new IconDescriptorComparator()); - - // Setting some sensible defaults. - this.privileged = false; - this.skipMemory = false; - this.skipDisk = false; - this.skipNetwork = false; - this.targetSize = context.getResources().getDimensionPixelSize(R.dimen.favicon_bg); - this.prepareOnly = false; - } - - /** - * Execute this request and try to load an icon. Once an icon has been loaded successfully the - * callback will be executed. - * - * The returned Future can be used to cancel the job. - */ - public Future execute(IconCallback callback) { - setCallback(callback); - - return IconRequestExecutor.submit(this); - } - - @VisibleForTesting void setCallback(IconCallback callback) { - this.callback = callback; - } - - /** - * Get the (application) context associated with this request. - */ - public Context getContext() { - return context; - } - - /** - * Get the descriptor for the potentially best icon. This is the icon that should be loaded if - * possible. - */ - public IconDescriptor getBestIcon() { - return icons.first(); - } - - /** - * Get the URL of the page for which an icon should be loaded. - */ - public String getPageUrl() { - return pageUrl; - } - - /** - * Is this request allowed to load icons from internal data sources like the omni.ja? - */ - public boolean isPrivileged() { - return privileged; - } - - /** - * Get the number of icon descriptors associated with this request. - */ - public int getIconCount() { - return icons.size(); - } - - /** - * Get the required target size of the icon. - */ - public int getTargetSize() { - return targetSize; - } - - /** - * Should a loader access the network to load this icon? - */ - public boolean shouldSkipNetwork() { - return skipNetwork; - } - - /** - * Should a loader access the disk to load this icon? - */ - public boolean shouldSkipDisk() { - return skipDisk; - } - - /** - * Should a loader access the memory cache to load this icon? - */ - public boolean shouldSkipMemory() { - return skipMemory; - } - - /** - * Get an iterator to iterate over all icon descriptors associated with this request. - */ - public Iterator getIconIterator() { - return icons.iterator(); - } - - /** - * Create a builder to modify this request. - * - * Calling methods on the builder will modify this object and not create a copy. - */ - public IconRequestBuilder modify() { - return new IconRequestBuilder(this); - } - - /** - * Should the callback be executed on a background thread? By default a callback is always - * executed on the UI thread because an icon is usually loaded in order to display it somewhere - * in the UI. - */ - /* package-private */ boolean shouldRunOnBackgroundThread() { - return backgroundThread; - } - - /* package-private */ IconCallback getCallback() { - return callback; - } - - /* package-private */ boolean hasIconDescriptors() { - return !icons.isEmpty(); - } - - /** - * Move to the next icon. This method is called after all loaders for the current best icon - * have failed. After calling this method getBestIcon() will return the next icon to try. - * hasIconDescriptors() should be called before requesting the next icon. - */ - /* package-private */ void moveToNextIcon() { - if (!icons.remove(getBestIcon())) { - // Calling this method when there's no next icon is an error (use hasIconDescriptors()). - // Theoretically this method can fail even if there's a next icon (like it did in bug 1331808). - // In this case crashing to see and fix the issue is desired. - throw new IllegalStateException("Moving to next icon failed. Could not remove first icon from set."); - } - } - - /** - * Should this request be prepared but not actually load an icon? - */ - /* package-private */ boolean shouldPrepareOnly() { - return prepareOnly; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/icons/IconRequestBuilder.java b/mobile/android/base/java/org/mozilla/gecko/icons/IconRequestBuilder.java deleted file mode 100644 index d9fd9ec5af..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/icons/IconRequestBuilder.java +++ /dev/null @@ -1,143 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.icons; - -import android.content.Context; -import android.support.annotation.CheckResult; - -import org.mozilla.gecko.GeckoAppShell; - -import ch.boye.httpclientandroidlib.util.TextUtils; - -/** - * Builder for creating a request to load an icon. - */ -public class IconRequestBuilder { - private final IconRequest request; - - /* package-private */ IconRequestBuilder(Context context) { - this(new IconRequest(context)); - } - - /* package-private */ IconRequestBuilder(IconRequest request) { - this.request = request; - } - - /** - * Set the URL of the page for which the icon should be loaded. - */ - @CheckResult - public IconRequestBuilder pageUrl(String pageUrl) { - request.pageUrl = pageUrl; - return this; - } - - /** - * Set whether this request is allowed to load icons from non http(s) URLs (e.g. the omni.ja). - * - * For example web content referencing internal URLs should not lead to us loading icons from - * internal data structures like the omni.ja. - */ - @CheckResult - public IconRequestBuilder privileged(boolean privileged) { - request.privileged = privileged; - return this; - } - - /** - * Add an icon descriptor describing the location and properties of an icon. All descriptors - * will be ranked and tried in order of their rank. Executing the request will modify the list - * of icons (filter or add additional descriptors). - */ - @CheckResult - public IconRequestBuilder icon(IconDescriptor descriptor) { - request.icons.add(descriptor); - return this; - } - - /** - * Skip the network and do not load an icon from a network connection. - */ - @CheckResult - public IconRequestBuilder skipNetwork() { - request.skipNetwork = true; - return this; - } - - /** - * Skip the disk cache and do not load an icon from disk. - */ - @CheckResult - public IconRequestBuilder skipDisk() { - request.skipDisk = true; - return this; - } - - /** - * Skip the memory cache and do not return a previously loaded icon. - */ - @CheckResult - public IconRequestBuilder skipMemory() { - request.skipMemory = true; - return this; - } - - /** - * The icon will be used as (Android) launcher icon. The loaded icon will be scaled to the - * preferred Android launcher icon size. - */ - public IconRequestBuilder forLauncherIcon() { - request.targetSize = GeckoAppShell.getPreferredIconSize(); - return this; - } - - /** - * Execute the callback on the background thread. By default the callback is always executed on - * the UI thread in order to add the loaded icon to a view easily. - */ - @CheckResult - public IconRequestBuilder executeCallbackOnBackgroundThread() { - request.backgroundThread = true; - return this; - } - - /** - * When executing the request then only prepare executing it but do not actually load an icon. - * This mode is only used for some legacy code that uses the icon URL and therefore needs to - * perform a lookup of the URL but doesn't want to load the icon yet. - */ - public IconRequestBuilder prepareOnly() { - request.prepareOnly = true; - return this; - } - - /** - * Return the request built with this builder. - */ - @CheckResult - public IconRequest build() { - if (TextUtils.isEmpty(request.pageUrl)) { - throw new IllegalStateException("Page URL is required"); - } - - return request; - } - - /** - * This is a no-op method. - * - * All builder methods are annotated with @CheckResult to denote that the - * methods return the builder object and that it is typically an error to not call another method - * on it until eventually calling build(). - * - * However in some situations code can keep a reference - * to the builder object and call methods only when a specific event occurs. To make this explicit - * and avoid lint errors this method can be called. - */ - public void deferBuild() { - // No op - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/icons/IconRequestExecutor.java b/mobile/android/base/java/org/mozilla/gecko/icons/IconRequestExecutor.java deleted file mode 100644 index aad7849808..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/icons/IconRequestExecutor.java +++ /dev/null @@ -1,152 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.icons; - -import android.support.annotation.NonNull; - -import org.mozilla.gecko.icons.loader.ContentProviderLoader; -import org.mozilla.gecko.icons.loader.DataUriLoader; -import org.mozilla.gecko.icons.loader.DiskLoader; -import org.mozilla.gecko.icons.loader.IconDownloader; -import org.mozilla.gecko.icons.loader.IconGenerator; -import org.mozilla.gecko.icons.loader.IconLoader; -import org.mozilla.gecko.icons.loader.JarLoader; -import org.mozilla.gecko.icons.loader.LegacyLoader; -import org.mozilla.gecko.icons.loader.MemoryLoader; -import org.mozilla.gecko.icons.preparation.AboutPagesPreparer; -import org.mozilla.gecko.icons.preparation.AddDefaultIconUrl; -import org.mozilla.gecko.icons.preparation.FilterKnownFailureUrls; -import org.mozilla.gecko.icons.preparation.FilterMimeTypes; -import org.mozilla.gecko.icons.preparation.FilterPrivilegedUrls; -import org.mozilla.gecko.icons.preparation.LookupIconUrl; -import org.mozilla.gecko.icons.preparation.Preparer; -import org.mozilla.gecko.icons.processing.ColorProcessor; -import org.mozilla.gecko.icons.processing.DiskProcessor; -import org.mozilla.gecko.icons.processing.MemoryProcessor; -import org.mozilla.gecko.icons.processing.Processor; -import org.mozilla.gecko.icons.processing.ResizingProcessor; - -import java.util.Arrays; -import java.util.List; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Future; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.ThreadFactory; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.TimeUnit; - -/** - * Executor for icon requests. - */ -/* package-private */ class IconRequestExecutor { - /** - * Loader implementation that generates an icon if none could be loaded. - */ - private static final IconLoader GENERATOR = new IconGenerator(); - - /** - * Ordered list of prepares that run before any icon is loaded. - */ - private static final List PREPARERS = Arrays.asList( - // First we look into our memory and disk caches if there are some known icon URLs for - // the page URL of the request. - new LookupIconUrl(), - - // For all icons with MIME type we filter entries with unknown MIME type that we probably - // cannot decode anyways. - new FilterMimeTypes(), - - // If this is not a request that is allowed to load icons from privileged locations (omni.jar) - // then filter such icon URLs. - new FilterPrivilegedUrls(), - - // This preparer adds an icon URL for about pages. It's added after the filter for privileged - // URLs. We always want to be able to load those specific icons. - new AboutPagesPreparer(), - - // Add the default favicon URL (*/favicon.ico) to the list of icon URLs; with a low priority, - // this icon URL should be tried last. - new AddDefaultIconUrl(), - - // Finally we filter all URLs that failed to load recently (4xx / 5xx errors). - new FilterKnownFailureUrls() - ); - - /** - * Ordered list of loaders. If a loader returns a response object then subsequent loaders are not run. - */ - private static final List LOADERS = Arrays.asList( - // First we try to load an icon that is already in the memory. That's cheap. - new MemoryLoader(), - - // Try to decode the icon if it is a data: URI. - new DataUriLoader(), - - // Try to load the icon from the omni.ha if it's a jar:jar URI. - new JarLoader(), - - // Try to load the icon from a content provider (if applicable). - new ContentProviderLoader(), - - // Try to load the icon from the disk cache. - new DiskLoader(), - - // If the icon is not in any of our cashes and can't be decoded then look into the - // database (legacy). Maybe this icon was loaded before the new code was deployed. - new LegacyLoader(), - - // Download the icon from the web. - new IconDownloader() - ); - - /** - * Ordered list of processors that run after an icon has been loaded. - */ - private static final List PROCESSORS = Arrays.asList( - // Store the icon (and mapping) in the disk cache if needed - new DiskProcessor(), - - // Resize the icon to match the target size (if possible) - new ResizingProcessor(), - - // Extract the dominant color from the icon - new ColorProcessor(), - - // Store the icon in the memory cache - new MemoryProcessor() - ); - - private static final ExecutorService EXECUTOR; - static { - final ThreadFactory factory = new ThreadFactory() { - @Override - public Thread newThread(@NonNull Runnable runnable) { - Thread thread = new Thread(runnable, "GeckoIconTask"); - thread.setDaemon(false); - thread.setPriority(Thread.NORM_PRIORITY); - return thread; - } - }; - - // Single thread executor - EXECUTOR = new ThreadPoolExecutor( - 1, /* corePoolSize */ - 1, /* maximumPoolSize */ - 0L, /* keepAliveTime */ - TimeUnit.MILLISECONDS, - new LinkedBlockingQueue(), - factory); - } - - /** - * Submit the request for execution. - */ - /* package-private */ static Future submit(IconRequest request) { - return EXECUTOR.submit( - new IconTask(request, PREPARERS, LOADERS, PROCESSORS, GENERATOR) - ); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/icons/IconResponse.java b/mobile/android/base/java/org/mozilla/gecko/icons/IconResponse.java deleted file mode 100644 index 726619eb9a..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/icons/IconResponse.java +++ /dev/null @@ -1,167 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.icons; - -import android.graphics.Bitmap; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.text.TextUtils; - -/** - * Response object containing a successful loaded icon and meta data. - */ -public class IconResponse { - /** - * Create a response for a plain bitmap. - */ - public static IconResponse create(@NonNull Bitmap bitmap) { - return new IconResponse(bitmap); - } - - /** - * Create a response for a bitmap that has been loaded from the network by requesting a specific URL. - */ - public static IconResponse createFromNetwork(@NonNull Bitmap bitmap, @NonNull String url) { - final IconResponse response = new IconResponse(bitmap); - response.url = url; - response.fromNetwork = true; - return response; - } - - /** - * Create a response for a generated bitmap with a dominant color. - */ - public static IconResponse createGenerated(@NonNull Bitmap bitmap, int color) { - final IconResponse response = new IconResponse(bitmap); - response.color = color; - response.generated = true; - return response; - } - - /** - * Create a response for a bitmap that has been loaded from the memory cache. - */ - public static IconResponse createFromMemory(@NonNull Bitmap bitmap, @NonNull String url, int color) { - final IconResponse response = new IconResponse(bitmap); - response.url = url; - response.color = color; - response.fromMemory = true; - return response; - } - - /** - * Create a response for a bitmap that has been loaded from the disk cache. - */ - public static IconResponse createFromDisk(@NonNull Bitmap bitmap, @NonNull String url) { - final IconResponse response = new IconResponse(bitmap); - response.url = url; - response.fromDisk = true; - return response; - } - - private Bitmap bitmap; - private int color; - private boolean fromNetwork; - private boolean fromMemory; - private boolean fromDisk; - private boolean generated; - private String url; - - private IconResponse(Bitmap bitmap) { - if (bitmap == null) { - throw new NullPointerException("Bitmap is null"); - } - - this.bitmap = bitmap; - this.color = 0; - this.url = null; - this.fromNetwork = false; - this.fromMemory = false; - this.fromDisk = false; - this.generated = false; - } - - /** - * Get the icon bitmap. This method will always return a bitmap. - */ - @NonNull - public Bitmap getBitmap() { - return bitmap; - } - - /** - * Get the dominant color of the icon. Will return 0 if no color could be extracted. - */ - public int getColor() { - return color; - } - - /** - * Does this response contain a dominant color? - */ - public boolean hasColor() { - return color != 0; - } - - /** - * Has this icon been loaded from the network? - */ - public boolean isFromNetwork() { - return fromNetwork; - } - - /** - * Has this icon been generated? - */ - public boolean isGenerated() { - return generated; - } - - /** - * Has this icon been loaded from memory (cache)? - */ - public boolean isFromMemory() { - return fromMemory; - } - - /** - * Has this icon been loaded from disk (cache)? - */ - public boolean isFromDisk() { - return fromDisk; - } - - /** - * Get the URL this icon has been loaded from. - */ - @Nullable - public String getUrl() { - return url; - } - - /** - * Does this response contain an URL from which the icon has been loaded? - */ - public boolean hasUrl() { - return !TextUtils.isEmpty(url); - } - - /** - * Update the color of this response. This method is called by processors updating meta data - * after the icon has been loaded. - */ - public void updateColor(int color) { - this.color = color; - } - - /** - * Update the bitmap of this response. This method is called by processors that modify the - * loaded icon. - */ - public void updateBitmap(Bitmap bitmap) { - this.bitmap = bitmap; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/icons/IconTask.java b/mobile/android/base/java/org/mozilla/gecko/icons/IconTask.java deleted file mode 100644 index 411a31980f..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/icons/IconTask.java +++ /dev/null @@ -1,222 +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.icons; - -import android.graphics.Bitmap; -import android.support.annotation.NonNull; -import android.util.Log; - -import org.mozilla.gecko.AppConstants; -import org.mozilla.gecko.icons.loader.IconLoader; -import org.mozilla.gecko.icons.preparation.Preparer; -import org.mozilla.gecko.icons.processing.Processor; -import org.mozilla.gecko.util.ThreadUtils; - -import java.util.List; -import java.util.concurrent.Callable; - -/** - * Task that will be run by the IconRequestExecutor for every icon request. - */ -/* package-private */ class IconTask implements Callable { - private static final String LOGTAG = "Gecko/IconTask"; - private static final boolean DEBUG = false; - - private final List preparers; - private final List loaders; - private final List processors; - private final IconLoader generator; - private final IconRequest request; - - /* package-private */ IconTask( - @NonNull IconRequest request, - @NonNull List preparers, - @NonNull List loaders, - @NonNull List processors, - @NonNull IconLoader generator) { - this.request = request; - this.preparers = preparers; - this.loaders = loaders; - this.processors = processors; - this.generator = generator; - } - - @Override - public IconResponse call() { - try { - logRequest(request); - - prepareRequest(request); - - if (request.shouldPrepareOnly()) { - // This request should only be prepared but not load an actual icon. - return null; - } - - final IconResponse response = loadIcon(request); - - if (response != null) { - processIcon(request, response); - executeCallback(request, response); - - logResponse(response); - - return response; - } - } catch (InterruptedException e) { - Log.d(LOGTAG, "IconTask was interrupted", e); - - // Clear interrupt thread. - Thread.interrupted(); - } catch (Throwable e) { - handleException(e); - } - - return null; - } - - /** - * Check if this thread was interrupted (e.g. this task was cancelled). Throws an InterruptedException - * to stop executing the task in this case. - */ - private void ensureNotInterrupted() throws InterruptedException { - if (Thread.currentThread().isInterrupted()) { - throw new InterruptedException("Task has been cancelled"); - } - } - - private void executeCallback(IconRequest request, final IconResponse response) { - final IconCallback callback = request.getCallback(); - - if (callback != null) { - if (request.shouldRunOnBackgroundThread()) { - ThreadUtils.postToBackgroundThread(new Runnable() { - @Override - public void run() { - callback.onIconResponse(response); - } - }); - } else { - ThreadUtils.postToUiThread(new Runnable() { - @Override - public void run() { - callback.onIconResponse(response); - } - }); - } - } - } - - private void prepareRequest(IconRequest request) throws InterruptedException { - for (Preparer preparer : preparers) { - ensureNotInterrupted(); - - preparer.prepare(request); - - logPreparer(request, preparer); - } - } - - private IconResponse loadIcon(IconRequest request) throws InterruptedException { - while (request.hasIconDescriptors()) { - for (IconLoader loader : loaders) { - ensureNotInterrupted(); - - IconResponse response = loader.load(request); - - logLoader(request, loader, response); - - if (response != null) { - return response; - } - } - - request.moveToNextIcon(); - } - - return generator.load(request); - } - - private void processIcon(IconRequest request, IconResponse response) throws InterruptedException { - for (Processor processor : processors) { - ensureNotInterrupted(); - - processor.process(request, response); - - logProcessor(processor); - } - } - - private void handleException(final Throwable t) { - if (AppConstants.NIGHTLY_BUILD) { - // We want to be aware of problems: Let's re-throw the exception on the main thread to - // force an app crash. However we only do this in Nightly builds. Every other build - // (especially release builds) should just carry on and log the error. - ThreadUtils.postToUiThread(new Runnable() { - @Override - public void run() { - throw new RuntimeException("Icon task thread crashed", t); - } - }); - } else { - Log.e(LOGTAG, "Icon task crashed", t); - } - } - - private boolean shouldLog() { - // Do not log anything if debugging is disabled and never log anything in a non-nightly build. - return DEBUG && AppConstants.NIGHTLY_BUILD; - } - - private void logPreparer(IconRequest request, Preparer preparer) { - if (!shouldLog()) { - return; - } - - Log.d(LOGTAG, String.format(" PREPARE %s" + " (%s)", - preparer.getClass().getSimpleName(), - request.getIconCount())); - } - - private void logLoader(IconRequest request, IconLoader loader, IconResponse response) { - if (!shouldLog()) { - return; - } - - Log.d(LOGTAG, String.format(" LOAD [%s] %s : %s", - response != null ? "X" : " ", - loader.getClass().getSimpleName(), - request.getBestIcon().getUrl())); - } - - private void logProcessor(Processor processor) { - if (!shouldLog()) { - return; - } - - Log.d(LOGTAG, " PROCESS " + processor.getClass().getSimpleName()); - } - - private void logResponse(IconResponse response) { - if (!shouldLog()) { - return; - } - - final Bitmap bitmap = response.getBitmap(); - - Log.d(LOGTAG, String.format("=> ICON: %sx%s", bitmap.getWidth(), bitmap.getHeight())); - } - - private void logRequest(IconRequest request) { - if (!shouldLog()) { - return; - } - - Log.d(LOGTAG, String.format("REQUEST (%s) %s", - request.getIconCount(), - request.getPageUrl())); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/icons/Icons.java b/mobile/android/base/java/org/mozilla/gecko/icons/Icons.java deleted file mode 100644 index a5505a6941..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/icons/Icons.java +++ /dev/null @@ -1,35 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.icons; - -import android.content.Context; -import android.support.annotation.CheckResult; - -/** - * Entry point for loading icons for websites (just high quality icons, can be favicons or - * touch icons). - * - * The API is loosely inspired by Picasso's builder. - * - * Example: - * - * Icons.with(context) - * .pageUrl(pageURL) - * .skipNetwork() - * .privileged(true) - * .icon(IconDescriptor.createGenericIcon(url)) - * .build() - * .execute(callback); - */ -public abstract class Icons { - /** - * Create a new request for loading a website icon. - */ - @CheckResult - public static IconRequestBuilder with(Context context) { - return new IconRequestBuilder(context); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/icons/IconsHelper.java b/mobile/android/base/java/org/mozilla/gecko/icons/IconsHelper.java deleted file mode 100644 index d351eb4b75..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/icons/IconsHelper.java +++ /dev/null @@ -1,140 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.icons; - -import android.net.Uri; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.text.TextUtils; -import android.util.Log; - -import org.mozilla.gecko.AboutPages; -import org.mozilla.gecko.util.StringUtils; - -import java.util.HashSet; - -/** - * Helper methods for icon related tasks. - */ -public class IconsHelper { - private static final String LOGTAG = "Gecko/IconsHelper"; - - // Mime types of things we are capable of decoding. - private static final HashSet sDecodableMimeTypes = new HashSet<>(); - - // Mime types of things we are both capable of decoding and are container formats (May contain - // multiple different sizes of image) - private static final HashSet sContainerMimeTypes = new HashSet<>(); - - static { - // MIME types extracted from http://filext.com - ostensibly all in-use mime types for the - // corresponding formats. - // ICO - sContainerMimeTypes.add("image/vnd.microsoft.icon"); - sContainerMimeTypes.add("image/ico"); - sContainerMimeTypes.add("image/icon"); - sContainerMimeTypes.add("image/x-icon"); - sContainerMimeTypes.add("text/ico"); - sContainerMimeTypes.add("application/ico"); - - // Add supported container types to the set of supported types. - sDecodableMimeTypes.addAll(sContainerMimeTypes); - - // PNG - sDecodableMimeTypes.add("image/png"); - sDecodableMimeTypes.add("application/png"); - sDecodableMimeTypes.add("application/x-png"); - - // GIF - sDecodableMimeTypes.add("image/gif"); - - // JPEG - sDecodableMimeTypes.add("image/jpeg"); - sDecodableMimeTypes.add("image/jpg"); - sDecodableMimeTypes.add("image/pipeg"); - sDecodableMimeTypes.add("image/vnd.swiftview-jpeg"); - sDecodableMimeTypes.add("application/jpg"); - sDecodableMimeTypes.add("application/x-jpg"); - - // BMP - sDecodableMimeTypes.add("application/bmp"); - sDecodableMimeTypes.add("application/x-bmp"); - sDecodableMimeTypes.add("application/x-win-bitmap"); - sDecodableMimeTypes.add("image/bmp"); - sDecodableMimeTypes.add("image/x-bmp"); - sDecodableMimeTypes.add("image/x-bitmap"); - sDecodableMimeTypes.add("image/x-xbitmap"); - sDecodableMimeTypes.add("image/x-win-bitmap"); - sDecodableMimeTypes.add("image/x-windows-bitmap"); - sDecodableMimeTypes.add("image/x-ms-bitmap"); - sDecodableMimeTypes.add("image/x-ms-bmp"); - sDecodableMimeTypes.add("image/ms-bmp"); - } - - /** - * Helper method to getIcon the default Favicon URL for a given pageURL. Generally: somewhere.com/favicon.ico - * - * @param pageURL Page URL for which a default Favicon URL is requested - * @return The default Favicon URL or null if no default URL could be guessed. - */ - @Nullable - public static String guessDefaultFaviconURL(String pageURL) { - if (TextUtils.isEmpty(pageURL)) { - return null; - } - - // Special-casing for about: pages. The favicon for about:pages which don't provide a link tag - // is bundled in the database, keyed only by page URL, hence the need to return the page URL - // here. If the database ever migrates to stop being silly in this way, this can plausibly - // be removed. - if (AboutPages.isAboutPage(pageURL) || pageURL.startsWith("jar:")) { - return pageURL; - } - - if (!StringUtils.isHttpOrHttps(pageURL)) { - // Guessing a default URL only makes sense for http(s) URLs. - return null; - } - - try { - // Fall back to trying "someScheme:someDomain.someExtension/favicon.ico". - Uri uri = Uri.parse(pageURL); - if (uri.getAuthority().isEmpty()) { - return null; - } - - return uri.buildUpon() - .path("favicon.ico") - .clearQuery() - .fragment("") - .build() - .toString(); - } catch (Exception e) { - Log.d(LOGTAG, "Exception getting default favicon URL"); - return null; - } - } - - /** - * Helper function to determine if the provided mime type is that of a format that can contain - * multiple image types. At time of writing, the only such type is ICO. - * @param mimeType Mime type to check. - * @return true if the given mime type is a container type, false otherwise. - */ - public static boolean isContainerType(@NonNull String mimeType) { - return sContainerMimeTypes.contains(mimeType); - } - - /** - * Helper function to determine if we can decode a particular mime type. - * - * @param imgType Mime type to check for decodability. - * @return false if the given mime type is certainly not decodable, true if it might be. - */ - public static boolean canDecodeType(@NonNull String imgType) { - return sDecodableMimeTypes.contains(imgType); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/icons/decoders/FaviconDecoder.java b/mobile/android/base/java/org/mozilla/gecko/icons/decoders/FaviconDecoder.java deleted file mode 100644 index 43f5d0ac60..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/icons/decoders/FaviconDecoder.java +++ /dev/null @@ -1,197 +0,0 @@ -/* 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.icons.decoders; - -import android.content.Context; -import android.graphics.Bitmap; -import android.util.Base64; -import android.util.Log; - -import org.mozilla.gecko.gfx.BitmapUtils; - -import java.util.Iterator; -import java.util.NoSuchElementException; - -/** - * Class providing static utility methods for decoding favicons. - */ -public class FaviconDecoder { - private static final String LOG_TAG = "GeckoFaviconDecoder"; - - static enum ImageMagicNumbers { - // It is irritating that Java bytes are signed... - PNG(new byte[] {(byte) (0x89 & 0xFF), 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a}), - GIF(new byte[] {0x47, 0x49, 0x46, 0x38}), - JPEG(new byte[] {-0x1, -0x28, -0x1, -0x20}), - BMP(new byte[] {0x42, 0x4d}), - WEB(new byte[] {0x57, 0x45, 0x42, 0x50, 0x0a}); - - public byte[] value; - - private ImageMagicNumbers(byte[] value) { - this.value = value; - } - } - - /** - * Check for image format magic numbers of formats supported by Android. - * @param buffer Byte buffer to check for magic numbers - * @param offset Offset at which to look for magic numbers. - * @return true if the buffer contains a bitmap decodable by Android (Or at least, a sequence - * starting with the magic numbers thereof). false otherwise. - */ - private static boolean isDecodableByAndroid(byte[] buffer, int offset) { - for (ImageMagicNumbers m : ImageMagicNumbers.values()) { - if (bufferStartsWith(buffer, m.value, offset)) { - return true; - } - } - - return false; - } - - /** - * Utility function to check for the existence of a test byte sequence at a given offset in a - * buffer. - * - * @param buffer Byte buffer to search. - * @param test Byte sequence to search for. - * @param bufferOffset Index in input buffer to expect test sequence. - * @return true if buffer contains the byte sequence given in test at offset bufferOffset, false - * otherwise. - */ - static boolean bufferStartsWith(byte[] buffer, byte[] test, int bufferOffset) { - if (buffer.length < test.length) { - return false; - } - - for (int i = 0; i < test.length; ++i) { - if (buffer[bufferOffset + i] != test[i]) { - return false; - } - } - return true; - } - - /** - * Decode the favicon present in the region of the provided byte[] starting at offset and - * proceeding for length bytes, if any. Returns either the resulting LoadFaviconResult or null if the - * given range does not contain a bitmap we know how to decode. - * - * @param buffer Byte array containing the favicon to decode. - * @param offset The index of the first byte in the array of the region of interest. - * @param length The length of the region in the array to decode. - * @return The decoded version of the bitmap in the described region, or null if none can be - * decoded. - */ - public static LoadFaviconResult decodeFavicon(Context context, byte[] buffer, int offset, int length) { - LoadFaviconResult result; - if (isDecodableByAndroid(buffer, offset)) { - result = new LoadFaviconResult(); - result.offset = offset; - result.length = length; - result.isICO = false; - - Bitmap decodedImage = BitmapUtils.decodeByteArray(buffer, offset, length); - if (decodedImage == null) { - // What we got wasn't decodable after all. Probably corrupted image, or we got a muffled OOM. - return null; - } - - // We assume here that decodeByteArray doesn't hold on to the entire supplied - // buffer -- worst case, each of our buffers will be twice the necessary size. - result.bitmapsDecoded = new SingleBitmapIterator(decodedImage); - result.faviconBytes = buffer; - - return result; - } - - // If it's not decodable by Android, it might be an ICO. Let's try. - ICODecoder decoder = new ICODecoder(context, buffer, offset, length); - - result = decoder.decode(); - - if (result == null) { - return null; - } - - return result; - } - - public static LoadFaviconResult decodeDataURI(Context context, String uri) { - if (uri == null) { - Log.w(LOG_TAG, "Can't decode null data: URI."); - return null; - } - - if (!uri.startsWith("data:image/")) { - // Can't decode non-image data: URI. - return null; - } - - // Otherwise, let's attack this blindly. Strictly we should be parsing. - int offset = uri.indexOf(',') + 1; - if (offset == 0) { - Log.w(LOG_TAG, "No ',' in data: URI; malformed?"); - return null; - } - - try { - String base64 = uri.substring(offset); - byte[] raw = Base64.decode(base64, Base64.DEFAULT); - return decodeFavicon(context, raw); - } catch (Exception e) { - Log.w(LOG_TAG, "Couldn't decode data: URI.", e); - return null; - } - } - - public static LoadFaviconResult decodeFavicon(Context context, byte[] buffer) { - return decodeFavicon(context, buffer, 0, buffer.length); - } - - /** - * Iterator to hold a single bitmap. - */ - static class SingleBitmapIterator implements Iterator { - private Bitmap bitmap; - - public SingleBitmapIterator(Bitmap b) { - bitmap = b; - } - - /** - * Slightly cheating here - this iterator supports peeking (Handy in a couple of obscure - * places where the runtime type of the Iterator under consideration is known and - * destruction of it is discouraged. - * - * @return The bitmap carried by this SingleBitmapIterator. - */ - public Bitmap peek() { - return bitmap; - } - - @Override - public boolean hasNext() { - return bitmap != null; - } - - @Override - public Bitmap next() { - if (bitmap == null) { - throw new NoSuchElementException("Element already returned from SingleBitmapIterator."); - } - - Bitmap ret = bitmap; - bitmap = null; - return ret; - } - - @Override - public void remove() { - throw new UnsupportedOperationException("remove() not supported on SingleBitmapIterator."); - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/icons/decoders/ICODecoder.java b/mobile/android/base/java/org/mozilla/gecko/icons/decoders/ICODecoder.java deleted file mode 100644 index 44e3f1252b..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/icons/decoders/ICODecoder.java +++ /dev/null @@ -1,396 +0,0 @@ -/* 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.icons.decoders; - -import android.content.Context; -import android.graphics.Bitmap; -import android.support.annotation.VisibleForTesting; -import android.util.SparseArray; - -import java.util.Iterator; -import java.util.NoSuchElementException; - -import org.mozilla.gecko.annotation.RobocopTarget; -import org.mozilla.gecko.gfx.BitmapUtils; -import org.mozilla.gecko.R; - -/** - * Utility class for determining the region of a provided array which contains the largest bitmap, - * assuming the provided array is a valid ICO and the bitmap desired is square, and for pruning - * unwanted entries from ICO files, if desired. - * - * An ICO file is a container format that may hold up to 255 images in either BMP or PNG format. - * A mixture of image types may not exist. - * - * The format consists of a header specifying the number, n, of images, followed by the Icon Directory. - * - * The Icon Directory consists of n Icon Directory Entries, each 16 bytes in length, specifying, for - * the corresponding image, the dimensions, colour information, payload size, and location in the file. - * - * All numerical fields follow a little-endian byte ordering. - * - * Header format: - * - * 0 1 2 3 - * 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 - * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - * | Reserved field. Must be zero | Type (1 for ICO, 2 for CUR) | - * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - * | Image count (n) | - * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - * - * The type field is expected to always be 1. CUR format images should not be used for Favicons. - * - * - * Icon Directory Entry format: - * - * 0 1 2 3 - * 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 - * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - * | Image width | Image height | Palette size | Reserved (0) | - * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - * | Colour plane count | Bits per pixel | - * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - * | Size of image data, in bytes | - * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - * | Start of image data, as an offset from start of file | - * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - * - * Image dimensions of zero are to be interpreted as image dimensions of 256. - * - * The palette size field records the number of colours in the stored BMP, if a palette is used. Zero - * if the payload is a PNG or no palette is in use. - * - * The number of colour planes is, usually, 0 (Not in use) or 1. Values greater than 1 are to be - * interpreted not as a colour plane count, but as a multiplying factor on the bits per pixel field. - * (Apparently 65535 was not deemed a sufficiently large maximum value of bits per pixel.) - * - * - * The Icon Directory consists of n-many Icon Directory Entries in sequence, with no gaps. - * - * This class is not thread safe. - */ -public class ICODecoder implements Iterable { - // The number of bytes that compacting will save for us to bother doing it. - public static final int COMPACT_THRESHOLD = 4000; - - // Some geometry of an ICO file. - public static final int ICO_HEADER_LENGTH_BYTES = 6; - public static final int ICO_ICONDIRENTRY_LENGTH_BYTES = 16; - - // The buffer containing bytes to attempt to decode. - private byte[] decodand; - - // The region of the decodand to decode. - private int offset; - private int len; - - IconDirectoryEntry[] iconDirectory; - private boolean isValid; - private boolean hasDecoded; - private int largestFaviconSize; - - @RobocopTarget - public ICODecoder(Context context, byte[] decodand, int offset, int len) { - this.decodand = decodand; - this.offset = offset; - this.len = len; - this.largestFaviconSize = context.getResources() - .getDimensionPixelSize(R.dimen.favicon_largest_interesting_size); - } - - /** - * Decode the Icon Directory for this ICO and store the result in iconDirectory. - * - * @return true if ICO decoding was considered to probably be a success, false if it certainly - * was a failure. - */ - private boolean decodeIconDirectoryAndPossiblyPrune() { - hasDecoded = true; - - // Fail if the end of the described range is out of bounds. - if (offset + len > decodand.length) { - return false; - } - - // Fail if we don't have enough space for the header. - if (len < ICO_HEADER_LENGTH_BYTES) { - return false; - } - - // Check that the reserved fields in the header are indeed zero, and that the type field - // specifies ICO. If not, we've probably been given something that isn't really an ICO. - if (decodand[offset] != 0 || - decodand[offset + 1] != 0 || - decodand[offset + 2] != 1 || - decodand[offset + 3] != 0) { - return false; - } - - // Here, and in many other places, byte values are ANDed with 0xFF. This is because Java - // bytes are signed - to obtain a numerical value of a longer type which holds the unsigned - // interpretation of the byte of interest, we do this. - int numEncodedImages = (decodand[offset + 4] & 0xFF) | - (decodand[offset + 5] & 0xFF) << 8; - - - // Fail if there are no images or the field is corrupt. - if (numEncodedImages <= 0) { - return false; - } - - final int headerAndDirectorySize = ICO_HEADER_LENGTH_BYTES + (numEncodedImages * ICO_ICONDIRENTRY_LENGTH_BYTES); - - // Fail if there is not enough space in the buffer for the stated number of icondir entries, - // let alone the data. - if (len < headerAndDirectorySize) { - return false; - } - - // Put the pointer on the first byte of the first Icon Directory Entry. - int bufferIndex = offset + ICO_HEADER_LENGTH_BYTES; - - // We now iterate over the Icon Directory, decoding each entry as we go. We also need to - // discard all entries except one >= the maximum interesting size. - - // Size of the smallest image larger than the limit encountered. - int minimumMaximum = Integer.MAX_VALUE; - - // Used to track the best entry for each size. The entries we want to keep. - SparseArray preferenceArray = new SparseArray(); - - for (int i = 0; i < numEncodedImages; i++, bufferIndex += ICO_ICONDIRENTRY_LENGTH_BYTES) { - // Decode the Icon Directory Entry at this offset. - IconDirectoryEntry newEntry = IconDirectoryEntry.createFromBuffer(decodand, offset, len, bufferIndex); - newEntry.index = i; - - if (newEntry.isErroneous) { - continue; - } - - if (newEntry.width > largestFaviconSize) { - // If we already have a smaller image larger than the maximum size of interest, we - // don't care about the new one which is larger than the smallest image larger than - // the maximum size. - if (newEntry.width >= minimumMaximum) { - continue; - } - - // Remove the previous minimum-maximum. - preferenceArray.delete(minimumMaximum); - - minimumMaximum = newEntry.width; - } - - IconDirectoryEntry oldEntry = preferenceArray.get(newEntry.width); - if (oldEntry == null) { - preferenceArray.put(newEntry.width, newEntry); - continue; - } - - if (oldEntry.compareTo(newEntry) < 0) { - preferenceArray.put(newEntry.width, newEntry); - } - } - - final int count = preferenceArray.size(); - - // Abort if no entries are desired (Perhaps all are corrupt?) - if (count == 0) { - return false; - } - - // Allocate space for the icon directory entries in the decoded directory. - iconDirectory = new IconDirectoryEntry[count]; - - // The size of the data in the buffer that we find useful. - int retainedSpace = ICO_HEADER_LENGTH_BYTES; - - for (int i = 0; i < count; i++) { - IconDirectoryEntry e = preferenceArray.valueAt(i); - retainedSpace += ICO_ICONDIRENTRY_LENGTH_BYTES + e.payloadSize; - iconDirectory[i] = e; - } - - isValid = true; - - // Set the number of images field in the buffer to reflect the number of retained entries. - decodand[offset + 4] = (byte) iconDirectory.length; - decodand[offset + 5] = (byte) (iconDirectory.length >>> 8); - - if ((len - retainedSpace) > COMPACT_THRESHOLD) { - compactingCopy(retainedSpace); - } - - return true; - } - - /** - * Copy the buffer into a new array of exactly the required size, omitting any unwanted data. - */ - private void compactingCopy(int spaceRetained) { - byte[] buf = new byte[spaceRetained]; - - // Copy the header. - System.arraycopy(decodand, offset, buf, 0, ICO_HEADER_LENGTH_BYTES); - - int headerPtr = ICO_HEADER_LENGTH_BYTES; - - int payloadPtr = ICO_HEADER_LENGTH_BYTES + (iconDirectory.length * ICO_ICONDIRENTRY_LENGTH_BYTES); - - int ind = 0; - for (IconDirectoryEntry entry : iconDirectory) { - // Copy this entry. - System.arraycopy(decodand, offset + entry.getOffset(), buf, headerPtr, ICO_ICONDIRENTRY_LENGTH_BYTES); - - // Copy its payload. - System.arraycopy(decodand, offset + entry.payloadOffset, buf, payloadPtr, entry.payloadSize); - - // Update the offset field. - buf[headerPtr + 12] = (byte) payloadPtr; - buf[headerPtr + 13] = (byte) (payloadPtr >>> 8); - buf[headerPtr + 14] = (byte) (payloadPtr >>> 16); - buf[headerPtr + 15] = (byte) (payloadPtr >>> 24); - - entry.payloadOffset = payloadPtr; - entry.index = ind; - - payloadPtr += entry.payloadSize; - headerPtr += ICO_ICONDIRENTRY_LENGTH_BYTES; - ind++; - } - - decodand = buf; - offset = 0; - len = spaceRetained; - } - - /** - * Decode and return the bitmap represented by the given index in the Icon Directory, if valid. - * - * @param index The index into the Icon Directory of the image of interest. - * @return The decoded Bitmap object for this image, or null if the entry is invalid or decoding - * fails. - */ - public Bitmap decodeBitmapAtIndex(int index) { - final IconDirectoryEntry iconDirEntry = iconDirectory[index]; - - if (iconDirEntry.payloadIsPNG) { - // PNG payload. Simply extract it and decode it. - return BitmapUtils.decodeByteArray(decodand, offset + iconDirEntry.payloadOffset, iconDirEntry.payloadSize); - } - - // The payload is a BMP, so we need to do some magic to get the decoder to do what we want. - // We construct an ICO containing just the image we want, and let Android do the rest. - byte[] decodeTarget = new byte[ICO_HEADER_LENGTH_BYTES + ICO_ICONDIRENTRY_LENGTH_BYTES + iconDirEntry.payloadSize]; - - // Set the type field in the ICO header. - decodeTarget[2] = 1; - - // Set the num-images field in the header to 1. - decodeTarget[4] = 1; - - // Copy the ICONDIRENTRY we need into the new buffer. - System.arraycopy(decodand, offset + iconDirEntry.getOffset(), decodeTarget, ICO_HEADER_LENGTH_BYTES, ICO_ICONDIRENTRY_LENGTH_BYTES); - - // Copy the payload into the new buffer. - final int singlePayloadOffset = ICO_HEADER_LENGTH_BYTES + ICO_ICONDIRENTRY_LENGTH_BYTES; - System.arraycopy(decodand, offset + iconDirEntry.payloadOffset, decodeTarget, singlePayloadOffset, iconDirEntry.payloadSize); - - // Update the offset field of the ICONDIRENTRY to make the new ICO valid. - decodeTarget[ICO_HEADER_LENGTH_BYTES + 12] = singlePayloadOffset; - decodeTarget[ICO_HEADER_LENGTH_BYTES + 13] = (singlePayloadOffset >>> 8); - decodeTarget[ICO_HEADER_LENGTH_BYTES + 14] = (singlePayloadOffset >>> 16); - decodeTarget[ICO_HEADER_LENGTH_BYTES + 15] = (singlePayloadOffset >>> 24); - - // Decode the newly-constructed singleton-ICO. - return BitmapUtils.decodeByteArray(decodeTarget); - } - - /** - * Fetch an iterator over the images in this ICO, or null if this ICO seems to be invalid. - * - * @return An iterator over the Bitmaps stored in this ICO, or null if decoding fails. - */ - @Override - public ICOIterator iterator() { - // If a previous call to decode concluded this ICO is invalid, abort. - if (hasDecoded && !isValid) { - return null; - } - - // If we've not been decoded before, but now fail to make any sense of the ICO, abort. - if (!hasDecoded) { - if (!decodeIconDirectoryAndPossiblyPrune()) { - return null; - } - } - - // If decoding was a success, return an iterator over the images in this ICO. - return new ICOIterator(); - } - - /** - * Decode this ICO and return the result as a LoadFaviconResult. - * @return A LoadFaviconResult representing the decoded ICO. - */ - public LoadFaviconResult decode() { - // The call to iterator returns null if decoding fails. - Iterator bitmaps = iterator(); - if (bitmaps == null) { - return null; - } - - LoadFaviconResult result = new LoadFaviconResult(); - - result.bitmapsDecoded = bitmaps; - result.faviconBytes = decodand; - result.offset = offset; - result.length = len; - result.isICO = true; - - return result; - } - - @VisibleForTesting - @RobocopTarget - public IconDirectoryEntry[] getIconDirectory() { - return iconDirectory; - } - - @VisibleForTesting - @RobocopTarget - public int getLargestFaviconSize() { - return largestFaviconSize; - } - - /** - * Inner class to iterate over the elements in the ICO represented by the enclosing instance. - */ - private class ICOIterator implements Iterator { - private int mIndex; - - @Override - public boolean hasNext() { - return mIndex < iconDirectory.length; - } - - @Override - public Bitmap next() { - if (mIndex > iconDirectory.length) { - throw new NoSuchElementException("No more elements in this ICO."); - } - return decodeBitmapAtIndex(mIndex++); - } - - @Override - public void remove() { - if (iconDirectory[mIndex] == null) { - throw new IllegalStateException("Remove already called for element " + mIndex); - } - iconDirectory[mIndex] = null; - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/icons/decoders/IconDirectoryEntry.java b/mobile/android/base/java/org/mozilla/gecko/icons/decoders/IconDirectoryEntry.java deleted file mode 100644 index 82ff91a55b..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/icons/decoders/IconDirectoryEntry.java +++ /dev/null @@ -1,212 +0,0 @@ -/* 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.icons.decoders; - -import android.support.annotation.VisibleForTesting; - -import org.mozilla.gecko.annotation.RobocopTarget; - -/** - * Representation of an ICO file ICONDIRENTRY structure. - */ -public class IconDirectoryEntry implements Comparable { - - public static int maxBPP; - - int width; - int height; - int paletteSize; - int bitsPerPixel; - int payloadSize; - int payloadOffset; - boolean payloadIsPNG; - - // Tracks the index in the Icon Directory of this entry. Useful only for pruning. - int index; - boolean isErroneous; - - @RobocopTarget - public IconDirectoryEntry(int width, int height, int paletteSize, int bitsPerPixel, int payloadSize, int payloadOffset, boolean payloadIsPNG) { - this.width = width; - this.height = height; - this.paletteSize = paletteSize; - this.bitsPerPixel = bitsPerPixel; - this.payloadSize = payloadSize; - this.payloadOffset = payloadOffset; - this.payloadIsPNG = payloadIsPNG; - } - - /** - * Method to get a dummy Icon Directory Entry with the Erroneous bit set. - * - * @return An erroneous placeholder Icon Directory Entry. - */ - public static IconDirectoryEntry getErroneousEntry() { - IconDirectoryEntry ret = new IconDirectoryEntry(-1, -1, -1, -1, -1, -1, false); - ret.isErroneous = true; - - return ret; - } - - /** - * Create an IconDirectoryEntry object from a byte[]. Interprets the buffer starting at the given - * offset as an IconDirectoryEntry and returns the result. - * - * @param buffer Byte array containing the icon directory entry to decode. - * @param regionOffset Offset into the byte array of the valid region of the buffer. - * @param regionLength Length of the valid region in the buffer. - * @param entryOffset Offset of the icon directory entry to decode within the buffer. - * @return An IconDirectoryEntry object representing the entry specified, or null if the entry - * is obviously invalid. - */ - public static IconDirectoryEntry createFromBuffer(byte[] buffer, int regionOffset, int regionLength, int entryOffset) { - // Verify that the reserved field is really zero. - if (buffer[entryOffset + 3] != 0) { - return getErroneousEntry(); - } - - // Verify that the entry points to a region that actually exists in the buffer, else bin it. - int fieldPtr = entryOffset + 8; - int entryLength = (buffer[fieldPtr] & 0xFF) | - (buffer[fieldPtr + 1] & 0xFF) << 8 | - (buffer[fieldPtr + 2] & 0xFF) << 16 | - (buffer[fieldPtr + 3] & 0xFF) << 24; - - // Advance to the offset field. - fieldPtr += 4; - - int payloadOffset = (buffer[fieldPtr] & 0xFF) | - (buffer[fieldPtr + 1] & 0xFF) << 8 | - (buffer[fieldPtr + 2] & 0xFF) << 16 | - (buffer[fieldPtr + 3] & 0xFF) << 24; - - // Fail if the entry describes a region outside the buffer. - if (payloadOffset < 0 || entryLength < 0 || payloadOffset + entryLength > regionOffset + regionLength) { - return getErroneousEntry(); - } - - // Extract the image dimensions. - int imageWidth = buffer[entryOffset] & 0xFF; - int imageHeight = buffer[entryOffset + 1] & 0xFF; - - // Because Microsoft, a size value of zero represents an image size of 256. - if (imageWidth == 0) { - imageWidth = 256; - } - - if (imageHeight == 0) { - imageHeight = 256; - } - - // If the image uses a colour palette, this is the number of colours, otherwise this is zero. - int paletteSize = buffer[entryOffset + 2] & 0xFF; - - // The plane count - usually 0 or 1. When > 1, taken as multiplier on bitsPerPixel. - int colorPlanes = buffer[entryOffset + 4] & 0xFF; - - int bitsPerPixel = (buffer[entryOffset + 6] & 0xFF) | - (buffer[entryOffset + 7] & 0xFF) << 8; - - if (colorPlanes > 1) { - bitsPerPixel *= colorPlanes; - } - - // Look for PNG magic numbers at the start of the payload. - boolean payloadIsPNG = FaviconDecoder.bufferStartsWith(buffer, FaviconDecoder.ImageMagicNumbers.PNG.value, regionOffset + payloadOffset); - - return new IconDirectoryEntry(imageWidth, imageHeight, paletteSize, bitsPerPixel, entryLength, payloadOffset, payloadIsPNG); - } - - /** - * Get the number of bytes from the start of the ICO file to the beginning of this entry. - */ - public int getOffset() { - return ICODecoder.ICO_HEADER_LENGTH_BYTES + (index * ICODecoder.ICO_ICONDIRENTRY_LENGTH_BYTES); - } - - @Override - public int compareTo(IconDirectoryEntry another) { - if (width > another.width) { - return 1; - } - - if (width < another.width) { - return -1; - } - - // Where both images exceed the max BPP, take the smaller of the two BPP values. - if (bitsPerPixel >= maxBPP && another.bitsPerPixel >= maxBPP) { - if (bitsPerPixel < another.bitsPerPixel) { - return 1; - } - - if (bitsPerPixel > another.bitsPerPixel) { - return -1; - } - } - - // Otherwise, take the larger of the BPP values. - if (bitsPerPixel > another.bitsPerPixel) { - return 1; - } - - if (bitsPerPixel < another.bitsPerPixel) { - return -1; - } - - // Prefer large palettes. - if (paletteSize > another.paletteSize) { - return 1; - } - - if (paletteSize < another.paletteSize) { - return -1; - } - - // Prefer smaller payloads. - if (payloadSize < another.payloadSize) { - return 1; - } - - if (payloadSize > another.payloadSize) { - return -1; - } - - // If all else fails, prefer PNGs over BMPs. They tend to be smaller. - if (payloadIsPNG && !another.payloadIsPNG) { - return 1; - } - - if (!payloadIsPNG && another.payloadIsPNG) { - return -1; - } - - return 0; - } - - public static void setMaxBPP(int maxBPP) { - IconDirectoryEntry.maxBPP = maxBPP; - } - - @VisibleForTesting - @RobocopTarget - public int getWidth() { - return width; - } - - @Override - public String toString() { - return "IconDirectoryEntry{" + - "\nwidth=" + width + - ", \nheight=" + height + - ", \npaletteSize=" + paletteSize + - ", \nbitsPerPixel=" + bitsPerPixel + - ", \npayloadSize=" + payloadSize + - ", \npayloadOffset=" + payloadOffset + - ", \npayloadIsPNG=" + payloadIsPNG + - ", \nindex=" + index + - '}'; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/icons/decoders/LoadFaviconResult.java b/mobile/android/base/java/org/mozilla/gecko/icons/decoders/LoadFaviconResult.java deleted file mode 100644 index cc196b91e3..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/icons/decoders/LoadFaviconResult.java +++ /dev/null @@ -1,133 +0,0 @@ -/* 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.icons.decoders; - -import android.graphics.Bitmap; -import android.support.annotation.Nullable; -import android.util.Log; -import android.util.SparseArray; - -import java.io.ByteArrayOutputStream; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Iterator; -import java.util.List; - -/** - * Class representing the result of loading a favicon. - * This operation will produce either a collection of favicons, a single favicon, or no favicon. - * It is necessary to model single favicons differently to a collection of one favicon (An entity - * that may not exist with this scheme) since the in-database representation of these things differ. - * (In particular, collections of favicons are stored in encoded ICO format, whereas single icons are - * stored as decoded bitmap blobs.) - */ -public class LoadFaviconResult { - private static final String LOGTAG = "LoadFaviconResult"; - - byte[] faviconBytes; - int offset; - int length; - - boolean isICO; - Iterator bitmapsDecoded; - - public Iterator getBitmaps() { - return bitmapsDecoded; - } - - /** - * Return a representation of this result suitable for storing in the database. - * - * @return A byte array containing the bytes from which this result was decoded, - * or null if re-encoding failed. - */ - public byte[] getBytesForDatabaseStorage() { - // Begin by normalising the buffer. - if (offset != 0 || length != faviconBytes.length) { - final byte[] normalised = new byte[length]; - System.arraycopy(faviconBytes, offset, normalised, 0, length); - offset = 0; - faviconBytes = normalised; - } - - // For results containing multiple images, we store the result verbatim. (But cutting the - // buffer to size first). - // We may instead want to consider re-encoding the entire ICO as a collection of efficiently - // encoded PNGs. This may not be worth the CPU time (Indeed, the encoding of single-image - // favicons may also not be worth the time/space tradeoff.). - if (isICO) { - return faviconBytes; - } - - // For results containing a single image, we re-encode the - // result as a PNG in an effort to save space. - final Bitmap favicon = ((FaviconDecoder.SingleBitmapIterator) bitmapsDecoded).peek(); - final ByteArrayOutputStream stream = new ByteArrayOutputStream(); - - try { - if (favicon.compress(Bitmap.CompressFormat.PNG, 100, stream)) { - return stream.toByteArray(); - } - } catch (OutOfMemoryError e) { - Log.w(LOGTAG, "Out of memory re-compressing favicon."); - } - - Log.w(LOGTAG, "Favicon re-compression failed."); - return null; - } - - @Nullable - public Bitmap getBestBitmap(int targetWidthAndHeight) { - final SparseArray iconMap = new SparseArray<>(); - final List sizes = new ArrayList<>(); - - while (bitmapsDecoded.hasNext()) { - final Bitmap b = bitmapsDecoded.next(); - - // It's possible to receive null, most likely due to OOM or a zero-sized image, - // from BitmapUtils.decodeByteArray(byte[], int, int, BitmapFactory.Options) - if (b != null) { - iconMap.put(b.getWidth(), b); - sizes.add(b.getWidth()); - } - } - - int bestSize = selectBestSizeFromList(sizes, targetWidthAndHeight); - - if (bestSize == -1) { - // No icons found: this could occur if we weren't able to process any of the - // supplied icons. - return null; - } - - return iconMap.get(bestSize); - } - - /** - * Select the closest icon size from a list of icon sizes. - * We just find the first icon that is larger than the preferred size if available, or otherwise select the - * largest icon (if all icons are smaller than the preferred size). - * - * @return The closest icon size, or -1 if no sizes are supplied. - */ - public static int selectBestSizeFromList(final List sizes, final int preferredSize) { - if (sizes.isEmpty()) { - // This isn't ideal, however current code assumes this as an error value for now. - return -1; - } - - Collections.sort(sizes); - - for (int size : sizes) { - if (size >= preferredSize) { - return size; - } - } - - // If all icons are smaller than the preferred size then we don't have an icon - // selected yet, therefore just take the largest (last) icon. - return sizes.get(sizes.size() - 1); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/icons/loader/ContentProviderLoader.java b/mobile/android/base/java/org/mozilla/gecko/icons/loader/ContentProviderLoader.java deleted file mode 100644 index be8e6d7def..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/icons/loader/ContentProviderLoader.java +++ /dev/null @@ -1,96 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.icons.loader; - -import android.content.Context; -import android.database.Cursor; -import android.graphics.Bitmap; -import android.net.Uri; -import android.text.TextUtils; - -import org.mozilla.gecko.distribution.PartnerBookmarksProviderProxy; -import org.mozilla.gecko.icons.decoders.FaviconDecoder; -import org.mozilla.gecko.icons.decoders.LoadFaviconResult; -import org.mozilla.gecko.icons.IconRequest; -import org.mozilla.gecko.icons.IconResponse; - -/** - * Loader for loading icons from a content provider. This loader was primarily written to load icons - * from the partner bookmarks provider. However it can load icons from arbitrary content providers - * as long as they return a cursor with a "favicon" or "touchicon" column (blob). - */ -public class ContentProviderLoader implements IconLoader { - @Override - public IconResponse load(IconRequest request) { - if (request.shouldSkipDisk()) { - // If we should not load data from disk then we do not load from content providers either. - return null; - } - - final String iconUrl = request.getBestIcon().getUrl(); - final Context context = request.getContext(); - final int targetSize = request.getTargetSize(); - - if (TextUtils.isEmpty(iconUrl) || !iconUrl.startsWith("content://")) { - return null; - } - - Cursor cursor = context.getContentResolver().query( - Uri.parse(iconUrl), - new String[] { - PartnerBookmarksProviderProxy.PartnerContract.TOUCHICON, - PartnerBookmarksProviderProxy.PartnerContract.FAVICON, - }, - null, - null, - null - ); - - if (cursor == null) { - return null; - } - - try { - if (!cursor.moveToFirst()) { - return null; - } - - // Try the touch icon first. It has a higher resolution usually. - Bitmap icon = decodeFromCursor(request.getContext(), cursor, PartnerBookmarksProviderProxy.PartnerContract.TOUCHICON, targetSize); - if (icon != null) { - return IconResponse.create(icon); - } - - icon = decodeFromCursor(request.getContext(), cursor, PartnerBookmarksProviderProxy.PartnerContract.FAVICON, targetSize); - if (icon != null) { - return IconResponse.create(icon); - } - } finally { - cursor.close(); - } - - return null; - } - - private Bitmap decodeFromCursor(Context context, Cursor cursor, String column, int targetWidthAndHeight) { - final int index = cursor.getColumnIndex(column); - if (index == -1) { - return null; - } - - if (cursor.isNull(index)) { - return null; - } - - final byte[] data = cursor.getBlob(index); - LoadFaviconResult result = FaviconDecoder.decodeFavicon(context, data, 0, data.length); - if (result == null) { - return null; - } - - return result.getBestBitmap(targetWidthAndHeight); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/icons/loader/DataUriLoader.java b/mobile/android/base/java/org/mozilla/gecko/icons/loader/DataUriLoader.java deleted file mode 100644 index 9ddc138ec2..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/icons/loader/DataUriLoader.java +++ /dev/null @@ -1,36 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.icons.loader; - -import org.mozilla.gecko.icons.decoders.FaviconDecoder; -import org.mozilla.gecko.icons.decoders.LoadFaviconResult; -import org.mozilla.gecko.icons.IconRequest; -import org.mozilla.gecko.icons.IconResponse; - -/** - * Loader for loading icons from a data URI. This loader will try to decode any data with an - * "image/*" MIME type. - * - * https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs - */ -public class DataUriLoader implements IconLoader { - @Override - public IconResponse load(IconRequest request) { - final String iconUrl = request.getBestIcon().getUrl(); - - if (!iconUrl.startsWith("data:image/")) { - return null; - } - - LoadFaviconResult loadFaviconResult = FaviconDecoder.decodeDataURI(request.getContext(), iconUrl); - if (loadFaviconResult == null) { - return null; - } - - return IconResponse.create( - loadFaviconResult.getBestBitmap(request.getTargetSize())); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/icons/loader/DiskLoader.java b/mobile/android/base/java/org/mozilla/gecko/icons/loader/DiskLoader.java deleted file mode 100644 index 18a38e32b9..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/icons/loader/DiskLoader.java +++ /dev/null @@ -1,27 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.icons.loader; - -import org.mozilla.gecko.icons.IconRequest; -import org.mozilla.gecko.icons.IconResponse; -import org.mozilla.gecko.icons.storage.DiskStorage; - -/** - * Loader implementation for loading icons from the disk cache (Implemented by DiskStorage). - */ -public class DiskLoader implements IconLoader { - @Override - public IconResponse load(IconRequest request) { - if (request.shouldSkipDisk()) { - return null; - } - - final DiskStorage storage = DiskStorage.get(request.getContext()); - final String iconUrl = request.getBestIcon().getUrl(); - - return storage.getIcon(iconUrl); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/icons/loader/IconDownloader.java b/mobile/android/base/java/org/mozilla/gecko/icons/loader/IconDownloader.java deleted file mode 100644 index 3ae9d15d0e..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/icons/loader/IconDownloader.java +++ /dev/null @@ -1,219 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.icons.loader; - -import android.content.Context; -import android.graphics.Bitmap; -import android.support.annotation.VisibleForTesting; -import android.util.Log; - -import org.mozilla.gecko.GeckoAppShell; -import org.mozilla.gecko.icons.decoders.FaviconDecoder; -import org.mozilla.gecko.icons.decoders.LoadFaviconResult; -import org.mozilla.gecko.icons.IconRequest; -import org.mozilla.gecko.icons.IconResponse; -import org.mozilla.gecko.icons.storage.FailureCache; -import org.mozilla.gecko.util.IOUtils; -import org.mozilla.gecko.util.ProxySelector; -import org.mozilla.gecko.util.StringUtils; - -import java.io.IOException; -import java.io.InputStream; -import java.net.HttpURLConnection; -import java.net.URI; -import java.net.URISyntaxException; -import java.util.HashSet; - -/** - * This loader implementation downloads icons from http(s) URLs. - */ -public class IconDownloader implements IconLoader { - private static final String LOGTAG = "Gecko/Downloader"; - - /** - * The maximum number of http redirects (3xx) until we give up. - */ - private static final int MAX_REDIRECTS_TO_FOLLOW = 5; - - /** - * The default size of the buffer to use for downloading Favicons in the event no size is given - * by the server. */ - private static final int DEFAULT_FAVICON_BUFFER_SIZE_BYTES = 25000; - - @Override - public IconResponse load(IconRequest request) { - if (request.shouldSkipNetwork()) { - return null; - } - - final String iconUrl = request.getBestIcon().getUrl(); - - if (!StringUtils.isHttpOrHttps(iconUrl)) { - return null; - } - - try { - final LoadFaviconResult result = downloadAndDecodeImage(request.getContext(), iconUrl); - if (result == null) { - return null; - } - - final Bitmap bitmap = result.getBestBitmap(request.getTargetSize()); - if (bitmap == null) { - return null; - } - - return IconResponse.createFromNetwork(bitmap, iconUrl); - } catch (Exception e) { - Log.e(LOGTAG, "Error reading favicon", e); - } catch (OutOfMemoryError e) { - Log.e(LOGTAG, "Insufficient memory to process favicon"); - } - - return null; - } - - /** - * Download the Favicon from the given URL and pass it to the decoder function. - * - * @param targetFaviconURL URL of the favicon to download. - * @return A LoadFaviconResult containing the bitmap(s) extracted from the downloaded file, or - * null if no or corrupt data was received. - * @throws IOException If attempts to fully read the stream result in such an exception, such as - * in the event of a transient connection failure. - * @throws URISyntaxException If the underlying call to tryDownload retries and raises such an - * exception trying a fallback URL. - */ - @VisibleForTesting - LoadFaviconResult downloadAndDecodeImage(Context context, String targetFaviconURL) throws IOException, URISyntaxException { - // Try the URL we were given. - final HttpURLConnection connection = tryDownload(targetFaviconURL); - if (connection == null) { - return null; - } - - InputStream stream = null; - - // Decode the image from the fetched response. - try { - stream = connection.getInputStream(); - return decodeImageFromResponse(context, stream, connection.getHeaderFieldInt("Content-Length", -1)); - } finally { - // Close the stream and free related resources. - IOUtils.safeStreamClose(stream); - connection.disconnect(); - } - } - - /** - * Helper method for trying the download request to grab a Favicon. - * - * @param faviconURI URL of Favicon to try and download - * @return The HttpResponse containing the downloaded Favicon if successful, null otherwise. - */ - private HttpURLConnection tryDownload(String faviconURI) throws URISyntaxException, IOException { - final HashSet visitedLinkSet = new HashSet<>(); - visitedLinkSet.add(faviconURI); - return tryDownloadRecurse(faviconURI, visitedLinkSet); - } - - /** - * Try to download from the favicon URL and recursively follow redirects. - */ - private HttpURLConnection tryDownloadRecurse(String faviconURI, HashSet visited) throws URISyntaxException, IOException { - if (visited.size() == MAX_REDIRECTS_TO_FOLLOW) { - return null; - } - - final HttpURLConnection connection = connectTo(faviconURI); - - // Was the response a failure? - final int status = connection.getResponseCode(); - - // Handle HTTP status codes requesting a redirect. - if (status >= 300 && status < 400) { - final String newURI = connection.getHeaderField("Location"); - - // Handle mad web servers. - try { - if (newURI == null || newURI.equals(faviconURI)) { - return null; - } - - if (visited.contains(newURI)) { - // Already been redirected here - abort. - return null; - } - - visited.add(newURI); - } finally { - connection.disconnect(); - } - - return tryDownloadRecurse(newURI, visited); - } - - if (status >= 400) { - // Client or Server error. Let's not retry loading from this URL again for some time. - FailureCache.get().rememberFailure(faviconURI); - - connection.disconnect(); - return null; - } - - return connection; - } - - @VisibleForTesting - HttpURLConnection connectTo(String faviconURI) throws URISyntaxException, IOException { - final HttpURLConnection connection = (HttpURLConnection) ProxySelector.openConnectionWithProxy( - new URI(faviconURI)); - - connection.setRequestProperty("User-Agent", GeckoAppShell.getGeckoInterface().getDefaultUAString()); - - // We implemented or own way of following redirects back when this code was using HttpClient. - // Nowadays we should let HttpUrlConnection do the work - assuming that it doesn't follow - // redirects in loops forever. - connection.setInstanceFollowRedirects(false); - - connection.connect(); - - return connection; - } - - /** - * Copies the favicon stream to a buffer and decodes downloaded content into bitmaps using the - * FaviconDecoder. - * - * @param stream to decode - * @param contentLength as reported by the server (or -1) - * @return A LoadFaviconResult containing the bitmap(s) extracted from the downloaded file, or - * null if no or corrupt data were received. - * @throws IOException If attempts to fully read the stream result in such an exception, such as - * in the event of a transient connection failure. - */ - private LoadFaviconResult decodeImageFromResponse(Context context, InputStream stream, int contentLength) throws IOException { - // This may not be provided, but if it is, it's useful. - final int bufferSize; - if (contentLength > 0) { - // The size was reported and sane, so let's use that. - // Integer overflow should not be a problem for Favicon sizes... - bufferSize = contentLength + 1; - } else { - // No declared size, so guess and reallocate later if it turns out to be too small. - bufferSize = DEFAULT_FAVICON_BUFFER_SIZE_BYTES; - } - - // Read the InputStream into a byte[]. - final IOUtils.ConsumedInputStream result = IOUtils.readFully(stream, bufferSize); - if (result == null) { - return null; - } - - // Having downloaded the image, decode it. - return FaviconDecoder.decodeFavicon(context, result.getData(), 0, result.consumedLength); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/icons/loader/IconGenerator.java b/mobile/android/base/java/org/mozilla/gecko/icons/loader/IconGenerator.java deleted file mode 100644 index e0139345d8..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/icons/loader/IconGenerator.java +++ /dev/null @@ -1,168 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.icons.loader; - -import android.content.Context; -import android.content.res.Resources; -import android.graphics.Bitmap; -import android.graphics.Canvas; -import android.graphics.Color; -import android.graphics.Paint; -import android.graphics.RectF; -import android.net.Uri; -import android.support.annotation.NonNull; -import android.support.annotation.VisibleForTesting; -import android.text.TextUtils; -import android.util.TypedValue; - -import org.mozilla.gecko.R; -import org.mozilla.gecko.icons.IconRequest; -import org.mozilla.gecko.icons.IconResponse; - -/** - * This loader will generate an icon in case no icon could be loaded. In order to do so this needs - * to be the last loader that will be tried. - */ -public class IconGenerator implements IconLoader { - // Mozilla's Visual Design Colour Palette - // http://firefoxux.github.io/StyleGuide/#/visualDesign/colours - private static final int[] COLORS = { - 0xFFc33c32, - 0xFFf25820, - 0xFFff9216, - 0xFFffcb00, - 0xFF57bd35, - 0xFF01bdad, - 0xFF0996f8, - 0xFF02538b, - 0xFF1f386e, - 0xFF7a2f7a, - 0xFFea385e, - }; - - // List of common prefixes of host names. Those prefixes will be striped before a prepresentative - // character for an URL is determined. - private static final String[] COMMON_PREFIXES = { - "www.", - "m.", - "mobile.", - }; - - private static final int TEXT_SIZE_DP = 12; - @Override - public IconResponse load(IconRequest request) { - if (request.getIconCount() > 1) { - // There are still other icons to try. We will only generate an icon if there's only one - // icon left and all previous loaders have failed (assuming this is the last one). - return null; - } - - return generate(request.getContext(), request.getPageUrl()); - } - - /** - * Generate default favicon for the given page URL. - */ - @VisibleForTesting static IconResponse generate(Context context, String pageURL) { - final Resources resources = context.getResources(); - final int widthAndHeight = resources.getDimensionPixelSize(R.dimen.favicon_bg); - final int roundedCorners = resources.getDimensionPixelOffset(R.dimen.favicon_corner_radius); - - final Bitmap favicon = Bitmap.createBitmap(widthAndHeight, widthAndHeight, Bitmap.Config.ARGB_8888); - final Canvas canvas = new Canvas(favicon); - - final int color = pickColor(pageURL); - - final Paint paint = new Paint(); - paint.setColor(color); - - canvas.drawRoundRect(new RectF(0, 0, widthAndHeight, widthAndHeight), roundedCorners, roundedCorners, paint); - - paint.setColor(Color.WHITE); - - final String character = getRepresentativeCharacter(pageURL); - - final float textSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, TEXT_SIZE_DP, context.getResources().getDisplayMetrics()); - - paint.setTextAlign(Paint.Align.CENTER); - paint.setTextSize(textSize); - paint.setAntiAlias(true); - - canvas.drawText(character, - canvas.getWidth() / 2, - (int) ((canvas.getHeight() / 2) - ((paint.descent() + paint.ascent()) / 2)), - paint); - - return IconResponse.createGenerated(favicon, color); - } - - /** - * Get a representative character for the given URL. - * - * For example this method will return "f" for "http://m.facebook.com/foobar". - */ - @VisibleForTesting static String getRepresentativeCharacter(String url) { - if (TextUtils.isEmpty(url)) { - return "?"; - } - - final String snippet = getRepresentativeSnippet(url); - for (int i = 0; i < snippet.length(); i++) { - char c = snippet.charAt(i); - - if (Character.isLetterOrDigit(c)) { - return String.valueOf(Character.toUpperCase(c)); - } - } - - // Nothing found.. - return "?"; - } - - /** - * Return a color for this URL. Colors will be based on the host. URLs with the same host will - * return the same color. - */ - @VisibleForTesting static int pickColor(String url) { - if (TextUtils.isEmpty(url)) { - return COLORS[0]; - } - - final String snippet = getRepresentativeSnippet(url); - final int color = Math.abs(snippet.hashCode() % COLORS.length); - - return COLORS[color]; - } - - /** - * Get the representative part of the URL. Usually this is the host (without common prefixes). - */ - private static String getRepresentativeSnippet(@NonNull String url) { - Uri uri = Uri.parse(url); - - // Use the host if available - String snippet = uri.getHost(); - - if (TextUtils.isEmpty(snippet)) { - // If the uri does not have a host (e.g. file:// uris) then use the path - snippet = uri.getPath(); - } - - if (TextUtils.isEmpty(snippet)) { - // If we still have no snippet then just return the question mark - return "?"; - } - - // Strip common prefixes that we do not want to use to determine the representative character - for (String prefix : COMMON_PREFIXES) { - if (snippet.startsWith(prefix)) { - snippet = snippet.substring(prefix.length()); - } - } - - return snippet; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/icons/loader/IconLoader.java b/mobile/android/base/java/org/mozilla/gecko/icons/loader/IconLoader.java deleted file mode 100644 index 8158babc38..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/icons/loader/IconLoader.java +++ /dev/null @@ -1,23 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.icons.loader; - -import android.support.annotation.Nullable; - -import org.mozilla.gecko.icons.IconRequest; -import org.mozilla.gecko.icons.IconResponse; - -/** - * Generic interface for classes that can load icons. - */ -public interface IconLoader { - /** - * Loads the icon for this request or returns null if this loader can't load an icon for this - * request or just failed this time. - */ - @Nullable - IconResponse load(IconRequest request); -} diff --git a/mobile/android/base/java/org/mozilla/gecko/icons/loader/JarLoader.java b/mobile/android/base/java/org/mozilla/gecko/icons/loader/JarLoader.java deleted file mode 100644 index 882d32da5f..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/icons/loader/JarLoader.java +++ /dev/null @@ -1,45 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.icons.loader; - -import android.content.Context; -import android.util.Log; - -import org.mozilla.gecko.icons.IconRequest; -import org.mozilla.gecko.icons.IconResponse; -import org.mozilla.gecko.util.GeckoJarReader; - -/** - * Loader implementation for loading icons from the omni.ja (jar:jar: URLs). - * - * https://developer.mozilla.org/en-US/docs/Mozilla/About_omni.ja_(formerly_omni.jar) - */ -public class JarLoader implements IconLoader { - private static final String LOGTAG = "Gecko/JarLoader"; - - @Override - public IconResponse load(IconRequest request) { - if (request.shouldSkipDisk()) { - return null; - } - - final String iconUrl = request.getBestIcon().getUrl(); - - if (!iconUrl.startsWith("jar:jar:")) { - return null; - } - - try { - final Context context = request.getContext(); - return IconResponse.create( - GeckoJarReader.getBitmap(context, context.getResources(), iconUrl)); - } catch (Exception e) { - // Just about anything could happen here. - Log.w(LOGTAG, "Error fetching favicon from JAR.", e); - return null; - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/icons/loader/LegacyLoader.java b/mobile/android/base/java/org/mozilla/gecko/icons/loader/LegacyLoader.java deleted file mode 100644 index d1efc3ad97..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/icons/loader/LegacyLoader.java +++ /dev/null @@ -1,74 +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.icons.loader; - -import android.content.ContentResolver; -import android.content.Context; -import android.graphics.Bitmap; - -import org.mozilla.gecko.GeckoProfile; -import org.mozilla.gecko.db.BrowserDB; -import org.mozilla.gecko.icons.decoders.LoadFaviconResult; -import org.mozilla.gecko.icons.IconRequest; -import org.mozilla.gecko.icons.IconResponse; - -/** - * This legacy loader loads icons from the abandoned database storage. This loader should only exist - * for a couple of releases and be removed afterwards. - * - * When updating to an app version with the new loaders our initial storage won't have any data so - * we need to continue loading from the database storage until the new storage has a good set of data. - */ -public class LegacyLoader implements IconLoader { - @Override - public IconResponse load(IconRequest request) { - if (!request.shouldSkipNetwork()) { - // If we are allowed to load from the network for this request then just ommit the legacy - // loader and fetch a fresh new icon. - return null; - } - - if (request.shouldSkipDisk()) { - return null; - } - - if (request.getIconCount() > 1) { - // There are still other icon URLs to try. Let's try to load from the legacy loader only - // if there's one icon left and the other loads have failed. We will ignore the icon URL - // anyways and try to receive the legacy icon URL from the database. - return null; - } - - final Bitmap bitmap = loadBitmapFromDatabase(request); - - if (bitmap == null) { - return null; - } - - return IconResponse.create(bitmap); - } - - /* package-private */ Bitmap loadBitmapFromDatabase(IconRequest request) { - final Context context = request.getContext(); - final ContentResolver contentResolver = context.getContentResolver(); - final BrowserDB db = BrowserDB.from(context); - - // We ask the database for the favicon URL and ignore the icon URL in the request object: - // As we are not updating the database anymore the icon might be stored under a different URL. - final String legacyFaviconUrl = db.getFaviconURLFromPageURL(contentResolver, request.getPageUrl()); - if (legacyFaviconUrl == null) { - // No URL -> Nothing to load. - return null; - } - - final LoadFaviconResult result = db.getFaviconForUrl(context, context.getContentResolver(), legacyFaviconUrl); - if (result == null) { - return null; - } - - return result.getBestBitmap(request.getTargetSize()); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/icons/loader/MemoryLoader.java b/mobile/android/base/java/org/mozilla/gecko/icons/loader/MemoryLoader.java deleted file mode 100644 index 98b651fc76..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/icons/loader/MemoryLoader.java +++ /dev/null @@ -1,31 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.icons.loader; - -import org.mozilla.gecko.icons.IconRequest; -import org.mozilla.gecko.icons.IconResponse; -import org.mozilla.gecko.icons.storage.MemoryStorage; - -/** - * Loader implementation for loading icons from an in-memory cached (Implemented by MemoryStorage). - */ -public class MemoryLoader implements IconLoader { - private final MemoryStorage storage; - - public MemoryLoader() { - storage = MemoryStorage.get(); - } - - @Override - public IconResponse load(IconRequest request) { - if (request.shouldSkipMemory()) { - return null; - } - - final String iconUrl = request.getBestIcon().getUrl(); - return storage.getIcon(iconUrl); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/icons/preparation/AboutPagesPreparer.java b/mobile/android/base/java/org/mozilla/gecko/icons/preparation/AboutPagesPreparer.java deleted file mode 100644 index d335cbf514..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/icons/preparation/AboutPagesPreparer.java +++ /dev/null @@ -1,39 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.icons.preparation; - -import org.mozilla.gecko.AboutPages; -import org.mozilla.gecko.icons.IconDescriptor; -import org.mozilla.gecko.icons.IconRequest; -import org.mozilla.gecko.util.GeckoJarReader; - -import java.util.Collections; -import java.util.HashSet; -import java.util.Set; - -/** - * Preparer implementation for adding the omni.ja URL for internal about: pages. - */ -public class AboutPagesPreparer implements Preparer { - private Set aboutUrls; - - public AboutPagesPreparer() { - aboutUrls = new HashSet<>(); - - Collections.addAll(aboutUrls, AboutPages.DEFAULT_ICON_PAGES); - } - - @Override - public void prepare(IconRequest request) { - if (aboutUrls.contains(request.getPageUrl())) { - final String iconUrl = GeckoJarReader.getJarURL(request.getContext(), "chrome/chrome/content/branding/favicon64.png"); - - request.modify() - .icon(IconDescriptor.createLookupIcon(iconUrl)) - .deferBuild(); - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/icons/preparation/AddDefaultIconUrl.java b/mobile/android/base/java/org/mozilla/gecko/icons/preparation/AddDefaultIconUrl.java deleted file mode 100644 index 5bc7d1c1f2..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/icons/preparation/AddDefaultIconUrl.java +++ /dev/null @@ -1,39 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.icons.preparation; - -import android.text.TextUtils; - -import org.mozilla.gecko.icons.IconDescriptor; -import org.mozilla.gecko.icons.IconRequest; -import org.mozilla.gecko.icons.IconsHelper; -import org.mozilla.gecko.util.StringUtils; - -/** - * Preparer to add the "default/guessed" favicon URL (domain/favicon.ico) to the list of URLs to - * try loading the favicon from. - * - * The default URL will be added with a very low priority so that we will only try to load from this - * URL if all other options failed. - */ -public class AddDefaultIconUrl implements Preparer { - @Override - public void prepare(IconRequest request) { - if (!StringUtils.isHttpOrHttps(request.getPageUrl())) { - return; - } - - final String defaultFaviconUrl = IconsHelper.guessDefaultFaviconURL(request.getPageUrl()); - if (TextUtils.isEmpty(defaultFaviconUrl)) { - // We couldn't generate a default favicon URL for this URL. Nothing to do here. - return; - } - - request.modify() - .icon(IconDescriptor.createGenericIcon(defaultFaviconUrl)) - .deferBuild(); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/icons/preparation/FilterKnownFailureUrls.java b/mobile/android/base/java/org/mozilla/gecko/icons/preparation/FilterKnownFailureUrls.java deleted file mode 100644 index effd31a03d..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/icons/preparation/FilterKnownFailureUrls.java +++ /dev/null @@ -1,29 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.icons.preparation; - -import org.mozilla.gecko.icons.IconDescriptor; -import org.mozilla.gecko.icons.IconRequest; -import org.mozilla.gecko.icons.storage.FailureCache; - -import java.util.Iterator; - -public class FilterKnownFailureUrls implements Preparer { - @Override - public void prepare(IconRequest request) { - final FailureCache failureCache = FailureCache.get(); - final Iterator iterator = request.getIconIterator(); - - while (iterator.hasNext()) { - final IconDescriptor descriptor = iterator.next(); - - if (failureCache.isKnownFailure(descriptor.getUrl())) { - // Loading from this URL has failed in the past. Do not try again. - iterator.remove(); - } - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/icons/preparation/FilterMimeTypes.java b/mobile/android/base/java/org/mozilla/gecko/icons/preparation/FilterMimeTypes.java deleted file mode 100644 index a12dad2ad4..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/icons/preparation/FilterMimeTypes.java +++ /dev/null @@ -1,39 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.icons.preparation; - -import android.text.TextUtils; - -import org.mozilla.gecko.icons.IconDescriptor; -import org.mozilla.gecko.icons.IconRequest; -import org.mozilla.gecko.icons.IconsHelper; - -import java.util.Iterator; - -/** - * Preparer implementation to filter unknown MIME types to avoid loading images that we cannot decode. - */ -public class FilterMimeTypes implements Preparer { - @Override - public void prepare(IconRequest request) { - final Iterator iterator = request.getIconIterator(); - - while (iterator.hasNext()) { - final IconDescriptor descriptor = iterator.next(); - final String mimeType = descriptor.getMimeType(); - - if (TextUtils.isEmpty(mimeType)) { - // We do not have a MIME type for this icon, so we cannot know in advance if we are able - // to decode it. Let's just continue. - continue; - } - - if (!IconsHelper.canDecodeType(mimeType)) { - iterator.remove(); - } - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/icons/preparation/FilterPrivilegedUrls.java b/mobile/android/base/java/org/mozilla/gecko/icons/preparation/FilterPrivilegedUrls.java deleted file mode 100644 index abf34c0385..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/icons/preparation/FilterPrivilegedUrls.java +++ /dev/null @@ -1,30 +0,0 @@ -package org.mozilla.gecko.icons.preparation; - -import org.mozilla.gecko.icons.IconDescriptor; -import org.mozilla.gecko.icons.IconRequest; -import org.mozilla.gecko.util.StringUtils; - -import java.util.Iterator; - -/** - * Filter non http/https URLs if the request is not from privileged code. - */ -public class FilterPrivilegedUrls implements Preparer { - @Override - public void prepare(IconRequest request) { - if (request.isPrivileged()) { - // This request is privileged. No need to filter anything. - return; - } - - final Iterator iterator = request.getIconIterator(); - - while (iterator.hasNext()) { - IconDescriptor descriptor = iterator.next(); - - if (!StringUtils.isHttpOrHttps(descriptor.getUrl())) { - iterator.remove(); - } - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/icons/preparation/LookupIconUrl.java b/mobile/android/base/java/org/mozilla/gecko/icons/preparation/LookupIconUrl.java deleted file mode 100644 index 0c76411120..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/icons/preparation/LookupIconUrl.java +++ /dev/null @@ -1,56 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.icons.preparation; - -import org.mozilla.gecko.icons.IconDescriptor; -import org.mozilla.gecko.icons.IconRequest; -import org.mozilla.gecko.icons.storage.DiskStorage; -import org.mozilla.gecko.icons.storage.MemoryStorage; - -/** - * Preparer implementation to lookup the icon URL for the page URL in the request. This class tries - * to locate the icon URL by looking through previously stored mappings on disk and in memory. - */ -public class LookupIconUrl implements Preparer { - @Override - public void prepare(IconRequest request) { - if (lookupFromMemory(request)) { - return; - } - - lookupFromDisk(request); - } - - private boolean lookupFromMemory(IconRequest request) { - final String iconUrl = MemoryStorage.get() - .getMapping(request.getPageUrl()); - - if (iconUrl != null) { - request.modify() - .icon(IconDescriptor.createLookupIcon(iconUrl)) - .deferBuild(); - - return true; - } - - return false; - } - - private boolean lookupFromDisk(IconRequest request) { - final String iconUrl = DiskStorage.get(request.getContext()) - .getMapping(request.getPageUrl()); - - if (iconUrl != null) { - request.modify() - .icon(IconDescriptor.createLookupIcon(iconUrl)) - .deferBuild(); - - return true; - } - - return false; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/icons/preparation/Preparer.java b/mobile/android/base/java/org/mozilla/gecko/icons/preparation/Preparer.java deleted file mode 100644 index 466307ead6..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/icons/preparation/Preparer.java +++ /dev/null @@ -1,19 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.icons.preparation; - -import org.mozilla.gecko.icons.IconRequest; - -/** - * Generic interface for a class "preparing" a request before we try to load icons. A class - * implementing this interface can modify the request (e.g. filter or add icon URLs). - */ -public interface Preparer { - /** - * Inspects or modifies the request before any icon is loaded. - */ - void prepare(IconRequest request); -} diff --git a/mobile/android/base/java/org/mozilla/gecko/icons/processing/ColorProcessor.java b/mobile/android/base/java/org/mozilla/gecko/icons/processing/ColorProcessor.java deleted file mode 100644 index 3f71100346..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/icons/processing/ColorProcessor.java +++ /dev/null @@ -1,61 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.icons.processing; - -import android.support.v7.graphics.Palette; -import android.util.Log; - -import org.mozilla.gecko.gfx.BitmapUtils; -import org.mozilla.gecko.icons.IconRequest; -import org.mozilla.gecko.icons.IconResponse; -import org.mozilla.gecko.util.HardwareUtils; - -/** - * Processor implementation to extract the dominant color from the icon and attach it to the icon - * response object. - */ -public class ColorProcessor implements Processor { - private static final String LOGTAG = "GeckoColorProcessor"; - private static final int DEFAULT_COLOR = 0; // 0 == No color - - @Override - public void process(IconRequest request, IconResponse response) { - if (response.hasColor()) { - return; - } - - if (HardwareUtils.isX86System()) { - // (Bug 1318667) We are running into crashes when using the palette library with - // specific icons on x86 devices. They take down the whole VM and are not recoverable. - // Unfortunately our release icon is triggering this crash. Until we can switch to a - // newer version of the support library where this does not happen, we are using our - // own slower implementation. - extractColorUsingCustomImplementation(response); - } else { - extractColorUsingPaletteSupportLibrary(response); - } - } - - private void extractColorUsingPaletteSupportLibrary(final IconResponse response) { - try { - final Palette palette = Palette.from(response.getBitmap()).generate(); - response.updateColor(palette.getVibrantColor(DEFAULT_COLOR)); - } catch (ArrayIndexOutOfBoundsException e) { - // We saw the palette library fail with an ArrayIndexOutOfBoundsException intermittently - // in automation. In this case lets just swallow the exception and move on without a - // color. This is a valid condition and callers should handle this gracefully (Bug 1318560). - Log.e(LOGTAG, "Palette generation failed with ArrayIndexOutOfBoundsException", e); - - response.updateColor(DEFAULT_COLOR); - } - } - - private void extractColorUsingCustomImplementation(final IconResponse response) { - final int dominantColor = BitmapUtils.getDominantColor(response.getBitmap()); - - response.updateColor(dominantColor); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/icons/processing/DiskProcessor.java b/mobile/android/base/java/org/mozilla/gecko/icons/processing/DiskProcessor.java deleted file mode 100644 index 150aa503b8..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/icons/processing/DiskProcessor.java +++ /dev/null @@ -1,36 +0,0 @@ -package org.mozilla.gecko.icons.processing; - -import org.mozilla.gecko.icons.IconRequest; -import org.mozilla.gecko.icons.IconResponse; -import org.mozilla.gecko.icons.storage.DiskStorage; -import org.mozilla.gecko.util.StringUtils; - -public class DiskProcessor implements Processor { - @Override - public void process(IconRequest request, IconResponse response) { - if (request.shouldSkipDisk()) { - return; - } - - if (!response.hasUrl() || !StringUtils.isHttpOrHttps(response.getUrl())) { - // If the response does not contain an URL from which the icon was loaded or if this is - // not a http(s) URL then we cannot store this or do not need to (because it's already - // stored somewhere else, like for URLs pointing inside the omni.ja). - return; - } - - final DiskStorage storage = DiskStorage.get(request.getContext()); - - if (response.isFromNetwork()) { - // The icon has been loaded from the network. Store it on the disk now. - storage.putIcon(response); - } - - if (response.isFromMemory() || response.isFromDisk() || response.isFromNetwork()) { - // Remember mapping between page URL and storage URL. Even when this icon has been loaded - // from memory or disk this does not mean that we stored this mapping already: We could - // have loaded this icon for a different page URL previously. - storage.putMapping(request, response.getUrl()); - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/icons/processing/MemoryProcessor.java b/mobile/android/base/java/org/mozilla/gecko/icons/processing/MemoryProcessor.java deleted file mode 100644 index 245faded54..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/icons/processing/MemoryProcessor.java +++ /dev/null @@ -1,38 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.icons.processing; - -import org.mozilla.gecko.icons.IconRequest; -import org.mozilla.gecko.icons.IconResponse; -import org.mozilla.gecko.icons.storage.MemoryStorage; - -public class MemoryProcessor implements Processor { - private final MemoryStorage storage; - - public MemoryProcessor() { - storage = MemoryStorage.get(); - } - - @Override - public void process(IconRequest request, IconResponse response) { - if (request.shouldSkipMemory() || request.getIconCount() == 0 || response.isGenerated()) { - // Do not cache this icon in memory if we should skip the memory cache or if this icon - // has been generated. We can re-generate it if needed. - return; - } - - final String iconUrl = request.getBestIcon().getUrl(); - - if (iconUrl.startsWith("data:image/")) { - // The image data is encoded in the URL. It doesn't make sense to store the URL and the - // bitmap in cache. - return; - } - - storage.putMapping(request, iconUrl); - storage.putIcon(iconUrl, response); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/icons/processing/Processor.java b/mobile/android/base/java/org/mozilla/gecko/icons/processing/Processor.java deleted file mode 100644 index df7d63c6cc..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/icons/processing/Processor.java +++ /dev/null @@ -1,21 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.icons.processing; - -import org.mozilla.gecko.icons.IconRequest; -import org.mozilla.gecko.icons.IconResponse; - -/** - * Generic interface for a class that processes a response object after an icon has been loaded and - * decoded. A class implementing this interface can attach additional data to the response or modify - * the bitmap (e.g. resizing). - */ -public interface Processor { - /** - * Process a response object containing an icon loaded for this request. - */ - void process(IconRequest request, IconResponse response); -} diff --git a/mobile/android/base/java/org/mozilla/gecko/icons/processing/ResizingProcessor.java b/mobile/android/base/java/org/mozilla/gecko/icons/processing/ResizingProcessor.java deleted file mode 100644 index 63b4790219..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/icons/processing/ResizingProcessor.java +++ /dev/null @@ -1,68 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.icons.processing; - -import android.graphics.Bitmap; -import android.support.annotation.VisibleForTesting; - -import org.mozilla.gecko.icons.IconRequest; -import org.mozilla.gecko.icons.IconResponse; - -/** - * Processor implementation for resizing the loaded icon based on the target size. - */ -public class ResizingProcessor implements Processor { - @Override - public void process(IconRequest request, IconResponse response) { - if (response.isFromMemory()) { - // This bitmap has been loaded from memory, so it has already gone through the resizing - // process. We do not want to resize the image every time we hit the memory cache. - return; - } - - final Bitmap originalBitmap = response.getBitmap(); - final int size = originalBitmap.getWidth(); - - final int targetSize = request.getTargetSize(); - - if (size == targetSize) { - // The bitmap has exactly the size we are looking for. - return; - } - - final Bitmap resizedBitmap; - - if (size > targetSize) { - resizedBitmap = resize(originalBitmap, targetSize); - } else { - // Our largest primary is smaller than the desired size. Upscale by a maximum of 2x. - // 'largestSize' now reflects the maximum size we can upscale to. - final int largestSize = size * 2; - - if (largestSize > targetSize) { - // Perfect! We can upscale by less than 2x and reach the needed size. Do it. - resizedBitmap = resize(originalBitmap, targetSize); - } else { - // We don't have enough information to make the target size look non terrible. Best effort: - resizedBitmap = resize(originalBitmap, largestSize); - } - } - - response.updateBitmap(resizedBitmap); - - originalBitmap.recycle(); - } - - @VisibleForTesting Bitmap resize(Bitmap bitmap, int targetSize) { - try { - return Bitmap.createScaledBitmap(bitmap, targetSize, targetSize, true); - } catch (OutOfMemoryError error) { - // There's not enough memory to create a resized copy of the bitmap in memory. Let's just - // use what we have. - return bitmap; - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/icons/storage/DiskStorage.java b/mobile/android/base/java/org/mozilla/gecko/icons/storage/DiskStorage.java deleted file mode 100644 index 3c0e2a5542..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/icons/storage/DiskStorage.java +++ /dev/null @@ -1,293 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.icons.storage; - -import android.content.Context; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.support.annotation.CheckResult; -import android.support.annotation.Nullable; -import android.util.Log; - -import com.jakewharton.disklrucache.DiskLruCache; - -import org.mozilla.gecko.background.nativecode.NativeCrypto; -import org.mozilla.gecko.icons.IconRequest; -import org.mozilla.gecko.icons.IconResponse; -import org.mozilla.gecko.sync.Utils; -import org.mozilla.gecko.util.IOUtils; - -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.io.UnsupportedEncodingException; -import java.security.MessageDigest; - -/** - * Least Recently Used (LRU) disk cache for icons and the mappings from page URLs to icon URLs. - */ -public class DiskStorage { - private static final String LOGTAG = "Gecko/DiskStorage"; - - /** - * Maximum size (in bytes) of the cache. This cache is located in the cache directory of the - * application and can be cleared by the user. - */ - private static final int DISK_CACHE_SIZE = 50 * 1024 * 1024; - - /** - * Version of the cache. Updating the version will invalidate all existing items. - */ - private static final int CACHE_VERSION = 1; - - private static final String KEY_PREFIX_ICON = "icon:"; - private static final String KEY_PREFIX_MAPPING = "mapping:"; - - private static DiskStorage instance; - - public static DiskStorage get(Context context) { - if (instance == null) { - instance = new DiskStorage(context); - } - - return instance; - } - - private Context context; - private DiskLruCache cache; - - private DiskStorage(Context context) { - this.context = context.getApplicationContext(); - } - - @CheckResult - private synchronized DiskLruCache ensureCacheIsReady() throws IOException { - if (cache == null || cache.isClosed()) { - cache = DiskLruCache.open( - new File(context.getCacheDir(), "icons"), - CACHE_VERSION, - 1, - DISK_CACHE_SIZE); - } - - return cache; - } - - /** - * Store a mapping from page URL to icon URL in the cache. - */ - public void putMapping(IconRequest request, String iconUrl) { - putMapping(request.getPageUrl(), iconUrl); - } - - /** - * Store a mapping from page URL to icon URL in the cache. - */ - public void putMapping(String pageUrl, String iconUrl) { - DiskLruCache.Editor editor = null; - - try { - final DiskLruCache cache = ensureCacheIsReady(); - - final String key = createKey(KEY_PREFIX_MAPPING, pageUrl); - if (key == null) { - return; - } - - editor = cache.edit(key); - if (editor == null) { - return; - } - - editor.set(0, iconUrl); - editor.commit(); - } catch (IOException e) { - Log.w(LOGTAG, "IOException while accessing disk cache", e); - - abortSilently(editor); - } - } - - /** - * Store an icon in the cache (uses the icon URL as key). - */ - public void putIcon(IconResponse response) { - putIcon(response.getUrl(), response.getBitmap()); - } - - /** - * Store an icon in the cache (uses the icon URL as key). - */ - public void putIcon(String iconUrl, Bitmap bitmap) { - OutputStream outputStream = null; - DiskLruCache.Editor editor = null; - - try { - final DiskLruCache cache = ensureCacheIsReady(); - - final String key = createKey(KEY_PREFIX_ICON, iconUrl); - if (key == null) { - return; - } - - editor = cache.edit(key); - if (editor == null) { - return; - } - - outputStream = editor.newOutputStream(0); - boolean success = bitmap.compress(Bitmap.CompressFormat.PNG, 100 /* quality; ignored. PNG is lossless */, outputStream); - - outputStream.close(); - - if (success) { - editor.commit(); - } else { - editor.abort(); - } - } catch (IOException e) { - Log.w(LOGTAG, "IOException while accessing disk cache", e); - - abortSilently(editor); - } finally { - IOUtils.safeStreamClose(outputStream); - } - } - - - - /** - * Get an icon for the icon URL from the cache. Returns null if no icon is cached for this URL. - */ - @Nullable - public IconResponse getIcon(String iconUrl) { - InputStream inputStream = null; - - try { - final DiskLruCache cache = ensureCacheIsReady(); - - final String key = createKey(KEY_PREFIX_ICON, iconUrl); - if (key == null) { - return null; - } - - if (cache.isClosed()) { - throw new RuntimeException("CLOSED"); - } - - final DiskLruCache.Snapshot snapshot = cache.get(key); - if (snapshot == null) { - return null; - } - - inputStream = snapshot.getInputStream(0); - - final Bitmap bitmap = BitmapFactory.decodeStream(inputStream); - if (bitmap == null) { - return null; - } - - return IconResponse.createFromDisk(bitmap, iconUrl); - } catch (IOException e) { - Log.w(LOGTAG, "IOException while accessing disk cache", e); - } finally { - IOUtils.safeStreamClose(inputStream); - } - - return null; - } - - /** - * Get the icon URL for this page URL. Returns null if no mapping is in the cache. - */ - @Nullable - public String getMapping(String pageUrl) { - try { - final DiskLruCache cache = ensureCacheIsReady(); - - final String key = createKey(KEY_PREFIX_MAPPING, pageUrl); - if (key == null) { - return null; - } - - DiskLruCache.Snapshot snapshot = cache.get(key); - if (snapshot == null) { - return null; - } - - return snapshot.getString(0); - } catch (IOException e) { - Log.w(LOGTAG, "IOException while accessing disk cache", e); - } - - return null; - } - - /** - * Remove all entries from this cache. - */ - public void evictAll() { - try { - final DiskLruCache cache = ensureCacheIsReady(); - - cache.delete(); - - } catch (IOException e) { - Log.w(LOGTAG, "IOException while accessing disk cache", e); - } - } - - /** - * Create a key for this URL using the given prefix. - * - * The disk cache requires valid file names to be used as key. Therefore we hash the created key - * (SHA-256). - */ - @Nullable - private String createKey(String prefix, String url) { - try { - // We use our own crypto implementation to avoid the penalty of loading the java crypto - // framework. - byte[] ctx = NativeCrypto.sha256init(); - if (ctx == null) { - return null; - } - - byte[] data = prefix.getBytes("UTF-8"); - NativeCrypto.sha256update(ctx, data, data.length); - - data = url.getBytes("UTF-8"); - NativeCrypto.sha256update(ctx, data, data.length); - return Utils.byte2Hex(NativeCrypto.sha256finalize(ctx)); - } catch (NoClassDefFoundError | ExceptionInInitializerError error) { - // We could not load libmozglue.so. Let's use Java's MessageDigest as fallback. We do - // this primarily for our unit tests that can't load native libraries. On an device - // we will have a lot of other problems if we can't load libmozglue.so - try { - MessageDigest md = MessageDigest.getInstance("SHA-256"); - md.update(prefix.getBytes("UTF-8")); - md.update(url.getBytes("UTF-8")); - return Utils.byte2Hex(md.digest()); - } catch (Exception e) { - // Just give up. And let everyone know. - throw new RuntimeException(e); - } - } catch (UnsupportedEncodingException e) { - throw new AssertionError("Should not happen: Device does not understand UTF-8"); - } - } - - private void abortSilently(DiskLruCache.Editor editor) { - if (editor != null) { - try { - editor.abort(); - } catch (IOException e) { - // Ignore - } - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/icons/storage/FailureCache.java b/mobile/android/base/java/org/mozilla/gecko/icons/storage/FailureCache.java deleted file mode 100644 index b45cb0fce1..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/icons/storage/FailureCache.java +++ /dev/null @@ -1,70 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.icons.storage; - -import android.os.SystemClock; -import android.support.annotation.VisibleForTesting; -import android.util.LruCache; - -/** - * In-memory cache to remember URLs from which loading icons has failed recently. - */ -public class FailureCache { - /** - * Retry loading failed icons after 4 hours. - */ - private static final long FAILURE_RETRY_MILLISECONDS = 1000 * 60 * 60 * 4; - - private static final int MAX_ENTRIES = 25; - - private static FailureCache instance; - - public static synchronized FailureCache get() { - if (instance == null) { - instance = new FailureCache(); - } - - return instance; - } - - private final LruCache cache; - - private FailureCache() { - cache = new LruCache<>(MAX_ENTRIES); - } - - /** - * Remember this icon URL after loading from it (over the network) has failed. - */ - public void rememberFailure(String iconUrl) { - cache.put(iconUrl, SystemClock.elapsedRealtime()); - } - - /** - * Has loading from this URL failed previously and recently? - */ - public boolean isKnownFailure(String iconUrl) { - synchronized (cache) { - final Long failedAt = cache.get(iconUrl); - if (failedAt == null) { - return false; - } - - if (failedAt + FAILURE_RETRY_MILLISECONDS < SystemClock.elapsedRealtime()) { - // The wait time has passed and we can retry loading from this URL. - cache.remove(iconUrl); - return false; - } - } - - return true; - } - - @VisibleForTesting - public void evictAll() { - cache.evictAll(); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/icons/storage/MemoryStorage.java b/mobile/android/base/java/org/mozilla/gecko/icons/storage/MemoryStorage.java deleted file mode 100644 index e0a96f7c7e..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/icons/storage/MemoryStorage.java +++ /dev/null @@ -1,112 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.icons.storage; - -import android.graphics.Bitmap; -import android.support.annotation.Nullable; -import android.util.Log; -import android.util.LruCache; - -import org.mozilla.gecko.icons.IconRequest; -import org.mozilla.gecko.icons.IconResponse; - -/** - * Least Recently Used (LRU) memory cache for icons and the mappings from page URLs to icon URLs. - */ -public class MemoryStorage { - /** - * Maximum number of items in the cache for mapping page URLs to icon URLs. - */ - private static final int MAPPING_CACHE_SIZE = 500; - - private static MemoryStorage instance; - - public static synchronized MemoryStorage get() { - if (instance == null) { - instance = new MemoryStorage(); - } - - return instance; - } - - /** - * Class representing an cached icon. We store the original bitmap and the color in cache only. - */ - private static class CacheEntry { - private final Bitmap bitmap; - private final int color; - - private CacheEntry(Bitmap bitmap, int color) { - this.bitmap = bitmap; - this.color = color; - } - } - - private final LruCache iconCache; // Guarded by 'this' - private final LruCache mappingCache; // Guarded by 'this' - - private MemoryStorage() { - iconCache = new LruCache(calculateCacheSize()) { - @Override - protected int sizeOf(String key, CacheEntry value) { - return value.bitmap.getByteCount() / 1024; - } - }; - - mappingCache = new LruCache<>(MAPPING_CACHE_SIZE); - } - - private int calculateCacheSize() { - // Use a maximum of 1/8 of the available memory for storing cached icons. - int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024); - return maxMemory / 8; - } - - /** - * Store a mapping from page URL to icon URL in the cache. - */ - public synchronized void putMapping(IconRequest request, String iconUrl) { - mappingCache.put(request.getPageUrl(), iconUrl); - } - - /** - * Get the icon URL for this page URL. Returns null if no mapping is in the cache. - */ - @Nullable - public synchronized String getMapping(String pageUrl) { - return mappingCache.get(pageUrl); - } - - /** - * Store an icon in the cache (uses the icon URL as key). - */ - public synchronized void putIcon(String url, IconResponse response) { - final CacheEntry entry = new CacheEntry(response.getBitmap(), response.getColor()); - - iconCache.put(url, entry); - } - - /** - * Get an icon for the icon URL from the cache. Returns null if no icon is cached for this URL. - */ - @Nullable - public synchronized IconResponse getIcon(String iconUrl) { - final CacheEntry entry = iconCache.get(iconUrl); - if (entry == null) { - return null; - } - - return IconResponse.createFromMemory(entry.bitmap, iconUrl, entry.color); - } - - /** - * Remove all entries from this cache. - */ - public synchronized void evictAll() { - iconCache.evictAll(); - mappingCache.evictAll(); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/javaaddons/JavaAddonManager.java b/mobile/android/base/java/org/mozilla/gecko/javaaddons/JavaAddonManager.java deleted file mode 100644 index 33a97955f4..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/javaaddons/JavaAddonManager.java +++ /dev/null @@ -1,195 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.javaaddons; - -import android.content.Context; -import android.os.Bundle; -import android.os.Handler; -import android.os.Message; -import android.util.Log; -import dalvik.system.DexClassLoader; -import org.json.JSONException; -import org.json.JSONObject; -import org.mozilla.gecko.EventDispatcher; -import org.mozilla.gecko.util.GeckoEventListener; - -import java.io.File; -import java.lang.reflect.Constructor; -import java.util.HashMap; -import java.util.Iterator; -import java.util.Map; - -/** - * The manager for addon-provided Java code. - * - * Java code in addons can be loaded using the Dex:Load message, and unloaded - * via the Dex:Unload message. Addon classes loaded are checked for a constructor - * that takes a Map<String, Handler.Callback>. If such a constructor - * exists, it is called and the objects populated into the map by the constructor - * are registered as event listeners. If no such constructor exists, the default - * constructor is invoked instead. - * - * Note: The Map and Handler.Callback classes were used in this API definition - * rather than defining a custom class. This was done explicitly so that the - * addon code can be compiled against the android.jar provided in the Android - * SDK, rather than having to be compiled against Fennec source code. - * - * The Handler.Callback instances provided (as described above) are invoked with - * Message objects when the corresponding events are dispatched. The Bundle - * object attached to the Message will contain the "primitive" values from the - * JSON of the event. ("primitive" includes bool/int/long/double/String). If - * the addon callback wishes to synchronously return a value back to the event - * dispatcher, they can do so by inserting the response string into the bundle - * under the key "response". - */ -public class JavaAddonManager implements GeckoEventListener { - private static final String LOGTAG = "GeckoJavaAddonManager"; - - private static JavaAddonManager sInstance; - - private final EventDispatcher mDispatcher; - private final Map> mAddonCallbacks; - - private Context mApplicationContext; - - public static JavaAddonManager getInstance() { - if (sInstance == null) { - sInstance = new JavaAddonManager(); - } - return sInstance; - } - - private JavaAddonManager() { - mDispatcher = EventDispatcher.getInstance(); - mAddonCallbacks = new HashMap>(); - } - - public void init(Context applicationContext) { - if (mApplicationContext != null) { - // we've already done this registration. don't do it again - return; - } - mApplicationContext = applicationContext; - mDispatcher.registerGeckoThreadListener(this, - "Dex:Load", - "Dex:Unload"); - JavaAddonManagerV1.getInstance().init(applicationContext); - } - - @Override - public void handleMessage(String event, JSONObject message) { - try { - if (event.equals("Dex:Load")) { - String zipFile = message.getString("zipfile"); - String implClass = message.getString("impl"); - Log.d(LOGTAG, "Attempting to load classes.dex file from " + zipFile + " and instantiate " + implClass); - try { - File tmpDir = mApplicationContext.getDir("dex", 0); - DexClassLoader loader = new DexClassLoader(zipFile, tmpDir.getAbsolutePath(), null, mApplicationContext.getClassLoader()); - Class c = loader.loadClass(implClass); - try { - Constructor constructor = c.getDeclaredConstructor(Map.class); - Map callbacks = new HashMap(); - constructor.newInstance(callbacks); - registerCallbacks(zipFile, callbacks); - } catch (NoSuchMethodException nsme) { - Log.d(LOGTAG, "Did not find constructor with parameters Map. Falling back to default constructor..."); - // fallback for instances with no constructor that takes a Map - c.newInstance(); - } - } catch (Exception e) { - Log.e(LOGTAG, "Unable to load dex successfully", e); - } - } else if (event.equals("Dex:Unload")) { - String zipFile = message.getString("zipfile"); - unregisterCallbacks(zipFile); - } - } catch (JSONException e) { - Log.e(LOGTAG, "Exception handling message [" + event + "]:", e); - } - } - - private void registerCallbacks(String zipFile, Map callbacks) { - Map addonCallbacks = mAddonCallbacks.get(zipFile); - if (addonCallbacks != null) { - Log.w(LOGTAG, "Found pre-existing callbacks for zipfile [" + zipFile + "]; aborting re-registration!"); - return; - } - addonCallbacks = new HashMap(); - for (String event : callbacks.keySet()) { - CallbackWrapper wrapper = new CallbackWrapper(callbacks.get(event)); - mDispatcher.registerGeckoThreadListener(wrapper, event); - addonCallbacks.put(event, wrapper); - } - mAddonCallbacks.put(zipFile, addonCallbacks); - } - - private void unregisterCallbacks(String zipFile) { - Map callbacks = mAddonCallbacks.remove(zipFile); - if (callbacks == null) { - Log.w(LOGTAG, "Attempting to unregister callbacks from zipfile [" + zipFile + "] which has no callbacks registered."); - return; - } - for (String event : callbacks.keySet()) { - mDispatcher.unregisterGeckoThreadListener(callbacks.get(event), event); - } - } - - private static class CallbackWrapper implements GeckoEventListener { - private final Handler.Callback mDelegate; - private Bundle mBundle; - - CallbackWrapper(Handler.Callback delegate) { - mDelegate = delegate; - } - - private Bundle jsonToBundle(JSONObject json) { - // XXX right now we only support primitive types; - // we don't recurse down into JSONArray or JSONObject instances - Bundle b = new Bundle(); - for (Iterator keys = json.keys(); keys.hasNext(); ) { - try { - String key = (String)keys.next(); - Object value = json.get(key); - if (value instanceof Integer) { - b.putInt(key, (Integer)value); - } else if (value instanceof String) { - b.putString(key, (String)value); - } else if (value instanceof Boolean) { - b.putBoolean(key, (Boolean)value); - } else if (value instanceof Long) { - b.putLong(key, (Long)value); - } else if (value instanceof Double) { - b.putDouble(key, (Double)value); - } - } catch (JSONException e) { - Log.d(LOGTAG, "Error during JSON->bundle conversion", e); - } - } - return b; - } - - @Override - public void handleMessage(String event, JSONObject json) { - try { - if (mBundle != null) { - Log.w(LOGTAG, "Event [" + event + "] handler is re-entrant; response messages may be lost"); - } - mBundle = jsonToBundle(json); - Message msg = new Message(); - msg.setData(mBundle); - mDelegate.handleMessage(msg); - - JSONObject obj = new JSONObject(); - obj.put("response", mBundle.getString("response")); - EventDispatcher.sendResponse(json, obj); - mBundle = null; - } catch (Exception e) { - Log.e(LOGTAG, "Caught exception thrown from wrapped addon message handler", e); - } - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/javaaddons/JavaAddonManagerV1.java b/mobile/android/base/java/org/mozilla/gecko/javaaddons/JavaAddonManagerV1.java deleted file mode 100644 index f361773ca5..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/javaaddons/JavaAddonManagerV1.java +++ /dev/null @@ -1,260 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.javaaddons; - -import android.content.Context; -import android.util.Log; -import android.util.Pair; -import dalvik.system.DexClassLoader; -import org.json.JSONException; -import org.json.JSONObject; -import org.mozilla.gecko.GeckoAppShell; -import org.mozilla.gecko.sync.Utils; -import org.mozilla.gecko.util.GeckoJarReader; -import org.mozilla.gecko.util.GeckoRequest; -import org.mozilla.gecko.util.NativeEventListener; -import org.mozilla.gecko.util.NativeJSObject; -import org.mozilla.javaaddons.JavaAddonInterfaceV1; - -import java.io.File; -import java.io.IOException; -import java.lang.reflect.Constructor; -import java.lang.reflect.InvocationTargetException; -import java.util.HashMap; -import java.util.IdentityHashMap; -import java.util.Map; - -public class JavaAddonManagerV1 implements NativeEventListener { - private static final String LOGTAG = "GeckoJavaAddonMgrV1"; - public static final String MESSAGE_LOAD = "JavaAddonManagerV1:Load"; - public static final String MESSAGE_UNLOAD = "JavaAddonManagerV1:Unload"; - - private static JavaAddonManagerV1 sInstance; - - // Protected by static synchronized. - private Context mApplicationContext; - - private final org.mozilla.gecko.EventDispatcher mDispatcher; - - // Protected by synchronized (this). - private final Map mGUIDToDispatcherMap = new HashMap<>(); - - public static synchronized JavaAddonManagerV1 getInstance() { - if (sInstance == null) { - sInstance = new JavaAddonManagerV1(); - } - return sInstance; - } - - private JavaAddonManagerV1() { - mDispatcher = org.mozilla.gecko.EventDispatcher.getInstance(); - } - - public synchronized void init(Context applicationContext) { - if (mApplicationContext != null) { - // We've already registered; don't register again. - return; - } - mApplicationContext = applicationContext; - mDispatcher.registerGeckoThreadListener(this, - MESSAGE_LOAD, - MESSAGE_UNLOAD); - } - - protected String getExtension(String filename) { - if (filename == null) { - return ""; - } - final int last = filename.lastIndexOf("."); - if (last < 0) { - return ""; - } - return filename.substring(last); - } - - protected synchronized EventDispatcherImpl registerNewInstance(String classname, String filename) - throws ClassNotFoundException, InstantiationException, IllegalAccessException, InvocationTargetException, NoSuchMethodException, IOException { - Log.d(LOGTAG, "Attempting to instantiate " + classname + "from filename " + filename); - - // It's important to maintain the extension, either .dex, .apk, .jar. - final String extension = getExtension(filename); - final File dexFile = GeckoJarReader.extractStream(mApplicationContext, filename, mApplicationContext.getCacheDir(), "." + extension); - try { - if (dexFile == null) { - throw new IOException("Could not find file " + filename); - } - final File tmpDir = mApplicationContext.getDir("dex", 0); // We'd prefer getCodeCacheDir but it's API 21+. - final DexClassLoader loader = new DexClassLoader(dexFile.getAbsolutePath(), tmpDir.getAbsolutePath(), null, mApplicationContext.getClassLoader()); - final Class c = loader.loadClass(classname); - final Constructor constructor = c.getDeclaredConstructor(Context.class, JavaAddonInterfaceV1.EventDispatcher.class); - final String guid = Utils.generateGuid(); - final EventDispatcherImpl dispatcher = new EventDispatcherImpl(guid, filename); - final Object instance = constructor.newInstance(mApplicationContext, dispatcher); - mGUIDToDispatcherMap.put(guid, dispatcher); - return dispatcher; - } finally { - // DexClassLoader writes an optimized version, so we can get rid of our temporary extracted version. - if (dexFile != null) { - dexFile.delete(); - } - } - } - - @Override - public synchronized void handleMessage(String event, NativeJSObject message, org.mozilla.gecko.util.EventCallback callback) { - try { - switch (event) { - case MESSAGE_LOAD: { - if (callback == null) { - throw new IllegalArgumentException("callback must not be null"); - } - final String classname = message.getString("classname"); - final String filename = message.getString("filename"); - final EventDispatcherImpl dispatcher = registerNewInstance(classname, filename); - callback.sendSuccess(dispatcher.guid); - } - break; - case MESSAGE_UNLOAD: { - if (callback == null) { - throw new IllegalArgumentException("callback must not be null"); - } - final String guid = message.getString("guid"); - final EventDispatcherImpl dispatcher = mGUIDToDispatcherMap.remove(guid); - if (dispatcher == null) { - Log.w(LOGTAG, "Attempting to unload addon with unknown associated dispatcher; ignoring."); - callback.sendSuccess(false); - } - dispatcher.unregisterAllEventListeners(); - callback.sendSuccess(true); - } - break; - } - } catch (Exception e) { - Log.e(LOGTAG, "Exception handling message [" + event + "]", e); - if (callback != null) { - callback.sendError("Exception handling message [" + event + "]: " + e.toString()); - } - } - } - - /** - * An event dispatcher is tied to a single Java Addon instance. It serves to prefix all - * messages with its unique GUID. - *

- * Curiously, the dispatcher does not hold a direct reference to its add-on instance. It will - * likely hold indirect instances through its wrapping map, since the instance will probably - * register event listeners that hold a reference to itself. When these listeners are - * unregistered, any link will be broken, allowing the instances to be garbage collected. - */ - private class EventDispatcherImpl implements JavaAddonInterfaceV1.EventDispatcher { - private final String guid; - private final String dexFileName; - - // Protected by synchronized (this). - private final Map> mListenerToWrapperMap = new IdentityHashMap<>(); - - public EventDispatcherImpl(String guid, String dexFileName) { - this.guid = guid; - this.dexFileName = dexFileName; - } - - protected class ListenerWrapper implements NativeEventListener { - private final JavaAddonInterfaceV1.EventListener listener; - - public ListenerWrapper(JavaAddonInterfaceV1.EventListener listener) { - this.listener = listener; - } - - @Override - public void handleMessage(String prefixedEvent, NativeJSObject message, final org.mozilla.gecko.util.EventCallback callback) { - if (!prefixedEvent.startsWith(guid + ":")) { - return; - } - final String event = prefixedEvent.substring(guid.length() + 1); // Skip "guid:". - try { - JavaAddonInterfaceV1.EventCallback callbackAdapter = null; - if (callback != null) { - callbackAdapter = new JavaAddonInterfaceV1.EventCallback() { - @Override - public void sendSuccess(Object response) { - callback.sendSuccess(response); - } - - @Override - public void sendError(Object response) { - callback.sendError(response); - } - }; - } - final JSONObject json = new JSONObject(message.toString()); - listener.handleMessage(mApplicationContext, event, json, callbackAdapter); - } catch (Exception e) { - Log.e(LOGTAG, "Exception handling message [" + prefixedEvent + "]", e); - if (callback != null) { - callback.sendError("Got exception handling message [" + prefixedEvent + "]: " + e.toString()); - } - } - } - } - - @Override - public synchronized void registerEventListener(final JavaAddonInterfaceV1.EventListener listener, String... events) { - if (mListenerToWrapperMap.containsKey(listener)) { - Log.e(LOGTAG, "Attempting to register listener which is already registered; ignoring."); - return; - } - - final NativeEventListener listenerWrapper = new ListenerWrapper(listener); - - final String[] prefixedEvents = new String[events.length]; - for (int i = 0; i < events.length; i++) { - prefixedEvents[i] = this.guid + ":" + events[i]; - } - mDispatcher.registerGeckoThreadListener(listenerWrapper, prefixedEvents); - mListenerToWrapperMap.put(listener, new Pair<>(listenerWrapper, prefixedEvents)); - } - - @Override - public synchronized void unregisterEventListener(final JavaAddonInterfaceV1.EventListener listener) { - final Pair pair = mListenerToWrapperMap.remove(listener); - if (pair == null) { - Log.e(LOGTAG, "Attempting to unregister listener which is not registered; ignoring."); - return; - } - mDispatcher.unregisterGeckoThreadListener(pair.first, pair.second); - } - - - protected synchronized void unregisterAllEventListeners() { - // Unregister everything, then forget everything. - for (Pair pair : mListenerToWrapperMap.values()) { - mDispatcher.unregisterGeckoThreadListener(pair.first, pair.second); - } - mListenerToWrapperMap.clear(); - } - - @Override - public void sendRequestToGecko(final String event, final JSONObject message, final JavaAddonInterfaceV1.RequestCallback callback) { - final String prefixedEvent = guid + ":" + event; - GeckoAppShell.sendRequestToGecko(new GeckoRequest(prefixedEvent, message) { - @Override - public void onResponse(NativeJSObject nativeJSObject) { - if (callback == null) { - // Nothing to do. - return; - } - try { - final JSONObject json = new JSONObject(nativeJSObject.toString()); - callback.onResponse(GeckoAppShell.getContext(), json); - } catch (JSONException e) { - // No way to report failure. - Log.e(LOGTAG, "Exception handling response to request [" + event + "]:", e); - } - } - }); - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/lwt/LightweightTheme.java b/mobile/android/base/java/org/mozilla/gecko/lwt/LightweightTheme.java deleted file mode 100644 index 0f27c1febc..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/lwt/LightweightTheme.java +++ /dev/null @@ -1,455 +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.lwt; - -import java.io.IOException; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.List; - -import org.json.JSONObject; -import org.mozilla.gecko.AppConstants.Versions; -import org.mozilla.gecko.EventDispatcher; -import org.mozilla.gecko.GeckoSharedPrefs; -import org.mozilla.gecko.gfx.BitmapUtils; -import org.mozilla.gecko.util.GeckoEventListener; -import org.mozilla.gecko.util.WindowUtils; -import org.mozilla.gecko.util.ThreadUtils; -import org.mozilla.gecko.util.ThreadUtils.AssertBehavior; - -import android.app.Application; -import android.content.SharedPreferences; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.graphics.Canvas; -import android.graphics.Color; -import android.graphics.Paint; -import android.graphics.Rect; -import android.graphics.Shader; -import android.graphics.drawable.BitmapDrawable; -import android.graphics.drawable.Drawable; -import android.text.TextUtils; -import android.util.Log; -import android.view.Gravity; -import android.view.View; -import android.view.ViewParent; - -public class LightweightTheme implements GeckoEventListener { - private static final String LOGTAG = "GeckoLightweightTheme"; - - private static final String PREFS_URL = "lightweightTheme.headerURL"; - private static final String PREFS_COLOR = "lightweightTheme.color"; - - private static final String ASSETS_PREFIX = "resource://android/assets/"; - - private final Application mApplication; - - private Bitmap mBitmap; - private int mColor; - private boolean mIsLight; - - public static interface OnChangeListener { - // The View should change its background/text color. - public void onLightweightThemeChanged(); - - // The View should reset to its default background/text color. - public void onLightweightThemeReset(); - } - - private final List mListeners; - - class LightweightThemeRunnable implements Runnable { - private String mHeaderURL; - private String mColor; - - private String mSavedURL; - private String mSavedColor; - - LightweightThemeRunnable() { - } - - LightweightThemeRunnable(final String headerURL, final String color) { - mHeaderURL = headerURL; - mColor = color; - } - - private void loadFromPrefs() { - SharedPreferences prefs = GeckoSharedPrefs.forProfile(mApplication); - mSavedURL = prefs.getString(PREFS_URL, null); - mSavedColor = prefs.getString(PREFS_COLOR, null); - } - - private void saveToPrefs() { - GeckoSharedPrefs.forProfile(mApplication) - .edit() - .putString(PREFS_URL, mHeaderURL) - .putString(PREFS_COLOR, mColor) - .apply(); - - // Let's keep the saved data in sync. - mSavedURL = mHeaderURL; - mSavedColor = mColor; - } - - @Override - public void run() { - // Load the data from preferences, if it exists. - loadFromPrefs(); - - if (TextUtils.isEmpty(mHeaderURL)) { - // mHeaderURL is null is this is the early startup path. Use - // the saved values, if we have any. - mHeaderURL = mSavedURL; - mColor = mSavedColor; - if (TextUtils.isEmpty(mHeaderURL)) { - // We don't have any saved values, so we probably don't have - // any lightweight theme set yet. - return; - } - } else if (TextUtils.equals(mHeaderURL, mSavedURL)) { - // If we are already using the given header, just return - // without doing any work. - return; - } else { - // mHeaderURL and mColor probably need to be saved if we get here. - saveToPrefs(); - } - - String croppedURL = mHeaderURL; - int mark = croppedURL.indexOf('?'); - if (mark != -1) { - croppedURL = croppedURL.substring(0, mark); - } - - if (croppedURL.startsWith(ASSETS_PREFIX)) { - onBitmapLoaded(loadFromAssets(croppedURL)); - } else { - onBitmapLoaded(BitmapUtils.decodeUrl(croppedURL)); - } - } - - private Bitmap loadFromAssets(String url) { - InputStream stream = null; - - try { - stream = mApplication.getAssets().open(url.substring(ASSETS_PREFIX.length())); - return BitmapFactory.decodeStream(stream); - } catch (IOException e) { - return null; - } finally { - if (stream != null) { - try { - stream.close(); - } catch (IOException e) { } - } - } - } - - private void onBitmapLoaded(final Bitmap bitmap) { - ThreadUtils.postToUiThread(new Runnable() { - @Override - public void run() { - setLightweightTheme(bitmap, mColor); - } - }); - } - } - - public LightweightTheme(Application application) { - mApplication = application; - mListeners = new ArrayList(); - - // unregister isn't needed as the lifetime is same as the application. - EventDispatcher.getInstance().registerGeckoThreadListener(this, - "LightweightTheme:Update", - "LightweightTheme:Disable"); - - ThreadUtils.postToBackgroundThread(new LightweightThemeRunnable()); - } - - public void addListener(final OnChangeListener listener) { - // Don't inform the listeners that attached late. - // Their onLayout() will take care of them before their onDraw(); - mListeners.add(listener); - } - - public void removeListener(OnChangeListener listener) { - mListeners.remove(listener); - } - - @Override - public void handleMessage(String event, JSONObject message) { - try { - if (event.equals("LightweightTheme:Update")) { - JSONObject lightweightTheme = message.getJSONObject("data"); - final String headerURL = lightweightTheme.getString("headerURL"); - final String color = lightweightTheme.optString("accentcolor"); - - ThreadUtils.postToBackgroundThread(new LightweightThemeRunnable(headerURL, color)); - } else if (event.equals("LightweightTheme:Disable")) { - // Clear the saved data when a theme is disabled. - // Called on the Gecko thread, but should be very lightweight. - clearPrefs(); - - ThreadUtils.postToUiThread(new Runnable() { - @Override - public void run() { - resetLightweightTheme(); - } - }); - } - } catch (Exception e) { - Log.e(LOGTAG, "Exception handling message \"" + event + "\":", e); - } - } - - /** - * Clear the data stored in preferences for fast path loading during startup - */ - private void clearPrefs() { - GeckoSharedPrefs.forProfile(mApplication) - .edit() - .remove(PREFS_URL) - .remove(PREFS_COLOR) - .apply(); - } - - /** - * Set a new lightweight theme with the given bitmap. - * Note: This should be called on the UI thread to restrict accessing the - * bitmap to a single thread. - * - * @param bitmap The bitmap used for the lightweight theme. - * @param color The background/accent color used for the lightweight theme. - */ - private void setLightweightTheme(Bitmap bitmap, String color) { - if (bitmap == null || bitmap.getWidth() == 0 || bitmap.getHeight() == 0) { - mBitmap = null; - return; - } - - // Get the max display dimension so we can crop or expand the theme. - final int maxWidth = WindowUtils.getLargestDimension(mApplication); - - // The lightweight theme image's width and height. - final int bitmapWidth = bitmap.getWidth(); - final int bitmapHeight = bitmap.getHeight(); - - try { - mColor = Color.parseColor(color); - } catch (Exception e) { - // Malformed or missing color. - // Default to TRANSPARENT. - mColor = Color.TRANSPARENT; - } - - // Calculate the luminance to determine if it's a light or a dark theme. - double luminance = (0.2125 * ((mColor & 0x00FF0000) >> 16)) + - (0.7154 * ((mColor & 0x0000FF00) >> 8)) + - (0.0721 * (mColor & 0x000000FF)); - mIsLight = luminance > 110; - - // The bitmap image might be smaller than the device's width. - // If it's smaller, fill the extra space on the left with the dominant color. - if (bitmapWidth >= maxWidth) { - mBitmap = Bitmap.createBitmap(bitmap, bitmapWidth - maxWidth, 0, maxWidth, bitmapHeight); - } else { - Paint paint = new Paint(); - paint.setAntiAlias(true); - - // Create a bigger image that can fill the device width. - // By creating a canvas for the bitmap, anything drawn on the canvas - // will be drawn on the bitmap. - mBitmap = Bitmap.createBitmap(maxWidth, bitmapHeight, Bitmap.Config.ARGB_8888); - Canvas canvas = new Canvas(mBitmap); - - // Fill the canvas with dominant color. - canvas.drawColor(mColor); - - // The image should be top-right aligned. - Rect rect = new Rect(); - Gravity.apply(Gravity.TOP | Gravity.RIGHT, - bitmapWidth, - bitmapHeight, - new Rect(0, 0, maxWidth, bitmapHeight), - rect); - - // Draw the bitmap. - canvas.drawBitmap(bitmap, null, rect, paint); - } - - for (OnChangeListener listener : mListeners) { - listener.onLightweightThemeChanged(); - } - } - - /** - * Reset the lightweight theme. - * Note: This should be called on the UI thread to restrict accessing the - * bitmap to a single thread. - */ - private void resetLightweightTheme() { - ThreadUtils.assertOnUiThread(AssertBehavior.NONE); - if (mBitmap == null) { - return; - } - - // Reset the bitmap. - mBitmap = null; - - for (OnChangeListener listener : mListeners) { - listener.onLightweightThemeReset(); - } - } - - /** - * A lightweight theme is enabled only if there is an active bitmap. - * - * @return True if the theme is enabled. - */ - public boolean isEnabled() { - return (mBitmap != null); - } - - /** - * Based on the luminance of the domanint color, a theme is classified as light or dark. - * - * @return True if the theme is light. - */ - public boolean isLightTheme() { - return mIsLight; - } - - /** - * Crop the image based on the position of the view on the window. - * Either the View or one of its ancestors might have scrolled or translated. - * This value should be taken into account while mapping the View to the Bitmap. - * - * @param view The view requesting a cropped bitmap. - */ - private Bitmap getCroppedBitmap(View view) { - if (mBitmap == null || view == null) { - return null; - } - - // Get the global position of the view on the entire screen. - Rect rect = new Rect(); - view.getGlobalVisibleRect(rect); - - // Get the activity's window position. This does an IPC call, may be expensive. - Rect window = new Rect(); - view.getWindowVisibleDisplayFrame(window); - - // Calculate the coordinates for the cropped bitmap. - int screenWidth = view.getContext().getResources().getDisplayMetrics().widthPixels; - int left = mBitmap.getWidth() - screenWidth + rect.left; - int right = mBitmap.getWidth() - screenWidth + rect.right; - int top = rect.top - window.top; - int bottom = rect.bottom - window.top; - - int offsetX = 0; - int offsetY = 0; - - // Find if this view or any of its ancestors has been translated or scrolled. - ViewParent parent; - View curView = view; - do { - offsetX += (int) curView.getTranslationX() - curView.getScrollX(); - offsetY += (int) curView.getTranslationY() - curView.getScrollY(); - - parent = curView.getParent(); - - if (parent instanceof View) { - curView = (View) parent; - } - - } while (parent instanceof View); - - // Adjust the coordinates for the offset. - left -= offsetX; - right -= offsetX; - top -= offsetY; - bottom -= offsetY; - - // The either the required height may be less than the available image height or more than it. - // If the height required is more, crop only the available portion on the image. - int width = right - left; - int height = (bottom > mBitmap.getHeight() ? mBitmap.getHeight() - top : bottom - top); - - // There is a chance that the view is not visible or doesn't fall within the phone's size. - // In this case, 'rect' will have all values as '0'. Hence 'top' and 'bottom' may be negative, - // and createBitmap() will fail. - // The view will get a background in its next layout pass. - try { - return Bitmap.createBitmap(mBitmap, left, top, width, height); - } catch (Exception e) { - return null; - } - } - - /** - * Converts the cropped bitmap to a BitmapDrawable and returns the same. - * - * @param view The view for which a background drawable is required. - * @return Either the cropped bitmap as a Drawable or null. - */ - public Drawable getDrawable(View view) { - Bitmap bitmap = getCroppedBitmap(view); - if (bitmap == null) { - return null; - } - - BitmapDrawable drawable = new BitmapDrawable(view.getContext().getResources(), bitmap); - drawable.setGravity(Gravity.TOP | Gravity.RIGHT); - drawable.setTileModeXY(Shader.TileMode.CLAMP, Shader.TileMode.CLAMP); - return drawable; - } - - /** - * Converts the cropped bitmap to a LightweightThemeDrawable, placing it over the dominant color. - * - * @param view The view for which a background drawable is required. - * @return Either the cropped bitmap as a Drawable or null. - */ - public LightweightThemeDrawable getColorDrawable(View view) { - return getColorDrawable(view, mColor, false); - } - - /** - * Converts the cropped bitmap to a LightweightThemeDrawable, placing it over the required color. - * - * @param view The view for which a background drawable is required. - * @param color The color over which the drawable should be drawn. - * @return Either the cropped bitmap as a Drawable or null. - */ - public LightweightThemeDrawable getColorDrawable(View view, int color) { - return getColorDrawable(view, color, false); - } - - /** - * Converts the cropped bitmap to a LightweightThemeDrawable, placing it over the required color. - * - * @param view The view for which a background drawable is required. - * @param color The color over which the drawable should be drawn. - * @param needsDominantColor A layer of dominant color is needed or not. - * @return Either the cropped bitmap as a Drawable or null. - */ - public LightweightThemeDrawable getColorDrawable(View view, int color, boolean needsDominantColor) { - Bitmap bitmap = getCroppedBitmap(view); - if (bitmap == null) { - return null; - } - - LightweightThemeDrawable drawable = new LightweightThemeDrawable(view.getContext().getResources(), bitmap); - if (needsDominantColor) { - drawable.setColorWithFilter(color, (mColor & 0x22FFFFFF)); - } else { - drawable.setColor(color); - } - - return drawable; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/lwt/LightweightThemeDrawable.java b/mobile/android/base/java/org/mozilla/gecko/lwt/LightweightThemeDrawable.java deleted file mode 100644 index c0ae6eaedf..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/lwt/LightweightThemeDrawable.java +++ /dev/null @@ -1,133 +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.lwt; - -import android.content.res.Resources; -import android.graphics.Bitmap; -import android.graphics.BitmapShader; -import android.graphics.Canvas; -import android.graphics.ColorFilter; -import android.graphics.ComposeShader; -import android.graphics.LinearGradient; -import android.graphics.Paint; -import android.graphics.PixelFormat; -import android.graphics.PorterDuff; -import android.graphics.PorterDuffColorFilter; -import android.graphics.Rect; -import android.graphics.Shader; -import android.graphics.drawable.Drawable; - -/** - * A special drawable used with lightweight themes. This draws a color - * (with an optional color-filter) and a bitmap (with a linear gradient - * to specify the alpha) in order. - */ -public class LightweightThemeDrawable extends Drawable { - private final Paint mPaint; - private Paint mColorPaint; - - private final Bitmap mBitmap; - private final Resources mResources; - - private int mStartColor; - private int mEndColor; - - public LightweightThemeDrawable(Resources resources, Bitmap bitmap) { - mBitmap = bitmap; - mResources = resources; - - mPaint = new Paint(); - mPaint.setAntiAlias(true); - mPaint.setStrokeWidth(0.0f); - } - - @Override - protected void onBoundsChange(Rect bounds) { - super.onBoundsChange(bounds); - initializeBitmapShader(); - } - - @Override - public void draw(Canvas canvas) { - // Draw the colors, if available. - if (mColorPaint != null) { - canvas.drawPaint(mColorPaint); - } - - // Draw the bitmap. - canvas.drawPaint(mPaint); - } - - @Override - public int getOpacity() { - return PixelFormat.TRANSLUCENT; - } - - @Override - public void setAlpha(int alpha) { - // A StateListDrawable will reset the alpha value with 255. - // We cannot use to be the bitmap alpha. - mPaint.setAlpha(alpha); - } - - @Override - public void setColorFilter(ColorFilter filter) { - mPaint.setColorFilter(filter); - } - - /** - * Creates a paint that paint a particular color. - * - * Note that the given color should include an alpha value. - * - * @param color The color to be painted. - */ - public void setColor(int color) { - mColorPaint = new Paint(mPaint); - mColorPaint.setColor(color); - } - - /** - * Creates a paint that paint a particular color, and a filter for the color. - * - * Note that the given color should include an alpha value. - * - * @param color The color to be painted. - * @param filter The filter color to be applied using SRC_OVER mode. - */ - public void setColorWithFilter(int color, int filter) { - mColorPaint = new Paint(mPaint); - mColorPaint.setColor(color); - mColorPaint.setColorFilter(new PorterDuffColorFilter(filter, PorterDuff.Mode.SRC_OVER)); - } - - /** - * Set the alpha for the linear gradient used with the bitmap's shader. - * - * @param startAlpha The starting alpha (0..255) value to be applied to the LinearGradient. - * @param startAlpha The ending alpha (0..255) value to be applied to the LinearGradient. - */ - public void setAlpha(int startAlpha, int endAlpha) { - mStartColor = startAlpha << 24; - mEndColor = endAlpha << 24; - initializeBitmapShader(); - } - - private void initializeBitmapShader() { - // A bitmap-shader to draw the bitmap. - // Clamp mode will repeat the last row of pixels. - // Hence its better to have an endAlpha of 0 for the linear-gradient. - BitmapShader bitmapShader = new BitmapShader(mBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP); - - // A linear-gradient to specify the opacity of the bitmap. - LinearGradient gradient = new LinearGradient(0, 0, 0, mBitmap.getHeight(), mStartColor, mEndColor, Shader.TileMode.CLAMP); - - // Make a combined shader -- a performance win. - // The linear-gradient is the 'SRC' and the bitmap-shader is the 'DST'. - // Drawing the DST in the SRC will provide the opacity. - mPaint.setShader(new ComposeShader(bitmapShader, gradient, PorterDuff.Mode.DST_IN)); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/mdns/MulticastDNSManager.java b/mobile/android/base/java/org/mozilla/gecko/mdns/MulticastDNSManager.java deleted file mode 100644 index 6f23790b9e..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/mdns/MulticastDNSManager.java +++ /dev/null @@ -1,535 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.mdns; - -import org.mozilla.gecko.AppConstants.Versions; -import org.mozilla.gecko.EventDispatcher; -import org.mozilla.gecko.GeckoAppShell; -import org.mozilla.gecko.util.EventCallback; -import org.mozilla.gecko.util.GeckoRequest; -import org.mozilla.gecko.util.NativeEventListener; -import org.mozilla.gecko.util.NativeJSObject; -import org.mozilla.gecko.util.ThreadUtils; - -import org.json.JSONException; -import org.json.JSONObject; - -import android.annotation.TargetApi; -import android.content.Context; -import android.net.nsd.NsdManager; -import android.net.nsd.NsdServiceInfo; -import android.support.annotation.UiThread; -import android.util.Log; - -import java.net.InetAddress; -import java.util.HashMap; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - -/** - * This class is the bridge between XPCOM mDNS module and NsdManager. - * - * @See nsIDNSServiceDiscovery.idl - */ -public abstract class MulticastDNSManager { - protected static final String LOGTAG = "GeckoMDNSManager"; - private static MulticastDNSManager instance = null; - - public static MulticastDNSManager getInstance(final Context context) { - if (instance == null) { - instance = new DummyMulticastDNSManager(); - } - return instance; - } - - public abstract void init(); - public abstract void tearDown(); -} - -/** - * Mix-in class for MulticastDNSManagers to call EventDispatcher. - */ -class MulticastDNSEventManager { - private NativeEventListener mListener = null; - private boolean mEventsRegistered = false; - - MulticastDNSEventManager(NativeEventListener listener) { - mListener = listener; - } - - @UiThread - public void init() { - ThreadUtils.assertOnUiThread(); - - if (mEventsRegistered || mListener == null) { - return; - } - - registerEvents(); - mEventsRegistered = true; - } - - @UiThread - public void tearDown() { - ThreadUtils.assertOnUiThread(); - - if (!mEventsRegistered || mListener == null) { - return; - } - - unregisterEvents(); - mEventsRegistered = false; - } - - private void registerEvents() { - EventDispatcher.getInstance().registerGeckoThreadListener(mListener, - "NsdManager:DiscoverServices", - "NsdManager:StopServiceDiscovery", - "NsdManager:RegisterService", - "NsdManager:UnregisterService", - "NsdManager:ResolveService"); - } - - private void unregisterEvents() { - EventDispatcher.getInstance().unregisterGeckoThreadListener(mListener, - "NsdManager:DiscoverServices", - "NsdManager:StopServiceDiscovery", - "NsdManager:RegisterService", - "NsdManager:UnregisterService", - "NsdManager:ResolveService"); - } -} - -class NsdMulticastDNSManager extends MulticastDNSManager implements NativeEventListener { - private final NsdManager nsdManager; - private final MulticastDNSEventManager mEventManager; - private Map mDiscoveryListeners = null; - private Map mRegistrationListeners = null; - - @TargetApi(16) - public NsdMulticastDNSManager(final Context context) { - nsdManager = (NsdManager) context.getSystemService(Context.NSD_SERVICE); - mEventManager = new MulticastDNSEventManager(this); - mDiscoveryListeners = new ConcurrentHashMap(); - mRegistrationListeners = new ConcurrentHashMap(); - } - - @Override - public void init() { - mEventManager.init(); - } - - @Override - public void tearDown() { - mDiscoveryListeners.clear(); - mRegistrationListeners.clear(); - - mEventManager.tearDown(); - } - - @Override - public void handleMessage(final String event, final NativeJSObject message, final EventCallback callback) { - Log.v(LOGTAG, "handleMessage: " + event); - - switch (event) { - case "NsdManager:DiscoverServices": { - DiscoveryListener listener = new DiscoveryListener(nsdManager); - listener.discoverServices(message.getString("serviceType"), callback); - mDiscoveryListeners.put(message.getString("uniqueId"), listener); - break; - } - case "NsdManager:StopServiceDiscovery": { - String uuid = message.getString("uniqueId"); - DiscoveryListener listener = mDiscoveryListeners.remove(uuid); - if (listener == null) { - Log.e(LOGTAG, "DiscoveryListener " + uuid + " was not found."); - return; - } - listener.stopServiceDiscovery(callback); - break; - } - case "NsdManager:RegisterService": { - RegistrationListener listener = new RegistrationListener(nsdManager); - listener.registerService(message.getInt("port"), - message.optString("serviceName", android.os.Build.MODEL), - message.getString("serviceType"), - parseAttributes(message.optObjectArray("attributes", null)), - callback); - mRegistrationListeners.put(message.getString("uniqueId"), listener); - break; - } - case "NsdManager:UnregisterService": { - String uuid = message.getString("uniqueId"); - RegistrationListener listener = mRegistrationListeners.remove(uuid); - if (listener == null) { - Log.e(LOGTAG, "RegistrationListener " + uuid + " was not found."); - return; - } - listener.unregisterService(callback); - break; - } - case "NsdManager:ResolveService": { - (new ResolveListener(nsdManager)).resolveService(message.getString("serviceName"), - message.getString("serviceType"), - callback); - break; - } - } - } - - private Map parseAttributes(final NativeJSObject[] jsobjs) { - if (jsobjs == null || jsobjs.length == 0 || !Versions.feature21Plus) { - return null; - } - - Map attributes = new HashMap(jsobjs.length); - for (NativeJSObject obj : jsobjs) { - attributes.put(obj.getString("name"), obj.getString("value")); - } - - return attributes; - } - - @TargetApi(16) - public static JSONObject toJSON(final NsdServiceInfo serviceInfo) throws JSONException { - JSONObject obj = new JSONObject(); - - InetAddress host = serviceInfo.getHost(); - if (host != null) { - obj.put("host", host.getCanonicalHostName()); - obj.put("address", host.getHostAddress()); - } - - int port = serviceInfo.getPort(); - if (port != 0) { - obj.put("port", port); - } - - String serviceName = serviceInfo.getServiceName(); - if (serviceName != null) { - obj.put("serviceName", serviceName); - } - - String serviceType = serviceInfo.getServiceType(); - if (serviceType != null) { - obj.put("serviceType", serviceType); - } - - return obj; - } -} - -class DummyMulticastDNSManager extends MulticastDNSManager implements NativeEventListener { - static final int FAILURE_UNSUPPORTED = -65544; - private final MulticastDNSEventManager mEventManager; - - public DummyMulticastDNSManager() { - mEventManager = new MulticastDNSEventManager(this); - } - - @Override - public void init() { - mEventManager.init(); - } - - @Override - public void tearDown() { - mEventManager.tearDown(); - } - - @Override - public void handleMessage(final String event, final NativeJSObject message, final EventCallback callback) { - Log.v(LOGTAG, "handleMessage: " + event); - callback.sendError(FAILURE_UNSUPPORTED); - } -} - -@TargetApi(16) -class DiscoveryListener implements NsdManager.DiscoveryListener { - private static final String LOGTAG = "GeckoMDNSManager"; - private final NsdManager nsdManager; - - // Callbacks are called from different thread, and every callback can be called only once. - private EventCallback mStartCallback = null; - private EventCallback mStopCallback = null; - - DiscoveryListener(final NsdManager nsdManager) { - this.nsdManager = nsdManager; - } - - public void discoverServices(final String serviceType, final EventCallback callback) { - synchronized (this) { - mStartCallback = callback; - } - nsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD, this); - } - - public void stopServiceDiscovery(final EventCallback callback) { - synchronized (this) { - mStopCallback = callback; - } - nsdManager.stopServiceDiscovery(this); - } - - @Override - public synchronized void onDiscoveryStarted(final String serviceType) { - Log.d(LOGTAG, "onDiscoveryStarted: " + serviceType); - - EventCallback callback; - synchronized (this) { - callback = mStartCallback; - } - - if (callback == null) { - return; - } - - callback.sendSuccess(serviceType); - } - - @Override - public synchronized void onStartDiscoveryFailed(final String serviceType, final int errorCode) { - Log.e(LOGTAG, "onStartDiscoveryFailed: " + serviceType + "(" + errorCode + ")"); - - EventCallback callback; - synchronized (this) { - callback = mStartCallback; - } - - callback.sendError(errorCode); - } - - @Override - public synchronized void onDiscoveryStopped(final String serviceType) { - Log.d(LOGTAG, "onDiscoveryStopped: " + serviceType); - - EventCallback callback; - synchronized (this) { - callback = mStopCallback; - } - - if (callback == null) { - return; - } - - callback.sendSuccess(serviceType); - } - - @Override - public synchronized void onStopDiscoveryFailed(final String serviceType, final int errorCode) { - Log.e(LOGTAG, "onStopDiscoveryFailed: " + serviceType + "(" + errorCode + ")"); - - EventCallback callback; - synchronized (this) { - callback = mStopCallback; - } - - if (callback == null) { - return; - } - - callback.sendError(errorCode); - } - - @Override - public void onServiceFound(final NsdServiceInfo serviceInfo) { - Log.d(LOGTAG, "onServiceFound: " + serviceInfo.getServiceName()); - JSONObject json; - try { - json = NsdMulticastDNSManager.toJSON(serviceInfo); - } catch (JSONException e) { - throw new RuntimeException(e); - } - GeckoAppShell.sendRequestToGecko(new GeckoRequest("NsdManager:ServiceFound", json) { - @Override - public void onResponse(NativeJSObject nativeJSObject) { - // don't care return value. - } - }); - } - - @Override - public void onServiceLost(final NsdServiceInfo serviceInfo) { - Log.d(LOGTAG, "onServiceLost: " + serviceInfo.getServiceName()); - JSONObject json; - try { - json = NsdMulticastDNSManager.toJSON(serviceInfo); - } catch (JSONException e) { - throw new RuntimeException(e); - } - GeckoAppShell.sendRequestToGecko(new GeckoRequest("NsdManager:ServiceLost", json) { - @Override - public void onResponse(NativeJSObject nativeJSObject) { - // don't care return value. - } - }); - } -} - -@TargetApi(16) -class RegistrationListener implements NsdManager.RegistrationListener { - private static final String LOGTAG = "GeckoMDNSManager"; - private final NsdManager nsdManager; - - // Callbacks are called from different thread, and every callback can be called only once. - private EventCallback mStartCallback = null; - private EventCallback mStopCallback = null; - - RegistrationListener(final NsdManager nsdManager) { - this.nsdManager = nsdManager; - } - - public void registerService(final int port, final String serviceName, final String serviceType, final Map attributes, final EventCallback callback) { - Log.d(LOGTAG, "registerService: " + serviceName + "." + serviceType + ":" + port); - - NsdServiceInfo serviceInfo = new NsdServiceInfo(); - serviceInfo.setPort(port); - serviceInfo.setServiceName(serviceName); - serviceInfo.setServiceType(serviceType); - setAttributes(serviceInfo, attributes); - - synchronized (this) { - mStartCallback = callback; - } - nsdManager.registerService(serviceInfo, NsdManager.PROTOCOL_DNS_SD, this); - } - - @TargetApi(21) - private void setAttributes(final NsdServiceInfo serviceInfo, final Map attributes) { - if (attributes == null || !Versions.feature21Plus) { - return; - } - - for (Map.Entry entry : attributes.entrySet()) { - serviceInfo.setAttribute(entry.getKey(), entry.getValue()); - } - } - - public void unregisterService(final EventCallback callback) { - Log.d(LOGTAG, "unregisterService"); - synchronized (this) { - mStopCallback = callback; - } - - nsdManager.unregisterService(this); - } - - @Override - public synchronized void onServiceRegistered(final NsdServiceInfo serviceInfo) { - Log.d(LOGTAG, "onServiceRegistered: " + serviceInfo.getServiceName()); - - EventCallback callback; - synchronized (this) { - callback = mStartCallback; - } - - if (callback == null) { - return; - } - - try { - callback.sendSuccess(NsdMulticastDNSManager.toJSON(serviceInfo)); - } catch (JSONException e) { - throw new RuntimeException(e); - } - } - - @Override - public synchronized void onRegistrationFailed(final NsdServiceInfo serviceInfo, final int errorCode) { - Log.e(LOGTAG, "onRegistrationFailed: " + serviceInfo.getServiceName() + "(" + errorCode + ")"); - - EventCallback callback; - synchronized (this) { - callback = mStartCallback; - } - - callback.sendError(errorCode); - } - - @Override - public synchronized void onServiceUnregistered(final NsdServiceInfo serviceInfo) { - Log.d(LOGTAG, "onServiceUnregistered: " + serviceInfo.getServiceName()); - - EventCallback callback; - synchronized (this) { - callback = mStopCallback; - } - - if (callback == null) { - return; - } - - try { - callback.sendSuccess(NsdMulticastDNSManager.toJSON(serviceInfo)); - } catch (JSONException e) { - throw new RuntimeException(e); - } - } - - @Override - public synchronized void onUnregistrationFailed(final NsdServiceInfo serviceInfo, final int errorCode) { - Log.e(LOGTAG, "onUnregistrationFailed: " + serviceInfo.getServiceName() + "(" + errorCode + ")"); - - EventCallback callback; - synchronized (this) { - callback = mStopCallback; - } - - if (callback == null) { - return; - } - - callback.sendError(errorCode); - } -} - -@TargetApi(16) -class ResolveListener implements NsdManager.ResolveListener { - private static final String LOGTAG = "GeckoMDNSManager"; - private final NsdManager nsdManager; - - // Callback is called from different thread, and the callback can be called only once. - private EventCallback mCallback = null; - - public ResolveListener(final NsdManager nsdManager) { - this.nsdManager = nsdManager; - } - - public void resolveService(final String serviceName, final String serviceType, final EventCallback callback) { - NsdServiceInfo serviceInfo = new NsdServiceInfo(); - serviceInfo.setServiceName(serviceName); - serviceInfo.setServiceType(serviceType); - - mCallback = callback; - nsdManager.resolveService(serviceInfo, this); - } - - - @Override - public synchronized void onResolveFailed(final NsdServiceInfo serviceInfo, final int errorCode) { - Log.e(LOGTAG, "onResolveFailed: " + serviceInfo.getServiceName() + "(" + errorCode + ")"); - - if (mCallback == null) { - return; - } - mCallback.sendError(errorCode); - } - - @Override - public synchronized void onServiceResolved(final NsdServiceInfo serviceInfo) { - Log.d(LOGTAG, "onServiceResolved: " + serviceInfo.getServiceName()); - - if (mCallback == null) { - return; - } - - try { - mCallback.sendSuccess(NsdMulticastDNSManager.toJSON(serviceInfo)); - } catch (JSONException e) { - throw new RuntimeException(e); - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/media/AsyncCodec.java b/mobile/android/base/java/org/mozilla/gecko/media/AsyncCodec.java deleted file mode 100644 index c9c620606e..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/media/AsyncCodec.java +++ /dev/null @@ -1,34 +0,0 @@ -/* 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.media; - -import android.media.MediaCodec.BufferInfo; -import android.media.MediaFormat; -import android.os.Handler; -import android.view.Surface; - -import java.nio.ByteBuffer; - -// A wrapper interface that mimics the new {@link android.media.MediaCodec} -// asynchronous mode API in Lollipop. -public interface AsyncCodec { - public interface Callbacks { - void onInputBufferAvailable(AsyncCodec codec, int index); - void onOutputBufferAvailable(AsyncCodec codec, int index, BufferInfo info); - void onError(AsyncCodec codec, int error); - void onOutputFormatChanged(AsyncCodec codec, MediaFormat format); - } - - public abstract void setCallbacks(Callbacks callbacks, Handler handler); - public abstract void configure(MediaFormat format, Surface surface, int flags); - public abstract void start(); - public abstract void stop(); - public abstract void flush(); - public abstract void release(); - public abstract ByteBuffer getInputBuffer(int index); - public abstract ByteBuffer getOutputBuffer(int index); - public abstract void queueInputBuffer(int index, int offset, int size, long presentationTimeUs, int flags); - public abstract void releaseOutputBuffer(int index, boolean render); -} diff --git a/mobile/android/base/java/org/mozilla/gecko/media/AsyncCodecFactory.java b/mobile/android/base/java/org/mozilla/gecko/media/AsyncCodecFactory.java deleted file mode 100644 index fd670e21b5..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/media/AsyncCodecFactory.java +++ /dev/null @@ -1,14 +0,0 @@ -/* 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.media; - -import java.io.IOException; - -public final class AsyncCodecFactory { - public static AsyncCodec create(String name) throws IOException { - // TODO: create (to be implemented) LollipopAsyncCodec when running on Lollipop or later devices. - return new JellyBeanAsyncCodec(name); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/media/AudioFocusAgent.java b/mobile/android/base/java/org/mozilla/gecko/media/AudioFocusAgent.java deleted file mode 100644 index 93a63bcb5d..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/media/AudioFocusAgent.java +++ /dev/null @@ -1,135 +0,0 @@ -package org.mozilla.gecko.media; - -import org.mozilla.gecko.annotation.RobocopTarget; -import org.mozilla.gecko.annotation.WrapForJNI; -import org.mozilla.gecko.EventDispatcher; -import org.mozilla.gecko.GeckoAppShell; - -import android.content.Context; -import android.content.Intent; -import android.media.AudioManager; -import android.media.AudioManager.OnAudioFocusChangeListener; -import android.util.Log; - -public class AudioFocusAgent { - private static final String LOGTAG = "AudioFocusAgent"; - - private static Context mContext; - private AudioManager mAudioManager; - private OnAudioFocusChangeListener mAfChangeListener; - - public static final String OWN_FOCUS = "own_focus"; - public static final String LOST_FOCUS = "lost_focus"; - public static final String LOST_FOCUS_TRANSIENT = "lost_focus_transient"; - - private String mAudioFocusState = LOST_FOCUS; - - @WrapForJNI(calledFrom = "gecko") - public static void notifyStartedPlaying() { - if (!isAttachedToContext()) { - return; - } - Log.d(LOGTAG, "NotifyStartedPlaying"); - AudioFocusAgent.getInstance().requestAudioFocusIfNeeded(); - } - - @WrapForJNI(calledFrom = "gecko") - public static void notifyStoppedPlaying() { - if (!isAttachedToContext()) { - return; - } - Log.d(LOGTAG, "NotifyStoppedPlaying"); - AudioFocusAgent.getInstance().abandonAudioFocusIfNeeded(); - } - - public synchronized void attachToContext(Context context) { - if (isAttachedToContext()) { - return; - } - - mContext = context; - mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE); - - mAfChangeListener = new OnAudioFocusChangeListener() { - public void onAudioFocusChange(int focusChange) { - switch (focusChange) { - case AudioManager.AUDIOFOCUS_LOSS: - Log.d(LOGTAG, "onAudioFocusChange, AUDIOFOCUS_LOSS"); - notifyObservers("AudioFocusChanged", "lostAudioFocus"); - notifyMediaControlService(MediaControlService.ACTION_PAUSE_BY_AUDIO_FOCUS); - mAudioFocusState = LOST_FOCUS; - break; - case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT: - Log.d(LOGTAG, "onAudioFocusChange, AUDIOFOCUS_LOSS_TRANSIENT"); - notifyObservers("AudioFocusChanged", "lostAudioFocusTransiently"); - notifyMediaControlService(MediaControlService.ACTION_PAUSE_BY_AUDIO_FOCUS); - mAudioFocusState = LOST_FOCUS_TRANSIENT; - break; - case AudioManager.AUDIOFOCUS_GAIN: - if (!mAudioFocusState.equals(LOST_FOCUS_TRANSIENT)) { - return; - } - Log.d(LOGTAG, "onAudioFocusChange, AUDIOFOCUS_GAIN"); - notifyObservers("AudioFocusChanged", "gainAudioFocus"); - notifyMediaControlService(MediaControlService.ACTION_RESUME_BY_AUDIO_FOCUS); - mAudioFocusState = OWN_FOCUS; - break; - default: - } - } - }; - notifyMediaControlService(MediaControlService.ACTION_INIT); - } - - @RobocopTarget - public static AudioFocusAgent getInstance() { - return AudioFocusAgent.SingletonHolder.INSTANCE; - } - - private static class SingletonHolder { - private static final AudioFocusAgent INSTANCE = new AudioFocusAgent(); - } - - private static boolean isAttachedToContext() { - return (mContext != null); - } - - private void notifyObservers(String topic, String data) { - GeckoAppShell.notifyObservers(topic, data); - } - - private AudioFocusAgent() {} - - private void requestAudioFocusIfNeeded() { - if (mAudioFocusState.equals(OWN_FOCUS)) { - return; - } - - int result = mAudioManager.requestAudioFocus(mAfChangeListener, - AudioManager.STREAM_MUSIC, - AudioManager.AUDIOFOCUS_GAIN); - - String focusMsg = (result == AudioManager.AUDIOFOCUS_GAIN) ? - "AudioFocus request granted" : "AudioFoucs request failed"; - Log.d(LOGTAG, focusMsg); - if (result == AudioManager.AUDIOFOCUS_GAIN) { - mAudioFocusState = OWN_FOCUS; - } - } - - private void abandonAudioFocusIfNeeded() { - if (!mAudioFocusState.equals(OWN_FOCUS)) { - return; - } - - Log.d(LOGTAG, "Abandon AudioFocus"); - mAudioManager.abandonAudioFocus(mAfChangeListener); - mAudioFocusState = LOST_FOCUS; - } - - private void notifyMediaControlService(String action) { - Intent intent = new Intent(mContext, MediaControlService.class); - intent.setAction(action); - mContext.startService(intent); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/media/Codec.java b/mobile/android/base/java/org/mozilla/gecko/media/Codec.java deleted file mode 100644 index b0a26dfb31..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/media/Codec.java +++ /dev/null @@ -1,366 +0,0 @@ -/* 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.media; - -import android.media.MediaCodec; -import android.media.MediaCodecInfo; -import android.media.MediaCodecList; -import android.media.MediaFormat; -import android.os.IBinder; -import android.os.RemoteException; -import android.os.TransactionTooLargeException; -import android.util.Log; -import android.view.Surface; - -import java.io.IOException; -import java.nio.ByteBuffer; -import java.util.LinkedList; -import java.util.NoSuchElementException; -import java.util.Queue; -import java.util.concurrent.ConcurrentLinkedQueue; - -/* package */ final class Codec extends ICodec.Stub implements IBinder.DeathRecipient { - private static final String LOGTAG = "GeckoRemoteCodec"; - private static final boolean DEBUG = false; - - public enum Error { - DECODE, FATAL - }; - - private final class Callbacks implements AsyncCodec.Callbacks { - private ICodecCallbacks mRemote; - private boolean mHasInputCapacitySet; - private boolean mHasOutputCapacitySet; - - public Callbacks(ICodecCallbacks remote) { - mRemote = remote; - } - - @Override - public void onInputBufferAvailable(AsyncCodec codec, int index) { - if (mFlushing) { - // Flush invalidates all buffers. - return; - } - if (!mHasInputCapacitySet) { - int capacity = codec.getInputBuffer(index).capacity(); - if (capacity > 0) { - mSamplePool.setInputBufferSize(capacity); - mHasInputCapacitySet = true; - } - } - if (!mInputProcessor.onBuffer(index)) { - reportError(Error.FATAL, new Exception("FAIL: input buffer queue is full")); - } - } - - @Override - public void onOutputBufferAvailable(AsyncCodec codec, int index, MediaCodec.BufferInfo info) { - if (mFlushing) { - // Flush invalidates all buffers. - return; - } - ByteBuffer output = codec.getOutputBuffer(index); - if (!mHasOutputCapacitySet) { - int capacity = output.capacity(); - if (capacity > 0) { - mSamplePool.setOutputBufferSize(capacity); - mHasOutputCapacitySet = true; - } - } - Sample copy = mSamplePool.obtainOutput(info); - try { - if (info.size > 0) { - copy.buffer.readFromByteBuffer(output, info.offset, info.size); - } - mSentOutputs.add(copy); - mRemote.onOutput(copy); - } catch (IOException e) { - Log.e(LOGTAG, "Fail to read output buffer:" + e.getMessage()); - outputDummy(info); - } catch (TransactionTooLargeException ttle) { - Log.e(LOGTAG, "Output is too large:" + ttle.getMessage()); - outputDummy(info); - } catch (RemoteException e) { - // Dead recipient. - e.printStackTrace(); - } - - mCodec.releaseOutputBuffer(index, true); - boolean eos = (info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0; - if (DEBUG && eos) { - Log.d(LOGTAG, "output EOS"); - } - } - - private void outputDummy(MediaCodec.BufferInfo info) { - try { - if (DEBUG) Log.d(LOGTAG, "return dummy sample"); - mRemote.onOutput(Sample.create(null, info, null)); - } catch (RemoteException e) { - // Dead recipient. - e.printStackTrace(); - } - } - - @Override - public void onError(AsyncCodec codec, int error) { - reportError(Error.FATAL, new Exception("codec error:" + error)); - } - - @Override - public void onOutputFormatChanged(AsyncCodec codec, MediaFormat format) { - try { - mRemote.onOutputFormatChanged(new FormatParam(format)); - } catch (RemoteException re) { - // Dead recipient. - re.printStackTrace(); - } - } - } - - private final class InputProcessor { - private Queue mInputSamples = new LinkedList<>(); - private Queue mAvailableInputBuffers = new LinkedList<>(); - private Queue mDequeuedSamples = new LinkedList<>(); - - private synchronized Sample onAllocate(int size) { - Sample sample = mSamplePool.obtainInput(size); - mDequeuedSamples.add(sample); - return sample; - } - - private synchronized boolean onSample(Sample sample) { - if (sample == null) { - return false; - } - - if (!sample.isEOS()) { - Sample temp = sample; - sample = mDequeuedSamples.remove(); - sample.info = temp.info; - sample.cryptoInfo = temp.cryptoInfo; - temp.dispose(); - } - - if (!mInputSamples.offer(sample)) { - return false; - } - feedSampleToBuffer(); - return true; - } - - private synchronized boolean onBuffer(int index) { - if (!mAvailableInputBuffers.offer(index)) { - return false; - } - feedSampleToBuffer(); - return true; - } - - private void feedSampleToBuffer() { - while (!mAvailableInputBuffers.isEmpty() && !mInputSamples.isEmpty()) { - int index = mAvailableInputBuffers.poll(); - int len = 0; - Sample sample = mInputSamples.poll(); - long pts = sample.info.presentationTimeUs; - int flags = sample.info.flags; - if (!sample.isEOS() && sample.buffer != null) { - len = sample.info.size; - ByteBuffer buf = mCodec.getInputBuffer(index); - try { - sample.writeToByteBuffer(buf); - mCallbacks.onInputExhausted(); - } catch (IOException e) { - e.printStackTrace(); - } catch (RemoteException e) { - e.printStackTrace(); - } - mSamplePool.recycleInput(sample); - } - mCodec.queueInputBuffer(index, 0, len, pts, flags); - } - } - - private synchronized void reset() { - mInputSamples.clear(); - mAvailableInputBuffers.clear(); - } - } - - private volatile ICodecCallbacks mCallbacks; - private AsyncCodec mCodec; - private InputProcessor mInputProcessor; - private volatile boolean mFlushing = false; - private SamplePool mSamplePool; - private Queue mSentOutputs = new ConcurrentLinkedQueue<>(); - - public synchronized void setCallbacks(ICodecCallbacks callbacks) throws RemoteException { - mCallbacks = callbacks; - callbacks.asBinder().linkToDeath(this, 0); - } - - // IBinder.DeathRecipient - @Override - public synchronized void binderDied() { - Log.e(LOGTAG, "Callbacks is dead"); - try { - release(); - } catch (RemoteException e) { - // Nowhere to report the error. - } - } - - @Override - public synchronized boolean configure(FormatParam format, Surface surface, int flags) throws RemoteException { - if (mCallbacks == null) { - Log.e(LOGTAG, "FAIL: callbacks must be set before calling configure()"); - return false; - } - - if (mCodec != null) { - if (DEBUG) Log.d(LOGTAG, "release existing codec: " + mCodec); - releaseCodec(); - } - - if (DEBUG) Log.d(LOGTAG, "configure " + this); - - MediaFormat fmt = format.asFormat(); - String codecName = getDecoderForFormat(fmt); - if (codecName == null) { - Log.e(LOGTAG, "FAIL: cannot find codec"); - return false; - } - - try { - AsyncCodec codec = AsyncCodecFactory.create(codecName); - codec.setCallbacks(new Callbacks(mCallbacks), null); - codec.configure(fmt, surface, flags); - mCodec = codec; - mInputProcessor = new InputProcessor(); - mSamplePool = new SamplePool(codecName); - if (DEBUG) Log.d(LOGTAG, codec.toString() + " created"); - return true; - } catch (Exception e) { - if (DEBUG) Log.d(LOGTAG, "FAIL: cannot create codec -- " + codecName); - e.printStackTrace(); - return false; - } - } - - private void releaseCodec() { - mInputProcessor.reset(); - try { - mCodec.release(); - } catch (Exception e) { - reportError(Error.FATAL, e); - } - mCodec = null; - } - - private String getDecoderForFormat(MediaFormat format) { - String mime = format.getString(MediaFormat.KEY_MIME); - if (mime == null) { - return null; - } - int numCodecs = MediaCodecList.getCodecCount(); - for (int i = 0; i < numCodecs; i++) { - MediaCodecInfo info = MediaCodecList.getCodecInfoAt(i); - if (info.isEncoder()) { - continue; - } - String[] types = info.getSupportedTypes(); - for (String t : types) { - if (t.equalsIgnoreCase(mime)) { - return info.getName(); - } - } - } - return null; - // TODO: API 21+ is simpler. - //static MediaCodecList sCodecList = new MediaCodecList(MediaCodecList.ALL_CODECS); - //return sCodecList.findDecoderForFormat(format); - } - - @Override - public synchronized void start() throws RemoteException { - if (DEBUG) Log.d(LOGTAG, "start " + this); - mFlushing = false; - try { - mCodec.start(); - } catch (Exception e) { - reportError(Error.FATAL, e); - } - } - - private void reportError(Error error, Exception e) { - if (e != null) { - e.printStackTrace(); - } - try { - mCallbacks.onError(error == Error.FATAL); - } catch (RemoteException re) { - re.printStackTrace(); - } - } - - @Override - public synchronized void stop() throws RemoteException { - if (DEBUG) Log.d(LOGTAG, "stop " + this); - try { - mCodec.stop(); - } catch (Exception e) { - reportError(Error.FATAL, e); - } - } - - @Override - public synchronized void flush() throws RemoteException { - mFlushing = true; - if (DEBUG) Log.d(LOGTAG, "flush " + this); - mInputProcessor.reset(); - try { - mCodec.flush(); - } catch (Exception e) { - reportError(Error.FATAL, e); - } - - mFlushing = false; - if (DEBUG) Log.d(LOGTAG, "flushed " + this); - } - - @Override - public synchronized Sample dequeueInput(int size) { - return mInputProcessor.onAllocate(size); - } - - @Override - public synchronized void queueInput(Sample sample) throws RemoteException { - if (!mInputProcessor.onSample(sample)) { - reportError(Error.FATAL, new Exception("FAIL: input sample queue is full")); - } - } - - @Override - public synchronized void releaseOutput(Sample sample) { - try { - mSamplePool.recycleOutput(mSentOutputs.remove()); - } catch (Exception e) { - Log.e(LOGTAG, "failed to release output:" + sample); - e.printStackTrace(); - } - sample.dispose(); - } - - @Override - public synchronized void release() throws RemoteException { - if (DEBUG) Log.d(LOGTAG, "release " + this); - releaseCodec(); - mSamplePool.reset(); - mSamplePool = null; - mCallbacks.asBinder().unlinkToDeath(this, 0); - mCallbacks = null; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/media/CodecProxy.java b/mobile/android/base/java/org/mozilla/gecko/media/CodecProxy.java deleted file mode 100644 index 3025c14d06..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/media/CodecProxy.java +++ /dev/null @@ -1,191 +0,0 @@ -/* 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.media; - -import android.media.MediaCodec; -import android.media.MediaCodec.BufferInfo; -import android.media.MediaCodec.CryptoInfo; -import android.media.MediaFormat; -import android.os.DeadObjectException; -import android.os.RemoteException; -import android.util.Log; -import android.view.Surface; - -import org.mozilla.gecko.annotation.WrapForJNI; -import org.mozilla.gecko.mozglue.JNIObject; - -import java.io.IOException; -import java.nio.ByteBuffer; - -// Proxy class of ICodec binder. -public final class CodecProxy { - private static final String LOGTAG = "GeckoRemoteCodecProxy"; - private static final boolean DEBUG = false; - - private ICodec mRemote; - private FormatParam mFormat; - private Surface mOutputSurface; - private CallbacksForwarder mCallbacks; - - public interface Callbacks { - void onInputExhausted(); - void onOutputFormatChanged(MediaFormat format); - void onOutput(Sample output); - void onError(boolean fatal); - } - - @WrapForJNI - public static class NativeCallbacks extends JNIObject implements Callbacks { - public native void onInputExhausted(); - public native void onOutputFormatChanged(MediaFormat format); - public native void onOutput(Sample output); - public native void onError(boolean fatal); - - @Override // JNIObject - protected native void disposeNative(); - } - - private class CallbacksForwarder extends ICodecCallbacks.Stub { - private final Callbacks mCallbacks; - - CallbacksForwarder(Callbacks callbacks) { - mCallbacks = callbacks; - } - - @Override - public void onInputExhausted() throws RemoteException { - mCallbacks.onInputExhausted(); - } - - @Override - public void onOutputFormatChanged(FormatParam format) throws RemoteException { - mCallbacks.onOutputFormatChanged(format.asFormat()); - } - - @Override - public void onOutput(Sample sample) throws RemoteException { - mCallbacks.onOutput(sample); - mRemote.releaseOutput(sample); - sample.dispose(); - } - - @Override - public void onError(boolean fatal) throws RemoteException { - reportError(fatal); - } - - public void reportError(boolean fatal) { - mCallbacks.onError(fatal); - } - } - - @WrapForJNI - public static CodecProxy create(MediaFormat format, Surface surface, Callbacks callbacks) { - return RemoteManager.getInstance().createCodec(format, surface, callbacks); - } - - public static CodecProxy createCodecProxy(MediaFormat format, Surface surface, Callbacks callbacks) { - return new CodecProxy(format, surface, callbacks); - } - - private CodecProxy(MediaFormat format, Surface surface, Callbacks callbacks) { - mFormat = new FormatParam(format); - mOutputSurface = surface; - mCallbacks = new CallbacksForwarder(callbacks); - } - - boolean init(ICodec remote) { - try { - remote.setCallbacks(mCallbacks); - remote.configure(mFormat, mOutputSurface, 0); - remote.start(); - } catch (RemoteException e) { - e.printStackTrace(); - return false; - } - - mRemote = remote; - return true; - } - - boolean deinit() { - try { - mRemote.stop(); - mRemote.release(); - mRemote = null; - return true; - } catch (RemoteException e) { - e.printStackTrace(); - return false; - } - } - - @WrapForJNI - public synchronized boolean input(ByteBuffer bytes, BufferInfo info, CryptoInfo cryptoInfo) { - if (mRemote == null) { - Log.e(LOGTAG, "cannot send input to an ended codec"); - return false; - } - - try { - Sample sample = (info.flags == MediaCodec.BUFFER_FLAG_END_OF_STREAM) ? - Sample.EOS : mRemote.dequeueInput(info.size).set(bytes, info, cryptoInfo); - mRemote.queueInput(sample); - sample.dispose(); - } catch (IOException e) { - e.printStackTrace(); - return false; - } catch (DeadObjectException e) { - return false; - } catch (RemoteException e) { - e.printStackTrace(); - Log.e(LOGTAG, "fail to input sample: size=" + info.size + - ", pts=" + info.presentationTimeUs + - ", flags=" + Integer.toHexString(info.flags)); - return false; - } - return true; - } - - @WrapForJNI - public synchronized boolean flush() { - if (mRemote == null) { - Log.e(LOGTAG, "cannot flush an ended codec"); - return false; - } - try { - if (DEBUG) Log.d(LOGTAG, "flush " + this); - mRemote.flush(); - } catch (DeadObjectException e) { - return false; - } catch (RemoteException e) { - e.printStackTrace(); - return false; - } - return true; - } - - @WrapForJNI - public synchronized boolean release() { - if (mRemote == null) { - Log.w(LOGTAG, "codec already ended"); - return true; - } - if (DEBUG) Log.d(LOGTAG, "release " + this); - try { - RemoteManager.getInstance().releaseCodec(this); - } catch (DeadObjectException e) { - return false; - } catch (RemoteException e) { - e.printStackTrace(); - return false; - } - return true; - } - - public synchronized void reportError(boolean fatal) { - mCallbacks.reportError(fatal); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/media/FormatParam.java b/mobile/android/base/java/org/mozilla/gecko/media/FormatParam.java deleted file mode 100644 index c6762672d1..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/media/FormatParam.java +++ /dev/null @@ -1,133 +0,0 @@ -/* 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.media; - -import android.media.MediaFormat; -import android.os.Bundle; -import android.os.Parcel; -import android.os.Parcelable; - -import java.nio.ByteBuffer; - -/** A wrapper to make {@link MediaFormat} parcelable. - * Supports following keys: - *

    - *
  • {@link MediaFormat#KEY_MIME}
  • - *
  • {@link MediaFormat#KEY_WIDTH}
  • - *
  • {@link MediaFormat#KEY_HEIGHT}
  • - *
  • {@link MediaFormat#KEY_CHANNEL_COUNT}
  • - *
  • {@link MediaFormat#KEY_SAMPLE_RATE}
  • - *
  • "csd-0"
  • - *
  • "csd-1"
  • - *
- */ -public final class FormatParam implements Parcelable { - // Keys for codec specific config bits not exposed in {@link MediaFormat}. - private static final String KEY_CONFIG_0 = "csd-0"; - private static final String KEY_CONFIG_1 = "csd-1"; - - private MediaFormat mFormat; - - public MediaFormat asFormat() { - return mFormat; - } - - public FormatParam(MediaFormat format) { - mFormat = format; - } - - protected FormatParam(Parcel in) { - mFormat = new MediaFormat(); - readFromParcel(in); - } - - public static final Creator CREATOR = new Creator() { - @Override - public FormatParam createFromParcel(Parcel in) { - return new FormatParam(in); - } - - @Override - public FormatParam[] newArray(int size) { - return new FormatParam[size]; - } - }; - - @Override - public int describeContents() { - return 0; - } - - public void readFromParcel(Parcel in) { - Bundle bundle = in.readBundle(); - fromBundle(bundle); - } - - private void fromBundle(Bundle bundle) { - if (bundle.containsKey(MediaFormat.KEY_MIME)) { - mFormat.setString(MediaFormat.KEY_MIME, - bundle.getString(MediaFormat.KEY_MIME)); - } - if (bundle.containsKey(MediaFormat.KEY_WIDTH)) { - mFormat.setInteger(MediaFormat.KEY_WIDTH, - bundle.getInt(MediaFormat.KEY_WIDTH)); - } - if (bundle.containsKey(MediaFormat.KEY_HEIGHT)) { - mFormat.setInteger(MediaFormat.KEY_HEIGHT, - bundle.getInt(MediaFormat.KEY_HEIGHT)); - } - if (bundle.containsKey(MediaFormat.KEY_CHANNEL_COUNT)) { - mFormat.setInteger(MediaFormat.KEY_CHANNEL_COUNT, - bundle.getInt(MediaFormat.KEY_CHANNEL_COUNT)); - } - if (bundle.containsKey(MediaFormat.KEY_SAMPLE_RATE)) { - mFormat.setInteger(MediaFormat.KEY_SAMPLE_RATE, - bundle.getInt(MediaFormat.KEY_SAMPLE_RATE)); - } - if (bundle.containsKey(KEY_CONFIG_0)) { - mFormat.setByteBuffer(KEY_CONFIG_0, - ByteBuffer.wrap(bundle.getByteArray(KEY_CONFIG_0))); - } - if (bundle.containsKey(KEY_CONFIG_1)) { - mFormat.setByteBuffer(KEY_CONFIG_1, - ByteBuffer.wrap(bundle.getByteArray((KEY_CONFIG_1)))); - } - } - - @Override - public void writeToParcel(Parcel dest, int flags) { - dest.writeBundle(toBundle()); - } - - private Bundle toBundle() { - Bundle bundle = new Bundle(); - if (mFormat.containsKey(MediaFormat.KEY_MIME)) { - bundle.putString(MediaFormat.KEY_MIME, mFormat.getString(MediaFormat.KEY_MIME)); - } - if (mFormat.containsKey(MediaFormat.KEY_WIDTH)) { - bundle.putInt(MediaFormat.KEY_WIDTH, mFormat.getInteger(MediaFormat.KEY_WIDTH)); - } - if (mFormat.containsKey(MediaFormat.KEY_HEIGHT)) { - bundle.putInt(MediaFormat.KEY_HEIGHT, mFormat.getInteger(MediaFormat.KEY_HEIGHT)); - } - if (mFormat.containsKey(MediaFormat.KEY_CHANNEL_COUNT)) { - bundle.putInt(MediaFormat.KEY_CHANNEL_COUNT, mFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT)); - } - if (mFormat.containsKey(MediaFormat.KEY_SAMPLE_RATE)) { - bundle.putInt(MediaFormat.KEY_SAMPLE_RATE, mFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE)); - } - if (mFormat.containsKey(KEY_CONFIG_0)) { - ByteBuffer bytes = mFormat.getByteBuffer(KEY_CONFIG_0); - bundle.putByteArray(KEY_CONFIG_0, - Sample.byteArrayFromBuffer(bytes, 0, bytes.capacity())); - } - if (mFormat.containsKey(KEY_CONFIG_1)) { - ByteBuffer bytes = mFormat.getByteBuffer(KEY_CONFIG_1); - bundle.putByteArray(KEY_CONFIG_1, - Sample.byteArrayFromBuffer(bytes, 0, bytes.capacity())); - } - return bundle; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/media/GeckoMediaDrm.java b/mobile/android/base/java/org/mozilla/gecko/media/GeckoMediaDrm.java deleted file mode 100644 index 7b3bda3fd7..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/media/GeckoMediaDrm.java +++ /dev/null @@ -1,35 +0,0 @@ -/* 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.media; - -import android.media.MediaCrypto; - -public interface GeckoMediaDrm { - public interface Callbacks { - void onSessionCreated(int createSessionToken, - int promiseId, - byte[] sessionId, - byte[] request); - void onSessionUpdated(int promiseId, byte[] sessionId); - void onSessionClosed(int promiseId, byte[] sessionId); - void onSessionMessage(byte[] sessionId, - int sessionMessageType, - byte[] request); - void onSessionError(byte[] sessionId, String message); - void onSessionBatchedKeyChanged(byte[] sessionId, - SessionKeyInfo[] keyInfos); - // All failure cases should go through this function. - void onRejectPromise(int promiseId, String message); - } - void setCallbacks(Callbacks callbacks); - void createSession(int createSessionToken, - int promiseId, - String initDataType, - byte[] initData); - void updateSession(int promiseId, String sessionId, byte[] response); - void closeSession(int promiseId, String sessionId); - void release(); - MediaCrypto getMediaCrypto(); -} diff --git a/mobile/android/base/java/org/mozilla/gecko/media/GeckoMediaDrmBridgeV21.java b/mobile/android/base/java/org/mozilla/gecko/media/GeckoMediaDrmBridgeV21.java deleted file mode 100644 index 6ccaf80dfc..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/media/GeckoMediaDrmBridgeV21.java +++ /dev/null @@ -1,627 +0,0 @@ -/* 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.media; - -import java.lang.*; -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStreamReader; -import java.net.HttpURLConnection; -import java.net.URL; -import java.net.URLEncoder; -import java.nio.ByteBuffer; -import java.util.HashMap; -import java.util.HashSet; -import java.util.UUID; -import java.util.ArrayDeque; - -import android.annotation.SuppressLint; -import android.os.AsyncTask; -import android.os.Handler; -import android.os.HandlerThread; -import android.media.MediaCrypto; -import android.media.MediaCryptoException; -import android.media.MediaDrm; -import android.media.MediaDrmException; -import android.util.Log; - -public class GeckoMediaDrmBridgeV21 implements GeckoMediaDrm { - private static final String LOGTAG = "GeckoMediaDrmBridgeV21"; - private static final String INVALID_SESSION_ID = "Invalid"; - private static final String WIDEVINE_KEY_SYSTEM = "com.widevine.alpha"; - private static final boolean DEBUG = false; - private static final UUID WIDEVINE_SCHEME_UUID = - new UUID(0xedef8ba979d64aceL, 0xa3c827dcd51d21edL); - // MediaDrm.KeyStatus information listener is supported on M+, adding a - // dummy key id to report key status. - private static final byte[] DUMMY_KEY_ID = new byte[] {0}; - - private UUID mSchemeUUID; - private Handler mHandler; - private HandlerThread mHandlerThread; - private ByteBuffer mCryptoSessionId; - - // mProvisioningPromiseId is great than 0 only during provisioning. - private int mProvisioningPromiseId; - private HashSet mSessionIds; - private HashMap mSessionMIMETypes; - private ArrayDeque mPendingCreateSessionDataQueue; - private GeckoMediaDrm.Callbacks mCallbacks; - - private MediaCrypto mCrypto; - protected MediaDrm mDrm; - - public static int LICENSE_REQUEST_INITIAL = 0; /*MediaKeyMessageType::License_request*/ - public static int LICENSE_REQUEST_RENEWAL = 1; /*MediaKeyMessageType::License_renewal*/ - public static int LICENSE_REQUEST_RELEASE = 2; /*MediaKeyMessageType::License_release*/ - - // Store session data while provisioning - private static class PendingCreateSessionData { - public final int mToken; - public final int mPromiseId; - public final byte[] mInitData; - public final String mMimeType; - - private PendingCreateSessionData(int token, int promiseId, - byte[] initData, String mimeType) { - mToken = token; - mPromiseId = promiseId; - mInitData = initData; - mMimeType = mimeType; - } - } - - public boolean isSecureDecoderComonentRequired(String mimeType) { - if (mCrypto != null) { - return mCrypto.requiresSecureDecoderComponent(mimeType); - } - return false; - } - - private static void assertTrue(boolean condition) { - if (DEBUG && !condition) { - throw new AssertionError("Expected condition to be true"); - } - } - - @SuppressLint("WrongConstant") - private void configureVendorSpecificProperty() { - assertTrue(mDrm != null); - // Support L3 for now - mDrm.setPropertyString("securityLevel", "L3"); - // Refer to chromium, set multi-session mode for Widevine. - if (mSchemeUUID.equals(WIDEVINE_SCHEME_UUID)) { - mDrm.setPropertyString("sessionSharing", "enable"); - } - } - - GeckoMediaDrmBridgeV21(String keySystem) throws Exception { - if (DEBUG) Log.d(LOGTAG, "GeckoMediaDrmBridgeV21()"); - - mProvisioningPromiseId = 0; - mSessionIds = new HashSet(); - mSessionMIMETypes = new HashMap(); - mPendingCreateSessionDataQueue = new ArrayDeque(); - - mSchemeUUID = convertKeySystemToSchemeUUID(keySystem); - mCryptoSessionId = null; - - if (DEBUG) Log.d(LOGTAG, "mSchemeUUID : " + mSchemeUUID.toString()); - - // The caller of GeckoMediaDrmBridgeV21 ctor should handle exceptions - // threw by the following steps. - mDrm = new MediaDrm(mSchemeUUID); - configureVendorSpecificProperty(); - mDrm.setOnEventListener(new MediaDrmListener()); - } - - @Override - public void setCallbacks(GeckoMediaDrm.Callbacks callbacks) { - assertTrue(callbacks != null); - mCallbacks = callbacks; - } - - @Override - public void createSession(int createSessionToken, - int promiseId, - String initDataType, - byte[] initData) { - if (DEBUG) Log.d(LOGTAG, "createSession()"); - if (mDrm == null) { - onRejectPromise(promiseId, "MediaDrm instance doesn't exist !!"); - return; - } - - if (mProvisioningPromiseId > 0 && mCrypto == null) { - if (DEBUG) Log.d(LOGTAG, "Pending createSession because it's provisioning !"); - savePendingCreateSessionData(createSessionToken, promiseId, - initData, initDataType); - return; - } - - ByteBuffer sessionId = null; - String strSessionId = null; - try { - boolean hasMediaCrypto = ensureMediaCryptoCreated(); - if (!hasMediaCrypto) { - onRejectPromise(promiseId, "MediaCrypto intance is not created !"); - return; - } - - sessionId = openSession(); - if (sessionId == null) { - onRejectPromise(promiseId, "Cannot get a session id from MediaDrm !"); - return; - } - - MediaDrm.KeyRequest request = getKeyRequest(sessionId, initData, initDataType); - if (request == null) { - mDrm.closeSession(sessionId.array()); - onRejectPromise(promiseId, "Cannot get a key request from MediaDrm !"); - return; - } - onSessionCreated(createSessionToken, - promiseId, - sessionId.array(), - request.getData()); - onSessionMessage(sessionId.array(), - LICENSE_REQUEST_INITIAL, - request.getData()); - mSessionMIMETypes.put(sessionId, initDataType); - strSessionId = new String(sessionId.array()); - mSessionIds.add(sessionId); - if (DEBUG) Log.d(LOGTAG, " StringID : " + strSessionId + " is put into mSessionIds "); - } catch (android.media.NotProvisionedException e) { - if (DEBUG) Log.d(LOGTAG, "Device not provisioned:" + e.getMessage()); - if (sessionId != null) { - // The promise of this createSession will be either resolved - // or rejected after provisioning. - mDrm.closeSession(sessionId.array()); - } - savePendingCreateSessionData(createSessionToken, promiseId, - initData, initDataType); - startProvisioning(promiseId); - } - } - - @Override - public void updateSession(int promiseId, - String sessionId, - byte[] response) { - if (DEBUG) Log.d(LOGTAG, "updateSession(), sessionId = " + sessionId); - if (mDrm == null) { - onRejectPromise(promiseId, "MediaDrm instance doesn't exist !!"); - return; - } - - ByteBuffer session = ByteBuffer.wrap(sessionId.getBytes()); - if (!sessionExists(session)) { - onRejectPromise(promiseId, "Invalid session during updateSession."); - return; - } - - try { - final byte [] keySetId = mDrm.provideKeyResponse(session.array(), response); - if (DEBUG) { - HashMap infoMap = mDrm.queryKeyStatus(session.array()); - for (String strKey : infoMap.keySet()) { - String strValue = infoMap.get(strKey); - Log.d(LOGTAG, "InfoMap : key(" + strKey + ")/value(" + strValue + ")"); - } - } - SessionKeyInfo[] keyInfos = new SessionKeyInfo[1]; - keyInfos[0] = new SessionKeyInfo(DUMMY_KEY_ID, - MediaDrm.KeyStatus.STATUS_USABLE); - onSessionBatchedKeyChanged(session.array(), keyInfos); - if (DEBUG) Log.d(LOGTAG, "Key successfully added for session " + sessionId); - onSessionUpdated(promiseId, session.array()); - return; - } catch (android.media.NotProvisionedException e) { - if (DEBUG) Log.d(LOGTAG, "Failed to provide key response:" + e.getMessage()); - onSessionError(session.array(), "Got NotProvisionedException."); - onRejectPromise(promiseId, "Not provisioned during updateSession."); - } catch (android.media.DeniedByServerException e) { - if (DEBUG) Log.d(LOGTAG, "Failed to provide key response:" + e.getMessage()); - onSessionError(session.array(), "Got DeniedByServerException."); - onRejectPromise(promiseId, "Denied by server during updateSession."); - } catch (java.lang.IllegalStateException e) { - if (DEBUG) Log.d(LOGTAG, "Exception when calling provideKeyResponse():" + e.getMessage()); - onSessionError(session.array(), "Got IllegalStateException."); - onRejectPromise(promiseId, "Rejected during updateSession."); - } - release(); - return; - } - - @Override - public void closeSession(int promiseId, String sessionId) { - if (DEBUG) Log.d(LOGTAG, "closeSession()"); - if (mDrm == null) { - onRejectPromise(promiseId, "MediaDrm instance doesn't exist !!"); - return; - } - - ByteBuffer session = ByteBuffer.wrap(sessionId.getBytes()); - mSessionIds.remove(session); - mDrm.closeSession(session.array()); - onSessionClosed(promiseId, session.array()); - } - - @Override - public void release() { - if (DEBUG) Log.d(LOGTAG, "release()"); - if (mProvisioningPromiseId > 0) { - onRejectPromise(mProvisioningPromiseId, "Releasing ... reject provisioning session."); - mProvisioningPromiseId = 0; - } - while (!mPendingCreateSessionDataQueue.isEmpty()) { - PendingCreateSessionData pendingData = mPendingCreateSessionDataQueue.poll(); - onRejectPromise(pendingData.mPromiseId, "Releasing ... reject all pending sessions."); - } - mPendingCreateSessionDataQueue = null; - - if (mDrm != null) { - for (ByteBuffer session : mSessionIds) { - mDrm.closeSession(session.array()); - } - mDrm.release(); - mDrm = null; - } - mSessionIds.clear(); - mSessionIds = null; - mSessionMIMETypes.clear(); - mSessionMIMETypes = null; - - mCryptoSessionId = null; - if (mCrypto != null) { - mCrypto.release(); - mCrypto = null; - } - if (mHandlerThread != null) { - mHandlerThread.quitSafely(); - mHandlerThread = null; - } - mHandler = null; - } - - @Override - public MediaCrypto getMediaCrypto() { - if (DEBUG) Log.d(LOGTAG, "getMediaCrypto()"); - return mCrypto; - } - - protected void onSessionCreated(int createSessionToken, - int promiseId, - byte[] sessionId, - byte[] request) { - assertTrue(mCallbacks != null); - mCallbacks.onSessionCreated(createSessionToken, promiseId, sessionId, request); - } - - protected void onSessionUpdated(int promiseId, byte[] sessionId) { - assertTrue(mCallbacks != null); - mCallbacks.onSessionUpdated(promiseId, sessionId); - } - - protected void onSessionClosed(int promiseId, byte[] sessionId) { - assertTrue(mCallbacks != null); - mCallbacks.onSessionClosed(promiseId, sessionId); - } - - protected void onSessionMessage(byte[] sessionId, - int sessionMessageType, - byte[] request) { - assertTrue(mCallbacks != null); - mCallbacks.onSessionMessage(sessionId, sessionMessageType, request); - } - - protected void onSessionError(byte[] sessionId, String message) { - assertTrue(mCallbacks != null); - mCallbacks.onSessionError(sessionId, message); - } - - protected void onSessionBatchedKeyChanged(byte[] sessionId, - SessionKeyInfo[] keyInfos) { - assertTrue(mCallbacks != null); - mCallbacks.onSessionBatchedKeyChanged(sessionId, keyInfos); - } - - protected void onRejectPromise(int promiseId, String message) { - assertTrue(mCallbacks != null); - mCallbacks.onRejectPromise(promiseId, message); - } - - private MediaDrm.KeyRequest getKeyRequest(ByteBuffer aSession, - byte[] data, - String mimeType) - throws android.media.NotProvisionedException { - if (mProvisioningPromiseId > 0) { - // Now provisioning. - return null; - } - - try { - HashMap optionalParameters = new HashMap(); - return mDrm.getKeyRequest(aSession.array(), - data, - mimeType, - MediaDrm.KEY_TYPE_STREAMING, - optionalParameters); - } catch (Exception e) { - Log.e(LOGTAG, "Got excpetion during MediaDrm.getKeyRequest", e); - } - return null; - } - - private class MediaDrmListener implements MediaDrm.OnEventListener { - @Override - public void onEvent(MediaDrm mediaDrm, byte[] sessionArray, int event, - int extra, byte[] data) { - if (DEBUG) Log.d(LOGTAG, "MediaDrmListener.onEvent()"); - if (sessionArray == null) { - if (DEBUG) Log.d(LOGTAG, "MediaDrmListener: Null session."); - return; - } - ByteBuffer session = ByteBuffer.wrap(sessionArray); - if (!sessionExists(session)) { - if (DEBUG) Log.d(LOGTAG, "MediaDrmListener: Invalid session."); - return; - } - // On L, these events are treated as exceptions and handled correspondingly. - // Leaving this code block for logging message. - String sessionId = new String(session.array()); - switch (event) { - case MediaDrm.EVENT_PROVISION_REQUIRED: - if (DEBUG) Log.d(LOGTAG, "MediaDrm.EVENT_PROVISION_REQUIRED"); - break; - case MediaDrm.EVENT_KEY_REQUIRED: - if (DEBUG) Log.d(LOGTAG, "MediaDrm.EVENT_KEY_REQUIRED"); - // No need to handle here if we're not in privacy mode. - break; - case MediaDrm.EVENT_KEY_EXPIRED: - if (DEBUG) Log.d(LOGTAG, "MediaDrm.EVENT_KEY_EXPIRED, sessionId=" + sessionId); - break; - case MediaDrm.EVENT_VENDOR_DEFINED: - if (DEBUG) Log.d(LOGTAG, "MediaDrm.EVENT_VENDOR_DEFINED, sessionId=" + sessionId); - break; - default: - if (DEBUG) Log.d(LOGTAG, "Invalid DRM event " + event); - return; - } - } - } - - private ByteBuffer openSession() throws android.media.NotProvisionedException { - try { - byte[] sessionId = mDrm.openSession(); - // ByteBuffer.wrap() is backed by the byte[]. Make a clone here in - // case the underlying byte[] is modified. - return ByteBuffer.wrap(sessionId.clone()); - } catch (android.media.NotProvisionedException e) { - // Throw NotProvisionedException so that we can startProvisioning(). - throw e; - } catch (java.lang.RuntimeException e) { - if (DEBUG) Log.d(LOGTAG, "Cannot open a new session:" + e.getMessage()); - release(); - return null; - } catch (android.media.MediaDrmException e) { - // Other MediaDrmExceptions (e.g. ResourceBusyException) are not - // recoverable. - release(); - return null; - } - } - - private boolean sessionExists(ByteBuffer session) { - if (mCryptoSessionId == null) { - if (DEBUG) Log.d(LOGTAG, "Session doesn't exist because media crypto session is not created."); - return false; - } - if (session == null) { - if (DEBUG) Log.d(LOGTAG, "Session is null, not in map !"); - return false; - } - return !session.equals(mCryptoSessionId) && mSessionIds.contains(session); - } - - private class PostRequestTask extends AsyncTask { - private static final String LOGTAG = "PostRequestTask"; - - private int mPromiseId; - private String mURL; - private byte[] mDrmRequest; - private byte[] mResponseBody; - - PostRequestTask(int promiseId, String url, byte[] drmRequest) { - this.mPromiseId = promiseId; - this.mURL = url; - this.mDrmRequest = drmRequest; - } - - @Override - protected Void doInBackground(Void... params) { - try { - URL finalURL = new URL(mURL + "&signedRequest=" + URLEncoder.encode(new String(mDrmRequest), "UTF-8")); - HttpURLConnection urlConnection = (HttpURLConnection) finalURL.openConnection(); - urlConnection.setRequestMethod("POST"); - if (DEBUG) Log.d(LOGTAG, "Provisioning, posting url =" + finalURL.toString()); - - // Add data - urlConnection.setRequestProperty("Accept", "*/*"); - urlConnection.setRequestProperty("User-Agent", getCDMUserAgent()); - urlConnection.setRequestProperty("Content-Type", "application/json"); - - // Execute HTTP Post Request - urlConnection.connect(); - - int responseCode = urlConnection.getResponseCode(); - if (responseCode == HttpURLConnection.HTTP_OK) { - BufferedReader in = - new BufferedReader(new InputStreamReader(urlConnection.getInputStream())); - String inputLine; - StringBuffer response = new StringBuffer(); - - while ((inputLine = in.readLine()) != null) { - response.append(inputLine); - } - in.close(); - mResponseBody = String.valueOf(response).getBytes(); - if (DEBUG) Log.d(LOGTAG, "Provisioning, response received."); - if (mResponseBody != null) Log.d(LOGTAG, "response length=" + mResponseBody.length); - } else { - Log.d(LOGTAG, "Provisioning, server returned HTTP error code :" + responseCode); - } - } catch (IOException e) { - Log.e(LOGTAG, "Got exception during posting provisioning request ...", e); - } - return null; - } - - @Override - protected void onPostExecute(Void v) { - onProvisionResponse(mPromiseId, mResponseBody); - } - } - - private boolean provideProvisionResponse(byte[] response) { - if (response == null || response.length == 0) { - if (DEBUG) Log.d(LOGTAG, "Invalid provision response."); - return false; - } - - try { - mDrm.provideProvisionResponse(response); - return true; - } catch (android.media.DeniedByServerException e) { - if (DEBUG) Log.d(LOGTAG, "Failed to provide provision response:" + e.getMessage()); - } catch (java.lang.IllegalStateException e) { - if (DEBUG) Log.d(LOGTAG, "Failed to provide provision response:" + e.getMessage()); - } - return false; - } - - private void savePendingCreateSessionData(int token, - int promiseId, - byte[] initData, - String mime) { - if (DEBUG) Log.d(LOGTAG, "savePendingCreateSessionData, promiseId : " + promiseId); - mPendingCreateSessionDataQueue.offer(new PendingCreateSessionData(token, promiseId, initData, mime)); - } - - private void processPendingCreateSessionData() { - if (DEBUG) Log.d(LOGTAG, "processPendingCreateSessionData ... "); - - assertTrue(mProvisioningPromiseId == 0); - try { - while (!mPendingCreateSessionDataQueue.isEmpty()) { - PendingCreateSessionData pendingData = mPendingCreateSessionDataQueue.poll(); - if (DEBUG) Log.d(LOGTAG, "processPendingCreateSessionData, promiseId : " + pendingData.mPromiseId); - - createSession(pendingData.mToken, - pendingData.mPromiseId, - pendingData.mMimeType, - pendingData.mInitData); - } - } catch (Exception e) { - Log.e(LOGTAG, "Got excpetion during processPendingCreateSessionData ...", e); - } - } - - private void resumePendingOperations() { - if (mHandlerThread == null) { - mHandlerThread = new HandlerThread("PendingSessionOpsThread"); - mHandlerThread.start(); - } - if (mHandler == null) { - mHandler = new Handler(mHandlerThread.getLooper()); - } - mHandler.post(new Runnable() { - @Override - public void run() { - processPendingCreateSessionData(); - } - }); - } - - // Only triggered when failed on {openSession, getKeyRequest} - private void startProvisioning(int promiseId) { - if (DEBUG) Log.d(LOGTAG, "startProvisioning()"); - if (mProvisioningPromiseId > 0) { - // Already in provisioning. - return; - } - try { - mProvisioningPromiseId = promiseId; - MediaDrm.ProvisionRequest request = mDrm.getProvisionRequest(); - PostRequestTask postTask = - new PostRequestTask(promiseId, request.getDefaultUrl(), request.getData()); - postTask.execute(); - } catch (Exception e) { - onRejectPromise(promiseId, "Exception happened in startProvisioning !"); - mProvisioningPromiseId = 0; - } - } - - private void onProvisionResponse(int promiseId, byte[] response) { - if (DEBUG) Log.d(LOGTAG, "onProvisionResponse()"); - - mProvisioningPromiseId = 0; - boolean success = provideProvisionResponse(response); - if (success) { - // Promise will either be resovled / rejected in createSession during - // resuming operations. - resumePendingOperations(); - } else { - onRejectPromise(promiseId, "Failed to provide provision response."); - } - } - - private boolean ensureMediaCryptoCreated() throws android.media.NotProvisionedException { - if (mCrypto != null) { - return true; - } - try { - mCryptoSessionId = openSession(); - if (mCryptoSessionId == null) { - if (DEBUG) Log.d(LOGTAG, "Cannot open session for MediaCrypto"); - return false; - } - - if (MediaCrypto.isCryptoSchemeSupported(mSchemeUUID)) { - final byte [] cryptoSessionId = mCryptoSessionId.array(); - mCrypto = new MediaCrypto(mSchemeUUID, cryptoSessionId); - String strCryptoSessionId = new String(cryptoSessionId); - mSessionIds.add(mCryptoSessionId); - if (DEBUG) Log.d(LOGTAG, "MediaCrypto successfully created! - SId " + INVALID_SESSION_ID + ", " + strCryptoSessionId); - return true; - } else { - if (DEBUG) Log.d(LOGTAG, "Cannot create MediaCrypto for unsupported scheme."); - return false; - } - } catch (android.media.MediaCryptoException e) { - if (DEBUG) Log.d(LOGTAG, "Cannot create MediaCrypto:" + e.getMessage()); - release(); - return false; - } catch (android.media.NotProvisionedException e) { - if (DEBUG) Log.d(LOGTAG, "ensureMediaCryptoCreated::Device not provisioned:" + e.getMessage()); - throw e; - } - } - - private UUID convertKeySystemToSchemeUUID(String keySystem) { - if (WIDEVINE_KEY_SYSTEM.equals(keySystem)) { - return WIDEVINE_SCHEME_UUID; - } - if (DEBUG) Log.d(LOGTAG, "Cannot convert unsupported key system : " + keySystem); - return null; - } - - private String getCDMUserAgent() { - // This user agent is found and hard-coded in Android(L) source code and - // Chromium project. Not sure if it's gonna change in the future. - String ua = "Widevine CDM v1.0"; - return ua; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/media/GeckoMediaDrmBridgeV23.java b/mobile/android/base/java/org/mozilla/gecko/media/GeckoMediaDrmBridgeV23.java deleted file mode 100644 index 74144f28ed..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/media/GeckoMediaDrmBridgeV23.java +++ /dev/null @@ -1,44 +0,0 @@ -/* 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.media; - -import android.annotation.TargetApi; -import static android.os.Build.VERSION_CODES.M; -import android.media.MediaDrm; -import android.util.Log; -import java.util.List; - -public class GeckoMediaDrmBridgeV23 extends GeckoMediaDrmBridgeV21 { - - private static final String LOGTAG = "GeckoMediaDrmBridgeV23"; - private static final boolean DEBUG = false; - - GeckoMediaDrmBridgeV23(String keySystem) throws Exception { - super(keySystem); - if (DEBUG) Log.d(LOGTAG, "GeckoMediaDrmBridgeV23 ctor"); - mDrm.setOnKeyStatusChangeListener(new KeyStatusChangeListener(), null); - } - - @TargetApi(M) - private class KeyStatusChangeListener implements MediaDrm.OnKeyStatusChangeListener { - @Override - public void onKeyStatusChange(MediaDrm mediaDrm, - byte[] sessionId, - List keyInformation, - boolean hasNewUsableKey) { - if (DEBUG) Log.d(LOGTAG, "[onKeyStatusChange] hasNewUsableKey = " + hasNewUsableKey); - if (keyInformation.size() == 0) { - return; - } - SessionKeyInfo[] keyInfos = new SessionKeyInfo[keyInformation.size()]; - for (int i = 0; i < keyInformation.size(); i++) { - MediaDrm.KeyStatus keyStatus = keyInformation.get(i); - keyInfos[i] = new SessionKeyInfo(keyStatus.getKeyId(), - keyStatus.getStatusCode()); - } - onSessionBatchedKeyChanged(sessionId, keyInfos); - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/media/JellyBeanAsyncCodec.java b/mobile/android/base/java/org/mozilla/gecko/media/JellyBeanAsyncCodec.java deleted file mode 100644 index 3df01f1fe2..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/media/JellyBeanAsyncCodec.java +++ /dev/null @@ -1,405 +0,0 @@ -/* 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.media; - -import android.media.MediaCodec; -import android.media.MediaFormat; -import android.os.Handler; -import android.os.HandlerThread; -import android.os.Looper; -import android.os.Message; -import android.util.Log; -import android.view.Surface; - -import java.io.IOException; -import java.nio.ByteBuffer; - -// Implement async API using MediaCodec sync mode (API v16). -// This class uses internal worker thread/handler (mBufferPoller) to poll -// input and output buffer and notifies the client through callbacks. -final class JellyBeanAsyncCodec implements AsyncCodec { - private static final String LOGTAG = "GeckoAsyncCodecAPIv16"; - private static final boolean DEBUG = false; - - private static final int ERROR_CODEC = -10000; - - private abstract class CancelableHandler extends Handler { - private static final int MSG_CANCELLATION = 0x434E434C; // 'CNCL' - - protected CancelableHandler(Looper looper) { - super(looper); - } - - protected void cancel() { - removeCallbacksAndMessages(null); - sendEmptyMessage(MSG_CANCELLATION); - // Wait until handleMessageLocked() is done. - synchronized (this) { } - } - - protected boolean isCanceled() { - return hasMessages(MSG_CANCELLATION); - } - - // Subclass should implement this and return true if it handles msg. - // Warning: Never, ever call super.handleMessage() in this method! - protected abstract boolean handleMessageLocked(Message msg); - - public final void handleMessage(Message msg) { - // Block cancel() during handleMessageLocked(). - synchronized (this) { - if (isCanceled() || handleMessageLocked(msg)) { - return; - } - } - - switch (msg.what) { - case MSG_CANCELLATION: - // Just a marker. Nothing to do here. - if (DEBUG) Log.d(LOGTAG, "handler " + this + " done cancellation, codec=" + JellyBeanAsyncCodec.this); - break; - default: - super.handleMessage(msg); - break; - } - } - } - - // A handler to invoke AsyncCodec.Callbacks methods. - private final class CallbackSender extends CancelableHandler { - private static final int MSG_INPUT_BUFFER_AVAILABLE = 1; - private static final int MSG_OUTPUT_BUFFER_AVAILABLE = 2; - private static final int MSG_OUTPUT_FORMAT_CHANGE = 3; - private static final int MSG_ERROR = 4; - private Callbacks mCallbacks; - - private CallbackSender(Looper looper, Callbacks callbacks) { - super(looper); - mCallbacks = callbacks; - } - - public void notifyInputBuffer(int index) { - if (isCanceled()) { - return; - } - - Message msg = obtainMessage(MSG_INPUT_BUFFER_AVAILABLE); - msg.arg1 = index; - processMessage(msg); - } - - private void processMessage(Message msg) { - if (Looper.myLooper() == getLooper()) { - handleMessage(msg); - } else { - sendMessage(msg); - } - } - - public void notifyOutputBuffer(int index, MediaCodec.BufferInfo info) { - if (isCanceled()) { - return; - } - - Message msg = obtainMessage(MSG_OUTPUT_BUFFER_AVAILABLE, info); - msg.arg1 = index; - processMessage(msg); - } - - public void notifyOutputFormat(MediaFormat format) { - if (isCanceled()) { - return; - } - processMessage(obtainMessage(MSG_OUTPUT_FORMAT_CHANGE, format)); - } - - public void notifyError(int result) { - Log.e(LOGTAG, "codec error:" + result); - processMessage(obtainMessage(MSG_ERROR, result, 0)); - } - - protected boolean handleMessageLocked(Message msg) { - switch (msg.what) { - case MSG_INPUT_BUFFER_AVAILABLE: // arg1: buffer index. - mCallbacks.onInputBufferAvailable(JellyBeanAsyncCodec.this, - msg.arg1); - break; - case MSG_OUTPUT_BUFFER_AVAILABLE: // arg1: buffer index, obj: info. - mCallbacks.onOutputBufferAvailable(JellyBeanAsyncCodec.this, - msg.arg1, - (MediaCodec.BufferInfo)msg.obj); - break; - case MSG_OUTPUT_FORMAT_CHANGE: // obj: output format. - mCallbacks.onOutputFormatChanged(JellyBeanAsyncCodec.this, - (MediaFormat)msg.obj); - break; - case MSG_ERROR: // arg1: error code. - mCallbacks.onError(JellyBeanAsyncCodec.this, msg.arg1); - break; - default: - return false; - } - - return true; - } - } - - // Handler to poll input and output buffers using dequeue(Input|Output)Buffer(), - // with 10ms time-out. Once triggered and successfully gets a buffer, it - // will schedule next polling until EOS or failure. To prevent it from - // automatically polling more buffer, use cancel() it inherits from - // CancelableHandler. - private final class BufferPoller extends CancelableHandler { - private static final int MSG_POLL_INPUT_BUFFERS = 1; - private static final int MSG_POLL_OUTPUT_BUFFERS = 2; - - private static final long DEQUEUE_TIMEOUT_US = 10000; - - public BufferPoller(Looper looper) { - super(looper); - } - - private void schedulePollingIfNotCanceled(int what) { - if (isCanceled()) { - return; - } - - schedulePolling(what); - } - - private void schedulePolling(int what) { - if (needsBuffer(what)) { - sendEmptyMessage(what); - } - } - - private boolean needsBuffer(int what) { - if (mOutputEnded && (what == MSG_POLL_OUTPUT_BUFFERS)) { - return false; - } - - if (mInputEnded && (what == MSG_POLL_INPUT_BUFFERS)) { - return false; - } - - return true; - } - - protected boolean handleMessageLocked(Message msg) { - try { - switch (msg.what) { - case MSG_POLL_INPUT_BUFFERS: - pollInputBuffer(); - break; - case MSG_POLL_OUTPUT_BUFFERS: - pollOutputBuffer(); - break; - default: - return false; - } - } catch (IllegalStateException e) { - e.printStackTrace(); - mCallbackSender.notifyError(ERROR_CODEC); - } - - return true; - } - - private void pollInputBuffer() { - int result = mCodec.dequeueInputBuffer(DEQUEUE_TIMEOUT_US); - if (result >= 0) { - mCallbackSender.notifyInputBuffer(result); - schedulePollingIfNotCanceled(BufferPoller.MSG_POLL_INPUT_BUFFERS); - } else if (result != MediaCodec.INFO_TRY_AGAIN_LATER) { - mCallbackSender.notifyError(result); - } - } - - private void pollOutputBuffer() { - boolean dequeueMoreBuffer = true; - MediaCodec.BufferInfo info = new MediaCodec.BufferInfo(); - int result = mCodec.dequeueOutputBuffer(info, DEQUEUE_TIMEOUT_US); - if (result >= 0) { - if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) { - mOutputEnded = true; - } - mCallbackSender.notifyOutputBuffer(result, info); - if (!hasMessages(MSG_POLL_INPUT_BUFFERS)) { - schedulePollingIfNotCanceled(MSG_POLL_INPUT_BUFFERS); - } - } else if (result == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) { - mOutputBuffers = mCodec.getOutputBuffers(); - } else if (result == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { - mCallbackSender.notifyOutputFormat(mCodec.getOutputFormat()); - } else if (result == MediaCodec.INFO_TRY_AGAIN_LATER) { - // When input ended, keep polling remaining output buffer until EOS. - dequeueMoreBuffer = mInputEnded; - } else { - mCallbackSender.notifyError(result); - dequeueMoreBuffer = false; - } - - if (dequeueMoreBuffer) { - schedulePollingIfNotCanceled(MSG_POLL_OUTPUT_BUFFERS); - } - } - } - - private MediaCodec mCodec; - private ByteBuffer[] mInputBuffers; - private ByteBuffer[] mOutputBuffers; - private AsyncCodec.Callbacks mCallbacks; - private CallbackSender mCallbackSender; - - private BufferPoller mBufferPoller; - private volatile boolean mInputEnded; - private volatile boolean mOutputEnded; - - // Must be called on a thread with looper. - /* package */ JellyBeanAsyncCodec(String name) throws IOException { - mCodec = MediaCodec.createByCodecName(name); - initBufferPoller(name + " buffer poller"); - } - - private void initBufferPoller(String name) { - if (mBufferPoller != null) { - Log.e(LOGTAG, "poller already initialized"); - return; - } - HandlerThread thread = new HandlerThread(name); - thread.start(); - mBufferPoller = new BufferPoller(thread.getLooper()); - if (DEBUG) Log.d(LOGTAG, "start poller for codec:" + this + ", thread=" + thread.getThreadId()); - } - - @Override - public void setCallbacks(AsyncCodec.Callbacks callbacks, Handler handler) { - if (callbacks == null) { - return; - } - - Looper looper = (handler == null) ? null : handler.getLooper(); - if (looper == null) { - // Use this thread if no handler supplied. - looper = Looper.myLooper(); - } - if (looper == null) { - // This thread has no looper. Use poller thread. - looper = mBufferPoller.getLooper(); - } - mCallbackSender = new CallbackSender(looper, callbacks); - if (DEBUG) Log.d(LOGTAG, "setCallbacks(): sender=" + mCallbackSender); - } - - @Override - public void configure(MediaFormat format, Surface surface, int flags) { - assertCallbacks(); - - mCodec.configure(format, surface, null, flags); - } - - private void assertCallbacks() { - if (mCallbackSender == null) { - throw new IllegalStateException(LOGTAG + ": callback must be supplied with setCallbacks()."); - } - } - - @Override - public void start() { - assertCallbacks(); - - mCodec.start(); - mInputEnded = false; - mOutputEnded = false; - mInputBuffers = mCodec.getInputBuffers(); - mOutputBuffers = mCodec.getOutputBuffers(); - mBufferPoller.schedulePolling(BufferPoller.MSG_POLL_INPUT_BUFFERS); - } - - @Override - public final void queueInputBuffer(int index, int offset, int size, long presentationTimeUs, int flags) { - assertCallbacks(); - - mInputEnded = (flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0; - - try { - mCodec.queueInputBuffer(index, offset, size, presentationTimeUs, flags); - } catch (IllegalStateException e) { - e.printStackTrace(); - mCallbackSender.notifyError(ERROR_CODEC); - return; - } - - mBufferPoller.schedulePolling(BufferPoller.MSG_POLL_INPUT_BUFFERS); - mBufferPoller.schedulePolling(BufferPoller.MSG_POLL_OUTPUT_BUFFERS); - } - - @Override - public final void releaseOutputBuffer(int index, boolean render) { - assertCallbacks(); - - mCodec.releaseOutputBuffer(index, render); - } - - @Override - public final ByteBuffer getInputBuffer(int index) { - assertCallbacks(); - - return mInputBuffers[index]; - } - - @Override - public final ByteBuffer getOutputBuffer(int index) { - assertCallbacks(); - - return mOutputBuffers[index]; - } - - @Override - public void flush() { - assertCallbacks(); - - mInputEnded = false; - mOutputEnded = false; - cancelPendingTasks(); - mCodec.flush(); - mBufferPoller.schedulePolling(BufferPoller.MSG_POLL_INPUT_BUFFERS); - } - - private void cancelPendingTasks() { - mBufferPoller.cancel(); - mCallbackSender.cancel(); - } - - @Override - public void stop() { - assertCallbacks(); - - cancelPendingTasks(); - mCodec.stop(); - } - - @Override - public void release() { - assertCallbacks(); - - cancelPendingTasks(); - mCallbackSender = null; - mCodec.release(); - stopBufferPoller(); - } - - private void stopBufferPoller() { - if (mBufferPoller == null) { - Log.e(LOGTAG, "no initialized poller."); - return; - } - - mBufferPoller.getLooper().quit(); - mBufferPoller = null; - - if (DEBUG) Log.d(LOGTAG, "stop poller " + this); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/media/LocalMediaDrmBridge.java b/mobile/android/base/java/org/mozilla/gecko/media/LocalMediaDrmBridge.java deleted file mode 100644 index 2aad674b6a..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/media/LocalMediaDrmBridge.java +++ /dev/null @@ -1,162 +0,0 @@ -/* 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.media; -import org.mozilla.gecko.AppConstants; - -import android.media.MediaCrypto; -import android.util.Log; - -final class LocalMediaDrmBridge implements GeckoMediaDrm { - private static final String LOGTAG = "GeckoLocalMediaDrmBridge"; - private static final boolean DEBUG = false; - private GeckoMediaDrm mBridge = null; - private CallbacksForwarder mCallbacksFwd; - - // Forward the callback calls from GeckoMediaDrmBridgeV{21,23} - // to the callback MediaDrmProxy.Callbacks. - private class CallbacksForwarder implements GeckoMediaDrm.Callbacks { - private final GeckoMediaDrm.Callbacks mProxyCallbacks; - - CallbacksForwarder(GeckoMediaDrm.Callbacks callbacks) { - assertTrue(callbacks != null); - mProxyCallbacks = callbacks; - } - - @Override - public void onSessionCreated(int createSessionToken, - int promiseId, - byte[] sessionId, - byte[] request) { - assertTrue(mProxyCallbacks != null); - mProxyCallbacks.onSessionCreated(createSessionToken, - promiseId, - sessionId, - request); - } - - @Override - public void onSessionUpdated(int promiseId, byte[] sessionId) { - assertTrue(mProxyCallbacks != null); - mProxyCallbacks.onSessionUpdated(promiseId, sessionId); - } - - @Override - public void onSessionClosed(int promiseId, byte[] sessionId) { - assertTrue(mProxyCallbacks != null); - mProxyCallbacks.onSessionClosed(promiseId, sessionId); - } - - @Override - public void onSessionMessage(byte[] sessionId, - int sessionMessageType, - byte[] request) { - assertTrue(mProxyCallbacks != null); - mProxyCallbacks.onSessionMessage(sessionId, sessionMessageType, request); - } - - @Override - public void onSessionError(byte[] sessionId, - String message) { - assertTrue(mProxyCallbacks != null); - mProxyCallbacks.onSessionError(sessionId, message); - } - - @Override - public void onSessionBatchedKeyChanged(byte[] sessionId, - SessionKeyInfo[] keyInfos) { - assertTrue(mProxyCallbacks != null); - mProxyCallbacks.onSessionBatchedKeyChanged(sessionId, keyInfos); - } - - @Override - public void onRejectPromise(int promiseId, String message) { - if (DEBUG) Log.d(LOGTAG, message); - assertTrue(mProxyCallbacks != null); - mProxyCallbacks.onRejectPromise(promiseId, message); - } - } // CallbacksForwarder - - private static void assertTrue(boolean condition) { - if (DEBUG && !condition) { - throw new AssertionError("Expected condition to be true"); - } - } - - LocalMediaDrmBridge(String keySystem) throws Exception { - if (AppConstants.Versions.preLollipop) { - mBridge = null; - } else if (AppConstants.Versions.feature21Plus && - AppConstants.Versions.preMarshmallow) { - mBridge = new GeckoMediaDrmBridgeV21(keySystem); - } else { - mBridge = new GeckoMediaDrmBridgeV23(keySystem); - } - } - - @Override - public synchronized void setCallbacks(Callbacks callbacks) { - if (DEBUG) Log.d(LOGTAG, "setCallbacks()"); - mCallbacksFwd = new CallbacksForwarder(callbacks); - assertTrue(mBridge != null); - mBridge.setCallbacks(mCallbacksFwd); - } - - @Override - public synchronized void createSession(int createSessionToken, - int promiseId, - String initDataType, - byte[] initData) { - if (DEBUG) Log.d(LOGTAG, "createSession()"); - assertTrue(mCallbacksFwd != null); - try { - mBridge.createSession(createSessionToken, promiseId, initDataType, initData); - } catch (Exception e) { - Log.e(LOGTAG, "Failed to createSession.", e); - mCallbacksFwd.onRejectPromise(promiseId, "Failed to createSession."); - } - } - - @Override - public synchronized void updateSession(int promiseId, String sessionId, byte[] response) { - if (DEBUG) Log.d(LOGTAG, "updateSession()"); - assertTrue(mCallbacksFwd != null); - try { - mBridge.updateSession(promiseId, sessionId, response); - } catch (Exception e) { - Log.e(LOGTAG, "Failed to updateSession.", e); - mCallbacksFwd.onRejectPromise(promiseId, "Failed to updateSession."); - } - } - - @Override - public synchronized void closeSession(int promiseId, String sessionId) { - if (DEBUG) Log.d(LOGTAG, "closeSession()"); - assertTrue(mCallbacksFwd != null); - try { - mBridge.closeSession(promiseId, sessionId); - } catch (Exception e) { - Log.e(LOGTAG, "Failed to closeSession.", e); - mCallbacksFwd.onRejectPromise(promiseId, "Failed to closeSession."); - } - } - - @Override - public synchronized void release() { - if (DEBUG) Log.d(LOGTAG, "release()"); - try { - mBridge.release(); - mBridge = null; - mCallbacksFwd = null; - } catch (Exception e) { - Log.e(LOGTAG, "Failed to release", e); - } - } - - @Override - public synchronized MediaCrypto getMediaCrypto() { - if (DEBUG) Log.d(LOGTAG, "getMediaCrypto()"); - return mBridge != null ? mBridge.getMediaCrypto() : null; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/media/MediaControlService.java b/mobile/android/base/java/org/mozilla/gecko/media/MediaControlService.java deleted file mode 100644 index 2aa7830508..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/media/MediaControlService.java +++ /dev/null @@ -1,431 +0,0 @@ -package org.mozilla.gecko.media; - -import android.app.Notification; -import android.app.PendingIntent; -import android.app.Service; -import android.content.Intent; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.graphics.Canvas; -import android.graphics.Paint; -import android.graphics.Rect; -import android.media.session.MediaController; -import android.media.session.MediaSession; -import android.os.Build; -import android.os.Bundle; -import android.os.IBinder; -import android.support.v4.app.NotificationManagerCompat; -import android.util.Log; - -import org.mozilla.gecko.AppConstants; -import org.mozilla.gecko.BrowserApp; -import org.mozilla.gecko.GeckoApp; -import org.mozilla.gecko.GeckoAppShell; -import org.mozilla.gecko.PrefsHelper; -import org.mozilla.gecko.R; -import org.mozilla.gecko.Tab; -import org.mozilla.gecko.Tabs; -import org.mozilla.gecko.util.ThreadUtils; - -import java.lang.ref.WeakReference; - -public class MediaControlService extends Service implements Tabs.OnTabsChangedListener { - private static final String LOGTAG = "MediaControlService"; - - public static final String ACTION_INIT = "action_init"; - public static final String ACTION_RESUME = "action_resume"; - public static final String ACTION_PAUSE = "action_pause"; - public static final String ACTION_STOP = "action_stop"; - public static final String ACTION_RESUME_BY_AUDIO_FOCUS = "action_resume_audio_focus"; - public static final String ACTION_PAUSE_BY_AUDIO_FOCUS = "action_pause_audio_focus"; - - private static final int MEDIA_CONTROL_ID = 1; - private static final String MEDIA_CONTROL_PREF = "dom.audiochannel.mediaControl"; - - private String mActionState = ACTION_STOP; - - private MediaSession mSession; - private MediaController mController; - - private PrefsHelper.PrefHandler mPrefsObserver; - private final String[] mPrefs = { MEDIA_CONTROL_PREF }; - - private boolean mInitialize = false; - private boolean mIsMediaControlPrefOn = true; - - private static WeakReference mTabReference = new WeakReference<>(null); - - private int minCoverSize; - private int coverSize; - - @Override - public void onCreate() { - initialize(); - } - - @Override - public void onDestroy() { - shutdown(); - } - - @Override - public int onStartCommand(Intent intent, int flags, int startId) { - handleIntent(intent); - return START_NOT_STICKY; - } - - @Override - public IBinder onBind(Intent intent) { - return null; - } - - @Override - public boolean onUnbind(Intent intent) { - mSession.release(); - return super.onUnbind(intent); - } - - @Override - public void onTaskRemoved(Intent rootIntent) { - shutdown(); - } - - @Override - public void onTabChanged(Tab tab, Tabs.TabEvents msg, String data) { - if (!mInitialize) { - return; - } - - final Tab playingTab = mTabReference.get(); - switch (msg) { - case MEDIA_PLAYING_CHANGE: - // The 'MEDIA_PLAYING_CHANGE' would only be received when the - // media starts or ends. - if (playingTab != tab && tab.isMediaPlaying()) { - mTabReference = new WeakReference<>(tab); - notifyControlInterfaceChanged(ACTION_PAUSE); - } else if (playingTab == tab && !tab.isMediaPlaying()) { - notifyControlInterfaceChanged(ACTION_STOP); - mTabReference = new WeakReference<>(null); - } - break; - case MEDIA_PLAYING_RESUME: - // user resume the paused-by-control media from page so that we - // should make the control interface consistent. - if (playingTab == tab && !isMediaPlaying()) { - notifyControlInterfaceChanged(ACTION_PAUSE); - } - break; - case CLOSED: - if (playingTab == null || playingTab == tab) { - // Remove the controls when the playing tab disappeared or was closed. - notifyControlInterfaceChanged(ACTION_STOP); - } - break; - case FAVICON: - if (playingTab == tab) { - final String actionForPendingIntent = isMediaPlaying() ? - ACTION_PAUSE : ACTION_RESUME; - notifyControlInterfaceChanged(actionForPendingIntent); - } - break; - } - } - - private boolean isMediaPlaying() { - return mActionState.equals(ACTION_RESUME); - } - - private void initialize() { - if (mInitialize || - !isAndroidVersionLollopopOrHigher()) { - return; - } - - Log.d(LOGTAG, "initialize"); - getGeckoPreference(); - initMediaSession(); - - coverSize = (int) getResources().getDimension(R.dimen.notification_media_cover); - minCoverSize = getResources().getDimensionPixelSize(R.dimen.favicon_bg); - - Tabs.registerOnTabsChangedListener(this); - mInitialize = true; - } - - private void shutdown() { - if (!mInitialize) { - return; - } - - Log.d(LOGTAG, "shutdown"); - notifyControlInterfaceChanged(ACTION_STOP); - PrefsHelper.removeObserver(mPrefsObserver); - - Tabs.unregisterOnTabsChangedListener(this); - mInitialize = false; - stopSelf(); - } - - private boolean isAndroidVersionLollopopOrHigher() { - return Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP; - } - - private void handleIntent(Intent intent) { - if (intent == null || intent.getAction() == null || !mInitialize) { - return; - } - - Log.d(LOGTAG, "HandleIntent, action = " + intent.getAction() + ", actionState = " + mActionState); - switch (intent.getAction()) { - case ACTION_INIT : - // This action is used to create a service and do the initialization, - // the actual operation would be executed via control interface's - // pending intent. - break; - case ACTION_RESUME : - mController.getTransportControls().play(); - break; - case ACTION_PAUSE : - mController.getTransportControls().pause(); - break; - case ACTION_STOP : - mController.getTransportControls().stop(); - break; - case ACTION_PAUSE_BY_AUDIO_FOCUS : - mController.getTransportControls().sendCustomAction(ACTION_PAUSE_BY_AUDIO_FOCUS, null); - break; - case ACTION_RESUME_BY_AUDIO_FOCUS : - mController.getTransportControls().sendCustomAction(ACTION_RESUME_BY_AUDIO_FOCUS, null); - break; - } - } - - private void getGeckoPreference() { - mPrefsObserver = new PrefsHelper.PrefHandlerBase() { - @Override - public void prefValue(String pref, boolean value) { - if (pref.equals(MEDIA_CONTROL_PREF)) { - mIsMediaControlPrefOn = value; - - // If media is playing, we just need to create or remove - // the media control interface. - if (mActionState.equals(ACTION_RESUME)) { - notifyControlInterfaceChanged(mIsMediaControlPrefOn ? - ACTION_PAUSE : ACTION_STOP); - } - - // If turn off pref during pausing, except removing media - // interface, we also need to stop the service and notify - // gecko about that. - if (mActionState.equals(ACTION_PAUSE) && - !mIsMediaControlPrefOn) { - Intent intent = new Intent(getApplicationContext(), MediaControlService.class); - intent.setAction(ACTION_STOP); - handleIntent(intent); - } - } - } - }; - PrefsHelper.addObserver(mPrefs, mPrefsObserver); - } - - private void initMediaSession() { - // Android MediaSession is introduced since version L. - mSession = new MediaSession(getApplicationContext(), - "fennec media session"); - mController = new MediaController(getApplicationContext(), - mSession.getSessionToken()); - - mSession.setCallback(new MediaSession.Callback() { - @Override - public void onCustomAction(String action, Bundle extras) { - if (action.equals(ACTION_PAUSE_BY_AUDIO_FOCUS)) { - Log.d(LOGTAG, "Controller, pause by audio focus changed"); - notifyControlInterfaceChanged(ACTION_RESUME); - } else if (action.equals(ACTION_RESUME_BY_AUDIO_FOCUS)) { - Log.d(LOGTAG, "Controller, resume by audio focus changed"); - notifyControlInterfaceChanged(ACTION_PAUSE); - } - } - - @Override - public void onPlay() { - Log.d(LOGTAG, "Controller, onPlay"); - super.onPlay(); - notifyControlInterfaceChanged(ACTION_PAUSE); - notifyObservers("MediaControl", "resumeMedia"); - // To make sure we always own audio focus during playing. - AudioFocusAgent.notifyStartedPlaying(); - } - - @Override - public void onPause() { - Log.d(LOGTAG, "Controller, onPause"); - super.onPause(); - notifyControlInterfaceChanged(ACTION_RESUME); - notifyObservers("MediaControl", "mediaControlPaused"); - AudioFocusAgent.notifyStoppedPlaying(); - } - - @Override - public void onStop() { - Log.d(LOGTAG, "Controller, onStop"); - super.onStop(); - notifyControlInterfaceChanged(ACTION_STOP); - notifyObservers("MediaControl", "mediaControlStopped"); - mTabReference = new WeakReference<>(null); - } - }); - } - - private void notifyObservers(String topic, String data) { - GeckoAppShell.notifyObservers(topic, data); - } - - private boolean isNeedToRemoveControlInterface(String action) { - return action.equals(ACTION_STOP); - } - - private void notifyControlInterfaceChanged(final String uiAction) { - if (!mInitialize) { - return; - } - - Log.d(LOGTAG, "notifyControlInterfaceChanged, action = " + uiAction); - - if (isNeedToRemoveControlInterface(uiAction)) { - stopForeground(false); - NotificationManagerCompat.from(this).cancel(MEDIA_CONTROL_ID); - setActionState(uiAction); - return; - } - - if (!mIsMediaControlPrefOn) { - return; - } - - final Tab tab = mTabReference.get(); - - if (tab == null) { - return; - } - - setActionState(uiAction); - - ThreadUtils.postToBackgroundThread(new Runnable() { - @Override - public void run() { - updateNotification(tab, uiAction); - } - }); - } - - private void setActionState(final String uiAction) { - switch (uiAction) { - case ACTION_PAUSE: - mActionState = ACTION_RESUME; - break; - case ACTION_RESUME: - mActionState = ACTION_PAUSE; - break; - case ACTION_STOP: - mActionState = ACTION_STOP; - break; - } - } - - private void updateNotification(Tab tab, String action) { - ThreadUtils.assertNotOnUiThread(); - - final Notification.MediaStyle style = new Notification.MediaStyle(); - style.setShowActionsInCompactView(0); - - final boolean isPlaying = isMediaPlaying(); - final int visibility = tab.isPrivate() ? - Notification.VISIBILITY_PRIVATE : Notification.VISIBILITY_PUBLIC; - - final Notification notification = new Notification.Builder(this) - .setSmallIcon(R.drawable.flat_icon) - .setLargeIcon(generateCoverArt(tab)) - .setContentTitle(tab.getTitle()) - .setContentText(tab.getURL()) - .setContentIntent(createContentIntent(tab.getId())) - .setDeleteIntent(createDeleteIntent()) - .setStyle(style) - .addAction(createNotificationAction(action)) - .setOngoing(isPlaying) - .setShowWhen(false) - .setWhen(0) - .setVisibility(visibility) - .build(); - - if (isPlaying) { - startForeground(MEDIA_CONTROL_ID, notification); - } else { - stopForeground(false); - NotificationManagerCompat.from(this) - .notify(MEDIA_CONTROL_ID, notification); - } - } - - private Notification.Action createNotificationAction(String action) { - boolean isPlayAction = action.equals(ACTION_RESUME); - - int icon = isPlayAction ? R.drawable.ic_media_play : R.drawable.ic_media_pause; - String title = getString(isPlayAction ? R.string.media_play : R.string.media_pause); - - final Intent intent = new Intent(getApplicationContext(), MediaControlService.class); - intent.setAction(action); - final PendingIntent pendingIntent = PendingIntent.getService(getApplicationContext(), 1, intent, 0); - - //noinspection deprecation - The new constructor is only for API > 23 - return new Notification.Action.Builder(icon, title, pendingIntent).build(); - } - - private PendingIntent createContentIntent(int tabId) { - Intent intent = new Intent(getApplicationContext(), BrowserApp.class); - intent.setAction(GeckoApp.ACTION_SWITCH_TAB); - intent.putExtra("TabId", tabId); - return PendingIntent.getActivity(getApplicationContext(), 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); - } - - private PendingIntent createDeleteIntent() { - Intent intent = new Intent(getApplicationContext(), MediaControlService.class); - intent.setAction(ACTION_STOP); - return PendingIntent.getService(getApplicationContext(), 1, intent, 0); - } - - private Bitmap generateCoverArt(Tab tab) { - final Bitmap favicon = tab.getFavicon(); - - // If we do not have a favicon or if it's smaller than 72 pixels then just use the default icon. - if (favicon == null || favicon.getWidth() < minCoverSize || favicon.getHeight() < minCoverSize) { - // Use the launcher icon as fallback - return BitmapFactory.decodeResource(getResources(), R.drawable.notification_media); - } - - // Favicon should at least have half of the size of the cover - int width = Math.max(favicon.getWidth(), coverSize / 2); - int height = Math.max(favicon.getHeight(), coverSize / 2); - - final Bitmap coverArt = Bitmap.createBitmap(coverSize, coverSize, Bitmap.Config.ARGB_8888); - final Canvas canvas = new Canvas(coverArt); - canvas.drawColor(0xFF777777); - - int left = Math.max(0, (coverArt.getWidth() / 2) - (width / 2)); - int right = Math.min(coverSize, left + width); - int top = Math.max(0, (coverArt.getHeight() / 2) - (height / 2)); - int bottom = Math.min(coverSize, top + height); - - final Paint paint = new Paint(); - paint.setAntiAlias(true); - - canvas.drawBitmap(favicon, - new Rect(0, 0, favicon.getWidth(), favicon.getHeight()), - new Rect(left, top, right, bottom), - paint); - - return coverArt; - } -} \ No newline at end of file diff --git a/mobile/android/base/java/org/mozilla/gecko/media/MediaDrmProxy.java b/mobile/android/base/java/org/mozilla/gecko/media/MediaDrmProxy.java deleted file mode 100644 index faca2389ee..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/media/MediaDrmProxy.java +++ /dev/null @@ -1,307 +0,0 @@ -/* 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.media; - -import java.util.ArrayList; -import java.util.UUID; - -import org.mozilla.gecko.mozglue.JNIObject; -import org.mozilla.gecko.annotation.WrapForJNI; -import org.mozilla.gecko.AppConstants; - -import android.media.MediaCodecInfo; -import android.media.MediaCodecList; -import android.media.MediaCrypto; -import android.media.MediaDrm; -import android.util.Log; -import android.os.Build; - -public final class MediaDrmProxy { - private static final String LOGTAG = "GeckoMediaDrmProxy"; - private static final boolean DEBUG = false; - private static final UUID WIDEVINE_SCHEME_UUID = - new UUID(0xedef8ba979d64aceL, 0xa3c827dcd51d21edL); - - private static final String WIDEVINE_KEY_SYSTEM = "com.widevine.alpha"; - @WrapForJNI - private static final String AAC = "audio/mp4a-latm"; - @WrapForJNI - private static final String AVC = "video/avc"; - @WrapForJNI - private static final String VORBIS = "audio/vorbis"; - @WrapForJNI - private static final String VP8 = "video/x-vnd.on2.vp8"; - @WrapForJNI - private static final String VP9 = "video/x-vnd.on2.vp9"; - @WrapForJNI - private static final String OPUS = "audio/opus"; - - // A flag to avoid using the native object that has been destroyed. - private boolean mDestroyed; - private GeckoMediaDrm mImpl; - public static ArrayList mProxyList = new ArrayList(); - - private static boolean isSystemSupported() { - // Support versions >= LOLLIPOP - if (AppConstants.Versions.preLollipop) { - if (DEBUG) Log.d(LOGTAG, "System Not supported !!, current SDK version is " + Build.VERSION.SDK_INT); - return false; - } - return true; - } - - @WrapForJNI - public static boolean isSchemeSupported(String keySystem) { - if (!isSystemSupported()) { - return false; - } - if (keySystem.equals(WIDEVINE_KEY_SYSTEM)) { - return MediaDrm.isCryptoSchemeSupported(WIDEVINE_SCHEME_UUID) - && MediaCrypto.isCryptoSchemeSupported(WIDEVINE_SCHEME_UUID); - } - if (DEBUG) Log.d(LOGTAG, "isSchemeSupported key sytem = " + keySystem); - return false; - } - - @WrapForJNI - public static boolean IsCryptoSchemeSupported(String keySystem, - String container) { - if (!isSystemSupported()) { - return false; - } - if (keySystem.equals(WIDEVINE_KEY_SYSTEM)) { - return MediaDrm.isCryptoSchemeSupported(WIDEVINE_SCHEME_UUID, container); - } - if (DEBUG) Log.d(LOGTAG, "cannot decrypt key sytem = " + keySystem + ", container = " + container); - return false; - } - - @WrapForJNI - public static boolean CanDecode(String mimeType) { - for (int i = 0; i < MediaCodecList.getCodecCount(); ++i) { - MediaCodecInfo info = MediaCodecList.getCodecInfoAt(i); - if (info.isEncoder()) { - continue; - } - for (String m : info.getSupportedTypes()) { - if (m.equals(mimeType)) { - return true; - } - } - } - if (DEBUG) Log.d(LOGTAG, "cannot decode mimetype = " + mimeType); - return false; - } - - // Interface for callback to native. - public interface Callbacks { - void onSessionCreated(int createSessionToken, - int promiseId, - byte[] sessionId, - byte[] request); - - void onSessionUpdated(int promiseId, byte[] sessionId); - - void onSessionClosed(int promiseId, byte[] sessionId); - - void onSessionMessage(byte[] sessionId, - int sessionMessageType, - byte[] request); - - void onSessionError(byte[] sessionId, - String message); - - // MediaDrm.KeyStatus is available in API level 23(M) - // https://developer.android.com/reference/android/media/MediaDrm.KeyStatus.html - // For compatibility between L and M above, we'll unwrap the KeyStatus structure - // and store the keyid and status into SessionKeyInfo and pass to native(MediaDrmCDMProxy). - void onSessionBatchedKeyChanged(byte[] sessionId, - SessionKeyInfo[] keyInfos); - - void onRejectPromise(int promiseId, - String message); - } // Callbacks - - public static class NativeMediaDrmProxyCallbacks extends JNIObject implements Callbacks { - @WrapForJNI(calledFrom = "gecko") - NativeMediaDrmProxyCallbacks() {} - - @Override - @WrapForJNI(dispatchTo = "gecko") - public native void onSessionCreated(int createSessionToken, - int promiseId, - byte[] sessionId, - byte[] request); - - @Override - @WrapForJNI(dispatchTo = "gecko") - public native void onSessionUpdated(int promiseId, byte[] sessionId); - - @Override - @WrapForJNI(dispatchTo = "gecko") - public native void onSessionClosed(int promiseId, byte[] sessionId); - - @Override - @WrapForJNI(dispatchTo = "gecko") - public native void onSessionMessage(byte[] sessionId, - int sessionMessageType, - byte[] request); - - @Override - @WrapForJNI(dispatchTo = "gecko") - public native void onSessionError(byte[] sessionId, - String message); - - @Override - @WrapForJNI(dispatchTo = "gecko") - public native void onSessionBatchedKeyChanged(byte[] sessionId, - SessionKeyInfo[] keyInfos); - - @Override - @WrapForJNI(dispatchTo = "gecko") - public native void onRejectPromise(int promiseId, - String message); - - @Override // JNIObject - protected void disposeNative() { - throw new UnsupportedOperationException(); - } - } // NativeMediaDrmProxyCallbacks - - // A proxy to callback from LocalMediaDrmBridge to native instance. - public static class MediaDrmProxyCallbacks implements GeckoMediaDrm.Callbacks { - private final Callbacks mNativeCallbacks; - private final MediaDrmProxy mProxy; - - public MediaDrmProxyCallbacks(MediaDrmProxy proxy, Callbacks callbacks) { - mNativeCallbacks = callbacks; - mProxy = proxy; - } - - @Override - public void onSessionCreated(int createSessionToken, - int promiseId, - byte[] sessionId, - byte[] request) { - if (!mProxy.isDestroyed()) { - mNativeCallbacks.onSessionCreated(createSessionToken, - promiseId, - sessionId, - request); - } - } - - @Override - public void onSessionUpdated(int promiseId, byte[] sessionId) { - if (!mProxy.isDestroyed()) { - mNativeCallbacks.onSessionUpdated(promiseId, sessionId); - } - } - - @Override - public void onSessionClosed(int promiseId, byte[] sessionId) { - if (!mProxy.isDestroyed()) { - mNativeCallbacks.onSessionClosed(promiseId, sessionId); - } - } - - @Override - public void onSessionMessage(byte[] sessionId, - int sessionMessageType, - byte[] request) { - if (!mProxy.isDestroyed()) { - mNativeCallbacks.onSessionMessage(sessionId, sessionMessageType, request); - } - } - - @Override - public void onSessionError(byte[] sessionId, - String message) { - if (!mProxy.isDestroyed()) { - mNativeCallbacks.onSessionError(sessionId, message); - } - } - - @Override - public void onSessionBatchedKeyChanged(byte[] sessionId, - SessionKeyInfo[] keyInfos) { - if (!mProxy.isDestroyed()) { - mNativeCallbacks.onSessionBatchedKeyChanged(sessionId, keyInfos); - } - } - - @Override - public void onRejectPromise(int promiseId, - String message) { - if (!mProxy.isDestroyed()) { - mNativeCallbacks.onRejectPromise(promiseId, message); - } - } - } // MediaDrmProxyCallbacks - - public boolean isDestroyed() { - return mDestroyed; - } - - @WrapForJNI(calledFrom = "gecko") - public static MediaDrmProxy create(String keySystem, - Callbacks nativeCallbacks, - boolean isRemote) { - // TODO: Will implement {Local,Remote}MediaDrmBridge instantiation by - // '''isRemote''' flag in Bug 1307818. - MediaDrmProxy proxy = new MediaDrmProxy(keySystem, nativeCallbacks); - return proxy; - } - - MediaDrmProxy(String keySystem, Callbacks nativeCallbacks) { - if (DEBUG) Log.d(LOGTAG, "Constructing MediaDrmProxy"); - // TODO: Bug 1306185 will implement the LocalMediaDrmBridge as an impl - // of GeckoMediaDrm for in-process decoding mode. - //mImpl = new LocalMediaDrmBridge(keySystem); - mImpl.setCallbacks(new MediaDrmProxyCallbacks(this, nativeCallbacks)); - mProxyList.add(this); - } - - @WrapForJNI - private void createSession(int createSessionToken, - int promiseId, - String initDataType, - byte[] initData) { - if (DEBUG) Log.d(LOGTAG, "createSession, promiseId = " + promiseId); - mImpl.createSession(createSessionToken, - promiseId, - initDataType, - initData); - } - - @WrapForJNI - private void updateSession(int promiseId, String sessionId, byte[] response) { - if (DEBUG) Log.d(LOGTAG, "updateSession, primiseId(" + promiseId + "sessionId(" + sessionId + ")"); - mImpl.updateSession(promiseId, sessionId, response); - } - - @WrapForJNI - private void closeSession(int promiseId, String sessionId) { - if (DEBUG) Log.d(LOGTAG, "closeSession, primiseId(" + promiseId + "sessionId(" + sessionId + ")"); - mImpl.closeSession(promiseId, sessionId); - } - - @WrapForJNI // Called when natvie object is destroyed. - private void destroy() { - if (DEBUG) Log.d(LOGTAG, "destroy!! Native object is destroyed."); - if (mDestroyed) { - return; - } - mDestroyed = true; - release(); - } - - private void release() { - if (DEBUG) Log.d(LOGTAG, "release"); - mProxyList.remove(this); - mImpl.release(); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/media/MediaManager.java b/mobile/android/base/java/org/mozilla/gecko/media/MediaManager.java deleted file mode 100644 index fcb0fc6594..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/media/MediaManager.java +++ /dev/null @@ -1,44 +0,0 @@ -/* 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.media; - -import android.app.Service; -import android.content.Intent; -import android.os.Binder; -import android.os.IBinder; -import android.os.RemoteException; - -import org.mozilla.gecko.mozglue.GeckoLoader; - -public final class MediaManager extends Service { - private static boolean sNativeLibLoaded; - - private Binder mBinder = new IMediaManager.Stub() { - @Override - public ICodec createCodec() throws RemoteException { - return new Codec(); - } - - @Override - public IMediaDrmBridge createRemoteMediaDrmBridge(String keySystem, - String stubId) - throws RemoteException { - return new RemoteMediaDrmBridgeStub(keySystem, stubId); - } - }; - - @Override - public synchronized void onCreate() { - if (!sNativeLibLoaded) { - GeckoLoader.doLoadLibrary(this, "mozglue"); - sNativeLibLoaded = true; - } - } - - @Override - public IBinder onBind(Intent intent) { - return mBinder; - } -} \ No newline at end of file diff --git a/mobile/android/base/java/org/mozilla/gecko/media/RemoteManager.java b/mobile/android/base/java/org/mozilla/gecko/media/RemoteManager.java deleted file mode 100644 index 260ca73c14..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/media/RemoteManager.java +++ /dev/null @@ -1,224 +0,0 @@ -/* 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.media; - -import org.mozilla.gecko.GeckoAppShell; -import org.mozilla.gecko.Telemetry; - -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.content.ServiceConnection; -import android.media.MediaFormat; -import android.os.DeadObjectException; -import android.os.IBinder; -import android.os.RemoteException; -import android.view.Surface; -import android.util.Log; - -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; -import java.util.LinkedList; -import java.util.List; - -public final class RemoteManager implements IBinder.DeathRecipient { - private static final String LOGTAG = "GeckoRemoteManager"; - private static final boolean DEBUG = false; - private static RemoteManager sRemoteManager = null; - - public synchronized static RemoteManager getInstance() { - if (sRemoteManager == null) { - sRemoteManager = new RemoteManager(); - } - - sRemoteManager.init(); - return sRemoteManager; - } - - private List mProxies = new LinkedList(); - private volatile IMediaManager mRemote; - private volatile CountDownLatch mConnectionLatch; - private final ServiceConnection mConnection = new ServiceConnection() { - @Override - public void onServiceConnected(ComponentName name, IBinder service) { - if (DEBUG) Log.d(LOGTAG, "service connected"); - try { - service.linkToDeath(RemoteManager.this, 0); - } catch (RemoteException e) { - e.printStackTrace(); - } - mRemote = IMediaManager.Stub.asInterface(service); - if (mConnectionLatch != null) { - mConnectionLatch.countDown(); - } - } - - /** - * Called when a connection to the Service has been lost. This typically - * happens when the process hosting the service has crashed or been killed. - * This does not remove the ServiceConnection itself -- this - * binding to the service will remain active, and you will receive a call - * to {@link #onServiceConnected} when the Service is next running. - * - * @param name The concrete component name of the service whose - * connection has been lost. - */ - @Override - public void onServiceDisconnected(ComponentName name) { - if (DEBUG) Log.d(LOGTAG, "service disconnected"); - mRemote.asBinder().unlinkToDeath(RemoteManager.this, 0); - mRemote = null; - if (mConnectionLatch != null) { - mConnectionLatch.countDown(); - } - } - }; - - private synchronized boolean init() { - if (mRemote != null) { - return true; - } - - if (DEBUG) Log.d(LOGTAG, "init remote manager " + this); - Context appCtxt = GeckoAppShell.getApplicationContext(); - if (DEBUG) Log.d(LOGTAG, "ctxt=" + appCtxt); - appCtxt.bindService(new Intent(appCtxt, MediaManager.class), - mConnection, Context.BIND_AUTO_CREATE); - if (!waitConnection()) { - appCtxt.unbindService(mConnection); - return false; - } - return true; - } - - private boolean waitConnection() { - boolean ok = false; - - mConnectionLatch = new CountDownLatch(1); - try { - int retryCount = 0; - while (retryCount < 5) { - if (DEBUG) Log.d(LOGTAG, "waiting for connection latch:" + mConnectionLatch); - mConnectionLatch.await(1, TimeUnit.SECONDS); - if (mConnectionLatch.getCount() == 0) { - break; - } - Log.w(LOGTAG, "Creator not connected in 1s. Try again."); - retryCount++; - } - ok = true; - } catch (InterruptedException e) { - Log.e(LOGTAG, "service not connected in 5 seconds. Stop waiting."); - e.printStackTrace(); - } - mConnectionLatch = null; - - return ok; - } - - public synchronized CodecProxy createCodec(MediaFormat format, - Surface surface, - CodecProxy.Callbacks callbacks) { - if (mRemote == null) { - if (DEBUG) Log.d(LOGTAG, "createCodec failed due to not initialize"); - return null; - } - try { - ICodec remote = mRemote.createCodec(); - CodecProxy proxy = CodecProxy.createCodecProxy(format, surface, callbacks); - if (proxy.init(remote)) { - mProxies.add(proxy); - return proxy; - } else { - return null; - } - } catch (RemoteException e) { - e.printStackTrace(); - return null; - } - } - - private static final String MEDIA_DECODING_PROCESS_CRASH = "MEDIA_DECODING_PROCESS_CRASH"; - private void reportDecodingProcessCrash() { - Telemetry.addToHistogram(MEDIA_DECODING_PROCESS_CRASH, 1); - } - - public synchronized IMediaDrmBridge createRemoteMediaDrmBridge(String keySystem, - String stubId) { - if (mRemote == null) { - if (DEBUG) Log.d(LOGTAG, "createRemoteMediaDrmBridge failed due to not initialize"); - return null; - } - try { - IMediaDrmBridge remoteBridge = - mRemote.createRemoteMediaDrmBridge(keySystem, stubId); - return remoteBridge; - } catch (RemoteException e) { - Log.e(LOGTAG, "Got exception during createRemoteMediaDrmBridge().", e); - return null; - } - } - - @Override - public void binderDied() { - Log.e(LOGTAG, "remote codec is dead"); - reportDecodingProcessCrash(); - handleRemoteDeath(); - } - - private synchronized void handleRemoteDeath() { - // Wait for onServiceDisconnected() - if (!waitConnection()) { - notifyError(true); - return; - } - // Restart - if (init() && recoverRemoteCodec()) { - notifyError(false); - } else { - notifyError(true); - } - } - - private synchronized void notifyError(boolean fatal) { - for (CodecProxy proxy : mProxies) { - proxy.reportError(fatal); - } - } - - private synchronized boolean recoverRemoteCodec() { - if (DEBUG) Log.d(LOGTAG, "recover codec"); - boolean ok = true; - try { - for (CodecProxy proxy : mProxies) { - ok &= proxy.init(mRemote.createCodec()); - } - return ok; - } catch (RemoteException e) { - return false; - } - } - - public void releaseCodec(CodecProxy proxy) throws DeadObjectException, RemoteException { - if (mRemote == null) { - if (DEBUG) Log.d(LOGTAG, "releaseCodec called but not initialized yet"); - return; - } - proxy.deinit(); - synchronized (this) { - if (mProxies.remove(proxy) && mProxies.isEmpty()) { - release(); - } - } - } - - private void release() { - if (DEBUG) Log.d(LOGTAG, "release remote manager " + this); - Context appCtxt = GeckoAppShell.getApplicationContext(); - mRemote.asBinder().unlinkToDeath(this, 0); - mRemote = null; - appCtxt.unbindService(mConnection); - } -} // RemoteManager \ No newline at end of file diff --git a/mobile/android/base/java/org/mozilla/gecko/media/RemoteMediaDrmBridge.java b/mobile/android/base/java/org/mozilla/gecko/media/RemoteMediaDrmBridge.java deleted file mode 100644 index d65bb7872f..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/media/RemoteMediaDrmBridge.java +++ /dev/null @@ -1,152 +0,0 @@ -/* 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.media; - -import android.media.MediaCrypto; -import android.util.Log; - -final class RemoteMediaDrmBridge implements GeckoMediaDrm { - private static final String LOGTAG = "GeckoRemoteMediaDrmBridge"; - private static final boolean DEBUG = false; - private CallbacksForwarder mCallbacksFwd; - private IMediaDrmBridge mRemote; - - // Forward callbacks from remote bridge stub to MediaDrmProxy. - private static class CallbacksForwarder extends IMediaDrmBridgeCallbacks.Stub { - private final GeckoMediaDrm.Callbacks mProxyCallbacks; - CallbacksForwarder(Callbacks callbacks) { - assertTrue(callbacks != null); - mProxyCallbacks = callbacks; - } - - @Override - public void onSessionCreated(int createSessionToken, - int promiseId, - byte[] sessionId, - byte[] request) { - mProxyCallbacks.onSessionCreated(createSessionToken, - promiseId, - sessionId, - request); - } - - @Override - public void onSessionUpdated(int promiseId, byte[] sessionId) { - mProxyCallbacks.onSessionUpdated(promiseId, sessionId); - } - - @Override - public void onSessionClosed(int promiseId, byte[] sessionId) { - mProxyCallbacks.onSessionClosed(promiseId, sessionId); - } - - @Override - public void onSessionMessage(byte[] sessionId, - int sessionMessageType, - byte[] request) { - mProxyCallbacks.onSessionMessage(sessionId, sessionMessageType, request); - } - - @Override - public void onSessionError(byte[] sessionId, String message) { - mProxyCallbacks.onSessionError(sessionId, message); - } - - @Override - public void onSessionBatchedKeyChanged(byte[] sessionId, - SessionKeyInfo[] keyInfos) { - mProxyCallbacks.onSessionBatchedKeyChanged(sessionId, keyInfos); - } - - @Override - public void onRejectPromise(int promiseId, String message) { - mProxyCallbacks.onRejectPromise(promiseId, message); - } - } // CallbacksForwarder - - /* package-private */ static void assertTrue(boolean condition) { - if (DEBUG && !condition) { - throw new AssertionError("Expected condition to be true"); - } - } - - public RemoteMediaDrmBridge(IMediaDrmBridge remoteBridge) { - assertTrue(remoteBridge != null); - mRemote = remoteBridge; - } - - @Override - public synchronized void setCallbacks(Callbacks callbacks) { - if (DEBUG) Log.d(LOGTAG, "setCallbacks()"); - assertTrue(callbacks != null); - assertTrue(mRemote != null); - - mCallbacksFwd = new CallbacksForwarder(callbacks); - try { - mRemote.setCallbacks(mCallbacksFwd); - } catch (Exception e) { - Log.e(LOGTAG, "Got exception during setCallbacks", e); - } - } - - @Override - public synchronized void createSession(int createSessionToken, - int promiseId, - String initDataType, - byte[] initData) { - if (DEBUG) Log.d(LOGTAG, "createSession()"); - - try { - mRemote.createSession(createSessionToken, promiseId, initDataType, initData); - } catch (Exception e) { - Log.e(LOGTAG, "Got exception while creating remote session.", e); - mCallbacksFwd.onRejectPromise(promiseId, "Failed to create session."); - } - } - - @Override - public synchronized void updateSession(int promiseId, String sessionId, byte[] response) { - if (DEBUG) Log.d(LOGTAG, "updateSession()"); - - try { - mRemote.updateSession(promiseId, sessionId, response); - } catch (Exception e) { - Log.e(LOGTAG, "Got exception while updating remote session.", e); - mCallbacksFwd.onRejectPromise(promiseId, "Failed to update session."); - } - } - - @Override - public synchronized void closeSession(int promiseId, String sessionId) { - if (DEBUG) Log.d(LOGTAG, "closeSession()"); - - try { - mRemote.closeSession(promiseId, sessionId); - } catch (Exception e) { - Log.e(LOGTAG, "Got exception while closing remote session.", e); - mCallbacksFwd.onRejectPromise(promiseId, "Failed to close session."); - } - } - - @Override - public synchronized void release() { - if (DEBUG) Log.d(LOGTAG, "release()"); - - try { - mRemote.release(); - } catch (Exception e) { - Log.e(LOGTAG, "Got exception while releasing RemoteDrmBridge.", e); - } - mRemote = null; - mCallbacksFwd = null; - } - - @Override - public synchronized MediaCrypto getMediaCrypto() { - if (DEBUG) Log.d(LOGTAG, "getMediaCrypto(), should not enter here!"); - assertTrue(false); - return null; - } -} \ No newline at end of file diff --git a/mobile/android/base/java/org/mozilla/gecko/media/RemoteMediaDrmBridgeStub.java b/mobile/android/base/java/org/mozilla/gecko/media/RemoteMediaDrmBridgeStub.java deleted file mode 100644 index 8aed0f8515..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/media/RemoteMediaDrmBridgeStub.java +++ /dev/null @@ -1,247 +0,0 @@ -/* 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.media; -import org.mozilla.gecko.AppConstants; - -import java.util.ArrayList; - -import android.media.MediaCrypto; -import android.os.IBinder; -import android.os.RemoteException; -import android.util.Log; - -final class RemoteMediaDrmBridgeStub extends IMediaDrmBridge.Stub implements IBinder.DeathRecipient { - private static final String LOGTAG = "GeckoRemoteMediaDrmBridgeStub"; - private static final boolean DEBUG = false; - private volatile IMediaDrmBridgeCallbacks mCallbacks = null; - - // Underlying bridge implmenetaion, i.e. GeckoMediaDrmBrdigeV21. - private GeckoMediaDrm mBridge = null; - - // mStubId is initialized during stub construction. It should be a unique - // string which is generated in MediaDrmProxy in Fennec App process and is - // used for Codec to obtain corresponding MediaCrypto as input to achieve - // decryption. - // The generated stubId will be delivered to Codec via a code path starting - // from MediaDrmProxy -> MediaDrmCDMProxy -> RemoteDataDecoder => IPC => Codec. - private String mStubId = ""; - - public static ArrayList mBridgeStubs = - new ArrayList(); - - private String getId() { - return mStubId; - } - - private MediaCrypto getMediaCryptoFromBridge() { - return mBridge != null ? mBridge.getMediaCrypto() : null; - } - - public static synchronized MediaCrypto getMediaCrypto(String stubId) { - if (DEBUG) Log.d(LOGTAG, "getMediaCrypto()"); - - for (int i = 0; i < mBridgeStubs.size(); i++) { - if (mBridgeStubs.get(i) != null && - mBridgeStubs.get(i).getId().equals(stubId)) { - return mBridgeStubs.get(i).getMediaCryptoFromBridge(); - } - } - return null; - } - - // Callback to RemoteMediaDrmBridge. - private final class Callbacks implements GeckoMediaDrm.Callbacks { - private IMediaDrmBridgeCallbacks mRemoteCallbacks; - - public Callbacks(IMediaDrmBridgeCallbacks remote) { - mRemoteCallbacks = remote; - } - - @Override - public void onSessionCreated(int createSessionToken, - int promiseId, - byte[] sessionId, - byte[] request) { - if (DEBUG) Log.d(LOGTAG, "onSessionCreated()"); - try { - mRemoteCallbacks.onSessionCreated(createSessionToken, - promiseId, - sessionId, - request); - } catch (RemoteException e) { - Log.e(LOGTAG, "Exception ! Dead recipient !!", e); - } - } - - @Override - public void onSessionUpdated(int promiseId, byte[] sessionId) { - if (DEBUG) Log.d(LOGTAG, "onSessionUpdated()"); - try { - mRemoteCallbacks.onSessionUpdated(promiseId, sessionId); - } catch (RemoteException e) { - Log.e(LOGTAG, "Exception ! Dead recipient !!", e); - } - } - - @Override - public void onSessionClosed(int promiseId, byte[] sessionId) { - if (DEBUG) Log.d(LOGTAG, "onSessionClosed()"); - try { - mRemoteCallbacks.onSessionClosed(promiseId, sessionId); - } catch (RemoteException e) { - Log.e(LOGTAG, "Exception ! Dead recipient !!", e); - } - } - - @Override - public void onSessionMessage(byte[] sessionId, - int sessionMessageType, - byte[] request) { - if (DEBUG) Log.d(LOGTAG, "onSessionMessage()"); - try { - mRemoteCallbacks.onSessionMessage(sessionId, sessionMessageType, request); - } catch (RemoteException e) { - Log.e(LOGTAG, "Exception ! Dead recipient !!", e); - } - } - - @Override - public void onSessionError(byte[] sessionId, String message) { - if (DEBUG) Log.d(LOGTAG, "onSessionError()"); - try { - mRemoteCallbacks.onSessionError(sessionId, message); - } catch (RemoteException e) { - Log.e(LOGTAG, "Exception ! Dead recipient !!", e); - } - } - - @Override - public void onSessionBatchedKeyChanged(byte[] sessionId, - SessionKeyInfo[] keyInfos) { - if (DEBUG) Log.d(LOGTAG, "onSessionBatchedKeyChanged()"); - try { - mRemoteCallbacks.onSessionBatchedKeyChanged(sessionId, keyInfos); - } catch (RemoteException e) { - Log.e(LOGTAG, "Exception ! Dead recipient !!", e); - } - } - - @Override - public void onRejectPromise(int promiseId, String message) { - if (DEBUG) Log.d(LOGTAG, "onRejectPromise()"); - try { - mRemoteCallbacks.onRejectPromise(promiseId, message); - } catch (RemoteException e) { - Log.e(LOGTAG, "Exception ! Dead recipient !!", e); - } - } - } - - /* package-private */ void assertTrue(boolean condition) { - if (DEBUG && !condition) { - throw new AssertionError("Expected condition to be true"); - } - } - - RemoteMediaDrmBridgeStub(String keySystem, String stubId) throws RemoteException { - if (AppConstants.Versions.preLollipop) { - Log.e(LOGTAG, "Pre-Lollipop should never enter here!!"); - throw new RemoteException("Error, unsupported version!"); - } - try { - if (AppConstants.Versions.feature21Plus && - AppConstants.Versions.preMarshmallow) { - mBridge = new GeckoMediaDrmBridgeV21(keySystem); - } else { - mBridge = new GeckoMediaDrmBridgeV23(keySystem); - } - mStubId = stubId; - mBridgeStubs.add(this); - } catch (Exception e) { - throw new RemoteException("RemoteMediaDrmBridgeStub cannot create bridge implementation."); - } - } - - @Override - public synchronized void setCallbacks(IMediaDrmBridgeCallbacks callbacks) throws RemoteException { - if (DEBUG) Log.d(LOGTAG, "setCallbacks()"); - assertTrue(mBridge != null); - assertTrue(callbacks != null); - mCallbacks = callbacks; - callbacks.asBinder().linkToDeath(this, 0); - mBridge.setCallbacks(new Callbacks(mCallbacks)); - } - - @Override - public synchronized void createSession(int createSessionToken, - int promiseId, - String initDataType, - byte[] initData) throws RemoteException { - if (DEBUG) Log.d(LOGTAG, "createSession()"); - try { - assertTrue(mCallbacks != null); - assertTrue(mBridge != null); - mBridge.createSession(createSessionToken, - promiseId, - initDataType, - initData); - } catch (Exception e) { - Log.e(LOGTAG, "Failed to createSession.", e); - mCallbacks.onRejectPromise(promiseId, "Failed to createSession."); - } - } - - @Override - public synchronized void updateSession(int promiseId, - String sessionId, - byte[] response) throws RemoteException { - if (DEBUG) Log.d(LOGTAG, "updateSession()"); - try { - assertTrue(mCallbacks != null); - assertTrue(mBridge != null); - mBridge.updateSession(promiseId, sessionId, response); - } catch (Exception e) { - Log.e(LOGTAG, "Failed to updateSession.", e); - mCallbacks.onRejectPromise(promiseId, "Failed to updateSession."); - } - } - - @Override - public synchronized void closeSession(int promiseId, String sessionId) throws RemoteException { - if (DEBUG) Log.d(LOGTAG, "closeSession()"); - try { - assertTrue(mCallbacks != null); - assertTrue(mBridge != null); - mBridge.closeSession(promiseId, sessionId); - } catch (Exception e) { - Log.e(LOGTAG, "Failed to closeSession.", e); - mCallbacks.onRejectPromise(promiseId, "Failed to closeSession."); - } - } - - // IBinder.DeathRecipient - @Override - public synchronized void binderDied() { - Log.e(LOGTAG, "Binder died !!"); - try { - release(); - } catch (Exception e) { - Log.e(LOGTAG, "Exception ! Dead recipient !!", e); - } - } - - @Override - public synchronized void release() { - if (DEBUG) Log.d(LOGTAG, "release()"); - mBridgeStubs.remove(this); - if (mBridge != null) { - mBridge.release(); - mBridge = null; - } - mCallbacks.asBinder().unlinkToDeath(this, 0); - mCallbacks = null; - mStubId = ""; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/media/Sample.java b/mobile/android/base/java/org/mozilla/gecko/media/Sample.java deleted file mode 100644 index b7a98da8ab..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/media/Sample.java +++ /dev/null @@ -1,264 +0,0 @@ -/* 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.media; - -import android.media.MediaCodec; -import android.media.MediaCodec.BufferInfo; -import android.media.MediaCodec.CryptoInfo; -import android.os.Parcel; -import android.os.Parcelable; - -import org.mozilla.gecko.annotation.WrapForJNI; -import org.mozilla.gecko.mozglue.SharedMemBuffer; -import org.mozilla.gecko.mozglue.SharedMemory; - -import java.io.IOException; -import java.nio.ByteBuffer; - -// Parcelable carrying input/output sample data and info cross process. -public final class Sample implements Parcelable { - public static final Sample EOS; - static { - BufferInfo eosInfo = new BufferInfo(); - eosInfo.set(0, 0, Long.MIN_VALUE, MediaCodec.BUFFER_FLAG_END_OF_STREAM); - EOS = new Sample(null, eosInfo, null); - } - - public interface Buffer extends Parcelable { - int capacity(); - void readFromByteBuffer(ByteBuffer src, int offset, int size) throws IOException; - void writeToByteBuffer(ByteBuffer dest, int offset, int size) throws IOException; - void dispose(); - } - - private static final class ArrayBuffer implements Buffer { - private byte[] mArray; - - public static final Creator CREATOR = new Creator() { - @Override - public ArrayBuffer createFromParcel(Parcel in) { - return new ArrayBuffer(in); - } - - @Override - public ArrayBuffer[] newArray(int size) { - return new ArrayBuffer[size]; - } - }; - - private ArrayBuffer(Parcel in) { - mArray = in.createByteArray(); - } - - private ArrayBuffer(byte[] bytes) { mArray = bytes; } - - @Override - public int describeContents() { return 0; } - - @Override - public void writeToParcel(Parcel dest, int flags) { - dest.writeByteArray(mArray); - } - - @Override - public int capacity() { - return mArray != null ? mArray.length : 0; - } - - @Override - public void readFromByteBuffer(ByteBuffer src, int offset, int size) throws IOException { - src.position(offset); - if (mArray == null || mArray.length != size) { - mArray = new byte[size]; - } - src.get(mArray, 0, size); - } - - @Override - public void writeToByteBuffer(ByteBuffer dest, int offset, int size) throws IOException { - dest.put(mArray, offset, size); - } - - @Override - public void dispose() { - mArray = null; - } - } - - public Buffer buffer; - @WrapForJNI - public BufferInfo info; - public CryptoInfo cryptoInfo; - - public static Sample create() { return create(null, new BufferInfo(), null); } - - public static Sample create(ByteBuffer src, BufferInfo info, CryptoInfo cryptoInfo) { - ArrayBuffer buffer = new ArrayBuffer(byteArrayFromBuffer(src, info.offset, info.size)); - - BufferInfo bufferInfo = new BufferInfo(); - bufferInfo.set(0, info.size, info.presentationTimeUs, info.flags); - - return new Sample(buffer, bufferInfo, cryptoInfo); - } - - public static Sample create(SharedMemory sharedMem) { - return new Sample(new SharedMemBuffer(sharedMem), new BufferInfo(), null); - } - - private Sample(Buffer bytes, BufferInfo info, CryptoInfo cryptoInfo) { - buffer = bytes; - this.info = info; - this.cryptoInfo = cryptoInfo; - } - - private Sample(Parcel in) { - readInfo(in); - readCrypto(in); - buffer = in.readParcelable(Sample.class.getClassLoader()); - } - - private void readInfo(Parcel in) { - int offset = in.readInt(); - int size = in.readInt(); - long pts = in.readLong(); - int flags = in.readInt(); - - info = new BufferInfo(); - info.set(offset, size, pts, flags); - } - - private void readCrypto(Parcel in) { - int hasCryptoInfo = in.readInt(); - if (hasCryptoInfo == 0) { - return; - } - - byte[] iv = in.createByteArray(); - byte[] key = in.createByteArray(); - int mode = in.readInt(); - int[] numBytesOfClearData = in.createIntArray(); - int[] numBytesOfEncryptedData = in.createIntArray(); - int numSubSamples = in.readInt(); - - cryptoInfo = new CryptoInfo(); - cryptoInfo.set(numSubSamples, - numBytesOfClearData, - numBytesOfEncryptedData, - key, - iv, - mode); - } - - public Sample set(ByteBuffer bytes, BufferInfo info, CryptoInfo cryptoInfo) throws IOException { - if (bytes != null && info.size > 0) { - buffer.readFromByteBuffer(bytes, info.offset, info.size); - } - this.info.set(0, info.size, info.presentationTimeUs, info.flags); - this.cryptoInfo = cryptoInfo; - - return this; - } - - public void dispose() { - if (isEOS()) { - return; - } - - if (buffer != null) { - buffer.dispose(); - buffer = null; - } - info = null; - cryptoInfo = null; - } - - public boolean isEOS() { - return (this == EOS) || - ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0); - } - - public static final Creator CREATOR = new Creator() { - @Override - public Sample createFromParcel(Parcel in) { - return new Sample(in); - } - - @Override - public Sample[] newArray(int size) { - return new Sample[size]; - } - }; - - @Override - public int describeContents() { - return 0; - } - - @Override - public void writeToParcel(Parcel dest, int parcelableFlags) { - writeInfo(dest); - writeCrypto(dest); - dest.writeParcelable(buffer, parcelableFlags); - } - - private void writeInfo(Parcel dest) { - dest.writeInt(info.offset); - dest.writeInt(info.size); - dest.writeLong(info.presentationTimeUs); - dest.writeInt(info.flags); - } - - private void writeCrypto(Parcel dest) { - if (cryptoInfo != null) { - dest.writeInt(1); - dest.writeByteArray(cryptoInfo.iv); - dest.writeByteArray(cryptoInfo.key); - dest.writeInt(cryptoInfo.mode); - dest.writeIntArray(cryptoInfo.numBytesOfClearData); - dest.writeIntArray(cryptoInfo.numBytesOfEncryptedData); - dest.writeInt(cryptoInfo.numSubSamples); - } else { - dest.writeInt(0); - } - } - - public static byte[] byteArrayFromBuffer(ByteBuffer buffer, int offset, int size) { - if (buffer == null || buffer.capacity() == 0 || size == 0) { - return null; - } - if (buffer.hasArray() && offset == 0 && buffer.array().length == size) { - return buffer.array(); - } - int length = Math.min(offset + size, buffer.capacity()) - offset; - byte[] bytes = new byte[length]; - buffer.position(offset); - buffer.get(bytes); - return bytes; - } - - @WrapForJNI - public void writeToByteBuffer(ByteBuffer dest) throws IOException { - if (buffer != null && dest != null && info.size > 0) { - buffer.writeToByteBuffer(dest, info.offset, info.size); - } - } - - @Override - public String toString() { - if (isEOS()) { - return "EOS sample"; - } - - StringBuilder str = new StringBuilder(); - str.append("{ buffer=").append(buffer). - append(", info="). - append("{ offset=").append(info.offset). - append(", size=").append(info.size). - append(", pts=").append(info.presentationTimeUs). - append(", flags=").append(Integer.toHexString(info.flags)).append(" }"). - append(" }"); - return str.toString(); - } -} \ No newline at end of file diff --git a/mobile/android/base/java/org/mozilla/gecko/media/SamplePool.java b/mobile/android/base/java/org/mozilla/gecko/media/SamplePool.java deleted file mode 100644 index 9041e37561..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/media/SamplePool.java +++ /dev/null @@ -1,115 +0,0 @@ -/* 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.media; - -import android.media.MediaCodec; - -import org.mozilla.gecko.mozglue.SharedMemory; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; - -final class SamplePool { - private final class Impl { - private final String mName; - private int mNextId = 0; - private int mDefaultBufferSize = 4096; - private final List mRecycledSamples = new ArrayList<>(); - - private Impl(String name) { - mName = name; - } - - private void setDefaultBufferSize(int size) { - mDefaultBufferSize = size; - } - - private synchronized Sample allocate(int size) { - Sample sample; - if (!mRecycledSamples.isEmpty()) { - sample = mRecycledSamples.remove(0); - sample.info.set(0, 0, 0, 0); - } else { - SharedMemory shm = null; - try { - shm = new SharedMemory(mNextId++, Math.max(size, mDefaultBufferSize)); - } catch (NoSuchMethodException e) { - e.printStackTrace(); - } catch (IOException e) { - e.printStackTrace(); - } - if (shm != null) { - sample = Sample.create(shm); - } else { - sample = Sample.create(); - } - } - - return sample; - } - - private synchronized void recycle(Sample recycled) { - if (recycled.buffer.capacity() >= mDefaultBufferSize) { - mRecycledSamples.add(recycled); - } else { - recycled.dispose(); - } - } - - private synchronized void clear() { - for (Sample s : mRecycledSamples) { - s.dispose(); - } - - mRecycledSamples.clear(); - } - - @Override - protected void finalize() { - clear(); - } - } - - private final Impl mInputs; - private final Impl mOutputs; - - /* package */ SamplePool(String name) { - mInputs = new Impl(name + " input buffer pool"); - mOutputs = new Impl(name + " output buffer pool"); - } - - /* package */ void setInputBufferSize(int size) { - mInputs.setDefaultBufferSize(size); - } - - /* package */ void setOutputBufferSize(int size) { - mOutputs.setDefaultBufferSize(size); - } - - /* package */ Sample obtainInput(int size) { - return mInputs.allocate(size); - } - - /* package */ Sample obtainOutput(MediaCodec.BufferInfo info) { - Sample output = mOutputs.allocate(info.size); - output.info.set(0, info.size, info.presentationTimeUs, info.flags); - return output; - } - - /* package */ void recycleInput(Sample sample) { - sample.cryptoInfo = null; - mInputs.recycle(sample); - } - - /* package */ void recycleOutput(Sample sample) { - mOutputs.recycle(sample); - } - - /* package */ void reset() { - mInputs.clear(); - mOutputs.clear(); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/media/SessionKeyInfo.java b/mobile/android/base/java/org/mozilla/gecko/media/SessionKeyInfo.java deleted file mode 100644 index b41ef36253..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/media/SessionKeyInfo.java +++ /dev/null @@ -1,51 +0,0 @@ -/* 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.media; - -import android.os.Parcel; -import android.os.Parcelable; -import org.mozilla.gecko.annotation.WrapForJNI; - -public final class SessionKeyInfo implements Parcelable { - @WrapForJNI - public byte[] keyId; - - @WrapForJNI - public int status; - - @WrapForJNI - public SessionKeyInfo(byte[] keyId, int status) { - this.keyId = keyId; - this.status = status; - } - - @Override - public int describeContents() { - return 0; - } - - @Override - public void writeToParcel(Parcel dest, int parcelableFlags) { - dest.writeByteArray(keyId); - dest.writeInt(status); - } - - public static final Creator CREATOR = new Creator() { - @Override - public SessionKeyInfo createFromParcel(Parcel in) { - return new SessionKeyInfo(in); - } - - @Override - public SessionKeyInfo[] newArray(int size) { - return new SessionKeyInfo[size]; - } - }; - - private SessionKeyInfo(Parcel src) { - keyId = src.createByteArray(); - status = src.readInt(); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/media/VideoPlayer.java b/mobile/android/base/java/org/mozilla/gecko/media/VideoPlayer.java deleted file mode 100644 index 508b9d015f..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/media/VideoPlayer.java +++ /dev/null @@ -1,204 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.media; - -import android.content.Context; - -import android.graphics.Color; - -import android.net.Uri; - -import android.util.AttributeSet; -import android.util.Log; -import android.view.Gravity; -import android.view.KeyEvent; -import android.view.MotionEvent; -import android.view.View; -import android.view.ViewGroup; - -import android.widget.ImageButton; -import android.widget.FrameLayout; -import android.widget.ImageView; -import android.widget.LinearLayout; -import android.widget.MediaController; -import android.widget.VideoView; - -import org.mozilla.gecko.R; - -public class VideoPlayer extends FrameLayout { - private VideoView video; - private FullScreenMediaController controller; - private FullScreenListener fullScreenListener; - - private boolean isFullScreen; - - public VideoPlayer(Context ctx) { - this(ctx, null); - } - - public VideoPlayer(Context ctx, AttributeSet attrs) { - this(ctx, attrs, 0); - } - - public VideoPlayer(Context ctx, AttributeSet attrs, int defStyle) { - super(ctx, attrs, defStyle); - setFullScreen(false); - setVisibility(View.GONE); - } - - public void start(Uri uri) { - stop(); - - video = new VideoView(getContext()); - controller = new FullScreenMediaController(getContext()); - video.setMediaController(controller); - controller.setAnchorView(video); - - video.setVideoURI(uri); - - FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams( - FrameLayout.LayoutParams.MATCH_PARENT, - FrameLayout.LayoutParams.WRAP_CONTENT, - Gravity.CENTER); - - addView(video, layoutParams); - setVisibility(View.VISIBLE); - - video.setZOrderOnTop(true); - video.start(); - } - - public boolean isPlaying() { - return video != null; - } - - public void stop() { - if (video == null) { - return; - } - - removeAllViews(); - setVisibility(View.GONE); - video.stopPlayback(); - - video = null; - controller = null; - } - - public void setFullScreenListener(FullScreenListener listener) { - fullScreenListener = listener; - } - - public boolean isFullScreen() { - return isFullScreen; - } - - public void setFullScreen(boolean fullScreen) { - isFullScreen = fullScreen; - if (fullScreen) { - setBackgroundColor(Color.BLACK); - } else { - setBackgroundResource(R.color.dark_transparent_overlay); - } - - if (controller != null) { - controller.setFullScreen(fullScreen); - } - } - - @Override - public boolean onKeyDown(int keyCode, KeyEvent event) { - if (event.isSystem()) { - return super.onKeyDown(keyCode, event); - } - return true; - } - - @Override - public boolean onKeyUp(int keyCode, KeyEvent event) { - if (event.isSystem()) { - return super.onKeyUp(keyCode, event); - } - return true; - } - - @Override - public boolean onTouchEvent(MotionEvent event) { - super.onTouchEvent(event); - return true; - } - - @Override - public boolean onTrackballEvent(MotionEvent event) { - super.onTrackballEvent(event); - return true; - } - - public interface FullScreenListener { - void onFullScreenChanged(boolean fullScreen); - } - - private class FullScreenMediaController extends MediaController { - private ImageButton mButton; - - public FullScreenMediaController(Context ctx) { - super(ctx); - - mButton = new ImageButton(getContext()); - mButton.setScaleType(ImageView.ScaleType.FIT_CENTER); - mButton.setBackgroundColor(Color.TRANSPARENT); - mButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - FullScreenMediaController.this.onFullScreenClicked(); - } - }); - - updateFullScreenButton(false); - } - - public void setFullScreen(boolean fullScreen) { - updateFullScreenButton(fullScreen); - } - - private void updateFullScreenButton(boolean fullScreen) { - mButton.setImageResource(fullScreen ? R.drawable.exit_fullscreen : R.drawable.fullscreen); - } - - private void onFullScreenClicked() { - if (VideoPlayer.this.fullScreenListener != null) { - boolean fullScreen = !VideoPlayer.this.isFullScreen(); - VideoPlayer.this.fullScreenListener.onFullScreenChanged(fullScreen); - } - } - - @Override - public void setAnchorView(final View view) { - super.setAnchorView(view); - - // Add the fullscreen button here because this is where the parent class actually creates - // the media buttons and their layout. - // - // http://androidxref.com/6.0.1_r10/xref/frameworks/base/core/java/android/widget/MediaController.java#239 - // - // The media buttons are in a horizontal linear layout which is itself packed into - // a vertical layout. The vertical layout is the only child of the FrameLayout which - // MediaController inherits from. - LinearLayout child = (LinearLayout) getChildAt(0); - LinearLayout buttons = (LinearLayout) child.getChildAt(0); - - LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT, - LayoutParams.FILL_PARENT); - params.gravity = Gravity.CENTER_VERTICAL; - - if (mButton.getParent() != null) { - ((ViewGroup)mButton.getParent()).removeView(mButton); - } - - buttons.addView(mButton, params); - } - } -} \ No newline at end of file diff --git a/mobile/android/base/java/org/mozilla/gecko/menu/GeckoMenu.java b/mobile/android/base/java/org/mozilla/gecko/menu/GeckoMenu.java deleted file mode 100644 index 512f320025..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/menu/GeckoMenu.java +++ /dev/null @@ -1,928 +0,0 @@ -/* 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.menu; - -import org.mozilla.gecko.AppConstants; -import org.mozilla.gecko.GeckoAppShell; -import org.mozilla.gecko.R; -import org.mozilla.gecko.util.ThreadUtils; -import org.mozilla.gecko.util.ThreadUtils.AssertBehavior; -import org.mozilla.gecko.widget.GeckoActionProvider; - -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.util.AttributeSet; -import android.util.Log; -import android.util.SparseArray; -import android.view.KeyEvent; -import android.view.LayoutInflater; -import android.view.Menu; -import android.view.MenuItem; -import android.view.SubMenu; -import android.view.View; -import android.view.ViewGroup; -import android.widget.AdapterView; -import android.widget.BaseAdapter; -import android.widget.LinearLayout; -import android.widget.ListView; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -public class GeckoMenu extends ListView - implements Menu, - AdapterView.OnItemClickListener, - GeckoMenuItem.OnShowAsActionChangedListener { - private static final String LOGTAG = "GeckoMenu"; - - /** - * Controls whether off-UI-thread method calls in this class cause an - * exception or just logging. - */ - private static final AssertBehavior THREAD_ASSERT_BEHAVIOR = AppConstants.RELEASE_OR_BETA ? AssertBehavior.NONE : AssertBehavior.THROW; - - /* - * A callback for a menu item click/long click event. - */ - public static interface Callback { - // Called when a menu item is clicked, with the actual menu item as the argument. - public boolean onMenuItemClick(MenuItem item); - - // Called when a menu item is long-clicked, with the actual menu item as the argument. - public boolean onMenuItemLongClick(MenuItem item); - } - - /* - * An interface for a presenter to show the menu. - * Either an Activity or a View can be a presenter, that can watch for events - * and show/hide menu. - */ - public static interface MenuPresenter { - // Open the menu. - public void openMenu(); - - // Show the actual view containing the menu items. This can either be a parent or sub-menu. - public void showMenu(View menu); - - // Close the menu. - public void closeMenu(); - } - - /* - * An interface for a presenter of action-items. - * Either an Activity or a View can be a presenter, that can watch for events - * and add/remove action-items. If not ActionItemBarPresenter, the menu uses a - * DefaultActionItemBar, that shows the action-items as a header over list-view. - */ - public static interface ActionItemBarPresenter { - // Add an action-item. - public boolean addActionItem(View actionItem); - - // Remove an action-item. - public void removeActionItem(View actionItem); - } - - protected static final int NO_ID = 0; - - // List of all menu items. - private final List mItems; - - // Quick lookup array used to make a fast path in findItem. - private final SparseArray mItemsById; - - // Map of "always" action-items in action-bar and their views. - private final Map mPrimaryActionItems; - - // Map of "ifRoom" action-items in action-bar and their views. - private final Map mSecondaryActionItems; - - // Map of "collapseActionView" action-items in action-bar and their views. - private final Map mQuickShareActionItems; - - // Reference to a callback for menu events. - private Callback mCallback; - - // Reference to menu presenter. - private MenuPresenter mMenuPresenter; - - // Reference to "always" action-items bar in action-bar. - private ActionItemBarPresenter mPrimaryActionItemBar; - - // Reference to "ifRoom" action-items bar in action-bar. - private final ActionItemBarPresenter mSecondaryActionItemBar; - - // Reference to "collapseActionView" action-items bar in action-bar. - private final ActionItemBarPresenter mQuickShareActionItemBar; - - // Adapter to hold the list of menu items. - private final MenuItemsAdapter mAdapter; - - // Show/hide icons in the list. - boolean mShowIcons; - - public GeckoMenu(Context context) { - this(context, null); - } - - public GeckoMenu(Context context, AttributeSet attrs) { - this(context, attrs, R.attr.geckoMenuListViewStyle); - } - - public GeckoMenu(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - - setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, - LayoutParams.MATCH_PARENT)); - - // Attach an adapter. - mAdapter = new MenuItemsAdapter(); - setAdapter(mAdapter); - setOnItemClickListener(this); - - mItems = new ArrayList(); - mItemsById = new SparseArray(); - mPrimaryActionItems = new HashMap(); - mSecondaryActionItems = new HashMap(); - mQuickShareActionItems = new HashMap(); - - mPrimaryActionItemBar = (DefaultActionItemBar) LayoutInflater.from(context).inflate(R.layout.menu_action_bar, null); - mSecondaryActionItemBar = (DefaultActionItemBar) LayoutInflater.from(context).inflate(R.layout.menu_secondary_action_bar, null); - mQuickShareActionItemBar = (DefaultActionItemBar) LayoutInflater.from(context).inflate(R.layout.menu_secondary_action_bar, null); - } - - private static void assertOnUiThread() { - ThreadUtils.assertOnUiThread(THREAD_ASSERT_BEHAVIOR); - } - - @Override - public MenuItem add(CharSequence title) { - GeckoMenuItem menuItem = new GeckoMenuItem(this, NO_ID, 0, title); - addItem(menuItem); - return menuItem; - } - - @Override - public MenuItem add(int groupId, int itemId, int order, int titleRes) { - GeckoMenuItem menuItem = new GeckoMenuItem(this, itemId, order, titleRes); - addItem(menuItem); - return menuItem; - } - - @Override - public MenuItem add(int titleRes) { - GeckoMenuItem menuItem = new GeckoMenuItem(this, NO_ID, 0, titleRes); - addItem(menuItem); - return menuItem; - } - - @Override - public MenuItem add(int groupId, int itemId, int order, CharSequence title) { - GeckoMenuItem menuItem = new GeckoMenuItem(this, itemId, order, title); - addItem(menuItem); - return menuItem; - } - - private void addItem(GeckoMenuItem menuItem) { - assertOnUiThread(); - menuItem.setOnShowAsActionChangedListener(this); - mAdapter.addMenuItem(menuItem); - mItems.add(menuItem); - } - - private boolean addActionItem(final GeckoMenuItem menuItem) { - assertOnUiThread(); - menuItem.setOnShowAsActionChangedListener(this); - - final View actionView = menuItem.getActionView(); - final int actionEnum = menuItem.getActionEnum(); - boolean added = false; - - if (actionEnum == GeckoMenuItem.SHOW_AS_ACTION_ALWAYS) { - if (mPrimaryActionItems.size() == 0 && - mPrimaryActionItemBar instanceof DefaultActionItemBar) { - // Reset the adapter before adding the header view to a list. - setAdapter(null); - addHeaderView((DefaultActionItemBar) mPrimaryActionItemBar); - setAdapter(mAdapter); - } - - if (added = mPrimaryActionItemBar.addActionItem(actionView)) { - mPrimaryActionItems.put(menuItem, actionView); - mItems.add(menuItem); - } - } else if (actionEnum == GeckoMenuItem.SHOW_AS_ACTION_IF_ROOM) { - if (mSecondaryActionItems.size() == 0) { - // Reset the adapter before adding the header view to a list. - setAdapter(null); - addHeaderView((DefaultActionItemBar) mSecondaryActionItemBar); - setAdapter(mAdapter); - } - - if (added = mSecondaryActionItemBar.addActionItem(actionView)) { - mSecondaryActionItems.put(menuItem, actionView); - mItems.add(menuItem); - } - } else if (actionEnum == GeckoMenuItem.SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW) { - if (actionView instanceof MenuItemSwitcherLayout) { - final MenuItemSwitcherLayout quickShareView = (MenuItemSwitcherLayout) actionView; - - // We don't want to add the quick share bar if we don't have any quick share items. - if (quickShareView.getActionButtonCount() > 0 && - (added = mQuickShareActionItemBar.addActionItem(quickShareView))) { - if (mQuickShareActionItems.size() == 0) { - // Reset the adapter before adding the header view to a list. - setAdapter(null); - addHeaderView((DefaultActionItemBar) mQuickShareActionItemBar); - setAdapter(mAdapter); - } - - mQuickShareActionItems.put(menuItem, quickShareView); - mItems.add(menuItem); - } - } - } - - // Set the listeners. - if (actionView instanceof MenuItemActionBar) { - ((MenuItemActionBar) actionView).setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - handleMenuItemClick(menuItem); - } - }); - ((MenuItemActionBar) actionView).setOnLongClickListener(new View.OnLongClickListener() { - @Override - public boolean onLongClick(View view) { - if (handleMenuItemLongClick(menuItem)) { - GeckoAppShell.vibrateOnHapticFeedbackEnabled(getResources().getIntArray(R.array.long_press_vibrate_msec)); - return true; - } - return false; - } - }); - } else if (actionView instanceof MenuItemSwitcherLayout) { - ((MenuItemSwitcherLayout) actionView).setMenuItemClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - handleMenuItemClick(menuItem); - } - }); - ((MenuItemSwitcherLayout) actionView).setMenuItemLongClickListener(new View.OnLongClickListener() { - @Override - public boolean onLongClick(View view) { - if (handleMenuItemLongClick(menuItem)) { - GeckoAppShell.vibrateOnHapticFeedbackEnabled(getResources().getIntArray(R.array.long_press_vibrate_msec)); - return true; - } - return false; - } - }); - } - - return added; - } - - @Override - public int addIntentOptions(int groupId, int itemId, int order, ComponentName caller, Intent[] specifics, Intent intent, int flags, MenuItem[] outSpecificItems) { - return 0; - } - - @Override - public SubMenu addSubMenu(int groupId, int itemId, int order, CharSequence title) { - MenuItem menuItem = add(groupId, itemId, order, title); - return addSubMenu(menuItem); - } - - @Override - public SubMenu addSubMenu(int groupId, int itemId, int order, int titleRes) { - MenuItem menuItem = add(groupId, itemId, order, titleRes); - return addSubMenu(menuItem); - } - - @Override - public SubMenu addSubMenu(CharSequence title) { - MenuItem menuItem = add(title); - return addSubMenu(menuItem); - } - - @Override - public SubMenu addSubMenu(int titleRes) { - MenuItem menuItem = add(titleRes); - return addSubMenu(menuItem); - } - - private SubMenu addSubMenu(MenuItem menuItem) { - GeckoSubMenu subMenu = new GeckoSubMenu(getContext()); - subMenu.setMenuItem(menuItem); - subMenu.setCallback(mCallback); - subMenu.setMenuPresenter(mMenuPresenter); - ((GeckoMenuItem) menuItem).setSubMenu(subMenu); - return subMenu; - } - - private void removePrimaryActionBarView() { - // Reset the adapter before removing the header view from a list. - setAdapter(null); - removeHeaderView((DefaultActionItemBar) mPrimaryActionItemBar); - setAdapter(mAdapter); - } - - private void removeSecondaryActionBarView() { - // Reset the adapter before removing the header view from a list. - setAdapter(null); - removeHeaderView((DefaultActionItemBar) mSecondaryActionItemBar); - setAdapter(mAdapter); - } - - private void removeQuickShareActionBarView() { - // Reset the adapter before removing the header view from a list. - setAdapter(null); - removeHeaderView((DefaultActionItemBar) mQuickShareActionItemBar); - setAdapter(mAdapter); - } - - @Override - public void clear() { - assertOnUiThread(); - for (GeckoMenuItem menuItem : mItems) { - if (menuItem.hasSubMenu()) { - SubMenu sub = menuItem.getSubMenu(); - if (sub == null) { - continue; - } - try { - sub.clear(); - } catch (Exception ex) { - Log.e(LOGTAG, "Couldn't clear submenu.", ex); - } - } - } - - mAdapter.clear(); - mItems.clear(); - - /* - * Reinflating the menu will re-add any action items to the toolbar, so - * remove the old ones. This also ensures that any text associated with - * these is switched to the correct locale. - */ - if (mPrimaryActionItemBar != null) { - for (View item : mPrimaryActionItems.values()) { - mPrimaryActionItemBar.removeActionItem(item); - } - } - mPrimaryActionItems.clear(); - - if (mSecondaryActionItemBar != null) { - for (View item : mSecondaryActionItems.values()) { - mSecondaryActionItemBar.removeActionItem(item); - } - } - mSecondaryActionItems.clear(); - - if (mQuickShareActionItemBar != null) { - for (View item : mQuickShareActionItems.values()) { - mQuickShareActionItemBar.removeActionItem(item); - } - } - mQuickShareActionItems.clear(); - - // Remove the view, too -- the first addActionItem will re-add it, - // and this is simpler than changing that logic. - if (mPrimaryActionItemBar instanceof DefaultActionItemBar) { - removePrimaryActionBarView(); - } - - removeSecondaryActionBarView(); - removeQuickShareActionBarView(); - } - - @Override - public void close() { - if (mMenuPresenter != null) - mMenuPresenter.closeMenu(); - } - - private void showMenu(View viewForMenu) { - if (mMenuPresenter != null) - mMenuPresenter.showMenu(viewForMenu); - } - - @Override - public MenuItem findItem(int id) { - assertOnUiThread(); - MenuItem quickItem = mItemsById.get(id); - if (quickItem != null) { - return quickItem; - } - - for (GeckoMenuItem menuItem : mItems) { - if (menuItem.getItemId() == id) { - mItemsById.put(id, menuItem); - return menuItem; - } else if (menuItem.hasSubMenu()) { - if (!menuItem.hasActionProvider()) { - SubMenu subMenu = menuItem.getSubMenu(); - MenuItem item = subMenu.findItem(id); - if (item != null) { - mItemsById.put(id, item); - return item; - } - } - } - } - return null; - } - - @Override - public MenuItem getItem(int index) { - if (index < mItems.size()) - return mItems.get(index); - - return null; - } - - @Override - public boolean hasVisibleItems() { - assertOnUiThread(); - for (GeckoMenuItem menuItem : mItems) { - if (menuItem.isVisible() && - !mPrimaryActionItems.containsKey(menuItem) && - !mSecondaryActionItems.containsKey(menuItem) && - !mQuickShareActionItems.containsKey(menuItem)) - return true; - } - - return false; - } - - @Override - public boolean onKeyDown(int keyCode, KeyEvent event) { - // Close the menu if it is open and the hardware menu key is pressed. - if (keyCode == KeyEvent.KEYCODE_MENU && isShown()) { - close(); - return true; - } - - return super.onKeyDown(keyCode, event); - } - - @Override - public boolean isShortcutKey(int keyCode, KeyEvent event) { - return true; - } - - @Override - public boolean performIdentifierAction(int id, int flags) { - return false; - } - - @Override - public boolean performShortcut(int keyCode, KeyEvent event, int flags) { - return false; - } - - @Override - public void removeGroup(int groupId) { - } - - @Override - public void removeItem(int id) { - assertOnUiThread(); - GeckoMenuItem item = (GeckoMenuItem) findItem(id); - if (item == null) - return; - - // Remove it from the cache. - mItemsById.remove(id); - - // Remove it from any sub-menu. - for (GeckoMenuItem menuItem : mItems) { - if (menuItem.hasSubMenu()) { - SubMenu subMenu = menuItem.getSubMenu(); - if (subMenu != null && subMenu.findItem(id) != null) { - subMenu.removeItem(id); - return; - } - } - } - - // Remove it from own menu. - if (mPrimaryActionItems.containsKey(item)) { - if (mPrimaryActionItemBar != null) - mPrimaryActionItemBar.removeActionItem(mPrimaryActionItems.get(item)); - - mPrimaryActionItems.remove(item); - mItems.remove(item); - - if (mPrimaryActionItems.size() == 0 && - mPrimaryActionItemBar instanceof DefaultActionItemBar) { - removePrimaryActionBarView(); - } - - return; - } - - if (mSecondaryActionItems.containsKey(item)) { - if (mSecondaryActionItemBar != null) - mSecondaryActionItemBar.removeActionItem(mSecondaryActionItems.get(item)); - - mSecondaryActionItems.remove(item); - mItems.remove(item); - - if (mSecondaryActionItems.size() == 0) { - removeSecondaryActionBarView(); - } - - return; - } - - if (mQuickShareActionItems.containsKey(item)) { - if (mQuickShareActionItemBar != null) - mQuickShareActionItemBar.removeActionItem(mQuickShareActionItems.get(item)); - - mQuickShareActionItems.remove(item); - mItems.remove(item); - - if (mQuickShareActionItems.size() == 0) { - removeQuickShareActionBarView(); - } - - return; - } - - mAdapter.removeMenuItem(item); - mItems.remove(item); - } - - @Override - public void setGroupCheckable(int group, boolean checkable, boolean exclusive) { - } - - @Override - public void setGroupEnabled(int group, boolean enabled) { - } - - @Override - public void setGroupVisible(int group, boolean visible) { - } - - @Override - public void setQwertyMode(boolean isQwerty) { - } - - @Override - public int size() { - return mItems.size(); - } - - @Override - public boolean hasActionItemBar() { - return (mPrimaryActionItemBar != null) && - (mSecondaryActionItemBar != null) && - (mQuickShareActionItemBar != null); - } - - @Override - public void onShowAsActionChanged(GeckoMenuItem item) { - removeItem(item.getItemId()); - - if (item.isActionItem() && addActionItem(item)) { - return; - } - - addItem(item); - } - - public void onItemChanged(GeckoMenuItem item) { - assertOnUiThread(); - if (item.isActionItem()) { - final View actionView; - final int actionEnum = item.getActionEnum(); - if (actionEnum == GeckoMenuItem.SHOW_AS_ACTION_ALWAYS) { - actionView = mPrimaryActionItems.get(item); - } else if (actionEnum == GeckoMenuItem.SHOW_AS_ACTION_IF_ROOM) { - actionView = mSecondaryActionItems.get(item); - } else { - actionView = mQuickShareActionItems.get(item); - } - - if (actionView != null) { - if (item.isVisible()) { - actionView.setVisibility(View.VISIBLE); - if (actionView instanceof MenuItemActionBar) { - ((MenuItemActionBar) actionView).initialize(item); - } else { - ((MenuItemSwitcherLayout) actionView).initialize(item); - } - } else { - actionView.setVisibility(View.GONE); - } - } - } else { - mAdapter.notifyDataSetChanged(); - } - } - - @Override - public void onItemClick(AdapterView parent, View view, int position, long id) { - // We might be showing headers. Account them while using the position. - position -= getHeaderViewsCount(); - - GeckoMenuItem item = mAdapter.getItem(position); - handleMenuItemClick(item); - } - - void handleMenuItemClick(GeckoMenuItem item) { - if (!item.isEnabled()) - return; - - if (item.invoke()) { - close(); - } else if (item.hasSubMenu()) { - // Refresh the submenu for the provider. - GeckoActionProvider provider = item.getGeckoActionProvider(); - if (provider != null) { - GeckoSubMenu subMenu = new GeckoSubMenu(getContext()); - subMenu.setShowIcons(true); - provider.onPrepareSubMenu(subMenu); - item.setSubMenu(subMenu); - } - - // Show the submenu. - GeckoSubMenu subMenu = (GeckoSubMenu) item.getSubMenu(); - showMenu(subMenu); - } else { - close(); - mCallback.onMenuItemClick(item); - } - } - - boolean handleMenuItemLongClick(GeckoMenuItem item) { - if (!item.isEnabled()) { - return false; - } - - if (mCallback != null) { - if (mCallback.onMenuItemLongClick(item)) { - close(); - return true; - } - } - return false; - } - - public Callback getCallback() { - return mCallback; - } - - public MenuPresenter getMenuPresenter() { - return mMenuPresenter; - } - - public void setCallback(Callback callback) { - mCallback = callback; - - // Update the submenus just in case this changes on the fly. - for (GeckoMenuItem menuItem : mItems) { - if (menuItem.hasSubMenu()) { - GeckoSubMenu subMenu = (GeckoSubMenu) menuItem.getSubMenu(); - subMenu.setCallback(mCallback); - } - } - } - - public void setMenuPresenter(MenuPresenter presenter) { - mMenuPresenter = presenter; - - // Update the submenus just in case this changes on the fly. - for (GeckoMenuItem menuItem : mItems) { - if (menuItem.hasSubMenu()) { - GeckoSubMenu subMenu = (GeckoSubMenu) menuItem.getSubMenu(); - subMenu.setMenuPresenter(mMenuPresenter); - } - } - } - - public void setActionItemBarPresenter(ActionItemBarPresenter presenter) { - mPrimaryActionItemBar = presenter; - } - - public void setShowIcons(boolean show) { - if (mShowIcons != show) { - mShowIcons = show; - mAdapter.notifyDataSetChanged(); - } - } - - // Action Items are added to the header view by default. - // URL bar can register itself as a presenter, in case it has a different place to show them. - public static class DefaultActionItemBar extends LinearLayout - implements ActionItemBarPresenter { - private final int mRowHeight; - private float mWeightSum; - - public DefaultActionItemBar(Context context) { - this(context, null); - } - - public DefaultActionItemBar(Context context, AttributeSet attrs) { - super(context, attrs); - - mRowHeight = getResources().getDimensionPixelSize(R.dimen.menu_item_row_height); - } - - @Override - public boolean addActionItem(View actionItem) { - ViewGroup.LayoutParams actualParams = actionItem.getLayoutParams(); - LinearLayout.LayoutParams params; - - if (actualParams != null) { - params = new LinearLayout.LayoutParams(actionItem.getLayoutParams()); - params.width = 0; - } else { - params = new LinearLayout.LayoutParams(0, mRowHeight); - } - - if (actionItem instanceof MenuItemSwitcherLayout) { - params.weight = ((MenuItemSwitcherLayout) actionItem).getChildCount(); - } else { - params.weight = 1.0f; - } - - mWeightSum += params.weight; - - actionItem.setLayoutParams(params); - addView(actionItem); - setWeightSum(mWeightSum); - return true; - } - - @Override - public void removeActionItem(View actionItem) { - if (indexOfChild(actionItem) != -1) { - LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) actionItem.getLayoutParams(); - mWeightSum -= params.weight; - removeView(actionItem); - } - } - } - - // Adapter to bind menu items to the list. - private class MenuItemsAdapter extends BaseAdapter { - private static final int VIEW_TYPE_DEFAULT = 0; - private static final int VIEW_TYPE_ACTION_MODE = 1; - - private final List mItems; - - public MenuItemsAdapter() { - mItems = new ArrayList(); - } - - @Override - public int getCount() { - if (mItems == null) - return 0; - - int visibleCount = 0; - for (GeckoMenuItem item : mItems) { - if (item.isVisible()) - visibleCount++; - } - - return visibleCount; - } - - @Override - public GeckoMenuItem getItem(int position) { - for (GeckoMenuItem item : mItems) { - if (item.isVisible()) { - position--; - - if (position < 0) - return item; - } - } - - return null; - } - - @Override - public long getItemId(int position) { - return position; - } - - @Override - public View getView(int position, View convertView, ViewGroup parent) { - GeckoMenuItem item = getItem(position); - GeckoMenuItem.Layout view = null; - - // Try to re-use the view. - if (convertView == null && getItemViewType(position) == VIEW_TYPE_DEFAULT) { - view = new MenuItemDefault(parent.getContext(), null); - } else { - view = (GeckoMenuItem.Layout) convertView; - } - - if (view == null || view instanceof MenuItemSwitcherLayout) { - // Always get from the menu item. - // This will ensure that the default activity is refreshed. - view = (MenuItemSwitcherLayout) item.getActionView(); - - // ListView will not perform an item click if the row has a focusable view in it. - // Hence, forward the click event on the menu item in the action-view to the ListView. - final View actionView = (View) view; - final int pos = position; - final long id = getItemId(position); - ((MenuItemSwitcherLayout) view).setMenuItemClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - GeckoMenu listView = GeckoMenu.this; - listView.performItemClick(actionView, pos + listView.getHeaderViewsCount(), id); - } - }); - } - - // Initialize the view. - view.setShowIcon(mShowIcons); - view.initialize(item); - return (View) view; - } - - @Override - public int getItemViewType(int position) { - return getItem(position).getGeckoActionProvider() == null ? VIEW_TYPE_DEFAULT : VIEW_TYPE_ACTION_MODE; - } - - @Override - public int getViewTypeCount() { - return 2; - } - - @Override - public boolean hasStableIds() { - return false; - } - - @Override - public boolean areAllItemsEnabled() { - // Setting this to true is a workaround to fix disappearing - // dividers in the menu (bug 963249). - return true; - } - - @Override - public boolean isEnabled(int position) { - // Setting this to true is a workaround to fix disappearing - // dividers in the menu in L (bug 1050780). - return true; - } - - public void addMenuItem(GeckoMenuItem menuItem) { - if (mItems.contains(menuItem)) - return; - - // Insert it in proper order. - int index = 0; - for (GeckoMenuItem item : mItems) { - if (item.getOrder() > menuItem.getOrder()) { - mItems.add(index, menuItem); - notifyDataSetChanged(); - return; - } else { - index++; - } - } - - // Add the menuItem at the end. - mItems.add(menuItem); - notifyDataSetChanged(); - } - - public void removeMenuItem(GeckoMenuItem menuItem) { - // Remove it from the list. - mItems.remove(menuItem); - notifyDataSetChanged(); - } - - public void clear() { - mItemsById.clear(); - mItems.clear(); - notifyDataSetChanged(); - } - - public GeckoMenuItem getMenuItem(int id) { - for (GeckoMenuItem item : mItems) { - if (item.getItemId() == id) - return item; - } - - return null; - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/menu/GeckoMenuInflater.java b/mobile/android/base/java/org/mozilla/gecko/menu/GeckoMenuInflater.java deleted file mode 100644 index dfcb31c5fb..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/menu/GeckoMenuInflater.java +++ /dev/null @@ -1,163 +0,0 @@ -/* 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.menu; - -import java.io.IOException; - -import org.mozilla.gecko.AppConstants.Versions; -import org.mozilla.gecko.R; -import org.xmlpull.v1.XmlPullParser; -import org.xmlpull.v1.XmlPullParserException; - -import android.content.Context; -import android.content.res.TypedArray; -import android.content.res.XmlResourceParser; -import android.util.AttributeSet; -import android.util.Xml; -import android.view.InflateException; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.view.SubMenu; - -public class GeckoMenuInflater extends MenuInflater { - private static final String TAG_MENU = "menu"; - private static final String TAG_ITEM = "item"; - private static final int NO_ID = 0; - - private final Context mContext; - - // Private class to hold the parsed menu item. - private class ParsedItem { - public int id; - public int order; - public CharSequence title; - public int iconRes; - public boolean checkable; - public boolean checked; - public boolean visible; - public boolean enabled; - public int showAsAction; - public boolean hasSubMenu; - } - - public GeckoMenuInflater(Context context) { - super(context); - mContext = context; - } - - @Override - public void inflate(int menuRes, Menu menu) { - - // This does not check for a well-formed XML. - - XmlResourceParser parser = null; - try { - parser = mContext.getResources().getXml(menuRes); - AttributeSet attrs = Xml.asAttributeSet(parser); - - parseMenu(parser, attrs, menu); - - } catch (XmlPullParserException | IOException e) { - throw new InflateException("Error inflating menu XML", e); - } finally { - if (parser != null) - parser.close(); - } - } - - private void parseMenu(XmlResourceParser parser, AttributeSet attrs, Menu menu) - throws XmlPullParserException, IOException { - ParsedItem item = null; - - String tag; - int eventType = parser.getEventType(); - - do { - tag = parser.getName(); - - switch (eventType) { - case XmlPullParser.START_TAG: - if (tag.equals(TAG_ITEM)) { - // Parse the menu item. - item = new ParsedItem(); - parseItem(item, attrs); - } else if (tag.equals(TAG_MENU)) { - if (item != null) { - // Add the submenu item. - SubMenu subMenu = menu.addSubMenu(NO_ID, item.id, item.order, item.title); - item.hasSubMenu = true; - - // Set the menu item in main menu. - MenuItem menuItem = subMenu.getItem(); - setValues(item, menuItem); - - // Start parsing the sub menu. - parseMenu(parser, attrs, subMenu); - } - } - break; - - case XmlPullParser.END_TAG: - if (parser.getName().equals(TAG_ITEM)) { - if (!item.hasSubMenu) { - // Add the item. - MenuItem menuItem = menu.add(NO_ID, item.id, item.order, item.title); - setValues(item, menuItem); - } - } else if (tag.equals(TAG_MENU)) { - return; - } - break; - } - - eventType = parser.next(); - - } while (eventType != XmlPullParser.END_DOCUMENT); - } - - public void parseItem(ParsedItem item, AttributeSet attrs) { - TypedArray a = mContext.obtainStyledAttributes(attrs, R.styleable.MenuItem); - - item.id = a.getResourceId(R.styleable.MenuItem_android_id, NO_ID); - item.order = a.getInt(R.styleable.MenuItem_android_orderInCategory, 0); - item.title = a.getText(R.styleable.MenuItem_android_title); - item.checkable = a.getBoolean(R.styleable.MenuItem_android_checkable, false); - item.checked = a.getBoolean(R.styleable.MenuItem_android_checked, false); - item.visible = a.getBoolean(R.styleable.MenuItem_android_visible, true); - item.enabled = a.getBoolean(R.styleable.MenuItem_android_enabled, true); - item.hasSubMenu = false; - item.iconRes = a.getResourceId(R.styleable.MenuItem_android_icon, 0); - item.showAsAction = a.getInt(R.styleable.MenuItem_android_showAsAction, 0); - - a.recycle(); - } - - public void setValues(ParsedItem item, MenuItem menuItem) { - // We are blocking any presenter updates during inflation. - GeckoMenuItem geckoItem = null; - if (menuItem instanceof GeckoMenuItem) { - geckoItem = (GeckoMenuItem) menuItem; - } - - if (geckoItem != null) { - geckoItem.stopDispatchingChanges(); - } - - menuItem.setChecked(item.checked) - .setVisible(item.visible) - .setEnabled(item.enabled) - .setCheckable(item.checkable) - .setIcon(item.iconRes); - - menuItem.setShowAsAction(item.showAsAction); - - if (geckoItem != null) { - // We don't need to allow presenter updates during inflation, - // so we use the weak form of re-enabling changes. - geckoItem.resumeDispatchingChanges(); - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/menu/GeckoMenuItem.java b/mobile/android/base/java/org/mozilla/gecko/menu/GeckoMenuItem.java deleted file mode 100644 index 21df4208d6..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/menu/GeckoMenuItem.java +++ /dev/null @@ -1,472 +0,0 @@ -/* 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.menu; - -import org.mozilla.gecko.R; -import org.mozilla.gecko.util.ResourceDrawableUtils; -import org.mozilla.gecko.widget.GeckoActionProvider; - -import android.content.Intent; -import android.graphics.drawable.Drawable; -import android.text.TextUtils; -import android.view.ActionProvider; -import android.view.ContextMenu; -import android.view.MenuItem; -import android.view.SubMenu; -import android.view.View; - -public class GeckoMenuItem implements MenuItem { - private static final int SHARE_BAR_HISTORY_SIZE = 2; - - // These values mirror MenuItem values that are only available on API >= 11. - public static final int SHOW_AS_ACTION_NEVER = 0; - public static final int SHOW_AS_ACTION_IF_ROOM = 1; - public static final int SHOW_AS_ACTION_ALWAYS = 2; - public static final int SHOW_AS_ACTION_WITH_TEXT = 4; - public static final int SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW = 8; - - // A View that can show a MenuItem should be able to initialize from - // the properties of the MenuItem. - public static interface Layout { - public void initialize(GeckoMenuItem item); - public void setShowIcon(boolean show); - } - - public static interface OnShowAsActionChangedListener { - public boolean hasActionItemBar(); - public void onShowAsActionChanged(GeckoMenuItem item); - } - - private final int mId; - private final int mOrder; - private View mActionView; - private int mActionEnum; - private CharSequence mTitle; - private CharSequence mTitleCondensed; - private boolean mCheckable; - private boolean mChecked; - private boolean mVisible = true; - private boolean mEnabled = true; - private Drawable mIcon; - private int mIconRes; - private GeckoActionProvider mActionProvider; - private GeckoSubMenu mSubMenu; - private MenuItem.OnMenuItemClickListener mMenuItemClickListener; - final GeckoMenu mMenu; - OnShowAsActionChangedListener mShowAsActionChangedListener; - - private volatile boolean mShouldDispatchChanges = true; - private volatile boolean mDidChange; - - public GeckoMenuItem(GeckoMenu menu, int id, int order, int titleRes) { - mMenu = menu; - mId = id; - mOrder = order; - mTitle = mMenu.getResources().getString(titleRes); - } - - public GeckoMenuItem(GeckoMenu menu, int id, int order, CharSequence title) { - mMenu = menu; - mId = id; - mOrder = order; - mTitle = title; - } - - /** - * Stop dispatching item changed events to presenters until - * [start|resume]DispatchingItemsChanged() is called. Useful when - * many menu operations are going to be performed as a batch. - */ - public void stopDispatchingChanges() { - mDidChange = false; - mShouldDispatchChanges = false; - } - - /** - * Resume dispatching item changed events to presenters. This method - * will NOT call onItemChanged if any menu operations were queued. - * Only future menu operations will call onItemChanged. Useful for - * sequelching presenter updates. - */ - public void resumeDispatchingChanges() { - mShouldDispatchChanges = true; - } - - /** - * Start dispatching item changed events to presenters. This method - * will call onItemChanged if any menu operations were queued. - */ - public void startDispatchingChanges() { - if (mDidChange) { - mMenu.onItemChanged(this); - } - mShouldDispatchChanges = true; - } - - @Override - public boolean collapseActionView() { - return false; - } - - @Override - public boolean expandActionView() { - return false; - } - - public boolean hasActionProvider() { - return (mActionProvider != null); - } - - public int getActionEnum() { - return mActionEnum; - } - - public GeckoActionProvider getGeckoActionProvider() { - return mActionProvider; - } - - @Override - public ActionProvider getActionProvider() { - return null; - } - - @Override - public View getActionView() { - if (mActionProvider != null) { - return mActionProvider.onCreateActionView(SHARE_BAR_HISTORY_SIZE, - GeckoActionProvider.ActionViewType.DEFAULT); - } - - return mActionView; - } - - @Override - public char getAlphabeticShortcut() { - return 0; - } - - @Override - public int getGroupId() { - return 0; - } - - @Override - public Drawable getIcon() { - if (mIcon == null) { - if (mIconRes != 0) - return ResourceDrawableUtils.getDrawable(mMenu.getContext(), mIconRes); - else - return null; - } else { - return mIcon; - } - } - - @Override - public Intent getIntent() { - return null; - } - - @Override - public int getItemId() { - return mId; - } - - @Override - public ContextMenu.ContextMenuInfo getMenuInfo() { - return null; - } - - @Override - public char getNumericShortcut() { - return 0; - } - - @Override - public int getOrder() { - return mOrder; - } - - @Override - public SubMenu getSubMenu() { - return mSubMenu; - } - - @Override - public CharSequence getTitle() { - return mTitle; - } - - @Override - public CharSequence getTitleCondensed() { - return mTitleCondensed; - } - - @Override - public boolean hasSubMenu() { - if (mActionProvider != null) - return mActionProvider.hasSubMenu(); - - return (mSubMenu != null); - } - - public boolean isActionItem() { - return (mActionEnum > 0); - } - - @Override - public boolean isActionViewExpanded() { - return false; - } - - @Override - public boolean isCheckable() { - return mCheckable; - } - - @Override - public boolean isChecked() { - return mChecked; - } - - @Override - public boolean isEnabled() { - return mEnabled; - } - - @Override - public boolean isVisible() { - return mVisible; - } - - @Override - public MenuItem setActionProvider(ActionProvider actionProvider) { - return this; - } - - public MenuItem setActionProvider(GeckoActionProvider actionProvider) { - mActionProvider = actionProvider; - if (mActionProvider != null) { - actionProvider.setOnTargetSelectedListener(new GeckoActionProvider.OnTargetSelectedListener() { - @Override - public void onTargetSelected() { - mMenu.close(); - - // Refresh the menu item to show the high frequency apps. - mShowAsActionChangedListener.onShowAsActionChanged(GeckoMenuItem.this); - } - }); - } - - mShowAsActionChangedListener.onShowAsActionChanged(this); - return this; - } - - @Override - public MenuItem setActionView(int resId) { - return this; - } - - @Override - public MenuItem setActionView(View view) { - return this; - } - - @Override - public MenuItem setAlphabeticShortcut(char alphaChar) { - return this; - } - - @Override - public MenuItem setCheckable(boolean checkable) { - if (mCheckable != checkable) { - mCheckable = checkable; - if (mShouldDispatchChanges) { - mMenu.onItemChanged(this); - } else { - mDidChange = true; - } - } - return this; - } - - @Override - public MenuItem setChecked(boolean checked) { - if (mChecked != checked) { - mChecked = checked; - if (mShouldDispatchChanges) { - mMenu.onItemChanged(this); - } else { - mDidChange = true; - } - } - return this; - } - - @Override - public MenuItem setEnabled(boolean enabled) { - if (mEnabled != enabled) { - mEnabled = enabled; - if (mShouldDispatchChanges) { - mMenu.onItemChanged(this); - } else { - mDidChange = true; - } - } - return this; - } - - @Override - public MenuItem setIcon(Drawable icon) { - if (mIcon != icon) { - mIcon = icon; - if (mShouldDispatchChanges) { - mMenu.onItemChanged(this); - } else { - mDidChange = true; - } - } - return this; - } - - @Override - public MenuItem setIcon(int iconRes) { - if (mIconRes != iconRes) { - mIconRes = iconRes; - if (mShouldDispatchChanges) { - mMenu.onItemChanged(this); - } else { - mDidChange = true; - } - } - return this; - } - - @Override - public MenuItem setIntent(Intent intent) { - return this; - } - - @Override - public MenuItem setNumericShortcut(char numericChar) { - return this; - } - - @Override - public MenuItem setOnActionExpandListener(MenuItem.OnActionExpandListener listener) { - return this; - } - - @Override - public MenuItem setOnMenuItemClickListener(MenuItem.OnMenuItemClickListener menuItemClickListener) { - mMenuItemClickListener = menuItemClickListener; - return this; - } - - @Override - public MenuItem setShortcut(char numericChar, char alphaChar) { - return this; - } - - @Override - public void setShowAsAction(int actionEnum) { - setShowAsAction(actionEnum, 0); - } - - public void setShowAsAction(int actionEnum, int style) { - if (mShowAsActionChangedListener == null) - return; - - if (mActionEnum == actionEnum) - return; - - if (actionEnum > 0) { - if (!mShowAsActionChangedListener.hasActionItemBar()) - return; - - if (!hasActionProvider()) { - // Change the type to just an icon - MenuItemActionBar actionView; - if (style != 0) { - actionView = new MenuItemActionBar(mMenu.getContext(), null, style); - } else { - if (actionEnum == SHOW_AS_ACTION_ALWAYS) { - actionView = new MenuItemActionBar(mMenu.getContext()); - } else { - actionView = new MenuItemActionBar(mMenu.getContext(), null, R.attr.menuItemSecondaryActionBarStyle); - } - } - - actionView.initialize(this); - mActionView = actionView; - } - - mActionEnum = actionEnum; - } - - mShowAsActionChangedListener.onShowAsActionChanged(this); - } - - @Override - public MenuItem setShowAsActionFlags(int actionEnum) { - return this; - } - - public MenuItem setSubMenu(GeckoSubMenu subMenu) { - mSubMenu = subMenu; - return this; - } - - @Override - public MenuItem setTitle(CharSequence title) { - if (!TextUtils.equals(mTitle, title)) { - mTitle = title; - if (mShouldDispatchChanges) { - mMenu.onItemChanged(this); - } else { - mDidChange = true; - } - } - return this; - } - - @Override - public MenuItem setTitle(int title) { - CharSequence newTitle = mMenu.getResources().getString(title); - return setTitle(newTitle); - } - - @Override - public MenuItem setTitleCondensed(CharSequence title) { - mTitleCondensed = title; - return this; - } - - @Override - public MenuItem setVisible(boolean visible) { - // Action views are not normal menu items and visibility can get out - // of sync unless we dispatch whenever required. - if (isActionItem() || mVisible != visible) { - mVisible = visible; - if (mShouldDispatchChanges) { - mMenu.onItemChanged(this); - } else { - mDidChange = true; - } - } - return this; - } - - public boolean invoke() { - if (mMenuItemClickListener != null) - return mMenuItemClickListener.onMenuItemClick(this); - else - return false; - } - - public void setOnShowAsActionChangedListener(OnShowAsActionChangedListener listener) { - mShowAsActionChangedListener = listener; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/menu/GeckoSubMenu.java b/mobile/android/base/java/org/mozilla/gecko/menu/GeckoSubMenu.java deleted file mode 100644 index d774bdd370..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/menu/GeckoSubMenu.java +++ /dev/null @@ -1,81 +0,0 @@ -/* 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.menu; - -import android.content.Context; -import android.graphics.drawable.Drawable; -import android.util.AttributeSet; -import android.view.MenuItem; -import android.view.SubMenu; -import android.view.View; - -public class GeckoSubMenu extends GeckoMenu - implements SubMenu { - private static final String LOGTAG = "GeckoSubMenu"; - - // MenuItem associated with this submenu. - private MenuItem mMenuItem; - - public GeckoSubMenu(Context context) { - super(context); - } - - public GeckoSubMenu(Context context, AttributeSet attrs) { - super(context, attrs); - } - - public GeckoSubMenu(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - } - - @Override - public void clearHeader() { - } - - public SubMenu setMenuItem(MenuItem item) { - mMenuItem = item; - return this; - } - - @Override - public MenuItem getItem() { - return mMenuItem; - } - - @Override - public SubMenu setHeaderIcon(Drawable icon) { - return this; - } - - @Override - public SubMenu setHeaderIcon(int iconRes) { - return this; - } - - @Override - public SubMenu setHeaderTitle(CharSequence title) { - return this; - } - - @Override - public SubMenu setHeaderTitle(int titleRes) { - return this; - } - - @Override - public SubMenu setHeaderView(View view) { - return this; - } - - @Override - public SubMenu setIcon(Drawable icon) { - return this; - } - - @Override - public SubMenu setIcon(int iconRes) { - return this; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/menu/MenuItemActionBar.java b/mobile/android/base/java/org/mozilla/gecko/menu/MenuItemActionBar.java deleted file mode 100644 index 882187dd60..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/menu/MenuItemActionBar.java +++ /dev/null @@ -1,64 +0,0 @@ -/* 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.menu; - -import org.mozilla.gecko.R; -import org.mozilla.gecko.util.ResourceDrawableUtils; -import org.mozilla.gecko.widget.themed.ThemedImageButton; - -import android.content.Context; -import android.graphics.drawable.Drawable; -import android.util.AttributeSet; - -public class MenuItemActionBar extends ThemedImageButton - implements GeckoMenuItem.Layout { - private static final String LOGTAG = "GeckoMenuItemActionBar"; - - public MenuItemActionBar(Context context) { - this(context, null); - } - - public MenuItemActionBar(Context context, AttributeSet attrs) { - this(context, attrs, R.attr.menuItemActionBarStyle); - } - - public MenuItemActionBar(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - } - - @Override - public void initialize(GeckoMenuItem item) { - if (item == null) - return; - - setIcon(item.getIcon()); - setTitle(item.getTitle()); - setEnabled(item.isEnabled()); - setId(item.getItemId()); - } - - void setIcon(Drawable icon) { - if (icon == null) { - setVisibility(GONE); - } else { - setVisibility(VISIBLE); - setImageDrawable(icon); - } - } - - void setIcon(int icon) { - setIcon((icon == 0) ? null : ResourceDrawableUtils.getDrawable(getContext(), icon)); - } - - void setTitle(CharSequence title) { - // set accessibility contentDescription here - setContentDescription(title); - } - - @Override - public void setShowIcon(boolean show) { - // Do nothing. - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/menu/MenuItemDefault.java b/mobile/android/base/java/org/mozilla/gecko/menu/MenuItemDefault.java deleted file mode 100644 index 5b50693348..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/menu/MenuItemDefault.java +++ /dev/null @@ -1,152 +0,0 @@ -/* 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.menu; - -import org.mozilla.gecko.R; -import org.mozilla.gecko.util.ResourceDrawableUtils; - -import android.content.Context; -import android.content.res.Resources; -import android.graphics.Rect; -import android.graphics.drawable.Drawable; -import android.util.AttributeSet; -import android.widget.TextView; - -public class MenuItemDefault extends TextView - implements GeckoMenuItem.Layout { - private static final int[] STATE_MORE = new int[] { R.attr.state_more }; - private static final int[] STATE_CHECKED = new int[] { android.R.attr.state_checkable, android.R.attr.state_checked }; - private static final int[] STATE_UNCHECKED = new int[] { android.R.attr.state_checkable }; - - private Drawable mIcon; - private final Drawable mState; - private static Rect sIconBounds; - - private boolean mCheckable; - private boolean mChecked; - private boolean mHasSubMenu; - private boolean mShowIcon; - - public MenuItemDefault(Context context) { - this(context, null); - } - - public MenuItemDefault(Context context, AttributeSet attrs) { - this(context, attrs, R.attr.menuItemDefaultStyle); - } - - public MenuItemDefault(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - - Resources res = getResources(); - int width = res.getDimensionPixelSize(R.dimen.menu_item_row_width); - int height = res.getDimensionPixelSize(R.dimen.menu_item_row_height); - setMinimumWidth(width); - setMinimumHeight(height); - - int stateIconSize = res.getDimensionPixelSize(R.dimen.menu_item_state_icon); - Rect stateIconBounds = new Rect(0, 0, stateIconSize, stateIconSize); - - mState = res.getDrawable(R.drawable.menu_item_state).mutate(); - mState.setBounds(stateIconBounds); - - if (sIconBounds == null) { - int iconSize = res.getDimensionPixelSize(R.dimen.menu_item_icon); - sIconBounds = new Rect(0, 0, iconSize, iconSize); - } - - setCompoundDrawables(mIcon, null, mState, null); - } - - @Override - public int[] onCreateDrawableState(int extraSpace) { - final int[] drawableState = super.onCreateDrawableState(extraSpace + 2); - - if (mHasSubMenu) - mergeDrawableStates(drawableState, STATE_MORE); - else if (mCheckable && mChecked) - mergeDrawableStates(drawableState, STATE_CHECKED); - else if (mCheckable && !mChecked) - mergeDrawableStates(drawableState, STATE_UNCHECKED); - - return drawableState; - } - - @Override - public void initialize(GeckoMenuItem item) { - if (item == null) - return; - - setTitle(item.getTitle()); - setIcon(item.getIcon()); - setEnabled(item.isEnabled()); - setCheckable(item.isCheckable()); - setChecked(item.isChecked()); - setSubMenuIndicator(item.hasSubMenu()); - } - - private void refreshIcon() { - setCompoundDrawables(mShowIcon ? mIcon : null, null, mState, null); - } - - void setIcon(Drawable icon) { - mIcon = icon; - - if (mIcon != null) { - mIcon.setBounds(sIconBounds); - mIcon.setAlpha(isEnabled() ? 255 : 99); - } - - refreshIcon(); - } - - void setIcon(int icon) { - setIcon((icon == 0) ? null : ResourceDrawableUtils.getDrawable(getContext(), icon)); - } - - void setTitle(CharSequence title) { - setText(title); - } - - @Override - public void setEnabled(boolean enabled) { - super.setEnabled(enabled); - - if (mIcon != null) - mIcon.setAlpha(enabled ? 255 : 99); - - if (mState != null) - mState.setAlpha(enabled ? 255 : 99); - } - - private void setCheckable(boolean checkable) { - if (mCheckable != checkable) { - mCheckable = checkable; - refreshDrawableState(); - } - } - - private void setChecked(boolean checked) { - if (mChecked != checked) { - mChecked = checked; - refreshDrawableState(); - } - } - - @Override - public void setShowIcon(boolean show) { - if (mShowIcon != show) { - mShowIcon = show; - refreshIcon(); - } - } - - void setSubMenuIndicator(boolean hasSubMenu) { - if (mHasSubMenu != hasSubMenu) { - mHasSubMenu = hasSubMenu; - refreshDrawableState(); - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/menu/MenuItemSwitcherLayout.java b/mobile/android/base/java/org/mozilla/gecko/menu/MenuItemSwitcherLayout.java deleted file mode 100644 index d01f526876..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/menu/MenuItemSwitcherLayout.java +++ /dev/null @@ -1,188 +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.menu; - -import java.util.ArrayList; -import java.util.List; - -import org.mozilla.gecko.R; - -import android.annotation.TargetApi; -import android.content.Context; -import android.graphics.drawable.Drawable; -import android.util.AttributeSet; -import android.view.LayoutInflater; -import android.view.View; -import android.widget.ImageButton; -import android.widget.LinearLayout; - -/** - * This class is a container view for menu items that: - * * Shows text if there is enough space and there are - * no action buttons ({@link #mActionButtons}). - * * Shows an icon if there is not enough space for text, - * or there are action buttons. - */ -public class MenuItemSwitcherLayout extends LinearLayout - implements GeckoMenuItem.Layout, - View.OnClickListener { - private final MenuItemDefault mMenuItem; - private final MenuItemActionBar mMenuButton; - private final List mActionButtons; - private final List mActionButtonListeners = new ArrayList(); - - public MenuItemSwitcherLayout(Context context) { - this(context, null); - } - - public MenuItemSwitcherLayout(Context context, AttributeSet attrs) { - this(context, attrs, R.attr.menuItemSwitcherLayoutStyle); - } - - @TargetApi(14) - public MenuItemSwitcherLayout(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs); - - LayoutInflater.from(context).inflate(R.layout.menu_item_switcher_layout, this); - mMenuItem = (MenuItemDefault) findViewById(R.id.menu_item); - mMenuButton = (MenuItemActionBar) findViewById(R.id.menu_item_button); - mActionButtons = new ArrayList(); - } - - @Override - protected void onLayout(boolean changed, int left, int top, int right, int bottom) { - final int width = right - left; - - final View parent = (View) getParent(); - final int parentPadding = parent.getPaddingLeft() + parent.getPaddingRight(); - final int horizontalSpaceAvailableInParent = parent.getMeasuredWidth() - parentPadding; - - // Check if there is another View sharing horizontal - // space with this View in the parent. - if (width < horizontalSpaceAvailableInParent || mActionButtons.size() != 0) { - // Use the icon. - mMenuItem.setVisibility(View.GONE); - mMenuButton.setVisibility(View.VISIBLE); - } else { - // Use the button. - mMenuItem.setVisibility(View.VISIBLE); - mMenuButton.setVisibility(View.GONE); - } - - super.onLayout(changed, left, top, right, bottom); - } - - @Override - public void initialize(GeckoMenuItem item) { - if (item == null) { - return; - } - - mMenuItem.initialize(item); - mMenuButton.initialize(item); - setEnabled(item.isEnabled()); - } - - @Override - public void setEnabled(boolean enabled) { - super.setEnabled(enabled); - mMenuItem.setEnabled(enabled); - mMenuButton.setEnabled(enabled); - - for (ImageButton button : mActionButtons) { - button.setEnabled(enabled); - button.setAlpha(enabled ? 255 : 99); - } - } - - public void setMenuItemClickListener(View.OnClickListener listener) { - mMenuItem.setOnClickListener(listener); - mMenuButton.setOnClickListener(listener); - } - - public void setMenuItemLongClickListener(View.OnLongClickListener listener) { - mMenuItem.setOnLongClickListener(listener); - mMenuButton.setOnLongClickListener(listener); - } - - public void addActionButtonClickListener(View.OnClickListener listener) { - mActionButtonListeners.add(listener); - } - - @Override - public void setShowIcon(boolean show) { - mMenuItem.setShowIcon(show); - } - - public void setIcon(Drawable icon) { - mMenuItem.setIcon(icon); - mMenuButton.setIcon(icon); - } - - public void setIcon(int icon) { - mMenuItem.setIcon(icon); - mMenuButton.setIcon(icon); - } - - public void setTitle(CharSequence title) { - mMenuItem.setTitle(title); - mMenuButton.setContentDescription(title); - } - - public void setSubMenuIndicator(boolean hasSubMenu) { - mMenuItem.setSubMenuIndicator(hasSubMenu); - } - - public void addActionButton(Drawable drawable, CharSequence label) { - // If this is the first icon, retain the text. - // If not, make the menu item an icon. - final int count = mActionButtons.size(); - mMenuItem.setVisibility(View.GONE); - mMenuButton.setVisibility(View.VISIBLE); - - if (drawable != null) { - ImageButton button = new ImageButton(getContext(), null, R.attr.menuItemSecondaryActionBarStyle); - button.setImageDrawable(drawable); - button.setContentDescription(label); - button.setOnClickListener(this); - button.setTag(count); - - final int height = (int) (getResources().getDimension(R.dimen.menu_item_row_height)); - LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(0, height); - params.weight = 1.0f; - button.setLayoutParams(params); - - // Place action buttons to the right of the actual menu item - mActionButtons.add(button); - addView(button, count + 1); - } - } - - protected int getActionButtonCount() { - return mActionButtons.size(); - } - - @Override - public void onClick(View view) { - for (View.OnClickListener listener : mActionButtonListeners) { - listener.onClick(view); - } - } - - /** - * Update the styles if this view is being used in the context menus. - * - * Ideally, we just use different layout files and styles to set this, but - * MenuItemSwitcherLayout is too integrated into GeckoActionProvider to provide - * an easy separation so instead I provide this hack. I'm sorry. - */ - public void initContextMenuStyles() { - final int defaultContextMenuPadding = getContext().getResources().getDimensionPixelOffset( - R.dimen.context_menu_item_horizontal_padding); - mMenuItem.setPadding(defaultContextMenuPadding, getPaddingTop(), - defaultContextMenuPadding, getPaddingBottom()); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/menu/MenuPanel.java b/mobile/android/base/java/org/mozilla/gecko/menu/MenuPanel.java deleted file mode 100644 index ce4da8b7fc..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/menu/MenuPanel.java +++ /dev/null @@ -1,36 +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.menu; - -import org.mozilla.gecko.AppConstants.Versions; -import org.mozilla.gecko.R; - -import android.content.Context; -import android.util.AttributeSet; -import android.view.ViewGroup; -import android.view.accessibility.AccessibilityEvent; -import android.widget.LinearLayout; - -/** - * The outer container for the custom menu. On phones with h/w menu button, - * this is given to Android which inflates it to the right panel. On phones - * with s/w menu button, this is added to a MenuPopup. - */ -public class MenuPanel extends LinearLayout { - public MenuPanel(Context context, AttributeSet attrs) { - super(context, attrs); - - int width = (int) context.getResources().getDimension(R.dimen.menu_item_row_width); - setLayoutParams(new ViewGroup.LayoutParams(width, ViewGroup.LayoutParams.WRAP_CONTENT)); - } - - @Override - public boolean dispatchPopulateAccessibilityEvent (AccessibilityEvent event) { - onPopulateAccessibilityEvent(event); - - return true; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/menu/MenuPopup.java b/mobile/android/base/java/org/mozilla/gecko/menu/MenuPopup.java deleted file mode 100644 index 227cc76309..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/menu/MenuPopup.java +++ /dev/null @@ -1,76 +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.menu; - -import org.mozilla.gecko.AppConstants; -import org.mozilla.gecko.R; - -import android.content.Context; -import android.graphics.Color; -import android.graphics.drawable.ColorDrawable; -import android.support.v7.widget.CardView; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.FrameLayout; -import android.widget.PopupWindow; - -/** - * A popup to show the inflated MenuPanel. - */ -public class MenuPopup extends PopupWindow { - private final CardView mPanel; - - private final int mPopupWidth; - - public MenuPopup(Context context) { - super(context); - - setFocusable(true); - - mPopupWidth = context.getResources().getDimensionPixelSize(R.dimen.menu_popup_width); - - // Setting a null background makes the popup to not close on touching outside. - setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT)); - setWindowLayoutMode(ViewGroup.LayoutParams.WRAP_CONTENT, - ViewGroup.LayoutParams.WRAP_CONTENT); - - LayoutInflater inflater = LayoutInflater.from(context); - mPanel = (CardView) inflater.inflate(R.layout.menu_popup, null); - setContentView(mPanel); - - setAnimationStyle(R.style.PopupAnimation); - } - - /** - * Adds the panel with the menu to its content. - * - * @param view The panel view with the menu to be shown. - */ - public void setPanelView(View view) { - view.setLayoutParams(new FrameLayout.LayoutParams(mPopupWidth, - FrameLayout.LayoutParams.WRAP_CONTENT)); - - mPanel.removeAllViews(); - mPanel.addView(view); - } - - /** - * A small little offset. - */ - @Override - public void showAsDropDown(View anchor) { - // Set a height, so that the popup will not be displayed below the bottom of the screen. - // We use the exact height of the internal content, which is the technique described in - // http://stackoverflow.com/a/7698709 - setHeight(mPanel.getHeight()); - - // Attempt to align the center of the popup with the center of the anchor. If the anchor is - // near the edge of the screen, the popup will just align with the edge of the screen. - final int xOffset = anchor.getWidth() / 2 - mPopupWidth / 2; - showAsDropDown(anchor, xOffset, -anchor.getHeight()); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/mozglue/SharedMemBuffer.java b/mobile/android/base/java/org/mozilla/gecko/mozglue/SharedMemBuffer.java deleted file mode 100644 index cf22685c2c..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/mozglue/SharedMemBuffer.java +++ /dev/null @@ -1,81 +0,0 @@ -/* 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.mozglue; - -import android.os.Parcel; - -import org.mozilla.gecko.media.Sample; - -import java.io.IOException; -import java.nio.ByteBuffer; - -public final class SharedMemBuffer implements Sample.Buffer { - private SharedMemory mSharedMem; - - /* package */ - public SharedMemBuffer(SharedMemory sharedMem) { - mSharedMem = sharedMem; - } - - protected SharedMemBuffer(Parcel in) { - mSharedMem = in.readParcelable(Sample.class.getClassLoader()); - } - - @Override - public void writeToParcel(Parcel dest, int flags) { - dest.writeParcelable(mSharedMem, flags); - } - - @Override - public int describeContents() { - return 0; - } - - public static final Creator CREATOR = new Creator() { - @Override - public SharedMemBuffer createFromParcel(Parcel in) { - return new SharedMemBuffer(in); - } - - @Override - public SharedMemBuffer[] newArray(int size) { - return new SharedMemBuffer[size]; - } - }; - - @Override - public int capacity() { - return mSharedMem != null ? mSharedMem.getSize() : 0; - } - - @Override - public void readFromByteBuffer(ByteBuffer src, int offset, int size) throws IOException { - if (!src.isDirect()) { - throw new IOException("SharedMemBuffer only support reading from direct byte buffer."); - } - nativeReadFromDirectBuffer(src, mSharedMem.getPointer(), offset, size); - } - - private native static void nativeReadFromDirectBuffer(ByteBuffer src, long dest, int offset, int size); - - @Override - public void writeToByteBuffer(ByteBuffer dest, int offset, int size) throws IOException { - if (!dest.isDirect()) { - throw new IOException("SharedMemBuffer only support writing to direct byte buffer."); - } - nativeWriteToDirectBuffer(mSharedMem.getPointer(), dest, offset, size); - } - - private native static void nativeWriteToDirectBuffer(long src, ByteBuffer dest, int offset, int size); - - @Override - public void dispose() { - if (mSharedMem != null) { - mSharedMem.dispose(); - mSharedMem = null; - } - } - - @Override public String toString() { return "Buffer: " + mSharedMem; } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/mozglue/SharedMemory.java b/mobile/android/base/java/org/mozilla/gecko/mozglue/SharedMemory.java deleted file mode 100644 index bc43a27554..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/mozglue/SharedMemory.java +++ /dev/null @@ -1,171 +0,0 @@ -/* 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.mozglue; - -import android.os.MemoryFile; -import android.os.Parcel; -import android.os.ParcelFileDescriptor; -import android.os.Parcelable; -import android.util.Log; - -import java.io.FileDescriptor; -import java.io.IOException; -import java.lang.reflect.Method; - -public class SharedMemory implements Parcelable { - private static final String LOGTAG = "GeckoShmem"; - private static Method sGetFDMethod = null; // MemoryFile.getFileDescriptor() is hidden. :( - private ParcelFileDescriptor mDescriptor; - private int mSize; - private int mId; - private long mHandle; // The native pointer. - private boolean mIsMapped; - private MemoryFile mBackedFile; - - static { - try { - sGetFDMethod = MemoryFile.class.getDeclaredMethod("getFileDescriptor"); - } catch (NoSuchMethodException e) { - e.printStackTrace(); - } - } - - private SharedMemory(Parcel in) { - mDescriptor = in.readFileDescriptor(); - mSize = in.readInt(); - mId = in.readInt(); - } - - public static final Creator CREATOR = new Creator() { - @Override - public SharedMemory createFromParcel(Parcel in) { - return new SharedMemory(in); - } - - @Override - public SharedMemory[] newArray(int size) { - return new SharedMemory[size]; - } - }; - - @Override - public int describeContents() { - return 0; - } - - @Override - public void writeToParcel(Parcel dest, int flags) { - // We don't want ParcelFileDescriptor.writeToParcel() to close the fd. - dest.writeFileDescriptor(mDescriptor.getFileDescriptor()); - dest.writeInt(mSize); - dest.writeInt(mId); - } - - public SharedMemory(int id, int size) throws NoSuchMethodException, IOException { - if (sGetFDMethod == null) { - throw new NoSuchMethodException("MemoryFile.getFileDescriptor() doesn't exist."); - } - mBackedFile = new MemoryFile(null, size); - try { - FileDescriptor fd = (FileDescriptor)sGetFDMethod.invoke(mBackedFile); - mDescriptor = ParcelFileDescriptor.dup(fd); - mSize = size; - mId = id; - mBackedFile.allowPurging(false); - } catch (Exception e) { - e.printStackTrace(); - close(); - throw new IOException(e.getMessage()); - } - } - - public void flush() { - if (mBackedFile == null) { - close(); - } - } - - public void close() { - if (mIsMapped) { - unmap(mHandle, mSize); - mHandle = 0; - } - - if (mDescriptor != null) { - try { - mDescriptor.close(); - } catch (IOException e) { - e.printStackTrace(); - } - mDescriptor = null; - } - } - - // Should only be called by process that allocates shared memory. - public void dispose() { - if (!isValid()) { - return; - } - - close(); - - if (mBackedFile != null) { - mBackedFile.close(); - mBackedFile = null; - } - } - - private native void unmap(long address, int size); - - public boolean isValid() { return mDescriptor != null; } - - public int getSize() { return mSize; } - - private int getFD() { - return isValid() ? mDescriptor.getFd() : -1; - } - - public long getPointer() { - if (!isValid()) { - return 0; - } - - if (!mIsMapped) { - mHandle = map(getFD(), mSize); - if (mHandle != 0) { - mIsMapped = true; - } - } - return mHandle; - } - - private native long map(int fd, int size); - - @Override - protected void finalize() throws Throwable { - if (mBackedFile != null) { - Log.w(LOGTAG, "dispose() not called before finalizing"); - } - dispose(); - - super.finalize(); - } - - @Override - public String toString() { - return "SHM(" + getSize() + " bytes): id=" + mId + ", backing=" + mBackedFile + ",fd=" + mDescriptor; - } - - @Override - public boolean equals(Object that) { - return (this == that) || - ((that instanceof SharedMemory) && (hashCode() == that.hashCode())); - } - - @Override - public int hashCode() { - return mId; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/notifications/NotificationClient.java b/mobile/android/base/java/org/mozilla/gecko/notifications/NotificationClient.java deleted file mode 100644 index c46c010507..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/notifications/NotificationClient.java +++ /dev/null @@ -1,324 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.notifications; - -import android.app.Notification; -import android.app.PendingIntent; -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.graphics.Bitmap; -import android.net.Uri; -import android.support.v4.app.NotificationCompat; -import android.support.v4.app.NotificationManagerCompat; -import android.util.Log; - -import java.util.HashMap; - -import org.mozilla.gecko.AppConstants; -import org.mozilla.gecko.GeckoApp; -import org.mozilla.gecko.GeckoAppShell; -import org.mozilla.gecko.GeckoService; -import org.mozilla.gecko.NotificationListener; -import org.mozilla.gecko.R; -import org.mozilla.gecko.gfx.BitmapUtils; - -/** - * Client for posting notifications. - */ -public final class NotificationClient implements NotificationListener { - private static final String LOGTAG = "GeckoNotificationClient"; - /* package */ static final String CLICK_ACTION = AppConstants.ANDROID_PACKAGE_NAME + ".NOTIFICATION_CLICK"; - /* package */ static final String CLOSE_ACTION = AppConstants.ANDROID_PACKAGE_NAME + ".NOTIFICATION_CLOSE"; - /* package */ static final String PERSISTENT_INTENT_EXTRA = "persistentIntent"; - - private final Context mContext; - private final NotificationManagerCompat mNotificationManager; - - private final HashMap mNotifications = new HashMap<>(); - - /** - * Notification associated with this service's foreground state. - * - * {@link android.app.Service#startForeground(int, android.app.Notification)} - * associates the foreground with exactly one notification from the service. - * To keep Fennec alive during downloads (and to make sure it can be killed - * once downloads are complete), we make sure that the foreground is always - * associated with an active progress notification if and only if at least - * one download is in progress. - */ - private String mForegroundNotification; - - public NotificationClient(Context context) { - mContext = context.getApplicationContext(); - mNotificationManager = NotificationManagerCompat.from(mContext); - } - - @Override // NotificationListener - public void showNotification(String name, String cookie, String title, - String text, String host, String imageUrl) { - showNotification(name, cookie, title, text, host, imageUrl, /* data */ null); - } - - @Override // NotificationListener - public void showPersistentNotification(String name, String cookie, String title, - String text, String host, String imageUrl, - String data) { - showNotification(name, cookie, title, text, host, imageUrl, data != null ? data : ""); - } - - private void showNotification(String name, String cookie, String title, - String text, String host, String imageUrl, - String persistentData) { - // Put the strings into the intent as an URI - // "alert:?name=&cookie=" - String packageName = AppConstants.ANDROID_PACKAGE_NAME; - String className = AppConstants.MOZ_ANDROID_BROWSER_INTENT_CLASS; - if (GeckoAppShell.getGeckoInterface() != null) { - final ComponentName comp = GeckoAppShell.getGeckoInterface() - .getActivity().getComponentName(); - packageName = comp.getPackageName(); - className = comp.getClassName(); - } - final Uri dataUri = (new Uri.Builder()) - .scheme("moz-notification") - .authority(packageName) - .path(className) - .appendQueryParameter("name", name) - .appendQueryParameter("cookie", cookie) - .build(); - - final Intent clickIntent = new Intent(CLICK_ACTION); - clickIntent.setClass(mContext, NotificationReceiver.class); - clickIntent.setData(dataUri); - - if (persistentData != null) { - final Intent persistentIntent = GeckoService.getIntentToCreateServices( - mContext, "persistent-notification-click", persistentData); - clickIntent.putExtra(PERSISTENT_INTENT_EXTRA, persistentIntent); - } - - final PendingIntent clickPendingIntent = PendingIntent.getBroadcast( - mContext, 0, clickIntent, PendingIntent.FLAG_UPDATE_CURRENT); - - final Intent closeIntent = new Intent(CLOSE_ACTION); - closeIntent.setClass(mContext, NotificationReceiver.class); - closeIntent.setData(dataUri); - - if (persistentData != null) { - final Intent persistentIntent = GeckoService.getIntentToCreateServices( - mContext, "persistent-notification-close", persistentData); - closeIntent.putExtra(PERSISTENT_INTENT_EXTRA, persistentIntent); - } - - final PendingIntent closePendingIntent = PendingIntent.getBroadcast( - mContext, 0, closeIntent, PendingIntent.FLAG_UPDATE_CURRENT); - - add(name, imageUrl, host, title, text, clickPendingIntent, closePendingIntent); - GeckoAppShell.onNotificationShow(name, cookie); - } - - @Override // NotificationListener - public void closeNotification(String name) - { - remove(name); - } - - /** - * Adds a notification; used for web notifications. - * - * @param name the unique name of the notification - * @param imageUrl URL of the image to use - * @param alertTitle title of the notification - * @param alertText text of the notification - * @param contentIntent Intent used when the notification is clicked - * @param deleteIntent Intent used when the notification is closed - */ - private void add(final String name, final String imageUrl, final String host, - final String alertTitle, final String alertText, - final PendingIntent contentIntent, final PendingIntent deleteIntent) { - final NotificationCompat.Builder builder = new NotificationCompat.Builder(mContext) - .setContentTitle(alertTitle) - .setContentText(alertText) - .setSmallIcon(R.drawable.ic_status_logo) - .setContentIntent(contentIntent) - .setDeleteIntent(deleteIntent) - .setAutoCancel(true) - .setStyle(new NotificationCompat.BigTextStyle() - .bigText(alertText) - .setSummaryText(host)); - - // Fetch icon. - if (!imageUrl.isEmpty()) { - final Bitmap image = BitmapUtils.decodeUrl(imageUrl); - builder.setLargeIcon(image); - } - - builder.setWhen(System.currentTimeMillis()); - final Notification notification = builder.build(); - - synchronized (this) { - mNotifications.put(name, notification); - } - - mNotificationManager.notify(name, 0, notification); - } - - /** - * Adds a notification; used for Fennec app notifications. - * - * @param name the unique name of the notification - * @param notification the Notification to add - */ - public synchronized void add(final String name, final Notification notification) { - final boolean ongoing = isOngoing(notification); - - if (ongoing != isOngoing(mNotifications.get(name))) { - // In order to change notification from ongoing to non-ongoing, or vice versa, - // we have to remove the previous notification, because ongoing notifications - // use a different id value than non-ongoing notifications. - onNotificationClose(name); - } - - mNotifications.put(name, notification); - - if (!ongoing) { - mNotificationManager.notify(name, 0, notification); - return; - } - - // Ongoing - if (mForegroundNotification == null) { - setForegroundNotificationLocked(name, notification); - } else if (mForegroundNotification.equals(name)) { - // Shortcut to update the current foreground notification, instead of - // going through the service. - mNotificationManager.notify(R.id.foregroundNotification, notification); - } - } - - /** - * Updates a notification. - * - * @param name Name of existing notification - * @param progress progress of item being updated - * @param progressMax max progress of item being updated - * @param alertText text of the notification - */ - public void update(final String name, final long progress, - final long progressMax, final String alertText) { - Notification notification; - synchronized (this) { - notification = mNotifications.get(name); - } - if (notification == null) { - return; - } - - notification = new NotificationCompat.Builder(mContext) - .setContentText(alertText) - .setSmallIcon(notification.icon) - .setWhen(notification.when) - .setContentIntent(notification.contentIntent) - .setProgress((int) progressMax, (int) progress, false) - .build(); - - add(name, notification); - } - - /* package */ synchronized Notification onNotificationClose(final String name) { - mNotificationManager.cancel(name, 0); - - final Notification notification = mNotifications.remove(name); - if (notification != null) { - updateForegroundNotificationLocked(name); - } - return notification; - } - - /** - * Removes a notification. - * - * @param name Name of existing notification - */ - public synchronized void remove(final String name) { - final Notification notification = onNotificationClose(name); - if (notification == null || notification.deleteIntent == null) { - return; - } - - // Canceling the notification doesn't trigger the delete intent, so we - // have to trigger it manually. - try { - notification.deleteIntent.send(); - } catch (final PendingIntent.CanceledException e) { - // Ignore. - } - } - - /** - * Determines whether the service is done. - * - * The service is considered finished when all notifications have been - * removed. - * - * @return whether all notifications have been removed - */ - public synchronized boolean isDone() { - return mNotifications.isEmpty(); - } - - /** - * Determines whether a notification should hold a foreground service to keep Gecko alive - * - * @param name the name of the notification to check - * @return whether the notification is ongoing - */ - public synchronized boolean isOngoing(final String name) { - return isOngoing(mNotifications.get(name)); - } - - /** - * Determines whether a notification should hold a foreground service to keep Gecko alive - * - * @param notification the notification to check - * @return whether the notification is ongoing - */ - public boolean isOngoing(final Notification notification) { - if (notification != null && (notification.flags & Notification.FLAG_ONGOING_EVENT) != 0) { - return true; - } - return false; - } - - private void setForegroundNotificationLocked(final String name, - final Notification notification) { - mForegroundNotification = name; - - final Intent intent = new Intent(mContext, NotificationService.class); - intent.putExtra(NotificationService.EXTRA_NOTIFICATION, notification); - mContext.startService(intent); - } - - private void updateForegroundNotificationLocked(final String oldName) { - if (mForegroundNotification == null || !mForegroundNotification.equals(oldName)) { - return; - } - - // If we're removing the notification associated with the - // foreground, we need to pick another active notification to act - // as the foreground notification. - for (final String name : mNotifications.keySet()) { - final Notification notification = mNotifications.get(name); - if (isOngoing(notification)) { - setForegroundNotificationLocked(name, notification); - return; - } - } - - setForegroundNotificationLocked(null, null); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/notifications/NotificationHelper.java b/mobile/android/base/java/org/mozilla/gecko/notifications/NotificationHelper.java deleted file mode 100644 index 1e33031b5e..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/notifications/NotificationHelper.java +++ /dev/null @@ -1,366 +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.notifications; - -import java.util.HashMap; -import java.util.Iterator; - -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; -import org.mozilla.gecko.AppConstants; -import org.mozilla.gecko.EventDispatcher; -import org.mozilla.gecko.GeckoAppShell; -import org.mozilla.gecko.R; -import org.mozilla.gecko.gfx.BitmapUtils; -import org.mozilla.gecko.mozglue.SafeIntent; -import org.mozilla.gecko.util.GeckoEventListener; - -import android.app.PendingIntent; -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.graphics.Bitmap; -import android.net.Uri; -import android.support.v4.app.NotificationCompat; -import android.util.Log; - -public final class NotificationHelper implements GeckoEventListener { - public static final String HELPER_BROADCAST_ACTION = AppConstants.ANDROID_PACKAGE_NAME + ".helperBroadcastAction"; - - public static final String NOTIFICATION_ID = "NotificationHelper_ID"; - private static final String LOGTAG = "GeckoNotificationHelper"; - private static final String HELPER_NOTIFICATION = "helperNotif"; - - // Attributes mandatory to be used while sending a notification from js. - private static final String TITLE_ATTR = "title"; - private static final String TEXT_ATTR = "text"; - /* package */ static final String ID_ATTR = "id"; - private static final String SMALLICON_ATTR = "smallIcon"; - - // Attributes that can be used while sending a notification from js. - private static final String PROGRESS_VALUE_ATTR = "progress_value"; - private static final String PROGRESS_MAX_ATTR = "progress_max"; - private static final String PROGRESS_INDETERMINATE_ATTR = "progress_indeterminate"; - private static final String LIGHT_ATTR = "light"; - private static final String ONGOING_ATTR = "ongoing"; - private static final String WHEN_ATTR = "when"; - private static final String PRIORITY_ATTR = "priority"; - private static final String LARGE_ICON_ATTR = "largeIcon"; - private static final String ACTIONS_ATTR = "actions"; - private static final String ACTION_ID_ATTR = "buttonId"; - private static final String ACTION_TITLE_ATTR = "title"; - private static final String ACTION_ICON_ATTR = "icon"; - private static final String PERSISTENT_ATTR = "persistent"; - private static final String HANDLER_ATTR = "handlerKey"; - private static final String COOKIE_ATTR = "cookie"; - static final String EVENT_TYPE_ATTR = "eventType"; - - private static final String NOTIFICATION_SCHEME = "moz-notification"; - - private static final String BUTTON_EVENT = "notification-button-clicked"; - private static final String CLICK_EVENT = "notification-clicked"; - static final String CLEARED_EVENT = "notification-cleared"; - - static final String ORIGINAL_EXTRA_COMPONENT = "originalComponent"; - - private final Context mContext; - - // Holds a list of notifications that should be cleared if the Fennec Activity is shut down. - // Will not include ongoing or persistent notifications that are tied to Gecko's lifecycle. - private HashMap mClearableNotifications; - - private boolean mInitialized; - private static NotificationHelper sInstance; - - private NotificationHelper(Context context) { - mContext = context; - } - - public void init() { - if (mInitialized) { - return; - } - - mClearableNotifications = new HashMap(); - EventDispatcher.getInstance().registerGeckoThreadListener(this, - "Notification:Show", - "Notification:Hide"); - mInitialized = true; - } - - public static NotificationHelper getInstance(Context context) { - // If someone else created this singleton, but didn't initialize it, something has gone wrong. - if (sInstance != null && !sInstance.mInitialized) { - throw new IllegalStateException("NotificationHelper was created by someone else but not initialized"); - } - - if (sInstance == null) { - sInstance = new NotificationHelper(context.getApplicationContext()); - } - return sInstance; - } - - @Override - public void handleMessage(String event, JSONObject message) { - if (event.equals("Notification:Show")) { - showNotification(message); - } else if (event.equals("Notification:Hide")) { - hideNotification(message); - } - } - - public boolean isHelperIntent(Intent i) { - return i.getBooleanExtra(HELPER_NOTIFICATION, false); - } - - public static void getArgsAndSendNotificationIntent(SafeIntent intent) { - final JSONObject args = new JSONObject(); - final Uri data = intent.getData(); - - final String notificationType = data.getQueryParameter(EVENT_TYPE_ATTR); - - try { - args.put(ID_ATTR, data.getQueryParameter(ID_ATTR)); - args.put(EVENT_TYPE_ATTR, notificationType); - args.put(HANDLER_ATTR, data.getQueryParameter(HANDLER_ATTR)); - args.put(COOKIE_ATTR, intent.getStringExtra(COOKIE_ATTR)); - - if (BUTTON_EVENT.equals(notificationType)) { - final String actionName = data.getQueryParameter(ACTION_ID_ATTR); - args.put(ACTION_ID_ATTR, actionName); - } - - Log.i(LOGTAG, "Send " + args.toString()); - GeckoAppShell.notifyObservers("Notification:Event", args.toString()); - } catch (JSONException e) { - Log.e(LOGTAG, "Error building JSON notification arguments.", e); - } - } - - public void handleNotificationIntent(SafeIntent i) { - final Uri data = i.getData(); - final String notificationType = data.getQueryParameter(EVENT_TYPE_ATTR); - final String id = data.getQueryParameter(ID_ATTR); - if (id == null || notificationType == null) { - Log.e(LOGTAG, "handleNotificationEvent: invalid intent parameters"); - return; - } - - getArgsAndSendNotificationIntent(i); - - // If the notification was clicked, we are closing it. This must be executed after - // sending the event to js side because when the notification is canceled no event can be - // handled. - if (CLICK_EVENT.equals(notificationType) && !i.getBooleanExtra(ONGOING_ATTR, false)) { - // The handler and cookie parameters are optional. - final String handler = data.getQueryParameter(HANDLER_ATTR); - final String cookie = i.getStringExtra(COOKIE_ATTR); - hideNotification(id, handler, cookie); - } - } - - private Uri.Builder getNotificationBuilder(JSONObject message, String type) { - Uri.Builder b = new Uri.Builder(); - b.scheme(NOTIFICATION_SCHEME).appendQueryParameter(EVENT_TYPE_ATTR, type); - - try { - final String id = message.getString(ID_ATTR); - b.appendQueryParameter(ID_ATTR, id); - } catch (JSONException ex) { - Log.i(LOGTAG, "buildNotificationPendingIntent, error parsing", ex); - } - - try { - final String id = message.getString(HANDLER_ATTR); - b.appendQueryParameter(HANDLER_ATTR, id); - } catch (JSONException ex) { - Log.i(LOGTAG, "Notification doesn't have a handler"); - } - - return b; - } - - private Intent buildNotificationIntent(JSONObject message, Uri.Builder builder) { - Intent notificationIntent = new Intent(HELPER_BROADCAST_ACTION); - final boolean ongoing = message.optBoolean(ONGOING_ATTR); - notificationIntent.putExtra(ONGOING_ATTR, ongoing); - - final Uri dataUri = builder.build(); - notificationIntent.setData(dataUri); - notificationIntent.putExtra(HELPER_NOTIFICATION, true); - notificationIntent.putExtra(COOKIE_ATTR, message.optString(COOKIE_ATTR)); - - // All intents get routed through the notificationReceiver. That lets us bail if we don't want to start Gecko - final ComponentName name = new ComponentName(mContext, GeckoAppShell.getGeckoInterface().getActivity().getClass()); - notificationIntent.putExtra(ORIGINAL_EXTRA_COMPONENT, name); - - return notificationIntent; - } - - private PendingIntent buildNotificationPendingIntent(JSONObject message, String type) { - Uri.Builder builder = getNotificationBuilder(message, type); - final Intent notificationIntent = buildNotificationIntent(message, builder); - return PendingIntent.getBroadcast(mContext, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT); - } - - private PendingIntent buildButtonClickPendingIntent(JSONObject message, JSONObject action) { - Uri.Builder builder = getNotificationBuilder(message, BUTTON_EVENT); - try { - // Action name must be in query uri, otherwise buttons pending intents - // would be collapsed. - if (action.has(ACTION_ID_ATTR)) { - builder.appendQueryParameter(ACTION_ID_ATTR, action.getString(ACTION_ID_ATTR)); - } else { - Log.i(LOGTAG, "button event with no name"); - } - } catch (JSONException ex) { - Log.i(LOGTAG, "buildNotificationPendingIntent, error parsing", ex); - } - final Intent notificationIntent = buildNotificationIntent(message, builder); - PendingIntent res = PendingIntent.getBroadcast(mContext, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT); - return res; - } - - private void showNotification(JSONObject message) { - NotificationCompat.Builder builder = new NotificationCompat.Builder(mContext); - - // These attributes are required - final String id; - try { - builder.setContentTitle(message.getString(TITLE_ATTR)); - builder.setContentText(message.getString(TEXT_ATTR)); - id = message.getString(ID_ATTR); - } catch (JSONException ex) { - Log.i(LOGTAG, "Error parsing", ex); - return; - } - - Uri imageUri = Uri.parse(message.optString(SMALLICON_ATTR)); - builder.setSmallIcon(BitmapUtils.getResource(mContext, imageUri)); - - JSONArray light = message.optJSONArray(LIGHT_ATTR); - if (light != null && light.length() == 3) { - try { - builder.setLights(light.getInt(0), - light.getInt(1), - light.getInt(2)); - } catch (JSONException ex) { - Log.i(LOGTAG, "Error parsing", ex); - } - } - - boolean ongoing = message.optBoolean(ONGOING_ATTR); - builder.setOngoing(ongoing); - - if (message.has(WHEN_ATTR)) { - long when = message.optLong(WHEN_ATTR); - builder.setWhen(when); - } - - if (message.has(PRIORITY_ATTR)) { - int priority = message.optInt(PRIORITY_ATTR); - builder.setPriority(priority); - } - - if (message.has(LARGE_ICON_ATTR)) { - Bitmap b = BitmapUtils.getBitmapFromDataURI(message.optString(LARGE_ICON_ATTR)); - builder.setLargeIcon(b); - } - - if (message.has(PROGRESS_VALUE_ATTR) && - message.has(PROGRESS_MAX_ATTR) && - message.has(PROGRESS_INDETERMINATE_ATTR)) { - try { - final int progress = message.getInt(PROGRESS_VALUE_ATTR); - final int progressMax = message.getInt(PROGRESS_MAX_ATTR); - final boolean progressIndeterminate = message.getBoolean(PROGRESS_INDETERMINATE_ATTR); - builder.setProgress(progressMax, progress, progressIndeterminate); - } catch (JSONException ex) { - Log.i(LOGTAG, "Error parsing", ex); - } - } - - JSONArray actions = message.optJSONArray(ACTIONS_ATTR); - if (actions != null) { - try { - for (int i = 0; i < actions.length(); i++) { - JSONObject action = actions.getJSONObject(i); - final PendingIntent pending = buildButtonClickPendingIntent(message, action); - final String actionTitle = action.getString(ACTION_TITLE_ATTR); - final Uri actionImage = Uri.parse(action.optString(ACTION_ICON_ATTR)); - builder.addAction(BitmapUtils.getResource(mContext, actionImage), - actionTitle, - pending); - } - } catch (JSONException ex) { - Log.i(LOGTAG, "Error parsing", ex); - } - } - - PendingIntent pi = buildNotificationPendingIntent(message, CLICK_EVENT); - builder.setContentIntent(pi); - PendingIntent deletePendingIntent = buildNotificationPendingIntent(message, CLEARED_EVENT); - builder.setDeleteIntent(deletePendingIntent); - - ((NotificationClient) GeckoAppShell.getNotificationListener()).add(id, builder.build()); - - boolean persistent = message.optBoolean(PERSISTENT_ATTR); - // We add only not persistent notifications to the list since we want to purge only - // them when geckoapp is destroyed. - if (!persistent && !mClearableNotifications.containsKey(id)) { - mClearableNotifications.put(id, message.toString()); - } - } - - private void hideNotification(JSONObject message) { - final String id; - final String handler; - final String cookie; - try { - id = message.getString("id"); - handler = message.optString("handlerKey"); - cookie = message.optString("cookie"); - } catch (JSONException ex) { - Log.i(LOGTAG, "Error parsing", ex); - return; - } - - hideNotification(id, handler, cookie); - } - - private void closeNotification(String id, String handlerKey, String cookie) { - ((NotificationClient) GeckoAppShell.getNotificationListener()).remove(id); - } - - public void hideNotification(String id, String handlerKey, String cookie) { - mClearableNotifications.remove(id); - closeNotification(id, handlerKey, cookie); - } - - private void clearAll() { - for (Iterator i = mClearableNotifications.keySet().iterator(); i.hasNext();) { - final String id = i.next(); - final String json = mClearableNotifications.get(id); - i.remove(); - - JSONObject obj; - try { - obj = new JSONObject(json); - } catch (JSONException ex) { - obj = new JSONObject(); - } - - closeNotification(id, obj.optString(HANDLER_ATTR), obj.optString(COOKIE_ATTR)); - } - } - - public static void destroy() { - if (sInstance != null) { - sInstance.clearAll(); - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/notifications/NotificationReceiver.java b/mobile/android/base/java/org/mozilla/gecko/notifications/NotificationReceiver.java deleted file mode 100644 index c3dd43297b..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/notifications/NotificationReceiver.java +++ /dev/null @@ -1,106 +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.notifications; - -import org.mozilla.gecko.GeckoApp; -import org.mozilla.gecko.GeckoAppShell; -import org.mozilla.gecko.GeckoThread; -import org.mozilla.gecko.mozglue.SafeIntent; - -import android.content.BroadcastReceiver; -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.net.Uri; -import android.util.Log; - -/** - * Broadcast receiver for Notifications. Will forward them to GeckoApp (and start Gecko) if they're clicked. - * If they're being dismissed, it will not start Gecko, but may forward them to JS if Gecko is running. - * This is also the only entry point for notification intents. - */ -public class NotificationReceiver extends BroadcastReceiver { - private static final String LOGTAG = "GeckoNotificationReceiver"; - - public void onReceive(Context context, Intent intent) { - final Uri data = intent.getData(); - if (data == null) { - Log.e(LOGTAG, "handleNotificationEvent: empty data"); - return; - } - - final String action = intent.getAction(); - if (NotificationClient.CLICK_ACTION.equals(action) || - NotificationClient.CLOSE_ACTION.equals(action)) { - onNotificationClientAction(context, action, data, intent); - return; - } - - final String notificationType = data.getQueryParameter(NotificationHelper.EVENT_TYPE_ATTR); - if (notificationType == null) { - return; - } - - // In case the user swiped out the notification, we empty the id set. - if (NotificationHelper.CLEARED_EVENT.equals(notificationType)) { - // If Gecko isn't running, we throw away events where the notification was cancelled. - // i.e. Don't bug the user if they're just closing a bunch of notifications. - if (GeckoThread.isRunning()) { - NotificationHelper.getArgsAndSendNotificationIntent(new SafeIntent(intent)); - } - - final NotificationClient client = (NotificationClient) - GeckoAppShell.getNotificationListener(); - client.onNotificationClose(data.getQueryParameter(NotificationHelper.ID_ATTR)); - return; - } - - forwardMessageToActivity(intent, context); - } - - private void forwardMessageToActivity(final Intent intent, final Context context) { - final ComponentName name = intent.getExtras().getParcelable(NotificationHelper.ORIGINAL_EXTRA_COMPONENT); - intent.setComponent(name); - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - context.startActivity(intent); - } - - private void onNotificationClientAction(final Context context, final String action, - final Uri data, final Intent intent) { - final String name = data.getQueryParameter("name"); - final String cookie = data.getQueryParameter("cookie"); - final Intent persistentIntent = (Intent) - intent.getParcelableExtra(NotificationClient.PERSISTENT_INTENT_EXTRA); - - if (persistentIntent != null) { - // Go through GeckoService for persistent notifications. - context.startService(persistentIntent); - } - - if (NotificationClient.CLICK_ACTION.equals(action)) { - GeckoAppShell.onNotificationClick(name, cookie); - - if (persistentIntent != null) { - // Don't launch GeckoApp if it's a background persistent notification. - return; - } - - final Intent appIntent = new Intent(GeckoApp.ACTION_ALERT_CALLBACK); - appIntent.setComponent(new ComponentName( - data.getAuthority(), data.getPath().substring(1))); // exclude leading slash. - appIntent.setData(data); - appIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - context.startActivity(appIntent); - - } else if (NotificationClient.CLOSE_ACTION.equals(action)) { - GeckoAppShell.onNotificationClose(name, cookie); - - final NotificationClient client = (NotificationClient) - GeckoAppShell.getNotificationListener(); - client.onNotificationClose(name); - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/notifications/NotificationService.java b/mobile/android/base/java/org/mozilla/gecko/notifications/NotificationService.java deleted file mode 100644 index 04b94cd1a6..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/notifications/NotificationService.java +++ /dev/null @@ -1,37 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.notifications; - -import android.app.Notification; -import android.app.Service; -import android.content.Intent; -import android.os.IBinder; - -import org.mozilla.gecko.R; - -public final class NotificationService extends Service { - public static final String EXTRA_NOTIFICATION = "notification"; - - @Override // Service - public int onStartCommand(final Intent intent, final int flags, final int startId) { - final Notification notification = intent.getParcelableExtra(EXTRA_NOTIFICATION); - if (notification != null) { - // Start foreground notification. - startForeground(R.id.foregroundNotification, notification); - return START_NOT_STICKY; - } - - // Stop foreground notification - stopForeground(true); - stopSelfResult(startId); - return START_NOT_STICKY; - } - - @Override // Service - public IBinder onBind(final Intent intent) { - return null; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/notifications/WhatsNewReceiver.java b/mobile/android/base/java/org/mozilla/gecko/notifications/WhatsNewReceiver.java deleted file mode 100644 index 6e799bf742..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/notifications/WhatsNewReceiver.java +++ /dev/null @@ -1,99 +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.notifications; - -import android.app.Notification; -import android.app.NotificationManager; -import android.app.PendingIntent; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.net.Uri; -import android.support.v4.app.NotificationCompat; -import android.text.TextUtils; - -import com.keepsafe.switchboard.SwitchBoard; -import org.mozilla.gecko.AppConstants; -import org.mozilla.gecko.GeckoSharedPrefs; -import org.mozilla.gecko.Locales; -import org.mozilla.gecko.R; -import org.mozilla.gecko.Telemetry; -import org.mozilla.gecko.TelemetryContract; -import org.mozilla.gecko.preferences.GeckoPreferences; -import org.mozilla.gecko.Experiments; - -import java.util.Locale; - -public class WhatsNewReceiver extends BroadcastReceiver { - - public static final String EXTRA_WHATSNEW_NOTIFICATION = "whatsnew_notification"; - private static final String ACTION_NOTIFICATION_CANCELLED = "notification_cancelled"; - - @Override - public void onReceive(Context context, Intent intent) { - if (ACTION_NOTIFICATION_CANCELLED.equals(intent.getAction())) { - Telemetry.sendUIEvent(TelemetryContract.Event.CANCEL, TelemetryContract.Method.NOTIFICATION, EXTRA_WHATSNEW_NOTIFICATION); - return; - } - - final String dataString = intent.getDataString(); - if (TextUtils.isEmpty(dataString) || !dataString.contains(AppConstants.ANDROID_PACKAGE_NAME)) { - return; - } - - if (!SwitchBoard.isInExperiment(context, Experiments.WHATSNEW_NOTIFICATION)) { - return; - } - - if (!isPreferenceEnabled(context)) { - return; - } - - showWhatsNewNotification(context); - } - - private boolean isPreferenceEnabled(Context context) { - return GeckoSharedPrefs.forApp(context).getBoolean(GeckoPreferences.PREFS_NOTIFICATIONS_WHATS_NEW, true); - } - - private void showWhatsNewNotification(Context context) { - final Notification notification = new NotificationCompat.Builder(context) - .setContentTitle(context.getString(R.string.whatsnew_notification_title)) - .setContentText(context.getString(R.string.whatsnew_notification_summary)) - .setSmallIcon(R.drawable.ic_status_logo) - .setAutoCancel(true) - .setContentIntent(getContentIntent(context)) - .setDeleteIntent(getDeleteIntent(context)) - .build(); - - final NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); - final int notificationID = EXTRA_WHATSNEW_NOTIFICATION.hashCode(); - notificationManager.notify(notificationID, notification); - - Telemetry.sendUIEvent(TelemetryContract.Event.SHOW, TelemetryContract.Method.NOTIFICATION, EXTRA_WHATSNEW_NOTIFICATION); - } - - private PendingIntent getContentIntent(Context context) { - final String link = context.getString(R.string.whatsnew_notification_url, - AppConstants.MOZ_APP_VERSION, - AppConstants.OS_TARGET, - Locales.getLanguageTag(Locale.getDefault())); - - final Intent i = new Intent(Intent.ACTION_VIEW); - i.setClassName(AppConstants.ANDROID_PACKAGE_NAME, AppConstants.MOZ_ANDROID_BROWSER_INTENT_CLASS); - i.setData(Uri.parse(link)); - i.putExtra(EXTRA_WHATSNEW_NOTIFICATION, true); - - return PendingIntent.getActivity(context, 0, i, PendingIntent.FLAG_UPDATE_CURRENT); - } - - private PendingIntent getDeleteIntent(Context context) { - final Intent i = new Intent(context, WhatsNewReceiver.class); - i.setAction(ACTION_NOTIFICATION_CANCELLED); - - return PendingIntent.getBroadcast(context, 0, i, PendingIntent.FLAG_CANCEL_CURRENT); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/overlays/OverlayConstants.java b/mobile/android/base/java/org/mozilla/gecko/overlays/OverlayConstants.java deleted file mode 100644 index 16f5560d3d..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/overlays/OverlayConstants.java +++ /dev/null @@ -1,68 +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.overlays; - -/** - * Constants used by the share handler service (and clients). - * The intent API used by the service is defined herein. - */ -public class OverlayConstants { - /* - * OverlayIntentHandler service intent actions. - */ - - /* - * Causes the service to broadcast an intent containing state necessary for proper display of - * a UI to select a target share method. - * - * Intent parameters: - * - * None. - */ - public static final String ACTION_PREPARE_SHARE = "org.mozilla.gecko.overlays.ACTION_PREPARE_SHARE"; - - /* - * Action for sharing a page. - * - * Intent parameters: - * - * $EXTRA_URL: URL of page to share. (required) - * $EXTRA_SHARE_METHOD: Method(s) via which to share this url/title combination. Can be either a - * ShareType or a ShareType[] - * $EXTRA_TITLE: Title of page to share (optional) - * $EXTRA_PARAMETERS: Parcelable of extra data to pass to the ShareMethod (optional) - */ - public static final String ACTION_SHARE = "org.mozilla.gecko.overlays.ACTION_SHARE"; - - /* - * OverlayIntentHandler service intent extra field keys. - */ - - // The URL/title of the page being shared - public static final String EXTRA_URL = "URL"; - public static final String EXTRA_TITLE = "TITLE"; - - // The optional extra Parcelable parameters for a ShareMethod. - public static final String EXTRA_PARAMETERS = "EXTRA"; - - // The extra field key used for holding the ShareMethod.Type we wish to use for an operation. - public static final String EXTRA_SHARE_METHOD = "SHARE_METHOD"; - - /* - * ShareMethod UI event intent constants. Broadcast by ShareMethods using LocalBroadcastManager - * when state has changed that requires an update of any currently-displayed share UI. - */ - - /* - * Action for a ShareMethod UI event. - * - * Intent parameters: - * - * $EXTRA_SHARE_METHOD: The ShareType to which this event relates. - * ... ShareType-specific parameters as desired... (optional) - */ - public static final String SHARE_METHOD_UI_EVENT = "org.mozilla.gecko.overlays.ACTION_SHARE_METHOD_UI_EVENT"; -} diff --git a/mobile/android/base/java/org/mozilla/gecko/overlays/service/OverlayActionService.java b/mobile/android/base/java/org/mozilla/gecko/overlays/service/OverlayActionService.java deleted file mode 100644 index 7182fcce73..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/overlays/service/OverlayActionService.java +++ /dev/null @@ -1,126 +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.overlays.service; - -import android.app.Service; -import android.content.Context; -import android.content.Intent; -import android.os.IBinder; -import android.util.Log; - -import org.mozilla.gecko.overlays.service.sharemethods.AddBookmark; -import org.mozilla.gecko.overlays.service.sharemethods.SendTab; -import org.mozilla.gecko.overlays.service.sharemethods.ShareMethod; -import org.mozilla.gecko.util.ThreadUtils; - -import java.util.EnumMap; -import java.util.Map; - -import static org.mozilla.gecko.overlays.OverlayConstants.ACTION_PREPARE_SHARE; -import static org.mozilla.gecko.overlays.OverlayConstants.ACTION_SHARE; - -/** - * A service to receive requests from overlays to perform actions. - * See OverlayConstants for details of the intent API supported by this service. - * - * Currently supported operations are: - * - * Add bookmark* - * Send tab (delegates to Sync's existing handler) - * Future: Load page in background. - * - * * Neither of these incur a page fetch on the service... yet. That will require headless Gecko, - * something we're yet to have. Refactoring Gecko as a service itself and restructing the rest of - * the app to talk to it seems like the way to go there. - */ -public class OverlayActionService extends Service { - private static final String LOGTAG = "GeckoOverlayService"; - - // Map used for selecting the appropriate helper object when handling a share. - final Map shareTypes = new EnumMap<>(ShareMethod.Type.class); - - // Map relating Strings representing share types to the corresponding ShareMethods. - // Share methods are initialised (and shown in the UI) in the order they are given here. - // This map is used to look up the appropriate ShareMethod when handling a request, as well as - // for identifying which ShareMethod needs re-initialising in response to such an intent (which - // will be necessary in situations such as the deletion of Sync accounts). - - // Not a bindable service. - @Override - public IBinder onBind(Intent intent) { - return null; - } - - @Override - public int onStartCommand(Intent intent, int flags, int startId) { - if (intent == null) { - return START_NOT_STICKY; - } - - // Dispatch intent to appropriate method according to its action. - String action = intent.getAction(); - - switch (action) { - case ACTION_SHARE: - handleShare(intent); - break; - case ACTION_PREPARE_SHARE: - initShareMethods(getApplicationContext()); - break; - default: - throw new IllegalArgumentException("Unsupported intent action: " + action); - } - - return START_NOT_STICKY; - } - - /** - * Reinitialise all ShareMethods, causing them to broadcast any UI update events necessary. - */ - private void initShareMethods(final Context context) { - ThreadUtils.postToBackgroundThread(new Runnable() { - @Override - public void run() { - shareTypes.clear(); - - shareTypes.put(ShareMethod.Type.ADD_BOOKMARK, new AddBookmark(context)); - shareTypes.put(ShareMethod.Type.SEND_TAB, new SendTab(context)); - } - }); - } - - public void handleShare(final Intent intent) { - ThreadUtils.postToBackgroundThread(new Runnable() { - @Override - public void run() { - ShareData shareData; - try { - shareData = ShareData.fromIntent(intent); - } catch (IllegalArgumentException e) { - Log.e(LOGTAG, "Error parsing share intent: ", e); - return; - } - - ShareMethod shareMethod = shareTypes.get(shareData.shareMethodType); - - final ShareMethod.Result result = shareMethod.handle(shareData); - // Dispatch the share to the targeted ShareMethod. - switch (result) { - case SUCCESS: - Log.d(LOGTAG, "Share was successful"); - break; - case TRANSIENT_FAILURE: - // Fall-through - case PERMANENT_FAILURE: - Log.e(LOGTAG, "Share failed: " + result); - break; - default: - throw new IllegalStateException("Unknown share method result code: " + result); - } - } - }); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/overlays/service/ShareData.java b/mobile/android/base/java/org/mozilla/gecko/overlays/service/ShareData.java deleted file mode 100644 index df233d74a0..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/overlays/service/ShareData.java +++ /dev/null @@ -1,48 +0,0 @@ -/* 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.overlays.service; - -import android.content.Intent; -import android.os.Bundle; -import android.os.Parcelable; -import org.mozilla.gecko.overlays.OverlayConstants; -import org.mozilla.gecko.overlays.service.sharemethods.ShareMethod; - -import static org.mozilla.gecko.overlays.OverlayConstants.EXTRA_SHARE_METHOD; - -/** - * Class to hold information related to a particular request to perform a share. - */ -public class ShareData { - private static final String LOGTAG = "GeckoShareRequest"; - - public final String url; - public final String title; - public final Parcelable extra; - public final ShareMethod.Type shareMethodType; - - public ShareData(String url, String title, Parcelable extra, ShareMethod.Type shareMethodType) { - if (url == null) { - throw new IllegalArgumentException("Null url passed to ShareData!"); - } - - this.url = url; - this.title = title; - this.extra = extra; - this.shareMethodType = shareMethodType; - } - - public static ShareData fromIntent(Intent intent) { - Bundle extras = intent.getExtras(); - - // Fish the parameters out of the Intent. - final String url = extras.getString(OverlayConstants.EXTRA_URL); - final String title = extras.getString(OverlayConstants.EXTRA_TITLE); - final Parcelable extra = extras.getParcelable(OverlayConstants.EXTRA_PARAMETERS); - ShareMethod.Type shareMethodType = (ShareMethod.Type) extras.get(EXTRA_SHARE_METHOD); - - return new ShareData(url, title, extra, shareMethodType); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/overlays/service/sharemethods/AddBookmark.java b/mobile/android/base/java/org/mozilla/gecko/overlays/service/sharemethods/AddBookmark.java deleted file mode 100644 index 71931e6838..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/overlays/service/sharemethods/AddBookmark.java +++ /dev/null @@ -1,30 +0,0 @@ -/* 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.overlays.service.sharemethods; - -import android.content.ContentResolver; -import android.content.Context; -import org.mozilla.gecko.GeckoProfile; -import org.mozilla.gecko.R; -import org.mozilla.gecko.db.LocalBrowserDB; -import org.mozilla.gecko.overlays.service.ShareData; - -public class AddBookmark extends ShareMethod { - private static final String LOGTAG = "GeckoAddBookmark"; - - @Override - public Result handle(ShareData shareData) { - ContentResolver resolver = context.getContentResolver(); - - LocalBrowserDB browserDB = new LocalBrowserDB(GeckoProfile.DEFAULT_PROFILE); - browserDB.addBookmark(resolver, shareData.title, shareData.url); - - return Result.SUCCESS; - } - - public AddBookmark(Context context) { - super(context); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/overlays/service/sharemethods/SendTab.java b/mobile/android/base/java/org/mozilla/gecko/overlays/service/sharemethods/SendTab.java deleted file mode 100644 index 5abcbd99f5..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/overlays/service/sharemethods/SendTab.java +++ /dev/null @@ -1,296 +0,0 @@ -/* 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.overlays.service.sharemethods; - -import android.accounts.Account; -import android.accounts.AccountManager; -import android.content.Context; -import android.content.Intent; -import android.content.SharedPreferences; -import android.database.Cursor; -import android.os.Bundle; -import android.os.Parcelable; -import android.support.v4.content.LocalBroadcastManager; -import android.util.Log; - -import org.mozilla.gecko.GeckoProfile; -import org.mozilla.gecko.db.BrowserDB; -import org.mozilla.gecko.db.RemoteClient; -import org.mozilla.gecko.db.TabsAccessor; -import org.mozilla.gecko.fxa.FxAccountConstants; -import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount; -import org.mozilla.gecko.fxa.login.State; -import org.mozilla.gecko.overlays.OverlayConstants; -import org.mozilla.gecko.overlays.service.ShareData; -import org.mozilla.gecko.sync.CommandProcessor; -import org.mozilla.gecko.sync.CommandRunner; -import org.mozilla.gecko.sync.GlobalSession; -import org.mozilla.gecko.sync.SyncConfiguration; - -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - -/** - * ShareMethod implementation to handle Sync's "Send tab to device" mechanism. - * See OverlayConstants for documentation of OverlayIntentHandler service intent API (which is how - * this class is chiefly interacted with). - */ -public class SendTab extends ShareMethod { - private static final String LOGTAG = "GeckoSendTab"; - - // Key used in the extras Bundle in the share intent used for a send tab ShareMethod. - public static final String SEND_TAB_TARGET_DEVICES = "SEND_TAB_TARGET_DEVICES"; - - // Key used in broadcast intent from SendTab ShareMethod specifying available RemoteClients. - public static final String EXTRA_REMOTE_CLIENT_RECORDS = "RECORDS"; - - // The intent we should dispatch when the button for this ShareMethod is tapped, instead of - // taking the normal action (e.g., "Set up Sync!") - public static final String OVERRIDE_INTENT = "OVERRIDE_INTENT"; - - private Set validGUIDs; - - // A TabSender appropriate to the account type we're connected to. - private TabSender tabSender; - - @Override - public Result handle(ShareData shareData) { - if (shareData.extra == null) { - Log.e(LOGTAG, "No target devices specified!"); - - // Retrying with an identical lack of devices ain't gonna fix it... - return Result.PERMANENT_FAILURE; - } - - String[] targetGUIDs = ((Bundle) shareData.extra).getStringArray(SEND_TAB_TARGET_DEVICES); - - // Ensure all target GUIDs are devices we actually know about. - if (!validGUIDs.containsAll(Arrays.asList(targetGUIDs))) { - // Find the set of invalid GUIDs to provide a nice error message. - Log.e(LOGTAG, "Not all provided GUIDs are real devices:"); - for (String targetGUID : targetGUIDs) { - if (!validGUIDs.contains(targetGUID)) { - Log.e(LOGTAG, "Invalid GUID: " + targetGUID); - } - } - - return Result.PERMANENT_FAILURE; - } - - Log.i(LOGTAG, "Send tab handler invoked."); - - final CommandProcessor processor = CommandProcessor.getProcessor(); - - final String accountGUID = tabSender.getAccountGUID(); - Log.d(LOGTAG, "Retrieved local account GUID '" + accountGUID + "'."); - - if (accountGUID == null) { - Log.e(LOGTAG, "Cannot determine account GUID"); - - // It's not completely out of the question that a background sync might come along and - // fix everything for us... - return Result.TRANSIENT_FAILURE; - } - - // Queue up the share commands for each destination device. - // Remember that ShareMethod.handle is always run on the background thread, so the database - // access here is of no concern. - for (int i = 0; i < targetGUIDs.length; i++) { - processor.sendURIToClientForDisplay(shareData.url, targetGUIDs[i], shareData.title, accountGUID, context); - } - - // Request an immediate sync to push these new commands to the network ASAP. - Log.i(LOGTAG, "Requesting immediate clients stage sync."); - tabSender.sync(); - - return Result.SUCCESS; - // ... Probably. - } - - /** - * Get an Intent suitable for broadcasting the UI state of this ShareMethod. - * The caller shall populate the intent with the actual state. - */ - private Intent getUIStateIntent() { - Intent uiStateIntent = new Intent(OverlayConstants.SHARE_METHOD_UI_EVENT); - uiStateIntent.putExtra(OverlayConstants.EXTRA_SHARE_METHOD, (Parcelable) Type.SEND_TAB); - return uiStateIntent; - } - - /** - * Broadcast the given intent to any UIs that may be listening. - */ - private void broadcastUIState(Intent uiStateIntent) { - LocalBroadcastManager.getInstance(context).sendBroadcast(uiStateIntent); - } - - /** - * Load the state of the user's Firefox Sync accounts and broadcast it to any registered - * listeners. This will cause any UIs that may exist that depend on this information to update. - */ - public SendTab(Context aContext) { - super(aContext); - // Initialise the UI state intent... - - // Determine if the user has a new or old style sync account and load the available sync - // clients for it. - final AccountManager accountManager = AccountManager.get(context); - final Account[] fxAccounts = accountManager.getAccountsByType(FxAccountConstants.ACCOUNT_TYPE); - - if (fxAccounts.length > 0) { - final AndroidFxAccount fxAccount = new AndroidFxAccount(context, fxAccounts[0]); - if (fxAccount.getState().getNeededAction() != State.Action.None) { - // We have a Firefox Account, but it's definitely not able to send a tab - // right now. Redirect to the status activity. - Log.w(LOGTAG, "Firefox Account named like " + fxAccount.getObfuscatedEmail() + - " needs action before it can send a tab; redirecting to status activity."); - - setOverrideIntentAction(FxAccountConstants.ACTION_FXA_STATUS); - return; - } - - tabSender = new FxAccountTabSender(fxAccount); - - updateClientList(tabSender); - - Log.i(LOGTAG, "Allowing tab send for Firefox Account."); - registerDisplayURICommand(); - return; - } - - // Have registered UIs offer to set up a Firefox Account. - setOverrideIntentAction(FxAccountConstants.ACTION_FXA_GET_STARTED); - } - - /** - * Load the list of Sync clients that are not this device using the given TabSender. - */ - private void updateClientList(TabSender tabSender) { - Collection otherClients = getOtherClients(tabSender); - - // Put the list of RemoteClients into the uiStateIntent and broadcast it. - RemoteClient[] records = new RemoteClient[otherClients.size()]; - records = otherClients.toArray(records); - - validGUIDs = new HashSet<>(); - - for (RemoteClient client : otherClients) { - validGUIDs.add(client.guid); - } - - if (validGUIDs.isEmpty()) { - // Guess we'd better override. We have no clients. - // This does the broadcast for us. - setOverrideIntentAction(FxAccountConstants.ACTION_FXA_GET_STARTED); - return; - } - - Intent uiStateIntent = getUIStateIntent(); - uiStateIntent.putExtra(EXTRA_REMOTE_CLIENT_RECORDS, records); - broadcastUIState(uiStateIntent); - } - - /** - * Record our intention to redirect the user to a different activity when they attempt to share - * with us, usually because we found something wrong with their Sync account (a need to login, - * register, etc.) - * This will be recorded in the OVERRIDE_INTENT field of the UI broadcast. Consumers should - * dispatch this intent instead of attempting to share with this ShareMethod whenever it is - * non-null. - * - * @param action to launch instead of invoking a share. - */ - protected void setOverrideIntentAction(final String action) { - Intent intent = new Intent(action); - // Per http://stackoverflow.com/a/8992365, this triggers a known bug with - // the soft keyboard not being shown for the started activity. Why, Android, why? - intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION); - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - - Intent uiStateIntent = getUIStateIntent(); - uiStateIntent.putExtra(OVERRIDE_INTENT, intent); - - broadcastUIState(uiStateIntent); - } - - private static void registerDisplayURICommand() { - final CommandProcessor processor = CommandProcessor.getProcessor(); - processor.registerCommand("displayURI", new CommandRunner(3) { - @Override - public void executeCommand(final GlobalSession session, List args) { - CommandProcessor.displayURI(args, session.getContext()); - } - }); - } - - /** - * @return A collection of unique remote clients sorted by most recently used. - */ - protected Collection getOtherClients(final TabSender sender) { - if (sender == null) { - Log.w(LOGTAG, "No tab sender when fetching other client IDs."); - return Collections.emptyList(); - } - - final BrowserDB browserDB = BrowserDB.from(context); - final TabsAccessor tabsAccessor = browserDB.getTabsAccessor(); - final Cursor remoteTabsCursor = tabsAccessor.getRemoteClientsByRecencyCursor(context); - try { - if (remoteTabsCursor.getCount() == 0) { - return Collections.emptyList(); - } - return tabsAccessor.getClientsWithoutTabsByRecencyFromCursor(remoteTabsCursor); - } finally { - remoteTabsCursor.close(); - } - } - - /** - * Inteface for interacting with Sync accounts. Used to hide the difference in implementation - * between FXA and "old sync" accounts when sending tabs. - */ - private interface TabSender { - public static final String[] STAGES_TO_SYNC = new String[] { "clients", "tabs" }; - - /** - * @return Return null if the account isn't correctly initialized. Return - * the account GUID otherwise. - */ - String getAccountGUID(); - - /** - * Sync this account, specifying only clients and tabs as the engines to sync. - */ - void sync(); - } - - private static class FxAccountTabSender implements TabSender { - private final AndroidFxAccount fxAccount; - - public FxAccountTabSender(AndroidFxAccount fxa) { - fxAccount = fxa; - } - - @Override - public String getAccountGUID() { - try { - final SharedPreferences prefs = fxAccount.getSyncPrefs(); - return prefs.getString(SyncConfiguration.PREF_ACCOUNT_GUID, null); - } catch (Exception e) { - Log.w(LOGTAG, "Could not get Firefox Account parameters or preferences; aborting."); - return null; - } - } - - @Override - public void sync() { - fxAccount.requestImmediateSync(STAGES_TO_SYNC, null); - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/overlays/service/sharemethods/ShareMethod.java b/mobile/android/base/java/org/mozilla/gecko/overlays/service/sharemethods/ShareMethod.java deleted file mode 100644 index 768176d635..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/overlays/service/sharemethods/ShareMethod.java +++ /dev/null @@ -1,82 +0,0 @@ -/*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.overlays.service.sharemethods; - -import android.content.Context; -import android.os.Parcel; -import android.os.Parcelable; -import org.mozilla.gecko.overlays.service.ShareData; - -/** - * Represents a method of sharing a URL/title. Add a bookmark? Send to a device? Add to reading list? - */ -public abstract class ShareMethod { - protected final Context context; - - public ShareMethod(Context aContext) { - context = aContext; - } - - /** - * Perform a share for the given title/URL combination. Called on the background thread by the - * handler service when a request is made. The "extra" parameter is provided should a ShareMethod - * desire to handle the share differently based on some additional parameters. - * - * @param title The page title for the page being shared. May be null if none can be found. - * @param url The URL of the page to be shared. Never null. - * @param extra A Parcelable of ShareMethod-specific parameters that may be provided by the - * caller. Generally null, but this field may be used to provide extra input to - * the ShareMethod (such as the device to share to in the case of SendTab). - * @return true if the attempt to share was a success. False in the event of an error. - */ - public abstract Result handle(ShareData shareData); - - /** - * Enum representing the possible results of performing a share. - */ - public static enum Result { - // Victory! - SUCCESS, - - // Failure, but retrying the same action again might lead to success. - TRANSIENT_FAILURE, - - // Failure, and you're not going to succeed until you reinitialise the ShareMethod (ie. - // until you repeat the entire share action). Examples include broken Sync accounts, or - // Sync accounts with no valid target devices (so the only way to fix this is to add some - // and try again: pushing a retry button isn't sane). - PERMANENT_FAILURE - } - - /** - * Enum representing types of ShareMethod. Parcelable so it may be efficiently used in Intents. - */ - public static enum Type implements Parcelable { - ADD_BOOKMARK, - SEND_TAB; - - @Override - public int describeContents() { - return 0; - } - - @Override - public void writeToParcel(final Parcel dest, final int flags) { - dest.writeInt(ordinal()); - } - - public static final Creator CREATOR = new Creator() { - @Override - public Type createFromParcel(final Parcel source) { - return Type.values()[source.readInt()]; - } - - @Override - public Type[] newArray(final int size) { - return new Type[size]; - } - }; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/overlays/ui/OverlayDialogButton.java b/mobile/android/base/java/org/mozilla/gecko/overlays/ui/OverlayDialogButton.java deleted file mode 100644 index 8b7bc872b4..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/overlays/ui/OverlayDialogButton.java +++ /dev/null @@ -1,128 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.overlays.ui; - -import org.mozilla.gecko.R; - -import android.content.Context; -import android.content.res.TypedArray; -import android.graphics.drawable.Drawable; -import android.util.AttributeSet; -import android.util.Log; -import android.view.animation.Animation; -import android.view.animation.AnimationUtils; -import android.view.LayoutInflater; -import android.view.View; -import android.widget.ImageView; -import android.widget.LinearLayout; -import android.widget.TextView; - -/** - * A button in the share overlay, such as the "Add to Reading List" button. - * Has an associated icon and label, and two states: enabled and disabled. - * - * When disabled, tapping results in a "pop" animation causing the icon to pulse. When enabled, - * tapping calls the OnClickListener set by the consumer in the usual way. - */ -public class OverlayDialogButton extends LinearLayout { - private static final String LOGTAG = "GeckoOverlayDialogButton"; - - // We can't use super.isEnabled(), since we want to stay clickable in disabled state. - private boolean isEnabled = true; - - private final ImageView iconView; - private final TextView labelView; - - private String enabledText = ""; - private String disabledText = ""; - - private OnClickListener enabledOnClickListener; - - public OverlayDialogButton(Context context) { - this(context, null); - } - - public OverlayDialogButton(Context context, AttributeSet attrs) { - super(context, attrs); - - setOrientation(LinearLayout.HORIZONTAL); - - LayoutInflater.from(context).inflate(R.layout.overlay_share_button, this); - - iconView = (ImageView) findViewById(R.id.overlaybtn_icon); - labelView = (TextView) findViewById(R.id.overlaybtn_label); - - super.setOnClickListener(new OnClickListener() { - - @Override - public void onClick(View v) { - - if (isEnabled) { - if (enabledOnClickListener != null) { - enabledOnClickListener.onClick(v); - } else { - Log.e(LOGTAG, "enabledOnClickListener is null."); - } - } else { - Animation anim = AnimationUtils.loadAnimation(getContext(), R.anim.overlay_pop); - iconView.startAnimation(anim); - } - } - }); - - final TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.OverlayDialogButton); - - Drawable drawable = typedArray.getDrawable(R.styleable.OverlayDialogButton_drawable); - if (drawable != null) { - setDrawable(drawable); - } - - String disabledText = typedArray.getString(R.styleable.OverlayDialogButton_disabledText); - if (disabledText != null) { - this.disabledText = disabledText; - } - - String enabledText = typedArray.getString(R.styleable.OverlayDialogButton_enabledText); - if (enabledText != null) { - this.enabledText = enabledText; - } - - typedArray.recycle(); - - setEnabled(true); - } - - public void setDrawable(Drawable drawable) { - iconView.setImageDrawable(drawable); - } - - public void setText(String text) { - labelView.setText(text); - } - - @Override - public void setOnClickListener(OnClickListener listener) { - enabledOnClickListener = listener; - } - - /** - * Set the enabledness state of this view. We don't call super.setEnabled, as we want to remain - * clickable even in the disabled state (but with a different click listener). - */ - @Override - public void setEnabled(boolean enabled) { - isEnabled = enabled; - iconView.setEnabled(enabled); - labelView.setEnabled(enabled); - - if (enabled) { - setText(enabledText); - } else { - setText(disabledText); - } - } - -} diff --git a/mobile/android/base/java/org/mozilla/gecko/overlays/ui/SendTabDeviceListArrayAdapter.java b/mobile/android/base/java/org/mozilla/gecko/overlays/ui/SendTabDeviceListArrayAdapter.java deleted file mode 100644 index 08e9c59f55..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/overlays/ui/SendTabDeviceListArrayAdapter.java +++ /dev/null @@ -1,185 +0,0 @@ -/* 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.overlays.ui; - -import java.util.Collection; - -import org.mozilla.gecko.AppConstants; -import org.mozilla.gecko.R; -import org.mozilla.gecko.db.RemoteClient; -import org.mozilla.gecko.overlays.ui.SendTabList.State; - -import android.app.AlertDialog; -import android.content.Context; -import android.graphics.drawable.Drawable; -import android.view.View; -import android.view.View.OnClickListener; -import android.view.ViewGroup; -import android.widget.ArrayAdapter; - -public class SendTabDeviceListArrayAdapter extends ArrayAdapter { - @SuppressWarnings("unused") - private static final String LOGTAG = "GeckoSendTabAdapter"; - - private State currentState; - - // String to display when in a "button-like" special state. Instead of using a - // RemoteClient we override the rendering using this string. - private String dummyRecordName; - - private final SendTabTargetSelectedListener listener; - - private Collection records; - - // The AlertDialog to show in the event the record is pressed while in the SHOW_DEVICES state. - // This will show the user a prompt to select a device from a longer list of devices. - private AlertDialog dialog; - - public SendTabDeviceListArrayAdapter(Context context, SendTabTargetSelectedListener aListener) { - super(context, R.layout.overlay_share_send_tab_item, R.id.overlaybtn_label); - - listener = aListener; - - // We do this manually and avoid multiple notifications when doing compound operations. - setNotifyOnChange(false); - } - - /** - * Get an array of the contents of this adapter were it in the LIST state. - * Useful for determining the "real" contents of the adapter. - */ - public RemoteClient[] toArray() { - return records.toArray(new RemoteClient[records.size()]); - } - - public void setRemoteClientsList(Collection remoteClientsList) { - records = remoteClientsList; - updateRecordList(); - } - - /** - * Ensure the contents of the Adapter are synchronised with the `records` field. This may not - * be the case if records has recently changed, or if we have experienced a state change. - */ - public void updateRecordList() { - if (currentState != State.LIST) { - return; - } - - clear(); - - setNotifyOnChange(false); // So we don't notify for each add. - addAll(records); - - notifyDataSetChanged(); - } - - @Override - public View getView(final int position, View convertView, ViewGroup parent) { - final Context context = getContext(); - - // Reuse View objects if they exist. - OverlayDialogButton row = (OverlayDialogButton) convertView; - if (row == null) { - row = (OverlayDialogButton) View.inflate(context, R.layout.overlay_share_send_tab_item, null); - } - - // The first view in the list has a unique style. - if (position == 0) { - row.setBackgroundResource(R.drawable.overlay_share_button_background_first); - } else { - row.setBackgroundResource(R.drawable.overlay_share_button_background); - } - - if (currentState != State.LIST) { - // If we're in a special "Button-like" state, use the override string and a generic icon. - final Drawable sendTabIcon = context.getResources().getDrawable(R.drawable.shareplane); - row.setText(dummyRecordName); - row.setDrawable(sendTabIcon); - } - - // If we're just a button to launch the dialog, set the listener and abort. - if (currentState == State.SHOW_DEVICES) { - row.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View view) { - dialog.show(); - } - }); - - return row; - } - - // The remaining states delegate to the SentTabTargetSelectedListener. - final RemoteClient remoteClient = getItem(position); - if (currentState == State.LIST) { - final Drawable clientIcon = context.getResources().getDrawable(getImage(remoteClient)); - row.setText(remoteClient.name); - row.setDrawable(clientIcon); - - final String listenerGUID = remoteClient.guid; - - row.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View view) { - listener.onSendTabTargetSelected(listenerGUID); - } - }); - } else { - row.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View view) { - listener.onSendTabActionSelected(); - } - }); - } - - return row; - } - - private static int getImage(RemoteClient record) { - if ("mobile".equals(record.deviceType)) { - return R.drawable.device_mobile; - } - - return R.drawable.device_desktop; - } - - public void switchState(State newState) { - if (currentState == newState) { - return; - } - - currentState = newState; - - switch (newState) { - case LIST: - updateRecordList(); - break; - case NONE: - showDummyRecord(getContext().getResources().getString(R.string.overlay_share_send_tab_btn_label)); - break; - case SHOW_DEVICES: - showDummyRecord(getContext().getResources().getString(R.string.overlay_share_send_other)); - break; - default: - throw new IllegalStateException("Unexpected state transition: " + newState); - } - } - - /** - * Set the dummy override string to the given value and clear the list. - */ - private void showDummyRecord(String name) { - dummyRecordName = name; - clear(); - add(null); - notifyDataSetChanged(); - } - - public void setDialog(AlertDialog aDialog) { - dialog = aDialog; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/overlays/ui/SendTabList.java b/mobile/android/base/java/org/mozilla/gecko/overlays/ui/SendTabList.java deleted file mode 100644 index 4fc6caaa99..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/overlays/ui/SendTabList.java +++ /dev/null @@ -1,150 +0,0 @@ -/* 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.overlays.ui; - -import static org.mozilla.gecko.overlays.ui.SendTabList.State.LOADING; -import static org.mozilla.gecko.overlays.ui.SendTabList.State.SHOW_DEVICES; - -import java.util.Arrays; - -import org.mozilla.gecko.R; -import org.mozilla.gecko.Telemetry; -import org.mozilla.gecko.TelemetryContract; -import org.mozilla.gecko.db.RemoteClient; - -import android.app.AlertDialog; -import android.content.Context; -import android.content.DialogInterface; -import android.util.AttributeSet; -import android.widget.ListAdapter; -import android.widget.ListView; - -/** - * The SendTab button has a few different states depending on the available devices (and whether - * we've loaded them yet...) - * - * Initially, the view resembles a disabled button. (the LOADING state) - * Once state is loaded from Sync's database, we know how many devices the user may send their tab - * to. - * - * If there are no targets, the user was found to not have a Sync account, or their Sync account is - * in a state that prevents it from being able to send a tab, we enter the NONE state and display - * a generic button which launches an appropriate activity to fix the situation when tapped (such - * as the set up Sync wizard). - * - * If the number of targets does not MAX_INLINE_SYNC_TARGETS, we present a button for each of them. - * (the LIST state) - * - * Otherwise, we enter the SHOW_DEVICES state, in which we display a "Send to other devices" button - * that takes the user to a menu for selecting a target device from their complete list of many - * devices. - */ -public class SendTabList extends ListView { - @SuppressWarnings("unused") - private static final String LOGTAG = "GeckoSendTabList"; - - // The maximum number of target devices to show in the main list. Further devices are available - // from a secondary menu. - public static final int MAXIMUM_INLINE_ELEMENTS = R.integer.number_of_inline_share_devices; - - private SendTabDeviceListArrayAdapter clientListAdapter; - - // Listener to fire when a share target is selected (either directly or via the prompt) - private SendTabTargetSelectedListener listener; - - private final State currentState = LOADING; - - /** - * Enum defining the states this view may occupy. - */ - public enum State { - // State when no sync targets exist (a generic "Send to Firefox Sync" button which launches - // an activity to set it up) - NONE, - - // As NONE, but disabled. Initial state. Used until we get information from Sync about what - // we really want. - LOADING, - - // A list of devices to share to. - LIST, - - // A single button prompting the user to select a device to share to. - SHOW_DEVICES - } - - public SendTabList(Context context) { - super(context); - } - - public SendTabList(Context context, AttributeSet attrs) { - super(context, attrs); - } - - @Override - public void setAdapter(ListAdapter adapter) { - if (!(adapter instanceof SendTabDeviceListArrayAdapter)) { - throw new IllegalArgumentException("adapter must be a SendTabDeviceListArrayAdapter instance"); - } - - clientListAdapter = (SendTabDeviceListArrayAdapter) adapter; - super.setAdapter(adapter); - } - - public void setSendTabTargetSelectedListener(SendTabTargetSelectedListener aListener) { - listener = aListener; - } - - public void switchState(State state) { - if (state == currentState) { - return; - } - - clientListAdapter.switchState(state); - if (state == SHOW_DEVICES) { - clientListAdapter.setDialog(getDialog()); - } - } - - public void setSyncClients(final RemoteClient[] c) { - final RemoteClient[] clients = c == null ? new RemoteClient[0] : c; - - clientListAdapter.setRemoteClientsList(Arrays.asList(clients)); - } - - /** - * Get an AlertDialog listing all devices, allowing the user to select the one they want. - * Used when more than MAXIMUM_INLINE_ELEMENTS devices are found (to avoid displaying them all - * inline and looking crazy). - */ - public AlertDialog getDialog() { - final Context context = getContext(); - - final AlertDialog.Builder builder = new AlertDialog.Builder(context); - - final RemoteClient[] records = clientListAdapter.toArray(); - final String[] dialogElements = new String[records.length]; - - for (int i = 0; i < records.length; i++) { - dialogElements[i] = records[i].name; - } - - builder.setTitle(R.string.overlay_share_select_device) - .setItems(dialogElements, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int index) { - listener.onSendTabTargetSelected(records[index].guid); - } - }) - .setOnCancelListener(new DialogInterface.OnCancelListener() { - @Override - public void onCancel(DialogInterface dialogInterface) { - Telemetry.sendUIEvent(TelemetryContract.Event.CANCEL, TelemetryContract.Method.SHARE_OVERLAY, "device_selection_cancel"); - } - }); - - return builder.create(); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/overlays/ui/SendTabTargetSelectedListener.java b/mobile/android/base/java/org/mozilla/gecko/overlays/ui/SendTabTargetSelectedListener.java deleted file mode 100644 index 79da526da5..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/overlays/ui/SendTabTargetSelectedListener.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * 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.overlays.ui; - -/** - * Interface for classes that wish to listen for the selection of an element from a SendTabList. - */ -public interface SendTabTargetSelectedListener { - /** - * Called when a row in the SendTabList is clicked. - * - * @param targetGUID The GUID of the ClientRecord the element represents (if any, otherwise null) - */ - public void onSendTabTargetSelected(String targetGUID); - - /** - * Called when the overall Send Tab item is clicked. - * - * This implies that the clients list was unavailable. - */ - public void onSendTabActionSelected(); -} diff --git a/mobile/android/base/java/org/mozilla/gecko/overlays/ui/ShareDialog.java b/mobile/android/base/java/org/mozilla/gecko/overlays/ui/ShareDialog.java deleted file mode 100644 index 156fdda2a0..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/overlays/ui/ShareDialog.java +++ /dev/null @@ -1,493 +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.overlays.ui; - -import java.net.URISyntaxException; - -import org.mozilla.gecko.AppConstants; -import org.mozilla.gecko.GeckoProfile; -import org.mozilla.gecko.Locales; -import org.mozilla.gecko.R; -import org.mozilla.gecko.Telemetry; -import org.mozilla.gecko.TelemetryContract; -import org.mozilla.gecko.db.LocalBrowserDB; -import org.mozilla.gecko.db.RemoteClient; -import org.mozilla.gecko.overlays.OverlayConstants; -import org.mozilla.gecko.overlays.service.OverlayActionService; -import org.mozilla.gecko.overlays.service.sharemethods.SendTab; -import org.mozilla.gecko.overlays.service.sharemethods.ShareMethod; -import org.mozilla.gecko.sync.setup.activities.WebURLFinder; -import org.mozilla.gecko.util.IntentUtils; -import org.mozilla.gecko.util.ThreadUtils; -import org.mozilla.gecko.util.UIAsyncTask; - -import android.content.BroadcastReceiver; -import android.content.ContentResolver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.graphics.drawable.Drawable; -import android.os.Bundle; -import android.os.Parcelable; -import android.support.v4.content.LocalBroadcastManager; -import android.text.TextUtils; -import android.util.Log; -import android.view.animation.AnimationSet; -import android.view.MotionEvent; -import android.view.View; -import android.view.animation.Animation; -import android.view.animation.AnimationUtils; -import android.widget.TextView; -import android.widget.Toast; - -/** - * A transparent activity that displays the share overlay. - */ -public class ShareDialog extends Locales.LocaleAwareActivity implements SendTabTargetSelectedListener { - - private enum State { - DEFAULT, - DEVICES_ONLY // Only display the device list. - } - - private static final String LOGTAG = "GeckoShareDialog"; - - /** Flag to indicate that we should always show the device list; specific to this release channel. **/ - public static final String INTENT_EXTRA_DEVICES_ONLY = - AppConstants.ANDROID_PACKAGE_NAME + ".intent.extra.DEVICES_ONLY"; - - /** The maximum number of devices we'll show in the dialog when in State.DEFAULT. **/ - private static final int MAXIMUM_INLINE_DEVICES = 2; - - private State state; - - private SendTabList sendTabList; - private OverlayDialogButton bookmarkButton; - - // The bookmark button drawable set from XML - we need this to reset state. - private Drawable bookmarkButtonDrawable; - - private String url; - private String title; - - // The override intent specified by SendTab (if any). See SendTab.java. - private Intent sendTabOverrideIntent; - - // Flag set during animation to prevent animation multiple-start. - private boolean isAnimating; - - // BroadcastReceiver to receive callbacks from ShareMethods which are changing state. - private final BroadcastReceiver uiEventListener = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - ShareMethod.Type originShareMethod = intent.getParcelableExtra(OverlayConstants.EXTRA_SHARE_METHOD); - switch (originShareMethod) { - case SEND_TAB: - handleSendTabUIEvent(intent); - break; - default: - throw new IllegalArgumentException("UIEvent broadcast from ShareMethod that isn't thought to support such broadcasts."); - } - } - }; - - /** - * Called when a UI event broadcast is received from the SendTab ShareMethod. - */ - protected void handleSendTabUIEvent(Intent intent) { - sendTabOverrideIntent = intent.getParcelableExtra(SendTab.OVERRIDE_INTENT); - - RemoteClient[] remoteClientRecords = (RemoteClient[]) intent.getParcelableArrayExtra(SendTab.EXTRA_REMOTE_CLIENT_RECORDS); - - // Escape hatch: we don't show the option to open this dialog in this state so this should - // never be run. However, due to potential inconsistencies in synced client state - // (e.g. bug 1122302 comment 47), we might fail. - if (state == State.DEVICES_ONLY && - (remoteClientRecords == null || remoteClientRecords.length == 0)) { - Log.e(LOGTAG, "In state: " + State.DEVICES_ONLY + " and received 0 synced clients. Finishing..."); - Toast.makeText(this, getResources().getText(R.string.overlay_no_synced_devices), Toast.LENGTH_SHORT) - .show(); - finish(); - return; - } - - sendTabList.setSyncClients(remoteClientRecords); - - if (state == State.DEVICES_ONLY || - remoteClientRecords == null || - remoteClientRecords.length <= MAXIMUM_INLINE_DEVICES) { - // Show the list of devices in-line. - sendTabList.switchState(SendTabList.State.LIST); - - // The first item in the list has a unique style. If there are no items - // in the list, the next button appears to be the first item in the list. - // - // Note: a more thorough implementation would add this - // (and other non-ListView buttons) into a custom ListView. - if (remoteClientRecords == null || remoteClientRecords.length == 0) { - bookmarkButton.setBackgroundResource( - R.drawable.overlay_share_button_background_first); - } - return; - } - - // Just show a button to launch the list of devices to choose from. - sendTabList.switchState(SendTabList.State.SHOW_DEVICES); - } - - @Override - protected void onDestroy() { - // Remove the listener when the activity is destroyed: we no longer care. - // Note: The activity can be destroyed without onDestroy being called. However, this occurs - // only when the application is killed, something which also kills the registered receiver - // list, and the service, and everything else: so we don't care. - LocalBroadcastManager.getInstance(this).unregisterReceiver(uiEventListener); - - super.onDestroy(); - } - - /** - * Show a toast indicating we were started with no URL, and then stop. - */ - private void abortDueToNoURL() { - Log.e(LOGTAG, "Unable to process shared intent. No URL found!"); - - // Display toast notifying the user of failure (most likely a developer who screwed up - // trying to send a share intent). - Toast toast = Toast.makeText(this, getResources().getText(R.string.overlay_share_no_url), Toast.LENGTH_SHORT); - toast.show(); - finish(); - } - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - setContentView(R.layout.overlay_share_dialog); - - LocalBroadcastManager.getInstance(this).registerReceiver(uiEventListener, - new IntentFilter(OverlayConstants.SHARE_METHOD_UI_EVENT)); - - // Send tab. - sendTabList = (SendTabList) findViewById(R.id.overlay_send_tab_btn); - - // Register ourselves as both the listener and the context for the Adapter. - final SendTabDeviceListArrayAdapter adapter = new SendTabDeviceListArrayAdapter(this, this); - sendTabList.setAdapter(adapter); - sendTabList.setSendTabTargetSelectedListener(this); - - bookmarkButton = (OverlayDialogButton) findViewById(R.id.overlay_share_bookmark_btn); - - bookmarkButtonDrawable = bookmarkButton.getBackground(); - - // Bookmark button - bookmarkButton = (OverlayDialogButton) findViewById(R.id.overlay_share_bookmark_btn); - bookmarkButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - addBookmark(); - } - }); - } - - @Override - protected void onResume() { - super.onResume(); - - final Intent intent = getIntent(); - - state = intent.getBooleanExtra(INTENT_EXTRA_DEVICES_ONLY, false) ? - State.DEVICES_ONLY : State.DEFAULT; - - // If the Activity is being reused, we need to reset the state. Ideally, we create a - // new instance for each call, but Android L breaks this (bug 1137928). - sendTabList.switchState(SendTabList.State.LOADING); - bookmarkButton.setBackgroundDrawable(bookmarkButtonDrawable); - - // The URL is usually hiding somewhere in the extra text. Extract it. - final String extraText = IntentUtils.getStringExtraSafe(intent, Intent.EXTRA_TEXT); - if (TextUtils.isEmpty(extraText)) { - abortDueToNoURL(); - return; - } - - final String pageUrl = new WebURLFinder(extraText).bestWebURL(); - if (TextUtils.isEmpty(pageUrl)) { - abortDueToNoURL(); - return; - } - - // Have the service start any initialisation work that's necessary for us to show the correct - // UI. The results of such work will come in via the BroadcastListener. - Intent serviceStartupIntent = new Intent(this, OverlayActionService.class); - serviceStartupIntent.setAction(OverlayConstants.ACTION_PREPARE_SHARE); - startService(serviceStartupIntent); - - // Start the slide-up animation. - getWindow().setWindowAnimations(0); - final Animation anim = AnimationUtils.loadAnimation(this, R.anim.overlay_slide_up); - findViewById(R.id.sharedialog).startAnimation(anim); - - // If provided, we use the subject text to give us something nice to display. - // If not, we wing it with the URL. - - // TODO: Consider polling Fennec databases to find better information to display. - final String subjectText = intent.getStringExtra(Intent.EXTRA_SUBJECT); - - final String telemetryExtras = "title=" + (subjectText != null); - if (subjectText != null) { - ((TextView) findViewById(R.id.title)).setText(subjectText); - } - - Telemetry.sendUIEvent(TelemetryContract.Event.SHOW, TelemetryContract.Method.SHARE_OVERLAY, telemetryExtras); - - title = subjectText; - url = pageUrl; - - // Set the subtitle text on the view and cause it to marquee if it's too long (which it will - // be, since it's a URL). - final TextView subtitleView = (TextView) findViewById(R.id.subtitle); - subtitleView.setText(pageUrl); - subtitleView.setEllipsize(TextUtils.TruncateAt.MARQUEE); - subtitleView.setSingleLine(true); - subtitleView.setMarqueeRepeatLimit(5); - subtitleView.setSelected(true); - - final View titleView = findViewById(R.id.title); - - if (state == State.DEVICES_ONLY) { - bookmarkButton.setVisibility(View.GONE); - - titleView.setOnClickListener(null); - subtitleView.setOnClickListener(null); - return; - } - - bookmarkButton.setVisibility(View.VISIBLE); - - // Configure buttons. - final View.OnClickListener launchBrowser = new View.OnClickListener() { - @Override - public void onClick(View view) { - ShareDialog.this.launchBrowser(); - } - }; - - titleView.setOnClickListener(launchBrowser); - subtitleView.setOnClickListener(launchBrowser); - - final LocalBrowserDB browserDB = new LocalBrowserDB(getCurrentProfile()); - setButtonState(url, browserDB); - } - - @Override - protected void onNewIntent(final Intent intent) { - super.onNewIntent(intent); - - // The intent returned by getIntent is not updated automatically. - setIntent(intent); - } - - /** - * Sets the state of the bookmark/reading list buttons: they are disabled if the given URL is - * already in the corresponding list. - */ - private void setButtonState(final String pageURL, final LocalBrowserDB browserDB) { - new UIAsyncTask.WithoutParams(ThreadUtils.getBackgroundHandler()) { - // Flags to hold the result - boolean isBookmark; - - @Override - protected Void doInBackground() { - final ContentResolver contentResolver = getApplicationContext().getContentResolver(); - - isBookmark = browserDB.isBookmark(contentResolver, pageURL); - - return null; - } - - @Override - protected void onPostExecute(Void aVoid) { - findViewById(R.id.overlay_share_bookmark_btn).setEnabled(!isBookmark); - } - }.execute(); - } - - /** - * Helper method to get an overlay service intent populated with the data held in this dialog. - */ - private Intent getServiceIntent(ShareMethod.Type method) { - final Intent serviceIntent = new Intent(this, OverlayActionService.class); - serviceIntent.setAction(OverlayConstants.ACTION_SHARE); - - serviceIntent.putExtra(OverlayConstants.EXTRA_SHARE_METHOD, (Parcelable) method); - serviceIntent.putExtra(OverlayConstants.EXTRA_URL, url); - serviceIntent.putExtra(OverlayConstants.EXTRA_TITLE, title); - - return serviceIntent; - } - - @Override - public void finish() { - finish(true); - } - - private void finish(final boolean shouldOverrideAnimations) { - super.finish(); - if (shouldOverrideAnimations) { - // Don't perform an activity-dismiss animation. - overridePendingTransition(0, 0); - } - } - - /* - * Button handlers. Send intents to the background service responsible for processing requests - * on Fennec in the background. (a nice extensible mechanism for "doing stuff without properly - * launching Fennec"). - */ - - @Override - public void onSendTabActionSelected() { - // This requires an override intent. - if (sendTabOverrideIntent == null) { - throw new IllegalStateException("sendTabOverrideIntent must not be null"); - } - - startActivity(sendTabOverrideIntent); - finish(); - } - - @Override - public void onSendTabTargetSelected(String targetGUID) { - // targetGUID being null with no override intent should be an impossible state. - if (targetGUID == null) { - throw new IllegalStateException("targetGUID must not be null"); - } - - Intent serviceIntent = getServiceIntent(ShareMethod.Type.SEND_TAB); - - // Currently, only one extra parameter is necessary (the GUID of the target device). - Bundle extraParameters = new Bundle(); - - // Future: Handle multiple-selection. Bug 1061297. - extraParameters.putStringArray(SendTab.SEND_TAB_TARGET_DEVICES, new String[] { targetGUID }); - - serviceIntent.putExtra(OverlayConstants.EXTRA_PARAMETERS, extraParameters); - - startService(serviceIntent); - animateOut(true); - - Telemetry.sendUIEvent(TelemetryContract.Event.SHARE, TelemetryContract.Method.SHARE_OVERLAY, "sendtab"); - } - - public void addBookmark() { - startService(getServiceIntent(ShareMethod.Type.ADD_BOOKMARK)); - animateOut(true); - - Telemetry.sendUIEvent(TelemetryContract.Event.SAVE, TelemetryContract.Method.SHARE_OVERLAY, "bookmark"); - } - - public void launchBrowser() { - try { - // This can launch in the guest profile. Sorry. - final Intent i = Intent.parseUri(url, Intent.URI_INTENT_SCHEME); - i.setClassName(AppConstants.ANDROID_PACKAGE_NAME, AppConstants.MOZ_ANDROID_BROWSER_INTENT_CLASS); - startActivity(i); - } catch (URISyntaxException e) { - // Nothing much we can do. - } finally { - // Since we're changing apps, users expect the default app switch animations. - finish(false); - } - } - - private String getCurrentProfile() { - return GeckoProfile.DEFAULT_PROFILE; - } - - /** - * Slide the overlay down off the screen, display - * a check (if given), and finish the activity. - */ - private void animateOut(final boolean shouldDisplayConfirmation) { - if (isAnimating) { - return; - } - - isAnimating = true; - final Animation slideOutAnim = AnimationUtils.loadAnimation(this, R.anim.overlay_slide_down); - - final Animation animationToFinishActivity; - if (!shouldDisplayConfirmation) { - animationToFinishActivity = slideOutAnim; - } else { - final View check = findViewById(R.id.check); - check.setVisibility(View.VISIBLE); - final Animation checkEntryAnim = AnimationUtils.loadAnimation(this, R.anim.overlay_check_entry); - final Animation checkExitAnim = AnimationUtils.loadAnimation(this, R.anim.overlay_check_exit); - checkExitAnim.setStartOffset(checkEntryAnim.getDuration() + 500); - - final AnimationSet checkAnimationSet = new AnimationSet(this, null); - checkAnimationSet.addAnimation(checkEntryAnim); - checkAnimationSet.addAnimation(checkExitAnim); - - check.startAnimation(checkAnimationSet); - animationToFinishActivity = checkExitAnim; - } - - findViewById(R.id.sharedialog).startAnimation(slideOutAnim); - animationToFinishActivity.setAnimationListener(new Animation.AnimationListener() { - @Override - public void onAnimationStart(Animation animation) { /* Unused. */ } - - @Override - public void onAnimationEnd(Animation animation) { - finish(); - } - - @Override - public void onAnimationRepeat(Animation animation) { /* Unused. */ } - }); - - // Allows the user to dismiss the animation early. - setFullscreenFinishOnClickListener(); - } - - /** - * Sets a fullscreen {@link #finish()} click listener. We do this rather than attaching an - * onClickListener to the root View because in that case, we need to remove all of the - * existing listeners, which is less robust. - */ - private void setFullscreenFinishOnClickListener() { - final View clickTarget = findViewById(R.id.fullscreen_click_target); - clickTarget.setVisibility(View.VISIBLE); - clickTarget.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - finish(); - } - }); - } - - /** - * Close the dialog if back is pressed. - */ - @Override - public void onBackPressed() { - animateOut(false); - Telemetry.sendUIEvent(TelemetryContract.Event.CANCEL, TelemetryContract.Method.SHARE_OVERLAY); - } - - /** - * Close the dialog if the anything that isn't a button is tapped. - */ - @Override - public boolean onTouchEvent(MotionEvent event) { - animateOut(false); - Telemetry.sendUIEvent(TelemetryContract.Event.CANCEL, TelemetryContract.Method.SHARE_OVERLAY); - return true; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/preferences/AlignRightLinkPreference.java b/mobile/android/base/java/org/mozilla/gecko/preferences/AlignRightLinkPreference.java deleted file mode 100644 index b68a018f2d..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/preferences/AlignRightLinkPreference.java +++ /dev/null @@ -1,24 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.preferences; - -import org.mozilla.gecko.R; - -import android.content.Context; -import android.util.AttributeSet; - -class AlignRightLinkPreference extends LinkPreference { - - public AlignRightLinkPreference(Context context, AttributeSet attrs) { - super(context, attrs); - setLayoutResource(R.layout.preference_rightalign_icon); - } - - public AlignRightLinkPreference(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - setLayoutResource(R.layout.preference_rightalign_icon); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/preferences/AndroidImport.java b/mobile/android/base/java/org/mozilla/gecko/preferences/AndroidImport.java deleted file mode 100644 index bb71ce78b7..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/preferences/AndroidImport.java +++ /dev/null @@ -1,230 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.preferences; - -import android.content.ContentValues; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.os.Build; -import org.mozilla.gecko.GeckoProfile; -import org.mozilla.gecko.db.BrowserContract; -import org.mozilla.gecko.db.BrowserContract.Bookmarks; -import org.mozilla.gecko.db.LocalBrowserDB; -import org.mozilla.gecko.icons.IconResponse; -import org.mozilla.gecko.icons.IconsHelper; -import org.mozilla.gecko.icons.storage.DiskStorage; - -import android.content.ContentProviderOperation; -import android.content.ContentResolver; -import android.content.Context; -import android.content.OperationApplicationException; -import android.database.Cursor; -import android.net.Uri; -import android.os.RemoteException; -import android.provider.BaseColumns; -import android.text.TextUtils; -import android.util.Log; - -import java.util.ArrayList; - -public class AndroidImport implements Runnable { - /** - * The Android M SDK removed several fields and methods from android.provider.Browser. This class is used as a - * replacement to support building with the new SDK but at the same time still use these fields on lower Android - * versions. - */ - private static class LegacyBrowserProvider { - public static final Uri BOOKMARKS_URI = Uri.parse("content://browser/bookmarks"); - - // Incomplete: This are just the fields we currently use in our code base - public static class BookmarkColumns implements BaseColumns { - public static final String URL = "url"; - public static final String VISITS = "visits"; - public static final String DATE = "date"; - public static final String BOOKMARK = "bookmark"; - public static final String TITLE = "title"; - public static final String CREATED = "created"; - public static final String FAVICON = "favicon"; - } - } - - public static final Uri SAMSUNG_BOOKMARKS_URI = Uri.parse("content://com.sec.android.app.sbrowser.browser/bookmarks"); - public static final Uri SAMSUNG_HISTORY_URI = Uri.parse("content://com.sec.android.app.sbrowser.browser/history"); - public static final String SAMSUNG_MANUFACTURER = "samsung"; - - private static final String LOGTAG = "AndroidImport"; - private final Context mContext; - private final Runnable mOnDoneRunnable; - private final ArrayList mOperations; - private final ContentResolver mCr; - private final LocalBrowserDB mDB; - private final boolean mImportBookmarks; - private final boolean mImportHistory; - - public AndroidImport(Context context, Runnable onDoneRunnable, - boolean doBookmarks, boolean doHistory) { - mContext = context; - mOnDoneRunnable = onDoneRunnable; - mOperations = new ArrayList(); - mCr = mContext.getContentResolver(); - mDB = new LocalBrowserDB(GeckoProfile.get(context).getName()); - mImportBookmarks = doBookmarks; - mImportHistory = doHistory; - } - - public void mergeBookmarks() { - Cursor cursor = null; - try { - cursor = query(LegacyBrowserProvider.BOOKMARKS_URI, - SAMSUNG_BOOKMARKS_URI, - LegacyBrowserProvider.BookmarkColumns.BOOKMARK + " = 1"); - - if (cursor != null) { - final int faviconCol = cursor.getColumnIndexOrThrow(LegacyBrowserProvider.BookmarkColumns.FAVICON); - final int titleCol = cursor.getColumnIndexOrThrow(LegacyBrowserProvider.BookmarkColumns.TITLE); - final int urlCol = cursor.getColumnIndexOrThrow(LegacyBrowserProvider.BookmarkColumns.URL); - // http://code.google.com/p/android/issues/detail?id=17969 - final int createCol = cursor.getColumnIndex(LegacyBrowserProvider.BookmarkColumns.CREATED); - - cursor.moveToFirst(); - while (!cursor.isAfterLast()) { - String url = cursor.getString(urlCol); - String title = cursor.getString(titleCol); - long created; - if (createCol >= 0) { - created = cursor.getLong(createCol); - } else { - created = System.currentTimeMillis(); - } - // Need to set it to the current time so Sync picks it up. - long modified = System.currentTimeMillis(); - byte[] data = cursor.getBlob(faviconCol); - mDB.updateBookmarkInBatch(mCr, mOperations, - url, title, null, -1, - created, modified, - BrowserContract.Bookmarks.DEFAULT_POSITION, - null, Bookmarks.TYPE_BOOKMARK); - if (data != null) { - storeBitmap(data, url); - } - cursor.moveToNext(); - } - } - } finally { - if (cursor != null) - cursor.close(); - } - - flushBatchOperations(); - } - - public void mergeHistory() { - ArrayList visitsToSynthesize = new ArrayList<>(); - Cursor cursor = null; - try { - cursor = query (LegacyBrowserProvider.BOOKMARKS_URI, - SAMSUNG_HISTORY_URI, - LegacyBrowserProvider.BookmarkColumns.BOOKMARK + " = 0 AND " + - LegacyBrowserProvider.BookmarkColumns.VISITS + " > 0"); - - if (cursor != null) { - final int dateCol = cursor.getColumnIndexOrThrow(LegacyBrowserProvider.BookmarkColumns.DATE); - final int faviconCol = cursor.getColumnIndexOrThrow(LegacyBrowserProvider.BookmarkColumns.FAVICON); - final int titleCol = cursor.getColumnIndexOrThrow(LegacyBrowserProvider.BookmarkColumns.TITLE); - final int urlCol = cursor.getColumnIndexOrThrow(LegacyBrowserProvider.BookmarkColumns.URL); - final int visitsCol = cursor.getColumnIndexOrThrow(LegacyBrowserProvider.BookmarkColumns.VISITS); - - cursor.moveToFirst(); - while (!cursor.isAfterLast()) { - String url = cursor.getString(urlCol); - String title = cursor.getString(titleCol); - long date = cursor.getLong(dateCol); - int visits = cursor.getInt(visitsCol); - byte[] data = cursor.getBlob(faviconCol); - mDB.updateHistoryInBatch(mCr, mOperations, url, title, date, visits); - if (data != null) { - storeBitmap(data, url); - } - ContentValues visitData = new ContentValues(); - visitData.put(LocalBrowserDB.HISTORY_VISITS_DATE, date); - visitData.put(LocalBrowserDB.HISTORY_VISITS_URL, url); - visitData.put(LocalBrowserDB.HISTORY_VISITS_COUNT, visits); - visitsToSynthesize.add(visitData); - cursor.moveToNext(); - } - } - } finally { - if (cursor != null) - cursor.close(); - } - - flushBatchOperations(); - - // Now that we have flushed history records, we need to synthesize individual visits. We have - // gathered information about all of the visits we need to synthesize into visitsForSynthesis. - mDB.insertVisitsFromImportHistoryInBatch(mCr, mOperations, visitsToSynthesize); - - flushBatchOperations(); - } - - private void storeBitmap(byte[] data, String url) { - if (TextUtils.isEmpty(url) || data == null) { - return; - } - - final Bitmap bitmap = BitmapFactory.decodeByteArray(data, 0, data.length); - if (bitmap == null) { - return; - } - - final String iconUrl = IconsHelper.guessDefaultFaviconURL(url); - if (iconUrl == null) { - return; - } - - final DiskStorage storage = DiskStorage.get(mContext); - - storage.putIcon(url, bitmap); - storage.putMapping(url, iconUrl); - } - - protected Cursor query(Uri mainUri, Uri fallbackUri, String condition) { - final Cursor cursor = mCr.query(mainUri, null, condition, null, null); - if (Build.MANUFACTURER.equals(SAMSUNG_MANUFACTURER) && (cursor == null || cursor.getCount() == 0)) { - if (cursor != null) { - cursor.close(); - } - return mCr.query(fallbackUri, null, null, null, null); - } - return cursor; - } - - protected void flushBatchOperations() { - Log.d(LOGTAG, "Flushing " + mOperations.size() + " DB operations"); - try { - // We don't really care for the results, this is best-effort. - mCr.applyBatch(BrowserContract.AUTHORITY, mOperations); - } catch (RemoteException e) { - Log.e(LOGTAG, "Remote exception while updating db: ", e); - } catch (OperationApplicationException e) { - // Bug 716729 means this happens even in normal circumstances - Log.d(LOGTAG, "Error while applying database updates: ", e); - } - mOperations.clear(); - } - - @Override - public void run() { - if (mImportBookmarks) { - mergeBookmarks(); - } - if (mImportHistory) { - mergeHistory(); - } - - mOnDoneRunnable.run(); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/preferences/AndroidImportPreference.java b/mobile/android/base/java/org/mozilla/gecko/preferences/AndroidImportPreference.java deleted file mode 100644 index 0f1d3ec3f1..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/preferences/AndroidImportPreference.java +++ /dev/null @@ -1,112 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.preferences; - -import org.mozilla.gecko.AppConstants.Versions; -import org.mozilla.gecko.R; -import org.mozilla.gecko.restrictions.Restrictable; -import org.mozilla.gecko.restrictions.Restrictions; -import org.mozilla.gecko.util.ThreadUtils; - -import java.util.Set; - -import android.app.ProgressDialog; -import android.content.Context; -import android.preference.Preference; -import android.util.AttributeSet; -import android.util.Log; - -class AndroidImportPreference extends MultiPrefMultiChoicePreference { - private static final String LOGTAG = "AndroidImport"; - public static final String PREF_KEY = "android.not_a_preference.import_android"; - private static final String PREF_KEY_PREFIX = "import_android.data."; - private final Context mContext; - - public static class Handler implements GeckoPreferences.PrefHandler { - public boolean setupPref(Context context, Preference pref) { - // Feature disabled on devices running Android M+ (Bug 1183559) - return Versions.preMarshmallow && Restrictions.isAllowed(context, Restrictable.IMPORT_SETTINGS); - } - - public void onChange(Context context, Preference pref, Object newValue) { } - } - - public AndroidImportPreference(Context context, AttributeSet attrs) { - super(context, attrs); - mContext = context; - } - - @Override - protected void onDialogClosed(boolean positiveResult) { - super.onDialogClosed(positiveResult); - - if (!positiveResult) - return; - - boolean bookmarksChecked = false; - boolean historyChecked = false; - - Set values = getValues(); - - for (String value : values) { - // Import checkbox values are stored in Android prefs to - // remember their check states. The key names are import_android.data.X - String key = value.substring(PREF_KEY_PREFIX.length()); - if ("bookmarks".equals(key)) { - bookmarksChecked = true; - } else if ("history".equals(key)) { - historyChecked = true; - } - } - - runImport(bookmarksChecked, historyChecked); - } - - protected void runImport(final boolean doBookmarks, final boolean doHistory) { - Log.i(LOGTAG, "Importing Android history/bookmarks"); - if (!doBookmarks && !doHistory) { - return; - } - - final String dialogTitle; - if (doBookmarks && doHistory) { - dialogTitle = mContext.getString(R.string.bookmarkhistory_import_both); - } else if (doBookmarks) { - dialogTitle = mContext.getString(R.string.bookmarkhistory_import_bookmarks); - } else { - dialogTitle = mContext.getString(R.string.bookmarkhistory_import_history); - } - - final ProgressDialog dialog = - ProgressDialog.show(mContext, - dialogTitle, - mContext.getString(R.string.bookmarkhistory_import_wait), - true); - - final Runnable stopCallback = new Runnable() { - @Override - public void run() { - ThreadUtils.postToUiThread(new Runnable() { - @Override - public void run() { - dialog.dismiss(); - } - }); - } - }; - - ThreadUtils.postToBackgroundThread( - // Constructing AndroidImport may need finding the profile, - // which hits disk, so it needs to go into a Runnable too. - new Runnable() { - @Override - public void run() { - new AndroidImport(mContext, stopCallback, doBookmarks, doHistory).run(); - } - } - ); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/preferences/AppCompatPreferenceActivity.java b/mobile/android/base/java/org/mozilla/gecko/preferences/AppCompatPreferenceActivity.java deleted file mode 100644 index fb4a8f751d..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/preferences/AppCompatPreferenceActivity.java +++ /dev/null @@ -1,115 +0,0 @@ -/* - * Copyright (C) 2014 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.mozilla.gecko.preferences; - - -import android.content.res.Configuration; -import android.os.Bundle; -import android.preference.PreferenceActivity; -import android.support.annotation.LayoutRes; -import android.support.annotation.Nullable; -import android.support.v7.app.ActionBar; -import android.support.v7.app.AppCompatDelegate; -import android.support.v7.widget.Toolbar; -import android.view.MenuInflater; -import android.view.View; -import android.view.ViewGroup; - -/** - * A {@link android.preference.PreferenceActivity} which implements and proxies the necessary calls - * to be used with AppCompat. - * - * This technique can be used with an {@link android.app.Activity} class, not just - * {@link android.preference.PreferenceActivity}. - * - * This class was directly imported (without any modifications) from Android SDK examples, at: - * https://android.googlesource.com/platform/development/+/master/samples/Support7Demos/src/com/example/android/supportv7/app/AppCompatPreferenceActivity.java - */ -public abstract class AppCompatPreferenceActivity extends PreferenceActivity { - private AppCompatDelegate mDelegate; - @Override - protected void onCreate(Bundle savedInstanceState) { - getDelegate().installViewFactory(); - getDelegate().onCreate(savedInstanceState); - super.onCreate(savedInstanceState); - } - @Override - protected void onPostCreate(Bundle savedInstanceState) { - super.onPostCreate(savedInstanceState); - getDelegate().onPostCreate(savedInstanceState); - } - public ActionBar getSupportActionBar() { - return getDelegate().getSupportActionBar(); - } - public void setSupportActionBar(@Nullable Toolbar toolbar) { - getDelegate().setSupportActionBar(toolbar); - } - @Override - public MenuInflater getMenuInflater() { - return getDelegate().getMenuInflater(); - } - @Override - public void setContentView(@LayoutRes int layoutResID) { - getDelegate().setContentView(layoutResID); - } - @Override - public void setContentView(View view) { - getDelegate().setContentView(view); - } - @Override - public void setContentView(View view, ViewGroup.LayoutParams params) { - getDelegate().setContentView(view, params); - } - @Override - public void addContentView(View view, ViewGroup.LayoutParams params) { - getDelegate().addContentView(view, params); - } - @Override - protected void onPostResume() { - super.onPostResume(); - getDelegate().onPostResume(); - } - @Override - protected void onTitleChanged(CharSequence title, int color) { - super.onTitleChanged(title, color); - getDelegate().setTitle(title); - } - @Override - public void onConfigurationChanged(Configuration newConfig) { - super.onConfigurationChanged(newConfig); - getDelegate().onConfigurationChanged(newConfig); - } - @Override - protected void onStop() { - super.onStop(); - getDelegate().onStop(); - } - @Override - protected void onDestroy() { - super.onDestroy(); - getDelegate().onDestroy(); - } - public void invalidateOptionsMenu() { - getDelegate().invalidateOptionsMenu(); - } - private AppCompatDelegate getDelegate() { - if (mDelegate == null) { - mDelegate = AppCompatDelegate.create(this, null); - } - return mDelegate; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/preferences/ClearOnShutdownPref.java b/mobile/android/base/java/org/mozilla/gecko/preferences/ClearOnShutdownPref.java deleted file mode 100644 index 5218cd06db..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/preferences/ClearOnShutdownPref.java +++ /dev/null @@ -1,37 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.preferences; - -import java.util.HashSet; -import java.util.Set; - -import org.mozilla.gecko.GeckoSharedPrefs; -import org.mozilla.gecko.util.PrefUtils; - -import android.content.Context; -import android.content.SharedPreferences; -import android.preference.Preference; - -public class ClearOnShutdownPref implements GeckoPreferences.PrefHandler { - public static final String PREF = GeckoPreferences.NON_PREF_PREFIX + "history.clear_on_exit"; - - @Override - public boolean setupPref(Context context, Preference pref) { - // The pref is initialized asynchronously. Read the pref explicitly - // here to make sure we have the data. - final SharedPreferences prefs = GeckoSharedPrefs.forProfile(context); - final Set clearItems = PrefUtils.getStringSet(prefs, PREF, new HashSet()); - ((ListCheckboxPreference) pref).setChecked(clearItems.size() > 0); - return true; - } - - @Override - @SuppressWarnings("unchecked") - public void onChange(Context context, Preference pref, Object newValue) { - final Set vals = (Set) newValue; - ((ListCheckboxPreference) pref).setChecked(vals.size() > 0); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/preferences/CustomCheckBoxPreference.java b/mobile/android/base/java/org/mozilla/gecko/preferences/CustomCheckBoxPreference.java deleted file mode 100644 index 2934ca88e4..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/preferences/CustomCheckBoxPreference.java +++ /dev/null @@ -1,44 +0,0 @@ -/* 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.preferences; - -import android.content.Context; -import android.preference.CheckBoxPreference; -import android.util.AttributeSet; -import android.view.View; -import android.widget.TextView; - -/** - * Represents a Checkbox element in a preference menu. - * The title of the Checkbox can be larger than the view. - * In this case, it will be displayed in 2 or more lines. - * The default behavior of the class CheckBoxPreference - * doesn't wrap the title. - */ - -public class CustomCheckBoxPreference extends CheckBoxPreference { - - public CustomCheckBoxPreference(Context context) { - super(context); - } - - public CustomCheckBoxPreference(Context context, AttributeSet attrs) { - super(context, attrs); - } - public CustomCheckBoxPreference(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - } - - @Override - protected void onBindView(View view) { - super.onBindView(view); - final TextView title = (TextView) view.findViewById(android.R.id.title); - if (title != null) { - title.setSingleLine(false); - title.setEllipsize(null); - } - } - -} diff --git a/mobile/android/base/java/org/mozilla/gecko/preferences/CustomListCategory.java b/mobile/android/base/java/org/mozilla/gecko/preferences/CustomListCategory.java deleted file mode 100644 index ee5a46bef2..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/preferences/CustomListCategory.java +++ /dev/null @@ -1,72 +0,0 @@ -/* 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.preferences; - -import android.content.Context; -import android.preference.PreferenceCategory; -import android.util.AttributeSet; - -public abstract class CustomListCategory extends PreferenceCategory { - protected CustomListPreference mDefaultReference; - - public CustomListCategory(Context context) { - super(context); - } - - public CustomListCategory(Context context, AttributeSet attrs) { - super(context, attrs); - } - - public CustomListCategory(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - } - - @Override - protected void onAttachedToActivity() { - super.onAttachedToActivity(); - - setOrderingAsAdded(true); - } - - /** - * Set the default to some available list item. Used if the current default is removed or - * disabled. - */ - protected void setFallbackDefault() { - if (getPreferenceCount() > 0) { - CustomListPreference aItem = (CustomListPreference) getPreference(0); - setDefault(aItem); - } - } - - /** - * Removes the given item from the set of available list items. - * This only updates the UI, so callers are responsible for persisting any state. - * - * @param item The given item to remove. - */ - public void uninstall(CustomListPreference item) { - removePreference(item); - if (item == mDefaultReference) { - // If the default is being deleted, set a new default. - setFallbackDefault(); - } - } - - /** - * Sets the given item as the current default. - * This only updates the UI, so callers are responsible for persisting any state. - * - * @param item The intended new default. - */ - public void setDefault(CustomListPreference item) { - if (mDefaultReference != null) { - mDefaultReference.setIsDefault(false); - } - - item.setIsDefault(true); - mDefaultReference = item; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/preferences/CustomListPreference.java b/mobile/android/base/java/org/mozilla/gecko/preferences/CustomListPreference.java deleted file mode 100644 index 8b7e0e7b39..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/preferences/CustomListPreference.java +++ /dev/null @@ -1,182 +0,0 @@ -/* 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.preferences; - -import org.mozilla.gecko.R; - -import android.app.AlertDialog; -import android.content.Context; -import android.content.DialogInterface; -import android.content.res.Resources; -import android.preference.Preference; -import android.view.View; -import android.widget.TextView; - -/** - * Represents an element in a CustomListCategory preference menu. - * This preference con display a dialog when clicked, and also supports - * being set as a default item within the preference list category. - */ - -public abstract class CustomListPreference extends Preference implements View.OnLongClickListener { - protected String LOGTAG = "CustomListPreference"; - - // Indices of the buttons of the Dialog. - public static final int INDEX_SET_DEFAULT_BUTTON = 0; - - // Dialog item labels. - private String[] mDialogItems; - - // Dialog displayed when this element is tapped. - protected AlertDialog mDialog; - - // Cache label to avoid repeated use of the resource system. - protected final String LABEL_IS_DEFAULT; - protected final String LABEL_SET_AS_DEFAULT; - protected final String LABEL_REMOVE; - - protected boolean mIsDefault; - - // Enclosing parent category that contains this preference. - protected final CustomListCategory mParentCategory; - - /** - * Create a preference object to represent a list preference that is attached to - * a category. - * - * @param context The activity context we operate under. - * @param parentCategory The PreferenceCategory this object exists within. - */ - public CustomListPreference(Context context, CustomListCategory parentCategory) { - super(context); - - mParentCategory = parentCategory; - setLayoutResource(getPreferenceLayoutResource()); - - setOnPreferenceClickListener(new OnPreferenceClickListener() { - @Override - public boolean onPreferenceClick(Preference preference) { - CustomListPreference sPref = (CustomListPreference) preference; - sPref.showDialog(); - return true; - } - }); - - Resources res = getContext().getResources(); - - // Fetch these strings now, instead of every time we ever want to relabel a button. - LABEL_IS_DEFAULT = res.getString(R.string.pref_default); - LABEL_SET_AS_DEFAULT = res.getString(R.string.pref_dialog_set_default); - LABEL_REMOVE = res.getString(R.string.pref_dialog_remove); - } - - /** - * Returns the Android resource id for the layout. - */ - protected abstract int getPreferenceLayoutResource(); - - /** - * Set whether this object's UI should display this as the default item. - * Note: This must be called from the UI thread because it touches the view hierarchy. - * - * To ensure proper ordering, this method should only be called after this Preference - * is added to the PreferenceCategory. - * - * @param isDefault Flag indicating if this represents the default list item. - */ - public void setIsDefault(boolean isDefault) { - mIsDefault = isDefault; - if (isDefault) { - setOrder(0); - setSummary(LABEL_IS_DEFAULT); - } else { - setOrder(1); - setSummary(""); - } - } - - private String[] getCachedDialogItems() { - if (mDialogItems == null) { - mDialogItems = createDialogItems(); - } - return mDialogItems; - } - - /** - * Returns the strings to be displayed in the dialog. - */ - abstract protected String[] createDialogItems(); - - /** - * Display a dialog for this preference, when the preference is clicked. - */ - public void showDialog() { - final AlertDialog.Builder builder = new AlertDialog.Builder(getContext()); - builder.setTitle(getTitle().toString()); - builder.setItems(getCachedDialogItems(), new DialogInterface.OnClickListener() { - // Forward relevant events to the container class for handling. - @Override - public void onClick(DialogInterface dialog, int indexClicked) { - hideDialog(); - onDialogIndexClicked(indexClicked); - } - }); - - configureDialogBuilder(builder); - - // We have to construct the dialog itself on the UI thread. - mDialog = builder.create(); - mDialog.setOnShowListener(new DialogInterface.OnShowListener() { - // Called when the dialog is shown (so we're finally able to manipulate button enabledness). - @Override - public void onShow(DialogInterface dialog) { - configureShownDialog(); - } - }); - mDialog.show(); - } - - /** - * (Optional) Configure the AlertDialog builder. - */ - protected void configureDialogBuilder(AlertDialog.Builder builder) { - return; - } - - abstract protected void onDialogIndexClicked(int index); - - /** - * Disables buttons in the shown AlertDialog as required. The button elements are not created - * until after show is called, so this method has to be called from the onShowListener above. - * @see this.showDialog - */ - protected void configureShownDialog() { - // If this is already the default list item, disable the button for setting this as the default. - final TextView defaultButton = (TextView) mDialog.getListView().getChildAt(INDEX_SET_DEFAULT_BUTTON); - if (mIsDefault) { - defaultButton.setEnabled(false); - - // Failure to unregister this listener leads to tapping the button dismissing the dialog - // without doing anything. - defaultButton.setOnClickListener(null); - } - } - - /** - * Hide the dialog we previously created, if any. - */ - public void hideDialog() { - if (mDialog != null && mDialog.isShowing()) { - mDialog.dismiss(); - } - } - - @Override - public boolean onLongClick(View view) { - // Show the preference dialog on long-press. - showDialog(); - return true; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/preferences/DistroSharedPrefsImport.java b/mobile/android/base/java/org/mozilla/gecko/preferences/DistroSharedPrefsImport.java deleted file mode 100644 index 1e235640e3..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/preferences/DistroSharedPrefsImport.java +++ /dev/null @@ -1,61 +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.preferences; - -import org.mozilla.gecko.GeckoSharedPrefs; -import org.mozilla.gecko.distribution.Distribution; - -import android.content.Context; -import android.content.SharedPreferences; -import android.util.Log; -import org.json.JSONException; -import org.json.JSONObject; - -import java.util.Iterator; - -public class DistroSharedPrefsImport { - - public static final String LOGTAG = DistroSharedPrefsImport.class.getSimpleName(); - - public static void importPreferences(final Context context, final Distribution distribution) { - if (distribution == null) { - return; - } - - final JSONObject preferences = distribution.getAndroidPreferences(); - if (preferences.length() == 0) { - return; - } - - final Iterator keys = preferences.keys(); - final SharedPreferences.Editor sharedPreferences = GeckoSharedPrefs.forProfile(context).edit(); - - while (keys.hasNext()) { - final String key = (String) keys.next(); - final Object value; - try { - value = preferences.get(key); - } catch (JSONException e) { - Log.e(LOGTAG, "Unable to completely process Android Preferences JSON.", e); - continue; - } - - // We currently don't support Float preferences. - if (value instanceof String) { - sharedPreferences.putString(GeckoPreferences.NON_PREF_PREFIX + key, (String) value); - } else if (value instanceof Boolean) { - sharedPreferences.putBoolean(GeckoPreferences.NON_PREF_PREFIX + key, (boolean) value); - } else if (value instanceof Integer) { - sharedPreferences.putInt(GeckoPreferences.NON_PREF_PREFIX + key, (int) value); - } else if (value instanceof Long) { - sharedPreferences.putLong(GeckoPreferences.NON_PREF_PREFIX + key, (long) value); - } else { - Log.d(LOGTAG, "Unknown preference value type whilst importing android preferences from distro file."); - } - } - sharedPreferences.apply(); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/preferences/FontSizePreference.java b/mobile/android/base/java/org/mozilla/gecko/preferences/FontSizePreference.java deleted file mode 100644 index c77c2cc23a..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/preferences/FontSizePreference.java +++ /dev/null @@ -1,192 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.preferences; - -import org.mozilla.gecko.R; - -import android.app.AlertDialog; -import android.content.Context; -import android.content.res.Resources; -import android.graphics.Color; -import android.preference.DialogPreference; -import android.util.AttributeSet; -import android.util.Log; -import android.util.TypedValue; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.Button; -import android.widget.ScrollView; -import android.widget.TextView; - -import java.util.HashMap; - -class FontSizePreference extends DialogPreference { - private static final String LOGTAG = "FontSizePreference"; - private static final int TWIP_TO_PT_RATIO = 20; // 20 twip = 1 point. - private static final int PREVIEW_FONT_SIZE_UNIT = TypedValue.COMPLEX_UNIT_PT; - private static final int DEFAULT_FONT_INDEX = 2; - - private final Context mContext; - /** Container for mPreviewFontView to allow for scrollable padding at the top of the view. */ - private ScrollView mScrollingContainer; - private TextView mPreviewFontView; - private Button mIncreaseFontButton; - private Button mDecreaseFontButton; - - private final String[] mFontTwipValues; - private final String[] mFontSizeNames; // Ex: "Small". - /** Index into the above arrays for the saved preference value (from Gecko). */ - private int mSavedFontIndex = DEFAULT_FONT_INDEX; - /** Index into the above arrays for the currently displayed font size (the preview). */ - private int mPreviewFontIndex = mSavedFontIndex; - private final HashMap mFontTwipToIndexMap; - - public FontSizePreference(Context context, AttributeSet attrs) { - super(context, attrs); - mContext = context; - - final Resources res = mContext.getResources(); - mFontTwipValues = res.getStringArray(R.array.pref_font_size_values); - mFontSizeNames = res.getStringArray(R.array.pref_font_size_entries); - mFontTwipToIndexMap = new HashMap(); - for (int i = 0; i < mFontTwipValues.length; ++i) { - mFontTwipToIndexMap.put(mFontTwipValues[i], i); - } - } - - @Override - protected void onPrepareDialogBuilder(AlertDialog.Builder builder) { - final LayoutInflater inflater = - (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); - View dialogView = inflater.inflate(R.layout.font_size_preference, null); - initInternalViews(dialogView); - updatePreviewFontSize(mFontTwipValues[mPreviewFontIndex]); - - builder.setTitle(null); - builder.setView(dialogView); - } - - /** Saves relevant views to instance variables and initializes their settings. */ - private void initInternalViews(View dialogView) { - mScrollingContainer = (ScrollView) dialogView.findViewById(R.id.scrolling_container); - // Background cannot be set in XML (see bug 783597 - TODO: Change this to XML when bug is fixed). - mScrollingContainer.setBackgroundColor(Color.WHITE); - mPreviewFontView = (TextView) dialogView.findViewById(R.id.preview); - - mDecreaseFontButton = (Button) dialogView.findViewById(R.id.decrease_preview_font_button); - mIncreaseFontButton = (Button) dialogView.findViewById(R.id.increase_preview_font_button); - setButtonState(mPreviewFontIndex); - mDecreaseFontButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - mPreviewFontIndex = Math.max(mPreviewFontIndex - 1, 0); - updatePreviewFontSize(mFontTwipValues[mPreviewFontIndex]); - mIncreaseFontButton.setEnabled(true); - // If we reached the minimum index, disable the button. - if (mPreviewFontIndex == 0) { - mDecreaseFontButton.setEnabled(false); - } - } - }); - mIncreaseFontButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - mPreviewFontIndex = Math.min(mPreviewFontIndex + 1, mFontTwipValues.length - 1); - updatePreviewFontSize(mFontTwipValues[mPreviewFontIndex]); - - mDecreaseFontButton.setEnabled(true); - // If we reached the maximum index, disable the button. - if (mPreviewFontIndex == mFontTwipValues.length - 1) { - mIncreaseFontButton.setEnabled(false); - } - } - }); - } - - @Override - protected void onDialogClosed(boolean positiveResult) { - super.onDialogClosed(positiveResult); - if (!positiveResult) { - mPreviewFontIndex = mSavedFontIndex; - return; - } - mSavedFontIndex = mPreviewFontIndex; - final String twipVal = mFontTwipValues[mSavedFontIndex]; - final OnPreferenceChangeListener prefChangeListener = getOnPreferenceChangeListener(); - if (prefChangeListener == null) { - Log.e(LOGTAG, "PreferenceChangeListener is null. FontSizePreference will not be saved to Gecko."); - return; - } - prefChangeListener.onPreferenceChange(this, twipVal); - } - - /** - * Finds the index of the given twip value and sets it as the saved preference value. Also the - * current preview text size to the given value. Does not update the mPreviewFontView text size. - */ - protected void setSavedFontSize(String twip) { - final Integer index = mFontTwipToIndexMap.get(twip); - if (index != null) { - mSavedFontIndex = index; - mPreviewFontIndex = mSavedFontIndex; - return; - } - resetSavedFontSizeToDefault(); - Log.e(LOGTAG, "setSavedFontSize: Given font size does not exist in twip values map. Reverted to default font size."); - } - - /** - * Updates the mPreviewFontView to the given text size, resets the container's scroll to the top - * left, and invalidates the view. Does not update the font indices. - */ - private void updatePreviewFontSize(String twip) { - float pt = convertTwipStrToPT(twip); - // Android will not render a font size of 0 pt but for Gecko, 0 twip turns off font - // inflation. Thus we special case 0 twip to display a renderable font size. - if (pt == 0) { - // Android adds an inexplicable extra margin on the smallest font size so to get around - // this, we reinflate the view. - ViewGroup parentView = (ViewGroup) mScrollingContainer.getParent(); - parentView.removeAllViews(); - final LayoutInflater inflater = - (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); - View dialogView = inflater.inflate(R.layout.font_size_preference, parentView); - initInternalViews(dialogView); - mPreviewFontView.setTextSize(PREVIEW_FONT_SIZE_UNIT, 1); - } else { - mPreviewFontView.setTextSize(PREVIEW_FONT_SIZE_UNIT, pt); - } - mScrollingContainer.scrollTo(0, 0); - } - - /** - * Resets the font indices to the default value. Does not update the mPreviewFontView text size. - */ - private void resetSavedFontSizeToDefault() { - mSavedFontIndex = DEFAULT_FONT_INDEX; - mPreviewFontIndex = mSavedFontIndex; - } - - private void setButtonState(int index) { - if (index == 0) { - mDecreaseFontButton.setEnabled(false); - } else if (index == mFontTwipValues.length - 1) { - mIncreaseFontButton.setEnabled(false); - } - } - - /** - * Returns the name of the font size (ex: "Small") at the currently saved preference value. - */ - protected String getSavedFontSizeName() { - return mFontSizeNames[mSavedFontIndex]; - } - - private float convertTwipStrToPT(String twip) { - return Float.parseFloat(twip) / TWIP_TO_PT_RATIO; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/preferences/GeckoPreferenceFragment.java b/mobile/android/base/java/org/mozilla/gecko/preferences/GeckoPreferenceFragment.java deleted file mode 100644 index 6be9e6ea50..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/preferences/GeckoPreferenceFragment.java +++ /dev/null @@ -1,296 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.preferences; - -import java.util.Locale; - -import org.mozilla.gecko.AppConstants.Versions; -import org.mozilla.gecko.BrowserLocaleManager; -import org.mozilla.gecko.GeckoApplication; -import org.mozilla.gecko.GeckoSharedPrefs; -import org.mozilla.gecko.LocaleManager; -import org.mozilla.gecko.PrefsHelper; -import org.mozilla.gecko.R; -import org.mozilla.gecko.Telemetry; -import org.mozilla.gecko.TelemetryContract; -import org.mozilla.gecko.TelemetryContract.Method; -import org.mozilla.gecko.fxa.AccountLoader; -import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount; - -import android.accounts.Account; -import android.app.ActionBar; -import android.app.Activity; -import android.app.LoaderManager; -import android.content.Context; -import android.content.Loader; -import android.content.res.Configuration; -import android.content.res.Resources; -import android.os.Bundle; -import android.preference.PreferenceActivity; -import android.preference.PreferenceFragment; -import android.preference.PreferenceScreen; -import android.util.Log; -import android.view.Menu; -import android.view.MenuInflater; - -import com.squareup.leakcanary.RefWatcher; - -/* A simple implementation of PreferenceFragment for large screen devices - * This will strip category headers (so that they aren't shown to the user twice) - * as well as initializing Gecko prefs when a fragment is shown. -*/ -public class GeckoPreferenceFragment extends PreferenceFragment { - - public static final int ACCOUNT_LOADER_ID = 1; - private AccountLoaderCallbacks accountLoaderCallbacks; - private SyncPreference syncPreference; - - @Override - public void onConfigurationChanged(Configuration newConfig) { - super.onConfigurationChanged(newConfig); - Log.d(LOGTAG, "onConfigurationChanged: " + newConfig.locale); - - final Activity context = getActivity(); - - final LocaleManager localeManager = BrowserLocaleManager.getInstance(); - final Locale changed = localeManager.onSystemConfigurationChanged(context, getResources(), newConfig, lastLocale); - if (changed != null) { - applyLocale(changed); - } - } - - private static final String LOGTAG = "GeckoPreferenceFragment"; - private PrefsHelper.PrefHandler mPrefsRequest; - private Locale lastLocale = Locale.getDefault(); - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - // Write prefs to our custom GeckoSharedPrefs file. - getPreferenceManager().setSharedPreferencesName(GeckoSharedPrefs.APP_PREFS_NAME); - - int res = getResource(); - if (res == R.xml.preferences) { - Telemetry.startUISession(TelemetryContract.Session.SETTINGS); - } else { - final String resourceName = getArguments().getString("resource"); - Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, Method.SETTINGS, resourceName); - } - - // Display a menu for Search preferences. - if (res == R.xml.preferences_search) { - setHasOptionsMenu(true); - } - - addPreferencesFromResource(res); - - PreferenceScreen screen = getPreferenceScreen(); - setPreferenceScreen(screen); - mPrefsRequest = ((GeckoPreferences)getActivity()).setupPreferences(screen); - syncPreference = (SyncPreference) findPreference(GeckoPreferences.PREFS_SYNC); - } - - /** - * Return the title to use for this preference fragment. - * - * We only return titles for the preference screens that are - * launched directly, and thus might need to be redisplayed. - * - * This method sets the title that you see on non-multi-pane devices. - */ - private String getTitle() { - final int res = getResource(); - if (res == R.xml.preferences) { - return getString(R.string.settings_title); - } - - // We can launch this category from the Data Reporting notification. - if (res == R.xml.preferences_privacy) { - return getString(R.string.pref_category_privacy_short); - } - - // We can launch this category from the the magnifying glass in the quick search bar. - if (res == R.xml.preferences_search) { - return getString(R.string.pref_category_search); - } - - // Launched as action from content notifications. - if (res == R.xml.preferences_notifications) { - return getString(R.string.pref_category_notifications); - } - - return null; - } - - /** - * Return the header id for this preference fragment. This allows - * us to select the correct header when launching a preference - * screen directly. - * - * We only return titles for the preference screens that are - * launched directly. - */ - private int getHeader() { - final int res = getResource(); - if (res == R.xml.preferences) { - return R.id.pref_header_general; - } - - // We can launch this category from the Data Reporting notification. - if (res == R.xml.preferences_privacy) { - return R.id.pref_header_privacy; - } - - // We can launch this category from the the magnifying glass in the quick search bar. - if (res == R.xml.preferences_search) { - return R.id.pref_header_search; - } - - // Launched as action from content notifications. - if (res == R.xml.preferences_notifications) { - return R.id.pref_header_notifications; - } - - return -1; - } - - private void updateTitle() { - final String newTitle = getTitle(); - if (newTitle == null) { - Log.d(LOGTAG, "No new title to show."); - return; - } - - final GeckoPreferences activity = (GeckoPreferences) getActivity(); - if (activity.isMultiPane()) { - // In a multi-pane activity, the title is "Settings", and the action - // bar is along the top of the screen. We don't want to change those. - activity.showBreadCrumbs(newTitle, newTitle); - activity.switchToHeader(getHeader()); - return; - } - - Log.v(LOGTAG, "Setting activity title to " + newTitle); - activity.setTitle(newTitle); - } - - @Override - public void onActivityCreated(Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - accountLoaderCallbacks = new AccountLoaderCallbacks(); - getLoaderManager().initLoader(ACCOUNT_LOADER_ID, null, accountLoaderCallbacks); - } - - @Override - public void onResume() { - // This is a little delicate. Ensure that you do nothing prior to - // super.onResume that you wouldn't do in onCreate. - applyLocale(Locale.getDefault()); - super.onResume(); - - // Force reload as the account may have been deleted while the app was in background. - getLoaderManager().restartLoader(ACCOUNT_LOADER_ID, null, accountLoaderCallbacks); - } - - private void applyLocale(final Locale currentLocale) { - final Context context = getActivity().getApplicationContext(); - - BrowserLocaleManager.getInstance().updateConfiguration(context, currentLocale); - - if (!currentLocale.equals(lastLocale)) { - // Locales differ. Let's redisplay. - Log.d(LOGTAG, "Locale changed: " + currentLocale); - this.lastLocale = currentLocale; - - // Rebuild the list to reflect the current locale. - getPreferenceScreen().removeAll(); - addPreferencesFromResource(getResource()); - } - - // Fix the parent title regardless. - updateTitle(); - } - - /* - * Get the resource from Fragment arguments and return it. - * - * If no resource can be found, return the resource id of the default preference screen. - */ - private int getResource() { - int resid = 0; - - final String resourceName = getArguments().getString("resource"); - final Activity activity = getActivity(); - - if (resourceName != null) { - // Fetch resource id by resource name. - final Resources resources = activity.getResources(); - final String packageName = activity.getPackageName(); - resid = resources.getIdentifier(resourceName, "xml", packageName); - } - - if (resid == 0) { - // The resource was invalid. Use the default resource. - Log.e(LOGTAG, "Failed to find resource: " + resourceName + ". Displaying default settings."); - - boolean isMultiPane = ((GeckoPreferences) activity).isMultiPane(); - resid = isMultiPane ? R.xml.preferences_general_tablet : R.xml.preferences; - } - - return resid; - } - - @Override - public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { - super.onCreateOptionsMenu(menu, inflater); - inflater.inflate(R.menu.preferences_search_menu, menu); - } - - @Override - public void onDestroy() { - super.onDestroy(); - if (mPrefsRequest != null) { - PrefsHelper.removeObserver(mPrefsRequest); - mPrefsRequest = null; - } - - final int res = getResource(); - if (res == R.xml.preferences) { - Telemetry.stopUISession(TelemetryContract.Session.SETTINGS); - } - - GeckoApplication.watchReference(getActivity(), this); - } - - private class AccountLoaderCallbacks implements LoaderManager.LoaderCallbacks { - @Override - public Loader onCreateLoader(int id, Bundle args) { - return new AccountLoader(getActivity()); - } - - @Override - public void onLoadFinished(Loader loader, Account account) { - if (syncPreference == null) { - return; - } - - if (account == null) { - syncPreference.update(null); - return; - } - - syncPreference.update(new AndroidFxAccount(getActivity(), account)); - } - - @Override - public void onLoaderReset(Loader loader) { - if (syncPreference != null) { - syncPreference.update(null); - } - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/preferences/GeckoPreferences.java b/mobile/android/base/java/org/mozilla/gecko/preferences/GeckoPreferences.java deleted file mode 100644 index aab5be2de4..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/preferences/GeckoPreferences.java +++ /dev/null @@ -1,1514 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.preferences; - -import org.json.JSONArray; -import org.mozilla.gecko.AboutPages; -import org.mozilla.gecko.AdjustConstants; -import org.mozilla.gecko.AppConstants; -import org.mozilla.gecko.AppConstants.Versions; -import org.mozilla.gecko.BrowserApp; -import org.mozilla.gecko.BrowserLocaleManager; -import org.mozilla.gecko.DataReportingNotification; -import org.mozilla.gecko.DynamicToolbar; -import org.mozilla.gecko.EventDispatcher; -import org.mozilla.gecko.GeckoActivityStatus; -import org.mozilla.gecko.GeckoApp; -import org.mozilla.gecko.GeckoAppShell; -import org.mozilla.gecko.GeckoApplication; -import org.mozilla.gecko.GeckoProfile; -import org.mozilla.gecko.GeckoSharedPrefs; -import org.mozilla.gecko.LocaleManager; -import org.mozilla.gecko.Locales; -import org.mozilla.gecko.PrefsHelper; -import org.mozilla.gecko.R; -import org.mozilla.gecko.SnackbarBuilder; -import org.mozilla.gecko.Telemetry; -import org.mozilla.gecko.TelemetryContract; -import org.mozilla.gecko.TelemetryContract.Method; -import org.mozilla.gecko.activitystream.ActivityStream; -import org.mozilla.gecko.background.common.GlobalConstants; -import org.mozilla.gecko.db.BrowserContract.SuggestedSites; -import org.mozilla.gecko.feeds.FeedService; -import org.mozilla.gecko.feeds.action.CheckForUpdatesAction; -import org.mozilla.gecko.permissions.Permissions; -import org.mozilla.gecko.restrictions.Restrictable; -import org.mozilla.gecko.restrictions.Restrictions; -import org.mozilla.gecko.tabqueue.TabQueueHelper; -import org.mozilla.gecko.tabqueue.TabQueuePrompt; -import org.mozilla.gecko.updater.UpdateService; -import org.mozilla.gecko.updater.UpdateServiceHelper; -import org.mozilla.gecko.util.ContextUtils; -import org.mozilla.gecko.util.EventCallback; -import org.mozilla.gecko.util.HardwareUtils; -import org.mozilla.gecko.util.InputOptionsUtils; -import org.mozilla.gecko.util.NativeEventListener; -import org.mozilla.gecko.util.NativeJSObject; -import org.mozilla.gecko.util.ThreadUtils; - -import android.annotation.TargetApi; -import android.app.AlertDialog; -import android.app.Dialog; -import android.app.Fragment; -import android.app.FragmentManager; -import android.app.NotificationManager; -import android.content.ContentResolver; -import android.content.Context; -import android.content.DialogInterface; -import android.content.Intent; -import android.content.SharedPreferences; -import android.content.SharedPreferences.OnSharedPreferenceChangeListener; -import android.content.res.Configuration; -import android.Manifest; -import android.os.Build; -import android.os.Bundle; -import android.preference.CheckBoxPreference; -import android.preference.EditTextPreference; -import android.preference.ListPreference; -import android.preference.Preference; -import android.preference.Preference.OnPreferenceChangeListener; -import android.preference.Preference.OnPreferenceClickListener; -import android.preference.PreferenceActivity; -import android.preference.PreferenceGroup; -import android.preference.SwitchPreference; -import android.preference.TwoStatePreference; -import android.support.design.widget.Snackbar; -import android.support.design.widget.TextInputLayout; -import android.support.v4.content.LocalBroadcastManager; -import android.support.v7.app.ActionBar; -import android.text.Editable; -import android.text.InputType; -import android.text.TextUtils; -import android.text.TextWatcher; -import android.util.Log; -import android.view.MenuItem; -import android.view.View; -import android.widget.AdapterView; -import android.widget.EditText; -import android.widget.LinearLayout; -import android.widget.ListAdapter; -import android.widget.ListView; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; -import java.util.Locale; -import java.util.Map; - -public class GeckoPreferences -extends AppCompatPreferenceActivity -implements -GeckoActivityStatus, -NativeEventListener, -OnPreferenceChangeListener, -OnSharedPreferenceChangeListener -{ - private static final String LOGTAG = "GeckoPreferences"; - - // We have a white background, which makes transitions on - // some devices look bad. Don't use transitions on those - // devices. - private static final boolean NO_TRANSITIONS = HardwareUtils.IS_KINDLE_DEVICE; - private static final int NO_SUCH_ID = 0; - - public static final String NON_PREF_PREFIX = "android.not_a_preference."; - public static final String INTENT_EXTRA_RESOURCES = "resource"; - public static final String PREFS_TRACKING_PROTECTION_PROMPT_SHOWN = NON_PREF_PREFIX + "trackingProtectionPromptShown"; - public static String PREFS_HEALTHREPORT_UPLOAD_ENABLED = NON_PREF_PREFIX + "healthreport.uploadEnabled"; - public static final String PREFS_SYNC = NON_PREF_PREFIX + "sync"; - - private static boolean sIsCharEncodingEnabled; - private boolean mInitialized; - private PrefsHelper.PrefHandler mPrefsRequest; - private List
mHeaders; - - // These match keys in resources/xml*/preferences*.xml - private static final String PREFS_SEARCH_RESTORE_DEFAULTS = NON_PREF_PREFIX + "search.restore_defaults"; - private static final String PREFS_DATA_REPORTING_PREFERENCES = NON_PREF_PREFIX + "datareporting.preferences"; - private static final String PREFS_TELEMETRY_ENABLED = "toolkit.telemetry.enabled"; - private static final String PREFS_CRASHREPORTER_ENABLED = "datareporting.crashreporter.submitEnabled"; - private static final String PREFS_MENU_CHAR_ENCODING = "browser.menu.showCharacterEncoding"; - private static final String PREFS_MP_ENABLED = "privacy.masterpassword.enabled"; - private static final String PREFS_UPDATER_AUTODOWNLOAD = "app.update.autodownload"; - private static final String PREFS_UPDATER_URL = "app.update.url.android"; - private static final String PREFS_GEO_REPORTING = NON_PREF_PREFIX + "app.geo.reportdata"; - private static final String PREFS_GEO_LEARN_MORE = NON_PREF_PREFIX + "geo.learn_more"; - private static final String PREFS_HEALTHREPORT_LINK = NON_PREF_PREFIX + "healthreport.link"; - private static final String PREFS_DEVTOOLS_REMOTE_USB_ENABLED = "devtools.remote.usb.enabled"; - private static final String PREFS_DEVTOOLS_REMOTE_WIFI_ENABLED = "devtools.remote.wifi.enabled"; - private static final String PREFS_DEVTOOLS_REMOTE_LINK = NON_PREF_PREFIX + "remote_debugging.link"; - private static final String PREFS_TRACKING_PROTECTION = "privacy.trackingprotection.state"; - private static final String PREFS_TRACKING_PROTECTION_PB = "privacy.trackingprotection.pbmode.enabled"; - private static final String PREFS_ZOOMED_VIEW_ENABLED = "ui.zoomedview.enabled"; - public static final String PREFS_VOICE_INPUT_ENABLED = NON_PREF_PREFIX + "voice_input_enabled"; - public static final String PREFS_QRCODE_ENABLED = NON_PREF_PREFIX + "qrcode_enabled"; - private static final String PREFS_TRACKING_PROTECTION_PRIVATE_BROWSING = "privacy.trackingprotection.pbmode.enabled"; - private static final String PREFS_TRACKING_PROTECTION_LEARN_MORE = NON_PREF_PREFIX + "trackingprotection.learn_more"; - private static final String PREFS_CLEAR_PRIVATE_DATA = NON_PREF_PREFIX + "privacy.clear"; - private static final String PREFS_CLEAR_PRIVATE_DATA_EXIT = NON_PREF_PREFIX + "history.clear_on_exit"; - private static final String PREFS_SCREEN_ADVANCED = NON_PREF_PREFIX + "advanced_screen"; - public static final String PREFS_HOMEPAGE = NON_PREF_PREFIX + "homepage"; - public static final String PREFS_HOMEPAGE_PARTNER_COPY = GeckoPreferences.PREFS_HOMEPAGE + ".partner"; - public static final String PREFS_HISTORY_SAVED_SEARCH = NON_PREF_PREFIX + "search.search_history.enabled"; - private static final String PREFS_FAQ_LINK = NON_PREF_PREFIX + "faq.link"; - private static final String PREFS_FEEDBACK_LINK = NON_PREF_PREFIX + "feedback.link"; - public static final String PREFS_NOTIFICATIONS_CONTENT = NON_PREF_PREFIX + "notifications.content"; - public static final String PREFS_NOTIFICATIONS_CONTENT_LEARN_MORE = NON_PREF_PREFIX + "notifications.content.learn_more"; - public static final String PREFS_NOTIFICATIONS_WHATS_NEW = NON_PREF_PREFIX + "notifications.whats_new"; - public static final String PREFS_APP_UPDATE_LAST_BUILD_ID = "app.update.last_build_id"; - public static final String PREFS_READ_PARTNER_CUSTOMIZATIONS_PROVIDER = NON_PREF_PREFIX + "distribution.read_partner_customizations_provider"; - public static final String PREFS_READ_PARTNER_BOOKMARKS_PROVIDER = NON_PREF_PREFIX + "distribution.read_partner_bookmarks_provider"; - public static final String PREFS_CUSTOM_TABS = NON_PREF_PREFIX + "customtabs"; - public static final String PREFS_ACTIVITY_STREAM = NON_PREF_PREFIX + "activitystream"; - public static final String PREFS_CATEGORY_EXPERIMENTAL_FEATURES = NON_PREF_PREFIX + "category_experimental"; - - private static final String ACTION_STUMBLER_UPLOAD_PREF = "STUMBLER_PREF"; - - - // This isn't a Gecko pref, even if it looks like one. - private static final String PREFS_BROWSER_LOCALE = "locale"; - - public static final String PREFS_RESTORE_SESSION = NON_PREF_PREFIX + "restoreSession3"; - public static final String PREFS_RESTORE_SESSION_FROM_CRASH = "browser.sessionstore.resume_from_crash"; - public static final String PREFS_RESTORE_SESSION_MAX_CRASH_RESUMES = "browser.sessionstore.max_resumed_crashes"; - public static final String PREFS_TAB_QUEUE = NON_PREF_PREFIX + "tab_queue"; - public static final String PREFS_TAB_QUEUE_LAST_SITE = NON_PREF_PREFIX + "last_site"; - public static final String PREFS_TAB_QUEUE_LAST_TIME = NON_PREF_PREFIX + "last_time"; - - private static final String PREFS_DYNAMIC_TOOLBAR = "browser.chrome.dynamictoolbar"; - - // These values are chosen to be distinct from other Activity constants. - private static final int REQUEST_CODE_PREF_SCREEN = 5; - private static final int RESULT_CODE_EXIT_SETTINGS = 6; - - // Result code used when a locale preference changes. - // Callers can recognize this code to refresh themselves to - // accommodate a locale change. - public static final int RESULT_CODE_LOCALE_DID_CHANGE = 7; - - private static final int REQUEST_CODE_TAB_QUEUE = 8; - - private final Map HANDLERS; - { - final HashMap tempHandlers = new HashMap<>(2); - tempHandlers.put(ClearOnShutdownPref.PREF, new ClearOnShutdownPref()); - tempHandlers.put(AndroidImportPreference.PREF_KEY, new AndroidImportPreference.Handler()); - HANDLERS = Collections.unmodifiableMap(tempHandlers); - } - - private SwitchPreference tabQueuePreference; - - /** - * Track the last locale so we know whether to redisplay. - */ - private Locale lastLocale = Locale.getDefault(); - private boolean localeSwitchingIsEnabled; - - private void startActivityForResultChoosingTransition(final Intent intent, final int requestCode) { - startActivityForResult(intent, requestCode); - if (NO_TRANSITIONS) { - overridePendingTransition(0, 0); - } - } - - private void finishChoosingTransition() { - finish(); - if (NO_TRANSITIONS) { - overridePendingTransition(0, 0); - } - } - private void updateActionBarTitle(int title) { - final String newTitle = getString(title); - if (newTitle != null) { - Log.v(LOGTAG, "Setting action bar title to " + newTitle); - - setTitle(newTitle); - } - } - - /** - * We only call this method for pre-HC versions of Android. - */ - private void updateTitleForPrefsResource(int res) { - // At present we only need to do this for non-leaf prefs views - // and the locale switcher itself. - int title = -1; - if (res == R.xml.preferences) { - title = R.string.settings_title; - } else if (res == R.xml.preferences_locale) { - title = R.string.pref_category_language; - } else if (res == R.xml.preferences_vendor) { - title = R.string.pref_category_vendor; - } else if (res == R.xml.preferences_general) { - title = R.string.pref_category_general; - } else if (res == R.xml.preferences_search) { - title = R.string.pref_category_search; - } - if (title != -1) { - setTitle(title); - } - } - - private void onLocaleChanged(Locale newLocale) { - Log.d(LOGTAG, "onLocaleChanged: " + newLocale); - - BrowserLocaleManager.getInstance().updateConfiguration(getApplicationContext(), newLocale); - this.lastLocale = newLocale; - - if (isMultiPane()) { - // This takes care of the left pane. - invalidateHeaders(); - - // Detach and reattach the current prefs pane so that it - // reflects the new locale. - final FragmentManager fragmentManager = getFragmentManager(); - int id = getResources().getIdentifier("android:id/prefs", null, null); - final Fragment current = fragmentManager.findFragmentById(id); - if (current != null) { - fragmentManager.beginTransaction() - .disallowAddToBackStack() - .detach(current) - .attach(current) - .commitAllowingStateLoss(); - } else { - Log.e(LOGTAG, "No prefs fragment to reattach!"); - } - - // Because Android just rebuilt the activity itself with the - // old language, we need to update the top title and other - // wording again. - if (onIsMultiPane()) { - updateActionBarTitle(R.string.settings_title); - } - - // Update the title to for the preference pane that we're currently showing. - final int titleId = getIntent().getExtras().getInt(PreferenceActivity.EXTRA_SHOW_FRAGMENT_TITLE); - if (titleId != NO_SUCH_ID) { - setTitle(titleId); - } else { - throw new IllegalStateException("Title id not found in intent bundle extras"); - } - - // Don't finish the activity -- we just reloaded all of the - // individual parts! -- but when it returns, make sure that the - // caller knows the locale changed. - setResult(RESULT_CODE_LOCALE_DID_CHANGE); - return; - } - - refreshSuggestedSites(); - - // Cause the current fragment to redisplay, the hard way. - // This avoids nonsense with trying to reach inside fragments and force them - // to redisplay themselves. - // We also don't need to update the title. - final Intent intent = (Intent) getIntent().clone(); - intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION); - startActivityForResultChoosingTransition(intent, REQUEST_CODE_PREF_SCREEN); - - setResult(RESULT_CODE_LOCALE_DID_CHANGE); - finishChoosingTransition(); - } - - private void checkLocale() { - final Locale currentLocale = Locale.getDefault(); - Log.v(LOGTAG, "Checking locale: " + currentLocale + " vs " + lastLocale); - if (currentLocale.equals(lastLocale)) { - return; - } - - onLocaleChanged(currentLocale); - } - - @Override - protected void onCreate(Bundle savedInstanceState) { - // Apply the current user-selected locale, if necessary. - checkLocale(); - - // Track this so we can decide whether to show locale options. - // See also the workaround below for Bug 1015209. - localeSwitchingIsEnabled = BrowserLocaleManager.getInstance().isEnabled(); - - // For Android v11+ where we use Fragments (v11+ only due to bug 866352), - // check that PreferenceActivity.EXTRA_SHOW_FRAGMENT has been set - // (or set it) before super.onCreate() is called so Android can display - // the correct Fragment resource. - // Note: this seems to only be required for non-multipane devices, multipane - // manages to automatically select the correct fragments. - if (!getIntent().hasExtra(PreferenceActivity.EXTRA_SHOW_FRAGMENT)) { - // Set up the default fragment if there is no explicit fragment to show. - setupTopLevelFragmentIntent(); - } - - // We must call this before setTitle to avoid crashes. Most devices don't seem to care - // (we used to call onCreate later), however the ASUS TF300T (running 4.2) crashes - // with an NPE in android.support.v7.app.AppCompatDelegateImplV7.ensureSubDecor(), and it's - // likely other strange devices (other Asus devices, some Samsungs) could do the same. - super.onCreate(savedInstanceState); - - if (onIsMultiPane()) { - // So that Android doesn't put the fragment title (or nothing at - // all) in the action bar. - updateActionBarTitle(R.string.settings_title); - - if (Build.VERSION.SDK_INT < 13) { - // Affected by Bug 1015209 -- no detach/attach. - // If we try rejigging fragments, we'll crash, so don't - // enable locale switching at all. - localeSwitchingIsEnabled = false; - throw new IllegalStateException("foobar"); - } - } - - // Use setResourceToOpen to specify these extras. - Bundle intentExtras = getIntent().getExtras(); - - EventDispatcher.getInstance().registerGeckoThreadListener(this, - "Sanitize:Finished", - "Snackbar:Show"); - - // Add handling for long-press click. - // This is only for Android 3.0 and below (which use the long-press-context-menu paradigm). - final ListView mListView = getListView(); - mListView.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() { - @Override - public boolean onItemLongClick(AdapterView parent, View view, int position, long id) { - // Call long-click handler if it the item implements it. - final ListAdapter listAdapter = ((ListView) parent).getAdapter(); - final Object listItem = listAdapter.getItem(position); - - // Only CustomListPreference handles long clicks. - if (listItem instanceof CustomListPreference && listItem instanceof View.OnLongClickListener) { - final View.OnLongClickListener longClickListener = (View.OnLongClickListener) listItem; - return longClickListener.onLongClick(view); - } - return false; - } - }); - - // N.B., if we ever need to redisplay the locale selection UI without - // just finishing and recreating the activity, right here we'll need to - // capture EXTRA_SHOW_FRAGMENT_TITLE from the intent and store the title ID. - - // If launched from notification, explicitly cancel the notification. - if (intentExtras != null && intentExtras.containsKey(DataReportingNotification.ALERT_NAME_DATAREPORTING_NOTIFICATION)) { - Telemetry.sendUIEvent(TelemetryContract.Event.LAUNCH, Method.NOTIFICATION, "settings-data-choices"); - NotificationManager notificationManager = (NotificationManager) this.getSystemService(Context.NOTIFICATION_SERVICE); - notificationManager.cancel(DataReportingNotification.ALERT_NAME_DATAREPORTING_NOTIFICATION.hashCode()); - } - - // Launched from "Notifications settings" action button in a notification. - if (intentExtras != null && intentExtras.containsKey(CheckForUpdatesAction.EXTRA_CONTENT_NOTIFICATION)) { - Telemetry.startUISession(TelemetryContract.Session.EXPERIMENT, FeedService.getEnabledExperiment(this)); - Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, Method.BUTTON, "notification-settings"); - Telemetry.stopUISession(TelemetryContract.Session.EXPERIMENT, FeedService.getEnabledExperiment(this)); - } - } - - /** - * Set intent to display top-level settings fragment, - * and show the correct title. - */ - private void setupTopLevelFragmentIntent() { - Intent intent = getIntent(); - // Check intent to determine settings screen to display. - Bundle intentExtras = intent.getExtras(); - Bundle fragmentArgs = new Bundle(); - // Add resource argument to fragment if it exists. - if (intentExtras != null && intentExtras.containsKey(INTENT_EXTRA_RESOURCES)) { - String resourceName = intentExtras.getString(INTENT_EXTRA_RESOURCES); - fragmentArgs.putString(INTENT_EXTRA_RESOURCES, resourceName); - } else { - // Use top-level settings screen. - if (!onIsMultiPane()) { - fragmentArgs.putString(INTENT_EXTRA_RESOURCES, "preferences"); - } else { - fragmentArgs.putString(INTENT_EXTRA_RESOURCES, "preferences_general_tablet"); - } - } - - // Build fragment intent. - intent.putExtra(PreferenceActivity.EXTRA_SHOW_FRAGMENT, GeckoPreferenceFragment.class.getName()); - intent.putExtra(PreferenceActivity.EXTRA_SHOW_FRAGMENT_ARGUMENTS, fragmentArgs); - // Used to get fragment title when locale changes (see onLocaleChanged method above) - intent.putExtra(PreferenceActivity.EXTRA_SHOW_FRAGMENT_TITLE, R.string.settings_title); - } - - @Override - public boolean isValidFragment(String fragmentName) { - return GeckoPreferenceFragment.class.getName().equals(fragmentName); - } - - @TargetApi(11) - @Override - public void onBuildHeaders(List
target) { - if (onIsMultiPane()) { - loadHeadersFromResource(R.xml.preference_headers, target); - - Iterator
iterator = target.iterator(); - - while (iterator.hasNext()) { - Header header = iterator.next(); - - if (header.id == R.id.pref_header_advanced && !Restrictions.isAllowed(this, Restrictable.ADVANCED_SETTINGS)) { - iterator.remove(); - } else if (header.id == R.id.pref_header_clear_private_data - && !Restrictions.isAllowed(this, Restrictable.CLEAR_HISTORY)) { - iterator.remove(); - } - } - - mHeaders = target; - } - } - - @TargetApi(11) - public void switchToHeader(int id) { - if (mHeaders == null) { - // Can't switch to a header if there are no headers! - return; - } - - for (Header header : mHeaders) { - if (header.id == id) { - switchToHeader(header); - return; - } - } - } - - @Override - public void onWindowFocusChanged(boolean hasFocus) { - if (!hasFocus || mInitialized) - return; - - mInitialized = true; - } - - @Override - public void onBackPressed() { - super.onBackPressed(); - - if (NO_TRANSITIONS) { - overridePendingTransition(0, 0); - } - - Telemetry.sendUIEvent(TelemetryContract.Event.CANCEL, Method.BACK, "settings"); - } - - @Override - protected void onDestroy() { - super.onDestroy(); - - EventDispatcher.getInstance().unregisterGeckoThreadListener(this, - "Sanitize:Finished", - "Snackbar:Show"); - - if (mPrefsRequest != null) { - PrefsHelper.removeObserver(mPrefsRequest); - mPrefsRequest = null; - } - } - - @Override - public void onPause() { - // Symmetric with onResume. - if (isMultiPane()) { - SharedPreferences prefs = GeckoSharedPrefs.forApp(this); - prefs.unregisterOnSharedPreferenceChangeListener(this); - } - - super.onPause(); - - if (getApplication() instanceof GeckoApplication) { - ((GeckoApplication) getApplication()).onActivityPause(this); - } - } - - @Override - public void onResume() { - super.onResume(); - - if (getApplication() instanceof GeckoApplication) { - ((GeckoApplication) getApplication()).onActivityResume(this); - } - - // Watch prefs, otherwise we don't reliably get told when they change. - // See documentation for onSharedPreferenceChange for more. - // Inexplicably only needed on tablet. - if (isMultiPane()) { - SharedPreferences prefs = GeckoSharedPrefs.forApp(this); - prefs.registerOnSharedPreferenceChangeListener(this); - } - } - - @Override - public void startActivity(Intent intent) { - // For settings, we want to be able to pass results up the chain - // of preference screens so Settings can behave as a single unit. - // Specifically, when we open a link, we want to back out of all - // the settings screens. - // We need to start nested PreferenceScreens withStartActivityForResult(). - // Android doesn't let us do that (see Preference.onClick), so we're overriding here. - startActivityForResultChoosingTransition(intent, REQUEST_CODE_PREF_SCREEN); - } - - @Override - public void startWithFragment(String fragmentName, Bundle args, - Fragment resultTo, int resultRequestCode, int titleRes, int shortTitleRes) { - Log.v(LOGTAG, "Starting with fragment: " + fragmentName + ", title " + titleRes); - - // Overriding because we want to use startActivityForResult for Fragment intents. - Intent intent = onBuildStartFragmentIntent(fragmentName, args, titleRes, shortTitleRes); - if (resultTo == null) { - startActivityForResultChoosingTransition(intent, REQUEST_CODE_PREF_SCREEN); - } else { - resultTo.startActivityForResult(intent, resultRequestCode); - if (NO_TRANSITIONS) { - overridePendingTransition(0, 0); - } - } - } - - @Override - public void onActivityResult(int requestCode, int resultCode, Intent data) { - // We might have just returned from a settings activity that allows us - // to switch locales, so reflect any change that occurred. - checkLocale(); - - switch (requestCode) { - case REQUEST_CODE_PREF_SCREEN: - switch (resultCode) { - case RESULT_CODE_EXIT_SETTINGS: - updateActionBarTitle(R.string.settings_title); - - // Pass this result up to the parent activity. - setResult(RESULT_CODE_EXIT_SETTINGS); - finishChoosingTransition(); - break; - } - break; - case REQUEST_CODE_TAB_QUEUE: - if (TabQueueHelper.processTabQueuePromptResponse(resultCode, this)) { - tabQueuePreference.setChecked(true); - } - break; - } - } - - @Override - public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { - Permissions.onRequestPermissionsResult(this, permissions, grantResults); - } - - @Override - public void handleMessage(final String event, final NativeJSObject message, final EventCallback callback) { - try { - switch (event) { - case "Sanitize:Finished": - boolean success = message.getBoolean("success"); - final int stringRes = success ? R.string.private_data_success : R.string.private_data_fail; - - SnackbarBuilder.builder(GeckoPreferences.this) - .message(stringRes) - .duration(Snackbar.LENGTH_LONG) - .buildAndShow(); - break; - case "Snackbar:Show": - SnackbarBuilder.builder(this) - .fromEvent(message) - .callback(callback) - .buildAndShow(); - break; - } - } catch (Exception e) { - Log.e(LOGTAG, "Exception handling message \"" + event + "\":", e); - } - } - - /** - * Initialize all of the preferences (native of Gecko ones) for this screen. - * - * @param prefs The android.preference.PreferenceGroup to initialize - * @return The integer id for the PrefsHelper.PrefHandlerBase listener added - * to monitor changes to Gecko prefs. - */ - public PrefsHelper.PrefHandler setupPreferences(PreferenceGroup prefs) { - ArrayList list = new ArrayList(); - setupPreferences(prefs, list); - return getGeckoPreferences(prefs, list); - } - - /** - * Recursively loop through a PreferenceGroup. Initialize native Android prefs, - * and build a list of Gecko preferences in the passed in prefs array - * - * @param preferences The android.preference.PreferenceGroup to initialize - * @param prefs An ArrayList to fill with Gecko preferences that need to be - * initialized - * @return The integer id for the PrefsHelper.PrefHandlerBase listener added - * to monitor changes to Gecko prefs. - */ - private void setupPreferences(PreferenceGroup preferences, ArrayList prefs) { - for (int i = 0; i < preferences.getPreferenceCount(); i++) { - final Preference pref = preferences.getPreference(i); - - // Eliminate locale switching if necessary. - // This logic will need to be extended when - // content language selection (Bug 881510) is implemented. - if (!localeSwitchingIsEnabled && - "preferences_locale".equals(pref.getExtras().getString("resource"))) { - preferences.removePreference(pref); - i--; - continue; - } - - String key = pref.getKey(); - if (pref instanceof PreferenceGroup) { - // If datareporting is disabled, remove UI. - if (PREFS_DATA_REPORTING_PREFERENCES.equals(key)) { - if (!AppConstants.MOZ_DATA_REPORTING || !Restrictions.isAllowed(this, Restrictable.DATA_CHOICES)) { - preferences.removePreference(pref); - i--; - continue; - } - } else if (PREFS_SCREEN_ADVANCED.equals(key) && - !Restrictions.isAllowed(this, Restrictable.ADVANCED_SETTINGS)) { - preferences.removePreference(pref); - i--; - continue; - } else if (PREFS_CATEGORY_EXPERIMENTAL_FEATURES.equals(key) - && !AppConstants.MOZ_ANDROID_ACTIVITY_STREAM - && !AppConstants.MOZ_ANDROID_CUSTOM_TABS) { - preferences.removePreference(pref); - i--; - continue; - } - setupPreferences((PreferenceGroup) pref, prefs); - } else { - if (HANDLERS.containsKey(key)) { - PrefHandler handler = HANDLERS.get(key); - if (!handler.setupPref(this, pref)) { - preferences.removePreference(pref); - i--; - continue; - } - } - - pref.setOnPreferenceChangeListener(this); - if (PREFS_UPDATER_AUTODOWNLOAD.equals(key)) { - if (!AppConstants.MOZ_UPDATER || ContextUtils.isInstalledFromGooglePlay(this)) { - preferences.removePreference(pref); - i--; - continue; - } - } else if (PREFS_TRACKING_PROTECTION.equals(key)) { - // Remove UI for global TP pref in non-Nightly builds. - if (!AppConstants.NIGHTLY_BUILD) { - preferences.removePreference(pref); - i--; - continue; - } - } else if (PREFS_TRACKING_PROTECTION_PB.equals(key)) { - // Remove UI for private-browsing-only TP pref in Nightly builds. - if (AppConstants.NIGHTLY_BUILD) { - preferences.removePreference(pref); - i--; - continue; - } - } else if (PREFS_TELEMETRY_ENABLED.equals(key)) { - if (!AppConstants.MOZ_TELEMETRY_REPORTING || !Restrictions.isAllowed(this, Restrictable.DATA_CHOICES)) { - preferences.removePreference(pref); - i--; - continue; - } - } else if (PREFS_HEALTHREPORT_UPLOAD_ENABLED.equals(key) || - PREFS_HEALTHREPORT_LINK.equals(key)) { - if (!AppConstants.MOZ_SERVICES_HEALTHREPORT || !Restrictions.isAllowed(this, Restrictable.DATA_CHOICES)) { - preferences.removePreference(pref); - i--; - continue; - } - } else if (PREFS_GEO_REPORTING.equals(key) || - PREFS_GEO_LEARN_MORE.equals(key)) { - if (!AppConstants.MOZ_STUMBLER_BUILD_TIME_ENABLED || !Restrictions.isAllowed(this, Restrictable.DATA_CHOICES)) { - preferences.removePreference(pref); - i--; - continue; - } - } else if (PREFS_DEVTOOLS_REMOTE_USB_ENABLED.equals(key)) { - if (!Restrictions.isAllowed(this, Restrictable.REMOTE_DEBUGGING)) { - preferences.removePreference(pref); - i--; - continue; - } - } else if (PREFS_DEVTOOLS_REMOTE_WIFI_ENABLED.equals(key)) { - if (!Restrictions.isAllowed(this, Restrictable.REMOTE_DEBUGGING)) { - preferences.removePreference(pref); - i--; - continue; - } - if (!InputOptionsUtils.supportsQrCodeReader(getApplicationContext())) { - // WiFi debugging requires a QR code reader - pref.setEnabled(false); - pref.setSummary(getString(R.string.pref_developer_remotedebugging_wifi_disabled_summary)); - continue; - } - } else if (PREFS_DEVTOOLS_REMOTE_LINK.equals(key)) { - // Remove the "Learn more" link if remote debugging is disabled - if (!Restrictions.isAllowed(this, Restrictable.REMOTE_DEBUGGING)) { - preferences.removePreference(pref); - i--; - continue; - } - } else if (PREFS_RESTORE_SESSION.equals(key) || - PREFS_BROWSER_LOCALE.equals(key)) { - // Set the summary string to the current entry. The summary - // for other list prefs will be set in the PrefsHelper - // callback, but since this pref doesn't live in Gecko, we - // need to handle it separately. - ListPreference listPref = (ListPreference) pref; - CharSequence selectedEntry = listPref.getEntry(); - listPref.setSummary(selectedEntry); - continue; - } else if (PREFS_SYNC.equals(key)) { - // Don't show sync prefs while in guest mode. - if (!Restrictions.isAllowed(this, Restrictable.MODIFY_ACCOUNTS)) { - preferences.removePreference(pref); - i--; - continue; - } - } else if (PREFS_SEARCH_RESTORE_DEFAULTS.equals(key)) { - pref.setOnPreferenceClickListener(new OnPreferenceClickListener() { - @Override - public boolean onPreferenceClick(Preference preference) { - GeckoPreferences.this.restoreDefaultSearchEngines(); - Telemetry.sendUIEvent(TelemetryContract.Event.SEARCH_RESTORE_DEFAULTS, Method.LIST_ITEM); - return true; - } - }); - } else if (PREFS_TAB_QUEUE.equals(key)) { - tabQueuePreference = (SwitchPreference) pref; - // Only show tab queue pref on nightly builds with the tab queue build flag. - if (!TabQueueHelper.TAB_QUEUE_ENABLED) { - preferences.removePreference(pref); - i--; - continue; - } - } else if (PREFS_ZOOMED_VIEW_ENABLED.equals(key)) { - if (!AppConstants.NIGHTLY_BUILD) { - preferences.removePreference(pref); - i--; - continue; - } - } else if (PREFS_VOICE_INPUT_ENABLED.equals(key)) { - if (!InputOptionsUtils.supportsVoiceRecognizer(getApplicationContext(), getResources().getString(R.string.voicesearch_prompt))) { - // Remove UI for voice input on non nightly builds. - preferences.removePreference(pref); - i--; - continue; - } - } else if (PREFS_QRCODE_ENABLED.equals(key)) { - if (!InputOptionsUtils.supportsQrCodeReader(getApplicationContext())) { - // Remove UI for qr code input on non nightly builds - preferences.removePreference(pref); - i--; - continue; - } - } else if (PREFS_TRACKING_PROTECTION_PRIVATE_BROWSING.equals(key)) { - if (!Restrictions.isAllowed(this, Restrictable.PRIVATE_BROWSING)) { - preferences.removePreference(pref); - i--; - continue; - } - } else if (PREFS_TRACKING_PROTECTION_LEARN_MORE.equals(key)) { - if (!Restrictions.isAllowed(this, Restrictable.PRIVATE_BROWSING)) { - preferences.removePreference(pref); - i--; - continue; - } - } else if (PREFS_MP_ENABLED.equals(key)) { - if (!Restrictions.isAllowed(this, Restrictable.MASTER_PASSWORD)) { - preferences.removePreference(pref); - i--; - continue; - } - } else if (PREFS_CLEAR_PRIVATE_DATA.equals(key) || PREFS_CLEAR_PRIVATE_DATA_EXIT.equals(key)) { - if (!Restrictions.isAllowed(this, Restrictable.CLEAR_HISTORY)) { - preferences.removePreference(pref); - i--; - continue; - } - } else if (PREFS_HOMEPAGE.equals(key)) { - String setUrl = GeckoSharedPrefs.forProfile(getBaseContext()).getString(PREFS_HOMEPAGE, AboutPages.HOME); - setHomePageSummary(pref, setUrl); - pref.setOnPreferenceChangeListener(this); - } else if (PREFS_FAQ_LINK.equals(key)) { - // Format the FAQ link - final String VERSION = AppConstants.MOZ_APP_VERSION; - final String OS = AppConstants.OS_TARGET; - final String LOCALE = Locales.getLanguageTag(Locale.getDefault()); - - final String url = getResources().getString(R.string.faq_link, VERSION, OS, LOCALE); - ((LinkPreference) pref).setUrl(url); - } else if (PREFS_FEEDBACK_LINK.equals(key)) { - // Format the feedback link. We can't easily use this "app.feedbackURL" - // Gecko preference because the URL must be formatted. - final String url = getResources().getString(R.string.feedback_link, AppConstants.MOZ_APP_VERSION, AppConstants.MOZ_UPDATE_CHANNEL); - ((LinkPreference) pref).setUrl(url); - } else if (PREFS_DYNAMIC_TOOLBAR.equals(key)) { - if (DynamicToolbar.isForceDisabled()) { - preferences.removePreference(pref); - i--; - continue; - } - } else if (PREFS_NOTIFICATIONS_CONTENT.equals(key) || - PREFS_NOTIFICATIONS_CONTENT_LEARN_MORE.equals(key)) { - if (!FeedService.isInExperiment(this)) { - preferences.removePreference(pref); - i--; - continue; - } - } else if (PREFS_CUSTOM_TABS.equals(key) && !AppConstants.MOZ_ANDROID_CUSTOM_TABS) { - preferences.removePreference(pref); - i--; - continue; - } else if (PREFS_ACTIVITY_STREAM.equals(key) && !ActivityStream.isUserEligible(this)) { - preferences.removePreference(pref); - i--; - continue; - } - - // Some Preference UI elements are not actually preferences, - // but they require a key to work correctly. For example, - // "Clear private data" requires a key for its state to be - // saved when the orientation changes. It uses the - // "android.not_a_preference.privacy.clear" key - which doesn't - // exist in Gecko - to satisfy this requirement. - if (isGeckoPref(key)) { - prefs.add(key); - } - } - } - } - - private void setHomePageSummary(Preference pref, String value) { - if (!TextUtils.isEmpty(value)) { - pref.setSummary(value); - } else { - pref.setSummary(AboutPages.HOME); - } - } - - private boolean isGeckoPref(String key) { - if (TextUtils.isEmpty(key)) { - return false; - } - - if (key.startsWith(NON_PREF_PREFIX)) { - return false; - } - - if (key.equals(PREFS_BROWSER_LOCALE)) { - return false; - } - - return true; - } - - /** - * Restore default search engines in Gecko and retrigger a search engine refresh. - */ - protected void restoreDefaultSearchEngines() { - GeckoAppShell.notifyObservers("SearchEngines:RestoreDefaults", null); - - // Send message to Gecko to get engines. SearchPreferenceCategory listens for the response. - GeckoAppShell.notifyObservers("SearchEngines:GetVisible", null); - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - int itemId = item.getItemId(); - switch (itemId) { - case android.R.id.home: - finishChoosingTransition(); - return true; - } - - // Generated R.id.* apparently aren't constant expressions, so they can't be switched. - if (itemId == R.id.restore_defaults) { - restoreDefaultSearchEngines(); - Telemetry.sendUIEvent(TelemetryContract.Event.SEARCH_RESTORE_DEFAULTS, Method.MENU); - return true; - } - - return super.onOptionsItemSelected(item); - } - - final private int DIALOG_CREATE_MASTER_PASSWORD = 0; - final private int DIALOG_REMOVE_MASTER_PASSWORD = 1; - - public static void setCharEncodingState(boolean enabled) { - sIsCharEncodingEnabled = enabled; - } - - public static boolean getCharEncodingState() { - return sIsCharEncodingEnabled; - } - - public static void broadcastAction(final Context context, final Intent intent) { - fillIntentWithProfileInfo(context, intent); - LocalBroadcastManager.getInstance(context).sendBroadcast(intent); - } - - private static void fillIntentWithProfileInfo(final Context context, final Intent intent) { - // There is a race here, but GeckoProfile returns the default profile - // when Gecko is not explicitly running for a different profile. In a - // multi-profile world, this will need to be updated (possibly to - // broadcast settings for all profiles). See Bug 882182. - GeckoProfile profile = GeckoProfile.get(context); - if (profile != null) { - intent.putExtra("profileName", profile.getName()) - .putExtra("profilePath", profile.getDir().getAbsolutePath()); - } - } - - /** - * Broadcast the provided value as the value of the - * PREFS_GEO_REPORTING pref. - */ - public static void broadcastStumblerPref(final Context context, final boolean value) { - Intent intent = new Intent(ACTION_STUMBLER_UPLOAD_PREF) - .putExtra("pref", PREFS_GEO_REPORTING) - .putExtra("branch", GeckoSharedPrefs.APP_PREFS_NAME) - .putExtra("enabled", value) - .putExtra("moz_mozilla_api_key", AppConstants.MOZ_MOZILLA_API_KEY); - if (GeckoAppShell.getGeckoInterface() != null) { - intent.putExtra("user_agent", GeckoAppShell.getGeckoInterface().getDefaultUAString()); - } - broadcastAction(context, intent); - } - - /** - * Broadcast the current value of the - * PREFS_GEO_REPORTING pref. - */ - public static void broadcastStumblerPref(final Context context) { - final boolean value = getBooleanPref(context, PREFS_GEO_REPORTING, false); - broadcastStumblerPref(context, value); - } - - /** - * Return the value of the named preference in the default preferences file. - * - * This corresponds to the storage that backs preferences.xml. - * @param context a Context; the - * PreferenceActivity will suffice, but this - * method is intended to be called from other contexts - * within the application, not just this Activity. - * @param name the name of the preference to retrieve. - * @param def the default value to return if the preference is not present. - * @return the value of the preference, or the default. - */ - public static boolean getBooleanPref(final Context context, final String name, boolean def) { - final SharedPreferences prefs = GeckoSharedPrefs.forApp(context); - return prefs.getBoolean(name, def); - } - - /** - * Immediately handle the user's selection of a browser locale. - * - * Earlier locale-handling code did this with centralized logic in - * GeckoApp, delegating to LocaleManager for persistence and refreshing - * the activity as necessary. - * - * We no longer handle this by sending a message to GeckoApp, for - * several reasons: - * - * * GeckoApp might not be running. Activities don't always stick around. - * A Java bridge message might not be handled. - * * We need to adapt the preferences UI to the locale ourselves. - * * The user might not hit Back (or Up) -- they might hit Home and never - * come back. - * - * We handle the case of the user returning to the browser via the - * onActivityResult mechanism: see {@link BrowserApp#onActivityResult(int, int, Intent)}. - */ - private boolean onLocaleSelected(final String currentLocale, final String newValue) { - final Context context = getApplicationContext(); - - // LocaleManager operations need to occur on the background thread. - // ... but activity operations need to occur on the UI thread. So we - // have nested runnables. - ThreadUtils.postToBackgroundThread(new Runnable() { - @Override - public void run() { - final LocaleManager localeManager = BrowserLocaleManager.getInstance(); - - if (TextUtils.isEmpty(newValue)) { - BrowserLocaleManager.getInstance().resetToSystemLocale(context); - Telemetry.sendUIEvent(TelemetryContract.Event.LOCALE_BROWSER_RESET); - } else { - if (null == localeManager.setSelectedLocale(context, newValue)) { - localeManager.updateConfiguration(context, Locale.getDefault()); - } - Telemetry.sendUIEvent(TelemetryContract.Event.LOCALE_BROWSER_UNSELECTED, Method.NONE, - currentLocale == null ? "unknown" : currentLocale); - Telemetry.sendUIEvent(TelemetryContract.Event.LOCALE_BROWSER_SELECTED, Method.NONE, newValue); - } - - ThreadUtils.postToUiThread(new Runnable() { - @Override - public void run() { - onLocaleChanged(Locale.getDefault()); - } - }); - } - }); - - return true; - } - - private void refreshSuggestedSites() { - final ContentResolver cr = getApplicationContext().getContentResolver(); - - // This will force all active suggested sites cursors - // to request a refresh (e.g. cursor loaders). - cr.notifyChange(SuggestedSites.CONTENT_URI, null); - } - - @Override - public void onConfigurationChanged(Configuration newConfig) { - super.onConfigurationChanged(newConfig); - - Log.d(LOGTAG, "onConfigurationChanged: " + newConfig.locale); - - if (lastLocale.equals(newConfig.locale)) { - Log.d(LOGTAG, "Old locale same as new locale. Short-circuiting."); - return; - } - - final LocaleManager localeManager = BrowserLocaleManager.getInstance(); - final Locale changed = localeManager.onSystemConfigurationChanged(this, getResources(), newConfig, lastLocale); - if (changed != null) { - onLocaleChanged(changed); - } - } - - /** - * Implementation for the {@link OnSharedPreferenceChangeListener} interface, - * which we use to watch changes in our prefs file. - * - * This is reliably called whenever the pref changes, which is not the case - * for multiple consecutive changes in the case of onPreferenceChange. - * - * Note that this listener is not always registered: we use it only on - * tablets, Honeycomb and up, where we'll have a multi-pane view and prefs - * changing multiple times. - */ - @Override - public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { - if (PREFS_BROWSER_LOCALE.equals(key)) { - onLocaleSelected(Locales.getLanguageTag(lastLocale), - sharedPreferences.getString(key, null)); - } - } - - public interface PrefHandler { - // Allows the pref to do any initialization it needs. Return false to have the pref removed - // from the prefs screen entirely. - public boolean setupPref(Context context, Preference pref); - public void onChange(Context context, Preference pref, Object newValue); - } - - private void recordSettingChangeTelemetry(String prefName, Object newValue) { - final String value; - if (newValue instanceof Boolean) { - value = (Boolean) newValue ? "1" : "0"; - } else if (prefName.equals(PREFS_HOMEPAGE)) { - // Don't record the user's homepage preference. - value = "*"; - } else { - value = newValue.toString(); - } - - final JSONArray extras = new JSONArray(); - extras.put(prefName); - extras.put(value); - Telemetry.sendUIEvent(TelemetryContract.Event.EDIT, Method.SETTINGS, extras.toString()); - } - - @Override - public boolean onPreferenceChange(Preference preference, Object newValue) { - final String prefName = preference.getKey(); - Log.i(LOGTAG, "Changed " + prefName + " = " + newValue); - recordSettingChangeTelemetry(prefName, newValue); - - if (PREFS_MP_ENABLED.equals(prefName)) { - showDialog((Boolean) newValue ? DIALOG_CREATE_MASTER_PASSWORD : DIALOG_REMOVE_MASTER_PASSWORD); - - // We don't want the "use master password" pref to change until the - // user has gone through the dialog. - return false; - } - - if (PREFS_HOMEPAGE.equals(prefName)) { - setHomePageSummary(preference, String.valueOf(newValue)); - } - - if (PREFS_BROWSER_LOCALE.equals(prefName)) { - // Even though this is a list preference, we don't want to handle it - // below, so we return here. - return onLocaleSelected(Locales.getLanguageTag(lastLocale), (String) newValue); - } - - if (PREFS_MENU_CHAR_ENCODING.equals(prefName)) { - setCharEncodingState(((String) newValue).equals("true")); - } else if (PREFS_UPDATER_AUTODOWNLOAD.equals(prefName)) { - UpdateServiceHelper.setAutoDownloadPolicy(this, UpdateService.AutoDownloadPolicy.get((String) newValue)); - } else if (PREFS_UPDATER_URL.equals(prefName)) { - UpdateServiceHelper.setUpdateUrl(this, (String) newValue); - } else if (PREFS_HEALTHREPORT_UPLOAD_ENABLED.equals(prefName)) { - final Boolean newBooleanValue = (Boolean) newValue; - AdjustConstants.getAdjustHelper().setEnabled(newBooleanValue); - } else if (PREFS_GEO_REPORTING.equals(prefName)) { - if ((Boolean) newValue) { - enableStumbler((CheckBoxPreference) preference); - return false; - } else { - broadcastStumblerPref(GeckoPreferences.this, false); - return true; - } - } else if (PREFS_TAB_QUEUE.equals(prefName)) { - if ((Boolean) newValue && !TabQueueHelper.canDrawOverlays(this)) { - Intent promptIntent = new Intent(this, TabQueuePrompt.class); - startActivityForResult(promptIntent, REQUEST_CODE_TAB_QUEUE); - return false; - } - } else if (PREFS_NOTIFICATIONS_CONTENT.equals(prefName)) { - FeedService.setup(this); - } else if (PREFS_ACTIVITY_STREAM.equals(prefName)) { - ThreadUtils.postDelayedToUiThread(new Runnable() { - @Override - public void run() { - GeckoAppShell.scheduleRestart(); - } - }, 1000); - } else if (HANDLERS.containsKey(prefName)) { - PrefHandler handler = HANDLERS.get(prefName); - handler.onChange(this, preference, newValue); - } - - // Send Gecko-side pref changes to Gecko - if (isGeckoPref(prefName)) { - PrefsHelper.setPref(prefName, newValue, true /* flush */); - } - - if (preference instanceof ListPreference) { - // We need to find the entry for the new value - int newIndex = ((ListPreference) preference).findIndexOfValue((String) newValue); - CharSequence newEntry = ((ListPreference) preference).getEntries()[newIndex]; - ((ListPreference) preference).setSummary(newEntry); - } else if (preference instanceof LinkPreference) { - setResult(RESULT_CODE_EXIT_SETTINGS); - finishChoosingTransition(); - } else if (preference instanceof FontSizePreference) { - final FontSizePreference fontSizePref = (FontSizePreference) preference; - fontSizePref.setSummary(fontSizePref.getSavedFontSizeName()); - } - - return true; - } - - private void enableStumbler(final CheckBoxPreference preference) { - Permissions - .from(this) - .withPermissions(Manifest.permission.ACCESS_FINE_LOCATION) - .onUIThread() - .andFallback(new Runnable() { - @Override - public void run() { - preference.setChecked(false); - } - }) - .run(new Runnable() { - @Override - public void run() { - preference.setChecked(true); - broadcastStumblerPref(GeckoPreferences.this, true); - } - }); - } - - private TextInputLayout getTextBox(int aHintText) { - final EditText input = new EditText(this); - int inputtype = InputType.TYPE_CLASS_TEXT; - inputtype |= InputType.TYPE_TEXT_VARIATION_PASSWORD | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS; - input.setInputType(inputtype); - - input.setHint(aHintText); - - final TextInputLayout layout = new TextInputLayout(this); - layout.addView(input); - - return layout; - } - - private class PasswordTextWatcher implements TextWatcher { - EditText input1; - EditText input2; - AlertDialog dialog; - - PasswordTextWatcher(EditText aInput1, EditText aInput2, AlertDialog aDialog) { - input1 = aInput1; - input2 = aInput2; - dialog = aDialog; - } - - @Override - public void afterTextChanged(Editable s) { - if (dialog == null) - return; - - String text1 = input1.getText().toString(); - String text2 = input2.getText().toString(); - boolean disabled = TextUtils.isEmpty(text1) || TextUtils.isEmpty(text2) || !text1.equals(text2); - dialog.getButton(DialogInterface.BUTTON_POSITIVE).setEnabled(!disabled); - } - - @Override - public void beforeTextChanged(CharSequence s, int start, int count, int after) { } - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) { } - } - - private class EmptyTextWatcher implements TextWatcher { - EditText input; - AlertDialog dialog; - - EmptyTextWatcher(EditText aInput, AlertDialog aDialog) { - input = aInput; - dialog = aDialog; - } - - @Override - public void afterTextChanged(Editable s) { - if (dialog == null) - return; - - String text = input.getText().toString(); - boolean disabled = TextUtils.isEmpty(text); - dialog.getButton(DialogInterface.BUTTON_POSITIVE).setEnabled(!disabled); - } - - @Override - public void beforeTextChanged(CharSequence s, int start, int count, int after) { } - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) { } - } - - @Override - protected Dialog onCreateDialog(int id) { - AlertDialog.Builder builder = new AlertDialog.Builder(this); - LinearLayout linearLayout = new LinearLayout(this); - linearLayout.setOrientation(LinearLayout.VERTICAL); - AlertDialog dialog; - switch (id) { - case DIALOG_CREATE_MASTER_PASSWORD: - final TextInputLayout inputLayout1 = getTextBox(R.string.masterpassword_password); - final TextInputLayout inputLayout2 = getTextBox(R.string.masterpassword_confirm); - linearLayout.addView(inputLayout1); - linearLayout.addView(inputLayout2); - - final EditText input1 = inputLayout1.getEditText(); - final EditText input2 = inputLayout2.getEditText(); - - builder.setTitle(R.string.masterpassword_create_title) - .setView((View) linearLayout) - .setPositiveButton(R.string.button_ok, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - PrefsHelper.setPref(PREFS_MP_ENABLED, - input1.getText().toString(), - /* flush */ true); - } - }) - .setNegativeButton(R.string.button_cancel, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - return; - } - }); - dialog = builder.create(); - dialog.setOnShowListener(new DialogInterface.OnShowListener() { - @Override - public void onShow(DialogInterface dialog) { - input1.setText(""); - input2.setText(""); - input1.requestFocus(); - } - }); - - PasswordTextWatcher watcher = new PasswordTextWatcher(input1, input2, dialog); - input1.addTextChangedListener((TextWatcher) watcher); - input2.addTextChangedListener((TextWatcher) watcher); - - break; - case DIALOG_REMOVE_MASTER_PASSWORD: - final TextInputLayout inputLayout = getTextBox(R.string.masterpassword_password); - linearLayout.addView(inputLayout); - final EditText input = inputLayout.getEditText(); - - builder.setTitle(R.string.masterpassword_remove_title) - .setView((View) linearLayout) - .setPositiveButton(R.string.button_ok, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - PrefsHelper.setPref(PREFS_MP_ENABLED, input.getText().toString()); - } - }) - .setNegativeButton(R.string.button_cancel, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - return; - } - }); - dialog = builder.create(); - dialog.setOnDismissListener(new DialogInterface.OnDismissListener() { - @Override - public void onDismiss(DialogInterface dialog) { - input.setText(""); - } - }); - dialog.setOnShowListener(new DialogInterface.OnShowListener() { - @Override - public void onShow(DialogInterface dialog) { - input.setText(""); - } - }); - input.addTextChangedListener(new EmptyTextWatcher(input, dialog)); - break; - default: - return null; - } - - return dialog; - } - - // Initialize preferences by requesting the preference values from Gecko - private static class PrefCallbacks extends PrefsHelper.PrefHandlerBase { - private final PreferenceGroup screen; - - public PrefCallbacks(final PreferenceGroup screen) { - this.screen = screen; - } - - private Preference getField(String prefName) { - return screen.findPreference(prefName); - } - - @Override - public void prefValue(String prefName, final boolean value) { - final TwoStatePreference pref = (TwoStatePreference) getField(prefName); - ThreadUtils.postToUiThread(new Runnable() { - @Override - public void run() { - if (pref.isChecked() != value) { - pref.setChecked(value); - } - } - }); - } - - @Override - public void prefValue(String prefName, final String value) { - final Preference pref = getField(prefName); - if (pref instanceof EditTextPreference) { - ThreadUtils.postToUiThread(new Runnable() { - @Override - public void run() { - ((EditTextPreference) pref).setText(value); - } - }); - } else if (pref instanceof ListPreference) { - ThreadUtils.postToUiThread(new Runnable() { - @Override - public void run() { - ((ListPreference) pref).setValue(value); - // Set the summary string to the current entry - CharSequence selectedEntry = ((ListPreference) pref).getEntry(); - ((ListPreference) pref).setSummary(selectedEntry); - } - }); - } else if (pref instanceof FontSizePreference) { - final FontSizePreference fontSizePref = (FontSizePreference) pref; - fontSizePref.setSavedFontSize(value); - final String fontSizeName = fontSizePref.getSavedFontSizeName(); - ThreadUtils.postToUiThread(new Runnable() { - @Override - public void run() { - fontSizePref.setSummary(fontSizeName); // Ex: "Small". - } - }); - } - } - - @Override - public void prefValue(String prefName, final int value) { - final Preference pref = getField(prefName); - Log.w(LOGTAG, "Unhandled int value for pref [" + pref + "]"); - } - - @Override - public void finish() { - // enable all preferences once we have them from gecko - ThreadUtils.postToUiThread(new Runnable() { - @Override - public void run() { - screen.setEnabled(true); - } - }); - } - } - - private PrefsHelper.PrefHandler getGeckoPreferences(final PreferenceGroup screen, - ArrayList prefs) { - final PrefsHelper.PrefHandler prefHandler = new PrefCallbacks(screen); - final String[] prefNames = prefs.toArray(new String[prefs.size()]); - PrefsHelper.addObserver(prefNames, prefHandler); - return prefHandler; - } - - @Override - public boolean isGeckoActivityOpened() { - return false; - } - - /** - * Given an Intent instance, add extras to specify which settings section to - * open. - * - * resource should be a valid Android XML resource identifier. - * - * The mechanism to open a section differs based on Android version. - */ - public static void setResourceToOpen(final Intent intent, final String resource) { - if (intent == null) { - throw new IllegalArgumentException("intent must not be null"); - } - if (resource == null) { - return; - } - - intent.putExtra(PreferenceActivity.EXTRA_SHOW_FRAGMENT, GeckoPreferenceFragment.class.getName()); - - Bundle fragmentArgs = new Bundle(); - fragmentArgs.putString("resource", resource); - intent.putExtra(PreferenceActivity.EXTRA_SHOW_FRAGMENT_ARGUMENTS, fragmentArgs); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/preferences/LinkPreference.java b/mobile/android/base/java/org/mozilla/gecko/preferences/LinkPreference.java deleted file mode 100644 index 774f78c536..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/preferences/LinkPreference.java +++ /dev/null @@ -1,35 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.preferences; - -import org.mozilla.gecko.Tabs; - -import android.content.Context; -import android.preference.Preference; -import android.util.AttributeSet; - -class LinkPreference extends Preference { - private String mUrl; - - public LinkPreference(Context context, AttributeSet attrs) { - super(context, attrs); - mUrl = attrs.getAttributeValue(null, "url"); - } - public LinkPreference(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - mUrl = attrs.getAttributeValue(null, "url"); - } - - public void setUrl(String url) { - mUrl = url; - } - - @Override - protected void onClick() { - Tabs.getInstance().loadUrlInTab(mUrl); - callChangeListener(mUrl); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/preferences/ListCheckboxPreference.java b/mobile/android/base/java/org/mozilla/gecko/preferences/ListCheckboxPreference.java deleted file mode 100644 index f56ea58b9d..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/preferences/ListCheckboxPreference.java +++ /dev/null @@ -1,58 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.preferences; - -import android.content.Context; -import android.util.AttributeSet; -import android.view.View; -import android.widget.Checkable; - -import org.mozilla.gecko.R; - -/** - * This preference shows a checkbox on its left hand side, but will show a menu when clicked. - * Its used for preferences like "Clear on Exit" that have a boolean on-off state, but that represent - * multiple boolean options inside. - **/ -class ListCheckboxPreference extends MultiChoicePreference implements Checkable { - private static final String LOGTAG = "GeckoListCheckboxPreference"; - private boolean checked; - - public ListCheckboxPreference(Context context, AttributeSet attrs) { - super(context, attrs); - setWidgetLayoutResource(R.layout.preference_checkbox); - } - - @Override - public boolean isChecked() { - return checked; - } - - @Override - protected void onBindView(View view) { - super.onBindView(view); - - View checkboxView = view.findViewById(R.id.checkbox); - if (checkboxView instanceof Checkable) { - ((Checkable) checkboxView).setChecked(checked); - } - } - - @Override - public void setChecked(boolean checked) { - boolean changed = checked != this.checked; - this.checked = checked; - if (changed) { - notifyDependencyChange(shouldDisableDependents()); - notifyChanged(); - } - } - - @Override - public void toggle() { - checked = !checked; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/preferences/LocaleListPreference.java b/mobile/android/base/java/org/mozilla/gecko/preferences/LocaleListPreference.java deleted file mode 100644 index c962a3d197..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/preferences/LocaleListPreference.java +++ /dev/null @@ -1,316 +0,0 @@ -/* 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.preferences; - -import java.nio.ByteBuffer; -import java.text.Collator; -import java.util.Arrays; -import java.util.Collection; -import java.util.HashSet; -import java.util.Locale; -import java.util.Set; - -import org.mozilla.gecko.AppConstants.Versions; -import org.mozilla.gecko.BrowserLocaleManager; -import org.mozilla.gecko.Locales; -import org.mozilla.gecko.R; - -import android.content.Context; -import android.graphics.Bitmap; -import android.graphics.Canvas; -import android.graphics.Paint; -import android.preference.ListPreference; -import android.text.TextUtils; -import android.util.AttributeSet; -import android.util.Log; - -public class LocaleListPreference extends ListPreference { - private static final String LOG_TAG = "GeckoLocaleList"; - - /** - * With thanks to for the - * initial solution. - * - * This class encapsulates an approach to checking whether a script - * is usable on a device. We attempt to draw a character from the - * script (e.g., ব). If the fonts on the device don't have the correct - * glyph, Android typically renders whitespace (rather than .notdef). - * - * Pass in part of the name of the locale in its local representation, - * and a whitespace character; this class performs the graphical comparison. - * - * See Bug 1023451 Comment 24 for extensive explanation. - */ - private static class CharacterValidator { - private static final int BITMAP_WIDTH = 32; - private static final int BITMAP_HEIGHT = 48; - - private final Paint paint = new Paint(); - private final byte[] missingCharacter; - - public CharacterValidator(String missing) { - this.missingCharacter = getPixels(drawBitmap(missing)); - } - - private Bitmap drawBitmap(String text) { - Bitmap b = Bitmap.createBitmap(BITMAP_WIDTH, BITMAP_HEIGHT, Bitmap.Config.ALPHA_8); - Canvas c = new Canvas(b); - c.drawText(text, 0, BITMAP_HEIGHT / 2, this.paint); - return b; - } - - private static byte[] getPixels(final Bitmap b) { - final int byteCount; - if (Versions.feature19Plus) { - byteCount = b.getAllocationByteCount(); - } else { - // Close enough for government work. - // Equivalent to getByteCount, but works on <12. - byteCount = b.getRowBytes() * b.getHeight(); - } - - final ByteBuffer buffer = ByteBuffer.allocate(byteCount); - try { - b.copyPixelsToBuffer(buffer); - } catch (RuntimeException e) { - // Android throws this if there's not enough space in the buffer. - // This should never occur, but if it does, we don't - // really care -- we probably don't need the entire image. - // This is awful. I apologize. - if ("Buffer not large enough for pixels".equals(e.getMessage())) { - return buffer.array(); - } - throw e; - } - - return buffer.array(); - } - - public boolean characterIsMissingInFont(String ch) { - byte[] rendered = getPixels(drawBitmap(ch)); - return Arrays.equals(rendered, missingCharacter); - } - } - - private volatile Locale entriesLocale; - private final CharacterValidator characterValidator; - - public LocaleListPreference(Context context) { - this(context, null); - } - - public LocaleListPreference(Context context, AttributeSet attributes) { - super(context, attributes); - - // Thus far, missing glyphs are replaced by whitespace, not a box - // or other Unicode codepoint. - this.characterValidator = new CharacterValidator(" "); - buildList(); - } - - private static final class LocaleDescriptor implements Comparable { - // We use Locale.US here to ensure a stable ordering of entries. - private static final Collator COLLATOR = Collator.getInstance(Locale.US); - - public final String tag; - private final String nativeName; - - public LocaleDescriptor(String tag) { - this(Locales.parseLocaleCode(tag), tag); - } - - public LocaleDescriptor(Locale locale, String tag) { - this.tag = tag; - - final String displayName = locale.getDisplayName(locale); - if (TextUtils.isEmpty(displayName)) { - // There's nothing sane we can do. - Log.w(LOG_TAG, "Display name is empty. Using " + locale.toString()); - this.nativeName = locale.toString(); - return; - } - - // For now, uppercase the first character of LTR locale names. - // This is pretty much what Android does. This is a reasonable hack - // for Bug 1014602, but it won't generalize to all locales. - final byte directionality = Character.getDirectionality(displayName.charAt(0)); - if (directionality == Character.DIRECTIONALITY_LEFT_TO_RIGHT) { - this.nativeName = displayName.substring(0, 1).toUpperCase(locale) + - displayName.substring(1); - return; - } - - this.nativeName = displayName; - } - - public String getTag() { - return this.tag; - } - - public String getDisplayName() { - return this.nativeName; - } - - @Override - public String toString() { - return this.nativeName; - } - - - @Override - public int compareTo(LocaleDescriptor another) { - // We sort by name, so we use Collator. - return COLLATOR.compare(this.nativeName, another.nativeName); - } - - /** - * See Bug 1023451 Comment 10 for the research that led to - * this method. - * - * @return true if this locale can be used for displaying UI - * on this device without known issues. - */ - public boolean isUsable(CharacterValidator validator) { - if (Versions.preLollipop && this.tag.matches("[a-zA-Z]{3}.*")) { - // Earlier versions of Android can't load three-char locale code - // resources. - return false; - } - - // Oh, for Java 7 switch statements. - if (this.tag.equals("bn-IN")) { - // Bengali sometimes has an English label if the Bengali script - // is missing. This prevents us from simply checking character - // rendering for bn-IN; we'll get a false positive for "B", not "ব". - // - // This doesn't seem to affect other Bengali-script locales - // (below), which always have a label in native script. - if (!this.nativeName.startsWith("বাংলা")) { - // We're on an Android version that doesn't even have - // characters to say বাংলা. Definite failure. - return false; - } - } - - // These locales use a script that is often unavailable - // on common Android devices. Make sure we can show them. - // See documentation for CharacterValidator. - // Note that bn-IN is checked here even if it passed above. - if (this.tag.equals("or") || - this.tag.equals("my") || - this.tag.equals("pa-IN") || - this.tag.equals("gu-IN") || - this.tag.equals("bn-IN")) { - if (validator.characterIsMissingInFont(this.nativeName.substring(0, 1))) { - return false; - } - } - - return true; - } - } - - /** - * Not every locale we ship can be used on every device, due to - * font or rendering constraints. - * - * This method filters down the list before generating the descriptor array. - */ - private LocaleDescriptor[] getUsableLocales() { - Collection shippingLocales = BrowserLocaleManager.getPackagedLocaleTags(getContext()); - - // Future: single-locale builds should be specified, too. - if (shippingLocales == null) { - final String fallbackTag = BrowserLocaleManager.getInstance().getFallbackLocaleTag(); - return new LocaleDescriptor[] { new LocaleDescriptor(fallbackTag) }; - } - - final int initialCount = shippingLocales.size(); - final Set locales = new HashSet(initialCount); - for (String tag : shippingLocales) { - final LocaleDescriptor descriptor = new LocaleDescriptor(tag); - - if (!descriptor.isUsable(this.characterValidator)) { - Log.w(LOG_TAG, "Skipping locale " + tag + " on this device."); - continue; - } - - locales.add(descriptor); - } - - final int usableCount = locales.size(); - final LocaleDescriptor[] descriptors = locales.toArray(new LocaleDescriptor[usableCount]); - Arrays.sort(descriptors, 0, usableCount); - return descriptors; - } - - @Override - protected void onDialogClosed(boolean positiveResult) { - // The superclass will take care of persistence. - super.onDialogClosed(positiveResult); - - // Use this hook to try to fix up the environment ASAP. - // Do this so that the redisplayed fragment is inflated - // with the right locale. - final Locale selectedLocale = getSelectedLocale(); - final Context context = getContext(); - BrowserLocaleManager.getInstance().updateConfiguration(context, selectedLocale); - } - - private Locale getSelectedLocale() { - final String tag = getValue(); - if (tag == null || tag.equals("")) { - return Locale.getDefault(); - } - return Locales.parseLocaleCode(tag); - } - - @Override - public CharSequence getSummary() { - final String value = getValue(); - - if (TextUtils.isEmpty(value)) { - return getContext().getString(R.string.locale_system_default); - } - - // We can't trust super.getSummary() across locale changes, - // apparently, so let's do the same work. - return new LocaleDescriptor(value).getDisplayName(); - } - - private void buildList() { - final Locale currentLocale = Locale.getDefault(); - Log.d(LOG_TAG, "Building locales list. Current locale: " + currentLocale); - - if (currentLocale.equals(this.entriesLocale) && - getEntries() != null) { - Log.v(LOG_TAG, "No need to build list."); - return; - } - - final LocaleDescriptor[] descriptors = getUsableLocales(); - final int count = descriptors.length; - - this.entriesLocale = currentLocale; - - // We leave room for "System default". - final String[] entries = new String[count + 1]; - final String[] values = new String[count + 1]; - - entries[0] = getContext().getString(R.string.locale_system_default); - values[0] = ""; - - for (int i = 0; i < count; ++i) { - final String displayName = descriptors[i].getDisplayName(); - final String tag = descriptors[i].getTag(); - Log.v(LOG_TAG, displayName + " => " + tag); - entries[i + 1] = displayName; - values[i + 1] = tag; - } - - setEntries(entries); - setEntryValues(values); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/preferences/ModifiableHintPreference.java b/mobile/android/base/java/org/mozilla/gecko/preferences/ModifiableHintPreference.java deleted file mode 100644 index c545472e24..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/preferences/ModifiableHintPreference.java +++ /dev/null @@ -1,67 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.preferences; - -import org.mozilla.gecko.R; - -import android.content.Context; -import android.graphics.drawable.Drawable; -import android.preference.Preference; -import android.text.Spanned; -import android.text.SpannableStringBuilder; -import android.text.style.ImageSpan; -import android.util.AttributeSet; -import android.view.View; -import android.view.ViewGroup; -import android.widget.TextView; - -class ModifiableHintPreference extends Preference { - private static final String LOGTAG = "ModifiableHintPref"; - private final Context mContext; - - private final String MATCH_STRING = "%I"; - private final int RESID_TEXT_VIEW = R.id.label_search_hint; - private final int RESID_DRAWABLE = R.drawable.ab_add_search_engine; - private final double SCALE_FACTOR = 0.5; - - public ModifiableHintPreference(Context context, AttributeSet attrs) { - super(context, attrs); - mContext = context; - } - - public ModifiableHintPreference(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - mContext = context; - } - - @Override - protected View onCreateView(ViewGroup parent) { - View thisView = super.onCreateView(parent); - configurePreferenceView(thisView); - return thisView; - } - - private void configurePreferenceView(View view) { - TextView textView = (TextView) view.findViewById(RESID_TEXT_VIEW); - String searchHint = textView.getText().toString(); - - // Use an ImageSpan to include the "add search" icon in the Tip. - int imageSpanIndex = searchHint.indexOf(MATCH_STRING); - if (imageSpanIndex != -1) { - // Scale the resource. - Drawable drawable = mContext.getResources().getDrawable(RESID_DRAWABLE); - drawable.setBounds(0, 0, (int) (drawable.getIntrinsicWidth() * SCALE_FACTOR), - (int) (drawable.getIntrinsicHeight() * SCALE_FACTOR)); - - ImageSpan searchIcon = new ImageSpan(drawable); - final SpannableStringBuilder hintBuilder = new SpannableStringBuilder(searchHint); - - // Insert the image. - hintBuilder.setSpan(searchIcon, imageSpanIndex, imageSpanIndex + MATCH_STRING.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE); - textView.setText(hintBuilder, TextView.BufferType.SPANNABLE); - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/preferences/MultiChoicePreference.java b/mobile/android/base/java/org/mozilla/gecko/preferences/MultiChoicePreference.java deleted file mode 100644 index 5749bf29df..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/preferences/MultiChoicePreference.java +++ /dev/null @@ -1,271 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.preferences; - -import org.mozilla.gecko.R; -import org.mozilla.gecko.GeckoSharedPrefs; -import org.mozilla.gecko.util.PrefUtils; -import org.mozilla.gecko.util.ThreadUtils; - -import android.app.AlertDialog.Builder; -import android.content.Context; -import android.content.DialogInterface; -import android.content.res.TypedArray; -import android.content.SharedPreferences; -import android.preference.DialogPreference; -import android.util.AttributeSet; - -import java.util.HashSet; -import java.util.Set; - -class MultiChoicePreference extends DialogPreference implements DialogInterface.OnMultiChoiceClickListener { - private static final String LOGTAG = "GeckoMultiChoicePreference"; - - private boolean mValues[]; - private boolean mPrevValues[]; - private CharSequence mEntryValues[]; - private CharSequence mEntries[]; - private CharSequence mInitialValues[]; - - public MultiChoicePreference(Context context, AttributeSet attrs) { - super(context, attrs); - - TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.MultiChoicePreference); - mEntries = a.getTextArray(R.styleable.MultiChoicePreference_entries); - mEntryValues = a.getTextArray(R.styleable.MultiChoicePreference_entryValues); - mInitialValues = a.getTextArray(R.styleable.MultiChoicePreference_initialValues); - a.recycle(); - - loadPersistedValues(); - } - - public MultiChoicePreference(Context context) { - this(context, null); - } - - /** - * Sets the human-readable entries to be shown in the list. This will be - * shown in subsequent dialogs. - *

- * Each entry must have a corresponding index in - * {@link #setEntryValues(CharSequence[])} and - * {@link #setInitialValues(CharSequence[])}. - * - * @param entries The entries. - */ - public void setEntries(CharSequence[] entries) { - mEntries = entries.clone(); - } - - /** - * @param entriesResId The entries array as a resource. - */ - public void setEntries(int entriesResId) { - setEntries(getContext().getResources().getTextArray(entriesResId)); - } - - /** - * Sets the preference values for preferences shown in the list. - * - * @param entryValues The entry values. - */ - public void setEntryValues(CharSequence[] entryValues) { - mEntryValues = entryValues.clone(); - loadPersistedValues(); - } - - /** - * Entry values define a separate pref for each row in the dialog. - * - * @param entryValuesResId The entryValues array as a resource. - */ - public void setEntryValues(int entryValuesResId) { - setEntryValues(getContext().getResources().getTextArray(entryValuesResId)); - } - - /** - * The array of initial entry values in this list. Each entryValue - * corresponds to an entryKey. These values are used if a) the preference - * isn't persisted, or b) the preference is persisted but hasn't yet been - * set. - * - * @param initialValues The entry values - */ - public void setInitialValues(CharSequence[] initialValues) { - mInitialValues = initialValues.clone(); - loadPersistedValues(); - } - - /** - * @param initialValuesResId The initialValues array as a resource. - */ - public void setInitialValues(int initialValuesResId) { - setInitialValues(getContext().getResources().getTextArray(initialValuesResId)); - } - - /** - * The list of translated strings corresponding to each preference. - * - * @return The array of entries. - */ - public CharSequence[] getEntries() { - return mEntries.clone(); - } - - /** - * The list of values corresponding to each preference. - * - * @return The array of values. - */ - public CharSequence[] getEntryValues() { - return mEntryValues.clone(); - } - - /** - * The list of initial values for each preference. Each string in this list - * should be either "true" or "false". - * - * @return The array of initial values. - */ - public CharSequence[] getInitialValues() { - return mInitialValues.clone(); - } - - public void setValue(final int i, final boolean value) { - mValues[i] = value; - mPrevValues = mValues.clone(); - } - - /** - * The list of values for each preference. These values are updated after - * the dialog has been displayed. - * - * @return The array of values. - */ - public Set getValues() { - final Set values = new HashSet(); - - if (mValues == null) { - return values; - } - - for (int i = 0; i < mValues.length; i++) { - if (mValues[i]) { - values.add(mEntryValues[i].toString()); - } - } - - return values; - } - - @Override - public void onClick(DialogInterface dialog, int which, boolean val) { - } - - @Override - protected void onPrepareDialogBuilder(Builder builder) { - if (mEntries == null || mInitialValues == null || mEntryValues == null) { - throw new IllegalStateException( - "MultiChoicePreference requires entries, entryValues, and initialValues arrays."); - } - - if (mEntries.length != mEntryValues.length || mEntries.length != mInitialValues.length) { - throw new IllegalStateException( - "MultiChoicePreference entries, entryValues, and initialValues arrays must be the same length"); - } - - builder.setMultiChoiceItems(mEntries, mValues, this); - } - - @Override - protected void onDialogClosed(boolean positiveResult) { - if (mPrevValues == null || mInitialValues == null) { - // Initialization is done asynchronously, so these values may not - // have been set before the dialog was closed. - return; - } - - if (!positiveResult) { - // user cancelled; reset checkbox values to their previous state - mValues = mPrevValues.clone(); - return; - } - - mPrevValues = mValues.clone(); - - if (!callChangeListener(getValues())) { - return; - } - - persist(); - } - - /* Persists the current data stored by this pref to SharedPreferences. */ - public boolean persist() { - if (isPersistent()) { - final SharedPreferences.Editor edit = GeckoSharedPrefs.forProfile(getContext()).edit(); - final boolean res = persist(edit); - edit.apply(); - return res; - } - - return false; - } - - /* Internal persist method. Take an edit so that multiple prefs can be persisted in a single commit. */ - protected boolean persist(SharedPreferences.Editor edit) { - if (isPersistent()) { - Set vals = getValues(); - PrefUtils.putStringSet(edit, getKey(), vals).apply();; - return true; - } - - return false; - } - - /* Returns a list of EntryValues that are currently enabled. */ - public Set getPersistedStrings(Set defaultVal) { - if (!isPersistent()) { - return defaultVal; - } - - final SharedPreferences prefs = GeckoSharedPrefs.forProfile(getContext()); - return PrefUtils.getStringSet(prefs, getKey(), defaultVal); - } - - /** - * Loads persistent prefs from shared preferences. If the preferences - * aren't persistent or haven't yet been stored, they will be set to their - * initial values. - */ - protected void loadPersistedValues() { - final int entryCount = mInitialValues.length; - mValues = new boolean[entryCount]; - - if (entryCount != mEntries.length || entryCount != mEntryValues.length) { - throw new IllegalStateException( - "MultiChoicePreference entryValues and initialValues arrays must be the same length"); - } - - ThreadUtils.postToBackgroundThread(new Runnable() { - @Override - public void run() { - final Set stringVals = getPersistedStrings(null); - - for (int i = 0; i < entryCount; i++) { - if (stringVals != null) { - mValues[i] = stringVals.contains(mEntryValues[i]); - } else { - final boolean defaultVal = mInitialValues[i].equals("true"); - mValues[i] = defaultVal; - } - } - - mPrevValues = mValues.clone(); - } - }); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/preferences/MultiPrefMultiChoicePreference.java b/mobile/android/base/java/org/mozilla/gecko/preferences/MultiPrefMultiChoicePreference.java deleted file mode 100644 index 580d613cab..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/preferences/MultiPrefMultiChoicePreference.java +++ /dev/null @@ -1,116 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.preferences; - -import org.mozilla.gecko.R; -import org.mozilla.gecko.GeckoSharedPrefs; -import org.mozilla.gecko.util.ThreadUtils; - -import android.app.AlertDialog; -import android.content.Context; -import android.content.DialogInterface; -import android.content.res.TypedArray; -import android.content.SharedPreferences; -import android.widget.Button; -import android.util.AttributeSet; -import android.util.Log; - -import java.util.Set; - -/* Provides backwards compatibility for some old multi-choice pref types used by Gecko. - * This will import the old data from the old prefs the first time it is run. - */ -class MultiPrefMultiChoicePreference extends MultiChoicePreference { - private static final String LOGTAG = "GeckoMultiPrefPreference"; - private static final String IMPORT_SUFFIX = "_imported_"; - private final CharSequence[] keys; - - public MultiPrefMultiChoicePreference(Context context, AttributeSet attrs) { - super(context, attrs); - - final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.MultiPrefMultiChoicePreference); - keys = a.getTextArray(R.styleable.MultiPrefMultiChoicePreference_entryKeys); - a.recycle(); - - loadPersistedValues(); - } - - // Helper method for reading a boolean pref. - private boolean getPersistedBoolean(SharedPreferences prefs, String key, boolean defaultReturnValue) { - if (!isPersistent()) { - return defaultReturnValue; - } - - return prefs.getBoolean(key, defaultReturnValue); - } - - // Overridden to do a one time import for the old preference type to the new one. - @Override - protected synchronized void loadPersistedValues() { - // This will load the new pref if it exists. - super.loadPersistedValues(); - - // First check if we've already done the import the old data. If so, nothing to load. - final SharedPreferences prefs = GeckoSharedPrefs.forApp(getContext()); - final boolean imported = getPersistedBoolean(prefs, getKey() + IMPORT_SUFFIX, false); - if (imported) { - return; - } - - // Load the data we'll need to find the old style prefs - final CharSequence[] init = getInitialValues(); - final CharSequence[] entries = getEntries(); - if (keys == null || init == null) { - return; - } - - final int entryCount = keys.length; - if (entryCount != entries.length || entryCount != init.length) { - throw new IllegalStateException("MultiChoicePreference entryKeys and initialValues arrays must be the same length"); - } - - // Now iterate through the entries on a background thread. - final SharedPreferences.Editor edit = prefs.edit(); - ThreadUtils.postToBackgroundThread(new Runnable() { - @Override - public void run() { - try { - // Use one editor to batch as many changes as we can. - for (int i = 0; i < entryCount; i++) { - String key = keys[i].toString(); - boolean initialValue = "true".equals(init[i]); - boolean val = getPersistedBoolean(prefs, key, initialValue); - - // Save the pref and remove the old preference. - setValue(i, val); - edit.remove(key); - } - - persist(edit); - edit.putBoolean(getKey() + IMPORT_SUFFIX, true); - edit.apply(); - } catch (Exception ex) { - Log.i(LOGTAG, "Err", ex); - } - } - }); - } - - - @Override - public void onClick(DialogInterface dialog, int which, boolean val) { - // enable positive button only if at least one item is checked - boolean enabled = false; - final Set values = getValues(); - - enabled = (values.size() > 0); - final Button button = ((AlertDialog) dialog).getButton(DialogInterface.BUTTON_POSITIVE); - if (button.isEnabled() != enabled) { - button.setEnabled(enabled); - } - } - -} diff --git a/mobile/android/base/java/org/mozilla/gecko/preferences/PanelsPreference.java b/mobile/android/base/java/org/mozilla/gecko/preferences/PanelsPreference.java deleted file mode 100644 index 337d9dd2fa..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/preferences/PanelsPreference.java +++ /dev/null @@ -1,255 +0,0 @@ -/* 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.preferences; - -import org.mozilla.gecko.animation.PropertyAnimator; -import org.mozilla.gecko.animation.PropertyAnimator.Property; -import org.mozilla.gecko.animation.ViewHelper; -import org.mozilla.gecko.R; - -import android.app.AlertDialog; -import android.app.Dialog; -import android.content.Context; -import android.content.DialogInterface; -import android.content.DialogInterface.OnClickListener; -import android.content.DialogInterface.OnShowListener; -import android.content.res.Resources; -import android.util.Log; -import android.view.View; -import android.view.ViewGroup; -import android.widget.TextView; - -public class PanelsPreference extends CustomListPreference { - protected String LOGTAG = "PanelsPreference"; - - // Position state of this Preference in enclosing category. - private static final int STATE_IS_FIRST = 0; - private static final int STATE_IS_LAST = 1; - - /** - * Index of the context menu button for controlling display options. - * For (removable) Dynamic panels, this button removes the panel. - * For built-in panels, this button toggles showing or hiding the panel. - */ - private static final int INDEX_DISPLAY_BUTTON = 1; - private static final int INDEX_REORDER_BUTTON = 2; - - // Indices of buttons in context menu for reordering. - private static final int INDEX_MOVE_UP_BUTTON = 0; - private static final int INDEX_MOVE_DOWN_BUTTON = 1; - - private String LABEL_HIDE; - private String LABEL_SHOW; - - private View preferenceView; - protected boolean mIsHidden; - private final boolean mIsRemovable; - - private boolean mAnimate; - private static final int ANIMATION_DURATION_MS = 400; - - // State for reordering. - private int mPositionState = -1; - private final int mIndex; - - public PanelsPreference(Context context, CustomListCategory parentCategory, boolean isRemovable, int index, boolean animate) { - super(context, parentCategory); - mIsRemovable = isRemovable; - mIndex = index; - mAnimate = animate; - } - - @Override - protected int getPreferenceLayoutResource() { - return R.layout.preference_panels; - } - - @Override - protected void onBindView(View view) { - super.onBindView(view); - - // Override view handling so we can grey out "hidden" PanelPreferences. - view.setEnabled(!mIsHidden); - - if (view instanceof ViewGroup) { - final ViewGroup group = (ViewGroup) view; - for (int i = 0; i < group.getChildCount(); i++) { - group.getChildAt(i).setEnabled(!mIsHidden); - } - preferenceView = group; - } - - if (mAnimate) { - ViewHelper.setAlpha(preferenceView, 0); - - final PropertyAnimator animator = new PropertyAnimator(ANIMATION_DURATION_MS); - animator.attach(preferenceView, Property.ALPHA, 1); - animator.start(); - - // Clear animate flag. - mAnimate = false; - } - } - - @Override - protected String[] createDialogItems() { - final Resources res = getContext().getResources(); - final String labelReorder = res.getString(R.string.pref_panels_reorder); - - if (mIsRemovable) { - return new String[] { LABEL_SET_AS_DEFAULT, LABEL_REMOVE, labelReorder }; - } - - // Built-in panels can't be removed, so use show/hide options. - LABEL_HIDE = res.getString(R.string.pref_panels_hide); - LABEL_SHOW = res.getString(R.string.pref_panels_show); - - return new String[] { LABEL_SET_AS_DEFAULT, LABEL_HIDE, labelReorder }; - } - - @Override - public void setIsDefault(boolean isDefault) { - mIsDefault = isDefault; - if (isDefault) { - setSummary(LABEL_IS_DEFAULT); - if (mIsHidden) { - // Unhide the panel if it's being set as the default. - setHidden(false); - } - } else { - setSummary(""); - } - } - - @Override - protected void onDialogIndexClicked(int index) { - switch (index) { - case INDEX_SET_DEFAULT_BUTTON: - mParentCategory.setDefault(this); - break; - - case INDEX_DISPLAY_BUTTON: - // Handle display options for the panel. - if (mIsRemovable) { - // For removable panels, the button displays text for removing the panel. - mParentCategory.uninstall(this); - } else { - // Otherwise, the button toggles between text for showing or hiding the panel. - ((PanelsPreferenceCategory) mParentCategory).setHidden(this, !mIsHidden); - } - break; - - case INDEX_REORDER_BUTTON: - // Display dialog for changing preference order. - final Dialog orderDialog = makeReorderDialog(); - orderDialog.show(); - break; - - default: - Log.w(LOGTAG, "Selected index out of range: " + index); - } - } - - @Override - protected void configureShownDialog() { - super.configureShownDialog(); - - // Handle Show/Hide buttons. - if (!mIsRemovable) { - final TextView hideButton = (TextView) mDialog.getListView().getChildAt(INDEX_DISPLAY_BUTTON); - hideButton.setText(mIsHidden ? LABEL_SHOW : LABEL_HIDE); - } - } - - - private Dialog makeReorderDialog() { - final AlertDialog.Builder builder = new AlertDialog.Builder(getContext()); - - final Resources res = getContext().getResources(); - final String labelUp = res.getString(R.string.pref_panels_move_up); - final String labelDown = res.getString(R.string.pref_panels_move_down); - - builder.setTitle(getTitle()); - builder.setItems(new String[] { labelUp, labelDown }, new OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int index) { - dialog.dismiss(); - switch (index) { - case INDEX_MOVE_UP_BUTTON: - ((PanelsPreferenceCategory) mParentCategory).moveUp(PanelsPreference.this); - break; - - case INDEX_MOVE_DOWN_BUTTON: - ((PanelsPreferenceCategory) mParentCategory).moveDown(PanelsPreference.this); - break; - } - } - }); - - final Dialog dialog = builder.create(); - dialog.setOnShowListener(new OnShowListener() { - @Override - public void onShow(DialogInterface dialog) { - setReorderItemsEnabled(dialog); - } - }); - - return dialog; - } - - public void setIsFirst() { - mPositionState = STATE_IS_FIRST; - } - - public void setIsLast() { - mPositionState = STATE_IS_LAST; - } - - /** - * Configure enabled state of the reorder dialog, which must be done after the dialog is shown. - * @param dialog Dialog to configure - */ - private void setReorderItemsEnabled(DialogInterface dialog) { - // Update button enabled-ness for reordering. - switch (mPositionState) { - case STATE_IS_FIRST: - final TextView itemUp = (TextView) ((AlertDialog) dialog).getListView().getChildAt(INDEX_MOVE_UP_BUTTON); - itemUp.setEnabled(false); - // Disable clicks to this view. - itemUp.setOnClickListener(null); - break; - - case STATE_IS_LAST: - final TextView itemDown = (TextView) ((AlertDialog) dialog).getListView().getChildAt(INDEX_MOVE_DOWN_BUTTON); - itemDown.setEnabled(false); - // Disable clicks to this view. - itemDown.setOnClickListener(null); - break; - - default: - // Do nothing. - break; - } - } - - public void setHidden(boolean toHide) { - if (toHide) { - setIsDefault(false); - } - - if (mIsHidden != toHide) { - mIsHidden = toHide; - notifyChanged(); - } - } - - public boolean isHidden() { - return mIsHidden; - } - - public int getIndex() { - return mIndex; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/preferences/PanelsPreferenceCategory.java b/mobile/android/base/java/org/mozilla/gecko/preferences/PanelsPreferenceCategory.java deleted file mode 100644 index d44b6eaa9e..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/preferences/PanelsPreferenceCategory.java +++ /dev/null @@ -1,261 +0,0 @@ -/* 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.preferences; - -import org.mozilla.gecko.GeckoSharedPrefs; -import org.mozilla.gecko.Telemetry; -import org.mozilla.gecko.TelemetryContract; -import org.mozilla.gecko.TelemetryContract.Method; -import org.mozilla.gecko.home.HomeConfig; -import org.mozilla.gecko.home.HomeConfig.PanelConfig; -import org.mozilla.gecko.home.HomeConfig.State; -import org.mozilla.gecko.util.ThreadUtils; -import org.mozilla.gecko.util.UIAsyncTask; - -import android.content.Context; -import android.text.TextUtils; -import android.util.AttributeSet; - -public class PanelsPreferenceCategory extends CustomListCategory { - public static final String LOGTAG = "PanelsPrefCategory"; - - protected HomeConfig mHomeConfig; - protected HomeConfig.Editor mConfigEditor; - - protected UIAsyncTask.WithoutParams mLoadTask; - - public PanelsPreferenceCategory(Context context) { - super(context); - initConfig(context); - } - - public PanelsPreferenceCategory(Context context, AttributeSet attrs) { - super(context, attrs); - initConfig(context); - } - - public PanelsPreferenceCategory(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - initConfig(context); - } - - protected void initConfig(Context context) { - mHomeConfig = HomeConfig.getDefault(context); - } - - @Override - public void onAttachedToActivity() { - super.onAttachedToActivity(); - - loadHomeConfig(null); - } - - /** - * Load the Home Panels config and populate the preferences screen and maintain local state. - */ - private void loadHomeConfig(final String animatePanelId) { - mLoadTask = new UIAsyncTask.WithoutParams(ThreadUtils.getBackgroundHandler()) { - @Override - public HomeConfig.State doInBackground() { - return mHomeConfig.load(); - } - - @Override - public void onPostExecute(HomeConfig.State configState) { - mConfigEditor = configState.edit(); - displayHomeConfig(configState, animatePanelId); - } - }; - mLoadTask.execute(); - } - - /** - * Simplified refresh of Home Panels when there is no state to be persisted. - */ - public void refresh() { - refresh(null, null); - } - - /** - * Refresh the Home Panels list and animate a panel, if specified. - * If null, load from HomeConfig. - * - * @param State HomeConfig.State to rebuild Home Panels list from. - * @param String panelId of panel to be animated. - */ - public void refresh(State state, String animatePanelId) { - // Clear all the existing home panels. - removeAll(); - - if (state == null) { - loadHomeConfig(animatePanelId); - } else { - displayHomeConfig(state, animatePanelId); - } - } - - private void displayHomeConfig(HomeConfig.State configState, String animatePanelId) { - int index = 0; - for (PanelConfig panelConfig : configState) { - final boolean isRemovable = panelConfig.isDynamic(); - - // Create and add the pref. - final String panelId = panelConfig.getId(); - final boolean animate = TextUtils.equals(animatePanelId, panelId); - - final PanelsPreference pref = new PanelsPreference(getContext(), PanelsPreferenceCategory.this, isRemovable, index, animate); - pref.setTitle(panelConfig.getTitle()); - pref.setKey(panelConfig.getId()); - // XXX: Pull icon from PanelInfo. - addPreference(pref); - - if (panelConfig.isDisabled()) { - pref.setHidden(true); - } - - index++; - } - - setPositionState(); - setDefaultFromConfig(); - } - - private void setPositionState() { - final int prefCount = getPreferenceCount(); - - // Pass in position state to first and last preference. - final PanelsPreference firstPref = (PanelsPreference) getPreference(0); - firstPref.setIsFirst(); - - final PanelsPreference lastPref = (PanelsPreference) getPreference(prefCount - 1); - lastPref.setIsLast(); - } - - private void setDefaultFromConfig() { - final String defaultPanelId = mConfigEditor.getDefaultPanelId(); - if (defaultPanelId == null) { - mDefaultReference = null; - return; - } - - final int prefCount = getPreferenceCount(); - - for (int i = 0; i < prefCount; i++) { - final PanelsPreference pref = (PanelsPreference) getPreference(i); - - if (defaultPanelId.equals(pref.getKey())) { - super.setDefault(pref); - break; - } - } - } - - @Override - public void setDefault(CustomListPreference pref) { - super.setDefault(pref); - - final String id = pref.getKey(); - - final String defaultPanelId = mConfigEditor.getDefaultPanelId(); - if (defaultPanelId != null && defaultPanelId.equals(id)) { - return; - } - - updateVisibilityPrefsForPanel(id, true); - - mConfigEditor.setDefault(id); - mConfigEditor.apply(); - - Telemetry.sendUIEvent(TelemetryContract.Event.PANEL_SET_DEFAULT, Method.DIALOG, id); - } - - @Override - protected void onPrepareForRemoval() { - super.onPrepareForRemoval(); - if (mLoadTask != null) { - mLoadTask.cancel(); - } - } - - @Override - public void uninstall(CustomListPreference pref) { - final String id = pref.getKey(); - mConfigEditor.uninstall(id); - mConfigEditor.apply(); - - Telemetry.sendUIEvent(TelemetryContract.Event.PANEL_REMOVE, Method.DIALOG, id); - - super.uninstall(pref); - } - - public void moveUp(PanelsPreference pref) { - final int panelIndex = pref.getIndex(); - if (panelIndex > 0) { - final String panelKey = pref.getKey(); - mConfigEditor.moveTo(panelKey, panelIndex - 1); - final State state = mConfigEditor.apply(); - - Telemetry.sendUIEvent(TelemetryContract.Event.PANEL_MOVE, Method.DIALOG, panelKey); - - refresh(state, panelKey); - } - } - - public void moveDown(PanelsPreference pref) { - final int panelIndex = pref.getIndex(); - if (panelIndex < getPreferenceCount() - 1) { - final String panelKey = pref.getKey(); - mConfigEditor.moveTo(panelKey, panelIndex + 1); - final State state = mConfigEditor.apply(); - - Telemetry.sendUIEvent(TelemetryContract.Event.PANEL_MOVE, Method.DIALOG, panelKey); - - refresh(state, panelKey); - } - } - - /** - * Update the hide/show state of the preference and save the HomeConfig - * changes. - * - * @param pref Preference to update - * @param toHide New hidden state of the preference - */ - protected void setHidden(PanelsPreference pref, boolean toHide) { - final String id = pref.getKey(); - mConfigEditor.setDisabled(id, toHide); - mConfigEditor.apply(); - - if (toHide) { - Telemetry.sendUIEvent(TelemetryContract.Event.PANEL_HIDE, Method.DIALOG, id); - } else { - Telemetry.sendUIEvent(TelemetryContract.Event.PANEL_SHOW, Method.DIALOG, id); - } - - updateVisibilityPrefsForPanel(id, !toHide); - - pref.setHidden(toHide); - setDefaultFromConfig(); - } - - /** - * When the default panel is removed or disabled, find an enabled panel - * if possible and set it as mDefaultReference. - */ - @Override - protected void setFallbackDefault() { - setDefaultFromConfig(); - } - - private void updateVisibilityPrefsForPanel(String panelId, boolean toShow) { - if (HomeConfig.getIdForBuiltinPanelType(HomeConfig.PanelType.BOOKMARKS).equals(panelId)) { - GeckoSharedPrefs.forProfile(getContext()).edit().putBoolean(HomeConfig.PREF_KEY_BOOKMARKS_PANEL_ENABLED, toShow).apply(); - } - - if (HomeConfig.getIdForBuiltinPanelType(HomeConfig.PanelType.COMBINED_HISTORY).equals(panelId)) { - GeckoSharedPrefs.forProfile(getContext()).edit().putBoolean(HomeConfig.PREF_KEY_HISTORY_PANEL_ENABLED, toShow).apply(); - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/preferences/PrivateDataPreference.java b/mobile/android/base/java/org/mozilla/gecko/preferences/PrivateDataPreference.java deleted file mode 100644 index 61eff98e78..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/preferences/PrivateDataPreference.java +++ /dev/null @@ -1,67 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.preferences; - -import org.mozilla.gecko.GeckoAppShell; -import org.mozilla.gecko.Telemetry; -import org.mozilla.gecko.TelemetryContract; - -import org.json.JSONException; -import org.json.JSONObject; -import org.mozilla.gecko.icons.storage.DiskStorage; - -import java.util.Arrays; -import java.util.Collections; -import java.util.Set; - -import android.content.Context; -import android.util.AttributeSet; -import android.util.Log; - -class PrivateDataPreference extends MultiPrefMultiChoicePreference { - private static final String LOGTAG = "GeckoPrivateDataPreference"; - private static final String PREF_KEY_PREFIX = "private.data."; - - public PrivateDataPreference(Context context, AttributeSet attrs) { - super(context, attrs); - } - - @Override - protected void onDialogClosed(boolean positiveResult) { - super.onDialogClosed(positiveResult); - - if (!positiveResult) { - return; - } - - Telemetry.sendUIEvent(TelemetryContract.Event.SANITIZE, TelemetryContract.Method.DIALOG, "settings"); - - final Set values = getValues(); - final JSONObject json = new JSONObject(); - - for (String value : values) { - // Privacy pref checkbox values are stored in Android prefs to - // remember their check states. The key names are private.data.X, - // where X is a string from Gecko sanitization. This prefix is - // removed here so we can send the values to Gecko, which then does - // the sanitization for each key. - final String key = value.substring(PREF_KEY_PREFIX.length()); - try { - json.put(key, true); - } catch (JSONException e) { - Log.e(LOGTAG, "JSON error", e); - } - } - - if (values.contains("private.data.offlineApps")) { - // Remove all icons from storage if removing "Offline website data" was selected. - DiskStorage.get(getContext()).evictAll(); - } - - // clear private data in gecko - GeckoAppShell.notifyObservers("Sanitize:ClearData", json.toString()); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/preferences/SearchEnginePreference.java b/mobile/android/base/java/org/mozilla/gecko/preferences/SearchEnginePreference.java deleted file mode 100644 index 3ba80b562d..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/preferences/SearchEnginePreference.java +++ /dev/null @@ -1,183 +0,0 @@ -/* 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.preferences; - -import org.json.JSONException; -import org.json.JSONObject; -import org.mozilla.gecko.R; -import org.mozilla.gecko.SnackbarBuilder; -import org.mozilla.gecko.icons.IconCallback; -import org.mozilla.gecko.icons.IconDescriptor; -import org.mozilla.gecko.icons.IconResponse; -import org.mozilla.gecko.icons.Icons; -import org.mozilla.gecko.widget.FaviconView; - -import android.app.Activity; -import android.app.AlertDialog; -import android.content.Context; -import android.graphics.Bitmap; -import android.graphics.drawable.BitmapDrawable; -import android.support.design.widget.Snackbar; -import android.text.SpannableString; -import android.util.Log; -import android.view.View; - -/** - * Represents an element in the list of search engines on the preferences menu. - */ -public class SearchEnginePreference extends CustomListPreference { - protected String LOGTAG = "SearchEnginePreference"; - - protected static final int INDEX_REMOVE_BUTTON = 1; - - // The icon to display in the prompt when clicked. - private BitmapDrawable mPromptIcon; - - // The bitmap backing the drawable above - needed separately for the FaviconView. - private Bitmap mIconBitmap; - private final Object bitmapLock = new Object(); - - private FaviconView mFaviconView; - - // Search engine identifier specified by the gecko search service. This will be "other" - // for engines that are not shipped with the app. - private String mIdentifier; - - public SearchEnginePreference(Context context, SearchPreferenceCategory parentCategory) { - super(context, parentCategory); - } - - /** - * Called by Android when we're bound to the custom view. Allows us to set the custom properties - * of our custom view elements as we desire (We can now use findViewById on them). - * - * @param view The view instance for this Preference object. - */ - @Override - protected void onBindView(View view) { - super.onBindView(view); - - // We synchronise to avoid a race condition between this and the favicon loading callback in - // setSearchEngineFromJSON. - synchronized (bitmapLock) { - // Set the icon in the FaviconView. - mFaviconView = ((FaviconView) view.findViewById(R.id.search_engine_icon)); - - if (mIconBitmap != null) { - mFaviconView.updateAndScaleImage(IconResponse.create(mIconBitmap)); - } - } - } - - @Override - protected int getPreferenceLayoutResource() { - return R.layout.preference_search_engine; - } - - /** - * Returns the strings to be displayed in the dialog. - */ - @Override - protected String[] createDialogItems() { - return new String[] { LABEL_SET_AS_DEFAULT, - LABEL_REMOVE }; - } - - @Override - public void showDialog() { - // If this is the last engine, then we are the default, and none of the options - // on this menu can do anything. - if (mParentCategory.getPreferenceCount() == 1) { - Activity activity = (Activity) getContext(); - - SnackbarBuilder.builder(activity) - .message(R.string.pref_search_last_toast) - .duration(Snackbar.LENGTH_LONG) - .buildAndShow(); - - return; - } - - super.showDialog(); - } - - @Override - protected void configureDialogBuilder(AlertDialog.Builder builder) { - // Copy the icon from this object to the prompt we produce. We lazily create the drawable, - // as the user may not ever actually tap this object. - if (mPromptIcon == null && mIconBitmap != null) { - mPromptIcon = new BitmapDrawable(getContext().getResources(), mFaviconView.getBitmap()); - } - - builder.setIcon(mPromptIcon); - } - - @Override - protected void onDialogIndexClicked(int index) { - switch (index) { - case INDEX_SET_DEFAULT_BUTTON: - mParentCategory.setDefault(this); - break; - - case INDEX_REMOVE_BUTTON: - mParentCategory.uninstall(this); - break; - - default: - Log.w(LOGTAG, "Selected index out of range."); - break; - } - } - - /** - * @return Identifier of built-in search engine, or "other" if engine is not built-in. - */ - public String getIdentifier() { - return mIdentifier; - } - - /** - * Configure this Preference object from the Gecko search engine JSON object. - * @param geckoEngineJSON The Gecko-formatted JSON object representing the search engine. - * @throws JSONException If the JSONObject is invalid. - */ - public void setSearchEngineFromJSON(JSONObject geckoEngineJSON) throws JSONException { - mIdentifier = geckoEngineJSON.getString("identifier"); - - // A null JS value gets converted into a string. - if (mIdentifier.equals("null")) { - mIdentifier = "other"; - } - - final String engineName = geckoEngineJSON.getString("name"); - final SpannableString titleSpannable = new SpannableString(engineName); - - setTitle(titleSpannable); - - final String iconURI = geckoEngineJSON.getString("iconURI"); - // Keep a reference to the bitmap - we'll need it later in onBindView. - try { - Icons.with(getContext()) - .pageUrl(mIdentifier) - .icon(IconDescriptor.createGenericIcon(iconURI)) - .privileged(true) - .build() - .execute(new IconCallback() { - @Override - public void onIconResponse(IconResponse response) { - mIconBitmap = response.getBitmap(); - - if (mFaviconView != null) { - mFaviconView.updateAndScaleImage(response); - } - } - }); - } catch (IllegalArgumentException e) { - Log.e(LOGTAG, "IllegalArgumentException creating Bitmap. Most likely a zero-length bitmap.", e); - } catch (NullPointerException e) { - Log.e(LOGTAG, "NullPointerException creating Bitmap. Most likely a zero-length bitmap.", e); - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/preferences/SearchPreferenceCategory.java b/mobile/android/base/java/org/mozilla/gecko/preferences/SearchPreferenceCategory.java deleted file mode 100644 index 47db8b9b08..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/preferences/SearchPreferenceCategory.java +++ /dev/null @@ -1,145 +0,0 @@ -/* 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.preferences; - -import android.content.Context; -import android.preference.Preference; -import android.util.AttributeSet; -import android.util.Log; - -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - -import org.mozilla.gecko.EventDispatcher; -import org.mozilla.gecko.GeckoApp; -import org.mozilla.gecko.GeckoAppShell; -import org.mozilla.gecko.Telemetry; -import org.mozilla.gecko.TelemetryContract; -import org.mozilla.gecko.TelemetryContract.Method; -import org.mozilla.gecko.util.GeckoEventListener; -import org.mozilla.gecko.util.ThreadUtils; - -public class SearchPreferenceCategory extends CustomListCategory implements GeckoEventListener { - public static final String LOGTAG = "SearchPrefCategory"; - - public SearchPreferenceCategory(Context context) { - super(context); - } - - public SearchPreferenceCategory(Context context, AttributeSet attrs) { - super(context, attrs); - } - - public SearchPreferenceCategory(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - } - - @Override - protected void onAttachedToActivity() { - super.onAttachedToActivity(); - - // Register for SearchEngines messages and request list of search engines from Gecko. - GeckoApp.getEventDispatcher().registerGeckoThreadListener(this, "SearchEngines:Data"); - GeckoAppShell.notifyObservers("SearchEngines:GetVisible", null); - } - - @Override - protected void onPrepareForRemoval() { - super.onPrepareForRemoval(); - - GeckoApp.getEventDispatcher().unregisterGeckoThreadListener(this, "SearchEngines:Data"); - } - - @Override - public void setDefault(CustomListPreference item) { - super.setDefault(item); - - sendGeckoEngineEvent("SearchEngines:SetDefault", item.getTitle().toString()); - - final String identifier = ((SearchEnginePreference) item).getIdentifier(); - Telemetry.sendUIEvent(TelemetryContract.Event.SEARCH_SET_DEFAULT, Method.DIALOG, identifier); - } - - @Override - public void uninstall(CustomListPreference item) { - super.uninstall(item); - - sendGeckoEngineEvent("SearchEngines:Remove", item.getTitle().toString()); - - final String identifier = ((SearchEnginePreference) item).getIdentifier(); - Telemetry.sendUIEvent(TelemetryContract.Event.SEARCH_REMOVE, Method.DIALOG, identifier); - } - - @Override - public void handleMessage(String event, final JSONObject data) { - if (event.equals("SearchEngines:Data")) { - // Parse engines array from JSON. - JSONArray engines; - try { - engines = data.getJSONArray("searchEngines"); - } catch (JSONException e) { - Log.e(LOGTAG, "Unable to decode search engine data from Gecko.", e); - return; - } - - // Clear the preferences category from this thread. - this.removeAll(); - - // Create an element in this PreferenceCategory for each engine. - for (int i = 0; i < engines.length(); i++) { - try { - final JSONObject engineJSON = engines.getJSONObject(i); - - final SearchEnginePreference enginePreference = new SearchEnginePreference(getContext(), this); - enginePreference.setSearchEngineFromJSON(engineJSON); - enginePreference.setOnPreferenceClickListener(new OnPreferenceClickListener() { - @Override - public boolean onPreferenceClick(Preference preference) { - SearchEnginePreference sPref = (SearchEnginePreference) preference; - // Display the configuration dialog associated with the tapped engine. - sPref.showDialog(); - return true; - } - }); - - addPreference(enginePreference); - - // The first element in the array is the default engine. - if (i == 0) { - // We set this here, not in setSearchEngineFromJSON, because it allows us to - // keep a reference to the default engine to use when the AlertDialog - // callbacks are used. - ThreadUtils.postToUiThread(new Runnable() { - @Override - public void run() { - enginePreference.setIsDefault(true); - } - }); - mDefaultReference = enginePreference; - } - } catch (JSONException e) { - Log.e(LOGTAG, "JSONException parsing engine at index " + i, e); - } - } - } - } - - /** - * Helper method to send a particular event string to Gecko with an associated engine name. - * @param event The type of event to send. - * @param engine The engine to which the event relates. - */ - private void sendGeckoEngineEvent(String event, String engineName) { - JSONObject json = new JSONObject(); - try { - json.put("engine", engineName); - } catch (JSONException e) { - Log.e(LOGTAG, "JSONException creating search engine configuration change message for Gecko.", e); - return; - } - GeckoAppShell.notifyObservers(event, json.toString()); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/preferences/SetHomepagePreference.java b/mobile/android/base/java/org/mozilla/gecko/preferences/SetHomepagePreference.java deleted file mode 100644 index 55be702c4c..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/preferences/SetHomepagePreference.java +++ /dev/null @@ -1,124 +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.preferences; - -import org.mozilla.gecko.AboutPages; -import org.mozilla.gecko.GeckoSharedPrefs; -import org.mozilla.gecko.R; - -import android.app.AlertDialog; -import android.content.Context; -import android.content.SharedPreferences; -import android.preference.DialogPreference; -import android.text.TextUtils; -import android.util.AttributeSet; -import android.view.View; -import android.view.inputmethod.InputMethodManager; -import android.widget.EditText; -import android.widget.RadioButton; -import android.widget.RadioGroup; - -public class SetHomepagePreference extends DialogPreference { - private static final String DEFAULT_HOMEPAGE = AboutPages.HOME; - - private final SharedPreferences prefs; - - private RadioGroup homepageLayout; - private RadioButton defaultRadio; - private RadioButton userAddressRadio; - private EditText homepageEditText; - - // This is the url that 1) was loaded from prefs or, 2) stored - // when the user pressed the "default homepage" checkbox. - private String storedUrl; - - public SetHomepagePreference(final Context context, final AttributeSet attrs) { - super(context, attrs); - prefs = GeckoSharedPrefs.forProfile(context); - } - - @Override - protected void onPrepareDialogBuilder(AlertDialog.Builder builder) { - // Without this GB devices have a black background to the dialog. - builder.setInverseBackgroundForced(true); - } - - @Override - protected void onBindDialogView(final View view) { - super.onBindDialogView(view); - - homepageLayout = (RadioGroup) view.findViewById(R.id.homepage_layout); - defaultRadio = (RadioButton) view.findViewById(R.id.radio_default); - userAddressRadio = (RadioButton) view.findViewById(R.id.radio_user_address); - homepageEditText = (EditText) view.findViewById(R.id.edittext_user_address); - - storedUrl = prefs.getString(GeckoPreferences.PREFS_HOMEPAGE, DEFAULT_HOMEPAGE); - - homepageLayout.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() { - @Override - public void onCheckedChanged(final RadioGroup radioGroup, final int checkedId) { - if (checkedId == R.id.radio_user_address) { - homepageEditText.setVisibility(View.VISIBLE); - openKeyboardAndSelectAll(getContext(), homepageEditText); - } else { - homepageEditText.setVisibility(View.GONE); - } - } - }); - setUIState(storedUrl); - } - - private void setUIState(final String url) { - if (isUrlDefaultHomepage(url)) { - defaultRadio.setChecked(true); - } else { - userAddressRadio.setChecked(true); - homepageEditText.setText(url); - } - } - - private boolean isUrlDefaultHomepage(final String url) { - return TextUtils.isEmpty(url) || DEFAULT_HOMEPAGE.equals(url); - } - - private static void openKeyboardAndSelectAll(final Context context, final View viewToFocus) { - viewToFocus.requestFocus(); - viewToFocus.post(new Runnable() { - @Override - public void run() { - InputMethodManager imm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE); - imm.showSoftInput(viewToFocus, InputMethodManager.SHOW_IMPLICIT); - // android:selectAllOnFocus doesn't work for the initial focus: - // I'm not sure why. We manually selectAll instead. - if (viewToFocus instanceof EditText) { - ((EditText) viewToFocus).selectAll(); - } - } - }); - } - - @Override - protected void onDialogClosed(final boolean positiveResult) { - super.onDialogClosed(positiveResult); - if (positiveResult) { - final SharedPreferences.Editor editor = prefs.edit(); - final String homePageEditTextValue = homepageEditText.getText().toString(); - final String newPrefValue; - if (homepageLayout.getCheckedRadioButtonId() == R.id.radio_default || - isUrlDefaultHomepage(homePageEditTextValue)) { - newPrefValue = ""; - editor.remove(GeckoPreferences.PREFS_HOMEPAGE); - } else { - newPrefValue = homePageEditTextValue; - editor.putString(GeckoPreferences.PREFS_HOMEPAGE, newPrefValue); - } - editor.apply(); - - if (getOnPreferenceChangeListener() != null) { - getOnPreferenceChangeListener().onPreferenceChange(this, newPrefValue); - } - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/preferences/SyncPreference.java b/mobile/android/base/java/org/mozilla/gecko/preferences/SyncPreference.java deleted file mode 100644 index 350ac8fc0a..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/preferences/SyncPreference.java +++ /dev/null @@ -1,103 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.preferences; - -import android.content.Context; -import android.content.Intent; -import android.preference.Preference; -import android.text.TextUtils; -import android.util.AttributeSet; - -import com.squareup.picasso.Picasso; -import com.squareup.picasso.Target; - -import org.mozilla.gecko.AppConstants; -import org.mozilla.gecko.R; -import org.mozilla.gecko.Telemetry; -import org.mozilla.gecko.TelemetryContract; -import org.mozilla.gecko.TelemetryContract.Method; -import org.mozilla.gecko.fxa.FxAccountConstants; -import org.mozilla.gecko.fxa.activities.FxAccountWebFlowActivity; -import org.mozilla.gecko.fxa.activities.PicassoPreferenceIconTarget; -import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount; -import org.mozilla.gecko.sync.ExtendedJSONObject; -import org.mozilla.gecko.util.ThreadUtils; - -class SyncPreference extends Preference { - private final Context mContext; - private final Target profileAvatarTarget; - - public SyncPreference(Context context, AttributeSet attrs) { - super(context, attrs); - mContext = context; - final float cornerRadius = mContext.getResources().getDimension(R.dimen.fxaccount_profile_image_width) / 2; - profileAvatarTarget = new PicassoPreferenceIconTarget(mContext.getResources(), this, cornerRadius); - } - - private void launchFxASetup() { - final Intent intent = new Intent(FxAccountConstants.ACTION_FXA_GET_STARTED); - intent.putExtra(FxAccountWebFlowActivity.EXTRA_ENDPOINT, FxAccountConstants.ENDPOINT_PREFERENCES); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - intent.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION); - mContext.startActivity(intent); - } - - public void update(final AndroidFxAccount fxAccount) { - if (fxAccount == null) { - ThreadUtils.postToUiThread(new Runnable() { - @Override - public void run() { - setTitle(R.string.pref_sync); - setSummary(R.string.pref_sync_summary); - // Cancel any pending task. - Picasso.with(mContext).cancelRequest(profileAvatarTarget); - // Clear previously set icon. - // Bug 1312719 - IconDrawable is prior to IconResId, drawable must be set null before setIcon(resId) - // http://androidxref.com/5.1.1_r6/xref/frameworks/base/core/java/android/preference/Preference.java#562 - setIcon(null); - setIcon(R.drawable.sync_avatar_default); - } - }); - return; - } - - // Update title from account email. - ThreadUtils.postToUiThread(new Runnable() { - @Override - public void run() { - setTitle(fxAccount.getEmail()); - setSummary(""); - } - }); - - final ExtendedJSONObject profileJSON = fxAccount.getProfileJSON(); - if (profileJSON == null) { - return; - } - - // Avatar URI empty, return early. - final String avatarURI = profileJSON.getString(FxAccountConstants.KEY_PROFILE_JSON_AVATAR); - if (TextUtils.isEmpty(avatarURI)) { - return; - } - - Picasso.with(mContext) - .load(avatarURI) - .centerInside() - .resizeDimen(R.dimen.fxaccount_profile_image_width, R.dimen.fxaccount_profile_image_height) - .placeholder(R.drawable.sync_avatar_default) - .error(R.drawable.sync_avatar_default) - .into(profileAvatarTarget); - } - - @Override - protected void onClick() { - // Launch the FxA "Get started" activity, which will dispatch to the - // right location. - launchFxASetup(); - Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, Method.SETTINGS, "sync_setup"); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/promotion/AddToHomeScreenPromotion.java b/mobile/android/base/java/org/mozilla/gecko/promotion/AddToHomeScreenPromotion.java deleted file mode 100644 index c1eeb6bd50..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/promotion/AddToHomeScreenPromotion.java +++ /dev/null @@ -1,237 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.promotion; - -import android.app.Activity; -import android.content.Context; -import android.database.Cursor; -import android.os.Bundle; -import android.support.annotation.CallSuper; -import android.util.Log; - -import com.keepsafe.switchboard.SwitchBoard; - -import org.json.JSONException; -import org.json.JSONObject; -import org.mozilla.gecko.AboutPages; -import org.mozilla.gecko.BrowserApp; -import org.mozilla.gecko.GeckoProfile; -import org.mozilla.gecko.Tab; -import org.mozilla.gecko.Tabs; -import org.mozilla.gecko.db.BrowserContract; -import org.mozilla.gecko.db.BrowserDB; -import org.mozilla.gecko.db.UrlAnnotations; -import org.mozilla.gecko.delegates.TabsTrayVisibilityAwareDelegate; -import org.mozilla.gecko.Experiments; -import org.mozilla.gecko.util.ThreadUtils; - -import java.lang.ref.WeakReference; - -import ch.boye.httpclientandroidlib.util.TextUtils; - -/** - * Promote "Add to home screen" if user visits website often. - */ -public class AddToHomeScreenPromotion extends TabsTrayVisibilityAwareDelegate implements Tabs.OnTabsChangedListener { - private static class URLHistory { - public final long visits; - public final long lastVisit; - - private URLHistory(long visits, long lastVisit) { - this.visits = visits; - this.lastVisit = lastVisit; - } - } - - private static final String LOGTAG = "GeckoPromoteShortcut"; - - private static final String EXPERIMENT_MINIMUM_TOTAL_VISITS = "minimumTotalVisits"; - private static final String EXPERIMENT_LAST_VISIT_MINIMUM_AGE = "lastVisitMinimumAgeMs"; - private static final String EXPERIMENT_LAST_VISIT_MAXIMUM_AGE = "lastVisitMaximumAgeMs"; - - private WeakReference activityReference; - private boolean isEnabled; - private int minimumVisits; - private int lastVisitMinimumAgeMs; - private int lastVisitMaximumAgeMs; - - @CallSuper - @Override - public void onCreate(BrowserApp browserApp, Bundle savedInstanceState) { - super.onCreate(browserApp, savedInstanceState); - activityReference = new WeakReference(browserApp); - - initializeExperiment(browserApp); - } - - @Override - public void onResume(BrowserApp browserApp) { - Tabs.registerOnTabsChangedListener(this); - } - - @Override - public void onPause(BrowserApp browserApp) { - Tabs.unregisterOnTabsChangedListener(this); - } - - private void initializeExperiment(Context context) { - if (!SwitchBoard.isInExperiment(context, Experiments.PROMOTE_ADD_TO_HOMESCREEN)) { - Log.v(LOGTAG, "Experiment not enabled"); - // Experiment is not enabled. No need to try to read values. - return; - } - - JSONObject values = SwitchBoard.getExperimentValuesFromJson(context, Experiments.PROMOTE_ADD_TO_HOMESCREEN); - if (values == null) { - // We didn't get any values for this experiment. Let's disable it instead of picking default - // values that might be bad. - return; - } - - try { - initializeWithValues( - values.getInt(EXPERIMENT_MINIMUM_TOTAL_VISITS), - values.getInt(EXPERIMENT_LAST_VISIT_MINIMUM_AGE), - values.getInt(EXPERIMENT_LAST_VISIT_MAXIMUM_AGE)); - } catch (JSONException e) { - Log.w(LOGTAG, "Could not read experiment values", e); - } - } - - private void initializeWithValues(int minimumVisits, int lastVisitMinimumAgeMs, int lastVisitMaximumAgeMs) { - this.isEnabled = true; - - this.minimumVisits = minimumVisits; - this.lastVisitMinimumAgeMs = lastVisitMinimumAgeMs; - this.lastVisitMaximumAgeMs = lastVisitMaximumAgeMs; - } - - @Override - public void onTabChanged(final Tab tab, Tabs.TabEvents msg, String data) { - if (tab == null) { - return; - } - - if (!Tabs.getInstance().isSelectedTab(tab)) { - // We only ever want to show this promotion for the current tab. - return; - } - - if (Tabs.TabEvents.LOADED != msg) { - return; - } - - if (tab.isPrivate()) { - // Never show the prompt for private browsing tabs. - return; - } - - if (isTabsTrayVisible()) { - // We only want to show this prompt if this tab is in the foreground and not on top - // of the tabs tray. - return; - } - - ThreadUtils.postToBackgroundThread(new Runnable() { - @Override - public void run() { - maybeShowPromotionForUrl(tab.getURL(), tab.getTitle()); - } - }); - } - - private void maybeShowPromotionForUrl(String url, String title) { - if (!isEnabled) { - return; - } - - final Context context = activityReference.get(); - if (context == null) { - return; - } - - if (!shouldShowPromotion(context, url, title)) { - return; - } - - HomeScreenPrompt.show(context, url, title); - } - - private boolean shouldShowPromotion(Context context, String url, String title) { - if (TextUtils.isEmpty(url) || TextUtils.isEmpty(title)) { - // We require an URL and a title for the shortcut. - return false; - } - - if (AboutPages.isAboutPage(url)) { - // No promotion for our internal sites. - return false; - } - - if (!url.startsWith("https://")) { - // Only promote websites that are served over HTTPS. - return false; - } - - URLHistory history = getHistoryForURL(context, url); - if (history == null) { - // There's no history for this URL yet or we can't read it right now. Just ignore. - return false; - } - - if (history.visits < minimumVisits) { - // This URL has not been visited often enough. - return false; - } - - if (history.lastVisit > System.currentTimeMillis() - lastVisitMinimumAgeMs) { - // The last visit is too new. Do not show promotion. This is mostly to avoid that the - // promotion shows up for a quick refreshs and in the worst case the last visit could - // be the current visit (race). - return false; - } - - if (history.lastVisit < System.currentTimeMillis() - lastVisitMaximumAgeMs) { - // The last visit is to old. Do not show promotion. - return false; - } - - if (hasAcceptedOrDeclinedHomeScreenShortcut(context, url)) { - // The user has already created a shortcut in the past or actively declined to create one. - // Let's not ask again for this url - We do not want to be annoying. - return false; - } - - return true; - } - - protected boolean hasAcceptedOrDeclinedHomeScreenShortcut(Context context, String url) { - final UrlAnnotations urlAnnotations = BrowserDB.from(context).getUrlAnnotations(); - return urlAnnotations.hasAcceptedOrDeclinedHomeScreenShortcut(context.getContentResolver(), url); - } - - protected URLHistory getHistoryForURL(Context context, String url) { - final GeckoProfile profile = GeckoProfile.get(context); - final BrowserDB browserDB = BrowserDB.from(profile); - - Cursor cursor = null; - try { - cursor = browserDB.getHistoryForURL(context.getContentResolver(), url); - - if (cursor.moveToFirst()) { - return new URLHistory( - cursor.getInt(cursor.getColumnIndex(BrowserContract.History.VISITS)), - cursor.getLong(cursor.getColumnIndex(BrowserContract.History.DATE_LAST_VISITED))); - } - } finally { - if (cursor != null) { - cursor.close(); - } - } - - return null; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/promotion/HomeScreenPrompt.java b/mobile/android/base/java/org/mozilla/gecko/promotion/HomeScreenPrompt.java deleted file mode 100644 index 0f2df8a2c3..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/promotion/HomeScreenPrompt.java +++ /dev/null @@ -1,237 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.promotion; - -import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; -import android.animation.AnimatorSet; -import android.animation.ObjectAnimator; -import android.content.Context; -import android.content.Intent; -import android.net.Uri; -import android.os.Bundle; -import android.view.MotionEvent; -import android.view.View; -import android.widget.ImageView; -import android.widget.TextView; - -import org.mozilla.gecko.GeckoAppShell; -import org.mozilla.gecko.GeckoProfile; -import org.mozilla.gecko.Locales; -import org.mozilla.gecko.R; -import org.mozilla.gecko.Telemetry; -import org.mozilla.gecko.TelemetryContract; -import org.mozilla.gecko.db.BrowserDB; -import org.mozilla.gecko.db.UrlAnnotations; -import org.mozilla.gecko.icons.IconCallback; -import org.mozilla.gecko.icons.IconResponse; -import org.mozilla.gecko.icons.Icons; -import org.mozilla.gecko.Experiments; -import org.mozilla.gecko.util.ActivityUtils; -import org.mozilla.gecko.util.ThreadUtils; - -/** - * Prompt to promote adding the current website to the home screen. - */ -public class HomeScreenPrompt extends Locales.LocaleAwareActivity implements IconCallback { - private static final String EXTRA_TITLE = "title"; - private static final String EXTRA_URL = "url"; - - private static final String TELEMETRY_EXTRA = "home_screen_promotion"; - - private View containerView; - private ImageView iconView; - private String title; - private String url; - private boolean isAnimating; - private boolean hasAccepted; - private boolean hasDeclined; - - public static void show(Context context, String url, String title) { - Intent intent = new Intent(context, HomeScreenPrompt.class); - intent.putExtra(EXTRA_TITLE, title); - intent.putExtra(EXTRA_URL, url); - context.startActivity(intent); - } - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - fetchDataFromIntent(); - setupViews(); - loadShortcutIcon(); - - slideIn(); - - Telemetry.startUISession(TelemetryContract.Session.EXPERIMENT, Experiments.PROMOTE_ADD_TO_HOMESCREEN); - - // Technically this isn't triggered by a "service". But it's also triggered by a background task and without - // actual user interaction. - Telemetry.sendUIEvent(TelemetryContract.Event.SHOW, TelemetryContract.Method.SERVICE, TELEMETRY_EXTRA); - } - - @Override - protected void onDestroy() { - super.onDestroy(); - - Telemetry.stopUISession(TelemetryContract.Session.EXPERIMENT, Experiments.PROMOTE_ADD_TO_HOMESCREEN); - } - - private void fetchDataFromIntent() { - final Bundle extras = getIntent().getExtras(); - - title = extras.getString(EXTRA_TITLE); - url = extras.getString(EXTRA_URL); - } - - private void setupViews() { - setContentView(R.layout.homescreen_prompt); - - ((TextView) findViewById(R.id.title)).setText(title); - - Uri uri = Uri.parse(url); - ((TextView) findViewById(R.id.host)).setText(uri.getHost()); - - containerView = findViewById(R.id.container); - iconView = (ImageView) findViewById(R.id.icon); - - findViewById(R.id.add).setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - hasAccepted = true; - - addToHomeScreen(); - } - }); - - findViewById(R.id.close).setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - onDecline(); - } - }); - } - - private void addToHomeScreen() { - ThreadUtils.postToBackgroundThread(new Runnable() { - @Override - public void run() { - GeckoAppShell.createShortcut(title, url); - - Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.BUTTON, TELEMETRY_EXTRA); - - ActivityUtils.goToHomeScreen(HomeScreenPrompt.this); - - finish(); - } - }); - } - - - - private void loadShortcutIcon() { - Icons.with(this) - .pageUrl(url) - .skipNetwork() - .skipMemory() - .forLauncherIcon() - .build() - .execute(this); - } - - private void slideIn() { - containerView.setTranslationY(500); - containerView.setAlpha(0); - - final Animator translateAnimator = ObjectAnimator.ofFloat(containerView, "translationY", 0); - translateAnimator.setDuration(400); - - final Animator alphaAnimator = ObjectAnimator.ofFloat(containerView, "alpha", 1); - alphaAnimator.setStartDelay(200); - alphaAnimator.setDuration(600); - - final AnimatorSet set = new AnimatorSet(); - set.playTogether(alphaAnimator, translateAnimator); - set.setStartDelay(400); - - set.start(); - } - - /** - * Remember that the user rejected creating a home screen shortcut for this URL. - */ - private void rememberRejection() { - ThreadUtils.postToBackgroundThread(new Runnable() { - @Override - public void run() { - final UrlAnnotations urlAnnotations = BrowserDB.from(HomeScreenPrompt.this).getUrlAnnotations(); - urlAnnotations.insertHomeScreenShortcut(getContentResolver(), url, false); - } - }); - } - - private void slideOut() { - if (isAnimating) { - return; - } - - isAnimating = true; - - ObjectAnimator animator = ObjectAnimator.ofFloat(containerView, "translationY", containerView.getHeight()); - animator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - finish(); - } - - }); - animator.start(); - } - - @Override - public void finish() { - super.finish(); - - // Don't perform an activity-dismiss animation. - overridePendingTransition(0, 0); - } - - @Override - public void onBackPressed() { - onDecline(); - } - - private void onDecline() { - if (hasDeclined || hasAccepted) { - return; - } - - rememberRejection(); - slideOut(); - - // Technically not always an action triggered by the "back" button but with the same effect: Finishing this - // activity and going back to the previous one. - Telemetry.sendUIEvent(TelemetryContract.Event.CANCEL, TelemetryContract.Method.BACK, TELEMETRY_EXTRA); - - hasDeclined = true; - } - - /** - * User clicked outside of the prompt. - */ - @Override - public boolean onTouchEvent(MotionEvent event) { - onDecline(); - - return true; - } - - @Override - public void onIconResponse(IconResponse response) { - iconView.setImageBitmap(response.getBitmap()); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/promotion/ReaderViewBookmarkPromotion.java b/mobile/android/base/java/org/mozilla/gecko/promotion/ReaderViewBookmarkPromotion.java deleted file mode 100644 index db5a531c66..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/promotion/ReaderViewBookmarkPromotion.java +++ /dev/null @@ -1,103 +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.promotion; - -import android.content.Intent; -import android.content.SharedPreferences; - -import com.keepsafe.switchboard.SwitchBoard; - -import org.mozilla.gecko.BrowserApp; -import org.mozilla.gecko.GeckoSharedPrefs; -import org.mozilla.gecko.R; -import org.mozilla.gecko.Tab; -import org.mozilla.gecko.Tabs; -import org.mozilla.gecko.delegates.BrowserAppDelegateWithReference; -import org.mozilla.gecko.reader.ReaderModeUtils; -import org.mozilla.gecko.Experiments; - -public class ReaderViewBookmarkPromotion extends BrowserAppDelegateWithReference implements Tabs.OnTabsChangedListener { - private static final String PREF_FIRST_RV_HINT_SHOWN = "first_reader_view_hint_shown"; - private static final String FIRST_READERVIEW_OPEN_TELEMETRYEXTRA = "first_readerview_open_prompt"; - - private boolean hasEnteredReaderMode = false; - - @Override - public void onResume(BrowserApp browserApp) { - Tabs.registerOnTabsChangedListener(this); - } - - @Override - public void onPause(BrowserApp browserApp) { - Tabs.unregisterOnTabsChangedListener(this); - } - - @Override - public void onTabChanged(Tab tab, Tabs.TabEvents msg, String data) { - switch (msg) { - case LOCATION_CHANGE: - // old url: data - // new url: tab.getURL() - final boolean enteringReaderMode = ReaderModeUtils.isEnteringReaderMode(data, tab.getURL()); - - if (!hasEnteredReaderMode && enteringReaderMode) { - hasEnteredReaderMode = true; - promoteBookmarking(); - } - - break; - } - } - - @Override - public void onActivityResult(BrowserApp browserApp, int requestCode, int resultCode, Intent data) { - switch (requestCode) { - case BrowserApp.ACTIVITY_REQUEST_TRIPLE_READERVIEW: - if (resultCode == BrowserApp.ACTIVITY_RESULT_TRIPLE_READERVIEW_ADD_BOOKMARK) { - final Tab tab = Tabs.getInstance().getSelectedTab(); - if (tab != null) { - tab.addBookmark(); - } - } else if (resultCode == BrowserApp.ACTIVITY_RESULT_TRIPLE_READERVIEW_IGNORE) { - // Nothing to do: we won't show this promotion again either way. - } - break; - } - } - - private void promoteBookmarking() { - final BrowserApp browserApp = getBrowserApp(); - if (browserApp == null) { - return; - } - - final SharedPreferences prefs = GeckoSharedPrefs.forProfile(browserApp); - final boolean isEnabled = SwitchBoard.isInExperiment(browserApp, Experiments.TRIPLE_READERVIEW_BOOKMARK_PROMPT); - - // We reuse the same preference as for the first offline reader view bookmark - // as we only want to show one of the two UIs (they both explain the same - // functionality). - if (!isEnabled || prefs.getBoolean(PREF_FIRST_RV_HINT_SHOWN, false)) { - return; - } - - SimpleHelperUI.show(browserApp, - FIRST_READERVIEW_OPEN_TELEMETRYEXTRA, - BrowserApp.ACTIVITY_REQUEST_TRIPLE_READERVIEW, - R.string.helper_triple_readerview_open_title, - R.string.helper_triple_readerview_open_message, - R.drawable.helper_readerview_bookmark, // We share the icon with the usual helper UI - R.string.helper_triple_readerview_open_button, - BrowserApp.ACTIVITY_RESULT_TRIPLE_READERVIEW_ADD_BOOKMARK, - BrowserApp.ACTIVITY_RESULT_TRIPLE_READERVIEW_IGNORE); - - prefs - .edit() - .putBoolean(PREF_FIRST_RV_HINT_SHOWN, true) - .apply(); - } - -} diff --git a/mobile/android/base/java/org/mozilla/gecko/promotion/SimpleHelperUI.java b/mobile/android/base/java/org/mozilla/gecko/promotion/SimpleHelperUI.java deleted file mode 100644 index b6b857fb9f..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/promotion/SimpleHelperUI.java +++ /dev/null @@ -1,194 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.promotion; - -import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; -import android.animation.AnimatorSet; -import android.animation.ObjectAnimator; -import android.app.Activity; -import android.content.Intent; -import android.os.Bundle; -import android.support.annotation.DrawableRes; -import android.support.annotation.StringRes; -import android.view.MotionEvent; -import android.view.View; -import android.widget.Button; -import android.widget.ImageView; -import android.widget.TextView; - -import org.mozilla.gecko.Locales; -import org.mozilla.gecko.R; -import org.mozilla.gecko.Telemetry; -import org.mozilla.gecko.TelemetryContract; - -/** - * Generic HelperUI (prompt) that can be populated with an image, title, message and action button. - * See show() for usage. This is run as an Activity, results must be handled in the parent Activities - * onActivityResult(). - */ -public class SimpleHelperUI extends Locales.LocaleAwareActivity { - public static final String PREF_FIRST_RVBP_SHOWN = "first_reader_view_bookmark_prompt_shown"; - public static final String FIRST_RVBP_SHOWN_TELEMETRYEXTRA = "first_readerview_bookmark_prompt"; - - private View containerView; - - private boolean isAnimating; - - private String mTelemetryExtra; - - private static final String EXTRA_TELEMETRYEXTRA = "telemetryextra"; - private static final String EXTRA_TITLE = "title"; - private static final String EXTRA_MESSAGE = "message"; - private static final String EXTRA_IMAGE = "image"; - private static final String EXTRA_BUTTON = "button"; - private static final String EXTRA_RESULTCODE_POSITIVE = "positive"; - private static final String EXTRA_RESULTCODE_NEGATIVE = "negative"; - - - /** - * Show a generic helper UI/prompt. - * - * @param owner The owning Activity, the result of this prompt will be delivered to its - * onActivityResult(). - * @param requestCode The request code for the Activity that will be created, this is passed to - * onActivityResult() to identify the prompt. - * - * @param positiveResultCode The result code passed to onActivityResult() when the button has - * been pressed. - * @param negativeResultCode The result code passed to onActivityResult() when the prompt was - * dismissed, either by pressing outside the prompt or by pressing the - * device back button. - */ - public static void show(Activity owner, String telemetryExtra, - int requestCode, - @StringRes int title, @StringRes int message, - @DrawableRes int image, @StringRes int buttonText, - int positiveResultCode, int negativeResultCode) { - Intent intent = new Intent(owner, SimpleHelperUI.class); - - intent.putExtra(EXTRA_TELEMETRYEXTRA, telemetryExtra); - - intent.putExtra(EXTRA_TITLE, title); - intent.putExtra(EXTRA_MESSAGE, message); - - intent.putExtra(EXTRA_IMAGE, image); - intent.putExtra(EXTRA_BUTTON, buttonText); - - intent.putExtra(EXTRA_RESULTCODE_POSITIVE, positiveResultCode); - intent.putExtra(EXTRA_RESULTCODE_NEGATIVE, negativeResultCode); - - owner.startActivityForResult(intent, requestCode); - } - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - mTelemetryExtra = getIntent().getStringExtra(EXTRA_TELEMETRYEXTRA); - - setupViews(); - - slideIn(); - } - - private void setupViews() { - final Intent i = getIntent(); - - setContentView(R.layout.simple_helper_ui); - - containerView = findViewById(R.id.container); - - ((ImageView) findViewById(R.id.image)).setImageResource(i.getIntExtra(EXTRA_IMAGE, 0)); - - ((TextView) findViewById(R.id.title)).setText(i.getIntExtra(EXTRA_TITLE, 0)); - - ((TextView) findViewById(R.id.message)).setText(i.getIntExtra(EXTRA_MESSAGE, 0)); - - final Button button = ((Button) findViewById(R.id.button)); - button.setText(i.getIntExtra(EXTRA_BUTTON, 0)); - button.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - slideOut(); - - Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.BUTTON, mTelemetryExtra); - - setResult(i.getIntExtra(EXTRA_RESULTCODE_POSITIVE, -1)); - } - }); - } - - private void slideIn() { - containerView.setTranslationY(500); - containerView.setAlpha(0); - - final Animator translateAnimator = ObjectAnimator.ofFloat(containerView, "translationY", 0); - translateAnimator.setDuration(400); - - final Animator alphaAnimator = ObjectAnimator.ofFloat(containerView, "alpha", 1); - alphaAnimator.setStartDelay(200); - alphaAnimator.setDuration(600); - - final AnimatorSet set = new AnimatorSet(); - set.playTogether(alphaAnimator, translateAnimator); - set.setStartDelay(400); - - set.start(); - } - - private void slideOut() { - if (isAnimating) { - return; - } - - isAnimating = true; - - ObjectAnimator animator = ObjectAnimator.ofFloat(containerView, "translationY", containerView.getHeight()); - animator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - finish(); - } - - }); - animator.start(); - } - - @Override - public void finish() { - super.finish(); - - // Don't perform an activity-dismiss animation. - overridePendingTransition(0, 0); - } - - @Override - public void onBackPressed() { - slideOut(); - - Telemetry.sendUIEvent(TelemetryContract.Event.CANCEL, TelemetryContract.Method.BACK, mTelemetryExtra); - - setResult(getIntent().getIntExtra(EXTRA_RESULTCODE_NEGATIVE, -1)); - - } - - /** - * User clicked outside of the prompt. - */ - @Override - public boolean onTouchEvent(MotionEvent event) { - slideOut(); - - // Not really an action triggered by the "back" button but with the same effect: Finishing this - // activity and going back to the previous one. - Telemetry.sendUIEvent(TelemetryContract.Event.CANCEL, TelemetryContract.Method.BACK, mTelemetryExtra); - - setResult(getIntent().getIntExtra(EXTRA_RESULTCODE_NEGATIVE, -1)); - - return true; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/prompts/ColorPickerInput.java b/mobile/android/base/java/org/mozilla/gecko/prompts/ColorPickerInput.java deleted file mode 100644 index 3d66eeea89..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/prompts/ColorPickerInput.java +++ /dev/null @@ -1,59 +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.prompts; - -import org.json.JSONObject; -import org.mozilla.gecko.R; -import org.mozilla.gecko.widget.BasicColorPicker; - -import android.content.Context; -import android.graphics.Color; -import android.view.LayoutInflater; -import android.view.View; - -public class ColorPickerInput extends PromptInput { - public static final String INPUT_TYPE = "color"; - public static final String LOGTAG = "GeckoColorPickerInput"; - - private final boolean mShowAdvancedButton = true; - private final int mInitialColor; - - public ColorPickerInput(JSONObject obj) { - super(obj); - String init = obj.optString("value"); - mInitialColor = Color.rgb(Integer.parseInt(init.substring(1, 3), 16), - Integer.parseInt(init.substring(3, 5), 16), - Integer.parseInt(init.substring(5, 7), 16)); - } - - @Override - public View getView(Context context) throws UnsupportedOperationException { - LayoutInflater inflater = LayoutInflater.from(context); - mView = inflater.inflate(R.layout.basic_color_picker_dialog, null); - - BasicColorPicker cp = (BasicColorPicker) mView.findViewById(R.id.colorpicker); - cp.setColor(mInitialColor); - - return mView; - } - - @Override - public Object getValue() { - BasicColorPicker cp = (BasicColorPicker) mView.findViewById(R.id.colorpicker); - int color = cp.getColor(); - return "#" + Integer.toHexString(color).substring(2); - } - - @Override - public boolean getScrollable() { - return true; - } - - @Override - public boolean canApplyInputStyle() { - return false; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/prompts/IconGridInput.java b/mobile/android/base/java/org/mozilla/gecko/prompts/IconGridInput.java deleted file mode 100644 index bc7d7ac206..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/prompts/IconGridInput.java +++ /dev/null @@ -1,171 +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.prompts; - -import java.util.ArrayList; -import java.util.List; - -import org.json.JSONArray; -import org.json.JSONObject; -import org.mozilla.gecko.GeckoAppShell; -import org.mozilla.gecko.R; -import org.mozilla.gecko.util.ResourceDrawableUtils; - -import android.content.Context; -import android.graphics.drawable.Drawable; -import android.text.TextUtils; -import android.view.Display; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.view.WindowManager; -import android.widget.AdapterView; -import android.widget.AdapterView.OnItemClickListener; -import android.widget.ArrayAdapter; -import android.widget.GridView; -import android.widget.ImageView; -import android.widget.TextView; - -public class IconGridInput extends PromptInput implements OnItemClickListener { - public static final String INPUT_TYPE = "icongrid"; - public static final String LOGTAG = "GeckoIconGridInput"; - - private ArrayAdapter mAdapter; // An adapter holding a list of items to show in the grid - - private static int mColumnWidth = -1; // The maximum width of columns - private static int mMaxColumns = -1; // The maximum number of columns to show - private static int mIconSize = -1; // Size of icons in the grid - private int mSelected; // Current selection - private final JSONArray mArray; - - public IconGridInput(JSONObject obj) { - super(obj); - mArray = obj.optJSONArray("items"); - } - - @Override - public View getView(Context context) throws UnsupportedOperationException { - if (mColumnWidth < 0) { - // getColumnWidth isn't available on pre-ICS, so we pull it out and assign it here - mColumnWidth = context.getResources().getDimensionPixelSize(R.dimen.icongrid_columnwidth); - } - - if (mIconSize < 0) { - mIconSize = GeckoAppShell.getPreferredIconSize(); - } - - if (mMaxColumns < 0) { - mMaxColumns = context.getResources().getInteger(R.integer.max_icon_grid_columns); - } - - // TODO: Dynamically handle size changes - final WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); - final Display display = wm.getDefaultDisplay(); - final int screenWidth = display.getWidth(); - int maxColumns = Math.min(mMaxColumns, screenWidth / mColumnWidth); - - final GridView view = (GridView) LayoutInflater.from(context).inflate(R.layout.icon_grid, null, false); - view.setColumnWidth(mColumnWidth); - - final ArrayList items = new ArrayList(mArray.length()); - for (int i = 0; i < mArray.length(); i++) { - IconGridItem item = new IconGridItem(context, mArray.optJSONObject(i)); - items.add(item); - if (item.selected) { - mSelected = i; - } - } - - view.setNumColumns(Math.min(items.size(), maxColumns)); - view.setOnItemClickListener(this); - // Despite what the docs say, setItemChecked was not moved into the AbsListView class until sometime between - // Android 2.3.7 and Android 4.0.3. For other versions the item won't be visually highlighted, BUT we really only - // mSelected will still be set so that we default to its behavior. - if (mSelected > -1) { - view.setItemChecked(mSelected, true); - } - - mAdapter = new IconGridAdapter(context, -1, items); - view.setAdapter(mAdapter); - mView = view; - return mView; - } - - @Override - public void onItemClick(AdapterView parent, View view, int position, long id) { - mSelected = position; - notifyListeners(Integer.toString(position)); - } - - @Override - public Object getValue() { - return mSelected; - } - - @Override - public boolean getScrollable() { - return true; - } - - private class IconGridAdapter extends ArrayAdapter { - public IconGridAdapter(Context context, int resource, List items) { - super(context, resource, items); - } - - @Override - public View getView(int position, View convert, ViewGroup parent) { - final Context context = parent.getContext(); - if (convert == null) { - convert = LayoutInflater.from(context).inflate(R.layout.icon_grid_item, parent, false); - } - bindView(convert, context, position); - return convert; - } - - private void bindView(View v, Context c, int position) { - final IconGridItem item = getItem(position); - final TextView text1 = (TextView) v.findViewById(android.R.id.text1); - text1.setText(item.label); - - final TextView text2 = (TextView) v.findViewById(android.R.id.text2); - if (TextUtils.isEmpty(item.description)) { - text2.setVisibility(View.GONE); - } else { - text2.setVisibility(View.VISIBLE); - text2.setText(item.description); - } - - final ImageView icon = (ImageView) v.findViewById(R.id.icon); - icon.setImageDrawable(item.icon); - ViewGroup.LayoutParams lp = icon.getLayoutParams(); - lp.width = lp.height = mIconSize; - } - } - - private class IconGridItem { - final String label; - final String description; - final boolean selected; - Drawable icon; - - public IconGridItem(final Context context, final JSONObject obj) { - label = obj.optString("name"); - final String iconUrl = obj.optString("iconUri"); - description = obj.optString("description"); - selected = obj.optBoolean("selected"); - - ResourceDrawableUtils.getDrawable(context, iconUrl, new ResourceDrawableUtils.BitmapLoader() { - @Override - public void onBitmapFound(Drawable d) { - icon = d; - if (mAdapter != null) { - mAdapter.notifyDataSetChanged(); - } - } - }); - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/prompts/IntentChooserPrompt.java b/mobile/android/base/java/org/mozilla/gecko/prompts/IntentChooserPrompt.java deleted file mode 100644 index 502f1156d9..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/prompts/IntentChooserPrompt.java +++ /dev/null @@ -1,158 +0,0 @@ -/* 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.prompts; - -import org.mozilla.gecko.GeckoAppShell; -import org.mozilla.gecko.util.ThreadUtils; -import org.mozilla.gecko.widget.GeckoActionProvider; - -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.content.pm.ResolveInfo; -import android.widget.ListView; -import android.util.Log; - -import org.json.JSONException; -import org.json.JSONObject; - -import java.util.ArrayList; -import java.util.List; - -/** - * Shows a prompt letting the user pick from a list of intent handlers for a set of Intents or - * for a GeckoActionProvider. Basic usage: - * IntentChooserPrompt prompt = new IntentChooserPrompt(context, new Intent[] { - * ... // some intents - * }); - * prompt.show("Title", context, new IntentHandler() { - * public void onIntentSelected(Intent intent, int position) { } - * public void onCancelled() { } - * }); - **/ -public class IntentChooserPrompt { - private static final String LOGTAG = "GeckoIntentChooser"; - - private final ArrayList mItems; - - public IntentChooserPrompt(Context context, Intent[] intents) { - mItems = getItems(context, intents); - } - - public IntentChooserPrompt(Context context, GeckoActionProvider provider) { - mItems = getItems(context, provider); - } - - /* If an IntentHandler is passed in, will asynchronously call the handler when the dialog is closed - * Otherwise, will return the Intent that was chosen by the user. Must be called on the UI thread. - */ - public void show(final String title, final Context context, final IntentHandler handler) { - ThreadUtils.assertOnUiThread(); - - if (mItems.isEmpty()) { - Log.i(LOGTAG, "No activities for the intent chooser!"); - handler.onCancelled(); - return; - } - - // If there's only one item in the intent list, just return it - if (mItems.size() == 1) { - handler.onIntentSelected(mItems.get(0).getIntent(), 0); - return; - } - - final Prompt prompt = new Prompt(context, new Prompt.PromptCallback() { - @Override - public void onPromptFinished(String promptServiceResult) { - if (handler == null) { - return; - } - - int itemId = -1; - try { - itemId = new JSONObject(promptServiceResult).getInt("button"); - } catch (JSONException e) { - Log.e(LOGTAG, "result from promptservice was invalid: ", e); - } - - if (itemId == -1) { - handler.onCancelled(); - } else { - handler.onIntentSelected(mItems.get(itemId).getIntent(), itemId); - } - } - }); - - PromptListItem[] arrays = new PromptListItem[mItems.size()]; - mItems.toArray(arrays); - prompt.show(title, "", arrays, ListView.CHOICE_MODE_NONE); - - return; - } - - // Whether or not any activities were found. Useful for checking if you should try a different Intent set - public boolean hasActivities(Context context) { - return mItems.isEmpty(); - } - - // Gets a list of PromptListItems for an Intent array - private ArrayList getItems(final Context context, Intent[] intents) { - final ArrayList items = new ArrayList(); - - // If we have intents, use them to build the initial list - for (final Intent intent : intents) { - items.addAll(getItemsForIntent(context, intent)); - } - - return items; - } - - // Gets a list of PromptListItems for a GeckoActionProvider - private ArrayList getItems(final Context context, final GeckoActionProvider provider) { - final ArrayList items = new ArrayList(); - - // Add any intents from the provider. - final PackageManager packageManager = context.getPackageManager(); - final ArrayList infos = provider.getSortedActivities(); - - for (final ResolveInfo info : infos) { - items.add(getItemForResolveInfo(info, packageManager, provider.getIntent())); - } - - return items; - } - - private PromptListItem getItemForResolveInfo(ResolveInfo info, PackageManager pm, Intent intent) { - PromptListItem item = new PromptListItem(info.loadLabel(pm).toString()); - item.setIcon(info.loadIcon(pm)); - - Intent i = new Intent(intent); - // These intents should be implicit. - i.setComponent(new ComponentName(info.activityInfo.applicationInfo.packageName, - info.activityInfo.name)); - item.setIntent(new Intent(i)); - - return item; - } - - private ArrayList getItemsForIntent(Context context, Intent intent) { - ArrayList items = new ArrayList(); - PackageManager pm = context.getPackageManager(); - List lri = pm.queryIntentActivityOptions(GeckoAppShell.getGeckoInterface().getActivity().getComponentName(), null, intent, 0); - - // If we didn't find any activities, just return the empty list - if (lri == null) { - return items; - } - - // Otherwise, convert the ResolveInfo. Note we don't currently check for duplicates here. - for (ResolveInfo ri : lri) { - items.add(getItemForResolveInfo(ri, pm, intent)); - } - - return items; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/prompts/IntentHandler.java b/mobile/android/base/java/org/mozilla/gecko/prompts/IntentHandler.java deleted file mode 100644 index 1509ab6264..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/prompts/IntentHandler.java +++ /dev/null @@ -1,12 +0,0 @@ -/* 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.prompts; - -import android.content.Intent; - -public interface IntentHandler { - public void onIntentSelected(Intent intent, int position); - public void onCancelled(); -} diff --git a/mobile/android/base/java/org/mozilla/gecko/prompts/Prompt.java b/mobile/android/base/java/org/mozilla/gecko/prompts/Prompt.java deleted file mode 100644 index 11121b2cce..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/prompts/Prompt.java +++ /dev/null @@ -1,586 +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.prompts; - -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; -import org.mozilla.gecko.GeckoAppShell; -import org.mozilla.gecko.R; -import org.mozilla.gecko.gfx.LayerView; -import org.mozilla.gecko.util.ThreadUtils; -import org.mozilla.gecko.Tab; -import org.mozilla.gecko.Tabs; - -import android.app.AlertDialog; -import android.content.Context; -import android.content.DialogInterface; -import android.content.DialogInterface.OnCancelListener; -import android.content.DialogInterface.OnClickListener; -import android.content.res.Resources; -import android.text.TextUtils; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.View; -import android.widget.AdapterView; -import android.widget.AdapterView.OnItemClickListener; -import android.widget.LinearLayout; -import android.widget.ListView; -import android.widget.ScrollView; - -import java.util.ArrayList; - -public class Prompt implements OnClickListener, OnCancelListener, OnItemClickListener, - PromptInput.OnChangeListener, Tabs.OnTabsChangedListener { - private static final String LOGTAG = "GeckoPromptService"; - - private String[] mButtons; - private PromptInput[] mInputs; - private AlertDialog mDialog; - private int mDoubleTapButtonType; - - private final LayoutInflater mInflater; - private final Context mContext; - private PromptCallback mCallback; - private String mGuid; - private PromptListAdapter mAdapter; - - private static boolean mInitialized; - private static int mInputPaddingSize; - - private int mTabId = Tabs.INVALID_TAB_ID; - private Object mPreviousInputValue = null; - - public Prompt(Context context, PromptCallback callback) { - this(context); - mCallback = callback; - } - - private Prompt(Context context) { - mContext = context; - mInflater = LayoutInflater.from(mContext); - - if (!mInitialized) { - Resources res = mContext.getResources(); - mInputPaddingSize = (int) (res.getDimension(R.dimen.prompt_service_inputs_padding)); - mInitialized = true; - } - } - - private View applyInputStyle(View view, PromptInput input) { - // Don't add padding to color picker views - if (input.canApplyInputStyle()) { - view.setPadding(mInputPaddingSize, 0, mInputPaddingSize, 0); - } - return view; - } - - public void show(JSONObject message) { - String title = message.optString("title"); - String text = message.optString("text"); - mGuid = message.optString("guid"); - - mButtons = getStringArray(message, "buttons"); - final int buttonCount = mButtons == null ? 0 : mButtons.length; - mDoubleTapButtonType = convertIndexToButtonType(message.optInt("doubleTapButton", -1), buttonCount); - mPreviousInputValue = null; - - JSONArray inputs = getSafeArray(message, "inputs"); - mInputs = new PromptInput[inputs.length()]; - for (int i = 0; i < mInputs.length; i++) { - try { - mInputs[i] = PromptInput.getInput(inputs.getJSONObject(i)); - mInputs[i].setListener(this); - } catch (Exception ex) { } - } - - PromptListItem[] menuitems = PromptListItem.getArray(message.optJSONArray("listitems")); - String selected = message.optString("choiceMode"); - - int choiceMode = ListView.CHOICE_MODE_NONE; - if ("single".equals(selected)) { - choiceMode = ListView.CHOICE_MODE_SINGLE; - } else if ("multiple".equals(selected)) { - choiceMode = ListView.CHOICE_MODE_MULTIPLE; - } - - if (message.has("tabId")) { - mTabId = message.optInt("tabId", Tabs.INVALID_TAB_ID); - } - - show(title, text, menuitems, choiceMode); - } - - private int convertIndexToButtonType(final int buttonIndex, final int buttonCount) { - if (buttonIndex < 0 || buttonIndex >= buttonCount) { - // All valid DialogInterface button values are < 0, - // so we return 0 as an invalid value. - return 0; - } - - switch (buttonIndex) { - case 0: - return DialogInterface.BUTTON_POSITIVE; - case 1: - return DialogInterface.BUTTON_NEUTRAL; - case 2: - return DialogInterface.BUTTON_NEGATIVE; - default: - return 0; - } - } - - public void show(String title, String text, PromptListItem[] listItems, int choiceMode) { - ThreadUtils.assertOnUiThread(); - - try { - create(title, text, listItems, choiceMode); - } catch (IllegalStateException ex) { - Log.i(LOGTAG, "Error building dialog", ex); - return; - } - - if (mTabId != Tabs.INVALID_TAB_ID) { - Tabs.registerOnTabsChangedListener(this); - - final Tab tab = Tabs.getInstance().getTab(mTabId); - if (Tabs.getInstance().getSelectedTab() == tab) { - mDialog.show(); - } - } else { - mDialog.show(); - } - } - - @Override - public void onTabChanged(final Tab tab, final Tabs.TabEvents msg, final String data) { - if (tab != Tabs.getInstance().getTab(mTabId)) { - return; - } - - switch (msg) { - case SELECTED: - Log.i(LOGTAG, "Selected"); - mDialog.show(); - break; - case UNSELECTED: - Log.i(LOGTAG, "Unselected"); - mDialog.hide(); - break; - case LOCATION_CHANGE: - Log.i(LOGTAG, "Location change"); - mDialog.cancel(); - break; - } - } - - private void create(String title, String text, PromptListItem[] listItems, int choiceMode) - throws IllegalStateException { - - AlertDialog.Builder builder = new AlertDialog.Builder(mContext); - if (!TextUtils.isEmpty(title)) { - // Long strings can delay showing the dialog, so we cap the number of characters shown to 256. - builder.setTitle(title.substring(0, Math.min(title.length(), 256))); - } - - if (!TextUtils.isEmpty(text)) { - builder.setMessage(text); - } - - // Because lists are currently added through the normal Android AlertBuilder interface, they're - // incompatible with also adding additional input elements to a dialog. - if (listItems != null && listItems.length > 0) { - addListItems(builder, listItems, choiceMode); - } else if (!addInputs(builder)) { - throw new IllegalStateException("Could not add inputs to dialog"); - } - - int length = mButtons == null ? 0 : mButtons.length; - if (length > 0) { - builder.setPositiveButton(mButtons[0], this); - if (length > 1) { - builder.setNeutralButton(mButtons[1], this); - if (length > 2) { - builder.setNegativeButton(mButtons[2], this); - } - } - } - - mDialog = builder.create(); - mDialog.setOnCancelListener(Prompt.this); - } - - public void setButtons(String[] buttons) { - mButtons = buttons; - } - - public void setInputs(PromptInput[] inputs) { - mInputs = inputs; - } - - /* Adds to a result value from the lists that can be shown in dialogs. - * Will set the selected value(s) to the button attribute of the - * object that's passed in. If this is a multi-select dialog, sets a - * selected attribute to an array of booleans. - */ - private void addListResult(final JSONObject result, int which) { - if (mAdapter == null) { - return; - } - - try { - JSONArray selected = new JSONArray(); - - // If the button has already been filled in - ArrayList selectedItems = mAdapter.getSelected(); - for (Integer item : selectedItems) { - selected.put(item); - } - - // If we haven't assigned a button yet, or we assigned it to -1, assign the which - // parameter to both selected and the button. - if (!result.has("button") || result.optInt("button") == -1) { - if (!selectedItems.contains(which)) { - selected.put(which); - } - - result.put("button", which); - } - - result.put("list", selected); - } catch (JSONException ex) { } - } - - /* Adds to a result value from the inputs that can be shown in dialogs. - * Each input will set its own value in the result. - */ - private void addInputValues(final JSONObject result) { - try { - if (mInputs != null) { - for (int i = 0; i < mInputs.length; i++) { - if (mInputs[i] != null) { - result.put(mInputs[i].getId(), mInputs[i].getValue()); - } - } - } - } catch (JSONException ex) { } - } - - /* Adds the selected button to a result. This should only be called if there - * are no lists shown on the dialog, since they also write their results to the button - * attribute. - */ - private void addButtonResult(final JSONObject result, int which) { - int button = -1; - switch (which) { - case DialogInterface.BUTTON_POSITIVE : button = 0; break; - case DialogInterface.BUTTON_NEUTRAL : button = 1; break; - case DialogInterface.BUTTON_NEGATIVE : button = 2; break; - } - try { - result.put("button", button); - } catch (JSONException ex) { } - } - - @Override - public void onClick(DialogInterface dialog, int which) { - ThreadUtils.assertOnUiThread(); - closeDialog(which); - } - - /* Adds a set of list items to the prompt. This can be used for either context menu type dialogs, checked lists, - * or multiple selection lists. - * - * @param builder - * The alert builder currently building this dialog. - * @param listItems - * The items to add. - * @param choiceMode - * One of the ListView.CHOICE_MODE constants to designate whether this list shows checkmarks, radios buttons, or nothing. - */ - private void addListItems(AlertDialog.Builder builder, PromptListItem[] listItems, int choiceMode) { - switch (choiceMode) { - case ListView.CHOICE_MODE_MULTIPLE_MODAL: - case ListView.CHOICE_MODE_MULTIPLE: - addMultiSelectList(builder, listItems); - break; - case ListView.CHOICE_MODE_SINGLE: - addSingleSelectList(builder, listItems); - break; - case ListView.CHOICE_MODE_NONE: - default: - addMenuList(builder, listItems); - } - } - - /* Shows a multi-select list with checkmarks on the side. Android doesn't support using an adapter for - * multi-choice lists by default so instead we insert our own custom list so that we can do fancy things - * to the rows like disabling/indenting them. - * - * @param builder - * The alert builder currently building this dialog. - * @param listItems - * The items to add. - */ - private void addMultiSelectList(AlertDialog.Builder builder, PromptListItem[] listItems) { - ListView listView = (ListView) mInflater.inflate(R.layout.select_dialog_list, null); - listView.setOnItemClickListener(this); - listView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE); - - mAdapter = new PromptListAdapter(mContext, R.layout.select_dialog_multichoice, listItems); - listView.setAdapter(mAdapter); - builder.setView(listView); - } - - /* Shows a single-select list with radio boxes on the side. - * - * @param builder - * the alert builder currently building this dialog. - * @param listItems - * The items to add. - */ - private void addSingleSelectList(AlertDialog.Builder builder, PromptListItem[] listItems) { - mAdapter = new PromptListAdapter(mContext, R.layout.select_dialog_singlechoice, listItems); - builder.setSingleChoiceItems(mAdapter, mAdapter.getSelectedIndex(), new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - // The adapter isn't aware of single vs. multi choice lists, so manually - // clear any other selected items first. - ArrayList selected = mAdapter.getSelected(); - for (Integer sel : selected) { - mAdapter.toggleSelected(sel); - } - - // Now select this item. - mAdapter.toggleSelected(which); - closeIfNoButtons(which); - } - }); - } - - /* Shows a single-select list. - * - * @param builder - * the alert builder currently building this dialog. - * @param listItems - * The items to add. - */ - private void addMenuList(AlertDialog.Builder builder, PromptListItem[] listItems) { - mAdapter = new PromptListAdapter(mContext, android.R.layout.simple_list_item_1, listItems); - builder.setAdapter(mAdapter, this); - } - - /* Wraps an input in a linearlayout. We do this so that we can set padding that appears outside the background - * drawable for the view. - */ - private View wrapInput(final PromptInput input) { - final LinearLayout linearLayout = new LinearLayout(mContext); - linearLayout.setOrientation(LinearLayout.VERTICAL); - applyInputStyle(linearLayout, input); - - linearLayout.addView(input.getView(mContext)); - - return linearLayout; - } - - /* Add the requested input elements to the dialog. - * - * @param builder - * the alert builder currently building this dialog. - * @return - * return true if the inputs were added successfully. This may fail - * if the requested input is compatible with this Android version. - */ - private boolean addInputs(AlertDialog.Builder builder) { - int length = mInputs == null ? 0 : mInputs.length; - if (length == 0) { - return true; - } - - try { - View root = null; - boolean scrollable = false; // If any of the inputs are scrollable, we won't wrap this in a ScrollView - - if (length == 1) { - root = wrapInput(mInputs[0]); - scrollable |= mInputs[0].getScrollable(); - } else if (length > 1) { - LinearLayout linearLayout = new LinearLayout(mContext); - linearLayout.setOrientation(LinearLayout.VERTICAL); - for (int i = 0; i < length; i++) { - View content = wrapInput(mInputs[i]); - linearLayout.addView(content); - scrollable |= mInputs[i].getScrollable(); - } - root = linearLayout; - } - - if (scrollable) { - // If we're showing some sort of scrollable list, force an inverse background. - builder.setInverseBackgroundForced(true); - builder.setView(root); - } else { - ScrollView view = new ScrollView(mContext); - view.addView(root); - builder.setView(view); - } - } catch (Exception ex) { - Log.e(LOGTAG, "Error showing prompt inputs", ex); - // We cannot display these input widgets with this sdk version, - // do not display any dialog and finish the prompt now. - cancelDialog(); - return false; - } - - return true; - } - - /* AdapterView.OnItemClickListener - * Called when a list item is clicked - */ - @Override - public void onItemClick(AdapterView parent, View view, int position, long id) { - ThreadUtils.assertOnUiThread(); - mAdapter.toggleSelected(position); - - // If there are no buttons on this dialog, then we take selecting an item as a sign to close - // the dialog. Note that means it will be hard to select multiple things in this list, but - // given there is no way to confirm+close the dialog, it seems reasonable. - closeIfNoButtons(position); - } - - private boolean closeIfNoButtons(int selected) { - ThreadUtils.assertOnUiThread(); - if (mButtons == null || mButtons.length == 0) { - closeDialog(selected); - return true; - } - return false; - } - - /* @DialogInterface.OnCancelListener - * Called when the user hits back to cancel a dialog. The dialog will close itself when this - * ends. Setup the correct return values here. - * - * @param aDialog - * A dialog interface for the dialog that's being closed. - */ - @Override - public void onCancel(DialogInterface aDialog) { - ThreadUtils.assertOnUiThread(); - cancelDialog(); - } - - /* Called in situations where we want to cancel the dialog . This can happen if the user hits back, - * or if the dialog can't be created because of invalid JSON. - */ - private void cancelDialog() { - JSONObject ret = new JSONObject(); - try { - ret.put("button", -1); - } catch (Exception ex) { } - addInputValues(ret); - notifyClosing(ret); - } - - /* Called any time we're closing the dialog to cleanup and notify listeners that the dialog - * is closing. - */ - private void closeDialog(int which) { - JSONObject ret = new JSONObject(); - mDialog.dismiss(); - - addButtonResult(ret, which); - addListResult(ret, which); - addInputValues(ret); - - notifyClosing(ret); - } - - /* Called any time we're closing the dialog to cleanup and notify listeners that the dialog - * is closing. - */ - private void notifyClosing(JSONObject aReturn) { - try { - aReturn.put("guid", mGuid); - } catch (JSONException ex) { } - - if (mTabId != Tabs.INVALID_TAB_ID) { - Tabs.unregisterOnTabsChangedListener(this); - } - - if (mCallback != null) { - mCallback.onPromptFinished(aReturn.toString()); - } - } - - // Called when the prompt inputs on the dialog change - @Override - public void onChange(PromptInput input) { - // If there are no buttons on this dialog, assuming that "changing" an input - // means something was selected and we can close. This provides a way to tap - // on a list item and close the dialog automatically. - if (!closeIfNoButtons(-1)) { - // Alternatively, if a default button has been specified for double tapping, - // we want to close the dialog if the same input value has been transmitted - // twice in a row. - closeIfDoubleTapEnabled(input.getValue()); - } - } - - private boolean closeIfDoubleTapEnabled(Object inputValue) { - if (mDoubleTapButtonType != 0 && inputValue == mPreviousInputValue) { - closeDialog(mDoubleTapButtonType); - return true; - } - mPreviousInputValue = inputValue; - return false; - } - - private static JSONArray getSafeArray(JSONObject json, String key) { - try { - return json.getJSONArray(key); - } catch (Exception e) { - return new JSONArray(); - } - } - - public static String[] getStringArray(JSONObject aObject, String aName) { - JSONArray items = getSafeArray(aObject, aName); - int length = items.length(); - String[] list = new String[length]; - for (int i = 0; i < length; i++) { - try { - list[i] = items.getString(i); - } catch (Exception ex) { } - } - return list; - } - - private static boolean[] getBooleanArray(JSONObject aObject, String aName) { - JSONArray items = new JSONArray(); - try { - items = aObject.getJSONArray(aName); - } catch (Exception ex) { return null; } - int length = items.length(); - boolean[] list = new boolean[length]; - for (int i = 0; i < length; i++) { - try { - list[i] = items.getBoolean(i); - } catch (Exception ex) { } - } - return list; - } - - public interface PromptCallback { - - /** - * Called when the Prompt has been completed (i.e. when the user has selected an item or action in the Prompt). - * This callback is run on the UI thread. - */ - public void onPromptFinished(String jsonResult); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/prompts/PromptInput.java b/mobile/android/base/java/org/mozilla/gecko/prompts/PromptInput.java deleted file mode 100644 index 752f5c24cf..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/prompts/PromptInput.java +++ /dev/null @@ -1,398 +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.prompts; - -import java.text.SimpleDateFormat; -import java.util.Calendar; -import java.util.GregorianCalendar; - -import org.json.JSONObject; -import org.mozilla.gecko.AppConstants.Versions; -import org.mozilla.gecko.widget.AllCapsTextView; -import org.mozilla.gecko.widget.DateTimePicker; - -import android.content.Context; -import android.content.res.Configuration; -import android.support.design.widget.TextInputLayout; -import android.support.v7.widget.AppCompatCheckBox; -import android.text.Html; -import android.text.InputType; -import android.text.TextUtils; -import android.text.format.DateFormat; -import android.util.Log; -import android.view.View; -import android.view.ViewGroup.LayoutParams; -import android.view.inputmethod.InputMethodManager; -import android.widget.ArrayAdapter; -import android.widget.CheckBox; -import android.widget.DatePicker; -import android.widget.EditText; -import android.widget.LinearLayout; -import android.widget.Spinner; -import android.widget.TextView; -import android.widget.TimePicker; - -public abstract class PromptInput { - protected final String mLabel; - protected final String mType; - protected final String mId; - protected final String mValue; - protected final String mMinValue; - protected final String mMaxValue; - protected OnChangeListener mListener; - protected View mView; - public static final String LOGTAG = "GeckoPromptInput"; - - public interface OnChangeListener { - void onChange(PromptInput input); - } - - public void setListener(OnChangeListener listener) { - mListener = listener; - } - - public static class EditInput extends PromptInput { - protected final String mHint; - protected final boolean mAutofocus; - public static final String INPUT_TYPE = "textbox"; - - public EditInput(JSONObject object) { - super(object); - mHint = object.optString("hint"); - mAutofocus = object.optBoolean("autofocus"); - } - - @Override - public View getView(final Context context) throws UnsupportedOperationException { - EditText input = new EditText(context); - input.setInputType(InputType.TYPE_CLASS_TEXT); - input.setText(mValue); - - if (!TextUtils.isEmpty(mHint)) { - input.setHint(mHint); - } - - if (mAutofocus) { - input.setOnFocusChangeListener(new View.OnFocusChangeListener() { - @Override - public void onFocusChange(View v, boolean hasFocus) { - if (hasFocus) { - ((InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE)).showSoftInput(v, 0); - } - } - }); - input.requestFocus(); - } - - TextInputLayout inputLayout = new TextInputLayout(context); - inputLayout.addView(input); - - mView = (View) inputLayout; - return mView; - } - - @Override - public Object getValue() { - final TextInputLayout inputLayout = (TextInputLayout) mView; - return inputLayout.getEditText().getText(); - } - } - - public static class NumberInput extends EditInput { - public static final String INPUT_TYPE = "number"; - public NumberInput(JSONObject obj) { - super(obj); - } - - @Override - public View getView(final Context context) throws UnsupportedOperationException { - final TextInputLayout inputLayout = (TextInputLayout) super.getView(context); - final EditText input = inputLayout.getEditText(); - input.setRawInputType(Configuration.KEYBOARD_12KEY); - input.setInputType(InputType.TYPE_CLASS_NUMBER | - InputType.TYPE_NUMBER_FLAG_SIGNED); - return input; - } - } - - public static class PasswordInput extends EditInput { - public static final String INPUT_TYPE = "password"; - public PasswordInput(JSONObject obj) { - super(obj); - } - - @Override - public View getView(Context context) throws UnsupportedOperationException { - final TextInputLayout inputLayout = (TextInputLayout) super.getView(context); - inputLayout.getEditText().setInputType(InputType.TYPE_CLASS_TEXT | - InputType.TYPE_TEXT_VARIATION_PASSWORD | - InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS); - return inputLayout; - } - } - - public static class CheckboxInput extends PromptInput { - public static final String INPUT_TYPE = "checkbox"; - private final boolean mChecked; - - public CheckboxInput(JSONObject obj) { - super(obj); - mChecked = obj.optBoolean("checked"); - } - - @Override - public View getView(Context context) throws UnsupportedOperationException { - final CheckBox checkbox = new AppCompatCheckBox(context); - checkbox.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); - checkbox.setText(mLabel); - checkbox.setChecked(mChecked); - mView = (View)checkbox; - return mView; - } - - @Override - public Object getValue() { - CheckBox checkbox = (CheckBox)mView; - return checkbox.isChecked() ? Boolean.TRUE : Boolean.FALSE; - } - } - - public static class DateTimeInput extends PromptInput { - public static final String[] INPUT_TYPES = new String[] { - "date", - "week", - "time", - "datetime-local", - "datetime", - "month" - }; - - public DateTimeInput(JSONObject obj) { - super(obj); - } - - @Override - public View getView(Context context) throws UnsupportedOperationException { - if (mType.equals("date")) { - try { - DateTimePicker input = new DateTimePicker(context, "yyyy-MM-dd", mValue, - DateTimePicker.PickersState.DATE, mMinValue, mMaxValue); - input.toggleCalendar(true); - mView = (View)input; - } catch (UnsupportedOperationException ex) { - // We can't use our custom version of the DatePicker widget because the sdk is too old. - // But we can fallback on the native one. - DatePicker input = new DatePicker(context); - try { - if (!TextUtils.isEmpty(mValue)) { - GregorianCalendar calendar = new GregorianCalendar(); - calendar.setTime(new SimpleDateFormat("yyyy-MM-dd").parse(mValue)); - input.updateDate(calendar.get(Calendar.YEAR), - calendar.get(Calendar.MONTH), - calendar.get(Calendar.DAY_OF_MONTH)); - } - } catch (Exception e) { - Log.e(LOGTAG, "error parsing format string: " + e); - } - mView = (View)input; - } - } else if (mType.equals("week")) { - DateTimePicker input = new DateTimePicker(context, "yyyy-'W'ww", mValue, - DateTimePicker.PickersState.WEEK, mMinValue, mMaxValue); - mView = (View)input; - } else if (mType.equals("time")) { - TimePicker input = new TimePicker(context); - input.setIs24HourView(DateFormat.is24HourFormat(context)); - - GregorianCalendar calendar = new GregorianCalendar(); - if (!TextUtils.isEmpty(mValue)) { - try { - calendar.setTime(new SimpleDateFormat("HH:mm").parse(mValue)); - } catch (Exception e) { } - } - input.setCurrentHour(calendar.get(GregorianCalendar.HOUR_OF_DAY)); - input.setCurrentMinute(calendar.get(GregorianCalendar.MINUTE)); - mView = (View)input; - } else if (mType.equals("datetime-local") || mType.equals("datetime")) { - DateTimePicker input = new DateTimePicker(context, "yyyy-MM-dd HH:mm", mValue.replace("T", " ").replace("Z", ""), - DateTimePicker.PickersState.DATETIME, - mMinValue.replace("T", " ").replace("Z", ""), mMaxValue.replace("T", " ").replace("Z", "")); - input.toggleCalendar(true); - mView = (View)input; - } else if (mType.equals("month")) { - DateTimePicker input = new DateTimePicker(context, "yyyy-MM", mValue, - DateTimePicker.PickersState.MONTH, mMinValue, mMaxValue); - mView = (View)input; - } - return mView; - } - - private static String formatDateString(String dateFormat, Calendar calendar) { - return new SimpleDateFormat(dateFormat).format(calendar.getTime()); - } - - @Override - public Object getValue() { - if (mType.equals("time")) { - TimePicker tp = (TimePicker)mView; - GregorianCalendar calendar = - new GregorianCalendar(0, 0, 0, tp.getCurrentHour(), tp.getCurrentMinute()); - return formatDateString("HH:mm", calendar); - } else { - DateTimePicker dp = (DateTimePicker)mView; - GregorianCalendar calendar = new GregorianCalendar(); - calendar.setTimeInMillis(dp.getTimeInMillis()); - if (mType.equals("date")) { - return formatDateString("yyyy-MM-dd", calendar); - } else if (mType.equals("week")) { - return formatDateString("yyyy-'W'ww", calendar); - } else if (mType.equals("datetime-local")) { - return formatDateString("yyyy-MM-dd'T'HH:mm", calendar); - } else if (mType.equals("datetime")) { - calendar.set(GregorianCalendar.ZONE_OFFSET, 0); - calendar.setTimeInMillis(dp.getTimeInMillis()); - return formatDateString("yyyy-MM-dd'T'HH:mm'Z'", calendar); - } else if (mType.equals("month")) { - return formatDateString("yyyy-MM", calendar); - } - } - return super.getValue(); - } - } - - public static class MenulistInput extends PromptInput { - public static final String INPUT_TYPE = "menulist"; - private static String[] mListitems; - private static int mSelected; - - public Spinner spinner; - public AllCapsTextView textView; - - public MenulistInput(JSONObject obj) { - super(obj); - mListitems = Prompt.getStringArray(obj, "values"); - mSelected = obj.optInt("selected"); - } - - @Override - public View getView(final Context context) throws UnsupportedOperationException { - spinner = new Spinner(context, Spinner.MODE_DIALOG); - try { - if (mListitems.length > 0) { - ArrayAdapter adapter = new ArrayAdapter(context, android.R.layout.simple_spinner_item, mListitems); - adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); - - spinner.setAdapter(adapter); - spinner.setSelection(mSelected); - } - } catch (Exception ex) { - } - - if (!TextUtils.isEmpty(mLabel)) { - LinearLayout container = new LinearLayout(context); - container.setOrientation(LinearLayout.VERTICAL); - - textView = new AllCapsTextView(context, null); - textView.setText(mLabel); - container.addView(textView); - - container.addView(spinner); - return container; - } - - return spinner; - } - - @Override - public Object getValue() { - return spinner.getSelectedItemPosition(); - } - } - - public static class LabelInput extends PromptInput { - public static final String INPUT_TYPE = "label"; - public LabelInput(JSONObject obj) { - super(obj); - } - - @Override - public View getView(Context context) throws UnsupportedOperationException { - // not really an input, but a way to add labels and such to the dialog - TextView view = new TextView(context); - view.setText(Html.fromHtml(mLabel)); - mView = view; - return mView; - } - } - - public PromptInput(JSONObject obj) { - mLabel = obj.optString("label"); - mType = obj.optString("type"); - String id = obj.optString("id"); - mId = TextUtils.isEmpty(id) ? mType : id; - mValue = obj.optString("value"); - mMaxValue = obj.optString("max"); - mMinValue = obj.optString("min"); - } - - public static PromptInput getInput(JSONObject obj) { - String type = obj.optString("type"); - switch (type) { - case EditInput.INPUT_TYPE: - return new EditInput(obj); - case NumberInput.INPUT_TYPE: - return new NumberInput(obj); - case PasswordInput.INPUT_TYPE: - return new PasswordInput(obj); - case CheckboxInput.INPUT_TYPE: - return new CheckboxInput(obj); - case MenulistInput.INPUT_TYPE: - return new MenulistInput(obj); - case LabelInput.INPUT_TYPE: - return new LabelInput(obj); - case IconGridInput.INPUT_TYPE: - return new IconGridInput(obj); - case ColorPickerInput.INPUT_TYPE: - return new ColorPickerInput(obj); - case TabInput.INPUT_TYPE: - return new TabInput(obj); - default: - for (String dtType : DateTimeInput.INPUT_TYPES) { - if (dtType.equals(type)) { - return new DateTimeInput(obj); - } - } - - break; - } - - return null; - } - - public abstract View getView(Context context) throws UnsupportedOperationException; - - public String getId() { - return mId; - } - - public Object getValue() { - return null; - } - - public boolean getScrollable() { - return false; - } - - public boolean canApplyInputStyle() { - return true; - } - - protected void notifyListeners(String val) { - if (mListener != null) { - mListener.onChange(this); - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/prompts/PromptListAdapter.java b/mobile/android/base/java/org/mozilla/gecko/prompts/PromptListAdapter.java deleted file mode 100644 index 720086c92f..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/prompts/PromptListAdapter.java +++ /dev/null @@ -1,281 +0,0 @@ -package org.mozilla.gecko.prompts; - -import org.mozilla.gecko.R; -import org.mozilla.gecko.Telemetry; -import org.mozilla.gecko.TelemetryContract; -import org.mozilla.gecko.menu.MenuItemSwitcherLayout; -import org.mozilla.gecko.widget.GeckoActionProvider; - -import android.content.Context; -import android.content.Intent; -import android.content.res.Resources; -import android.graphics.drawable.Drawable; -import android.graphics.Bitmap; -import android.graphics.drawable.BitmapDrawable; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.CheckedTextView; -import android.widget.TextView; -import android.widget.ListView; -import android.widget.ArrayAdapter; -import android.util.TypedValue; - -import java.util.ArrayList; - -public class PromptListAdapter extends ArrayAdapter { - private static final int VIEW_TYPE_ITEM = 0; - private static final int VIEW_TYPE_GROUP = 1; - private static final int VIEW_TYPE_ACTIONS = 2; - private static final int VIEW_TYPE_COUNT = 3; - - private static final String LOGTAG = "GeckoPromptListAdapter"; - - private final int mResourceId; - private Drawable mBlankDrawable; - private Drawable mMoreDrawable; - private static int mGroupPaddingSize; - private static int mLeftRightTextWithIconPadding; - private static int mTopBottomTextWithIconPadding; - private static int mIconSize; - private static int mMinRowSize; - private static int mIconTextPadding; - private static float mTextSize; - private static boolean mInitialized; - - PromptListAdapter(Context context, int textViewResourceId, PromptListItem[] objects) { - super(context, textViewResourceId, objects); - mResourceId = textViewResourceId; - init(); - } - - private void init() { - if (!mInitialized) { - Resources res = getContext().getResources(); - mGroupPaddingSize = (int) (res.getDimension(R.dimen.prompt_service_group_padding_size)); - mLeftRightTextWithIconPadding = (int) (res.getDimension(R.dimen.prompt_service_left_right_text_with_icon_padding)); - mTopBottomTextWithIconPadding = (int) (res.getDimension(R.dimen.prompt_service_top_bottom_text_with_icon_padding)); - mIconTextPadding = (int) (res.getDimension(R.dimen.prompt_service_icon_text_padding)); - mIconSize = (int) (res.getDimension(R.dimen.prompt_service_icon_size)); - mMinRowSize = (int) (res.getDimension(R.dimen.menu_item_row_height)); - mTextSize = res.getDimension(R.dimen.menu_item_textsize); - - mInitialized = true; - } - } - - @Override - public int getItemViewType(int position) { - PromptListItem item = getItem(position); - if (item.isGroup) { - return VIEW_TYPE_GROUP; - } else if (item.showAsActions) { - return VIEW_TYPE_ACTIONS; - } else { - return VIEW_TYPE_ITEM; - } - } - - @Override - public int getViewTypeCount() { - return VIEW_TYPE_COUNT; - } - - private Drawable getMoreDrawable(Resources res) { - if (mMoreDrawable == null) { - mMoreDrawable = res.getDrawable(R.drawable.menu_item_more); - } - return mMoreDrawable; - } - - private Drawable getBlankDrawable(Resources res) { - if (mBlankDrawable == null) { - mBlankDrawable = res.getDrawable(R.drawable.blank); - } - return mBlankDrawable; - } - - public void toggleSelected(int position) { - PromptListItem item = getItem(position); - item.setSelected(!item.getSelected()); - } - - private void maybeUpdateIcon(PromptListItem item, TextView t) { - if (item.getIcon() == null && !item.inGroup && !item.isParent) { - t.setCompoundDrawablesWithIntrinsicBounds(null, null, null, null); - return; - } - - Drawable d = null; - Resources res = getContext().getResources(); - // Set the padding between the icon and the text. - t.setCompoundDrawablePadding(mIconTextPadding); - if (item.getIcon() != null) { - // We want the icon to be of a specific size. Some do not - // follow this rule so we have to resize them. - Bitmap bitmap = ((BitmapDrawable) item.getIcon()).getBitmap(); - d = new BitmapDrawable(res, Bitmap.createScaledBitmap(bitmap, mIconSize, mIconSize, true)); - } else if (item.inGroup) { - // We don't currently support "indenting" items with icons - d = getBlankDrawable(res); - } - - Drawable moreDrawable = null; - if (item.isParent) { - moreDrawable = getMoreDrawable(res); - } - - if (d != null || moreDrawable != null) { - t.setCompoundDrawablesWithIntrinsicBounds(d, null, moreDrawable, null); - } - } - - private void maybeUpdateCheckedState(ListView list, int position, PromptListItem item, ViewHolder viewHolder) { - viewHolder.textView.setEnabled(!item.disabled && !item.isGroup); - viewHolder.textView.setClickable(item.isGroup || item.disabled); - if (viewHolder.textView instanceof CheckedTextView) { - // Apparently just using ct.setChecked(true) doesn't work, so this - // is stolen from the android source code as a way to set the checked - // state of these items - list.setItemChecked(position, item.getSelected()); - } - } - - boolean isSelected(int position) { - return getItem(position).getSelected(); - } - - ArrayList getSelected() { - int length = getCount(); - - ArrayList selected = new ArrayList(); - for (int i = 0; i < length; i++) { - if (isSelected(i)) { - selected.add(i); - } - } - - return selected; - } - - int getSelectedIndex() { - int length = getCount(); - for (int i = 0; i < length; i++) { - if (isSelected(i)) { - return i; - } - } - return -1; - } - - private View getActionView(PromptListItem item, final ListView list, final int position) { - final GeckoActionProvider provider = GeckoActionProvider.getForType(item.getIntent().getType(), getContext()); - provider.setIntent(item.getIntent()); - - final MenuItemSwitcherLayout view = (MenuItemSwitcherLayout) provider.onCreateActionView( - GeckoActionProvider.ActionViewType.CONTEXT_MENU); - // If a quickshare button is clicked, we need to close the dialog. - view.addActionButtonClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - ListView.OnItemClickListener listener = list.getOnItemClickListener(); - if (listener != null) { - listener.onItemClick(list, view, position, position); - } - } - }); - - return view; - } - - private void updateActionView(final PromptListItem item, final MenuItemSwitcherLayout view, final ListView list, final int position) { - view.setTitle(item.label); - view.setIcon(item.getIcon()); - view.setSubMenuIndicator(item.isParent); - - // If the share button is clicked, we need to close the dialog and then show an intent chooser - view.setMenuItemClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - ListView.OnItemClickListener listener = list.getOnItemClickListener(); - if (listener != null) { - listener.onItemClick(list, view, position, position); - } - - final GeckoActionProvider provider = GeckoActionProvider.getForType(item.getIntent().getType(), getContext()); - IntentChooserPrompt prompt = new IntentChooserPrompt(getContext(), provider); - prompt.show(item.label, getContext(), new IntentHandler() { - @Override - public void onIntentSelected(final Intent intent, final int p) { - provider.chooseActivity(p); - - // Context: Sharing via content contextmenu list (no explicit session is active) - Telemetry.sendUIEvent(TelemetryContract.Event.SHARE, TelemetryContract.Method.LIST, "promptlist"); - } - - @Override - public void onCancelled() { - // do nothing - } - }); - } - }); - } - - @Override - public View getView(int position, View convertView, ViewGroup parent) { - PromptListItem item = getItem(position); - int type = getItemViewType(position); - ViewHolder viewHolder = null; - - if (convertView == null) { - if (type == VIEW_TYPE_ACTIONS) { - convertView = getActionView(item, (ListView) parent, position); - } else { - int resourceId = mResourceId; - if (item.isGroup) { - resourceId = R.layout.list_item_header; - } - - LayoutInflater mInflater = LayoutInflater.from(getContext()); - convertView = mInflater.inflate(resourceId, null); - convertView.setMinimumHeight(mMinRowSize); - - TextView tv = (TextView) convertView.findViewById(android.R.id.text1); - tv.setTextSize(TypedValue.COMPLEX_UNIT_PX, mTextSize); - viewHolder = new ViewHolder(tv, tv.getPaddingLeft(), tv.getPaddingRight(), - tv.getPaddingTop(), tv.getPaddingBottom()); - - convertView.setTag(viewHolder); - } - } else { - viewHolder = (ViewHolder) convertView.getTag(); - } - - if (type == VIEW_TYPE_ACTIONS) { - updateActionView(item, (MenuItemSwitcherLayout) convertView, (ListView) parent, position); - } else { - viewHolder.textView.setText(item.label); - maybeUpdateCheckedState((ListView) parent, position, item, viewHolder); - maybeUpdateIcon(item, viewHolder.textView); - } - - return convertView; - } - - private static class ViewHolder { - public final TextView textView; - public final int paddingLeft; - public final int paddingRight; - public final int paddingTop; - public final int paddingBottom; - - ViewHolder(TextView aTextView, int aLeft, int aRight, int aTop, int aBottom) { - textView = aTextView; - paddingLeft = aLeft; - paddingRight = aRight; - paddingTop = aTop; - paddingBottom = aBottom; - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/prompts/PromptListItem.java b/mobile/android/base/java/org/mozilla/gecko/prompts/PromptListItem.java deleted file mode 100644 index 48ace735c1..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/prompts/PromptListItem.java +++ /dev/null @@ -1,128 +0,0 @@ -package org.mozilla.gecko.prompts; - -import org.json.JSONException; -import org.mozilla.gecko.IntentHelper; -import org.mozilla.gecko.GeckoAppShell; -import org.mozilla.gecko.ThumbnailHelper; -import org.mozilla.gecko.util.ResourceDrawableUtils; -import org.mozilla.gecko.widget.GeckoActionProvider; - -import org.json.JSONArray; -import org.json.JSONObject; - -import android.content.Context; -import android.content.Intent; -import android.graphics.drawable.Drawable; - -import java.util.List; -import java.util.ArrayList; - -// This class should die and be replaced with normal menu items -public class PromptListItem { - private static final String LOGTAG = "GeckoPromptListItem"; - public final String label; - public final boolean isGroup; - public final boolean inGroup; - public final boolean disabled; - public final int id; - public final boolean showAsActions; - public final boolean isParent; - - public Intent mIntent; - public boolean mSelected; - public Drawable mIcon; - - PromptListItem(JSONObject aObject) { - Context context = GeckoAppShell.getContext(); - label = aObject.isNull("label") ? "" : aObject.optString("label"); - isGroup = aObject.optBoolean("isGroup"); - inGroup = aObject.optBoolean("inGroup"); - disabled = aObject.optBoolean("disabled"); - id = aObject.optInt("id"); - mSelected = aObject.optBoolean("selected"); - - JSONObject obj = aObject.optJSONObject("showAsActions"); - if (obj != null) { - showAsActions = true; - String uri = obj.isNull("uri") ? "" : obj.optString("uri"); - String type = obj.isNull("type") ? GeckoActionProvider.DEFAULT_MIME_TYPE : - obj.optString("type", GeckoActionProvider.DEFAULT_MIME_TYPE); - - mIntent = IntentHelper.getShareIntent(context, uri, type, ""); - isParent = true; - } else { - mIntent = null; - showAsActions = false; - // Support both "isParent" (backwards compat for older consumers), and "menu" for the new Tabbed prompt ui. - isParent = aObject.optBoolean("isParent") || aObject.optBoolean("menu"); - } - - final String iconStr = aObject.optString("icon"); - if (iconStr != null) { - final ResourceDrawableUtils.BitmapLoader loader = new ResourceDrawableUtils.BitmapLoader() { - @Override - public void onBitmapFound(Drawable d) { - mIcon = d; - } - }; - - if (iconStr.startsWith("thumbnail:")) { - final int id = Integer.parseInt(iconStr.substring(10), 10); - ThumbnailHelper.getInstance().getAndProcessThumbnailFor(id, loader); - } else { - ResourceDrawableUtils.getDrawable(context, iconStr, loader); - } - } - } - - public void setIntent(Intent i) { - mIntent = i; - } - - public Intent getIntent() { - return mIntent; - } - - public void setIcon(Drawable icon) { - mIcon = icon; - } - - public Drawable getIcon() { - return mIcon; - } - - public void setSelected(boolean selected) { - mSelected = selected; - } - - public boolean getSelected() { - return mSelected; - } - - public PromptListItem(String aLabel) { - label = aLabel; - isGroup = false; - inGroup = false; - isParent = false; - disabled = false; - id = 0; - showAsActions = false; - } - - static PromptListItem[] getArray(JSONArray items) { - if (items == null) { - return new PromptListItem[0]; - } - - int length = items.length(); - List list = new ArrayList<>(length); - for (int i = 0; i < length; i++) { - try { - PromptListItem item = new PromptListItem(items.getJSONObject(i)); - list.add(item); - } catch (JSONException ex) { } - } - - return list.toArray(new PromptListItem[length]); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/prompts/PromptService.java b/mobile/android/base/java/org/mozilla/gecko/prompts/PromptService.java deleted file mode 100644 index 8155cc1c64..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/prompts/PromptService.java +++ /dev/null @@ -1,72 +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.prompts; - -import org.json.JSONException; -import org.json.JSONObject; -import org.mozilla.gecko.EventDispatcher; -import org.mozilla.gecko.GeckoApp; -import org.mozilla.gecko.GeckoAppShell; -import org.mozilla.gecko.util.GeckoEventListener; -import org.mozilla.gecko.util.ThreadUtils; - -import android.content.Context; -import android.util.Log; - -public class PromptService implements GeckoEventListener { - private static final String LOGTAG = "GeckoPromptService"; - - private final Context mContext; - - public PromptService(Context context) { - GeckoApp.getEventDispatcher().registerGeckoThreadListener(this, - "Prompt:Show", - "Prompt:ShowTop"); - mContext = context; - } - - public void destroy() { - GeckoApp.getEventDispatcher().unregisterGeckoThreadListener(this, - "Prompt:Show", - "Prompt:ShowTop"); - } - - public void show(final String aTitle, final String aText, final PromptListItem[] aMenuList, - final int aChoiceMode, final Prompt.PromptCallback callback) { - // The dialog must be created on the UI thread. - ThreadUtils.postToUiThread(new Runnable() { - @Override - public void run() { - Prompt p; - p = new Prompt(mContext, callback); - p.show(aTitle, aText, aMenuList, aChoiceMode); - } - }); - } - - // GeckoEventListener implementation - @Override - public void handleMessage(String event, final JSONObject message) { - // The dialog must be created on the UI thread. - ThreadUtils.postToUiThread(new Runnable() { - @Override - public void run() { - Prompt p; - p = new Prompt(mContext, new Prompt.PromptCallback() { - @Override - public void onPromptFinished(String jsonResult) { - try { - EventDispatcher.sendResponse(message, new JSONObject(jsonResult)); - } catch (JSONException ex) { - Log.i(LOGTAG, "Error building json response", ex); - } - } - }); - p.show(message); - } - }); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/prompts/TabInput.java b/mobile/android/base/java/org/mozilla/gecko/prompts/TabInput.java deleted file mode 100644 index ab490e79cf..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/prompts/TabInput.java +++ /dev/null @@ -1,107 +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.prompts; - -import java.util.LinkedHashMap; - -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; -import org.mozilla.gecko.AppConstants.Versions; -import org.mozilla.gecko.R; -import org.mozilla.gecko.util.ThreadUtils; - -import android.content.Context; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.View; -import android.widget.AdapterView; -import android.widget.ListView; -import android.widget.TabHost; -import android.widget.TextView; - -public class TabInput extends PromptInput implements AdapterView.OnItemClickListener { - public static final String INPUT_TYPE = "tabs"; - public static final String LOGTAG = "GeckoTabInput"; - - /* Keeping the order of this in sync with the JSON is important. */ - final private LinkedHashMap mTabs; - - private TabHost mHost; - private int mPosition; - - public TabInput(JSONObject obj) { - super(obj); - mTabs = new LinkedHashMap(); - try { - JSONArray tabs = obj.getJSONArray("items"); - for (int i = 0; i < tabs.length(); i++) { - JSONObject tab = tabs.getJSONObject(i); - String title = tab.getString("label"); - JSONArray items = tab.getJSONArray("items"); - mTabs.put(title, PromptListItem.getArray(items)); - } - } catch (JSONException ex) { - Log.e(LOGTAG, "Exception", ex); - } - } - - @Override - public View getView(final Context context) throws UnsupportedOperationException { - final LayoutInflater inflater = LayoutInflater.from(context); - mHost = (TabHost) inflater.inflate(R.layout.tab_prompt_input, null); - mHost.setup(); - - for (String title : mTabs.keySet()) { - final TabHost.TabSpec spec = mHost.newTabSpec(title); - spec.setContent(new TabHost.TabContentFactory() { - @Override - public View createTabContent(final String tag) { - PromptListAdapter adapter = new PromptListAdapter(context, android.R.layout.simple_list_item_1, mTabs.get(tag)); - ListView listView = new ListView(context); - listView.setCacheColorHint(0); - listView.setOnItemClickListener(TabInput.this); - listView.setAdapter(adapter); - return listView; - } - }); - - spec.setIndicator(title); - mHost.addTab(spec); - } - mView = mHost; - return mHost; - } - - @Override - public Object getValue() { - JSONObject obj = new JSONObject(); - try { - obj.put("tab", mHost.getCurrentTab()); - obj.put("item", mPosition); - } catch (JSONException ex) { } - - return obj; - } - - @Override - public boolean getScrollable() { - return true; - } - - @Override - public boolean canApplyInputStyle() { - return false; - } - - @Override - public void onItemClick(AdapterView parent, View view, int position, long id) { - ThreadUtils.assertOnUiThread(); - mPosition = position; - notifyListeners(Integer.toString(position)); - } - -} diff --git a/mobile/android/base/java/org/mozilla/gecko/push/Fetched.java b/mobile/android/base/java/org/mozilla/gecko/push/Fetched.java deleted file mode 100644 index 42a7c6a90b..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/push/Fetched.java +++ /dev/null @@ -1,71 +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.push; - -import android.support.annotation.NonNull; -import org.json.JSONException; -import org.json.JSONObject; - -/** - * Pair a (String) value with a timestamp. The timestamp is usually when the - * value was fetched from a remote service or when the value was locally - * generated. - * - * It's awkward to serialize generic values to JSON -- that requires lots of - * factory classes -- so we specialize to String instances. - */ -public class Fetched { - public final String value; - public final long timestamp; - - public Fetched(String value, long timestamp) { - this.value = value; - this.timestamp = timestamp; - } - - public static Fetched now(String value) { - return new Fetched(value, System.currentTimeMillis()); - } - - public static @NonNull Fetched fromJSONObject(@NonNull JSONObject json) { - final String value = json.optString("value", null); - final String timestampString = json.optString("timestamp", null); - final long timestamp = timestampString != null ? Long.valueOf(timestampString) : 0L; - return new Fetched(value, timestamp); - } - - public JSONObject toJSONObject() throws JSONException { - final JSONObject jsonObject = new JSONObject(); - if (value != null) { - jsonObject.put("value", value); - } else { - jsonObject.remove("value"); - } - jsonObject.put("timestamp", Long.toString(timestamp)); - return jsonObject; - } - - @Override - public boolean equals(Object o) { - // Auto-generated. - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - Fetched fetched = (Fetched) o; - - if (timestamp != fetched.timestamp) return false; - return !(value != null ? !value.equals(fetched.value) : fetched.value != null); - - } - - @Override - public int hashCode() { - // Auto-generated. - int result = value != null ? value.hashCode() : 0; - result = 31 * result + (int) (timestamp ^ (timestamp >>> 32)); - return result; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/push/PushClient.java b/mobile/android/base/java/org/mozilla/gecko/push/PushClient.java deleted file mode 100644 index 9c1fab5f99..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/push/PushClient.java +++ /dev/null @@ -1,110 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.push; - -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; - -import org.mozilla.gecko.push.RegisterUserAgentResponse; -import org.mozilla.gecko.push.SubscribeChannelResponse; -import org.mozilla.gecko.push.autopush.AutopushClient; -import org.mozilla.gecko.push.autopush.AutopushClientException; -import org.mozilla.gecko.sync.Utils; - -import java.util.concurrent.Executor; - -/** - * This class bridges the autopush client, which is written in callback style, with the Fennec - * push implementation, which is written in a linear style. It handles returning results and - * re-throwing exceptions passed as messages. - *

- * TODO: fold this into the autopush client directly. - */ -public class PushClient { - public static class LocalException extends Exception { - private static final long serialVersionUID = 2387554736L; - - public LocalException(Throwable throwable) { - super(throwable); - } - } - - private final AutopushClient autopushClient; - - public PushClient(String serverURI) { - this.autopushClient = new AutopushClient(serverURI, Utils.newSynchronousExecutor()); - } - - /** - * Each instance is single-use! Exactly one delegate method should be invoked once, - * but we take care to handle multiple invocations (favoring the earliest), just to be safe. - */ - protected static class Delegate implements AutopushClient.RequestDelegate { - Object result; // Oh, for an algebraic data type when you need one! - - @SuppressWarnings("unchecked") - public T responseOrThrow() throws LocalException, AutopushClientException { - if (result instanceof LocalException) { - throw (LocalException) result; - } - if (result instanceof AutopushClientException) { - throw (AutopushClientException) result; - } - return (T) result; - } - - @Override - public void handleError(Exception e) { - if (result == null) { - result = new LocalException(e); - } - } - - @Override - public void handleFailure(AutopushClientException e) { - if (result == null) { - result = e; - } - } - - @Override - public void handleSuccess(T response) { - if (result == null) { - result = response; - } - } - } - - public RegisterUserAgentResponse registerUserAgent(@NonNull String token) throws LocalException, AutopushClientException { - final Delegate delegate = new Delegate<>(); - autopushClient.registerUserAgent(token, delegate); - return delegate.responseOrThrow(); - } - - public void reregisterUserAgent(@NonNull String uaid, @NonNull String secret, @NonNull String token) throws LocalException, AutopushClientException { - final Delegate delegate = new Delegate<>(); - autopushClient.reregisterUserAgent(uaid, secret, token, delegate); - delegate.responseOrThrow(); // For side-effects only. - } - - public void unregisterUserAgent(@NonNull String uaid, @NonNull String secret) throws LocalException, AutopushClientException { - final Delegate delegate = new Delegate<>(); - autopushClient.unregisterUserAgent(uaid, secret, delegate); - delegate.responseOrThrow(); // For side-effects only. - } - - public SubscribeChannelResponse subscribeChannel(@NonNull String uaid, @NonNull String secret, @Nullable String appServerKey) throws LocalException, AutopushClientException { - final Delegate delegate = new Delegate<>(); - autopushClient.subscribeChannel(uaid, secret, appServerKey, delegate); - return delegate.responseOrThrow(); - } - - public void unsubscribeChannel(@NonNull String uaid, @NonNull String secret, @NonNull String chid) throws LocalException, AutopushClientException { - final Delegate delegate = new Delegate<>(); - autopushClient.unsubscribeChannel(uaid, secret, chid, delegate); - delegate.responseOrThrow(); // For side-effects only. - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/push/PushManager.java b/mobile/android/base/java/org/mozilla/gecko/push/PushManager.java deleted file mode 100644 index 42ef60b619..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/push/PushManager.java +++ /dev/null @@ -1,354 +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.push; - -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.util.Log; - -import org.json.JSONObject; -import org.mozilla.gecko.AppConstants; -import org.mozilla.gecko.gcm.GcmTokenClient; -import org.mozilla.gecko.push.autopush.AutopushClientException; -import org.mozilla.gecko.util.ThreadUtils; - -import java.io.IOException; -import java.util.Collections; -import java.util.HashMap; -import java.util.Iterator; -import java.util.Map; - -/** - * The push manager advances push registrations, ensuring that the upstream autopush endpoint has - * a fresh GCM token. It brokers channel subscription requests to the upstream and maintains - * local state. - *

- * This class is not thread safe. An individual instance should be accessed on a single - * (background) thread. - */ -public class PushManager { - public static final long TIME_BETWEEN_AUTOPUSH_UAID_REGISTRATION_IN_MILLIS = 7 * 24 * 60 * 60 * 1000L; // One week. - - public static class ProfileNeedsConfigurationException extends Exception { - private static final long serialVersionUID = 3326738888L; - - public ProfileNeedsConfigurationException() { - super(); - } - } - - private static final String LOG_TAG = "GeckoPushManager"; - - protected final @NonNull PushState state; - protected final @NonNull GcmTokenClient gcmClient; - protected final @NonNull PushClientFactory pushClientFactory; - - // For testing only. - public interface PushClientFactory { - PushClient getPushClient(String autopushEndpoint, boolean debug); - } - - public PushManager(@NonNull PushState state, @NonNull GcmTokenClient gcmClient, @NonNull PushClientFactory pushClientFactory) { - this.state = state; - this.gcmClient = gcmClient; - this.pushClientFactory = pushClientFactory; - } - - public PushRegistration registrationForSubscription(String chid) { - // chids are globally unique, so we're not concerned about finding a chid associated to - // any particular profile. - for (Map.Entry entry : state.getRegistrations().entrySet()) { - final PushSubscription subscription = entry.getValue().getSubscription(chid); - if (subscription != null) { - return entry.getValue(); - } - } - return null; - } - - public Map allSubscriptionsForProfile(String profileName) { - final PushRegistration registration = state.getRegistration(profileName); - if (registration == null) { - return Collections.emptyMap(); - } - return Collections.unmodifiableMap(registration.subscriptions); - } - - public PushRegistration registerUserAgent(final @NonNull String profileName, final long now) throws ProfileNeedsConfigurationException, AutopushClientException, PushClient.LocalException, GcmTokenClient.NeedsGooglePlayServicesException, IOException { - Log.i(LOG_TAG, "Registering user agent for profile named: " + profileName); - return advanceRegistration(profileName, now); - } - - public PushRegistration unregisterUserAgent(final @NonNull String profileName, final long now) throws ProfileNeedsConfigurationException { - Log.i(LOG_TAG, "Unregistering user agent for profile named: " + profileName); - - final PushRegistration registration = state.getRegistration(profileName); - if (registration == null) { - Log.w(LOG_TAG, "Cannot find registration corresponding to subscription; not unregistering remote uaid for profileName: " + profileName); - return null; - } - - final String uaid = registration.uaid.value; - final String secret = registration.secret; - if (uaid == null || secret == null) { - Log.e(LOG_TAG, "Cannot unregisterUserAgent with null registration uaid or secret!"); - return null; - } - - unregisterUserAgentOnBackgroundThread(registration); - return registration; - } - - public PushSubscription subscribeChannel(final @NonNull String profileName, final @NonNull String service, final @NonNull JSONObject serviceData, @Nullable String appServerKey, final long now) throws ProfileNeedsConfigurationException, AutopushClientException, PushClient.LocalException, GcmTokenClient.NeedsGooglePlayServicesException, IOException { - Log.i(LOG_TAG, "Subscribing to channel for service: " + service + "; for profile named: " + profileName); - final PushRegistration registration = advanceRegistration(profileName, now); - final PushSubscription subscription = subscribeChannel(registration, profileName, service, serviceData, appServerKey, System.currentTimeMillis()); - return subscription; - } - - protected PushSubscription subscribeChannel(final @NonNull PushRegistration registration, final @NonNull String profileName, final @NonNull String service, final @NonNull JSONObject serviceData, @Nullable String appServerKey, final long now) throws AutopushClientException, PushClient.LocalException { - final String uaid = registration.uaid.value; - final String secret = registration.secret; - if (uaid == null || secret == null) { - throw new IllegalStateException("Cannot subscribeChannel with null uaid or secret!"); - } - - // Verify endpoint is not null? - final PushClient pushClient = pushClientFactory.getPushClient(registration.autopushEndpoint, registration.debug); - - final SubscribeChannelResponse result = pushClient.subscribeChannel(uaid, secret, appServerKey); - if (registration.debug) { - Log.i(LOG_TAG, "Got chid: " + result.channelID + " and endpoint: " + result.endpoint); - } else { - Log.i(LOG_TAG, "Got chid and endpoint."); - } - - final PushSubscription subscription = new PushSubscription(result.channelID, profileName, result.endpoint, service, serviceData); - registration.putSubscription(result.channelID, subscription); - state.checkpoint(); - - return subscription; - } - - public PushSubscription unsubscribeChannel(final @NonNull String chid) { - Log.i(LOG_TAG, "Unsubscribing from channel with chid: " + chid); - - final PushRegistration registration = registrationForSubscription(chid); - if (registration == null) { - Log.w(LOG_TAG, "Cannot find registration corresponding to subscription; not unregistering remote subscription: " + chid); - return null; - } - - // We remove the local subscription before the remote subscription: without the local - // subscription we'll ignoring incoming messages, and after some amount of time the - // server will expire the channel due to non-activity. This is also Desktop's approach. - final PushSubscription subscription = registration.removeSubscription(chid); - state.checkpoint(); - - if (subscription == null) { - // This should never happen. - Log.e(LOG_TAG, "Subscription did not exist: " + chid); - return null; - } - - final String uaid = registration.uaid.value; - final String secret = registration.secret; - if (uaid == null || secret == null) { - Log.e(LOG_TAG, "Cannot unsubscribeChannel with null registration uaid or secret!"); - return null; - } - - final PushClient pushClient = pushClientFactory.getPushClient(registration.autopushEndpoint, registration.debug); - // Fire and forget. - ThreadUtils.postToBackgroundThread(new Runnable() { - @Override - public void run() { - try { - pushClient.unsubscribeChannel(registration.uaid.value, registration.secret, chid); - Log.i(LOG_TAG, "Unsubscribed from channel with chid: " + chid); - } catch (PushClient.LocalException | AutopushClientException e) { - Log.w(LOG_TAG, "Failed to unsubscribe from channel with chid; ignoring: " + chid, e); - } - } - }); - - return subscription; - } - - public PushRegistration configure(final @NonNull String profileName, final @NonNull String endpoint, final boolean debug, final long now) { - Log.i(LOG_TAG, "Updating configuration."); - final PushRegistration registration = state.getRegistration(profileName); - final PushRegistration newRegistration; - if (registration != null) { - if (!endpoint.equals(registration.autopushEndpoint)) { - if (debug) { - Log.i(LOG_TAG, "Push configuration autopushEndpoint changed! Was: " + registration.autopushEndpoint + "; now: " + endpoint); - } else { - Log.i(LOG_TAG, "Push configuration autopushEndpoint changed!"); - } - - newRegistration = new PushRegistration(endpoint, debug, Fetched.now(null), null); - - if (registration.uaid.value != null) { - // New endpoint! All registrations and subscriptions have been dropped, and - // should be removed remotely. - unregisterUserAgentOnBackgroundThread(registration); - } - } else if (debug != registration.debug) { - Log.i(LOG_TAG, "Push configuration debug changed: " + debug); - newRegistration = registration.withDebug(debug); - } else { - newRegistration = registration; - } - } else { - if (debug) { - Log.i(LOG_TAG, "Push configuration set: " + endpoint + "; debug: " + debug); - } else { - Log.i(LOG_TAG, "Push configuration set!"); - } - newRegistration = new PushRegistration(endpoint, debug, new Fetched(null, now), null); - } - - if (newRegistration != registration) { - state.putRegistration(profileName, newRegistration); - state.checkpoint(); - } - - return newRegistration; - } - - private void unregisterUserAgentOnBackgroundThread(final PushRegistration registration) { - ThreadUtils.postToBackgroundThread(new Runnable() { - @Override - public void run() { - try { - pushClientFactory.getPushClient(registration.autopushEndpoint, registration.debug).unregisterUserAgent(registration.uaid.value, registration.secret); - Log.i(LOG_TAG, "Unregistered user agent with uaid: " + registration.uaid.value); - } catch (PushClient.LocalException | AutopushClientException e) { - Log.w(LOG_TAG, "Failed to unregister user agent with uaid; ignoring: " + registration.uaid.value, e); - } - } - }); - } - - protected @NonNull PushRegistration advanceRegistration(final @NonNull String profileName, final long now) throws ProfileNeedsConfigurationException, AutopushClientException, PushClient.LocalException, GcmTokenClient.NeedsGooglePlayServicesException, IOException { - final PushRegistration registration = state.getRegistration(profileName); - if (registration == null || registration.autopushEndpoint == null) { - Log.i(LOG_TAG, "Cannot advance to registered: registration needs configuration."); - throw new ProfileNeedsConfigurationException(); - } - return advanceRegistration(registration, profileName, now); - } - - protected @NonNull PushRegistration advanceRegistration(final PushRegistration registration, final @NonNull String profileName, final long now) throws AutopushClientException, PushClient.LocalException, GcmTokenClient.NeedsGooglePlayServicesException, IOException { - final Fetched gcmToken = gcmClient.getToken(AppConstants.MOZ_ANDROID_GCM_SENDERID, registration.debug); - - final PushClient pushClient = pushClientFactory.getPushClient(registration.autopushEndpoint, registration.debug); - - if (registration.uaid.value == null) { - if (registration.debug) { - Log.i(LOG_TAG, "No uaid; requesting from autopush endpoint: " + registration.autopushEndpoint); - } else { - Log.i(LOG_TAG, "No uaid: requesting from autopush endpoint."); - } - final RegisterUserAgentResponse result = pushClient.registerUserAgent(gcmToken.value); - if (registration.debug) { - Log.i(LOG_TAG, "Got uaid: " + result.uaid + " and secret: " + result.secret); - } else { - Log.i(LOG_TAG, "Got uaid and secret."); - } - final long nextNow = System.currentTimeMillis(); - final PushRegistration nextRegistration = registration.withUserAgentID(result.uaid, result.secret, nextNow); - state.putRegistration(profileName, nextRegistration); - state.checkpoint(); - return advanceRegistration(nextRegistration, profileName, nextNow); - } - - if (registration.uaid.timestamp + TIME_BETWEEN_AUTOPUSH_UAID_REGISTRATION_IN_MILLIS < now - || registration.uaid.timestamp < gcmToken.timestamp) { - if (registration.debug) { - Log.i(LOG_TAG, "Stale uaid; re-registering with autopush endpoint: " + registration.autopushEndpoint); - } else { - Log.i(LOG_TAG, "Stale uaid: re-registering with autopush endpoint."); - } - - pushClient.reregisterUserAgent(registration.uaid.value, registration.secret, gcmToken.value); - - Log.i(LOG_TAG, "Re-registered uaid and secret."); - final long nextNow = System.currentTimeMillis(); - final PushRegistration nextRegistration = registration.withUserAgentID(registration.uaid.value, registration.secret, nextNow); - state.putRegistration(profileName, nextRegistration); - state.checkpoint(); - return advanceRegistration(nextRegistration, profileName, nextNow); - } - - Log.d(LOG_TAG, "Existing uaid is fresh; no need to request from autopush endpoint."); - return registration; - } - - public void invalidateGcmToken() { - gcmClient.invalidateToken(); - } - - public void startup(long now) { - try { - Log.i(LOG_TAG, "Startup: requesting GCM token."); - gcmClient.getToken(AppConstants.MOZ_ANDROID_GCM_SENDERID, false); // For side-effects. - } catch (GcmTokenClient.NeedsGooglePlayServicesException e) { - // Requires user intervention. At App startup, we don't want to address this. In - // response to user activity, we do want to try to have the user address this. - Log.w(LOG_TAG, "Startup: needs Google Play Services. Ignoring until GCM is requested in response to user activity."); - return; - } catch (IOException e) { - // We're temporarily unable to get a GCM token. There's nothing to be done; we'll - // try to advance the App's state in response to user activity or at next startup. - Log.w(LOG_TAG, "Startup: Google Play Services is available, but we can't get a token; ignoring.", e); - return; - } - - Log.i(LOG_TAG, "Startup: advancing all registrations."); - final Map registrations = state.getRegistrations(); - - // Now advance all registrations. - try { - final Iterator> it = registrations.entrySet().iterator(); - while (it.hasNext()) { - final Map.Entry entry = it.next(); - final String profileName = entry.getKey(); - final PushRegistration registration = entry.getValue(); - if (registration.subscriptions.isEmpty()) { - Log.i(LOG_TAG, "Startup: no subscriptions for profileName; not advancing registration: " + profileName); - continue; - } - - try { - advanceRegistration(profileName, now); // For side-effects. - Log.i(LOG_TAG, "Startup: advanced registration for profileName: " + profileName); - } catch (ProfileNeedsConfigurationException e) { - Log.i(LOG_TAG, "Startup: cannot advance registration for profileName: " + profileName + "; profile needs configuration from Gecko."); - } catch (AutopushClientException e) { - if (e.isTransientError()) { - Log.w(LOG_TAG, "Startup: cannot advance registration for profileName: " + profileName + "; got transient autopush error. Ignoring; will advance on demand.", e); - } else { - Log.w(LOG_TAG, "Startup: cannot advance registration for profileName: " + profileName + "; got permanent autopush error. Removing registration entirely.", e); - it.remove(); - } - } catch (PushClient.LocalException e) { - Log.w(LOG_TAG, "Startup: cannot advance registration for profileName: " + profileName + "; got local exception. Ignoring; will advance on demand.", e); - } - } - } catch (GcmTokenClient.NeedsGooglePlayServicesException e) { - Log.w(LOG_TAG, "Startup: cannot advance any registrations; need Google Play Services!", e); - return; - } catch (IOException e) { - Log.w(LOG_TAG, "Startup: cannot advance any registrations; intermittent Google Play Services exception; ignoring, will advance on demand.", e); - return; - } - - // We may have removed registrations above. Checkpoint just to be safe! - state.checkpoint(); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/push/PushRegistration.java b/mobile/android/base/java/org/mozilla/gecko/push/PushRegistration.java deleted file mode 100644 index a991774ff1..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/push/PushRegistration.java +++ /dev/null @@ -1,126 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.push; - -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import org.json.JSONException; -import org.json.JSONObject; - -import java.util.HashMap; -import java.util.Iterator; -import java.util.Map; - -/** - * Represent an autopush User Agent registration. - *

- * Such a registration associates an endpoint, optional debug flag, some Google - * Cloud Messaging data, and the returned uaid and secret. - *

- * Each registration is associated to a single Gecko profile, although we don't - * enforce that here. This class is immutable, so it is by definition - * thread-safe. - */ -public class PushRegistration { - public final String autopushEndpoint; - public final boolean debug; - // TODO: fold (timestamp, {uaid, secret}) into this class. - public final @NonNull Fetched uaid; - public final String secret; - - protected final @NonNull Map subscriptions; - - public PushRegistration(String autopushEndpoint, boolean debug, @NonNull Fetched uaid, @Nullable String secret, @NonNull Map subscriptions) { - this.autopushEndpoint = autopushEndpoint; - this.debug = debug; - this.uaid = uaid; - this.secret = secret; - this.subscriptions = subscriptions; - } - - public PushRegistration(String autopushEndpoint, boolean debug, @NonNull Fetched uaid, @Nullable String secret) { - this(autopushEndpoint, debug, uaid, secret, new HashMap()); - } - - public JSONObject toJSONObject() throws JSONException { - final JSONObject subscriptions = new JSONObject(); - for (Map.Entry entry : this.subscriptions.entrySet()) { - subscriptions.put(entry.getKey(), entry.getValue().toJSONObject()); - } - - final JSONObject jsonObject = new JSONObject(); - jsonObject.put("autopushEndpoint", autopushEndpoint); - jsonObject.put("debug", debug); - jsonObject.put("uaid", uaid.toJSONObject()); - jsonObject.put("secret", secret); - jsonObject.put("subscriptions", subscriptions); - return jsonObject; - } - - public static PushRegistration fromJSONObject(@NonNull JSONObject registration) throws JSONException { - final String endpoint = registration.optString("autopushEndpoint", null); - final boolean debug = registration.getBoolean("debug"); - final Fetched uaid = Fetched.fromJSONObject(registration.getJSONObject("uaid")); - final String secret = registration.optString("secret", null); - - final JSONObject subscriptionsObject = registration.getJSONObject("subscriptions"); - final Map subscriptions = new HashMap<>(); - final Iterator it = subscriptionsObject.keys(); - while (it.hasNext()) { - final String chid = it.next(); - subscriptions.put(chid, PushSubscription.fromJSONObject(subscriptionsObject.getJSONObject(chid))); - } - - return new PushRegistration(endpoint, debug, uaid, secret, subscriptions); - } - - @Override - public boolean equals(Object o) { - // Auto-generated. - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - PushRegistration that = (PushRegistration) o; - - if (autopushEndpoint != null ? !autopushEndpoint.equals(that.autopushEndpoint) : that.autopushEndpoint != null) - return false; - if (!uaid.equals(that.uaid)) return false; - if (secret != null ? !secret.equals(that.secret) : that.secret != null) return false; - if (subscriptions != null ? !subscriptions.equals(that.subscriptions) : that.subscriptions != null) return false; - return (debug == that.debug); - } - - @Override - public int hashCode() { - // Auto-generated. - int result = autopushEndpoint != null ? autopushEndpoint.hashCode() : 0; - result = 31 * result + (debug ? 1 : 0); - result = 31 * result + uaid.hashCode(); - result = 31 * result + (secret != null ? secret.hashCode() : 0); - result = 31 * result + (subscriptions != null ? subscriptions.hashCode() : 0); - return result; - } - - public PushRegistration withDebug(boolean debug) { - return new PushRegistration(this.autopushEndpoint, debug, this.uaid, this.secret, this.subscriptions); - } - - public PushRegistration withUserAgentID(String uaid, String secret, long nextNow) { - return new PushRegistration(this.autopushEndpoint, this.debug, new Fetched(uaid, nextNow), secret, this.subscriptions); - } - - public PushSubscription getSubscription(@NonNull String chid) { - return subscriptions.get(chid); - } - - public PushSubscription putSubscription(@NonNull String chid, @NonNull PushSubscription subscription) { - return subscriptions.put(chid, subscription); - } - - public PushSubscription removeSubscription(@NonNull String chid) { - return subscriptions.remove(chid); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/push/PushService.java b/mobile/android/base/java/org/mozilla/gecko/push/PushService.java deleted file mode 100644 index 7c3a9434f2..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/push/PushService.java +++ /dev/null @@ -1,440 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.push; - -import android.content.Context; -import android.content.Intent; -import android.os.Bundle; -import android.support.annotation.NonNull; -import android.util.Log; - -import org.json.JSONException; -import org.json.JSONObject; -import org.mozilla.gecko.EventDispatcher; -import org.mozilla.gecko.GeckoAppShell; -import org.mozilla.gecko.GeckoProfile; -import org.mozilla.gecko.GeckoService; -import org.mozilla.gecko.GeckoThread; -import org.mozilla.gecko.Telemetry; -import org.mozilla.gecko.TelemetryContract; -import org.mozilla.gecko.annotation.ReflectionTarget; -import org.mozilla.gecko.db.BrowserDB; -import org.mozilla.gecko.gcm.GcmTokenClient; -import org.mozilla.gecko.push.autopush.AutopushClientException; -import org.mozilla.gecko.util.BundleEventListener; -import org.mozilla.gecko.util.EventCallback; -import org.mozilla.gecko.util.ThreadUtils; - -import java.io.File; -import java.io.IOException; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; - -/** - * Class that handles messages used in the Google Cloud Messaging and DOM push API integration. - *

- * This singleton services Gecko messages from dom/push/PushServiceAndroidGCM.jsm and Google Cloud - * Messaging requests. - *

- * It is expected that Gecko is started (if not already running) soon after receiving GCM messages - * otherwise there is a greater risk that pending messages that have not been handle by Gecko will - * be lost if this service is killed. - *

- * It's worth noting that we allow the DOM push API in restricted profiles. - */ -@ReflectionTarget -public class PushService implements BundleEventListener { - private static final String LOG_TAG = "GeckoPushService"; - - public static final String SERVICE_WEBPUSH = "webpush"; - public static final String SERVICE_FXA = "fxa"; - - private static PushService sInstance; - - private static final String[] GECKO_EVENTS = new String[] { - "PushServiceAndroidGCM:Configure", - "PushServiceAndroidGCM:DumpRegistration", - "PushServiceAndroidGCM:DumpSubscriptions", - "PushServiceAndroidGCM:Initialized", - "PushServiceAndroidGCM:Uninitialized", - "PushServiceAndroidGCM:RegisterUserAgent", - "PushServiceAndroidGCM:UnregisterUserAgent", - "PushServiceAndroidGCM:SubscribeChannel", - "PushServiceAndroidGCM:UnsubscribeChannel", - "History:GetPrePathLastVisitedTimeMilliseconds", - }; - - private enum GeckoComponent { - PushServiceAndroidGCM - } - - public static synchronized PushService getInstance(Context context) { - if (sInstance == null) { - onCreate(context); - } - return sInstance; - } - - @ReflectionTarget - public static synchronized void onCreate(Context context) { - if (sInstance != null) { - return; - } - sInstance = new PushService(context); - - sInstance.registerGeckoEventListener(); - sInstance.onStartup(); - } - - protected final PushManager pushManager; - - // NB: These are not thread-safe, we're depending on these being access from the same background thread. - private boolean isReadyPushServiceAndroidGCM = false; - private final List pendingPushMessages; - - public PushService(Context context) { - pushManager = new PushManager(new PushState(context, "GeckoPushState.json"), new GcmTokenClient(context), new PushManager.PushClientFactory() { - @Override - public PushClient getPushClient(String autopushEndpoint, boolean debug) { - return new PushClient(autopushEndpoint); - } - }); - - pendingPushMessages = new LinkedList<>(); - } - - public void onStartup() { - Log.i(LOG_TAG, "Starting up."); - ThreadUtils.assertOnBackgroundThread(); - - try { - pushManager.startup(System.currentTimeMillis()); - } catch (Exception e) { - Log.e(LOG_TAG, "Got exception during startup; ignoring.", e); - return; - } - } - - public void onRefresh() { - Log.i(LOG_TAG, "Google Play Services requested GCM token refresh; invalidating GCM token and running startup again."); - ThreadUtils.assertOnBackgroundThread(); - - pushManager.invalidateGcmToken(); - try { - pushManager.startup(System.currentTimeMillis()); - } catch (Exception e) { - Log.e(LOG_TAG, "Got exception during refresh; ignoring.", e); - return; - } - } - - public void onMessageReceived(final @NonNull Context context, final @NonNull Bundle bundle) { - Log.i(LOG_TAG, "Google Play Services GCM message received; delivering."); - ThreadUtils.assertOnBackgroundThread(); - - final String chid = bundle.getString("chid"); - if (chid == null) { - Log.w(LOG_TAG, "No chid found; ignoring message."); - return; - } - - final PushRegistration registration = pushManager.registrationForSubscription(chid); - if (registration == null) { - Log.w(LOG_TAG, "Cannot find registration corresponding to subscription for chid: " + chid + "; ignoring message."); - return; - } - - final PushSubscription subscription = registration.getSubscription(chid); - if (subscription == null) { - // This should never happen. There's not much to be done; in the future, perhaps we - // could try to drop the remote subscription? - Log.e(LOG_TAG, "No subscription found for chid: " + chid + "; ignoring message."); - return; - } - - boolean isWebPush = SERVICE_WEBPUSH.equals(subscription.service); - boolean isFxAPush = SERVICE_FXA.equals(subscription.service); - if (!isWebPush && !isFxAPush) { - Log.e(LOG_TAG, "Message directed to unknown service; dropping: " + subscription.service); - return; - } - - Log.i(LOG_TAG, "Message directed to service: " + subscription.service); - - if (subscription.serviceData == null) { - Log.e(LOG_TAG, "No serviceData found for chid: " + chid + "; ignoring dom/push message."); - return; - } - - Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.SERVICE, "dom-push-api"); - - final String profileName = subscription.serviceData.optString("profileName", null); - final String profilePath = subscription.serviceData.optString("profilePath", null); - if (profileName == null || profilePath == null) { - Log.e(LOG_TAG, "Corrupt serviceData found for chid: " + chid + "; ignoring dom/push message."); - return; - } - - if (canSendPushMessagesToGecko()) { - if (!GeckoThread.canUseProfile(profileName, new File(profilePath))) { - Log.e(LOG_TAG, "Mismatched profile for chid: " + chid + "; ignoring dom/push message."); - return; - } - } else { - final Intent intent = GeckoService.getIntentToCreateServices(context, "android-push-service"); - GeckoService.setIntentProfile(intent, profileName, profilePath); - context.startService(intent); - } - - final JSONObject data = new JSONObject(); - try { - data.put("channelID", chid); - data.put("con", bundle.getString("con")); - data.put("enc", bundle.getString("enc")); - // Only one of cryptokey (newer) and enckey (deprecated) should be set, but the - // Gecko handler will verify this. - data.put("cryptokey", bundle.getString("cryptokey")); - data.put("enckey", bundle.getString("enckey")); - data.put("message", bundle.getString("body")); - - if (!canSendPushMessagesToGecko()) { - data.put("profileName", profileName); - data.put("profilePath", profilePath); - data.put("service", subscription.service); - } - } catch (JSONException e) { - Log.e(LOG_TAG, "Got exception delivering dom/push message to Gecko!", e); - return; - } - - if (!canSendPushMessagesToGecko()) { - Log.i(LOG_TAG, "Required service not initialized, adding message to queue."); - pendingPushMessages.add(data); - return; - } - - if (isWebPush) { - sendMessageToGeckoService(data); - } else { - sendMessageToDecodeToGeckoService(data); - } - } - - protected static void sendMessageToGeckoService(final @NonNull JSONObject message) { - Log.i(LOG_TAG, "Delivering dom/push message to Gecko!"); - GeckoAppShell.notifyObservers("PushServiceAndroidGCM:ReceivedPushMessage", - message.toString(), - GeckoThread.State.PROFILE_READY); - } - - protected static void sendMessageToDecodeToGeckoService(final @NonNull JSONObject message) { - Log.i(LOG_TAG, "Delivering dom/push message to decode to Gecko!"); - } - - protected void registerGeckoEventListener() { - Log.d(LOG_TAG, "Registered Gecko event listener."); - EventDispatcher.getInstance().registerBackgroundThreadListener(this, GECKO_EVENTS); - } - - protected void unregisterGeckoEventListener() { - Log.d(LOG_TAG, "Unregistered Gecko event listener."); - EventDispatcher.getInstance().unregisterBackgroundThreadListener(this, GECKO_EVENTS); - } - - @Override - public void handleMessage(final String event, final Bundle message, final EventCallback callback) { - Log.i(LOG_TAG, "Handling event: " + event); - ThreadUtils.assertOnBackgroundThread(); - - final Context context = GeckoAppShell.getApplicationContext(); - // We're invoked in response to a Gecko message on a background thread. We should always - // be able to safely retrieve the current Gecko profile. - final GeckoProfile geckoProfile = GeckoProfile.get(context); - - if (callback == null) { - Log.e(LOG_TAG, "callback must not be null in " + event); - return; - } - - try { - if ("PushServiceAndroidGCM:Initialized".equals(event)) { - processComponentState(GeckoComponent.PushServiceAndroidGCM, true); - callback.sendSuccess(null); - return; - } - if ("PushServiceAndroidGCM:Uninitialized".equals(event)) { - processComponentState(GeckoComponent.PushServiceAndroidGCM, false); - callback.sendSuccess(null); - return; - } - if ("PushServiceAndroidGCM:Configure".equals(event)) { - final String endpoint = message.getString("endpoint"); - if (endpoint == null) { - callback.sendError("endpoint must not be null in " + event); - return; - } - final boolean debug = message.getBoolean("debug", false); - pushManager.configure(geckoProfile.getName(), endpoint, debug, System.currentTimeMillis()); // For side effects. - callback.sendSuccess(null); - return; - } - if ("PushServiceAndroidGCM:DumpRegistration".equals(event)) { - // In the future, this might be used to interrogate the Java Push Manager - // registration state from JavaScript. - callback.sendError("Not yet implemented!"); - return; - } - if ("PushServiceAndroidGCM:DumpSubscriptions".equals(event)) { - try { - final Map result = pushManager.allSubscriptionsForProfile(geckoProfile.getName()); - - final JSONObject json = new JSONObject(); - for (Map.Entry entry : result.entrySet()) { - json.put(entry.getKey(), entry.getValue().toJSONObject()); - } - callback.sendSuccess(json); - } catch (JSONException e) { - callback.sendError("Got exception handling message [" + event + "]: " + e.toString()); - } - return; - } - if ("PushServiceAndroidGCM:RegisterUserAgent".equals(event)) { - try { - pushManager.registerUserAgent(geckoProfile.getName(), System.currentTimeMillis()); // For side-effects. - callback.sendSuccess(null); - } catch (PushManager.ProfileNeedsConfigurationException | AutopushClientException | PushClient.LocalException | IOException e) { - Log.e(LOG_TAG, "Got exception in " + event, e); - callback.sendError("Got exception handling message [" + event + "]: " + e.toString()); - } - return; - } - if ("PushServiceAndroidGCM:UnregisterUserAgent".equals(event)) { - // In the future, this might be used to tell the Java Push Manager to unregister - // a User Agent entirely from JavaScript. Right now, however, everything is - // subscription based; there's no concept of unregistering all subscriptions - // simultaneously. - callback.sendError("Not yet implemented!"); - return; - } - if ("PushServiceAndroidGCM:SubscribeChannel".equals(event)) { - final String service = SERVICE_FXA.equals(message.getString("service")) ? - SERVICE_FXA : - SERVICE_WEBPUSH; - final JSONObject serviceData; - final String appServerKey = message.getString("appServerKey"); - try { - serviceData = new JSONObject(); - serviceData.put("profileName", geckoProfile.getName()); - serviceData.put("profilePath", geckoProfile.getDir().getAbsolutePath()); - } catch (JSONException e) { - Log.e(LOG_TAG, "Got exception in " + event, e); - callback.sendError("Got exception handling message [" + event + "]: " + e.toString()); - return; - } - - final PushSubscription subscription; - try { - subscription = pushManager.subscribeChannel(geckoProfile.getName(), service, serviceData, appServerKey, System.currentTimeMillis()); - } catch (PushManager.ProfileNeedsConfigurationException | AutopushClientException | PushClient.LocalException | IOException e) { - Log.e(LOG_TAG, "Got exception in " + event, e); - callback.sendError("Got exception handling message [" + event + "]: " + e.toString()); - return; - } - - final JSONObject json = new JSONObject(); - try { - json.put("channelID", subscription.chid); - json.put("endpoint", subscription.webpushEndpoint); - } catch (JSONException e) { - Log.e(LOG_TAG, "Got exception in " + event, e); - callback.sendError("Got exception handling message [" + event + "]: " + e.toString()); - return; - } - - Telemetry.sendUIEvent(TelemetryContract.Event.SAVE, TelemetryContract.Method.SERVICE, "dom-push-api"); - callback.sendSuccess(json); - return; - } - if ("PushServiceAndroidGCM:UnsubscribeChannel".equals(event)) { - final String channelID = message.getString("channelID"); - if (channelID == null) { - callback.sendError("channelID must not be null in " + event); - return; - } - - // Fire and forget. See comments in the function itself. - final PushSubscription pushSubscription = pushManager.unsubscribeChannel(channelID); - if (pushSubscription != null) { - Telemetry.sendUIEvent(TelemetryContract.Event.UNSAVE, TelemetryContract.Method.SERVICE, "dom-push-api"); - callback.sendSuccess(null); - return; - } - - callback.sendError("Could not unsubscribe from channel: " + channelID); - return; - } - if ("History:GetPrePathLastVisitedTimeMilliseconds".equals(event)) { - if (callback == null) { - Log.e(LOG_TAG, "callback must not be null in " + event); - return; - } - final String prePath = message.getString("prePath"); - if (prePath == null) { - callback.sendError("prePath must not be null in " + event); - return; - } - // We're on a background thread, so we can be synchronous. - final long millis = BrowserDB.from(geckoProfile).getPrePathLastVisitedTimeMilliseconds( - context.getContentResolver(), prePath); - callback.sendSuccess(millis); - return; - } - } catch (GcmTokenClient.NeedsGooglePlayServicesException e) { - // TODO: improve this. Can we find a point where the user is *definitely* interacting - // with the WebPush? Perhaps we can show a dialog when interacting with the Push - // permissions, and then be more aggressive showing this notification when we have - // registrations and subscriptions that can't be advanced. - callback.sendError("To handle event [" + event + "], user interaction is needed to enable Google Play Services."); - } - } - - private void processComponentState(@NonNull GeckoComponent component, boolean isReady) { - if (component == GeckoComponent.PushServiceAndroidGCM) { - isReadyPushServiceAndroidGCM = isReady; - } - - // Send all pending messages to Gecko. - if (canSendPushMessagesToGecko()) { - sendPushMessagesToGecko(pendingPushMessages); - pendingPushMessages.clear(); - } - } - - private boolean canSendPushMessagesToGecko() { - return isReadyPushServiceAndroidGCM; - } - - private static void sendPushMessagesToGecko(@NonNull List messages) { - for (JSONObject pushMessage : messages) { - final String profileName = pushMessage.optString("profileName", null); - final String profilePath = pushMessage.optString("profilePath", null); - final String service = pushMessage.optString("service", null); - if (profileName == null || profilePath == null || - !GeckoThread.canUseProfile(profileName, new File(profilePath))) { - Log.e(LOG_TAG, "Mismatched profile for chid: " + - pushMessage.optString("channelID") + - "; ignoring dom/push message."); - continue; - } - if (SERVICE_WEBPUSH.equals(service)) { - sendMessageToGeckoService(pushMessage); - } else if (SERVICE_FXA.equals(service)) { - sendMessageToDecodeToGeckoService(pushMessage); - } - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/push/PushState.java b/mobile/android/base/java/org/mozilla/gecko/push/PushState.java deleted file mode 100644 index 686bf5a0d1..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/push/PushState.java +++ /dev/null @@ -1,137 +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.push; - -import android.content.Context; -import android.support.annotation.NonNull; -import android.support.annotation.WorkerThread; -import android.support.v4.util.AtomicFile; -import android.util.Log; - -import org.json.JSONException; -import org.json.JSONObject; - -import java.io.File; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.IOException; -import java.util.HashMap; -import java.util.Iterator; -import java.util.Map; - -/** - * Firefox for Android maintains an App-wide mapping associating - * profile names to push registrations. Each push registration in turn associates channels to - * push subscriptions. - *

- * We use a simple storage model of JSON backed by an atomic file. It is assumed that instances - * of this class will reference distinct files on disk; and that all accesses will be happen on a - * single (worker thread). - */ -public class PushState { - private static final String LOG_TAG = "GeckoPushState"; - - private static final long VERSION = 1L; - - protected final @NonNull AtomicFile file; - - protected final @NonNull Map registrations; - - public PushState(Context context, @NonNull String fileName) { - this.registrations = new HashMap<>(); - - file = new AtomicFile(new File(context.getApplicationInfo().dataDir, fileName)); - synchronized (file) { - try { - final String s = new String(file.readFully(), "UTF-8"); - final JSONObject temp = new JSONObject(s); - if (temp.optLong("version", 0L) != VERSION) { - throw new JSONException("Unknown version!"); - } - - final JSONObject registrationsObject = temp.getJSONObject("registrations"); - final Iterator it = registrationsObject.keys(); - while (it.hasNext()) { - final String profileName = it.next(); - final PushRegistration registration = PushRegistration.fromJSONObject(registrationsObject.getJSONObject(profileName)); - this.registrations.put(profileName, registration); - } - } catch (FileNotFoundException e) { - Log.i(LOG_TAG, "No storage found; starting fresh."); - this.registrations.clear(); - } catch (IOException | JSONException e) { - Log.w(LOG_TAG, "Got exception reading storage; dropping storage and starting fresh.", e); - this.registrations.clear(); - } - } - } - - public JSONObject toJSONObject() throws JSONException { - final JSONObject registrations = new JSONObject(); - for (Map.Entry entry : this.registrations.entrySet()) { - registrations.put(entry.getKey(), entry.getValue().toJSONObject()); - } - - final JSONObject jsonObject = new JSONObject(); - jsonObject.put("version", 1L); - jsonObject.put("registrations", registrations); - return jsonObject; - } - - /** - * Synchronously persist the cache to disk. - * @return whether the cache was persisted successfully. - */ - @WorkerThread - public boolean checkpoint() { - synchronized (file) { - FileOutputStream fileOutputStream = null; - try { - fileOutputStream = file.startWrite(); - fileOutputStream.write(toJSONObject().toString().getBytes("UTF-8")); - file.finishWrite(fileOutputStream); - return true; - } catch (JSONException | IOException e) { - Log.e(LOG_TAG, "Got exception writing JSON storage; ignoring.", e); - if (fileOutputStream != null) { - file.failWrite(fileOutputStream); - } - return false; - } - } - } - - public PushRegistration putRegistration(@NonNull String profileName, @NonNull PushRegistration registration) { - return registrations.put(profileName, registration); - } - - /** - * Return the existing push registration for the given profile name. - * @return the push registration, if one is registered; null otherwise. - */ - public PushRegistration getRegistration(@NonNull String profileName) { - return registrations.get(profileName); - } - - /** - * Return all push registrations, keyed by profile names. - * @return a map of all push registrations. The map is intentionally mutable - be careful! - */ - public @NonNull Map getRegistrations() { - return registrations; - } - - /** - * Remove any existing push registration for the given profile name. - *

- * Most registration removals are during iteration, which should use an iterator that is - * aware of removals. - * @return the removed push registration, if one was removed; null otherwise. - */ - public PushRegistration removeRegistration(@NonNull String profileName) { - return registrations.remove(profileName); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/push/PushSubscription.java b/mobile/android/base/java/org/mozilla/gecko/push/PushSubscription.java deleted file mode 100644 index ecf752591f..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/push/PushSubscription.java +++ /dev/null @@ -1,81 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.push; - -import android.support.annotation.NonNull; -import org.json.JSONException; -import org.json.JSONObject; - -/** - * Represent an autopush Channel subscription. - *

- * Such a subscription associates a user agent and autopush data with a channel - * ID, a WebPush endpoint, and some service-specific data. - *

- * Cloud Messaging data, and the returned uaid and secret. - *

- * Each registration is associated to a single Gecko profile, although we don't - * enforce that here. This class is immutable, so it is by definition - * thread-safe. - */ -public class PushSubscription { - public final @NonNull String chid; - public final @NonNull String profileName; - public final @NonNull String webpushEndpoint; - public final @NonNull String service; - public final JSONObject serviceData; - - public PushSubscription(@NonNull String chid, @NonNull String profileName, @NonNull String webpushEndpoint, @NonNull String service, JSONObject serviceData) { - this.chid = chid; - this.profileName = profileName; - this.webpushEndpoint = webpushEndpoint; - this.service = service; - this.serviceData = serviceData; - } - - public JSONObject toJSONObject() throws JSONException { - final JSONObject jsonObject = new JSONObject(); - jsonObject.put("chid", chid); - jsonObject.put("profileName", profileName); - jsonObject.put("webpushEndpoint", webpushEndpoint); - jsonObject.put("service", service); - jsonObject.put("serviceData", serviceData); - return jsonObject; - } - - public static PushSubscription fromJSONObject(@NonNull JSONObject subscription) throws JSONException { - final String chid = subscription.getString("chid"); - final String profileName = subscription.getString("profileName"); - final String webpushEndpoint = subscription.getString("webpushEndpoint"); - final String service = subscription.getString("service"); - final JSONObject serviceData = subscription.optJSONObject("serviceData"); - return new PushSubscription(chid, profileName, webpushEndpoint, service, serviceData); - } - - @Override - public boolean equals(Object o) { - // Auto-generated. - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - PushSubscription that = (PushSubscription) o; - - if (!chid.equals(that.chid)) return false; - if (!profileName.equals(that.profileName)) return false; - if (!webpushEndpoint.equals(that.webpushEndpoint)) return false; - return service.equals(that.service); - } - - @Override - public int hashCode() { - // Auto-generated. - int result = profileName.hashCode(); - result = 31 * result + chid.hashCode(); - result = 31 * result + webpushEndpoint.hashCode(); - result = 31 * result + service.hashCode(); - return result; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/reader/ReaderModeUtils.java b/mobile/android/base/java/org/mozilla/gecko/reader/ReaderModeUtils.java deleted file mode 100644 index e70aac5b5b..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/reader/ReaderModeUtils.java +++ /dev/null @@ -1,72 +0,0 @@ -/* 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.reader; - -import org.mozilla.gecko.AboutPages; -import org.mozilla.gecko.util.StringUtils; - -import android.net.Uri; - -public class ReaderModeUtils { - private static final String LOGTAG = "ReaderModeUtils"; - - /** - * Extract the URL from a valid about:reader URL. You may want to use stripAboutReaderUrl - * instead to always obtain a valid String. - * - * @see #stripAboutReaderUrl(String) for a safer version that returns the original URL for malformed/invalid - * URLs. - * @return null if the URL is malformed or doesn't contain a URL parameter. - */ - private static String getUrlFromAboutReader(String aboutReaderUrl) { - return StringUtils.getQueryParameter(aboutReaderUrl, "url"); - } - - public static boolean isEnteringReaderMode(String oldURL, String newURL) { - if (oldURL == null || newURL == null) { - return false; - } - - if (!AboutPages.isAboutReader(newURL)) { - return false; - } - - String urlFromAboutReader = getUrlFromAboutReader(newURL); - if (urlFromAboutReader == null) { - return false; - } - - return urlFromAboutReader.equals(oldURL); - } - - public static String getAboutReaderForUrl(String url) { - return getAboutReaderForUrl(url, -1); - } - - /** - * Obtain the underlying URL from an about:reader URL. - * This will return the input URL if either of the following is true: - * 1. the input URL is a non about:reader URL - * 2. the input URL is an invalid/unparseable about:reader URL - */ - public static String stripAboutReaderUrl(String url) { - if (!AboutPages.isAboutReader(url)) { - return url; - } - - final String strippedUrl = getUrlFromAboutReader(url); - return strippedUrl != null ? strippedUrl : url; - } - - public static String getAboutReaderForUrl(String url, int tabId) { - String aboutReaderUrl = AboutPages.READER + "?url=" + Uri.encode(url); - - if (tabId >= 0) { - aboutReaderUrl += "&tabId=" + tabId; - } - - return aboutReaderUrl; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/reader/ReadingListHelper.java b/mobile/android/base/java/org/mozilla/gecko/reader/ReadingListHelper.java deleted file mode 100644 index e01ff79aca..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/reader/ReadingListHelper.java +++ /dev/null @@ -1,154 +0,0 @@ -/* 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.reader; - -import org.json.JSONException; -import org.json.JSONObject; - -import org.mozilla.gecko.AboutPages; -import org.mozilla.gecko.EventDispatcher; -import org.mozilla.gecko.GeckoApp; -import org.mozilla.gecko.GeckoAppShell; -import org.mozilla.gecko.GeckoProfile; -import org.mozilla.gecko.db.BrowserDB; -import org.mozilla.gecko.icons.IconRequest; -import org.mozilla.gecko.icons.Icons; -import org.mozilla.gecko.util.EventCallback; -import org.mozilla.gecko.util.NativeEventListener; -import org.mozilla.gecko.util.NativeJSObject; -import org.mozilla.gecko.util.ThreadUtils; -import org.mozilla.gecko.util.UIAsyncTask; - -import android.content.Context; -import android.util.Log; - -import java.util.concurrent.ExecutionException; - -public final class ReadingListHelper implements NativeEventListener { - private static final String LOGTAG = "GeckoReadingListHelper"; - - protected final Context context; - private final BrowserDB db; - - public ReadingListHelper(Context context, GeckoProfile profile) { - this.context = context; - this.db = BrowserDB.from(profile); - - GeckoApp.getEventDispatcher().registerGeckoThreadListener((NativeEventListener) this, - "Reader:FaviconRequest", "Reader:AddedToCache"); - } - - public void uninit() { - GeckoApp.getEventDispatcher().unregisterGeckoThreadListener((NativeEventListener) this, - "Reader:FaviconRequest", "Reader:AddedToCache"); - } - - @Override - public void handleMessage(final String event, final NativeJSObject message, - final EventCallback callback) { - switch (event) { - case "Reader:FaviconRequest": { - handleReaderModeFaviconRequest(callback, message.getString("url")); - break; - } - case "Reader:AddedToCache": { - // AddedToCache is a one way message: callback will be null, and we therefore shouldn't - // attempt to handle it. - handleAddedToCache(message.getString("url"), message.getString("path"), message.getInt("size")); - break; - } - } - } - - /** - * Gecko (ReaderMode) requests the page favicon to append to the - * document head for display. - */ - private void handleReaderModeFaviconRequest(final EventCallback callback, final String url) { - (new UIAsyncTask.WithoutParams(ThreadUtils.getBackgroundHandler()) { - @Override - public String doInBackground() { - // This is a bit ridiculous if you look at the bigger picture: Reader mode extracts - // the article content. We insert the content into a new document (about:reader). - // Some events are exchanged to lookup the icon URL for the actual website. This - // URL is then added to the markup which will then trigger our icon loading code in - // the Tab class. - // - // The Tab class could just lookup and load the icon itself. All it needs to do is - // to strip the about:reader URL and perform a normal icon load from cache. - // - // A more global solution (looking at desktop and iOS) would be to copy the - // markup from the original page to the about:reader page and then rely on our normal - // icon loading code. This would work even if we do not have anything in the cache - // for some kind of reason. - - final IconRequest request = Icons.with(context) - .pageUrl(url) - .prepareOnly() - .build(); - - try { - request.execute(null).get(); - if (request.getIconCount() > 0) { - return request.getBestIcon().getUrl(); - } - } catch (InterruptedException | ExecutionException e) { - // Ignore - } - - return null; - } - - @Override - public void onPostExecute(String faviconUrl) { - JSONObject args = new JSONObject(); - if (faviconUrl != null) { - try { - args.put("url", url); - args.put("faviconUrl", faviconUrl); - } catch (JSONException e) { - Log.w(LOGTAG, "Error building JSON favicon arguments.", e); - } - } - callback.sendSuccess(args.toString()); - } - }).execute(); - } - - private void handleAddedToCache(final String url, final String path, final int size) { - final SavedReaderViewHelper rch = SavedReaderViewHelper.getSavedReaderViewHelper(context); - - rch.put(url, path, size); - } - - public static void cacheReaderItem(final String url, final int tabID, Context context) { - if (AboutPages.isAboutReader(url)) { - throw new IllegalArgumentException("Page url must be original (not about:reader) url"); - } - - SavedReaderViewHelper rch = SavedReaderViewHelper.getSavedReaderViewHelper(context); - - if (!rch.isURLCached(url)) { - GeckoAppShell.notifyObservers("Reader:AddToCache", Integer.toString(tabID)); - } - } - - public static void removeCachedReaderItem(final String url, Context context) { - if (AboutPages.isAboutReader(url)) { - throw new IllegalArgumentException("Page url must be original (not about:reader) url"); - } - - SavedReaderViewHelper rch = SavedReaderViewHelper.getSavedReaderViewHelper(context); - - if (rch.isURLCached(url)) { - GeckoAppShell.notifyObservers("Reader:RemoveFromCache", url); - } - - // When removing items from the cache we can probably spare ourselves the async callback - // that we use when adding cached items. We know the cached item will be gone, hence - // we no longer need to track it in the SavedReaderViewHelper - rch.remove(url); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/reader/SavedReaderViewHelper.java b/mobile/android/base/java/org/mozilla/gecko/reader/SavedReaderViewHelper.java deleted file mode 100644 index e60abac71b..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/reader/SavedReaderViewHelper.java +++ /dev/null @@ -1,247 +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.reader; - -import android.content.Context; -import android.support.annotation.NonNull; -import android.util.Log; - -import org.json.JSONException; -import org.json.JSONObject; -import org.mozilla.gecko.GeckoProfile; -import org.mozilla.gecko.annotation.RobocopTarget; -import org.mozilla.gecko.db.BrowserDB; -import org.mozilla.gecko.db.UrlAnnotations; -import org.mozilla.gecko.util.ThreadUtils; - -import java.io.File; -import java.io.IOException; -import java.util.Iterator; - -/** - * Helper to keep track of items that are stored in the reader view cache. This is an in-memory list - * of the reader view items that are cached on disk. It is intended to allow quickly determining whether - * a given URL is in the cache, and also how many cached items there are. - * - * Currently we have 1:1 correspondence of reader view items (in the URL-annotations table) - * to cached items. This is _not_ a true cache, we never purge/cleanup items here - we only remove - * items when we un-reader-view/bookmark them. This is an acceptable model while we can guarantee the - * 1:1 correspondence. - * - * It isn't strictly necessary to mirror cached items in SQL at this stage, however it seems sensible - * to maintain URL anotations to avoid additional DB migrations in future. - * It is also simpler to implement the reading list smart-folder using the annotations (even if we do - * all other decoration from our in-memory cache record), as that is what we will need when - * we move away from the 1:1 correspondence. - * - * Bookmarks can be in one of two states - plain bookmark, or reader view bookmark that is also saved - * offline. We're hoping to introduce real cache management / cleanup in future, in which case a - * third user-visible state (reader view bookmark without a cache entry) will be added. However that logic is - * much more complicated and requires substantial changes in how we decorate reader view bookmarks. - * With the current 1:1 correspondence we can use this in-memory helper to quickly decorate - * bookmarks (in all the various lists and panels that are used), whereas supporting - * the third state requires significant changes in order to allow joining with the - * URL-annotations table wherever bookmarks might be retrieved (i.e. multiple homepanels, each with - * their own loaders and adapter). - * - * If/when cache cleanup and sync are implemented, URL annotations will be the canonical record of - * user intent, and the cache will no longer represent all reader view bookmarks. We will have (A) - * cached items that are not a bookmark, or bookmarks without the reader view annotation (both of - * these would need purging), and (B) bookmarks with a reader view annotation, but not stored in - * the cache (which we might want to download in the background). Supporting (B) is currently difficult, - * see previous paragraph. - */ -public class SavedReaderViewHelper { - private static final String LOG_TAG = "SavedReaderViewHelper"; - - private static final String PATH = "path"; - private static final String SIZE = "size"; - - private static final String DIRECTORY = "readercache"; - private static final String FILE_NAME = "items.json"; - private static final String FILE_PATH = DIRECTORY + "/" + FILE_NAME; - - // We use null to indicate that the cache hasn't yet been loaded. Loading has to be explicitly - // requested by client code, and must happen on the background thread. Attempting to access - // items (which happens mainly on the UI thread) before explicitly loading them is not permitted. - private JSONObject mItems = null; - - private final Context mContext; - - private static SavedReaderViewHelper instance = null; - - private SavedReaderViewHelper(Context context) { - mContext = context; - } - - public static synchronized SavedReaderViewHelper getSavedReaderViewHelper(final Context context) { - if (instance == null) { - instance = new SavedReaderViewHelper(context); - } - - return instance; - } - - /** - * Load the reader view cache list from our JSON file. - * - * Must not be run on the UI thread due to file access. - */ - public synchronized void loadItems() { - // TODO bug 1264489 - // This is a band aid fix for Bug 1264134. We need to figure out the root cause and reenable this - // assertion. - // ThreadUtils.assertNotOnUiThread(); - - if (mItems != null) { - return; - } - - try { - mItems = GeckoProfile.get(mContext).readJSONObjectFromFile(FILE_PATH); - } catch (IOException e) { - mItems = new JSONObject(); - } - } - - private synchronized void assertItemsLoaded() { - if (mItems == null) { - throw new IllegalStateException("SavedReaderView items must be explicitly loaded using loadItems() before access."); - } - } - - private JSONObject makeItem(@NonNull String path, long size) throws JSONException { - final JSONObject item = new JSONObject(); - - item.put(PATH, path); - item.put(SIZE, size); - - return item; - } - - public synchronized boolean isURLCached(@NonNull final String URL) { - assertItemsLoaded(); - return mItems.has(URL); - } - - /** - * Insert an item into the list of cached items. - * - * This may be called from any thread. - */ - public synchronized void put(@NonNull final String pageURL, @NonNull final String path, final long size) { - assertItemsLoaded(); - - try { - mItems.put(pageURL, makeItem(path, size)); - } catch (JSONException e) { - Log.w(LOG_TAG, "Item insertion failed:", e); - // This should never happen, absent any errors in our own implementation - throw new IllegalStateException("Failure inserting into SavedReaderViewHelper json"); - } - - ThreadUtils.postToBackgroundThread(new Runnable() { - @Override - public void run() { - UrlAnnotations annotations = BrowserDB.from(mContext).getUrlAnnotations(); - annotations.insertReaderViewUrl(mContext.getContentResolver(), pageURL); - - commit(); - } - }); - } - - protected synchronized void remove(@NonNull final String pageURL) { - assertItemsLoaded(); - - mItems.remove(pageURL); - - ThreadUtils.postToBackgroundThread(new Runnable() { - @Override - public void run() { - UrlAnnotations annotations = BrowserDB.from(mContext).getUrlAnnotations(); - annotations.deleteReaderViewUrl(mContext.getContentResolver(), pageURL); - - commit(); - } - }); - } - - @RobocopTarget - public synchronized int size() { - assertItemsLoaded(); - return mItems.length(); - } - - private synchronized void commit() { - ThreadUtils.assertOnBackgroundThread(); - - GeckoProfile profile = GeckoProfile.get(mContext); - File cacheDir = new File(profile.getDir(), DIRECTORY); - - if (!cacheDir.exists()) { - Log.i(LOG_TAG, "No preexisting cache directory, creating now"); - - boolean cacheDirCreated = cacheDir.mkdir(); - if (!cacheDirCreated) { - throw new IllegalStateException("Couldn't create cache directory, unable to track reader view cache"); - } - } - - profile.writeFile(FILE_PATH, mItems.toString()); - } - - /** - * Return the Reader View URL for a given URL if it is contained in the cache. Returns the - * plain URL if the page is not cached. - */ - public static String getReaderURLIfCached(final Context context, @NonNull final String pageURL) { - SavedReaderViewHelper rvh = getSavedReaderViewHelper(context); - - if (rvh.isURLCached(pageURL)) { - return ReaderModeUtils.getAboutReaderForUrl(pageURL); - } else { - return pageURL; - } - } - - /** - * Obtain the total disk space used for saved reader view items, in KB. - * - * @return Total disk space used (KB), or Integer.MAX_VALUE on overflow. - */ - public synchronized int getDiskSpacedUsedKB() { - // JSONObject is not thread safe - we need to be synchronized to avoid issues (most likely to - // occur if items are removed during iteration). - final Iterator keys = mItems.keys(); - long bytes = 0; - - while (keys.hasNext()) { - final String pageURL = keys.next(); - try { - final JSONObject item = mItems.getJSONObject(pageURL); - bytes += item.getLong(SIZE); - - // Overflow is highly unlikely (we will hit device storage limits before we hit integer limits), - // but we should still handle this for correctness. - // We definitely can't store our output in an int if we overflow the long here. - if (bytes < 0) { - return Integer.MAX_VALUE; - } - } catch (JSONException e) { - // This shouldn't ever happen: - throw new IllegalStateException("Must be able to access items in saved reader view list", e); - } - } - - long kb = bytes / 1024; - if (kb > Integer.MAX_VALUE) { - return Integer.MAX_VALUE; - } else { - return (int) kb; - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/restrictions/DefaultConfiguration.java b/mobile/android/base/java/org/mozilla/gecko/restrictions/DefaultConfiguration.java deleted file mode 100644 index 480078a98d..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/restrictions/DefaultConfiguration.java +++ /dev/null @@ -1,34 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.restrictions; - -/** - * Default implementation of RestrictionConfiguration interface. Used whenever no restrictions are enforced for the - * current profile. - */ -public class DefaultConfiguration implements RestrictionConfiguration { - @Override - public boolean isAllowed(Restrictable restrictable) { - if (restrictable == Restrictable.BLOCK_LIST) { - return false; - } - - return true; - } - - @Override - public boolean canLoadUrl(String url) { - return true; - } - - @Override - public boolean isRestricted() { - return false; - } - - @Override - public void update() {} -} diff --git a/mobile/android/base/java/org/mozilla/gecko/restrictions/GuestProfileConfiguration.java b/mobile/android/base/java/org/mozilla/gecko/restrictions/GuestProfileConfiguration.java deleted file mode 100644 index f9663ccf7e..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/restrictions/GuestProfileConfiguration.java +++ /dev/null @@ -1,83 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.restrictions; - -import android.net.Uri; - -import java.util.Arrays; -import java.util.List; - -/** - * RestrictionConfiguration implementation for guest profiles. - */ -public class GuestProfileConfiguration implements RestrictionConfiguration { - static List DISABLED_FEATURES = Arrays.asList( - Restrictable.DOWNLOAD, - Restrictable.INSTALL_EXTENSION, - Restrictable.INSTALL_APPS, - Restrictable.BROWSE, - Restrictable.SHARE, - Restrictable.BOOKMARK, - Restrictable.ADD_CONTACT, - Restrictable.SET_IMAGE, - Restrictable.MODIFY_ACCOUNTS, - Restrictable.REMOTE_DEBUGGING, - Restrictable.IMPORT_SETTINGS, - Restrictable.BLOCK_LIST, - Restrictable.DATA_CHOICES, - Restrictable.DEFAULT_THEME - ); - - @SuppressWarnings("serial") - private static final List BANNED_SCHEMES = Arrays.asList( - "file", - "chrome", - "resource", - "jar", - "wyciwyg" - ); - - private static final List BANNED_URLS = Arrays.asList( - "about:config", - "about:addons" - ); - - @Override - public boolean isAllowed(Restrictable restrictable) { - return !DISABLED_FEATURES.contains(restrictable); - } - - @Override - public boolean canLoadUrl(String url) { - // Null URLs are always permitted. - if (url == null) { - return true; - } - - final Uri u = Uri.parse(url); - final String scheme = u.getScheme(); - if (BANNED_SCHEMES.contains(scheme)) { - return false; - } - - url = url.toLowerCase(); - for (String banned : BANNED_URLS) { - if (url.startsWith(banned)) { - return false; - } - } - - return true; - } - - @Override - public boolean isRestricted() { - return true; - } - - @Override - public void update() {} -} diff --git a/mobile/android/base/java/org/mozilla/gecko/restrictions/Restrictable.java b/mobile/android/base/java/org/mozilla/gecko/restrictions/Restrictable.java deleted file mode 100644 index f794c57825..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/restrictions/Restrictable.java +++ /dev/null @@ -1,112 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.restrictions; - -import org.mozilla.gecko.R; - -import android.content.Context; -import android.support.annotation.StringRes; - -/** - * This is a list of things we can restrict you from doing. Some of these are reflected in Android UserManager constants. - * Others are specific to us. - * These constants should be in sync with the ones from toolkit/components/parentalcontrols/nsIParentalControlsService.idl - */ -public enum Restrictable { - DOWNLOAD(1, "downloads", 0, 0), - - INSTALL_EXTENSION( - 2, "no_install_extensions", - R.string.restrictable_feature_addons_installation, - R.string.restrictable_feature_addons_installation_description), - - // UserManager.DISALLOW_INSTALL_APPS - INSTALL_APPS(3, "no_install_apps", 0 , 0), - - BROWSE(4, "browse", 0, 0), - - SHARE(5, "share", 0, 0), - - BOOKMARK(6, "bookmark", 0, 0), - - ADD_CONTACT(7, "add_contact", 0, 0), - - SET_IMAGE(8, "set_image", 0, 0), - - // UserManager.DISALLOW_MODIFY_ACCOUNTS - MODIFY_ACCOUNTS(9, "no_modify_accounts", 0, 0), - - REMOTE_DEBUGGING(10, "remote_debugging", 0, 0), - - IMPORT_SETTINGS(11, "import_settings", 0, 0), - - PRIVATE_BROWSING( - 12, "private_browsing", - R.string.restrictable_feature_private_browsing, - R.string.restrictable_feature_private_browsing_description), - - DATA_CHOICES(13, "data_coices", 0, 0), - - CLEAR_HISTORY(14, "clear_history", - R.string.restrictable_feature_clear_history, - R.string.restrictable_feature_clear_history_description), - - MASTER_PASSWORD(15, "master_password", 0, 0), - - GUEST_BROWSING(16, "guest_browsing", 0, 0), - - ADVANCED_SETTINGS(17, "advanced_settings", - R.string.restrictable_feature_advanced_settings, - R.string.restrictable_feature_advanced_settings_description), - - CAMERA_MICROPHONE(18, "camera_microphone", - R.string.restrictable_feature_camera_microphone, - R.string.restrictable_feature_camera_microphone_description), - - BLOCK_LIST(19, "block_list", - R.string.restrictable_feature_block_list, - R.string.restrictable_feature_block_list_description), - - TELEMETRY(20, "telemetry", - R.string.datareporting_telemetry_title, - R.string.datareporting_telemetry_summary), - - HEALTH_REPORT(21, "health_report", - R.string.datareporting_fhr_title, - R.string.datareporting_fhr_summary2), - - DEFAULT_THEME(22, "default_theme", 0, 0); - - public final int id; - public final String name; - - @StringRes - public final int title; - - @StringRes - public final int description; - - Restrictable(final int id, final String name, @StringRes int title, @StringRes int description) { - this.id = id; - this.name = name; - this.title = title; - this.description = description; - } - - public String getTitle(Context context) { - if (title == 0) { - return toString(); - } - return context.getResources().getString(title); - } - - public String getDescription(Context context) { - if (description == 0) { - return null; - } - return context.getResources().getString(description); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/restrictions/RestrictedProfileConfiguration.java b/mobile/android/base/java/org/mozilla/gecko/restrictions/RestrictedProfileConfiguration.java deleted file mode 100644 index 15a0b97f44..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/restrictions/RestrictedProfileConfiguration.java +++ /dev/null @@ -1,129 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.restrictions; - -import org.mozilla.gecko.AboutPages; -import org.mozilla.gecko.AppConstants; -import org.mozilla.gecko.util.ThreadUtils; - -import android.annotation.TargetApi; -import android.content.Context; -import android.os.Build; -import android.os.Bundle; -import android.os.StrictMode; -import android.os.UserManager; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; - -@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2) -public class RestrictedProfileConfiguration implements RestrictionConfiguration { - // Mapping from restrictable feature to default state (on/off) - private static Map configuration = new LinkedHashMap<>(); - static { - configuration.put(Restrictable.INSTALL_EXTENSION, false); - configuration.put(Restrictable.PRIVATE_BROWSING, false); - configuration.put(Restrictable.CLEAR_HISTORY, false); - configuration.put(Restrictable.MASTER_PASSWORD, false); - configuration.put(Restrictable.GUEST_BROWSING, false); - configuration.put(Restrictable.ADVANCED_SETTINGS, false); - configuration.put(Restrictable.CAMERA_MICROPHONE, false); - configuration.put(Restrictable.DATA_CHOICES, false); - configuration.put(Restrictable.BLOCK_LIST, false); - configuration.put(Restrictable.TELEMETRY, false); - configuration.put(Restrictable.HEALTH_REPORT, true); - configuration.put(Restrictable.DEFAULT_THEME, true); - } - - /** - * These restrictions are hidden from the admin configuration UI. - */ - private static List hiddenRestrictions = new ArrayList<>(); - static { - hiddenRestrictions.add(Restrictable.MASTER_PASSWORD); - hiddenRestrictions.add(Restrictable.GUEST_BROWSING); - hiddenRestrictions.add(Restrictable.DATA_CHOICES); - hiddenRestrictions.add(Restrictable.DEFAULT_THEME); - - // Hold behind Nightly flag until we have an actual block list deployed. - if (!AppConstants.NIGHTLY_BUILD) { - hiddenRestrictions.add(Restrictable.BLOCK_LIST); - } - } - - /* package-private */ static boolean shouldHide(Restrictable restrictable) { - return hiddenRestrictions.contains(restrictable); - } - - /* package-private */ static Map getConfiguration() { - return configuration; - } - - private Context context; - - public RestrictedProfileConfiguration(Context context) { - this.context = context.getApplicationContext(); - } - - @Override - public synchronized boolean isAllowed(Restrictable restrictable) { - // Special casing system/user restrictions - if (restrictable == Restrictable.INSTALL_APPS || restrictable == Restrictable.MODIFY_ACCOUNTS) { - return RestrictionCache.getUserRestriction(context, restrictable.name); - } - - if (!RestrictionCache.hasApplicationRestriction(context, restrictable.name) && !configuration.containsKey(restrictable)) { - // Always allow features that are not in the configuration - return true; - } - - return RestrictionCache.getApplicationRestriction(context, restrictable.name, configuration.get(restrictable)); - } - - @Override - public boolean canLoadUrl(String url) { - if (!isAllowed(Restrictable.INSTALL_EXTENSION) && AboutPages.isAboutAddons(url)) { - return false; - } - - if (!isAllowed(Restrictable.PRIVATE_BROWSING) && AboutPages.isAboutPrivateBrowsing(url)) { - return false; - } - - if (AboutPages.isAboutConfig(url)) { - // Always block access to about:config to prevent circumventing restrictions (Bug 1189233) - return false; - } - - return true; - } - - @Override - public boolean isRestricted() { - return true; - } - - @Override - public synchronized void update() { - RestrictionCache.invalidate(); - } - - public static List getVisibleRestrictions() { - final List visibleList = new ArrayList<>(); - - for (Restrictable restrictable : configuration.keySet()) { - if (hiddenRestrictions.contains(restrictable)) { - continue; - } - visibleList.add(restrictable); - } - - return visibleList; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/restrictions/RestrictionCache.java b/mobile/android/base/java/org/mozilla/gecko/restrictions/RestrictionCache.java deleted file mode 100644 index 523cc113b4..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/restrictions/RestrictionCache.java +++ /dev/null @@ -1,99 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.restrictions; - -import android.annotation.TargetApi; -import android.content.Context; -import android.os.Build; -import android.os.Bundle; -import android.os.StrictMode; -import android.os.UserManager; - -import org.mozilla.gecko.util.ThreadUtils; - -/** - * Cache for user and application restrictions. - */ -public class RestrictionCache { - private static Bundle cachedAppRestrictions; - private static Bundle cachedUserRestrictions; - private static boolean isCacheInvalid = true; - - private RestrictionCache() {} - - public static synchronized boolean getUserRestriction(Context context, String restriction) { - updateCacheIfNeeded(context); - return cachedUserRestrictions.getBoolean(restriction); - } - - public static synchronized boolean hasApplicationRestriction(Context context, String restriction) { - updateCacheIfNeeded(context); - return cachedAppRestrictions.containsKey(restriction); - } - - public static synchronized boolean getApplicationRestriction(Context context, String restriction, boolean defaultValue) { - updateCacheIfNeeded(context); - return cachedAppRestrictions.getBoolean(restriction, defaultValue); - } - - public static synchronized boolean hasApplicationRestrictions(Context context) { - updateCacheIfNeeded(context); - return !cachedAppRestrictions.isEmpty(); - } - - public static synchronized void invalidate() { - isCacheInvalid = true; - } - - private static void updateCacheIfNeeded(Context context) { - // If we are not on the UI thread then we can just go ahead and read the values (Bug 1189347). - // Otherwise we read from the cache to avoid blocking the UI thread. If the cache is invalid - // then we hazard the consequences and just do the read. - if (isCacheInvalid || !ThreadUtils.isOnUiThread()) { - readRestrictions(context); - isCacheInvalid = false; - } - } - - @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2) - private static void readRestrictions(Context context) { - final UserManager mgr = (UserManager) context.getSystemService(Context.USER_SERVICE); - - // If we do not have anything in the cache yet then this read might happen on the UI thread (Bug 1189347). - final StrictMode.ThreadPolicy policy = StrictMode.allowThreadDiskReads(); - - try { - Bundle appRestrictions = mgr.getApplicationRestrictions(context.getPackageName()); - migrateRestrictionsIfNeeded(appRestrictions); - - cachedAppRestrictions = appRestrictions; - cachedUserRestrictions = mgr.getUserRestrictions(); // Always implies disk read - } finally { - StrictMode.setThreadPolicy(policy); - } - } - - /** - * This method migrates the old set of DISALLOW_ restrictions to the new restrictable feature ones (Bug 1189336). - */ - /* package-private */ static void migrateRestrictionsIfNeeded(Bundle bundle) { - if (!bundle.containsKey(Restrictable.INSTALL_EXTENSION.name) && bundle.containsKey("no_install_extensions")) { - bundle.putBoolean(Restrictable.INSTALL_EXTENSION.name, !bundle.getBoolean("no_install_extensions")); - } - - if (!bundle.containsKey(Restrictable.PRIVATE_BROWSING.name) && bundle.containsKey("no_private_browsing")) { - bundle.putBoolean(Restrictable.PRIVATE_BROWSING.name, !bundle.getBoolean("no_private_browsing")); - } - - if (!bundle.containsKey(Restrictable.CLEAR_HISTORY.name) && bundle.containsKey("no_clear_history")) { - bundle.putBoolean(Restrictable.CLEAR_HISTORY.name, !bundle.getBoolean("no_clear_history")); - } - - if (!bundle.containsKey(Restrictable.ADVANCED_SETTINGS.name) && bundle.containsKey("no_advanced_settings")) { - bundle.putBoolean(Restrictable.ADVANCED_SETTINGS.name, !bundle.getBoolean("no_advanced_settings")); - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/restrictions/RestrictionConfiguration.java b/mobile/android/base/java/org/mozilla/gecko/restrictions/RestrictionConfiguration.java deleted file mode 100644 index 7c40da734b..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/restrictions/RestrictionConfiguration.java +++ /dev/null @@ -1,31 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.restrictions; - -/** - * Interface for classes that Restrictions will delegate to for making decisions. - */ -public interface RestrictionConfiguration { - /** - * Is the user allowed to perform this action? - */ - boolean isAllowed(Restrictable restrictable); - - /** - * Is the user allowed to load the given URL? - */ - boolean canLoadUrl(String url); - - /** - * Is this user restricted in any way? - */ - boolean isRestricted(); - - /** - * Update restrictions if needed. - */ - void update(); -} diff --git a/mobile/android/base/java/org/mozilla/gecko/restrictions/RestrictionProvider.java b/mobile/android/base/java/org/mozilla/gecko/restrictions/RestrictionProvider.java deleted file mode 100644 index 26b9a446f2..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/restrictions/RestrictionProvider.java +++ /dev/null @@ -1,84 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.restrictions; - -import org.mozilla.gecko.AppConstants; - -import android.annotation.TargetApi; -import android.app.Activity; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.RestrictionEntry; -import android.os.Build; -import android.os.Bundle; -import android.text.TextUtils; - -import java.util.ArrayList; -import java.util.Map; - -/** - * Broadcast receiver providing supported restrictions to the system. - */ -@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2) -public class RestrictionProvider extends BroadcastReceiver { - @Override - public void onReceive(final Context context, final Intent intent) { - if (AppConstants.Versions.preJBMR2) { - // This broadcast does not make any sense prior to Jelly Bean MR2. - return; - } - - final PendingResult result = goAsync(); - - new Thread() { - @Override - public void run() { - final Bundle oldRestrictions = intent.getBundleExtra(Intent.EXTRA_RESTRICTIONS_BUNDLE); - RestrictionCache.migrateRestrictionsIfNeeded(oldRestrictions); - - final Bundle extras = new Bundle(); - - ArrayList entries = initRestrictions(context, oldRestrictions); - extras.putParcelableArrayList(Intent.EXTRA_RESTRICTIONS_LIST, entries); - - result.setResult(Activity.RESULT_OK, null, extras); - result.finish(); - } - }.start(); - } - - private ArrayList initRestrictions(Context context, Bundle oldRestrictions) { - ArrayList entries = new ArrayList(); - - final Map configuration = RestrictedProfileConfiguration.getConfiguration(); - - for (Restrictable restrictable : configuration.keySet()) { - if (RestrictedProfileConfiguration.shouldHide(restrictable)) { - continue; - } - - RestrictionEntry entry = createRestrictionEntryWithDefaultValue(context, restrictable, - oldRestrictions.getBoolean(restrictable.name, configuration.get(restrictable))); - entries.add(entry); - } - - return entries; - } - - private RestrictionEntry createRestrictionEntryWithDefaultValue(Context context, Restrictable restrictable, boolean defaultValue) { - RestrictionEntry entry = new RestrictionEntry(restrictable.name, defaultValue); - - entry.setTitle(restrictable.getTitle(context)); - - final String description = restrictable.getDescription(context); - if (!TextUtils.isEmpty(description)) { - entry.setDescription(description); - } - - return entry; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/restrictions/Restrictions.java b/mobile/android/base/java/org/mozilla/gecko/restrictions/Restrictions.java deleted file mode 100644 index 0cf6808103..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/restrictions/Restrictions.java +++ /dev/null @@ -1,127 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.restrictions; - -import android.annotation.TargetApi; -import android.content.Context; -import android.os.Build; -import android.util.Log; - -import org.mozilla.gecko.AppConstants.Versions; -import org.mozilla.gecko.GeckoAppShell; -import org.mozilla.gecko.GeckoProfile; -import org.mozilla.gecko.annotation.RobocopTarget; -import org.mozilla.gecko.annotation.WrapForJNI; - -@RobocopTarget -public class Restrictions { - private static final String LOGTAG = "GeckoRestrictedProfiles"; - - private static RestrictionConfiguration configuration; - - private static RestrictionConfiguration getConfiguration(Context context) { - if (configuration == null) { - configuration = createConfiguration(context); - } - - return configuration; - } - - public static synchronized RestrictionConfiguration createConfiguration(Context context) { - if (configuration != null) { - // This method is synchronized and another thread might already have created the configuration. - return configuration; - } - - if (isGuestProfile(context)) { - return new GuestProfileConfiguration(); - } else if (isRestrictedProfile(context)) { - return new RestrictedProfileConfiguration(context); - } else { - return new DefaultConfiguration(); - } - } - - private static boolean isGuestProfile(Context context) { - if (configuration != null) { - return configuration instanceof GuestProfileConfiguration; - } - - GeckoAppShell.GeckoInterface geckoInterface = GeckoAppShell.getGeckoInterface(); - if (geckoInterface != null) { - return geckoInterface.getProfile().inGuestMode(); - } - - return GeckoProfile.get(context).inGuestMode(); - } - - @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2) - public static boolean isRestrictedProfile(Context context) { - if (configuration != null) { - return configuration instanceof RestrictedProfileConfiguration; - } - - if (Versions.preJBMR2) { - // Early versions don't support restrictions at all - return false; - } - - // The user is on a restricted profile if, and only if, we injected application restrictions during account setup. - return RestrictionCache.hasApplicationRestrictions(context); - } - - public static void update(Context context) { - getConfiguration(context).update(); - } - - private static Restrictable geckoActionToRestriction(int action) { - for (Restrictable rest : Restrictable.values()) { - if (rest.id == action) { - return rest; - } - } - - throw new IllegalArgumentException("Unknown action " + action); - } - - private static boolean canLoadUrl(final Context context, final String url) { - return getConfiguration(context).canLoadUrl(url); - } - - @WrapForJNI(calledFrom = "gecko") - public static boolean isUserRestricted() { - return isUserRestricted(GeckoAppShell.getApplicationContext()); - } - - public static boolean isUserRestricted(final Context context) { - return getConfiguration(context).isRestricted(); - } - - public static boolean isAllowed(final Context context, final Restrictable restrictable) { - return getConfiguration(context).isAllowed(restrictable); - } - - @WrapForJNI(calledFrom = "gecko") - public static boolean isAllowed(int action, String url) { - final Restrictable restrictable; - try { - restrictable = geckoActionToRestriction(action); - } catch (IllegalArgumentException ex) { - // Unknown actions represent a coding error, so we - // refuse the action and log. - Log.e(LOGTAG, "Unknown action " + action + "; check calling code."); - return false; - } - - final Context context = GeckoAppShell.getApplicationContext(); - - if (Restrictable.BROWSE == restrictable) { - return canLoadUrl(context, url); - } else { - return isAllowed(context, restrictable); - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/search/SearchEngine.java b/mobile/android/base/java/org/mozilla/gecko/search/SearchEngine.java deleted file mode 100644 index d4d9938e26..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/search/SearchEngine.java +++ /dev/null @@ -1,304 +0,0 @@ -/* 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.search; - -import android.net.Uri; -import android.util.Log; -import android.util.Xml; - -import org.mozilla.gecko.util.StringUtils; -import org.xmlpull.v1.XmlPullParser; -import org.xmlpull.v1.XmlPullParserException; - -import java.io.IOException; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.List; -import java.util.Locale; -import java.util.Set; - -/** - * Extend this class to add a new search engine to - * the search activity. - */ -public class SearchEngine { - private static final String LOG_TAG = "SearchEngine"; - - private static final String URLTYPE_SUGGEST_JSON = "application/x-suggestions+json"; - private static final String URLTYPE_SEARCH_HTML = "text/html"; - - private static final String URL_REL_MOBILE = "mobile"; - - // Parameters copied from nsSearchService.js - private static final String MOZ_PARAM_LOCALE = "\\{moz:locale\\}"; - private static final String MOZ_PARAM_DIST_ID = "\\{moz:distributionID\\}"; - private static final String MOZ_PARAM_OFFICIAL = "\\{moz:official\\}"; - - // Supported OpenSearch parameters - // See http://opensearch.a9.com/spec/1.1/querysyntax/#core - private static final String OS_PARAM_USER_DEFINED = "\\{searchTerms\\??\\}"; - private static final String OS_PARAM_INPUT_ENCODING = "\\{inputEncoding\\??\\}"; - private static final String OS_PARAM_LANGUAGE = "\\{language\\??\\}"; - private static final String OS_PARAM_OUTPUT_ENCODING = "\\{outputEncoding\\??\\}"; - private static final String OS_PARAM_OPTIONAL = "\\{(?:\\w+:)?\\w+\\?\\}"; - - // Boilerplate bookmarklet-style JS for injecting CSS into the - // head of a web page. The actual CSS is inserted at `%s`. - private static final String STYLE_INJECTION_SCRIPT = - "javascript:(function(){" + - "var tag=document.createElement('style');" + - "tag.type='text/css';" + - "document.getElementsByTagName('head')[0].appendChild(tag);" + - "tag.innerText='%s'})();"; - - // The Gecko search identifier. This will be null for engines that don't ship with the locale. - private final String identifier; - - private String shortName; - private String iconURL; - - // Ordered list of preferred results URIs. - private final List resultsUris = new ArrayList(); - private Uri suggestUri; - - /** - * - * @param in InputStream of open search plugin XML - */ - public SearchEngine(String identifier, InputStream in) throws IOException, XmlPullParserException { - this.identifier = identifier; - - final XmlPullParser parser = Xml.newPullParser(); - parser.setInput(in, null); - parser.nextTag(); - readSearchPlugin(parser); - } - - private void readSearchPlugin(XmlPullParser parser) throws XmlPullParserException, IOException { - if (XmlPullParser.START_TAG != parser.getEventType()) { - throw new XmlPullParserException("Expected start tag: " + parser.getPositionDescription()); - } - - final String name = parser.getName(); - if (!"SearchPlugin".equals(name) && !"OpenSearchDescription".equals(name)) { - throw new XmlPullParserException("Expected or as root tag: " - + parser.getPositionDescription()); - } - - while (parser.next() != XmlPullParser.END_TAG) { - if (parser.getEventType() != XmlPullParser.START_TAG) { - continue; - } - - final String tag = parser.getName(); - if (tag.equals("ShortName")) { - readShortName(parser); - } else if (tag.equals("Url")) { - readUrl(parser); - } else if (tag.equals("Image")) { - readImage(parser); - } else { - skip(parser); - } - } - } - - private void readShortName(XmlPullParser parser) throws IOException, XmlPullParserException { - parser.require(XmlPullParser.START_TAG, null, "ShortName"); - if (parser.next() == XmlPullParser.TEXT) { - shortName = parser.getText(); - parser.nextTag(); - } - } - - private void readUrl(XmlPullParser parser) throws XmlPullParserException, IOException { - parser.require(XmlPullParser.START_TAG, null, "Url"); - - final String type = parser.getAttributeValue(null, "type"); - final String template = parser.getAttributeValue(null, "template"); - final String rel = parser.getAttributeValue(null, "rel"); - - Uri uri = Uri.parse(template); - - while (parser.next() != XmlPullParser.END_TAG) { - if (parser.getEventType() != XmlPullParser.START_TAG) { - continue; - } - - final String tag = parser.getName(); - - if (tag.equals("Param")) { - final String name = parser.getAttributeValue(null, "name"); - final String value = parser.getAttributeValue(null, "value"); - uri = uri.buildUpon().appendQueryParameter(name, value).build(); - parser.nextTag(); - // TODO: Support for other tags - //} else if (tag.equals("MozParam")) { - } else { - skip(parser); - } - } - - if (type.equals(URLTYPE_SEARCH_HTML)) { - // Prefer mobile URIs. - if (rel != null && rel.equals(URL_REL_MOBILE)) { - resultsUris.add(0, uri); - } else { - resultsUris.add(uri); - } - } else if (type.equals(URLTYPE_SUGGEST_JSON)) { - suggestUri = uri; - } - } - - private void readImage(XmlPullParser parser) throws XmlPullParserException, IOException { - parser.require(XmlPullParser.START_TAG, null, "Image"); - - // TODO: Use width and height to get a preferred icon URL. - //final int width = Integer.parseInt(parser.getAttributeValue(null, "width")); - //final int height = Integer.parseInt(parser.getAttributeValue(null, "height")); - - if (parser.next() == XmlPullParser.TEXT) { - iconURL = parser.getText(); - parser.nextTag(); - } - } - - private void skip(XmlPullParser parser) throws XmlPullParserException, IOException { - if (parser.getEventType() != XmlPullParser.START_TAG) { - throw new IllegalStateException(); - } - int depth = 1; - while (depth != 0) { - switch (parser.next()) { - case XmlPullParser.END_TAG: - depth--; - break; - case XmlPullParser.START_TAG: - depth++; - break; - } - } - } - - /** - * HACKS! We'll need to replace this with endpoints that return the correct content. - * - * Retrieve a JS snippet, in bookmarklet style, that can be used - * to modify the results page. - */ - public String getInjectableJs() { - final String css; - - if (identifier == null) { - css = ""; - } else if (identifier.equals("bing")) { - css = "#mHeader{display:none}#contentWrapper{margin-top:0}"; - } else if (identifier.equals("google")) { - css = "#sfcnt,#top_nav{display:none}"; - } else if (identifier.equals("yahoo")) { - css = "#nav,#header{display:none}"; - } else { - css = ""; - } - - return String.format(STYLE_INJECTION_SCRIPT, css); - } - - public String getIdentifier() { - return identifier; - } - - public String getName() { - return shortName; - } - - public String getIconURL() { - return iconURL; - } - - /** - * Finds the search query encoded in a given results URL. - * - * @param url Current results URL. - * @return The search query, or an empty string if a query couldn't be found. - */ - public String queryForResultsUrl(String url) { - final Uri resultsUri = getResultsUri(); - final Set names = StringUtils.getQueryParameterNames(resultsUri); - for (String name : names) { - if (resultsUri.getQueryParameter(name).matches(OS_PARAM_USER_DEFINED)) { - return Uri.parse(url).getQueryParameter(name); - } - } - return ""; - } - - /** - * Create a uri string that can be used to fetch the results page. - * - * @param query The user's query. This method will escape and encode the query. - */ - public String resultsUriForQuery(String query) { - final Uri resultsUri = getResultsUri(); - if (resultsUri == null) { - Log.e(LOG_TAG, "No results URL for search engine: " + shortName); - return ""; - } - final String template = Uri.decode(resultsUri.toString()); - return paramSubstitution(template, Uri.encode(query)); - } - - /** - * Create a uri string to fetch autocomplete suggestions. - * - * @param query The user's query. This method will escape and encode the query. - */ - public String getSuggestionTemplate(String query) { - if (suggestUri == null) { - Log.e(LOG_TAG, "No suggestions template for search engine: " + shortName); - return ""; - } - final String template = Uri.decode(suggestUri.toString()); - return paramSubstitution(template, Uri.encode(query)); - } - - /** - * @return Preferred results URI. - */ - private Uri getResultsUri() { - if (resultsUris.isEmpty()) { - return null; - } - return resultsUris.get(0); - } - - /** - * Formats template string with proper parameters. Modeled after - * ParamSubstitution in nsSearchService.js - * - * @param template - * @param query - * @return - */ - private String paramSubstitution(String template, String query) { - final String locale = Locale.getDefault().toString(); - - template = template.replaceAll(MOZ_PARAM_LOCALE, locale); - template = template.replaceAll(MOZ_PARAM_DIST_ID, ""); - template = template.replaceAll(MOZ_PARAM_OFFICIAL, "unofficial"); - - template = template.replaceAll(OS_PARAM_USER_DEFINED, query); - template = template.replaceAll(OS_PARAM_INPUT_ENCODING, "UTF-8"); - - template = template.replaceAll(OS_PARAM_LANGUAGE, locale); - template = template.replaceAll(OS_PARAM_OUTPUT_ENCODING, "UTF-8"); - - // Replace any optional parameters - template = template.replaceAll(OS_PARAM_OPTIONAL, ""); - - return template; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/search/SearchEngineManager.java b/mobile/android/base/java/org/mozilla/gecko/search/SearchEngineManager.java deleted file mode 100644 index 4b33db40a7..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/search/SearchEngineManager.java +++ /dev/null @@ -1,764 +0,0 @@ -/* 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.search; - -import android.content.Context; -import android.content.SharedPreferences; -import android.support.annotation.Nullable; -import android.support.annotation.UiThread; -import android.support.annotation.WorkerThread; -import android.text.TextUtils; -import android.util.Log; -import org.json.JSONException; -import org.json.JSONObject; -import org.mozilla.gecko.AppConstants; -import org.mozilla.gecko.GeckoProfile; -import org.mozilla.gecko.GeckoSharedPrefs; -import org.mozilla.gecko.Locales; -import org.mozilla.gecko.R; -import org.mozilla.gecko.distribution.Distribution; -import org.mozilla.gecko.util.FileUtils; -import org.mozilla.gecko.util.GeckoJarReader; -import org.mozilla.gecko.util.HardwareUtils; -import org.mozilla.gecko.util.RawResource; -import org.mozilla.gecko.util.ThreadUtils; -import org.xmlpull.v1.XmlPullParserException; - -import java.io.BufferedInputStream; -import java.io.BufferedReader; -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.OutputStream; -import java.io.UnsupportedEncodingException; -import java.lang.ref.WeakReference; -import java.net.HttpURLConnection; -import java.net.URL; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Locale; - -/** - * This class is not thread-safe, except where otherwise noted. - * - * This class contains a reference to {@link Context} - DO NOT LEAK! - */ -public class SearchEngineManager implements SharedPreferences.OnSharedPreferenceChangeListener { - private static final String LOG_TAG = "GeckoSearchEngineManager"; - - // Gecko pref that defines the name of the default search engine. - private static final String PREF_GECKO_DEFAULT_ENGINE = "browser.search.defaultenginename"; - - // Gecko pref that defines the name of the default searchplugin locale. - private static final String PREF_GECKO_DEFAULT_LOCALE = "distribution.searchplugins.defaultLocale"; - - // Key for shared preference that stores default engine name. - private static final String PREF_DEFAULT_ENGINE_KEY = "search.engines.defaultname"; - - // Key for shared preference that stores search region. - private static final String PREF_REGION_KEY = "search.region"; - - // URL for the geo-ip location service. Keep in sync with "browser.search.geoip.url" perference in Gecko. - private static final String GEOIP_LOCATION_URL = "https://location.services.mozilla.com/v1/country?key=" + AppConstants.MOZ_MOZILLA_API_KEY; - - // This should go through GeckoInterface to get the UA, but the search activity - // doesn't use a GeckoView yet. Until it does, get the UA directly. - private static final String USER_AGENT = HardwareUtils.isTablet() ? - AppConstants.USER_AGENT_FENNEC_TABLET : AppConstants.USER_AGENT_FENNEC_MOBILE; - - private final Context context; - private final Distribution distribution; - @Nullable private volatile SearchEngineCallback changeCallback; - @Nullable private volatile SearchEngine engine; - - // Cached version of default locale included in Gecko chrome manifest. - // This should only be accessed from the background thread. - private String fallbackLocale; - - // Cached version of default locale included in Distribution preferences. - // This should only be accessed from the background thread. - private String distributionLocale; - - public static interface SearchEngineCallback { - public void execute(@Nullable SearchEngine engine); - } - - public SearchEngineManager(Context context, Distribution distribution) { - this.context = context; - this.distribution = distribution; - GeckoSharedPrefs.forApp(context).registerOnSharedPreferenceChangeListener(this); - } - - /** - * Sets a callback to be called when the default engine changes. This can be called from any thread. - * - * @param changeCallback SearchEngineCallback to be called after the search engine - * changed. This will run on the UI thread. - * Note: callback may be called with null engine. - */ - public void setChangeCallback(SearchEngineCallback changeCallback) { - this.changeCallback = changeCallback; - } - - /** - * Perform an action with the user's default search engine. This can be called from any thread. - * - * @param callback The callback to be used with the user's default search engine. The call - * may be sync or async; if the call is async, it will be called on the - * ui thread. - */ - public void getEngine(SearchEngineCallback callback) { - if (engine != null) { - callback.execute(engine); - } else { - getDefaultEngine(callback); - } - } - - /** - * Should be called when the object goes out of scope. - */ - public void unregisterListeners() { - GeckoSharedPrefs.forApp(context).unregisterOnSharedPreferenceChangeListener(this); - } - - private volatile int ignorePreferenceChange = 0; - - @UiThread // according to the docs. - @Override - public void onSharedPreferenceChanged(final SharedPreferences sharedPreferences, final String key) { - if (!TextUtils.equals(PREF_DEFAULT_ENGINE_KEY, key)) { - return; - } - - if (ignorePreferenceChange > 0) { - ignorePreferenceChange--; - return; - } - - getDefaultEngine(changeCallback); - } - - /** - * Runs a SearchEngineCallback on the main thread. - */ - private void runCallback(final SearchEngine engine, @Nullable final SearchEngineCallback callback) { - ThreadUtils.postToUiThread(new RunCallbackUiThreadRunnable(this, engine, callback)); - } - - // Static is not strictly necessary but the outer class has a reference to Context so we should GC ASAP. - private static class RunCallbackUiThreadRunnable implements Runnable { - private final WeakReference searchEngineManagerWeakReference; - private final SearchEngine searchEngine; - private final SearchEngineCallback callback; - - public RunCallbackUiThreadRunnable(final SearchEngineManager searchEngineManager, final SearchEngine searchEngine, - final SearchEngineCallback callback) { - this.searchEngineManagerWeakReference = new WeakReference<>(searchEngineManager); - this.searchEngine = searchEngine; - this.callback = callback; - } - - @UiThread - @Override - public void run() { - final SearchEngineManager searchEngineManager = searchEngineManagerWeakReference.get(); - if (searchEngineManager == null) { - return; - } - - // Cache engine for future calls to getEngine. - searchEngineManager.engine = searchEngine; - if (callback != null) { - callback.execute(searchEngine); - } - - } - } - - /** - * This method finds and creates the default search engine. It will first look for - * the default engine name, then create the engine from that name. - * - * To find the default engine name, we first look in shared preferences, then - * the distribution (if one exists), and finally fall back to the localized default. - * - * @param callback SearchEngineCallback to be called after successfully looking - * up the search engine. This will run on the UI thread. - * Note: callback may be called with null engine. - */ - private void getDefaultEngine(final SearchEngineCallback callback) { - // This runnable is posted to the background thread. - distribution.addOnDistributionReadyCallback(new GetDefaultEngineDistributionCallbacks(this, callback)); - } - - // Static is not strictly necessary but the outer class contains a reference to Context so we should GC ASAP. - private static class GetDefaultEngineDistributionCallbacks implements Distribution.ReadyCallback { - private final WeakReference searchEngineManagerWeakReference; - private final SearchEngineCallback callback; - - public GetDefaultEngineDistributionCallbacks(final SearchEngineManager searchEngineManager, - final SearchEngineCallback callback) { - this.searchEngineManagerWeakReference = new WeakReference<>(searchEngineManager); - this.callback = callback; - } - - @Override - public void distributionNotFound() { - defaultBehavior(); - } - - @Override - public void distributionFound(Distribution distribution) { - defaultBehavior(); - } - - @Override - public void distributionArrivedLate(Distribution distribution) { - final SearchEngineManager searchEngineManager = searchEngineManagerWeakReference.get(); - if (searchEngineManager == null) { - return; - } - - // Let's see if there's a name in the distro. - // If so, just this once we'll override the saved value. - final String name = searchEngineManager.getDefaultEngineNameFromDistribution(); - - if (name == null) { - return; - } - - // Store the default engine name for the future. - // Increment an 'ignore' counter so that this preference change - // won't cause getDefaultEngine to be called again. - searchEngineManager.ignorePreferenceChange++; - GeckoSharedPrefs.forApp(searchEngineManager.context) - .edit() - .putString(PREF_DEFAULT_ENGINE_KEY, name) - .apply(); - - final SearchEngine engine = searchEngineManager.createEngineFromName(name); - searchEngineManager.runCallback(engine, callback); - } - - @WorkerThread // calling methods are @WorkerThread - private void defaultBehavior() { - final SearchEngineManager searchEngineManager = searchEngineManagerWeakReference.get(); - if (searchEngineManager == null) { - return; - } - - // First look for a default name stored in shared preferences. - String name = GeckoSharedPrefs.forApp(searchEngineManager.context).getString(PREF_DEFAULT_ENGINE_KEY, null); - - // Check for a region stored in shared preferences. If we don't have a region, - // we should force a recheck of the default engine. - String region = GeckoSharedPrefs.forApp(searchEngineManager.context).getString(PREF_REGION_KEY, null); - - if (name != null && region != null) { - Log.d(LOG_TAG, "Found default engine name in SharedPreferences: " + name); - } else { - // First, look for the default search engine in a distribution. - name = searchEngineManager.getDefaultEngineNameFromDistribution(); - if (name == null) { - // Otherwise, get the default engine that we ship. - name = searchEngineManager.getDefaultEngineNameFromLocale(); - } - - // Store the default engine name for the future. - // Increment an 'ignore' counter so that this preference change - // won't cause getDefaultEngine to be called again. - searchEngineManager.ignorePreferenceChange++; - GeckoSharedPrefs.forApp(searchEngineManager.context) - .edit() - .putString(PREF_DEFAULT_ENGINE_KEY, name) - .apply(); - } - - final SearchEngine engine = searchEngineManager.createEngineFromName(name); - searchEngineManager.runCallback(engine, callback); - } - } - - /** - * Looks for a default search engine included in a distribution. - * This method must be called after the distribution is ready. - * - * @return search engine name. - */ - private String getDefaultEngineNameFromDistribution() { - if (!distribution.exists()) { - return null; - } - - final File prefFile = distribution.getDistributionFile("preferences.json"); - if (prefFile == null) { - return null; - } - - try { - final JSONObject all = FileUtils.readJSONObjectFromFile(prefFile); - - // First, look for a default locale specified by the distribution. - if (all.has("Preferences")) { - final JSONObject prefs = all.getJSONObject("Preferences"); - if (prefs.has(PREF_GECKO_DEFAULT_LOCALE)) { - Log.d(LOG_TAG, "Found default searchplugin locale in distribution Preferences."); - distributionLocale = prefs.getString(PREF_GECKO_DEFAULT_LOCALE); - } - } - - // Then, check to see if there's a locale-specific default engine override. - final String languageTag = Locales.getLanguageTag(Locale.getDefault()); - final String overridesKey = "LocalizablePreferences." + languageTag; - if (all.has(overridesKey)) { - final JSONObject overridePrefs = all.getJSONObject(overridesKey); - if (overridePrefs.has(PREF_GECKO_DEFAULT_ENGINE)) { - Log.d(LOG_TAG, "Found default engine name in distribution LocalizablePreferences override."); - return overridePrefs.getString(PREF_GECKO_DEFAULT_ENGINE); - } - } - - // Next, check to see if there's a non-override default engine pref. - if (all.has("LocalizablePreferences")) { - final JSONObject localizablePrefs = all.getJSONObject("LocalizablePreferences"); - if (localizablePrefs.has(PREF_GECKO_DEFAULT_ENGINE)) { - Log.d(LOG_TAG, "Found default engine name in distribution LocalizablePreferences."); - return localizablePrefs.getString(PREF_GECKO_DEFAULT_ENGINE); - } - } - } catch (IOException e) { - Log.e(LOG_TAG, "Error getting search engine name from preferences.json", e); - } catch (JSONException e) { - Log.e(LOG_TAG, "Error parsing preferences.json", e); - } - return null; - } - - /** - * Helper function for converting an InputStream to a String. - * @param is InputStream you want to convert to a String - * - * @return String containing the data - */ - private String getHttpResponse(HttpURLConnection conn) { - InputStream is = null; - try { - is = new BufferedInputStream(conn.getInputStream()); - return new java.util.Scanner(is).useDelimiter("\\A").next(); - } catch (Exception e) { - return ""; - } finally { - if (is != null) { - try { - is.close(); - } catch (IOException e) { - Log.e(LOG_TAG, "Error closing InputStream", e); - } - } - } - } - - /** - * Gets the country code based on the current IP, using the Mozilla Location Service. - * We cache the country code in a shared preference, so we only fetch from the network - * once. - * - * @return String containing the country code - */ - private String fetchCountryCode() { - // First, we look to see if we have a cached code. - final String region = GeckoSharedPrefs.forApp(context).getString(PREF_REGION_KEY, null); - if (region != null) { - return region; - } - - // Since we didn't have a cached code, we need to fetch a code from the service. - try { - String responseText = null; - - URL url = new URL(GEOIP_LOCATION_URL); - HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection(); - try { - // POST an empty JSON object. - final String message = "{}"; - - urlConnection.setDoOutput(true); - urlConnection.setConnectTimeout(10000); - urlConnection.setReadTimeout(10000); - urlConnection.setRequestMethod("POST"); - urlConnection.setRequestProperty("User-Agent", USER_AGENT); - urlConnection.setRequestProperty("Content-Type", "application/json"); - urlConnection.setFixedLengthStreamingMode(message.getBytes().length); - - final OutputStream out = urlConnection.getOutputStream(); - out.write(message.getBytes()); - out.close(); - - responseText = getHttpResponse(urlConnection); - } finally { - urlConnection.disconnect(); - } - - if (responseText == null) { - Log.e(LOG_TAG, "Country code fetch failed"); - return null; - } - - // Extract the country code and save it for later in a cache. - final JSONObject response = new JSONObject(responseText); - return response.optString("country_code", null); - } catch (Exception e) { - Log.e(LOG_TAG, "Country code fetch failed", e); - } - - return null; - } - - /** - * Looks for the default search engine shipped in the locale. - * - * @return search engine name. - */ - private String getDefaultEngineNameFromLocale() { - try { - final JSONObject browsersearch = new JSONObject(RawResource.getAsString(context, R.raw.browsersearch)); - - // Get the region used to fence search engines. - String region = fetchCountryCode(); - - // Store the result, even if it's empty. If we fail to get a region, we never - // try to get it again, and we will always fallback to the non-region engine. - GeckoSharedPrefs.forApp(context) - .edit() - .putString(PREF_REGION_KEY, (region == null ? "" : region)) - .apply(); - - if (region != null) { - if (browsersearch.has("regions")) { - final JSONObject regions = browsersearch.getJSONObject("regions"); - if (regions.has(region)) { - final JSONObject regionData = regions.getJSONObject(region); - Log.d(LOG_TAG, "Found region-specific default engine name in browsersearch.json."); - return regionData.getString("default"); - } - } - } - - // Either we have no geoip region, or we didn't find the right region and we are falling back to the default. - if (browsersearch.has("default")) { - Log.d(LOG_TAG, "Found default engine name in browsersearch.json."); - return browsersearch.getString("default"); - } - } catch (IOException e) { - Log.e(LOG_TAG, "Error getting search engine name from browsersearch.json", e); - } catch (JSONException e) { - Log.e(LOG_TAG, "Error parsing browsersearch.json", e); - } - return null; - } - - /** - * Creates a SearchEngine instance from an engine name. - * - * To create the engine, we first try to find the search plugin in the distribution - * (if one exists), followed by the localized plugins we ship with the browser, and - * then finally third-party plugins that are installed in the profile directory. - * - * This method must be called after the distribution is ready. - * - * @param name The search engine name (e.g. "Google" or "Amazon.com") - * @return SearchEngine instance for name. - */ - private SearchEngine createEngineFromName(String name) { - // First, look in the distribution. - SearchEngine engine = createEngineFromDistribution(name); - - // Second, look in the jar for plugins shipped with the locale. - if (engine == null) { - engine = createEngineFromLocale(name); - } - - // Finally, look in the profile for third-party plugins. - if (engine == null) { - engine = createEngineFromProfile(name); - } - - if (engine == null) { - Log.e(LOG_TAG, "Could not create search engine from name: " + name); - } - - return engine; - } - - /** - * Creates a SearchEngine instance for a distribution search plugin. - * - * This method iterates through the distribution searchplugins directory, - * creating SearchEngine instances until it finds one with the right name. - * - * This method must be called after the distribution is ready. - * - * @param name Search engine name. - * @return SearchEngine instance for name. - */ - private SearchEngine createEngineFromDistribution(String name) { - if (!distribution.exists()) { - return null; - } - - final File pluginsDir = distribution.getDistributionFile("searchplugins"); - if (pluginsDir == null) { - return null; - } - - // Collect an array of files to scan using the same approach as - // DirectoryService._appendDistroSearchDirs which states: - // Common engines are loaded for all locales. If there is no locale directory for - // the current locale, there is a pref: "distribution.searchplugins.defaultLocale", - // which specifies a default locale to use. - ArrayList files = new ArrayList<>(); - - // Load files from the common folder first - final File[] commonFiles = (new File(pluginsDir, "common")).listFiles(); - if (commonFiles != null) { - Collections.addAll(files, commonFiles); - } - - // Next, check to see if there's a locale-specific override. - final File localeDir = new File(pluginsDir, "locale"); - if (localeDir != null) { - final String languageTag = Locales.getLanguageTag(Locale.getDefault()); - final File[] localeFiles = (new File(localeDir, languageTag)).listFiles(); - if (localeFiles != null) { - Collections.addAll(files, localeFiles); - } else { - // We didn't append the locale dir - try the default one. - if (distributionLocale != null) { - final File[] defaultLocaleFiles = (new File(localeDir, distributionLocale)).listFiles(); - if (defaultLocaleFiles != null) { - Collections.addAll(files, defaultLocaleFiles); - } - } - } - } - - if (files.isEmpty()) { - Log.e(LOG_TAG, "Could not find search plugin files in distribution directory"); - return null; - } - - return createEngineFromFileList(files.toArray(new File[files.size()]), name); - } - - /** - * Creates a SearchEngine instance for a search plugin shipped in the locale. - * - * This method reads the list of search plugin file names from list.txt, then - * iterates through the files, creating SearchEngine instances until it finds one - * with the right name. Unfortunately, we need to do this because there is no - * other way to map the search engine "name" to the file for the search plugin. - * - * @param name Search engine name. - * @return SearchEngine instance for name. - */ - private SearchEngine createEngineFromLocale(String name) { - final InputStream in = getInputStreamFromSearchPluginsJar("list.txt"); - if (in == null) { - return null; - } - final BufferedReader br = getBufferedReader(in); - - try { - String identifier; - while ((identifier = br.readLine()) != null) { - final InputStream pluginIn = getInputStreamFromSearchPluginsJar(identifier + ".xml"); - // pluginIn can be null if the xml file doesn't exist which - // can happen with :hidden plugins - if (pluginIn != null) { - final SearchEngine engine = createEngineFromInputStream(identifier, pluginIn); - if (engine != null && engine.getName().equals(name)) { - return engine; - } - } - } - } catch (Exception e) { - Log.e(LOG_TAG, "Error creating shipped search engine from name: " + name, e); - } finally { - try { - br.close(); - } catch (IOException e) { - // Ignore. - } - } - return null; - } - - /** - * Creates a SearchEngine instance for a search plugin in the profile directory. - * - * This method iterates through the profile searchplugins directory, creating - * SearchEngine instances until it finds one with the right name. - * - * @param name Search engine name. - * @return SearchEngine instance for name. - */ - private SearchEngine createEngineFromProfile(String name) { - final File pluginsDir = GeckoProfile.get(context).getFile("searchplugins"); - if (pluginsDir == null) { - return null; - } - - final File[] files = pluginsDir.listFiles(); - if (files == null) { - Log.e(LOG_TAG, "Could not find search plugin files in profile directory"); - return null; - } - return createEngineFromFileList(files, name); - } - - /** - * This method iterates through an array of search plugin files, creating - * SearchEngine instances until it finds one with the right name. - * - * @param files Array of search plugin files. Should not be null. - * @param name Search engine name. - * @return SearchEngine instance for name. - */ - private SearchEngine createEngineFromFileList(File[] files, String name) { - for (int i = 0; i < files.length; i++) { - try { - final FileInputStream fis = new FileInputStream(files[i]); - final SearchEngine engine = createEngineFromInputStream(null, fis); - if (engine != null && engine.getName().equals(name)) { - return engine; - } - } catch (IOException e) { - Log.e(LOG_TAG, "Error creating search engine from name: " + name, e); - } - } - return null; - } - - /** - * Creates a SearchEngine instance from an InputStream. - * - * This method closes the stream after it is done reading it. - * - * @param identifier Seach engine identifier. This only exists for search engines that - * ship with the default set of engines in the locale. - * @param in InputStream for search plugin XML file. - * @return SearchEngine instance. - */ - private SearchEngine createEngineFromInputStream(String identifier, InputStream in) { - try { - try { - return new SearchEngine(identifier, in); - } finally { - in.close(); - } - } catch (Exception e) { - Log.e(LOG_TAG, "Exception creating search engine", e); - } - - return null; - } - - /** - * Reads a file from the searchplugins directory in the Gecko jar. - * - * @param fileName name of the file to read. - * @return InputStream for file. - */ - private InputStream getInputStreamFromSearchPluginsJar(String fileName) { - final Locale locale = Locale.getDefault(); - - // First, try a file path for the full locale. - final String languageTag = Locales.getLanguageTag(locale); - String url = getSearchPluginsJarURL(context, languageTag, fileName); - - InputStream in = GeckoJarReader.getStream(context, url); - if (in != null) { - return in; - } - - // If that doesn't work, try a file path for just the language. - final String language = Locales.getLanguage(locale); - if (!languageTag.equals(language)) { - url = getSearchPluginsJarURL(context, language, fileName); - in = GeckoJarReader.getStream(context, url); - if (in != null) { - return in; - } - } - - // Finally, fall back to default locale defined in chrome registry. - url = getSearchPluginsJarURL(context, getFallbackLocale(), fileName); - return GeckoJarReader.getStream(context, url); - } - - /** - * Finds a fallback locale in the Gecko chrome registry. If a locale is declared - * here, we should be guaranteed to find a searchplugins directory for it. - * - * This method should only be accessed from the background thread. - */ - private String getFallbackLocale() { - if (fallbackLocale != null) { - return fallbackLocale; - } - - final InputStream in = GeckoJarReader.getStream( - context, GeckoJarReader.getJarURL(context, "chrome/chrome.manifest")); - if (in == null) { - return null; - } - final BufferedReader br = getBufferedReader(in); - - try { - String line; - while ((line = br.readLine()) != null) { - // We're looking for a line like "locale global en-US en-US/locale/en-US/global/" - // https://developer.mozilla.org/en/docs/Chrome_Registration#locale - if (line.startsWith("locale global ")) { - fallbackLocale = line.split(" ", 4)[2]; - break; - } - } - } catch (IOException e) { - Log.e(LOG_TAG, "Error reading fallback locale from chrome registry", e); - } finally { - try { - br.close(); - } catch (IOException e) { - // Ignore. - } - } - return fallbackLocale; - } - - /** - * Gets the jar URL for a file in the searchplugins directory. - * - * @param locale String representing the Gecko locale (e.g. "en-US"). - * @param fileName The name of the file to read. - * @return URL for jar file. - */ - private static String getSearchPluginsJarURL(Context context, String locale, String fileName) { - final String path = "chrome/" + locale + "/locale/" + locale + "/browser/searchplugins/" + fileName; - return GeckoJarReader.getJarURL(context, path); - } - - private BufferedReader getBufferedReader(InputStream in) { - try { - return new BufferedReader(new InputStreamReader(in, "UTF-8")); - } catch (UnsupportedEncodingException e) { - // Cannot happen. - return null; - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/tabqueue/TabQueueHelper.java b/mobile/android/base/java/org/mozilla/gecko/tabqueue/TabQueueHelper.java deleted file mode 100644 index 667eb8f6c4..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/tabqueue/TabQueueHelper.java +++ /dev/null @@ -1,357 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.tabqueue; - -import org.mozilla.gecko.AppConstants; -import org.mozilla.gecko.GeckoAppShell; -import org.mozilla.gecko.GeckoProfile; -import org.mozilla.gecko.GeckoSharedPrefs; -import org.mozilla.gecko.R; -import org.mozilla.gecko.preferences.GeckoPreferences; -import org.mozilla.gecko.util.ThreadUtils; - -import android.app.NotificationManager; -import android.app.PendingIntent; -import android.content.Context; -import android.content.Intent; -import android.content.SharedPreferences; -import android.content.res.Resources; -import android.graphics.PixelFormat; -import android.support.v4.app.NotificationCompat; -import android.support.v4.content.ContextCompat; -import android.text.TextUtils; -import android.util.Log; -import android.view.View; -import android.view.WindowManager; - -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - -import java.util.ArrayList; -import java.util.List; - -public class TabQueueHelper { - private static final String LOGTAG = "Gecko" + TabQueueHelper.class.getSimpleName(); - - // Disable Tab Queue for API level 10 (GB) - Bug 1206055 - public static final boolean TAB_QUEUE_ENABLED = true; - - public static final String FILE_NAME = "tab_queue_url_list.json"; - public static final String LOAD_URLS_ACTION = "TAB_QUEUE_LOAD_URLS_ACTION"; - public static final int TAB_QUEUE_NOTIFICATION_ID = R.id.tabQueueNotification; - - public static final String PREF_TAB_QUEUE_COUNT = "tab_queue_count"; - public static final String PREF_TAB_QUEUE_LAUNCHES = "tab_queue_launches"; - public static final String PREF_TAB_QUEUE_TIMES_PROMPT_SHOWN = "tab_queue_times_prompt_shown"; - - public static final int MAX_TIMES_TO_SHOW_PROMPT = 3; - public static final int EXTERNAL_LAUNCHES_BEFORE_SHOWING_PROMPT = 3; - - // result codes for returning from the prompt - public static final int TAB_QUEUE_YES = 201; - public static final int TAB_QUEUE_NO = 202; - - /** - * Checks if the specified context can draw on top of other apps. As of API level 23, an app - * cannot draw on top of other apps unless it declares the SYSTEM_ALERT_WINDOW permission in - * its manifest, AND the user specifically grants the app this capability. - * - * @return true if the specified context can draw on top of other apps, false otherwise. - */ - public static boolean canDrawOverlays(Context context) { - if (AppConstants.Versions.preMarshmallow) { - return true; // We got the permission at install time. - } - - // It would be nice to just use Settings.canDrawOverlays() - but this helper is buggy for - // apps using sharedUserId (See bug 1244722). - // Instead we'll add and remove an invisible view. If this is successful then we seem to - // have permission to draw overlays. - - View view = new View(context); - view.setVisibility(View.INVISIBLE); - - WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams( - 1, 1, - WindowManager.LayoutParams.TYPE_PHONE, - WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE, - PixelFormat.TRANSLUCENT); - - WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); - - try { - windowManager.addView(view, layoutParams); - windowManager.removeView(view); - return true; - } catch (final SecurityException | WindowManager.BadTokenException e) { - return false; - } - } - - /** - * Check if we should show the tab queue prompt - * - * @param context - * @return true if we should display the prompt, false if not. - */ - public static boolean shouldShowTabQueuePrompt(Context context) { - final SharedPreferences prefs = GeckoSharedPrefs.forApp(context); - - int numberOfTimesTabQueuePromptSeen = prefs.getInt(PREF_TAB_QUEUE_TIMES_PROMPT_SHOWN, 0); - - // Exit early if the feature is already enabled or the user has seen the - // prompt more than MAX_TIMES_TO_SHOW_PROMPT times. - if (isTabQueueEnabled(prefs) || numberOfTimesTabQueuePromptSeen >= MAX_TIMES_TO_SHOW_PROMPT) { - return false; - } - - final int viewActionIntentLaunches = prefs.getInt(PREF_TAB_QUEUE_LAUNCHES, 0) + 1; - if (viewActionIntentLaunches < EXTERNAL_LAUNCHES_BEFORE_SHOWING_PROMPT) { - // Allow a few external links to open before we prompt the user. - prefs.edit().putInt(PREF_TAB_QUEUE_LAUNCHES, viewActionIntentLaunches).apply(); - } else if (viewActionIntentLaunches == EXTERNAL_LAUNCHES_BEFORE_SHOWING_PROMPT) { - // Reset to avoid repeatedly showing the prompt if the user doesn't interact with it and - // we get more external VIEW action intents in. - final SharedPreferences.Editor editor = prefs.edit(); - editor.remove(TabQueueHelper.PREF_TAB_QUEUE_LAUNCHES); - - int timesPromptShown = prefs.getInt(TabQueueHelper.PREF_TAB_QUEUE_TIMES_PROMPT_SHOWN, 0) + 1; - editor.putInt(TabQueueHelper.PREF_TAB_QUEUE_TIMES_PROMPT_SHOWN, timesPromptShown); - editor.apply(); - - // Show the prompt - return true; - } - - return false; - } - - /** - * Reads file and converts any content to JSON, adds passed in URL to the data and writes back to the file, - * creating the file if it doesn't already exist. This should not be run on the UI thread. - * - * @param profile - * @param url URL to add - * @param filename filename to add URL to - * @return the number of tabs currently queued - */ - public static int queueURL(final GeckoProfile profile, final String url, final String filename) { - ThreadUtils.assertNotOnUiThread(); - - JSONArray jsonArray = profile.readJSONArrayFromFile(filename); - - jsonArray.put(url); - - profile.writeFile(filename, jsonArray.toString()); - - return jsonArray.length(); - } - - /** - * Remove a url from the file, if it exists. - * If the url exists multiple times, all instances of it will be removed. - * This should not be run on the UI thread. - * - * @param context - * @param urlToRemove URL to remove - * @param filename filename to remove URL from - * @return the number of queued urls - */ - public static int removeURLFromFile(final Context context, final String urlToRemove, final String filename) { - ThreadUtils.assertNotOnUiThread(); - - final GeckoProfile profile = GeckoProfile.get(context); - - JSONArray jsonArray = profile.readJSONArrayFromFile(filename); - JSONArray newArray = new JSONArray(); - String url; - - // Since JSONArray.remove was only added in API 19, we have to use two arrays in order to remove. - for (int i = 0; i < jsonArray.length(); i++) { - try { - url = jsonArray.getString(i); - } catch (JSONException e) { - url = ""; - } - if (!TextUtils.isEmpty(url) && !urlToRemove.equals(url)) { - newArray.put(url); - } - } - - profile.writeFile(filename, newArray.toString()); - - final SharedPreferences prefs = GeckoSharedPrefs.forApp(context); - prefs.edit().putInt(PREF_TAB_QUEUE_COUNT, newArray.length()).apply(); - - return newArray.length(); - } - - /** - * Get up to eight of the last queued URLs for displaying in the notification. - */ - public static List getLastURLs(final Context context, final String filename) { - final GeckoProfile profile = GeckoProfile.get(context); - final JSONArray jsonArray = profile.readJSONArrayFromFile(filename); - final List urls = new ArrayList<>(8); - - for (int i = 0; i < 8; i++) { - try { - urls.add(jsonArray.getString(i)); - } catch (JSONException e) { - Log.w(LOGTAG, "Unable to parse URL from tab queue array", e); - } - } - - return urls; - } - - /** - * Displays a notification showing the total number of tabs queue. If there is already a notification displayed, it - * will be replaced. - * - * @param context - * @param tabsQueued - */ - public static void showNotification(final Context context, final int tabsQueued, final List urls) { - ThreadUtils.assertNotOnUiThread(); - - Intent resultIntent = new Intent(); - resultIntent.setClassName(context, AppConstants.MOZ_ANDROID_BROWSER_INTENT_CLASS); - resultIntent.setAction(TabQueueHelper.LOAD_URLS_ACTION); - - PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, resultIntent, PendingIntent.FLAG_CANCEL_CURRENT); - - final String text; - final Resources resources = context.getResources(); - if (tabsQueued == 1) { - text = resources.getString(R.string.tab_queue_notification_text_singular); - } else { - text = resources.getString(R.string.tab_queue_notification_text_plural, tabsQueued); - } - - NotificationCompat.InboxStyle inboxStyle = new NotificationCompat.InboxStyle(); - inboxStyle.setBigContentTitle(text); - for (String url : urls) { - inboxStyle.addLine(url); - } - inboxStyle.setSummaryText(resources.getString(R.string.tab_queue_notification_title)); - - NotificationCompat.Builder builder = new NotificationCompat.Builder(context) - .setSmallIcon(R.drawable.ic_status_logo) - .setContentTitle(text) - .setContentText(resources.getString(R.string.tab_queue_notification_title)) - .setStyle(inboxStyle) - .setColor(ContextCompat.getColor(context, R.color.fennec_ui_orange)) - .setNumber(tabsQueued) - .setContentIntent(pendingIntent); - - NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); - notificationManager.notify(TabQueueHelper.TAB_QUEUE_NOTIFICATION_ID, builder.build()); - } - - public static boolean shouldOpenTabQueueUrls(final Context context) { - ThreadUtils.assertNotOnUiThread(); - - // TODO: Use profile shared prefs when bug 1147925 gets fixed. - final SharedPreferences prefs = GeckoSharedPrefs.forApp(context); - - int tabsQueued = prefs.getInt(PREF_TAB_QUEUE_COUNT, 0); - - return isTabQueueEnabled(prefs) && tabsQueued > 0; - } - - public static int getTabQueueLength(final Context context) { - ThreadUtils.assertNotOnUiThread(); - - // TODO: Use profile shared prefs when bug 1147925 gets fixed. - final SharedPreferences prefs = GeckoSharedPrefs.forApp(context); - return prefs.getInt(PREF_TAB_QUEUE_COUNT, 0); - } - - public static void openQueuedUrls(final Context context, final GeckoProfile profile, final String filename, boolean shouldPerformJavaScriptCallback) { - ThreadUtils.assertNotOnUiThread(); - - removeNotification(context); - - // exit early if we don't have any tabs queued - if (getTabQueueLength(context) < 1) { - return; - } - - JSONArray jsonArray = profile.readJSONArrayFromFile(filename); - - if (jsonArray.length() > 0) { - JSONObject data = new JSONObject(); - try { - data.put("urls", jsonArray); - data.put("shouldNotifyTabsOpenedToJava", shouldPerformJavaScriptCallback); - GeckoAppShell.notifyObservers("Tabs:OpenMultiple", data.toString()); - } catch (JSONException e) { - // Don't exit early as we perform cleanup at the end of this function. - Log.e(LOGTAG, "Error sending tab queue data", e); - } - } - - try { - profile.deleteFileFromProfileDir(filename); - } catch (IllegalArgumentException e) { - Log.e(LOGTAG, "Error deleting Tab Queue data file.", e); - } - - // TODO: Use profile shared prefs when bug 1147925 gets fixed. - final SharedPreferences prefs = GeckoSharedPrefs.forApp(context); - prefs.edit().remove(PREF_TAB_QUEUE_COUNT).apply(); - } - - protected static void removeNotification(Context context) { - NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); - notificationManager.cancel(TAB_QUEUE_NOTIFICATION_ID); - } - - public static boolean processTabQueuePromptResponse(int resultCode, Context context) { - final SharedPreferences prefs = GeckoSharedPrefs.forApp(context); - final SharedPreferences.Editor editor = prefs.edit(); - - switch (resultCode) { - case TAB_QUEUE_YES: - editor.putBoolean(GeckoPreferences.PREFS_TAB_QUEUE, true); - - // By making this one more than EXTERNAL_LAUNCHES_BEFORE_SHOWING_PROMPT we ensure the prompt - // will never show again without having to keep track of an extra pref. - editor.putInt(TabQueueHelper.PREF_TAB_QUEUE_LAUNCHES, - TabQueueHelper.EXTERNAL_LAUNCHES_BEFORE_SHOWING_PROMPT + 1); - break; - - case TAB_QUEUE_NO: - // The user clicked the 'no' button, so let's make sure the user never sees the prompt again by - // maxing out the pref used to count the VIEW action intents received and times they've seen the prompt. - - editor.putInt(TabQueueHelper.PREF_TAB_QUEUE_LAUNCHES, - TabQueueHelper.EXTERNAL_LAUNCHES_BEFORE_SHOWING_PROMPT + 1); - - editor.putInt(TabQueueHelper.PREF_TAB_QUEUE_TIMES_PROMPT_SHOWN, - TabQueueHelper.MAX_TIMES_TO_SHOW_PROMPT + 1); - break; - - default: - // We shouldn't ever get here. - Log.w(LOGTAG, "Unrecognized result code received from the tab queue prompt: " + resultCode); - } - - editor.apply(); - - return resultCode == TAB_QUEUE_YES; - } - - public static boolean isTabQueueEnabled(Context context) { - return isTabQueueEnabled(GeckoSharedPrefs.forApp(context)); - } - - public static boolean isTabQueueEnabled(SharedPreferences prefs) { - return prefs.getBoolean(GeckoPreferences.PREFS_TAB_QUEUE, false); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/tabqueue/TabQueuePrompt.java b/mobile/android/base/java/org/mozilla/gecko/tabqueue/TabQueuePrompt.java deleted file mode 100644 index ead16ccba1..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/tabqueue/TabQueuePrompt.java +++ /dev/null @@ -1,215 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.tabqueue; - -import org.mozilla.gecko.GeckoSharedPrefs; -import org.mozilla.gecko.Locales; -import org.mozilla.gecko.R; -import org.mozilla.gecko.Telemetry; -import org.mozilla.gecko.TelemetryContract; - -import android.annotation.TargetApi; -import android.content.Intent; -import android.net.Uri; -import android.os.Build; -import android.os.Bundle; -import android.os.Handler; -import android.provider.Settings; -import android.util.Log; -import android.view.MotionEvent; -import android.view.View; -import android.widget.Toast; - -import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; -import android.animation.AnimatorSet; -import android.animation.ObjectAnimator; - -public class TabQueuePrompt extends Locales.LocaleAwareActivity { - public static final String LOGTAG = "Gecko" + TabQueuePrompt.class.getSimpleName(); - - private static final int SETTINGS_REQUEST_CODE = 1; - - // Flag set during animation to prevent animation multiple-start. - private boolean isAnimating; - - private View containerView; - private View buttonContainer; - private View enabledConfirmation; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - showTabQueueEnablePrompt(); - } - - private void showTabQueueEnablePrompt() { - setContentView(R.layout.tab_queue_prompt); - - final View okButton = findViewById(R.id.ok_button); - okButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - onConfirmButtonPressed(); - Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.BUTTON, "tabqueue_prompt_yes"); - } - }); - findViewById(R.id.cancel_button).setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.BUTTON, "tabqueue_prompt_no"); - setResult(TabQueueHelper.TAB_QUEUE_NO); - finish(); - } - }); - final View settingsButton = findViewById(R.id.settings_button); - settingsButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - onSettingsButtonPressed(); - } - }); - - final View tipView = findViewById(R.id.tip_text); - final View settingsPermitView = findViewById(R.id.settings_permit_text); - - if (TabQueueHelper.canDrawOverlays(this)) { - okButton.setVisibility(View.VISIBLE); - settingsButton.setVisibility(View.GONE); - tipView.setVisibility(View.VISIBLE); - settingsPermitView.setVisibility(View.GONE); - } else { - okButton.setVisibility(View.GONE); - settingsButton.setVisibility(View.VISIBLE); - tipView.setVisibility(View.GONE); - settingsPermitView.setVisibility(View.VISIBLE); - } - - containerView = findViewById(R.id.tab_queue_container); - buttonContainer = findViewById(R.id.button_container); - enabledConfirmation = findViewById(R.id.enabled_confirmation); - - containerView.setTranslationY(500); - containerView.setAlpha(0); - - final Animator translateAnimator = ObjectAnimator.ofFloat(containerView, "translationY", 0); - translateAnimator.setDuration(400); - - final Animator alphaAnimator = ObjectAnimator.ofFloat(containerView, "alpha", 1); - alphaAnimator.setStartDelay(200); - alphaAnimator.setDuration(600); - - final AnimatorSet set = new AnimatorSet(); - set.playTogether(alphaAnimator, translateAnimator); - set.setStartDelay(400); - - set.start(); - } - - @Override - public void finish() { - super.finish(); - - // Don't perform an activity-dismiss animation. - overridePendingTransition(0, 0); - } - - private void onConfirmButtonPressed() { - enabledConfirmation.setVisibility(View.VISIBLE); - enabledConfirmation.setAlpha(0); - - final Animator buttonsAlphaAnimator = ObjectAnimator.ofFloat(buttonContainer, "alpha", 0); - buttonsAlphaAnimator.setDuration(300); - - final Animator messagesAlphaAnimator = ObjectAnimator.ofFloat(enabledConfirmation, "alpha", 1); - messagesAlphaAnimator.setDuration(300); - messagesAlphaAnimator.setStartDelay(200); - - final AnimatorSet set = new AnimatorSet(); - set.playTogether(buttonsAlphaAnimator, messagesAlphaAnimator); - - set.addListener(new AnimatorListenerAdapter() { - - @Override - public void onAnimationEnd(Animator animation) { - - new Handler().postDelayed(new Runnable() { - @Override - public void run() { - slideOut(); - setResult(TabQueueHelper.TAB_QUEUE_YES); - } - }, 1000); - } - }); - - set.start(); - } - - @TargetApi(Build.VERSION_CODES.M) - private void onSettingsButtonPressed() { - Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION); - intent.setData(Uri.parse("package:" + getPackageName())); - startActivityForResult(intent, SETTINGS_REQUEST_CODE); - - Toast.makeText(this, R.string.tab_queue_prompt_permit_drawing_over_apps, Toast.LENGTH_LONG).show(); - } - - @Override - protected void onActivityResult(int requestCode, int resultCode, Intent data) { - if (requestCode != SETTINGS_REQUEST_CODE) { - return; - } - - if (TabQueueHelper.canDrawOverlays(this)) { - // User granted the permission in Android's settings. - Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.BUTTON, "tabqueue_prompt_yes"); - - setResult(TabQueueHelper.TAB_QUEUE_YES); - finish(); - } - } - - /** - * Slide the overlay down off the screen and destroy it. - */ - private void slideOut() { - if (isAnimating) { - return; - } - - isAnimating = true; - - ObjectAnimator animator = ObjectAnimator.ofFloat(containerView, "translationY", containerView.getHeight()); - animator.addListener(new AnimatorListenerAdapter() { - - @Override - public void onAnimationEnd(Animator animation) { - finish(); - } - - }); - animator.start(); - } - - /** - * Close the dialog if back is pressed. - */ - @Override - public void onBackPressed() { - slideOut(); - } - - /** - * Close the dialog if the anything that isn't a button is tapped. - */ - @Override - public boolean onTouchEvent(MotionEvent event) { - slideOut(); - return true; - } -} \ No newline at end of file diff --git a/mobile/android/base/java/org/mozilla/gecko/tabqueue/TabQueueService.java b/mobile/android/base/java/org/mozilla/gecko/tabqueue/TabQueueService.java deleted file mode 100644 index ebb1bd761e..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/tabqueue/TabQueueService.java +++ /dev/null @@ -1,342 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.tabqueue; - -import org.mozilla.gecko.AppConstants; -import org.mozilla.gecko.GeckoProfile; -import org.mozilla.gecko.GeckoSharedPrefs; -import org.mozilla.gecko.R; -import org.mozilla.gecko.Telemetry; -import org.mozilla.gecko.TelemetryContract; -import org.mozilla.gecko.preferences.GeckoPreferences; - -import android.annotation.TargetApi; -import android.app.Notification; -import android.app.PendingIntent; -import android.app.Service; -import android.content.Context; -import android.content.Intent; -import android.content.SharedPreferences; -import android.content.res.Resources; -import android.graphics.PixelFormat; -import android.net.Uri; -import android.os.Build; -import android.os.Handler; -import android.os.HandlerThread; -import android.os.IBinder; -import android.provider.Settings; -import android.support.v4.app.NotificationCompat; -import android.support.v4.app.NotificationManagerCompat; -import android.text.TextUtils; -import android.util.Log; -import android.view.Gravity; -import android.view.LayoutInflater; -import android.view.View; -import android.view.WindowManager; -import android.widget.Button; -import android.widget.TextView; -import android.widget.Toast; -import org.mozilla.gecko.mozglue.SafeIntent; - -import java.util.List; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; - - -/** - * On launch this Service displays a View over the currently running process with an action to open the url in Fennec - * immediately. If the user takes no action, allowing the runnable to be processed after the specified - * timeout (TOAST_TIMEOUT), the url is added to a file which is then read in Fennec on next launch, this allows the - * user to quickly queue urls to open without having to open Fennec each time. If the Service receives an Intent whilst - * the created View is still active, the old url is immediately processed and the View is re-purposed with the new - * Intent data. - *

- * The SYSTEM_ALERT_WINDOW permission is used to allow us to insert a View from this Service which responds to user - * interaction, whilst still allowing whatever is in the background to be seen and interacted with. - *

- * Using an Activity to do this doesn't seem to work as there's an issue to do with the native android intent resolver - * dialog not being hidden when the toast is shown. Using an IntentService instead of a Service doesn't work as - * each new Intent received kicks off the IntentService lifecycle anew which means that a new View is created each time, - * meaning that we can't quickly queue the current data and re-purpose the View. The asynchronous nature of the - * IntentService is another prohibitive factor. - *

- * General approach taken is similar to the FB chat heads functionality: - * http://stackoverflow.com/questions/15975988/what-apis-in-android-is-facebook-using-to-create-chat-heads - */ -public class TabQueueService extends Service { - private static final String LOGTAG = "Gecko" + TabQueueService.class.getSimpleName(); - - private static final long TOAST_TIMEOUT = 3000; - private static final long TOAST_DOUBLE_TAP_TIMEOUT_MILLIS = 6000; - - private WindowManager windowManager; - private View toastLayout; - private Button openNowButton; - private Handler tabQueueHandler; - private WindowManager.LayoutParams toastLayoutParams; - private volatile StopServiceRunnable stopServiceRunnable; - private HandlerThread handlerThread; - private ExecutorService executorService; - - @Override - public IBinder onBind(Intent intent) { - // Not used - return null; - } - - @Override - public void onCreate() { - super.onCreate(); - executorService = Executors.newSingleThreadExecutor(); - - handlerThread = new HandlerThread("TabQueueHandlerThread"); - handlerThread.start(); - tabQueueHandler = new Handler(handlerThread.getLooper()); - - windowManager = (WindowManager) getSystemService(WINDOW_SERVICE); - - LayoutInflater layoutInflater = (LayoutInflater) getSystemService(LAYOUT_INFLATER_SERVICE); - toastLayout = layoutInflater.inflate(R.layout.tab_queue_toast, null); - - final Resources resources = getResources(); - - TextView messageView = (TextView) toastLayout.findViewById(R.id.toast_message); - messageView.setText(resources.getText(R.string.tab_queue_toast_message)); - - openNowButton = (Button) toastLayout.findViewById(R.id.toast_button); - openNowButton.setText(resources.getText(R.string.tab_queue_toast_action)); - - toastLayoutParams = new WindowManager.LayoutParams( - WindowManager.LayoutParams.MATCH_PARENT, - WindowManager.LayoutParams.WRAP_CONTENT, - WindowManager.LayoutParams.TYPE_PHONE, - WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL | - WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH | - WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, - PixelFormat.TRANSLUCENT); - - toastLayoutParams.gravity = Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL; - } - - @Override - public int onStartCommand(final Intent intent, final int flags, final int startId) { - // If this is a redelivery then lets bypass the entire double tap to open now code as that's a big can of worms, - // we also don't expect redeliveries because of the short time window associated with this feature. - if (flags != START_FLAG_REDELIVERY) { - final Context applicationContext = getApplicationContext(); - final SharedPreferences sharedPreferences = GeckoSharedPrefs.forApp(applicationContext); - - final String lastUrl = sharedPreferences.getString(GeckoPreferences.PREFS_TAB_QUEUE_LAST_SITE, ""); - - final SafeIntent safeIntent = new SafeIntent(intent); - final String intentUrl = safeIntent.getDataString(); - - final long lastRunTime = sharedPreferences.getLong(GeckoPreferences.PREFS_TAB_QUEUE_LAST_TIME, 0); - final boolean isWithinDoubleTapTimeLimit = System.currentTimeMillis() - lastRunTime < TOAST_DOUBLE_TAP_TIMEOUT_MILLIS; - - if (!TextUtils.isEmpty(lastUrl) && lastUrl.equals(intentUrl) && isWithinDoubleTapTimeLimit) { - // Background thread because we could do some file IO if we have to remove a url from the list. - tabQueueHandler.post(new Runnable() { - @Override - public void run() { - // If there is a runnable around, that means that the previous process hasn't yet completed, so - // we will need to prevent it from running and remove the view from the window manager. - // If there is no runnable around then the url has already been added to the list, so we'll - // need to remove it before proceeding or that url will open multiple times. - if (stopServiceRunnable != null) { - tabQueueHandler.removeCallbacks(stopServiceRunnable); - stopSelfResult(stopServiceRunnable.getStartId()); - stopServiceRunnable = null; - removeView(); - } else { - TabQueueHelper.removeURLFromFile(applicationContext, intentUrl, TabQueueHelper.FILE_NAME); - } - openNow(safeIntent.getUnsafe()); - - Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.INTENT, "tabqueue-doubletap"); - stopSelfResult(startId); - } - }); - - return START_REDELIVER_INTENT; - } - - sharedPreferences.edit().putString(GeckoPreferences.PREFS_TAB_QUEUE_LAST_SITE, intentUrl) - .putLong(GeckoPreferences.PREFS_TAB_QUEUE_LAST_TIME, System.currentTimeMillis()) - .apply(); - } - - if (stopServiceRunnable != null) { - // If we're already displaying a toast, keep displaying it but store the previous url. - // The open button will refer to the most recently opened link. - tabQueueHandler.removeCallbacks(stopServiceRunnable); - stopServiceRunnable.run(false); - } else { - try { - windowManager.addView(toastLayout, toastLayoutParams); - } catch (final SecurityException | WindowManager.BadTokenException e) { - Toast.makeText(this, getText(R.string.tab_queue_toast_message), Toast.LENGTH_SHORT).show(); - showSettingsNotification(); - } - } - - stopServiceRunnable = new StopServiceRunnable(startId) { - @Override - public void onRun() { - addURLToTabQueue(intent, TabQueueHelper.FILE_NAME); - stopServiceRunnable = null; - } - }; - - openNowButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(final View view) { - tabQueueHandler.removeCallbacks(stopServiceRunnable); - stopServiceRunnable = null; - removeView(); - openNow(intent); - - Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.INTENT, "tabqueue-now"); - stopSelfResult(startId); - } - }); - - tabQueueHandler.postDelayed(stopServiceRunnable, TOAST_TIMEOUT); - - return START_REDELIVER_INTENT; - } - - private void openNow(Intent intent) { - Intent forwardIntent = new Intent(intent); - forwardIntent.setClassName(getApplicationContext(), AppConstants.MOZ_ANDROID_BROWSER_INTENT_CLASS); - forwardIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - startActivity(forwardIntent); - - TabQueueHelper.removeNotification(getApplicationContext()); - - GeckoSharedPrefs.forApp(getApplicationContext()).edit().remove(GeckoPreferences.PREFS_TAB_QUEUE_LAST_SITE) - .remove(GeckoPreferences.PREFS_TAB_QUEUE_LAST_TIME) - .apply(); - - executorService.submit(new Runnable() { - @Override - public void run() { - int queuedTabCount = TabQueueHelper.getTabQueueLength(TabQueueService.this); - Telemetry.addToHistogram("FENNEC_TABQUEUE_QUEUESIZE", queuedTabCount); - } - }); - - } - - @TargetApi(Build.VERSION_CODES.M) - private void showSettingsNotification() { - if (AppConstants.Versions.preMarshmallow) { - return; - } - - final Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION); - intent.setData(Uri.parse("package:" + getPackageName())); - PendingIntent pendingIntent = PendingIntent.getActivity(this, intent.hashCode(), intent, 0); - - final String text = getString(R.string.tab_queue_notification_settings); - - final NotificationCompat.BigTextStyle style = new NotificationCompat.BigTextStyle() - .bigText(text); - - final Notification notification = new NotificationCompat.Builder(this) - .setContentTitle(getString(R.string.pref_tab_queue_title)) - .setContentText(text) - .setCategory(NotificationCompat.CATEGORY_ERROR) - .setStyle(style) - .setSmallIcon(R.drawable.ic_status_logo) - .setContentIntent(pendingIntent) - .setPriority(NotificationCompat.PRIORITY_MAX) - .setAutoCancel(true) - .addAction(R.drawable.ic_action_settings, getString(R.string.tab_queue_prompt_settings_button), pendingIntent) - .build(); - - NotificationManagerCompat.from(this).notify(R.id.tabQueueSettingsNotification, notification); - } - - private void removeView() { - try { - windowManager.removeView(toastLayout); - } catch (IllegalArgumentException | IllegalStateException e) { - // This can happen if the Service is killed by the system. If this happens the View will have already - // been removed but the runnable will have been kept alive. - Log.e(LOGTAG, "Error removing Tab Queue toast from service", e); - } - } - - private void addURLToTabQueue(final Intent intent, final String filename) { - if (intent == null) { - // This should never happen, but let's return silently instead of crashing if it does. - Log.w(LOGTAG, "Error adding URL to tab queue - invalid intent passed in."); - return; - } - final SafeIntent safeIntent = new SafeIntent(intent); - final String intentData = safeIntent.getDataString(); - - // As we're doing disk IO, let's run this stuff in a separate thread. - executorService.submit(new Runnable() { - @Override - public void run() { - Context applicationContext = getApplicationContext(); - final GeckoProfile profile = GeckoProfile.get(applicationContext); - int tabsQueued = TabQueueHelper.queueURL(profile, intentData, filename); - List urls = TabQueueHelper.getLastURLs(applicationContext, filename); - - TabQueueHelper.showNotification(applicationContext, tabsQueued, urls); - - // Store the number of URLs queued so that we don't have to read and process the file to see if we have - // any urls to open. - // TODO: Use profile shared prefs when bug 1147925 gets fixed. - final SharedPreferences prefs = GeckoSharedPrefs.forApp(applicationContext); - - prefs.edit().putInt(TabQueueHelper.PREF_TAB_QUEUE_COUNT, tabsQueued).apply(); - } - }); - } - - @Override - public void onDestroy() { - super.onDestroy(); - handlerThread.quit(); - } - - /** - * A modified Runnable which additionally removes the view from the window view hierarchy and stops the service - * when run, unless explicitly instructed not to. - */ - private abstract class StopServiceRunnable implements Runnable { - - private final int startId; - - public StopServiceRunnable(final int startId) { - this.startId = startId; - } - - public void run() { - run(true); - } - - public void run(final boolean shouldRemoveView) { - onRun(); - - if (shouldRemoveView) { - removeView(); - } - - stopSelfResult(startId); - } - - public int getStartId() { - return startId; - } - - public abstract void onRun(); - } -} \ No newline at end of file diff --git a/mobile/android/base/java/org/mozilla/gecko/tabqueue/TabReceivedService.java b/mobile/android/base/java/org/mozilla/gecko/tabqueue/TabReceivedService.java deleted file mode 100644 index 4f5baacdbd..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/tabqueue/TabReceivedService.java +++ /dev/null @@ -1,130 +0,0 @@ -/* 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.tabqueue; - -import org.mozilla.gecko.AppConstants; -import org.mozilla.gecko.BrowserLocaleManager; -import org.mozilla.gecko.GeckoSharedPrefs; -import org.mozilla.gecko.R; -import org.mozilla.gecko.db.BrowserContract; - -import android.app.IntentService; -import android.app.PendingIntent; -import android.content.Intent; -import android.content.SharedPreferences; -import android.content.res.Resources; -import android.database.Cursor; -import android.media.RingtoneManager; -import android.net.Uri; -import android.support.annotation.Nullable; -import android.support.annotation.WorkerThread; -import android.support.v4.app.NotificationCompat; -import android.support.v4.app.NotificationManagerCompat; -import android.util.Log; - -/** - * An IntentService that displays a notification for a tab sent to this device. - * - * The expected Intent should contain: - * * Data: URI to open in the notification - * * EXTRA_TITLE: Page title of the URI to open - */ -public class TabReceivedService extends IntentService { - private static final String LOGTAG = "Gecko" + TabReceivedService.class.getSimpleName(); - - private static final String PREF_NOTIFICATION_ID = "tab_received_notification_id"; - - private static final int MAX_NOTIFICATION_COUNT = 1000; - - public TabReceivedService() { - super(LOGTAG); - setIntentRedelivery(true); - } - - @Override - protected void onHandleIntent(final Intent intent) { - // IntentServices don't keep the process alive so - // we need to do this every time. Ideally, we wouldn't. - final Resources res = getResources(); - BrowserLocaleManager.getInstance().correctLocale(this, res, res.getConfiguration()); - - final String uri = intent.getDataString(); - if (uri == null) { - Log.d(LOGTAG, "Received null uri – ignoring"); - return; - } - - final Intent notificationIntent = new Intent(Intent.ACTION_VIEW, intent.getData()); - notificationIntent.putExtra(BrowserContract.SKIP_TAB_QUEUE_FLAG, true); - final PendingIntent contentIntent = PendingIntent.getActivity(this, 0, notificationIntent, 0); - - final String notificationTitle = getNotificationTitle(intent.getStringExtra(BrowserContract.EXTRA_CLIENT_GUID)); - final NotificationCompat.Builder builder = new NotificationCompat.Builder(this); - builder.setSmallIcon(R.drawable.flat_icon); - builder.setContentTitle(notificationTitle); - builder.setWhen(System.currentTimeMillis()); - builder.setAutoCancel(true); - builder.setContentText(uri); - builder.setContentIntent(contentIntent); - - // Trigger "heads-up" notification mode on supported Android versions. - builder.setPriority(NotificationCompat.PRIORITY_HIGH); - final Uri notificationSoundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION); - if (notificationSoundUri != null) { - builder.setSound(notificationSoundUri); - } - - final SharedPreferences prefs = GeckoSharedPrefs.forApp(this); - final int notificationId = getNextNotificationId(prefs.getInt(PREF_NOTIFICATION_ID, 0)); - final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this); - notificationManager.notify(notificationId, builder.build()); - - // Save the ID last so if the Service is killed and the Intent is redelivered, - // the ID is unlikely to have been updated and we would re-use the the old one. - // This would prevent two identical notifications from appearing if the - // notification was shown during the previous Intent processing attempt. - prefs.edit().putInt(PREF_NOTIFICATION_ID, notificationId).apply(); - } - - /** - * @param clientGUID the guid of the client in the clients table - * @return the client's name from the clients table, if possible, else the brand name. - */ - @WorkerThread - private String getNotificationTitle(@Nullable final String clientGUID) { - if (clientGUID == null) { - Log.w(LOGTAG, "Received null guid, using brand name."); - return AppConstants.MOZ_APP_DISPLAYNAME; - } - - final Cursor c = getContentResolver().query(BrowserContract.Clients.CONTENT_URI, - new String[] { BrowserContract.Clients.NAME }, - BrowserContract.Clients.GUID + "=?", new String[] { clientGUID }, null); - try { - if (c != null && c.moveToFirst()) { - return c.getString(c.getColumnIndex(BrowserContract.Clients.NAME)); - } else { - Log.w(LOGTAG, "Device not found, using brand name."); - return AppConstants.MOZ_APP_DISPLAYNAME; - } - } finally { - if (c != null) { - c.close(); - } - } - } - - /** - * Notification IDs must be unique else a notification - * will be overwritten so we cycle them. - */ - private int getNextNotificationId(final int currentId) { - if (currentId > MAX_NOTIFICATION_COUNT) { - return 0; - } else { - return currentId + 1; - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/tabs/PrivateTabsPanel.java b/mobile/android/base/java/org/mozilla/gecko/tabs/PrivateTabsPanel.java deleted file mode 100644 index b7bd833763..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/tabs/PrivateTabsPanel.java +++ /dev/null @@ -1,63 +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.tabs; - -import org.mozilla.gecko.R; -import org.mozilla.gecko.tabs.TabsPanel.CloseAllPanelView; -import org.mozilla.gecko.tabs.TabsPanel.TabsLayout; - -import android.content.Context; -import android.util.AttributeSet; -import android.view.LayoutInflater; -import android.view.View; -import android.widget.FrameLayout; - -/** - * A container that wraps the private tabs {@link android.widget.AdapterView} and empty - * {@link android.view.View} to manage both of their visibility states by changing the visibility of - * this container as calling {@link android.widget.AdapterView#setVisibility} does not affect the - * empty View's visibility. - */ -class PrivateTabsPanel extends FrameLayout implements CloseAllPanelView { - private final TabsLayout tabsLayout; - - public PrivateTabsPanel(final Context context, final AttributeSet attrs) { - super(context, attrs); - - LayoutInflater.from(context).inflate(R.layout.private_tabs_panel, this); - tabsLayout = (TabsLayout) findViewById(R.id.private_tabs_layout); - - final View emptyTabsFrame = findViewById(R.id.private_tabs_empty); - tabsLayout.setEmptyView(emptyTabsFrame); - } - - @Override - public void setTabsPanel(final TabsPanel panel) { - tabsLayout.setTabsPanel(panel); - } - - @Override - public void show() { - tabsLayout.show(); - setVisibility(View.VISIBLE); - } - - @Override - public void hide() { - setVisibility(View.GONE); - tabsLayout.hide(); - } - - @Override - public boolean shouldExpand() { - return tabsLayout.shouldExpand(); - } - - @Override - public void closeAll() { - tabsLayout.closeAll(); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/tabs/TabCurve.java b/mobile/android/base/java/org/mozilla/gecko/tabs/TabCurve.java deleted file mode 100644 index 0b6a30d7a0..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/tabs/TabCurve.java +++ /dev/null @@ -1,70 +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.tabs; - -import android.graphics.Path; - -/** - * Utility methods to draws Firefox's tab curve shape. - */ -public class TabCurve { - - public enum Direction { - LEFT(-1), - RIGHT(1); - - private final int value; - - private Direction(int value) { - this.value = value; - } - } - - // Curve's aspect ratio - private static final float ASPECT_RATIO = 0.729f; - - // Width multipliers - private static final float W_M1 = 0.343f; - private static final float W_M2 = 0.514f; - private static final float W_M3 = 0.49f; - private static final float W_M4 = 0.545f; - private static final float W_M5 = 0.723f; - - // Height multipliers - private static final float H_M1 = 0.25f; - private static final float H_M2 = 0.5f; - private static final float H_M3 = 0.72f; - private static final float H_M4 = 0.961f; - - private TabCurve() { - } - - public static float getWidthForHeight(float height) { - return (int) (height * ASPECT_RATIO); - } - - public static void drawFromTop(Path path, float from, float height, Direction dir) { - final float width = getWidthForHeight(height); - - path.cubicTo(from + width * W_M1 * dir.value, 0.0f, - from + width * W_M3 * dir.value, height * H_M1, - from + width * W_M2 * dir.value, height * H_M2); - path.cubicTo(from + width * W_M4 * dir.value, height * H_M3, - from + width * W_M5 * dir.value, height * H_M4, - from + width * dir.value, height); - } - - public static void drawFromBottom(Path path, float from, float height, Direction dir) { - final float width = getWidthForHeight(height); - - path.cubicTo(from + width * (1f - W_M5) * dir.value, height * H_M4, - from + width * (1f - W_M4) * dir.value, height * H_M3, - from + width * (1f - W_M2) * dir.value, height * H_M2); - path.cubicTo(from + width * (1f - W_M3) * dir.value, height * H_M1, - from + width * (1f - W_M1) * dir.value, 0.0f, - from + width * dir.value, 0.0f); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/tabs/TabHistoryController.java b/mobile/android/base/java/org/mozilla/gecko/tabs/TabHistoryController.java deleted file mode 100644 index 7b06c994cd..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/tabs/TabHistoryController.java +++ /dev/null @@ -1,87 +0,0 @@ -/* 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.tabs; - -import java.util.ArrayList; -import java.util.List; - -import org.json.JSONException; -import org.json.JSONObject; -import org.mozilla.gecko.GeckoAppShell; -import org.mozilla.gecko.Tab; -import org.mozilla.gecko.util.GeckoRequest; -import org.mozilla.gecko.util.NativeJSObject; - -import android.util.Log; - -public class TabHistoryController { - private static final String LOGTAG = "TabHistoryController"; - private final OnShowTabHistory showTabHistoryListener; - - public static enum HistoryAction { - ALL, - BACK, - FORWARD - }; - - public interface OnShowTabHistory { - void onShowHistory(List historyPageList, int toIndex); - } - - public TabHistoryController(OnShowTabHistory showTabHistoryListener) { - this.showTabHistoryListener = showTabHistoryListener; - } - - /** - * This method will show the history for the current tab. - */ - public boolean showTabHistory(final Tab tab, final HistoryAction action) { - JSONObject json = new JSONObject(); - try { - json.put("action", action.name()); - json.put("tabId", tab.getId()); - } catch (JSONException e) { - Log.e(LOGTAG, "JSON error", e); - } - - GeckoAppShell.sendRequestToGecko(new GeckoRequest("Session:GetHistory", json) { - @Override - public void onResponse(NativeJSObject nativeJSObject) { - /* - * The response from gecko request is of the form - * { - * "historyItems" : [ - * { - * "title": "google", - * "url": "google.com", - * "selected": false - * } - * ], - * toIndex = 1 - * } - */ - - final NativeJSObject[] historyItems = nativeJSObject.getObjectArray("historyItems"); - if (historyItems.length == 0) { - // Empty history, return without showing the popup. - return; - } - - final List historyPageList = new ArrayList<>(historyItems.length); - final int toIndex = nativeJSObject.getInt("toIndex"); - - for (NativeJSObject obj : historyItems) { - final String title = obj.getString("title"); - final String url = obj.getString("url"); - final boolean selected = obj.getBoolean("selected"); - historyPageList.add(new TabHistoryPage(title, url, selected)); - } - - showTabHistoryListener.onShowHistory(historyPageList, toIndex); - } - }); - return true; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/tabs/TabHistoryFragment.java b/mobile/android/base/java/org/mozilla/gecko/tabs/TabHistoryFragment.java deleted file mode 100644 index e6deabdcf9..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/tabs/TabHistoryFragment.java +++ /dev/null @@ -1,172 +0,0 @@ -/* 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.tabs; - -import java.util.ArrayList; -import java.util.List; - -import org.mozilla.gecko.GeckoAppShell; -import org.mozilla.gecko.GeckoApplication; -import org.mozilla.gecko.R; - -import android.content.Context; -import android.content.DialogInterface; -import android.os.Bundle; -import android.os.Parcelable; -import android.support.v4.app.Fragment; -import android.support.v4.app.FragmentManager; -import android.support.v4.app.FragmentTransaction; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.View; -import android.view.View.OnClickListener; -import android.view.ViewGroup; -import android.widget.AdapterView; -import android.widget.AdapterView.OnItemClickListener; -import android.widget.ArrayAdapter; -import android.widget.ListView; - -public class TabHistoryFragment extends Fragment implements OnItemClickListener, OnClickListener { - private static final String ARG_LIST = "historyPageList"; - private static final String ARG_INDEX = "index"; - private static final String BACK_STACK_ID = "backStateId"; - - private List historyPageList; - private int toIndex; - private ListView dialogList; - private int backStackId = -1; - private ViewGroup parent; - private boolean dismissed; - - public TabHistoryFragment() { - - } - - public static TabHistoryFragment newInstance(List historyPageList, int toIndex) { - final TabHistoryFragment fragment = new TabHistoryFragment(); - final Bundle args = new Bundle(); - args.putParcelableArrayList(ARG_LIST, (ArrayList) historyPageList); - args.putInt(ARG_INDEX, toIndex); - fragment.setArguments(args); - return fragment; - } - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - if (savedInstanceState != null) { - backStackId = savedInstanceState.getInt(BACK_STACK_ID, -1); - } - } - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - this.parent = container; - parent.setVisibility(View.VISIBLE); - View view = inflater.inflate(R.layout.tab_history_layout, container, false); - view.setOnClickListener(this); - dialogList = (ListView) view.findViewById(R.id.tab_history_list); - dialogList.setDivider(null); - return view; - } - - @Override - public void onActivityCreated(Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - Bundle bundle = getArguments(); - historyPageList = bundle.getParcelableArrayList(ARG_LIST); - toIndex = bundle.getInt(ARG_INDEX); - final ArrayAdapter urlAdapter = new TabHistoryAdapter(getActivity(), historyPageList); - dialogList.setAdapter(urlAdapter); - dialogList.setOnItemClickListener(this); - } - - @Override - public void onItemClick(AdapterView parent, View view, int position, long id) { - String index = String.valueOf(toIndex - position); - GeckoAppShell.notifyObservers("Session:Navigate", index); - dismiss(); - } - - @Override - public void onClick(View v) { - // Since the fragment view fills the entire screen, any clicks outside of the history - // ListView will end up here. - dismiss(); - } - - @Override - public void onPause() { - super.onPause(); - dismiss(); - } - - @Override - public void onDestroy() { - super.onDestroy(); - dismiss(); - - GeckoApplication.watchReference(getActivity(), this); - } - - @Override - public void onSaveInstanceState(Bundle outState) { - if (backStackId >= 0) { - outState.putInt(BACK_STACK_ID, backStackId); - } - } - - // Function to add this fragment to activity state with containerViewId as parent. - // This similar in functionality to DialogFragment.show() except that containerId is provided here. - public void show(final int containerViewId, final FragmentTransaction transaction, final String tag) { - dismissed = false; - transaction.add(containerViewId, this, tag); - transaction.addToBackStack(tag); - // Populating the tab history requires a gecko call (which can be slow) - therefore the app - // state by the time we try to show this fragment is unknown, and we could be in the - // middle of shutting down: - backStackId = transaction.commitAllowingStateLoss(); - } - - // Pop the fragment from backstack if it exists. - public void dismiss() { - if (dismissed) { - return; - } - - dismissed = true; - - if (backStackId >= 0) { - getFragmentManager().popBackStackImmediate(backStackId, FragmentManager.POP_BACK_STACK_INCLUSIVE); - backStackId = -1; - } - - if (parent != null) { - parent.setVisibility(View.GONE); - } - } - - private static class TabHistoryAdapter extends ArrayAdapter { - private final List pages; - private final Context context; - - public TabHistoryAdapter(Context context, List pages) { - super(context, R.layout.tab_history_item_row, pages); - this.context = context; - this.pages = pages; - } - - @Override - public View getView(int position, View convertView, ViewGroup parent) { - TabHistoryItemRow row = (TabHistoryItemRow) convertView; - if (row == null) { - row = new TabHistoryItemRow(context, null); - } - - row.update(pages.get(position), position == 0, position == pages.size() - 1); - return row; - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/tabs/TabHistoryItemRow.java b/mobile/android/base/java/org/mozilla/gecko/tabs/TabHistoryItemRow.java deleted file mode 100644 index 112dbc07dc..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/tabs/TabHistoryItemRow.java +++ /dev/null @@ -1,69 +0,0 @@ -/* 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.tabs; - -import android.content.Context; -import android.graphics.Typeface; -import android.util.AttributeSet; -import android.view.LayoutInflater; -import android.view.View; -import android.widget.ImageView; -import android.widget.RelativeLayout; -import android.widget.TextView; - -import org.mozilla.gecko.R; -import org.mozilla.gecko.icons.IconResponse; -import org.mozilla.gecko.icons.Icons; -import org.mozilla.gecko.util.ThreadUtils; -import org.mozilla.gecko.widget.FaviconView; - -import java.util.concurrent.Future; - -public class TabHistoryItemRow extends RelativeLayout { - private final FaviconView favicon; - private final TextView title; - private final ImageView timeLineTop; - private final ImageView timeLineBottom; - private Future ongoingIconLoad; - - public TabHistoryItemRow(Context context, AttributeSet attrs) { - super(context, attrs); - LayoutInflater.from(context).inflate(R.layout.tab_history_item_row, this); - favicon = (FaviconView) findViewById(R.id.tab_history_icon); - title = (TextView) findViewById(R.id.tab_history_title); - timeLineTop = (ImageView) findViewById(R.id.tab_history_timeline_top); - timeLineBottom = (ImageView) findViewById(R.id.tab_history_timeline_bottom); - } - - // Update the views with historic page detail. - public void update(final TabHistoryPage historyPage, boolean isFirstElement, boolean isLastElement) { - ThreadUtils.assertOnUiThread(); - - timeLineTop.setVisibility(isFirstElement ? View.INVISIBLE : View.VISIBLE); - timeLineBottom.setVisibility(isLastElement ? View.INVISIBLE : View.VISIBLE); - title.setText(historyPage.getTitle()); - - if (historyPage.isSelected()) { - // Highlight title with bold font. - title.setTypeface(null, Typeface.BOLD); - } else { - // Clear previously set bold font. - title.setTypeface(null, Typeface.NORMAL); - } - - favicon.setEnabled(historyPage.isSelected()); - favicon.clearImage(); - - if (ongoingIconLoad != null) { - ongoingIconLoad.cancel(true); - } - - ongoingIconLoad = Icons.with(getContext()) - .pageUrl(historyPage.getUrl()) - .skipNetwork() - .build() - .execute(favicon.createIconCallback()); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/tabs/TabHistoryPage.java b/mobile/android/base/java/org/mozilla/gecko/tabs/TabHistoryPage.java deleted file mode 100644 index 6c608b2ac7..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/tabs/TabHistoryPage.java +++ /dev/null @@ -1,60 +0,0 @@ -/* 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.tabs; - -import android.os.Parcel; -import android.os.Parcelable; - -public class TabHistoryPage implements Parcelable { - private final String title; - private final String url; - private final boolean selected; - - public TabHistoryPage(String title, String url, boolean selected) { - this.title = title; - this.url = url; - this.selected = selected; - } - - public String getTitle() { - return title; - } - - public String getUrl() { - return url; - } - - public boolean isSelected() { - return selected; - } - - @Override - public int describeContents() { - return 0; - } - - public void writeToParcel(Parcel dest, int flags) { - dest.writeString(title); - dest.writeString(url); - dest.writeInt(selected ? 1 : 0); - } - - public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { - @Override - public TabHistoryPage createFromParcel(final Parcel source) { - final String title = source.readString(); - final String url = source.readString(); - final boolean selected = source.readByte() != 0; - - final TabHistoryPage page = new TabHistoryPage(title, url, selected); - return page; - } - - @Override - public TabHistoryPage[] newArray(int size) { - return new TabHistoryPage[size]; - } - }; -} diff --git a/mobile/android/base/java/org/mozilla/gecko/tabs/TabPanelBackButton.java b/mobile/android/base/java/org/mozilla/gecko/tabs/TabPanelBackButton.java deleted file mode 100644 index 7ea02407e9..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/tabs/TabPanelBackButton.java +++ /dev/null @@ -1,55 +0,0 @@ -/* 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.tabs; - -import org.mozilla.gecko.R; - -import android.content.Context; -import android.content.res.TypedArray; -import android.graphics.Canvas; -import android.graphics.drawable.Drawable; -import android.util.AttributeSet; -import android.view.ViewGroup; -import android.widget.ImageButton; - -public class TabPanelBackButton extends ImageButton { - - private int dividerWidth = 0; - - private final Drawable divider; - private final int dividerPadding; - - public TabPanelBackButton(Context context, AttributeSet attrs) { - super(context, attrs); - - TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TabPanelBackButton); - divider = a.getDrawable(R.styleable.TabPanelBackButton_rightDivider); - dividerPadding = (int) a.getDimension(R.styleable.TabPanelBackButton_dividerVerticalPadding, 0); - a.recycle(); - - if (divider != null) { - dividerWidth = divider.getIntrinsicWidth(); - } - } - - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - super.onMeasure(widthMeasureSpec, heightMeasureSpec); - setMeasuredDimension(getMeasuredWidth() + dividerWidth, getMeasuredHeight()); - } - - @Override - protected void onDraw(Canvas canvas) { - super.onDraw(canvas); - if (divider != null) { - final ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) getLayoutParams(); - final int left = getRight() - lp.rightMargin - dividerWidth; - - divider.setBounds(left, getPaddingTop() + dividerPadding, - left + dividerWidth, getHeight() - getPaddingBottom() - dividerPadding); - divider.draw(canvas); - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/tabs/TabStrip.java b/mobile/android/base/java/org/mozilla/gecko/tabs/TabStrip.java deleted file mode 100644 index 5d37193433..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/tabs/TabStrip.java +++ /dev/null @@ -1,170 +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.tabs; - -import android.content.Context; -import android.graphics.drawable.Drawable; -import android.graphics.drawable.StateListDrawable; -import android.graphics.Rect; -import android.support.v4.content.ContextCompat; -import android.util.AttributeSet; -import android.view.LayoutInflater; -import android.view.TouchDelegate; -import android.view.View; -import android.view.ViewTreeObserver; - -import org.mozilla.gecko.BrowserApp.TabStripInterface; -import org.mozilla.gecko.R; -import org.mozilla.gecko.Tab; -import org.mozilla.gecko.Tabs; -import org.mozilla.gecko.widget.themed.ThemedImageButton; -import org.mozilla.gecko.widget.themed.ThemedLinearLayout; - -public class TabStrip extends ThemedLinearLayout - implements TabStripInterface { - private static final String LOGTAG = "GeckoTabStrip"; - - private final TabStripView tabStripView; - private final ThemedImageButton addTabButton; - - private final TabsListener tabsListener; - private OnTabAddedOrRemovedListener tabChangedListener; - - public TabStrip(Context context) { - this(context, null); - } - - public TabStrip(Context context, AttributeSet attrs) { - super(context, attrs); - setOrientation(HORIZONTAL); - - LayoutInflater.from(context).inflate(R.layout.tab_strip_inner, this); - tabStripView = (TabStripView) findViewById(R.id.tab_strip); - - addTabButton = (ThemedImageButton) findViewById(R.id.add_tab); - addTabButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - final Tabs tabs = Tabs.getInstance(); - if (isPrivateMode()) { - tabs.addPrivateTab(); - } else { - tabs.addTab(); - } - } - }); - - getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { - @Override - public boolean onPreDraw() { - getViewTreeObserver().removeOnPreDrawListener(this); - - final Rect r = new Rect(); - r.left = addTabButton.getRight(); - r.right = getWidth(); - r.top = 0; - r.bottom = getHeight(); - - // Redirect touch events between the 'new tab' button and the edge - // of the screen to the 'new tab' button. - setTouchDelegate(new TouchDelegate(r, addTabButton)); - - return true; - } - }); - - tabsListener = new TabsListener(); - } - - @Override - public void onAttachedToWindow() { - super.onAttachedToWindow(); - - Tabs.registerOnTabsChangedListener(tabsListener); - tabStripView.refreshTabs(); - } - - @Override - public void onDetachedFromWindow() { - super.onDetachedFromWindow(); - - Tabs.unregisterOnTabsChangedListener(tabsListener); - tabStripView.clearTabs(); - } - - @Override - public void setPrivateMode(boolean isPrivate) { - super.setPrivateMode(isPrivate); - addTabButton.setPrivateMode(isPrivate); - } - - public void setOnTabChangedListener(OnTabAddedOrRemovedListener listener) { - tabChangedListener = listener; - } - - private class TabsListener implements Tabs.OnTabsChangedListener { - @Override - public void onTabChanged(Tab tab, Tabs.TabEvents msg, String data) { - switch (msg) { - case RESTORED: - tabStripView.restoreTabs(); - break; - - case ADDED: - tabStripView.addTab(tab); - if (tabChangedListener != null) { - tabChangedListener.onTabChanged(); - } - break; - - case CLOSED: - tabStripView.removeTab(tab); - if (tabChangedListener != null) { - tabChangedListener.onTabChanged(); - } - break; - - case SELECTED: - // Update the selected position, then fall through... - tabStripView.selectTab(tab); - setPrivateMode(tab.isPrivate()); - case UNSELECTED: - // We just need to update the style for the unselected tab... - case TITLE: - case FAVICON: - case RECORDING_CHANGE: - case AUDIO_PLAYING_CHANGE: - tabStripView.updateTab(tab); - break; - } - } - } - - @Override - public void refresh() { - tabStripView.refresh(); - } - - @Override - public void onLightweightThemeChanged() { - final Drawable drawable = getTheme().getDrawable(this); - if (drawable == null) { - return; - } - - final StateListDrawable stateList = new StateListDrawable(); - stateList.addState(PRIVATE_STATE_SET, getColorDrawable(R.color.text_and_tabs_tray_grey)); - stateList.addState(EMPTY_STATE_SET, drawable); - - setBackgroundDrawable(stateList); - } - - @Override - public void onLightweightThemeReset() { - final int defaultBackgroundColor = ContextCompat.getColor(getContext(), R.color.text_and_tabs_tray_grey); - setBackgroundColor(defaultBackgroundColor); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/tabs/TabStripAdapter.java b/mobile/android/base/java/org/mozilla/gecko/tabs/TabStripAdapter.java deleted file mode 100644 index 8778aac312..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/tabs/TabStripAdapter.java +++ /dev/null @@ -1,98 +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.tabs; - -import android.content.Context; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.BaseAdapter; - -import java.util.ArrayList; -import java.util.List; - -import org.mozilla.gecko.R; -import org.mozilla.gecko.Tab; -import org.mozilla.gecko.Tabs; - -class TabStripAdapter extends BaseAdapter { - private static final String LOGTAG = "GeckoTabStripAdapter"; - - private final Context context; - private List tabs; - - public TabStripAdapter(Context context) { - this.context = context; - } - - @Override - public Tab getItem(int position) { - return (tabs != null && - position >= 0 && - position < tabs.size() ? tabs.get(position) : null); - } - - @Override - public long getItemId(int position) { - final Tab tab = getItem(position); - return (tab != null ? tab.getId() : -1); - } - - @Override - public boolean hasStableIds() { - return true; - } - - @Override - public View getView(int position, View convertView, ViewGroup parent) { - final TabStripItemView item; - if (convertView == null) { - item = (TabStripItemView) - LayoutInflater.from(context).inflate(R.layout.tab_strip_item, parent, false); - } else { - item = (TabStripItemView) convertView; - } - - final Tab tab = tabs.get(position); - item.updateFromTab(tab); - - return item; - } - - @Override - public int getCount() { - return (tabs != null ? tabs.size() : 0); - } - - int getPositionForTab(Tab tab) { - if (tabs == null || tab == null) { - return -1; - } - - return tabs.indexOf(tab); - } - - void removeTab(Tab tab) { - if (tabs == null) { - return; - } - - tabs.remove(tab); - notifyDataSetChanged(); - } - - void refresh(List tabs) { - // The list of tabs is guaranteed to be non-null. - // See TabStripView.refreshTabs(). - this.tabs = tabs; - notifyDataSetChanged(); - } - - void clear() { - tabs = null; - notifyDataSetInvalidated(); - } -} \ No newline at end of file diff --git a/mobile/android/base/java/org/mozilla/gecko/tabs/TabStripItemView.java b/mobile/android/base/java/org/mozilla/gecko/tabs/TabStripItemView.java deleted file mode 100644 index 27eaed125d..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/tabs/TabStripItemView.java +++ /dev/null @@ -1,254 +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.tabs; - -import org.mozilla.gecko.AboutPages; -import org.mozilla.gecko.GeckoAppShell; -import org.mozilla.gecko.R; -import org.mozilla.gecko.Tab; -import org.mozilla.gecko.Tabs; -import org.mozilla.gecko.widget.ResizablePathDrawable; -import org.mozilla.gecko.widget.ResizablePathDrawable.NonScaledPathShape; -import org.mozilla.gecko.widget.themed.ThemedImageButton; -import org.mozilla.gecko.widget.themed.ThemedLinearLayout; -import org.mozilla.gecko.widget.themed.ThemedTextView; - -import org.json.JSONException; -import org.json.JSONObject; - -import android.content.Context; -import android.content.res.ColorStateList; -import android.content.res.Resources; -import android.graphics.Bitmap; -import android.graphics.Canvas; -import android.graphics.Path; -import android.graphics.Region; -import android.util.AttributeSet; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.MotionEvent; -import android.view.View; -import android.widget.Checkable; -import android.widget.ImageView; - -public class TabStripItemView extends ThemedLinearLayout - implements Checkable { - private static final String LOGTAG = "GeckoTabStripItem"; - - private static final int[] STATE_CHECKED = { - android.R.attr.state_checked - }; - - private int id = -1; - private boolean checked; - - private final ImageView faviconView; - private final ThemedTextView titleView; - private final ThemedImageButton closeView; - - private final ResizablePathDrawable backgroundDrawable; - private final Region tabRegion; - private final Region tabClipRegion; - private boolean tabRegionNeedsUpdate; - - private final int faviconSize; - private Bitmap lastFavicon; - - public TabStripItemView(Context context) { - this(context, null); - } - - public TabStripItemView(Context context, AttributeSet attrs) { - super(context, attrs); - setOrientation(HORIZONTAL); - - tabRegion = new Region(); - tabClipRegion = new Region(); - - final Resources res = context.getResources(); - - final ColorStateList tabColors = - res.getColorStateList(R.color.tab_strip_item_bg); - backgroundDrawable = new ResizablePathDrawable(new TabCurveShape(), tabColors); - setBackgroundDrawable(backgroundDrawable); - - faviconSize = res.getDimensionPixelSize(R.dimen.browser_toolbar_favicon_size); - - LayoutInflater.from(context).inflate(R.layout.tab_strip_item_view, this); - setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - if (id < 0) { - throw new IllegalStateException("Invalid tab id:" + id); - } - - Tabs.getInstance().selectTab(id); - } - }); - - faviconView = (ImageView) findViewById(R.id.favicon); - titleView = (ThemedTextView) findViewById(R.id.title); - - closeView = (ThemedImageButton) findViewById(R.id.close); - closeView.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - if (id < 0) { - throw new IllegalStateException("Invalid tab id:" + id); - } - - final Tabs tabs = Tabs.getInstance(); - tabs.closeTab(tabs.getTab(id), true); - } - }); - } - - @Override - protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) { - super.onSizeChanged(width, height, oldWidth, oldHeight); - - // Queue a tab region update in the next draw() call. We don't - // update it immediately here because we need the new path from - // the background drawable to be updated first. - tabRegionNeedsUpdate = true; - } - - @Override - public boolean onTouchEvent(MotionEvent event) { - final int action = event.getActionMasked(); - final int x = (int) event.getX(); - final int y = (int) event.getY(); - - // Let motion events through if they're off the tab shape bounds. - if (action == MotionEvent.ACTION_DOWN && !tabRegion.contains(x, y)) { - return false; - } - - return super.onTouchEvent(event); - } - - @Override - public void draw(Canvas canvas) { - super.draw(canvas); - - if (tabRegionNeedsUpdate) { - final Path path = backgroundDrawable.getPath(); - tabClipRegion.set(0, 0, getWidth(), getHeight()); - tabRegion.setPath(path, tabClipRegion); - tabRegionNeedsUpdate = false; - } - } - - @Override - public int[] onCreateDrawableState(int extraSpace) { - final int[] drawableState = super.onCreateDrawableState(extraSpace + 1); - - if (checked) { - mergeDrawableStates(drawableState, STATE_CHECKED); - } - - return drawableState; - } - - @Override - public boolean isChecked() { - return checked; - } - - @Override - public void setChecked(boolean checked) { - if (this.checked == checked) { - return; - } - - this.checked = checked; - refreshDrawableState(); - } - - @Override - public void toggle() { - setChecked(!checked); - } - - @Override - public void setPressed(boolean pressed) { - super.setPressed(pressed); - - // The surrounding tab strip dividers need to be hidden - // when a tab item enters pressed state. - View parent = (View) getParent(); - if (parent != null) { - parent.invalidate(); - } - } - - void updateFromTab(Tab tab) { - if (tab == null) { - return; - } - - id = tab.getId(); - - updateTitle(tab); - updateFavicon(tab.getFavicon()); - setPrivateMode(tab.isPrivate()); - } - - private void updateTitle(Tab tab) { - final String title; - - // Avoid flickering the about:home URL on every load given how often - // this page is used in the UI. - if (AboutPages.isAboutHome(tab.getURL())) { - titleView.setText(R.string.home_title); - } else { - titleView.setText(tab.getDisplayTitle()); - } - - // TODO: Set content description to indicate audio is playing. - if (tab.isAudioPlaying()) { - titleView.setCompoundDrawablesWithIntrinsicBounds(R.drawable.tab_audio_playing, 0, 0, 0); - } else { - titleView.setCompoundDrawables(null, null, null, null); - } - } - - private void updateFavicon(final Bitmap favicon) { - if (favicon == null) { - lastFavicon = null; - faviconView.setImageResource(R.drawable.toolbar_favicon_default); - return; - } - if (favicon == lastFavicon) { - return; - } - - // Cache the original so we can debounce without scaling. - lastFavicon = favicon; - - final Bitmap scaledFavicon = - Bitmap.createScaledBitmap(favicon, faviconSize, faviconSize, false); - faviconView.setImageBitmap(scaledFavicon); - } - - private static class TabCurveShape extends NonScaledPathShape { - @Override - protected void onResize(float width, float height) { - final Path path = getPath(); - - path.reset(); - - final float curveWidth = TabCurve.getWidthForHeight(height); - - path.moveTo(0, height); - TabCurve.drawFromBottom(path, 0, height, TabCurve.Direction.RIGHT); - path.lineTo(width - curveWidth, 0); - - TabCurve.drawFromTop(path, width - curveWidth, height, TabCurve.Direction.RIGHT); - path.lineTo(0, height); - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/tabs/TabStripView.java b/mobile/android/base/java/org/mozilla/gecko/tabs/TabStripView.java deleted file mode 100644 index f3ec19cef6..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/tabs/TabStripView.java +++ /dev/null @@ -1,449 +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.tabs; - -import android.content.Context; -import android.content.res.Resources; -import android.graphics.Canvas; -import android.graphics.LinearGradient; -import android.graphics.Paint; -import android.graphics.Rect; -import android.graphics.Shader; -import android.graphics.drawable.Drawable; -import android.util.AttributeSet; -import android.view.animation.DecelerateInterpolator; -import android.view.View; -import android.view.ViewTreeObserver.OnPreDrawListener; - -import android.animation.Animator; -import android.animation.Animator.AnimatorListener; -import android.animation.AnimatorSet; -import android.animation.ObjectAnimator; - -import java.util.ArrayList; -import java.util.List; - -import org.mozilla.gecko.R; -import org.mozilla.gecko.Tab; -import org.mozilla.gecko.Tabs; -import org.mozilla.gecko.widget.TwoWayView; - -public class TabStripView extends TwoWayView { - private static final String LOGTAG = "GeckoTabStrip"; - - private static final int ANIM_TIME_MS = 200; - private static final DecelerateInterpolator ANIM_INTERPOLATOR = - new DecelerateInterpolator(); - - private final TabStripAdapter adapter; - private final Drawable divider; - - private final TabAnimatorListener animatorListener; - - private boolean isRestoringTabs; - - // Filled by calls to ShapeDrawable.getPadding(); - // saved to prevent allocation in draw(). - private final Rect dividerPadding = new Rect(); - - private boolean isPrivate; - - private final Paint fadingEdgePaint; - private final int fadingEdgeSize; - - public TabStripView(Context context, AttributeSet attrs) { - super(context, attrs); - - setOrientation(Orientation.HORIZONTAL); - setChoiceMode(ChoiceMode.SINGLE); - setItemsCanFocus(true); - setChildrenDrawingOrderEnabled(true); - setWillNotDraw(false); - - final Resources resources = getResources(); - - divider = resources.getDrawable(R.drawable.tab_strip_divider); - divider.getPadding(dividerPadding); - - final int itemMargin = - resources.getDimensionPixelSize(R.dimen.tablet_tab_strip_item_margin); - setItemMargin(itemMargin); - - animatorListener = new TabAnimatorListener(); - - fadingEdgePaint = new Paint(); - fadingEdgeSize = - resources.getDimensionPixelOffset(R.dimen.tablet_tab_strip_fading_edge_size); - - adapter = new TabStripAdapter(context); - setAdapter(adapter); - } - - private View getViewForTab(Tab tab) { - final int position = adapter.getPositionForTab(tab); - return getChildAt(position - getFirstVisiblePosition()); - } - - private int getPositionForSelectedTab() { - return adapter.getPositionForTab(Tabs.getInstance().getSelectedTab()); - } - - private void updateSelectedStyle(int selected) { - setItemChecked(selected, true); - } - - private void updateSelectedPosition(boolean ensureVisible) { - final int selected = getPositionForSelectedTab(); - if (selected != -1) { - updateSelectedStyle(selected); - - if (ensureVisible) { - ensurePositionIsVisible(selected, true); - } - } - } - - private void animateRemoveTab(Tab removedTab) { - final int removedPosition = adapter.getPositionForTab(removedTab); - - final View removedView = getViewForTab(removedTab); - - // The removed position might not have a matching child view - // when it's not within the visible range of positions in the strip. - if (removedView == null) { - return; - } - - // We don't animate the removed child view (it just disappears) - // but we still need its size of animate all affected children - // within the visible viewport. - final int removedSize = removedView.getWidth() + getItemMargin(); - - getViewTreeObserver().addOnPreDrawListener(new OnPreDrawListener() { - @Override - public boolean onPreDraw() { - getViewTreeObserver().removeOnPreDrawListener(this); - - final int firstPosition = getFirstVisiblePosition(); - final List childAnimators = new ArrayList(); - - final int childCount = getChildCount(); - for (int i = removedPosition - firstPosition; i < childCount; i++) { - final View child = getChildAt(i); - - final ObjectAnimator animator = - ObjectAnimator.ofFloat(child, "translationX", removedSize, 0); - childAnimators.add(animator); - } - - final AnimatorSet animatorSet = new AnimatorSet(); - animatorSet.playTogether(childAnimators); - animatorSet.setDuration(ANIM_TIME_MS); - animatorSet.setInterpolator(ANIM_INTERPOLATOR); - animatorSet.addListener(animatorListener); - - animatorSet.start(); - - return true; - } - }); - } - - private void animateNewTab(Tab newTab) { - final int newPosition = adapter.getPositionForTab(newTab); - if (newPosition < 0) { - return; - } - - getViewTreeObserver().addOnPreDrawListener(new OnPreDrawListener() { - @Override - public boolean onPreDraw() { - getViewTreeObserver().removeOnPreDrawListener(this); - - final int firstPosition = getFirstVisiblePosition(); - - final View newChild = getChildAt(newPosition - firstPosition); - if (newChild == null) { - return true; - } - - final List childAnimators = new ArrayList(); - childAnimators.add( - ObjectAnimator.ofFloat(newChild, "translationY", newChild.getHeight(), 0)); - - // This will momentaneously add a gap on the right side - // because TwoWayView doesn't provide APIs to control - // view recycling programatically to handle these transitory - // states in the container during animations. - - final int tabSize = newChild.getWidth(); - final int newIndex = newPosition - firstPosition; - final int childCount = getChildCount(); - for (int i = newIndex + 1; i < childCount; i++) { - final View child = getChildAt(i); - - childAnimators.add( - ObjectAnimator.ofFloat(child, "translationX", -tabSize, 0)); - } - - final AnimatorSet animatorSet = new AnimatorSet(); - animatorSet.playTogether(childAnimators); - animatorSet.setDuration(ANIM_TIME_MS); - animatorSet.setInterpolator(ANIM_INTERPOLATOR); - animatorSet.addListener(animatorListener); - - animatorSet.start(); - - return true; - } - }); - } - - private void animateRestoredTabs() { - getViewTreeObserver().addOnPreDrawListener(new OnPreDrawListener() { - @Override - public boolean onPreDraw() { - getViewTreeObserver().removeOnPreDrawListener(this); - - final List childAnimators = new ArrayList(); - - final int tabHeight = getHeight() - getPaddingTop() - getPaddingBottom(); - final int childCount = getChildCount(); - for (int i = 0; i < childCount; i++) { - final View child = getChildAt(i); - - childAnimators.add( - ObjectAnimator.ofFloat(child, "translationY", tabHeight, 0)); - } - - final AnimatorSet animatorSet = new AnimatorSet(); - animatorSet.playTogether(childAnimators); - animatorSet.setDuration(ANIM_TIME_MS); - animatorSet.setInterpolator(ANIM_INTERPOLATOR); - animatorSet.addListener(animatorListener); - - animatorSet.start(); - - return true; - } - }); - } - - /** - * Ensures the tab at the given position is visible. If we are not restoring tabs and - * shouldAnimate == true, the tab will animate to be visible, if it is not already visible. - */ - private void ensurePositionIsVisible(final int position, final boolean shouldAnimate) { - // We just want to move the strip to the right position - // when restoring tabs on startup. - if (isRestoringTabs || !shouldAnimate) { - setSelection(position); - return; - } - - getViewTreeObserver().addOnPreDrawListener(new OnPreDrawListener() { - @Override - public boolean onPreDraw() { - getViewTreeObserver().removeOnPreDrawListener(this); - smoothScrollToPosition(position); - return true; - } - }); - } - - private int getCheckedIndex(int childCount) { - final int checkedIndex = getCheckedItemPosition() - getFirstVisiblePosition(); - if (checkedIndex < 0 || checkedIndex > childCount - 1) { - return INVALID_POSITION; - } - - return checkedIndex; - } - - void refreshTabs() { - // Store a different copy of the tabs, so that we don't have - // to worry about accidentally updating it on the wrong thread. - final List tabs = new ArrayList(); - - for (Tab tab : Tabs.getInstance().getTabsInOrder()) { - if (tab.isPrivate() == isPrivate) { - tabs.add(tab); - } - } - - adapter.refresh(tabs); - updateSelectedPosition(true); - } - - void clearTabs() { - adapter.clear(); - } - - void restoreTabs() { - isRestoringTabs = true; - refreshTabs(); - animateRestoredTabs(); - isRestoringTabs = false; - } - - void addTab(Tab tab) { - // Refresh the list to make sure the new tab is - // added in the right position. - refreshTabs(); - animateNewTab(tab); - } - - void removeTab(Tab tab) { - animateRemoveTab(tab); - adapter.removeTab(tab); - updateSelectedPosition(false); - } - - void selectTab(Tab tab) { - if (tab.isPrivate() != isPrivate) { - isPrivate = tab.isPrivate(); - refreshTabs(); - } else { - updateSelectedPosition(true); - } - } - - void updateTab(Tab tab) { - final TabStripItemView item = (TabStripItemView) getViewForTab(tab); - if (item != null) { - item.updateFromTab(tab); - } - } - - private float getFadingEdgeStrength() { - final int childCount = getChildCount(); - if (childCount == 0) { - return 0.0f; - } else { - if (getFirstVisiblePosition() + childCount - 1 < adapter.getCount() - 1) { - return 1.0f; - } - - final int right = getChildAt(childCount - 1).getRight(); - final int paddingRight = getPaddingRight(); - final int width = getWidth(); - - final float strength = (right > width - paddingRight ? - (float) (right - width + paddingRight) / fadingEdgeSize : 0.0f); - - return Math.max(0.0f, Math.min(strength, 1.0f)); - } - } - - @Override - protected void onSizeChanged(int w, int h, int oldw, int oldh) { - super.onSizeChanged(w, h, oldw, oldh); - - fadingEdgePaint.setShader(new LinearGradient(w - fadingEdgeSize, 0, w, 0, - new int[] { 0x0, 0x11292C29, 0xDD292C29 }, - new float[] { 0, 0.4f, 1.0f }, Shader.TileMode.CLAMP)); - } - - @Override - protected int getChildDrawingOrder(int childCount, int i) { - final int checkedIndex = getCheckedIndex(childCount); - if (checkedIndex == INVALID_POSITION) { - return i; - } - - // Always draw the currently selected tab on top of all - // other child views so that its curve is fully visible. - if (i == childCount - 1) { - return checkedIndex; - } else if (checkedIndex <= i) { - return i + 1; - } else { - return i; - } - } - - private void drawDividers(Canvas canvas) { - final int bottom = getHeight() - getPaddingBottom() - dividerPadding.bottom; - final int top = bottom - divider.getIntrinsicHeight(); - - final int dividerWidth = divider.getIntrinsicWidth(); - final int itemMargin = getItemMargin(); - - final int childCount = getChildCount(); - final int checkedIndex = getCheckedIndex(childCount); - - for (int i = 1; i < childCount; i++) { - final View child = getChildAt(i); - - final boolean pressed = (child.isPressed() || getChildAt(i - 1).isPressed()); - final boolean checked = (i == checkedIndex || i == checkedIndex + 1); - - // Don't draw dividers for around checked or pressed items - // so that they are not drawn on top of the tab curves. - if (pressed || checked) { - continue; - } - - final int left = child.getLeft() - (itemMargin / 2) - dividerWidth; - final int right = left + dividerWidth; - - divider.setBounds(left, top, right, bottom); - divider.draw(canvas); - } - } - - private void drawFadingEdge(Canvas canvas) { - final float strength = getFadingEdgeStrength(); - if (strength > 0.0f) { - final int r = getRight(); - canvas.drawRect(r - fadingEdgeSize, getTop(), r, getBottom(), fadingEdgePaint); - fadingEdgePaint.setAlpha((int) (strength * 255)); - } - } - - @Override - public void draw(Canvas canvas) { - super.draw(canvas); - drawDividers(canvas); - drawFadingEdge(canvas); - } - - public void refresh() { - final int selectedPosition = getPositionForSelectedTab(); - if (selectedPosition != -1) { - ensurePositionIsVisible(selectedPosition, false); - } - } - - private class TabAnimatorListener implements AnimatorListener { - private void setLayerType(int layerType) { - final int childCount = getChildCount(); - for (int i = 0; i < childCount; i++) { - getChildAt(i).setLayerType(layerType, null); - } - } - - @Override - public void onAnimationStart(Animator animation) { - setLayerType(View.LAYER_TYPE_HARDWARE); - } - - @Override - public void onAnimationEnd(Animator animation) { - // This method is called even if the animator is canceled. - setLayerType(View.LAYER_TYPE_NONE); - } - - @Override - public void onAnimationRepeat(Animator animation) { - } - - @Override - public void onAnimationCancel(Animator animation) { - } - - } -} \ No newline at end of file diff --git a/mobile/android/base/java/org/mozilla/gecko/tabs/TabsGridLayout.java b/mobile/android/base/java/org/mozilla/gecko/tabs/TabsGridLayout.java deleted file mode 100644 index ead7db9fe6..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/tabs/TabsGridLayout.java +++ /dev/null @@ -1,712 +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.tabs; - -import org.mozilla.gecko.AppConstants; -import org.mozilla.gecko.GeckoAppShell; -import org.mozilla.gecko.R; -import org.mozilla.gecko.Tab; -import org.mozilla.gecko.Tabs; -import org.mozilla.gecko.animation.PropertyAnimator; -import org.mozilla.gecko.tabs.TabsPanel.TabsLayout; -import org.mozilla.gecko.widget.themed.ThemedRelativeLayout; - -import android.content.Context; -import android.content.res.Resources; -import android.content.res.TypedArray; -import android.graphics.PointF; -import android.graphics.Rect; -import android.os.Build; -import android.util.AttributeSet; -import android.util.SparseArray; -import android.view.MotionEvent; -import android.view.VelocityTracker; -import android.view.View; -import android.view.ViewConfiguration; -import android.view.ViewGroup; -import android.view.ViewTreeObserver; -import android.view.animation.DecelerateInterpolator; -import android.widget.AbsListView; -import android.widget.AdapterView; -import android.widget.Button; -import android.widget.GridView; -import android.animation.Animator; -import android.animation.AnimatorSet; -import android.animation.ObjectAnimator; -import android.animation.PropertyValuesHolder; -import android.animation.ValueAnimator; - -import java.util.ArrayList; -import java.util.List; - -/** - * A tabs layout implementation for the tablet redesign (bug 1014156) and later ported to mobile (bug 1193745). - */ - -class TabsGridLayout extends GridView - implements TabsLayout, - Tabs.OnTabsChangedListener { - - private static final String LOGTAG = "Gecko" + TabsGridLayout.class.getSimpleName(); - - public static final int ANIM_DELAY_MULTIPLE_MS = 20; - private static final int ANIM_TIME_MS = 200; - private static final DecelerateInterpolator ANIM_INTERPOLATOR = new DecelerateInterpolator(); - - private final SparseArray tabLocations = new SparseArray(); - private final boolean isPrivate; - private final TabsLayoutAdapter tabsAdapter; - private final int columnWidth; - private TabsPanel tabsPanel; - private int lastSelectedTabId; - - public TabsGridLayout(final Context context, final AttributeSet attrs) { - super(context, attrs, R.attr.tabGridLayoutViewStyle); - - TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TabsLayout); - isPrivate = (a.getInt(R.styleable.TabsLayout_tabs, 0x0) == 1); - a.recycle(); - - tabsAdapter = new TabsGridLayoutAdapter(context); - setAdapter(tabsAdapter); - - setRecyclerListener(new RecyclerListener() { - @Override - public void onMovedToScrapHeap(View view) { - TabsLayoutItemView item = (TabsLayoutItemView) view; - item.setThumbnail(null); - } - }); - - // The clipToPadding setting in the styles.xml doesn't seem to be working (bug 1101784) - // so lets set it manually in code for the moment as it's needed for the padding animation - setClipToPadding(false); - - setVerticalFadingEdgeEnabled(false); - - final Resources resources = getResources(); - columnWidth = resources.getDimensionPixelSize(R.dimen.tab_panel_column_width); - - final int padding = resources.getDimensionPixelSize(R.dimen.tab_panel_grid_padding); - final int paddingTop = resources.getDimensionPixelSize(R.dimen.tab_panel_grid_padding_top); - - // Lets set double the top padding on the bottom so that the last row shows up properly! - // Your demise, GridView, cannot come fast enough. - final int paddingBottom = paddingTop * 2; - - setPadding(padding, paddingTop, padding, paddingBottom); - - setOnItemClickListener(new OnItemClickListener() { - @Override - public void onItemClick(AdapterView parent, View view, int position, long id) { - final TabsLayoutItemView tabView = (TabsLayoutItemView) view; - final int tabId = tabView.getTabId(); - final Tab tab = Tabs.getInstance().selectTab(tabId); - if (tab == null) { - return; - } - autoHidePanel(); - Tabs.getInstance().notifyListeners(tab, Tabs.TabEvents.OPENED_FROM_TABS_TRAY); - } - }); - - TabSwipeGestureListener mSwipeListener = new TabSwipeGestureListener(); - setOnTouchListener(mSwipeListener); - setOnScrollListener(mSwipeListener.makeScrollListener()); - } - - private void populateTabLocations(final Tab removedTab) { - tabLocations.clear(); - - final int firstPosition = getFirstVisiblePosition(); - final int lastPosition = getLastVisiblePosition(); - final int numberOfColumns = getNumColumns(); - final int childCount = getChildCount(); - final int removedPosition = tabsAdapter.getPositionForTab(removedTab); - - for (int x = 1, i = (removedPosition - firstPosition) + 1; i < childCount; i++, x++) { - final View child = getChildAt(i); - if (child != null) { - // Reset the transformations here in case the user is swiping tabs away fast and they swipe a tab - // before the last animation has finished (bug 1179195). - resetTransforms(child); - - tabLocations.append(x, new PointF(child.getX(), child.getY())); - } - } - - final boolean firstChildOffScreen = ((firstPosition > 0) || getChildAt(0).getY() < 0); - final boolean lastChildVisible = (lastPosition - childCount == firstPosition - 1); - final boolean oneItemOnLastRow = (lastPosition % numberOfColumns == 0); - if (firstChildOffScreen && lastChildVisible && oneItemOnLastRow) { - // We need to set the view's bottom padding to prevent a sudden jump as the - // last item in the row is being removed. We then need to remove the padding - // via a sweet animation - - final int removedHeight = getChildAt(0).getMeasuredHeight(); - final int verticalSpacing = - getResources().getDimensionPixelOffset(R.dimen.tab_panel_grid_vspacing); - - ValueAnimator paddingAnimator = ValueAnimator.ofInt(getPaddingBottom() + removedHeight + verticalSpacing, getPaddingBottom()); - paddingAnimator.setDuration(ANIM_TIME_MS * 2); - - paddingAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { - - @Override - public void onAnimationUpdate(ValueAnimator animation) { - setPadding(getPaddingLeft(), getPaddingTop(), getPaddingRight(), (Integer) animation.getAnimatedValue()); - } - }); - paddingAnimator.start(); - } - } - - @Override - public void setTabsPanel(TabsPanel panel) { - tabsPanel = panel; - } - - @Override - public void show() { - setVisibility(View.VISIBLE); - Tabs.getInstance().refreshThumbnails(); - Tabs.registerOnTabsChangedListener(this); - refreshTabsData(); - - final Tab currentlySelectedTab = Tabs.getInstance().getSelectedTab(); - final int position = currentlySelectedTab != null ? tabsAdapter.getPositionForTab(currentlySelectedTab) : -1; - if (position != -1) { - final boolean selectionChanged = lastSelectedTabId != currentlySelectedTab.getId(); - final boolean positionIsVisible = position >= getFirstVisiblePosition() && position <= getLastVisiblePosition(); - - if (selectionChanged || !positionIsVisible) { - smoothScrollToPosition(position); - } - } - } - - @Override - public void hide() { - lastSelectedTabId = Tabs.getInstance().getSelectedTab().getId(); - setVisibility(View.GONE); - Tabs.unregisterOnTabsChangedListener(this); - GeckoAppShell.notifyObservers("Tab:Screenshot:Cancel", ""); - tabsAdapter.clear(); - } - - @Override - public boolean shouldExpand() { - return true; - } - - private void autoHidePanel() { - tabsPanel.autoHidePanel(); - } - - @Override - public void onTabChanged(Tab tab, Tabs.TabEvents msg, String data) { - switch (msg) { - case ADDED: - // Refresh only if panel is shown. show() will call refreshTabsData() later again. - if (tabsPanel.isShown()) { - // Refresh the list to make sure the new tab is added in the right position. - refreshTabsData(); - } - break; - - case CLOSED: - - // This is limited to >= ICS as animations on GB devices are generally pants - if (Build.VERSION.SDK_INT >= 11 && tabsAdapter.getCount() > 0) { - animateRemoveTab(tab); - } - - final Tabs tabsInstance = Tabs.getInstance(); - - if (tabsAdapter.removeTab(tab)) { - if (tab.isPrivate() == isPrivate && tabsAdapter.getCount() > 0) { - int selected = tabsAdapter.getPositionForTab(tabsInstance.getSelectedTab()); - updateSelectedStyle(selected); - } - if (!tab.isPrivate()) { - // Make sure we always have at least one normal tab - final Iterable tabs = tabsInstance.getTabsInOrder(); - boolean removedTabIsLastNormalTab = true; - for (Tab singleTab : tabs) { - if (!singleTab.isPrivate()) { - removedTabIsLastNormalTab = false; - break; - } - } - if (removedTabIsLastNormalTab) { - tabsInstance.addTab(); - } - } - } - break; - - case SELECTED: - // Update the selected position, then fall through... - updateSelectedPosition(); - case UNSELECTED: - // We just need to update the style for the unselected tab... - case THUMBNAIL: - case TITLE: - case RECORDING_CHANGE: - case AUDIO_PLAYING_CHANGE: - View view = getChildAt(tabsAdapter.getPositionForTab(tab) - getFirstVisiblePosition()); - if (view == null) - return; - - ((TabsLayoutItemView) view).assignValues(tab); - break; - } - } - - // Updates the selected position in the list so that it will be scrolled to the right place. - private void updateSelectedPosition() { - int selected = tabsAdapter.getPositionForTab(Tabs.getInstance().getSelectedTab()); - updateSelectedStyle(selected); - - if (selected != -1) { - setSelection(selected); - } - } - - /** - * Updates the selected/unselected style for the tabs. - * - * @param selected position of the selected tab - */ - private void updateSelectedStyle(final int selected) { - post(new Runnable() { - @Override - public void run() { - final int displayCount = tabsAdapter.getCount(); - - for (int i = 0; i < displayCount; i++) { - final Tab tab = tabsAdapter.getItem(i); - final boolean checked = displayCount == 1 || i == selected; - final View tabView = getViewForTab(tab); - if (tabView != null) { - ((TabsLayoutItemView) tabView).setChecked(checked); - } - // setItemChecked doesn't exist until API 11, despite what the API docs say! - setItemChecked(i, checked); - } - } - }); - } - - private void refreshTabsData() { - // Store a different copy of the tabs, so that we don't have to worry about - // accidentally updating it on the wrong thread. - ArrayList tabData = new ArrayList<>(); - - Iterable allTabs = Tabs.getInstance().getTabsInOrder(); - for (Tab tab : allTabs) { - if (tab.isPrivate() == isPrivate) - tabData.add(tab); - } - - tabsAdapter.setTabs(tabData); - updateSelectedPosition(); - } - - private void resetTransforms(View view) { - view.setAlpha(1); - view.setTranslationX(0); - view.setTranslationY(0); - - ((TabsLayoutItemView) view).setCloseVisible(true); - } - - @Override - public void closeAll() { - - autoHidePanel(); - - if (getChildCount() == 0) { - return; - } - - final Iterable tabs = Tabs.getInstance().getTabsInOrder(); - for (Tab tab : tabs) { - // In the normal panel we want to close all tabs (both private and normal), - // but in the private panel we only want to close private tabs. - if (!isPrivate || tab.isPrivate()) { - Tabs.getInstance().closeTab(tab, false); - } - } - } - - private View getViewForTab(Tab tab) { - final int position = tabsAdapter.getPositionForTab(tab); - return getChildAt(position - getFirstVisiblePosition()); - } - - void closeTab(View v) { - if (tabsAdapter.getCount() == 1) { - autoHidePanel(); - } - - TabsLayoutItemView itemView = (TabsLayoutItemView) v.getTag(); - Tab tab = Tabs.getInstance().getTab(itemView.getTabId()); - - Tabs.getInstance().closeTab(tab, true); - } - - private void animateRemoveTab(final Tab removedTab) { - final int removedPosition = tabsAdapter.getPositionForTab(removedTab); - - final View removedView = getViewForTab(removedTab); - - // The removed position might not have a matching child view - // when it's not within the visible range of positions in the strip. - if (removedView == null) { - return; - } - final int removedHeight = removedView.getMeasuredHeight(); - - populateTabLocations(removedTab); - - getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { - @Override - public boolean onPreDraw() { - getViewTreeObserver().removeOnPreDrawListener(this); - // We don't animate the removed child view (it just disappears) - // but we still need its size to animate all affected children - // within the visible viewport. - final int childCount = getChildCount(); - final int firstPosition = getFirstVisiblePosition(); - final int numberOfColumns = getNumColumns(); - - final List childAnimators = new ArrayList<>(); - - PropertyValuesHolder translateX, translateY; - for (int x = 0, i = removedPosition - firstPosition; i < childCount; i++, x++) { - final View child = getChildAt(i); - ObjectAnimator animator; - - if (i % numberOfColumns == numberOfColumns - 1) { - // Animate X & Y - translateX = PropertyValuesHolder.ofFloat("translationX", -(columnWidth * numberOfColumns), 0); - translateY = PropertyValuesHolder.ofFloat("translationY", removedHeight, 0); - animator = ObjectAnimator.ofPropertyValuesHolder(child, translateX, translateY); - } else { - // Just animate X - translateX = PropertyValuesHolder.ofFloat("translationX", columnWidth, 0); - animator = ObjectAnimator.ofPropertyValuesHolder(child, translateX); - } - animator.setStartDelay(x * ANIM_DELAY_MULTIPLE_MS); - childAnimators.add(animator); - } - - final AnimatorSet animatorSet = new AnimatorSet(); - animatorSet.playTogether(childAnimators); - animatorSet.setDuration(ANIM_TIME_MS); - animatorSet.setInterpolator(ANIM_INTERPOLATOR); - animatorSet.start(); - - // Set the starting position of the child views - because we are delaying the start - // of the animation, we need to prevent the items being drawn in their final position - // prior to the animation starting - for (int x = 1, i = (removedPosition - firstPosition) + 1; i < childCount; i++, x++) { - final View child = getChildAt(i); - - final PointF targetLocation = tabLocations.get(x + 1); - if (targetLocation == null) { - continue; - } - - child.setX(targetLocation.x); - child.setY(targetLocation.y); - } - - return true; - } - }); - } - - - private void animateCancel(final View view) { - PropertyAnimator animator = new PropertyAnimator(ANIM_TIME_MS); - animator.attach(view, PropertyAnimator.Property.ALPHA, 1); - animator.attach(view, PropertyAnimator.Property.TRANSLATION_X, 0); - - animator.addPropertyAnimationListener(new PropertyAnimator.PropertyAnimationListener() { - @Override - public void onPropertyAnimationStart() { - } - - @Override - public void onPropertyAnimationEnd() { - TabsLayoutItemView tab = (TabsLayoutItemView) view; - tab.setCloseVisible(true); - } - }); - - animator.start(); - } - - private class TabsGridLayoutAdapter extends TabsLayoutAdapter { - - final private Button.OnClickListener mCloseClickListener; - - public TabsGridLayoutAdapter(Context context) { - super(context, R.layout.tabs_layout_item_view); - - mCloseClickListener = new Button.OnClickListener() { - @Override - public void onClick(View v) { - closeTab(v); - } - }; - } - - @Override - TabsLayoutItemView newView(int position, ViewGroup parent) { - final TabsLayoutItemView item = super.newView(position, parent); - - item.setCloseOnClickListener(mCloseClickListener); - ((ThemedRelativeLayout) item.findViewById(R.id.wrapper)).setPrivateMode(isPrivate); - - return item; - } - - @Override - public void bindView(TabsLayoutItemView view, Tab tab) { - super.bindView(view, tab); - - // If we're recycling this view, there's a chance it was transformed during - // the close animation. Remove any of those properties. - resetTransforms(view); - } - } - - private class TabSwipeGestureListener implements View.OnTouchListener { - // same value the stock browser uses for after drag animation velocity in pixels/sec - // http://androidxref.com/4.0.4/xref/packages/apps/Browser/src/com/android/browser/NavTabScroller.java#61 - private static final float MIN_VELOCITY = 750; - - private final int mSwipeThreshold; - private final int mMinFlingVelocity; - - private final int mMaxFlingVelocity; - private VelocityTracker mVelocityTracker; - - private int mTabWidth = 1; - - private View mSwipeView; - private Runnable mPendingCheckForTap; - - private float mSwipeStartX; - private boolean mSwiping; - private boolean mEnabled; - - public TabSwipeGestureListener() { - mEnabled = true; - - ViewConfiguration vc = ViewConfiguration.get(TabsGridLayout.this.getContext()); - mSwipeThreshold = vc.getScaledTouchSlop(); - mMinFlingVelocity = (int) (TabsGridLayout.this.getContext().getResources().getDisplayMetrics().density * MIN_VELOCITY); - mMaxFlingVelocity = vc.getScaledMaximumFlingVelocity(); - } - - public void setEnabled(boolean enabled) { - mEnabled = enabled; - } - - public OnScrollListener makeScrollListener() { - return new OnScrollListener() { - @Override - public void onScrollStateChanged(AbsListView view, int scrollState) { - setEnabled(scrollState != GridView.OnScrollListener.SCROLL_STATE_TOUCH_SCROLL); - } - - @Override - public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { - - } - }; - } - - @Override - public boolean onTouch(View view, MotionEvent e) { - if (!mEnabled) { - return false; - } - - switch (e.getActionMasked()) { - case MotionEvent.ACTION_DOWN: { - // Check if we should set pressed state on the - // touched view after a standard delay. - triggerCheckForTap(); - - final float x = e.getRawX(); - final float y = e.getRawY(); - - // Find out which view is being touched - mSwipeView = findViewAt(x, y); - - if (mSwipeView != null) { - if (mTabWidth < 2) { - mTabWidth = mSwipeView.getWidth(); - } - - mSwipeStartX = e.getRawX(); - - mVelocityTracker = VelocityTracker.obtain(); - mVelocityTracker.addMovement(e); - } - - view.onTouchEvent(e); - return true; - } - - case MotionEvent.ACTION_UP: { - if (mSwipeView == null) { - break; - } - - cancelCheckForTap(); - mSwipeView.setPressed(false); - - if (!mSwiping) { - final TabsLayoutItemView item = (TabsLayoutItemView) mSwipeView; - final int tabId = item.getTabId(); - final Tab tab = Tabs.getInstance().selectTab(tabId); - if (tab != null) { - autoHidePanel(); - Tabs.getInstance().notifyListeners(tab, Tabs.TabEvents.OPENED_FROM_TABS_TRAY); - } - - mVelocityTracker.recycle(); - mVelocityTracker = null; - break; - } - - mVelocityTracker.addMovement(e); - mVelocityTracker.computeCurrentVelocity(1000, mMaxFlingVelocity); - - float velocityX = Math.abs(mVelocityTracker.getXVelocity()); - - boolean dismiss = false; - - float deltaX = mSwipeView.getTranslationX(); - - if (Math.abs(deltaX) > mTabWidth / 2) { - dismiss = true; - } else if (mMinFlingVelocity <= velocityX && velocityX <= mMaxFlingVelocity) { - dismiss = mSwiping && (deltaX * mVelocityTracker.getYVelocity() > 0); - } - if (dismiss) { - closeTab(mSwipeView.findViewById(R.id.close)); - } else { - animateCancel(mSwipeView); - } - mVelocityTracker.recycle(); - mVelocityTracker = null; - mSwipeView = null; - - mSwipeStartX = 0; - mSwiping = false; - } - - case MotionEvent.ACTION_MOVE: { - if (mSwipeView == null || mVelocityTracker == null) { - break; - } - - mVelocityTracker.addMovement(e); - - float delta = e.getRawX() - mSwipeStartX; - - boolean isScrollingX = Math.abs(delta) > mSwipeThreshold; - boolean isSwipingToClose = isScrollingX; - - // If we're actually swiping, make sure we don't - // set pressed state on the swiped view. - if (isScrollingX) { - cancelCheckForTap(); - } - - if (isSwipingToClose) { - mSwiping = true; - TabsGridLayout.this.requestDisallowInterceptTouchEvent(true); - - ((TabsLayoutItemView) mSwipeView).setCloseVisible(false); - - // Stops listview from highlighting the touched item - // in the list when swiping. - MotionEvent cancelEvent = MotionEvent.obtain(e); - cancelEvent.setAction(MotionEvent.ACTION_CANCEL | - (e.getActionIndex() << MotionEvent.ACTION_POINTER_INDEX_SHIFT)); - TabsGridLayout.this.onTouchEvent(cancelEvent); - cancelEvent.recycle(); - } - - if (mSwiping) { - mSwipeView.setTranslationX(delta); - - mSwipeView.setAlpha(Math.min(1f, 1f - 2f * Math.abs(delta) / mTabWidth)); - - return true; - } - - break; - } - } - return false; - } - - private View findViewAt(float rawX, float rawY) { - Rect rect = new Rect(); - - int[] listViewCoords = new int[2]; - TabsGridLayout.this.getLocationOnScreen(listViewCoords); - - int x = (int) rawX - listViewCoords[0]; - int y = (int) rawY - listViewCoords[1]; - - for (int i = 0; i < TabsGridLayout.this.getChildCount(); i++) { - View child = TabsGridLayout.this.getChildAt(i); - child.getHitRect(rect); - - if (rect.contains(x, y)) { - return child; - } - } - - return null; - } - - private void triggerCheckForTap() { - if (mPendingCheckForTap == null) { - mPendingCheckForTap = new CheckForTap(); - } - - TabsGridLayout.this.postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout()); - } - - private void cancelCheckForTap() { - if (mPendingCheckForTap == null) { - return; - } - - TabsGridLayout.this.removeCallbacks(mPendingCheckForTap); - } - - private class CheckForTap implements Runnable { - @Override - public void run() { - if (!mSwiping && mSwipeView != null && mEnabled) { - mSwipeView.setPressed(true); - } - } - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/tabs/TabsLayout.java b/mobile/android/base/java/org/mozilla/gecko/tabs/TabsLayout.java deleted file mode 100644 index d5362f1f10..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/tabs/TabsLayout.java +++ /dev/null @@ -1,216 +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.tabs; - -import org.mozilla.gecko.GeckoAppShell; -import org.mozilla.gecko.R; -import org.mozilla.gecko.Tab; -import org.mozilla.gecko.Tabs; -import org.mozilla.gecko.widget.RecyclerViewClickSupport; - -import android.content.Context; -import android.content.res.TypedArray; -import android.support.v7.widget.RecyclerView; -import android.util.AttributeSet; -import android.view.View; -import android.widget.Button; - -import java.util.ArrayList; - -public abstract class TabsLayout extends RecyclerView - implements TabsPanel.TabsLayout, - Tabs.OnTabsChangedListener, - RecyclerViewClickSupport.OnItemClickListener, - TabsTouchHelperCallback.DismissListener { - - private static final String LOGTAG = "Gecko" + TabsLayout.class.getSimpleName(); - - private final boolean isPrivate; - private TabsPanel tabsPanel; - private final TabsLayoutRecyclerAdapter tabsAdapter; - - public TabsLayout(Context context, AttributeSet attrs, int itemViewLayoutResId) { - super(context, attrs); - - TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TabsLayout); - isPrivate = (a.getInt(R.styleable.TabsLayout_tabs, 0x0) == 1); - a.recycle(); - - tabsAdapter = new TabsLayoutRecyclerAdapter(context, itemViewLayoutResId, isPrivate, - /* close on click listener */ - new Button.OnClickListener() { - @Override - public void onClick(View v) { - // The view here is the close button, which has a reference - // to the parent TabsLayoutItemView in its tag, hence the getTag() call. - TabsLayoutItemView itemView = (TabsLayoutItemView) v.getTag(); - closeTab(itemView); - } - }); - setAdapter(tabsAdapter); - - RecyclerViewClickSupport.addTo(this).setOnItemClickListener(this); - - setRecyclerListener(new RecyclerListener() { - @Override - public void onViewRecycled(RecyclerView.ViewHolder holder) { - final TabsLayoutItemView itemView = (TabsLayoutItemView) holder.itemView; - itemView.setThumbnail(null); - itemView.setCloseVisible(true); - } - }); - } - - @Override - public void setTabsPanel(TabsPanel panel) { - tabsPanel = panel; - } - - @Override - public void show() { - setVisibility(View.VISIBLE); - Tabs.getInstance().refreshThumbnails(); - Tabs.registerOnTabsChangedListener(this); - refreshTabsData(); - } - - @Override - public void hide() { - setVisibility(View.GONE); - Tabs.unregisterOnTabsChangedListener(this); - GeckoAppShell.notifyObservers("Tab:Screenshot:Cancel", ""); - tabsAdapter.clear(); - } - - @Override - public boolean shouldExpand() { - return true; - } - - protected void autoHidePanel() { - tabsPanel.autoHidePanel(); - } - - @Override - public void onTabChanged(Tab tab, Tabs.TabEvents msg, String data) { - switch (msg) { - case ADDED: - final int tabIndex = Integer.parseInt(data); - tabsAdapter.notifyTabInserted(tab, tabIndex); - if (addAtIndexRequiresScroll(tabIndex)) { - // (The current Tabs implementation updates the SELECTED tab *after* this - // call to ADDED, so don't just call updateSelectedPosition().) - scrollToPosition(tabIndex); - } - break; - - case CLOSED: - if (tab.isPrivate() == isPrivate && tabsAdapter.getItemCount() > 0) { - tabsAdapter.removeTab(tab); - } - break; - - case SELECTED: - case UNSELECTED: - case THUMBNAIL: - case TITLE: - case RECORDING_CHANGE: - case AUDIO_PLAYING_CHANGE: - tabsAdapter.notifyTabChanged(tab); - break; - } - } - - // Addition of a tab at selected positions (dependent on LayoutManager) will result in a tab - // being added out of view - return true if index is such a position. - abstract protected boolean addAtIndexRequiresScroll(int index); - - @Override - public void onItemClicked(RecyclerView recyclerView, int position, View v) { - final TabsLayoutItemView item = (TabsLayoutItemView) v; - final int tabId = item.getTabId(); - final Tab tab = Tabs.getInstance().selectTab(tabId); - if (tab == null) { - // The tab that was clicked no longer exists in the tabs list (which can happen if you - // tap on a tab while its remove animation is running), so ignore the click. - return; - } - - autoHidePanel(); - Tabs.getInstance().notifyListeners(tab, Tabs.TabEvents.OPENED_FROM_TABS_TRAY); - } - - // Updates the selected position in the list so that it will be scrolled to the right place. - private void updateSelectedPosition() { - final int selected = tabsAdapter.getPositionForTab(Tabs.getInstance().getSelectedTab()); - if (selected != NO_POSITION) { - scrollToPosition(selected); - } - } - - private void refreshTabsData() { - // Store a different copy of the tabs, so that we don't have to worry about - // accidentally updating it on the wrong thread. - final ArrayList tabData = new ArrayList<>(); - final Iterable allTabs = Tabs.getInstance().getTabsInOrder(); - - for (final Tab tab : allTabs) { - if (tab.isPrivate() == isPrivate) { - tabData.add(tab); - } - } - - tabsAdapter.setTabs(tabData); - updateSelectedPosition(); - } - - private void closeTab(View view) { - final TabsLayoutItemView itemView = (TabsLayoutItemView) view; - final Tab tab = getTabForView(itemView); - if (tab == null) { - // We can be null here if this is the second closeTab call resulting from a sufficiently - // fast double tap on the close tab button. - return; - } - - final boolean closingLastTab = tabsAdapter.getItemCount() == 1; - Tabs.getInstance().closeTab(tab, true); - if (closingLastTab) { - autoHidePanel(); - } - } - - protected void closeAllTabs() { - final Iterable tabs = Tabs.getInstance().getTabsInOrder(); - for (final Tab tab : tabs) { - // In the normal panel we want to close all tabs (both private and normal), - // but in the private panel we only want to close private tabs. - if (!isPrivate || tab.isPrivate()) { - Tabs.getInstance().closeTab(tab, false); - } - } - } - - @Override - public void onItemDismiss(View view) { - closeTab(view); - } - - private Tab getTabForView(View view) { - if (view == null) { - return null; - } - return Tabs.getInstance().getTab(((TabsLayoutItemView) view).getTabId()); - } - - @Override - public void setEmptyView(View emptyView) { - // We never display an empty view. - } - - @Override - abstract public void closeAll(); -} diff --git a/mobile/android/base/java/org/mozilla/gecko/tabs/TabsLayoutAdapter.java b/mobile/android/base/java/org/mozilla/gecko/tabs/TabsLayoutAdapter.java deleted file mode 100644 index 367da640ff..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/tabs/TabsLayoutAdapter.java +++ /dev/null @@ -1,100 +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.tabs; - -import org.mozilla.gecko.R; -import org.mozilla.gecko.Tab; - -import android.content.Context; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.BaseAdapter; - -import java.util.ArrayList; - -// Adapter to bind tabs into a list -public class TabsLayoutAdapter extends BaseAdapter { - public static final String LOGTAG = "Gecko" + TabsLayoutAdapter.class.getSimpleName(); - - private final Context mContext; - private final int mTabLayoutId; - private ArrayList mTabs; - private final LayoutInflater mInflater; - - public TabsLayoutAdapter (Context context, int tabLayoutId) { - mContext = context; - mInflater = LayoutInflater.from(mContext); - mTabLayoutId = tabLayoutId; - } - - final void setTabs (ArrayList tabs) { - mTabs = tabs; - notifyDataSetChanged(); // Be sure to call this whenever mTabs changes. - } - - final boolean removeTab (Tab tab) { - boolean tabRemoved = mTabs.remove(tab); - if (tabRemoved) { - notifyDataSetChanged(); // Be sure to call this whenever mTabs changes. - } - return tabRemoved; - } - - final void clear() { - mTabs = null; - - notifyDataSetChanged(); // Be sure to call this whenever mTabs changes. - } - - @Override - public int getCount() { - return (mTabs == null ? 0 : mTabs.size()); - } - - @Override - public Tab getItem(int position) { - return mTabs.get(position); - } - - @Override - public long getItemId(int position) { - return position; - } - - final int getPositionForTab(Tab tab) { - if (mTabs == null || tab == null) - return -1; - - return mTabs.indexOf(tab); - } - - @Override - public boolean isEnabled(int position) { - return true; - } - - @Override - final public TabsLayoutItemView getView(int position, View convertView, ViewGroup parent) { - final TabsLayoutItemView view; - if (convertView == null) { - view = newView(position, parent); - } else { - view = (TabsLayoutItemView) convertView; - } - final Tab tab = mTabs.get(position); - bindView(view, tab); - return view; - } - - TabsLayoutItemView newView(int position, ViewGroup parent) { - return (TabsLayoutItemView) mInflater.inflate(mTabLayoutId, parent, false); - } - - void bindView(TabsLayoutItemView view, Tab tab) { - view.assignValues(tab); - } -} \ No newline at end of file diff --git a/mobile/android/base/java/org/mozilla/gecko/tabs/TabsLayoutItemView.java b/mobile/android/base/java/org/mozilla/gecko/tabs/TabsLayoutItemView.java deleted file mode 100644 index 975e779d6e..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/tabs/TabsLayoutItemView.java +++ /dev/null @@ -1,172 +0,0 @@ -/* 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.tabs; - -import org.mozilla.gecko.R; -import org.mozilla.gecko.Tab; -import org.mozilla.gecko.Tabs; -import org.mozilla.gecko.widget.TabThumbnailWrapper; -import org.mozilla.gecko.widget.TouchDelegateWithReset; -import org.mozilla.gecko.widget.themed.ThemedRelativeLayout; - -import android.content.Context; -import android.graphics.Rect; -import android.graphics.drawable.Drawable; -import android.util.AttributeSet; -import android.util.TypedValue; -import android.view.View; -import android.view.ViewTreeObserver; -import android.widget.Checkable; -import android.widget.ImageView; -import android.widget.LinearLayout; -import android.widget.TextView; - -public class TabsLayoutItemView extends LinearLayout - implements Checkable { - private static final String LOGTAG = "Gecko" + TabsLayoutItemView.class.getSimpleName(); - private static final int[] STATE_CHECKED = { android.R.attr.state_checked }; - private boolean mChecked; - - private int mTabId; - private TextView mTitle; - private TabsPanelThumbnailView mThumbnail; - private ImageView mCloseButton; - private TabThumbnailWrapper mThumbnailWrapper; - - public TabsLayoutItemView(Context context, AttributeSet attrs) { - super(context, attrs); - } - - @Override - public int[] onCreateDrawableState(int extraSpace) { - final int[] drawableState = super.onCreateDrawableState(extraSpace + 1); - - if (mChecked) { - mergeDrawableStates(drawableState, STATE_CHECKED); - } - - return drawableState; - } - - @Override - public boolean isEnabled() { - return true; - } - - @Override - public boolean isChecked() { - return mChecked; - } - - @Override - public void setChecked(boolean checked) { - if (mChecked == checked) { - return; - } - - mChecked = checked; - refreshDrawableState(); - - int count = getChildCount(); - for (int i = 0; i < count; i++) { - final View child = getChildAt(i); - if (child instanceof Checkable) { - ((Checkable) child).setChecked(checked); - } - } - } - - @Override - public void toggle() { - mChecked = !mChecked; - } - - public void setCloseOnClickListener(OnClickListener mOnClickListener) { - mCloseButton.setOnClickListener(mOnClickListener); - } - - @Override - protected void onFinishInflate() { - super.onFinishInflate(); - mTitle = (TextView) findViewById(R.id.title); - mThumbnail = (TabsPanelThumbnailView) findViewById(R.id.thumbnail); - mCloseButton = (ImageView) findViewById(R.id.close); - mThumbnailWrapper = (TabThumbnailWrapper) findViewById(R.id.wrapper); - - growCloseButtonHitArea(); - } - - private void growCloseButtonHitArea() { - getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { - @Override - public boolean onPreDraw() { - getViewTreeObserver().removeOnPreDrawListener(this); - - // Ideally we want the close button hit area to be 40x40dp but we are constrained by the height of the parent, so - // we make it as tall as the parent view and 40dp across. - final int targetHitArea = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 40, getResources().getDisplayMetrics());; - - final Rect hitRect = new Rect(); - hitRect.top = 0; - hitRect.right = getWidth(); - hitRect.left = getWidth() - targetHitArea; - hitRect.bottom = targetHitArea; - - setTouchDelegate(new TouchDelegateWithReset(hitRect, mCloseButton)); - - return true; - } - }); - } - - protected void assignValues(Tab tab) { - if (tab == null) { - return; - } - - mTabId = tab.getId(); - - setChecked(Tabs.getInstance().isSelectedTab(tab)); - - Drawable thumbnailImage = tab.getThumbnail(); - mThumbnail.setImageDrawable(thumbnailImage); - - mThumbnail.setPrivateMode(tab.isPrivate()); - - if (mThumbnailWrapper != null) { - mThumbnailWrapper.setRecording(tab.isRecording()); - } - - final String tabTitle = tab.getDisplayTitle(); - mTitle.setText(tabTitle); - mCloseButton.setTag(this); - - if (tab.isAudioPlaying()) { - mTitle.setCompoundDrawablesWithIntrinsicBounds(R.drawable.tab_audio_playing, 0, 0, 0); - final String tabTitleWithAudio = - getResources().getString(R.string.tab_title_prefix_is_playing_audio, tabTitle); - mTitle.setContentDescription(tabTitleWithAudio); - } else { - mTitle.setCompoundDrawables(null, null, null, null); - mTitle.setContentDescription(tabTitle); - } - } - - public int getTabId() { - return mTabId; - } - - public void setThumbnail(Drawable thumbnail) { - mThumbnail.setImageDrawable(thumbnail); - } - - public void setCloseVisible(boolean visible) { - mCloseButton.setVisibility(visible ? View.VISIBLE : View.INVISIBLE); - } - - public void setPrivateMode(boolean isPrivate) { - ((ThemedRelativeLayout) findViewById(R.id.wrapper)).setPrivateMode(isPrivate); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/tabs/TabsLayoutRecyclerAdapter.java b/mobile/android/base/java/org/mozilla/gecko/tabs/TabsLayoutRecyclerAdapter.java deleted file mode 100644 index 090d74f9db..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/tabs/TabsLayoutRecyclerAdapter.java +++ /dev/null @@ -1,124 +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.tabs; - -import org.mozilla.gecko.Tab; - -import android.content.Context; -import android.support.annotation.NonNull; -import android.support.v7.widget.RecyclerView; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.Button; - -import java.util.ArrayList; - -public class TabsLayoutRecyclerAdapter - extends RecyclerView.Adapter { - - private static final String LOGTAG = "Gecko" + TabsLayoutRecyclerAdapter.class.getSimpleName(); - - private final int tabLayoutId; - private @NonNull ArrayList tabs; - private final LayoutInflater inflater; - private final boolean isPrivate; - // Click listener for the close button on itemViews. - private final Button.OnClickListener closeOnClickListener; - - // The TabsLayoutItemView takes care of caching its own Views, so we don't need to do anything - // here except not be abstract. - public static class TabsListViewHolder extends RecyclerView.ViewHolder { - public TabsListViewHolder(View itemView) { - super(itemView); - } - } - - public TabsLayoutRecyclerAdapter(Context context, int tabLayoutId, boolean isPrivate, - Button.OnClickListener closeOnClickListener) { - inflater = LayoutInflater.from(context); - this.tabLayoutId = tabLayoutId; - this.isPrivate = isPrivate; - this.closeOnClickListener = closeOnClickListener; - tabs = new ArrayList<>(0); - } - - /* package */ final void setTabs(@NonNull ArrayList tabs) { - this.tabs = tabs; - notifyDataSetChanged(); - } - - /* package */ final void clear() { - tabs = new ArrayList<>(0); - notifyDataSetChanged(); - } - - /* package */ final boolean removeTab(Tab tab) { - final int position = getPositionForTab(tab); - if (position == -1) { - return false; - } - tabs.remove(position); - notifyItemRemoved(position); - return true; - } - - /* package */ final int getPositionForTab(Tab tab) { - if (tab == null) { - return -1; - } - - return tabs.indexOf(tab); - } - - /* package */ void notifyTabChanged(Tab tab) { - notifyItemChanged(getPositionForTab(tab)); - } - - /* package */ void notifyTabInserted(Tab tab, int index) { - if (index >= 0 && index <= tabs.size()) { - tabs.add(index, tab); - notifyItemInserted(index); - } else { - // Add to the end. - tabs.add(tab); - notifyItemInserted(tabs.size() - 1); - // index == -1 is a valid way to add to the end, the other cases are errors. - if (index != -1) { - Log.e(LOGTAG, "Tab was inserted at an invalid position: " + Integer.toString(index)); - } - } - } - - @Override - public int getItemCount() { - return tabs.size(); - } - - private Tab getItem(int position) { - return tabs.get(position); - } - - @Override - public void onBindViewHolder(TabsListViewHolder viewHolder, int position) { - final Tab tab = getItem(position); - final TabsLayoutItemView itemView = (TabsLayoutItemView) viewHolder.itemView; - itemView.assignValues(tab); - // Be careful (re)setting position values here: bind is called on each notifyItemChanged, - // so you could be stomping on values that have been set in support of other animations - // that are already underway. - } - - @Override - public TabsListViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { - final TabsLayoutItemView viewItem = (TabsLayoutItemView) inflater.inflate(tabLayoutId, parent, false); - viewItem.setPrivateMode(isPrivate); - viewItem.setCloseOnClickListener(closeOnClickListener); - - return new TabsListViewHolder(viewItem); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/tabs/TabsListLayout.java b/mobile/android/base/java/org/mozilla/gecko/tabs/TabsListLayout.java deleted file mode 100644 index 8cf2f8eded..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/tabs/TabsListLayout.java +++ /dev/null @@ -1,118 +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.tabs; - -import org.mozilla.gecko.R; -import org.mozilla.gecko.animation.PropertyAnimator; -import org.mozilla.gecko.util.ThreadUtils; - -import android.content.Context; -import android.support.v7.widget.LinearLayoutManager; -import android.support.v7.widget.helper.ItemTouchHelper; -import android.util.AttributeSet; -import android.view.View; - -public class TabsListLayout extends TabsLayout { - // Time to animate non-flinged tabs of screen, in milliseconds - private static final int ANIMATION_DURATION = 250; - - // Time between starting successive tab animations in closeAllTabs. - private static final int ANIMATION_CASCADE_DELAY = 75; - - private int closeAllAnimationCount; - - public TabsListLayout(Context context, AttributeSet attrs) { - super(context, attrs, R.layout.tabs_list_item_view); - - setHasFixedSize(true); - - setLayoutManager(new LinearLayoutManager(context)); - - // A TouchHelper handler for swipe to close. - final TabsTouchHelperCallback callback = new TabsTouchHelperCallback(this); - final ItemTouchHelper touchHelper = new ItemTouchHelper(callback); - touchHelper.attachToRecyclerView(this); - - setItemAnimator(new TabsListLayoutAnimator(ANIMATION_DURATION)); - } - - @Override - public void closeAll() { - final int childCount = getChildCount(); - - // Just close the panel if there are no tabs to close. - if (childCount == 0) { - autoHidePanel(); - return; - } - - // Disable the view so that gestures won't interfere wth the tab close animation. - setEnabled(false); - - // Delay starting each successive animation to create a cascade effect. - int cascadeDelay = 0; - closeAllAnimationCount = 0; - for (int i = childCount - 1; i >= 0; i--) { - final View view = getChildAt(i); - if (view == null) { - continue; - } - - final PropertyAnimator animator = new PropertyAnimator(ANIMATION_DURATION); - animator.attach(view, PropertyAnimator.Property.ALPHA, 0); - - animator.attach(view, PropertyAnimator.Property.TRANSLATION_X, view.getWidth()); - - closeAllAnimationCount++; - - animator.addPropertyAnimationListener(new PropertyAnimator.PropertyAnimationListener() { - @Override - public void onPropertyAnimationStart() { - } - - @Override - public void onPropertyAnimationEnd() { - closeAllAnimationCount--; - if (closeAllAnimationCount > 0) { - return; - } - - // Hide the panel after the animation is done. - autoHidePanel(); - - // Re-enable the view after the animation is done. - TabsListLayout.this.setEnabled(true); - - // Then actually close all the tabs. - closeAllTabs(); - } - }); - - ThreadUtils.postDelayedToUiThread(new Runnable() { - @Override - public void run() { - animator.start(); - } - }, cascadeDelay); - - cascadeDelay += ANIMATION_CASCADE_DELAY; - } - } - - @Override - protected boolean addAtIndexRequiresScroll(int index) { - return index == 0 || index == getAdapter().getItemCount() - 1; - } - - @Override - public void onChildAttachedToWindow(View child) { - // Make sure we reset any attributes that may have been animated in this child's previous - // incarnation. - child.setTranslationX(0); - child.setTranslationY(0); - child.setAlpha(1); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/tabs/TabsListLayoutAnimator.java b/mobile/android/base/java/org/mozilla/gecko/tabs/TabsListLayoutAnimator.java deleted file mode 100644 index 471abf8839..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/tabs/TabsListLayoutAnimator.java +++ /dev/null @@ -1,65 +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.tabs; - -import org.mozilla.gecko.widget.DefaultItemAnimatorBase; - -import android.support.v4.view.ViewCompat; -import android.support.v7.widget.RecyclerView; -import android.view.View; - -class TabsListLayoutAnimator extends DefaultItemAnimatorBase { - public TabsListLayoutAnimator(int animationDuration) { - setRemoveDuration(animationDuration); - setAddDuration(animationDuration); - // A fade in/out each time the title/thumbnail/etc. gets updated isn't helpful, so disable - // the change animation. - setSupportsChangeAnimations(false); - } - - @Override - protected boolean preAnimateRemoveImpl(final RecyclerView.ViewHolder holder) { - // If the view isn't at full alpha then we were closed by a swipe which an - // ItemTouchHelper is animating for us, so just return without animating the remove and - // let runPendingAnimations pick up the rest. - if (holder.itemView.getAlpha() < 1) { - return false; - } - resetAnimation(holder); - return true; - } - - @Override - protected void animateRemoveImpl(final RecyclerView.ViewHolder holder) { - final View itemView = holder.itemView; - ViewCompat.animate(itemView) - .setDuration(getRemoveDuration()) - .translationX(itemView.getWidth()) - .alpha(0) - .setListener(new DefaultRemoveVpaListener(holder)) - .start(); - } - - @Override - protected boolean preAnimateAddImpl(RecyclerView.ViewHolder holder) { - resetAnimation(holder); - final View itemView = holder.itemView; - itemView.setTranslationX(itemView.getWidth()); - itemView.setAlpha(0); - return true; - } - - @Override - protected void animateAddImpl(final RecyclerView.ViewHolder holder) { - final View itemView = holder.itemView; - ViewCompat.animate(itemView) - .setDuration(getAddDuration()) - .translationX(0) - .alpha(1) - .setListener(new DefaultAddVpaListener(holder)) - .start(); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/tabs/TabsPanel.java b/mobile/android/base/java/org/mozilla/gecko/tabs/TabsPanel.java deleted file mode 100644 index 2be1270100..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/tabs/TabsPanel.java +++ /dev/null @@ -1,456 +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.tabs; - -import android.support.v4.content.ContextCompat; -import org.mozilla.gecko.AppConstants.Versions; -import org.mozilla.gecko.GeckoApp; -import org.mozilla.gecko.GeckoApplication; -import org.mozilla.gecko.R; -import org.mozilla.gecko.Telemetry; -import org.mozilla.gecko.TelemetryContract; -import org.mozilla.gecko.animation.PropertyAnimator; -import org.mozilla.gecko.animation.ViewHelper; -import org.mozilla.gecko.lwt.LightweightTheme; -import org.mozilla.gecko.lwt.LightweightThemeDrawable; -import org.mozilla.gecko.restrictions.Restrictable; -import org.mozilla.gecko.restrictions.Restrictions; -import org.mozilla.gecko.util.HardwareUtils; -import org.mozilla.gecko.widget.GeckoPopupMenu; -import org.mozilla.gecko.widget.IconTabWidget; - -import android.content.Context; -import android.content.res.Configuration; -import android.content.res.Resources; -import android.util.AttributeSet; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.Menu; -import android.view.MenuItem; -import android.view.View; -import android.widget.Button; -import android.widget.FrameLayout; -import android.widget.ImageButton; -import android.widget.LinearLayout; -import android.widget.RelativeLayout; -import org.mozilla.gecko.widget.themed.ThemedImageButton; - -public class TabsPanel extends LinearLayout - implements GeckoPopupMenu.OnMenuItemClickListener, - LightweightTheme.OnChangeListener, - IconTabWidget.OnTabChangedListener { - private static final String LOGTAG = "Gecko" + TabsPanel.class.getSimpleName(); - - public enum Panel { - NORMAL_TABS, - PRIVATE_TABS, - } - - public interface PanelView { - void setTabsPanel(TabsPanel panel); - void show(); - void hide(); - boolean shouldExpand(); - } - - public interface CloseAllPanelView extends PanelView { - void closeAll(); - } - - public interface TabsLayout extends CloseAllPanelView { - void setEmptyView(View view); - } - - public interface TabsLayoutChangeListener { - void onTabsLayoutChange(int width, int height); - } - - public static View createTabsLayout(final Context context, final AttributeSet attrs) { - final boolean isLandscape = context.getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE; - - if (HardwareUtils.isTablet() || isLandscape) { - return new TabsGridLayout(context, attrs); - } else { - return new TabsListLayout(context, attrs); - } - } - - private final Context mContext; - private final GeckoApp mActivity; - private final LightweightTheme mTheme; - private RelativeLayout mHeader; - private FrameLayout mTabsContainer; - private PanelView mPanel; - private PanelView mPanelNormal; - private PanelView mPanelPrivate; - private TabsLayoutChangeListener mLayoutChangeListener; - - private IconTabWidget mTabWidget; - private View mMenuButton; - private ImageButton mAddTab; - private ImageButton mNavBackButton; - - private Panel mCurrentPanel; - private boolean mVisible; - private boolean mHeaderVisible; - - private final GeckoPopupMenu mPopupMenu; - - public TabsPanel(Context context, AttributeSet attrs) { - super(context, attrs); - mContext = context; - mActivity = (GeckoApp) context; - mTheme = ((GeckoApplication) context.getApplicationContext()).getLightweightTheme(); - - mCurrentPanel = Panel.NORMAL_TABS; - - mPopupMenu = new GeckoPopupMenu(context); - mPopupMenu.inflate(R.menu.tabs_menu); - mPopupMenu.setOnMenuItemClickListener(this); - - inflateLayout(context); - initialize(); - } - - private void inflateLayout(Context context) { - LayoutInflater.from(context).inflate(R.layout.tabs_panel_default, this); - } - - private void initialize() { - mHeader = (RelativeLayout) findViewById(R.id.tabs_panel_header); - mTabsContainer = (FrameLayout) findViewById(R.id.tabs_container); - - mPanelNormal = (PanelView) findViewById(R.id.normal_tabs); - mPanelNormal.setTabsPanel(this); - - mPanelPrivate = (PanelView) findViewById(R.id.private_tabs_panel); - mPanelPrivate.setTabsPanel(this); - - mAddTab = (ImageButton) findViewById(R.id.add_tab); - mAddTab.setOnClickListener(new Button.OnClickListener() { - @Override - public void onClick(View v) { - TabsPanel.this.addTab(); - } - }); - - mTabWidget = (IconTabWidget) findViewById(R.id.tab_widget); - - mTabWidget.addTab(R.drawable.tabs_normal, R.string.tabs_normal); - final ThemedImageButton privateTabsPanel = - (ThemedImageButton) mTabWidget.addTab(R.drawable.tabs_private, R.string.tabs_private); - privateTabsPanel.setPrivateMode(true); - - if (!Restrictions.isAllowed(mContext, Restrictable.PRIVATE_BROWSING)) { - mTabWidget.setVisibility(View.GONE); - } - - mTabWidget.setTabSelectionListener(this); - - mMenuButton = findViewById(R.id.menu); - mMenuButton.setOnClickListener(new Button.OnClickListener() { - @Override - public void onClick(View view) { - showMenu(); - } - }); - - mNavBackButton = (ImageButton) findViewById(R.id.nav_back); - mNavBackButton.setOnClickListener(new Button.OnClickListener() { - @Override - public void onClick(View view) { - mActivity.onBackPressed(); - } - }); - } - - public void showMenu() { - final Menu menu = mPopupMenu.getMenu(); - - // Each panel has a "+" shortcut button, so don't show it for that panel. - menu.findItem(R.id.new_tab).setVisible(mCurrentPanel != Panel.NORMAL_TABS); - menu.findItem(R.id.new_private_tab).setVisible(mCurrentPanel != Panel.PRIVATE_TABS - && Restrictions.isAllowed(mContext, Restrictable.PRIVATE_BROWSING)); - - // Only show "Clear * tabs" for current panel. - menu.findItem(R.id.close_all_tabs).setVisible(mCurrentPanel == Panel.NORMAL_TABS); - menu.findItem(R.id.close_private_tabs).setVisible(mCurrentPanel == Panel.PRIVATE_TABS); - - mPopupMenu.show(); - } - - private void addTab() { - Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.ACTIONBAR, "new_tab"); - - if (mCurrentPanel == Panel.NORMAL_TABS) { - mActivity.addTab(); - } else { - mActivity.addPrivateTab(); - } - - mActivity.autoHideTabs(); - } - - @Override - public void onTabChanged(int index) { - if (index == 0) { - show(Panel.NORMAL_TABS); - } else { - show(Panel.PRIVATE_TABS); - } - } - - @Override - public boolean onMenuItemClick(MenuItem item) { - final int itemId = item.getItemId(); - - if (itemId == R.id.close_all_tabs) { - if (mCurrentPanel == Panel.NORMAL_TABS) { - Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.MENU, "close_all_tabs"); - - // Disable the menu button so that the menu won't interfere with the tab close animation. - mMenuButton.setEnabled(false); - ((CloseAllPanelView) mPanelNormal).closeAll(); - } else { - Log.e(LOGTAG, "Close all tabs menu item should only be visible for normal tabs panel"); - } - return true; - } - - if (itemId == R.id.close_private_tabs) { - if (mCurrentPanel == Panel.PRIVATE_TABS) { - // Mask private browsing - Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.MENU, "close_all_tabs"); - - ((CloseAllPanelView) mPanelPrivate).closeAll(); - } else { - Log.e(LOGTAG, "Close private tabs menu item should only be visible for private tabs panel"); - } - return true; - } - - if (itemId == R.id.new_tab || itemId == R.id.new_private_tab) { - hide(); - } - - return mActivity.onOptionsItemSelected(item); - } - - private static int getTabContainerHeight(FrameLayout tabsContainer) { - final Resources resources = tabsContainer.getContext().getResources(); - - final int screenHeight = resources.getDisplayMetrics().heightPixels; - final int actionBarHeight = resources.getDimensionPixelSize(R.dimen.browser_toolbar_height); - - return screenHeight - actionBarHeight; - } - - @Override - public void onAttachedToWindow() { - super.onAttachedToWindow(); - mTheme.addListener(this); - } - - @Override - public void onDetachedFromWindow() { - super.onDetachedFromWindow(); - mTheme.removeListener(this); - } - - @Override - @SuppressWarnings("deprecation") // setBackgroundDrawable deprecated by API level 16 - public void onLightweightThemeChanged() { - final int background = ContextCompat.getColor(getContext(), R.color.text_and_tabs_tray_grey); - final LightweightThemeDrawable drawable = mTheme.getColorDrawable(this, background, true); - if (drawable == null) - return; - - drawable.setAlpha(34, 0); - setBackgroundDrawable(drawable); - } - - @Override - public void onLightweightThemeReset() { - setBackgroundColor(ContextCompat.getColor(getContext(), R.color.text_and_tabs_tray_grey)); - } - - @Override - protected void onLayout(boolean changed, int left, int top, int right, int bottom) { - super.onLayout(changed, left, top, right, bottom); - onLightweightThemeChanged(); - } - - // Tabs Panel Toolbar contains the Buttons - static class TabsPanelToolbar extends LinearLayout - implements LightweightTheme.OnChangeListener { - private final LightweightTheme mTheme; - - public TabsPanelToolbar(Context context, AttributeSet attrs) { - super(context, attrs); - mTheme = ((GeckoApplication) context.getApplicationContext()).getLightweightTheme(); - } - - @Override - public void onAttachedToWindow() { - super.onAttachedToWindow(); - mTheme.addListener(this); - } - - @Override - public void onDetachedFromWindow() { - super.onDetachedFromWindow(); - mTheme.removeListener(this); - } - - @Override - @SuppressWarnings("deprecation") // setBackgroundDrawable deprecated by API level 16 - public void onLightweightThemeChanged() { - final int background = ContextCompat.getColor(getContext(), R.color.text_and_tabs_tray_grey); - final LightweightThemeDrawable drawable = mTheme.getColorDrawable(this, background); - if (drawable == null) - return; - - drawable.setAlpha(34, 34); - setBackgroundDrawable(drawable); - } - - @Override - public void onLightweightThemeReset() { - setBackgroundColor(ContextCompat.getColor(getContext(), R.color.text_and_tabs_tray_grey)); - } - - @Override - protected void onLayout(boolean changed, int left, int top, int right, int bottom) { - super.onLayout(changed, left, top, right, bottom); - onLightweightThemeChanged(); - } - } - - public void show(Panel panelToShow) { - prepareToShow(panelToShow); - int height = getVerticalPanelHeight(); - dispatchLayoutChange(getWidth(), height); - mHeaderVisible = true; - } - - public void prepareToShow(Panel panelToShow) { - if (!isShown()) { - setVisibility(View.VISIBLE); - } - - if (mPanel != null) { - // Hide the old panel. - mPanel.hide(); - } - - mVisible = true; - mCurrentPanel = panelToShow; - - int index = panelToShow.ordinal(); - mTabWidget.setCurrentTab(index); - - switch (panelToShow) { - case NORMAL_TABS: - mPanel = mPanelNormal; - break; - case PRIVATE_TABS: - mPanel = mPanelPrivate; - break; - - default: - throw new IllegalArgumentException("Unknown panel type " + panelToShow); - } - mPanel.show(); - - mAddTab.setVisibility(View.VISIBLE); - - mMenuButton.setEnabled(true); - mPopupMenu.setAnchor(mMenuButton); - } - - public int getVerticalPanelHeight() { - final int actionBarHeight = mContext.getResources().getDimensionPixelSize(R.dimen.browser_toolbar_height); - final int height = actionBarHeight + getTabContainerHeight(mTabsContainer); - return height; - } - - public void hide() { - mHeaderVisible = false; - - if (mVisible) { - mVisible = false; - mPopupMenu.dismiss(); - dispatchLayoutChange(0, 0); - } - } - - public void refresh() { - removeAllViews(); - - inflateLayout(mContext); - initialize(); - - if (mVisible) - show(mCurrentPanel); - } - - public void autoHidePanel() { - mActivity.autoHideTabs(); - } - - @Override - public boolean isShown() { - return mVisible; - } - - public void setHWLayerEnabled(boolean enabled) { - if (enabled) { - mHeader.setLayerType(View.LAYER_TYPE_HARDWARE, null); - mTabsContainer.setLayerType(View.LAYER_TYPE_HARDWARE, null); - } else { - mHeader.setLayerType(View.LAYER_TYPE_NONE, null); - mTabsContainer.setLayerType(View.LAYER_TYPE_NONE, null); - } - } - - public void prepareTabsAnimation(PropertyAnimator animator) { - if (!mHeaderVisible) { - final Resources resources = getContext().getResources(); - final int toolbarHeight = resources.getDimensionPixelSize(R.dimen.browser_toolbar_height); - final int translationY = (mVisible ? 0 : -toolbarHeight); - if (mVisible) { - ViewHelper.setTranslationY(mHeader, -toolbarHeight); - ViewHelper.setTranslationY(mTabsContainer, -toolbarHeight); - ViewHelper.setAlpha(mTabsContainer, 0.0f); - } - animator.attach(mTabsContainer, PropertyAnimator.Property.ALPHA, mVisible ? 1.0f : 0.0f); - animator.attach(mTabsContainer, PropertyAnimator.Property.TRANSLATION_Y, translationY); - animator.attach(mHeader, PropertyAnimator.Property.TRANSLATION_Y, translationY); - } - - setHWLayerEnabled(true); - } - - public void finishTabsAnimation() { - setHWLayerEnabled(false); - - // If the tray is now hidden, call hide() on current panel and unset it as the current panel - // to avoid hide() being called again when the layout is opened next. - if (!mVisible && mPanel != null) { - mPanel.hide(); - mPanel = null; - } - } - - public void setTabsLayoutChangeListener(TabsLayoutChangeListener listener) { - mLayoutChangeListener = listener; - } - - private void dispatchLayoutChange(int width, int height) { - if (mLayoutChangeListener != null) - mLayoutChangeListener.onTabsLayoutChange(width, height); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/tabs/TabsPanelThumbnailView.java b/mobile/android/base/java/org/mozilla/gecko/tabs/TabsPanelThumbnailView.java deleted file mode 100644 index 09254bf767..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/tabs/TabsPanelThumbnailView.java +++ /dev/null @@ -1,52 +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.tabs; - -import org.mozilla.gecko.R; -import org.mozilla.gecko.ThumbnailHelper; -import org.mozilla.gecko.widget.CropImageView; - -import android.content.Context; -import android.graphics.drawable.Drawable; -import android.util.AttributeSet; - -/** - * A width constrained ImageView to show thumbnails of open tabs in the tabs panel. - */ -public class TabsPanelThumbnailView extends CropImageView { - public static final String LOGTAG = "Gecko" + TabsPanelThumbnailView.class.getSimpleName(); - - - public TabsPanelThumbnailView(final Context context) { - this(context, null); - } - - public TabsPanelThumbnailView(final Context context, final AttributeSet attrs) { - this(context, attrs, 0); - } - - public TabsPanelThumbnailView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - } - - @Override - protected float getAspectRatio() { - return ThumbnailHelper.TABS_PANEL_THUMBNAIL_ASPECT_RATIO; - } - - @Override - public void setImageDrawable(Drawable drawable) { - boolean resize = true; - - if (drawable == null) { - drawable = getResources().getDrawable(R.drawable.tab_panel_tab_background); - resize = false; - setScaleType(ScaleType.FIT_XY); - } - - super.setImageDrawable(drawable, resize); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/tabs/TabsTouchHelperCallback.java b/mobile/android/base/java/org/mozilla/gecko/tabs/TabsTouchHelperCallback.java deleted file mode 100644 index 36e9e47391..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/tabs/TabsTouchHelperCallback.java +++ /dev/null @@ -1,69 +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.tabs; - -import android.graphics.Canvas; -import android.support.v7.widget.RecyclerView; -import android.support.v7.widget.helper.ItemTouchHelper; -import android.view.View; - -class TabsTouchHelperCallback extends ItemTouchHelper.Callback { - private final DismissListener dismissListener; - - interface DismissListener { - void onItemDismiss(View view); - } - - public TabsTouchHelperCallback(DismissListener dismissListener) { - this.dismissListener = dismissListener; - } - - @Override - public boolean isItemViewSwipeEnabled() { - return true; - } - - @Override - public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) { - return makeFlag(ItemTouchHelper.ACTION_STATE_SWIPE, ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT); - } - - @Override - public void onSwiped(RecyclerView.ViewHolder viewHolder, int i) { - dismissListener.onItemDismiss(viewHolder.itemView); - } - - @Override - public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, - RecyclerView.ViewHolder target) { - return false; - } - - // Alpha on an itemView being swiped should decrease to a min over a distance equal to the - // width of the item being swiped. - @Override - public void onChildDraw(Canvas c, - RecyclerView recyclerView, - RecyclerView.ViewHolder viewHolder, - float dX, - float dY, - int actionState, - boolean isCurrentlyActive) { - if (actionState != ItemTouchHelper.ACTION_STATE_SWIPE) { - return; - } - - super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive); - - viewHolder.itemView.setAlpha(Math.max(0.1f, - Math.min(1f, 1f - 2f * Math.abs(dX) / viewHolder.itemView.getWidth()))); - } - - public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) { - super.clearView(recyclerView, viewHolder); - viewHolder.itemView.setAlpha(1); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryConstants.java b/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryConstants.java deleted file mode 100644 index 6ed4bb0d44..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryConstants.java +++ /dev/null @@ -1,16 +0,0 @@ -/* 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.telemetry; - -import org.mozilla.gecko.AppConstants; - -public class TelemetryConstants { - // To test, set this to true & change "toolkit.telemetry.server" in about:config. - public static final boolean UPLOAD_ENABLED = AppConstants.MOZILLA_OFFICIAL; // Disabled for developer builds. - - public static final String USER_AGENT = - "Firefox-Android-Telemetry/" + AppConstants.MOZ_APP_VERSION + " (" + AppConstants.MOZ_APP_UA_NAME + ")"; - -} diff --git a/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryCorePingDelegate.java b/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryCorePingDelegate.java deleted file mode 100644 index fae674b2dc..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryCorePingDelegate.java +++ /dev/null @@ -1,188 +0,0 @@ -/* - * 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.telemetry; - -import android.content.Context; -import android.content.SharedPreferences; -import android.support.annotation.Nullable; -import android.support.annotation.WorkerThread; -import android.util.Log; -import org.mozilla.gecko.BrowserApp; -import org.mozilla.gecko.GeckoProfile; -import org.mozilla.gecko.GeckoSharedPrefs; -import org.mozilla.gecko.adjust.AttributionHelperListener; -import org.mozilla.gecko.telemetry.measurements.CampaignIdMeasurements; -import org.mozilla.gecko.delegates.BrowserAppDelegateWithReference; -import org.mozilla.gecko.distribution.DistributionStoreCallback; -import org.mozilla.gecko.search.SearchEngineManager; -import org.mozilla.gecko.sync.ExtendedJSONObject; -import org.mozilla.gecko.telemetry.measurements.SearchCountMeasurements; -import org.mozilla.gecko.telemetry.measurements.SessionMeasurements; -import org.mozilla.gecko.telemetry.pingbuilders.TelemetryCorePingBuilder; -import org.mozilla.gecko.util.StringUtils; -import org.mozilla.gecko.util.ThreadUtils; - -import java.io.IOException; - -/** - * An activity-lifecycle delegate for uploading the core ping. - */ -public class TelemetryCorePingDelegate extends BrowserAppDelegateWithReference - implements SearchEngineManager.SearchEngineCallback, AttributionHelperListener { - private static final String LOGTAG = StringUtils.safeSubstring( - "Gecko" + TelemetryCorePingDelegate.class.getSimpleName(), 0, 23); - - private static final String PREF_IS_FIRST_RUN = "telemetry-isFirstRun"; - - private TelemetryDispatcher telemetryDispatcher; // lazy - private final SessionMeasurements sessionMeasurements = new SessionMeasurements(); - - @Override - public void onStart(final BrowserApp browserApp) { - TelemetryPreferences.initPreferenceObserver(browserApp, browserApp.getProfile().getName()); - - // We don't upload in onCreate because that's only called when the Activity needs to be instantiated - // and it's possible the system will never free the Activity from memory. - // - // We don't upload in onResume/onPause because that will be called each time the Activity is obscured, - // including by our own Activities/dialogs, and there is no reason to upload each time we're unobscured. - // - // We're left with onStart/onStop and we upload in onStart because onStop is not guaranteed to be called - // and we want to upload the first run ASAP (e.g. to get install data before the app may crash). - uploadPing(browserApp); - } - - @Override - public void onStop(final BrowserApp browserApp) { - // We've decided to upload primarily in onStart (see note there). However, if it's the first run, - // it's possible a user used fennec and decided never to return to it again - it'd be great to get - // their session information before they decided to give it up so we upload here on first run. - // - // Caveats: - // * onStop is not guaranteed to be called in low memory conditions so it's possible we won't upload, - // but it's better than it was before. - // * Besides first run (because of this call), we can never get the user's *last* session data. - // - // If we are really interested in the user's last session data, we could consider uploading in onStop - // but it's less robust (see discussion in bug 1277091). - final SharedPreferences sharedPrefs = getSharedPreferences(browserApp); - if (sharedPrefs.getBoolean(PREF_IS_FIRST_RUN, true)) { - sharedPrefs.edit() - .putBoolean(PREF_IS_FIRST_RUN, false) - .apply(); - uploadPing(browserApp); - } - } - - private void uploadPing(final BrowserApp browserApp) { - final SearchEngineManager searchEngineManager = browserApp.getSearchEngineManager(); - searchEngineManager.getEngine(this); - } - - @Override - public void onResume(BrowserApp browserApp) { - sessionMeasurements.recordSessionStart(); - } - - @Override - public void onPause(BrowserApp browserApp) { - // onStart/onStop is ideal over onResume/onPause. However, onStop is not guaranteed to be called and - // dealing with that possibility adds a lot of complexity that we don't want to handle at this point. - sessionMeasurements.recordSessionEnd(browserApp); - } - - @WorkerThread // via constructor - private TelemetryDispatcher getTelemetryDispatcher(final BrowserApp browserApp) { - if (telemetryDispatcher == null) { - final GeckoProfile profile = browserApp.getProfile(); - final String profilePath = profile.getDir().getAbsolutePath(); - final String profileName = profile.getName(); - telemetryDispatcher = new TelemetryDispatcher(profilePath, profileName); - } - return telemetryDispatcher; - } - - private SharedPreferences getSharedPreferences(final BrowserApp activity) { - return GeckoSharedPrefs.forProfileName(activity, activity.getProfile().getName()); - } - - // via SearchEngineCallback - may be called from any thread. - @Override - public void execute(@Nullable final org.mozilla.gecko.search.SearchEngine engine) { - // Don't waste resources queueing to the background thread if we don't have a reference. - if (getBrowserApp() == null) { - return; - } - - // The containing method can be called from onStart: queue this work so that - // the first launch of the activity doesn't trigger profile init too early. - // - // Additionally, getAndIncrementSequenceNumber must be called from a worker thread. - ThreadUtils.postToBackgroundThread(new Runnable() { - @WorkerThread - @Override - public void run() { - final BrowserApp activity = getBrowserApp(); - if (activity == null) { - return; - } - - final GeckoProfile profile = activity.getProfile(); - if (!TelemetryUploadService.isUploadEnabledByProfileConfig(activity, profile)) { - Log.d(LOGTAG, "Core ping upload disabled by profile config. Returning."); - return; - } - - final String clientID; - try { - clientID = profile.getClientId(); - } catch (final IOException e) { - Log.w(LOGTAG, "Unable to get client ID to generate core ping: " + e); - return; - } - - // Each profile can have different telemetry data so we intentionally grab the shared prefs for the profile. - final SharedPreferences sharedPrefs = getSharedPreferences(activity); - final SessionMeasurements.SessionMeasurementsContainer sessionMeasurementsContainer = - sessionMeasurements.getAndResetSessionMeasurements(activity); - final TelemetryCorePingBuilder pingBuilder = new TelemetryCorePingBuilder(activity) - .setClientID(clientID) - .setDefaultSearchEngine(TelemetryCorePingBuilder.getEngineIdentifier(engine)) - .setProfileCreationDate(TelemetryCorePingBuilder.getProfileCreationDate(activity, profile)) - .setSequenceNumber(TelemetryCorePingBuilder.getAndIncrementSequenceNumber(sharedPrefs)) - .setSessionCount(sessionMeasurementsContainer.sessionCount) - .setSessionDuration(sessionMeasurementsContainer.elapsedSeconds); - maybeSetOptionalMeasurements(activity, sharedPrefs, pingBuilder); - - getTelemetryDispatcher(activity).queuePingForUpload(activity, pingBuilder); - } - }); - } - - private void maybeSetOptionalMeasurements(final Context context, final SharedPreferences sharedPrefs, - final TelemetryCorePingBuilder pingBuilder) { - final String distributionId = sharedPrefs.getString(DistributionStoreCallback.PREF_DISTRIBUTION_ID, null); - if (distributionId != null) { - pingBuilder.setOptDistributionID(distributionId); - } - - final ExtendedJSONObject searchCounts = SearchCountMeasurements.getAndZeroSearch(sharedPrefs); - if (searchCounts.size() > 0) { - pingBuilder.setOptSearchCounts(searchCounts); - } - - final String campaignId = CampaignIdMeasurements.getCampaignIdFromPrefs(context); - if (campaignId != null) { - pingBuilder.setOptCampaignId(campaignId); - } - } - - @Override - public void onCampaignIdChanged(String campaignId) { - CampaignIdMeasurements.updateCampaignIdPref(getBrowserApp(), campaignId); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryDispatcher.java b/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryDispatcher.java deleted file mode 100644 index c702bb92c0..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryDispatcher.java +++ /dev/null @@ -1,118 +0,0 @@ -/* - * 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.telemetry; - -import android.content.Context; -import android.support.annotation.WorkerThread; -import android.util.Log; -import org.mozilla.gecko.telemetry.pingbuilders.TelemetryCorePingBuilder; -import org.mozilla.gecko.telemetry.schedulers.TelemetryUploadScheduler; -import org.mozilla.gecko.telemetry.schedulers.TelemetryUploadAllPingsImmediatelyScheduler; -import org.mozilla.gecko.telemetry.stores.TelemetryJSONFilePingStore; -import org.mozilla.gecko.telemetry.stores.TelemetryPingStore; -import org.mozilla.gecko.util.ThreadUtils; - -import java.io.File; -import java.io.IOException; - -/** - * The entry-point for Java-based telemetry. This class handles: - * * Initializing the Stores & Schedulers. - * * Queueing upload requests for a given ping. - * - * To test Telemetry , see {@link TelemetryConstants} & - * https://wiki.mozilla.org/Mobile/Fennec/Android/Java_telemetry. - * - * The full architecture is: - * - * Fennec -(PingBuilder)-> Dispatcher -2-> Scheduler -> UploadService - * | 1 | - * Store <-------------------------- - * - * The store acts as a single store of truth and contains a list of all - * pings waiting to be uploaded. The dispatcher will queue a ping to upload - * by writing it to the store. Later, the UploadService will try to upload - * this queued ping by reading directly from the store. - * - * To implement a new ping type, you should: - * 1) Implement a {@link org.mozilla.gecko.telemetry.pingbuilders.TelemetryPingBuilder} for your ping type. - * 2) Re-use a ping store in .../stores/ or implement a new one: {@link TelemetryPingStore}. The - * type of store may be affected by robustness requirements (e.g. do you have data in addition to - * pings that need to be atomically updated when a ping is stored?) and performance requirements. - * 3) Re-use an upload scheduler in .../schedulers/ or implement a new one: {@link TelemetryUploadScheduler}. - * 4) Initialize your Store & (if new) Scheduler in the constructor of this class - * 5) Add a queuePingForUpload method for your PingBuilder class (see - * {@link #queuePingForUpload(Context, TelemetryCorePingBuilder)}) - * 6) In Fennec, where you want to store a ping and attempt upload, create a PingBuilder and - * pass it to the new queuePingForUpload method. - */ -public class TelemetryDispatcher { - private static final String LOGTAG = "Gecko" + TelemetryDispatcher.class.getSimpleName(); - - private static final String STORE_CONTAINER_DIR_NAME = "telemetry_java"; - private static final String CORE_STORE_DIR_NAME = "core"; - - private final TelemetryJSONFilePingStore coreStore; - - private final TelemetryUploadAllPingsImmediatelyScheduler uploadAllPingsImmediatelyScheduler; - - @WorkerThread // via TelemetryJSONFilePingStore - public TelemetryDispatcher(final String profilePath, final String profileName) { - final String storePath = profilePath + File.separator + STORE_CONTAINER_DIR_NAME; - - // There are measurements in the core ping (e.g. seq #) that would ideally be atomically updated - // when the ping is stored. However, for simplicity, we use the json store and accept the possible - // loss of data (see bug 1243585 comment 16+ for more). - coreStore = new TelemetryJSONFilePingStore(new File(storePath, CORE_STORE_DIR_NAME), profileName); - - uploadAllPingsImmediatelyScheduler = new TelemetryUploadAllPingsImmediatelyScheduler(); - } - - private void queuePingForUpload(final Context context, final TelemetryPing ping, final TelemetryPingStore store, - final TelemetryUploadScheduler scheduler) { - final QueuePingRunnable runnable = new QueuePingRunnable(context, ping, store, scheduler); - ThreadUtils.postToBackgroundThread(runnable); // TODO: Investigate how busy this thread is. See if we want another. - } - - /** - * Queues the given ping for upload and potentially schedules upload. This method can be called from any thread. - */ - public void queuePingForUpload(final Context context, final TelemetryCorePingBuilder pingBuilder) { - final TelemetryPing ping = pingBuilder.build(); - queuePingForUpload(context, ping, coreStore, uploadAllPingsImmediatelyScheduler); - } - - private static class QueuePingRunnable implements Runnable { - private final Context applicationContext; - private final TelemetryPing ping; - private final TelemetryPingStore store; - private final TelemetryUploadScheduler scheduler; - - public QueuePingRunnable(final Context context, final TelemetryPing ping, final TelemetryPingStore store, - final TelemetryUploadScheduler scheduler) { - this.applicationContext = context.getApplicationContext(); - this.ping = ping; - this.store = store; - this.scheduler = scheduler; - } - - @Override - public void run() { - // We block while storing the ping so the scheduled upload is guaranteed to have the newly-stored value. - try { - store.storePing(ping); - } catch (final IOException e) { - // Don't log exception to avoid leaking profile path. - Log.e(LOGTAG, "Unable to write ping to disk. Continuing with upload attempt"); - } - - if (scheduler.isReadyToUpload(store)) { - scheduler.scheduleUpload(applicationContext, store); - } - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryPing.java b/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryPing.java deleted file mode 100644 index b6ee9c2d88..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryPing.java +++ /dev/null @@ -1,34 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.telemetry; - -import org.mozilla.gecko.sync.ExtendedJSONObject; - -/** - * Container for telemetry data and the data necessary to upload it. - * - * The doc ID is used by a Store to manipulate its internal pings and should - * be the same value found in the urlPath. - * - * If you want to create one of these, consider extending - * {@link org.mozilla.gecko.telemetry.pingbuilders.TelemetryPingBuilder} - * or one of its descendants. - */ -public class TelemetryPing { - private final String urlPath; - private final ExtendedJSONObject payload; - private final String docID; - - public TelemetryPing(final String urlPath, final ExtendedJSONObject payload, final String docID) { - this.urlPath = urlPath; - this.payload = payload; - this.docID = docID; - } - - public String getURLPath() { return urlPath; } - public ExtendedJSONObject getPayload() { return payload; } - public String getDocID() { return docID; } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryPreferences.java b/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryPreferences.java deleted file mode 100644 index 329f5b8036..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryPreferences.java +++ /dev/null @@ -1,73 +0,0 @@ -/* 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.telemetry; - -import android.content.Context; -import android.content.SharedPreferences; -import org.mozilla.gecko.GeckoSharedPrefs; -import org.mozilla.gecko.PrefsHelper; -import org.mozilla.gecko.PrefsHelper.PrefHandler; - -import java.lang.ref.WeakReference; - -/** - * Manages getting and setting any preferences related to telemetry. - * - * This class persists any Gecko preferences beyond shutdown so that these values - * can be accessed on the next run before Gecko is started as we expect Telemetry - * to run before Gecko is available. - */ -public class TelemetryPreferences { - private TelemetryPreferences() {} - - private static final String GECKO_PREF_SERVER_URL = "toolkit.telemetry.server"; - private static final String SHARED_PREF_SERVER_URL = "telemetry-serverUrl"; - - // Defaults are a mirror of about:config defaults so we can access them before Gecko is available. - private static final String DEFAULT_SERVER_URL = "https://incoming.telemetry.mozilla.org"; - - private static final String[] OBSERVED_PREFS = { - GECKO_PREF_SERVER_URL, - }; - - public static String getServerSchemeHostPort(final Context context, final String profileName) { - return getSharedPrefs(context, profileName).getString(SHARED_PREF_SERVER_URL, DEFAULT_SERVER_URL); - } - - public static void initPreferenceObserver(final Context context, final String profileName) { - final PrefHandler prefHandler = new TelemetryPrefHandler(context, profileName); - PrefsHelper.addObserver(OBSERVED_PREFS, prefHandler); // gets preference value when gecko starts. - } - - private static SharedPreferences getSharedPrefs(final Context context, final String profileName) { - return GeckoSharedPrefs.forProfileName(context, profileName); - } - - private static class TelemetryPrefHandler extends PrefsHelper.PrefHandlerBase { - private final WeakReference contextWeakReference; - private final String profileName; - - private TelemetryPrefHandler(final Context context, final String profileName) { - contextWeakReference = new WeakReference<>(context); - this.profileName = profileName; - } - - @Override - public void prefValue(final String pref, final String value) { - final Context context = contextWeakReference.get(); - if (context == null) { - return; - } - - if (!pref.equals(GECKO_PREF_SERVER_URL)) { - throw new IllegalStateException("Unknown preference: " + pref); - } - - getSharedPrefs(context, profileName).edit() - .putString(SHARED_PREF_SERVER_URL, value) - .apply(); - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryUploadService.java b/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryUploadService.java deleted file mode 100644 index 543281174a..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryUploadService.java +++ /dev/null @@ -1,347 +0,0 @@ -/* 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.telemetry; - -import android.app.IntentService; -import android.content.Context; -import android.content.Intent; -import android.util.Log; -import ch.boye.httpclientandroidlib.HttpHeaders; -import ch.boye.httpclientandroidlib.HttpResponse; -import ch.boye.httpclientandroidlib.client.ClientProtocolException; -import ch.boye.httpclientandroidlib.client.methods.HttpRequestBase; -import ch.boye.httpclientandroidlib.impl.client.DefaultHttpClient; -import org.mozilla.gecko.GeckoProfile; -import org.mozilla.gecko.preferences.GeckoPreferences; -import org.mozilla.gecko.restrictions.Restrictable; -import org.mozilla.gecko.restrictions.Restrictions; -import org.mozilla.gecko.sync.ExtendedJSONObject; -import org.mozilla.gecko.sync.net.BaseResource; -import org.mozilla.gecko.sync.net.BaseResourceDelegate; -import org.mozilla.gecko.sync.net.Resource; -import org.mozilla.gecko.telemetry.stores.TelemetryPingStore; -import org.mozilla.gecko.util.DateUtil; -import org.mozilla.gecko.util.NetworkUtils; -import org.mozilla.gecko.util.StringUtils; - -import java.io.IOException; -import java.net.URISyntaxException; -import java.security.GeneralSecurityException; -import java.util.Calendar; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import java.util.concurrent.TimeUnit; - -/** - * The service that handles retrieving a list of telemetry pings to upload from the given - * {@link TelemetryPingStore}, uploading those payloads to the associated server, and reporting - * back to the Store which uploads were a success. - */ -public class TelemetryUploadService extends IntentService { - private static final String LOGTAG = StringUtils.safeSubstring("Gecko" + TelemetryUploadService.class.getSimpleName(), 0, 23); - private static final String WORKER_THREAD_NAME = LOGTAG + "Worker"; - - public static final String ACTION_UPLOAD = "upload"; - public static final String EXTRA_STORE = "store"; - - // TelemetryUploadService can run in a background thread so for future proofing, we set it volatile. - private static volatile boolean isDisabled = false; - - public static void setDisabled(final boolean isDisabled) { - TelemetryUploadService.isDisabled = isDisabled; - if (isDisabled) { - Log.d(LOGTAG, "Telemetry upload disabled (env var?"); - } - } - - public TelemetryUploadService() { - super(WORKER_THREAD_NAME); - - // Intent redelivery can fail hard (e.g. we OOM as we try to upload, the Intent gets redelivered, repeat) - // so for simplicity, we avoid it. We expect the upload service to eventually get called again by the caller. - setIntentRedelivery(false); - } - - /** - * Handles a ping with the mandatory extras: - * * EXTRA_STORE: A {@link TelemetryPingStore} where the pings to upload are located - */ - @Override - public void onHandleIntent(final Intent intent) { - Log.d(LOGTAG, "Service started"); - - if (!isReadyToUpload(this, intent)) { - return; - } - - final TelemetryPingStore store = intent.getParcelableExtra(EXTRA_STORE); - final boolean wereAllUploadsSuccessful = uploadPendingPingsFromStore(this, store); - store.maybePrunePings(); - Log.d(LOGTAG, "Service finished: upload and prune attempts completed"); - - if (!wereAllUploadsSuccessful) { - // If we had an upload failure, we should stop the IntentService and drop any - // pending Intents in the queue so we don't waste resources (e.g. battery) - // trying to upload when there's likely to be another connection failure. - Log.d(LOGTAG, "Clearing Intent queue due to connection failures"); - stopSelf(); - } - } - - /** - * @return true if all pings were uploaded successfully, false otherwise. - */ - private static boolean uploadPendingPingsFromStore(final Context context, final TelemetryPingStore store) { - final List pingsToUpload = store.getAllPings(); - if (pingsToUpload.isEmpty()) { - return true; - } - - final String serverSchemeHostPort = TelemetryPreferences.getServerSchemeHostPort(context, store.getProfileName()); - final HashSet successfulUploadIDs = new HashSet<>(pingsToUpload.size()); // used for side effects. - final PingResultDelegate delegate = new PingResultDelegate(successfulUploadIDs); - for (final TelemetryPing ping : pingsToUpload) { - // TODO: It'd be great to re-use the same HTTP connection for each upload request. - delegate.setDocID(ping.getDocID()); - final String url = serverSchemeHostPort + "/" + ping.getURLPath(); - uploadPayload(url, ping.getPayload(), delegate); - - // There are minimal gains in trying to upload if we already failed one attempt. - if (delegate.hadConnectionError()) { - break; - } - } - - final boolean wereAllUploadsSuccessful = !delegate.hadConnectionError(); - if (wereAllUploadsSuccessful) { - // We don't log individual successful uploads to avoid log spam. - Log.d(LOGTAG, "Telemetry upload success!"); - } - store.onUploadAttemptComplete(successfulUploadIDs); - return wereAllUploadsSuccessful; - } - - private static void uploadPayload(final String url, final ExtendedJSONObject payload, final ResultDelegate delegate) { - final BaseResource resource; - try { - resource = new BaseResource(url); - } catch (final URISyntaxException e) { - Log.w(LOGTAG, "URISyntaxException for server URL when creating BaseResource: returning."); - return; - } - - delegate.setResource(resource); - resource.delegate = delegate; - resource.setShouldCompressUploadedEntity(true); - resource.setShouldChunkUploadsHint(false); // Telemetry servers don't support chunking. - - // We're in a background thread so we don't have any reason to do this asynchronously. - // If we tried, onStartCommand would return and IntentService might stop itself before we finish. - resource.postBlocking(payload); - } - - private static boolean isReadyToUpload(final Context context, final Intent intent) { - // Sanity check: is upload enabled? Generally, the caller should check this before starting the service. - // Since we don't have the profile here, we rely on the caller to check the enabled state for the profile. - if (!isUploadEnabledByAppConfig(context)) { - Log.w(LOGTAG, "Upload is not available by configuration; returning"); - return false; - } - - if (!NetworkUtils.isConnected(context)) { - Log.w(LOGTAG, "Network is not connected; returning"); - return false; - } - - if (!isIntentValid(intent)) { - Log.w(LOGTAG, "Received invalid Intent; returning"); - return false; - } - - if (!ACTION_UPLOAD.equals(intent.getAction())) { - Log.w(LOGTAG, "Unknown action: " + intent.getAction() + ". Returning"); - return false; - } - - return true; - } - - /** - * Determines if the telemetry upload feature is enabled via the application configuration. Prefer to use - * {@link #isUploadEnabledByProfileConfig(Context, GeckoProfile)} if the profile is available as it takes into - * account more information. - * - * You may wish to also check if the network is connected when calling this method. - * - * Note that this method logs debug statements when upload is disabled. - */ - public static boolean isUploadEnabledByAppConfig(final Context context) { - if (!TelemetryConstants.UPLOAD_ENABLED) { - Log.d(LOGTAG, "Telemetry upload feature is compile-time disabled"); - return false; - } - - if (isDisabled) { - Log.d(LOGTAG, "Telemetry upload feature is disabled by intent (in testing?)"); - return false; - } - - if (!GeckoPreferences.getBooleanPref(context, GeckoPreferences.PREFS_HEALTHREPORT_UPLOAD_ENABLED, true)) { - Log.d(LOGTAG, "Telemetry upload opt-out"); - return false; - } - - if (Restrictions.isRestrictedProfile(context) && - !Restrictions.isAllowed(context, Restrictable.HEALTH_REPORT)) { - Log.d(LOGTAG, "Telemetry upload feature disabled by admin profile"); - return false; - } - - return true; - } - - /** - * Determines if the telemetry upload feature is enabled via profile & application level configurations. This is the - * preferred method. - * - * You may wish to also check if the network is connected when calling this method. - * - * Note that this method logs debug statements when upload is disabled. - */ - public static boolean isUploadEnabledByProfileConfig(final Context context, final GeckoProfile profile) { - if (profile.inGuestMode()) { - Log.d(LOGTAG, "Profile is in guest mode"); - return false; - } - - return isUploadEnabledByAppConfig(context); - } - - private static boolean isIntentValid(final Intent intent) { - // Intent can be null. Bug 1025937. - if (intent == null) { - Log.d(LOGTAG, "Received null intent"); - return false; - } - - if (intent.getParcelableExtra(EXTRA_STORE) == null) { - Log.d(LOGTAG, "Received invalid store in Intent"); - return false; - } - - return true; - } - - /** - * Logs on success & failure and appends the set ID to the given Set on success. - * - * Note: you *must* set the ping ID before attempting upload or we'll throw! - * - * We use mutation on the set ID and the successful upload array to avoid object allocation. - */ - private static class PingResultDelegate extends ResultDelegate { - // We persist pings and don't need to worry about losing data so we keep these - // durations short to save resources (e.g. battery). - private static final int SOCKET_TIMEOUT_MILLIS = (int) TimeUnit.SECONDS.toMillis(30); - private static final int CONNECTION_TIMEOUT_MILLIS = (int) TimeUnit.SECONDS.toMillis(30); - - /** The store ID of the ping currently being uploaded. Use {@link #getDocID()} to access it. */ - private String docID = null; - private final Set successfulUploadIDs; - - private boolean hadConnectionError = false; - - public PingResultDelegate(final Set successfulUploadIDs) { - super(); - this.successfulUploadIDs = successfulUploadIDs; - } - - @Override - public int socketTimeout() { - return SOCKET_TIMEOUT_MILLIS; - } - - @Override - public int connectionTimeout() { - return CONNECTION_TIMEOUT_MILLIS; - } - - private String getDocID() { - if (docID == null) { - throw new IllegalStateException("Expected ping ID to have been updated before retrieval"); - } - return docID; - } - - public void setDocID(final String id) { - docID = id; - } - - @Override - public String getUserAgent() { - return TelemetryConstants.USER_AGENT; - } - - @Override - public void handleHttpResponse(final HttpResponse response) { - final int status = response.getStatusLine().getStatusCode(); - switch (status) { - case 200: - case 201: - successfulUploadIDs.add(getDocID()); - break; - default: - Log.w(LOGTAG, "Telemetry upload failure. HTTP status: " + status); - hadConnectionError = true; - } - } - - @Override - public void handleHttpProtocolException(final ClientProtocolException e) { - // We don't log the exception to prevent leaking user data. - Log.w(LOGTAG, "HttpProtocolException when trying to upload telemetry"); - hadConnectionError = true; - } - - @Override - public void handleHttpIOException(final IOException e) { - // We don't log the exception to prevent leaking user data. - Log.w(LOGTAG, "HttpIOException when trying to upload telemetry"); - hadConnectionError = true; - } - - @Override - public void handleTransportException(final GeneralSecurityException e) { - // We don't log the exception to prevent leaking user data. - Log.w(LOGTAG, "Transport exception when trying to upload telemetry"); - hadConnectionError = true; - } - - private boolean hadConnectionError() { - return hadConnectionError; - } - - @Override - public void addHeaders(final HttpRequestBase request, final DefaultHttpClient client) { - super.addHeaders(request, client); - request.addHeader(HttpHeaders.DATE, DateUtil.getDateInHTTPFormat(Calendar.getInstance().getTime())); - } - } - - /** - * A hack because I want to set the resource after the Delegate is constructed. - * Be sure to call {@link #setResource(Resource)}! - */ - private static abstract class ResultDelegate extends BaseResourceDelegate { - public ResultDelegate() { - super(null); - } - - protected void setResource(final Resource resource) { - this.resource = resource; - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/telemetry/measurements/CampaignIdMeasurements.java b/mobile/android/base/java/org/mozilla/gecko/telemetry/measurements/CampaignIdMeasurements.java deleted file mode 100644 index 61229b21b8..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/telemetry/measurements/CampaignIdMeasurements.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * 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.telemetry.measurements; - -import android.content.Context; -import android.support.annotation.NonNull; -import android.text.TextUtils; - -import org.mozilla.gecko.GeckoSharedPrefs; -import org.mozilla.gecko.adjust.AttributionHelperListener; - -/** - * A class to retrieve and store the campaign Id pref that is used when the Adjust SDK gives us - * new attribution from the {@link AttributionHelperListener}. - */ -public class CampaignIdMeasurements { - private static final String PREF_CAMPAIGN_ID = "measurements-campaignId"; - - public static String getCampaignIdFromPrefs(@NonNull final Context context) { - return GeckoSharedPrefs.forProfile(context) - .getString(PREF_CAMPAIGN_ID, null); - } - - public static void updateCampaignIdPref(@NonNull final Context context, @NonNull final String campaignId) { - if (TextUtils.isEmpty(campaignId)) { - return; - } - GeckoSharedPrefs.forProfile(context) - .edit() - .putString(PREF_CAMPAIGN_ID, campaignId) - .apply(); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/telemetry/measurements/SearchCountMeasurements.java b/mobile/android/base/java/org/mozilla/gecko/telemetry/measurements/SearchCountMeasurements.java deleted file mode 100644 index c08ad6c021..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/telemetry/measurements/SearchCountMeasurements.java +++ /dev/null @@ -1,100 +0,0 @@ -/* - * 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.telemetry.measurements; - -import android.content.SharedPreferences; -import android.support.annotation.NonNull; -import android.support.annotation.VisibleForTesting; -import org.mozilla.gecko.sync.ExtendedJSONObject; - -import java.util.Collections; -import java.util.HashSet; -import java.util.Set; - -/** - * A place to store and retrieve the number of times a user has searched with a specific engine from a - * specific location. This is designed for use as a telemetry core ping measurement. - * - * The implementation works by storing a preference for each engine-location pair and incrementing them - * each time {@link #incrementSearch(SharedPreferences, String, String)} is called. In order to - * retrieve the full set of keys later, we store all the available key names in another preference. - * - * When we retrieve the keys in {@link #getAndZeroSearch(SharedPreferences)} (using the set of keys - * preference), the values saved to the preferences are returned and the preferences are removed - * (i.e. zeroed) from Shared Preferences. The reason we remove the preferences (rather than actually - * zeroing them) is to avoid bloating shared preferences if 1) the set of engines ever changes or - * 2) we remove this feature. - * - * Since we increment a value on each successive search, which doesn't take up more space, we don't - * have to worry about using excess disk space if the measurements are never zeroed (e.g. telemetry - * upload is disabled). In the worst case, we overflow the integer and may return negative values. - * - * This class is thread-safe by locking access to its public methods. When this class was written, incrementing & - * retrieval were called from multiple threads so rather than enforcing the callers keep their threads straight, it - * was simpler to lock all access. - */ -public class SearchCountMeasurements { - /** The set of "engine + where" keys we've stored; used for retrieving stored engines. */ - @VisibleForTesting static final String PREF_SEARCH_KEYSET = "measurements-search-count-keyset"; - private static final String PREF_SEARCH_PREFIX = "measurements-search-count-engine-"; // + "engine.where" - - private SearchCountMeasurements() {} - - public static synchronized void incrementSearch(@NonNull final SharedPreferences prefs, - @NonNull final String engineIdentifier, @NonNull final String where) { - final String engineWhereStr = engineIdentifier + "." + where; - final String key = getEngineSearchCountKey(engineWhereStr); - - final int count = prefs.getInt(key, 0); - prefs.edit().putInt(key, count + 1).apply(); - - unionKeyToSearchKeyset(prefs, engineWhereStr); - } - - /** - * @param key Engine of the form, "engine.where" - */ - private static void unionKeyToSearchKeyset(@NonNull final SharedPreferences prefs, @NonNull final String key) { - final Set keysFromPrefs = prefs.getStringSet(PREF_SEARCH_KEYSET, Collections.emptySet()); - if (keysFromPrefs.contains(key)) { - return; - } - - // String set returned by shared prefs cannot be modified so we copy. - final Set keysToSave = new HashSet<>(keysFromPrefs); - keysToSave.add(key); - prefs.edit().putStringSet(PREF_SEARCH_KEYSET, keysToSave).apply(); - } - - /** - * Gets and zeroes search counts. - * - * We return ExtendedJSONObject for now because that's the format needed by the core telemetry ping. - */ - public static synchronized ExtendedJSONObject getAndZeroSearch(@NonNull final SharedPreferences prefs) { - final ExtendedJSONObject out = new ExtendedJSONObject(); - final SharedPreferences.Editor editor = prefs.edit(); - - final Set keysFromPrefs = prefs.getStringSet(PREF_SEARCH_KEYSET, Collections.emptySet()); - for (final String engineWhereStr : keysFromPrefs) { - final String key = getEngineSearchCountKey(engineWhereStr); - out.put(engineWhereStr, prefs.getInt(key, 0)); - editor.remove(key); - } - editor.remove(PREF_SEARCH_KEYSET) - .apply(); - return out; - } - - /** - * @param engineWhereStr string of the form "engine.where" - * @return the key for the engines' search counts in shared preferences - */ - @VisibleForTesting static String getEngineSearchCountKey(final String engineWhereStr) { - return PREF_SEARCH_PREFIX + engineWhereStr; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/telemetry/measurements/SessionMeasurements.java b/mobile/android/base/java/org/mozilla/gecko/telemetry/measurements/SessionMeasurements.java deleted file mode 100644 index 6f7d2127a1..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/telemetry/measurements/SessionMeasurements.java +++ /dev/null @@ -1,99 +0,0 @@ -/* - * 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.telemetry.measurements; - -import android.content.Context; -import android.content.SharedPreferences; -import android.support.annotation.UiThread; -import android.support.annotation.VisibleForTesting; -import org.mozilla.gecko.GeckoSharedPrefs; - -import java.util.concurrent.TimeUnit; - -/** - * A class to measure the number of user sessions & their durations. It was created for use with the - * telemetry core ping. A session is the time between {@link #recordSessionStart()} and - * {@link #recordSessionEnd(Context)}. - * - * This class is thread-safe, provided the thread annotations are followed. Under the hood, this class uses - * SharedPreferences & because there is no atomic getAndSet operation, we synchronize access to it. - */ -public class SessionMeasurements { - @VisibleForTesting static final String PREF_SESSION_COUNT = "measurements-session-count"; - @VisibleForTesting static final String PREF_SESSION_DURATION = "measurements-session-duration"; - - private boolean sessionStarted = false; - private long timeAtSessionStartNano = -1; - - @UiThread // we assume this will be called on the same thread as session end so we don't have to synchronize sessionStarted. - public void recordSessionStart() { - if (sessionStarted) { - throw new IllegalStateException("Trying to start session but it is already started"); - } - sessionStarted = true; - timeAtSessionStartNano = getSystemTimeNano(); - } - - @UiThread // we assume this will be called on the same thread as session start so we don't have to synchronize sessionStarted. - public void recordSessionEnd(final Context context) { - if (!sessionStarted) { - throw new IllegalStateException("Expected session to be started before session end is called"); - } - sessionStarted = false; - - final long sessionElapsedSeconds = TimeUnit.NANOSECONDS.toSeconds(getSystemTimeNano() - timeAtSessionStartNano); - final SharedPreferences sharedPrefs = getSharedPreferences(context); - synchronized (this) { - final int sessionCount = sharedPrefs.getInt(PREF_SESSION_COUNT, 0); - final long totalElapsedSeconds = sharedPrefs.getLong(PREF_SESSION_DURATION, 0); - sharedPrefs.edit() - .putInt(PREF_SESSION_COUNT, sessionCount + 1) - .putLong(PREF_SESSION_DURATION, totalElapsedSeconds + sessionElapsedSeconds) - .apply(); - } - } - - /** - * Gets the session measurements since the last time the measurements were last retrieved. - */ - public synchronized SessionMeasurementsContainer getAndResetSessionMeasurements(final Context context) { - final SharedPreferences sharedPrefs = getSharedPreferences(context); - final int sessionCount = sharedPrefs.getInt(PREF_SESSION_COUNT, 0); - final long totalElapsedSeconds = sharedPrefs.getLong(PREF_SESSION_DURATION, 0); - sharedPrefs.edit() - .putInt(PREF_SESSION_COUNT, 0) - .putLong(PREF_SESSION_DURATION, 0) - .apply(); - return new SessionMeasurementsContainer(sessionCount, totalElapsedSeconds); - } - - @VisibleForTesting SharedPreferences getSharedPreferences(final Context context) { - return GeckoSharedPrefs.forProfile(context); - } - - /** - * Returns (roughly) the system uptime in nanoseconds. A less coupled implementation would - * take this value from the caller of recordSession*, however, we do this internally to ensure - * the caller uses both a time system consistent between the start & end calls and uses the - * appropriate time system (i.e. not wall time, which can change when the clock is changed). - */ - @VisibleForTesting long getSystemTimeNano() { // TODO: necessary? - return System.nanoTime(); - } - - public static final class SessionMeasurementsContainer { - /** The number of sessions. */ - public final int sessionCount; - /** The number of seconds elapsed in ALL sessions included in {@link #sessionCount}. */ - public final long elapsedSeconds; - - private SessionMeasurementsContainer(final int sessionCount, final long elapsedSeconds) { - this.sessionCount = sessionCount; - this.elapsedSeconds = elapsedSeconds; - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/telemetry/pingbuilders/TelemetryCorePingBuilder.java b/mobile/android/base/java/org/mozilla/gecko/telemetry/pingbuilders/TelemetryCorePingBuilder.java deleted file mode 100644 index 3f5480f379..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/telemetry/pingbuilders/TelemetryCorePingBuilder.java +++ /dev/null @@ -1,247 +0,0 @@ -/* - * 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.telemetry.pingbuilders; - -import android.content.Context; -import android.content.SharedPreferences; -import android.os.Build; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.support.annotation.WorkerThread; -import android.text.TextUtils; - -import android.util.Log; -import org.mozilla.gecko.AppConstants; -import org.mozilla.gecko.GeckoProfile; -import org.mozilla.gecko.Locales; -import org.mozilla.gecko.search.SearchEngine; -import org.mozilla.gecko.sync.ExtendedJSONObject; -import org.mozilla.gecko.telemetry.TelemetryPing; -import org.mozilla.gecko.util.DateUtil; -import org.mozilla.gecko.Experiments; -import org.mozilla.gecko.util.StringUtils; - -import java.text.DateFormat; -import java.text.SimpleDateFormat; -import java.util.Calendar; -import java.util.Locale; -import java.util.concurrent.TimeUnit; - -/** - * Builds a {@link TelemetryPing} representing a core ping. - * - * See https://gecko.readthedocs.org/en/latest/toolkit/components/telemetry/telemetry/core-ping.html - * for details on the core ping. - */ -public class TelemetryCorePingBuilder extends TelemetryPingBuilder { - private static final String LOGTAG = StringUtils.safeSubstring(TelemetryCorePingBuilder.class.getSimpleName(), 0, 23); - - // For legacy reasons, this preference key is not namespaced with "core". - private static final String PREF_SEQ_COUNT = "telemetry-seqCount"; - - private static final String NAME = "core"; - private static final int VERSION_VALUE = 7; // For version history, see toolkit/components/telemetry/docs/core-ping.rst - private static final String OS_VALUE = "Android"; - - private static final String ARCHITECTURE = "arch"; - private static final String CAMPAIGN_ID = "campaignId"; - private static final String CLIENT_ID = "clientId"; - private static final String DEFAULT_SEARCH_ENGINE = "defaultSearch"; - private static final String DEVICE = "device"; - private static final String DISTRIBUTION_ID = "distributionId"; - private static final String EXPERIMENTS = "experiments"; - private static final String LOCALE = "locale"; - private static final String OS_ATTR = "os"; - private static final String OS_VERSION = "osversion"; - private static final String PING_CREATION_DATE = "created"; - private static final String PROFILE_CREATION_DATE = "profileDate"; - private static final String SEARCH_COUNTS = "searches"; - private static final String SEQ = "seq"; - private static final String SESSION_COUNT = "sessions"; - private static final String SESSION_DURATION = "durations"; - private static final String TIMEZONE_OFFSET = "tz"; - private static final String VERSION_ATTR = "v"; - - public TelemetryCorePingBuilder(final Context context) { - initPayloadConstants(context); - } - - private void initPayloadConstants(final Context context) { - payload.put(VERSION_ATTR, VERSION_VALUE); - payload.put(OS_ATTR, OS_VALUE); - - // We limit the device descriptor to 32 characters because it can get long. We give fewer characters to the - // manufacturer because we're less likely to have manufacturers with similar names than we are for a - // manufacturer to have two devices with the similar names (e.g. Galaxy S6 vs. Galaxy Note 6). - final String deviceDescriptor = - StringUtils.safeSubstring(Build.MANUFACTURER, 0, 12) + '-' + StringUtils.safeSubstring(Build.MODEL, 0, 19); - - final Calendar nowCalendar = Calendar.getInstance(); - final DateFormat pingCreationDateFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.US); - - payload.put(ARCHITECTURE, AppConstants.ANDROID_CPU_ARCH); - payload.put(DEVICE, deviceDescriptor); - payload.put(LOCALE, Locales.getLanguageTag(Locale.getDefault())); - payload.put(OS_VERSION, Integer.toString(Build.VERSION.SDK_INT)); // A String for cross-platform reasons. - payload.put(PING_CREATION_DATE, pingCreationDateFormat.format(nowCalendar.getTime())); - payload.put(TIMEZONE_OFFSET, DateUtil.getTimezoneOffsetInMinutesForGivenDate(nowCalendar)); - payload.putArray(EXPERIMENTS, Experiments.getActiveExperiments(context)); - } - - @Override - public String getDocType() { - return NAME; - } - - @Override - public String[] getMandatoryFields() { - return new String[] { - ARCHITECTURE, - CLIENT_ID, - DEFAULT_SEARCH_ENGINE, - DEVICE, - LOCALE, - OS_ATTR, - OS_VERSION, - PING_CREATION_DATE, - PROFILE_CREATION_DATE, - SEQ, - TIMEZONE_OFFSET, - VERSION_ATTR, - }; - } - - public TelemetryCorePingBuilder setClientID(@NonNull final String clientID) { - if (clientID == null) { - throw new IllegalArgumentException("Expected non-null clientID"); - } - payload.put(CLIENT_ID, clientID); - return this; - } - - /** - * @param engine the default search engine identifier, or null if there is an error. - */ - public TelemetryCorePingBuilder setDefaultSearchEngine(@Nullable final String engine) { - if (engine != null && engine.isEmpty()) { - throw new IllegalArgumentException("Received empty string. Expected identifier or null."); - } - payload.put(DEFAULT_SEARCH_ENGINE, engine); - return this; - } - - public TelemetryCorePingBuilder setOptDistributionID(@NonNull final String distributionID) { - if (distributionID == null) { - throw new IllegalArgumentException("Expected non-null distribution ID"); - } - payload.put(DISTRIBUTION_ID, distributionID); - return this; - } - - /** - * @param searchCounts non-empty JSON with {"engine.where": } - */ - public TelemetryCorePingBuilder setOptSearchCounts(@NonNull final ExtendedJSONObject searchCounts) { - if (searchCounts == null) { - throw new IllegalStateException("Expected non-null search counts"); - } else if (searchCounts.size() == 0) { - throw new IllegalStateException("Expected non-empty search counts"); - } - - payload.put(SEARCH_COUNTS, searchCounts); - return this; - } - - public TelemetryCorePingBuilder setOptCampaignId(final String campaignId) { - if (campaignId == null) { - throw new IllegalStateException("Expected non-null campaign ID."); - } - payload.put(CAMPAIGN_ID, campaignId); - return this; - } - - /** - * @param date The profile creation date in days to the unix epoch (not millis!), or null if there is an error. - */ - public TelemetryCorePingBuilder setProfileCreationDate(@Nullable final Long date) { - if (date != null && date < 0) { - throw new IllegalArgumentException("Expect positive date value. Received: " + date); - } - payload.put(PROFILE_CREATION_DATE, date); - return this; - } - - /** - * @param seq a positive sequence number. - */ - public TelemetryCorePingBuilder setSequenceNumber(final int seq) { - if (seq < 0) { - // Since this is an increasing value, it's possible we can overflow into negative values and get into a - // crash loop so we don't crash on invalid arg - we can investigate if we see negative values on the server. - Log.w(LOGTAG, "Expected positive sequence number. Received: " + seq); - } - payload.put(SEQ, seq); - return this; - } - - public TelemetryCorePingBuilder setSessionCount(final int sessionCount) { - if (sessionCount < 0) { - // Since this is an increasing value, it's possible we can overflow into negative values and get into a - // crash loop so we don't crash on invalid arg - we can investigate if we see negative values on the server. - Log.w(LOGTAG, "Expected positive session count. Received: " + sessionCount); - } - payload.put(SESSION_COUNT, sessionCount); - return this; - } - - public TelemetryCorePingBuilder setSessionDuration(final long sessionDuration) { - if (sessionDuration < 0) { - // Since this is an increasing value, it's possible we can overflow into negative values and get into a - // crash loop so we don't crash on invalid arg - we can investigate if we see negative values on the server. - Log.w(LOGTAG, "Expected positive session duration. Received: " + sessionDuration); - } - payload.put(SESSION_DURATION, sessionDuration); - return this; - } - - /** - * Gets the sequence number from shared preferences and increments it in the prefs. This method - * is not thread safe. - */ - @WorkerThread // synchronous shared prefs write. - public static int getAndIncrementSequenceNumber(final SharedPreferences sharedPrefsForProfile) { - final int seq = sharedPrefsForProfile.getInt(PREF_SEQ_COUNT, 1); - - sharedPrefsForProfile.edit().putInt(PREF_SEQ_COUNT, seq + 1).apply(); - return seq; - } - - /** - * @return the profile creation date in the format expected by - * {@link TelemetryCorePingBuilder#setProfileCreationDate(Long)}. - */ - @WorkerThread - public static Long getProfileCreationDate(final Context context, final GeckoProfile profile) { - final long profileMillis = profile.getAndPersistProfileCreationDate(context); - if (profileMillis < 0) { - return null; - } - return (long) Math.floor((double) profileMillis / TimeUnit.DAYS.toMillis(1)); - } - - /** - * @return the search engine identifier in the format expected by the core ping. - */ - @Nullable - public static String getEngineIdentifier(@Nullable final SearchEngine searchEngine) { - if (searchEngine == null) { - return null; - } - final String identifier = searchEngine.getIdentifier(); - return TextUtils.isEmpty(identifier) ? null : identifier; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/telemetry/pingbuilders/TelemetryPingBuilder.java b/mobile/android/base/java/org/mozilla/gecko/telemetry/pingbuilders/TelemetryPingBuilder.java deleted file mode 100644 index 57fa0fd8bf..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/telemetry/pingbuilders/TelemetryPingBuilder.java +++ /dev/null @@ -1,87 +0,0 @@ -/* - * 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.telemetry.pingbuilders; - -import org.mozilla.gecko.AppConstants; -import org.mozilla.gecko.sync.ExtendedJSONObject; -import org.mozilla.gecko.telemetry.TelemetryPing; - -import java.util.Set; -import java.util.UUID; - -/** - * A generic Builder for {@link TelemetryPing} instances. Each overriding class is - * expected to create a specific type of ping (e.g. "core"). - * - * This base class handles the common ping operations under the hood: - * * Validating mandatory fields - * * Forming the server url - */ -abstract class TelemetryPingBuilder { - // In the server url, the initial path directly after the "scheme://host:port/" - private static final String SERVER_INITIAL_PATH = "submit/telemetry"; - - private final String serverPath; - protected final ExtendedJSONObject payload; - private final String docID; - - public TelemetryPingBuilder() { - docID = UUID.randomUUID().toString(); - serverPath = getTelemetryServerPath(getDocType(), docID); - payload = new ExtendedJSONObject(); - } - - /** - * @return the name of the ping (e.g. "core") - */ - public abstract String getDocType(); - - /** - * @return the fields that are mandatory for the resultant ping to be uploaded to - * the server. These will be validated before the ping is built. - */ - public abstract String[] getMandatoryFields(); - - public TelemetryPing build() { - validatePayload(); - return new TelemetryPing(serverPath, payload, docID); - } - - private void validatePayload() { - final Set keySet = payload.keySet(); - for (final String mandatoryField : getMandatoryFields()) { - if (!keySet.contains(mandatoryField)) { - throw new IllegalArgumentException("Builder does not contain mandatory field: " + - mandatoryField); - } - } - } - - /** - * Returns a url of the format: - * http://hostname/submit/telemetry/docId/docType/appName/appVersion/appUpdateChannel/appBuildID - * - * @param docType The name of the ping (e.g. "main") - * @return a url at which to POST the telemetry data to - */ - private static String getTelemetryServerPath(final String docType, final String docID) { - final String appName = AppConstants.MOZ_APP_BASENAME; - final String appVersion = AppConstants.MOZ_APP_VERSION; - final String appUpdateChannel = AppConstants.MOZ_UPDATE_CHANNEL; - final String appBuildId = AppConstants.MOZ_APP_BUILDID; - - // The compiler will optimize a single String concatenation into a StringBuilder statement. - // If you change this `return`, be sure to keep it as a single statement to keep it optimized! - return SERVER_INITIAL_PATH + '/' + - docID + '/' + - docType + '/' + - appName + '/' + - appVersion + '/' + - appUpdateChannel + '/' + - appBuildId; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/telemetry/schedulers/TelemetryUploadAllPingsImmediatelyScheduler.java b/mobile/android/base/java/org/mozilla/gecko/telemetry/schedulers/TelemetryUploadAllPingsImmediatelyScheduler.java deleted file mode 100644 index 047a646c30..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/telemetry/schedulers/TelemetryUploadAllPingsImmediatelyScheduler.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * 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.telemetry.schedulers; - -import android.content.Context; -import android.content.Intent; -import org.mozilla.gecko.telemetry.stores.TelemetryPingStore; -import org.mozilla.gecko.telemetry.TelemetryUploadService; - -/** - * Schedules an upload with all pings to be sent immediately. - */ -public class TelemetryUploadAllPingsImmediatelyScheduler implements TelemetryUploadScheduler { - - @Override - public boolean isReadyToUpload(final TelemetryPingStore store) { - // We're ready since we don't have any conditions to wait on (e.g. on wifi, accumulated X pings). - return true; - } - - @Override - public void scheduleUpload(final Context applicationContext, final TelemetryPingStore store) { - final Intent i = new Intent(TelemetryUploadService.ACTION_UPLOAD); - i.setClass(applicationContext, TelemetryUploadService.class); - i.putExtra(TelemetryUploadService.EXTRA_STORE, store); - applicationContext.startService(i); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/telemetry/schedulers/TelemetryUploadScheduler.java b/mobile/android/base/java/org/mozilla/gecko/telemetry/schedulers/TelemetryUploadScheduler.java deleted file mode 100644 index 63305aad5a..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/telemetry/schedulers/TelemetryUploadScheduler.java +++ /dev/null @@ -1,26 +0,0 @@ -/* - * 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.telemetry.schedulers; - -import android.content.Context; -import org.mozilla.gecko.telemetry.stores.TelemetryPingStore; - -/** - * An implementation of this class can investigate the given {@link TelemetryPingStore} to - * decide if it's ready to upload the pings inside that Store (e.g. on wifi? have we - * accumulated X pings?) and can schedule that upload. Typically, the upload will be - * scheduled by sending an {@link android.content.Intent} to the - * {@link org.mozilla.gecko.telemetry.TelemetryUploadService}, either immediately or - * via an external scheduler (e.g. {@link android.app.job.JobScheduler}). - * - * N.B.: If the Store is not ready to upload, an implementation *should not* try to reschedule - * the check to see if it's time to upload - this is expected to be handled by the caller. - */ -public interface TelemetryUploadScheduler { - boolean isReadyToUpload(TelemetryPingStore store); - void scheduleUpload(Context applicationContext, TelemetryPingStore store); -} diff --git a/mobile/android/base/java/org/mozilla/gecko/telemetry/stores/TelemetryJSONFilePingStore.java b/mobile/android/base/java/org/mozilla/gecko/telemetry/stores/TelemetryJSONFilePingStore.java deleted file mode 100644 index d523821465..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/telemetry/stores/TelemetryJSONFilePingStore.java +++ /dev/null @@ -1,301 +0,0 @@ -/* - * 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.telemetry.stores; - -import android.os.Parcel; -import android.os.Parcelable; -import android.support.annotation.VisibleForTesting; -import android.support.annotation.WorkerThread; -import android.util.Log; -import org.json.JSONException; -import org.json.JSONObject; -import org.mozilla.gecko.sync.ExtendedJSONObject; -import org.mozilla.gecko.sync.NonObjectJSONException; -import org.mozilla.gecko.telemetry.TelemetryPing; -import org.mozilla.gecko.util.FileUtils; -import org.mozilla.gecko.util.FileUtils.FileLastModifiedComparator; -import org.mozilla.gecko.util.FileUtils.FilenameRegexFilter; -import org.mozilla.gecko.util.FileUtils.FilenameWhitelistFilter; -import org.mozilla.gecko.util.StringUtils; -import org.mozilla.gecko.util.UUIDUtil; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.FilenameFilter; -import java.io.IOException; -import java.nio.channels.FileLock; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.Iterator; -import java.util.List; -import java.util.Set; -import java.util.SortedSet; -import java.util.TreeSet; - -/** - * An implementation of TelemetryPingStore that is backed by JSON files. - * - * This implementation seeks simplicity. Each ping to upload is stored in its own file with its doc ID - * as the filename. The doc ID is sent with a ping to be uploaded and is expected to be returned with - * {@link #onUploadAttemptComplete(Set)} so the associated file can be removed. - * - * During prune, the pings with the oldest modified time will be removed first. Different filesystems will - * handle clock skew (e.g. manual time changes, daylight savings time, changing timezones) in different ways - * and we accept that these modified times may not be consistent - newer data is not more important than - * older data and the choice to delete the oldest data first is largely arbitrary so we don't care if - * the timestamps are occasionally inconsistent. - * - * Using separate files for this store allows for less restrictive concurrency: - * * requires locking: {@link #storePing(TelemetryPing)} writes a new file - * * requires locking: {@link #getAllPings()} reads all files, including those potentially being written, - * hence locking - * * no locking: {@link #maybePrunePings()} deletes the least recently written pings, none of which should - * be currently written - * * no locking: {@link #onUploadAttemptComplete(Set)} deletes the given pings, none of which should be - * currently written - */ -public class TelemetryJSONFilePingStore extends TelemetryPingStore { - private static final String LOGTAG = StringUtils.safeSubstring( - "Gecko" + TelemetryJSONFilePingStore.class.getSimpleName(), 0, 23); - - @VisibleForTesting static final int MAX_PING_COUNT = 40; // TODO: value. - - // We keep the key names short to reduce storage size impact. - @VisibleForTesting static final String KEY_PAYLOAD = "p"; - @VisibleForTesting static final String KEY_URL_PATH = "u"; - - private final File storeDir; - private final FilenameFilter uuidFilenameFilter; - private final FileLastModifiedComparator fileLastModifiedComparator = new FileLastModifiedComparator(); - - @WorkerThread // Writes to disk - public TelemetryJSONFilePingStore(final File storeDir, final String profileName) { - super(profileName); - if (storeDir.exists() && !storeDir.isDirectory()) { - // An alternative is to create a new directory, but we wouldn't - // be able to access it later so it's better to throw. - throw new IllegalStateException("Store dir unexpectedly exists & is not a directory - cannot continue"); - } - - this.storeDir = storeDir; - this.storeDir.mkdirs(); - uuidFilenameFilter = new FilenameRegexFilter(UUIDUtil.UUID_PATTERN); - - if (!this.storeDir.canRead() || !this.storeDir.canWrite() || !this.storeDir.canExecute()) { - throw new IllegalStateException("Cannot read, write, or execute store dir: " + - this.storeDir.canRead() + " " + this.storeDir.canWrite() + " " + this.storeDir.canExecute()); - } - } - - @VisibleForTesting File getPingFile(final String docID) { - return new File(storeDir, docID); - } - - @Override - public void storePing(final TelemetryPing ping) throws IOException { - final String output; - try { - output = new JSONObject() - .put(KEY_PAYLOAD, ping.getPayload()) - .put(KEY_URL_PATH, ping.getURLPath()) - .toString(); - } catch (final JSONException e) { - // Do not log the exception to avoid leaking personal data. - throw new IOException("Unable to create JSON to store to disk"); - } - - final FileOutputStream outputStream = new FileOutputStream(getPingFile(ping.getDocID()), false); - blockForLockAndWriteFileAndCloseStream(outputStream, output); - } - - @Override - public void maybePrunePings() { - final File[] files = storeDir.listFiles(uuidFilenameFilter); - if (files == null) { - return; - } - - if (files.length < MAX_PING_COUNT) { - return; - } - - // It's possible that multiple files will have the same timestamp: in this case they are treated - // as equal by the fileLastModifiedComparator. We therefore have to use a sorted list (as - // opposed to a set, or map). - final ArrayList sortedFiles = new ArrayList<>(Arrays.asList(files)); - Collections.sort(sortedFiles, fileLastModifiedComparator); - deleteSmallestFiles(sortedFiles, files.length - MAX_PING_COUNT); - } - - private void deleteSmallestFiles(final ArrayList files, final int numFilesToRemove) { - final Iterator it = files.iterator(); - int i = 0; - - while (i < numFilesToRemove) { - i += 1; - - // Sorted list so we're iterating over ascending files. - final File file = it.next(); // file count > files to remove so this should not throw. - file.delete(); - } - } - - @Override - public ArrayList getAllPings() { - final File[] fileArray = storeDir.listFiles(uuidFilenameFilter); - if (fileArray == null) { - // Intentionally don't log all info for the store directory to prevent leaking the path. - Log.w(LOGTAG, "listFiles unexpectedly returned null - unable to retrieve pings. Debug: exists? " + - storeDir.exists() + "; directory? " + storeDir.isDirectory()); - return new ArrayList<>(1); - } - - final List files = Arrays.asList(fileArray); - Collections.sort(files, fileLastModifiedComparator); // oldest to newest - final ArrayList out = new ArrayList<>(files.size()); - for (final File file : files) { - final JSONObject obj = lockAndReadJSONFromFile(file); - if (obj == null) { - // We log in the method to get the JSONObject if we return null. - continue; - } - - try { - final String url = obj.getString(KEY_URL_PATH); - final ExtendedJSONObject payload = new ExtendedJSONObject(obj.getString(KEY_PAYLOAD)); - out.add(new TelemetryPing(url, payload, file.getName())); - } catch (final IOException | JSONException | NonObjectJSONException e) { - Log.w(LOGTAG, "Bad json in ping. Ignoring."); - continue; - } - } - return out; - } - - /** - * Logs if there is an error. - * - * @return the JSON object from the given file or null if there is an error. - */ - private JSONObject lockAndReadJSONFromFile(final File file) { - // lockAndReadFileAndCloseStream doesn't handle file size of 0. - if (file.length() == 0) { - Log.w(LOGTAG, "Unexpected empty file: " + file.getName() + ". Ignoring"); - return null; - } - - final FileInputStream inputStream; - try { - inputStream = new FileInputStream(file); - } catch (final FileNotFoundException e) { - // permission problem might also cause same exception. To get more debug information. - String fileInfo = String.format("existence: %b, can write: %b, size: %d.", - file.exists(), file.canWrite(), file.length()); - String msg = String.format( - "Expected file to exist but got exception in thread: %s. File info - %s", - Thread.currentThread().getName(), fileInfo); - throw new IllegalStateException(msg); - } - - final JSONObject obj; - try { - // Potential optimization: re-use the same buffer for reading from files. - obj = lockAndReadFileAndCloseStream(inputStream, (int) file.length()); - } catch (final IOException | JSONException e) { - // We couldn't read this file so let's just skip it. These potentially - // corrupted files should be removed when the data is pruned. - Log.w(LOGTAG, "Error when reading file: " + file.getName() + " Likely corrupted. Ignoring"); - return null; - } - - if (obj == null) { - Log.d(LOGTAG, "Could not read given file: " + file.getName() + " File is locked. Ignoring"); - } - return obj; - } - - @Override - public void onUploadAttemptComplete(final Set successfulRemoveIDs) { - if (successfulRemoveIDs.isEmpty()) { - return; - } - - final File[] files = storeDir.listFiles(new FilenameWhitelistFilter(successfulRemoveIDs)); - for (final File file : files) { - file.delete(); - } - } - - /** - * Locks the given {@link FileOutputStream} and writes the given String. This method will close the given stream. - * - * Note: this method blocks until a file lock can be acquired. - */ - private static void blockForLockAndWriteFileAndCloseStream(final FileOutputStream outputStream, final String str) - throws IOException { - try { - final FileLock lock = outputStream.getChannel().lock(0, Long.MAX_VALUE, false); - if (lock != null) { - // The file lock is released when the stream is closed. If we try to redundantly close it, we get - // a ClosedChannelException. To be safe, we could catch that every time but there is a performance - // hit to exception handling so instead we assume the file lock will be closed. - FileUtils.writeStringToOutputStreamAndCloseStream(outputStream, str); - } - } finally { - outputStream.close(); // redundant: closed when the stream is closed, but let's be safe. - } - } - - /** - * Locks the given {@link FileInputStream} and reads the data. This method will close the given stream. - * - * Note: this method returns null when a lock could not be acquired. - */ - private static JSONObject lockAndReadFileAndCloseStream(final FileInputStream inputStream, final int fileSize) - throws IOException, JSONException { - try { - final FileLock lock = inputStream.getChannel().tryLock(0, Long.MAX_VALUE, true); // null when lock not acquired - if (lock == null) { - return null; - } - // The file lock is released when the stream is closed. If we try to redundantly close it, we get - // a ClosedChannelException. To be safe, we could catch that every time but there is a performance - // hit to exception handling so instead we assume the file lock will be closed. - return new JSONObject(FileUtils.readStringFromInputStreamAndCloseStream(inputStream, fileSize)); - } finally { - inputStream.close(); // redundant: closed when the stream is closed, but let's be safe. - } - } - - public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { - @Override - public TelemetryJSONFilePingStore createFromParcel(final Parcel source) { - final String storeDirPath = source.readString(); - final String profileName = source.readString(); - return new TelemetryJSONFilePingStore(new File(storeDirPath), profileName); - } - - @Override - public TelemetryJSONFilePingStore[] newArray(final int size) { - return new TelemetryJSONFilePingStore[size]; - } - }; - - @Override - public int describeContents() { - return 0; - } - - @Override - public void writeToParcel(final Parcel dest, final int flags) { - dest.writeString(storeDir.getAbsolutePath()); - dest.writeString(getProfileName()); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/telemetry/stores/TelemetryPingStore.java b/mobile/android/base/java/org/mozilla/gecko/telemetry/stores/TelemetryPingStore.java deleted file mode 100644 index 7d781cf26b..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/telemetry/stores/TelemetryPingStore.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * 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.telemetry.stores; - -import android.os.Parcelable; -import org.mozilla.gecko.telemetry.TelemetryPing; - -import java.io.IOException; -import java.util.List; -import java.util.Set; - -/** - * Persistent storage for TelemetryPings that are queued for upload. - * - * An implementation of this class is expected to be thread-safe. Additionally, - * multiple instances can be created and run simultaneously so they must be able - * to synchronize state (or be stateless!). - * - * The pings in {@link #getAllPings()} and {@link #maybePrunePings()} are returned in the - * same order in order to guarantee consistent results. - */ -public abstract class TelemetryPingStore implements Parcelable { - private final String profileName; - - public TelemetryPingStore(final String profileName) { - this.profileName = profileName; - } - - /** - * @return the profile name associated with this store. - */ - public String getProfileName() { - return profileName; - } - - /** - * @return a list of all the telemetry pings in the store that are ready for upload, ascending oldest to newest. - */ - public abstract List getAllPings(); - - /** - * Save a ping to the store. - * - * @param ping the ping to store - * @throws IOException for underlying store access errors - */ - public abstract void storePing(TelemetryPing ping) throws IOException; - - /** - * Removes telemetry pings from the store if there are too many pings or they take up too much space. - * Pings should be removed from oldest to newest. - */ - public abstract void maybePrunePings(); - - /** - * Removes the successfully uploaded pings from the database and performs another other actions necessary - * for when upload is completed. - * - * @param successfulRemoveIDs doc ids of pings that were successfully uploaded - */ - public abstract void onUploadAttemptComplete(Set successfulRemoveIDs); -} diff --git a/mobile/android/base/java/org/mozilla/gecko/text/FloatingActionModeCallback.java b/mobile/android/base/java/org/mozilla/gecko/text/FloatingActionModeCallback.java deleted file mode 100644 index 07f17590d1..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/text/FloatingActionModeCallback.java +++ /dev/null @@ -1,69 +0,0 @@ -/* 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.text; - -import android.annotation.TargetApi; -import android.graphics.Rect; -import android.os.Build; -import android.view.ActionMode; -import android.view.Menu; -import android.view.MenuItem; -import android.view.View; - -import org.mozilla.gecko.GeckoAppShell; - -import java.util.List; - -@TargetApi(Build.VERSION_CODES.M) -public class FloatingActionModeCallback extends ActionMode.Callback2 { - private FloatingToolbarTextSelection textSelection; - private List actions; - - public FloatingActionModeCallback(FloatingToolbarTextSelection textSelection, List actions) { - this.textSelection = textSelection; - this.actions = actions; - } - - public void updateActions(List actions) { - this.actions = actions; - } - - @Override - public boolean onCreateActionMode(ActionMode mode, Menu menu) { - return true; - } - - @Override - public boolean onPrepareActionMode(ActionMode mode, Menu menu) { - menu.clear(); - - for (int i = 0; i < actions.size(); i++) { - final TextAction action = actions.get(i); - menu.add(Menu.NONE, i, action.getFloatingOrder(), action.getLabel()); - } - - return true; - } - - @Override - public boolean onActionItemClicked(ActionMode mode, MenuItem item) { - final TextAction action = actions.get(item.getItemId()); - - GeckoAppShell.notifyObservers("TextSelection:Action", action.getId()); - - return true; - } - - @Override - public void onDestroyActionMode(ActionMode mode) {} - - @Override - public void onGetContentRect(ActionMode mode, View view, Rect outRect) { - final Rect contentRect = textSelection.contentRect; - if (contentRect != null) { - outRect.set(contentRect); - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/text/FloatingToolbarTextSelection.java b/mobile/android/base/java/org/mozilla/gecko/text/FloatingToolbarTextSelection.java deleted file mode 100644 index 7a09624d41..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/text/FloatingToolbarTextSelection.java +++ /dev/null @@ -1,206 +0,0 @@ -/* 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.text; - -import android.annotation.TargetApi; -import android.app.Activity; -import android.graphics.Rect; -import android.os.Build; -import android.util.Log; -import android.util.TypedValue; -import android.view.ActionMode; - -import org.json.JSONException; -import org.json.JSONObject; - -import org.mozilla.gecko.EventDispatcher; -import org.mozilla.gecko.GeckoApp; -import org.mozilla.gecko.GeckoAppShell; -import org.mozilla.gecko.Telemetry; -import org.mozilla.gecko.TelemetryContract; -import org.mozilla.gecko.gfx.LayerView; -import org.mozilla.gecko.util.GeckoEventListener; -import org.mozilla.gecko.util.ThreadUtils; - -import java.util.List; - -import ch.boye.httpclientandroidlib.util.TextUtils; - -/** - * Floating toolbar for text selection actions. Only on Android 6+. - */ -@TargetApi(Build.VERSION_CODES.M) -public class FloatingToolbarTextSelection implements TextSelection, GeckoEventListener { - private static final String LOGTAG = "GeckoFloatTextSelection"; - - // This is an additional offset we add to the height of the selection. This will avoid that the - // floating toolbar overlays the bottom handle(s). - private static final int HANDLES_OFFSET_DP = 20; - - private final Activity activity; - private final LayerView layerView; - private final int[] locationInWindow; - private final float handlesOffset; - - private ActionMode actionMode; - private FloatingActionModeCallback actionModeCallback; - private String selectionID; - /* package-private */ Rect contentRect; - - public FloatingToolbarTextSelection(Activity activity, LayerView layerView) { - this.activity = activity; - this.layerView = layerView; - this.locationInWindow = new int[2]; - - this.handlesOffset = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, - HANDLES_OFFSET_DP, activity.getResources().getDisplayMetrics()); - } - - @Override - public boolean dismiss() { - if (finishActionMode()) { - endTextSelection(); - return true; - } - - return false; - } - - private void endTextSelection() { - if (TextUtils.isEmpty(selectionID)) { - return; - } - - final JSONObject args = new JSONObject(); - try { - args.put("selectionID", selectionID); - } catch (JSONException e) { - Log.e(LOGTAG, "Error building JSON arguments for TextSelection:End", e); - return; - } - - GeckoAppShell.notifyObservers("TextSelection:End", args.toString()); - } - - @Override - public void create() { - registerForEvents(); - } - - @Override - public void destroy() { - unregisterFromEvents(); - } - - private void registerForEvents() { - GeckoApp.getEventDispatcher().registerGeckoThreadListener(this, - "TextSelection:ActionbarInit", - "TextSelection:ActionbarStatus", - "TextSelection:ActionbarUninit", - "TextSelection:Update", - "TextSelection:Visibility"); - } - - private void unregisterFromEvents() { - GeckoApp.getEventDispatcher().unregisterGeckoThreadListener(this, - "TextSelection:ActionbarInit", - "TextSelection:ActionbarStatus", - "TextSelection:ActionbarUninit", - "TextSelection:Update", - "TextSelection:Visibility"); - } - - @Override - public void handleMessage(final String event, final JSONObject message) { - ThreadUtils.postToUiThread(new Runnable() { - @Override - public void run() { - handleOnMainThread(event, message); - } - }); - } - - private void handleOnMainThread(final String event, final JSONObject message) { - if ("TextSelection:ActionbarInit".equals(event)) { - Telemetry.sendUIEvent(TelemetryContract.Event.SHOW, - TelemetryContract.Method.CONTENT, "text_selection"); - - selectionID = message.optString("selectionID"); - } else if ("TextSelection:ActionbarStatus".equals(event)) { - // Ensure async updates from SearchService for example are valid. - if (selectionID != message.optString("selectionID")) { - return; - } - - updateRect(message); - - if (!isRectVisible()) { - finishActionMode(); - } else { - startActionMode(TextAction.fromEventMessage(message)); - } - } else if ("TextSelection:ActionbarUninit".equals(event)) { - finishActionMode(); - } else if ("TextSelection:Update".equals(event)) { - startActionMode(TextAction.fromEventMessage(message)); - } else if ("TextSelection:Visibility".equals(event)) { - finishActionMode(); - } - } - - private void startActionMode(List actions) { - if (actionMode != null) { - actionModeCallback.updateActions(actions); - actionMode.invalidate(); - return; - } - - actionModeCallback = new FloatingActionModeCallback(this, actions); - actionMode = activity.startActionMode(actionModeCallback, ActionMode.TYPE_FLOATING); - } - - private boolean finishActionMode() { - if (actionMode != null) { - actionMode.finish(); - actionMode = null; - actionModeCallback = null; - return true; - } - - return false; - } - - /** - * If the content rect is a point (left == right and top == bottom) then this means that the - * content rect is not in the currently visible part. - */ - private boolean isRectVisible() { - // There's another case of an empty rect where just left == right but not top == bottom. - // That's the rect for a collapsed selection. While technically this rect isn't visible too - // we are not interested in this case because we do not want to hide the toolbar. - return contentRect.left != contentRect.right || contentRect.top != contentRect.bottom; - } - - private void updateRect(JSONObject message) { - try { - final double x = message.getDouble("x"); - final double y = (int) message.getDouble("y"); - final double width = (int) message.getDouble("width"); - final double height = (int) message.getDouble("height"); - - final float zoomFactor = layerView.getZoomFactor(); - layerView.getLocationInWindow(locationInWindow); - - contentRect = new Rect( - (int) (x * zoomFactor + locationInWindow[0]), - (int) (y * zoomFactor + locationInWindow[1]), - (int) ((x + width) * zoomFactor + locationInWindow[0]), - (int) ((y + height) * zoomFactor + locationInWindow[1] + - (height > 0 ? handlesOffset : 0))); - } catch (JSONException e) { - Log.w(LOGTAG, "Could not calculate content rect", e); - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/text/TextAction.java b/mobile/android/base/java/org/mozilla/gecko/text/TextAction.java deleted file mode 100644 index 9fcbce4a4a..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/text/TextAction.java +++ /dev/null @@ -1,68 +0,0 @@ -/* 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.text; - -import android.util.Log; - -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - -import java.util.ArrayList; -import java.util.List; - -/** - * Text selection action like "copy", "paste", .. - */ -public class TextAction { - private static final String LOGTAG = "GeckoTextAction"; - - private String id; - private String label; - private int order; - private int floatingOrder; - - private TextAction() {} - - public static List fromEventMessage(JSONObject message) { - final List actions = new ArrayList<>(); - - try { - final JSONArray array = message.getJSONArray("actions"); - - for (int i = 0; i < array.length(); i++) { - final JSONObject object = array.getJSONObject(i); - - final TextAction action = new TextAction(); - action.id = object.getString("id"); - action.label = object.getString("label"); - action.order = object.getInt("order"); - action.floatingOrder = object.optInt("floatingOrder", i); - - actions.add(action); - } - } catch (JSONException e) { - Log.w(LOGTAG, "Could not parse text actions", e); - } - - return actions; - } - - public String getId() { - return id; - } - - public String getLabel() { - return label; - } - - public int getOrder() { - return order; - } - - public int getFloatingOrder() { - return floatingOrder; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/text/TextSelection.java b/mobile/android/base/java/org/mozilla/gecko/text/TextSelection.java deleted file mode 100644 index 29e8e43f58..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/text/TextSelection.java +++ /dev/null @@ -1,13 +0,0 @@ -/* 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.text; - -public interface TextSelection { - void create(); - - boolean dismiss(); - - void destroy(); -} diff --git a/mobile/android/base/java/org/mozilla/gecko/toolbar/AutocompleteHandler.java b/mobile/android/base/java/org/mozilla/gecko/toolbar/AutocompleteHandler.java deleted file mode 100644 index 4a1559823a..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/toolbar/AutocompleteHandler.java +++ /dev/null @@ -1,10 +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.toolbar; - -public interface AutocompleteHandler { - void onAutocomplete(String res); -} diff --git a/mobile/android/base/java/org/mozilla/gecko/toolbar/BackButton.java b/mobile/android/base/java/org/mozilla/gecko/toolbar/BackButton.java deleted file mode 100644 index 267c95e096..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/toolbar/BackButton.java +++ /dev/null @@ -1,26 +0,0 @@ -/* 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.toolbar; - -import android.content.Context; -import android.graphics.Path; -import android.util.AttributeSet; - -public class BackButton extends NavButton { - public BackButton(Context context, AttributeSet attrs) { - super(context, attrs); - } - - @Override - protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) { - super.onSizeChanged(width, height, oldWidth, oldHeight); - - mPath.reset(); - mPath.addCircle(width / 2, height / 2, width / 2, Path.Direction.CW); - - mBorderPath.reset(); - mBorderPath.addCircle(width / 2, height / 2, (width / 2) - (mBorderWidth / 2), Path.Direction.CW); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/toolbar/BrowserToolbar.java b/mobile/android/base/java/org/mozilla/gecko/toolbar/BrowserToolbar.java deleted file mode 100644 index b24e3b3ea5..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/toolbar/BrowserToolbar.java +++ /dev/null @@ -1,960 +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.toolbar; - -import java.util.ArrayList; -import java.util.EnumSet; -import java.util.List; - -import android.support.annotation.Nullable; -import android.support.v4.content.ContextCompat; -import org.mozilla.gecko.AppConstants.Versions; -import org.mozilla.gecko.BrowserApp; -import org.mozilla.gecko.GeckoAppShell; -import org.mozilla.gecko.R; -import org.mozilla.gecko.SiteIdentity; -import org.mozilla.gecko.Tab; -import org.mozilla.gecko.Tabs; -import org.mozilla.gecko.Telemetry; -import org.mozilla.gecko.TelemetryContract; -import org.mozilla.gecko.TouchEventInterceptor; -import org.mozilla.gecko.animation.PropertyAnimator; -import org.mozilla.gecko.animation.PropertyAnimator.PropertyAnimationListener; -import org.mozilla.gecko.animation.ViewHelper; -import org.mozilla.gecko.lwt.LightweightTheme; -import org.mozilla.gecko.lwt.LightweightThemeDrawable; -import org.mozilla.gecko.menu.GeckoMenu; -import org.mozilla.gecko.menu.MenuPopup; -import org.mozilla.gecko.tabs.TabHistoryController; -import org.mozilla.gecko.toolbar.ToolbarDisplayLayout.OnStopListener; -import org.mozilla.gecko.toolbar.ToolbarDisplayLayout.OnTitleChangeListener; -import org.mozilla.gecko.toolbar.ToolbarDisplayLayout.UpdateFlags; -import org.mozilla.gecko.util.Clipboard; -import org.mozilla.gecko.util.HardwareUtils; -import org.mozilla.gecko.util.MenuUtils; -import org.mozilla.gecko.widget.themed.ThemedFrameLayout; -import org.mozilla.gecko.widget.themed.ThemedImageButton; -import org.mozilla.gecko.widget.themed.ThemedImageView; -import org.mozilla.gecko.widget.themed.ThemedRelativeLayout; - -import android.content.Context; -import android.content.res.Resources; -import android.graphics.Canvas; -import android.graphics.Paint; -import android.graphics.drawable.Drawable; -import android.graphics.drawable.StateListDrawable; -import android.text.TextUtils; -import android.util.AttributeSet; -import android.util.Log; -import android.view.ContextMenu; -import android.view.LayoutInflater; -import android.view.MenuInflater; -import android.view.MotionEvent; -import android.view.View; -import android.view.ViewTreeObserver.OnGlobalLayoutListener; -import android.view.inputmethod.InputMethodManager; -import android.widget.Button; -import android.widget.LinearLayout; -import android.widget.PopupWindow; -import android.support.annotation.NonNull; - -/** -* {@code BrowserToolbar} is single entry point for users of the toolbar -* subsystem i.e. this should be the only import outside the 'toolbar' -* package. -* -* {@code BrowserToolbar} serves at the single event bus for all -* sub-components in the toolbar. It tracks tab events and gecko messages -* and update the state of its inner components accordingly. -* -* It has two states, display and edit, which are controlled by -* ToolbarEditLayout and ToolbarDisplayLayout. In display state, the toolbar -* displays the current state for the selected tab. In edit state, it shows -* a text entry for searching bookmarks/history. {@code BrowserToolbar} -* provides public API to enter, cancel, and commit the edit state as well -* as a set of listeners to allow {@code BrowserToolbar} users to react -* to state changes accordingly. -*/ -public abstract class BrowserToolbar extends ThemedRelativeLayout - implements Tabs.OnTabsChangedListener, - GeckoMenu.ActionItemBarPresenter { - private static final String LOGTAG = "GeckoToolbar"; - - private static final int LIGHTWEIGHT_THEME_INVERT_ALPHA = 34; // 255 - alpha = invert_alpha - - public interface OnActivateListener { - public void onActivate(); - } - - public interface OnCommitListener { - public void onCommit(); - } - - public interface OnDismissListener { - public void onDismiss(); - } - - public interface OnFilterListener { - public void onFilter(String searchText, AutocompleteHandler handler); - } - - public interface OnStartEditingListener { - public void onStartEditing(); - } - - public interface OnStopEditingListener { - public void onStopEditing(); - } - - protected enum UIMode { - EDIT, - DISPLAY - } - - protected final ToolbarDisplayLayout urlDisplayLayout; - protected final ToolbarEditLayout urlEditLayout; - protected final View urlBarEntry; - protected boolean isSwitchingTabs; - protected final ThemedImageButton tabsButton; - - private ToolbarProgressView progressBar; - protected final TabCounter tabsCounter; - protected final ThemedFrameLayout menuButton; - protected final ThemedImageView menuIcon; - private MenuPopup menuPopup; - protected final List focusOrder; - - private OnActivateListener activateListener; - private OnFocusChangeListener focusChangeListener; - private OnStartEditingListener startEditingListener; - private OnStopEditingListener stopEditingListener; - private TouchEventInterceptor mTouchEventInterceptor; - - protected final BrowserApp activity; - - protected UIMode uiMode; - protected TabHistoryController tabHistoryController; - - private final Paint shadowPaint; - private final int shadowColor; - private final int shadowPrivateColor; - private final int shadowSize; - - private final ToolbarPrefs prefs; - - public abstract boolean isAnimating(); - - protected abstract boolean isTabsButtonOffscreen(); - - protected abstract void updateNavigationButtons(Tab tab); - - protected abstract void triggerStartEditingTransition(PropertyAnimator animator); - protected abstract void triggerStopEditingTransition(); - public abstract void triggerTabsPanelTransition(PropertyAnimator animator, boolean areTabsShown); - - /** - * Returns a Drawable overlaid with the theme's bitmap. - */ - protected Drawable getLWTDefaultStateSetDrawable() { - return getTheme().getDrawable(this); - } - - public static BrowserToolbar create(final Context context, final AttributeSet attrs) { - final boolean isLargeResource = context.getResources().getBoolean(R.bool.is_large_resource); - final BrowserToolbar toolbar; - if (isLargeResource) { - toolbar = new BrowserToolbarTablet(context, attrs); - } else { - toolbar = new BrowserToolbarPhone(context, attrs); - } - return toolbar; - } - - protected BrowserToolbar(final Context context, final AttributeSet attrs) { - super(context, attrs); - setWillNotDraw(false); - - // BrowserToolbar is attached to BrowserApp only. - activity = (BrowserApp) context; - - LayoutInflater.from(context).inflate(R.layout.browser_toolbar, this); - - Tabs.registerOnTabsChangedListener(this); - isSwitchingTabs = true; - - urlDisplayLayout = (ToolbarDisplayLayout) findViewById(R.id.display_layout); - urlBarEntry = findViewById(R.id.url_bar_entry); - urlEditLayout = (ToolbarEditLayout) findViewById(R.id.edit_layout); - - tabsButton = (ThemedImageButton) findViewById(R.id.tabs); - tabsCounter = (TabCounter) findViewById(R.id.tabs_counter); - tabsCounter.setLayerType(View.LAYER_TYPE_SOFTWARE, null); - - menuButton = (ThemedFrameLayout) findViewById(R.id.menu); - menuIcon = (ThemedImageView) findViewById(R.id.menu_icon); - - // The focusOrder List should be filled by sub-classes. - focusOrder = new ArrayList(); - - final Resources res = getResources(); - shadowSize = res.getDimensionPixelSize(R.dimen.browser_toolbar_shadow_size); - - shadowPaint = new Paint(); - shadowColor = ContextCompat.getColor(context, R.color.url_bar_shadow); - shadowPrivateColor = ContextCompat.getColor(context, R.color.url_bar_shadow_private); - shadowPaint.setColor(shadowColor); - shadowPaint.setStrokeWidth(0.0f); - - setUIMode(UIMode.DISPLAY); - - prefs = new ToolbarPrefs(); - urlDisplayLayout.setToolbarPrefs(prefs); - urlEditLayout.setToolbarPrefs(prefs); - - setOnCreateContextMenuListener(new View.OnCreateContextMenuListener() { - @Override - public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) { - // Do not show the context menu while editing - if (isEditing()) { - return; - } - - // NOTE: Use MenuUtils.safeSetVisible because some actions might - // be on the Page menu - MenuInflater inflater = activity.getMenuInflater(); - inflater.inflate(R.menu.titlebar_contextmenu, menu); - - String clipboard = Clipboard.getText(); - if (TextUtils.isEmpty(clipboard)) { - menu.findItem(R.id.pasteandgo).setVisible(false); - menu.findItem(R.id.paste).setVisible(false); - } - - Tab tab = Tabs.getInstance().getSelectedTab(); - if (tab != null) { - String url = tab.getURL(); - if (url == null) { - menu.findItem(R.id.copyurl).setVisible(false); - menu.findItem(R.id.add_to_launcher).setVisible(false); - } - - MenuUtils.safeSetVisible(menu, R.id.subscribe, tab.hasFeeds()); - MenuUtils.safeSetVisible(menu, R.id.add_search_engine, tab.hasOpenSearch()); - } else { - // if there is no tab, remove anything tab dependent - menu.findItem(R.id.copyurl).setVisible(false); - menu.findItem(R.id.add_to_launcher).setVisible(false); - MenuUtils.safeSetVisible(menu, R.id.subscribe, false); - MenuUtils.safeSetVisible(menu, R.id.add_search_engine, false); - } - } - }); - - setOnClickListener(new OnClickListener() { - @Override - public void onClick(View v) { - if (activateListener != null) { - activateListener.onActivate(); - } - } - }); - } - - @Override - public void onAttachedToWindow() { - super.onAttachedToWindow(); - - prefs.open(); - - urlDisplayLayout.setOnStopListener(new OnStopListener() { - @Override - public Tab onStop() { - final Tab tab = Tabs.getInstance().getSelectedTab(); - if (tab != null) { - tab.doStop(); - return tab; - } - - return null; - } - }); - - urlDisplayLayout.setOnTitleChangeListener(new OnTitleChangeListener() { - @Override - public void onTitleChange(CharSequence title) { - final String contentDescription; - if (title != null) { - contentDescription = title.toString(); - } else { - contentDescription = activity.getString(R.string.url_bar_default_text); - } - - // The title and content description should - // always be sync. - setContentDescription(contentDescription); - } - }); - - urlEditLayout.setOnFocusChangeListener(new View.OnFocusChangeListener() { - @Override - public void onFocusChange(View v, boolean hasFocus) { - // This will select the url bar when entering editing mode. - setSelected(hasFocus); - if (focusChangeListener != null) { - focusChangeListener.onFocusChange(v, hasFocus); - } - } - }); - - tabsButton.setOnClickListener(new Button.OnClickListener() { - @Override - public void onClick(View v) { - // Clear focus so a back press with the tabs - // panel open does not go to the editing field. - urlEditLayout.clearFocus(); - - toggleTabs(); - } - }); - tabsButton.setImageLevel(0); - - menuButton.setOnClickListener(new Button.OnClickListener() { - @Override - public void onClick(View view) { - // Drop the soft keyboard. - urlEditLayout.clearFocus(); - activity.openOptionsMenu(); - } - }); - } - - @Override - public void onDetachedFromWindow() { - super.onDetachedFromWindow(); - - prefs.close(); - } - - @Override - public void draw(Canvas canvas) { - super.draw(canvas); - - final int height = getHeight(); - canvas.drawRect(0, height - shadowSize, getWidth(), height, shadowPaint); - } - - public void onParentFocus() { - urlEditLayout.onParentFocus(); - } - - public void setProgressBar(ToolbarProgressView progressBar) { - this.progressBar = progressBar; - } - - public void setTabHistoryController(TabHistoryController tabHistoryController) { - this.tabHistoryController = tabHistoryController; - } - - public void refresh() { - urlDisplayLayout.dismissSiteIdentityPopup(); - } - - public boolean onBackPressed() { - // If we exit editing mode during the animation, - // we're put into an inconsistent state (bug 1017276). - if (isEditing() && !isAnimating()) { - Telemetry.sendUIEvent(TelemetryContract.Event.CANCEL, - TelemetryContract.Method.BACK); - cancelEdit(); - return true; - } - - return urlDisplayLayout.dismissSiteIdentityPopup(); - } - - @Override - protected void onSizeChanged(int w, int h, int oldw, int oldh) { - super.onSizeChanged(w, h, oldw, oldh); - - if (h != oldh) { - // Post this to happen outside of onSizeChanged, as this may cause - // a layout change and relayouts within a layout change don't work. - post(new Runnable() { - @Override - public void run() { - activity.refreshToolbarHeight(); - } - }); - } - } - - public void saveTabEditingState(final TabEditingState editingState) { - urlEditLayout.saveTabEditingState(editingState); - } - - public void restoreTabEditingState(final TabEditingState editingState) { - if (!isEditing()) { - throw new IllegalStateException("Expected to be editing"); - } - - urlEditLayout.restoreTabEditingState(editingState); - } - - @Override - public void onTabChanged(@Nullable Tab tab, Tabs.TabEvents msg, String data) { - Log.d(LOGTAG, "onTabChanged: " + msg); - final Tabs tabs = Tabs.getInstance(); - - // These conditions are split into three phases: - // * Always do first - // * Handling specific to the selected tab - // * Always do afterwards. - - switch (msg) { - case ADDED: - case CLOSED: - updateTabCount(tabs.getDisplayCount()); - break; - case RESTORED: - // TabCount fixup after OOM - case SELECTED: - urlDisplayLayout.dismissSiteIdentityPopup(); - updateTabCount(tabs.getDisplayCount()); - isSwitchingTabs = true; - break; - } - - if (tabs.isSelectedTab(tab)) { - final EnumSet flags = EnumSet.noneOf(UpdateFlags.class); - - // Progress-related handling - switch (msg) { - case START: - updateProgressVisibility(tab, Tab.LOAD_PROGRESS_INIT); - // Fall through. - case ADDED: - case LOCATION_CHANGE: - case LOAD_ERROR: - case LOADED: - case STOP: - flags.add(UpdateFlags.PROGRESS); - if (progressBar.getVisibility() == View.VISIBLE) { - progressBar.animateProgress(tab.getLoadProgress()); - } - break; - - case SELECTED: - flags.add(UpdateFlags.PROGRESS); - updateProgressVisibility(); - break; - } - - switch (msg) { - case STOP: - // Reset the title in case we haven't navigated - // to a new page yet. - flags.add(UpdateFlags.TITLE); - // Fall through. - case START: - case CLOSED: - case ADDED: - updateNavigationButtons(tab); - break; - - case SELECTED: - flags.add(UpdateFlags.PRIVATE_MODE); - setPrivateMode(tab.isPrivate()); - // Fall through. - case LOAD_ERROR: - case LOCATION_CHANGE: - // We're displaying the tab URL in place of the title, - // so we always need to update our "title" here as well. - flags.add(UpdateFlags.TITLE); - flags.add(UpdateFlags.FAVICON); - flags.add(UpdateFlags.SITE_IDENTITY); - - updateNavigationButtons(tab); - break; - - case TITLE: - flags.add(UpdateFlags.TITLE); - break; - - case FAVICON: - flags.add(UpdateFlags.FAVICON); - break; - - case SECURITY_CHANGE: - flags.add(UpdateFlags.SITE_IDENTITY); - break; - } - - if (!flags.isEmpty() && tab != null) { - updateDisplayLayout(tab, flags); - } - } - - switch (msg) { - case SELECTED: - case LOAD_ERROR: - case LOCATION_CHANGE: - isSwitchingTabs = false; - } - } - - private void updateProgressVisibility() { - final Tab selectedTab = Tabs.getInstance().getSelectedTab(); - // The selected tab may be null if GeckoApp (and thus the - // selected tab) are not yet initialized (bug 1090287). - if (selectedTab != null) { - updateProgressVisibility(selectedTab, selectedTab.getLoadProgress()); - } - } - - private void updateProgressVisibility(Tab selectedTab, int progress) { - if (!isEditing() && selectedTab.getState() == Tab.STATE_LOADING) { - progressBar.setProgress(progress); - progressBar.setPrivateMode(selectedTab.isPrivate()); - progressBar.setVisibility(View.VISIBLE); - } else { - progressBar.setVisibility(View.GONE); - } - } - - protected boolean isVisible() { - return ViewHelper.getTranslationY(this) == 0; - } - - @Override - public void setNextFocusDownId(int nextId) { - super.setNextFocusDownId(nextId); - tabsButton.setNextFocusDownId(nextId); - urlDisplayLayout.setNextFocusDownId(nextId); - menuButton.setNextFocusDownId(nextId); - } - - public boolean hideVirtualKeyboard() { - InputMethodManager imm = - (InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE); - return imm.hideSoftInputFromWindow(tabsButton.getWindowToken(), 0); - } - - private void showSelectedTabs() { - Tab tab = Tabs.getInstance().getSelectedTab(); - if (tab != null) { - if (!tab.isPrivate()) - activity.showNormalTabs(); - else - activity.showPrivateTabs(); - } - } - - private void toggleTabs() { - if (activity.areTabsShown()) { - return; - } - - if (hideVirtualKeyboard()) { - getViewTreeObserver().addOnGlobalLayoutListener(new OnGlobalLayoutListener() { - @Override - public void onGlobalLayout() { - getViewTreeObserver().removeGlobalOnLayoutListener(this); - showSelectedTabs(); - } - }); - } else { - showSelectedTabs(); - } - } - - protected void updateTabCount(final int count) { - // If toolbar is in edit mode on a phone, this means the entry is expanded - // and the tabs button is translated offscreen. Don't trigger tabs counter - // updates until the tabs button is back on screen. - // See stopEditing() - if (isTabsButtonOffscreen()) { - return; - } - - // Set TabCounter based on visibility - if (isVisible() && ViewHelper.getAlpha(tabsCounter) != 0 && !isEditing()) { - tabsCounter.setCountWithAnimation(count); - } else { - tabsCounter.setCount(count); - } - - // Update A11y information - tabsButton.setContentDescription((count > 1) ? - activity.getString(R.string.num_tabs, count) : - activity.getString(R.string.one_tab)); - } - - private void updateDisplayLayout(@NonNull Tab tab, EnumSet flags) { - if (isSwitchingTabs) { - flags.add(UpdateFlags.DISABLE_ANIMATIONS); - } - - urlDisplayLayout.updateFromTab(tab, flags); - - if (flags.contains(UpdateFlags.TITLE)) { - if (!isEditing()) { - urlEditLayout.setText(tab.getURL()); - } - } - - if (flags.contains(UpdateFlags.PROGRESS)) { - updateFocusOrder(); - } - } - - private void updateFocusOrder() { - if (focusOrder.size() == 0) { - throw new IllegalStateException("Expected focusOrder to be initialized in subclass"); - } - - View prevView = null; - - // If the element that has focus becomes disabled or invisible, focus - // is given to the URL bar. - boolean needsNewFocus = false; - - for (View view : focusOrder) { - if (view.getVisibility() != View.VISIBLE || !view.isEnabled()) { - if (view.hasFocus()) { - needsNewFocus = true; - } - continue; - } - - if (view.getId() == R.id.menu_items) { - final LinearLayout actionItemBar = (LinearLayout) view; - final int childCount = actionItemBar.getChildCount(); - for (int child = 0; child < childCount; child++) { - View childView = actionItemBar.getChildAt(child); - if (prevView != null) { - childView.setNextFocusLeftId(prevView.getId()); - prevView.setNextFocusRightId(childView.getId()); - } - prevView = childView; - } - } else { - if (prevView != null) { - view.setNextFocusLeftId(prevView.getId()); - prevView.setNextFocusRightId(view.getId()); - } - prevView = view; - } - } - - if (needsNewFocus) { - requestFocus(); - } - } - - public void setToolBarButtonsAlpha(float alpha) { - ViewHelper.setAlpha(tabsCounter, alpha); - if (!HardwareUtils.isTablet()) { - ViewHelper.setAlpha(menuIcon, alpha); - } - } - - public void onEditSuggestion(String suggestion) { - if (!isEditing()) { - return; - } - - urlEditLayout.onEditSuggestion(suggestion); - } - - public void setTitle(CharSequence title) { - urlDisplayLayout.setTitle(title); - } - - public void setOnActivateListener(final OnActivateListener listener) { - activateListener = listener; - } - - public void setOnCommitListener(OnCommitListener listener) { - urlEditLayout.setOnCommitListener(listener); - } - - public void setOnDismissListener(OnDismissListener listener) { - urlEditLayout.setOnDismissListener(listener); - } - - public void setOnFilterListener(OnFilterListener listener) { - urlEditLayout.setOnFilterListener(listener); - } - - @Override - public void setOnFocusChangeListener(OnFocusChangeListener listener) { - focusChangeListener = listener; - } - - public void setOnStartEditingListener(OnStartEditingListener listener) { - startEditingListener = listener; - } - - public void setOnStopEditingListener(OnStopEditingListener listener) { - stopEditingListener = listener; - } - - protected void showUrlEditLayout() { - setUrlEditLayoutVisibility(true, null); - } - - protected void showUrlEditLayout(final PropertyAnimator animator) { - setUrlEditLayoutVisibility(true, animator); - } - - protected void hideUrlEditLayout() { - setUrlEditLayoutVisibility(false, null); - } - - protected void hideUrlEditLayout(final PropertyAnimator animator) { - setUrlEditLayoutVisibility(false, animator); - } - - protected void setUrlEditLayoutVisibility(final boolean showEditLayout, PropertyAnimator animator) { - if (showEditLayout) { - urlEditLayout.prepareShowAnimation(animator); - } - - // If this view is GONE, we trigger a measure pass when setting the view to - // VISIBLE. Since this will occur during the toolbar open animation, it causes jank. - final int hiddenViewVisibility = View.INVISIBLE; - - if (animator == null) { - final View viewToShow = (showEditLayout ? urlEditLayout : urlDisplayLayout); - final View viewToHide = (showEditLayout ? urlDisplayLayout : urlEditLayout); - - viewToHide.setVisibility(hiddenViewVisibility); - viewToShow.setVisibility(View.VISIBLE); - return; - } - - animator.addPropertyAnimationListener(new PropertyAnimationListener() { - @Override - public void onPropertyAnimationStart() { - if (!showEditLayout) { - urlEditLayout.setVisibility(hiddenViewVisibility); - urlDisplayLayout.setVisibility(View.VISIBLE); - } - } - - @Override - public void onPropertyAnimationEnd() { - if (showEditLayout) { - urlDisplayLayout.setVisibility(hiddenViewVisibility); - urlEditLayout.setVisibility(View.VISIBLE); - } - } - }); - } - - private void setUIMode(final UIMode uiMode) { - this.uiMode = uiMode; - urlEditLayout.setEnabled(uiMode == UIMode.EDIT); - } - - /** - * Returns whether or not the URL bar is in editing mode (url bar is expanded, hiding the new - * tab button). Note that selection state is independent of editing mode. - */ - public boolean isEditing() { - return (uiMode == UIMode.EDIT); - } - - public void startEditing(String url, PropertyAnimator animator) { - if (isEditing()) { - return; - } - - urlEditLayout.setText(url != null ? url : ""); - - setUIMode(UIMode.EDIT); - - updateProgressVisibility(); - - if (startEditingListener != null) { - startEditingListener.onStartEditing(); - } - - triggerStartEditingTransition(animator); - } - - /** - * Exits edit mode without updating the toolbar title. - * - * @return the url that was entered - */ - public String cancelEdit() { - Telemetry.stopUISession(TelemetryContract.Session.AWESOMESCREEN); - return stopEditing(); - } - - /** - * Exits edit mode, updating the toolbar title with the url that was just entered. - * - * @return the url that was entered - */ - public String commitEdit() { - Tab tab = Tabs.getInstance().getSelectedTab(); - if (tab != null) { - tab.resetSiteIdentity(); - } - - final String url = stopEditing(); - if (!TextUtils.isEmpty(url)) { - setTitle(url); - } - return url; - } - - private String stopEditing() { - final String url = urlEditLayout.getText(); - if (!isEditing()) { - return url; - } - setUIMode(UIMode.DISPLAY); - - if (stopEditingListener != null) { - stopEditingListener.onStopEditing(); - } - - updateProgressVisibility(); - triggerStopEditingTransition(); - - return url; - } - - @Override - public void setPrivateMode(boolean isPrivate) { - super.setPrivateMode(isPrivate); - - tabsButton.setPrivateMode(isPrivate); - menuButton.setPrivateMode(isPrivate); - urlEditLayout.setPrivateMode(isPrivate); - - shadowPaint.setColor(isPrivate ? shadowPrivateColor : shadowColor); - } - - public void show() { - setVisibility(View.VISIBLE); - } - - public void hide() { - setVisibility(View.GONE); - } - - public View getDoorHangerAnchor() { - return urlDisplayLayout; - } - - public void onDestroy() { - Tabs.unregisterOnTabsChangedListener(this); - urlDisplayLayout.destroy(); - } - - public boolean openOptionsMenu() { - // Initialize the popup. - if (menuPopup == null) { - View panel = activity.getMenuPanel(); - menuPopup = new MenuPopup(activity); - menuPopup.setPanelView(panel); - - menuPopup.setOnDismissListener(new PopupWindow.OnDismissListener() { - @Override - public void onDismiss() { - activity.onOptionsMenuClosed(null); - } - }); - } - - GeckoAppShell.getGeckoInterface().invalidateOptionsMenu(); - if (!menuPopup.isShowing()) { - menuPopup.showAsDropDown(menuButton); - } - - return true; - } - - public boolean closeOptionsMenu() { - if (menuPopup != null && menuPopup.isShowing()) { - menuPopup.dismiss(); - } - - return true; - } - - @Override - public void onLightweightThemeChanged() { - final Drawable drawable = getLWTDefaultStateSetDrawable(); - if (drawable == null) { - return; - } - - final StateListDrawable stateList = new StateListDrawable(); - stateList.addState(PRIVATE_STATE_SET, getColorDrawable(R.color.tabs_tray_grey_pressed)); - stateList.addState(EMPTY_STATE_SET, drawable); - - setBackgroundDrawable(stateList); - } - - public void setTouchEventInterceptor(TouchEventInterceptor interceptor) { - mTouchEventInterceptor = interceptor; - } - - @Override - public boolean onInterceptTouchEvent(MotionEvent event) { - if (mTouchEventInterceptor != null && mTouchEventInterceptor.onInterceptTouchEvent(this, event)) { - return true; - } - return super.onInterceptTouchEvent(event); - } - - @Override - public void onLightweightThemeReset() { - setBackgroundResource(R.drawable.url_bar_bg); - } - - public static LightweightThemeDrawable getLightweightThemeDrawable(final View view, - final LightweightTheme theme, final int colorResID) { - final int color = ContextCompat.getColor(view.getContext(), colorResID); - - final LightweightThemeDrawable drawable = theme.getColorDrawable(view, color); - if (drawable != null) { - drawable.setAlpha(LIGHTWEIGHT_THEME_INVERT_ALPHA, LIGHTWEIGHT_THEME_INVERT_ALPHA); - } - - return drawable; - } - - public static class TabEditingState { - // The edited text from the most recent time this tab was unselected. - protected String lastEditingText; - protected int selectionStart; - protected int selectionEnd; - - public boolean isBrowserSearchShown; - - public void copyFrom(final TabEditingState s2) { - lastEditingText = s2.lastEditingText; - selectionStart = s2.selectionStart; - selectionEnd = s2.selectionEnd; - - isBrowserSearchShown = s2.isBrowserSearchShown; - } - - public boolean isBrowserSearchShown() { - return isBrowserSearchShown; - } - - public void setIsBrowserSearchShown(final boolean isShown) { - isBrowserSearchShown = isShown; - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/toolbar/BrowserToolbarPhone.java b/mobile/android/base/java/org/mozilla/gecko/toolbar/BrowserToolbarPhone.java deleted file mode 100644 index a5fc57f1ad..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/toolbar/BrowserToolbarPhone.java +++ /dev/null @@ -1,128 +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.toolbar; - -import org.mozilla.gecko.Tabs; -import org.mozilla.gecko.animation.PropertyAnimator; -import org.mozilla.gecko.animation.PropertyAnimator.PropertyAnimationListener; -import org.mozilla.gecko.util.HardwareUtils; - -import android.content.Context; -import android.util.AttributeSet; -import android.view.View; - -/** - * A toolbar implementation for phones. - */ -class BrowserToolbarPhone extends BrowserToolbarPhoneBase { - - private final PropertyAnimationListener showEditingListener; - private final PropertyAnimationListener stopEditingListener; - - protected boolean isAnimatingEntry; - - protected BrowserToolbarPhone(final Context context, final AttributeSet attrs) { - super(context, attrs); - - // Create these listeners here, once, to avoid constructing new listeners - // each time they are set on an animator (i.e. each time the url bar is clicked). - showEditingListener = new PropertyAnimationListener() { - @Override - public void onPropertyAnimationStart() { /* Do nothing */ } - - @Override - public void onPropertyAnimationEnd() { - isAnimatingEntry = false; - } - }; - - stopEditingListener = new PropertyAnimationListener() { - @Override - public void onPropertyAnimationStart() { /* Do nothing */ } - - @Override - public void onPropertyAnimationEnd() { - urlBarTranslatingEdge.setVisibility(View.INVISIBLE); - - final PropertyAnimator buttonsAnimator = new PropertyAnimator(300); - urlDisplayLayout.prepareStopEditingAnimation(buttonsAnimator); - buttonsAnimator.start(); - - isAnimatingEntry = false; - - // Trigger animation to update the tabs counter once the - // tabs button is back on screen. - updateTabCountAndAnimate(Tabs.getInstance().getDisplayCount()); - } - }; - } - - @Override - public boolean isAnimating() { - return isAnimatingEntry; - } - - @Override - protected void triggerStartEditingTransition(final PropertyAnimator animator) { - if (isAnimatingEntry) { - return; - } - - // The animation looks cleaner if the text in the URL bar is - // not selected so clear the selection by clearing focus. - urlEditLayout.clearFocus(); - - urlDisplayLayout.prepareStartEditingAnimation(); - addAnimationsForEditing(animator, true); - showUrlEditLayout(animator); - urlBarTranslatingEdge.setVisibility(View.VISIBLE); - animator.addPropertyAnimationListener(showEditingListener); - - isAnimatingEntry = true; // To be correct, this should be called last. - } - - @Override - protected void triggerStopEditingTransition() { - final PropertyAnimator animator = new PropertyAnimator(250); - animator.setUseHardwareLayer(false); - - addAnimationsForEditing(animator, false); - hideUrlEditLayout(animator); - animator.addPropertyAnimationListener(stopEditingListener); - - isAnimatingEntry = true; - animator.start(); - } - - private void addAnimationsForEditing(final PropertyAnimator animator, final boolean isEditing) { - final int curveTranslation; - final int entryTranslation; - if (isEditing) { - curveTranslation = getUrlBarCurveTranslation(); - entryTranslation = getUrlBarEntryTranslation(); - } else { - curveTranslation = 0; - entryTranslation = 0; - } - - // Slide toolbar elements. - animator.attach(urlBarTranslatingEdge, - PropertyAnimator.Property.TRANSLATION_X, - entryTranslation); - animator.attach(tabsButton, - PropertyAnimator.Property.TRANSLATION_X, - curveTranslation); - animator.attach(tabsCounter, - PropertyAnimator.Property.TRANSLATION_X, - curveTranslation); - animator.attach(menuButton, - PropertyAnimator.Property.TRANSLATION_X, - curveTranslation); - animator.attach(menuIcon, - PropertyAnimator.Property.TRANSLATION_X, - curveTranslation); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/toolbar/BrowserToolbarPhoneBase.java b/mobile/android/base/java/org/mozilla/gecko/toolbar/BrowserToolbarPhoneBase.java deleted file mode 100644 index 5588ddcd3c..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/toolbar/BrowserToolbarPhoneBase.java +++ /dev/null @@ -1,219 +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.toolbar; - -import java.util.Arrays; - -import android.support.v4.content.ContextCompat; -import org.mozilla.gecko.R; -import org.mozilla.gecko.Tab; -import org.mozilla.gecko.Telemetry; -import org.mozilla.gecko.TelemetryContract; -import org.mozilla.gecko.animation.PropertyAnimator; -import org.mozilla.gecko.animation.ViewHelper; -import org.mozilla.gecko.widget.themed.ThemedImageView; - -import android.content.Context; -import android.content.res.Resources; -import android.graphics.Canvas; -import android.graphics.Paint; -import android.graphics.Path; -import android.util.AttributeSet; -import android.view.View; -import android.view.animation.AccelerateInterpolator; -import android.view.animation.Interpolator; -import android.widget.ImageView; - -/** - * A base implementations of the browser toolbar for phones. - * This class manages any Views, variables, etc. that are exclusive to phone. - */ -abstract class BrowserToolbarPhoneBase extends BrowserToolbar { - - protected final ImageView urlBarTranslatingEdge; - protected final ThemedImageView editCancel; - - private final Path roundCornerShape; - private final Paint roundCornerPaint; - - private final Interpolator buttonsInterpolator = new AccelerateInterpolator(); - - public BrowserToolbarPhoneBase(final Context context, final AttributeSet attrs) { - super(context, attrs); - final Resources res = context.getResources(); - - urlBarTranslatingEdge = (ImageView) findViewById(R.id.url_bar_translating_edge); - - // This will clip the translating edge's image at 60% of its width - urlBarTranslatingEdge.getDrawable().setLevel(6000); - - editCancel = (ThemedImageView) findViewById(R.id.edit_cancel); - - focusOrder.add(this); - focusOrder.addAll(urlDisplayLayout.getFocusOrder()); - focusOrder.addAll(Arrays.asList(tabsButton, menuButton)); - - roundCornerShape = new Path(); - roundCornerShape.moveTo(0, 0); - roundCornerShape.lineTo(30, 0); - roundCornerShape.cubicTo(0, 0, 0, 0, 0, 30); - roundCornerShape.lineTo(0, 0); - - roundCornerPaint = new Paint(); - roundCornerPaint.setAntiAlias(true); - roundCornerPaint.setColor(ContextCompat.getColor(context, R.color.text_and_tabs_tray_grey)); - roundCornerPaint.setStrokeWidth(0.0f); - } - - @Override - public void onAttachedToWindow() { - super.onAttachedToWindow(); - - editCancel.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View v) { - // If we exit editing mode during the animation, - // we're put into an inconsistent state (bug 1017276). - if (!isAnimating()) { - Telemetry.sendUIEvent(TelemetryContract.Event.CANCEL, - TelemetryContract.Method.ACTIONBAR, - getResources().getResourceEntryName(editCancel.getId())); - cancelEdit(); - } - } - }); - } - - @Override - public void setPrivateMode(final boolean isPrivate) { - super.setPrivateMode(isPrivate); - editCancel.setPrivateMode(isPrivate); - } - - @Override - protected boolean isTabsButtonOffscreen() { - return isEditing(); - } - - @Override - public boolean addActionItem(final View actionItem) { - // We have no action item bar. - return false; - } - - @Override - public void removeActionItem(final View actionItem) { - // We have no action item bar. - } - - @Override - protected void updateNavigationButtons(final Tab tab) { - // We have no navigation buttons so do nothing. - } - - @Override - public void draw(final Canvas canvas) { - super.draw(canvas); - - if (uiMode == UIMode.DISPLAY) { - canvas.drawPath(roundCornerShape, roundCornerPaint); - } - } - - @Override - public void triggerTabsPanelTransition(final PropertyAnimator animator, final boolean areTabsShown) { - if (areTabsShown) { - ViewHelper.setAlpha(tabsCounter, 0.0f); - ViewHelper.setAlpha(menuIcon, 0.0f); - return; - } - - final PropertyAnimator buttonsAnimator = - new PropertyAnimator(animator.getDuration(), buttonsInterpolator); - buttonsAnimator.attach(tabsCounter, - PropertyAnimator.Property.ALPHA, - 1.0f); - buttonsAnimator.attach(menuIcon, - PropertyAnimator.Property.ALPHA, - 1.0f); - buttonsAnimator.start(); - } - - /** - * Returns the number of pixels the url bar translating edge - * needs to translate to the right to enter its editing mode state. - * A negative value means the edge must translate to the left. - */ - protected int getUrlBarEntryTranslation() { - // Find the distance from the right-edge of the url bar (where we're translating from) to - // the left-edge of the cancel button (where we're translating to; note that the cancel - // button must be laid out, i.e. not View.GONE). - return editCancel.getLeft() - urlBarEntry.getRight(); - } - - protected int getUrlBarCurveTranslation() { - return getWidth() - tabsButton.getLeft(); - } - - protected void updateTabCountAndAnimate(final int count) { - // Don't animate if the toolbar is hidden. - if (!isVisible()) { - updateTabCount(count); - return; - } - - // If toolbar is in edit mode on a phone, this means the entry is expanded - // and the tabs button is translated offscreen. Don't trigger tabs counter - // updates until the tabs button is back on screen. - // See stopEditing() - if (!isTabsButtonOffscreen()) { - tabsCounter.setCount(count); - - tabsButton.setContentDescription((count > 1) ? - activity.getString(R.string.num_tabs, count) : - activity.getString(R.string.one_tab)); - } - } - - @Override - protected void setUrlEditLayoutVisibility(final boolean showEditLayout, - final PropertyAnimator animator) { - super.setUrlEditLayoutVisibility(showEditLayout, animator); - - if (animator == null) { - editCancel.setVisibility(showEditLayout ? View.VISIBLE : View.INVISIBLE); - return; - } - - animator.addPropertyAnimationListener(new PropertyAnimator.PropertyAnimationListener() { - @Override - public void onPropertyAnimationStart() { - if (!showEditLayout) { - editCancel.setVisibility(View.INVISIBLE); - } - } - - @Override - public void onPropertyAnimationEnd() { - if (showEditLayout) { - editCancel.setVisibility(View.VISIBLE); - } - } - }); - } - - @Override - public void onLightweightThemeChanged() { - super.onLightweightThemeChanged(); - editCancel.onLightweightThemeChanged(); - } - - @Override - public void onLightweightThemeReset() { - super.onLightweightThemeReset(); - editCancel.onLightweightThemeReset(); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/toolbar/BrowserToolbarTablet.java b/mobile/android/base/java/org/mozilla/gecko/toolbar/BrowserToolbarTablet.java deleted file mode 100644 index 2159341617..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/toolbar/BrowserToolbarTablet.java +++ /dev/null @@ -1,211 +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.toolbar; - -import org.mozilla.gecko.R; -import org.mozilla.gecko.animation.PropertyAnimator; -import org.mozilla.gecko.animation.ViewHelper; - -import android.content.Context; -import android.graphics.drawable.Drawable; -import android.util.AttributeSet; - -/** - * The toolbar implementation for tablet. - */ -class BrowserToolbarTablet extends BrowserToolbarTabletBase { - - private static final int FORWARD_ANIMATION_DURATION = 450; - - private enum ForwardButtonState { - HIDDEN, - DISPLAYED, - TRANSITIONING, - } - - private final int forwardButtonTranslationWidth; - - private ForwardButtonState forwardButtonState; - - private boolean backButtonWasEnabledOnStartEditing; - - public BrowserToolbarTablet(final Context context, final AttributeSet attrs) { - super(context, attrs); - - forwardButtonTranslationWidth = - getResources().getDimensionPixelOffset(R.dimen.tablet_nav_button_width); - - // The forward button is initially expanded (in the layout file) - // so translate it for start of the expansion animation; future - // iterations translate it to this position when hiding and will already be set up. - ViewHelper.setTranslationX(forwardButton, -forwardButtonTranslationWidth); - - // TODO: Move this to *TabletBase when old tablet is removed. - // We don't want users clicking the forward button in transitions, but we don't want it to - // look disabled to avoid flickering complications (e.g. disabled in editing mode), so undo - // the work of the super class' constructor. - forwardButton.setEnabled(true); - - updateForwardButtonState(ForwardButtonState.HIDDEN); - } - - private void updateForwardButtonState(final ForwardButtonState state) { - forwardButtonState = state; - forwardButton.setEnabled(forwardButtonState == ForwardButtonState.DISPLAYED); - } - - @Override - public boolean isAnimating() { - return false; - } - - @Override - protected void triggerStartEditingTransition(final PropertyAnimator animator) { - showUrlEditLayout(); - } - - @Override - protected void triggerStopEditingTransition() { - hideUrlEditLayout(); - } - - @Override - protected void animateForwardButton(final ForwardButtonAnimation animation) { - final boolean willShowForward = (animation == ForwardButtonAnimation.SHOW); - if ((forwardButtonState != ForwardButtonState.HIDDEN && willShowForward) || - (forwardButtonState != ForwardButtonState.DISPLAYED && !willShowForward)) { - return; - } - updateForwardButtonState(ForwardButtonState.TRANSITIONING); - - // We want the forward button to show immediately when switching tabs - final PropertyAnimator forwardAnim = - new PropertyAnimator(isSwitchingTabs ? 10 : FORWARD_ANIMATION_DURATION); - - forwardAnim.addPropertyAnimationListener(new PropertyAnimator.PropertyAnimationListener() { - @Override - public void onPropertyAnimationStart() { - if (!willShowForward) { - // Set the margin before the transition when hiding the forward button. We - // have to do this so that the favicon isn't clipped during the transition - MarginLayoutParams layoutParams = - (MarginLayoutParams) urlDisplayLayout.getLayoutParams(); - layoutParams.leftMargin = 0; - - // Do the same on the URL edit container - layoutParams = (MarginLayoutParams) urlEditLayout.getLayoutParams(); - layoutParams.leftMargin = 0; - - requestLayout(); - // Note, we already translated the favicon, site security, and text field - // in prepareForwardAnimation, so they should appear to have not moved at - // all at this point. - } - } - - @Override - public void onPropertyAnimationEnd() { - final ForwardButtonState newForwardButtonState; - if (willShowForward) { - // Increase the margins to ensure the text does not run outside the View. - MarginLayoutParams layoutParams = - (MarginLayoutParams) urlDisplayLayout.getLayoutParams(); - layoutParams.leftMargin = forwardButtonTranslationWidth; - - layoutParams = (MarginLayoutParams) urlEditLayout.getLayoutParams(); - layoutParams.leftMargin = forwardButtonTranslationWidth; - - newForwardButtonState = ForwardButtonState.DISPLAYED; - } else { - newForwardButtonState = ForwardButtonState.HIDDEN; - } - - urlDisplayLayout.finishForwardAnimation(); - updateForwardButtonState(newForwardButtonState); - - requestLayout(); - } - }); - - prepareForwardAnimation(forwardAnim, animation, forwardButtonTranslationWidth); - forwardAnim.start(); - } - - private void prepareForwardAnimation(PropertyAnimator anim, ForwardButtonAnimation animation, int width) { - if (animation == ForwardButtonAnimation.HIDE) { - anim.attach(forwardButton, - PropertyAnimator.Property.TRANSLATION_X, - -width); - anim.attach(forwardButton, - PropertyAnimator.Property.ALPHA, - 0); - - } else { - anim.attach(forwardButton, - PropertyAnimator.Property.TRANSLATION_X, - 0); - anim.attach(forwardButton, - PropertyAnimator.Property.ALPHA, - 1); - } - - urlDisplayLayout.prepareForwardAnimation(anim, animation, width); - } - - @Override - public void triggerTabsPanelTransition(final PropertyAnimator animator, final boolean areTabsShown) { - // Do nothing. - } - - @Override - public void setToolBarButtonsAlpha(float alpha) { - // Do nothing. - } - - - @Override - public void startEditing(final String url, final PropertyAnimator animator) { - // We already know the forward button state - no need to store it here. - backButtonWasEnabledOnStartEditing = backButton.isEnabled(); - - backButton.setEnabled(false); - forwardButton.setEnabled(false); - - super.startEditing(url, animator); - } - - @Override - public String commitEdit() { - stopEditingNewTablet(); - return super.commitEdit(); - } - - @Override - public String cancelEdit() { - // This can get called when we're not editing but we only want - // to make these changes when leaving editing mode. - if (isEditing()) { - stopEditingNewTablet(); - - backButton.setEnabled(backButtonWasEnabledOnStartEditing); - updateForwardButtonState(forwardButtonState); - } - - return super.cancelEdit(); - } - - private void stopEditingNewTablet() { - // Undo the changes caused by calling setEnabled for forwardButton in startEditing. - // Note that this should be called first so the enabled state of the - // forward button is set to the proper value. - forwardButton.setEnabled(true); - } - - @Override - protected Drawable getLWTDefaultStateSetDrawable() { - return BrowserToolbar.getLightweightThemeDrawable(this, getTheme(), R.color.toolbar_grey); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/toolbar/BrowserToolbarTabletBase.java b/mobile/android/base/java/org/mozilla/gecko/toolbar/BrowserToolbarTabletBase.java deleted file mode 100644 index e818bb95ca..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/toolbar/BrowserToolbarTabletBase.java +++ /dev/null @@ -1,182 +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.toolbar; - -import java.util.Arrays; - -import android.support.v4.content.ContextCompat; -import org.mozilla.gecko.R; -import org.mozilla.gecko.Tab; -import org.mozilla.gecko.Tabs; -import org.mozilla.gecko.tabs.TabHistoryController; -import org.mozilla.gecko.menu.MenuItemActionBar; -import org.mozilla.gecko.util.HardwareUtils; -import org.mozilla.gecko.widget.themed.ThemedTextView; - -import android.content.Context; -import android.graphics.PorterDuff; -import android.graphics.PorterDuffColorFilter; -import android.util.AttributeSet; -import android.view.View; -import android.view.ViewGroup.LayoutParams; -import android.widget.Button; -import android.widget.ImageButton; -import android.widget.LinearLayout; - -/** - * A base implementations of the browser toolbar for tablets. - * This class manages any Views, variables, etc. that are exclusive to tablet. - */ -abstract class BrowserToolbarTabletBase extends BrowserToolbar { - - protected enum ForwardButtonAnimation { - SHOW, - HIDE - } - - protected final LinearLayout actionItemBar; - - protected final BackButton backButton; - protected final ForwardButton forwardButton; - - protected final View menuButtonMarginView; - - private final PorterDuffColorFilter privateBrowsingTabletMenuItemColorFilter; - - protected abstract void animateForwardButton(ForwardButtonAnimation animation); - - public BrowserToolbarTabletBase(final Context context, final AttributeSet attrs) { - super(context, attrs); - - actionItemBar = (LinearLayout) findViewById(R.id.menu_items); - - backButton = (BackButton) findViewById(R.id.back); - backButton.setEnabled(false); - forwardButton = (ForwardButton) findViewById(R.id.forward); - forwardButton.setEnabled(false); - initButtonListeners(); - - focusOrder.addAll(Arrays.asList(tabsButton, (View) backButton, (View) forwardButton, this)); - focusOrder.addAll(urlDisplayLayout.getFocusOrder()); - focusOrder.addAll(Arrays.asList(actionItemBar, menuButton)); - - urlDisplayLayout.updateSiteIdentityAnchor(backButton); - - privateBrowsingTabletMenuItemColorFilter = new PorterDuffColorFilter( - ContextCompat.getColor(context, R.color.tabs_tray_icon_grey), PorterDuff.Mode.SRC_IN); - - menuButtonMarginView = findViewById(R.id.menu_margin); - if (menuButtonMarginView != null) { - menuButtonMarginView.setVisibility(View.VISIBLE); - } - } - - private void initButtonListeners() { - backButton.setOnClickListener(new Button.OnClickListener() { - @Override - public void onClick(View view) { - Tabs.getInstance().getSelectedTab().doBack(); - } - }); - backButton.setOnLongClickListener(new Button.OnLongClickListener() { - @Override - public boolean onLongClick(View view) { - return tabHistoryController.showTabHistory(Tabs.getInstance().getSelectedTab(), - TabHistoryController.HistoryAction.BACK); - } - }); - - forwardButton.setOnClickListener(new Button.OnClickListener() { - @Override - public void onClick(View view) { - Tabs.getInstance().getSelectedTab().doForward(); - } - }); - forwardButton.setOnLongClickListener(new Button.OnLongClickListener() { - @Override - public boolean onLongClick(View view) { - return tabHistoryController.showTabHistory(Tabs.getInstance().getSelectedTab(), - TabHistoryController.HistoryAction.FORWARD); - } - }); - } - - @Override - protected boolean isTabsButtonOffscreen() { - return false; - } - - @Override - public boolean addActionItem(final View actionItem) { - actionItemBar.addView(actionItem, LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT); - return true; - } - - @Override - public void removeActionItem(final View actionItem) { - actionItemBar.removeView(actionItem); - } - - @Override - protected void updateNavigationButtons(final Tab tab) { - backButton.setEnabled(canDoBack(tab)); - animateForwardButton( - canDoForward(tab) ? ForwardButtonAnimation.SHOW : ForwardButtonAnimation.HIDE); - } - - @Override - public void setNextFocusDownId(int nextId) { - super.setNextFocusDownId(nextId); - backButton.setNextFocusDownId(nextId); - forwardButton.setNextFocusDownId(nextId); - } - - @Override - public void setPrivateMode(final boolean isPrivate) { - super.setPrivateMode(isPrivate); - - // If we had backgroundTintList, we could remove the colorFilter - // code in favor of setPrivateMode (bug 1197432). - final PorterDuffColorFilter colorFilter = - isPrivate ? privateBrowsingTabletMenuItemColorFilter : null; - setTabsCounterPrivateMode(isPrivate, colorFilter); - - backButton.setPrivateMode(isPrivate); - forwardButton.setPrivateMode(isPrivate); - menuIcon.setPrivateMode(isPrivate); - for (int i = 0; i < actionItemBar.getChildCount(); ++i) { - final MenuItemActionBar child = (MenuItemActionBar) actionItemBar.getChildAt(i); - child.setPrivateMode(isPrivate); - } - } - - private void setTabsCounterPrivateMode(final boolean isPrivate, final PorterDuffColorFilter colorFilter) { - // The TabsCounter is a TextSwitcher which cycles two views - // to provide animations, hence looping over these two children. - for (int i = 0; i < 2; ++i) { - final ThemedTextView view = (ThemedTextView) tabsCounter.getChildAt(i); - view.setPrivateMode(isPrivate); - view.getBackground().mutate().setColorFilter(colorFilter); - } - - // To prevent animation of the background, - // it is set to a different Drawable. - tabsCounter.getBackground().mutate().setColorFilter(colorFilter); - } - - @Override - public View getDoorHangerAnchor() { - return backButton; - } - - protected boolean canDoBack(final Tab tab) { - return (tab.canDoBack() && !isEditing()); - } - - protected boolean canDoForward(final Tab tab) { - return (tab.canDoForward() && !isEditing()); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/toolbar/CanvasDelegate.java b/mobile/android/base/java/org/mozilla/gecko/toolbar/CanvasDelegate.java deleted file mode 100644 index 55567fba30..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/toolbar/CanvasDelegate.java +++ /dev/null @@ -1,62 +0,0 @@ -/* 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.toolbar; - -import org.mozilla.gecko.AppConstants.Versions; - -import android.graphics.Bitmap; -import android.graphics.Canvas; -import android.graphics.Paint; -import android.graphics.Path; -import android.graphics.PorterDuff.Mode; -import android.graphics.PorterDuffXfermode; -import android.graphics.Shader; - -class CanvasDelegate { - Paint mPaint; - PorterDuffXfermode mMode; - DrawManager mDrawManager; - - // DrawManager would do a default draw of the background. - static interface DrawManager { - public void defaultDraw(Canvas canvas); - } - - CanvasDelegate(DrawManager drawManager, Mode mode, Paint paint) { - mDrawManager = drawManager; - - // DST_IN masks, DST_OUT clips. - mMode = new PorterDuffXfermode(mode); - - mPaint = paint; - } - - void draw(Canvas canvas, Path path, int width, int height) { - // Save the canvas. All PorterDuff operations should be done in a offscreen bitmap. - int count = canvas.saveLayer(0, 0, width, height, null, - Canvas.MATRIX_SAVE_FLAG | - Canvas.CLIP_SAVE_FLAG | - Canvas.HAS_ALPHA_LAYER_SAVE_FLAG | - Canvas.FULL_COLOR_LAYER_SAVE_FLAG | - Canvas.CLIP_TO_LAYER_SAVE_FLAG); - - // Do a default draw. - mDrawManager.defaultDraw(canvas); - - if (path != null && !path.isEmpty()) { - // ICS added double-buffering, which made it easier for drawing the Path directly over the DST. - // In pre-ICS, drawPath() doesn't seem to use ARGB_8888 mode for performance, hence transparency is not preserved. - mPaint.setXfermode(mMode); - canvas.drawPath(path, mPaint); - } - - // Restore the canvas. - canvas.restoreToCount(count); - } - - void setShader(Shader shader) { - mPaint.setShader(shader); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/toolbar/ForwardButton.java b/mobile/android/base/java/org/mozilla/gecko/toolbar/ForwardButton.java deleted file mode 100644 index f95bb5e8a5..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/toolbar/ForwardButton.java +++ /dev/null @@ -1,23 +0,0 @@ -/* 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.toolbar; - -import android.content.Context; -import android.util.AttributeSet; - -public class ForwardButton extends NavButton { - public ForwardButton(Context context, AttributeSet attrs) { - super(context, attrs); - } - - @Override - protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) { - super.onSizeChanged(width, height, oldWidth, oldHeight); - - mBorderPath.reset(); - mBorderPath.moveTo(width - mBorderWidth, 0); - mBorderPath.lineTo(width - mBorderWidth, height); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/toolbar/NavButton.java b/mobile/android/base/java/org/mozilla/gecko/toolbar/NavButton.java deleted file mode 100644 index 68194e2227..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/toolbar/NavButton.java +++ /dev/null @@ -1,85 +0,0 @@ -/* 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.toolbar; - -import android.support.v4.content.ContextCompat; -import org.mozilla.gecko.R; - -import android.content.Context; -import android.content.res.Resources; -import android.graphics.Canvas; -import android.graphics.Paint; -import android.graphics.Path; -import android.graphics.drawable.Drawable; -import android.graphics.drawable.StateListDrawable; -import android.util.AttributeSet; - -abstract class NavButton extends ShapedButton { - protected final Path mBorderPath; - protected final Paint mBorderPaint; - protected final float mBorderWidth; - - protected final int mBorderColor; - protected final int mBorderColorPrivate; - - public NavButton(Context context, AttributeSet attrs) { - super(context, attrs); - - final Resources res = getResources(); - mBorderColor = ContextCompat.getColor(context, R.color.disabled_grey); - mBorderColorPrivate = ContextCompat.getColor(context, R.color.toolbar_icon_grey); - mBorderWidth = res.getDimension(R.dimen.nav_button_border_width); - - // Paint to draw the border. - mBorderPaint = new Paint(); - mBorderPaint.setAntiAlias(true); - mBorderPaint.setStrokeWidth(mBorderWidth); - mBorderPaint.setStyle(Paint.Style.STROKE); - - // Path is masked. - mBorderPath = new Path(); - - setPrivateMode(false); - } - - @Override - public void setPrivateMode(boolean isPrivate) { - super.setPrivateMode(isPrivate); - mBorderPaint.setColor(isPrivate ? mBorderColorPrivate : mBorderColor); - } - - @Override - public void draw(Canvas canvas) { - super.draw(canvas); - - // Draw the border on top. - canvas.drawPath(mBorderPath, mBorderPaint); - } - - // The drawable is constructed as per @drawable/url_bar_nav_button. - @Override - public void onLightweightThemeChanged() { - final Drawable drawable = BrowserToolbar.getLightweightThemeDrawable(this, getTheme(), R.color.toolbar_grey); - - if (drawable == null) { - return; - } - - final StateListDrawable stateList = new StateListDrawable(); - stateList.addState(PRIVATE_PRESSED_STATE_SET, getColorDrawable(R.color.placeholder_active_grey)); - stateList.addState(PRESSED_ENABLED_STATE_SET, getColorDrawable(R.color.toolbar_grey_pressed)); - stateList.addState(PRIVATE_FOCUSED_STATE_SET, getColorDrawable(R.color.text_and_tabs_tray_grey)); - stateList.addState(FOCUSED_STATE_SET, getColorDrawable(R.color.tablet_highlight_focused)); - stateList.addState(PRIVATE_STATE_SET, getColorDrawable(R.color.tabs_tray_grey_pressed)); - stateList.addState(EMPTY_STATE_SET, drawable); - - setBackgroundDrawable(stateList); - } - - @Override - public void onLightweightThemeReset() { - setBackgroundResource(R.drawable.url_bar_nav_button); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/toolbar/PageActionLayout.java b/mobile/android/base/java/org/mozilla/gecko/toolbar/PageActionLayout.java deleted file mode 100644 index 9361d59076..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/toolbar/PageActionLayout.java +++ /dev/null @@ -1,371 +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.toolbar; - -import org.mozilla.gecko.GeckoApp; -import org.mozilla.gecko.GeckoAppShell; -import org.mozilla.gecko.R; -import org.mozilla.gecko.util.ResourceDrawableUtils; -import org.mozilla.gecko.util.EventCallback; -import org.mozilla.gecko.util.NativeEventListener; -import org.mozilla.gecko.util.NativeJSObject; -import org.mozilla.gecko.util.ThreadUtils; -import org.mozilla.gecko.widget.GeckoPopupMenu; - -import android.content.Context; -import android.content.res.Resources; -import android.graphics.drawable.Drawable; -import android.os.Bundle; -import android.util.AttributeSet; -import android.view.Menu; -import android.view.MenuItem; -import android.view.View; -import android.widget.ImageButton; -import android.widget.ImageView; -import android.widget.LinearLayout; - -import java.util.Iterator; -import java.util.List; -import java.util.UUID; -import java.util.ArrayList; - -public class PageActionLayout extends LinearLayout implements NativeEventListener, - View.OnClickListener, - View.OnLongClickListener { - private static final String MENU_BUTTON_KEY = "MENU_BUTTON_KEY"; - private static final int DEFAULT_PAGE_ACTIONS_SHOWN = 2; - - private final Context mContext; - private final LinearLayout mLayout; - private final List mPageActionList; - - private GeckoPopupMenu mPageActionsMenu; - - // By default it's two, can be changed by calling setNumberShown(int) - private int mMaxVisiblePageActions; - - public PageActionLayout(Context context, AttributeSet attrs) { - super(context, attrs); - mContext = context; - mLayout = this; - - mPageActionList = new ArrayList(); - setNumberShown(DEFAULT_PAGE_ACTIONS_SHOWN); - refreshPageActionIcons(); - } - - @Override - protected void onAttachedToWindow() { - super.onAttachedToWindow(); - - GeckoApp.getEventDispatcher().registerGeckoThreadListener(this, - "PageActions:Add", - "PageActions:Remove"); - } - - @Override - protected void onDetachedFromWindow() { - GeckoApp.getEventDispatcher().unregisterGeckoThreadListener(this, - "PageActions:Add", - "PageActions:Remove"); - - super.onDetachedFromWindow(); - } - - private void setNumberShown(int count) { - ThreadUtils.assertOnUiThread(); - - mMaxVisiblePageActions = count; - - for (int index = 0; index < count; index++) { - if ((getChildCount() - 1) < index) { - mLayout.addView(createImageButton()); - } - } - } - - @Override - public void handleMessage(final String event, final NativeJSObject message, final EventCallback callback) { - // NativeJSObject cannot be used off of the Gecko thread, so convert it to a Bundle. - final Bundle bundle = message.toBundle(); - - ThreadUtils.postToUiThread(new Runnable() { - @Override - public void run() { - handleUiMessage(event, bundle); - } - }); - } - - private void handleUiMessage(final String event, final Bundle message) { - ThreadUtils.assertOnUiThread(); - - if (event.equals("PageActions:Add")) { - final String id = message.getString("id"); - final String title = message.getString("title"); - final String imageURL = message.getString("icon"); - final boolean important = message.getBoolean("important"); - - addPageAction(id, title, imageURL, new OnPageActionClickListeners() { - @Override - public void onClick(String id) { - GeckoAppShell.notifyObservers("PageActions:Clicked", id); - } - - @Override - public boolean onLongClick(String id) { - GeckoAppShell.notifyObservers("PageActions:LongClicked", id); - return true; - } - }, important); - } else if (event.equals("PageActions:Remove")) { - final String id = message.getString("id"); - - removePageAction(id); - } - } - - private void addPageAction(final String id, final String title, final String imageData, - final OnPageActionClickListeners onPageActionClickListeners, boolean important) { - ThreadUtils.assertOnUiThread(); - - final PageAction pageAction = new PageAction(id, title, null, onPageActionClickListeners, important); - - int insertAt = mPageActionList.size(); - while (insertAt > 0 && mPageActionList.get(insertAt - 1).isImportant()) { - insertAt--; - } - mPageActionList.add(insertAt, pageAction); - - ResourceDrawableUtils.getDrawable(mContext, imageData, new ResourceDrawableUtils.BitmapLoader() { - @Override - public void onBitmapFound(final Drawable d) { - if (mPageActionList.contains(pageAction)) { - pageAction.setDrawable(d); - refreshPageActionIcons(); - } - } - }); - } - - private void removePageAction(String id) { - ThreadUtils.assertOnUiThread(); - - final Iterator iter = mPageActionList.iterator(); - while (iter.hasNext()) { - final PageAction pageAction = iter.next(); - if (pageAction.getID().equals(id)) { - iter.remove(); - refreshPageActionIcons(); - return; - } - } - } - - private ImageButton createImageButton() { - ThreadUtils.assertOnUiThread(); - - final int width = mContext.getResources().getDimensionPixelSize(R.dimen.page_action_button_width); - ImageButton imageButton = new ImageButton(mContext, null, R.style.UrlBar_ImageButton); - imageButton.setLayoutParams(new LayoutParams(width, LayoutParams.MATCH_PARENT)); - imageButton.setScaleType(ImageView.ScaleType.CENTER_INSIDE); - imageButton.setOnClickListener(this); - imageButton.setOnLongClickListener(this); - return imageButton; - } - - @Override - public void onClick(View v) { - String buttonClickedId = (String)v.getTag(); - if (buttonClickedId != null) { - if (buttonClickedId.equals(MENU_BUTTON_KEY)) { - showMenu(v, mPageActionList.size() - mMaxVisiblePageActions + 1); - } else { - getPageActionWithId(buttonClickedId).onClick(); - } - } - } - - @Override - public boolean onLongClick(View v) { - String buttonClickedId = (String)v.getTag(); - if (buttonClickedId.equals(MENU_BUTTON_KEY)) { - showMenu(v, mPageActionList.size() - mMaxVisiblePageActions + 1); - return true; - } else { - return getPageActionWithId(buttonClickedId).onLongClick(); - } - } - - private void setActionForView(final ImageButton view, final PageAction pageAction) { - ThreadUtils.assertOnUiThread(); - - if (pageAction == null) { - view.setTag(null); - view.setImageDrawable(null); - view.setVisibility(View.GONE); - view.setContentDescription(null); - return; - } - - view.setTag(pageAction.getID()); - view.setImageDrawable(pageAction.getDrawable()); - view.setVisibility(View.VISIBLE); - view.setContentDescription(pageAction.getTitle()); - } - - private void refreshPageActionIcons() { - ThreadUtils.assertOnUiThread(); - - final Resources resources = mContext.getResources(); - for (int i = 0; i < this.getChildCount(); i++) { - final ImageButton v = (ImageButton) this.getChildAt(i); - final PageAction pageAction = getPageActionForViewAt(i); - - // If there are more page actions than buttons, set the menu icon. - // Otherwise, set the page action's icon if there is a page action. - if ((i == this.getChildCount() - 1) && (mPageActionList.size() > mMaxVisiblePageActions)) { - v.setTag(MENU_BUTTON_KEY); - v.setImageDrawable(resources.getDrawable(R.drawable.icon_pageaction)); - v.setVisibility((pageAction != null) ? View.VISIBLE : View.GONE); - v.setContentDescription(resources.getString(R.string.page_action_dropmarker_description)); - } else { - setActionForView(v, pageAction); - } - } - } - - private PageAction getPageActionForViewAt(int index) { - ThreadUtils.assertOnUiThread(); - - /** - * We show the user the most recent pageaction added since this keeps the user aware of any new page actions being added - * Also, the order of the pageAction is important i.e. if a page action is added, instead of shifting the pagactions to the - * left to make space for the new one, it would be more visually appealing to have the pageaction appear in the blank space. - * - * buttonIndex is needed for this reason because every new View added to PageActionLayout gets added to the right of its neighbouring View. - * Hence the button on the very leftmost has the index 0. We want our pageactions to start from the rightmost - * and hence we maintain the insertion order of the child Views which is essentially the reverse of their index - */ - - final int buttonIndex = (this.getChildCount() - 1) - index; - - if (mPageActionList.size() > buttonIndex) { - // Return the pageactions starting from the end of the list for the number of visible pageactions. - final int buttonCount = Math.min(mPageActionList.size(), getChildCount()); - return mPageActionList.get((mPageActionList.size() - buttonCount) + buttonIndex); - } - return null; - } - - private PageAction getPageActionWithId(String id) { - ThreadUtils.assertOnUiThread(); - - for (PageAction pageAction : mPageActionList) { - if (pageAction.getID().equals(id)) { - return pageAction; - } - } - return null; - } - - private void showMenu(View pageActionButton, int toShow) { - ThreadUtils.assertOnUiThread(); - - if (mPageActionsMenu == null) { - mPageActionsMenu = new GeckoPopupMenu(pageActionButton.getContext(), pageActionButton); - mPageActionsMenu.inflate(0); - mPageActionsMenu.setOnMenuItemClickListener(new GeckoPopupMenu.OnMenuItemClickListener() { - @Override - public boolean onMenuItemClick(MenuItem item) { - int id = item.getItemId(); - for (int i = 0; i < mPageActionList.size(); i++) { - PageAction pageAction = mPageActionList.get(i); - if (pageAction.key() == id) { - pageAction.onClick(); - return true; - } - } - return false; - } - }); - } - Menu menu = mPageActionsMenu.getMenu(); - menu.clear(); - - for (int i = 0; i < mPageActionList.size() && i < toShow; i++) { - PageAction pageAction = mPageActionList.get(i); - MenuItem item = menu.add(Menu.NONE, pageAction.key(), Menu.NONE, pageAction.getTitle()); - item.setIcon(pageAction.getDrawable()); - } - mPageActionsMenu.show(); - } - - private static interface OnPageActionClickListeners { - public void onClick(String id); - public boolean onLongClick(String id); - } - - private static class PageAction { - private final OnPageActionClickListeners mOnPageActionClickListeners; - private Drawable mDrawable; - private final String mTitle; - private final String mId; - private final int key; - private final boolean mImportant; - - public PageAction(String id, - String title, - Drawable image, - OnPageActionClickListeners onPageActionClickListeners, - boolean important) { - mId = id; - mTitle = title; - mDrawable = image; - mOnPageActionClickListeners = onPageActionClickListeners; - mImportant = important; - - key = UUID.fromString(mId.subSequence(1, mId.length() - 2).toString()).hashCode(); - } - - public Drawable getDrawable() { - return mDrawable; - } - - public void setDrawable(Drawable d) { - mDrawable = d; - } - - public String getTitle() { - return mTitle; - } - - public String getID() { - return mId; - } - - public int key() { - return key; - } - - public boolean isImportant() { - return mImportant; - } - - public void onClick() { - if (mOnPageActionClickListeners != null) { - mOnPageActionClickListeners.onClick(mId); - } - } - - public boolean onLongClick() { - if (mOnPageActionClickListeners != null) { - return mOnPageActionClickListeners.onLongClick(mId); - } - return false; - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/toolbar/PhoneTabsButton.java b/mobile/android/base/java/org/mozilla/gecko/toolbar/PhoneTabsButton.java deleted file mode 100644 index 4164854945..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/toolbar/PhoneTabsButton.java +++ /dev/null @@ -1,29 +0,0 @@ -/* 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.toolbar; - -import android.content.Context; -import android.util.AttributeSet; - -import org.mozilla.gecko.tabs.TabCurve; - -public class PhoneTabsButton extends ShapedButton { - public PhoneTabsButton(Context context, AttributeSet attrs) { - super(context, attrs); - } - - @Override - protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) { - super.onSizeChanged(width, height, oldWidth, oldHeight); - - mPath.reset(); - - mPath.moveTo(0, 0); - TabCurve.drawFromTop(mPath, 0, height, TabCurve.Direction.RIGHT); - mPath.lineTo(width, height); - mPath.lineTo(width, 0); - mPath.lineTo(0, 0); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/toolbar/ShapedButton.java b/mobile/android/base/java/org/mozilla/gecko/toolbar/ShapedButton.java deleted file mode 100644 index 003dada2da..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/toolbar/ShapedButton.java +++ /dev/null @@ -1,109 +0,0 @@ -/* 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.toolbar; - -import android.support.v4.content.ContextCompat; -import org.mozilla.gecko.R; -import org.mozilla.gecko.lwt.LightweightThemeDrawable; -import org.mozilla.gecko.widget.themed.ThemedImageButton; - -import android.annotation.SuppressLint; -import android.content.Context; -import android.graphics.Canvas; -import android.graphics.Paint; -import android.graphics.Path; -import android.graphics.PorterDuff.Mode; -import android.graphics.drawable.Drawable; -import android.graphics.drawable.StateListDrawable; -import android.util.AttributeSet; - -/** - * A ImageButton with a custom drawn path and lightweight theme support. Note that {@link ShapedButtonFrameLayout} - * copies the lwt support so if you change it here, you should probably change it there. - */ -public class ShapedButton extends ThemedImageButton - implements CanvasDelegate.DrawManager { - - protected final Path mPath; - protected final CanvasDelegate mCanvasDelegate; - - public ShapedButton(Context context, AttributeSet attrs) { - super(context, attrs); - - // Path is clipped. - mPath = new Path(); - - final Paint paint = new Paint(); - paint.setAntiAlias(true); - paint.setColor(ContextCompat.getColor(context, R.color.canvas_delegate_paint)); - paint.setStrokeWidth(0.0f); - mCanvasDelegate = new CanvasDelegate(this, Mode.DST_IN, paint); - - setWillNotDraw(false); - } - - @Override - @SuppressLint("MissingSuperCall") // Super gets called from defaultDraw(). - // It is intentionally not called in the other case. - public void draw(Canvas canvas) { - if (mCanvasDelegate != null) - mCanvasDelegate.draw(canvas, mPath, getWidth(), getHeight()); - else - defaultDraw(canvas); - } - - @Override - public void defaultDraw(Canvas canvas) { - super.draw(canvas); - } - - // The drawable is constructed as per @drawable/shaped_button. - @Override - public void onLightweightThemeChanged() { - final int background = ContextCompat.getColor(getContext(), R.color.text_and_tabs_tray_grey); - final LightweightThemeDrawable lightWeight = getTheme().getColorDrawable(this, background); - - if (lightWeight == null) - return; - - lightWeight.setAlpha(34, 34); - - final StateListDrawable stateList = new StateListDrawable(); - stateList.addState(PRESSED_ENABLED_STATE_SET, getColorDrawable(R.color.highlight_shaped)); - stateList.addState(FOCUSED_STATE_SET, getColorDrawable(R.color.highlight_shaped_focused)); - stateList.addState(PRIVATE_STATE_SET, getColorDrawable(R.color.text_and_tabs_tray_grey)); - stateList.addState(EMPTY_STATE_SET, lightWeight); - - setBackgroundDrawable(stateList); - } - - @Override - public void onLightweightThemeReset() { - setBackgroundResource(R.drawable.shaped_button); - } - - @Override - public void setBackgroundDrawable(Drawable drawable) { - if (getBackground() == null || drawable == null) { - super.setBackgroundDrawable(drawable); - return; - } - - int[] padding = new int[] { getPaddingLeft(), - getPaddingTop(), - getPaddingRight(), - getPaddingBottom() - }; - drawable.setLevel(getBackground().getLevel()); - super.setBackgroundDrawable(drawable); - - setPadding(padding[0], padding[1], padding[2], padding[3]); - } - - @Override - public void setBackgroundResource(int resId) { - setBackgroundDrawable(getResources().getDrawable(resId)); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/toolbar/ShapedButtonFrameLayout.java b/mobile/android/base/java/org/mozilla/gecko/toolbar/ShapedButtonFrameLayout.java deleted file mode 100644 index c14829aec2..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/toolbar/ShapedButtonFrameLayout.java +++ /dev/null @@ -1,74 +0,0 @@ -/* 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.toolbar; - -import android.support.v4.content.ContextCompat; -import org.mozilla.gecko.R; -import org.mozilla.gecko.lwt.LightweightThemeDrawable; -import org.mozilla.gecko.widget.themed.ThemedFrameLayout; - -import android.content.Context; -import android.graphics.drawable.Drawable; -import android.graphics.drawable.StateListDrawable; -import android.util.AttributeSet; - -/** A FrameLayout with lightweight theme support. Note that {@link ShapedButton}'s lwt support is basically the same so - * if you change it here, you should probably change it there. Note also that this doesn't have ShapedButton's path code - * so shouldn't have "ShapedButton" in the name, but I wanted to make the connection apparent so I left it. - */ -public class ShapedButtonFrameLayout extends ThemedFrameLayout { - - public ShapedButtonFrameLayout(Context context, AttributeSet attrs) { - super(context, attrs); - } - - // The drawable is constructed as per @drawable/shaped_button. - @Override - public void onLightweightThemeChanged() { - final int background = ContextCompat.getColor(getContext(), R.color.text_and_tabs_tray_grey); - final LightweightThemeDrawable lightWeight = getTheme().getColorDrawable(this, background); - - if (lightWeight == null) - return; - - lightWeight.setAlpha(34, 34); - - final StateListDrawable stateList = new StateListDrawable(); - stateList.addState(PRESSED_ENABLED_STATE_SET, getColorDrawable(R.color.highlight_shaped)); - stateList.addState(FOCUSED_STATE_SET, getColorDrawable(R.color.highlight_shaped_focused)); - stateList.addState(PRIVATE_STATE_SET, getColorDrawable(R.color.text_and_tabs_tray_grey)); - stateList.addState(EMPTY_STATE_SET, lightWeight); - - setBackgroundDrawable(stateList); - } - - @Override - public void onLightweightThemeReset() { - setBackgroundResource(R.drawable.shaped_button); - } - - @Override - public void setBackgroundDrawable(Drawable drawable) { - if (getBackground() == null || drawable == null) { - super.setBackgroundDrawable(drawable); - return; - } - - int[] padding = new int[] { getPaddingLeft(), - getPaddingTop(), - getPaddingRight(), - getPaddingBottom() - }; - drawable.setLevel(getBackground().getLevel()); - super.setBackgroundDrawable(drawable); - - setPadding(padding[0], padding[1], padding[2], padding[3]); - } - - @Override - public void setBackgroundResource(int resId) { - setBackgroundDrawable(getResources().getDrawable(resId)); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/toolbar/SiteIdentityPopup.java b/mobile/android/base/java/org/mozilla/gecko/toolbar/SiteIdentityPopup.java deleted file mode 100644 index 14230a2ecb..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/toolbar/SiteIdentityPopup.java +++ /dev/null @@ -1,571 +0,0 @@ -/* 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.toolbar; - -import android.content.ClipData; -import android.content.ClipboardManager; -import android.content.res.Resources; -import android.graphics.Bitmap; -import android.graphics.drawable.BitmapDrawable; -import android.graphics.drawable.Drawable; -import android.support.design.widget.Snackbar; -import android.support.v4.content.ContextCompat; -import android.widget.ImageView; -import android.widget.Toast; -import org.json.JSONException; -import org.json.JSONArray; -import org.mozilla.gecko.AboutPages; -import org.mozilla.gecko.AppConstants; -import org.mozilla.gecko.EventDispatcher; -import org.mozilla.gecko.R; -import org.mozilla.gecko.GeckoApp; -import org.mozilla.gecko.GeckoAppShell; -import org.mozilla.gecko.SiteIdentity; -import org.mozilla.gecko.SiteIdentity.SecurityMode; -import org.mozilla.gecko.SiteIdentity.MixedMode; -import org.mozilla.gecko.SiteIdentity.TrackingMode; -import org.mozilla.gecko.SnackbarBuilder; -import org.mozilla.gecko.Tab; -import org.mozilla.gecko.Tabs; -import org.mozilla.gecko.util.GeckoEventListener; -import org.mozilla.gecko.util.ThreadUtils; -import org.mozilla.gecko.widget.AnchoredPopup; -import org.mozilla.gecko.widget.DoorHanger; -import org.mozilla.gecko.widget.DoorHanger.OnButtonClickListener; -import org.json.JSONObject; - -import android.app.Activity; -import android.content.Context; -import android.text.TextUtils; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.View; -import android.widget.LinearLayout; -import android.widget.TextView; -import org.mozilla.gecko.widget.DoorhangerConfig; -import org.mozilla.gecko.widget.SiteLogins; - -/** - * SiteIdentityPopup is a singleton class that displays site identity data in - * an arrow panel popup hanging from the lock icon in the browser toolbar. - * - * A site identity icon may be displayed in the url, and is set in ToolbarDisplayLayout. - */ -public class SiteIdentityPopup extends AnchoredPopup implements GeckoEventListener { - - public static enum ButtonType { DISABLE, ENABLE, KEEP_BLOCKING, CANCEL, COPY } - - private static final String LOGTAG = "GeckoSiteIdentityPopup"; - - private static final String MIXED_CONTENT_SUPPORT_URL = - "https://support.mozilla.org/kb/how-does-insecure-content-affect-safety-android"; - private static final String TRACKING_CONTENT_SUPPORT_URL = - "https://support.mozilla.org/kb/firefox-android-tracking-protection"; - - // Placeholder string. - private final static String FORMAT_S = "%s"; - - private final Resources mResources; - private SiteIdentity mSiteIdentity; - - private LinearLayout mIdentity; - - private LinearLayout mIdentityKnownContainer; - - private ImageView mIcon; - private TextView mTitle; - private TextView mSecurityState; - private TextView mMixedContentActivity; - private TextView mOwner; - private TextView mOwnerSupplemental; - private TextView mVerifier; - private TextView mLink; - private TextView mSiteSettingsLink; - - private View mDivider; - - private DoorHanger mTrackingContentNotification; - private DoorHanger mSelectLoginDoorhanger; - - private final OnButtonClickListener mContentButtonClickListener; - - public SiteIdentityPopup(Context context) { - super(context); - - mResources = mContext.getResources(); - - mContentButtonClickListener = new ContentNotificationButtonListener(); - - GeckoApp.getEventDispatcher().registerGeckoThreadListener(this, - "Doorhanger:Logins", - "Permissions:CheckResult"); - } - - @Override - protected void init() { - super.init(); - - // Make the popup focusable so it doesn't inadvertently trigger click events elsewhere - // which may reshow the popup (see bug 785156) - setFocusable(true); - - LayoutInflater inflater = LayoutInflater.from(mContext); - mIdentity = (LinearLayout) inflater.inflate(R.layout.site_identity, null); - mContent.addView(mIdentity); - - mIdentityKnownContainer = - (LinearLayout) mIdentity.findViewById(R.id.site_identity_known_container); - - mIcon = (ImageView) mIdentity.findViewById(R.id.site_identity_icon); - mTitle = (TextView) mIdentity.findViewById(R.id.site_identity_title); - mSecurityState = (TextView) mIdentity.findViewById(R.id.site_identity_state); - mMixedContentActivity = (TextView) mIdentity.findViewById(R.id.mixed_content_activity); - - mOwner = (TextView) mIdentityKnownContainer.findViewById(R.id.owner); - mOwnerSupplemental = (TextView) mIdentityKnownContainer.findViewById(R.id.owner_supplemental); - mVerifier = (TextView) mIdentityKnownContainer.findViewById(R.id.verifier); - mDivider = mIdentity.findViewById(R.id.divider_doorhanger); - - mLink = (TextView) mIdentity.findViewById(R.id.site_identity_link); - mLink.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - Tabs.getInstance().loadUrlInTab(MIXED_CONTENT_SUPPORT_URL); - } - }); - - mSiteSettingsLink = (TextView) mIdentity.findViewById(R.id.site_settings_link); - } - - private void updateIdentity(final SiteIdentity siteIdentity) { - if (!mInflated) { - init(); - } - - final boolean isIdentityKnown = (siteIdentity.getSecurityMode() == SecurityMode.IDENTIFIED || - siteIdentity.getSecurityMode() == SecurityMode.VERIFIED); - updateConnectionState(siteIdentity); - toggleIdentityKnownContainerVisibility(isIdentityKnown); - - if (isIdentityKnown) { - updateIdentityInformation(siteIdentity); - } - - GeckoAppShell.notifyObservers("Permissions:Check", null); - } - - @Override - public void handleMessage(String event, JSONObject geckoObject) { - if ("Doorhanger:Logins".equals(event)) { - try { - final Tab selectedTab = Tabs.getInstance().getSelectedTab(); - if (selectedTab != null) { - final JSONObject data = geckoObject.getJSONObject("data"); - addLoginsToTab(data); - } - if (isShowing()) { - addSelectLoginDoorhanger(selectedTab); - } - } catch (JSONException e) { - Log.e(LOGTAG, "Error accessing logins in Doorhanger:Logins message", e); - } - } else if ("Permissions:CheckResult".equals(event)) { - final boolean hasPermissions = geckoObject.optBoolean("hasPermissions", false); - if (hasPermissions) { - mSiteSettingsLink.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - GeckoAppShell.notifyObservers("Permissions:Get", null); - dismiss(); - } - }); - } - - ThreadUtils.postToUiThread(new Runnable() { - @Override - public void run() { - mSiteSettingsLink.setVisibility(hasPermissions ? View.VISIBLE : View.GONE); - } - }); - } - } - - private void addLoginsToTab(JSONObject data) throws JSONException { - final JSONArray logins = data.getJSONArray("logins"); - - final SiteLogins siteLogins = new SiteLogins(logins); - Tabs.getInstance().getSelectedTab().setSiteLogins(siteLogins); - } - - private void addSelectLoginDoorhanger(Tab tab) throws JSONException { - final SiteLogins siteLogins = tab.getSiteLogins(); - if (siteLogins == null) { - return; - } - - final JSONArray logins = siteLogins.getLogins(); - if (logins.length() == 0) { - return; - } - - final JSONObject login = (JSONObject) logins.get(0); - - // Create button click listener for copying a password to the clipboard. - final OnButtonClickListener buttonClickListener = new OnButtonClickListener() { - Activity activity = (Activity) mContext; - @Override - public void onButtonClick(JSONObject response, DoorHanger doorhanger) { - try { - final int buttonId = response.getInt("callback"); - if (buttonId == ButtonType.COPY.ordinal()) { - final ClipboardManager manager = (ClipboardManager) mContext.getSystemService(Context.CLIPBOARD_SERVICE); - String password; - if (response.has("password")) { - // Click listener being called from List Dialog. - password = response.optString("password"); - } else { - password = login.getString("password"); - } - - manager.setPrimaryClip(ClipData.newPlainText("password", password)); - - SnackbarBuilder.builder(activity) - .message(R.string.doorhanger_login_select_toast_copy) - .duration(Snackbar.LENGTH_SHORT) - .buildAndShow(); - } - dismiss(); - } catch (JSONException e) { - Log.e(LOGTAG, "Error handling Select login button click", e); - SnackbarBuilder.builder(activity) - .message(R.string.doorhanger_login_select_toast_copy_error) - .duration(Snackbar.LENGTH_SHORT) - .buildAndShow(); - } - } - }; - - final DoorhangerConfig config = new DoorhangerConfig(DoorHanger.Type.LOGIN, buttonClickListener); - - // Set buttons. - config.setButton(mContext.getString(R.string.button_cancel), ButtonType.CANCEL.ordinal(), false); - config.setButton(mContext.getString(R.string.button_copy), ButtonType.COPY.ordinal(), true); - - // Set message. - String username = ((JSONObject) logins.get(0)).getString("username"); - if (TextUtils.isEmpty(username)) { - username = mContext.getString(R.string.doorhanger_login_no_username); - } - - final String message = mContext.getString(R.string.doorhanger_login_select_message).replace(FORMAT_S, username); - config.setMessage(message); - - // Set options. - final JSONObject options = new JSONObject(); - - // Add action text only if there are other logins to select. - if (logins.length() > 1) { - - final JSONObject actionText = new JSONObject(); - actionText.put("type", "SELECT"); - - final JSONObject bundle = new JSONObject(); - bundle.put("logins", logins); - - actionText.put("bundle", bundle); - options.put("actionText", actionText); - } - - config.setOptions(options); - - ThreadUtils.postToUiThread(new Runnable() { - @Override - public void run() { - if (!mInflated) { - init(); - } - - removeSelectLoginDoorhanger(); - - mSelectLoginDoorhanger = DoorHanger.Get(mContext, config); - mContent.addView(mSelectLoginDoorhanger); - mDivider.setVisibility(View.VISIBLE); - } - }); - } - - private void removeSelectLoginDoorhanger() { - if (mSelectLoginDoorhanger != null) { - mContent.removeView(mSelectLoginDoorhanger); - mSelectLoginDoorhanger = null; - } - } - - private void toggleIdentityKnownContainerVisibility(final boolean isIdentityKnown) { - final int identityInfoVisibility = isIdentityKnown ? View.VISIBLE : View.GONE; - mIdentityKnownContainer.setVisibility(identityInfoVisibility); - } - - /** - * Update the Site Identity content to reflect connection state. - * - * The connection state should reflect the combination of: - * a) Connection encryption - * b) Mixed Content state (Active/Display Mixed content, loaded, blocked, none, etc) - * and update the icons and strings to inform the user of that state. - * - * @param siteIdentity SiteIdentity information about the connection. - */ - private void updateConnectionState(final SiteIdentity siteIdentity) { - if (siteIdentity.getSecurityMode() == SecurityMode.CHROMEUI) { - mSecurityState.setText(R.string.identity_connection_chromeui); - mSecurityState.setTextColor(ContextCompat.getColor(mContext, R.color.placeholder_active_grey)); - - mIcon.setImageResource(R.drawable.icon); - clearSecurityStateIcon(); - - mMixedContentActivity.setVisibility(View.GONE); - mLink.setVisibility(View.GONE); - } else if (!siteIdentity.isSecure()) { - if (siteIdentity.getMixedModeActive() == MixedMode.MIXED_CONTENT_LOADED) { - // Active Mixed Content loaded because user has disabled blocking. - mIcon.setImageResource(R.drawable.lock_disabled); - clearSecurityStateIcon(); - mMixedContentActivity.setVisibility(View.VISIBLE); - mMixedContentActivity.setText(R.string.mixed_content_protection_disabled); - - mLink.setVisibility(View.VISIBLE); - } else if (siteIdentity.getMixedModeDisplay() == MixedMode.MIXED_CONTENT_LOADED) { - // Passive Mixed Content loaded. - mIcon.setImageResource(R.drawable.lock_inactive); - setSecurityStateIcon(R.drawable.warning_major, 1); - mMixedContentActivity.setVisibility(View.VISIBLE); - if (siteIdentity.getMixedModeActive() == MixedMode.MIXED_CONTENT_BLOCKED) { - mMixedContentActivity.setText(R.string.mixed_content_blocked_some); - } else { - mMixedContentActivity.setText(R.string.mixed_content_display_loaded); - } - mLink.setVisibility(View.VISIBLE); - - } else { - // Unencrypted connection with no mixed content. - mIcon.setImageResource(R.drawable.globe_light); - clearSecurityStateIcon(); - - mMixedContentActivity.setVisibility(View.GONE); - mLink.setVisibility(View.GONE); - } - - mSecurityState.setText(R.string.identity_connection_insecure); - mSecurityState.setTextColor(ContextCompat.getColor(mContext, R.color.placeholder_active_grey)); - } else { - // Connection is secure. - mIcon.setImageResource(R.drawable.lock_secure); - - setSecurityStateIcon(R.drawable.img_check, 2); - mSecurityState.setTextColor(ContextCompat.getColor(mContext, R.color.affirmative_green)); - mSecurityState.setText(R.string.identity_connection_secure); - - // Mixed content has been blocked, if present. - if (siteIdentity.getMixedModeActive() == MixedMode.MIXED_CONTENT_BLOCKED || - siteIdentity.getMixedModeDisplay() == MixedMode.MIXED_CONTENT_BLOCKED) { - mMixedContentActivity.setVisibility(View.VISIBLE); - mMixedContentActivity.setText(R.string.mixed_content_blocked_all); - mLink.setVisibility(View.VISIBLE); - } else { - mMixedContentActivity.setVisibility(View.GONE); - mLink.setVisibility(View.GONE); - } - } - } - - private void clearSecurityStateIcon() { - mSecurityState.setCompoundDrawablePadding(0); - mSecurityState.setCompoundDrawables(null, null, null, null); - } - - private void setSecurityStateIcon(int resource, int factor) { - final Drawable stateIcon = ContextCompat.getDrawable(mContext, resource); - stateIcon.setBounds(0, 0, stateIcon.getIntrinsicWidth() / factor, stateIcon.getIntrinsicHeight() / factor); - mSecurityState.setCompoundDrawables(stateIcon, null, null, null); - mSecurityState.setCompoundDrawablePadding((int) mResources.getDimension(R.dimen.doorhanger_drawable_padding)); - } - private void updateIdentityInformation(final SiteIdentity siteIdentity) { - String owner = siteIdentity.getOwner(); - if (owner == null) { - mOwner.setVisibility(View.GONE); - mOwnerSupplemental.setVisibility(View.GONE); - } else { - mOwner.setVisibility(View.VISIBLE); - mOwner.setText(owner); - - // Supplemental data is optional. - final String supplemental = siteIdentity.getSupplemental(); - if (!TextUtils.isEmpty(supplemental)) { - mOwnerSupplemental.setText(supplemental); - mOwnerSupplemental.setVisibility(View.VISIBLE); - } else { - mOwnerSupplemental.setVisibility(View.GONE); - } - } - - final String verifier = siteIdentity.getVerifier(); - mVerifier.setText(verifier); - } - - private void addTrackingContentNotification(boolean blocked) { - // Remove any existing tracking content notification. - removeTrackingContentNotification(); - - final DoorhangerConfig config = new DoorhangerConfig(DoorHanger.Type.TRACKING, mContentButtonClickListener); - - final int icon = blocked ? R.drawable.shield_enabled : R.drawable.shield_disabled; - - final JSONObject options = new JSONObject(); - final JSONObject tracking = new JSONObject(); - try { - tracking.put("enabled", blocked); - options.put("tracking_protection", tracking); - } catch (JSONException e) { - Log.e(LOGTAG, "Error adding tracking protection options", e); - } - config.setOptions(options); - - config.setLink(mContext.getString(R.string.learn_more), TRACKING_CONTENT_SUPPORT_URL); - - addNotificationButtons(config, blocked); - - mTrackingContentNotification = DoorHanger.Get(mContext, config); - - mTrackingContentNotification.setIcon(icon); - - mContent.addView(mTrackingContentNotification); - mDivider.setVisibility(View.VISIBLE); - } - - private void removeTrackingContentNotification() { - if (mTrackingContentNotification != null) { - mContent.removeView(mTrackingContentNotification); - mTrackingContentNotification = null; - } - } - - private void addNotificationButtons(DoorhangerConfig config, boolean blocked) { - if (blocked) { - config.setButton(mContext.getString(R.string.disable_protection), ButtonType.DISABLE.ordinal(), false); - } else { - config.setButton(mContext.getString(R.string.enable_protection), ButtonType.ENABLE.ordinal(), true); - } - } - - /* - * @param identityData A JSONObject that holds the current tab's identity data. - */ - void setSiteIdentity(SiteIdentity siteIdentity) { - mSiteIdentity = siteIdentity; - } - - @Override - public void show() { - if (mSiteIdentity == null) { - Log.e(LOGTAG, "Can't show site identity popup for undefined state"); - return; - } - - // Verified about: pages have the CHROMEUI SiteIdentity, however there can also - // be unverified about: pages for which "This site's identity is unknown" or - // "This is a secure Firefox page" are both misleading, so don't show a popup. - final Tab selectedTab = Tabs.getInstance().getSelectedTab(); - if (selectedTab != null && - AboutPages.isAboutPage(selectedTab.getURL()) && - mSiteIdentity.getSecurityMode() != SecurityMode.CHROMEUI) { - Log.d(LOGTAG, "We don't show site identity popups for unverified about: pages"); - return; - } - - updateIdentity(mSiteIdentity); - - final TrackingMode trackingMode = mSiteIdentity.getTrackingMode(); - if (trackingMode != TrackingMode.UNKNOWN) { - addTrackingContentNotification(trackingMode == TrackingMode.TRACKING_CONTENT_BLOCKED); - } - - try { - addSelectLoginDoorhanger(selectedTab); - } catch (JSONException e) { - Log.e(LOGTAG, "Error adding selectLogin doorhanger", e); - } - - if (mSiteIdentity.getSecurityMode() == SecurityMode.CHROMEUI) { - // For about: pages we display the product icon in place of the verified/globe - // image, hence we don't also set the favicon (for most about pages the - // favicon is the product icon, hence we'd be showing the same icon twice). - mTitle.setText(R.string.moz_app_displayname); - } else { - mTitle.setText(selectedTab.getBaseDomain()); - - final Bitmap favicon = selectedTab.getFavicon(); - if (favicon != null) { - final Drawable faviconDrawable = new BitmapDrawable(mResources, favicon); - final int dimen = (int) mResources.getDimension(R.dimen.browser_toolbar_favicon_size); - faviconDrawable.setBounds(0, 0, dimen, dimen); - - mTitle.setCompoundDrawables(faviconDrawable, null, null, null); - mTitle.setCompoundDrawablePadding((int) mContext.getResources().getDimension(R.dimen.doorhanger_drawable_padding)); - } - } - - showDividers(); - - super.show(); - } - - // Show the right dividers - private void showDividers() { - final int count = mContent.getChildCount(); - DoorHanger lastVisibleDoorHanger = null; - - for (int i = 0; i < count; i++) { - final View child = mContent.getChildAt(i); - - if (!(child instanceof DoorHanger)) { - continue; - } - - DoorHanger dh = (DoorHanger) child; - dh.showDivider(); - if (dh.getVisibility() == View.VISIBLE) { - lastVisibleDoorHanger = dh; - } - } - - if (lastVisibleDoorHanger != null) { - lastVisibleDoorHanger.hideDivider(); - } - } - - void destroy() { - GeckoApp.getEventDispatcher().unregisterGeckoThreadListener(this, - "Doorhanger:Logins", - "Permissions:CheckResult"); - } - - @Override - public void dismiss() { - super.dismiss(); - removeTrackingContentNotification(); - removeSelectLoginDoorhanger(); - mTitle.setCompoundDrawablesWithIntrinsicBounds(null, null, null, null); - mDivider.setVisibility(View.GONE); - } - - private class ContentNotificationButtonListener implements OnButtonClickListener { - @Override - public void onButtonClick(JSONObject response, DoorHanger doorhanger) { - GeckoAppShell.notifyObservers("Session:Reload", response.toString()); - dismiss(); - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/toolbar/TabCounter.java b/mobile/android/base/java/org/mozilla/gecko/toolbar/TabCounter.java deleted file mode 100644 index 1e0ca516b4..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/toolbar/TabCounter.java +++ /dev/null @@ -1,154 +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.toolbar; - -import org.mozilla.gecko.AppConstants.Versions; -import org.mozilla.gecko.R; -import org.mozilla.gecko.animation.Rotate3DAnimation; -import org.mozilla.gecko.widget.themed.ThemedTextSwitcher; - -import android.content.Context; -import android.content.res.TypedArray; -import android.util.AttributeSet; -import android.view.LayoutInflater; -import android.view.View; -import android.view.accessibility.AccessibilityNodeInfo; -import android.view.animation.AlphaAnimation; -import android.view.animation.AnimationSet; -import android.widget.ViewSwitcher; - -public class TabCounter extends ThemedTextSwitcher - implements ViewSwitcher.ViewFactory { - - private static final float CENTER_X = 0.5f; - private static final float CENTER_Y = 1.25f; - private static final int DURATION = 500; - private static final float Z_DISTANCE = 200; - - private final AnimationSet mFlipInForward; - private final AnimationSet mFlipInBackward; - private final AnimationSet mFlipOutForward; - private final AnimationSet mFlipOutBackward; - private final LayoutInflater mInflater; - private final int mLayoutId; - - private int mCount; - public static final int MAX_VISIBLE_TABS = 99; - public static final String SO_MANY_TABS_OPEN = "∞"; - - private enum FadeMode { - FADE_IN, - FADE_OUT - } - - public TabCounter(Context context, AttributeSet attrs) { - super(context, attrs); - - final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TabCounter); - mLayoutId = a.getResourceId(R.styleable.TabCounter_android_layout, R.layout.tabs_counter); - a.recycle(); - - mInflater = LayoutInflater.from(context); - - mFlipInForward = createAnimation(-90, 0, FadeMode.FADE_IN, -1 * Z_DISTANCE, false); - mFlipInBackward = createAnimation(90, 0, FadeMode.FADE_IN, Z_DISTANCE, false); - mFlipOutForward = createAnimation(0, -90, FadeMode.FADE_OUT, -1 * Z_DISTANCE, true); - mFlipOutBackward = createAnimation(0, 90, FadeMode.FADE_OUT, Z_DISTANCE, true); - - removeAllViews(); - setFactory(this); - - if (Versions.feature16Plus) { - // This adds the TextSwitcher to the a11y node tree, where we in turn - // could make it return an empty info node. If we don't do this the - // TextSwitcher's child TextViews get picked up, and we don't want - // that since the tabs ImageButton is already properly labeled for - // accessibility. - setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES); - setAccessibilityDelegate(new View.AccessibilityDelegate() { - @Override - public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {} - }); - } - } - - void setCountWithAnimation(int count) { - // Don't animate from initial state - if (mCount == 0) { - setCount(count); - return; - } - - if (mCount == count) { - return; - } - - // don't animate if there are still over MAX_VISIBLE_TABS tabs open - if (mCount > MAX_VISIBLE_TABS && count > MAX_VISIBLE_TABS) { - mCount = count; - return; - } - - if (count < mCount) { - setInAnimation(mFlipInBackward); - setOutAnimation(mFlipOutForward); - } else { - setInAnimation(mFlipInForward); - setOutAnimation(mFlipOutBackward); - } - - // Eliminate screen artifact. Set explicit In/Out animation pair order. This will always - // animate pair in In->Out child order, prevent alternating use of the Out->In case. - setDisplayedChild(0); - - // Set In value, trigger animation to Out value - setCurrentText(formatForDisplay(mCount)); - setText(formatForDisplay(count)); - - mCount = count; - } - - private String formatForDisplay(int count) { - if (count > MAX_VISIBLE_TABS) { - return SO_MANY_TABS_OPEN; - } - return String.valueOf(count); - } - - void setCount(int count) { - setCurrentText(formatForDisplay(count)); - mCount = count; - } - - // Alpha animations in editing mode cause action bar corruption on the - // Nexus 7 (bug 961749). As a workaround, skip these animations in editing - // mode. - void onEnterEditingMode() { - final int childCount = getChildCount(); - for (int i = 0; i < childCount; i++) { - getChildAt(i).clearAnimation(); - } - } - - private AnimationSet createAnimation(float startAngle, float endAngle, - FadeMode fadeMode, - float zEnd, boolean reverse) { - final Context context = getContext(); - AnimationSet set = new AnimationSet(context, null); - set.addAnimation(new Rotate3DAnimation(startAngle, endAngle, CENTER_X, CENTER_Y, zEnd, reverse)); - set.addAnimation(fadeMode == FadeMode.FADE_IN ? new AlphaAnimation(0.0f, 1.0f) : - new AlphaAnimation(1.0f, 0.0f)); - set.setDuration(DURATION); - set.setInterpolator(context, android.R.anim.accelerate_interpolator); - return set; - } - - @Override - public View makeView() { - return mInflater.inflate(mLayoutId, null); - } - -} diff --git a/mobile/android/base/java/org/mozilla/gecko/toolbar/ToolbarDisplayLayout.java b/mobile/android/base/java/org/mozilla/gecko/toolbar/ToolbarDisplayLayout.java deleted file mode 100644 index 163ed4a510..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/toolbar/ToolbarDisplayLayout.java +++ /dev/null @@ -1,530 +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.toolbar; - -import java.util.Arrays; -import java.util.EnumSet; -import java.util.List; - -import android.support.v4.content.ContextCompat; -import org.mozilla.gecko.AboutPages; -import org.mozilla.gecko.BrowserApp; -import org.mozilla.gecko.R; -import org.mozilla.gecko.reader.ReaderModeUtils; -import org.mozilla.gecko.SiteIdentity; -import org.mozilla.gecko.SiteIdentity.MixedMode; -import org.mozilla.gecko.SiteIdentity.SecurityMode; -import org.mozilla.gecko.SiteIdentity.TrackingMode; -import org.mozilla.gecko.Tab; -import org.mozilla.gecko.animation.PropertyAnimator; -import org.mozilla.gecko.animation.ViewHelper; -import org.mozilla.gecko.toolbar.BrowserToolbarTabletBase.ForwardButtonAnimation; -import org.mozilla.gecko.Experiments; -import org.mozilla.gecko.util.HardwareUtils; -import org.mozilla.gecko.util.StringUtils; -import org.mozilla.gecko.widget.themed.ThemedLinearLayout; -import org.mozilla.gecko.widget.themed.ThemedTextView; - -import android.content.Context; -import android.os.SystemClock; -import android.support.annotation.NonNull; -import android.text.Spannable; -import android.text.SpannableString; -import android.text.SpannableStringBuilder; -import android.text.TextUtils; -import android.text.style.ForegroundColorSpan; -import android.util.AttributeSet; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.View; -import android.widget.Button; -import android.widget.ImageButton; - -import com.keepsafe.switchboard.SwitchBoard; - -/** -* {@code ToolbarDisplayLayout} is the UI for when the toolbar is in -* display state. It's used to display the state of the currently selected -* tab. It should always be updated through a single entry point -* (updateFromTab) and should never track any tab events or gecko messages -* on its own to keep it as dumb as possible. -* -* The UI has two possible modes: progress and display which are triggered -* when UpdateFlags.PROGRESS is used depending on the current tab state. -* The progress mode is triggered when the tab is loading a page. Display mode -* is used otherwise. -* -* {@code ToolbarDisplayLayout} is meant to be owned by {@code BrowserToolbar} -* which is the main event bus for the toolbar subsystem. -*/ -public class ToolbarDisplayLayout extends ThemedLinearLayout { - - private static final String LOGTAG = "GeckoToolbarDisplayLayout"; - private boolean mTrackingProtectionEnabled; - - // To be used with updateFromTab() to allow the caller - // to give enough context for the requested state change. - enum UpdateFlags { - TITLE, - FAVICON, - PROGRESS, - SITE_IDENTITY, - PRIVATE_MODE, - - // Disable any animation that might be - // triggered from this state change. Mostly - // used on tab switches, see BrowserToolbar. - DISABLE_ANIMATIONS - } - - private enum UIMode { - PROGRESS, - DISPLAY - } - - interface OnStopListener { - Tab onStop(); - } - - interface OnTitleChangeListener { - void onTitleChange(CharSequence title); - } - - private final BrowserApp mActivity; - - private UIMode mUiMode; - - private boolean mIsAttached; - - private final ThemedTextView mTitle; - private final int mTitlePadding; - private ToolbarPrefs mPrefs; - private OnTitleChangeListener mTitleChangeListener; - - private final ImageButton mSiteSecurity; - - private final ImageButton mStop; - private OnStopListener mStopListener; - - private final PageActionLayout mPageActionLayout; - - private final SiteIdentityPopup mSiteIdentityPopup; - private int mSecurityImageLevel; - - // Security level constants, which map to the icons / levels defined in: - // http://dxr.mozilla.org/mozilla-central/source/mobile/android/base/java/org/mozilla/gecko/resources/drawable/site_security_level.xml - // Default level (unverified pages) - globe icon: - private static final int LEVEL_DEFAULT_GLOBE = 0; - // Levels for displaying Mixed Content state icons. - private static final int LEVEL_WARNING_MINOR = 3; - private static final int LEVEL_LOCK_DISABLED = 4; - // Levels for displaying Tracking Protection state icons. - private static final int LEVEL_SHIELD_ENABLED = 5; - private static final int LEVEL_SHIELD_DISABLED = 6; - // Icon used for about:home - private static final int LEVEL_SEARCH_ICON = 999; - - private final ForegroundColorSpan mUrlColorSpan; - private final ForegroundColorSpan mPrivateUrlColorSpan; - private final ForegroundColorSpan mBlockedColorSpan; - private final ForegroundColorSpan mDomainColorSpan; - private final ForegroundColorSpan mPrivateDomainColorSpan; - private final ForegroundColorSpan mCertificateOwnerColorSpan; - - public ToolbarDisplayLayout(Context context, AttributeSet attrs) { - super(context, attrs); - setOrientation(HORIZONTAL); - - mActivity = (BrowserApp) context; - - LayoutInflater.from(context).inflate(R.layout.toolbar_display_layout, this); - - mTitle = (ThemedTextView) findViewById(R.id.url_bar_title); - mTitlePadding = mTitle.getPaddingRight(); - - mUrlColorSpan = new ForegroundColorSpan(ContextCompat.getColor(context, R.color.url_bar_urltext)); - mPrivateUrlColorSpan = new ForegroundColorSpan(ContextCompat.getColor(context, R.color.url_bar_urltext_private)); - mBlockedColorSpan = new ForegroundColorSpan(ContextCompat.getColor(context, R.color.url_bar_blockedtext)); - mDomainColorSpan = new ForegroundColorSpan(ContextCompat.getColor(context, R.color.url_bar_domaintext)); - mPrivateDomainColorSpan = new ForegroundColorSpan(ContextCompat.getColor(context, R.color.url_bar_domaintext_private)); - mCertificateOwnerColorSpan = new ForegroundColorSpan(ContextCompat.getColor(context, R.color.affirmative_green)); - - mSiteSecurity = (ImageButton) findViewById(R.id.site_security); - - mSiteIdentityPopup = new SiteIdentityPopup(mActivity); - mSiteIdentityPopup.setAnchor(this); - mSiteIdentityPopup.setOnVisibilityChangeListener(mActivity); - - mStop = (ImageButton) findViewById(R.id.stop); - mPageActionLayout = (PageActionLayout) findViewById(R.id.page_action_layout); - } - - @Override - public void onAttachedToWindow() { - super.onAttachedToWindow(); - - mIsAttached = true; - - mSiteSecurity.setOnClickListener(new Button.OnClickListener() { - @Override - public void onClick(View view) { - mSiteIdentityPopup.show(); - } - }); - - mStop.setOnClickListener(new Button.OnClickListener() { - @Override - public void onClick(View v) { - if (mStopListener != null) { - // Force toolbar to switch to Display mode - // immediately based on the stopped tab. - final Tab tab = mStopListener.onStop(); - if (tab != null) { - updateUiMode(UIMode.DISPLAY); - } - } - } - }); - } - - @Override - public void onDetachedFromWindow() { - super.onDetachedFromWindow(); - mIsAttached = false; - } - - @Override - public void setNextFocusDownId(int nextId) { - mStop.setNextFocusDownId(nextId); - mSiteSecurity.setNextFocusDownId(nextId); - mPageActionLayout.setNextFocusDownId(nextId); - } - - void setToolbarPrefs(final ToolbarPrefs prefs) { - mPrefs = prefs; - } - - void updateFromTab(@NonNull Tab tab, EnumSet flags) { - // Several parts of ToolbarDisplayLayout's state depends - // on the views being attached to the view tree. - if (!mIsAttached) { - return; - } - - if (flags.contains(UpdateFlags.TITLE)) { - updateTitle(tab); - } - - if (flags.contains(UpdateFlags.SITE_IDENTITY)) { - updateSiteIdentity(tab); - } - - if (flags.contains(UpdateFlags.PROGRESS)) { - updateProgress(tab); - } - - if (flags.contains(UpdateFlags.PRIVATE_MODE)) { - mTitle.setPrivateMode(tab.isPrivate()); - } - } - - void setTitle(CharSequence title) { - mTitle.setText(title); - - if (mTitleChangeListener != null) { - mTitleChangeListener.onTitleChange(title); - } - } - - private void updateTitle(@NonNull Tab tab) { - // Keep the title unchanged if there's no selected tab, - // or if the tab is entering reader mode. - if (tab.isEnteringReaderMode()) { - return; - } - - final String url = tab.getURL(); - - // Setting a null title will ensure we just see the - // "Enter Search or Address" placeholder text. - if (AboutPages.isTitlelessAboutPage(url)) { - setTitle(null); - setContentDescription(null); - return; - } - - // Show the about:blocked page title in red, regardless of prefs - if (tab.getErrorType() == Tab.ErrorType.BLOCKED) { - final String title = tab.getDisplayTitle(); - - final SpannableStringBuilder builder = new SpannableStringBuilder(title); - builder.setSpan(mBlockedColorSpan, 0, title.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE); - - setTitle(builder); - setContentDescription(null); - return; - } - - final String baseDomain = tab.getBaseDomain(); - - String strippedURL = stripAboutReaderURL(url); - - final boolean isHttpOrHttps = StringUtils.isHttpOrHttps(strippedURL); - - if (mPrefs.shouldTrimUrls()) { - strippedURL = StringUtils.stripCommonSubdomains(StringUtils.stripScheme(strippedURL)); - } - - // The URL bar does not support RTL currently (See bug 928688 and meta bug 702845). - // Displaying a URL using RTL (or mixed) characters can lead to an undesired reordering - // of elements of the URL. That's why we are forcing the URL to use LTR (bug 1284372). - strippedURL = StringUtils.forceLTR(strippedURL); - - // This value is not visible to screen readers but we rely on it when running UI tests. Screen - // readers will instead focus BrowserToolbar and read the "base domain" from there. UI tests - // will read the content description to obtain the full URL for performing assertions. - setContentDescription(strippedURL); - - final SiteIdentity siteIdentity = tab.getSiteIdentity(); - if (siteIdentity.hasOwner() && SwitchBoard.isInExperiment(mActivity, Experiments.URLBAR_SHOW_EV_CERT_OWNER)) { - // Show Owner of EV certificate as title - updateTitleFromSiteIdentity(siteIdentity); - } else if (isHttpOrHttps && !HardwareUtils.isTablet() && !TextUtils.isEmpty(baseDomain) - && SwitchBoard.isInExperiment(mActivity, Experiments.URLBAR_SHOW_ORIGIN_ONLY)) { - // Show just the base domain as title - setTitle(baseDomain); - } else { - // Display full URL with base domain highlighted as title - updateAndColorTitleFromFullURL(strippedURL, baseDomain, tab.isPrivate()); - } - } - - private void updateTitleFromSiteIdentity(SiteIdentity siteIdentity) { - final String title; - - if (siteIdentity.hasCountry()) { - title = String.format("%s (%s)", siteIdentity.getOwner(), siteIdentity.getCountry()); - } else { - title = siteIdentity.getOwner(); - } - - final SpannableString spannable = new SpannableString(title); - spannable.setSpan(mCertificateOwnerColorSpan, 0, title.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - - setTitle(spannable); - } - - private void updateAndColorTitleFromFullURL(String url, String baseDomain, boolean isPrivate) { - if (TextUtils.isEmpty(baseDomain)) { - setTitle(url); - return; - } - - int index = url.indexOf(baseDomain); - if (index == -1) { - setTitle(url); - return; - } - - final SpannableStringBuilder builder = new SpannableStringBuilder(url); - - builder.setSpan(isPrivate ? mPrivateUrlColorSpan : mUrlColorSpan, 0, url.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE); - builder.setSpan(isPrivate ? mPrivateDomainColorSpan : mDomainColorSpan, - index, index + baseDomain.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE); - - setTitle(builder); - } - - private String stripAboutReaderURL(final String url) { - if (!AboutPages.isAboutReader(url)) { - return url; - } - - return ReaderModeUtils.stripAboutReaderUrl(url); - } - - private void updateSiteIdentity(@NonNull Tab tab) { - final SiteIdentity siteIdentity = tab.getSiteIdentity(); - - mSiteIdentityPopup.setSiteIdentity(siteIdentity); - - final SecurityMode securityMode; - final MixedMode activeMixedMode; - final MixedMode displayMixedMode; - final TrackingMode trackingMode; - if (siteIdentity == null) { - securityMode = SecurityMode.UNKNOWN; - activeMixedMode = MixedMode.UNKNOWN; - displayMixedMode = MixedMode.UNKNOWN; - trackingMode = TrackingMode.UNKNOWN; - } else { - securityMode = siteIdentity.getSecurityMode(); - activeMixedMode = siteIdentity.getMixedModeActive(); - displayMixedMode = siteIdentity.getMixedModeDisplay(); - trackingMode = siteIdentity.getTrackingMode(); - } - - // This is a bit tricky, but we have one icon and three potential indicators. - // Default to the identity level - int imageLevel = securityMode.ordinal(); - - // about: pages should default to having no icon too (the same as SecurityMode.UNKNOWN), however - // SecurityMode.CHROMEUI has a different ordinal - hence we need to manually reset it here. - // (We then continue and process the tracking / mixed content icons as usual, even for about: pages, as they - // can still load external sites.) - if (securityMode == SecurityMode.CHROMEUI) { - imageLevel = LEVEL_DEFAULT_GLOBE; // == SecurityMode.UNKNOWN.ordinal() - } - - // Check to see if any protection was overridden first - if (AboutPages.isTitlelessAboutPage(tab.getURL())) { - // We always want to just show a search icon on about:home - imageLevel = LEVEL_SEARCH_ICON; - } else if (trackingMode == TrackingMode.TRACKING_CONTENT_LOADED) { - imageLevel = LEVEL_SHIELD_DISABLED; - } else if (trackingMode == TrackingMode.TRACKING_CONTENT_BLOCKED) { - imageLevel = LEVEL_SHIELD_ENABLED; - } else if (activeMixedMode == MixedMode.MIXED_CONTENT_LOADED) { - imageLevel = LEVEL_LOCK_DISABLED; - } else if (displayMixedMode == MixedMode.MIXED_CONTENT_LOADED) { - imageLevel = LEVEL_WARNING_MINOR; - } - - if (mSecurityImageLevel != imageLevel) { - mSecurityImageLevel = imageLevel; - mSiteSecurity.setImageLevel(mSecurityImageLevel); - updatePageActions(); - } - - mTrackingProtectionEnabled = trackingMode == TrackingMode.TRACKING_CONTENT_BLOCKED; - } - - private void updateProgress(@NonNull Tab tab) { - final boolean shouldShowThrobber = tab.getState() == Tab.STATE_LOADING; - - updateUiMode(shouldShowThrobber ? UIMode.PROGRESS : UIMode.DISPLAY); - - if (Tab.STATE_SUCCESS == tab.getState() && mTrackingProtectionEnabled) { - mActivity.showTrackingProtectionPromptIfApplicable(); - } - } - - private void updateUiMode(UIMode uiMode) { - if (mUiMode == uiMode) { - return; - } - - mUiMode = uiMode; - - // The "Throbber start" and "Throbber stop" log messages in this method - // are needed by S1/S2 tests (http://mrcote.info/phonedash/#). - // See discussion in Bug 804457. Bug 805124 tracks paring these down. - if (mUiMode == UIMode.PROGRESS) { - Log.i(LOGTAG, "zerdatime " + SystemClock.uptimeMillis() + " - Throbber start"); - } else { - Log.i(LOGTAG, "zerdatime " + SystemClock.uptimeMillis() + " - Throbber stop"); - } - - updatePageActions(); - } - - private void updatePageActions() { - final boolean isShowingProgress = (mUiMode == UIMode.PROGRESS); - - mStop.setVisibility(isShowingProgress ? View.VISIBLE : View.GONE); - mPageActionLayout.setVisibility(!isShowingProgress ? View.VISIBLE : View.GONE); - - // We want title to fill the whole space available for it when there are icons - // being shown on the right side of the toolbar as the icons already have some - // padding in them. This is just to avoid wasting space when icons are shown. - mTitle.setPadding(0, 0, (!isShowingProgress ? mTitlePadding : 0), 0); - } - - List getFocusOrder() { - return Arrays.asList(mSiteSecurity, mPageActionLayout, mStop); - } - - void setOnStopListener(OnStopListener listener) { - mStopListener = listener; - } - - void setOnTitleChangeListener(OnTitleChangeListener listener) { - mTitleChangeListener = listener; - } - - /** - * Update the Site Identity popup anchor. - * - * Tablet UI has a tablet-specific doorhanger anchor, so update it after all the views - * are inflated. - * @param view View to use as the anchor for the Site Identity popup. - */ - void updateSiteIdentityAnchor(View view) { - mSiteIdentityPopup.setAnchor(view); - } - - void prepareForwardAnimation(PropertyAnimator anim, ForwardButtonAnimation animation, int width) { - if (animation == ForwardButtonAnimation.HIDE) { - // We animate these items individually, rather than this entire view, - // so that we don't animate certain views, e.g. the stop button. - anim.attach(mTitle, - PropertyAnimator.Property.TRANSLATION_X, - 0); - anim.attach(mSiteSecurity, - PropertyAnimator.Property.TRANSLATION_X, - 0); - - // We're hiding the forward button. We're going to reset the margin before - // the animation starts, so we shift these items to the right so that they don't - // appear to move initially. - ViewHelper.setTranslationX(mTitle, width); - ViewHelper.setTranslationX(mSiteSecurity, width); - } else { - anim.attach(mTitle, - PropertyAnimator.Property.TRANSLATION_X, - width); - anim.attach(mSiteSecurity, - PropertyAnimator.Property.TRANSLATION_X, - width); - } - } - - void finishForwardAnimation() { - ViewHelper.setTranslationX(mTitle, 0); - ViewHelper.setTranslationX(mSiteSecurity, 0); - } - - void prepareStartEditingAnimation() { - // Hide page actions/stop buttons immediately - ViewHelper.setAlpha(mPageActionLayout, 0); - ViewHelper.setAlpha(mStop, 0); - } - - void prepareStopEditingAnimation(PropertyAnimator anim) { - // Fade toolbar buttons (page actions, stop) after the entry - // is shrunk back to its original size. - anim.attach(mPageActionLayout, - PropertyAnimator.Property.ALPHA, - 1); - - anim.attach(mStop, - PropertyAnimator.Property.ALPHA, - 1); - } - - boolean dismissSiteIdentityPopup() { - if (mSiteIdentityPopup != null && mSiteIdentityPopup.isShowing()) { - mSiteIdentityPopup.dismiss(); - return true; - } - - return false; - } - - void destroy() { - mSiteIdentityPopup.destroy(); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/toolbar/ToolbarEditLayout.java b/mobile/android/base/java/org/mozilla/gecko/toolbar/ToolbarEditLayout.java deleted file mode 100644 index c9731a4014..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/toolbar/ToolbarEditLayout.java +++ /dev/null @@ -1,348 +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.toolbar; - -import android.app.Activity; -import android.content.Intent; -import android.graphics.drawable.Drawable; -import android.speech.RecognizerIntent; -import android.widget.Button; -import android.widget.ImageButton; -import org.mozilla.gecko.ActivityHandlerHelper; -import org.mozilla.gecko.GeckoAppShell; -import org.mozilla.gecko.GeckoSharedPrefs; -import org.mozilla.gecko.R; -import org.mozilla.gecko.Telemetry; -import org.mozilla.gecko.TelemetryContract; -import org.mozilla.gecko.animation.PropertyAnimator; -import org.mozilla.gecko.animation.PropertyAnimator.PropertyAnimationListener; -import org.mozilla.gecko.preferences.GeckoPreferences; -import org.mozilla.gecko.toolbar.BrowserToolbar.OnCommitListener; -import org.mozilla.gecko.toolbar.BrowserToolbar.OnDismissListener; -import org.mozilla.gecko.toolbar.BrowserToolbar.OnFilterListener; -import org.mozilla.gecko.toolbar.BrowserToolbar.TabEditingState; -import org.mozilla.gecko.util.ActivityResultHandler; -import org.mozilla.gecko.util.DrawableUtil; -import org.mozilla.gecko.util.HardwareUtils; -import org.mozilla.gecko.util.StringUtils; -import org.mozilla.gecko.util.InputOptionsUtils; -import org.mozilla.gecko.widget.themed.ThemedLinearLayout; - -import android.content.Context; -import android.util.AttributeSet; -import android.view.LayoutInflater; -import android.view.View; -import android.view.inputmethod.InputMethodManager; -import android.widget.ImageView; - -import java.util.List; - -/** -* {@code ToolbarEditLayout} is the UI for when the toolbar is in -* edit state. It controls a text entry ({@code ToolbarEditText}) -* and its matching 'go' button which changes depending on the -* current type of text in the entry. -*/ -public class ToolbarEditLayout extends ThemedLinearLayout { - - public interface OnSearchStateChangeListener { - public void onSearchStateChange(boolean isActive); - } - - private final ImageView mSearchIcon; - - private final ToolbarEditText mEditText; - - private final ImageButton mVoiceInput; - private final ImageButton mQrCode; - - private OnFocusChangeListener mFocusChangeListener; - - private boolean showKeyboardOnFocus = false; // Indicates if we need to show the keyboard after the app resumes - - public ToolbarEditLayout(Context context, AttributeSet attrs) { - super(context, attrs); - - setOrientation(HORIZONTAL); - - LayoutInflater.from(context).inflate(R.layout.toolbar_edit_layout, this); - mSearchIcon = (ImageView) findViewById(R.id.search_icon); - - mEditText = (ToolbarEditText) findViewById(R.id.url_edit_text); - - mVoiceInput = (ImageButton) findViewById(R.id.mic); - mQrCode = (ImageButton) findViewById(R.id.qrcode); - } - - @Override - public void onAttachedToWindow() { - super.onAttachedToWindow(); - - if (HardwareUtils.isTablet()) { - mSearchIcon.setVisibility(View.VISIBLE); - } - - mEditText.setOnFocusChangeListener(new OnFocusChangeListener() { - @Override - public void onFocusChange(View v, boolean hasFocus) { - if (mFocusChangeListener != null) { - mFocusChangeListener.onFocusChange(ToolbarEditLayout.this, hasFocus); - - // Checking if voice and QR code input are enabled each time the user taps on the URL bar - if (hasFocus) { - if (voiceIsEnabled(getContext(), getResources().getString(R.string.voicesearch_prompt))) { - mVoiceInput.setVisibility(View.VISIBLE); - } else { - mVoiceInput.setVisibility(View.GONE); - } - - if (qrCodeIsEnabled(getContext())) { - mQrCode.setVisibility(View.VISIBLE); - } else { - mQrCode.setVisibility(View.GONE); - } - } - } - } - }); - - mEditText.setOnSearchStateChangeListener(new OnSearchStateChangeListener() { - @Override - public void onSearchStateChange(boolean isActive) { - updateSearchIcon(isActive); - } - }); - - mVoiceInput.setOnClickListener(new Button.OnClickListener() { - @Override - public void onClick(View v) { - launchVoiceRecognizer(); - } - }); - - mQrCode.setOnClickListener(new Button.OnClickListener() { - @Override - public void onClick(View v) { - launchQRCodeReader(); - } - }); - - // Set an inactive search icon on tablet devices when in editing mode - updateSearchIcon(false); - } - - /** - * Update the search icon at the left of the edittext based - * on its state. - * - * @param isActive The state of the edittext. Active is when the initialized - * text has changed and is not empty. - */ - void updateSearchIcon(boolean isActive) { - if (!HardwareUtils.isTablet()) { - return; - } - - // When on tablet show a magnifying glass in editing mode - final int searchDrawableId = R.drawable.search_icon_active; - final Drawable searchDrawable; - if (!isActive) { - searchDrawable = DrawableUtil.tintDrawableWithColorRes(getContext(), searchDrawableId, R.color.placeholder_grey); - } else { - if (isPrivateMode()) { - searchDrawable = DrawableUtil.tintDrawableWithColorRes(getContext(), searchDrawableId, R.color.tabs_tray_icon_grey); - } else { - searchDrawable = getResources().getDrawable(searchDrawableId); - } - } - - mSearchIcon.setImageDrawable(searchDrawable); - } - - @Override - public void setOnFocusChangeListener(OnFocusChangeListener listener) { - mFocusChangeListener = listener; - } - - @Override - public void setEnabled(boolean enabled) { - super.setEnabled(enabled); - mEditText.setEnabled(enabled); - } - - @Override - public void setPrivateMode(boolean isPrivate) { - super.setPrivateMode(isPrivate); - mEditText.setPrivateMode(isPrivate); - } - - /** - * Called when the parent gains focus (on app launch and resume) - */ - public void onParentFocus() { - if (showKeyboardOnFocus) { - showKeyboardOnFocus = false; - - Activity activity = GeckoAppShell.getGeckoInterface().getActivity(); - activity.runOnUiThread(new Runnable() { - public void run() { - mEditText.requestFocus(); - showSoftInput(); - } - }); - } - - // Checking if qr code is supported after resuming the app - if (qrCodeIsEnabled(getContext())) { - mQrCode.setVisibility(View.VISIBLE); - } else { - mQrCode.setVisibility(View.GONE); - } - } - - void setToolbarPrefs(final ToolbarPrefs prefs) { - mEditText.setToolbarPrefs(prefs); - } - - private void showSoftInput() { - InputMethodManager imm = - (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE); - imm.showSoftInput(mEditText, InputMethodManager.SHOW_IMPLICIT); - } - - void prepareShowAnimation(final PropertyAnimator animator) { - if (animator == null) { - mEditText.requestFocus(); - showSoftInput(); - return; - } - - animator.addPropertyAnimationListener(new PropertyAnimationListener() { - @Override - public void onPropertyAnimationStart() { - mEditText.requestFocus(); - } - - @Override - public void onPropertyAnimationEnd() { - showSoftInput(); - } - }); - } - - void setOnCommitListener(OnCommitListener listener) { - mEditText.setOnCommitListener(listener); - } - - void setOnDismissListener(OnDismissListener listener) { - mEditText.setOnDismissListener(listener); - } - - void setOnFilterListener(OnFilterListener listener) { - mEditText.setOnFilterListener(listener); - } - - void onEditSuggestion(String suggestion) { - mEditText.setText(suggestion); - mEditText.setSelection(mEditText.getText().length()); - mEditText.requestFocus(); - - showSoftInput(); - } - - void setText(String text) { - mEditText.setText(text); - } - - String getText() { - return mEditText.getText().toString(); - } - - protected void saveTabEditingState(final TabEditingState editingState) { - editingState.lastEditingText = mEditText.getNonAutocompleteText(); - editingState.selectionStart = mEditText.getSelectionStart(); - editingState.selectionEnd = mEditText.getSelectionEnd(); - } - - protected void restoreTabEditingState(final TabEditingState editingState) { - mEditText.setText(editingState.lastEditingText); - mEditText.setSelection(editingState.selectionStart, editingState.selectionEnd); - } - - private boolean voiceIsEnabled(Context context, String prompt) { - final boolean voiceIsSupported = InputOptionsUtils.supportsVoiceRecognizer(context, prompt); - if (!voiceIsSupported) { - return false; - } - return GeckoSharedPrefs.forApp(context) - .getBoolean(GeckoPreferences.PREFS_VOICE_INPUT_ENABLED, true); - } - - private void launchVoiceRecognizer() { - Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.ACTIONBAR, "voice_input_launch"); - final Intent intent = InputOptionsUtils.createVoiceRecognizerIntent(getResources().getString(R.string.voicesearch_prompt)); - - Activity activity = GeckoAppShell.getGeckoInterface().getActivity(); - ActivityHandlerHelper.startIntentForActivity(activity, intent, new ActivityResultHandler() { - @Override - public void onActivityResult(int resultCode, Intent data) { - if (resultCode != Activity.RESULT_OK) { - return; - } - - Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.ACTIONBAR, "voice_input_success"); - // We have RESULT_OK, not RESULT_NO_MATCH so it should be safe to assume that - // we have at least one match. We only need one: this will be - // used for showing the user search engines with this search term in it. - List voiceStrings = data.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS); - String text = voiceStrings.get(0); - mEditText.setText(text); - mEditText.setSelection(0, text.length()); - - final InputMethodManager imm = - (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE); - imm.showSoftInput(mEditText, InputMethodManager.SHOW_IMPLICIT); - } - }); - } - - private boolean qrCodeIsEnabled(Context context) { - final boolean qrCodeIsSupported = InputOptionsUtils.supportsQrCodeReader(context); - if (!qrCodeIsSupported) { - return false; - } - return GeckoSharedPrefs.forApp(context) - .getBoolean(GeckoPreferences.PREFS_QRCODE_ENABLED, true); - } - - private void launchQRCodeReader() { - Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.ACTIONBAR, "qrcode_input_launch"); - final Intent intent = InputOptionsUtils.createQRCodeReaderIntent(); - - Activity activity = GeckoAppShell.getGeckoInterface().getActivity(); - ActivityHandlerHelper.startIntentForActivity(activity, intent, new ActivityResultHandler() { - @Override - public void onActivityResult(int resultCode, Intent intent) { - if (resultCode == Activity.RESULT_OK) { - String text = intent.getStringExtra("SCAN_RESULT"); - if (!StringUtils.isSearchQuery(text, false)) { - Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.ACTIONBAR, "qrcode_input_success"); - mEditText.setText(text); - mEditText.selectAll(); - - // Queuing up the keyboard show action. - // At this point the app has not resumed yet, and trying to show - // the keyboard will fail. - showKeyboardOnFocus = true; - } - } - // We can get the SCAN_RESULT_FORMAT, SCAN_RESULT_BYTES, - // SCAN_RESULT_ORIENTATION and SCAN_RESULT_ERROR_CORRECTION_LEVEL - // as well as the actual result, which may hold a URL. - } - }); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/toolbar/ToolbarEditText.java b/mobile/android/base/java/org/mozilla/gecko/toolbar/ToolbarEditText.java deleted file mode 100644 index b385f815a3..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/toolbar/ToolbarEditText.java +++ /dev/null @@ -1,630 +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.toolbar; - -import org.mozilla.gecko.AboutPages; -import org.mozilla.gecko.AppConstants.Versions; -import org.mozilla.gecko.CustomEditText; -import org.mozilla.gecko.InputMethods; -import org.mozilla.gecko.R; -import org.mozilla.gecko.toolbar.BrowserToolbar.OnCommitListener; -import org.mozilla.gecko.toolbar.BrowserToolbar.OnDismissListener; -import org.mozilla.gecko.toolbar.BrowserToolbar.OnFilterListener; -import org.mozilla.gecko.toolbar.ToolbarEditLayout.OnSearchStateChangeListener; -import org.mozilla.gecko.util.GamepadUtils; -import org.mozilla.gecko.util.StringUtils; - -import android.content.Context; -import android.graphics.Rect; -import android.text.Editable; -import android.text.NoCopySpan; -import android.text.Selection; -import android.text.Spanned; -import android.text.TextUtils; -import android.text.TextWatcher; -import android.text.style.BackgroundColorSpan; -import android.util.AttributeSet; -import android.util.Log; -import android.view.KeyEvent; -import android.view.View; -import android.view.inputmethod.BaseInputConnection; -import android.view.inputmethod.EditorInfo; -import android.view.inputmethod.InputConnection; -import android.view.inputmethod.InputConnectionWrapper; -import android.view.inputmethod.InputMethodManager; -import android.view.accessibility.AccessibilityEvent; -import android.widget.TextView; - -/** -* {@code ToolbarEditText} is the text entry used when the toolbar -* is in edit state. It handles all the necessary input method machinery. -* It's meant to be owned by {@code ToolbarEditLayout}. -*/ -public class ToolbarEditText extends CustomEditText - implements AutocompleteHandler { - - private static final String LOGTAG = "GeckoToolbarEditText"; - private static final NoCopySpan AUTOCOMPLETE_SPAN = new NoCopySpan.Concrete(); - - private final Context mContext; - - private OnCommitListener mCommitListener; - private OnDismissListener mDismissListener; - private OnFilterListener mFilterListener; - private OnSearchStateChangeListener mSearchStateChangeListener; - - private ToolbarPrefs mPrefs; - - // The previous autocomplete result returned to us - private String mAutoCompleteResult = ""; - // Length of the user-typed portion of the result - private int mAutoCompletePrefixLength; - // If text change is due to us setting autocomplete - private boolean mSettingAutoComplete; - // Spans used for marking the autocomplete text - private Object[] mAutoCompleteSpans; - // Do not process autocomplete result - private boolean mDiscardAutoCompleteResult; - - public ToolbarEditText(Context context, AttributeSet attrs) { - super(context, attrs); - mContext = context; - } - - void setOnCommitListener(OnCommitListener listener) { - mCommitListener = listener; - } - - void setOnDismissListener(OnDismissListener listener) { - mDismissListener = listener; - } - - void setOnFilterListener(OnFilterListener listener) { - mFilterListener = listener; - } - - void setOnSearchStateChangeListener(OnSearchStateChangeListener listener) { - mSearchStateChangeListener = listener; - } - - @Override - public void onAttachedToWindow() { - super.onAttachedToWindow(); - setOnKeyListener(new KeyListener()); - setOnKeyPreImeListener(new KeyPreImeListener()); - setOnSelectionChangedListener(new SelectionChangeListener()); - addTextChangedListener(new TextChangeListener()); - } - - @Override - public void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) { - super.onFocusChanged(gainFocus, direction, previouslyFocusedRect); - - // Make search icon inactive when edit toolbar search term isn't a user entered - // search term - final boolean isActive = !TextUtils.isEmpty(getText()); - if (mSearchStateChangeListener != null) { - mSearchStateChangeListener.onSearchStateChange(isActive); - } - - if (gainFocus) { - resetAutocompleteState(); - return; - } - - removeAutocomplete(getText()); - - final InputMethodManager imm = InputMethods.getInputMethodManager(mContext); - try { - imm.restartInput(this); - imm.hideSoftInputFromWindow(getWindowToken(), 0); - } catch (NullPointerException e) { - Log.e(LOGTAG, "InputMethodManagerService, why are you throwing" - + " a NullPointerException? See bug 782096", e); - } - } - - @Override - public void setText(final CharSequence text, final TextView.BufferType type) { - final String textString = (text == null) ? "" : text.toString(); - - // If we're on the home or private browsing page, we don't set the "about" url. - final CharSequence finalText; - if (AboutPages.isAboutHome(textString) || AboutPages.isAboutPrivateBrowsing(textString)) { - finalText = ""; - } else { - finalText = text; - } - - super.setText(finalText, type); - - // Any autocomplete text would have been overwritten, so reset our autocomplete states. - resetAutocompleteState(); - } - - @Override - public void sendAccessibilityEventUnchecked(AccessibilityEvent event) { - // We need to bypass the isShown() check in the default implementation - // for TYPE_VIEW_TEXT_SELECTION_CHANGED events so that accessibility - // services could detect a url change. - if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED && - getParent() != null && !isShown()) { - onInitializeAccessibilityEvent(event); - dispatchPopulateAccessibilityEvent(event); - getParent().requestSendAccessibilityEvent(this, event); - } else { - super.sendAccessibilityEventUnchecked(event); - } - } - - void setToolbarPrefs(final ToolbarPrefs prefs) { - mPrefs = prefs; - } - - /** - * Mark the start of autocomplete changes so our text change - * listener does not react to changes in autocomplete text - */ - private void beginSettingAutocomplete() { - beginBatchEdit(); - mSettingAutoComplete = true; - } - - /** - * Mark the end of autocomplete changes - */ - private void endSettingAutocomplete() { - mSettingAutoComplete = false; - endBatchEdit(); - } - - /** - * Reset autocomplete states to their initial values - */ - private void resetAutocompleteState() { - mAutoCompleteSpans = new Object[] { - // Span to mark the autocomplete text - AUTOCOMPLETE_SPAN, - // Span to change the autocomplete text color - new BackgroundColorSpan(getHighlightColor()) - }; - - mAutoCompleteResult = ""; - - // Pretend we already autocompleted the existing text, - // so that actions like backspacing don't trigger autocompletion. - mAutoCompletePrefixLength = getText().length(); - - // Show the cursor. - setCursorVisible(true); - } - - protected String getNonAutocompleteText() { - return getNonAutocompleteText(getText()); - } - - /** - * Get the portion of text that is not marked as autocomplete text. - * - * @param text Current text content that may include autocomplete text - */ - private static String getNonAutocompleteText(final Editable text) { - final int start = text.getSpanStart(AUTOCOMPLETE_SPAN); - if (start < 0) { - // No autocomplete text; return the whole string. - return text.toString(); - } - - // Only return the portion that's not autocomplete text - return TextUtils.substring(text, 0, start); - } - - /** - * Remove any autocomplete text - * - * @param text Current text content that may include autocomplete text - */ - private boolean removeAutocomplete(final Editable text) { - final int start = text.getSpanStart(AUTOCOMPLETE_SPAN); - if (start < 0) { - // No autocomplete text - return false; - } - - beginSettingAutocomplete(); - - // When we call delete() here, the autocomplete spans we set are removed as well. - text.delete(start, text.length()); - - // Keep mAutoCompletePrefixLength the same because the prefix has not changed. - // Clear mAutoCompleteResult to make sure we get fresh autocomplete text next time. - mAutoCompleteResult = ""; - - // Reshow the cursor. - setCursorVisible(true); - - endSettingAutocomplete(); - return true; - } - - /** - * Convert any autocomplete text to regular text - * - * @param text Current text content that may include autocomplete text - */ - private boolean commitAutocomplete(final Editable text) { - final int start = text.getSpanStart(AUTOCOMPLETE_SPAN); - if (start < 0) { - // No autocomplete text - return false; - } - - beginSettingAutocomplete(); - - // Remove all spans here to convert from autocomplete text to regular text - for (final Object span : mAutoCompleteSpans) { - text.removeSpan(span); - } - - // Keep mAutoCompleteResult the same because the result has not changed. - // Reset mAutoCompletePrefixLength because the prefix now includes the autocomplete text. - mAutoCompletePrefixLength = text.length(); - - // Reshow the cursor. - setCursorVisible(true); - - endSettingAutocomplete(); - - // Filter on the new text - if (mFilterListener != null) { - mFilterListener.onFilter(text.toString(), null); - } - return true; - } - - /** - * Add autocomplete text based on the result URI. - * - * @param result Result URI to be turned into autocomplete text - */ - @Override - public final void onAutocomplete(final String result) { - // If mDiscardAutoCompleteResult is true, we temporarily disabled - // autocomplete (due to backspacing, etc.) and we should bail early. - if (mDiscardAutoCompleteResult) { - return; - } - - if (!isEnabled() || result == null) { - mAutoCompleteResult = ""; - return; - } - - final Editable text = getText(); - final int textLength = text.length(); - final int resultLength = result.length(); - final int autoCompleteStart = text.getSpanStart(AUTOCOMPLETE_SPAN); - mAutoCompleteResult = result; - - if (autoCompleteStart > -1) { - // Autocomplete text already exists; we should replace existing autocomplete text. - - // If the result and the current text don't have the same prefixes, - // the result is stale and we should wait for the another result to come in. - if (!TextUtils.regionMatches(result, 0, text, 0, autoCompleteStart)) { - return; - } - - beginSettingAutocomplete(); - - // Replace the existing autocomplete text with new one. - // replace() preserves the autocomplete spans that we set before. - text.replace(autoCompleteStart, textLength, result, autoCompleteStart, resultLength); - - // Reshow the cursor if there is no longer any autocomplete text. - if (autoCompleteStart == resultLength) { - setCursorVisible(true); - } - - endSettingAutocomplete(); - - } else { - // No autocomplete text yet; we should add autocomplete text - - // If the result prefix doesn't match the current text, - // the result is stale and we should wait for the another result to come in. - if (resultLength <= textLength || - !TextUtils.regionMatches(result, 0, text, 0, textLength)) { - return; - } - - final Object[] spans = text.getSpans(textLength, textLength, Object.class); - final int[] spanStarts = new int[spans.length]; - final int[] spanEnds = new int[spans.length]; - final int[] spanFlags = new int[spans.length]; - - // Save selection/composing span bounds so we can restore them later. - for (int i = 0; i < spans.length; i++) { - final Object span = spans[i]; - final int spanFlag = text.getSpanFlags(span); - - // We don't care about spans that are not selection or composing spans. - // For those spans, spanFlag[i] will be 0 and we don't restore them. - if ((spanFlag & Spanned.SPAN_COMPOSING) == 0 && - (span != Selection.SELECTION_START) && - (span != Selection.SELECTION_END)) { - continue; - } - - spanStarts[i] = text.getSpanStart(span); - spanEnds[i] = text.getSpanEnd(span); - spanFlags[i] = spanFlag; - } - - beginSettingAutocomplete(); - - // First add trailing text. - text.append(result, textLength, resultLength); - - // Restore selection/composing spans. - for (int i = 0; i < spans.length; i++) { - final int spanFlag = spanFlags[i]; - if (spanFlag == 0) { - // Skip if the span was ignored before. - continue; - } - text.setSpan(spans[i], spanStarts[i], spanEnds[i], spanFlag); - } - - // Mark added text as autocomplete text. - for (final Object span : mAutoCompleteSpans) { - text.setSpan(span, textLength, resultLength, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - } - - // Hide the cursor. - setCursorVisible(false); - - // Make sure the autocomplete text is visible. If the autocomplete text is too - // long, it would appear the cursor will be scrolled out of view. However, this - // is not the case in practice, because EditText still makes sure the cursor is - // still in view. - bringPointIntoView(resultLength); - - endSettingAutocomplete(); - } - } - - private static boolean hasCompositionString(Editable content) { - Object[] spans = content.getSpans(0, content.length(), Object.class); - - if (spans != null) { - for (Object span : spans) { - if ((content.getSpanFlags(span) & Spanned.SPAN_COMPOSING) != 0) { - // Found composition string. - return true; - } - } - } - - return false; - } - - /** - * Code to handle deleting autocomplete first when backspacing. - * If there is no autocomplete text, both removeAutocomplete() and commitAutocomplete() - * are no-ops and return false. Therefore we can use them here without checking explicitly - * if we have autocomplete text or not. - */ - @Override - public InputConnection onCreateInputConnection(final EditorInfo outAttrs) { - final InputConnection ic = super.onCreateInputConnection(outAttrs); - if (ic == null) { - return null; - } - - return new InputConnectionWrapper(ic, false) { - @Override - public boolean deleteSurroundingText(final int beforeLength, final int afterLength) { - if (removeAutocomplete(getText())) { - // If we have autocomplete text, the cursor is at the boundary between - // regular and autocomplete text. So regardless of which direction we - // are deleting, we should delete the autocomplete text first. - // Make the IME aware that we interrupted the deleteSurroundingText call, - // by restarting the IME. - final InputMethodManager imm = InputMethods.getInputMethodManager(mContext); - if (imm != null) { - imm.restartInput(ToolbarEditText.this); - } - return false; - } - return super.deleteSurroundingText(beforeLength, afterLength); - } - - private boolean removeAutocompleteOnComposing(final CharSequence text) { - final Editable editable = getText(); - final int composingStart = BaseInputConnection.getComposingSpanStart(editable); - final int composingEnd = BaseInputConnection.getComposingSpanEnd(editable); - // We only delete the autocomplete text when the user is backspacing, - // i.e. when the composing text is getting shorter. - if (composingStart >= 0 && - composingEnd >= 0 && - (composingEnd - composingStart) > text.length() && - removeAutocomplete(editable)) { - // Make the IME aware that we interrupted the setComposingText call, - // by having finishComposingText() send change notifications to the IME. - finishComposingText(); - setComposingRegion(composingStart, composingEnd); - return true; - } - return false; - } - - @Override - public boolean commitText(CharSequence text, int newCursorPosition) { - if (removeAutocompleteOnComposing(text)) { - return false; - } - return super.commitText(text, newCursorPosition); - } - - @Override - public boolean setComposingText(final CharSequence text, final int newCursorPosition) { - if (removeAutocompleteOnComposing(text)) { - return false; - } - return super.setComposingText(text, newCursorPosition); - } - }; - } - - private class SelectionChangeListener implements OnSelectionChangedListener { - @Override - public void onSelectionChanged(final int selStart, final int selEnd) { - // The user has repositioned the cursor somewhere. We need to adjust - // the autocomplete text depending on where the new cursor is. - - final Editable text = getText(); - final int start = text.getSpanStart(AUTOCOMPLETE_SPAN); - - if (mSettingAutoComplete || start < 0 || (start == selStart && start == selEnd)) { - // Do not commit autocomplete text if there is no autocomplete text - // or if selection is still at start of autocomplete text - return; - } - - if (selStart <= start && selEnd <= start) { - // The cursor is in user-typed text; remove any autocomplete text. - removeAutocomplete(text); - } else { - // The cursor is in the autocomplete text; commit it so it becomes regular text. - commitAutocomplete(text); - } - } - } - - private class TextChangeListener implements TextWatcher { - @Override - public void afterTextChanged(final Editable editable) { - if (!isEnabled() || mSettingAutoComplete) { - return; - } - - final String text = getNonAutocompleteText(editable); - final int textLength = text.length(); - boolean doAutocomplete = mPrefs.shouldAutocomplete(); - - if (StringUtils.isSearchQuery(text, false)) { - doAutocomplete = false; - } else if (mAutoCompletePrefixLength > textLength) { - // If you're hitting backspace (the string is getting smaller), don't autocomplete - doAutocomplete = false; - } - - mAutoCompletePrefixLength = textLength; - - // If we are not autocompleting, we set mDiscardAutoCompleteResult to true - // to discard any autocomplete results that are in-flight, and vice versa. - mDiscardAutoCompleteResult = !doAutocomplete; - - if (doAutocomplete && mAutoCompleteResult.startsWith(text)) { - // If this text already matches our autocomplete text, autocomplete likely - // won't change. Just reuse the old autocomplete value. - onAutocomplete(mAutoCompleteResult); - doAutocomplete = false; - } else { - // Otherwise, remove the old autocomplete text - // until any new autocomplete text gets added. - removeAutocomplete(editable); - } - - // Update search icon with an active state since user is typing - if (mSearchStateChangeListener != null) { - mSearchStateChangeListener.onSearchStateChange(textLength > 0); - } - - if (mFilterListener != null) { - mFilterListener.onFilter(text, doAutocomplete ? ToolbarEditText.this : null); - } - } - - @Override - public void beforeTextChanged(CharSequence s, int start, int count, - int after) { - // do nothing - } - - @Override - public void onTextChanged(CharSequence s, int start, int before, - int count) { - // do nothing - } - } - - private class KeyPreImeListener implements OnKeyPreImeListener { - @Override - public boolean onKeyPreIme(View v, int keyCode, KeyEvent event) { - // We only want to process one event per tap - if (event.getAction() != KeyEvent.ACTION_DOWN) { - return false; - } - - if (keyCode == KeyEvent.KEYCODE_ENTER) { - // If the edit text has a composition string, don't submit the text yet. - // ENTER is needed to commit the composition string. - final Editable content = getText(); - if (!hasCompositionString(content)) { - if (mCommitListener != null) { - mCommitListener.onCommit(); - } - - return true; - } - } - - if (keyCode == KeyEvent.KEYCODE_BACK) { - // Drop the virtual keyboard. - clearFocus(); - return true; - } - - return false; - } - } - - private class KeyListener implements View.OnKeyListener { - @Override - public boolean onKey(View v, int keyCode, KeyEvent event) { - if (keyCode == KeyEvent.KEYCODE_ENTER || GamepadUtils.isActionKey(event)) { - if (event.getAction() != KeyEvent.ACTION_DOWN) { - return true; - } - - if (mCommitListener != null) { - mCommitListener.onCommit(); - } - - return true; - } - - if (GamepadUtils.isBackKey(event)) { - if (mDismissListener != null) { - mDismissListener.onDismiss(); - } - - return true; - } - - if ((keyCode == KeyEvent.KEYCODE_DEL || - (keyCode == KeyEvent.KEYCODE_FORWARD_DEL)) && - removeAutocomplete(getText())) { - // Delete autocomplete text when backspacing or forward deleting. - return true; - } - - return false; - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/toolbar/ToolbarPrefs.java b/mobile/android/base/java/org/mozilla/gecko/toolbar/ToolbarPrefs.java deleted file mode 100644 index f881de154a..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/toolbar/ToolbarPrefs.java +++ /dev/null @@ -1,78 +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.toolbar; - -import org.mozilla.gecko.PrefsHelper; -import org.mozilla.gecko.Tab; -import org.mozilla.gecko.Tabs; -import org.mozilla.gecko.util.ThreadUtils; - -class ToolbarPrefs { - private static final String PREF_AUTOCOMPLETE_ENABLED = "browser.urlbar.autocomplete.enabled"; - private static final String PREF_TRIM_URLS = "browser.urlbar.trimURLs"; - - private static final String[] PREFS = { - PREF_AUTOCOMPLETE_ENABLED, - PREF_TRIM_URLS - }; - - private final TitlePrefsHandler HANDLER = new TitlePrefsHandler(); - - private volatile boolean enableAutocomplete; - private volatile boolean trimUrls; - - ToolbarPrefs() { - // Skip autocompletion while Gecko is loading. - // We will get the correct pref value once Gecko is loaded. - enableAutocomplete = false; - trimUrls = true; - } - - boolean shouldAutocomplete() { - return enableAutocomplete; - } - - boolean shouldTrimUrls() { - return trimUrls; - } - - void open() { - PrefsHelper.addObserver(PREFS, HANDLER); - } - - void close() { - PrefsHelper.removeObserver(HANDLER); - } - - private void triggerTitleChangeListener() { - ThreadUtils.postToUiThread(new Runnable() { - @Override - public void run() { - final Tabs tabs = Tabs.getInstance(); - final Tab tab = tabs.getSelectedTab(); - if (tab != null) { - tabs.notifyListeners(tab, Tabs.TabEvents.TITLE); - } - } - }); - } - - private class TitlePrefsHandler extends PrefsHelper.PrefHandlerBase { - @Override - public void prefValue(String pref, boolean value) { - if (PREF_AUTOCOMPLETE_ENABLED.equals(pref)) { - enableAutocomplete = value; - - } else if (PREF_TRIM_URLS.equals(pref)) { - // Handles PREF_TRIM_URLS, which should usually be a boolean. - if (value != trimUrls) { - trimUrls = value; - triggerTitleChangeListener(); - } - } - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/toolbar/ToolbarProgressView.java b/mobile/android/base/java/org/mozilla/gecko/toolbar/ToolbarProgressView.java deleted file mode 100644 index 43181cbef5..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/toolbar/ToolbarProgressView.java +++ /dev/null @@ -1,195 +0,0 @@ -/* - * Copyright (C) 2010 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.mozilla.gecko.toolbar; - -import android.support.v4.content.ContextCompat; -import org.mozilla.gecko.AppConstants.Versions; -import org.mozilla.gecko.R; -import org.mozilla.gecko.widget.themed.ThemedImageView; -import org.mozilla.gecko.util.WeakReferenceHandler; - -import android.content.Context; -import android.graphics.Canvas; -import android.graphics.PorterDuff; -import android.graphics.PorterDuffColorFilter; -import android.graphics.Rect; -import android.graphics.drawable.Drawable; -import android.os.Handler; -import android.os.Message; -import android.util.AttributeSet; -import android.view.View; -import android.view.animation.Animation; - -/** - * Progress view used for page loads. - * - * Because we're given limited information about the page load progress, the - * bar also includes incremental animation between each step to improve - * perceived performance. - */ -public class ToolbarProgressView extends ThemedImageView { - private static final int MAX_PROGRESS = 10000; - private static final int MSG_UPDATE = 0; - private static final int MSG_HIDE = 1; - private static final int STEPS = 10; - private static final int DELAY = 40; - - private int mTargetProgress; - private int mIncrement; - private Rect mBounds; - private Handler mHandler; - private int mCurrentProgress; - - private PorterDuffColorFilter mPrivateBrowsingColorFilter; - - public ToolbarProgressView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - init(context); - } - - public ToolbarProgressView(Context context, AttributeSet attrs) { - super(context, attrs); - init(context); - } - - private void init(Context ctx) { - mBounds = new Rect(0, 0, 0, 0); - mTargetProgress = 0; - - mPrivateBrowsingColorFilter = new PorterDuffColorFilter( - ContextCompat.getColor(ctx, R.color.private_browsing_purple), PorterDuff.Mode.SRC_IN); - - mHandler = new ToolbarProgressHandler(this); - } - - @Override - public void onLayout(boolean f, int l, int t, int r, int b) { - mBounds.left = 0; - mBounds.right = (r - l) * mCurrentProgress / MAX_PROGRESS; - mBounds.top = 0; - mBounds.bottom = b - t; - } - - @Override - public void onDraw(Canvas canvas) { - final Drawable d = getDrawable(); - d.setBounds(mBounds); - d.draw(canvas); - } - - /** - * Immediately sets the progress bar to the given progress percentage. - * - * @param progress Percentage (0-100) to which progress bar should be set - */ - void setProgress(int progressPercentage) { - mCurrentProgress = mTargetProgress = getAbsoluteProgress(progressPercentage); - updateBounds(); - - clearMessages(); - } - - /** - * Animates the progress bar from the current progress value to the given - * progress percentage. - * - * @param progress Percentage (0-100) to which progress bar should be animated - */ - void animateProgress(int progressPercentage) { - final int absoluteProgress = getAbsoluteProgress(progressPercentage); - if (absoluteProgress <= mTargetProgress) { - // After we manually click stop, we can still receive page load - // events (e.g., DOMContentLoaded). Updating for other updates - // after a STOP event can freeze the progress bar, so guard against - // that here. - return; - } - - mTargetProgress = absoluteProgress; - mIncrement = (mTargetProgress - mCurrentProgress) / STEPS; - - clearMessages(); - mHandler.sendEmptyMessage(MSG_UPDATE); - } - - private void clearMessages() { - mHandler.removeMessages(MSG_UPDATE); - mHandler.removeMessages(MSG_HIDE); - } - - private int getAbsoluteProgress(int progressPercentage) { - if (progressPercentage < 0) { - return 0; - } - - if (progressPercentage > 100) { - return 100; - } - - return progressPercentage * MAX_PROGRESS / 100; - } - - private void updateBounds() { - mBounds.right = getWidth() * mCurrentProgress / MAX_PROGRESS; - invalidate(); - } - - @Override - public void setPrivateMode(final boolean isPrivate) { - super.setPrivateMode(isPrivate); - - // Note: android:tint is better but ColorStateLists are not supported until API 21. - if (isPrivate) { - setColorFilter(mPrivateBrowsingColorFilter); - } else { - clearColorFilter(); - } - } - - private static class ToolbarProgressHandler extends WeakReferenceHandler { - public ToolbarProgressHandler(final ToolbarProgressView that) { - super(that); - } - - @Override - public void handleMessage(Message msg) { - final ToolbarProgressView that = mTarget.get(); - if (that == null) { - return; - } - - switch (msg.what) { - case MSG_UPDATE: - that.mCurrentProgress = Math.min(that.mTargetProgress, that.mCurrentProgress + that.mIncrement); - - that.updateBounds(); - - if (that.mCurrentProgress < that.mTargetProgress) { - final int delay = (that.mTargetProgress < MAX_PROGRESS) ? DELAY : DELAY / 4; - sendMessageDelayed(that.mHandler.obtainMessage(msg.what), delay); - } else if (that.mCurrentProgress == MAX_PROGRESS) { - sendMessageDelayed(that.mHandler.obtainMessage(MSG_HIDE), DELAY); - } - break; - - case MSG_HIDE: - that.setVisibility(View.GONE); - break; - } - } - }; -} diff --git a/mobile/android/base/java/org/mozilla/gecko/trackingprotection/TrackingProtectionPrompt.java b/mobile/android/base/java/org/mozilla/gecko/trackingprotection/TrackingProtectionPrompt.java deleted file mode 100644 index dcc62b6d44..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/trackingprotection/TrackingProtectionPrompt.java +++ /dev/null @@ -1,131 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.trackingprotection; - -import org.mozilla.gecko.Locales; -import org.mozilla.gecko.R; -import org.mozilla.gecko.preferences.GeckoPreferences; -import org.mozilla.gecko.util.HardwareUtils; - -import android.content.Intent; -import android.os.Bundle; -import android.view.MotionEvent; -import android.view.View; -import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; -import android.animation.AnimatorSet; -import android.animation.ObjectAnimator; - -public class TrackingProtectionPrompt extends Locales.LocaleAwareActivity { - public static final String LOGTAG = "Gecko" + TrackingProtectionPrompt.class.getSimpleName(); - - // Flag set during animation to prevent animation multiple-start. - private boolean isAnimating; - - private View containerView; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - showPrompt(); - } - - private void showPrompt() { - setContentView(R.layout.tracking_protection_prompt); - - findViewById(R.id.ok_button).setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - onConfirmButtonPressed(); - } - }); - findViewById(R.id.link_text).setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - slideOut(); - final Intent settingsIntent = new Intent(TrackingProtectionPrompt.this, GeckoPreferences.class); - GeckoPreferences.setResourceToOpen(settingsIntent, "preferences_privacy"); - startActivity(settingsIntent); - - // Don't use a transition to settings if we're on a device where that - // would look bad. - if (HardwareUtils.IS_KINDLE_DEVICE) { - overridePendingTransition(0, 0); - } - } - }); - - containerView = findViewById(R.id.tracking_protection_inner_container); - - containerView.setTranslationY(500); - containerView.setAlpha(0); - - final Animator translateAnimator = ObjectAnimator.ofFloat(containerView, "translationY", 0); - translateAnimator.setDuration(400); - - final Animator alphaAnimator = ObjectAnimator.ofFloat(containerView, "alpha", 1); - alphaAnimator.setStartDelay(200); - alphaAnimator.setDuration(600); - - final AnimatorSet set = new AnimatorSet(); - set.playTogether(alphaAnimator, translateAnimator); - set.setStartDelay(400); - - set.start(); - } - - @Override - public void finish() { - super.finish(); - - // Don't perform an activity-dismiss animation. - overridePendingTransition(0, 0); - } - - private void onConfirmButtonPressed() { - slideOut(); - } - - /** - * Slide the overlay down off the screen and destroy it. - */ - private void slideOut() { - if (isAnimating) { - return; - } - - isAnimating = true; - - ObjectAnimator animator = ObjectAnimator.ofFloat(containerView, "translationY", containerView.getHeight()); - animator.addListener(new AnimatorListenerAdapter() { - - @Override - public void onAnimationEnd(Animator animation) { - finish(); - } - - }); - animator.start(); - } - - /** - * Close the dialog if back is pressed. - */ - @Override - public void onBackPressed() { - slideOut(); - } - - /** - * Close the dialog if the anything that isn't a button is tapped. - */ - @Override - public boolean onTouchEvent(MotionEvent event) { - slideOut(); - return true; - } - } diff --git a/mobile/android/base/java/org/mozilla/gecko/updater/PostUpdateHandler.java b/mobile/android/base/java/org/mozilla/gecko/updater/PostUpdateHandler.java deleted file mode 100644 index f0ad78e776..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/updater/PostUpdateHandler.java +++ /dev/null @@ -1,120 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.updater; - -import android.content.Context; -import android.content.res.AssetManager; -import android.content.SharedPreferences; -import android.util.Log; - -import com.keepsafe.switchboard.SwitchBoard; - -import org.mozilla.gecko.AppConstants; -import org.mozilla.gecko.BrowserApp; -import org.mozilla.gecko.delegates.BrowserAppDelegateWithReference; -import org.mozilla.gecko.GeckoSharedPrefs; -import org.mozilla.gecko.preferences.GeckoPreferences; -import org.mozilla.gecko.util.IOUtils; -import org.mozilla.gecko.util.ThreadUtils; - -import java.io.File; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.InputStream; -import java.io.IOException; -import java.io.OutputStream; -import java.util.Enumeration; -import java.util.zip.ZipEntry; -import java.util.zip.ZipFile; - -/** - * Perform tasks in the background after the app has been installed/updated. - */ -public class PostUpdateHandler extends BrowserAppDelegateWithReference { - private static final String LOGTAG = "PostUpdateHandler"; - - @Override - public void onStart(final BrowserApp browserApp) { - ThreadUtils.postToBackgroundThread(new Runnable() { - @Override - public void run() { - final SharedPreferences prefs = GeckoSharedPrefs.forApp(browserApp); - - // Check if this is a new installation or if the app has been updated since the last start. - if (!AppConstants.MOZ_APP_BUILDID.equals(prefs.getString(GeckoPreferences.PREFS_APP_UPDATE_LAST_BUILD_ID, null))) { - Log.d(LOGTAG, "Build ID changed since last start: '" + AppConstants.MOZ_APP_BUILDID + "', '" + prefs.getString(GeckoPreferences.PREFS_APP_UPDATE_LAST_BUILD_ID, null) + "'"); - - // Copy the bundled system add-ons from the APK to the data directory. - copyFeaturesFromAPK(browserApp); - } - } - }); - } - - /** - * Copies the /assets/features folder out of the APK and into the app's data directory. - */ - private void copyFeaturesFromAPK(BrowserApp browserApp) { - Log.d(LOGTAG, "Copying system add-ons from APK to dataDir"); - - final String dataDir = browserApp.getApplicationInfo().dataDir; - final SharedPreferences prefs = GeckoSharedPrefs.forApp(browserApp); - final AssetManager assetManager = browserApp.getContext().getAssets(); - - try { - final String[] assetNames = assetManager.list("features"); - - for (int i = 0; i < assetNames.length; i++) { - final String assetPath = "features/" + assetNames[i]; - - Log.d(LOGTAG, "Copying '" + assetPath + "' from APK to dataDir"); - - final InputStream assetStream = assetManager.open(assetPath); - final File outFile = getDataFile(dataDir, assetPath); - - if (outFile == null) { - continue; - } - - final OutputStream outStream = new FileOutputStream(outFile); - - try { - IOUtils.copy(assetStream, outStream); - } catch (IOException e) { - Log.e(LOGTAG, "Error copying '" + assetPath + "' from APK to dataDir"); - } finally { - outStream.close(); - } - } - } catch (IOException e) { - Log.e(LOGTAG, "Error retrieving packaged system add-ons from APK", e); - } - - // Save the Build ID so we don't perform post-update operations again until the app is updated. - prefs.edit().putString(GeckoPreferences.PREFS_APP_UPDATE_LAST_BUILD_ID, AppConstants.MOZ_APP_BUILDID).apply(); - } - - /** - * Return a File instance in the data directory, ensuring - * that the parent exists. - * - * @return null if the parents could not be created. - */ - private File getDataFile(final String dataDir, final String name) { - File outFile = new File(dataDir, name); - File dir = outFile.getParentFile(); - - if (!dir.exists()) { - Log.d(LOGTAG, "Creating " + dir.getAbsolutePath()); - if (!dir.mkdirs()) { - Log.e(LOGTAG, "Unable to create directories: " + dir.getAbsolutePath()); - return null; - } - } - - return outFile; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/updater/UpdateService.java b/mobile/android/base/java/org/mozilla/gecko/updater/UpdateService.java deleted file mode 100644 index 7ccc43e282..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/updater/UpdateService.java +++ /dev/null @@ -1,795 +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.updater; - -import org.mozilla.gecko.AppConstants; -import org.mozilla.gecko.CrashHandler; -import org.mozilla.gecko.R; - -import org.mozilla.apache.commons.codec.binary.Hex; - -import org.mozilla.gecko.permissions.Permissions; -import org.mozilla.gecko.util.ProxySelector; -import org.w3c.dom.Document; -import org.w3c.dom.Node; -import org.w3c.dom.NodeList; - -import android.Manifest; -import android.app.AlarmManager; -import android.app.IntentService; -import android.app.Notification; -import android.app.NotificationManager; -import android.app.PendingIntent; -import android.app.Service; -import android.content.Context; -import android.content.Intent; -import android.content.SharedPreferences; -import android.net.ConnectivityManager; -import android.net.NetworkInfo; -import android.net.Uri; -import android.net.wifi.WifiManager; -import android.net.wifi.WifiManager.WifiLock; -import android.os.Environment; -import android.provider.Settings; -import android.support.v4.app.NotificationManagerCompat; -import android.support.v4.content.ContextCompat; -import android.support.v4.net.ConnectivityManagerCompat; -import android.support.v4.app.NotificationCompat; -import android.support.v4.app.NotificationCompat.Builder; -import android.util.Log; - -import java.io.BufferedInputStream; -import java.io.BufferedOutputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.InputStream; -import java.io.OutputStream; -import java.net.URI; -import java.net.URL; -import java.net.URLConnection; -import java.security.MessageDigest; -import java.util.Calendar; -import java.util.GregorianCalendar; -import java.util.List; -import java.util.TimeZone; - -import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; - -public class UpdateService extends IntentService { - private static final int BUFSIZE = 8192; - private static final int NOTIFICATION_ID = 0x3e40ddbd; - - private static final String LOGTAG = "UpdateService"; - - private static final int INTERVAL_LONG = 86400000; // in milliseconds - private static final int INTERVAL_SHORT = 14400000; // again, in milliseconds - private static final int INTERVAL_RETRY = 3600000; - - private static final String PREFS_NAME = "UpdateService"; - private static final String KEY_LAST_BUILDID = "UpdateService.lastBuildID"; - private static final String KEY_LAST_HASH_FUNCTION = "UpdateService.lastHashFunction"; - private static final String KEY_LAST_HASH_VALUE = "UpdateService.lastHashValue"; - private static final String KEY_LAST_FILE_NAME = "UpdateService.lastFileName"; - private static final String KEY_LAST_ATTEMPT_DATE = "UpdateService.lastAttemptDate"; - private static final String KEY_AUTODOWNLOAD_POLICY = "UpdateService.autoDownloadPolicy"; - private static final String KEY_UPDATE_URL = "UpdateService.updateUrl"; - - private SharedPreferences mPrefs; - - private NotificationManagerCompat mNotificationManager; - private ConnectivityManager mConnectivityManager; - private Builder mBuilder; - - private volatile WifiLock mWifiLock; - - private boolean mDownloading; - private boolean mCancelDownload; - private boolean mApplyImmediately; - - private CrashHandler mCrashHandler; - - public enum AutoDownloadPolicy { - NONE(-1), - WIFI(0), - DISABLED(1), - ENABLED(2); - - public final int value; - - private AutoDownloadPolicy(int value) { - this.value = value; - } - - private final static AutoDownloadPolicy[] sValues = AutoDownloadPolicy.values(); - - public static AutoDownloadPolicy get(int value) { - for (AutoDownloadPolicy id: sValues) { - if (id.value == value) { - return id; - } - } - return NONE; - } - - public static AutoDownloadPolicy get(String name) { - for (AutoDownloadPolicy id: sValues) { - if (name.equalsIgnoreCase(id.toString())) { - return id; - } - } - return NONE; - } - } - - private enum CheckUpdateResult { - // Keep these in sync with mobile/android/chrome/content/about.xhtml - NOT_AVAILABLE, - AVAILABLE, - DOWNLOADING, - DOWNLOADED - } - - - public UpdateService() { - super("updater"); - } - - @Override - public void onCreate () { - mCrashHandler = CrashHandler.createDefaultCrashHandler(getApplicationContext()); - - super.onCreate(); - - mPrefs = getSharedPreferences(PREFS_NAME, 0); - mNotificationManager = NotificationManagerCompat.from(this); - mConnectivityManager = (ConnectivityManager)getSystemService(Context.CONNECTIVITY_SERVICE); - mWifiLock = ((WifiManager)getSystemService(Context.WIFI_SERVICE)) - .createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, PREFS_NAME); - mCancelDownload = false; - } - - @Override - public void onDestroy() { - mCrashHandler.unregister(); - mCrashHandler = null; - - if (mWifiLock.isHeld()) { - mWifiLock.release(); - } - } - - @Override - public synchronized int onStartCommand (Intent intent, int flags, int startId) { - // If we are busy doing a download, the new Intent here would normally be queued for - // execution once that is done. In this case, however, we want to flip the boolean - // while that is running, so handle that now. - if (mDownloading && UpdateServiceHelper.ACTION_APPLY_UPDATE.equals(intent.getAction())) { - Log.i(LOGTAG, "will apply update when download finished"); - - mApplyImmediately = true; - showDownloadNotification(); - } else if (UpdateServiceHelper.ACTION_CANCEL_DOWNLOAD.equals(intent.getAction())) { - mCancelDownload = true; - } else { - super.onStartCommand(intent, flags, startId); - } - - return Service.START_REDELIVER_INTENT; - } - - @Override - protected void onHandleIntent (final Intent intent) { - if (UpdateServiceHelper.ACTION_REGISTER_FOR_UPDATES.equals(intent.getAction())) { - AutoDownloadPolicy policy = AutoDownloadPolicy.get( - intent.getIntExtra(UpdateServiceHelper.EXTRA_AUTODOWNLOAD_NAME, - AutoDownloadPolicy.NONE.value)); - - if (policy != AutoDownloadPolicy.NONE) { - setAutoDownloadPolicy(policy); - } - - String url = intent.getStringExtra(UpdateServiceHelper.EXTRA_UPDATE_URL_NAME); - if (url != null) { - setUpdateUrl(url); - } - - registerForUpdates(false); - } else if (UpdateServiceHelper.ACTION_CHECK_FOR_UPDATE.equals(intent.getAction())) { - startUpdate(intent.getIntExtra(UpdateServiceHelper.EXTRA_UPDATE_FLAGS_NAME, 0)); - // Use this instead for forcing a download from about:fennec - // startUpdate(UpdateServiceHelper.FLAG_FORCE_DOWNLOAD | UpdateServiceHelper.FLAG_REINSTALL); - } else if (UpdateServiceHelper.ACTION_DOWNLOAD_UPDATE.equals(intent.getAction())) { - // We always want to do the download and apply it here - mApplyImmediately = true; - startUpdate(UpdateServiceHelper.FLAG_FORCE_DOWNLOAD); - } else if (UpdateServiceHelper.ACTION_APPLY_UPDATE.equals(intent.getAction())) { - applyUpdate(intent.getStringExtra(UpdateServiceHelper.EXTRA_PACKAGE_PATH_NAME)); - } - } - - private static boolean hasFlag(int flags, int flag) { - return (flags & flag) == flag; - } - - private void sendCheckUpdateResult(CheckUpdateResult result) { - Intent resultIntent = new Intent(UpdateServiceHelper.ACTION_CHECK_UPDATE_RESULT); - resultIntent.putExtra("result", result.toString()); - sendBroadcast(resultIntent); - } - - private int getUpdateInterval(boolean isRetry) { - int interval; - if (isRetry) { - interval = INTERVAL_RETRY; - } else if (!AppConstants.RELEASE_OR_BETA) { - interval = INTERVAL_SHORT; - } else { - interval = INTERVAL_LONG; - } - - return interval; - } - - private void registerForUpdates(boolean isRetry) { - Calendar lastAttempt = getLastAttemptDate(); - Calendar now = new GregorianCalendar(TimeZone.getTimeZone("GMT")); - - int interval = getUpdateInterval(isRetry); - - if (lastAttempt == null || (now.getTimeInMillis() - lastAttempt.getTimeInMillis()) > interval) { - // We've either never attempted an update, or we are passed the desired - // time. Start an update now. - Log.i(LOGTAG, "no update has ever been attempted, checking now"); - startUpdate(0); - return; - } - - AlarmManager manager = (AlarmManager) getSystemService(Context.ALARM_SERVICE); - if (manager == null) - return; - - PendingIntent pending = PendingIntent.getService(this, 0, new Intent(UpdateServiceHelper.ACTION_CHECK_FOR_UPDATE, null, this, UpdateService.class), PendingIntent.FLAG_UPDATE_CURRENT); - manager.cancel(pending); - - lastAttempt.setTimeInMillis(lastAttempt.getTimeInMillis() + interval); - Log.i(LOGTAG, "next update will be at: " + lastAttempt.getTime()); - - manager.set(AlarmManager.RTC_WAKEUP, lastAttempt.getTimeInMillis(), pending); - } - - private void startUpdate(final int flags) { - setLastAttemptDate(); - - NetworkInfo netInfo = mConnectivityManager.getActiveNetworkInfo(); - if (netInfo == null || !netInfo.isConnected()) { - Log.i(LOGTAG, "not connected to the network"); - registerForUpdates(true); - sendCheckUpdateResult(CheckUpdateResult.NOT_AVAILABLE); - return; - } - - registerForUpdates(false); - - final UpdateInfo info = findUpdate(hasFlag(flags, UpdateServiceHelper.FLAG_REINSTALL)); - boolean haveUpdate = (info != null); - - if (!haveUpdate) { - Log.i(LOGTAG, "no update available"); - sendCheckUpdateResult(CheckUpdateResult.NOT_AVAILABLE); - return; - } - - Log.i(LOGTAG, "update available, buildID = " + info.buildID); - - Permissions.from(this) - .withPermissions(Manifest.permission.WRITE_EXTERNAL_STORAGE) - .doNotPrompt() - .andFallback(new Runnable() { - @Override - public void run() { - showPermissionNotification(); - sendCheckUpdateResult(CheckUpdateResult.NOT_AVAILABLE); - }}) - .run(new Runnable() { - @Override - public void run() { - startDownload(info, flags); - }}); - } - - private void startDownload(UpdateInfo info, int flags) { - AutoDownloadPolicy policy = getAutoDownloadPolicy(); - - // We only start a download automatically if one of following criteria are met: - // - // - We have a FORCE_DOWNLOAD flag passed in - // - The preference is set to 'always' - // - The preference is set to 'wifi' and we are using a non-metered network (i.e. the user - // is OK with large data transfers occurring) - // - boolean shouldStartDownload = hasFlag(flags, UpdateServiceHelper.FLAG_FORCE_DOWNLOAD) || - policy == AutoDownloadPolicy.ENABLED || - (policy == AutoDownloadPolicy.WIFI && !ConnectivityManagerCompat.isActiveNetworkMetered(mConnectivityManager)); - - if (!shouldStartDownload) { - Log.i(LOGTAG, "not initiating automatic update download due to policy " + policy.toString()); - sendCheckUpdateResult(CheckUpdateResult.AVAILABLE); - - // We aren't autodownloading here, so prompt to start the update - Intent notificationIntent = new Intent(UpdateServiceHelper.ACTION_DOWNLOAD_UPDATE); - notificationIntent.setClass(this, UpdateService.class); - PendingIntent contentIntent = PendingIntent.getService(this, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT); - - NotificationCompat.Builder builder = new NotificationCompat.Builder(this); - builder.setSmallIcon(R.drawable.ic_status_logo); - builder.setWhen(System.currentTimeMillis()); - builder.setAutoCancel(true); - builder.setContentTitle(getString(R.string.updater_start_title)); - builder.setContentText(getString(R.string.updater_start_select)); - builder.setContentIntent(contentIntent); - - mNotificationManager.notify(NOTIFICATION_ID, builder.build()); - - return; - } - - File pkg = downloadUpdatePackage(info, hasFlag(flags, UpdateServiceHelper.FLAG_OVERWRITE_EXISTING)); - if (pkg == null) { - sendCheckUpdateResult(CheckUpdateResult.NOT_AVAILABLE); - return; - } - - Log.i(LOGTAG, "have update package at " + pkg); - - saveUpdateInfo(info, pkg); - sendCheckUpdateResult(CheckUpdateResult.DOWNLOADED); - - if (mApplyImmediately) { - applyUpdate(pkg); - } else { - // Prompt to apply the update - - Intent notificationIntent = new Intent(UpdateServiceHelper.ACTION_APPLY_UPDATE); - notificationIntent.setClass(this, UpdateService.class); - notificationIntent.putExtra(UpdateServiceHelper.EXTRA_PACKAGE_PATH_NAME, pkg.getAbsolutePath()); - PendingIntent contentIntent = PendingIntent.getService(this, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT); - - - NotificationCompat.Builder builder = new NotificationCompat.Builder(this); - builder.setSmallIcon(R.drawable.ic_status_logo); - builder.setWhen(System.currentTimeMillis()); - builder.setAutoCancel(true); - builder.setContentTitle(getString(R.string.updater_apply_title)); - builder.setContentText(getString(R.string.updater_apply_select)); - builder.setContentIntent(contentIntent); - - mNotificationManager.notify(NOTIFICATION_ID, builder.build()); - } - } - - private UpdateInfo findUpdate(boolean force) { - try { - URI uri = getUpdateURI(force); - - if (uri == null) { - Log.e(LOGTAG, "failed to get update URI"); - return null; - } - - DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); - Document dom = builder.parse(ProxySelector.openConnectionWithProxy(uri).getInputStream()); - - NodeList nodes = dom.getElementsByTagName("update"); - if (nodes == null || nodes.getLength() == 0) - return null; - - Node updateNode = nodes.item(0); - Node buildIdNode = updateNode.getAttributes().getNamedItem("buildID"); - if (buildIdNode == null) - return null; - - nodes = dom.getElementsByTagName("patch"); - if (nodes == null || nodes.getLength() == 0) - return null; - - Node patchNode = nodes.item(0); - Node urlNode = patchNode.getAttributes().getNamedItem("URL"); - Node hashFunctionNode = patchNode.getAttributes().getNamedItem("hashFunction"); - Node hashValueNode = patchNode.getAttributes().getNamedItem("hashValue"); - Node sizeNode = patchNode.getAttributes().getNamedItem("size"); - - if (urlNode == null || hashFunctionNode == null || - hashValueNode == null || sizeNode == null) { - return null; - } - - // Fill in UpdateInfo from the XML data - UpdateInfo info = new UpdateInfo(); - info.uri = new URI(urlNode.getTextContent()); - info.buildID = buildIdNode.getTextContent(); - info.hashFunction = hashFunctionNode.getTextContent(); - info.hashValue = hashValueNode.getTextContent(); - - try { - info.size = Integer.parseInt(sizeNode.getTextContent()); - } catch (NumberFormatException e) { - Log.e(LOGTAG, "Failed to find APK size: ", e); - return null; - } - - // Make sure we have all the stuff we need to apply the update - if (!info.isValid()) { - Log.e(LOGTAG, "missing some required update information, have: " + info); - return null; - } - - return info; - } catch (Exception e) { - Log.e(LOGTAG, "failed to check for update: ", e); - return null; - } - } - - private MessageDigest createMessageDigest(String hashFunction) { - String javaHashFunction = null; - - if ("sha512".equalsIgnoreCase(hashFunction)) { - javaHashFunction = "SHA-512"; - } else { - Log.e(LOGTAG, "Unhandled hash function: " + hashFunction); - return null; - } - - try { - return MessageDigest.getInstance(javaHashFunction); - } catch (java.security.NoSuchAlgorithmException e) { - Log.e(LOGTAG, "Couldn't find algorithm " + javaHashFunction, e); - return null; - } - } - - private void showDownloadNotification() { - showDownloadNotification(null); - } - - private void showDownloadNotification(File downloadFile) { - - Intent notificationIntent = new Intent(UpdateServiceHelper.ACTION_APPLY_UPDATE); - notificationIntent.setClass(this, UpdateService.class); - - Intent cancelIntent = new Intent(UpdateServiceHelper.ACTION_CANCEL_DOWNLOAD); - cancelIntent.setClass(this, UpdateService.class); - - if (downloadFile != null) - notificationIntent.putExtra(UpdateServiceHelper.EXTRA_PACKAGE_PATH_NAME, downloadFile.getAbsolutePath()); - - PendingIntent contentIntent = PendingIntent.getService(this, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT); - PendingIntent deleteIntent = PendingIntent.getService(this, 0, cancelIntent, PendingIntent.FLAG_CANCEL_CURRENT); - - mBuilder = new NotificationCompat.Builder(this); - mBuilder.setContentTitle(getResources().getString(R.string.updater_downloading_title)) - .setContentText(mApplyImmediately ? "" : getResources().getString(R.string.updater_downloading_select)) - .setSmallIcon(android.R.drawable.stat_sys_download) - .setContentIntent(contentIntent) - .setDeleteIntent(deleteIntent); - - mBuilder.setProgress(100, 0, true); - mNotificationManager.notify(NOTIFICATION_ID, mBuilder.build()); - } - - private void showDownloadFailure() { - Intent notificationIntent = new Intent(UpdateServiceHelper.ACTION_CHECK_FOR_UPDATE); - notificationIntent.setClass(this, UpdateService.class); - PendingIntent contentIntent = PendingIntent.getService(this, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT); - - NotificationCompat.Builder builder = new NotificationCompat.Builder(this); - builder.setSmallIcon(R.drawable.ic_status_logo); - builder.setWhen(System.currentTimeMillis()); - builder.setContentTitle(getString(R.string.updater_downloading_title_failed)); - builder.setContentText(getString(R.string.updater_downloading_retry)); - builder.setContentIntent(contentIntent); - - mNotificationManager.notify(NOTIFICATION_ID, builder.build()); - } - - private boolean deleteUpdatePackage(String path) { - if (path == null) { - return false; - } - - File pkg = new File(path); - if (!pkg.exists()) { - return false; - } - - pkg.delete(); - Log.i(LOGTAG, "deleted update package: " + path); - - return true; - } - - private File downloadUpdatePackage(UpdateInfo info, boolean overwriteExisting) { - URL url = null; - try { - url = info.uri.toURL(); - } catch (java.net.MalformedURLException e) { - Log.e(LOGTAG, "failed to read URL: ", e); - return null; - } - - File path = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS); - path.mkdirs(); - String fileName = new File(url.getFile()).getName(); - File downloadFile = new File(path, fileName); - - if (!overwriteExisting && info.buildID.equals(getLastBuildID()) && downloadFile.exists()) { - // The last saved buildID is the same as the one for the current update. We also have a file - // already downloaded, so it's probably the package we want. Verify it to be sure and just - // return that if it matches. - - if (verifyDownloadedPackage(downloadFile)) { - Log.i(LOGTAG, "using existing update package"); - return downloadFile; - } else { - // Didn't match, so we're going to download a new one. - downloadFile.delete(); - } - } - - if (!info.buildID.equals(getLastBuildID())) { - // Delete the previous package when a new version becomes available. - deleteUpdatePackage(getLastFileName()); - } - - Log.i(LOGTAG, "downloading update package"); - sendCheckUpdateResult(CheckUpdateResult.DOWNLOADING); - - OutputStream output = null; - InputStream input = null; - - mDownloading = true; - mCancelDownload = false; - showDownloadNotification(downloadFile); - - try { - NetworkInfo netInfo = mConnectivityManager.getActiveNetworkInfo(); - if (netInfo != null && netInfo.isConnected() && - netInfo.getType() == ConnectivityManager.TYPE_WIFI) { - mWifiLock.acquire(); - } - - URLConnection conn = ProxySelector.openConnectionWithProxy(info.uri); - int length = conn.getContentLength(); - - output = new BufferedOutputStream(new FileOutputStream(downloadFile)); - input = new BufferedInputStream(conn.getInputStream()); - - byte[] buf = new byte[BUFSIZE]; - int len = 0; - - int bytesRead = 0; - int lastNotify = 0; - - while ((len = input.read(buf, 0, BUFSIZE)) > 0 && !mCancelDownload) { - output.write(buf, 0, len); - bytesRead += len; - // Updating the notification takes time so only do it every 1MB - if (bytesRead - lastNotify > 1048576) { - mBuilder.setProgress(length, bytesRead, false); - mNotificationManager.notify(NOTIFICATION_ID, mBuilder.build()); - lastNotify = bytesRead; - } - } - - mNotificationManager.cancel(NOTIFICATION_ID); - - // if the download was canceled by the user - // delete the update package - if (mCancelDownload) { - Log.i(LOGTAG, "download canceled by user!"); - downloadFile.delete(); - - return null; - } else { - Log.i(LOGTAG, "completed update download!"); - return downloadFile; - } - } catch (Exception e) { - downloadFile.delete(); - showDownloadFailure(); - - Log.e(LOGTAG, "failed to download update: ", e); - return null; - } finally { - try { - if (input != null) - input.close(); - } catch (java.io.IOException e) { } - - try { - if (output != null) - output.close(); - } catch (java.io.IOException e) { } - - mDownloading = false; - - if (mWifiLock.isHeld()) { - mWifiLock.release(); - } - } - } - - private boolean verifyDownloadedPackage(File updateFile) { - MessageDigest digest = createMessageDigest(getLastHashFunction()); - if (digest == null) - return false; - - InputStream input = null; - - try { - input = new BufferedInputStream(new FileInputStream(updateFile)); - - byte[] buf = new byte[BUFSIZE]; - int len; - while ((len = input.read(buf, 0, BUFSIZE)) > 0) { - digest.update(buf, 0, len); - } - } catch (java.io.IOException e) { - Log.e(LOGTAG, "Failed to verify update package: ", e); - return false; - } finally { - try { - if (input != null) - input.close(); - } catch (java.io.IOException e) { } - } - - String hex = Hex.encodeHexString(digest.digest()); - if (!hex.equals(getLastHashValue())) { - Log.e(LOGTAG, "Package hash does not match"); - return false; - } - - return true; - } - - private void applyUpdate(String updatePath) { - if (updatePath == null) { - updatePath = getLastFileName(); - } - - if (updatePath != null) { - applyUpdate(new File(updatePath)); - } - } - - private void applyUpdate(File updateFile) { - mApplyImmediately = false; - - if (!updateFile.exists()) - return; - - Log.i(LOGTAG, "Verifying package: " + updateFile); - - if (!verifyDownloadedPackage(updateFile)) { - Log.e(LOGTAG, "Not installing update, failed verification"); - return; - } - - Intent intent = new Intent(Intent.ACTION_VIEW); - intent.setDataAndType(Uri.fromFile(updateFile), "application/vnd.android.package-archive"); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - startActivity(intent); - } - - private void showPermissionNotification() { - Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, - Uri.fromParts("package", getPackageName(), null)); - - PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, 0); - - NotificationCompat.BigTextStyle bigTextStyle = new NotificationCompat.BigTextStyle() - .bigText(getString(R.string.updater_permission_text)); - - Notification notification = new NotificationCompat.Builder(this) - .setContentTitle(getString(R.string.updater_permission_title)) - .setContentText(getString(R.string.updater_permission_text)) - .setStyle(bigTextStyle) - .setAutoCancel(true) - .setSmallIcon(R.drawable.ic_status_logo) - .setColor(ContextCompat.getColor(this, R.color.rejection_red)) - .setContentIntent(pendingIntent) - .build(); - - NotificationManagerCompat.from(this) - .notify(R.id.updateServicePermissionNotification, notification); - } - - private String getLastBuildID() { - return mPrefs.getString(KEY_LAST_BUILDID, null); - } - - private String getLastHashFunction() { - return mPrefs.getString(KEY_LAST_HASH_FUNCTION, null); - } - - private String getLastHashValue() { - return mPrefs.getString(KEY_LAST_HASH_VALUE, null); - } - - private String getLastFileName() { - return mPrefs.getString(KEY_LAST_FILE_NAME, null); - } - - private Calendar getLastAttemptDate() { - long lastAttempt = mPrefs.getLong(KEY_LAST_ATTEMPT_DATE, -1); - if (lastAttempt < 0) - return null; - - GregorianCalendar cal = new GregorianCalendar(TimeZone.getTimeZone("GMT")); - cal.setTimeInMillis(lastAttempt); - return cal; - } - - private void setLastAttemptDate() { - SharedPreferences.Editor editor = mPrefs.edit(); - editor.putLong(KEY_LAST_ATTEMPT_DATE, System.currentTimeMillis()); - editor.commit(); - } - - private AutoDownloadPolicy getAutoDownloadPolicy() { - return AutoDownloadPolicy.get(mPrefs.getInt(KEY_AUTODOWNLOAD_POLICY, AutoDownloadPolicy.WIFI.value)); - } - - private void setAutoDownloadPolicy(AutoDownloadPolicy policy) { - SharedPreferences.Editor editor = mPrefs.edit(); - editor.putInt(KEY_AUTODOWNLOAD_POLICY, policy.value); - editor.commit(); - } - - private URI getUpdateURI(boolean force) { - return UpdateServiceHelper.expandUpdateURI(this, mPrefs.getString(KEY_UPDATE_URL, null), force); - } - - private void setUpdateUrl(String url) { - SharedPreferences.Editor editor = mPrefs.edit(); - editor.putString(KEY_UPDATE_URL, url); - editor.commit(); - } - - private void saveUpdateInfo(UpdateInfo info, File downloaded) { - SharedPreferences.Editor editor = mPrefs.edit(); - editor.putString(KEY_LAST_BUILDID, info.buildID); - editor.putString(KEY_LAST_HASH_FUNCTION, info.hashFunction); - editor.putString(KEY_LAST_HASH_VALUE, info.hashValue); - editor.putString(KEY_LAST_FILE_NAME, downloaded.toString()); - editor.commit(); - } - - private class UpdateInfo { - public URI uri; - public String buildID; - public String hashFunction; - public String hashValue; - public int size; - - private boolean isNonEmpty(String s) { - return s != null && s.length() > 0; - } - - public boolean isValid() { - return uri != null && isNonEmpty(buildID) && - isNonEmpty(hashFunction) && isNonEmpty(hashValue) && size > 0; - } - - @Override - public String toString() { - return "uri = " + uri + ", buildID = " + buildID + ", hashFunction = " + hashFunction + ", hashValue = " + hashValue + ", size = " + size; - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/updater/UpdateServiceHelper.java b/mobile/android/base/java/org/mozilla/gecko/updater/UpdateServiceHelper.java deleted file mode 100644 index c4d198ae73..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/updater/UpdateServiceHelper.java +++ /dev/null @@ -1,213 +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.updater; - -import org.mozilla.gecko.annotation.RobocopTarget; -import org.mozilla.gecko.AppConstants; -import org.mozilla.gecko.PrefsHelper; -import org.mozilla.gecko.util.ContextUtils; -import org.mozilla.gecko.util.GeckoJarReader; - -import android.content.Context; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.content.pm.ApplicationInfo; -import android.os.Build; -import android.util.Log; - -import java.net.URI; -import java.util.ArrayList; -import java.util.HashMap; - -public class UpdateServiceHelper { - public static final String ACTION_REGISTER_FOR_UPDATES = AppConstants.ANDROID_PACKAGE_NAME + ".REGISTER_FOR_UPDATES"; - public static final String ACTION_UNREGISTER_FOR_UPDATES = AppConstants.ANDROID_PACKAGE_NAME + ".UNREGISTER_FOR_UPDATES"; - public static final String ACTION_CHECK_FOR_UPDATE = AppConstants.ANDROID_PACKAGE_NAME + ".CHECK_FOR_UPDATE"; - public static final String ACTION_CHECK_UPDATE_RESULT = AppConstants.ANDROID_PACKAGE_NAME + ".CHECK_UPDATE_RESULT"; - public static final String ACTION_DOWNLOAD_UPDATE = AppConstants.ANDROID_PACKAGE_NAME + ".DOWNLOAD_UPDATE"; - public static final String ACTION_APPLY_UPDATE = AppConstants.ANDROID_PACKAGE_NAME + ".APPLY_UPDATE"; - public static final String ACTION_CANCEL_DOWNLOAD = AppConstants.ANDROID_PACKAGE_NAME + ".CANCEL_DOWNLOAD"; - - // Flags for ACTION_CHECK_FOR_UPDATE - protected static final int FLAG_FORCE_DOWNLOAD = 1; - protected static final int FLAG_OVERWRITE_EXISTING = 1 << 1; - protected static final int FLAG_REINSTALL = 1 << 2; - protected static final int FLAG_RETRY = 1 << 3; - - // Name of the Intent extra for the autodownload policy, used with ACTION_REGISTER_FOR_UPDATES - protected static final String EXTRA_AUTODOWNLOAD_NAME = "autodownload"; - - // Name of the Intent extra that holds the flags for ACTION_CHECK_FOR_UPDATE - protected static final String EXTRA_UPDATE_FLAGS_NAME = "updateFlags"; - - // Name of the Intent extra that holds the APK path, used with ACTION_APPLY_UPDATE - protected static final String EXTRA_PACKAGE_PATH_NAME = "packagePath"; - - // Name of the Intent extra for the update URL, used with ACTION_REGISTER_FOR_UPDATES - protected static final String EXTRA_UPDATE_URL_NAME = "updateUrl"; - - private static final String LOGTAG = "UpdateServiceHelper"; - private static final String DEFAULT_UPDATE_LOCALE = "en-US"; - - // So that updates can be disabled by tests. - private static volatile boolean isEnabled = true; - - private enum Pref { - AUTO_DOWNLOAD_POLICY("app.update.autodownload"), - UPDATE_URL("app.update.url.android"); - - public final String name; - - private Pref(String name) { - this.name = name; - } - - public final static String[] names; - - @Override - public String toString() { - return this.name; - } - - static { - ArrayList nameList = new ArrayList(); - - for (Pref id: Pref.values()) { - nameList.add(id.toString()); - } - - names = nameList.toArray(new String[0]); - } - } - - @RobocopTarget - public static void setEnabled(final boolean enabled) { - isEnabled = enabled; - } - - public static URI expandUpdateURI(Context context, String updateUri, boolean force) { - if (updateUri == null) { - return null; - } - - PackageManager pm = context.getPackageManager(); - - String pkgSpecial = AppConstants.MOZ_PKG_SPECIAL != null ? - "-" + AppConstants.MOZ_PKG_SPECIAL : - ""; - String locale = DEFAULT_UPDATE_LOCALE; - - try { - ApplicationInfo info = pm.getApplicationInfo(AppConstants.ANDROID_PACKAGE_NAME, 0); - String updateLocaleUrl = "jar:jar:file://" + info.sourceDir + "!/" + AppConstants.OMNIJAR_NAME + "!/update.locale"; - - final String jarLocale = GeckoJarReader.getText(context, updateLocaleUrl); - if (jarLocale != null) { - locale = jarLocale.trim(); - } - } catch (android.content.pm.PackageManager.NameNotFoundException e) { - // Shouldn't really be possible, but fallback to default locale - Log.i(LOGTAG, "Failed to read update locale file, falling back to " + locale); - } - - String url = updateUri.replace("%PRODUCT%", AppConstants.MOZ_APP_BASENAME) - .replace("%VERSION%", AppConstants.MOZ_APP_VERSION) - .replace("%BUILD_ID%", force ? "0" : AppConstants.MOZ_APP_BUILDID) - .replace("%BUILD_TARGET%", "Android_" + AppConstants.MOZ_APP_ABI + pkgSpecial) - .replace("%LOCALE%", locale) - .replace("%CHANNEL%", AppConstants.MOZ_UPDATE_CHANNEL) - .replace("%OS_VERSION%", Build.VERSION.RELEASE) - .replace("%DISTRIBUTION%", "default") - .replace("%DISTRIBUTION_VERSION%", "default") - .replace("%MOZ_VERSION%", AppConstants.MOZILLA_VERSION); - - try { - return new URI(url); - } catch (java.net.URISyntaxException e) { - Log.e(LOGTAG, "Failed to create update url: ", e); - return null; - } - } - - public static boolean isUpdaterEnabled(final Context context) { - return AppConstants.MOZ_UPDATER && isEnabled && !ContextUtils.isInstalledFromGooglePlay(context); - } - - public static void setUpdateUrl(Context context, String url) { - registerForUpdates(context, null, url); - } - - public static void setAutoDownloadPolicy(Context context, UpdateService.AutoDownloadPolicy policy) { - registerForUpdates(context, policy, null); - } - - public static void checkForUpdate(Context context) { - if (context == null) { - return; - } - - context.startService(createIntent(context, ACTION_CHECK_FOR_UPDATE)); - } - - public static void downloadUpdate(Context context) { - if (context == null) { - return; - } - - context.startService(createIntent(context, ACTION_DOWNLOAD_UPDATE)); - } - - public static void applyUpdate(Context context) { - if (context == null) { - return; - } - - context.startService(createIntent(context, ACTION_APPLY_UPDATE)); - } - - public static void registerForUpdates(final Context context) { - if (!isUpdaterEnabled(context)) { - return; - } - - final HashMap prefs = new HashMap(); - - PrefsHelper.getPrefs(Pref.names, new PrefsHelper.PrefHandlerBase() { - @Override public void prefValue(String pref, String value) { - prefs.put(pref, value); - } - - @Override public void finish() { - UpdateServiceHelper.registerForUpdates(context, - UpdateService.AutoDownloadPolicy.get( - (String) prefs.get(Pref.AUTO_DOWNLOAD_POLICY.toString())), - (String) prefs.get(Pref.UPDATE_URL.toString())); - } - }); - } - - public static void registerForUpdates(Context context, UpdateService.AutoDownloadPolicy policy, String url) { - if (!isUpdaterEnabled(context)) { - return; - } - - Intent intent = createIntent(context, ACTION_REGISTER_FOR_UPDATES); - - if (policy != null) { - intent.putExtra(EXTRA_AUTODOWNLOAD_NAME, policy.value); - } - - if (url != null) { - intent.putExtra(EXTRA_UPDATE_URL_NAME, url); - } - - context.startService(intent); - } - - private static Intent createIntent(Context context, String action) { - return new Intent(action, null, context, UpdateService.class); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/util/ColorUtil.java b/mobile/android/base/java/org/mozilla/gecko/util/ColorUtil.java deleted file mode 100644 index ec227d1ce6..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/util/ColorUtil.java +++ /dev/null @@ -1,44 +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.util; - -import android.graphics.Color; - -public class ColorUtil { - public static int darken(final int color, final double fraction) { - int red = Color.red(color); - int green = Color.green(color); - int blue = Color.blue(color); - red = darkenColor(red, fraction); - green = darkenColor(green, fraction); - blue = darkenColor(blue, fraction); - final int alpha = Color.alpha(color); - return Color.argb(alpha, red, green, blue); - } - - public static int getReadableTextColor(final int backgroundColor) { - final int greyValue = grayscaleFromRGB(backgroundColor); - // 186 chosen rather than the seemingly obvious 128 because of gamma. - if (greyValue < 186) { - return Color.WHITE; - } else { - return Color.BLACK; - } - } - - private static int darkenColor(final int color, final double fraction) { - return (int) Math.max(color - (color * fraction), 0); - } - - private static int grayscaleFromRGB(final int color) { - final int red = Color.red(color); - final int green = Color.green(color); - final int blue = Color.blue(color); - // Magic weighting taken from a stackoverflow post, supposedly related to how - // humans perceive color. - return (int) (0.299 * red + 0.587 * green + 0.114 * blue); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/util/DrawableUtil.java b/mobile/android/base/java/org/mozilla/gecko/util/DrawableUtil.java deleted file mode 100644 index f3c9eef837..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/util/DrawableUtil.java +++ /dev/null @@ -1,66 +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.util; - -import android.content.Context; -import android.content.res.ColorStateList; -import android.graphics.drawable.Drawable; -import android.support.annotation.CheckResult; -import android.support.annotation.ColorInt; -import android.support.annotation.ColorRes; -import android.support.annotation.DrawableRes; -import android.support.annotation.NonNull; -import android.support.v4.content.ContextCompat; -import android.support.v4.graphics.drawable.DrawableCompat; - -import org.mozilla.gecko.AppConstants; - -public class DrawableUtil { - - /** - * Tints the given drawable with the given color and returns it. - */ - @CheckResult - public static Drawable tintDrawable(@NonNull final Context context, - @DrawableRes final int drawableID, - @ColorInt final int color) { - final Drawable icon = DrawableCompat.wrap(ResourceDrawableUtils.getDrawable(context, drawableID) - .mutate()); - DrawableCompat.setTint(icon, color); - return icon; - } - - /** - * Tints the given drawable with the given color and returns it. - */ - @CheckResult - public static Drawable tintDrawableWithColorRes(@NonNull final Context context, - @DrawableRes final int drawableID, - @ColorRes final int colorID) { - return tintDrawable(context, drawableID, ContextCompat.getColor(context, colorID)); - } - - /** - * Tints the given drawable with the given tint list and returns it. Note that you - * should no longer use the argument Drawable because the argument is not mutated - * on pre-Lollipop devices but is mutated on L+ due to differences in the Support - * Library implementation (bug 1193950). - */ - @CheckResult - public static Drawable tintDrawableWithStateList(@NonNull final Drawable drawable, - @NonNull final ColorStateList colorList) { - final Drawable wrappedDrawable = DrawableCompat.wrap(drawable.mutate()); - DrawableCompat.setTintList(wrappedDrawable, colorList); - - // DrawableCompat on pre-L doesn't handle its bounds correctly, and by default therefore won't - // be rendered - we need to manually copy the bounds as a workaround: - if (AppConstants.Versions.preMarshmallow) { - wrappedDrawable.setBounds(0, 0, wrappedDrawable.getIntrinsicHeight(), wrappedDrawable.getIntrinsicHeight()); - } - - return wrappedDrawable; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/util/ResourceDrawableUtils.java b/mobile/android/base/java/org/mozilla/gecko/util/ResourceDrawableUtils.java deleted file mode 100644 index 1e5c2a723c..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/util/ResourceDrawableUtils.java +++ /dev/null @@ -1,136 +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.util; - -import android.content.Context; -import android.graphics.drawable.BitmapDrawable; -import android.graphics.drawable.Drawable; -import android.net.Uri; -import android.support.annotation.DrawableRes; -import android.support.annotation.NonNull; -import android.support.v7.widget.AppCompatDrawableManager; -import android.text.TextUtils; -import android.util.Log; - -import org.mozilla.gecko.util.GeckoJarReader; -import org.mozilla.gecko.util.ThreadUtils; -import org.mozilla.gecko.util.UIAsyncTask; - -import java.io.InputStream; -import java.net.URL; - -import static org.mozilla.gecko.gfx.BitmapUtils.getBitmapFromDataURI; -import static org.mozilla.gecko.gfx.BitmapUtils.getResource; - -public class ResourceDrawableUtils { - private static final String LOGTAG = "ResourceDrawableUtils"; - - public static Drawable getDrawable(@NonNull final Context context, - @DrawableRes final int drawableID) { - // TODO: upgrade this call to use AppCompatResources when upgrading to support library >= 24.2 - // https://developer.android.com/reference/android/support/v7/content/res/AppCompatResources.html#getDrawable(android.content.Context,%20int) - return AppCompatDrawableManager.get().getDrawable(context, drawableID); - } - - public interface BitmapLoader { - public void onBitmapFound(Drawable d); - } - - public static void runOnBitmapFoundOnUiThread(final BitmapLoader loader, final Drawable d) { - if (ThreadUtils.isOnUiThread()) { - loader.onBitmapFound(d); - return; - } - - ThreadUtils.postToUiThread(new Runnable() { - @Override - public void run() { - loader.onBitmapFound(d); - } - }); - } - - /** - * Attempts to find a drawable associated with a given string, using its URI scheme to determine - * how to load the drawable. The BitmapLoader's `onBitmapFound` method is always called, and - * will be called with `null` if no drawable is found. - * - * The BitmapLoader `onBitmapFound` method always runs on the UI thread. - */ - public static void getDrawable(final Context context, final String data, final BitmapLoader loader) { - if (TextUtils.isEmpty(data)) { - runOnBitmapFoundOnUiThread(loader, null); - return; - } - - if (data.startsWith("data")) { - final BitmapDrawable d = new BitmapDrawable(context.getResources(), getBitmapFromDataURI(data)); - runOnBitmapFoundOnUiThread(loader, d); - return; - } - - if (data.startsWith("jar:") || data.startsWith("file://")) { - (new UIAsyncTask.WithoutParams(ThreadUtils.getBackgroundHandler()) { - @Override - public Drawable doInBackground() { - try { - if (data.startsWith("jar:jar")) { - return GeckoJarReader.getBitmapDrawable( - context, context.getResources(), data); - } - - // Don't attempt to validate the JAR signature when loading an add-on icon - if (data.startsWith("jar:file")) { - return GeckoJarReader.getBitmapDrawable( - context, context.getResources(), Uri.decode(data)); - } - - final URL url = new URL(data); - final InputStream is = (InputStream) url.getContent(); - try { - return Drawable.createFromStream(is, "src"); - } finally { - is.close(); - } - } catch (Exception e) { - Log.w(LOGTAG, "Unable to set icon", e); - } - return null; - } - - @Override - public void onPostExecute(Drawable drawable) { - loader.onBitmapFound(drawable); - } - }).execute(); - return; - } - - if (data.startsWith("-moz-icon://")) { - final Uri imageUri = Uri.parse(data); - final String ssp = imageUri.getSchemeSpecificPart(); - final String resource = ssp.substring(ssp.lastIndexOf('/') + 1); - - try { - final Drawable d = context.getPackageManager().getApplicationIcon(resource); - runOnBitmapFoundOnUiThread(loader, d); - } catch (Exception ex) { } - - return; - } - - if (data.startsWith("drawable://")) { - final Uri imageUri = Uri.parse(data); - final int id = getResource(context, imageUri); - final Drawable d = getDrawable(context, id); - - runOnBitmapFoundOnUiThread(loader, d); - return; - } - - runOnBitmapFoundOnUiThread(loader, null); - } - -} diff --git a/mobile/android/base/java/org/mozilla/gecko/util/TouchTargetUtil.java b/mobile/android/base/java/org/mozilla/gecko/util/TouchTargetUtil.java deleted file mode 100644 index 6414dec9fb..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/util/TouchTargetUtil.java +++ /dev/null @@ -1,48 +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.util; - -import android.graphics.Rect; -import android.view.TouchDelegate; -import android.view.View; - -import org.mozilla.gecko.R; - -public class TouchTargetUtil { - /** - * Ensures that a given targetView has a large enough touch area to ensure it can be selected. - * A TouchDelegate will be added to the enclosingView as necessary. - * - * @param targetView - * @param enclosingView - */ - public static void ensureTargetHitArea(final View targetView, final View enclosingView) { - enclosingView.post(new Runnable() { - @Override - public void run() { - Rect delegateArea = new Rect(); - targetView.getHitRect(delegateArea); - - final int targetHitArea = enclosingView.getContext().getResources().getDimensionPixelSize(R.dimen.touch_target_size); - - final int widthDelta = (targetHitArea - delegateArea.width()) / 2; - delegateArea.right += widthDelta; - delegateArea.left -= widthDelta; - - final int heightDelta = (targetHitArea - delegateArea.height()) / 2; - delegateArea.bottom += heightDelta; - delegateArea.top -= heightDelta; - - if (heightDelta <= 0 && widthDelta <= 0) { - return; - } - - TouchDelegate touchDelegate = new TouchDelegate(delegateArea, targetView); - enclosingView.setTouchDelegate(touchDelegate); - } - }); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/util/UnusedResourcesUtil.java b/mobile/android/base/java/org/mozilla/gecko/util/UnusedResourcesUtil.java deleted file mode 100644 index 0033e72a0c..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/util/UnusedResourcesUtil.java +++ /dev/null @@ -1,128 +0,0 @@ -package org.mozilla.gecko.util; - -import org.mozilla.gecko.R; - -/** - * (linter: UnusedResources) We use resources in places Android Lint can't check (e.g. JS) - this is - * a set of those references so Android Lint stops complaining. - */ -@SuppressWarnings("unused") -final class UnusedResourcesUtil { - public static final int[] CONSTANTS = { - R.dimen.match_parent, - R.dimen.wrap_content, - }; - - public static final int[] USED_IN_BRANDING = { - R.drawable.large_icon - }; - - public static final int[] USED_IN_COLOR_PALETTE = { - R.color.private_browsing_purple, // This will be used eventually, then this item removed. - }; - - public static final int[] USED_IN_CRASH_REPORTER = { - R.string.crash_allow_contact2, - R.string.crash_close_label, - R.string.crash_comment, - R.string.crash_email, - R.string.crash_include_url2, - R.string.crash_message2, - R.string.crash_restart_label, - R.string.crash_send_report_message3, - R.string.crash_sorry, - }; - - public static final int[] USED_IN_JS = { - R.drawable.ab_search, - R.drawable.alert_camera, - R.drawable.alert_download, - R.drawable.alert_download_animation, - R.drawable.alert_mic, - R.drawable.alert_mic_camera, - R.drawable.casting, - R.drawable.casting_active, - R.drawable.close, - R.drawable.homepage_banner_firstrun, - R.drawable.icon_openinapp, - R.drawable.pause, - R.drawable.phone, - R.drawable.play, - R.drawable.reader, - R.drawable.reader_active, - R.drawable.sync_promo, - R.drawable.undo_button_icon, - }; - - public static final int[] USED_IN_MANIFEST = { - R.drawable.search_launcher, - R.string.crash_reporter_title, - R.xml.fxaccount_authenticator, - R.xml.fxaccount_syncadapter, - R.xml.search_widget_info, - R.xml.searchable, - }; - - public static final int[] USED_IN_SUGGESTEDSITES = { - R.drawable.suggestedsites_amazon, - R.drawable.suggestedsites_facebook, - R.drawable.suggestedsites_restricted_fxsupport, - R.drawable.suggestedsites_restricted_mozilla, - R.drawable.suggestedsites_twitter, - R.drawable.suggestedsites_webmaker, - R.drawable.suggestedsites_wikipedia, - R.drawable.suggestedsites_youtube, - }; - - public static final int[] USED_IN_BOOKMARKDEFAULTS = { - R.raw.bookmarkdefaults_favicon_addons, - R.raw.bookmarkdefaults_favicon_support, - R.raw.bookmarkdefaults_favicon_restricted_support, - R.raw.bookmarkdefaults_favicon_restricted_webmaker, - R.string.bookmarkdefaults_title_restricted_support, - R.string.bookmarkdefaults_url_restricted_support, - R.string.bookmarkdefaults_title_restricted_webmaker, - R.string.bookmarkdefaults_url_restricted_webmaker, - }; - - public static final int[] USED_IN_PREFS = { - R.xml.preferences_advanced, - R.xml.preferences_accessibility, - R.xml.preferences_home, - R.xml.preferences_privacy, - R.xml.preferences_privacy_clear_tablet, - R.xml.preferences_default_browser_tablet - }; - - // We are migrating to Gradle 2.10 and the Android Gradle plugin 2.0. The new plugin does find - // more unused resources but we are not ready to remove them yet. Some of the resources are going - // to be reused soon. This is a temporary solution so that the gradle migration is not blocked. - // See bug 1263390 / bug 1268414. - public static final int[] TEMPORARY_UNUSED_WHILE_MIGRATING_GRADLE = { - R.color.remote_tabs_setup_button_background_hit, - - R.drawable.remote_tabs_setup_button_background, - - R.style.TabsPanelSectionBase, - R.style.TabsPanelSection, - R.style.TabsPanelItemBase, - R.style.TabsPanelItem, - R.style.TabsPanelItem_TextAppearance, - R.style.TabsPanelItem_TextAppearance_Header, - R.style.TabsPanelItem_TextAppearance_Linkified, - R.style.TabWidget, - R.style.GeckoDialogTitle, - R.style.GeckoDialogTitle_SubTitle, - R.style.RemoteTabsPanelItem, - R.style.RemoteTabsPanelItem_TextAppearance, - R.style.RemoteTabsPanelItem_TextAppearance_Header, - R.style.RemoteTabsPanelItem_TextAppearance_Linkified, - R.style.RemoteTabsPanelItem_Button, - }; - - // String resources that are used in the full-pane Activity Stream that are temporarily - // not needed while Activity Stream is part of the HomePager - public static final int[] TEMPORARY_UNUSED_ACTIVITY_STREAM = { - R.string.activity_stream_topsites - }; -} diff --git a/mobile/android/base/java/org/mozilla/gecko/util/ViewUtil.java b/mobile/android/base/java/org/mozilla/gecko/util/ViewUtil.java deleted file mode 100644 index 180e821e7c..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/util/ViewUtil.java +++ /dev/null @@ -1,33 +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.util; - -import android.content.res.TypedArray; -import android.view.View; - -import org.mozilla.gecko.AppConstants; -import org.mozilla.gecko.R; - -public class ViewUtil { - - /** - * Enable a circular touch ripple for a given view. This is intended for borderless views, - * such as (3-dot) menu buttons. - * - * Because of platform limitations a square ripple is used on Android 4. - */ - public static void enableTouchRipple(View view) { - final TypedArray backgroundDrawableArray; - if (AppConstants.Versions.feature21Plus) { - backgroundDrawableArray = view.getContext().obtainStyledAttributes(new int[] { R.attr.selectableItemBackgroundBorderless }); - } else { - backgroundDrawableArray = view.getContext().obtainStyledAttributes(new int[] { R.attr.selectableItemBackground }); - } - - // This call is deprecated, but the replacement setBackground(Drawable) isn't available - // until API 16. - view.setBackgroundDrawable(backgroundDrawableArray.getDrawable(0)); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/widget/ActivityChooserModel.java b/mobile/android/base/java/org/mozilla/gecko/widget/ActivityChooserModel.java deleted file mode 100644 index 8cde1ee05f..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/widget/ActivityChooserModel.java +++ /dev/null @@ -1,1359 +0,0 @@ -/* - * Copyright (C) 2011 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * Mozilla: Changing the package. - */ -//package android.widget; -package org.mozilla.gecko.widget; - -// Mozilla: New import -import android.accounts.Account; -import android.content.pm.PackageManager; - -import org.mozilla.gecko.db.BrowserDB; -import org.mozilla.gecko.db.TabsAccessor; -import org.mozilla.gecko.distribution.Distribution; -import org.mozilla.gecko.GeckoProfile; -import org.mozilla.gecko.fxa.FirefoxAccounts; -import org.mozilla.gecko.fxa.SyncStatusListener; -import org.mozilla.gecko.overlays.ui.ShareDialog; -import org.mozilla.gecko.R; -import java.io.File; - -import android.content.BroadcastReceiver; -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.pm.ResolveInfo; -import android.database.Cursor; -import android.database.DataSetObservable; -import android.os.AsyncTask; -import android.text.TextUtils; -import android.util.Log; -import android.util.Xml; - -/** - * Mozilla: Unused import. - */ -//import com.android.internal.content.PackageMonitor; - -import org.xmlpull.v1.XmlPullParser; -import org.xmlpull.v1.XmlPullParserException; -import org.xmlpull.v1.XmlSerializer; - -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.IOException; -import java.math.BigDecimal; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; -import java.util.Map; - -/** - *

- * This class represents a data model for choosing a component for handing a - * given {@link Intent}. The model is responsible for querying the system for - * activities that can handle the given intent and order found activities - * based on historical data of previous choices. The historical data is stored - * in an application private file. If a client does not want to have persistent - * choice history the file can be omitted, thus the activities will be ordered - * based on historical usage for the current session. - *

- *

- * For each backing history file there is a singleton instance of this class. Thus, - * several clients that specify the same history file will share the same model. Note - * that if multiple clients are sharing the same model they should implement semantically - * equivalent functionality since setting the model intent will change the found - * activities and they may be inconsistent with the functionality of some of the clients. - * For example, choosing a share activity can be implemented by a single backing - * model and two different views for performing the selection. If however, one of the - * views is used for sharing but the other for importing, for example, then each - * view should be backed by a separate model. - *

- *

- * The way clients interact with this class is as follows: - *

- *

- *

- * 
- *  // Get a model and set it to a couple of clients with semantically similar function.
- *  ActivityChooserModel dataModel =
- *      ActivityChooserModel.get(context, "task_specific_history_file_name.xml");
- *
- *  ActivityChooserModelClient modelClient1 = getActivityChooserModelClient1();
- *  modelClient1.setActivityChooserModel(dataModel);
- *
- *  ActivityChooserModelClient modelClient2 = getActivityChooserModelClient2();
- *  modelClient2.setActivityChooserModel(dataModel);
- *
- *  // Set an intent to choose a an activity for.
- *  dataModel.setIntent(intent);
- * 
- * 
- * 

- *

- * Note: This class is thread safe. - *

- * - * @hide - */ -public class ActivityChooserModel extends DataSetObservable { - - /** - * Client that utilizes an {@link ActivityChooserModel}. - */ - public interface ActivityChooserModelClient { - - /** - * Sets the {@link ActivityChooserModel}. - * - * @param dataModel The model. - */ - public void setActivityChooserModel(ActivityChooserModel dataModel); - } - - /** - * Defines a sorter that is responsible for sorting the activities - * based on the provided historical choices and an intent. - */ - public interface ActivitySorter { - - /** - * Sorts the activities in descending order of relevance - * based on previous history and an intent. - * - * @param intent The {@link Intent}. - * @param activities Activities to be sorted. - * @param historicalRecords Historical records. - */ - // This cannot be done by a simple comparator since an Activity weight - // is computed from history. Note that Activity implements Comparable. - public void sort(Intent intent, List activities, - List historicalRecords); - } - - /** - * Listener for choosing an activity. - */ - public interface OnChooseActivityListener { - - /** - * Called when an activity has been chosen. The client can decide whether - * an activity can be chosen and if so the caller of - * {@link ActivityChooserModel#chooseActivity(int)} will receive and {@link Intent} - * for launching it. - *

- * Note: Modifying the intent is not permitted and - * any changes to the latter will be ignored. - *

- * - * @param host The listener's host model. - * @param intent The intent for launching the chosen activity. - * @return Whether the intent is handled and should not be delivered to clients. - * - * @see ActivityChooserModel#chooseActivity(int) - */ - public boolean onChooseActivity(ActivityChooserModel host, Intent intent); - } - - /** - * Flag for selecting debug mode. - */ - private static final boolean DEBUG = false; - - /** - * Tag used for logging. - */ - static final String LOG_TAG = ActivityChooserModel.class.getSimpleName(); - - /** - * The root tag in the history file. - */ - private static final String TAG_HISTORICAL_RECORDS = "historical-records"; - - /** - * The tag for a record in the history file. - */ - private static final String TAG_HISTORICAL_RECORD = "historical-record"; - - /** - * Attribute for the activity. - */ - private static final String ATTRIBUTE_ACTIVITY = "activity"; - - /** - * Attribute for the choice time. - */ - private static final String ATTRIBUTE_TIME = "time"; - - /** - * Attribute for the choice weight. - */ - private static final String ATTRIBUTE_WEIGHT = "weight"; - - /** - * The default maximal length of the choice history. - */ - public static final int DEFAULT_HISTORY_MAX_LENGTH = 50; - - /** - * The amount with which to inflate a chosen activity when set as default. - */ - private static final int DEFAULT_ACTIVITY_INFLATION = 5; - - /** - * Default weight for a choice record. - */ - private static final float DEFAULT_HISTORICAL_RECORD_WEIGHT = 1.0f; - - /** - * The extension of the history file. - */ - private static final String HISTORY_FILE_EXTENSION = ".xml"; - - /** - * An invalid item index. - */ - private static final int INVALID_INDEX = -1; - - /** - * Lock to guard the model registry. - */ - private static final Object sRegistryLock = new Object(); - - /** - * This the registry for data models. - */ - private static final Map sDataModelRegistry = - new HashMap(); - - /** - * Lock for synchronizing on this instance. - */ - private final Object mInstanceLock = new Object(); - - /** - * List of activities that can handle the current intent. - */ - private final List mActivities = new ArrayList(); - - /** - * List with historical choice records. - */ - private final List mHistoricalRecords = new ArrayList(); - - /** - * Monitor for added and removed packages. - */ - /** - * Mozilla: Converted from a PackageMonitor to a DataModelPackageMonitor to avoid importing a new class. - */ - private final DataModelPackageMonitor mPackageMonitor = new DataModelPackageMonitor(); - - /** - * Context for accessing resources. - */ - final Context mContext; - - /** - * The name of the history file that backs this model. - */ - final String mHistoryFileName; - - /** - * The intent for which a activity is being chosen. - */ - private Intent mIntent; - - /** - * The sorter for ordering activities based on intent and past choices. - */ - private ActivitySorter mActivitySorter = new DefaultSorter(); - - /** - * The maximal length of the choice history. - */ - private int mHistoryMaxSize = DEFAULT_HISTORY_MAX_LENGTH; - - /** - * Flag whether choice history can be read. In general many clients can - * share the same data model and {@link #readHistoricalDataIfNeeded()} may be called - * by arbitrary of them any number of times. Therefore, this class guarantees - * that the very first read succeeds and subsequent reads can be performed - * only after a call to {@link #persistHistoricalDataIfNeeded()} followed by change - * of the share records. - */ - boolean mCanReadHistoricalData = true; - - /** - * Flag whether the choice history was read. This is used to enforce that - * before calling {@link #persistHistoricalDataIfNeeded()} a call to - * {@link #persistHistoricalDataIfNeeded()} has been made. This aims to avoid a - * scenario in which a choice history file exits, it is not read yet and - * it is overwritten. Note that always all historical records are read in - * full and the file is rewritten. This is necessary since we need to - * purge old records that are outside of the sliding window of past choices. - */ - private boolean mReadShareHistoryCalled; - - /** - * Flag whether the choice records have changed. In general many clients can - * share the same data model and {@link #persistHistoricalDataIfNeeded()} may be called - * by arbitrary of them any number of times. Therefore, this class guarantees - * that choice history will be persisted only if it has changed. - */ - private boolean mHistoricalRecordsChanged = true; - - /** - * Flag whether to reload the activities for the current intent. - */ - boolean mReloadActivities; - - /** - * Policy for controlling how the model handles chosen activities. - */ - private OnChooseActivityListener mActivityChooserModelPolicy; - - /** - * Mozilla: Share overlay variables. - */ - private final SyncStatusListener mSyncStatusListener = new SyncStatusDelegate(); - - /** - * Gets the data model backed by the contents of the provided file with historical data. - * Note that only one data model is backed by a given file, thus multiple calls with - * the same file name will return the same model instance. If no such instance is present - * it is created. - * - *

- * Always use difference historical data files for semantically different actions. - * For example, sharing is different from importing. - *

- * - * @param context Context for loading resources. - * @param historyFileName File name with choice history, null - * if the model should not be backed by a file. In this case the activities - * will be ordered only by data from the current session. - * - * @return The model. - */ - public static ActivityChooserModel get(Context context, String historyFileName) { - synchronized (sRegistryLock) { - ActivityChooserModel dataModel = sDataModelRegistry.get(historyFileName); - if (dataModel == null) { - dataModel = new ActivityChooserModel(context, historyFileName); - sDataModelRegistry.put(historyFileName, dataModel); - } - return dataModel; - } - } - - /** - * Creates a new instance. - * - * @param context Context for loading resources. - * @param historyFileName The history XML file. - */ - private ActivityChooserModel(Context context, String historyFileName) { - mContext = context.getApplicationContext(); - if (!TextUtils.isEmpty(historyFileName) - && !historyFileName.endsWith(HISTORY_FILE_EXTENSION)) { - mHistoryFileName = historyFileName + HISTORY_FILE_EXTENSION; - } else { - mHistoryFileName = historyFileName; - } - - /** - * Mozilla: Uses modified receiver - */ - mPackageMonitor.register(mContext); - - /** - * Mozilla: Add Sync Status Listener. - */ - // TODO: We only need to add a sync status listener if the ShareDialog passes the intent filter. - FirefoxAccounts.addSyncStatusListener(mSyncStatusListener); - } - - /** - * Sets an intent for which to choose a activity. - *

- * Note: Clients must set only semantically similar - * intents for each data model. - *

- * - * @param intent The intent. - */ - public void setIntent(Intent intent) { - synchronized (mInstanceLock) { - if (mIntent == intent) { - return; - } - mIntent = intent; - mReloadActivities = true; - ensureConsistentState(); - } - } - - /** - * Gets the intent for which a activity is being chosen. - * - * @return The intent. - */ - public Intent getIntent() { - synchronized (mInstanceLock) { - return mIntent; - } - } - - /** - * Gets the number of activities that can handle the intent. - * - * @return The activity count. - * - * @see #setIntent(Intent) - */ - public int getActivityCount() { - synchronized (mInstanceLock) { - ensureConsistentState(); - return mActivities.size(); - } - } - - /** - * Gets an activity at a given index. - * - * @return The activity. - * - * @see ActivityResolveInfo - * @see #setIntent(Intent) - */ - public ResolveInfo getActivity(int index) { - synchronized (mInstanceLock) { - ensureConsistentState(); - return mActivities.get(index).resolveInfo; - } - } - - /** - * Gets the index of a the given activity. - * - * @param activity The activity index. - * - * @return The index if found, -1 otherwise. - */ - public int getActivityIndex(ResolveInfo activity) { - synchronized (mInstanceLock) { - ensureConsistentState(); - List activities = mActivities; - final int activityCount = activities.size(); - for (int i = 0; i < activityCount; i++) { - ActivityResolveInfo currentActivity = activities.get(i); - if (currentActivity.resolveInfo == activity) { - return i; - } - } - return INVALID_INDEX; - } - } - - /** - * Chooses a activity to handle the current intent. This will result in - * adding a historical record for that action and construct intent with - * its component name set such that it can be immediately started by the - * client. - *

- * Note: By calling this method the client guarantees - * that the returned intent will be started. This intent is returned to - * the client solely to let additional customization before the start. - *

- * - * @return An {@link Intent} for launching the activity or null if the - * policy has consumed the intent or there is not current intent - * set via {@link #setIntent(Intent)}. - * - * @see HistoricalRecord - * @see OnChooseActivityListener - */ - public Intent chooseActivity(int index) { - synchronized (mInstanceLock) { - if (mIntent == null) { - return null; - } - - ensureConsistentState(); - - ActivityResolveInfo chosenActivity = mActivities.get(index); - - ComponentName chosenName = new ComponentName( - chosenActivity.resolveInfo.activityInfo.packageName, - chosenActivity.resolveInfo.activityInfo.name); - - Intent choiceIntent = new Intent(mIntent); - choiceIntent.setComponent(chosenName); - - if (mActivityChooserModelPolicy != null) { - // Do not allow the policy to change the intent. - Intent choiceIntentCopy = new Intent(choiceIntent); - final boolean handled = mActivityChooserModelPolicy.onChooseActivity(this, - choiceIntentCopy); - if (handled) { - return null; - } - } - - HistoricalRecord historicalRecord = new HistoricalRecord(chosenName, - System.currentTimeMillis(), DEFAULT_HISTORICAL_RECORD_WEIGHT); - addHistoricalRecord(historicalRecord); - - return choiceIntent; - } - } - - /** - * Sets the listener for choosing an activity. - * - * @param listener The listener. - */ - public void setOnChooseActivityListener(OnChooseActivityListener listener) { - synchronized (mInstanceLock) { - mActivityChooserModelPolicy = listener; - } - } - - /** - * Gets the default activity, The default activity is defined as the one - * with highest rank i.e. the first one in the list of activities that can - * handle the intent. - * - * @return The default activity, null id not activities. - * - * @see #getActivity(int) - */ - public ResolveInfo getDefaultActivity() { - synchronized (mInstanceLock) { - ensureConsistentState(); - if (!mActivities.isEmpty()) { - return mActivities.get(0).resolveInfo; - } - } - return null; - } - - /** - * Sets the default activity. The default activity is set by adding a - * historical record with weight high enough that this activity will - * become the highest ranked. Such a strategy guarantees that the default - * will eventually change if not used. Also the weight of the record for - * setting a default is inflated with a constant amount to guarantee that - * it will stay as default for awhile. - * - * @param index The index of the activity to set as default. - */ - public void setDefaultActivity(int index) { - synchronized (mInstanceLock) { - ensureConsistentState(); - - ActivityResolveInfo newDefaultActivity = mActivities.get(index); - ActivityResolveInfo oldDefaultActivity = mActivities.get(0); - - final float weight; - if (oldDefaultActivity != null) { - // Add a record with weight enough to boost the chosen at the top. - weight = oldDefaultActivity.weight - newDefaultActivity.weight - + DEFAULT_ACTIVITY_INFLATION; - } else { - weight = DEFAULT_HISTORICAL_RECORD_WEIGHT; - } - - ComponentName defaultName = new ComponentName( - newDefaultActivity.resolveInfo.activityInfo.packageName, - newDefaultActivity.resolveInfo.activityInfo.name); - HistoricalRecord historicalRecord = new HistoricalRecord(defaultName, - System.currentTimeMillis(), weight); - addHistoricalRecord(historicalRecord); - } - } - - /** - * Persists the history data to the backing file if the latter - * was provided. Calling this method before a call to {@link #readHistoricalDataIfNeeded()} - * throws an exception. Calling this method more than one without choosing an - * activity has not effect. - * - * @throws IllegalStateException If this method is called before a call to - * {@link #readHistoricalDataIfNeeded()}. - */ - private void persistHistoricalDataIfNeeded() { - if (!mReadShareHistoryCalled) { - throw new IllegalStateException("No preceding call to #readHistoricalData"); - } - if (!mHistoricalRecordsChanged) { - return; - } - mHistoricalRecordsChanged = false; - if (!TextUtils.isEmpty(mHistoryFileName)) { - /** - * Mozilla: Converted to a normal task.execute call so that this works on < ICS phones. - */ - new PersistHistoryAsyncTask().execute(new ArrayList(mHistoricalRecords), mHistoryFileName); - } - } - - /** - * Sets the sorter for ordering activities based on historical data and an intent. - * - * @param activitySorter The sorter. - * - * @see ActivitySorter - */ - public void setActivitySorter(ActivitySorter activitySorter) { - synchronized (mInstanceLock) { - if (mActivitySorter == activitySorter) { - return; - } - mActivitySorter = activitySorter; - if (sortActivitiesIfNeeded()) { - notifyChanged(); - } - } - } - - /** - * Sets the maximal size of the historical data. Defaults to - * {@link #DEFAULT_HISTORY_MAX_LENGTH} - *

- * Note: Setting this property will immediately - * enforce the specified max history size by dropping enough old - * historical records to enforce the desired size. Thus, any - * records that exceed the history size will be discarded and - * irreversibly lost. - *

- * - * @param historyMaxSize The max history size. - */ - public void setHistoryMaxSize(int historyMaxSize) { - synchronized (mInstanceLock) { - if (mHistoryMaxSize == historyMaxSize) { - return; - } - mHistoryMaxSize = historyMaxSize; - pruneExcessiveHistoricalRecordsIfNeeded(); - if (sortActivitiesIfNeeded()) { - notifyChanged(); - } - } - } - - /** - * Gets the history max size. - * - * @return The history max size. - */ - public int getHistoryMaxSize() { - synchronized (mInstanceLock) { - return mHistoryMaxSize; - } - } - - /** - * Gets the history size. - * - * @return The history size. - */ - public int getHistorySize() { - synchronized (mInstanceLock) { - ensureConsistentState(); - return mHistoricalRecords.size(); - } - } - - public int getDistinctActivityCountInHistory() { - synchronized (mInstanceLock) { - ensureConsistentState(); - final List packages = new ArrayList(); - for (HistoricalRecord record : mHistoricalRecords) { - String activity = record.activity.flattenToString(); - if (!packages.contains(activity)) { - packages.add(activity); - } - } - return packages.size(); - } - } - - @Override - protected void finalize() throws Throwable { - super.finalize(); - - /** - * Mozilla: Not needed for the application. - */ - mPackageMonitor.unregister(); - FirefoxAccounts.removeSyncStatusListener(mSyncStatusListener); - } - - /** - * Ensures the model is in a consistent state which is the - * activities for the current intent have been loaded, the - * most recent history has been read, and the activities - * are sorted. - */ - private void ensureConsistentState() { - boolean stateChanged = loadActivitiesIfNeeded(); - stateChanged |= readHistoricalDataIfNeeded(); - pruneExcessiveHistoricalRecordsIfNeeded(); - if (stateChanged) { - sortActivitiesIfNeeded(); - notifyChanged(); - } - } - - /** - * Sorts the activities if necessary which is if there is a - * sorter, there are some activities to sort, and there is some - * historical data. - * - * @return Whether sorting was performed. - */ - private boolean sortActivitiesIfNeeded() { - if (mActivitySorter != null && mIntent != null - && !mActivities.isEmpty() && !mHistoricalRecords.isEmpty()) { - mActivitySorter.sort(mIntent, mActivities, - Collections.unmodifiableList(mHistoricalRecords)); - return true; - } - return false; - } - - /** - * Loads the activities for the current intent if needed which is - * if they are not already loaded for the current intent. - * - * @return Whether loading was performed. - */ - private boolean loadActivitiesIfNeeded() { - if (mReloadActivities && mIntent != null) { - mReloadActivities = false; - mActivities.clear(); - List resolveInfos = mContext.getPackageManager() - .queryIntentActivities(mIntent, 0); - final int resolveInfoCount = resolveInfos.size(); - - /** - * Mozilla: Temporary variables to prevent performance degradation in the loop. - */ - final PackageManager packageManager = mContext.getPackageManager(); - final String channelToRemoveLabel = mContext.getResources().getString(R.string.overlay_share_label); - final String shareDialogClassName = ShareDialog.class.getCanonicalName(); - - for (int i = 0; i < resolveInfoCount; i++) { - ResolveInfo resolveInfo = resolveInfos.get(i); - - /** - * Mozilla: We want "Add to Firefox" to appear differently inside of Firefox than - * from external applications - override the name and icon here. - * - * Do not display the menu item if there are no devices to share to. - * - * Note: we check both the class name and the label to ensure we only change the - * label of the current channel. - */ - if (shareDialogClassName.equals(resolveInfo.activityInfo.name) && - channelToRemoveLabel.equals(resolveInfo.loadLabel(packageManager))) { - // Don't add the menu item if there are no devices to share to. - if (!hasOtherSyncClients()) { - continue; - } - - resolveInfo.labelRes = R.string.overlay_share_send_other; - resolveInfo.icon = R.drawable.icon_shareplane; - } - - mActivities.add(new ActivityResolveInfo(resolveInfo)); - } - return true; - } - return false; - } - - /** - * Reads the historical data if necessary which is it has - * changed, there is a history file, and there is not persist - * in progress. - * - * @return Whether reading was performed. - */ - private boolean readHistoricalDataIfNeeded() { - if (mCanReadHistoricalData && mHistoricalRecordsChanged && - !TextUtils.isEmpty(mHistoryFileName)) { - mCanReadHistoricalData = false; - mReadShareHistoryCalled = true; - readHistoricalDataImpl(); - return true; - } - return false; - } - - /** - * Adds a historical record. - * - * @param historicalRecord The record to add. - * @return True if the record was added. - */ - private boolean addHistoricalRecord(HistoricalRecord historicalRecord) { - final boolean added = mHistoricalRecords.add(historicalRecord); - if (added) { - mHistoricalRecordsChanged = true; - pruneExcessiveHistoricalRecordsIfNeeded(); - persistHistoricalDataIfNeeded(); - sortActivitiesIfNeeded(); - notifyChanged(); - } - return added; - } - - /** - * Removes all historical records for this pkg. - * - * @param historicalRecord The pkg to delete records for. - * @return True if the record was added. - */ - boolean removeHistoricalRecordsForPackage(final String pkg) { - boolean removed = false; - - for (Iterator i = mHistoricalRecords.iterator(); i.hasNext();) { - final HistoricalRecord record = i.next(); - if (record.activity.getPackageName().equals(pkg)) { - i.remove(); - removed = true; - } - } - - if (removed) { - mHistoricalRecordsChanged = true; - pruneExcessiveHistoricalRecordsIfNeeded(); - persistHistoricalDataIfNeeded(); - sortActivitiesIfNeeded(); - notifyChanged(); - } - - return removed; - } - - /** - * Prunes older excessive records to guarantee maxHistorySize. - */ - private void pruneExcessiveHistoricalRecordsIfNeeded() { - final int pruneCount = mHistoricalRecords.size() - mHistoryMaxSize; - if (pruneCount <= 0) { - return; - } - mHistoricalRecordsChanged = true; - for (int i = 0; i < pruneCount; i++) { - HistoricalRecord prunedRecord = mHistoricalRecords.remove(0); - if (DEBUG) { - Log.i(LOG_TAG, "Pruned: " + prunedRecord); - } - } - } - - /** - * Represents a record in the history. - */ - public final static class HistoricalRecord { - - /** - * The activity name. - */ - public final ComponentName activity; - - /** - * The choice time. - */ - public final long time; - - /** - * The record weight. - */ - public final float weight; - - /** - * Creates a new instance. - * - * @param activityName The activity component name flattened to string. - * @param time The time the activity was chosen. - * @param weight The weight of the record. - */ - public HistoricalRecord(String activityName, long time, float weight) { - this(ComponentName.unflattenFromString(activityName), time, weight); - } - - /** - * Creates a new instance. - * - * @param activityName The activity name. - * @param time The time the activity was chosen. - * @param weight The weight of the record. - */ - public HistoricalRecord(ComponentName activityName, long time, float weight) { - this.activity = activityName; - this.time = time; - this.weight = weight; - } - - @Override - public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + ((activity == null) ? 0 : activity.hashCode()); - result = prime * result + (int) (time ^ (time >>> 32)); - result = prime * result + Float.floatToIntBits(weight); - return result; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (obj == null) { - return false; - } - if (getClass() != obj.getClass()) { - return false; - } - HistoricalRecord other = (HistoricalRecord) obj; - if (activity == null) { - if (other.activity != null) { - return false; - } - } else if (!activity.equals(other.activity)) { - return false; - } - if (time != other.time) { - return false; - } - if (Float.floatToIntBits(weight) != Float.floatToIntBits(other.weight)) { - return false; - } - return true; - } - - @Override - public String toString() { - StringBuilder builder = new StringBuilder(); - builder.append("["); - builder.append("; activity:").append(activity); - builder.append("; time:").append(time); - builder.append("; weight:").append(new BigDecimal(weight)); - builder.append("]"); - return builder.toString(); - } - } - - /** - * Represents an activity. - */ - public final class ActivityResolveInfo implements Comparable { - - /** - * The {@link ResolveInfo} of the activity. - */ - public final ResolveInfo resolveInfo; - - /** - * Weight of the activity. Useful for sorting. - */ - public float weight; - - /** - * Creates a new instance. - * - * @param resolveInfo activity {@link ResolveInfo}. - */ - public ActivityResolveInfo(ResolveInfo resolveInfo) { - this.resolveInfo = resolveInfo; - } - - @Override - public int hashCode() { - return 31 + Float.floatToIntBits(weight); - } - - @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (obj == null) { - return false; - } - if (getClass() != obj.getClass()) { - return false; - } - ActivityResolveInfo other = (ActivityResolveInfo) obj; - if (Float.floatToIntBits(weight) != Float.floatToIntBits(other.weight)) { - return false; - } - return true; - } - - @Override - public int compareTo(ActivityResolveInfo another) { - return Float.floatToIntBits(another.weight) - Float.floatToIntBits(weight); - } - - @Override - public String toString() { - StringBuilder builder = new StringBuilder(); - builder.append("["); - builder.append("resolveInfo:").append(resolveInfo.toString()); - builder.append("; weight:").append(new BigDecimal(weight)); - builder.append("]"); - return builder.toString(); - } - } - - /** - * Default activity sorter implementation. - */ - private final class DefaultSorter implements ActivitySorter { - private static final float WEIGHT_DECAY_COEFFICIENT = 0.95f; - - private final Map mPackageNameToActivityMap = - new HashMap(); - - @Override - public void sort(Intent intent, List activities, - List historicalRecords) { - Map packageNameToActivityMap = - mPackageNameToActivityMap; - packageNameToActivityMap.clear(); - - final int activityCount = activities.size(); - for (int i = 0; i < activityCount; i++) { - ActivityResolveInfo activity = activities.get(i); - activity.weight = 0.0f; - - // Make sure we're using a non-ambiguous name here - ComponentName chosenName = new ComponentName( - activity.resolveInfo.activityInfo.packageName, - activity.resolveInfo.activityInfo.name); - String packageName = chosenName.flattenToString(); - packageNameToActivityMap.put(packageName, activity); - } - - final int lastShareIndex = historicalRecords.size() - 1; - float nextRecordWeight = 1; - for (int i = lastShareIndex; i >= 0; i--) { - HistoricalRecord historicalRecord = historicalRecords.get(i); - String packageName = historicalRecord.activity.flattenToString(); - ActivityResolveInfo activity = packageNameToActivityMap.get(packageName); - if (activity != null) { - activity.weight += historicalRecord.weight * nextRecordWeight; - nextRecordWeight = nextRecordWeight * WEIGHT_DECAY_COEFFICIENT; - } - } - - Collections.sort(activities); - - if (DEBUG) { - for (int i = 0; i < activityCount; i++) { - Log.i(LOG_TAG, "Sorted: " + activities.get(i)); - } - } - } - } - - /** - * Command for reading the historical records from a file off the UI thread. - */ - private void readHistoricalDataImpl() { - try { - GeckoProfile profile = GeckoProfile.get(mContext); - File f = profile.getFile(mHistoryFileName); - if (!f.exists()) { - // Fall back to the non-profile aware file if it exists... - File oldFile = new File(mHistoryFileName); - oldFile.renameTo(f); - } - readHistoricalDataFromStream(new FileInputStream(f)); - } catch (FileNotFoundException fnfe) { - final Distribution dist = Distribution.getInstance(mContext); - dist.addOnDistributionReadyCallback(new Distribution.ReadyCallback() { - @Override - public void distributionNotFound() { - } - - @Override - public void distributionFound(Distribution distribution) { - try { - File distFile = dist.getDistributionFile("quickshare/" + mHistoryFileName); - if (distFile == null) { - if (DEBUG) { - Log.i(LOG_TAG, "Could not open historical records file: " + mHistoryFileName); - } - return; - } - readHistoricalDataFromStream(new FileInputStream(distFile)); - } catch (Exception ex) { - if (DEBUG) { - Log.i(LOG_TAG, "Could not open historical records file: " + mHistoryFileName); - } - return; - } - } - - @Override - public void distributionArrivedLate(Distribution distribution) { - distributionFound(distribution); - } - }); - } - } - - void readHistoricalDataFromStream(FileInputStream fis) { - try { - XmlPullParser parser = Xml.newPullParser(); - parser.setInput(fis, null); - - int type = XmlPullParser.START_DOCUMENT; - while (type != XmlPullParser.END_DOCUMENT && type != XmlPullParser.START_TAG) { - type = parser.next(); - } - - if (!TAG_HISTORICAL_RECORDS.equals(parser.getName())) { - throw new XmlPullParserException("Share records file does not start with " - + TAG_HISTORICAL_RECORDS + " tag."); - } - - List historicalRecords = mHistoricalRecords; - historicalRecords.clear(); - - while (true) { - type = parser.next(); - if (type == XmlPullParser.END_DOCUMENT) { - break; - } - if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) { - continue; - } - String nodeName = parser.getName(); - if (!TAG_HISTORICAL_RECORD.equals(nodeName)) { - throw new XmlPullParserException("Share records file not well-formed."); - } - - String activity = parser.getAttributeValue(null, ATTRIBUTE_ACTIVITY); - final long time = - Long.parseLong(parser.getAttributeValue(null, ATTRIBUTE_TIME)); - final float weight = - Float.parseFloat(parser.getAttributeValue(null, ATTRIBUTE_WEIGHT)); - HistoricalRecord readRecord = new HistoricalRecord(activity, time, weight); - historicalRecords.add(readRecord); - - if (DEBUG) { - Log.i(LOG_TAG, "Read " + readRecord.toString()); - } - } - - if (DEBUG) { - Log.i(LOG_TAG, "Read " + historicalRecords.size() + " historical records."); - } - } catch (XmlPullParserException | IOException xppe) { - Log.e(LOG_TAG, "Error reading historical record file: " + mHistoryFileName, xppe); - } finally { - if (fis != null) { - try { - fis.close(); - } catch (IOException ioe) { - /* ignore */ - } - } - } - } - - /** - * Command for persisting the historical records to a file off the UI thread. - */ - private final class PersistHistoryAsyncTask extends AsyncTask { - - @Override - @SuppressWarnings("unchecked") - public Void doInBackground(Object... args) { - List historicalRecords = (List) args[0]; - String historyFileName = (String) args[1]; - - FileOutputStream fos = null; - - try { - // Mozilla - Update the location we save files to - GeckoProfile profile = GeckoProfile.get(mContext); - File file = profile.getFile(historyFileName); - fos = new FileOutputStream(file); - } catch (FileNotFoundException fnfe) { - Log.e(LOG_TAG, "Error writing historical record file: " + historyFileName, fnfe); - return null; - } - - XmlSerializer serializer = Xml.newSerializer(); - - try { - serializer.setOutput(fos, null); - serializer.startDocument("UTF-8", true); - serializer.startTag(null, TAG_HISTORICAL_RECORDS); - - final int recordCount = historicalRecords.size(); - for (int i = 0; i < recordCount; i++) { - HistoricalRecord record = historicalRecords.remove(0); - serializer.startTag(null, TAG_HISTORICAL_RECORD); - serializer.attribute(null, ATTRIBUTE_ACTIVITY, - record.activity.flattenToString()); - serializer.attribute(null, ATTRIBUTE_TIME, String.valueOf(record.time)); - serializer.attribute(null, ATTRIBUTE_WEIGHT, String.valueOf(record.weight)); - serializer.endTag(null, TAG_HISTORICAL_RECORD); - if (DEBUG) { - Log.i(LOG_TAG, "Wrote " + record.toString()); - } - } - - serializer.endTag(null, TAG_HISTORICAL_RECORDS); - serializer.endDocument(); - - if (DEBUG) { - Log.i(LOG_TAG, "Wrote " + recordCount + " historical records."); - } - } catch (IllegalArgumentException | IOException | IllegalStateException e) { - Log.e(LOG_TAG, "Error writing historical record file: " + mHistoryFileName, e); - } finally { - mCanReadHistoricalData = true; - if (fos != null) { - try { - fos.close(); - } catch (IOException e) { - /* ignore */ - } - } - } - return null; - } - } - - /** - * Keeps in sync the historical records and activities with the installed applications. - */ - /** - * Mozilla: Adapted significantly - */ - private static final String LOGTAG = "GeckoActivityChooserModel"; - private final class DataModelPackageMonitor extends BroadcastReceiver { - Context mContext; - - public DataModelPackageMonitor() { } - - public void register(Context context) { - mContext = context; - - String[] intents = new String[] { - Intent.ACTION_PACKAGE_REMOVED, - Intent.ACTION_PACKAGE_ADDED, - Intent.ACTION_PACKAGE_CHANGED - }; - - for (String intent : intents) { - IntentFilter removeFilter = new IntentFilter(intent); - removeFilter.addDataScheme("package"); - context.registerReceiver(this, removeFilter); - } - } - - public void unregister() { - mContext.unregisterReceiver(this); - mContext = null; - } - - @Override - public void onReceive(Context context, Intent intent) { - String action = intent.getAction(); - if (Intent.ACTION_PACKAGE_REMOVED.equals(action)) { - String packageName = intent.getData().getSchemeSpecificPart(); - removeHistoricalRecordsForPackage(packageName); - } - - mReloadActivities = true; - } - } - - /** - * Mozilla: Return whether or not there are other synced clients. - */ - private boolean hasOtherSyncClients() { - // ClientsDatabaseAccessor returns stale data (bug 1145896) so we work around this by - // checking if we have accounts set up - if not, we can't have any clients. - if (!FirefoxAccounts.firefoxAccountsExist(mContext)) { - return false; - } - - final BrowserDB browserDB = BrowserDB.from(mContext); - final TabsAccessor tabsAccessor = browserDB.getTabsAccessor(); - final Cursor remoteClientsCursor = tabsAccessor - .getRemoteClientsByRecencyCursor(mContext); - if (remoteClientsCursor == null) { - return false; - } - - try { - return remoteClientsCursor.getCount() > 0; - } finally { - remoteClientsCursor.close(); - } - } - - /** - * Mozilla: Reload activities on sync. - */ - private class SyncStatusDelegate implements SyncStatusListener { - @Override - public Context getContext() { - return mContext; - } - - @Override - public Account getAccount() { - return FirefoxAccounts.getFirefoxAccount(getContext()); - } - - @Override - public void onSyncStarted() { - } - - @Override - public void onSyncFinished() { - // TODO: We only need to reload activities when the number of devices changes. - // This may not be worth it if we have to touch the DB to get the client count. - synchronized (mInstanceLock) { - mReloadActivities = true; - } - } - } -} - diff --git a/mobile/android/base/java/org/mozilla/gecko/widget/AllCapsTextView.java b/mobile/android/base/java/org/mozilla/gecko/widget/AllCapsTextView.java deleted file mode 100644 index 6bd1e36e46..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/widget/AllCapsTextView.java +++ /dev/null @@ -1,21 +0,0 @@ -/* 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.widget; - -import android.content.Context; -import android.util.AttributeSet; -import android.widget.TextView; - -public class AllCapsTextView extends TextView { - - public AllCapsTextView(Context context, AttributeSet attrs) { - super(context, attrs); - } - - @Override - public void setText(CharSequence text, BufferType type) { - super.setText(text.toString().toUpperCase(), type); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/widget/AnchoredPopup.java b/mobile/android/base/java/org/mozilla/gecko/widget/AnchoredPopup.java deleted file mode 100644 index a504c58320..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/widget/AnchoredPopup.java +++ /dev/null @@ -1,130 +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.widget; - -import org.mozilla.gecko.AppConstants.Versions; -import org.mozilla.gecko.R; - -import android.app.Activity; -import android.content.Context; -import android.graphics.drawable.BitmapDrawable; -import android.view.Gravity; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.LinearLayout; -import android.widget.PopupWindow; -import org.mozilla.gecko.util.HardwareUtils; - -/** - * AnchoredPopup is the base class for doorhanger notifications, and is anchored to the urlbar. - */ -public abstract class AnchoredPopup extends PopupWindow { - public interface OnVisibilityChangeListener { - public void onDoorHangerShow(); - public void onDoorHangerHide(); - } - - private View mAnchor; - private OnVisibilityChangeListener onVisibilityChangeListener; - - protected RoundedCornerLayout mContent; - protected boolean mInflated; - - protected final Context mContext; - - public AnchoredPopup(Context context) { - super(context); - - mContext = context; - - setAnimationStyle(R.style.PopupAnimation); - } - - protected void init() { - // Hide the default window background. Passing null prevents the below setOutTouchable() - // call from working, so use an empty BitmapDrawable instead. - setBackgroundDrawable(new BitmapDrawable(mContext.getResources())); - - // Allow the popup to be dismissed when touching outside. - setOutsideTouchable(true); - - // PopupWindow has a default width and height of 0, so set the width here. - int width = (int) mContext.getResources().getDimension(R.dimen.doorhanger_width); - setWindowLayoutMode(0, ViewGroup.LayoutParams.WRAP_CONTENT); - setWidth(width); - - final LayoutInflater inflater = LayoutInflater.from(mContext); - final View layout = inflater.inflate(R.layout.anchored_popup, null); - setContentView(layout); - - mContent = (RoundedCornerLayout) layout.findViewById(R.id.content); - - mInflated = true; - } - - /** - * Sets the anchor for this popup. - * - * @param anchor Anchor view for positioning the arrow. - */ - public void setAnchor(View anchor) { - mAnchor = anchor; - } - - public void setOnVisibilityChangeListener(OnVisibilityChangeListener listener) { - onVisibilityChangeListener = listener; - } - - /** - * Shows the popup with the arrow pointing to the center of the anchor view. If the anchor - * isn't visible, the popup will just be shown at the top of the root view. - */ - public void show() { - if (!mInflated) { - throw new IllegalStateException("ArrowPopup#init() must be called before ArrowPopup#show()"); - } - - if (onVisibilityChangeListener != null) { - onVisibilityChangeListener.onDoorHangerShow(); - } - - final int[] anchorLocation = new int[2]; - if (mAnchor != null) { - mAnchor.getLocationInWindow(anchorLocation); - } - - // The doorhanger should overlap the bottom of the urlbar. - int offsetY = mContext.getResources().getDimensionPixelOffset(R.dimen.doorhanger_offsetY); - final View decorView = ((Activity) mContext).getWindow().getDecorView(); - - final boolean validAnchor = (mAnchor != null) && (anchorLocation[1] > 0); - if (HardwareUtils.isTablet()) { - if (validAnchor) { - showAsDropDown(mAnchor, 0, 0); - } else { - // The anchor will be offscreen if the dynamic toolbar is hidden, so anticipate the re-shown position - // of the toolbar. - final int offsetX = mContext.getResources().getDimensionPixelOffset(R.dimen.doorhanger_offsetX); - showAtLocation(decorView, Gravity.TOP | Gravity.LEFT, offsetX, offsetY); - } - } else { - // If the anchor is null or out of the window bounds, just show the popup at the top of the - // root view. - final View anchor = validAnchor ? mAnchor : decorView; - - showAtLocation(anchor, Gravity.TOP | Gravity.CENTER_HORIZONTAL, 0, offsetY); - } - } - - @Override - public void dismiss() { - super.dismiss(); - if (onVisibilityChangeListener != null) { - onVisibilityChangeListener.onDoorHangerHide(); - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/widget/AnimatedHeightLayout.java b/mobile/android/base/java/org/mozilla/gecko/widget/AnimatedHeightLayout.java deleted file mode 100644 index f1343b0fbf..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/widget/AnimatedHeightLayout.java +++ /dev/null @@ -1,77 +0,0 @@ -/* 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.widget; - -import org.mozilla.gecko.animation.HeightChangeAnimation; - -import android.content.Context; -import android.util.AttributeSet; -import android.view.animation.Animation; -import android.view.animation.DecelerateInterpolator; -import android.widget.RelativeLayout; - -public class AnimatedHeightLayout extends RelativeLayout { - private static final String LOGTAG = "GeckoAnimatedHeightLayout"; - private static final int ANIMATION_DURATION = 100; - private boolean mAnimating; - - public AnimatedHeightLayout(Context context) { - super(context, null); - } - - public AnimatedHeightLayout(Context context, AttributeSet attrs) { - super(context, attrs, 0); - } - - public AnimatedHeightLayout(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - } - - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - int oldHeight = getMeasuredHeight(); - super.onMeasure(widthMeasureSpec, heightMeasureSpec); - int newHeight = getMeasuredHeight(); - - if (!mAnimating && oldHeight != 0 && oldHeight != newHeight) { - mAnimating = true; - setMeasuredDimension(getMeasuredWidth(), oldHeight); - - // Animate the difference of suggestion row height - Animation anim = new HeightChangeAnimation(this, oldHeight, newHeight); - anim.setDuration(ANIMATION_DURATION); - anim.setInterpolator(new DecelerateInterpolator()); - anim.setAnimationListener(new Animation.AnimationListener() { - @Override - public void onAnimationStart(Animation animation) {} - @Override - public void onAnimationRepeat(Animation animation) {} - @Override - public void onAnimationEnd(Animation animation) { - post(new Runnable() { - @Override - public void run() { - finishAnimation(); - } - }); - } - }); - startAnimation(anim); - } - } - - @Override - protected void onDetachedFromWindow() { - super.onDetachedFromWindow(); - finishAnimation(); - } - - void finishAnimation() { - if (mAnimating) { - getLayoutParams().height = LayoutParams.WRAP_CONTENT; - mAnimating = false; - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/widget/BasicColorPicker.java b/mobile/android/base/java/org/mozilla/gecko/widget/BasicColorPicker.java deleted file mode 100644 index 4f14682034..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/widget/BasicColorPicker.java +++ /dev/null @@ -1,140 +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.widget; - -import org.mozilla.gecko.R; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -import android.content.Context; -import android.content.res.Resources; -import android.graphics.Color; -import android.graphics.PorterDuff; -import android.graphics.Rect; -import android.graphics.drawable.Drawable; -import android.view.View; -import android.view.ViewGroup; -import android.view.WindowManager; -import android.widget.ArrayAdapter; -import android.widget.AdapterView; -import android.widget.CheckedTextView; -import android.widget.ListView; -import android.util.AttributeSet; -import android.util.DisplayMetrics; -import android.util.TypedValue; - -public class BasicColorPicker extends ListView { - private final static String LOGTAG = "GeckoBasicColorPicker"; - private final static List DEFAULT_COLORS = Arrays.asList(Color.rgb(215, 57, 32), - Color.rgb(255, 134, 5), - Color.rgb(255, 203, 19), - Color.rgb(95, 173, 71), - Color.rgb(84, 201, 168), - Color.rgb(33, 161, 222), - Color.rgb(16, 36, 87), - Color.rgb(91, 32, 103), - Color.rgb(212, 221, 228), - Color.BLACK); - - private static Drawable mCheckDrawable; - int mSelected; - final ColorPickerListAdapter mAdapter; - - public BasicColorPicker(Context context) { - this(context, null); - } - - public BasicColorPicker(Context context, AttributeSet attrs) { - this(context, attrs, 0); - } - - public BasicColorPicker(Context context, AttributeSet attrs, int style) { - this(context, attrs, style, DEFAULT_COLORS); - } - - public BasicColorPicker(Context context, AttributeSet attrs, int style, List colors) { - super(context, attrs, style); - mAdapter = new ColorPickerListAdapter(context, new ArrayList(colors)); - setAdapter(mAdapter); - - setOnItemClickListener(new AdapterView.OnItemClickListener() { - @Override - public void onItemClick(AdapterView parent, View view, int position, long id) { - mSelected = position; - mAdapter.notifyDataSetChanged(); - } - }); - } - - public int getColor() { - return mAdapter.getItem(mSelected); - } - - public void setColor(int color) { - if (!DEFAULT_COLORS.contains(color)) { - mSelected = mAdapter.getCount(); - mAdapter.add(color); - } else { - mSelected = DEFAULT_COLORS.indexOf(color); - } - - setSelection(mSelected); - mAdapter.notifyDataSetChanged(); - } - - Drawable getCheckDrawable() { - if (mCheckDrawable == null) { - Resources res = getContext().getResources(); - - TypedValue typedValue = new TypedValue(); - getContext().getTheme().resolveAttribute(android.R.attr.listPreferredItemHeight, typedValue, true); - DisplayMetrics metrics = new android.util.DisplayMetrics(); - ((WindowManager)getContext().getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay().getMetrics(metrics); - int height = (int) typedValue.getDimension(metrics); - - Drawable background = res.getDrawable(R.drawable.color_picker_row_bg); - Rect r = new Rect(); - background.getPadding(r); - height -= r.top + r.bottom; - - mCheckDrawable = res.getDrawable(R.drawable.color_picker_checkmark); - mCheckDrawable.setBounds(0, 0, height, height); - } - - return mCheckDrawable; - } - - private class ColorPickerListAdapter extends ArrayAdapter { - private final List mColors; - - public ColorPickerListAdapter(Context context, List colors) { - super(context, R.layout.color_picker_row, colors); - mColors = colors; - } - - @Override - public View getView(int position, View convertView, ViewGroup parent) { - View v = super.getView(position, convertView, parent); - - Drawable d = v.getBackground(); - d.setColorFilter(getItem(position), PorterDuff.Mode.MULTIPLY); - v.setBackgroundDrawable(d); - - Drawable check = null; - CheckedTextView checked = ((CheckedTextView) v); - if (mSelected == position) { - check = getCheckDrawable(); - } - - checked.setCompoundDrawables(check, null, null, null); - checked.setText(""); - - return v; - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/widget/CheckableLinearLayout.java b/mobile/android/base/java/org/mozilla/gecko/widget/CheckableLinearLayout.java deleted file mode 100644 index b740592fe2..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/widget/CheckableLinearLayout.java +++ /dev/null @@ -1,52 +0,0 @@ -/* 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.widget; - -import org.mozilla.gecko.R; - -import android.content.Context; -import android.util.AttributeSet; -import android.widget.CheckBox; -import android.widget.Checkable; -import android.widget.LinearLayout; - - -public class CheckableLinearLayout extends LinearLayout implements Checkable { - - private CheckBox mCheckBox; - - public CheckableLinearLayout(Context context, AttributeSet attrs) { - super(context, attrs); - } - - @Override - public boolean isChecked() { - return mCheckBox != null && mCheckBox.isChecked(); - } - - @Override - public void setChecked(boolean isChecked) { - if (mCheckBox != null) { - mCheckBox.setChecked(isChecked); - } - } - - @Override - public void toggle() { - if (mCheckBox != null) { - mCheckBox.toggle(); - } - } - - @Override - protected void onFinishInflate() { - super.onFinishInflate(); - - mCheckBox = (CheckBox) findViewById(R.id.checkbox); - mCheckBox.setClickable(false); - } -} - - diff --git a/mobile/android/base/java/org/mozilla/gecko/widget/ClickableWhenDisabledEditText.java b/mobile/android/base/java/org/mozilla/gecko/widget/ClickableWhenDisabledEditText.java deleted file mode 100644 index 206341212c..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/widget/ClickableWhenDisabledEditText.java +++ /dev/null @@ -1,25 +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.widget; - -import android.content.Context; -import android.util.AttributeSet; -import android.view.MotionEvent; -import android.widget.EditText; - -public class ClickableWhenDisabledEditText extends EditText { - public ClickableWhenDisabledEditText(Context context, AttributeSet attrs) { - super(context, attrs); - } - - @Override - public boolean onTouchEvent(MotionEvent event) { - if (!isEnabled() && event.getAction() == MotionEvent.ACTION_UP) { - return performClick(); - } - return super.onTouchEvent(event); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/widget/ContentSecurityDoorHanger.java b/mobile/android/base/java/org/mozilla/gecko/widget/ContentSecurityDoorHanger.java deleted file mode 100644 index 96b20a6c31..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/widget/ContentSecurityDoorHanger.java +++ /dev/null @@ -1,127 +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.widget; - -import android.support.v4.content.ContextCompat; -import android.util.Log; -import android.widget.Button; -import android.widget.TextView; -import org.mozilla.gecko.R; - -import org.json.JSONException; -import org.json.JSONObject; - -import android.content.Context; -import android.view.View; - -import org.mozilla.gecko.Telemetry; -import org.mozilla.gecko.TelemetryContract; -import org.mozilla.gecko.toolbar.SiteIdentityPopup; - -import java.util.Locale; - -public class ContentSecurityDoorHanger extends DoorHanger { - private static final String LOGTAG = "GeckoSecurityDoorHanger"; - - private final TextView mTitle; - private final TextView mSecurityState; - private final TextView mMessage; - - public ContentSecurityDoorHanger(Context context, DoorhangerConfig config, Type type) { - super(context, config, type); - - mTitle = (TextView) findViewById(R.id.security_title); - mSecurityState = (TextView) findViewById(R.id.security_state); - mMessage = (TextView) findViewById(R.id.security_message); - - loadConfig(config); - } - - @Override - protected void loadConfig(DoorhangerConfig config) { - final String message = config.getMessage(); - if (message != null) { - mMessage.setText(message); - } - - final JSONObject options = config.getOptions(); - if (options != null) { - setOptions(options); - } - - final DoorhangerConfig.Link link = config.getLink(); - if (link != null) { - addLink(link.label, link.url); - } - - addButtonsToLayout(config); - } - - @Override - protected int getContentResource() { - return R.layout.doorhanger_security; - } - - @Override - public void setOptions(final JSONObject options) { - super.setOptions(options); - final JSONObject link = options.optJSONObject("link"); - if (link != null) { - try { - final String linkLabel = link.getString("label"); - final String linkUrl = link.getString("url"); - addLink(linkLabel, linkUrl); - } catch (JSONException e) { } - } - - final JSONObject trackingProtection = options.optJSONObject("tracking_protection"); - if (trackingProtection != null) { - mTitle.setVisibility(VISIBLE); - mTitle.setText(R.string.doorhanger_tracking_title); - try { - final boolean enabled = trackingProtection.getBoolean("enabled"); - if (enabled) { - mMessage.setText(R.string.doorhanger_tracking_message_enabled); - mSecurityState.setText(R.string.doorhanger_tracking_state_enabled); - mSecurityState.setTextColor(ContextCompat.getColor(getContext(), R.color.affirmative_green)); - } else { - mMessage.setText(R.string.doorhanger_tracking_message_disabled); - mSecurityState.setText(R.string.doorhanger_tracking_state_disabled); - mSecurityState.setTextColor(ContextCompat.getColor(getContext(), R.color.rejection_red)); - } - mMessage.setVisibility(VISIBLE); - mSecurityState.setVisibility(VISIBLE); - } catch (JSONException e) { } - } - } - - @Override - protected OnClickListener makeOnButtonClickListener(final int id, final String telemetryExtra) { - return new Button.OnClickListener() { - @Override - public void onClick(View v) { - final String expandedExtra = mType.toString().toLowerCase(Locale.US) + "-" + telemetryExtra; - Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.DOORHANGER, expandedExtra); - - final JSONObject response = new JSONObject(); - try { - switch (mType) { - case TRACKING: - response.put("allowContent", (id == SiteIdentityPopup.ButtonType.DISABLE.ordinal())); - response.put("contentType", ("tracking")); - break; - default: - Log.w(LOGTAG, "Unknown doorhanger type " + mType.toString()); - } - } catch (JSONException e) { - Log.e(LOGTAG, "Error creating onClick response", e); - } - - mOnButtonClickListener.onButtonClick(response, ContentSecurityDoorHanger.this); - } - }; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/widget/CropImageView.java b/mobile/android/base/java/org/mozilla/gecko/widget/CropImageView.java deleted file mode 100644 index 63cb84c5aa..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/widget/CropImageView.java +++ /dev/null @@ -1,143 +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.widget; - -import android.content.Context; -import android.graphics.Bitmap; -import android.graphics.Matrix; -import android.graphics.drawable.Drawable; -import android.util.AttributeSet; -import android.widget.ImageView; - -import org.mozilla.gecko.widget.themed.ThemedImageView; - -/** - * An ImageView which will always display at the given width and calculated height (based on the width and - * the supplied aspect ratio), drawn starting from the top left hand corner. A supplied drawable will be resized to fit - * the width of the view; if the resized drawable is too tall for the view then the drawable will be cropped at the - * bottom, however if the resized drawable is too short for the view to display whilst honouring it's given width and - * height then the drawable will be displayed at full height with the right hand side cropped. - */ -public abstract class CropImageView extends ThemedImageView { - public static final String LOGTAG = "Gecko" + CropImageView.class.getSimpleName(); - - private int viewWidth; - private int viewHeight; - private int drawableWidth; - private int drawableHeight; - - private boolean resize = true; - private Matrix layoutCurrentMatrix = new Matrix(); - private Matrix layoutNextMatrix = new Matrix(); - - - public CropImageView(final Context context) { - this(context, null); - } - - public CropImageView(final Context context, final AttributeSet attrs) { - this(context, attrs, 0); - } - - public CropImageView(final Context context, final AttributeSet attrs, final int defStyleAttr) { - super(context, attrs, defStyleAttr); - init(); - } - - protected abstract float getAspectRatio(); - - protected void init() { - // Setting the pivots means that the image will be drawn from the top left hand corner. There are - // issues in Android 4.1 (16) which mean setting these values to 0 may not work. - // http://stackoverflow.com/questions/26658124/setpivotx-doesnt-work-on-android-4-1-1-nineoldandroids - setPivotX(1); - setPivotY(1); - } - - /** - * Measure the view to determine the measured width and height. - * The height is constrained by the measured width. - * - * @param widthMeasureSpec horizontal space requirements as imposed by the parent. - * @param heightMeasureSpec vertical space requirements as imposed by the parent, but ignored. - */ - @Override - protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) { - // Default measuring. - super.onMeasure(widthMeasureSpec, heightMeasureSpec); - - // Force the height based on the aspect ratio. - viewWidth = getMeasuredWidth(); - viewHeight = (int) (viewWidth * getAspectRatio()); - - setMeasuredDimension(viewWidth, viewHeight); - - updateImageMatrix(); - } - - protected void updateImageMatrix() { - if (!resize || getDrawable() == null) { - return; - } - - setScaleType(ImageView.ScaleType.MATRIX); - - getDrawable().setBounds(0, 0, viewWidth, viewHeight); - - final float horizontalScaleValue = (float) viewWidth / (float) drawableWidth; - final float verticalScaleValue = (float) viewHeight / (float) drawableHeight; - - final float scale = Math.max(verticalScaleValue, horizontalScaleValue); - - layoutNextMatrix.reset(); - layoutNextMatrix.setScale(scale, scale); - setImageMatrix(layoutNextMatrix); - - // You can't modify the matrix in place and we want to avoid allocation, so let's keep two references to two - // different matrix objects that we can swap when the values need to change - final Matrix swapReferenceMatrix = layoutCurrentMatrix; - layoutCurrentMatrix = layoutNextMatrix; - layoutNextMatrix = swapReferenceMatrix; - } - - public void setImageBitmap(final Bitmap bm, final boolean resize) { - super.setImageBitmap(bm); - - this.resize = resize; - updateImageMatrix(); - } - - @Override - public void setImageResource(final int resId) { - super.setImageResource(resId); - setImageMatrix(null); - resize = false; - } - - @Override - public void setImageDrawable(final Drawable drawable) { - this.setImageDrawable(drawable, false); - } - - public void setImageDrawable(final Drawable drawable, final boolean resize) { - super.setImageDrawable(drawable); - - if (drawable != null) { - // Reset the matrix to ensure that any previous changes aren't carried through. - setImageMatrix(null); - - drawableWidth = drawable.getIntrinsicWidth(); - drawableHeight = drawable.getIntrinsicHeight(); - } else { - drawableWidth = -1; - drawableHeight = -1; - } - - this.resize = resize; - - updateImageMatrix(); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/widget/DateTimePicker.java b/mobile/android/base/java/org/mozilla/gecko/widget/DateTimePicker.java deleted file mode 100644 index 67f1bcd1d8..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/widget/DateTimePicker.java +++ /dev/null @@ -1,665 +0,0 @@ -/* - * Copyright (C) 2007 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.mozilla.gecko.widget; - -import java.text.SimpleDateFormat; -import java.util.Arrays; -import java.util.Calendar; -import java.util.Locale; - -import org.mozilla.gecko.AppConstants; -import org.mozilla.gecko.AppConstants.Versions; -import org.mozilla.gecko.R; - -import android.content.Context; -import android.text.format.DateFormat; -import android.text.format.DateUtils; -import android.util.DisplayMetrics; -import android.util.Log; -import android.util.TypedValue; -import android.view.Display; -import android.view.LayoutInflater; -import android.view.WindowManager; -import android.view.accessibility.AccessibilityEvent; -import android.view.inputmethod.InputMethodManager; -import android.widget.CalendarView; -import android.widget.EditText; -import android.widget.FrameLayout; -import android.widget.LinearLayout; -import android.widget.NumberPicker; - -public class DateTimePicker extends FrameLayout { - private static final boolean DEBUG = true; - private static final String LOGTAG = "GeckoDateTimePicker"; - private static final int DEFAULT_START_YEAR = 1; - private static final int DEFAULT_END_YEAR = 9999; - private static final char DATE_FORMAT_DAY = 'd'; - private static final char DATE_FORMAT_MONTH = 'M'; - private static final char DATE_FORMAT_YEAR = 'y'; - - boolean mYearEnabled = true; - boolean mMonthEnabled = true; - boolean mWeekEnabled; - boolean mDayEnabled = true; - boolean mHourEnabled = true; - boolean mMinuteEnabled = true; - boolean mIs12HourMode; - private boolean mCalendarEnabled; - - // Size of the screen in inches; - private final int mScreenWidth; - private final int mScreenHeight; - private final OnValueChangeListener mOnChangeListener; - private final LinearLayout mPickers; - private final LinearLayout mDateSpinners; - private final LinearLayout mTimeSpinners; - - final NumberPicker mDaySpinner; - final NumberPicker mMonthSpinner; - final NumberPicker mWeekSpinner; - final NumberPicker mYearSpinner; - final NumberPicker mHourSpinner; - final NumberPicker mMinuteSpinner; - final NumberPicker mAMPMSpinner; - private final CalendarView mCalendar; - private final EditText mDaySpinnerInput; - private final EditText mMonthSpinnerInput; - private final EditText mWeekSpinnerInput; - private final EditText mYearSpinnerInput; - private final EditText mHourSpinnerInput; - private final EditText mMinuteSpinnerInput; - private final EditText mAMPMSpinnerInput; - private Locale mCurrentLocale; - private String[] mShortMonths; - private String[] mShortAMPMs; - private int mNumberOfMonths; - - Calendar mTempDate; - Calendar mCurrentDate; - private Calendar mMinDate; - private Calendar mMaxDate; - private final PickersState mState; - - public static enum PickersState { DATE, MONTH, WEEK, TIME, DATETIME }; - - public class OnValueChangeListener implements NumberPicker.OnValueChangeListener { - @Override - public void onValueChange(NumberPicker picker, int oldVal, int newVal) { - updateInputState(); - mTempDate.setTimeInMillis(mCurrentDate.getTimeInMillis()); - if (DEBUG) { - Log.d(LOGTAG, "SDK version > 10, using new behavior"); - } - - // The native date picker widget on these SDKs increments - // the next field when one field reaches the maximum. - if (picker == mDaySpinner && mDayEnabled) { - int maxDayOfMonth = mTempDate.getActualMaximum(Calendar.DAY_OF_MONTH); - int old = mTempDate.get(Calendar.DAY_OF_MONTH); - setTempDate(Calendar.DAY_OF_MONTH, old, newVal, 1, maxDayOfMonth); - } else if (picker == mMonthSpinner && mMonthEnabled) { - int old = mTempDate.get(Calendar.MONTH); - setTempDate(Calendar.MONTH, old, newVal, Calendar.JANUARY, Calendar.DECEMBER); - } else if (picker == mWeekSpinner) { - int old = mTempDate.get(Calendar.WEEK_OF_YEAR); - int maxWeekOfYear = mTempDate.getActualMaximum(Calendar.WEEK_OF_YEAR); - setTempDate(Calendar.WEEK_OF_YEAR, old, newVal, 0, maxWeekOfYear); - } else if (picker == mYearSpinner && mYearEnabled) { - int month = mTempDate.get(Calendar.MONTH); - mTempDate.set(Calendar.YEAR, newVal); - // Changing the year shouldn't change the month. (in case of non-leap year a Feb 29) - // change the day instead; - if (month != mTempDate.get(Calendar.MONTH)) { - mTempDate.set(Calendar.MONTH, month); - mTempDate.set(Calendar.DAY_OF_MONTH, - mTempDate.getActualMaximum(Calendar.DAY_OF_MONTH)); - } - } else if (picker == mHourSpinner && mHourEnabled) { - if (mIs12HourMode) { - setTempDate(Calendar.HOUR, oldVal, newVal, 1, 12); - } else { - setTempDate(Calendar.HOUR_OF_DAY, oldVal, newVal, 0, 23); - } - } else if (picker == mMinuteSpinner && mMinuteEnabled) { - setTempDate(Calendar.MINUTE, oldVal, newVal, 0, 59); - } else if (picker == mAMPMSpinner && mHourEnabled) { - mTempDate.set(Calendar.AM_PM, newVal); - } else { - throw new IllegalArgumentException(); - } - setDate(mTempDate); - if (mDayEnabled) { - mDaySpinner.setMaxValue(mCurrentDate.getActualMaximum(Calendar.DAY_OF_MONTH)); - } - if (mWeekEnabled) { - mWeekSpinner.setMaxValue(mCurrentDate.getActualMaximum(Calendar.WEEK_OF_YEAR)); - } - updateCalendar(); - updateSpinners(); - notifyDateChanged(); - } - - private void setTempDate(int field, int oldVal, int newVal, int min, int max) { - if (oldVal == max && newVal == min) { - mTempDate.add(field, 1); - } else if (oldVal == min && newVal == max) { - mTempDate.add(field, -1); - } else { - mTempDate.add(field, newVal - oldVal); - } - } - } - - private static final NumberPicker.Formatter TWO_DIGIT_FORMATTER = new NumberPicker.Formatter() { - final StringBuilder mBuilder = new StringBuilder(); - - final java.util.Formatter mFmt = new java.util.Formatter(mBuilder, java.util.Locale.US); - - final Object[] mArgs = new Object[1]; - - @Override - public String format(int value) { - mArgs[0] = value; - mBuilder.delete(0, mBuilder.length()); - mFmt.format("%02d", mArgs); - return mFmt.toString(); - } - }; - - private void displayPickers() { - setWeekShown(false); - set12HourShown(mIs12HourMode); - if (mState == PickersState.DATETIME) { - return; - } - - setHourShown(false); - setMinuteShown(false); - if (mState == PickersState.WEEK) { - setDayShown(false); - setMonthShown(false); - setWeekShown(true); - } else if (mState == PickersState.MONTH) { - setDayShown(false); - } - } - - public DateTimePicker(Context context) { - this(context, "", "", PickersState.DATE, null, null); - } - - public DateTimePicker(Context context, String dateFormat, String dateTimeValue, PickersState state, String minDateValue, String maxDateValue) { - super(context); - - setCurrentLocale(Locale.getDefault()); - - mState = state; - LayoutInflater inflater = LayoutInflater.from(context); - inflater.inflate(R.layout.datetime_picker, this, true); - - mOnChangeListener = new OnValueChangeListener(); - - mDateSpinners = (LinearLayout)findViewById(R.id.date_spinners); - mTimeSpinners = (LinearLayout)findViewById(R.id.time_spinners); - mPickers = (LinearLayout)findViewById(R.id.datetime_picker); - - // We will display differently according to the screen size width. - WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); - Display display = wm.getDefaultDisplay(); - DisplayMetrics dm = new DisplayMetrics(); - display.getMetrics(dm); - mScreenWidth = display.getWidth() / dm.densityDpi; - mScreenHeight = display.getHeight() / dm.densityDpi; - - if (DEBUG) { - Log.d(LOGTAG, "screen width: " + mScreenWidth + " screen height: " + mScreenHeight); - } - - // Set the min / max attribute. - try { - if (minDateValue != null && !minDateValue.equals("")) { - mMinDate.setTime(new SimpleDateFormat(dateFormat).parse(minDateValue)); - } else { - mMinDate.set(DEFAULT_START_YEAR, Calendar.JANUARY, 1); - } - } catch (Exception ex) { - Log.e(LOGTAG, "Error parsing format sting: " + ex); - mMinDate.set(DEFAULT_START_YEAR, Calendar.JANUARY, 1); - } - - try { - if (maxDateValue != null && !maxDateValue.equals("")) { - mMaxDate.setTime(new SimpleDateFormat(dateFormat).parse(maxDateValue)); - } else { - mMaxDate.set(DEFAULT_END_YEAR, Calendar.DECEMBER, 31); - } - } catch (Exception ex) { - Log.e(LOGTAG, "Error parsing format string: " + ex); - mMaxDate.set(DEFAULT_END_YEAR, Calendar.DECEMBER, 31); - } - - // Find the initial date from the constructor arguments. - try { - if (!dateTimeValue.equals("")) { - mTempDate.setTime(new SimpleDateFormat(dateFormat).parse(dateTimeValue)); - } else { - mTempDate.setTimeInMillis(System.currentTimeMillis()); - } - } catch (Exception ex) { - Log.e(LOGTAG, "Error parsing format string: " + ex); - mTempDate.setTimeInMillis(System.currentTimeMillis()); - } - - if (mMaxDate.before(mMinDate)) { - // If the input date range is illogical/garbage, we should not restrict the input range (i.e. allow the - // user to select any date). If we try to make any assumptions based on the illogical min/max date we could - // potentially prevent the user from selecting dates that are in the developers intended range, so it's best - // to allow anything. - mMinDate.set(DEFAULT_START_YEAR, Calendar.JANUARY, 1); - mMaxDate.set(DEFAULT_END_YEAR, Calendar.DECEMBER, 31); - } - - // mTempDate will either be a site-supplied value, or today's date otherwise. CalendarView implementations can - // crash if they're supplied an invalid date (i.e. a date not in the specified range), hence we need to set - // a sensible default date here. - if (mTempDate.before(mMinDate) || mTempDate.after(mMaxDate)) { - mTempDate.setTimeInMillis(mMinDate.getTimeInMillis()); - } - - // If we're displaying a date, the screen is wide enough - // (and if we're using an SDK where the calendar view exists) - // then display a calendar. - if (mState == PickersState.DATE || mState == PickersState.DATETIME) { - mCalendar = new CalendarView(context); - mCalendar.setVisibility(GONE); - - mCalendar.setFocusable(true); - mCalendar.setFocusableInTouchMode(true); - mCalendar.setMaxDate(mMaxDate.getTimeInMillis()); - mCalendar.setMinDate(mMinDate.getTimeInMillis()); - mCalendar.setDate(mTempDate.getTimeInMillis(), false, false); - - mCalendar.setOnDateChangeListener(new CalendarView.OnDateChangeListener() { - @Override - public void onSelectedDayChange( - CalendarView view, int year, int month, int monthDay) { - mTempDate.set(year, month, monthDay); - setDate(mTempDate); - notifyDateChanged(); - } - }); - - final int height; - if (Versions.preLollipop) { - // The 4.X version of CalendarView doesn't request any height, resulting in - // the whole dialog not appearing unless we manually request height. - height = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 200, getResources().getDisplayMetrics());; - } else { - height = LayoutParams.WRAP_CONTENT; - } - - mPickers.addView(mCalendar, LayoutParams.MATCH_PARENT, height); - - } else { - // If the screen is more wide than high, we are displaying day and - // time spinners, and if there is no calendar displayed, we should - // display the fields in one row. - if (mScreenWidth > mScreenHeight && mState == PickersState.DATETIME) { - mPickers.setOrientation(LinearLayout.HORIZONTAL); - } - mCalendar = null; - } - - // Initialize all spinners. - mDaySpinner = setupSpinner(R.id.day, 1, - mTempDate.get(Calendar.DAY_OF_MONTH)); - mDaySpinner.setFormatter(TWO_DIGIT_FORMATTER); - mDaySpinnerInput = (EditText) mDaySpinner.getChildAt(1); - - mMonthSpinner = setupSpinner(R.id.month, 1, - mTempDate.get(Calendar.MONTH) + 1); // Month is 0-based - mMonthSpinner.setFormatter(TWO_DIGIT_FORMATTER); - mMonthSpinner.setDisplayedValues(mShortMonths); - mMonthSpinnerInput = (EditText) mMonthSpinner.getChildAt(1); - - mWeekSpinner = setupSpinner(R.id.week, 1, - mTempDate.get(Calendar.WEEK_OF_YEAR)); - mWeekSpinner.setFormatter(TWO_DIGIT_FORMATTER); - mWeekSpinnerInput = (EditText) mWeekSpinner.getChildAt(1); - - mYearSpinner = setupSpinner(R.id.year, DEFAULT_START_YEAR, - DEFAULT_END_YEAR); - mYearSpinnerInput = (EditText) mYearSpinner.getChildAt(1); - - mAMPMSpinner = setupSpinner(R.id.ampm, 0, 1); - mAMPMSpinner.setFormatter(TWO_DIGIT_FORMATTER); - - if (mIs12HourMode) { - mHourSpinner = setupSpinner(R.id.hour, 1, 12); - mAMPMSpinnerInput = (EditText) mAMPMSpinner.getChildAt(1); - mAMPMSpinner.setDisplayedValues(mShortAMPMs); - } else { - mHourSpinner = setupSpinner(R.id.hour, 0, 23); - mAMPMSpinnerInput = null; - } - - mHourSpinner.setFormatter(TWO_DIGIT_FORMATTER); - mHourSpinnerInput = (EditText) mHourSpinner.getChildAt(1); - - mMinuteSpinner = setupSpinner(R.id.minute, 0, 59); - mMinuteSpinner.setFormatter(TWO_DIGIT_FORMATTER); - mMinuteSpinnerInput = (EditText) mMinuteSpinner.getChildAt(1); - - // The order in which the spinners are displayed are locale-dependent - reorderDateSpinners(); - - // Set the date to the initial date. Since this date can come from the user, - // it can fire an exception (out-of-bound date) - try { - updateDate(mTempDate); - } catch (Exception ex) { - } - - // Display only the pickers needed for the current state. - displayPickers(); - } - - public NumberPicker setupSpinner(int id, int min, int max) { - NumberPicker mSpinner = (NumberPicker) findViewById(id); - mSpinner.setMinValue(min); - mSpinner.setMaxValue(max); - mSpinner.setOnValueChangedListener(mOnChangeListener); - mSpinner.setOnLongPressUpdateInterval(100); - return mSpinner; - } - - public long getTimeInMillis() { - return mCurrentDate.getTimeInMillis(); - } - - private void reorderDateSpinners() { - mDateSpinners.removeAllViews(); - char[] order = DateFormat.getDateFormatOrder(getContext()); - final int spinnerCount = order.length; - - for (int i = 0; i < spinnerCount; i++) { - switch (order[i]) { - case DATE_FORMAT_DAY: - mDateSpinners.addView(mDaySpinner); - break; - case DATE_FORMAT_MONTH: - mDateSpinners.addView(mMonthSpinner); - break; - case DATE_FORMAT_YEAR: - mDateSpinners.addView(mYearSpinner); - break; - default: - throw new IllegalArgumentException(); - } - } - - mDateSpinners.addView(mWeekSpinner); - } - - void setDate(Calendar calendar) { - mCurrentDate = mTempDate; - if (mCurrentDate.before(mMinDate)) { - mCurrentDate.setTimeInMillis(mMinDate.getTimeInMillis()); - } else if (mCurrentDate.after(mMaxDate)) { - mCurrentDate.setTimeInMillis(mMaxDate.getTimeInMillis()); - } - } - - void updateInputState() { - InputMethodManager inputMethodManager = (InputMethodManager) - getContext().getSystemService(Context.INPUT_METHOD_SERVICE); - if (mYearEnabled && inputMethodManager.isActive(mYearSpinnerInput)) { - mYearSpinnerInput.clearFocus(); - inputMethodManager.hideSoftInputFromWindow(getWindowToken(), 0); - } else if (mMonthEnabled && inputMethodManager.isActive(mMonthSpinnerInput)) { - mMonthSpinnerInput.clearFocus(); - inputMethodManager.hideSoftInputFromWindow(getWindowToken(), 0); - } else if (mDayEnabled && inputMethodManager.isActive(mDaySpinnerInput)) { - mDaySpinnerInput.clearFocus(); - inputMethodManager.hideSoftInputFromWindow(getWindowToken(), 0); - } else if (mHourEnabled && inputMethodManager.isActive(mHourSpinnerInput)) { - mHourSpinnerInput.clearFocus(); - inputMethodManager.hideSoftInputFromWindow(getWindowToken(), 0); - } else if (mMinuteEnabled && inputMethodManager.isActive(mMinuteSpinnerInput)) { - mMinuteSpinnerInput.clearFocus(); - inputMethodManager.hideSoftInputFromWindow(getWindowToken(), 0); - } - } - - void updateSpinners() { - if (mDayEnabled) { - if (mCurrentDate.equals(mMinDate)) { - mDaySpinner.setMinValue(mCurrentDate.get(Calendar.DAY_OF_MONTH)); - mDaySpinner.setMaxValue(mCurrentDate.getActualMaximum(Calendar.DAY_OF_MONTH)); - } else if (mCurrentDate.equals(mMaxDate)) { - mDaySpinner.setMinValue(mCurrentDate.getActualMinimum(Calendar.DAY_OF_MONTH)); - mDaySpinner.setMaxValue(mCurrentDate.get(Calendar.DAY_OF_MONTH)); - } else { - mDaySpinner.setMinValue(1); - mDaySpinner.setMaxValue(mCurrentDate.getActualMaximum(Calendar.DAY_OF_MONTH)); - } - mDaySpinner.setValue(mCurrentDate.get(Calendar.DAY_OF_MONTH)); - } - - if (mWeekEnabled) { - mWeekSpinner.setMinValue(1); - mWeekSpinner.setMaxValue(mCurrentDate.getActualMaximum(Calendar.WEEK_OF_YEAR)); - mWeekSpinner.setValue(mCurrentDate.get(Calendar.WEEK_OF_YEAR)); - } - - if (mMonthEnabled) { - mMonthSpinner.setDisplayedValues(null); - if (mCurrentDate.equals(mMinDate)) { - mMonthSpinner.setMinValue(mCurrentDate.get(Calendar.MONTH)); - mMonthSpinner.setMaxValue(mCurrentDate.getActualMaximum(Calendar.MONTH)); - } else if (mCurrentDate.equals(mMaxDate)) { - mMonthSpinner.setMinValue(mCurrentDate.getActualMinimum(Calendar.MONTH)); - mMonthSpinner.setMaxValue(mCurrentDate.get(Calendar.MONTH)); - } else { - mMonthSpinner.setMinValue(Calendar.JANUARY); - mMonthSpinner.setMaxValue(Calendar.DECEMBER); - } - - String[] displayedValues = Arrays.copyOfRange(mShortMonths, - mMonthSpinner.getMinValue(), mMonthSpinner.getMaxValue() + 1); - mMonthSpinner.setDisplayedValues(displayedValues); - mMonthSpinner.setValue(mCurrentDate.get(Calendar.MONTH)); - } - - if (mYearEnabled) { - mYearSpinner.setMinValue(mMinDate.get(Calendar.YEAR)); - mYearSpinner.setMaxValue(mMaxDate.get(Calendar.YEAR)); - mYearSpinner.setValue(mCurrentDate.get(Calendar.YEAR)); - } - - if (mHourEnabled) { - if (mIs12HourMode) { - mHourSpinner.setValue(mCurrentDate.get(Calendar.HOUR)); - mAMPMSpinner.setValue(mCurrentDate.get(Calendar.AM_PM)); - mAMPMSpinner.setDisplayedValues(mShortAMPMs); - } else { - mHourSpinner.setValue(mCurrentDate.get(Calendar.HOUR_OF_DAY)); - } - } - if (mMinuteEnabled) { - mMinuteSpinner.setValue(mCurrentDate.get(Calendar.MINUTE)); - } - } - - void updateCalendar() { - if (mCalendarEnabled) { - mCalendar.setDate(mCurrentDate.getTimeInMillis(), false, false); - } - } - - void notifyDateChanged() { - sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED); - } - - public void toggleCalendar(boolean shown) { - if ((mState != PickersState.DATE && mState != PickersState.DATETIME)) { - return; - } - - if (shown) { - mCalendarEnabled = true; - mCalendar.setVisibility(VISIBLE); - setYearShown(false); - setWeekShown(false); - setMonthShown(false); - setDayShown(false); - } else { - mCalendar.setVisibility(GONE); - setYearShown(true); - setMonthShown(true); - setDayShown(true); - mPickers.setOrientation(LinearLayout.HORIZONTAL); - mCalendarEnabled = false; - } - } - - private void setYearShown(boolean shown) { - if (shown) { - toggleCalendar(false); - mYearSpinner.setVisibility(VISIBLE); - mYearEnabled = true; - } else { - mYearSpinner.setVisibility(GONE); - mYearEnabled = false; - } - } - - private void setWeekShown(boolean shown) { - if (shown) { - toggleCalendar(false); - mWeekSpinner.setVisibility(VISIBLE); - mWeekEnabled = true; - } else { - mWeekSpinner.setVisibility(GONE); - mWeekEnabled = false; - } - } - - private void setMonthShown(boolean shown) { - if (shown) { - toggleCalendar(false); - mMonthSpinner.setVisibility(VISIBLE); - mMonthEnabled = true; - } else { - mMonthSpinner.setVisibility(GONE); - mMonthEnabled = false; - } - } - - private void setDayShown(boolean shown) { - if (shown) { - toggleCalendar(false); - mDaySpinner.setVisibility(VISIBLE); - mDayEnabled = true; - } else { - mDaySpinner.setVisibility(GONE); - mDayEnabled = false; - } - } - - private void set12HourShown(boolean shown) { - if (shown) { - mAMPMSpinner.setVisibility(VISIBLE); - } else { - mAMPMSpinner.setVisibility(GONE); - } - } - - private void setHourShown(boolean shown) { - if (shown) { - mHourSpinner.setVisibility(VISIBLE); - mHourEnabled = true; - } else { - mHourSpinner.setVisibility(GONE); - mAMPMSpinner.setVisibility(GONE); - mTimeSpinners.setVisibility(GONE); - mHourEnabled = false; - } - } - - private void setMinuteShown(boolean shown) { - if (shown) { - mMinuteSpinner.setVisibility(VISIBLE); - mTimeSpinners.findViewById(R.id.mincolon).setVisibility(VISIBLE); - mMinuteEnabled = true; - } else { - mMinuteSpinner.setVisibility(GONE); - mTimeSpinners.findViewById(R.id.mincolon).setVisibility(GONE); - mMinuteEnabled = false; - } - } - - private void setCurrentLocale(Locale locale) { - if (locale.equals(mCurrentLocale)) { - return; - } - - mCurrentLocale = locale; - mIs12HourMode = !DateFormat.is24HourFormat(getContext()); - mTempDate = getCalendarForLocale(mTempDate, locale); - mMinDate = getCalendarForLocale(mMinDate, locale); - mMaxDate = getCalendarForLocale(mMaxDate, locale); - mCurrentDate = getCalendarForLocale(mCurrentDate, locale); - - mNumberOfMonths = mTempDate.getActualMaximum(Calendar.MONTH) + 1; - - mShortAMPMs = new String[2]; - mShortAMPMs[0] = DateUtils.getAMPMString(Calendar.AM); - mShortAMPMs[1] = DateUtils.getAMPMString(Calendar.PM); - - mShortMonths = new String[mNumberOfMonths]; - for (int i = 0; i < mNumberOfMonths; i++) { - mShortMonths[i] = DateUtils.getMonthString(Calendar.JANUARY + i, - DateUtils.LENGTH_MEDIUM); - } - } - - private Calendar getCalendarForLocale(Calendar oldCalendar, Locale locale) { - if (oldCalendar == null) { - return Calendar.getInstance(locale); - } - - final long currentTimeMillis = oldCalendar.getTimeInMillis(); - Calendar newCalendar = Calendar.getInstance(locale); - newCalendar.setTimeInMillis(currentTimeMillis); - return newCalendar; - } - - public void updateDate(Calendar calendar) { - if (mCurrentDate.equals(calendar)) { - return; - } - mCurrentDate.setTimeInMillis(calendar.getTimeInMillis()); - if (mCurrentDate.before(mMinDate)) { - mCurrentDate.setTimeInMillis(mMinDate.getTimeInMillis()); - } else if (mCurrentDate.after(mMaxDate)) { - mCurrentDate.setTimeInMillis(mMaxDate.getTimeInMillis()); - } - updateSpinners(); - notifyDateChanged(); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/widget/DefaultDoorHanger.java b/mobile/android/base/java/org/mozilla/gecko/widget/DefaultDoorHanger.java deleted file mode 100644 index cb8716af74..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/widget/DefaultDoorHanger.java +++ /dev/null @@ -1,190 +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.widget; - -import android.support.v4.content.ContextCompat; -import android.text.Html; -import android.text.Spanned; -import android.util.Log; -import android.widget.Button; -import android.widget.TextView; -import org.mozilla.gecko.R; -import org.mozilla.gecko.Telemetry; -import org.mozilla.gecko.TelemetryContract; -import org.mozilla.gecko.prompts.PromptInput; - -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - -import android.content.Context; -import android.text.TextUtils; -import android.view.View; -import android.view.ViewGroup; -import android.widget.CheckBox; - -import java.util.ArrayList; -import java.util.List; -import java.util.Locale; - -public class DefaultDoorHanger extends DoorHanger { - private static final String LOGTAG = "GeckoDefaultDoorHanger"; - - private static int sSpinnerTextColor = -1; - - private final TextView mMessage; - private List mInputs; - private CheckBox mCheckBox; - - public DefaultDoorHanger(Context context, DoorhangerConfig config, Type type) { - super(context, config, type); - - mMessage = (TextView) findViewById(R.id.doorhanger_message); - - if (sSpinnerTextColor == -1) { - sSpinnerTextColor = ContextCompat.getColor(context, R.color.text_color_primary_disable_only); - } - - switch (mType) { - case GEOLOCATION: - mIcon.setImageResource(R.drawable.location); - mIcon.setVisibility(VISIBLE); - break; - - case DESKTOPNOTIFICATION2: - mIcon.setImageResource(R.drawable.push_notification); - mIcon.setVisibility(VISIBLE); - break; - } - - loadConfig(config); - } - - @Override - protected void loadConfig(DoorhangerConfig config) { - final String message = config.getMessage(); - if (message != null) { - setMessage(message); - } - - final JSONObject options = config.getOptions(); - if (options != null) { - setOptions(options); - } - - final DoorhangerConfig.Link link = config.getLink(); - if (link != null) { - addLink(link.label, link.url); - } - - addButtonsToLayout(config); - } - - @Override - protected int getContentResource() { - return R.layout.default_doorhanger; - } - - private List getInputs() { - return mInputs; - } - - private CheckBox getCheckBox() { - return mCheckBox; - } - - @Override - public void setOptions(final JSONObject options) { - super.setOptions(options); - - final JSONArray inputs = options.optJSONArray("inputs"); - if (inputs != null) { - mInputs = new ArrayList(); - - final ViewGroup group = (ViewGroup) findViewById(R.id.doorhanger_inputs); - group.setVisibility(VISIBLE); - - for (int i = 0; i < inputs.length(); i++) { - try { - PromptInput input = PromptInput.getInput(inputs.getJSONObject(i)); - mInputs.add(input); - - final int padding = mResources.getDimensionPixelSize(R.dimen.doorhanger_section_padding_medium); - View v = input.getView(getContext()); - styleInput(input, v); - v.setPadding(0, 0, 0, padding); - group.addView(v); - } catch (JSONException ex) { } - } - } - - final String checkBoxText = options.optString("checkbox"); - if (!TextUtils.isEmpty(checkBoxText)) { - mCheckBox = (CheckBox) findViewById(R.id.doorhanger_checkbox); - mCheckBox.setText(checkBoxText); - if (options.has("checkboxState")) { - final boolean checkBoxState = options.optBoolean("checkboxState"); - mCheckBox.setChecked(checkBoxState); - } - mCheckBox.setVisibility(VISIBLE); - } - } - - @Override - protected OnClickListener makeOnButtonClickListener(final int id, final String telemetryExtra) { - return new Button.OnClickListener() { - @Override - public void onClick(View v) { - final String expandedExtra = mType.toString().toLowerCase(Locale.US) + "-" + telemetryExtra; - Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.DOORHANGER, expandedExtra); - - final JSONObject response = new JSONObject(); - try { - response.put("callback", id); - - CheckBox checkBox = getCheckBox(); - // If the checkbox is being used, pass its value - if (checkBox != null) { - response.put("checked", checkBox.isChecked()); - } - - List doorHangerInputs = getInputs(); - if (doorHangerInputs != null) { - JSONObject inputs = new JSONObject(); - for (PromptInput input : doorHangerInputs) { - inputs.put(input.getId(), input.getValue()); - } - response.put("inputs", inputs); - } - } catch (JSONException e) { - Log.e(LOGTAG, "Error creating onClick response", e); - } - - mOnButtonClickListener.onButtonClick(response, DefaultDoorHanger.this); - } - }; - } - - private void setMessage(String message) { - Spanned markupMessage = Html.fromHtml(message); - mMessage.setText(markupMessage); - } - - private void styleInput(PromptInput input, View view) { - if (input instanceof PromptInput.MenulistInput) { - styleDropdownInputs(input, view); - } - view.setPadding(0, 0, 0, mResources.getDimensionPixelSize(R.dimen.doorhanger_subsection_padding)); - } - - private void styleDropdownInputs(PromptInput input, View view) { - PromptInput.MenulistInput spinInput = (PromptInput.MenulistInput) input; - - if (spinInput.textView != null) { - spinInput.textView.setTextColor(sSpinnerTextColor); - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/widget/DefaultItemAnimatorBase.java b/mobile/android/base/java/org/mozilla/gecko/widget/DefaultItemAnimatorBase.java deleted file mode 100644 index 5beec3a5c7..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/widget/DefaultItemAnimatorBase.java +++ /dev/null @@ -1,685 +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.widget; - -import android.support.annotation.NonNull; -import android.support.v4.animation.AnimatorCompatHelper; -import android.support.v4.view.ViewCompat; -import android.support.v4.view.ViewPropertyAnimatorCompat; -import android.support.v4.view.ViewPropertyAnimatorListener; -import android.support.v7.widget.RecyclerView; -import android.support.v7.widget.SimpleItemAnimator; -import android.view.View; - -import java.util.ArrayList; -import java.util.List; - -/** - * This basically follows the approach taken by Wasabeef: - * https://github.com/wasabeef/recyclerview-animators - * based off of Android's DefaultItemAnimator from October 2016: - * - * https://github.com/android/platform_frameworks_support/blob/432f3317f8a9b8cf98277938ea5df4021e983055/v7/recyclerview/src/android/support/v7/widget/DefaultItemAnimator.java - * - *

- * Usage Notes: - *

- *
    - *
  • You must add a Default*VpaListener to your animate*Impl animation - the - * listener takes care of animation bookkeeping.
  • - *
  • You should call {@link #resetAnimation(RecyclerView.ViewHolder)} at some point in - * preAnimate*Impl if you choose to proceed with the animation. Some animations will want to - * know some or all of the current animation values for initializing their own animation - * values before resetting the current animation, so this class does not provide the reset - * service itself.
  • - *
  • {@link #resetViewProperties(View)} is used to reset a view any time an animation ends or - * gets canceled - you should redefine resetViewProperties if the version here doesn't reset - * all of the properties you're animating.
  • - *
- */ -public class DefaultItemAnimatorBase extends SimpleItemAnimator { - private List pendingRemovals = new ArrayList<>(); - private List pendingAdditions = new ArrayList<>(); - private List pendingMoves = new ArrayList<>(); - private List pendingChanges = new ArrayList<>(); - - private List> additionsList = new ArrayList<>(); - private List> movesList = new ArrayList<>(); - private List> changesList = new ArrayList<>(); - - private List addAnimations = new ArrayList<>(); - private List moveAnimations = new ArrayList<>(); - private List removeAnimations = new ArrayList<>(); - private List changeAnimations = new ArrayList<>(); - - protected static class MoveInfo { - public RecyclerView.ViewHolder holder; - public int fromX, fromY, toX, toY; - - public MoveInfo(RecyclerView.ViewHolder holder, int fromX, int fromY, int toX, int toY) { - this.holder = holder; - this.fromX = fromX; - this.fromY = fromY; - this.toX = toX; - this.toY = toY; - } - } - - protected static class ChangeInfo { - public RecyclerView.ViewHolder oldHolder, newHolder; - public int fromX, fromY, toX, toY; - - public ChangeInfo(RecyclerView.ViewHolder oldHolder, RecyclerView.ViewHolder newHolder) { - this.oldHolder = oldHolder; - this.newHolder = newHolder; - } - - public ChangeInfo(RecyclerView.ViewHolder oldHolder, RecyclerView.ViewHolder newHolder, - int fromX, int fromY, int toX, int toY) { - this(oldHolder, newHolder); - this.fromX = fromX; - this.fromY = fromY; - this.toX = toX; - this.toY = toY; - } - - @Override - public String toString() { - return "ChangeInfo{" + - "oldHolder=" + oldHolder + - ", newHolder=" + newHolder + - ", fromX=" + fromX + - ", fromY=" + fromY + - ", toX=" + toX + - ", toY=" + toY + - '}'; - } - } - - @Override - public void runPendingAnimations() { - final boolean removalsPending = !pendingRemovals.isEmpty(); - final boolean movesPending = !pendingMoves.isEmpty(); - final boolean changesPending = !pendingChanges.isEmpty(); - final boolean additionsPending = !pendingAdditions.isEmpty(); - if (!removalsPending && !movesPending && !additionsPending && !changesPending) { - return; - } - // First, remove stuff. - for (final RecyclerView.ViewHolder holder : pendingRemovals) { - animateRemoveImpl(holder); - } - pendingRemovals.clear(); - // Next, move stuff. - if (movesPending) { - final List moves = new ArrayList<>(); - moves.addAll(pendingMoves); - movesList.add(moves); - pendingMoves.clear(); - final Runnable mover = new Runnable() { - @Override - public void run() { - for (final MoveInfo moveInfo : moves) { - animateMoveImpl(moveInfo.holder, moveInfo.fromX, moveInfo.fromY, - moveInfo.toX, moveInfo.toY); - } - moves.clear(); - movesList.remove(moves); - } - }; - if (removalsPending) { - final View view = moves.get(0).holder.itemView; - ViewCompat.postOnAnimationDelayed(view, mover, getRemoveDuration()); - } else { - mover.run(); - } - } - // Next, change stuff, to run in parallel with move animations. - if (changesPending) { - final List changes = new ArrayList<>(); - changes.addAll(pendingChanges); - changesList.add(changes); - pendingChanges.clear(); - final Runnable changer = new Runnable() { - @Override - public void run() { - for (final ChangeInfo change : changes) { - animateChangeImpl(change); - } - changes.clear(); - changesList.remove(changes); - } - }; - if (removalsPending) { - RecyclerView.ViewHolder holder = changes.get(0).oldHolder; - ViewCompat.postOnAnimationDelayed(holder.itemView, changer, getRemoveDuration()); - } else { - changer.run(); - } - } - // Next, add stuff. - if (additionsPending) { - final List additions = new ArrayList<>(); - additions.addAll(pendingAdditions); - additionsList.add(additions); - pendingAdditions.clear(); - final Runnable adder = new Runnable() { - public void run() { - for (final RecyclerView.ViewHolder holder : additions) { - animateAddImpl(holder); - } - additions.clear(); - additionsList.remove(additions); - } - }; - if (removalsPending || movesPending || changesPending) { - final long removeDuration = removalsPending ? getRemoveDuration() : 0; - final long moveDuration = movesPending ? getMoveDuration() : 0; - final long changeDuration = changesPending ? getChangeDuration() : 0; - final long totalDelay = removeDuration + Math.max(moveDuration, changeDuration); - final View view = additions.get(0).itemView; - ViewCompat.postOnAnimationDelayed(view, adder, totalDelay); - } else { - adder.run(); - } - } - } - - @Override - public boolean animateRemove(final RecyclerView.ViewHolder holder) { - if (!preAnimateRemoveImpl(holder)) { - dispatchRemoveFinished(holder); - return false; - } - pendingRemovals.add(holder); - return true; - } - - protected boolean preAnimateRemoveImpl(final RecyclerView.ViewHolder holder) { - resetAnimation(holder); - return true; - } - - protected void animateRemoveImpl(final RecyclerView.ViewHolder holder) { - ViewCompat.animate(holder.itemView) - .setDuration(getRemoveDuration()) - .alpha(0) - .setListener(new DefaultRemoveVpaListener(holder)) - .start(); - } - - @Override - public boolean animateAdd(final RecyclerView.ViewHolder holder) { - if (!preAnimateAddImpl(holder)) { - dispatchAddFinished(holder); - return false; - } - pendingAdditions.add(holder); - return true; - } - - protected boolean preAnimateAddImpl(RecyclerView.ViewHolder holder) { - resetAnimation(holder); - holder.itemView.setAlpha(0); - return true; - } - - protected void animateAddImpl(final RecyclerView.ViewHolder holder) { - ViewCompat.animate(holder.itemView) - .setDuration(getAddDuration()) - .alpha(1) - .setListener(new DefaultAddVpaListener(holder)) - .start(); - } - - @Override - public boolean animateMove(final RecyclerView.ViewHolder holder, - int fromX, int fromY, int toX, int toY) { - final View view = holder.itemView; - fromX += ViewCompat.getTranslationX(holder.itemView); - fromY += ViewCompat.getTranslationY(holder.itemView); - final int deltaX = toX - fromX; - final int deltaY = toY - fromY; - if (deltaX == 0 && deltaY == 0) { - dispatchMoveFinished(holder); - return false; - } - resetAnimation(holder); - if (deltaX != 0) { - ViewCompat.setTranslationX(view, -deltaX); - } - if (deltaY != 0) { - ViewCompat.setTranslationY(view, -deltaY); - } - pendingMoves.add(new MoveInfo(holder, fromX, fromY, toX, toY)); - return true; - } - - protected void animateMoveImpl(final RecyclerView.ViewHolder holder, - int fromX, int fromY, int toX, int toY) { - final View view = holder.itemView; - final int deltaX = toX - fromX; - final int deltaY = toY - fromY; - if (deltaX != 0) { - ViewCompat.animate(view).translationX(0); - } - if (deltaY != 0) { - ViewCompat.animate(view).translationY(0); - } - // TODO: make EndActions end listeners instead, since end actions aren't called when - // vpas are canceled (and can't end them. why?) - // need listener functionality in VPACompat for this. Ick. - final ViewPropertyAnimatorCompat animation = ViewCompat.animate(view); - moveAnimations.add(holder); - animation.setDuration(getMoveDuration()).setListener(new VpaListenerAdapter() { - @Override - public void onAnimationStart(View view) { - dispatchMoveStarting(holder); - } - @Override - public void onAnimationCancel(View view) { - resetViewProperties(view); - } - @Override - public void onAnimationEnd(View view) { - animation.setListener(null); - dispatchMoveFinished(holder); - moveAnimations.remove(holder); - dispatchFinishedWhenDone(); - } - }).start(); - } - - @Override - public boolean animateChange(RecyclerView.ViewHolder oldHolder, RecyclerView.ViewHolder newHolder, - int fromX, int fromY, int toX, int toY) { - if (oldHolder == newHolder) { - // Don't know how to run change animations when the same view holder is re-used. - // Run a move animation to handle position changes (if there are any). - if (fromX != toX || fromY != toY) { - // *Don't* call dispatchChangeFinished here, it leads to unbalanced isRecyclable calls. - return animateMove(oldHolder, fromX, fromY, toX, toY); - } - dispatchChangeFinished(oldHolder, true); - return false; - } - final float prevTranslationX = ViewCompat.getTranslationX(oldHolder.itemView); - final float prevTranslationY = ViewCompat.getTranslationY(oldHolder.itemView); - final float prevAlpha = ViewCompat.getAlpha(oldHolder.itemView); - resetAnimation(oldHolder); - final int deltaX = (int) (toX - fromX - prevTranslationX); - final int deltaY = (int) (toY - fromY - prevTranslationY); - // Recover previous translation state after ending animation. - ViewCompat.setTranslationX(oldHolder.itemView, prevTranslationX); - ViewCompat.setTranslationY(oldHolder.itemView, prevTranslationY); - ViewCompat.setAlpha(oldHolder.itemView, prevAlpha); - if (newHolder != null) { - // Carry over translation values. - resetAnimation(newHolder); - ViewCompat.setTranslationX(newHolder.itemView, -deltaX); - ViewCompat.setTranslationY(newHolder.itemView, -deltaY); - ViewCompat.setAlpha(newHolder.itemView, 0); - } - pendingChanges.add(new ChangeInfo(oldHolder, newHolder, fromX, fromY, toX, toY)); - return true; - } - - protected void animateChangeImpl(final ChangeInfo changeInfo) { - final RecyclerView.ViewHolder holder = changeInfo.oldHolder; - final View view = holder == null ? null : holder.itemView; - final RecyclerView.ViewHolder newHolder = changeInfo.newHolder; - final View newView = newHolder != null ? newHolder.itemView : null; - if (view != null) { - final ViewPropertyAnimatorCompat oldViewAnim = ViewCompat.animate(view).setDuration( - getChangeDuration()); - changeAnimations.add(changeInfo.oldHolder); - oldViewAnim.translationX(changeInfo.toX - changeInfo.fromX); - oldViewAnim.translationY(changeInfo.toY - changeInfo.fromY); - oldViewAnim.alpha(0).setListener(new VpaListenerAdapter() { - @Override - public void onAnimationStart(View view) { - dispatchChangeStarting(changeInfo.oldHolder, true); - } - - @Override - public void onAnimationEnd(View view) { - oldViewAnim.setListener(null); - resetViewProperties(view); - dispatchChangeFinished(changeInfo.oldHolder, true); - changeAnimations.remove(changeInfo.oldHolder); - dispatchFinishedWhenDone(); - } - }).start(); - } - if (newView != null) { - final ViewPropertyAnimatorCompat newViewAnimation = ViewCompat.animate(newView); - changeAnimations.add(changeInfo.newHolder); - newViewAnimation.translationX(0).translationY(0).setDuration(getChangeDuration()). - alpha(1).setListener(new VpaListenerAdapter() { - @Override - public void onAnimationStart(View view) { - dispatchChangeStarting(changeInfo.newHolder, false); - } - @Override - public void onAnimationEnd(View view) { - newViewAnimation.setListener(null); - resetViewProperties(view); - dispatchChangeFinished(changeInfo.newHolder, false); - changeAnimations.remove(changeInfo.newHolder); - dispatchFinishedWhenDone(); - } - }).start(); -} - } - - private void endChangeAnimation(List infoList, RecyclerView.ViewHolder item) { - for (int i = infoList.size() - 1; i >= 0; i--) { - final ChangeInfo changeInfo = infoList.get(i); - if (endChangeAnimationIfNecessary(changeInfo, item)) { - if (changeInfo.oldHolder == null && changeInfo.newHolder == null) { - infoList.remove(changeInfo); - } - } - } - } - - private void endChangeAnimationIfNecessary(ChangeInfo changeInfo) { - if (changeInfo.oldHolder != null) { - endChangeAnimationIfNecessary(changeInfo, changeInfo.oldHolder); - } - if (changeInfo.newHolder != null) { - endChangeAnimationIfNecessary(changeInfo, changeInfo.newHolder); - } - } - - private boolean endChangeAnimationIfNecessary(ChangeInfo changeInfo, RecyclerView.ViewHolder item) { - boolean oldItem = false; - if (changeInfo.newHolder == item) { - changeInfo.newHolder = null; - } else if (changeInfo.oldHolder == item) { - changeInfo.oldHolder = null; - oldItem = true; - } else { - return false; - } - resetViewProperties(item.itemView); - dispatchChangeFinished(item, oldItem); - return true; - } - - /** - * Called to reset all properties possibly animated by any and all defined animations. - */ - protected void resetViewProperties(View view) { - view.setTranslationX(0); - view.setTranslationY(0); - view.setAlpha(1); - } - - @Override - public void endAnimation(RecyclerView.ViewHolder item) { - - final View view = item.itemView; - // This calls dispatch*Finished, resets view properties, and removes item from current - // animations list if the view is currently being animated. - ViewCompat.animate(view).cancel(); - // TODO if some other animations are chained to end, how do we cancel them as well? - for (int i = pendingMoves.size() - 1; i >= 0; i--) { - final MoveInfo moveInfo = pendingMoves.get(i); - if (moveInfo.holder == item) { - resetViewProperties(view); - dispatchMoveFinished(item); - pendingMoves.remove(i); - } - } - endChangeAnimation(pendingChanges, item); - if (pendingRemovals.remove(item)) { - resetViewProperties(view); - dispatchRemoveFinished(item); - } - if (pendingAdditions.remove(item)) { - resetViewProperties(view); - dispatchAddFinished(item); - } - - for (int i = changesList.size() - 1; i >= 0; i--) { - final List changes = changesList.get(i); - endChangeAnimation(changes, item); - if (changes.isEmpty()) { - changesList.remove(i); - } - } - for (int i = movesList.size() - 1; i >= 0; i--) { - final List moves = movesList.get(i); - for (int j = moves.size() - 1; j >= 0; j--) { - final MoveInfo moveInfo = moves.get(j); - if (moveInfo.holder == item) { - resetViewProperties(view); - dispatchMoveFinished(item); - moves.remove(j); - if (moves.isEmpty()) { - movesList.remove(i); - } - break; - } - } - } - for (int i = additionsList.size() - 1; i >= 0; i--) { - final List additions = additionsList.get(i); - if (additions.remove(item)) { - resetViewProperties(view); - dispatchAddFinished(item); - if (additions.isEmpty()) { - additionsList.remove(i); - } - } - } - dispatchFinishedWhenDone(); - } - - protected void resetAnimation(RecyclerView.ViewHolder holder) { - AnimatorCompatHelper.clearInterpolator(holder.itemView); - endAnimation(holder); - } - - @Override - public boolean isRunning() { - return (!pendingAdditions.isEmpty() || - !pendingChanges.isEmpty() || - !pendingMoves.isEmpty() || - !pendingRemovals.isEmpty() || - !moveAnimations.isEmpty() || - !removeAnimations.isEmpty() || - !addAnimations.isEmpty() || - !changeAnimations.isEmpty() || - !movesList.isEmpty() || - !additionsList.isEmpty() || - !changesList.isEmpty()); - } - - /** - * Check the state of currently pending and running animations. If there are none - * pending/running, call {@link #dispatchAnimationsFinished()} to notify any - * listeners. - */ - protected void dispatchFinishedWhenDone() { - if (!isRunning()) { - dispatchAnimationsFinished(); - } - } - - @Override - public void endAnimations() { - int count = pendingMoves.size(); - for (int i = count - 1; i >= 0; i--) { - final MoveInfo item = pendingMoves.get(i); - resetViewProperties(item.holder.itemView); - dispatchMoveFinished(item.holder); - pendingMoves.remove(i); - } - count = pendingRemovals.size(); - for (int i = count - 1; i >= 0; i--) { - final RecyclerView.ViewHolder item = pendingRemovals.get(i); - resetViewProperties(item.itemView); - dispatchRemoveFinished(item); - pendingRemovals.remove(i); - } - count = pendingAdditions.size(); - for (int i = count - 1; i >= 0; i--) { - final RecyclerView.ViewHolder item = pendingAdditions.get(i); - resetViewProperties(item.itemView); - dispatchAddFinished(item); - pendingAdditions.remove(i); - } - count = pendingChanges.size(); - for (int i = count - 1; i >= 0; i--) { - endChangeAnimationIfNecessary(pendingChanges.get(i)); - } - pendingChanges.clear(); - if (!isRunning()) { - return; - } - - int listCount = movesList.size(); - for (int i = listCount - 1; i >= 0; i--) { - final List moves = movesList.get(i); - count = moves.size(); - for (int j = count - 1; j >= 0; j--) { - final MoveInfo moveInfo = moves.get(j); - final RecyclerView.ViewHolder item = moveInfo.holder; - resetViewProperties(item.itemView); - dispatchMoveFinished(item); - moves.remove(j); - if (moves.isEmpty()) { - movesList.remove(moves); - } - } - } - listCount = additionsList.size(); - for (int i = listCount - 1; i >= 0; i--) { - final List additions = additionsList.get(i); - count = additions.size(); - for (int j = count - 1; j >= 0; j--) { - final RecyclerView.ViewHolder item = additions.get(j); - resetViewProperties(item.itemView); - dispatchAddFinished(item); - additions.remove(j); - if (additions.isEmpty()) { - additionsList.remove(additions); - } - } - } - listCount = changesList.size(); - for (int i = listCount - 1; i >= 0; i--) { - final List changes = changesList.get(i); - count = changes.size(); - for (int j = count - 1; j >= 0; j--) { - endChangeAnimationIfNecessary(changes.get(j)); - if (changes.isEmpty()) { - changesList.remove(changes); - } - } - } - - cancelAll(removeAnimations); - cancelAll(moveAnimations); - cancelAll(addAnimations); - cancelAll(changeAnimations); - - dispatchAnimationsFinished(); - } - - public void cancelAll(List viewHolders) { - for (int i = viewHolders.size() - 1; i >= 0; i--) { - ViewCompat.animate(viewHolders.get(i).itemView).cancel(); - } - } - - /** - * {@inheritDoc} - *

- * If the payload list is not empty, DefaultItemAnimator returns true. - * When this is the case: - *

    - *
  • If you override - * {@link #animateChange(RecyclerView.ViewHolder, RecyclerView.ViewHolder, int, int, int, int)}, - * both ViewHolder arguments will be the same instance. - *
  • - *
  • - * If you are not overriding - * {@link #animateChange(RecyclerView.ViewHolder, RecyclerView.ViewHolder, int, int, int, int)}, - * then DefaultItemAnimator will call - * {@link #animateMove(RecyclerView.ViewHolder, int, int, int, int)} and run a move animation - * instead. - *
  • - *
- */ - @Override - public boolean canReuseUpdatedViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, - @NonNull List payloads) { - return !payloads.isEmpty() || super.canReuseUpdatedViewHolder(viewHolder, payloads); - } - - private class VpaListenerAdapter implements ViewPropertyAnimatorListener { - @Override - public void onAnimationStart(View view) {} - - // Note that onAnimationEnd is called (in addition to OnAnimationCancel) whenever an - // animation is canceled. - @Override - public void onAnimationEnd(View view) { - resetViewProperties(view); - view.animate().setListener(null); - } - - @Override - public void onAnimationCancel(View view) {} - } - - protected class DefaultRemoveVpaListener extends VpaListenerAdapter { - private final RecyclerView.ViewHolder viewHolder; - - public DefaultRemoveVpaListener(final RecyclerView.ViewHolder holder) { - viewHolder = holder; - } - - @Override - public void onAnimationStart(View view) { - removeAnimations.add(viewHolder); - dispatchRemoveStarting(viewHolder); - } - - @Override - public void onAnimationEnd(View view) { - removeAnimations.remove(viewHolder); - dispatchRemoveFinished(viewHolder); - dispatchFinishedWhenDone(); - super.onAnimationEnd(view); - } - } - - protected class DefaultAddVpaListener extends VpaListenerAdapter { - private final RecyclerView.ViewHolder viewHolder; - - public DefaultAddVpaListener(final RecyclerView.ViewHolder holder) { - viewHolder = holder; - } - - @Override - public void onAnimationStart(View view) { - addAnimations.add(viewHolder); - dispatchAddStarting(viewHolder); - } - - @Override - public void onAnimationEnd(View view) { - addAnimations.remove(viewHolder); - dispatchAddFinished(viewHolder); - dispatchFinishedWhenDone(); - super.onAnimationEnd(view); - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/widget/DoorHanger.java b/mobile/android/base/java/org/mozilla/gecko/widget/DoorHanger.java deleted file mode 100644 index 7d32278e43..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/widget/DoorHanger.java +++ /dev/null @@ -1,220 +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.widget; - -import android.content.Context; -import android.content.res.Resources; -import android.graphics.Bitmap; -import android.graphics.drawable.BitmapDrawable; -import android.support.v4.content.ContextCompat; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewStub; -import android.widget.Button; -import android.widget.ImageView; -import android.widget.LinearLayout; -import android.widget.TextView; -import org.json.JSONObject; -import org.mozilla.gecko.R; -import org.mozilla.gecko.Tabs; -import org.mozilla.gecko.Telemetry; -import org.mozilla.gecko.TelemetryContract; - -import java.util.Locale; - -public abstract class DoorHanger extends LinearLayout { - - public static DoorHanger Get(Context context, DoorhangerConfig config) { - final Type type = config.getType(); - switch (type) { - case LOGIN: - return new LoginDoorHanger(context, config); - case TRACKING: - return new ContentSecurityDoorHanger(context, config, type); - } - return new DefaultDoorHanger(context, config, type); - } - - // Doorhanger types created from Gecko are checked against enum strings to determine type. - public static enum Type { DEFAULT, LOGIN, TRACKING, GEOLOCATION, DESKTOPNOTIFICATION2, WEBRTC, VIBRATION } - - public interface OnButtonClickListener { - public void onButtonClick(JSONObject response, DoorHanger doorhanger); - } - - private static final String LOGTAG = "GeckoDoorHanger"; - - // Divider between doorhangers. - private final View mDivider; - - private final Button mNegativeButton; - private final Button mPositiveButton; - protected final OnButtonClickListener mOnButtonClickListener; - - // The tab this doorhanger is associated with. - private final int mTabId; - - // DoorHanger identifier. - private final String mIdentifier; - - protected final Type mType; - - protected final ImageView mIcon; - protected final TextView mLink; - protected final TextView mDoorhangerTitle; - - protected final Context mContext; - protected final Resources mResources; - - protected int mDividerColor; - - protected boolean mPersistWhileVisible; - protected int mPersistenceCount; - protected long mTimeout; - - protected DoorHanger(Context context, DoorhangerConfig config, Type type) { - super(context); - - mContext = context; - mResources = context.getResources(); - mTabId = config.getTabId(); - mIdentifier = config.getId(); - mType = type; - - LayoutInflater.from(context).inflate(R.layout.doorhanger, this); - setOrientation(VERTICAL); - - mDivider = findViewById(R.id.divider_doorhanger); - mIcon = (ImageView) findViewById(R.id.doorhanger_icon); - mLink = (TextView) findViewById(R.id.doorhanger_link); - mDoorhangerTitle = (TextView) findViewById(R.id.doorhanger_title); - - mNegativeButton = (Button) findViewById(R.id.doorhanger_button_negative); - mPositiveButton = (Button) findViewById(R.id.doorhanger_button_positive); - mOnButtonClickListener = config.getButtonClickListener(); - - mDividerColor = ContextCompat.getColor(context, R.color.toolbar_divider_grey); - - final ViewStub contentStub = (ViewStub) findViewById(R.id.content); - contentStub.setLayoutResource(getContentResource()); - contentStub.inflate(); - - final String typeExtra = mType.toString().toLowerCase(Locale.US); - Telemetry.sendUIEvent(TelemetryContract.Event.SHOW, TelemetryContract.Method.DOORHANGER, typeExtra); - } - - protected abstract int getContentResource(); - - protected abstract void loadConfig(DoorhangerConfig config); - - protected void setOptions(final JSONObject options) { - final int persistence = options.optInt("persistence"); - if (persistence > 0) { - mPersistenceCount = persistence; - } - - mPersistWhileVisible = options.optBoolean("persistWhileVisible"); - - final long timeout = options.optLong("timeout"); - if (timeout > 0) { - mTimeout = timeout; - } - } - - protected void addButtonsToLayout(DoorhangerConfig config) { - final DoorhangerConfig.ButtonConfig negativeButtonConfig = config.getNegativeButtonConfig(); - final DoorhangerConfig.ButtonConfig positiveButtonConfig = config.getPositiveButtonConfig(); - - if (negativeButtonConfig != null) { - mNegativeButton.setText(negativeButtonConfig.label); - mNegativeButton.setOnClickListener(makeOnButtonClickListener(negativeButtonConfig.callback, "negative")); - mNegativeButton.setVisibility(VISIBLE); - } - - if (positiveButtonConfig != null) { - mPositiveButton.setText(positiveButtonConfig.label); - mPositiveButton.setOnClickListener(makeOnButtonClickListener(positiveButtonConfig.callback, "positive")); - mPositiveButton.setVisibility(VISIBLE); - } - } - - public int getTabId() { - return mTabId; - } - - public String getIdentifier() { - return mIdentifier; - } - - public void showDivider() { - mDivider.setVisibility(View.VISIBLE); - } - - public void hideDivider() { - mDivider.setVisibility(View.GONE); - } - - public void setIcon(int resId) { - mIcon.setImageResource(resId); - mIcon.setVisibility(View.VISIBLE); - } - - protected void addLink(String label, final String url) { - mLink.setText(label); - mLink.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View view) { - final String typeExtra = mType.toString().toLowerCase(Locale.US); - Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.DOORHANGER, typeExtra); - Tabs.getInstance().loadUrlInTab(url); - } - }); - mLink.setVisibility(VISIBLE); - } - - protected abstract OnClickListener makeOnButtonClickListener(final int id, final String telemetryExtra); - - /* - * Checks with persistence and timeout options to see if it's okay to remove a doorhanger. - * - * @param isShowing Whether or not this doorhanger is currently visible to the user. - * (e.g. the DoorHanger view might be VISIBLE, but its parent could be hidden) - */ - public boolean shouldRemove(boolean isShowing) { - if (mPersistWhileVisible && isShowing) { - // We still want to decrement mPersistence, even if the popup is showing - if (mPersistenceCount != 0) - mPersistenceCount--; - return false; - } - - // If persistence is set to -1, the doorhanger will never be - // automatically removed. - if (mPersistenceCount != 0) { - mPersistenceCount--; - return false; - } - - if (System.currentTimeMillis() <= mTimeout) { - return false; - } - - return true; - } - - public void showTitle(Bitmap favicon, String title) { - mDoorhangerTitle.setText(title); - mDoorhangerTitle.setCompoundDrawablesWithIntrinsicBounds(new BitmapDrawable(getResources(), favicon), null, null, null); - if (favicon != null) { - mDoorhangerTitle.setCompoundDrawablePadding((int) mContext.getResources().getDimension(R.dimen.doorhanger_drawable_padding)); - } - mDoorhangerTitle.setVisibility(VISIBLE); - } - - public void hideTitle() { - mDoorhangerTitle.setVisibility(GONE); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/widget/DoorhangerConfig.java b/mobile/android/base/java/org/mozilla/gecko/widget/DoorhangerConfig.java deleted file mode 100644 index 98f1e57e14..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/widget/DoorhangerConfig.java +++ /dev/null @@ -1,127 +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.widget; - -import android.util.Log; -import org.json.JSONException; -import org.json.JSONObject; - -import org.mozilla.gecko.widget.DoorHanger.Type; - -public class DoorhangerConfig { - - public static class Link { - public final String label; - public final String url; - - private Link(String label, String url) { - this.label = label; - this.url = url; - } - } - - public static class ButtonConfig { - public final String label; - public final int callback; - - public ButtonConfig(String label, int callback) { - this.label = label; - this.callback = callback; - } - } - private static final String LOGTAG = "DoorhangerConfig"; - - private final int tabId; - private final String id; - private final DoorHanger.OnButtonClickListener buttonClickListener; - private final DoorHanger.Type type; - private String message; - private JSONObject options; - private Link link; - private ButtonConfig positiveButtonConfig; - private ButtonConfig negativeButtonConfig; - - public DoorhangerConfig(Type type, DoorHanger.OnButtonClickListener listener) { - // XXX: This should only be used by SiteIdentityPopup doorhangers which - // don't need tab or id references, until bug 1141904 unifies doorhangers. - - this(-1, null, type, listener); - } - - public DoorhangerConfig(int tabId, String id, DoorHanger.Type type, DoorHanger.OnButtonClickListener buttonClickListener) { - this.tabId = tabId; - this.id = id; - this.type = type; - this.buttonClickListener = buttonClickListener; - } - - public int getTabId() { - return tabId; - } - - public String getId() { - return id; - } - - public Type getType() { - return type; - } - - public void setMessage(String message) { - this.message = message; - } - - public String getMessage() { - return message; - } - - public void setOptions(JSONObject options) { - this.options = options; - - // Set link if there is a link provided in options. - final JSONObject linkObj = options.optJSONObject("link"); - if (linkObj != null) { - try { - setLink(linkObj.getString("label"), linkObj.getString("url")); - } catch (JSONException e) { - Log.e(LOGTAG, "Malformed link object in options"); - } - } - } - - public JSONObject getOptions() { - return options; - } - - public void setButton(String label, int callbackId, boolean isPositive) { - final ButtonConfig buttonConfig = new ButtonConfig(label, callbackId); - if (isPositive) { - positiveButtonConfig = buttonConfig; - } else { - negativeButtonConfig = buttonConfig; - } - } - - public ButtonConfig getPositiveButtonConfig() { - return positiveButtonConfig; - } - - public ButtonConfig getNegativeButtonConfig() { - return negativeButtonConfig; - } - - public DoorHanger.OnButtonClickListener getButtonClickListener() { - return this.buttonClickListener; - } - - public void setLink(String label, String url) { - this.link = new Link(label, url); - } - - public Link getLink() { - return link; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/widget/EllipsisTextView.java b/mobile/android/base/java/org/mozilla/gecko/widget/EllipsisTextView.java deleted file mode 100644 index 44f88e668f..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/widget/EllipsisTextView.java +++ /dev/null @@ -1,65 +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.widget; - -import org.mozilla.gecko.R; - -import android.content.Context; -import android.content.res.TypedArray; -import android.util.AttributeSet; -import android.widget.TextView; - -/** - * Text view that correctly handles maxLines and ellipsizing for Android < 2.3. - */ -public class EllipsisTextView extends TextView { - private final String ellipsis; - - private final int maxLines; - private CharSequence originalText; - - public EllipsisTextView(Context context) { - this(context, null); - } - - public EllipsisTextView(Context context, AttributeSet attrs) { - this(context, attrs, android.R.attr.textViewStyle); - } - - public EllipsisTextView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - - ellipsis = getResources().getString(R.string.ellipsis); - - TypedArray a = context.getTheme() - .obtainStyledAttributes(attrs, R.styleable.EllipsisTextView, 0, 0); - maxLines = a.getInteger(R.styleable.EllipsisTextView_ellipsizeAtLine, 1); - a.recycle(); - } - - public void setOriginalText(CharSequence text) { - originalText = text; - setText(text); - } - - @Override - public void onLayout(boolean changed, int left, int top, int right, int bottom) { - super.onLayout(changed, left, top, right, bottom); - - // There is extra space, start over with the original text - if (getLineCount() < maxLines) { - setText(originalText); - } - - // If we are over the max line attribute, ellipsize - if (getLineCount() > maxLines) { - final int endIndex = getLayout().getLineEnd(maxLines - 1) - 1 - ellipsis.length(); - final String text = getText().subSequence(0, endIndex) + ellipsis; - // Make sure that we don't change originalText - setText(text); - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/widget/ExternalIntentDuringPrivateBrowsingPromptFragment.java b/mobile/android/base/java/org/mozilla/gecko/widget/ExternalIntentDuringPrivateBrowsingPromptFragment.java deleted file mode 100644 index b4d1e13d96..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/widget/ExternalIntentDuringPrivateBrowsingPromptFragment.java +++ /dev/null @@ -1,106 +0,0 @@ -// 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.widget; - -import org.mozilla.gecko.ActivityHandlerHelper; -import org.mozilla.gecko.GeckoApplication; -import org.mozilla.gecko.R; -import org.mozilla.gecko.Tab; -import org.mozilla.gecko.Tabs; - -import android.app.Dialog; -import android.content.Context; -import android.content.DialogInterface; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.content.pm.ResolveInfo; -import android.os.Bundle; -import android.support.v4.app.DialogFragment; -import android.support.v4.app.FragmentManager; -import android.support.v7.app.AlertDialog; -import android.util.Log; - -import java.util.List; - -/** - * A DialogFragment to contain a dialog that appears when the user clicks an Intent:// URI during private browsing. The - * dialog appears to notify the user that a clicked link will open in an external application, potentially leaking their - * browsing history. - */ -public class ExternalIntentDuringPrivateBrowsingPromptFragment extends DialogFragment { - private static final String LOGTAG = ExternalIntentDuringPrivateBrowsingPromptFragment.class.getSimpleName(); - private static final String FRAGMENT_TAG = "ExternalIntentPB"; - - private static final String KEY_APPLICATION_NAME = "matchingApplicationName"; - private static final String KEY_INTENT = "intent"; - - @Override - public Dialog onCreateDialog(final Bundle savedInstanceState) { - final Bundle args = getArguments(); - final CharSequence matchingApplicationName = args.getCharSequence(KEY_APPLICATION_NAME); - final Intent intent = args.getParcelable(KEY_INTENT); - - final Context context = getActivity(); - final String promptMessage = context.getString(R.string.intent_uri_private_browsing_prompt, matchingApplicationName); - - final AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); - builder.setMessage(promptMessage) - .setTitle(intent.getDataString()) - .setPositiveButton(R.string.button_yes, new DialogInterface.OnClickListener() { - public void onClick(final DialogInterface dialog, final int id) { - context.startActivity(intent); - } - }) - .setNegativeButton(R.string.button_no, null /* we do nothing if the user rejects */ ); - return builder.create(); - } - - @Override - public void onDestroy() { - super.onDestroy(); - - GeckoApplication.watchReference(getActivity(), this); - } - - /** - * @return true if the Activity is started or a dialog is shown. false if the Activity fails to start. - */ - public static boolean showDialogOrAndroidChooser(final Context context, final FragmentManager fragmentManager, - final Intent intent) { - final Tab selectedTab = Tabs.getInstance().getSelectedTab(); - if (selectedTab == null || !selectedTab.isPrivate()) { - return ActivityHandlerHelper.startIntentAndCatch(LOGTAG, context, intent); - } - - final PackageManager pm = context.getPackageManager(); - final List matchingActivities = pm.queryIntentActivities(intent, 0); - if (matchingActivities.size() == 1) { - final ExternalIntentDuringPrivateBrowsingPromptFragment fragment = new ExternalIntentDuringPrivateBrowsingPromptFragment(); - - final Bundle args = new Bundle(2); - args.putCharSequence(KEY_APPLICATION_NAME, matchingActivities.get(0).loadLabel(pm)); - args.putParcelable(KEY_INTENT, intent); - fragment.setArguments(args); - - fragment.show(fragmentManager, FRAGMENT_TAG); - // We don't know the results of the user interaction with the fragment so just return true. - return true; - } else if (matchingActivities.size() > 1) { - // We want to show the Android Intent Chooser. However, we have no way of distinguishing regular tabs from - // private tabs to the chooser. Thus, if a user chooses "Always" in regular browsing mode, the chooser will - // not be shown and the URL will be opened. Therefore we explicitly show the chooser (which notably does not - // have an "Always" option). - final String androidChooserTitle = - context.getResources().getString(R.string.intent_uri_private_browsing_multiple_match_title); - final Intent chooserIntent = Intent.createChooser(intent, androidChooserTitle); - return ActivityHandlerHelper.startIntentAndCatch(LOGTAG, context, chooserIntent); - } else { - // Normally, we show about:neterror when an Intent does not resolve - // but we don't have the references here to do that so log instead. - Log.w(LOGTAG, "showDialogOrAndroidChooser unexpectedly called with Intent that does not resolve"); - return false; - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/widget/FadedMultiColorTextView.java b/mobile/android/base/java/org/mozilla/gecko/widget/FadedMultiColorTextView.java deleted file mode 100644 index 08bb55ef63..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/widget/FadedMultiColorTextView.java +++ /dev/null @@ -1,108 +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.widget; - -import org.mozilla.gecko.R; - -import android.content.Context; -import android.content.res.ColorStateList; -import android.content.res.TypedArray; -import android.graphics.Canvas; -import android.graphics.Color; -import android.graphics.LinearGradient; -import android.graphics.Paint; -import android.graphics.Shader; -import android.util.AttributeSet; - -/** - * Fades the end of the text by gecko:fadeWidth amount, - * if the text is too long and requires an ellipsis. - * - * This implementation is an improvement over Android's built-in fadingEdge - * but potentially slower than the {@link org.mozilla.gecko.widget.FadedSingleColorTextView}. - * It works for text of multiple colors but only one background color. It works by - * drawing a gradient rectangle with the background color over the text, fading it out. - */ -public class FadedMultiColorTextView extends FadedTextView { - private final ColorStateList fadeBackgroundColorList; - - private final Paint fadePaint; - private FadedTextGradient backgroundGradient; - - public FadedMultiColorTextView(Context context, AttributeSet attrs) { - super(context, attrs); - - fadePaint = new Paint(); - - final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.FadedMultiColorTextView); - fadeBackgroundColorList = - a.getColorStateList(R.styleable.FadedMultiColorTextView_fadeBackgroundColor); - a.recycle(); - } - - @Override - public void onDraw(Canvas canvas) { - super.onDraw(canvas); - - final boolean needsEllipsis = needsEllipsis(); - if (needsEllipsis) { - final int right = getWidth() - getCompoundPaddingRight(); - final float left = right - fadeWidth; - - updateGradientShader(needsEllipsis, right); - - // Shrink height of gradient to prevent it overlaying parent view border. - // The shrunk size just nee to cover the text itself. - final float density = getResources().getDisplayMetrics().density; - final float h = Math.abs(fadePaint.getFontMetrics().top) + 1; - final float l = fadePaint.getFontMetrics().bottom + 1; - final float top = getBaseline() - h * density; - final float bottom = getBaseline() + l * density; - - canvas.drawRect(left, top, right, bottom, fadePaint); - } - } - - private void updateGradientShader(final boolean needsEllipsis, final int gradientEndRight) { - final int backgroundColor = - fadeBackgroundColorList.getColorForState(getDrawableState(), Color.RED); - - final boolean needsNewGradient = (backgroundGradient == null || - backgroundGradient.getBackgroundColor() != backgroundColor || - backgroundGradient.getEndRight() != gradientEndRight); - - if (needsEllipsis && needsNewGradient) { - backgroundGradient = new FadedTextGradient(gradientEndRight, fadeWidth, backgroundColor); - fadePaint.setShader(backgroundGradient); - } - } - - private static class FadedTextGradient extends LinearGradient { - private final int endRight; - private final int backgroundColor; - - public FadedTextGradient(final int gradientEndRight, final int fadeWidth, - final int backgroundColor) { - super(gradientEndRight - fadeWidth, 0, gradientEndRight, 0, - getColorWithZeroedAlpha(backgroundColor), backgroundColor, Shader.TileMode.CLAMP); - - this.endRight = gradientEndRight; - this.backgroundColor = backgroundColor; - } - - private static int getColorWithZeroedAlpha(final int color) { - return Color.argb(0, Color.red(color), Color.green(color), Color.blue(color)); - } - - public int getEndRight() { - return endRight; - } - - public int getBackgroundColor() { - return backgroundColor; - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/widget/FadedSingleColorTextView.java b/mobile/android/base/java/org/mozilla/gecko/widget/FadedSingleColorTextView.java deleted file mode 100644 index 866b7ecbd0..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/widget/FadedSingleColorTextView.java +++ /dev/null @@ -1,74 +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.widget; - -import android.content.Context; -import android.graphics.Canvas; -import android.graphics.LinearGradient; -import android.graphics.Shader; -import android.util.AttributeSet; - -/** - * Fades the end of the text by gecko:fadeWidth amount, - * if the text is too long and requires an ellipsis. - * - * This implementation is an improvement over Android's built-in fadingEdge - * and the fastest of Fennec's implementations. However, it only works for - * text of one color. It works by applying a linear gradient directly to the text. - */ -public class FadedSingleColorTextView extends FadedTextView { - // Shader for the fading edge. - private FadedTextGradient mTextGradient; - - public FadedSingleColorTextView(Context context, AttributeSet attrs) { - super(context, attrs); - } - - private void updateGradientShader() { - final int color = getCurrentTextColor(); - final int width = getAvailableWidth(); - - final boolean needsNewGradient = (mTextGradient == null || - mTextGradient.getColor() != color || - mTextGradient.getWidth() != width); - - final boolean needsEllipsis = needsEllipsis(); - if (needsEllipsis && needsNewGradient) { - mTextGradient = new FadedTextGradient(width, fadeWidth, color); - } - - getPaint().setShader(needsEllipsis ? mTextGradient : null); - } - - @Override - public void onDraw(Canvas canvas) { - updateGradientShader(); - super.onDraw(canvas); - } - - private static class FadedTextGradient extends LinearGradient { - private final int mWidth; - private final int mColor; - - public FadedTextGradient(int width, int fadeWidth, int color) { - super(0, 0, width, 0, - new int[] { color, color, 0x0 }, - new float[] { 0, ((float) (width - fadeWidth) / width), 1.0f }, - Shader.TileMode.CLAMP); - - mWidth = width; - mColor = color; - } - - public int getWidth() { - return mWidth; - } - - public int getColor() { - return mColor; - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/widget/FadedTextView.java b/mobile/android/base/java/org/mozilla/gecko/widget/FadedTextView.java deleted file mode 100644 index e104330832..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/widget/FadedTextView.java +++ /dev/null @@ -1,48 +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.widget; - -import android.content.Context; -import android.content.res.TypedArray; -import android.text.Layout; -import android.util.AttributeSet; - -import org.mozilla.gecko.R; -import org.mozilla.gecko.widget.themed.ThemedTextView; - -/** - * An implementation of FadedTextView should fade the end of the text - * by gecko:fadeWidth amount, if the text is too long and requires an ellipsis. - */ -public abstract class FadedTextView extends ThemedTextView { - // Width of the fade effect from end of the view. - protected final int fadeWidth; - - public FadedTextView(final Context context, final AttributeSet attrs) { - super(context, attrs); - - setSingleLine(true); - setEllipsize(null); - - final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.FadedTextView); - fadeWidth = a.getDimensionPixelSize(R.styleable.FadedTextView_fadeWidth, 0); - a.recycle(); - } - - protected int getAvailableWidth() { - return getWidth() - getCompoundPaddingLeft() - getCompoundPaddingRight(); - } - - protected boolean needsEllipsis() { - final int width = getAvailableWidth(); - if (width <= 0) { - return false; - } - - final Layout layout = getLayout(); - return (layout != null && layout.getLineWidth(0) > width); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/widget/FaviconView.java b/mobile/android/base/java/org/mozilla/gecko/widget/FaviconView.java deleted file mode 100644 index 4652345b4d..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/widget/FaviconView.java +++ /dev/null @@ -1,268 +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.widget; - -import org.mozilla.gecko.R; -import org.mozilla.gecko.icons.IconCallback; -import org.mozilla.gecko.icons.IconResponse; - -import android.content.Context; -import android.content.res.TypedArray; -import android.graphics.Bitmap; -import android.graphics.Canvas; -import android.graphics.Paint; -import android.graphics.RectF; -import android.util.AttributeSet; -import android.util.DisplayMetrics; -import android.util.TypedValue; -import android.widget.ImageView; - -import java.lang.ref.WeakReference; - -/** - * Special version of ImageView for favicons. - * Displays solid colour background around Favicon to fill space not occupied by the icon. Colour - * selected is the dominant colour of the provided Favicon. - */ -public class FaviconView extends ImageView { - private static final String LOGTAG = "GeckoFaviconView"; - - private static String DEFAULT_FAVICON_KEY = FaviconView.class.getSimpleName() + "DefaultFavicon"; - - // Default x/y-radius of the oval used to round the corners of the background (dp) - private static final int DEFAULT_CORNER_RADIUS_DP = 4; - - private Bitmap mIconBitmap; - - // Reference to the unscaled bitmap, if any, to prevent repeated assignments of the same bitmap - // to the view from causing repeated rescalings (Some of the callers do this) - private Bitmap mUnscaledBitmap; - - private int mActualWidth; - private int mActualHeight; - - // Flag indicating if the most recently assigned image is considered likely to need scaling. - private boolean mScalingExpected; - - // Dominant color of the favicon. - private int mDominantColor; - - // Paint for drawing the background. - private static final Paint sBackgroundPaint; - - // Size of the background rectangle. - private final RectF mBackgroundRect; - - // The x/y-radius of the oval used to round the corners of the background (pixels) - private final float mBackgroundCornerRadius; - - // Type of the border whose value is defined in attrs.xml . - private final boolean isDominantBorderEnabled; - - // boolean switch for overriding scaletype, whose value is defined in attrs.xml . - private final boolean isOverrideScaleTypeEnabled; - - // boolean switch for disabling rounded corners, value defined in attrs.xml . - private final boolean areRoundCornersEnabled; - - // Initializing the static paints. - static { - sBackgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG); - sBackgroundPaint.setStyle(Paint.Style.FILL); - } - - public FaviconView(Context context, AttributeSet attrs) { - super(context, attrs); - TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.FaviconView, 0, 0); - - try { - isDominantBorderEnabled = a.getBoolean(R.styleable.FaviconView_dominantBorderEnabled, true); - isOverrideScaleTypeEnabled = a.getBoolean(R.styleable.FaviconView_overrideScaleType, true); - areRoundCornersEnabled = a.getBoolean(R.styleable.FaviconView_enableRoundCorners, true); - } finally { - a.recycle(); - } - - if (isOverrideScaleTypeEnabled) { - setScaleType(ImageView.ScaleType.CENTER); - } - - final DisplayMetrics metrics = getResources().getDisplayMetrics(); - - mBackgroundRect = new RectF(0, 0, 0, 0); - mBackgroundCornerRadius = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, DEFAULT_CORNER_RADIUS_DP, metrics); - } - - @Override - protected void onSizeChanged(int w, int h, int oldw, int oldh) { - super.onSizeChanged(w, h, oldw, oldh); - - // No point rechecking the image if there hasn't really been any change. - if (w == mActualWidth && h == mActualHeight) { - return; - } - - mActualWidth = w; - mActualHeight = h; - - mBackgroundRect.right = w; - mBackgroundRect.bottom = h; - - formatImage(); - } - - @Override - public void onDraw(Canvas canvas) { - if (isDominantBorderEnabled) { - sBackgroundPaint.setColor(mDominantColor & 0x7FFFFFFF); - - if (areRoundCornersEnabled) { - canvas.drawRoundRect(mBackgroundRect, mBackgroundCornerRadius, mBackgroundCornerRadius, sBackgroundPaint); - } else { - canvas.drawRect(mBackgroundRect, sBackgroundPaint); - } - } - - super.onDraw(canvas); - } - - /** - * 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() { - // We're waiting for both onSizeChanged and updateImage to be called before scaling. - if (mIconBitmap == null || mActualWidth == 0 || mActualHeight == 0) { - showNoImage(); - return; - } - - if (mScalingExpected && mActualWidth != mIconBitmap.getWidth()) { - scaleBitmap(); - // Don't scale the image every time something changes. - mScalingExpected = false; - } - - setImageBitmap(mIconBitmap); - - // After scaling, determine if we have empty space around the scaled image which we need to - // fill with the coloured background. If applicable, show it. - // We assume Favicons are still squares and only bother with the background if more than 3px - // of it would be displayed. - if (Math.abs(mIconBitmap.getWidth() - mActualWidth) < 3) { - mDominantColor = 0; - } - } - - private void scaleBitmap() { - // If the Favicon can be resized to fill the view exactly without an enlargment of more than - // a factor of two, do so. - int doubledSize = mIconBitmap.getWidth() * 2; - if (mActualWidth > doubledSize) { - // If the view is more than twice the size of the image, just double the image size - // and do the rest with padding. - mIconBitmap = Bitmap.createScaledBitmap(mIconBitmap, doubledSize, doubledSize, true); - } else { - // Otherwise, scale the image to fill the view. - mIconBitmap = Bitmap.createScaledBitmap(mIconBitmap, mActualWidth, mActualWidth, true); - } - } - - /** - * Sets the icon displayed in this Favicon view to the bitmap provided. If the size of the view - * has been set, the display will be updated right away, otherwise the update will be deferred - * until then. The key provided is used to cache the result of the calculation of the dominant - * colour of the provided image - this value is used to draw the coloured background in this view - * if the icon is not large enough to fill it. - * - * @param allowScaling If true, allows the provided bitmap to be scaled by this FaviconView. - * Typically, you should prefer using Favicons obtained via the caching system - * (Favicons class), so as to exploit caching. - */ - private void updateImageInternal(IconResponse response, boolean allowScaling) { - // Reassigning the same bitmap? Don't bother. - if (mUnscaledBitmap == response.getBitmap()) { - return; - } - mUnscaledBitmap = response.getBitmap(); - mIconBitmap = response.getBitmap(); - mDominantColor = response.getColor(); - mScalingExpected = allowScaling; - - // Possibly update the display. - formatImage(); - } - - private void showNoImage() { - setImageDrawable(null); - mDominantColor = 0; - } - - /** - * Clear image and background shown by this view. - */ - public void clearImage() { - showNoImage(); - mUnscaledBitmap = null; - mIconBitmap = null; - mDominantColor = 0; - mScalingExpected = false; - } - - /** - * 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 - * which the scaling logic here is applied. - */ - public void updateAndScaleImage(IconResponse response) { - updateImageInternal(response, true); - } - - /** - * Update the image displayed in the Favicon view without scaling. Images larger than the view - * will be centrally cropped. Images smaller than the view will be placed centrally and the - * extra space filled with the dominant colour of the provided image. - */ - public void updateImage(IconResponse response) { - updateImageInternal(response, false); - } - - public Bitmap getBitmap() { - return mIconBitmap; - } - - /** - * Create an IconCallback implementation that will update this view after an icon has been loaded. - */ - public IconCallback createIconCallback() { - return new Callback(this); - } - - private static class Callback implements IconCallback { - private final WeakReference viewReference; - - private Callback(FaviconView view) { - this.viewReference = new WeakReference(view); - } - - @Override - public void onIconResponse(IconResponse response) { - final FaviconView view = viewReference.get(); - if (view == null) { - return; - } - - view.updateImage(response); - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/widget/FilledCardView.java b/mobile/android/base/java/org/mozilla/gecko/widget/FilledCardView.java deleted file mode 100644 index f1662896e9..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/widget/FilledCardView.java +++ /dev/null @@ -1,39 +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.widget; - -import android.content.Context; -import android.support.v7.widget.CardView; -import android.util.AttributeSet; - -import org.mozilla.gecko.AppConstants; - -/** - * CardView that ensures its content can fill the entire card. Use this instead of CardView - * if you want to fill the card with e.g. images, backgrounds, etc. - * - * On API < 21, CardView content isn't clipped for performance reasons. We work around this by disabling - * rounded corners on those devices. - */ -public class FilledCardView extends CardView { - - public FilledCardView(Context context, AttributeSet attrs) { - super(context, attrs); - - // Disable corners on < lollipop: - // CardView only supports clipping content on API >= 21 (for performance reasons). Without - // content clipping, any cards that provide their own content that fills the card will look - // ugly: by default there is a 2px white edge along the top and sides (i.e. an inset corresponding - // to the corner radius), if we disable the inset then the corners overlap. - // It's possible to implement custom clipping, however given that the support library - // chose not to support this for performance reasons, we too have chosen to just disable - // corners on < 21, see Bug 1271428. - if (AppConstants.Versions.preLollipop) { - setRadius(0); - } - - setUseCompatPadding(true); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/widget/FlowLayout.java b/mobile/android/base/java/org/mozilla/gecko/widget/FlowLayout.java deleted file mode 100644 index 042e748510..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/widget/FlowLayout.java +++ /dev/null @@ -1,91 +0,0 @@ -/* 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.widget; - -import org.mozilla.gecko.R; - -import android.content.Context; -import android.content.res.TypedArray; -import android.util.AttributeSet; -import android.view.View; -import android.view.ViewGroup; - -public class FlowLayout extends ViewGroup { - private int mSpacing; - - public FlowLayout(Context context) { - super(context); - } - - public FlowLayout(Context context, AttributeSet attrs) { - super(context, attrs); - TypedArray a = context.obtainStyledAttributes(attrs, org.mozilla.gecko.R.styleable.FlowLayout); - mSpacing = a.getDimensionPixelSize(R.styleable.FlowLayout_spacing, (int) context.getResources().getDimension(R.dimen.flow_layout_spacing)); - a.recycle(); - } - - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - final int parentWidth = MeasureSpec.getSize(widthMeasureSpec); - final int childCount = getChildCount(); - int rowWidth = 0; - int totalWidth = 0; - int totalHeight = 0; - boolean firstChild = true; - - for (int i = 0; i < childCount; i++) { - final View child = getChildAt(i); - if (child.getVisibility() == GONE) - continue; - - measureChild(child, widthMeasureSpec, heightMeasureSpec); - - final int childWidth = child.getMeasuredWidth(); - final int childHeight = child.getMeasuredHeight(); - - if (firstChild || (rowWidth + childWidth > parentWidth)) { - rowWidth = 0; - totalHeight += childHeight; - if (!firstChild) - totalHeight += mSpacing; - firstChild = false; - } - - rowWidth += childWidth; - - if (rowWidth > totalWidth) - totalWidth = rowWidth; - - rowWidth += mSpacing; - } - - setMeasuredDimension(totalWidth, totalHeight); - } - - @Override - protected void onLayout(boolean changed, int l, int t, int r, int b) { - final int childCount = getChildCount(); - final int totalWidth = r - l; - int x = 0; - int y = 0; - int prevChildHeight = 0; - - for (int i = 0; i < childCount; i++) { - final View child = getChildAt(i); - if (child.getVisibility() == GONE) - continue; - - final int childWidth = child.getMeasuredWidth(); - final int childHeight = child.getMeasuredHeight(); - if (x + childWidth > totalWidth) { - x = 0; - y += prevChildHeight + mSpacing; - } - prevChildHeight = childHeight; - child.layout(x, y, x + childWidth, y + childHeight); - x += childWidth + mSpacing; - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/widget/GeckoActionProvider.java b/mobile/android/base/java/org/mozilla/gecko/widget/GeckoActionProvider.java deleted file mode 100644 index d864792a6e..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/widget/GeckoActionProvider.java +++ /dev/null @@ -1,360 +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.widget; - -import android.app.Activity; -import android.net.Uri; -import android.support.design.widget.Snackbar; -import android.util.Base64; -import android.view.Menu; - -import org.mozilla.gecko.GeckoApp; -import org.mozilla.gecko.R; -import org.mozilla.gecko.SnackbarBuilder; -import org.mozilla.gecko.Telemetry; -import org.mozilla.gecko.TelemetryContract; -import org.mozilla.gecko.overlays.ui.ShareDialog; -import org.mozilla.gecko.menu.MenuItemSwitcherLayout; -import org.mozilla.gecko.util.IOUtils; -import org.mozilla.gecko.util.IntentUtils; -import org.mozilla.gecko.util.ThreadUtils; - -import android.content.Context; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.content.pm.ResolveInfo; -import android.view.MenuItem; -import android.view.MenuItem.OnMenuItemClickListener; -import android.view.SubMenu; -import android.view.View; -import android.view.View.OnClickListener; -import android.text.TextUtils; -import android.webkit.MimeTypeMap; -import android.webkit.URLUtil; - -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.net.URL; -import java.util.ArrayList; -import java.util.HashMap; - -public class GeckoActionProvider { - private static final int MAX_HISTORY_SIZE_DEFAULT = 2; - - /** - * A listener to know when a target was selected. - * When setting a provider, the activity can listen to this, - * to close the menu. - */ - public interface OnTargetSelectedListener { - public void onTargetSelected(); - } - - final Context mContext; - - public final static String DEFAULT_MIME_TYPE = "text/plain"; - - public static final String DEFAULT_HISTORY_FILE_NAME = "history.xml"; - - // History file. - String mHistoryFileName = DEFAULT_HISTORY_FILE_NAME; - - OnTargetSelectedListener mOnTargetListener; - - private final Callbacks mCallbacks = new Callbacks(); - - private static final HashMap mProviders = new HashMap(); - - private static String getFilenameFromMimeType(String mimeType) { - String[] mime = mimeType.split("/"); - - // All text mimetypes use the default provider - if ("text".equals(mime[0])) { - return DEFAULT_HISTORY_FILE_NAME; - } - - return "history-" + mime[0] + ".xml"; - } - - // Gets the action provider for a particular mimetype - public static GeckoActionProvider getForType(String mimeType, Context context) { - if (!mProviders.keySet().contains(mimeType)) { - GeckoActionProvider provider = new GeckoActionProvider(context); - - // For empty types, we just return a default provider - if (TextUtils.isEmpty(mimeType)) { - return provider; - } - - provider.setHistoryFileName(getFilenameFromMimeType(mimeType)); - mProviders.put(mimeType, provider); - } - return mProviders.get(mimeType); - } - - public GeckoActionProvider(Context context) { - mContext = context; - } - - /** - * Creates the action view using the default history size. - */ - public View onCreateActionView(final ActionViewType viewType) { - return onCreateActionView(MAX_HISTORY_SIZE_DEFAULT, viewType); - } - - public View onCreateActionView(final int maxHistorySize, final ActionViewType viewType) { - // Create the view and set its data model. - ActivityChooserModel dataModel = ActivityChooserModel.get(mContext, mHistoryFileName); - final MenuItemSwitcherLayout view; - switch (viewType) { - case DEFAULT: - view = new MenuItemSwitcherLayout(mContext, null); - break; - - case CONTEXT_MENU: - view = new MenuItemSwitcherLayout(mContext, null); - view.initContextMenuStyles(); - break; - - default: - throw new IllegalArgumentException( - "Unknown " + ActionViewType.class.getSimpleName() + ": " + viewType); - } - view.addActionButtonClickListener(mCallbacks); - - final PackageManager packageManager = mContext.getPackageManager(); - int historySize = dataModel.getDistinctActivityCountInHistory(); - if (historySize > maxHistorySize) { - historySize = maxHistorySize; - } - - // Historical data is dependent on past selection of activities. - // Activity count is determined by the number of activities that can handle - // the particular intent. When no intent is set, the activity count is 0, - // while the history count can be a valid number. - if (historySize > dataModel.getActivityCount()) { - return view; - } - - for (int i = 0; i < historySize; i++) { - view.addActionButton(dataModel.getActivity(i).loadIcon(packageManager), - dataModel.getActivity(i).loadLabel(packageManager)); - } - - return view; - } - - public boolean hasSubMenu() { - return true; - } - - public void onPrepareSubMenu(SubMenu subMenu) { - // Clear since the order of items may change. - subMenu.clear(); - - ActivityChooserModel dataModel = ActivityChooserModel.get(mContext, mHistoryFileName); - PackageManager packageManager = mContext.getPackageManager(); - - // Populate the sub-menu with a sub set of the activities. - final String shareDialogClassName = ShareDialog.class.getCanonicalName(); - final String sendTabLabel = mContext.getResources().getString(R.string.overlay_share_send_other); - final int count = dataModel.getActivityCount(); - for (int i = 0; i < count; i++) { - ResolveInfo activity = dataModel.getActivity(i); - final CharSequence activityLabel = activity.loadLabel(packageManager); - - // Pin internal actions to the top. Note: - // the order here does not affect quick share. - final int order; - if (shareDialogClassName.equals(activity.activityInfo.name) && - sendTabLabel.equals(activityLabel)) { - order = Menu.FIRST + i; - } else { - order = Menu.FIRST + (i | Menu.CATEGORY_SECONDARY); - } - - subMenu.add(0, i, order, activityLabel) - .setIcon(activity.loadIcon(packageManager)) - .setOnMenuItemClickListener(mCallbacks); - } - } - - public void setHistoryFileName(String historyFile) { - mHistoryFileName = historyFile; - } - - public Intent getIntent() { - ActivityChooserModel dataModel = ActivityChooserModel.get(mContext, mHistoryFileName); - return dataModel.getIntent(); - } - - public void setIntent(Intent intent) { - ActivityChooserModel dataModel = ActivityChooserModel.get(mContext, mHistoryFileName); - dataModel.setIntent(intent); - - // Inform the target listener to refresh it's UI, if needed. - if (mOnTargetListener != null) { - mOnTargetListener.onTargetSelected(); - } - } - - public void setOnTargetSelectedListener(OnTargetSelectedListener listener) { - mOnTargetListener = listener; - } - - public ArrayList getSortedActivities() { - ArrayList infos = new ArrayList(); - - ActivityChooserModel dataModel = ActivityChooserModel.get(mContext, mHistoryFileName); - - // Populate the sub-menu with a sub set of the activities. - final int count = dataModel.getActivityCount(); - for (int i = 0; i < count; i++) { - infos.add(dataModel.getActivity(i)); - } - - return infos; - } - - public void chooseActivity(int position) { - mCallbacks.chooseActivity(position); - } - - /** - * Listener for handling default activity / menu item clicks. - */ - private class Callbacks implements OnMenuItemClickListener, - OnClickListener { - void chooseActivity(int index) { - final ActivityChooserModel dataModel = ActivityChooserModel.get(mContext, mHistoryFileName); - final Intent launchIntent = dataModel.chooseActivity(index); - if (launchIntent != null) { - // This may cause a download to happen. Make sure we're on the background thread. - ThreadUtils.postToBackgroundThread(new Runnable() { - @Override - public void run() { - // Share image downloads the image before sharing it. - String type = launchIntent.getType(); - if (Intent.ACTION_SEND.equals(launchIntent.getAction()) && type != null && type.startsWith("image/")) { - downloadImageForIntent(launchIntent); - } - - launchIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET); - mContext.startActivity(launchIntent); - } - }); - } - - if (mOnTargetListener != null) { - mOnTargetListener.onTargetSelected(); - } - } - - @Override - public boolean onMenuItemClick(MenuItem item) { - chooseActivity(item.getItemId()); - - // Context: Sharing via chrome mainmenu list (no explicit session is active) - Telemetry.sendUIEvent(TelemetryContract.Event.SHARE, TelemetryContract.Method.LIST, "actionprovider"); - return true; - } - - @Override - public void onClick(View view) { - Integer index = (Integer) view.getTag(); - chooseActivity(index); - - // Context: Sharing via chrome mainmenu and content contextmenu quickshare (no explicit session is active) - Telemetry.sendUIEvent(TelemetryContract.Event.SHARE, TelemetryContract.Method.BUTTON, "actionprovider"); - } - } - - public enum ActionViewType { - DEFAULT, - CONTEXT_MENU, - } - - - /** - * Downloads the URI pointed to by a share intent, and alters the intent to point to the - * locally stored file. - * - * @param intent share intent to alter in place. - */ - public void downloadImageForIntent(final Intent intent) { - final String src = IntentUtils.getStringExtraSafe(intent, Intent.EXTRA_TEXT); - final File dir = GeckoApp.getTempDirectory(); - - if (src == null || dir == null) { - // We should be, but currently aren't, statically guaranteed an Activity context. - // Try our best. - if (mContext instanceof Activity) { - SnackbarBuilder.builder((Activity) mContext) - .message(mContext.getApplicationContext().getString(R.string.share_image_failed)) - .duration(Snackbar.LENGTH_LONG) - .buildAndShow(); - } - return; - } - - GeckoApp.deleteTempFiles(); - - String type = intent.getType(); - OutputStream os = null; - try { - // Create a temporary file for the image - if (src.startsWith("data:")) { - final int dataStart = src.indexOf(","); - - String extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(type); - - // If we weren't given an explicit mimetype, try to dig one out of the data uri. - if (TextUtils.isEmpty(extension) && dataStart > 5) { - type = src.substring(5, dataStart).replace(";base64", ""); - extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(type); - } - - final File imageFile = File.createTempFile("image", "." + extension, dir); - os = new FileOutputStream(imageFile); - - byte[] buf = Base64.decode(src.substring(dataStart + 1), Base64.DEFAULT); - os.write(buf); - - // Only alter the intent when we're sure everything has worked - intent.putExtra(Intent.EXTRA_STREAM, Uri.fromFile(imageFile)); - } else { - InputStream is = null; - try { - final byte[] buf = new byte[2048]; - final URL url = new URL(src); - final String filename = URLUtil.guessFileName(src, null, type); - is = url.openStream(); - - final File imageFile = new File(dir, filename); - os = new FileOutputStream(imageFile); - - int length; - while ((length = is.read(buf)) != -1) { - os.write(buf, 0, length); - } - - // Only alter the intent when we're sure everything has worked - intent.putExtra(Intent.EXTRA_STREAM, Uri.fromFile(imageFile)); - } finally { - IOUtils.safeStreamClose(is); - } - } - } catch (IOException ex) { - // If something went wrong, we'll just leave the intent un-changed - } finally { - IOUtils.safeStreamClose(os); - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/widget/GeckoPopupMenu.java b/mobile/android/base/java/org/mozilla/gecko/widget/GeckoPopupMenu.java deleted file mode 100644 index 7e7f506621..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/widget/GeckoPopupMenu.java +++ /dev/null @@ -1,189 +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.widget; - -import org.mozilla.gecko.menu.GeckoMenu; -import org.mozilla.gecko.menu.GeckoMenuInflater; -import org.mozilla.gecko.menu.MenuPanel; -import org.mozilla.gecko.menu.MenuPopup; - -import android.content.Context; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.view.View; - -/** - * A PopupMenu that uses the custom GeckoMenu. This menu is - * usually tied to an anchor, and show as a dropdrown from the anchor. - */ -public class GeckoPopupMenu implements GeckoMenu.Callback, - GeckoMenu.MenuPresenter { - - // An interface for listeners for dismissal. - public static interface OnDismissListener { - public boolean onDismiss(GeckoMenu menu); - } - - // An interface for listeners for menu item click events. - public static interface OnMenuItemClickListener { - public boolean onMenuItemClick(MenuItem item); - } - - // An interface for listeners for menu item long click events. - public static interface OnMenuItemLongClickListener { - public boolean onMenuItemLongClick(MenuItem item); - } - - private View mAnchor; - - private MenuPopup mMenuPopup; - private MenuPanel mMenuPanel; - - private GeckoMenu mMenu; - private GeckoMenuInflater mMenuInflater; - - private OnDismissListener mDismissListener; - private OnMenuItemClickListener mClickListener; - private OnMenuItemLongClickListener mLongClickListener; - - public GeckoPopupMenu(Context context) { - initialize(context, null); - } - - public GeckoPopupMenu(Context context, View anchor) { - initialize(context, anchor); - } - - /** - * This method creates an empty menu and attaches the necessary listeners. - * If an anchor is supplied, it is stored as well. - */ - private void initialize(Context context, View anchor) { - mMenu = new GeckoMenu(context, null); - mMenu.setCallback(this); - mMenu.setMenuPresenter(this); - mMenuInflater = new GeckoMenuInflater(context); - - mMenuPopup = new MenuPopup(context); - mMenuPanel = new MenuPanel(context, null); - - mMenuPanel.addView(mMenu); - mMenuPopup.setPanelView(mMenuPanel); - - setAnchor(anchor); - } - - /** - * Returns the menu that is current being shown. - * - * @return The menu being shown. - */ - public GeckoMenu getMenu() { - return mMenu; - } - - /** - * Returns the menu inflater that was used to create the menu. - * - * @return The menu inflater used. - */ - public MenuInflater getMenuInflater() { - return mMenuInflater; - } - - /** - * Inflates a menu resource to the menu using the menu inflater. - * - * @param menuRes The menu resource to be inflated. - */ - public void inflate(int menuRes) { - if (menuRes > 0) { - mMenuInflater.inflate(menuRes, mMenu); - } - } - - /** - * Set a different anchor after the menu is inflated. - * - * @param anchor The new anchor for the popup. - */ - public void setAnchor(View anchor) { - mAnchor = anchor; - - // Reposition the popup if the anchor changes while it's showing. - if (mMenuPopup.isShowing()) { - mMenuPopup.dismiss(); - mMenuPopup.showAsDropDown(mAnchor); - } - } - - public void setOnDismissListener(OnDismissListener listener) { - mDismissListener = listener; - } - - public void setOnMenuItemClickListener(OnMenuItemClickListener listener) { - mClickListener = listener; - } - - public void setOnMenuItemLongClickListener(OnMenuItemLongClickListener listener) { - mLongClickListener = listener; - } - - /** - * Show the inflated menu. - */ - public void show() { - if (!mMenuPopup.isShowing()) - mMenuPopup.showAsDropDown(mAnchor); - } - - /** - * Hide the inflated menu. - */ - public void dismiss() { - if (mMenuPopup.isShowing()) { - mMenuPopup.dismiss(); - - if (mDismissListener != null) - mDismissListener.onDismiss(mMenu); - } - } - - @Override - public boolean onMenuItemClick(MenuItem item) { - if (mClickListener != null) { - return mClickListener.onMenuItemClick(item); - } - return false; - } - - @Override - public boolean onMenuItemLongClick(MenuItem item) { - if (mLongClickListener != null) { - return mLongClickListener.onMenuItemLongClick(item); - } - return false; - } - - @Override - public void openMenu() { - show(); - } - - @Override - public void showMenu(View menu) { - mMenuPanel.removeAllViews(); - mMenuPanel.addView(menu); - - openMenu(); - } - - @Override - public void closeMenu() { - dismiss(); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/widget/HistoryDividerItemDecoration.java b/mobile/android/base/java/org/mozilla/gecko/widget/HistoryDividerItemDecoration.java deleted file mode 100644 index 9c98e8a0d9..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/widget/HistoryDividerItemDecoration.java +++ /dev/null @@ -1,66 +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.widget; - -import android.content.Context; -import android.graphics.Canvas; -import android.graphics.Paint; -import android.graphics.Rect; -import android.support.v4.content.ContextCompat; -import android.support.v7.widget.RecyclerView; -import android.view.View; -import org.mozilla.gecko.R; -import org.mozilla.gecko.home.CombinedHistoryItem; - -public class HistoryDividerItemDecoration extends RecyclerView.ItemDecoration { - private final int mDividerHeight; - private final Paint mDividerPaint; - - public HistoryDividerItemDecoration(Context context) { - mDividerHeight = (int) context.getResources().getDimension(R.dimen.page_row_divider_height); - - mDividerPaint = new Paint(); - mDividerPaint.setColor(ContextCompat.getColor(context, R.color.toolbar_divider_grey)); - mDividerPaint.setStyle(Paint.Style.FILL_AND_STROKE); - } - - @Override - public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) { - final int position = parent.getChildAdapterPosition(view); - if (position == RecyclerView.NO_POSITION) { - // This view is no longer corresponds to an adapter position (pending changes). - return; - } - - if (parent.getAdapter().getItemViewType(position) != - CombinedHistoryItem.ItemType.itemTypeToViewType(CombinedHistoryItem.ItemType.SECTION_HEADER)) { - outRect.set(0, 0, 0, mDividerHeight); - } - } - - @Override - public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) { - if (parent.getChildCount() == 0) { - return; - } - - for (int i = 0; i < parent.getChildCount(); i++) { - final View child = parent.getChildAt(i); - final int position = parent.getChildAdapterPosition(child); - - if (position == RecyclerView.NO_POSITION) { - // This view is no longer corresponds to an adapter position (pending changes). - continue; - } - - if (parent.getAdapter().getItemViewType(position) != - CombinedHistoryItem.ItemType.itemTypeToViewType(CombinedHistoryItem.ItemType.SECTION_HEADER)) { - final float bottom = child.getBottom() + child.getTranslationY(); - c.drawRect(0, bottom, parent.getWidth(), bottom + mDividerHeight, mDividerPaint); - } - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/widget/IconTabWidget.java b/mobile/android/base/java/org/mozilla/gecko/widget/IconTabWidget.java deleted file mode 100644 index 71987bf8c9..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/widget/IconTabWidget.java +++ /dev/null @@ -1,111 +0,0 @@ -/* 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.widget; - -import org.mozilla.gecko.R; - -import android.content.Context; -import android.content.res.TypedArray; -import android.graphics.drawable.Drawable; -import android.util.AttributeSet; -import android.view.LayoutInflater; -import android.view.View; -import android.widget.ImageButton; -import android.widget.TabWidget; -import android.widget.TextView; - -public class IconTabWidget extends TabWidget { - OnTabChangedListener mListener; - private final int mButtonLayoutId; - private final boolean mIsIcon; - - public static interface OnTabChangedListener { - public void onTabChanged(int tabIndex); - } - - public IconTabWidget(Context context, AttributeSet attrs) { - super(context, attrs); - - TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.IconTabWidget); - mButtonLayoutId = a.getResourceId(R.styleable.IconTabWidget_android_layout, 0); - mIsIcon = (a.getInt(R.styleable.IconTabWidget_display, 0x00) == 0x00); - a.recycle(); - - if (mButtonLayoutId == 0) { - throw new RuntimeException("You must supply layout attribute"); - } - } - - public View addTab(final int imageResId, final int stringResId) { - View button = LayoutInflater.from(getContext()).inflate(mButtonLayoutId, this, false); - if (mIsIcon) { - ((ImageButton) button).setImageResource(imageResId); - button.setContentDescription(getContext().getString(stringResId)); - } else { - ((TextView) button).setText(getContext().getString(stringResId)); - } - - addView(button); - button.setOnClickListener(new TabClickListener(getTabCount() - 1)); - button.setOnFocusChangeListener(this); - return button; - } - - public void setTabSelectionListener(OnTabChangedListener listener) { - mListener = listener; - } - - @Override - public void onFocusChange(View view, boolean hasFocus) { - } - - private class TabClickListener implements OnClickListener { - private final int mIndex; - - public TabClickListener(int index) { - mIndex = index; - } - - @Override - public void onClick(View view) { - if (mListener != null) - mListener.onTabChanged(mIndex); - } - } - - /** - * Fetch the Drawable icon corresponding to the given panel. - * @param panel to fetch icon for. - * @return Drawable instance, or null if no icon is being displayed, or the icon does not exist. - */ - public Drawable getIconDrawable(int index) { - if (!mIsIcon) { - return null; - } - // We can have multiple views in the tabs panel for each child. This finds the - // first view corresponding to the given tab. This varies by Android - // version. The first view should always be our ImageButton, but let's - // be safe. - final View view = getChildTabViewAt(index); - if (view instanceof ImageButton) { - return ((ImageButton) view).getDrawable(); - } - return null; - } - - public void setIconDrawable(int index, int resource) { - if (!mIsIcon) { - return; - } - // We can have multiple views in the tabs panel for each child. This finds the - // first view corresponding to the given tab. This varies by Android - // version. The first view should always be our ImageButton, but let's - // be safe. - final View view = getChildTabViewAt(index); - if (view instanceof ImageButton) { - ((ImageButton) view).setImageResource(resource); - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/widget/LoginDoorHanger.java b/mobile/android/base/java/org/mozilla/gecko/widget/LoginDoorHanger.java deleted file mode 100644 index 232674813b..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/widget/LoginDoorHanger.java +++ /dev/null @@ -1,228 +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.widget; - -import android.app.AlertDialog; -import android.app.Dialog; -import android.content.Context; -import android.content.DialogInterface; -import android.text.Html; -import android.text.Spanned; -import android.text.TextUtils; -import android.text.method.PasswordTransformationMethod; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.View; -import android.widget.Button; -import android.widget.CheckBox; -import android.widget.CompoundButton; -import android.widget.EditText; -import android.widget.TextView; -import android.widget.Toast; - -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; -import org.mozilla.gecko.R; -import org.mozilla.gecko.Telemetry; -import org.mozilla.gecko.TelemetryContract; - -import java.util.Locale; - -public class LoginDoorHanger extends DoorHanger { - private static final String LOGTAG = "LoginDoorHanger"; - private enum ActionType { EDIT, SELECT } - - private final TextView mMessage; - private final DoorhangerConfig.ButtonConfig mButtonConfig; - - public LoginDoorHanger(Context context, DoorhangerConfig config) { - super(context, config, Type.LOGIN); - - mMessage = (TextView) findViewById(R.id.doorhanger_message); - mIcon.setImageResource(R.drawable.icon_key); - mIcon.setVisibility(View.VISIBLE); - - mButtonConfig = config.getPositiveButtonConfig(); - - loadConfig(config); - } - - private void setMessage(String message) { - Spanned markupMessage = Html.fromHtml(message); - mMessage.setText(markupMessage); - } - - @Override - protected void loadConfig(DoorhangerConfig config) { - setOptions(config.getOptions()); - setMessage(config.getMessage()); - // Store the positive callback id for nested dialogs that need the same callback id. - addButtonsToLayout(config); - } - - @Override - protected int getContentResource() { - return R.layout.login_doorhanger; - } - - @Override - protected void setOptions(final JSONObject options) { - super.setOptions(options); - - final JSONObject actionText = options.optJSONObject("actionText"); - addActionText(actionText); - } - - @Override - protected OnClickListener makeOnButtonClickListener(final int id, final String telemetryExtra) { - return new Button.OnClickListener() { - @Override - public void onClick(View v) { - final String expandedExtra = mType.toString().toLowerCase(Locale.US) + "-" + telemetryExtra; - Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.DOORHANGER, expandedExtra); - - final JSONObject response = new JSONObject(); - try { - response.put("callback", id); - } catch (JSONException e) { - Log.e(LOGTAG, "Error making doorhanger response message", e); - } - mOnButtonClickListener.onButtonClick(response, LoginDoorHanger.this); - } - }; - } - - /** - * Add sub-text to the doorhanger and add the click action. - * - * If the parsing the action from the JSON throws, the text is left visible, but there is no - * click action. - * @param actionTextObj JSONObject containing blob for making an action. - */ - private void addActionText(JSONObject actionTextObj) { - if (actionTextObj == null) { - mLink.setVisibility(View.GONE); - return; - } - - // Make action. - try { - final JSONObject bundle = actionTextObj.getJSONObject("bundle"); - final ActionType type = ActionType.valueOf(actionTextObj.getString("type")); - final AlertDialog.Builder builder = new AlertDialog.Builder(mContext); - - switch (type) { - case EDIT: - builder.setTitle(mResources.getString(R.string.doorhanger_login_edit_title)); - - final View view = LayoutInflater.from(mContext).inflate(R.layout.login_edit_dialog, null); - final EditText username = (EditText) view.findViewById(R.id.username_edit); - username.setText(bundle.getString("username")); - final EditText password = (EditText) view.findViewById(R.id.password_edit); - password.setText(bundle.getString("password")); - final CheckBox passwordCheckbox = (CheckBox) view.findViewById(R.id.checkbox_toggle_password); - passwordCheckbox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { - @Override - public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { - if (isChecked) { - password.setTransformationMethod(null); - } else { - password.setTransformationMethod(PasswordTransformationMethod.getInstance()); - } - } - }); - builder.setView(view); - - builder.setPositiveButton(mButtonConfig.label, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - JSONObject response = new JSONObject(); - try { - response.put("callback", mButtonConfig.callback); - final JSONObject inputs = new JSONObject(); - inputs.put("username", username.getText()); - inputs.put("password", password.getText()); - response.put("inputs", inputs); - } catch (JSONException e) { - Log.e(LOGTAG, "Error creating doorhanger reply message"); - response = null; - Toast.makeText(mContext, mResources.getString(R.string.doorhanger_login_edit_toast_error), Toast.LENGTH_SHORT).show(); - } - mOnButtonClickListener.onButtonClick(response, LoginDoorHanger.this); - } - }); - builder.setNegativeButton(R.string.button_cancel, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - dialog.dismiss(); - } - }); - String text = actionTextObj.optString("text"); - if (TextUtils.isEmpty(text)) { - text = mResources.getString(R.string.doorhanger_login_no_username); - } - mLink.setText(text); - mLink.setVisibility(View.VISIBLE); - break; - - case SELECT: - try { - builder.setTitle(mResources.getString(R.string.doorhanger_login_select_title)); - final JSONArray logins = bundle.getJSONArray("logins"); - final int numLogins = logins.length(); - final CharSequence[] usernames = new CharSequence[numLogins]; - final String[] passwords = new String[numLogins]; - final String noUser = mResources.getString(R.string.doorhanger_login_no_username); - for (int i = 0; i < numLogins; i++) { - final JSONObject login = (JSONObject) logins.get(i); - String user = login.getString("username"); - if (TextUtils.isEmpty(user)) { - user = noUser; - } - usernames[i] = user; - passwords[i] = login.getString("password"); - } - builder.setItems(usernames, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - final JSONObject response = new JSONObject(); - try { - response.put("callback", mButtonConfig.callback); - response.put("password", passwords[which]); - } catch (JSONException e) { - Log.e(LOGTAG, "Error making login select dialog JSON", e); - } - mOnButtonClickListener.onButtonClick(response, LoginDoorHanger.this); - } - }); - builder.setNegativeButton(R.string.button_cancel, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - dialog.dismiss(); - } - }); - mLink.setText(R.string.doorhanger_login_select_action_text); - mLink.setVisibility(View.VISIBLE); - } catch (JSONException e) { - Log.e(LOGTAG, "Problem creating list of logins"); - } - break; - } - - final Dialog dialog = builder.create(); - mLink.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View v) { - dialog.show(); - } - }); - - } catch (JSONException e) { - Log.e(LOGTAG, "Error fetching actionText from JSON", e); - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/widget/RecyclerViewClickSupport.java b/mobile/android/base/java/org/mozilla/gecko/widget/RecyclerViewClickSupport.java deleted file mode 100644 index a0c6049c52..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/widget/RecyclerViewClickSupport.java +++ /dev/null @@ -1,105 +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.widget; - -import android.support.v7.widget.RecyclerView; -import android.view.View; - -import org.mozilla.gecko.R; - -/** - * {@link RecyclerViewClickSupport} implementation that will notify an OnClickListener about clicks and long clicks - * on items displayed by the RecyclerView. - * @see littlerobots.nl - */ -public class RecyclerViewClickSupport { - private final RecyclerView mRecyclerView; - private OnItemClickListener mOnItemClickListener; - private OnItemLongClickListener mOnItemLongClickListener; - private View.OnClickListener mOnClickListener = new View.OnClickListener() { - @Override - public void onClick(View v) { - if (mOnItemClickListener != null) { - RecyclerView.ViewHolder holder = mRecyclerView.getChildViewHolder(v); - mOnItemClickListener.onItemClicked(mRecyclerView, holder.getAdapterPosition(), v); - } - } - }; - private View.OnLongClickListener mOnLongClickListener = new View.OnLongClickListener() { - @Override - public boolean onLongClick(View v) { - if (mOnItemLongClickListener != null) { - RecyclerView.ViewHolder holder = mRecyclerView.getChildViewHolder(v); - return mOnItemLongClickListener.onItemLongClicked(mRecyclerView, holder.getAdapterPosition(), v); - } - return false; - } - }; - private RecyclerView.OnChildAttachStateChangeListener mAttachListener - = new RecyclerView.OnChildAttachStateChangeListener() { - @Override - public void onChildViewAttachedToWindow(View view) { - if (mOnItemClickListener != null) { - view.setOnClickListener(mOnClickListener); - } - if (mOnItemLongClickListener != null) { - view.setOnLongClickListener(mOnLongClickListener); - } - } - - @Override - public void onChildViewDetachedFromWindow(View view) { - - } - }; - - private RecyclerViewClickSupport(RecyclerView recyclerView) { - mRecyclerView = recyclerView; - mRecyclerView.setTag(R.id.recycler_view_click_support, this); - mRecyclerView.addOnChildAttachStateChangeListener(mAttachListener); - } - - public static RecyclerViewClickSupport addTo(RecyclerView view) { - RecyclerViewClickSupport support = (RecyclerViewClickSupport) view.getTag(R.id.recycler_view_click_support); - if (support == null) { - support = new RecyclerViewClickSupport(view); - } - return support; - } - - public static RecyclerViewClickSupport removeFrom(RecyclerView view) { - RecyclerViewClickSupport support = (RecyclerViewClickSupport) view.getTag(R.id.recycler_view_click_support); - if (support != null) { - support.detach(view); - } - return support; - } - - public RecyclerViewClickSupport setOnItemClickListener(OnItemClickListener listener) { - mOnItemClickListener = listener; - return this; - } - - public RecyclerViewClickSupport setOnItemLongClickListener(OnItemLongClickListener listener) { - mOnItemLongClickListener = listener; - return this; - } - - private void detach(RecyclerView view) { - view.removeOnChildAttachStateChangeListener(mAttachListener); - view.setTag(R.id.recycler_view_click_support, null); - } - - public interface OnItemClickListener { - - void onItemClicked(RecyclerView recyclerView, int position, View v); - } - - public interface OnItemLongClickListener { - - boolean onItemLongClicked(RecyclerView recyclerView, int position, View v); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/widget/ResizablePathDrawable.java b/mobile/android/base/java/org/mozilla/gecko/widget/ResizablePathDrawable.java deleted file mode 100644 index ff0709cb76..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/widget/ResizablePathDrawable.java +++ /dev/null @@ -1,117 +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.widget; - -import android.content.res.ColorStateList; -import android.graphics.Canvas; -import android.graphics.Color; -import android.graphics.Paint; -import android.graphics.Path; -import android.graphics.drawable.ShapeDrawable; -import android.graphics.drawable.shapes.Shape; - -public class ResizablePathDrawable extends ShapeDrawable { - // An attribute mirroring the super class' value. getAlpha() is only - // available in API 19+ so to use that alpha value, we have to mirror it. - private int alpha = 255; - - private final ColorStateList colorStateList; - private int currentColor; - - public ResizablePathDrawable(NonScaledPathShape shape, int color) { - this(shape, ColorStateList.valueOf(color)); - } - - public ResizablePathDrawable(NonScaledPathShape shape, ColorStateList colorStateList) { - super(shape); - this.colorStateList = colorStateList; - updateColor(getState()); - } - - private boolean updateColor(int[] stateSet) { - int newColor = colorStateList.getColorForState(stateSet, Color.WHITE); - if (newColor != currentColor) { - currentColor = newColor; - alpha = Color.alpha(currentColor); - invalidateSelf(); - return true; - } - - return false; - } - - public Path getPath() { - final NonScaledPathShape shape = (NonScaledPathShape) getShape(); - return shape.path; - } - - @Override - public boolean isStateful() { - return true; - } - - @Override - protected void onDraw(Shape shape, Canvas canvas, Paint paint) { - paint.setColor(currentColor); - // setAlpha overrides the alpha value in set color. Since we just set the color, - // the alpha value is reset: override the alpha value with the old value. We don't - // set alpha if the color is transparent. - // - // Note: We *should* be able to call Shape.setAlpha, rather than Paint.setAlpha, but - // then the opacity doesn't change - dunno why but probably not worth the time. - if (currentColor != Color.TRANSPARENT) { - paint.setAlpha(alpha); - } - - super.onDraw(shape, canvas, paint); - } - - @Override - public void setAlpha(final int alpha) { - super.setAlpha(alpha); - this.alpha = alpha; - } - - @Override - protected boolean onStateChange(int[] stateSet) { - return updateColor(stateSet); - } - - /** - * Path-based shape implementation that re-creates the path - * when it gets resized as opposed to PathShape's scaling - * behaviour. - */ - public static class NonScaledPathShape extends Shape { - private Path path; - - public NonScaledPathShape() { - path = new Path(); - } - - @Override - public void draw(Canvas canvas, Paint paint) { - // No point in drawing the shape if it's not - // going to be visible. - if (paint.getColor() == Color.TRANSPARENT) { - return; - } - - canvas.drawPath(path, paint); - } - - protected Path getPath() { - return path; - } - - @Override - public NonScaledPathShape clone() throws CloneNotSupportedException { - final NonScaledPathShape clonedShape = (NonScaledPathShape) super.clone(); - clonedShape.path = new Path(path); - return clonedShape; - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/widget/RoundedCornerLayout.java b/mobile/android/base/java/org/mozilla/gecko/widget/RoundedCornerLayout.java deleted file mode 100644 index a102981eed..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/widget/RoundedCornerLayout.java +++ /dev/null @@ -1,79 +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.widget; - -import org.mozilla.gecko.AppConstants; -import org.mozilla.gecko.R; - -import android.content.Context; -import android.graphics.Canvas; -import android.graphics.Path; -import android.graphics.RectF; -import android.util.AttributeSet; -import android.util.DisplayMetrics; -import android.util.TypedValue; -import android.widget.LinearLayout; - -public class RoundedCornerLayout extends LinearLayout { - private static final String LOGTAG = "Gecko" + RoundedCornerLayout.class.getSimpleName(); - private float cornerRadius; - - private Path path; - boolean cannotClipPath; - - public RoundedCornerLayout(Context context) { - super(context); - init(context); - } - - public RoundedCornerLayout(Context context, AttributeSet attrs) { - super(context, attrs); - init(context); - } - - public RoundedCornerLayout(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - init(context); - } - - private void init(Context context) { - // Bug 1201081 - clipPath with hardware acceleration crashes on r11-18. - cannotClipPath = !AppConstants.Versions.feature19Plus; - - final DisplayMetrics metrics = context.getResources().getDisplayMetrics(); - - cornerRadius = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_PX, - getResources().getDimensionPixelSize(R.dimen.doorhanger_rounded_corner_radius), metrics); - - setWillNotDraw(false); - } - - @Override - protected void onSizeChanged(int w, int h, int oldw, int oldh) { - super.onSizeChanged(w, h, oldw, oldh); - if (cannotClipPath) { - return; - } - - final RectF r = new RectF(0, 0, w, h); - path = new Path(); - path.addRoundRect(r, cornerRadius, cornerRadius, Path.Direction.CW); - path.close(); - } - - @Override - public void draw(Canvas canvas) { - if (cannotClipPath) { - super.draw(canvas); - return; - } - - canvas.save(); - canvas.clipPath(path); - super.draw(canvas); - canvas.restore(); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/widget/SiteLogins.java b/mobile/android/base/java/org/mozilla/gecko/widget/SiteLogins.java deleted file mode 100644 index 4d4a922758..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/widget/SiteLogins.java +++ /dev/null @@ -1,16 +0,0 @@ -package org.mozilla.gecko.widget; - -import org.json.JSONArray; -import org.json.JSONObject; - -public class SiteLogins { - private final JSONArray logins; - - public SiteLogins(JSONArray logins) { - this.logins = logins; - } - - public JSONArray getLogins() { - return logins; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/widget/SquaredImageView.java b/mobile/android/base/java/org/mozilla/gecko/widget/SquaredImageView.java deleted file mode 100644 index 0b77e9d1ca..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/widget/SquaredImageView.java +++ /dev/null @@ -1,21 +0,0 @@ -package org.mozilla.gecko.widget; - -import android.content.Context; -import android.util.AttributeSet; -import android.widget.ImageView; - -final class SquaredImageView extends ImageView { - public SquaredImageView(Context context) { - super(context); - } - - public SquaredImageView(Context context, AttributeSet attrs) { - super(context, attrs); - } - - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - super.onMeasure(widthMeasureSpec, heightMeasureSpec); - setMeasuredDimension(getMeasuredWidth(), getMeasuredWidth()); - } -} \ No newline at end of file diff --git a/mobile/android/base/java/org/mozilla/gecko/widget/SquaredRelativeLayout.java b/mobile/android/base/java/org/mozilla/gecko/widget/SquaredRelativeLayout.java deleted file mode 100644 index c0dca0bec3..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/widget/SquaredRelativeLayout.java +++ /dev/null @@ -1,33 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.widget; - -import android.content.Context; -import android.util.AttributeSet; -import android.widget.RelativeLayout; - -public class SquaredRelativeLayout extends RelativeLayout { - public SquaredRelativeLayout(Context context) { - super(context); - } - - public SquaredRelativeLayout(Context context, AttributeSet attrs) { - super(context, attrs); - } - - public SquaredRelativeLayout(Context context, AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - } - - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - super.onMeasure(widthMeasureSpec, heightMeasureSpec); - - int squareMeasureSpec = MeasureSpec.makeMeasureSpec(getMeasuredWidth(), MeasureSpec.EXACTLY); - - super.onMeasure(squareMeasureSpec, squareMeasureSpec); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/widget/SwipeDismissListViewTouchListener.java b/mobile/android/base/java/org/mozilla/gecko/widget/SwipeDismissListViewTouchListener.java deleted file mode 100644 index 8267fe8a3e..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/widget/SwipeDismissListViewTouchListener.java +++ /dev/null @@ -1,356 +0,0 @@ -/* - * Copyright 2012 Roman Nurik - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.mozilla.gecko.widget; - -import android.graphics.Rect; -import android.view.MotionEvent; -import android.view.VelocityTracker; -import android.view.View; -import android.view.ViewConfiguration; -import android.view.ViewGroup; -import android.widget.AbsListView; -import android.widget.AbsListView.RecyclerListener; -import android.widget.ListView; - -import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; -import android.animation.ValueAnimator; -import android.view.ViewPropertyAnimator; - -import org.mozilla.gecko.R; - -/** - * This code is based off of Jake Wharton's NOA port (https://github.com/JakeWharton/SwipeToDismissNOA) - * of Roman Nurik's SwipeToDismiss library. It has been modified for better support with async - * adapters. - * - * A {@link android.view.View.OnTouchListener} that makes the list items in a {@link ListView} - * dismissable. {@link ListView} is given special treatment because by default it handles touches - * for its list items... i.e. it's in charge of drawing the pressed state (the list selector), - * handling list item clicks, etc. - * - *

After creating the listener, the caller should also call - * {@link ListView#setOnScrollListener(android.widget.AbsListView.OnScrollListener)}, passing - * in the scroll listener returned by {@link #makeScrollListener()}. If a scroll listener is - * already assigned, the caller should still pass scroll changes through to this listener. This will - * ensure that this {@link SwipeDismissListViewTouchListener} is paused during list view - * scrolling.

- * - *

Example usage:

- * - *
- * SwipeDismissListViewTouchListener touchListener =
- *         new SwipeDismissListViewTouchListener(
- *                 listView,
- *                 new SwipeDismissListViewTouchListener.OnDismissCallback() {
- *                     public void onDismiss(ListView listView, int[] reverseSortedPositions) {
- *                         for (int position : reverseSortedPositions) {
- *                             adapter.remove(adapter.getItem(position));
- *                         }
- *                         adapter.notifyDataSetChanged();
- *                     }
- *                 });
- * listView.setOnTouchListener(touchListener);
- * listView.setOnScrollListener(touchListener.makeScrollListener());
- * 
- * - *

For a generalized {@link android.view.View.OnTouchListener} that makes any view dismissable, - * see {@link SwipeDismissTouchListener}.

- * - * @see SwipeDismissTouchListener - */ -public class SwipeDismissListViewTouchListener implements View.OnTouchListener { - // Cached ViewConfiguration and system-wide constant values - private final int mSlop; - private final int mMinFlingVelocity; - private final int mMaxFlingVelocity; - private final long mAnimationTime; - - // Fixed properties - private final ListView mListView; - private final OnDismissCallback mCallback; - private int mViewWidth = 1; // 1 and not 0 to prevent dividing by zero - - // Transient properties - private float mDownX; - private boolean mSwiping; - private VelocityTracker mVelocityTracker; - private int mDownPosition; - private View mDownView; - private boolean mPaused; - private boolean mDismissing; - - /** - * The callback interface used by {@link SwipeDismissListViewTouchListener} to inform its client - * about a successful dismissal of a list item. - */ - public interface OnDismissCallback { - /** - * Called when the user has indicated they she would like to dismiss one or more list item - * positions. - * - * @param listView The originating {@link ListView}. - * @param position The position being dismissed. - */ - void onDismiss(ListView listView, int position); - } - - /** - * Constructs a new swipe-to-dismiss touch listener for the given list view. - * - * @param listView The list view whose items should be dismissable. - * @param callback The callback to trigger when the user has indicated that she would like to - * dismiss one or more list items. - */ - public SwipeDismissListViewTouchListener(ListView listView, OnDismissCallback callback) { - ViewConfiguration vc = ViewConfiguration.get(listView.getContext()); - mSlop = vc.getScaledTouchSlop(); - mMinFlingVelocity = vc.getScaledMinimumFlingVelocity(); - mMaxFlingVelocity = vc.getScaledMaximumFlingVelocity(); - mAnimationTime = listView.getContext().getResources().getInteger( - android.R.integer.config_shortAnimTime); - mListView = listView; - mCallback = callback; - } - - /** - * Enables or disables (pauses or resumes) watching for swipe-to-dismiss gestures. - * - * @param enabled Whether or not to watch for gestures. - */ - public void setEnabled(boolean enabled) { - mPaused = !enabled; - } - - /** - * Returns an {@link android.widget.AbsListView.OnScrollListener} to be added to the - * {@link ListView} using - * {@link ListView#setOnScrollListener(android.widget.AbsListView.OnScrollListener)}. - * If a scroll listener is already assigned, the caller should still pass scroll changes - * through to this listener. This will ensure that this - * {@link SwipeDismissListViewTouchListener} is paused during list view scrolling.

- * - * @see {@link SwipeDismissListViewTouchListener} - */ - public AbsListView.OnScrollListener makeScrollListener() { - return new AbsListView.OnScrollListener() { - @Override - public void onScrollStateChanged(AbsListView absListView, int scrollState) { - setEnabled(scrollState != AbsListView.OnScrollListener.SCROLL_STATE_TOUCH_SCROLL); - } - - @Override - public void onScroll(AbsListView absListView, int i, int i1, int i2) { - } - }; - } - - /** - * Returns a {@link android.widget.AbsListView.RecyclerListener} to be added to the - * {@link ListView} using {@link ListView#setRecyclerListener(RecyclerListener)}. - */ - public AbsListView.RecyclerListener makeRecyclerListener() { - return new AbsListView.RecyclerListener() { - @Override - public void onMovedToScrapHeap(View view) { - final Object tag = view.getTag(R.id.original_height); - - // To reset the view to the correct height after its animation, the view's height - // is stored in its tag. Reset the view here. - if (tag instanceof Integer) { - view.setAlpha(1f); - view.setTranslationX(0); - final ViewGroup.LayoutParams lp = view.getLayoutParams(); - lp.height = (int) tag; - view.setLayoutParams(lp); - view.setTag(R.id.original_height, null); - } - } - }; - } - - @Override - public boolean onTouch(View view, MotionEvent motionEvent) { - if (mViewWidth < 2) { - mViewWidth = mListView.getWidth(); - } - - switch (motionEvent.getActionMasked()) { - case MotionEvent.ACTION_DOWN: { - if (mPaused) { - return false; - } - - if (mDismissing) { - return true; - } - - // TODO: ensure this is a finger, and set a flag - - // Find the child view that was touched (perform a hit test) - Rect rect = new Rect(); - int childCount = mListView.getChildCount(); - int[] listViewCoords = new int[2]; - mListView.getLocationOnScreen(listViewCoords); - int x = (int) motionEvent.getRawX() - listViewCoords[0]; - int y = (int) motionEvent.getRawY() - listViewCoords[1]; - View child; - for (int i = 0; i < childCount; i++) { - child = mListView.getChildAt(i); - child.getHitRect(rect); - if (rect.contains(x, y)) { - mDownView = child; - break; - } - } - - if (mDownView != null) { - mDownX = motionEvent.getRawX(); - mDownPosition = mListView.getPositionForView(mDownView); - - mVelocityTracker = VelocityTracker.obtain(); - mVelocityTracker.addMovement(motionEvent); - } - view.onTouchEvent(motionEvent); - return true; - } - - case MotionEvent.ACTION_UP: { - if (mVelocityTracker == null) { - break; - } - - float deltaX = motionEvent.getRawX() - mDownX; - mVelocityTracker.addMovement(motionEvent); - mVelocityTracker.computeCurrentVelocity(1000); - float velocityX = Math.abs(mVelocityTracker.getXVelocity()); - float velocityY = Math.abs(mVelocityTracker.getYVelocity()); - boolean dismiss = false; - boolean dismissRight = false; - if (Math.abs(deltaX) > mViewWidth / 2) { - dismiss = true; - dismissRight = deltaX > 0; - } else if (mMinFlingVelocity <= velocityX && velocityX <= mMaxFlingVelocity - && velocityY < velocityX) { - dismiss = true; - dismissRight = mVelocityTracker.getXVelocity() > 0; - } - if (dismiss) { - // dismiss - mDismissing = true; - final View downView = mDownView; // mDownView gets null'd before animation ends - final int downPosition = mDownPosition; - mDownView.animate() - .translationX(dismissRight ? mViewWidth : -mViewWidth) - .alpha(0) - .setDuration(mAnimationTime) - .setListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - performDismiss(downView, downPosition); - } - }); - } else { - // cancel - mDownView.animate() - .translationX(0) - .alpha(1) - .setDuration(mAnimationTime) - .setListener(null); - } - - if (mVelocityTracker != null) { - mVelocityTracker.recycle(); - mVelocityTracker = null; - } - - mDownX = 0; - mDownView = null; - mDownPosition = ListView.INVALID_POSITION; - mSwiping = false; - break; - } - - case MotionEvent.ACTION_MOVE: { - if (mVelocityTracker == null || mPaused) { - break; - } - - mVelocityTracker.addMovement(motionEvent); - float deltaX = motionEvent.getRawX() - mDownX; - if (Math.abs(deltaX) > mSlop) { - mSwiping = true; - mListView.requestDisallowInterceptTouchEvent(true); - - // Cancel ListView's touch (un-highlighting the item) - MotionEvent cancelEvent = MotionEvent.obtain(motionEvent); - cancelEvent.setAction(MotionEvent.ACTION_CANCEL | - (motionEvent.getActionIndex() - << MotionEvent.ACTION_POINTER_INDEX_SHIFT)); - mListView.onTouchEvent(cancelEvent); - cancelEvent.recycle(); - } - - if (mSwiping) { - mDownView.setTranslationX(deltaX); - mDownView.setAlpha(Math.max(0f, Math.min(1f, 1f - 2f * Math.abs(deltaX) / mViewWidth))); - return true; - } - break; - } - } - return false; - } - - /** - * Animate the dismissed list item to zero-height and fire the dismiss callback when it finishes. - * - * @param dismissView ListView item to dismiss - * @param dismissPosition Position of dismissed item - */ - private void performDismiss(final View dismissView, final int dismissPosition) { - final ViewGroup.LayoutParams lp = dismissView.getLayoutParams(); - final int originalHeight = lp.height; - - ValueAnimator animator = ValueAnimator.ofInt(dismissView.getHeight(), 1).setDuration(mAnimationTime); - - animator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - // Since the view is still a part of the ListView, we can't reset the animated - // properties yet; otherwise, the view would briefly reappear. Store the original - // height in the view's tag to flag it for the recycler. This is racy since the user - // could scroll the dismissed view off the screen, then back on the screen, before - // it's removed from the adapter, causing the dismissed view to briefly reappear. - dismissView.setTag(R.id.original_height, originalHeight); - - mCallback.onDismiss(mListView, dismissPosition); - mDismissing = false; - } - }); - - animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { - @Override - public void onAnimationUpdate(ValueAnimator valueAnimator) { - lp.height = (Integer) valueAnimator.getAnimatedValue(); - dismissView.setLayoutParams(lp); - } - }); - - animator.start(); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/widget/TabThumbnailWrapper.java b/mobile/android/base/java/org/mozilla/gecko/widget/TabThumbnailWrapper.java deleted file mode 100644 index 848e2f6ed1..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/widget/TabThumbnailWrapper.java +++ /dev/null @@ -1,38 +0,0 @@ -package org.mozilla.gecko.widget; - -import android.content.Context; -import android.util.AttributeSet; -import org.mozilla.gecko.R; -import org.mozilla.gecko.widget.themed.ThemedRelativeLayout; - - -public class TabThumbnailWrapper extends ThemedRelativeLayout { - private boolean mRecording; - private static final int[] STATE_RECORDING = { R.attr.state_recording }; - - public TabThumbnailWrapper(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - } - - public TabThumbnailWrapper(Context context, AttributeSet attrs) { - super(context, attrs); - } - - @Override - public int[] onCreateDrawableState(int extraSpace) { - final int[] drawableState = super.onCreateDrawableState(extraSpace + 1); - - if (mRecording) { - mergeDrawableStates(drawableState, STATE_RECORDING); - } - return drawableState; - } - - public void setRecording(boolean recording) { - if (mRecording != recording) { - mRecording = recording; - refreshDrawableState(); - } - } - -} diff --git a/mobile/android/base/java/org/mozilla/gecko/widget/ThumbnailView.java b/mobile/android/base/java/org/mozilla/gecko/widget/ThumbnailView.java deleted file mode 100644 index 5ab00ea7f2..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/widget/ThumbnailView.java +++ /dev/null @@ -1,86 +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.widget; - -import org.mozilla.gecko.R; - -import android.content.Context; -import android.graphics.Canvas; -import android.graphics.Matrix; -import android.graphics.drawable.Drawable; -import android.util.AttributeSet; -import android.support.v4.content.ContextCompat; -import org.mozilla.gecko.widget.themed.ThemedImageView; - -/* Special version of ImageView for thumbnails. Scales a thumbnail so that it maintains its aspect - * ratio and so that the images width and height are the same size or greater than the view size - */ -public class ThumbnailView extends ThemedImageView { - private static final String LOGTAG = "GeckoThumbnailView"; - - final private Matrix mMatrix; - private int mWidthSpec = -1; - private int mHeightSpec = -1; - private boolean mLayoutChanged; - private boolean mScale = false; - - public ThumbnailView(Context context, AttributeSet attrs) { - super(context, attrs); - mMatrix = new Matrix(); - mLayoutChanged = true; - } - - @Override - public void onDraw(Canvas canvas) { - if (!mScale) { - super.onDraw(canvas); - return; - } - - Drawable d = getDrawable(); - if (mLayoutChanged) { - int w1 = d.getIntrinsicWidth(); - int h1 = d.getIntrinsicHeight(); - int w2 = getWidth(); - int h2 = getHeight(); - - float scale = ((w2 / h2) < (w1 / h1)) ? (float) h2 / h1 : (float) w2 / w1; - mMatrix.setScale(scale, scale); - } - - int saveCount = canvas.save(); - canvas.concat(mMatrix); - d.draw(canvas); - canvas.restoreToCount(saveCount); - } - - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - // OnLayout.changed isn't a reliable measure of whether or not the size of this view has changed - // neither is onSizeChanged called often enough. Instead, we track changes in size ourselves, and - // only invalidate this matrix if we have a new width/height spec - if (widthMeasureSpec != mWidthSpec || heightMeasureSpec != mHeightSpec) { - mWidthSpec = widthMeasureSpec; - mHeightSpec = heightMeasureSpec; - mLayoutChanged = true; - } - super.onMeasure(widthMeasureSpec, heightMeasureSpec); - } - - @Override - public void setImageDrawable(Drawable drawable) { - if (drawable == null) { - drawable = ContextCompat.getDrawable(getContext(), R.drawable.tab_panel_tab_background); - setScaleType(ScaleType.FIT_XY); - mScale = false; - } else { - mScale = true; - setScaleType(ScaleType.FIT_CENTER); - } - - super.setImageDrawable(drawable); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/widget/TouchDelegateWithReset.java b/mobile/android/base/java/org/mozilla/gecko/widget/TouchDelegateWithReset.java deleted file mode 100644 index 52e0b1fd04..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/widget/TouchDelegateWithReset.java +++ /dev/null @@ -1,134 +0,0 @@ -/* 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.widget; - -import android.graphics.Rect; -import android.view.MotionEvent; -import android.view.TouchDelegate; -import android.view.View; -import android.view.ViewConfiguration; - -/** - * This is a copy of TouchDelegate from - * https://github.com/android/platform_frameworks_base/blob/4b1a8f46d6ec55796bf77fd8921a5a242a219278/core/java/android/view/TouchDelegate.java - * with a fix to reset mDelegateTargeted on each new gesture - the sole substantive change is a new - * else leg in the ACTION_DOWN case of onTouchEvent marked by "START|END BUG FIX" comments. - */ - -/** - * Helper class to handle situations where you want a view to have a larger touch area than its - * actual view bounds. The view whose touch area is changed is called the delegate view. This - * class should be used by an ancestor of the delegate. To use a TouchDelegate, first create an - * instance that specifies the bounds that should be mapped to the delegate and the delegate - * view itself. - *

- * The ancestor should then forward all of its touch events received in its - * {@link android.view.View#onTouchEvent(MotionEvent)} to {@link #onTouchEvent(MotionEvent)}. - *

- */ -public class TouchDelegateWithReset extends TouchDelegate { - - /** - * View that should receive forwarded touch events - */ - private View mDelegateView; - - /** - * Bounds in local coordinates of the containing view that should be mapped to the delegate - * view. This rect is used for initial hit testing. - */ - private Rect mBounds; - - /** - * mBounds inflated to include some slop. This rect is to track whether the motion events - * should be considered to be be within the delegate view. - */ - private Rect mSlopBounds; - - /** - * True if the delegate had been targeted on a down event (intersected mBounds). - */ - private boolean mDelegateTargeted; - - private int mSlop; - - /** - * Constructor - * - * @param bounds Bounds in local coordinates of the containing view that should be mapped to - * the delegate view - * @param delegateView The view that should receive motion events - */ - public TouchDelegateWithReset(Rect bounds, View delegateView) { - super(bounds, delegateView); - - mBounds = bounds; - - mSlop = ViewConfiguration.get(delegateView.getContext()).getScaledTouchSlop(); - mSlopBounds = new Rect(bounds); - mSlopBounds.inset(-mSlop, -mSlop); - mDelegateView = delegateView; - } - - /** - * Will forward touch events to the delegate view if the event is within the bounds - * specified in the constructor. - * - * @param event The touch event to forward - * @return True if the event was forwarded to the delegate, false otherwise. - */ - @Override - public boolean onTouchEvent(MotionEvent event) { - int x = (int)event.getX(); - int y = (int)event.getY(); - boolean sendToDelegate = false; - boolean hit = true; - boolean handled = false; - - switch (event.getAction()) { - case MotionEvent.ACTION_DOWN: - Rect bounds = mBounds; - - if (bounds.contains(x, y)) { - mDelegateTargeted = true; - sendToDelegate = true; - } /* START BUG FIX */ - else { - mDelegateTargeted = false; - } - /* END BUG FIX */ - break; - case MotionEvent.ACTION_UP: - case MotionEvent.ACTION_MOVE: - sendToDelegate = mDelegateTargeted; - if (sendToDelegate) { - Rect slopBounds = mSlopBounds; - if (!slopBounds.contains(x, y)) { - hit = false; - } - } - break; - case MotionEvent.ACTION_CANCEL: - sendToDelegate = mDelegateTargeted; - mDelegateTargeted = false; - break; - } - if (sendToDelegate) { - final View delegateView = mDelegateView; - - if (hit) { - // Offset event coordinates to be inside the target view - event.setLocation(delegateView.getWidth() / 2, delegateView.getHeight() / 2); - } else { - // Offset event coordinates to be outside the target view (in case it does - // something like tracking pressed state) - int slop = mSlop; - event.setLocation(-(slop * 2), -(slop * 2)); - } - handled = delegateView.dispatchTouchEvent(event); - } - return handled; - } -} \ No newline at end of file diff --git a/mobile/android/base/java/org/mozilla/gecko/widget/TwoWayView.java b/mobile/android/base/java/org/mozilla/gecko/widget/TwoWayView.java deleted file mode 100644 index b5ad36ab71..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/widget/TwoWayView.java +++ /dev/null @@ -1,7191 +0,0 @@ -/* - * Copyright (C) 2013 Lucas Rocha - * - * This code is based on bits and pieces of Android's AbsListView, - * Listview, and StaggeredGridView. - * - * Copyright (C) 2012 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.mozilla.gecko.widget; - -import org.mozilla.gecko.R; - -import java.util.ArrayList; -import java.util.List; - -import android.annotation.TargetApi; -import android.content.Context; -import android.content.res.TypedArray; -import android.database.DataSetObserver; -import android.graphics.Canvas; -import android.graphics.Rect; -import android.graphics.drawable.Drawable; -import android.graphics.drawable.TransitionDrawable; -import android.os.Build; -import android.os.Bundle; -import android.os.Parcel; -import android.os.Parcelable; -import android.os.SystemClock; -import android.support.v4.util.LongSparseArray; -import android.support.v4.util.SparseArrayCompat; -import android.support.v4.view.AccessibilityDelegateCompat; -import android.support.v4.view.KeyEventCompat; -import android.support.v4.view.MotionEventCompat; -import android.support.v4.view.VelocityTrackerCompat; -import android.support.v4.view.ViewCompat; -import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat; -import android.support.v4.widget.EdgeEffectCompat; -import android.util.AttributeSet; -import android.util.Log; -import android.util.SparseBooleanArray; -import android.view.ContextMenu.ContextMenuInfo; -import android.view.FocusFinder; -import android.view.HapticFeedbackConstants; -import android.view.KeyEvent; -import android.view.MotionEvent; -import android.view.SoundEffectConstants; -import android.view.VelocityTracker; -import android.view.View; -import android.view.ViewConfiguration; -import android.view.ViewGroup; -import android.view.ViewParent; -import android.view.ViewTreeObserver; -import android.view.accessibility.AccessibilityEvent; -import android.view.accessibility.AccessibilityNodeInfo; -import android.widget.AdapterView; -import android.widget.Checkable; -import android.widget.ListAdapter; -import android.widget.Scroller; - -import static android.os.Build.VERSION_CODES.HONEYCOMB; - -/* - * Implementation Notes: - * - * Some terminology: - * - * index - index of the items that are currently visible - * position - index of the items in the cursor - * - * Given the bi-directional nature of this view, the source code - * usually names variables with 'start' to mean 'top' or 'left'; and - * 'end' to mean 'bottom' or 'right', depending on the current - * orientation of the widget. - */ - -/** - * A view that shows items in a vertical or horizontal scrolling list. - * The items come from the {@link ListAdapter} associated with this view. - */ -public class TwoWayView extends AdapterView implements - ViewTreeObserver.OnTouchModeChangeListener { - private static final String LOGTAG = "TwoWayView"; - - private static final int NO_POSITION = -1; - private static final int INVALID_POINTER = -1; - - public static final int[] STATE_NOTHING = new int[] { 0 }; - - private static final int TOUCH_MODE_REST = -1; - private static final int TOUCH_MODE_DOWN = 0; - private static final int TOUCH_MODE_TAP = 1; - private static final int TOUCH_MODE_DONE_WAITING = 2; - private static final int TOUCH_MODE_DRAGGING = 3; - private static final int TOUCH_MODE_FLINGING = 4; - private static final int TOUCH_MODE_OVERSCROLL = 5; - - private static final int TOUCH_MODE_UNKNOWN = -1; - private static final int TOUCH_MODE_ON = 0; - private static final int TOUCH_MODE_OFF = 1; - - private static final int LAYOUT_NORMAL = 0; - private static final int LAYOUT_FORCE_TOP = 1; - private static final int LAYOUT_SET_SELECTION = 2; - private static final int LAYOUT_FORCE_BOTTOM = 3; - private static final int LAYOUT_SPECIFIC = 4; - private static final int LAYOUT_SYNC = 5; - private static final int LAYOUT_MOVE_SELECTION = 6; - - private static final int SYNC_SELECTED_POSITION = 0; - private static final int SYNC_FIRST_POSITION = 1; - - private static final int SYNC_MAX_DURATION_MILLIS = 100; - - private static final int CHECK_POSITION_SEARCH_DISTANCE = 20; - - private static final float MAX_SCROLL_FACTOR = 0.33f; - - private static final int MIN_SCROLL_PREVIEW_PIXELS = 10; - - public static enum ChoiceMode { - NONE, - SINGLE, - MULTIPLE - } - - public static enum Orientation { - HORIZONTAL, - VERTICAL - } - - private final Context mContext; - - private ListAdapter mAdapter; - - private boolean mIsVertical; - - private int mItemMargin; - - private boolean mInLayout; - private boolean mBlockLayoutRequests; - - private boolean mIsAttached; - - private final RecycleBin mRecycler; - private AdapterDataSetObserver mDataSetObserver; - - private boolean mItemsCanFocus; - - final boolean[] mIsScrap = new boolean[1]; - - private boolean mDataChanged; - private int mItemCount; - private int mOldItemCount; - private boolean mHasStableIds; - private boolean mAreAllItemsSelectable; - - private int mFirstPosition; - private int mSpecificStart; - - private SavedState mPendingSync; - - private PositionScroller mPositionScroller; - private Runnable mPositionScrollAfterLayout; - - private final int mTouchSlop; - private final int mMaximumVelocity; - private final int mFlingVelocity; - private float mLastTouchPos; - private float mTouchRemainderPos; - private int mActivePointerId; - - private final Rect mTempRect; - - private final ArrowScrollFocusResult mArrowScrollFocusResult; - - private Rect mTouchFrame; - private int mMotionPosition; - private CheckForTap mPendingCheckForTap; - private CheckForLongPress mPendingCheckForLongPress; - private CheckForKeyLongPress mPendingCheckForKeyLongPress; - private PerformClick mPerformClick; - private Runnable mTouchModeReset; - private int mResurrectToPosition; - - private boolean mIsChildViewEnabled; - - private boolean mDrawSelectorOnTop; - private Drawable mSelector; - private int mSelectorPosition; - private final Rect mSelectorRect; - - private int mOverScroll; - private final int mOverscrollDistance; - - private boolean mDesiredFocusableState; - private boolean mDesiredFocusableInTouchModeState; - - private SelectionNotifier mSelectionNotifier; - - private boolean mNeedSync; - private int mSyncMode; - private int mSyncPosition; - private long mSyncRowId; - private long mSyncSize; - private int mSelectedStart; - - private int mNextSelectedPosition; - private long mNextSelectedRowId; - private int mSelectedPosition; - private long mSelectedRowId; - private int mOldSelectedPosition; - private long mOldSelectedRowId; - - private ChoiceMode mChoiceMode; - private int mCheckedItemCount; - private SparseBooleanArray mCheckStates; - LongSparseArray mCheckedIdStates; - - private ContextMenuInfo mContextMenuInfo; - - private int mLayoutMode; - private int mTouchMode; - private int mLastTouchMode; - private VelocityTracker mVelocityTracker; - private final Scroller mScroller; - - private EdgeEffectCompat mStartEdge; - private EdgeEffectCompat mEndEdge; - - private OnScrollListener mOnScrollListener; - private int mLastScrollState; - - private View mEmptyView; - - private ListItemAccessibilityDelegate mAccessibilityDelegate; - - private int mLastAccessibilityScrollEventFromIndex; - private int mLastAccessibilityScrollEventToIndex; - - public interface OnScrollListener { - - /** - * The view is not scrolling. Note navigating the list using the trackball counts as - * being in the idle state since these transitions are not animated. - */ - public static int SCROLL_STATE_IDLE = 0; - - /** - * The user is scrolling using touch, and their finger is still on the screen - */ - public static int SCROLL_STATE_TOUCH_SCROLL = 1; - - /** - * The user had previously been scrolling using touch and had performed a fling. The - * animation is now coasting to a stop - */ - public static int SCROLL_STATE_FLING = 2; - - /** - * Callback method to be invoked while the list view or grid view is being scrolled. If the - * view is being scrolled, this method will be called before the next frame of the scroll is - * rendered. In particular, it will be called before any calls to - * {@link android.widget.Adapter#getView(int, View, ViewGroup)}. - * - * @param view The view whose scroll state is being reported - * - * @param scrollState The current scroll state. One of {@link #SCROLL_STATE_IDLE}, - * {@link #SCROLL_STATE_TOUCH_SCROLL} or {@link #SCROLL_STATE_IDLE}. - */ - public void onScrollStateChanged(TwoWayView view, int scrollState); - - /** - * Callback method to be invoked when the list or grid has been scrolled. This will be - * called after the scroll has completed - * @param view The view whose scroll state is being reported - * @param firstVisibleItem the index of the first visible cell (ignore if - * visibleItemCount == 0) - * @param visibleItemCount the number of visible cells - * @param totalItemCount the number of items in the list adaptor - */ - public void onScroll(TwoWayView view, int firstVisibleItem, int visibleItemCount, - int totalItemCount); - } - - /** - * A RecyclerListener is used to receive a notification whenever a View is placed - * inside the RecycleBin's scrap heap. This listener is used to free resources - * associated to Views placed in the RecycleBin. - * - * @see TwoWayView.RecycleBin - * @see TwoWayView#setRecyclerListener(TwoWayView.RecyclerListener) - */ - public static interface RecyclerListener { - /** - * Indicates that the specified View was moved into the recycler's scrap heap. - * The view is not displayed on screen any more and any expensive resource - * associated with the view should be discarded. - * - * @param view - */ - void onMovedToScrapHeap(View view); - } - - public TwoWayView(Context context) { - this(context, null); - } - - public TwoWayView(Context context, AttributeSet attrs) { - this(context, attrs, 0); - } - - public TwoWayView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - - mContext = context; - - mLayoutMode = LAYOUT_NORMAL; - mTouchMode = TOUCH_MODE_REST; - mLastTouchMode = TOUCH_MODE_UNKNOWN; - - mLastScrollState = OnScrollListener.SCROLL_STATE_IDLE; - - final ViewConfiguration vc = ViewConfiguration.get(context); - mTouchSlop = vc.getScaledTouchSlop(); - mMaximumVelocity = vc.getScaledMaximumFlingVelocity(); - mFlingVelocity = vc.getScaledMinimumFlingVelocity(); - mOverscrollDistance = getScaledOverscrollDistance(vc); - - mScroller = new Scroller(context); - - mIsVertical = true; - - mTempRect = new Rect(); - - mArrowScrollFocusResult = new ArrowScrollFocusResult(); - - mSelectorPosition = INVALID_POSITION; - - mSelectorRect = new Rect(); - - mResurrectToPosition = INVALID_POSITION; - - mNextSelectedPosition = INVALID_POSITION; - mNextSelectedRowId = INVALID_ROW_ID; - mSelectedPosition = INVALID_POSITION; - mSelectedRowId = INVALID_ROW_ID; - mOldSelectedPosition = INVALID_POSITION; - mOldSelectedRowId = INVALID_ROW_ID; - - mChoiceMode = ChoiceMode.NONE; - - mRecycler = new RecycleBin(); - - mAreAllItemsSelectable = true; - - setClickable(true); - setFocusableInTouchMode(true); - setWillNotDraw(false); - setAlwaysDrawnWithCacheEnabled(false); - setWillNotDraw(false); - setClipToPadding(false); - - ViewCompat.setOverScrollMode(this, ViewCompat.OVER_SCROLL_IF_CONTENT_SCROLLS); - - TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TwoWayView, defStyle, 0); - - mDrawSelectorOnTop = a.getBoolean( - R.styleable.TwoWayView_android_drawSelectorOnTop, false); - - Drawable d = a.getDrawable(R.styleable.TwoWayView_android_listSelector); - if (d != null) { - setSelector(d); - } - - int orientation = a.getInt(R.styleable.TwoWayView_android_orientation, -1); - if (orientation >= 0) { - setOrientation(Orientation.values()[orientation]); - } - - int choiceMode = a.getInt(R.styleable.TwoWayView_android_choiceMode, -1); - if (choiceMode >= 0) { - setChoiceMode(ChoiceMode.values()[choiceMode]); - } - - a.recycle(); - } - - public void setOrientation(Orientation orientation) { - final boolean isVertical = (orientation == Orientation.VERTICAL); - if (mIsVertical == isVertical) { - return; - } - - mIsVertical = isVertical; - - resetState(); - mRecycler.clear(); - - requestLayout(); - } - - public Orientation getOrientation() { - return (mIsVertical ? Orientation.VERTICAL : Orientation.HORIZONTAL); - } - - public void setItemMargin(int itemMargin) { - if (mItemMargin == itemMargin) { - return; - } - - mItemMargin = itemMargin; - requestLayout(); - } - - @SuppressWarnings("unused") - public int getItemMargin() { - return mItemMargin; - } - - /** - * Indicates that the views created by the ListAdapter can contain focusable - * items. - * - * @param itemsCanFocus true if items can get focus, false otherwise - */ - @SuppressWarnings("unused") - public void setItemsCanFocus(boolean itemsCanFocus) { - mItemsCanFocus = itemsCanFocus; - if (!itemsCanFocus) { - setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS); - } - } - - /** - * @return Whether the views created by the ListAdapter can contain focusable - * items. - */ - @SuppressWarnings("unused") - public boolean getItemsCanFocus() { - return mItemsCanFocus; - } - - /** - * Set the listener that will receive notifications every time the list scrolls. - * - * @param l the scroll listener - */ - public void setOnScrollListener(OnScrollListener l) { - mOnScrollListener = l; - invokeOnItemScrollListener(); - } - - /** - * Sets the recycler listener to be notified whenever a View is set aside in - * the recycler for later reuse. This listener can be used to free resources - * associated to the View. - * - * @param l The recycler listener to be notified of views set aside - * in the recycler. - * - * @see TwoWayView.RecycleBin - * @see TwoWayView.RecyclerListener - */ - public void setRecyclerListener(RecyclerListener l) { - mRecycler.mRecyclerListener = l; - } - - /** - * Controls whether the selection highlight drawable should be drawn on top of the item or - * behind it. - * - * @param drawSelectorOnTop If true, the selector will be drawn on the item it is highlighting. - * The default is false. - * - * @attr ref android.R.styleable#AbsListView_drawSelectorOnTop - */ - @SuppressWarnings("unused") - public void setDrawSelectorOnTop(boolean drawSelectorOnTop) { - mDrawSelectorOnTop = drawSelectorOnTop; - } - - /** - * Set a Drawable that should be used to highlight the currently selected item. - * - * @param resID A Drawable resource to use as the selection highlight. - * - * @attr ref android.R.styleable#AbsListView_listSelector - */ - @SuppressWarnings("unused") - public void setSelector(int resID) { - setSelector(getResources().getDrawable(resID)); - } - - /** - * Set a Drawable that should be used to highlight the currently selected item. - * - * @param selector A Drawable to use as the selection highlight. - * - * @attr ref android.R.styleable#AbsListView_listSelector - */ - public void setSelector(Drawable selector) { - if (mSelector != null) { - mSelector.setCallback(null); - unscheduleDrawable(mSelector); - } - - mSelector = selector; - Rect padding = new Rect(); - selector.getPadding(padding); - - selector.setCallback(this); - updateSelectorState(); - } - - /** - * Returns the selector {@link android.graphics.drawable.Drawable} that is used to draw the - * selection in the list. - * - * @return the drawable used to display the selector - */ - @SuppressWarnings("unused") - public Drawable getSelector() { - return mSelector; - } - - /** - * {@inheritDoc} - */ - @Override - public int getSelectedItemPosition() { - return mNextSelectedPosition; - } - - /** - * {@inheritDoc} - */ - @Override - public long getSelectedItemId() { - return mNextSelectedRowId; - } - - /** - * Returns the number of items currently selected. This will only be valid - * if the choice mode is not {@link ChoiceMode#NONE} (default). - * - *

To determine the specific items that are currently selected, use one of - * the getChecked* methods. - * - * @return The number of items currently selected - * - * @see #getCheckedItemPosition() - * @see #getCheckedItemPositions() - * @see #getCheckedItemIds() - */ - @SuppressWarnings("unused") - public int getCheckedItemCount() { - return mCheckedItemCount; - } - - /** - * Returns the checked state of the specified position. The result is only - * valid if the choice mode has been set to {@link ChoiceMode#SINGLE} - * or {@link ChoiceMode#MULTIPLE}. - * - * @param position The item whose checked state to return - * @return The item's checked state or false if choice mode - * is invalid - * - * @see #setChoiceMode(ChoiceMode) - */ - public boolean isItemChecked(int position) { - if (mChoiceMode == ChoiceMode.NONE && mCheckStates != null) { - return mCheckStates.get(position); - } - - return false; - } - - /** - * Returns the currently checked item. The result is only valid if the choice - * mode has been set to {@link ChoiceMode#SINGLE}. - * - * @return The position of the currently checked item or - * {@link #INVALID_POSITION} if nothing is selected - * - * @see #setChoiceMode(ChoiceMode) - */ - public int getCheckedItemPosition() { - if (mChoiceMode == ChoiceMode.SINGLE && mCheckStates != null && mCheckStates.size() == 1) { - return mCheckStates.keyAt(0); - } - - return INVALID_POSITION; - } - - /** - * Returns the set of checked items in the list. The result is only valid if - * the choice mode has not been set to {@link ChoiceMode#NONE}. - * - * @return A SparseBooleanArray which will return true for each call to - * get(int position) where position is a position in the list, - * or null if the choice mode is set to - * {@link ChoiceMode#NONE}. - */ - public SparseBooleanArray getCheckedItemPositions() { - if (mChoiceMode != ChoiceMode.NONE) { - return mCheckStates; - } - - return null; - } - - /** - * Returns the set of checked items ids. The result is only valid if the - * choice mode has not been set to {@link ChoiceMode#NONE} and the adapter - * has stable IDs. ({@link ListAdapter#hasStableIds()} == {@code true}) - * - * @return A new array which contains the id of each checked item in the - * list. - */ - public long[] getCheckedItemIds() { - if (mChoiceMode == ChoiceMode.NONE || mCheckedIdStates == null || mAdapter == null) { - return new long[0]; - } - - final LongSparseArray idStates = mCheckedIdStates; - final int count = idStates.size(); - final long[] ids = new long[count]; - - for (int i = 0; i < count; i++) { - ids[i] = idStates.keyAt(i); - } - - return ids; - } - - /** - * Sets the checked state of the specified position. The is only valid if - * the choice mode has been set to {@link ChoiceMode#SINGLE} or - * {@link ChoiceMode#MULTIPLE}. - * - * @param position The item whose checked state is to be checked - * @param value The new checked state for the item - */ - @SuppressWarnings("unused") - public void setItemChecked(int position, boolean value) { - if (mChoiceMode == ChoiceMode.NONE) { - return; - } - - if (mChoiceMode == ChoiceMode.MULTIPLE) { - boolean oldValue = mCheckStates.get(position); - mCheckStates.put(position, value); - - if (mCheckedIdStates != null && mAdapter.hasStableIds()) { - if (value) { - mCheckedIdStates.put(mAdapter.getItemId(position), position); - } else { - mCheckedIdStates.delete(mAdapter.getItemId(position)); - } - } - - if (oldValue != value) { - if (value) { - mCheckedItemCount++; - } else { - mCheckedItemCount--; - } - } - } else { - boolean updateIds = mCheckedIdStates != null && mAdapter.hasStableIds(); - - // Clear all values if we're checking something, or unchecking the currently - // selected item - if (value || isItemChecked(position)) { - mCheckStates.clear(); - - if (updateIds) { - mCheckedIdStates.clear(); - } - } - - // This may end up selecting the value we just cleared but this way - // we ensure length of mCheckStates is 1, a fact getCheckedItemPosition relies on - if (value) { - mCheckStates.put(position, true); - - if (updateIds) { - mCheckedIdStates.put(mAdapter.getItemId(position), position); - } - - mCheckedItemCount = 1; - } else if (mCheckStates.size() == 0 || !mCheckStates.valueAt(0)) { - mCheckedItemCount = 0; - } - } - - // Do not generate a data change while we are in the layout phase - if (!mInLayout && !mBlockLayoutRequests) { - mDataChanged = true; - rememberSyncState(); - requestLayout(); - } - } - - /** - * Clear any choices previously set - */ - @SuppressWarnings("unused") - public void clearChoices() { - if (mCheckStates != null) { - mCheckStates.clear(); - } - - if (mCheckedIdStates != null) { - mCheckedIdStates.clear(); - } - - mCheckedItemCount = 0; - } - - /** - * @see #setChoiceMode(ChoiceMode) - * - * @return The current choice mode - */ - @SuppressWarnings("unused") - public ChoiceMode getChoiceMode() { - return mChoiceMode; - } - - /** - * Defines the choice behavior for the List. By default, Lists do not have any choice behavior - * ({@link ChoiceMode#NONE}). By setting the choiceMode to {@link ChoiceMode#SINGLE}, the - * List allows up to one item to be in a chosen state. By setting the choiceMode to - * {@link ChoiceMode#MULTIPLE}, the list allows any number of items to be chosen. - * - * @param choiceMode One of {@link ChoiceMode#NONE}, {@link ChoiceMode#SINGLE}, or - * {@link ChoiceMode#MULTIPLE} - */ - public void setChoiceMode(ChoiceMode choiceMode) { - mChoiceMode = choiceMode; - - if (mChoiceMode != ChoiceMode.NONE) { - if (mCheckStates == null) { - mCheckStates = new SparseBooleanArray(); - } - - if (mCheckedIdStates == null && mAdapter != null && mAdapter.hasStableIds()) { - mCheckedIdStates = new LongSparseArray(); - } - } - } - - @Override - public ListAdapter getAdapter() { - return mAdapter; - } - - @Override - public void setAdapter(ListAdapter adapter) { - if (mAdapter != null && mDataSetObserver != null) { - mAdapter.unregisterDataSetObserver(mDataSetObserver); - } - - resetState(); - mRecycler.clear(); - - mAdapter = adapter; - mDataChanged = true; - - mOldSelectedPosition = INVALID_POSITION; - mOldSelectedRowId = INVALID_ROW_ID; - - if (mCheckStates != null) { - mCheckStates.clear(); - } - - if (mCheckedIdStates != null) { - mCheckedIdStates.clear(); - } - - if (mAdapter != null) { - mOldItemCount = mItemCount; - mItemCount = adapter.getCount(); - - mDataSetObserver = new AdapterDataSetObserver(); - mAdapter.registerDataSetObserver(mDataSetObserver); - - mRecycler.setViewTypeCount(adapter.getViewTypeCount()); - - mHasStableIds = adapter.hasStableIds(); - mAreAllItemsSelectable = adapter.areAllItemsEnabled(); - - if (mChoiceMode != ChoiceMode.NONE && mHasStableIds && mCheckedIdStates == null) { - mCheckedIdStates = new LongSparseArray(); - } - - final int position = lookForSelectablePosition(0); - setSelectedPositionInt(position); - setNextSelectedPositionInt(position); - - if (mItemCount == 0) { - checkSelectionChanged(); - } - } else { - mItemCount = 0; - mHasStableIds = false; - mAreAllItemsSelectable = true; - - checkSelectionChanged(); - } - - checkFocus(); - requestLayout(); - } - - @Override - public int getFirstVisiblePosition() { - return mFirstPosition; - } - - @Override - public int getLastVisiblePosition() { - return mFirstPosition + getChildCount() - 1; - } - - @Override - public int getCount() { - return mItemCount; - } - - @Override - public int getPositionForView(View view) { - View child = view; - try { - View v; - while (!(v = (View) child.getParent()).equals(this)) { - child = v; - } - } catch (ClassCastException e) { - // We made it up to the window without find this list view - return INVALID_POSITION; - } - - // Search the children for the list item - final int childCount = getChildCount(); - for (int i = 0; i < childCount; i++) { - if (getChildAt(i).equals(child)) { - return mFirstPosition + i; - } - } - - // Child not found! - return INVALID_POSITION; - } - - @Override - public void getFocusedRect(Rect r) { - View view = getSelectedView(); - - if (view != null && view.getParent() == this) { - // The focused rectangle of the selected view offset into the - // coordinate space of this view. - view.getFocusedRect(r); - offsetDescendantRectToMyCoords(view, r); - } else { - super.getFocusedRect(r); - } - } - - @Override - protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) { - super.onFocusChanged(gainFocus, direction, previouslyFocusedRect); - - if (gainFocus && mSelectedPosition < 0 && !isInTouchMode()) { - if (!mIsAttached && mAdapter != null) { - // Data may have changed while we were detached and it's valid - // to change focus while detached. Refresh so we don't die. - mDataChanged = true; - mOldItemCount = mItemCount; - mItemCount = mAdapter.getCount(); - } - - resurrectSelection(); - } - - final ListAdapter adapter = mAdapter; - int closetChildIndex = INVALID_POSITION; - int closestChildStart = 0; - - if (adapter != null && gainFocus && previouslyFocusedRect != null) { - previouslyFocusedRect.offset(getScrollX(), getScrollY()); - - // Don't cache the result of getChildCount or mFirstPosition here, - // it could change in layoutChildren. - if (adapter.getCount() < getChildCount() + mFirstPosition) { - mLayoutMode = LAYOUT_NORMAL; - layoutChildren(); - } - - // Figure out which item should be selected based on previously - // focused rect. - Rect otherRect = mTempRect; - int minDistance = Integer.MAX_VALUE; - final int childCount = getChildCount(); - final int firstPosition = mFirstPosition; - - for (int i = 0; i < childCount; i++) { - // Only consider selectable views - if (!adapter.isEnabled(firstPosition + i)) { - continue; - } - - View other = getChildAt(i); - other.getDrawingRect(otherRect); - offsetDescendantRectToMyCoords(other, otherRect); - int distance = getDistance(previouslyFocusedRect, otherRect, direction); - - if (distance < minDistance) { - minDistance = distance; - closetChildIndex = i; - closestChildStart = getChildStartEdge(other); - } - } - } - - if (closetChildIndex >= 0) { - setSelectionFromOffset(closetChildIndex + mFirstPosition, closestChildStart); - } else { - requestLayout(); - } - } - - @Override - protected void onAttachedToWindow() { - super.onAttachedToWindow(); - - final ViewTreeObserver treeObserver = getViewTreeObserver(); - treeObserver.addOnTouchModeChangeListener(this); - - if (mAdapter != null && mDataSetObserver == null) { - mDataSetObserver = new AdapterDataSetObserver(); - mAdapter.registerDataSetObserver(mDataSetObserver); - - // Data may have changed while we were detached. Refresh. - mDataChanged = true; - mOldItemCount = mItemCount; - mItemCount = mAdapter.getCount(); - } - - mIsAttached = true; - } - - @Override - protected void onDetachedFromWindow() { - super.onDetachedFromWindow(); - - // Detach any view left in the scrap heap - mRecycler.clear(); - - final ViewTreeObserver treeObserver = getViewTreeObserver(); - treeObserver.removeOnTouchModeChangeListener(this); - - if (mAdapter != null) { - mAdapter.unregisterDataSetObserver(mDataSetObserver); - mDataSetObserver = null; - } - - if (mPerformClick != null) { - removeCallbacks(mPerformClick); - } - - if (mTouchModeReset != null) { - removeCallbacks(mTouchModeReset); - mTouchModeReset.run(); - } - - finishSmoothScrolling(); - - mIsAttached = false; - } - - @Override - public void onWindowFocusChanged(boolean hasWindowFocus) { - super.onWindowFocusChanged(hasWindowFocus); - - final int touchMode = isInTouchMode() ? TOUCH_MODE_ON : TOUCH_MODE_OFF; - - if (!hasWindowFocus) { - if (!mScroller.isFinished()) { - finishSmoothScrolling(); - if (mOverScroll != 0) { - mOverScroll = 0; - finishEdgeGlows(); - invalidate(); - } - } - - if (touchMode == TOUCH_MODE_OFF) { - // Remember the last selected element - mResurrectToPosition = mSelectedPosition; - } - } else { - // If we changed touch mode since the last time we had focus - if (touchMode != mLastTouchMode && mLastTouchMode != TOUCH_MODE_UNKNOWN) { - // If we come back in trackball mode, we bring the selection back - if (touchMode == TOUCH_MODE_OFF) { - // This will trigger a layout - resurrectSelection(); - - // If we come back in touch mode, then we want to hide the selector - } else { - hideSelector(); - mLayoutMode = LAYOUT_NORMAL; - layoutChildren(); - } - } - } - - mLastTouchMode = touchMode; - } - - @Override - protected void onOverScrolled(int scrollX, int scrollY, boolean clampedX, boolean clampedY) { - boolean needsInvalidate = false; - - if (mIsVertical && mOverScroll != scrollY) { - onScrollChanged(getScrollX(), scrollY, getScrollX(), mOverScroll); - mOverScroll = scrollY; - needsInvalidate = true; - } else if (!mIsVertical && mOverScroll != scrollX) { - onScrollChanged(scrollX, getScrollY(), mOverScroll, getScrollY()); - mOverScroll = scrollX; - needsInvalidate = true; - } - - if (needsInvalidate) { - invalidate(); - awakenScrollbarsInternal(); - } - } - - @TargetApi(9) - private boolean overScrollByInternal(int deltaX, int deltaY, - int scrollX, int scrollY, - int scrollRangeX, int scrollRangeY, - int maxOverScrollX, int maxOverScrollY, - boolean isTouchEvent) { - if (Build.VERSION.SDK_INT < 9) { - return false; - } - - return super.overScrollBy(deltaX, deltaY, scrollX, scrollY, scrollRangeX, - scrollRangeY, maxOverScrollX, maxOverScrollY, isTouchEvent); - } - - @Override - @TargetApi(9) - public void setOverScrollMode(int mode) { - if (Build.VERSION.SDK_INT < 9) { - return; - } - - if (mode != ViewCompat.OVER_SCROLL_NEVER) { - if (mStartEdge == null) { - Context context = getContext(); - - mStartEdge = new EdgeEffectCompat(context); - mEndEdge = new EdgeEffectCompat(context); - } - } else { - mStartEdge = null; - mEndEdge = null; - } - - super.setOverScrollMode(mode); - } - - public int pointToPosition(int x, int y) { - Rect frame = mTouchFrame; - if (frame == null) { - mTouchFrame = new Rect(); - frame = mTouchFrame; - } - - final int count = getChildCount(); - for (int i = count - 1; i >= 0; i--) { - final View child = getChildAt(i); - - if (child.getVisibility() == View.VISIBLE) { - child.getHitRect(frame); - - if (frame.contains(x, y)) { - return mFirstPosition + i; - } - } - } - return INVALID_POSITION; - } - - @Override - protected float getTopFadingEdgeStrength() { - if (!mIsVertical) { - return 0f; - } - - final float fadingEdge = super.getTopFadingEdgeStrength(); - - final int childCount = getChildCount(); - if (childCount == 0) { - return fadingEdge; - } else { - if (mFirstPosition > 0) { - return 1.0f; - } - - final int top = getChildAt(0).getTop(); - final int paddingTop = getPaddingTop(); - - final float length = (float) getVerticalFadingEdgeLength(); - - return (top < paddingTop ? (float) -(top - paddingTop) / length : fadingEdge); - } - } - - @Override - protected float getBottomFadingEdgeStrength() { - if (!mIsVertical) { - return 0f; - } - - final float fadingEdge = super.getBottomFadingEdgeStrength(); - - final int childCount = getChildCount(); - if (childCount == 0) { - return fadingEdge; - } else { - if (mFirstPosition + childCount - 1 < mItemCount - 1) { - return 1.0f; - } - - final int bottom = getChildAt(childCount - 1).getBottom(); - final int paddingBottom = getPaddingBottom(); - - final int height = getHeight(); - final float length = (float) getVerticalFadingEdgeLength(); - - return (bottom > height - paddingBottom ? - (float) (bottom - height + paddingBottom) / length : fadingEdge); - } - } - - @Override - protected float getLeftFadingEdgeStrength() { - if (mIsVertical) { - return 0f; - } - - final float fadingEdge = super.getLeftFadingEdgeStrength(); - - final int childCount = getChildCount(); - if (childCount == 0) { - return fadingEdge; - } else { - if (mFirstPosition > 0) { - return 1.0f; - } - - final int left = getChildAt(0).getLeft(); - final int paddingLeft = getPaddingLeft(); - - final float length = (float) getHorizontalFadingEdgeLength(); - - return (left < paddingLeft ? (float) -(left - paddingLeft) / length : fadingEdge); - } - } - - @Override - protected float getRightFadingEdgeStrength() { - if (mIsVertical) { - return 0f; - } - - final float fadingEdge = super.getRightFadingEdgeStrength(); - - final int childCount = getChildCount(); - if (childCount == 0) { - return fadingEdge; - } else { - if (mFirstPosition + childCount - 1 < mItemCount - 1) { - return 1.0f; - } - - final int right = getChildAt(childCount - 1).getRight(); - final int paddingRight = getPaddingRight(); - - final int width = getWidth(); - final float length = (float) getHorizontalFadingEdgeLength(); - - return (right > width - paddingRight ? - (float) (right - width + paddingRight) / length : fadingEdge); - } - } - - @Override - protected int computeVerticalScrollExtent() { - final int count = getChildCount(); - if (count == 0) { - return 0; - } - - int extent = count * 100; - - View child = getChildAt(0); - final int childTop = child.getTop(); - - int childHeight = child.getHeight(); - if (childHeight > 0) { - extent += (childTop * 100) / childHeight; - } - - child = getChildAt(count - 1); - final int childBottom = child.getBottom(); - - childHeight = child.getHeight(); - if (childHeight > 0) { - extent -= ((childBottom - getHeight()) * 100) / childHeight; - } - - return extent; - } - - @Override - protected int computeHorizontalScrollExtent() { - final int count = getChildCount(); - if (count == 0) { - return 0; - } - - int extent = count * 100; - - View child = getChildAt(0); - final int childLeft = child.getLeft(); - - int childWidth = child.getWidth(); - if (childWidth > 0) { - extent += (childLeft * 100) / childWidth; - } - - child = getChildAt(count - 1); - final int childRight = child.getRight(); - - childWidth = child.getWidth(); - if (childWidth > 0) { - extent -= ((childRight - getWidth()) * 100) / childWidth; - } - - return extent; - } - - @Override - protected int computeVerticalScrollOffset() { - final int firstPosition = mFirstPosition; - final int childCount = getChildCount(); - - if (firstPosition < 0 || childCount == 0) { - return 0; - } - - final View child = getChildAt(0); - final int childTop = child.getTop(); - - int childHeight = child.getHeight(); - if (childHeight > 0) { - return Math.max(firstPosition * 100 - (childTop * 100) / childHeight, 0); - } - - return 0; - } - - @Override - protected int computeHorizontalScrollOffset() { - final int firstPosition = mFirstPosition; - final int childCount = getChildCount(); - - if (firstPosition < 0 || childCount == 0) { - return 0; - } - - final View child = getChildAt(0); - final int childLeft = child.getLeft(); - - int childWidth = child.getWidth(); - if (childWidth > 0) { - return Math.max(firstPosition * 100 - (childLeft * 100) / childWidth, 0); - } - - return 0; - } - - @Override - protected int computeVerticalScrollRange() { - int result = Math.max(mItemCount * 100, 0); - - if (mIsVertical && mOverScroll != 0) { - // Compensate for overscroll - result += Math.abs((int) ((float) mOverScroll / getHeight() * mItemCount * 100)); - } - - return result; - } - - @Override - protected int computeHorizontalScrollRange() { - int result = Math.max(mItemCount * 100, 0); - - if (!mIsVertical && mOverScroll != 0) { - // Compensate for overscroll - result += Math.abs((int) ((float) mOverScroll / getWidth() * mItemCount * 100)); - } - - return result; - } - - @Override - public boolean showContextMenuForChild(View originalView) { - final int longPressPosition = getPositionForView(originalView); - if (longPressPosition >= 0) { - final long longPressId = mAdapter.getItemId(longPressPosition); - boolean handled = false; - - OnItemLongClickListener listener = getOnItemLongClickListener(); - if (listener != null) { - handled = listener.onItemLongClick(TwoWayView.this, originalView, - longPressPosition, longPressId); - } - - if (!handled) { - mContextMenuInfo = createContextMenuInfo( - getChildAt(longPressPosition - mFirstPosition), - longPressPosition, longPressId); - - handled = super.showContextMenuForChild(originalView); - } - - return handled; - } - - return false; - } - - @Override - public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) { - if (disallowIntercept) { - recycleVelocityTracker(); - } - - super.requestDisallowInterceptTouchEvent(disallowIntercept); - } - - @Override - public boolean onInterceptTouchEvent(MotionEvent ev) { - if (!mIsAttached || mAdapter == null) { - return false; - } - - final int action = ev.getAction() & MotionEventCompat.ACTION_MASK; - switch (action) { - case MotionEvent.ACTION_DOWN: - initOrResetVelocityTracker(); - mVelocityTracker.addMovement(ev); - - mScroller.abortAnimation(); - if (mPositionScroller != null) { - mPositionScroller.stop(); - } - - final float x = ev.getX(); - final float y = ev.getY(); - - mLastTouchPos = (mIsVertical ? y : x); - - final int motionPosition = findMotionRowOrColumn((int) mLastTouchPos); - - mActivePointerId = MotionEventCompat.getPointerId(ev, 0); - mTouchRemainderPos = 0; - - if (mTouchMode == TOUCH_MODE_FLINGING) { - return true; - } else if (motionPosition >= 0) { - mMotionPosition = motionPosition; - mTouchMode = TOUCH_MODE_DOWN; - } - - break; - - case MotionEvent.ACTION_MOVE: { - if (mTouchMode != TOUCH_MODE_DOWN) { - break; - } - - initVelocityTrackerIfNotExists(); - mVelocityTracker.addMovement(ev); - - final int index = MotionEventCompat.findPointerIndex(ev, mActivePointerId); - if (index < 0) { - Log.e(LOGTAG, "onInterceptTouchEvent could not find pointer with id " + - mActivePointerId + " - did TwoWayView receive an inconsistent " + - "event stream?"); - return false; - } - - final float pos; - if (mIsVertical) { - pos = MotionEventCompat.getY(ev, index); - } else { - pos = MotionEventCompat.getX(ev, index); - } - - final float diff = pos - mLastTouchPos + mTouchRemainderPos; - final int delta = (int) diff; - mTouchRemainderPos = diff - delta; - - if (maybeStartScrolling(delta)) { - return true; - } - - break; - } - - case MotionEvent.ACTION_CANCEL: - case MotionEvent.ACTION_UP: - mActivePointerId = INVALID_POINTER; - mTouchMode = TOUCH_MODE_REST; - recycleVelocityTracker(); - reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE); - - break; - } - - return false; - } - - @Override - public boolean onTouchEvent(MotionEvent ev) { - if (!isEnabled()) { - // A disabled view that is clickable still consumes the touch - // events, it just doesn't respond to them. - return isClickable() || isLongClickable(); - } - - if (!mIsAttached || mAdapter == null) { - return false; - } - - boolean needsInvalidate = false; - - initVelocityTrackerIfNotExists(); - mVelocityTracker.addMovement(ev); - - final int action = ev.getAction() & MotionEventCompat.ACTION_MASK; - switch (action) { - case MotionEvent.ACTION_DOWN: { - if (mDataChanged) { - break; - } - - mVelocityTracker.clear(); - mScroller.abortAnimation(); - if (mPositionScroller != null) { - mPositionScroller.stop(); - } - - final float x = ev.getX(); - final float y = ev.getY(); - - mLastTouchPos = (mIsVertical ? y : x); - - int motionPosition = pointToPosition((int) x, (int) y); - - mActivePointerId = MotionEventCompat.getPointerId(ev, 0); - mTouchRemainderPos = 0; - - if (mDataChanged) { - break; - } - - if (mTouchMode == TOUCH_MODE_FLINGING) { - mTouchMode = TOUCH_MODE_DRAGGING; - reportScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL); - motionPosition = findMotionRowOrColumn((int) mLastTouchPos); - } else if (mMotionPosition >= 0 && mAdapter.isEnabled(mMotionPosition)) { - mTouchMode = TOUCH_MODE_DOWN; - triggerCheckForTap(); - } - - mMotionPosition = motionPosition; - - break; - } - - case MotionEvent.ACTION_MOVE: { - final int index = MotionEventCompat.findPointerIndex(ev, mActivePointerId); - if (index < 0) { - Log.e(LOGTAG, "onInterceptTouchEvent could not find pointer with id " + - mActivePointerId + " - did TwoWayView receive an inconsistent " + - "event stream?"); - return false; - } - - final float pos; - if (mIsVertical) { - pos = MotionEventCompat.getY(ev, index); - } else { - pos = MotionEventCompat.getX(ev, index); - } - - if (mDataChanged) { - // Re-sync everything if data has been changed - // since the scroll operation can query the adapter. - layoutChildren(); - } - - final float diff = pos - mLastTouchPos + mTouchRemainderPos; - final int delta = (int) diff; - mTouchRemainderPos = diff - delta; - - switch (mTouchMode) { - case TOUCH_MODE_DOWN: - case TOUCH_MODE_TAP: - case TOUCH_MODE_DONE_WAITING: - // Check if we have moved far enough that it looks more like a - // scroll than a tap - maybeStartScrolling(delta); - break; - - case TOUCH_MODE_DRAGGING: - case TOUCH_MODE_OVERSCROLL: - mLastTouchPos = pos; - maybeScroll(delta); - break; - } - - break; - } - - case MotionEvent.ACTION_CANCEL: - cancelCheckForTap(); - mTouchMode = TOUCH_MODE_REST; - reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE); - - setPressed(false); - View motionView = this.getChildAt(mMotionPosition - mFirstPosition); - if (motionView != null) { - motionView.setPressed(false); - } - - if (mStartEdge != null && mEndEdge != null) { - needsInvalidate = mStartEdge.onRelease() | mEndEdge.onRelease(); - } - - recycleVelocityTracker(); - - break; - - case MotionEvent.ACTION_UP: { - switch (mTouchMode) { - case TOUCH_MODE_DOWN: - case TOUCH_MODE_TAP: - case TOUCH_MODE_DONE_WAITING: { - final int motionPosition = mMotionPosition; - final View child = getChildAt(motionPosition - mFirstPosition); - - final float x = ev.getX(); - final float y = ev.getY(); - - final boolean inList; - if (mIsVertical) { - inList = x > getPaddingLeft() && x < getWidth() - getPaddingRight(); - } else { - inList = y > getPaddingTop() && y < getHeight() - getPaddingBottom(); - } - - if (child != null && !child.hasFocusable() && inList) { - if (mTouchMode != TOUCH_MODE_DOWN) { - child.setPressed(false); - } - - if (mPerformClick == null) { - mPerformClick = new PerformClick(); - } - - final PerformClick performClick = mPerformClick; - performClick.mClickMotionPosition = motionPosition; - performClick.rememberWindowAttachCount(); - - mResurrectToPosition = motionPosition; - - if (mTouchMode == TOUCH_MODE_DOWN || mTouchMode == TOUCH_MODE_TAP) { - if (mTouchMode == TOUCH_MODE_DOWN) { - cancelCheckForTap(); - } else { - cancelCheckForLongPress(); - } - - mLayoutMode = LAYOUT_NORMAL; - - if (!mDataChanged && mAdapter.isEnabled(motionPosition)) { - mTouchMode = TOUCH_MODE_TAP; - - setPressed(true); - positionSelector(mMotionPosition, child); - child.setPressed(true); - - if (mSelector != null) { - Drawable d = mSelector.getCurrent(); - if (d != null && d instanceof TransitionDrawable) { - ((TransitionDrawable) d).resetTransition(); - } - } - - if (mTouchModeReset != null) { - removeCallbacks(mTouchModeReset); - } - - mTouchModeReset = new Runnable() { - @Override - public void run() { - mTouchMode = TOUCH_MODE_REST; - - setPressed(false); - child.setPressed(false); - - if (!mDataChanged) { - performClick.run(); - } - - mTouchModeReset = null; - } - }; - - postDelayed(mTouchModeReset, - ViewConfiguration.getPressedStateDuration()); - } else { - mTouchMode = TOUCH_MODE_REST; - updateSelectorState(); - } - } else if (!mDataChanged && mAdapter.isEnabled(motionPosition)) { - performClick.run(); - } - } - - mTouchMode = TOUCH_MODE_REST; - - finishSmoothScrolling(); - updateSelectorState(); - - break; - } - - case TOUCH_MODE_DRAGGING: - if (contentFits()) { - mTouchMode = TOUCH_MODE_REST; - reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE); - break; - } - - mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); - - final float velocity; - if (mIsVertical) { - velocity = VelocityTrackerCompat.getYVelocity(mVelocityTracker, - mActivePointerId); - } else { - velocity = VelocityTrackerCompat.getXVelocity(mVelocityTracker, - mActivePointerId); - } - - if (Math.abs(velocity) >= mFlingVelocity) { - mTouchMode = TOUCH_MODE_FLINGING; - reportScrollStateChange(OnScrollListener.SCROLL_STATE_FLING); - - mScroller.fling(0, 0, - (int) (mIsVertical ? 0 : velocity), - (int) (mIsVertical ? velocity : 0), - (mIsVertical ? 0 : Integer.MIN_VALUE), - (mIsVertical ? 0 : Integer.MAX_VALUE), - (mIsVertical ? Integer.MIN_VALUE : 0), - (mIsVertical ? Integer.MAX_VALUE : 0)); - - mLastTouchPos = 0; - needsInvalidate = true; - } else { - mTouchMode = TOUCH_MODE_REST; - reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE); - } - - break; - - case TOUCH_MODE_OVERSCROLL: - mTouchMode = TOUCH_MODE_REST; - reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE); - break; - } - - cancelCheckForTap(); - cancelCheckForLongPress(); - setPressed(false); - - if (mStartEdge != null && mEndEdge != null) { - needsInvalidate |= mStartEdge.onRelease() | mEndEdge.onRelease(); - } - - recycleVelocityTracker(); - - break; - } - } - - if (needsInvalidate) { - ViewCompat.postInvalidateOnAnimation(this); - } - - return true; - } - - @Override - public void onTouchModeChanged(boolean isInTouchMode) { - if (isInTouchMode) { - // Get rid of the selection when we enter touch mode - hideSelector(); - - // Layout, but only if we already have done so previously. - // (Otherwise may clobber a LAYOUT_SYNC layout that was requested to restore - // state.) - if (getWidth() > 0 && getHeight() > 0 && getChildCount() > 0) { - layoutChildren(); - } - - updateSelectorState(); - } else { - final int touchMode = mTouchMode; - if (touchMode == TOUCH_MODE_OVERSCROLL) { - finishSmoothScrolling(); - if (mOverScroll != 0) { - mOverScroll = 0; - finishEdgeGlows(); - invalidate(); - } - } - } - } - - @Override - public boolean onKeyDown(int keyCode, KeyEvent event) { - return handleKeyEvent(keyCode, 1, event); - } - - @Override - public boolean onKeyMultiple(int keyCode, int repeatCount, KeyEvent event) { - return handleKeyEvent(keyCode, repeatCount, event); - } - - @Override - public boolean onKeyUp(int keyCode, KeyEvent event) { - return handleKeyEvent(keyCode, 1, event); - } - - @Override - public void sendAccessibilityEvent(int eventType) { - // Since this class calls onScrollChanged even if the mFirstPosition and the - // child count have not changed we will avoid sending duplicate accessibility - // events. - if (eventType == AccessibilityEvent.TYPE_VIEW_SCROLLED) { - final int firstVisiblePosition = getFirstVisiblePosition(); - final int lastVisiblePosition = getLastVisiblePosition(); - - if (mLastAccessibilityScrollEventFromIndex == firstVisiblePosition - && mLastAccessibilityScrollEventToIndex == lastVisiblePosition) { - return; - } else { - mLastAccessibilityScrollEventFromIndex = firstVisiblePosition; - mLastAccessibilityScrollEventToIndex = lastVisiblePosition; - } - } - - super.sendAccessibilityEvent(eventType); - } - - @Override - @TargetApi(14) - public void onInitializeAccessibilityEvent(AccessibilityEvent event) { - super.onInitializeAccessibilityEvent(event); - event.setClassName(TwoWayView.class.getName()); - } - - @Override - @TargetApi(14) - public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { - super.onInitializeAccessibilityNodeInfo(info); - info.setClassName(TwoWayView.class.getName()); - - AccessibilityNodeInfoCompat infoCompat = new AccessibilityNodeInfoCompat(info); - - if (isEnabled()) { - if (getFirstVisiblePosition() > 0) { - infoCompat.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD); - } - - if (getLastVisiblePosition() < getCount() - 1) { - infoCompat.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD); - } - } - } - - @Override - @TargetApi(16) - public boolean performAccessibilityAction(int action, Bundle arguments) { - if (super.performAccessibilityAction(action, arguments)) { - return true; - } - - switch (action) { - case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD: - if (isEnabled() && getLastVisiblePosition() < getCount() - 1) { - // TODO: Use some form of smooth scroll instead - scrollListItemsBy(getAvailableSize()); - return true; - } - return false; - - case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD: - if (isEnabled() && mFirstPosition > 0) { - // TODO: Use some form of smooth scroll instead - scrollListItemsBy(-getAvailableSize()); - return true; - } - return false; - } - - return false; - } - - /** - * Return true if child is an ancestor of parent, (or equal to the parent). - */ - private boolean isViewAncestorOf(View child, View parent) { - if (child == parent) { - return true; - } - - final ViewParent theParent = child.getParent(); - - return (theParent instanceof ViewGroup) && - isViewAncestorOf((View) theParent, parent); - } - - private void forceValidFocusDirection(int direction) { - if (mIsVertical && direction != View.FOCUS_UP && direction != View.FOCUS_DOWN) { - throw new IllegalArgumentException("Focus direction must be one of" - + " {View.FOCUS_UP, View.FOCUS_DOWN} for vertical orientation"); - } else if (!mIsVertical && direction != View.FOCUS_LEFT && direction != View.FOCUS_RIGHT) { - throw new IllegalArgumentException("Focus direction must be one of" - + " {View.FOCUS_LEFT, View.FOCUS_RIGHT} for vertical orientation"); - } - } - - private void forceValidInnerFocusDirection(int direction) { - if (mIsVertical && direction != View.FOCUS_LEFT && direction != View.FOCUS_RIGHT) { - throw new IllegalArgumentException("Direction must be one of" - + " {View.FOCUS_LEFT, View.FOCUS_RIGHT} for vertical orientation"); - } else if (!mIsVertical && direction != View.FOCUS_UP && direction != View.FOCUS_DOWN) { - throw new IllegalArgumentException("direction must be one of" - + " {View.FOCUS_UP, View.FOCUS_DOWN} for horizontal orientation"); - } - } - - /** - * Scrolls up or down by the number of items currently present on screen. - * - * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN} or - * {@link View#FOCUS_LEFT} or {@link View#FOCUS_RIGHT} depending on the - * current view orientation. - * - * @return whether selection was moved - */ - boolean pageScroll(int direction) { - forceValidFocusDirection(direction); - - boolean forward = false; - int nextPage = -1; - - if (direction == View.FOCUS_UP || direction == View.FOCUS_LEFT) { - nextPage = Math.max(0, mSelectedPosition - getChildCount() - 1); - } else if (direction == View.FOCUS_DOWN || direction == View.FOCUS_RIGHT) { - nextPage = Math.min(mItemCount - 1, mSelectedPosition + getChildCount() - 1); - forward = true; - } - - if (nextPage < 0) { - return false; - } - - final int position = lookForSelectablePosition(nextPage, forward); - if (position >= 0) { - mLayoutMode = LAYOUT_SPECIFIC; - mSpecificStart = getStartEdge() + getFadingEdgeLength(); - - if (forward && position > mItemCount - getChildCount()) { - mLayoutMode = LAYOUT_FORCE_BOTTOM; - } - - if (!forward && position < getChildCount()) { - mLayoutMode = LAYOUT_FORCE_TOP; - } - - setSelectionInt(position); - invokeOnItemScrollListener(); - - if (!awakenScrollbarsInternal()) { - invalidate(); - } - - return true; - } - - return false; - } - - /** - * Go to the last or first item if possible (not worrying about panning across or navigating - * within the internal focus of the currently selected item.) - * - * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN} or - * {@link View#FOCUS_LEFT} or {@link View#FOCUS_RIGHT} depending on the - * current view orientation. - * - * @return whether selection was moved - */ - boolean fullScroll(int direction) { - forceValidFocusDirection(direction); - - boolean moved = false; - if (direction == View.FOCUS_UP || direction == View.FOCUS_LEFT) { - if (mSelectedPosition != 0) { - int position = lookForSelectablePosition(0, true); - if (position >= 0) { - mLayoutMode = LAYOUT_FORCE_TOP; - setSelectionInt(position); - invokeOnItemScrollListener(); - } - - moved = true; - } - } else if (direction == View.FOCUS_DOWN || direction == View.FOCUS_RIGHT) { - if (mSelectedPosition < mItemCount - 1) { - int position = lookForSelectablePosition(mItemCount - 1, true); - if (position >= 0) { - mLayoutMode = LAYOUT_FORCE_BOTTOM; - setSelectionInt(position); - invokeOnItemScrollListener(); - } - - moved = true; - } - } - - if (moved && !awakenScrollbarsInternal()) { - awakenScrollbarsInternal(); - invalidate(); - } - - return moved; - } - - /** - * To avoid horizontal/vertical focus searches changing the selected item, - * we manually focus search within the selected item (as applicable), and - * prevent focus from jumping to something within another item. - * - * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN} or - * {@link View#FOCUS_LEFT} or {@link View#FOCUS_RIGHT} depending on the - * current view orientation. - * - * @return Whether this consumes the key event. - */ - private boolean handleFocusWithinItem(int direction) { - forceValidInnerFocusDirection(direction); - - final int numChildren = getChildCount(); - - if (mItemsCanFocus && numChildren > 0 && mSelectedPosition != INVALID_POSITION) { - final View selectedView = getSelectedView(); - - if (selectedView != null && selectedView.hasFocus() && - selectedView instanceof ViewGroup) { - - final View currentFocus = selectedView.findFocus(); - final View nextFocus = FocusFinder.getInstance().findNextFocus( - (ViewGroup) selectedView, currentFocus, direction); - - if (nextFocus != null) { - // Do the math to get interesting rect in next focus' coordinates - currentFocus.getFocusedRect(mTempRect); - offsetDescendantRectToMyCoords(currentFocus, mTempRect); - offsetRectIntoDescendantCoords(nextFocus, mTempRect); - - if (nextFocus.requestFocus(direction, mTempRect)) { - return true; - } - } - - // We are blocking the key from being handled (by returning true) - // if the global result is going to be some other view within this - // list. This is to achieve the overall goal of having horizontal/vertical - // d-pad navigation remain in the current item depending on the current - // orientation in this view. - final View globalNextFocus = FocusFinder.getInstance().findNextFocus( - (ViewGroup) getRootView(), currentFocus, direction); - - if (globalNextFocus != null) { - return isViewAncestorOf(globalNextFocus, this); - } - } - } - - return false; - } - - /** - * Scrolls to the next or previous item if possible. - * - * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN} or - * {@link View#FOCUS_LEFT} or {@link View#FOCUS_RIGHT} depending on the - * current view orientation. - * - * @return whether selection was moved - */ - private boolean arrowScroll(int direction) { - forceValidFocusDirection(direction); - - try { - mInLayout = true; - - final boolean handled = arrowScrollImpl(direction); - if (handled) { - playSoundEffect(SoundEffectConstants.getContantForFocusDirection(direction)); - } - - return handled; - } finally { - mInLayout = false; - } - } - - /** - * When selection changes, it is possible that the previously selected or the - * next selected item will change its size. If so, we need to offset some folks, - * and re-layout the items as appropriate. - * - * @param selectedView The currently selected view (before changing selection). - * should be null if there was no previous selection. - * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN} or - * {@link View#FOCUS_LEFT} or {@link View#FOCUS_RIGHT} depending on the - * current view orientation. - * @param newSelectedPosition The position of the next selection. - * @param newFocusAssigned whether new focus was assigned. This matters because - * when something has focus, we don't want to show selection (ugh). - */ - private void handleNewSelectionChange(View selectedView, int direction, int newSelectedPosition, - boolean newFocusAssigned) { - forceValidFocusDirection(direction); - - if (newSelectedPosition == INVALID_POSITION) { - throw new IllegalArgumentException("newSelectedPosition needs to be valid"); - } - - // Whether or not we are moving down/right or up/left, we want to preserve the - // top/left of whatever view is at the start: - // - moving down/right: the view that had selection - // - moving up/left: the view that is getting selection - final int selectedIndex = mSelectedPosition - mFirstPosition; - final int nextSelectedIndex = newSelectedPosition - mFirstPosition; - int startViewIndex, endViewIndex; - boolean topSelected = false; - View startView; - View endView; - - if (direction == View.FOCUS_UP || direction == View.FOCUS_LEFT) { - startViewIndex = nextSelectedIndex; - endViewIndex = selectedIndex; - startView = getChildAt(startViewIndex); - endView = selectedView; - topSelected = true; - } else { - startViewIndex = selectedIndex; - endViewIndex = nextSelectedIndex; - startView = selectedView; - endView = getChildAt(endViewIndex); - } - - final int numChildren = getChildCount(); - - // start with top view: is it changing size? - if (startView != null) { - startView.setSelected(!newFocusAssigned && topSelected); - measureAndAdjustDown(startView, startViewIndex, numChildren); - } - - // is the bottom view changing size? - if (endView != null) { - endView.setSelected(!newFocusAssigned && !topSelected); - measureAndAdjustDown(endView, endViewIndex, numChildren); - } - } - - /** - * Re-measure a child, and if its height changes, lay it out preserving its - * top, and adjust the children below it appropriately. - * - * @param child The child - * @param childIndex The view group index of the child. - * @param numChildren The number of children in the view group. - */ - private void measureAndAdjustDown(View child, int childIndex, int numChildren) { - int oldSize = getChildSize(child); - measureChild(child); - - if (getChildMeasuredSize(child) == oldSize) { - return; - } - - // lay out the view, preserving its top - relayoutMeasuredChild(child); - - // adjust views below appropriately - final int sizeDelta = getChildMeasuredSize(child) - oldSize; - for (int i = childIndex + 1; i < numChildren; i++) { - getChildAt(i).offsetTopAndBottom(sizeDelta); - } - } - - /** - * Do an arrow scroll based on focus searching. If a new view is - * given focus, return the selection delta and amount to scroll via - * an {@link ArrowScrollFocusResult}, otherwise, return null. - * - * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN} or - * {@link View#FOCUS_LEFT} or {@link View#FOCUS_RIGHT} depending on the - * current view orientation. - * - * @return The result if focus has changed, or null. - */ - private ArrowScrollFocusResult arrowScrollFocused(final int direction) { - forceValidFocusDirection(direction); - - final View selectedView = getSelectedView(); - final View newFocus; - final int searchPoint; - - if (selectedView != null && selectedView.hasFocus()) { - View oldFocus = selectedView.findFocus(); - newFocus = FocusFinder.getInstance().findNextFocus(this, oldFocus, direction); - } else { - if (direction == View.FOCUS_DOWN || direction == View.FOCUS_RIGHT) { - boolean fadingEdgeShowing = (mFirstPosition > 0); - final int start = getStartEdge() + - (fadingEdgeShowing ? getArrowScrollPreviewLength() : 0); - - final int selectedStart; - if (selectedView != null) { - selectedStart = getChildStartEdge(selectedView); - } else { - selectedStart = start; - } - - searchPoint = Math.max(selectedStart, start); - } else { - final boolean fadingEdgeShowing = - (mFirstPosition + getChildCount() - 1) < mItemCount; - final int end = getEndEdge() - (fadingEdgeShowing ? getArrowScrollPreviewLength() : 0); - - final int selectedEnd; - if (selectedView != null) { - selectedEnd = getChildEndEdge(selectedView); - } else { - selectedEnd = end; - } - - searchPoint = Math.min(selectedEnd, end); - } - - final int x = (mIsVertical ? 0 : searchPoint); - final int y = (mIsVertical ? searchPoint : 0); - mTempRect.set(x, y, x, y); - - newFocus = FocusFinder.getInstance().findNextFocusFromRect(this, mTempRect, direction); - } - - if (newFocus != null) { - final int positionOfNewFocus = positionOfNewFocus(newFocus); - - // If the focus change is in a different new position, make sure - // we aren't jumping over another selectable position. - if (mSelectedPosition != INVALID_POSITION && positionOfNewFocus != mSelectedPosition) { - final int selectablePosition = lookForSelectablePositionOnScreen(direction); - - final boolean movingForward = - (direction == View.FOCUS_DOWN || direction == View.FOCUS_RIGHT); - final boolean movingBackward = - (direction == View.FOCUS_UP || direction == View.FOCUS_LEFT); - - if (selectablePosition != INVALID_POSITION && - ((movingForward && selectablePosition < positionOfNewFocus) || - (movingBackward && selectablePosition > positionOfNewFocus))) { - return null; - } - } - - int focusScroll = amountToScrollToNewFocus(direction, newFocus, positionOfNewFocus); - - final int maxScrollAmount = getMaxScrollAmount(); - if (focusScroll < maxScrollAmount) { - // Not moving too far, safe to give next view focus - newFocus.requestFocus(direction); - mArrowScrollFocusResult.populate(positionOfNewFocus, focusScroll); - return mArrowScrollFocusResult; - } else if (distanceToView(newFocus) < maxScrollAmount) { - // Case to consider: - // Too far to get entire next focusable on screen, but by going - // max scroll amount, we are getting it at least partially in view, - // so give it focus and scroll the max amount. - newFocus.requestFocus(direction); - mArrowScrollFocusResult.populate(positionOfNewFocus, maxScrollAmount); - return mArrowScrollFocusResult; - } - } - - return null; - } - - /** - * @return The maximum amount a list view will scroll in response to - * an arrow event. - */ - public int getMaxScrollAmount() { - return (int) (MAX_SCROLL_FACTOR * getSize()); - } - - /** - * @return The amount to preview next items when arrow scrolling. - */ - private int getArrowScrollPreviewLength() { - return mItemMargin + Math.max(MIN_SCROLL_PREVIEW_PIXELS, getFadingEdgeLength()); - } - - /** - * @param newFocus The view that would have focus. - * @return the position that contains newFocus - */ - private int positionOfNewFocus(View newFocus) { - final int numChildren = getChildCount(); - - for (int i = 0; i < numChildren; i++) { - final View child = getChildAt(i); - if (isViewAncestorOf(newFocus, child)) { - return mFirstPosition + i; - } - } - - throw new IllegalArgumentException("newFocus is not a child of any of the" - + " children of the list!"); - } - - /** - * Handle an arrow scroll going up or down. Take into account whether items are selectable, - * whether there are focusable items, etc. - * - * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN} or - * {@link View#FOCUS_LEFT} or {@link View#FOCUS_RIGHT} depending on the - * current view orientation. - * - * @return Whether any scrolling, selection or focus change occurred. - */ - private boolean arrowScrollImpl(int direction) { - forceValidFocusDirection(direction); - - if (getChildCount() <= 0) { - return false; - } - - View selectedView = getSelectedView(); - int selectedPos = mSelectedPosition; - - int nextSelectedPosition = lookForSelectablePositionOnScreen(direction); - int amountToScroll = amountToScroll(direction, nextSelectedPosition); - - // If we are moving focus, we may OVERRIDE the default behaviour - final ArrowScrollFocusResult focusResult = (mItemsCanFocus ? arrowScrollFocused(direction) : null); - if (focusResult != null) { - nextSelectedPosition = focusResult.getSelectedPosition(); - amountToScroll = focusResult.getAmountToScroll(); - } - - boolean needToRedraw = (focusResult != null); - if (nextSelectedPosition != INVALID_POSITION) { - handleNewSelectionChange(selectedView, direction, nextSelectedPosition, focusResult != null); - - setSelectedPositionInt(nextSelectedPosition); - setNextSelectedPositionInt(nextSelectedPosition); - - selectedView = getSelectedView(); - selectedPos = nextSelectedPosition; - - if (mItemsCanFocus && focusResult == null) { - // There was no new view found to take focus, make sure we - // don't leave focus with the old selection. - final View focused = getFocusedChild(); - if (focused != null) { - focused.clearFocus(); - } - } - - needToRedraw = true; - checkSelectionChanged(); - } - - if (amountToScroll > 0) { - scrollListItemsBy(direction == View.FOCUS_UP || direction == View.FOCUS_LEFT ? - amountToScroll : -amountToScroll); - needToRedraw = true; - } - - // If we didn't find a new focusable, make sure any existing focused - // item that was panned off screen gives up focus. - if (mItemsCanFocus && focusResult == null && - selectedView != null && selectedView.hasFocus()) { - final View focused = selectedView.findFocus(); - if (!isViewAncestorOf(focused, this) || distanceToView(focused) > 0) { - focused.clearFocus(); - } - } - - // If the current selection is panned off, we need to remove the selection - if (nextSelectedPosition == INVALID_POSITION && selectedView != null - && !isViewAncestorOf(selectedView, this)) { - selectedView = null; - hideSelector(); - - // But we don't want to set the ressurect position (that would make subsequent - // unhandled key events bring back the item we just scrolled off) - mResurrectToPosition = INVALID_POSITION; - } - - if (needToRedraw) { - if (selectedView != null) { - positionSelector(selectedPos, selectedView); - mSelectedStart = getChildStartEdge(selectedView); - } - - if (!awakenScrollbarsInternal()) { - invalidate(); - } - - invokeOnItemScrollListener(); - return true; - } - - return false; - } - - /** - * Determine how much we need to scroll in order to get the next selected view - * visible. The amount is capped at {@link #getMaxScrollAmount()}. - * - * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN} or - * {@link View#FOCUS_LEFT} or {@link View#FOCUS_RIGHT} depending on the - * current view orientation. - * @param nextSelectedPosition The position of the next selection, or - * {@link #INVALID_POSITION} if there is no next selectable position - * - * @return The amount to scroll. Note: this is always positive! Direction - * needs to be taken into account when actually scrolling. - */ - private int amountToScroll(int direction, int nextSelectedPosition) { - forceValidFocusDirection(direction); - - final int numChildren = getChildCount(); - - if (direction == View.FOCUS_DOWN || direction == View.FOCUS_RIGHT) { - final int end = getEndEdge(); - - int indexToMakeVisible = numChildren - 1; - if (nextSelectedPosition != INVALID_POSITION) { - indexToMakeVisible = nextSelectedPosition - mFirstPosition; - } - - final int positionToMakeVisible = mFirstPosition + indexToMakeVisible; - final View viewToMakeVisible = getChildAt(indexToMakeVisible); - - int goalEnd = end; - if (positionToMakeVisible < mItemCount - 1) { - goalEnd -= getArrowScrollPreviewLength(); - } - - final int viewToMakeVisibleStart = getChildStartEdge(viewToMakeVisible); - final int viewToMakeVisibleEnd = getChildEndEdge(viewToMakeVisible); - - if (viewToMakeVisibleEnd <= goalEnd) { - // Target item is fully visible - return 0; - } - - if (nextSelectedPosition != INVALID_POSITION && - (goalEnd - viewToMakeVisibleStart) >= getMaxScrollAmount()) { - // Item already has enough of it visible, changing selection is good enough - return 0; - } - - int amountToScroll = (viewToMakeVisibleEnd - goalEnd); - - if (mFirstPosition + numChildren == mItemCount) { - final int lastChildEnd = getChildEndEdge(getChildAt(numChildren - 1)); - - // Last is last in list -> Make sure we don't scroll past it - final int max = lastChildEnd - end; - amountToScroll = Math.min(amountToScroll, max); - } - - return Math.min(amountToScroll, getMaxScrollAmount()); - } else { - final int start = getStartEdge(); - - int indexToMakeVisible = 0; - if (nextSelectedPosition != INVALID_POSITION) { - indexToMakeVisible = nextSelectedPosition - mFirstPosition; - } - - final int positionToMakeVisible = mFirstPosition + indexToMakeVisible; - final View viewToMakeVisible = getChildAt(indexToMakeVisible); - - int goalStart = start; - if (positionToMakeVisible > 0) { - goalStart += getArrowScrollPreviewLength(); - } - - final int viewToMakeVisibleStart = getChildStartEdge(viewToMakeVisible); - final int viewToMakeVisibleEnd = getChildEndEdge(viewToMakeVisible); - - if (viewToMakeVisibleStart >= goalStart) { - // Item is fully visible - return 0; - } - - if (nextSelectedPosition != INVALID_POSITION && - (viewToMakeVisibleEnd - goalStart) >= getMaxScrollAmount()) { - // Item already has enough of it visible, changing selection is good enough - return 0; - } - - int amountToScroll = (goalStart - viewToMakeVisibleStart); - - if (mFirstPosition == 0) { - final int firstChildStart = getChildStartEdge(getChildAt(0)); - - // First is first in list -> make sure we don't scroll past it - final int max = start - firstChildStart; - amountToScroll = Math.min(amountToScroll, max); - } - - return Math.min(amountToScroll, getMaxScrollAmount()); - } - } - - /** - * Determine how much we need to scroll in order to get newFocus in view. - * - * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN} or - * {@link View#FOCUS_LEFT} or {@link View#FOCUS_RIGHT} depending on the - * current view orientation. - * @param newFocus The view that would take focus. - * @param positionOfNewFocus The position of the list item containing newFocus - * - * @return The amount to scroll. Note: this is always positive! Direction - * needs to be taken into account when actually scrolling. - */ - private int amountToScrollToNewFocus(int direction, View newFocus, int positionOfNewFocus) { - forceValidFocusDirection(direction); - - int amountToScroll = 0; - - newFocus.getDrawingRect(mTempRect); - offsetDescendantRectToMyCoords(newFocus, mTempRect); - - if (direction == View.FOCUS_UP || direction == View.FOCUS_LEFT) { - final int start = getStartEdge(); - final int newFocusStart = (mIsVertical ? mTempRect.top : mTempRect.left); - - if (newFocusStart < start) { - amountToScroll = start - newFocusStart; - if (positionOfNewFocus > 0) { - amountToScroll += getArrowScrollPreviewLength(); - } - } - } else { - final int end = getEndEdge(); - final int newFocusEnd = (mIsVertical ? mTempRect.bottom : mTempRect.right); - - if (newFocusEnd > end) { - amountToScroll = newFocusEnd - end; - if (positionOfNewFocus < mItemCount - 1) { - amountToScroll += getArrowScrollPreviewLength(); - } - } - } - - return amountToScroll; - } - - /** - * Determine the distance to the nearest edge of a view in a particular - * direction. - * - * @param descendant A descendant of this list. - * @return The distance, or 0 if the nearest edge is already on screen. - */ - private int distanceToView(View descendant) { - descendant.getDrawingRect(mTempRect); - offsetDescendantRectToMyCoords(descendant, mTempRect); - - final int start = getStartEdge(); - final int end = getEndEdge(); - - final int viewStart = (mIsVertical ? mTempRect.top : mTempRect.left); - final int viewEnd = (mIsVertical ? mTempRect.bottom : mTempRect.right); - - int distance = 0; - if (viewEnd < start) { - distance = start - viewEnd; - } else if (viewStart > end) { - distance = viewStart - end; - } - - return distance; - } - - private boolean handleKeyScroll(KeyEvent event, int count, int direction) { - boolean handled = false; - - if (KeyEventCompat.hasNoModifiers(event)) { - handled = resurrectSelectionIfNeeded(); - if (!handled) { - while (count-- > 0) { - if (arrowScroll(direction)) { - handled = true; - } else { - break; - } - } - } - } else if (KeyEventCompat.hasModifiers(event, KeyEvent.META_ALT_ON)) { - handled = resurrectSelectionIfNeeded() || fullScroll(direction); - } - - return handled; - } - - private boolean handleKeyEvent(int keyCode, int count, KeyEvent event) { - if (mAdapter == null || !mIsAttached) { - return false; - } - - if (mDataChanged) { - layoutChildren(); - } - - boolean handled = false; - final int action = event.getAction(); - - if (action != KeyEvent.ACTION_UP) { - switch (keyCode) { - case KeyEvent.KEYCODE_DPAD_UP: - if (mIsVertical) { - handled = handleKeyScroll(event, count, View.FOCUS_UP); - } else if (KeyEventCompat.hasNoModifiers(event)) { - handled = handleFocusWithinItem(View.FOCUS_UP); - } - break; - - case KeyEvent.KEYCODE_DPAD_DOWN: { - if (mIsVertical) { - handled = handleKeyScroll(event, count, View.FOCUS_DOWN); - } else if (KeyEventCompat.hasNoModifiers(event)) { - handled = handleFocusWithinItem(View.FOCUS_DOWN); - } - break; - } - - case KeyEvent.KEYCODE_DPAD_LEFT: - if (!mIsVertical) { - handled = handleKeyScroll(event, count, View.FOCUS_LEFT); - } else if (KeyEventCompat.hasNoModifiers(event)) { - handled = handleFocusWithinItem(View.FOCUS_LEFT); - } - break; - - case KeyEvent.KEYCODE_DPAD_RIGHT: - if (!mIsVertical) { - handled = handleKeyScroll(event, count, View.FOCUS_RIGHT); - } else if (KeyEventCompat.hasNoModifiers(event)) { - handled = handleFocusWithinItem(View.FOCUS_RIGHT); - } - break; - - case KeyEvent.KEYCODE_DPAD_CENTER: - case KeyEvent.KEYCODE_ENTER: - if (KeyEventCompat.hasNoModifiers(event)) { - handled = resurrectSelectionIfNeeded(); - if (!handled - && event.getRepeatCount() == 0 && getChildCount() > 0) { - keyPressed(); - handled = true; - } - } - break; - - case KeyEvent.KEYCODE_SPACE: - if (KeyEventCompat.hasNoModifiers(event)) { - handled = resurrectSelectionIfNeeded() || - pageScroll(mIsVertical ? View.FOCUS_DOWN : View.FOCUS_RIGHT); - } else if (KeyEventCompat.hasModifiers(event, KeyEvent.META_SHIFT_ON)) { - handled = resurrectSelectionIfNeeded() || - fullScroll(mIsVertical ? View.FOCUS_UP : View.FOCUS_LEFT); - } - - handled = true; - break; - - case KeyEvent.KEYCODE_PAGE_UP: - if (KeyEventCompat.hasNoModifiers(event)) { - handled = resurrectSelectionIfNeeded() || - pageScroll(mIsVertical ? View.FOCUS_UP : View.FOCUS_LEFT); - } else if (KeyEventCompat.hasModifiers(event, KeyEvent.META_ALT_ON)) { - handled = resurrectSelectionIfNeeded() || - fullScroll(mIsVertical ? View.FOCUS_UP : View.FOCUS_LEFT); - } - break; - - case KeyEvent.KEYCODE_PAGE_DOWN: - if (KeyEventCompat.hasNoModifiers(event)) { - handled = resurrectSelectionIfNeeded() || - pageScroll(mIsVertical ? View.FOCUS_DOWN : View.FOCUS_RIGHT); - } else if (KeyEventCompat.hasModifiers(event, KeyEvent.META_ALT_ON)) { - handled = resurrectSelectionIfNeeded() || - fullScroll(mIsVertical ? View.FOCUS_DOWN : View.FOCUS_RIGHT); - } - break; - - case KeyEvent.KEYCODE_MOVE_HOME: - if (KeyEventCompat.hasNoModifiers(event)) { - handled = resurrectSelectionIfNeeded() || - fullScroll(mIsVertical ? View.FOCUS_UP : View.FOCUS_LEFT); - } - break; - - case KeyEvent.KEYCODE_MOVE_END: - if (KeyEventCompat.hasNoModifiers(event)) { - handled = resurrectSelectionIfNeeded() || - fullScroll(mIsVertical ? View.FOCUS_DOWN : View.FOCUS_RIGHT); - } - break; - } - } - - if (handled) { - return true; - } - - switch (action) { - case KeyEvent.ACTION_DOWN: - return super.onKeyDown(keyCode, event); - - case KeyEvent.ACTION_UP: - if (!isEnabled()) { - return true; - } - - if (isClickable() && isPressed() && - mSelectedPosition >= 0 && mAdapter != null && - mSelectedPosition < mAdapter.getCount()) { - - final View child = getChildAt(mSelectedPosition - mFirstPosition); - if (child != null) { - performItemClick(child, mSelectedPosition, mSelectedRowId); - child.setPressed(false); - } - - setPressed(false); - return true; - } - - return false; - - case KeyEvent.ACTION_MULTIPLE: - return super.onKeyMultiple(keyCode, count, event); - - default: - return false; - } - } - - private void initOrResetVelocityTracker() { - if (mVelocityTracker == null) { - mVelocityTracker = VelocityTracker.obtain(); - } else { - mVelocityTracker.clear(); - } - } - - private void initVelocityTrackerIfNotExists() { - if (mVelocityTracker == null) { - mVelocityTracker = VelocityTracker.obtain(); - } - } - - private void recycleVelocityTracker() { - if (mVelocityTracker != null) { - mVelocityTracker.recycle(); - mVelocityTracker = null; - } - } - - /** - * Notify our scroll listener (if there is one) of a change in scroll state - */ - private void invokeOnItemScrollListener() { - if (mOnScrollListener != null) { - mOnScrollListener.onScroll(this, mFirstPosition, getChildCount(), mItemCount); - } - - // Dummy values, View's implementation does not use these. - onScrollChanged(0, 0, 0, 0); - } - - private void reportScrollStateChange(int newState) { - if (newState == mLastScrollState) { - return; - } - - if (mOnScrollListener != null) { - mLastScrollState = newState; - mOnScrollListener.onScrollStateChanged(this, newState); - } - } - - private boolean maybeStartScrolling(int delta) { - final boolean isOverScroll = (mOverScroll != 0); - if (Math.abs(delta) <= mTouchSlop && !isOverScroll) { - return false; - } - - if (isOverScroll) { - mTouchMode = TOUCH_MODE_OVERSCROLL; - } else { - mTouchMode = TOUCH_MODE_DRAGGING; - } - - // Time to start stealing events! Once we've stolen them, don't - // let anyone steal from us. - final ViewParent parent = getParent(); - if (parent != null) { - parent.requestDisallowInterceptTouchEvent(true); - } - - cancelCheckForLongPress(); - - setPressed(false); - View motionView = getChildAt(mMotionPosition - mFirstPosition); - if (motionView != null) { - motionView.setPressed(false); - } - - reportScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL); - - return true; - } - - private void maybeScroll(int delta) { - if (mTouchMode == TOUCH_MODE_DRAGGING) { - handleDragChange(delta); - } else if (mTouchMode == TOUCH_MODE_OVERSCROLL) { - handleOverScrollChange(delta); - } - } - - private void handleDragChange(int delta) { - // Time to start stealing events! Once we've stolen them, don't - // let anyone steal from us. - final ViewParent parent = getParent(); - if (parent != null) { - parent.requestDisallowInterceptTouchEvent(true); - } - - final int motionIndex; - if (mMotionPosition >= 0) { - motionIndex = mMotionPosition - mFirstPosition; - } else { - // If we don't have a motion position that we can reliably track, - // pick something in the middle to make a best guess at things below. - motionIndex = getChildCount() / 2; - } - - int motionViewPrevStart = 0; - View motionView = this.getChildAt(motionIndex); - if (motionView != null) { - motionViewPrevStart = getChildStartEdge(motionView); - } - - boolean atEdge = scrollListItemsBy(delta); - - motionView = this.getChildAt(motionIndex); - if (motionView != null) { - final int motionViewRealStart = getChildStartEdge(motionView); - - if (atEdge) { - final int overscroll = -delta - (motionViewRealStart - motionViewPrevStart); - updateOverScrollState(delta, overscroll); - } - } - } - - private void updateOverScrollState(int delta, int overscroll) { - overScrollByInternal((mIsVertical ? 0 : overscroll), - (mIsVertical ? overscroll : 0), - (mIsVertical ? 0 : mOverScroll), - (mIsVertical ? mOverScroll : 0), - 0, 0, - (mIsVertical ? 0 : mOverscrollDistance), - (mIsVertical ? mOverscrollDistance : 0), - true); - - if (Math.abs(mOverscrollDistance) == Math.abs(mOverScroll)) { - // Break fling velocity if we impacted an edge - if (mVelocityTracker != null) { - mVelocityTracker.clear(); - } - } - - final int overscrollMode = ViewCompat.getOverScrollMode(this); - if (overscrollMode == ViewCompat.OVER_SCROLL_ALWAYS || - (overscrollMode == ViewCompat.OVER_SCROLL_IF_CONTENT_SCROLLS && !contentFits())) { - mTouchMode = TOUCH_MODE_OVERSCROLL; - - float pull = (float) overscroll / getSize(); - if (delta > 0) { - mStartEdge.onPull(pull); - - if (!mEndEdge.isFinished()) { - mEndEdge.onRelease(); - } - } else if (delta < 0) { - mEndEdge.onPull(pull); - - if (!mStartEdge.isFinished()) { - mStartEdge.onRelease(); - } - } - - if (delta != 0) { - ViewCompat.postInvalidateOnAnimation(this); - } - } - } - - private void handleOverScrollChange(int delta) { - final int oldOverScroll = mOverScroll; - final int newOverScroll = oldOverScroll - delta; - - int overScrollDistance = -delta; - if ((newOverScroll < 0 && oldOverScroll >= 0) || - (newOverScroll > 0 && oldOverScroll <= 0)) { - overScrollDistance = -oldOverScroll; - delta += overScrollDistance; - } else { - delta = 0; - } - - if (overScrollDistance != 0) { - updateOverScrollState(delta, overScrollDistance); - } - - if (delta != 0) { - if (mOverScroll != 0) { - mOverScroll = 0; - ViewCompat.postInvalidateOnAnimation(this); - } - - scrollListItemsBy(delta); - mTouchMode = TOUCH_MODE_DRAGGING; - - // We did not scroll the full amount. Treat this essentially like the - // start of a new touch scroll - mMotionPosition = findClosestMotionRowOrColumn((int) mLastTouchPos); - mTouchRemainderPos = 0; - } - } - - /** - * What is the distance between the source and destination rectangles given the direction of - * focus navigation between them? The direction basically helps figure out more quickly what is - * self evident by the relationship between the rects... - * - * @param source the source rectangle - * @param dest the destination rectangle - * @param direction the direction - * @return the distance between the rectangles - */ - private static int getDistance(Rect source, Rect dest, int direction) { - int sX, sY; // source x, y - int dX, dY; // dest x, y - - switch (direction) { - case View.FOCUS_RIGHT: - sX = source.right; - sY = source.top + source.height() / 2; - dX = dest.left; - dY = dest.top + dest.height() / 2; - break; - - case View.FOCUS_DOWN: - sX = source.left + source.width() / 2; - sY = source.bottom; - dX = dest.left + dest.width() / 2; - dY = dest.top; - break; - - case View.FOCUS_LEFT: - sX = source.left; - sY = source.top + source.height() / 2; - dX = dest.right; - dY = dest.top + dest.height() / 2; - break; - - case View.FOCUS_UP: - sX = source.left + source.width() / 2; - sY = source.top; - dX = dest.left + dest.width() / 2; - dY = dest.bottom; - break; - - case View.FOCUS_FORWARD: - case View.FOCUS_BACKWARD: - sX = source.right + source.width() / 2; - sY = source.top + source.height() / 2; - dX = dest.left + dest.width() / 2; - dY = dest.top + dest.height() / 2; - break; - - default: - throw new IllegalArgumentException("direction must be one of " - + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT, " - + "FOCUS_FORWARD, FOCUS_BACKWARD}."); - } - - int deltaX = dX - sX; - int deltaY = dY - sY; - - return deltaY * deltaY + deltaX * deltaX; - } - - private int findMotionRowOrColumn(int motionPos) { - int childCount = getChildCount(); - if (childCount == 0) { - return INVALID_POSITION; - } - - for (int i = 0; i < childCount; i++) { - final View v = getChildAt(i); - if (motionPos <= getChildEndEdge(v)) { - return mFirstPosition + i; - } - } - - return INVALID_POSITION; - } - - private int findClosestMotionRowOrColumn(int motionPos) { - final int childCount = getChildCount(); - if (childCount == 0) { - return INVALID_POSITION; - } - - final int motionRow = findMotionRowOrColumn(motionPos); - if (motionRow != INVALID_POSITION) { - return motionRow; - } else { - return mFirstPosition + childCount - 1; - } - } - - @TargetApi(9) - private int getScaledOverscrollDistance(ViewConfiguration vc) { - if (Build.VERSION.SDK_INT < 9) { - return 0; - } - - return vc.getScaledOverscrollDistance(); - } - - private int getStartEdge() { - return (mIsVertical ? getPaddingTop() : getPaddingLeft()); - } - - private int getEndEdge() { - if (mIsVertical) { - return (getHeight() - getPaddingBottom()); - } else { - return (getWidth() - getPaddingRight()); - } - } - - private int getSize() { - return (mIsVertical ? getHeight() : getWidth()); - } - - private int getAvailableSize() { - if (mIsVertical) { - return getHeight() - getPaddingBottom() - getPaddingTop(); - } else { - return getWidth() - getPaddingRight() - getPaddingLeft(); - } - } - - private int getChildStartEdge(View child) { - return (mIsVertical ? child.getTop() : child.getLeft()); - } - - private int getChildEndEdge(View child) { - return (mIsVertical ? child.getBottom() : child.getRight()); - } - - private int getChildSize(View child) { - return (mIsVertical ? child.getHeight() : child.getWidth()); - } - - private int getChildMeasuredSize(View child) { - return (mIsVertical ? child.getMeasuredHeight() : child.getMeasuredWidth()); - } - - private int getFadingEdgeLength() { - return (mIsVertical ? getVerticalFadingEdgeLength() : getHorizontalFadingEdgeLength()); - } - - private int getMinSelectionPixel(int start, int fadingEdgeLength, int selectedPosition) { - // First pixel we can draw the selection into. - int selectionPixelStart = start; - if (selectedPosition > 0) { - selectionPixelStart += fadingEdgeLength; - } - - return selectionPixelStart; - } - - private int getMaxSelectionPixel(int end, int fadingEdgeLength, - int selectedPosition) { - int selectionPixelEnd = end; - if (selectedPosition != mItemCount - 1) { - selectionPixelEnd -= fadingEdgeLength; - } - - return selectionPixelEnd; - } - - private boolean contentFits() { - final int childCount = getChildCount(); - if (childCount == 0) { - return true; - } - - if (childCount != mItemCount) { - return false; - } - - View first = getChildAt(0); - View last = getChildAt(childCount - 1); - - return (getChildStartEdge(first) >= getStartEdge() && - getChildEndEdge(last) <= getEndEdge()); - } - - private void triggerCheckForTap() { - if (mPendingCheckForTap == null) { - mPendingCheckForTap = new CheckForTap(); - } - - postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout()); - } - - private void cancelCheckForTap() { - if (mPendingCheckForTap == null) { - return; - } - - removeCallbacks(mPendingCheckForTap); - } - - private void triggerCheckForLongPress() { - if (mPendingCheckForLongPress == null) { - mPendingCheckForLongPress = new CheckForLongPress(); - } - - mPendingCheckForLongPress.rememberWindowAttachCount(); - - postDelayed(mPendingCheckForLongPress, - ViewConfiguration.getLongPressTimeout()); - } - - private void cancelCheckForLongPress() { - if (mPendingCheckForLongPress == null) { - return; - } - - removeCallbacks(mPendingCheckForLongPress); - } - - private boolean scrollListItemsBy(int incrementalDelta) { - final int childCount = getChildCount(); - if (childCount == 0) { - return true; - } - - final int firstStart = getChildStartEdge(getChildAt(0)); - final int lastEnd = getChildEndEdge(getChildAt(childCount - 1)); - - final int paddingTop = getPaddingTop(); - final int paddingLeft = getPaddingLeft(); - - final int paddingStart = (mIsVertical ? paddingTop : paddingLeft); - - final int spaceBefore = paddingStart - firstStart; - final int end = getEndEdge(); - final int spaceAfter = lastEnd - end; - - final int size = getAvailableSize(); - - if (incrementalDelta < 0) { - incrementalDelta = Math.max(-(size - 1), incrementalDelta); - } else { - incrementalDelta = Math.min(size - 1, incrementalDelta); - } - - final int firstPosition = mFirstPosition; - - final boolean cannotScrollDown = (firstPosition == 0 && - firstStart >= paddingStart && incrementalDelta >= 0); - final boolean cannotScrollUp = (firstPosition + childCount == mItemCount && - lastEnd <= end && incrementalDelta <= 0); - - if (cannotScrollDown || cannotScrollUp) { - return incrementalDelta != 0; - } - - final boolean inTouchMode = isInTouchMode(); - if (inTouchMode) { - hideSelector(); - } - - int start = 0; - int count = 0; - - final boolean down = (incrementalDelta < 0); - if (down) { - int childrenStart = -incrementalDelta + paddingStart; - - for (int i = 0; i < childCount; i++) { - final View child = getChildAt(i); - final int childEnd = getChildEndEdge(child); - - if (childEnd >= childrenStart) { - break; - } - - count++; - mRecycler.addScrapView(child, firstPosition + i); - } - } else { - int childrenEnd = end - incrementalDelta; - - for (int i = childCount - 1; i >= 0; i--) { - final View child = getChildAt(i); - final int childStart = getChildStartEdge(child); - - if (childStart <= childrenEnd) { - break; - } - - start = i; - count++; - mRecycler.addScrapView(child, firstPosition + i); - } - } - - mBlockLayoutRequests = true; - - if (count > 0) { - detachViewsFromParent(start, count); - } - - // invalidate before moving the children to avoid unnecessary invalidate - // calls to bubble up from the children all the way to the top - if (!awakenScrollbarsInternal()) { - invalidate(); - } - - offsetChildren(incrementalDelta); - - if (down) { - mFirstPosition += count; - } - - final int absIncrementalDelta = Math.abs(incrementalDelta); - if (spaceBefore < absIncrementalDelta || spaceAfter < absIncrementalDelta) { - fillGap(down); - } - - if (!inTouchMode && mSelectedPosition != INVALID_POSITION) { - final int childIndex = mSelectedPosition - mFirstPosition; - if (childIndex >= 0 && childIndex < getChildCount()) { - positionSelector(mSelectedPosition, getChildAt(childIndex)); - } - } else if (mSelectorPosition != INVALID_POSITION) { - final int childIndex = mSelectorPosition - mFirstPosition; - if (childIndex >= 0 && childIndex < getChildCount()) { - positionSelector(INVALID_POSITION, getChildAt(childIndex)); - } - } else { - mSelectorRect.setEmpty(); - } - - mBlockLayoutRequests = false; - - invokeOnItemScrollListener(); - - return false; - } - - @TargetApi(14) - private final float getCurrVelocity() { - if (Build.VERSION.SDK_INT >= 14) { - return mScroller.getCurrVelocity(); - } - - return 0; - } - - @TargetApi(5) - private boolean awakenScrollbarsInternal() { - return (Build.VERSION.SDK_INT >= 5) && super.awakenScrollBars(); - } - - @Override - public void computeScroll() { - if (!mScroller.computeScrollOffset()) { - return; - } - - final int pos; - if (mIsVertical) { - pos = mScroller.getCurrY(); - } else { - pos = mScroller.getCurrX(); - } - - final int diff = (int) (pos - mLastTouchPos); - mLastTouchPos = pos; - - final boolean stopped = scrollListItemsBy(diff); - - if (!stopped && !mScroller.isFinished()) { - ViewCompat.postInvalidateOnAnimation(this); - } else { - if (stopped) { - final int overScrollMode = ViewCompat.getOverScrollMode(this); - if (overScrollMode != ViewCompat.OVER_SCROLL_NEVER) { - final EdgeEffectCompat edge = - (diff > 0 ? mStartEdge : mEndEdge); - - boolean needsInvalidate = - edge.onAbsorb(Math.abs((int) getCurrVelocity())); - - if (needsInvalidate) { - ViewCompat.postInvalidateOnAnimation(this); - } - } - - finishSmoothScrolling(); - } - - mTouchMode = TOUCH_MODE_REST; - reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE); - } - } - - private void finishEdgeGlows() { - if (mStartEdge != null) { - mStartEdge.finish(); - } - - if (mEndEdge != null) { - mEndEdge.finish(); - } - } - - private boolean drawStartEdge(Canvas canvas) { - if (mStartEdge.isFinished()) { - return false; - } - - if (mIsVertical) { - return mStartEdge.draw(canvas); - } - - final int restoreCount = canvas.save(); - final int height = getHeight(); - - canvas.translate(0, height); - canvas.rotate(270); - - final boolean needsInvalidate = mStartEdge.draw(canvas); - canvas.restoreToCount(restoreCount); - return needsInvalidate; - } - - private boolean drawEndEdge(Canvas canvas) { - if (mEndEdge.isFinished()) { - return false; - } - - final int restoreCount = canvas.save(); - final int width = getWidth(); - final int height = getHeight(); - - if (mIsVertical) { - canvas.translate(-width, height); - canvas.rotate(180, width, 0); - } else { - canvas.translate(width, 0); - canvas.rotate(90); - } - - final boolean needsInvalidate = mEndEdge.draw(canvas); - canvas.restoreToCount(restoreCount); - return needsInvalidate; - } - - private void finishSmoothScrolling() { - mTouchMode = TOUCH_MODE_REST; - reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE); - - mScroller.abortAnimation(); - if (mPositionScroller != null) { - mPositionScroller.stop(); - } - } - - private void drawSelector(Canvas canvas) { - if (!mSelectorRect.isEmpty()) { - final Drawable selector = mSelector; - selector.setBounds(mSelectorRect); - selector.draw(canvas); - } - } - - private void useDefaultSelector() { - setSelector(getResources().getDrawable( - android.R.drawable.list_selector_background)); - } - - private boolean shouldShowSelector() { - return (hasFocus() && !isInTouchMode()) || touchModeDrawsInPressedState(); - } - - private void positionSelector(int position, View selected) { - if (position != INVALID_POSITION) { - mSelectorPosition = position; - } - - mSelectorRect.set(selected.getLeft(), selected.getTop(), selected.getRight(), - selected.getBottom()); - - final boolean isChildViewEnabled = mIsChildViewEnabled; - if (selected.isEnabled() != isChildViewEnabled) { - mIsChildViewEnabled = !isChildViewEnabled; - - if (getSelectedItemPosition() != INVALID_POSITION) { - refreshDrawableState(); - } - } - } - - private void hideSelector() { - if (mSelectedPosition != INVALID_POSITION) { - if (mLayoutMode != LAYOUT_SPECIFIC) { - mResurrectToPosition = mSelectedPosition; - } - - if (mNextSelectedPosition >= 0 && mNextSelectedPosition != mSelectedPosition) { - mResurrectToPosition = mNextSelectedPosition; - } - - setSelectedPositionInt(INVALID_POSITION); - setNextSelectedPositionInt(INVALID_POSITION); - - mSelectedStart = 0; - } - } - - private void setSelectedPositionInt(int position) { - mSelectedPosition = position; - mSelectedRowId = getItemIdAtPosition(position); - } - - private void setSelectionInt(int position) { - setNextSelectedPositionInt(position); - boolean awakeScrollbars = false; - - final int selectedPosition = mSelectedPosition; - if (selectedPosition >= 0) { - if (position == selectedPosition - 1) { - awakeScrollbars = true; - } else if (position == selectedPosition + 1) { - awakeScrollbars = true; - } - } - - layoutChildren(); - - if (awakeScrollbars) { - awakenScrollbarsInternal(); - } - } - - private void setNextSelectedPositionInt(int position) { - mNextSelectedPosition = position; - mNextSelectedRowId = getItemIdAtPosition(position); - - // If we are trying to sync to the selection, update that too - if (mNeedSync && mSyncMode == SYNC_SELECTED_POSITION && position >= 0) { - mSyncPosition = position; - mSyncRowId = mNextSelectedRowId; - } - } - - private boolean touchModeDrawsInPressedState() { - switch (mTouchMode) { - case TOUCH_MODE_TAP: - case TOUCH_MODE_DONE_WAITING: - return true; - default: - return false; - } - } - - /** - * Sets the selector state to "pressed" and posts a CheckForKeyLongPress to see if - * this is a long press. - */ - private void keyPressed() { - if (!isEnabled() || !isClickable()) { - return; - } - - final Drawable selector = mSelector; - final Rect selectorRect = mSelectorRect; - - if (selector != null && (isFocused() || touchModeDrawsInPressedState()) - && !selectorRect.isEmpty()) { - - final View child = getChildAt(mSelectedPosition - mFirstPosition); - - if (child != null) { - if (child.hasFocusable()) { - return; - } - - child.setPressed(true); - } - - setPressed(true); - - final boolean longClickable = isLongClickable(); - final Drawable d = selector.getCurrent(); - if (d != null && d instanceof TransitionDrawable) { - if (longClickable) { - ((TransitionDrawable) d).startTransition( - ViewConfiguration.getLongPressTimeout()); - } else { - ((TransitionDrawable) d).resetTransition(); - } - } - - if (longClickable && !mDataChanged) { - if (mPendingCheckForKeyLongPress == null) { - mPendingCheckForKeyLongPress = new CheckForKeyLongPress(); - } - - mPendingCheckForKeyLongPress.rememberWindowAttachCount(); - postDelayed(mPendingCheckForKeyLongPress, ViewConfiguration.getLongPressTimeout()); - } - } - } - - private void updateSelectorState() { - if (mSelector != null) { - if (shouldShowSelector()) { - mSelector.setState(getDrawableState()); - } else { - mSelector.setState(STATE_NOTHING); - } - } - } - - private void checkSelectionChanged() { - if ((mSelectedPosition != mOldSelectedPosition) || (mSelectedRowId != mOldSelectedRowId)) { - selectionChanged(); - mOldSelectedPosition = mSelectedPosition; - mOldSelectedRowId = mSelectedRowId; - } - } - - private void selectionChanged() { - OnItemSelectedListener listener = getOnItemSelectedListener(); - if (listener == null) { - return; - } - - if (mInLayout || mBlockLayoutRequests) { - // If we are in a layout traversal, defer notification - // by posting. This ensures that the view tree is - // in a consistent state and is able to accommodate - // new layout or invalidate requests. - if (mSelectionNotifier == null) { - mSelectionNotifier = new SelectionNotifier(); - } - - post(mSelectionNotifier); - } else { - fireOnSelected(); - performAccessibilityActionsOnSelected(); - } - } - - private void fireOnSelected() { - OnItemSelectedListener listener = getOnItemSelectedListener(); - if (listener == null) { - return; - } - - final int selection = getSelectedItemPosition(); - if (selection >= 0) { - View v = getSelectedView(); - listener.onItemSelected(this, v, selection, - mAdapter.getItemId(selection)); - } else { - listener.onNothingSelected(this); - } - } - - private void performAccessibilityActionsOnSelected() { - final int position = getSelectedItemPosition(); - if (position >= 0) { - // We fire selection events here not in View - sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED); - } - } - - private int lookForSelectablePosition(int position) { - return lookForSelectablePosition(position, true); - } - - private int lookForSelectablePosition(int position, boolean lookDown) { - final ListAdapter adapter = mAdapter; - if (adapter == null || isInTouchMode()) { - return INVALID_POSITION; - } - - final int itemCount = mItemCount; - if (!mAreAllItemsSelectable) { - if (lookDown) { - position = Math.max(0, position); - while (position < itemCount && !adapter.isEnabled(position)) { - position++; - } - } else { - position = Math.min(position, itemCount - 1); - while (position >= 0 && !adapter.isEnabled(position)) { - position--; - } - } - - if (position < 0 || position >= itemCount) { - return INVALID_POSITION; - } - - return position; - } else { - if (position < 0 || position >= itemCount) { - return INVALID_POSITION; - } - - return position; - } - } - - /** - * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN} or - * {@link View#FOCUS_LEFT} or {@link View#FOCUS_RIGHT} depending on the - * current view orientation. - * - * @return The position of the next selectable position of the views that - * are currently visible, taking into account the fact that there might - * be no selection. Returns {@link #INVALID_POSITION} if there is no - * selectable view on screen in the given direction. - */ - private int lookForSelectablePositionOnScreen(int direction) { - forceValidFocusDirection(direction); - - final int firstPosition = mFirstPosition; - final ListAdapter adapter = getAdapter(); - - if (direction == View.FOCUS_DOWN || direction == View.FOCUS_RIGHT) { - int startPos = (mSelectedPosition != INVALID_POSITION ? - mSelectedPosition + 1 : firstPosition); - - if (startPos >= adapter.getCount()) { - return INVALID_POSITION; - } - - if (startPos < firstPosition) { - startPos = firstPosition; - } - - final int lastVisiblePos = getLastVisiblePosition(); - - for (int pos = startPos; pos <= lastVisiblePos; pos++) { - if (adapter.isEnabled(pos) - && getChildAt(pos - firstPosition).getVisibility() == View.VISIBLE) { - return pos; - } - } - } else { - final int last = firstPosition + getChildCount() - 1; - - int startPos = (mSelectedPosition != INVALID_POSITION) ? - mSelectedPosition - 1 : firstPosition + getChildCount() - 1; - - if (startPos < 0 || startPos >= adapter.getCount()) { - return INVALID_POSITION; - } - - if (startPos > last) { - startPos = last; - } - - for (int pos = startPos; pos >= firstPosition; pos--) { - if (adapter.isEnabled(pos) - && getChildAt(pos - firstPosition).getVisibility() == View.VISIBLE) { - return pos; - } - } - } - - return INVALID_POSITION; - } - - @Override - protected void drawableStateChanged() { - super.drawableStateChanged(); - updateSelectorState(); - } - - @Override - protected int[] onCreateDrawableState(int extraSpace) { - // If the child view is enabled then do the default behavior. - if (mIsChildViewEnabled) { - // Common case - return super.onCreateDrawableState(extraSpace); - } - - // The selector uses this View's drawable state. The selected child view - // is disabled, so we need to remove the enabled state from the drawable - // states. - final int enabledState = ENABLED_STATE_SET[0]; - - // If we don't have any extra space, it will return one of the static state arrays, - // and clearing the enabled state on those arrays is a bad thing! If we specify - // we need extra space, it will create+copy into a new array that safely mutable. - int[] state = super.onCreateDrawableState(extraSpace + 1); - int enabledPos = -1; - for (int i = state.length - 1; i >= 0; i--) { - if (state[i] == enabledState) { - enabledPos = i; - break; - } - } - - // Remove the enabled state - if (enabledPos >= 0) { - System.arraycopy(state, enabledPos + 1, state, enabledPos, - state.length - enabledPos - 1); - } - - return state; - } - - @Override - protected boolean canAnimate() { - return (super.canAnimate() && mItemCount > 0); - } - - @Override - protected void dispatchDraw(Canvas canvas) { - final boolean drawSelectorOnTop = mDrawSelectorOnTop; - if (!drawSelectorOnTop) { - drawSelector(canvas); - } - - super.dispatchDraw(canvas); - - if (drawSelectorOnTop) { - drawSelector(canvas); - } - } - - @Override - public void draw(Canvas canvas) { - super.draw(canvas); - - boolean needsInvalidate = false; - - if (mStartEdge != null) { - needsInvalidate |= drawStartEdge(canvas); - } - - if (mEndEdge != null) { - needsInvalidate |= drawEndEdge(canvas); - } - - if (needsInvalidate) { - ViewCompat.postInvalidateOnAnimation(this); - } - } - - @Override - public void requestLayout() { - if (!mInLayout && !mBlockLayoutRequests) { - super.requestLayout(); - } - } - - @Override - public View getSelectedView() { - if (mItemCount > 0 && mSelectedPosition >= 0) { - return getChildAt(mSelectedPosition - mFirstPosition); - } else { - return null; - } - } - - @Override - public void setSelection(int position) { - setSelectionFromOffset(position, 0); - } - - public void setSelectionFromOffset(int position, int offset) { - if (mAdapter == null) { - return; - } - - if (!isInTouchMode()) { - position = lookForSelectablePosition(position); - if (position >= 0) { - setNextSelectedPositionInt(position); - } - } else { - mResurrectToPosition = position; - } - - if (position >= 0) { - mLayoutMode = LAYOUT_SPECIFIC; - - if (mIsVertical) { - mSpecificStart = getPaddingTop() + offset; - } else { - mSpecificStart = getPaddingLeft() + offset; - } - - if (mNeedSync) { - mSyncPosition = position; - mSyncRowId = mAdapter.getItemId(position); - } - - requestLayout(); - } - } - - public void scrollBy(int offset) { - scrollListItemsBy(-offset); - } - - /** - * Smoothly scroll to the specified adapter position. The view will - * scroll such that the indicated position is displayed. - * @param position Scroll to this adapter position. - */ - public void smoothScrollToPosition(int position) { - if (mPositionScroller == null) { - mPositionScroller = new PositionScroller(); - } - mPositionScroller.start(position); - } - - /** - * Smoothly scroll to the specified adapter position. The view will scroll - * such that the indicated position is displayed offset pixels from - * the top/left edge of the view, according to the orientation. If this is - * impossible, (e.g. the offset would scroll the first or last item beyond the boundaries - * of the list) it will get as close as possible. The scroll will take - * duration milliseconds to complete. - * - * @param position Position to scroll to - * @param offset Desired distance in pixels of position from the top/left - * of the view when scrolling is finished - * @param duration Number of milliseconds to use for the scroll - */ - public void smoothScrollToPositionFromOffset(int position, int offset, int duration) { - if (mPositionScroller == null) { - mPositionScroller = new PositionScroller(); - } - mPositionScroller.startWithOffset(position, offset, duration); - } - - /** - * Smoothly scroll to the specified adapter position. The view will scroll - * such that the indicated position is displayed offset pixels from - * the top edge of the view. If this is impossible, (e.g. the offset would scroll - * the first or last item beyond the boundaries of the list) it will get as close - * as possible. - * - * @param position Position to scroll to - * @param offset Desired distance in pixels of position from the top - * of the view when scrolling is finished - */ - public void smoothScrollToPositionFromOffset(int position, int offset) { - if (mPositionScroller == null) { - mPositionScroller = new PositionScroller(); - } - mPositionScroller.startWithOffset(position, offset); - } - - /** - * Smoothly scroll to the specified adapter position. The view will - * scroll such that the indicated position is displayed, but it will - * stop early if scrolling further would scroll boundPosition out of - * view. - * - * @param position Scroll to this adapter position. - * @param boundPosition Do not scroll if it would move this adapter - * position out of view. - */ - public void smoothScrollToPosition(int position, int boundPosition) { - if (mPositionScroller == null) { - mPositionScroller = new PositionScroller(); - } - mPositionScroller.start(position, boundPosition); - } - - /** - * Smoothly scroll by distance pixels over duration milliseconds. - * @param distance Distance to scroll in pixels. - * @param duration Duration of the scroll animation in milliseconds. - */ - public void smoothScrollBy(int distance, int duration) { - // No sense starting to scroll if we're not going anywhere - final int firstPosition = mFirstPosition; - final int childCount = getChildCount(); - final int lastPosition = firstPosition + childCount; - final int start = getStartEdge(); - final int end = getEndEdge(); - - if (distance == 0 || mItemCount == 0 || childCount == 0 || - (firstPosition == 0 && getChildStartEdge(getChildAt(0)) == start && distance < 0) || - (lastPosition == mItemCount && - getChildEndEdge(getChildAt(childCount - 1)) == end && distance > 0)) { - finishSmoothScrolling(); - } else { - mScroller.startScroll(0, 0, - mIsVertical ? 0 : -distance, - mIsVertical ? -distance : 0, - duration); - - mLastTouchPos = 0; - - mTouchMode = TOUCH_MODE_FLINGING; - reportScrollStateChange(OnScrollListener.SCROLL_STATE_FLING); - - ViewCompat.postInvalidateOnAnimation(this); - } - } - - @Override - public boolean dispatchKeyEvent(KeyEvent event) { - // Dispatch in the normal way - boolean handled = super.dispatchKeyEvent(event); - if (!handled) { - // If we didn't handle it... - final View focused = getFocusedChild(); - if (focused != null && event.getAction() == KeyEvent.ACTION_DOWN) { - // ... and our focused child didn't handle it - // ... give it to ourselves so we can scroll if necessary - handled = onKeyDown(event.getKeyCode(), event); - } - } - - return handled; - } - - @Override - protected void dispatchSetPressed(boolean pressed) { - // Don't dispatch setPressed to our children. We call setPressed on ourselves to - // get the selector in the right state, but we don't want to press each child. - } - - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - if (mSelector == null) { - useDefaultSelector(); - } - - int widthMode = MeasureSpec.getMode(widthMeasureSpec); - int heightMode = MeasureSpec.getMode(heightMeasureSpec); - int widthSize = MeasureSpec.getSize(widthMeasureSpec); - int heightSize = MeasureSpec.getSize(heightMeasureSpec); - - int childWidth = 0; - int childHeight = 0; - - mItemCount = (mAdapter == null ? 0 : mAdapter.getCount()); - if (mItemCount > 0 && (widthMode == MeasureSpec.UNSPECIFIED || - heightMode == MeasureSpec.UNSPECIFIED)) { - final View child = obtainView(0, mIsScrap); - - final int secondaryMeasureSpec = - (mIsVertical ? widthMeasureSpec : heightMeasureSpec); - - measureScrapChild(child, 0, secondaryMeasureSpec); - - childWidth = child.getMeasuredWidth(); - childHeight = child.getMeasuredHeight(); - - if (recycleOnMeasure()) { - mRecycler.addScrapView(child, -1); - } - } - - if (widthMode == MeasureSpec.UNSPECIFIED) { - widthSize = getPaddingLeft() + getPaddingRight() + childWidth; - if (mIsVertical) { - widthSize += getVerticalScrollbarWidth(); - } - } - - if (heightMode == MeasureSpec.UNSPECIFIED) { - heightSize = getPaddingTop() + getPaddingBottom() + childHeight; - if (!mIsVertical) { - heightSize += getHorizontalScrollbarHeight(); - } - } - - if (mIsVertical && heightMode == MeasureSpec.AT_MOST) { - heightSize = measureHeightOfChildren(widthMeasureSpec, 0, NO_POSITION, heightSize, -1); - } - - if (!mIsVertical && widthMode == MeasureSpec.AT_MOST) { - widthSize = measureWidthOfChildren(heightMeasureSpec, 0, NO_POSITION, widthSize, -1); - } - - setMeasuredDimension(widthSize, heightSize); - } - - @Override - protected void onLayout(boolean changed, int l, int t, int r, int b) { - mInLayout = true; - - if (changed) { - final int childCount = getChildCount(); - for (int i = 0; i < childCount; i++) { - getChildAt(i).forceLayout(); - } - - mRecycler.markChildrenDirty(); - } - - layoutChildren(); - - mInLayout = false; - - final int width = r - l - getPaddingLeft() - getPaddingRight(); - final int height = b - t - getPaddingTop() - getPaddingBottom(); - - if (mStartEdge != null && mEndEdge != null) { - if (mIsVertical) { - mStartEdge.setSize(width, height); - mEndEdge.setSize(width, height); - } else { - mStartEdge.setSize(height, width); - mEndEdge.setSize(height, width); - } - } - } - - private void layoutChildren() { - if (getWidth() == 0 || getHeight() == 0) { - return; - } - - final boolean blockLayoutRequests = mBlockLayoutRequests; - if (!blockLayoutRequests) { - mBlockLayoutRequests = true; - } else { - return; - } - - try { - invalidate(); - - if (mAdapter == null) { - resetState(); - return; - } - - final int start = getStartEdge(); - final int end = getEndEdge(); - - int childCount = getChildCount(); - int index = 0; - int delta = 0; - - View focusLayoutRestoreView = null; - - View selected = null; - View oldSelected = null; - View newSelected = null; - View oldFirstChild = null; - - switch (mLayoutMode) { - case LAYOUT_SET_SELECTION: - index = mNextSelectedPosition - mFirstPosition; - if (index >= 0 && index < childCount) { - newSelected = getChildAt(index); - } - - break; - - case LAYOUT_FORCE_TOP: - case LAYOUT_FORCE_BOTTOM: - case LAYOUT_SPECIFIC: - case LAYOUT_SYNC: - break; - - case LAYOUT_MOVE_SELECTION: - default: - // Remember the previously selected view - index = mSelectedPosition - mFirstPosition; - if (index >= 0 && index < childCount) { - oldSelected = getChildAt(index); - } - - // Remember the previous first child - oldFirstChild = getChildAt(0); - - if (mNextSelectedPosition >= 0) { - delta = mNextSelectedPosition - mSelectedPosition; - } - - // Caution: newSelected might be null - newSelected = getChildAt(index + delta); - } - - final boolean dataChanged = mDataChanged; - if (dataChanged) { - handleDataChanged(); - } - - // Handle the empty set by removing all views that are visible - // and calling it a day - if (mItemCount == 0) { - resetState(); - return; - } else if (mItemCount != mAdapter.getCount()) { - throw new IllegalStateException("The content of the adapter has changed but " - + "TwoWayView did not receive a notification. Make sure the content of " - + "your adapter is not modified from a background thread, but only " - + "from the UI thread. [in TwoWayView(" + getId() + ", " + getClass() - + ") with Adapter(" + mAdapter.getClass() + ")]"); - } - - setSelectedPositionInt(mNextSelectedPosition); - - // Reset the focus restoration - View focusLayoutRestoreDirectChild = null; - - // Pull all children into the RecycleBin. - // These views will be reused if possible - final int firstPosition = mFirstPosition; - final RecycleBin recycleBin = mRecycler; - - if (dataChanged) { - for (int i = 0; i < childCount; i++) { - recycleBin.addScrapView(getChildAt(i), firstPosition + i); - } - } else { - recycleBin.fillActiveViews(childCount, firstPosition); - } - - // Take focus back to us temporarily to avoid the eventual - // call to clear focus when removing the focused child below - // from messing things up when ViewAncestor assigns focus back - // to someone else. - final View focusedChild = getFocusedChild(); - if (focusedChild != null) { - // We can remember the focused view to restore after relayout if the - // data hasn't changed, or if the focused position is a header or footer. - if (!dataChanged) { - focusLayoutRestoreDirectChild = focusedChild; - - // Remember the specific view that had focus - focusLayoutRestoreView = findFocus(); - if (focusLayoutRestoreView != null) { - // Tell it we are going to mess with it - focusLayoutRestoreView.onStartTemporaryDetach(); - } - } - - requestFocus(); - } - - // FIXME: We need a way to save current accessibility focus here - // so that it can be restored after we re-attach the children on each - // layout round. - - detachAllViewsFromParent(); - - switch (mLayoutMode) { - case LAYOUT_SET_SELECTION: - if (newSelected != null) { - final int newSelectedStart = getChildStartEdge(newSelected); - selected = fillFromSelection(newSelectedStart, start, end); - } else { - selected = fillFromMiddle(start, end); - } - - break; - - case LAYOUT_SYNC: - selected = fillSpecific(mSyncPosition, mSpecificStart); - break; - - case LAYOUT_FORCE_BOTTOM: - selected = fillBefore(mItemCount - 1, end); - adjustViewsStartOrEnd(); - break; - - case LAYOUT_FORCE_TOP: - mFirstPosition = 0; - selected = fillFromOffset(start); - adjustViewsStartOrEnd(); - break; - - case LAYOUT_SPECIFIC: - selected = fillSpecific(reconcileSelectedPosition(), mSpecificStart); - break; - - case LAYOUT_MOVE_SELECTION: - selected = moveSelection(oldSelected, newSelected, delta, start, end); - break; - - default: - if (childCount == 0) { - final int position = lookForSelectablePosition(0); - setSelectedPositionInt(position); - selected = fillFromOffset(start); - } else { - if (mSelectedPosition >= 0 && mSelectedPosition < mItemCount) { - int offset = start; - if (oldSelected != null) { - offset = getChildStartEdge(oldSelected); - } - selected = fillSpecific(mSelectedPosition, offset); - } else if (mFirstPosition < mItemCount) { - int offset = start; - if (oldFirstChild != null) { - offset = getChildStartEdge(oldFirstChild); - } - - selected = fillSpecific(mFirstPosition, offset); - } else { - selected = fillSpecific(0, start); - } - } - - break; - - } - - recycleBin.scrapActiveViews(); - - if (selected != null) { - if (mItemsCanFocus && hasFocus() && !selected.hasFocus()) { - final boolean focusWasTaken = (selected == focusLayoutRestoreDirectChild && - focusLayoutRestoreView != null && - focusLayoutRestoreView.requestFocus()) || selected.requestFocus(); - - if (!focusWasTaken) { - // Selected item didn't take focus, fine, but still want - // to make sure something else outside of the selected view - // has focus - final View focused = getFocusedChild(); - if (focused != null) { - focused.clearFocus(); - } - - positionSelector(INVALID_POSITION, selected); - } else { - selected.setSelected(false); - mSelectorRect.setEmpty(); - } - } else { - positionSelector(INVALID_POSITION, selected); - } - - mSelectedStart = getChildStartEdge(selected); - } else { - if (mTouchMode > TOUCH_MODE_DOWN && mTouchMode < TOUCH_MODE_DRAGGING) { - View child = getChildAt(mMotionPosition - mFirstPosition); - - if (child != null) { - positionSelector(mMotionPosition, child); - } - } else { - mSelectedStart = 0; - mSelectorRect.setEmpty(); - } - - // Even if there is not selected position, we may need to restore - // focus (i.e. something focusable in touch mode) - if (hasFocus() && focusLayoutRestoreView != null) { - focusLayoutRestoreView.requestFocus(); - } - } - - // Tell focus view we are done mucking with it, if it is still in - // our view hierarchy. - if (focusLayoutRestoreView != null - && focusLayoutRestoreView.getWindowToken() != null) { - focusLayoutRestoreView.onFinishTemporaryDetach(); - } - - mLayoutMode = LAYOUT_NORMAL; - mDataChanged = false; - mNeedSync = false; - - setNextSelectedPositionInt(mSelectedPosition); - if (mItemCount > 0) { - checkSelectionChanged(); - } - - invokeOnItemScrollListener(); - } finally { - if (!blockLayoutRequests) { - mBlockLayoutRequests = false; - mDataChanged = false; - } - } - } - - protected boolean recycleOnMeasure() { - return true; - } - - private void offsetChildren(int offset) { - final int childCount = getChildCount(); - - for (int i = 0; i < childCount; i++) { - final View child = getChildAt(i); - - if (mIsVertical) { - child.offsetTopAndBottom(offset); - } else { - child.offsetLeftAndRight(offset); - } - } - } - - private View moveSelection(View oldSelected, View newSelected, int delta, int start, - int end) { - final int fadingEdgeLength = getFadingEdgeLength(); - final int selectedPosition = mSelectedPosition; - - final int oldSelectedStart = getChildStartEdge(oldSelected); - final int oldSelectedEnd = getChildEndEdge(oldSelected); - - final int minStart = getMinSelectionPixel(start, fadingEdgeLength, selectedPosition); - final int maxEnd = getMaxSelectionPixel(end, fadingEdgeLength, selectedPosition); - - View selected = null; - - if (delta > 0) { - /* - * Case 1: Scrolling down. - */ - - /* - * Before After - * | | | | - * +-------+ +-------+ - * | A | | A | - * | 1 | => +-------+ - * +-------+ | B | - * | B | | 2 | - * +-------+ +-------+ - * | | | | - * - * Try to keep the top of the previously selected item where it was. - * oldSelected = A - * selected = B - */ - - // Put oldSelected (A) where it belongs - oldSelected = makeAndAddView(selectedPosition - 1, oldSelectedStart, true, false); - - final int itemMargin = mItemMargin; - - // Now put the new selection (B) below that - selected = makeAndAddView(selectedPosition, oldSelectedEnd + itemMargin, true, true); - - final int selectedStart = getChildStartEdge(selected); - final int selectedEnd = getChildEndEdge(selected); - - // Some of the newly selected item extends below the bottom of the list - if (selectedEnd > end) { - // Find space available above the selection into which we can scroll upwards - final int spaceBefore = selectedStart - minStart; - - // Find space required to bring the bottom of the selected item fully into view - final int spaceAfter = selectedEnd - maxEnd; - - // Don't scroll more than half the size of the list - final int halfSpace = (end - start) / 2; - int offset = Math.min(spaceBefore, spaceAfter); - offset = Math.min(offset, halfSpace); - - if (mIsVertical) { - oldSelected.offsetTopAndBottom(-offset); - selected.offsetTopAndBottom(-offset); - } else { - oldSelected.offsetLeftAndRight(-offset); - selected.offsetLeftAndRight(-offset); - } - } - - // Fill in views before and after - fillBefore(mSelectedPosition - 2, selectedStart - itemMargin); - adjustViewsStartOrEnd(); - fillAfter(mSelectedPosition + 1, selectedEnd + itemMargin); - } else if (delta < 0) { - /* - * Case 2: Scrolling up. - */ - - /* - * Before After - * | | | | - * +-------+ +-------+ - * | A | | A | - * +-------+ => | 1 | - * | B | +-------+ - * | 2 | | B | - * +-------+ +-------+ - * | | | | - * - * Try to keep the top of the item about to become selected where it was. - * newSelected = A - * olSelected = B - */ - - if (newSelected != null) { - // Try to position the top of newSel (A) where it was before it was selected - final int newSelectedStart = getChildStartEdge(newSelected); - selected = makeAndAddView(selectedPosition, newSelectedStart, true, true); - } else { - // If (A) was not on screen and so did not have a view, position - // it above the oldSelected (B) - selected = makeAndAddView(selectedPosition, oldSelectedStart, false, true); - } - - final int selectedStart = getChildStartEdge(selected); - final int selectedEnd = getChildEndEdge(selected); - - // Some of the newly selected item extends above the top of the list - if (selectedStart < minStart) { - // Find space required to bring the top of the selected item fully into view - final int spaceBefore = minStart - selectedStart; - - // Find space available below the selection into which we can scroll downwards - final int spaceAfter = maxEnd - selectedEnd; - - // Don't scroll more than half the height of the list - final int halfSpace = (end - start) / 2; - int offset = Math.min(spaceBefore, spaceAfter); - offset = Math.min(offset, halfSpace); - - if (mIsVertical) { - selected.offsetTopAndBottom(offset); - } else { - selected.offsetLeftAndRight(offset); - } - } - - // Fill in views above and below - fillBeforeAndAfter(selected, selectedPosition); - } else { - /* - * Case 3: Staying still - */ - - selected = makeAndAddView(selectedPosition, oldSelectedStart, true, true); - - final int selectedStart = getChildStartEdge(selected); - final int selectedEnd = getChildEndEdge(selected); - - // We're staying still... - if (oldSelectedStart < start) { - // ... but the top of the old selection was off screen. - // (This can happen if the data changes size out from under us) - int newEnd = selectedEnd; - if (newEnd < start + 20) { - // Not enough visible -- bring it onscreen - if (mIsVertical) { - selected.offsetTopAndBottom(start - selectedStart); - } else { - selected.offsetLeftAndRight(start - selectedStart); - } - } - } - - // Fill in views above and below - fillBeforeAndAfter(selected, selectedPosition); - } - - return selected; - } - - void confirmCheckedPositionsById() { - // Clear out the positional check states, we'll rebuild it below from IDs. - mCheckStates.clear(); - - for (int checkedIndex = 0; checkedIndex < mCheckedIdStates.size(); checkedIndex++) { - final long id = mCheckedIdStates.keyAt(checkedIndex); - final int lastPos = mCheckedIdStates.valueAt(checkedIndex); - - final long lastPosId = mAdapter.getItemId(lastPos); - if (id != lastPosId) { - // Look around to see if the ID is nearby. If not, uncheck it. - final int start = Math.max(0, lastPos - CHECK_POSITION_SEARCH_DISTANCE); - final int end = Math.min(lastPos + CHECK_POSITION_SEARCH_DISTANCE, mItemCount); - boolean found = false; - - for (int searchPos = start; searchPos < end; searchPos++) { - final long searchId = mAdapter.getItemId(searchPos); - if (id == searchId) { - found = true; - mCheckStates.put(searchPos, true); - mCheckedIdStates.setValueAt(checkedIndex, searchPos); - break; - } - } - - if (!found) { - mCheckedIdStates.delete(id); - checkedIndex--; - mCheckedItemCount--; - } - } else { - mCheckStates.put(lastPos, true); - } - } - } - - private void handleDataChanged() { - if (mChoiceMode != ChoiceMode.NONE && mAdapter != null && mAdapter.hasStableIds()) { - confirmCheckedPositionsById(); - } - - mRecycler.clearTransientStateViews(); - - final int itemCount = mItemCount; - if (itemCount > 0) { - int newPos; - int selectablePos; - - // Find the row we are supposed to sync to - if (mNeedSync) { - // Update this first, since setNextSelectedPositionInt inspects it - mNeedSync = false; - mPendingSync = null; - - switch (mSyncMode) { - case SYNC_SELECTED_POSITION: - if (isInTouchMode()) { - // We saved our state when not in touch mode. (We know this because - // mSyncMode is SYNC_SELECTED_POSITION.) Now we are trying to - // restore in touch mode. Just leave mSyncPosition as it is (possibly - // adjusting if the available range changed) and return. - mLayoutMode = LAYOUT_SYNC; - mSyncPosition = Math.min(Math.max(0, mSyncPosition), itemCount - 1); - - return; - } else { - // See if we can find a position in the new data with the same - // id as the old selection. This will change mSyncPosition. - newPos = findSyncPosition(); - if (newPos >= 0) { - // Found it. Now verify that new selection is still selectable - selectablePos = lookForSelectablePosition(newPos, true); - if (selectablePos == newPos) { - // Same row id is selected - mSyncPosition = newPos; - - if (mSyncSize == getSize()) { - // If we are at the same height as when we saved state, try - // to restore the scroll position too. - mLayoutMode = LAYOUT_SYNC; - } else { - // We are not the same height as when the selection was saved, so - // don't try to restore the exact position - mLayoutMode = LAYOUT_SET_SELECTION; - } - - // Restore selection - setNextSelectedPositionInt(newPos); - return; - } - } - } - break; - - case SYNC_FIRST_POSITION: - // Leave mSyncPosition as it is -- just pin to available range - mLayoutMode = LAYOUT_SYNC; - mSyncPosition = Math.min(Math.max(0, mSyncPosition), itemCount - 1); - - return; - } - } - - if (!isInTouchMode()) { - // We couldn't find matching data -- try to use the same position - newPos = getSelectedItemPosition(); - - // Pin position to the available range - if (newPos >= itemCount) { - newPos = itemCount - 1; - } - if (newPos < 0) { - newPos = 0; - } - - // Make sure we select something selectable -- first look down - selectablePos = lookForSelectablePosition(newPos, true); - - if (selectablePos >= 0) { - setNextSelectedPositionInt(selectablePos); - return; - } else { - // Looking down didn't work -- try looking up - selectablePos = lookForSelectablePosition(newPos, false); - if (selectablePos >= 0) { - setNextSelectedPositionInt(selectablePos); - return; - } - } - } else { - // We already know where we want to resurrect the selection - if (mResurrectToPosition >= 0) { - return; - } - } - } - - // Nothing is selected. Give up and reset everything. - mLayoutMode = LAYOUT_FORCE_TOP; - mSelectedPosition = INVALID_POSITION; - mSelectedRowId = INVALID_ROW_ID; - mNextSelectedPosition = INVALID_POSITION; - mNextSelectedRowId = INVALID_ROW_ID; - mNeedSync = false; - mPendingSync = null; - mSelectorPosition = INVALID_POSITION; - - checkSelectionChanged(); - } - - private int reconcileSelectedPosition() { - int position = mSelectedPosition; - if (position < 0) { - position = mResurrectToPosition; - } - - position = Math.max(0, position); - position = Math.min(position, mItemCount - 1); - - return position; - } - - boolean resurrectSelection() { - final int childCount = getChildCount(); - if (childCount <= 0) { - return false; - } - - int selectedStart = 0; - int selectedPosition; - - int start = getStartEdge(); - int end = getEndEdge(); - - final int firstPosition = mFirstPosition; - final int toPosition = mResurrectToPosition; - boolean down = true; - - if (toPosition >= firstPosition && toPosition < firstPosition + childCount) { - selectedPosition = toPosition; - - final View selected = getChildAt(selectedPosition - mFirstPosition); - selectedStart = getChildStartEdge(selected); - - final int selectedEnd = getChildEndEdge(selected); - - // We are scrolled, don't get in the fade - if (selectedStart < start) { - selectedStart = start + getFadingEdgeLength(); - } else if (selectedEnd > end) { - selectedStart = end - getChildMeasuredSize(selected) - getFadingEdgeLength(); - } - } else if (toPosition < firstPosition) { - // Default to selecting whatever is first - selectedPosition = firstPosition; - - for (int i = 0; i < childCount; i++) { - final View child = getChildAt(i); - final int childStart = getChildStartEdge(child); - - if (i == 0) { - // Remember the position of the first item - selectedStart = childStart; - - // See if we are scrolled at all - if (firstPosition > 0 || childStart < start) { - // If we are scrolled, don't select anything that is - // in the fade region - start += getFadingEdgeLength(); - } - } - - if (childStart >= start) { - // Found a view whose top is fully visible - selectedPosition = firstPosition + i; - selectedStart = childStart; - break; - } - } - } else { - final int itemCount = mItemCount; - selectedPosition = firstPosition + childCount - 1; - down = false; - - for (int i = childCount - 1; i >= 0; i--) { - final View child = getChildAt(i); - final int childStart = getChildStartEdge(child); - final int childEnd = getChildEndEdge(child); - - if (i == childCount - 1) { - selectedStart = childStart; - - if (firstPosition + childCount < itemCount || childEnd > end) { - end -= getFadingEdgeLength(); - } - } - - if (childEnd <= end) { - selectedPosition = firstPosition + i; - selectedStart = childStart; - break; - } - } - } - - mResurrectToPosition = INVALID_POSITION; - - finishSmoothScrolling(); - - mTouchMode = TOUCH_MODE_REST; - reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE); - - mSpecificStart = selectedStart; - - selectedPosition = lookForSelectablePosition(selectedPosition, down); - if (selectedPosition >= firstPosition && selectedPosition <= getLastVisiblePosition()) { - mLayoutMode = LAYOUT_SPECIFIC; - updateSelectorState(); - setSelectionInt(selectedPosition); - invokeOnItemScrollListener(); - } else { - selectedPosition = INVALID_POSITION; - } - - return selectedPosition >= 0; - } - - /** - * If there is a selection returns false. - * Otherwise resurrects the selection and returns true if resurrected. - */ - boolean resurrectSelectionIfNeeded() { - if (mSelectedPosition < 0 && resurrectSelection()) { - updateSelectorState(); - return true; - } - - return false; - } - - private int getChildWidthMeasureSpec(LayoutParams lp) { - if (!mIsVertical && lp.width == LayoutParams.WRAP_CONTENT) { - return MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); - } else if (mIsVertical) { - final int maxWidth = getWidth() - getPaddingLeft() - getPaddingRight(); - return MeasureSpec.makeMeasureSpec(maxWidth, MeasureSpec.EXACTLY); - } else { - return MeasureSpec.makeMeasureSpec(lp.width, MeasureSpec.EXACTLY); - } - } - - private int getChildHeightMeasureSpec(LayoutParams lp) { - if (mIsVertical && lp.height == LayoutParams.WRAP_CONTENT) { - return MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); - } else if (!mIsVertical) { - final int maxHeight = getHeight() - getPaddingTop() - getPaddingBottom(); - return MeasureSpec.makeMeasureSpec(maxHeight, MeasureSpec.EXACTLY); - } else { - return MeasureSpec.makeMeasureSpec(lp.height, MeasureSpec.EXACTLY); - } - } - - private void measureChild(View child) { - measureChild(child, (LayoutParams) child.getLayoutParams()); - } - - private void measureChild(View child, LayoutParams lp) { - final int widthSpec = getChildWidthMeasureSpec(lp); - final int heightSpec = getChildHeightMeasureSpec(lp); - child.measure(widthSpec, heightSpec); - } - - private void relayoutMeasuredChild(View child) { - final int w = child.getMeasuredWidth(); - final int h = child.getMeasuredHeight(); - - final int childLeft = getPaddingLeft(); - final int childRight = childLeft + w; - final int childTop = child.getTop(); - final int childBottom = childTop + h; - - child.layout(childLeft, childTop, childRight, childBottom); - } - - private void measureScrapChild(View scrapChild, int position, int secondaryMeasureSpec) { - LayoutParams lp = (LayoutParams) scrapChild.getLayoutParams(); - if (lp == null) { - lp = generateDefaultLayoutParams(); - scrapChild.setLayoutParams(lp); - } - - lp.viewType = mAdapter.getItemViewType(position); - lp.forceAdd = true; - - final int widthMeasureSpec; - final int heightMeasureSpec; - if (mIsVertical) { - widthMeasureSpec = secondaryMeasureSpec; - heightMeasureSpec = getChildHeightMeasureSpec(lp); - } else { - widthMeasureSpec = getChildWidthMeasureSpec(lp); - heightMeasureSpec = secondaryMeasureSpec; - } - - scrapChild.measure(widthMeasureSpec, heightMeasureSpec); - } - - /** - * Measures the height of the given range of children (inclusive) and - * returns the height with this TwoWayView's padding and item margin heights - * included. If maxHeight is provided, the measuring will stop when the - * current height reaches maxHeight. - * - * @param widthMeasureSpec The width measure spec to be given to a child's - * {@link View#measure(int, int)}. - * @param startPosition The position of the first child to be shown. - * @param endPosition The (inclusive) position of the last child to be - * shown. Specify {@link #NO_POSITION} if the last child should be - * the last available child from the adapter. - * @param maxHeight The maximum height that will be returned (if all the - * children don't fit in this value, this value will be - * returned). - * @param disallowPartialChildPosition In general, whether the returned - * height should only contain entire children. This is more - * powerful--it is the first inclusive position at which partial - * children will not be allowed. Example: it looks nice to have - * at least 3 completely visible children, and in portrait this - * will most likely fit; but in landscape there could be times - * when even 2 children can not be completely shown, so a value - * of 2 (remember, inclusive) would be good (assuming - * startPosition is 0). - * @return The height of this TwoWayView with the given children. - */ - private int measureHeightOfChildren(int widthMeasureSpec, int startPosition, int endPosition, - final int maxHeight, int disallowPartialChildPosition) { - - final int paddingTop = getPaddingTop(); - final int paddingBottom = getPaddingBottom(); - - final ListAdapter adapter = mAdapter; - if (adapter == null) { - return paddingTop + paddingBottom; - } - - // Include the padding of the list - int returnedHeight = paddingTop + paddingBottom; - final int itemMargin = mItemMargin; - - // The previous height value that was less than maxHeight and contained - // no partial children - int prevHeightWithoutPartialChild = 0; - int i; - View child; - - // mItemCount - 1 since endPosition parameter is inclusive - endPosition = (endPosition == NO_POSITION) ? adapter.getCount() - 1 : endPosition; - final RecycleBin recycleBin = mRecycler; - final boolean shouldRecycle = recycleOnMeasure(); - final boolean[] isScrap = mIsScrap; - - for (i = startPosition; i <= endPosition; ++i) { - child = obtainView(i, isScrap); - - measureScrapChild(child, i, widthMeasureSpec); - - if (i > 0) { - // Count the item margin for all but one child - returnedHeight += itemMargin; - } - - // Recycle the view before we possibly return from the method - if (shouldRecycle) { - recycleBin.addScrapView(child, -1); - } - - returnedHeight += child.getMeasuredHeight(); - - if (returnedHeight >= maxHeight) { - // We went over, figure out which height to return. If returnedHeight > maxHeight, - // then the i'th position did not fit completely. - return (disallowPartialChildPosition >= 0) // Disallowing is enabled (> -1) - && (i > disallowPartialChildPosition) // We've past the min pos - && (prevHeightWithoutPartialChild > 0) // We have a prev height - && (returnedHeight != maxHeight) // i'th child did not fit completely - ? prevHeightWithoutPartialChild - : maxHeight; - } - - if ((disallowPartialChildPosition >= 0) && (i >= disallowPartialChildPosition)) { - prevHeightWithoutPartialChild = returnedHeight; - } - } - - // At this point, we went through the range of children, and they each - // completely fit, so return the returnedHeight - return returnedHeight; - } - - /** - * Measures the width of the given range of children (inclusive) and - * returns the width with this TwoWayView's padding and item margin widths - * included. If maxWidth is provided, the measuring will stop when the - * current width reaches maxWidth. - * - * @param heightMeasureSpec The height measure spec to be given to a child's - * {@link View#measure(int, int)}. - * @param startPosition The position of the first child to be shown. - * @param endPosition The (inclusive) position of the last child to be - * shown. Specify {@link #NO_POSITION} if the last child should be - * the last available child from the adapter. - * @param maxWidth The maximum width that will be returned (if all the - * children don't fit in this value, this value will be - * returned). - * @param disallowPartialChildPosition In general, whether the returned - * width should only contain entire children. This is more - * powerful--it is the first inclusive position at which partial - * children will not be allowed. Example: it looks nice to have - * at least 3 completely visible children, and in portrait this - * will most likely fit; but in landscape there could be times - * when even 2 children can not be completely shown, so a value - * of 2 (remember, inclusive) would be good (assuming - * startPosition is 0). - * @return The width of this TwoWayView with the given children. - */ - private int measureWidthOfChildren(int heightMeasureSpec, int startPosition, int endPosition, - final int maxWidth, int disallowPartialChildPosition) { - - final int paddingLeft = getPaddingLeft(); - final int paddingRight = getPaddingRight(); - - final ListAdapter adapter = mAdapter; - if (adapter == null) { - return paddingLeft + paddingRight; - } - - // Include the padding of the list - int returnedWidth = paddingLeft + paddingRight; - final int itemMargin = mItemMargin; - - // The previous height value that was less than maxHeight and contained - // no partial children - int prevWidthWithoutPartialChild = 0; - int i; - View child; - - // mItemCount - 1 since endPosition parameter is inclusive - endPosition = (endPosition == NO_POSITION) ? adapter.getCount() - 1 : endPosition; - final RecycleBin recycleBin = mRecycler; - final boolean shouldRecycle = recycleOnMeasure(); - final boolean[] isScrap = mIsScrap; - - for (i = startPosition; i <= endPosition; ++i) { - child = obtainView(i, isScrap); - - measureScrapChild(child, i, heightMeasureSpec); - - if (i > 0) { - // Count the item margin for all but one child - returnedWidth += itemMargin; - } - - // Recycle the view before we possibly return from the method - if (shouldRecycle) { - recycleBin.addScrapView(child, -1); - } - - returnedWidth += child.getMeasuredWidth(); - - if (returnedWidth >= maxWidth) { - // We went over, figure out which width to return. If returnedWidth > maxWidth, - // then the i'th position did not fit completely. - return (disallowPartialChildPosition >= 0) // Disallowing is enabled (> -1) - && (i > disallowPartialChildPosition) // We've past the min pos - && (prevWidthWithoutPartialChild > 0) // We have a prev width - && (returnedWidth != maxWidth) // i'th child did not fit completely - ? prevWidthWithoutPartialChild - : maxWidth; - } - - if ((disallowPartialChildPosition >= 0) && (i >= disallowPartialChildPosition)) { - prevWidthWithoutPartialChild = returnedWidth; - } - } - - // At this point, we went through the range of children, and they each - // completely fit, so return the returnedWidth - return returnedWidth; - } - - private View makeAndAddView(int position, int offset, boolean flow, boolean selected) { - final int top; - final int left; - - // Compensate item margin on the first item of the list if the item margin - // is negative to avoid incorrect offset for the very first child. - if (mIsVertical) { - top = offset; - left = getPaddingLeft(); - } else { - top = getPaddingTop(); - left = offset; - } - - if (!mDataChanged) { - // Try to use an existing view for this position - final View activeChild = mRecycler.getActiveView(position); - if (activeChild != null) { - // Found it -- we're using an existing child - // This just needs to be positioned - setupChild(activeChild, position, top, left, flow, selected, true); - - return activeChild; - } - } - - // Make a new view for this position, or convert an unused view if possible - final View child = obtainView(position, mIsScrap); - - // This needs to be positioned and measured - setupChild(child, position, top, left, flow, selected, mIsScrap[0]); - - return child; - } - - @TargetApi(11) - private void setupChild(View child, int position, int top, int left, - boolean flow, boolean selected, boolean recycled) { - final boolean isSelected = selected && shouldShowSelector(); - final boolean updateChildSelected = isSelected != child.isSelected(); - final int touchMode = mTouchMode; - - final boolean isPressed = touchMode > TOUCH_MODE_DOWN && touchMode < TOUCH_MODE_DRAGGING && - mMotionPosition == position; - - final boolean updateChildPressed = isPressed != child.isPressed(); - final boolean needToMeasure = !recycled || updateChildSelected || child.isLayoutRequested(); - - // Respect layout params that are already in the view. Otherwise make some up... - LayoutParams lp = (LayoutParams) child.getLayoutParams(); - if (lp == null) { - lp = generateDefaultLayoutParams(); - } - - lp.viewType = mAdapter.getItemViewType(position); - - if (recycled && !lp.forceAdd) { - attachViewToParent(child, (flow ? -1 : 0), lp); - } else { - lp.forceAdd = false; - addViewInLayout(child, (flow ? -1 : 0), lp, true); - } - - if (updateChildSelected) { - child.setSelected(isSelected); - } - - if (updateChildPressed) { - child.setPressed(isPressed); - } - - if (mChoiceMode != ChoiceMode.NONE && mCheckStates != null) { - if (child instanceof Checkable) { - ((Checkable) child).setChecked(mCheckStates.get(position)); - } else if (Build.VERSION.SDK_INT >= HONEYCOMB) { - child.setActivated(mCheckStates.get(position)); - } - } - - if (needToMeasure) { - measureChild(child, lp); - } else { - cleanupLayoutState(child); - } - - final int w = child.getMeasuredWidth(); - final int h = child.getMeasuredHeight(); - - final int childTop = (mIsVertical && !flow ? top - h : top); - final int childLeft = (!mIsVertical && !flow ? left - w : left); - - if (needToMeasure) { - final int childRight = childLeft + w; - final int childBottom = childTop + h; - - child.layout(childLeft, childTop, childRight, childBottom); - } else { - child.offsetLeftAndRight(childLeft - child.getLeft()); - child.offsetTopAndBottom(childTop - child.getTop()); - } - } - - void fillGap(boolean down) { - final int childCount = getChildCount(); - - if (down) { - final int start = getStartEdge(); - final int lastEnd = getChildEndEdge(getChildAt(childCount - 1)); - final int offset = (childCount > 0 ? lastEnd + mItemMargin : start); - fillAfter(mFirstPosition + childCount, offset); - correctTooHigh(getChildCount()); - } else { - final int end = getEndEdge(); - final int firstStart = getChildStartEdge(getChildAt(0)); - final int offset = (childCount > 0 ? firstStart - mItemMargin : end); - fillBefore(mFirstPosition - 1, offset); - correctTooLow(getChildCount()); - } - } - - private View fillBefore(int pos, int nextOffset) { - View selectedView = null; - - final int start = getStartEdge(); - - while (nextOffset > start && pos >= 0) { - boolean isSelected = (pos == mSelectedPosition); - - View child = makeAndAddView(pos, nextOffset, false, isSelected); - nextOffset = getChildStartEdge(child) - mItemMargin; - - if (isSelected) { - selectedView = child; - } - - pos--; - } - - mFirstPosition = pos + 1; - - return selectedView; - } - - private View fillAfter(int pos, int nextOffset) { - View selectedView = null; - - final int end = getEndEdge(); - - while (nextOffset < end && pos < mItemCount) { - boolean selected = (pos == mSelectedPosition); - - View child = makeAndAddView(pos, nextOffset, true, selected); - nextOffset = getChildEndEdge(child) + mItemMargin; - - if (selected) { - selectedView = child; - } - - pos++; - } - - return selectedView; - } - - private View fillSpecific(int position, int offset) { - final boolean tempIsSelected = (position == mSelectedPosition); - View temp = makeAndAddView(position, offset, true, tempIsSelected); - - // Possibly changed again in fillBefore if we add rows above this one. - mFirstPosition = position; - - final int offsetBefore = getChildStartEdge(temp) - mItemMargin; - final View before = fillBefore(position - 1, offsetBefore); - - // This will correct for the top of the first view not touching the top of the list - adjustViewsStartOrEnd(); - - final int offsetAfter = getChildEndEdge(temp) + mItemMargin; - final View after = fillAfter(position + 1, offsetAfter); - - final int childCount = getChildCount(); - if (childCount > 0) { - correctTooHigh(childCount); - } - - if (tempIsSelected) { - return temp; - } else if (before != null) { - return before; - } else { - return after; - } - } - - private View fillFromOffset(int nextOffset) { - mFirstPosition = Math.min(mFirstPosition, mSelectedPosition); - mFirstPosition = Math.min(mFirstPosition, mItemCount - 1); - - if (mFirstPosition < 0) { - mFirstPosition = 0; - } - - return fillAfter(mFirstPosition, nextOffset); - } - - private View fillFromMiddle(int start, int end) { - final int size = end - start; - int position = reconcileSelectedPosition(); - - View selected = makeAndAddView(position, start, true, true); - mFirstPosition = position; - - if (mIsVertical) { - int selectedHeight = selected.getMeasuredHeight(); - if (selectedHeight <= size) { - selected.offsetTopAndBottom((size - selectedHeight) / 2); - } - } else { - int selectedWidth = selected.getMeasuredWidth(); - if (selectedWidth <= size) { - selected.offsetLeftAndRight((size - selectedWidth) / 2); - } - } - - fillBeforeAndAfter(selected, position); - correctTooHigh(getChildCount()); - - return selected; - } - - private void fillBeforeAndAfter(View selected, int position) { - final int offsetBefore = getChildStartEdge(selected) + mItemMargin; - fillBefore(position - 1, offsetBefore); - - adjustViewsStartOrEnd(); - - final int offsetAfter = getChildEndEdge(selected) + mItemMargin; - fillAfter(position + 1, offsetAfter); - } - - private View fillFromSelection(int selectedTop, int start, int end) { - int fadingEdgeLength = getFadingEdgeLength(); - final int selectedPosition = mSelectedPosition; - - final int minStart = getMinSelectionPixel(start, fadingEdgeLength, selectedPosition); - final int maxEnd = getMaxSelectionPixel(end, fadingEdgeLength, selectedPosition); - - View selected = makeAndAddView(selectedPosition, selectedTop, true, true); - - final int selectedStart = getChildStartEdge(selected); - final int selectedEnd = getChildEndEdge(selected); - - // Some of the newly selected item extends below the bottom of the list - if (selectedEnd > maxEnd) { - // Find space available above the selection into which we can scroll - // upwards - final int spaceAbove = selectedStart - minStart; - - // Find space required to bring the bottom of the selected item - // fully into view - final int spaceBelow = selectedEnd - maxEnd; - - final int offset = Math.min(spaceAbove, spaceBelow); - - // Now offset the selected item to get it into view - selected.offsetTopAndBottom(-offset); - } else if (selectedStart < minStart) { - // Find space required to bring the top of the selected item fully - // into view - final int spaceAbove = minStart - selectedStart; - - // Find space available below the selection into which we can scroll - // downwards - final int spaceBelow = maxEnd - selectedEnd; - - final int offset = Math.min(spaceAbove, spaceBelow); - - // Offset the selected item to get it into view - selected.offsetTopAndBottom(offset); - } - - // Fill in views above and below - fillBeforeAndAfter(selected, selectedPosition); - correctTooHigh(getChildCount()); - - return selected; - } - - private void correctTooHigh(int childCount) { - // First see if the last item is visible. If it is not, it is OK for the - // top of the list to be pushed up. - final int lastPosition = mFirstPosition + childCount - 1; - if (lastPosition != mItemCount - 1 || childCount == 0) { - return; - } - - // Get the last child end edge - final int lastEnd = getChildEndEdge(getChildAt(childCount - 1)); - - // This is bottom of our drawable area - final int start = getStartEdge(); - final int end = getEndEdge(); - - // This is how far the end edge of the last view is from the end of the - // drawable area - int endOffset = end - lastEnd; - - View firstChild = getChildAt(0); - int firstStart = getChildStartEdge(firstChild); - - // Make sure we are 1) Too high, and 2) Either there are more rows above the - // first row or the first row is scrolled off the top of the drawable area - if (endOffset > 0 && (mFirstPosition > 0 || firstStart < start)) { - if (mFirstPosition == 0) { - // Don't pull the top too far down - endOffset = Math.min(endOffset, start - firstStart); - } - - // Move everything down - offsetChildren(endOffset); - - if (mFirstPosition > 0) { - firstStart = getChildStartEdge(firstChild); - - // Fill the gap that was opened above mFirstPosition with more rows, if - // possible - fillBefore(mFirstPosition - 1, firstStart - mItemMargin); - - // Close up the remaining gap - adjustViewsStartOrEnd(); - } - } - } - - private void correctTooLow(int childCount) { - // First see if the first item is visible. If it is not, it is OK for the - // bottom of the list to be pushed down. - if (mFirstPosition != 0 || childCount == 0) { - return; - } - - final int firstStart = getChildStartEdge(getChildAt(0)); - - final int start = getStartEdge(); - final int end = getEndEdge(); - - // This is how far the start edge of the first view is from the start of the - // drawable area - int startOffset = firstStart - start; - - View last = getChildAt(childCount - 1); - int lastEnd = getChildEndEdge(last); - - int lastPosition = mFirstPosition + childCount - 1; - - // Make sure we are 1) Too low, and 2) Either there are more columns/rows below the - // last column/row or the last column/row is scrolled off the end of the - // drawable area - if (startOffset > 0) { - if (lastPosition < mItemCount - 1 || lastEnd > end) { - if (lastPosition == mItemCount - 1) { - // Don't pull the bottom too far up - startOffset = Math.min(startOffset, lastEnd - end); - } - - // Move everything up - offsetChildren(-startOffset); - - if (lastPosition < mItemCount - 1) { - lastEnd = getChildEndEdge(last); - - // Fill the gap that was opened below the last position with more rows, if - // possible - fillAfter(lastPosition + 1, lastEnd + mItemMargin); - - // Close up the remaining gap - adjustViewsStartOrEnd(); - } - } else if (lastPosition == mItemCount - 1) { - adjustViewsStartOrEnd(); - } - } - } - - private void adjustViewsStartOrEnd() { - if (getChildCount() == 0) { - return; - } - - int delta = getChildStartEdge(getChildAt(0)) - getStartEdge(); - - // If item margin is negative we shouldn't apply it in the - // first item of the list to avoid offsetting it incorrectly. - if (mItemMargin >= 0 || mFirstPosition != 0) { - delta -= mItemMargin; - } - - if (delta < 0) { - // We only are looking to see if we are too low, not too high - delta = 0; - } - - if (delta != 0) { - offsetChildren(-delta); - } - } - - @TargetApi(14) - private SparseBooleanArray cloneCheckStates() { - if (mCheckStates == null) { - return null; - } - - SparseBooleanArray checkedStates; - - if (Build.VERSION.SDK_INT >= 14) { - checkedStates = mCheckStates.clone(); - } else { - checkedStates = new SparseBooleanArray(); - - for (int i = 0; i < mCheckStates.size(); i++) { - checkedStates.put(mCheckStates.keyAt(i), mCheckStates.valueAt(i)); - } - } - - return checkedStates; - } - - private int findSyncPosition() { - int itemCount = mItemCount; - - if (itemCount == 0) { - return INVALID_POSITION; - } - - final long idToMatch = mSyncRowId; - - // If there isn't a selection don't hunt for it - if (idToMatch == INVALID_ROW_ID) { - return INVALID_POSITION; - } - - // Pin seed to reasonable values - int seed = mSyncPosition; - seed = Math.max(0, seed); - seed = Math.min(itemCount - 1, seed); - - long endTime = SystemClock.uptimeMillis() + SYNC_MAX_DURATION_MILLIS; - - long rowId; - - // first position scanned so far - int first = seed; - - // last position scanned so far - int last = seed; - - // True if we should move down on the next iteration - boolean next = false; - - // True when we have looked at the first item in the data - boolean hitFirst; - - // True when we have looked at the last item in the data - boolean hitLast; - - // Get the item ID locally (instead of getItemIdAtPosition), so - // we need the adapter - final ListAdapter adapter = mAdapter; - if (adapter == null) { - return INVALID_POSITION; - } - - while (SystemClock.uptimeMillis() <= endTime) { - rowId = adapter.getItemId(seed); - if (rowId == idToMatch) { - // Found it! - return seed; - } - - hitLast = (last == itemCount - 1); - hitFirst = (first == 0); - - if (hitLast && hitFirst) { - // Looked at everything - break; - } - - if (hitFirst || (next && !hitLast)) { - // Either we hit the top, or we are trying to move down - last++; - seed = last; - - // Try going up next time - next = false; - } else if (hitLast || (!next && !hitFirst)) { - // Either we hit the bottom, or we are trying to move up - first--; - seed = first; - - // Try going down next time - next = true; - } - } - - return INVALID_POSITION; - } - - @TargetApi(16) - private View obtainView(int position, boolean[] isScrap) { - isScrap[0] = false; - - View scrapView = mRecycler.getTransientStateView(position); - if (scrapView != null) { - return scrapView; - } - - scrapView = mRecycler.getScrapView(position); - - final View child; - if (scrapView != null) { - child = mAdapter.getView(position, scrapView, this); - - if (child != scrapView) { - mRecycler.addScrapView(scrapView, position); - } else { - isScrap[0] = true; - } - } else { - child = mAdapter.getView(position, null, this); - } - - if (ViewCompat.getImportantForAccessibility(child) == ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_AUTO) { - ViewCompat.setImportantForAccessibility(child, ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES); - } - - if (mHasStableIds) { - LayoutParams lp = (LayoutParams) child.getLayoutParams(); - - if (lp == null) { - lp = generateDefaultLayoutParams(); - } else if (!checkLayoutParams(lp)) { - lp = generateLayoutParams(lp); - } - - lp.id = mAdapter.getItemId(position); - - child.setLayoutParams(lp); - } - - if (mAccessibilityDelegate == null) { - mAccessibilityDelegate = new ListItemAccessibilityDelegate(); - } - - ViewCompat.setAccessibilityDelegate(child, mAccessibilityDelegate); - - return child; - } - - void resetState() { - mScroller.forceFinished(true); - - removeAllViewsInLayout(); - - mSelectedStart = 0; - mFirstPosition = 0; - mDataChanged = false; - mNeedSync = false; - mPendingSync = null; - mOldSelectedPosition = INVALID_POSITION; - mOldSelectedRowId = INVALID_ROW_ID; - - mOverScroll = 0; - - setSelectedPositionInt(INVALID_POSITION); - setNextSelectedPositionInt(INVALID_POSITION); - - mSelectorPosition = INVALID_POSITION; - mSelectorRect.setEmpty(); - - invalidate(); - } - - private void rememberSyncState() { - if (getChildCount() == 0) { - return; - } - - mNeedSync = true; - - if (mSelectedPosition >= 0) { - View child = getChildAt(mSelectedPosition - mFirstPosition); - - mSyncRowId = mNextSelectedRowId; - mSyncPosition = mNextSelectedPosition; - - if (child != null) { - mSpecificStart = getChildStartEdge(child); - } - - mSyncMode = SYNC_SELECTED_POSITION; - } else { - // Sync the based on the offset of the first view - View child = getChildAt(0); - ListAdapter adapter = getAdapter(); - - if (mFirstPosition >= 0 && mFirstPosition < adapter.getCount()) { - mSyncRowId = adapter.getItemId(mFirstPosition); - } else { - mSyncRowId = NO_ID; - } - - mSyncPosition = mFirstPosition; - - if (child != null) { - mSpecificStart = getChildStartEdge(child); - } - - mSyncMode = SYNC_FIRST_POSITION; - } - } - - private ContextMenuInfo createContextMenuInfo(View view, int position, long id) { - return new AdapterContextMenuInfo(view, position, id); - } - - @TargetApi(11) - private void updateOnScreenCheckedViews() { - final int firstPos = mFirstPosition; - final int count = getChildCount(); - - for (int i = 0; i < count; i++) { - final View child = getChildAt(i); - final int position = firstPos + i; - - if (child instanceof Checkable) { - ((Checkable) child).setChecked(mCheckStates.get(position)); - } else if (Build.VERSION.SDK_INT >= HONEYCOMB) { - child.setActivated(mCheckStates.get(position)); - } - } - } - - @Override - public boolean performItemClick(View view, int position, long id) { - boolean checkedStateChanged = false; - - if (mChoiceMode == ChoiceMode.MULTIPLE) { - boolean checked = !mCheckStates.get(position, false); - mCheckStates.put(position, checked); - - if (mCheckedIdStates != null && mAdapter.hasStableIds()) { - if (checked) { - mCheckedIdStates.put(mAdapter.getItemId(position), position); - } else { - mCheckedIdStates.delete(mAdapter.getItemId(position)); - } - } - - if (checked) { - mCheckedItemCount++; - } else { - mCheckedItemCount--; - } - - checkedStateChanged = true; - } else if (mChoiceMode == ChoiceMode.SINGLE) { - boolean checked = !mCheckStates.get(position, false); - if (checked) { - mCheckStates.clear(); - mCheckStates.put(position, true); - - if (mCheckedIdStates != null && mAdapter.hasStableIds()) { - mCheckedIdStates.clear(); - mCheckedIdStates.put(mAdapter.getItemId(position), position); - } - - mCheckedItemCount = 1; - } else if (mCheckStates.size() == 0 || !mCheckStates.valueAt(0)) { - mCheckedItemCount = 0; - } - - checkedStateChanged = true; - } - - if (checkedStateChanged) { - updateOnScreenCheckedViews(); - } - - return super.performItemClick(view, position, id); - } - - private boolean performLongPress(final View child, - final int longPressPosition, final long longPressId) { - // CHOICE_MODE_MULTIPLE_MODAL takes over long press. - boolean handled = false; - - OnItemLongClickListener listener = getOnItemLongClickListener(); - if (listener != null) { - handled = listener.onItemLongClick(TwoWayView.this, child, - longPressPosition, longPressId); - } - - if (!handled) { - mContextMenuInfo = createContextMenuInfo(child, longPressPosition, longPressId); - handled = super.showContextMenuForChild(TwoWayView.this); - } - - if (handled) { - performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); - } - - return handled; - } - - @Override - protected LayoutParams generateDefaultLayoutParams() { - if (mIsVertical) { - return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); - } else { - return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT); - } - } - - @Override - protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) { - return new LayoutParams(lp); - } - - @Override - protected boolean checkLayoutParams(ViewGroup.LayoutParams lp) { - return lp instanceof LayoutParams; - } - - @Override - public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) { - return new LayoutParams(getContext(), attrs); - } - - @Override - protected ContextMenuInfo getContextMenuInfo() { - return mContextMenuInfo; - } - - @Override - public Parcelable onSaveInstanceState() { - Parcelable superState = super.onSaveInstanceState(); - SavedState ss = new SavedState(superState); - - if (mPendingSync != null) { - ss.selectedId = mPendingSync.selectedId; - ss.firstId = mPendingSync.firstId; - ss.viewStart = mPendingSync.viewStart; - ss.position = mPendingSync.position; - ss.size = mPendingSync.size; - - return ss; - } - - boolean haveChildren = (getChildCount() > 0 && mItemCount > 0); - long selectedId = getSelectedItemId(); - ss.selectedId = selectedId; - ss.size = getSize(); - - if (selectedId >= 0) { - ss.viewStart = mSelectedStart; - ss.position = getSelectedItemPosition(); - ss.firstId = INVALID_POSITION; - } else if (haveChildren && mFirstPosition > 0) { - // Remember the position of the first child. - // We only do this if we are not currently at the top of - // the list, for two reasons: - // - // (1) The list may be in the process of becoming empty, in - // which case mItemCount may not be 0, but if we try to - // ask for any information about position 0 we will crash. - // - // (2) Being "at the top" seems like a special case, anyway, - // and the user wouldn't expect to end up somewhere else when - // they revisit the list even if its content has changed. - - ss.viewStart = getChildStartEdge(getChildAt(0)); - - int firstPos = mFirstPosition; - if (firstPos >= mItemCount) { - firstPos = mItemCount - 1; - } - - ss.position = firstPos; - ss.firstId = mAdapter.getItemId(firstPos); - } else { - ss.viewStart = 0; - ss.firstId = INVALID_POSITION; - ss.position = 0; - } - - if (mCheckStates != null) { - ss.checkState = cloneCheckStates(); - } - - if (mCheckedIdStates != null) { - final LongSparseArray idState = new LongSparseArray(); - - final int count = mCheckedIdStates.size(); - for (int i = 0; i < count; i++) { - idState.put(mCheckedIdStates.keyAt(i), mCheckedIdStates.valueAt(i)); - } - - ss.checkIdState = idState; - } - - ss.checkedItemCount = mCheckedItemCount; - - return ss; - } - - @Override - public void onRestoreInstanceState(Parcelable state) { - SavedState ss = (SavedState) state; - super.onRestoreInstanceState(ss.getSuperState()); - - mDataChanged = true; - mSyncSize = ss.size; - - if (ss.selectedId >= 0) { - mNeedSync = true; - mPendingSync = ss; - mSyncRowId = ss.selectedId; - mSyncPosition = ss.position; - mSpecificStart = ss.viewStart; - mSyncMode = SYNC_SELECTED_POSITION; - } else if (ss.firstId >= 0) { - setSelectedPositionInt(INVALID_POSITION); - - // Do this before setting mNeedSync since setNextSelectedPosition looks at mNeedSync - setNextSelectedPositionInt(INVALID_POSITION); - - mSelectorPosition = INVALID_POSITION; - mNeedSync = true; - mPendingSync = ss; - mSyncRowId = ss.firstId; - mSyncPosition = ss.position; - mSpecificStart = ss.viewStart; - mSyncMode = SYNC_FIRST_POSITION; - } - - if (ss.checkState != null) { - mCheckStates = ss.checkState; - } - - if (ss.checkIdState != null) { - mCheckedIdStates = ss.checkIdState; - } - - mCheckedItemCount = ss.checkedItemCount; - - requestLayout(); - } - - public static class LayoutParams extends ViewGroup.LayoutParams { - /** - * Type of this view as reported by the adapter - */ - int viewType; - - /** - * The stable ID of the item this view displays - */ - long id = -1; - - /** - * The position the view was removed from when pulled out of the - * scrap heap. - * @hide - */ - int scrappedFromPosition; - - /** - * When a TwoWayView is measured with an AT_MOST measure spec, it needs - * to obtain children views to measure itself. When doing so, the children - * are not attached to the window, but put in the recycler which assumes - * they've been attached before. Setting this flag will force the reused - * view to be attached to the window rather than just attached to the - * parent. - */ - boolean forceAdd; - - public LayoutParams(int width, int height) { - super(width, height); - - if (this.width == MATCH_PARENT) { - Log.w(LOGTAG, "Constructing LayoutParams with width FILL_PARENT " + - "does not make much sense as the view might change orientation. " + - "Falling back to WRAP_CONTENT"); - this.width = WRAP_CONTENT; - } - - if (this.height == MATCH_PARENT) { - Log.w(LOGTAG, "Constructing LayoutParams with height FILL_PARENT " + - "does not make much sense as the view might change orientation. " + - "Falling back to WRAP_CONTENT"); - this.height = WRAP_CONTENT; - } - } - - public LayoutParams(Context c, AttributeSet attrs) { - super(c, attrs); - - if (this.width == MATCH_PARENT) { - Log.w(LOGTAG, "Inflation setting LayoutParams width to MATCH_PARENT - " + - "does not make much sense as the view might change orientation. " + - "Falling back to WRAP_CONTENT"); - this.width = MATCH_PARENT; - } - - if (this.height == MATCH_PARENT) { - Log.w(LOGTAG, "Inflation setting LayoutParams height to MATCH_PARENT - " + - "does not make much sense as the view might change orientation. " + - "Falling back to WRAP_CONTENT"); - this.height = WRAP_CONTENT; - } - } - - public LayoutParams(ViewGroup.LayoutParams other) { - super(other); - - if (this.width == MATCH_PARENT) { - Log.w(LOGTAG, "Constructing LayoutParams with width MATCH_PARENT - " + - "does not make much sense as the view might change orientation. " + - "Falling back to WRAP_CONTENT"); - this.width = WRAP_CONTENT; - } - - if (this.height == MATCH_PARENT) { - Log.w(LOGTAG, "Constructing LayoutParams with height MATCH_PARENT - " + - "does not make much sense as the view might change orientation. " + - "Falling back to WRAP_CONTENT"); - this.height = WRAP_CONTENT; - } - } - } - - class RecycleBin { - private RecyclerListener mRecyclerListener; - private int mFirstActivePosition; - private View[] mActiveViews = new View[0]; - private ArrayList[] mScrapViews; - private int mViewTypeCount; - private ArrayList mCurrentScrap; - private SparseArrayCompat mTransientStateViews; - - public void setViewTypeCount(int viewTypeCount) { - if (viewTypeCount < 1) { - throw new IllegalArgumentException("Can't have a viewTypeCount < 1"); - } - - @SuppressWarnings({"unchecked", "rawtypes"}) - ArrayList[] scrapViews = new ArrayList[viewTypeCount]; - for (int i = 0; i < viewTypeCount; i++) { - scrapViews[i] = new ArrayList(); - } - - mViewTypeCount = viewTypeCount; - mCurrentScrap = scrapViews[0]; - mScrapViews = scrapViews; - } - - public void markChildrenDirty() { - if (mViewTypeCount == 1) { - final ArrayList scrap = mCurrentScrap; - final int scrapCount = scrap.size(); - - for (int i = 0; i < scrapCount; i++) { - scrap.get(i).forceLayout(); - } - } else { - final int typeCount = mViewTypeCount; - for (int i = 0; i < typeCount; i++) { - for (View scrap : mScrapViews[i]) { - scrap.forceLayout(); - } - } - } - - if (mTransientStateViews != null) { - final int count = mTransientStateViews.size(); - for (int i = 0; i < count; i++) { - mTransientStateViews.valueAt(i).forceLayout(); - } - } - } - - public boolean shouldRecycleViewType(int viewType) { - return viewType >= 0; - } - - void clear() { - if (mViewTypeCount == 1) { - final ArrayList scrap = mCurrentScrap; - final int scrapCount = scrap.size(); - - for (int i = 0; i < scrapCount; i++) { - removeDetachedView(scrap.remove(scrapCount - 1 - i), false); - } - } else { - final int typeCount = mViewTypeCount; - for (int i = 0; i < typeCount; i++) { - final ArrayList scrap = mScrapViews[i]; - final int scrapCount = scrap.size(); - - for (int j = 0; j < scrapCount; j++) { - removeDetachedView(scrap.remove(scrapCount - 1 - j), false); - } - } - } - - if (mTransientStateViews != null) { - mTransientStateViews.clear(); - } - } - - void fillActiveViews(int childCount, int firstActivePosition) { - if (mActiveViews.length < childCount) { - mActiveViews = new View[childCount]; - } - - mFirstActivePosition = firstActivePosition; - - final View[] activeViews = mActiveViews; - for (int i = 0; i < childCount; i++) { - View child = getChildAt(i); - - // Note: We do place AdapterView.ITEM_VIEW_TYPE_IGNORE in active views. - // However, we will NOT place them into scrap views. - activeViews[i] = child; - } - } - - View getActiveView(int position) { - final int index = position - mFirstActivePosition; - final View[] activeViews = mActiveViews; - - if (index >= 0 && index < activeViews.length) { - final View match = activeViews[index]; - activeViews[index] = null; - - return match; - } - - return null; - } - - View getTransientStateView(int position) { - if (mTransientStateViews == null) { - return null; - } - - final int index = mTransientStateViews.indexOfKey(position); - if (index < 0) { - return null; - } - - final View result = mTransientStateViews.valueAt(index); - mTransientStateViews.removeAt(index); - - return result; - } - - void clearTransientStateViews() { - if (mTransientStateViews != null) { - mTransientStateViews.clear(); - } - } - - View getScrapView(int position) { - if (mViewTypeCount == 1) { - return retrieveFromScrap(mCurrentScrap, position); - } else { - int whichScrap = mAdapter.getItemViewType(position); - if (whichScrap >= 0 && whichScrap < mScrapViews.length) { - return retrieveFromScrap(mScrapViews[whichScrap], position); - } - } - - return null; - } - - @TargetApi(14) - void addScrapView(View scrap, int position) { - LayoutParams lp = (LayoutParams) scrap.getLayoutParams(); - if (lp == null) { - return; - } - - lp.scrappedFromPosition = position; - - final int viewType = lp.viewType; - final boolean scrapHasTransientState = ViewCompat.hasTransientState(scrap); - - // Don't put views that should be ignored into the scrap heap - if (!shouldRecycleViewType(viewType) || scrapHasTransientState) { - if (scrapHasTransientState) { - if (mTransientStateViews == null) { - mTransientStateViews = new SparseArrayCompat(); - } - - mTransientStateViews.put(position, scrap); - } - - return; - } - - if (mViewTypeCount == 1) { - mCurrentScrap.add(scrap); - } else { - mScrapViews[viewType].add(scrap); - } - - // FIXME: Unfortunately, ViewCompat.setAccessibilityDelegate() doesn't accept - // null delegates. - if (Build.VERSION.SDK_INT >= 14) { - scrap.setAccessibilityDelegate(null); - } - - if (mRecyclerListener != null) { - mRecyclerListener.onMovedToScrapHeap(scrap); - } - } - - @TargetApi(14) - void scrapActiveViews() { - final View[] activeViews = mActiveViews; - final boolean multipleScraps = (mViewTypeCount > 1); - - ArrayList scrapViews = mCurrentScrap; - final int count = activeViews.length; - - for (int i = count - 1; i >= 0; i--) { - final View victim = activeViews[i]; - if (victim != null) { - final LayoutParams lp = (LayoutParams) victim.getLayoutParams(); - int whichScrap = lp.viewType; - - activeViews[i] = null; - - final boolean scrapHasTransientState = ViewCompat.hasTransientState(victim); - if (!shouldRecycleViewType(whichScrap) || scrapHasTransientState) { - if (scrapHasTransientState) { - removeDetachedView(victim, false); - - if (mTransientStateViews == null) { - mTransientStateViews = new SparseArrayCompat(); - } - - mTransientStateViews.put(mFirstActivePosition + i, victim); - } - - continue; - } - - if (multipleScraps) { - scrapViews = mScrapViews[whichScrap]; - } - - lp.scrappedFromPosition = mFirstActivePosition + i; - scrapViews.add(victim); - - // FIXME: Unfortunately, ViewCompat.setAccessibilityDelegate() doesn't accept - // null delegates. - if (Build.VERSION.SDK_INT >= 14) { - victim.setAccessibilityDelegate(null); - } - - if (mRecyclerListener != null) { - mRecyclerListener.onMovedToScrapHeap(victim); - } - } - } - - pruneScrapViews(); - } - - private void pruneScrapViews() { - final int maxViews = mActiveViews.length; - final int viewTypeCount = mViewTypeCount; - final ArrayList[] scrapViews = mScrapViews; - - for (int i = 0; i < viewTypeCount; ++i) { - final ArrayList scrapPile = scrapViews[i]; - int size = scrapPile.size(); - final int extras = size - maxViews; - - size--; - - for (int j = 0; j < extras; j++) { - removeDetachedView(scrapPile.remove(size--), false); - } - } - - if (mTransientStateViews != null) { - for (int i = 0; i < mTransientStateViews.size(); i++) { - final View v = mTransientStateViews.valueAt(i); - if (!ViewCompat.hasTransientState(v)) { - mTransientStateViews.removeAt(i); - i--; - } - } - } - } - - void reclaimScrapViews(List views) { - if (mViewTypeCount == 1) { - views.addAll(mCurrentScrap); - } else { - final int viewTypeCount = mViewTypeCount; - final ArrayList[] scrapViews = mScrapViews; - - for (int i = 0; i < viewTypeCount; ++i) { - final ArrayList scrapPile = scrapViews[i]; - views.addAll(scrapPile); - } - } - } - - View retrieveFromScrap(ArrayList scrapViews, int position) { - int size = scrapViews.size(); - if (size <= 0) { - return null; - } - - for (int i = 0; i < size; i++) { - final View scrapView = scrapViews.get(i); - final LayoutParams lp = (LayoutParams) scrapView.getLayoutParams(); - - if (lp.scrappedFromPosition == position) { - scrapViews.remove(i); - return scrapView; - } - } - - return scrapViews.remove(size - 1); - } - } - - @Override - public void setEmptyView(View emptyView) { - super.setEmptyView(emptyView); - mEmptyView = emptyView; - updateEmptyStatus(); - } - - @Override - public void setFocusable(boolean focusable) { - final ListAdapter adapter = getAdapter(); - final boolean empty = (adapter == null || adapter.getCount() == 0); - - mDesiredFocusableState = focusable; - if (!focusable) { - mDesiredFocusableInTouchModeState = false; - } - - super.setFocusable(focusable && !empty); - } - - @Override - public void setFocusableInTouchMode(boolean focusable) { - final ListAdapter adapter = getAdapter(); - final boolean empty = (adapter == null || adapter.getCount() == 0); - - mDesiredFocusableInTouchModeState = focusable; - if (focusable) { - mDesiredFocusableState = true; - } - - super.setFocusableInTouchMode(focusable && !empty); - } - - private void checkFocus() { - final ListAdapter adapter = getAdapter(); - final boolean focusable = (adapter != null && adapter.getCount() > 0); - - // The order in which we set focusable in touch mode/focusable may matter - // for the client, see View.setFocusableInTouchMode() comments for more - // details - super.setFocusableInTouchMode(focusable && mDesiredFocusableInTouchModeState); - super.setFocusable(focusable && mDesiredFocusableState); - - if (mEmptyView != null) { - updateEmptyStatus(); - } - } - - private void updateEmptyStatus() { - final boolean isEmpty = (mAdapter == null || mAdapter.isEmpty()); - - if (isEmpty) { - if (mEmptyView != null) { - mEmptyView.setVisibility(View.VISIBLE); - setVisibility(View.GONE); - } else { - // If the caller just removed our empty view, make sure the list - // view is visible - setVisibility(View.VISIBLE); - } - - // We are now GONE, so pending layouts will not be dispatched. - // Force one here to make sure that the state of the list matches - // the state of the adapter. - if (mDataChanged) { - layout(getLeft(), getTop(), getRight(), getBottom()); - } - } else { - if (mEmptyView != null) { - mEmptyView.setVisibility(View.GONE); - } - - setVisibility(View.VISIBLE); - } - } - - private class AdapterDataSetObserver extends DataSetObserver { - private Parcelable mInstanceState = null; - - @Override - public void onChanged() { - mDataChanged = true; - mOldItemCount = mItemCount; - mItemCount = getAdapter().getCount(); - - // Detect the case where a cursor that was previously invalidated has - // been re-populated with new data. - if (TwoWayView.this.mHasStableIds && mInstanceState != null - && mOldItemCount == 0 && mItemCount > 0) { - TwoWayView.this.onRestoreInstanceState(mInstanceState); - mInstanceState = null; - } else { - rememberSyncState(); - } - - checkFocus(); - requestLayout(); - } - - @Override - public void onInvalidated() { - mDataChanged = true; - - if (TwoWayView.this.mHasStableIds) { - // Remember the current state for the case where our hosting activity is being - // stopped and later restarted - mInstanceState = TwoWayView.this.onSaveInstanceState(); - } - - // Data is invalid so we should reset our state - mOldItemCount = mItemCount; - mItemCount = 0; - - mSelectedPosition = INVALID_POSITION; - mSelectedRowId = INVALID_ROW_ID; - - mNextSelectedPosition = INVALID_POSITION; - mNextSelectedRowId = INVALID_ROW_ID; - - mNeedSync = false; - - checkFocus(); - requestLayout(); - } - } - - static class SavedState extends BaseSavedState { - long selectedId; - long firstId; - int viewStart; - int position; - int size; - int checkedItemCount; - SparseBooleanArray checkState; - LongSparseArray checkIdState; - - /** - * Constructor called from {@link TwoWayView#onSaveInstanceState()} - */ - SavedState(Parcelable superState) { - super(superState); - } - - /** - * Constructor called from {@link #CREATOR} - */ - private SavedState(Parcel in) { - super(in); - - selectedId = in.readLong(); - firstId = in.readLong(); - viewStart = in.readInt(); - position = in.readInt(); - size = in.readInt(); - - checkedItemCount = in.readInt(); - checkState = in.readSparseBooleanArray(); - - final int N = in.readInt(); - if (N > 0) { - checkIdState = new LongSparseArray(); - for (int i = 0; i < N; i++) { - final long key = in.readLong(); - final int value = in.readInt(); - checkIdState.put(key, value); - } - } - } - - @Override - public void writeToParcel(Parcel out, int flags) { - super.writeToParcel(out, flags); - - out.writeLong(selectedId); - out.writeLong(firstId); - out.writeInt(viewStart); - out.writeInt(position); - out.writeInt(size); - - out.writeInt(checkedItemCount); - out.writeSparseBooleanArray(checkState); - - final int N = checkIdState != null ? checkIdState.size() : 0; - out.writeInt(N); - - for (int i = 0; i < N; i++) { - out.writeLong(checkIdState.keyAt(i)); - out.writeInt(checkIdState.valueAt(i)); - } - } - - @Override - public String toString() { - return "TwoWayView.SavedState{" - + Integer.toHexString(System.identityHashCode(this)) - + " selectedId=" + selectedId - + " firstId=" + firstId - + " viewStart=" + viewStart - + " size=" + size - + " position=" + position - + " checkState=" + checkState + "}"; - } - - public static final Parcelable.Creator CREATOR - = new Parcelable.Creator() { - @Override - public SavedState createFromParcel(Parcel in) { - return new SavedState(in); - } - - @Override - public SavedState[] newArray(int size) { - return new SavedState[size]; - } - }; - } - - private class SelectionNotifier implements Runnable { - @Override - public void run() { - if (mDataChanged) { - // Data has changed between when this SelectionNotifier - // was posted and now. We need to wait until the AdapterView - // has been synched to the new data. - if (mAdapter != null) { - post(this); - } - } else { - fireOnSelected(); - performAccessibilityActionsOnSelected(); - } - } - } - - private class WindowRunnable { - private int mOriginalAttachCount; - - public void rememberWindowAttachCount() { - mOriginalAttachCount = getWindowAttachCount(); - } - - public boolean sameWindow() { - return hasWindowFocus() && getWindowAttachCount() == mOriginalAttachCount; - } - } - - private class PerformClick extends WindowRunnable implements Runnable { - int mClickMotionPosition; - - @Override - public void run() { - if (mDataChanged) { - return; - } - - final ListAdapter adapter = mAdapter; - final int motionPosition = mClickMotionPosition; - - if (adapter != null && mItemCount > 0 && - motionPosition != INVALID_POSITION && - motionPosition < adapter.getCount() && sameWindow()) { - - final View child = getChildAt(motionPosition - mFirstPosition); - if (child != null) { - performItemClick(child, motionPosition, adapter.getItemId(motionPosition)); - } - } - } - } - - private final class CheckForTap implements Runnable { - @Override - public void run() { - if (mTouchMode != TOUCH_MODE_DOWN) { - return; - } - - mTouchMode = TOUCH_MODE_TAP; - - final View child = getChildAt(mMotionPosition - mFirstPosition); - if (child != null && !child.hasFocusable()) { - mLayoutMode = LAYOUT_NORMAL; - - if (!mDataChanged) { - setPressed(true); - child.setPressed(true); - - layoutChildren(); - positionSelector(mMotionPosition, child); - refreshDrawableState(); - - positionSelector(mMotionPosition, child); - refreshDrawableState(); - - final boolean longClickable = isLongClickable(); - - if (mSelector != null) { - Drawable d = mSelector.getCurrent(); - - if (d != null && d instanceof TransitionDrawable) { - if (longClickable) { - final int longPressTimeout = ViewConfiguration.getLongPressTimeout(); - ((TransitionDrawable) d).startTransition(longPressTimeout); - } else { - ((TransitionDrawable) d).resetTransition(); - } - } - } - - if (longClickable) { - triggerCheckForLongPress(); - } else { - mTouchMode = TOUCH_MODE_DONE_WAITING; - } - } else { - mTouchMode = TOUCH_MODE_DONE_WAITING; - } - } - } - } - - private class CheckForLongPress extends WindowRunnable implements Runnable { - @Override - public void run() { - final int motionPosition = mMotionPosition; - final View child = getChildAt(motionPosition - mFirstPosition); - - if (child != null) { - final long longPressId = mAdapter.getItemId(mMotionPosition); - - boolean handled = false; - if (sameWindow() && !mDataChanged) { - handled = performLongPress(child, motionPosition, longPressId); - } - - if (handled) { - mTouchMode = TOUCH_MODE_REST; - setPressed(false); - child.setPressed(false); - } else { - mTouchMode = TOUCH_MODE_DONE_WAITING; - } - } - } - } - - private class CheckForKeyLongPress extends WindowRunnable implements Runnable { - public void run() { - if (!isPressed() || mSelectedPosition < 0) { - return; - } - - final int index = mSelectedPosition - mFirstPosition; - final View v = getChildAt(index); - - if (!mDataChanged) { - boolean handled = false; - - if (sameWindow()) { - handled = performLongPress(v, mSelectedPosition, mSelectedRowId); - } - - if (handled) { - setPressed(false); - v.setPressed(false); - } - } else { - setPressed(false); - - if (v != null) { - v.setPressed(false); - } - } - } - } - - private static class ArrowScrollFocusResult { - private int mSelectedPosition; - private int mAmountToScroll; - - /** - * How {@link TwoWayView#arrowScrollFocused} returns its values. - */ - void populate(int selectedPosition, int amountToScroll) { - mSelectedPosition = selectedPosition; - mAmountToScroll = amountToScroll; - } - - public int getSelectedPosition() { - return mSelectedPosition; - } - - public int getAmountToScroll() { - return mAmountToScroll; - } - } - - private class ListItemAccessibilityDelegate extends AccessibilityDelegateCompat { - @Override - public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) { - super.onInitializeAccessibilityNodeInfo(host, info); - - final int position = getPositionForView(host); - final ListAdapter adapter = getAdapter(); - - // Cannot perform actions on invalid items - if (position == INVALID_POSITION || adapter == null) { - return; - } - - // Cannot perform actions on disabled items - if (!isEnabled() || !adapter.isEnabled(position)) { - return; - } - - if (position == getSelectedItemPosition()) { - info.setSelected(true); - info.addAction(AccessibilityNodeInfoCompat.ACTION_CLEAR_SELECTION); - } else { - info.addAction(AccessibilityNodeInfoCompat.ACTION_SELECT); - } - - if (isClickable()) { - info.addAction(AccessibilityNodeInfoCompat.ACTION_CLICK); - info.setClickable(true); - } - - if (isLongClickable()) { - info.addAction(AccessibilityNodeInfoCompat.ACTION_LONG_CLICK); - info.setLongClickable(true); - } - } - - @Override - public boolean performAccessibilityAction(View host, int action, Bundle arguments) { - if (super.performAccessibilityAction(host, action, arguments)) { - return true; - } - - final int position = getPositionForView(host); - final ListAdapter adapter = getAdapter(); - - // Cannot perform actions on invalid items - if (position == INVALID_POSITION || adapter == null) { - return false; - } - - // Cannot perform actions on disabled items - if (!isEnabled() || !adapter.isEnabled(position)) { - return false; - } - - final long id = getItemIdAtPosition(position); - - switch (action) { - case AccessibilityNodeInfoCompat.ACTION_CLEAR_SELECTION: - if (getSelectedItemPosition() == position) { - setSelection(INVALID_POSITION); - return true; - } - return false; - - case AccessibilityNodeInfoCompat.ACTION_SELECT: - if (getSelectedItemPosition() != position) { - setSelection(position); - return true; - } - return false; - - case AccessibilityNodeInfoCompat.ACTION_CLICK: - return isClickable() && performItemClick(host, position, id); - - case AccessibilityNodeInfoCompat.ACTION_LONG_CLICK: - return isLongClickable() && performLongPress(host, position, id); - } - - return false; - } - } - - private class PositionScroller implements Runnable { - private static final int SCROLL_DURATION = 200; - - private static final int MOVE_AFTER_POS = 1; - private static final int MOVE_BEFORE_POS = 2; - private static final int MOVE_AFTER_BOUND = 3; - private static final int MOVE_BEFORE_BOUND = 4; - private static final int MOVE_OFFSET = 5; - - private int mMode; - private int mTargetPosition; - private int mBoundPosition; - private int mLastSeenPosition; - private int mScrollDuration; - private final int mExtraScroll; - - private int mOffsetFromStart; - - PositionScroller() { - mExtraScroll = ViewConfiguration.get(mContext).getScaledFadingEdgeLength(); - } - - void start(final int position) { - stop(); - - if (mDataChanged) { - // Wait until we're back in a stable state to try this. - mPositionScrollAfterLayout = new Runnable() { - @Override public void run() { - start(position); - } - }; - - return; - } - - final int childCount = getChildCount(); - if (childCount == 0) { - // Can't scroll without children. - return; - } - - final int firstPosition = mFirstPosition; - final int lastPosition = firstPosition + childCount - 1; - - final int clampedPosition = Math.max(0, Math.min(getCount() - 1, position)); - - final int viewTravelCount; - if (clampedPosition < firstPosition) { - viewTravelCount = firstPosition - clampedPosition + 1; - mMode = MOVE_BEFORE_POS; - } else if (clampedPosition > lastPosition) { - viewTravelCount = clampedPosition - lastPosition + 1; - mMode = MOVE_AFTER_POS; - } else { - scrollToVisible(clampedPosition, INVALID_POSITION, SCROLL_DURATION); - return; - } - - if (viewTravelCount > 0) { - mScrollDuration = SCROLL_DURATION / viewTravelCount; - } else { - mScrollDuration = SCROLL_DURATION; - } - - mTargetPosition = clampedPosition; - mBoundPosition = INVALID_POSITION; - mLastSeenPosition = INVALID_POSITION; - - ViewCompat.postOnAnimation(TwoWayView.this, this); - } - - void start(final int position, final int boundPosition) { - stop(); - - if (boundPosition == INVALID_POSITION) { - start(position); - return; - } - - if (mDataChanged) { - // Wait until we're back in a stable state to try this. - mPositionScrollAfterLayout = new Runnable() { - @Override public void run() { - start(position, boundPosition); - } - }; - - return; - } - - final int childCount = getChildCount(); - if (childCount == 0) { - // Can't scroll without children. - return; - } - - final int firstPosition = mFirstPosition; - final int lastPosition = firstPosition + childCount - 1; - - final int clampedPosition = Math.max(0, Math.min(getCount() - 1, position)); - - final int viewTravelCount; - if (clampedPosition < firstPosition) { - final int boundPositionFromLast = lastPosition - boundPosition; - if (boundPositionFromLast < 1) { - // Moving would shift our bound position off the screen. Abort. - return; - } - - final int positionTravel = firstPosition - clampedPosition + 1; - final int boundTravel = boundPositionFromLast - 1; - if (boundTravel < positionTravel) { - viewTravelCount = boundTravel; - mMode = MOVE_BEFORE_BOUND; - } else { - viewTravelCount = positionTravel; - mMode = MOVE_BEFORE_POS; - } - } else if (clampedPosition > lastPosition) { - final int boundPositionFromFirst = boundPosition - firstPosition; - if (boundPositionFromFirst < 1) { - // Moving would shift our bound position off the screen. Abort. - return; - } - - final int positionTravel = clampedPosition - lastPosition + 1; - final int boundTravel = boundPositionFromFirst - 1; - if (boundTravel < positionTravel) { - viewTravelCount = boundTravel; - mMode = MOVE_AFTER_BOUND; - } else { - viewTravelCount = positionTravel; - mMode = MOVE_AFTER_POS; - } - } else { - scrollToVisible(clampedPosition, boundPosition, SCROLL_DURATION); - return; - } - - if (viewTravelCount > 0) { - mScrollDuration = SCROLL_DURATION / viewTravelCount; - } else { - mScrollDuration = SCROLL_DURATION; - } - - mTargetPosition = clampedPosition; - mBoundPosition = boundPosition; - mLastSeenPosition = INVALID_POSITION; - - ViewCompat.postOnAnimation(TwoWayView.this, this); - } - - void startWithOffset(int position, int offset) { - startWithOffset(position, offset, SCROLL_DURATION); - } - - void startWithOffset(final int position, int offset, final int duration) { - stop(); - - if (mDataChanged) { - // Wait until we're back in a stable state to try this. - final int postOffset = offset; - mPositionScrollAfterLayout = new Runnable() { - @Override public void run() { - startWithOffset(position, postOffset, duration); - } - }; - - return; - } - - final int childCount = getChildCount(); - if (childCount == 0) { - // Can't scroll without children. - return; - } - - offset += getStartEdge(); - - mTargetPosition = Math.max(0, Math.min(getCount() - 1, position)); - mOffsetFromStart = offset; - mBoundPosition = INVALID_POSITION; - mLastSeenPosition = INVALID_POSITION; - mMode = MOVE_OFFSET; - - final int firstPosition = mFirstPosition; - final int lastPosition = firstPosition + childCount - 1; - - final int viewTravelCount; - if (mTargetPosition < firstPosition) { - viewTravelCount = firstPosition - mTargetPosition; - } else if (mTargetPosition > lastPosition) { - viewTravelCount = mTargetPosition - lastPosition; - } else { - // On-screen, just scroll. - final View targetView = getChildAt(mTargetPosition - firstPosition); - final int targetStart = getChildStartEdge(targetView); - smoothScrollBy(targetStart - offset, duration); - return; - } - - // Estimate how many screens we should travel - final float screenTravelCount = (float) viewTravelCount / childCount; - mScrollDuration = screenTravelCount < 1 ? - duration : (int) (duration / screenTravelCount); - mLastSeenPosition = INVALID_POSITION; - - ViewCompat.postOnAnimation(TwoWayView.this, this); - } - - /** - * Scroll such that targetPos is in the visible padded region without scrolling - * boundPos out of view. Assumes targetPos is onscreen. - */ - void scrollToVisible(int targetPosition, int boundPosition, int duration) { - final int childCount = getChildCount(); - final int firstPosition = mFirstPosition; - final int lastPosition = firstPosition + childCount - 1; - - final int start = getStartEdge(); - final int end = getEndEdge(); - - if (targetPosition < firstPosition || targetPosition > lastPosition) { - Log.w(LOGTAG, "scrollToVisible called with targetPosition " + targetPosition + - " not visible [" + firstPosition + ", " + lastPosition + "]"); - } - - if (boundPosition < firstPosition || boundPosition > lastPosition) { - // boundPos doesn't matter, it's already offscreen. - boundPosition = INVALID_POSITION; - } - - final View targetChild = getChildAt(targetPosition - firstPosition); - final int targetStart = getChildStartEdge(targetChild); - final int targetEnd = getChildEndEdge(targetChild); - - int scrollBy = 0; - if (targetEnd > end) { - scrollBy = targetEnd - end; - } - if (targetStart < start) { - scrollBy = targetStart - start; - } - - if (scrollBy == 0) { - return; - } - - if (boundPosition >= 0) { - final View boundChild = getChildAt(boundPosition - firstPosition); - final int boundStart = getChildStartEdge(boundChild); - final int boundEnd = getChildEndEdge(boundChild); - final int absScroll = Math.abs(scrollBy); - - if (scrollBy < 0 && boundEnd + absScroll > end) { - // Don't scroll the bound view off the end of the screen. - scrollBy = Math.max(0, boundEnd - end); - } else if (scrollBy > 0 && boundStart - absScroll < start) { - // Don't scroll the bound view off the top of the screen. - scrollBy = Math.min(0, boundStart - start); - } - } - - smoothScrollBy(scrollBy, duration); - } - - void stop() { - removeCallbacks(this); - } - - @Override - public void run() { - final int size = getAvailableSize(); - final int firstPosition = mFirstPosition; - - final int startPadding = (mIsVertical ? getPaddingTop() : getPaddingLeft()); - final int endPadding = (mIsVertical ? getPaddingBottom() : getPaddingRight()); - - switch (mMode) { - case MOVE_AFTER_POS: { - final int lastViewIndex = getChildCount() - 1; - if (lastViewIndex < 0) { - return; - } - - final int lastPosition = firstPosition + lastViewIndex; - if (lastPosition == mLastSeenPosition) { - // No new views, let things keep going. - ViewCompat.postOnAnimation(TwoWayView.this, this); - return; - } - - final View lastView = getChildAt(lastViewIndex); - final int lastViewSize = getChildSize(lastView); - final int lastViewStart = getChildStartEdge(lastView); - final int lastViewPixelsShowing = size - lastViewStart; - final int extraScroll = lastPosition < mItemCount - 1 ? - Math.max(endPadding, mExtraScroll) : endPadding; - - final int scrollBy = lastViewSize - lastViewPixelsShowing + extraScroll; - smoothScrollBy(scrollBy, mScrollDuration); - - mLastSeenPosition = lastPosition; - if (lastPosition < mTargetPosition) { - ViewCompat.postOnAnimation(TwoWayView.this, this); - } - - break; - } - - case MOVE_AFTER_BOUND: { - final int nextViewIndex = 1; - final int childCount = getChildCount(); - if (firstPosition == mBoundPosition || - childCount <= nextViewIndex || - firstPosition + childCount >= mItemCount) { - return; - } - - final int nextPosition = firstPosition + nextViewIndex; - - if (nextPosition == mLastSeenPosition) { - // No new views, let things keep going. - ViewCompat.postOnAnimation(TwoWayView.this, this); - return; - } - - final View nextView = getChildAt(nextViewIndex); - final int nextViewSize = getChildSize(nextView); - final int nextViewStart = getChildStartEdge(nextView); - final int extraScroll = Math.max(endPadding, mExtraScroll); - if (nextPosition < mBoundPosition) { - smoothScrollBy(Math.max(0, nextViewSize + nextViewStart - extraScroll), - mScrollDuration); - mLastSeenPosition = nextPosition; - ViewCompat.postOnAnimation(TwoWayView.this, this); - } else { - if (nextViewSize > extraScroll) { - smoothScrollBy(nextViewSize - extraScroll, mScrollDuration); - } - } - - break; - } - - case MOVE_BEFORE_POS: { - if (firstPosition == mLastSeenPosition) { - // No new views, let things keep going. - ViewCompat.postOnAnimation(TwoWayView.this, this); - return; - } - - final View firstView = getChildAt(0); - if (firstView == null) { - return; - } - - final int firstViewTop = getChildStartEdge(firstView); - final int extraScroll = firstPosition > 0 ? - Math.max(mExtraScroll, startPadding) : startPadding; - - smoothScrollBy(firstViewTop - extraScroll, mScrollDuration); - mLastSeenPosition = firstPosition; - - if (firstPosition > mTargetPosition) { - ViewCompat.postOnAnimation(TwoWayView.this, this); - } - - break; - } - - case MOVE_BEFORE_BOUND: { - final int lastViewIndex = getChildCount() - 2; - if (lastViewIndex < 0) { - return; - } - - final int lastPosition = firstPosition + lastViewIndex; - - if (lastPosition == mLastSeenPosition) { - // No new views, let things keep going. - ViewCompat.postOnAnimation(TwoWayView.this, this); - return; - } - - final View lastView = getChildAt(lastViewIndex); - final int lastViewSize = getChildSize(lastView); - final int lastViewStart = getChildStartEdge(lastView); - final int lastViewPixelsShowing = size - lastViewStart; - final int extraScroll = Math.max(startPadding, mExtraScroll); - - mLastSeenPosition = lastPosition; - - if (lastPosition > mBoundPosition) { - smoothScrollBy(-(lastViewPixelsShowing - extraScroll), mScrollDuration); - ViewCompat.postOnAnimation(TwoWayView.this, this); - } else { - final int end = size - extraScroll; - final int lastViewEnd = lastViewStart + lastViewSize; - if (end > lastViewEnd) { - smoothScrollBy(-(end - lastViewEnd), mScrollDuration); - } - } - - break; - } - - case MOVE_OFFSET: { - if (mLastSeenPosition == firstPosition) { - // No new views, let things keep going. - ViewCompat.postOnAnimation(TwoWayView.this, this); - return; - } - - mLastSeenPosition = firstPosition; - - final int childCount = getChildCount(); - final int position = mTargetPosition; - final int lastPos = firstPosition + childCount - 1; - - int viewTravelCount = 0; - if (position < firstPosition) { - viewTravelCount = firstPosition - position + 1; - } else if (position > lastPos) { - viewTravelCount = position - lastPos; - } - - // Estimate how many screens we should travel - final float screenTravelCount = (float) viewTravelCount / childCount; - - final float modifier = Math.min(Math.abs(screenTravelCount), 1.f); - if (position < firstPosition) { - final int distance = (int) (-getSize() * modifier); - final int duration = (int) (mScrollDuration * modifier); - smoothScrollBy(distance, duration); - ViewCompat.postOnAnimation(TwoWayView.this, this); - } else if (position > lastPos) { - final int distance = (int) (getSize() * modifier); - final int duration = (int) (mScrollDuration * modifier); - smoothScrollBy(distance, duration); - ViewCompat.postOnAnimation(TwoWayView.this, this); - } else { - // On-screen, just scroll. - final View targetView = getChildAt(position - firstPosition); - final int targetStart = getChildStartEdge(targetView); - final int distance = targetStart - mOffsetFromStart; - final int duration = (int) (mScrollDuration * - ((float) Math.abs(distance) / getSize())); - smoothScrollBy(distance, duration); - } - - break; - } - - default: - break; - } - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/widget/themed/ThemedEditText.java b/mobile/android/base/java/org/mozilla/gecko/widget/themed/ThemedEditText.java deleted file mode 100644 index c84686e905..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/widget/themed/ThemedEditText.java +++ /dev/null @@ -1,172 +0,0 @@ -// This file is generated by generate_themed_views.py; do not edit. - -/* 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.widget.themed; - -import android.support.v4.content.ContextCompat; -import org.mozilla.gecko.GeckoApplication; -import org.mozilla.gecko.lwt.LightweightTheme; -import org.mozilla.gecko.R; -import org.mozilla.gecko.util.DrawableUtil; - -import android.content.Context; -import android.content.res.ColorStateList; -import android.content.res.TypedArray; -import android.graphics.drawable.ColorDrawable; -import android.graphics.drawable.Drawable; -import android.util.AttributeSet; - -public class ThemedEditText extends android.widget.EditText - implements LightweightTheme.OnChangeListener { - private LightweightTheme theme; - - private static final int[] STATE_PRIVATE_MODE = { R.attr.state_private }; - private static final int[] STATE_LIGHT = { R.attr.state_light }; - private static final int[] STATE_DARK = { R.attr.state_dark }; - - protected static final int[] PRIVATE_PRESSED_STATE_SET = { R.attr.state_private, android.R.attr.state_pressed }; - protected static final int[] PRIVATE_FOCUSED_STATE_SET = { R.attr.state_private, android.R.attr.state_focused }; - protected static final int[] PRIVATE_STATE_SET = { R.attr.state_private }; - - private boolean isPrivate; - private boolean isLight; - private boolean isDark; - private boolean autoUpdateTheme; // always false if there's no theme. - - private ColorStateList drawableColors; - - public ThemedEditText(Context context, AttributeSet attrs) { - super(context, attrs); - initialize(context, attrs, 0); - } - - public ThemedEditText(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - initialize(context, attrs, defStyle); - } - - private void initialize(final Context context, final AttributeSet attrs, final int defStyle) { - // The theme can be null, particularly if we might be instantiating this - // View in an IDE, with no ambient GeckoApplication. - final Context applicationContext = context.getApplicationContext(); - if (applicationContext instanceof GeckoApplication) { - theme = ((GeckoApplication) applicationContext).getLightweightTheme(); - } - - final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.LightweightTheme); - autoUpdateTheme = theme != null && a.getBoolean(R.styleable.LightweightTheme_autoUpdateTheme, true); - a.recycle(); - } - - @Override - public void onAttachedToWindow() { - super.onAttachedToWindow(); - - if (autoUpdateTheme) - theme.addListener(this); - } - - @Override - public void onDetachedFromWindow() { - super.onDetachedFromWindow(); - - if (autoUpdateTheme) - theme.removeListener(this); - } - - @Override - public int[] onCreateDrawableState(int extraSpace) { - final int[] drawableState = super.onCreateDrawableState(extraSpace + 1); - - if (isPrivate) - mergeDrawableStates(drawableState, STATE_PRIVATE_MODE); - else if (isLight) - mergeDrawableStates(drawableState, STATE_LIGHT); - else if (isDark) - mergeDrawableStates(drawableState, STATE_DARK); - - return drawableState; - } - - @Override - public void onLightweightThemeChanged() { - if (autoUpdateTheme && theme.isEnabled()) - setTheme(theme.isLightTheme()); - } - - @Override - public void onLightweightThemeReset() { - if (autoUpdateTheme) - resetTheme(); - } - - @Override - protected void onLayout(boolean changed, int left, int top, int right, int bottom) { - super.onLayout(changed, left, top, right, bottom); - onLightweightThemeChanged(); - } - - public boolean isPrivateMode() { - return isPrivate; - } - - public void setPrivateMode(boolean isPrivate) { - if (this.isPrivate != isPrivate) { - this.isPrivate = isPrivate; - refreshDrawableState(); - invalidate(); - } - } - - public void setTheme(boolean isLight) { - // Set the theme only if it is different from existing theme. - if ((isLight && this.isLight != isLight) || - (!isLight && this.isDark == isLight)) { - if (isLight) { - this.isLight = true; - this.isDark = false; - } else { - this.isLight = false; - this.isDark = true; - } - - refreshDrawableState(); - invalidate(); - } - } - - public void resetTheme() { - if (isLight || isDark) { - isLight = false; - isDark = false; - refreshDrawableState(); - invalidate(); - } - } - - public void setAutoUpdateTheme(boolean autoUpdateTheme) { - if (theme == null) { - return; - } - - if (this.autoUpdateTheme != autoUpdateTheme) { - this.autoUpdateTheme = autoUpdateTheme; - - if (autoUpdateTheme) - theme.addListener(this); - else - theme.removeListener(this); - } - } - - public ColorDrawable getColorDrawable(int id) { - return new ColorDrawable(ContextCompat.getColor(getContext(), id)); - } - - protected LightweightTheme getTheme() { - return theme; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/widget/themed/ThemedFrameLayout.java b/mobile/android/base/java/org/mozilla/gecko/widget/themed/ThemedFrameLayout.java deleted file mode 100644 index a95fe2d9f2..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/widget/themed/ThemedFrameLayout.java +++ /dev/null @@ -1,172 +0,0 @@ -// This file is generated by generate_themed_views.py; do not edit. - -/* 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.widget.themed; - -import android.support.v4.content.ContextCompat; -import org.mozilla.gecko.GeckoApplication; -import org.mozilla.gecko.lwt.LightweightTheme; -import org.mozilla.gecko.R; -import org.mozilla.gecko.util.DrawableUtil; - -import android.content.Context; -import android.content.res.ColorStateList; -import android.content.res.TypedArray; -import android.graphics.drawable.ColorDrawable; -import android.graphics.drawable.Drawable; -import android.util.AttributeSet; - -public class ThemedFrameLayout extends android.widget.FrameLayout - implements LightweightTheme.OnChangeListener { - private LightweightTheme theme; - - private static final int[] STATE_PRIVATE_MODE = { R.attr.state_private }; - private static final int[] STATE_LIGHT = { R.attr.state_light }; - private static final int[] STATE_DARK = { R.attr.state_dark }; - - protected static final int[] PRIVATE_PRESSED_STATE_SET = { R.attr.state_private, android.R.attr.state_pressed }; - protected static final int[] PRIVATE_FOCUSED_STATE_SET = { R.attr.state_private, android.R.attr.state_focused }; - protected static final int[] PRIVATE_STATE_SET = { R.attr.state_private }; - - private boolean isPrivate; - private boolean isLight; - private boolean isDark; - private boolean autoUpdateTheme; // always false if there's no theme. - - private ColorStateList drawableColors; - - public ThemedFrameLayout(Context context, AttributeSet attrs) { - super(context, attrs); - initialize(context, attrs, 0); - } - - public ThemedFrameLayout(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - initialize(context, attrs, defStyle); - } - - private void initialize(final Context context, final AttributeSet attrs, final int defStyle) { - // The theme can be null, particularly if we might be instantiating this - // View in an IDE, with no ambient GeckoApplication. - final Context applicationContext = context.getApplicationContext(); - if (applicationContext instanceof GeckoApplication) { - theme = ((GeckoApplication) applicationContext).getLightweightTheme(); - } - - final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.LightweightTheme); - autoUpdateTheme = theme != null && a.getBoolean(R.styleable.LightweightTheme_autoUpdateTheme, true); - a.recycle(); - } - - @Override - public void onAttachedToWindow() { - super.onAttachedToWindow(); - - if (autoUpdateTheme) - theme.addListener(this); - } - - @Override - public void onDetachedFromWindow() { - super.onDetachedFromWindow(); - - if (autoUpdateTheme) - theme.removeListener(this); - } - - @Override - public int[] onCreateDrawableState(int extraSpace) { - final int[] drawableState = super.onCreateDrawableState(extraSpace + 1); - - if (isPrivate) - mergeDrawableStates(drawableState, STATE_PRIVATE_MODE); - else if (isLight) - mergeDrawableStates(drawableState, STATE_LIGHT); - else if (isDark) - mergeDrawableStates(drawableState, STATE_DARK); - - return drawableState; - } - - @Override - public void onLightweightThemeChanged() { - if (autoUpdateTheme && theme.isEnabled()) - setTheme(theme.isLightTheme()); - } - - @Override - public void onLightweightThemeReset() { - if (autoUpdateTheme) - resetTheme(); - } - - @Override - protected void onLayout(boolean changed, int left, int top, int right, int bottom) { - super.onLayout(changed, left, top, right, bottom); - onLightweightThemeChanged(); - } - - public boolean isPrivateMode() { - return isPrivate; - } - - public void setPrivateMode(boolean isPrivate) { - if (this.isPrivate != isPrivate) { - this.isPrivate = isPrivate; - refreshDrawableState(); - invalidate(); - } - } - - public void setTheme(boolean isLight) { - // Set the theme only if it is different from existing theme. - if ((isLight && this.isLight != isLight) || - (!isLight && this.isDark == isLight)) { - if (isLight) { - this.isLight = true; - this.isDark = false; - } else { - this.isLight = false; - this.isDark = true; - } - - refreshDrawableState(); - invalidate(); - } - } - - public void resetTheme() { - if (isLight || isDark) { - isLight = false; - isDark = false; - refreshDrawableState(); - invalidate(); - } - } - - public void setAutoUpdateTheme(boolean autoUpdateTheme) { - if (theme == null) { - return; - } - - if (this.autoUpdateTheme != autoUpdateTheme) { - this.autoUpdateTheme = autoUpdateTheme; - - if (autoUpdateTheme) - theme.addListener(this); - else - theme.removeListener(this); - } - } - - public ColorDrawable getColorDrawable(int id) { - return new ColorDrawable(ContextCompat.getColor(getContext(), id)); - } - - protected LightweightTheme getTheme() { - return theme; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/widget/themed/ThemedImageButton.java b/mobile/android/base/java/org/mozilla/gecko/widget/themed/ThemedImageButton.java deleted file mode 100644 index 88e94c6c74..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/widget/themed/ThemedImageButton.java +++ /dev/null @@ -1,200 +0,0 @@ -// This file is generated by generate_themed_views.py; do not edit. - -/* 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.widget.themed; - -import android.support.v4.content.ContextCompat; -import org.mozilla.gecko.GeckoApplication; -import org.mozilla.gecko.lwt.LightweightTheme; -import org.mozilla.gecko.R; -import org.mozilla.gecko.util.DrawableUtil; - -import android.content.Context; -import android.content.res.ColorStateList; -import android.content.res.TypedArray; -import android.graphics.drawable.ColorDrawable; -import android.graphics.drawable.Drawable; -import android.util.AttributeSet; - -public class ThemedImageButton extends android.widget.ImageButton - implements LightweightTheme.OnChangeListener { - private LightweightTheme theme; - - private static final int[] STATE_PRIVATE_MODE = { R.attr.state_private }; - private static final int[] STATE_LIGHT = { R.attr.state_light }; - private static final int[] STATE_DARK = { R.attr.state_dark }; - - protected static final int[] PRIVATE_PRESSED_STATE_SET = { R.attr.state_private, android.R.attr.state_pressed }; - protected static final int[] PRIVATE_FOCUSED_STATE_SET = { R.attr.state_private, android.R.attr.state_focused }; - protected static final int[] PRIVATE_STATE_SET = { R.attr.state_private }; - - private boolean isPrivate; - private boolean isLight; - private boolean isDark; - private boolean autoUpdateTheme; // always false if there's no theme. - - private ColorStateList drawableColors; - - public ThemedImageButton(Context context, AttributeSet attrs) { - super(context, attrs); - initialize(context, attrs, 0); - } - - public ThemedImageButton(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - initialize(context, attrs, defStyle); - } - - private void initialize(final Context context, final AttributeSet attrs, final int defStyle) { - // The theme can be null, particularly if we might be instantiating this - // View in an IDE, with no ambient GeckoApplication. - final Context applicationContext = context.getApplicationContext(); - if (applicationContext instanceof GeckoApplication) { - theme = ((GeckoApplication) applicationContext).getLightweightTheme(); - } - - final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.LightweightTheme); - autoUpdateTheme = theme != null && a.getBoolean(R.styleable.LightweightTheme_autoUpdateTheme, true); - a.recycle(); - - final TypedArray themedA = context.obtainStyledAttributes(attrs, R.styleable.ThemedView, defStyle, 0); - drawableColors = themedA.getColorStateList(R.styleable.ThemedView_drawableTintList); - themedA.recycle(); - - // Apply the tint initially - the Drawable is - // initially set by XML via super's constructor. - setTintedImageDrawable(getDrawable()); - } - - @Override - public void onAttachedToWindow() { - super.onAttachedToWindow(); - - if (autoUpdateTheme) - theme.addListener(this); - } - - @Override - public void onDetachedFromWindow() { - super.onDetachedFromWindow(); - - if (autoUpdateTheme) - theme.removeListener(this); - } - - @Override - public int[] onCreateDrawableState(int extraSpace) { - final int[] drawableState = super.onCreateDrawableState(extraSpace + 1); - - if (isPrivate) - mergeDrawableStates(drawableState, STATE_PRIVATE_MODE); - else if (isLight) - mergeDrawableStates(drawableState, STATE_LIGHT); - else if (isDark) - mergeDrawableStates(drawableState, STATE_DARK); - - return drawableState; - } - - @Override - public void onLightweightThemeChanged() { - if (autoUpdateTheme && theme.isEnabled()) - setTheme(theme.isLightTheme()); - } - - @Override - public void onLightweightThemeReset() { - if (autoUpdateTheme) - resetTheme(); - } - - @Override - protected void onLayout(boolean changed, int left, int top, int right, int bottom) { - super.onLayout(changed, left, top, right, bottom); - onLightweightThemeChanged(); - } - - public boolean isPrivateMode() { - return isPrivate; - } - - public void setPrivateMode(boolean isPrivate) { - if (this.isPrivate != isPrivate) { - this.isPrivate = isPrivate; - refreshDrawableState(); - invalidate(); - } - } - - public void setTheme(boolean isLight) { - // Set the theme only if it is different from existing theme. - if ((isLight && this.isLight != isLight) || - (!isLight && this.isDark == isLight)) { - if (isLight) { - this.isLight = true; - this.isDark = false; - } else { - this.isLight = false; - this.isDark = true; - } - - refreshDrawableState(); - invalidate(); - } - } - - public void resetTheme() { - if (isLight || isDark) { - isLight = false; - isDark = false; - refreshDrawableState(); - invalidate(); - } - } - - public void setAutoUpdateTheme(boolean autoUpdateTheme) { - if (theme == null) { - return; - } - - if (this.autoUpdateTheme != autoUpdateTheme) { - this.autoUpdateTheme = autoUpdateTheme; - - if (autoUpdateTheme) - theme.addListener(this); - else - theme.removeListener(this); - } - } - - @Override - public void setImageDrawable(final Drawable drawable) { - setTintedImageDrawable(drawable); - } - - private void setTintedImageDrawable(final Drawable drawable) { - final Drawable tintedDrawable; - if (drawableColors == null || R.id.bookmark == getId()) { - // NB: The bookmarked state uses a blue star, so this is a hack to keep it untinted. - // NB: If we tint a drawable with a null ColorStateList, it will override - // any existing colorFilters and tint... so don't! - tintedDrawable = drawable; - } else if (drawable == null) { - tintedDrawable = null; - } else { - tintedDrawable = DrawableUtil.tintDrawableWithStateList(drawable, drawableColors); - } - super.setImageDrawable(tintedDrawable); - } - - public ColorDrawable getColorDrawable(int id) { - return new ColorDrawable(ContextCompat.getColor(getContext(), id)); - } - - protected LightweightTheme getTheme() { - return theme; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/widget/themed/ThemedImageView.java b/mobile/android/base/java/org/mozilla/gecko/widget/themed/ThemedImageView.java deleted file mode 100644 index befbe6fb5e..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/widget/themed/ThemedImageView.java +++ /dev/null @@ -1,199 +0,0 @@ -// This file is generated by generate_themed_views.py; do not edit. - -/* 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.widget.themed; - -import android.support.v4.content.ContextCompat; -import org.mozilla.gecko.GeckoApplication; -import org.mozilla.gecko.lwt.LightweightTheme; -import org.mozilla.gecko.R; -import org.mozilla.gecko.util.DrawableUtil; - -import android.content.Context; -import android.content.res.ColorStateList; -import android.content.res.TypedArray; -import android.graphics.drawable.ColorDrawable; -import android.graphics.drawable.Drawable; -import android.util.AttributeSet; - -public class ThemedImageView extends android.widget.ImageView - implements LightweightTheme.OnChangeListener { - private LightweightTheme theme; - - private static final int[] STATE_PRIVATE_MODE = { R.attr.state_private }; - private static final int[] STATE_LIGHT = { R.attr.state_light }; - private static final int[] STATE_DARK = { R.attr.state_dark }; - - protected static final int[] PRIVATE_PRESSED_STATE_SET = { R.attr.state_private, android.R.attr.state_pressed }; - protected static final int[] PRIVATE_FOCUSED_STATE_SET = { R.attr.state_private, android.R.attr.state_focused }; - protected static final int[] PRIVATE_STATE_SET = { R.attr.state_private }; - - private boolean isPrivate; - private boolean isLight; - private boolean isDark; - private boolean autoUpdateTheme; // always false if there's no theme. - - private ColorStateList drawableColors; - - public ThemedImageView(Context context, AttributeSet attrs) { - super(context, attrs); - initialize(context, attrs, 0); - } - - public ThemedImageView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - initialize(context, attrs, defStyle); - } - - private void initialize(final Context context, final AttributeSet attrs, final int defStyle) { - // The theme can be null, particularly if we might be instantiating this - // View in an IDE, with no ambient GeckoApplication. - final Context applicationContext = context.getApplicationContext(); - if (applicationContext instanceof GeckoApplication) { - theme = ((GeckoApplication) applicationContext).getLightweightTheme(); - } - - final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.LightweightTheme); - autoUpdateTheme = theme != null && a.getBoolean(R.styleable.LightweightTheme_autoUpdateTheme, true); - a.recycle(); - - final TypedArray themedA = context.obtainStyledAttributes(attrs, R.styleable.ThemedView, defStyle, 0); - drawableColors = themedA.getColorStateList(R.styleable.ThemedView_drawableTintList); - themedA.recycle(); - - // Apply the tint initially - the Drawable is - // initially set by XML via super's constructor. - setTintedImageDrawable(getDrawable()); - } - - @Override - public void onAttachedToWindow() { - super.onAttachedToWindow(); - - if (autoUpdateTheme) - theme.addListener(this); - } - - @Override - public void onDetachedFromWindow() { - super.onDetachedFromWindow(); - - if (autoUpdateTheme) - theme.removeListener(this); - } - - @Override - public int[] onCreateDrawableState(int extraSpace) { - final int[] drawableState = super.onCreateDrawableState(extraSpace + 1); - - if (isPrivate) - mergeDrawableStates(drawableState, STATE_PRIVATE_MODE); - else if (isLight) - mergeDrawableStates(drawableState, STATE_LIGHT); - else if (isDark) - mergeDrawableStates(drawableState, STATE_DARK); - - return drawableState; - } - - @Override - public void onLightweightThemeChanged() { - if (autoUpdateTheme && theme.isEnabled()) - setTheme(theme.isLightTheme()); - } - - @Override - public void onLightweightThemeReset() { - if (autoUpdateTheme) - resetTheme(); - } - - @Override - protected void onLayout(boolean changed, int left, int top, int right, int bottom) { - super.onLayout(changed, left, top, right, bottom); - onLightweightThemeChanged(); - } - - public boolean isPrivateMode() { - return isPrivate; - } - - public void setPrivateMode(boolean isPrivate) { - if (this.isPrivate != isPrivate) { - this.isPrivate = isPrivate; - refreshDrawableState(); - invalidate(); - } - } - - public void setTheme(boolean isLight) { - // Set the theme only if it is different from existing theme. - if ((isLight && this.isLight != isLight) || - (!isLight && this.isDark == isLight)) { - if (isLight) { - this.isLight = true; - this.isDark = false; - } else { - this.isLight = false; - this.isDark = true; - } - - refreshDrawableState(); - invalidate(); - } - } - - public void resetTheme() { - if (isLight || isDark) { - isLight = false; - isDark = false; - refreshDrawableState(); - invalidate(); - } - } - - public void setAutoUpdateTheme(boolean autoUpdateTheme) { - if (theme == null) { - return; - } - - if (this.autoUpdateTheme != autoUpdateTheme) { - this.autoUpdateTheme = autoUpdateTheme; - - if (autoUpdateTheme) - theme.addListener(this); - else - theme.removeListener(this); - } - } - - @Override - public void setImageDrawable(final Drawable drawable) { - setTintedImageDrawable(drawable); - } - - private void setTintedImageDrawable(final Drawable drawable) { - final Drawable tintedDrawable; - if (drawableColors == null) { - // NB: If we tint a drawable with a null ColorStateList, it will override - // any existing colorFilters and tint... so don't! - tintedDrawable = drawable; - } else if (drawable == null) { - tintedDrawable = null; - } else { - tintedDrawable = DrawableUtil.tintDrawableWithStateList(drawable, drawableColors); - } - super.setImageDrawable(tintedDrawable); - } - - public ColorDrawable getColorDrawable(int id) { - return new ColorDrawable(ContextCompat.getColor(getContext(), id)); - } - - protected LightweightTheme getTheme() { - return theme; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/widget/themed/ThemedLinearLayout.java b/mobile/android/base/java/org/mozilla/gecko/widget/themed/ThemedLinearLayout.java deleted file mode 100644 index 87ec58ce05..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/widget/themed/ThemedLinearLayout.java +++ /dev/null @@ -1,167 +0,0 @@ -// This file is generated by generate_themed_views.py; do not edit. - -/* 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.widget.themed; - -import android.support.v4.content.ContextCompat; -import org.mozilla.gecko.GeckoApplication; -import org.mozilla.gecko.lwt.LightweightTheme; -import org.mozilla.gecko.R; -import org.mozilla.gecko.util.DrawableUtil; - -import android.content.Context; -import android.content.res.ColorStateList; -import android.content.res.TypedArray; -import android.graphics.drawable.ColorDrawable; -import android.graphics.drawable.Drawable; -import android.util.AttributeSet; - -public class ThemedLinearLayout extends android.widget.LinearLayout - implements LightweightTheme.OnChangeListener { - private LightweightTheme theme; - - private static final int[] STATE_PRIVATE_MODE = { R.attr.state_private }; - private static final int[] STATE_LIGHT = { R.attr.state_light }; - private static final int[] STATE_DARK = { R.attr.state_dark }; - - protected static final int[] PRIVATE_PRESSED_STATE_SET = { R.attr.state_private, android.R.attr.state_pressed }; - protected static final int[] PRIVATE_FOCUSED_STATE_SET = { R.attr.state_private, android.R.attr.state_focused }; - protected static final int[] PRIVATE_STATE_SET = { R.attr.state_private }; - - private boolean isPrivate; - private boolean isLight; - private boolean isDark; - private boolean autoUpdateTheme; // always false if there's no theme. - - private ColorStateList drawableColors; - - public ThemedLinearLayout(Context context, AttributeSet attrs) { - super(context, attrs); - initialize(context, attrs, 0); - } - - private void initialize(final Context context, final AttributeSet attrs, final int defStyle) { - // The theme can be null, particularly if we might be instantiating this - // View in an IDE, with no ambient GeckoApplication. - final Context applicationContext = context.getApplicationContext(); - if (applicationContext instanceof GeckoApplication) { - theme = ((GeckoApplication) applicationContext).getLightweightTheme(); - } - - final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.LightweightTheme); - autoUpdateTheme = theme != null && a.getBoolean(R.styleable.LightweightTheme_autoUpdateTheme, true); - a.recycle(); - } - - @Override - public void onAttachedToWindow() { - super.onAttachedToWindow(); - - if (autoUpdateTheme) - theme.addListener(this); - } - - @Override - public void onDetachedFromWindow() { - super.onDetachedFromWindow(); - - if (autoUpdateTheme) - theme.removeListener(this); - } - - @Override - public int[] onCreateDrawableState(int extraSpace) { - final int[] drawableState = super.onCreateDrawableState(extraSpace + 1); - - if (isPrivate) - mergeDrawableStates(drawableState, STATE_PRIVATE_MODE); - else if (isLight) - mergeDrawableStates(drawableState, STATE_LIGHT); - else if (isDark) - mergeDrawableStates(drawableState, STATE_DARK); - - return drawableState; - } - - @Override - public void onLightweightThemeChanged() { - if (autoUpdateTheme && theme.isEnabled()) - setTheme(theme.isLightTheme()); - } - - @Override - public void onLightweightThemeReset() { - if (autoUpdateTheme) - resetTheme(); - } - - @Override - protected void onLayout(boolean changed, int left, int top, int right, int bottom) { - super.onLayout(changed, left, top, right, bottom); - onLightweightThemeChanged(); - } - - public boolean isPrivateMode() { - return isPrivate; - } - - public void setPrivateMode(boolean isPrivate) { - if (this.isPrivate != isPrivate) { - this.isPrivate = isPrivate; - refreshDrawableState(); - invalidate(); - } - } - - public void setTheme(boolean isLight) { - // Set the theme only if it is different from existing theme. - if ((isLight && this.isLight != isLight) || - (!isLight && this.isDark == isLight)) { - if (isLight) { - this.isLight = true; - this.isDark = false; - } else { - this.isLight = false; - this.isDark = true; - } - - refreshDrawableState(); - invalidate(); - } - } - - public void resetTheme() { - if (isLight || isDark) { - isLight = false; - isDark = false; - refreshDrawableState(); - invalidate(); - } - } - - public void setAutoUpdateTheme(boolean autoUpdateTheme) { - if (theme == null) { - return; - } - - if (this.autoUpdateTheme != autoUpdateTheme) { - this.autoUpdateTheme = autoUpdateTheme; - - if (autoUpdateTheme) - theme.addListener(this); - else - theme.removeListener(this); - } - } - - public ColorDrawable getColorDrawable(int id) { - return new ColorDrawable(ContextCompat.getColor(getContext(), id)); - } - - protected LightweightTheme getTheme() { - return theme; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/widget/themed/ThemedRelativeLayout.java b/mobile/android/base/java/org/mozilla/gecko/widget/themed/ThemedRelativeLayout.java deleted file mode 100644 index 14ef25c622..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/widget/themed/ThemedRelativeLayout.java +++ /dev/null @@ -1,172 +0,0 @@ -// This file is generated by generate_themed_views.py; do not edit. - -/* 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.widget.themed; - -import android.support.v4.content.ContextCompat; -import org.mozilla.gecko.GeckoApplication; -import org.mozilla.gecko.lwt.LightweightTheme; -import org.mozilla.gecko.R; -import org.mozilla.gecko.util.DrawableUtil; - -import android.content.Context; -import android.content.res.ColorStateList; -import android.content.res.TypedArray; -import android.graphics.drawable.ColorDrawable; -import android.graphics.drawable.Drawable; -import android.util.AttributeSet; - -public class ThemedRelativeLayout extends android.widget.RelativeLayout - implements LightweightTheme.OnChangeListener { - private LightweightTheme theme; - - private static final int[] STATE_PRIVATE_MODE = { R.attr.state_private }; - private static final int[] STATE_LIGHT = { R.attr.state_light }; - private static final int[] STATE_DARK = { R.attr.state_dark }; - - protected static final int[] PRIVATE_PRESSED_STATE_SET = { R.attr.state_private, android.R.attr.state_pressed }; - protected static final int[] PRIVATE_FOCUSED_STATE_SET = { R.attr.state_private, android.R.attr.state_focused }; - protected static final int[] PRIVATE_STATE_SET = { R.attr.state_private }; - - private boolean isPrivate; - private boolean isLight; - private boolean isDark; - private boolean autoUpdateTheme; // always false if there's no theme. - - private ColorStateList drawableColors; - - public ThemedRelativeLayout(Context context, AttributeSet attrs) { - super(context, attrs); - initialize(context, attrs, 0); - } - - public ThemedRelativeLayout(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - initialize(context, attrs, defStyle); - } - - private void initialize(final Context context, final AttributeSet attrs, final int defStyle) { - // The theme can be null, particularly if we might be instantiating this - // View in an IDE, with no ambient GeckoApplication. - final Context applicationContext = context.getApplicationContext(); - if (applicationContext instanceof GeckoApplication) { - theme = ((GeckoApplication) applicationContext).getLightweightTheme(); - } - - final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.LightweightTheme); - autoUpdateTheme = theme != null && a.getBoolean(R.styleable.LightweightTheme_autoUpdateTheme, true); - a.recycle(); - } - - @Override - public void onAttachedToWindow() { - super.onAttachedToWindow(); - - if (autoUpdateTheme) - theme.addListener(this); - } - - @Override - public void onDetachedFromWindow() { - super.onDetachedFromWindow(); - - if (autoUpdateTheme) - theme.removeListener(this); - } - - @Override - public int[] onCreateDrawableState(int extraSpace) { - final int[] drawableState = super.onCreateDrawableState(extraSpace + 1); - - if (isPrivate) - mergeDrawableStates(drawableState, STATE_PRIVATE_MODE); - else if (isLight) - mergeDrawableStates(drawableState, STATE_LIGHT); - else if (isDark) - mergeDrawableStates(drawableState, STATE_DARK); - - return drawableState; - } - - @Override - public void onLightweightThemeChanged() { - if (autoUpdateTheme && theme.isEnabled()) - setTheme(theme.isLightTheme()); - } - - @Override - public void onLightweightThemeReset() { - if (autoUpdateTheme) - resetTheme(); - } - - @Override - protected void onLayout(boolean changed, int left, int top, int right, int bottom) { - super.onLayout(changed, left, top, right, bottom); - onLightweightThemeChanged(); - } - - public boolean isPrivateMode() { - return isPrivate; - } - - public void setPrivateMode(boolean isPrivate) { - if (this.isPrivate != isPrivate) { - this.isPrivate = isPrivate; - refreshDrawableState(); - invalidate(); - } - } - - public void setTheme(boolean isLight) { - // Set the theme only if it is different from existing theme. - if ((isLight && this.isLight != isLight) || - (!isLight && this.isDark == isLight)) { - if (isLight) { - this.isLight = true; - this.isDark = false; - } else { - this.isLight = false; - this.isDark = true; - } - - refreshDrawableState(); - invalidate(); - } - } - - public void resetTheme() { - if (isLight || isDark) { - isLight = false; - isDark = false; - refreshDrawableState(); - invalidate(); - } - } - - public void setAutoUpdateTheme(boolean autoUpdateTheme) { - if (theme == null) { - return; - } - - if (this.autoUpdateTheme != autoUpdateTheme) { - this.autoUpdateTheme = autoUpdateTheme; - - if (autoUpdateTheme) - theme.addListener(this); - else - theme.removeListener(this); - } - } - - public ColorDrawable getColorDrawable(int id) { - return new ColorDrawable(ContextCompat.getColor(getContext(), id)); - } - - protected LightweightTheme getTheme() { - return theme; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/widget/themed/ThemedTextSwitcher.java b/mobile/android/base/java/org/mozilla/gecko/widget/themed/ThemedTextSwitcher.java deleted file mode 100644 index 294abd9baf..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/widget/themed/ThemedTextSwitcher.java +++ /dev/null @@ -1,167 +0,0 @@ -// This file is generated by generate_themed_views.py; do not edit. - -/* 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.widget.themed; - -import android.support.v4.content.ContextCompat; -import org.mozilla.gecko.GeckoApplication; -import org.mozilla.gecko.lwt.LightweightTheme; -import org.mozilla.gecko.R; -import org.mozilla.gecko.util.DrawableUtil; - -import android.content.Context; -import android.content.res.ColorStateList; -import android.content.res.TypedArray; -import android.graphics.drawable.ColorDrawable; -import android.graphics.drawable.Drawable; -import android.util.AttributeSet; - -public class ThemedTextSwitcher extends android.widget.TextSwitcher - implements LightweightTheme.OnChangeListener { - private LightweightTheme theme; - - private static final int[] STATE_PRIVATE_MODE = { R.attr.state_private }; - private static final int[] STATE_LIGHT = { R.attr.state_light }; - private static final int[] STATE_DARK = { R.attr.state_dark }; - - protected static final int[] PRIVATE_PRESSED_STATE_SET = { R.attr.state_private, android.R.attr.state_pressed }; - protected static final int[] PRIVATE_FOCUSED_STATE_SET = { R.attr.state_private, android.R.attr.state_focused }; - protected static final int[] PRIVATE_STATE_SET = { R.attr.state_private }; - - private boolean isPrivate; - private boolean isLight; - private boolean isDark; - private boolean autoUpdateTheme; // always false if there's no theme. - - private ColorStateList drawableColors; - - public ThemedTextSwitcher(Context context, AttributeSet attrs) { - super(context, attrs); - initialize(context, attrs, 0); - } - - private void initialize(final Context context, final AttributeSet attrs, final int defStyle) { - // The theme can be null, particularly if we might be instantiating this - // View in an IDE, with no ambient GeckoApplication. - final Context applicationContext = context.getApplicationContext(); - if (applicationContext instanceof GeckoApplication) { - theme = ((GeckoApplication) applicationContext).getLightweightTheme(); - } - - final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.LightweightTheme); - autoUpdateTheme = theme != null && a.getBoolean(R.styleable.LightweightTheme_autoUpdateTheme, true); - a.recycle(); - } - - @Override - public void onAttachedToWindow() { - super.onAttachedToWindow(); - - if (autoUpdateTheme) - theme.addListener(this); - } - - @Override - public void onDetachedFromWindow() { - super.onDetachedFromWindow(); - - if (autoUpdateTheme) - theme.removeListener(this); - } - - @Override - public int[] onCreateDrawableState(int extraSpace) { - final int[] drawableState = super.onCreateDrawableState(extraSpace + 1); - - if (isPrivate) - mergeDrawableStates(drawableState, STATE_PRIVATE_MODE); - else if (isLight) - mergeDrawableStates(drawableState, STATE_LIGHT); - else if (isDark) - mergeDrawableStates(drawableState, STATE_DARK); - - return drawableState; - } - - @Override - public void onLightweightThemeChanged() { - if (autoUpdateTheme && theme.isEnabled()) - setTheme(theme.isLightTheme()); - } - - @Override - public void onLightweightThemeReset() { - if (autoUpdateTheme) - resetTheme(); - } - - @Override - protected void onLayout(boolean changed, int left, int top, int right, int bottom) { - super.onLayout(changed, left, top, right, bottom); - onLightweightThemeChanged(); - } - - public boolean isPrivateMode() { - return isPrivate; - } - - public void setPrivateMode(boolean isPrivate) { - if (this.isPrivate != isPrivate) { - this.isPrivate = isPrivate; - refreshDrawableState(); - invalidate(); - } - } - - public void setTheme(boolean isLight) { - // Set the theme only if it is different from existing theme. - if ((isLight && this.isLight != isLight) || - (!isLight && this.isDark == isLight)) { - if (isLight) { - this.isLight = true; - this.isDark = false; - } else { - this.isLight = false; - this.isDark = true; - } - - refreshDrawableState(); - invalidate(); - } - } - - public void resetTheme() { - if (isLight || isDark) { - isLight = false; - isDark = false; - refreshDrawableState(); - invalidate(); - } - } - - public void setAutoUpdateTheme(boolean autoUpdateTheme) { - if (theme == null) { - return; - } - - if (this.autoUpdateTheme != autoUpdateTheme) { - this.autoUpdateTheme = autoUpdateTheme; - - if (autoUpdateTheme) - theme.addListener(this); - else - theme.removeListener(this); - } - } - - public ColorDrawable getColorDrawable(int id) { - return new ColorDrawable(ContextCompat.getColor(getContext(), id)); - } - - protected LightweightTheme getTheme() { - return theme; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/widget/themed/ThemedTextView.java b/mobile/android/base/java/org/mozilla/gecko/widget/themed/ThemedTextView.java deleted file mode 100644 index 51a23a406c..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/widget/themed/ThemedTextView.java +++ /dev/null @@ -1,172 +0,0 @@ -// This file is generated by generate_themed_views.py; do not edit. - -/* 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.widget.themed; - -import android.support.v4.content.ContextCompat; -import org.mozilla.gecko.GeckoApplication; -import org.mozilla.gecko.lwt.LightweightTheme; -import org.mozilla.gecko.R; -import org.mozilla.gecko.util.DrawableUtil; - -import android.content.Context; -import android.content.res.ColorStateList; -import android.content.res.TypedArray; -import android.graphics.drawable.ColorDrawable; -import android.graphics.drawable.Drawable; -import android.util.AttributeSet; - -public class ThemedTextView extends android.widget.TextView - implements LightweightTheme.OnChangeListener { - private LightweightTheme theme; - - private static final int[] STATE_PRIVATE_MODE = { R.attr.state_private }; - private static final int[] STATE_LIGHT = { R.attr.state_light }; - private static final int[] STATE_DARK = { R.attr.state_dark }; - - protected static final int[] PRIVATE_PRESSED_STATE_SET = { R.attr.state_private, android.R.attr.state_pressed }; - protected static final int[] PRIVATE_FOCUSED_STATE_SET = { R.attr.state_private, android.R.attr.state_focused }; - protected static final int[] PRIVATE_STATE_SET = { R.attr.state_private }; - - private boolean isPrivate; - private boolean isLight; - private boolean isDark; - private boolean autoUpdateTheme; // always false if there's no theme. - - private ColorStateList drawableColors; - - public ThemedTextView(Context context, AttributeSet attrs) { - super(context, attrs); - initialize(context, attrs, 0); - } - - public ThemedTextView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - initialize(context, attrs, defStyle); - } - - private void initialize(final Context context, final AttributeSet attrs, final int defStyle) { - // The theme can be null, particularly if we might be instantiating this - // View in an IDE, with no ambient GeckoApplication. - final Context applicationContext = context.getApplicationContext(); - if (applicationContext instanceof GeckoApplication) { - theme = ((GeckoApplication) applicationContext).getLightweightTheme(); - } - - final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.LightweightTheme); - autoUpdateTheme = theme != null && a.getBoolean(R.styleable.LightweightTheme_autoUpdateTheme, true); - a.recycle(); - } - - @Override - public void onAttachedToWindow() { - super.onAttachedToWindow(); - - if (autoUpdateTheme) - theme.addListener(this); - } - - @Override - public void onDetachedFromWindow() { - super.onDetachedFromWindow(); - - if (autoUpdateTheme) - theme.removeListener(this); - } - - @Override - public int[] onCreateDrawableState(int extraSpace) { - final int[] drawableState = super.onCreateDrawableState(extraSpace + 1); - - if (isPrivate) - mergeDrawableStates(drawableState, STATE_PRIVATE_MODE); - else if (isLight) - mergeDrawableStates(drawableState, STATE_LIGHT); - else if (isDark) - mergeDrawableStates(drawableState, STATE_DARK); - - return drawableState; - } - - @Override - public void onLightweightThemeChanged() { - if (autoUpdateTheme && theme.isEnabled()) - setTheme(theme.isLightTheme()); - } - - @Override - public void onLightweightThemeReset() { - if (autoUpdateTheme) - resetTheme(); - } - - @Override - protected void onLayout(boolean changed, int left, int top, int right, int bottom) { - super.onLayout(changed, left, top, right, bottom); - onLightweightThemeChanged(); - } - - public boolean isPrivateMode() { - return isPrivate; - } - - public void setPrivateMode(boolean isPrivate) { - if (this.isPrivate != isPrivate) { - this.isPrivate = isPrivate; - refreshDrawableState(); - invalidate(); - } - } - - public void setTheme(boolean isLight) { - // Set the theme only if it is different from existing theme. - if ((isLight && this.isLight != isLight) || - (!isLight && this.isDark == isLight)) { - if (isLight) { - this.isLight = true; - this.isDark = false; - } else { - this.isLight = false; - this.isDark = true; - } - - refreshDrawableState(); - invalidate(); - } - } - - public void resetTheme() { - if (isLight || isDark) { - isLight = false; - isDark = false; - refreshDrawableState(); - invalidate(); - } - } - - public void setAutoUpdateTheme(boolean autoUpdateTheme) { - if (theme == null) { - return; - } - - if (this.autoUpdateTheme != autoUpdateTheme) { - this.autoUpdateTheme = autoUpdateTheme; - - if (autoUpdateTheme) - theme.addListener(this); - else - theme.removeListener(this); - } - } - - public ColorDrawable getColorDrawable(int id) { - return new ColorDrawable(ContextCompat.getColor(getContext(), id)); - } - - protected LightweightTheme getTheme() { - return theme; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/widget/themed/ThemedView.java b/mobile/android/base/java/org/mozilla/gecko/widget/themed/ThemedView.java deleted file mode 100644 index 77ecfd271c..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/widget/themed/ThemedView.java +++ /dev/null @@ -1,172 +0,0 @@ -// This file is generated by generate_themed_views.py; do not edit. - -/* 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.widget.themed; - -import android.support.v4.content.ContextCompat; -import org.mozilla.gecko.GeckoApplication; -import org.mozilla.gecko.lwt.LightweightTheme; -import org.mozilla.gecko.R; -import org.mozilla.gecko.util.DrawableUtil; - -import android.content.Context; -import android.content.res.ColorStateList; -import android.content.res.TypedArray; -import android.graphics.drawable.ColorDrawable; -import android.graphics.drawable.Drawable; -import android.util.AttributeSet; - -public class ThemedView extends android.view.View - implements LightweightTheme.OnChangeListener { - private LightweightTheme theme; - - private static final int[] STATE_PRIVATE_MODE = { R.attr.state_private }; - private static final int[] STATE_LIGHT = { R.attr.state_light }; - private static final int[] STATE_DARK = { R.attr.state_dark }; - - protected static final int[] PRIVATE_PRESSED_STATE_SET = { R.attr.state_private, android.R.attr.state_pressed }; - protected static final int[] PRIVATE_FOCUSED_STATE_SET = { R.attr.state_private, android.R.attr.state_focused }; - protected static final int[] PRIVATE_STATE_SET = { R.attr.state_private }; - - private boolean isPrivate; - private boolean isLight; - private boolean isDark; - private boolean autoUpdateTheme; // always false if there's no theme. - - private ColorStateList drawableColors; - - public ThemedView(Context context, AttributeSet attrs) { - super(context, attrs); - initialize(context, attrs, 0); - } - - public ThemedView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - initialize(context, attrs, defStyle); - } - - private void initialize(final Context context, final AttributeSet attrs, final int defStyle) { - // The theme can be null, particularly if we might be instantiating this - // View in an IDE, with no ambient GeckoApplication. - final Context applicationContext = context.getApplicationContext(); - if (applicationContext instanceof GeckoApplication) { - theme = ((GeckoApplication) applicationContext).getLightweightTheme(); - } - - final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.LightweightTheme); - autoUpdateTheme = theme != null && a.getBoolean(R.styleable.LightweightTheme_autoUpdateTheme, true); - a.recycle(); - } - - @Override - public void onAttachedToWindow() { - super.onAttachedToWindow(); - - if (autoUpdateTheme) - theme.addListener(this); - } - - @Override - public void onDetachedFromWindow() { - super.onDetachedFromWindow(); - - if (autoUpdateTheme) - theme.removeListener(this); - } - - @Override - public int[] onCreateDrawableState(int extraSpace) { - final int[] drawableState = super.onCreateDrawableState(extraSpace + 1); - - if (isPrivate) - mergeDrawableStates(drawableState, STATE_PRIVATE_MODE); - else if (isLight) - mergeDrawableStates(drawableState, STATE_LIGHT); - else if (isDark) - mergeDrawableStates(drawableState, STATE_DARK); - - return drawableState; - } - - @Override - public void onLightweightThemeChanged() { - if (autoUpdateTheme && theme.isEnabled()) - setTheme(theme.isLightTheme()); - } - - @Override - public void onLightweightThemeReset() { - if (autoUpdateTheme) - resetTheme(); - } - - @Override - protected void onLayout(boolean changed, int left, int top, int right, int bottom) { - super.onLayout(changed, left, top, right, bottom); - onLightweightThemeChanged(); - } - - public boolean isPrivateMode() { - return isPrivate; - } - - public void setPrivateMode(boolean isPrivate) { - if (this.isPrivate != isPrivate) { - this.isPrivate = isPrivate; - refreshDrawableState(); - invalidate(); - } - } - - public void setTheme(boolean isLight) { - // Set the theme only if it is different from existing theme. - if ((isLight && this.isLight != isLight) || - (!isLight && this.isDark == isLight)) { - if (isLight) { - this.isLight = true; - this.isDark = false; - } else { - this.isLight = false; - this.isDark = true; - } - - refreshDrawableState(); - invalidate(); - } - } - - public void resetTheme() { - if (isLight || isDark) { - isLight = false; - isDark = false; - refreshDrawableState(); - invalidate(); - } - } - - public void setAutoUpdateTheme(boolean autoUpdateTheme) { - if (theme == null) { - return; - } - - if (this.autoUpdateTheme != autoUpdateTheme) { - this.autoUpdateTheme = autoUpdateTheme; - - if (autoUpdateTheme) - theme.addListener(this); - else - theme.removeListener(this); - } - } - - public ColorDrawable getColorDrawable(int id) { - return new ColorDrawable(ContextCompat.getColor(getContext(), id)); - } - - protected LightweightTheme getTheme() { - return theme; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/widget/themed/ThemedView.java.frag b/mobile/android/base/java/org/mozilla/gecko/widget/themed/ThemedView.java.frag deleted file mode 100644 index e731a0ebe4..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/widget/themed/ThemedView.java.frag +++ /dev/null @@ -1,211 +0,0 @@ -//#filter substitution -// This file is generated by generate_themed_views.py; do not edit. - -/* 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.widget.themed; - -import android.support.v4.content.ContextCompat; -import org.mozilla.gecko.GeckoApplication; -import org.mozilla.gecko.lwt.LightweightTheme; -import org.mozilla.gecko.R; -import org.mozilla.gecko.util.DrawableUtil; - -import android.content.Context; -import android.content.res.ColorStateList; -import android.content.res.TypedArray; -import android.graphics.drawable.ColorDrawable; -import android.graphics.drawable.Drawable; -import android.util.AttributeSet; - -public class Themed@VIEW_NAME_SUFFIX@ extends @BASE_TYPE@ - implements LightweightTheme.OnChangeListener { - private LightweightTheme theme; - - private static final int[] STATE_PRIVATE_MODE = { R.attr.state_private }; - private static final int[] STATE_LIGHT = { R.attr.state_light }; - private static final int[] STATE_DARK = { R.attr.state_dark }; - - protected static final int[] PRIVATE_PRESSED_STATE_SET = { R.attr.state_private, android.R.attr.state_pressed }; - protected static final int[] PRIVATE_FOCUSED_STATE_SET = { R.attr.state_private, android.R.attr.state_focused }; - protected static final int[] PRIVATE_STATE_SET = { R.attr.state_private }; - - private boolean isPrivate; - private boolean isLight; - private boolean isDark; - private boolean autoUpdateTheme; // always false if there's no theme. - - private ColorStateList drawableColors; - - public Themed@VIEW_NAME_SUFFIX@(Context context, AttributeSet attrs) { - super(context, attrs); - initialize(context, attrs, 0); - } - -//#ifdef STYLE_CONSTRUCTOR - public Themed@VIEW_NAME_SUFFIX@(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - initialize(context, attrs, defStyle); - } - -//#endif - private void initialize(final Context context, final AttributeSet attrs, final int defStyle) { - // The theme can be null, particularly if we might be instantiating this - // View in an IDE, with no ambient GeckoApplication. - final Context applicationContext = context.getApplicationContext(); - if (applicationContext instanceof GeckoApplication) { - theme = ((GeckoApplication) applicationContext).getLightweightTheme(); - } - - final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.LightweightTheme); - autoUpdateTheme = theme != null && a.getBoolean(R.styleable.LightweightTheme_autoUpdateTheme, true); - a.recycle(); -//#if TINT_FOREGROUND_DRAWABLE - - final TypedArray themedA = context.obtainStyledAttributes(attrs, R.styleable.ThemedView, defStyle, 0); - drawableColors = themedA.getColorStateList(R.styleable.ThemedView_drawableTintList); - themedA.recycle(); - - // Apply the tint initially - the Drawable is - // initially set by XML via super's constructor. - setTintedImageDrawable(getDrawable()); -//#endif - } - - @Override - public void onAttachedToWindow() { - super.onAttachedToWindow(); - - if (autoUpdateTheme) - theme.addListener(this); - } - - @Override - public void onDetachedFromWindow() { - super.onDetachedFromWindow(); - - if (autoUpdateTheme) - theme.removeListener(this); - } - - @Override - public int[] onCreateDrawableState(int extraSpace) { - final int[] drawableState = super.onCreateDrawableState(extraSpace + 1); - - if (isPrivate) - mergeDrawableStates(drawableState, STATE_PRIVATE_MODE); - else if (isLight) - mergeDrawableStates(drawableState, STATE_LIGHT); - else if (isDark) - mergeDrawableStates(drawableState, STATE_DARK); - - return drawableState; - } - - @Override - public void onLightweightThemeChanged() { - if (autoUpdateTheme && theme.isEnabled()) - setTheme(theme.isLightTheme()); - } - - @Override - public void onLightweightThemeReset() { - if (autoUpdateTheme) - resetTheme(); - } - - @Override - protected void onLayout(boolean changed, int left, int top, int right, int bottom) { - super.onLayout(changed, left, top, right, bottom); - onLightweightThemeChanged(); - } - - public boolean isPrivateMode() { - return isPrivate; - } - - public void setPrivateMode(boolean isPrivate) { - if (this.isPrivate != isPrivate) { - this.isPrivate = isPrivate; - refreshDrawableState(); - invalidate(); - } - } - - public void setTheme(boolean isLight) { - // Set the theme only if it is different from existing theme. - if ((isLight && this.isLight != isLight) || - (!isLight && this.isDark == isLight)) { - if (isLight) { - this.isLight = true; - this.isDark = false; - } else { - this.isLight = false; - this.isDark = true; - } - - refreshDrawableState(); - invalidate(); - } - } - - public void resetTheme() { - if (isLight || isDark) { - isLight = false; - isDark = false; - refreshDrawableState(); - invalidate(); - } - } - - public void setAutoUpdateTheme(boolean autoUpdateTheme) { - if (theme == null) { - return; - } - - if (this.autoUpdateTheme != autoUpdateTheme) { - this.autoUpdateTheme = autoUpdateTheme; - - if (autoUpdateTheme) - theme.addListener(this); - else - theme.removeListener(this); - } - } - -//#ifdef TINT_FOREGROUND_DRAWABLE - @Override - public void setImageDrawable(final Drawable drawable) { - setTintedImageDrawable(drawable); - } - - private void setTintedImageDrawable(final Drawable drawable) { - final Drawable tintedDrawable; -//#ifdef BOOKMARK_NO_TINT - if (drawableColors == null || R.id.bookmark == getId()) { - // NB: The bookmarked state uses a blue star, so this is a hack to keep it untinted. -//#else - if (drawableColors == null) { -//#endif - // NB: If we tint a drawable with a null ColorStateList, it will override - // any existing colorFilters and tint... so don't! - tintedDrawable = drawable; - } else if (drawable == null) { - tintedDrawable = null; - } else { - tintedDrawable = DrawableUtil.tintDrawableWithStateList(drawable, drawableColors); - } - super.setImageDrawable(tintedDrawable); - } - -//#endif - public ColorDrawable getColorDrawable(int id) { - return new ColorDrawable(ContextCompat.getColor(getContext(), id)); - } - - protected LightweightTheme getTheme() { - return theme; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/widget/themed/generate_themed_views.py b/mobile/android/base/java/org/mozilla/gecko/widget/themed/generate_themed_views.py deleted file mode 100644 index 3b5a00b403..0000000000 --- a/mobile/android/base/java/org/mozilla/gecko/widget/themed/generate_themed_views.py +++ /dev/null @@ -1,72 +0,0 @@ -#!/bin/python - -# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- -# 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/. - -''' -Script to generate Themed*.java source files for Fennec. - -This script runs the preprocessor on a input template and writes -updated files into the source directory. - -To update the themed views, update the input template -(ThemedView.java.frag) and run the script using 'mach python '. Use version control to -examine the differences, and don't forget to commit the changes to the -template and the outputs. -''' - -from __future__ import ( - print_function, - unicode_literals, -) - -import os - -from mozbuild.preprocessor import Preprocessor - -__DIR__ = os.path.dirname(os.path.abspath(__file__)) - -template = os.path.join(__DIR__, 'ThemedView.java.frag') -dest_format_string = 'Themed%(VIEW_NAME_SUFFIX)s.java' - -views = [ - dict(VIEW_NAME_SUFFIX='EditText', - BASE_TYPE='android.widget.EditText', - STYLE_CONSTRUCTOR=1), - dict(VIEW_NAME_SUFFIX='FrameLayout', - BASE_TYPE='android.widget.FrameLayout', - STYLE_CONSTRUCTOR=1), - dict(VIEW_NAME_SUFFIX='ImageButton', - BASE_TYPE='android.widget.ImageButton', - STYLE_CONSTRUCTOR=1, - TINT_FOREGROUND_DRAWABLE=1, - BOOKMARK_NO_TINT=1), - dict(VIEW_NAME_SUFFIX='ImageView', - BASE_TYPE='android.widget.ImageView', - STYLE_CONSTRUCTOR=1, - TINT_FOREGROUND_DRAWABLE=1), - dict(VIEW_NAME_SUFFIX='LinearLayout', - BASE_TYPE='android.widget.LinearLayout'), - dict(VIEW_NAME_SUFFIX='RelativeLayout', - BASE_TYPE='android.widget.RelativeLayout', - STYLE_CONSTRUCTOR=1), - dict(VIEW_NAME_SUFFIX='TextSwitcher', - BASE_TYPE='android.widget.TextSwitcher'), - dict(VIEW_NAME_SUFFIX='TextView', - BASE_TYPE='android.widget.TextView', - STYLE_CONSTRUCTOR=1), - dict(VIEW_NAME_SUFFIX='View', - BASE_TYPE='android.view.View', - STYLE_CONSTRUCTOR=1), -] - -for view in views: - pp = Preprocessor(defines=view, marker='//#') - - dest = os.path.join(__DIR__, dest_format_string % view) - with open(template, 'rU') as input: - with open(dest, 'wt') as output: - pp.processFile(input=input, output=output) - print('%s' % dest) diff --git a/mobile/android/base/locales/Makefile.in b/mobile/android/base/locales/Makefile.in deleted file mode 100644 index ce0636c126..0000000000 --- a/mobile/android/base/locales/Makefile.in +++ /dev/null @@ -1,114 +0,0 @@ -# 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/. - -include $(topsrcdir)/config/config.mk - -# special case some locale codes, he and id -# http://code.google.com/p/android/issues/detail?id=3639 -AB_rCD = $(if $(filter he, $(AB_CD)),iw,$(if $(filter id, $(AB_CD)),in,$(subst -,-r,$(AB_CD)))) - -# The search strings path is always passed to strings.xml.in; the -# decision to include is made based on the feature flag at the -# inclusion site. -SEARCHSTRINGSPATH = $(abspath $(call MERGE_FILE,search_strings.dtd)) - -SYNCSTRINGSPATH = $(abspath $(call MERGE_FILE,sync_strings.dtd)) -STRINGSPATH = $(abspath $(call MERGE_FILE,android_strings.dtd)) -ifeq (,$(XPI_NAME)) -BRANDPATH = $(topobjdir)/dist/bin/chrome/$(AB_CD)/locale/branding/brand.dtd -else -BRANDPATH = $(ABS_DIST)/xpi-stage/$(XPI_NAME)/chrome/$(AB_CD)/locale/branding/brand.dtd -endif -$(warnIfEmpty,AB_CD) # todo: $(errorIfEmpty ) - -dir-res-values := ../res/values -strings-xml := $(dir-res-values)/strings.xml -strings-xml-in := $(srcdir)/../strings.xml.in - -GARBAGE += $(strings-xml) - -dir-res-raw := ../res/raw -suggestedsites := $(dir-res-raw)/suggestedsites.json -browsersearch := $(dir-res-raw)/browsersearch.json - -libs realchrome:: \ - $(strings-xml) \ - $(NULL) - -chrome-%:: AB_CD=$* -chrome-%:: - @$(MAKE) \ - $(dir-res-values)-$(AB_rCD)/strings.xml \ - $(dir-res-raw)-$(AB_rCD)/suggestedsites.json \ - $(dir-res-raw)-$(AB_rCD)/browsersearch.json \ - AB_CD=$* - -# Determine the ../res/values[-*]/ path -strings-xml-bypath = $(filter %/strings.xml,$(MAKECMDGOALS)) -ifeq (,$(strip $(strings-xml-bypath))) - strings-xml-bypath = $(strings-xml) -endif -dir-strings-xml = $(patsubst %/,%,$(dir $(strings-xml-bypath))) - -strings-xml-preqs =\ - $(strings-xml-in) \ - $(BRANDPATH) \ - $(STRINGSPATH) \ - $(SEARCHSTRINGSPATH) \ - $(SYNCSTRINGSPATH) \ - $(if $(IS_LANGUAGE_REPACK),FORCE) \ - $(NULL) - -$(dir-strings-xml)/strings.xml: $(strings-xml-preqs) - $(call py_action,preprocessor, \ - $(DEFINES) \ - $(ACDEFINES) \ - -DANDROID_PACKAGE_NAME=$(ANDROID_PACKAGE_NAME) \ - -DBRANDPATH='$(BRANDPATH)' \ - -DMOZ_APP_DISPLAYNAME='@MOZ_APP_DISPLAYNAME@' \ - -DSTRINGSPATH='$(STRINGSPATH)' \ - -DSYNCSTRINGSPATH='$(SYNCSTRINGSPATH)' \ - -DSEARCHSTRINGSPATH='$(SEARCHSTRINGSPATH)' \ - $< \ - -o $@) - -# Arg 1: Valid Make identifier, like suggestedsites. -# Arg 2: File name, like suggestedsites.json. -define generated_file_template - -# Determine the ../res/raw[-*] path. This can be ../res/raw when no -# locale is explicitly specified. -$(1)-bypath = $(filter %/$(2),$(MAKECMDGOALS)) -ifeq (,$$(strip $$($(1)-bypath))) - $(1)-bypath = $($(1)) -endif -$(1)-dstdir-raw = $$(patsubst %/,%,$$(dir $$($(1)-bypath))) - -GARBAGE += $($(1)) - -libs realchrome:: $($(1)) -endef - -# L10NBASEDIR is not defined for en-US. -l10n-srcdir := $(if $(filter en-US,$(AB_CD)),,$(or $(realpath $(L10NBASEDIR)),$(abspath $(L10NBASEDIR)))/$(AB_CD)/mobile/chrome) - -$(eval $(call generated_file_template,suggestedsites,suggestedsites.json)) - -$(suggestedsites-dstdir-raw)/suggestedsites.json: FORCE - $(call py_action,generate_suggestedsites, \ - --verbose \ - --android-package-name=$(ANDROID_PACKAGE_NAME) \ - --resources=$(srcdir)/../resources \ - $(if $(filter en-US,$(AB_CD)),,--srcdir=$(l10n-srcdir)) \ - --srcdir=$(topsrcdir)/mobile/locales/en-US/chrome \ - $@) - -$(eval $(call generated_file_template,browsersearch,browsersearch.json)) - -$(browsersearch-dstdir-raw)/browsersearch.json: FORCE - $(call py_action,generate_browsersearch, \ - --verbose \ - $(if $(filter en-US,$(AB_CD)),,--srcdir=$(l10n-srcdir)) \ - --srcdir=$(topsrcdir)/mobile/locales/en-US/chrome \ - $@) diff --git a/mobile/android/base/locales/en-US/android_strings.dtd b/mobile/android/base/locales/en-US/android_strings.dtd deleted file mode 100644 index 140e2a2168..0000000000 --- a/mobile/android/base/locales/en-US/android_strings.dtd +++ /dev/null @@ -1,848 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/mobile/android/base/locales/en-US/search_strings.dtd b/mobile/android/base/locales/en-US/search_strings.dtd deleted file mode 100644 index fe8180cff7..0000000000 --- a/mobile/android/base/locales/en-US/search_strings.dtd +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/mobile/android/base/locales/en-US/sync_strings.dtd b/mobile/android/base/locales/en-US/sync_strings.dtd deleted file mode 100644 index 117600cac8..0000000000 --- a/mobile/android/base/locales/en-US/sync_strings.dtd +++ /dev/null @@ -1,126 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/mobile/android/base/locales/moz.build b/mobile/android/base/locales/moz.build deleted file mode 100644 index 079d4d6409..0000000000 --- a/mobile/android/base/locales/moz.build +++ /dev/null @@ -1,8 +0,0 @@ -# -*- Mode: python; 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/. - -if CONFIG['MOZ_ANDROID_SEARCH_ACTIVITY']: - DEFINES['MOZ_ANDROID_SEARCH_ACTIVITY'] = 1 diff --git a/mobile/android/base/moz.build b/mobile/android/base/moz.build deleted file mode 100644 index eac831421f..0000000000 --- a/mobile/android/base/moz.build +++ /dev/null @@ -1,1143 +0,0 @@ -# -*- 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/. - -DIRS += ['locales'] - -CONFIGURE_SUBST_FILES += ['adjust_sdk_app_token'] - -include('android-services.mozbuild') - -geckoview_source_dir = TOPSRCDIR + '/mobile/android/geckoview/src/main/' -geckoview_thirdparty_source_dir = TOPSRCDIR + '/mobile/android/geckoview/src/thirdparty/' -thirdparty_source_dir = TOPSRCDIR + '/mobile/android/thirdparty/' - -constants_jar = add_java_jar('constants') -constants_jar.sources += [geckoview_source_dir + 'java/org/mozilla/gecko/' + x for x in [ - 'annotation/JNITarget.java', - 'annotation/ReflectionTarget.java', - 'annotation/RobocopTarget.java', - 'annotation/WebRTCJNITarget.java', - 'annotation/WrapForJNI.java', - 'SysInfo.java', -]] -constants_jar.sources += ['java/org/mozilla/gecko/' + x for x in [ - 'adjust/AdjustHelperInterface.java', - 'adjust/AttributionHelperListener.java', - 'db/BrowserContract.java', - 'LocaleManager.java', - 'Locales.java', -]] -constants_jar.generated_sources = [ - 'preprocessed/org/mozilla/gecko/AdjustConstants.java', - 'preprocessed/org/mozilla/gecko/AppConstants.java', -] -constants_jar.extra_jars = [ - CONFIG['ANDROID_SUPPORT_ANNOTATIONS_JAR_LIB'], - CONFIG['ANDROID_SUPPORT_V4_AAR_LIB'], - CONFIG['ANDROID_SUPPORT_V4_AAR_INTERNAL_LIB'], - CONFIG['ANDROID_APPCOMPAT_V7_AAR_LIB'], -] - -if CONFIG['MOZ_INSTALL_TRACKING']: - constants_jar.sources += ['java/org/mozilla/gecko/' + x for x in [ - 'adjust/AdjustHelper.java', - ]] - constants_jar.extra_jars += [ - 'gecko-thirdparty-adjust_sdk.jar', - ] -else: - constants_jar.sources += ['java/org/mozilla/gecko/' + x for x in [ - 'adjust/StubAdjustHelper.java', - ]] - -resjar = add_java_jar('gecko-R') -resjar.sources = [] -resjar.generated_sources += [ - 'org/mozilla/gecko/R.java', -] - -if CONFIG['ANDROID_SUPPORT_V4_AAR']: - ANDROID_EXTRA_PACKAGES += ['android.support.v4'] - ANDROID_EXTRA_RES_DIRS += ['%' + CONFIG['ANDROID_SUPPORT_V4_AAR_RES']] -# (no resources) resjar.generated_sources += ['android/support/v4/R.java'] -if CONFIG['ANDROID_APPCOMPAT_V7_AAR']: - ANDROID_EXTRA_PACKAGES += ['android.support.v7.appcompat'] - ANDROID_EXTRA_RES_DIRS += ['%' + CONFIG['ANDROID_APPCOMPAT_V7_AAR_RES']] - resjar.generated_sources += ['android/support/v7/appcompat/R.java'] -if CONFIG['ANDROID_SUPPORT_VECTOR_DRAWABLE_AAR']: - ANDROID_EXTRA_PACKAGES += ['android.support.graphics.drawable'] - ANDROID_EXTRA_RES_DIRS += ['%' + CONFIG['ANDROID_SUPPORT_VECTOR_DRAWABLE_AAR_RES']] -# (no reosurces) resjar.generated_sources += ['android/support/graphics/drawable/R.java'] -if CONFIG['ANDROID_ANIMATED_VECTOR_DRAWABLE_AAR']: - ANDROID_EXTRA_PACKAGES += ['android.support.graphics.drawable.animated'] - ANDROID_EXTRA_RES_DIRS += ['%' + CONFIG['ANDROID_ANIMATED_VECTOR_DRAWABLE_AAR_RES']] -# (no resources) resjar.generated_sources += ['android/support/graphics/drawable/animated/R.java'] -if CONFIG['ANDROID_CARDVIEW_V7_AAR']: - ANDROID_EXTRA_PACKAGES += ['android.support.v7.cardview'] - ANDROID_EXTRA_RES_DIRS += ['%' + CONFIG['ANDROID_CARDVIEW_V7_AAR_RES']] - resjar.generated_sources += ['android/support/v7/cardview/R.java'] -if CONFIG['ANDROID_DESIGN_AAR']: - ANDROID_EXTRA_PACKAGES += ['android.support.design'] - ANDROID_EXTRA_RES_DIRS += ['%' + CONFIG['ANDROID_DESIGN_AAR_RES']] - resjar.generated_sources += ['android/support/design/R.java'] -if CONFIG['ANDROID_RECYCLERVIEW_V7_AAR']: - ANDROID_EXTRA_PACKAGES += ['android.support.v7.recyclerview'] - ANDROID_EXTRA_RES_DIRS += ['%' + CONFIG['ANDROID_RECYCLERVIEW_V7_AAR_RES']] - resjar.generated_sources += ['android/support/v7/recyclerview/R.java'] -if CONFIG['ANDROID_CUSTOMTABS_AAR']: - ANDROID_EXTRA_PACKAGES += ['android.support.customtabs'] - ANDROID_EXTRA_RES_DIRS += ['%' + CONFIG['ANDROID_CUSTOMTABS_AAR_RES']] -# (no resources) resjar.generated_sources += ['android/support/customtabs/R.java'] -if CONFIG['ANDROID_PALETTE_V7_AAR']: - ANDROID_EXTRA_PACKAGES += ['android.support.v7.palette'] - ANDROID_EXTRA_RES_DIRS += ['%' + CONFIG['ANDROID_PALETTE_V7_AAR_RES']] -# (no resources) resjar.generated_sources += ['android/support/v7/palette/R.java'] - -resjar.javac_flags += ['-Xlint:all'] - -mgjar = add_java_jar('gecko-mozglue') -mgjar.sources += [geckoview_source_dir + 'java/org/mozilla/gecko/' + x for x in [ - 'mozglue/ByteBufferInputStream.java', - 'mozglue/DirectBufferAllocator.java', - 'mozglue/GeckoLoader.java', - 'mozglue/JNIObject.java', - 'mozglue/NativeReference.java', - 'mozglue/NativeZip.java', - 'mozglue/SafeIntent.java', -]] -mgjar.generated_sources = [] # Keep it this way. -mgjar.extra_jars += [ - CONFIG['ANDROID_SUPPORT_ANNOTATIONS_JAR_LIB'], - 'constants.jar', -] -mgjar.javac_flags += ['-Xlint:all'] - -gujar = add_java_jar('gecko-util') -gujar.sources += [geckoview_source_dir + 'java/org/mozilla/gecko/' + x for x in [ - 'util/ActivityResultHandler.java', - 'util/ActivityResultHandlerMap.java', - 'util/ActivityUtils.java', - 'util/BundleEventListener.java', - 'util/Clipboard.java', - 'util/ContextUtils.java', - 'util/DateUtil.java', - 'util/EventCallback.java', - 'util/FileUtils.java', - 'util/FloatUtils.java', - 'util/GamepadUtils.java', - 'util/GeckoBackgroundThread.java', - 'util/GeckoEventListener.java', - 'util/GeckoJarReader.java', - 'util/GeckoRequest.java', - 'util/HardwareCodecCapabilityUtils.java', - 'util/HardwareUtils.java', - 'util/INIParser.java', - 'util/INISection.java', - 'util/InputOptionsUtils.java', - 'util/IntentUtils.java', - 'util/IOUtils.java', - 'util/JSONUtils.java', - 'util/MenuUtils.java', - 'util/NativeEventListener.java', - 'util/NativeJSContainer.java', - 'util/NativeJSObject.java', - 'util/NetworkUtils.java', - 'util/NonEvictingLruCache.java', - 'util/PrefUtils.java', - 'util/ProxySelector.java', - 'util/publicsuffix/PublicSuffix.java', - 'util/publicsuffix/PublicSuffixPatterns.java', - 'util/RawResource.java', - 'util/StringUtils.java', - 'util/ThreadUtils.java', - 'util/UIAsyncTask.java', - 'util/UUIDUtil.java', - 'util/WeakReferenceHandler.java', - 'util/WindowUtils.java', -]] -gujar.extra_jars = [ - CONFIG['ANDROID_SUPPORT_ANNOTATIONS_JAR_LIB'], - CONFIG['ANDROID_SUPPORT_V4_AAR_LIB'], - CONFIG['ANDROID_SUPPORT_V4_AAR_INTERNAL_LIB'], - 'constants.jar', - 'gecko-mozglue.jar', -] -gujar.javac_flags += ['-Xlint:all,-deprecation'] - -stjar = add_java_jar('sync-thirdparty') -stjar.sources += [ thirdparty_source_dir + f for f in sync_thirdparty_java_files ] -stjar.javac_flags = ['-Xlint:none'] - -services_jar = add_java_jar('services') -services_jar.sources += sync_java_files -services_jar.extra_jars = [ - CONFIG['ANDROID_SUPPORT_ANNOTATIONS_JAR_LIB'], - CONFIG['ANDROID_SUPPORT_V4_AAR_LIB'], - CONFIG['ANDROID_SUPPORT_V4_AAR_INTERNAL_LIB'], - CONFIG['ANDROID_APPCOMPAT_V7_AAR_LIB'], - 'constants.jar', - 'gecko-R.jar', - 'gecko-mozglue.jar', - 'gecko-thirdparty.jar', - 'gecko-util.jar', - 'sync-thirdparty.jar', -] -services_jar.javac_flags += ['-Xlint:all,-deprecation'] - -if CONFIG['MOZ_WEBRTC']: - video_root = TOPSRCDIR + '/media/webrtc/trunk/webrtc/modules/video_capture/android/java/src/org/webrtc/videoengine/' - video_render_root = TOPSRCDIR + '/media/webrtc/trunk/webrtc/modules/video_render/android/java/src/org/webrtc/videoengine/' - audio_root = TOPSRCDIR + '/media/webrtc/trunk/webrtc/modules/audio_device/android/java/src/org/webrtc/voiceengine/' - wrjar = add_java_jar('webrtc') - wrjar.sources += [ - video_root + 'CaptureCapabilityAndroid.java', - video_root + 'VideoCaptureAndroid.java', - video_root + 'VideoCaptureDeviceInfoAndroid.java', - video_render_root + 'ViEAndroidGLES20.java', - video_render_root + 'ViERenderer.java', - ] - wrjar.sources += [ - audio_root + 'AudioManagerAndroid.java', - audio_root + 'WebRtcAudioManager.java', - audio_root + 'WebRtcAudioRecord.java', - audio_root + 'WebRtcAudioTrack.java', - audio_root + 'WebRtcAudioUtils.java', - ] - wrjar.extra_jars = [ - 'constants.jar', - 'gecko-R.jar', - 'gecko-browser.jar', - 'gecko-mozglue.jar', - 'gecko-util.jar', - 'gecko-view.jar', - ] - wrjar.javac_flags += ['-Xlint:all,-deprecation,-cast'] - -gvjar = add_java_jar('gecko-view') - -gvjar.sources += [geckoview_source_dir + 'java/org/mozilla/gecko/' + x - for x in [ - 'AlarmReceiver.java', - 'AndroidGamepadManager.java', - 'BaseGeckoInterface.java', - 'ContextGetter.java', - 'CrashHandler.java', - 'EventDispatcher.java', - 'GeckoAccessibility.java', - 'GeckoAppShell.java', - 'GeckoBatteryManager.java', - 'GeckoEditable.java', - 'GeckoEditableClient.java', - 'GeckoEditableListener.java', - 'GeckoHalDefines.java', - 'GeckoInputConnection.java', - 'GeckoNetworkManager.java', - 'GeckoProfile.java', - 'GeckoProfileDirectories.java', - 'GeckoScreenOrientation.java', - 'GeckoSharedPrefs.java', - 'GeckoThread.java', - 'GeckoView.java', - 'GeckoViewChrome.java', - 'GeckoViewContent.java', - 'GeckoViewFragment.java', - 'gfx/BitmapUtils.java', - 'gfx/BufferedImage.java', - 'gfx/BufferedImageGLInfo.java', - 'gfx/DynamicToolbarAnimator.java', - 'gfx/FloatSize.java', - 'gfx/FullScreenState.java', - 'gfx/GeckoLayerClient.java', - 'gfx/ImmutableViewportMetrics.java', - 'gfx/IntSize.java', - 'gfx/LayerRenderer.java', - 'gfx/LayerView.java', - 'gfx/NativePanZoomController.java', - 'gfx/Overscroll.java', - 'gfx/OverscrollEdgeEffect.java', - 'gfx/PanningPerfAPI.java', - 'gfx/PanZoomController.java', - 'gfx/PanZoomTarget.java', - 'gfx/PointUtils.java', - 'gfx/ProgressiveUpdateData.java', - 'gfx/RectUtils.java', - 'gfx/RenderTask.java', - 'gfx/StackScroller.java', - 'gfx/SurfaceTextureListener.java', - 'gfx/ViewTransform.java', - 'InputConnectionListener.java', - 'InputMethods.java', - 'NotificationListener.java', - 'NSSBridge.java', - 'permissions/PermissionBlock.java', - 'permissions/Permissions.java', - 'permissions/PermissionsHelper.java', - 'PrefsHelper.java', - 'sqlite/ByteBufferInputStream.java', - 'sqlite/MatrixBlobCursor.java', - 'sqlite/SQLiteBridge.java', - 'sqlite/SQLiteBridgeException.java', - 'TouchEventInterceptor.java', -]] - -gvjar.sources += [geckoview_thirdparty_source_dir + f for f in [ - 'java/com/googlecode/eyesfree/braille/selfbraille/ISelfBrailleService.java', - 'java/com/googlecode/eyesfree/braille/selfbraille/SelfBrailleClient.java', - 'java/com/googlecode/eyesfree/braille/selfbraille/WriteData.java', -]] - -gvjar.extra_jars += [ - CONFIG['ANDROID_SUPPORT_ANNOTATIONS_JAR_LIB'], - CONFIG['ANDROID_SUPPORT_V4_AAR_LIB'], - CONFIG['ANDROID_SUPPORT_V4_AAR_INTERNAL_LIB'], - 'constants.jar', - 'gecko-mozglue.jar', - 'gecko-util.jar', -] - -gvjar.javac_flags += [ - '-Xlint:all,-deprecation,-fallthrough', - '-J-Xmx512m', - '-J-Xms128m' -] - - -gbjar = add_java_jar('gecko-browser') - -gbjar.sources += ['java/org/mozilla/gecko/' + x for x in [ - 'AboutPages.java', - 'AccountsHelper.java', - 'ActionBarTextSelection.java', - 'ActionModeCompat.java', - 'ActionModeCompatView.java', - 'ActivityHandlerHelper.java', - 'activitystream/ActivityStream.java', - 'adjust/AdjustBrowserAppDelegate.java', - 'animation/AnimationUtils.java', - 'animation/HeightChangeAnimation.java', - 'animation/PropertyAnimator.java', - 'animation/Rotate3DAnimation.java', - 'animation/ViewHelper.java', - 'ANRReporter.java', - 'BootReceiver.java', - 'BrowserApp.java', - 'BrowserLocaleManager.java', - 'cleanup/FileCleanupController.java', - 'cleanup/FileCleanupService.java', - 'CustomEditText.java', - 'customtabs/CustomTabsActivity.java', - 'customtabs/GeckoCustomTabsService.java', - 'DataReportingNotification.java', - 'db/AbstractPerProfileDatabaseProvider.java', - 'db/AbstractTransactionalProvider.java', - 'db/BaseTable.java', - 'db/BrowserDatabaseHelper.java', - 'db/BrowserDB.java', - 'db/BrowserProvider.java', - 'db/DBUtils.java', - 'db/FormHistoryProvider.java', - 'db/HomeProvider.java', - 'db/LocalBrowserDB.java', - 'db/LocalSearches.java', - 'db/LocalTabsAccessor.java', - 'db/LocalUrlAnnotations.java', - 'db/LocalURLMetadata.java', - 'db/LoginsProvider.java', - 'db/PasswordsProvider.java', - 'db/PerProfileDatabaseProvider.java', - 'db/PerProfileDatabases.java', - 'db/RemoteClient.java', - 'db/RemoteTab.java', - 'db/Searches.java', - 'db/SearchHistoryProvider.java', - 'db/SharedBrowserDatabaseProvider.java', - 'db/SQLiteBridgeContentProvider.java', - 'db/SuggestedSites.java', - 'db/Table.java', - 'db/TabsAccessor.java', - 'db/TabsProvider.java', - 'db/UrlAnnotations.java', - 'db/URLMetadata.java', - 'db/URLMetadataTable.java', - 'delegates/BookmarkStateChangeDelegate.java', - 'delegates/BrowserAppDelegate.java', - 'delegates/BrowserAppDelegateWithReference.java', - 'delegates/OfflineTabStatusDelegate.java', - 'delegates/ScreenshotDelegate.java', - 'delegates/TabsTrayVisibilityAwareDelegate.java', - 'DevToolsAuthHelper.java', - 'distribution/Distribution.java', - 'distribution/DistributionStoreCallback.java', - 'distribution/PartnerBookmarksProviderProxy.java', - 'distribution/PartnerBrowserCustomizationsClient.java', - 'distribution/ReferrerDescriptor.java', - 'distribution/ReferrerReceiver.java', - 'dlc/BaseAction.java', - 'dlc/catalog/DownloadContent.java', - 'dlc/catalog/DownloadContentBootstrap.java', - 'dlc/catalog/DownloadContentBuilder.java', - 'dlc/catalog/DownloadContentCatalog.java', - 'dlc/DownloadAction.java', - 'dlc/DownloadContentService.java', - 'dlc/StudyAction.java', - 'dlc/SyncAction.java', - 'dlc/VerifyAction.java', - 'DoorHangerPopup.java', - 'DownloadsIntegration.java', - 'DynamicToolbar.java', - 'EditBookmarkDialog.java', - 'Experiments.java', - 'feeds/action/CheckForUpdatesAction.java', - 'feeds/action/EnrollSubscriptionsAction.java', - 'feeds/action/FeedAction.java', - 'feeds/action/SetupAlarmsAction.java', - 'feeds/action/SubscribeToFeedAction.java', - 'feeds/action/WithdrawSubscriptionsAction.java', - 'feeds/ContentNotificationsDelegate.java', - 'feeds/FeedAlarmReceiver.java', - 'feeds/FeedFetcher.java', - 'feeds/FeedService.java', - 'feeds/knownsites/KnownSite.java', - 'feeds/knownsites/KnownSiteBlogger.java', - 'feeds/knownsites/KnownSiteMedium.java', - 'feeds/knownsites/KnownSiteTumblr.java', - 'feeds/knownsites/KnownSiteWordpress.java', - 'feeds/parser/Feed.java', - 'feeds/parser/Item.java', - 'feeds/parser/SimpleFeedParser.java', - 'feeds/subscriptions/FeedSubscription.java', - 'FilePicker.java', - 'FilePickerResultHandler.java', - 'FindInPageBar.java', - 'firstrun/DataPanel.java', - 'firstrun/FirstrunAnimationContainer.java', - 'firstrun/FirstrunPager.java', - 'firstrun/FirstrunPagerConfig.java', - 'firstrun/FirstrunPanel.java', - 'firstrun/RestrictedWelcomePanel.java', - 'firstrun/SyncPanel.java', - 'firstrun/TabQueuePanel.java', - 'FormAssistPopup.java', - 'GeckoActivity.java', - 'GeckoActivityStatus.java', - 'GeckoApp.java', - 'GeckoApplication.java', - 'GeckoJavaSampler.java', - 'GeckoMessageReceiver.java', - 'GeckoProfilesProvider.java', - 'GeckoService.java', - 'GeckoUpdateReceiver.java', - 'GlobalHistory.java', - 'GlobalPageMetadata.java', - 'GuestSession.java', - 'health/HealthRecorder.java', - 'health/SessionInformation.java', - 'health/StubbedHealthRecorder.java', - 'home/activitystream/ActivityStream.java', - 'home/activitystream/ActivityStreamHomeFragment.java', - 'home/activitystream/ActivityStreamHomeScreen.java', - 'home/activitystream/menu/ActivityStreamContextMenu.java', - 'home/activitystream/menu/BottomSheetContextMenu.java', - 'home/activitystream/menu/PopupContextMenu.java', - 'home/activitystream/StreamItem.java', - 'home/activitystream/StreamRecyclerAdapter.java', - 'home/activitystream/topsites/CirclePageIndicator.java', - 'home/activitystream/topsites/TopSitesCard.java', - 'home/activitystream/topsites/TopSitesPage.java', - 'home/activitystream/topsites/TopSitesPageAdapter.java', - 'home/activitystream/topsites/TopSitesPagerAdapter.java', - 'home/BookmarkFolderView.java', - 'home/BookmarkScreenshotRow.java', - 'home/BookmarksListAdapter.java', - 'home/BookmarksListView.java', - 'home/BookmarksPanel.java', - 'home/BrowserSearch.java', - 'home/ClientsAdapter.java', - 'home/CombinedHistoryAdapter.java', - 'home/CombinedHistoryItem.java', - 'home/CombinedHistoryPanel.java', - 'home/CombinedHistoryRecyclerView.java', - 'home/DynamicPanel.java', - 'home/FramePanelLayout.java', - 'home/HistorySectionsHelper.java', - 'home/HomeAdapter.java', - 'home/HomeBanner.java', - 'home/HomeConfig.java', - 'home/HomeConfigLoader.java', - 'home/HomeConfigPrefsBackend.java', - 'home/HomeContextMenuInfo.java', - 'home/HomeExpandableListView.java', - 'home/HomeFragment.java', - 'home/HomeListView.java', - 'home/HomePager.java', - 'home/HomePanelsManager.java', - 'home/HomeScreen.java', - 'home/ImageLoader.java', - 'home/MultiTypeCursorAdapter.java', - 'home/PanelAuthCache.java', - 'home/PanelAuthLayout.java', - 'home/PanelBackItemView.java', - 'home/PanelHeaderView.java', - 'home/PanelInfoManager.java', - 'home/PanelItemView.java', - 'home/PanelLayout.java', - 'home/PanelListView.java', - 'home/PanelRecyclerView.java', - 'home/PanelRecyclerViewAdapter.java', - 'home/PanelRefreshLayout.java', - 'home/PanelViewAdapter.java', - 'home/PanelViewItemHandler.java', - 'home/PinSiteDialog.java', - 'home/RecentTabsAdapter.java', - 'home/RemoteTabsExpandableListState.java', - 'home/SearchEngine.java', - 'home/SearchEngineAdapter.java', - 'home/SearchEngineBar.java', - 'home/SearchEngineRow.java', - 'home/SearchLoader.java', - 'home/SimpleCursorLoader.java', - 'home/SpacingDecoration.java', - 'home/TabMenuStrip.java', - 'home/TabMenuStripLayout.java', - 'home/TopSitesGridItemView.java', - 'home/TopSitesGridView.java', - 'home/TopSitesPanel.java', - 'home/TopSitesThumbnailView.java', - 'home/TwoLinePageRow.java', - 'icons/decoders/FaviconDecoder.java', - 'icons/decoders/ICODecoder.java', - 'icons/decoders/IconDirectoryEntry.java', - 'icons/decoders/LoadFaviconResult.java', - 'icons/IconCallback.java', - 'icons/IconDescriptor.java', - 'icons/IconDescriptorComparator.java', - 'icons/IconRequest.java', - 'icons/IconRequestBuilder.java', - 'icons/IconRequestExecutor.java', - 'icons/IconResponse.java', - 'icons/Icons.java', - 'icons/IconsHelper.java', - 'icons/IconTask.java', - 'icons/loader/ContentProviderLoader.java', - 'icons/loader/DataUriLoader.java', - 'icons/loader/DiskLoader.java', - 'icons/loader/IconDownloader.java', - 'icons/loader/IconGenerator.java', - 'icons/loader/IconLoader.java', - 'icons/loader/JarLoader.java', - 'icons/loader/LegacyLoader.java', - 'icons/loader/MemoryLoader.java', - 'icons/preparation/AboutPagesPreparer.java', - 'icons/preparation/AddDefaultIconUrl.java', - 'icons/preparation/FilterKnownFailureUrls.java', - 'icons/preparation/FilterMimeTypes.java', - 'icons/preparation/FilterPrivilegedUrls.java', - 'icons/preparation/LookupIconUrl.java', - 'icons/preparation/Preparer.java', - 'icons/processing/ColorProcessor.java', - 'icons/processing/DiskProcessor.java', - 'icons/processing/MemoryProcessor.java', - 'icons/processing/Processor.java', - 'icons/processing/ResizingProcessor.java', - 'icons/storage/DiskStorage.java', - 'icons/storage/FailureCache.java', - 'icons/storage/MemoryStorage.java', - 'IntentHelper.java', - 'javaaddons/JavaAddonManager.java', - 'javaaddons/JavaAddonManagerV1.java', - 'LauncherActivity.java', - 'lwt/LightweightTheme.java', - 'lwt/LightweightThemeDrawable.java', - 'mdns/MulticastDNSManager.java', - 'media/AsyncCodec.java', - 'media/AsyncCodecFactory.java', - 'media/AudioFocusAgent.java', - 'media/Codec.java', - 'media/CodecProxy.java', - 'media/FormatParam.java', - 'media/GeckoMediaDrm.java', - 'media/GeckoMediaDrmBridgeV21.java', - 'media/GeckoMediaDrmBridgeV23.java', - 'media/JellyBeanAsyncCodec.java', - 'media/LocalMediaDrmBridge.java', - 'media/MediaControlService.java', - 'media/MediaDrmProxy.java', - 'media/MediaManager.java', - 'media/RemoteManager.java', - 'media/RemoteMediaDrmBridge.java', - 'media/RemoteMediaDrmBridgeStub.java', - 'media/Sample.java', - 'media/SamplePool.java', - 'media/SessionKeyInfo.java', - 'media/VideoPlayer.java', - 'MediaCastingBar.java', - 'MemoryMonitor.java', - 'menu/GeckoMenu.java', - 'menu/GeckoMenuInflater.java', - 'menu/GeckoMenuItem.java', - 'menu/GeckoSubMenu.java', - 'menu/MenuItemActionBar.java', - 'menu/MenuItemDefault.java', - 'menu/MenuItemSwitcherLayout.java', - 'menu/MenuPanel.java', - 'menu/MenuPopup.java', - 'MotionEventInterceptor.java', - 'mozglue/SharedMemBuffer.java', - 'mozglue/SharedMemory.java', - 'notifications/NotificationClient.java', - 'notifications/NotificationHelper.java', - 'notifications/NotificationReceiver.java', - 'notifications/NotificationService.java', - 'notifications/WhatsNewReceiver.java', - 'overlays/OverlayConstants.java', - 'overlays/service/OverlayActionService.java', - 'overlays/service/ShareData.java', - 'overlays/service/sharemethods/AddBookmark.java', - 'overlays/service/sharemethods/SendTab.java', - 'overlays/service/sharemethods/ShareMethod.java', - 'overlays/ui/OverlayDialogButton.java', - 'overlays/ui/SendTabDeviceListArrayAdapter.java', - 'overlays/ui/SendTabList.java', - 'overlays/ui/SendTabTargetSelectedListener.java', - 'overlays/ui/ShareDialog.java', - 'PackageReplacedReceiver.java', - 'preferences/AlignRightLinkPreference.java', - 'preferences/AndroidImport.java', - 'preferences/AndroidImportPreference.java', - 'preferences/AppCompatPreferenceActivity.java', - 'preferences/ClearOnShutdownPref.java', - 'preferences/CustomCheckBoxPreference.java', - 'preferences/CustomListCategory.java', - 'preferences/CustomListPreference.java', - 'preferences/DistroSharedPrefsImport.java', - 'preferences/FontSizePreference.java', - 'preferences/GeckoPreferenceFragment.java', - 'preferences/GeckoPreferences.java', - 'preferences/LinkPreference.java', - 'preferences/ListCheckboxPreference.java', - 'preferences/LocaleListPreference.java', - 'preferences/ModifiableHintPreference.java', - 'preferences/MultiChoicePreference.java', - 'preferences/MultiPrefMultiChoicePreference.java', - 'preferences/PanelsPreference.java', - 'preferences/PanelsPreferenceCategory.java', - 'preferences/PrivateDataPreference.java', - 'preferences/SearchEnginePreference.java', - 'preferences/SearchPreferenceCategory.java', - 'preferences/SetHomepagePreference.java', - 'preferences/SyncPreference.java', - 'PresentationView.java', - 'PrintHelper.java', - 'PrivateTab.java', - 'promotion/AddToHomeScreenPromotion.java', - 'promotion/HomeScreenPrompt.java', - 'promotion/ReaderViewBookmarkPromotion.java', - 'promotion/SimpleHelperUI.java', - 'prompts/ColorPickerInput.java', - 'prompts/IconGridInput.java', - 'prompts/IntentChooserPrompt.java', - 'prompts/IntentHandler.java', - 'prompts/Prompt.java', - 'prompts/PromptInput.java', - 'prompts/PromptListAdapter.java', - 'prompts/PromptListItem.java', - 'prompts/PromptService.java', - 'prompts/TabInput.java', - 'reader/ReaderModeUtils.java', - 'reader/ReadingListHelper.java', - 'reader/SavedReaderViewHelper.java', - 'RemoteClientsDialogFragment.java', - 'Restarter.java', - 'restrictions/DefaultConfiguration.java', - 'restrictions/GuestProfileConfiguration.java', - 'restrictions/Restrictable.java', - 'restrictions/RestrictedProfileConfiguration.java', - 'restrictions/RestrictionCache.java', - 'restrictions/RestrictionConfiguration.java', - 'restrictions/RestrictionProvider.java', - 'restrictions/Restrictions.java', - 'ScreenManagerHelper.java', - 'ScreenshotObserver.java', - 'search/SearchEngine.java', - 'search/SearchEngineManager.java', - 'SessionParser.java', - 'SharedPreferencesHelper.java', - 'SiteIdentity.java', - 'SnackbarBuilder.java', - 'SuggestClient.java', - 'Tab.java', - 'tabqueue/TabQueueHelper.java', - 'tabqueue/TabQueuePrompt.java', - 'tabqueue/TabQueueService.java', - 'tabqueue/TabReceivedService.java', - 'Tabs.java', - 'tabs/PrivateTabsPanel.java', - 'tabs/TabCurve.java', - 'tabs/TabHistoryController.java', - 'tabs/TabHistoryFragment.java', - 'tabs/TabHistoryItemRow.java', - 'tabs/TabHistoryPage.java', - 'tabs/TabPanelBackButton.java', - 'tabs/TabsGridLayout.java', - 'tabs/TabsLayout.java', - 'tabs/TabsLayoutAdapter.java', - 'tabs/TabsLayoutItemView.java', - 'tabs/TabsLayoutRecyclerAdapter.java', - 'tabs/TabsListLayout.java', - 'tabs/TabsListLayoutAnimator.java', - 'tabs/TabsPanel.java', - 'tabs/TabsPanelThumbnailView.java', - 'tabs/TabsTouchHelperCallback.java', - 'Telemetry.java', - 'telemetry/measurements/CampaignIdMeasurements.java', - 'telemetry/measurements/SearchCountMeasurements.java', - 'telemetry/measurements/SessionMeasurements.java', - 'telemetry/pingbuilders/TelemetryCorePingBuilder.java', - 'telemetry/pingbuilders/TelemetryPingBuilder.java', - 'telemetry/schedulers/TelemetryUploadAllPingsImmediatelyScheduler.java', - 'telemetry/schedulers/TelemetryUploadScheduler.java', - 'telemetry/stores/TelemetryJSONFilePingStore.java', - 'telemetry/stores/TelemetryPingStore.java', - 'telemetry/TelemetryConstants.java', - 'telemetry/TelemetryCorePingDelegate.java', - 'telemetry/TelemetryDispatcher.java', - 'telemetry/TelemetryPing.java', - 'telemetry/TelemetryPreferences.java', - 'telemetry/TelemetryUploadService.java', - 'TelemetryContract.java', - 'text/FloatingActionModeCallback.java', - 'text/FloatingToolbarTextSelection.java', - 'text/TextAction.java', - 'text/TextSelection.java', - 'ThumbnailHelper.java', - 'toolbar/AutocompleteHandler.java', - 'toolbar/BackButton.java', - 'toolbar/BrowserToolbar.java', - 'toolbar/BrowserToolbarPhone.java', - 'toolbar/BrowserToolbarPhoneBase.java', - 'toolbar/BrowserToolbarTablet.java', - 'toolbar/BrowserToolbarTabletBase.java', - 'toolbar/CanvasDelegate.java', - 'toolbar/ForwardButton.java', - 'toolbar/NavButton.java', - 'toolbar/PageActionLayout.java', - 'toolbar/PhoneTabsButton.java', - 'toolbar/ShapedButton.java', - 'toolbar/ShapedButtonFrameLayout.java', - 'toolbar/SiteIdentityPopup.java', - 'toolbar/TabCounter.java', - 'toolbar/ToolbarDisplayLayout.java', - 'toolbar/ToolbarEditLayout.java', - 'toolbar/ToolbarEditText.java', - 'toolbar/ToolbarPrefs.java', - 'toolbar/ToolbarProgressView.java', - 'trackingprotection/TrackingProtectionPrompt.java', - 'updater/PostUpdateHandler.java', - 'updater/UpdateService.java', - 'updater/UpdateServiceHelper.java', - 'util/ColorUtil.java', - 'util/DrawableUtil.java', - 'util/ResourceDrawableUtils.java', - 'util/TouchTargetUtil.java', - 'util/ViewUtil.java', - 'widget/ActivityChooserModel.java', - 'widget/AllCapsTextView.java', - 'widget/AnchoredPopup.java', - 'widget/AnimatedHeightLayout.java', - 'widget/BasicColorPicker.java', - 'widget/CheckableLinearLayout.java', - 'widget/ClickableWhenDisabledEditText.java', - 'widget/ContentSecurityDoorHanger.java', - 'widget/CropImageView.java', - 'widget/DateTimePicker.java', - 'widget/DefaultDoorHanger.java', - 'widget/DefaultItemAnimatorBase.java', - 'widget/DoorHanger.java', - 'widget/DoorhangerConfig.java', - 'widget/EllipsisTextView.java', - 'widget/ExternalIntentDuringPrivateBrowsingPromptFragment.java', - 'widget/FadedMultiColorTextView.java', - 'widget/FadedSingleColorTextView.java', - 'widget/FadedTextView.java', - 'widget/FaviconView.java', - 'widget/FilledCardView.java', - 'widget/FlowLayout.java', - 'widget/GeckoActionProvider.java', - 'widget/GeckoPopupMenu.java', - 'widget/HistoryDividerItemDecoration.java', - 'widget/IconTabWidget.java', - 'widget/LoginDoorHanger.java', - 'widget/RecyclerViewClickSupport.java', - 'widget/ResizablePathDrawable.java', - 'widget/RoundedCornerLayout.java', - 'widget/SiteLogins.java', - 'widget/SquaredImageView.java', - 'widget/SquaredRelativeLayout.java', - 'widget/SwipeDismissListViewTouchListener.java', - 'widget/TabThumbnailWrapper.java', - 'widget/ThumbnailView.java', - 'widget/TouchDelegateWithReset.java', - 'widget/TwoWayView.java', - 'ZoomedView.java', -]] -# The following sources are checked in to version control but -# generated by a script (java/org/mozilla/gecko/widget/themed/generate_themed_views.py). -# If you're editing this list, make sure to edit that script. -gbjar.sources += ['java/org/mozilla/gecko/' + x for x in [ - 'widget/themed/ThemedEditText.java', - 'widget/themed/ThemedFrameLayout.java', - 'widget/themed/ThemedImageButton.java', - 'widget/themed/ThemedImageView.java', - 'widget/themed/ThemedLinearLayout.java', - 'widget/themed/ThemedRelativeLayout.java', - 'widget/themed/ThemedTextSwitcher.java', - 'widget/themed/ThemedTextView.java', - 'widget/themed/ThemedView.java', -]] -android_package_dir = CONFIG['ANDROID_PACKAGE_NAME'].replace('.', '/') -gbjar.generated_sources = [] # Keep it this way. -gbjar.extra_jars += [ - CONFIG['ANDROID_SUPPORT_ANNOTATIONS_JAR_LIB'], - CONFIG['ANDROID_SUPPORT_V4_AAR_LIB'], - CONFIG['ANDROID_SUPPORT_V4_AAR_INTERNAL_LIB'], - 'constants.jar' -] -if CONFIG['MOZ_ANDROID_GCM']: - gbjar.sources += ['java/org/mozilla/gecko/' + x for x in [ - 'gcm/GcmInstanceIDListenerService.java', - 'gcm/GcmMessageListenerService.java', - 'gcm/GcmTokenClient.java', - 'push/Fetched.java', - 'push/PushClient.java', - 'push/PushManager.java', - 'push/PushRegistration.java', - 'push/PushService.java', - 'push/PushState.java', - 'push/PushSubscription.java', - ]] - -if (CONFIG['MOZ_ANDROID_MAX_SDK_VERSION']): - max_sdk_version = int(CONFIG['MOZ_ANDROID_MAX_SDK_VERSION']) -else: - max_sdk_version = 999 - -# Only bother to include new tablet code if we're building for tablet-capable -# OS releases. -if max_sdk_version >= 11: - gbjar.sources += ['java/org/mozilla/gecko/' + x for x in [ - 'tabs/TabStrip.java', - 'tabs/TabStripAdapter.java', - 'tabs/TabStripItemView.java', - 'tabs/TabStripView.java' - ]] - -gbjar.extra_jars += [ - OBJDIR + '/../javaaddons/javaaddons-1.0.jar', - 'gecko-R.jar', - 'gecko-mozglue.jar', - 'gecko-thirdparty.jar', - 'gecko-util.jar', - 'gecko-view.jar', - 'sync-thirdparty.jar', - 'services.jar', -] - -moz_native_devices_jars = [ - CONFIG['ANDROID_MEDIAROUTER_V7_AAR_LIB'], - CONFIG['ANDROID_MEDIAROUTER_V7_AAR_INTERNAL_LIB'], - CONFIG['ANDROID_PLAY_SERVICES_BASE_AAR_LIB'], - CONFIG['ANDROID_PLAY_SERVICES_BASEMENT_AAR_LIB'], - CONFIG['ANDROID_PLAY_SERVICES_CAST_AAR_LIB'], -] -moz_native_devices_sources = ['java/org/mozilla/gecko/' + x for x in [ - 'ChromeCastDisplay.java', - 'ChromeCastPlayer.java', - 'GeckoMediaPlayer.java', - 'GeckoPresentationDisplay.java', - 'MediaPlayerManager.java', - 'PresentationMediaPlayerManager.java', - 'RemotePresentationService.java', -]] -if CONFIG['MOZ_NATIVE_DEVICES']: - gbjar.extra_jars += moz_native_devices_jars - gbjar.sources += moz_native_devices_sources - - if CONFIG['ANDROID_MEDIAROUTER_V7_AAR']: - ANDROID_EXTRA_PACKAGES += ['android.support.v7.mediarouter'] - ANDROID_EXTRA_RES_DIRS += ['%' + CONFIG['ANDROID_MEDIAROUTER_V7_AAR_RES']] - resjar.generated_sources += ['android/support/v7/mediarouter/R.java'] - - if CONFIG['ANDROID_PLAY_SERVICES_BASE_AAR']: - ANDROID_EXTRA_PACKAGES += ['com.google.android.gms.base'] - ANDROID_EXTRA_RES_DIRS += ['%' + CONFIG['ANDROID_PLAY_SERVICES_BASE_AAR_RES']] - resjar.generated_sources += ['com/google/android/gms/base/R.java'] - - if CONFIG['ANDROID_PLAY_SERVICES_BASEMENT_AAR']: - ANDROID_EXTRA_PACKAGES += ['com.google.android.gms'] - ANDROID_EXTRA_RES_DIRS += ['%' + CONFIG['ANDROID_PLAY_SERVICES_BASEMENT_AAR_RES']] - resjar.generated_sources += ['com/google/android/gms/R.java'] - - if CONFIG['ANDROID_PLAY_SERVICES_CAST_AAR']: - ANDROID_EXTRA_PACKAGES += ['com.google.android.gms.cast'] - ANDROID_EXTRA_RES_DIRS += ['%' + CONFIG['ANDROID_PLAY_SERVICES_CAST_AAR_RES']] - resjar.generated_sources += ['com/google/android/gms/cast/R.java'] - -if CONFIG['MOZ_ANDROID_GCM']: - gbjar.extra_jars += [ - CONFIG['ANDROID_PLAY_SERVICES_BASE_AAR_LIB'], - CONFIG['ANDROID_PLAY_SERVICES_BASEMENT_AAR_LIB'], - CONFIG['ANDROID_PLAY_SERVICES_GCM_AAR_LIB'], - CONFIG['ANDROID_PLAY_SERVICES_MEASUREMENT_AAR_LIB'], - ] - - if CONFIG['ANDROID_PLAY_SERVICES_BASE_AAR']: - ANDROID_EXTRA_PACKAGES += ['com.google.android.gms'] - ANDROID_EXTRA_RES_DIRS += ['%' + CONFIG['ANDROID_PLAY_SERVICES_BASE_AAR_RES']] - resjar.generated_sources += ['com/google/android/gms/R.java'] - - if CONFIG['ANDROID_PLAY_SERVICES_BASEMENT_AAR']: - ANDROID_EXTRA_PACKAGES += ['com.google.android.gms'] - ANDROID_EXTRA_RES_DIRS += ['%' + CONFIG['ANDROID_PLAY_SERVICES_BASEMENT_AAR_RES']] - resjar.generated_sources += ['com/google/android/gms/R.java'] - - if CONFIG['ANDROID_PLAY_SERVICES_GCM_AAR']: - ANDROID_EXTRA_PACKAGES += ['com.google.android.gms.gcm'] - ANDROID_EXTRA_RES_DIRS += ['%' + CONFIG['ANDROID_PLAY_SERVICES_GCM_AAR_RES']] -# (no resources) resjar.generated_sources += ['com/google/android/gms/gcm/R.java'] - - if CONFIG['ANDROID_PLAY_SERVICES_MEASUREMENT_AAR']: - ANDROID_EXTRA_PACKAGES += ['com.google.android.gms.measurement'] - ANDROID_EXTRA_RES_DIRS += ['%' + CONFIG['ANDROID_PLAY_SERVICES_MEASUREMENT_AAR_RES']] -# (no resources) resjar.generated_sources += ['android/support/v7/palette/R.java'] - -if CONFIG['MOZ_INSTALL_TRACKING']: - gbjar.extra_jars += [ - CONFIG['ANDROID_PLAY_SERVICES_ADS_AAR_LIB'], - CONFIG['ANDROID_PLAY_SERVICES_BASEMENT_AAR_LIB'], - ] - - if CONFIG['ANDROID_PLAY_SERVICES_ADS_AAR']: - ANDROID_EXTRA_PACKAGES += ['com.google.android.gms.ads'] - ANDROID_EXTRA_RES_DIRS += ['%' + CONFIG['ANDROID_PLAY_SERVICES_ADS_AAR_RES']] - resjar.generated_sources += ['com/google/android/gms/ads/R.java'] - - if CONFIG['ANDROID_PLAY_SERVICES_BASEMENT_AAR']: - ANDROID_EXTRA_PACKAGES += ['com.google.android.gms'] - ANDROID_EXTRA_RES_DIRS += ['%' + CONFIG['ANDROID_PLAY_SERVICES_BASEMENT_AAR_RES']] - resjar.generated_sources += ['com/google/android/gms/R.java'] - -gbjar.extra_jars += [CONFIG['ANDROID_APPCOMPAT_V7_AAR_LIB']] -gbjar.extra_jars += [CONFIG['ANDROID_SUPPORT_VECTOR_DRAWABLE_AAR_LIB']] -gbjar.extra_jars += [CONFIG['ANDROID_ANIMATED_VECTOR_DRAWABLE_AAR_LIB']] -gbjar.extra_jars += [CONFIG['ANDROID_CARDVIEW_V7_AAR_LIB']] -gbjar.extra_jars += [CONFIG['ANDROID_DESIGN_AAR_LIB']] -gbjar.extra_jars += [CONFIG['ANDROID_RECYCLERVIEW_V7_AAR_LIB']] -gbjar.extra_jars += [CONFIG['ANDROID_CUSTOMTABS_AAR_LIB']] -gbjar.extra_jars += [CONFIG['ANDROID_PALETTE_V7_AAR_LIB']] - -gbjar.javac_flags += ['-Xlint:all,-deprecation,-fallthrough', '-J-Xmx512m', '-J-Xms128m'] - -# gecko-thirdparty is a good place to put small independent libraries -gtjar = add_java_jar('gecko-thirdparty') -gtjar.sources += [ thirdparty_source_dir + f for f in [ - 'com/jakewharton/disklrucache/DiskLruCache.java', - 'com/jakewharton/disklrucache/StrictLineReader.java', - 'com/jakewharton/disklrucache/Util.java', - 'com/keepsafe/switchboard/AsyncConfigLoader.java', - 'com/keepsafe/switchboard/DeviceUuidFactory.java', - 'com/keepsafe/switchboard/Preferences.java', - 'com/keepsafe/switchboard/Switch.java', - 'com/keepsafe/switchboard/SwitchBoard.java', - 'com/squareup/leakcanary/LeakCanary.java', - 'com/squareup/leakcanary/RefWatcher.java', - 'com/squareup/picasso/Action.java', - 'com/squareup/picasso/AssetBitmapHunter.java', - 'com/squareup/picasso/BitmapHunter.java', - 'com/squareup/picasso/Cache.java', - 'com/squareup/picasso/Callback.java', - 'com/squareup/picasso/ContactsPhotoBitmapHunter.java', - 'com/squareup/picasso/ContentStreamBitmapHunter.java', - 'com/squareup/picasso/DeferredRequestCreator.java', - 'com/squareup/picasso/Dispatcher.java', - 'com/squareup/picasso/Downloader.java', - 'com/squareup/picasso/FetchAction.java', - 'com/squareup/picasso/FileBitmapHunter.java', - 'com/squareup/picasso/GetAction.java', - 'com/squareup/picasso/ImageViewAction.java', - 'com/squareup/picasso/LruCache.java', - 'com/squareup/picasso/MarkableInputStream.java', - 'com/squareup/picasso/MediaStoreBitmapHunter.java', - 'com/squareup/picasso/NetworkBitmapHunter.java', - 'com/squareup/picasso/Picasso.java', - 'com/squareup/picasso/PicassoDrawable.java', - 'com/squareup/picasso/PicassoExecutorService.java', - 'com/squareup/picasso/Request.java', - 'com/squareup/picasso/RequestCreator.java', - 'com/squareup/picasso/ResourceBitmapHunter.java', - 'com/squareup/picasso/Stats.java', - 'com/squareup/picasso/StatsSnapshot.java', - 'com/squareup/picasso/Target.java', - 'com/squareup/picasso/TargetAction.java', - 'com/squareup/picasso/Transformation.java', - 'com/squareup/picasso/UrlConnectionDownloader.java', - 'com/squareup/picasso/Utils.java' -] ] -gtjar.extra_jars = [ - CONFIG['ANDROID_SUPPORT_ANNOTATIONS_JAR_LIB'], - CONFIG['ANDROID_SUPPORT_V4_AAR_LIB'], - CONFIG['ANDROID_SUPPORT_V4_AAR_INTERNAL_LIB'], -] - -if not CONFIG['MOZILLA_OFFICIAL']: - gtjar.sources += [ thirdparty_source_dir + f for f in [ - 'org/lucasr/dspec/DesignSpec.java', - 'org/lucasr/dspec/RawResource.java' - ] ] - -if CONFIG['MOZ_INSTALL_TRACKING']: - adjustjar = add_java_jar('gecko-thirdparty-adjust_sdk') - adjustjar.sources += [ thirdparty_source_dir + f for f in [ - 'com/adjust/sdk/ActivityHandler.java', - 'com/adjust/sdk/ActivityKind.java', - 'com/adjust/sdk/ActivityPackage.java', - 'com/adjust/sdk/ActivityState.java', - 'com/adjust/sdk/Adjust.java', - 'com/adjust/sdk/AdjustAttribution.java', - 'com/adjust/sdk/AdjustConfig.java', - 'com/adjust/sdk/AdjustEvent.java', - 'com/adjust/sdk/AdjustFactory.java', - 'com/adjust/sdk/AdjustInstance.java', - 'com/adjust/sdk/AdjustReferrerReceiver.java', - 'com/adjust/sdk/AttributionHandler.java', - 'com/adjust/sdk/Constants.java', - 'com/adjust/sdk/DeviceInfo.java', - 'com/adjust/sdk/IActivityHandler.java', - 'com/adjust/sdk/IAttributionHandler.java', - 'com/adjust/sdk/ILogger.java', - 'com/adjust/sdk/IPackageHandler.java', - 'com/adjust/sdk/IRequestHandler.java', - 'com/adjust/sdk/Logger.java', - 'com/adjust/sdk/LogLevel.java', - 'com/adjust/sdk/OnAttributionChangedListener.java', - 'com/adjust/sdk/PackageBuilder.java', - 'com/adjust/sdk/PackageHandler.java', - 'com/adjust/sdk/plugin/AndroidIdUtil.java', - 'com/adjust/sdk/plugin/MacAddressUtil.java', - 'com/adjust/sdk/plugin/Plugin.java', - 'com/adjust/sdk/Reflection.java', - 'com/adjust/sdk/RequestHandler.java', - 'com/adjust/sdk/UnitTestActivity.java', - 'com/adjust/sdk/Util.java' - ] ] - adjustjar.extra_jars += [ - 'sync-thirdparty.jar', - ] - -# Putting branding earlier allows branders to override default resources. -ANDROID_RES_DIRS += [ - '/' + CONFIG['MOZ_BRANDING_DIRECTORY'] + '/res', - 'resources', - '/mobile/android/services/src/main/res', - '!res', -] - -ANDROID_GENERATED_RESFILES += [ - 'res/raw/browsersearch.json', - 'res/raw/suggestedsites.json', - 'res/values/strings.xml', -] - -ANDROID_ASSETS_DIRS += [ - '/mobile/android/app/assets', -] - -if CONFIG['MOZ_ANDROID_DISTRIBUTION_DIRECTORY']: - # If you change this, also change its equivalent in mobile/android/bouncer. - if not CONFIG['MOZ_ANDROID_PACKAGE_INSTALL_BOUNCER']: - # If we are packaging the bouncer, it will have the distribution, so don't put - # it in the main APK as well. - ANDROID_ASSETS_DIRS += [ - '%' + CONFIG['MOZ_ANDROID_DISTRIBUTION_DIRECTORY'] + '/assets', - ] - -# We do not expose MOZ_ADJUST_SDK_KEY here because that # would leak the value -# to build logs. Instead we expose the token quietly where appropriate in -# Makefile.in. -for var in ('MOZ_ANDROID_ANR_REPORTER', 'MOZ_DEBUG', - 'MOZ_ANDROID_SEARCH_ACTIVITY', 'MOZ_NATIVE_DEVICES', 'MOZ_ANDROID_MLS_STUMBLER', - 'MOZ_ANDROID_DOWNLOADS_INTEGRATION', 'MOZ_INSTALL_TRACKING', - 'MOZ_ANDROID_GCM', 'MOZ_ANDROID_EXCLUDE_FONTS', 'MOZ_LOCALE_SWITCHER', - 'MOZ_ANDROID_BEAM', 'MOZ_ANDROID_DOWNLOAD_CONTENT_SERVICE', - 'MOZ_SWITCHBOARD', 'MOZ_ANDROID_CUSTOM_TABS', - 'MOZ_ANDROID_ACTIVITY_STREAM'): - if CONFIG[var]: - DEFINES[var] = 1 - -for var in ('MOZ_UPDATER', 'MOZ_PKG_SPECIAL', 'MOZ_ANDROID_GCM_SENDERID'): - if CONFIG[var]: - DEFINES[var] = CONFIG[var] - -for var in ('ANDROID_PACKAGE_NAME', 'ANDROID_CPU_ARCH', - 'GRE_MILESTONE', 'MOZ_APP_BASENAME', 'MOZ_MOZILLA_API_KEY', - 'MOZ_APP_DISPLAYNAME', 'MOZ_APP_UA_NAME', 'MOZ_APP_ID', 'MOZ_APP_NAME', - 'MOZ_APP_VENDOR', 'MOZ_APP_VERSION', 'MOZ_CHILD_PROCESS_NAME', - 'MOZ_ANDROID_APPLICATION_CLASS', 'MOZ_ANDROID_BROWSER_INTENT_CLASS', 'MOZ_ANDROID_SEARCH_INTENT_CLASS', - 'MOZ_UPDATE_CHANNEL', 'OMNIJAR_NAME', - 'OS_TARGET', 'TARGET_XPCOM_ABI'): - DEFINES[var] = CONFIG[var] - -# Mangle our package name to avoid Bug 750548. -DEFINES['MANGLED_ANDROID_PACKAGE_NAME'] = CONFIG['ANDROID_PACKAGE_NAME'].replace('fennec', 'f3nn3c') -DEFINES['MOZ_APP_ABI'] = CONFIG['TARGET_XPCOM_ABI'] -if not CONFIG['COMPILE_ENVIRONMENT']: - # These should really come from the included binaries, but that's not easy. - DEFINES['MOZ_APP_ABI'] = 'arm-eabi-gcc3' # Observe quote differences here ... - DEFINES['TARGET_XPCOM_ABI'] = '"arm-eabi-gcc3"' # ... and here. - -if '-march=armv7' in CONFIG['OS_CFLAGS']: - DEFINES['MOZ_MIN_CPU_VERSION'] = 7 -else: - DEFINES['MOZ_MIN_CPU_VERSION'] = 5 - -if CONFIG['MOZ_ANDROID_SEARCH_ACTIVITY']: - # The Search Activity is mostly independent of Fennec proper, but - # it does depend on Geckoview. Therefore, we build it as a jar - # that depends on the Geckoview jars. - search_source_dir = SRCDIR + '/../search' - include('../search/search_activity_sources.mozbuild') - - search_activity = add_java_jar('search-activity') - search_activity.sources += [search_source_dir + '/' + f for f in search_activity_sources] - search_activity.javac_flags += ['-Xlint:all'] - search_activity.extra_jars = [ - CONFIG['ANDROID_SUPPORT_ANNOTATIONS_JAR_LIB'], - CONFIG['ANDROID_SUPPORT_V4_AAR_LIB'], - CONFIG['ANDROID_SUPPORT_V4_AAR_INTERNAL_LIB'], - 'constants.jar', - 'gecko-R.jar', - 'gecko-browser.jar', - 'gecko-mozglue.jar', - 'gecko-thirdparty.jar', - 'gecko-util.jar', - 'gecko-view.jar', - ] - -FINAL_TARGET_PP_FILES += ['package-name.txt.in'] - -DEFINES['OBJDIR'] = OBJDIR -DEFINES['TOPOBJDIR'] = TOPOBJDIR - -OBJDIR_PP_FILES.mobile.android.base += [ - 'AndroidManifest.xml.in', -] - -gbjar.sources += ['generated/org/mozilla/gecko/' + x for x in [ - 'media/ICodec.java', - 'media/ICodecCallbacks.java', - 'media/IMediaDrmBridge.java', - 'media/IMediaDrmBridgeCallbacks.java', - 'media/IMediaManager.java', -]] diff --git a/mobile/android/base/package-name.txt.in b/mobile/android/base/package-name.txt.in deleted file mode 100644 index c7b9731cb3..0000000000 --- a/mobile/android/base/package-name.txt.in +++ /dev/null @@ -1,2 +0,0 @@ -#filter substitution -@ANDROID_PACKAGE_NAME@ diff --git a/mobile/android/base/resources/anim/grow_fade_in.xml b/mobile/android/base/resources/anim/grow_fade_in.xml deleted file mode 100644 index bf7a3f23d9..0000000000 --- a/mobile/android/base/resources/anim/grow_fade_in.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - diff --git a/mobile/android/base/resources/anim/overlay_check_entry.xml b/mobile/android/base/resources/anim/overlay_check_entry.xml deleted file mode 100644 index 5a80612a7f..0000000000 --- a/mobile/android/base/resources/anim/overlay_check_entry.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - diff --git a/mobile/android/base/resources/anim/overlay_check_exit.xml b/mobile/android/base/resources/anim/overlay_check_exit.xml deleted file mode 100644 index 9c42eaa0b0..0000000000 --- a/mobile/android/base/resources/anim/overlay_check_exit.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - diff --git a/mobile/android/base/resources/anim/overlay_pop.xml b/mobile/android/base/resources/anim/overlay_pop.xml deleted file mode 100644 index 6b5c412dce..0000000000 --- a/mobile/android/base/resources/anim/overlay_pop.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - diff --git a/mobile/android/base/resources/anim/overlay_slide_down.xml b/mobile/android/base/resources/anim/overlay_slide_down.xml deleted file mode 100644 index c7007458cc..0000000000 --- a/mobile/android/base/resources/anim/overlay_slide_down.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - diff --git a/mobile/android/base/resources/anim/overlay_slide_up.xml b/mobile/android/base/resources/anim/overlay_slide_up.xml deleted file mode 100644 index 2541493ed3..0000000000 --- a/mobile/android/base/resources/anim/overlay_slide_up.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - diff --git a/mobile/android/base/resources/anim/popup_hide.xml b/mobile/android/base/resources/anim/popup_hide.xml deleted file mode 100644 index 5ea5bc2cbe..0000000000 --- a/mobile/android/base/resources/anim/popup_hide.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - diff --git a/mobile/android/base/resources/anim/popup_show.xml b/mobile/android/base/resources/anim/popup_show.xml deleted file mode 100644 index 6057b9063f..0000000000 --- a/mobile/android/base/resources/anim/popup_show.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - diff --git a/mobile/android/base/resources/color/action_bar_menu_item_colors.xml b/mobile/android/base/resources/color/action_bar_menu_item_colors.xml deleted file mode 100644 index 756463d68c..0000000000 --- a/mobile/android/base/resources/color/action_bar_menu_item_colors.xml +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - - - diff --git a/mobile/android/base/resources/color/action_bar_secondary_menu_item_colors.xml b/mobile/android/base/resources/color/action_bar_secondary_menu_item_colors.xml deleted file mode 100644 index c72ce0923f..0000000000 --- a/mobile/android/base/resources/color/action_bar_secondary_menu_item_colors.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - diff --git a/mobile/android/base/resources/color/facet_button_text_color.xml b/mobile/android/base/resources/color/facet_button_text_color.xml deleted file mode 100644 index 90da6e3247..0000000000 --- a/mobile/android/base/resources/color/facet_button_text_color.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/mobile/android/base/resources/color/pressed_about_page_header_grey.xml b/mobile/android/base/resources/color/pressed_about_page_header_grey.xml deleted file mode 100644 index 18533ff670..0000000000 --- a/mobile/android/base/resources/color/pressed_about_page_header_grey.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - diff --git a/mobile/android/base/resources/color/primary_text.xml b/mobile/android/base/resources/color/primary_text.xml deleted file mode 100644 index 59d5699db3..0000000000 --- a/mobile/android/base/resources/color/primary_text.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/mobile/android/base/resources/color/primary_text_selector.xml b/mobile/android/base/resources/color/primary_text_selector.xml deleted file mode 100644 index d21a60880c..0000000000 --- a/mobile/android/base/resources/color/primary_text_selector.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/mobile/android/base/resources/color/recyclerview_selector.xml b/mobile/android/base/resources/color/recyclerview_selector.xml deleted file mode 100644 index 6ecc3c5e21..0000000000 --- a/mobile/android/base/resources/color/recyclerview_selector.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - diff --git a/mobile/android/base/resources/color/secondary_text.xml b/mobile/android/base/resources/color/secondary_text.xml deleted file mode 100644 index 73bebcd151..0000000000 --- a/mobile/android/base/resources/color/secondary_text.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/mobile/android/base/resources/color/select_item_multichoice.xml b/mobile/android/base/resources/color/select_item_multichoice.xml deleted file mode 100644 index 9eb9e2178a..0000000000 --- a/mobile/android/base/resources/color/select_item_multichoice.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/mobile/android/base/resources/color/state_pressed_toolbar_grey_pressed.xml b/mobile/android/base/resources/color/state_pressed_toolbar_grey_pressed.xml deleted file mode 100644 index 41eb992dee..0000000000 --- a/mobile/android/base/resources/color/state_pressed_toolbar_grey_pressed.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - diff --git a/mobile/android/base/resources/color/tab_item_title.xml b/mobile/android/base/resources/color/tab_item_title.xml deleted file mode 100644 index 443d4c11d4..0000000000 --- a/mobile/android/base/resources/color/tab_item_title.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - diff --git a/mobile/android/base/resources/color/tab_new_tab_strip_colors.xml b/mobile/android/base/resources/color/tab_new_tab_strip_colors.xml deleted file mode 100644 index fd7a97b21c..0000000000 --- a/mobile/android/base/resources/color/tab_new_tab_strip_colors.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/mobile/android/base/resources/color/tab_strip_item_bg.xml b/mobile/android/base/resources/color/tab_strip_item_bg.xml deleted file mode 100644 index b8e7e298bf..0000000000 --- a/mobile/android/base/resources/color/tab_strip_item_bg.xml +++ /dev/null @@ -1,48 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/mobile/android/base/resources/color/tab_strip_item_title.xml b/mobile/android/base/resources/color/tab_strip_item_title.xml deleted file mode 100644 index f4fca97505..0000000000 --- a/mobile/android/base/resources/color/tab_strip_item_title.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - - diff --git a/mobile/android/base/resources/color/tab_text_color.xml b/mobile/android/base/resources/color/tab_text_color.xml deleted file mode 100644 index 51aba7e2a6..0000000000 --- a/mobile/android/base/resources/color/tab_text_color.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/mobile/android/base/resources/color/tabs_counter_text_color.xml b/mobile/android/base/resources/color/tabs_counter_text_color.xml deleted file mode 100644 index 945634b7a6..0000000000 --- a/mobile/android/base/resources/color/tabs_counter_text_color.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/mobile/android/base/resources/color/tertiary_text.xml b/mobile/android/base/resources/color/tertiary_text.xml deleted file mode 100644 index 54884519c6..0000000000 --- a/mobile/android/base/resources/color/tertiary_text.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/mobile/android/base/resources/color/toolbar_display_layout_bg.xml b/mobile/android/base/resources/color/toolbar_display_layout_bg.xml deleted file mode 100644 index 9a99e0858b..0000000000 --- a/mobile/android/base/resources/color/toolbar_display_layout_bg.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - diff --git a/mobile/android/base/resources/color/top_sites_grid_item_title.xml b/mobile/android/base/resources/color/top_sites_grid_item_title.xml deleted file mode 100644 index 89ca50294d..0000000000 --- a/mobile/android/base/resources/color/top_sites_grid_item_title.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - - - diff --git a/mobile/android/base/resources/color/url_bar_title.xml b/mobile/android/base/resources/color/url_bar_title.xml deleted file mode 100644 index 473bee41d2..0000000000 --- a/mobile/android/base/resources/color/url_bar_title.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/mobile/android/base/resources/color/url_bar_title_hint.xml b/mobile/android/base/resources/color/url_bar_title_hint.xml deleted file mode 100644 index 0c748ac0bf..0000000000 --- a/mobile/android/base/resources/color/url_bar_title_hint.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - diff --git a/mobile/android/base/resources/drawable-hdpi-v11/alert_camera.png b/mobile/android/base/resources/drawable-hdpi-v11/alert_camera.png deleted file mode 100644 index 11800d0f10adf8df462788a1352e365401eeb984..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 181 zcmeAS@N?(olHy`uVBq!ia0vp^Dj>|k0wldT1B8K8ji-xaNCjiE#F7M&o~Hkb|NZ~| z|2E&EfNQ>?3Zuwgo&U=fES8@zeX;++|GOT4{@*`g{9^yB|Ir$3|J4N@N_YOLXMYlq zb>aV+|N9+tPVq7|P0YTq!ZC6}iOb?Ce`iHi<{ED;+W++beUT~d!Hy<}R@DCAFO+%C gj!mi|(143!=?vLPQ!R|Mf$m`NboFyt=akR{0C(F-JOBUy diff --git a/mobile/android/base/resources/drawable-hdpi-v11/alert_download.png b/mobile/android/base/resources/drawable-hdpi-v11/alert_download.png deleted file mode 100644 index 1b8f59e567b994703fbb7291b998984abda0adb4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 273 zcmV+s0q*{ZP)7L#}Mp4}w+NJmQ{@;{M_8mWS)fHo2 z5V6J`@2+u@CHDF1QnJJkEMNf(Sik}$1z08vTXg*vI3)|GzXxJkcgK|c4o9n;{EdWG zOv``hWtLDgbjkndXN3^+^okB6Y~gE-5z%9e6d%WoiXS2_(K=;Z{L!F^Mw5nwVUlb9 zbIpW=BW~$i4O~m#YLMu^)ziGK`&NT~mRtB%`*sW8YTs_*TjkrQEqtqdJ7u3pjQQvu X43)uH3^;-L00000NkvXXu0mjf;x>C* diff --git a/mobile/android/base/resources/drawable-hdpi-v11/alert_guest.png b/mobile/android/base/resources/drawable-hdpi-v11/alert_guest.png deleted file mode 100644 index 650b8246d14757b767154baefb4458dd58455dd2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 761 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 Sh56I~0000{XE z)7O>#0V}hBIlE8;do)l;HZvrm#5q4VH#M&W$Yo$~E=o--Nlj5G&n(GMaQE~LNYP7W zXJBB=4)6(a1u6LdAB0x@XW0LrVJ4U{|31U2#|(>JFsypTF!wdXlD7;q?=k=dr#)nt z^O#}b6NZ`380I}^n0*fCF2l5Y3^VUD%y`H!9n6?^pJDm~ z5O>;LhMD&mrURvbx_~55@E%ApkOVP+%76%{0BizK4UjPd%mxX9HG-sof?zICBS`OE zurin~pa{qiur3fAEDO;N)CkcIN{%q*;I9Gu)dynOrufX&49_H|QYV)!ExsAlD=laeDlk1Mh%WY0v(mt(jhR3dD zPwj-+qB37M@B2}FEAP^RwXKZ17HWU%VO%$9;Y;r3EzbT|IGbbCGX98N+p24LBrDz7 zbIXs&ko$&vzq^R*vm4LlJsMar<*w1@|2~aVFHPzV=v{y1!>ta1=~s$61=Oz;H3`gL z@zmJqYw%%)L%kO^2U>K@SmGW$u|UYw-T&F*2Rdmp<<=ZI$r2&evi;T2F9KJMi$t%8E z>Ll;7Ib=q2MnBi@2`)AP4-J``YS?zYeYGof0Tain7vF7Wu%__YU0yHMwEj!w{0*nL z)b72w%yUpdw|nV^sd^WTo)p%;e##NF?^ygX#TQTbt+#RnZ$2z%>Py|QIIJ+FMxi ztK7C}qNc(|t*7k1>EyOe;9T+Gz?5xoTvin|F}IzSI}(>!ZE+=mZ6n`t3!zWUN&ai* zFB9q5y7HmNWX2<0t2FsEc@&>b&3Sxa_bC=3#dfuCTUSI*keJf*P5Rjro}gHxTX$cq zDA75gV&=JadsY><$I%I;>cVr^+)R1-BsO08rT4G3YV{^I=9hOa5)s&J^sm5%;p3v$ z+du8&<~*%l?^7tN{m83tbp>DeF=iRXLboGRqq<(ReJQQ)(Po$W6WCw)w!vCu<%}Iw v40+j4Bz6=zy!*u{S6TC5K*YD81M|9u6{1-oD!M{r|mz r#M3X-(cRD4 z!ym|SaPtKUg4rPuK{uZucb_1jGFPuaAad~v04f6_H}4=I7or_V0+EYn08r4~C)m>u z#CG)#1ag54AoB1H1|pyo%aWItK#SE&g8YIRE;w;2uL$}}9FGcYjnd%8G=R4~4s(wcNwL4-lEEaPtMrGNj`^~~g7 z-qF`<-^1QfKaW3gpBG2%k9~(~tg1UF_%_6Tt6A7@<5hNE$V=&kn7Xu(nA;s?zd+-} z0Do@hUMZJHe8~>gXUzf*r*ST+D+yk>=+L?5T8pI(Z+u^~WpZ69v(MtpekROSk{;n6 zGXJr6r?IWqq<=zF>cf`p?qiDfxci*1cWbJW)tV#5t(%ljJ4x6FcU)Q}ykOCDjTjdbd&kg*A1a3F0cAvWi@m06ALrN#JUwlX1gV&Hq~aFxpQ_!Mg(Jf zA2*-fMoHU_{4OcZ=90fhdEP)^c&y`+3f%7y zKTsMTest9eJ+X?7j{>iu;?s$c{+*{kT4Ouy1aG3?r5~J_PTR@GrIT>PyI*Mjsv? zzl64825U<3?ap=p7njEZA0y!LN~GhJ0KR4k*zRlNDU=ZSga%$JAzykZrVTuY!{Kl^ ZoKJgYS)?S0F8Kfe002ovPDHLkV1jY;tzG~C diff --git a/mobile/android/base/resources/drawable-hdpi/ab_mic.png b/mobile/android/base/resources/drawable-hdpi/ab_mic.png deleted file mode 100644 index 13f8eb356e5e1054cb9575a760f0fa5efb409092..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 363 zcmV-x0hIoUP)vJ49aBQMJ2e0X7)Xmctd~S1e%>s=!Zv`*FbYLukj4`xWGLgVJOZpl%RmAI{+A6 z!3gtgfhg;5+`klb%6|QgWOz11yDZg653jpY_G+Y_+8G)2AE+?Qg^ z1+NoP;z=%Ol8$QBU{|8w{3A{BY~w507+>*hnl#_R5OWkY;L3bs*EkfuHm55zF;{KI zVFq_PV75qel)yY`>o-;7KnD+a4gF!h1jY5U4|+HUif|5d(G#)Nz7I5OV6OlG002ov JPDHLkV1h|1p8Eg* diff --git a/mobile/android/base/resources/drawable-hdpi/ab_paste.png b/mobile/android/base/resources/drawable-hdpi/ab_paste.png deleted file mode 100644 index 58fb8a31e9d18839414c1966124ffb75eb531c3b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 253 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA3?vioaBc-sS^+*Gu0Yzs&DYW0&(%B7#Vf$Y zGr+?)*xAG1!#Bj!FVx*9*x54xNV<9jK5Hxp1*+yN3GxeOxOTdI!5WX9#vJJzVkiBm z?EDIpDe-i145?szd(oTgumewX;>mN$FCYKAu6yzjkKWo7Y846l{_-Dk(=Rjs{MYHh zob+F<2hHayUHx3vIVCg!09){0g#Z8m diff --git a/mobile/android/base/resources/drawable-hdpi/ab_qrcode.png b/mobile/android/base/resources/drawable-hdpi/ab_qrcode.png deleted file mode 100644 index 727201a2c79de586618bcd1a541500d8813a5e8c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 169 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaN3?zjj6;1;w{s5m4S0KG^!{+rHHm}>bY5m5{ zch+vu0Sd8{1o;IsY}vZ)R7s5xknig0;us<^H94TLurP&zN#X602TV!|GU9CFY!(Ww zkD1sM`h=Pmb4+X#oFSv~yW?4cDeuW3)sW{Cp2}x5%xE|h;Mme9%)p*16}|6dtTWI+ N22WQ%mvv4FO#rYtG{*n{ diff --git a/mobile/android/base/resources/drawable-hdpi/ab_search.png b/mobile/android/base/resources/drawable-hdpi/ab_search.png deleted file mode 100644 index 0d217f11a1ca6d00ed0a7cbf5fde3f1d94a74043..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 799 zcmV+)1K|9LP)ZopG)HZ`!JMZn^c`yIZeKo=wvC7!BW7Rgo+O`|i-Bb6?df(gqlgvrJ z_uswe^4xJkiwF@SM5y5=Dm%KKznBYkkV|!qN%f8kdY}Bb&LkaTf|j}!staCQtsIXf zs>X7uE+#*|Yu z|43C&EM$P*YjZnh$~Lr=-UrFjsufxSOsY>dGH^ZJ1TsH~=&f-A(DRyp*=le>uGg01 zX$xaiEz`A2Asbjn$BurTfVL9;8sh|TN$HISVODhXJyJotG}4xoxT=pk$brJ8gx`=! zdZ=agzCM8rmQmHD6f*b-${=4`{rdH*%_Qv)*z^2(cIC97b14+*;nDGU<2)lDbY9XG zO`ezsZdfMO%M=IAV~j&+n~V+Xk@{UW`UfsOrxd^qi!(5l6A)(5HjoEcP_9Q~K#WPX zF8d8l8?^ej2i&mQjce-TJVS;A?Z)|Pl>}~BykX(a(iM~qBA?~~!M##EcBzRo5SLW0 zj$>3}+Wc-6|HY}D@syf#iyF4UxIj{TJUz^N4g{kQO4Q5Y+)=<9r mGg-bQo_biS9`}DD7sH+thCly53pxZefx*+&&t;ucLK6T0N-O&S diff --git a/mobile/android/base/resources/drawable-hdpi/alert_camera.png b/mobile/android/base/resources/drawable-hdpi/alert_camera.png deleted file mode 100644 index 6ae4be078719c7e055fe569afa90f1f2a1699dd4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 206 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM0wlfaz7_+ish%#5Ar*{oFK(4=4v=VlcsYHt z&rzKn-mHq9>s^MMXJb%dxw&y-;Fz-5E$(QQz-QxT*o(-n2 zW96Smns*C*l|PeR%Tt^k6!bsybHXOOJ5l+~m(OTVtGnpeFu!xRfDq7u44$rjF6*2U Fng9YyR<8g6 diff --git a/mobile/android/base/resources/drawable-hdpi/alert_download.png b/mobile/android/base/resources/drawable-hdpi/alert_download.png deleted file mode 100644 index b55e779fc6703ab365fef964a5592cddf66f7445..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 218 zcmeAS@N?(olHy`uVBq!ia0vp^5p_$IFdQVTs@978G?lO@~|M11zxe6$yM zryh5>ogrW&{l!+LX6WK##`|&1%zOSG{wS^M z*JzE`s^9V-{YQT~ilX|9t*+v(`T3Vej)VK8>C2iV!Tq|rqKb@$b}`f?O}oojb2$*T2|RSyOazG39p3%#5FZzn#I; z=H{lR@b&)v^PbmukJsWY|DpfTpAMBue6fWE+%-DN7kgk|Diyv+n&N&94c7A6s#QEp zh&<=BSliHm$Mp1sz$ax{b2FZz)YcXs<(v?BjwInDib6^}*U}Onu(8qJj+dy_c#jbJ z`tUNXtK<~_N5{wH;R zd~I)!-#9rL9!88tgJ1A|Y6@`%2d(7?AxMdMLL40Mjmt}=kk9;l2(N9{){vi4mzR8? zHHy)icPbSWx3yK|s3dWYZ#aj%_`$z!lp3W*WVLg26BU%y+Md!JiV SIQFvu0000djb>&fx~kE6pA1@0S?CzYB>NF1qy+~RUr^vu>wMPw6Jdb z`T3I((*kTDL@5_mmT(T^wiU}ziV$3}tZBN>)U~F;9YW^$I@2^5gNQJ^%JjWqfT%1x z%Z#IGf{dy<%1o1OgB)FNntg^bLO`Bj48u0FEUpXBI1XcL#`obl*IkxxD2m{nDB_&+ zB!TyW;MG!!@+)1pJ=(5&9NIr4Dx^Yx9g1TvElm$Xo+lqV`Cp1cE|cf$><3T7gmtda RYKH&-002ovPDHLkV1llKX;1(F diff --git a/mobile/android/base/resources/drawable-hdpi/alert_download_animation_5.png b/mobile/android/base/resources/drawable-hdpi/alert_download_animation_5.png deleted file mode 100644 index f8d1a15d6c5dee25985be1fd2c5f18106c019857..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 270 zcmV+p0rCEcP)S68oM>rZS|YM3rAhFN&*4gX}r=g+kl zB76n{-#Sz-Q``DFURqqFwjjW3YBg%9R`I%?o+zTSdL3_boK}lUqNt|_A`=s7N~vbE zzaKK5cX&vlICdPUD3vJCZo4j2ZEo@n`}@Pg(9vk{6=^y(1wDgrZS|YKHE)XU5wdGd&pZeg6FR z$6NRW)VYv47kXK_Ol_;Hcy4~4+M)UF%%^I9z`Ns_KEyk%@m z2)?V??Cr&Sf?$82Z%R|ogNjm#Z)mrDAF9^Z`HJ1$!9nO~H24A`CMKY#zdud+gp@%5 zeZIfB$p?;)!w_a>XQjN)L=nt|;qfuA#4+r|F)vgquvaWb5gh0HWf>pHvfs7rbFKV@ Ub-`~f1^@s607*qoM6N<$f^{i+T>t<8 diff --git a/mobile/android/base/resources/drawable-hdpi/alert_guest.png b/mobile/android/base/resources/drawable-hdpi/alert_guest.png deleted file mode 100644 index 17fc059c3e0b3109814e481c0ba626e24464b767..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 878 zcmeAS@N?(olHy`uVBq!ia0vp^_8`o`3?vg8AG`%pdjotzT!D0aa>fuv^2LYl0X<+> z666=mz{teR%E>PxE+em^p=)SnWAEY>5E2!anwgznQc>H`+C5>~+@&kmuHU?4-=X7Y z&Rx8I_u=DbFW$WU_~qNLzt=>35*ZknlssJ=Ln;{GUVS?2mV-pwL-F&$hHpl1t$+FnrJ0}P25pL&?d$+kuQT$X<{qxg@;~P2x`xJE6fIovMP$bYBgxl7GnR-NKDnW{xJKt4XaeDr%W$o50(tx5qd;-Uq(}Sj6u3)V|19y!vhn`&isz6)c?9f;`|S@ z%+@W53%^gh5Y(f$kG(Zj_Jntk;J3)5Cmz4nNaD!d>>I!FgQ>_z7TfhA_4|L`yvK5X z#nd2&w;vNGZR7eD>F0Ru?X(3?UE_?3)Poyl-iu~F=NvlEr}~9ZX@%X|G?mcz-}e}= zp6wIy`P`E$ITtp0KH;%C|IB;!iObO+`ik{Dmxk9|`V``LmHpmJ&s~p$<+8fG=ISI& zofKB%6}Rz{^orAug44VT*r&hxpn2WvLbH18XH8wz6^BFmf%2R>8%&=F=ct}+UMxA^ zAZU_|Ov-1@axaz>4`;tPu)RX?{*jCI|NkDo$8cWB;8FU#lOKTTj=|H_&t;ucLK6T5 C`IDgl diff --git a/mobile/android/base/resources/drawable-hdpi/alert_mic.png b/mobile/android/base/resources/drawable-hdpi/alert_mic.png deleted file mode 100644 index d218bc88c4982fa80e2d27a643cbfb6bceb82020..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 278 zcmV+x0qOpUP)P8@lY#`BcmF?6V zrRASO&+ue5KlhxSb8f>^qr(y@d()p8eKwu+FTW>cQ^54P;DSY4qgUSau3FG9BDyD3 zY>%!ji*{W*!<<{zKl(fG*~(dYe7kh}SHE1AMX1g8ZnYVyXYMBNDwC5nnfN06`JwAa z+>9sc-N>(zJ?df{*=#F2>!ie*1&e3)9ORM$GZ)S5xwtif<5l#s1$VSFn*Ops?OWfu uXYxgs|Cc`*#@_a>RJZk9@z5)elkwxZ%~f{n;)j7gVeoYIb6Mw<&;$U3lxlkb diff --git a/mobile/android/base/resources/drawable-hdpi/arrow_up.png b/mobile/android/base/resources/drawable-hdpi/arrow_up.png deleted file mode 100644 index dfa79aaff4ca2bb5975e95a12c5d35aa0359054c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 276 zcmV+v0qg#WP)I*gNJW58DW>K?I;Wxhg1BD zV^xrX)>ny7VF&|5apat6H&TL}g99jm(y=GnIcrJuvoW6POui0S-#CYHr+jG$;yF;jVTCy8` av^N18Hm>?bC;?9Z0000E5Xp)of3L^z|sBIrM2nBkjWS}lOMXM+Uw%;W)~ z!F=cz;g>0VN#cWN;>?BoDB~1iUcGlCteXop5kwrv2w4Pk#Rn1On(_A|Y!gB{!j}44 uMyMu?A9EmKKs|T)A%aST2`@c8*X$3Nkl7cd2WY=^@oH_QT4m{qc%Q!ajk4lsuR zc!V4908e;UsvrwUfL(4f$o@*UCl&pd@foq_nt0}Ar-gY&T`~BWFX-BR_po; z-d&$;^zQ87sxVsEeb4We4ijUcqRVs6ee9d(?YTc;n?^ie{ZWg(oX%gWdb_rK>5=ctZ!J1C{pfb_+U$2B`TjKq4TT@=n4jtVTxu@Il@PP% z`lOu?*1Tt#pi<`@Q}BBM8xup-2gVPNTo^W3$TR4iV`u38%y{5Y4VbF|-Lve9?aH^ g^AHjQfuF{5i^MEaot!^A0KLxO>FVdQ&MBb@0OWLgV*mgE diff --git a/mobile/android/base/resources/drawable-hdpi/close_edit_mode_dark.png b/mobile/android/base/resources/drawable-hdpi/close_edit_mode_dark.png deleted file mode 100644 index 1e28f00c5bcace1d2aacaf738c4f01be0d0dc9b4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 234 zcmeAS@N?(olHy`uVBq!ia0vp^LLkh+1|-AI^@Rhejh-%!Ar*|hS6W368}P6M>@9m{ zWc*knZgtNmme%Sz&z;K+>e&6JWo?~x<84We#fzf-_UT70-jvzNzPq@xSWlDj?>?Z zmU%cs*6?|Yd$Y34X63S;XGbO(7vIom(@Ntwer{*L78b>bkS$iK97o?Bi@Bh=(7sVB ibYtp)*AL_r4D#-synD)w<&z}P@eH1>elF{r5}E+|U12Ez diff --git a/mobile/android/base/resources/drawable-hdpi/close_edit_mode_light.png b/mobile/android/base/resources/drawable-hdpi/close_edit_mode_light.png deleted file mode 100644 index 4c82e5696ef7eeedd4e0d2356b0cc76766431ddf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 235 zcmVO7}Z~J6UbvExEPrEfjBeHd%udQoMNu`nCjL| zo}1-OtFd|xA*`Jc%k|aoCtko%&Ec!&;w+Vif&qp|%g^=c^c)3*b!3pc?+MEYVC%Q%5+VsPU{c5^a V+RhEGSAb?Qc)I$ztaD0e0swKVDa-%> diff --git a/mobile/android/base/resources/drawable-hdpi/device_desktop.png b/mobile/android/base/resources/drawable-hdpi/device_desktop.png deleted file mode 100644 index ef5300abd6f90b0889e994def39963a12877c05f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 217 zcmeAS@N?(olHy`uVBq!ia0vp^IzTMT!2%?!SGzR z-tWiuH&K~f9*II7-V)Q)45!2@`eoh}ZToaT!HwCDBka#?#zpOn@(rsbWxU*{_G!t6 zdPxMW+psH{oOZ!e zKf~vWgWnVU`-#5=7CwVmRKh2~o?)O8$bflp z0J^{c7zMAvd)C_p6Qa*fuw_$%ZQ{)eKL{!s=UW5MM7>^cKt*q83R$j9Kw(pVoRx}F zIV;Ct&{z0}nx(!3X{Fu>SOHgH9V{?x2AZ}X7;SYlwDiQsVj^EUz83CHm9Qb%z-2~slbyw*a4fGvXwBQQnkY#N1GRlgBCm$*a!WL zRO_1ZWFw|?qB!hGh?RQ^2B<{IQ;BnsrK2xtCNpLeAgeBEm~bo1Z<*03@T3Zc8Qtni zS=KiDXUgmkHYof8>*qlb0k`z4IM`9jYFdY8QWXfw4GJG;6bd4R5znw|Pw50vZFa4J z9eN1FM97Vj635AbUrJd~m(Qyy@fVlb(5Vv6sN?U<<8QzDbq>KVLroZU*?U>9p0XBkNC*nX4pqJ zSOvpkQg*-yH^D=&Z|FDeSb=#Em^QL>2nB|e+lVoc5Wg*pmAL{>MZOgnxB!ddK=hG^ zqCU~~+>%N#nRCbJY9kj;T%iqNHsuBbMOLg(mB-4Y_)SUX%rRE7C(NcZ#0jmRN>o5S zM97ti+&ja0pr{)#W9W1V%r$vI_BQO1l5J9k@+s4;bIoEtFPl(c9NdMBa$~}D+($|d zH0QW<%6lp>9pcPz1oDPgkKt?x1%wzA@ zUrs@h^=!|yxyYYf)unC`6_9GjL-)IgAG&MI0(#p0Vm}QwoA_6QGRU;VTs+c+DIR7@ pB89#qb^cajok{SZ|k0wldT1B8LpMo$;VkP61PSJasf1u!rNM0Fo3 zX4PwWvt`FAt9J)>If|}Z&RgAD;`;wc=^lHlw0KTV$1b+a=QmZqtq|llX1!J2&9`jl z{JVSs5esX#UpTXQc4CKSnuyAZNt!2lbbSNW7HgcfxNUjk9mQl>ae>i~B4`mhB(uM7Q8jdy$1|Zo%$n+Qq~dr#*Wgz9+or i-pUo|k0wldT1B8K8xu=U`NCo5D3#Lp=h7v6oPsZu8 zABf!@(piHn<-R z71+GXc;=NZu74|R>P>e8nR7Xpx7E*FICbOqb}1#@GW!{N@16$6K5;#JH&<$s%F`0o YOF{1Iqc2VJ2Rebl)78&qol`;+08nQ^MgRZ+ diff --git a/mobile/android/base/resources/drawable-hdpi/find_prev.png b/mobile/android/base/resources/drawable-hdpi/find_prev.png deleted file mode 100644 index 4c40a1e8ab2d634a33715fcb192a7ee1b08b5e12..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 167 zcmeAS@N?(olHy`uVBq!ia0vp^Dj>|k0wldT1B8K8o~MgrNCo5D3#LrYiXtw75z?_S z`d^4iCe zIkM*ydLP|4+`XM=MFm@kPV?J)0rLNz{mu`cK7YaGY>$5qu~SlxF4pj3E^csbaJh2q Q4$x)>Pgg&ebxsLQ0IGRFBme*a diff --git a/mobile/android/base/resources/drawable-hdpi/firefox_settings_alert.png b/mobile/android/base/resources/drawable-hdpi/firefox_settings_alert.png deleted file mode 100644 index 7094c9aad550926d9e5d19d8841879aad10e916e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 412 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 MPNBM#JB6nF57?hh9{>OV diff --git a/mobile/android/base/resources/drawable-hdpi/grid_icon_bg_activated.9.png b/mobile/android/base/resources/drawable-hdpi/grid_icon_bg_activated.9.png deleted file mode 100644 index d2ca3fdd4345f7596104fd074bdf6a7183927f25..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 105 zcmeAS@N?(olHy`uVBq!ia0vp^;vmey1|%P7U0DF63_M*NLn;`P6&U}tC;w^+G~nvl zZueES<)iA|g}Fce2Ojq1`W$#zlYMc5$RG8Nj|>cQs}CvHFHegF>SgeB^>bP0l+XkK D<^3VW diff --git a/mobile/android/base/resources/drawable-hdpi/grid_icon_bg_focused.9.png b/mobile/android/base/resources/drawable-hdpi/grid_icon_bg_focused.9.png deleted file mode 100644 index 510d297ea6a0b2d940155b2024d1155fb40ebef1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 139 zcmeAS@N?(olHy`uVBq!ia0vp^;vmey1|%P7U0DF6f;?RuLn;`P6&U}tC$DM_OxW`^ zlSg1Z!z1GsgOFGrO}6=p%);9kws&*>ZdmZ}gj7R}*OmYBx7z;8o6dP-03<%ic>Ukh l`YC8)kpb5~;Ug6c3|rFcSt9I&)&Nas@O1TaS?83{1OOsIF3$h} diff --git a/mobile/android/base/resources/drawable-hdpi/handle_end.png b/mobile/android/base/resources/drawable-hdpi/handle_end.png deleted file mode 100644 index 9cb9d6fbb4c3417cbb9e9d0dc2dacb9de42c5a5f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 570 zcmeAS@N?(olHy`uVBq!ia0vp^PCy*a!3HEZ_)k6nq}Y|gW!U_%O?XxI14-? ziy0WWg+Z8+Vb&Z8pdfpRr>`sf17|3pIEGZ*dV9;(i`h}4{o`{} z*I#e9h!wHvD>UuxH9P3mS=Ah|Lj8kwSy<*Jb`|jp+AM1(Du05L96``hBXJO!+H7D@GR^GVNJR2R( z^{@q>n^_>P7x8<)q8*!I)xyJ`98c#I-ScM{=|dv7Y7{+>TLOG`~XWS7gfuvF|Kxoy!tm zY`p#UyIkO79Z!zs_rG!nURYbB#pKMjE3$P)ZFxqx4S(+X_p7zxf-d)%eW#6E92 zv$af>yN2nJ>IplZ*7aQcarrXit$E*DPB}BQ$eGIBP}=Z)wSs6J?|PLZ(>WvAuBG43 q_CDe}HEG)vzi|gW!U_%O?XxI14-? ziy0WWg+Z8+Vb&Z8pdfpRr>`sf17t;`95!#uNwq9W8*4))AJ#KZ>GN&JDWoEr-p7D6m8rLS3 zg}FTMZ$ERpqV;NC^}QGWe=~eOWc++i^?8E{6W1HQv}N0OOl4-|IgU9O*wq@o&v`iO z?!&wFqK~F?+)wRIbW}>|Pi$05=}kPSl+vBZsGQQ7=%{=}?l6=43ez^${(xwn-|R|P zgcb)@wv-+dc43(Dpk?2BV>!E)?~+eDdRYYPj#wle{@rsdPB6cckgY;9DhR#2f z^B6vx?)`t}ck;S+?uX891@Ep`S!zUlR8-y=c0@XlG1&C}8&SM`vey`YG=2BxluE>yJ?@gCUJU?>9IvE7AIsL zDR_1KJ)`(7xg}%6^q(8c$$&!)cc0S4?H*h(6e{jN|xIn?tP|^mbctJ5022YTTR< zTjt!Rk(`sPqi&}#eQosF$x4wN47*PYCukl#rO~kT(`Vr?zF#}u8@7Mud7Q)g{JeP2 z?Q4k>-m#iq<$7nZ;H1umuhZqXx=y}1MR3x(j`{m8ERT7n{k`DUjr=nuGLJ=l!*Vyy zo4&Q`ztxvH+%;)@&$-UIN^L1Pmt3>xf$}l-9>wPpH_pDfUM+|nOtk;V>p5@xFSa95 RQ-SG=!PC{xWt~$(699CiDd+$I diff --git a/mobile/android/base/resources/drawable-hdpi/handle_start.png b/mobile/android/base/resources/drawable-hdpi/handle_start.png deleted file mode 100644 index ccbc1767da1d0851169a248e7e01388532242bf8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 581 zcmeAS@N?(olHy`uVBq!ia0vp^PCy*a!3HEZ_)k6nq}Y|gW!U_%O?XxI14-? ziy0WWg+Z8+Vb&Z8pdfpRr>`sf17aSW-r_4d|AFXcdy_K)v< zShRK7bnZlbkgQ?kT(bS*F+cNn`y6%7NV_y;9?|ms)$oJK&iR0qrpRyIFL@6)Y)DeJ zEPuE6!~d@{6YcoQpPf6B5h<~m{fhx#gZu%#4a_00Wv5M?KymItKrxu5!E!s@`H65l1Je;PRw}Y8a(?v8@=-(7kMK2Dun5kk1 zZCxZT>!$j4h$)?U!QjmG)Lc-1&XZp~7xFp|u(Gb6bFFRfq~!;Tm8wL}sUJDc=A}3* zc8QZobY?5Vt0UJx&hzfLenfFzG3Q1vnWU-S9jT85PTM%K$W2Zwu-Jdy<&3cZ5u0v- zYtK6mC|xwsS#v(vZuUC9>z+;)bFb^WoZa$uC5O(6nQ5Yj`YM>&bk84NV43eG{BdL0 zo#dV<&xD>e=l9-!xM#U(K;hw@9r^|G7SB)Q{B?h=AjeYNX~%NrV>&$>{>krDTGe}U73N70T<+*UEc#$R_? ztMGozblUf;-sNu2)9?eEZd#wQohRYyf)l8e*E9Y-pPgMf$Lazw!WleW{an^LB{Ts5 Dzy$Ca diff --git a/mobile/android/base/resources/drawable-hdpi/helper_readerview_bookmark.webp b/mobile/android/base/resources/drawable-hdpi/helper_readerview_bookmark.webp deleted file mode 100644 index f1f22df5c271423b15ac7295f0dec5cd9aa3b34f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1710 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 diff --git a/mobile/android/base/resources/drawable-hdpi/home_group_collapsed.png b/mobile/android/base/resources/drawable-hdpi/home_group_collapsed.png deleted file mode 100644 index ca2f764f186000662b913360a5b52d5f74c8eecc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 228 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 U&gF~`;{X5v07*qoM6N<$f@1^kkpKVy diff --git a/mobile/android/base/resources/drawable-hdpi/ic_action_settings.png b/mobile/android/base/resources/drawable-hdpi/ic_action_settings.png deleted file mode 100644 index de96174a31c50c05ead4ff960e05c2d0124ba4dd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 446 zcmV;v0YUzWP)J@)*us&?xhg*9yMcude8B-OaGFF!LMpl8))MIk&zC3%$-u(L+)i)7{cmX4 zEDUL6aDRztv;hriW@req&b9-Ab@t4oTT*nR4;f{&1sv8bE<`T^h#Y(_5KBTEY`$WI zonz=kuK4B-q*tP76bLQ}Heft@^9GGiu%sJJd?@am$CpvTCjsNK5bnTjj!dQXj!`K_ zsh@aLk+?3}h1PriNe{kB6?J6x#x<}wN_eL}76hcpuo+EIA`zsoa zw#kIPq`2x{pn-jKmi9h^hJalg`D8$}0Z)d^&bsVQ?~Lvv^9{?@C-__phP`2dG2O77 o16<(D{>_m6D=3q9T?Q}J557mFf>y=<;s5{u07*qoM6N<$g0ASprvLx| diff --git a/mobile/android/base/resources/drawable-hdpi/ic_media_pause.png b/mobile/android/base/resources/drawable-hdpi/ic_media_pause.png deleted file mode 100644 index 3f4f2b31613e6918a4d987dfee383e046839ed78..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 107 zcmeAS@N?(olHy`uVBq!ia0vp^LO?9R!2%>>V^VhkDI-r8$B+ufB|CJd diff --git a/mobile/android/base/resources/drawable-hdpi/ic_media_play.png b/mobile/android/base/resources/drawable-hdpi/ic_media_play.png deleted file mode 100644 index 172dfd9d6552323613dc20814f136f797a58effe..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 230 zcmV5*e#BDsGWTL=zD@nu#O|4vGHZ4hbo^tnXQL>YbCH-}?+pT+wGV zL;%>PS_S?SV4P$TXgQ-v23i)#a|?t=`td}dWrrHKKnO8Gnh3O9(<}omixj&B!Yc!K z1~(>M1BVToTUzyu5vIx3%j{97&-2L;3HsnGlxhj@>D2CtGEa`S*&&VExIV^6)X%j} gg?g^8umMF0Q* diff --git a/mobile/android/base/resources/drawable-hdpi/ic_menu_share.png b/mobile/android/base/resources/drawable-hdpi/ic_menu_share.png deleted file mode 100644 index 66a0ad1983cf4823a1dd1d565a6495b00cd09b7c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 494 zcmV@6yh8h-Mrs<|-7OurxNP2#nY*WV z@$buSVIiIFzooog)LYNN7?@BRZ4dOUJ+5P$RLY0`Nqgt&FamWHFw^KzXg$Y4c0m;=q z1;`IQ@do{9TC%k$5X}LU3rTq2c$EDBFy#xZSgh}T{yhF?VAXC9OaqcbDFy)Yu~wI7 z6&XeIKGp@^nHC)oU(60VT67*cdg5FCp35~KxcT{&K{-g~E+C(26E6kqEeGPuN!}m1 zAe)viUp~TsWKv*}w?Oe=vl*wFkxY{J69B#0rIC!bSLr0?;OVUdB?j%7A;yd)Q7mXtU))am-cu^yvHNIG#~lx7eDsPK>G!8 k`C9|VX4P=*Z=?k07*qoM6N<$f{4oAhyVZp diff --git a/mobile/android/base/resources/drawable-hdpi/ic_status_logo.png b/mobile/android/base/resources/drawable-hdpi/ic_status_logo.png deleted file mode 100644 index 4cd8e3c07123f4599ef408db0389e2b63d9eba4f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 537 zcmV+!0_OdRP)Mhj=#Mx4wx|Fdn~r*3`((pBy(u!_lqV>!;ZXCDXWhyxee)u{7Vs8suSU)PXBUJ{j znKa9MO}b)l&zX%YY-6FF97inxQj@)Bq>9QaIhDFOe{(`N6YCdIFVt7)q8~Le8v!5s zMH7<@RTK!~R1-VOaR`9MWM^}LAk8wcqjnyRLj#3w;6Xc`nKI=`abCxM<(=R`oSNyg z5kP%|acN$nB6#4MWhO#9kXC5eD;g90oWgYV4Vg*vK-#HdN9CP-ooyWQNgu19(uq$X zZdI_OU;so*KmhrAW-h-0@wki~(QXJux{;YnmjSJlv0Jo@ZqG)zgm^$aK()-o>Gtx+ z>~s^*!&5k1=`B4L(ul`%J$e|#Yf9MTTe21JQj;KqD}i36|0iyrh#k=@YErBLxL_B% zW<|*vT9Cp~C}6Z`9TH7cq@(-^*W_I89BUEw%b(y53eUa0L~qD2vp9ECz~3`LfkU(o zckx?c{!M%oc!`#v*YNmj&h7wV#7D8$gJI@GtQUNZnZ;w1k}Ij*>~-c0wLUpNL6LL+ be*EVP1h@O8HfF!Z_lpfWipg;_;_t` zK3mv|ot%2lJ*^m`H@MqATeiZg(Xv-1C*bar^OscwejS~;@5%D{ufCRL?zT7a`@k_l zpDEA6d3v9$%3)Efaw})98+SJ9%dEG&%$)SlBFRJ6e*#y|tBRE--!v}^23`Df`b$Up v)53&oq`&HV_Fe9SEkE?no6k=Ix{ATm)z4*}Q$iB}s>V(@ diff --git a/mobile/android/base/resources/drawable-hdpi/ic_widget_new_tab.png b/mobile/android/base/resources/drawable-hdpi/ic_widget_new_tab.png deleted file mode 100644 index 48a966414004fdb47933f7af3e76ec071071a48b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 173 zcmeAS@N?(olHy`uVBq!ia0vp^Vj#@H0wnYHF4+L2N<3X0Ln;`P53nq4eHwqdep;!4 z0Q0GN)Bdv`x}UkmMMJNiIqZ*pBlG%Afk#s-pYorMS2(<>Uf^h|e&7-LX-^HO)HZqu zwxz~PoT?LC`$OD0v;3y~qACA0-?HtpYE(roB|^JcPC zRSNqHn6XUfNZ<;q#!+Jp6eN_XjHdw{-dbi5&IVfgqb_IrzDf}fyx{X7wTt$-^5w&XGeSXLYlRt1Q+JeSJHBKKbjTwG0F$t2o2 zgE{>L28)&09z8fRS>#+Ue7*;jlV#I( qYwn?APb$cD_#{b*UV}aOj4yM%S@$U`(UuPY0000>^=bSzxaO=O`^NTU>j_k`@YWg6bwca7r{m{d`haVPL=oHI6{mgWH7x&+y zmk+K?x-9>bX?j)F#ls6PUEmHrG`H(sNP*gt?@ktzetelX|3L2yyRL^WMiOI|R`E*NYUDmp%Ox=_8xbvpES65usJ+X_$DD&dEzXB6O zOuiIHFZr>r*upF{?}Xaq@Us&0HhSJV>iE9;i_N_I?1j_qvO4mEU91+p6kDFK)$--3 zXalKB*B^BE#GJS>aq0V(^LkGXwZ=yN()t$Z8h^l6CFAjY%_QerK0Iy?x0p7hUAp?9 zufgQ(1I>1piUgB)#wN}@^B2k{xvkkH^fA`acQH4#_(x!1RxnfhkODtYm^#Xv>sCt^gNygJEfZ#( zoF*w`d-IZW(xTGo51d}Baj2NCoq3DJbN4-I)f?+?2Yo!T6N`NflN1VznwF z1owyJXmQRfZkLMFvYWzl-qPUTVlKZ)cXdC$y}z(c=V0ti|24P1$(}lYAX?Khd3}=e z(QlIuJ+k=foFn{8Og(G+_9oqz8^Ryuu6fWZy7#eaSXj~4y-oU=8}1vuS)Jh3#>#ZM vrt{T8%?) diff --git a/mobile/android/base/resources/drawable-hdpi/icon_home_empty_firefox.png b/mobile/android/base/resources/drawable-hdpi/icon_home_empty_firefox.png deleted file mode 100644 index 2a490d7ac86167f8d699587339f3d85923288d66..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2579 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 diff --git a/mobile/android/base/resources/drawable-hdpi/icon_openinapp.png b/mobile/android/base/resources/drawable-hdpi/icon_openinapp.png deleted file mode 100644 index 0527956daae506f18e2baaebe9316cbe8308c9f9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 307 zcmV-30nGl1P)vj6o6rgCPs22^9=e5W*f<1q51|sgULrwn~{x;Kr>Nn1R{yXENsU=z{Us{;qJ~L zXZs3_{csp8=K1;T4}paYhY8pE$Y4#4KTu*d@`hh{=sdNDH*TF{HrCiSHn2PAcMxoK zSAK%vj082Lr^XXUq*`lk*Vcwdy6U`N2N7hV2+r83jTE9Og*|4yA*4~lgNs}d$>G97 z32B6|z6l4Aju))vT&xJkz0XNHf}!XV1*9iI2pJF}f%Ftu4*hY&5MLF(8Vui5;mlw- zQ-v#o;Yt;DOj3nQlT=|bX%5ffpM>wI4Mb~P7W4eod;n6nxU5F0dfWg2002ovPDHLk FV1l;-fLH(k diff --git a/mobile/android/base/resources/drawable-hdpi/icon_pageaction.png b/mobile/android/base/resources/drawable-hdpi/icon_pageaction.png deleted file mode 100644 index 2b4b09003c5cbe8917c1e5a6112bcfdbc3ecaeae..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 152 zcmeAS@N?(olHy`uVBq!ia0vp^av;pX1|+Qw)-3{3@t!V@Ar-gYo;Bn;pdjFKaj&bx zp4qOmV!Ui7Cd|I9(*3sVen_F6kEKnp*UJAP=T`rmcQll#pZ|g1gD1N!t@gLKYMcw~ zE1vS$@{vsJaphS&M+*Cj<}8+x-)9%Nbjqzo;;%RT{Jg2p=L^s(22WQ%mvv4FO#nad BJQ4r^ diff --git a/mobile/android/base/resources/drawable-hdpi/icon_remote_tabs_empty.png b/mobile/android/base/resources/drawable-hdpi/icon_remote_tabs_empty.png deleted file mode 100644 index e520d2db85f4d458cf1f741ca754f6c0c62ffed8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 742 zcmeAS@N?(olHy`uVBq!ia0vp^?I6s-3?#WNcUb_bkN}?$S0H`#!Y%5e^A3eRKyzhE zg8YIRmU68VRbbuRZ#qv}AftOz!!-TsB#(lY7WZ>@S7VtEu>1e`d4gnY-l;HZn*SHE!krPQuD zcQ5~u-Qmuq89aMeT`KC^uyOCg?BEt=+pvWdMR`T;@Cl+302$&F@-017;XKL1SRz-`!MRX_cjV(tmBpFYL zp2}!!c;rUUkqe2!!iRaL90_<@5p-n1hb7w2%CsIue3&47dRnz2AG3gWQ+r2*k&Vg` zA+6)SMIL-76)jcTpG=UM7l zkvRF$qraU;IroYM|5^3s7t4PU#g>fcn_nm2jE(dEefj(RIRBrM+VocbKKWO5srO5U z{KR+Lx3=lW{k>QdSO1~%db06;xyRhG^FOt&z5esiH~pG<=WR_kX8c&yWcU36_m%hF z$R76Sv)`om=kx27dwZt*%ZTgJ|MEv7UMz23pZouQbw5<{!{bU2hHYmzD-fMriI~A+eo%IHouoygD{an^LB{Ts5 DSWjoV diff --git a/mobile/android/base/resources/drawable-hdpi/icon_search_empty_firefox.png b/mobile/android/base/resources/drawable-hdpi/icon_search_empty_firefox.png deleted file mode 100644 index fd271d423359626079f8851c6ef0483d0dea6294..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1741 zcmV;;1~U1HP)1r5x1x& zxR1c+9D(^<%^f(*J=nk{oQ$9Ooa$_xC2>+w_^aX}e8L3`nf`6YU&ux?DOcjuE&K*M z8M5_v1jCtvni6|jRYEt+Wyta0cJ)eZeHzactYOIQ9}e>UjQSD_g4bZ0=?A)r^Du@J zIghJxuuP%bls`F1yd=E_zt9f2jNhP#an}ER6JP4UNbsM;JMjnxa*synG@dD5flIh3 z>L4RkXSA?%9AA&mxg&~=eegL7Q0inFr6gwZP=xVa_=soQ4p|*IGg2u2NUnyiS z`s2S6Q|0Xlqr3TVRxPK&3-@tqgwUz*aWn3*K0QuDF4q@+1b8FD;NLu^^sv6B{(3fJ zL>jk;Sv`<9?b0p&r<{EBP3r0bk*8xXL&)WxyzAx0-BLtyHKw5tHX)Z^q}Q;WO8B6Qh?D&-Nt1FuN}+U;sBKsB}g4ow-}e2iTta0 z=H^M#&Gz+0X2+X(Q4cP|x^S6T#~tbp(UY81G9Tj|iHdPu@ezehXd#a9*Td^<&oj9! zYGArZbf~Mt{;T(bXQ&K(j((WH5$MbJ*^9k!xcC*Sl{hA=A?IKfALhJBnOK$6Sm*w9 zn>l!bxB@M>f+5U^nvq6mf;`@{C(^jzeJ;~yc%!341OM17y0R*ZYxuC%{_5mv5($Uc z61m8WWkIV1&e4s4rzH{$)l1TaUia`$t;kKSDn*LyYfmI{Rd?|KjMci~!6>DLgF1Tz zU!Xw)LHOV^Iyzaypp(`OpGj!~=jTO0w}0!qRuP+Yt%5BYk@Le#i-`=m|HC#X(+Qtz ztnDvl4G%En$=~1N0M$ktEgMNO-uRKbf~Pr4k;v8fvN((L3Qr^+!b9Q^%r(tw_|gN3 z)76#Y5Ph~oYVm4ro7?Gv%vK5F0={V?l6SiF;qN((#WDF=MP+EhdC%a**CmPw-*w@` zoop?xU~6^l(Myf}Zulf`LBToBMXKAxCAgVe)rqAvvIX`E+CcgXEyh@(Vh@k8e#(aC+sv!ETQA7i1YD>2%ExV>0@ zG{dO7#5Fu%S`V-`HrSTDe z!1=cQI88i-D@?{)D7fCX$O+;pSZ*@jNHOlUKWUPyiHDfY34EC}{@99s=uvi+X;kN#=w>p`3K7A5>K4aoaS3~|c45rKL-;Rzrvh`!Ixvj0@G3qv z@#D97TOxr|T$Q{>FoPp71$#AvZ1JBU8qfg~u+YSQ>%5kc#GCK9y83%bH1K^KHrAQPNA$-PramdBjfpb|~;`MuPCN@epym_nVB9{cO5fkvdsg=zo zN|>%yxWiK~er=(<7LS#tmY$Q6@GH**PO`Q*vl94>v%=PFj+V3AOP5smly{0n*xSMg z$>F{0^|nRsS9fGJR9Es=b!FgOdkuo%#a2d8kB7Ve6(`}v*=q5+M2@#4(jT$3#9l}F zM%ihbsuQDcQEUEgmvHcw!>KZby#nW2D1_tj7KdGz$QLYc95j*03Wob!%<8cSs`DhS z42eJHy>gFbo0BC1x2>z)cn_ybRCo-}Rv+88_=H5mXvD29qvUM$igB+xF~6aQnt}6? ziodW5KiQ8RzNQi#^zbaG@5ez`?|)Ua#Bl^e75|6DWqu7i3h&=yH}g(BIt2SBrlxH;J27XK_dz zoApoiJ98f{an#AVwS~xkNQN+)8~WCmN=2 zHXj!X<(ci)A`^gul5C)~eBEflIC7-*5=7-iNt-{j0%&v)%Av&a&j4`#qR?!FDq3mt zXch`Zr0{8p&D~8b7J#f37|np^ww6lc zla{wq3rK|$5}RKMHys}>}U^#y%aNx0&U%Z{=-L9_kbX8UM>iAh20F4Slv#%w;c)EOtEM>po z`s=Sh9h7LGvH2pAfZXzn zzoQ#R@#j9a=1e>$QGg)75X;HK@j*$OZ;H(GIRm~jD`6fTXFwkEi@zhF2VrG3*ZVBB zR5T80c?pk;GawK7#oq&62^t*Vz44f&02-}ybl7licVcr;gTOxrhG;Pdh)a)H=}nLqbNDj3pos`NiKu34JprhGnqTTH1>L2};!OgwLA`{X z?mm{~HBeh6Qq>@qGaS3?IBK>Ogkz#qG}mAO1v+Z;-pOHQMPj)ve_cklg@i8K3K z&p;yMUBw#-gT#y&f1+fo8;aN#WNB37AM?fe7id7VZSALnm;xmH5;9n30Cb5i| zxZtRIAnAiNm)K=|4u!?Z(QSUeEvm;NOB?;K};+fhq00000NkvXXu0mjf D+Kx0j diff --git a/mobile/android/base/resources/drawable-hdpi/location.png b/mobile/android/base/resources/drawable-hdpi/location.png deleted file mode 100644 index 18c54718a36d0b7fea62912a88379b622687d094..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 851 zcmeAS@N?(olHy`uVBq!ia0vp^xDIq&@`rgt!9f4V$;F-?(MNrY&2y zZr#6s|AviQ)^FMjWNg~Bb@S%^8#iwSifq`ldHu%CKs7*>n>X*?wrv|k3sCU?|NkJ_ zO&}A1X!GW6Teg5q*t2I3P#H+erY#W0=FK}cYy_#?xEW{^NaIGJO0Wo!yL~3EL`q{rE$S;_o;=>OKmMwdEB1KA<9RFGVOpg6=-#4aBXa3E1 zQ#vNB{QIux45R5YQ(rLpy?nKv|IwAz#g>OvTpT68vR>!N(fX1xODNCg&$JTbv^%$d ziM{UUKIY57zs?i{`KEHB|WA;x$3{Ur)q70 zs@b!*eoI4MZh0=v84-H=u-^4uhl@nj7Q1z>3QA^~8>O?rPhhp8Qs5F#L&l?v{5Jn$ znviRrdHGJ;6Yt4oXIWcZ=IVWI@mTD!J&AXrMC}oQX^Wo7_&?qJH^)viyof(LOCvQ! z;FLzGDZ_%OMJjU-N9p-`uH;(a2mQ#m+4Lunzh6OUZ{Nt z<@8iNzPoX%NGw~Rq~;0BTOvUhJOeWpJ3llo`6ObsBH)CRy7$5+d68ciUIuF{kQB( z(uCOi7oI(nnh<;88BWdM}e(s+s+j4bOtF9&_C#-WX=_ ztDR>Lk+nJeqI8kpdr>j{KQCCrSeLr&zO8t@ZF|8C-=BF_&6jr^3P|PQ|33Rq`e(Vg oWA{Dk)S!%=Mz!%_X7`>BL4Cfs3~Jlm-UQoM?R#%0+q~K2 zf4=$Vw^qHVCu34wH8_j3g>%YcnKF1^8Th0vKF|g$>E{MuF#zC}>nWYT&8bu>)pA4vfWO9khkx+T!;BR4~}Wsbn(QHU#o~&8Puh z&FFz&BE=%&KxObIHOr30TeMc>jHE<~wbq@WExuv{(?azKn=>*htd7TfYYPuKsDLH= z7y$a_hKvAPR#w(nSt#d%g0XPT^SlN*BK_2k8gPyc)!?kmq2Y36@qhwnH+{}&23cGe z0!cC6=+TW?$dZzhR?5JmESl7&u(1E%4@2<*^!vN0&TXK+$lL1(j#SUdtHnr&Q zGS#x@)so1N!OcM`s4)Z1zMSeA*#FNNri^!W_S9$p9jQu6+G&el0#?_%uIo0 z6m4-w-b1-aW$-hN8wrTcJ!vj#gGN~>{TaL)1Wo^2iczNY+N{ZsnMjYuO|MIRQGe8D zXkV>vj$W)y6q20qFhEF>eckc`MeG9nAfh;$?$O7Ea^ks_e<(k{Kc%RtJ2rehgM8PIgB zWF)k07*qoM6N<$f)Pq;;Q#;t diff --git a/mobile/android/base/resources/drawable-hdpi/lock_inactive.png b/mobile/android/base/resources/drawable-hdpi/lock_inactive.png deleted file mode 100644 index f1bfe63cc59509741cff1ab85942eede4967324c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 535 zcmeAS@N?(olHy`uVBq!ia0vp^xDIq?QKwgt!9f^&2;@-?VwdrY-yT z?*}s0Z``te)0U0fwgE|?)P~JlH*DIxapN8!d($R};1;0B=FK}cY}^8r1uEOIV+T+K zXaW#y+O&1U)~y>iZr-+S8%S{DmNRG0tlz&MC<`@= zFN}6%&lFnvPcu?CM`q7PR)OEe?DnM_s_vD%)aVpz6J5BVOZ~H)04vah*PbqpAr*{o zPrEuDa}a2`*me5-yLU~_cmJnFC_7JTOl*FpyL+FT*UEr3zc$9}ih2F;SRDCmbJ6C; z?wX*|ltjLgKy&iV+X5YE5{YG;hUq5$P3XBClO+7f3jn zE|E}qrP|6p)qA7Q6*1RGK8b%1TCKl(_sX`n4TtKwl{Vy7?<#KGZ0q}0zreFN{6pqB zcKgS(U+p>fde6D+_GdCKCgO@cnOb~}21OZ%I-Tw?PT-m_cU!^xqd;#nc)I$ztaD0e F0sz4+7M=hA diff --git a/mobile/android/base/resources/drawable-hdpi/lock_secure.png b/mobile/android/base/resources/drawable-hdpi/lock_secure.png deleted file mode 100644 index c1e95f3d8651d36a0b4895c2047979a9591c2aa7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 535 zcmeAS@N?(olHy`uVBq!ia0vp^xDIq?QKwgt!9fg8e=P`@Qn_c{S`0 zE!gJ`WCO{fgMRs7M!|kxAfxJpA4m|$-scTe2_y>+`4;R4D)s@27aj`*2?Et0^e#By zRd~?9aKBI4{-AuIf_>gI&!iRX4*(ilxZiuy?cBnBzWE2dfO-poMuAKKs_`n`?+ese zax@Ugt~>;E21J2R{t-W*$vz@=JV1wtaiOtV+ckgraS{bnB*T#5VF|QvUizAtqG5l=B>CcBE7;)U#<{!GTjL|m~aQ;Vkg AzW@LL diff --git a/mobile/android/base/resources/drawable-hdpi/media_bar_pause.png b/mobile/android/base/resources/drawable-hdpi/media_bar_pause.png deleted file mode 100644 index 46e838347b51bbbfc46eb591e462afc811efd633..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 166 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA0wn)(8}b0DTu&FrkP61P)3!4nHsE2oS;*n< zod3}9#zN)mWtUb4ojdpN{b}|B-8mF!dyA0swvF>H!Y0Y*C>JfDhEEd@6jd>m)OHz;c~sjq;!YkDK{8V*Jqqq;?{@ zfMuCzP{1J%W$~%uj~*bk5zz&F<|)$zz~WJ@2}p6$bpf9#O5Y0X@RBSZRhj@eppj%f zK#IQ}*y5!*Af^Xc4*7447GU2zj@aNOwaHA-0PH(=2-bKij}6)5bO0v@8Xdp^4I~<) z19&nh1Bm5E7Pk^(^nhj=lp27=Q7eN|1F)HrSpo}dF*4yiPf3NT8( i(TZl`B%83XFVG2qLqPM1h)}O0b6Y==H3t39lgb z>AjQI`*0BYtO@5=790zunxWtzqgGE*OgMMK>I54F^>UJ0pD~d}9F&EaG}g-MH%P-` zWkTy^VyUQphb&lW^^{iRtX}3Kw3HKVfu2>n&@U(5}p0zF+ELn;{GUNB{34rFjS7$^`R zT=Ml` mUkpAj7jI;L+yAb9uCj~r-kUmiCQAVgXYh3Ob6Mw<&;$Ux>MxZ5 diff --git a/mobile/android/base/resources/drawable-hdpi/menu_item_check.png b/mobile/android/base/resources/drawable-hdpi/menu_item_check.png deleted file mode 100644 index e13d6288afdc1ef27fece55b24fb4448f4bb7537..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 502 zcmeAS@N?(olHy`uVBq!ia0vp^(jd&i3?z4Pv802j0G|+7APoc!4Gp28p$-iVX=!Od zMp9BzbaZZHT3T#IMod~-TwF$6N@i?gT0%-@TvB>m5`>YJn355noF1E+k&v98kdhvk z3RIS!ker#20uqT&&WHzbGvbrd<5Mz0jFe2E`go8oARD9?BnU!a#ZY!;9Fzf(!c+hi zXVtQ21v=ffB*-tAfsu)sg_Vt$Pee>oTF>0V(#F=&$=Th*%f~;ewWF_p!j$PV7A{`0 zeBJsD8+Ts2b?4rLr_Y|hc>DgxuitmdKI;Vst07?`aX#fBK diff --git a/mobile/android/base/resources/drawable-hdpi/menu_item_uncheck.png b/mobile/android/base/resources/drawable-hdpi/menu_item_uncheck.png deleted file mode 100644 index 2fa5d52513cadf6ae274cef6d69d4abefe8a072d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 254 zcmeAS@N?(olHy`uVBq!ia0vp^(jd&i3?z4Pv7`ejj{u(#S0LTc(BRO}5E>eqkdg^R zi76TJ$?37F841bhaX>N!#E4JMh)+pR0FjwsHc$-;a@lY%4``NnNswPK0}~6Ig{_mb zyN6F}M_>Ph>E|w8eevckLkpWRP>G+Xi(^OyWAXyQgp`NRoLOyhd;%=P1Pe)jX91|T?&9#mz>$QlzZ+i4+$K%*3%v#5foa+{Q|0m{*iPvlf Yom#$>>$ju2fF?6|y85}Sb4q9e06+Iu(EtDd diff --git a/mobile/android/base/resources/drawable-hdpi/network_error.png b/mobile/android/base/resources/drawable-hdpi/network_error.png deleted file mode 100644 index bdaa961d341370eef10d82011f0aefc08a2a3704..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1160 zcmV;31b6$1P)F@?8spCizc6}}R8aIEKFtk64~8k)JLrJo&C3Qfsm}gc0H}EKY-6Br4<6VU(I; zWRa_=GCUM~KLW1*5%`kC0cd+?;HDai#Y@QD}$ z=4=jbnO2;|D8*N`)=DFowL**$*{p{ORSzHvQ7zdIU=MaerkexMRGx(^wWPjT#qwOT6IgiR6k?zOoVzP~Xz}!wB!k$J)C(VbNbA z9ye3#O{%7e+pro5106vjOp-iQuBO)M(=&%wA zX>Q?h;Ty8|+!j$8!*;57rEiZd&C*6ZMv^h&G?9-Hq>OltBxAy1yk54kdH8zcRquiH zo`6|4Iw0I(8=mKJa?0J5;a4PwjpB0ak@=qjYTuLazI)n_i01O#pmsiL6XyDF;*m}^ zO*D~@5u}WGjU;2lX(JvZNf_}ONXLlLumJx0Du4yOx!ASt4&vCWzC}#gJ+idHQZ_S_<;-FUVUB*;X|cRW$gDOj2NnN8+#_Y zqp1MR9}fBJTBA6Ub&K;GMmZMce1qlMFPUfuDY0AKE_KEaWI@BeAaVOLBKJTml`jkT aO>#CQLc>m1mPe??#VfxrXaG?h0002BvF29* diff --git a/mobile/android/base/resources/drawable-hdpi/open_in_browser.png b/mobile/android/base/resources/drawable-hdpi/open_in_browser.png deleted file mode 100644 index e78d96665dab927ed3fbff20f7a9fdd76fa23eda..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 194 zcmeAS@N?(olHy`uVBq!ia0vp^Dj>|k0wldT1B8K8r>Bc!NCo5DD~?=;3f&9k@Nw>@2@%VARB9H)+kE>N&@Q)T^6mvE!mR_$ toVmsMCCk&YBv#q%OyoQDR$zs*{MGX50^2mto(4LK!PC{xWt~$(696AZOQHY( diff --git a/mobile/android/base/resources/drawable-hdpi/overlay_bookmark_icon.png b/mobile/android/base/resources/drawable-hdpi/overlay_bookmark_icon.png deleted file mode 100644 index 6cb5559934e88d6646c66203837254b0f4f92f85..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1181 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 Ssrupo00000S z;!wer77pvUa=8i7ZjUPZ)fcB8&v?MOyxjZdgER9s8%iq8`8Pq}vWD^B9-qz+$8QHz zyPZnA^06pZb$40MP66Xx+n@Z%UlzDzxAOmOtn%%9^hEo9Z2-EB!PC{xWt~$(69BDP BR7C&) diff --git a/mobile/android/base/resources/drawable-hdpi/play.png b/mobile/android/base/resources/drawable-hdpi/play.png deleted file mode 100644 index 8e599c16db67dde3d5be8d833fdb7ed9ae5318e6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 300 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1|-9oezpUt&z>%hAr-gY-dxCaC_%&_(a|7+ z$F3nEYKug#*yM|80tXXJbl2WIyRYG9(pE0J=CcV=8zPoXH?+7}@nCVg1ToKj8-k$D)}K-AX>knERNx69K+kA3<7ZsvU?b;x)>D}Z4-7-7jR?f z=o5BG7cgUpU9?WvAsi$yPuRg6C~$Z2J|M$-fdO;TYx97@qc0imTy$d4mvnPD8a;{s zLt>?H!_(*csvDlBU+aD&>sxDXc-nuBd+o_jY7D3Q*YZcN&q%3_s&Sk7rEvPw-UfFQ slYb8?%ue&K**{~`F9m;)L+hWhUvR5fqH8^iALxAsPgg&ebxsLQ0G7FTJpcdz diff --git a/mobile/android/base/resources/drawable-hdpi/private_masq.png b/mobile/android/base/resources/drawable-hdpi/private_masq.png deleted file mode 100644 index 8791f24bc4c5a736583cee6efcb82bd7133416c6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1660 zcmYLJ3piAH6dzO!$>g%PEb~3fdV?tUz+>F^JOSX^Lq;9(RC@Oud6F<>5w zVl@JR076i3q{IK$KpZ#pN=1*erAwrXNB4uoR#Go(C=gDavN^8Eibp?s@of_&_#-1hY7NF4rF@ z1icF@T2R6a3Ie?_&Vr@bxG*+A;{nZJEx0_42*MC>oosGkV4%OhKbQ_=G5=!_l+HQ9 zz;1ByAi%h=5daj1fNKNAba2E+=Hbdgiq8kiQM#WWHV(cVtQY3NumCI;TQB~Z76K-l z54NG3QV2StJIt{#NNX0E4ytB^K#^*t$0JmaY;UUnd+b#{KPDcn}!hLYhQ|8{eg zt5)*fylwazW|8@AB4G#l!S}&izI#?JQZ?9p>hAlg(aD+VkzQHo_%N+A-p5M+aQfeC zTKtY__eWa>=8avaIeq`g94rW1c(31W4U+lEks_{@GawPtLmd2Ux zg(BtXJ4CVVXi=IQWmvMt@>UOPb9JU{K)7P(Kabpq<&$x$5qM^Hm>=U zTFM`=>sY(iwTviEIxWi(j3|m*LVLtJG<*Y06tqRP-A{spZ6Mzg$*bQ5Z{&W0#0UhhCgq`mDqqo0Tq zQHxAE%$939WT;S=pMGX)R@8gi+B6n2-1t|2$wOk(V1B|S&%)ce99SpB%#v)D46WT^ zMphfwPF*vEdOGg=Dff$7SUA^vU{}}^$zwKY&MV%nz$i0iyMk$My!BI8+IwoMLR7~M zLeEMi^Sn%>+6PZXdnYw5^b0OuJa{M-qEKu^I;y-ZkJY(nWtkoaDC#1is!nE?L&SdW zHX2QX;1W2sqC5t1+NS&bThv(ErwlC#Ul8s^&h5$Sdg&CrVcJc3aqZ8|qqnJf#A3)? zUNmtXeMFcoHYxlmU34dv3sVPiSejPJcJludn2-!5dQYjr;JcxT&Ll)iE#BVufNv%eC^#n7EhiCFkj+E%u4p7hC$1!g`8j zX0pU6-8ILT-GtVEw)9Pki>*J1XlIOGyZyL(no67eV#ptlx2ZT6z!M^qR;I||HfJ}N z69!wad_7{{d^ddElg4}!Ds9!PL6(gbg$kRh7fTguV1?|4yAxOMMw1Lv0003VNkle6&x?dF($OH12nfgxxe!<6wjc`svob}I z0+DFENJlIZWvHunM)t`bIm%=ydG}q(P{b7nPeqq-<3&=P#FYe>$voL0hm^c7Nn+GP zu@@kpqlEJQ#q>Rcc*xS{GA1;SAO|kBE%OfAhvp++XOY<_(QQgzG^1peJsi%EO-i0O zqvRZOuy7@RyiX<~t?Y}{{WSbkpZuY_yLe|;-rXY6Hn%^1cYi{@_^gcFiG2P!*#U1H p`@k?sleGVClWej+i6)lx9{^Md9ui2Ke3<|M002ovPDHLkV1lT0m+}Ar diff --git a/mobile/android/base/resources/drawable-hdpi/reader.png b/mobile/android/base/resources/drawable-hdpi/reader.png deleted file mode 100644 index 2ac1b8d46a2a9b41f6169e4d8d8923f2b6054471..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 315 zcmV-B0mS}^P)AIE;*&G>0r7t#@mHvN3ncwWP$b--_E+Tn$X)5JkQS`LUmVGC}kxExen z8xTu^*tB+#ED%2d=Ne4>9LYhMeZwXxjXMRx3GBy^=t@2$bfi+5GrTW_u_Oh%b_;=Pxl1YJ*gqgfUu! z><*e`zlqR4gW8Fc7aB6p(OQm%(AuB=0IiAAHkf)V4p>5E6pVsV003c(ySe2{@bv%y N002ovPDHLkV1gXXet-Y~ diff --git a/mobile/android/base/resources/drawable-hdpi/reader_active.png b/mobile/android/base/resources/drawable-hdpi/reader_active.png deleted file mode 100644 index 99851ae7c56d9b0eedb4ae15e11f49bf298c8630..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 316 zcmV-C0mJ@@P)AIE{!d}p2gLu0#9yK6Es*pl0PztZeout{eFPnZtbs&_83J(+sd}mJAOzk5 z#D8h(AR^Q%Lqp{^2t)N<24Zg@<|4@s$`vC3)B(StX@?gkP7?|n$suc?%~@uKfdFVtl(xatTXDb=Dx+W&i~<0<9!f4;OjI8L O0000Z@zv1 z_507?e+Ao2rGOeId%8G=R4~3h=h}26K!7RWQD%90f6cr4Js&%FzxlOxHN%&4P5X{# z>LnKK;I2BdwVzx0nzmbvqJ(0O8^?qecOy3IB69Q!-ZPQl1 i{NksS^7WFo75)bbog|iqdHe%9kHOQ`&t;ucLK6UaoxLIe diff --git a/mobile/android/base/resources/drawable-hdpi/search_clear.png b/mobile/android/base/resources/drawable-hdpi/search_clear.png deleted file mode 100644 index 4dbefcb68734408f9fbeaae2157085df81e2aa2c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 238 zcmeAS@N?(olHy`uVBq!ia0vp^LLkh+3?vf;>QaG}aez;VE07Ki4V^i2=IlB1=FFKp zd+t0CWA1{v^A^mVzhK_{1#=fH1d{XSFI>5L?W#5F)^FTwX4Z2Ys7;_G$S;_InRV;- z9lMSkJ9*~Z`3rykTzd1c4Ja4o>Eakt!5DjvmG20HKx@FOd;fmNmz5sk`Y7IWx-&*WYUJ$NLWCpgy3xSf;FTa(9Y zT7##q9Am@0lva6O(UW{1{EOOdEuU#qE21*lURNVuGs|+$ zG-BZ0`B*7?tG{5t&FiJ2z+dG;6U-pyH5yz6)8yXEg6z1Y1#Inwcq?N`3Odyenb ptM(r+um2S<_iuCi|IZKi*D-HxP*h2qTRsaE)}F3@F6*2UngI4M#J~Um diff --git a/mobile/android/base/resources/drawable-hdpi/search_icon_active.png b/mobile/android/base/resources/drawable-hdpi/search_icon_active.png deleted file mode 100644 index 65e921896c6d14aae7ecdeba56989477026e33a7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 450 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 KKbLh*2~7Z-Dqcqb diff --git a/mobile/android/base/resources/drawable-hdpi/settings_notifications.png b/mobile/android/base/resources/drawable-hdpi/settings_notifications.png deleted file mode 100644 index d2b23cf74ac46c7b61871a95fa04ee538f83ffc9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 385 zcmV-{0e=38P)$X!;qb+Ua zjza((k~d7DN7!qOwz9`GzLP^|$zUMHK-ptI09mw`BxpIJSNmVipT~%F&MuiI+DAC?aX%00000NkvXXu0mjfVpgT? diff --git a/mobile/android/base/resources/drawable-hdpi/shareplane.png b/mobile/android/base/resources/drawable-hdpi/shareplane.png deleted file mode 100644 index 748893b493c29631e654f003ddf163cd04bfbbf0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 947 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 KMNUMnLSTYnU%0OT diff --git a/mobile/android/base/resources/drawable-hdpi/status_icon_readercache.png b/mobile/android/base/resources/drawable-hdpi/status_icon_readercache.png deleted file mode 100644 index 142ac4bdd859f59ee9ea13e77dbcf24ae88c7a3b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 445 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 diff --git a/mobile/android/base/resources/drawable-hdpi/suggestedsites_facebook.png b/mobile/android/base/resources/drawable-hdpi/suggestedsites_facebook.png deleted file mode 100644 index 340b9a5e4d29acd5ac3cb2aaca16795d2765b9ad..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 603 zcmV-h0;K(kP){pP^|oG1VJA`d;#H#E3CAz(f&%Wun|NMvGD=4 z6twl~L+}L@TN`aev9hr+U}OB|43ax%cTGt4vd0e0+lb4P z{XG}m!gdY4hrU5Sp^S;!`2=(dx{4-K!&a0&5uhue*(IVo5mm~FE;))_ErCAN0385O z{XgJSJhWdOXMj!sd|!(uhr?k8=mq$iIWQWhI3%Es5q&PwLd7M4R-wp~gE^`JdPMYj z35kME!#H-J8_-4Q7?hwa#vnT)pq_uQXg{~n>yNZvgTu(qFfXf%FH$C1H#to{R|IUu zJ8J@I^0^{l!oa@VOSvipYy`FbKtLNBM~>WUPsT@kuuB)LcX|A;wT8COcb$@)qs1;s z4z?9cK)vFE#rO7^1alsA6L686*7?|8=^99$1?IdZ0O0djKEpl}STKQSU!|82@C`nJ zqPx&03U66R`wcqHnPBgjFy|0oCI$bBVCynD`e~6aKwzuu%&8p^ctWwFV6#irYrx7V z5U_@*ci>h)Nz1GEj1Wt%na8u!gAQ0U-k=2iSV}LC8So#R)6ZY7U@? pGr(_s_$qCFB5Nf7C3yL|%{Q45Tnzs?hm!yR002ovPDHLkV1fg#1hW7D diff --git a/mobile/android/base/resources/drawable-hdpi/suggestedsites_twitter.png b/mobile/android/base/resources/drawable-hdpi/suggestedsites_twitter.png deleted file mode 100644 index f3d9fd2380e45817386b089235c5bde331607f03..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1304 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 TY;}KT00000NkvXXu0mjf%tquG diff --git a/mobile/android/base/resources/drawable-hdpi/suggestedsites_youtube.png b/mobile/android/base/resources/drawable-hdpi/suggestedsites_youtube.png deleted file mode 100644 index 7e73e60750367dde5c6657f9ab75cec60e1a7aa0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2773 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 bvw8SGnwqd#rD#~v00000NkvXXu0mjfdT(!0 diff --git a/mobile/android/base/resources/drawable-hdpi/switch_button_icon.png b/mobile/android/base/resources/drawable-hdpi/switch_button_icon.png deleted file mode 100644 index 56c4cd050604ee29d5c740ff6c245a5dea650364..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 186 zcmeAS@N?(olHy`uVBq!ia0vp^av;pX0wgC|rfCAHMo$;VkP61PSGJ2DHsE0i2)pAL zbwxYDk?%ncv+$0usRX^w2x}&FWeVx%Kc4xo9=l{iApY=U^ewynvrrzAj)+4RDEN#hl32n3g%VrqP kOO|6UH!BQ!+?i&~Af!4+!K=L|4CojJPgg&ebxsLQ0IeNIvH$=8 diff --git a/mobile/android/base/resources/drawable-hdpi/tab_audio_playing.png b/mobile/android/base/resources/drawable-hdpi/tab_audio_playing.png deleted file mode 100644 index 4635cf5aef43afe516f6905433e3dc9b8118a6d0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 290 zcmV+-0p0$IP)t7yi8s3;#m$L#@cb*Z9bnHaN_)IWN>}Xn-F7Z7jA8PDVEU2I&uFf!yh$Z^cHt|`ag8JhtcQ3xa*ZsE|N`g&2RBOH>P#L6@wMoZ+ oCBKz$IJ+{w24SnLF?}?JNUjP6A07*qoM6N<$f<+^O;Q#;t diff --git a/mobile/android/base/resources/drawable-hdpi/tab_close.png b/mobile/android/base/resources/drawable-hdpi/tab_close.png deleted file mode 100644 index 7aadf4c45b511ead05554de58347283b7ea8de16..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 178 zcmeAS@N?(olHy`uVBq!ia0vp^d?3uh0wlLOK8*rWm7Xq+Ar*|V=eLS72MRa`h9;~y zbYZ6Q<*3UygIJektduc+@xtH2yz)@pxoJ<+m}HaAG|7r5h=1SlS*h;Q%dg#XI*;|* zUYwJf&8eRh7!w|Rz(P^N+S0mm;nJl~=3JU8p4|UEF+tJmlGcXLPKN9Sg0|Tg+ZR9D c5O2re$-er@j(H0%03E^L>FVdQ&MBb@0PCJZq5uE@ diff --git a/mobile/android/base/resources/drawable-hdpi/tab_close_active.png b/mobile/android/base/resources/drawable-hdpi/tab_close_active.png deleted file mode 100644 index 39752bb9165640bd4e0248d14a967333a52dd909..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 186 zcmeAS@N?(olHy`uVBq!ia0vp^d?3uh0wlLOK8*rWjh-%!Ar*|V=e9B)b`Ws6sCy)& zZ=<#Gl>(V~TXjt#_q1qk;d9=0cf|Gu|IB1c*}d_UQ+cf8i$}_KF}1T4Zoc~aQ!YeK z{)ETJgmdKI;Vst0G4M+dH?_b diff --git a/mobile/android/base/resources/drawable-hdpi/tab_indicator_background.9.png b/mobile/android/base/resources/drawable-hdpi/tab_indicator_background.9.png deleted file mode 100644 index e225e8ce943c386ea5a780b2fae803db71b2f659..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 107 zcmeAS@N?(olHy`uVBq!ia0vp^tU%1i!2~2TPB|6=DYlXzzhEHEz>vPT;Byv`R`7Ij z45?sDPDt=bcW5ZlV0Gqco-qGky%sY|;={$i*%_EuiGOw7;FAi{_+8 B8xjBj diff --git a/mobile/android/base/resources/drawable-hdpi/tab_indicator_divider.9.png b/mobile/android/base/resources/drawable-hdpi/tab_indicator_divider.9.png deleted file mode 100644 index f1338d8031ce95bd4ee12befcf44868e3ce293dd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 75 zcmeAS@N?(olHy`uVBq!ia0vp^%plCc0wn(^)|vn*Ax{^_kP60RhNk}shd$WPU{qo- X{>Q32k2U8lP?o{d)z4*}Q$iB}9-9%Y diff --git a/mobile/android/base/resources/drawable-hdpi/tab_indicator_selected.9.png b/mobile/android/base/resources/drawable-hdpi/tab_indicator_selected.9.png deleted file mode 100644 index 7ebd02f6f76ef9479f34fc0c2213e63d0191b6cb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 88 zcmeAS@N?(olHy`uVBq!ia0vp^tU%1i!3HFsuehcLq~twa978G?lN*};8}tY`u>S89 mZvBdriV_VNze8Xb9u_|_ZLhq*R;r(!k)s9$- z4i4d=RmAGo77!f#$ycm4#6mW}AxtTytIfu$3y1Hoal#(z@=zz1SUA@H-q`d;`~aEd VAvACDqzM23002ovPDHLkV1gKQUcCSS diff --git a/mobile/android/base/resources/drawable-hdpi/tab_preview_masq.png b/mobile/android/base/resources/drawable-hdpi/tab_preview_masq.png deleted file mode 100644 index 69dd8a093cb077cf29dd8fd881f9e314fa71aaaa..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1131 zcmV-x1eE)UP)?Af+$+w6bst;X{8ACVIeh0L$3vTLgP z(dd@+?mfG>uOi|*vHHGZ@dyz+CaCYJ8aE2=OWk+heUs8Cgfb|LI;e{_&=%T6+vo#* z(fFiBqcMeQV^vl^C8*sW#D2=`zfl^>sWlM%qmMp174;Wh<1>7hv#tB#1AKu`RHfK# zb9Y~W3hKZA{+mR#u}(0*nbm)Q(`Ag+K4`;l?n=sz>0#{$*let zi(SQy#)6b6dHCV!g8IQM{vI3~9}K?2XZTKu0u!(SBd{X;8LH9eSp7VaV*}&YQLcT_-ODQF(8%!c@!}sR>X|h!2m3Vsyhf+g4I?7tAW+PYG5_6 z8dz;LunK`;@s?J&Hn1YLxO-ZK+Q5p~Y~8d9wSg5$qp?KkhQz)LY7YhR8(96q%uZ*u zOM?2XLG95X_Qx2QF8>DgmssuYV16a5pOM*l_{W*WTiNT6%k}|Q)ZN`ZOECxSH6%fa zap`Q^WMUe=2JLIPug+>(Jt%uYHEx4A687tQvBmvZ{6PTD+gGdAnwcOLeB$zC}XXxtsvDe_l~MhFroo6yKmN% zIgNt{sz^^i{q$sor)GLeNJ zC6BRQ!75uU9?E8$AUR{PXlC=^%DVf)mX!h3#sy=JNP40d``8`a9X{pZgy!{ECzD%sxbd#VK_Ejmjg%eZ4#zk0^^e xsEanx7TQGH=mUMBPxK8R2K>MQzEySN^*<6qsQ@)^RXzX!002ovPDHLkV1lK0FFF7K diff --git a/mobile/android/base/resources/drawable-hdpi/tabs_count.png b/mobile/android/base/resources/drawable-hdpi/tabs_count.png deleted file mode 100644 index c8db01a1aa80944b08d8cdaae8bfb096fdb1140a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 145 zcmeAS@N?(olHy`uVBq!ia0vp^Dj>|k0wldT1B8K8gr|#RNCjiE1mgqy^Zz&efBQfE zQM-faJC>eCX(x$QkBl{XGLq9Cl?q?6efEE*29IJ#Y(TPxImZc!jAl1pDbtKA3j&+f s0#7gS$&x(6Y?38(xPWnSf(Qe{q)F%UG}5i^0PSG#boFyt=akR{0Q)K~@c;k- diff --git a/mobile/android/base/resources/drawable-hdpi/tabs_count_foreground.png b/mobile/android/base/resources/drawable-hdpi/tabs_count_foreground.png deleted file mode 100644 index c9b9c055b090145c6f82336590b8e8561e52fe1c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 146 zcmeAS@N?(olHy`uVBq!ia0vp^Dj>|k0wldT1B8K8q^FBxNCo5Dt2-GR90Z&KKfle_ z+8~ydu=;PCbme`k3Ht?h`8<2L@c5ORQi4-7R~UZ2%HkS()q?9%;DmCgweuT=yfhT$ uO_i;(ZaiACQa9uM4|6Sndxh@trx?EEch7Lx8DR{xgu&C*&t;ucLK6Vv8Z*-X diff --git a/mobile/android/base/resources/drawable-hdpi/tabs_normal.png b/mobile/android/base/resources/drawable-hdpi/tabs_normal.png deleted file mode 100644 index 49b4ec8bda41434fe0f6ba92048afa68c8fb96e9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 216 zcmeAS@N?(olHy`uVBq!ia0vp^Dj>|k0wldT1B8Lpd`}n0kP61P7nd>}3XnKj7^7Ux zu;#&3pT~?7Jffoy=`IreJ@4Ctykh(R|90;#=d1QQE60^!$Xc7xt0~p-(ecm&hx*lr zzs@dP$oeSsuXza5N4=k~tCuk;ytAId#;Ns(cNJGl*Zl>C^-qpBD>Dg2yfzLI<5~N# zI=v>Mi6y+^|3@7Um&dow8!e}}$*XL#yX1bDagw_9BWA--Pd23c``>0R@NhQBZ+ou= PbSi_VtDnm{r-UW|pAb{S diff --git a/mobile/android/base/resources/drawable-hdpi/tabs_panel_nav_back.png b/mobile/android/base/resources/drawable-hdpi/tabs_panel_nav_back.png deleted file mode 100644 index cc74ae4d6dcb286921be2e877f0398b9bbb41dce..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 453 zcmV;$0XqJPP)4}0{!QB&9u?h{R;O=e>cZZ#`pQ1g`D+zaZhwJj+JwNtwcjnE{yzA~y zM+l)kuv|yhk3h0}?b@|X-TxS#xjT^FLDnC|0O>(a^2Aee_5c2@ZdzW30dSKaPH^r9vG}Al9UAU={`j3!xXLhIlZ36OQMC@F- z?UytH?snD#Naw_Ej%)(!-t1jy4ARuJI}uQin7_;k%B1w$UQc`Po7px59-loMQIOSV zF9TCBFpol#*C`Fzd*9L)3^Fi#KgJu%m1D<_wKfGkQBPofkOBt#-&K-yMV4FEzpM0y vUfTidjRpsPzah~O^lgCTS|)G)^hf*?39NT1K&6D}00000NkvXXu0mjfL^sIk diff --git a/mobile/android/base/resources/drawable-hdpi/tabs_private.png b/mobile/android/base/resources/drawable-hdpi/tabs_private.png deleted file mode 100644 index 50ff113093e11639a741d85aae8a262d28d5e1bb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 429 zcmV;e0aE^nP)@c*tv>bBhMH z_*u&=&w0&5db!9_iHF~@IQqSqi`>G7SEI*I-QwUbSCqU3^b1ZtQOQ|KnZkrO3tE_> zl+#r59pYo}KJtD+`pN@8k&kSRZ%v7g2-{ X)St7*z{ZMK00000NkvXXu0mjf)$YML diff --git a/mobile/android/base/resources/drawable-hdpi/tip_addsearch.png b/mobile/android/base/resources/drawable-hdpi/tip_addsearch.png deleted file mode 100644 index 8f02949c5bc6452fcabd8975931786131870be8b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2606 zcmZ{l2{e>@AIE3p+Lvy$*rF*UTg@fe$yn;jHg=-Pl6@|j8Zs1NaEHitZ9^K2r4Y$r zawAKUvJ8?@S;scUHkz^A=Y74e(|ON%pYwmd&*%61{r|u3bNq?oDT#7 z30^US*#J*Ga0l=l0@SGOkrxPb2xMh$YYc>YdwU!l9AGf`mX9x>z@=6~1+t!m-y$mNJ}?{ zxPr@qBQ1S$m9awoGS*5amlPNO5g3>d7gt_dnwy=yK%0vQLnHnDJrRg~ z3*cqGj8pqQemZ>ka9V1bsHo`L`nrICKzZeJVnr1f7Z;Ps+}_@9YI*&2g8Htj^Sp|R zyqp~HUB*U7*Voo!qobQiq|*?{hxa`Ti;Gba5h5Z-?BQ@uPEI33!|N{2l#$V?=^68@ zSGwN4pZNN%AUAh*cDAkUt-h|#IRyn4>qph|n&F`#==t;H);9&jdQ4c@3Y{Ju6{V@6 zas1dZM>{(PW7XNwK}TDQmzNjV)y7w^o>kYvt*lEzkNtx~BNPf6g?dq4Jv}v5RZ*d=q_nfMlZwM-XQcm?m37t36crq-28C8vRn^ut zAl%(nhFX_!2O129iT5H&%JE zIj}J}*>=mQaAdpq&xr-3S9tZq7YiGoO}kfiHAP;;fk0fAS73&=Xz&u{P`c!glW!S& z%j@O=V=n{8BB{5=E>b9TYWK*??|<6Ij95^bYT$NKsyW2+HH#=AcG+u!3*iQmuguro z1Yi5yz`aoQa9@Y@Z z3yH}YDWms_f;%{LSrs+Acn)nTLrZDo>4Ju`wqt`!v6 zbcXboX13mCF9qb4#5V}PBc5*Zc=r4rIl;!`xS+k8uY5?tRAzqH)0x5K>a^2Snyy9O zRc29mHc7}fha8jcU~guw)7^WvXr|2(q%C|w0G?_T($aMp&(56de1(qiK(aTj*5oce(utR1m?s$oxq_*8HgSx`zE6(f zLfH-#x8$dBUPsmn^dimVoE{dRQG9fl3o#gQ#Az;m=ibxnlPr-ArsTr(8sk`Z`^aEG zOJ%m_w!fJPqj$DA;q_)%WYx2G{6?_LL)c1?DS#0$26ThlzO56%_`Hyau&3D=8yUa?M<7an5bmtl9>Y7S-p0Py?YOGFNiBd`iTm>0W52Sb@*NJjTN%49)sde zg2nd}IgfsTi^V1H4bEyQh_!?(PV>}MS`JCcLU_5$*e6PT(L#ITaS>@*&k8%czMj-I zQ4937#)quu%R`z{q_G~(N(DvBM5RhYS#`!3xV?kJhtiR#H@^MgCRU{arYjM^?wAT58Nc#Ssu00_|BSdB7~1vprVKhy7DH;h)={^>Vp>O;{2~~C z^TAH2mIEp=XUDEgx3a|;qapaVDfF|D>-4p0H66^+vf2!x7AElu6K_la4A;N;Q6T;77m-b$-m;v7hM|9y)vvw?a+@KY>OnN z=f@TvrnB3PgpC~5kRt4L+L~~)$#Al60-|K&XwlT@`!?h80v&b;;w zi#Sixx3f7p4XRPbj+*LOk84O8D*b}elMRte6)4rl-Ytf_>$l$4crdAmqj&qWJbvNs zFPFXxD1rY-c$WV+PkaJB1<@5^PauFSKh^hlej05jZ=POh@1w5*f8+@JRJ3CLsa+n7R2vP z;{dZ&*HD;n^w35$%6pxbf{p)F zI<%>e{0JsCHBdD&XrK4MX4F7x)oi0mg>`j9o(hJTTWc~_Ld0s3V)1kdc)FMKOAZx1 zW>~B+#>vSKeOuMR!_Bzzwt_D1-j`E&E!*)5RjhfSUo8 z+RK!S>@5l?Uiv5lLfo@l$#fdXXi=*i0AbPb7VbOIwfM}xv05Jga)0EFc9qVhQ|vYe yNt&DU4F!QX3+1iplnk6Y`)!J?fkEpYK|iP diff --git a/mobile/android/base/resources/drawable-hdpi/top_site_add.png b/mobile/android/base/resources/drawable-hdpi/top_site_add.png deleted file mode 100644 index cbfd8e4d417bb51b10116c28213fc64c13d8bdf6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 113 zcmeAS@N?(olHy`uVBq!ia0vp^HXzK%3?vzhybXaQTYyi9E0Df$`Py6W+(SSXV@Z%- zFoVOh8)-m}q^FBxNCji^0j?+RY>WTdOUNj+874KcGBW(P*2=ncy8ACs5re0zpUXO@ GgeCxLz8$*& diff --git a/mobile/android/base/resources/drawable-hdpi/tracking_protection_toolbar_illustration.png b/mobile/android/base/resources/drawable-hdpi/tracking_protection_toolbar_illustration.png deleted file mode 100644 index 9984d7f3ecfc9ba314d078c723a7e75a4a8015cc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2474 zcmX9=dpy)xA3oliCAMfq>oSbX5Mff<$|aeIF=34%_bKaAgm>Ip*Xd20MndB-Y;A}?OD!>W#Telap4(~)=QM_MMz;PLf`zXVOjqto z_XGwweZ#9AYL3289j_iUI8-n6CVAEtht;~pw)7fkX&>C2*pUcnaO*&RUMm+F-BiG; z>IW~I6kNSu8`94^*3T<`(AG08(b3U4c$}AVH?(gm++;7ubALZy_^g}kC(IZmy%c$W=JC)E`{97hG(<`M0--SIr=QMF zuD;C3%EJ5ja8C8iuhcB~cfZ;braqo}bpKsh!vp|+6^mOkGBd00_XagYqzDS1nF#07 zss~9|UmQLJz=!d1cQ1yY`v!SjVoL5j&@H*M8(ja4%IR@POiXNkni3p!Nm1X(23F6> z(S)8Okx2OI^J$MND=Q%YN<59k{Ys5;zN6b$FRSWj9{?cZ?p^9q>N7Jplz}Dy-)m?z z+BJknMGIgw%523mHa2#QO>A0rIlfp#)V`O^<@!^*0<_|D3KX!n-}acFSWx#fu&}Ub z(GOa2v8w+|0M(5>W$*uPPys9RzPV-c+Qo{Bilxxd;Ly6T?RwQ|}Z-?S}cTMAtNx+SMCF3)7Uj9~EG<;Se9fD1>+nJ4q`j-#i3r;NZH zXVv`*YGapca#^g?KE&e7x0PdBaW`ZQKLMbW$3E}c|L8>{G?)-bnDu#g#Y-B<7)bpw zhD0I-$^$xQ>-B$uBg-CMTzu;1w0+X7FWCXUAKV-weFRD5?Ao=9ew3Vf=$|#l&S<9Z zXtHu&a<@Zp2dbKxEPA_{-&SU&Gq5eJYpYFkN{I{b5nrgQdx((*XO7`|L+wYuP+wDZ z{+vGNEHP`+DV`_M>0dC$LXjV~hCl8_W<~LRqyJay+_#Z(5+lFmM)2pK2Kq?k=bDD39#?lf;cLP)ajj*VA$m~Ssd32DR zp^U40y2%ui7$F%##6NhLKOi6R#`<7f5ZMCrgI&&Cf8nAEGw-o=f$u=iN^sot ziC;ZfV4Y7n@*GVeRTe`)iv9JOn_c(^S>fzZKiYv}{R^v`+O+ zS1+khSEd*An5`1?<*bRfO8_M)6x?y> z99%0+d^(S)K&vzV;;-WKexLQYOBIgLv^jUfcQ!07PX(MIqVswJui~~XP^MbU{?t<6 z-k3?hjl}wdj$GDlp9ZKUU${*cZuwzhf<{aR1L;H1zc(y$XlSe1_%C%Ce^%{iTS5t4 z*#pfSM5KE&q=2m#;TP<0`NW98!0A#w=jGz8yJ%60i7C5Y zSEn?h>|lW!pvxgpu|fzX#Cw1 zk(%)=@1FOAu|Im)tP?yr9)t0OV9EY=^?nDG0row{X*9#4`;-ToWnyc4SqM2L<#8Y*e7w@UWi&K#uQ4% z@e;aV1_2b3afl9#QX8a=eQ(1WZe?Y3^{eNj%tNGcWAQ0n zoI9SNaVY}qKSo2bgio=d7KD$niM?4hCP{fZT@iTVbKb1tK&mV#xmGFshg zjGWkrkyGMB^zW?(>g_f}*VXh5r2)7+{7kG|6f`-kpzCf8z{Sg#9Tp?32i79g{uqOW z0ePCIeYi{4I;Z(kF+%r!u;FvLMK4~w_)sdQ_jKKZE%~NYIDT~IbmH8KrlBER5W%C1 zWU}jcz@7Ptmi08KQw9Je#jwhBP4xP!QDJQ?H83P`-_J!t-IgubwhurtT^7IxK*W{_K4bZP;@eCH9StLy#l%adRloHeivbGdm<7j4l z7bY8A@}($JM#|F@9JSEu2W2Fwx(62RP!Ie41TR~M54i_FlY zgq9kN&r;ZN0(G%~dPXh+~KdG35wAr)u{ zT@as&x=Gx<15)_)xj$_!w?F~)6u8nI=^Q`e+Xhy{O`kR{q`JVV7iDq0m(b5fo~PTv z#HfQP*9XrRdu6oe5he+HJL*x$ch&w%6{hW{d6wSBw4q5TgayCj>oVQAe_i^kUk{^0 z+@ZL{)OJ7l*rv>QXtNVzfeDO8D0JYP%uMKm1Aj4x&p5EZIlRh≶5F4xDBR|1O6Y eNTucMe_{o016vH!*64o#0000@O7F zq`0De-Y>TWW#@{{sUGya@Bcr`q@uI+9dnaoN$_c-=gT?_^gG@@IIK~4Ea>bE3AYd1 zsv92szB+B`?M?9q+? z7EU1*kAR5~YV-M`=J)lN9T(qpxm;DU>s-d>B;#9eO5L($wD0be%}`M~tfXSJ5$IM1 MPgg&ebxsLQ07mLooB#j- diff --git a/mobile/android/base/resources/drawable-hdpi/url_bar_entry_default_pb.9.png b/mobile/android/base/resources/drawable-hdpi/url_bar_entry_default_pb.9.png deleted file mode 100644 index 0bd6115fcf9b8d930d4e083d0e4cf93e32cc13ee..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 252 zcmeAS@N?(olHy`uVBq!ia0vp^CLqkh3?$9N*lqwR-2k5uS0D`p@hO?HiD_|(>G8=K z30Yb3iRr;%Q6{G58k(9)N=l|?=FYAjaY^YvCF)iJRr8kw`2{olPdoSRzr>->|AFB5 z_k5ds3xQJko-U3d6^zLYjE+1990Uwzwj?p6vrgcNKDy?vM1pTMQ$W|>Z+SWi`HafU z&ByZAEa22g?Tme85b#R0Y^7=s57WgPOSkbUt_%c0O`DrC{tp}0dmIcFc&M>2Y@2!m nx9#o#o(~a?jSK=uxFjSPPH$3PJgZYC7~~00S3j3^P6QR_{GY-8 zf0gn7X)OPzv;03^0hPM9@&CO||Gzx_|L^br|Ns9SG7JDpaFzu51v5yb9Xj{<+xOpD zfu|LKf<>M#jv*C{$qbD%B^nwP5{jZ*8G}R|jCz)(|4(kXeUQmP^Vz=t5eK-L{n*&9 ze!sEgA1mY1nt#cx(G5XcZ!BF`Xyg;oAN%UMz$B@uAb4o<*^eT8Ke|M0SOxnYz4qI{ qu;W7Uah)$-A`%=8Gb0$8nHgs1lq`KodHOA@mL+aWLGVLLu}v%UJ|$Lv y$CkxEqHfM((EIYn+3-L*D;tx8fObLx14BfL%7TcvId6fsGkCiCxvXWX%7S5`9#jlQVGI$4F$`2cgSoOX`UBQwJ_eVm7%s|w1D^njib;kW zoq#di=w8hFQ;Y$%^MD$F!9RAo-UF(o5nwE+Nm_YdTat$N14oU)D1dQ&PqpK0s$#L& a59|fiXE(Gv(~Elm000007hZAP;53%;0X*K#6~j`3>Iz9kVqDjkzzENC?>8z%@A zCIICIP+3KWV}1j13d|f@IBFJ&j){Tkr=_F%iF8aT1HAyq=b$1VhhuI5u_*(+0jS6a q;!YsG0>rPOC44q06VS@eQ2+p~IlD`cHWFF@0000^9ELNT z;S6WE5g5*YQn)z7!Nsi$hh5=Z9Kyv-!$);4&PVkxEL~hFf#X0+BIU!2rlr#963N41{-Hcr27kzz8-00000NkvXXu0mjf Dwn@7Z diff --git a/mobile/android/base/resources/drawable-hdpi/warning_major.png b/mobile/android/base/resources/drawable-hdpi/warning_major.png deleted file mode 100644 index 61cd21f9787ec30f2268694a69588a4a59be3ab5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 283 zcmeAS@N?(olHy`uVBq!ia0vp^av;pX3?zBp#Z3TGW&u7Su0Z<#7Uut3nf`BK`M;Uv z{}!hIJJ>-aP!P!7%nTCW3S=<-->(1?-v(m4q|NjM>J%&q@(X4-Qt#4mN9ErV2d|#C z9F8pe`}2P@0wtS0T^vIy7^j|b6mtm>IdXx4Vb9R diff --git a/mobile/android/base/resources/drawable-hdpi/warning_minor.png b/mobile/android/base/resources/drawable-hdpi/warning_minor.png deleted file mode 100644 index c78b77a134908a60bd05eda8122bf0792a3374c4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 283 zcmeAS@N?(olHy`uVBq!ia0vp^av;pX3?zBp#Z3TGW&u7Su0VRjrY##cZ{4tE%leI* z*KgdiapMl4$cBwu)^FatVe{sVn}Lk28#iv=zkfeaeAA|FKxH4NU;7W#DOeKZ7tC;^ z-lgG=%D*KJUOjC&99j1F=l^B|N;Z4CIEGX(PCer&<`N)sMDiUbfo) zBjkQ@@O2Z}mzz3zQe6LNzuR!<$fs-_^Nn|o_}(`AB%HUb2Gr1+$$uo+O=rs%~>YL45v}n4C{~lhz)XGEY4r?vu;u bGtTx!o5ey>Ijl^8&SCI$^>bP0l+XkK<)wl1 diff --git a/mobile/android/base/resources/drawable-hdpi/widget_bg.9.png b/mobile/android/base/resources/drawable-hdpi/widget_bg.9.png deleted file mode 100644 index a5df36d99706d4f309de54e52e449a90df92517b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 354 zcmV-o0iFJdP)lI?Bq*`}_O+{QUm@{`&g*0002zo-+Uc|M>X$=Ak^%pjhRjK;flK-KA5Tc}m)& zRK}WE)1qAErAPAe@>fPI=b<{hyu7%RSJlChxhOWbg1BW5d;f;e1I_RK-4m#+dgATydVF>F14#S>@Og`**7$j0_@PMIo zSnyElB@&YvP6N8lcEh*Q>h#Cp;f_)&24V+@3+-QF(Opeq>;M1&07*qoM6N<$f)<{u ALI3~& diff --git a/mobile/android/base/resources/drawable-large-hdpi-v11/ic_menu_back.png b/mobile/android/base/resources/drawable-large-hdpi-v11/ic_menu_back.png deleted file mode 100644 index d18fc9cd5f009e949933f7149fba6fc0e517d2eb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 471 zcmV;|0Vw{7P)D)_AVy3*}v7)-}I{>o2`Omh_|wmi)szfXg{FODWEkVzbP=u`U8xn+8b z0eP;!eMx~nn?zU*0^$|vz2WyUxeC1=CA=X6^s3(SL@lQ&An!H$O7OhcZKYH-qvf3iDA8EZjUMYsvwI0RO(PWL1{#Hv-DW zuu_Y?#fdBeaGifkbzYna{2ooZ6{DM-Avr?mZvCExdIiWPVzV#?zH{8ROak^0hxPw>|n_Rbg!#gM7u1ye#xY{0-dBTnk=%D`Nlv N002ovPDHLkV1jqR(Bl9A diff --git a/mobile/android/base/resources/drawable-large-hdpi-v11/ic_menu_forward.png b/mobile/android/base/resources/drawable-large-hdpi-v11/ic_menu_forward.png deleted file mode 100644 index 22a822d678a3d189be030e07ad7bb545266543ef..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 360 zcmV-u0hj)XP)@@y#cjgF4_pUUhN$-YY+s?%ggh%5iKBQVq6l21paVnPtT7NOi$`2ksv+Yq`$tVc?UfqpDY@7yS zBqRVyU*9ypq@>W$w79sq(8R>p*x1m7gqWDLh@_WB{nHNJ~=%$F(o!3Jw6dAn-QCw8kd*>6bF%sX|YKl1XL3TWF%(9ffyO_$!S0` z7Dy%nxnPu>77t`3X8?79M3TWKBmvcfxbZ*+ND!nKNPl&NbIR!;V$EIiG9XZ3 zH*DUzeaFr{`wkpDeB$J}^B1mMy>{!)-FpunJ$?S-<-7NvzkL7k^Vjb`fB*HwE%*nF zI&n`I$B+uf(hHU5R)r$RKU(jPPcgjNs*`eZ(%elco?5##xHX-A+WdEaaPG_8*Q<>` z|N370?&qB1ledjJAH?WX*6k6NRGPh!OLW$uN{dgyb&Kkw19G=8+&$cp&%f$y_{Z?K zQmb48zZaFw&~=K)6`J;{?1X}tSJv9iIsQD8lix%~@vIS;kP>$?b^+T1V~v%+#V#F6 z(4JKFS)hc)?7@?id+FNu!Ba+RE)cMn_o>!&^)(g($d;rq6gm9Z<1u+uy^^jMXy(K|l3?zm1T2})pi2$DvS0Ei48X6gy5}J?@lawBp zlopqq9-o}id3QTVGkZypUoZo+xP*S~!&d?obAUqDo-U3d6^zLW@&;{F4?C=4aOv18 z!^ouIl;h#VDWLeRC_Pyspn+3Tfk(|fBcy{jxI;9!!;nYA#D$SzlA8i|+!~%_pfL=d Lu6{1-oD!M|l3?zm1T2})p{s5m4S0Ei48XA|B9+#XRpPcdk z^QwtJA(oOLzhDNoj%U2O{%SzJj;D)bNCjiEL{h_1R=yqOzC0QRQZ-s@a(gC*H_?GH)+<|44zyz}~1bjaRF};6O#lwI??j zqvu>`Xk4!8SN%AP+X7J6wllS^SdE1%Y&bv$J`ED?MyNy|T zO5XNuX-$ER{poHe9!t8I9+vx4V)($H{ZM(qZwG_0DDMy3l_NGJ9Bg9c7Slm63O0Ee zrgqL+WhU44>G$awu9degJN!xZ`1RTUl))vQpG>UEc_|m3mVN>Hi^0>?&t;ucLK6U0 C)NT|2 diff --git a/mobile/android/base/resources/drawable-large-hdpi-v11/url_bar_entry_default_pb.9.png b/mobile/android/base/resources/drawable-large-hdpi-v11/url_bar_entry_default_pb.9.png deleted file mode 100644 index 0e91b33abc716c653b4c42e053ffb9b05f8973f6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 281 zcmeAS@N?(olHy`uVBq!ia0vp^HXzKw3=&b&bO2Ju0X`wFKsqZcD={@IJ~<;kB{MNK zGa)%MAtfUqC`?ORTUlAz#@1d*Ny*B_4yXXA7|7nKz71p^UrCT(FauA?v3K^*e_cND zZ2zAFbNIex0%h7gT^vIy;@)1j7CWpU;2c;d$6UtpJ3g1A^|F!AtT~IX-;XQu&$ax1 zxTb^i^iqjG*X5W}i)_BR=xx;x_daG>B7Kc}-R{-9^gHHTDDbboBANK8zopr03bwWNdN!< diff --git a/mobile/android/base/resources/drawable-large-hdpi-v11/url_bar_entry_pressed.9.png b/mobile/android/base/resources/drawable-large-hdpi-v11/url_bar_entry_pressed.9.png deleted file mode 100644 index a55ad0b9c3d92c8690cea01d0b6d530c17ce434f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 281 zcmeAS@N?(olHy`uVBq!ia0vp^HXzKw3=&b&bO2Ju0X`wFK>GhwhX0co{!eE3e`VJH ztF!)3Vfg>?{{Iaw|Ns60v4OHc^#4Cp<^TT`d3xbM4qr)-UoZoYeaZd<$4*>+_Wa$i zKeEef?gC}nJzX3_DsH{KZY_3LfyX5+KvBNy_z1~0kh^TUjFNwvia)MJLTQ% zM0CQNman&!IP_!2H>a+hy3>;nT`he(P2%}om%HjS*uHt(R S8CL+E#Ng@b=d#Wzp$Pz@>v|Rd diff --git a/mobile/android/base/resources/drawable-large-hdpi-v11/url_bar_entry_pressed_pb.9.png b/mobile/android/base/resources/drawable-large-hdpi-v11/url_bar_entry_pressed_pb.9.png deleted file mode 100644 index ac6476de7067c51056291b5c476d7a1e40bc51bf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 281 zcmeAS@N?(olHy`uVBq!ia0vp^HXzKw3=&b&bO2Ju0X`wFK>BLd|BG4w&u9FbP0l+XkKH~4Vw diff --git a/mobile/android/base/resources/drawable-large-v11/browser_toolbar_action_bar_button.xml b/mobile/android/base/resources/drawable-large-v11/browser_toolbar_action_bar_button.xml deleted file mode 100644 index e1654c1cf6..0000000000 --- a/mobile/android/base/resources/drawable-large-v11/browser_toolbar_action_bar_button.xml +++ /dev/null @@ -1,77 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/mobile/android/base/resources/drawable-large-v11/url_bar_nav_button.xml b/mobile/android/base/resources/drawable-large-v11/url_bar_nav_button.xml deleted file mode 100644 index 0a34179271..0000000000 --- a/mobile/android/base/resources/drawable-large-v11/url_bar_nav_button.xml +++ /dev/null @@ -1,36 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/mobile/android/base/resources/drawable-large-xhdpi-v11/ic_menu_back.png b/mobile/android/base/resources/drawable-large-xhdpi-v11/ic_menu_back.png deleted file mode 100644 index 78a33ffab48009630569e0e1588d5af71b8be4c2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 598 zcmV-c0;&CpP)2P=WJ((mkB(u0XR5-Lqf9P;%um*2%cXxN!w++r4Ec^cNoLkQw zSylg;JSsQfB5RNM79c-^fET5urR7VV8REGf>5pFvh*!67-#)*xOddd9VhsFDfYB6X z3zIz%FE@dIZHQMY8}#VWqYx0UGeMIZ(gWh37uPKYsJ9Ac-g`kbRK-ny-!Bcwdy<>s zHxO`!jmlI6*b0Tl*#drnSHNfkE`aKA*rS3+erwDUK>Mn(u_A2E>Re<=I zP^otwGALUBK)FQVebV0Hq7IcU%ryg$cS@!b8mVm2r%zNAkT=U48&6QSNJ~pA2*_(> zQ#>P8Pv|6yW-E*2@sO8Erd$eLWfMSNX#KN6HWkfG@*OiVMZag!m>;gcs8 k4>k{ggdUn!{>zu%CmfVBFC#VXr~m)}07*qoM6N<$f(>c|8UO$Q diff --git a/mobile/android/base/resources/drawable-large-xhdpi-v11/ic_menu_forward.png b/mobile/android/base/resources/drawable-large-xhdpi-v11/ic_menu_forward.png deleted file mode 100644 index 7a284903f3dba21f1c85e62e288b2ca2d48f5abd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 393 zcmV;40e1e0P)D%cmHDWZX2=C zRsqVKL74&MO7Bs^VYthX93&>7^gIftB#7QQhz67iZK*?OK>0pBH6)w5P-HU!c}o=v zLP^zEQvv@o$Q!z93gf>3WnEcK$u~e=RfdW_8{}nW&ByNn<-aZz$Qy&asH^!x`@#^< zUmNB7_Q}b|CVF|-2^2>|ynSU9plt!}z6VdjYx6ko4bA^7dsIK*{nRA`Eed8VnFO>%kz`X~I-$z_`|k3?#4J%UH$0z&JI)C&U%Vg#ZT!2V>*3(9lp% zPfv%01R%rLH!U_cHZ(LXAR!?tDk?564#PBgpBCq^!TJS zFgraaIV~jgRtBmz_6#m#$vBe(Uy~yZ0YFeDwI)^OtYlzI*@S)8{YWzW@03_ur+yr8&SPQ|0O67*fI5 zd+zyc?m(HNAC1!%ub-N_F~atm*6Phddj$ejssvdUcPLKmaB5L0{+BoJzRk|>=TheV z{;|h0f1f_zzr(67x{>z9C3(-E1!kNxJ+$F%lX$?Fy)|qLD_x~7=JgBef3sq^V}0e~ zm14e1%g!scuU;gyb6OogeI>9l?9!aYpWX$2Jl34$RHBivqOyodsdklEcUZY*Rr)jM z!t-Uy7j3)3*p_w9lJpJUVt#1LHb-Nv;4_D|EOcd__AN#J(3Y>-zB3j_usc6fS1yQt zX3AqM>$Xs-Sn$?7`<{dDGm|eIJNEcP^rF}eztk?)MeLpUf@RGqp}Q?tMLYNH-f+{e zkj3m=dZ!e3V)4{PEsEU>b$&Y*${9|-zii>0v=v(z+FY-D)~m9)#Tka!K3K8*_p3k8 zFE3ejY_?3w&zGXgD?_chO537>SKYd@OpN8qzMRH&b_Nsov$b73?(-pqNAO(Ye8czY zhDXj;F;4xrIfkqA(#_qX|HF6x`Vy9B$y3iLY@n?y^uWdelv_Pr{an^LB{Ts5Z`a)#xK*)~8GoFzei!3-ku3W~O~??2gm?l31%(8troF{FYqc>#Zd z&-_l7Q!EdJgp30nlw*{HLzARp772wWS;ZthJ0l=&V8F(#tis7Ia(seIZS!&4ueDNqdzjOmk4U37#&FAr*{oLtPme6a=CtoVJvb z!5j)eLqA*ncp{KVZ&bKV7yZw>|s#rq>_mF|M)=7q2<(uncGygQu&X%Q~loCIHqx BHX#52 diff --git a/mobile/android/base/resources/drawable-large-xhdpi-v11/url_bar_entry_default.9.png b/mobile/android/base/resources/drawable-large-xhdpi-v11/url_bar_entry_default.9.png deleted file mode 100644 index 2c8c0d80f8cc15e929fc8bd1549a256fa290f747..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 351 zcmeAS@N?(olHy`uVBq!ia0vp^V9db+WaX-Q3otM+s(HFNhEy=Vy=`80*nx*7V7cvr z$CX!Jv)C~7J2I@3%{r>Bk-ONYZbEU6_?Ov?*2~TZoRj-2B{HLaik9j1An*9iiw>l` z^u8Mt{*r%D%n6Ucx)c?0dnRl3TVI*qu?OGgpBSiAX+P)I-vu&g!P}B(#?ND>tuxCZr258&ip^CwO8@!vHz}b1T?Mxo6mk- zp0aj^R8wje*UI}7Yme4-B;4c|y;Sd<6{esZwaIC-GXHERLBn@SlTI`#PV!LdR3U~q z@xXLLndmE}aN*mbpVmz35i#bDo!%R5lNaA=T_9YnMKu|{qYv(GkC}LSSWn0j zluL0ru4H|LeddMDC*)qbMKSYiVVJ1o+9jeHw8R66@pVaAJG( szddK<5B?0c{U30mqWZ$-E$?oL#!O6g$?m=L4CoUEPgg&ebxsLQ03K(PegFUf diff --git a/mobile/android/base/resources/drawable-large-xhdpi-v11/url_bar_entry_pressed.9.png b/mobile/android/base/resources/drawable-large-xhdpi-v11/url_bar_entry_pressed.9.png deleted file mode 100644 index 26dc0221b55c08b09631ef9a7cd9a5ab783ca72c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 373 zcmeAS@N?(olHy`uVBq!ia0vp^V9db`WPSKlyb?%x2l#}z0_pz^4FA&@{!e1~KbhhG z6o&s(!Q|up|DWvt|NQ9xXGi~keFYLC0}>z;fLi|l-=k~s637uN z3GxeOU}6ml4eMTd;J|@{=WgD*edq4y&p-c8+03yRsN%4vi(^Oy_nD2?X3x!89sVRf<;X3mg=MZalM|hir)2d!bWM9Aj@TdHd?6)xs(^xml<#5L=n>(ML vr~cyjXMA<~m6h}AH7?}!XW!1;>0a)d<-F8?!p>bl?=X0}`njxgN@xNAvHGzz diff --git a/mobile/android/base/resources/drawable-large-xhdpi-v11/url_bar_entry_pressed_pb.9.png b/mobile/android/base/resources/drawable-large-xhdpi-v11/url_bar_entry_pressed_pb.9.png deleted file mode 100644 index 224738af610d9815626c527d551384114d27a5f2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 367 zcmeAS@N?(olHy`uVBq!ia0vp^V9db`WPSKlyb?%x1o(uw0%;)lKlA_jOc1&NM&~pB zUoB>y*n1lLAg_A`yrP|;LYR#s9{I+OOx%P17cFwigsst2+!WdEPD)TtC`kVHw4 zUogXe*3hLlKmQN9!_iTiyjm*b);f8<>*cx3 zSFZAI=HGP8;_Ts*&oW*9DslImcF2$nwDH`YB(34Aaku%-#WONRc@N}M9IPi~3Cg9o z99ObF!anoD<`Z%+-J+OzwlGXoa_th)3|iuW#P~e%Ph*e$2 p@&|tg+x`zYQBi$i^OkqFL}MnVx@7m>c?R?egQu&X%Q~loCIFN)qND%- diff --git a/mobile/android/base/resources/drawable-large-xxhdpi-v11/ic_menu_back.png b/mobile/android/base/resources/drawable-large-xxhdpi-v11/ic_menu_back.png deleted file mode 100644 index 33b45b31d0aa95388c39a2a2f652dfd9a89c03fd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 845 zcmV-T1G4;yP)hXbzqMj4$gwk=goGcXQydVcW;u}h;Q)y{w`AI^h0F)4b=lnrw!@aF-J0Hz- zZpF$AP@njP(s8Kckt9^q!72jOT?6-9MmAXp5liF}9~&f!$=`oHfh1JA9jnVhz4Q;| z4kO(6#hY+Y@B9JbHx{^^DEEC#Nc_TQ=KDf8m`WCk%Qy(BzV-?AR^KpLDB-9c+MRr6 zzGvx9ycT6GHO!z!-HY08b*fAW0&s!>)Ky)y$!aO?sCfo8>SStatSDJ6C8Szy0JVoP zH!oc+1!}WjC;_;UZr>M;<`Giu_6OyGeX3LYN46IN)L{#h?Lnocx`l*PM-8Azn&r4% ze-wyPoeu|;S3|EvIH<)Tg7SLPh>-X+Y+%|Vx=RQrnD_h~su+7_&}Gm!3viLw7jg$D zhTx`N>*sRDSgzj+Ne4%n6==1B*>*AACw?r`W$W)%vZMN%!Qm|Lk%q3YbUWx;VBiW1 zb<7ffmuP)`gdDm!#tp$#WxH390rfm`m=LYB5$rprnk)D44dd6?>e&pRoOQ<_V> zWZ;%3v|1+(r`s4kW;G}UXZ3eU!)e#z5)T=~+(ph+Nz!n7Q$S}vgP8jyP0F%i-0jeA zc~c0tVi2}*))*A=JJD#Y6!n|SC;%6jY!HwJ^VGO>r}&+EGRCq5WStMk`jPZn#|3Et zv;V(T-&2FQGTZs@)VN+M3+$FDcU-}x`tpxZYO8^)p|{B+ByJIa&$+~F9NdBLQ)Pb# XJSa24O7Z(N00000NkvXXu0mjfg8z&{ diff --git a/mobile/android/base/resources/drawable-large-xxhdpi-v11/ic_menu_forward.png b/mobile/android/base/resources/drawable-large-xxhdpi-v11/ic_menu_forward.png deleted file mode 100644 index ac5166cd8261a19e9876cbd1e87058f62497030c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 544 zcmV+*0^j|KP)*iRWVB(ToXD9jy5*M?!C$R^RI1*{& zyHuDs%C&TCcR*{)!e&nN79=i{q>V}R1|%*o(FP=X6%)r?mqaf^;{2Z^(F>3`-)Sb% zbC5V9t45-y!EnL+$O%YX9LE)j?dY$-6^W%VcC5RJ^`H|w^tUfzdHX?}%i0|O0>UN0 zg+~Vi=gVq@0t#@Vs~};H?G9>+-HrU5Ivk8`X+*3Wuu!_SckkZCBO)qCqT5kW?b@X! z!tTt3w3MHh@biIx`$D2yehKPd3YXO-cqq~Na3~U;1&4|`od$<;tSPa6K&vRSa40_p zbYkz(QYu5N@4!+jodAa-wlO637AvI^9pOaZfTdL75Mq4|g(^sNfMb0LmQraSVtWDB zP15#G^f?GB=+qvAL;2McIFt_}DhGuMaI6Q=P{Bq_DAd=s&N{Zu|0^n3%yVq(9NQgY iJ4LK>{nAzRo8uD!$^54-=0<`50000Qzt zhNq{eLqYj?-qNAfjL(>8h5~8A_LPL`yBhx|?6M?dkktv}G2@y$2ad8>Z z(J2v$i4h4Iv9T#JG0AapsWC}OkqK!)ZhU-dY;tl;QhHQEYD{7pP;o4fOiGVS0+MOb z$>}i(Kz3?Oa$0OcdOVmBmy{8clp2?q9-EvNpOgV)#3cY3>2Zl^u}K;6KyETn7R*jc zj|GY+1IaWX8K0N|WW*OINO4zj^E4gGY~_Jbm`!<*V0k-oE?r@zduo zU%!3-@$=X3KY#ztjy!Ubfq}`v)5S5Qf^qKopW!|EGRHnG~6I;n*73(E^qnt|9#9^&1;n!U0-;wzw3YEL+!aCY3t1A zeAkej)YHD|4F~T^m zNldMi16OT|<2o9kv6L&Qm$NqS#Hp1nwIX^BWplGuESbP*9nLJfAUNiI;`4pp>OU0L zncWb*SKN}Xw7RKJbK+Eo<8w~QGyE zPOJP*)c=385@+p=W7~YC&g+n%*Ds@ljqjAj7pn7`?eo)Hq0A_BeJMwBc7b#1b%kV! zQ_amkF2}!A5bl#H@DK#!p_*(>cZucWA7W^ Y@Mq;Y99MDTH3B7nPgg&ebxsLQ0AwCG8~^|S diff --git a/mobile/android/base/resources/drawable-large-xxhdpi-v11/tabs_count.png b/mobile/android/base/resources/drawable-large-xxhdpi-v11/tabs_count.png deleted file mode 100644 index af9ee4d3ad40b028794f41c673b6d34cdc9c8521..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 232 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY3?!3`olAj~N`Oy@E07KiO^ZoNk4i|5OHPhS zPK!&*h)+z9OG=MVN{>sQC;he|~RL5NsBX3aAa z2=othITOqySRYgSaQ}zj9af(t43my@2r9evNXUvshAs)cxTZ@XG;p%QDd{X(7nhLt YY#U~ar!|)_g8ahZ>FVdQ&MBb@0LA4@WdHyG diff --git a/mobile/android/base/resources/drawable-large-xxhdpi-v11/tabs_count_foreground.png b/mobile/android/base/resources/drawable-large-xxhdpi-v11/tabs_count_foreground.png deleted file mode 100644 index fa051203520b14607993e790fb23daaee3d2f96e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 196 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xZ3?!EyURMI7Bm#UwT!AzY#Kfe_j8(qsbAb}FVdQ&MBb@0C~(jv;Y7A diff --git a/mobile/android/base/resources/drawable-large-xxhdpi-v11/toolbar_favicon_default.png b/mobile/android/base/resources/drawable-large-xxhdpi-v11/toolbar_favicon_default.png deleted file mode 100644 index bbccd51ef4d181e3d13890728de17cab6aae7b57..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 193 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA0wn)(8}b0D4o?@ykP61PH&!yTD)O*gSlDCI z(RV7OeYyOVS@o?<|6K2!nWxns8`9*{eeUGZ-wTrq-P{lNNwdFaJzjA7s`ylYCfun^y%~G&p;9=_4DV?U%!34s;M zS!9Bz{mmVWXH*+^^ISM0pTqfOQ2_gt_c{9-1*h`l{FIJv+*@+vwA0s9YSuoBuCP>p zIwbbMMVI|3ceL~BAUL-*C3eRUeSN>(9JM;=_%7wZaS> zEDi!|Rs=TG&s5=--!{|OfbH9c2pzHRqe&ZK)SEMja5hBdhQ`6uO@}qs_at4?nA_|0 zP@{XD(g*uW`|axQLm$ZBm0b1NtvK{&Im_`m58AH0y|qKWhGD*iLFux8IXS>^W$<+M Kb6Mw<&;$VEsQs`2 diff --git a/mobile/android/base/resources/drawable-large-xxhdpi-v11/url_bar_entry_default_pb.9.png b/mobile/android/base/resources/drawable-large-xxhdpi-v11/url_bar_entry_default_pb.9.png deleted file mode 100644 index 3aba71baea3c0bb9089d01ce9af62d1587120284..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 499 zcmeAS@N?(olHy`uVBq!ia0vp^WgyJK3?#KGrz-=ggaDrqS0J66o12)D6`ztBpOOJa znJFo`3CS7p$r*`RS*up9N=V59a?{ev6H+n*LnF*BEY;Q3mBAoBIWr(QTth>{(%MEz zNl8ynUr%2jNCNc&xj@4$j@T3eDcO=BzhDNw@Wm$||CX8mQk7iru!Ft{>$P& z^bCY;OXdW8;qRVoa+=$3#fi-(o%OEsPjv;aKJ|++_g3HK*SDT6uX@SYRyb?@yOwpI zP52o89xXl|yO;6wE5F>>hTBhP=T;@$zI*bm^abbFns);*B_buOX)9!F*9$E(wh%lwuO`oVYD<_FvSI)*Ez(yXt5AgTe~DWM4f D7(&hI diff --git a/mobile/android/base/resources/drawable-large-xxhdpi-v11/url_bar_entry_pressed.9.png b/mobile/android/base/resources/drawable-large-xxhdpi-v11/url_bar_entry_pressed.9.png deleted file mode 100644 index 779f2721f5446c210f21cfa760d3c1dc1dedd8c9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 497 zcmeAS@N?(olHy`uVBq!ia0vp^WgyJK3?#KGrz-=ggaDrqS0MdAl;Qs*hW{O4vX|li zWH6e-@P8`9e;|2%(f>Q^{=Yi=|JAwwFVBKRzP|eZ<2^`ro!$SnHXxCIf1ry0{|9mZ z{|6ZhRPg`*tCX0VE;cA zhvo9Gm3|7JT{U;X+t7d9y{CI0>1`!^_`sd~fnnEQ3&)#UIG`foVf<*E-z-t}i*B=OSwBO%Ws=$Y{2$yLxhf4_tB({FzU^jL^vBFb3^0c>88UP>wA(eY0T|)dZ^L8 zPU(YvrTupG_n{Bu?@F%v>{cB5vz+DloCj@J-rm|FU&Aop!k~27znmOkxH5RU`njxg HN@xNAT`BRj diff --git a/mobile/android/base/resources/drawable-large-xxhdpi-v11/url_bar_entry_pressed_pb.9.png b/mobile/android/base/resources/drawable-large-xxhdpi-v11/url_bar_entry_pressed_pb.9.png deleted file mode 100644 index e15f58ab260f22e86e06e932e48ddbb595e191c2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 500 zcmeAS@N?(olHy`uVBq!ia0vp^WgyJK3?#KGrz-=ggaDrqS0H^m_y2{g{}(dbuL8?v{J&ccQnMoBOtfXDuDYR$vZ}JOGEih`_(=l|(`1_xB_$Y896QZ{@T$SJL zBpCaiU80{c>1dazcG#MTjRYBQ=5K6Nt7v3vZ+tej@z|2aWt$t@4ke~tOmu$`$i1g( z_8xYrAM7>h9AEYbehNJrR$Xz1^GBP=zSGHKHO%wt7_R6)7h4YuS_V&7KbLh*2~7ah C_2jSs diff --git a/mobile/android/base/resources/drawable-nodpi/cloud.png b/mobile/android/base/resources/drawable-nodpi/cloud.png deleted file mode 100644 index 22261d8127517223d9da95e10a1e7d266e6996c8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2188 zcmV;72y^#|P)Px-MoC0LRCodHn|W+hRTPJrnJ#J5f>NupYCx=sCK{+|rL`?!!4SZO5abUH3NDa< z7=lU^kxg8PBDj=TVj5$_M1?9r5R|qoQPYSTD}oZBQoyznOBX5K{Laur)jb@l6|&%uKSW9sYc$GBXsJ6c*=Qpks4>OfKg_5Rdr$*ajv zk{>4jE;2GQKP@e-JQyyyg3*cHKKAb28{u}lCj-Q6+DsvjwA)tX0I!+s0BLJPM8v1* z>FK3H?~bQe04LFP@#4ir0QDqJUgLzLfY^w>O&*VDMMg$Og%dh`b@V83IOZxUDteH7 zHA#k|;|~CyNB3G3uOByV+y$pP=rP~`EU~${`9qw_cFGwmJplMS!*?pf_lT98U0#m> zS5Q!JCkAc-w83@_bwUpC{-MtqMBbi2^|=F8(AD;Zg@yOi=9^%HCU(;26@d38FK%8} z?KaB?HR3S%J`DI62W+jDIN5+~5pA}!+Wty@hT(l?=+L3n<>lqqHa0d6p*|eBiO5ZZ zpFpzp&}M#CR@S?=gcU+T1CB@&=eFAf?HE9_z{N^t*pYCv!oYselzn>qm`@`kI0@%U#}sP zWF<#mm28@m*)*TDl2h@z9XN>uqD|(s43z-}4JV3lVun*Y|JJ=>9{tWR`q1P*#mC3r zo}8TApy4aY-M%;g+QQC2!?L%Lncx&?;_2A2WB<*}%$&&!S*7&+GV#{h+Sm zpCca*JzCWS3)BdeV(hJnii*09A%0H9H`BTV978X*rlw{t zfXF5`o#Zhqv|Hy5&t{0`*_C%fj@y?9(DRW_7vMd@-tK)J-DR%hVKG1eeO-;MdQLEB} zpD_&$4YqD;l)$0D!|wY{qAZtkl&X^eXgg$&6+rudnlcnG@bUya<%7@Tl?)S62^^93 zI)E)S$vRTS=$(!z3P1@qXd@n;WFf%GcYFmj2W=9c4}|9?sB8)%a*@Q}10Il8oT z%=2%^k4ZWip?iUAJw|Cy1VrOvhUZsSib9EpKXQgvJ_L}9k+(<|5CD8gDFo5jj!f4H z@CFXC2yD>tLzJ$KIKv+Rj0e0t=6>1Bb{M{Hx#fV7W;8J8@SX;|pq?qsHaK0!r}16T zIfuymzzm*Su7*(30Z~y5U`LtP_H?0>UN0)wOBgw|D~1BvEWmrow~ZTcGJOfA@-|aF zANPl3tqqtuTAN)8bvm$VzHD{rmgjo>)>1pEzd zDq7&IcG#xQL%vGHwXi{dkE7%yoT;~P$os>R%yG8PW)_OViY347gUM4mrwFv0_+lVM zB2N3!9YZy!UlZw!4K|(4{wBctMb|GQFDq=8ohx&>En2U$8PMwD;^KC- z0hf}J@;AiJzE$%8+N->gpNT&Cy)Y4D7u zQ9Cj~pMtQJj^y`<6a0GgXRy9^YiEd`75TshpoWnqph9j&bYC$UJygZU#*Xv`nL#mf z8oNRMGwOYI^J`$C$QQ64ye0Iup=t;bMa^9B%_84o(EiW82;>={Nv*?B^#zH@TS|Sk zM4>Oe50(r0Z=Ok`MvZE_r?Uivi@mjgG==Cp-Uq=lT+TCtCJf7nGg`_$du3(iJdPpD zpd}deFwd(9m}LZyT%~t-YyoGqlK)%a6a3i>UGK-!;W&f=s}6s2WnU|+r{TLf?-Fpn zjpf@4fSX1dLz4gC8!kCA1AOIra~YfETd%X&T}Jr`-gcK-)M1$pH$*4GLE O0000aKlT~=mZ=0k@LvFhHtV|?h) zQSi{ABm5_iLKfJ3C@1uB%;}cFtwV=OVp#q@I}Xhcdl}!=I#krdzXY)lJysA2{f#Ct~vs@b&*(AVP25 zzwsab&-9imI$VJNzqn%aMt_7cUHmX_x#at@#d zx#R0@-n_ZExI(PTgLD>az7wmyYinymOVFsPskt3{6%vQ=VpTq*Al=g<+1mthpgc)c zKOhZZploSr1!C?chz<^p93)F@r^zf-WtzMm z2zOa)t(lu!_;>gR2M`BxIg~U7x%@W`Xh#bR3sBBbGVzOZ_^(NV?R9g(LYb|jwY7~8 z9z5VJ`=PF`UKJm@w6wIpzc1F`cBQpeqyf9NwG~O4U0GS7QmJnU<5xNwbVvJ0B+_!p zNBN;HF)^{-zl_J@zyBs*+xm2G8>6xEGgMz+pG+oe+`RSYvk9p@eZ2q~xG*i&)4a2@ z6Sp|WUt6}3=hCu4URzre>;5GrD?7UNC%&pyOeDOR86#F^ySlmt z1O$wA*7awF&(@{}eJy|N?D@5S!O{19N=l0T;-f)x zb2F@|=5^fX;^N}O=?NnnhX)-^kGfiPhqDu*8zalx*wExqTF%+iTPKg~@vk52ymJ0J z17PCdI%SJ`Iw0dOAy6E#!A;tJ_L03AFRI)(QTGUE++7Swo_C#c~>v}RrlO&|HE5%Kh?Pm+KII}g=G{ps;a4Kgb(qV zz0hJRh?gAHDe0^d=39=GEO}V?aZZ!@$)zcQ7TLJjvUzQvK?BNC|n&wU(j zHF1%pc<{~kUVxLpS$WQ9sk4XcFH;ikycUmz6;QWMQ66YK<~(=5M6F(w(h`E3Jgc4N zAVX+q6I*{`oS@r^WM-kvdSHse6FIj*4pn`6{Fo(;mpv9yK2Er21|ZA zgI#~dV%eBT12U?!!97>X+&sI$Z(=0M?J~v8&zs8*|5Cs`E$icUZn_uXrKUQ%N;8*#^%a{fBPLRZIXXQ)1oOU;LkX0$y zZ9Bz?zY)LKx7qjU>#{6y6nQL6u;Q?;CUWF!YhD0z$Yo_sC8=xI&+IX%JQAAHtpX}S zMP1KFX%Y6;txQ5vBciho(Rg8t;nYtRtE^H7)`m^nGxdA}s5RpJ&Zxdei%TGt~sWbJ{ZVg3^=K04{T-IEPTy;UCv?E0<35VIDop z{ltUvkg06C_s;kCWD3J8s7KfBXV1=Mhut|!djBz8WQrUee!{mI`A(aYe_|$IiKNiT z%j9%+skrQ@&bie-JN52^bESfF%-r`j!~26ZE(+gXe1lw7+C0?5d_ybAu3}=6`35OZ zXi~Q#Gp{c_Gv(>+_n_zRl7|;S4sB;%0cM5#rrZ-8aVk5hW=`pskKvNb&j;)EAaBla z`UHtwl75pX^t76#hNbg3i~Y$*Z}nd{UZ64GI8xsBGB1$V^90`!ub)OQ8^@i3SdPSz zo+dR2P*ZOm`_k~Z;+oRWb3CYPDD63#!W@{Yz@TW*Cm8T(&r_1i?Z+`vE zAYHF(zHEGQh1^j~iaJ+Q9^1WT-JTzE{S_WMvTE2;!w%}_YdgJ-YJ(SqOu0P#wXRqY za+F^vq)FiCiLJ7qxZ@TjnITiseOFt85Pt;gQPs!tpS56y;Le-N`&Bz)Qm+pX&o}ub zkyS(KkzZnAnBJ0iVV3v~ab8r}4!`UZF5&F=^Y~?+VOt30{Ri3nKh*xumIS8Ni;~4j zdubD1YAYS|=5}!v6m6X+1NK%A?C)W$y?+>)yLP2Rbr~f_=V?P4>$O8rFXCln?xPSO zI}`ipLI+YR`7)dp<@74jLG3p;b@(H;{6ZHpPjbFc7`}8p?WOJWOrT=EHuI_4Tt>=A zY!Yy#71^51tOdTMCLIyxwdIM!<^W-l&OVR1sKW`^L7su_17JJ=c)g(h^bKofIx}je z)6($5(6*(DE;vp#%(*a9#g*)UI@n-;vJ~Th(o26Cbx_So9Ztqt$(5E=BzDGOTY<9l z{P%^0p&udmt#Pz<8iVh!v<2PN+eq4S)W=D%2)j|4SnM#MHxUVM+YA9Jh|<2T*ojxV z)L{%F&MS5D@mUCA&t4$N=!tb$dVy{RuPyZ3A&@>X`=to`vAdsqtFt%-<2m!`W z>tF7K0#og(P3Y-$WlriFVYu@1a-1XLCRjADKI#Cd=7`O&-3Nc$0ji?7m*9wo+$Lgk z^afdKZb}*+3`})cg4bq0z0v|@7JPrbM|1>!#Ok3~R86coh2dB#vGzfjNZkC|V^FuU zvMO8ZA2N0vRL#}}ix#Hr2XR8O^J^AhoYSPLMu`sif#&!MpBP@jiNYl#!aEdU&_$l= zviuS0BX&Bdo$;o!DzzRBt2giV=$HtFu9?qy{`X6O*g#8IsU}7?hl|Rl3V&0ioaKCj z+_7o{J#8`#+EwR)os7>%-&}X3WoU5Ft!GTAY%p>+TefX&T1q#Xx<6G08rdY!aX+2A zEL|e~Hm{1(>ORS?IQZ#-&xqE~{v?!`yy@dj~L$w^JRyA86 zeF6@c=BLIXoKY7|0vun+CDL86M5C#Uc!Vd4#g=7gwfzZLHdK}x3o&Tba`@03s?FZ) z-XhfiJWRy9+ObVz>ea|gDpUF$ss)fFVjKLsfRY_va=Zlv#ZOh+nA@RxN0uB7|4m1Z z4RwZp2jC+tq$HrJ({gm|i)j-SVDs010>Bn;ey=J4&aZ@&T_X2UqXz;Fzekn;@L?8G zC~&(=HU7)7MCRvGLYAGF1W*+}L<$hPp zvsvO~BA^_n`z`IBNbc|q9OO?~UMx-d#9~KLh9|Wu$V3S6c2CS#a&M>8FYDPE%0!G+ zOPtdnLGD_w<572)5;>_wCP6CavQc*zLmp6xu2*&iYxQ(!(S0x3=B;N5mKcBsuc{R$ z7mJ-=x|Lcw1_*m62%)?ttK;)Q5B^|%u)rr@{#rV=x|gSc4lw=stY@K5a$gm`Ls5tK z=$Ym?X~0b~&jcT!=;B(5Uv>Zk-$Ok6B6R7VH>FdkGq z-A=CaL6#Ock2kBU;KlT!<9q2LAL;c;3_4jauLDC%@s>g(+R?SZfwvHujb)m*V84zB zHNDeIHk`+++FiAZrOTrZHf^`o*RVgXvd`S;OG?tJ?)Y}r;^xPE4iO%XSuPQm=g*&C zBbak#`Y6zZV&5CNYzr@6<5TyF4tXB-O1Y2Q!h)IGV(n~W<;euqV2+PBQtjU|hj4Gb z=yfQ3J-k~;rn6uAYM#03JM2EvqB{4QwU=5%52lN{oy_A0vj9HT=P}F9`g6J1OW?xj znLagS*oH@B+ldHR&i;Vq8F<~J*|AcrEQ{T#W`UC2*T@b`2>g_E7~&*3bhIyYe;=&x z;sU!$E`N5Quo0|bkqGNfF48-sY*T2!@GsS1gbxoHxeWNPlBZ3N4#BJnUZ>D4pwMT* z%YbUw#ybBmhE*rPa4mYBjF3>8EdF^CTCi*CN8rMWM*U-g8n2G@8r>$gBLjG+gf z9jTR7>BSh3*3C}9)qw-g48#SMbn>_R+^yx5{b3X+o7J?_`il=tmxvv|fo#GQpS0S;iPvIMQpNdN8Sx59HrY8W%w}sRFu-o^{Os05e3ifQTy=Un`kG z3Q^L|7=0L|u#t~@->{Vc2zUoqW64k$&?Ko7MmyKQ2NKSGZrb@U!%7x0`}XB1NC|~) zykdjf^n`y`#GyveUKhAbT%{r#EmuuHNJUmpb0Sw}qzOXvR~9`rl)n8~kY1EO;dcME zW^_U@_3@vO2f3PB&9R*Hxtq@7Ft{#vKE~V%v+iHXO$+b!06&7)kptXqB}RU;UT%e2nVJ!owSW7cK~;xn3dj z?fP$K+z-J9#)$sKW+LX#Pau1>L*M6^cQA4^0R|^fw!Kpr1~ZfEa4wAkSOLrg_0|63 znp(t&mpULOd|T%@H&uz^;n6iXK8K~XzsyITpV7Jpw`iYef8!;|-747o$w>}f6hg;T zo%wx(1;tYL^~(Hs(qZsIZ_x_^o_=)C@`8LT<8izZfO%W`*2k5AuSrSZ9&3Y+}x1r6|>rMl{>iW(B+oSnIb@ z6mCy@QxpVK?O8~$wdW(#{5IAF=6#zvLG7Kqj{{dHkg%N=+>4L&XL?94uK2Iqqphlx zf_qAub&3?5fK#{dLD(yRe@PI`Ry=mll~Nf@zpRR^4?V?B8lEY{&^aWL@@1m=%*csG zhM6SN?2^`_0fIXdx#=;*u{W)NyJ>3l=lH2QnW#of4R}&ZuuRmsCO)u5DguR5I04@P%(>$&lp%if(JTWf zuGn0|#5Xyl9tmHXGTAw^rb@nk_PzTNz?Xy<`2Nn48D5W)Z3$?DlPKIh7=%~mbPqu~ z9Y!X8u{;6KM9JO`SafWha00D7WpEwZl&xe02ihVSb`jjCX7p~;ch}V18obH`I|AsB_iq=B~+ZIK8&AEdQ!4?D6LZ*Im5rj1{^6W;W z-;O~THWSE|-%@uz0qo7I2bGX->k-z3;cO8*{rpHqJDN>+I)du-YeEy0r(M|bQ{V>o zHw3$?=wBr$TQT&rt+leIqpb|`Y-tqYcs7WE@`Zuvq8rYy3J-yP8!}IyM|CJ7^%n5W zX-4<}tSvBBLNx6ntZ|VSB9X=8$KaVr`15AQkO-2|gdv~U9exf0wA|cOnW#)4Ru)Ls zw+9%*cc)3$r;%ow5={Q9F+6Kv;(;8nH?t>aX`!~Nu7~npaZ955#gwc+D=rGgvIAyy zoloc`ygRYyfj5=cqP(XlX3lS%=)nNSukfe?&bQ*oT^xT-Gtd) zg8OaHoH{ev zMGzFBZm8dZ0VrgAUK@Q=9AKpp32SInj^MPJ<&1j@y;H3fOHNgx`>HU*n*e2!L1>RJ zJ6bUeMgltYGkGq!7E+Y(46atCD2YjzeF7_d&t2NyZ6HOC%zLoF3O`!g)t+-_mp#-b z>es|IINQ75*?-9H%BLW$v3{*q?lud8dkQS#xxLHF)oO$nvP@jZn7gg03F30MINy7F zzO81)J&FEUN8y4cziSBA9F+Rd|FdrUmhH$FkhVV(?{}^5e54bv2CmXz+8M_V&jzq^ z#X7y^M?qsG`KdyD^$2Z%qSS#g^LksS=3Yz^n{oe0SCV=8`Qz{+uP2mzJDOEE_1AdO zi-wAZ34Zcgq#d(+NDjyO5UesN$fmf#M(d(ec{@S`onMps=hH_IZ6$!aSkk^W^@)PNB!nS;yN*` z(h4psk78=%DN4W40(Qwd(g*S_P3zZzAlF@#f2-Vs`&m&xm;`~KDSvPTj}{hAHeQl$ zIU1u*YFyvn2Xf-+mXVg79m_mSTF_Gz+`}?Pyf+*TB8?00tS$45D60`aA%iatl!-q6 zmG|w@#24??JvOo!fVgWpA)8Oa>vG=D#+pXgzCp4R1fnYc7VUEED+wq4b_O~#r`u-K zBN6^$oa2%_iId2lzS^pO=siK13=d6epTM%OG`nJGFwfXCGj%N!qYOF#1Mv_NY!S~hL1Jojk_SA7fI zBQK`Pgm|@Lt{V;!;_Q^?tjQ8?$Vl?9TlI*JJManm& zc)V}RkOZ&At?%K|XFrX`Wb8^!jQFr5tfpva76u@^q{@jL$V{&&#Pg)J)A2<6A5r(; z$1mZq{3t4l=A$2 zoEjL$QZJIdy9*W)=!+}%s|sS;64jH0J8z(0_VKiJw}dwI*%_;un?KUF>y`_pmuMc* zQ4fBPRampp1W#=L55)_!bb#r$=U@bQP>XevnI>Nx=d&O@KHC)M7T98 zNTJEQFMs}c_K9IMWu6UW9^aR!(M2&=7OVTJBT^7wh|`hOXdR=>XJx!E+&z+>bE@ zvfs>_)m!~HjP>gsjL#aNsZ$g!TE`M`?Zm+}I0Y!hwjSoUy(Yr+VyV8*HS2-2b2ig) z-8A>|Drw!poENe09=PkfPAduhc~J3TI}}c`vRceaRv(lOEOFEQGp>E|3U>eSH*AOl z!leAWTxy5CO$LhL2d_N8)E(wTg!PKp+laAR)LS=La`xg#W<57ctI0tO_<0I=zK3sW zK468WsaYLJk9G!|7DZ{ke3ZY8G8Sa}b(#QtDh zCkcdqcOPZ}To2BNl-gg0d&gIK*K3Ntr=xFA^GLQ7%&Lcba4s=onNXzr+#4Bcy%+8Z zxz?}W0+l;B!JtLa#+^{LcAVRBGP>TgGIlZgF3^U1dYH`T<053U*??2fWDNi$5t6LfI1Z73)q;P|1oeEMSI6HRxT+(8E(Uv-D|I_I8G=Gqt7_>TrP#T zF$jgJh~AUuiS}ff$q=j(7_Z;mvGsj?_6T`5mDkd|{nYo-AU3$8%Y0v&=8-U%yyC0X z;KDIMQ>46!FzJoxNq7ZdUejUCas!B2uYpiFV&*o*qlY1=2UC{cG5A5*-+D88!|9ZA z#g+zAa-;5LhF38EWN(?zsaSq;zE1|PvYPENFbY*>5M|YudKo!F6SNOguFj4}5Ujh< zak0GBq6abTjkS0*GI-_HD-&y9ipD%1NVggZxdgC7Z;S>WQl1XnAZ^a)HBYs?t$!F6 z)jLhd!Awq+>>I`cu+v2Az81d8dDa=W1?%BP^gbJx;w%5PSi@XuQaSAe4E43BJx&;v zJgh*qIRZoFt)F6|%Csm@GZu;Odr zxb&bWJOFwk(n!uoc=s`NKi?6!A`NJ!wOhuK_LlwP?CyIHHZhre0O$$cB2j(SQlT+>8X6_Tc*^q#subHAh9za*(xpR=(}TL>lXNXsMP^MgaGo@v?IUmtFB zfK#mi%;ELQ*a#I?8Qxuc-!{eauOZn+(o3-nCEFwIX<+y@|E9R2$wG}e%(hFiV5(-T z;&1*PbS+{>h#Ya|!b9ZSKU)M^3Ga;U8*G9%Bvjf~Y9QjlaWTr*x{>~M?daT&`}e#^ z%aI_mKktC!nCSIzWKPY8e0|0u;5XM9QGM8%D;1rUk;2h{&VGO{-m?$9pd9-?PEsPV ztBTma<8L}9OH6|AY-6}iDGL55n8SbcAM{$OePUiFD}-q;b=^$tUewo5!nPt>fA@=Z zVXJt>N^b>vI;$k|)b8CPFRBSdiCs6k{Y=#Pj*^20ZAuX;jTTD07lRaIO+Je?WhE}@ zUzJl*;*M#{?srzg7@8%Kyp=3>?k?O-v=uQ<&6UjZAU{{uR6FO diff --git a/mobile/android/base/resources/drawable-nodpi/firstrun_bookmarks.png b/mobile/android/base/resources/drawable-nodpi/firstrun_bookmarks.png deleted file mode 100644 index 28dd68f5205c1c9e425861d25ba8ff84e51091af..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 19817 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 Z%5QX5lh=a}{AK_E002ovPDHLkV1iH8Ym)!~ diff --git a/mobile/android/base/resources/drawable-nodpi/firstrun_data_on.png b/mobile/android/base/resources/drawable-nodpi/firstrun_data_on.png deleted file mode 100644 index 3dc037a48a234ce9e64a2674ca18b16d7e772079..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16918 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 BB5VKv diff --git a/mobile/android/base/resources/drawable-nodpi/firstrun_readerview.png b/mobile/android/base/resources/drawable-nodpi/firstrun_readerview.png deleted file mode 100644 index c611f08c9b22120cf5bc8aeecb759d0e634a182d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9895 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 z zOW_o`5qj2ySIg@Pce#q;MrdZ9Z4C)8lTAZtPA7f62rt)h$`M+K@HI)a6=548EeVHh zgwzSI?PoTZUfHj~HbU{V(a4|?FE(giL1-y@Q9a0z5w9Fj0z+uYM!cK>#4BaYHbSdG zyuP2}0}!SCI%p%bO1-LXrx1%5x2xpQ5K1H#-)OWFGvlRJV}n>5p@bQ)ZLc;QuaviI zkd07^zIy$jxOB)X#e+KDI6~Qge52afDlfrbFO|0%)eXQ#D2tfa8e6S$X+d*dDV1AW zjT%@Zv>uVUWzcK&#@2qTRW28crBbO00000NkvXXu0mjfBQc|# diff --git a/mobile/android/base/resources/drawable-nodpi/firstrun_sync.png b/mobile/android/base/resources/drawable-nodpi/firstrun_sync.png deleted file mode 100644 index 9f2f3cbc83ced80e966282ae107c3219e6401cd0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16191 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 diff --git a/mobile/android/base/resources/drawable-nodpi/firstrun_tabqueue_on.png b/mobile/android/base/resources/drawable-nodpi/firstrun_tabqueue_on.png deleted file mode 100644 index 5439f7a5c8d84f6d48d3b81e1508c9cf0d803acb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9444 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 |Qsi=hDVxZ;Y=A1)=-OYQ=u@QXF?<&O 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 R{-%e#P|nH=B~;tHhy zgTa0{~|di^fYSq3FRe!&ck%q%>-;)-hOI=c1_j-Fnj;ZfgnPb;&FU#*v+yg1A(~_AzcC59*mBK+Fhb&|JTRL zZxTpgvfSz4_+*|>+B+Qv=Lw;6rcd9M`O16dacw3xg}TD!vUj-ayF3pGyB2V{cgDM~ zI{4AsJ4)^a2cP#G(JLh)r41Xm>032kFLRjn_6qZJ9j-e+@9-{qv*zFZvsEUorQSBB z$G!8o)~7GLQ?bPFzL0wDiMM@o8*8rDHSVq6IQ@O_pG_imHOq4&u7`fTdrF~}yM9}t zz{je*DbiYA+E0r=i+r4XOQ3wE&VngHU#pehJ+4x83e@bms32;U%+J|se!`-cV}A<+ zLreC*i=Q7huyM<^3o|mvn+h)WG_GaP*kLv&bJpRnK@0-(PTL#!Hn%*tZWP^r!+2rk zpATF~>naTvGM;ajDB5=7OopWOT<4Qw6D4(%mV58m;<5J$LcQ9Miy7nSFLn}{%*&U6P9a|qcDIQ%q z)u=7(lv1IPaMWyzTPCgxmaN>Sp%BpBw2nnl^{>CCe0@MV^m}Z?;!Qc-+k9_qdwTeI!GVG~FBuOP&Dkh7FUQ^V_Q4BPJX>X} hZVUdlvP$~TtZs27{ritSKY)pa!PC{xWt~$(69BE6f@J^z diff --git a/mobile/android/base/resources/drawable-v12/toast_button_background.xml b/mobile/android/base/resources/drawable-v12/toast_button_background.xml deleted file mode 100644 index 856d070c2b..0000000000 --- a/mobile/android/base/resources/drawable-v12/toast_button_background.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/mobile/android/base/resources/drawable-v21/logo.xml b/mobile/android/base/resources/drawable-v21/logo.xml deleted file mode 100644 index 568cbec00e..0000000000 --- a/mobile/android/base/resources/drawable-v21/logo.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - diff --git a/mobile/android/base/resources/drawable-xhdpi-v11/alert_camera.png b/mobile/android/base/resources/drawable-xhdpi-v11/alert_camera.png deleted file mode 100644 index ead824430f02165b9db3207c86c4d50a19da4986..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 204 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA0wn)(8}b0D$(}BbAr*{ouWe;K?7-8Scy;9? z+f9>~d%O)a3Mya;Ph9hVOIq6g^~U!+t&AkO1QR=t_sOTm7U;@r*R0wVAtzIlwShw{ z$LagW6{^#EgP+*#ekOI`src^m{ZWfncA5tStC;GV2zdHh^D_K3U%+;yK`NtuWk%t~ z?Ao1qwTClnPv=x$USFA086{{T${*){O-;%81G{GSv0^=&hr5B!WAJqKb6Mw<&;$Ux CluM5Q diff --git a/mobile/android/base/resources/drawable-xhdpi-v11/alert_download.png b/mobile/android/base/resources/drawable-xhdpi-v11/alert_download.png deleted file mode 100644 index 1b8f59e567b994703fbb7291b998984abda0adb4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 273 zcmV+s0q*{ZP)7L#}Mp4}w+NJmQ{@;{M_8mWS)fHo2 z5V6J`@2+u@CHDF1QnJJkEMNf(Sik}$1z08vTXg*vI3)|GzXxJkcgK|c4o9n;{EdWG zOv``hWtLDgbjkndXN3^+^okB6Y~gE-5z%9e6d%WoiXS2_(K=;Z{L!F^Mw5nwVUlb9 zbIpW=BW~$i4O~m#YLMu^)ziGK`&NT~mRtB%`*sW8YTs_*TjkrQEqtqdJ7u3pjQQvu X43)uH3^;-L00000NkvXXu0mjf;x>C* diff --git a/mobile/android/base/resources/drawable-xhdpi-v11/alert_guest.png b/mobile/android/base/resources/drawable-xhdpi-v11/alert_guest.png deleted file mode 100644 index 1a17f03bebbe010f9c9740e71ee82ae2978a9d5f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1016 zcmVy000BONklgAC{s{SvSe9wB8v(ZT4-VENLUJ$xfX_*@nHYL z=tk!o?|R;Cwm6^gVEMt!lOPC!AP9os>S8`G@E&{VA>t@ID6&#gU@Q+)=3CAH=c!X= zEz=|&W>X=;^#!-cu#jUfuJaTaE5pT7_7bOur$vbol!>>4*)kl=;V8-SqCy+VzGb3l zF@le22y7A+pdN4t1p6nimtu)_IU4I?stQ{Y?ttWZCA z(=_B`jH+mG1HRhqG4D}gl*FJLqtN0(E~!ctMSRs8zQKFV(}8J37)vDvUtr|KA0Ojy zc0;gy%N%iVKgJG;#diCR{BZ(*x0z#X?jCZZkWCmRaX1gtA-zuaUfML&+a`e7$Fn zam$j$C=Mm(Jrg;Bu|T(1>hbSOhEwPpP)_dTynykO=+I6gcTq+MM{NQ*xQzifNE7U^ z#ZHI2@$t5J;|q*J;%f9-(c%kQ>0!QVqioa@0;4Q?HfgOgBqJAXN0OPwCm=pDt@0lGHeaajlucWuskEqr)~==8%;(NsThb3&P!Gi<71&hRu)KVEXDX%XeTSPB{k;QO3DthGEGur8pau+&cDGK;VAzGJ;E9O m6@-X?1qp&62!bF86n+EtS4kV3B$0gp00003n&JiY!YZ`_;83Hd%~m6g%% z$?BjoTQa&Pi0Es|0yOE1vX1ox^~Ijd!7g8@B=ewWn+N(1S(gc{b3oq% z+xa&28+~uYgc$17F@(fp*KGdT{=hO1SjmrV5*A6=c5ycAF-y?@W diff --git a/mobile/android/base/resources/drawable-xhdpi-v11/alert_mic_camera.png b/mobile/android/base/resources/drawable-xhdpi-v11/alert_mic_camera.png deleted file mode 100644 index 26ba6520b91a303d431d9d661ddaa7ed75080d90..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 268 zcmV+n0rUQeP)h%)#1oZIF|b`RbuGiu>@rwiQ@T8%uLh3EZ&}s56<{NCk~wt&dLcs zNaI8Dz%?p!Y_g+Cc6dS~FM6KnHQ*nGvm7P*&Mr`aU-oKiGIKzY`oIQ`>l6SY?F(v| zcb5&%HO;%L$TsgKhYORsYei=-Z-a{dm0<`grNQ%D|bJnAie7%+})) S$*0=@0000{5I`-MSk0lVa@vX52!x%kbR%geVL&wqbr{rj4<+yB3MPEzrlwCdKY zXK}jMI^G^R*71kGSpS<)_1{~;IgLJiX?9vZD}K%hsBhgFob<22HD^K$U)oNoy*54z zPR{tgv98Nb3l+;(R=kf{4t z?zFe@re(;C&J_m20n!_l%A}(gDS>@!RK;AJJ^Pi^-!gq*5HfhW`njxgN@xNAE+(5N diff --git a/mobile/android/base/resources/drawable-xhdpi-v11/ic_menu_bookmark_add.png b/mobile/android/base/resources/drawable-xhdpi-v11/ic_menu_bookmark_add.png deleted file mode 100644 index 8084baf9847fdfaf2092edd840fec00d46f62918..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1910 zcmYjS3se(l8VwHvM2W9}7+$p$8w4M`Tt#sKgCPhpA&KTWGntUdfGDZ8MOttz;Uyp} zG(HZMm4t?c0x5O1s|6GSt3rkH5XC5pc11y{@=&9+(wT(y?40xebMO7{_uX&i%sF$E z&0;u{s3Zb`;LMDnCt!CTz8vka-T(Z^GPTm9Aw5vJB-*< zoJi|#tDQy&1lurn94{I>vR-^XKO!O`I2geIlgW&LAPmIEBT}gp17fk5&qvr0Boc}E zd^rFBLLtJJ$*@zQFp&>I5(y%JVE{nI;zWT=1^|diE*HTt2qGd#2>=QqgotHI5L8Iz zaxsJeK%xXziXa6bmrG;_2qsEl%!Z1gL;zMwWC}5e0x%+h5~VVQ1i~=@D?k{rP$eKk zrH~SY6@W}Bkz+?F2%{L5$`znY2|$SCJ%yFHB17;JEJCi7Vy$FYLmY#0rKK~1d8}B9 znei;tLQ#x@mi|)QiC1AkQaJ)z0AK+Y5j({iVq;(i9N=cG1P6E!zR#CffmOs)EPhO! zPsFJ;=zSvA_Wv+G_Pha)vzo2Z)=*5e>iCWgVD(!%;Dr`yZ8%RYGyG4%OWtG9GCxk| zf#nUz(Yd*~eCMu<*r(HcFoDG-*x1_HFCaQNI*}GGA}?ODblGx>i|Y!vl~i{RPcQEe zeSBBb*7ygkUAKNiU{G+##!Z{UwrmamYeXb{2O}mfp0$h3*}X>~6p1BLK$fUPllP|V zOFj76q4bQ*!+$&SRhBwCCpWL4sJNu`Yt8YK<=>n>Q>m-cSJ%|m)i)TvZ~Woxx${5% zyQ#V5(&g4RW5<84T{OQE=7n4)dFJHYj zzj-_R`ycOSB#J!jIc(sa40=oWV1GJ+VE2|ukL0>F9Ut*z<^Lyn7`vy+dXVDyh)O{te`5W(lK+K0W< zy#hp(vfZH}JpHP($xrsE^Sf@1{qwO;nPEU>w{(UWFJ7`aK13SX(cH80O}>WMgGOa1 zK0STB@S6D_SqAQf?EGs}oC~V2vMza*s>*Yn-O)`Yj;c9^)(O>BDt$6OMu+TEbPpV| z*G~B5)V*kmm5uvm*Oli+nO*u{cp5b`f5~xm6Vc+3Ra?*Wy>MNWI>0V$4Dik=09KD>E(Zt$1PZ6-~du4p6oULoW27T1yw&c-eVpS%w2jR*rV;hbN8au?AZC5?+vwv!_U3l){Mm_h`&MiB~Sj${L8YAmJrP|Wx z=(YQNT_J_L{w;dlXLLpEL?`>iT~=)%ak`)>uC{$aw1FJkJ?Z$NHmARZLStTJjNLq7 zf84x9Z{X2lCUiGS?HvsJW5bgi{TlKTQb`{A*WB@VhT~X^<~QXB*^lQc|0`U;sN1R` zf)O$6$((VqvqRl1EnH-`;C6}M%cdoox-ubJMd3_f~`xZ8tz9=8R1&h|?uxhAP_cz4-Ch}IAlZQ7r4?M3WhW^62K-@mn_lc>< zJ3jXTH7Bkn{c9uB?8`eA)Qa+Le`h7ovdRyT9<`QDt?JoZli|c>X>F5>IYadmGyO1} z<$ABegpfY2tS*gWZdp6?Q+<6{h3kYb<*=H%-Y&$Dcfam(P%A;xVcx{}897A~ZMw?y z;MMD;w>Aoz2cnx&Y7U*;y|_yUEk8eN({N8+ka_y$<97o4{DzDU;UhMuGieujU3>Eh z&kSSItQL0APf}N89T+U)nrvJq+h?XLK4~wVEh;(OKFKc2%zXB4GSo4tJMj6?%acE# ZrGs-P)ZX0K&~7sRS7b)B=!PgL>rcxIKGOgI diff --git a/mobile/android/base/resources/drawable-xhdpi-v11/ic_menu_forward.png b/mobile/android/base/resources/drawable-xhdpi-v11/ic_menu_forward.png deleted file mode 100644 index 2d6ba52ac791bf82755f57aa4ba451436e6d2023..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 368 zcmV-$0gwKPP)IHjwc|JHKOS3KB)KiO3>Ls${vG;!6-!J+5+?O*q$0SLT zBuRtP;_B8!Y$I00%0&K$IUBoA9N$ax diff --git a/mobile/android/base/resources/drawable-xhdpi-v11/ic_menu_reload.png b/mobile/android/base/resources/drawable-xhdpi-v11/ic_menu_reload.png deleted file mode 100644 index d456630ee902076111fb1a98923f1f20a1e69412..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 777 zcmeAS@N?(olHy`uVBq!ia0vp^At21b3?#L;KKKlzdINkyT!D0aa>gJ*A}lFzsbGA2jdjyf1Cf@8Zp_#3-rf3r|L%$AE!r6slFy?L#H**uPVrhA zWGI@>em6_`)~(rco+1x^P4->A_5PU}^KG6QM(WoyCr^5NW&8PuS^cjrzNsJXQ6(bXf`0;aZhI_e!a%zpa4 z{CHVPNz7Zzy`9bZoPglJNr7DLRfcC@HM|Ne_Bk1M#H3nBV5>yba+9lz)V=0O?stC` zczM|t&&*Y>(o>G5c?Ijdy0PRNR?__{qX8oid+z{gv4XtrPGbV zJyH!GILdp~zPOx{z*Req=gh+X;|&H2&-Pv7y%nRmb-~%TNzA#crnKL-g2$S>N7ny7*WP42zocf*?Y_Ic)uH_gR&T~9ykRxaAia$n-|xpl@{e;#|GwBNRP zqjbPV@iS(54!x$-$IuRCDc-1EHFSF_vZCJ%afM2}quqb0W zoD^ZWJDuSSE_e%P)Yj z4tJv)9pen}d*<`g*~!a3Rd}y$Gz^z=KxDZ`y|WS#NLVGZ^z^HGb3`)(rMztZ$TCX3 zW1^W2lzGx)9&@+hk)WHO)x6w#`)gG!8A&cv^Sdddo}E;2c9gKadIOtlIOli!MJY?F z*Dq?!FXx)_KV_{HL_fWw-kQG2~lvc zv=4pf#>g_ZBqbam<#pXmqhDm%qilgQq?{2I*;%urqTo6yuSZ2jXtqZb+#>D5Xkbdy zL>ms%Y{gs*3U?Y(l#=s(?K_w0*D@3C*XRpVMnONnDtEuDJfH4LuF~xCe5*)%(_@ya zpN3kk5%rv;x)J$S?=6!=Ju6E4UCk|qM+*-nQ?$l+QoeJru}(17V$p_vT3r>*94zgs zDB+n(?a`FVU2cmuo4VVL+SxT)887ElE1Fq1Jq@&lv!%@#?f69A@4oQ0az{mbmN6l` zZjwkbURS+tnkeHKU2%)vkz+rd^NSHtjdgU!icy_~y(}$1H>z;3>JE?2Fwn(ves-aS zqw@@Rg0Fq&3pKy`+N*AHly%J*U1v)DOlL9MJIqezF|*z=(fA9#`h5dI^e9#U0000< KMNUMnLSTZpFkLbL diff --git a/mobile/android/base/resources/drawable-xhdpi-v11/star_blue.png b/mobile/android/base/resources/drawable-xhdpi-v11/star_blue.png deleted file mode 100644 index 56c9367c07eb70aaeed5493a9936aa956a73c990..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1418 zcmZXS2~ZPP7{^~=!V!XoA)*aJDTmd7sUp%iF{U{tIJIu6670+Dc~iXbgQK@>54o0w{+GrMo!{{F}NW_I(L3_nv0 z0RsRqrTbGu;FE>iM*47dY#CdC4`Z(Xz61bF%#j-fs%kbsXvm`n(G17X`ZzLS$@+K@ zL{@REFrLpb4kzX)555B6^G(c`yqH5a$K!Zx8$Wsw^AIV<5CGI9Gmu3?3~DuUomT^a zW^Gji(I_}S2?S5TxhZgd8r01qq+%SLRUwc+1*(-mFa~NT0DlrxtAOwiAQ%BPO2i|W z0F{%VY6J*ILFG888VCFdM1_$`C8!z&{4pe3IRPr5VGQtLWCT=brBOtx7ypybwkxKe=+n z>|ZbD7Z#UZF28!c@@Dm|?`gYWI4bI3fFD&H*?(0GfZjejmBLC!%{~Z?w%?6M&zZ|O ziq_fH1i7W6m7}1GiV2ck390SYPhUp16PMTF@$=FPGoeNUuRGD6;(>LACe4ma>6w6g z;+{Q?ZKy;{+qn(*B2-n5Ooyx^7;zW9aE3%Fq)w~6^6hpE^mbFS*@QYurhb`A593D1 zs0%$Ow&M=lIdx-XT0PCpZP+8Q;F;arlVihqnV*ImvZJSQSC+lPOXs4ZKj#EJ_c>w9 z@C+1Zos3=Y+F5HFYrV=>l7mEt!xOH$uj9zaA5W@xR3F~#s`z1b_SvgKE?0H;QN!Vx zq(0d-k6n6~I~M@xk6f} zlYNo3!>Q}GPUXV7+)m$Ql6%+MT$w2i8+O;1JQ&dp_8iTYaQ0yR7n7DRT`oeg2hpM> zm;RQA^0~<K>dIN@!>W0m&91>+}-YDDmBIUV5w$kfQ_iPVu zQjYi6C5{p%TGhT~U~LlzuRuuy@EurbJw6pB&DgSp4%vba$YSlh6>qcCyQvA+^}OCB z*CwHW^f>3fWBVMz*)uLS{`;hKpG~5@`}<1m=mZ(&lYZ}lHwj@}*J29}&RBGo{wlGd zZ7-v2$h3q^kqmeA{1zG@DJyC7lP^6zyUi%=Aw#~>m?2jlH@lRVtoS|gqRoqn3ppOg zGG$vuxoy){_SW2?<>Fdy^THi6-~GhR+a9FKe0LyPbF-UE8THMpak7eAO~j^KtqeJb dq+n7gl!bah7LIk(5 zcRwKM07j1Peh_hI4}T!T*)zbyH^c=@0u{Ua1iAYJ1NFLk1-f_zxOxWy1%cXuYJlQE zL7)_n0ahOjWB?8E@C|nI@CQl(5s(X108tDy9LNSDpg9*A>Q(~XWKa_17tBz=Byr*R zBHtxGfP94xF&7)I?&g@snIBwrM_n2)~vVu2=SFE{3Pf5{vlt-h8_I>dwQy zRo(mc9W0spLE&ZT*QuKx-@UW+pPpg<>+GKB2KjTxCLKGyllfci&7;Bz^X&J^t6Vsf zTDq0h{&Mlu<}kIz#UHHqUMVe@;QMajmF(#gM2svCL^|h}BhiU(|?7-*i*uPG;Q8=qa51zwU3f|9OPbO1Em8z6i^WZ?(0~ z2U@>>bC_H`Ip)5|ik+L~U*BfBDb^6SCZ9*+UCZ}p4O^_|Y)|FCA^0hs_09JO^+34a b{+7G*J*gL$b9`%o(aPZI>gTe~DWM4f0lXJF diff --git a/mobile/android/base/resources/drawable-xhdpi/ab_copy.png b/mobile/android/base/resources/drawable-xhdpi/ab_copy.png deleted file mode 100644 index 131b4bb1b7119c81ec8c2480d2c69c44c6e0910f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 174 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0L3?#3!&-4XSJOMr-t{_1-Ur)bKM|VG8vEP4y ze3p_RzhH)Ig6ET_Ue*NieLYU}D3?2wJNPBlOOmVGmyb!>y z(#Yn+D9{?t@JO16@zU7?A_^yYR{d0EQ50y$fPKzCqfx0~XLXc#GfqH=|fgPY(|R?bav>@(X4-xo*p0u{qbSUg9{%SDI+PyT5(P zN&}YsX#a(cTp6yKk_U8zY`ZJH4)b3UVPIg)^mK6ysbG9NjrH1W2MJg9+Qk+}Y`6Tr zpQwK0XYCT(sJw!mH`EoiweuTU|2H2<-rt`fx}~}?qA>s9%!jTgY*{@Sj#RU0gdcR_ zUGmFNBCoD#4wJPl|G_%3lG{P5PmBc{m=&)xd_3PI!r(ps3A1F}8~%;gB;F+`FVHlb zw8t^NV%dacnh+hyT5p=(Ag5){g6&H+(+jwoW3#NyttAhix}KM2YrY%Z#0#-AzHyZLQ123Q=R;&UG>SHtQ&rwzILr3o^j`L#xCFQ?{ELsiukX+e$5|7!BRzi#bdW_ Q0V9gR)78&qol`;+09zjLdjJ3c diff --git a/mobile/android/base/resources/drawable-xhdpi/ab_done.png b/mobile/android/base/resources/drawable-xhdpi/ab_done.png deleted file mode 100644 index 0639b034eb966c99b48491656530489f83708b97..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 501 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I3?%1nZ+ru!asqrpT!FNcyPv0DsJlro& zxxTu(d)+q2j`f|=Vsnd|;xhuTd~HcK&xpI4TPouDI6d><&z>jkKSC3K7|q%*V*m1a zoBYxD@@gO796!zeZ{M@&%^vd(TK`R+w4HzI;r=;gq93E{PM632HogBcL}fky(>kFD!ox5h= zX1{0Q({I*NkN#xLI2#)M`}>FEIWxp(wcF<%*0|UIKT+nqa_1hFkCv8}2` diff --git a/mobile/android/base/resources/drawable-xhdpi/ab_mic.png b/mobile/android/base/resources/drawable-xhdpi/ab_mic.png deleted file mode 100644 index 79c459bdbcb77af75f28cc13f25399d9b5b7acaa..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 412 zcmV;N0b~A&P)_XC?*1mYyB#1E*5EFY`6fQg3NCM4-unKD zh^bK8f(_g`(gtmr3MJY=w!9CUCpkvhM4hQnuKB1M&PrQg3rw^{Rm6r!#D=*;1`y@npF$VEsHu@SC2b@QQ+!vCS1iR6pR`&`BF$ogmB-kX z#W3Q5CV1YTynxDyG{TukL#1QJ#?#hRBJpKDiuS?gm5%5(-Nup6+~8O=5g}KwjpB4d zt8GC<2`Pd$U?)J@L-TwTQD;0?E_F*~QXbKR*0A_53a|r*{O>~=)7PH>0000oN9I(!&9dP5x*T^cR)SY!GGSd-W{XeSi4+|;|(5PC4Q<@cHmS`{4u z3jLvu3QmnGVXV@ETuU};IxseeF&<$)#3Fj(8ZU=K0Auf61t7Whuw%mlCaa^Yj;v31 jCx$SxE!f=@$iR>>QFF^4<^BFZXEJ!Y`njxgN@xNAsrzBi diff --git a/mobile/android/base/resources/drawable-xhdpi/ab_qrcode.png b/mobile/android/base/resources/drawable-xhdpi/ab_qrcode.png deleted file mode 100644 index d0d4c3a0cabb193569d91906b561bfa35223a76a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 177 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnH3?%tPCZz)@o&cW^*Yz7VuivGATGq9I`m};lKTf1ONXAN-SJB#n`~NEwYN`9nT~g{_jRg0xpV5iVTfC V2id1y@CP}C!PC{xWt~$(69A{gIYa;e diff --git a/mobile/android/base/resources/drawable-xhdpi/ab_search.png b/mobile/android/base/resources/drawable-xhdpi/ab_search.png deleted file mode 100644 index 67063dd6c7e9f1a9d1a1613996753a8bf653ebe8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 989 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I3?%1nZ+yeRz-STR6XFWw0zqhKsDp!pv2mJ1 zLIRNO78?sB{gRTRqoV^rBFTYqagmW}p^1qxF=-JINuhBW;jt;viRt07$&qm>;YsOH z38_(W8If`65s9hM@flHZX^{!(G4W|J@u^XX>5=iNK*c~510)ktqZ2Zs64PRU3KD>V z8Bric8kmt5lLX{uL?vYa)x;zLWz(XQK)RxUY@lKgI|E1p6@!d|m0yg zAT6@wT+^AT2n3<0VHx&&qgL}OCMueDP?0s}6+B*-tA zfsvVom4lO;hnG)4NLWNvQc7A=OGi)Nz|7Lh+R@e9&p$XkCNU{BGry>;ysEmsp{cdK zqqD2Kcgoah)921xxq8jojhnXY+P!Dr;ghG&U$}Vb%8grh9z1&d?D>nAZ$5nd^!eM5 zpTGb9tBmG!0Y=+fPZ!6K3dXmWnm6eh3bZ~f&AGiT_x84(_x|(dm6~OLD=}LBY})s5 zw|{*TrY&0~d(&>;@2Y(_qH|6koj>*LU89G|t2yg~EI#`6&$VK?{^LN%{$xpx72mt} znSB;GA@Vp*pWk1;Ua~k$Cc|#=x%O-YuifT#5``03mmkxY3XhfSSbZ<$$fn=_D7j=WMQMnly{Ny%0NNJ=0koSM8GDDnFySj`F#RmUN5U{V@Hr<0O5_3tIzo4#Zm@ zmD4FY+cQ7wht(p9#xn)J}Ki!bI=wfro)P9lI8*VTBk<$=@SzeA4`fo$4bc>FSVoIXiL$2MH9Dy7g j@jRu?6^qxPk=Te5*G|k0wldT1B8Lp6;Bt(kP61PQ;vqRBucQ@t1vbx zbnF#o@^lchez#eQQ{ANDBty{+;e8&DFYxEb{96~jH~fC;kvnyF?(F(}ciHC4M^|Ue zvu0*poO=20!L_f=WHm0#oOzCG=Q%%K*4=Sd(fONgo?T9)gd%Kxe`{r=DW--J2WKbh@`|k0wldT1B8LpTTd6qkP61PXH+Aa6B&+tJe=w6 zU2*7hRm8!y7q80;vDWSnFIax@Mds9hYbLol-oC}f*M2%?|IhdH-v=x!`(yoQy8U0} zwziU7<1K&Wa}7a+5r{AV5^If3mSp!XRJEQRnSAQ3@}*Y~w1R#66lYC6EV^upW682( z6}*$*acsU-=ovU0N>d(Kf5ExKIRJr5P|6+Sn(|y~MhD-c+9KC$~;wJW) ok>6_Wolz=29bClIy5teByxCt1?(270fgWe@boFyt=akR{0DvEYkN^Mx diff --git a/mobile/android/base/resources/drawable-xhdpi/alert_download_animation_1.png b/mobile/android/base/resources/drawable-xhdpi/alert_download_animation_1.png deleted file mode 100644 index 22d6288ea41bdd2b77b1f79a10ff445976f55385..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 266 zcmV+l0rmcgP)grW&{l!+LX6WK##`|&1%zOSG{wS^M z*JzE`s^9V-{YQT~ilX|9t*+v(`T3Vej)VK8>C2iV!Tq|rqKb@$b}`f?O}oojb2$*T2|RSyOazG39p3%#5FZzn#I; z=H{lR@b&)v^PbmukJsWY|DpfTpAMBue6fWE+%-DN7kgk|Diyv+n&N&94c7A6s#QEp zh&<=BSliHm$Mp1sz$ax{b2FZz)YcXs<(v?BjwInDib6^}*U}Onu(8qJj+dy_c#jbJ z`tUNXtK<~_N5{wH;R zd~I)!-#9rL9!88tgJ1A|Y6@`%2d(7?AxMdMLL40Mjmt}=kk9;l2(N9{){vi4mzR8? zHHy)icPbSWx3yK|s3dWYZ#aj%_`$z!lp3W*WVLg26BU%y+Md!JiV SIQFvu0000djb>&fx~kE6pA1@0S?CzYB>NF1qy+~RUr^vu>wMPw6Jdb z`T3I((*kTDL@5_mmT(T^wiU}ziV$3}tZBN>)U~F;9YW^$I@2^5gNQJ^%JjWqfT%1x z%Z#IGf{dy<%1o1OgB)FNntg^bLO`Bj48u0FEUpXBI1XcL#`obl*IkxxD2m{nDB_&+ zB!TyW;MG!!@+)1pJ=(5&9NIr4Dx^Yx9g1TvElm$Xo+lqV`Cp1cE|cf$><3T7gmtda RYKH&-002ovPDHLkV1llKX;1(F diff --git a/mobile/android/base/resources/drawable-xhdpi/alert_download_animation_5.png b/mobile/android/base/resources/drawable-xhdpi/alert_download_animation_5.png deleted file mode 100644 index f8d1a15d6c5dee25985be1fd2c5f18106c019857..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 270 zcmV+p0rCEcP)S68oM>rZS|YM3rAhFN&*4gX}r=g+kl zB76n{-#Sz-Q``DFURqqFwjjW3YBg%9R`I%?o+zTSdL3_boK}lUqNt|_A`=s7N~vbE zzaKK5cX&vlICdPUD3vJCZo4j2ZEo@n`}@Pg(9vk{6=^y(1wDgrZS|YKHE)XU5wdGd&pZeg6FR z$6NRW)VYv47kXK_Ol_;Hcy4~4+M)UF%%^I9z`Ns_KEyk%@m z2)?V??Cr&Sf?$82Z%R|ogNjm#Z)mrDAF9^Z`HJ1$!9nO~H24A`CMKY#zdud+gp@%5 zeZIfB$p?;)!w_a>XQjN)L=nt|;qfuA#4+r|F)vgquvaWb5gh0HWf>pHvfs7rbFKV@ Ub-`~f1^@s607*qoM6N<$f^{i+T>t<8 diff --git a/mobile/android/base/resources/drawable-xhdpi/alert_guest.png b/mobile/android/base/resources/drawable-xhdpi/alert_guest.png deleted file mode 100644 index f48ae407cbca540bc14f235174a9db13f800395c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 289 zcmeAS@N?(olHy`uVBq!ia0vp^At21i3?$W-HZ=h$wg8_H*ZAZNAQF0(DGwB3ED7=p zW^j0RBMrzo>FMGaQW1Ccf+62w1s>J_fgs~wY!8(}i?T49f*HGmziadB-R-|vSs*2I{ChON%D1~Oemv&>CI4)ZUH;sQA?N@4J8kDPj|h+d zzwza||DFdAac>Uq-p+V@=HxW@%IVk2|w%j?&R9D73+6)_y%atIr^(% la>P;n&xPB6oK^2M->o4!=kEpXYd}vic)I$ztaD0e0suU~d4B)^ diff --git a/mobile/android/base/resources/drawable-xhdpi/alert_mic.png b/mobile/android/base/resources/drawable-xhdpi/alert_mic.png deleted file mode 100644 index 527d42c4255faee4cf8d7f30faa0a86e3b0008e8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 404 zcmV;F0c-w=P)&RQP;n)anzz@N^RqI3R@QG53=t9l1MzjB#LB!8EaA0n; zf5PD(M{1UO%pI{zBEPw9MD>}QKm5@=XgwP@NB#^V*2_Q=<1TaLWjE#+Wb4&`2P|fe zdozg|*$oBBq8`&`+D+%=)ohIy)vDAsqD)~U6clVc$=bNJHi9yT*hz?hNkG3Umc&g0 z)_jQAQ8uB?Buv2KlpGk89LN?d!n8Sue^|VOAqgLNhr3X2!JQ<4KmJNcfO=4V!R@$9 yYh>Gj3X1`XxXFr&@P{E^pJLn*oqsSWs=)~N|H!Zpt~Lz-0000F; zbwrQY6BuDn^b{()RlHBE!?D0L_)Y;0ujN}DbJ!`en|sY1ZwC4P7`pgQTmxPC`{{!_ zjV-aa4y@jZniEctMaMpZOs{o$3-{F`z-pETDFnN=$-hzu1g3s2S3}o&}IgwlK zZiF%iX|TsuyK@-HY@~7DF)Q|EG-DRhNa(z?$V3_mokx*@G!i=REYguiLg!JWA&rDi zd68LBFJdAUX|Tsuhk6*v)IGB3u~(yRhB9SbK?=dIUzrMIN}YN^69xZA%2*&9Y8aXw jIv&D8%(4%g|M{O=Z*0Qv^dluU00000NkvXXu0mjf*Hw&% diff --git a/mobile/android/base/resources/drawable-xhdpi/arrow_up.png b/mobile/android/base/resources/drawable-xhdpi/arrow_up.png deleted file mode 100644 index ea344e7a47e002063cb0fcd1957817718ac45add..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 247 zcmVE3H^)Y~llm};JXum|^I%WN`1Z6l) zS-%Zxg_N0tl=a7mW3PZF*{u}5C+4+~lCS*ZuYy)FWtjW3+!E7?_hQO0@l7MuDI&HP x@5PjvA>)(UUSahvwBb4pDUs8Fi*~(BqC)AKJ%Re&q|X2V002ovPDHLkV1k6-ZpQ!s diff --git a/mobile/android/base/resources/drawable-xhdpi/blank.png b/mobile/android/base/resources/drawable-xhdpi/blank.png deleted file mode 100644 index c6efe7e8ef1bf9ebfa8e506b9f449d6d2d489135..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 81 zcmeAS@N?(olHy`uVBq!ia0vp^20+Zn2qYLb#0w|^DW;MjzhDN2vdL#HfIKcw7srqa b#^eSdo1KBNOfK~akj3EX>gTe~DWM4f5?Bu> diff --git a/mobile/android/base/resources/drawable-xhdpi/casting.png b/mobile/android/base/resources/drawable-xhdpi/casting.png deleted file mode 100644 index a20840234927faac86ad6bba95d098d0889427ca..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 370 zcmV-&0ge8NP)gN8?>LnOWfVv9eTXx;o9M@nLDd>{-FP4cXRHIeDSQt zwZsDlIKYA125`-0c=UjnBmM{g4sc)q2hssF;1&EN0-z;NQUTXEzzC9HARRyhuFwrP zDS!oBsvu?f%KRkbGyGAFQyH=VJ4S%|0AtkQie}t{Cqn?$2*Yn#_Q4j-x&s%sC;0*u z(4$Hp|EaT#AFQvU6GTw0i*>5&SOKAE$GHs2q>cwJsPba>EHc5EjEJE+oAMYF0CcSJ z0+lq=DJ+EQH;$;X7P6Dbe-$`GHa-gD*Rs9nq;4VQ6d^ssmMDNGO%Y*1=i&-RLiVNp z1kF(|9=Jw6N-QC@ABAlH3$RI-#+%lXHcXxM(9Q%oJC&OJkc)`?p#J;PVuKG@9dZqWR zo3UgRi^XEGSXQlzr!ryE0XTr-yDXu!z=@P+A+pa`sIBA%a3r1(nRcO4;02zl)m^{}ndus^5xonbO}2yh2o!;L zr1u_75YH@lBzkYari^EyJ>Wz7Z^0tzSCC$m3;{erBREu>aWe5ZfFsg#lJSY`3m8xf z5kUH>q&^@`0GJ<<-Me23L kKc;pZ7%LWw#bU8oXEBw;{gRuSng9R*07*qoM6N<$f;Jk$RsaA1 diff --git a/mobile/android/base/resources/drawable-xhdpi/close.png b/mobile/android/base/resources/drawable-xhdpi/close.png deleted file mode 100644 index e23ea4666b189b9f04b2fa59442f8b0cc90d6559..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 373 zcmeAS@N?(olHy`uVBq!ia0vp^At21b3?#L;KKKlz6a#!hT!D0aat0pczWcT^P!U&2 zkY6yv2Xl*5W<%kBm!jf|_W(tIdb&7?$ysUZLW diff --git a/mobile/android/base/resources/drawable-xhdpi/close_edit_mode_dark.png b/mobile/android/base/resources/drawable-xhdpi/close_edit_mode_dark.png deleted file mode 100644 index 93bee42dafcee2ce4a6d8aaaa982f6810caf5ddd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 283 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1|%Pp+x`Gjk33x*Ln>~~o$APS$UwmLxq*^g zph%1shn&yEc}`k$C#pSQoFcEGnGzVszR6?Z`?D-<`C8@oU7l3CoO<{8VA!cA-&?!9se&7E$KB}EeQOZ+iL`PN?8(#Uz b@}Ido;`=XC8+SXPZy7vY{an^LB{Ts5&|z(x diff --git a/mobile/android/base/resources/drawable-xhdpi/close_edit_mode_light.png b/mobile/android/base/resources/drawable-xhdpi/close_edit_mode_light.png deleted file mode 100644 index 11e9d19ce311056983c2ed928b1bcfb08eff22c6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 288 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1|%Pp+x`Gj&pllnLn>~~o$APS$brZ8xq*^g zph%1sN8FMtB}SI5O7ombqCYUa5@!>f_(|$S(y1?dw+Pe)nfChY9DjKAuibQ4smN)` zdTo`Kvu=K2{=TC`uK(7wTJML|{a?+M*X4Jrt>>>-5%_o6(og7%RnOE2z1jcYPv5Pl zQz|iG!%6$5&>&7Dm4hG7m4DTnP+&35!D2dt1&}cvj3ShFsMq}6*>V_+*4y8x;8T~j zm+DjPvSetVv}wu$_5y{KjgOylJn)%x+ER0d=mq8i9mD?U+sAZPssuW`o}6{2SMzVN ep-E+{J!A0+marJ@3B^D!GkCiCxvXqjv*C{$%zh3OXGTu@tong z!dfqRz9i4RV4{vre)@`p1)Li1U#vNjr06Whyw*XDIgI;fMw+61(S)Ri|J{=RnNR-j z`L8&Gk)!dDf}B$iqlZ!j$5#QNQ|UjB2e2CouWyYl4C%4e5xXSOBf#yhE3>uW%Ar66 zz6W1{?{9Ax_o}ufBQ&@FoPqPFE(pOyi?cC mNxzbiu%!LF#QO5Jj0{_Zy_WCHTE!3a2!p4qpUXO@geCw;xLu(D diff --git a/mobile/android/base/resources/drawable-xhdpi/device_mobile.png b/mobile/android/base/resources/drawable-xhdpi/device_mobile.png deleted file mode 100644 index ee665d2b6c0bddb24d22463c74bc25b0123ece30..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 215 zcmeAS@N?(olHy`uVBq!ia0vp^GC-`u!2%@j)Cp_?Qu91r978G?lM@m+QnmAS_6Pji z{l{MLgLyz|vWB$FZOsJ?o*Ukn1Y~FOc6e^O7S*40y{ry^ZtTC)BeWH4@S zv|tGL-*zmpcd?YaX!%a9MfHgtO+S3BDY++Cw| z483w7wPPBhZD?j1q+@5hg6rN`X!pRtm?a3hxN$E+iUUhH;tOnc0+8ZevfTWAL0 z>=@5S$2dk{h??!kIv?YSV-z-72yAZ0jtKw&01$-rZ%vC`Sb$Sy*sx*4h7B7w%nu$q zSXCQ|vN!+$002ovPDHLk FV1m5y$Y%fm diff --git a/mobile/android/base/resources/drawable-xhdpi/favicon_globe.png b/mobile/android/base/resources/drawable-xhdpi/favicon_globe.png deleted file mode 100644 index e4d5949117c60cdc226921f391a2c8304d4bf6bc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1617 zcmeH{`#aMM0LH&=86rBu72xStv>`{c6aGd|(`~l~A-plj;`uX8a!(z~q;)>z`07%+cTR0uH`M-t; z9c6J#0uKNLbg_2MmPZrsPxxQ|g+T3w{mUa>{$WlS93VEr-rM3I>CuWC|xtK>da+Q=UsPKbCIo>+(dgo@giYk zT;`x+W;&%`d91G~bB=_c=jj}{^h)Dbpw*%2$cj3v;R@&uK^UB%sa!r0iU@KS`*ZhI z7M_1vbD-01@oo`U~Fa|B!O3gv>R)X!w_ zA1eK_d5;KxzzK~;`jD~(ki(T<)wDCp#*j{`vXUwEkA|f z-o?6^GfQ|5-n|OMbHQlsWK`2$l2EX@aZq$ZrQLvNVgH?m8pz&gn1!HrvcFq^M?IG# zQ&dx*L5hDd5Xr+qP!Z#rJ2&R~CT1I`BNs-R$;$T6tDbu>DZI zE@~SN(r&w<7~jr+wWSNxx~K5gt?*#xWiO>dkf{xq+hID7ku*xe(|kY z|E!C0=KRkRVRg&M*4jpySTQ^?BV`E9V8`kv6 z^$YvStL+%p&`G0rrUg-Du;T*;-sNz4METHwliK+>QUM6DqC&PVp5kd`5}Cfk{IctV+BW-&e3PMAEG;(EJ(=E zhCYOQlr}Pq6Q`^ye`O%6cUVT$Dd~}9%r-JhhVMd$>C#@MBl9;hZbVKUM=<6epYR!6 zl{ruBH)wyLX^K(8RAgsti5E62-9s#>Wnu8L;>+b))((G`)j!EA_1C#86&>&^BL=9` z+mf7aE<>W{R}H;p=>sx3@=mca!6lj9YW%OpONtMkR_1#xGQgs?TZal&IuDU}4x?WV zMYGrH3yK=k5KEGkfG!1#pq46O+s~ScSvUDBYgu% kl=D~SUhcluJSF*8l(j diff --git a/mobile/android/base/resources/drawable-xhdpi/find_close.png b/mobile/android/base/resources/drawable-xhdpi/find_close.png deleted file mode 100644 index a76c69e09755f6d8cb5752ccd2b45dc29052887b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 293 zcmV+=0owkFP)I;P5<2YT?Sy8#cu16~fhm=3JSK{X)M-mqgQvukR#Kv54;L^o!k|-I|%`cNMQ$~J; z&F^NBX-Re6I-h;jrp?*kDzS{o?r7@IBO-=QTxuHrP6CH_|4?j-Q5K#v-TzC@Pr2uw z9~Pz_{=GwCe#YI3&kvlsKRy%oWz?USaZuIphl^aaOu(*>vzBGvdSAk_sUks2P%2Uk P=u`$zS3j3^P6 zDJJRe*2RmT&NqAgt||S}`8^uq0!|!`0$N|bhOnHy$glYOtAL4Teh^cjk3zV#QpU2n z=^e9H-p^@T(fRAXu=HP{Pf4fO3wyl%lCKhE|H{`xft&Z!L6aqCuDlnzS^o9t+uMzRf?WMyKC zQrvp5s3O4?nsI9qQ^YZmFQ!PtM4^~sHzv-C8AkFM6CF$!6CB3W6$y*UTp^0XjFVs# zQcFICoTY=Pn%PYXaY{*JfFxrBdNwbqB7@beAcb09)0bSe!EJQzu}EB)r_aF>2QhjH z(m|=_v9=MUm)TC>K7zCn7Y(-1frn>&@HzSjpp+mVoS*~vdn6%nug`1-P=Sq`%{&Q# zMc8;9Ww;u)4Y!vP67O(3lRK-96U$oHuHRD z=skUo8lFpEUQVCk;xoI zl`ZDj!aaKVqe5KR|G%|GE(0VQV(jNLhrIJZmH-QBih4{@_0)3li1vMD!RPuLI>H-+BUVnGu-Sa)t)k4 zWHDL%%Sf(J@r+H36%7_neQ^s}LY5uz2dxg#RN^xl@Z-7Jwx*^YjYxX|lnjpp2| z%_tEz*5-4cdee##5k|M0>m42~8O7m-N&IQPXk>11mqJugaFFluxd|hcrTr%HjU$wz zmg51ZMHt6omNu(VT?>B8TA+gM9OFY37e+0QO8hR-1vor1DZ9xhhN##&s`)}IgNzqB z7|DC8{c7x}@Qqdmnmlr`gj&Z%g>SX?iPcSD9Ag=_wPue0 zcu8JMMhYVusM_ku4d-=fM7M^tOZ>x4@@TVQJja>?qC6X0@9sBNyMO zHzXV-%?!~EK2xtPoGZ;t(G5OV?}KoSH0MV*_+7nw!pYJdtPoX9YF^_;0|jGibYNIi zhjh>88Z8$8Frf~4{a|N>XkZJCK8S>eq%BLp1Y9R9Hv0*K8P7OkiPcTH7Nk6g(l14_)gp z2RP9~zSE|)pN$sPOlzRNv~l*S^~SN$j8pZEBcl~Z>8*h_i8OZ6splRrRb;WIKlRMB zk&mf7rTcy}d*r2H4{f^RZL>x97|#Vd1?S|P@ z*Q~eKIy`N}x0OVwsVlPNDX*B4%DR})tIf|V!*g%Ty|IjY@!RRN)Oqvd zGr=OR^ZNusb_#xS*zwLOPHpjfj_;M1O}>}L)-TDd@-{5WF?+NpxqqvS6QAM}L(fUi z7ioN3+iI&fW8Jy;4+?^(Jyklr>1Ll*)y)eWVhj?+ZMt9kO#F0J_wM;-u;4B0Nv0;< zU%gAk@9du@wEVh3TJ!v)S{zN+4S4F=#2*|vw@~8Pn%evQ?6;lxVyvbrbqfD+e6ObM z5Te|A+LPOXYqKZ!1d))29R|;qa`wF3cjg+43F9TkyG~0QOLnU{M0+r0FyUs-@ypMc zGLfNW_Z+|VZnOORMc;kmJ+m<70(0D~m-YOc2u`1nh!W1Zk&2bO;PO zy{xPZhMDE%FJ@u^!E04kcQU6bQX&R0t8`!mb1Plf!ry&d=n4b@{p=oqF9|}P+fm)OTrbDHr zC>kFS%wQd@;)_Jp!t|=Dsx=VM6cr?+6rd253UtCSx`H6(Ycfz|2D$~7&?^3nvV-`4 zmq0B7p*aW*^aV2zqzuI1LA&59;Wvef1S>FH269kXAQeKn_;gH=35-P@1F&>D@EKh} z_|`F*R2By0D(C{3Q(g|kpo9)pRG`{G35XjT0Rj~$;ZvhC!i1nI z;N;p0Uj|ACjt@NG6<`8A;GofRgaI5zGf}lD$l44vpht;X%J+G#Q?xZcEJ}xhfKx^l zf-cZ2R0i^aCnz9#EBKD_&lIrp0hy>{AfHaJtgHkIAqZ&TmxB`i5dZ-I6=*}LA!Ne* zf(W?BlOfKIE|?VohjqgXzeV4CU*d|7Iy=hjm$?1MJCSMVqHvEK>9x|wgLfZq^tm_p zb=x@3zoM_XGSGLdDdR)`kDx?1!EaN%v_CcOcmE`hXd*4o2X6^2wnvo8mLzy8hn@Eb zQ8k*?nyG2k|E1XL>UG^O>C->#eXx!T{*K)!(1*yTg-AK7%8ZYoS! zZy;rI)&B4$_pBGY;g&N-c}Q^Vzl@#mPMgE({Ge#cd$<8 zJZzvIDQk0L*m0udC##OC?B3d9i#_wHk;X4+C(BbYm_O8Q ztu0(H3p5P~RjT^pM0m1_eN0joX)tV_ZcaKiy0t5n^t~Tpp6i%Z|7m-4rXYYqcDa6> zML09vvnuELicQ^K6n0|h*o(oBg}O!TuKNB=heVS%PX( zpU6fTj4h@#1m5%BxL1mYJBkmMZX)Yh;W|1o7jwNQ7ZXdC#_OHP-_Mi1ej2){2U@0$ z$_}1dh?f(_nl)ouR1|F+*r$g!b$~x>V2L^Mc>2vZFZnp2Iz0F)cLTzlRM=RqSF`E>WMA2>OIi<7x5J5 zI?qp%F!nIMPr(_RIBlr@A{U=wa3yfTWXj71mKK;!vyZW}ll9v2w;-1ybvw$^4j03g z&}}{@+5VI$h8cIYdmrz-Dl#H1-aF=TDQ7>YrtZGAFmoI5$ zts9yglef37Rc9{}2?OQ6wPVv&R|rzP`#ztou%^*L?>hb8Kl;rwv>@NWi`I+&2pFl3 zUhD~-m5&K{+Ag@Kg-!d!bifH`Y6Um2A-LmEzcahIV$bENjX%lx3u4Ke6}bibj0pF! z?wR(KBC#c=vG;Es%C$qYGqG~ZH9cZ`cLcDdLMs__3@5Fj4U}7X1DqhkR(FdJ6z{<0 zG?n-Qi!V7o{nt5foSUP%wq4jAF^v^j4UfxTk+L&SOCQnp`_-vPbyCrB89mh&nBOlU zPi{%RP`?=?9^bHQbvA_-`q2%$Q$u!ggHG+&^N}9Q37_&uXEhO(f!drtd=AercBTpQ zg2)x{h~ON(^npv^f|HYlQ^u4h(TZ;`IDkWrT$F<&8mxAP{3 zc&IL`n!6n~&H7*<*x#6=WMHcuo+O?0W)D?)PxvyD8SLonn>En(WY06Muswxs%XU(a zt@JeDymV>nw0)z&?(K8d7vL4=jEtQ1;Z?EXx{Ju<^zi7KEoub&x&D@a7O$U|&1nre zXSu^j-gLLReI8M3k>TGtP?X?jqMBZ_Yku_fk?Iq84VR0VQXS5ICQoKCJ#8k2`u>+i z^Dh4?JeKh_(q$lO<6+M;k#F66-i~^!-3xpWb3(>TDLq2cA-g1>oiQKco@cq&(rnZA zs>;Cx8;QuJ8P$nG&TiwUH)HaOiAGboPPmx38|O6y72V~IMcCZ!zLtK=)PHY76)XP! z2JB#z3q|Qn&y|dGzHwyDhFMgu`wHcV zxwyA@ZRAemqR-E42cy>#7u9<3#dCW9-sK&?JE!~EY_`-zLH|z5&cjheR1qvGOZ&S5p$`32-$AACQeRIw!61>Bz6j=5>*_TW>_5fvKSt<5qq20!U^p#z|!|x2)4J#D*Pv-q7ShR&23{{Gv zR6DeaD!LD6lGw%LrTge#QkSfi6>il%-)gfmuiCxpR=DFq%a_E*Et#v4b!97`S61gQ qTD)R+O}&sXUn!hyOI`RidKwyeDhECE>jK|NjO{^3>z9@uG5-NvijCO- diff --git a/mobile/android/base/resources/drawable-xhdpi/grid_icon_bg_activated.9.png b/mobile/android/base/resources/drawable-xhdpi/grid_icon_bg_activated.9.png deleted file mode 100644 index ef25e2076591af8708dd10220b76f437beb573fe..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 116 zcmeAS@N?(olHy`uVBq!ia0vp^av;pX1|+Qw)-3{2o-U3d6^zLW!hhTk?l_ zL?XZTl{c5#GKNK~k1iJbFMms{Kgwp-DtBEu!vv9@roa#LO?)I67*e+TO=$Wiu@-0y NgQu&X%Q~loCIF-oB3%Fg diff --git a/mobile/android/base/resources/drawable-xhdpi/grid_icon_bg_focused.9.png b/mobile/android/base/resources/drawable-xhdpi/grid_icon_bg_focused.9.png deleted file mode 100644 index 27660f0d3d23ab36457c651c1a1eff2fb4c46843..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 165 zcmeAS@N?(olHy`uVBq!ia0vp^av;pX1|+Qw)-3{3Ii4<#Ar*|t3c`Qf4=!m4G~nvl zZa0H^kdQD8uWIG|iRmkQ!%{jKZ&8vb*Q*8Z$MMt$ewn@Yqhs>Du z>;L7ej{>{@^ILbj>&p3FT`YEa|D*-=TRr}_9XMcK>vmA%hrg5>55u#rCDp46g4P1< OW$<+Mb6Mw<&;$UU7d!d@ diff --git a/mobile/android/base/resources/drawable-xhdpi/handle_end.png b/mobile/android/base/resources/drawable-xhdpi/handle_end.png deleted file mode 100644 index a637c37469cdabaa1609b38f4746b50d1a2ed4d6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 707 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 Y(7hOoVp0@UGs`oaJ88PK0000006X&iCjbBd diff --git a/mobile/android/base/resources/drawable-xhdpi/home_group_collapsed.png b/mobile/android/base/resources/drawable-xhdpi/home_group_collapsed.png deleted file mode 100644 index 80aea0ddcf301b62dc8b434a8eb0e4c9704a1c15..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 204 zcmV;-05kuIP)D0G0s9e3}Cne?)^*97hm_6wG|{_B$LV_5`Gy^_S^1#FIGg9uw?u(_%J zONfAtL_K|g8}CtQHEdKmA+$@WPoJrEzNWKhiuSh@bF!=!rM4in`44Ih0)5G$KD5sC z?a`TjE{s`6zdPc^6`n%(o5zK~nf3wjSYG;4Za%4U%xnSlvg7g3J}YPd0000ZCgQY+hJ`*wQbwoyG5MTcC6agkG)&lw)f7njh&egEq0HjA;I7$M)D*;kw>80FGxf@(GM$o<~ zrj;slJ(-+xN4a2}Ksx3;X9h_U07yHySiC@cmzdY}L3|*cKo5ATp&q6`T^)~38|7bz7}E-sn2^b{MFPIz#u(_ zRX0njDI>Iw*y@Dx^hORFnDKt5RQ$)~OW2JO?Z`b<`J1q5;+X*QHhdyy*z zFi0C9Q9&Ba3vxAvH9djl{T;!yf4g2lpo9VOEN?Y&X8a~a`QQyD!`D7Or>@@ejB-Jr zV2!sH#27x>5`5}PM$mo^EBs|4%J5MZBo9w0IToz63P8Blnq3> z?@sT0c!}h9rSPResv;d_2&T}`1O0e2fAsqY}f5+2EDZrp15ZCauk_p%Z z@eR)-jevxluvlePmqP&3n*?6(7r^t%Am9!pFg$PS2+#tw04+cZ&;qmoEkFy<0<-`v zKnpOex~>(#V^4JE9?ih#*4-z7K8dj>NstT|3P4D*@ac1;3oa6HQH(iBGx$!q@{q%t1^Z9n~p)M-a9S`l`HwHbjnZvl^s$kA&$8DEicv@3!S58>;oPeZ#SJcQKtX3=>ND05VXwe749a_U9r z7d(Vi_HlkgbKdv;fx;1JN1oRwJlMoe<@%hF*()*x^z!(HhgcSdy}wk>WSsezmn~b? zJqsE1(9cNb3_O0WD5>n8RHL9WFV~@}%=`>+p)C41s@(9 z2gl2>>A{0BeOQv?;9=bdWe}Nd9F*jB9hA!45Hd0zm3BvXlD|B2Qaz&p*D=?vhAbe#b} zKVCWmc%+QjEEj^FCXnIJ&@rWfS!MGb`>Y}E+bAw>V!Z07*qoM6N<$f*b62umAu6 diff --git a/mobile/android/base/resources/drawable-xhdpi/ic_menu_share.png b/mobile/android/base/resources/drawable-xhdpi/ic_menu_share.png deleted file mode 100644 index 31cca8e157433f0e0b705da5cd02282fc32e70a9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 713 zcmV;)0yh1LP)hn*-N)$p?wr|s=Ixv*V%28MGN{7FwHvEuSlhPl1m&JhAMTw^a&Phn z`xakjj?b%;Essc20ifA0(L9pMe2_@jL7A-;i2dqh(^FAV(N-eu3Lw9y6Ck|-MYFSo zw=N(Z{{_e^E11tf=}4n@5q8CXzG_lsV6X@v?C`IunqoK~m9yUjS8?O)lX_gJiOcMA{5UU(!H) z4fNqZ9d1{spaliG8K7Pa%DiR(jwXTvjNdnRx2J~`5>Ac@R zP?j`(0BLuikAJ?Op4#MMGh!&Do}wvI(OJe}9)fSkCX7sV1)v^lZPmtU``&OseHti{ z_A;RF0II%%w zl@lKeGYILuy#>$nT194$IXO9vZEUPsPN#1+^8n~x(U*BX&9CaI+8c4^@vL2DlGOd3 z0awR%0aH0Ka8xD{J6%{|4HWGx^ZlM7AcdPagrYS};&7Xq!FCU5&Ty$&`g#_hnjK)$tXJj z)lFDOl(#jhE$`8qScVYBsaV#PE=&nNQ3v(XkZqL*h#V>xWLX7-$$~(3y;H(*ebh<7Tq_Jo@&Vb0rWfp zTZL4iI6aep3G^-=TSYBXZ=h$gmw?`2Z@@nHH;e8$8jgmUj}yI0&!jg18pdHKskI~@ zT+V*dp7d|>cW1xoSwtcvmE~~ivd`&N+82ePj~HS{RW>W)dVZ)ZxL9ZP#l~{?n=H1 z;WF_0=CSfvS!?*O7KI}IN z27x0=jld1WNTv3?MnY=b46R|{fIKB|3o*7!`5Z08*b8NB+#CTikp@M~#i$_@Yq9o diff --git a/mobile/android/base/resources/drawable-xhdpi/ic_widget_new_tab.png b/mobile/android/base/resources/drawable-xhdpi/ic_widget_new_tab.png deleted file mode 100644 index 8a5cbae011a16d427d0d5295cb1b14bdaddd35f1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 184 zcmeAS@N?(olHy`uVBq!ia0vp^av;pX0wgC|rfCAHdQTU}kP61+1FWZ(obsRYSNnf_ z*e&ET%d^Al_9+1=?NRmXEIRMb}xN% z`x$3!E$V0VRD)x#=6~X~a8xxayVli&{&?yJe=hpy@OgD%%{GLD8nI=RZohEtv}h<= zp`Zhj`;dLRWY&y;ScQSg=VBxJp0vluMMRG+`qN7pY8n|=UCrp z^@RR(}%WQo;E4Mt9$0 z3xU>$$;rWH-?>{H?)|q7-<-8}Te{4pcV()gz5CUFq;f6^{-JVulg{Z)K5Lg=d%Eg_ z+po@lre76-^q|spAf_5(T>a%UJie0l`aI2{TnQM*C zvprukX>ZzLshinmXQY1e>;72TyUgIEbopYA*OtniN zZxdN7x-Ts??@rggg+lw%T61HHqqkqXwEad|bUvH^lV9GOr%bE!neJWuQnq5|$6d0Q z9Xxq2Z;W+selq9%wY{5;oCy1UtFm1r#NOO};Y-hwLX+b4b>9|V{UTKQrTx{P_58k< zPv4A}UsYb=akgD8@Qb1LoWfq!#+=6m(=|O;TzbG4Iw7LfDZ;tQlw-3>M6ZJLBFUBt zp|i?IcvWsZ5V$faLHAIeN<_Q0*iC-fv@D@j-*&9tt}^LeVbT?)+4&AJTmPgbY|GYZ+So+MDy0Zn# zCft?zw(HfQ2e*ARmqgZ;E&$@p56-KEF6ivFx&YJ_=0V5ZIN>%i@AMT#MTD}f~z@st~l%wx%9d-vj0n3l9}?}wi%`#?-kP6FKKr~ zHa@xIaE49&L4a~XVQj};*8}IB54=xSEIWOA{hrHTt@i#&Sgbr*_JPu7Q=!inPdb!r zbB~IxDXJ-J-*WB86aKt^Mc;nS(tdAJ@6~ta-o!A`$ucMO;tDsce7R6*g3R>XiBmc~ z`HH6>zxixs`>aPo9)0KcDK5>fDYDqT#Q6Gh*{oC6Cli<7ln#pWn^dS|R9)#+a`U0| zu1}iAh4!9)GxjPkeWqfWSk^q{*yJZIHH9Z?K4(^iVsg$KbLh*2~7YN CIxs2# diff --git a/mobile/android/base/resources/drawable-xhdpi/icon_home_empty_firefox.png b/mobile/android/base/resources/drawable-xhdpi/icon_home_empty_firefox.png deleted file mode 100644 index 92b96607146bef4d94837e02226be07d220ca5f0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3271 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 Gi2oM}c+uSe diff --git a/mobile/android/base/resources/drawable-xhdpi/icon_openinapp.png b/mobile/android/base/resources/drawable-xhdpi/icon_openinapp.png deleted file mode 100644 index 4caa44ea885bccf2507d96545095241012f1834f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 420 zcmV;V0bBlwP)N&IZN+5=ck75 zaGuv==f<6fYtV-}EHSiCABIq4VrT2|4uaoMU=Tu&sjFB3VHWF?+}By?hqI;vS78W3 zwaXPgf$Mnm!)2Vnnt#?^f)IqD>1XjWgpat40)(I_y72@)_#wQ3&x;qP8%n}E2v1Ch ztv~@jLIxCJiK)0P=z$FD#&%QRD|w_3I}?rDjy}j^L)ac~+!C}w|7gZu97J23xAP$O zq8a)}8y3YA7=TfFfvE5h^Ewz~+qc!TY+1Qtbk zsPEwsMxhvu;t}ped9I6M`K3LFQsg;SMGADF6mud4mP0946e+L`O0nihffG=QO+*U( z2&LG^JUd=Z>TfS#18txwHZTQjpbdNlrPyeszvj6vpurh*TPp#v>677T>@p@CD2~0GUiCxfyI!o3q&6^b3dW7BH*0pEJC)XzwqJ z$+nL<+7PGTx{0a4wLA@)CKQ%FX)sDK@6}DxO zC^GYf$L7oK^PbMl5Hq}-bFwZg>e8woH$J~?3RA7Rzg=aQ$Ko3YzdK)44l#+c|8_2Q zX`tvEdrsp#Q&!(Dw(V)R=DZ0{I@)BNK5I^4y5^hkCX0}D-&vv_`?tJayiV%R^Nbbq zK4|3lY-6>V#dFrZ=JFXIVgCxXDLx@=HpN{&#^&Mgqr!a(GCw%k8y~#$xb@i~mQdLO z-8IwyzT4r;Tc-0gqQKp78h4q=WwGzw`q>8-7i7e}`VjY`Y@Ymwd*?p$Gb$-5eR_U+ z=eG~suRr8XE&sOu(}VOle}4VV)3<#6!n9wUoG?Zxm=z4 zt(xbQ^P?xpaMl0^$O>y-JeAI{I2l{WkR z!wLD5x}3MoH0ArbjM*|}_TeWJavx1~UUxHzFLK!q-w&tl_0OkkK0k52>toTY4T~-F me^2JzUd#+nUV?%@x7snkO-gy@qoh#|${e1qelF{r5}E*jZJm?= diff --git a/mobile/android/base/resources/drawable-xhdpi/icon_search_empty_firefox.png b/mobile/android/base/resources/drawable-xhdpi/icon_search_empty_firefox.png deleted file mode 100644 index 8332271d8e1f4dc398bee88ed407e83dcb58a052..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2323 zcmV+u3GDWXP)m|>8H-NZM?S4?`+$)?}GE)e!txug58;&OwT6y$E<#~ zXR4~JtC!3nzz>C$791>A;9u|I!!J+pozjB5;=`rq zBRXLp+ho4rBm#OTp|~T!4}M<_j)!R}Wv7!G-u&HwX8c zuVV*(!WX<2OR)~$;7@Gh3;fDWc{`QLD z?G%Z?#Q4;?IfP-n%#2}h3I=YukoFJiunk+0gJK8IV;hIRe`G9au-W;Ijdk{!l1*SL zUgX?w;z~pyuoffz1Qp=xjz6#S?mJ2Om=iOnxEe)6WI+`S#v1;bCu>|X8d%HoUKMw8 zPpL|JdF%J^A-zK>g{Qd3$FPex@CNMR6Fe_^W`G>{ki*}&@BN(1cwoUuymTq!{{LJCa0D_TbVTd;#BL zFp`^LSG3LTltP&9kZKZ@Z>8?;1ArG;oJ8jGe}O-|U>j2>91QX&oOgsaeOI;G7)l2riSBcxEyMe8V=LsCDCD8vnyKFcIN~)cyY#)(j>ITWYWSA_t#Pwg0uf z+>TYG|7s*z zK*yM_I^u(2VLc)jUCAV@wRDW=ED_;~(M-cLeQI1ohp;0=#4ijAyGtH{tLYfiPWwG( zIFBcfpj6c{s;?<1pK+fURKO#`>gyQObmEf6h4Dm>2y3Gwmi8x9GA;~ZmONxS=%-^u zbx72dl5ZU5)TRrY;QszWP&(ooIKZzY%-R8akTw$?iNpyr!UzIycd1K2z{H zcJN;OfjOu{UhW@Ou!nu#@AiYdE~4r)2z#WZ!0S1e(?<5TP8x@HEB|EVaP2uJO~fcX#WWq=CU%gLNc3l0+`VXT?&24F;-(`wzlDp(r7*6h)sU&uJFc-0 z?S%)P7lfDmkS}4mzpum`(Y|1b4F2%2taI+4IO3_g3KQcfGGd$uMZJpLPpl2j>gidC zmfVTjo|1)&8Xch;t)Ut(KgVd9#T}2x`a*9`iB)?3zmt&9YfTq*9xbJG|=_iBn=*6pb+>T&c!ceB*C&u+?Hv5CLQj`KOg zYpJp84pw4<(n?A0RulRmgVu(UYw)Gy7?3R z=h=KfGNn6jL0kN5*vfyTf{B?vo?{m;gAiTNQZyHx#BeOd9>)p6N4O?oC0<|CGvPSf z650!Xacd_EuM?Q85I0Y3iWLbi;8T|1%Xld3G%Aobn$i}=Gg(Uml93$v z1AY-tWbGH#NhV5N+=~y+-Gnx3OCH`E*TR&#WCnqWu_{jI>nAb~(TN|%k*xDdCNdLi zZr&6J!mgq)nT{8RBFwYC5IM**Sn}XI4~cprMg{nJ`k62fnJ#OCC>STth{+$AxMjG7 zJChg16_zxZfe^`(9%3;Ur?_WOS}~6A$~e%EBfLOqAt4u(8NThMPy68bXWoG4dJg*V zDg1zGqLchcB@I!K!CSD@ZWsUn diff --git a/mobile/android/base/resources/drawable-xhdpi/icon_shareplane.png b/mobile/android/base/resources/drawable-xhdpi/icon_shareplane.png deleted file mode 100644 index f2dfe2d5ab4832395845f9b4d30c27af2c3eeb13..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1823 zcmX|Cd0dir97Z5D@1=IQT3K0)W?0}_+3d|yqZAb-<#B^NIW-D`TBd2D$-wPU_mxt$jJFrkETQi??)gi?kp5Q0SIprepsD!^eJ^Z{3*1PKrZQgRZ( zRe&J4u_P1}k&`H)G7<%fD-n#8eJTmCNy3B*^nn+3ODIvCM1TuH0#o2lq0!gqNcn;c z07nVvs9>rCVgPQkqe_JG{e&O@HAUo<1vmi~#wpdL4owPj(~jw!TBCLVQqxg1osS}F zJQ-aWRRfyxkz1oa@Ex%;34SV#WHii3s*y0J(TMU$LSqKX(I^fqOu>wHlo2RL@)(oI zRrbWd9Pk=l*(?-<(#+@#<`{De%duA0Hnw*54xf#4baEa)VUo+|lc#(!&2`31x7jTA zoUb?@^SQhQ-z;3T*n0`I)MvS`--?y10s@1Cp<$xc;SpkqR0bm`hRc;I^_sO&>o;!N z92>V~>$Vilj?}dDeVJL=dHDr}MF$QRmmWHNvE$kkCr_RJ`K+#@@lK zuUx%${YG8=&0DwcG&cR#dhdQ)`|l4PcKq?^ap#k#&z`^Ne);FC*FA4~|NiIScYXi$ z4-CE^`tVU-5V3(qW7P4NFAYmDdvky1gV!r*sZT`F4=bWZpXOMl@2sdi!Enn=ZgyWH zrxeZS!8;d!3}5H2JCT&yq;2zmWy9>Q({h6cSXyq=i-mggSsxBJLJe<@HbO@Zw7N_* zq%LCQmVxTsCf?Ro$CpECX^ver=dmykBmeFtWxw&cy@#1v1mI(9?n>{wV9BiVKSEPJZ61$&C({?3){0oIC^>08&O zH|m6*?pMz=rauaCVO3?WmTRhXAw2i1KBbT1ztd<+)Os)4ljtcJ*VHfys&X>FV4fhHK0Z#f)#4 zc$8#%WR|sUFC4E84Y727oMcmf%=bdLS$XUbXMMSEe-o{|Z$q}GVM$TjUAy|1aV?>c zw|jZW&z5$Vy1X$#Mu%hm!wl9~^jA?fI2m&1v4nJ=5gFb?T5A^LpZNO`QA5X(gUK z!+B=?ZbnEXJ$2590=30DvwyaF=55A2+Z0Z<3JM6;WL_ZoI-2Y^ zcc?ign%fW`;@>IV#tMDn+@Q&--OOs;|J*gH)5n1w(<|jo_=MS$G`~b0^6iQb-&ea)HNWtV zP_x^u^j5KR19XwA({ABZl&mdnZgPEJvR@;}?|ZjvwdmK*$GNo1^n2OY)}>TXe{+0a L!Sd5S*pB}J{|6R; diff --git a/mobile/android/base/resources/drawable-xhdpi/img_check.png b/mobile/android/base/resources/drawable-xhdpi/img_check.png deleted file mode 100644 index 986143b2431351ed24d666ffda029d5322df6d54..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 907 zcmV;619bd}P)PgxebkcA4?DmHM}Od z$OY~)_&QTmDsu3ylklZO>f*2+`-KeW!9L3noGXH^Tj;9YUGN?Z*55fuZ69cOZD0Yz z)N4raLS*6DkmPI;G&zIRZZ_b5u}^~sPjbE;cs7_7S$HL=qm2{3tN++Q3|^@FYJ(1E)n2UK4zvi}6#0-*r1(wTo?d9)%qvPYwh; zlAL4L^YT3`#!mD;Wf`?E5~R_qM+Xi^Uei^>j3?`#`hlYBd0k;iz;yfX@$ zX_ABf=NQ?Ge=I-3Bgwa%k~-S^CY7MC$3bHHE!D(6jllCTVmapDz-sbH;Z<0KSL^+U z*<#{!Kfoi&4S9+kF1$*Y)j1s74b<2#HL2^x|J1Dm;yWS1OuK^(tjB8NFj0@tz(6}5 zfTuW=nhsB}u258mhPW;DxZv@Mo(iQs&+v;0OZ{q3&d}ln1xEn9)a*WLJ(eSQv6s=(5=PZ;12Sq h+aHp*ySK)E(2rU78rnw0j7$Ik002ovPDHLkV1gNly*2;< diff --git a/mobile/android/base/resources/drawable-xhdpi/location.png b/mobile/android/base/resources/drawable-xhdpi/location.png deleted file mode 100644 index 8a6b546bd0c8082b940c67cdf9bc698b18e66415..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1019 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 diff --git a/mobile/android/base/resources/drawable-xhdpi/menu_item_check.png b/mobile/android/base/resources/drawable-xhdpi/menu_item_check.png deleted file mode 100644 index 9943ead84c0ee5272c45ad7a62aa49e8c25dfb02..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 555 zcmeAS@N?(olHy`uVBq!ia0vp^Dj>|k3?#4J%UA`Z4hQ&zxB}_W(8B2G+_bc`goJ{q z%*^EEm zYijFz`sb}$y=LvEtvhz@-gn^0@l&VIoxgbf!^cnGe*XIX_us?N?M^_mu6w#ThE&`- zd-*uuAp-%|i<~VNs^2f1-g3|Bz>)v|Llq2-T)wfWnC)3wYEqv1eu<*&iw4Tv0}r&jx`o4;Rib+r5utZJ8oF8 zO;9Xe#JbXU`o))8?{3_fbV1F2$B$?ut0jt7wfk=0m~>H(bHUm5`=-sgoyOR6u+?|N zQkOq7n%xDXSq>>}<7jcs5#;E;p(G%-(N#ewsoQDC{eaK2`va_kPpst@nYVM^r@q|= l_x4vF4_Om@@nL*9yEOa!NRx@(j6g3kc)I$ztaD0e0suQc>jeM+ diff --git a/mobile/android/base/resources/drawable-xhdpi/menu_item_more.png b/mobile/android/base/resources/drawable-xhdpi/menu_item_more.png deleted file mode 100644 index e6f71ec5bc1ccff48c47097081977e8029a399fc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 111 zcmeAS@N?(olHy`uVBq!ia0vp^Dj>|k0wldT1B8K;nWu|mNCjiEgkFM(#PYz?3yhCt zh8T5txk)a+cyvYJVz=H!`yCe_>ydK)y=dCM)_SSK9Sn;Z7`W4&KAx^#<^eQ;!PC{x JWt~$(69D|k3?#4J%UA`Zk^_7~T!D0SbZ%N&T0%lWRAy#! za&kg)W_(gcTuNp_azYCd6p8i!^x9>at@zb}z|75k> zX8_fvd%8G=RNQ)d*^{q9LBJ(YnnE86+{}=A@4tHlOKUw}gh<8!N)oDs$?_d6X h<|NcPX>-7C29aYb6O~piZ3kM(;OXk;vd$@?2>|Iue0cx> diff --git a/mobile/android/base/resources/drawable-xhdpi/network_error.png b/mobile/android/base/resources/drawable-xhdpi/network_error.png deleted file mode 100644 index c5613865cdf31d1463c67f7c1ef999cf11719f02..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1677 zcmV;826Fj{P)cDT*8hBdsk%sP z0`E{)$P` zTpjb#!HfUZL0#@Nso*lP3|EIA8FgMN*HQIMDp(%7RKlRs;kbg0RRoXYQ}o3MPR2Z} z#1`!1cCNt!PQ@tp!{?n2PppFvj5;r~p-F|y)Fl!m(X;Rf$~c6}aDX~>usp>}&HKi0 z9WbdxFP1>aQ#ax#c%O@@{RQ8pd`ElF)9)+|K&3{ZQ;W}V8FhNWKYhHXB@C9Ri!!|? zn(=k4r_Lnm=bm0W-%k1~i!*wzL@!kTFlqP?dWjeE@m69L=K@KSd^X0n+)o`$(B5lZ z^f!KgSDGhSw<8h90_s51T|9vYu!q{1cbgZ8KbjY-2v<=Do}Y>boCjT%1i^bgbeb2a zE?i6e1o5-PaC zH?%dWU{>iFQVBdwZO9kpI{ElL4)_+CqDtH%ff2jT_*|K(PM2!%DQZLhB6!iK#vZ)S zyCrb=Cqu0nu8ILZVd`!~UBV5XRo19qK6;vj0-b8rrYEa~$E#nv?vR?NN7N?#s@KsH z8axh*D%Qgf&Z&|VlvTNcn@Kt7mO^AZ{@U{d2^r@CF7~YAf`3*}>1sG=)q}UpXKpAu z)7^@rBBzKa_&c}D+O9fO9zyues4^dQC5uE3L+$vzetMPY#ZJ>|_tnU;_@Ro(UiG)= z5t5rY7jR9*hgIwb$z^yqcmKCjz%i0BB`3u$jorYzRRcW4#!d~~j4PsNNxozUbfo5P zRg%dV4%(`J;8RTEW@=-0b2h%mv#LbhSffzFgbr>_OX6b0@wwj-18n0U{z=^>`CKt|(v2?GAqcj?)D8^3Yd@lJJ-IvvL} zFNhuyIi>Xc*bVAoDD?uy_?iJ$Ccs+V`GUt)sSozwG#AlupD zDKyrjEPjjRiBIC^F@{+wR5k>>3L|m~7bX;$gw7c?*m%VIW4u&Ix@m@L8^LE|9Q^9Gjh9-TbsV#38IEkTrvd zql|C!8TE73oX1IS61|wOV`H@$(Y~slpVVkbP#|J;D{ zpxB9OP3=2xik+Y=4Vme-%DnGi1{-aa8A$Cv&5OlG_(d8rJH<};Tso6UVkam~L#DUA z9gKgOAB(MEnopGjv2*N|Y41;l?~29b`&xdo+qpn&CVrETS?QSAj%w+3LR@kI)d{;& zs=VB-NOA+`4lYjCFxz%RPCWtZV7)B~~%w2vY;PH!vtDgTgj%W?j9xpVB?e>mkD9{xQ$oqys#f!RI< z|DN4E{G`4`oWWyno9F^nYdFEXy6gsm+#lBc`NT-Y36h-(u9HewNi3Trq<6yFM-y@cUXU=XGYJO#pF}k zH(&XWqV|o4X3t#GPj4jT7LX%$&SGHr_czgjK_G@rjALc}ii`v07yo4CNJ`$GIWO-1jQ$rqZs3n@^THGEaCqz4)@xmM2@=XZX)uSTs?@g`53V%8%Ua;0CJz`;WaR zPd>@=^kdt4zI+2m(|v}lE2sT+O}<^3sSBTAY-0Ase?)m9*YsA;ScAYM+ zv-w(p?AGIF;=)w_g#Y{`=^3^ADCe8)A(7E4?~iz>&%CR3Yp!aw&f@OG?*T4*gumU+ zlZ+SGaCn!N^25%TCgrRC`oG;Sa(4c~Cjzfuy^-%s#Fi_K zvfV+l2B#Pb6t=w8Z(w#(X8h>8n4xf)V8e;YrV6K0I6GFSuy#akVmK1}iq-0#JeS3T z0|!&L^4Hn#2@_7(Wm~tq(R@9VMn~%v9+hQ(ehV4gdd$Es^Mhf{v5$Ly8ZCbTbS8tR LtDnm{r-UW|J&#Xv diff --git a/mobile/android/base/resources/drawable-xhdpi/overlay_bookmark_icon.png b/mobile/android/base/resources/drawable-xhdpi/overlay_bookmark_icon.png deleted file mode 100644 index db438939c133a39b96c8d7f58922439d1a0c5aae..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1513 zcmV0yosPra+_+P2ae;D{1|7%LH>#n=**pkeL06Ox(|8TA80E@^l z0NRF7yj4?yU1gP3#t@3%bpZZg@x}F+O#@gG3jlMCfK0hr@U}_f*Qx=QWZr=$G(6fs z=?*JE%V>%)B^5sx14icVzVWEVmfwIzG;-Ws-!4sFDTvBzTcsT8-UgPgdE6r12Zmi2 zil2LbPG4eYkeTQnSCLFQhIbRV_c_iX$=e!iikEQpu2 zNe*^v0laB~ls9gqMB^4qoP8?c_Et&!qD9J2Hw4(;u)K&}VrGNn4Kkh9XQ;exBdGJ$ z7TbNcS~ez>dDSB2#~MMp|5yNTS~X^qDcGcpSl;izdV2H}NFHRQw-idBJj5gO=e7mI zK5Ox~9UUF*lK9PpMCBW)%tMm+bP<_t5b}C@N>gVJtRsr145ISNs8=QNDN^zMg3K58 zMcD`JGY$Qd=G}tK*Mj)LVH+VxuNnlwR5eXjhfDu?x&D@^hi+>l2UzWtz2Z|4aU8V&L=0hrd zq#$!6Gz9O=YJI>8z`vwa)4$+cgyLHz@$s3*x_10{#UR&7kBP;H3D}fbtxde)k$m|H`1#g~+hxnNm ziM9;4qLb{2!iqZ8*HIamh3Lz#3vG0)Vb6C%4PlEi| zv7>`JI@Dyo!JFpIti(Qot8j6#F3PU5&VIqUkbLCp{DTxXFz$ z!@d9`mv@-eM<`tU^oojV4x#u{yA#tZ_yUYd$qtx5NAcj*mcSke;y)@Uhxha~C)Cwe zrWQiIU@mzpPTy46&?nwKv@+jTg3cuuC=xGhQLVdj;JY7=m3xt1tY13%eg8WyhxX0s z@p;SL{sBj3@3=jc$@jdVcVd7e-Y<_f=tc2~_0rx=DKx8C9M`4(0aqmLaA0Yj2RaAE zu(V>@Ov~C~%-vfOzgII*;w46ACcu26AC*G8zNjc*e(;{pS~#y;s-}jao=1|oCZWFm zSCY9LNliVc3Xt}C1B`6SMvght(a4-@<&Q0i1h^+xPArH|vYiO>sLo}wgoYrYvN^t< zu7M!)s~|qgR)I-kZ8^+V^69XSh)YH-I_&yJL=y@!2T0-{GiZx^j}SOAom7m<%f$in z(LRI!^%WET13Cy4E|~u;I*zsi0Wlc-6bK26M%?HV@GF&dXh5ti2fv*IEe{X}DW7?- zZ-`j6M*NzuqM_YZC8b>Sn?oDqtr++l=PO531OCL?vM0}oW~(2l`2-Hf)~CY%a7_?@ zT<`l4(mXh<^15}L0QfeD%x)+tYb!hTt~k$;cAg;Q^*n?m-abtq77aQui1l&K=Fl{K zYfwNwiiU$~u_v6X$Rhi}JJ8_6JF5bY_+TFgy$^J>U;gbImr1E-&w5_G6IBuBaBTE} z*>hRaET6y{-U=2Z52gikZx^0008INklklh8Mfsl;ksyVGEH0D(R6oCP!nNfAmP5em5DU1_C(Xo<$+Ct zEh*`XM_TZvCec_-!gFzLS|AhPtqJjogx5AOfyOK!V1dgNP%&&zOdm-2Qr<`f7r~y3 znqWNabeDwh#dF$=S!jeiuq$d zgNa(=%6I`foup|IZ+`Ua1~yk%oJ98(V8wI*V|WOF1ss}jZ|OG@TztkH2<8TH1BGFrVc7-nqz>*G!-;< zt6MXX?Lz}J-BCgd>1E7yp-8ByEvqDL&8Ui|I+8T1tD-STmhPvDrWKMjEm20Fkfo3K zGHcOJ5?=d;`IDnxdbxtvdA;QU7()r~GmR~_1k7WQ@X!am?dBl={6192C66D@LNauI z5^nl}`Z%wBPYcAGd3Xi2kfEEpJjw4cdP79_gEa3-PhD;|PJ*T$PMd$iEK4l!(<}{f zo^7yDf<||TjaOQdG9R-w8Ycfg-(bGc*Qm{&iAjbhz~aA4xx5yW30l()Jf_vQyv$(i lsWFT0(gQxSuYcw;UjdI$4BjAfzFGhP002ovPDHLkV1mHyY~}y} diff --git a/mobile/android/base/resources/drawable-xhdpi/overlay_check.png b/mobile/android/base/resources/drawable-xhdpi/overlay_check.png deleted file mode 100644 index db17eda8dd5e5b4a62ea0ed6dabae0242ddaee23..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1586 zcmXYw3pA8z7{|vYB5bWfH(eDrDJA5f+`ezzZ(~W;H#25-aw`&(MJlIOF71)qiApOG zJKxOY5<6xlm(VJrp>?N{Mlrc0g=XJ(>^aZ(&ig&@^Zefb^Pia%4>v?zb(1QAKu{;5 z_FmwV37!+H6~JmgL>VLyp_lR^dX%5sZZd;Ybh*1%crx8AlnoXLvOclVyUy zDGSz=NlCIwl+GhjxF|fu5enBW2nQQ9x;79KVl>a20wI#jnX6v*BMrVMj#0WLahR1$XN+`g)7Da3W-oTj*vO- zfCDA5i5SNTG*WOdnv?7nm?y8Gz*^x7C&iIq7LkGzsaz+h5Q+pSkcUh_2nGYat^iJkUl5!KlSz_F zr7|o^#ht0J3tR*RP*!@`D6|eo>Cfm8RZR@VJ*hP$*aF4;AD|wTtz1Xa-VT2&HPK#f z-UPjit>Uk5`H@;01xwP|1cg_%KkOZUs{XEV&FVdmlsJlG4>J8mcL$d48hSJLdgpDo z9bNLrPj62eiQC2;J{P5XCj5KoR>mOR=$J8Cu-J5R8Pjinx#bt@sTq%f0P_?3OH?-Z zzi->*d!*01_sciWwH;U7xoWNpPM<#5Y$Qhf>sU4Kz+0#D*;azSu=+#0$MWcOx0&hT ziAj^rj|XtM%cOk=4WDFKJda=hcvap8%^yV%ueLOltELK*-Az1%!gaD#IBtjJI<@m%(at` zvsiyq{RY1gQJlTvIFXpIZ=xEPt|g#G3#kT~nlCIh;?lJRn$bc{0~3S7Sd+P%JL5|x z68jBIjPhc?sl7M#UTE)HZqMPLE%Y^s9a?g+lgO8*G5mFQ)-t9uPcBQkXdeE8OVSgy zTfEAXB_XAM`L{_~0mcbttwZS^y`raQUbcwi?egzFq!|^34Jrln$F>eRmhS!-oEFG4 z<)_XcqtP|r=nsa@PTk-y-p!0mirhM&wuVQO+->x*h{`f-abzZVXV}r~t-IX{eE7=2 z5(9nL!tCtEy1J5%V-H658QrqH?x&qBaVQP^Gu`eiWw!;*3Fe^q7TSyCA7)=2y{&Dd*M!OZ!_rf=i;yuSDa|16tEBAb~@% z^(iwE6Oj?t1>&#RQB$Usc18j-|F+%*MQv+c`|B}@@;~b)IJsZTX)K=DF;`f*cFI+w zlO@vW#@{U3+q3X6<%vk->M{uvMKV`MC?*&C1P}OJ)?_vLnACtCEWM3 zXIP6Nqmq?vs~ls;{w~g{iI@AE)p4KS@~CilAfLZdqsExF6wf+Rl`A{Vz#p1Gba1oBZLv%L E0d&>*r~m)} diff --git a/mobile/android/base/resources/drawable-xhdpi/pause.png b/mobile/android/base/resources/drawable-xhdpi/pause.png deleted file mode 100644 index 6aae6c5eb7f54500a09c7bb03c72e0cdc1928bed..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 220 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0J3?w7mbKU|e#Q>iWS0EjqoPh_q@4l@JRK!&h zIuMj~Rm+pvVDi?~tE*JkzTiq*%f!TL zFq!vAOxrpKE)^|?G^3^&+!saHFg3C!sEN<8U6l}Wa79xftAhcM#duP*OKhRmNsf#b lfh|GWjA2I7}9n%E=v7-^F7cm22WQ%mvv4FO#miiMUnsj diff --git a/mobile/android/base/resources/drawable-xhdpi/phone.png b/mobile/android/base/resources/drawable-xhdpi/phone.png deleted file mode 100644 index 19308f870a906210e85c4915e42c7ff63f019e14..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 689 zcmV;i0#5yjP)8}S7{KumLI|ZpD5AU{pc#s|#A3-%h@gX$YXp}NNI@!e(OD{S5flTCQXz{76;>bxE|mhP9YPNT@A2_Aw;8xoNEC#FoYOt zBIQbeTNps5wCzHG48B8VFhnu#0Ukg?IL1<(1AK&pv=iq5JxBz~aSbqlMBoJJI0iU^ zL|}l0I0kqH38{~2T#vvVNC@wdi}Ob?i64+DeMQNYNw5Yn^a&*-UAhYdI5n91)?HqR zyoPAxT@J8pFcZ|=44^#M1n6MFs!*??$#Kn97B7Ll~BfJL7R!8FLwXizWL3lmIP=MstP_d3SXvJ#N_M-P}b3f7-&p5689lf=%H*kfU=4X#6TC1$6TeGAzq@g_5vt5 zv>}E%c#2gNBY2Z6*bATp*q`xbSwd>t4WOh^hg`6q^ASL)q6fJkRSN;MuHpc4&KmYU zf_iDx)H#n_8<*?{&|AY1Btm_x*`EWq!D{Db*zezk#7<3#y;RoM?jUYJJ1MkwNJn;G_NXG4<3@eEX%);DGnn};EVIAOn d&}{yX-2h}uG-VV)$8P`t002ovPDHLkV1jK{Tb2L- diff --git a/mobile/android/base/resources/drawable-xhdpi/play.png b/mobile/android/base/resources/drawable-xhdpi/play.png deleted file mode 100644 index cc5c7b9e4553688efdcae4e6c535b0d12bb4d0d4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 244 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I3?%1nZ+ru!Bm#UwT!D0aat0>qu*g#b%CVOO z`2{o7H>Ai|-{FZ|7YP(<@pN$vskrs@`bOSE3=GVU!V3HUAIlMN-NnRt=l|Dx>)(BK zd+;XFVc`U>l|-P}w?A_q-001;f4ltemigZ<*WL2}jfb;;b94W5`M1pPrb(!{2Xi_D P-NWGN>gTe~DWM4fV^U777el(c_5*ZVvNWG1!)izFdz_+u*qf&o3<)T$RuR4fGDP-uMk;+vRIG= z3GfgR6k|awI~EZlo2Y2n6s#`Ii#+-O%;Vg zsk^&5`+(nk@SH#^f+%y2ZbqRt7*M_ZTtFEN!NCw~ZEX$2AUZ1ulnhn~pa1zBDNmgO zxl}3zk;#;Rwup#`Afy?lvDqL7GuYrTgb5-X1hHuh0K%d{Y=J-k0E0mTlM8YL5R4$r zFfv7F@pwECgXwGtf+2_tK|C77S(kxyf&_`{2mlblXRw*fa5{$rK?ox)l!wTJfCe^= z&IUOG3&Cul3W90tIVkC&92%1YX1_Ghfk>bkL55*ugu&pfs{)#l_5ZzDhyY|4I0T02 zbRJSN5xOs2NGCYqBIk$D0@G-Kk;h}P zB7me2;9F=I!U(j{Szr++lSgMEQo-&(Cj^CugmMwybq|BU`gGQnh2QMJhf)>kL-9qe zE-O6y`*nbN(?Tz%Q*lwbaMSRdPVK&N^3@BM|432lm@(S$=(k8w)%@l`H&easM`oX= zo|FDkG3v_Lq5I8a!h=M9=-WT5)jmNbXYH+sA0AG8Wc6z%8`?C)Ps<11S7u4{En}zx z?v2balfIwN-`RE=S9jg%d-vcUyKefGUQ^-Sf5#Cv54{Nqzx|@d6WueWVclrF<#3Xn z|L(i@+FNZ&G%UU>&|q?0pe`IUr%OxC5W6cr!0M1FEf#cK+Ju(M!(57yngJcKTLZfBl8!g6xyYo<*``Mu7GJ zf4eY6X4`Md7n^LpW@=KGU9kLVvLtms?Iv-0USj)=zb~Bg7;nfE6GapI<$PkuP3JuO zzcj@r&f>y%l0UB&D--17Z$7wP8u;HP{cR&U-Q5u;IJbwroM_XFD&2}$@p6UJ?`En%C-EQUNa2Yc0pLzQW-GPK#_8;RPK7E}C=pX8RY zdoOmx>)s{h{Xeb3cDKPBhlA$vw?`)_9&Pr4QG0Sb1UA}gYAPyb zV%>YP{#g@ENq41JO)hYu5pVm@!+${8=jyGbA4xE4Ngt8cM+*SCyaHUMq^&;830JqwD0gfoS0Hv{huze|K5a1MB$0ko`)D zoFLlP==+^W8kgU8Qu-t{KHtWJF?@&V@mi0aD{awxLb<~voyqtrQxEOY;DJ5XrN_!i z_{wVTq-D|5FmxRS+o#tS;ChEkvU0+eVD=xzc44|ba2DsDcaw2b7^q0!SgF$2bz@-m zO*NU$rKrTDw->W)Ri0$YJX1yDsf0_>go#9SxFv%)g0*1Ord|%2jm{n7TWbXLh zG29lz8!#Tri@VjXv~z4>q_QlE;8rp&6zUTQvBK#jKSGv<41cTGNZ(Xln(H?jFRiFS z6SA9flGOZ|OY$%~QG&PkR#DI3Y7Rlo<=gkD9!Fc;dPBR+xVQ;hFPv)~m{w%*p`B-xlLM@5-+ zCuEU~7A1AF7NV@lZiNS%lB)RTLpIfp%?A|2woBjA1@#-xHrz7L_lZdm>U^z|R-}~8 zHy2bn<$SPGc^YnrMlYo)%+t{EE9q?lw-v?Mg5Pndk1sKnTI2@fRgM2nXP7O&ezp7C z^o!_a|Hr7K#nu_oqSq-{IVmnWW6t_$ z#X$n1>V*Sj&05@8y<#)vULVf-#fiLhPt7eFvNHY$1HN>=khEy`ES}*7Hym`Eql)Wy z>HM+FCc4)N=gK*Ibt>9iYu8}4f&YnA(Xb@-$Cf(PyS&xsL{io4G1kOYVo~!o5kuZ* zXQfm#v)Yj<9_6^-H!TwWUAfP4a=En9QZ06e;%G9nceFAx8T|30++8TnrH;X|{{VAm Bid6sr diff --git a/mobile/android/base/resources/drawable-xhdpi/progress.9.png b/mobile/android/base/resources/drawable-xhdpi/progress.9.png deleted file mode 100644 index 8682ea26ef03e83c390fddffb60cdad48c824b9d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 425 zcmV;a0apHrP)1cL_h-fIs0cC`EfBW zw#f@rG7}U$Wp2S0xMbdfL_u9lTWIB7M5&7pISb+o#~t=XvZok@j=?@S1ZUtHVo7EP zoEzGdx|lBIK`ihkl|>=h1EtS&3;N1qir<41unAVc64(W&5WB64$#^5$&rq0Xp7%Kc zu4D2zxuRzRNj)iozPukAB$}S*6ZVfek7k5&`w+Wqix`kWTrPlZh@Jm&F%z3ZGpc_T zKQ}x=k+DA2fHLuzU1n`~bc*3|47R|sK0H1#?iG9wK_Mz{O010g3(6f3ecb=hZsId1 zXa@w`g3B*GLI21bP~TQ%gxnJdHOV%?CT3zLW@090VkXx9KmJV2#7vA$%f#FRcm${t TMueR<00000NkvXXu0mjfPdc=K diff --git a/mobile/android/base/resources/drawable-xhdpi/push_notification.png b/mobile/android/base/resources/drawable-xhdpi/push_notification.png deleted file mode 100644 index 77e0be68918ab5baf207c128ecd90147576f9f47..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 446 zcmeAS@N?(olHy`uVBq!ia0vp^0YL1e>P_2gCqs-wY&y1_H^=oA+~G$&pK+HX-vst` z96md=1RFKOmnuz2*1F)p%xeN9o-UvFCuZ7xnSbw6PaZhK&R{Rx$QmyCX=(a`2g_?t zZxKvcV5Sz8f5?aX+zkCG_mcSP-mkLQ{+&^xf6nym+3gL*+b_=89&(l6!7ePk%Q2!V zX~JDEo=vgcj<+0nHf5$fIQ+7C*}58rRm=D{HBFUB*?+Am{4#s$Y&Xt0U->Pisy1&# YGJ{gz`$cS21p1P})78&qol`;+0OIh}?EnA( diff --git a/mobile/android/base/resources/drawable-xhdpi/reader.png b/mobile/android/base/resources/drawable-xhdpi/reader.png deleted file mode 100644 index b202314d407a96a52ddc9a24e1ca5bb1b2ec27d0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 355 zcmV-p0i6DcP)3>Zn3OcjlP60qQPSD0c=LAVa@6jm`o;{$znHI1nbJUp1~2ug)!%@+zrlw z54OwCe|NS6e*@GTuy>+l+`8}v>c7a5 z@QwG#0DFuJcpKgT*U!M#a4z?ffe;ar0Y#|VzyZ8!I4D9gp#Oj(Bm*+g83W{1LGILC zz{)+qI8%wNzHj(5@f8e!0jCgW;>?4IE!Y4XVyv?EG8+PIA0F(Ye~VEW)A;kVIk8+@ zv&M9NESZ#Z&+8=WllVoh#PoGQJSm7INs^?l>J5}QT4M`-4)*{6002ovPDHLkV1h8Y Bnr#38 diff --git a/mobile/android/base/resources/drawable-xhdpi/reader_active.png b/mobile/android/base/resources/drawable-xhdpi/reader_active.png deleted file mode 100644 index 8a9872be1e17dbd12075492e763f0d2309a4f8f3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 360 zcmV-u0hj)XP)dlpbs!NSsfTNvwecXL`_v)Rn2@=OeoaFgu8#?6Cn^n zmRpj*Iq-u*(=R#jLCJNBq9}@@C{IZEbQvmy9!mUTi=;Mdi8T($d(N$p^)FyI8Q_W+ ztZ*JUtNKrY5Wx;#F=mZbAV)EY0qoq z1rBE6$gB$3jP${g$rRx838BNu)uIAptdI(5h1v`3aqF z_Ed)d5E3W|WWYrr42UeUAVdu^30DIb1d2cqTq8scSWx=$f*n9V*p~$P1v4-*F|)9; zv2$>8ar5%Y$SJEBnVH)=dH4hbB_^k47nPKD^-Ny0c-`i0dk-8vb@B4`8@KL1eD>ni z=da(s|M>NntF&%A(5Qo+E{-7;jBhV6Pdbty!E!jR(^Ud3|-_4$uB2qcOacOzQd?B?x;j7P8`yLcOba%VEQS+y+n5oRchvxle zerGYcv)TGQkAuiVR>tp@)ePk|wFlIB|L$MyCoZ5n^nxYf9*u1S1@ ixVN}|QB~XTSmrPND=Hg29c6%?VeoYIb6Mw<&;$TeK=;bx zoi%6f>^bvh&z(1S-u$`q7R;T$V9vY+OP8-$wtOWJEnTr{k`i9B%y}@lh zwX;RH_^$Bx|KCj8|2}Nf-uK&#_q;co osP}%?TFE7k;}&Xv*L=)zm0x0Ac*d+GpgR~mUHx3vIVCg!0EMN6bpQYW diff --git a/mobile/android/base/resources/drawable-xhdpi/search_history.png b/mobile/android/base/resources/drawable-xhdpi/search_history.png deleted file mode 100644 index ed661a729e5ab5302df2644891fe818e056d4a34..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 561 zcmeAS@N?(olHy`uVBq!ia0vp^Dj>|k3?#4J%UA`Z8UuVnT!AzYtXj3Ip`l^s%$f7& z&j+&SuV24(>C$!U)~#K;cKQ1CYu9dEw{G*=4I9?1-?V=H#GO}@3X5KAFfcHhdb&7&By)64dY4kVC#G+;r}@F+w@-Wb=bbi_ zGu#nDSRcL{bbkOn|WEbF8C?y*>$Xwml>?J zJ-et!~ z3`gg6M-wf}Nz zG1(258DFomh!u^sTJF94_3M(_rvhAB``Z_&KmD>~pZ&S_=l<;Ge|+`&=fCP|lkS<+ ewO@{FYLfrHC!i^At^aXQ%zL`}xvXJ%=P)FxR zP*0Ak=K_^`IT)T`sD?sSjU(!t=88^_#^R{EFc+XQ=*D|K%ZUd zMtsdY>-Nr-4x6yIWddz5?lZ2m+@2w-iE^s!nZ#Nb)WOEbGh-Z{tlmMsTk)D*Qq(Jj zX;>SNjPn6icnmyR56Mxg3UVstfc0U1=%zlemLv7*`nySCt_BZNljeA(wkMcI)x{C> zWHYJXgL_f9q#wQ{R=mJQOqXHJhGH7l|9zKZqbB4l4A8qdI&cAxu!lK>YK;pef4|DT zurc-Je)xGR$3)!mVjf`r?}#gKA&Sx`yQ{2{W53^x&+-muu$BiUsc7s`l$JkI>`_QY zRQL_NpA)J{a~IWA@9|c=<1oOyl2@8=*9n&6duVs zt{rHCmMimJ7_oaidZl_=V}gz<&@K@}*rGru4U29nP%0LvUV)yQ)+aUfj5r`UDM)Eb zQGP&Q4GXvYYi6z+J{T4qQ`XKnAlj@tI&WB56`UdGu4Yx11ZNT(Oi&*_AdNl{s)O3h zINOoE5iDs_n{1eLOM0R}kD_bFqUn6DPgIt;FvE&*m=DMz*EMp9$2*>BZSN+;&~@iw zlAY-dmpzkpQ%bdnt9htD`3%c;)sDiqu#{e53+m-0Oq6xlq~GP(VoQi6$MD_k|1Q%L z>`!!XpwOKzXZ`v0%rI<;boS8wJQd2hxMzhtl8gRCJf(;(dr|z_4y3g>`S;!UiVt)i z8#3dwMc|ys~CMww}PbfOUW{#+LY~_IB`zdD>_6{o+xhUC* fPS87S_=EWc9Qb+B?~1C{00000NkvXXu0mjf_n{F! diff --git a/mobile/android/base/resources/drawable-xhdpi/search_launcher.png b/mobile/android/base/resources/drawable-xhdpi/search_launcher.png deleted file mode 100644 index be0fd65fb4453d3f9677d07fa81bc4ce16f4c409..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4931 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 BlAizo diff --git a/mobile/android/base/resources/drawable-xhdpi/search_plus.png b/mobile/android/base/resources/drawable-xhdpi/search_plus.png deleted file mode 100644 index de3b631cf7809d20c35808cc15b118bf8608b7a7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 231 zcmeAS@N?(olHy`uVBq!ia0vp^G9b*s1|*Ak?@s|z>pWc?Ln>~)y|$K%$x(p)!^=n7 z9rd z$w%V0?lO6w6zny&#MNyoWax8&t;ucLK6TzD_#}= diff --git a/mobile/android/base/resources/drawable-xhdpi/settings_notifications.png b/mobile/android/base/resources/drawable-xhdpi/settings_notifications.png deleted file mode 100644 index 3e96f6deb5661fec3f515ef1d54aab551cf05ae1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 493 zcmVynt8G!Zj&IVlpSR-%k)fIpx^6Pj+R{$1@dxbcyD*!Xa&7xe= z704w-+%3r+JpssJnz&t7R{*l*AC9&FWHDBK{VG&sFkI()fHVf`Tn~^!Z=LG_66y9Y z0YBpDs5byHw8^IfP51*s1kH6Pu6tV$iczR*;sZ6j1p*iaT%N@>xn3Q$hyf{vS~_0k jS_B9slz@iy8U_6YxMYF<6Oo$W00000NkvXXu0mjf4>#9b diff --git a/mobile/android/base/resources/drawable-xhdpi/shareplane.png b/mobile/android/base/resources/drawable-xhdpi/shareplane.png deleted file mode 100644 index 44e6878aafbce8af4420346714b5d5b462dcf446..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1215 zcmV;w1VHgD^s+smv{cRO=kwchOX?<)DX zE*dHp$MNV$eAzT2E_~L5Ef$V2U{CuH+ zlB7Hn5I^hLTE7DEp@rIVNM?1^{FTpXP?kaS+l2uF&AXtsfB6r-ISe)LR|rvv<9J*o zdu5ihK=y`GNzaPJzvLdMC*|3Y_@zu3%`dJ`G@d}^UbaEmby4%THDJCFfk;wbPhRI4 zXrx5(-a!0!wv&>p@$!=Hh!Er!sJFLweAIk>JqJOVvV0%N?klhJ2qf`kF3|j1T?dJe ziR63{%`8HYOQ0m>jeRMLN6~JL<9HNVA!rLwqRbLN{9#Kl#HUBj!i69lC?Q(`;@?_?xlgufBo-zF;XrO= z(dxw{jS*Ro#JoZfersuD(b7#aY^=-^iG>S60FWD5X#OrMoHRG-z`w zD&9^02);f6DBhs)sbni~yg+<)5Rhz*644xS9FG;XXwq{=vhP=SJY%IM%Ua`l2$HGC zv7WfkXINfDfZJLRB`L4n{~VcZGl8l)JwUvbt6H6vRAp%(zPoJ|D-vH=<>)inK;D@3 zGEikL->_Ihz@P(3;#n>EJ!xgZMuFVTcfP~~R^sEJHc22gJ-+^QwKbrqze#}45R{F8 z>~~TbM|rkH z**#oLst4q@Os-A#w#yY=O!5QqgR)JdBADchQX+ujV|_ppb6g-TjLCS##iaf~W}m2F z=4!6DTud4QeC5Zq4002ovPDHLkV1jM6Ie7p8 diff --git a/mobile/android/base/resources/drawable-xhdpi/shield_disabled.png b/mobile/android/base/resources/drawable-xhdpi/shield_disabled.png deleted file mode 100644 index 20e58daf78b63388878d3a0422b0f18c2a431811..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2222 zcmV;f2vPTmP)Y zltiwwN+i2EkMPdz|FLArlE}=Q3z!=z)&l@#kzt=geq`{zkDLLNC5mM@BDV&S5-yi3 zK_K(nlQToYUna-v^#)aLcM=dOjm+%;c{Gr$FkG;OIOmQc*$Tun6-pmOsR>$VG_5p@ zlf3ulNZVm|_B8kRZ!g0+T)s^b6C-%>sv`qfOH2S9uFw%6v;K`(j3Sma_4k1W&z1vMZ8Us zOh@D%id|K-)~U=N4jv@HVZIZ`9)nr80+Nw{vK)vneqLIaw^F`GUiS2fKKi(?MSPni zxv7ZPoBc#XCwswamL9$q5#%=ukH-@Y(Ul*Wn;^+xK=G{V+UfdtOEc`rmbS~M$2O9~ zpJ+vA70W0^(&j}w=Kw7`4hG=0ZQHhO<8W=;wr$(S?KXY)*0u|6`6l_D|IxU8eYC5h=P*T^F{^2UM0*wrjUg!nyY+X_I$Fpoj`Y7f-Ml(d zOw3_(Eoz_VEu`OzFkfl}EgYPMb0V&mreRzwXbj1QLpI4Ki{4W!XbfB+_eT)* zs4h5tXvUanL%{ti?6~dI?+`Z923HkmQEV=1i{owm;Di`j6vsO{JMJEBa8-de;>2y` z2PZ@y2yb`Tagn)88(dYOjR3cK066oFAmjGv2rju@8(dYO@!;L~!3p6RoMT7E-BJN? zs?Z#88-{@UOmOlKaNfCIA6)s+diLx&y|fl<a<(rO zBIBMBWZc>5cgPv&gDc;S0+6&8LSg~n;=#g)FLsXVQTpJ@_dTr=SVdY3RP92&IZar~5efmrjAmdCVfV(IC4xw~?aOFoi$jC`+AcE0icpC>xp_^#Ppemq$RNl4o=b<*!=_ z8iS%o0o=WzrClq@{#v}WqeNb>KQw<81JRODaI-rf7GqKhM6Q%MxU~I=U+-%0h%#p zKA$O)SF=7K%fk9XlNFt;QS*eCK9M7)^@YZuFkC7aAFIizP?A7kD5j$%luXnQnm<8B zDce|0h6a^FN8A^6{;k{U42`P?{7x|mpCdl?wD3=KcaH0(^@HY5JVov83y)J|M3mxa zYAi;+Ln4DhojO8eun`1V{&KEKDZ6JPZ}n1SZm1(P#1Y9D0o=JN!95G=@$@@v>)yv7 z^Jh`e#NeUA;6_hI&~~LFEb7yIlRv-wqK6g(*^f8Q#MF`Dgc3nLoMpCYNYH?|P__#HR1ivvdjTG8Z?%14{wVsV#M zL2bv~^r7j48$5XMq$Jp-o85AIjhutNB3RgEK;u!2yPcPDCH^e6Ek5Aj{h!c>rU&lP zBw0pMbQtgiW>uhRY0t925s;8v$PsbI-Rloc3EZPma+wG%GQC>{8c!LiKeAX9OjjBj z!nYY0eT^}T+VB2K`VP8UR0-OVnWgZw_$d(_T_)^Nd^-bLTcx21NuV>iOsoyg+GLZ{ zTTYg1@q6?Ws46tVHuGnBB6cVA*8)elh{}VExqqb|q_WUZ1T><;y~!>q>#~vE6~l@I z$7K$h?q&TUwDqVoH0Mgw`JI)#)AxvAs0ICP9&l}Omn}Q%>czS$4^5COPf;P@^CK}G zxMbJh(VWV6ht|${U1g!sGSSl*?^kvoMSMjyFXgfd!oG88@#VZ*G`P!()SlD|)6B*5 zLq4|vkI2*Gm+Tt5*wd=*HyVo;0!oIr$mKbp2{Nna%pow6Gq=MGBdVy9r!kQ9>od%Lz@6 z;R99^p4sc&l@EkWDL(25BvQVk_s0SbEX4D4r5M+uGdUmffgOcSdd26U!KMl@Nz wTxezS|G=)pf2FY>9T4$C(5NKEtG3pE02pyg2{oG2Q~&?~07*qoM6N<$f?3NU-2eap diff --git a/mobile/android/base/resources/drawable-xhdpi/shield_enabled.png b/mobile/android/base/resources/drawable-xhdpi/shield_enabled.png deleted file mode 100644 index c95a4876e1317196132a14cbf8277aba60764b0b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2097 zcmV-12+sG3P)000O0Nkl)2U z_iEf~-A5$7gv34*#r{H~+ez#!5{q1e!%Kdy0z$od}#QN1Js!{CVl2yeK4w3a7=d67w z3{UfU%;aKY&DBE;V!z-;yCUg@p#eB&&9B!&3=DxEXQk%_&t$ zo&sV{cZhj)M$B((BnJ6sZ!M~=CkD^_lBU;(2BO-pDK{W0YqmK><;GBHvJ2Hv+VU5J3ly2-FrS`*v@6Ox%GKu+4jl>|d@s)4;0U+jp z9UUC~NN4~#j2ekS`ootPy%&htb4MLxpA8M*-!&40+_O(i_s$`TZg47pUkwc)dtW0l zI5OT^QcAZ1G1E>4?+00|^o|;df%Wm$f&wF~Qp&%8L{n%04quJL!1ec!(x$7raS(~d z&;UvyH4=lE&?^K607a(!i>MBS1~{ch)@haW?T!z3@_&i>vh&4#vV87cY9t1gQD^T5 z|5vfT+flx!=XIGHi9y+SM`CnM0FadO778*l@AvHLB!;cysF8opsXyT4WW*d^88NPN zi~84pL+{%ak~NRbe#oZkh;e#<;LMh@SY8$nVpkC{3)**NJ;`Uz&*IfUOzEVcn%dm- z_Ibks*3Z>LOgS7#Dc!Mo*2wXGmu2Vibu|%#K4NDb7vqRECFVjZBOOrf*@jXtTwAkptU|0L*_MQdv^dr~GfQ+nz0ql7rc9~x zWs^+kyqcQ2c2jCFpEMc7_`S+7A{ev?3q`RctLcmR{NyAMquRMINN|Wri_M4bN}>mP zQ*4+n%ihHA$DNoc+AU`f7Ku{530>k&AKUTcN(|-#eBEOITymeimkY)(443*3A1`8{ z6Wty|){8|tcMD4eGUon9?8x1B-~C!y*VWg@ju^II6sBc=Fn}NKA1O!}W~Rr5n35DD zmyN_+KSp`4>*x@-Q5r5ciN;YAgLtkupRKKDhkgO`JEd7`TzQgDbU;)`MGUSZh+^vt z>;3Z}vR*$f=$m8EFY6q^_hIB`n8d*8gtaM-uZ(hX6{~OdtVqdka8#`s&L5-t}`fhVT=6{ z&42wQ1`2jS3=0@5p5yHwPi7CDtIGf)pC^o#Iz56Kduc5QhM|my-Prftx$cI+ z^#ptGz4sO8N{S&tgbRF76Ztm4)%62bQI7Tv>iwiO4xejStzm*=kwn64x&xJ9- z$W0g}aAkp~H?oEPO;zvE`}{v{Ue5}SV|TQ?I&nwCpHG53%cz{^_5{<0fkCppTx~!{w91Aj}QWJ z7km+PR4m40j-ra#C`|OZRQ(Q0hrtyVp@*a)_GxtFCF%^n9{vNwQw9YB;3(RKx|vlnn7%bMzMde>bv}$ b?C@|I1-Y6kJ3lf`00000NkvXXu0mjfZYB3@ diff --git a/mobile/android/base/resources/drawable-xhdpi/status_icon_readercache.png b/mobile/android/base/resources/drawable-xhdpi/status_icon_readercache.png deleted file mode 100644 index e5211c71be52bcf611b7cb1de6745d5b53fa1a5d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 531 zcmeAS@N?(olHy`uVBq!ia0vp^B0#Lb!3-o-_pE3EQjEnx?oJHr&dIz4a<&Kfgt!7} zhX4N=X8vcGf1hF6LxzP<7-l|TnEixd)?J1f4;f}XWSIVlVJ1-OKEsR$3^VRA%)G}i z1H!$}F#RsWG$8jL!*qxUm#DP-Nfe0uF5r?qvfwX|cfsDIg5-b8&gQ*KB0+IzI zn8rTq-`{{Pwlu^^t)-kuRv~%-M%gD^m%_}G>Evu-isco1(bM~?o ztM(r{bN%MMM^B$UfAR9wo44;ie*XREpV^iv+(2^wUlYCoqRUQT))id&f?6z0q4Bej-r3 zKY2HAufnF}tc24DZkaYa8WYH5Voy*&UMJ9Uw z4_~uflGm-$X~OayuV3ab)@{xxoNxXwp?Cix{?N_73Em8xe}TSX@O1TaS?83{1OP?o B?qvV~ diff --git a/mobile/android/base/resources/drawable-xhdpi/suggestedsites_amazon.png b/mobile/android/base/resources/drawable-xhdpi/suggestedsites_amazon.png deleted file mode 100644 index 012060dd9a5b1c128528d11a6aa0074f9eee38db..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5924 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 rgD~SaGKR9LwS6mE00000NkvXXu0mjf8c$U? diff --git a/mobile/android/base/resources/drawable-xhdpi/suggestedsites_restricted_mozilla.png b/mobile/android/base/resources/drawable-xhdpi/suggestedsites_restricted_mozilla.png deleted file mode 100644 index 03a2e9491c18526ce5d78c88895bede3c3c8e13e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2324 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 J*DKmY{RiDqh*SUo diff --git a/mobile/android/base/resources/drawable-xhdpi/suggestedsites_wikipedia.png b/mobile/android/base/resources/drawable-xhdpi/suggestedsites_wikipedia.png deleted file mode 100644 index 76140d48b2a250e45dc6e1bcc6719b5005f20b7b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2874 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 diff --git a/mobile/android/base/resources/drawable-xhdpi/suggestedsites_youtube.png b/mobile/android/base/resources/drawable-xhdpi/suggestedsites_youtube.png deleted file mode 100644 index 2896a9c17a98ef60186c37197cab58d22337e592..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3686 zcmV-s4w>PuAhZ}$!bjsDME@|v7!s%=%Q#j?pN$?XM90wOT=Hy2yzX#Y{%13~A zfy32Y$^r*~{ngx-_eN19Z5K*(piG(v2r3Db*}yd5N?<$}TY{<0QzXH>9asmf27X20 zy+)py%0TnnLx2s2bSLmF0!v1%GG%(ejIO_cWf1|lImB89n&TOQY$E8B1lUI$a#^g& z1CdO-w-IbhGyJL1?OB!wuaJh%}FJ# zs0X6?jBF3NQe~jw4Fc_X{$A@krYv{I17;c>N3iX*TH1cRLuuoqT19=dXdiN$6xm6%BiE#r z(j1IgW(*==eip~4$b5)@lVvdipG(V#4x{LTEZJ76!`5YiKFH12kVG4w(%bO;T`L~T z=4dL>&MS>=r&fEi_~Jv@B}Z-MJZ*W(>JGv*%M_@e5umSOG}{%ZYc=xKt!7Z~vd0|R zT`ql+HQMhgl}HX&y%fuI`U#KdqCBms=_!@kVnl8t4#(q*Tg> zf+palXb*&?9?_x?;IGaNN=q#Bit)3ZI>jHMAV~xj0XZ6n0)C2iLwU)kwlIEP3)|M4 zOf!Bi9B6nT5qOSARx&ace#O_3@9CmE(3Gk99vH#k6fre+B!Xls1a|Yy%YT+k9#4RN z0rhroEEH&=5z**E$2bbIogl|PZvp>7=MM7RgzO~>({aZpQw<+m78Q2lQ)xH6@{YWl z&T)}mQ1%|qqW$#gUx3{FmvJ437w=V*1EBQ>?gX}^Wn@P={wFXO@SKz|OWHl2mXBXeavA%6nC3I>OjUT- z>Ab=pg7{5ekHrx>oR*8XL!YR?9SZL}g->MnCwYS>0~d>mMIF57z`tJS_d6K>5#HcA z9d*qJw9U2I=Gs~~&^Gyg5uE;fIo-DYJ{-I}s3mL&9YGOj#*e`z0rd-#x?H_dj`snf z8LP7^@W>hYz1g~4-SH%MjaFfLS!f`&HG!^&3C-Yl-+-i&sC!1djx*PAV@&m$k&yzdl)=LB#sy6)*D zW*3HP7j{KjCO)4am;ilxx-L2n?YAENc7kk*N6SG?u*qIjPo>mU-%|}YshwN7_{vdY zS8LR>S6X?vi;^fUq<&3;l7qm%m|%M`hfS&n)EH<+)ph*6#*~tO{!D|iXf%m{Wbr4T zs)bS{4+5dIAXL!=dgmIZ3A*!CCe}WA%(c{W4NeIk{+A`os0^7G(sj{RXiL&`qi>`P zGJx_rm653BO_zaMpkYW?;5d!nL9^OADlu)sKMg*!cjCxWjCcZyM&S^i_98xKGo4Ga zRN^sosK)8m6D^MRsu2ypT?9Of8}{J;t(2DrT(Uu-KVbvatwYEC=wg4=Jb1khKH06W zmjBAN)RtQ=YdX+qP@4)7ij|#o^YN%Z)yh}hT3Uea?NUK5i{wQb6SRU;Za_;08HX|I zrjFY(BpO~7H(X<-!g+N(6R3`s@eTBIl-~}%XGSqEQn*!rauF#bE7zOB#U}G2#X^hO z9DUN4yy;V&Iz@*cJXWutuce`#9`P~{x2Qj4`?%$6;M=HHWh#F^r%f@Y8Vo7Bx8_T)bSo}(N}CCkOu?p_xR#iK*%Ng)27sYz-4gCzCItftii`~ z%g}(l85g>USV+d=qNC3#g$ z3p8zKUKI`y&kE2>a&Zsz4~V#@Byz2_q{W6j=xOO4jp))eWwt(N`i$fVi3NTtb$43o zu6)X%l91_v!F4sz?&mVOyF5N0q)<&}wQ%N{&J#c{Q|FUDfO!sd% zc%fo2&}5O%lgB!=eLSEs6lm}F`X^v+NiO06uD>juCKZv%;?0^mMUGKTOOZtOYOSWE zl^)PMkQZp1H6dtQh6Fo!^{4hRPCExfqVM=e)@QdQh+%{mA~Vy`yg;jUgve~A2P=!%E2Elr zcH4vIwscdY*>RZPAPbM>tVsE_OZo8zpn7QHt6}PsEvK#94L<=H&R`P~I9XM_#QC9hR%i3bDKLib|El&Udd$ z_1@sxTOT3EoZ>+$^{O}z4tYmV2lA%QP=i_Y?v51iW+`uA-TnsZYE@OCb4>TJeOSPF z?K*}Ywgv}uSn~d61e*5Bi8SaRmixFhDs{)$iW>5ZYv8dg`)r{>d65yt<2C>GCht|7 zddv*<(HX_!!Hh^+u=($-N&E6rtd=s~*fHNJG-X87-slkHrg;|nxUJr33r?PU3W2k} z&g1xOM<%Zrx(Io4p?d9Z58cQ#k3QMM1>L|}%zvr% zC52B&7X;`+-bdeZPX;pNgH?YI)Z9-noF{eUvcY#>fKTg;_JnvTdM*U|0Joy+!GOLH zS-WFHytUB1QObx63JCs#i^vyC2ISw`JP_b3LY{@b-STAc?UOsZpOpF3mC^V5TEZfMp1rhA=*VpRZsJ; zp;y*R1HeNMJEQTJ{{rJ&5OPG&BM(IIjBm#0WjB80gRJK7TPB0(6Xp9<-o1xh$mY*URtF?lk4gn^} z;5h@xJI2w}bkdL?B8cb&LO~`hSIM7biZ2AeJxS}p&?QfryCm1@q20|j2(7n8WS9JD zc%X?=(vRu#vW{Y{f1^j>d@R8Ie1Kjhn=f&`0rG4%U!rzL0&5;JJxetKBGcTjZvtL^mf|aVeUb{_%}|!2FVt@_l(RN4Pep%#{yljALLN-i%dZv9#FodNY)nrt_p7t*qi!z0-3kb z3BUT2SRq^F$4#ybR0bR_|NApqiJbh$H`=4cj#2DJiZbnttm+#NJ!L0#{xupNuz{&l z{!3r9miq4__aN=bx=q>}P9{_1cLWb7^KbneALKLgrz0Hb338S+}v zdSMDUrxy8QJk`#QMM{Quae(}=a@JZxZ2&!^s5l$gjJ%9ybtiN^j>EoIw}L`~X6-6> zx14<72Yr+)q%xP;Y)_no?L)Lz$LwWpX4w~g3y{z6xOTZC7;H9~50!^=olf*vpxLHd zDJRh8;G?(;BjtbPCq(QTd&cq6IiS9^c#QH;$_}l515Cvu>@M@ zrg%A`b@-G`NArI$yX)anzQY6Gbc0MtWuV~%oU)DV3uUn3g>I+lF8sOFS5r=%jz^e6 z$AQYG@Dzt!7Hjf=`LSgg$sX0%%Ms1-3QpPtdKDwvjlio;Rap#rKsKV~#HRAGP>_je zIidv}$ha1p|Al@nESCclfiY0!RHBWO*>~KA1J(hnf#n3)Zt~1j23n?j(v<|CoEysd zq#Vh}E6`^FXI6ppM)OhfAaWn%U|j<_%j{nTTb8^p;O5Le_N>?(4V|Lif*do+*|HDE zp~;b#3rE3a+rfn+;BuVjgB%eG$P1K`zfDld-eckPe`?{nSHu1cZU6uP07*qoM6N<$ Ef;plZyZ`_I diff --git a/mobile/android/base/resources/drawable-xhdpi/switch_button_icon.png b/mobile/android/base/resources/drawable-xhdpi/switch_button_icon.png deleted file mode 100644 index 6dfa79e5188777304be8a6d0f361a2c5c2b67926..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 222 zcmeAS@N?(olHy`uVBq!ia0vp^8X(NU0wmSG7d!(}OFdm2Ln;{GUUOt}cHm*XaA%?R zjv3s~5_C`0oSRT~mthj`nfU62ABu~1iTLT3L6qGV$|k V*WA1~H4f-t22WQ%mvv4FO#r~mRI>m8 diff --git a/mobile/android/base/resources/drawable-xhdpi/tab_audio_playing.png b/mobile/android/base/resources/drawable-xhdpi/tab_audio_playing.png deleted file mode 100644 index cb4466e0e2e238d5e81d0996162810e093ec13c6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 341 zcmV-b0jmCqP)71Q{0003TNkl#uwj8B8&kx_~!Wu!at@*2XH*?T(@+7Z53MY-}t8 z6c4@uegQ<(WqwXfk3)mYn>Au zH+fnj?HOZ|W89N4fl5(>Z$@5QUS3YL*2X!=Ti$bIszL`64duSN$kjCPWt;aL@<8Z7 z_vfIl4a_*_o-=cPp!H59z@ed`Y#+$i?zu50>+M_~C~s2vppi}sK9)>;zqBErh?O?+ n;w|5v>$#d=o_;* zdLqwt19jcDQw0+LJHM^Szu$D_w9)U7B$4=8rI9b3cHNe0^9`gbaixPMiRQAL4%m2^meQ@A=)((~_l^<>ZAq}y$ru!M^%X=;Hv&_N8Iu6{1-oD!MtL3OE)feSFJlua4ROp6;tt^PFK*|46Pk`TwW1 zNjBRnTUPJa&;O?OzEqc?PVJ`tM{fp&Wr9gQGYk`txx78(I71+Lk?}jHyI)F<H zjA8Co7Cbo1eTl5#!AFddn(t=2UvDy6chTmUbJ#8>`4*oU>(<3=20D+y)78&qol`;+ E09{v9X8-^I diff --git a/mobile/android/base/resources/drawable-xhdpi/tab_indicator_background.9.png b/mobile/android/base/resources/drawable-xhdpi/tab_indicator_background.9.png deleted file mode 100644 index 8b561749af9c1485fb030024adcd1bc73d0bd028..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 110 zcmeAS@N?(olHy`uVBq!ia0vp^Y(Ol;!2~3~zqFVH5-17s3kK2*4C#9dK4$@GWltB! zkP61+ganUt2ga8MT#XVMf?o2soc=F2Iv~iWct(MR!Tz4+%(HCF7eV?wUHx3vIVCg! E02B-xlK=n! diff --git a/mobile/android/base/resources/drawable-xhdpi/tab_indicator_divider.9.png b/mobile/android/base/resources/drawable-xhdpi/tab_indicator_divider.9.png deleted file mode 100644 index f1338d8031ce95bd4ee12befcf44868e3ce293dd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 75 zcmeAS@N?(olHy`uVBq!ia0vp^%plCc0wn(^)|vn*Ax{^_kP60RhNk}shd$WPU{qo- X{>Q32k2U8lP?o{d)z4*}Q$iB}9-9%Y diff --git a/mobile/android/base/resources/drawable-xhdpi/tab_indicator_selected.9.png b/mobile/android/base/resources/drawable-xhdpi/tab_indicator_selected.9.png deleted file mode 100644 index e78a2ddba67eeabe8cc14db9ffaa96602f184667..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 97 zcmeAS@N?(olHy`uVBq!ia0vp^Y(Ol;0U|59*B=E^8lEnWAr*|tDGd$(c~&$`6x2G= uq4sh9ETM!VtyNEZZaVbn8EQ2eFJf4huE#v}LjE719tKZWKbLh*2~7Z~vmH?Y diff --git a/mobile/android/base/resources/drawable-xhdpi/tab_indicator_selected_focused.9.png b/mobile/android/base/resources/drawable-xhdpi/tab_indicator_selected_focused.9.png deleted file mode 100644 index 3e1fe5560fe24739fdf612a97041da1f20fca512..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 103 zcmeAS@N?(olHy`uVBq!ia0vp^Y(Ol;0U|59*B=E^dY&$hAr*|tDGd$(c~&$`6jb?9 zFV)Q1l_7Rma+Oy@%CbdK=VtI8c_Fk|$U>Qmp;Ow7ImJ~<45*XA)78&qol`;+03&o9 AasU7T diff --git a/mobile/android/base/resources/drawable-xhdpi/tab_new.png b/mobile/android/base/resources/drawable-xhdpi/tab_new.png deleted file mode 100644 index 76a5a11827677a2227f68da9ac9efcbe3d5b0c73..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 293 zcmV+=0owkFP)(r9oV#ps!KefnLDHJkd5R0+E$FP_p zVllF5Q`!e|NSC%?#A1MG>yV$JF=7x){#j4Pj-n-^UsVl|-<2MF8e+i$|f_fUniN~nND11 zSe9kuh-AyM>dvfo7KsY6z39c>A<@r0>rbBTPi$j<4Q(WP18LaP%w~7PX*d?=;9RtU zw$LWpMjzNlpZR_P7jObMaCAM@_~D1gCe7CNtY>?%r#hvFyr2wz99F|~xX%^Z8vtM7Gkk{zMIYnPx=$ebzzw0JziN26hBNa)ZUscqhcF)?lC6&Y zBz78(^Q?d1yk3q!IyBQ$bIn%oqZhP;hS(2HaU3)rTsy7{Ih2qBk^+*D0+IrfkOGnd zl8^$D0+Nsdl8^$@#o+qPsU&nh5DNt4+pa+<0kLpi?ES7mC;_o>#P(p<`jB?+7_l;#{Gf)VNdI=k{*tx{| ztY`hF^R7(&kl1cwRKbL>460RB@0+N`btJ6pH8(d;PFXewvu)tnj`pG%iT)Cs2V&v8 zHmw+;F;r-@*05tu{E1x&Yj1C#(6gRMtUs3KQKCQsD|#hMnMPI3R`G0C$4ZP43x_M9 z%+(3uNGzc4simc55*Nv8SK|c4c4x)U>i4|af-aKb&S$fA@W{d4R+860h=qfBvFD4N ztg2JG3hlrD{u`0RE~qGzc^;1-SE1qVLGeOSUapU^hSLk3jH{~wMxyPJ$BG|A2-7x* z`@2QBAqK9GVIV8`{5{h(zFW5_0m^}7P;*)NT{(!AI$ zkp}S}^4h7&t?1(lNdaOoP$PO}!GZB{lvGykoa!3Ua{LAu(&7@_u5<$%D!xlJ)ZW(C zHg2U3-gn=4k#hWcJ{68vL;=!wZ^2 zdp9^^4M&xoHb$DN+BQu^8Ovk!1_ZMOF0Nth9gW#`C)Q8O@`C0=%bzDEvES>N&nFJo zW11oIAQ|cnNX!8pk7;H-954LT!wFgsb7mvjWTV*gD8^>9<)41~>5=QaLwvn_@+_vb zVSbAw>`!dXNW~kLXrN=vO+|m7LhL>eKo-JmTQO%#GB?e`SvEiZXd-tP$rcj{Yk9T< ziS3GfY2b@U&i_Je&((0NXFX0NYz_{X0fp@jzkV$}+(A3pqc^dp{-DDDeg_hA?XitK6p|zNN;!(nest9nbbrZwmx`hVQN` z8eTO(Nn~#r@Zn<(r@`lL@zG0G-Pen~8U5W8T#;<|da=t%?08H`kY<|#*v3BmhGTGS z>@VEGC-?>*U3auB%VzXqyI?K5hR0EaOkzLhuR#2PZIrRr@Ck{&hGS}I&c7^r8fn-E zrT+NZXqj~coP%@G2HHZKXd8Xxd|pG};E?nB1Kh9;t~dtV;RAee72E%Q5?d*2kDaUl O0000u_-Y{BJv}*X^Iba$$j4(~@1=KX16V diff --git a/mobile/android/base/resources/drawable-xhdpi/tabs_count_foreground.png b/mobile/android/base/resources/drawable-xhdpi/tabs_count_foreground.png deleted file mode 100644 index a5a75227cae0c6503242cfb6fabc3adf717ff593..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 172 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA0wn)(8}b0DVow*xkP61P*DM*C19_M)uI*Ly zRxg*Bc_9C;L9TDBnG zDN-*cGcdF9NEjp>a9BCNZdLl}XxlwaigWwylkR!)JliU4_hZ79Wk3HZF8hC~mQmi$ W^Mb&N@Qpz089ZJ6T-G@yGywnvsy+As diff --git a/mobile/android/base/resources/drawable-xhdpi/tabs_normal.png b/mobile/android/base/resources/drawable-xhdpi/tabs_normal.png deleted file mode 100644 index f76b0221c3b416f81949acf4d7b130ee2cfd68f6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 248 zcmV|ao z+@eB}(14_aCcdgk9s=@Y&+}scH;f_~r%B#?SwO5tj2IEewIxa;&oF==6`%}oCNcj# z5HB)+*asBt1T%H&)c8P*ei%~40RfrJEjx5vv4_@jS@}D90?x+)|25xkx6TCO+qz4DzLLm?yasbB`EWomC)ym)ak2gz_9P4d(-W_aGbc?kl?XiY6O z$qU<<0igPdsP;du8lFdaK4BDcCr$F&2jik%19E?ZY47L{+6(zk2uQ9+3dmGQBcG2T z(Cd&wtOz7eBBRS>&xf@~mj&bp_Al^xAYT8kOQWn+Bs0HS^}~oIHOXzug7_GKD!IEo z02Bx0YnG&*nsvh&$|K4TC>JbAJu^pCXDFpqc0f64N$Ld3?SZOLn+N&ouTH;kwk#J&Vn*s*vA>l^SpR~0v431A?~lx_N2x* zePjTXSvHi?+{*2n5Yi;;V}t9aG!6AR?)G)<`GDdc-yMIE!ww#-d{BrD%>}?5K}}cw b&6D0+qXHUpP-Xu400000NkvXXu0mjfrOpXg diff --git a/mobile/android/base/resources/drawable-xhdpi/tabs_private.png b/mobile/android/base/resources/drawable-xhdpi/tabs_private.png deleted file mode 100644 index 14a3f4b79e8c56189b44436dcde2b4fc8869eb17..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 554 zcmV+_0@eMAP)KF{3vGQyuY2wsV&@qTi23Zm^XywsM2UztLvyvYpERaC23UgK7@)o~Tcc zw;ZHe@yy|Ze01=M4J?oVS@!XosNav*>?0%NW&s=cM7s$5Nrps&^oY4U5eSHe?9nL# z?L#QgE&|O%DDYJTJ`JJ3dl7gsgaS`R;I1|0(MH(oJ=5mcDmO&noL+fOlVP%)L#o+B zBW~(Oc2kY)`^jWEN2kgk6M^mVnsb4*%+cqO8xHzw%+R&0<$|#Czwqv@j;3# zjc>KBS!#+nh-cJfj{H=JZ1RfjzT`4Qr$3wMM@0!{f$Da52%4;5S zO*$dW;V~VF_Pgw3D;wC#L7pbgO$U$JO;P`T sHWpD$hDwVuSCgfhBFUskkz$1L0|*T7g~cmcSO5S307*qoM6N<$g7Xan9smFU diff --git a/mobile/android/base/resources/drawable-xhdpi/tip_addsearch.png b/mobile/android/base/resources/drawable-xhdpi/tip_addsearch.png deleted file mode 100644 index d7c18bdbfbe259d4fc80591c19cdef014e811e4b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3120 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 diff --git a/mobile/android/base/resources/drawable-xhdpi/top_site_add.png b/mobile/android/base/resources/drawable-xhdpi/top_site_add.png deleted file mode 100644 index 7ddc503bf7dc0c080c5a09cc430f3ef2d4dff2b9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 118 zcmeAS@N?(olHy`uVBq!ia0vp^0U*rC3?#SQPk0BU*aCb)T!Hk3%h%p|=NmdKI;Vst0QzhrE&u=k diff --git a/mobile/android/base/resources/drawable-xhdpi/tracking_protection_toolbar_illustration.png b/mobile/android/base/resources/drawable-xhdpi/tracking_protection_toolbar_illustration.png deleted file mode 100644 index 744d3573ca8b450dbf8063b843d322a83c3f80eb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3450 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 Mr>mdKI;Vst0M+w$dH?_b diff --git a/mobile/android/base/resources/drawable-xhdpi/url_bar_entry_default_pb.9.png b/mobile/android/base/resources/drawable-xhdpi/url_bar_entry_default_pb.9.png deleted file mode 100644 index ca9e114b69e5b098e30fae53b5162d5c32afc4fe..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 318 zcmeAS@N?(olHy`uVBq!ia0vp^E+EXo3?%u_CN}^nrvRT2S0J65nw^l6nUI_TL=6oM z@u?XQZah#VB{MQMF)B7GJ|z>#RZ>z?RZ}xEG1WIPbocZD>UH<>tvdXB2av^A666=m zpxuA<6Wip!d;U%N_wS1D7oC+rnYo@Wjv*C{Z*RCVwK(v&TznAM$o8)OO5x3Ieb>(a zd#n8^r`TuGl&7wo$4@Dpe-asL{$bV1)$JD-MW0>McQ)S0Merf#LrYFq#ZwK)6#u zT#y1FW1a2)<5mCfZTx?4)Bi6||Nr~@|NsAtqmSHy(%dCMe!&cE+WnKK?74FF)8ClY zA-90Sb3I)gLn;{G-Y{osVc=j5Jd=6m`~KIn*1DP*e_8+kL}aO9rB~t<@t8Fy76+Vb z+Tju^$=*J3k>1=XUdx0QckqsI8q5 ca@S*-97m2~#otYFOd$Vxy85}Sb4q9e01yj=vH$=8 diff --git a/mobile/android/base/resources/drawable-xhdpi/url_bar_entry_pressed_pb.9.png b/mobile/android/base/resources/drawable-xhdpi/url_bar_entry_pressed_pb.9.png deleted file mode 100644 index f34244e4cbe90021eef886e94ec161ebc7bccf10..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 310 zcmeAS@N?(olHy`uVBq!ia0vp^E+EXo3?%u_CN}^nn*g5>S0H^M`~Stv|K~IQU> zwE;w4$b^w#aT84oB_$L}j63>4xn3GxeO(C)wbiEZ-V zJyWju6}~wK6rShl;uuoF`1Xb?Q;P!+>qWCI1@G#wAB+^PvYGn#_Wv21za>4>jC{^4 z&V4Vkc-j)lMVm}4dt?fKF4>Z{iecBjRr+CHWhbobIGd1@<#?83W~>1F-IR_ge0Q2E zB(f51r*I1`n4~1ia@mjl%AqCfqzO;=6F#?>T7T znf7+E#=$+*>E@=VdX8I$AjDO5mtWuH^4`vM8pVr`wWO9czJyUFRzav<@FJy zygq`E7esLK`UqZLAJLAGuLbhN^?<3*#bU8o?4R}oZqD2*t*0u(00000NkvXXu0mjf2Je-8 diff --git a/mobile/android/base/resources/drawable-xhdpi/validation_arrow.png b/mobile/android/base/resources/drawable-xhdpi/validation_arrow.png deleted file mode 100644 index 0dfea5c2faeae4549fc4f29fcf1d7b2884e7088f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 262 zcmV+h0r~!kP)pfh@-TM%6XL$ve^SecM=2v&#Kjt^y|4mFz zS0aid++hb16lf1GfC2f!HRh10F}@Zj5STHLttq}2cMuy6QJ^8d!#rNW2p{5~93nf6 z_+}5{pX?wz==f_0pnZh@@lQ69?q__XdA!mQJgxOV9>6Nn6~rqT!IMf{KtiyDWcBeT z7Le?Jypj=5@%^}lD6)ehv+#68AxfQK7;&$o6H$%`P9V^36aC131SyqCra{J+I{*Lx M07*qoM6N<$g6oZH+yDRo diff --git a/mobile/android/base/resources/drawable-xhdpi/validation_arrow_inverted.png b/mobile/android/base/resources/drawable-xhdpi/validation_arrow_inverted.png deleted file mode 100644 index 9122e2ce6259ad348c06c0d21329567823552268..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 272 zcmeAS@N?(olHy`uVBq!ia0vp^20$#r!3HEx+wWTrq^^6qIEGX(CT}SImcp9ktniNQ zk-x*AdX5Lu4s#ffOyW7gprOEajP1C1<(9KeH<`K*q1j@{9j6SzzdrStBa<%3 z*mupzVVkTj(paId*Y51SPD~+fY3tuB>$Zk7ee@2v)HKa#?gJI$GY2#n80wqVj=Y&s R%m?%$gQu&X%Q~loCIILtZhZg% diff --git a/mobile/android/base/resources/drawable-xhdpi/validation_bg.9.png b/mobile/android/base/resources/drawable-xhdpi/validation_bg.9.png deleted file mode 100644 index 773aa03a8ef3d86d73e87499efc4f8a33146007d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 487 zcmVUO>z{q= z?6dEEv6}vlOtz>A0002Y)H_Fl%)68WM~FQB6%F8sqVJ^z!%j@8_OA zy!G3M$c1OlU${4C&fFa`NFnp-jcc7#lKPrJQ&Q#-x}uP{MapE6GKHlBhsdl-Oc!U$ z48(b&qs4O2tQ7q$lb|-Kfa>`1gEO)o2NIFqiiU}Fi3D~e6Ujs}k$^~|%|s&dzg=w* zX-y;1CfYx75QHy(eSO!F)b;YVhmzLFHa{ix|FyOIf4))In7mX{*CCO&hP8SN0RR91 dPah0~pByoI&Ej|cq~ZVo002ovPDHLkV1mD--}3+f diff --git a/mobile/android/base/resources/drawable-xhdpi/warning_major.png b/mobile/android/base/resources/drawable-xhdpi/warning_major.png deleted file mode 100644 index 896062e988f6ce50118ef6eda4ca11269c867de3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 507 zcmVK02%8RZWQeS zq7rku%w7tFT4B-=T(k>79Wl3yoBLcBkhyN*UZWiX>xeZjYwt-HkTq`LvS^!tO~hIk z*&9~WSthc<9O8FH7KgU~_qIeDe3bleXleKq;u_-`=r=go0ud`#*fOz6ud+kSfG8(clbd^`(+Kc+Q+(=_B9>;y60fi` zg{gkCpjJ*yo!Yx(p-U07?#*4EWzVT{V%N#u1q-?mvDI+ncuCHlqZ*Wz!=Spk=QWMg z4J(k%vv^j~vGf<&LDzsVz*1Iwr?W}!k60Egg^s1aFddjM=!qsU$+dUFfKP%+I^N*Z xH$a%=5BrC2fH28Nd^jb3_XQ3Xn0kNV`36=8XJqpqT5kXV002ovPDHLkV1jTW=6nDE diff --git a/mobile/android/base/resources/drawable-xhdpi/warning_minor.png b/mobile/android/base/resources/drawable-xhdpi/warning_minor.png deleted file mode 100644 index 84fcff9bb853718a25369028e3f4bc3de3736066..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 516 zcmV+f0{i`mP)q^g76)L&9>}-&v|JYN$+3DU$nL|j}ZFC5&9bm zrLN}+PAPTS5UM*aew!P>hE(n}AivE8oRr=LKw+Y+c0fmJ69q_Ft#HDab3o!mo%%*k zXq^B^E~spVJ0GY;WOPU9yOk4bF9U z=~?<9$%lmmwTYP}_nv52rHGk!=I#teuVS@{oo3H{q(L1bcEX7z*bW~(>p3`38-}=< z`;gf*r(xpz0HRM#sW2$l&rW6Wm1phe-Fs0Gt_?%h%_ zPl9Ru_5RNrAWUOBhx(g0K$u2=N?rAdf93@a7MNh0`=6Tk&?(MM3+}m!6xxa62TN~rcwpTqmebufs^>ce8|HMB%KeMlRU-hoX z9v4fdFa&xqGW^JA1eyy26P{K^v8|Yvv4-c>sj1q#%ii6otg(@kS6?dQ#BZ#r*btO8 z`}MBPHs@#md%1kRp2fo!;Wy<6RhUeIe1x4i0>5n%Cuxk*9OlMt0$}b z@2h>k_xric9A1BgQhfLQez)7K`t8>1bEl=wRdpy{o_zG%+uQmJ=RQe~`Xv%NoyF5I zqsL07>_+1K$!Gsv++OG75-S{V`quK;r<=@jZ|(8BK2OalGLcc_bptcs4uA0rN!z>3 zjHYQbxtv<7XZS7WU+}5UfU`|a@!sz8_nrrh9UM+~a;y(tc_m_(Pr#WCEfMW}vd5ep zX1WwS)y=%1*e-L)?nWn1SCVp})!ufFfH1*>?q191mfZ@N=J1_kn!x8;`#$F#Nx>8O zfEJ2*PGAaQxxH=e3gd&9&NBzx(>+-@M~bmc&oM}0K~u%NbQdpy1xX*S=|?g#bFOVu zWKd!7XkcOBoWP*KD8!)Rz|_Fv!7zb=lR-#<(SeB^$puqYu^ZVXAj(+bvZN&!=sMA< z3@gg-RinX7d#4_kkKR@h!$k?p_rKWff6mk67Ww^t@&O@@Q2za2e_#2%PlSJGXWo`PvjaXWezq#^ z_py5Z)H?f@_sKx7z{w9bEWA5qF8@BupWh!ouc>=$RDB>v)mL>EOF>X=(e^$0_5~MG z^X(s3CaK$Ih_y7WB0Gw)+>syy_^=bdvlP+ zE@3MNu7@Y5Y`%OlFLvM2$ggj&Of8;Ym48d-`2W=}tu>-GqJQo>aCrd}=Op39OEjtj zZUtKf+zQ;Jqj79g&xv|&Hj$&xcZfvaj@?#V7Wiq;-`&63S9GnGzHxEe3MFsPU?CB0 zl_o|-#;bQu%gkrqy+drr`h5v&IJ*5JnCy!usU9)p{p#1a*i$3uQ;hd4hS2NwJ0{Ca zRz0e@aX}G}kJH`B2Ub3_kTF+Lh+mdKI;Vst00B%E A-2eap diff --git a/mobile/android/base/resources/drawable-xlarge-hdpi-v11/ic_menu_bookmark_add.png b/mobile/android/base/resources/drawable-xlarge-hdpi-v11/ic_menu_bookmark_add.png deleted file mode 100644 index 54d88fd13aa85da0699d63021e2f27028d34dbdc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 765 zcmVz(2;aePAsIFJgDgT%SO!$|S6n~Z2feD|Q0b^%i(yr&l1d8f42z%>*(x}}JCN1Ps zN{U@E>R`iSW28#HE?oIw7)wV!Bf6EGkWkVf{6%oLg!`O1j3w~-BmIIx!f-7lc#(Q`l`0yG1NyvRH7UVFVf@w^FJ8 zW)bX!FVkCa%b_ zY(Q%b%N`+)yGJ-(2{D@G?};49rRE3IlWg+32H}r0Pq2&8X#U8dQ+6ryrq~lr^Vdrk zh(W_()dr27MKwoq6OcoJzC`h65~iAg7HAO4k(?Ybz%Cvt6fuaEb0N1*Y0GQ_G|O(U z6bIl(yrx;yEfiYBsKu!h)-l(>G?RPL^pzvp7eYVZuk7?Iiqu6Y;HjwJlZ>kKDW=^3mi44;zw{Sf z3Iohu2VWyA&?#8?npRny#!B>F7{>59^U%syC?qHzbh$DIn`T+`W6k2@M6wTR3xp4I z&_FBCC0JG^cT{!Nls(f3H+8}u<<Alowl0;@I;tK%Tt8^C0dEgw$0wD3y$hc&dBs7aQ?^q9 vp9-OC@lpR@Dz;m0?rwjGK`e40S9Y3zw_4!lvI6;x#X;^) z4C~IxyaaMS2l#}z`eYU|OuNf4?H-uC%P<|tcnD(80Wx5MAjSib;HpO;awbsybB38u zVeESh(}5yLX!?DISwKS|;$TC73VH=8@L?C-#Hf}EgdOx5f z$S;`T_naE>e3}R_&zrvVI?4i67E)tsk3I zE;Z)6Y*oj_;i_wkh^L%bE<$CzcM50#Q`=8mtsaKTrIsW7?zjG7c2sBL&xvDZToyTBe zb^@D$uSX?I*&n05dO>|Asx^%5Fmf||g zwH}YZ7S1SoC6&}|_H3@%TH({Cwvip$-ztOCmfNpp2yl$K`|sZa-&4Djz6QKu?7!Eg Vv3FZL4=@}UJYD@<);T3K0RZA&Ka>Cf diff --git a/mobile/android/base/resources/drawable-xlarge-xhdpi-v11/ic_menu_bookmark_add.png b/mobile/android/base/resources/drawable-xlarge-xhdpi-v11/ic_menu_bookmark_add.png deleted file mode 100644 index e54d42905bbaee8705c44cae962046b6814b2edb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 958 zcmV;v13~h7PCNDkc?uS!#Yf?$> zxhI($H3dlr=k zbb%I=hgIAAB(D)+ygmdOjmoAZuVGQK!&{)(j~OO;g{T{=7B5JN-2jWP!wi$Wf)cP^ z6H^PO&El@S$vr2ljOp)$p{xtfi`-l0wXLA6{qTBtiKBt;vbQa04{KX8!-^eqygH9ZO8t#sJN58 zgIHro{?LIKh-HX>7upEWiit`{R70obXwhG|}` zM)@%b8Q=uB;U4+erm?!_6N2$3rUb^u9Ci+Z(=c;SEmt&4zUo|VE0NC(D{AJO<17fk z0WO^%F)bFOX_4UQ^4Mx72RW|b3l1Z@CGH#we zVLlXiUlIfs<0G^Mv%)VIe6^Gr6~f(5IvVM4zg+Ou(uIX^bCv@%QW+ozr8d~X!38og zOFLc4`sISJcF~<1hQNdIq%B(TA3e5^FR`d#C=&zVYxw#Q2=C)uj2rQF)81xN!EGup zzv+iE>P&kkIO6`|hk>2}>PqypmNVE4df7e4R>M`By7MgW1lL*j8HhTzSjoz-a>eBcWXZJ~4|K7rAW%s|9bi^;Jt zl+zZ@`kSGZlU3ZyA8q<|-f*^`BYxqP zYj`(D98o=!#Rea*-1Pem@qPUh(VIz1=a7Xm*ITIasU7T07*qoM6N<$g0?%z1poj5 diff --git a/mobile/android/base/resources/drawable-xlarge-xhdpi-v11/star_blue.png b/mobile/android/base/resources/drawable-xlarge-xhdpi-v11/star_blue.png deleted file mode 100644 index c0278d5748c13bd37a4168b322af42fde6db4b2a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 794 zcmeAS@N?(olHy`uVBq!ia0vp^8X(NU3?z3ec*FxKmUKs7M+SzC{oH>NS%G}U;vjb? zhIQv;UIN*C0X`wFKAD9KGw(A@zsE4`E&~1k&j3U~!RbJ$`%v+DPoNCA`dJS^A~RqL zro*_??=sAUn+7%+qz23YN&y)l5`s|eg^Gjig{TLSFf}vof;ED<^Y1gvcnIRoyvH!> zF<1>)9OMz0dY~-G!21k9#Xxb8lYmk{Hq7c({~4wM-3-wP@4pR9e}b0$KDXPobM3|APai)lf8VFD zDW!Myg?&YrR|RvJOf;AAn{z~e*ZkL&Hw-1hyeE4`pUq=)I(a|Pdx3NvF!)_PT^vI! zPS3rPKDo(2fbD_D#^xKl=e>Os_x`Wn`tSSx|L>RK3UYEX`d-&GH@(bh6N{q1(WW_$ zHiC>Ua^JbC^ei8g@L4M^>prB!s1{@TKyLL@%Ndqw2iB_Ee95#uKXGgkX)d9rZYQkJ`_Ugw0@Tu3}x zdrfne_rqX8#oZn0HK$S{-2T6dP(D11UF^h$hTQ=Zc$$C2ufDX+{b<4@w#gDA836|p z&KrhETlcYkH@LAqZNb*g3r9nff8IO(;J})A`x)iO)qk?Sh!fzj6E{@s%e)7VfKiIcT7S$6uH=`LCYYd*QelF{r5}E)pOmAfX diff --git a/mobile/android/base/resources/drawable-xlarge-xxhdpi-v11/ic_menu_bookmark_add.png b/mobile/android/base/resources/drawable-xlarge-xxhdpi-v11/ic_menu_bookmark_add.png deleted file mode 100644 index 0fa0711374b82191fd94177a29b7c727e1d27194..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1419 zcmV;61$6p}P)Q$RIK&)Jg$Q4_KZ#PZ+nvbw1_YS^sFP1wtC;EQPHxZ6{*B;Kd>so0KaV(YrcCf%$2VyUYg4bMXV zE9h&!y_UUlQpBlyA&(; zZKg!y=1HyQz8E}s@H7*T#XIx~R(`4}$c~7MD~lE*yKmYY^vQFI-J9WazDn4;KlY2` znbYQ=4`T!?KiUk|{YOyzSl?WXNsHNaCpLVzon0H=P$za9W$a0#JTiZtk~02YDA;*? zGBE~YWf}a9Hts=9e40w!_6T{yhj-3Zbz*~)WR55)ZlvHziBB#8-zPbka!nDRnH3LX zE0gWMR>TjCSqMdTlPHSj%mmhv@Uo*U=1x`Q?pKDt@`C!?KZSx>JS$<0#he^C;zEE6 zIDy+(5UhwIcULaqe(PS5!|7cSLKkoZSDa>UtEynbpDN;em9eK6h?c69Fy>$`WI$Fm zbpuxr{w2r6Uy%O68j9SZ$cZE3)%y_5P;EZ~nUD<(U;z`@dG*GL=4sEM`{0S;6bdnK zp_E;)LGs*o=mMRf8+3%OWw{VP(}tYYw+~C{-(-!R@IXi3{vTTdj|?B_S(%F)yb{q3 zI(iIu^u*rr6c6e18uZB44&BP>4ksw@3FFT9kUj(B8jS22o~UsV#-;9q(x59nt94SN zu!Sxjve$V14{iVyxtl!u;e>jVg$|xXEDqv}N#*uGGT!a^l7 zk#bI_$UWi_d`PXy0@IUVJXRt*ea_fsXYwT`QR4Qi65z13kLzl0X_Z5#t#Q1{BR29=Smfq47!(4Jy67d$fcYxKgbsL>|Y(RT_FUXP&ns* zCt2;p?Gg$TvLy>6{Di*(C0_^UYU1n0%)@imI0_8m#P&l_vdC5C%WK!mJCFEe#7-%mqR*EH14F__+8Z)~Uta>% z+AfwBvvtDzz!JWiF~(qQ+Bp^&@?739;OATTH zAsyDy(iwDu)YI^0cvmS=NiJl0|KyhNaUo;jb!nkN>*8{#BL%nwyA`s50W6{ahLD6` z5kkW$%DA%>@!y$=5I-YVQsf5NzyOvY>$Q+q8Gm%iVkjbz8-hA98lLh0$}WETe+Ib< z8bP#ZexmLZO>CvV2jd<36!FJ-<>Qk-6eJ8;N~Rctx|LYn@U$rA9)ash z{(D$l(B-KhqEf2J9RV4c36tG9#G&EPEc*%ntEfiy0~v5qkm)6y@L@GAfTIFZYL_A# zsx~fUA{+surWXPo+O3z88Qp>^NG&QYz)Go$uOF&5jgR3=A9lx&I`x0{M)^LGDfr z>(0r%1hN+f_=LFnWEL{azt1q^A;a{$4FCT#%mku)46FV#OuGw3K*2@N8D`vP*bfoB z4^jz|y$4eRWpO}6^Q^ZpFfa*tx;Tb-9G`nFJzUCBgzbT&K$#=U%C0Wn zH48ery12rEd`$L!U$*RfQj(|7=YOvRA`;5V+ce*se>+$FZO+`8r(IY0MJ-zvH6f*H zH^Z)~r)zQrx^8apd?+x*NU?SO)Q|JdtbX4#EnVnF+8f18~zbEw7f*@N*bf};unT{#1Aaz74tJIy8YTwxsCfy#l@6^-`g(R zNf;*YICN*7+V9(+&961in3%uw$~Q@+;+S7Gd5*n|+TWw}%j*y9yZ3(Ue0_FvM}>p8 z|NWV@)MI*>$b_yEBpW=iK`L>t_DDMuOAX}A3forQu+bYWb%S=7J*yWAh)j3z<3im|J(z^TP)tvm- m3ES`WeAgELa5i!Of5yuAo0a#y1PXvDmci52&t;ucLK6VcQ9RKA diff --git a/mobile/android/base/resources/drawable-xxhdpi-v11/ic_status_logo.png b/mobile/android/base/resources/drawable-xxhdpi-v11/ic_status_logo.png deleted file mode 100644 index 0bb9777d743ee46f2de856e45742a0361f1e9ed6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1343 zcmV-F1;F}=P)Px(`AI}URA}Dqntx1{RT#%#xPT;T8g3-!l1iNpm;JEV+_ITze)G}Ab&{es2CJQI2<6LqGK^cKoCCt0TJ(g&w1}T@4eSQ z%IB}Ud!OffKRfR^&v~A6Uc&f6->S(8%JM?j#CeH8`DZ~(bH&J59q6vGu zMF|_3?r)T-q;d&$yUh_E^B2q%KH@L*ZHPn6^)*ZYuhJp>yM9{vg;I`lf?_VvCG;BP zAhUdjGmQevUvyDO3QGyHc+X@N`}o-s(a8(W5tdH)9L+^H>9ZtwUPa7fkK3pYvzq|! z*T-4G`SH&Q^qNGEvRFfD5?g3B#TWAq`C3o}@&kQLHVngHok{F>mXCY6n8OpyRL~1$ zFSjSC3;=Z+Gm7;L18VA6|+|*FmgPgl?cnP;Z%+NdXoiO^Ku=_$hEAzOwWNy6Wsq~CGT^T zaw<4UCa+IeZ=1E#Y84gaFye-k;YKpj1cMZ87}< zts?uN5+$H#946ckglaF^g_J4*6%yzOD3r?rwUc%sWr}-v9y1&vZ4+n>NOKY)8r9Aa z&0M6B2I|RCO><0h+lodlLFT>-r?y0SjE)GR_lsS_x#a9pVi0 zipDI85GYiM9D}&(2xpM3niJc)(pp7ss~q8!>g{QUVHi|eRIZEMaf;STapwqfwGMXQZCx9yq*+T#=9C^0s(g>;Kn3&%Kac6^I^O`wI2 zfX>rJwxF5n;)2;Rncp;lo^u3r3b-ZIYcV@MX(^rLU3(eLbg;+~P`*^R$LzR7d1PKP z3<7!CVF8jO&-P}=SVf!$FT3C`rONGQ$4!brm0kjUAyxj>?3k(u)J=$2KwnFh3(Srm zD$k+qUIBeC)&11$n67;GGf1o#KtD-!6U~l0IHgf;q?;ccL9FF4<(%X5fBSz6Oca5_ zdU4duhgvM>pjKLJWjk{XgJ7N_lWV5mz9UQs^d5;)y%uY=nC9rX?vC*LZ-hX{Y__Lt ztNZi#&gz6fS8O7zy&01k@&QT>8(GbR0-R`@s19LkE;tv|vjI}&4J7+)BGbhoJH5iG zwL9#?B{{_%O(e5r%hH*RP(z}9bIlr?_+IXlg`HDgT`6o}7inxG)^jOK0JXMyvawa( zVxgB68u>ZdgIbilx4*2>kL~c>ud>!Kc-$sG1N7NEZnkR8o@<;YgVh8W;?^^lZuZdQ zL8aXOsJmTrN|&&VP{CL~Bs^9S+1uVdZ;>_e8Oc0NB(s>u8a|2&1N%Z*;XGG_URE8Aop?J`-Ov4vO2pJ6f04K|rFB3dV zwO25mZoR2b-fwu#wv~}v4_OYDx#Cr2DlJ-_NF|$?;@@{y;#f!&5$YE)_v}Ame*<5s3;0>gv_${_002ovPDHLkV1i** Be{%o; diff --git a/mobile/android/base/resources/drawable-xxhdpi/ab_mic.png b/mobile/android/base/resources/drawable-xxhdpi/ab_mic.png deleted file mode 100644 index 14e4cff784b69833b2512c14930b613186bd6af4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 587 zcmV-R0<`^!P)WFdW0006KNkl*v1ru*v8m~0`JTmS$7 diff --git a/mobile/android/base/resources/drawable-xxhdpi/arrow_up.png b/mobile/android/base/resources/drawable-xxhdpi/arrow_up.png deleted file mode 100644 index 193bbc3b4f476656caabf47626c6e5012b9b002e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 372 zcmV-)0gL{LP)Fg#=p~A?6t{YDZQHhcfMPa{p5G+%|H8#@0m;Yx7{Xjo7YC+K z{K2L6JQG+(D=qmZaIkg1&<2)maO~8l?g1P6Ra_T zgPk?h+Q7047foOpT@jnWGP?0y6FAtN>#Jd!a|ZA|4*tN6N|*HM<{0+O6u!Ig>$H70GJ^=8%)M3f1$BQ`keR>{X zNCQh)L#J`xJOuQmgC(~T4`bf_1-KvM)V#onYAU0>82CQ$kRFz(1h=9IJOltdriUfJ z5Z5E%F^~k;!%|B=El0wU_>{n@hPgN$6_$7e_vw@-y0So3R~Ajs3GVX<{PznR-Kc4z SogkP10000|k3?#4J%UA`Zq5^zET!Hk97cXADdUfN*>)W^A z-o5+o<;!=kUIEej*RO#H$bAoFL(uEjAAks%3sVMC{OT1*7OV@%{ow1l;Q-Kd*^(f? zU!WzW&Kmrq7&n=;+D&51u@I^Y+6>Go6APK!qDUT^vIy7+Wuy_8nzl zU=D1zPp)|UTfTUwRAkrd>y;{^+f(0&T<3ee_ey`|&2OqRC+F^8T|fD1|Ifgsd1fL# ze=j6yN!4eVi1wzJE?e|5a96;^J1VPkR%}|hsFO=gdxmZ4)J><g{^y?j zO^Z%;wP{JON?DMw?s4p=djVq3=^Q^89ZJ6T-G@yGywn+3duYG diff --git a/mobile/android/base/resources/drawable-xxhdpi/close_edit_mode_light.png b/mobile/android/base/resources/drawable-xxhdpi/close_edit_mode_light.png deleted file mode 100644 index 5f9a2f7e5e5bb60c9311b1820a862f62c7ac6368..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 377 zcmeAS@N?(olHy`uVBq!ia0vp^Dj>|k3?#4J%UA`Zq5^zET!HkWMT=IgS~Yj$#wAOZ z02%AoZv>)s8#b@ouxZ@}AY;?|jhohP1ad)wKsE>g86X8f5~ybLx{VM9m<>_`LVD|h zg@LBamIV0)GjQ|p^6_Wpe7{^2Ix)BD-E+uT&A;p87`QI^XNPSNbb&ep8(}Id}i+`pH-O ze+DkiGZX3gdm%|nsy@R+v^TwU*`kkuy8?i*v>^Zh diff --git a/mobile/android/base/resources/drawable-xxhdpi/device_desktop.png b/mobile/android/base/resources/drawable-xxhdpi/device_desktop.png deleted file mode 100644 index 6a386c4e71b8fade36ef587420d96909e0778872..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 298 zcmeAS@N?(olHy`uVBq!ia0vp^5kPFg!2%>NXMBGOq&|AOIEGX(zCE|q@pyoU+sESO zlAHs4B_SmiqK6;6v*Q+hHzi;rv-X3si~G6xCcZy1`FmWQo1?AinG2a)|0LICo&MhJ z{ahr3gRhD|Q-SC0S@VbQn|50_g*r{zu9LH$`<>(#%|k)&{~Y@LfKysc<>!e*PnI9p z*VnwW`dPyn;XdYhwoFC)_Vwx;+8f(_es{2KKhHe#e+%QpBN7@}#c&YLtltX%DN6sf zu~6aEDNs6)s9*IUPTa!0as5g)tGf~BCtekv`RB&2+fAiPmz{Sz=I{0oNDpnX+P_uU d?fG?iM(_R?@N>}1eJ!-f;n_i5eguQ8auG%u2U_Nxrx+3`#6D4r|-SfMB5%=?$&N`N!3DS`;u z-*)eUp=!y(#U~ObA*KYEnU z^+2CrCYq&d9M!-$41JOqq?EE*8kA!0Lt@~@EnJPPBpP<++!D~c#8ID#hy-Ra5g0gf ziiut)22daYr@ETTpOg`aX{UBR>P~2KooJq5%Cu!d1|q^!jxF3p<%!6hAm+_lre;!h zY0;DnPdT)hsXyUwB_b0PX)Yq?mZ6E@_MAm#l9}j|ncLi9lA;6TC;);~nBd-FrY<`Z zJweTH!I6mkz3wC6$Uiy}cO1HBl9|X;NU;mj6Is)v@+o$#XA>QY=ZOqrlnij$0&xm4 z?Js3}3&a*?xNQO1mmNeF3Ka_oa%N)Yl`%~`2uP@#Me{pRH3x9j?o9N~QoKidhcU5b z6G&D4E0NbVGh7MSLd|;RTq(J5EU8S~4# zi+*waKblyMtoeW0FZvaz-_rU;vfp+4b-e$Mf&Zq4{`mv6j4_??rm+?P0000MBZ*SS>Uv`jS`|#W+$aLYNu=Yh83@JNYGp1!4YAUuu|btmmSY^iQ5}xZ+SLXXy^?JkA8{>+yWDz7-;{^mCO*OR3x?>?nQ6&}f*zCcKby|t^Q zTF;SHDEVBtR?Z|Z2T7soJqeL-CVDw?3t8KwhYEQYuVixZ;_8c>uDo60)0Eej*jp+E z>bB91?WsFaC;%e@50o=^sD9fqxN4COfO95b|7qr0BH&;dB(Z=;QzXEPi+zj2%=pGCwLp-SW`xZ)Rmj@A`tbn z>~UYSn@8MP7&dh09WJXsvQ|~F{bljfw3!7d>?Jq578m6(mn>sk)Ef9`pIAoHEoBBT zAJeKG$z@gU4L=&3d*yiS&-60nPO1#>Sn`EKnoF~I8DH}h>nH6FT6ao+sIR@pR(r&F z|B{#+k)MyHnVQ|7wJH3w-DyFM&n#06?uh=d&dW$9}w)skYyF zBi-h%e6N^!_i*gT+bSNt7o6uO2eHliohIUV|Jt>kaeFIccK_(r)R?>O%AK^Jla}H7 z6SO?GZR2|vIzjJ-clHwJ0M^WN7U9aWO9)CuxvO!`OE6Kg6&(}tXLSk>)T}u zmM>HC&iZxdh)|X1teNkA%Fl4WcKktpWB@S1sg}4#l%yn}eU zBjXT5Gb>|rD+5zq15+yl1I}&EOHefA=BH$)RpQoA@%py}Py>UftDnm{r-UW|ebZU` diff --git a/mobile/android/base/resources/drawable-xxhdpi/flat_icon.png b/mobile/android/base/resources/drawable-xxhdpi/flat_icon.png deleted file mode 100644 index 57d83a59e7ca3f5a5467bd6071a22dd9f326b696..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1240 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 diff --git a/mobile/android/base/resources/drawable-xxhdpi/home_group_collapsed.png b/mobile/android/base/resources/drawable-xxhdpi/home_group_collapsed.png deleted file mode 100644 index 319ab3d50d144e0fd62f841402582aa4a10efac0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 293 zcmV+=0owkFP)()mD)efqh`Im2g3GZN^H<9w-T$wMJew-W zdr(n2hl0G^0|dbF0UUUY7r=p!>7qFBKdRCz{zpV`Fcr+Atqnw5_(2L#GO`W^D+iEv rFdFiK6$dU^c+JC$CSG;nvJK1-vA$9+6||`>00000NkvXXu0mjfS;czP diff --git a/mobile/android/base/resources/drawable-xxhdpi/homepage_banner_firstrun.png b/mobile/android/base/resources/drawable-xxhdpi/homepage_banner_firstrun.png deleted file mode 100644 index 3ef8f157d0abb4de134463f8d7543596cb591b03..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 985 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7Ro>U|!(q;uuoF_;xPy1`~IYR{xya zI!E=6`EhN`y?w`N&Y5#_Z)hIfmRr3&U;StDZNF#d^7n2xzPwp>PVm>Y&mY$OzHrsp z_Q&<5Uk@DdxNCRfkFlk_XonqdVYT>qQwg=tn|ZYVt^fGJ=i@B4Lv#MTDLL`ybl4Y# zJu!RC5@uY>p6vAJZ&*RUO+b#Y${7}cy@$GVPfk6$&F5^&zY^^&LXU(Dww|&-(6y#@ zM{|Ip&nxrRANE;X2LD`MnYniwtX#)(asJu^3LXEN%F;y*Y*i|A13Uho>f%0g-NULo zxxt`wHuuc>>s;9^7rz%C)Be1`&EmqU^Q$xV8`x&GFt6R!VK*h-zpXy^4S&?_wbDIj z;&YFF5xtmwABSkg29~TxT)zC8!EDN9aAdwK<3_=971kruH|Vmu zxYnOte|=HIjE=h5@%p}ug+Do#mx?cF*kLi}#>`HpN1rSv9bXRQ-f8h&Kd*yn(W2*p z4kceCxH@`Y^cAuO9LS&W&gzAC&LO4C?<4+5bky}P*)1d0`}e>0t<;B)p9*g&bu^iD zZF8{G7tU!m8g}W^KD4)`RcW7oD_Xm%;xJqH3ss%1I#t;-8fX0wj!bhs5+AX-=I(UY zYc4UXlYZVk%Otzl^~l?xZGS=)e{rAYKK4H2^{*G2+p6ciFxSzwuvaM5<|%t{^pDx= z!<(d>z67hR*X=2MR$^{!^gbfwvCzFEnfhnDE*9PL4!-%Ik@ahDde^!3<7wp)UM-Sa zl@?iV>Y4GG<57H~J+j z*Gsm)+jVVgY2MvpKMR?B-yy&UKd8TXm4hMdTT;LXQU?dX#Qey4t+;_vDaeB}JiH^9o zeX4KQ_%bFd+%3IuJ>?%8mqgnS>5K1qqGO*b2OMyB6%R~GpM2uW!~bg;F1mNMMlC+( zS2pdZZLwVY3-kY#jEn9+v1z>WUi6Vmem!%ljPjOL@8ng$EX&~O>gTe~DWM4flr+i> diff --git a/mobile/android/base/resources/drawable-xxhdpi/ic_action_settings.png b/mobile/android/base/resources/drawable-xxhdpi/ic_action_settings.png deleted file mode 100644 index 9ce42fffe35b6c7eb2d0315bb6c88ad7a6f56aa7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 818 zcmV-21I_%2P)0|x+m0Q&#M$BQFmuP6PLr7=IPB2%u)+BQ@E>qp-92n@+2HOjeQ|n5Dk+sa zIj8>R%lovAM^haNjL#=jv=S=c7)*Yxw&59UV5>zr=u}7ucD0#&Wh7y;+0qb$i4Sgp z5C?H12Slh?3xQrX#El$~LB#>DK!_W`FC>q61wz~?0$uU}bSY{)?JZ&NyaFMv<)d9- z>c)=co%EtuW|(3#De|1I>w|0Arc$X6#N;Ii^RvMReAP+^8VXeAp$Xcc(OJbv=?KEa zHTxUXsW@nD0Ou3-?ujc*Nv&1g6)otUDHlgDE-?q>Qt#x5iDc;9&daFSZ+e%DP zulMAbVHl?knybR(O);7<`7v%l&zRoEa#BqamTR~7> zjt6nx!0@Zs?Pe9|#nL3_K;kU4I?JSHX_~{)nlGtT zsdpusN~ky!w~7^-q5a=H!^#Ue;Uyh=Apm5m=&`tVOin0Prps5>rm5eEN`w7egQhuH;jv*C{Z?9{LH8AinUtBeB zuliflrj*{KcFDQlYn@gGWk;=^lDMgQ>b6w|H}^(Gm(F+-r^Y?A0RrTB7=b{xOp(vp dwEgZHMsvw$8@Fuddk8d{!PC{xWt~$(699XcEU^Fp diff --git a/mobile/android/base/resources/drawable-xxhdpi/ic_media_play.png b/mobile/android/base/resources/drawable-xxhdpi/ic_media_play.png deleted file mode 100644 index 648e6f67ada0c8c1f1ed5747109c5c9be101a6dc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 374 zcmV-+0g3*JP)+dYk8-u^GVvz~j-x%l3jP}OPo%vC56 z6MF!!43a7WeCKD5sFghUd%l<ipdk=(H$Pj#0f5`pF07*qoM6N<$f~BjV2mk;8 diff --git a/mobile/android/base/resources/drawable-xxhdpi/ic_menu_share.png b/mobile/android/base/resources/drawable-xxhdpi/ic_menu_share.png deleted file mode 100644 index de6f092ee398595758825701e8f90e487e168bab..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 832 zcmeAS@N?(olHy`uVBq!ia0vp^PC)Fz!3-oz1uyRbQXc|*LR^6~5I8tE7#pXBhKBkj zB}GR^hlZvFBqT&6CdS33MI|RkBxJAO%2WAT=NcPz09HVC<4UJ9BT|9hz{rm$0gMvddv$At?^9ssZ+uA$3x~DE! zv}EbB6?^v|ICSLHjv*C{ zXKy@iQg#qvdC=Fp_3e#qci;avdH(YA-LlsE|L>|SUM0zNLvQldha0Bt*&_I2&B^;F z=Q$E9AItBF^myM}|B3An*T17Pt5nWfbSx|L(f?^aeaYk}_f4`yBHKhr>I=6BvD0*>6o~)X}{@yGME3f!y-ie`$jKO1Ix^Ip?P4ArhGX ztzcg7DaG2d3t_UB`NyYPn&ueCWFrO^0{xUMZ%BYKzpRsRs&J!mV zj?|kg<_R1So*ww-Zj|Twk2;GjYD+UeWnS@`D>U)zD&@s9zP$Xraz}kcnzH<{Po7uK zZ>SOaTP;1sKS`=E`KQXIf2p5e7OH<*x$$Ssq5rJ^oIgA~qAnT})jefC%bo-GV^kjG zmSrn5=6@3w+NJz_1(S^BfrIQV5(j2a5O@(6%_)3pW1y|l1<$FyIUNSqZol6s^IhQB zS1r-(KTi~T^tXn%{#>gub*roAoQI{pH{LBZIhTCulExjuJ-fObW=+?;aQb>h;l&nx z;Y0r#;xmtwgl}HIP55T{&Y#)#|9&*sH$LDu{823MOXI7c3wzITO>sj9V6tHFboFyt I=akR{0O~J!WdHyG diff --git a/mobile/android/base/resources/drawable-xxhdpi/ic_widget_new_tab.png b/mobile/android/base/resources/drawable-xxhdpi/ic_widget_new_tab.png deleted file mode 100644 index 2ab09b8fb86322392fa2987756587476ce664bda..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 238 zcmV!GrBYmY$O6sqn@h^P>n8IF>3Uv(W6G= z0Ha2a8l5Xz9#)ot50nnREvyt^fc407*qoM6N<$fJCDhOO;{|@*ke7$GGOT1!3*!qD%S3XP~eoaWSp+sc< zr_|`XeEM6%$HG%aKU51oF#4<^`Y5BnsRap)-fV^zQH(B~-9&{hX2BZ6td}~2RK6l8 zv7uQbk|PV00{X~VpD}F65spk#pM8(der=fbV@5w!pS{!BYrDHXczq{nua7Gw)>!EY zLI&-KaN@*lwWblpJ3R|)uLoY!+8IBxH?;NY*?y{Ezo>_%lTyriPg!I~N_T`xPCEe_LnJ)7M7f5}+9QrBNir%~4)#o>~_3ZIUS&10m=}>3{-4Iwhy*kT&^CTn63+F3+qNFDbWL)P3N6Grkhlu4QFdCX5thc;#rC&VziqU00000NkvXXu0mjftV~<0 diff --git a/mobile/android/base/resources/drawable-xxhdpi/icon_key.png b/mobile/android/base/resources/drawable-xxhdpi/icon_key.png deleted file mode 100644 index 0f6925b04e3884c6e6eefbda0c37cde9a58257f6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1789 zcmY+F3pmqj9LI-B#0pz(6>=#Phsf!2qRCyfvsRui%A?$sB$wpW!g8IY2)TqIyZ!gy za7sy~gG9OK&Po@g?VQ+=Tj%|6JWprO_TA_G{=V<;{k_|sXNd$i2Ss@ec@zqz=;UZi z1n-f^Yqbnebanj*3MIXdaDZe7mf%4qlj(Fi(CqAN0O{%JK(n)XK=WXjMxy~JfS{6+ z5&&#AyR;N3i2(+{VzJ8LW#Dk=bRObB5Kst$=yaILWP&gq2Eapr!(lSHOeRc+ASMj6 zSUeCD0~Y{`#bvWW2bjU(uwa-092jP^xeSQM;&9k(m?>5W;sTJAm>eDeCd6YxTqFZ} zazIbOKw%cdWsA1}Uu=R?KmeFaRS5J4OPI%k5CAF(Krjz1fdWTT5<5r+0T6?^Y)G5| zyO1y_@eq?E*|<~%oe>}Gk{r6M$Pxx7Z%HMAWe1o)`I5{}VbD{OT-q)^6mcZ5JO+%w z&*y?+{+~fEE}mKZfkzT4u${RkCvO z3X15p>y(sLFxd5~YU&!A8#Zpz(%!7Ar*E)jt0B(F#MErNxy264oxAqzwc3Zb*>7v- z;OOM+a=_J%aENr+{phh19w$$E`}q0?1O}ZA4!IC|G5k_g^p&gR*!aYxK6JI6;!oR;wO^g1W`93>0zpyAQpPNUaWLQqNcv7U)WM6EI{vHg4 zza!s2^=yCD(v$HzsvwfTSHYKJt zeCTDu%OdmSJ*xK383p0ghnG3`l!&E$IZ$)M*DA-v3r1EQBc3XsHAU$@!*{&*xo6Z= ze7mn=5f*8Gye;=R!JMzTQdEL}>D82rlf}L=3@g^aFlhrB?Lte6N$Q=LvXe@AU*Za? zgU9@&qcK+Y?ge-2>MW`skviY+yIK_Qf4(YqJv#MVTK}LvHG)i;~sAlhVx|4jy5}bUSO}ZK?MhLYg(&bdr zg9nWUX}j^W!)~MN9BcGFHfe`^)4uIWTx=v|Ngwvj9n@BekhY=vf9lq9c+_G1@!jsY zzSiz%ZPOj?<2S4tPwb5+TppjnX{CfklL?CBk@AHH#vjNPR(FOdY;VaYHrOXzjB?TM z&@mI5S594f+LLgsXd6BPJ)Ij<`J7SiCl!>@gLh@EcqQxVJ{hqS?`rPJm^qG{B`I|q z5ZiF86&4(gPcilvjd|2;%rp#5z~y`6?{{ttP?Ivt@EDSTrAR`~Hd@~wv|7QWPS@xT zY)TA?wS{E5hKLRR{Tb2GJHI%lHw||wZn4NL4Oe}4F+cnosnB>T;-2)2%=Z4fauglQ zeDLM?%$Qq81Ovros?)E@#n*o^6a@)HVJ@Yj za(B#E`e0elpO(TSB6|G^50gpE2lV>ukg}{l|H6;VHJcatcHT3%$G6s_Z%emMga#D~ zyp4@>9qZmi&7VAp4*urP_wpZsKJfJAKdvSBXOL?%tu4sVO}U)CY>IW|lO_ghgdvE? zsR>BYJ1XqvtF#PNUau+*_7go^Q_*$FnI~8%WVLV1(YLobE?RjkYsJ};RI}jtPm4Y~ zE3o~qqMPmLn7$@cK2|{`zr~avqM)*A14S<4N0T<+QlPFkFoTVa3;gX&+WChQ|In_d zm@6ooge2gf+l8AQh>V?Ap(Rn8-&Unsj}eq-vua(_4#^4DVxOqt=C_lw^ld1CtK=LG zzWH_AXJG|@Fo(2NL5E^GwV;K@xW&m2pJrR>cFhjDjCSWGV&;@o*@e0T_UHVOe=#RJ KH(R!iXUu<8w9-}p diff --git a/mobile/android/base/resources/drawable-xxhdpi/icon_search_empty_firefox.png b/mobile/android/base/resources/drawable-xxhdpi/icon_search_empty_firefox.png deleted file mode 100644 index b16c1ab79fe1ba85a7cd9268a96f522aa0b44332..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3465 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 YmDv;gmD>4AGW^dE=N;g6)04*g7cjJn$p8QV diff --git a/mobile/android/base/resources/drawable-xxhdpi/img_check.png b/mobile/android/base/resources/drawable-xxhdpi/img_check.png deleted file mode 100644 index 13e92046418a5208de484502963990517f7e303c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1276 zcmVJOc~K0 zS7^egqqS`fFFW702uzUgrS|UtehBgv$zvd| zj5KgrukwR@{LrKPBO$NodSMx#@-^XM;D;bzCE}1=%jcCSfAZylkXLF{T-vAnh;aWP z-&==^`#@gVxhaYNcu^W_yy3J%Tlb+#a3k;x@3-+=^Ti0bBqDjx z=)VO1#GOtas$5YgDzhsg)PA~-05bN z4}TzTROak{e?BheOFs9DnsAGhj@*bOyG04{b~i_LSiT~B$D8~_ZU$+Q^$+8YUe^oT z4T5~m0_eIdLz?i1c=~ZCwA<|o#+%!r!lJ(9^S(`GL=n=)TmFQDu(8Lj)v3oNpPMXS z^5aBG$O27xGk>VJAn_!px#3^%87g1$xrvPGC?G2oi9NlDw=$}=MqypQ@;UIExGpw^ zEZP5_>J*CbP2YvUqYd6S$4x9htSIJ_MTN=mE3fMBHs@VP;j&y|R;oPj}Qmm90oX%g2CXtVPPU%UZ> z4&MT(2~QueX6IubUc)dE5h~6Y_J7S69}Vp_VKLzv9hL~aC;0eG7z{HbM$WAXAMV4N z|304&fIPt2RBgnieKgv6FP9hNMF}3o`+$7P=O&SN%t2n^-2+P!wdM3Ppl)M zM!b@7v-37j7U2&9yR^zPYr@+ys`+p7McTo-1)mepglDE)^WWGdkqbef!=gWlA5$oR z*R9 zCb*MT@!(0*(Iw(=3b72IAR0J|#bN=BKoH7@QLw=CBLWBo3mj1t zg-~>TeI0 zObSA4YHENNgvdnrklY0k0;J3Z!oUU;>vz-08*9; zNP!D@0XnDvRtpjIUcAFI#xE4l&&G|= zvUA9!PoruoyUxFS6>RzCZ^R5Es!lKQ_B;-W%9`Xqh)+`=IxKplm(jjm>rf%xU160{ z!qMMYSId;XU)uN11&6~+yxgfl$qHCEKFs{JAz{|Z<7||vFggYnM$bI^x_#m`Kc91v zdx|^yiO5{%rKUzcp*ZeycGA!G2umv+nvZWz)_r51wAKD+c21kml*NF1Tkpf5#^5yH z#kiXm`$wFr3?2Yjyd34fBN~HCXUmemeKcYfD$FR9=lCi@0SkBOn zC;#VKRNxw4Sk6#G` zXUta(#Ge=RB8;xgFLzE{b+26e`qOeFF){&vwTE4{8|kHl{@OD&P;rYE3D=~AF{PZ0 zA@=b~+LD@I1tVMC@6YEI)GEys^GE?9_kp2P7lJRIOqgty{QYSC8c&sJ$%|DL^k2zk{R{M7!-gi)o(0vDJs~|hF^xe*!n}}}FLu$%dx=We z6=oMJ?T#^XH^HGd5Dkws$Z}mpIr|7F4n({RtT0IS&|nkmQ%C)+vz@P-HwNwAaQcpd zwYphI2a9MOu|m50G$w#L?+|r^K_w&x9q5o&T1Tn1gq!H4`=0p{QL#>pGDy5qTE^?B zcc8=;yH~zdqP%67HANX{KJDkUf4oRpg1u5cdWQ?jzsBi{msNM2Zw@<3Eg1GRoU>Dl z-fOgcon#okkeDy7dX{n2duF+<&OE#trrint@vJ2KesHiO%jMmvp&aQ!je&nO!7l~p MMe}p#xgJaXA5woI4FCWD diff --git a/mobile/android/base/resources/drawable-xxhdpi/lock_disabled.png b/mobile/android/base/resources/drawable-xxhdpi/lock_disabled.png deleted file mode 100644 index 0396ff06e4b50bd85299e2b1453879f460c161db..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1215 zcmeAS@N?(olHy`uVBq!ia0vp^B|u!k!3-ot+SKwG7#K|hd_r7-+}pN>my9(p8EITI z(OkcA^Oh}J)^FMZWUSw`dHqHZW9QDD8#Zm(zkffF1adcS-U?!D-g3!UV*^MEta9__ zty@9jn>U{Ust1~|apUIy|Nn2;xMjzV9U$)Jt(!M*11a9PW!tuG8@Fr$%5K^O6xp(A z6Ucm^b(=SXRBixTy>T;8BT(_CONbZ+%R^x^y1{H zTwysS^Ja_7uJA7Y@baprdGqQ{iJH^Bx&7dmIh5|}GF#0jY&>DtbEC-9 z*E=O$bd?-7KR%KZ@2e1C-g$G|E)nMqc{_c}RpX0`_!wtPJWR{)*SG3A@b~)Koeg3~ zA9Y6APF{Ik=%d7}(|6yW+hQ?G>q+pp-68uLmOZ*VlmBAK(wx3iq9$LrNM88+Ae~l}9-q)`jrj@}3Q@2f9`tTL6X=U`gznOA7zrDV>``p=wDl(#{ zo(1lFU~qn?=j_JKdn%{#pWb4)T+(}H`pO7%wVlh4_KNVI+>)z4%OFTe<+9qGjI$nF zC-~ehT~w-k)!9h*rHirKsg@L}Ig-7zji$_&f}kz!vxTPIw>Q-=$B#rc%-Mk|+D$af z6&-4Q8DC|yxm~}uoW1Ue6Z^lC?{en#{Wg2|rTu$r(@>xO{7T8Uvg7At93_^m7R|E@ zn{hxmP41ny&l`E666a(lX_i_=qmPpo%vKVf%@R73!QE8N-IZ3zHB`+xRn4(FMeYA` zWh24XP7NQIL?tfaz!@EfTtw6s8GwaO_||VxG3LB=f4T8pjX+Vgp4CFaZlS%L$*WGB zkqmTI^E(qAI@7|)KQ!&pjXfcAf4<*Qxk7cO<(K}C|HRiUpP$(+Z&#Pq!v4&!ukZNX aW8%}6-*#P56w?jNPz;`~elF{r5}E)O3Q5}l diff --git a/mobile/android/base/resources/drawable-xxhdpi/lock_inactive.png b/mobile/android/base/resources/drawable-xxhdpi/lock_inactive.png deleted file mode 100644 index 3276f338b2e57860649912a0984b25d742f797a6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 777 zcmeAS@N?(olHy`uVBq!ia0vp^Q9xY5!3-o_g+zIQ)PVq>5LY0*e&go#n>K^U&0B!v z&Ye5gZ``te^X3hkHgDXxdBesn`}gl(ziG>cO`^PN0Dsw}8>+tsA#)1sMWV zwi##?(C{5Vm0%OLYyeulX$#QUO`EoE+_(p%2B>lKR-iJl6i61xW;gbm3iN}0NswPK zL&M>p3;v1~I~=dP&r|SwGnbp>GS3fh{;+R4rgrvwXVn_ne@xF54@EfE*x9Fre>Uot zf7SIv)L;DarP8t+*GnW%#;;{yVBG2H;uuoF`1XoxnT(@A%fpO!iD_wu-f74G`ll|- zT6givsh?{<|J4s~begqxQ}n7YvdiN&cRmdhR)2K;8=vU8ea{|$)sre!TVbyE!7AM> z_@-jt^>+rr%X^hetIu3~{YazncuX8at@HHO9nKk5a+?=?-2CjkW$Mzk-?w}7>Y1=y z5n8Zeox5(FIP2ExD_61?&*0ly8gN2VxS~wJZ`D;#i5qglEgFZ?&g8s%%wKPp+|$dX zC%HMsabuLxGohbSJrVi>ny7M`TFveN%P}A9G2zX!Q8-a^2oN}zl-v`cON~R5LX~wu-_+tzjwht?*cGc zax}0I$N&j?6&&y>*zeV_KNKXj-wP1sh$1aS+z#?AJa3%LlKTOcJ^uEpN+cZUv>Qu^%uWgU?efAzx~ zoo21w6us(;?DBZcolnDr)gN8|#wU7i-?PVG^`uJGR+#I3uu3-zzNy%E{hdMZ@?PcA z>N6K#KhkJC9uvn<>pZ=6hjT`i+~x%zH$OXXnYwiC_wC-idL}GagcfXA=dK$k&bqbw z%9ZTJGx)Za2Aq%-t|$}mTXoe_;)a}Xi^id}Gdb@b^Vi!Y_w+L9Np6mD+!$r_Oz5Xn zPlUd}<_P0OjWPMJE`BlH}g>0R$)~EHk^3?Z%f!Csgr6qIr=IUmid77@}bt3HIjJ63~T${eGoS|^I zrOR%UWk|@xgjv+rR5*zP@~A()_p& zhh=$pFgNgzopr0DpdEBLDyZ diff --git a/mobile/android/base/resources/drawable-xxhdpi/menu.png b/mobile/android/base/resources/drawable-xxhdpi/menu.png deleted file mode 100644 index a6b457fb14918a223e8bc7b8e9080de43deca319..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 217 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 Goc;%~jiZME diff --git a/mobile/android/base/resources/drawable-xxhdpi/overlay_bookmarked_already_icon.png b/mobile/android/base/resources/drawable-xxhdpi/overlay_bookmarked_already_icon.png deleted file mode 100644 index 9afb290c4d306a3f1e7b3923e05aab491c7e51f4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1113 zcmV-f1g86mP)-*b=L$ zC5dCiJ$Ns*WbuP@*Ly-!uB4WxIZE6Dw^M!i{m=^H4ig(;In|FiLfnBjQvHcv0Lyuijze2DqB)Uwm^_c}bp$wYiWQL>wZ?!%L|F{VMV6ovnd+)S!9?NwVO4YH;yo zqSt$9HO!)h5eG+J1?i}@@*ELber7wh>w#_&dSI0lTXF2Q)X zlGoka4_DwNeuh=lgxtWNFdny*lQ{{u8da@4uI>KT^{pmU&x?2wtG3OZ>=+!nxG6i7tX!>5HA|D0nG#v8PHo$@Rth zbj8Lg@;kgRRiVfKQ>){#)WrTMUq}Ats`7X)6`?=(8Wbp+IL!6X_{M8eL5+(h^RFo^Cpn*7tbBxzMw-UHvr2-g7GSynHXe9n_DViM`PD7r%^a zh%cN7y#zx7?#1BSq(fu5x48r*@m0@CY#Vl0cS>ScdNp>Duw*)NMk2A-dV58k51AeN zm0rgKB`Wz|m>+Lr$I>Ps24GQ)p-&K794z)QTf`RoDx0y7r^e)qHe=rwU)M?O!Ev$0UMlwB zchtBvEyNBFSBi7@%xoie$aba1r#V^dfIVZ4eV5n)7f@q#qSyhC$J<#IJH*=+#Cki! z8nN$OqK8B$Q=b~I*hiw{ZA=a2TX=~ZLVZgvlIWPDLR_y;&F&&1Uhhz4!IRdfg?;#) zIatd5?`|>ZeUj*KWRPuTb9_;Q{NDCKTK$+W(ZQEgU(UhVB@Jnrz5Vyh$2E*R5JE^%A3ReHJ`DmM8kBTmf{NBlBw^%nlvYu-o-UXOEfTqT7qlvK&DxU$=LHt zZinK0iAH|2;(x+;CfhFjusW{cQfkrSVu^+~h+3p0z3vvykK9r8X&sy+(YUHSrc(=; z^9ETw()CE;wNtK>M8%x`|6f6VE3r(bBVPDl88B3AV2;L{T*~kGq}fEu2-CsbgipDY fpYujPCv$!UN50Tc+P;Mv00000NkvXXu0mjf{Ax6i diff --git a/mobile/android/base/resources/drawable-xxhdpi/overlay_check.png b/mobile/android/base/resources/drawable-xxhdpi/overlay_check.png deleted file mode 100644 index 1bc3abe8ef52825b5e9b49773af68c089be195f0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2144 zcmYLJ2|U#4A9joljd84Vl6$C@wBwFNu9@E$lr_4<%otXVD0Z222r*jaidn*d{b4By1$y+aLwE2a_AgC!dibYM{_YQbHez%JU4a z^dPhG0c>1M-LZ|04LD}u0RhYbA{-#(Y62XyaGOkU1QN+CJbVNOz&ZQDR>Z*vaNGd` z;%`SV4J6{=^vwZc(J9P_WB3S0ICvT;y9u&z!5j$iDO@P{AqfWmpN^++-2tD%CBOyQ zP+%=dWNA|oo&lM{g2LDbL=r#+qOpN1*x_b<*gVYnb|V@OA6N~l;VEnaECeiZ0288c z0D_~iAu zDv1;>o?7J&DgXpL#HPJK9sz8DG(c#gIwH?-wEzwTgy2%}-9jp6Qh^QlUEmSOA;RqveHB#D zB~dFti;h>h152XLz`dn%;P={e3pz&OdI2(UMFF;1z`005gpdJ^1qHI*!KHqD;jRxp z)A1N@&!5E*5|UEVGFy<}$;tk;zEymA4Yl(43sZfhxvK3Y`fF?BS%vCn56zywWzAMR z_-A3MZ1V2s4>gsG)92L&+h6NH-zDGl=r*Ts?-i@nSfzJHuWIj&rF~z{JGyhUZhq?4 zeQbWuZtZbmUWjwhnF~(Ga{tiqeiJh>Q%LdB{gZto;#1BK5$7lpE&Wpp`OH`HdVLR zbpG2xn#rDxXoi;d>*f( zi^~wkJuh3D>MWM9!EGN*S44%oo%c2lS73U0j~2_(J;46>sdue@rPm>xw7UZCjYV9J_YA0qvP#j71mc&Wf8-MWw_j{k_CP`i|OL zQbktnE=x5dZQ)~b?0ag^^Q+UHIg{1*g-YiZN(>AZPb^X^ifDDqd@F53-SvHwq|mbP z1-qy|maguY4`R%+=7NurJRIPo}kNbMLmtKN$syg6d5Ik z{rlBGc$^`t9e8r#(FC4IF;B+GD}=D61|I7}=}WQ(Gw~@Bb8|(Jak+?5shtHylI91K z${%GS=xoPswoX`~`$SIJ>&(0xz0nJw$A?!HWa7o&Z zF*_4^g>kK<{% zh??4@*+SFAu$fAOFb8DZSVTiztu|zf)JK1}-e;%gcQzx-?%+@9){EIZA;vbXL@K41 zqqDR->T&?lIO`}G-!gWv$zs9oM~~E9ZEB1NY;WAG^e5Lor;GL)wB#??nb;6X4fDTb zE#vjmknxogHTCRVV&+Lz%OlH{1ufU41r5>J?eqSIGoRL#u^rt*T<2AFY)=C15NFmI zJCGlBXf}Lz0lym6PuPZ6T@Fz;zN+G$XWfH*p?oph)qWkJnkH}f0wI49f4Uj#xL$FK_bd596m61h2HAbmPg_PiYUm3q=yM)m&<9PD>KI)SWER zOBmuFljQJRTk#aR&)IytfaaS@rbChOn(6uP^;A*@5;()Ip=bU;v47U`#@tu6OVje+ zVI+Z!9%c4gd1ThKdR#I=%R|$n%#k)_{E(_EdhXm-nXR|*@~<$Cy%jsUpB8#EhP=>I zR!F^bI;><%J+2~CV<|)W9rkWZBOwREIgoCuIpkc&c6XM9G1cGhVi{h({L&@XH>*F- z_tx)>a%NqN%_Q^GmgA=SD%+WS9iwH8+y?qfd8+1{2gMDTN?c!+iC|Jsu!fp2<0D7> SSWm%!uo&d(>B4p*C;kV^GVr1R diff --git a/mobile/android/base/resources/drawable-xxhdpi/private_masq.png b/mobile/android/base/resources/drawable-xxhdpi/private_masq.png deleted file mode 100644 index e62cdbe137ce32dfb52bf157edbf74c7bd10291c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2872 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 Rq=2!(;OXk;vd$@?2>>9HPn`e& diff --git a/mobile/android/base/resources/drawable-xxhdpi/search_clear.png b/mobile/android/base/resources/drawable-xxhdpi/search_clear.png deleted file mode 100644 index f21257bea6ce2caede7a8c55c8de5bbd9181a584..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 370 zcmeAS@N?(olHy`uVBq!ia0vp^Dj>|k3?#4J%UA`ZLIQk3T!HkYNs}f_n9$$f-`CgI zJ9X-;*>h*ko-=FCTp*e|f5AL3f-vSUSU7LN!ubmqEm^i4h?Xv2v2xX#m8*eh?drAb zRrsehg*E^@hF`h7Xe>N>d<3G^#44$rjF6*2UngBzY|u+B-OUg~!FG=T=tNOqnrr*4%k(*KgRi|Ip#n zXU<-@b?^SO=da#;|M~0BUupLFix?OfpL)7DhEy=loyIEm*h0X?J25RS&F|Mg_t`fm zMlUS2toxU!;hERQuXg#xxw7Z4zwVh`DlOS8Fyqhz75O=At?^#LH@o<6do9?oMdZ*X z7Pl-dMxWphODm>1oOAsVu>YiW|KY6KiWAn`t@d@6J~_HE-tWkp?VUycHT10vYd$TL zddhpx;OE8J?_Re&n(_RDeNk~?&793I*1ZmDRkoeI^x=xQJ6}cb>%7mYIPvIYKuUx9 z&F0PNTzt>h`wHo+DGBfK$eZ(j!IV33o&}PV^6Y!X7Yml2RPtzhjhuok7iz=Ah+}Wra2WN3_K=gAEMcMr!yv)O#hsiQhBz#xyNlir?Ros zbxD&Wmz*UeeN$H0TIRp8DC~*e zwN89Sdd%C`r^1)*X}{<7uQ2rHujOmlPDiDFnQ-=`^y6)EpH{T1s0DU-mK|Z_DrG3# ze(3JngIC!bS;RS0TU(7hWm?tvJcC=8YHD+FX>%=M5fod;vaxCTgeLdXtDR11>ptVs ze72=jV@c@+x1|%@TyHNrbUW#bn#|s(%Kr=#zQi%R_47!i%-Ja)rfqc2MPi;(Z`MWj m!!MFEzAT*HcjNZ~!Qc9-4fhXt|K$q6nP)YdW6rj12hO&A&+lF6Y}*{i8ClcYJ=Jx7Uxo%*Um>d+ zV2`^k^~Ke6gwEk}d_m{wH(Y}QL`V3#h?Y1LFVicFxJGlS-B%HzK}#@1uj~WEw9MV) z%S;J4f&BDJF@iW9k~B@l1$anB!UO*_N;FiWqr6JWah=MYtDZO7)IHoxb7`m;LUV8< z-Dz}mFGZ!Z2{e!D8BP#Y5DT0bDyK2@#H71b=29*T(^~m)QgH!T;L4@3Pk^^ z;e^P38Z{6FDyM4^S+mskm0#|oyA%{CD#X9!jUZY=G-!!DDyE9M(sWhI-30a+t)$b! z&n8d-PZ#uZU6gE`DfMjdI&e>#`SOtop=H z`k}fq(TA@ZBv{E_u<{t9E$huj76L2iJnPdOZP{eLSFL5gNp++NtzfO(1}kt4>(d>G z(Ue_bt?aXw4d6>lp&#{*;L;X^QA0dW*@lf*nle*y=zhD)AOe)roz$3op@D zw&K>5x#*}<0p6$IOyByH>asKyXHn3BYSo*vNTRK#Ujm{|^-aMhCDF-G4JOfkZ5@D7 zgY)U0&4c?I`C!6b^h!g=i1YNs@wkzWqnBP`h@RnX+9yVfI`henmzYFD$BBUiu!XOh zwod)giG1HtC+aBhKi1V->x}B}sKY-2zMt+1!1vSr4)DEn0gb0x&B_cI4WN#`PDiDI iQH_7n6Z-w*1il6cZ4>61)I)#(0000cGg*!Nm>1Mw`;aYw-Zoqf6(jWY2Zej1j0k!3B#A&&l1oxU9 z3j_*QHkUI(OY_9IF0-mnRAyt|q~-14eok{AD6^lelJ@y2f3SDdi?osd$y4SQ%)xAI z<_Y|sR`bdj>M4O~p6a=|9K!+?gfJt2FcmmnX-`4_pUiFn&z(+Nd{I5g8*|=!9rA;R87c`Q2QdxH~EPEj(2E2jP3t#R1*L zwutZpPupf7Ln_Y2n;S%ID+3-43xxkxN3m%6=duHJET zTSO9;c!goM@IUOF@jgr_;xGoe1BCZW0UPP!z`gRL!Dyn3%58vegzqQr1@6e0Xp;w- zjWtoXXz4VEFs_qM_#uCwdjj!IlGg#fj;f0L(vcx7$f;-FftbygNgjV`r$47Tf1)K1 z{^i`fzNsT@&JxZ3R()r375$g8!3?GU%ESSz1_`dhezBO6&lwLmV<>*5RiJZ~7x^vE zh>F`8TH&c@z+bqNl8vxdUj}PnBV+m~95k9_R^^$L-=hL&Z~5JDQVaBnG4@?IU-N{M z#t*O!f7*g#I4l>BPbY+n5oe4=wEW^Q&H*>PIC1x-vS@MtOI?xTa965|5QnJvsx8XSMAR5xWGIO% kGL*m#$9?{fyKfWt8axkiMCS|Ni2wiq07*qoM6N<$f~>=x4*&oF diff --git a/mobile/android/base/resources/drawable-xxhdpi/search_launcher.png b/mobile/android/base/resources/drawable-xxhdpi/search_launcher.png deleted file mode 100644 index 6c8fc767833e37e4a4431deb783bf256685cec1d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8383 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 diff --git a/mobile/android/base/resources/drawable-xxhdpi/shield_disabled.png b/mobile/android/base/resources/drawable-xxhdpi/shield_disabled.png deleted file mode 100644 index 57d669d5a6581921958b952d47860e8a99ce4b82..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2926 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 diff --git a/mobile/android/base/resources/drawable-xxhdpi/shield_enabled.png b/mobile/android/base/resources/drawable-xxhdpi/shield_enabled.png deleted file mode 100644 index edf20af1178d65921e7ea29bc086cef5443d64d6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2789 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 OM-lKgxDs=ZJO2Ye^Y9e_ diff --git a/mobile/android/base/resources/drawable-xxhdpi/status_icon_readercache.png b/mobile/android/base/resources/drawable-xxhdpi/status_icon_readercache.png deleted file mode 100644 index f6e070b029261c3b0c46562617f13d1c050e2991..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 712 zcmeAS@N?(olHy`uVBq!ia0vp^azJdr!3-n~J*=()DaPU;cPEB*=VV?2IX?q@LR^6~ z!~g#vH1j{h{QC@xUN9_q%P{jU!@TDp?#u@ab02}YGaoU`y2~)@A;XM&AjWhc`G8>> zki5?@?JmQN2Ou>-!5Q}%rb8I_7^VT)_n{&{T|mV^1wav?f*E(gT%cZ{0d-Z~c zvuYvBDg8pp-sw9yl@4+L<*Lq~Z*)#ZzUSeg##SNOe#`W$GXkm(czb={aB;<|E&bC? zvKLR>w@T7({@uGHet=5Pn*TrwJozJGIlK%pZ@Vwvn|sFrjM_hyfmsosnFBa&t;ucLK6TpLPM+o diff --git a/mobile/android/base/resources/drawable-xxhdpi/suggestedsites_amazon.png b/mobile/android/base/resources/drawable-xxhdpi/suggestedsites_amazon.png deleted file mode 100644 index 5b0d2fdc5f74e72ae9d741c716d370d4e9b594ae..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9225 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 diff --git a/mobile/android/base/resources/drawable-xxhdpi/suggestedsites_facebook.png b/mobile/android/base/resources/drawable-xxhdpi/suggestedsites_facebook.png deleted file mode 100644 index 46e2db588224ad65e0d39af46463eb4c8ab7eefc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1152 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7OGoEa{HEjtmSN`?>!lvI4p0o-U3d z6^w7^T4##{O0+#cB7H=R4AF-dHrSL5&ap985N6xtg zgmm;~HY;fMv^HsMG;W>#=11M;-7aF>&#TX$i9END-7hWd^Su1;mFLd9o3~d*WN+BC zrz~aZ7nfh~7jd<`_FF%9kpPD*&*rXuJ$B+w5t|d6|1$h}eZ%I+y}aFPu9-A$Xyh`U zutB2AxZd@{>^3`#I@QEWe#RDWGoJB3%KTS!wB{Pi4a<04-bPJp*4#%N7i9B<7#A`# z&NcGP6xi@y*>fl;2t!;kejp8rjFSH|Ome?vD^s#!{*hJ+#KFJYPY}8y!C-$CKreOfwydF z|KGR$ym&ZWYe5CWkNFu^x;zsWHYA)+c)#UwOis|fduzw(POWF}6KVYSI^jNT$- z$yFiQ>&MNU`prdW&(W99xexQcw)$3fHv2{RMgPU}F59o3f0aAuM%3?WdGXV+SNB`h zrKl~adc2z1*_`P|`^N7q4$;+{qwmHVhldh*c9aJXVOH?`Ftrz6Sq}2F@&-lpvNj%^3p5B~go^$ed z6@%Dgy+za3{=J?XzM;cB*8Ix zsVEpW9(h{I;^C0QwDQzr#TB*G?@Bh)E1Ye(_Qtv7J16BnKjKfA Xx^z+S&U<#iB89=z)z4*}Q$iB}p{e#y diff --git a/mobile/android/base/resources/drawable-xxhdpi/suggestedsites_twitter.png b/mobile/android/base/resources/drawable-xxhdpi/suggestedsites_twitter.png deleted file mode 100644 index 781b7b5a11f2a6045b83faacf3c1fcd50289d664..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2932 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 diff --git a/mobile/android/base/resources/drawable-xxhdpi/tab_close_active.png b/mobile/android/base/resources/drawable-xxhdpi/tab_close_active.png deleted file mode 100644 index 279135f9391504fb756b41f44470e2fbc56a3f9a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 292 zcmV+<0o(qGP)FsH0v-h<*!DxGA@3Z+Xy{}<-ZjZkqBb)F_4hwtu4H?-S6{4^9 z@EbC+(RW+?0tjfo+25Q12*{Zh3&Adkpf}5ip$#L3j2JRv$Oz-j{$;_EK=9{s5yUlYX_Mz^E zYQ)B4`pr2LR&RKw#{Xi;WJ#I4hTEbA-}f);;(d21Hh$H<(ofPa1HaEbT6ynf^O>Lt z#pi?ec{eucv`_fDOG3Nlo3vgTGziy-~3q3!b>6n=n`b&r*y*gAA-iq;?pCy hsvBK<|Nl)4qh9jE?{~R3X#hiw!PC{xWt~$(698=ck^=w$ diff --git a/mobile/android/base/resources/drawable-xxhdpi/tab_preview_masq.png b/mobile/android/base/resources/drawable-xxhdpi/tab_preview_masq.png deleted file mode 100644 index 9b24f329fda5dfe3079786096bb50c1e8e8e1435..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2075 zcmXX`2~<cHndb`WZ?2|#EQ1YH0 zE_m?H0#AUHB-pQqr7fUP63M=&39jJEf?zg>yS23icCf$|iEz2KwYA_-Utf>J#Tant z>+552cx)aIq(BJ_gIWX+LDg(NAA$h`Qi3pWU^c{MaR8(ZIRj=MkImsh5RU^GU>-}% z%mQ)1z=1^~5y*16TtHG&Qv*sME~3lfR6~dgm(S+|S0II4A&?Tg6+1^rfCt1spyIZ* zwXtD7TU^D0c@WHl;c5T?Du5pV01pe|0z9w^ZBvOG;Q%>4aQLldv3ap`xEh9mB`&Z8 z`T-b32=s_}Kz87b1#@{|ES&8fNg+t25zsGC0c;2@sD}lhePpEKsj?6`ganX7kN_!! zxOH`Ph#U-N#Rr~|pcu35AL)n>$^iiAB1kbnAlNPkexRl%pomO{gLno=L>)nX>vWqF zF$Q#nLcoSh7zTZb*^qI48x#@&3v>a-2!@Yb0S^ycYY`0iv5+kA0}A;dE@lw(02|fS zV$nJT2^KI6EPW#ZU)OVDGWZ00WAWbps9T%ph2@Xy?t4~tG>!4{G5*+TiT_aIj0{qG zrBeT_eIYYKHooQ>6s!>c$I}|=Xq~$%EXQ{zzQ45FMfS(PpZ!`c8L{-+;^a;*!q>D5 zHuHSNtG0tVau*v%CtypDwb!EKADs2>pVF|ux6x6@-IaM@MvX1NX_xt&Yp42|#?a?( z(*~ZLJ)8~gtxs~&FCOYMY$Lg4B{M(ayOb)r56QoJ)31ByzyDr&_)lqGsM)^1lz(^| zkp9>z?TGhuaU_t?Go9nv zkryRgN44Axy7|PR*Bs&ypZL8kaVT!NZF)_AzNzty)H-!prXQZoxuVQ4IktQ*{jr6H z7usDSwLeBJShoC4fkr=O=YDBp_4Y+5YpLpPc@?`cN@j@|qvjl)S@T61V-sFR&A(g# zo5(+0R{keo{$X}@WGQF0Wmpg$A8E@N{2gDq>eyD**r^nlse!|mj9rOTeORVzUTr8B zjOj=*Vqz+um--aYI4udz;6HkF}hh^BlOj+B!c}Av}Za zc;KgYpqG8;6T4hn@7Yy1(OkfA0CE2KlqXJDm&$4#csDhN3e94*4kv{?GYTD$PAJ+- zEw)H+@<=GUS5|CM;iCG~PqlC&qmXv`N4gxnXN@kY<|@ycnGDggo2hO5-=yrgFnCO| zboR!E={3^uQn~gi%|n|8=~kI`HjOSn7oplpWXAc=bUa*VwA`*OZPg_!DqU00fCwfU z4l0ZXcI$@f969@S50jD+U7D(v*}`5V9g~QSHRo|-x;_gj{bkq0|D2h!>$v)S+54H?N>*CqOKbI$1k=f31$)bvo2wn`=!yrF=!{a8 zp11X^N74NY6ra3xKanswB4sYdH>9J92-^?ZjVIevs2UJ6i)+=&?t7slf3y)iL z#z=$AQRxw=^%x~c`_R3-OsSJydnbbS7HU^!{0@OLNRDe^MG6P{RKx=%+CCd$mG z%ip`5oNkzaQz7IQMdzdnVf_U%9qv>JFJ4o3X6m2yiSOCv>>xLc#puX+#j=h_lD{&# zb8#C7TTJplnf0T?WHY-$GO-QUsh0~A&6`J8&JFY|E)0Bmo01WGBEt9whd0W?eujCS z0_C;$05qjZ@>|>ZXt25VMM>_D%X-g$x;icD&FRgE*b}T5?p1<5UT)~?bK-iq&xocf z#a%`;Y@=>XxwLRQilkhk)jm~_)4cg-U@QF9$CuVt@~qQyU#XIJKD4X+ z`qb9^d}Z>-$JB+Mi+#Rla^tLQ#{$c@`fqi{)i#IQ<&zGECz+Dpq9MY{tIi_hS@0K& N@^tlfdE#_F<^Qh2Zbbk9 diff --git a/mobile/android/base/resources/drawable-xxhdpi/tabs_panel_nav_back.png b/mobile/android/base/resources/drawable-xxhdpi/tabs_panel_nav_back.png deleted file mode 100644 index 3b21f3aa2a2d1d9aaf380f3ba182b8fec222edf8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 816 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 diff --git a/mobile/android/base/resources/drawable-xxhdpi/url_bar_entry_default.9.png b/mobile/android/base/resources/drawable-xxhdpi/url_bar_entry_default.9.png deleted file mode 100644 index e7b58136c759cdbc82fd90662e1c4fe51b5d9964..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 406 zcmeAS@N?(olHy`uVBq!ia0vp^DIm=_e4`RS@GohxlrnYx z=^z!cW&-Oh_DhG-T5~SgO^_?oPHF$w$Z%2TL7R+I3x}YRONW3WR>sK|k#8PthMP~D zpT5O^X6Le^-xuw#*DfrW`&aPrzo1D^Ws>VYZ?xCB8Mpq|^4FpN_%?S{haa|Qe<5d6 RVF~m*gQu&X%Q~loCIA=?zuN!+ diff --git a/mobile/android/base/resources/drawable-xxhdpi/url_bar_entry_default_pb.9.png b/mobile/android/base/resources/drawable-xxhdpi/url_bar_entry_default_pb.9.png deleted file mode 100644 index b5b5a8d3281380959a70d7ca50dabf7972ccd25d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 418 zcmeAS@N?(olHy`uVBq!ia0vp^DImDDlnH#WAEJ?(MCUg@+A9SON38lycN*|29r{>p;ee2+hLyYPR{*1GqVPw}6E?3?z)1#d3YDu?n% z8)Y`{JLSRKpK?X`RgZ~5q@u8iWM<+vr?d>?M~NGq#6CzePKrFxxJF&ksf9yO$)!U8 zJEQPf$Ils+%>~zVvh5A#n@+vm|L^%-@fe^ag+y1UNRe8EbN{i#8Pj=p|S@RZEd~LpQx#H=wy?#gc3Cgm1N(Kn2yy}uL z>=SKV6TfIimxZI8%A*2;?m1xt<_U;bWw+RCx)O|#^Gt@oe4yf=@Rn8MC)xNh$Qp diff --git a/mobile/android/base/resources/drawable-xxhdpi/url_bar_entry_pressed_pb.9.png b/mobile/android/base/resources/drawable-xxhdpi/url_bar_entry_pressed_pb.9.png deleted file mode 100644 index 5eb9cecb4191b33d7fbe4d81a37f017886b152d9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 423 zcmeAS@N?(olHy`uVBq!ia0vp^DIm-y}NEv1wDx z&7$tD8bAr&k|4ie2AS+_H$Q)pn*GNw@1O0aC~=^~Z%-G;kP61Px7?>KaS&h$+;VKe zg**QjEd6SwygU8YTc-a~iQ+!{D%iG`N;BMqK?r@MuYPj&s58KTLSdUbc8 z`HU-9^2=j-cS`fEUCiff9zXT8?&G5;f)idYzm^%ipCPi&y{miUwsX4>nB zONW4>QwxV67KX})h8yb{;e{scwTbxp7UUQbGcU}bs1B0ilpUXO@geCwx4Y$<* diff --git a/mobile/android/base/resources/drawable-xxhdpi/urlbar_stop.png b/mobile/android/base/resources/drawable-xxhdpi/urlbar_stop.png deleted file mode 100644 index 510cd7b3c342568cb4c31e4f8c634442cdefb95f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 318 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 diff --git a/mobile/android/base/resources/drawable/action_bar_button.xml b/mobile/android/base/resources/drawable/action_bar_button.xml deleted file mode 100644 index fe36bc43d1..0000000000 --- a/mobile/android/base/resources/drawable/action_bar_button.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/mobile/android/base/resources/drawable/action_bar_button_inverse.xml b/mobile/android/base/resources/drawable/action_bar_button_inverse.xml deleted file mode 100644 index b853873312..0000000000 --- a/mobile/android/base/resources/drawable/action_bar_button_inverse.xml +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/mobile/android/base/resources/drawable/action_bar_button_negative.xml b/mobile/android/base/resources/drawable/action_bar_button_negative.xml deleted file mode 100644 index 7611d70ba5..0000000000 --- a/mobile/android/base/resources/drawable/action_bar_button_negative.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/mobile/android/base/resources/drawable/action_bar_button_positive.xml b/mobile/android/base/resources/drawable/action_bar_button_positive.xml deleted file mode 100644 index ac7020b97c..0000000000 --- a/mobile/android/base/resources/drawable/action_bar_button_positive.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/mobile/android/base/resources/drawable/alert_download_animation.xml b/mobile/android/base/resources/drawable/alert_download_animation.xml deleted file mode 100644 index e50472f0b6..0000000000 --- a/mobile/android/base/resources/drawable/alert_download_animation.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - - - - diff --git a/mobile/android/base/resources/drawable/arrow_down.xml b/mobile/android/base/resources/drawable/arrow_down.xml deleted file mode 100644 index cfb14ed4c6..0000000000 --- a/mobile/android/base/resources/drawable/arrow_down.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - diff --git a/mobile/android/base/resources/drawable/as_bin.xml b/mobile/android/base/resources/drawable/as_bin.xml deleted file mode 100644 index 46de6104ea..0000000000 --- a/mobile/android/base/resources/drawable/as_bin.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/mobile/android/base/resources/drawable/as_bookmark.xml b/mobile/android/base/resources/drawable/as_bookmark.xml deleted file mode 100644 index 890838be3b..0000000000 --- a/mobile/android/base/resources/drawable/as_bookmark.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/mobile/android/base/resources/drawable/as_contextmenu_divider.xml b/mobile/android/base/resources/drawable/as_contextmenu_divider.xml deleted file mode 100644 index f24f9a2382..0000000000 --- a/mobile/android/base/resources/drawable/as_contextmenu_divider.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/mobile/android/base/resources/drawable/as_copy.xml b/mobile/android/base/resources/drawable/as_copy.xml deleted file mode 100644 index 516459edbe..0000000000 --- a/mobile/android/base/resources/drawable/as_copy.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/mobile/android/base/resources/drawable/as_dimiss.xml b/mobile/android/base/resources/drawable/as_dimiss.xml deleted file mode 100644 index ccc028e483..0000000000 --- a/mobile/android/base/resources/drawable/as_dimiss.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/mobile/android/base/resources/drawable/as_home.xml b/mobile/android/base/resources/drawable/as_home.xml deleted file mode 100644 index aece2b1959..0000000000 --- a/mobile/android/base/resources/drawable/as_home.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/mobile/android/base/resources/drawable/as_private.xml b/mobile/android/base/resources/drawable/as_private.xml deleted file mode 100644 index 96c8fbdd23..0000000000 --- a/mobile/android/base/resources/drawable/as_private.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/mobile/android/base/resources/drawable/as_share.xml b/mobile/android/base/resources/drawable/as_share.xml deleted file mode 100644 index ecb0f200b3..0000000000 --- a/mobile/android/base/resources/drawable/as_share.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/mobile/android/base/resources/drawable/as_tab.xml b/mobile/android/base/resources/drawable/as_tab.xml deleted file mode 100644 index b8c4b19e95..0000000000 --- a/mobile/android/base/resources/drawable/as_tab.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/mobile/android/base/resources/drawable/autocomplete_list_bg.xml b/mobile/android/base/resources/drawable/autocomplete_list_bg.xml deleted file mode 100644 index 9747b48a22..0000000000 --- a/mobile/android/base/resources/drawable/autocomplete_list_bg.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/mobile/android/base/resources/drawable/bookmark_folder_arrow_up.xml b/mobile/android/base/resources/drawable/bookmark_folder_arrow_up.xml deleted file mode 100644 index 717cc59526..0000000000 --- a/mobile/android/base/resources/drawable/bookmark_folder_arrow_up.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - diff --git a/mobile/android/base/resources/drawable/button_background_action_blue_round.xml b/mobile/android/base/resources/drawable/button_background_action_blue_round.xml deleted file mode 100644 index 9b5ba80682..0000000000 --- a/mobile/android/base/resources/drawable/button_background_action_blue_round.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - diff --git a/mobile/android/base/resources/drawable/button_background_action_orange_round.xml b/mobile/android/base/resources/drawable/button_background_action_orange_round.xml deleted file mode 100644 index 02a6c6673e..0000000000 --- a/mobile/android/base/resources/drawable/button_background_action_orange_round.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - diff --git a/mobile/android/base/resources/drawable/button_enabled_action_blue_round.xml b/mobile/android/base/resources/drawable/button_enabled_action_blue_round.xml deleted file mode 100644 index 94a6b99352..0000000000 --- a/mobile/android/base/resources/drawable/button_enabled_action_blue_round.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - diff --git a/mobile/android/base/resources/drawable/button_enabled_action_orange_round.xml b/mobile/android/base/resources/drawable/button_enabled_action_orange_round.xml deleted file mode 100644 index 7a9d55b109..0000000000 --- a/mobile/android/base/resources/drawable/button_enabled_action_orange_round.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - diff --git a/mobile/android/base/resources/drawable/button_pressed_action_blue_round.xml b/mobile/android/base/resources/drawable/button_pressed_action_blue_round.xml deleted file mode 100644 index bca19fab6a..0000000000 --- a/mobile/android/base/resources/drawable/button_pressed_action_blue_round.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - diff --git a/mobile/android/base/resources/drawable/button_pressed_action_orange_round.xml b/mobile/android/base/resources/drawable/button_pressed_action_orange_round.xml deleted file mode 100644 index 2720b7fba2..0000000000 --- a/mobile/android/base/resources/drawable/button_pressed_action_orange_round.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - diff --git a/mobile/android/base/resources/drawable/close_edit_mode_selector.xml b/mobile/android/base/resources/drawable/close_edit_mode_selector.xml deleted file mode 100644 index aab2a469c1..0000000000 --- a/mobile/android/base/resources/drawable/close_edit_mode_selector.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - - diff --git a/mobile/android/base/resources/drawable/color_picker_checkmark.xml b/mobile/android/base/resources/drawable/color_picker_checkmark.xml deleted file mode 100644 index 645ec81155..0000000000 --- a/mobile/android/base/resources/drawable/color_picker_checkmark.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - diff --git a/mobile/android/base/resources/drawable/divider_vertical.xml b/mobile/android/base/resources/drawable/divider_vertical.xml deleted file mode 100644 index d326d94f95..0000000000 --- a/mobile/android/base/resources/drawable/divider_vertical.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - diff --git a/mobile/android/base/resources/drawable/edit_text_default.xml b/mobile/android/base/resources/drawable/edit_text_default.xml deleted file mode 100644 index edb6632db3..0000000000 --- a/mobile/android/base/resources/drawable/edit_text_default.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/mobile/android/base/resources/drawable/edit_text_focused.xml b/mobile/android/base/resources/drawable/edit_text_focused.xml deleted file mode 100644 index 38782652e9..0000000000 --- a/mobile/android/base/resources/drawable/edit_text_focused.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/mobile/android/base/resources/drawable/facet_button_background.xml b/mobile/android/base/resources/drawable/facet_button_background.xml deleted file mode 100644 index de1b6e996f..0000000000 --- a/mobile/android/base/resources/drawable/facet_button_background.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - diff --git a/mobile/android/base/resources/drawable/facet_button_background_default.xml b/mobile/android/base/resources/drawable/facet_button_background_default.xml deleted file mode 100644 index b3358d2ede..0000000000 --- a/mobile/android/base/resources/drawable/facet_button_background_default.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - diff --git a/mobile/android/base/resources/drawable/facet_button_background_pressed.xml b/mobile/android/base/resources/drawable/facet_button_background_pressed.xml deleted file mode 100644 index 0a46f60573..0000000000 --- a/mobile/android/base/resources/drawable/facet_button_background_pressed.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - diff --git a/mobile/android/base/resources/drawable/home_banner.xml b/mobile/android/base/resources/drawable/home_banner.xml deleted file mode 100644 index ea536ced0f..0000000000 --- a/mobile/android/base/resources/drawable/home_banner.xml +++ /dev/null @@ -1,38 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/mobile/android/base/resources/drawable/home_history_clear_button_bg.xml b/mobile/android/base/resources/drawable/home_history_clear_button_bg.xml deleted file mode 100644 index f4fee99f4c..0000000000 --- a/mobile/android/base/resources/drawable/home_history_clear_button_bg.xml +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/mobile/android/base/resources/drawable/home_pager_empty_state.xml b/mobile/android/base/resources/drawable/home_pager_empty_state.xml deleted file mode 100644 index 71389ebc63..0000000000 --- a/mobile/android/base/resources/drawable/home_pager_empty_state.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/mobile/android/base/resources/drawable/ic_as_bookmarked.xml b/mobile/android/base/resources/drawable/ic_as_bookmarked.xml deleted file mode 100644 index 0337186417..0000000000 --- a/mobile/android/base/resources/drawable/ic_as_bookmarked.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/mobile/android/base/resources/drawable/ic_as_visited.xml b/mobile/android/base/resources/drawable/ic_as_visited.xml deleted file mode 100644 index 05006ef955..0000000000 --- a/mobile/android/base/resources/drawable/ic_as_visited.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/mobile/android/base/resources/drawable/icon_grid_item_bg.xml b/mobile/android/base/resources/drawable/icon_grid_item_bg.xml deleted file mode 100644 index 45c9cb1f35..0000000000 --- a/mobile/android/base/resources/drawable/icon_grid_item_bg.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - - - - - diff --git a/mobile/android/base/resources/drawable/logo.xml b/mobile/android/base/resources/drawable/logo.xml deleted file mode 100644 index e188f80dce..0000000000 --- a/mobile/android/base/resources/drawable/logo.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - diff --git a/mobile/android/base/resources/drawable/menu_item_action_bar_bg.xml b/mobile/android/base/resources/drawable/menu_item_action_bar_bg.xml deleted file mode 100644 index ad00f49f96..0000000000 --- a/mobile/android/base/resources/drawable/menu_item_action_bar_bg.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/mobile/android/base/resources/drawable/menu_item_state.xml b/mobile/android/base/resources/drawable/menu_item_state.xml deleted file mode 100644 index c7063f4e3c..0000000000 --- a/mobile/android/base/resources/drawable/menu_item_state.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - - - - - - diff --git a/mobile/android/base/resources/drawable/overlay_share_bookmark_button.xml b/mobile/android/base/resources/drawable/overlay_share_bookmark_button.xml deleted file mode 100644 index bc1d51c5de..0000000000 --- a/mobile/android/base/resources/drawable/overlay_share_bookmark_button.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - diff --git a/mobile/android/base/resources/drawable/overlay_share_button_background.xml b/mobile/android/base/resources/drawable/overlay_share_button_background.xml deleted file mode 100644 index 6077ad38af..0000000000 --- a/mobile/android/base/resources/drawable/overlay_share_button_background.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - diff --git a/mobile/android/base/resources/drawable/overlay_share_button_background_first.xml b/mobile/android/base/resources/drawable/overlay_share_button_background_first.xml deleted file mode 100644 index 65ee5de9de..0000000000 --- a/mobile/android/base/resources/drawable/overlay_share_button_background_first.xml +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - diff --git a/mobile/android/base/resources/drawable/panel_auth_button.xml b/mobile/android/base/resources/drawable/panel_auth_button.xml deleted file mode 100644 index 4a9adf428c..0000000000 --- a/mobile/android/base/resources/drawable/panel_auth_button.xml +++ /dev/null @@ -1,38 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/mobile/android/base/resources/drawable/progressbar.xml b/mobile/android/base/resources/drawable/progressbar.xml deleted file mode 100644 index 627484a1f3..0000000000 --- a/mobile/android/base/resources/drawable/progressbar.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/mobile/android/base/resources/drawable/push_notification.png b/mobile/android/base/resources/drawable/push_notification.png deleted file mode 100644 index 2a52dbd505d31f3f85a7fcf6a07a4537a16723e9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 391 zcmeAS@N?(olHy`uVBq!ia0vp^Hb88_!3-p)ZO#q@Qpo{6A+A7r!^SOFuU`HC|Nq9# zn>TFQvSHKa&6{@u1%d4K+qZArxMe+%v2pYI&D+6(K*qN98@C=ib_^&E(zS61NOmJo zFG%~QP209^-MV@6{`DYpwgbiGu1mN9tx+fm@(X6T#KxHa@3zLlDutj6f1KA{44A;V zmPzIBx7SZBEakt!T9z{sM8S#0fviRyLUhTH~q-nmuzK?YyX?i z;+*!@YPZ7it-5bw=k#hH*Xxt9e$%y&`?sRQi|!4_L+5cs$E0=|n9u#dIe&xP>-kIG zr9U{|b^pTSf9!ld48{V9lUJWPTtDwu>TCWt+v`<2@2DLqk_om5dMz>`ZRf|V$Nw}Q z@3Zl)T)wKV - - - - - - - - - - - - - - - - - diff --git a/mobile/android/base/resources/drawable/search_row_background.xml b/mobile/android/base/resources/drawable/search_row_background.xml deleted file mode 100644 index ded70ec6d2..0000000000 --- a/mobile/android/base/resources/drawable/search_row_background.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - diff --git a/mobile/android/base/resources/drawable/search_suggestion_button.xml b/mobile/android/base/resources/drawable/search_suggestion_button.xml deleted file mode 100644 index b91fd4bf0d..0000000000 --- a/mobile/android/base/resources/drawable/search_suggestion_button.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/mobile/android/base/resources/drawable/search_suggestion_prompt_no.xml b/mobile/android/base/resources/drawable/search_suggestion_prompt_no.xml deleted file mode 100644 index b976cfa5ad..0000000000 --- a/mobile/android/base/resources/drawable/search_suggestion_prompt_no.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/mobile/android/base/resources/drawable/search_suggestion_prompt_yes.xml b/mobile/android/base/resources/drawable/search_suggestion_prompt_yes.xml deleted file mode 100644 index 12551497be..0000000000 --- a/mobile/android/base/resources/drawable/search_suggestion_prompt_yes.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/mobile/android/base/resources/drawable/shaped_button.xml b/mobile/android/base/resources/drawable/shaped_button.xml deleted file mode 100644 index 74fc45f850..0000000000 --- a/mobile/android/base/resources/drawable/shaped_button.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - diff --git a/mobile/android/base/resources/drawable/site_security_level.xml b/mobile/android/base/resources/drawable/site_security_level.xml deleted file mode 100644 index 5fee9fffac..0000000000 --- a/mobile/android/base/resources/drawable/site_security_level.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/mobile/android/base/resources/drawable/site_security_unknown.xml b/mobile/android/base/resources/drawable/site_security_unknown.xml deleted file mode 100644 index 86ff863e1d..0000000000 --- a/mobile/android/base/resources/drawable/site_security_unknown.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - diff --git a/mobile/android/base/resources/drawable/tab_history_bg.xml b/mobile/android/base/resources/drawable/tab_history_bg.xml deleted file mode 100644 index 1819ddd581..0000000000 --- a/mobile/android/base/resources/drawable/tab_history_bg.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/mobile/android/base/resources/drawable/tab_history_icon_state.xml b/mobile/android/base/resources/drawable/tab_history_icon_state.xml deleted file mode 100644 index 504dd58041..0000000000 --- a/mobile/android/base/resources/drawable/tab_history_icon_state.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/mobile/android/base/resources/drawable/tab_item_close_button.xml b/mobile/android/base/resources/drawable/tab_item_close_button.xml deleted file mode 100644 index 4012346331..0000000000 --- a/mobile/android/base/resources/drawable/tab_item_close_button.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - - - - diff --git a/mobile/android/base/resources/drawable/tab_panel_tab_background.xml b/mobile/android/base/resources/drawable/tab_panel_tab_background.xml deleted file mode 100644 index c69bfbd81e..0000000000 --- a/mobile/android/base/resources/drawable/tab_panel_tab_background.xml +++ /dev/null @@ -1,38 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/mobile/android/base/resources/drawable/tab_queue_dismiss_button_foreground.xml b/mobile/android/base/resources/drawable/tab_queue_dismiss_button_foreground.xml deleted file mode 100644 index 843ce58706..0000000000 --- a/mobile/android/base/resources/drawable/tab_queue_dismiss_button_foreground.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/mobile/android/base/resources/drawable/tab_row.xml b/mobile/android/base/resources/drawable/tab_row.xml deleted file mode 100644 index cefd990f34..0000000000 --- a/mobile/android/base/resources/drawable/tab_row.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - - - diff --git a/mobile/android/base/resources/drawable/tab_strip_button.xml b/mobile/android/base/resources/drawable/tab_strip_button.xml deleted file mode 100644 index 7daa9d5c41..0000000000 --- a/mobile/android/base/resources/drawable/tab_strip_button.xml +++ /dev/null @@ -1,45 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/mobile/android/base/resources/drawable/tab_strip_divider.xml b/mobile/android/base/resources/drawable/tab_strip_divider.xml deleted file mode 100644 index 4ff4ff31f1..0000000000 --- a/mobile/android/base/resources/drawable/tab_strip_divider.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - - - diff --git a/mobile/android/base/resources/drawable/tab_thumbnail.xml b/mobile/android/base/resources/drawable/tab_thumbnail.xml deleted file mode 100644 index e51de927ef..0000000000 --- a/mobile/android/base/resources/drawable/tab_thumbnail.xml +++ /dev/null @@ -1,87 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/mobile/android/base/resources/drawable/tabs_panel_indicator.xml b/mobile/android/base/resources/drawable/tabs_panel_indicator.xml deleted file mode 100644 index 4c1ab76554..0000000000 --- a/mobile/android/base/resources/drawable/tabs_panel_indicator.xml +++ /dev/null @@ -1,55 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/mobile/android/base/resources/drawable/tabs_panel_indicator_selected.xml b/mobile/android/base/resources/drawable/tabs_panel_indicator_selected.xml deleted file mode 100644 index c74b343e56..0000000000 --- a/mobile/android/base/resources/drawable/tabs_panel_indicator_selected.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - diff --git a/mobile/android/base/resources/drawable/tabs_panel_indicator_selected_private.xml b/mobile/android/base/resources/drawable/tabs_panel_indicator_selected_private.xml deleted file mode 100644 index 93dc049865..0000000000 --- a/mobile/android/base/resources/drawable/tabs_panel_indicator_selected_private.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - diff --git a/mobile/android/base/resources/drawable/tabs_strip_indicator.xml b/mobile/android/base/resources/drawable/tabs_strip_indicator.xml deleted file mode 100644 index 32ca3115a2..0000000000 --- a/mobile/android/base/resources/drawable/tabs_strip_indicator.xml +++ /dev/null @@ -1,48 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - diff --git a/mobile/android/base/resources/drawable/toast_background.xml b/mobile/android/base/resources/drawable/toast_background.xml deleted file mode 100644 index 55cd9d9b21..0000000000 --- a/mobile/android/base/resources/drawable/toast_background.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - diff --git a/mobile/android/base/resources/drawable/toast_button_background.xml b/mobile/android/base/resources/drawable/toast_button_background.xml deleted file mode 100644 index 6570d9b45f..0000000000 --- a/mobile/android/base/resources/drawable/toast_button_background.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/mobile/android/base/resources/drawable/toolbar_favicon_default.xml b/mobile/android/base/resources/drawable/toolbar_favicon_default.xml deleted file mode 100644 index 92e294fb3b..0000000000 --- a/mobile/android/base/resources/drawable/toolbar_favicon_default.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - diff --git a/mobile/android/base/resources/drawable/toolbar_grey_round.xml b/mobile/android/base/resources/drawable/toolbar_grey_round.xml deleted file mode 100644 index ada0146ddf..0000000000 --- a/mobile/android/base/resources/drawable/toolbar_grey_round.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - diff --git a/mobile/android/base/resources/drawable/top_sites_thumbnail_bg.xml b/mobile/android/base/resources/drawable/top_sites_thumbnail_bg.xml deleted file mode 100644 index 9d106441e1..0000000000 --- a/mobile/android/base/resources/drawable/top_sites_thumbnail_bg.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - diff --git a/mobile/android/base/resources/drawable/url_bar_bg.xml b/mobile/android/base/resources/drawable/url_bar_bg.xml deleted file mode 100644 index 52954c1cec..0000000000 --- a/mobile/android/base/resources/drawable/url_bar_bg.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - diff --git a/mobile/android/base/resources/drawable/url_bar_entry.xml b/mobile/android/base/resources/drawable/url_bar_entry.xml deleted file mode 100644 index 4090ceb8e7..0000000000 --- a/mobile/android/base/resources/drawable/url_bar_entry.xml +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/mobile/android/base/resources/drawable/url_bar_nav_button.xml b/mobile/android/base/resources/drawable/url_bar_nav_button.xml deleted file mode 100644 index 2afadaf5e2..0000000000 --- a/mobile/android/base/resources/drawable/url_bar_nav_button.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - diff --git a/mobile/android/base/resources/drawable/url_bar_translating_edge.xml b/mobile/android/base/resources/drawable/url_bar_translating_edge.xml deleted file mode 100644 index 379499284a..0000000000 --- a/mobile/android/base/resources/drawable/url_bar_translating_edge.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - \ No newline at end of file diff --git a/mobile/android/base/resources/drawable/widget_button_left.xml b/mobile/android/base/resources/drawable/widget_button_left.xml deleted file mode 100644 index 9891f8ada9..0000000000 --- a/mobile/android/base/resources/drawable/widget_button_left.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - - diff --git a/mobile/android/base/resources/drawable/widget_button_left_default.xml b/mobile/android/base/resources/drawable/widget_button_left_default.xml deleted file mode 100644 index 7aff6bc1a8..0000000000 --- a/mobile/android/base/resources/drawable/widget_button_left_default.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - diff --git a/mobile/android/base/resources/drawable/widget_button_left_pressed.xml b/mobile/android/base/resources/drawable/widget_button_left_pressed.xml deleted file mode 100644 index d4ae5a715e..0000000000 --- a/mobile/android/base/resources/drawable/widget_button_left_pressed.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - diff --git a/mobile/android/base/resources/drawable/widget_button_middle.xml b/mobile/android/base/resources/drawable/widget_button_middle.xml deleted file mode 100644 index e7d74b0cc1..0000000000 --- a/mobile/android/base/resources/drawable/widget_button_middle.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - diff --git a/mobile/android/base/resources/drawable/widget_button_middle_pressed.xml b/mobile/android/base/resources/drawable/widget_button_middle_pressed.xml deleted file mode 100644 index 19236d641b..0000000000 --- a/mobile/android/base/resources/drawable/widget_button_middle_pressed.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - diff --git a/mobile/android/base/resources/drawable/widget_button_right.xml b/mobile/android/base/resources/drawable/widget_button_right.xml deleted file mode 100644 index 54fae20185..0000000000 --- a/mobile/android/base/resources/drawable/widget_button_right.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - diff --git a/mobile/android/base/resources/drawable/widget_button_right_pressed.xml b/mobile/android/base/resources/drawable/widget_button_right_pressed.xml deleted file mode 100644 index 2ebf614898..0000000000 --- a/mobile/android/base/resources/drawable/widget_button_right_pressed.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - diff --git a/mobile/android/base/resources/layout-large-v11/browser_toolbar.xml b/mobile/android/base/resources/layout-large-v11/browser_toolbar.xml deleted file mode 100644 index 77a968369f..0000000000 --- a/mobile/android/base/resources/layout-large-v11/browser_toolbar.xml +++ /dev/null @@ -1,153 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/mobile/android/base/resources/layout-large-v11/tabs_counter.xml b/mobile/android/base/resources/layout-large-v11/tabs_counter.xml deleted file mode 100644 index df771a2317..0000000000 --- a/mobile/android/base/resources/layout-large-v11/tabs_counter.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - diff --git a/mobile/android/base/resources/layout-xlarge-v11/font_size_preference.xml b/mobile/android/base/resources/layout-xlarge-v11/font_size_preference.xml deleted file mode 100644 index a406b55a2a..0000000000 --- a/mobile/android/base/resources/layout-xlarge-v11/font_size_preference.xml +++ /dev/null @@ -1,52 +0,0 @@ - - - - - - - - - - - - - -