[tor-commits] [Git][tpo/applications/android-components][android-components-102.0.14-12.0-1] 4 commits: Bug 1783561 - Stop BrowserToolbarBehavior positioning the snackbar

Richard Pospesel (@richard) git at gitlab.torproject.org
Thu Mar 16 11:53:33 UTC 2023



Richard Pospesel pushed to branch android-components-102.0.14-12.0-1 at The Tor Project / Applications / android-components


Commits:
8c3c4617 by Mugurell at 2023-03-15T18:01:20+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

- - - - -
e135dc89 by Mugurell at 2023-03-15T18:01:54+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

- - - - -
c5c27b90 by Mugurell at 2023-03-15T18:01:54+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

- - - - -
952ef13c by Mugurell at 2023-03-15T18:01:54+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/cfaa78712cdc065d034e75cbec9d9c102f169934...952ef13ce41cf73958418bc6c2271c13468ed113

-- 
View it on GitLab: https://gitlab.torproject.org/tpo/applications/android-components/-/compare/cfaa78712cdc065d034e75cbec9d9c102f169934...952ef13ce41cf73958418bc6c2271c13468ed113
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/tor-commits/attachments/20230316/0ee339ff/attachment-0001.htm>


More information about the tor-commits mailing list