Add side arrow navigation buttons

This commit is contained in:
ajp_anton
2026-06-22 00:42:17 +00:00
parent e658caa399
commit 0956bbc4d7
8 changed files with 1902 additions and 70 deletions
+1
View File
@@ -4,3 +4,4 @@ build/
app/build/ app/build/
local/ local/
local.properties local.properties
*.log
@@ -39,6 +39,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,
@@ -91,6 +107,61 @@ 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())
} }
@@ -99,6 +170,17 @@ class NavButtonSettingsStore(context: Context) {
return durationMs.coerceIn(MIN_LONG_PRESS_DURATION_MS, MAX_LONG_PRESS_DURATION_MS) return durationMs.coerceIn(MIN_LONG_PRESS_DURATION_MS, MAX_LONG_PRESS_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 {
return remotePreferences ?: localPreferences return remotePreferences ?: localPreferences
} }
@@ -129,9 +211,15 @@ 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"
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 MIN_LONG_PRESS_DURATION_MS = 100
@@ -141,6 +229,10 @@ class NavButtonSettingsStore(context: Context) {
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 +253,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 +314,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 +339,27 @@ 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_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 {
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,15 @@ 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
import java.util.concurrent.TimeUnit
class SettingsActivity : Activity() { class SettingsActivity : Activity() {
private val handler = Handler(Looper.getMainLooper()) private val handler = Handler(Looper.getMainLooper())
@@ -61,23 +67,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 +115,45 @@ 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 inner class NavButtonSectionController( private inner class NavButtonSectionController(
sectionView: View, sectionView: View,
private val buttonId: NavButtonId, private val buttonId: NavButtonId,
@@ -228,45 +286,6 @@ class SettingsActivity : Activity() {
} }
} }
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 +322,346 @@ 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 restartSamsungUiButton: Button =
sectionView.findViewById(R.id.restart_samsung_ui_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()
bindTestButton()
updateOptionsEnabled()
sideArrowsCheckbox.setOnCheckedChangeListener { _, isChecked ->
settingsStore.setSideArrowsEnabled(isChecked)
updateOptionsEnabled()
sendBroadcast(
Intent(NavButtonSettingsStore.ACTION_SETTINGS_CHANGED).putExtra(
NavButtonSettingsStore.EXTRA_SIDE_ARROWS_ENABLED,
isChecked,
),
)
}
restartSamsungUiButton.setOnClickListener {
restartSamsungUiButton.isEnabled = false
Toast.makeText(
this@SettingsActivity,
"Restarting One UI Home and SystemUI...",
Toast.LENGTH_SHORT,
).show()
Thread {
val result = restartSamsungUiProcesses()
handler.post {
restartSamsungUiButton.isEnabled = true
Toast.makeText(this@SettingsActivity, result, Toast.LENGTH_LONG).show()
}
}.start()
}
}
private fun bindLongPressActionSpinner() {
val labels = SideArrowLongPressAction.entries.map { getString(it.labelRes) }
val adapter = ArrayAdapter(
this@SettingsActivity,
R.layout.item_spinner_selected,
labels,
)
adapter.setDropDownViewResource(R.layout.item_spinner_dropdown)
longPressActionSpinner.adapter = adapter
longPressActionSpinner.setSelection(
SideArrowLongPressAction.entries.indexOf(settingsStore.getSideArrowLongPressAction()),
false,
)
longPressActionSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(
parent: AdapterView<*>?,
view: View?,
position: Int,
id: Long,
) {
val action = SideArrowLongPressAction.entries[position]
settingsStore.setSideArrowLongPressAction(action)
updateRepeatSpeedVisibility(action)
}
override fun onNothingSelected(parent: AdapterView<*>?) = Unit
}
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 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 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)
}
}
private fun restartSamsungUiProcesses(): String {
return try {
val command = listOf(
"am force-stop com.sec.android.app.launcher",
"killall com.android.systemui",
"sleep 1",
"am start -a android.intent.action.MAIN -c android.intent.category.HOME",
).joinToString("; ")
val process = Runtime.getRuntime().exec(arrayOf("su", "-c", command))
val finished = process.waitFor(10, TimeUnit.SECONDS)
if (!finished) {
process.destroyForcibly()
"Restart command timed out."
} else if (process.exitValue() == 0) {
"Restart command completed."
} else {
"Restart command failed. Is root allowed?"
}
} catch (_: Throwable) {
"Restart command failed. Is root allowed?"
}
}
}
} }
@@ -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,186 @@
<?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>
<Button
android:id="@+id/restart_samsung_ui_button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="14dp"
android:text="@string/restart_samsung_ui_button_label" />
</LinearLayout>
+7 -1
View File
@@ -2,10 +2,16 @@
<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="restart_samsung_ui_button_label">Restart One UI Home + SystemUI</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,4 @@
android android
com.android.systemui com.android.systemui
com.sec.android.app.launcher com.sec.android.app.launcher
com.samsung.systemui.navillera