mirror of
https://github.com/jrpie/Launcher.git
synced 2025-02-22 22:11:27 +01:00
add logging to collect debugging data
This commit is contained in:
parent
86528f4e27
commit
51ed03dd72
5 changed files with 119 additions and 16 deletions
|
@ -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>
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
|
@ -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
|
||||||
|
|
Loading…
Add table
Reference in a new issue