Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6b11e76504 | ||
|
|
60769980cd | ||
|
|
c2ee5a5581 | ||
|
|
465dec356d | ||
|
|
1fc63aef8d | ||
|
|
e9686f6cb3 | ||
|
|
d9b55c6811 | ||
|
|
0956bbc4d7 |
@@ -4,3 +4,4 @@ build/
|
|||||||
app/build/
|
app/build/
|
||||||
local/
|
local/
|
||||||
local.properties
|
local.properties
|
||||||
|
*.log
|
||||||
|
|||||||
@@ -13,16 +13,33 @@ It lets you choose what should happen when Back, Home, or Recents is pressed or
|
|||||||
- Kill the foreground app.
|
- Kill the foreground app.
|
||||||
- Toggle auto-rotate.
|
- Toggle auto-rotate.
|
||||||
- Toggle the flashlight.
|
- Toggle the flashlight.
|
||||||
|
- Add optional side-arrow buttons for moving the text cursor left and right.
|
||||||
|
- Configure side-arrow long-press behavior, including repeated cursor movement.
|
||||||
- Test long-press durations from the settings screen.
|
- Test long-press durations from the settings screen.
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
- Android device using three-button navigation.
|
- Android device using three-button navigation.
|
||||||
- LSPosed.
|
- LSPosed with Xposed API 101 or newer.
|
||||||
- Xposed API 101 or newer.
|
- Android 14 / API 34 or newer.
|
||||||
- The module enabled for System UI.
|
- The module enabled for the relevant navigation packages.
|
||||||
|
|
||||||
This has only been tested on a Samsung Galaxy Z Fold5 running One UI 8.0 / Android 16. Compatibility may vary between Android versions and vendor System UI implementations.
|
The module declares scope entries for:
|
||||||
|
|
||||||
|
- `android` and `system`, used for privileged actions such as killing the foreground app.
|
||||||
|
- `com.android.systemui`, used for phone navigation bars.
|
||||||
|
- `com.android.launcher3` and `com.google.android.apps.nexuslauncher`, used for AOSP/Google taskbar navigation.
|
||||||
|
- `com.sec.android.app.launcher` and `com.samsung.systemui.navillera`, used for Samsung/One UI navigation.
|
||||||
|
|
||||||
|
## Tested On
|
||||||
|
|
||||||
|
- Samsung Galaxy Z Fold5 running One UI 8.0 / Android 16 with Magisk and LSPosed.
|
||||||
|
- AOSP emulator API 34 / Android 14 with Magisk and LSPosed.
|
||||||
|
- AOSP emulator API 35 / Android 15 with Magisk and LSPosed.
|
||||||
|
- AOSP emulator API 36 / Android 16 with Magisk and LSPosed.
|
||||||
|
- Android 16 Google APIs emulator with Magisk and LSPosed.
|
||||||
|
|
||||||
|
The emulator tests covered button press overrides, long-press overrides with custom duration, side-arrow visibility, side-arrow tinting, enabling/disabling side arrows, and side-arrow long-press repeat behavior where supported by the emulator navigation implementation. Actual device testing with others than the Galaxy Z Fold 5 has not been done. Compatibility may vary between Android versions and vendor System UI / launcher implementations.
|
||||||
|
|
||||||
## AI Assistance
|
## AI Assistance
|
||||||
|
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ android {
|
|||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "se.ajpanton.navbuttons"
|
applicationId = "se.ajpanton.navbuttons"
|
||||||
minSdk = 35
|
minSdk = 34
|
||||||
targetSdk = 35
|
targetSdk = 35
|
||||||
versionCode = project.findProperty("nbVersionCode")?.toString()?.toIntOrNull() ?: 1777070659
|
versionCode = project.findProperty("nbVersionCode")?.toString()?.toIntOrNull() ?: 1777070659
|
||||||
versionName = project.findProperty("nbVersionName")?.toString() ?: "0.3"
|
versionName = project.findProperty("nbVersionName")?.toString() ?: "0.3"
|
||||||
|
|||||||
@@ -3,6 +3,11 @@
|
|||||||
xmlns:tools="http://schemas.android.com/tools">
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.KILL_BACKGROUND_PROCESSES" />
|
<uses-permission android:name="android.permission.KILL_BACKGROUND_PROCESSES" />
|
||||||
|
|
||||||
|
<queries>
|
||||||
|
<package android:name="org.lsposed.manager" />
|
||||||
|
</queries>
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import android.view.ViewConfiguration
|
|||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import io.github.libxposed.service.XposedService
|
import io.github.libxposed.service.XposedService
|
||||||
import io.github.libxposed.service.XposedServiceHelper
|
import io.github.libxposed.service.XposedServiceHelper
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
enum class NavButtonId(
|
enum class NavButtonId(
|
||||||
val preferencePrefix: String,
|
val preferencePrefix: String,
|
||||||
@@ -39,6 +40,22 @@ enum class NavButtonAction(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum class SideArrowLongPressAction(
|
||||||
|
val storageValue: String,
|
||||||
|
@get:StringRes val labelRes: Int,
|
||||||
|
) {
|
||||||
|
NOTHING("nothing", R.string.side_arrow_long_press_nothing),
|
||||||
|
SINGLE_STEP("single_step", R.string.side_arrow_long_press_single_step),
|
||||||
|
MULTIPLE_STEPS("multiple_steps", R.string.side_arrow_long_press_multiple_steps),
|
||||||
|
;
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun fromStorageValue(value: String?): SideArrowLongPressAction {
|
||||||
|
return entries.firstOrNull { it.storageValue == value } ?: MULTIPLE_STEPS
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
data class NavButtonConfig(
|
data class NavButtonConfig(
|
||||||
val pressAction: NavButtonAction,
|
val pressAction: NavButtonAction,
|
||||||
val longPressAction: NavButtonAction,
|
val longPressAction: NavButtonAction,
|
||||||
@@ -52,6 +69,7 @@ class NavButtonSettingsStore(context: Context) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
ensurePrivilegedActionToken()
|
||||||
registerRemotePreferencesListener(localPreferences)
|
registerRemotePreferencesListener(localPreferences)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,12 +109,78 @@ class NavButtonSettingsStore(context: Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun isSideArrowsEnabled(): Boolean {
|
||||||
|
return currentPreferences().getBoolean(KEY_SIDE_ARROWS_ENABLED, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setSideArrowsEnabled(enabled: Boolean) {
|
||||||
|
persistChanges {
|
||||||
|
putBoolean(KEY_SIDE_ARROWS_ENABLED, enabled)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getSideArrowLongPressAction(): SideArrowLongPressAction {
|
||||||
|
return SideArrowLongPressAction.fromStorageValue(
|
||||||
|
currentPreferences().getString(KEY_SIDE_ARROW_LONG_PRESS_ACTION, null),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setSideArrowLongPressAction(action: SideArrowLongPressAction) {
|
||||||
|
persistChanges {
|
||||||
|
putString(KEY_SIDE_ARROW_LONG_PRESS_ACTION, action.storageValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getSideArrowRepeatStepsPerSecond(): Float {
|
||||||
|
return sanitizeSideArrowRepeatSpeed(
|
||||||
|
currentPreferences().getFloat(
|
||||||
|
KEY_SIDE_ARROW_REPEAT_STEPS_PER_SECOND,
|
||||||
|
DEFAULT_SIDE_ARROW_REPEAT_STEPS_PER_SECOND,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setSideArrowRepeatStepsPerSecond(stepsPerSecond: Float) {
|
||||||
|
persistChanges {
|
||||||
|
putFloat(
|
||||||
|
KEY_SIDE_ARROW_REPEAT_STEPS_PER_SECOND,
|
||||||
|
sanitizeSideArrowRepeatSpeed(stepsPerSecond),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getSideArrowLongPressDurationMs(): Int {
|
||||||
|
return sanitizeDuration(
|
||||||
|
currentPreferences().getInt(
|
||||||
|
KEY_SIDE_ARROW_LONG_PRESS_DURATION_MS,
|
||||||
|
defaultLongPressDurationMs(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setSideArrowLongPressDurationMs(durationMs: Int) {
|
||||||
|
persistChanges {
|
||||||
|
putInt(KEY_SIDE_ARROW_LONG_PRESS_DURATION_MS, sanitizeDuration(durationMs))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun defaultLongPressDurationMs(): Int {
|
fun defaultLongPressDurationMs(): Int {
|
||||||
return sanitizeDuration(ViewConfiguration.getLongPressTimeout())
|
return sanitizeDuration(ViewConfiguration.getLongPressTimeout())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun sanitizeDuration(durationMs: Int): Int {
|
fun sanitizeDuration(durationMs: Int): Int {
|
||||||
return durationMs.coerceIn(MIN_LONG_PRESS_DURATION_MS, MAX_LONG_PRESS_DURATION_MS)
|
return durationMs.coerceIn(MIN_DURATION_MS, MAX_DURATION_MS)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun sanitizeSideArrowRepeatSpeed(stepsPerSecond: Float): Float {
|
||||||
|
return if (stepsPerSecond.isFinite()) {
|
||||||
|
stepsPerSecond.coerceIn(
|
||||||
|
MIN_SIDE_ARROW_REPEAT_STEPS_PER_SECOND,
|
||||||
|
MAX_SIDE_ARROW_REPEAT_STEPS_PER_SECOND,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
DEFAULT_SIDE_ARROW_REPEAT_STEPS_PER_SECOND
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun currentPreferences(): SharedPreferences {
|
private fun currentPreferences(): SharedPreferences {
|
||||||
@@ -115,6 +199,15 @@ class NavButtonSettingsStore(context: Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun ensurePrivilegedActionToken() {
|
||||||
|
if (localPreferences.contains(KEY_PRIVILEGED_ACTION_TOKEN)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeToPreferences(localPreferences, System.currentTimeMillis()) {
|
||||||
|
putString(KEY_PRIVILEGED_ACTION_TOKEN, UUID.randomUUID().toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun writeToPreferences(
|
private fun writeToPreferences(
|
||||||
preferences: SharedPreferences,
|
preferences: SharedPreferences,
|
||||||
updatedAtMs: Long,
|
updatedAtMs: Long,
|
||||||
@@ -129,18 +222,27 @@ class NavButtonSettingsStore(context: Context) {
|
|||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "NavButtons"
|
private const val TAG = "NavButtons"
|
||||||
const val PREFERENCES_NAME = "nav_button_settings"
|
const val PREFERENCES_NAME = "nav_button_settings"
|
||||||
|
const val ACTION_SETTINGS_CHANGED = "se.ajpanton.navbuttons.SETTINGS_CHANGED"
|
||||||
|
const val EXTRA_SIDE_ARROWS_ENABLED = "side_arrows_enabled"
|
||||||
const val KEY_PRESS_ACTION = "press_action"
|
const val KEY_PRESS_ACTION = "press_action"
|
||||||
const val KEY_LONG_PRESS_ACTION = "long_press_action"
|
const val KEY_LONG_PRESS_ACTION = "long_press_action"
|
||||||
const val KEY_LONG_PRESS_DURATION_MS = "long_press_duration_ms"
|
const val KEY_LONG_PRESS_DURATION_MS = "long_press_duration_ms"
|
||||||
|
const val KEY_SIDE_ARROWS_ENABLED = "side_arrows_enabled"
|
||||||
|
const val KEY_SIDE_ARROW_LONG_PRESS_ACTION = "side_arrow_long_press_action"
|
||||||
|
const val KEY_SIDE_ARROW_REPEAT_STEPS_PER_SECOND = "side_arrow_repeat_steps_per_second"
|
||||||
|
const val KEY_SIDE_ARROW_LONG_PRESS_DURATION_MS = "side_arrow_long_press_duration_ms"
|
||||||
|
const val KEY_PRIVILEGED_ACTION_TOKEN = "privileged_action_token"
|
||||||
private const val KEY_LAST_UPDATED_AT_MS = "last_updated_at_ms"
|
private const val KEY_LAST_UPDATED_AT_MS = "last_updated_at_ms"
|
||||||
|
|
||||||
const val MIN_LONG_PRESS_DURATION_MS = 100
|
|
||||||
const val MAX_LONG_PRESS_DURATION_MS = 10_000
|
|
||||||
const val MIN_DURATION_MS = 100
|
const val MIN_DURATION_MS = 100
|
||||||
const val MAX_DURATION_MS = 10_000
|
const val MAX_DURATION_MS = 10_000
|
||||||
const val DURATION_STEP_MS = 100
|
const val DURATION_STEP_MS = 100
|
||||||
const val HOLD_REPEAT_START_DELAY_MS = 700L
|
const val HOLD_REPEAT_START_DELAY_MS = 700L
|
||||||
const val HOLD_REPEAT_INTERVAL_MS = 100L
|
const val HOLD_REPEAT_INTERVAL_MS = 100L
|
||||||
|
const val DEFAULT_SIDE_ARROW_REPEAT_STEPS_PER_SECOND = 20f
|
||||||
|
const val MIN_SIDE_ARROW_REPEAT_STEPS_PER_SECOND = 0.1f
|
||||||
|
const val MAX_SIDE_ARROW_REPEAT_STEPS_PER_SECOND = 100f
|
||||||
|
const val SIDE_ARROW_REPEAT_SPEED_STEP = 1f
|
||||||
|
|
||||||
fun pressActionKey(buttonId: NavButtonId): String = key(buttonId, KEY_PRESS_ACTION)
|
fun pressActionKey(buttonId: NavButtonId): String = key(buttonId, KEY_PRESS_ACTION)
|
||||||
|
|
||||||
@@ -161,13 +263,15 @@ class NavButtonSettingsStore(context: Context) {
|
|||||||
pressActionKey(buttonId),
|
pressActionKey(buttonId),
|
||||||
longPressActionKey(buttonId),
|
longPressActionKey(buttonId),
|
||||||
)
|
)
|
||||||
}
|
} + KEY_SIDE_ARROW_LONG_PRESS_ACTION
|
||||||
}
|
}
|
||||||
|
|
||||||
private val durationKeys by lazy(LazyThreadSafetyMode.NONE) {
|
private val durationKeys by lazy(LazyThreadSafetyMode.NONE) {
|
||||||
NavButtonId.entries.map(::longPressDurationKey)
|
NavButtonId.entries.map(::longPressDurationKey) + KEY_SIDE_ARROW_LONG_PRESS_DURATION_MS
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val floatKeys = listOf(KEY_SIDE_ARROW_REPEAT_STEPS_PER_SECOND)
|
||||||
|
|
||||||
private fun key(buttonId: NavButtonId, suffix: String): String {
|
private fun key(buttonId: NavButtonId, suffix: String): String {
|
||||||
return "${buttonId.preferencePrefix}_$suffix"
|
return "${buttonId.preferencePrefix}_$suffix"
|
||||||
}
|
}
|
||||||
@@ -220,7 +324,10 @@ class NavButtonSettingsStore(context: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun hasStoredConfig(preferences: SharedPreferences): Boolean {
|
private fun hasStoredConfig(preferences: SharedPreferences): Boolean {
|
||||||
return actionKeys.any(preferences::contains) || durationKeys.any(preferences::contains)
|
return actionKeys.any(preferences::contains) ||
|
||||||
|
durationKeys.any(preferences::contains) ||
|
||||||
|
floatKeys.any(preferences::contains) ||
|
||||||
|
preferences.contains(KEY_SIDE_ARROWS_ENABLED)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun copyPreferences(source: SharedPreferences, target: SharedPreferences) {
|
private fun copyPreferences(source: SharedPreferences, target: SharedPreferences) {
|
||||||
@@ -242,6 +349,33 @@ class NavButtonSettingsStore(context: Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (key in floatKeys) {
|
||||||
|
if (source.contains(key)) {
|
||||||
|
editor.putFloat(
|
||||||
|
key,
|
||||||
|
source.getFloat(key, DEFAULT_SIDE_ARROW_REPEAT_STEPS_PER_SECOND)
|
||||||
|
.coerceIn(
|
||||||
|
MIN_SIDE_ARROW_REPEAT_STEPS_PER_SECOND,
|
||||||
|
MAX_SIDE_ARROW_REPEAT_STEPS_PER_SECOND,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
editor.remove(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (source.contains(KEY_SIDE_ARROWS_ENABLED)) {
|
||||||
|
editor.putBoolean(KEY_SIDE_ARROWS_ENABLED, source.getBoolean(KEY_SIDE_ARROWS_ENABLED, false))
|
||||||
|
} else {
|
||||||
|
editor.remove(KEY_SIDE_ARROWS_ENABLED)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (source.contains(KEY_PRIVILEGED_ACTION_TOKEN)) {
|
||||||
|
editor.putString(KEY_PRIVILEGED_ACTION_TOKEN, source.getString(KEY_PRIVILEGED_ACTION_TOKEN, null))
|
||||||
|
} else {
|
||||||
|
editor.remove(KEY_PRIVILEGED_ACTION_TOKEN)
|
||||||
|
}
|
||||||
|
|
||||||
if (source.contains(KEY_LAST_UPDATED_AT_MS)) {
|
if (source.contains(KEY_LAST_UPDATED_AT_MS)) {
|
||||||
editor.putLong(KEY_LAST_UPDATED_AT_MS, source.getLong(KEY_LAST_UPDATED_AT_MS, 0L))
|
editor.putLong(KEY_LAST_UPDATED_AT_MS, source.getLong(KEY_LAST_UPDATED_AT_MS, 0L))
|
||||||
} else {
|
} else {
|
||||||
@@ -252,7 +386,7 @@ class NavButtonSettingsStore(context: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun sanitizeDuration(durationMs: Int): Int {
|
private fun sanitizeDuration(durationMs: Int): Int {
|
||||||
return durationMs.coerceIn(MIN_LONG_PRESS_DURATION_MS, MAX_LONG_PRESS_DURATION_MS)
|
return durationMs.coerceIn(MIN_DURATION_MS, MAX_DURATION_MS)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,7 @@
|
|||||||
package se.ajpanton.navbuttons
|
package se.ajpanton.navbuttons
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
@@ -13,10 +14,14 @@ import android.view.ViewGroup
|
|||||||
import android.widget.AdapterView
|
import android.widget.AdapterView
|
||||||
import android.widget.ArrayAdapter
|
import android.widget.ArrayAdapter
|
||||||
import android.widget.Button
|
import android.widget.Button
|
||||||
|
import android.widget.CheckBox
|
||||||
import android.widget.EditText
|
import android.widget.EditText
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import android.widget.Spinner
|
import android.widget.Spinner
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
import android.widget.Toast
|
||||||
|
import java.text.DecimalFormatSymbols
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
class SettingsActivity : Activity() {
|
class SettingsActivity : Activity() {
|
||||||
private val handler = Handler(Looper.getMainLooper())
|
private val handler = Handler(Looper.getMainLooper())
|
||||||
@@ -61,23 +66,36 @@ class SettingsActivity : Activity() {
|
|||||||
false,
|
false,
|
||||||
)
|
)
|
||||||
if (index > 0) {
|
if (index > 0) {
|
||||||
val marginTop = resources.getDimensionPixelSize(R.dimen.section_spacing)
|
applySectionTopMargin(sectionView)
|
||||||
val layoutParams = sectionView.layoutParams as? ViewGroup.MarginLayoutParams
|
|
||||||
if (layoutParams != null) {
|
|
||||||
layoutParams.topMargin = marginTop
|
|
||||||
} else {
|
|
||||||
sectionView.layoutParams = LinearLayout.LayoutParams(
|
|
||||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
|
||||||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
|
||||||
).apply {
|
|
||||||
topMargin = marginTop
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
NavButtonSectionController(sectionView, buttonId).bind()
|
NavButtonSectionController(sectionView, buttonId).bind()
|
||||||
sectionsContainer.addView(sectionView)
|
sectionsContainer.addView(sectionView)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val sideArrowsSectionView = inflater.inflate(
|
||||||
|
R.layout.item_side_arrows_section,
|
||||||
|
sectionsContainer,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
applySectionTopMargin(sideArrowsSectionView)
|
||||||
|
SideArrowsSectionController(sideArrowsSectionView).bind()
|
||||||
|
sectionsContainer.addView(sideArrowsSectionView)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun applySectionTopMargin(sectionView: View) {
|
||||||
|
val marginTop = resources.getDimensionPixelSize(R.dimen.section_spacing)
|
||||||
|
val layoutParams = sectionView.layoutParams as? ViewGroup.MarginLayoutParams
|
||||||
|
if (layoutParams != null) {
|
||||||
|
layoutParams.topMargin = marginTop
|
||||||
|
} else {
|
||||||
|
sectionView.layoutParams = LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||||
|
).apply {
|
||||||
|
topMargin = marginTop
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startTestPreview(durationMs: Int) {
|
private fun startTestPreview(durationMs: Int) {
|
||||||
@@ -96,6 +114,91 @@ class SettingsActivity : Activity() {
|
|||||||
previewOverlay.visibility = View.GONE
|
previewOverlay.visibility = View.GONE
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun attachRepeatButton(button: Button, action: () -> Unit) {
|
||||||
|
var repeated = false
|
||||||
|
val repeatRunnable = object : Runnable {
|
||||||
|
override fun run() {
|
||||||
|
repeated = true
|
||||||
|
action()
|
||||||
|
handler.postDelayed(this, NavButtonSettingsStore.HOLD_REPEAT_INTERVAL_MS)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
button.setOnClickListener {
|
||||||
|
if (!repeated) {
|
||||||
|
action()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
button.setOnTouchListener { _, event ->
|
||||||
|
when (event.actionMasked) {
|
||||||
|
MotionEvent.ACTION_DOWN -> {
|
||||||
|
repeated = false
|
||||||
|
handler.postDelayed(
|
||||||
|
repeatRunnable,
|
||||||
|
NavButtonSettingsStore.HOLD_REPEAT_START_DELAY_MS,
|
||||||
|
)
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
MotionEvent.ACTION_UP,
|
||||||
|
MotionEvent.ACTION_CANCEL,
|
||||||
|
-> {
|
||||||
|
handler.removeCallbacks(repeatRunnable)
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun attachTestButton(button: Button, durationProvider: () -> Int) {
|
||||||
|
button.setOnTouchListener { view, event ->
|
||||||
|
when (event.actionMasked) {
|
||||||
|
MotionEvent.ACTION_DOWN -> {
|
||||||
|
view.isPressed = true
|
||||||
|
startTestPreview(durationProvider())
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
MotionEvent.ACTION_UP,
|
||||||
|
MotionEvent.ACTION_CANCEL,
|
||||||
|
-> {
|
||||||
|
view.isPressed = false
|
||||||
|
stopTestPreview()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun <T> bindSpinner(
|
||||||
|
spinner: Spinner,
|
||||||
|
items: List<T>,
|
||||||
|
selectedItem: T,
|
||||||
|
labelRes: (T) -> Int,
|
||||||
|
onSelected: (T) -> Unit,
|
||||||
|
) {
|
||||||
|
val adapter = ArrayAdapter(
|
||||||
|
this,
|
||||||
|
R.layout.item_spinner_selected,
|
||||||
|
items.map { getString(labelRes(it)) },
|
||||||
|
)
|
||||||
|
adapter.setDropDownViewResource(R.layout.item_spinner_dropdown)
|
||||||
|
spinner.adapter = adapter
|
||||||
|
spinner.setSelection(items.indexOf(selectedItem).coerceAtLeast(0), false)
|
||||||
|
spinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
|
||||||
|
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
|
||||||
|
onSelected(items[position])
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onNothingSelected(parent: AdapterView<*>?) = Unit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private inner class NavButtonSectionController(
|
private inner class NavButtonSectionController(
|
||||||
sectionView: View,
|
sectionView: View,
|
||||||
private val buttonId: NavButtonId,
|
private val buttonId: NavButtonId,
|
||||||
@@ -130,7 +233,7 @@ class SettingsActivity : Activity() {
|
|||||||
applyDuration(config.longPressDurationMs, persist = false)
|
applyDuration(config.longPressDurationMs, persist = false)
|
||||||
bindDurationInput()
|
bindDurationInput()
|
||||||
bindStepButtons()
|
bindStepButtons()
|
||||||
bindTestButton()
|
attachTestButton(testButton, ::resolveDurationForUse)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun bindActionSpinner(
|
private fun bindActionSpinner(
|
||||||
@@ -138,27 +241,12 @@ class SettingsActivity : Activity() {
|
|||||||
selectedAction: NavButtonAction,
|
selectedAction: NavButtonAction,
|
||||||
storeAction: (NavButtonId, NavButtonAction) -> Unit,
|
storeAction: (NavButtonId, NavButtonAction) -> Unit,
|
||||||
) {
|
) {
|
||||||
val labels = NavButtonAction.entries.map { getString(it.labelRes) }
|
bindSpinner(
|
||||||
val adapter = ArrayAdapter(
|
spinner,
|
||||||
this@SettingsActivity,
|
NavButtonAction.entries,
|
||||||
R.layout.item_spinner_selected,
|
selectedAction,
|
||||||
labels,
|
NavButtonAction::labelRes,
|
||||||
)
|
) { action -> storeAction(buttonId, action) }
|
||||||
adapter.setDropDownViewResource(R.layout.item_spinner_dropdown)
|
|
||||||
spinner.adapter = adapter
|
|
||||||
spinner.setSelection(NavButtonAction.entries.indexOf(selectedAction), false)
|
|
||||||
spinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
|
|
||||||
override fun onItemSelected(
|
|
||||||
parent: AdapterView<*>?,
|
|
||||||
view: View?,
|
|
||||||
position: Int,
|
|
||||||
id: Long,
|
|
||||||
) {
|
|
||||||
storeAction(buttonId, NavButtonAction.entries[position])
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onNothingSelected(parent: AdapterView<*>?) = Unit
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun bindDurationInput() {
|
private fun bindDurationInput() {
|
||||||
@@ -206,67 +294,6 @@ class SettingsActivity : Activity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun bindTestButton() {
|
|
||||||
testButton.setOnTouchListener { view, event ->
|
|
||||||
when (event.actionMasked) {
|
|
||||||
MotionEvent.ACTION_DOWN -> {
|
|
||||||
view.isPressed = true
|
|
||||||
startTestPreview(resolveDurationForUse())
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
MotionEvent.ACTION_UP,
|
|
||||||
MotionEvent.ACTION_CANCEL,
|
|
||||||
-> {
|
|
||||||
view.isPressed = false
|
|
||||||
stopTestPreview()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun attachRepeatButton(button: Button, action: () -> Unit) {
|
|
||||||
var repeated = false
|
|
||||||
val repeatRunnable = object : Runnable {
|
|
||||||
override fun run() {
|
|
||||||
repeated = true
|
|
||||||
action()
|
|
||||||
handler.postDelayed(this, NavButtonSettingsStore.HOLD_REPEAT_INTERVAL_MS)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
button.setOnClickListener {
|
|
||||||
if (!repeated) {
|
|
||||||
action()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
button.setOnTouchListener { view, event ->
|
|
||||||
when (event.actionMasked) {
|
|
||||||
MotionEvent.ACTION_DOWN -> {
|
|
||||||
repeated = false
|
|
||||||
handler.postDelayed(
|
|
||||||
repeatRunnable,
|
|
||||||
NavButtonSettingsStore.HOLD_REPEAT_START_DELAY_MS,
|
|
||||||
)
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
MotionEvent.ACTION_UP,
|
|
||||||
MotionEvent.ACTION_CANCEL,
|
|
||||||
-> {
|
|
||||||
handler.removeCallbacks(repeatRunnable)
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun adjustDuration(deltaMs: Int) {
|
private fun adjustDuration(deltaMs: Int) {
|
||||||
val current = resolveDurationForUse()
|
val current = resolveDurationForUse()
|
||||||
val updated = (current + deltaMs).coerceIn(
|
val updated = (current + deltaMs).coerceIn(
|
||||||
@@ -303,4 +330,268 @@ class SettingsActivity : Activity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private inner class SideArrowsSectionController(sectionView: View) {
|
||||||
|
private val sideArrowsCheckbox: CheckBox = sectionView.findViewById(R.id.side_arrows_checkbox)
|
||||||
|
private val optionsContainer: LinearLayout =
|
||||||
|
sectionView.findViewById(R.id.side_arrows_options_container)
|
||||||
|
private val longPressActionSpinner: Spinner =
|
||||||
|
sectionView.findViewById(R.id.side_arrow_long_press_action_spinner)
|
||||||
|
private val repeatSpeedContainer: LinearLayout =
|
||||||
|
sectionView.findViewById(R.id.side_arrow_repeat_speed_container)
|
||||||
|
private val repeatSpeedInput: EditText =
|
||||||
|
sectionView.findViewById(R.id.side_arrow_repeat_speed_input)
|
||||||
|
private val speedMinusButton: Button =
|
||||||
|
sectionView.findViewById(R.id.side_arrow_speed_minus_button)
|
||||||
|
private val speedPlusButton: Button =
|
||||||
|
sectionView.findViewById(R.id.side_arrow_speed_plus_button)
|
||||||
|
private val durationInput: EditText =
|
||||||
|
sectionView.findViewById(R.id.side_arrow_long_press_duration_input)
|
||||||
|
private val durationMinusButton: Button =
|
||||||
|
sectionView.findViewById(R.id.side_arrow_duration_minus_button)
|
||||||
|
private val durationPlusButton: Button =
|
||||||
|
sectionView.findViewById(R.id.side_arrow_duration_plus_button)
|
||||||
|
private val testButton: Button = sectionView.findViewById(R.id.side_arrow_test_button)
|
||||||
|
|
||||||
|
private val decimalSeparator = DecimalFormatSymbols.getInstance().decimalSeparator
|
||||||
|
private var lastValidDurationMs = settingsStore.defaultLongPressDurationMs()
|
||||||
|
private var lastValidRepeatSpeed = NavButtonSettingsStore.DEFAULT_SIDE_ARROW_REPEAT_STEPS_PER_SECOND
|
||||||
|
private var updatingDurationText = false
|
||||||
|
private var updatingRepeatSpeedText = false
|
||||||
|
|
||||||
|
fun bind() {
|
||||||
|
sideArrowsCheckbox.isChecked = settingsStore.isSideArrowsEnabled()
|
||||||
|
bindLongPressActionSpinner()
|
||||||
|
applyRepeatSpeed(settingsStore.getSideArrowRepeatStepsPerSecond(), persist = false)
|
||||||
|
bindRepeatSpeedInput()
|
||||||
|
bindRepeatSpeedButtons()
|
||||||
|
applyDuration(settingsStore.getSideArrowLongPressDurationMs(), persist = false)
|
||||||
|
bindDurationInput()
|
||||||
|
bindDurationButtons()
|
||||||
|
attachTestButton(testButton, ::resolveDurationForUse)
|
||||||
|
updateOptionsEnabled()
|
||||||
|
|
||||||
|
sideArrowsCheckbox.setOnCheckedChangeListener { _, isChecked ->
|
||||||
|
settingsStore.setSideArrowsEnabled(isChecked)
|
||||||
|
updateOptionsEnabled()
|
||||||
|
sendBroadcast(
|
||||||
|
Intent(NavButtonSettingsStore.ACTION_SETTINGS_CHANGED).putExtra(
|
||||||
|
NavButtonSettingsStore.EXTRA_SIDE_ARROWS_ENABLED,
|
||||||
|
isChecked,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun bindLongPressActionSpinner() {
|
||||||
|
bindSpinner(
|
||||||
|
longPressActionSpinner,
|
||||||
|
SideArrowLongPressAction.entries,
|
||||||
|
settingsStore.getSideArrowLongPressAction(),
|
||||||
|
SideArrowLongPressAction::labelRes,
|
||||||
|
) { action ->
|
||||||
|
settingsStore.setSideArrowLongPressAction(action)
|
||||||
|
updateRepeatSpeedVisibility(action)
|
||||||
|
}
|
||||||
|
updateRepeatSpeedVisibility(settingsStore.getSideArrowLongPressAction())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun bindRepeatSpeedInput() {
|
||||||
|
repeatSpeedInput.setOnFocusChangeListener { _, hasFocus ->
|
||||||
|
if (!hasFocus) {
|
||||||
|
normalizeTypedRepeatSpeed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
repeatSpeedInput.addTextChangedListener(object : TextWatcher {
|
||||||
|
override fun beforeTextChanged(
|
||||||
|
s: CharSequence?,
|
||||||
|
start: Int,
|
||||||
|
count: Int,
|
||||||
|
after: Int,
|
||||||
|
) = Unit
|
||||||
|
|
||||||
|
override fun onTextChanged(
|
||||||
|
s: CharSequence?,
|
||||||
|
start: Int,
|
||||||
|
before: Int,
|
||||||
|
count: Int,
|
||||||
|
) = Unit
|
||||||
|
|
||||||
|
override fun afterTextChanged(editable: Editable?) {
|
||||||
|
if (updatingRepeatSpeedText) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val parsed = parseRepeatSpeed(editable?.toString()) ?: return
|
||||||
|
if (parsed in NavButtonSettingsStore.MIN_SIDE_ARROW_REPEAT_STEPS_PER_SECOND..
|
||||||
|
NavButtonSettingsStore.MAX_SIDE_ARROW_REPEAT_STEPS_PER_SECOND
|
||||||
|
) {
|
||||||
|
lastValidRepeatSpeed = parsed
|
||||||
|
settingsStore.setSideArrowRepeatStepsPerSecond(parsed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun bindRepeatSpeedButtons() {
|
||||||
|
attachRepeatButton(speedMinusButton) {
|
||||||
|
adjustRepeatSpeed(-NavButtonSettingsStore.SIDE_ARROW_REPEAT_SPEED_STEP)
|
||||||
|
}
|
||||||
|
attachRepeatButton(speedPlusButton) {
|
||||||
|
adjustRepeatSpeed(NavButtonSettingsStore.SIDE_ARROW_REPEAT_SPEED_STEP)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun bindDurationInput() {
|
||||||
|
durationInput.setOnFocusChangeListener { _, hasFocus ->
|
||||||
|
if (!hasFocus) {
|
||||||
|
normalizeTypedDuration()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
durationInput.addTextChangedListener(object : TextWatcher {
|
||||||
|
override fun beforeTextChanged(
|
||||||
|
s: CharSequence?,
|
||||||
|
start: Int,
|
||||||
|
count: Int,
|
||||||
|
after: Int,
|
||||||
|
) = Unit
|
||||||
|
|
||||||
|
override fun onTextChanged(
|
||||||
|
s: CharSequence?,
|
||||||
|
start: Int,
|
||||||
|
before: Int,
|
||||||
|
count: Int,
|
||||||
|
) = Unit
|
||||||
|
|
||||||
|
override fun afterTextChanged(editable: Editable?) {
|
||||||
|
if (updatingDurationText) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val parsed = editable?.toString()?.toIntOrNull() ?: return
|
||||||
|
if (parsed in NavButtonSettingsStore.MIN_DURATION_MS..NavButtonSettingsStore.MAX_DURATION_MS) {
|
||||||
|
lastValidDurationMs = parsed
|
||||||
|
settingsStore.setSideArrowLongPressDurationMs(parsed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun bindDurationButtons() {
|
||||||
|
attachRepeatButton(durationMinusButton) {
|
||||||
|
adjustDuration(-NavButtonSettingsStore.DURATION_STEP_MS)
|
||||||
|
}
|
||||||
|
attachRepeatButton(durationPlusButton) {
|
||||||
|
adjustDuration(NavButtonSettingsStore.DURATION_STEP_MS)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateOptionsEnabled() {
|
||||||
|
val enabled = sideArrowsCheckbox.isChecked
|
||||||
|
optionsContainer.alpha = if (enabled) 1f else 0.45f
|
||||||
|
setEnabledRecursively(optionsContainer, enabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setEnabledRecursively(view: View, enabled: Boolean) {
|
||||||
|
view.isEnabled = enabled
|
||||||
|
if (view is ViewGroup) {
|
||||||
|
repeat(view.childCount) { index ->
|
||||||
|
setEnabledRecursively(view.getChildAt(index), enabled)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateRepeatSpeedVisibility(action: SideArrowLongPressAction) {
|
||||||
|
repeatSpeedContainer.visibility =
|
||||||
|
if (action == SideArrowLongPressAction.MULTIPLE_STEPS) View.VISIBLE else View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun adjustRepeatSpeed(delta: Float) {
|
||||||
|
applyRepeatSpeed(resolveRepeatSpeedForUse() + delta, persist = true)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun normalizeTypedRepeatSpeed(): Float {
|
||||||
|
val parsed = parseRepeatSpeed(repeatSpeedInput.text.toString()) ?: lastValidRepeatSpeed
|
||||||
|
val normalized = settingsStore.sanitizeSideArrowRepeatSpeed(parsed)
|
||||||
|
applyRepeatSpeed(normalized, persist = true)
|
||||||
|
return normalized
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun resolveRepeatSpeedForUse(): Float {
|
||||||
|
return normalizeTypedRepeatSpeed()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun applyRepeatSpeed(stepsPerSecond: Float, persist: Boolean) {
|
||||||
|
val sanitized = settingsStore.sanitizeSideArrowRepeatSpeed(stepsPerSecond)
|
||||||
|
val text = formatRepeatSpeed(sanitized)
|
||||||
|
updatingRepeatSpeedText = true
|
||||||
|
if (repeatSpeedInput.text.toString() != text) {
|
||||||
|
repeatSpeedInput.setText(text)
|
||||||
|
repeatSpeedInput.setSelection(repeatSpeedInput.text.length)
|
||||||
|
}
|
||||||
|
updatingRepeatSpeedText = false
|
||||||
|
|
||||||
|
lastValidRepeatSpeed = sanitized
|
||||||
|
if (persist) {
|
||||||
|
settingsStore.setSideArrowRepeatStepsPerSecond(sanitized)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseRepeatSpeed(text: String?): Float? {
|
||||||
|
return text
|
||||||
|
?.trim()
|
||||||
|
?.replace(',', '.')
|
||||||
|
?.toFloatOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun formatRepeatSpeed(stepsPerSecond: Float): String {
|
||||||
|
val raw = if (stepsPerSecond == stepsPerSecond.toInt().toFloat()) {
|
||||||
|
stepsPerSecond.toInt().toString()
|
||||||
|
} else {
|
||||||
|
String.format(Locale.US, "%.2f", stepsPerSecond)
|
||||||
|
.trimEnd('0')
|
||||||
|
.trimEnd('.')
|
||||||
|
}
|
||||||
|
return raw.replace('.', decimalSeparator)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun adjustDuration(deltaMs: Int) {
|
||||||
|
val current = resolveDurationForUse()
|
||||||
|
val updated = (current + deltaMs).coerceIn(
|
||||||
|
NavButtonSettingsStore.MIN_DURATION_MS,
|
||||||
|
NavButtonSettingsStore.MAX_DURATION_MS,
|
||||||
|
)
|
||||||
|
applyDuration(updated, persist = true)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun normalizeTypedDuration(): Int {
|
||||||
|
val parsed = durationInput.text.toString().toIntOrNull() ?: lastValidDurationMs
|
||||||
|
val normalized = settingsStore.sanitizeDuration(parsed)
|
||||||
|
applyDuration(normalized, persist = true)
|
||||||
|
return normalized
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun resolveDurationForUse(): Int {
|
||||||
|
return normalizeTypedDuration()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun applyDuration(durationMs: Int, persist: Boolean) {
|
||||||
|
val sanitized = settingsStore.sanitizeDuration(durationMs)
|
||||||
|
val text = sanitized.toString()
|
||||||
|
updatingDurationText = true
|
||||||
|
if (durationInput.text.toString() != text) {
|
||||||
|
durationInput.setText(text)
|
||||||
|
durationInput.setSelection(durationInput.text.length)
|
||||||
|
}
|
||||||
|
updatingDurationText = false
|
||||||
|
|
||||||
|
lastValidDurationMs = sanitized
|
||||||
|
if (persist) {
|
||||||
|
settingsStore.setSideArrowLongPressDurationMs(sanitized)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,15 +29,6 @@
|
|||||||
android:textSize="30sp"
|
android:textSize="30sp"
|
||||||
android:textStyle="bold" />
|
android:textStyle="bold" />
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="8dp"
|
|
||||||
android:lineSpacingExtra="3dp"
|
|
||||||
android:text="@string/settings_summary"
|
|
||||||
android:textColor="@color/settings_text_secondary"
|
|
||||||
android:textSize="15sp" />
|
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/button_sections_container"
|
android:id="@+id/button_sections_container"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
|||||||
@@ -0,0 +1,179 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="@drawable/bg_button_section"
|
||||||
|
android:elevation="2dp"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="18dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/side_arrows_section_title"
|
||||||
|
android:textColor="@color/settings_text_primary"
|
||||||
|
android:textSize="22sp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
<CheckBox
|
||||||
|
android:id="@+id/side_arrows_checkbox"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="14dp"
|
||||||
|
android:buttonTint="@color/settings_label_text"
|
||||||
|
android:saveEnabled="false"
|
||||||
|
android:text="@string/side_arrows_enable_label"
|
||||||
|
android:textColor="@color/settings_text_primary"
|
||||||
|
android:textSize="16sp" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/side_arrows_options_container"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/long_press_action_label"
|
||||||
|
android:textColor="@color/settings_label_text"
|
||||||
|
android:textSize="13sp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
<Spinner
|
||||||
|
android:id="@+id/side_arrow_long_press_action_spinner"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="6dp"
|
||||||
|
android:background="@drawable/bg_spinner_field"
|
||||||
|
android:dropDownWidth="match_parent"
|
||||||
|
android:paddingStart="12dp"
|
||||||
|
android:paddingTop="6dp"
|
||||||
|
android:paddingEnd="12dp"
|
||||||
|
android:paddingBottom="6dp"
|
||||||
|
android:popupBackground="@drawable/bg_spinner_popup"
|
||||||
|
android:saveEnabled="false" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/side_arrow_repeat_speed_container"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/side_arrow_repeat_speed_label"
|
||||||
|
android:textColor="@color/settings_label_text"
|
||||||
|
android:textSize="13sp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/side_arrow_speed_minus_button"
|
||||||
|
android:layout_width="48dp"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:minWidth="48dp"
|
||||||
|
android:text="@string/minus_button_label"
|
||||||
|
android:textAllCaps="false"
|
||||||
|
android:textSize="22sp" />
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/side_arrow_repeat_speed_input"
|
||||||
|
android:layout_width="88dp"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:gravity="center"
|
||||||
|
android:importantForAutofill="no"
|
||||||
|
android:inputType="numberDecimal"
|
||||||
|
android:maxLength="6"
|
||||||
|
android:saveEnabled="false"
|
||||||
|
android:singleLine="true"
|
||||||
|
android:textColor="@color/settings_input_text"
|
||||||
|
android:textSize="18sp" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/side_arrow_speed_plus_button"
|
||||||
|
android:layout_width="48dp"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:minWidth="48dp"
|
||||||
|
android:text="@string/plus_button_label"
|
||||||
|
android:textAllCaps="false"
|
||||||
|
android:textSize="20sp" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:text="@string/long_press_duration_label"
|
||||||
|
android:textColor="@color/settings_label_text"
|
||||||
|
android:textSize="13sp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/side_arrow_duration_minus_button"
|
||||||
|
android:layout_width="48dp"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:minWidth="48dp"
|
||||||
|
android:text="@string/minus_button_label"
|
||||||
|
android:textAllCaps="false"
|
||||||
|
android:textSize="22sp" />
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/side_arrow_long_press_duration_input"
|
||||||
|
android:layout_width="88dp"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:gravity="center"
|
||||||
|
android:importantForAutofill="no"
|
||||||
|
android:inputType="number"
|
||||||
|
android:maxLength="5"
|
||||||
|
android:saveEnabled="false"
|
||||||
|
android:singleLine="true"
|
||||||
|
android:textColor="@color/settings_input_text"
|
||||||
|
android:textSize="18sp" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/side_arrow_duration_plus_button"
|
||||||
|
android:layout_width="48dp"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:minWidth="48dp"
|
||||||
|
android:text="@string/plus_button_label"
|
||||||
|
android:textAllCaps="false"
|
||||||
|
android:textSize="20sp" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/side_arrow_test_button"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="54dp"
|
||||||
|
android:layout_marginTop="14dp"
|
||||||
|
android:minWidth="160dp"
|
||||||
|
android:text="@string/test_button_label"
|
||||||
|
android:textAllCaps="false"
|
||||||
|
android:textSize="16sp" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
@@ -2,10 +2,15 @@
|
|||||||
<string name="app_name">NavButtons</string>
|
<string name="app_name">NavButtons</string>
|
||||||
<string name="xposed_desc">Customize navigation button press and long-press actions.</string>
|
<string name="xposed_desc">Customize navigation button press and long-press actions.</string>
|
||||||
<string name="settings_title">Nav Button Settings</string>
|
<string name="settings_title">Nav Button Settings</string>
|
||||||
<string name="settings_summary">Choose what each navigation button should do on press and long press, then tune how long a long press should feel.</string>
|
|
||||||
<string name="button_back">Back Button</string>
|
<string name="button_back">Back Button</string>
|
||||||
<string name="button_home">Home Button</string>
|
<string name="button_home">Home Button</string>
|
||||||
<string name="button_recents">Recents Button</string>
|
<string name="button_recents">Recents Button</string>
|
||||||
|
<string name="side_arrows_section_title">Side arrows</string>
|
||||||
|
<string name="side_arrows_enable_label">Enable side arrows</string>
|
||||||
|
<string name="side_arrow_long_press_nothing">Nothing</string>
|
||||||
|
<string name="side_arrow_long_press_single_step">Single step</string>
|
||||||
|
<string name="side_arrow_long_press_multiple_steps">Multiple steps</string>
|
||||||
|
<string name="side_arrow_repeat_speed_label">Repeat speed (steps/sec)</string>
|
||||||
<string name="press_action_label">When pressed</string>
|
<string name="press_action_label">When pressed</string>
|
||||||
<string name="long_press_action_label">When long-pressed</string>
|
<string name="long_press_action_label">When long-pressed</string>
|
||||||
<string name="long_press_duration_label">Long-press duration (ms)</string>
|
<string name="long_press_duration_label">Long-press duration (ms)</string>
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
android
|
android
|
||||||
|
system
|
||||||
com.android.systemui
|
com.android.systemui
|
||||||
|
com.android.launcher3
|
||||||
|
com.google.android.apps.nexuslauncher
|
||||||
com.sec.android.app.launcher
|
com.sec.android.app.launcher
|
||||||
|
com.samsung.systemui.navillera
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
version=1.0
|
version=2.1
|
||||||
|
|||||||
Reference in New Issue
Block a user