You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
iceraven-browser/app/src/main/java/org/mozilla/fenix/FenixApplication.kt

497 lines
21 KiB
Kotlin

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix
import android.annotation.SuppressLint
import android.os.Build
import android.os.Build.VERSION.SDK_INT
import android.os.StrictMode
import android.util.Log.INFO
import androidx.annotation.CallSuper
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.content.getSystemService
import androidx.work.Configuration.Builder
import androidx.work.Configuration.Provider
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
import mozilla.appservices.Megazord
import mozilla.components.browser.state.action.SystemAction
import mozilla.components.browser.state.selector.selectedTab
import mozilla.components.concept.base.crash.Breadcrumb
import mozilla.components.concept.push.PushProcessor
import mozilla.components.feature.addons.update.GlobalAddonDependencyProvider
import mozilla.components.lib.crash.CrashReporter
import mozilla.components.service.glean.Glean
import mozilla.components.service.glean.config.Configuration
import mozilla.components.service.glean.net.ConceptFetchHttpUploader
import mozilla.components.support.base.facts.register
import mozilla.components.support.base.log.Log
import mozilla.components.support.base.log.logger.Logger
import mozilla.components.support.ktx.android.content.isMainProcess
import mozilla.components.support.ktx.android.content.runOnlyInMainProcess
import mozilla.components.support.locale.LocaleAwareApplication
import mozilla.components.support.rusthttp.RustHttpConfig
import mozilla.components.support.rustlog.RustLog
import mozilla.components.support.utils.logElapsedTime
import mozilla.components.support.webextensions.WebExtensionSupport
import org.mozilla.fenix.GleanMetrics.PerfStartup
import org.mozilla.fenix.components.Components
import org.mozilla.fenix.components.metrics.MetricServiceType
import org.mozilla.fenix.components.metrics.SecurePrefsTelemetry
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.perf.ProfilerMarkerFactProcessor
import org.mozilla.fenix.perf.StartupTimeline
import org.mozilla.fenix.perf.StorageStatsMetrics
import org.mozilla.fenix.perf.runBlockingIncrement
import org.mozilla.fenix.push.PushFxaIntegration
import org.mozilla.fenix.push.WebPushEngineIntegration
import org.mozilla.fenix.session.PerformanceActivityLifecycleCallbacks
import org.mozilla.fenix.session.VisibilityLifecycleCallback
import org.mozilla.fenix.utils.BrowsersCache
import java.util.concurrent.TimeUnit
/**
*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.
*/
@Suppress("Registered", "TooManyFunctions", "LargeClass")
open class FenixApplication : LocaleAwareApplication(), Provider {
init {
recordOnInit() // DO NOT MOVE ANYTHING ABOVE HERE: the timing of this measurement is critical.
}
private val logger = Logger("FenixApplication")
open val components by lazy { Components(this) }
var visibilityLifecycleCallback: VisibilityLifecycleCallback? = null
private set
override fun onCreate() {
val methodDurationTimerId = PerfStartup.applicationOnCreate.start() // DO NOT MOVE ANYTHING ABOVE HERE.
super.onCreate()
setupInAllProcesses()
if (!isMainProcess()) {
// 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
// situation where we create a GeckoRuntime from the Gecko child process.
return
}
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()
}
setupInMainProcessOnly()
// We use start/stop instead of measure so we don't measure outside the main process.
PerfStartup.applicationOnCreate.stopAndAccumulate(methodDurationTimerId) // DO NOT MOVE ANYTHING BELOW HERE.
}
protected open fun initializeGlean() {
val telemetryEnabled = settings().isTelemetryEnabled
logger.debug("Initializing Glean (uploadEnabled=$telemetryEnabled, isFennec=${Config.channel.isFennec})")
Glean.initialize(
applicationContext = this,
configuration = Configuration(
channel = BuildConfig.BUILD_TYPE,
httpClient = ConceptFetchHttpUploader(
lazy(LazyThreadSafetyMode.NONE) { components.core.client }
)),
uploadEnabled = telemetryEnabled
)
}
@CallSuper
open fun setupInAllProcesses() {
setupCrashReporting()
// We want the log messages of all builds to go to Android logcat
Log.addSink(FenixLogSink(logsDebug = Config.channel.isDebug))
}
@CallSuper
open fun setupInMainProcessOnly() {
ProfilerMarkerFactProcessor.create { components.core.engine.profiler }.register()
run {
// Attention: Do not invoke any code from a-s in this scope.
val megazordSetup = setupMegazord()
setDayNightTheme()
components.strictMode.enableStrictMode(true)
warmBrowsersCache()
// Make sure the engine is initialized and ready to use.
components.strictMode.resetAfter(StrictMode.allowThreadDiskReads()) {
components.core.engine.warmUp()
}
initializeWebExtensionSupport()
restoreBrowserState()
restoreDownloads()
// 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) {
runBlockingIncrement { megazordSetup.await() }
}
}
setupLeakCanary()
startMetricsIfEnabled()
setupPush()
visibilityLifecycleCallback = VisibilityLifecycleCallback(getSystemService())
registerActivityLifecycleCallbacks(visibilityLifecycleCallback)
// 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()
// }
initVisualCompletenessQueueAndQueueTasks()
components.appStartupTelemetry.onFenixApplicationOnCreate()
}
private fun restoreBrowserState() = GlobalScope.launch(Dispatchers.Main) {
val store = components.core.store
val sessionStorage = components.core.sessionStorage
components.useCases.tabsUseCases.restore(sessionStorage, settings().getTabTimeout())
// Now that we have restored our previous state (if there's one) let's setup auto saving the state while
// the app is used.
sessionStorage.autoSave(store)
.periodicallyInForeground(interval = 30, unit = TimeUnit.SECONDS)
.whenGoingToBackground()
.whenSessionsChange()
}
private fun restoreDownloads() {
components.useCases.downloadUseCases.restoreDownloads()
}
private fun initVisualCompletenessQueueAndQueueTasks() {
val queue = components.performance.visualCompletenessQueue.queue
fun initQueue() {
registerActivityLifecycleCallbacks(PerformanceActivityLifecycleCallbacks(queue))
}
fun queueInitStorageAndServices() {
components.performance.visualCompletenessQueue.queue.runIfReadyOrQueue {
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()
}
SecurePrefsTelemetry(this@FenixApplication, components.analytics.experiments).startTests()
}
// Account manager initialization needs to happen on the main thread.
GlobalScope.launch(Dispatchers.Main) {
logElapsedTime(logger, "Kicking-off account manager") {
components.backgroundServices.accountManager
}
}
}
}
fun queueMetrics() {
if (SDK_INT >= Build.VERSION_CODES.O) { // required by StorageStatsMetrics.
queue.runIfReadyOrQueue {
// 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)
}
}
}
fun queueReviewPrompt() {
GlobalScope.launch(Dispatchers.IO) {
components.reviewPromptController.trackApplicationLaunch()
}
}
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).
queueInitStorageAndServices()
queueMetrics()
queueReviewPrompt()
}
private fun startMetricsIfEnabled() {
if (settings().isTelemetryEnabled) {
components.analytics.metrics.start(MetricServiceType.Data)
}
if (settings().isMarketingTelemetryEnabled) {
components.analytics.metrics.start(MetricServiceType.Marketing)
}
}
// 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.
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()
}
settings().lastPlacesStorageMaintenance = System.currentTimeMillis()
}
protected open fun setupLeakCanary() {
// no-op, LeakCanary is disabled by default
}
open fun updateLeakCanaryState(isEnabled: Boolean) {
// no-op, LeakCanary is disabled by default
}
private fun setupPush() {
// 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.
components.push.feature?.let {
Logger.info("AutoPushFeature is configured, initializing it...")
// Install the AutoPush singleton to receive messages.
PushProcessor.install(it)
WebPushEngineIntegration(components.core.engine, it).start()
// Perform a one-time initialization of the account manager if a message is received.
PushFxaIntegration(it, lazy { components.backgroundServices.accountManager }).launch()
// Initialize the service. This could potentially be done in a coroutine in the future.
it.initialize()
}
}
private fun setupCrashReporting() {
components
.analytics
.crashReporter
.install(this)
}
/**
* Initiate Megazord sequence! Megazord Battle Mode!
*
* 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:
* - https://github.com/mozilla/application-services/blob/master/docs/design/megazords.md
* - https://mozilla.github.io/application-services/docs/applications/consuming-megazord-libraries.html
*/
private fun setupMegazord(): Deferred<Unit> {
// Note: Megazord.init() must be called as soon as possible ...
Megazord.init()
return GlobalScope.async(Dispatchers.IO) {
// ... but RustHttpConfig.setClient() and RustLog.enable() can be called later.
RustHttpConfig.setClient(lazy { components.core.client })
RustLog.enable(components.analytics.crashReporter)
// We want to ensure Nimbus is initialized as early as possible so we can
// experiment on features close to startup.
// But we need viaduct (the RustHttp client) to be ready before we do.
components.analytics.experiments.initialize()
}
}
override fun onTrimMemory(level: Int) {
super.onTrimMemory(level)
// Additional logging and breadcrumb to debug memory issues:
// https://github.com/mozilla-mobile/fenix/issues/12731
logger.info("onTrimMemory(), level=$level, main=${isMainProcess()}")
components.analytics.crashReporter.recordCrashBreadcrumb(Breadcrumb(
category = "Memory",
message = "onTrimMemory()",
data = mapOf(
"level" to level.toString(),
"main" to isMainProcess().toString()
),
level = Breadcrumb.Level.INFO
))
runOnlyInMainProcess {
components.core.icons.onTrimMemory(level)
// We want to be judicious in passing low mamory messages to
// android-components, because it is (at time of writing) hardcoded
// to drop tab states (and any user data in them) as soon as we
// reach "moderate" memory pressure on the system, even if the
// browser is in no danger of being killed. See
// https://github.com/mozilla-mobile/android-components/blob/38186676d46c555b5a24268e5fa361e45e57102c/components/browser/session/src/main/java/mozilla/components/browser/session/engine/middleware/TrimMemoryMiddleware.kt#L53-L64
// for the relvant android-components code and
// https://stuff.mit.edu/afs/sipb/project/android/docs/reference/android/content/ComponentCallbacks2.html
// for the list of memory pressure levels.
val settings = this.settings()
if (settings.shouldRelinquishMemoryUnderPressure) {
// We will give up our RAM when asked nicely
components.core.store.dispatch(SystemAction.LowMemoryAction(level))
}
// Otherwise we will die for our RAM, if pressed.
}
}
@SuppressLint("WrongConstant")
// Suppressing erroneous lint warning about using MODE_NIGHT_AUTO_BATTERY, a likely library bug
private fun setDayNightTheme() {
val settings = this.settings()
when {
settings.shouldUseLightTheme -> {
AppCompatDelegate.setDefaultNightMode(
AppCompatDelegate.MODE_NIGHT_NO
)
}
settings.shouldUseDarkTheme -> {
AppCompatDelegate.setDefaultNightMode(
AppCompatDelegate.MODE_NIGHT_YES
)
}
SDK_INT < Build.VERSION_CODES.P && settings.shouldUseAutoBatteryTheme -> {
AppCompatDelegate.setDefaultNightMode(
AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY
)
}
SDK_INT >= Build.VERSION_CODES.P && settings.shouldFollowDeviceTheme -> {
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 -> {
if (SDK_INT >= Build.VERSION_CODES.P) {
AppCompatDelegate.setDefaultNightMode(
AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
)
settings.shouldFollowDeviceTheme = true
} else {
AppCompatDelegate.setDefaultNightMode(
AppCompatDelegate.MODE_NIGHT_NO
)
settings.shouldUseLightTheme = true
}
}
}
}
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)
}
}
private fun initializeWebExtensionSupport() {
try {
GlobalAddonDependencyProvider.initialize(
components.addonManager,
components.addonUpdater,
onCrash = { exception ->
components.analytics.crashReporter.submitCaughtException(exception)
}
)
WebExtensionSupport.initialize(
components.core.engine,
components.core.store,
onNewTabOverride = {
_, engineSession, url ->
val shouldCreatePrivateSession =
components.core.store.state.selectedTab?.content?.private
?: components.settings.openLinksInAPrivateTab
if (shouldCreatePrivateSession) {
components.useCases.tabsUseCases.addPrivateTab(
url = url,
selectTab = true,
engineSession = engineSession
)
} else {
components.useCases.tabsUseCases.addTab(
url = url,
selectTab = true,
engineSession = engineSession
)
}
},
onCloseTabOverride = {
_, sessionId -> components.useCases.tabsUseCases.removeTab(sessionId)
},
onSelectTabOverride = {
_, sessionId -> components.useCases.tabsUseCases.selectTab(sessionId)
},
onExtensionsLoaded = { extensions ->
components.addonUpdater.registerForFutureUpdates(extensions)
components.supportedAddonsChecker.registerForChecks()
},
onUpdatePermissionRequest = components.addonUpdater::onUpdatePermissionRequest
)
} catch (e: UnsupportedOperationException) {
Logger.error("Failed to initialize web extension support", e)
}
}
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.
//
// 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.
}
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
if (isMainProcess()) {
// We can only do this on the main process as resetAfter will access components.core, which
// will initialize the engine and create an additional GeckoRuntime from the Gecko
// child process, causing a crash.
// There's a strict mode violation in A-Cs LocaleAwareApplication which
// reads from shared prefs: https://github.com/mozilla-mobile/android-components/issues/8816
components.strictMode.resetAfter(StrictMode.allowThreadDiskReads()) {
super.onConfigurationChanged(config)
}
} else {
super.onConfigurationChanged(config)
}
}
override fun getWorkManagerConfiguration() = Builder().setMinimumLoggingLevel(INFO).build()
}