tor-commits
Threads by month
- ----- 2025 -----
- June
- May
- April
- March
- February
- January
- ----- 2024 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2023 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2022 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2021 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2020 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2019 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2018 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2017 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2016 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2015 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2014 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2013 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2012 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2011 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
March 2023
- 3 participants
- 142 discussions

[Git][tpo/applications/fenix][tor-browser-102.2.1-12.0-2] 5 commits: Bug 1812518 - Control the snackbar positioning from Fenix
by Richard Pospesel (@richard) 16 Mar '23
by Richard Pospesel (@richard) 16 Mar '23
16 Mar '23
Richard Pospesel pushed to branch tor-browser-102.2.1-12.0-2 at The Tor Project / Applications / fenix
Commits:
6b5be770 by Mugurell at 2023-03-15T18:43:49+00:00
Bug 1812518 - Control the snackbar positioning from Fenix
Previously Android-Components - BrowserToolbarBehavior would be responsible
for positioning the snackbar above the toolbar.
With that responsibility removed we can handle in Fenix positioning the
snackbar depending on the toolbar and many more cases - like positioning it
depending on the download dialogs.
- - - - -
09278aff by Mugurell at 2023-03-15T18:49:18+00:00
Bug 1812518 - Show the download dialog as an Android View
Tried to mimic the UX of a modal dialog while using Android Views.
This meant including a scrim that would consume all touches and theming the
navigation bar and status bar.
Avoiding a dialog and a separate window will allow the snackbar to see the
new "dialog" as a sibling in a CoordinatorLayout parent and so be able to
position itself based on the new "dialog".
This patch also added "start_download_dialog_layout" from A-C as it leads to
simpler and less code needed to style the layout - colors / shapes with
everything happening in XML versus calculating the values then setting them
programatically.
- - - - -
9eb98437 by Mugurell at 2023-03-15T18:49:50+00:00
Bug 1812518 - Show the 3rd party download dialog as an Android View
This uses the same direction as the before patch - inflating a new View that
can then serve as an anchor for the Snackbar.
Here we could use directly the AC layout as it needed no special customization.
- - - - -
94d239f2 by Mugurell at 2023-03-15T19:03:40+00:00
Bug 1812518 - Fix UI tests affected by the refactoring.
- - - - -
cf3ce1dd by Mugurell at 2023-03-15T19:06:08+00:00
Bug 1812518 - Update to latest AndroidComponents
The new version will contain the support for allowing to style download dialogs.
- - - - -
20 changed files:
- app/src/androidTest/java/org/mozilla/fenix/ui/CollectionTest.kt
- app/src/androidTest/java/org/mozilla/fenix/ui/DownloadFileTypesTest.kt
- app/src/androidTest/java/org/mozilla/fenix/ui/DownloadTest.kt
- app/src/androidTest/java/org/mozilla/fenix/ui/robots/DownloadRobot.kt
- app/src/main/java/org/mozilla/fenix/browser/BaseBrowserFragment.kt
- app/src/main/java/org/mozilla/fenix/browser/BrowserFragment.kt
- app/src/main/java/org/mozilla/fenix/components/FenixSnackbar.kt
- + app/src/main/java/org/mozilla/fenix/components/FenixSnackbarBehavior.kt
- + app/src/main/java/org/mozilla/fenix/downloads/StartDownloadDialog.kt
- + app/src/main/res/drawable/download_dialog_download_button_background.xml
- + app/src/main/res/layout/dialog_scrim.xml
- app/src/main/res/layout/fragment_browser.xml
- + app/src/main/res/layout/start_download_dialog_layout.xml
- app/src/main/res/values/colors.xml
- app/src/main/res/values/dimens.xml
- + app/src/test/java/org/mozilla/fenix/components/FenixSnackbarBehaviorTest.kt
- + app/src/test/java/org/mozilla/fenix/downloads/FirstPartyDownloadDialogTest.kt
- + app/src/test/java/org/mozilla/fenix/downloads/StartDownloadDialogTest.kt
- + app/src/test/java/org/mozilla/fenix/downloads/ThirdPartyDownloadDialogTest.kt
- app/src/test/java/org/mozilla/fenix/tabstray/ext/FenixSnackbarKtTest.kt
Changes:
=====================================
app/src/androidTest/java/org/mozilla/fenix/ui/CollectionTest.kt
=====================================
@@ -57,8 +57,10 @@ class CollectionTest {
featureSettingsHelper.resetAllFeatureFlags()
}
- @Test
+
// open a webpage, and add currently opened tab to existing collection
+ @Ignore("Failing, see: https://bugzilla.mozilla.org/show_bug.cgi?id=1812580")
+ @Test
fun mainMenuSaveToExistingCollection() {
val firstWebPage = getGenericAsset(mockWebServer, 1)
val secondWebPage = getGenericAsset(mockWebServer, 2)
@@ -84,6 +86,7 @@ class CollectionTest {
}
}
+ @Ignore("Failing, see: https://bugzilla.mozilla.org/show_bug.cgi?id=1812580")
@Test
fun verifyAddTabButtonOfCollectionMenu() {
val firstWebPage = getGenericAsset(mockWebServer, 1)
=====================================
app/src/androidTest/java/org/mozilla/fenix/ui/DownloadFileTypesTest.kt
=====================================
@@ -75,7 +75,7 @@ class DownloadFileTypesTest(fileName: String) {
verifyDownloadPrompt(downloadFile)
}.clickDownload {
verifyDownloadNotificationPopup()
- }.closePrompt {
+ }.closeCompletedDownloadPrompt {
}.openThreeDotMenu {
}.openDownloadsManager {
waitForDownloadsListToExist()
=====================================
app/src/androidTest/java/org/mozilla/fenix/ui/DownloadTest.kt
=====================================
@@ -105,7 +105,7 @@ class DownloadTest {
verifyDownloadPrompt(downloadFile)
}.clickDownload {
verifyDownloadNotificationPopup()
- }.closePrompt { }
+ }
mDevice.openNotification()
notificationShade {
verifySystemNotificationExists("Download completed")
=====================================
app/src/androidTest/java/org/mozilla/fenix/ui/robots/DownloadRobot.kt
=====================================
@@ -12,7 +12,6 @@ import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.intent.Intents
import androidx.test.espresso.intent.matcher.IntentMatchers
-import androidx.test.espresso.matcher.RootMatchers.isDialog
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withContentDescription
import androidx.test.espresso.matcher.ViewMatchers.withId
@@ -82,6 +81,13 @@ class DownloadRobot {
return Transition()
}
+ fun closeCompletedDownloadPrompt(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
+ closeCompletedDownloadButton().click()
+
+ BrowserRobot().interact()
+ return BrowserRobot.Transition()
+ }
+
fun closePrompt(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
closePromptButton().click()
@@ -177,12 +183,14 @@ private fun assertDownloadNotificationPopup() {
)
}
+private fun closeCompletedDownloadButton() =
+ onView(withId(R.id.download_dialog_close_button))
+
private fun closePromptButton() =
- onView(withContentDescription("Close"))
+ onView(withId(R.id.close_button))
private fun downloadButton() =
onView(withText("Download"))
- .inRoot(isDialog())
.check(matches(isDisplayed()))
private fun openDownloadButton() =
=====================================
app/src/main/java/org/mozilla/fenix/browser/BaseBrowserFragment.kt
=====================================
@@ -108,6 +108,9 @@ import org.mozilla.fenix.components.toolbar.DefaultBrowserToolbarMenuController
import org.mozilla.fenix.components.toolbar.ToolbarIntegration
import org.mozilla.fenix.downloads.DownloadService
import org.mozilla.fenix.downloads.DynamicDownloadDialog
+import org.mozilla.fenix.downloads.FirstPartyDownloadDialog
+import org.mozilla.fenix.downloads.StartDownloadDialog
+import org.mozilla.fenix.downloads.ThirdPartyDownloadDialog
import org.mozilla.fenix.ext.accessibilityManager
import org.mozilla.fenix.ext.breadcrumb
import org.mozilla.fenix.ext.components
@@ -213,6 +216,8 @@ abstract class BaseBrowserFragment :
@VisibleForTesting
internal val onboarding by lazy { FenixOnboarding(requireContext()) }
+ private var currentStartDownloadDialog: StartDownloadDialog? = null
+
@CallSuper
override fun onCreateView(
inflater: LayoutInflater,
@@ -345,7 +350,7 @@ abstract class BaseBrowserFragment :
}
viewLifecycleOwner.lifecycleScope.allowUndo(
- binding.browserLayout,
+ binding.dynamicSnackbarContainer,
snackbarMessage,
requireContext().getString(R.string.snackbar_deleted_undo),
{
@@ -424,7 +429,7 @@ abstract class BaseBrowserFragment :
feature = ContextMenuFeature(
fragmentManager = parentFragmentManager,
store = store,
- candidates = getContextMenuCandidates(context, binding.browserLayout),
+ candidates = getContextMenuCandidates(context, binding.dynamicSnackbarContainer),
engineView = binding.engineView,
useCases = context.components.useCases.contextMenuUseCases,
tabId = customTabSessionId
@@ -493,7 +498,32 @@ abstract class BaseBrowserFragment :
),
onNeedToRequestPermissions = { permissions ->
requestPermissions(permissions, REQUEST_CODE_DOWNLOAD_PERMISSIONS)
- }
+ },
+ customFirstPartyDownloadDialog = { filename, contentSize, positiveAction, negativeAction ->
+ FirstPartyDownloadDialog(
+ activity = requireActivity(),
+ filename = filename.value,
+ contentSize = contentSize.value,
+ positiveButtonAction = positiveAction.value,
+ negativeButtonAction = negativeAction.value,
+ ).onDismiss {
+ currentStartDownloadDialog = null
+ }.show(binding.startDownloadDialogContainer).also {
+ currentStartDownloadDialog = it
+ }
+ },
+ customThirdPartyDownloadDialog = { downloaderApps, onAppSelected, negativeActionCallback ->
+ ThirdPartyDownloadDialog(
+ activity = requireActivity(),
+ downloaderApps = downloaderApps.value,
+ onAppSelected = onAppSelected.value,
+ negativeButtonAction = negativeActionCallback.value,
+ ).onDismiss {
+ currentStartDownloadDialog = null
+ }.show(binding.startDownloadDialogContainer).also {
+ currentStartDownloadDialog = it
+ }
+ },
)
downloadFeature.onDownloadStopped = { downloadState, _, downloadJobStatus ->
@@ -512,7 +542,7 @@ abstract class BaseBrowserFragment :
didFail = downloadJobStatus == DownloadState.Status.FAILED,
tryAgain = downloadFeature::tryAgain,
onCannotOpenFile = {
- showCannotOpenFileError(binding.browserLayout, context, it)
+ showCannotOpenFileError(binding.dynamicSnackbarContainer, context, it)
},
binding = binding.viewDynamicDownloadDialog,
toolbarHeight = toolbarHeight
@@ -945,7 +975,7 @@ abstract class BaseBrowserFragment :
didFail = savedDownloadState.second,
tryAgain = onTryAgain,
onCannotOpenFile = {
- showCannotOpenFileError(binding.browserLayout, context, it)
+ showCannotOpenFileError(binding.dynamicSnackbarContainer, context, it)
},
binding = binding.viewDynamicDownloadDialog,
toolbarHeight = toolbarHeight,
@@ -1036,6 +1066,7 @@ abstract class BaseBrowserFragment :
it.selectedTab
}
.collect {
+ currentStartDownloadDialog?.dismiss()
handleTabSelected(it)
}
}
@@ -1104,6 +1135,7 @@ abstract class BaseBrowserFragment :
override fun onStop() {
super.onStop()
initUIJob?.cancel()
+ currentStartDownloadDialog?.dismiss()
requireComponents.core.store.state.findTabOrCustomTabOrSelectedTab(customTabSessionId)
?.let { session ->
@@ -1119,6 +1151,10 @@ abstract class BaseBrowserFragment :
return findInPageIntegration.onBackPressed() ||
fullScreenFeature.onBackPressed() ||
promptsFeature.onBackPressed() ||
+ currentStartDownloadDialog?.let {
+ it.dismiss()
+ true
+ } ?: false ||
sessionFeature.onBackPressed() ||
removeSessionIfNeeded()
}
@@ -1281,7 +1317,7 @@ abstract class BaseBrowserFragment :
withContext(Main) {
view?.let {
FenixSnackbar.make(
- view = binding.browserLayout,
+ view = binding.dynamicSnackbarContainer,
duration = FenixSnackbar.LENGTH_LONG,
isDisplayedWithBrowserToolbar = true
)
@@ -1303,7 +1339,7 @@ abstract class BaseBrowserFragment :
view?.let {
FenixSnackbar.make(
- view = binding.browserLayout,
+ view = binding.dynamicSnackbarContainer,
duration = FenixSnackbar.LENGTH_LONG,
isDisplayedWithBrowserToolbar = true
)
@@ -1346,7 +1382,7 @@ abstract class BaseBrowserFragment :
// Close find in page bar if opened
findInPageIntegration.onBackPressed()
FenixSnackbar.make(
- view = binding.browserLayout,
+ view = binding.dynamicSnackbarContainer,
duration = Snackbar.LENGTH_SHORT,
isDisplayedWithBrowserToolbar = false
)
@@ -1420,12 +1456,12 @@ abstract class BaseBrowserFragment :
}
private fun showCannotOpenFileError(
- view: View,
+ container: ViewGroup,
context: Context,
downloadState: DownloadState
) {
FenixSnackbar.make(
- view = view,
+ view = container,
duration = Snackbar.LENGTH_SHORT,
isDisplayedWithBrowserToolbar = true
).setText(DynamicDownloadDialog.getCannotOpenFileErrorMessage(context, downloadState))
=====================================
app/src/main/java/org/mozilla/fenix/browser/BrowserFragment.kt
=====================================
@@ -347,7 +347,7 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler {
}
}
FenixSnackbar.make(
- view = binding.browserLayout,
+ view = binding.dynamicSnackbarContainer,
duration = Snackbar.LENGTH_SHORT,
isDisplayedWithBrowserToolbar = true
)
=====================================
app/src/main/java/org/mozilla/fenix/components/FenixSnackbar.kt
=====================================
@@ -141,10 +141,20 @@ class FenixSnackbar private constructor(
0
}
)
+
+ if (parent.id == R.id.dynamicSnackbarContainer) {
+ (parent.layoutParams as? CoordinatorLayout.LayoutParams)?.apply {
+ behavior = FenixSnackbarBehavior<FrameLayout>(
+ context = view.context,
+ toolbarPosition = view.context.settings().toolbarPosition,
+ )
+ }
+ }
}
}
// Use the same implementation of `Snackbar`
+ @Suppress("ReturnCount")
private fun findSuitableParent(_view: View?): ViewGroup? {
var view = _view
var fallback: ViewGroup? = null
@@ -159,6 +169,10 @@ class FenixSnackbar private constructor(
return view
}
+ if (view.id == R.id.dynamicSnackbarContainer) {
+ return view
+ }
+
fallback = view
}
=====================================
app/src/main/java/org/mozilla/fenix/components/FenixSnackbarBehavior.kt
=====================================
@@ -0,0 +1,77 @@
+/* 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 android.view.View
+import androidx.annotation.VisibleForTesting
+import androidx.coordinatorlayout.widget.CoordinatorLayout
+import androidx.core.view.children
+import androidx.core.view.isVisible
+import org.mozilla.fenix.R
+import org.mozilla.fenix.components.toolbar.ToolbarPosition
+
+/**
+ * [CoordinatorLayout.Behavior] to be used by a snackbar that want to ensure it it always positioned
+ * such that it will be shown on top (vertically) of other siblings that may obstruct it's view.
+ *
+ * @param context [Context] used for various system interactions.
+ * @param toolbarPosition Where the toolbar is positioned on the screen.
+ * Depending on it's position (top / bottom) the snackbar will be shown below / above the toolbar.
+ */
+class FenixSnackbarBehavior<V : View>(
+ context: Context,
+ @get:VisibleForTesting internal val toolbarPosition: ToolbarPosition,
+) : CoordinatorLayout.Behavior<V>(context, null) {
+
+ private val dependenciesIds = listOf(
+ R.id.startDownloadDialogContainer,
+ R.id.viewDynamicDownloadDialog,
+ R.id.toolbar,
+ )
+
+ private var currentAnchorId: Int? = View.NO_ID
+
+ override fun layoutDependsOn(
+ parent: CoordinatorLayout,
+ child: V,
+ dependency: View,
+ ): Boolean {
+ val anchorId = dependenciesIds
+ .intersect(parent.children.filter { it.isVisible }.map { it.id }.toSet())
+ .firstOrNull()
+
+ // It is possible that previous anchor's visibility is changed.
+ // The layout is updated and layoutDependsOn is called but onDependentViewChanged not.
+ // We have to check here if a new anchor is available and reparent the snackbar.
+ // This check also ensures we are not positioning the snackbar multiple times for the same anchor.
+ return if (anchorId != currentAnchorId) {
+ positionSnackbar(child, parent.children.firstOrNull { it.id == anchorId })
+ true
+ } else {
+ false
+ }
+ }
+
+ private fun positionSnackbar(snackbar: View, dependency: View?) {
+ currentAnchorId = dependency?.id ?: View.NO_ID
+ val params = snackbar.layoutParams as CoordinatorLayout.LayoutParams
+
+ if (dependency == null || (dependency.id == R.id.toolbar && toolbarPosition == ToolbarPosition.TOP)) {
+ // Position the snackbar at the bottom of the screen.
+ params.anchorId = View.NO_ID
+ params.anchorGravity = Gravity.NO_GRAVITY
+ params.gravity = Gravity.BOTTOM or Gravity.CENTER_HORIZONTAL
+ } else {
+ // Position the snackbar just above the anchor.
+ params.anchorId = dependency.id
+ params.anchorGravity = Gravity.TOP or Gravity.CENTER_HORIZONTAL
+ params.gravity = Gravity.TOP or Gravity.CENTER_HORIZONTAL
+ }
+
+ snackbar.layoutParams = params
+ }
+}
=====================================
app/src/main/java/org/mozilla/fenix/downloads/StartDownloadDialog.kt
=====================================
@@ -0,0 +1,248 @@
+/* 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.downloads
+
+import android.app.Activity
+import android.app.Dialog
+import android.view.Gravity
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.view.ViewTreeObserver.OnGlobalLayoutListener
+import android.view.Window
+import android.view.accessibility.AccessibilityEvent
+import android.view.accessibility.AccessibilityNodeInfo
+import androidx.annotation.VisibleForTesting
+import androidx.coordinatorlayout.widget.CoordinatorLayout
+import androidx.core.content.ContextCompat
+import androidx.core.view.ViewCompat
+import androidx.core.view.children
+import androidx.viewbinding.ViewBinding
+import mozilla.components.feature.downloads.databinding.MozacDownloaderChooserPromptBinding
+import mozilla.components.feature.downloads.toMegabyteOrKilobyteString
+import mozilla.components.feature.downloads.ui.DownloaderApp
+import mozilla.components.feature.downloads.ui.DownloaderAppAdapter
+import mozilla.components.support.ktx.android.view.setNavigationBarTheme
+import mozilla.components.support.ktx.android.view.setStatusBarTheme
+import org.mozilla.fenix.R
+import org.mozilla.fenix.databinding.DialogScrimBinding
+import org.mozilla.fenix.databinding.StartDownloadDialogLayoutBinding
+import org.mozilla.fenix.ext.settings
+
+/**
+ * Parent of all download views that can mimic a modal [Dialog].
+ *
+ * @param activity The [Activity] in which the dialog will be shown.
+ * Used to update the activity [Window] to best mimic a modal dialog.
+ */
+abstract class StartDownloadDialog(
+ private val activity: Activity,
+) {
+ @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
+ internal var binding: ViewBinding? = null
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
+ internal var container: ViewGroup? = null
+ private var scrim: DialogScrimBinding? = null
+
+ @VisibleForTesting
+ internal var onDismiss: () -> Unit = {}
+
+ @VisibleForTesting
+ internal var initialNavigationBarColor = activity.window.navigationBarColor
+
+ @VisibleForTesting
+ internal var initialStatusBarColor = activity.window.statusBarColor
+
+ /**
+ * Show the download view.
+ *
+ * @param container The [ViewGroup] in which the download view will be inflated.
+ */
+ fun show(container: ViewGroup): StartDownloadDialog {
+ this.container = container
+
+ val dialogParent = container.parent as? ViewGroup
+ dialogParent?.let {
+ scrim = DialogScrimBinding.inflate(LayoutInflater.from(activity), dialogParent, true).apply {
+ this.scrim.setOnClickListener {
+ // Empty listener needed to prevent clicking through.
+ }
+ }
+ }
+
+ setupView()
+
+ if (activity.settings().accessibilityServicesEnabled) {
+ disableSiblingsAccessibility(dialogParent)
+ }
+
+ container.apply {
+ val params = layoutParams as CoordinatorLayout.LayoutParams
+ params.gravity = Gravity.BOTTOM or Gravity.CENTER_HORIZONTAL
+ layoutParams = params
+
+ // Set a higher elevation than the toolbar sibling which we should cover.
+ elevation = activity.resources.getDimension(R.dimen.browser_fragment_download_dialog_elevation)
+ visibility = View.VISIBLE
+ }
+
+ activity.window.setNavigationBarTheme(ContextCompat.getColor(activity, R.color.material_scrim_color))
+ activity.window.setStatusBarTheme(ContextCompat.getColor(activity, R.color.material_scrim_color))
+
+ return this
+ }
+
+ /**
+ * Set a callback for when the download view is dismissed.
+ *
+ * @param callback The callback for when the view is dismissed.
+ */
+ fun onDismiss(callback: () -> Unit): StartDownloadDialog {
+ this.onDismiss = callback
+ return this
+ }
+
+ /**
+ * Immediately dismiss the current download view if it is shown.
+ * This will restore the previous UI removing any other layout / window customizations.
+ */
+ fun dismiss() {
+ scrim?.let {
+ (it.root.parent as? ViewGroup)?.removeView(it.root)
+ }
+ binding?.let {
+ (it.root.parent as? ViewGroup)?.removeView(it.root)
+ }
+ enableSiblingsAccessibility(container?.parent as? ViewGroup)
+
+ container?.visibility = View.GONE
+
+ activity.window.setNavigationBarTheme(initialNavigationBarColor)
+ activity.window.setStatusBarTheme(initialStatusBarColor)
+
+ onDismiss()
+ }
+
+ @VisibleForTesting
+ internal fun enableSiblingsAccessibility(parent: ViewGroup?) {
+ parent?.children
+ ?.filterNot { it.id == R.id.startDownloadDialogContainer }
+ ?.forEach {
+ ViewCompat.setImportantForAccessibility(
+ it,
+ ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES,
+ )
+ }
+ }
+
+ @VisibleForTesting
+ internal fun disableSiblingsAccessibility(parent: ViewGroup?) {
+ parent?.children
+ ?.filterNot { it.id == R.id.startDownloadDialogContainer }
+ ?.forEach {
+ ViewCompat.setImportantForAccessibility(
+ it,
+ ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS,
+ )
+ }
+ }
+
+ /**
+ * Bind all download data to the download view.
+ */
+ @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
+ internal abstract fun setupView()
+}
+
+/**
+ * A download view mimicking a modal dialog that allows the user to download a file with the current application.
+ *
+ * @param activity The [Activity] in which the dialog will be shown.
+ * Used to update the activity [Window] to best mimic a modal dialog.
+ * @param filename Name of the file to be downloaded. It wil be shown without any modification.
+ * @param contentSize Size of the file to be downloaded expressed as a number of bytes.
+ * It will automatically be parsed to the appropriate kilobyte or megabyte value before being shown.
+ * @param positiveButtonAction Callback for when the user interacts with the dialog to start the download.
+ * @param negativeButtonAction Callback for when the user interacts with the dialog to dismiss it.
+ */
+class FirstPartyDownloadDialog(
+ private val activity: Activity,
+ private val filename: String,
+ private val contentSize: Long,
+ private val positiveButtonAction: () -> Unit,
+ private val negativeButtonAction: () -> Unit,
+) : StartDownloadDialog(activity) {
+ override fun setupView() {
+ val dialog = StartDownloadDialogLayoutBinding.inflate(LayoutInflater.from(activity), container, true)
+ .also { binding = it }
+
+ if (contentSize > 0L) {
+ val contentSize = contentSize.toMegabyteOrKilobyteString()
+ dialog.title.text =
+ activity.getString(R.string.mozac_feature_downloads_dialog_title2, contentSize)
+ }
+
+ dialog.filename.text = filename
+
+ dialog.downloadButton.setOnClickListener {
+ positiveButtonAction()
+ dismiss()
+ }
+
+ dialog.closeButton.setOnClickListener {
+ negativeButtonAction()
+ dismiss()
+ }
+
+ if (activity.settings().accessibilityServicesEnabled) {
+ // Ensure the title of the dialog is focused and read by talkback first.
+ dialog.root.viewTreeObserver.addOnGlobalLayoutListener(
+ object : OnGlobalLayoutListener {
+ override fun onGlobalLayout() {
+ dialog.root.viewTreeObserver.removeOnGlobalLayoutListener(this)
+ dialog.title.run {
+ sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED)
+ performAccessibilityAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS, null)
+ }
+ }
+ },
+ )
+ }
+ }
+}
+
+/**
+ * A download view mimicking a modal dialog that presents the user with a list of all apps
+ * that can handle the download request.
+ *
+ * @param activity The [Activity] in which the dialog will be shown.
+ * Used to update the activity [Window] to best mimic a modal dialog.
+ * @param downloaderApps List of all applications that can handle the download request.
+ * @param onAppSelected Callback for when the user chooses a specific application to handle the download request.
+ * @param negativeButtonAction Callback for when the user interacts with the dialog to dismiss it.
+ */
+class ThirdPartyDownloadDialog(
+ private val activity: Activity,
+ private val downloaderApps: List<DownloaderApp>,
+ private val onAppSelected: (DownloaderApp) -> Unit,
+ private val negativeButtonAction: () -> Unit,
+) : StartDownloadDialog(activity) {
+ override fun setupView() {
+ val dialog = MozacDownloaderChooserPromptBinding.inflate(LayoutInflater.from(activity), container, true)
+ .also { binding = it }
+
+ val recyclerView = dialog.appsList
+ recyclerView.adapter = DownloaderAppAdapter(activity, downloaderApps) { app ->
+ onAppSelected(app)
+ dismiss()
+ }
+
+ dialog.closeButton.setOnClickListener {
+ negativeButtonAction()
+ dismiss()
+ }
+ }
+}
=====================================
app/src/main/res/drawable/download_dialog_download_button_background.xml
=====================================
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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/. -->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+ <corners android:radius="@dimen/bottom_sheet_corner_radius"/>
+ <solid android:color="?attr/accent" />
+</shape>
=====================================
app/src/main/res/layout/dialog_scrim.xml
=====================================
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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/. -->
+
+<FrameLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/scrim"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@color/material_scrim_color"
+ android:clipToPadding="false"
+ android:fitsSystemWindows="true"
+ android:importantForAccessibility="no"
+ android:soundEffectsEnabled="false" />
=====================================
app/src/main/res/layout/fragment_browser.xml
=====================================
@@ -66,6 +66,20 @@
android:layout_height="match_parent"
android:visibility="gone" />
+ <FrameLayout
+ android:id="@+id/startDownloadDialogContainer"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="bottom"
+ android:visibility="gone"
+ android:elevation="@dimen/browser_fragment_toolbar_elevation"/>
+
+ <FrameLayout
+ android:id="@+id/dynamicSnackbarContainer"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:elevation="@dimen/browser_fragment_toolbar_elevation"/>
+
</androidx.coordinatorlayout.widget.CoordinatorLayout>
<mozilla.components.feature.prompts.creditcard.CreditCardSelectBar
=====================================
app/src/main/res/layout/start_download_dialog_layout.xml
=====================================
@@ -0,0 +1,96 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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/. -->
+
+<RelativeLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/dialogLayout"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="?android:windowBackground"
+ android:orientation="vertical"
+ app:layout_constraintBottom_toBottomOf="parent">
+
+ <androidx.appcompat.widget.AppCompatImageView
+ android:id="@+id/icon"
+ android:layout_width="32dp"
+ android:layout_height="32dp"
+ android:layout_alignParentTop="true"
+ android:layout_marginStart="16dp"
+ android:layout_marginTop="16dp"
+ android:importantForAccessibility="no"
+ android:scaleType="center"
+ app:srcCompat="@drawable/mozac_feature_download_ic_download"
+ app:tint="?android:attr/textColorPrimary" />
+
+ <TextView
+ android:id="@+id/title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignBaseline="@id/icon"
+ android:layout_alignParentTop="true"
+ android:layout_marginStart="3dp"
+ android:layout_marginTop="16dp"
+ android:layout_marginEnd="11dp"
+ android:layout_toStartOf="@id/close_button"
+ android:layout_toEndOf="@id/icon"
+ android:paddingStart="5dp"
+ android:paddingTop="4dp"
+ android:paddingEnd="5dp"
+ android:text="@string/mozac_feature_downloads_dialog_download"
+ android:textColor="?android:attr/textColorPrimary"
+ tools:text="Download (85.7 MB)"
+ tools:textColor="#000000" />
+
+ <androidx.appcompat.widget.AppCompatImageButton
+ android:id="@+id/close_button"
+ android:layout_width="48dp"
+ android:layout_height="48dp"
+ android:layout_alignBaseline="@id/icon"
+ android:layout_alignParentTop="true"
+ android:layout_alignParentEnd="true"
+ android:layout_marginStart="3dp"
+ android:scaleType="centerInside"
+ android:background="@null"
+ android:contentDescription="@string/mozac_feature_downloads_button_close"
+ app:srcCompat="@drawable/mozac_ic_close"
+ app:tint="?android:attr/textColorPrimary"
+ tools:textColor="#000000" />
+
+ <TextView
+ android:id="@+id/filename"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/title"
+ android:layout_alignBaseline="@id/icon"
+ android:layout_marginStart="3dp"
+ android:layout_marginTop="16dp"
+ android:layout_toEndOf="@id/icon"
+ android:paddingStart="5dp"
+ android:paddingTop="4dp"
+ android:paddingEnd="5dp"
+ android:textColor="?android:attr/textColorPrimary"
+ tools:text="@tools:sample/lorem/random"
+ tools:textColor="#000000" />
+
+ <Button
+ android:id="@+id/download_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/filename"
+ android:layout_alignParentEnd="true"
+ android:layout_marginStart="8dp"
+ android:layout_marginTop="16dp"
+ android:layout_marginEnd="16dp"
+ android:layout_marginBottom="16dp"
+ android:paddingStart="8dp"
+ android:paddingEnd="8dp"
+ android:text="@string/mozac_feature_downloads_dialog_download"
+ android:background="@drawable/download_dialog_download_button_background"
+ android:textColor="?attr/textOnColorPrimary"
+ android:textAllCaps="false"
+ tools:ignore="ButtonStyleXmlDetector" />
+</RelativeLayout>
=====================================
app/src/main/res/values/colors.xml
=====================================
@@ -337,4 +337,7 @@
<!-- App Spinners colors -->
<color name="spinner_selected_item">#1415141A</color>
+
+ <!-- Material Design colors -->
+ <color name="material_scrim_color">#52000000</color>
</resources>
=====================================
app/src/main/res/values/dimens.xml
=====================================
@@ -82,6 +82,8 @@
<!--The size of the gap between the tab preview and content layout.-->
<dimen name="browser_fragment_gesture_preview_offset">48dp</dimen>
<dimen name="browser_fragment_toolbar_elevation">16dp</dimen>
+ <!-- The download dialogs are shown above the toolbar so they need a bigger elevation. -->
+ <dimen name="browser_fragment_download_dialog_elevation">17dp</dimen>
<!-- Search Fragment -->
<dimen name="search_fragment_clipboard_item_height">56dp</dimen>
=====================================
app/src/test/java/org/mozilla/fenix/components/FenixSnackbarBehaviorTest.kt
=====================================
@@ -0,0 +1,251 @@
+/* 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.view.Gravity
+import android.view.View
+import android.view.ViewGroup
+import android.widget.FrameLayout
+import androidx.coordinatorlayout.widget.CoordinatorLayout
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mozilla.fenix.R
+import org.mozilla.fenix.components.toolbar.ToolbarPosition
+import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
+
+@RunWith(FenixRobolectricTestRunner::class)
+class FenixSnackbarBehaviorTest {
+ private val snackbarParams = CoordinatorLayout.LayoutParams(0, 0)
+ private val snackbarContainer = FrameLayout(testContext)
+ private val dependency = View(testContext)
+ private val parent = CoordinatorLayout(testContext)
+
+ @Before
+ fun setup() {
+ snackbarContainer.layoutParams = snackbarParams
+ parent.addView(dependency)
+ }
+
+ @Test
+ fun `GIVEN no valid anchors are shown WHEN the snackbar is shown THEN don't anchor it`() {
+ val behavior = FenixSnackbarBehavior<ViewGroup>(testContext, ToolbarPosition.BOTTOM)
+
+ behavior.layoutDependsOn(parent, snackbarContainer, dependency)
+
+ assertSnackbarIsPlacedAtTheBottomOfTheScreen()
+ }
+
+ @Test
+ fun `GIVEN the dynamic download dialog is shown WHEN the snackbar is shown THEN place the snackbar above the dialog`() {
+ dependency.id = R.id.viewDynamicDownloadDialog
+ val behavior = FenixSnackbarBehavior<ViewGroup>(testContext, ToolbarPosition.BOTTOM)
+
+ behavior.layoutDependsOn(parent, snackbarContainer, dependency)
+
+ assertSnackbarPlacementAboveAnchor()
+ }
+
+ @Test
+ fun `GIVEN a bottom toolbar is shown WHEN the snackbar is shown THEN place the snackbar above the toolbar`() {
+ dependency.id = R.id.toolbar
+ val behavior = FenixSnackbarBehavior<ViewGroup>(testContext, ToolbarPosition.BOTTOM)
+
+ behavior.layoutDependsOn(parent, snackbarContainer, dependency)
+
+ assertSnackbarPlacementAboveAnchor()
+ }
+
+ @Test
+ fun `GIVEN a toolbar and a dynamic download dialog are shown WHEN the snackbar is shown THEN place the snackbar above the dialog`() {
+ listOf(R.id.viewDynamicDownloadDialog, R.id.toolbar).forEach {
+ parent.addView(View(testContext).apply { id = it })
+ }
+ val behavior = FenixSnackbarBehavior<ViewGroup>(testContext, ToolbarPosition.BOTTOM)
+
+ behavior.layoutDependsOn(parent, snackbarContainer, dependency)
+
+ assertSnackbarPlacementAboveAnchor(parent.findViewById(R.id.viewDynamicDownloadDialog))
+ }
+
+ @Test
+ fun `GIVEN a toolbar, a download dialog and a dynamic download dialog are shown WHEN the snackbar is shown THEN place the snackbar above the download dialog`() {
+ listOf(R.id.viewDynamicDownloadDialog, R.id.toolbar, R.id.startDownloadDialogContainer).forEach {
+ parent.addView(View(testContext).apply { id = it })
+ }
+ val behavior = FenixSnackbarBehavior<ViewGroup>(testContext, ToolbarPosition.BOTTOM)
+
+ behavior.layoutDependsOn(parent, snackbarContainer, dependency)
+
+ assertSnackbarPlacementAboveAnchor(parent.findViewById(R.id.startDownloadDialogContainer))
+ }
+
+ @Test
+ fun `GIVEN the snackbar is anchored to the dynamic download dialog and a bottom toolbar is shown WHEN the dialog is not shown anymore THEN place the snackbar above the toolbar`() {
+ val dialog = View(testContext)
+ .apply { id = R.id.viewDynamicDownloadDialog }
+ .also { parent.addView(it) }
+ val toolbar = View(testContext)
+ .apply { id = R.id.toolbar }
+ .also { parent.addView(it) }
+ val behavior = FenixSnackbarBehavior<ViewGroup>(testContext, ToolbarPosition.BOTTOM)
+
+ // Test the scenario where the dialog is invisible.
+ behavior.layoutDependsOn(parent, snackbarContainer, dependency)
+ assertSnackbarPlacementAboveAnchor(dialog)
+ dialog.visibility = View.GONE
+ behavior.layoutDependsOn(parent, snackbarContainer, dependency)
+ assertSnackbarPlacementAboveAnchor(toolbar)
+
+ // Test the scenario where the dialog is removed from parent.
+ dialog.visibility = View.VISIBLE
+ behavior.layoutDependsOn(parent, snackbarContainer, dependency)
+ assertSnackbarPlacementAboveAnchor(dialog)
+ parent.removeView(dialog)
+ behavior.layoutDependsOn(parent, snackbarContainer, dependency)
+ assertSnackbarPlacementAboveAnchor(toolbar)
+ }
+
+ @Test
+ fun `GIVEN the snackbar is anchored to a download dialog and another dynamic dialog is shown WHEN the dialog is not shown anymore THEN place the snackbar above the dynamic dialog`() {
+ val dialog = View(testContext)
+ .apply { id = R.id.startDownloadDialogContainer }
+ .also { parent.addView(it) }
+ val dynamicDialog = View(testContext)
+ .apply { id = R.id.viewDynamicDownloadDialog }
+ .also { parent.addView(it) }
+ val behavior = FenixSnackbarBehavior<ViewGroup>(testContext, ToolbarPosition.BOTTOM)
+
+ // Test the scenario where the dialog is invisible.
+ behavior.layoutDependsOn(parent, snackbarContainer, dependency)
+ assertSnackbarPlacementAboveAnchor(dialog)
+ dialog.visibility = View.GONE
+ behavior.layoutDependsOn(parent, snackbarContainer, dependency)
+ assertSnackbarPlacementAboveAnchor(dynamicDialog)
+
+ // Test the scenario where the dialog is removed from parent.
+ dialog.visibility = View.VISIBLE
+ behavior.layoutDependsOn(parent, snackbarContainer, dependency)
+ assertSnackbarPlacementAboveAnchor(dialog)
+ parent.removeView(dialog)
+ behavior.layoutDependsOn(parent, snackbarContainer, dependency)
+ assertSnackbarPlacementAboveAnchor(dynamicDialog)
+ }
+
+ @Test
+ fun `GIVEN the snackbar is anchored to a download dialog and a bottom toolbar is shown WHEN the dialog is not shown anymore THEN place the snackbar above the toolbar`() {
+ val dialog = View(testContext)
+ .apply { id = R.id.startDownloadDialogContainer }
+ .also { parent.addView(it) }
+ val toolbar = View(testContext)
+ .apply { id = R.id.toolbar }
+ .also { parent.addView(it) }
+ val behavior = FenixSnackbarBehavior<ViewGroup>(testContext, ToolbarPosition.BOTTOM)
+
+ // Test the scenario where the dialog is invisible.
+ behavior.layoutDependsOn(parent, snackbarContainer, dependency)
+ assertSnackbarPlacementAboveAnchor(dialog)
+ dialog.visibility = View.GONE
+ behavior.layoutDependsOn(parent, snackbarContainer, dependency)
+ assertSnackbarPlacementAboveAnchor(toolbar)
+
+ // Test the scenario where the dialog is removed from parent.
+ dialog.visibility = View.VISIBLE
+ behavior.layoutDependsOn(parent, snackbarContainer, dependency)
+ assertSnackbarPlacementAboveAnchor(dialog)
+ parent.removeView(dialog)
+ behavior.layoutDependsOn(parent, snackbarContainer, dependency)
+ assertSnackbarPlacementAboveAnchor(toolbar)
+ }
+
+ @Test
+ fun `GIVEN the snackbar is anchored to the bottom toolbar WHEN the toolbar is not shown anymore THEN place the snackbar at the bottom`() {
+ val toolbar = View(testContext)
+ .apply { id = R.id.toolbar }
+ .also { parent.addView(it) }
+ val behavior = FenixSnackbarBehavior<ViewGroup>(testContext, ToolbarPosition.BOTTOM)
+
+ // Test the scenario where the toolbar is invisible.
+ behavior.layoutDependsOn(parent, snackbarContainer, dependency)
+ assertSnackbarPlacementAboveAnchor(toolbar)
+ toolbar.visibility = View.GONE
+ behavior.layoutDependsOn(parent, snackbarContainer, dependency)
+ assertSnackbarIsPlacedAtTheBottomOfTheScreen()
+
+ // Test the scenario where the toolbar is removed from parent.
+ toolbar.visibility = View.VISIBLE
+ behavior.layoutDependsOn(parent, snackbarContainer, dependency)
+ assertSnackbarPlacementAboveAnchor(toolbar)
+ parent.removeView(toolbar)
+ behavior.layoutDependsOn(parent, snackbarContainer, dependency)
+ assertSnackbarIsPlacedAtTheBottomOfTheScreen()
+ }
+
+ @Test
+ fun `GIVEN the snackbar is anchored to the dynamic download dialog and a top toolbar is shown WHEN the dialog is not shown anymore THEN place the snackbar to the bottom`() {
+ val dialog = View(testContext)
+ .apply { id = R.id.viewDynamicDownloadDialog }
+ .also { parent.addView(it) }
+ View(testContext)
+ .apply { id = R.id.toolbar }
+ .also { parent.addView(it) }
+ val behavior = FenixSnackbarBehavior<ViewGroup>(testContext, ToolbarPosition.TOP)
+
+ // Test the scenario where the dialog is invisible.
+ behavior.layoutDependsOn(parent, snackbarContainer, dependency)
+ assertSnackbarPlacementAboveAnchor(dialog)
+ dialog.visibility = View.GONE
+ behavior.layoutDependsOn(parent, snackbarContainer, dependency)
+ assertSnackbarIsPlacedAtTheBottomOfTheScreen()
+
+ // Test the scenario where the dialog is removed from parent.
+ dialog.visibility = View.VISIBLE
+ behavior.layoutDependsOn(parent, snackbarContainer, dependency)
+ assertSnackbarPlacementAboveAnchor(dialog)
+ parent.removeView(dialog)
+ behavior.layoutDependsOn(parent, snackbarContainer, dependency)
+ assertSnackbarIsPlacedAtTheBottomOfTheScreen()
+ }
+
+ @Test
+ fun `GIVEN the snackbar is anchored based on a top toolbar WHEN the toolbar is not shown anymore THEN place the snackbar at the bottom`() {
+ val toolbar = View(testContext)
+ .apply { id = R.id.toolbar }
+ .also { parent.addView(it) }
+ val behavior = FenixSnackbarBehavior<ViewGroup>(testContext, ToolbarPosition.TOP)
+
+ // Test the scenario where the toolbar is invisible.
+ behavior.layoutDependsOn(parent, snackbarContainer, dependency)
+ assertSnackbarIsPlacedAtTheBottomOfTheScreen()
+ toolbar.visibility = View.GONE
+ behavior.layoutDependsOn(parent, snackbarContainer, dependency)
+ assertSnackbarIsPlacedAtTheBottomOfTheScreen()
+
+ // Test the scenario where the toolbar is removed from parent.
+ toolbar.visibility = View.VISIBLE
+ behavior.layoutDependsOn(parent, snackbarContainer, dependency)
+ assertSnackbarIsPlacedAtTheBottomOfTheScreen()
+ parent.removeView(toolbar)
+ behavior.layoutDependsOn(parent, snackbarContainer, dependency)
+ assertSnackbarIsPlacedAtTheBottomOfTheScreen()
+ }
+
+ private fun assertSnackbarPlacementAboveAnchor(anchor: View = dependency) {
+ assertEquals(anchor.id, snackbarContainer.params.anchorId)
+ assertEquals(Gravity.TOP or Gravity.CENTER_HORIZONTAL, snackbarContainer.params.anchorGravity)
+ assertEquals(Gravity.TOP or Gravity.CENTER_HORIZONTAL, snackbarContainer.params.gravity)
+ }
+
+ private fun assertSnackbarIsPlacedAtTheBottomOfTheScreen() {
+ assertEquals(View.NO_ID, snackbarContainer.params.anchorId)
+ assertEquals(Gravity.NO_GRAVITY, snackbarContainer.params.anchorGravity)
+ assertEquals(Gravity.BOTTOM or Gravity.CENTER_HORIZONTAL, snackbarContainer.params.gravity)
+ }
+
+ private val FrameLayout.params
+ get() = layoutParams as CoordinatorLayout.LayoutParams
+}
=====================================
app/src/test/java/org/mozilla/fenix/downloads/FirstPartyDownloadDialogTest.kt
=====================================
@@ -0,0 +1,115 @@
+/* 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.downloads
+
+import android.app.Activity
+import android.widget.FrameLayout
+import io.mockk.Runs
+import io.mockk.every
+import io.mockk.just
+import io.mockk.spyk
+import io.mockk.verify
+import mozilla.components.feature.downloads.toMegabyteOrKilobyteString
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mozilla.fenix.R
+import org.mozilla.fenix.databinding.StartDownloadDialogLayoutBinding
+import org.mozilla.fenix.ext.settings
+import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
+import org.robolectric.Robolectric
+
+@RunWith(FenixRobolectricTestRunner::class)
+class FirstPartyDownloadDialogTest {
+ private val activity: Activity = Robolectric.buildActivity(Activity::class.java).create().get()
+
+ @Before
+ fun setup() {
+ every { activity.settings().accessibilityServicesEnabled } returns false
+ }
+
+ @Test
+ fun `GIVEN the size of the download is known WHEN setting it's View THEN bind all provided download data and show the download size`() {
+ var wasPositiveActionDone = false
+ var wasNegativeActionDone = false
+ val contentSize = 5566L
+ val dialog = spyk(
+ FirstPartyDownloadDialog(
+ activity = activity,
+ filename = "Test",
+ contentSize = contentSize,
+ positiveButtonAction = { wasPositiveActionDone = true },
+ negativeButtonAction = { wasNegativeActionDone = true },
+ ),
+ )
+ every { dialog.dismiss() } just Runs
+ val dialogParent = FrameLayout(testContext)
+ dialog.container = dialogParent
+
+ dialog.setupView()
+
+ assertEquals(1, dialogParent.childCount)
+ assertEquals(R.id.dialogLayout, dialogParent.getChildAt(0).id)
+ val dialogBinding = dialog.binding as StartDownloadDialogLayoutBinding
+ assertEquals(
+ testContext.getString(
+ R.string.mozac_feature_downloads_dialog_title2,
+ contentSize.toMegabyteOrKilobyteString(),
+ ),
+ dialogBinding.title.text,
+ )
+ assertEquals("Test", dialogBinding.filename.text)
+ assertFalse(wasPositiveActionDone)
+ assertFalse(wasNegativeActionDone)
+ dialogBinding.downloadButton.callOnClick()
+ verify { dialog.dismiss() }
+ assertTrue(wasPositiveActionDone)
+ dialogBinding.closeButton.callOnClick()
+ verify(exactly = 2) { dialog.dismiss() }
+ assertTrue(wasNegativeActionDone)
+ }
+
+ @Test
+ fun `GIVEN the size of the download is not known WHEN setting it's View THEN bind all provided download data and show the download size`() {
+ var wasPositiveActionDone = false
+ var wasNegativeActionDone = false
+ val contentSize = 0L
+ val dialog = spyk(
+ FirstPartyDownloadDialog(
+ activity = activity,
+ filename = "Test",
+ contentSize = contentSize,
+ positiveButtonAction = { wasPositiveActionDone = true },
+ negativeButtonAction = { wasNegativeActionDone = true },
+ ),
+ )
+ every { dialog.dismiss() } just Runs
+ val dialogParent = FrameLayout(testContext)
+ dialog.container = dialogParent
+
+ dialog.setupView()
+
+ assertEquals(1, dialogParent.childCount)
+ assertEquals(R.id.dialogLayout, dialogParent.getChildAt(0).id)
+ val dialogBinding = dialog.binding as StartDownloadDialogLayoutBinding
+ assertEquals(
+ testContext.getString(R.string.mozac_feature_downloads_dialog_download),
+ dialogBinding.title.text,
+ )
+ assertEquals("Test", dialogBinding.filename.text)
+ assertFalse(wasPositiveActionDone)
+ assertFalse(wasNegativeActionDone)
+ dialogBinding.downloadButton.callOnClick()
+ verify { dialog.dismiss() }
+ assertTrue(wasPositiveActionDone)
+ dialogBinding.closeButton.callOnClick()
+ verify(exactly = 2) { dialog.dismiss() }
+ assertTrue(wasNegativeActionDone)
+ }
+}
=====================================
app/src/test/java/org/mozilla/fenix/downloads/StartDownloadDialogTest.kt
=====================================
@@ -0,0 +1,242 @@
+/* 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.downloads
+
+import android.app.Activity
+import android.content.Context
+import android.graphics.Color
+import android.view.Gravity
+import android.view.LayoutInflater
+import android.view.View
+import android.widget.FrameLayout
+import androidx.coordinatorlayout.widget.CoordinatorLayout
+import androidx.core.content.ContextCompat
+import androidx.core.view.children
+import androidx.core.view.isVisible
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.mockkStatic
+import io.mockk.verify
+import mozilla.components.support.ktx.android.view.setNavigationBarTheme
+import mozilla.components.support.ktx.android.view.setStatusBarTheme
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mozilla.fenix.R
+import org.mozilla.fenix.databinding.StartDownloadDialogLayoutBinding
+import org.mozilla.fenix.ext.settings
+import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
+import org.mozilla.fenix.utils.Settings
+import org.robolectric.Robolectric
+
+@RunWith(FenixRobolectricTestRunner::class)
+class StartDownloadDialogTest {
+ @Test
+ fun `WHEN the dialog is instantiated THEN cache the navigation and status bar colors`() {
+ val navigationBarColor = Color.RED
+ val statusBarColor = Color.BLUE
+ val activity: Activity = mockk {
+ every { window.navigationBarColor } returns navigationBarColor
+ every { window.statusBarColor } returns statusBarColor
+ }
+ val dialog = TestDownloadDialog(activity)
+
+ assertEquals(navigationBarColor, dialog.initialNavigationBarColor)
+ assertEquals(statusBarColor, dialog.initialStatusBarColor)
+ }
+
+ @Test
+ fun `WHEN the view is to be shown THEN set the scrim and other window customization bind the download values`() {
+ val activity = Robolectric.buildActivity(Activity::class.java).create().get()
+ val dialogParent = FrameLayout(testContext)
+ val dialogContainer = FrameLayout(testContext).also {
+ dialogParent.addView(it)
+ it.layoutParams = CoordinatorLayout.LayoutParams(0, 0)
+ }
+ val dialog = TestDownloadDialog(activity)
+
+ mockkStatic("mozilla.components.support.ktx.android.view.WindowKt", "org.mozilla.fenix.ext.ContextKt") {
+ every { any<Context>().settings() } returns mockk(relaxed = true)
+ val fluentDialog = dialog.show(dialogContainer)
+
+ val scrim = dialogParent.children.first { it.id == R.id.scrim }
+ assertTrue(scrim.hasOnClickListeners())
+ assertFalse(scrim.isSoundEffectsEnabled)
+ assertTrue(dialog.wasDownloadDataBinded)
+ assertEquals(
+ Gravity.BOTTOM or Gravity.CENTER_HORIZONTAL,
+ (dialogContainer.layoutParams as CoordinatorLayout.LayoutParams).gravity,
+ )
+ assertEquals(
+ testContext.resources.getDimension(R.dimen.browser_fragment_download_dialog_elevation),
+ dialogContainer.elevation,
+ )
+ assertTrue(dialogContainer.isVisible)
+ verify {
+ activity.window.setNavigationBarTheme(ContextCompat.getColor(activity, R.color.material_scrim_color))
+ activity.window.setStatusBarTheme(ContextCompat.getColor(activity, R.color.material_scrim_color))
+ }
+ assertEquals(dialog, fluentDialog)
+ }
+ }
+
+ @Test
+ fun `GIVEN a dismiss callback WHEN the dialog is dismissed THEN the callback is informed`() {
+ var wasDismissCalled = false
+ val dialog = TestDownloadDialog(mockk(relaxed = true))
+
+ val fluentDialog = dialog.onDismiss { wasDismissCalled = true }
+ dialog.onDismiss()
+
+ assertTrue(wasDismissCalled)
+ assertEquals(dialog, fluentDialog)
+ }
+
+ @Test
+ fun `GIVEN the download dialog is shown WHEN dismissed THEN remove the scrim, the dialog and any window customizations`() {
+ val activity = Robolectric.buildActivity(Activity::class.java).create().get()
+ val dialogParent = FrameLayout(testContext)
+ val dialogContainer = FrameLayout(testContext).also {
+ dialogParent.addView(it)
+ it.layoutParams = CoordinatorLayout.LayoutParams(0, 0)
+ }
+ val dialog = TestDownloadDialog(activity)
+ mockkStatic("mozilla.components.support.ktx.android.view.WindowKt", "org.mozilla.fenix.ext.ContextKt") {
+ every { any<Context>().settings() } returns mockk(relaxed = true)
+ dialog.show(dialogContainer)
+ dialog.binding = StartDownloadDialogLayoutBinding
+ .inflate(LayoutInflater.from(activity), dialogContainer, true)
+
+ dialog.dismiss()
+
+ assertNull(dialogParent.children.firstOrNull { it.id == R.id.scrim })
+ assertTrue(dialogParent.childCount == 1)
+ assertTrue(dialogContainer.childCount == 0)
+ assertFalse(dialogContainer.isVisible)
+ verify {
+ activity.window.setNavigationBarTheme(dialog.initialNavigationBarColor)
+ activity.window.setStatusBarTheme(dialog.initialStatusBarColor)
+ }
+ }
+ }
+
+ @Test
+ fun `GIVEN a ViewGroup WHEN enabling accessibility THEN enable it for all children but the dialog container`() {
+ val activity: Activity = mockk(relaxed = true)
+ val dialogParent = FrameLayout(testContext)
+ FrameLayout(testContext).also {
+ dialogParent.addView(it)
+ it.id = R.id.startDownloadDialogContainer
+ it.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO
+ }
+ val otherView = View(testContext).also {
+ dialogParent.addView(it)
+ it.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO
+ }
+ val dialog = TestDownloadDialog(activity)
+
+ dialog.enableSiblingsAccessibility(dialogParent)
+
+ assertEquals(listOf(otherView), dialogParent.children.filter { it.isImportantForAccessibility }.toList())
+ }
+
+ @Test
+ fun `GIVEN a ViewGroup WHEN disabling accessibility THEN disable it for all children but the dialog container`() {
+ val activity: Activity = mockk(relaxed = true)
+ val dialogParent = FrameLayout(testContext)
+ val dialogContainer = FrameLayout(testContext).also {
+ dialogParent.addView(it)
+ it.id = R.id.startDownloadDialogContainer
+ it.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_YES
+ }
+ View(testContext).also {
+ dialogParent.addView(it)
+ it.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_YES
+ }
+ val dialog = TestDownloadDialog(activity)
+
+ dialog.disableSiblingsAccessibility(dialogParent)
+
+ assertEquals(listOf(dialogContainer), dialogParent.children.filter { it.isImportantForAccessibility }.toList())
+ }
+
+ @Test
+ fun `GIVEN accessibility services are enabled WHEN the dialog is shown THEN disable siblings accessibility`() {
+ val activity = Robolectric.buildActivity(Activity::class.java).create().get()
+ val dialogParent = FrameLayout(testContext)
+ val dialogContainer = FrameLayout(testContext).also {
+ dialogParent.addView(it)
+ it.id = R.id.startDownloadDialogContainer
+ it.layoutParams = CoordinatorLayout.LayoutParams(0, 0)
+ it.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_YES
+ }
+ View(testContext).also {
+ dialogParent.addView(it)
+ it.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_YES
+ }
+
+ mockkStatic("org.mozilla.fenix.ext.ContextKt") {
+ val dialog = TestDownloadDialog(activity)
+
+ val settings: Settings = mockk {
+ every { accessibilityServicesEnabled } returns false
+ }
+ every { any<Context>().settings() } returns settings
+ dialog.show(dialogContainer)
+ assertEquals(2, dialogParent.children.count { it.isImportantForAccessibility })
+
+ every { settings.accessibilityServicesEnabled } returns true
+ dialog.show(dialogContainer)
+ assertEquals(listOf(dialogContainer), dialogParent.children.filter { it.isImportantForAccessibility }.toList())
+ }
+ }
+
+ @Test
+ fun `WHEN the dialog is dismissed THEN re-enable siblings accessibility`() {
+ val activity = Robolectric.buildActivity(Activity::class.java).create().get()
+ val dialogParent = FrameLayout(testContext)
+ val dialogContainer = FrameLayout(testContext).also {
+ dialogParent.addView(it)
+ it.id = R.id.startDownloadDialogContainer
+ it.layoutParams = CoordinatorLayout.LayoutParams(0, 0)
+ it.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_YES
+ }
+ val accessibleView = View(testContext).also {
+ dialogParent.addView(it)
+ it.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_YES
+ }
+ mockkStatic("org.mozilla.fenix.ext.ContextKt") {
+ val settings: Settings = mockk {
+ every { accessibilityServicesEnabled } returns true
+ }
+ every { any<Context>().settings() } returns settings
+ val dialog = TestDownloadDialog(activity)
+ dialog.show(dialogContainer)
+ dialog.binding = StartDownloadDialogLayoutBinding
+ .inflate(LayoutInflater.from(activity), dialogContainer, true)
+
+ dialog.dismiss()
+
+ assertEquals(
+ listOf(accessibleView),
+ dialogParent.children.filter { it.isVisible && it.isImportantForAccessibility }.toList(),
+ )
+ }
+ }
+}
+
+private class TestDownloadDialog(
+ activity: Activity,
+) : StartDownloadDialog(activity) {
+ var wasDownloadDataBinded = false
+
+ override fun setupView() {
+ wasDownloadDataBinded = true
+ }
+}
=====================================
app/src/test/java/org/mozilla/fenix/downloads/ThirdPartyDownloadDialogTest.kt
=====================================
@@ -0,0 +1,54 @@
+/* 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.downloads
+
+import android.app.Activity
+import android.widget.FrameLayout
+import io.mockk.Runs
+import io.mockk.every
+import io.mockk.just
+import io.mockk.mockk
+import io.mockk.spyk
+import io.mockk.verify
+import mozilla.components.feature.downloads.databinding.MozacDownloaderChooserPromptBinding
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mozilla.fenix.R
+import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
+import org.robolectric.Robolectric
+
+@RunWith(FenixRobolectricTestRunner::class)
+class ThirdPartyDownloadDialogTest {
+ private val activity: Activity = Robolectric.buildActivity(Activity::class.java).create().get()
+
+ @Test
+ fun `GIVEN a list of downloader apps WHEN setting it's View THEN bind all provided download data`() {
+ var wasNegativeActionDone = false
+ val dialog = spyk(
+ ThirdPartyDownloadDialog(
+ activity = activity,
+ downloaderApps = listOf(mockk(), mockk()),
+ onAppSelected = { /* cannot test the viewholder click */ },
+ negativeButtonAction = { wasNegativeActionDone = true },
+ ),
+ )
+ every { dialog.dismiss() } just Runs
+ val dialogParent = FrameLayout(testContext)
+ dialog.container = dialogParent
+
+ dialog.setupView()
+
+ assertEquals(1, dialogParent.childCount)
+ assertEquals(R.id.relativeLayout, dialogParent.getChildAt(0).id)
+ val dialogBinding = dialog.binding as MozacDownloaderChooserPromptBinding
+ assertEquals(2, dialogBinding.appsList.adapter?.itemCount)
+ dialogBinding.closeButton.callOnClick()
+ assertTrue(wasNegativeActionDone)
+ verify { dialog.dismiss() }
+ }
+}
=====================================
app/src/test/java/org/mozilla/fenix/tabstray/ext/FenixSnackbarKtTest.kt
=====================================
@@ -6,16 +6,29 @@ package org.mozilla.fenix.tabstray.ext
import android.content.Context
import android.view.View
+import android.widget.FrameLayout
+import androidx.coordinatorlayout.widget.CoordinatorLayout
import io.mockk.every
import io.mockk.mockk
+import io.mockk.mockkStatic
import io.mockk.verifyOrder
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
import org.junit.Rule
import org.junit.Test
+import org.junit.runner.RunWith
import org.mozilla.fenix.R
import org.mozilla.fenix.components.FenixSnackbar
+import org.mozilla.fenix.components.FenixSnackbarBehavior
+import org.mozilla.fenix.components.toolbar.ToolbarPosition
+import org.mozilla.fenix.ext.settings
+import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
import org.mozilla.fenix.helpers.MockkRetryTestRule
import org.mozilla.fenix.tabstray.TabsTrayFragment.Companion.ELEVATION
+import org.mozilla.fenix.utils.Settings
+@RunWith(FenixRobolectricTestRunner::class)
class FenixSnackbarKtTest {
@get:Rule
@@ -94,4 +107,24 @@ class FenixSnackbarKtTest {
snackbar.setAction("test1", any())
}
}
+
+ @Test
+ fun `GIVEN the snackbar is a child of dynamic container WHEN it is shown THEN enable the dynamic behavior`() {
+ val container = FrameLayout(testContext).apply {
+ id = R.id.dynamicSnackbarContainer
+ layoutParams = CoordinatorLayout.LayoutParams(0, 0)
+ }
+ val settings: Settings = mockk(relaxed = true) {
+ every { toolbarPosition } returns ToolbarPosition.BOTTOM
+ }
+ mockkStatic("org.mozilla.fenix.ext.ContextKt") {
+ every { any<Context>().settings() } returns settings
+
+ FenixSnackbar.make(view = container)
+
+ val behavior = (container.layoutParams as? CoordinatorLayout.LayoutParams)?.behavior
+ assertTrue(behavior is FenixSnackbarBehavior)
+ assertEquals(ToolbarPosition.BOTTOM, (behavior as? FenixSnackbarBehavior)?.toolbarPosition)
+ }
+ }
}
View it on GitLab: https://gitlab.torproject.org/tpo/applications/fenix/-/compare/25e5afc9e6e8…
--
View it on GitLab: https://gitlab.torproject.org/tpo/applications/fenix/-/compare/25e5afc9e6e8…
You're receiving this email because of your account on gitlab.torproject.org.
1
0

[Git][tpo/applications/android-components][android-components-102.0.14-12.5-1] 4 commits: Bug 1783561 - Stop BrowserToolbarBehavior positioning the snackbar
by Richard Pospesel (@richard) 16 Mar '23
by Richard Pospesel (@richard) 16 Mar '23
16 Mar '23
Richard Pospesel pushed to branch android-components-102.0.14-12.5-1 at The Tor Project / Applications / android-components
Commits:
52f79946 by Mugurell at 2023-03-16T12:04:07+00:00
Bug 1783561 - Stop BrowserToolbarBehavior positioning the snackbar
The snackbar is placed depending on the toolbar so the relation should be
reversed. The toolbar should not know about and control the snackbar.
There could be other siblings that the snackbar wants to position itself by
and so removing this responsability from BrowserToolbarBehavior will help
better support current and future flows.
Backport of https://github.com/mozilla-mobile/firefox-android/commit/04a464e66a95892d9a…
- - - - -
4ec68dfa by Mugurell at 2023-03-16T12:04:07+00:00
Bug 1812518 - Allow a custom View for first party downloads
This will allow clients easily implement their own UI.
Backport of https://github.com/mozilla-mobile/firefox-android/commit/e678610bc10df96b85…
- - - - -
9589ff50 by Mugurell at 2023-03-16T12:04:07+00:00
Bug 1783561 - Allow a custom View for 3rd party downloads
This will allow clients easily implement their own UI
Backport of https://github.com/mozilla-mobile/firefox-android/commit/c53dd65718579006e1…
- - - - -
ccafd6d1 by Mugurell at 2023-03-16T12:04:07+00:00
Improve prompts UX
Backport of https://github.com/mozilla-mobile/firefox-android/commit/1dc21a3786506200be…
- - - - -
8 changed files:
- components/browser/toolbar/src/main/java/mozilla/components/browser/toolbar/behavior/BrowserToolbarBehavior.kt
- components/browser/toolbar/src/test/java/mozilla/components/browser/toolbar/behavior/BrowserToolbarBehaviorTest.kt
- components/feature/downloads/src/main/java/mozilla/components/feature/downloads/DownloadDialogFragment.kt
- components/feature/downloads/src/main/java/mozilla/components/feature/downloads/DownloadsFeature.kt
- components/feature/downloads/src/main/java/mozilla/components/feature/downloads/ext/DownloadState.kt
- components/feature/downloads/src/main/java/mozilla/components/feature/downloads/ui/DownloaderAppAdapter.kt
- components/feature/prompts/build.gradle
- components/feature/prompts/src/main/java/mozilla/components/feature/prompts/PromptFeature.kt
Changes:
=====================================
components/browser/toolbar/src/main/java/mozilla/components/browser/toolbar/behavior/BrowserToolbarBehavior.kt
=====================================
@@ -6,19 +6,15 @@ package mozilla.components.browser.toolbar.behavior
import android.content.Context
import android.util.AttributeSet
-import android.view.Gravity
import android.view.MotionEvent
import android.view.View
import androidx.annotation.VisibleForTesting
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.view.ViewCompat
-import com.google.android.material.snackbar.Snackbar
import mozilla.components.browser.toolbar.BrowserToolbar
import mozilla.components.concept.engine.EngineView
import mozilla.components.support.ktx.android.view.findViewInHierarchy
-private const val SMALL_ELEVATION_CHANGE = 0.01f
-
/**
* Where the toolbar is placed on the screen.
*/
@@ -35,7 +31,6 @@ enum class ToolbarPosition {
*
* This implementation will:
* - Show/Hide the [BrowserToolbar] automatically when scrolling vertically.
- * - On showing a [Snackbar] position it above the [BrowserToolbar].
* - Snap the [BrowserToolbar] to be hidden or visible when the user stops scrolling.
*/
class BrowserToolbarBehavior(
@@ -128,14 +123,6 @@ class BrowserToolbarBehavior(
return false // allow events to be passed to below listeners
}
- override fun layoutDependsOn(parent: CoordinatorLayout, child: BrowserToolbar, dependency: View): Boolean {
- if (toolbarPosition == ToolbarPosition.BOTTOM && dependency is Snackbar.SnackbarLayout) {
- positionSnackbar(child, dependency)
- }
-
- return super.layoutDependsOn(parent, child, dependency)
- }
-
override fun onLayoutChild(
parent: CoordinatorLayout,
child: BrowserToolbar,
@@ -179,23 +166,6 @@ class BrowserToolbarBehavior(
isScrollEnabled = false
}
- @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
- internal fun positionSnackbar(child: View, snackbarLayout: Snackbar.SnackbarLayout) {
- val params = snackbarLayout.layoutParams as CoordinatorLayout.LayoutParams
-
- // Position the snackbar above the toolbar so that it doesn't overlay the toolbar.
- params.anchorId = child.id
- params.anchorGravity = Gravity.TOP or Gravity.CENTER_HORIZONTAL
- params.gravity = Gravity.TOP or Gravity.CENTER_HORIZONTAL
-
- snackbarLayout.layoutParams = params
-
- // In order to avoid the snackbar casting a shadow on the toolbar we adjust the elevation of the snackbar here.
- // We still place it slightly behind the toolbar so that it will not animate over the toolbar but instead pop
- // out from under the toolbar.
- snackbarLayout.elevation = child.elevation - SMALL_ELEVATION_CHANGE
- }
-
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal fun tryToScrollVertically(distance: Float) {
browserToolbar?.let { toolbar ->
=====================================
components/browser/toolbar/src/test/java/mozilla/components/browser/toolbar/behavior/BrowserToolbarBehaviorTest.kt
=====================================
@@ -6,14 +6,12 @@ package mozilla.components.browser.toolbar.behavior
import android.content.Context
import android.graphics.Bitmap
-import android.view.Gravity
import android.view.MotionEvent.ACTION_DOWN
import android.view.MotionEvent.ACTION_MOVE
import android.widget.FrameLayout
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.view.ViewCompat
import androidx.test.ext.junit.runners.AndroidJUnit4
-import com.google.android.material.snackbar.Snackbar
import mozilla.components.browser.toolbar.BrowserToolbar
import mozilla.components.concept.engine.EngineSession
import mozilla.components.concept.engine.EngineView
@@ -474,29 +472,6 @@ class BrowserToolbarBehaviorTest {
verify(yTranslator).collapseWithAnimation(toolbar)
}
- @Test
- fun `Behavior will position snackbar above toolbar`() {
- val behavior = BrowserToolbarBehavior(testContext, null, ToolbarPosition.BOTTOM)
-
- val toolbar: BrowserToolbar = mock()
- doReturn(4223).`when`(toolbar).id
-
- val layoutParams: CoordinatorLayout.LayoutParams = CoordinatorLayout.LayoutParams(0, 0)
-
- val snackbarLayout: Snackbar.SnackbarLayout = mock()
- doReturn(layoutParams).`when`(snackbarLayout).layoutParams
-
- behavior.layoutDependsOn(
- parent = mock(),
- child = toolbar,
- dependency = snackbarLayout
- )
-
- assertEquals(4223, layoutParams.anchorId)
- assertEquals(Gravity.TOP or Gravity.CENTER_HORIZONTAL, layoutParams.anchorGravity)
- assertEquals(Gravity.TOP or Gravity.CENTER_HORIZONTAL, layoutParams.gravity)
- }
-
@Test
fun `Behavior will forceExpand when scrolling up and !shouldScroll if the touch was handled in the browser`() {
val behavior = spy(BrowserToolbarBehavior(testContext, null, ToolbarPosition.BOTTOM))
=====================================
components/feature/downloads/src/main/java/mozilla/components/feature/downloads/DownloadDialogFragment.kt
=====================================
@@ -10,7 +10,7 @@ import mozilla.components.browser.state.state.content.DownloadState
import mozilla.components.feature.downloads.DownloadDialogFragment.Companion.BYTES_TO_MB_LIMIT
import mozilla.components.feature.downloads.DownloadDialogFragment.Companion.KILOBYTE
import mozilla.components.feature.downloads.DownloadDialogFragment.Companion.MEGABYTE
-import mozilla.components.support.utils.DownloadUtils
+import mozilla.components.feature.downloads.ext.realFilenameOrGuessed
/**
* This is a general representation of a dialog meant to be used in collaboration with [DownloadsFeature]
@@ -34,11 +34,7 @@ abstract class DownloadDialogFragment : AppCompatDialogFragment() {
*/
fun setDownload(download: DownloadState) {
val args = arguments ?: Bundle()
- args.putString(
- KEY_FILE_NAME,
- download.fileName
- ?: DownloadUtils.guessFileName(null, download.destinationDirectory, download.url, download.contentType)
- )
+ args.putString(KEY_FILE_NAME, download.realFilenameOrGuessed)
args.putString(KEY_URL, download.url)
args.putLong(KEY_CONTENT_LENGTH, download.contentLength ?: 0)
arguments = args
=====================================
components/feature/downloads/src/main/java/mozilla/components/feature/downloads/DownloadsFeature.kt
=====================================
@@ -25,6 +25,7 @@ import mozilla.components.browser.state.state.SessionState
import mozilla.components.browser.state.state.content.DownloadState
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.feature.downloads.DownloadDialogFragment.Companion.FRAGMENT_TAG
+import mozilla.components.feature.downloads.ext.realFilenameOrGuessed
import mozilla.components.feature.downloads.manager.DownloadManager
import mozilla.components.feature.downloads.manager.noop
import mozilla.components.feature.downloads.manager.onDownloadStopped
@@ -41,6 +42,43 @@ import mozilla.components.support.ktx.kotlin.isSameOriginAs
import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged
import mozilla.components.support.utils.Browsers
+/**
+ * The name of the file to be downloaded.
+ */
+@JvmInline
+value class Filename(val value: String)
+
+/**
+ * The size of the file to be downloaded expressed as the number of `bytes`.
+ * The value will be `0` if the size is unknown.
+ */
+@JvmInline
+value class ContentSize(val value: Long)
+
+/**
+ * The list of all applications that can perform a download, including this application.
+ */
+@JvmInline
+value class ThirdPartyDownloaderApps(val value: List<DownloaderApp>)
+
+/**
+ * Callback for when the user picked a certain application with which to download the current file.
+ */
+@JvmInline
+value class ThirdPartyDownloaderAppChosenCallback(val value: (DownloaderApp) -> Unit)
+
+/**
+ * Callback for when the positive button of a download dialog was tapped.
+ */
+@JvmInline
+value class PositiveActionCallback(val value: () -> Unit)
+
+/**
+ * Callback for when the negative button of a download dialog was tapped.
+ */
+@JvmInline
+value class NegativeActionCallback(val value: () -> Unit)
+
/**
* Feature implementation to provide download functionality for the selected
* session. The feature will subscribe to the selected session and listen
@@ -60,6 +98,10 @@ import mozilla.components.support.utils.Browsers
* @property promptsStyling styling properties for the dialog.
* @property shouldForwardToThirdParties Indicates if downloads should be forward to third party apps,
* if there are multiple apps a chooser dialog will shown.
+ * @property customFirstPartyDownloadDialog An optional delegate for showing a dialog for a download
+ * that will be processed by the current application.
+ * @property customThirdPartyDownloadDialog An optional delegate for showing a dialog for a download
+ * that can be processed by multiple installed applications including the current one.
*/
@Suppress("LongParameterList", "LargeClass")
class DownloadsFeature(
@@ -73,7 +115,11 @@ class DownloadsFeature(
private val tabId: String? = null,
private val fragmentManager: FragmentManager? = null,
private val promptsStyling: PromptsStyling? = null,
- private val shouldForwardToThirdParties: () -> Boolean = { false }
+ private val shouldForwardToThirdParties: () -> Boolean = { false },
+ private val customFirstPartyDownloadDialog:
+ ((Filename, ContentSize, PositiveActionCallback, NegativeActionCallback) -> Unit)? = null,
+ private val customThirdPartyDownloadDialog:
+ ((ThirdPartyDownloaderApps, ThirdPartyDownloaderAppChosenCallback, NegativeActionCallback) -> Unit)? = null,
) : LifecycleAwareFeature, PermissionsFeature {
var onDownloadStopped: onDownloadStopped
@@ -159,16 +205,45 @@ class DownloadsFeature(
val shouldShowAppDownloaderDialog = shouldForwardToThirdParties() && apps.size > 1
return if (shouldShowAppDownloaderDialog) {
- showAppDownloaderDialog(tab, download, apps)
+ when (customThirdPartyDownloadDialog) {
+ null -> showAppDownloaderDialog(tab, download, apps)
+ else -> customThirdPartyDownloadDialog.invoke(
+ ThirdPartyDownloaderApps(apps),
+ ThirdPartyDownloaderAppChosenCallback {
+ onDownloaderAppSelected(it, tab, download)
+ },
+ NegativeActionCallback {
+ useCases.cancelDownloadRequest.invoke(tab.id, download.id)
+ },
+ )
+ }
+
false
} else {
if (applicationContext.isPermissionGranted(downloadManager.permissions.asIterable())) {
- if (fragmentManager != null && !download.skipConfirmation) {
- showDownloadDialog(tab, download)
- false
- } else {
- useCases.consumeDownload(tab.id, download.id)
- startDownload(download)
+ when {
+ customFirstPartyDownloadDialog != null && !download.skipConfirmation -> {
+ customFirstPartyDownloadDialog.invoke(
+ Filename(download.realFilenameOrGuessed),
+ ContentSize(download.contentLength ?: 0),
+ PositiveActionCallback {
+ startDownload(download)
+ useCases.consumeDownload.invoke(tab.id, download.id)
+ },
+ NegativeActionCallback {
+ useCases.cancelDownloadRequest.invoke(tab.id, download.id)
+ },
+ )
+ false
+ }
+ fragmentManager != null && !download.skipConfirmation -> {
+ showDownloadDialog(tab, download)
+ false
+ }
+ else -> {
+ useCases.consumeDownload(tab.id, download.id)
+ startDownload(download)
+ }
}
} else {
onNeedToRequestPermissions(downloadManager.permissions)
@@ -264,25 +339,7 @@ class DownloadsFeature(
) {
appChooserDialog.setApps(apps)
appChooserDialog.onAppSelected = { app ->
- if (app.packageName == applicationContext.packageName) {
- if (applicationContext.isPermissionGranted(downloadManager.permissions.asIterable())) {
- startDownload(download)
- useCases.consumeDownload(tab.id, download.id)
- } else {
- onNeedToRequestPermissions(downloadManager.permissions)
- }
- } else {
- try {
- applicationContext.startActivity(app.toIntent())
- } catch (error: ActivityNotFoundException) {
- val errorMessage = applicationContext.getString(
- R.string.mozac_feature_downloads_unable_to_open_third_party_app,
- app.name
- )
- Toast.makeText(applicationContext, errorMessage, Toast.LENGTH_SHORT).show()
- }
- useCases.consumeDownload(tab.id, download.id)
- }
+ onDownloaderAppSelected(app, tab, download)
}
appChooserDialog.onDismiss = {
@@ -294,6 +351,29 @@ class DownloadsFeature(
}
}
+ @VisibleForTesting
+ internal fun onDownloaderAppSelected(app: DownloaderApp, tab: SessionState, download: DownloadState) {
+ if (app.packageName == applicationContext.packageName) {
+ if (applicationContext.isPermissionGranted(downloadManager.permissions.asIterable())) {
+ startDownload(download)
+ useCases.consumeDownload(tab.id, download.id)
+ } else {
+ onNeedToRequestPermissions(downloadManager.permissions)
+ }
+ } else {
+ try {
+ applicationContext.startActivity(app.toIntent())
+ } catch (error: ActivityNotFoundException) {
+ val errorMessage = applicationContext.getString(
+ R.string.mozac_feature_downloads_unable_to_open_third_party_app,
+ app.name,
+ )
+ Toast.makeText(applicationContext, errorMessage, Toast.LENGTH_SHORT).show()
+ }
+ useCases.consumeDownload(tab.id, download.id)
+ }
+ }
+
private fun getAppDownloaderDialog() = findPreviousAppDownloaderDialogFragment()
?: DownloadAppChooserDialog.newInstance(
promptsStyling?.gravity,
=====================================
components/feature/downloads/src/main/java/mozilla/components/feature/downloads/ext/DownloadState.kt
=====================================
@@ -47,3 +47,6 @@ internal fun DownloadState.withResponse(headers: Headers, stream: InputStream?):
contentLength = contentLength ?: headers[CONTENT_LENGTH]?.toLongOrNull()
)
}
+
+internal val DownloadState.realFilenameOrGuessed
+ get() = fileName ?: DownloadUtils.guessFileName(null, destinationDirectory, url, contentType)
=====================================
components/feature/downloads/src/main/java/mozilla/components/feature/downloads/ui/DownloaderAppAdapter.kt
=====================================
@@ -16,10 +16,10 @@ import mozilla.components.feature.downloads.R
/**
* An adapter for displaying the applications that can perform downloads.
*/
-internal class DownloaderAppAdapter(
+class DownloaderAppAdapter(
context: Context,
private val apps: List<DownloaderApp>,
- val onAppSelected: ((DownloaderApp) -> Unit)
+ val onAppSelected: ((DownloaderApp) -> Unit),
) : RecyclerView.Adapter<DownloaderAppViewHolder>() {
private val inflater = LayoutInflater.from(context)
@@ -49,11 +49,14 @@ internal class DownloaderAppAdapter(
/**
* View holder for a [DownloaderApp] item.
*/
-internal class DownloaderAppViewHolder(
+class DownloaderAppViewHolder(
itemView: View,
val nameLabel: TextView,
- val iconImage: ImageView
+ val iconImage: ImageView,
) : RecyclerView.ViewHolder(itemView) {
+ /**
+ * Show a certain downloader application in the current View.
+ */
fun bind(app: DownloaderApp, onAppSelected: ((DownloaderApp) -> Unit)) {
itemView.app = app
itemView.setOnClickListener {
=====================================
components/feature/prompts/build.gradle
=====================================
@@ -31,6 +31,7 @@ tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach {
dependencies {
implementation project(':browser-state')
implementation project(':concept-engine')
+ implementation project(':feature-session')
implementation project(':lib-state')
implementation project(':support-ktx')
implementation project(':support-utils')
@@ -46,6 +47,7 @@ dependencies {
testImplementation Dependencies.testing_coroutines
testImplementation Dependencies.testing_robolectric
testImplementation Dependencies.testing_mockito
+ testImplementation project(':feature-session')
testImplementation project(':support-test')
testImplementation project(':support-test-libstate')
=====================================
components/feature/prompts/src/main/java/mozilla/components/feature/prompts/PromptFeature.kt
=====================================
@@ -71,6 +71,8 @@ import mozilla.components.feature.prompts.login.LoginExceptions
import mozilla.components.feature.prompts.login.LoginPicker
import mozilla.components.feature.prompts.share.DefaultShareDelegate
import mozilla.components.feature.prompts.share.ShareDelegate
+import mozilla.components.feature.session.SessionUseCases
+import mozilla.components.feature.session.SessionUseCases.ExitFullScreenUseCase
import mozilla.components.lib.state.ext.flowScoped
import mozilla.components.support.base.feature.ActivityResultHandler
import mozilla.components.support.base.feature.LifecycleAwareFeature
@@ -111,6 +113,7 @@ internal const val FRAGMENT_TAG = "mozac_feature_prompt_dialog"
* @property fragmentManager The [FragmentManager] to be used when displaying
* a dialog (fragment).
* @property shareDelegate Delegate used to display share sheet.
+ * @property exitFullscreenUsecase Usecase allowing to exit browser tabs' fullscreen mode.
* @property loginStorageDelegate Delegate used to access login storage. If null,
* 'save login'prompts will not be shown.
* @property isSaveLoginEnabled A callback invoked when a login prompt is triggered. If false,
@@ -144,6 +147,7 @@ class PromptFeature private constructor(
private var customTabId: String?,
private val fragmentManager: FragmentManager,
private val shareDelegate: ShareDelegate,
+ private val exitFullscreenUsecase: ExitFullScreenUseCase = SessionUseCases(store).exitFullscreen,
override val creditCardValidationDelegate: CreditCardValidationDelegate? = null,
override val loginValidationDelegate: LoginValidationDelegate? = null,
private val isSaveLoginEnabled: () -> Boolean = { false },
@@ -184,6 +188,7 @@ class PromptFeature private constructor(
customTabId: String? = null,
fragmentManager: FragmentManager,
shareDelegate: ShareDelegate = DefaultShareDelegate(),
+ exitFullscreenUsecase: ExitFullScreenUseCase = SessionUseCases(store).exitFullscreen,
creditCardValidationDelegate: CreditCardValidationDelegate? = null,
loginValidationDelegate: LoginValidationDelegate? = null,
isSaveLoginEnabled: () -> Boolean = { false },
@@ -202,6 +207,7 @@ class PromptFeature private constructor(
customTabId = customTabId,
fragmentManager = fragmentManager,
shareDelegate = shareDelegate,
+ exitFullscreenUsecase = exitFullscreenUsecase,
creditCardValidationDelegate = creditCardValidationDelegate,
loginValidationDelegate = loginValidationDelegate,
isSaveLoginEnabled = isSaveLoginEnabled,
@@ -222,6 +228,7 @@ class PromptFeature private constructor(
customTabId: String? = null,
fragmentManager: FragmentManager,
shareDelegate: ShareDelegate = DefaultShareDelegate(),
+ exitFullscreenUsecase: ExitFullScreenUseCase = SessionUseCases(store).exitFullscreen,
creditCardValidationDelegate: CreditCardValidationDelegate? = null,
loginValidationDelegate: LoginValidationDelegate? = null,
isSaveLoginEnabled: () -> Boolean = { false },
@@ -240,6 +247,7 @@ class PromptFeature private constructor(
customTabId = customTabId,
fragmentManager = fragmentManager,
shareDelegate = shareDelegate,
+ exitFullscreenUsecase = exitFullscreenUsecase,
creditCardValidationDelegate = creditCardValidationDelegate,
loginValidationDelegate = loginValidationDelegate,
isSaveLoginEnabled = isSaveLoginEnabled,
@@ -420,6 +428,10 @@ class PromptFeature private constructor(
internal fun onPromptRequested(session: SessionState) {
// Some requests are handle with intents
session.content.promptRequests.lastOrNull()?.let { promptRequest ->
+ store.state.findTabOrCustomTabOrSelectedTab(customTabId)?.let {
+ exitFullscreenUsecase(it.id)
+ }
+
when (promptRequest) {
is File -> filePicker.handleFileRequest(promptRequest)
is Share -> handleShareRequest(promptRequest, session)
View it on GitLab: https://gitlab.torproject.org/tpo/applications/android-components/-/compare…
--
View it on GitLab: https://gitlab.torproject.org/tpo/applications/android-components/-/compare…
You're receiving this email because of your account on gitlab.torproject.org.
1
0

[Git][tpo/applications/android-components][android-components-102.0.14-12.0-1] 4 commits: Bug 1783561 - Stop BrowserToolbarBehavior positioning the snackbar
by Richard Pospesel (@richard) 16 Mar '23
by Richard Pospesel (@richard) 16 Mar '23
16 Mar '23
Richard Pospesel pushed to branch android-components-102.0.14-12.0-1 at The Tor Project / Applications / android-components
Commits:
8c3c4617 by Mugurell at 2023-03-15T18:01:20+00:00
Bug 1783561 - Stop BrowserToolbarBehavior positioning the snackbar
The snackbar is placed depending on the toolbar so the relation should be
reversed. The toolbar should not know about and control the snackbar.
There could be other siblings that the snackbar wants to position itself by
and so removing this responsability from BrowserToolbarBehavior will help
better support current and future flows.
Backport of https://github.com/mozilla-mobile/firefox-android/commit/04a464e66a95892d9a…
- - - - -
e135dc89 by Mugurell at 2023-03-15T18:01:54+00:00
Bug 1812518 - Allow a custom View for first party downloads
This will allow clients easily implement their own UI.
Backport of https://github.com/mozilla-mobile/firefox-android/commit/e678610bc10df96b85…
- - - - -
c5c27b90 by Mugurell at 2023-03-15T18:01:54+00:00
Bug 1783561 - Allow a custom View for 3rd party downloads
This will allow clients easily implement their own UI
Backport of https://github.com/mozilla-mobile/firefox-android/commit/c53dd65718579006e1…
- - - - -
952ef13c by Mugurell at 2023-03-15T18:01:54+00:00
Improve prompts UX
Backport of https://github.com/mozilla-mobile/firefox-android/commit/1dc21a3786506200be…
- - - - -
8 changed files:
- components/browser/toolbar/src/main/java/mozilla/components/browser/toolbar/behavior/BrowserToolbarBehavior.kt
- components/browser/toolbar/src/test/java/mozilla/components/browser/toolbar/behavior/BrowserToolbarBehaviorTest.kt
- components/feature/downloads/src/main/java/mozilla/components/feature/downloads/DownloadDialogFragment.kt
- components/feature/downloads/src/main/java/mozilla/components/feature/downloads/DownloadsFeature.kt
- components/feature/downloads/src/main/java/mozilla/components/feature/downloads/ext/DownloadState.kt
- components/feature/downloads/src/main/java/mozilla/components/feature/downloads/ui/DownloaderAppAdapter.kt
- components/feature/prompts/build.gradle
- components/feature/prompts/src/main/java/mozilla/components/feature/prompts/PromptFeature.kt
Changes:
=====================================
components/browser/toolbar/src/main/java/mozilla/components/browser/toolbar/behavior/BrowserToolbarBehavior.kt
=====================================
@@ -6,19 +6,15 @@ package mozilla.components.browser.toolbar.behavior
import android.content.Context
import android.util.AttributeSet
-import android.view.Gravity
import android.view.MotionEvent
import android.view.View
import androidx.annotation.VisibleForTesting
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.view.ViewCompat
-import com.google.android.material.snackbar.Snackbar
import mozilla.components.browser.toolbar.BrowserToolbar
import mozilla.components.concept.engine.EngineView
import mozilla.components.support.ktx.android.view.findViewInHierarchy
-private const val SMALL_ELEVATION_CHANGE = 0.01f
-
/**
* Where the toolbar is placed on the screen.
*/
@@ -35,7 +31,6 @@ enum class ToolbarPosition {
*
* This implementation will:
* - Show/Hide the [BrowserToolbar] automatically when scrolling vertically.
- * - On showing a [Snackbar] position it above the [BrowserToolbar].
* - Snap the [BrowserToolbar] to be hidden or visible when the user stops scrolling.
*/
class BrowserToolbarBehavior(
@@ -128,14 +123,6 @@ class BrowserToolbarBehavior(
return false // allow events to be passed to below listeners
}
- override fun layoutDependsOn(parent: CoordinatorLayout, child: BrowserToolbar, dependency: View): Boolean {
- if (toolbarPosition == ToolbarPosition.BOTTOM && dependency is Snackbar.SnackbarLayout) {
- positionSnackbar(child, dependency)
- }
-
- return super.layoutDependsOn(parent, child, dependency)
- }
-
override fun onLayoutChild(
parent: CoordinatorLayout,
child: BrowserToolbar,
@@ -179,23 +166,6 @@ class BrowserToolbarBehavior(
isScrollEnabled = false
}
- @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
- internal fun positionSnackbar(child: View, snackbarLayout: Snackbar.SnackbarLayout) {
- val params = snackbarLayout.layoutParams as CoordinatorLayout.LayoutParams
-
- // Position the snackbar above the toolbar so that it doesn't overlay the toolbar.
- params.anchorId = child.id
- params.anchorGravity = Gravity.TOP or Gravity.CENTER_HORIZONTAL
- params.gravity = Gravity.TOP or Gravity.CENTER_HORIZONTAL
-
- snackbarLayout.layoutParams = params
-
- // In order to avoid the snackbar casting a shadow on the toolbar we adjust the elevation of the snackbar here.
- // We still place it slightly behind the toolbar so that it will not animate over the toolbar but instead pop
- // out from under the toolbar.
- snackbarLayout.elevation = child.elevation - SMALL_ELEVATION_CHANGE
- }
-
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal fun tryToScrollVertically(distance: Float) {
browserToolbar?.let { toolbar ->
=====================================
components/browser/toolbar/src/test/java/mozilla/components/browser/toolbar/behavior/BrowserToolbarBehaviorTest.kt
=====================================
@@ -6,14 +6,12 @@ package mozilla.components.browser.toolbar.behavior
import android.content.Context
import android.graphics.Bitmap
-import android.view.Gravity
import android.view.MotionEvent.ACTION_DOWN
import android.view.MotionEvent.ACTION_MOVE
import android.widget.FrameLayout
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.view.ViewCompat
import androidx.test.ext.junit.runners.AndroidJUnit4
-import com.google.android.material.snackbar.Snackbar
import mozilla.components.browser.toolbar.BrowserToolbar
import mozilla.components.concept.engine.EngineSession
import mozilla.components.concept.engine.EngineView
@@ -474,29 +472,6 @@ class BrowserToolbarBehaviorTest {
verify(yTranslator).collapseWithAnimation(toolbar)
}
- @Test
- fun `Behavior will position snackbar above toolbar`() {
- val behavior = BrowserToolbarBehavior(testContext, null, ToolbarPosition.BOTTOM)
-
- val toolbar: BrowserToolbar = mock()
- doReturn(4223).`when`(toolbar).id
-
- val layoutParams: CoordinatorLayout.LayoutParams = CoordinatorLayout.LayoutParams(0, 0)
-
- val snackbarLayout: Snackbar.SnackbarLayout = mock()
- doReturn(layoutParams).`when`(snackbarLayout).layoutParams
-
- behavior.layoutDependsOn(
- parent = mock(),
- child = toolbar,
- dependency = snackbarLayout
- )
-
- assertEquals(4223, layoutParams.anchorId)
- assertEquals(Gravity.TOP or Gravity.CENTER_HORIZONTAL, layoutParams.anchorGravity)
- assertEquals(Gravity.TOP or Gravity.CENTER_HORIZONTAL, layoutParams.gravity)
- }
-
@Test
fun `Behavior will forceExpand when scrolling up and !shouldScroll if the touch was handled in the browser`() {
val behavior = spy(BrowserToolbarBehavior(testContext, null, ToolbarPosition.BOTTOM))
=====================================
components/feature/downloads/src/main/java/mozilla/components/feature/downloads/DownloadDialogFragment.kt
=====================================
@@ -10,7 +10,7 @@ import mozilla.components.browser.state.state.content.DownloadState
import mozilla.components.feature.downloads.DownloadDialogFragment.Companion.BYTES_TO_MB_LIMIT
import mozilla.components.feature.downloads.DownloadDialogFragment.Companion.KILOBYTE
import mozilla.components.feature.downloads.DownloadDialogFragment.Companion.MEGABYTE
-import mozilla.components.support.utils.DownloadUtils
+import mozilla.components.feature.downloads.ext.realFilenameOrGuessed
/**
* This is a general representation of a dialog meant to be used in collaboration with [DownloadsFeature]
@@ -34,11 +34,7 @@ abstract class DownloadDialogFragment : AppCompatDialogFragment() {
*/
fun setDownload(download: DownloadState) {
val args = arguments ?: Bundle()
- args.putString(
- KEY_FILE_NAME,
- download.fileName
- ?: DownloadUtils.guessFileName(null, download.destinationDirectory, download.url, download.contentType)
- )
+ args.putString(KEY_FILE_NAME, download.realFilenameOrGuessed)
args.putString(KEY_URL, download.url)
args.putLong(KEY_CONTENT_LENGTH, download.contentLength ?: 0)
arguments = args
=====================================
components/feature/downloads/src/main/java/mozilla/components/feature/downloads/DownloadsFeature.kt
=====================================
@@ -25,6 +25,7 @@ import mozilla.components.browser.state.state.SessionState
import mozilla.components.browser.state.state.content.DownloadState
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.feature.downloads.DownloadDialogFragment.Companion.FRAGMENT_TAG
+import mozilla.components.feature.downloads.ext.realFilenameOrGuessed
import mozilla.components.feature.downloads.manager.DownloadManager
import mozilla.components.feature.downloads.manager.noop
import mozilla.components.feature.downloads.manager.onDownloadStopped
@@ -41,6 +42,43 @@ import mozilla.components.support.ktx.kotlin.isSameOriginAs
import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged
import mozilla.components.support.utils.Browsers
+/**
+ * The name of the file to be downloaded.
+ */
+@JvmInline
+value class Filename(val value: String)
+
+/**
+ * The size of the file to be downloaded expressed as the number of `bytes`.
+ * The value will be `0` if the size is unknown.
+ */
+@JvmInline
+value class ContentSize(val value: Long)
+
+/**
+ * The list of all applications that can perform a download, including this application.
+ */
+@JvmInline
+value class ThirdPartyDownloaderApps(val value: List<DownloaderApp>)
+
+/**
+ * Callback for when the user picked a certain application with which to download the current file.
+ */
+@JvmInline
+value class ThirdPartyDownloaderAppChosenCallback(val value: (DownloaderApp) -> Unit)
+
+/**
+ * Callback for when the positive button of a download dialog was tapped.
+ */
+@JvmInline
+value class PositiveActionCallback(val value: () -> Unit)
+
+/**
+ * Callback for when the negative button of a download dialog was tapped.
+ */
+@JvmInline
+value class NegativeActionCallback(val value: () -> Unit)
+
/**
* Feature implementation to provide download functionality for the selected
* session. The feature will subscribe to the selected session and listen
@@ -60,6 +98,10 @@ import mozilla.components.support.utils.Browsers
* @property promptsStyling styling properties for the dialog.
* @property shouldForwardToThirdParties Indicates if downloads should be forward to third party apps,
* if there are multiple apps a chooser dialog will shown.
+ * @property customFirstPartyDownloadDialog An optional delegate for showing a dialog for a download
+ * that will be processed by the current application.
+ * @property customThirdPartyDownloadDialog An optional delegate for showing a dialog for a download
+ * that can be processed by multiple installed applications including the current one.
*/
@Suppress("LongParameterList", "LargeClass")
class DownloadsFeature(
@@ -73,7 +115,11 @@ class DownloadsFeature(
private val tabId: String? = null,
private val fragmentManager: FragmentManager? = null,
private val promptsStyling: PromptsStyling? = null,
- private val shouldForwardToThirdParties: () -> Boolean = { false }
+ private val shouldForwardToThirdParties: () -> Boolean = { false },
+ private val customFirstPartyDownloadDialog:
+ ((Filename, ContentSize, PositiveActionCallback, NegativeActionCallback) -> Unit)? = null,
+ private val customThirdPartyDownloadDialog:
+ ((ThirdPartyDownloaderApps, ThirdPartyDownloaderAppChosenCallback, NegativeActionCallback) -> Unit)? = null,
) : LifecycleAwareFeature, PermissionsFeature {
var onDownloadStopped: onDownloadStopped
@@ -159,16 +205,45 @@ class DownloadsFeature(
val shouldShowAppDownloaderDialog = shouldForwardToThirdParties() && apps.size > 1
return if (shouldShowAppDownloaderDialog) {
- showAppDownloaderDialog(tab, download, apps)
+ when (customThirdPartyDownloadDialog) {
+ null -> showAppDownloaderDialog(tab, download, apps)
+ else -> customThirdPartyDownloadDialog.invoke(
+ ThirdPartyDownloaderApps(apps),
+ ThirdPartyDownloaderAppChosenCallback {
+ onDownloaderAppSelected(it, tab, download)
+ },
+ NegativeActionCallback {
+ useCases.cancelDownloadRequest.invoke(tab.id, download.id)
+ },
+ )
+ }
+
false
} else {
if (applicationContext.isPermissionGranted(downloadManager.permissions.asIterable())) {
- if (fragmentManager != null && !download.skipConfirmation) {
- showDownloadDialog(tab, download)
- false
- } else {
- useCases.consumeDownload(tab.id, download.id)
- startDownload(download)
+ when {
+ customFirstPartyDownloadDialog != null && !download.skipConfirmation -> {
+ customFirstPartyDownloadDialog.invoke(
+ Filename(download.realFilenameOrGuessed),
+ ContentSize(download.contentLength ?: 0),
+ PositiveActionCallback {
+ startDownload(download)
+ useCases.consumeDownload.invoke(tab.id, download.id)
+ },
+ NegativeActionCallback {
+ useCases.cancelDownloadRequest.invoke(tab.id, download.id)
+ },
+ )
+ false
+ }
+ fragmentManager != null && !download.skipConfirmation -> {
+ showDownloadDialog(tab, download)
+ false
+ }
+ else -> {
+ useCases.consumeDownload(tab.id, download.id)
+ startDownload(download)
+ }
}
} else {
onNeedToRequestPermissions(downloadManager.permissions)
@@ -264,25 +339,7 @@ class DownloadsFeature(
) {
appChooserDialog.setApps(apps)
appChooserDialog.onAppSelected = { app ->
- if (app.packageName == applicationContext.packageName) {
- if (applicationContext.isPermissionGranted(downloadManager.permissions.asIterable())) {
- startDownload(download)
- useCases.consumeDownload(tab.id, download.id)
- } else {
- onNeedToRequestPermissions(downloadManager.permissions)
- }
- } else {
- try {
- applicationContext.startActivity(app.toIntent())
- } catch (error: ActivityNotFoundException) {
- val errorMessage = applicationContext.getString(
- R.string.mozac_feature_downloads_unable_to_open_third_party_app,
- app.name
- )
- Toast.makeText(applicationContext, errorMessage, Toast.LENGTH_SHORT).show()
- }
- useCases.consumeDownload(tab.id, download.id)
- }
+ onDownloaderAppSelected(app, tab, download)
}
appChooserDialog.onDismiss = {
@@ -294,6 +351,29 @@ class DownloadsFeature(
}
}
+ @VisibleForTesting
+ internal fun onDownloaderAppSelected(app: DownloaderApp, tab: SessionState, download: DownloadState) {
+ if (app.packageName == applicationContext.packageName) {
+ if (applicationContext.isPermissionGranted(downloadManager.permissions.asIterable())) {
+ startDownload(download)
+ useCases.consumeDownload(tab.id, download.id)
+ } else {
+ onNeedToRequestPermissions(downloadManager.permissions)
+ }
+ } else {
+ try {
+ applicationContext.startActivity(app.toIntent())
+ } catch (error: ActivityNotFoundException) {
+ val errorMessage = applicationContext.getString(
+ R.string.mozac_feature_downloads_unable_to_open_third_party_app,
+ app.name,
+ )
+ Toast.makeText(applicationContext, errorMessage, Toast.LENGTH_SHORT).show()
+ }
+ useCases.consumeDownload(tab.id, download.id)
+ }
+ }
+
private fun getAppDownloaderDialog() = findPreviousAppDownloaderDialogFragment()
?: DownloadAppChooserDialog.newInstance(
promptsStyling?.gravity,
=====================================
components/feature/downloads/src/main/java/mozilla/components/feature/downloads/ext/DownloadState.kt
=====================================
@@ -47,3 +47,6 @@ internal fun DownloadState.withResponse(headers: Headers, stream: InputStream?):
contentLength = contentLength ?: headers[CONTENT_LENGTH]?.toLongOrNull()
)
}
+
+internal val DownloadState.realFilenameOrGuessed
+ get() = fileName ?: DownloadUtils.guessFileName(null, destinationDirectory, url, contentType)
=====================================
components/feature/downloads/src/main/java/mozilla/components/feature/downloads/ui/DownloaderAppAdapter.kt
=====================================
@@ -16,10 +16,10 @@ import mozilla.components.feature.downloads.R
/**
* An adapter for displaying the applications that can perform downloads.
*/
-internal class DownloaderAppAdapter(
+class DownloaderAppAdapter(
context: Context,
private val apps: List<DownloaderApp>,
- val onAppSelected: ((DownloaderApp) -> Unit)
+ val onAppSelected: ((DownloaderApp) -> Unit),
) : RecyclerView.Adapter<DownloaderAppViewHolder>() {
private val inflater = LayoutInflater.from(context)
@@ -49,11 +49,14 @@ internal class DownloaderAppAdapter(
/**
* View holder for a [DownloaderApp] item.
*/
-internal class DownloaderAppViewHolder(
+class DownloaderAppViewHolder(
itemView: View,
val nameLabel: TextView,
- val iconImage: ImageView
+ val iconImage: ImageView,
) : RecyclerView.ViewHolder(itemView) {
+ /**
+ * Show a certain downloader application in the current View.
+ */
fun bind(app: DownloaderApp, onAppSelected: ((DownloaderApp) -> Unit)) {
itemView.app = app
itemView.setOnClickListener {
=====================================
components/feature/prompts/build.gradle
=====================================
@@ -31,6 +31,7 @@ tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach {
dependencies {
implementation project(':browser-state')
implementation project(':concept-engine')
+ implementation project(':feature-session')
implementation project(':lib-state')
implementation project(':support-ktx')
implementation project(':support-utils')
@@ -46,6 +47,7 @@ dependencies {
testImplementation Dependencies.testing_coroutines
testImplementation Dependencies.testing_robolectric
testImplementation Dependencies.testing_mockito
+ testImplementation project(':feature-session')
testImplementation project(':support-test')
testImplementation project(':support-test-libstate')
=====================================
components/feature/prompts/src/main/java/mozilla/components/feature/prompts/PromptFeature.kt
=====================================
@@ -71,6 +71,8 @@ import mozilla.components.feature.prompts.login.LoginExceptions
import mozilla.components.feature.prompts.login.LoginPicker
import mozilla.components.feature.prompts.share.DefaultShareDelegate
import mozilla.components.feature.prompts.share.ShareDelegate
+import mozilla.components.feature.session.SessionUseCases
+import mozilla.components.feature.session.SessionUseCases.ExitFullScreenUseCase
import mozilla.components.lib.state.ext.flowScoped
import mozilla.components.support.base.feature.ActivityResultHandler
import mozilla.components.support.base.feature.LifecycleAwareFeature
@@ -111,6 +113,7 @@ internal const val FRAGMENT_TAG = "mozac_feature_prompt_dialog"
* @property fragmentManager The [FragmentManager] to be used when displaying
* a dialog (fragment).
* @property shareDelegate Delegate used to display share sheet.
+ * @property exitFullscreenUsecase Usecase allowing to exit browser tabs' fullscreen mode.
* @property loginStorageDelegate Delegate used to access login storage. If null,
* 'save login'prompts will not be shown.
* @property isSaveLoginEnabled A callback invoked when a login prompt is triggered. If false,
@@ -144,6 +147,7 @@ class PromptFeature private constructor(
private var customTabId: String?,
private val fragmentManager: FragmentManager,
private val shareDelegate: ShareDelegate,
+ private val exitFullscreenUsecase: ExitFullScreenUseCase = SessionUseCases(store).exitFullscreen,
override val creditCardValidationDelegate: CreditCardValidationDelegate? = null,
override val loginValidationDelegate: LoginValidationDelegate? = null,
private val isSaveLoginEnabled: () -> Boolean = { false },
@@ -184,6 +188,7 @@ class PromptFeature private constructor(
customTabId: String? = null,
fragmentManager: FragmentManager,
shareDelegate: ShareDelegate = DefaultShareDelegate(),
+ exitFullscreenUsecase: ExitFullScreenUseCase = SessionUseCases(store).exitFullscreen,
creditCardValidationDelegate: CreditCardValidationDelegate? = null,
loginValidationDelegate: LoginValidationDelegate? = null,
isSaveLoginEnabled: () -> Boolean = { false },
@@ -202,6 +207,7 @@ class PromptFeature private constructor(
customTabId = customTabId,
fragmentManager = fragmentManager,
shareDelegate = shareDelegate,
+ exitFullscreenUsecase = exitFullscreenUsecase,
creditCardValidationDelegate = creditCardValidationDelegate,
loginValidationDelegate = loginValidationDelegate,
isSaveLoginEnabled = isSaveLoginEnabled,
@@ -222,6 +228,7 @@ class PromptFeature private constructor(
customTabId: String? = null,
fragmentManager: FragmentManager,
shareDelegate: ShareDelegate = DefaultShareDelegate(),
+ exitFullscreenUsecase: ExitFullScreenUseCase = SessionUseCases(store).exitFullscreen,
creditCardValidationDelegate: CreditCardValidationDelegate? = null,
loginValidationDelegate: LoginValidationDelegate? = null,
isSaveLoginEnabled: () -> Boolean = { false },
@@ -240,6 +247,7 @@ class PromptFeature private constructor(
customTabId = customTabId,
fragmentManager = fragmentManager,
shareDelegate = shareDelegate,
+ exitFullscreenUsecase = exitFullscreenUsecase,
creditCardValidationDelegate = creditCardValidationDelegate,
loginValidationDelegate = loginValidationDelegate,
isSaveLoginEnabled = isSaveLoginEnabled,
@@ -420,6 +428,10 @@ class PromptFeature private constructor(
internal fun onPromptRequested(session: SessionState) {
// Some requests are handle with intents
session.content.promptRequests.lastOrNull()?.let { promptRequest ->
+ store.state.findTabOrCustomTabOrSelectedTab(customTabId)?.let {
+ exitFullscreenUsecase(it.id)
+ }
+
when (promptRequest) {
is File -> filePicker.handleFileRequest(promptRequest)
is Share -> handleShareRequest(promptRequest, session)
View it on GitLab: https://gitlab.torproject.org/tpo/applications/android-components/-/compare…
--
View it on GitLab: https://gitlab.torproject.org/tpo/applications/android-components/-/compare…
You're receiving this email because of your account on gitlab.torproject.org.
1
0

[Git][tpo/applications/tor-browser][tor-browser-102.9.0esr-12.5-1] fixup! Bug 27476: Implement about:torconnect captive portal within Tor Browser
by Pier Angelo Vendrame (@pierov) 15 Mar '23
by Pier Angelo Vendrame (@pierov) 15 Mar '23
15 Mar '23
Pier Angelo Vendrame pushed to branch tor-browser-102.9.0esr-12.5-1 at The Tor Project / Applications / Tor Browser
Commits:
d41e60a2 by Dan Ballard at 2023-03-15T23:04:02+01:00
fixup! Bug 27476: Implement about:torconnect captive portal within Tor Browser
bug 41526: make tor connection cancel button grey
- - - - -
2 changed files:
- browser/components/torconnect/content/aboutTorConnect.css
- browser/components/torconnect/content/aboutTorConnect.js
Changes:
=====================================
browser/components/torconnect/content/aboutTorConnect.css
=====================================
@@ -136,6 +136,13 @@ button {
fill: white;
}
+#cancelButton {
+ color: var(--in-content-button-text-color);
+ border: 1px solid var(--in-content-button-border-color);
+ border-radius: 4px;
+ background-color: var(--in-content-button-background);
+}
+
#locationDropdownLabel {
margin-block: auto;
margin-inline: 4px;
=====================================
browser/components/torconnect/content/aboutTorConnect.js
=====================================
@@ -517,9 +517,6 @@ class AboutTorConnect {
this.hide(this.elements.viewLogButton);
}
this.show(this.elements.cancelButton, true);
- if (state.StateChanged) {
- this.elements.cancelButton.focus();
- }
}
showOffline(error) {
View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/commit/d41e60a…
--
View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/commit/d41e60a…
You're receiving this email because of your account on gitlab.torproject.org.
1
0

[Git][tpo/applications/tor-browser][tor-browser-102.8.0esr-12.5-1] fixup! Bug 27476: Implement about:torconnect captive portal within Tor Browser
by Pier Angelo Vendrame (@pierov) 15 Mar '23
by Pier Angelo Vendrame (@pierov) 15 Mar '23
15 Mar '23
Pier Angelo Vendrame pushed to branch tor-browser-102.8.0esr-12.5-1 at The Tor Project / Applications / Tor Browser
Commits:
28b9d8f1 by Dan Ballard at 2023-03-15T13:36:18-05:00
fixup! Bug 27476: Implement about:torconnect captive portal within Tor Browser
bug 41526: make tor connection cancel button grey
- - - - -
2 changed files:
- browser/components/torconnect/content/aboutTorConnect.css
- browser/components/torconnect/content/aboutTorConnect.js
Changes:
=====================================
browser/components/torconnect/content/aboutTorConnect.css
=====================================
@@ -136,6 +136,13 @@ button {
fill: white;
}
+#cancelButton {
+ color: var(--in-content-button-text-color);
+ border: 1px solid var(--in-content-button-border-color);
+ border-radius: 4px;
+ background-color: var(--in-content-button-background);
+}
+
#locationDropdownLabel {
margin-block: auto;
margin-inline: 4px;
=====================================
browser/components/torconnect/content/aboutTorConnect.js
=====================================
@@ -517,9 +517,6 @@ class AboutTorConnect {
this.hide(this.elements.viewLogButton);
}
this.show(this.elements.cancelButton, true);
- if (state.StateChanged) {
- this.elements.cancelButton.focus();
- }
}
showOffline(error) {
View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/commit/28b9d8f…
--
View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/commit/28b9d8f…
You're receiving this email because of your account on gitlab.torproject.org.
1
0

15 Mar '23
This is an automated email from the git hooks/post-receive script.
dgoulet pushed a change to branch release-0.4.7
in repository tor.
from 322c1ed8fb Merge branch 'maint-0.4.7' into release-0.4.7
new 33d5a7da9e gitignore: Add tags file from ctags
new aef76beccc Merge branch 'maint-0.4.7' into release-0.4.7
The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
Summary of changes:
.gitignore | 1 +
1 file changed, 1 insertion(+)
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.
1
1
This is an automated email from the git hooks/post-receive script.
dgoulet pushed a change to branch maint-0.4.7
in repository tor.
from 36612b9bf8 vote AuthDirMaxServersPerAddr in consensus params
new 33d5a7da9e gitignore: Add tags file from ctags
The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
Summary of changes:
.gitignore | 1 +
1 file changed, 1 insertion(+)
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.
1
0

15 Mar '23
This is an automated email from the git hooks/post-receive script.
dgoulet pushed a commit to branch main
in repository tor.
The following commit(s) were added to refs/heads/main by this push:
new 33d5a7da9e gitignore: Add tags file from ctags
new 447775a5e0 Merge branch 'maint-0.4.7'
33d5a7da9e is described below
commit 33d5a7da9e8401bf8af15d4fb4811f6c469ae40c
Author: David Goulet <dgoulet(a)torproject.org>
AuthorDate: Wed Mar 15 11:13:43 2023 -0400
gitignore: Add tags file from ctags
Signed-off-by: David Goulet <dgoulet(a)torproject.org>
---
.gitignore | 1 +
1 file changed, 1 insertion(+)
diff --git a/.gitignore b/.gitignore
index d3ac3bbc52..94988ed982 100644
--- a/.gitignore
+++ b/.gitignore
@@ -67,6 +67,7 @@ core.*
/stamp-h
/stamp-h.in
/stamp-h1
+/tags
/TAGS
/test-driver
/tor.sh
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.
1
0

[Git][tpo/applications/tor-browser][base-browser-102.9.0esr-12.5-1] 2 commits: Bug 1802385 - Use NS_GetFinalChannelURI in FetchDriver r=rpl, valentin
by Richard Pospesel (@richard) 15 Mar '23
by Richard Pospesel (@richard) 15 Mar '23
15 Mar '23
Richard Pospesel pushed to branch base-browser-102.9.0esr-12.5-1 at The Tor Project / Applications / Tor Browser
Commits:
3ba3eaa0 by Rob Wu at 2023-03-15T12:04:18+00:00
Bug 1802385 - Use NS_GetFinalChannelURI in FetchDriver r=rpl,valentin
Depends on D164656
Differential Revision: https://phabricator.services.mozilla.com/D166108
- - - - -
7c4809a5 by Kash Shampur at 2023-03-15T12:04:19+00:00
Bug 1803109 - Discard blocks of data that are too big for two chunks. r=canaltinova
Currently, `ReserveAndPutRaw` allocates a second span even if the data would be too big for the chunk.
Here a second conditional is added to check if the block of data is too big in this scenario and silently discard the data if so.
Differential Revision: https://phabricator.services.mozilla.com/D167167
- - - - -
2 changed files:
- dom/fetch/FetchDriver.cpp
- mozglue/baseprofiler/public/ProfileChunkedBuffer.h
Changes:
=====================================
dom/fetch/FetchDriver.cpp
=====================================
@@ -1178,14 +1178,6 @@ FetchDriver::OnStartRequest(nsIRequest* aRequest) {
response->InitChannelInfo(channel);
- nsCOMPtr<nsIURI> channelURI;
- rv = channel->GetURI(getter_AddRefs(channelURI));
- if (NS_WARN_IF(NS_FAILED(rv))) {
- FailWithNetworkError(rv);
- // Cancel request.
- return rv;
- }
-
nsCOMPtr<nsILoadInfo> loadInfo = channel->LoadInfo();
// Propagate any tainting from the channel back to our response here. This
// step is not reflected in the spec because the spec is written such that
@@ -1501,7 +1493,7 @@ FetchDriver::AsyncOnChannelRedirect(nsIChannel* aOldChannel,
// Response.redirected to true if an internal redirect occurs. These
// should be transparent to script.
nsCOMPtr<nsIURI> uri;
- MOZ_ALWAYS_SUCCEEDS(aNewChannel->GetURI(getter_AddRefs(uri)));
+ MOZ_ALWAYS_SUCCEEDS(NS_GetFinalChannelURI(aNewChannel, getter_AddRefs(uri)));
nsCOMPtr<nsIURI> uriClone;
nsresult rv = NS_GetURIWithoutRef(uri, getter_AddRefs(uriClone));
=====================================
mozglue/baseprofiler/public/ProfileChunkedBuffer.h
=====================================
@@ -1088,6 +1088,12 @@ class ProfileChunkedBuffer {
MOZ_ASSERT(maybeEntryWriter->RemainingBytes() == blockBytes);
mRangeEnd += blockBytes;
mPushedBlockCount += aBlockCount;
+ } else if (blockBytes >= current->BufferBytes()) {
+ // Currently only two buffer chunks are held at a time and it is not
+ // possible to write an object that takes up more space than this. In
+ // this scenario, silently discard this block of data if it is unable
+ // to fit into the two reserved profiler chunks.
+ mFailedPutBytes += blockBytes;
} else {
// Block doesn't fit fully in current chunk, it needs to overflow into
// the next one.
View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/compare/45a8e3…
--
View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/compare/45a8e3…
You're receiving this email because of your account on gitlab.torproject.org.
1
0

[Git][tpo/applications/tor-browser][tor-browser-102.9.0esr-12.5-1] 2 commits: Bug 1802385 - Use NS_GetFinalChannelURI in FetchDriver r=rpl, valentin
by Richard Pospesel (@richard) 15 Mar '23
by Richard Pospesel (@richard) 15 Mar '23
15 Mar '23
Richard Pospesel pushed to branch tor-browser-102.9.0esr-12.5-1 at The Tor Project / Applications / Tor Browser
Commits:
03f8127e by Rob Wu at 2023-03-15T12:03:27+00:00
Bug 1802385 - Use NS_GetFinalChannelURI in FetchDriver r=rpl,valentin
Depends on D164656
Differential Revision: https://phabricator.services.mozilla.com/D166108
- - - - -
7ed39eac by Kash Shampur at 2023-03-15T12:03:27+00:00
Bug 1803109 - Discard blocks of data that are too big for two chunks. r=canaltinova
Currently, `ReserveAndPutRaw` allocates a second span even if the data would be too big for the chunk.
Here a second conditional is added to check if the block of data is too big in this scenario and silently discard the data if so.
Differential Revision: https://phabricator.services.mozilla.com/D167167
- - - - -
2 changed files:
- dom/fetch/FetchDriver.cpp
- mozglue/baseprofiler/public/ProfileChunkedBuffer.h
Changes:
=====================================
dom/fetch/FetchDriver.cpp
=====================================
@@ -1178,14 +1178,6 @@ FetchDriver::OnStartRequest(nsIRequest* aRequest) {
response->InitChannelInfo(channel);
- nsCOMPtr<nsIURI> channelURI;
- rv = channel->GetURI(getter_AddRefs(channelURI));
- if (NS_WARN_IF(NS_FAILED(rv))) {
- FailWithNetworkError(rv);
- // Cancel request.
- return rv;
- }
-
nsCOMPtr<nsILoadInfo> loadInfo = channel->LoadInfo();
// Propagate any tainting from the channel back to our response here. This
// step is not reflected in the spec because the spec is written such that
@@ -1501,7 +1493,7 @@ FetchDriver::AsyncOnChannelRedirect(nsIChannel* aOldChannel,
// Response.redirected to true if an internal redirect occurs. These
// should be transparent to script.
nsCOMPtr<nsIURI> uri;
- MOZ_ALWAYS_SUCCEEDS(aNewChannel->GetURI(getter_AddRefs(uri)));
+ MOZ_ALWAYS_SUCCEEDS(NS_GetFinalChannelURI(aNewChannel, getter_AddRefs(uri)));
nsCOMPtr<nsIURI> uriClone;
nsresult rv = NS_GetURIWithoutRef(uri, getter_AddRefs(uriClone));
=====================================
mozglue/baseprofiler/public/ProfileChunkedBuffer.h
=====================================
@@ -1088,6 +1088,12 @@ class ProfileChunkedBuffer {
MOZ_ASSERT(maybeEntryWriter->RemainingBytes() == blockBytes);
mRangeEnd += blockBytes;
mPushedBlockCount += aBlockCount;
+ } else if (blockBytes >= current->BufferBytes()) {
+ // Currently only two buffer chunks are held at a time and it is not
+ // possible to write an object that takes up more space than this. In
+ // this scenario, silently discard this block of data if it is unable
+ // to fit into the two reserved profiler chunks.
+ mFailedPutBytes += blockBytes;
} else {
// Block doesn't fit fully in current chunk, it needs to overflow into
// the next one.
View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/compare/9f4e31…
--
View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/compare/9f4e31…
You're receiving this email because of your account on gitlab.torproject.org.
1
0