1
0
Fork 0
fenix/app/src/main/java/org/mozilla/fenix/HomeActivity.kt

420 lines
16 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.content.Context
import android.content.Intent
import android.content.res.Configuration
import android.os.Bundle
import android.util.AttributeSet
import android.view.View
import androidx.annotation.CallSuper
import androidx.annotation.IdRes
import androidx.annotation.VisibleForTesting
import androidx.annotation.VisibleForTesting.PROTECTED
import androidx.appcompat.app.ActionBar
import androidx.appcompat.widget.Toolbar
import androidx.lifecycle.lifecycleScope
import androidx.navigation.NavDestination
import androidx.navigation.NavDirections
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.ui.AppBarConfiguration
import androidx.navigation.ui.NavigationUI
import kotlinx.android.synthetic.main.activity_home.navigationToolbarStub
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import mozilla.components.browser.search.SearchEngine
import mozilla.components.browser.session.Session
import mozilla.components.browser.session.SessionManager
import mozilla.components.browser.state.state.WebExtensionState
import mozilla.components.concept.engine.EngineView
import mozilla.components.feature.contextmenu.ext.DefaultSelectionActionDelegate
import mozilla.components.service.fxa.sync.SyncReason
import mozilla.components.support.base.feature.UserInteractionHandler
import mozilla.components.support.ktx.kotlin.isUrl
import mozilla.components.support.ktx.kotlin.toNormalizedUrl
import mozilla.components.support.locale.LocaleAwareAppCompatActivity
import mozilla.components.support.utils.SafeIntent
import mozilla.components.support.utils.toSafeIntent
import mozilla.components.support.webextensions.WebExtensionPopupFeature
import org.mozilla.fenix.browser.UriOpenedObserver
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
import org.mozilla.fenix.browser.browsingmode.BrowsingModeListener
import org.mozilla.fenix.components.metrics.BreadcrumbsRecorder
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.exceptions.ExceptionsFragmentDirections
import org.mozilla.fenix.ext.alreadyOnDestination
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.nav
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.home.HomeFragmentDirections
import org.mozilla.fenix.home.intent.CrashReporterIntentProcessor
import org.mozilla.fenix.home.intent.DeepLinkIntentProcessor
import org.mozilla.fenix.home.intent.OpenBrowserIntentProcessor
import org.mozilla.fenix.home.intent.SpeechProcessingIntentProcessor
import org.mozilla.fenix.home.intent.StartSearchIntentProcessor
import org.mozilla.fenix.library.bookmarks.BookmarkFragmentDirections
import org.mozilla.fenix.library.history.HistoryFragmentDirections
import org.mozilla.fenix.perf.HotStartPerformanceMonitor
import org.mozilla.fenix.perf.Performance
import org.mozilla.fenix.search.SearchFragmentDirections
import org.mozilla.fenix.settings.DefaultBrowserSettingsFragmentDirections
import org.mozilla.fenix.settings.SettingsFragmentDirections
import org.mozilla.fenix.settings.TrackingProtectionFragmentDirections
import org.mozilla.fenix.settings.about.AboutFragmentDirections
import org.mozilla.fenix.settings.logins.SavedLoginsFragmentDirections
import org.mozilla.fenix.theme.DefaultThemeManager
import org.mozilla.fenix.theme.ThemeManager
import org.mozilla.fenix.utils.BrowsersCache
@SuppressWarnings("TooManyFunctions", "LargeClass")
open class HomeActivity : LocaleAwareAppCompatActivity() {
private var webExtScope: CoroutineScope? = null
lateinit var themeManager: ThemeManager
private val browsingModeManager get() = components.browsingModeManager
private var sessionObserver: SessionManager.Observer? = null
private val hotStartMonitor = HotStartPerformanceMonitor()
private var isToolbarInflated = false
private val webExtensionPopupFeature by lazy {
WebExtensionPopupFeature(components.core.store, ::openPopup)
}
private val navHost by lazy {
supportFragmentManager.findFragmentById(R.id.container) as NavHostFragment
}
private val externalSourceIntentProcessors by lazy {
listOf(
SpeechProcessingIntentProcessor(this, components.analytics.metrics),
StartSearchIntentProcessor(components.analytics.metrics),
DeepLinkIntentProcessor(this),
OpenBrowserIntentProcessor(this, ::getIntentSessionId)
)
}
private val browsingModeListener = object : BrowsingModeListener {
override fun onBrowsingModeChange(newMode: BrowsingMode) {
themeManager.currentTheme = newMode
}
}
override fun applyOverrideConfiguration(overrideConfiguration: Configuration?) {
if (overrideConfiguration != null) {
val uiMode = overrideConfiguration.uiMode
overrideConfiguration.setTo(baseContext.resources.configuration)
overrideConfiguration.uiMode = uiMode
}
super.applyOverrideConfiguration(overrideConfiguration)
}
final override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
components.publicSuffixList.prefetch()
setupThemeAndBrowsingMode(getModeFromIntentOrLastKnown(intent))
setContentView(R.layout.activity_home)
Performance.instrumentColdStartupToHomescreenTime(this)
externalSourceIntentProcessors.any { it.process(intent, navHost.navController, this.intent) }
Performance.processIntentIfPerformanceTest(intent, this)
if (settings().isTelemetryEnabled) {
lifecycle.addObserver(BreadcrumbsRecorder(components.analytics.crashReporter,
navHost.navController, ::getBreadcrumbMessage))
intent
?.toSafeIntent()
?.let(::getIntentSource)
?.also { components.analytics.metrics.track(Event.OpenedApp(it)) }
}
supportActionBar?.hide()
lifecycle.addObserver(webExtensionPopupFeature)
}
@CallSuper
override fun onResume() {
super.onResume()
lifecycleScope.launch {
with(components.backgroundServices) {
// Make sure accountManager is initialized.
accountManager.initAsync().await()
// If we're authenticated, kick-off a sync and a device state refresh.
accountManager.authenticatedAccount()?.let {
accountManager.syncNowAsync(SyncReason.Startup, debounce = true)
}
}
}
}
final override fun onRestart() {
hotStartMonitor.onRestartFirstMethodCall()
super.onRestart()
}
final override fun onPostResume() {
super.onPostResume()
hotStartMonitor.onPostResumeFinalMethodCall()
}
final override fun onStart() {
super.onStart()
browsingModeManager.registerBrowsingModeListener(browsingModeListener)
}
final override fun onStop() {
super.onStop()
browsingModeManager.unregisterBrowsingModeListener()
}
final override fun onPause() {
super.onPause()
// Every time the application goes into the background, it is possible that the user
// is about to change the browsers installed on their system. Therefore, we reset the cache of
// all the installed browsers.
//
// NB: There are ways for the user to install new products without leaving the browser.
BrowsersCache.resetAll()
}
/**
* Handles intents received when the activity is open.
*/
final override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
intent ?: return
val intentProcessors = listOf(CrashReporterIntentProcessor()) + externalSourceIntentProcessors
intentProcessors.any { it.process(intent, navHost.navController, this.intent) }
browsingModeManager.mode = getModeFromIntentOrLastKnown(intent)
}
/**
* Overrides view inflation to inject a custom [EngineView] from [components].
*/
final override fun onCreateView(
parent: View?,
name: String,
context: Context,
attrs: AttributeSet
): View? = when (name) {
EngineView::class.java.name -> components.core.engine.createView(context, attrs).apply {
selectionActionDelegate = DefaultSelectionActionDelegate(
store = components.core.store,
context = context,
appName = getString(R.string.app_name)
)
}.asView()
else -> super.onCreateView(parent, name, context, attrs)
}
final override fun onBackPressed() {
supportFragmentManager.primaryNavigationFragment?.childFragmentManager?.fragments?.forEach {
if (it is UserInteractionHandler && it.onBackPressed()) {
return
}
}
super.onBackPressed()
}
protected open fun getBreadcrumbMessage(destination: NavDestination): String {
val fragmentName = resources.getResourceEntryName(destination.id)
return "Changing to fragment $fragmentName, isCustomTab: false"
}
@VisibleForTesting(otherwise = PROTECTED)
internal open fun getIntentSource(intent: SafeIntent): Event.OpenedApp.Source? {
return when {
intent.isLauncherIntent -> Event.OpenedApp.Source.APP_ICON
intent.action == Intent.ACTION_VIEW -> Event.OpenedApp.Source.LINK
else -> null
}
}
/**
* External sources such as 3rd party links and shortcuts use this function to enter
* private mode directly before the content view is created. Returns the mode set by the intent
* otherwise falls back to the last known mode.
*/
internal fun getModeFromIntentOrLastKnown(intent: Intent?): BrowsingMode {
intent?.toSafeIntent()?.let {
if (it.hasExtra(PRIVATE_BROWSING_MODE)) {
val startPrivateMode = it.getBooleanExtra(PRIVATE_BROWSING_MODE, false)
return BrowsingMode.fromBoolean(isPrivate = startPrivateMode)
}
}
return settings().lastKnownMode
}
private fun setupThemeAndBrowsingMode(mode: BrowsingMode) {
settings().lastKnownMode = mode
browsingModeManager.mode = mode
themeManager = createThemeManager()
themeManager.setActivityTheme(this)
themeManager.applyStatusBarTheme(this)
}
/**
* Returns the [supportActionBar], inflating it if necessary.
* Everyone should call this instead of supportActionBar.
*/
fun getSupportActionBarAndInflateIfNecessary(): ActionBar {
// Add ids to this that we don't want to have a toolbar back button
if (!isToolbarInflated) {
val navigationToolbar = navigationToolbarStub.inflate() as Toolbar
setSupportActionBar(navigationToolbar)
NavigationUI.setupWithNavController(
navigationToolbar,
navHost.navController,
AppBarConfiguration.Builder().build()
)
navigationToolbar.setNavigationOnClickListener {
onBackPressed()
}
isToolbarInflated = true
}
return supportActionBar!!
}
protected open fun getIntentSessionId(intent: SafeIntent): String? = null
@Suppress("LongParameterList")
fun openToBrowserAndLoad(
searchTermOrURL: String,
newTab: Boolean,
from: BrowserDirection,
customTabSessionId: String? = null,
engine: SearchEngine? = null,
forceSearch: Boolean = false
) {
openToBrowser(from, customTabSessionId)
load(searchTermOrURL, newTab, engine, forceSearch)
}
fun openToBrowser(from: BrowserDirection, customTabSessionId: String? = null) {
if (sessionObserver == null) {
sessionObserver = UriOpenedObserver(this)
}
if (navHost.navController.alreadyOnDestination(R.id.browserFragment)) return
@IdRes val fragmentId = if (from.fragmentId != 0) from.fragmentId else null
val directions = getNavDirections(from, customTabSessionId)
if (directions != null) {
navHost.navController.nav(fragmentId, directions)
}
}
protected open fun getNavDirections(
from: BrowserDirection,
customTabSessionId: String?
): NavDirections? = when (from) {
BrowserDirection.FromGlobal ->
NavGraphDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromHome ->
HomeFragmentDirections.actionHomeFragmentToBrowserFragment(customTabSessionId)
BrowserDirection.FromSearch ->
SearchFragmentDirections.actionSearchFragmentToBrowserFragment(customTabSessionId)
BrowserDirection.FromSettings ->
SettingsFragmentDirections.actionSettingsFragmentToBrowserFragment(customTabSessionId)
BrowserDirection.FromBookmarks ->
BookmarkFragmentDirections.actionBookmarkFragmentToBrowserFragment(customTabSessionId)
BrowserDirection.FromHistory ->
HistoryFragmentDirections.actionHistoryFragmentToBrowserFragment(customTabSessionId)
BrowserDirection.FromExceptions ->
ExceptionsFragmentDirections.actionExceptionsFragmentToBrowserFragment(
customTabSessionId
)
BrowserDirection.FromAbout ->
AboutFragmentDirections.actionAboutFragmentToBrowserFragment(customTabSessionId)
BrowserDirection.FromTrackingProtection ->
TrackingProtectionFragmentDirections.actionTrackingProtectionFragmentToBrowserFragment(
customTabSessionId
)
BrowserDirection.FromDefaultBrowserSettingsFragment ->
DefaultBrowserSettingsFragmentDirections.actionDefaultBrowserSettingsFragmentToBrowserFragment(
customTabSessionId
)
BrowserDirection.FromSavedLoginsFragment ->
SavedLoginsFragmentDirections.actionSavedLoginsFragmentToBrowserFragment(
customTabSessionId
)
}
private fun load(
searchTermOrURL: String,
newTab: Boolean,
engine: SearchEngine?,
forceSearch: Boolean
) {
val mode = browsingModeManager.mode
val loadUrlUseCase = if (newTab) {
when (mode) {
BrowsingMode.Private -> components.useCases.tabsUseCases.addPrivateTab
BrowsingMode.Normal -> components.useCases.tabsUseCases.addTab
}
} else components.useCases.sessionUseCases.loadUrl
val searchUseCase: (String) -> Unit = { searchTerms ->
if (newTab) {
components.useCases.searchUseCases.newTabSearch
.invoke(
searchTerms,
Session.Source.USER_ENTERED,
true,
mode.isPrivate,
searchEngine = engine
)
} else components.useCases.searchUseCases.defaultSearch.invoke(searchTerms, engine)
}
if (!forceSearch && searchTermOrURL.isUrl()) {
loadUrlUseCase.invoke(searchTermOrURL.toNormalizedUrl())
} else {
searchUseCase.invoke(searchTermOrURL)
}
}
fun updateThemeForSession(session: Session) {
val sessionMode = BrowsingMode.fromBoolean(session.private)
if (sessionMode != browsingModeManager.mode) {
browsingModeManager.mode = sessionMode
}
}
protected open fun createThemeManager(): ThemeManager {
return DefaultThemeManager(browsingModeManager.mode, this)
}
private fun openPopup(webExtensionState: WebExtensionState) {
val action = NavGraphDirections.actionGlobalWebExtensionActionPopupFragment(
webExtensionId = webExtensionState.id,
webExtensionTitle = webExtensionState.name
)
navHost.navController.navigate(action)
}
companion object {
const val OPEN_TO_BROWSER = "open_to_browser"
const val OPEN_TO_BROWSER_AND_LOAD = "open_to_browser_and_load"
const val OPEN_TO_SEARCH = "open_to_search"
const val PRIVATE_BROWSING_MODE = "private_browsing_mode"
const val EXTRA_DELETE_PRIVATE_TABS = "notification_delete_and_open"
const val EXTRA_OPENED_FROM_NOTIFICATION = "notification_open"
}
}