From ba449b4806cfae16fea3a8b55a91be929cd2bd47 Mon Sep 17 00:00:00 2001 From: Christian Sadilek Date: Thu, 25 Apr 2019 18:37:13 -0400 Subject: [PATCH] Closes #1385: Determine and expose reader-ability state --- .../components/browser/session/Session.kt | 8 ++ .../extensions/readerview/readerview.js | 34 ++++++-- .../feature/readerview/ReaderViewFeature.kt | 79 ++++++++++++++----- .../integration/ReaderViewIntegration.kt | 5 +- 4 files changed, 101 insertions(+), 25 deletions(-) diff --git a/components/browser/session/src/main/java/mozilla/components/browser/session/Session.kt b/components/browser/session/src/main/java/mozilla/components/browser/session/Session.kt index 6299d721b62..804e9220698 100644 --- a/components/browser/session/src/main/java/mozilla/components/browser/session/Session.kt +++ b/components/browser/session/src/main/java/mozilla/components/browser/session/Session.kt @@ -72,6 +72,7 @@ class Session( fun onMediaAdded(session: Session, media: List, added: Media) = Unit fun onCrashStateChanged(session: Session, crashed: Boolean) = Unit fun onIconChanged(session: Session, icon: Bitmap?) = Unit + fun onReaderableStateUpdated(session: Session, readerable: Boolean) = Unit } /** @@ -371,6 +372,13 @@ class Session( notifyObservers(old, new) { onCrashStateChanged(this@Session, new) } } + /** + * Readerable state, whether or not the current page can be shown in reader view. + */ + var readerable: Boolean by Delegates.observable(false) { _, _, new -> + notifyObservers{ onReaderableStateUpdated(this@Session, new) } + } + /** * Returns whether or not this session is used for a Custom Tab. */ diff --git a/components/feature/readerview/src/main/assets/extensions/readerview/readerview.js b/components/feature/readerview/src/main/assets/extensions/readerview/readerview.js index 99514ed829b..f645588e84b 100644 --- a/components/feature/readerview/src/main/assets/extensions/readerview/readerview.js +++ b/components/feature/readerview/src/main/assets/extensions/readerview/readerview.js @@ -5,9 +5,24 @@ /* Avoid adding ID selector rules in this style sheet, since they could * inadvertently match elements in the article content. */ +const supportedProtocols = ["http:", "https:"]; +const blockedHosts = ["amazon.com", "github.com", "mail.google.com", "pinterest.com", "reddit.com", "twitter.com", "youtube.com"]; + class ReaderView { static isReaderable() { + if (!supportedProtocols.includes(location.protocol)) { + return false; + } + + if (blockedHosts.some(blockedHost => location.hostname.endsWith(blockedHost))) { + return false; + } + + if (location.pathname == "/") { + return false; + } + return isProbablyReaderable(document); } @@ -261,15 +276,24 @@ class ReaderView { } } -// TODO remove (for testing purposes only) let port = browser.runtime.connectNative("mozacReaderview"); -port.postMessage(`Hello from ReaderView on page ${location.hostname}`); -port.onMessage.addListener((response) => { - console.log(`Received: ${JSON.stringify(response)} on page ${location.hostname}`); +port.onMessage.addListener((message) => { + switch (message.action) { + case 'show': + readerView.show({fontSize: 3, fontType: "serif", colorScheme: "light"}); + break; + case 'hide': + readerView.hide(); + break; + case 'checkReaderable': + port.postMessage({readerable: ReaderView.isReaderable()}); + break; + default: + console.error(`Received invalid action ${message.action}`); + } }); - // TODO remove hostname check (for testing purposes only) // e.g. https://blog.mozilla.org/firefox/reader-view if (ReaderView.isReaderable() && location.hostname.endsWith("blog.mozilla.org")) { diff --git a/components/feature/readerview/src/main/java/mozilla/components/feature/readerview/ReaderViewFeature.kt b/components/feature/readerview/src/main/java/mozilla/components/feature/readerview/ReaderViewFeature.kt index 78ec85c6ae2..998212700f1 100644 --- a/components/feature/readerview/src/main/java/mozilla/components/feature/readerview/ReaderViewFeature.kt +++ b/components/feature/readerview/src/main/java/mozilla/components/feature/readerview/ReaderViewFeature.kt @@ -36,16 +36,18 @@ typealias OnReaderViewAvailableChange = (available: Boolean) -> Unit * @property context a reference to the context. * @property engine a reference to the application's browser engine. * @property sessionManager a reference to the application's [SessionManager]. - * @property onReaderViewAvailableChange a callback invoked to indicate whether + * @property onReaderViewAvailableChanged a callback invoked to indicate whether * or not reader view is available for the page loaded by the currently selected - * (active) session. + * session. The callback will be invoked when a page is loaded or refreshed, + * on any navigation (back or forward) and when the selected session + * changes. */ class ReaderViewFeature( private val context: Context, private val engine: Engine, private val sessionManager: SessionManager, controlsView: ReaderViewControlsView, - private val onReaderViewAvailableChange: OnReaderViewAvailableChange = { } + private val onReaderViewAvailableChanged: OnReaderViewAvailableChange = { } ) : SelectionAwareSessionObserver(sessionManager), LifecycleAwareFeature, BackHandler { private val config = Config(context.getSharedPreferences("mozac_feature_reader_view", Context.MODE_PRIVATE)) @@ -93,6 +95,8 @@ class ReaderViewFeature( ReaderViewFeature.install(engine) } + checkReaderable() + controlsInteractor.start() } @@ -101,26 +105,24 @@ class ReaderViewFeature( return } - // TODO https://github.com/mozilla-mobile/android-components/issues/2624 val messageHandler = object : MessageHandler { override fun onPortConnected(port: Port) { - ReaderViewFeature.ports[port.engineSession] = port + ports[port.engineSession] = port + checkReaderable() } override fun onPortDisconnected(port: Port) { - ReaderViewFeature.ports.remove(port.engineSession) + ports.remove(port.engineSession) } override fun onPortMessage(message: Any, port: Port) { - Logger.info("Received message: $message") - val response = JSONObject().apply { - put("response", "Message received! Hello from Android!") + if (message is JSONObject) { + activeSession?.readerable = message.getBoolean(READERABLE_RESPONSE_MESSAGE_KEY) } - ReaderViewFeature.sendMessage(response, port.engineSession!!) } } - ReaderViewFeature.registerMessageHandler(sessionManager.getOrCreateEngineSession(session), messageHandler) + registerMessageHandler(sessionManager.getOrCreateEngineSession(session), messageHandler) } override fun stop() { @@ -136,19 +138,32 @@ class ReaderViewFeature( override fun onSessionSelected(session: Session) { // TODO restore selected state of whether the controls are open or not registerContentMessageHandler(activeSession) + checkReaderable() super.onSessionSelected(session) } override fun onSessionRemoved(session: Session) { - ReaderViewFeature.ports.remove(sessionManager.getEngineSession(session)) + ports.remove(sessionManager.getEngineSession(session)) + } + + override fun onUrlChanged(session: Session, url: String) { + checkReaderable() + } + + override fun onReaderableStateUpdated(session: Session, readerable: Boolean) { + onReaderViewAvailableChanged(readerable) } fun showReaderView() { - // TODO send message to show reader view (-> see ReaderView.show()) + activeSession?.let { + sendMessage(JSONObject().apply { put(ACTION_MESSAGE_KEY, ACTION_SHOW) }, it) + } } fun hideReaderView() { - // TODO send message to hide reader view (-> see ReaderView.hide()) + activeSession?.let { + sendMessage(JSONObject().apply { put(ACTION_MESSAGE_KEY, ACTION_HIDE) }, it) + } } /** @@ -165,12 +180,43 @@ class ReaderViewFeature( controlsPresenter.hide() } + internal fun checkReaderable() { + activeSession?.let { + if (ports.containsKey(sessionManager.getEngineSession(it))) { + sendMessage(JSONObject().apply { put(ACTION_MESSAGE_KEY, ACTION_CHECK_READERABLE) }, it) + } + } + } + + private fun sendMessage(msg: Any, session: Session) { + val port = ports[sessionManager.getEngineSession(session)] + port?.postMessage(msg) ?: throw IllegalStateException("No port connected for the provided session") + } + companion object { @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) internal const val READER_VIEW_EXTENSION_ID = "mozacReaderview" + + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) internal const val READER_VIEW_EXTENSION_URL = "resource://android/assets/extensions/readerview/" + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + internal const val ACTION_MESSAGE_KEY = "action" + + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + internal const val ACTION_SHOW = "show" + + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + internal const val ACTION_HIDE = "hide" + + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + internal const val ACTION_CHECK_READERABLE = "checkReaderable" + + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + internal const val READERABLE_RESPONSE_MESSAGE_KEY = "readerable" + @Volatile + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) internal var installedWebExt: WebExtension? = null @Volatile @@ -205,10 +251,5 @@ class ReaderViewFeature( installedWebExt?.let { registerContentMessageHandler(it) } } - - fun sendMessage(msg: Any, engineSession: EngineSession) { - val port = ports[engineSession] - port?.postMessage(msg) ?: throw IllegalStateException("No port connected for the provided session") - } } } diff --git a/samples/browser/src/main/java/org/mozilla/samples/browser/integration/ReaderViewIntegration.kt b/samples/browser/src/main/java/org/mozilla/samples/browser/integration/ReaderViewIntegration.kt index f766edef850..56aad8192f6 100644 --- a/samples/browser/src/main/java/org/mozilla/samples/browser/integration/ReaderViewIntegration.kt +++ b/samples/browser/src/main/java/org/mozilla/samples/browser/integration/ReaderViewIntegration.kt @@ -13,6 +13,7 @@ import mozilla.components.feature.readerview.ReaderViewFeature import mozilla.components.feature.readerview.view.ReaderViewControlsView import mozilla.components.support.base.feature.BackHandler import mozilla.components.support.base.feature.LifecycleAwareFeature +import mozilla.components.support.base.log.Log class ReaderViewIntegration( context: Context, @@ -21,7 +22,9 @@ class ReaderViewIntegration( private val view: ReaderViewControlsView ) : LifecycleAwareFeature, BackHandler { - private val feature = ReaderViewFeature(context, engine, sessionManager, view) + private val feature = ReaderViewFeature(context, engine, sessionManager, view) { sessionReaderable -> + Log.log(Log.Priority.ERROR, "Foo", null, "Selected session " + sessionManager.selectedSession?.url + " readerable " + sessionReaderable) + } override fun start() { feature.start()