9 Commits
Author SHA1 Message Date
ajp_anton 9fd4586899 Release 2.4 2026-07-16 00:08:35 +00:00
ajp_anton dd444f9fba Remove unused LoadedApk fallback hook 2026-07-15 23:56:59 +00:00
ajp_anton f537f724cd Remove unused settings import 2026-07-15 19:09:30 +00:00
ajp_anton d0313df2b3 Simplify support fallbacks 2026-07-15 19:08:50 +00:00
ajp_anton c260125f6e Deduplicate settings sanitizers 2026-07-15 19:08:07 +00:00
ajp_anton e46cdefe67 Share native side arrow touch handling 2026-07-15 19:06:13 +00:00
ajp_anton 91cdd1a665 Reduce duplicated cleanup code 2026-07-15 18:58:16 +00:00
ajp_anton d2e5af35ed Release 2.3 2026-07-11 19:14:35 +00:00
ajp_anton 09bab5db90 Fix Samsung unfolded long presses 2026-07-11 19:13:12 +00:00
16 changed files with 291 additions and 512 deletions
@@ -31,7 +31,7 @@ internal class ActionExecutor(
NavButtonAction.ASSISTANT -> sendConfiguredKeyPress(KeyEvent.KEYCODE_ASSIST) NavButtonAction.ASSISTANT -> sendConfiguredKeyPress(KeyEvent.KEYCODE_ASSIST)
NavButtonAction.RECENTS -> sendConfiguredKeyPress(KeyEvent.KEYCODE_APP_SWITCH) NavButtonAction.RECENTS -> sendConfiguredKeyPress(KeyEvent.KEYCODE_APP_SWITCH)
NavButtonAction.KILL_FOREGROUND_APP -> performPrivilegedActionOrDirect(PRIVILEGED_ACTION_KILL_FOREGROUND_APP) NavButtonAction.KILL_FOREGROUND_APP -> performPrivilegedActionOrDirect(PRIVILEGED_ACTION_KILL_FOREGROUND_APP)
NavButtonAction.TOGGLE_AUTO_ROTATE -> toggleAutoRotate() NavButtonAction.TOGGLE_AUTO_ROTATE -> performPrivilegedActionOrDirect(PRIVILEGED_ACTION_TOGGLE_AUTO_ROTATE)
NavButtonAction.TOGGLE_FLASHLIGHT -> performPrivilegedActionOrDirect(PRIVILEGED_ACTION_TOGGLE_FLASHLIGHT) NavButtonAction.TOGGLE_FLASHLIGHT -> performPrivilegedActionOrDirect(PRIVILEGED_ACTION_TOGGLE_FLASHLIGHT)
} }
} }
@@ -120,13 +120,14 @@ internal class ActionExecutor(
when (action) { when (action) {
PRIVILEGED_ACTION_KILL_FOREGROUND_APP -> killForegroundApp() PRIVILEGED_ACTION_KILL_FOREGROUND_APP -> killForegroundApp()
PRIVILEGED_ACTION_TOGGLE_FLASHLIGHT -> toggleFlashlight() PRIVILEGED_ACTION_TOGGLE_FLASHLIGHT -> toggleFlashlight()
PRIVILEGED_ACTION_TOGGLE_AUTO_ROTATE -> toggleAutoRotate()
} }
} else { } else {
requestPrivilegedAction(action, null) requestPrivilegedAction(action, null)
} }
} }
private fun toggleAutoRotate() { fun toggleAutoRotate() {
val context = contextProvider() ?: run { val context = contextProvider() ?: run {
log("Context unavailable, cannot toggle auto rotate.") log("Context unavailable, cannot toggle auto rotate.")
return return
@@ -394,44 +394,40 @@ internal class AospLauncherTaskbarHooks(
} }
private fun performAction(controller: Any?, action: NavButtonAction) { private fun performAction(controller: Any?, action: NavButtonAction) {
when (action) { val handled = when (action) {
NavButtonAction.STOCK, NavButtonAction.STOCK,
NavButtonAction.NONE, NavButtonAction.NONE,
-> Unit -> true
NavButtonAction.BACK -> { NavButtonAction.BACK ->
if (!callTaskbarMethod(controller, "executeBack", arrayOf(KeyEvent::class.java), null)) { callTaskbarMethod(controller, "executeBack", arrayOf(KeyEvent::class.java), null)
actions.perform(action)
}
}
NavButtonAction.HOME -> { NavButtonAction.HOME ->
if (!callTaskbarMethod(controller, "navigateHome", emptyArray())) { callTaskbarMethod(controller, "navigateHome", emptyArray())
actions.perform(action)
}
}
NavButtonAction.ASSISTANT -> { NavButtonAction.ASSISTANT ->
if (!callTaskbarMethod(controller, "onLongPressHome", emptyArray())) { callTaskbarMethod(controller, "onLongPressHome", emptyArray())
actions.perform(action)
}
}
NavButtonAction.RECENTS -> { NavButtonAction.RECENTS ->
if (!callTaskbarMethod(controller, "navigateToOverview", emptyArray())) { callTaskbarMethod(controller, "navigateToOverview", emptyArray())
actions.perform(action)
}
}
NavButtonAction.KILL_FOREGROUND_APP -> { NavButtonAction.KILL_FOREGROUND_APP -> {
requestPrivilegedAction(PRIVILEGED_ACTION_KILL_FOREGROUND_APP, null) requestPrivilegedAction(PRIVILEGED_ACTION_KILL_FOREGROUND_APP, null)
true
} }
NavButtonAction.TOGGLE_FLASHLIGHT -> { NavButtonAction.TOGGLE_FLASHLIGHT -> {
requestPrivilegedAction(PRIVILEGED_ACTION_TOGGLE_FLASHLIGHT, null) requestPrivilegedAction(PRIVILEGED_ACTION_TOGGLE_FLASHLIGHT, null)
true
} }
NavButtonAction.TOGGLE_AUTO_ROTATE -> actions.perform(action) NavButtonAction.TOGGLE_AUTO_ROTATE -> {
actions.perform(action)
true
}
}
if (!handled) {
actions.perform(action)
} }
} }
@@ -66,14 +66,7 @@ private fun packageNameFromComponent(component: Any?): String? {
} }
private fun packageNameFromActivityInfo(info: Any?): String? { private fun packageNameFromActivityInfo(info: Any?): String? {
if (info == null) { return getFieldValue(info, "packageName")?.toString()
return null
}
return try {
findField(info.javaClass, "packageName").get(info)?.toString()
} catch (_: Throwable) {
null
}
} }
internal fun shouldSkipForegroundPackage(packageName: String?, defaultLauncher: String?): Boolean { internal fun shouldSkipForegroundPackage(packageName: String?, defaultLauncher: String?): Boolean {
@@ -21,15 +21,8 @@ internal fun sendInjectedKeyPress(context: Context?, keyCode: Int, log: (String)
internal fun inputManagerForInjection(context: Context?): Any? { internal fun inputManagerForInjection(context: Context?): Any? {
context?.getSystemService(Context.INPUT_SERVICE)?.let { return it } context?.getSystemService(Context.INPUT_SERVICE)?.let { return it }
return listOf(INPUT_MANAGER_CLASS, INPUT_MANAGER_GLOBAL_CLASS).firstNotNullOfOrNull { className ->
return try { runCatching { callStaticMethod(findClass(className, null), "getInstance") }.getOrNull()
callStaticMethod(findClass(INPUT_MANAGER_CLASS, null), "getInstance")
} catch (_: Throwable) {
try {
callStaticMethod(findClass(INPUT_MANAGER_GLOBAL_CLASS, null), "getInstance")
} catch (_: Throwable) {
null
}
} }
} }
@@ -72,6 +72,7 @@ internal const val EXTRA_PRIVILEGED_ACTION_TOKEN = "privileged_action_token"
internal const val EXTRA_PRIVILEGED_KEY_CODE = "privileged_key_code" internal const val EXTRA_PRIVILEGED_KEY_CODE = "privileged_key_code"
internal const val PRIVILEGED_ACTION_KILL_FOREGROUND_APP = "kill_foreground_app" internal const val PRIVILEGED_ACTION_KILL_FOREGROUND_APP = "kill_foreground_app"
internal const val PRIVILEGED_ACTION_TOGGLE_FLASHLIGHT = "toggle_flashlight" internal const val PRIVILEGED_ACTION_TOGGLE_FLASHLIGHT = "toggle_flashlight"
internal const val PRIVILEGED_ACTION_TOGGLE_AUTO_ROTATE = "toggle_auto_rotate"
internal const val PRIVILEGED_ACTION_SEND_KEY_PRESS = "send_key_press" internal const val PRIVILEGED_ACTION_SEND_KEY_PRESS = "send_key_press"
internal const val HONEYSPACE_NAV_BUTTON_VIEW = internal const val HONEYSPACE_NAV_BUTTON_VIEW =
@@ -62,6 +62,17 @@ data class NavButtonConfig(
val longPressDurationMs: Int, val longPressDurationMs: Int,
) )
internal fun sanitizeSideArrowRepeatSpeed(stepsPerSecond: Float): Float {
return if (stepsPerSecond.isFinite()) {
stepsPerSecond.coerceIn(
NavButtonSettingsStore.MIN_SIDE_ARROW_REPEAT_STEPS_PER_SECOND,
NavButtonSettingsStore.MAX_SIDE_ARROW_REPEAT_STEPS_PER_SECOND,
)
} else {
NavButtonSettingsStore.DEFAULT_SIDE_ARROW_REPEAT_STEPS_PER_SECOND
}
}
class NavButtonSettingsStore(context: Context) { class NavButtonSettingsStore(context: Context) {
private val localPreferences = context.applicationContext.getSharedPreferences( private val localPreferences = context.applicationContext.getSharedPreferences(
PREFERENCES_NAME, PREFERENCES_NAME,
@@ -82,12 +93,10 @@ class NavButtonSettingsStore(context: Context) {
longPressAction = NavButtonAction.fromStorageValue( longPressAction = NavButtonAction.fromStorageValue(
preferences.getString(longPressActionKey(buttonId), null), preferences.getString(longPressActionKey(buttonId), null),
), ),
longPressDurationMs = sanitizeDuration( longPressDurationMs = preferences.getInt(
preferences.getInt( longPressDurationKey(buttonId),
longPressDurationKey(buttonId), defaultLongPressDurationMs(),
defaultLongPressDurationMs(), ).coerceLongPressDuration(),
),
),
) )
} }
@@ -105,7 +114,7 @@ class NavButtonSettingsStore(context: Context) {
fun setLongPressDurationMs(buttonId: NavButtonId, durationMs: Int) { fun setLongPressDurationMs(buttonId: NavButtonId, durationMs: Int) {
persistChanges { persistChanges {
putInt(longPressDurationKey(buttonId), sanitizeDuration(durationMs)) putInt(longPressDurationKey(buttonId), durationMs.coerceLongPressDuration())
} }
} }
@@ -150,37 +159,20 @@ class NavButtonSettingsStore(context: Context) {
} }
fun getSideArrowLongPressDurationMs(): Int { fun getSideArrowLongPressDurationMs(): Int {
return sanitizeDuration( return currentPreferences().getInt(
currentPreferences().getInt( KEY_SIDE_ARROW_LONG_PRESS_DURATION_MS,
KEY_SIDE_ARROW_LONG_PRESS_DURATION_MS, defaultLongPressDurationMs(),
defaultLongPressDurationMs(), ).coerceLongPressDuration()
),
)
} }
fun setSideArrowLongPressDurationMs(durationMs: Int) { fun setSideArrowLongPressDurationMs(durationMs: Int) {
persistChanges { persistChanges {
putInt(KEY_SIDE_ARROW_LONG_PRESS_DURATION_MS, sanitizeDuration(durationMs)) putInt(KEY_SIDE_ARROW_LONG_PRESS_DURATION_MS, durationMs.coerceLongPressDuration())
} }
} }
fun defaultLongPressDurationMs(): Int { fun defaultLongPressDurationMs(): Int {
return sanitizeDuration(ViewConfiguration.getLongPressTimeout()) return ViewConfiguration.getLongPressTimeout().coerceLongPressDuration()
}
fun sanitizeDuration(durationMs: Int): Int {
return durationMs.coerceIn(MIN_DURATION_MS, MAX_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 {
@@ -223,7 +215,6 @@ class NavButtonSettingsStore(context: Context) {
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 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"
@@ -343,7 +334,13 @@ class NavButtonSettingsStore(context: Context) {
for (key in durationKeys) { for (key in durationKeys) {
if (source.contains(key)) { if (source.contains(key)) {
editor.putInt(key, source.getInt(key, sanitizeDuration(ViewConfiguration.getLongPressTimeout()))) editor.putInt(
key,
source.getInt(
key,
ViewConfiguration.getLongPressTimeout().coerceLongPressDuration(),
).coerceLongPressDuration(),
)
} else { } else {
editor.remove(key) editor.remove(key)
} }
@@ -384,9 +381,5 @@ class NavButtonSettingsStore(context: Context) {
editor.apply() editor.apply()
} }
private fun sanitizeDuration(durationMs: Int): Int {
return durationMs.coerceIn(MIN_DURATION_MS, MAX_DURATION_MS)
}
} }
} }
@@ -1,7 +1,6 @@
package se.ajpanton.navbuttons package se.ajpanton.navbuttons
import android.content.Context import android.content.Context
import android.content.pm.ApplicationInfo
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.os.SystemClock import android.os.SystemClock
@@ -29,7 +28,6 @@ class NavButtons() : XposedModule() {
private var systemServerProcess = false private var systemServerProcess = false
private var launcherHookInstalled = false private var launcherHookInstalled = false
private var aospTaskbarHookInstalled = false private var aospTaskbarHookInstalled = false
private var loadedApkClassLoaderFallbackHookInstalled = false
private var appBindFallbackHookInstalled = false private var appBindFallbackHookInstalled = false
private var lastHomeLongPressHandledAtMs = 0L private var lastHomeLongPressHandledAtMs = 0L
private val targetHookRetryAttempts = mutableMapOf<String, Int>() private val targetHookRetryAttempts = mutableMapOf<String, Int>()
@@ -68,6 +66,7 @@ class NavButtons() : XposedModule() {
}, },
killForegroundApp = actions::killForegroundApp, killForegroundApp = actions::killForegroundApp,
toggleFlashlight = actions::toggleFlashlight, toggleFlashlight = actions::toggleFlashlight,
toggleAutoRotate = actions::toggleAutoRotate,
sendKeyPress = actions::sendKeyPress, sendKeyPress = actions::sendKeyPress,
) )
private val sideArrowLongPresses: SideArrowLongPressController = SideArrowLongPressController( private val sideArrowLongPresses: SideArrowLongPressController = SideArrowLongPressController(
@@ -163,7 +162,6 @@ class NavButtons() : XposedModule() {
override fun getClassLoader(): ClassLoader = classLoader override fun getClassLoader(): ClassLoader = classLoader
}) })
} else { } else {
hookLoadedApkClassLoaderFallback(param.processName)
hookAppBindFallback(param.processName) hookAppBindFallback(param.processName)
handleTargetPackage(param.processName, classLoader) handleTargetPackage(param.processName, classLoader)
scheduleTargetHookRetry(param.processName) scheduleTargetHookRetry(param.processName)
@@ -281,41 +279,6 @@ class NavButtons() : XposedModule() {
} }
} }
private fun hookLoadedApkClassLoaderFallback(processName: String) {
if (loadedApkClassLoaderFallbackHookInstalled || processName !in TARGET_PACKAGES) {
return
}
val loadedApkClass = findClassIfExists("android.app.LoadedApk", null) ?: return
val methods = findMethodsNamed(loadedApkClass, "createOrUpdateClassLoaderLocked")
if (methods.isEmpty()) {
log("LoadedApk classloader fallback method not found.")
return
}
loadedApkClassLoaderFallbackHookInstalled = true
methods.forEach { method ->
deoptimizeMethodIfPresent(method)
hook(method).intercept { chain ->
val result = chain.proceed()
val loadedApk = chain.thisObject ?: return@intercept result
val packageName = getLoadedApkPackageName(loadedApk) ?: return@intercept result
if (packageName in TARGET_PACKAGES) {
val classLoader = getFieldValue(loadedApk, "mClassLoader") as? ClassLoader
handleTargetPackage(packageName, classLoader)
}
result
}
}
}
private fun getLoadedApkPackageName(loadedApk: Any): String? {
val packageName = getFieldValue(loadedApk, "mPackageName") as? String
if (packageName != null) {
return packageName
}
return (getFieldValue(loadedApk, "mApplicationInfo") as? ApplicationInfo)?.packageName
}
private fun getAppBindClassLoader(appBindData: Any?): ClassLoader? { private fun getAppBindClassLoader(appBindData: Any?): ClassLoader? {
val loadedApk = getFieldValue(appBindData, "info") ?: return null val loadedApk = getFieldValue(appBindData, "info") ?: return null
return (getFieldValue(loadedApk, "mClassLoader") as? ClassLoader) return (getFieldValue(loadedApk, "mClassLoader") as? ClassLoader)
@@ -17,6 +17,7 @@ internal class PrivilegedActionBridge(
private val log: (String, Throwable?) -> Unit, private val log: (String, Throwable?) -> Unit,
private val killForegroundApp: () -> Unit, private val killForegroundApp: () -> Unit,
private val toggleFlashlight: () -> Unit, private val toggleFlashlight: () -> Unit,
private val toggleAutoRotate: () -> Unit,
private val sendKeyPress: (Int) -> Unit, private val sendKeyPress: (Int) -> Unit,
) { ) {
private var receiverRegistered = false private var receiverRegistered = false
@@ -78,6 +79,7 @@ internal class PrivilegedActionBridge(
when (action) { when (action) {
PRIVILEGED_ACTION_KILL_FOREGROUND_APP -> killForegroundApp() PRIVILEGED_ACTION_KILL_FOREGROUND_APP -> killForegroundApp()
PRIVILEGED_ACTION_TOGGLE_FLASHLIGHT -> toggleFlashlight() PRIVILEGED_ACTION_TOGGLE_FLASHLIGHT -> toggleFlashlight()
PRIVILEGED_ACTION_TOGGLE_AUTO_ROTATE -> toggleAutoRotate()
PRIVILEGED_ACTION_SEND_KEY_PRESS -> { PRIVILEGED_ACTION_SEND_KEY_PRESS -> {
if (keyCode != KeyEvent.KEYCODE_UNKNOWN) { if (keyCode != KeyEvent.KEYCODE_UNKNOWN) {
sendKeyPress(keyCode) sendKeyPress(keyCode)
@@ -92,12 +94,12 @@ internal class PrivilegedActionBridge(
senderPackage: String?, senderPackage: String?,
senderUid: Int, senderUid: Int,
): Boolean { ): Boolean {
if (senderPackage == SYSTEMUI_PACKAGE || senderPackage in AOSP_LAUNCHER_PACKAGES) { if (isTrustedUiPackage(senderPackage)) {
return true return true
} }
val caller = getParcelableExtraCompat(intent, EXTRA_PRIVILEGED_CALLER, PendingIntent::class.java) val caller = getParcelableExtraCompat(intent, EXTRA_PRIVILEGED_CALLER, PendingIntent::class.java)
if (caller?.creatorPackage == SYSTEMUI_PACKAGE || caller?.creatorPackage in AOSP_LAUNCHER_PACKAGES) { if (isTrustedUiPackage(caller?.creatorPackage)) {
return true return true
} }
if (caller?.creatorUid == Process.SYSTEM_UID) { if (caller?.creatorUid == Process.SYSTEM_UID) {
@@ -117,7 +119,13 @@ internal class PrivilegedActionBridge(
return true return true
} }
return context.packageManager.getPackagesForUid(senderUid) return context.packageManager.getPackagesForUid(senderUid)
?.any { it == SYSTEMUI_PACKAGE || it in AOSP_LAUNCHER_PACKAGES } == true ?.any(::isTrustedUiPackage) == true
}
private fun isTrustedUiPackage(packageName: String?): Boolean {
return packageName == SYSTEMUI_PACKAGE ||
packageName == SAMSUNG_LAUNCHER_PACKAGE ||
packageName in AOSP_LAUNCHER_PACKAGES
} }
private fun readToken(): String? { private fun readToken(): String? {
@@ -126,58 +126,29 @@ internal fun callIntIntMethodIfExists(
second: Int, second: Int,
): Boolean { ): Boolean {
methodNames.forEach { methodName -> methodNames.forEach { methodName ->
findMethodsNamed(target.javaClass, methodName) optionalMethod(target, methodName) { types ->
.firstOrNull { method -> types.size >= 2 && types[0] == Int::class.javaPrimitiveType && types[1] == Int::class.javaPrimitiveType
val types = method.parameterTypes }?.let { method -> return invokeOptional(method, target, first, second) }
types.size >= 2 &&
types[0] == Int::class.javaPrimitiveType &&
types[1] == Int::class.javaPrimitiveType
}
?.let { method ->
return try {
method.invoke(target, first, second)
true
} catch (_: Throwable) {
false
}
}
} }
return false return false
} }
internal fun callIntMethodIfExists(target: Any, methodName: String, value: Int): Boolean { internal fun callIntMethodIfExists(target: Any, methodName: String, value: Int): Boolean {
return findMethodsNamed(target.javaClass, methodName) return invokeOptional(target, methodName, value) { types ->
.firstOrNull { method -> types.size == 1 && types[0] == Int::class.javaPrimitiveType
val types = method.parameterTypes }
types.size == 1 && types[0] == Int::class.javaPrimitiveType
}
?.let { method ->
runCatching { method.invoke(target, value) }.isSuccess
} == true
} }
internal fun callIntIntMethodIfExists(target: Any, methodName: String, first: Int, second: Int): Boolean { internal fun callIntIntMethodIfExists(target: Any, methodName: String, first: Int, second: Int): Boolean {
return findMethodsNamed(target.javaClass, methodName) return invokeOptional(target, methodName, first, second) { types ->
.firstOrNull { method -> types.size == 2 && types[0] == Int::class.javaPrimitiveType && types[1] == Int::class.javaPrimitiveType
val types = method.parameterTypes }
types.size == 2 &&
types[0] == Int::class.javaPrimitiveType &&
types[1] == Int::class.javaPrimitiveType
}
?.let { method ->
runCatching { method.invoke(target, first, second) }.isSuccess
} == true
} }
internal fun callFloatMethodIfExists(target: Any, methodName: String, value: Float): Boolean { internal fun callFloatMethodIfExists(target: Any, methodName: String, value: Float): Boolean {
return findMethodsNamed(target.javaClass, methodName) return invokeOptional(target, methodName, value) { types ->
.firstOrNull { method -> types.size == 1 && types[0] == Float::class.javaPrimitiveType
val types = method.parameterTypes }
types.size == 1 && types[0] == Float::class.javaPrimitiveType
}
?.let { method ->
runCatching { method.invoke(target, value) }.isSuccess
} == true
} }
internal fun callSetButtonImageIfExists( internal fun callSetButtonImageIfExists(
@@ -186,36 +157,47 @@ internal fun callSetButtonImageIfExists(
lightDrawable: Drawable, lightDrawable: Drawable,
darkDrawable: Drawable, darkDrawable: Drawable,
): Boolean { ): Boolean {
return findMethodsNamed(target.javaClass, "setButtonImage") return invokeOptional(target, "setButtonImage", id, lightDrawable, darkDrawable) { types ->
.firstOrNull { method -> types.size == 3 &&
val types = method.parameterTypes types[0] == Int::class.javaPrimitiveType &&
types.size == 3 && Drawable::class.java.isAssignableFrom(types[1]) &&
types[0] == Int::class.javaPrimitiveType && Drawable::class.java.isAssignableFrom(types[2])
Drawable::class.java.isAssignableFrom(types[1]) && }
Drawable::class.java.isAssignableFrom(types[2])
}
?.let { method ->
runCatching { method.invoke(target, id, lightDrawable, darkDrawable) }.isSuccess
} == true
} }
internal fun callMethodIfExists(target: Any, methodName: String): Any? { internal fun callMethodIfExists(target: Any, methodName: String): Any? {
return findMethodsNamed(target.javaClass, methodName) return optionalMethod(target, methodName) { it.isEmpty() }
.firstOrNull { method -> method.parameterTypes.isEmpty() }
?.let { method -> ?.let { method ->
runCatching { method.invoke(target) }.getOrNull() runCatching { method.invoke(target) }.getOrNull()
} }
} }
internal fun callBooleanMethodIfExists(target: Any, methodName: String, value: Boolean): Boolean { internal fun callBooleanMethodIfExists(target: Any, methodName: String, value: Boolean): Boolean {
return findMethodsNamed(target.javaClass, methodName) return invokeOptional(target, methodName, value) { types ->
.firstOrNull { method -> types.size == 1 && types[0] == Boolean::class.javaPrimitiveType
val types = method.parameterTypes }
types.size == 1 && types[0] == Boolean::class.javaPrimitiveType }
}
?.let { method -> private fun optionalMethod(
runCatching { method.invoke(target, value) }.isSuccess target: Any,
} ?: false methodName: String,
accepts: (Array<Class<*>>) -> Boolean,
): Method? {
return findMethodsNamed(target.javaClass, methodName).firstOrNull { accepts(it.parameterTypes) }
}
private fun invokeOptional(
target: Any,
methodName: String,
vararg args: Any?,
accepts: (Array<Class<*>>) -> Boolean,
): Boolean {
val method = optionalMethod(target, methodName, accepts) ?: return false
return invokeOptional(method, target, *args)
}
private fun invokeOptional(method: Method, target: Any, vararg args: Any?): Boolean {
return runCatching { method.invoke(target, *args) }.isSuccess
} }
internal fun getFieldValue(target: Any?, fieldName: String): Any? { internal fun getFieldValue(target: Any?, fieldName: String): Any? {
@@ -3,6 +3,7 @@ package se.ajpanton.navbuttons
import android.content.Context import android.content.Context
import android.os.Handler import android.os.Handler
import android.os.SystemClock import android.os.SystemClock
import android.view.HapticFeedbackConstants
import android.view.KeyEvent import android.view.KeyEvent
import android.view.MotionEvent import android.view.MotionEvent
import android.view.View import android.view.View
@@ -28,6 +29,7 @@ internal class SamsungHoneyspaceHooks(
private val log: (String) -> Unit, private val log: (String) -> Unit,
) { ) {
private val longPressTriggered = WeakHashMap<Any, Boolean>() private val longPressTriggered = WeakHashMap<Any, Boolean>()
private val customLongPressRunnables = WeakHashMap<Any, Runnable>()
private val trackedNavContainers = Collections.newSetFromMap(WeakHashMap<Any, Boolean>()) private val trackedNavContainers = Collections.newSetFromMap(WeakHashMap<Any, Boolean>())
private val nativeSideArrowStates = WeakHashMap<Any, Boolean>() private val nativeSideArrowStates = WeakHashMap<Any, Boolean>()
private val homeStockLongPressGuard = HomeStockLongPressGuard( private val homeStockLongPressGuard = HomeStockLongPressGuard(
@@ -74,7 +76,7 @@ internal class SamsungHoneyspaceHooks(
registerSettingsReceiver() registerSettingsReceiver()
trackNavContainerFrom(navButtonView as? View) trackNavContainerFrom(navButtonView as? View)
return@hookMethod handleNativeSideArrowTouch(navButtonView, keyCode, motionEvent) { return@hookMethod sideArrowLongPresses.handleNativeTouch(navButtonView, keyCode, motionEvent) {
chain.proceed() chain.proceed()
} }
} }
@@ -86,20 +88,26 @@ internal class SamsungHoneyspaceHooks(
when (motionEvent.actionMasked) { when (motionEvent.actionMasked) {
MotionEvent.ACTION_DOWN -> { MotionEvent.ACTION_DOWN -> {
longPressTriggered[navButtonView] = false longPressTriggered[navButtonView] = false
cancelCustomLongPress(navButtonView)
homeStockLongPressGuard.cancel(navButtonView) homeStockLongPressGuard.cancel(navButtonView)
homeStockLongPressGuard.clear(navButtonView) homeStockLongPressGuard.clear(navButtonView)
val result = chain.proceed() val result = chain.proceed()
rescheduleHoneyspaceLongPress( val config = configProvider(buttonId)
navButtonView, if (config.longPressAction != NavButtonAction.STOCK) {
runnableClass, cancelHoneyspaceLongPress(navButtonView, runnableClass)
configProvider(buttonId).longPressDurationMs, scheduleCustomLongPress(
) navButtonView,
config.longPressAction,
config.longPressDurationMs,
)
}
homeStockLongPressGuard.schedule(navButtonView, buttonId) homeStockLongPressGuard.schedule(navButtonView, buttonId)
result result
} }
MotionEvent.ACTION_UP -> { MotionEvent.ACTION_UP -> {
val triggered = longPressTriggered[navButtonView] == true val triggered = longPressTriggered[navButtonView] == true
cancelCustomLongPress(navButtonView)
if (triggered) { if (triggered) {
return@hookMethod proceedWithCanceledAction(motionEvent) { return@hookMethod proceedWithCanceledAction(motionEvent) {
chain.proceed() chain.proceed()
@@ -137,6 +145,7 @@ internal class SamsungHoneyspaceHooks(
} }
MotionEvent.ACTION_CANCEL -> { MotionEvent.ACTION_CANCEL -> {
cancelCustomLongPress(navButtonView)
homeStockLongPressGuard.cancel(navButtonView) homeStockLongPressGuard.cancel(navButtonView)
longPressTriggered.remove(navButtonView) longPressTriggered.remove(navButtonView)
homeStockLongPressGuard.clear(navButtonView) homeStockLongPressGuard.clear(navButtonView)
@@ -179,6 +188,30 @@ internal class SamsungHoneyspaceHooks(
return true return true
} }
private fun scheduleCustomLongPress(
navButtonView: Any,
action: NavButtonAction,
durationMs: Int,
) {
val view = navButtonView as? View ?: return
val runnable = Runnable {
updateContextFromView(navButtonView)
longPressTriggered[navButtonView] = true
markLongClicked(navButtonView)
homeStockLongPressGuard.cancel(navButtonView)
view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
performAction(action)
}
customLongPressRunnables[navButtonView] = runnable
view.postDelayed(runnable, durationMs.toLong())
}
private fun cancelCustomLongPress(navButtonView: Any) {
customLongPressRunnables.remove(navButtonView)?.let { runnable ->
(navButtonView as? View)?.removeCallbacks(runnable)
}
}
fun refreshSideArrowLayouts(reason: String) { fun refreshSideArrowLayouts(reason: String) {
val containers = trackedNavContainers.toList() val containers = trackedNavContainers.toList()
containers.forEach { container -> containers.forEach { container ->
@@ -333,28 +366,6 @@ internal class SamsungHoneyspaceHooks(
restoreHoneyspaceSideArrowDrawables(container) restoreHoneyspaceSideArrowDrawables(container)
} }
private fun handleNativeSideArrowTouch(
view: Any,
keyCode: Int,
event: MotionEvent,
proceed: () -> Any?,
): Any? {
return when (event.actionMasked) {
MotionEvent.ACTION_DOWN -> {
sideArrowLongPresses.startAfter(view, keyCode, proceed)
}
MotionEvent.ACTION_UP,
MotionEvent.ACTION_CANCEL,
-> {
sideArrowLongPresses.finish(view)
proceed()
}
else -> proceed()
}
}
private fun markLongClicked(navButtonView: Any) { private fun markLongClicked(navButtonView: Any) {
try { try {
callMethod( callMethod(
@@ -48,15 +48,10 @@ internal fun isHoneyspaceNavButtonContainer(view: View): Boolean {
honeyspaceViewIdName(view) == "navbar_button_container" honeyspaceViewIdName(view) == "navbar_button_container"
} }
internal fun rescheduleHoneyspaceLongPress( internal fun cancelHoneyspaceLongPress(navButtonView: Any, runnableClass: Class<*>) {
navButtonView: Any,
runnableClass: Class<*>,
durationMs: Int,
) {
val view = navButtonView as? View ?: return val view = navButtonView as? View ?: return
val longPressRunnable = findHoneyspaceLongPressRunnable(navButtonView, runnableClass) ?: return val longPressRunnable = findHoneyspaceLongPressRunnable(navButtonView, runnableClass) ?: return
view.removeCallbacks(longPressRunnable) view.removeCallbacks(longPressRunnable)
view.postDelayed(longPressRunnable, durationMs.toLong())
} }
internal fun createHoneyspaceExtraButton( internal fun createHoneyspaceExtraButton(
@@ -19,7 +19,6 @@ 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.text.DecimalFormatSymbols
import java.util.Locale import java.util.Locale
@@ -224,6 +223,77 @@ class SettingsActivity : Activity() {
} }
} }
private fun EditText.afterTextChanged(onChanged: (String) -> Unit) {
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?) = onChanged(editable?.toString().orEmpty())
})
}
private inner class NumericField(
private val input: EditText,
private val minusButton: Button,
private val plusButton: Button,
private val step: Float,
private val parse: (String) -> Float?,
private val sanitize: (Float) -> Float,
private val format: (Float) -> String,
private val persist: (Float) -> Unit,
) {
private var lastValid = 0f
private var updatingText = false
fun bind(initialValue: Float) {
apply(initialValue, persist = false)
input.setOnFocusChangeListener { _, hasFocus ->
if (!hasFocus) {
normalize()
}
}
input.afterTextChanged { text ->
if (updatingText) {
return@afterTextChanged
}
val parsed = parse(text) ?: return@afterTextChanged
if (parsed == sanitize(parsed)) {
lastValid = parsed
persist(parsed)
sendSettingsChangedBroadcast()
}
}
attachRepeatButton(minusButton) { adjust(-step) }
attachRepeatButton(plusButton) { adjust(step) }
}
fun normalize(): Float {
val normalized = sanitize(parse(input.text.toString()) ?: lastValid)
apply(normalized, persist = true)
return normalized
}
private fun adjust(delta: Float) {
apply(normalize() + delta, persist = true)
}
private fun apply(value: Float, persist: Boolean) {
val sanitized = sanitize(value)
val text = format(sanitized)
updatingText = true
if (input.text.toString() != text) {
input.setText(text)
input.setSelection(input.text.length)
}
updatingText = false
lastValid = sanitized
if (persist) {
persist(sanitized)
sendSettingsChangedBroadcast()
}
}
}
private inner class NavButtonSectionController( private inner class NavButtonSectionController(
sectionView: View, sectionView: View,
private val buttonId: NavButtonId, private val buttonId: NavButtonId,
@@ -237,8 +307,7 @@ class SettingsActivity : Activity() {
private val plusButton: Button = sectionView.findViewById(R.id.plus_button) private val plusButton: Button = sectionView.findViewById(R.id.plus_button)
private val testButton: Button = sectionView.findViewById(R.id.test_button) private val testButton: Button = sectionView.findViewById(R.id.test_button)
private var lastValidDurationMs = settingsStore.defaultLongPressDurationMs() private lateinit var durationField: NumericField
private var updatingDurationText = false
fun bind() { fun bind() {
val config = settingsStore.getConfig(buttonId) val config = settingsStore.getConfig(buttonId)
@@ -255,10 +324,14 @@ class SettingsActivity : Activity() {
settingsStore::setLongPressAction, settingsStore::setLongPressAction,
) )
applyDuration(config.longPressDurationMs, persist = false) durationField = durationField(
bindDurationInput() durationInput,
bindStepButtons() minusButton,
attachTestButton(testButton, ::resolveDurationForUse) plusButton,
persist = { settingsStore.setLongPressDurationMs(buttonId, it) },
)
durationField.bind(config.longPressDurationMs.toFloat())
attachTestButton(testButton) { durationField.normalize().toInt() }
} }
private fun bindActionSpinner( private fun bindActionSpinner(
@@ -276,89 +349,6 @@ class SettingsActivity : Activity() {
sendSettingsChangedBroadcast() sendSettingsChangedBroadcast()
} }
} }
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.setLongPressDurationMs(buttonId, parsed)
sendSettingsChangedBroadcast()
}
}
})
}
private fun bindStepButtons() {
attachRepeatButton(minusButton) {
adjustDuration(-NavButtonSettingsStore.DURATION_STEP_MS)
}
attachRepeatButton(plusButton) {
adjustDuration(NavButtonSettingsStore.DURATION_STEP_MS)
}
}
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.setLongPressDurationMs(buttonId, sanitized)
sendSettingsChangedBroadcast()
}
}
} }
private inner class SideArrowsSectionController(sectionView: View) { private inner class SideArrowsSectionController(sectionView: View) {
@@ -384,21 +374,31 @@ class SettingsActivity : Activity() {
private val testButton: Button = sectionView.findViewById(R.id.side_arrow_test_button) private val testButton: Button = sectionView.findViewById(R.id.side_arrow_test_button)
private val decimalSeparator = DecimalFormatSymbols.getInstance().decimalSeparator private val decimalSeparator = DecimalFormatSymbols.getInstance().decimalSeparator
private var lastValidDurationMs = settingsStore.defaultLongPressDurationMs() private lateinit var durationField: NumericField
private var lastValidRepeatSpeed = NavButtonSettingsStore.DEFAULT_SIDE_ARROW_REPEAT_STEPS_PER_SECOND private lateinit var repeatSpeedField: NumericField
private var updatingDurationText = false
private var updatingRepeatSpeedText = false
fun bind() { fun bind() {
sideArrowsCheckbox.isChecked = settingsStore.isSideArrowsEnabled() sideArrowsCheckbox.isChecked = settingsStore.isSideArrowsEnabled()
bindLongPressActionSpinner() bindLongPressActionSpinner()
applyRepeatSpeed(settingsStore.getSideArrowRepeatStepsPerSecond(), persist = false) repeatSpeedField = NumericField(
bindRepeatSpeedInput() repeatSpeedInput,
bindRepeatSpeedButtons() speedMinusButton,
applyDuration(settingsStore.getSideArrowLongPressDurationMs(), persist = false) speedPlusButton,
bindDurationInput() NavButtonSettingsStore.SIDE_ARROW_REPEAT_SPEED_STEP,
bindDurationButtons() parse = ::parseRepeatSpeed,
attachTestButton(testButton, ::resolveDurationForUse) sanitize = ::sanitizeSideArrowRepeatSpeed,
format = ::formatRepeatSpeed,
persist = settingsStore::setSideArrowRepeatStepsPerSecond,
)
repeatSpeedField.bind(settingsStore.getSideArrowRepeatStepsPerSecond())
durationField = durationField(
durationInput,
durationMinusButton,
durationPlusButton,
persist = settingsStore::setSideArrowLongPressDurationMs,
)
durationField.bind(settingsStore.getSideArrowLongPressDurationMs().toFloat())
attachTestButton(testButton) { durationField.normalize().toInt() }
updateOptionsEnabled() updateOptionsEnabled()
sideArrowsCheckbox.setOnCheckedChangeListener { _, isChecked -> sideArrowsCheckbox.setOnCheckedChangeListener { _, isChecked ->
@@ -422,100 +422,6 @@ class SettingsActivity : Activity() {
updateRepeatSpeedVisibility(settingsStore.getSideArrowLongPressAction()) 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)
sendSettingsChangedBroadcast()
}
}
})
}
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)
sendSettingsChangedBroadcast()
}
}
})
}
private fun bindDurationButtons() {
attachRepeatButton(durationMinusButton) {
adjustDuration(-NavButtonSettingsStore.DURATION_STEP_MS)
}
attachRepeatButton(durationPlusButton) {
adjustDuration(NavButtonSettingsStore.DURATION_STEP_MS)
}
}
private fun updateOptionsEnabled() { private fun updateOptionsEnabled() {
val enabled = sideArrowsCheckbox.isChecked val enabled = sideArrowsCheckbox.isChecked
optionsContainer.alpha = if (enabled) 1f else 0.45f optionsContainer.alpha = if (enabled) 1f else 0.45f
@@ -536,38 +442,6 @@ class SettingsActivity : Activity() {
if (action == SideArrowLongPressAction.MULTIPLE_STEPS) View.VISIBLE else View.GONE 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)
sendSettingsChangedBroadcast()
}
}
private fun parseRepeatSpeed(text: String?): Float? { private fun parseRepeatSpeed(text: String?): Float? {
return text return text
?.trim() ?.trim()
@@ -586,42 +460,23 @@ class SettingsActivity : Activity() {
return raw.replace('.', decimalSeparator) 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)
sendSettingsChangedBroadcast()
}
}
private fun durationField(
input: EditText,
minusButton: Button,
plusButton: Button,
persist: (Int) -> Unit,
): NumericField {
return NumericField(
input,
minusButton,
plusButton,
NavButtonSettingsStore.DURATION_STEP_MS.toFloat(),
parse = { it.toIntOrNull()?.toFloat() },
sanitize = { it.toInt().coerceLongPressDuration().toFloat() },
format = { it.toInt().toString() },
persist = { persist(it.toInt()) },
)
} }
} }
@@ -211,14 +211,4 @@ internal class SettingsRepository(
) )
} }
private fun sanitizeSideArrowRepeatSpeed(stepsPerSecond: Float): Float {
return if (stepsPerSecond.isFinite()) {
stepsPerSecond.coerceIn(
NavButtonSettingsStore.MIN_SIDE_ARROW_REPEAT_STEPS_PER_SECOND,
NavButtonSettingsStore.MAX_SIDE_ARROW_REPEAT_STEPS_PER_SECOND,
)
} else {
NavButtonSettingsStore.DEFAULT_SIDE_ARROW_REPEAT_STEPS_PER_SECOND
}
}
} }
@@ -1,6 +1,7 @@
package se.ajpanton.navbuttons package se.ajpanton.navbuttons
import android.os.Handler import android.os.Handler
import android.view.MotionEvent
import android.view.View import android.view.View
import java.util.WeakHashMap import java.util.WeakHashMap
@@ -27,6 +28,25 @@ internal class SideArrowLongPressController(
return result return result
} }
fun handleNativeTouch(
target: Any,
keyCode: Int,
event: MotionEvent,
proceed: () -> Any?,
): Any? {
return when (event.actionMasked) {
MotionEvent.ACTION_DOWN -> startAfter(target, keyCode, proceed)
MotionEvent.ACTION_UP,
MotionEvent.ACTION_CANCEL,
-> {
finish(target)
proceed()
}
else -> proceed()
}
}
fun finish(target: Any): Boolean { fun finish(target: Any): Boolean {
val wasTriggered = triggered[target] == true val wasTriggered = triggered[target] == true
cancel(target) cancel(target)
@@ -86,7 +86,7 @@ internal class SystemUiNavHooks(
registerSettingsReceiver() registerSettingsReceiver()
trackSamsungNavigationBarView(keyButtonView) trackSamsungNavigationBarView(keyButtonView)
configureAospSideArrowButton(keyButtonView) configureAospSideArrowButton(keyButtonView)
return@hookMethod handleNativeSideArrowTouch(keyButtonView, keyCode, motionEvent) { return@hookMethod sideArrowLongPresses.handleNativeTouch(keyButtonView, keyCode, motionEvent) {
chain.proceed() chain.proceed()
} }
} }
@@ -205,28 +205,6 @@ internal class SystemUiNavHooks(
} }
} }
private fun handleNativeSideArrowTouch(
view: Any,
keyCode: Int,
event: MotionEvent,
proceed: () -> Any?,
): Any? {
return when (event.actionMasked) {
MotionEvent.ACTION_DOWN -> {
sideArrowLongPresses.startAfter(view, keyCode, proceed)
}
MotionEvent.ACTION_UP,
MotionEvent.ACTION_CANCEL,
-> {
sideArrowLongPresses.finish(view)
proceed()
}
else -> proceed()
}
}
private fun markLongClicked(keyButtonView: Any) { private fun markLongClicked(keyButtonView: Any) {
try { try {
findField(keyButtonView.javaClass, "mLongClicked").setBoolean(keyButtonView, true) findField(keyButtonView.javaClass, "mLongClicked").setBoolean(keyButtonView, true)
+1 -1
View File
@@ -1 +1 @@
version=2.2 version=2.4