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
-
09278aff
by Mugurell at 2023-03-15T18:49:18+00:00
-
9eb98437
by Mugurell at 2023-03-15T18:49:50+00:00
-
94d239f2
by Mugurell at 2023-03-15T19:03:40+00:00
-
cf3ce1dd
by Mugurell at 2023-03-15T19:06:08+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 | } |