Skip to content

Commit

Permalink
Fix mozilla-mobile#9614 - Add a new InputResultDetail
Browse files Browse the repository at this point in the history
This will serve the following purposes:
- wrapper for all the new data from GeckoView's onTouchEventForDetailResult
- filters out values not in range (eg: GV's INPUT_RESULT_IGNORED)
- controls how the data can be updated and offers clear APIs for querying this
data without needing to know about the implementation specifics.
  • Loading branch information
Mugurell committed Mar 26, 2021
1 parent 0bc7edf commit 9a1bba8
Show file tree
Hide file tree
Showing 2 changed files with 795 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,358 @@
/* 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 mozilla.components.concept.engine

import android.view.MotionEvent
import androidx.annotation.VisibleForTesting

// The below top-level values are following the same from [org.mozilla.geckoview.PanZoomController]

/**
* The content has no scrollable element.
*
* @see [InputResultDetail.isTouchUnhandled]
*/
@VisibleForTesting
internal const val INPUT_UNHANDLED = 0

/**
* The touch event is consumed by the [EngineView]
*
* @see [InputResultDetail.isTouchHandledByBrowser]
*/
@VisibleForTesting
internal const val INPUT_HANDLED = 1

/**
* The touch event is consumed by the website through it's own touch listeners.
*
* @see [InputResultDetail.isTouchHandledByWebsite]
*/
@VisibleForTesting
internal const val INPUT_HANDLED_CONTENT = 2

/**
* The website content is not scrollable.
*/
@VisibleForTesting
internal const val SCROLL_DIRECTIONS_NONE = 0

/**
* The website content can be scrolled to the top.
*
* @see [InputResultDetail.canScrollToTop]
*/
@VisibleForTesting
internal const val SCROLL_DIRECTIONS_TOP = 1 shl 0

/**
* The website content can be scrolled to the right.
*
* @see [InputResultDetail.canScrollToRight]
*/
@VisibleForTesting
internal const val SCROLL_DIRECTIONS_RIGHT = 1 shl 1

/**
* The website content can be scrolled to the bottom.
*
* @see [InputResultDetail.canScrollToBottom]
*/
@VisibleForTesting
internal const val SCROLL_DIRECTIONS_BOTTOM = 1 shl 2

/**
* The website content can be scrolled to the left.
*
* @see [InputResultDetail.canScrollToLeft]
*/
@VisibleForTesting
internal const val SCROLL_DIRECTIONS_LEFT = 1 shl 3

/**
* The website content cannot be overscrolled.
*/
@VisibleForTesting
internal const val OVERSCROLL_DIRECTIONS_NONE = 0

/**
* The website content can be overscrolled horizontally.
*
* @see [InputResultDetail.canOverscrollRight]
* @see [InputResultDetail.canOverscrollLeft]
*/
@VisibleForTesting
internal const val OVERSCROLL_DIRECTIONS_HORIZONTAL = 1 shl 0

/**
* The website content can be overscrolled vertically.
*
* @see [InputResultDetail.canOverscrollTop]
* @see [InputResultDetail.canOverscrollBottom]
*/
@VisibleForTesting
internal const val OVERSCROLL_DIRECTIONS_VERTICAL = 1 shl 1

/**
* All data about how a touch will be handled by the browser.
* - whether the event is used for panning/zooming by the browser / by the website or will be ignored.
* - whether the event can scroll the page and in what direction.
* - whether the event can overscroll the page and in what direction.
*
* @param inputResult Indicates who will use the current [MotionEvent].
* Possible values: [[INPUT_UNHANDLED], [INPUT_HANDLED], [INPUT_HANDLED_CONTENT]]<br>.
*
* @param scrollDirections Bitwise ORed value of the directions the page can be scrolled to.
* This is the same as GeckoView's [org.mozilla.geckoview.PanZoomController.ScrollableDirections].
*
* @param overscrollDirections Bitwise ORed value of the directions the page can be overscrolled to.
* This is the same as GeckoView's [org.mozilla.geckoview.PanZoomController.OverscrollDirections].
*/
@Suppress("TooManyFunctions")
class InputResultDetail private constructor(
val inputResult: Int = INPUT_UNHANDLED,
val scrollDirections: Int = SCROLL_DIRECTIONS_NONE,
val overscrollDirections: Int = OVERSCROLL_DIRECTIONS_NONE
) {

override fun equals(other: Any?): Boolean {
return if (this !== other) {
if (other is InputResultDetail) {
return inputResult == other.inputResult &&
scrollDirections == other.scrollDirections &&
overscrollDirections == other.overscrollDirections
} else {
false
}
} else {
true
}
}

@Suppress("MagicNumber")
override fun hashCode(): Int {
var hash = inputResult.hashCode() * 31
hash += (scrollDirections.hashCode()) * 31
hash += (overscrollDirections.hashCode()) * 31

return hash
}

override fun toString(): String {
return StringBuilder("InputResultDetail \$${hashCode()} (")
.append("Input ${getInputResultHandledDescription()}. ")
.append("Content ${getScrollDirectionsDescription()} and ${getOverscrollDirectionsDescription()}")
.append(')')
.toString()
}

/**
* Create a new instance of [InputResultDetail] with the option of keep some of the current values.
*
* The provided new values will be filtered out if not recognized and could corrupt the current state.
*/
fun copy(
inputResult: Int? = this.inputResult,
scrollDirections: Int? = this.scrollDirections,
overscrollDirections: Int? = this.overscrollDirections
): InputResultDetail {
// Ensure this data will not get corrupted by users sending unknown arguments

val newValidInputResult = if (inputResult in INPUT_UNHANDLED..INPUT_HANDLED_CONTENT) {
inputResult
} else {
this.inputResult
}
val newValidScrollDirections = if (scrollDirections in
SCROLL_DIRECTIONS_NONE..(SCROLL_DIRECTIONS_LEFT or (SCROLL_DIRECTIONS_LEFT - 1))
) {
scrollDirections
} else {
this.scrollDirections
}
val newValidOverscrollDirections = if (overscrollDirections in
OVERSCROLL_DIRECTIONS_NONE..(OVERSCROLL_DIRECTIONS_VERTICAL or (OVERSCROLL_DIRECTIONS_VERTICAL - 1))
) {
overscrollDirections
} else {
this.overscrollDirections
}

// The range check automatically checks for null but doesn't yet have a contract to say so.
// As such it it safe to use the not-null assertion operator.
return InputResultDetail(newValidInputResult!!, newValidScrollDirections!!, newValidOverscrollDirections!!)
}

/**
* The [EngineView] handled the last [MotionEvent] to pan or zoom the content.
*/
fun isTouchHandledByBrowser() = inputResult == INPUT_HANDLED

/**
* The website handled the last [MotionEvent] through it's own touch listeners
* and consumed it without the [EngineView] panning or zooming the website
*/
fun isTouchHandledByWebsite() = inputResult == INPUT_HANDLED_CONTENT

/**
* Neither the [EngineView], nor the website will handle this [MotionEvent].
*
* This might happen on a website without touch listeners that is not bigger than the screen
* or when the content has no scrollable element.
*/
fun isTouchUnhandled() = inputResult == INPUT_UNHANDLED

/**
* Whether the width of the webpage exceeds the display and the webpage can be scrolled to left.
*/
fun canScrollToLeft(): Boolean =
inputResult == INPUT_HANDLED &&
scrollDirections and SCROLL_DIRECTIONS_LEFT != 0

/**
* Whether the height of the webpage exceeds the display and the webpage can be scrolled to top.
*/
fun canScrollToTop(): Boolean =
inputResult == INPUT_HANDLED &&
scrollDirections and SCROLL_DIRECTIONS_TOP != 0

/**
* Whether the width of the webpage exceeds the display and the webpage can be scrolled to right.
*/
fun canScrollToRight(): Boolean =
inputResult == INPUT_HANDLED &&
scrollDirections and SCROLL_DIRECTIONS_RIGHT != 0

/**
* Whether the height of the webpage exceeds the display and the webpage can be scrolled to bottom.
*/
fun canScrollToBottom(): Boolean =
inputResult == INPUT_HANDLED &&
scrollDirections and SCROLL_DIRECTIONS_BOTTOM != 0

/**
* Whether the webpage can be overscrolled to the left.
*
* @return `true` if the page is already scrolled to the left most part
* and the touch event is not handled by the webpage.
*/
fun canOverscrollLeft(): Boolean =
inputResult != INPUT_HANDLED_CONTENT &&
(scrollDirections and SCROLL_DIRECTIONS_LEFT == 0) &&
(overscrollDirections and OVERSCROLL_DIRECTIONS_HORIZONTAL != 0)

/**
* Whether the webpage can be overscrolled to the top.
*
* @return `true` if the page is already scrolled to the top most part
* and the touch event is not handled by the webpage.
*/
fun canOverscrollTop(): Boolean =
inputResult != INPUT_HANDLED_CONTENT &&
(scrollDirections and SCROLL_DIRECTIONS_TOP == 0) &&
(overscrollDirections and OVERSCROLL_DIRECTIONS_VERTICAL != 0)

/**
* Whether the webpage can be overscrolled to the right.
*
* @return `true` if the page is already scrolled to the right most part
* and the touch event is not handled by the webpage.
*/
fun canOverscrollRight(): Boolean =
inputResult != INPUT_HANDLED_CONTENT &&
(scrollDirections and SCROLL_DIRECTIONS_RIGHT == 0) &&
(overscrollDirections and OVERSCROLL_DIRECTIONS_HORIZONTAL != 0)

/**
* Whether the webpage can be overscrolled to the bottom.
*
* @return `true` if the page is already scrolled to the bottom most part
* and the touch event is not handled by the webpage.
*/
fun canOverscrollBottom(): Boolean =
inputResult != INPUT_HANDLED_CONTENT &&
(scrollDirections and SCROLL_DIRECTIONS_BOTTOM == 0) &&
(overscrollDirections and OVERSCROLL_DIRECTIONS_VERTICAL != 0)

@VisibleForTesting
internal fun getInputResultHandledDescription() = when (inputResult) {
INPUT_HANDLED -> INPUT_HANDLED_TOSTRING_DESCRIPTION
INPUT_HANDLED_CONTENT -> INPUT_HANDLED_CONTENT_TOSTRING_DESCRIPTION
else -> INPUT_UNHANDLED_TOSTRING_DESCRIPTION
}

@VisibleForTesting
internal fun getScrollDirectionsDescription(): String {
if (scrollDirections == SCROLL_DIRECTIONS_NONE) {
return SCROLL_IMPOSSIBLE_TOSTRING_DESCRIPTION
}

val scrollDirections = StringBuilder()
.append(if (canScrollToLeft()) "$SCROLL_LEFT_TOSTRING_DESCRIPTION$TOSTRING_SEPARATOR" else "")
.append(if (canScrollToTop()) "$SCROLL_TOP_TOSTRING_DESCRIPTION$TOSTRING_SEPARATOR" else "")
.append(if (canScrollToRight()) "$SCROLL_RIGHT_TOSTRING_DESCRIPTION$TOSTRING_SEPARATOR" else "")
.append(if (canScrollToBottom()) SCROLL_BOTTOM_TOSTRING_DESCRIPTION else "")
.removeSuffix(TOSTRING_SEPARATOR)
.toString()

return if (scrollDirections.trim().isEmpty()) {
SCROLL_IMPOSSIBLE_TOSTRING_DESCRIPTION
} else {
SCROLL_TOSTRING_DESCRIPTION + scrollDirections
}
}

@VisibleForTesting
internal fun getOverscrollDirectionsDescription(): String {
if (overscrollDirections == OVERSCROLL_DIRECTIONS_NONE) {
return OVERSCROLL_IMPOSSIBLE_TOSTRING_DESCRIPTION
}

val overscrollDirections = StringBuilder()
.append(if (canOverscrollLeft()) "$SCROLL_LEFT_TOSTRING_DESCRIPTION$TOSTRING_SEPARATOR" else "")
.append(if (canOverscrollTop()) "$SCROLL_TOP_TOSTRING_DESCRIPTION$TOSTRING_SEPARATOR" else "")
.append(if (canOverscrollRight()) "$SCROLL_RIGHT_TOSTRING_DESCRIPTION$TOSTRING_SEPARATOR" else "")
.append(if (canOverscrollBottom()) SCROLL_BOTTOM_TOSTRING_DESCRIPTION else "")
.removeSuffix(TOSTRING_SEPARATOR)
.toString()

return if (overscrollDirections.trim().isEmpty()) {
OVERSCROLL_IMPOSSIBLE_TOSTRING_DESCRIPTION
} else {
OVERSCROLL_TOSTRING_DESCRIPTION + overscrollDirections
}
}

companion object {
/**
* Create a new instance of [InputResultDetail].
*
* @param verticalOverscrollInitiallyEnabled optional parameter for enabling pull to refresh
* in the cases in which this class can be used before valid values being set and it helps more to have
* overscroll vertically allowed and then stop depending on the values with which this class is updated
* rather than start with a disabled overscroll functionality for the current gesture.
*/
fun newInstance(verticalOverscrollInitiallyEnabled: Boolean = false) = InputResultDetail(
overscrollDirections = if (verticalOverscrollInitiallyEnabled) {
OVERSCROLL_DIRECTIONS_VERTICAL
} else {
OVERSCROLL_DIRECTIONS_NONE
}
)

@VisibleForTesting internal const val TOSTRING_SEPARATOR = ", "
@VisibleForTesting internal const val INPUT_HANDLED_TOSTRING_DESCRIPTION = "handled by the browser"
@VisibleForTesting internal const val INPUT_HANDLED_CONTENT_TOSTRING_DESCRIPTION = "handled by the website"
@VisibleForTesting internal const val INPUT_UNHANDLED_TOSTRING_DESCRIPTION = "unhandled"
@VisibleForTesting internal const val SCROLL_IMPOSSIBLE_TOSTRING_DESCRIPTION = "cannot be scrolled"
@VisibleForTesting internal const val OVERSCROLL_IMPOSSIBLE_TOSTRING_DESCRIPTION = "cannot be overscrolled"
@VisibleForTesting internal const val SCROLL_TOSTRING_DESCRIPTION = "can be scrolled to "
@VisibleForTesting internal const val OVERSCROLL_TOSTRING_DESCRIPTION = "can be overscrolled to "
@VisibleForTesting internal const val SCROLL_LEFT_TOSTRING_DESCRIPTION = "left"
@VisibleForTesting internal const val SCROLL_TOP_TOSTRING_DESCRIPTION = "top"
@VisibleForTesting internal const val SCROLL_RIGHT_TOSTRING_DESCRIPTION = "right"
@VisibleForTesting internal const val SCROLL_BOTTOM_TOSTRING_DESCRIPTION = "bottom"
}
}
Loading

0 comments on commit 9a1bba8

Please sign in to comment.