Richard Pospesel pushed to branch tor-browser-102.2.1-12.5-1 at The Tor Project / Applications / fenix

Commits:

20 changed files:

Changes:

  • app/src/androidTest/java/org/mozilla/fenix/ui/CollectionTest.kt
    ... ... @@ -57,8 +57,10 @@ class CollectionTest {
    57 57
             featureSettingsHelper.resetAllFeatureFlags()
    
    58 58
         }
    
    59 59
     
    
    60
    -    @Test
    
    60
    +
    
    61 61
         // open a webpage, and add currently opened tab to existing collection
    
    62
    +    @Ignore("Failing, see: https://bugzilla.mozilla.org/show_bug.cgi?id=1812580")
    
    63
    +    @Test
    
    62 64
         fun mainMenuSaveToExistingCollection() {
    
    63 65
             val firstWebPage = getGenericAsset(mockWebServer, 1)
    
    64 66
             val secondWebPage = getGenericAsset(mockWebServer, 2)
    
    ... ... @@ -84,6 +86,7 @@ class CollectionTest {
    84 86
             }
    
    85 87
         }
    
    86 88
     
    
    89
    +    @Ignore("Failing, see: https://bugzilla.mozilla.org/show_bug.cgi?id=1812580")
    
    87 90
         @Test
    
    88 91
         fun verifyAddTabButtonOfCollectionMenu() {
    
    89 92
             val firstWebPage = getGenericAsset(mockWebServer, 1)
    

  • app/src/androidTest/java/org/mozilla/fenix/ui/DownloadFileTypesTest.kt
    ... ... @@ -75,7 +75,7 @@ class DownloadFileTypesTest(fileName: String) {
    75 75
                 verifyDownloadPrompt(downloadFile)
    
    76 76
             }.clickDownload {
    
    77 77
                 verifyDownloadNotificationPopup()
    
    78
    -        }.closePrompt {
    
    78
    +        }.closeCompletedDownloadPrompt {
    
    79 79
             }.openThreeDotMenu {
    
    80 80
             }.openDownloadsManager {
    
    81 81
                 waitForDownloadsListToExist()
    

  • app/src/androidTest/java/org/mozilla/fenix/ui/DownloadTest.kt
    ... ... @@ -105,7 +105,7 @@ class DownloadTest {
    105 105
                 verifyDownloadPrompt(downloadFile)
    
    106 106
             }.clickDownload {
    
    107 107
                 verifyDownloadNotificationPopup()
    
    108
    -        }.closePrompt { }
    
    108
    +        }
    
    109 109
             mDevice.openNotification()
    
    110 110
             notificationShade {
    
    111 111
                 verifySystemNotificationExists("Download completed")
    

  • app/src/androidTest/java/org/mozilla/fenix/ui/robots/DownloadRobot.kt
    ... ... @@ -12,7 +12,6 @@ import androidx.test.espresso.Espresso.onView
    12 12
     import androidx.test.espresso.assertion.ViewAssertions.matches
    
    13 13
     import androidx.test.espresso.intent.Intents
    
    14 14
     import androidx.test.espresso.intent.matcher.IntentMatchers
    
    15
    -import androidx.test.espresso.matcher.RootMatchers.isDialog
    
    16 15
     import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
    
    17 16
     import androidx.test.espresso.matcher.ViewMatchers.withContentDescription
    
    18 17
     import androidx.test.espresso.matcher.ViewMatchers.withId
    
    ... ... @@ -82,6 +81,13 @@ class DownloadRobot {
    82 81
                 return Transition()
    
    83 82
             }
    
    84 83
     
    
    84
    +        fun closeCompletedDownloadPrompt(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
    
    85
    +            closeCompletedDownloadButton().click()
    
    86
    +
    
    87
    +            BrowserRobot().interact()
    
    88
    +            return BrowserRobot.Transition()
    
    89
    +        }
    
    90
    +
    
    85 91
             fun closePrompt(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
    
    86 92
                 closePromptButton().click()
    
    87 93
     
    
    ... ... @@ -177,12 +183,14 @@ private fun assertDownloadNotificationPopup() {
    177 183
         )
    
    178 184
     }
    
    179 185
     
    
    186
    +private fun closeCompletedDownloadButton() =
    
    187
    +    onView(withId(R.id.download_dialog_close_button))
    
    188
    +
    
    180 189
     private fun closePromptButton() =
    
    181
    -    onView(withContentDescription("Close"))
    
    190
    +    onView(withId(R.id.close_button))
    
    182 191
     
    
    183 192
     private fun downloadButton() =
    
    184 193
         onView(withText("Download"))
    
    185
    -        .inRoot(isDialog())
    
    186 194
             .check(matches(isDisplayed()))
    
    187 195
     
    
    188 196
     private fun openDownloadButton() =
    

  • app/src/main/java/org/mozilla/fenix/browser/BaseBrowserFragment.kt
    ... ... @@ -108,6 +108,9 @@ import org.mozilla.fenix.components.toolbar.DefaultBrowserToolbarMenuController
    108 108
     import org.mozilla.fenix.components.toolbar.ToolbarIntegration
    
    109 109
     import org.mozilla.fenix.downloads.DownloadService
    
    110 110
     import org.mozilla.fenix.downloads.DynamicDownloadDialog
    
    111
    +import org.mozilla.fenix.downloads.FirstPartyDownloadDialog
    
    112
    +import org.mozilla.fenix.downloads.StartDownloadDialog
    
    113
    +import org.mozilla.fenix.downloads.ThirdPartyDownloadDialog
    
    111 114
     import org.mozilla.fenix.ext.accessibilityManager
    
    112 115
     import org.mozilla.fenix.ext.breadcrumb
    
    113 116
     import org.mozilla.fenix.ext.components
    
    ... ... @@ -213,6 +216,8 @@ abstract class BaseBrowserFragment :
    213 216
         @VisibleForTesting
    
    214 217
         internal val onboarding by lazy { FenixOnboarding(requireContext()) }
    
    215 218
     
    
    219
    +    private var currentStartDownloadDialog: StartDownloadDialog? = null
    
    220
    +
    
    216 221
         @CallSuper
    
    217 222
         override fun onCreateView(
    
    218 223
             inflater: LayoutInflater,
    
    ... ... @@ -345,7 +350,7 @@ abstract class BaseBrowserFragment :
    345 350
                     }
    
    346 351
     
    
    347 352
                     viewLifecycleOwner.lifecycleScope.allowUndo(
    
    348
    -                    binding.browserLayout,
    
    353
    +                    binding.dynamicSnackbarContainer,
    
    349 354
                         snackbarMessage,
    
    350 355
                         requireContext().getString(R.string.snackbar_deleted_undo),
    
    351 356
                         {
    
    ... ... @@ -424,7 +429,7 @@ abstract class BaseBrowserFragment :
    424 429
                 feature = ContextMenuFeature(
    
    425 430
                     fragmentManager = parentFragmentManager,
    
    426 431
                     store = store,
    
    427
    -                candidates = getContextMenuCandidates(context, binding.browserLayout),
    
    432
    +                candidates = getContextMenuCandidates(context, binding.dynamicSnackbarContainer),
    
    428 433
                     engineView = binding.engineView,
    
    429 434
                     useCases = context.components.useCases.contextMenuUseCases,
    
    430 435
                     tabId = customTabSessionId
    
    ... ... @@ -493,7 +498,32 @@ abstract class BaseBrowserFragment :
    493 498
                 ),
    
    494 499
                 onNeedToRequestPermissions = { permissions ->
    
    495 500
                     requestPermissions(permissions, REQUEST_CODE_DOWNLOAD_PERMISSIONS)
    
    496
    -            }
    
    501
    +            },
    
    502
    +            customFirstPartyDownloadDialog = { filename, contentSize, positiveAction, negativeAction ->
    
    503
    +                FirstPartyDownloadDialog(
    
    504
    +                    activity = requireActivity(),
    
    505
    +                    filename = filename.value,
    
    506
    +                    contentSize = contentSize.value,
    
    507
    +                    positiveButtonAction = positiveAction.value,
    
    508
    +                    negativeButtonAction = negativeAction.value,
    
    509
    +                ).onDismiss {
    
    510
    +                    currentStartDownloadDialog = null
    
    511
    +                }.show(binding.startDownloadDialogContainer).also {
    
    512
    +                    currentStartDownloadDialog = it
    
    513
    +                }
    
    514
    +            },
    
    515
    +            customThirdPartyDownloadDialog = { downloaderApps, onAppSelected, negativeActionCallback ->
    
    516
    +                ThirdPartyDownloadDialog(
    
    517
    +                    activity = requireActivity(),
    
    518
    +                    downloaderApps = downloaderApps.value,
    
    519
    +                    onAppSelected = onAppSelected.value,
    
    520
    +                    negativeButtonAction = negativeActionCallback.value,
    
    521
    +                ).onDismiss {
    
    522
    +                    currentStartDownloadDialog = null
    
    523
    +                }.show(binding.startDownloadDialogContainer).also {
    
    524
    +                    currentStartDownloadDialog = it
    
    525
    +                }
    
    526
    +            },
    
    497 527
             )
    
    498 528
     
    
    499 529
             downloadFeature.onDownloadStopped = { downloadState, _, downloadJobStatus ->
    
    ... ... @@ -512,7 +542,7 @@ abstract class BaseBrowserFragment :
    512 542
                         didFail = downloadJobStatus == DownloadState.Status.FAILED,
    
    513 543
                         tryAgain = downloadFeature::tryAgain,
    
    514 544
                         onCannotOpenFile = {
    
    515
    -                        showCannotOpenFileError(binding.browserLayout, context, it)
    
    545
    +                        showCannotOpenFileError(binding.dynamicSnackbarContainer, context, it)
    
    516 546
                         },
    
    517 547
                         binding = binding.viewDynamicDownloadDialog,
    
    518 548
                         toolbarHeight = toolbarHeight
    
    ... ... @@ -945,7 +975,7 @@ abstract class BaseBrowserFragment :
    945 975
                 didFail = savedDownloadState.second,
    
    946 976
                 tryAgain = onTryAgain,
    
    947 977
                 onCannotOpenFile = {
    
    948
    -                showCannotOpenFileError(binding.browserLayout, context, it)
    
    978
    +                showCannotOpenFileError(binding.dynamicSnackbarContainer, context, it)
    
    949 979
                 },
    
    950 980
                 binding = binding.viewDynamicDownloadDialog,
    
    951 981
                 toolbarHeight = toolbarHeight,
    
    ... ... @@ -1036,6 +1066,7 @@ abstract class BaseBrowserFragment :
    1036 1066
                         it.selectedTab
    
    1037 1067
                     }
    
    1038 1068
                     .collect {
    
    1069
    +                    currentStartDownloadDialog?.dismiss()
    
    1039 1070
                         handleTabSelected(it)
    
    1040 1071
                     }
    
    1041 1072
             }
    
    ... ... @@ -1104,6 +1135,7 @@ abstract class BaseBrowserFragment :
    1104 1135
         override fun onStop() {
    
    1105 1136
             super.onStop()
    
    1106 1137
             initUIJob?.cancel()
    
    1138
    +        currentStartDownloadDialog?.dismiss()
    
    1107 1139
     
    
    1108 1140
             requireComponents.core.store.state.findTabOrCustomTabOrSelectedTab(customTabSessionId)
    
    1109 1141
                 ?.let { session ->
    
    ... ... @@ -1119,6 +1151,10 @@ abstract class BaseBrowserFragment :
    1119 1151
             return findInPageIntegration.onBackPressed() ||
    
    1120 1152
                 fullScreenFeature.onBackPressed() ||
    
    1121 1153
                 promptsFeature.onBackPressed() ||
    
    1154
    +            currentStartDownloadDialog?.let {
    
    1155
    +                it.dismiss()
    
    1156
    +                true
    
    1157
    +            } ?: false ||
    
    1122 1158
                 sessionFeature.onBackPressed() ||
    
    1123 1159
                 removeSessionIfNeeded()
    
    1124 1160
         }
    
    ... ... @@ -1281,7 +1317,7 @@ abstract class BaseBrowserFragment :
    1281 1317
                     withContext(Main) {
    
    1282 1318
                         view?.let {
    
    1283 1319
                             FenixSnackbar.make(
    
    1284
    -                            view = binding.browserLayout,
    
    1320
    +                            view = binding.dynamicSnackbarContainer,
    
    1285 1321
                                 duration = FenixSnackbar.LENGTH_LONG,
    
    1286 1322
                                 isDisplayedWithBrowserToolbar = true
    
    1287 1323
                             )
    
    ... ... @@ -1303,7 +1339,7 @@ abstract class BaseBrowserFragment :
    1303 1339
     
    
    1304 1340
                         view?.let {
    
    1305 1341
                             FenixSnackbar.make(
    
    1306
    -                            view = binding.browserLayout,
    
    1342
    +                            view = binding.dynamicSnackbarContainer,
    
    1307 1343
                                 duration = FenixSnackbar.LENGTH_LONG,
    
    1308 1344
                                 isDisplayedWithBrowserToolbar = true
    
    1309 1345
                             )
    
    ... ... @@ -1346,7 +1382,7 @@ abstract class BaseBrowserFragment :
    1346 1382
                 // Close find in page bar if opened
    
    1347 1383
                 findInPageIntegration.onBackPressed()
    
    1348 1384
                 FenixSnackbar.make(
    
    1349
    -                view = binding.browserLayout,
    
    1385
    +                view = binding.dynamicSnackbarContainer,
    
    1350 1386
                     duration = Snackbar.LENGTH_SHORT,
    
    1351 1387
                     isDisplayedWithBrowserToolbar = false
    
    1352 1388
                 )
    
    ... ... @@ -1420,12 +1456,12 @@ abstract class BaseBrowserFragment :
    1420 1456
         }
    
    1421 1457
     
    
    1422 1458
         private fun showCannotOpenFileError(
    
    1423
    -        view: View,
    
    1459
    +        container: ViewGroup,
    
    1424 1460
             context: Context,
    
    1425 1461
             downloadState: DownloadState
    
    1426 1462
         ) {
    
    1427 1463
             FenixSnackbar.make(
    
    1428
    -            view = view,
    
    1464
    +            view = container,
    
    1429 1465
                 duration = Snackbar.LENGTH_SHORT,
    
    1430 1466
                 isDisplayedWithBrowserToolbar = true
    
    1431 1467
             ).setText(DynamicDownloadDialog.getCannotOpenFileErrorMessage(context, downloadState))
    

  • app/src/main/java/org/mozilla/fenix/browser/BrowserFragment.kt
    ... ... @@ -347,7 +347,7 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler {
    347 347
                         }
    
    348 348
                     }
    
    349 349
                     FenixSnackbar.make(
    
    350
    -                    view = binding.browserLayout,
    
    350
    +                    view = binding.dynamicSnackbarContainer,
    
    351 351
                         duration = Snackbar.LENGTH_SHORT,
    
    352 352
                         isDisplayedWithBrowserToolbar = true
    
    353 353
                     )
    

  • app/src/main/java/org/mozilla/fenix/components/FenixSnackbar.kt
    ... ... @@ -141,10 +141,20 @@ class FenixSnackbar private constructor(
    141 141
                             0
    
    142 142
                         }
    
    143 143
                     )
    
    144
    +
    
    145
    +                if (parent.id == R.id.dynamicSnackbarContainer) {
    
    146
    +                    (parent.layoutParams as? CoordinatorLayout.LayoutParams)?.apply {
    
    147
    +                        behavior = FenixSnackbarBehavior<FrameLayout>(
    
    148
    +                            context = view.context,
    
    149
    +                            toolbarPosition = view.context.settings().toolbarPosition,
    
    150
    +                        )
    
    151
    +                    }
    
    152
    +                }
    
    144 153
                 }
    
    145 154
             }
    
    146 155
     
    
    147 156
             // Use the same implementation of `Snackbar`
    
    157
    +        @Suppress("ReturnCount")
    
    148 158
             private fun findSuitableParent(_view: View?): ViewGroup? {
    
    149 159
                 var view = _view
    
    150 160
                 var fallback: ViewGroup? = null
    
    ... ... @@ -159,6 +169,10 @@ class FenixSnackbar private constructor(
    159 169
                             return view
    
    160 170
                         }
    
    161 171
     
    
    172
    +                    if (view.id == R.id.dynamicSnackbarContainer) {
    
    173
    +                        return view
    
    174
    +                    }
    
    175
    +
    
    162 176
                         fallback = view
    
    163 177
                     }
    
    164 178
     
    

  • app/src/main/java/org/mozilla/fenix/components/FenixSnackbarBehavior.kt
    1
    +/* This Source Code Form is subject to the terms of the Mozilla Public
    
    2
    + * License, v. 2.0. If a copy of the MPL was not distributed with this
    
    3
    + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
    
    4
    +
    
    5
    +package org.mozilla.fenix.components
    
    6
    +
    
    7
    +import android.content.Context
    
    8
    +import android.view.Gravity
    
    9
    +import android.view.View
    
    10
    +import androidx.annotation.VisibleForTesting
    
    11
    +import androidx.coordinatorlayout.widget.CoordinatorLayout
    
    12
    +import androidx.core.view.children
    
    13
    +import androidx.core.view.isVisible
    
    14
    +import org.mozilla.fenix.R
    
    15
    +import org.mozilla.fenix.components.toolbar.ToolbarPosition
    
    16
    +
    
    17
    +/**
    
    18
    + * [CoordinatorLayout.Behavior] to be used by a snackbar that want to ensure it it always positioned
    
    19
    + * such that it will be shown on top (vertically) of other siblings that may obstruct it's view.
    
    20
    + *
    
    21
    + * @param context [Context] used for various system interactions.
    
    22
    + * @param toolbarPosition Where the toolbar is positioned on the screen.
    
    23
    + * Depending on it's position (top / bottom) the snackbar will be shown below / above the toolbar.
    
    24
    + */
    
    25
    +class FenixSnackbarBehavior<V : View>(
    
    26
    +    context: Context,
    
    27
    +    @get:VisibleForTesting internal val toolbarPosition: ToolbarPosition,
    
    28
    +) : CoordinatorLayout.Behavior<V>(context, null) {
    
    29
    +
    
    30
    +    private val dependenciesIds = listOf(
    
    31
    +        R.id.startDownloadDialogContainer,
    
    32
    +        R.id.viewDynamicDownloadDialog,
    
    33
    +        R.id.toolbar,
    
    34
    +    )
    
    35
    +
    
    36
    +    private var currentAnchorId: Int? = View.NO_ID
    
    37
    +
    
    38
    +    override fun layoutDependsOn(
    
    39
    +        parent: CoordinatorLayout,
    
    40
    +        child: V,
    
    41
    +        dependency: View,
    
    42
    +    ): Boolean {
    
    43
    +        val anchorId = dependenciesIds
    
    44
    +            .intersect(parent.children.filter { it.isVisible }.map { it.id }.toSet())
    
    45
    +            .firstOrNull()
    
    46
    +
    
    47
    +        // It is possible that previous anchor's visibility is changed.
    
    48
    +        // The layout is updated and layoutDependsOn is called but onDependentViewChanged not.
    
    49
    +        // We have to check here if a new anchor is available and reparent the snackbar.
    
    50
    +        // This check also ensures we are not positioning the snackbar multiple times for the same anchor.
    
    51
    +        return if (anchorId != currentAnchorId) {
    
    52
    +            positionSnackbar(child, parent.children.firstOrNull { it.id == anchorId })
    
    53
    +            true
    
    54
    +        } else {
    
    55
    +            false
    
    56
    +        }
    
    57
    +    }
    
    58
    +
    
    59
    +    private fun positionSnackbar(snackbar: View, dependency: View?) {
    
    60
    +        currentAnchorId = dependency?.id ?: View.NO_ID
    
    61
    +        val params = snackbar.layoutParams as CoordinatorLayout.LayoutParams
    
    62
    +
    
    63
    +        if (dependency == null || (dependency.id == R.id.toolbar && toolbarPosition == ToolbarPosition.TOP)) {
    
    64
    +            // Position the snackbar at the bottom of the screen.
    
    65
    +            params.anchorId = View.NO_ID
    
    66
    +            params.anchorGravity = Gravity.NO_GRAVITY
    
    67
    +            params.gravity = Gravity.BOTTOM or Gravity.CENTER_HORIZONTAL
    
    68
    +        } else {
    
    69
    +            // Position the snackbar just above the anchor.
    
    70
    +            params.anchorId = dependency.id
    
    71
    +            params.anchorGravity = Gravity.TOP or Gravity.CENTER_HORIZONTAL
    
    72
    +            params.gravity = Gravity.TOP or Gravity.CENTER_HORIZONTAL
    
    73
    +        }
    
    74
    +
    
    75
    +        snackbar.layoutParams = params
    
    76
    +    }
    
    77
    +}

  • app/src/main/java/org/mozilla/fenix/downloads/StartDownloadDialog.kt
    1
    +/* This Source Code Form is subject to the terms of the Mozilla Public
    
    2
    + * License, v. 2.0. If a copy of the MPL was not distributed with this
    
    3
    + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
    
    4
    +
    
    5
    +package org.mozilla.fenix.downloads
    
    6
    +
    
    7
    +import android.app.Activity
    
    8
    +import android.app.Dialog
    
    9
    +import android.view.Gravity
    
    10
    +import android.view.LayoutInflater
    
    11
    +import android.view.View
    
    12
    +import android.view.ViewGroup
    
    13
    +import android.view.ViewTreeObserver.OnGlobalLayoutListener
    
    14
    +import android.view.Window
    
    15
    +import android.view.accessibility.AccessibilityEvent
    
    16
    +import android.view.accessibility.AccessibilityNodeInfo
    
    17
    +import androidx.annotation.VisibleForTesting
    
    18
    +import androidx.coordinatorlayout.widget.CoordinatorLayout
    
    19
    +import androidx.core.content.ContextCompat
    
    20
    +import androidx.core.view.ViewCompat
    
    21
    +import androidx.core.view.children
    
    22
    +import androidx.viewbinding.ViewBinding
    
    23
    +import mozilla.components.feature.downloads.databinding.MozacDownloaderChooserPromptBinding
    
    24
    +import mozilla.components.feature.downloads.toMegabyteOrKilobyteString
    
    25
    +import mozilla.components.feature.downloads.ui.DownloaderApp
    
    26
    +import mozilla.components.feature.downloads.ui.DownloaderAppAdapter
    
    27
    +import mozilla.components.support.ktx.android.view.setNavigationBarTheme
    
    28
    +import mozilla.components.support.ktx.android.view.setStatusBarTheme
    
    29
    +import org.mozilla.fenix.R
    
    30
    +import org.mozilla.fenix.databinding.DialogScrimBinding
    
    31
    +import org.mozilla.fenix.databinding.StartDownloadDialogLayoutBinding
    
    32
    +import org.mozilla.fenix.ext.settings
    
    33
    +
    
    34
    +/**
    
    35
    + * Parent of all download views that can mimic a modal [Dialog].
    
    36
    + *
    
    37
    + * @param activity The [Activity] in which the dialog will be shown.
    
    38
    + * Used to update the activity [Window] to best mimic a modal dialog.
    
    39
    + */
    
    40
    +abstract class StartDownloadDialog(
    
    41
    +    private val activity: Activity,
    
    42
    +) {
    
    43
    +    @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
    
    44
    +    internal var binding: ViewBinding? = null
    
    45
    +
    
    46
    +    @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
    
    47
    +    internal var container: ViewGroup? = null
    
    48
    +    private var scrim: DialogScrimBinding? = null
    
    49
    +
    
    50
    +    @VisibleForTesting
    
    51
    +    internal var onDismiss: () -> Unit = {}
    
    52
    +
    
    53
    +    @VisibleForTesting
    
    54
    +    internal var initialNavigationBarColor = activity.window.navigationBarColor
    
    55
    +
    
    56
    +    @VisibleForTesting
    
    57
    +    internal var initialStatusBarColor = activity.window.statusBarColor
    
    58
    +
    
    59
    +    /**
    
    60
    +     * Show the download view.
    
    61
    +     *
    
    62
    +     * @param container The [ViewGroup] in which the download view will be inflated.
    
    63
    +     */
    
    64
    +    fun show(container: ViewGroup): StartDownloadDialog {
    
    65
    +        this.container = container
    
    66
    +
    
    67
    +        val dialogParent = container.parent as? ViewGroup
    
    68
    +        dialogParent?.let {
    
    69
    +            scrim = DialogScrimBinding.inflate(LayoutInflater.from(activity), dialogParent, true).apply {
    
    70
    +                this.scrim.setOnClickListener {
    
    71
    +                    // Empty listener needed to prevent clicking through.
    
    72
    +                }
    
    73
    +            }
    
    74
    +        }
    
    75
    +
    
    76
    +        setupView()
    
    77
    +
    
    78
    +        if (activity.settings().accessibilityServicesEnabled) {
    
    79
    +            disableSiblingsAccessibility(dialogParent)
    
    80
    +        }
    
    81
    +
    
    82
    +        container.apply {
    
    83
    +            val params = layoutParams as CoordinatorLayout.LayoutParams
    
    84
    +            params.gravity = Gravity.BOTTOM or Gravity.CENTER_HORIZONTAL
    
    85
    +            layoutParams = params
    
    86
    +
    
    87
    +            // Set a higher elevation than the toolbar sibling which we should cover.
    
    88
    +            elevation = activity.resources.getDimension(R.dimen.browser_fragment_download_dialog_elevation)
    
    89
    +            visibility = View.VISIBLE
    
    90
    +        }
    
    91
    +
    
    92
    +        activity.window.setNavigationBarTheme(ContextCompat.getColor(activity, R.color.material_scrim_color))
    
    93
    +        activity.window.setStatusBarTheme(ContextCompat.getColor(activity, R.color.material_scrim_color))
    
    94
    +
    
    95
    +        return this
    
    96
    +    }
    
    97
    +
    
    98
    +    /**
    
    99
    +     * Set a callback for when the download view is dismissed.
    
    100
    +     *
    
    101
    +     * @param callback The callback for when the view is dismissed.
    
    102
    +     */
    
    103
    +    fun onDismiss(callback: () -> Unit): StartDownloadDialog {
    
    104
    +        this.onDismiss = callback
    
    105
    +        return this
    
    106
    +    }
    
    107
    +
    
    108
    +    /**
    
    109
    +     * Immediately dismiss the current download view if it is shown.
    
    110
    +     * This will restore the previous UI removing any other layout / window customizations.
    
    111
    +     */
    
    112
    +    fun dismiss() {
    
    113
    +        scrim?.let {
    
    114
    +            (it.root.parent as? ViewGroup)?.removeView(it.root)
    
    115
    +        }
    
    116
    +        binding?.let {
    
    117
    +            (it.root.parent as? ViewGroup)?.removeView(it.root)
    
    118
    +        }
    
    119
    +        enableSiblingsAccessibility(container?.parent as? ViewGroup)
    
    120
    +
    
    121
    +        container?.visibility = View.GONE
    
    122
    +
    
    123
    +        activity.window.setNavigationBarTheme(initialNavigationBarColor)
    
    124
    +        activity.window.setStatusBarTheme(initialStatusBarColor)
    
    125
    +
    
    126
    +        onDismiss()
    
    127
    +    }
    
    128
    +
    
    129
    +    @VisibleForTesting
    
    130
    +    internal fun enableSiblingsAccessibility(parent: ViewGroup?) {
    
    131
    +        parent?.children
    
    132
    +            ?.filterNot { it.id == R.id.startDownloadDialogContainer }
    
    133
    +            ?.forEach {
    
    134
    +                ViewCompat.setImportantForAccessibility(
    
    135
    +                    it,
    
    136
    +                    ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES,
    
    137
    +                )
    
    138
    +            }
    
    139
    +    }
    
    140
    +
    
    141
    +    @VisibleForTesting
    
    142
    +    internal fun disableSiblingsAccessibility(parent: ViewGroup?) {
    
    143
    +        parent?.children
    
    144
    +            ?.filterNot { it.id == R.id.startDownloadDialogContainer }
    
    145
    +            ?.forEach {
    
    146
    +                ViewCompat.setImportantForAccessibility(
    
    147
    +                    it,
    
    148
    +                    ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS,
    
    149
    +                )
    
    150
    +            }
    
    151
    +    }
    
    152
    +
    
    153
    +    /**
    
    154
    +     * Bind all download data to the download view.
    
    155
    +     */
    
    156
    +    @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
    
    157
    +    internal abstract fun setupView()
    
    158
    +}
    
    159
    +
    
    160
    +/**
    
    161
    + * A download view mimicking a modal dialog that allows the user to download a file with the current application.
    
    162
    + *
    
    163
    + * @param activity The [Activity] in which the dialog will be shown.
    
    164
    + * Used to update the activity [Window] to best mimic a modal dialog.
    
    165
    + * @param filename Name of the file to be downloaded. It wil be shown without any modification.
    
    166
    + * @param contentSize Size of the file to be downloaded expressed as a number of bytes.
    
    167
    + * It will automatically be parsed to the appropriate kilobyte or megabyte value before being shown.
    
    168
    + * @param positiveButtonAction Callback for when the user interacts with the dialog to start the download.
    
    169
    + * @param negativeButtonAction Callback for when the user interacts with the dialog to dismiss it.
    
    170
    + */
    
    171
    +class FirstPartyDownloadDialog(
    
    172
    +    private val activity: Activity,
    
    173
    +    private val filename: String,
    
    174
    +    private val contentSize: Long,
    
    175
    +    private val positiveButtonAction: () -> Unit,
    
    176
    +    private val negativeButtonAction: () -> Unit,
    
    177
    +) : StartDownloadDialog(activity) {
    
    178
    +    override fun setupView() {
    
    179
    +        val dialog = StartDownloadDialogLayoutBinding.inflate(LayoutInflater.from(activity), container, true)
    
    180
    +            .also { binding = it }
    
    181
    +
    
    182
    +        if (contentSize > 0L) {
    
    183
    +            val contentSize = contentSize.toMegabyteOrKilobyteString()
    
    184
    +            dialog.title.text =
    
    185
    +                activity.getString(R.string.mozac_feature_downloads_dialog_title2, contentSize)
    
    186
    +        }
    
    187
    +
    
    188
    +        dialog.filename.text = filename
    
    189
    +
    
    190
    +        dialog.downloadButton.setOnClickListener {
    
    191
    +            positiveButtonAction()
    
    192
    +            dismiss()
    
    193
    +        }
    
    194
    +
    
    195
    +        dialog.closeButton.setOnClickListener {
    
    196
    +            negativeButtonAction()
    
    197
    +            dismiss()
    
    198
    +        }
    
    199
    +
    
    200
    +        if (activity.settings().accessibilityServicesEnabled) {
    
    201
    +            // Ensure the title of the dialog is focused and read by talkback first.
    
    202
    +            dialog.root.viewTreeObserver.addOnGlobalLayoutListener(
    
    203
    +                object : OnGlobalLayoutListener {
    
    204
    +                    override fun onGlobalLayout() {
    
    205
    +                        dialog.root.viewTreeObserver.removeOnGlobalLayoutListener(this)
    
    206
    +                        dialog.title.run {
    
    207
    +                            sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED)
    
    208
    +                            performAccessibilityAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS, null)
    
    209
    +                        }
    
    210
    +                    }
    
    211
    +                },
    
    212
    +            )
    
    213
    +        }
    
    214
    +    }
    
    215
    +}
    
    216
    +
    
    217
    +/**
    
    218
    + * A download view mimicking a modal dialog that presents the user with a list of all apps
    
    219
    + * that can handle the download request.
    
    220
    + *
    
    221
    + * @param activity The [Activity] in which the dialog will be shown.
    
    222
    + * Used to update the activity [Window] to best mimic a modal dialog.
    
    223
    + * @param downloaderApps List of all applications that can handle the download request.
    
    224
    + * @param onAppSelected Callback for when the user chooses a specific application to handle the download request.
    
    225
    + * @param negativeButtonAction Callback for when the user interacts with the dialog to dismiss it.
    
    226
    + */
    
    227
    +class ThirdPartyDownloadDialog(
    
    228
    +    private val activity: Activity,
    
    229
    +    private val downloaderApps: List<DownloaderApp>,
    
    230
    +    private val onAppSelected: (DownloaderApp) -> Unit,
    
    231
    +    private val negativeButtonAction: () -> Unit,
    
    232
    +) : StartDownloadDialog(activity) {
    
    233
    +    override fun setupView() {
    
    234
    +        val dialog = MozacDownloaderChooserPromptBinding.inflate(LayoutInflater.from(activity), container, true)
    
    235
    +            .also { binding = it }
    
    236
    +
    
    237
    +        val recyclerView = dialog.appsList
    
    238
    +        recyclerView.adapter = DownloaderAppAdapter(activity, downloaderApps) { app ->
    
    239
    +            onAppSelected(app)
    
    240
    +            dismiss()
    
    241
    +        }
    
    242
    +
    
    243
    +        dialog.closeButton.setOnClickListener {
    
    244
    +            negativeButtonAction()
    
    245
    +            dismiss()
    
    246
    +        }
    
    247
    +    }
    
    248
    +}

  • app/src/main/res/drawable/download_dialog_download_button_background.xml
    1
    +<?xml version="1.0" encoding="utf-8"?>
    
    2
    +<!-- This Source Code Form is subject to the terms of the Mozilla Public
    
    3
    +   - License, v. 2.0. If a copy of the MPL was not distributed with this
    
    4
    +   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
    
    5
    +<shape xmlns:android="http://schemas.android.com/apk/res/android"
    
    6
    +    android:shape="rectangle">
    
    7
    +    <corners android:radius="@dimen/bottom_sheet_corner_radius"/>
    
    8
    +    <solid android:color="?attr/accent" />
    
    9
    +</shape>

  • app/src/main/res/layout/dialog_scrim.xml
    1
    +<?xml version="1.0" encoding="utf-8"?>
    
    2
    +<!-- This Source Code Form is subject to the terms of the Mozilla Public
    
    3
    +   - License, v. 2.0. If a copy of the MPL was not distributed with this
    
    4
    +   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
    
    5
    +
    
    6
    +<FrameLayout
    
    7
    +    xmlns:android="http://schemas.android.com/apk/res/android"
    
    8
    +    android:id="@+id/scrim"
    
    9
    +    android:layout_width="match_parent"
    
    10
    +    android:layout_height="match_parent"
    
    11
    +    android:background="@color/material_scrim_color"
    
    12
    +    android:clipToPadding="false"
    
    13
    +    android:fitsSystemWindows="true"
    
    14
    +    android:importantForAccessibility="no"
    
    15
    +    android:soundEffectsEnabled="false" />

  • app/src/main/res/layout/fragment_browser.xml
    ... ... @@ -66,6 +66,20 @@
    66 66
                     android:layout_height="match_parent"
    
    67 67
                     android:visibility="gone" />
    
    68 68
     
    
    69
    +            <FrameLayout
    
    70
    +                android:id="@+id/startDownloadDialogContainer"
    
    71
    +                android:layout_width="match_parent"
    
    72
    +                android:layout_height="wrap_content"
    
    73
    +                android:layout_gravity="bottom"
    
    74
    +                android:visibility="gone"
    
    75
    +                android:elevation="@dimen/browser_fragment_toolbar_elevation"/>
    
    76
    +
    
    77
    +            <FrameLayout
    
    78
    +                android:id="@+id/dynamicSnackbarContainer"
    
    79
    +                android:layout_width="match_parent"
    
    80
    +                android:layout_height="wrap_content"
    
    81
    +                android:elevation="@dimen/browser_fragment_toolbar_elevation"/>
    
    82
    +
    
    69 83
             </androidx.coordinatorlayout.widget.CoordinatorLayout>
    
    70 84
     
    
    71 85
             <mozilla.components.feature.prompts.creditcard.CreditCardSelectBar
    

  • app/src/main/res/layout/start_download_dialog_layout.xml
    1
    +<?xml version="1.0" encoding="utf-8"?>
    
    2
    +<!-- This Source Code Form is subject to the terms of the Mozilla Public
    
    3
    +   - License, v. 2.0. If a copy of the MPL was not distributed with this
    
    4
    +   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
    
    5
    +
    
    6
    +<RelativeLayout
    
    7
    +    xmlns:android="http://schemas.android.com/apk/res/android"
    
    8
    +    xmlns:app="http://schemas.android.com/apk/res-auto"
    
    9
    +    xmlns:tools="http://schemas.android.com/tools"
    
    10
    +    android:id="@+id/dialogLayout"
    
    11
    +    android:layout_width="match_parent"
    
    12
    +    android:layout_height="wrap_content"
    
    13
    +    android:background="?android:windowBackground"
    
    14
    +    android:orientation="vertical"
    
    15
    +    app:layout_constraintBottom_toBottomOf="parent">
    
    16
    +
    
    17
    +    <androidx.appcompat.widget.AppCompatImageView
    
    18
    +        android:id="@+id/icon"
    
    19
    +        android:layout_width="32dp"
    
    20
    +        android:layout_height="32dp"
    
    21
    +        android:layout_alignParentTop="true"
    
    22
    +        android:layout_marginStart="16dp"
    
    23
    +        android:layout_marginTop="16dp"
    
    24
    +        android:importantForAccessibility="no"
    
    25
    +        android:scaleType="center"
    
    26
    +        app:srcCompat="@drawable/mozac_feature_download_ic_download"
    
    27
    +        app:tint="?android:attr/textColorPrimary" />
    
    28
    +
    
    29
    +    <TextView
    
    30
    +        android:id="@+id/title"
    
    31
    +        android:layout_width="wrap_content"
    
    32
    +        android:layout_height="wrap_content"
    
    33
    +        android:layout_alignBaseline="@id/icon"
    
    34
    +        android:layout_alignParentTop="true"
    
    35
    +        android:layout_marginStart="3dp"
    
    36
    +        android:layout_marginTop="16dp"
    
    37
    +        android:layout_marginEnd="11dp"
    
    38
    +        android:layout_toStartOf="@id/close_button"
    
    39
    +        android:layout_toEndOf="@id/icon"
    
    40
    +        android:paddingStart="5dp"
    
    41
    +        android:paddingTop="4dp"
    
    42
    +        android:paddingEnd="5dp"
    
    43
    +        android:text="@string/mozac_feature_downloads_dialog_download"
    
    44
    +        android:textColor="?android:attr/textColorPrimary"
    
    45
    +        tools:text="Download (85.7 MB)"
    
    46
    +        tools:textColor="#000000" />
    
    47
    +
    
    48
    +    <androidx.appcompat.widget.AppCompatImageButton
    
    49
    +        android:id="@+id/close_button"
    
    50
    +        android:layout_width="48dp"
    
    51
    +        android:layout_height="48dp"
    
    52
    +        android:layout_alignBaseline="@id/icon"
    
    53
    +        android:layout_alignParentTop="true"
    
    54
    +        android:layout_alignParentEnd="true"
    
    55
    +        android:layout_marginStart="3dp"
    
    56
    +        android:scaleType="centerInside"
    
    57
    +        android:background="@null"
    
    58
    +        android:contentDescription="@string/mozac_feature_downloads_button_close"
    
    59
    +        app:srcCompat="@drawable/mozac_ic_close"
    
    60
    +        app:tint="?android:attr/textColorPrimary"
    
    61
    +        tools:textColor="#000000" />
    
    62
    +
    
    63
    +    <TextView
    
    64
    +        android:id="@+id/filename"
    
    65
    +        android:layout_width="wrap_content"
    
    66
    +        android:layout_height="wrap_content"
    
    67
    +        android:layout_below="@id/title"
    
    68
    +        android:layout_alignBaseline="@id/icon"
    
    69
    +        android:layout_marginStart="3dp"
    
    70
    +        android:layout_marginTop="16dp"
    
    71
    +        android:layout_toEndOf="@id/icon"
    
    72
    +        android:paddingStart="5dp"
    
    73
    +        android:paddingTop="4dp"
    
    74
    +        android:paddingEnd="5dp"
    
    75
    +        android:textColor="?android:attr/textColorPrimary"
    
    76
    +        tools:text="@tools:sample/lorem/random"
    
    77
    +        tools:textColor="#000000" />
    
    78
    +
    
    79
    +    <Button
    
    80
    +        android:id="@+id/download_button"
    
    81
    +        android:layout_width="wrap_content"
    
    82
    +        android:layout_height="wrap_content"
    
    83
    +        android:layout_below="@id/filename"
    
    84
    +        android:layout_alignParentEnd="true"
    
    85
    +        android:layout_marginStart="8dp"
    
    86
    +        android:layout_marginTop="16dp"
    
    87
    +        android:layout_marginEnd="16dp"
    
    88
    +        android:layout_marginBottom="16dp"
    
    89
    +        android:paddingStart="8dp"
    
    90
    +        android:paddingEnd="8dp"
    
    91
    +        android:text="@string/mozac_feature_downloads_dialog_download"
    
    92
    +        android:background="@drawable/download_dialog_download_button_background"
    
    93
    +        android:textColor="?attr/textOnColorPrimary"
    
    94
    +        android:textAllCaps="false"
    
    95
    +        tools:ignore="ButtonStyleXmlDetector" />
    
    96
    +</RelativeLayout>

  • app/src/main/res/values/colors.xml
    ... ... @@ -337,4 +337,7 @@
    337 337
     
    
    338 338
         <!-- App Spinners colors -->
    
    339 339
         <color name="spinner_selected_item">#1415141A</color>
    
    340
    +
    
    341
    +    <!-- Material Design colors -->
    
    342
    +    <color name="material_scrim_color">#52000000</color>
    
    340 343
     </resources>

  • app/src/main/res/values/dimens.xml
    ... ... @@ -82,6 +82,8 @@
    82 82
         <!--The size of the gap between the tab preview and content layout.-->
    
    83 83
         <dimen name="browser_fragment_gesture_preview_offset">48dp</dimen>
    
    84 84
         <dimen name="browser_fragment_toolbar_elevation">16dp</dimen>
    
    85
    +    <!-- The download dialogs are shown above the toolbar so they need a bigger elevation. -->
    
    86
    +    <dimen name="browser_fragment_download_dialog_elevation">17dp</dimen>
    
    85 87
     
    
    86 88
         <!-- Search Fragment -->
    
    87 89
         <dimen name="search_fragment_clipboard_item_height">56dp</dimen>
    

  • app/src/test/java/org/mozilla/fenix/components/FenixSnackbarBehaviorTest.kt
    1
    +/* This Source Code Form is subject to the terms of the Mozilla Public
    
    2
    + * License, v. 2.0. If a copy of the MPL was not distributed with this
    
    3
    + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
    
    4
    +
    
    5
    +package org.mozilla.fenix.components
    
    6
    +
    
    7
    +import android.view.Gravity
    
    8
    +import android.view.View
    
    9
    +import android.view.ViewGroup
    
    10
    +import android.widget.FrameLayout
    
    11
    +import androidx.coordinatorlayout.widget.CoordinatorLayout
    
    12
    +import mozilla.components.support.test.robolectric.testContext
    
    13
    +import org.junit.Assert.assertEquals
    
    14
    +import org.junit.Before
    
    15
    +import org.junit.Test
    
    16
    +import org.junit.runner.RunWith
    
    17
    +import org.mozilla.fenix.R
    
    18
    +import org.mozilla.fenix.components.toolbar.ToolbarPosition
    
    19
    +import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
    
    20
    +
    
    21
    +@RunWith(FenixRobolectricTestRunner::class)
    
    22
    +class FenixSnackbarBehaviorTest {
    
    23
    +    private val snackbarParams = CoordinatorLayout.LayoutParams(0, 0)
    
    24
    +    private val snackbarContainer = FrameLayout(testContext)
    
    25
    +    private val dependency = View(testContext)
    
    26
    +    private val parent = CoordinatorLayout(testContext)
    
    27
    +
    
    28
    +    @Before
    
    29
    +    fun setup() {
    
    30
    +        snackbarContainer.layoutParams = snackbarParams
    
    31
    +        parent.addView(dependency)
    
    32
    +    }
    
    33
    +
    
    34
    +    @Test
    
    35
    +    fun `GIVEN no valid anchors are shown WHEN the snackbar is shown THEN don't anchor it`() {
    
    36
    +        val behavior = FenixSnackbarBehavior<ViewGroup>(testContext, ToolbarPosition.BOTTOM)
    
    37
    +
    
    38
    +        behavior.layoutDependsOn(parent, snackbarContainer, dependency)
    
    39
    +
    
    40
    +        assertSnackbarIsPlacedAtTheBottomOfTheScreen()
    
    41
    +    }
    
    42
    +
    
    43
    +    @Test
    
    44
    +    fun `GIVEN the dynamic download dialog is shown WHEN the snackbar is shown THEN place the snackbar above the dialog`() {
    
    45
    +        dependency.id = R.id.viewDynamicDownloadDialog
    
    46
    +        val behavior = FenixSnackbarBehavior<ViewGroup>(testContext, ToolbarPosition.BOTTOM)
    
    47
    +
    
    48
    +        behavior.layoutDependsOn(parent, snackbarContainer, dependency)
    
    49
    +
    
    50
    +        assertSnackbarPlacementAboveAnchor()
    
    51
    +    }
    
    52
    +
    
    53
    +    @Test
    
    54
    +    fun `GIVEN a bottom toolbar is shown WHEN the snackbar is shown THEN place the snackbar above the toolbar`() {
    
    55
    +        dependency.id = R.id.toolbar
    
    56
    +        val behavior = FenixSnackbarBehavior<ViewGroup>(testContext, ToolbarPosition.BOTTOM)
    
    57
    +
    
    58
    +        behavior.layoutDependsOn(parent, snackbarContainer, dependency)
    
    59
    +
    
    60
    +        assertSnackbarPlacementAboveAnchor()
    
    61
    +    }
    
    62
    +
    
    63
    +    @Test
    
    64
    +    fun `GIVEN a toolbar and a dynamic download dialog are shown WHEN the snackbar is shown THEN place the snackbar above the dialog`() {
    
    65
    +        listOf(R.id.viewDynamicDownloadDialog, R.id.toolbar).forEach {
    
    66
    +            parent.addView(View(testContext).apply { id = it })
    
    67
    +        }
    
    68
    +        val behavior = FenixSnackbarBehavior<ViewGroup>(testContext, ToolbarPosition.BOTTOM)
    
    69
    +
    
    70
    +        behavior.layoutDependsOn(parent, snackbarContainer, dependency)
    
    71
    +
    
    72
    +        assertSnackbarPlacementAboveAnchor(parent.findViewById(R.id.viewDynamicDownloadDialog))
    
    73
    +    }
    
    74
    +
    
    75
    +    @Test
    
    76
    +    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`() {
    
    77
    +        listOf(R.id.viewDynamicDownloadDialog, R.id.toolbar, R.id.startDownloadDialogContainer).forEach {
    
    78
    +            parent.addView(View(testContext).apply { id = it })
    
    79
    +        }
    
    80
    +        val behavior = FenixSnackbarBehavior<ViewGroup>(testContext, ToolbarPosition.BOTTOM)
    
    81
    +
    
    82
    +        behavior.layoutDependsOn(parent, snackbarContainer, dependency)
    
    83
    +
    
    84
    +        assertSnackbarPlacementAboveAnchor(parent.findViewById(R.id.startDownloadDialogContainer))
    
    85
    +    }
    
    86
    +
    
    87
    +    @Test
    
    88
    +    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`() {
    
    89
    +        val dialog = View(testContext)
    
    90
    +            .apply { id = R.id.viewDynamicDownloadDialog }
    
    91
    +            .also { parent.addView(it) }
    
    92
    +        val toolbar = View(testContext)
    
    93
    +            .apply { id = R.id.toolbar }
    
    94
    +            .also { parent.addView(it) }
    
    95
    +        val behavior = FenixSnackbarBehavior<ViewGroup>(testContext, ToolbarPosition.BOTTOM)
    
    96
    +
    
    97
    +        // Test the scenario where the dialog is invisible.
    
    98
    +        behavior.layoutDependsOn(parent, snackbarContainer, dependency)
    
    99
    +        assertSnackbarPlacementAboveAnchor(dialog)
    
    100
    +        dialog.visibility = View.GONE
    
    101
    +        behavior.layoutDependsOn(parent, snackbarContainer, dependency)
    
    102
    +        assertSnackbarPlacementAboveAnchor(toolbar)
    
    103
    +
    
    104
    +        // Test the scenario where the dialog is removed from parent.
    
    105
    +        dialog.visibility = View.VISIBLE
    
    106
    +        behavior.layoutDependsOn(parent, snackbarContainer, dependency)
    
    107
    +        assertSnackbarPlacementAboveAnchor(dialog)
    
    108
    +        parent.removeView(dialog)
    
    109
    +        behavior.layoutDependsOn(parent, snackbarContainer, dependency)
    
    110
    +        assertSnackbarPlacementAboveAnchor(toolbar)
    
    111
    +    }
    
    112
    +
    
    113
    +    @Test
    
    114
    +    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`() {
    
    115
    +        val dialog = View(testContext)
    
    116
    +            .apply { id = R.id.startDownloadDialogContainer }
    
    117
    +            .also { parent.addView(it) }
    
    118
    +        val dynamicDialog = View(testContext)
    
    119
    +            .apply { id = R.id.viewDynamicDownloadDialog }
    
    120
    +            .also { parent.addView(it) }
    
    121
    +        val behavior = FenixSnackbarBehavior<ViewGroup>(testContext, ToolbarPosition.BOTTOM)
    
    122
    +
    
    123
    +        // Test the scenario where the dialog is invisible.
    
    124
    +        behavior.layoutDependsOn(parent, snackbarContainer, dependency)
    
    125
    +        assertSnackbarPlacementAboveAnchor(dialog)
    
    126
    +        dialog.visibility = View.GONE
    
    127
    +        behavior.layoutDependsOn(parent, snackbarContainer, dependency)
    
    128
    +        assertSnackbarPlacementAboveAnchor(dynamicDialog)
    
    129
    +
    
    130
    +        // Test the scenario where the dialog is removed from parent.
    
    131
    +        dialog.visibility = View.VISIBLE
    
    132
    +        behavior.layoutDependsOn(parent, snackbarContainer, dependency)
    
    133
    +        assertSnackbarPlacementAboveAnchor(dialog)
    
    134
    +        parent.removeView(dialog)
    
    135
    +        behavior.layoutDependsOn(parent, snackbarContainer, dependency)
    
    136
    +        assertSnackbarPlacementAboveAnchor(dynamicDialog)
    
    137
    +    }
    
    138
    +
    
    139
    +    @Test
    
    140
    +    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`() {
    
    141
    +        val dialog = View(testContext)
    
    142
    +            .apply { id = R.id.startDownloadDialogContainer }
    
    143
    +            .also { parent.addView(it) }
    
    144
    +        val toolbar = View(testContext)
    
    145
    +            .apply { id = R.id.toolbar }
    
    146
    +            .also { parent.addView(it) }
    
    147
    +        val behavior = FenixSnackbarBehavior<ViewGroup>(testContext, ToolbarPosition.BOTTOM)
    
    148
    +
    
    149
    +        // Test the scenario where the dialog is invisible.
    
    150
    +        behavior.layoutDependsOn(parent, snackbarContainer, dependency)
    
    151
    +        assertSnackbarPlacementAboveAnchor(dialog)
    
    152
    +        dialog.visibility = View.GONE
    
    153
    +        behavior.layoutDependsOn(parent, snackbarContainer, dependency)
    
    154
    +        assertSnackbarPlacementAboveAnchor(toolbar)
    
    155
    +
    
    156
    +        // Test the scenario where the dialog is removed from parent.
    
    157
    +        dialog.visibility = View.VISIBLE
    
    158
    +        behavior.layoutDependsOn(parent, snackbarContainer, dependency)
    
    159
    +        assertSnackbarPlacementAboveAnchor(dialog)
    
    160
    +        parent.removeView(dialog)
    
    161
    +        behavior.layoutDependsOn(parent, snackbarContainer, dependency)
    
    162
    +        assertSnackbarPlacementAboveAnchor(toolbar)
    
    163
    +    }
    
    164
    +
    
    165
    +    @Test
    
    166
    +    fun `GIVEN the snackbar is anchored to the bottom toolbar WHEN the toolbar is not shown anymore THEN place the snackbar at the bottom`() {
    
    167
    +        val toolbar = View(testContext)
    
    168
    +            .apply { id = R.id.toolbar }
    
    169
    +            .also { parent.addView(it) }
    
    170
    +        val behavior = FenixSnackbarBehavior<ViewGroup>(testContext, ToolbarPosition.BOTTOM)
    
    171
    +
    
    172
    +        // Test the scenario where the toolbar is invisible.
    
    173
    +        behavior.layoutDependsOn(parent, snackbarContainer, dependency)
    
    174
    +        assertSnackbarPlacementAboveAnchor(toolbar)
    
    175
    +        toolbar.visibility = View.GONE
    
    176
    +        behavior.layoutDependsOn(parent, snackbarContainer, dependency)
    
    177
    +        assertSnackbarIsPlacedAtTheBottomOfTheScreen()
    
    178
    +
    
    179
    +        // Test the scenario where the toolbar is removed from parent.
    
    180
    +        toolbar.visibility = View.VISIBLE
    
    181
    +        behavior.layoutDependsOn(parent, snackbarContainer, dependency)
    
    182
    +        assertSnackbarPlacementAboveAnchor(toolbar)
    
    183
    +        parent.removeView(toolbar)
    
    184
    +        behavior.layoutDependsOn(parent, snackbarContainer, dependency)
    
    185
    +        assertSnackbarIsPlacedAtTheBottomOfTheScreen()
    
    186
    +    }
    
    187
    +
    
    188
    +    @Test
    
    189
    +    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`() {
    
    190
    +        val dialog = View(testContext)
    
    191
    +            .apply { id = R.id.viewDynamicDownloadDialog }
    
    192
    +            .also { parent.addView(it) }
    
    193
    +        View(testContext)
    
    194
    +            .apply { id = R.id.toolbar }
    
    195
    +            .also { parent.addView(it) }
    
    196
    +        val behavior = FenixSnackbarBehavior<ViewGroup>(testContext, ToolbarPosition.TOP)
    
    197
    +
    
    198
    +        // Test the scenario where the dialog is invisible.
    
    199
    +        behavior.layoutDependsOn(parent, snackbarContainer, dependency)
    
    200
    +        assertSnackbarPlacementAboveAnchor(dialog)
    
    201
    +        dialog.visibility = View.GONE
    
    202
    +        behavior.layoutDependsOn(parent, snackbarContainer, dependency)
    
    203
    +        assertSnackbarIsPlacedAtTheBottomOfTheScreen()
    
    204
    +
    
    205
    +        // Test the scenario where the dialog is removed from parent.
    
    206
    +        dialog.visibility = View.VISIBLE
    
    207
    +        behavior.layoutDependsOn(parent, snackbarContainer, dependency)
    
    208
    +        assertSnackbarPlacementAboveAnchor(dialog)
    
    209
    +        parent.removeView(dialog)
    
    210
    +        behavior.layoutDependsOn(parent, snackbarContainer, dependency)
    
    211
    +        assertSnackbarIsPlacedAtTheBottomOfTheScreen()
    
    212
    +    }
    
    213
    +
    
    214
    +    @Test
    
    215
    +    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`() {
    
    216
    +        val toolbar = View(testContext)
    
    217
    +            .apply { id = R.id.toolbar }
    
    218
    +            .also { parent.addView(it) }
    
    219
    +        val behavior = FenixSnackbarBehavior<ViewGroup>(testContext, ToolbarPosition.TOP)
    
    220
    +
    
    221
    +        // Test the scenario where the toolbar is invisible.
    
    222
    +        behavior.layoutDependsOn(parent, snackbarContainer, dependency)
    
    223
    +        assertSnackbarIsPlacedAtTheBottomOfTheScreen()
    
    224
    +        toolbar.visibility = View.GONE
    
    225
    +        behavior.layoutDependsOn(parent, snackbarContainer, dependency)
    
    226
    +        assertSnackbarIsPlacedAtTheBottomOfTheScreen()
    
    227
    +
    
    228
    +        // Test the scenario where the toolbar is removed from parent.
    
    229
    +        toolbar.visibility = View.VISIBLE
    
    230
    +        behavior.layoutDependsOn(parent, snackbarContainer, dependency)
    
    231
    +        assertSnackbarIsPlacedAtTheBottomOfTheScreen()
    
    232
    +        parent.removeView(toolbar)
    
    233
    +        behavior.layoutDependsOn(parent, snackbarContainer, dependency)
    
    234
    +        assertSnackbarIsPlacedAtTheBottomOfTheScreen()
    
    235
    +    }
    
    236
    +
    
    237
    +    private fun assertSnackbarPlacementAboveAnchor(anchor: View = dependency) {
    
    238
    +        assertEquals(anchor.id, snackbarContainer.params.anchorId)
    
    239
    +        assertEquals(Gravity.TOP or Gravity.CENTER_HORIZONTAL, snackbarContainer.params.anchorGravity)
    
    240
    +        assertEquals(Gravity.TOP or Gravity.CENTER_HORIZONTAL, snackbarContainer.params.gravity)
    
    241
    +    }
    
    242
    +
    
    243
    +    private fun assertSnackbarIsPlacedAtTheBottomOfTheScreen() {
    
    244
    +        assertEquals(View.NO_ID, snackbarContainer.params.anchorId)
    
    245
    +        assertEquals(Gravity.NO_GRAVITY, snackbarContainer.params.anchorGravity)
    
    246
    +        assertEquals(Gravity.BOTTOM or Gravity.CENTER_HORIZONTAL, snackbarContainer.params.gravity)
    
    247
    +    }
    
    248
    +
    
    249
    +    private val FrameLayout.params
    
    250
    +        get() = layoutParams as CoordinatorLayout.LayoutParams
    
    251
    +}

  • app/src/test/java/org/mozilla/fenix/downloads/FirstPartyDownloadDialogTest.kt
    1
    +/* This Source Code Form is subject to the terms of the Mozilla Public
    
    2
    + * License, v. 2.0. If a copy of the MPL was not distributed with this
    
    3
    + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
    
    4
    +
    
    5
    +package org.mozilla.fenix.downloads
    
    6
    +
    
    7
    +import android.app.Activity
    
    8
    +import android.widget.FrameLayout
    
    9
    +import io.mockk.Runs
    
    10
    +import io.mockk.every
    
    11
    +import io.mockk.just
    
    12
    +import io.mockk.spyk
    
    13
    +import io.mockk.verify
    
    14
    +import mozilla.components.feature.downloads.toMegabyteOrKilobyteString
    
    15
    +import mozilla.components.support.test.robolectric.testContext
    
    16
    +import org.junit.Assert.assertEquals
    
    17
    +import org.junit.Assert.assertFalse
    
    18
    +import org.junit.Assert.assertTrue
    
    19
    +import org.junit.Before
    
    20
    +import org.junit.Test
    
    21
    +import org.junit.runner.RunWith
    
    22
    +import org.mozilla.fenix.R
    
    23
    +import org.mozilla.fenix.databinding.StartDownloadDialogLayoutBinding
    
    24
    +import org.mozilla.fenix.ext.settings
    
    25
    +import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
    
    26
    +import org.robolectric.Robolectric
    
    27
    +
    
    28
    +@RunWith(FenixRobolectricTestRunner::class)
    
    29
    +class FirstPartyDownloadDialogTest {
    
    30
    +    private val activity: Activity = Robolectric.buildActivity(Activity::class.java).create().get()
    
    31
    +
    
    32
    +    @Before
    
    33
    +    fun setup() {
    
    34
    +        every { activity.settings().accessibilityServicesEnabled } returns false
    
    35
    +    }
    
    36
    +
    
    37
    +    @Test
    
    38
    +    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`() {
    
    39
    +        var wasPositiveActionDone = false
    
    40
    +        var wasNegativeActionDone = false
    
    41
    +        val contentSize = 5566L
    
    42
    +        val dialog = spyk(
    
    43
    +            FirstPartyDownloadDialog(
    
    44
    +                activity = activity,
    
    45
    +                filename = "Test",
    
    46
    +                contentSize = contentSize,
    
    47
    +                positiveButtonAction = { wasPositiveActionDone = true },
    
    48
    +                negativeButtonAction = { wasNegativeActionDone = true },
    
    49
    +            ),
    
    50
    +        )
    
    51
    +        every { dialog.dismiss() } just Runs
    
    52
    +        val dialogParent = FrameLayout(testContext)
    
    53
    +        dialog.container = dialogParent
    
    54
    +
    
    55
    +        dialog.setupView()
    
    56
    +
    
    57
    +        assertEquals(1, dialogParent.childCount)
    
    58
    +        assertEquals(R.id.dialogLayout, dialogParent.getChildAt(0).id)
    
    59
    +        val dialogBinding = dialog.binding as StartDownloadDialogLayoutBinding
    
    60
    +        assertEquals(
    
    61
    +            testContext.getString(
    
    62
    +                R.string.mozac_feature_downloads_dialog_title2,
    
    63
    +                contentSize.toMegabyteOrKilobyteString(),
    
    64
    +            ),
    
    65
    +            dialogBinding.title.text,
    
    66
    +        )
    
    67
    +        assertEquals("Test", dialogBinding.filename.text)
    
    68
    +        assertFalse(wasPositiveActionDone)
    
    69
    +        assertFalse(wasNegativeActionDone)
    
    70
    +        dialogBinding.downloadButton.callOnClick()
    
    71
    +        verify { dialog.dismiss() }
    
    72
    +        assertTrue(wasPositiveActionDone)
    
    73
    +        dialogBinding.closeButton.callOnClick()
    
    74
    +        verify(exactly = 2) { dialog.dismiss() }
    
    75
    +        assertTrue(wasNegativeActionDone)
    
    76
    +    }
    
    77
    +
    
    78
    +    @Test
    
    79
    +    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`() {
    
    80
    +        var wasPositiveActionDone = false
    
    81
    +        var wasNegativeActionDone = false
    
    82
    +        val contentSize = 0L
    
    83
    +        val dialog = spyk(
    
    84
    +            FirstPartyDownloadDialog(
    
    85
    +                activity = activity,
    
    86
    +                filename = "Test",
    
    87
    +                contentSize = contentSize,
    
    88
    +                positiveButtonAction = { wasPositiveActionDone = true },
    
    89
    +                negativeButtonAction = { wasNegativeActionDone = true },
    
    90
    +            ),
    
    91
    +        )
    
    92
    +        every { dialog.dismiss() } just Runs
    
    93
    +        val dialogParent = FrameLayout(testContext)
    
    94
    +        dialog.container = dialogParent
    
    95
    +
    
    96
    +        dialog.setupView()
    
    97
    +
    
    98
    +        assertEquals(1, dialogParent.childCount)
    
    99
    +        assertEquals(R.id.dialogLayout, dialogParent.getChildAt(0).id)
    
    100
    +        val dialogBinding = dialog.binding as StartDownloadDialogLayoutBinding
    
    101
    +        assertEquals(
    
    102
    +            testContext.getString(R.string.mozac_feature_downloads_dialog_download),
    
    103
    +            dialogBinding.title.text,
    
    104
    +        )
    
    105
    +        assertEquals("Test", dialogBinding.filename.text)
    
    106
    +        assertFalse(wasPositiveActionDone)
    
    107
    +        assertFalse(wasNegativeActionDone)
    
    108
    +        dialogBinding.downloadButton.callOnClick()
    
    109
    +        verify { dialog.dismiss() }
    
    110
    +        assertTrue(wasPositiveActionDone)
    
    111
    +        dialogBinding.closeButton.callOnClick()
    
    112
    +        verify(exactly = 2) { dialog.dismiss() }
    
    113
    +        assertTrue(wasNegativeActionDone)
    
    114
    +    }
    
    115
    +}

  • app/src/test/java/org/mozilla/fenix/downloads/StartDownloadDialogTest.kt
    1
    +/* This Source Code Form is subject to the terms of the Mozilla Public
    
    2
    + * License, v. 2.0. If a copy of the MPL was not distributed with this
    
    3
    + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
    
    4
    +
    
    5
    +package org.mozilla.fenix.downloads
    
    6
    +
    
    7
    +import android.app.Activity
    
    8
    +import android.content.Context
    
    9
    +import android.graphics.Color
    
    10
    +import android.view.Gravity
    
    11
    +import android.view.LayoutInflater
    
    12
    +import android.view.View
    
    13
    +import android.widget.FrameLayout
    
    14
    +import androidx.coordinatorlayout.widget.CoordinatorLayout
    
    15
    +import androidx.core.content.ContextCompat
    
    16
    +import androidx.core.view.children
    
    17
    +import androidx.core.view.isVisible
    
    18
    +import io.mockk.every
    
    19
    +import io.mockk.mockk
    
    20
    +import io.mockk.mockkStatic
    
    21
    +import io.mockk.verify
    
    22
    +import mozilla.components.support.ktx.android.view.setNavigationBarTheme
    
    23
    +import mozilla.components.support.ktx.android.view.setStatusBarTheme
    
    24
    +import mozilla.components.support.test.robolectric.testContext
    
    25
    +import org.junit.Assert.assertEquals
    
    26
    +import org.junit.Assert.assertFalse
    
    27
    +import org.junit.Assert.assertNull
    
    28
    +import org.junit.Assert.assertTrue
    
    29
    +import org.junit.Test
    
    30
    +import org.junit.runner.RunWith
    
    31
    +import org.mozilla.fenix.R
    
    32
    +import org.mozilla.fenix.databinding.StartDownloadDialogLayoutBinding
    
    33
    +import org.mozilla.fenix.ext.settings
    
    34
    +import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
    
    35
    +import org.mozilla.fenix.utils.Settings
    
    36
    +import org.robolectric.Robolectric
    
    37
    +
    
    38
    +@RunWith(FenixRobolectricTestRunner::class)
    
    39
    +class StartDownloadDialogTest {
    
    40
    +    @Test
    
    41
    +    fun `WHEN the dialog is instantiated THEN cache the navigation and status bar colors`() {
    
    42
    +        val navigationBarColor = Color.RED
    
    43
    +        val statusBarColor = Color.BLUE
    
    44
    +        val activity: Activity = mockk {
    
    45
    +            every { window.navigationBarColor } returns navigationBarColor
    
    46
    +            every { window.statusBarColor } returns statusBarColor
    
    47
    +        }
    
    48
    +        val dialog = TestDownloadDialog(activity)
    
    49
    +
    
    50
    +        assertEquals(navigationBarColor, dialog.initialNavigationBarColor)
    
    51
    +        assertEquals(statusBarColor, dialog.initialStatusBarColor)
    
    52
    +    }
    
    53
    +
    
    54
    +    @Test
    
    55
    +    fun `WHEN the view is to be shown THEN set the scrim and other window customization bind the download values`() {
    
    56
    +        val activity = Robolectric.buildActivity(Activity::class.java).create().get()
    
    57
    +        val dialogParent = FrameLayout(testContext)
    
    58
    +        val dialogContainer = FrameLayout(testContext).also {
    
    59
    +            dialogParent.addView(it)
    
    60
    +            it.layoutParams = CoordinatorLayout.LayoutParams(0, 0)
    
    61
    +        }
    
    62
    +        val dialog = TestDownloadDialog(activity)
    
    63
    +
    
    64
    +        mockkStatic("mozilla.components.support.ktx.android.view.WindowKt", "org.mozilla.fenix.ext.ContextKt") {
    
    65
    +            every { any<Context>().settings() } returns mockk(relaxed = true)
    
    66
    +            val fluentDialog = dialog.show(dialogContainer)
    
    67
    +
    
    68
    +            val scrim = dialogParent.children.first { it.id == R.id.scrim }
    
    69
    +            assertTrue(scrim.hasOnClickListeners())
    
    70
    +            assertFalse(scrim.isSoundEffectsEnabled)
    
    71
    +            assertTrue(dialog.wasDownloadDataBinded)
    
    72
    +            assertEquals(
    
    73
    +                Gravity.BOTTOM or Gravity.CENTER_HORIZONTAL,
    
    74
    +                (dialogContainer.layoutParams as CoordinatorLayout.LayoutParams).gravity,
    
    75
    +            )
    
    76
    +            assertEquals(
    
    77
    +                testContext.resources.getDimension(R.dimen.browser_fragment_download_dialog_elevation),
    
    78
    +                dialogContainer.elevation,
    
    79
    +            )
    
    80
    +            assertTrue(dialogContainer.isVisible)
    
    81
    +            verify {
    
    82
    +                activity.window.setNavigationBarTheme(ContextCompat.getColor(activity, R.color.material_scrim_color))
    
    83
    +                activity.window.setStatusBarTheme(ContextCompat.getColor(activity, R.color.material_scrim_color))
    
    84
    +            }
    
    85
    +            assertEquals(dialog, fluentDialog)
    
    86
    +        }
    
    87
    +    }
    
    88
    +
    
    89
    +    @Test
    
    90
    +    fun `GIVEN a dismiss callback WHEN the dialog is dismissed THEN the callback is informed`() {
    
    91
    +        var wasDismissCalled = false
    
    92
    +        val dialog = TestDownloadDialog(mockk(relaxed = true))
    
    93
    +
    
    94
    +        val fluentDialog = dialog.onDismiss { wasDismissCalled = true }
    
    95
    +        dialog.onDismiss()
    
    96
    +
    
    97
    +        assertTrue(wasDismissCalled)
    
    98
    +        assertEquals(dialog, fluentDialog)
    
    99
    +    }
    
    100
    +
    
    101
    +    @Test
    
    102
    +    fun `GIVEN the download dialog is shown WHEN dismissed THEN remove the scrim, the dialog and any window customizations`() {
    
    103
    +        val activity = Robolectric.buildActivity(Activity::class.java).create().get()
    
    104
    +        val dialogParent = FrameLayout(testContext)
    
    105
    +        val dialogContainer = FrameLayout(testContext).also {
    
    106
    +            dialogParent.addView(it)
    
    107
    +            it.layoutParams = CoordinatorLayout.LayoutParams(0, 0)
    
    108
    +        }
    
    109
    +        val dialog = TestDownloadDialog(activity)
    
    110
    +        mockkStatic("mozilla.components.support.ktx.android.view.WindowKt", "org.mozilla.fenix.ext.ContextKt") {
    
    111
    +            every { any<Context>().settings() } returns mockk(relaxed = true)
    
    112
    +            dialog.show(dialogContainer)
    
    113
    +            dialog.binding = StartDownloadDialogLayoutBinding
    
    114
    +                .inflate(LayoutInflater.from(activity), dialogContainer, true)
    
    115
    +
    
    116
    +            dialog.dismiss()
    
    117
    +
    
    118
    +            assertNull(dialogParent.children.firstOrNull { it.id == R.id.scrim })
    
    119
    +            assertTrue(dialogParent.childCount == 1)
    
    120
    +            assertTrue(dialogContainer.childCount == 0)
    
    121
    +            assertFalse(dialogContainer.isVisible)
    
    122
    +            verify {
    
    123
    +                activity.window.setNavigationBarTheme(dialog.initialNavigationBarColor)
    
    124
    +                activity.window.setStatusBarTheme(dialog.initialStatusBarColor)
    
    125
    +            }
    
    126
    +        }
    
    127
    +    }
    
    128
    +
    
    129
    +    @Test
    
    130
    +    fun `GIVEN a ViewGroup WHEN enabling accessibility THEN enable it for all children but the dialog container`() {
    
    131
    +        val activity: Activity = mockk(relaxed = true)
    
    132
    +        val dialogParent = FrameLayout(testContext)
    
    133
    +        FrameLayout(testContext).also {
    
    134
    +            dialogParent.addView(it)
    
    135
    +            it.id = R.id.startDownloadDialogContainer
    
    136
    +            it.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO
    
    137
    +        }
    
    138
    +        val otherView = View(testContext).also {
    
    139
    +            dialogParent.addView(it)
    
    140
    +            it.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO
    
    141
    +        }
    
    142
    +        val dialog = TestDownloadDialog(activity)
    
    143
    +
    
    144
    +        dialog.enableSiblingsAccessibility(dialogParent)
    
    145
    +
    
    146
    +        assertEquals(listOf(otherView), dialogParent.children.filter { it.isImportantForAccessibility }.toList())
    
    147
    +    }
    
    148
    +
    
    149
    +    @Test
    
    150
    +    fun `GIVEN a ViewGroup WHEN disabling accessibility THEN disable it for all children but the dialog container`() {
    
    151
    +        val activity: Activity = mockk(relaxed = true)
    
    152
    +        val dialogParent = FrameLayout(testContext)
    
    153
    +        val dialogContainer = FrameLayout(testContext).also {
    
    154
    +            dialogParent.addView(it)
    
    155
    +            it.id = R.id.startDownloadDialogContainer
    
    156
    +            it.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_YES
    
    157
    +        }
    
    158
    +        View(testContext).also {
    
    159
    +            dialogParent.addView(it)
    
    160
    +            it.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_YES
    
    161
    +        }
    
    162
    +        val dialog = TestDownloadDialog(activity)
    
    163
    +
    
    164
    +        dialog.disableSiblingsAccessibility(dialogParent)
    
    165
    +
    
    166
    +        assertEquals(listOf(dialogContainer), dialogParent.children.filter { it.isImportantForAccessibility }.toList())
    
    167
    +    }
    
    168
    +
    
    169
    +    @Test
    
    170
    +    fun `GIVEN accessibility services are enabled WHEN the dialog is shown THEN disable siblings accessibility`() {
    
    171
    +        val activity = Robolectric.buildActivity(Activity::class.java).create().get()
    
    172
    +        val dialogParent = FrameLayout(testContext)
    
    173
    +        val dialogContainer = FrameLayout(testContext).also {
    
    174
    +            dialogParent.addView(it)
    
    175
    +            it.id = R.id.startDownloadDialogContainer
    
    176
    +            it.layoutParams = CoordinatorLayout.LayoutParams(0, 0)
    
    177
    +            it.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_YES
    
    178
    +        }
    
    179
    +        View(testContext).also {
    
    180
    +            dialogParent.addView(it)
    
    181
    +            it.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_YES
    
    182
    +        }
    
    183
    +
    
    184
    +        mockkStatic("org.mozilla.fenix.ext.ContextKt") {
    
    185
    +            val dialog = TestDownloadDialog(activity)
    
    186
    +
    
    187
    +            val settings: Settings = mockk {
    
    188
    +                every { accessibilityServicesEnabled } returns false
    
    189
    +            }
    
    190
    +            every { any<Context>().settings() } returns settings
    
    191
    +            dialog.show(dialogContainer)
    
    192
    +            assertEquals(2, dialogParent.children.count { it.isImportantForAccessibility })
    
    193
    +
    
    194
    +            every { settings.accessibilityServicesEnabled } returns true
    
    195
    +            dialog.show(dialogContainer)
    
    196
    +            assertEquals(listOf(dialogContainer), dialogParent.children.filter { it.isImportantForAccessibility }.toList())
    
    197
    +        }
    
    198
    +    }
    
    199
    +
    
    200
    +    @Test
    
    201
    +    fun `WHEN the dialog is dismissed THEN re-enable siblings accessibility`() {
    
    202
    +        val activity = Robolectric.buildActivity(Activity::class.java).create().get()
    
    203
    +        val dialogParent = FrameLayout(testContext)
    
    204
    +        val dialogContainer = FrameLayout(testContext).also {
    
    205
    +            dialogParent.addView(it)
    
    206
    +            it.id = R.id.startDownloadDialogContainer
    
    207
    +            it.layoutParams = CoordinatorLayout.LayoutParams(0, 0)
    
    208
    +            it.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_YES
    
    209
    +        }
    
    210
    +        val accessibleView = View(testContext).also {
    
    211
    +            dialogParent.addView(it)
    
    212
    +            it.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_YES
    
    213
    +        }
    
    214
    +        mockkStatic("org.mozilla.fenix.ext.ContextKt") {
    
    215
    +            val settings: Settings = mockk {
    
    216
    +                every { accessibilityServicesEnabled } returns true
    
    217
    +            }
    
    218
    +            every { any<Context>().settings() } returns settings
    
    219
    +            val dialog = TestDownloadDialog(activity)
    
    220
    +            dialog.show(dialogContainer)
    
    221
    +            dialog.binding = StartDownloadDialogLayoutBinding
    
    222
    +                .inflate(LayoutInflater.from(activity), dialogContainer, true)
    
    223
    +
    
    224
    +            dialog.dismiss()
    
    225
    +
    
    226
    +            assertEquals(
    
    227
    +                listOf(accessibleView),
    
    228
    +                dialogParent.children.filter { it.isVisible && it.isImportantForAccessibility }.toList(),
    
    229
    +            )
    
    230
    +        }
    
    231
    +    }
    
    232
    +}
    
    233
    +
    
    234
    +private class TestDownloadDialog(
    
    235
    +    activity: Activity,
    
    236
    +) : StartDownloadDialog(activity) {
    
    237
    +    var wasDownloadDataBinded = false
    
    238
    +
    
    239
    +    override fun setupView() {
    
    240
    +        wasDownloadDataBinded = true
    
    241
    +    }
    
    242
    +}

  • app/src/test/java/org/mozilla/fenix/downloads/ThirdPartyDownloadDialogTest.kt
    1
    +/* This Source Code Form is subject to the terms of the Mozilla Public
    
    2
    + * License, v. 2.0. If a copy of the MPL was not distributed with this
    
    3
    + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
    
    4
    +
    
    5
    +package org.mozilla.fenix.downloads
    
    6
    +
    
    7
    +import android.app.Activity
    
    8
    +import android.widget.FrameLayout
    
    9
    +import io.mockk.Runs
    
    10
    +import io.mockk.every
    
    11
    +import io.mockk.just
    
    12
    +import io.mockk.mockk
    
    13
    +import io.mockk.spyk
    
    14
    +import io.mockk.verify
    
    15
    +import mozilla.components.feature.downloads.databinding.MozacDownloaderChooserPromptBinding
    
    16
    +import mozilla.components.support.test.robolectric.testContext
    
    17
    +import org.junit.Assert.assertEquals
    
    18
    +import org.junit.Assert.assertTrue
    
    19
    +import org.junit.Test
    
    20
    +import org.junit.runner.RunWith
    
    21
    +import org.mozilla.fenix.R
    
    22
    +import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
    
    23
    +import org.robolectric.Robolectric
    
    24
    +
    
    25
    +@RunWith(FenixRobolectricTestRunner::class)
    
    26
    +class ThirdPartyDownloadDialogTest {
    
    27
    +    private val activity: Activity = Robolectric.buildActivity(Activity::class.java).create().get()
    
    28
    +
    
    29
    +    @Test
    
    30
    +    fun `GIVEN a list of downloader apps WHEN setting it's View THEN bind all provided download data`() {
    
    31
    +        var wasNegativeActionDone = false
    
    32
    +        val dialog = spyk(
    
    33
    +            ThirdPartyDownloadDialog(
    
    34
    +                activity = activity,
    
    35
    +                downloaderApps = listOf(mockk(), mockk()),
    
    36
    +                onAppSelected = { /* cannot test the viewholder click */ },
    
    37
    +                negativeButtonAction = { wasNegativeActionDone = true },
    
    38
    +            ),
    
    39
    +        )
    
    40
    +        every { dialog.dismiss() } just Runs
    
    41
    +        val dialogParent = FrameLayout(testContext)
    
    42
    +        dialog.container = dialogParent
    
    43
    +
    
    44
    +        dialog.setupView()
    
    45
    +
    
    46
    +        assertEquals(1, dialogParent.childCount)
    
    47
    +        assertEquals(R.id.relativeLayout, dialogParent.getChildAt(0).id)
    
    48
    +        val dialogBinding = dialog.binding as MozacDownloaderChooserPromptBinding
    
    49
    +        assertEquals(2, dialogBinding.appsList.adapter?.itemCount)
    
    50
    +        dialogBinding.closeButton.callOnClick()
    
    51
    +        assertTrue(wasNegativeActionDone)
    
    52
    +        verify { dialog.dismiss() }
    
    53
    +    }
    
    54
    +}

  • app/src/test/java/org/mozilla/fenix/tabstray/ext/FenixSnackbarKtTest.kt
    ... ... @@ -6,16 +6,29 @@ package org.mozilla.fenix.tabstray.ext
    6 6
     
    
    7 7
     import android.content.Context
    
    8 8
     import android.view.View
    
    9
    +import android.widget.FrameLayout
    
    10
    +import androidx.coordinatorlayout.widget.CoordinatorLayout
    
    9 11
     import io.mockk.every
    
    10 12
     import io.mockk.mockk
    
    13
    +import io.mockk.mockkStatic
    
    11 14
     import io.mockk.verifyOrder
    
    15
    +import mozilla.components.support.test.robolectric.testContext
    
    16
    +import org.junit.Assert.assertEquals
    
    17
    +import org.junit.Assert.assertTrue
    
    12 18
     import org.junit.Rule
    
    13 19
     import org.junit.Test
    
    20
    +import org.junit.runner.RunWith
    
    14 21
     import org.mozilla.fenix.R
    
    15 22
     import org.mozilla.fenix.components.FenixSnackbar
    
    23
    +import org.mozilla.fenix.components.FenixSnackbarBehavior
    
    24
    +import org.mozilla.fenix.components.toolbar.ToolbarPosition
    
    25
    +import org.mozilla.fenix.ext.settings
    
    26
    +import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
    
    16 27
     import org.mozilla.fenix.helpers.MockkRetryTestRule
    
    17 28
     import org.mozilla.fenix.tabstray.TabsTrayFragment.Companion.ELEVATION
    
    29
    +import org.mozilla.fenix.utils.Settings
    
    18 30
     
    
    31
    +@RunWith(FenixRobolectricTestRunner::class)
    
    19 32
     class FenixSnackbarKtTest {
    
    20 33
     
    
    21 34
         @get:Rule
    
    ... ... @@ -94,4 +107,24 @@ class FenixSnackbarKtTest {
    94 107
                 snackbar.setAction("test1", any())
    
    95 108
             }
    
    96 109
         }
    
    110
    +
    
    111
    +    @Test
    
    112
    +    fun `GIVEN the snackbar is a child of dynamic container WHEN it is shown THEN enable the dynamic behavior`() {
    
    113
    +        val container = FrameLayout(testContext).apply {
    
    114
    +            id = R.id.dynamicSnackbarContainer
    
    115
    +            layoutParams = CoordinatorLayout.LayoutParams(0, 0)
    
    116
    +        }
    
    117
    +        val settings: Settings = mockk(relaxed = true) {
    
    118
    +            every { toolbarPosition } returns ToolbarPosition.BOTTOM
    
    119
    +        }
    
    120
    +        mockkStatic("org.mozilla.fenix.ext.ContextKt") {
    
    121
    +            every { any<Context>().settings() } returns settings
    
    122
    +
    
    123
    +            FenixSnackbar.make(view = container)
    
    124
    +
    
    125
    +            val behavior = (container.layoutParams as? CoordinatorLayout.LayoutParams)?.behavior
    
    126
    +            assertTrue(behavior is FenixSnackbarBehavior)
    
    127
    +            assertEquals(ToolbarPosition.BOTTOM, (behavior as? FenixSnackbarBehavior)?.toolbarPosition)
    
    128
    +        }
    
    129
    +    }
    
    97 130
     }