Skip to content

Commit

Permalink
For mozilla-mobile#9044 - Replace DownloadNotificationBottomSheetDial…
Browse files Browse the repository at this point in the history
…og with unobtrusive view
  • Loading branch information
codrut.topliceanu committed May 7, 2020
1 parent 9177871 commit 5caf9a3
Show file tree
Hide file tree
Showing 7 changed files with 503 additions and 135 deletions.
23 changes: 15 additions & 8 deletions app/src/main/java/org/mozilla/fenix/browser/BaseBrowserFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import mozilla.appservices.places.BookmarkRoot
import mozilla.components.browser.session.Session
import mozilla.components.browser.session.SessionManager
import mozilla.components.browser.session.runWithSessionIdOrSelected
import mozilla.components.browser.state.action.ContentAction
import mozilla.components.concept.engine.prompt.ShareData
import mozilla.components.feature.accounts.FxaCapability
import mozilla.components.feature.accounts.FxaWebChannelFeature
Expand Down Expand Up @@ -78,7 +79,7 @@ import org.mozilla.fenix.components.toolbar.BrowserToolbarViewInteractor
import org.mozilla.fenix.components.toolbar.DefaultBrowserToolbarController
import org.mozilla.fenix.components.toolbar.SwipeRefreshScrollingViewBehavior
import org.mozilla.fenix.components.toolbar.ToolbarIntegration
import org.mozilla.fenix.downloads.DownloadNotificationBottomSheetDialog
import org.mozilla.fenix.downloads.DownloadNotification
import org.mozilla.fenix.downloads.DownloadService
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.enterToImmersiveMode
Expand Down Expand Up @@ -289,15 +290,15 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
}
)

downloadFeature.onDownloadStopped = { download, _, downloadJobStatus ->
downloadFeature.onDownloadStopped = { downloadState, _, downloadJobStatus ->
// If the download is just paused, don't show any in-app notification
if (downloadJobStatus == AbstractFetchDownloadService.DownloadJobStatus.COMPLETED ||
downloadJobStatus == AbstractFetchDownloadService.DownloadJobStatus.FAILED
) {
val dialog = DownloadNotificationBottomSheetDialog(
context = context,
DownloadNotification(
container = view.browserLayout,
didFail = downloadJobStatus == AbstractFetchDownloadService.DownloadJobStatus.FAILED,
download = download,
downloadState = downloadState,
tryAgain = downloadFeature::tryAgain,
onCannotOpenFile = {
FenixSnackbar.make(
Expand All @@ -307,9 +308,15 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
)
.setText(context.getString(R.string.mozac_feature_downloads_could_not_open_file))
.show()
}
)
dialog.show()
},
onDismiss = {
store.dispatch(ContentAction.UpdateDownloadAction(
session.id,
downloadState.copy(dismissed = true)))
},
view = view.viewDownloadNotification,
toolbar = browserToolbarView
).expand()
}
}

Expand Down
143 changes: 143 additions & 0 deletions app/src/main/java/org/mozilla/fenix/downloads/DownloadNotification.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

package org.mozilla.fenix.downloads

import android.view.View
import android.view.ViewGroup
import androidx.coordinatorlayout.widget.CoordinatorLayout
import kotlinx.android.extensions.LayoutContainer
import kotlinx.android.synthetic.main.download_notification_layout.view.*
import mozilla.components.browser.state.state.content.DownloadState
import mozilla.components.feature.downloads.AbstractFetchDownloadService
import mozilla.components.feature.downloads.toMegabyteString
import org.mozilla.fenix.FeatureFlags
import org.mozilla.fenix.R
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.toolbar.BrowserToolbarView
import org.mozilla.fenix.ext.metrics
import org.mozilla.fenix.ext.settings

/**
* [DownloadNotification] is used to show a view in the current tab to the user, triggered when
* downloadFeature.onDownloadStopped gets invoked. It uses [DownloadNotificationBehavior] to
* hide when the users scrolls through a website as to not impede his activities. Uses
* [DownloadState.dismissed] to track whether the user has closed the view.
* */

class DownloadNotification(
private val container: ViewGroup,
private val downloadState: DownloadState?,
private val didFail: Boolean,
private val tryAgain: (Long) -> Unit,
private val onCannotOpenFile: () -> Unit,
private val view: View,
private val toolbar: BrowserToolbarView,
private val onDismiss: () -> Unit
) : LayoutContainer {

override val containerView: View?
get() = container

private val settings = container.context.settings()

init {
setupDownloadNotification(view)
}

private fun setupDownloadNotification(view: View) {
if (downloadState == null) return
view.apply {
if (FeatureFlags.dynamicBottomToolbar && layoutParams is CoordinatorLayout.LayoutParams) {
(layoutParams as CoordinatorLayout.LayoutParams).apply {

behavior =
DownloadNotificationBehavior<View>(
context,
null,
toolbar.view.height.toFloat()
)
toolbar.expand()
}
}
}

if (settings.shouldUseBottomToolbar) {
val params: ViewGroup.MarginLayoutParams =
view.layoutParams as ViewGroup.MarginLayoutParams
params.bottomMargin = toolbar.view.height
}

if (didFail) {
view.download_notification_title.text =
container.context.getString(R.string.mozac_feature_downloads_failed_notification_text2)

view.download_notification_icon.setImageResource(
mozilla.components.feature.downloads.R.drawable.mozac_feature_download_ic_download_failed
)

view.download_notification_action_button.apply {
text = context.getString(
mozilla.components.feature.downloads.R.string.mozac_feature_downloads_button_try_again
)
setOnClickListener {
tryAgain(downloadState.id)
context.metrics.track(Event.InAppNotificationDownloadTryAgain)
dismiss(view)
}
}
} else {
val titleText = container.context.getString(
R.string.mozac_feature_downloads_completed_notification_text2
) + " (${downloadState.contentLength?.toMegabyteString()})"

view.download_notification_title.text = titleText

view.download_notification_icon.setImageResource(
mozilla.components.feature.downloads.R.drawable.mozac_feature_download_ic_download_complete
)

view.download_notification_action_button.apply {
text = context.getString(
mozilla.components.feature.downloads.R.string.mozac_feature_downloads_button_open
)
setOnClickListener {
val fileWasOpened = AbstractFetchDownloadService.openFile(
context = context,
contentType = downloadState.contentType,
filePath = downloadState.filePath
)

if (!fileWasOpened) {
onCannotOpenFile()
}

context.metrics.track(Event.InAppNotificationDownloadOpen)
dismiss(view)
}
}
}

view.download_notification_close_button.setOnClickListener {
dismiss(view)
}

view.download_notification_filename.text = downloadState.fileName
}

fun expand() {
view.visibility = View.VISIBLE

if (FeatureFlags.dynamicBottomToolbar) {
(view.layoutParams as CoordinatorLayout.LayoutParams).apply {
(behavior as DownloadNotificationBehavior).forceExpand(view)
}
}
}

private fun dismiss(view: View) {
view.visibility = View.GONE
onDismiss()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

package org.mozilla.fenix.downloads

import android.animation.ValueAnimator
import android.content.Context
import android.util.AttributeSet
import android.view.View
import android.view.animation.DecelerateInterpolator
import androidx.annotation.VisibleForTesting
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.view.ViewCompat
import mozilla.components.concept.engine.EngineView
import mozilla.components.support.ktx.android.view.findViewInHierarchy
import kotlin.math.max
import kotlin.math.min

/**
* A [CoordinatorLayout.Behavior] implementation to be used when placing [DownloadNotification]
* at the bottom of the screen. Based off of BrowserToolbarBottomBehavior.
*
* This implementation will:
* - Show/Hide the [DownloadNotification] automatically when scrolling vertically.
* - Snap the [DownloadNotification] to be hidden or visible when the user stops scrolling.
*/

private const val SNAP_ANIMATION_DURATION = 150L

class DownloadNotificationBehavior<V : View>(
context: Context?,
attrs: AttributeSet?,
private val bottomToolbarHeight: Float = 0f
) : CoordinatorLayout.Behavior<V>(context, attrs) {

@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal var shouldSnapAfterScroll: Boolean = false

@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal var snapAnimator: ValueAnimator = ValueAnimator()
.apply {
interpolator = DecelerateInterpolator()
duration = SNAP_ANIMATION_DURATION
}

/**
* Reference to [EngineView] used to check user's [android.view.MotionEvent]s.
*/
private var engineView: EngineView? = null

/**
* Depending on how user's touch was consumed by EngineView / current website,
*
* we will animate the dynamic download notification dialog if:
* - touches were used for zooming / panning operations in the website.
*
* We will do nothing if:
* - the website is not scrollable
* - the website handles the touch events itself through it's own touch event listeners.
*/
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal val shouldScroll: Boolean
get() = engineView?.getInputResult() == EngineView.InputResult.INPUT_RESULT_HANDLED

override fun onStartNestedScroll(
coordinatorLayout: CoordinatorLayout,
child: V,
directTargetChild: View,
target: View,
axes: Int,
type: Int
): Boolean {
return if (shouldScroll && axes == ViewCompat.SCROLL_AXIS_VERTICAL) {
shouldSnapAfterScroll = type == ViewCompat.TYPE_TOUCH
snapAnimator.cancel()
true
} else if (engineView?.getInputResult() == EngineView.InputResult.INPUT_RESULT_UNHANDLED) {
// Force expand the notification dialog if event is unhandled, otherwise user could get stuck in a
// state where they cannot show it
forceExpand(child)
snapAnimator.cancel()
false
} else {
false
}
}

override fun onStopNestedScroll(
coordinatorLayout: CoordinatorLayout,
child: V,
target: View,
type: Int
) {
if (shouldSnapAfterScroll || type == ViewCompat.TYPE_NON_TOUCH) {
val minHeight: Float =
if (bottomToolbarHeight > 0) bottomToolbarHeight else child.height.toFloat()

if (child.translationY >= minHeight / 2) {
animateSnap(child, SnapDirection.DOWN)
} else {
animateSnap(child, SnapDirection.UP)
}
}
}

override fun onNestedPreScroll(
coordinatorLayout: CoordinatorLayout,
child: V,
target: View,
dx: Int,
dy: Int,
consumed: IntArray,
type: Int
) {
if (shouldScroll) {
super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed, type)
child.translationY = max(
0f,
min(
child.height.toFloat() + bottomToolbarHeight,
child.translationY + dy
)
)
}
}

override fun layoutDependsOn(
parent: CoordinatorLayout,
child: V,
dependency: View
): Boolean {
engineView = parent.findViewInHierarchy { it is EngineView } as? EngineView
return super.layoutDependsOn(parent, child, dependency)
}

fun forceExpand(view: View) {
animateSnap(view, SnapDirection.UP)
}

@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal fun animateSnap(child: View, direction: SnapDirection) = with(snapAnimator) {
addUpdateListener { child.translationY = it.animatedValue as Float }
setFloatValues(
child.translationY,
if (direction == SnapDirection.UP) 0f else child.height.toFloat() + bottomToolbarHeight
)
start()
}

@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal enum class SnapDirection {
UP,
DOWN
}
}
Loading

0 comments on commit 5caf9a3

Please sign in to comment.