forked from mozilla-mobile/fenix
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
For mozilla-mobile#9044 - Replace DownloadNotificationBottomSheetDial…
…og with unobtrusive view
- Loading branch information
codrut.topliceanu
committed
May 7, 2020
1 parent
9177871
commit 5caf9a3
Showing
7 changed files
with
503 additions
and
135 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
143 changes: 143 additions & 0 deletions
143
app/src/main/java/org/mozilla/fenix/downloads/DownloadNotification.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
} | ||
} |
156 changes: 156 additions & 0 deletions
156
app/src/main/java/org/mozilla/fenix/downloads/DownloadNotificationBehavior.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} |
Oops, something went wrong.