Richard Pospesel pushed to branch tor-browser-102.2.1-12.5-1 at The Tor Project / Applications / fenix
Commits:
- 
6c861589
by Mugurell at 2023-03-16T12:20:55+00:00
 - 
3dcc5001
by Mugurell at 2023-03-16T12:20:55+00:00
 - 
f66d9ac0
by Mugurell at 2023-03-16T12:20:55+00:00
 - 
39a422c9
by Mugurell at 2023-03-16T12:20:55+00:00
 - 
10a73978
by Mugurell at 2023-03-16T12:20:55+00:00
 
20 changed files:
- app/src/androidTest/java/org/mozilla/fenix/ui/CollectionTest.kt
 - app/src/androidTest/java/org/mozilla/fenix/ui/DownloadFileTypesTest.kt
 - app/src/androidTest/java/org/mozilla/fenix/ui/DownloadTest.kt
 - app/src/androidTest/java/org/mozilla/fenix/ui/robots/DownloadRobot.kt
 - app/src/main/java/org/mozilla/fenix/browser/BaseBrowserFragment.kt
 - app/src/main/java/org/mozilla/fenix/browser/BrowserFragment.kt
 - app/src/main/java/org/mozilla/fenix/components/FenixSnackbar.kt
 - + app/src/main/java/org/mozilla/fenix/components/FenixSnackbarBehavior.kt
 - + app/src/main/java/org/mozilla/fenix/downloads/StartDownloadDialog.kt
 - + app/src/main/res/drawable/download_dialog_download_button_background.xml
 - + app/src/main/res/layout/dialog_scrim.xml
 - app/src/main/res/layout/fragment_browser.xml
 - + app/src/main/res/layout/start_download_dialog_layout.xml
 - app/src/main/res/values/colors.xml
 - app/src/main/res/values/dimens.xml
 - + app/src/test/java/org/mozilla/fenix/components/FenixSnackbarBehaviorTest.kt
 - + app/src/test/java/org/mozilla/fenix/downloads/FirstPartyDownloadDialogTest.kt
 - + app/src/test/java/org/mozilla/fenix/downloads/StartDownloadDialogTest.kt
 - + app/src/test/java/org/mozilla/fenix/downloads/ThirdPartyDownloadDialogTest.kt
 - app/src/test/java/org/mozilla/fenix/tabstray/ext/FenixSnackbarKtTest.kt
 
Changes:
| ... | ... | @@ -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)
 | 
| ... | ... | @@ -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()
 | 
| ... | ... | @@ -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")
 | 
| ... | ... | @@ -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() =
 | 
| ... | ... | @@ -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))
 | 
| ... | ... | @@ -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 |                  )
 | 
| ... | ... | @@ -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 | 
| 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 | +} | 
| 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 | +} | 
| 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> | 
| 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" /> | 
| ... | ... | @@ -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
 | 
| 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> | 
| ... | ... | @@ -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> | 
| ... | ... | @@ -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>
 | 
| 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 | +} | 
| 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 | +} | 
| 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 | +} | 
| 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 | +} | 
| ... | ... | @@ -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 |  } |