From 5fc19e67558d657f6682ecbfe6c1e63e2edaf25b Mon Sep 17 00:00:00 2001 From: Arturo Mejia Date: Tue, 19 Dec 2023 16:10:09 -0500 Subject: [PATCH] Bug 1867717 - Add DownloadsFeature to AddonPopupBaseFragment. --- .../addons/AddonInternalSettingsFragment.kt | 18 +- .../fenix/addons/AddonPopupBaseFragment.kt | 124 ++++++++++++- .../addons/WebExtensionActionPopupFragment.kt | 19 +- .../fenix/browser/BaseBrowserFragment.kt | 85 +++++---- .../fenix/browser/DownloadDialogUtils.kt | 62 +++++++ .../mozilla/fenix/browser/DownloadUtils.kt | 53 ------ .../mozilla/fenix/components/Components.kt | 11 ++ .../fenix/components/DownloadStyling.kt | 36 ++++ .../fragment_add_on_internal_settings.xml | 14 ++ .../addons/AddonPopupBaseFragmentTest.kt | 53 ++++++ .../fenix/browser/BaseBrowserFragmentTest.kt | 73 +------- .../fenix/browser/DownloadDialogUtilsTest.kt | 173 ++++++++++++++++++ 12 files changed, 557 insertions(+), 164 deletions(-) create mode 100644 app/src/main/java/org/mozilla/fenix/browser/DownloadDialogUtils.kt delete mode 100644 app/src/main/java/org/mozilla/fenix/browser/DownloadUtils.kt create mode 100644 app/src/main/java/org/mozilla/fenix/components/DownloadStyling.kt create mode 100644 app/src/test/java/org/mozilla/fenix/addons/AddonPopupBaseFragmentTest.kt create mode 100644 app/src/test/java/org/mozilla/fenix/browser/DownloadDialogUtilsTest.kt diff --git a/app/src/main/java/org/mozilla/fenix/addons/AddonInternalSettingsFragment.kt b/app/src/main/java/org/mozilla/fenix/addons/AddonInternalSettingsFragment.kt index 8c08a95fd..7155c90ee 100644 --- a/app/src/main/java/org/mozilla/fenix/addons/AddonInternalSettingsFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/addons/AddonInternalSettingsFragment.kt @@ -12,6 +12,7 @@ import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs import mozilla.components.feature.addons.ui.translateName import org.mozilla.fenix.R +import org.mozilla.fenix.databinding.DownloadDialogLayoutBinding import org.mozilla.fenix.databinding.FragmentAddOnInternalSettingsBinding import org.mozilla.fenix.ext.showToolbar @@ -21,6 +22,8 @@ import org.mozilla.fenix.ext.showToolbar class AddonInternalSettingsFragment : AddonPopupBaseFragment() { private val args by navArgs() + private var _binding: FragmentAddOnInternalSettingsBinding? = null + internal val binding get() = _binding!! override fun onCreateView( inflater: LayoutInflater, @@ -31,9 +34,17 @@ class AddonInternalSettingsFragment : AddonPopupBaseFragment() { return inflater.inflate(R.layout.fragment_add_on_internal_settings, container, false) } + override fun getSnackBarContainer(): ViewGroup { + return binding.dynamicSnackbarContainer + } + + override fun getDownloadDialogLayoutBinding(): DownloadDialogLayoutBinding { + return binding.viewDynamicDownloadDialog + } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - val binding = FragmentAddOnInternalSettingsBinding.bind(view) + _binding = FragmentAddOnInternalSettingsBinding.bind(view) args.addon.installedState?.optionsPageUrl?.let { engineSession?.let { engineSession -> binding.addonSettingsEngineView.render(engineSession) @@ -48,4 +59,9 @@ class AddonInternalSettingsFragment : AddonPopupBaseFragment() { showToolbar(title = args.addon.translateName(it)) } } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } } diff --git a/app/src/main/java/org/mozilla/fenix/addons/AddonPopupBaseFragment.kt b/app/src/main/java/org/mozilla/fenix/addons/AddonPopupBaseFragment.kt index b1c54ee1f..05eeb740b 100644 --- a/app/src/main/java/org/mozilla/fenix/addons/AddonPopupBaseFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/addons/AddonPopupBaseFragment.kt @@ -5,21 +5,33 @@ package org.mozilla.fenix.addons import android.os.Bundle +import android.os.Environment import android.view.View +import android.view.ViewGroup +import androidx.annotation.VisibleForTesting import androidx.fragment.app.Fragment import androidx.navigation.fragment.findNavController +import com.google.android.material.snackbar.Snackbar import mozilla.components.browser.state.action.ContentAction import mozilla.components.browser.state.action.CustomTabListAction import mozilla.components.browser.state.state.CustomTabSessionState import mozilla.components.browser.state.state.EngineState import mozilla.components.browser.state.state.SessionState +import mozilla.components.browser.state.state.content.DownloadState import mozilla.components.browser.state.state.createCustomTab import mozilla.components.concept.engine.EngineSession import mozilla.components.concept.engine.prompt.PromptRequest import mozilla.components.concept.engine.window.WindowRequest +import mozilla.components.concept.fetch.Response +import mozilla.components.feature.downloads.DownloadsFeature import mozilla.components.feature.prompts.PromptFeature import mozilla.components.support.base.feature.UserInteractionHandler import mozilla.components.support.base.feature.ViewBoundFeatureWrapper +import org.mozilla.fenix.browser.DownloadDialogUtils +import org.mozilla.fenix.components.DownloadStyling +import org.mozilla.fenix.components.FenixSnackbar +import org.mozilla.fenix.databinding.DownloadDialogLayoutBinding +import org.mozilla.fenix.downloads.DynamicDownloadDialog import org.mozilla.fenix.ext.requireComponents /** @@ -28,19 +40,22 @@ import org.mozilla.fenix.ext.requireComponents */ abstract class AddonPopupBaseFragment : Fragment(), EngineSession.Observer, UserInteractionHandler { private val promptsFeature = ViewBoundFeatureWrapper() + private val downloadsFeature = ViewBoundFeatureWrapper() - protected var session: SessionState? = null + internal var session: SessionState? = null protected var engineSession: EngineSession? = null private var canGoBack: Boolean = false @Suppress("DEPRECATION") // https://github.com/mozilla-mobile/fenix/issues/19920 override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + val store = requireComponents.core.store + val safeContext = requireContext() session?.let { promptsFeature.set( feature = PromptFeature( fragment = this, - store = requireComponents.core.store, + store = store, customTabId = it.id, fragmentManager = parentFragmentManager, fileUploadsDirCleaner = requireComponents.core.fileUploadsDirCleaner, @@ -52,6 +67,52 @@ abstract class AddonPopupBaseFragment : Fragment(), EngineSession.Observer, User owner = this, view = view, ) + + downloadsFeature.set( + DownloadsFeature( + safeContext, + store = store, + useCases = requireComponents.useCases.downloadUseCases, + fragmentManager = parentFragmentManager, + tabId = it.id, + downloadManager = requireComponents.downloadManager, + promptsStyling = DownloadStyling.createPrompt(safeContext), + onNeedToRequestPermissions = { permissions -> + requestPermissions(permissions, REQUEST_CODE_DOWNLOAD_PERMISSIONS) + }, + ), + owner = this, + view = view, + ) + + downloadsFeature.get()?.onDownloadStopped = { downloadState, _, downloadJobStatus -> + val onCannotOpenFile: (DownloadState) -> Unit = { + FenixSnackbar.make( + view = getSnackBarContainer(), + duration = Snackbar.LENGTH_SHORT, + isDisplayedWithBrowserToolbar = true, + ).setText(DynamicDownloadDialog.getCannotOpenFileErrorMessage(requireContext(), it)) + .show() + } + DownloadDialogUtils.handleOnDownloadFinished( + context = requireContext(), + downloadState = downloadState, + downloadJobStatus = downloadJobStatus, + currentTab = it, + onCannotOpenFile = onCannotOpenFile, + onFinishedDialogShown = { + DynamicDownloadDialog( + context = requireContext(), + downloadState = downloadState, + didFail = downloadJobStatus == DownloadState.Status.FAILED, + tryAgain = downloadsFeature.get()!!::tryAgain, + onCannotOpenFile = onCannotOpenFile, + binding = getDownloadDialogLayoutBinding(), + toolbarHeight = 0, + ) {}.show() + }, + ) + } } } @@ -73,6 +134,44 @@ abstract class AddonPopupBaseFragment : Fragment(), EngineSession.Observer, User engineSession?.unregister(this) } + override fun onExternalResource( + url: String, + fileName: String?, + contentLength: Long?, + contentType: String?, + cookie: String?, + userAgent: String?, + isPrivate: Boolean, + skipConfirmation: Boolean, + openInApp: Boolean, + response: Response?, + ) { + session?.let { session -> + val fileSize = if (contentLength != null && contentLength < 0) null else contentLength + val download = DownloadState( + url, + fileName, + contentType, + fileSize, + 0, + DownloadState.Status.INITIATED, + userAgent, + Environment.DIRECTORY_DOWNLOADS, + private = isPrivate, + skipConfirmation = skipConfirmation, + openInApp = openInApp, + response = response, + ) + + provideBrowserStore().dispatch( + ContentAction.UpdateDownloadAction( + session.id, + download, + ), + ) + } + } + override fun onPromptRequest(promptRequest: PromptRequest) { session?.let { session -> requireComponents.core.store.dispatch( @@ -114,17 +213,34 @@ abstract class AddonPopupBaseFragment : Fragment(), EngineSession.Observer, User requireComponents.core.store.dispatch(CustomTabListAction.AddCustomTabAction(session as CustomTabSessionState)) } + @VisibleForTesting + internal fun provideBrowserStore() = requireComponents.core.store + final override fun onRequestPermissionsResult( requestCode: Int, permissions: Array, grantResults: IntArray, ) { - when (requestCode) { - REQUEST_CODE_PROMPT_PERMISSIONS -> promptsFeature.get()?.onPermissionsResult(permissions, grantResults) + val feature = when (requestCode) { + REQUEST_CODE_PROMPT_PERMISSIONS -> promptsFeature.get() + REQUEST_CODE_DOWNLOAD_PERMISSIONS -> downloadsFeature.get() + else -> null } + feature?.onPermissionsResult(permissions, grantResults) } + /** + * Returns a [ViewGroup] where a SnackBar message should be anchored. + */ + abstract fun getSnackBarContainer(): ViewGroup + + /** + * Returns a [DownloadDialogLayoutBinding] to access the download dialog items. + */ + abstract fun getDownloadDialogLayoutBinding(): DownloadDialogLayoutBinding + companion object { private const val REQUEST_CODE_PROMPT_PERMISSIONS = 1 + private const val REQUEST_CODE_DOWNLOAD_PERMISSIONS = 2 } } diff --git a/app/src/main/java/org/mozilla/fenix/addons/WebExtensionActionPopupFragment.kt b/app/src/main/java/org/mozilla/fenix/addons/WebExtensionActionPopupFragment.kt index 95b2246c5..12fa3597a 100644 --- a/app/src/main/java/org/mozilla/fenix/addons/WebExtensionActionPopupFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/addons/WebExtensionActionPopupFragment.kt @@ -15,6 +15,7 @@ import mozilla.components.concept.engine.EngineSession import mozilla.components.concept.engine.EngineView import mozilla.components.lib.state.ext.consumeFrom import org.mozilla.fenix.R +import org.mozilla.fenix.databinding.DownloadDialogLayoutBinding import org.mozilla.fenix.databinding.FragmentAddOnInternalSettingsBinding import org.mozilla.fenix.ext.requireComponents import org.mozilla.fenix.ext.showToolbar @@ -33,6 +34,9 @@ class WebExtensionActionPopupFragment : AddonPopupBaseFragment(), EngineSession. safeArguments.putBoolean("isSessionConsumed", value) } + private var _binding: FragmentAddOnInternalSettingsBinding? = null + internal val binding get() = _binding!! + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -46,6 +50,14 @@ class WebExtensionActionPopupFragment : AddonPopupBaseFragment(), EngineSession. return inflater.inflate(R.layout.fragment_add_on_internal_settings, container, false) } + override fun getSnackBarContainer(): ViewGroup { + return binding.dynamicSnackbarContainer + } + + override fun getDownloadDialogLayoutBinding(): DownloadDialogLayoutBinding { + return binding.viewDynamicDownloadDialog + } + override fun onResume() { super.onResume() val title = args.webExtensionTitle ?: args.webExtensionId @@ -55,7 +67,7 @@ class WebExtensionActionPopupFragment : AddonPopupBaseFragment(), EngineSession. override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - val binding = FragmentAddOnInternalSettingsBinding.bind(view) + _binding = FragmentAddOnInternalSettingsBinding.bind(view) val session = engineSession // If we have the session, render it otherwise consume it from the store. @@ -82,6 +94,11 @@ class WebExtensionActionPopupFragment : AddonPopupBaseFragment(), EngineSession. } } + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + private fun consumePopupSession() { coreComponents.store.dispatch( WebExtensionAction.UpdatePopupSessionAction(args.webExtensionId, popupSession = null), 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 cf237221d..db23b580b 100644 --- a/app/src/main/java/org/mozilla/fenix/browser/BaseBrowserFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/browser/BaseBrowserFragment.kt @@ -69,7 +69,6 @@ import mozilla.components.feature.app.links.AppLinksFeature import mozilla.components.feature.contextmenu.ContextMenuCandidate import mozilla.components.feature.contextmenu.ContextMenuFeature import mozilla.components.feature.downloads.DownloadsFeature -import mozilla.components.feature.downloads.manager.FetchDownloadManager import mozilla.components.feature.downloads.temporary.CopyDownloadFeature import mozilla.components.feature.downloads.temporary.ShareDownloadFeature import mozilla.components.feature.intent.ext.EXTRA_SESSION_ID @@ -125,6 +124,7 @@ import org.mozilla.fenix.OnBackLongPressedListener import org.mozilla.fenix.R import org.mozilla.fenix.browser.browsingmode.BrowsingMode import org.mozilla.fenix.browser.readermode.DefaultReaderModeController +import org.mozilla.fenix.components.DownloadStyling import org.mozilla.fenix.components.FenixSnackbar import org.mozilla.fenix.components.FindInPageIntegration import org.mozilla.fenix.components.StoreProvider @@ -139,7 +139,6 @@ import org.mozilla.fenix.components.toolbar.interactor.BrowserToolbarInteractor import org.mozilla.fenix.components.toolbar.interactor.DefaultBrowserToolbarInteractor import org.mozilla.fenix.crashes.CrashContentIntegration import org.mozilla.fenix.databinding.FragmentBrowserBinding -import org.mozilla.fenix.downloads.DownloadService import org.mozilla.fenix.downloads.DynamicDownloadDialog import org.mozilla.fenix.downloads.FirstPartyDownloadDialog import org.mozilla.fenix.downloads.StartDownloadDialog @@ -531,31 +530,14 @@ abstract class BaseBrowserFragment : useCases = context.components.useCases.downloadUseCases, fragmentManager = childFragmentManager, tabId = customTabSessionId, - downloadManager = FetchDownloadManager( - context.applicationContext, - store, - DownloadService::class, - notificationsDelegate = context.components.notificationsDelegate, - ), + downloadManager = context.components.downloadManager, 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.textOnColorPrimary, - context, - ), - positiveButtonRadius = (resources.getDimensionPixelSize(R.dimen.tab_corner_radius)).toFloat(), - ), + promptsStyling = DownloadStyling.createPrompt(context), onNeedToRequestPermissions = { permissions -> requestPermissions(permissions, REQUEST_CODE_DOWNLOAD_PERMISSIONS) }, @@ -608,7 +590,46 @@ abstract class BaseBrowserFragment : ) downloadFeature.onDownloadStopped = { downloadState, _, downloadJobStatus -> - handleOnDownloadFinished(downloadState, downloadJobStatus, downloadFeature::tryAgain) + val onCannotOpenFile: (DownloadState) -> Unit = { + FenixSnackbar.make( + view = binding.dynamicSnackbarContainer, + duration = Snackbar.LENGTH_SHORT, + isDisplayedWithBrowserToolbar = true, + ).setText( + DynamicDownloadDialog.getCannotOpenFileErrorMessage( + context, + downloadState, + ), + ).show() + } + + DownloadDialogUtils.handleOnDownloadFinished( + context = requireContext(), + downloadState = downloadState, + downloadJobStatus = downloadJobStatus, + currentTab = getCurrentTab(), + onFinishedDialogShown = { + saveDownloadDialogState( + downloadState.sessionId, + downloadState, + downloadJobStatus, + ) + browserToolbarView.expand() + + DynamicDownloadDialog( + context = context, + downloadState = downloadState, + didFail = downloadJobStatus == DownloadState.Status.FAILED, + tryAgain = downloadFeature::tryAgain, + onCannotOpenFile = onCannotOpenFile, + binding = binding.viewDynamicDownloadDialog, + toolbarHeight = toolbarHeight, + ) { + sharedViewModel.downloadDialogState.remove(downloadState.sessionId) + }.show() + }, + onCannotOpenFile = onCannotOpenFile, + ) } resumeDownloadDialogState( @@ -1098,7 +1119,12 @@ abstract class BaseBrowserFragment : didFail = savedDownloadState.second, tryAgain = onTryAgain, onCannotOpenFile = { - showCannotOpenFileError(binding.dynamicSnackbarContainer, context, it) + FenixSnackbar.make( + view = binding.dynamicSnackbarContainer, + duration = Snackbar.LENGTH_SHORT, + isDisplayedWithBrowserToolbar = true, + ).setText(DynamicDownloadDialog.getCannotOpenFileErrorMessage(context, it)) + .show() }, binding = binding.viewDynamicDownloadDialog, toolbarHeight = toolbarHeight, @@ -1599,19 +1625,6 @@ abstract class BaseBrowserFragment : ) } - internal fun showCannotOpenFileError( - container: ViewGroup, - context: Context, - downloadState: DownloadState, - ) { - FenixSnackbar.make( - view = container, - duration = Snackbar.LENGTH_SHORT, - isDisplayedWithBrowserToolbar = true, - ).setText(DynamicDownloadDialog.getCannotOpenFileErrorMessage(context, downloadState)) - .show() - } - companion object { private const val KEY_CUSTOM_TAB_SESSION_ID = "custom_tab_session_id" private const val REQUEST_CODE_DOWNLOAD_PERMISSIONS = 1 diff --git a/app/src/main/java/org/mozilla/fenix/browser/DownloadDialogUtils.kt b/app/src/main/java/org/mozilla/fenix/browser/DownloadDialogUtils.kt new file mode 100644 index 000000000..ee5926b5b --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/browser/DownloadDialogUtils.kt @@ -0,0 +1,62 @@ +/* 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.browser + +import android.content.Context +import androidx.annotation.VisibleForTesting +import mozilla.components.browser.state.state.SessionState +import mozilla.components.browser.state.state.content.DownloadState +import mozilla.components.browser.state.state.content.DownloadState.Status +import mozilla.components.feature.downloads.AbstractFetchDownloadService + +/** + * Utilities for handling download dialogs. + */ +object DownloadDialogUtils { + + internal fun handleOnDownloadFinished( + context: Context, + downloadState: DownloadState, + downloadJobStatus: Status, + currentTab: SessionState?, + onFinishedDialogShown: () -> Unit = {}, + onCannotOpenFile: (DownloadState) -> Unit, + ) { + // If the download is just paused, don't show any in-app notification + if (shouldShowCompletedDownloadDialog(downloadState, downloadJobStatus, currentTab)) { + if (downloadState.openInApp && downloadJobStatus == Status.COMPLETED) { + val fileWasOpened = openFile(context, downloadState) + if (!fileWasOpened) { + onCannotOpenFile(downloadState) + } + } else { + onFinishedDialogShown() + } + } + } + + @VisibleForTesting + internal var openFile: (Context, DownloadState) -> (Boolean) = { context, downloadState -> + AbstractFetchDownloadService.openFile( + applicationContext = context.applicationContext, + download = downloadState, + ) + } + + /** + * Indicates whether or not a completed download dialog should be shown. + */ + fun shouldShowCompletedDownloadDialog( + downloadState: DownloadState, + status: Status, + currentTab: SessionState?, + ): Boolean { + val isValidStatus = + status in listOf(Status.COMPLETED, Status.FAILED) + val isSameTab = downloadState.sessionId == (currentTab?.id ?: false) + + return isValidStatus && isSameTab + } +} diff --git a/app/src/main/java/org/mozilla/fenix/browser/DownloadUtils.kt b/app/src/main/java/org/mozilla/fenix/browser/DownloadUtils.kt deleted file mode 100644 index ed0930f76..000000000 --- a/app/src/main/java/org/mozilla/fenix/browser/DownloadUtils.kt +++ /dev/null @@ -1,53 +0,0 @@ -/* 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.browser - -import mozilla.components.browser.state.state.content.DownloadState -import mozilla.components.browser.state.state.content.DownloadState.Status -import mozilla.components.feature.downloads.AbstractFetchDownloadService -import org.mozilla.fenix.R -import org.mozilla.fenix.downloads.DynamicDownloadDialog - -internal fun BaseBrowserFragment.handleOnDownloadFinished( - downloadState: DownloadState, - downloadJobStatus: Status, - tryAgain: (String) -> Unit, -) { - // If the download is just paused, don't show any in-app notification - if (shouldShowCompletedDownloadDialog(downloadState, downloadJobStatus)) { - val safeContext = context ?: return - val onCannotOpenFile: (DownloadState) -> Unit = { - showCannotOpenFileError(binding.dynamicSnackbarContainer, safeContext, it) - } - if (downloadState.openInApp && downloadJobStatus == Status.COMPLETED) { - val fileWasOpened = AbstractFetchDownloadService.openFile( - applicationContext = safeContext.applicationContext, - download = downloadState, - ) - if (!fileWasOpened) { - onCannotOpenFile(downloadState) - } - } else { - saveDownloadDialogState( - downloadState.sessionId, - downloadState, - downloadJobStatus, - ) - - val dynamicDownloadDialog = DynamicDownloadDialog( - context = safeContext, - downloadState = downloadState, - didFail = downloadJobStatus == Status.FAILED, - tryAgain = tryAgain, - onCannotOpenFile = onCannotOpenFile, - binding = binding.viewDynamicDownloadDialog, - toolbarHeight = resources.getDimensionPixelSize(R.dimen.browser_toolbar_height), - ) { sharedViewModel.downloadDialogState.remove(downloadState.sessionId) } - - dynamicDownloadDialog.show() - browserToolbarView.expand() - } - } -} diff --git a/app/src/main/java/org/mozilla/fenix/components/Components.kt b/app/src/main/java/org/mozilla/fenix/components/Components.kt index 0346f0b4f..40d316b8a 100644 --- a/app/src/main/java/org/mozilla/fenix/components/Components.kt +++ b/app/src/main/java/org/mozilla/fenix/components/Components.kt @@ -16,6 +16,7 @@ import mozilla.components.feature.addons.amo.AMOAddonsProvider import mozilla.components.feature.addons.migration.DefaultSupportedAddonsChecker import mozilla.components.feature.addons.update.DefaultAddonUpdater import mozilla.components.feature.autofill.AutofillConfiguration +import mozilla.components.feature.downloads.manager.FetchDownloadManager import mozilla.components.lib.publicsuffixlist.PublicSuffixList import mozilla.components.support.base.android.NotificationsDelegate import mozilla.components.support.base.worker.Frequency @@ -29,6 +30,7 @@ import org.mozilla.fenix.autofill.AutofillUnlockActivity import org.mozilla.fenix.components.appstate.AppState import org.mozilla.fenix.components.metrics.MetricsMiddleware import org.mozilla.fenix.datastore.pocketStoriesSelectedCategoriesDataStore +import org.mozilla.fenix.downloads.DownloadService import org.mozilla.fenix.ext.asRecentTabs import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.filterState @@ -99,6 +101,15 @@ class Components(private val context: Context) { ) } + val downloadManager by lazyMonitored { + FetchDownloadManager( + context.applicationContext, + core.store, + DownloadService::class, + notificationsDelegate = notificationsDelegate, + ) + } + val intentProcessors by lazyMonitored { IntentProcessors( context, diff --git a/app/src/main/java/org/mozilla/fenix/components/DownloadStyling.kt b/app/src/main/java/org/mozilla/fenix/components/DownloadStyling.kt new file mode 100644 index 000000000..199f2312b --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/components/DownloadStyling.kt @@ -0,0 +1,36 @@ +/* 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.components + +import android.content.Context +import android.view.Gravity +import mozilla.components.feature.downloads.DownloadsFeature +import org.mozilla.fenix.R +import org.mozilla.fenix.theme.ThemeManager + +/** + * Provides access to all Fenix download styling. + */ +object DownloadStyling { + + /** + * creates [DownloadsFeature.PromptsStyling]. + */ + fun createPrompt(context: Context): DownloadsFeature.PromptsStyling { + return DownloadsFeature.PromptsStyling( + gravity = Gravity.BOTTOM, + shouldWidthMatchParent = true, + positiveButtonBackgroundColor = ThemeManager.resolveAttribute( + R.attr.accent, + context, + ), + positiveButtonTextColor = ThemeManager.resolveAttribute( + R.attr.textOnColorPrimary, + context, + ), + positiveButtonRadius = (context.resources.getDimensionPixelSize(R.dimen.tab_corner_radius)).toFloat(), + ) + } +} diff --git a/app/src/main/res/layout/fragment_add_on_internal_settings.xml b/app/src/main/res/layout/fragment_add_on_internal_settings.xml index 282f5bd3b..0b3fa0573 100644 --- a/app/src/main/res/layout/fragment_add_on_internal_settings.xml +++ b/app/src/main/res/layout/fragment_add_on_internal_settings.xml @@ -13,4 +13,18 @@ android:id="@+id/addonSettingsEngineView" android:layout_width="match_parent" android:layout_height="match_parent" /> + + + + diff --git a/app/src/test/java/org/mozilla/fenix/addons/AddonPopupBaseFragmentTest.kt b/app/src/test/java/org/mozilla/fenix/addons/AddonPopupBaseFragmentTest.kt new file mode 100644 index 000000000..28fc9e261 --- /dev/null +++ b/app/src/test/java/org/mozilla/fenix/addons/AddonPopupBaseFragmentTest.kt @@ -0,0 +1,53 @@ +/* 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.addons + +import io.mockk.every +import io.mockk.mockk +import io.mockk.spyk +import io.mockk.verify +import mozilla.components.browser.state.action.ContentAction +import mozilla.components.browser.state.state.SessionState +import mozilla.components.browser.state.store.BrowserStore +import mozilla.components.concept.fetch.Response +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mozilla.fenix.helpers.FenixRobolectricTestRunner + +@RunWith(FenixRobolectricTestRunner::class) +class AddonPopupBaseFragmentTest { + + private lateinit var fragment: AddonPopupBaseFragment + + @Before + fun setup() { + fragment = spyk() + } + + @Test + fun `WHEN onExternalResource is call THEN dispatch an UpdateDownloadAction`() { + val store = mockk(relaxed = true) + val session = mockk(relaxed = true) + val response = mockk(relaxed = true) + every { fragment.provideBrowserStore() } returns store + + fragment.session = session + + fragment.onExternalResource( + url = "url", + fileName = "fileName", + contentLength = 1, + contentType = "contentType", + userAgent = "userAgent", + isPrivate = true, + skipConfirmation = false, + openInApp = false, + response = response, + ) + + verify { store.dispatch(any()) } + } +} diff --git a/app/src/test/java/org/mozilla/fenix/browser/BaseBrowserFragmentTest.kt b/app/src/test/java/org/mozilla/fenix/browser/BaseBrowserFragmentTest.kt index 4670b824b..9db600442 100644 --- a/app/src/test/java/org/mozilla/fenix/browser/BaseBrowserFragmentTest.kt +++ b/app/src/test/java/org/mozilla/fenix/browser/BaseBrowserFragmentTest.kt @@ -12,11 +12,7 @@ import io.mockk.mockk import io.mockk.slot import io.mockk.spyk import io.mockk.verify -import junit.framework.TestCase.assertFalse -import junit.framework.TestCase.assertTrue import mozilla.components.browser.state.state.SessionState -import mozilla.components.browser.state.state.content.DownloadState -import mozilla.components.browser.state.state.createTab import mozilla.components.concept.engine.EngineView import mozilla.components.concept.engine.permission.SitePermissions import mozilla.components.feature.contextmenu.ContextMenuCandidate @@ -108,6 +104,7 @@ class BaseBrowserFragmentTest { } @Test + @Suppress("ktlint:standard:max-line-length") fun `initializeEngineView should set toolbar height as EngineView parent's bottom margin when using bottom toolbar`() { every { settings.isDynamicToolbarEnabled } returns false every { settings.shouldUseBottomToolbar } returns true @@ -118,7 +115,7 @@ class BaseBrowserFragmentTest { } @Test - fun `initializeEngineView should set toolbar height as EngineView parent's bottom margin if top toolbar is forced for a11y`() { + fun `initializeEngineView set toolbar height as EngineView parent's bottom margin if top toolbar`() { every { settings.shouldUseBottomToolbar } returns false every { settings.shouldUseFixedTopToolbar } returns true @@ -128,7 +125,8 @@ class BaseBrowserFragmentTest { } @Test - fun `initializeEngineView should set toolbar height as EngineView parent's bottom margin if bottom toolbar is forced for a11y`() { + @Suppress("MaxLineLength") + fun `initializeEngineView set toolbar height as EngineView parent's bottom margin if bottom toolbar`() { every { settings.shouldUseBottomToolbar } returns true every { settings.shouldUseFixedTopToolbar } returns true @@ -136,69 +134,6 @@ class BaseBrowserFragmentTest { verify { (swipeRefreshLayout.layoutParams as CoordinatorLayout.LayoutParams).bottomMargin = 13 } } - - @Test - fun `WHEN status is equals to FAILED or COMPLETED and it is the same tab then shouldShowCompletedDownloadDialog will be true`() { - every { fragment.getCurrentTab() } returns createTab(id = "1", url = "") - - val download = DownloadState( - url = "", - sessionId = "1", - destinationDirectory = "/", - ) - - val status = DownloadState.Status.values() - .filter { it == DownloadState.Status.COMPLETED && it == DownloadState.Status.FAILED } - - status.forEach { - val result = - fragment.shouldShowCompletedDownloadDialog(download, it) - - assertTrue(result) - } - } - - @Test - fun `WHEN status is different from FAILED or COMPLETED then shouldShowCompletedDownloadDialog will be false`() { - every { fragment.getCurrentTab() } returns createTab(id = "1", url = "") - - val download = DownloadState( - url = "", - sessionId = "1", - destinationDirectory = "/", - ) - - val status = DownloadState.Status.values() - .filter { it != DownloadState.Status.COMPLETED && it != DownloadState.Status.FAILED } - - status.forEach { - val result = - fragment.shouldShowCompletedDownloadDialog(download, it) - - assertFalse(result) - } - } - - @Test - fun `WHEN the tab is different from the initial one then shouldShowCompletedDownloadDialog will be false`() { - every { fragment.getCurrentTab() } returns createTab(id = "1", url = "") - - val download = DownloadState( - url = "", - sessionId = "2", - destinationDirectory = "/", - ) - - val status = DownloadState.Status.values() - .filter { it != DownloadState.Status.COMPLETED && it != DownloadState.Status.FAILED } - - status.forEach { - val result = - fragment.shouldShowCompletedDownloadDialog(download, it) - - assertFalse(result) - } - } } private class TestBaseBrowserFragment : BaseBrowserFragment() { diff --git a/app/src/test/java/org/mozilla/fenix/browser/DownloadDialogUtilsTest.kt b/app/src/test/java/org/mozilla/fenix/browser/DownloadDialogUtilsTest.kt new file mode 100644 index 000000000..fbd3d013e --- /dev/null +++ b/app/src/test/java/org/mozilla/fenix/browser/DownloadDialogUtilsTest.kt @@ -0,0 +1,173 @@ +/* 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.browser + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import mozilla.components.browser.state.state.content.DownloadState +import mozilla.components.browser.state.state.content.DownloadState.Status.COMPLETED +import mozilla.components.browser.state.state.content.DownloadState.Status.FAILED +import mozilla.components.browser.state.state.createTab +import mozilla.components.support.test.robolectric.testContext +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class DownloadDialogUtilsTest { + + @Test + @Suppress("MaxLineLength") + fun `GIVEN final status WHEN handleOnDownloadFinished try to open the file fails THEN show error message`() { + val downloadUtils = DownloadDialogUtils + val currentTab = createTab(id = "1", url = "") + val download = DownloadState( + url = "", + sessionId = currentTab.id, + destinationDirectory = "/", + openInApp = true, + ) + var dialogWasShown = false + var onCannotOpenFileWasCalled = false + + downloadUtils.openFile = { _, _ -> false } + + downloadUtils.handleOnDownloadFinished( + context = testContext, + downloadState = download, + downloadJobStatus = COMPLETED, + currentTab = currentTab, + onFinishedDialogShown = { dialogWasShown = true }, + onCannotOpenFile = { + onCannotOpenFileWasCalled = true + }, + ) + + assertTrue(onCannotOpenFileWasCalled) + assertFalse(dialogWasShown) + } + + @Test + fun `GIVEN final status and openInApp WHEN calling handleOnDownloadFinished THEN try to open the file`() { + val downloadUtils = DownloadDialogUtils + val download = DownloadState( + url = "", + sessionId = "1", + destinationDirectory = "/", + openInApp = true, + ) + val currentTab = createTab(id = "1", url = "") + var dialogWasShown = false + var onCannotOpenFileWasCalled = false + + downloadUtils.openFile = { _, _ -> true } + + downloadUtils.handleOnDownloadFinished( + context = testContext, + downloadState = download, + downloadJobStatus = COMPLETED, + currentTab = currentTab, + onFinishedDialogShown = { dialogWasShown = true }, + onCannotOpenFile = { + onCannotOpenFileWasCalled = true + }, + ) + + assertFalse(onCannotOpenFileWasCalled) + assertFalse(dialogWasShown) + } + + @Test + fun `GIVEN final status WHEN calling handleOnDownloadFinished THEN show a finished download dialog`() { + val downloadUtils = DownloadDialogUtils + val download = DownloadState( + url = "", + sessionId = "1", + destinationDirectory = "/", + ) + val currentTab = createTab(id = "1", url = "") + var dialogWasShown = false + + downloadUtils.handleOnDownloadFinished( + context = testContext, + downloadState = download, + downloadJobStatus = COMPLETED, + currentTab = currentTab, + onFinishedDialogShown = { dialogWasShown = true }, + onCannotOpenFile = {}, + ) + + assertTrue(dialogWasShown) + } + + @Test + fun `WHEN status is final and is same tab THEN shouldShowCompletedDownloadDialog will be true`() { + val currentTab = createTab(id = "1", url = "") + + val download = DownloadState( + url = "", + sessionId = "1", + destinationDirectory = "/", + ) + + val status = DownloadState.Status.values().filter { it == COMPLETED || it == FAILED } + + status.forEach { + val result = DownloadDialogUtils.shouldShowCompletedDownloadDialog( + downloadState = download, + status = it, + currentTab = currentTab, + ) + + assertTrue(result) + } + } + + @Test + fun `WHEN status is different from FAILED or COMPLETED then shouldShowCompletedDownloadDialog will be false`() { + val currentTab = createTab(id = "1", url = "") + + val download = DownloadState( + url = "", + sessionId = "1", + destinationDirectory = "/", + ) + + val completedStatus = listOf(COMPLETED, FAILED) + val status = DownloadState.Status.values().filter { it !in completedStatus } + + status.forEach { + val result = DownloadDialogUtils.shouldShowCompletedDownloadDialog( + downloadState = download, + status = it, + currentTab = currentTab, + ) + + assertFalse(result) + } + } + + @Test + fun `WHEN the tab is different from the initial one then shouldShowCompletedDownloadDialog will be false`() { + val currentTab = createTab(id = "1", url = "") + + val download = DownloadState( + url = "", + sessionId = "2", + destinationDirectory = "/", + ) + val completedStatus = listOf(COMPLETED, FAILED) + val status = DownloadState.Status.values().filter { it !in completedStatus } + status.forEach { + val result = + DownloadDialogUtils.shouldShowCompletedDownloadDialog( + downloadState = download, + status = it, + currentTab = currentTab, + ) + assertFalse(result) + } + } +}