add logging to collect debugging data

This commit is contained in:
Josia Pietsch 2025-02-20 22:19:31 +01:00
parent 86528f4e27
commit 51ed03dd72
Signed by: jrpie
GPG key ID: E70B571D66986A2D
5 changed files with 119 additions and 16 deletions

View file

@ -1,3 +1,3 @@
<resources>
<string name="app_name" translatable="false">μLauncher [debug]</string>
<string name="app_name" translatable="false">μLauncher [gesture detection debug]</string>
</resources>

View file

@ -16,6 +16,7 @@ import de.jrpie.android.launcher.R
import de.jrpie.android.launcher.actions.Action
import de.jrpie.android.launcher.actions.Gesture
import de.jrpie.android.launcher.actions.LauncherAction
import de.jrpie.android.launcher.copyToClipboard
import de.jrpie.android.launcher.databinding.HomeBinding
import de.jrpie.android.launcher.preferences.LauncherPreferences
import de.jrpie.android.launcher.ui.tutorial.TutorialActivity
@ -62,16 +63,19 @@ class HomeActivity : UIObject, AppCompatActivity() {
val width = displayMetrics.widthPixels
val height = displayMetrics.heightPixels
// Initialise layout
binding = HomeBinding.inflate(layoutInflater)
setContentView(binding.root)
touchGestureDetector = TouchGestureDetector(
this,
width,
height,
LauncherPreferences.enabled_gestures().edgeSwipeEdgeWidth() / 100f
)
// Initialise layout
binding = HomeBinding.inflate(layoutInflater)
setContentView(binding.root)
) { s ->
binding.logOutput.text = s
}
// Handle back key / gesture on Android 13+, cf. onKeyDown()
@ -86,6 +90,12 @@ class HomeActivity : UIObject, AppCompatActivity() {
LauncherAction.SETTINGS.invoke(this)
}
binding.buttonCopyLog.setOnClickListener {
copyToClipboard(this,
touchGestureDetector.log_output)
}
}

View file

@ -1,20 +1,26 @@
package de.jrpie.android.launcher.ui
import android.content.Context
import android.util.Log
import android.view.MotionEvent
import android.view.ViewConfiguration
import de.jrpie.android.launcher.actions.Gesture
import de.jrpie.android.launcher.preferences.LauncherPreferences
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import kotlin.math.abs
import kotlin.math.max
import kotlin.math.min
import kotlin.math.sqrt
import kotlin.math.tan
class TouchGestureDetector(
private val context: Context,
val width: Int,
val height: Int,
var edgeWidth: Float
var edgeWidth: Float,
val onLog: (String) -> Unit
) {
private val ANGULAR_THRESHOLD = tan(Math.PI / 6)
private val TOUCH_SLOP: Int
@ -27,26 +33,51 @@ class TouchGestureDetector(
private val MIN_TRIANGLE_HEIGHT = 250
var log_output = ""
private var log_output_ui = ""
private fun log(s: String, ui:Boolean = true) {
Log.i("Gesture Detection", s)
log_output += "$s"
if (ui) {
var lines = 50
log_output_ui = (log_output_ui + "$s").takeLastWhile {
if (it == '\n') {
lines--
}
lines > 0
}
onLog(log_output_ui)
}
}
@Serializable
data class Vector(val x: Float, val y: Float) {
fun absSquared(): Float {
return this.x * this.x + this.y * this.y
}
fun plus(vector: Vector): Vector {
return Vector(this.x + vector.x, this.y + vector.y)
}
fun max(other: Vector): Vector {
return Vector(max(this.x, other.x), max(this.y, other.y))
}
fun min(other: Vector): Vector {
return Vector(min(this.x, other.x), min(this.y, other.y))
}
operator fun minus(vector: Vector): Vector {
return Vector(this.x - vector.x, this.y - vector.y)
}
}
@Serializable
class PointerPath(
val number: Int,
val start: Vector,
@ -57,15 +88,18 @@ class TouchGestureDetector(
fun sizeSquared(): Float {
return (max - min).absSquared()
}
fun getDirection(): Vector {
return last - start
}
fun update(vector: Vector) {
min = min.min(vector)
max = max.max(vector)
last = vector
}
}
private fun PointerPath.isTap(): Boolean {
return sizeSquared() < TOUCH_SLOP_SQUARE
}
@ -80,6 +114,14 @@ class TouchGestureDetector(
LONG_PRESS_TIMEOUT = ViewConfiguration.getLongPressTimeout()
TAP_TIMEOUT = ViewConfiguration.getTapTimeout()
DOUBLE_TAP_TIMEOUT = ViewConfiguration.getDoubleTapTimeout()
log(
"TOUCH_SLOP: $TOUCH_SLOP\n" +
"DOUBLE_TAP_SLOP: $DOUBLE_TAP_SLOP\n" +
"LONG_PRESS_TIMEOUT: $LONG_PRESS_TIMEOUT\n" +
"TAP_TIMEOUT: $TAP_TIMEOUT\n" +
"DOUBLE_TAP_TIMEOUT: $DOUBLE_TAP_TIMEOUT\n======================\n"
)
}
private var paths = HashMap<Int, PointerPath>()
@ -92,6 +134,7 @@ class TouchGestureDetector(
(0..<event.pointerCount).associateBy { event.getPointerId(it) }
if (event.actionMasked == MotionEvent.ACTION_DOWN) {
log("\n========================\nNew gesture")
paths = HashMap()
}
@ -144,8 +187,11 @@ class TouchGestureDetector(
}
private fun classifyPaths(paths: Map<Int, PointerPath>, timeStart: Long, timeEnd: Long) {
log(" - gesture complete")
log("\npaths = ${paths.entries.map { (x,y) -> "$x: ${Json.encodeToString(y)}"}}", false)
val duration = timeEnd - timeStart
val pointerCount = paths.entries.size
log("\nDuration: $duration, pointers: $pointerCount")
if (paths.entries.isEmpty()) {
return
}
@ -155,41 +201,63 @@ class TouchGestureDetector(
// Ignore swipes at the very top, since this interferes with the status bar.
// TODO: replace 100px by sensible dp value (e.g. twice the height of the status bar)
if (paths.entries.any { it.value.start.y < 100 }) {
log("\nToo close to the top")
return
}
if (pointerCount == 1 && mainPointerPath.isTap()) {
// detect taps
log("\nSlop: ${sqrt(mainPointerPath.sizeSquared())} - click")
log("\nTime since last click: ${timeStart - lastTappedTime}")
log(
"\nDistance to last click: ${
sqrt(
(mainPointerPath.last - (lastTappedLocation ?: Vector(
Float.NEGATIVE_INFINITY,
Float.NEGATIVE_INFINITY
))).absSquared()
.toDouble()
)
}"
)
if (duration in 0..TAP_TIMEOUT) {
if (timeStart - lastTappedTime < DOUBLE_TAP_TIMEOUT &&
lastTappedLocation?.let {
(mainPointerPath.last - it).absSquared() < DOUBLE_TAP_SLOP_SQUARE} == true
(mainPointerPath.last - it).absSquared() < DOUBLE_TAP_SLOP_SQUARE
} == true
) {
log("\nDouble click detected")
Gesture.DOUBLE_CLICK.invoke(context)
} else {
log("\nNot a double click: last tap too long ago")
lastTappedTime = timeEnd
lastTappedLocation = mainPointerPath.last
}
} else if (duration > LONG_PRESS_TIMEOUT) {
// TODO: Don't wait until the finger is lifted.
// Instead set a timer to start long click as soon as LONG_PRESS_TIMEOUT is reached
log("\nLong click detected")
Gesture.LONG_CLICK.invoke(context)
}
} else {
log("\nSlop: ${sqrt(mainPointerPath.sizeSquared())} - not a click")
// detect swipes
val doubleActions = LauncherPreferences.enabled_gestures().doubleSwipe()
val edgeActions = LauncherPreferences.enabled_gestures().edgeSwipe()
var gesture = getGestureForDirection(mainPointerPath.getDirection())
log("\nbase gesture: ${gesture}")
if (doubleActions && pointerCount > 1) {
if (paths.entries.any { getGestureForDirection(it.value.getDirection()) != gesture }) {
// the directions of the pointers don't match
log("\ndirections of the pointers don't match")
return
}
gesture = gesture?.let(Gesture::getDoubleVariant)
log(", double variant: ${gesture}")
}
// detect triangles
@ -203,6 +271,7 @@ class TouchGestureDetector(
gesture = Gesture.SWIPE_SMALLER
}
}
Gesture.SWIPE_UP -> {
if (startEndMax.x + MIN_TRIANGLE_HEIGHT < mainPointerPath.max.x) {
gesture = Gesture.SWIPE_LARGER_REVERSE
@ -210,6 +279,7 @@ class TouchGestureDetector(
gesture = Gesture.SWIPE_SMALLER_REVERSE
}
}
Gesture.SWIPE_RIGHT -> {
if (startEndMax.y + MIN_TRIANGLE_HEIGHT < mainPointerPath.max.y) {
gesture = Gesture.SWIPE_V
@ -217,6 +287,7 @@ class TouchGestureDetector(
gesture = Gesture.SWIPE_LAMBDA
}
}
Gesture.SWIPE_LEFT -> {
if (startEndMax.y + MIN_TRIANGLE_HEIGHT < mainPointerPath.max.y) {
gesture = Gesture.SWIPE_V_REVERSE
@ -224,8 +295,10 @@ class TouchGestureDetector(
gesture = Gesture.SWIPE_LAMBDA_REVERSE
}
}
else -> {}
}
log(", v-variant: $gesture")
if (edgeActions) {
if (mainPointerPath.max.x < edgeWidth * width) {
@ -240,10 +313,12 @@ class TouchGestureDetector(
gesture = gesture?.getEdgeVariant(Gesture.Edge.BOTTOM)
}
}
log(", edge-variant: $gesture")
if (timeStart - lastTappedTime < 2 * DOUBLE_TAP_TIMEOUT) {
gesture = gesture?.getTapComboVariant()
}
log(", tap-combo-variant: $gesture")
gesture?.invoke(context)
}
}

View file

@ -48,4 +48,22 @@
app:layout_constraintEnd_toEndOf="parent"
app:srcCompat="@drawable/baseline_settings_24"/>
<TextView
android:id="@+id/log_output"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:ellipsize="start"
android:gravity="bottom"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
tools:text="Log\nLog\nLog" />
<Button
android:id="@+id/button_copy_log"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Copy log to clipboard"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name" translatable="false">μLauncher</string>
<string name="app_name" translatable="false">μLauncher [gesture detection]</string>
<!--
-
- Settings