[Author Prev][Author Next][Thread Prev][Thread Next][Author Index][Thread Index]

[tor-commits] [Git][tpo/applications/fenix][tor-browser-102.2.1-12.0-2] 5 commits: Bug 1812518 - Control the snackbar positioning from Fenix



Title: GitLab

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

Commits:

  • 6b5be770
    by Mugurell at 2023-03-15T18:43:49+00:00
    Bug 1812518 - Control the snackbar positioning from Fenix
    
    Previously Android-Components - BrowserToolbarBehavior would be responsible
    for positioning the snackbar above the toolbar.
    With that responsibility removed we can handle in Fenix positioning the
    snackbar depending on the toolbar and many more cases - like positioning it
    depending on the download dialogs.
    
  • 09278aff
    by Mugurell at 2023-03-15T18:49:18+00:00
    Bug 1812518 - Show the download dialog as an Android View
    
    Tried to mimic the UX of a modal dialog while using Android Views.
    This meant including a scrim that would consume all touches and theming the
    navigation bar and status bar.
    Avoiding a dialog and a separate window will allow the snackbar to see the
    new "dialog" as a sibling in a CoordinatorLayout parent and so be able to
    position itself based on the new "dialog".
    This patch also added "start_download_dialog_layout" from A-C as it leads to
    simpler and less code needed to style the layout - colors / shapes with
    everything happening in XML versus calculating the values then setting them
    programatically.
    
  • 9eb98437
    by Mugurell at 2023-03-15T18:49:50+00:00
    Bug 1812518 - Show the 3rd party download dialog as an Android View
    
    This uses the same direction as the before patch - inflating a new View that
    can then serve as an anchor for the Snackbar.
    Here we could use directly the AC layout as it needed no special customization.
    
  • 94d239f2
    by Mugurell at 2023-03-15T19:03:40+00:00
    Bug 1812518 - Fix UI tests affected by the refactoring.
    
  • cf3ce1dd
    by Mugurell at 2023-03-15T19:06:08+00:00
    Bug 1812518 - Update to latest AndroidComponents
    
    The new version will contain the support for allowing to style download dialogs.
    

20 changed files:

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
     }

  • _______________________________________________
    tor-commits mailing list
    tor-commits@xxxxxxxxxxxxxxxxxxxx
    https://lists.torproject.org/cgi-bin/mailman/listinfo/tor-commits