FenixApplication.kt 19 KB
Newer Older
Jeff Boek's avatar
Jeff Boek committed
1
/* This Source Code Form is subject to the terms of the Mozilla Public
2
3
 * 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/. */
Jeff Boek's avatar
Jeff Boek committed
4

5
6
package org.mozilla.fenix

7
import android.annotation.SuppressLint
8
9
import android.os.Build
import android.os.Build.VERSION.SDK_INT
10
import android.os.StrictMode
11
import android.util.Log.INFO
12
import androidx.annotation.CallSuper
Emily Kager's avatar
Emily Kager committed
13
import androidx.appcompat.app.AppCompatDelegate
14
import androidx.core.content.getSystemService
15
import androidx.work.Configuration.Builder
16
import androidx.work.Configuration.Provider
17
import kotlinx.coroutines.Deferred
18
19
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
20
21
22
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
23
import mozilla.appservices.Megazord
Gabriel Luong's avatar
Gabriel Luong committed
24
import mozilla.components.browser.session.Session
25
import mozilla.components.browser.state.action.SystemAction
26
import mozilla.components.concept.push.PushProcessor
27
import mozilla.components.feature.addons.update.GlobalAddonDependencyProvider
28
import mozilla.components.lib.crash.CrashReporter
29
import mozilla.components.service.experiments.Experiments
30
31
32
import mozilla.components.service.glean.Glean
import mozilla.components.service.glean.config.Configuration
import mozilla.components.service.glean.net.ConceptFetchHttpUploader
33
34
import mozilla.components.support.base.log.Log
import mozilla.components.support.base.log.logger.Logger
35
36
import mozilla.components.support.ktx.android.content.isMainProcess
import mozilla.components.support.ktx.android.content.runOnlyInMainProcess
37
import mozilla.components.support.locale.LocaleAwareApplication
38
import mozilla.components.support.rusthttp.RustHttpConfig
39
import mozilla.components.support.rustlog.RustLog
40
import mozilla.components.support.utils.logElapsedTime
Gabriel Luong's avatar
Gabriel Luong committed
41
import mozilla.components.support.webextensions.WebExtensionSupport
42
import org.mozilla.fenix.StrictModeManager.enableStrictMode
43
import org.mozilla.fenix.components.Components
44
import org.mozilla.fenix.components.metrics.MetricServiceType
45
import org.mozilla.fenix.ext.resetPoliciesAfter
46
import org.mozilla.fenix.ext.settings
47
import org.mozilla.fenix.perf.StorageStatsMetrics
48
import org.mozilla.fenix.perf.StartupTimeline
49
import org.mozilla.fenix.push.PushFxaIntegration
50
import org.mozilla.fenix.push.WebPushEngineIntegration
51
import org.mozilla.fenix.session.PerformanceActivityLifecycleCallbacks
52
import org.mozilla.fenix.session.VisibilityLifecycleCallback
53
import org.mozilla.fenix.utils.BrowsersCache
54
import org.torproject.android.service.util.Prefs
55

56
57
58
59
/**
 *The main application class for Fenix. Records data to measure initialization performance.
 *  Installs [CrashReporter], initializes [Glean]  in fenix builds and setup Megazord in the main process.
 */
60
@Suppress("Registered", "TooManyFunctions", "LargeClass")
61
open class FenixApplication : LocaleAwareApplication(), Provider {
62
63
64
65
    init {
        recordOnInit() // DO NOT MOVE ANYTHING ABOVE HERE: the timing of this measurement is critical.
    }

66
    private val logger = Logger("FenixApplication")
67

68
69
    var terminating = false

70
    open val components by lazy { Components(this) }
71

72
73
74
    var visibilityLifecycleCallback: VisibilityLifecycleCallback? = null
        private set

75
76
    override fun onCreate() {
        super.onCreate()
77

78
        setupInAllProcesses()
79

80
        if (!isMainProcess()) {
81
82
83
            // If this is not the main process then do not continue with the initialization here. Everything that
            // follows only needs to be done in our app's main process and should not be done in other processes like
            // a GeckoView child process or the crash handling process. Most importantly we never want to end up in a
84
            // situation where we create a GeckoRuntime from the Gecko child process.
85
            return
86
        }
87

88
89
90
91
92
93
        if (Config.channel.isFenix) {
            // We need to always initialize Glean and do it early here.
            // Note that we are only initializing Glean here for "fenix" builds. "fennec" builds
            // will initialize in MigratingFenixApplication because we first need to migrate the
            // user's choice from Fennec.
            initializeGlean()
94
        }
95
96
97
98

        setupInMainProcessOnly()
    }

99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
    fun isTerminating() = terminating

    fun terminate() {
        onTerminate()
        System.exit(0)
    }

    override fun onTerminate() {
        terminating = true

        super.onTerminate()
        components.torController.stop()
        components.torController.stopTor()
    }

114
    protected open fun initializeGlean() {
115
116
117
118
        val telemetryEnabled = settings().isTelemetryEnabled

        logger.debug("Initializing Glean (uploadEnabled=$telemetryEnabled, isFennec=${Config.channel.isFennec})")

119
120
121
122
123
124
125
        Glean.initialize(
            applicationContext = this,
            configuration = Configuration(
                channel = BuildConfig.BUILD_TYPE,
                httpClient = ConceptFetchHttpUploader(
                    lazy(LazyThreadSafetyMode.NONE) { components.core.client }
                )),
126
            uploadEnabled = telemetryEnabled
127
        )
128
129
130
131
132
133
134
    }

    @CallSuper
    open fun setupInAllProcesses() {
        setupCrashReporting()

        // We want the log messages of all builds to go to Android logcat
135
        Log.addSink(FenixLogSink(logsDebug = Config.channel.isDebug))
136
137
138
139
    }

    @CallSuper
    open fun setupInMainProcessOnly() {
140
141
142
143
144
        run {
            // Attention: Do not invoke any code from a-s in this scope.
            val megazordSetup = setupMegazord()

            setDayNightTheme()
145
            enableStrictMode(true)
146
            warmBrowsersCache()
147
148

            // Make sure the engine is initialized and ready to use.
149
            StrictMode.allowThreadDiskReads().resetPoliciesAfter {
150
151
                components.core.engine.warmUp()
            }
Gabriel Luong's avatar
Gabriel Luong committed
152
153
            initializeWebExtensionSupport()

154
155
            restoreDownloads()

156
157
158
159
160
161
            // Just to make sure it is impossible for any application-services pieces
            // to invoke parts of itself that require complete megazord initialization
            // before that process completes, we wait here, if necessary.
            if (!megazordSetup.isCompleted) {
                runBlocking { megazordSetup.await(); }
            }
162
163
164

            // Give TAS the base Context
            Prefs.setContext(applicationContext)
165
        }
166

167
        setupLeakCanary()
168
        startMetricsIfEnabled()
169
        setupPush()
170
171
172
173

        visibilityLifecycleCallback = VisibilityLifecycleCallback(getSystemService())
        registerActivityLifecycleCallbacks(visibilityLifecycleCallback)

174
175
176
177
178
        // Storage maintenance disabled, for now, as it was interfering with background migrations.
        // See https://github.com/mozilla-mobile/fenix/issues/7227 for context.
        // if ((System.currentTimeMillis() - settings().lastPlacesStorageMaintenance) > ONE_DAY_MILLIS) {
        //    runStorageMaintenance()
        // }
179

180
        initVisualCompletenessQueueAndQueueTasks()
181
182

        components.appStartupTelemetry.onFenixApplicationOnCreate()
183
        components.torController.start()
184
185
    }

186
    private fun restoreDownloads() {
187
        components.useCases.downloadUseCases.restoreDownloads()
188
189
    }

190
    private fun initVisualCompletenessQueueAndQueueTasks() {
191
        val queue = components.performance.visualCompletenessQueue.queue
192
193

        fun initQueue() {
194
            registerActivityLifecycleCallbacks(PerformanceActivityLifecycleCallbacks(queue))
195
196
197
198
        }

        fun queueInitExperiments() {
            if (settings().isExperimentationEnabled) {
199
                queue.runIfReadyOrQueue {
200
201
202
203
204
205
206
207
208
                    Experiments.initialize(
                        applicationContext = applicationContext,
                        onExperimentsUpdated = {
                            ExperimentsManager.initSearchWidgetExperiment(this)
                        },
                        configuration = mozilla.components.service.experiments.Configuration(
                            httpClient = components.core.client,
                            kintoEndpoint = KINTO_ENDPOINT_PROD
                        )
209
                    )
210
211
212
213
214
215
                    ExperimentsManager.initSearchWidgetExperiment(this)
                }
            } else {
                // We should make a better way to opt out for when we have more experiments
                // See https://github.com/mozilla-mobile/fenix/issues/6278
                ExperimentsManager.optOutSearchWidgetExperiment(this)
216
217
218
            }
        }

219
        fun queueInitStorageAndServices() {
220
            components.performance.visualCompletenessQueue.queue.runIfReadyOrQueue {
221
222
223
224
225
226
227
                GlobalScope.launch(Dispatchers.IO) {
                    logger.info("Running post-visual completeness tasks...")
                    logElapsedTime(logger, "Storage initialization") {
                        components.core.historyStorage.warmUp()
                        components.core.bookmarksStorage.warmUp()
                        components.core.passwordsStorage.warmUp()
                    }
228
                }
229
230
231
232
233
                // Account manager initialization needs to happen on the main thread.
                GlobalScope.launch(Dispatchers.Main) {
                    logElapsedTime(logger, "Kicking-off account manager") {
                        components.backgroundServices.accountManager
                    }
234
235
                }
            }
236
        }
237

238
239
        fun queueMetrics() {
            if (SDK_INT >= Build.VERSION_CODES.O) { // required by StorageStatsMetrics.
240
                queue.runIfReadyOrQueue {
241
242
243
244
245
246
247
248
                    // Because it may be slow to capture the storage stats, it might be preferred to
                    // create a WorkManager task for this metric, however, I ran out of
                    // implementation time and WorkManager is harder to test.
                    StorageStatsMetrics.report(this.applicationContext)
                }
            }
        }

249
250
251
252
253
254
        fun queueReviewPrompt() {
            GlobalScope.launch(Dispatchers.IO) {
                components.reviewPromptController.trackApplicationLaunch()
            }
        }

255
256
257
258
259
260
        initQueue()

        // We init these items in the visual completeness queue to avoid them initing in the critical
        // startup path, before the UI finishes drawing (i.e. visual completeness).
        queueInitExperiments()
        queueInitStorageAndServices()
261
        queueMetrics()
262
        queueReviewPrompt()
263
264
    }

265
266
267
268
269
270
271
272
273
274
    private fun startMetricsIfEnabled() {
        if (settings().isTelemetryEnabled) {
            components.analytics.metrics.start(MetricServiceType.Data)
        }

        if (settings().isMarketingTelemetryEnabled) {
            components.analytics.metrics.start(MetricServiceType.Marketing)
        }
    }

275
276
277
278
    // See https://github.com/mozilla-mobile/fenix/issues/7227 for context.
    // To re-enable this, we need to do so in a way that won't interfere with any startup operations
    // which acquire reserved+ sqlite lock. Currently, Fennec migrations need to write to storage
    // on startup, and since they run in a background service we can't simply order these operations.
279
280
281
282
283
284
    private fun runStorageMaintenance() {
        GlobalScope.launch(Dispatchers.IO) {
            // Bookmarks and history storage sit on top of the same db file so we only need to
            // run maintenance on one - arbitrarily using bookmarks.
            components.core.bookmarksStorage.runMaintenance()
        }
285
        settings().lastPlacesStorageMaintenance = System.currentTimeMillis()
286
287
    }

288
289
290
291
    protected open fun setupLeakCanary() {
        // no-op, LeakCanary is disabled by default
    }

292
    open fun updateLeakCanaryState(isEnabled: Boolean) {
293
294
295
        // no-op, LeakCanary is disabled by default
    }

296
    private fun setupPush() {
297
298
299
        // Sets the PushFeature as the singleton instance for push messages to go to.
        // We need the push feature setup here to deliver messages in the case where the service
        // starts up the app first.
300
        components.push.feature?.let {
301
            Logger.info("AutoPushFeature is configured, initializing it...")
302
303

            // Install the AutoPush singleton to receive messages.
304
305
            PushProcessor.install(it)

306
            WebPushEngineIntegration(components.core.engine, it).start()
307

308
309
            // Perform a one-time initialization of the account manager if a message is received.
            PushFxaIntegration(it, lazy { components.backgroundServices.accountManager }).launch()
310
311

            // Initialize the service. This could potentially be done in a coroutine in the future.
312
            it.initialize()
313
        }
314
315
    }

316
317
318
319
320
321
    private fun setupCrashReporting() {
        components
            .analytics
            .crashReporter
            .install(this)
    }
322
323
324
325

    /**
     * Initiate Megazord sequence! Megazord Battle Mode!
     *
326
327
328
329
     * The application-services combined libraries are known as the "megazord". We use the default `full`
     * megazord - it contains everything that fenix needs, and (currently) nothing more.
     *
     * Documentation on what megazords are, and why they're needed:
330
331
     * - https://github.com/mozilla/application-services/blob/master/docs/design/megazords.md
     * - https://mozilla.github.io/application-services/docs/applications/consuming-megazord-libraries.html
332
     */
333
334
    private fun setupMegazord(): Deferred<Unit> {
        // Note: Megazord.init() must be called as soon as possible ...
335
        Megazord.init()
336
337
338
339

        return GlobalScope.async(Dispatchers.IO) {
            // ... but RustHttpConfig.setClient() and RustLog.enable() can be called later.
            RustHttpConfig.setClient(lazy { components.core.client })
340
            RustLog.enable(components.analytics.crashReporter)
341
        }
342
    }
343

344
345
    override fun onTrimMemory(level: Int) {
        super.onTrimMemory(level)
346

347
        runOnlyInMainProcess {
348
            components.core.icons.onTrimMemory(level)
349
            components.core.store.dispatch(SystemAction.LowMemoryAction(level))
350
351
        }
    }
Emily Kager's avatar
Emily Kager committed
352

353
354
    @SuppressLint("WrongConstant")
    // Suppressing erroneous lint warning about using MODE_NIGHT_AUTO_BATTERY, a likely library bug
Emily Kager's avatar
Emily Kager committed
355
    private fun setDayNightTheme() {
356
        val settings = this.settings()
Emily Kager's avatar
Emily Kager committed
357
        when {
358
            settings.shouldUseLightTheme -> {
Emily Kager's avatar
Emily Kager committed
359
360
361
362
                AppCompatDelegate.setDefaultNightMode(
                    AppCompatDelegate.MODE_NIGHT_NO
                )
            }
363
            settings.shouldUseDarkTheme -> {
Emily Kager's avatar
Emily Kager committed
364
365
366
367
                AppCompatDelegate.setDefaultNightMode(
                    AppCompatDelegate.MODE_NIGHT_YES
                )
            }
368
            SDK_INT < Build.VERSION_CODES.P && settings.shouldUseAutoBatteryTheme -> {
Emily Kager's avatar
Emily Kager committed
369
370
371
372
                AppCompatDelegate.setDefaultNightMode(
                    AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY
                )
            }
373
            SDK_INT >= Build.VERSION_CODES.P && settings.shouldFollowDeviceTheme -> {
Emily Kager's avatar
Emily Kager committed
374
375
376
377
378
379
                AppCompatDelegate.setDefaultNightMode(
                    AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
                )
            }
            // First run of app no default set, set the default to Follow System for 28+ and Normal Mode otherwise
            else -> {
380
                if (SDK_INT >= Build.VERSION_CODES.P) {
Emily Kager's avatar
Emily Kager committed
381
382
383
                    AppCompatDelegate.setDefaultNightMode(
                        AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
                    )
384
                    settings.shouldFollowDeviceTheme = true
Emily Kager's avatar
Emily Kager committed
385
386
387
388
                } else {
                    AppCompatDelegate.setDefaultNightMode(
                        AppCompatDelegate.MODE_NIGHT_NO
                    )
389
                    settings.shouldUseLightTheme = true
Emily Kager's avatar
Emily Kager committed
390
391
392
393
                }
            }
        }
    }
394

395
396
397
398
399
400
401
402
    private fun warmBrowsersCache() {
        // We avoid blocking the main thread for BrowsersCache on startup by loading it on
        // background thread.
        GlobalScope.launch(Dispatchers.Default) {
            BrowsersCache.all(this@FenixApplication)
        }
    }

Gabriel Luong's avatar
Gabriel Luong committed
403
404
    private fun initializeWebExtensionSupport() {
        try {
405
406
            GlobalAddonDependencyProvider.initialize(
                components.addonManager,
407
408
409
410
                components.addonUpdater,
                onCrash = { exception ->
                    components.analytics.crashReporter.submitCaughtException(exception)
                }
411
            )
Gabriel Luong's avatar
Gabriel Luong committed
412
413
414
415
416
            WebExtensionSupport.initialize(
                components.core.engine,
                components.core.store,
                onNewTabOverride = {
                    _, engineSession, url ->
417
418
                        val shouldCreatePrivateSession =
                            components.core.sessionManager.selectedSession?.private
419
                                ?: components.settings.openLinksInAPrivateTab
420
421

                        val session = Session(url, shouldCreatePrivateSession)
Gabriel Luong's avatar
Gabriel Luong committed
422
423
424
425
                        components.core.sessionManager.add(session, true, engineSession)
                        session.id
                },
                onCloseTabOverride = {
426
                    _, sessionId -> components.useCases.tabsUseCases.removeTab(sessionId)
Gabriel Luong's avatar
Gabriel Luong committed
427
428
429
430
                },
                onSelectTabOverride = {
                    _, sessionId ->
                        val selected = components.core.sessionManager.findSessionById(sessionId)
431
                        selected?.let { components.useCases.tabsUseCases.selectTab(it) }
432
433
434
                },
                onExtensionsLoaded = { extensions ->
                    components.addonUpdater.registerForFutureUpdates(extensions)
435
                    components.supportedAddonsChecker.registerForChecks()
436
437
                },
                onUpdatePermissionRequest = components.addonUpdater::onUpdatePermissionRequest
Gabriel Luong's avatar
Gabriel Luong committed
438
439
440
441
442
            )
        } catch (e: UnsupportedOperationException) {
            Logger.error("Failed to initialize web extension support", e)
        }
    }
443
444
445
446

    protected fun recordOnInit() {
        // This gets called by more than one process. Ideally we'd only run this in the main process
        // but the code to check which process we're in crashes because the Context isn't valid yet.
447
448
449
        //
        // This method is not covered by our internal crash reporting: be very careful when modifying it.
        StartupTimeline.onApplicationInit() // DO NOT MOVE ANYTHING ABOVE HERE: the timing is critical.
450
    }
451
452
453
454
455
456

    override fun onConfigurationChanged(config: android.content.res.Configuration) {
        // Workaround for androidx appcompat issue where follow system day/night mode config changes
        // are not triggered when also using createConfigurationContext like we do in LocaleManager
        // https://issuetracker.google.com/issues/143570309#comment3
        applicationContext.resources.configuration.uiMode = config.uiMode
457
458
459
460
461

        // random StrictMode onDiskRead violation even when Fenix is not running in the background.
        StrictMode.allowThreadDiskReads().resetPoliciesAfter {
            super.onConfigurationChanged(config)
        }
462
    }
463
464
465
466

    companion object {
        private const val KINTO_ENDPOINT_PROD = "https://firefox.settings.services.mozilla.com/v1"
    }
467
468

    override fun getWorkManagerConfiguration() = Builder().setMinimumLoggingLevel(INFO).build()
469
}