Add more nav button actions
This commit is contained in:
@@ -56,8 +56,8 @@ android {
|
||||
applicationId = "se.ajpanton.navbuttons"
|
||||
minSdk = 35
|
||||
targetSdk = 35
|
||||
versionCode = 1777035207
|
||||
versionName = "0.1-1777035207"
|
||||
versionCode = 1777070659
|
||||
versionName = "0.3"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
|
||||
@@ -22,8 +22,14 @@ enum class NavButtonAction(
|
||||
@get:StringRes val labelRes: Int,
|
||||
) {
|
||||
STOCK("stock", R.string.action_stock),
|
||||
KILL_FOREGROUND_APP("kill_foreground_app", R.string.action_kill_foreground_app),
|
||||
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 {
|
||||
|
||||
@@ -7,12 +7,17 @@ import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.content.pm.ApplicationInfo
|
||||
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.Handler
|
||||
import android.os.Looper
|
||||
import android.os.Process
|
||||
import android.os.SystemClock
|
||||
import android.provider.Settings
|
||||
import android.util.Log
|
||||
import android.view.InputEvent
|
||||
import android.view.KeyEvent
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
@@ -41,6 +46,8 @@ class NavButtons : XposedModule() {
|
||||
private var systemServerHookInstalled = false
|
||||
private var launcherHookInstalled = false
|
||||
private var lastHomeLongPressHandledAtMs = 0L
|
||||
private var flashlightCameraId: String? = null
|
||||
private var flashlightEnabled = false
|
||||
private val systemUiLongPressTriggered = WeakHashMap<Any, Boolean>()
|
||||
private val systemUiHomeStockLongPressGuards = WeakHashMap<Any, Runnable>()
|
||||
private val systemUiHomeStockPressCanceled = WeakHashMap<Any, Boolean>()
|
||||
@@ -637,7 +644,110 @@ class NavButtons : XposedModule() {
|
||||
NavButtonAction.NONE,
|
||||
-> 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.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_UNKNOWN = 0
|
||||
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 STOCK_LONG_PRESS_GUARD_EARLY_MS = 75
|
||||
const val STOCK_LONG_PRESS_GUARD_MIN_DELAY_MS = 100
|
||||
|
||||
@@ -128,7 +128,7 @@ class SettingsActivity : Activity() {
|
||||
settingsStore::setLongPressAction,
|
||||
)
|
||||
|
||||
applyDuration(config.longPressDurationMs)
|
||||
applyDuration(config.longPressDurationMs, persist = false)
|
||||
bindDurationInput()
|
||||
bindStepButtons()
|
||||
bindTestButton()
|
||||
@@ -274,13 +274,13 @@ class SettingsActivity : Activity() {
|
||||
NavButtonSettingsStore.MIN_DURATION_MS,
|
||||
NavButtonSettingsStore.MAX_DURATION_MS,
|
||||
)
|
||||
applyDuration(updated)
|
||||
applyDuration(updated, persist = true)
|
||||
}
|
||||
|
||||
private fun normalizeTypedDuration(): Int {
|
||||
val parsed = durationInput.text.toString().toIntOrNull() ?: lastValidDurationMs
|
||||
val normalized = settingsStore.sanitizeDuration(parsed)
|
||||
applyDuration(normalized)
|
||||
applyDuration(normalized, persist = true)
|
||||
return normalized
|
||||
}
|
||||
|
||||
@@ -288,7 +288,7 @@ class SettingsActivity : Activity() {
|
||||
return normalizeTypedDuration()
|
||||
}
|
||||
|
||||
private fun applyDuration(durationMs: Int) {
|
||||
private fun applyDuration(durationMs: Int, persist: Boolean) {
|
||||
val sanitized = settingsStore.sanitizeDuration(durationMs)
|
||||
val text = sanitized.toString()
|
||||
updatingDurationText = true
|
||||
@@ -299,7 +299,9 @@ class SettingsActivity : Activity() {
|
||||
updatingDurationText = false
|
||||
|
||||
lastValidDurationMs = sanitized
|
||||
settingsStore.setLongPressDurationMs(buttonId, sanitized)
|
||||
if (persist) {
|
||||
settingsStore.setLongPressDurationMs(buttonId, sanitized)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
android:background="@drawable/bg_button_section"
|
||||
android:elevation="2dp"
|
||||
android:orientation="vertical"
|
||||
android:saveEnabled="false"
|
||||
android:padding="18dp">
|
||||
|
||||
<TextView
|
||||
@@ -35,7 +36,8 @@
|
||||
android:paddingTop="6dp"
|
||||
android:paddingEnd="12dp"
|
||||
android:paddingBottom="6dp"
|
||||
android:popupBackground="@drawable/bg_spinner_popup" />
|
||||
android:popupBackground="@drawable/bg_spinner_popup"
|
||||
android:saveEnabled="false" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
@@ -57,7 +59,8 @@
|
||||
android:paddingTop="6dp"
|
||||
android:paddingEnd="12dp"
|
||||
android:paddingBottom="6dp"
|
||||
android:popupBackground="@drawable/bg_spinner_popup" />
|
||||
android:popupBackground="@drawable/bg_spinner_popup"
|
||||
android:saveEnabled="false" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
@@ -93,6 +96,7 @@
|
||||
android:importantForAutofill="no"
|
||||
android:inputType="number"
|
||||
android:maxLength="5"
|
||||
android:saveEnabled="false"
|
||||
android:singleLine="true"
|
||||
android:textColor="@color/settings_input_text"
|
||||
android:textSize="18sp" />
|
||||
|
||||
@@ -10,8 +10,14 @@
|
||||
<string name="long_press_action_label">When long-pressed</string>
|
||||
<string name="long_press_duration_label">Long-press duration (ms)</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_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="plus_button_label">+</string>
|
||||
<string name="test_button_label">Duration test (hold)</string>
|
||||
|
||||
Reference in New Issue
Block a user