[fenix] Bug 1814410: Unified search migrate to general se after
[gecko.git] / mobile / android / fenix / app / src / main / java / org / mozilla / fenix / FenixApplication.kt
blobe08f7de5a764ecbf5a6573b764341bb29c619e94
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2  * License, v. 2.0. If a copy of the MPL was not distributed with this
3  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 package org.mozilla.fenix
7 import android.annotation.SuppressLint
8 import android.net.Uri
9 import android.os.Build
10 import android.os.Build.VERSION.SDK_INT
11 import android.os.StrictMode
12 import android.os.SystemClock
13 import android.util.Log.INFO
14 import androidx.annotation.CallSuper
15 import androidx.annotation.VisibleForTesting
16 import androidx.appcompat.app.AppCompatDelegate
17 import androidx.core.app.NotificationManagerCompat
18 import androidx.core.content.getSystemService
19 import androidx.lifecycle.ProcessLifecycleOwner
20 import androidx.work.Configuration.Builder
21 import androidx.work.Configuration.Provider
22 import kotlinx.coroutines.Deferred
23 import kotlinx.coroutines.DelicateCoroutinesApi
24 import kotlinx.coroutines.Dispatchers
25 import kotlinx.coroutines.GlobalScope
26 import kotlinx.coroutines.async
27 import kotlinx.coroutines.launch
28 import mozilla.appservices.Megazord
29 import mozilla.components.browser.state.action.SystemAction
30 import mozilla.components.browser.state.selector.selectedTab
31 import mozilla.components.browser.state.state.searchEngines
32 import mozilla.components.browser.state.state.selectedOrDefaultSearchEngine
33 import mozilla.components.browser.state.store.BrowserStore
34 import mozilla.components.browser.storage.sync.GlobalPlacesDependencyProvider
35 import mozilla.components.concept.base.crash.Breadcrumb
36 import mozilla.components.concept.engine.webextension.WebExtension
37 import mozilla.components.concept.engine.webextension.isUnsupported
38 import mozilla.components.concept.push.PushProcessor
39 import mozilla.components.concept.storage.FrecencyThresholdOption
40 import mozilla.components.feature.addons.migration.DefaultSupportedAddonsChecker
41 import mozilla.components.feature.addons.update.GlobalAddonDependencyProvider
42 import mozilla.components.feature.autofill.AutofillUseCases
43 import mozilla.components.feature.search.ext.buildSearchUrl
44 import mozilla.components.feature.search.ext.waitForSelectedOrDefaultSearchEngine
45 import mozilla.components.feature.top.sites.TopSitesFrecencyConfig
46 import mozilla.components.feature.top.sites.TopSitesProviderConfig
47 import mozilla.components.lib.crash.CrashReporter
48 import mozilla.components.service.fxa.manager.SyncEnginesStorage
49 import mozilla.components.service.glean.Glean
50 import mozilla.components.service.glean.config.Configuration
51 import mozilla.components.service.glean.net.ConceptFetchHttpUploader
52 import mozilla.components.support.base.facts.register
53 import mozilla.components.support.base.log.Log
54 import mozilla.components.support.base.log.logger.Logger
55 import mozilla.components.support.ktx.android.content.isMainProcess
56 import mozilla.components.support.ktx.android.content.runOnlyInMainProcess
57 import mozilla.components.support.locale.LocaleAwareApplication
58 import mozilla.components.support.rusterrors.initializeRustErrors
59 import mozilla.components.support.rusthttp.RustHttpConfig
60 import mozilla.components.support.rustlog.RustLog
61 import mozilla.components.support.utils.logElapsedTime
62 import mozilla.components.support.webextensions.WebExtensionSupport
63 import org.mozilla.fenix.GleanMetrics.Addons
64 import org.mozilla.fenix.GleanMetrics.AndroidAutofill
65 import org.mozilla.fenix.GleanMetrics.CustomizeHome
66 import org.mozilla.fenix.GleanMetrics.Events.marketingNotificationAllowed
67 import org.mozilla.fenix.GleanMetrics.GleanBuildInfo
68 import org.mozilla.fenix.GleanMetrics.Metrics
69 import org.mozilla.fenix.GleanMetrics.PerfStartup
70 import org.mozilla.fenix.GleanMetrics.Preferences
71 import org.mozilla.fenix.GleanMetrics.SearchDefaultEngine
72 import org.mozilla.fenix.GleanMetrics.TopSites
73 import org.mozilla.fenix.components.Components
74 import org.mozilla.fenix.components.Core
75 import org.mozilla.fenix.components.appstate.AppAction
76 import org.mozilla.fenix.components.metrics.MetricServiceType
77 import org.mozilla.fenix.components.metrics.MozillaProductDetector
78 import org.mozilla.fenix.components.toolbar.ToolbarPosition
79 import org.mozilla.fenix.experiments.maybeFetchExperiments
80 import org.mozilla.fenix.ext.areNotificationsEnabledSafe
81 import org.mozilla.fenix.ext.containsQueryParameters
82 import org.mozilla.fenix.ext.getCustomGleanServerUrlIfAvailable
83 import org.mozilla.fenix.ext.isCustomEngine
84 import org.mozilla.fenix.ext.isKnownSearchDomain
85 import org.mozilla.fenix.ext.isNotificationChannelEnabled
86 import org.mozilla.fenix.ext.setCustomEndpointIfAvailable
87 import org.mozilla.fenix.ext.settings
88 import org.mozilla.fenix.nimbus.FxNimbus
89 import org.mozilla.fenix.onboarding.MARKETING_CHANNEL_ID
90 import org.mozilla.fenix.perf.MarkersActivityLifecycleCallbacks
91 import org.mozilla.fenix.perf.ProfilerMarkerFactProcessor
92 import org.mozilla.fenix.perf.StartupTimeline
93 import org.mozilla.fenix.perf.StorageStatsMetrics
94 import org.mozilla.fenix.perf.runBlockingIncrement
95 import org.mozilla.fenix.push.PushFxaIntegration
96 import org.mozilla.fenix.push.WebPushEngineIntegration
97 import org.mozilla.fenix.session.PerformanceActivityLifecycleCallbacks
98 import org.mozilla.fenix.session.VisibilityLifecycleCallback
99 import org.mozilla.fenix.settings.CustomizationFragment
100 import org.mozilla.fenix.telemetry.TelemetryLifecycleObserver
101 import org.mozilla.fenix.utils.BrowsersCache
102 import org.mozilla.fenix.utils.Settings
103 import org.mozilla.fenix.utils.Settings.Companion.TOP_SITES_PROVIDER_MAX_THRESHOLD
104 import org.mozilla.fenix.wallpapers.Wallpaper
105 import java.util.UUID
106 import java.util.concurrent.TimeUnit
109  *The main application class for Fenix. Records data to measure initialization performance.
110  *  Installs [CrashReporter], initializes [Glean]  in fenix builds and setup Megazord in the main process.
111  */
112 @Suppress("Registered", "TooManyFunctions", "LargeClass")
113 open class FenixApplication : LocaleAwareApplication(), Provider {
114     init {
115         recordOnInit() // DO NOT MOVE ANYTHING ABOVE HERE: the timing of this measurement is critical.
116     }
118     private val logger = Logger("FenixApplication")
120     open val components by lazy { Components(this) }
122     var visibilityLifecycleCallback: VisibilityLifecycleCallback? = null
123         private set
125     override fun onCreate() {
126         // We measure ourselves to avoid a call into Glean before its loaded.
127         val start = SystemClock.elapsedRealtimeNanos()
129         super.onCreate()
131         setupInAllProcesses()
133         if (!isMainProcess()) {
134             // If this is not the main process then do not continue with the initialization here. Everything that
135             // follows only needs to be done in our app's main process and should not be done in other processes like
136             // a GeckoView child process or the crash handling process. Most importantly we never want to end up in a
137             // situation where we create a GeckoRuntime from the Gecko child process.
138             return
139         }
141         // DO NOT ADD ANYTHING ABOVE HERE.
142         setupInMainProcessOnly()
143         // DO NOT ADD ANYTHING UNDER HERE.
145         // DO NOT MOVE ANYTHING BELOW THIS elapsedRealtimeNanos CALL.
146         val stop = SystemClock.elapsedRealtimeNanos()
147         val durationMillis = TimeUnit.NANOSECONDS.toMillis(stop - start)
149         // We avoid blocking the main thread on startup by calling into Glean on the background thread.
150         @OptIn(DelicateCoroutinesApi::class)
151         GlobalScope.launch(Dispatchers.IO) {
152             PerfStartup.applicationOnCreate.accumulateSamples(listOf(durationMillis))
153         }
154     }
156     @OptIn(DelicateCoroutinesApi::class) // GlobalScope usage
157     protected open fun initializeGlean() {
158         val telemetryEnabled = settings().isTelemetryEnabled
160         logger.debug("Initializing Glean (uploadEnabled=$telemetryEnabled})")
162         // for performance reasons, this is only available in Nightly or Debug builds
163         val customEndpoint = if (Config.channel.isNightlyOrDebug) {
164             // for testing, if custom glean server url is set in the secret menu, use it to initialize Glean
165             getCustomGleanServerUrlIfAvailable(this)
166         } else {
167             null
168         }
170         val configuration = Configuration(
171             channel = BuildConfig.BUILD_TYPE,
172             httpClient = ConceptFetchHttpUploader(
173                 lazy(LazyThreadSafetyMode.NONE) { components.core.client },
174             ),
175         )
177         Glean.initialize(
178             applicationContext = this,
179             configuration = configuration.setCustomEndpointIfAvailable(customEndpoint),
180             uploadEnabled = telemetryEnabled,
181             buildInfo = GleanBuildInfo.buildInfo,
182         )
184         // We avoid blocking the main thread on startup by setting startup metrics on the background thread.
185         val store = components.core.store
186         GlobalScope.launch(Dispatchers.IO) {
187             setStartupMetrics(store, settings())
188         }
189     }
191     @CallSuper
192     open fun setupInAllProcesses() {
193         setupCrashReporting()
195         // We want the log messages of all builds to go to Android logcat
196         Log.addSink(FenixLogSink(logsDebug = Config.channel.isDebug))
197     }
199     @CallSuper
200     open fun setupInMainProcessOnly() {
201         // ⚠️ DO NOT ADD ANYTHING ABOVE THIS LINE.
202         // Especially references to the engine/BrowserStore which can alter the app initialization.
203         // See: https://github.com/mozilla-mobile/fenix/issues/26320
204         //
205         // We can initialize Nimbus before Glean because Glean will queue messages
206         // before it's initialized.
207         initializeNimbus()
209         ProfilerMarkerFactProcessor.create { components.core.engine.profiler }.register()
211         run {
212             // Make sure the engine is initialized and ready to use.
213             components.strictMode.resetAfter(StrictMode.allowThreadDiskReads()) {
214                 components.core.engine.warmUp()
215             }
217             // We need to always initialize Glean and do it early here.
218             initializeGlean()
220             // Attention: Do not invoke any code from a-s in this scope.
221             val megazordSetup = finishSetupMegazord()
223             setDayNightTheme()
224             components.strictMode.enableStrictMode(true)
225             warmBrowsersCache()
227             initializeWebExtensionSupport()
228             if (FeatureFlags.storageMaintenanceFeature) {
229                 // Make sure to call this function before registering a storage worker
230                 // (e.g. components.core.historyStorage.registerStorageMaintenanceWorker())
231                 // as the storage maintenance worker needs a places storage globally when
232                 // it is needed while the app is not running and WorkManager wakes up the app
233                 // for the periodic task.
234                 GlobalPlacesDependencyProvider.initialize(components.core.historyStorage)
235             }
236             restoreBrowserState()
237             restoreDownloads()
238             restoreMessaging()
240             // Just to make sure it is impossible for any application-services pieces
241             // to invoke parts of itself that require complete megazord initialization
242             // before that process completes, we wait here, if necessary.
243             if (!megazordSetup.isCompleted) {
244                 runBlockingIncrement { megazordSetup.await() }
245             }
246         }
248         setupLeakCanary()
249         startMetricsIfEnabled()
250         setupPush()
252         visibilityLifecycleCallback = VisibilityLifecycleCallback(getSystemService())
253         registerActivityLifecycleCallbacks(visibilityLifecycleCallback)
254         registerActivityLifecycleCallbacks(MarkersActivityLifecycleCallbacks(components.core.engine))
256         components.appStartReasonProvider.registerInAppOnCreate(this)
257         components.startupActivityLog.registerInAppOnCreate(this)
258         initVisualCompletenessQueueAndQueueTasks()
260         ProcessLifecycleOwner.get().lifecycle.addObserver(TelemetryLifecycleObserver(components.core.store))
262         components.analytics.metricsStorage.tryRegisterAsUsageRecorder(this)
264         downloadWallpapers()
265     }
267     @OptIn(DelicateCoroutinesApi::class) // GlobalScope usage
268     private fun restoreBrowserState() = GlobalScope.launch(Dispatchers.Main) {
269         val store = components.core.store
270         val sessionStorage = components.core.sessionStorage
272         components.useCases.tabsUseCases.restore(sessionStorage, settings().getTabTimeout())
274         // Now that we have restored our previous state (if there's one) let's setup auto saving the state while
275         // the app is used.
276         sessionStorage.autoSave(store)
277             .periodicallyInForeground(interval = 30, unit = TimeUnit.SECONDS)
278             .whenGoingToBackground()
279             .whenSessionsChange()
280     }
282     private fun restoreDownloads() {
283         components.useCases.downloadUseCases.restoreDownloads()
284     }
286     private fun initVisualCompletenessQueueAndQueueTasks() {
287         val queue = components.performance.visualCompletenessQueue.queue
289         fun initQueue() {
290             registerActivityLifecycleCallbacks(PerformanceActivityLifecycleCallbacks(queue))
291         }
293         @OptIn(DelicateCoroutinesApi::class) // GlobalScope usage
294         fun queueInitStorageAndServices() {
295             components.performance.visualCompletenessQueue.queue.runIfReadyOrQueue {
296                 GlobalScope.launch(Dispatchers.IO) {
297                     logger.info("Running post-visual completeness tasks...")
298                     logElapsedTime(logger, "Storage initialization") {
299                         components.core.historyStorage.warmUp()
300                         components.core.bookmarksStorage.warmUp()
301                         components.core.passwordsStorage.warmUp()
302                         components.core.autofillStorage.warmUp()
304                         // Populate the top site cache to improve initial load experience
305                         // of the home fragment when the app is launched to a tab. The actual
306                         // database call is not expensive. However, the additional context
307                         // switches delay rendering top sites when the cache is empty, which
308                         // we can prevent with this.
309                         components.core.topSitesStorage.getTopSites(
310                             totalSites = components.settings.topSitesMaxLimit,
311                             frecencyConfig = TopSitesFrecencyConfig(
312                                 FrecencyThresholdOption.SKIP_ONE_TIME_PAGES,
313                             ) {
314                                 !Uri.parse(it.url)
315                                     .containsQueryParameters(components.settings.frecencyFilterQuery)
316                             },
317                             providerConfig = TopSitesProviderConfig(
318                                 showProviderTopSites = components.settings.showContileFeature,
319                                 maxThreshold = TOP_SITES_PROVIDER_MAX_THRESHOLD,
320                             ),
321                         )
323                         // This service uses `historyStorage`, and so we can only touch it when we know
324                         // it's safe to touch `historyStorage. By 'safe', we mainly mean that underlying
325                         // places library will be able to load, which requires first running Megazord.init().
326                         // The visual completeness tasks are scheduled after the Megazord.init() call.
327                         components.core.historyMetadataService.cleanup(
328                             System.currentTimeMillis() - Core.HISTORY_METADATA_MAX_AGE_IN_MS,
329                         )
330                     }
331                 }
332                 // Account manager initialization needs to happen on the main thread.
333                 GlobalScope.launch(Dispatchers.Main) {
334                     logElapsedTime(logger, "Kicking-off account manager") {
335                         components.backgroundServices.accountManager
336                     }
337                 }
338             }
339         }
341         fun queueMetrics() {
342             if (SDK_INT >= Build.VERSION_CODES.O) { // required by StorageStatsMetrics.
343                 queue.runIfReadyOrQueue {
344                     // Because it may be slow to capture the storage stats, it might be preferred to
345                     // create a WorkManager task for this metric, however, I ran out of
346                     // implementation time and WorkManager is harder to test.
347                     StorageStatsMetrics.report(this.applicationContext)
348                 }
349             }
350         }
352         @OptIn(DelicateCoroutinesApi::class) // GlobalScope usage
353         fun queueReviewPrompt() {
354             GlobalScope.launch(Dispatchers.IO) {
355                 components.reviewPromptController.trackApplicationLaunch()
356             }
357         }
359         @OptIn(DelicateCoroutinesApi::class) // GlobalScope usage
360         fun queueRestoreLocale() {
361             components.performance.visualCompletenessQueue.queue.runIfReadyOrQueue {
362                 GlobalScope.launch(Dispatchers.IO) {
363                     components.useCases.localeUseCases.restore()
364                 }
365             }
366         }
368         fun queueStorageMaintenance() {
369             if (FeatureFlags.storageMaintenanceFeature) {
370                 queue.runIfReadyOrQueue {
371                     // Make sure GlobalPlacesDependencyProvider.initialize(components.core.historyStorage)
372                     // is called before this call. When app is not running and WorkManager wakes up
373                     // the app for the periodic task, it will require a globally provided places storage
374                     // to run the maintenance on.
375                     components.core.historyStorage.registerStorageMaintenanceWorker()
376                 }
377             }
378         }
380         @OptIn(DelicateCoroutinesApi::class) // GlobalScope usage
381         fun queueNimbusFetchInForeground() {
382             queue.runIfReadyOrQueue {
383                 GlobalScope.launch(Dispatchers.IO) {
384                     components.analytics.experiments.maybeFetchExperiments(
385                         context = this@FenixApplication,
386                     )
387                 }
388             }
389         }
391         initQueue()
393         // We init these items in the visual completeness queue to avoid them initing in the critical
394         // startup path, before the UI finishes drawing (i.e. visual completeness).
395         queueInitStorageAndServices()
396         queueMetrics()
397         queueReviewPrompt()
398         queueRestoreLocale()
399         queueStorageMaintenance()
400         queueNimbusFetchInForeground()
401     }
403     private fun startMetricsIfEnabled() {
404         if (settings().isTelemetryEnabled) {
405             components.analytics.metrics.start(MetricServiceType.Data)
406         }
408         if (settings().isMarketingTelemetryEnabled) {
409             components.analytics.metrics.start(MetricServiceType.Marketing)
410         }
411     }
413     protected open fun setupLeakCanary() {
414         // no-op, LeakCanary is disabled by default
415     }
417     open fun updateLeakCanaryState(isEnabled: Boolean) {
418         // no-op, LeakCanary is disabled by default
419     }
421     private fun setupPush() {
422         // Sets the PushFeature as the singleton instance for push messages to go to.
423         // We need the push feature setup here to deliver messages in the case where the service
424         // starts up the app first.
425         components.push.feature?.let {
426             Logger.info("AutoPushFeature is configured, initializing it...")
428             // Install the AutoPush singleton to receive messages.
429             PushProcessor.install(it)
431             WebPushEngineIntegration(components.core.engine, it).start()
433             // Perform a one-time initialization of the account manager if a message is received.
434             PushFxaIntegration(it, lazy { components.backgroundServices.accountManager }).launch()
436             // Initialize the service. This could potentially be done in a coroutine in the future.
437             it.initialize()
438         }
439     }
441     private fun setupCrashReporting() {
442         components
443             .analytics
444             .crashReporter
445             .install(this)
446     }
448     protected open fun initializeNimbus() {
449         beginSetupMegazord()
451         // This lazily constructs the Nimbus object…
452         val nimbus = components.analytics.experiments
453         // … which we then can populate the feature configuration.
454         FxNimbus.initialize { nimbus }
455     }
457     /**
458      * Initiate Megazord sequence! Megazord Battle Mode!
459      *
460      * The application-services combined libraries are known as the "megazord". We use the default `full`
461      * megazord - it contains everything that fenix needs, and (currently) nothing more.
462      *
463      * Documentation on what megazords are, and why they're needed:
464      * - https://github.com/mozilla/application-services/blob/master/docs/design/megazords.md
465      * - https://mozilla.github.io/application-services/docs/applications/consuming-megazord-libraries.html
466      *
467      * This is the initialization of the megazord without setting up networking, i.e. needing the
468      * engine for networking. This should do the minimum work necessary as it is done on the main
469      * thread, early in the app startup sequence.
470      */
471     private fun beginSetupMegazord() {
472         // Note: Megazord.init() must be called as soon as possible ...
473         Megazord.init()
475         initializeRustErrors(components.analytics.crashReporter)
476         // ... but RustHttpConfig.setClient() and RustLog.enable() can be called later.
478         // Once application-services has switched to using the new
479         // error reporting system, RustLog shouldn't input a CrashReporter
480         // anymore.
481         // (https://github.com/mozilla/application-services/issues/4981).
482         RustLog.enable(components.analytics.crashReporter)
483     }
485     @OptIn(DelicateCoroutinesApi::class) // GlobalScope usage
486     private fun finishSetupMegazord(): Deferred<Unit> {
487         return GlobalScope.async(Dispatchers.IO) {
488             if (Config.channel.isDebug) {
489                 RustHttpConfig.allowEmulatorLoopback()
490             }
491             RustHttpConfig.setClient(lazy { components.core.client })
493             // Now viaduct (the RustHttp client) is initialized we can ask Nimbus to fetch
494             // experiments recipes from the server.
495         }
496     }
498     private fun restoreMessaging() {
499         if (settings().isExperimentationEnabled) {
500             components.appStore.dispatch(AppAction.MessagingAction.Restore)
501         }
502     }
504     override fun onTrimMemory(level: Int) {
505         super.onTrimMemory(level)
507         // Additional logging and breadcrumb to debug memory issues:
508         // https://github.com/mozilla-mobile/fenix/issues/12731
510         logger.info("onTrimMemory(), level=$level, main=${isMainProcess()}")
512         components.analytics.crashReporter.recordCrashBreadcrumb(
513             Breadcrumb(
514                 category = "Memory",
515                 message = "onTrimMemory()",
516                 data = mapOf(
517                     "level" to level.toString(),
518                     "main" to isMainProcess().toString(),
519                 ),
520                 level = Breadcrumb.Level.INFO,
521             ),
522         )
524         runOnlyInMainProcess {
525             components.core.icons.onTrimMemory(level)
526             components.core.store.dispatch(SystemAction.LowMemoryAction(level))
527         }
528     }
530     @SuppressLint("WrongConstant")
531     // Suppressing erroneous lint warning about using MODE_NIGHT_AUTO_BATTERY, a likely library bug
532     private fun setDayNightTheme() {
533         val settings = this.settings()
534         when {
535             settings.shouldUseLightTheme -> {
536                 AppCompatDelegate.setDefaultNightMode(
537                     AppCompatDelegate.MODE_NIGHT_NO,
538                 )
539             }
540             settings.shouldUseDarkTheme -> {
541                 AppCompatDelegate.setDefaultNightMode(
542                     AppCompatDelegate.MODE_NIGHT_YES,
543                 )
544             }
545             SDK_INT < Build.VERSION_CODES.P && settings.shouldUseAutoBatteryTheme -> {
546                 AppCompatDelegate.setDefaultNightMode(
547                     AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY,
548                 )
549             }
550             SDK_INT >= Build.VERSION_CODES.P && settings.shouldFollowDeviceTheme -> {
551                 AppCompatDelegate.setDefaultNightMode(
552                     AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM,
553                 )
554             }
555             // First run of app no default set, set the default to Follow System for 28+ and Normal Mode otherwise
556             else -> {
557                 if (SDK_INT >= Build.VERSION_CODES.P) {
558                     AppCompatDelegate.setDefaultNightMode(
559                         AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM,
560                     )
561                     settings.shouldFollowDeviceTheme = true
562                 } else {
563                     AppCompatDelegate.setDefaultNightMode(
564                         AppCompatDelegate.MODE_NIGHT_NO,
565                     )
566                     settings.shouldUseLightTheme = true
567                 }
568             }
569         }
570     }
572     /**
573      * If unified search is enabled try to migrate the topic specific engine to the
574      * first general or custom search engine available.
575      */
576     @Suppress("NestedBlockDepth")
577     private fun migrateTopicSpecificSearchEngines() {
578         if (settings().showUnifiedSearchFeature) {
579             components.core.store.state.search.selectedOrDefaultSearchEngine.let { currentSearchEngine ->
580                 if (currentSearchEngine?.isGeneral == false) {
581                     components.core.store.state.search.searchEngines.firstOrNull() { nextSearchEngine ->
582                         nextSearchEngine.isGeneral
583                     }?.let {
584                         components.useCases.searchUseCases.selectSearchEngine(it)
585                     }
586                 }
587             }
588         }
589     }
591     @OptIn(DelicateCoroutinesApi::class) // GlobalScope usage
592     private fun warmBrowsersCache() {
593         // We avoid blocking the main thread for BrowsersCache on startup by loading it on
594         // background thread.
595         GlobalScope.launch(Dispatchers.Default) {
596             BrowsersCache.all(this@FenixApplication)
597         }
598     }
600     private fun initializeWebExtensionSupport() {
601         try {
602             GlobalAddonDependencyProvider.initialize(
603                 components.addonManager,
604                 components.addonUpdater,
605                 onCrash = { exception ->
606                     components.analytics.crashReporter.submitCaughtException(exception)
607                 },
608             )
609             WebExtensionSupport.initialize(
610                 components.core.engine,
611                 components.core.store,
612                 onNewTabOverride = { _, engineSession, url ->
613                     val shouldCreatePrivateSession =
614                         components.core.store.state.selectedTab?.content?.private
615                             ?: components.settings.openLinksInAPrivateTab
617                     components.useCases.tabsUseCases.addTab(
618                         url = url,
619                         selectTab = true,
620                         engineSession = engineSession,
621                         private = shouldCreatePrivateSession,
622                     )
623                 },
624                 onCloseTabOverride = { _, sessionId ->
625                     components.useCases.tabsUseCases.removeTab(sessionId)
626                 },
627                 onSelectTabOverride = { _, sessionId ->
628                     components.useCases.tabsUseCases.selectTab(sessionId)
629                 },
630                 onExtensionsLoaded = { extensions ->
631                     components.addonUpdater.registerForFutureUpdates(extensions)
632                     subscribeForNewAddonsIfNeeded(components.supportedAddonsChecker, extensions)
633                 },
634                 onUpdatePermissionRequest = components.addonUpdater::onUpdatePermissionRequest,
635             )
636         } catch (e: UnsupportedOperationException) {
637             Logger.error("Failed to initialize web extension support", e)
638         }
639     }
641     @VisibleForTesting
642     internal fun subscribeForNewAddonsIfNeeded(
643         checker: DefaultSupportedAddonsChecker,
644         installedExtensions: List<WebExtension>,
645     ) {
646         val hasUnsupportedAddons = installedExtensions.any { it.isUnsupported() }
647         if (hasUnsupportedAddons) {
648             checker.registerForChecks()
649         } else {
650             // As checks are a persistent subscriptions, we have to make sure
651             // we remove any previous subscriptions.
652             checker.unregisterForChecks()
653         }
654     }
656     /**
657      * This function is called right after Glean is initialized. Part of this function depends on
658      * shared preferences to be updated so the correct value is sent with the metrics ping.
659      *
660      * The reason we're using shared preferences to track these values is due to the limitations of
661      * the current metrics ping design. The values set here will be sent in every metrics ping even
662      * if these values have not changed since the last startup.
663      */
664     @Suppress("ComplexMethod", "LongMethod")
665     @VisibleForTesting
666     internal fun setStartupMetrics(
667         browserStore: BrowserStore,
668         settings: Settings,
669         browsersCache: BrowsersCache = BrowsersCache,
670         mozillaProductDetector: MozillaProductDetector = MozillaProductDetector,
671     ) {
672         setPreferenceMetrics(settings)
673         with(Metrics) {
674             // Set this early to guarantee it's in every ping from here on.
675             distributionId.set(
676                 when (Config.channel.isMozillaOnline) {
677                     true -> "MozillaOnline"
678                     false -> "Mozilla"
679                 },
680             )
682             defaultBrowser.set(browsersCache.all(applicationContext).isDefaultBrowser)
683             mozillaProductDetector.getMozillaBrowserDefault(applicationContext)?.also {
684                 defaultMozBrowser.set(it)
685             }
687             if (settings.contileContextId.isEmpty()) {
688                 settings.contileContextId = TopSites.contextId.generateAndSet().toString()
689             } else {
690                 TopSites.contextId.set(UUID.fromString(settings.contileContextId))
691             }
693             mozillaProducts.set(
694                 mozillaProductDetector.getInstalledMozillaProducts(
695                     applicationContext,
696                 ),
697             )
699             adjustCampaign.set(settings.adjustCampaignId)
700             adjustAdGroup.set(settings.adjustAdGroup)
701             adjustCreative.set(settings.adjustCreative)
702             adjustNetwork.set(settings.adjustNetwork)
704             searchWidgetInstalled.set(settings.searchWidgetInstalled)
706             val openTabsCount = settings.openTabsCount
707             hasOpenTabs.set(openTabsCount > 0)
708             if (openTabsCount > 0) {
709                 tabsOpenCount.add(openTabsCount)
710             }
712             val topSitesSize = settings.topSitesSize
713             hasTopSites.set(topSitesSize > 0)
714             if (topSitesSize > 0) {
715                 topSitesCount.add(topSitesSize)
716             }
718             val installedAddonSize = settings.installedAddonsCount
719             Addons.hasInstalledAddons.set(installedAddonSize > 0)
720             if (installedAddonSize > 0) {
721                 Addons.installedAddons.set(settings.installedAddonsList.split(','))
722             }
724             val enabledAddonSize = settings.enabledAddonsCount
725             Addons.hasEnabledAddons.set(enabledAddonSize > 0)
726             if (enabledAddonSize > 0) {
727                 Addons.enabledAddons.set(settings.enabledAddonsList.split(','))
728             }
730             val desktopBookmarksSize = settings.desktopBookmarksSize
731             hasDesktopBookmarks.set(desktopBookmarksSize > 0)
732             if (desktopBookmarksSize > 0) {
733                 desktopBookmarksCount.add(desktopBookmarksSize)
734             }
736             val mobileBookmarksSize = settings.mobileBookmarksSize
737             hasMobileBookmarks.set(mobileBookmarksSize > 0)
738             if (mobileBookmarksSize > 0) {
739                 mobileBookmarksCount.add(mobileBookmarksSize)
740             }
742             toolbarPosition.set(
743                 when (settings.toolbarPosition) {
744                     ToolbarPosition.BOTTOM -> CustomizationFragment.Companion.Position.BOTTOM.name
745                     ToolbarPosition.TOP -> CustomizationFragment.Companion.Position.TOP.name
746                 },
747             )
749             tabViewSetting.set(settings.getTabViewPingString())
750             closeTabSetting.set(settings.getTabTimeoutPingString())
752             val installSourcePackage = if (SDK_INT >= Build.VERSION_CODES.R) {
753                 packageManager.getInstallSourceInfo(packageName).installingPackageName
754             } else {
755                 @Suppress("DEPRECATION")
756                 packageManager.getInstallerPackageName(packageName)
757             }
758             installSource.set(installSourcePackage.orEmpty())
760             val isDefaultTheCurrentWallpaper =
761                 Wallpaper.nameIsDefault(settings.currentWallpaperName)
763             defaultWallpaper.set(isDefaultTheCurrentWallpaper)
765             val notificationManagerCompat = NotificationManagerCompat.from(applicationContext)
766             notificationsAllowed.set(notificationManagerCompat.areNotificationsEnabledSafe())
767             marketingNotificationAllowed.set(
768                 notificationManagerCompat.isNotificationChannelEnabled(MARKETING_CHANNEL_ID),
769             )
770         }
772         with(AndroidAutofill) {
773             val autofillUseCases = AutofillUseCases()
774             supported.set(autofillUseCases.isSupported(applicationContext))
775             enabled.set(autofillUseCases.isEnabled(applicationContext))
776         }
778         browserStore.waitForSelectedOrDefaultSearchEngine { searchEngine ->
779             searchEngine?.let {
780                 val sendSearchUrl =
781                     !searchEngine.isCustomEngine() || searchEngine.isKnownSearchDomain()
782                 if (sendSearchUrl) {
783                     SearchDefaultEngine.apply {
784                         code.set(searchEngine.id)
785                         name.set(searchEngine.name)
786                         searchUrl.set(searchEngine.buildSearchUrl(""))
787                     }
788                 } else {
789                     SearchDefaultEngine.apply {
790                         code.set(searchEngine.id)
791                         name.set("custom")
792                     }
793                 }
795                 migrateTopicSpecificSearchEngines()
796             }
797         }
798     }
800     @Suppress("ComplexMethod")
801     private fun setPreferenceMetrics(
802         settings: Settings,
803     ) {
804         with(Preferences) {
805             searchSuggestionsEnabled.set(settings.shouldShowSearchSuggestions)
806             remoteDebuggingEnabled.set(settings.isRemoteDebuggingEnabled)
807             studiesEnabled.set(settings.isExperimentationEnabled)
808             telemetryEnabled.set(settings.isTelemetryEnabled)
809             browsingHistorySuggestion.set(settings.shouldShowHistorySuggestions)
810             bookmarksSuggestion.set(settings.shouldShowBookmarkSuggestions)
811             clipboardSuggestionsEnabled.set(settings.shouldShowClipboardSuggestions)
812             searchShortcutsEnabled.set(settings.shouldShowSearchShortcuts)
813             voiceSearchEnabled.set(settings.shouldShowVoiceSearch)
814             openLinksInAppEnabled.set(settings.openLinksInExternalApp)
815             signedInSync.set(settings.signedInFxaAccount)
817             val syncedItems = SyncEnginesStorage(applicationContext).getStatus().entries.filter {
818                 it.value
819             }.map { it.key.nativeName }
820             syncItems.set(syncedItems)
822             toolbarPositionSetting.set(
823                 when {
824                     settings.shouldUseFixedTopToolbar -> "fixed_top"
825                     settings.shouldUseBottomToolbar -> "bottom"
826                     else -> "top"
827                 },
828             )
830             enhancedTrackingProtection.set(
831                 when {
832                     !settings.shouldUseTrackingProtection -> ""
833                     settings.useStandardTrackingProtection -> "standard"
834                     settings.useStrictTrackingProtection -> "strict"
835                     settings.useCustomTrackingProtection -> "custom"
836                     else -> ""
837                 },
838             )
839             etpCustomCookiesSelection.set(settings.blockCookiesSelectionInCustomTrackingProtection)
841             val accessibilitySelection = mutableListOf<String>()
843             if (settings.switchServiceIsEnabled) {
844                 accessibilitySelection.add("switch")
845             }
847             if (settings.touchExplorationIsEnabled) {
848                 accessibilitySelection.add("touch exploration")
849             }
851             accessibilityServices.set(accessibilitySelection.toList())
853             userTheme.set(
854                 when {
855                     settings.shouldUseLightTheme -> "light"
856                     settings.shouldUseDarkTheme -> "dark"
857                     settings.shouldFollowDeviceTheme -> "system"
858                     settings.shouldUseAutoBatteryTheme -> "battery"
859                     else -> ""
860                 },
861             )
863             inactiveTabsEnabled.set(settings.inactiveTabsAreEnabled)
864         }
865         reportHomeScreenMetrics(settings)
866     }
868     @VisibleForTesting
869     internal fun reportHomeScreenMetrics(settings: Settings) {
870         reportOpeningScreenMetrics(settings)
871         reportHomeScreenSectionMetrics(settings)
872     }
874     private fun reportOpeningScreenMetrics(settings: Settings) {
875         CustomizeHome.openingScreen.set(
876             when {
877                 settings.alwaysOpenTheHomepageWhenOpeningTheApp -> "homepage"
878                 settings.alwaysOpenTheLastTabWhenOpeningTheApp -> "last tab"
879                 settings.openHomepageAfterFourHoursOfInactivity -> "homepage after four hours"
880                 else -> ""
881             },
882         )
883     }
885     private fun reportHomeScreenSectionMetrics(settings: Settings) {
886         // These settings are backed by Nimbus features.
887         // We break them out here so they can be recorded when
888         // `nimbus.applyPendingExperiments()` is called.
889         CustomizeHome.jumpBackIn.set(settings.showRecentTabsFeature)
890         CustomizeHome.recentlySaved.set(settings.showRecentBookmarksFeature)
891         CustomizeHome.mostVisitedSites.set(settings.showTopSitesFeature)
892         CustomizeHome.recentlyVisited.set(settings.historyMetadataUIFeature)
893         CustomizeHome.pocket.set(settings.showPocketRecommendationsFeature)
894         CustomizeHome.sponsoredPocket.set(settings.showPocketSponsoredStories)
895         CustomizeHome.contile.set(settings.showContileFeature)
896     }
898     protected fun recordOnInit() {
899         // This gets called by more than one process. Ideally we'd only run this in the main process
900         // but the code to check which process we're in crashes because the Context isn't valid yet.
901         //
902         // This method is not covered by our internal crash reporting: be very careful when modifying it.
903         StartupTimeline.onApplicationInit() // DO NOT MOVE ANYTHING ABOVE HERE: the timing is critical.
904     }
906     override fun onConfigurationChanged(config: android.content.res.Configuration) {
907         // Workaround for androidx appcompat issue where follow system day/night mode config changes
908         // are not triggered when also using createConfigurationContext like we do in LocaleManager
909         // https://issuetracker.google.com/issues/143570309#comment3
910         applicationContext.resources.configuration.uiMode = config.uiMode
912         if (isMainProcess()) {
913             // We can only do this on the main process as resetAfter will access components.core, which
914             // will initialize the engine and create an additional GeckoRuntime from the Gecko
915             // child process, causing a crash.
917             // There's a strict mode violation in A-Cs LocaleAwareApplication which
918             // reads from shared prefs: https://github.com/mozilla-mobile/android-components/issues/8816
919             components.strictMode.resetAfter(StrictMode.allowThreadDiskReads()) {
920                 super.onConfigurationChanged(config)
921             }
922         } else {
923             super.onConfigurationChanged(config)
924         }
925     }
927     override fun getWorkManagerConfiguration() = Builder().setMinimumLoggingLevel(INFO).build()
929     @OptIn(DelicateCoroutinesApi::class)
930     open fun downloadWallpapers() {
931         GlobalScope.launch {
932             components.useCases.wallpaperUseCases.initialize()
933         }
934     }