/* 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.utils import android.view.View import androidx.appcompat.widget.ContentFrameLayout import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch import org.mozilla.fenix.R import org.mozilla.fenix.components.FenixSnackbar import org.mozilla.fenix.ext.settings import java.util.concurrent.atomic.AtomicBoolean internal const val UNDO_DELAY = 3000L internal const val ACCESSIBLE_UNDO_DELAY = 15000L /** * Runs [operation] after giving user time (see [UNDO_DELAY]) to cancel it. * In case of cancellation, [onCancel] is executed. * * Execution of suspend blocks happens on [Dispatchers.Main]. * * @param view A [View] used to determine a parent for the [FenixSnackbar]. * @param message A message displayed as part of [FenixSnackbar]. * @param undoActionTitle Label for the action associated with the [FenixSnackbar]. * @param onCancel A suspend block to execute in case of cancellation. * @param operation A suspend block to execute if user doesn't cancel via the displayed [FenixSnackbar]. * @param anchorView A [View] to which [FenixSnackbar] should be anchored. */ @Suppress("LongParameterList") fun CoroutineScope.allowUndo( view: View, message: String, undoActionTitle: String, onCancel: suspend () -> Unit = {}, operation: suspend () -> Unit, anchorView: View? = null, elevation: Float? = null, paddedForBottomToolbar: Boolean = false ) { // By using an AtomicBoolean, we achieve memory effects of reading and // writing a volatile variable. val requestedUndo = AtomicBoolean(false) fun showUndoSnackbar() { val snackbar = FenixSnackbar .make( view = view, duration = FenixSnackbar.LENGTH_INDEFINITE, isDisplayedWithBrowserToolbar = false ) .setText(message) .setAnchorView(anchorView) .setAction(undoActionTitle) { requestedUndo.set(true) launch { onCancel.invoke() } } elevation?.also { snackbar.view.elevation = it } val shouldUseBottomToolbar = view.context.settings().shouldUseBottomToolbar val toolbarHeight = view.context.resources .getDimensionPixelSize(R.dimen.browser_toolbar_height) snackbar.view.setPadding( 0, 0, 0, if ( paddedForBottomToolbar && shouldUseBottomToolbar && // If the view passed in is a ContentFrameLayout, it does not matter // if the user has a dynamicBottomToolbar or not, as the Android system // can't intelligently position the snackbar on the upper most view. // Ideally we should not pass ContentFrameLayout in, but it's the only // way to display snackbars through a fragment transition. view is ContentFrameLayout ) { toolbarHeight } else { 0 } ) snackbar.show() // Wait a bit, and if user didn't request cancellation, proceed with // requested operation and hide the snackbar. launch { val lengthToDelay = if (view.context.settings().accessibilityServicesEnabled) { ACCESSIBLE_UNDO_DELAY } else { UNDO_DELAY } delay(lengthToDelay) if (!requestedUndo.get()) { snackbar.dismiss() operation.invoke() } } } showUndoSnackbar() }