|
| 1 | +/* This Source Code Form is subject to the terms of the Mozilla Public |
| 2 | + * License, v. 2.0. If a copy of the MPL was not distributed with this |
| 3 | + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
| 4 | + |
| 5 | +package mozilla.components.concept.engine |
| 6 | + |
| 7 | +import android.view.MotionEvent |
| 8 | +import androidx.annotation.VisibleForTesting |
| 9 | + |
| 10 | +// The below top-level values are following the same from [org.mozilla.geckoview.PanZoomController] |
| 11 | + |
| 12 | +/** |
| 13 | + * The content has no scrollable element. |
| 14 | + * |
| 15 | + * @see [InputResultDetail.isTouchUnhandled] |
| 16 | + */ |
| 17 | +@VisibleForTesting |
| 18 | +internal const val INPUT_UNHANDLED = 0 |
| 19 | + |
| 20 | +/** |
| 21 | + * The touch event is consumed by the [EngineView] |
| 22 | + * |
| 23 | + * @see [InputResultDetail.isTouchHandledByBrowser] |
| 24 | + */ |
| 25 | +@VisibleForTesting |
| 26 | +internal const val INPUT_HANDLED = 1 |
| 27 | + |
| 28 | +/** |
| 29 | + * The touch event is consumed by the website through it's own touch listeners. |
| 30 | + * |
| 31 | + * @see [InputResultDetail.isTouchHandledByWebsite] |
| 32 | + */ |
| 33 | +@VisibleForTesting |
| 34 | +internal const val INPUT_HANDLED_CONTENT = 2 |
| 35 | + |
| 36 | +/** |
| 37 | + * The website content is not scrollable. |
| 38 | + */ |
| 39 | +@VisibleForTesting |
| 40 | +internal const val SCROLL_DIRECTIONS_NONE = 0 |
| 41 | + |
| 42 | +/** |
| 43 | + * The website content can be scrolled to the top. |
| 44 | + * |
| 45 | + * @see [InputResultDetail.canScrollToTop] |
| 46 | + */ |
| 47 | +@VisibleForTesting |
| 48 | +internal const val SCROLL_DIRECTIONS_TOP = 1 shl 0 |
| 49 | + |
| 50 | +/** |
| 51 | + * The website content can be scrolled to the right. |
| 52 | + * |
| 53 | + * @see [InputResultDetail.canScrollToRight] |
| 54 | + */ |
| 55 | +@VisibleForTesting |
| 56 | +internal const val SCROLL_DIRECTIONS_RIGHT = 1 shl 1 |
| 57 | + |
| 58 | +/** |
| 59 | + * The website content can be scrolled to the bottom. |
| 60 | + * |
| 61 | + * @see [InputResultDetail.canScrollToBottom] |
| 62 | + */ |
| 63 | +@VisibleForTesting |
| 64 | +internal const val SCROLL_DIRECTIONS_BOTTOM = 1 shl 2 |
| 65 | + |
| 66 | +/** |
| 67 | + * The website content can be scrolled to the left. |
| 68 | + * |
| 69 | + * @see [InputResultDetail.canScrollToLeft] |
| 70 | + */ |
| 71 | +@VisibleForTesting |
| 72 | +internal const val SCROLL_DIRECTIONS_LEFT = 1 shl 3 |
| 73 | + |
| 74 | +/** |
| 75 | + * The website content cannot be overscrolled. |
| 76 | + */ |
| 77 | +@VisibleForTesting |
| 78 | +internal const val OVERSCROLL_DIRECTIONS_NONE = 0 |
| 79 | + |
| 80 | +/** |
| 81 | + * The website content can be overscrolled horizontally. |
| 82 | + * |
| 83 | + * @see [InputResultDetail.canOverscrollRight] |
| 84 | + * @see [InputResultDetail.canOverscrollLeft] |
| 85 | + */ |
| 86 | +@VisibleForTesting |
| 87 | +internal const val OVERSCROLL_DIRECTIONS_HORIZONTAL = 1 shl 0 |
| 88 | + |
| 89 | +/** |
| 90 | + * The website content can be overscrolled vertically. |
| 91 | + * |
| 92 | + * @see [InputResultDetail.canOverscrollTop] |
| 93 | + * @see [InputResultDetail.canOverscrollBottom] |
| 94 | + */ |
| 95 | +@VisibleForTesting |
| 96 | +internal const val OVERSCROLL_DIRECTIONS_VERTICAL = 1 shl 1 |
| 97 | + |
| 98 | +/** |
| 99 | + * All data about how a touch will be handled by the browser. |
| 100 | + * - whether the event is used for panning/zooming by the browser / by the website or will be ignored. |
| 101 | + * - whether the event can scroll the page and in what direction. |
| 102 | + * - whether the event can overscroll the page and in what direction. |
| 103 | + * |
| 104 | + * @param inputResult Indicates who will use the current [MotionEvent]. |
| 105 | + * Possible values: [[INPUT_UNHANDLED], [INPUT_HANDLED], [INPUT_HANDLED_CONTENT]]<br>. |
| 106 | + * |
| 107 | + * @param scrollDirections Bitwise ORed value of the directions the page can be scrolled to. |
| 108 | + * This is the same as GeckoView's [org.mozilla.geckoview.PanZoomController.ScrollableDirections]. |
| 109 | + * |
| 110 | + * @param overscrollDirections Bitwise ORed value of the directions the page can be overscrolled to. |
| 111 | + * This is the same as GeckoView's [org.mozilla.geckoview.PanZoomController.OverscrollDirections]. |
| 112 | + */ |
| 113 | +@Suppress("TooManyFunctions") |
| 114 | +class InputResultDetail private constructor( |
| 115 | + val inputResult: Int = INPUT_UNHANDLED, |
| 116 | + val scrollDirections: Int = SCROLL_DIRECTIONS_NONE, |
| 117 | + val overscrollDirections: Int = OVERSCROLL_DIRECTIONS_NONE |
| 118 | +) { |
| 119 | + |
| 120 | + override fun equals(other: Any?): Boolean { |
| 121 | + return if (this !== other) { |
| 122 | + if (other is InputResultDetail) { |
| 123 | + return inputResult == other.inputResult && |
| 124 | + scrollDirections == other.scrollDirections && |
| 125 | + overscrollDirections == other.overscrollDirections |
| 126 | + } else { |
| 127 | + false |
| 128 | + } |
| 129 | + } else { |
| 130 | + true |
| 131 | + } |
| 132 | + } |
| 133 | + |
| 134 | + @Suppress("MagicNumber") |
| 135 | + override fun hashCode(): Int { |
| 136 | + var hash = inputResult.hashCode() * 31 |
| 137 | + hash += (scrollDirections.hashCode()) * 31 |
| 138 | + hash += (overscrollDirections.hashCode()) * 31 |
| 139 | + |
| 140 | + return hash |
| 141 | + } |
| 142 | + |
| 143 | + override fun toString(): String { |
| 144 | + return StringBuilder("InputResultDetail \$${hashCode()} (") |
| 145 | + .append("Input ${getInputResultHandledDescription()}. ") |
| 146 | + .append("Content ${getScrollDirectionsDescription()} and ${getOverscrollDirectionsDescription()}") |
| 147 | + .append(')') |
| 148 | + .toString() |
| 149 | + } |
| 150 | + |
| 151 | + /** |
| 152 | + * Create a new instance of [InputResultDetail] with the option of keep some of the current values. |
| 153 | + * |
| 154 | + * The provided new values will be filtered out if not recognized and could corrupt the current state. |
| 155 | + */ |
| 156 | + fun copy( |
| 157 | + inputResult: Int? = this.inputResult, |
| 158 | + scrollDirections: Int? = this.scrollDirections, |
| 159 | + overscrollDirections: Int? = this.overscrollDirections |
| 160 | + ): InputResultDetail { |
| 161 | + // Ensure this data will not get corrupted by users sending unknown arguments |
| 162 | + |
| 163 | + val newValidInputResult = if (inputResult in INPUT_UNHANDLED..INPUT_HANDLED_CONTENT) { |
| 164 | + inputResult |
| 165 | + } else { |
| 166 | + this.inputResult |
| 167 | + } |
| 168 | + val newValidScrollDirections = if (scrollDirections in |
| 169 | + SCROLL_DIRECTIONS_NONE..(SCROLL_DIRECTIONS_LEFT or (SCROLL_DIRECTIONS_LEFT - 1)) |
| 170 | + ) { |
| 171 | + scrollDirections |
| 172 | + } else { |
| 173 | + this.scrollDirections |
| 174 | + } |
| 175 | + val newValidOverscrollDirections = if (overscrollDirections in |
| 176 | + OVERSCROLL_DIRECTIONS_NONE..(OVERSCROLL_DIRECTIONS_VERTICAL or (OVERSCROLL_DIRECTIONS_VERTICAL - 1)) |
| 177 | + ) { |
| 178 | + overscrollDirections |
| 179 | + } else { |
| 180 | + this.overscrollDirections |
| 181 | + } |
| 182 | + |
| 183 | + // The range check automatically checks for null but doesn't yet have a contract to say so. |
| 184 | + // As such it it safe to use the not-null assertion operator. |
| 185 | + return InputResultDetail(newValidInputResult!!, newValidScrollDirections!!, newValidOverscrollDirections!!) |
| 186 | + } |
| 187 | + |
| 188 | + /** |
| 189 | + * The [EngineView] handled the last [MotionEvent] to pan or zoom the content. |
| 190 | + */ |
| 191 | + fun isTouchHandledByBrowser() = inputResult == INPUT_HANDLED |
| 192 | + |
| 193 | + /** |
| 194 | + * The website handled the last [MotionEvent] through it's own touch listeners |
| 195 | + * and consumed it without the [EngineView] panning or zooming the website |
| 196 | + */ |
| 197 | + fun isTouchHandledByWebsite() = inputResult == INPUT_HANDLED_CONTENT |
| 198 | + |
| 199 | + /** |
| 200 | + * Neither the [EngineView], nor the website will handle this [MotionEvent]. |
| 201 | + * |
| 202 | + * This might happen on a website without touch listeners that is not bigger than the screen |
| 203 | + * or when the content has no scrollable element. |
| 204 | + */ |
| 205 | + fun isTouchUnhandled() = inputResult == INPUT_UNHANDLED |
| 206 | + |
| 207 | + /** |
| 208 | + * Whether the width of the webpage exceeds the display and the webpage can be scrolled to left. |
| 209 | + */ |
| 210 | + fun canScrollToLeft(): Boolean = |
| 211 | + inputResult == INPUT_HANDLED && |
| 212 | + scrollDirections and SCROLL_DIRECTIONS_LEFT != 0 |
| 213 | + |
| 214 | + /** |
| 215 | + * Whether the height of the webpage exceeds the display and the webpage can be scrolled to top. |
| 216 | + */ |
| 217 | + fun canScrollToTop(): Boolean = |
| 218 | + inputResult == INPUT_HANDLED && |
| 219 | + scrollDirections and SCROLL_DIRECTIONS_TOP != 0 |
| 220 | + |
| 221 | + /** |
| 222 | + * Whether the width of the webpage exceeds the display and the webpage can be scrolled to right. |
| 223 | + */ |
| 224 | + fun canScrollToRight(): Boolean = |
| 225 | + inputResult == INPUT_HANDLED && |
| 226 | + scrollDirections and SCROLL_DIRECTIONS_RIGHT != 0 |
| 227 | + |
| 228 | + /** |
| 229 | + * Whether the height of the webpage exceeds the display and the webpage can be scrolled to bottom. |
| 230 | + */ |
| 231 | + fun canScrollToBottom(): Boolean = |
| 232 | + inputResult == INPUT_HANDLED && |
| 233 | + scrollDirections and SCROLL_DIRECTIONS_BOTTOM != 0 |
| 234 | + |
| 235 | + /** |
| 236 | + * Whether the webpage can be overscrolled to the left. |
| 237 | + * |
| 238 | + * @return `true` if the page is already scrolled to the left most part |
| 239 | + * and the touch event is not handled by the webpage. |
| 240 | + */ |
| 241 | + fun canOverscrollLeft(): Boolean = |
| 242 | + inputResult != INPUT_HANDLED_CONTENT && |
| 243 | + (scrollDirections and SCROLL_DIRECTIONS_LEFT == 0) && |
| 244 | + (overscrollDirections and OVERSCROLL_DIRECTIONS_HORIZONTAL != 0) |
| 245 | + |
| 246 | + /** |
| 247 | + * Whether the webpage can be overscrolled to the top. |
| 248 | + * |
| 249 | + * @return `true` if the page is already scrolled to the top most part |
| 250 | + * and the touch event is not handled by the webpage. |
| 251 | + */ |
| 252 | + fun canOverscrollTop(): Boolean = |
| 253 | + inputResult != INPUT_HANDLED_CONTENT && |
| 254 | + (scrollDirections and SCROLL_DIRECTIONS_TOP == 0) && |
| 255 | + (overscrollDirections and OVERSCROLL_DIRECTIONS_VERTICAL != 0) |
| 256 | + |
| 257 | + /** |
| 258 | + * Whether the webpage can be overscrolled to the right. |
| 259 | + * |
| 260 | + * @return `true` if the page is already scrolled to the right most part |
| 261 | + * and the touch event is not handled by the webpage. |
| 262 | + */ |
| 263 | + fun canOverscrollRight(): Boolean = |
| 264 | + inputResult != INPUT_HANDLED_CONTENT && |
| 265 | + (scrollDirections and SCROLL_DIRECTIONS_RIGHT == 0) && |
| 266 | + (overscrollDirections and OVERSCROLL_DIRECTIONS_HORIZONTAL != 0) |
| 267 | + |
| 268 | + /** |
| 269 | + * Whether the webpage can be overscrolled to the bottom. |
| 270 | + * |
| 271 | + * @return `true` if the page is already scrolled to the bottom most part |
| 272 | + * and the touch event is not handled by the webpage. |
| 273 | + */ |
| 274 | + fun canOverscrollBottom(): Boolean = |
| 275 | + inputResult != INPUT_HANDLED_CONTENT && |
| 276 | + (scrollDirections and SCROLL_DIRECTIONS_BOTTOM == 0) && |
| 277 | + (overscrollDirections and OVERSCROLL_DIRECTIONS_VERTICAL != 0) |
| 278 | + |
| 279 | + @VisibleForTesting |
| 280 | + internal fun getInputResultHandledDescription() = when (inputResult) { |
| 281 | + INPUT_HANDLED -> INPUT_HANDLED_TOSTRING_DESCRIPTION |
| 282 | + INPUT_HANDLED_CONTENT -> INPUT_HANDLED_CONTENT_TOSTRING_DESCRIPTION |
| 283 | + else -> INPUT_UNHANDLED_TOSTRING_DESCRIPTION |
| 284 | + } |
| 285 | + |
| 286 | + @VisibleForTesting |
| 287 | + internal fun getScrollDirectionsDescription(): String { |
| 288 | + if (scrollDirections == SCROLL_DIRECTIONS_NONE) { |
| 289 | + return SCROLL_IMPOSSIBLE_TOSTRING_DESCRIPTION |
| 290 | + } |
| 291 | + |
| 292 | + val scrollDirections = StringBuilder() |
| 293 | + .append(if (canScrollToLeft()) "$SCROLL_LEFT_TOSTRING_DESCRIPTION$TOSTRING_SEPARATOR" else "") |
| 294 | + .append(if (canScrollToTop()) "$SCROLL_TOP_TOSTRING_DESCRIPTION$TOSTRING_SEPARATOR" else "") |
| 295 | + .append(if (canScrollToRight()) "$SCROLL_RIGHT_TOSTRING_DESCRIPTION$TOSTRING_SEPARATOR" else "") |
| 296 | + .append(if (canScrollToBottom()) SCROLL_BOTTOM_TOSTRING_DESCRIPTION else "") |
| 297 | + .removeSuffix(TOSTRING_SEPARATOR) |
| 298 | + .toString() |
| 299 | + |
| 300 | + return if (scrollDirections.trim().isEmpty()) { |
| 301 | + SCROLL_IMPOSSIBLE_TOSTRING_DESCRIPTION |
| 302 | + } else { |
| 303 | + SCROLL_TOSTRING_DESCRIPTION + scrollDirections |
| 304 | + } |
| 305 | + } |
| 306 | + |
| 307 | + @VisibleForTesting |
| 308 | + internal fun getOverscrollDirectionsDescription(): String { |
| 309 | + if (overscrollDirections == OVERSCROLL_DIRECTIONS_NONE) { |
| 310 | + return OVERSCROLL_IMPOSSIBLE_TOSTRING_DESCRIPTION |
| 311 | + } |
| 312 | + |
| 313 | + val overscrollDirections = StringBuilder() |
| 314 | + .append(if (canOverscrollLeft()) "$SCROLL_LEFT_TOSTRING_DESCRIPTION$TOSTRING_SEPARATOR" else "") |
| 315 | + .append(if (canOverscrollTop()) "$SCROLL_TOP_TOSTRING_DESCRIPTION$TOSTRING_SEPARATOR" else "") |
| 316 | + .append(if (canOverscrollRight()) "$SCROLL_RIGHT_TOSTRING_DESCRIPTION$TOSTRING_SEPARATOR" else "") |
| 317 | + .append(if (canOverscrollBottom()) SCROLL_BOTTOM_TOSTRING_DESCRIPTION else "") |
| 318 | + .removeSuffix(TOSTRING_SEPARATOR) |
| 319 | + .toString() |
| 320 | + |
| 321 | + return if (overscrollDirections.trim().isEmpty()) { |
| 322 | + OVERSCROLL_IMPOSSIBLE_TOSTRING_DESCRIPTION |
| 323 | + } else { |
| 324 | + OVERSCROLL_TOSTRING_DESCRIPTION + overscrollDirections |
| 325 | + } |
| 326 | + } |
| 327 | + |
| 328 | + companion object { |
| 329 | + /** |
| 330 | + * Create a new instance of [InputResultDetail]. |
| 331 | + * |
| 332 | + * @param verticalOverscrollInitiallyEnabled optional parameter for enabling pull to refresh |
| 333 | + * in the cases in which this class can be used before valid values being set and it helps more to have |
| 334 | + * overscroll vertically allowed and then stop depending on the values with which this class is updated |
| 335 | + * rather than start with a disabled overscroll functionality for the current gesture. |
| 336 | + */ |
| 337 | + fun newInstance(verticalOverscrollInitiallyEnabled: Boolean = false) = InputResultDetail( |
| 338 | + overscrollDirections = if (verticalOverscrollInitiallyEnabled) { |
| 339 | + OVERSCROLL_DIRECTIONS_VERTICAL |
| 340 | + } else { |
| 341 | + OVERSCROLL_DIRECTIONS_NONE |
| 342 | + } |
| 343 | + ) |
| 344 | + |
| 345 | + @VisibleForTesting internal const val TOSTRING_SEPARATOR = ", " |
| 346 | + @VisibleForTesting internal const val INPUT_HANDLED_TOSTRING_DESCRIPTION = "handled by the browser" |
| 347 | + @VisibleForTesting internal const val INPUT_HANDLED_CONTENT_TOSTRING_DESCRIPTION = "handled by the website" |
| 348 | + @VisibleForTesting internal const val INPUT_UNHANDLED_TOSTRING_DESCRIPTION = "unhandled" |
| 349 | + @VisibleForTesting internal const val SCROLL_IMPOSSIBLE_TOSTRING_DESCRIPTION = "cannot be scrolled" |
| 350 | + @VisibleForTesting internal const val OVERSCROLL_IMPOSSIBLE_TOSTRING_DESCRIPTION = "cannot be overscrolled" |
| 351 | + @VisibleForTesting internal const val SCROLL_TOSTRING_DESCRIPTION = "can be scrolled to " |
| 352 | + @VisibleForTesting internal const val OVERSCROLL_TOSTRING_DESCRIPTION = "can be overscrolled to " |
| 353 | + @VisibleForTesting internal const val SCROLL_LEFT_TOSTRING_DESCRIPTION = "left" |
| 354 | + @VisibleForTesting internal const val SCROLL_TOP_TOSTRING_DESCRIPTION = "top" |
| 355 | + @VisibleForTesting internal const val SCROLL_RIGHT_TOSTRING_DESCRIPTION = "right" |
| 356 | + @VisibleForTesting internal const val SCROLL_BOTTOM_TOSTRING_DESCRIPTION = "bottom" |
| 357 | + } |
| 358 | +} |
0 commit comments