[tbb-commits] [Git][tpo/applications/android-components][android-components-102.0.14-12.5-1] 4 commits: Bug 1783561 - Stop BrowserToolbarBehavior positioning the snackbar
Richard Pospesel (@richard)
git at gitlab.torproject.org
Thu Mar 16 12:04:29 UTC 2023
Richard Pospesel pushed to branch android-components-102.0.14-12.5-1 at The Tor Project / Applications / android-components
Commits:
52f79946 by Mugurell at 2023-03-16T12:04:07+00:00
Bug 1783561 - Stop BrowserToolbarBehavior positioning the snackbar
The snackbar is placed depending on the toolbar so the relation should be
reversed. The toolbar should not know about and control the snackbar.
There could be other siblings that the snackbar wants to position itself by
and so removing this responsability from BrowserToolbarBehavior will help
better support current and future flows.
Backport of https://github.com/mozilla-mobile/firefox-android/commit/04a464e66a95892d9ac7929974da51116cd1585d
- - - - -
4ec68dfa by Mugurell at 2023-03-16T12:04:07+00:00
Bug 1812518 - Allow a custom View for first party downloads
This will allow clients easily implement their own UI.
Backport of https://github.com/mozilla-mobile/firefox-android/commit/e678610bc10df96b8543c26fb5eb7c1ba26e3e20
- - - - -
9589ff50 by Mugurell at 2023-03-16T12:04:07+00:00
Bug 1783561 - Allow a custom View for 3rd party downloads
This will allow clients easily implement their own UI
Backport of https://github.com/mozilla-mobile/firefox-android/commit/c53dd65718579006e174803acf683ffd44075049
- - - - -
ccafd6d1 by Mugurell at 2023-03-16T12:04:07+00:00
Improve prompts UX
Backport of https://github.com/mozilla-mobile/firefox-android/commit/1dc21a3786506200be124733e654dff8f39b5395
- - - - -
8 changed files:
- components/browser/toolbar/src/main/java/mozilla/components/browser/toolbar/behavior/BrowserToolbarBehavior.kt
- components/browser/toolbar/src/test/java/mozilla/components/browser/toolbar/behavior/BrowserToolbarBehaviorTest.kt
- components/feature/downloads/src/main/java/mozilla/components/feature/downloads/DownloadDialogFragment.kt
- components/feature/downloads/src/main/java/mozilla/components/feature/downloads/DownloadsFeature.kt
- components/feature/downloads/src/main/java/mozilla/components/feature/downloads/ext/DownloadState.kt
- components/feature/downloads/src/main/java/mozilla/components/feature/downloads/ui/DownloaderAppAdapter.kt
- components/feature/prompts/build.gradle
- components/feature/prompts/src/main/java/mozilla/components/feature/prompts/PromptFeature.kt
Changes:
=====================================
components/browser/toolbar/src/main/java/mozilla/components/browser/toolbar/behavior/BrowserToolbarBehavior.kt
=====================================
@@ -6,19 +6,15 @@ package mozilla.components.browser.toolbar.behavior
import android.content.Context
import android.util.AttributeSet
-import android.view.Gravity
import android.view.MotionEvent
import android.view.View
import androidx.annotation.VisibleForTesting
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.view.ViewCompat
-import com.google.android.material.snackbar.Snackbar
import mozilla.components.browser.toolbar.BrowserToolbar
import mozilla.components.concept.engine.EngineView
import mozilla.components.support.ktx.android.view.findViewInHierarchy
-private const val SMALL_ELEVATION_CHANGE = 0.01f
-
/**
* Where the toolbar is placed on the screen.
*/
@@ -35,7 +31,6 @@ enum class ToolbarPosition {
*
* This implementation will:
* - Show/Hide the [BrowserToolbar] automatically when scrolling vertically.
- * - On showing a [Snackbar] position it above the [BrowserToolbar].
* - Snap the [BrowserToolbar] to be hidden or visible when the user stops scrolling.
*/
class BrowserToolbarBehavior(
@@ -128,14 +123,6 @@ class BrowserToolbarBehavior(
return false // allow events to be passed to below listeners
}
- override fun layoutDependsOn(parent: CoordinatorLayout, child: BrowserToolbar, dependency: View): Boolean {
- if (toolbarPosition == ToolbarPosition.BOTTOM && dependency is Snackbar.SnackbarLayout) {
- positionSnackbar(child, dependency)
- }
-
- return super.layoutDependsOn(parent, child, dependency)
- }
-
override fun onLayoutChild(
parent: CoordinatorLayout,
child: BrowserToolbar,
@@ -179,23 +166,6 @@ class BrowserToolbarBehavior(
isScrollEnabled = false
}
- @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
- internal fun positionSnackbar(child: View, snackbarLayout: Snackbar.SnackbarLayout) {
- val params = snackbarLayout.layoutParams as CoordinatorLayout.LayoutParams
-
- // Position the snackbar above the toolbar so that it doesn't overlay the toolbar.
- params.anchorId = child.id
- params.anchorGravity = Gravity.TOP or Gravity.CENTER_HORIZONTAL
- params.gravity = Gravity.TOP or Gravity.CENTER_HORIZONTAL
-
- snackbarLayout.layoutParams = params
-
- // In order to avoid the snackbar casting a shadow on the toolbar we adjust the elevation of the snackbar here.
- // We still place it slightly behind the toolbar so that it will not animate over the toolbar but instead pop
- // out from under the toolbar.
- snackbarLayout.elevation = child.elevation - SMALL_ELEVATION_CHANGE
- }
-
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal fun tryToScrollVertically(distance: Float) {
browserToolbar?.let { toolbar ->
=====================================
components/browser/toolbar/src/test/java/mozilla/components/browser/toolbar/behavior/BrowserToolbarBehaviorTest.kt
=====================================
@@ -6,14 +6,12 @@ package mozilla.components.browser.toolbar.behavior
import android.content.Context
import android.graphics.Bitmap
-import android.view.Gravity
import android.view.MotionEvent.ACTION_DOWN
import android.view.MotionEvent.ACTION_MOVE
import android.widget.FrameLayout
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.view.ViewCompat
import androidx.test.ext.junit.runners.AndroidJUnit4
-import com.google.android.material.snackbar.Snackbar
import mozilla.components.browser.toolbar.BrowserToolbar
import mozilla.components.concept.engine.EngineSession
import mozilla.components.concept.engine.EngineView
@@ -474,29 +472,6 @@ class BrowserToolbarBehaviorTest {
verify(yTranslator).collapseWithAnimation(toolbar)
}
- @Test
- fun `Behavior will position snackbar above toolbar`() {
- val behavior = BrowserToolbarBehavior(testContext, null, ToolbarPosition.BOTTOM)
-
- val toolbar: BrowserToolbar = mock()
- doReturn(4223).`when`(toolbar).id
-
- val layoutParams: CoordinatorLayout.LayoutParams = CoordinatorLayout.LayoutParams(0, 0)
-
- val snackbarLayout: Snackbar.SnackbarLayout = mock()
- doReturn(layoutParams).`when`(snackbarLayout).layoutParams
-
- behavior.layoutDependsOn(
- parent = mock(),
- child = toolbar,
- dependency = snackbarLayout
- )
-
- assertEquals(4223, layoutParams.anchorId)
- assertEquals(Gravity.TOP or Gravity.CENTER_HORIZONTAL, layoutParams.anchorGravity)
- assertEquals(Gravity.TOP or Gravity.CENTER_HORIZONTAL, layoutParams.gravity)
- }
-
@Test
fun `Behavior will forceExpand when scrolling up and !shouldScroll if the touch was handled in the browser`() {
val behavior = spy(BrowserToolbarBehavior(testContext, null, ToolbarPosition.BOTTOM))
=====================================
components/feature/downloads/src/main/java/mozilla/components/feature/downloads/DownloadDialogFragment.kt
=====================================
@@ -10,7 +10,7 @@ import mozilla.components.browser.state.state.content.DownloadState
import mozilla.components.feature.downloads.DownloadDialogFragment.Companion.BYTES_TO_MB_LIMIT
import mozilla.components.feature.downloads.DownloadDialogFragment.Companion.KILOBYTE
import mozilla.components.feature.downloads.DownloadDialogFragment.Companion.MEGABYTE
-import mozilla.components.support.utils.DownloadUtils
+import mozilla.components.feature.downloads.ext.realFilenameOrGuessed
/**
* This is a general representation of a dialog meant to be used in collaboration with [DownloadsFeature]
@@ -34,11 +34,7 @@ abstract class DownloadDialogFragment : AppCompatDialogFragment() {
*/
fun setDownload(download: DownloadState) {
val args = arguments ?: Bundle()
- args.putString(
- KEY_FILE_NAME,
- download.fileName
- ?: DownloadUtils.guessFileName(null, download.destinationDirectory, download.url, download.contentType)
- )
+ args.putString(KEY_FILE_NAME, download.realFilenameOrGuessed)
args.putString(KEY_URL, download.url)
args.putLong(KEY_CONTENT_LENGTH, download.contentLength ?: 0)
arguments = args
=====================================
components/feature/downloads/src/main/java/mozilla/components/feature/downloads/DownloadsFeature.kt
=====================================
@@ -25,6 +25,7 @@ import mozilla.components.browser.state.state.SessionState
import mozilla.components.browser.state.state.content.DownloadState
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.feature.downloads.DownloadDialogFragment.Companion.FRAGMENT_TAG
+import mozilla.components.feature.downloads.ext.realFilenameOrGuessed
import mozilla.components.feature.downloads.manager.DownloadManager
import mozilla.components.feature.downloads.manager.noop
import mozilla.components.feature.downloads.manager.onDownloadStopped
@@ -41,6 +42,43 @@ import mozilla.components.support.ktx.kotlin.isSameOriginAs
import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged
import mozilla.components.support.utils.Browsers
+/**
+ * The name of the file to be downloaded.
+ */
+ at JvmInline
+value class Filename(val value: String)
+
+/**
+ * The size of the file to be downloaded expressed as the number of `bytes`.
+ * The value will be `0` if the size is unknown.
+ */
+ at JvmInline
+value class ContentSize(val value: Long)
+
+/**
+ * The list of all applications that can perform a download, including this application.
+ */
+ at JvmInline
+value class ThirdPartyDownloaderApps(val value: List<DownloaderApp>)
+
+/**
+ * Callback for when the user picked a certain application with which to download the current file.
+ */
+ at JvmInline
+value class ThirdPartyDownloaderAppChosenCallback(val value: (DownloaderApp) -> Unit)
+
+/**
+ * Callback for when the positive button of a download dialog was tapped.
+ */
+ at JvmInline
+value class PositiveActionCallback(val value: () -> Unit)
+
+/**
+ * Callback for when the negative button of a download dialog was tapped.
+ */
+ at JvmInline
+value class NegativeActionCallback(val value: () -> Unit)
+
/**
* Feature implementation to provide download functionality for the selected
* session. The feature will subscribe to the selected session and listen
@@ -60,6 +98,10 @@ import mozilla.components.support.utils.Browsers
* @property promptsStyling styling properties for the dialog.
* @property shouldForwardToThirdParties Indicates if downloads should be forward to third party apps,
* if there are multiple apps a chooser dialog will shown.
+ * @property customFirstPartyDownloadDialog An optional delegate for showing a dialog for a download
+ * that will be processed by the current application.
+ * @property customThirdPartyDownloadDialog An optional delegate for showing a dialog for a download
+ * that can be processed by multiple installed applications including the current one.
*/
@Suppress("LongParameterList", "LargeClass")
class DownloadsFeature(
@@ -73,7 +115,11 @@ class DownloadsFeature(
private val tabId: String? = null,
private val fragmentManager: FragmentManager? = null,
private val promptsStyling: PromptsStyling? = null,
- private val shouldForwardToThirdParties: () -> Boolean = { false }
+ private val shouldForwardToThirdParties: () -> Boolean = { false },
+ private val customFirstPartyDownloadDialog:
+ ((Filename, ContentSize, PositiveActionCallback, NegativeActionCallback) -> Unit)? = null,
+ private val customThirdPartyDownloadDialog:
+ ((ThirdPartyDownloaderApps, ThirdPartyDownloaderAppChosenCallback, NegativeActionCallback) -> Unit)? = null,
) : LifecycleAwareFeature, PermissionsFeature {
var onDownloadStopped: onDownloadStopped
@@ -159,16 +205,45 @@ class DownloadsFeature(
val shouldShowAppDownloaderDialog = shouldForwardToThirdParties() && apps.size > 1
return if (shouldShowAppDownloaderDialog) {
- showAppDownloaderDialog(tab, download, apps)
+ when (customThirdPartyDownloadDialog) {
+ null -> showAppDownloaderDialog(tab, download, apps)
+ else -> customThirdPartyDownloadDialog.invoke(
+ ThirdPartyDownloaderApps(apps),
+ ThirdPartyDownloaderAppChosenCallback {
+ onDownloaderAppSelected(it, tab, download)
+ },
+ NegativeActionCallback {
+ useCases.cancelDownloadRequest.invoke(tab.id, download.id)
+ },
+ )
+ }
+
false
} else {
if (applicationContext.isPermissionGranted(downloadManager.permissions.asIterable())) {
- if (fragmentManager != null && !download.skipConfirmation) {
- showDownloadDialog(tab, download)
- false
- } else {
- useCases.consumeDownload(tab.id, download.id)
- startDownload(download)
+ when {
+ customFirstPartyDownloadDialog != null && !download.skipConfirmation -> {
+ customFirstPartyDownloadDialog.invoke(
+ Filename(download.realFilenameOrGuessed),
+ ContentSize(download.contentLength ?: 0),
+ PositiveActionCallback {
+ startDownload(download)
+ useCases.consumeDownload.invoke(tab.id, download.id)
+ },
+ NegativeActionCallback {
+ useCases.cancelDownloadRequest.invoke(tab.id, download.id)
+ },
+ )
+ false
+ }
+ fragmentManager != null && !download.skipConfirmation -> {
+ showDownloadDialog(tab, download)
+ false
+ }
+ else -> {
+ useCases.consumeDownload(tab.id, download.id)
+ startDownload(download)
+ }
}
} else {
onNeedToRequestPermissions(downloadManager.permissions)
@@ -264,25 +339,7 @@ class DownloadsFeature(
) {
appChooserDialog.setApps(apps)
appChooserDialog.onAppSelected = { app ->
- if (app.packageName == applicationContext.packageName) {
- if (applicationContext.isPermissionGranted(downloadManager.permissions.asIterable())) {
- startDownload(download)
- useCases.consumeDownload(tab.id, download.id)
- } else {
- onNeedToRequestPermissions(downloadManager.permissions)
- }
- } else {
- try {
- applicationContext.startActivity(app.toIntent())
- } catch (error: ActivityNotFoundException) {
- val errorMessage = applicationContext.getString(
- R.string.mozac_feature_downloads_unable_to_open_third_party_app,
- app.name
- )
- Toast.makeText(applicationContext, errorMessage, Toast.LENGTH_SHORT).show()
- }
- useCases.consumeDownload(tab.id, download.id)
- }
+ onDownloaderAppSelected(app, tab, download)
}
appChooserDialog.onDismiss = {
@@ -294,6 +351,29 @@ class DownloadsFeature(
}
}
+ @VisibleForTesting
+ internal fun onDownloaderAppSelected(app: DownloaderApp, tab: SessionState, download: DownloadState) {
+ if (app.packageName == applicationContext.packageName) {
+ if (applicationContext.isPermissionGranted(downloadManager.permissions.asIterable())) {
+ startDownload(download)
+ useCases.consumeDownload(tab.id, download.id)
+ } else {
+ onNeedToRequestPermissions(downloadManager.permissions)
+ }
+ } else {
+ try {
+ applicationContext.startActivity(app.toIntent())
+ } catch (error: ActivityNotFoundException) {
+ val errorMessage = applicationContext.getString(
+ R.string.mozac_feature_downloads_unable_to_open_third_party_app,
+ app.name,
+ )
+ Toast.makeText(applicationContext, errorMessage, Toast.LENGTH_SHORT).show()
+ }
+ useCases.consumeDownload(tab.id, download.id)
+ }
+ }
+
private fun getAppDownloaderDialog() = findPreviousAppDownloaderDialogFragment()
?: DownloadAppChooserDialog.newInstance(
promptsStyling?.gravity,
=====================================
components/feature/downloads/src/main/java/mozilla/components/feature/downloads/ext/DownloadState.kt
=====================================
@@ -47,3 +47,6 @@ internal fun DownloadState.withResponse(headers: Headers, stream: InputStream?):
contentLength = contentLength ?: headers[CONTENT_LENGTH]?.toLongOrNull()
)
}
+
+internal val DownloadState.realFilenameOrGuessed
+ get() = fileName ?: DownloadUtils.guessFileName(null, destinationDirectory, url, contentType)
=====================================
components/feature/downloads/src/main/java/mozilla/components/feature/downloads/ui/DownloaderAppAdapter.kt
=====================================
@@ -16,10 +16,10 @@ import mozilla.components.feature.downloads.R
/**
* An adapter for displaying the applications that can perform downloads.
*/
-internal class DownloaderAppAdapter(
+class DownloaderAppAdapter(
context: Context,
private val apps: List<DownloaderApp>,
- val onAppSelected: ((DownloaderApp) -> Unit)
+ val onAppSelected: ((DownloaderApp) -> Unit),
) : RecyclerView.Adapter<DownloaderAppViewHolder>() {
private val inflater = LayoutInflater.from(context)
@@ -49,11 +49,14 @@ internal class DownloaderAppAdapter(
/**
* View holder for a [DownloaderApp] item.
*/
-internal class DownloaderAppViewHolder(
+class DownloaderAppViewHolder(
itemView: View,
val nameLabel: TextView,
- val iconImage: ImageView
+ val iconImage: ImageView,
) : RecyclerView.ViewHolder(itemView) {
+ /**
+ * Show a certain downloader application in the current View.
+ */
fun bind(app: DownloaderApp, onAppSelected: ((DownloaderApp) -> Unit)) {
itemView.app = app
itemView.setOnClickListener {
=====================================
components/feature/prompts/build.gradle
=====================================
@@ -31,6 +31,7 @@ tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach {
dependencies {
implementation project(':browser-state')
implementation project(':concept-engine')
+ implementation project(':feature-session')
implementation project(':lib-state')
implementation project(':support-ktx')
implementation project(':support-utils')
@@ -46,6 +47,7 @@ dependencies {
testImplementation Dependencies.testing_coroutines
testImplementation Dependencies.testing_robolectric
testImplementation Dependencies.testing_mockito
+ testImplementation project(':feature-session')
testImplementation project(':support-test')
testImplementation project(':support-test-libstate')
=====================================
components/feature/prompts/src/main/java/mozilla/components/feature/prompts/PromptFeature.kt
=====================================
@@ -71,6 +71,8 @@ import mozilla.components.feature.prompts.login.LoginExceptions
import mozilla.components.feature.prompts.login.LoginPicker
import mozilla.components.feature.prompts.share.DefaultShareDelegate
import mozilla.components.feature.prompts.share.ShareDelegate
+import mozilla.components.feature.session.SessionUseCases
+import mozilla.components.feature.session.SessionUseCases.ExitFullScreenUseCase
import mozilla.components.lib.state.ext.flowScoped
import mozilla.components.support.base.feature.ActivityResultHandler
import mozilla.components.support.base.feature.LifecycleAwareFeature
@@ -111,6 +113,7 @@ internal const val FRAGMENT_TAG = "mozac_feature_prompt_dialog"
* @property fragmentManager The [FragmentManager] to be used when displaying
* a dialog (fragment).
* @property shareDelegate Delegate used to display share sheet.
+ * @property exitFullscreenUsecase Usecase allowing to exit browser tabs' fullscreen mode.
* @property loginStorageDelegate Delegate used to access login storage. If null,
* 'save login'prompts will not be shown.
* @property isSaveLoginEnabled A callback invoked when a login prompt is triggered. If false,
@@ -144,6 +147,7 @@ class PromptFeature private constructor(
private var customTabId: String?,
private val fragmentManager: FragmentManager,
private val shareDelegate: ShareDelegate,
+ private val exitFullscreenUsecase: ExitFullScreenUseCase = SessionUseCases(store).exitFullscreen,
override val creditCardValidationDelegate: CreditCardValidationDelegate? = null,
override val loginValidationDelegate: LoginValidationDelegate? = null,
private val isSaveLoginEnabled: () -> Boolean = { false },
@@ -184,6 +188,7 @@ class PromptFeature private constructor(
customTabId: String? = null,
fragmentManager: FragmentManager,
shareDelegate: ShareDelegate = DefaultShareDelegate(),
+ exitFullscreenUsecase: ExitFullScreenUseCase = SessionUseCases(store).exitFullscreen,
creditCardValidationDelegate: CreditCardValidationDelegate? = null,
loginValidationDelegate: LoginValidationDelegate? = null,
isSaveLoginEnabled: () -> Boolean = { false },
@@ -202,6 +207,7 @@ class PromptFeature private constructor(
customTabId = customTabId,
fragmentManager = fragmentManager,
shareDelegate = shareDelegate,
+ exitFullscreenUsecase = exitFullscreenUsecase,
creditCardValidationDelegate = creditCardValidationDelegate,
loginValidationDelegate = loginValidationDelegate,
isSaveLoginEnabled = isSaveLoginEnabled,
@@ -222,6 +228,7 @@ class PromptFeature private constructor(
customTabId: String? = null,
fragmentManager: FragmentManager,
shareDelegate: ShareDelegate = DefaultShareDelegate(),
+ exitFullscreenUsecase: ExitFullScreenUseCase = SessionUseCases(store).exitFullscreen,
creditCardValidationDelegate: CreditCardValidationDelegate? = null,
loginValidationDelegate: LoginValidationDelegate? = null,
isSaveLoginEnabled: () -> Boolean = { false },
@@ -240,6 +247,7 @@ class PromptFeature private constructor(
customTabId = customTabId,
fragmentManager = fragmentManager,
shareDelegate = shareDelegate,
+ exitFullscreenUsecase = exitFullscreenUsecase,
creditCardValidationDelegate = creditCardValidationDelegate,
loginValidationDelegate = loginValidationDelegate,
isSaveLoginEnabled = isSaveLoginEnabled,
@@ -420,6 +428,10 @@ class PromptFeature private constructor(
internal fun onPromptRequested(session: SessionState) {
// Some requests are handle with intents
session.content.promptRequests.lastOrNull()?.let { promptRequest ->
+ store.state.findTabOrCustomTabOrSelectedTab(customTabId)?.let {
+ exitFullscreenUsecase(it.id)
+ }
+
when (promptRequest) {
is File -> filePicker.handleFileRequest(promptRequest)
is Share -> handleShareRequest(promptRequest, session)
View it on GitLab: https://gitlab.torproject.org/tpo/applications/android-components/-/compare/5ac318964f029425439132b92dcc413261847ed4...ccafd6d14b86ed9c5eeae97d0a7b2a5676d33a69
--
View it on GitLab: https://gitlab.torproject.org/tpo/applications/android-components/-/compare/5ac318964f029425439132b92dcc413261847ed4...ccafd6d14b86ed9c5eeae97d0a7b2a5676d33a69
You're receiving this email because of your account on gitlab.torproject.org.
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.torproject.org/pipermail/tbb-commits/attachments/20230316/645a3526/attachment-0001.htm>
More information about the tbb-commits
mailing list