Skip to content

Commit

Permalink
fix: revert autocapture scroll and swipe (#206)
Browse files Browse the repository at this point in the history
* fix: revert autocapture scrolls

* fix: revert autocapture scrolls
  • Loading branch information
PouriaAmini authored Jul 29, 2024
1 parent c239bba commit 4b834bd
Show file tree
Hide file tree
Showing 10 changed files with 34 additions and 506 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,5 @@ data class ViewTarget(
val view: Any?
get() = viewRef.get()

enum class Type {
Clickable,
Scrollable,
}
enum class Type { Clickable }
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,17 @@ package com.amplitude.android.internal.gestures
import android.app.Activity
import android.view.GestureDetector
import android.view.MotionEvent
import android.view.View
import androidx.annotation.VisibleForTesting
import com.amplitude.android.internal.ViewHierarchyScanner.findTarget
import com.amplitude.android.internal.ViewTarget
import com.amplitude.android.internal.locators.ViewTargetLocator
import com.amplitude.android.utilities.DefaultEventUtils.EventProperties.DIRECTION
import com.amplitude.android.utilities.DefaultEventUtils.EventProperties.ELEMENT_CLASS
import com.amplitude.android.utilities.DefaultEventUtils.EventProperties.ELEMENT_RESOURCE
import com.amplitude.android.utilities.DefaultEventUtils.EventProperties.ELEMENT_SOURCE
import com.amplitude.android.utilities.DefaultEventUtils.EventProperties.ELEMENT_TAG
import com.amplitude.android.utilities.DefaultEventUtils.EventTypes.ELEMENT_CLICKED
import com.amplitude.android.utilities.DefaultEventUtils.EventTypes.ELEMENT_SCROLLED
import com.amplitude.android.utilities.DefaultEventUtils.EventTypes.ELEMENT_SWIPED
import com.amplitude.common.Logger
import java.lang.ref.WeakReference
import kotlin.math.abs

@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
class AutocaptureGestureListener(
Expand All @@ -27,18 +22,18 @@ class AutocaptureGestureListener(
private val logger: Logger,
private val viewTargetLocators: List<ViewTargetLocator>,
) : GestureDetector.OnGestureListener {
private enum class MoveGestureType {
Scroll,
Swipe,
Unknown,
private val activityRef: WeakReference<Activity> = WeakReference(activity)

override fun onDown(e: MotionEvent): Boolean {
return false
}

private val activityRef: WeakReference<Activity> = WeakReference(activity)
private val scrollState = ScrollState()
override fun onShowPress(e: MotionEvent) {}

override fun onSingleTapUp(e: MotionEvent): Boolean {
val decorView = ensureWindowDecorView("onSingleTapUp") ?: return false

val decorView =
activityRef.get()?.window?.decorView
?: logger.error("DecorView is null in onSingleTapUp()").let { return false }
val target: ViewTarget =
decorView.findTarget(
Pair(e.x, e.y),
Expand All @@ -47,14 +42,17 @@ class AutocaptureGestureListener(
logger,
) ?: logger.warn("Unable to find click target. No event captured.").let { return false }

capture(target, ELEMENT_CLICKED)
return false
}
mapOf(
ELEMENT_CLASS to target.className,
ELEMENT_RESOURCE to target.resourceName,
ELEMENT_TAG to target.tag,
ELEMENT_SOURCE to
target.source
.replace("_", " ")
.split(" ")
.joinToString(" ") { it.replaceFirstChar { c -> c.uppercase() } },
).let { track(ELEMENT_CLICKED, it) }

override fun onDown(e: MotionEvent): Boolean {
scrollState.reset()
scrollState.startX = e.x
scrollState.startY = e.y
return false
}

Expand All @@ -64,133 +62,17 @@ class AutocaptureGestureListener(
distanceX: Float,
distanceY: Float,
): Boolean {
val decorView = ensureWindowDecorView("onScroll") ?: return false
if (e1 == null || scrollState.type != MoveGestureType.Unknown) {
return false
}

val target: ViewTarget =
decorView.findTarget(
Pair(e1.x, e1.y),
viewTargetLocators,
ViewTarget.Type.Scrollable,
logger,
) ?: logger.warn("Unable to find scroll target. No event captured.").let { return false }

scrollState.setTarget(target)
scrollState.type = MoveGestureType.Scroll
return false
}

fun onUp(e: MotionEvent) {
ensureWindowDecorView("onUp") ?: return

val scrollTarget = scrollState.viewTarget ?: return
if (scrollState.type == MoveGestureType.Unknown) {
logger.warn("Unable to define scroll type. No event captured.")
return
}

val direction = scrollState.calculateDirection(e)
val eventType =
if (scrollState.type == MoveGestureType.Scroll) {
ELEMENT_SCROLLED
} else {
ELEMENT_SWIPED
}

capture(scrollTarget, eventType, mapOf(DIRECTION to direction))
scrollState.reset()
}
override fun onLongPress(e: MotionEvent) {}

override fun onFling(
e1: MotionEvent?,
e2: MotionEvent,
velocityX: Float,
velocityY: Float,
): Boolean {
scrollState.type = MoveGestureType.Swipe
return false
}

override fun onShowPress(e: MotionEvent) {}

override fun onLongPress(e: MotionEvent) {}

private fun ensureWindowDecorView(caller: String): View? {
val activity = activityRef.get()
val window = activity?.window
val decorView = window?.decorView

if (activity == null) {
logger.error("Activity is null in $caller")
} else if (window == null) {
logger.error("Window is null in $caller")
} else if (decorView == null) {
logger.error("DecorView is null in $caller")
}

return decorView
}

private fun capture(
target: ViewTarget,
eventType: String,
additionalProperties: Map<String, Any?> = emptyMap(),
) {
mapOf(
ELEMENT_CLASS to target.className,
ELEMENT_RESOURCE to target.resourceName,
ELEMENT_TAG to target.tag,
ELEMENT_SOURCE to
target.source
.replace("_", " ")
.split(" ")
.joinToString(" ") { it.replaceFirstChar { c -> c.uppercase() } },
).let { track(eventType, it + additionalProperties) }
}

private inner class ScrollState {
var type = MoveGestureType.Unknown
var viewTarget: ViewTarget? = null
var startX = 0f
var startY = 0f

fun setTarget(target: ViewTarget) {
this.viewTarget = target
}

/**
* Calculates the direction of the scroll/swipe based on startX and startY and a given event
*
* @param endEvent - the event which notifies when the scroll/swipe ended
* @return String, one of (left|right|up|down)
*/
fun calculateDirection(endEvent: MotionEvent): String {
val diffX = endEvent.x - startX
val diffY = endEvent.y - startY
val direction =
if (abs(diffX.toDouble()) > abs(diffY.toDouble())) {
if (diffX > 0f) {
"Right"
} else {
"Left"
}
} else {
if (diffY > 0) {
"Down"
} else {
"Up"
}
}
return direction
}

fun reset() {
viewTarget = null
type = MoveGestureType.Unknown
startX = 0f
startY = 0f
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ internal class AutocaptureWindowCallback(
event?.let {
motionEventObtainer.obtain(event).let {
try {
handleTouchEvent(it)
gestureDetector.onTouchEvent(it)
} catch (e: Exception) {
logger.error("Error handling touch event: $e")
} finally {
Expand All @@ -33,13 +33,6 @@ internal class AutocaptureWindowCallback(
return super.dispatchTouchEvent(event)
}

private fun handleTouchEvent(event: MotionEvent) {
gestureDetector.onTouchEvent(event)
if (event.actionMasked == MotionEvent.ACTION_UP) {
gestureListener.onUp(event)
}
}

interface MotionEventObtainer {
fun obtain(origin: MotionEvent): MotionEvent {
return MotionEvent.obtain(origin)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,11 @@
package com.amplitude.android.internal.locators

import android.view.View
import android.widget.AbsListView
import android.widget.ScrollView
import androidx.core.view.ScrollingView
import com.amplitude.android.internal.ViewResourceUtils.resourceIdWithFallback
import com.amplitude.android.internal.ViewTarget
import com.amplitude.android.internal.ViewTarget.Type

internal class AndroidViewTargetLocator(
private val isAndroidXAvailable: Boolean,
) : ViewTargetLocator {
internal class AndroidViewTargetLocator : ViewTargetLocator {
private val coordinates = IntArray(2)

companion object {
Expand All @@ -21,19 +16,9 @@ internal class AndroidViewTargetLocator(
targetPosition: Pair<Float, Float>,
targetType: Type,
): ViewTarget? {
val view = this as? View ?: return null
with(view) {
if (!touchWithinBounds(targetPosition)) {
return null
}

if (targetType === Type.Clickable && isViewTappable()) {
return createViewTarget()
} else if (targetType === Type.Scrollable && isViewScrollable(isAndroidXAvailable)) {
return createViewTarget()
}
}
return null
return (this as? View)
?.takeIf { touchWithinBounds(targetPosition) && targetType === Type.Clickable && isViewTappable() }
?.let { createViewTarget() }
}

private fun View.createViewTarget(): ViewTarget {
Expand All @@ -58,22 +43,4 @@ internal class AndroidViewTargetLocator(
private fun View.isViewTappable(): Boolean {
return isClickable && visibility == View.VISIBLE
}

private fun View.isViewScrollable(isAndroidXAvailable: Boolean): Boolean {
return (
(
isJetpackScrollingView(isAndroidXAvailable) ||
AbsListView::class.java.isAssignableFrom(this.javaClass) ||
ScrollView::class.java.isAssignableFrom(this.javaClass)
) &&
visibility == View.VISIBLE
)
}

private fun View.isJetpackScrollingView(isAndroidXAvailable: Boolean): Boolean {
if (!isAndroidXAvailable) {
return false
}
return ScrollingView::class.java.isAssignableFrom(this.javaClass)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,6 @@ public ViewTarget locate(

if (node.isPlaced() && layoutNodeBoundsContain(composeLayoutNodeBoundsHelper, node, targetPosition.component1(), targetPosition.component2())) {
boolean isClickable = false;
boolean isScrollable = false;

final List<ModifierInfo> modifiers = node.getModifierInfo();
for (ModifierInfo modifierInfo : modifiers) {
if (modifierInfo.getModifier() instanceof SemanticsModifier) {
Expand All @@ -81,18 +79,12 @@ public ViewTarget locate(
semanticsModifierCore.getSemanticsConfiguration();
for (Map.Entry<? extends SemanticsPropertyKey<?>, ?> entry : semanticsConfiguration) {
final @Nullable String key = entry.getKey().getName();
switch (key) {
case "ScrollBy":
isScrollable = true;
break;
case "OnClick":
isClickable = true;
break;
case "TestTag":
if (entry.getValue() instanceof String) {
lastKnownTag = (String) entry.getValue();
}
break;
if ("OnClick".equals(key)) {
isClickable = true;
} else if ("TestTag".equals(key)) {
if (entry.getValue() instanceof String) {
lastKnownTag = (String) entry.getValue();
}
}
}
} else {
Expand All @@ -101,20 +93,13 @@ public ViewTarget locate(
if ("androidx.compose.foundation.ClickableElement".equals(type)
|| "androidx.compose.foundation.CombinedClickableElement".equals(type)) {
isClickable = true;
} else if ("androidx.compose.foundation.ScrollingLayoutElement".equals(type)) {
isScrollable = true;
}
}
}

if (isClickable && targetType == ViewTarget.Type.Clickable) {
targetTag = lastKnownTag;
}
if (isScrollable && targetType == ViewTarget.Type.Scrollable) {
targetTag = lastKnownTag;
// skip any children for scrollable targets
break;
}
}
queue.addAll(node.getZSortedChildren().asMutableList());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ internal object ViewTargetLocators {
if (ComposeUtils.isComposeAvailable()) {
add(ComposeViewTargetLocator(logger))
}
add(AndroidViewTargetLocator(ComposeUtils.isAndroidXScrollViewAvailable(logger)))
add(AndroidViewTargetLocator())
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,12 @@ class ComposeUtils {
companion object {
private val loadClass = LoadClass()
private const val COMPOSE_CLASS_NAME = "androidx.compose.ui.node.Owner"
private const val SCROLLING_VIEW_CLASS_NAME = "androidx.core.view.ScrollingView"
private const val COMPOSE_GESTURE_LOCATOR_CLASS_NAME =
"com.amplitude.android.internal.locators.ComposeViewTargetLocator"

fun isComposeAvailable(logger: Logger? = null): Boolean {
return loadClass.isClassAvailable(COMPOSE_CLASS_NAME, logger) &&
loadClass.isClassAvailable(COMPOSE_GESTURE_LOCATOR_CLASS_NAME, logger)
}

fun isAndroidXScrollViewAvailable(logger: Logger? = null): Boolean {
return loadClass.isClassAvailable(SCROLLING_VIEW_CLASS_NAME, logger)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ class AutocaptureGestureListenerClickTest {
activity,
track,
logger,
listOf(AndroidViewTargetLocator(true)),
listOf(AndroidViewTargetLocator()),
)
}
}
Expand Down
Loading

0 comments on commit 4b834bd

Please sign in to comment.