forked from mozilla-mobile/android-components
-
Notifications
You must be signed in to change notification settings - Fork 0
/
ReaderViewFeature.kt
255 lines (209 loc) · 9.37 KB
/
ReaderViewFeature.kt
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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package mozilla.components.feature.readerview
import android.content.Context
import android.content.SharedPreferences
import android.support.annotation.VisibleForTesting
import mozilla.components.browser.session.SelectionAwareSessionObserver
import mozilla.components.browser.session.Session
import mozilla.components.browser.session.SessionManager
import mozilla.components.concept.engine.Engine
import mozilla.components.concept.engine.EngineSession
import mozilla.components.concept.engine.webextension.MessageHandler
import mozilla.components.concept.engine.webextension.Port
import mozilla.components.concept.engine.webextension.WebExtension
import mozilla.components.feature.readerview.internal.ReaderViewControlsInteractor
import mozilla.components.feature.readerview.internal.ReaderViewControlsPresenter
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.logger.Logger
import org.json.JSONObject
import java.lang.IllegalStateException
import java.util.WeakHashMap
import kotlin.properties.Delegates
typealias OnReaderViewAvailableChange = (available: Boolean) -> Unit
/**
* Feature implementation that provides a reader view for the selected
* session. This feature is implemented as a web extension and
* needs to be installed prior to use (see [ReaderViewFeature.install]).
*
* @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 onReaderViewAvailableChanged a callback invoked to indicate whether
* or not reader view is available for the page loaded by the currently selected
* 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 onReaderViewAvailableChanged: OnReaderViewAvailableChange = { }
) : SelectionAwareSessionObserver(sessionManager), LifecycleAwareFeature, BackHandler {
private val config = Config(context.getSharedPreferences("mozac_feature_reader_view", Context.MODE_PRIVATE))
private val controlsPresenter = ReaderViewControlsPresenter(controlsView, config)
private val controlsInteractor = ReaderViewControlsInteractor(controlsView, config)
class Config(prefs: SharedPreferences) {
enum class FontType { SANS_SERIF, SERIF }
enum class ColorScheme { LIGHT, SEPIA, DARK }
var colorScheme by Delegates.observable(ColorScheme.valueOf(prefs.getString(COLOR_SCHEME_KEY, "LIGHT")!!)) {
_, old, new -> saveAndSendMessage(old, new, COLOR_SCHEME_KEY)
}
@Suppress("MagicNumber")
var fontSize by Delegates.observable(prefs.getInt(FONT_SIZE_KEY, 3)) {
_, old, new -> saveAndSendMessage(old, new, FONT_SIZE_KEY)
}
var fontType by Delegates.observable(FontType.valueOf(prefs.getString(FONT_TYPE_KEY, "SANS_SERIF")!!)) {
_, old, new -> saveAndSendMessage(old, new, FONT_TYPE_KEY)
}
@Suppress("UNUSED_PARAMETER")
private fun saveAndSendMessage(old: Any, new: Any, key: String) {
if (old != new) {
// TODO save shared preference
// TODO send message to reader view web extension
}
}
companion object {
const val COLOR_SCHEME_KEY = "mozac-readerview-colorscheme"
const val FONT_TYPE_KEY = "mozac-readerview-fonttype"
const val FONT_SIZE_KEY = "mozac-readerview-fontsize"
}
}
override fun start() {
observeSelected()
registerContentMessageHandler(activeSession)
if (ReaderViewFeature.installedWebExt == null) {
ReaderViewFeature.install(engine)
}
checkReaderable()
controlsInteractor.start()
}
private fun registerContentMessageHandler(session: Session?) {
if (session == null) {
return
}
val messageHandler = object : MessageHandler {
override fun onPortConnected(port: Port) {
ports[port.engineSession] = port
checkReaderable()
}
override fun onPortDisconnected(port: Port) {
ports.remove(port.engineSession)
}
override fun onPortMessage(message: Any, port: Port) {
if (message is JSONObject) {
activeSession?.readerable = message.getBoolean(READERABLE_RESPONSE_MESSAGE_KEY)
}
}
}
registerMessageHandler(sessionManager.getOrCreateEngineSession(session), messageHandler)
}
override fun stop() {
controlsInteractor.stop()
super.stop()
}
override fun onBackPressed(): Boolean {
// TODO send message to exit reader view (-> see ReaderView.hide())
return true
}
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) {
ports.remove(sessionManager.getEngineSession(session))
}
override fun onUrlChanged(session: Session, url: String) {
checkReaderable()
}
override fun onReaderableStateUpdated(session: Session, readerable: Boolean) {
onReaderViewAvailableChanged(readerable)
}
fun showReaderView() {
activeSession?.let {
sendMessage(JSONObject().apply { put(ACTION_MESSAGE_KEY, ACTION_SHOW) }, it)
}
}
fun hideReaderView() {
activeSession?.let {
sendMessage(JSONObject().apply { put(ACTION_MESSAGE_KEY, ACTION_HIDE) }, it)
}
}
/**
* Show ReaderView appearance controls.
*/
fun showControls() {
controlsPresenter.show()
}
/**
* Hide ReaderView appearance controls.
*/
fun hideControls() {
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
private var registerContentMessageHandler: (WebExtension) -> Unit? = { }
internal var ports = WeakHashMap<EngineSession, Port>()
/**
* Installs the readerview web extension in the provided engine.
*
* @param engine a reference to the application's browser engine.
*/
fun install(engine: Engine) {
engine.installWebExtension(READER_VIEW_EXTENSION_ID, READER_VIEW_EXTENSION_URL,
onSuccess = {
Logger.debug("Installed extension: ${it.id}")
registerContentMessageHandler(it)
installedWebExt = it
},
onError = { ext, throwable ->
Logger.error("Failed to install extension: $ext", throwable)
}
)
}
fun registerMessageHandler(session: EngineSession, messageHandler: MessageHandler) {
registerContentMessageHandler = {
if (!it.hasContentMessageHandler(session, READER_VIEW_EXTENSION_ID)) {
it.registerContentMessageHandler(session, READER_VIEW_EXTENSION_ID, messageHandler)
}
}
installedWebExt?.let { registerContentMessageHandler(it) }
}
}
}