FenixApplication.kt 19.1 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
165
166
            GlobalScope.launch(Dispatchers.IO) {
                // Give TAS the base Context
                Prefs.setContext(applicationContext)
            }
167
        }
168

169
        setupLeakCanary()
170
        startMetricsIfEnabled()
171
        setupPush()
172
173
174
175

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

176
177
178
179
180
        // 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()
        // }
181

182
        initVisualCompletenessQueueAndQueueTasks()
183
184

        components.appStartupTelemetry.onFenixApplicationOnCreate()
185
        components.torController.start()
186
187
    }

188
    private fun restoreDownloads() {
189
        components.useCases.downloadUseCases.restoreDownloads()
190
191
    }

192
    private fun initVisualCompletenessQueueAndQueueTasks() {
193
        val queue = components.performance.visualCompletenessQueue.queue
194
195

        fun initQueue() {
196
            registerActivityLifecycleCallbacks(PerformanceActivityLifecycleCallbacks(queue))
197
198
199
200
        }

        fun queueInitExperiments() {
            if (settings().isExperimentationEnabled) {
201
                queue.runIfReadyOrQueue {
202
203
204
205
206
207
208
209
210
                    Experiments.initialize(
                        applicationContext = applicationContext,
                        onExperimentsUpdated = {
                            ExperimentsManager.initSearchWidgetExperiment(this)
                        },
                        configuration = mozilla.components.service.experiments.Configuration(
                            httpClient = components.core.client,
                            kintoEndpoint = KINTO_ENDPOINT_PROD
                        )
211
                    )
212
213
214
215
216
217
                    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)
218
219
220
            }
        }

221
        fun queueInitStorageAndServices() {
222
            components.performance.visualCompletenessQueue.queue.runIfReadyOrQueue {
223
224
225
226
227
228
229
                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()
                    }
230
                }
231
232
233
234
235
                // Account manager initialization needs to happen on the main thread.
                GlobalScope.launch(Dispatchers.Main) {
                    logElapsedTime(logger, "Kicking-off account manager") {
                        components.backgroundServices.accountManager
                    }
236
237
                }
            }
238
        }
239

240
241
        fun queueMetrics() {
            if (SDK_INT >= Build.VERSION_CODES.O) { // required by StorageStatsMetrics.
242
                queue.runIfReadyOrQueue {
243
244
245
246
247
248
249
250
                    // 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)
                }
            }
        }

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

257
258
259
260
261
262
        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()
263
        queueMetrics()
264
        queueReviewPrompt()
265
266
    }

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

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

277
278
279
280
    // 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.
281
282
283
284
285
286
    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()
        }
287
        settings().lastPlacesStorageMaintenance = System.currentTimeMillis()
288
289
    }

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

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

298
    private fun setupPush() {
299
300
301
        // 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.
302
        components.push.feature?.let {
303
            Logger.info("AutoPushFeature is configured, initializing it...")
304
305

            // Install the AutoPush singleton to receive messages.
306
307
            PushProcessor.install(it)

308
            WebPushEngineIntegration(components.core.engine, it).start()
309

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

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

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

    /**
     * Initiate Megazord sequence! Megazord Battle Mode!
     *
328
329
330
331
     * 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:
332
333
     * - https://github.com/mozilla/application-services/blob/master/docs/design/megazords.md
     * - https://mozilla.github.io/application-services/docs/applications/consuming-megazord-libraries.html
334
     */
335
336
    private fun setupMegazord(): Deferred<Unit> {
        // Note: Megazord.init() must be called as soon as possible ...
337
        Megazord.init()
338
339
340
341

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

346
347
    override fun onTrimMemory(level: Int) {
        super.onTrimMemory(level)
348

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

355
356
    @SuppressLint("WrongConstant")
    // Suppressing erroneous lint warning about using MODE_NIGHT_AUTO_BATTERY, a likely library bug
Emily Kager's avatar
Emily Kager committed
357
    private fun setDayNightTheme() {
358
        val settings = this.settings()
Emily Kager's avatar
Emily Kager committed
359
        when {
360
            settings.shouldUseLightTheme -> {
Emily Kager's avatar
Emily Kager committed
361
362
363
364
                AppCompatDelegate.setDefaultNightMode(
                    AppCompatDelegate.MODE_NIGHT_NO
                )
            }
365
            settings.shouldUseDarkTheme -> {
Emily Kager's avatar
Emily Kager committed
366
367
368
369
                AppCompatDelegate.setDefaultNightMode(
                    AppCompatDelegate.MODE_NIGHT_YES
                )
            }
370
            SDK_INT < Build.VERSION_CODES.P && settings.shouldUseAutoBatteryTheme -> {
Emily Kager's avatar
Emily Kager committed
371
372
373
374
                AppCompatDelegate.setDefaultNightMode(
                    AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY
                )
            }
375
            SDK_INT >= Build.VERSION_CODES.P && settings.shouldFollowDeviceTheme -> {
Emily Kager's avatar
Emily Kager committed
376
377
378
379
380
381
                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 -> {
382
                if (SDK_INT >= Build.VERSION_CODES.P) {
Emily Kager's avatar
Emily Kager committed
383
384
385
                    AppCompatDelegate.setDefaultNightMode(
                        AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
                    )
386
                    settings.shouldFollowDeviceTheme = true
Emily Kager's avatar
Emily Kager committed
387
388
389
390
                } else {
                    AppCompatDelegate.setDefaultNightMode(
                        AppCompatDelegate.MODE_NIGHT_NO
                    )
391
                    settings.shouldUseLightTheme = true
Emily Kager's avatar
Emily Kager committed
392
393
394
395
                }
            }
        }
    }
396

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

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

    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.
449
450
451
        //
        // 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.
452
    }
453
454
455
456
457
458

    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
459
460
461
462
463

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

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

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