Skip to content
Snippets Groups Projects
Commit 3dcc5001 authored by Mugurell's avatar Mugurell Committed by Richard Pospesel
Browse files

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.
parent 6c861589
Branches
Tags
No related merge requests found
Showing
with 797 additions and 6 deletions
......@@ -108,6 +108,8 @@ 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.ext.accessibilityManager
import org.mozilla.fenix.ext.breadcrumb
import org.mozilla.fenix.ext.components
......@@ -213,6 +215,8 @@ abstract class BaseBrowserFragment :
@VisibleForTesting
internal val onboarding by lazy { FenixOnboarding(requireContext()) }
private var currentStartDownloadDialog: StartDownloadDialog? = null
@CallSuper
override fun onCreateView(
inflater: LayoutInflater,
......@@ -493,7 +497,20 @@ 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
}
},
)
downloadFeature.onDownloadStopped = { downloadState, _, downloadJobStatus ->
......@@ -1036,6 +1053,7 @@ abstract class BaseBrowserFragment :
it.selectedTab
}
.collect {
currentStartDownloadDialog?.dismiss()
handleTabSelected(it)
}
}
......@@ -1104,6 +1122,7 @@ abstract class BaseBrowserFragment :
override fun onStop() {
super.onStop()
initUIJob?.cancel()
currentStartDownloadDialog?.dismiss()
requireComponents.core.store.state.findTabOrCustomTabOrSelectedTab(customTabSessionId)
?.let { session ->
......@@ -1119,6 +1138,10 @@ abstract class BaseBrowserFragment :
return findInPageIntegration.onBackPressed() ||
fullScreenFeature.onBackPressed() ||
promptsFeature.onBackPressed() ||
currentStartDownloadDialog?.let {
it.dismiss()
true
} ?: false ||
sessionFeature.onBackPressed() ||
removeSessionIfNeeded()
}
......
......@@ -28,11 +28,12 @@ class FenixSnackbarBehavior<V : View>(
) : CoordinatorLayout.Behavior<V>(context, null) {
private val dependenciesIds = listOf(
R.id.startDownloadDialogContainer,
R.id.viewDynamicDownloadDialog,
R.id.toolbar,
)
private var currentAnchorId = View.NO_ID
private var currentAnchorId: Int? = View.NO_ID
override fun layoutDependsOn(
parent: CoordinatorLayout,
......@@ -55,9 +56,9 @@ class FenixSnackbarBehavior<V : View>(
}
}
private fun positionSnackbar(child: View, dependency: View?) {
private fun positionSnackbar(snackbar: View, dependency: View?) {
currentAnchorId = dependency?.id ?: View.NO_ID
val params = child.layoutParams as CoordinatorLayout.LayoutParams
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.
......@@ -71,6 +72,6 @@ class FenixSnackbarBehavior<V : View>(
params.gravity = Gravity.TOP or Gravity.CENTER_HORIZONTAL
}
child.layoutParams = params
snackbar.layoutParams = params
}
}
/* 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.toMegabyteOrKilobyteString
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)
}
}
},
)
}
}
}
<?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>
<?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" />
......@@ -66,10 +66,19 @@
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:layout_height="wrap_content"
android:elevation="@dimen/browser_fragment_toolbar_elevation"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
......
<?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>
......@@ -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>
......@@ -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>
......
......@@ -72,6 +72,18 @@ class FenixSnackbarBehaviorTest {
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)
......@@ -98,6 +110,58 @@ class FenixSnackbarBehaviorTest {
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)
......
/* 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)
}
}
/* 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
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please to comment