FenixApplication.kt 14.9 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 androidx.annotation.CallSuper
Emily Kager's avatar
Emily Kager committed
12
import androidx.appcompat.app.AppCompatDelegate
13
import androidx.core.content.getSystemService
14
import kotlinx.coroutines.Deferred
15
16
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
17
18
19
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
20
import mozilla.appservices.Megazord
Gabriel Luong's avatar
Gabriel Luong committed
21
import mozilla.components.browser.session.Session
22
import mozilla.components.concept.push.PushProcessor
23
import mozilla.components.feature.addons.update.GlobalAddonDependencyProvider
24
25
26
import mozilla.components.service.glean.Glean
import mozilla.components.service.glean.config.Configuration
import mozilla.components.service.glean.net.ConceptFetchHttpUploader
27
28
29
import mozilla.components.support.base.log.Log
import mozilla.components.support.base.log.logger.Logger
import mozilla.components.support.base.log.sink.AndroidLogSink
30
31
import mozilla.components.support.ktx.android.content.isMainProcess
import mozilla.components.support.ktx.android.content.runOnlyInMainProcess
32
import mozilla.components.support.locale.LocaleAwareApplication
33
import mozilla.components.support.rusthttp.RustHttpConfig
34
import mozilla.components.support.rustlog.RustLog
35
import mozilla.components.support.utils.logElapsedTime
Gabriel Luong's avatar
Gabriel Luong committed
36
import mozilla.components.support.webextensions.WebExtensionSupport
37
import org.mozilla.fenix.FeatureFlags.webPushIntegration
38
import org.mozilla.fenix.components.Components
39
import org.mozilla.fenix.components.metrics.MetricServiceType
40
import org.mozilla.fenix.ext.settings
41
import org.mozilla.fenix.push.PushFxaIntegration
42
import org.mozilla.fenix.push.WebPushEngineIntegration
43
import org.mozilla.fenix.session.NotificationSessionObserver
44
import org.mozilla.fenix.session.PerformanceActivityLifecycleCallbacks
45
import org.mozilla.fenix.session.VisibilityLifecycleCallback
46
import org.mozilla.fenix.utils.BrowsersCache
47
import org.mozilla.fenix.utils.Settings
48

49
@SuppressLint("Registered")
50
@Suppress("TooManyFunctions", "LargeClass")
51
open class FenixApplication : LocaleAwareApplication() {
52
    private val logger = Logger("FenixApplication")
53

54
    open val components by lazy { Components(this) }
55

56
57
58
    var visibilityLifecycleCallback: VisibilityLifecycleCallback? = null
        private set

59
60
    override fun onCreate() {
        super.onCreate()
61

62
        setupInAllProcesses()
63

64
        if (!isMainProcess()) {
65
66
67
            // 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
68
            // situation where we create a GeckoRuntime from the Gecko child process.
69
            return
70
        }
71

72
73
74
75
76
77
        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()
78
        }
79
80
81
82
83
84
85
86
87

        setupInMainProcessOnly()
    }

    protected fun initializeGlean() {
        val telemetryEnabled = settings().isTelemetryEnabled

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

88
89
90
91
92
93
94
        Glean.initialize(
            applicationContext = this,
            configuration = Configuration(
                channel = BuildConfig.BUILD_TYPE,
                httpClient = ConceptFetchHttpUploader(
                    lazy(LazyThreadSafetyMode.NONE) { components.core.client }
                )),
95
            uploadEnabled = telemetryEnabled
96
        )
97
98
99
100
101
102
103
104
105
106
107
108
    }

    @CallSuper
    open fun setupInAllProcesses() {
        setupCrashReporting()

        // We want the log messages of all builds to go to Android logcat
        Log.addSink(AndroidLogSink())
    }

    @CallSuper
    open fun setupInMainProcessOnly() {
109
110
111
112
113
114
        run {
            // Attention: Do not invoke any code from a-s in this scope.
            val megazordSetup = setupMegazord()

            setDayNightTheme()
            enableStrictMode()
115
            warmBrowsersCache()
116
117
118
119

            // Make sure the engine is initialized and ready to use.
            components.core.engine.warmUp()

Gabriel Luong's avatar
Gabriel Luong committed
120
121
            initializeWebExtensionSupport()

122
123
124
125
126
127
128
            // 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(); }
            }
        }
129

130
        setupLeakCanary()
131
        if (settings().isTelemetryEnabled) {
132
133
134
135
136
            components.analytics.metrics.start(MetricServiceType.Data)
        }

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

139
        setupPush()
140
141
142
143
144

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

        components.core.sessionManager.register(NotificationSessionObserver(this))
145

146
147
148
149
150
        // 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()
        // }
151
152

        registerActivityLifecycleCallbacks(
153
            PerformanceActivityLifecycleCallbacks(components.performance.visualCompletenessQueue)
154
        )
155

156
        components.performance.visualCompletenessQueue.runIfReadyOrQueue {
157
            GlobalScope.launch(Dispatchers.IO) {
158
                logger.info("Running post-visual completeness tasks...")
159
160
161
162
163
                logElapsedTime(logger, "Storage initialization") {
                    components.core.historyStorage.warmUp()
                    components.core.bookmarksStorage.warmUp()
                    components.core.passwordsStorage.warmUp()
                }
164
            }
165
166
167
168
169
170
            // Account manager initialization needs to happen on the main thread.
            GlobalScope.launch(Dispatchers.Main) {
                logElapsedTime(logger, "Kicking-off account manager") {
                    components.backgroundServices.accountManager
                }
            }
171
        }
172
173
    }

174
175
176
177
    // 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.
178
179
180
181
182
183
    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()
        }
184
        settings().lastPlacesStorageMaintenance = System.currentTimeMillis()
185
186
    }

187
188
189
190
    protected open fun setupLeakCanary() {
        // no-op, LeakCanary is disabled by default
    }

191
    open fun updateLeakCanaryState(isEnabled: Boolean) {
192
193
194
        // no-op, LeakCanary is disabled by default
    }

195
    private fun setupPush() {
196
197
198
        // 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.
199
        components.push.feature?.let {
200
            Logger.info("AutoPushFeature is configured, initializing it...")
201
202

            // Install the AutoPush singleton to receive messages.
203
204
            PushProcessor.install(it)

205
206
207
208
            if (webPushIntegration) {
                // WebPush integration to observe and deliver push messages to engine.
                WebPushEngineIntegration(components.core.engine, it).start()
            }
209

210
211
            // Perform a one-time initialization of the account manager if a message is received.
            PushFxaIntegration(it, lazy { components.backgroundServices.accountManager }).launch()
212
213

            // Initialize the service. This could potentially be done in a coroutine in the future.
214
            it.initialize()
215
        }
216
217
    }

218
219
220
221
222
223
    private fun setupCrashReporting() {
        components
            .analytics
            .crashReporter
            .install(this)
    }
224
225
226
227

    /**
     * Initiate Megazord sequence! Megazord Battle Mode!
     *
228
229
230
231
     * 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:
232
233
     * - https://github.com/mozilla/application-services/blob/master/docs/design/megazords.md
     * - https://mozilla.github.io/application-services/docs/applications/consuming-megazord-libraries.html
234
     */
235
236
    private fun setupMegazord(): Deferred<Unit> {
        // Note: Megazord.init() must be called as soon as possible ...
237
        Megazord.init()
238
239
240
241
242
243

        return GlobalScope.async(Dispatchers.IO) {
            // ... but RustHttpConfig.setClient() and RustLog.enable() can be called later.
            RustHttpConfig.setClient(lazy { components.core.client })
            RustLog.enable()
        }
244
    }
245

246
247
    override fun onTrimMemory(level: Int) {
        super.onTrimMemory(level)
248

249
        runOnlyInMainProcess {
250
251
            components.core.icons.onTrimMemory(level)
            components.core.sessionManager.onTrimMemory(level)
252
253
        }
    }
Emily Kager's avatar
Emily Kager committed
254

255
256
    @SuppressLint("WrongConstant")
    // Suppressing erroneous lint warning about using MODE_NIGHT_AUTO_BATTERY, a likely library bug
Emily Kager's avatar
Emily Kager committed
257
    private fun setDayNightTheme() {
258
        val settings = this.settings()
Emily Kager's avatar
Emily Kager committed
259
        when {
260
            settings.shouldUseLightTheme -> {
Emily Kager's avatar
Emily Kager committed
261
262
263
264
                AppCompatDelegate.setDefaultNightMode(
                    AppCompatDelegate.MODE_NIGHT_NO
                )
            }
265
            settings.shouldUseDarkTheme -> {
Emily Kager's avatar
Emily Kager committed
266
267
268
269
                AppCompatDelegate.setDefaultNightMode(
                    AppCompatDelegate.MODE_NIGHT_YES
                )
            }
270
            SDK_INT < Build.VERSION_CODES.P && settings.shouldUseAutoBatteryTheme -> {
Emily Kager's avatar
Emily Kager committed
271
272
273
274
                AppCompatDelegate.setDefaultNightMode(
                    AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY
                )
            }
275
            SDK_INT >= Build.VERSION_CODES.P && settings.shouldFollowDeviceTheme -> {
Emily Kager's avatar
Emily Kager committed
276
277
278
279
280
281
                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 -> {
282
                if (SDK_INT >= Build.VERSION_CODES.P) {
Emily Kager's avatar
Emily Kager committed
283
284
285
                    AppCompatDelegate.setDefaultNightMode(
                        AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
                    )
286
                    settings.shouldFollowDeviceTheme = true
Emily Kager's avatar
Emily Kager committed
287
288
289
290
                } else {
                    AppCompatDelegate.setDefaultNightMode(
                        AppCompatDelegate.MODE_NIGHT_NO
                    )
291
                    settings.shouldUseLightTheme = true
Emily Kager's avatar
Emily Kager committed
292
293
294
295
                }
            }
        }
    }
296

297
298
299
300
301
302
303
304
    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)
        }
    }

305
    private fun enableStrictMode() {
306
        if (Config.channel.isDebug) {
307
308
309
310
311
312
313
314
315
316
317
318
319
            StrictMode.setThreadPolicy(
                StrictMode.ThreadPolicy.Builder()
                    .detectAll()
                    .penaltyLog()
                    .build()
            )
            var builder = StrictMode.VmPolicy.Builder()
                .detectLeakedSqlLiteObjects()
                .detectLeakedClosableObjects()
                .detectLeakedRegistrationObjects()
                .detectActivityLeaks()
                .detectFileUriExposure()
                .penaltyLog()
320
321
            if (SDK_INT >= Build.VERSION_CODES.O) builder =
                builder.detectContentUriWithoutPermission()
322
323
324
325
            if (SDK_INT >= Build.VERSION_CODES.P) builder = builder.detectNonSdkApiUsage()
            StrictMode.setVmPolicy(builder.build())
        }
    }
Gabriel Luong's avatar
Gabriel Luong committed
326
327
328

    private fun initializeWebExtensionSupport() {
        try {
329
330
            GlobalAddonDependencyProvider.initialize(
                components.addonManager,
331
332
333
334
                components.addonUpdater,
                onCrash = { exception ->
                    components.analytics.crashReporter.submitCaughtException(exception)
                }
335
            )
Gabriel Luong's avatar
Gabriel Luong committed
336
337
338
339
340
            WebExtensionSupport.initialize(
                components.core.engine,
                components.core.store,
                onNewTabOverride = {
                    _, engineSession, url ->
341
342
343
344
345
346
                        val shouldCreatePrivateSession =
                            components.core.sessionManager.selectedSession?.private
                                ?: Settings.instance?.openLinksInAPrivateTab
                                ?: false

                        val session = Session(url, shouldCreatePrivateSession)
Gabriel Luong's avatar
Gabriel Luong committed
347
348
349
350
351
352
353
354
355
356
                        components.core.sessionManager.add(session, true, engineSession)
                        session.id
                },
                onCloseTabOverride = {
                    _, sessionId -> components.tabsUseCases.removeTab(sessionId)
                },
                onSelectTabOverride = {
                    _, sessionId ->
                        val selected = components.core.sessionManager.findSessionById(sessionId)
                        selected?.let { components.tabsUseCases.selectTab(it) }
357
358
359
                },
                onExtensionsLoaded = { extensions ->
                    components.addonUpdater.registerForFutureUpdates(extensions)
360
                    components.supportedAddChecker.registerForChecks()
361
362
                },
                onUpdatePermissionRequest = components.addonUpdater::onUpdatePermissionRequest
Gabriel Luong's avatar
Gabriel Luong committed
363
364
365
366
367
            )
        } catch (e: UnsupportedOperationException) {
            Logger.error("Failed to initialize web extension support", e)
        }
    }
368
}