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
 - 
4ec68dfa
by Mugurell at 2023-03-16T12:04:07+00:00
 - 
9589ff50
by Mugurell at 2023-03-16T12:04:07+00:00
 - 
ccafd6d1
by Mugurell at 2023-03-16T12:04:07+00:00
 
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:
| ... | ... | @@ -6,19 +6,15 @@ package mozilla.components.browser.toolbar.behavior | 
| 6 | 6 | |
| 7 | 7 |  import android.content.Context
 | 
| 8 | 8 |  import android.util.AttributeSet
 | 
| 9 | -import android.view.Gravity
 | 
|
| 10 | 9 |  import android.view.MotionEvent
 | 
| 11 | 10 |  import android.view.View
 | 
| 12 | 11 |  import androidx.annotation.VisibleForTesting
 | 
| 13 | 12 |  import androidx.coordinatorlayout.widget.CoordinatorLayout
 | 
| 14 | 13 |  import androidx.core.view.ViewCompat
 | 
| 15 | -import com.google.android.material.snackbar.Snackbar
 | 
|
| 16 | 14 |  import mozilla.components.browser.toolbar.BrowserToolbar
 | 
| 17 | 15 |  import mozilla.components.concept.engine.EngineView
 | 
| 18 | 16 |  import mozilla.components.support.ktx.android.view.findViewInHierarchy
 | 
| 19 | 17 | |
| 20 | -private const val SMALL_ELEVATION_CHANGE = 0.01f
 | 
|
| 21 | -  | 
|
| 22 | 18 |  /**
 | 
| 23 | 19 |   * Where the toolbar is placed on the screen.
 | 
| 24 | 20 |   */
 | 
| ... | ... | @@ -35,7 +31,6 @@ enum class ToolbarPosition { | 
| 35 | 31 |   *
 | 
| 36 | 32 |   * This implementation will:
 | 
| 37 | 33 |   * - Show/Hide the [BrowserToolbar] automatically when scrolling vertically.
 | 
| 38 | - * - On showing a [Snackbar] position it above the [BrowserToolbar].
 | 
|
| 39 | 34 |   * - Snap the [BrowserToolbar] to be hidden or visible when the user stops scrolling.
 | 
| 40 | 35 |   */
 | 
| 41 | 36 |  class BrowserToolbarBehavior(
 | 
| ... | ... | @@ -128,14 +123,6 @@ class BrowserToolbarBehavior( | 
| 128 | 123 |          return false // allow events to be passed to below listeners
 | 
| 129 | 124 |      }
 | 
| 130 | 125 | |
| 131 | -    override fun layoutDependsOn(parent: CoordinatorLayout, child: BrowserToolbar, dependency: View): Boolean {
 | 
|
| 132 | -        if (toolbarPosition == ToolbarPosition.BOTTOM && dependency is Snackbar.SnackbarLayout) {
 | 
|
| 133 | -            positionSnackbar(child, dependency)
 | 
|
| 134 | -        }
 | 
|
| 135 | -  | 
|
| 136 | -        return super.layoutDependsOn(parent, child, dependency)
 | 
|
| 137 | -    }
 | 
|
| 138 | -  | 
|
| 139 | 126 |      override fun onLayoutChild(
 | 
| 140 | 127 |          parent: CoordinatorLayout,
 | 
| 141 | 128 |          child: BrowserToolbar,
 | 
| ... | ... | @@ -179,23 +166,6 @@ class BrowserToolbarBehavior( | 
| 179 | 166 |          isScrollEnabled = false
 | 
| 180 | 167 |      }
 | 
| 181 | 168 | |
| 182 | -    @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
 | 
|
| 183 | -    internal fun positionSnackbar(child: View, snackbarLayout: Snackbar.SnackbarLayout) {
 | 
|
| 184 | -        val params = snackbarLayout.layoutParams as CoordinatorLayout.LayoutParams
 | 
|
| 185 | -  | 
|
| 186 | -        // Position the snackbar above the toolbar so that it doesn't overlay the toolbar.
 | 
|
| 187 | -        params.anchorId = child.id
 | 
|
| 188 | -        params.anchorGravity = Gravity.TOP or Gravity.CENTER_HORIZONTAL
 | 
|
| 189 | -        params.gravity = Gravity.TOP or Gravity.CENTER_HORIZONTAL
 | 
|
| 190 | -  | 
|
| 191 | -        snackbarLayout.layoutParams = params
 | 
|
| 192 | -  | 
|
| 193 | -        // In order to avoid the snackbar casting a shadow on the toolbar we adjust the elevation of the snackbar here.
 | 
|
| 194 | -        // We still place it slightly behind the toolbar so that it will not animate over the toolbar but instead pop
 | 
|
| 195 | -        // out from under the toolbar.
 | 
|
| 196 | -        snackbarLayout.elevation = child.elevation - SMALL_ELEVATION_CHANGE
 | 
|
| 197 | -    }
 | 
|
| 198 | -  | 
|
| 199 | 169 |      @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
 | 
| 200 | 170 |      internal fun tryToScrollVertically(distance: Float) {
 | 
| 201 | 171 |          browserToolbar?.let { toolbar ->
 | 
| ... | ... | @@ -6,14 +6,12 @@ package mozilla.components.browser.toolbar.behavior | 
| 6 | 6 | |
| 7 | 7 |  import android.content.Context
 | 
| 8 | 8 |  import android.graphics.Bitmap
 | 
| 9 | -import android.view.Gravity
 | 
|
| 10 | 9 |  import android.view.MotionEvent.ACTION_DOWN
 | 
| 11 | 10 |  import android.view.MotionEvent.ACTION_MOVE
 | 
| 12 | 11 |  import android.widget.FrameLayout
 | 
| 13 | 12 |  import androidx.coordinatorlayout.widget.CoordinatorLayout
 | 
| 14 | 13 |  import androidx.core.view.ViewCompat
 | 
| 15 | 14 |  import androidx.test.ext.junit.runners.AndroidJUnit4
 | 
| 16 | -import com.google.android.material.snackbar.Snackbar
 | 
|
| 17 | 15 |  import mozilla.components.browser.toolbar.BrowserToolbar
 | 
| 18 | 16 |  import mozilla.components.concept.engine.EngineSession
 | 
| 19 | 17 |  import mozilla.components.concept.engine.EngineView
 | 
| ... | ... | @@ -474,29 +472,6 @@ class BrowserToolbarBehaviorTest { | 
| 474 | 472 |          verify(yTranslator).collapseWithAnimation(toolbar)
 | 
| 475 | 473 |      }
 | 
| 476 | 474 | |
| 477 | -    @Test
 | 
|
| 478 | -    fun `Behavior will position snackbar above toolbar`() {
 | 
|
| 479 | -        val behavior = BrowserToolbarBehavior(testContext, null, ToolbarPosition.BOTTOM)
 | 
|
| 480 | -  | 
|
| 481 | -        val toolbar: BrowserToolbar = mock()
 | 
|
| 482 | -        doReturn(4223).`when`(toolbar).id
 | 
|
| 483 | -  | 
|
| 484 | -        val layoutParams: CoordinatorLayout.LayoutParams = CoordinatorLayout.LayoutParams(0, 0)
 | 
|
| 485 | -  | 
|
| 486 | -        val snackbarLayout: Snackbar.SnackbarLayout = mock()
 | 
|
| 487 | -        doReturn(layoutParams).`when`(snackbarLayout).layoutParams
 | 
|
| 488 | -  | 
|
| 489 | -        behavior.layoutDependsOn(
 | 
|
| 490 | -            parent = mock(),
 | 
|
| 491 | -            child = toolbar,
 | 
|
| 492 | -            dependency = snackbarLayout
 | 
|
| 493 | -        )
 | 
|
| 494 | -  | 
|
| 495 | -        assertEquals(4223, layoutParams.anchorId)
 | 
|
| 496 | -        assertEquals(Gravity.TOP or Gravity.CENTER_HORIZONTAL, layoutParams.anchorGravity)
 | 
|
| 497 | -        assertEquals(Gravity.TOP or Gravity.CENTER_HORIZONTAL, layoutParams.gravity)
 | 
|
| 498 | -    }
 | 
|
| 499 | -  | 
|
| 500 | 475 |      @Test
 | 
| 501 | 476 |      fun `Behavior will forceExpand when scrolling up and !shouldScroll if the touch was handled in the browser`() {
 | 
| 502 | 477 |          val behavior = spy(BrowserToolbarBehavior(testContext, null, ToolbarPosition.BOTTOM))
 | 
| ... | ... | @@ -10,7 +10,7 @@ import mozilla.components.browser.state.state.content.DownloadState | 
| 10 | 10 |  import mozilla.components.feature.downloads.DownloadDialogFragment.Companion.BYTES_TO_MB_LIMIT
 | 
| 11 | 11 |  import mozilla.components.feature.downloads.DownloadDialogFragment.Companion.KILOBYTE
 | 
| 12 | 12 |  import mozilla.components.feature.downloads.DownloadDialogFragment.Companion.MEGABYTE
 | 
| 13 | -import mozilla.components.support.utils.DownloadUtils
 | 
|
| 13 | +import mozilla.components.feature.downloads.ext.realFilenameOrGuessed
 | 
|
| 14 | 14 | |
| 15 | 15 |  /**
 | 
| 16 | 16 |   * This is a general representation of a dialog meant to be used in collaboration with [DownloadsFeature]
 | 
| ... | ... | @@ -34,11 +34,7 @@ abstract class DownloadDialogFragment : AppCompatDialogFragment() { | 
| 34 | 34 |       */
 | 
| 35 | 35 |      fun setDownload(download: DownloadState) {
 | 
| 36 | 36 |          val args = arguments ?: Bundle()
 | 
| 37 | -        args.putString(
 | 
|
| 38 | -            KEY_FILE_NAME,
 | 
|
| 39 | -            download.fileName
 | 
|
| 40 | -                ?: DownloadUtils.guessFileName(null, download.destinationDirectory, download.url, download.contentType)
 | 
|
| 41 | -        )
 | 
|
| 37 | +        args.putString(KEY_FILE_NAME, download.realFilenameOrGuessed)
 | 
|
| 42 | 38 |          args.putString(KEY_URL, download.url)
 | 
| 43 | 39 |          args.putLong(KEY_CONTENT_LENGTH, download.contentLength ?: 0)
 | 
| 44 | 40 |          arguments = args
 | 
| ... | ... | @@ -25,6 +25,7 @@ import mozilla.components.browser.state.state.SessionState | 
| 25 | 25 |  import mozilla.components.browser.state.state.content.DownloadState
 | 
| 26 | 26 |  import mozilla.components.browser.state.store.BrowserStore
 | 
| 27 | 27 |  import mozilla.components.feature.downloads.DownloadDialogFragment.Companion.FRAGMENT_TAG
 | 
| 28 | +import mozilla.components.feature.downloads.ext.realFilenameOrGuessed
 | 
|
| 28 | 29 |  import mozilla.components.feature.downloads.manager.DownloadManager
 | 
| 29 | 30 |  import mozilla.components.feature.downloads.manager.noop
 | 
| 30 | 31 |  import mozilla.components.feature.downloads.manager.onDownloadStopped
 | 
| ... | ... | @@ -41,6 +42,43 @@ import mozilla.components.support.ktx.kotlin.isSameOriginAs | 
| 41 | 42 |  import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged
 | 
| 42 | 43 |  import mozilla.components.support.utils.Browsers
 | 
| 43 | 44 | |
| 45 | +/**
 | 
|
| 46 | + * The name of the file to be downloaded.
 | 
|
| 47 | + */
 | 
|
| 48 | +@JvmInline
 | 
|
| 49 | +value class Filename(val value: String)
 | 
|
| 50 | +  | 
|
| 51 | +/**
 | 
|
| 52 | + * The size of the file to be downloaded expressed as the number of `bytes`.
 | 
|
| 53 | + * The value will be `0` if the size is unknown.
 | 
|
| 54 | + */
 | 
|
| 55 | +@JvmInline
 | 
|
| 56 | +value class ContentSize(val value: Long)
 | 
|
| 57 | +  | 
|
| 58 | +/**
 | 
|
| 59 | + * The list of all applications that can perform a download, including this application.
 | 
|
| 60 | + */
 | 
|
| 61 | +@JvmInline
 | 
|
| 62 | +value class ThirdPartyDownloaderApps(val value: List<DownloaderApp>)
 | 
|
| 63 | +  | 
|
| 64 | +/**
 | 
|
| 65 | + * Callback for when the user picked a certain application with which to download the current file.
 | 
|
| 66 | + */
 | 
|
| 67 | +@JvmInline
 | 
|
| 68 | +value class ThirdPartyDownloaderAppChosenCallback(val value: (DownloaderApp) -> Unit)
 | 
|
| 69 | +  | 
|
| 70 | +/**
 | 
|
| 71 | + * Callback for when the positive button of a download dialog was tapped.
 | 
|
| 72 | + */
 | 
|
| 73 | +@JvmInline
 | 
|
| 74 | +value class PositiveActionCallback(val value: () -> Unit)
 | 
|
| 75 | +  | 
|
| 76 | +/**
 | 
|
| 77 | + * Callback for when the negative button of a download dialog was tapped.
 | 
|
| 78 | + */
 | 
|
| 79 | +@JvmInline
 | 
|
| 80 | +value class NegativeActionCallback(val value: () -> Unit)
 | 
|
| 81 | +  | 
|
| 44 | 82 |  /**
 | 
| 45 | 83 |   * Feature implementation to provide download functionality for the selected
 | 
| 46 | 84 |   * session. The feature will subscribe to the selected session and listen
 | 
| ... | ... | @@ -60,6 +98,10 @@ import mozilla.components.support.utils.Browsers | 
| 60 | 98 |   * @property promptsStyling styling properties for the dialog.
 | 
| 61 | 99 |   * @property shouldForwardToThirdParties Indicates if downloads should be forward to third party apps,
 | 
| 62 | 100 |   * if there are multiple apps a chooser dialog will shown.
 | 
| 101 | + * @property customFirstPartyDownloadDialog An optional delegate for showing a dialog for a download
 | 
|
| 102 | + * that will be processed by the current application.
 | 
|
| 103 | + * @property customThirdPartyDownloadDialog An optional delegate for showing a dialog for a download
 | 
|
| 104 | + * that can be processed by multiple installed applications including the current one.
 | 
|
| 63 | 105 |   */
 | 
| 64 | 106 |  @Suppress("LongParameterList", "LargeClass")
 | 
| 65 | 107 |  class DownloadsFeature(
 | 
| ... | ... | @@ -73,7 +115,11 @@ class DownloadsFeature( | 
| 73 | 115 |      private val tabId: String? = null,
 | 
| 74 | 116 |      private val fragmentManager: FragmentManager? = null,
 | 
| 75 | 117 |      private val promptsStyling: PromptsStyling? = null,
 | 
| 76 | -    private val shouldForwardToThirdParties: () -> Boolean = { false }
 | 
|
| 118 | +    private val shouldForwardToThirdParties: () -> Boolean = { false },
 | 
|
| 119 | +    private val customFirstPartyDownloadDialog:
 | 
|
| 120 | +    ((Filename, ContentSize, PositiveActionCallback, NegativeActionCallback) -> Unit)? = null,
 | 
|
| 121 | +    private val customThirdPartyDownloadDialog:
 | 
|
| 122 | +    ((ThirdPartyDownloaderApps, ThirdPartyDownloaderAppChosenCallback, NegativeActionCallback) -> Unit)? = null,
 | 
|
| 77 | 123 |  ) : LifecycleAwareFeature, PermissionsFeature {
 | 
| 78 | 124 | |
| 79 | 125 |      var onDownloadStopped: onDownloadStopped
 | 
| ... | ... | @@ -159,16 +205,45 @@ class DownloadsFeature( | 
| 159 | 205 |          val shouldShowAppDownloaderDialog = shouldForwardToThirdParties() && apps.size > 1
 | 
| 160 | 206 | |
| 161 | 207 |          return if (shouldShowAppDownloaderDialog) {
 | 
| 162 | -            showAppDownloaderDialog(tab, download, apps)
 | 
|
| 208 | +            when (customThirdPartyDownloadDialog) {
 | 
|
| 209 | +                null -> showAppDownloaderDialog(tab, download, apps)
 | 
|
| 210 | +                else -> customThirdPartyDownloadDialog.invoke(
 | 
|
| 211 | +                    ThirdPartyDownloaderApps(apps),
 | 
|
| 212 | +                    ThirdPartyDownloaderAppChosenCallback {
 | 
|
| 213 | +                        onDownloaderAppSelected(it, tab, download)
 | 
|
| 214 | +                    },
 | 
|
| 215 | +                    NegativeActionCallback {
 | 
|
| 216 | +                        useCases.cancelDownloadRequest.invoke(tab.id, download.id)
 | 
|
| 217 | +                    },
 | 
|
| 218 | +                )
 | 
|
| 219 | +            }
 | 
|
| 220 | +  | 
|
| 163 | 221 |              false
 | 
| 164 | 222 |          } else {
 | 
| 165 | 223 |              if (applicationContext.isPermissionGranted(downloadManager.permissions.asIterable())) {
 | 
| 166 | -                if (fragmentManager != null && !download.skipConfirmation) {
 | 
|
| 167 | -                    showDownloadDialog(tab, download)
 | 
|
| 168 | -                    false
 | 
|
| 169 | -                } else {
 | 
|
| 170 | -                    useCases.consumeDownload(tab.id, download.id)
 | 
|
| 171 | -                    startDownload(download)
 | 
|
| 224 | +                when {
 | 
|
| 225 | +                    customFirstPartyDownloadDialog != null && !download.skipConfirmation -> {
 | 
|
| 226 | +                        customFirstPartyDownloadDialog.invoke(
 | 
|
| 227 | +                            Filename(download.realFilenameOrGuessed),
 | 
|
| 228 | +                            ContentSize(download.contentLength ?: 0),
 | 
|
| 229 | +                            PositiveActionCallback {
 | 
|
| 230 | +                                startDownload(download)
 | 
|
| 231 | +                                useCases.consumeDownload.invoke(tab.id, download.id)
 | 
|
| 232 | +                            },
 | 
|
| 233 | +                            NegativeActionCallback {
 | 
|
| 234 | +                                useCases.cancelDownloadRequest.invoke(tab.id, download.id)
 | 
|
| 235 | +                            },
 | 
|
| 236 | +                        )
 | 
|
| 237 | +                        false
 | 
|
| 238 | +                    }
 | 
|
| 239 | +                    fragmentManager != null && !download.skipConfirmation -> {
 | 
|
| 240 | +                        showDownloadDialog(tab, download)
 | 
|
| 241 | +                        false
 | 
|
| 242 | +                    }
 | 
|
| 243 | +                    else -> {
 | 
|
| 244 | +                        useCases.consumeDownload(tab.id, download.id)
 | 
|
| 245 | +                        startDownload(download)
 | 
|
| 246 | +                    }
 | 
|
| 172 | 247 |                  }
 | 
| 173 | 248 |              } else {
 | 
| 174 | 249 |                  onNeedToRequestPermissions(downloadManager.permissions)
 | 
| ... | ... | @@ -264,25 +339,7 @@ class DownloadsFeature( | 
| 264 | 339 |      ) {
 | 
| 265 | 340 |          appChooserDialog.setApps(apps)
 | 
| 266 | 341 |          appChooserDialog.onAppSelected = { app ->
 | 
| 267 | -            if (app.packageName == applicationContext.packageName) {
 | 
|
| 268 | -                if (applicationContext.isPermissionGranted(downloadManager.permissions.asIterable())) {
 | 
|
| 269 | -                    startDownload(download)
 | 
|
| 270 | -                    useCases.consumeDownload(tab.id, download.id)
 | 
|
| 271 | -                } else {
 | 
|
| 272 | -                    onNeedToRequestPermissions(downloadManager.permissions)
 | 
|
| 273 | -                }
 | 
|
| 274 | -            } else {
 | 
|
| 275 | -                try {
 | 
|
| 276 | -                    applicationContext.startActivity(app.toIntent())
 | 
|
| 277 | -                } catch (error: ActivityNotFoundException) {
 | 
|
| 278 | -                    val errorMessage = applicationContext.getString(
 | 
|
| 279 | -                        R.string.mozac_feature_downloads_unable_to_open_third_party_app,
 | 
|
| 280 | -                        app.name
 | 
|
| 281 | -                    )
 | 
|
| 282 | -                    Toast.makeText(applicationContext, errorMessage, Toast.LENGTH_SHORT).show()
 | 
|
| 283 | -                }
 | 
|
| 284 | -                useCases.consumeDownload(tab.id, download.id)
 | 
|
| 285 | -            }
 | 
|
| 342 | +            onDownloaderAppSelected(app, tab, download)
 | 
|
| 286 | 343 |          }
 | 
| 287 | 344 | |
| 288 | 345 |          appChooserDialog.onDismiss = {
 | 
| ... | ... | @@ -294,6 +351,29 @@ class DownloadsFeature( | 
| 294 | 351 |          }
 | 
| 295 | 352 |      }
 | 
| 296 | 353 | |
| 354 | +    @VisibleForTesting
 | 
|
| 355 | +    internal fun onDownloaderAppSelected(app: DownloaderApp, tab: SessionState, download: DownloadState) {
 | 
|
| 356 | +        if (app.packageName == applicationContext.packageName) {
 | 
|
| 357 | +            if (applicationContext.isPermissionGranted(downloadManager.permissions.asIterable())) {
 | 
|
| 358 | +                startDownload(download)
 | 
|
| 359 | +                useCases.consumeDownload(tab.id, download.id)
 | 
|
| 360 | +            } else {
 | 
|
| 361 | +                onNeedToRequestPermissions(downloadManager.permissions)
 | 
|
| 362 | +            }
 | 
|
| 363 | +        } else {
 | 
|
| 364 | +            try {
 | 
|
| 365 | +                applicationContext.startActivity(app.toIntent())
 | 
|
| 366 | +            } catch (error: ActivityNotFoundException) {
 | 
|
| 367 | +                val errorMessage = applicationContext.getString(
 | 
|
| 368 | +                    R.string.mozac_feature_downloads_unable_to_open_third_party_app,
 | 
|
| 369 | +                    app.name,
 | 
|
| 370 | +                )
 | 
|
| 371 | +                Toast.makeText(applicationContext, errorMessage, Toast.LENGTH_SHORT).show()
 | 
|
| 372 | +            }
 | 
|
| 373 | +            useCases.consumeDownload(tab.id, download.id)
 | 
|
| 374 | +        }
 | 
|
| 375 | +    }
 | 
|
| 376 | +  | 
|
| 297 | 377 |      private fun getAppDownloaderDialog() = findPreviousAppDownloaderDialogFragment()
 | 
| 298 | 378 |          ?: DownloadAppChooserDialog.newInstance(
 | 
| 299 | 379 |              promptsStyling?.gravity,
 | 
| ... | ... | @@ -47,3 +47,6 @@ internal fun DownloadState.withResponse(headers: Headers, stream: InputStream?): | 
| 47 | 47 |          contentLength = contentLength ?: headers[CONTENT_LENGTH]?.toLongOrNull()
 | 
| 48 | 48 |      )
 | 
| 49 | 49 |  }
 | 
| 50 | +  | 
|
| 51 | +internal val DownloadState.realFilenameOrGuessed
 | 
|
| 52 | +    get() = fileName ?: DownloadUtils.guessFileName(null, destinationDirectory, url, contentType) | 
| ... | ... | @@ -16,10 +16,10 @@ import mozilla.components.feature.downloads.R | 
| 16 | 16 |  /**
 | 
| 17 | 17 |   * An adapter for displaying the applications that can perform downloads.
 | 
| 18 | 18 |   */
 | 
| 19 | -internal class DownloaderAppAdapter(
 | 
|
| 19 | +class DownloaderAppAdapter(
 | 
|
| 20 | 20 |      context: Context,
 | 
| 21 | 21 |      private val apps: List<DownloaderApp>,
 | 
| 22 | -    val onAppSelected: ((DownloaderApp) -> Unit)
 | 
|
| 22 | +    val onAppSelected: ((DownloaderApp) -> Unit),
 | 
|
| 23 | 23 |  ) : RecyclerView.Adapter<DownloaderAppViewHolder>() {
 | 
| 24 | 24 | |
| 25 | 25 |      private val inflater = LayoutInflater.from(context)
 | 
| ... | ... | @@ -49,11 +49,14 @@ internal class DownloaderAppAdapter( | 
| 49 | 49 |  /**
 | 
| 50 | 50 |   * View holder for a [DownloaderApp] item.
 | 
| 51 | 51 |   */
 | 
| 52 | -internal class DownloaderAppViewHolder(
 | 
|
| 52 | +class DownloaderAppViewHolder(
 | 
|
| 53 | 53 |      itemView: View,
 | 
| 54 | 54 |      val nameLabel: TextView,
 | 
| 55 | -    val iconImage: ImageView
 | 
|
| 55 | +    val iconImage: ImageView,
 | 
|
| 56 | 56 |  ) : RecyclerView.ViewHolder(itemView) {
 | 
| 57 | +    /**
 | 
|
| 58 | +     * Show a certain downloader application in the current View.
 | 
|
| 59 | +     */
 | 
|
| 57 | 60 |      fun bind(app: DownloaderApp, onAppSelected: ((DownloaderApp) -> Unit)) {
 | 
| 58 | 61 |          itemView.app = app
 | 
| 59 | 62 |          itemView.setOnClickListener {
 | 
| ... | ... | @@ -31,6 +31,7 @@ tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach { | 
| 31 | 31 |  dependencies {
 | 
| 32 | 32 |      implementation project(':browser-state')
 | 
| 33 | 33 |      implementation project(':concept-engine')
 | 
| 34 | +    implementation project(':feature-session')
 | 
|
| 34 | 35 |      implementation project(':lib-state')
 | 
| 35 | 36 |      implementation project(':support-ktx')
 | 
| 36 | 37 |      implementation project(':support-utils')
 | 
| ... | ... | @@ -46,6 +47,7 @@ dependencies { | 
| 46 | 47 |      testImplementation Dependencies.testing_coroutines
 | 
| 47 | 48 |      testImplementation Dependencies.testing_robolectric
 | 
| 48 | 49 |      testImplementation Dependencies.testing_mockito
 | 
| 50 | +    testImplementation project(':feature-session')
 | 
|
| 49 | 51 |      testImplementation project(':support-test')
 | 
| 50 | 52 |      testImplementation project(':support-test-libstate')
 | 
| 51 | 53 | 
| ... | ... | @@ -71,6 +71,8 @@ import mozilla.components.feature.prompts.login.LoginExceptions | 
| 71 | 71 |  import mozilla.components.feature.prompts.login.LoginPicker
 | 
| 72 | 72 |  import mozilla.components.feature.prompts.share.DefaultShareDelegate
 | 
| 73 | 73 |  import mozilla.components.feature.prompts.share.ShareDelegate
 | 
| 74 | +import mozilla.components.feature.session.SessionUseCases
 | 
|
| 75 | +import mozilla.components.feature.session.SessionUseCases.ExitFullScreenUseCase
 | 
|
| 74 | 76 |  import mozilla.components.lib.state.ext.flowScoped
 | 
| 75 | 77 |  import mozilla.components.support.base.feature.ActivityResultHandler
 | 
| 76 | 78 |  import mozilla.components.support.base.feature.LifecycleAwareFeature
 | 
| ... | ... | @@ -111,6 +113,7 @@ internal const val FRAGMENT_TAG = "mozac_feature_prompt_dialog" | 
| 111 | 113 |   * @property fragmentManager The [FragmentManager] to be used when displaying
 | 
| 112 | 114 |   * a dialog (fragment).
 | 
| 113 | 115 |   * @property shareDelegate Delegate used to display share sheet.
 | 
| 116 | + * @property exitFullscreenUsecase Usecase allowing to exit browser tabs' fullscreen mode.
 | 
|
| 114 | 117 |   * @property loginStorageDelegate Delegate used to access login storage. If null,
 | 
| 115 | 118 |   * 'save login'prompts will not be shown.
 | 
| 116 | 119 |   * @property isSaveLoginEnabled A callback invoked when a login prompt is triggered. If false,
 | 
| ... | ... | @@ -144,6 +147,7 @@ class PromptFeature private constructor( | 
| 144 | 147 |      private var customTabId: String?,
 | 
| 145 | 148 |      private val fragmentManager: FragmentManager,
 | 
| 146 | 149 |      private val shareDelegate: ShareDelegate,
 | 
| 150 | +    private val exitFullscreenUsecase: ExitFullScreenUseCase = SessionUseCases(store).exitFullscreen,
 | 
|
| 147 | 151 |      override val creditCardValidationDelegate: CreditCardValidationDelegate? = null,
 | 
| 148 | 152 |      override val loginValidationDelegate: LoginValidationDelegate? = null,
 | 
| 149 | 153 |      private val isSaveLoginEnabled: () -> Boolean = { false },
 | 
| ... | ... | @@ -184,6 +188,7 @@ class PromptFeature private constructor( | 
| 184 | 188 |          customTabId: String? = null,
 | 
| 185 | 189 |          fragmentManager: FragmentManager,
 | 
| 186 | 190 |          shareDelegate: ShareDelegate = DefaultShareDelegate(),
 | 
| 191 | +        exitFullscreenUsecase: ExitFullScreenUseCase = SessionUseCases(store).exitFullscreen,
 | 
|
| 187 | 192 |          creditCardValidationDelegate: CreditCardValidationDelegate? = null,
 | 
| 188 | 193 |          loginValidationDelegate: LoginValidationDelegate? = null,
 | 
| 189 | 194 |          isSaveLoginEnabled: () -> Boolean = { false },
 | 
| ... | ... | @@ -202,6 +207,7 @@ class PromptFeature private constructor( | 
| 202 | 207 |          customTabId = customTabId,
 | 
| 203 | 208 |          fragmentManager = fragmentManager,
 | 
| 204 | 209 |          shareDelegate = shareDelegate,
 | 
| 210 | +        exitFullscreenUsecase = exitFullscreenUsecase,
 | 
|
| 205 | 211 |          creditCardValidationDelegate = creditCardValidationDelegate,
 | 
| 206 | 212 |          loginValidationDelegate = loginValidationDelegate,
 | 
| 207 | 213 |          isSaveLoginEnabled = isSaveLoginEnabled,
 | 
| ... | ... | @@ -222,6 +228,7 @@ class PromptFeature private constructor( | 
| 222 | 228 |          customTabId: String? = null,
 | 
| 223 | 229 |          fragmentManager: FragmentManager,
 | 
| 224 | 230 |          shareDelegate: ShareDelegate = DefaultShareDelegate(),
 | 
| 231 | +        exitFullscreenUsecase: ExitFullScreenUseCase = SessionUseCases(store).exitFullscreen,
 | 
|
| 225 | 232 |          creditCardValidationDelegate: CreditCardValidationDelegate? = null,
 | 
| 226 | 233 |          loginValidationDelegate: LoginValidationDelegate? = null,
 | 
| 227 | 234 |          isSaveLoginEnabled: () -> Boolean = { false },
 | 
| ... | ... | @@ -240,6 +247,7 @@ class PromptFeature private constructor( | 
| 240 | 247 |          customTabId = customTabId,
 | 
| 241 | 248 |          fragmentManager = fragmentManager,
 | 
| 242 | 249 |          shareDelegate = shareDelegate,
 | 
| 250 | +        exitFullscreenUsecase = exitFullscreenUsecase,
 | 
|
| 243 | 251 |          creditCardValidationDelegate = creditCardValidationDelegate,
 | 
| 244 | 252 |          loginValidationDelegate = loginValidationDelegate,
 | 
| 245 | 253 |          isSaveLoginEnabled = isSaveLoginEnabled,
 | 
| ... | ... | @@ -420,6 +428,10 @@ class PromptFeature private constructor( | 
| 420 | 428 |      internal fun onPromptRequested(session: SessionState) {
 | 
| 421 | 429 |          // Some requests are handle with intents
 | 
| 422 | 430 |          session.content.promptRequests.lastOrNull()?.let { promptRequest ->
 | 
| 431 | +            store.state.findTabOrCustomTabOrSelectedTab(customTabId)?.let {
 | 
|
| 432 | +                exitFullscreenUsecase(it.id)
 | 
|
| 433 | +            }
 | 
|
| 434 | +  | 
|
| 423 | 435 |              when (promptRequest) {
 | 
| 424 | 436 |                  is File -> filePicker.handleFileRequest(promptRequest)
 | 
| 425 | 437 |                  is Share -> handleShareRequest(promptRequest, session)
 |