Add more nav button actions

This commit is contained in:
ajp_anton
2026-04-24 22:50:20 +00:00
parent 9d57654bb2
commit 1c2b2f4b37
6 changed files with 141 additions and 11 deletions
+2 -2
View File
@@ -56,8 +56,8 @@ android {
applicationId = "se.ajpanton.navbuttons" applicationId = "se.ajpanton.navbuttons"
minSdk = 35 minSdk = 35
targetSdk = 35 targetSdk = 35
versionCode = 1777035207 versionCode = 1777070659
versionName = "0.1-1777035207" versionName = "0.3"
} }
buildTypes { buildTypes {
@@ -22,8 +22,14 @@ enum class NavButtonAction(
@get:StringRes val labelRes: Int, @get:StringRes val labelRes: Int,
) { ) {
STOCK("stock", R.string.action_stock), STOCK("stock", R.string.action_stock),
KILL_FOREGROUND_APP("kill_foreground_app", R.string.action_kill_foreground_app),
NONE("none", R.string.action_none), NONE("none", R.string.action_none),
BACK("back", R.string.action_back),
HOME("home", R.string.action_home),
ASSISTANT("assistant", R.string.action_assistant),
RECENTS("recents", R.string.action_recents),
KILL_FOREGROUND_APP("kill_foreground_app", R.string.action_kill_foreground_app),
TOGGLE_AUTO_ROTATE("toggle_auto_rotate", R.string.action_toggle_auto_rotate),
TOGGLE_FLASHLIGHT("toggle_flashlight", R.string.action_toggle_flashlight),
; ;
companion object { companion object {
@@ -7,12 +7,17 @@ import android.content.Intent
import android.content.SharedPreferences import android.content.SharedPreferences
import android.content.pm.ApplicationInfo import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.hardware.camera2.CameraAccessException
import android.hardware.camera2.CameraCharacteristics
import android.hardware.camera2.CameraManager
import android.os.Bundle import android.os.Bundle
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.os.Process import android.os.Process
import android.os.SystemClock import android.os.SystemClock
import android.provider.Settings
import android.util.Log import android.util.Log
import android.view.InputEvent
import android.view.KeyEvent import android.view.KeyEvent
import android.view.MotionEvent import android.view.MotionEvent
import android.view.View import android.view.View
@@ -41,6 +46,8 @@ class NavButtons : XposedModule() {
private var systemServerHookInstalled = false private var systemServerHookInstalled = false
private var launcherHookInstalled = false private var launcherHookInstalled = false
private var lastHomeLongPressHandledAtMs = 0L private var lastHomeLongPressHandledAtMs = 0L
private var flashlightCameraId: String? = null
private var flashlightEnabled = false
private val systemUiLongPressTriggered = WeakHashMap<Any, Boolean>() private val systemUiLongPressTriggered = WeakHashMap<Any, Boolean>()
private val systemUiHomeStockLongPressGuards = WeakHashMap<Any, Runnable>() private val systemUiHomeStockLongPressGuards = WeakHashMap<Any, Runnable>()
private val systemUiHomeStockPressCanceled = WeakHashMap<Any, Boolean>() private val systemUiHomeStockPressCanceled = WeakHashMap<Any, Boolean>()
@@ -637,7 +644,110 @@ class NavButtons : XposedModule() {
NavButtonAction.NONE, NavButtonAction.NONE,
-> Unit -> Unit
NavButtonAction.BACK -> sendKeyPress(KeyEvent.KEYCODE_BACK)
NavButtonAction.HOME -> sendKeyPress(KeyEvent.KEYCODE_HOME)
NavButtonAction.ASSISTANT -> sendKeyPress(KeyEvent.KEYCODE_ASSIST)
NavButtonAction.RECENTS -> sendKeyPress(KeyEvent.KEYCODE_APP_SWITCH)
NavButtonAction.KILL_FOREGROUND_APP -> killForegroundApp() NavButtonAction.KILL_FOREGROUND_APP -> killForegroundApp()
NavButtonAction.TOGGLE_AUTO_ROTATE -> toggleAutoRotate()
NavButtonAction.TOGGLE_FLASHLIGHT -> toggleFlashlight()
}
}
private fun sendKeyPress(keyCode: Int) {
try {
val inputManager = callStaticMethod(
findClass(INPUT_MANAGER_CLASS, null),
"getInstance",
) ?: run {
log("InputManager unavailable for keyCode=$keyCode")
return
}
val downTime = SystemClock.uptimeMillis()
injectKeyEvent(inputManager, KeyEvent(downTime, downTime, KeyEvent.ACTION_DOWN, keyCode, 0))
injectKeyEvent(inputManager, KeyEvent(downTime, SystemClock.uptimeMillis(), KeyEvent.ACTION_UP, keyCode, 0))
} catch (error: Throwable) {
log("Key injection failed for keyCode=$keyCode: ${error.javaClass.simpleName}")
}
}
private fun injectKeyEvent(inputManager: Any, keyEvent: KeyEvent) {
callMethod(
inputManager,
"injectInputEvent",
arrayOf(InputEvent::class.java, Int::class.javaPrimitiveType!!),
keyEvent,
INPUT_EVENT_INJECTION_SYNC_NONE,
)
}
private fun toggleAutoRotate() {
val currentContext = context ?: run {
log("Context unavailable, cannot toggle auto rotate.")
return
}
try {
val resolver = currentContext.contentResolver
val currentlyEnabled = Settings.System.getInt(
resolver,
Settings.System.ACCELEROMETER_ROTATION,
0,
) == 1
val enabled = !currentlyEnabled
Settings.System.putInt(
resolver,
Settings.System.ACCELEROMETER_ROTATION,
if (enabled) 1 else 0,
)
showToast("Auto rotate: ${if (enabled) "On" else "Off"}")
} catch (error: Throwable) {
log("Toggle auto rotate failed: ${error.javaClass.simpleName}")
showToast("Auto rotate toggle failed.")
}
}
private fun toggleFlashlight() {
val currentContext = context ?: run {
log("Context unavailable, cannot toggle flashlight.")
return
}
try {
val cameraManager = currentContext.getSystemService(CameraManager::class.java)
val cameraId = flashlightCameraId ?: findFlashlightCameraId(cameraManager)
if (cameraId == null) {
log("No flashlight-capable camera found.")
showToast("No flashlight found.")
return
}
flashlightCameraId = cameraId
val enabled = !flashlightEnabled
cameraManager.setTorchMode(cameraId, enabled)
flashlightEnabled = enabled
showToast("Flashlight: ${if (enabled) "On" else "Off"}")
} catch (error: CameraAccessException) {
log("Toggle flashlight failed: ${error.javaClass.simpleName}")
showToast("Flashlight toggle failed.")
} catch (error: SecurityException) {
log("Toggle flashlight permission denied.")
showToast("Flashlight permission denied.")
} catch (error: Throwable) {
log("Toggle flashlight failed: ${error.javaClass.simpleName}")
showToast("Flashlight toggle failed.")
}
}
private fun findFlashlightCameraId(cameraManager: CameraManager): String? {
return cameraManager.cameraIdList.firstOrNull { cameraId ->
val characteristics = cameraManager.getCameraCharacteristics(cameraId)
val hasFlash = characteristics.get(CameraCharacteristics.FLASH_INFO_AVAILABLE) == true
val lensFacing = characteristics.get(CameraCharacteristics.LENS_FACING)
hasFlash && lensFacing == CameraCharacteristics.LENS_FACING_BACK
} ?: cameraManager.cameraIdList.firstOrNull { cameraId ->
val characteristics = cameraManager.getCameraCharacteristics(cameraId)
characteristics.get(CameraCharacteristics.FLASH_INFO_AVAILABLE) == true
} }
} }
@@ -1202,6 +1312,8 @@ class NavButtons : XposedModule() {
const val ASSIST_INVOCATION_TYPE_KEY = "invocation_type" const val ASSIST_INVOCATION_TYPE_KEY = "invocation_type"
const val ASSIST_INVOCATION_TYPE_UNKNOWN = 0 const val ASSIST_INVOCATION_TYPE_UNKNOWN = 0
const val ASSIST_INVOCATION_TYPE_HOME_BUTTON_LONG_PRESS = 5 const val ASSIST_INVOCATION_TYPE_HOME_BUTTON_LONG_PRESS = 5
const val INPUT_MANAGER_CLASS = "android.hardware.input.InputManager"
const val INPUT_EVENT_INJECTION_SYNC_NONE = 0
const val HOME_LONG_PRESS_DEDUP_WINDOW_MS = 1_000L const val HOME_LONG_PRESS_DEDUP_WINDOW_MS = 1_000L
const val STOCK_LONG_PRESS_GUARD_EARLY_MS = 75 const val STOCK_LONG_PRESS_GUARD_EARLY_MS = 75
const val STOCK_LONG_PRESS_GUARD_MIN_DELAY_MS = 100 const val STOCK_LONG_PRESS_GUARD_MIN_DELAY_MS = 100
@@ -128,7 +128,7 @@ class SettingsActivity : Activity() {
settingsStore::setLongPressAction, settingsStore::setLongPressAction,
) )
applyDuration(config.longPressDurationMs) applyDuration(config.longPressDurationMs, persist = false)
bindDurationInput() bindDurationInput()
bindStepButtons() bindStepButtons()
bindTestButton() bindTestButton()
@@ -274,13 +274,13 @@ class SettingsActivity : Activity() {
NavButtonSettingsStore.MIN_DURATION_MS, NavButtonSettingsStore.MIN_DURATION_MS,
NavButtonSettingsStore.MAX_DURATION_MS, NavButtonSettingsStore.MAX_DURATION_MS,
) )
applyDuration(updated) applyDuration(updated, persist = true)
} }
private fun normalizeTypedDuration(): Int { private fun normalizeTypedDuration(): Int {
val parsed = durationInput.text.toString().toIntOrNull() ?: lastValidDurationMs val parsed = durationInput.text.toString().toIntOrNull() ?: lastValidDurationMs
val normalized = settingsStore.sanitizeDuration(parsed) val normalized = settingsStore.sanitizeDuration(parsed)
applyDuration(normalized) applyDuration(normalized, persist = true)
return normalized return normalized
} }
@@ -288,7 +288,7 @@ class SettingsActivity : Activity() {
return normalizeTypedDuration() return normalizeTypedDuration()
} }
private fun applyDuration(durationMs: Int) { private fun applyDuration(durationMs: Int, persist: Boolean) {
val sanitized = settingsStore.sanitizeDuration(durationMs) val sanitized = settingsStore.sanitizeDuration(durationMs)
val text = sanitized.toString() val text = sanitized.toString()
updatingDurationText = true updatingDurationText = true
@@ -299,7 +299,9 @@ class SettingsActivity : Activity() {
updatingDurationText = false updatingDurationText = false
lastValidDurationMs = sanitized lastValidDurationMs = sanitized
settingsStore.setLongPressDurationMs(buttonId, sanitized) if (persist) {
settingsStore.setLongPressDurationMs(buttonId, sanitized)
}
} }
} }
} }
@@ -5,6 +5,7 @@
android:background="@drawable/bg_button_section" android:background="@drawable/bg_button_section"
android:elevation="2dp" android:elevation="2dp"
android:orientation="vertical" android:orientation="vertical"
android:saveEnabled="false"
android:padding="18dp"> android:padding="18dp">
<TextView <TextView
@@ -35,7 +36,8 @@
android:paddingTop="6dp" android:paddingTop="6dp"
android:paddingEnd="12dp" android:paddingEnd="12dp"
android:paddingBottom="6dp" android:paddingBottom="6dp"
android:popupBackground="@drawable/bg_spinner_popup" /> android:popupBackground="@drawable/bg_spinner_popup"
android:saveEnabled="false" />
<TextView <TextView
android:layout_width="match_parent" android:layout_width="match_parent"
@@ -57,7 +59,8 @@
android:paddingTop="6dp" android:paddingTop="6dp"
android:paddingEnd="12dp" android:paddingEnd="12dp"
android:paddingBottom="6dp" android:paddingBottom="6dp"
android:popupBackground="@drawable/bg_spinner_popup" /> android:popupBackground="@drawable/bg_spinner_popup"
android:saveEnabled="false" />
<TextView <TextView
android:layout_width="match_parent" android:layout_width="match_parent"
@@ -93,6 +96,7 @@
android:importantForAutofill="no" android:importantForAutofill="no"
android:inputType="number" android:inputType="number"
android:maxLength="5" android:maxLength="5"
android:saveEnabled="false"
android:singleLine="true" android:singleLine="true"
android:textColor="@color/settings_input_text" android:textColor="@color/settings_input_text"
android:textSize="18sp" /> android:textSize="18sp" />
+7 -1
View File
@@ -10,8 +10,14 @@
<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>
<string name="action_stock">Stock</string> <string name="action_stock">Stock</string>
<string name="action_kill_foreground_app">Kill foreground app</string>
<string name="action_none">None</string> <string name="action_none">None</string>
<string name="action_back">Back</string>
<string name="action_home">Home</string>
<string name="action_assistant">Assistant</string>
<string name="action_recents">Recents</string>
<string name="action_kill_foreground_app">Kill foreground app</string>
<string name="action_toggle_auto_rotate">Toggle auto rotate</string>
<string name="action_toggle_flashlight">Toggle flashlight</string>
<string name="minus_button_label">-</string> <string name="minus_button_label">-</string>
<string name="plus_button_label">+</string> <string name="plus_button_label">+</string>
<string name="test_button_label">Duration test (hold)</string> <string name="test_button_label">Duration test (hold)</string>