diff --git a/app/src/main/java/org/mozilla/fenix/browser/BaseBrowserFragment.kt b/app/src/main/java/org/mozilla/fenix/browser/BaseBrowserFragment.kt index fc37b8b047..60dfe78f38 100644 --- a/app/src/main/java/org/mozilla/fenix/browser/BaseBrowserFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/browser/BaseBrowserFragment.kt @@ -184,6 +184,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, protected var webAppToolbarShouldBeVisible = true private val sharedViewModel: SharedViewModel by activityViewModels() + private val homeViewModel: HomeScreenViewModel by activityViewModels() @VisibleForTesting internal val onboarding by lazy { FenixOnboarding(requireContext()) } @@ -220,7 +221,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, } final override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - browserInitialized = initializeUI(view) != null + initializeUI(view) if (customTabSessionId == null) { // We currently only need this observer to navigate to home @@ -238,12 +239,19 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, requireContext().accessibilityManager.addAccessibilityStateChangeListener(this) } - private val homeViewModel: HomeScreenViewModel by activityViewModels() + private fun initializeUI(view: View) { + val tab = getCurrentTab() + browserInitialized = if (tab != null) { + initializeUI(view, tab) + true + } else { + false + } + } @Suppress("ComplexMethod", "LongMethod") @CallSuper - @VisibleForTesting - internal open fun initializeUI(view: View): Session? { + internal open fun initializeUI(view: View, tab: SessionState) { val context = requireContext() val sessionManager = context.components.core.sessionManager val store = context.components.core.store @@ -260,456 +268,454 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, beginAnimateInIfNecessary() } - return getSessionById()?.also { _ -> - val openInFenixIntent = Intent(context, IntentReceiverActivity::class.java).apply { - action = Intent.ACTION_VIEW - putExtra(HomeActivity.OPEN_TO_BROWSER, true) - } + val openInFenixIntent = Intent(context, IntentReceiverActivity::class.java).apply { + action = Intent.ACTION_VIEW + putExtra(HomeActivity.OPEN_TO_BROWSER, true) + } - val readerMenuController = DefaultReaderModeController( - readerViewFeature, - view.readerViewControlsBar, - isPrivate = activity.browsingModeManager.mode.isPrivate - ) - val browserToolbarController = DefaultBrowserToolbarController( - store = store, - activity = activity, - navController = findNavController(), - metrics = requireComponents.analytics.metrics, - readerModeController = readerMenuController, - sessionManager = requireComponents.core.sessionManager, - engineView = engineView, - homeViewModel = homeViewModel, - customTabSession = customTabSessionId?.let { sessionManager.findSessionById(it) }, - onTabCounterClicked = { - thumbnailsFeature.get()?.requestScreenshot() - findNavController().nav( - R.id.browserFragment, - BrowserFragmentDirections.actionGlobalTabTrayDialogFragment() - ) - }, - onCloseTab = { closedSession -> - val tab = store.state.findTab(closedSession.id) ?: return@DefaultBrowserToolbarController - - val snackbarMessage = if (tab.content.private) { - requireContext().getString(R.string.snackbar_private_tab_closed) - } else { - requireContext().getString(R.string.snackbar_tab_closed) - } - - viewLifecycleOwner.lifecycleScope.allowUndo( - requireView().browserLayout, - snackbarMessage, - requireContext().getString(R.string.snackbar_deleted_undo), - { - requireComponents.useCases.tabsUseCases.undo.invoke() - }, - paddedForBottomToolbar = true, - operation = { } - ) - } - ) - val browserToolbarMenuController = DefaultBrowserToolbarMenuController( - activity = activity, - navController = findNavController(), - metrics = requireComponents.analytics.metrics, - settings = context.settings(), - readerModeController = readerMenuController, - sessionManager = requireComponents.core.sessionManager, - sessionFeature = sessionFeature, - findInPageLauncher = { findInPageIntegration.withFeature { it.launch() } }, - swipeRefresh = swipeRefresh, - browserAnimator = browserAnimator, - customTabSession = customTabSessionId?.let { sessionManager.findSessionById(it) }, - openInFenixIntent = openInFenixIntent, - bookmarkTapped = { url: String, title: String -> - viewLifecycleOwner.lifecycleScope.launch { - bookmarkTapped(url, title) - } - }, - scope = viewLifecycleOwner.lifecycleScope, - tabCollectionStorage = requireComponents.core.tabCollectionStorage, - topSitesStorage = requireComponents.core.topSitesStorage, - browserStore = store - ) - - _browserInteractor = BrowserInteractor( - browserToolbarController, - browserToolbarMenuController - ) - - _browserToolbarView = BrowserToolbarView( - container = view.browserLayout, - toolbarPosition = context.settings().toolbarPosition, - interactor = browserInteractor, - customTabSession = customTabSessionId?.let { sessionManager.findSessionById(it) }, - lifecycleOwner = viewLifecycleOwner - ) - - toolbarIntegration.set( - feature = browserToolbarView.toolbarIntegration, - owner = this, - view = view - ) - - findInPageIntegration.set( - feature = FindInPageIntegration( - store = store, - sessionId = customTabSessionId, - stub = view.stubFindInPage, - engineView = view.engineView, - toolbar = browserToolbarView.view - ), - owner = this, - view = view - ) - - browserToolbarView.view.display.setOnSiteSecurityClickedListener { - showQuickSettingsDialog() - } - - browserToolbarView.view.display.setOnTrackingProtectionClickedListener { - context.metrics.track(Event.TrackingProtectionIconPressed) - showTrackingProtectionPanel() - } - - contextMenuFeature.set( - feature = ContextMenuFeature( - fragmentManager = parentFragmentManager, - store = store, - candidates = getContextMenuCandidates(context, view.browserLayout), - engineView = view.engineView, - useCases = context.components.useCases.contextMenuUseCases, - tabId = customTabSessionId - ), - owner = this, - view = view - ) - - val allowScreenshotsInPrivateMode = context.settings().allowScreenshotsInPrivateMode - secureWindowFeature.set( - feature = SecureWindowFeature( - window = requireActivity().window, - store = store, - customTabId = customTabSessionId, - isSecure = { !allowScreenshotsInPrivateMode && it.content.private } - ), - owner = this, - view = view - ) - - if (newMediaSessionApi) { - fullScreenMediaSessionFeature.set( - feature = MediaSessionFullscreenFeature( - requireActivity(), - context.components.core.store - ), - owner = this, - view = view + val readerMenuController = DefaultReaderModeController( + readerViewFeature, + view.readerViewControlsBar, + isPrivate = activity.browsingModeManager.mode.isPrivate + ) + val browserToolbarController = DefaultBrowserToolbarController( + store = store, + activity = activity, + navController = findNavController(), + metrics = requireComponents.analytics.metrics, + readerModeController = readerMenuController, + sessionManager = requireComponents.core.sessionManager, + engineView = engineView, + homeViewModel = homeViewModel, + customTabSession = customTabSessionId?.let { sessionManager.findSessionById(it) }, + onTabCounterClicked = { + thumbnailsFeature.get()?.requestScreenshot() + findNavController().nav( + R.id.browserFragment, + BrowserFragmentDirections.actionGlobalTabTrayDialogFragment() ) - } else { - fullScreenMediaFeature.set( - feature = MediaFullscreenOrientationFeature( - requireActivity(), - context.components.core.store - ), - owner = this, - view = view + }, + onCloseTab = { closedSession -> + val closedTab = store.state.findTab(closedSession.id) ?: return@DefaultBrowserToolbarController + + val snackbarMessage = if (closedTab.content.private) { + requireContext().getString(R.string.snackbar_private_tab_closed) + } else { + requireContext().getString(R.string.snackbar_tab_closed) + } + + viewLifecycleOwner.lifecycleScope.allowUndo( + requireView().browserLayout, + snackbarMessage, + requireContext().getString(R.string.snackbar_deleted_undo), + { + requireComponents.useCases.tabsUseCases.undo.invoke() + }, + paddedForBottomToolbar = true, + operation = { } ) } - - val downloadFeature = DownloadsFeature( - context.applicationContext, - store = store, - useCases = context.components.useCases.downloadUseCases, - fragmentManager = childFragmentManager, - tabId = customTabSessionId, - downloadManager = FetchDownloadManager( - context.applicationContext, - store, - DownloadService::class - ), - shouldForwardToThirdParties = { - PreferenceManager.getDefaultSharedPreferences(context).getBoolean( - context.getPreferenceKey(R.string.pref_key_external_download_manager), false - ) - }, - promptsStyling = DownloadsFeature.PromptsStyling( - gravity = Gravity.BOTTOM, - shouldWidthMatchParent = true, - positiveButtonBackgroundColor = ThemeManager.resolveAttribute( - R.attr.accent, - context - ), - positiveButtonTextColor = ThemeManager.resolveAttribute( - R.attr.contrastText, - context - ), - positiveButtonRadius = (resources.getDimensionPixelSize(R.dimen.tab_corner_radius)).toFloat() - ), - onNeedToRequestPermissions = { permissions -> - requestPermissions(permissions, REQUEST_CODE_DOWNLOAD_PERMISSIONS) + ) + val browserToolbarMenuController = DefaultBrowserToolbarMenuController( + activity = activity, + navController = findNavController(), + metrics = requireComponents.analytics.metrics, + settings = context.settings(), + readerModeController = readerMenuController, + sessionManager = requireComponents.core.sessionManager, + sessionFeature = sessionFeature, + findInPageLauncher = { findInPageIntegration.withFeature { it.launch() } }, + swipeRefresh = swipeRefresh, + browserAnimator = browserAnimator, + customTabSession = customTabSessionId?.let { sessionManager.findSessionById(it) }, + openInFenixIntent = openInFenixIntent, + bookmarkTapped = { url: String, title: String -> + viewLifecycleOwner.lifecycleScope.launch { + bookmarkTapped(url, title) } - ) + }, + scope = viewLifecycleOwner.lifecycleScope, + tabCollectionStorage = requireComponents.core.tabCollectionStorage, + topSitesStorage = requireComponents.core.topSitesStorage, + browserStore = store + ) - downloadFeature.onDownloadStopped = { downloadState, _, downloadJobStatus -> - // If the download is just paused, don't show any in-app notification - if (downloadJobStatus == DownloadState.Status.COMPLETED || - downloadJobStatus == DownloadState.Status.FAILED - ) { + _browserInteractor = BrowserInteractor( + browserToolbarController, + browserToolbarMenuController + ) - saveDownloadDialogState( - downloadState.sessionId, - downloadState, - downloadJobStatus - ) + _browserToolbarView = BrowserToolbarView( + container = view.browserLayout, + toolbarPosition = context.settings().toolbarPosition, + interactor = browserInteractor, + customTabSession = customTabSessionId?.let { sessionManager.findSessionById(it) }, + lifecycleOwner = viewLifecycleOwner + ) - val dynamicDownloadDialog = DynamicDownloadDialog( - container = view.browserLayout, - downloadState = downloadState, - metrics = requireComponents.analytics.metrics, - didFail = downloadJobStatus == DownloadState.Status.FAILED, - tryAgain = downloadFeature::tryAgain, - onCannotOpenFile = { - FenixSnackbar.make( - view = view.browserLayout, - duration = Snackbar.LENGTH_SHORT, - isDisplayedWithBrowserToolbar = true - ) - .setText(context.getString(R.string.mozac_feature_downloads_could_not_open_file)) - .show() - }, - view = view.viewDynamicDownloadDialog, - toolbarHeight = toolbarHeight, - onDismiss = { sharedViewModel.downloadDialogState.remove(downloadState.sessionId) } - ) + toolbarIntegration.set( + feature = browserToolbarView.toolbarIntegration, + owner = this, + view = view + ) - // Don't show the dialog if we aren't in the tab that started the download - if (downloadState.sessionId == sessionManager.selectedSession?.id) { - dynamicDownloadDialog.show() - browserToolbarView.expand() - } - } - } - - resumeDownloadDialogState( - sessionManager.selectedSession?.id, - store, view, context, toolbarHeight - ) - - downloadsFeature.set( - downloadFeature, - owner = this, - view = view - ) - - pipFeature = PictureInPictureFeature( + findInPageIntegration.set( + feature = FindInPageIntegration( store = store, - activity = requireActivity(), - crashReporting = context.components.analytics.crashReporter, + sessionId = customTabSessionId, + stub = view.stubFindInPage, + engineView = view.engineView, + toolbar = browserToolbarView.view + ), + owner = this, + view = view + ) + + browserToolbarView.view.display.setOnSiteSecurityClickedListener { + showQuickSettingsDialog() + } + + browserToolbarView.view.display.setOnTrackingProtectionClickedListener { + context.metrics.track(Event.TrackingProtectionIconPressed) + showTrackingProtectionPanel() + } + + contextMenuFeature.set( + feature = ContextMenuFeature( + fragmentManager = parentFragmentManager, + store = store, + candidates = getContextMenuCandidates(context, view.browserLayout), + engineView = view.engineView, + useCases = context.components.useCases.contextMenuUseCases, tabId = customTabSessionId - ) + ), + owner = this, + view = view + ) - appLinksFeature.set( - feature = AppLinksFeature( - context, - store = store, - sessionId = customTabSessionId, - fragmentManager = parentFragmentManager, - launchInApp = { context.settings().openLinksInExternalApp }, - loadUrlUseCase = context.components.useCases.sessionUseCases.loadUrl + val allowScreenshotsInPrivateMode = context.settings().allowScreenshotsInPrivateMode + secureWindowFeature.set( + feature = SecureWindowFeature( + window = requireActivity().window, + store = store, + customTabId = customTabSessionId, + isSecure = { !allowScreenshotsInPrivateMode && it.content.private } + ), + owner = this, + view = view + ) + + if (newMediaSessionApi) { + fullScreenMediaSessionFeature.set( + feature = MediaSessionFullscreenFeature( + requireActivity(), + context.components.core.store ), owner = this, view = view ) + } else { + fullScreenMediaFeature.set( + feature = MediaFullscreenOrientationFeature( + requireActivity(), + context.components.core.store + ), + owner = this, + view = view + ) + } - promptsFeature.set( - feature = PromptFeature( - fragment = this, - store = store, - customTabId = customTabSessionId, - fragmentManager = parentFragmentManager, - loginValidationDelegate = DefaultLoginValidationDelegate( - context.components.core.lazyPasswordsStorage - ), - isSaveLoginEnabled = { - context.settings().shouldPromptToSaveLogins + val downloadFeature = DownloadsFeature( + context.applicationContext, + store = store, + useCases = context.components.useCases.downloadUseCases, + fragmentManager = childFragmentManager, + tabId = customTabSessionId, + downloadManager = FetchDownloadManager( + context.applicationContext, + store, + DownloadService::class + ), + shouldForwardToThirdParties = { + PreferenceManager.getDefaultSharedPreferences(context).getBoolean( + context.getPreferenceKey(R.string.pref_key_external_download_manager), false + ) + }, + promptsStyling = DownloadsFeature.PromptsStyling( + gravity = Gravity.BOTTOM, + shouldWidthMatchParent = true, + positiveButtonBackgroundColor = ThemeManager.resolveAttribute( + R.attr.accent, + context + ), + positiveButtonTextColor = ThemeManager.resolveAttribute( + R.attr.contrastText, + context + ), + positiveButtonRadius = (resources.getDimensionPixelSize(R.dimen.tab_corner_radius)).toFloat() + ), + onNeedToRequestPermissions = { permissions -> + requestPermissions(permissions, REQUEST_CODE_DOWNLOAD_PERMISSIONS) + } + ) + + downloadFeature.onDownloadStopped = { downloadState, _, downloadJobStatus -> + // If the download is just paused, don't show any in-app notification + if (downloadJobStatus == DownloadState.Status.COMPLETED || + downloadJobStatus == DownloadState.Status.FAILED + ) { + + saveDownloadDialogState( + downloadState.sessionId, + downloadState, + downloadJobStatus + ) + + val dynamicDownloadDialog = DynamicDownloadDialog( + container = view.browserLayout, + downloadState = downloadState, + metrics = requireComponents.analytics.metrics, + didFail = downloadJobStatus == DownloadState.Status.FAILED, + tryAgain = downloadFeature::tryAgain, + onCannotOpenFile = { + FenixSnackbar.make( + view = view.browserLayout, + duration = Snackbar.LENGTH_SHORT, + isDisplayedWithBrowserToolbar = true + ) + .setText(context.getString(R.string.mozac_feature_downloads_could_not_open_file)) + .show() }, - loginExceptionStorage = context.components.core.loginExceptionStorage, - shareDelegate = object : ShareDelegate { - override fun showShareSheet( - context: Context, - shareData: ShareData, - onDismiss: () -> Unit, - onSuccess: () -> Unit - ) { - val directions = NavGraphDirections.actionGlobalShareFragment( - data = arrayOf(shareData), - showPage = true, - sessionId = getSessionById()?.id - ) - findNavController().navigate(directions) - } - }, - onNeedToRequestPermissions = { permissions -> - requestPermissions(permissions, REQUEST_CODE_PROMPT_PERMISSIONS) - }, - loginPickerView = loginSelectBar, - onManageLogins = { - browserAnimator.captureEngineViewAndDrawStatically { - val directions = - NavGraphDirections.actionGlobalSavedLoginsAuthFragment() - findNavController().navigate(directions) - } + view = view.viewDynamicDownloadDialog, + toolbarHeight = toolbarHeight, + onDismiss = { sharedViewModel.downloadDialogState.remove(downloadState.sessionId) } + ) + + // Don't show the dialog if we aren't in the tab that started the download + if (downloadState.sessionId == sessionManager.selectedSession?.id) { + dynamicDownloadDialog.show() + browserToolbarView.expand() + } + } + } + + resumeDownloadDialogState( + sessionManager.selectedSession?.id, + store, view, context, toolbarHeight + ) + + downloadsFeature.set( + downloadFeature, + owner = this, + view = view + ) + + pipFeature = PictureInPictureFeature( + store = store, + activity = requireActivity(), + crashReporting = context.components.analytics.crashReporter, + tabId = customTabSessionId + ) + + appLinksFeature.set( + feature = AppLinksFeature( + context, + store = store, + sessionId = customTabSessionId, + fragmentManager = parentFragmentManager, + launchInApp = { context.settings().openLinksInExternalApp }, + loadUrlUseCase = context.components.useCases.sessionUseCases.loadUrl + ), + owner = this, + view = view + ) + + promptsFeature.set( + feature = PromptFeature( + fragment = this, + store = store, + customTabId = customTabSessionId, + fragmentManager = parentFragmentManager, + loginValidationDelegate = DefaultLoginValidationDelegate( + context.components.core.lazyPasswordsStorage + ), + isSaveLoginEnabled = { + context.settings().shouldPromptToSaveLogins + }, + loginExceptionStorage = context.components.core.loginExceptionStorage, + shareDelegate = object : ShareDelegate { + override fun showShareSheet( + context: Context, + shareData: ShareData, + onDismiss: () -> Unit, + onSuccess: () -> Unit + ) { + val directions = NavGraphDirections.actionGlobalShareFragment( + data = arrayOf(shareData), + showPage = true, + sessionId = getSessionById()?.id + ) + findNavController().navigate(directions) } + }, + onNeedToRequestPermissions = { permissions -> + requestPermissions(permissions, REQUEST_CODE_PROMPT_PERMISSIONS) + }, + loginPickerView = loginSelectBar, + onManageLogins = { + browserAnimator.captureEngineViewAndDrawStatically { + val directions = + NavGraphDirections.actionGlobalSavedLoginsAuthFragment() + findNavController().navigate(directions) + } + } + ), + owner = this, + view = view + ) + + sessionFeature.set( + feature = SessionFeature( + requireComponents.core.store, + requireComponents.useCases.sessionUseCases.goBack, + view.engineView, + customTabSessionId + ), + owner = this, + view = view + ) + + searchFeature.set( + feature = SearchFeature(store, customTabSessionId) { request, tabId -> + val parentSession = sessionManager.findSessionById(tabId) + val useCase = if (request.isPrivate) { + requireComponents.useCases.searchUseCases.newPrivateTabSearch + } else { + requireComponents.useCases.searchUseCases.newTabSearch + } + + if (parentSession?.isCustomTabSession() == true) { + useCase.invoke(request.query) + requireActivity().startActivity(openInFenixIntent) + } else { + useCase.invoke(request.query, parentSessionId = parentSession?.id) + } + }, + owner = this, + view = view + ) + + val accentHighContrastColor = + ThemeManager.resolveAttribute(R.attr.accentHighContrast, context) + + sitePermissionsFeature.set( + feature = SitePermissionsFeature( + context = context, + storage = context.components.core.permissionStorage.permissionsStorage, + fragmentManager = parentFragmentManager, + promptsStyling = SitePermissionsFeature.PromptsStyling( + gravity = getAppropriateLayoutGravity(), + shouldWidthMatchParent = true, + positiveButtonBackgroundColor = accentHighContrastColor, + positiveButtonTextColor = R.color.photonWhite + ), + sessionId = customTabSessionId, + onNeedToRequestPermissions = { permissions -> + requestPermissions(permissions, REQUEST_CODE_APP_PERMISSIONS) + }, + onShouldShowRequestPermissionRationale = { + shouldShowRequestPermissionRationale( + it + ) + }, + store = store + ), + owner = this, + view = view + ) + + sitePermissionWifiIntegration.set( + feature = SitePermissionsWifiIntegration( + settings = context.settings(), + wifiConnectionMonitor = context.components.wifiConnectionMonitor + ), + owner = this, + view = view + ) + + if (FeatureFlags.webAuthFeature) { + webAuthnFeature.set( + feature = WebAuthnFeature( + engine = requireComponents.core.engine, + activity = requireActivity() ), owner = this, view = view ) + } - sessionFeature.set( - feature = SessionFeature( + context.settings().setSitePermissionSettingListener(viewLifecycleOwner) { + // If the user connects to WIFI while on the BrowserFragment, this will update the + // SitePermissionsRules (specifically autoplay) accordingly + runIfFragmentIsAttached { + assignSitePermissionsRules() + } + } + assignSitePermissionsRules() + + fullScreenFeature.set( + feature = FullScreenFeature( + requireComponents.core.store, + requireComponents.useCases.sessionUseCases, + customTabSessionId, + ::viewportFitChange, + ::fullScreenChanged + ), + owner = this, + view = view + ) + + expandToolbarOnNavigation(store) + + store.flowScoped(viewLifecycleOwner) { flow -> + flow.mapNotNull { state -> state.findTabOrCustomTabOrSelectedTab(customTabSessionId) } + .ifChanged { tab -> tab.content.pictureInPictureEnabled } + .collect { tab -> pipModeChanged(tab) } + } + + view.swipeRefresh.isEnabled = shouldPullToRefreshBeEnabled(false) + + if (view.swipeRefresh.isEnabled) { + val primaryTextColor = + ThemeManager.resolveAttribute(R.attr.primaryText, context) + view.swipeRefresh.setColorSchemeColors(primaryTextColor) + swipeRefreshFeature.set( + feature = SwipeRefreshFeature( requireComponents.core.store, - requireComponents.useCases.sessionUseCases.goBack, - view.engineView, + context.components.useCases.sessionUseCases.reload, + view.swipeRefresh, customTabSessionId ), owner = this, view = view ) - - searchFeature.set( - feature = SearchFeature(store, customTabSessionId) { request, tabId -> - val parentSession = sessionManager.findSessionById(tabId) - val useCase = if (request.isPrivate) { - requireComponents.useCases.searchUseCases.newPrivateTabSearch - } else { - requireComponents.useCases.searchUseCases.newTabSearch - } - - if (parentSession?.isCustomTabSession() == true) { - useCase.invoke(request.query) - requireActivity().startActivity(openInFenixIntent) - } else { - useCase.invoke(request.query, parentSessionId = parentSession?.id) - } - }, - owner = this, - view = view - ) - - val accentHighContrastColor = - ThemeManager.resolveAttribute(R.attr.accentHighContrast, context) - - sitePermissionsFeature.set( - feature = SitePermissionsFeature( - context = context, - storage = context.components.core.permissionStorage.permissionsStorage, - fragmentManager = parentFragmentManager, - promptsStyling = SitePermissionsFeature.PromptsStyling( - gravity = getAppropriateLayoutGravity(), - shouldWidthMatchParent = true, - positiveButtonBackgroundColor = accentHighContrastColor, - positiveButtonTextColor = R.color.photonWhite - ), - sessionId = customTabSessionId, - onNeedToRequestPermissions = { permissions -> - requestPermissions(permissions, REQUEST_CODE_APP_PERMISSIONS) - }, - onShouldShowRequestPermissionRationale = { - shouldShowRequestPermissionRationale( - it - ) - }, - store = store - ), - owner = this, - view = view - ) - - sitePermissionWifiIntegration.set( - feature = SitePermissionsWifiIntegration( - settings = context.settings(), - wifiConnectionMonitor = context.components.wifiConnectionMonitor - ), - owner = this, - view = view - ) - - if (FeatureFlags.webAuthFeature) { - webAuthnFeature.set( - feature = WebAuthnFeature( - engine = requireComponents.core.engine, - activity = requireActivity() - ), - owner = this, - view = view - ) - } - - context.settings().setSitePermissionSettingListener(viewLifecycleOwner) { - // If the user connects to WIFI while on the BrowserFragment, this will update the - // SitePermissionsRules (specifically autoplay) accordingly - runIfFragmentIsAttached { - assignSitePermissionsRules() - } - } - assignSitePermissionsRules() - - fullScreenFeature.set( - feature = FullScreenFeature( - requireComponents.core.store, - requireComponents.useCases.sessionUseCases, - customTabSessionId, - ::viewportFitChange, - ::fullScreenChanged - ), - owner = this, - view = view - ) - - expandToolbarOnNavigation(store) - - store.flowScoped(viewLifecycleOwner) { flow -> - flow.mapNotNull { state -> state.findTabOrCustomTabOrSelectedTab(customTabSessionId) } - .ifChanged { tab -> tab.content.pictureInPictureEnabled } - .collect { tab -> pipModeChanged(tab) } - } - - view.swipeRefresh.isEnabled = shouldPullToRefreshBeEnabled(false) - - if (view.swipeRefresh.isEnabled) { - val primaryTextColor = - ThemeManager.resolveAttribute(R.attr.primaryText, context) - view.swipeRefresh.setColorSchemeColors(primaryTextColor) - swipeRefreshFeature.set( - feature = SwipeRefreshFeature( - requireComponents.core.store, - context.components.useCases.sessionUseCases.reload, - view.swipeRefresh, - customTabSessionId - ), - owner = this, - view = view - ) - } - - webchannelIntegration.set( - feature = FxaWebChannelFeature( - requireContext(), - customTabSessionId, - requireComponents.core.engine, - requireComponents.core.store, - requireComponents.backgroundServices.accountManager, - requireComponents.backgroundServices.serverConfig, - setOf(FxaCapability.CHOOSE_WHAT_TO_SYNC) - ), - owner = this, - view = view - ) - - initializeEngineView(toolbarHeight) } + + webchannelIntegration.set( + feature = FxaWebChannelFeature( + requireContext(), + customTabSessionId, + requireComponents.core.engine, + requireComponents.core.store, + requireComponents.backgroundServices.accountManager, + requireComponents.backgroundServices.serverConfig, + setOf(FxaCapability.CHOOSE_WHAT_TO_SYNC) + ), + owner = this, + view = view + ) + + initializeEngineView(toolbarHeight) } @VisibleForTesting @@ -928,9 +934,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, resumeDownloadDialogState(selectedTab.id, context.components.core.store, view, context, toolbarHeight) } } else { - view?.let { view -> - browserInitialized = initializeUI(view) != null - } + view?.let { view -> initializeUI(view) } } } @@ -1070,7 +1074,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, sitePermissions: SitePermissions? ) - protected abstract fun navToTrackingProtectionPanel(session: Session) + protected abstract fun navToTrackingProtectionPanel(tab: SessionState) /** * Returns the layout [android.view.Gravity] for the quick settings and ETP dialog. @@ -1108,9 +1112,9 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, } private fun showTrackingProtectionPanel() { - val session = getSessionById() ?: return + val tab = getCurrentTab() ?: return view?.let { - navToTrackingProtectionPanel(session) + navToTrackingProtectionPanel(tab) } } diff --git a/app/src/main/java/org/mozilla/fenix/browser/BrowserFragment.kt b/app/src/main/java/org/mozilla/fenix/browser/BrowserFragment.kt index f5e8a3fe95..042d33a3b3 100644 --- a/app/src/main/java/org/mozilla/fenix/browser/BrowserFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/browser/BrowserFragment.kt @@ -15,7 +15,6 @@ import com.google.android.material.snackbar.Snackbar import kotlinx.android.synthetic.main.fragment_browser.* import kotlinx.android.synthetic.main.fragment_browser.view.* import kotlinx.coroutines.ExperimentalCoroutinesApi -import mozilla.components.browser.session.Session import mozilla.components.browser.state.selector.findTab import mozilla.components.browser.state.state.TabSessionState import mozilla.components.browser.state.state.SessionState @@ -56,107 +55,107 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler { private var pwaOnboardingObserver: PwaOnboardingObserver? = null @Suppress("LongMethod") - override fun initializeUI(view: View): Session? { + override fun initializeUI(view: View, tab: SessionState) { + super.initializeUI(view, tab) + val context = requireContext() val components = context.components - return super.initializeUI(view)?.also { - if (context.settings().isSwipeToolbarToSwitchTabsEnabled) { - gestureLayout.addGestureListener( - ToolbarGestureHandler( - activity = requireActivity(), - contentLayout = browserLayout, - tabPreview = tabPreview, - toolbarLayout = browserToolbarView.view, - store = components.core.store, - sessionManager = components.core.sessionManager - ) - ) - } - - val readerModeAction = - BrowserToolbar.ToggleButton( - image = AppCompatResources.getDrawable(requireContext(), R.drawable.ic_readermode)!!, - imageSelected = - AppCompatResources.getDrawable(requireContext(), R.drawable.ic_readermode_selected)!!, - contentDescription = requireContext().getString(R.string.browser_menu_read), - contentDescriptionSelected = requireContext().getString(R.string.browser_menu_read_close), - visible = { - readerModeAvailable - }, - selected = getSessionById()?.let { - activity?.components?.core?.store?.state?.findTab(it.id)?.readerState?.active - } ?: false, - listener = browserInteractor::onReaderModePressed - ) - - browserToolbarView.view.addPageAction(readerModeAction) - - thumbnailsFeature.set( - feature = BrowserThumbnails(context, view.engineView, components.core.store), - owner = this, - view = view - ) - - readerViewFeature.set( - feature = components.strictMode.resetAfter(StrictMode.allowThreadDiskReads()) { - ReaderViewFeature( - context, - components.core.engine, - components.core.store, - view.readerViewControlsBar - ) { available, active -> - if (available) { - components.analytics.metrics.track(Event.ReaderModeAvailable) - } - - readerModeAvailable = available - readerModeAction.setSelected(active) - safeInvalidateBrowserToolbarView() - } - }, - owner = this, - view = view - ) - - windowFeature.set( - feature = WindowFeature( + if (context.settings().isSwipeToolbarToSwitchTabsEnabled) { + gestureLayout.addGestureListener( + ToolbarGestureHandler( + activity = requireActivity(), + contentLayout = browserLayout, + tabPreview = tabPreview, + toolbarLayout = browserToolbarView.view, store = components.core.store, - tabsUseCases = components.useCases.tabsUseCases + sessionManager = components.core.sessionManager + ) + ) + } + + val readerModeAction = + BrowserToolbar.ToggleButton( + image = AppCompatResources.getDrawable(requireContext(), R.drawable.ic_readermode)!!, + imageSelected = + AppCompatResources.getDrawable(requireContext(), R.drawable.ic_readermode_selected)!!, + contentDescription = requireContext().getString(R.string.browser_menu_read), + contentDescriptionSelected = requireContext().getString(R.string.browser_menu_read_close), + visible = { + readerModeAvailable + }, + selected = getSessionById()?.let { + activity?.components?.core?.store?.state?.findTab(it.id)?.readerState?.active + } ?: false, + listener = browserInteractor::onReaderModePressed + ) + + browserToolbarView.view.addPageAction(readerModeAction) + + thumbnailsFeature.set( + feature = BrowserThumbnails(context, view.engineView, components.core.store), + owner = this, + view = view + ) + + readerViewFeature.set( + feature = components.strictMode.resetAfter(StrictMode.allowThreadDiskReads()) { + ReaderViewFeature( + context, + components.core.engine, + components.core.store, + view.readerViewControlsBar + ) { available, active -> + if (available) { + components.analytics.metrics.track(Event.ReaderModeAvailable) + } + + readerModeAvailable = available + readerModeAction.setSelected(active) + safeInvalidateBrowserToolbarView() + } + }, + owner = this, + view = view + ) + + windowFeature.set( + feature = WindowFeature( + store = components.core.store, + tabsUseCases = components.useCases.tabsUseCases + ), + owner = this, + view = view + ) + + if (context.settings().shouldShowOpenInAppCfr) { + openInAppOnboardingObserver.set( + feature = OpenInAppOnboardingObserver( + context = context, + store = context.components.core.store, + lifecycleOwner = this, + navController = findNavController(), + settings = context.settings(), + appLinksUseCases = context.components.useCases.appLinksUseCases, + container = browserLayout as ViewGroup + ), + owner = this, + view = view + ) + } + if (context.settings().shouldShowTrackingProtectionCfr) { + trackingProtectionOverlayObserver.set( + feature = TrackingProtectionOverlay( + context = context, + store = context.components.core.store, + lifecycleOwner = viewLifecycleOwner, + settings = context.settings(), + metrics = context.components.analytics.metrics, + getToolbar = { browserToolbarView.view } ), owner = this, view = view ) - - if (context.settings().shouldShowOpenInAppCfr) { - openInAppOnboardingObserver.set( - feature = OpenInAppOnboardingObserver( - context = context, - store = context.components.core.store, - lifecycleOwner = this, - navController = findNavController(), - settings = context.settings(), - appLinksUseCases = context.components.useCases.appLinksUseCases, - container = browserLayout as ViewGroup - ), - owner = this, - view = view - ) - } - if (context.settings().shouldShowTrackingProtectionCfr) { - trackingProtectionOverlayObserver.set( - feature = TrackingProtectionOverlay( - context = context, - store = context.components.core.store, - lifecycleOwner = viewLifecycleOwner, - settings = context.settings(), - metrics = context.components.analytics.metrics, - getToolbar = { browserToolbarView.view } - ), - owner = this, - view = view - ) - } } } @@ -219,15 +218,15 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler { nav(R.id.browserFragment, directions) } - override fun navToTrackingProtectionPanel(session: Session) { + override fun navToTrackingProtectionPanel(tab: SessionState) { val navController = findNavController() - requireComponents.useCases.trackingProtectionUseCases.containsException(session.id) { contains -> - val isEnabled = session.trackerBlockingEnabled && !contains + requireComponents.useCases.trackingProtectionUseCases.containsException(tab.id) { contains -> + val isEnabled = tab.trackingProtection.enabled && !contains val directions = BrowserFragmentDirections.actionBrowserFragmentToTrackingProtectionPanelDialogFragment( - sessionId = session.id, - url = session.url, + sessionId = tab.id, + url = tab.content.url, trackingProtectionEnabled = isEnabled, gravity = getAppropriateLayoutGravity() ) diff --git a/app/src/main/java/org/mozilla/fenix/customtabs/ExternalAppBrowserActivity.kt b/app/src/main/java/org/mozilla/fenix/customtabs/ExternalAppBrowserActivity.kt index 864a3692ce..eee8af3fcc 100644 --- a/app/src/main/java/org/mozilla/fenix/customtabs/ExternalAppBrowserActivity.kt +++ b/app/src/main/java/org/mozilla/fenix/customtabs/ExternalAppBrowserActivity.kt @@ -9,7 +9,6 @@ import androidx.annotation.VisibleForTesting import androidx.navigation.NavDestination import androidx.navigation.NavDirections import kotlinx.android.synthetic.main.activity_home.* -import mozilla.components.browser.session.runWithSession import mozilla.components.browser.state.selector.findCustomTab import mozilla.components.browser.state.state.SessionState import mozilla.components.concept.engine.manifest.WebAppManifestParser @@ -102,12 +101,9 @@ open class ExternalAppBrowserActivity : HomeActivity() { // When this activity finishes, the process is staying around and the session still // exists then remove it now to free all its resources. Once this activity is finished // then there's no way to get back to it other than relaunching it. - components.core.sessionManager.runWithSession(getExternalTabId()) { session -> - // If the custom tag config has been removed we are opening this in normal browsing - if (session.customTabConfig != null) { - remove(session) - } - true + val tabId = getExternalTabId() + if (tabId != null) { + components.useCases.customTabsUseCases.remove(tabId) } } } diff --git a/app/src/main/java/org/mozilla/fenix/customtabs/ExternalAppBrowserFragment.kt b/app/src/main/java/org/mozilla/fenix/customtabs/ExternalAppBrowserFragment.kt index 9b86bdf855..016f220c0c 100644 --- a/app/src/main/java/org/mozilla/fenix/customtabs/ExternalAppBrowserFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/customtabs/ExternalAppBrowserFragment.kt @@ -14,7 +14,6 @@ import androidx.navigation.fragment.navArgs import kotlinx.android.synthetic.main.component_browser_top_toolbar.* import kotlinx.android.synthetic.main.fragment_browser.* import kotlinx.coroutines.ExperimentalCoroutinesApi -import mozilla.components.browser.session.Session import mozilla.components.browser.state.state.SessionState import mozilla.components.concept.engine.manifest.WebAppManifestParser import mozilla.components.concept.engine.manifest.getOrNull @@ -52,113 +51,109 @@ class ExternalAppBrowserFragment : BaseBrowserFragment(), UserInteractionHandler private val hideToolbarFeature = ViewBoundFeatureWrapper() @Suppress("LongMethod", "ComplexMethod") - override fun initializeUI(view: View): Session? { - return super.initializeUI(view)?.also { - val activity = requireActivity() - val components = activity.components + override fun initializeUI(view: View, tab: SessionState) { + super.initializeUI(view, tab) - val manifest = args.webAppManifest?.let { json -> - WebAppManifestParser().parse(json).getOrNull() - } + val customTabSessionId = customTabSessionId ?: return + val activity = requireActivity() + val components = activity.components + val manifest = args.webAppManifest?.let { json -> WebAppManifestParser().parse(json).getOrNull() } - customTabSessionId?.let { customTabSessionId -> - customTabsIntegration.set( - feature = CustomTabsIntegration( - sessionManager = requireComponents.core.sessionManager, - store = requireComponents.core.store, - useCases = requireComponents.useCases.customTabsUseCases, - toolbar = toolbar, - sessionId = customTabSessionId, - activity = activity, - onItemTapped = { browserInteractor.onBrowserToolbarMenuItemTapped(it) }, - isPrivate = it.private, - shouldReverseItems = !activity.settings().shouldUseBottomToolbar - ), - owner = this, - view = view - ) + customTabsIntegration.set( + feature = CustomTabsIntegration( + sessionManager = requireComponents.core.sessionManager, + store = requireComponents.core.store, + useCases = requireComponents.useCases.customTabsUseCases, + toolbar = toolbar, + sessionId = customTabSessionId, + activity = activity, + onItemTapped = { browserInteractor.onBrowserToolbarMenuItemTapped(it) }, + isPrivate = tab.content.private, + shouldReverseItems = !activity.settings().shouldUseBottomToolbar + ), + owner = this, + view = view + ) - windowFeature.set( - feature = CustomTabWindowFeature( - activity, - components.core.store, - customTabSessionId - ) { uri -> - val intent = - Intent.parseUri("${BuildConfig.DEEP_LINK_SCHEME}://open?url=$uri", 0) - if (intent.action == Intent.ACTION_VIEW) { - intent.addCategory(Intent.CATEGORY_BROWSABLE) - intent.component = null - intent.selector = null - intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK - } - activity.startActivity(intent) - }, - owner = this, - view = view - ) - - hideToolbarFeature.set( - feature = WebAppHideToolbarFeature( - store = requireComponents.core.store, - customTabsStore = requireComponents.core.customTabsStore, - tabId = customTabSessionId, - manifest = manifest - ) { toolbarVisible -> - browserToolbarView.view.isVisible = toolbarVisible - webAppToolbarShouldBeVisible = toolbarVisible - if (!toolbarVisible) { - engineView.setDynamicToolbarMaxHeight(0) - val browserEngine = - swipeRefresh.layoutParams as CoordinatorLayout.LayoutParams - browserEngine.bottomMargin = 0 - } - }, - owner = this, - view = toolbar - ) - - if (manifest != null) { - activity.lifecycle.addObservers( - WebAppActivityFeature( - activity, - components.core.icons, - manifest - ), - ManifestUpdateFeature( - activity.applicationContext, - requireComponents.core.store, - requireComponents.core.webAppShortcutManager, - requireComponents.core.webAppManifestStorage, - customTabSessionId, - manifest - ) - ) - viewLifecycleOwner.lifecycle.addObserver( - WebAppSiteControlsFeature( - activity.applicationContext, - requireComponents.core.store, - requireComponents.useCases.sessionUseCases.reload, - customTabSessionId, - manifest, - WebAppSiteControlsBuilder( - requireComponents.core.sessionManager, - requireComponents.useCases.sessionUseCases.reload, - customTabSessionId, - manifest - ) - ) - ) - } else { - viewLifecycleOwner.lifecycle.addObserver( - PoweredByNotification( - activity.applicationContext, - requireComponents.core.store, - customTabSessionId - ) - ) + windowFeature.set( + feature = CustomTabWindowFeature( + activity, + components.core.store, + customTabSessionId + ) { uri -> + val intent = + Intent.parseUri("${BuildConfig.DEEP_LINK_SCHEME}://open?url=$uri", 0) + if (intent.action == Intent.ACTION_VIEW) { + intent.addCategory(Intent.CATEGORY_BROWSABLE) + intent.component = null + intent.selector = null + intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK } - } + activity.startActivity(intent) + }, + owner = this, + view = view + ) + + hideToolbarFeature.set( + feature = WebAppHideToolbarFeature( + store = requireComponents.core.store, + customTabsStore = requireComponents.core.customTabsStore, + tabId = customTabSessionId, + manifest = manifest + ) { toolbarVisible -> + browserToolbarView.view.isVisible = toolbarVisible + webAppToolbarShouldBeVisible = toolbarVisible + if (!toolbarVisible) { + engineView.setDynamicToolbarMaxHeight(0) + val browserEngine = + swipeRefresh.layoutParams as CoordinatorLayout.LayoutParams + browserEngine.bottomMargin = 0 + } + }, + owner = this, + view = toolbar + ) + + if (manifest != null) { + activity.lifecycle.addObservers( + WebAppActivityFeature( + activity, + components.core.icons, + manifest + ), + ManifestUpdateFeature( + activity.applicationContext, + requireComponents.core.store, + requireComponents.core.webAppShortcutManager, + requireComponents.core.webAppManifestStorage, + customTabSessionId, + manifest + ) + ) + viewLifecycleOwner.lifecycle.addObserver( + WebAppSiteControlsFeature( + activity.applicationContext, + requireComponents.core.store, + requireComponents.useCases.sessionUseCases.reload, + customTabSessionId, + manifest, + WebAppSiteControlsBuilder( + requireComponents.core.sessionManager, + requireComponents.useCases.sessionUseCases.reload, + customTabSessionId, + manifest + ) + ) + ) + } else { + viewLifecycleOwner.lifecycle.addObserver( + PoweredByNotification( + activity.applicationContext, + requireComponents.core.store, + customTabSessionId + ) + ) } } @@ -197,14 +192,14 @@ class ExternalAppBrowserFragment : BaseBrowserFragment(), UserInteractionHandler nav(R.id.externalAppBrowserFragment, directions) } - override fun navToTrackingProtectionPanel(session: Session) { - requireComponents.useCases.trackingProtectionUseCases.containsException(session.id) { contains -> - val isEnabled = session.trackerBlockingEnabled && !contains + override fun navToTrackingProtectionPanel(tab: SessionState) { + requireComponents.useCases.trackingProtectionUseCases.containsException(tab.id) { contains -> + val isEnabled = tab.trackingProtection.enabled && !contains val directions = ExternalAppBrowserFragmentDirections .actionGlobalTrackingProtectionPanelDialogFragment( - sessionId = session.id, - url = session.url, + sessionId = tab.id, + url = tab.content.url, trackingProtectionEnabled = isEnabled, gravity = getAppropriateLayoutGravity() ) diff --git a/app/src/test/java/org/mozilla/fenix/browser/BrowserFragmentTest.kt b/app/src/test/java/org/mozilla/fenix/browser/BrowserFragmentTest.kt index b3482825a4..adeb7b36ba 100644 --- a/app/src/test/java/org/mozilla/fenix/browser/BrowserFragmentTest.kt +++ b/app/src/test/java/org/mozilla/fenix/browser/BrowserFragmentTest.kt @@ -87,13 +87,13 @@ class BrowserFragmentTest { every { browserFragment.onboarding } returns onboarding every { browserFragment.requireContext() } returns context - every { browserFragment.initializeUI(any()) } returns mockk() + every { browserFragment.initializeUI(any(), any()) } returns mockk() every { browserFragment.fullScreenChanged(any()) } returns Unit every { browserFragment.resumeDownloadDialogState(any(), any(), any(), any(), any()) } returns Unit + testTab = createTab(url = "https://mozilla.org") store = BrowserStore() every { context.components.core.store } returns store - testTab = createTab(url = "https://mozilla.org") } @After @@ -122,10 +122,10 @@ class BrowserFragmentTest { @Test fun `GIVEN browser UI is not initialized WHEN selected tab changes THEN browser UI is initialized`() { browserFragment.observeTabSelection(store) - verify(exactly = 0) { browserFragment.initializeUI(view) } + verify(exactly = 0) { browserFragment.initializeUI(view, testTab) } addAndSelectTab(testTab) - verify(exactly = 1) { browserFragment.initializeUI(view) } + verify(exactly = 1) { browserFragment.initializeUI(view, testTab) } } @Test