Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • aguestuser/android-components
  • sysrqb/android-components
  • acat/android-components
  • gk/android-components
  • gaba/android-components
  • boklm/android-components
  • ma1/android-components
  • morgan/android-components
  • t-m-w/android-components
  • cypherpunks1/android-components
  • dan/android-components
11 results
Show changes
Commits on Source (4)
Showing
with 133 additions and 92 deletions
......@@ -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 ->
......
......@@ -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))
......
......@@ -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
......
......@@ -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.
*/
@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.
*/
@JvmInline
value class ContentSize(val value: Long)
/**
* The list of all applications that can perform a download, including this application.
*/
@JvmInline
value class ThirdPartyDownloaderApps(val value: List<DownloaderApp>)
/**
* Callback for when the user picked a certain application with which to download the current file.
*/
@JvmInline
value class ThirdPartyDownloaderAppChosenCallback(val value: (DownloaderApp) -> Unit)
/**
* Callback for when the positive button of a download dialog was tapped.
*/
@JvmInline
value class PositiveActionCallback(val value: () -> Unit)
/**
* Callback for when the negative button of a download dialog was tapped.
*/
@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,
......
......@@ -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)
......@@ -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 {
......
......@@ -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')
......
......@@ -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)
......