Add more nav button actions
This commit is contained in:
@@ -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
|
||||||
|
if (persist) {
|
||||||
settingsStore.setLongPressDurationMs(buttonId, sanitized)
|
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" />
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user