diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 642a8ef..2bda488 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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 { diff --git a/app/src/main/java/se/ajpanton/navbuttons/NavButtonSettingsStore.kt b/app/src/main/java/se/ajpanton/navbuttons/NavButtonSettingsStore.kt index c81f713..6b9bed6 100644 --- a/app/src/main/java/se/ajpanton/navbuttons/NavButtonSettingsStore.kt +++ b/app/src/main/java/se/ajpanton/navbuttons/NavButtonSettingsStore.kt @@ -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 { diff --git a/app/src/main/java/se/ajpanton/navbuttons/NavButtons.kt b/app/src/main/java/se/ajpanton/navbuttons/NavButtons.kt index 8859d01..6f7dfc6 100644 --- a/app/src/main/java/se/ajpanton/navbuttons/NavButtons.kt +++ b/app/src/main/java/se/ajpanton/navbuttons/NavButtons.kt @@ -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() private val systemUiHomeStockLongPressGuards = WeakHashMap() private val systemUiHomeStockPressCanceled = WeakHashMap() @@ -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 diff --git a/app/src/main/java/se/ajpanton/navbuttons/SettingsActivity.kt b/app/src/main/java/se/ajpanton/navbuttons/SettingsActivity.kt index deae4c1..467937f 100644 --- a/app/src/main/java/se/ajpanton/navbuttons/SettingsActivity.kt +++ b/app/src/main/java/se/ajpanton/navbuttons/SettingsActivity.kt @@ -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) + } } } } diff --git a/app/src/main/res/layout/item_nav_button_section.xml b/app/src/main/res/layout/item_nav_button_section.xml index 68f183b..16e5915 100644 --- a/app/src/main/res/layout/item_nav_button_section.xml +++ b/app/src/main/res/layout/item_nav_button_section.xml @@ -5,6 +5,7 @@ android:background="@drawable/bg_button_section" android:elevation="2dp" android:orientation="vertical" + android:saveEnabled="false" android:padding="18dp"> + android:popupBackground="@drawable/bg_spinner_popup" + android:saveEnabled="false" /> + android:popupBackground="@drawable/bg_spinner_popup" + android:saveEnabled="false" /> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 40ce52a..c4df638 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -10,8 +10,14 @@ When long-pressed Long-press duration (ms) Stock - Kill foreground app None + Back + Home + Assistant + Recents + Kill foreground app + Toggle auto rotate + Toggle flashlight - + Duration test (hold)