Core.kt 17.7 KB
Newer Older
1
2
3
4
5
6
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

package org.mozilla.fenix.components

7
import GeckoProvider
8
import android.content.Context
9
import android.content.res.Configuration
10
import android.os.Build
11
import android.os.StrictMode
12
import androidx.core.content.ContextCompat
13
import io.sentry.Sentry
14
15
16
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
17
import kotlinx.coroutines.withContext
18
import mozilla.components.browser.engine.gecko.GeckoEngine
19
import mozilla.components.browser.engine.gecko.fetch.GeckoViewFetchClient
20
import mozilla.components.browser.icons.BrowserIcons
21
import mozilla.components.browser.session.Session
22
import mozilla.components.browser.session.SessionManager
23
import mozilla.components.browser.session.engine.EngineMiddleware
24
import mozilla.components.browser.session.storage.SessionStorage
25
import mozilla.components.browser.session.undo.UndoMiddleware
ekager's avatar
ekager committed
26
import mozilla.components.browser.state.action.RecentlyClosedAction
27
import mozilla.components.browser.state.action.RestoreCompleteAction
28
29
import mozilla.components.browser.state.state.BrowserState
import mozilla.components.browser.state.store.BrowserStore
30
import mozilla.components.browser.storage.sync.PlacesBookmarksStorage
31
import mozilla.components.browser.storage.sync.PlacesHistoryStorage
32
import mozilla.components.browser.storage.sync.RemoteTabsStorage
33
import mozilla.components.browser.thumbnails.ThumbnailsMiddleware
34
import mozilla.components.browser.thumbnails.storage.ThumbnailStorage
35
import mozilla.components.concept.base.crash.CrashReporting
36
37
import mozilla.components.concept.engine.DefaultSettings
import mozilla.components.concept.engine.Engine
38
import mozilla.components.concept.engine.mediaquery.PreferredColorScheme
39
import mozilla.components.concept.fetch.Client
40
import mozilla.components.feature.customtabs.store.CustomTabsServiceStore
41
import mozilla.components.feature.downloads.DownloadMiddleware
42
import mozilla.components.feature.logins.exceptions.LoginExceptionStorage
43
import mozilla.components.feature.media.RecordingDevicesNotificationFeature
44
import mozilla.components.feature.media.middleware.MediaMiddleware
Tiger Oakes's avatar
Tiger Oakes committed
45
46
import mozilla.components.feature.pwa.ManifestStorage
import mozilla.components.feature.pwa.WebAppShortcutManager
47
import mozilla.components.feature.readerview.ReaderViewMiddleware
ekager's avatar
ekager committed
48
import mozilla.components.feature.recentlyclosed.RecentlyClosedMiddleware
49
import mozilla.components.feature.session.HistoryDelegate
50
51
import mozilla.components.feature.top.sites.DefaultTopSitesStorage
import mozilla.components.feature.top.sites.PinnedSiteStorage
52
import mozilla.components.feature.webcompat.WebCompatFeature
53
import mozilla.components.feature.webcompat.reporter.WebCompatReporterFeature
54
import mozilla.components.feature.webnotifications.WebNotificationFeature
55
56
import mozilla.components.lib.dataprotect.SecureAbove22Preferences
import mozilla.components.lib.dataprotect.generateEncryptionKey
Tiger Oakes's avatar
Tiger Oakes committed
57
import mozilla.components.service.digitalassetlinks.RelationChecker
58
59
import mozilla.components.service.digitalassetlinks.local.StatementApi
import mozilla.components.service.digitalassetlinks.local.StatementRelationChecker
60
import mozilla.components.service.sync.logins.SyncableLoginsStorage
61
import mozilla.components.support.locale.LocaleManager
62
import org.mozilla.fenix.AppRequestInterceptor
63
import org.mozilla.fenix.Config
64
65
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
66
import org.mozilla.fenix.perf.StrictModeManager
67
import org.mozilla.fenix.TelemetryMiddleware
68
import org.mozilla.fenix.downloads.DownloadService
69
import org.mozilla.fenix.ext.components
70
import org.mozilla.fenix.ext.settings
71
import org.mozilla.fenix.perf.lazyMonitored
72
import org.mozilla.fenix.media.MediaService
73
74
import org.mozilla.fenix.search.telemetry.ads.AdsTelemetry
import org.mozilla.fenix.search.telemetry.incontent.InContentTelemetry
75
76
import org.mozilla.fenix.settings.SupportUtils
import org.mozilla.fenix.settings.advanced.getSelectedLocale
77
import org.mozilla.fenix.utils.Mockable
78
import org.mozilla.fenix.utils.getUndoDelay
79
import java.util.concurrent.TimeUnit
80
81
82
83

/**
 * Component group for all core browser functionality.
 */
84
@Mockable
85
@Suppress("LargeClass")
86
87
88
89
90
class Core(
    private val context: Context,
    private val crashReporter: CrashReporting,
    strictMode: StrictModeManager
) {
91
92
93
94
    /**
     * The browser engine component initialized based on the build
     * configuration (see build variants).
     */
95
    val engine: Engine by lazyMonitored {
96
        val defaultSettings = DefaultSettings(
97
            requestInterceptor = AppRequestInterceptor(context),
98
99
            remoteDebuggingEnabled = context.settings().isRemoteDebuggingEnabled &&
                    Build.VERSION.SDK_INT >= Build.VERSION_CODES.M,
100
            testingModeEnabled = false,
101
            trackingProtectionPolicy = trackingProtectionPolicyFactory.createTrackingProtectionPolicy(),
102
            historyTrackingDelegate = HistoryDelegate(lazyHistoryStorage),
103
            preferredColorScheme = getPreferredColorScheme(),
104
105
            automaticFontSizeAdjustment = context.settings().shouldUseAutoSize,
            fontInflationEnabled = context.settings().shouldUseAutoSize,
106
            suspendMediaWhenInactive = false,
107
            forceUserScalableContent = context.settings().forceEnableZoom,
108
109
110
111
            loginAutofillEnabled = context.settings().shouldAutofillLogins,
            clearColor = ContextCompat.getColor(
                context,
                R.color.foundation_normal_theme
Alex Catarineu's avatar
Alex Catarineu committed
112
113
            ),
            spoofEnglish = context.settings().spoofEnglish
114
        )
115

116
117
118
        GeckoEngine(
            context,
            defaultSettings,
119
120
121
122
123
            GeckoProvider.getOrCreateRuntime(
                context,
                lazyPasswordsStorage,
                trackingProtectionPolicyFactory.createTrackingProtectionPolicy()
            )
124
        ).also {
125
126
127
128
129
130
131

            /**
             * There are some issues around localization to be resolved, as well as questions around
             * the capacity of the WebCompat team, so the "Report site issue" feature should stay
             * disabled in Fenix Release builds for now.
             * This is consistent with both Fennec and Firefox Desktop.
             */
Alex Catarineu's avatar
Alex Catarineu committed
132
133
            if (false && (Config.channel.isNightlyOrDebug || Config.channel.isBeta)) {
                WebCompatFeature.install(it)
134
                WebCompatReporterFeature.install(it, "fenix")
135
            }
Alex Catarineu's avatar
Alex Catarineu committed
136
137

            TorBrowserFeatures.install(context, it)
138
        }
139
140
    }

141
142
143
    /**
     * [Client] implementation to be used for code depending on `concept-fetch``
     */
144
    val client: Client by lazyMonitored {
145
146
        GeckoViewFetchClient(
            context,
147
148
149
150
151
            GeckoProvider.getOrCreateRuntime(
                context,
                lazyPasswordsStorage,
                trackingProtectionPolicyFactory.createTrackingProtectionPolicy()
            )
152
        )
153
154
    }

155
    private val sessionStorage: SessionStorage by lazyMonitored {
156
        SessionStorage(context, engine = engine)
157
158
    }

159
160
161
    /**
     * The [BrowserStore] holds the global [BrowserState].
     */
162
    val store by lazyMonitored {
163
164
        BrowserStore(
            middleware = listOf(
ekager's avatar
ekager committed
165
                RecentlyClosedMiddleware(context, RECENTLY_CLOSED_MAX, engine),
166
                MediaMiddleware(context, MediaService::class.java),
167
                DownloadMiddleware(context, DownloadService::class.java),
168
                ReaderViewMiddleware(),
169
170
171
172
173
                TelemetryMiddleware(
                    context.settings(),
                    adsTelemetry,
                    metrics
                ),
174
175
                ThumbnailsMiddleware(thumbnailStorage),
                UndoMiddleware(::lookupSessionManager, context.getUndoDelay())
176
            ) + EngineMiddleware.create(engine, ::findSessionById)
ekager's avatar
ekager committed
177
178
179
        ).also {
            it.dispatch(RecentlyClosedAction.InitializeRecentlyClosedState)
        }
180
181
    }

182
183
184
185
    private fun lookupSessionManager(): SessionManager {
        return sessionManager
    }

186
187
188
189
    private fun findSessionById(tabId: String): Session? {
        return sessionManager.findSessionById(tabId)
    }

190
191
192
    /**
     * The [CustomTabsServiceStore] holds global custom tabs related data.
     */
193
    val customTabsStore by lazyMonitored { CustomTabsServiceStore() }
194

Tiger Oakes's avatar
Tiger Oakes committed
195
196
197
    /**
     * The [RelationChecker] checks Digital Asset Links relationships for Trusted Web Activities.
     */
198
    val relationChecker: RelationChecker by lazyMonitored {
199
        StatementRelationChecker(StatementApi(client))
Tiger Oakes's avatar
Tiger Oakes committed
200
201
    }

202
203
204
205
206
207
    /**
     * The session manager component provides access to a centralized registry of
     * all browser sessions (i.e. tabs). It is initialized here to persist and restore
     * sessions from the [SessionStorage], and with a default session (about:blank) in
     * case all sessions/tabs are closed.
     */
208
    val sessionManager by lazyMonitored {
209
        SessionManager(engine, store).also { sessionManager ->
210
            // Install the "icons" WebExtension to automatically load icons for every visited website.
211
            icons.install(engine, store)
212

213
            // Install the "ads" WebExtension to get the links in an partner page.
214
215
216
217
            adsTelemetry.install(engine, store)

            // Install the "cookies" WebExtension and tracks user interaction with SERPs.
            searchTelemetry.install(engine, store)
218

219
220
221
222
            // Show an ongoing notification when recording devices (camera, microphone) are used by web content
            RecordingDevicesNotificationFeature(context, sessionManager)
                .enable()

223
            // Restore the previous state.
224
            GlobalScope.launch(Dispatchers.Main) {
225
                withContext(Dispatchers.IO) {
226
                    sessionStorage.restore()
227
                }?.let { snapshot ->
228
229
230
231
                    sessionManager.restore(
                        snapshot,
                        updateSelection = (sessionManager.selectedSession == null)
                    )
232
                }
233
234
235

                // Now that we have restored our previous state (if there's one) let's setup auto saving the state while
                // the app is used.
236
                sessionStorage.autoSave(store)
237
238
239
                    .periodicallyInForeground(interval = 30, unit = TimeUnit.SECONDS)
                    .whenGoingToBackground()
                    .whenSessionsChange()
240
241
242
243

                // Now that we have restored our previous state (if there's one) let's remove timed out tabs
                if (!context.settings().manuallyCloseTabs) {
                    store.state.tabs.filter {
244
245
                        (System.currentTimeMillis() - it.lastAccess) > context.settings()
                            .getTabTimeout()
246
247
248
249
250
251
252
                    }.forEach {
                        val session = sessionManager.findSessionById(it.id)
                        if (session != null) {
                            sessionManager.remove(session)
                        }
                    }
                }
253
254

                store.dispatch(RestoreCompleteAction)
255
            }
256

257
258
            WebNotificationFeature(
                context, engine, icons, R.drawable.ic_status_logo,
259
                permissionStorage.permissionsStorage, HomeActivity::class.java
260
            )
261
        }
262
263
    }

264
265
266
    /**
     * Icons component for loading, caching and processing website icons.
     */
267
    val icons by lazyMonitored {
Tiger Oakes's avatar
Tiger Oakes committed
268
269
270
        BrowserIcons(context, client)
    }

271
    val metrics by lazyMonitored {
272
273
274
        context.components.analytics.metrics
    }

275
    val adsTelemetry by lazyMonitored {
276
        AdsTelemetry(metrics)
277
278
    }

279
    val searchTelemetry by lazyMonitored {
280
        InContentTelemetry(metrics)
281
282
    }

Tiger Oakes's avatar
Tiger Oakes committed
283
284
285
    /**
     * Shortcut component for managing shortcuts on the device home screen.
     */
286
    val webAppShortcutManager by lazyMonitored {
Tiger Oakes's avatar
Tiger Oakes committed
287
288
289
        WebAppShortcutManager(
            context,
            client,
ekager's avatar
ekager committed
290
            webAppManifestStorage
Tiger Oakes's avatar
Tiger Oakes committed
291
        )
292
293
    }

294
295
296
297
298
    // Lazy wrappers around storage components are used to pass references to these components without
    // initializing them until they're accessed.
    // Use these for startup-path code, where we don't want to do any work that's not strictly necessary.
    // For example, this is how the GeckoEngine delegates (history, logins) are configured.
    // We can fully initialize GeckoEngine without initialized our storage.
299
300
301
    val lazyHistoryStorage = lazyMonitored { PlacesHistoryStorage(context, crashReporter) }
    val lazyBookmarksStorage = lazyMonitored { PlacesBookmarksStorage(context) }
    val lazyPasswordsStorage = lazyMonitored { SyncableLoginsStorage(context, passwordsEncryptionKey) }
302

303
304
305
    /**
     * The storage component to sync and persist tabs in a Firefox Sync account.
     */
306
    val lazyRemoteTabsStorage = lazyMonitored { RemoteTabsStorage() }
307

308
    // For most other application code (non-startup), these wrappers are perfectly fine and more ergonomic.
309
310
311
    val historyStorage: PlacesHistoryStorage get() = lazyHistoryStorage.value
    val bookmarksStorage: PlacesBookmarksStorage get() = lazyBookmarksStorage.value
    val passwordsStorage: SyncableLoginsStorage get() = lazyPasswordsStorage.value
312

313
    val tabCollectionStorage by lazyMonitored {
314
315
316
317
318
319
        TabCollectionStorage(
            context,
            sessionManager,
            strictMode
        )
    }
320

321
322
323
    /**
     * A storage component for persisting thumbnail images of tabs.
     */
324
    val thumbnailStorage by lazyMonitored { ThumbnailStorage(context) }
325

326
    val pinnedSiteStorage by lazyMonitored { PinnedSiteStorage(context) }
327

328
    val topSitesStorage by lazyMonitored {
329
330
        val defaultTopSites = mutableListOf<Pair<String, String>>()

331
        strictMode.resetAfter(StrictMode.allowThreadDiskReads()) {
332
            if (!context.settings().defaultTopSitesAdded) {
333
334
335
336
337
338
                if (Config.channel.isMozillaOnline) {
                    defaultTopSites.add(
                        Pair(
                            context.getString(R.string.default_top_site_baidu),
                            SupportUtils.BAIDU_URL
                        )
339
340
                    )

341
342
                    defaultTopSites.add(
                        Pair(
343
344
345
346
347
348
349
350
351
                            context.getString(R.string.default_top_site_jd),
                            SupportUtils.JD_URL
                        )
                    )
                } else {
                    defaultTopSites.add(
                        Pair(
                            context.getString(R.string.default_top_site_google),
                            SupportUtils.GOOGLE_URL
352
353
354
                        )
                    )

355
356
357
358
359
360
361
362
363
364
365
366
367
368
                    if (LocaleManager.getSelectedLocale(context).language == "en") {
                        defaultTopSites.add(
                            Pair(
                                context.getString(R.string.pocket_pinned_top_articles),
                                SupportUtils.POCKET_TRENDING_URL
                            )
                        )
                    }

                    defaultTopSites.add(
                        Pair(
                            context.getString(R.string.default_top_site_wikipedia),
                            SupportUtils.WIKIPEDIA_URL
                        )
369
                    )
370
                }
371

372
373
                context.settings().defaultTopSitesAdded = true
            }
374
375
376
        }

        DefaultTopSitesStorage(
377
            pinnedSiteStorage,
378
379
380
381
            historyStorage,
            defaultTopSites
        )
    }
382

383
    val permissionStorage by lazyMonitored { PermissionStorage(context) }
384

385
    val webAppManifestStorage by lazyMonitored { ManifestStorage(context) }
Tiger Oakes's avatar
Tiger Oakes committed
386

387
    val loginExceptionStorage by lazyMonitored { LoginExceptionStorage(context) }
388

389
390
    /**
     * Shared Preferences that encrypt/decrypt using Android KeyStore and lib-dataprotect for 23+
391
392
     * only on Nightly/Debug for now, otherwise simply stored.
     * See https://github.com/mozilla-mobile/fenix/issues/8324
393
     */
394
    private fun getSecureAbove22Preferences() =
395
396
397
398
399
        SecureAbove22Preferences(
            context = context,
            name = KEY_STORAGE_NAME,
            forceInsecure = !Config.channel.isNightlyOrDebug
        )
400

401
    private val passwordsEncryptionKey by lazyMonitored {
ekager's avatar
ekager committed
402
        getSecureAbove22Preferences().getString(PASSWORDS_KEY)
403
            ?: generateEncryptionKey(KEY_STRENGTH).also {
404
                if (context.settings().passwordsEncryptionKeyGenerated &&
405
406
                    isSentryEnabled()
                ) {
407
408
409
410
                    // We already had previously generated an encryption key, but we have lost it
                    Sentry.capture("Passwords encryption key for passwords storage was lost and we generated a new one")
                }
                context.settings().recordPasswordsEncryptionKeyGenerated()
ekager's avatar
ekager committed
411
                getSecureAbove22Preferences().putString(PASSWORDS_KEY, it)
412
            }
413
    }
414

415
    val trackingProtectionPolicyFactory = TrackingProtectionPolicyFactory(context.settings())
mcarare's avatar
mcarare committed
416

417
418
419
    /**
     * Sets Preferred Color scheme based on Dark/Light Theme Settings or Current Configuration
     */
420
    fun getPreferredColorScheme(): PreferredColorScheme {
421
422
        val inDark =
            (context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) ==
ekager's avatar
ekager committed
423
                    Configuration.UI_MODE_NIGHT_YES
424
        return when {
425
426
            context.settings().shouldUseDarkTheme -> PreferredColorScheme.Dark
            context.settings().shouldUseLightTheme -> PreferredColorScheme.Light
427
428
429
430
            inDark -> PreferredColorScheme.Dark
            else -> PreferredColorScheme.Light
        }
    }
431
432
433
434
435

    companion object {
        private const val KEY_STRENGTH = 256
        private const val KEY_STORAGE_NAME = "core_prefs"
        private const val PASSWORDS_KEY = "passwords"
436
        private const val RECENTLY_CLOSED_MAX = 10
437
    }
438
}