forked from mozilla-mobile/android-components
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Session.kt
412 lines (363 loc) · 15.3 KB
/
Session.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
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. 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.browser.session
import android.graphics.Bitmap
import mozilla.components.browser.session.engine.EngineSessionHolder
import mozilla.components.browser.session.manifest.WebAppManifest
import mozilla.components.browser.session.tab.CustomTabConfig
import mozilla.components.concept.engine.HitResult
import mozilla.components.concept.engine.media.Media
import mozilla.components.concept.engine.permission.PermissionRequest
import mozilla.components.concept.engine.prompt.PromptRequest
import mozilla.components.concept.engine.window.WindowRequest
import mozilla.components.support.base.observer.Consumable
import mozilla.components.support.base.observer.Observable
import mozilla.components.support.base.observer.ObserverRegistry
import java.util.UUID
import kotlin.properties.Delegates
/**
* Value type that represents the state of a browser session. Changes can be observed.
*/
@Suppress("TooManyFunctions")
class Session(
initialUrl: String,
val private: Boolean = false,
val source: Source = Source.NONE,
val id: String = UUID.randomUUID().toString(),
delegate: Observable<Session.Observer> = ObserverRegistry()
) : Observable<Session.Observer> by delegate {
/**
* Holder for keeping a reference to an engine session and its observer to update this session
* object.
*/
internal val engineSessionHolder = EngineSessionHolder()
/**
* Id of parent session, usually refer to the session which created this one. The clue to indicate if this session
* is terminated, which target we should go back.
*/
internal var parentId: String? = null
/**
* Interface to be implemented by classes that want to observe a session.
*/
interface Observer {
fun onUrlChanged(session: Session, url: String) = Unit
fun onTitleChanged(session: Session, title: String) = Unit
fun onProgress(session: Session, progress: Int) = Unit
fun onLoadingStateChanged(session: Session, loading: Boolean) = Unit
fun onNavigationStateChanged(session: Session, canGoBack: Boolean, canGoForward: Boolean) = Unit
fun onSearch(session: Session, searchTerms: String) = Unit
fun onSecurityChanged(session: Session, securityInfo: SecurityInfo) = Unit
fun onCustomTabConfigChanged(session: Session, customTabConfig: CustomTabConfig?) = Unit
fun onWebAppManifestChanged(session: Session, manifest: WebAppManifest?) = Unit
fun onDownload(session: Session, download: Download): Boolean = false
fun onTrackerBlockingEnabledChanged(session: Session, blockingEnabled: Boolean) = Unit
fun onTrackerBlocked(session: Session, blocked: String, all: List<String>) = Unit
fun onLongPress(session: Session, hitResult: HitResult): Boolean = false
fun onFindResult(session: Session, result: FindResult) = Unit
fun onDesktopModeChanged(session: Session, enabled: Boolean) = Unit
fun onFullScreenChanged(session: Session, enabled: Boolean) = Unit
fun onThumbnailChanged(session: Session, bitmap: Bitmap?) = Unit
fun onContentPermissionRequested(session: Session, permissionRequest: PermissionRequest): Boolean = false
fun onAppPermissionRequested(session: Session, permissionRequest: PermissionRequest): Boolean = false
fun onPromptRequested(session: Session, promptRequest: PromptRequest): Boolean = false
fun onOpenWindowRequested(session: Session, windowRequest: WindowRequest): Boolean = false
fun onCloseWindowRequested(session: Session, windowRequest: WindowRequest): Boolean = false
fun onMediaRemoved(session: Session, media: List<Media>, removed: Media) = Unit
fun onMediaAdded(session: Session, media: List<Media>, added: Media) = Unit
fun onCrashStateChanged(session: Session, crashed: Boolean) = Unit
fun onIconChanged(session: Session, icon: Bitmap?) = Unit
fun onReaderableStateUpdated(session: Session, readerable: Boolean) = Unit
}
/**
* A value type holding security information for a Session.
*
* @property secure true if the session is currently pointed to a URL with
* a valid SSL certificate, otherwise false.
* @property host domain for which the SSL certificate was issued.
* @property issuer name of the certificate authority who issued the SSL certificate.
*/
data class SecurityInfo(val secure: Boolean = false, val host: String = "", val issuer: String = "")
/**
* Represents the origin of a session to describe how and why it was created.
*/
enum class Source {
/**
* Created to handle an ACTION_SEND (share) intent
*/
ACTION_SEND,
/**
* Created to handle an ACTION_VIEW intent
*/
ACTION_VIEW,
/**
* Created to handle a CustomTabs intent
*/
CUSTOM_TAB,
/**
* User interacted with the home screen
*/
HOME_SCREEN,
/**
* User interacted with a menu
*/
MENU,
/**
* User opened a new tab
*/
NEW_TAB,
/**
* Default value and for testing purposes
*/
NONE,
/**
* Default value and for testing purposes
*/
TEXT_SELECTION,
/**
* User entered a URL or search term
*/
USER_ENTERED
}
/**
* A value type representing a result of a "find in page" operation.
*
* @property activeMatchOrdinal the zero-based ordinal of the currently selected match.
* @property numberOfMatches the match count
* @property isDoneCounting true if the find operation has completed, otherwise false.
*/
data class FindResult(val activeMatchOrdinal: Int, val numberOfMatches: Int, val isDoneCounting: Boolean)
/**
* The currently loading or loaded URL.
*/
var url: String by Delegates.observable(initialUrl) {
_, old, new -> notifyObservers(old, new) { onUrlChanged(this@Session, new) }
}
/**
* The title of the currently displayed website changed.
*/
var title: String by Delegates.observable("") {
_, old, new -> notifyObservers(old, new) { onTitleChanged(this@Session, new) }
}
/**
* The progress loading the current URL.
*/
var progress: Int by Delegates.observable(0) {
_, old, new -> notifyObservers(old, new) { onProgress(this@Session, new) }
}
/**
* Loading state, true if this session's url is currently loading, otherwise false.
*/
var loading: Boolean by Delegates.observable(false) {
_, old, new -> notifyObservers(old, new) { onLoadingStateChanged(this@Session, new) }
}
/**
* Navigation state, true if there's an history item to go back to, otherwise false.
*/
var canGoBack: Boolean by Delegates.observable(false) {
_, old, new -> notifyObservers(old, new) { onNavigationStateChanged(this@Session, new, canGoForward) }
}
/**
* Navigation state, true if there's an history item to go forward to, otherwise false.
*/
var canGoForward: Boolean by Delegates.observable(false) {
_, old, new -> notifyObservers(old, new) { onNavigationStateChanged(this@Session, canGoBack, new) }
}
/**
* The currently / last used search terms (or an empty string).
*/
var searchTerms: String by Delegates.observable("") {
_, _, new -> notifyObservers {
onSearch(this@Session, new)
}
}
/**
* Security information indicating whether or not the current session is
* for a secure URL, as well as the host and SSL certificate authority, if applicable.
*/
var securityInfo: SecurityInfo by Delegates.observable(SecurityInfo()) {
_, old, new -> notifyObservers(old, new) { onSecurityChanged(this@Session, new) }
}
/**
* Configuration data in case this session is used for a Custom Tab.
*/
var customTabConfig: CustomTabConfig? by Delegates.observable<CustomTabConfig?>(null) {
_, _, new -> notifyObservers { onCustomTabConfigChanged(this@Session, new) }
}
/**
* The Web App Manifest for the currently visited page (or null).
*/
var webAppManifest: WebAppManifest? by Delegates.observable<WebAppManifest?>(null) {
_, _, new -> notifyObservers { onWebAppManifestChanged(this@Session, new) }
}
/**
* Last download request if it wasn't consumed by at least one observer.
*/
var download: Consumable<Download> by Delegates.vetoable(Consumable.empty()) { _, _, download ->
val consumers = wrapConsumers<Download> { onDownload(this@Session, it) }
!download.consumeBy(consumers)
}
/**
* List of [Media] on the currently visited page.
*/
var media: List<Media> by Delegates.observable(emptyList()) { _, old, new ->
if (old.size > new.size) {
val removed = old - new
require(removed.size == 1) { "Expected only one item to be removed, but was ${removed.size}" }
notifyObservers {
onMediaRemoved(this@Session, new, removed[0])
}
} else if (new.size > old.size) {
val added = new - old
require(added.size == 1) { "Expected only one item to be added, but was ${added.size}" }
notifyObservers {
onMediaAdded(this@Session, new, added[0])
}
}
}
/**
* Tracker blocking state, true if blocking trackers is enabled, otherwise false.
*/
var trackerBlockingEnabled: Boolean by Delegates.observable(false) { _, old, new ->
notifyObservers(old, new) { onTrackerBlockingEnabledChanged(this@Session, new) }
}
/**
* List of URIs that have been blocked in this session.
*/
var trackersBlocked: List<String> by Delegates.observable(emptyList()) { _, old, new ->
notifyObservers(old, new) {
if (new.isNotEmpty()) {
onTrackerBlocked(this@Session, new.last(), new)
}
}
}
/**
* List of results of that latest "find in page" operation.
*/
var findResults: List<FindResult> by Delegates.observable(emptyList()) { _, old, new ->
notifyObservers(old, new) {
if (new.isNotEmpty()) {
onFindResult(this@Session, new.last())
}
}
}
/**
* The target of the latest long click operation.
*/
var hitResult: Consumable<HitResult> by Delegates.vetoable(Consumable.empty()) { _, _, result ->
val consumers = wrapConsumers<HitResult> { onLongPress(this@Session, it) }
!result.consumeBy(consumers)
}
/**
* The target of the latest thumbnail.
*/
var thumbnail: Bitmap? by Delegates.observable<Bitmap?>(null) {
_, _, new -> notifyObservers { onThumbnailChanged(this@Session, new) }
}
/**
* Desktop Mode state, true if the desktop mode is requested, otherwise false.
*/
var desktopMode: Boolean by Delegates.observable(false) { _, old, new ->
notifyObservers(old, new) { onDesktopModeChanged(this@Session, new) }
}
/**
* Exits fullscreen mode if it's in that state.
*/
var fullScreenMode: Boolean by Delegates.observable(false) { _, old, new ->
notifyObservers(old, new) { onFullScreenChanged(this@Session, new) }
}
/**
* An icon for the currently visible page.
*/
var icon: Bitmap? by Delegates.observable<Bitmap?>(null) { _, old, new ->
notifyObservers(old, new) { onIconChanged(this@Session, new) }
}
/**
* [Consumable] permission request from web content. A [PermissionRequest]
* must be consumed i.e. either [PermissionRequest.grant] or
* [PermissionRequest.reject] must be called. A content permission request
* can also be cancelled, which will result in a new empty [Consumable].
*/
var contentPermissionRequest: Consumable<PermissionRequest> by Delegates.vetoable(Consumable.empty()) {
_, _, request ->
val consumers = wrapConsumers<PermissionRequest> { onContentPermissionRequested(this@Session, it) }
!request.consumeBy(consumers)
}
/**
* [Consumable] permission request for the app. A [PermissionRequest]
* must be consumed i.e. either [PermissionRequest.grant] or
* [PermissionRequest.reject] must be called.
*/
var appPermissionRequest: Consumable<PermissionRequest> by Delegates.vetoable(Consumable.empty()) {
_, _, request ->
val consumers = wrapConsumers<PermissionRequest> { onAppPermissionRequested(this@Session, it) }
!request.consumeBy(consumers)
}
/**
* [Consumable] State for a prompt request from web content.
*/
var promptRequest: Consumable<PromptRequest> by Delegates.vetoable(Consumable.empty()) {
_, _, request ->
val consumers = wrapConsumers<PromptRequest> { onPromptRequested(this@Session, it) }
!request.consumeBy(consumers)
}
/**
* [Consumable] request to open/create a window.
*/
var openWindowRequest: Consumable<WindowRequest> by Delegates.vetoable(Consumable.empty()) {
_, _, request ->
val consumers = wrapConsumers<WindowRequest> { onOpenWindowRequested(this@Session, it) }
!request.consumeBy(consumers)
}
/**
* [Consumable] request to close a window.
*/
var closeWindowRequest: Consumable<WindowRequest> by Delegates.vetoable(Consumable.empty()) {
_, _, request ->
val consumers = wrapConsumers<WindowRequest> { onCloseWindowRequested(this@Session, it) }
!request.consumeBy(consumers)
}
/**
* Whether this [Session] has crashed.
*
* In conjunction with a `concept-engine` implementation that uses a multi-process architecture, single sessions
* can crash without crashing the whole app.
*
* A crashed session may still be operational (since the underlying engine implementation has recovered its content
* process), but further action may be needed to restore the last state before the session has crashed (if desired).
*/
var crashed: Boolean by Delegates.observable(false) { _, old, new ->
notifyObservers(old, new) { onCrashStateChanged(this@Session, new) }
}
/**
* Readerable state, whether or not the current page can be shown in a 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.
*/
fun isCustomTabSession() = customTabConfig != null
/**
* Helper method to notify observers only if a value changed.
*/
private fun notifyObservers(old: Any?, new: Any?, block: Observer.() -> Unit) {
if (old != new) {
notifyObservers(block)
}
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Session
if (id != other.id) return false
return true
}
override fun hashCode(): Int {
return id.hashCode()
}
override fun toString(): String {
return "Session($id, $url)"
}
}