diff options
Diffstat (limited to 'mobile/android/chrome/content/Reader.js')
-rw-r--r-- | mobile/android/chrome/content/Reader.js | 290 |
1 files changed, 290 insertions, 0 deletions
diff --git a/mobile/android/chrome/content/Reader.js b/mobile/android/chrome/content/Reader.js new file mode 100644 index 000000000..d0f3d7801 --- /dev/null +++ b/mobile/android/chrome/content/Reader.js @@ -0,0 +1,290 @@ +// -*- indent-tabs-mode: nil; js-indent-level: 2 -*- +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +XPCOMUtils.defineLazyModuleGetter(this, "Snackbars", "resource://gre/modules/Snackbars.jsm"); + +/*globals MAX_URI_LENGTH, MAX_TITLE_LENGTH */ + +var Reader = { + // These values should match those defined in BrowserContract.java. + STATUS_UNFETCHED: 0, + STATUS_FETCH_FAILED_TEMPORARY: 1, + STATUS_FETCH_FAILED_PERMANENT: 2, + STATUS_FETCH_FAILED_UNSUPPORTED_FORMAT: 3, + STATUS_FETCHED_ARTICLE: 4, + + get _hasUsedToolbar() { + delete this._hasUsedToolbar; + return this._hasUsedToolbar = Services.prefs.getBoolPref("reader.has_used_toolbar"); + }, + + /** + * BackPressListener (listeners / ReaderView Ids). + */ + _backPressListeners: [], + _backPressViewIds: [], + + /** + * Set a backPressListener for this tabId / ReaderView Id pair. + */ + _addBackPressListener: function(tabId, viewId, listener) { + this._backPressListeners[tabId] = listener; + this._backPressViewIds[viewId] = tabId; + }, + + /** + * Remove a backPressListener for this ReaderView Id. + */ + _removeBackPressListener: function(viewId) { + let tabId = this._backPressViewIds[viewId]; + if (tabId != undefined) { + this._backPressListeners[tabId] = null; + delete this._backPressViewIds[viewId]; + } + }, + + /** + * If the requested tab has a backPress listener, return its results, else false. + */ + onBackPress: function(tabId) { + let listener = this._backPressListeners[tabId]; + return { handled: (listener ? listener() : false) }; + }, + + observe: function Reader_observe(aMessage, aTopic, aData) { + switch (aTopic) { + case "Reader:RemoveFromCache": { + ReaderMode.removeArticleFromCache(aData).catch(e => Cu.reportError("Error removing article from cache: " + e)); + break; + } + + case "Reader:AddToCache": { + let tab = BrowserApp.getTabForId(aData); + if (!tab) { + throw new Error("No tab for tabID = " + aData + " when trying to save reader view article"); + } + + // If the article is coming from reader mode, we must have fetched it already. + this._getArticleData(tab.browser).then((article) => { + ReaderMode.storeArticleInCache(article); + }).catch(e => Cu.reportError("Error storing article in cache: " + e)); + break; + } + } + }, + + receiveMessage: function(message) { + switch (message.name) { + case "Reader:ArticleGet": + this._getArticle(message.data.url).then((article) => { + // Make sure the target browser is still alive before trying to send data back. + if (message.target.messageManager) { + message.target.messageManager.sendAsyncMessage("Reader:ArticleData", { article: article }); + } + }, e => { + if (e && e.newURL) { + message.target.loadURI("about:reader?url=" + encodeURIComponent(e.newURL)); + } + }); + break; + + // On DropdownClosed in ReaderView, we cleanup / clear existing BackPressListener. + case "Reader:DropdownClosed": { + this._removeBackPressListener(message.data); + break; + } + + // On DropdownOpened in ReaderView, we add BackPressListener to handle a subsequent BACK request. + case "Reader:DropdownOpened": { + let tabId = BrowserApp.selectedTab.id; + this._addBackPressListener(tabId, message.data, () => { + // User hit BACK key while ReaderView has the banner font-dropdown opened. + // Close it and return prevent-default. + if (message.target.messageManager) { + message.target.messageManager.sendAsyncMessage("Reader:CloseDropdown"); + return true; + } + // We can assume ReaderView banner's font-dropdown doesn't need to be closed. + return false; + }); + + break; + } + + case "Reader:FaviconRequest": { + Messaging.sendRequestForResult({ + type: "Reader:FaviconRequest", + url: message.data.url + }).then(data => { + message.target.messageManager.sendAsyncMessage("Reader:FaviconReturn", JSON.parse(data)); + }); + break; + } + + case "Reader:SystemUIVisibility": + Messaging.sendRequest({ + type: "SystemUI:Visibility", + visible: message.data.visible + }); + break; + + case "Reader:ToolbarHidden": + if (!this._hasUsedToolbar) { + Snackbars.show(Strings.browser.GetStringFromName("readerMode.toolbarTip"), Snackbars.LENGTH_LONG); + Services.prefs.setBoolPref("reader.has_used_toolbar", true); + this._hasUsedToolbar = true; + } + break; + + case "Reader:UpdateReaderButton": { + let tab = BrowserApp.getTabForBrowser(message.target); + tab.browser.isArticle = message.data.isArticle; + this.updatePageAction(tab); + break; + } + } + }, + + pageAction: { + readerModeCallback: function(browser) { + let url = browser.currentURI.spec; + if (url.startsWith("about:reader")) { + UITelemetry.addEvent("action.1", "button", null, "reader_exit"); + } else { + UITelemetry.addEvent("action.1", "button", null, "reader_enter"); + } + browser.messageManager.sendAsyncMessage("Reader:ToggleReaderMode"); + }, + }, + + updatePageAction: function(tab) { + if (!tab.getActive()) { + return; + } + + if (this.pageAction.id) { + PageActions.remove(this.pageAction.id); + delete this.pageAction.id; + } + + let showPageAction = (icon, title) => { + this.pageAction.id = PageActions.add({ + icon: icon, + title: title, + clickCallback: () => this.pageAction.readerModeCallback(browser), + important: true + }); + }; + + let browser = tab.browser; + if (browser.currentURI.spec.startsWith("about:reader")) { + showPageAction("drawable://reader_active", Strings.reader.GetStringFromName("readerView.close")); + // Only start a reader session if the viewer is in the foreground. We do + // not track background reader viewers. + UITelemetry.startSession("reader.1", null); + return; + } + + // Only stop a reader session if the foreground viewer is not visible. + UITelemetry.stopSession("reader.1", "", null); + + if (browser.isArticle) { + showPageAction("drawable://reader", Strings.reader.GetStringFromName("readerView.enter")); + UITelemetry.addEvent("show.1", "button", null, "reader_available"); + } else { + UITelemetry.addEvent("show.1", "button", null, "reader_unavailable"); + } + }, + + /** + * Gets an article for a given URL. This method will download and parse a document + * if it does not find the article in the cache. + * + * @param url The article URL. + * @return {Promise} + * @resolves JS object representing the article, or null if no article is found. + */ + _getArticle: Task.async(function* (url) { + // First try to find a parsed article in the cache. + let article = yield ReaderMode.getArticleFromCache(url); + if (article) { + return article; + } + + // Article hasn't been found in the cache, we need to + // download the page and parse the article out of it. + return yield ReaderMode.downloadAndParseDocument(url).catch(e => { + if (e && e.newURL) { + // Pass up the error so we can navigate the browser in question to the new URL: + throw e; + } + Cu.reportError("Error downloading and parsing document: " + e); + return null; + }); + }), + + _getArticleData: function(browser) { + return new Promise((resolve, reject) => { + if (browser == null) { + reject("_getArticleData needs valid browser"); + } + + let mm = browser.messageManager; + let listener = (message) => { + mm.removeMessageListener("Reader:StoredArticleData", listener); + resolve(message.data.article); + }; + mm.addMessageListener("Reader:StoredArticleData", listener); + mm.sendAsyncMessage("Reader:GetStoredArticleData"); + }); + }, + + + /** + * Migrates old indexedDB reader mode cache to new JSON cache. + */ + migrateCache: Task.async(function* () { + let cacheDB = yield new Promise((resolve, reject) => { + let request = window.indexedDB.open("about:reader", 1); + request.onsuccess = event => resolve(event.target.result); + request.onerror = event => reject(request.error); + + // If there is no DB to migrate, don't do anything. + request.onupgradeneeded = event => resolve(null); + }); + + if (!cacheDB) { + return; + } + + let articles = yield new Promise((resolve, reject) => { + let articles = []; + + let transaction = cacheDB.transaction(cacheDB.objectStoreNames); + let store = transaction.objectStore(cacheDB.objectStoreNames[0]); + + let request = store.openCursor(); + request.onsuccess = event => { + let cursor = event.target.result; + if (!cursor) { + resolve(articles); + } else { + articles.push(cursor.value); + cursor.continue(); + } + }; + request.onerror = event => reject(request.error); + }); + + for (let article of articles) { + yield ReaderMode.storeArticleInCache(article); + } + + // Delete the database. + window.indexedDB.deleteDatabase("about:reader"); + }), +}; |