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

View file

@ -1,20 +1,26 @@
package de.jrpie.android.launcher.ui package de.jrpie.android.launcher.ui
import android.content.Context import android.content.Context
import android.util.Log
import android.view.MotionEvent import android.view.MotionEvent
import android.view.ViewConfiguration import android.view.ViewConfiguration
import de.jrpie.android.launcher.actions.Gesture import de.jrpie.android.launcher.actions.Gesture
import de.jrpie.android.launcher.preferences.LauncherPreferences 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.abs
import kotlin.math.max import kotlin.math.max
import kotlin.math.min import kotlin.math.min
import kotlin.math.sqrt
import kotlin.math.tan import kotlin.math.tan
class TouchGestureDetector( class TouchGestureDetector(
private val context: Context, private val context: Context,
val width: Int, val width: Int,
val height: Int, val height: Int,
var edgeWidth: Float var edgeWidth: Float,
val onLog: (String) -> Unit
) { ) {
private val ANGULAR_THRESHOLD = tan(Math.PI / 6) private val ANGULAR_THRESHOLD = tan(Math.PI / 6)
private val TOUCH_SLOP: Int private val TOUCH_SLOP: Int
@ -27,26 +33,51 @@ class TouchGestureDetector(
private val MIN_TRIANGLE_HEIGHT = 250 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) { data class Vector(val x: Float, val y: Float) {
fun absSquared(): Float { fun absSquared(): Float {
return this.x * this.x + this.y * this.y return this.x * this.x + this.y * this.y
} }
fun plus(vector: Vector): Vector { fun plus(vector: Vector): Vector {
return Vector(this.x + vector.x, this.y + vector.y) return Vector(this.x + vector.x, this.y + vector.y)
} }
fun max(other: Vector): Vector { fun max(other: Vector): Vector {
return Vector(max(this.x, other.x), max(this.y, other.y)) return Vector(max(this.x, other.x), max(this.y, other.y))
} }
fun min(other: Vector): Vector { fun min(other: Vector): Vector {
return Vector(min(this.x, other.x), min(this.y, other.y)) return Vector(min(this.x, other.x), min(this.y, other.y))
} }
operator fun minus(vector: Vector): Vector { operator fun minus(vector: Vector): Vector {
return Vector(this.x - vector.x, this.y - vector.y) return Vector(this.x - vector.x, this.y - vector.y)
} }
} }
@Serializable
class PointerPath( class PointerPath(
val number: Int, val number: Int,
val start: Vector, val start: Vector,
@ -57,15 +88,18 @@ class TouchGestureDetector(
fun sizeSquared(): Float { fun sizeSquared(): Float {
return (max - min).absSquared() return (max - min).absSquared()
} }
fun getDirection(): Vector { fun getDirection(): Vector {
return last - start return last - start
} }
fun update(vector: Vector) { fun update(vector: Vector) {
min = min.min(vector) min = min.min(vector)
max = max.max(vector) max = max.max(vector)
last = vector last = vector
} }
} }
private fun PointerPath.isTap(): Boolean { private fun PointerPath.isTap(): Boolean {
return sizeSquared() < TOUCH_SLOP_SQUARE return sizeSquared() < TOUCH_SLOP_SQUARE
} }
@ -80,6 +114,14 @@ class TouchGestureDetector(
LONG_PRESS_TIMEOUT = ViewConfiguration.getLongPressTimeout() LONG_PRESS_TIMEOUT = ViewConfiguration.getLongPressTimeout()
TAP_TIMEOUT = ViewConfiguration.getTapTimeout() TAP_TIMEOUT = ViewConfiguration.getTapTimeout()
DOUBLE_TAP_TIMEOUT = ViewConfiguration.getDoubleTapTimeout() 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>() private var paths = HashMap<Int, PointerPath>()
@ -92,12 +134,13 @@ class TouchGestureDetector(
(0..<event.pointerCount).associateBy { event.getPointerId(it) } (0..<event.pointerCount).associateBy { event.getPointerId(it) }
if (event.actionMasked == MotionEvent.ACTION_DOWN) { if (event.actionMasked == MotionEvent.ACTION_DOWN) {
log("\n========================\nNew gesture")
paths = HashMap() paths = HashMap()
} }
// add new pointers // add new pointers
for(i in 0..<event.pointerCount){ for (i in 0..<event.pointerCount) {
if(paths.containsKey(event.getPointerId(i))) { if (paths.containsKey(event.getPointerId(i))) {
continue continue
} }
val index = pointerIdToIndex[i] ?: continue val index = pointerIdToIndex[i] ?: continue
@ -144,8 +187,11 @@ class TouchGestureDetector(
} }
private fun classifyPaths(paths: Map<Int, PointerPath>, timeStart: Long, timeEnd: Long) { 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 duration = timeEnd - timeStart
val pointerCount = paths.entries.size val pointerCount = paths.entries.size
log("\nDuration: $duration, pointers: $pointerCount")
if (paths.entries.isEmpty()) { if (paths.entries.isEmpty()) {
return return
} }
@ -155,41 +201,63 @@ class TouchGestureDetector(
// Ignore swipes at the very top, since this interferes with the status bar. // 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) // 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 }) { if (paths.entries.any { it.value.start.y < 100 }) {
log("\nToo close to the top")
return return
} }
if (pointerCount == 1 && mainPointerPath.isTap()) { if (pointerCount == 1 && mainPointerPath.isTap()) {
// detect taps // 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 (duration in 0..TAP_TIMEOUT) {
if (timeStart - lastTappedTime < DOUBLE_TAP_TIMEOUT && if (timeStart - lastTappedTime < DOUBLE_TAP_TIMEOUT &&
lastTappedLocation?.let { 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) Gesture.DOUBLE_CLICK.invoke(context)
} else { } else {
log("\nNot a double click: last tap too long ago")
lastTappedTime = timeEnd lastTappedTime = timeEnd
lastTappedLocation = mainPointerPath.last lastTappedLocation = mainPointerPath.last
} }
} else if (duration > LONG_PRESS_TIMEOUT) { } else if (duration > LONG_PRESS_TIMEOUT) {
// TODO: Don't wait until the finger is lifted. // 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 // 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) Gesture.LONG_CLICK.invoke(context)
} }
} else { } else {
log("\nSlop: ${sqrt(mainPointerPath.sizeSquared())} - not a click")
// detect swipes // detect swipes
val doubleActions = LauncherPreferences.enabled_gestures().doubleSwipe() val doubleActions = LauncherPreferences.enabled_gestures().doubleSwipe()
val edgeActions = LauncherPreferences.enabled_gestures().edgeSwipe() val edgeActions = LauncherPreferences.enabled_gestures().edgeSwipe()
var gesture = getGestureForDirection(mainPointerPath.getDirection()) var gesture = getGestureForDirection(mainPointerPath.getDirection())
log("\nbase gesture: ${gesture}")
if (doubleActions && pointerCount > 1) { if (doubleActions && pointerCount > 1) {
if (paths.entries.any { getGestureForDirection(it.value.getDirection()) != gesture }) { if (paths.entries.any { getGestureForDirection(it.value.getDirection()) != gesture }) {
// the directions of the pointers don't match // the directions of the pointers don't match
log("\ndirections of the pointers don't match")
return return
} }
gesture = gesture?.let(Gesture::getDoubleVariant) gesture = gesture?.let(Gesture::getDoubleVariant)
log(", double variant: ${gesture}")
} }
// detect triangles // detect triangles
@ -197,35 +265,40 @@ class TouchGestureDetector(
val startEndMax = mainPointerPath.start.max(mainPointerPath.last) val startEndMax = mainPointerPath.start.max(mainPointerPath.last)
when (gesture) { when (gesture) {
Gesture.SWIPE_DOWN -> { Gesture.SWIPE_DOWN -> {
if(startEndMax.x + MIN_TRIANGLE_HEIGHT < mainPointerPath.max.x) { if (startEndMax.x + MIN_TRIANGLE_HEIGHT < mainPointerPath.max.x) {
gesture = Gesture.SWIPE_LARGER gesture = Gesture.SWIPE_LARGER
} else if (startEndMin.x - MIN_TRIANGLE_HEIGHT > mainPointerPath.min.x) { } else if (startEndMin.x - MIN_TRIANGLE_HEIGHT > mainPointerPath.min.x) {
gesture = Gesture.SWIPE_SMALLER gesture = Gesture.SWIPE_SMALLER
} }
} }
Gesture.SWIPE_UP -> { Gesture.SWIPE_UP -> {
if(startEndMax.x + MIN_TRIANGLE_HEIGHT < mainPointerPath.max.x) { if (startEndMax.x + MIN_TRIANGLE_HEIGHT < mainPointerPath.max.x) {
gesture = Gesture.SWIPE_LARGER_REVERSE gesture = Gesture.SWIPE_LARGER_REVERSE
} else if (startEndMin.x - MIN_TRIANGLE_HEIGHT > mainPointerPath.min.x) { } else if (startEndMin.x - MIN_TRIANGLE_HEIGHT > mainPointerPath.min.x) {
gesture = Gesture.SWIPE_SMALLER_REVERSE gesture = Gesture.SWIPE_SMALLER_REVERSE
} }
} }
Gesture.SWIPE_RIGHT -> { Gesture.SWIPE_RIGHT -> {
if(startEndMax.y + MIN_TRIANGLE_HEIGHT < mainPointerPath.max.y) { if (startEndMax.y + MIN_TRIANGLE_HEIGHT < mainPointerPath.max.y) {
gesture = Gesture.SWIPE_V gesture = Gesture.SWIPE_V
} else if (startEndMin.y - MIN_TRIANGLE_HEIGHT > mainPointerPath.min.y) { } else if (startEndMin.y - MIN_TRIANGLE_HEIGHT > mainPointerPath.min.y) {
gesture = Gesture.SWIPE_LAMBDA gesture = Gesture.SWIPE_LAMBDA
} }
} }
Gesture.SWIPE_LEFT -> { Gesture.SWIPE_LEFT -> {
if(startEndMax.y + MIN_TRIANGLE_HEIGHT < mainPointerPath.max.y) { if (startEndMax.y + MIN_TRIANGLE_HEIGHT < mainPointerPath.max.y) {
gesture = Gesture.SWIPE_V_REVERSE gesture = Gesture.SWIPE_V_REVERSE
} else if (startEndMin.y - MIN_TRIANGLE_HEIGHT > mainPointerPath.min.y) { } else if (startEndMin.y - MIN_TRIANGLE_HEIGHT > mainPointerPath.min.y) {
gesture = Gesture.SWIPE_LAMBDA_REVERSE gesture = Gesture.SWIPE_LAMBDA_REVERSE
} }
} }
else -> { }
else -> {}
} }
log(", v-variant: $gesture")
if (edgeActions) { if (edgeActions) {
if (mainPointerPath.max.x < edgeWidth * width) { if (mainPointerPath.max.x < edgeWidth * width) {
@ -240,10 +313,12 @@ class TouchGestureDetector(
gesture = gesture?.getEdgeVariant(Gesture.Edge.BOTTOM) gesture = gesture?.getEdgeVariant(Gesture.Edge.BOTTOM)
} }
} }
log(", edge-variant: $gesture")
if (timeStart - lastTappedTime < 2 * DOUBLE_TAP_TIMEOUT) { if (timeStart - lastTappedTime < 2 * DOUBLE_TAP_TIMEOUT) {
gesture = gesture?.getTapComboVariant() gesture = gesture?.getTapComboVariant()
} }
log(", tap-combo-variant: $gesture")
gesture?.invoke(context) gesture?.invoke(context)
} }
} }

View file

@ -48,4 +48,22 @@
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:srcCompat="@drawable/baseline_settings_24"/> 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> </androidx.constraintlayout.widget.ConstraintLayout>

View file

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