Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9fd4586899 | ||
|
|
dd444f9fba | ||
|
|
f537f724cd | ||
|
|
d0313df2b3 | ||
|
|
c260125f6e | ||
|
|
e46cdefe67 | ||
|
|
91cdd1a665 | ||
|
|
d2e5af35ed | ||
|
|
09bab5db90 |
@@ -31,7 +31,7 @@ internal class ActionExecutor(
|
||||
NavButtonAction.ASSISTANT -> sendConfiguredKeyPress(KeyEvent.KEYCODE_ASSIST)
|
||||
NavButtonAction.RECENTS -> sendConfiguredKeyPress(KeyEvent.KEYCODE_APP_SWITCH)
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -120,13 +120,14 @@ internal class ActionExecutor(
|
||||
when (action) {
|
||||
PRIVILEGED_ACTION_KILL_FOREGROUND_APP -> killForegroundApp()
|
||||
PRIVILEGED_ACTION_TOGGLE_FLASHLIGHT -> toggleFlashlight()
|
||||
PRIVILEGED_ACTION_TOGGLE_AUTO_ROTATE -> toggleAutoRotate()
|
||||
}
|
||||
} else {
|
||||
requestPrivilegedAction(action, null)
|
||||
}
|
||||
}
|
||||
|
||||
private fun toggleAutoRotate() {
|
||||
fun toggleAutoRotate() {
|
||||
val context = contextProvider() ?: run {
|
||||
log("Context unavailable, cannot toggle auto rotate.")
|
||||
return
|
||||
|
||||
@@ -394,44 +394,40 @@ internal class AospLauncherTaskbarHooks(
|
||||
}
|
||||
|
||||
private fun performAction(controller: Any?, action: NavButtonAction) {
|
||||
when (action) {
|
||||
val handled = when (action) {
|
||||
NavButtonAction.STOCK,
|
||||
NavButtonAction.NONE,
|
||||
-> Unit
|
||||
-> true
|
||||
|
||||
NavButtonAction.BACK -> {
|
||||
if (!callTaskbarMethod(controller, "executeBack", arrayOf(KeyEvent::class.java), null)) {
|
||||
actions.perform(action)
|
||||
}
|
||||
}
|
||||
NavButtonAction.BACK ->
|
||||
callTaskbarMethod(controller, "executeBack", arrayOf(KeyEvent::class.java), null)
|
||||
|
||||
NavButtonAction.HOME -> {
|
||||
if (!callTaskbarMethod(controller, "navigateHome", emptyArray())) {
|
||||
actions.perform(action)
|
||||
}
|
||||
}
|
||||
NavButtonAction.HOME ->
|
||||
callTaskbarMethod(controller, "navigateHome", emptyArray())
|
||||
|
||||
NavButtonAction.ASSISTANT -> {
|
||||
if (!callTaskbarMethod(controller, "onLongPressHome", emptyArray())) {
|
||||
actions.perform(action)
|
||||
}
|
||||
}
|
||||
NavButtonAction.ASSISTANT ->
|
||||
callTaskbarMethod(controller, "onLongPressHome", emptyArray())
|
||||
|
||||
NavButtonAction.RECENTS -> {
|
||||
if (!callTaskbarMethod(controller, "navigateToOverview", emptyArray())) {
|
||||
actions.perform(action)
|
||||
}
|
||||
}
|
||||
NavButtonAction.RECENTS ->
|
||||
callTaskbarMethod(controller, "navigateToOverview", emptyArray())
|
||||
|
||||
NavButtonAction.KILL_FOREGROUND_APP -> {
|
||||
requestPrivilegedAction(PRIVILEGED_ACTION_KILL_FOREGROUND_APP, null)
|
||||
true
|
||||
}
|
||||
|
||||
NavButtonAction.TOGGLE_FLASHLIGHT -> {
|
||||
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? {
|
||||
if (info == null) {
|
||||
return null
|
||||
}
|
||||
return try {
|
||||
findField(info.javaClass, "packageName").get(info)?.toString()
|
||||
} catch (_: Throwable) {
|
||||
null
|
||||
}
|
||||
return getFieldValue(info, "packageName")?.toString()
|
||||
}
|
||||
|
||||
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? {
|
||||
context?.getSystemService(Context.INPUT_SERVICE)?.let { return it }
|
||||
|
||||
return try {
|
||||
callStaticMethod(findClass(INPUT_MANAGER_CLASS, null), "getInstance")
|
||||
} catch (_: Throwable) {
|
||||
try {
|
||||
callStaticMethod(findClass(INPUT_MANAGER_GLOBAL_CLASS, null), "getInstance")
|
||||
} catch (_: Throwable) {
|
||||
null
|
||||
}
|
||||
return listOf(INPUT_MANAGER_CLASS, INPUT_MANAGER_GLOBAL_CLASS).firstNotNullOfOrNull { className ->
|
||||
runCatching { callStaticMethod(findClass(className, null), "getInstance") }.getOrNull()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 PRIVILEGED_ACTION_KILL_FOREGROUND_APP = "kill_foreground_app"
|
||||
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 HONEYSPACE_NAV_BUTTON_VIEW =
|
||||
|
||||
@@ -62,6 +62,17 @@ data class NavButtonConfig(
|
||||
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) {
|
||||
private val localPreferences = context.applicationContext.getSharedPreferences(
|
||||
PREFERENCES_NAME,
|
||||
@@ -82,12 +93,10 @@ class NavButtonSettingsStore(context: Context) {
|
||||
longPressAction = NavButtonAction.fromStorageValue(
|
||||
preferences.getString(longPressActionKey(buttonId), null),
|
||||
),
|
||||
longPressDurationMs = sanitizeDuration(
|
||||
preferences.getInt(
|
||||
longPressDurationKey(buttonId),
|
||||
defaultLongPressDurationMs(),
|
||||
),
|
||||
),
|
||||
longPressDurationMs = preferences.getInt(
|
||||
longPressDurationKey(buttonId),
|
||||
defaultLongPressDurationMs(),
|
||||
).coerceLongPressDuration(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -105,7 +114,7 @@ class NavButtonSettingsStore(context: Context) {
|
||||
|
||||
fun setLongPressDurationMs(buttonId: NavButtonId, durationMs: Int) {
|
||||
persistChanges {
|
||||
putInt(longPressDurationKey(buttonId), sanitizeDuration(durationMs))
|
||||
putInt(longPressDurationKey(buttonId), durationMs.coerceLongPressDuration())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,37 +159,20 @@ class NavButtonSettingsStore(context: Context) {
|
||||
}
|
||||
|
||||
fun getSideArrowLongPressDurationMs(): Int {
|
||||
return sanitizeDuration(
|
||||
currentPreferences().getInt(
|
||||
KEY_SIDE_ARROW_LONG_PRESS_DURATION_MS,
|
||||
defaultLongPressDurationMs(),
|
||||
),
|
||||
)
|
||||
return currentPreferences().getInt(
|
||||
KEY_SIDE_ARROW_LONG_PRESS_DURATION_MS,
|
||||
defaultLongPressDurationMs(),
|
||||
).coerceLongPressDuration()
|
||||
}
|
||||
|
||||
fun setSideArrowLongPressDurationMs(durationMs: Int) {
|
||||
persistChanges {
|
||||
putInt(KEY_SIDE_ARROW_LONG_PRESS_DURATION_MS, sanitizeDuration(durationMs))
|
||||
putInt(KEY_SIDE_ARROW_LONG_PRESS_DURATION_MS, durationMs.coerceLongPressDuration())
|
||||
}
|
||||
}
|
||||
|
||||
fun defaultLongPressDurationMs(): Int {
|
||||
return sanitizeDuration(ViewConfiguration.getLongPressTimeout())
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
return ViewConfiguration.getLongPressTimeout().coerceLongPressDuration()
|
||||
}
|
||||
|
||||
private fun currentPreferences(): SharedPreferences {
|
||||
@@ -223,7 +215,6 @@ class NavButtonSettingsStore(context: Context) {
|
||||
private const val TAG = "NavButtons"
|
||||
const val PREFERENCES_NAME = "nav_button_settings"
|
||||
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_LONG_PRESS_ACTION = "long_press_action"
|
||||
const val KEY_LONG_PRESS_DURATION_MS = "long_press_duration_ms"
|
||||
@@ -343,7 +334,13 @@ class NavButtonSettingsStore(context: Context) {
|
||||
|
||||
for (key in durationKeys) {
|
||||
if (source.contains(key)) {
|
||||
editor.putInt(key, source.getInt(key, sanitizeDuration(ViewConfiguration.getLongPressTimeout())))
|
||||
editor.putInt(
|
||||
key,
|
||||
source.getInt(
|
||||
key,
|
||||
ViewConfiguration.getLongPressTimeout().coerceLongPressDuration(),
|
||||
).coerceLongPressDuration(),
|
||||
)
|
||||
} else {
|
||||
editor.remove(key)
|
||||
}
|
||||
@@ -384,9 +381,5 @@ class NavButtonSettingsStore(context: Context) {
|
||||
|
||||
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
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.os.SystemClock
|
||||
@@ -29,7 +28,6 @@ class NavButtons() : XposedModule() {
|
||||
private var systemServerProcess = false
|
||||
private var launcherHookInstalled = false
|
||||
private var aospTaskbarHookInstalled = false
|
||||
private var loadedApkClassLoaderFallbackHookInstalled = false
|
||||
private var appBindFallbackHookInstalled = false
|
||||
private var lastHomeLongPressHandledAtMs = 0L
|
||||
private val targetHookRetryAttempts = mutableMapOf<String, Int>()
|
||||
@@ -68,6 +66,7 @@ class NavButtons() : XposedModule() {
|
||||
},
|
||||
killForegroundApp = actions::killForegroundApp,
|
||||
toggleFlashlight = actions::toggleFlashlight,
|
||||
toggleAutoRotate = actions::toggleAutoRotate,
|
||||
sendKeyPress = actions::sendKeyPress,
|
||||
)
|
||||
private val sideArrowLongPresses: SideArrowLongPressController = SideArrowLongPressController(
|
||||
@@ -163,7 +162,6 @@ class NavButtons() : XposedModule() {
|
||||
override fun getClassLoader(): ClassLoader = classLoader
|
||||
})
|
||||
} else {
|
||||
hookLoadedApkClassLoaderFallback(param.processName)
|
||||
hookAppBindFallback(param.processName)
|
||||
handleTargetPackage(param.processName, classLoader)
|
||||
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? {
|
||||
val loadedApk = getFieldValue(appBindData, "info") ?: return null
|
||||
return (getFieldValue(loadedApk, "mClassLoader") as? ClassLoader)
|
||||
|
||||
@@ -17,6 +17,7 @@ internal class PrivilegedActionBridge(
|
||||
private val log: (String, Throwable?) -> Unit,
|
||||
private val killForegroundApp: () -> Unit,
|
||||
private val toggleFlashlight: () -> Unit,
|
||||
private val toggleAutoRotate: () -> Unit,
|
||||
private val sendKeyPress: (Int) -> Unit,
|
||||
) {
|
||||
private var receiverRegistered = false
|
||||
@@ -78,6 +79,7 @@ internal class PrivilegedActionBridge(
|
||||
when (action) {
|
||||
PRIVILEGED_ACTION_KILL_FOREGROUND_APP -> killForegroundApp()
|
||||
PRIVILEGED_ACTION_TOGGLE_FLASHLIGHT -> toggleFlashlight()
|
||||
PRIVILEGED_ACTION_TOGGLE_AUTO_ROTATE -> toggleAutoRotate()
|
||||
PRIVILEGED_ACTION_SEND_KEY_PRESS -> {
|
||||
if (keyCode != KeyEvent.KEYCODE_UNKNOWN) {
|
||||
sendKeyPress(keyCode)
|
||||
@@ -92,12 +94,12 @@ internal class PrivilegedActionBridge(
|
||||
senderPackage: String?,
|
||||
senderUid: Int,
|
||||
): Boolean {
|
||||
if (senderPackage == SYSTEMUI_PACKAGE || senderPackage in AOSP_LAUNCHER_PACKAGES) {
|
||||
if (isTrustedUiPackage(senderPackage)) {
|
||||
return true
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
if (caller?.creatorUid == Process.SYSTEM_UID) {
|
||||
@@ -117,7 +119,13 @@ internal class PrivilegedActionBridge(
|
||||
return true
|
||||
}
|
||||
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? {
|
||||
|
||||
@@ -126,58 +126,29 @@ internal fun callIntIntMethodIfExists(
|
||||
second: Int,
|
||||
): Boolean {
|
||||
methodNames.forEach { methodName ->
|
||||
findMethodsNamed(target.javaClass, methodName)
|
||||
.firstOrNull { method ->
|
||||
val types = method.parameterTypes
|
||||
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
|
||||
}
|
||||
}
|
||||
optionalMethod(target, methodName) { types ->
|
||||
types.size >= 2 && types[0] == Int::class.javaPrimitiveType && types[1] == Int::class.javaPrimitiveType
|
||||
}?.let { method -> return invokeOptional(method, target, first, second) }
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
internal fun callIntMethodIfExists(target: Any, methodName: String, value: Int): Boolean {
|
||||
return findMethodsNamed(target.javaClass, methodName)
|
||||
.firstOrNull { method ->
|
||||
val types = method.parameterTypes
|
||||
types.size == 1 && types[0] == Int::class.javaPrimitiveType
|
||||
}
|
||||
?.let { method ->
|
||||
runCatching { method.invoke(target, value) }.isSuccess
|
||||
} == true
|
||||
return invokeOptional(target, methodName, value) { types ->
|
||||
types.size == 1 && types[0] == Int::class.javaPrimitiveType
|
||||
}
|
||||
}
|
||||
|
||||
internal fun callIntIntMethodIfExists(target: Any, methodName: String, first: Int, second: Int): Boolean {
|
||||
return findMethodsNamed(target.javaClass, methodName)
|
||||
.firstOrNull { method ->
|
||||
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
|
||||
return invokeOptional(target, methodName, first, second) { types ->
|
||||
types.size == 2 && types[0] == Int::class.javaPrimitiveType && types[1] == Int::class.javaPrimitiveType
|
||||
}
|
||||
}
|
||||
|
||||
internal fun callFloatMethodIfExists(target: Any, methodName: String, value: Float): Boolean {
|
||||
return findMethodsNamed(target.javaClass, methodName)
|
||||
.firstOrNull { method ->
|
||||
val types = method.parameterTypes
|
||||
types.size == 1 && types[0] == Float::class.javaPrimitiveType
|
||||
}
|
||||
?.let { method ->
|
||||
runCatching { method.invoke(target, value) }.isSuccess
|
||||
} == true
|
||||
return invokeOptional(target, methodName, value) { types ->
|
||||
types.size == 1 && types[0] == Float::class.javaPrimitiveType
|
||||
}
|
||||
}
|
||||
|
||||
internal fun callSetButtonImageIfExists(
|
||||
@@ -186,36 +157,47 @@ internal fun callSetButtonImageIfExists(
|
||||
lightDrawable: Drawable,
|
||||
darkDrawable: Drawable,
|
||||
): Boolean {
|
||||
return findMethodsNamed(target.javaClass, "setButtonImage")
|
||||
.firstOrNull { method ->
|
||||
val types = method.parameterTypes
|
||||
types.size == 3 &&
|
||||
types[0] == Int::class.javaPrimitiveType &&
|
||||
Drawable::class.java.isAssignableFrom(types[1]) &&
|
||||
Drawable::class.java.isAssignableFrom(types[2])
|
||||
}
|
||||
?.let { method ->
|
||||
runCatching { method.invoke(target, id, lightDrawable, darkDrawable) }.isSuccess
|
||||
} == true
|
||||
return invokeOptional(target, "setButtonImage", id, lightDrawable, darkDrawable) { types ->
|
||||
types.size == 3 &&
|
||||
types[0] == Int::class.javaPrimitiveType &&
|
||||
Drawable::class.java.isAssignableFrom(types[1]) &&
|
||||
Drawable::class.java.isAssignableFrom(types[2])
|
||||
}
|
||||
}
|
||||
|
||||
internal fun callMethodIfExists(target: Any, methodName: String): Any? {
|
||||
return findMethodsNamed(target.javaClass, methodName)
|
||||
.firstOrNull { method -> method.parameterTypes.isEmpty() }
|
||||
return optionalMethod(target, methodName) { it.isEmpty() }
|
||||
?.let { method ->
|
||||
runCatching { method.invoke(target) }.getOrNull()
|
||||
}
|
||||
}
|
||||
|
||||
internal fun callBooleanMethodIfExists(target: Any, methodName: String, value: Boolean): Boolean {
|
||||
return findMethodsNamed(target.javaClass, methodName)
|
||||
.firstOrNull { method ->
|
||||
val types = method.parameterTypes
|
||||
types.size == 1 && types[0] == Boolean::class.javaPrimitiveType
|
||||
}
|
||||
?.let { method ->
|
||||
runCatching { method.invoke(target, value) }.isSuccess
|
||||
} ?: false
|
||||
return invokeOptional(target, methodName, value) { types ->
|
||||
types.size == 1 && types[0] == Boolean::class.javaPrimitiveType
|
||||
}
|
||||
}
|
||||
|
||||
private fun optionalMethod(
|
||||
target: Any,
|
||||
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? {
|
||||
|
||||
@@ -3,6 +3,7 @@ package se.ajpanton.navbuttons
|
||||
import android.content.Context
|
||||
import android.os.Handler
|
||||
import android.os.SystemClock
|
||||
import android.view.HapticFeedbackConstants
|
||||
import android.view.KeyEvent
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
@@ -28,6 +29,7 @@ internal class SamsungHoneyspaceHooks(
|
||||
private val log: (String) -> Unit,
|
||||
) {
|
||||
private val longPressTriggered = WeakHashMap<Any, Boolean>()
|
||||
private val customLongPressRunnables = WeakHashMap<Any, Runnable>()
|
||||
private val trackedNavContainers = Collections.newSetFromMap(WeakHashMap<Any, Boolean>())
|
||||
private val nativeSideArrowStates = WeakHashMap<Any, Boolean>()
|
||||
private val homeStockLongPressGuard = HomeStockLongPressGuard(
|
||||
@@ -74,7 +76,7 @@ internal class SamsungHoneyspaceHooks(
|
||||
registerSettingsReceiver()
|
||||
trackNavContainerFrom(navButtonView as? View)
|
||||
|
||||
return@hookMethod handleNativeSideArrowTouch(navButtonView, keyCode, motionEvent) {
|
||||
return@hookMethod sideArrowLongPresses.handleNativeTouch(navButtonView, keyCode, motionEvent) {
|
||||
chain.proceed()
|
||||
}
|
||||
}
|
||||
@@ -86,20 +88,26 @@ internal class SamsungHoneyspaceHooks(
|
||||
when (motionEvent.actionMasked) {
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
longPressTriggered[navButtonView] = false
|
||||
cancelCustomLongPress(navButtonView)
|
||||
homeStockLongPressGuard.cancel(navButtonView)
|
||||
homeStockLongPressGuard.clear(navButtonView)
|
||||
val result = chain.proceed()
|
||||
rescheduleHoneyspaceLongPress(
|
||||
navButtonView,
|
||||
runnableClass,
|
||||
configProvider(buttonId).longPressDurationMs,
|
||||
)
|
||||
val config = configProvider(buttonId)
|
||||
if (config.longPressAction != NavButtonAction.STOCK) {
|
||||
cancelHoneyspaceLongPress(navButtonView, runnableClass)
|
||||
scheduleCustomLongPress(
|
||||
navButtonView,
|
||||
config.longPressAction,
|
||||
config.longPressDurationMs,
|
||||
)
|
||||
}
|
||||
homeStockLongPressGuard.schedule(navButtonView, buttonId)
|
||||
result
|
||||
}
|
||||
|
||||
MotionEvent.ACTION_UP -> {
|
||||
val triggered = longPressTriggered[navButtonView] == true
|
||||
cancelCustomLongPress(navButtonView)
|
||||
if (triggered) {
|
||||
return@hookMethod proceedWithCanceledAction(motionEvent) {
|
||||
chain.proceed()
|
||||
@@ -137,6 +145,7 @@ internal class SamsungHoneyspaceHooks(
|
||||
}
|
||||
|
||||
MotionEvent.ACTION_CANCEL -> {
|
||||
cancelCustomLongPress(navButtonView)
|
||||
homeStockLongPressGuard.cancel(navButtonView)
|
||||
longPressTriggered.remove(navButtonView)
|
||||
homeStockLongPressGuard.clear(navButtonView)
|
||||
@@ -179,6 +188,30 @@ internal class SamsungHoneyspaceHooks(
|
||||
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) {
|
||||
val containers = trackedNavContainers.toList()
|
||||
containers.forEach { container ->
|
||||
@@ -333,28 +366,6 @@ internal class SamsungHoneyspaceHooks(
|
||||
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) {
|
||||
try {
|
||||
callMethod(
|
||||
|
||||
@@ -48,15 +48,10 @@ internal fun isHoneyspaceNavButtonContainer(view: View): Boolean {
|
||||
honeyspaceViewIdName(view) == "navbar_button_container"
|
||||
}
|
||||
|
||||
internal fun rescheduleHoneyspaceLongPress(
|
||||
navButtonView: Any,
|
||||
runnableClass: Class<*>,
|
||||
durationMs: Int,
|
||||
) {
|
||||
internal fun cancelHoneyspaceLongPress(navButtonView: Any, runnableClass: Class<*>) {
|
||||
val view = navButtonView as? View ?: return
|
||||
val longPressRunnable = findHoneyspaceLongPressRunnable(navButtonView, runnableClass) ?: return
|
||||
view.removeCallbacks(longPressRunnable)
|
||||
view.postDelayed(longPressRunnable, durationMs.toLong())
|
||||
}
|
||||
|
||||
internal fun createHoneyspaceExtraButton(
|
||||
|
||||
@@ -19,7 +19,6 @@ import android.widget.EditText
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.Spinner
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import java.text.DecimalFormatSymbols
|
||||
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(
|
||||
sectionView: View,
|
||||
private val buttonId: NavButtonId,
|
||||
@@ -237,8 +307,7 @@ class SettingsActivity : Activity() {
|
||||
private val plusButton: Button = sectionView.findViewById(R.id.plus_button)
|
||||
private val testButton: Button = sectionView.findViewById(R.id.test_button)
|
||||
|
||||
private var lastValidDurationMs = settingsStore.defaultLongPressDurationMs()
|
||||
private var updatingDurationText = false
|
||||
private lateinit var durationField: NumericField
|
||||
|
||||
fun bind() {
|
||||
val config = settingsStore.getConfig(buttonId)
|
||||
@@ -255,10 +324,14 @@ class SettingsActivity : Activity() {
|
||||
settingsStore::setLongPressAction,
|
||||
)
|
||||
|
||||
applyDuration(config.longPressDurationMs, persist = false)
|
||||
bindDurationInput()
|
||||
bindStepButtons()
|
||||
attachTestButton(testButton, ::resolveDurationForUse)
|
||||
durationField = durationField(
|
||||
durationInput,
|
||||
minusButton,
|
||||
plusButton,
|
||||
persist = { settingsStore.setLongPressDurationMs(buttonId, it) },
|
||||
)
|
||||
durationField.bind(config.longPressDurationMs.toFloat())
|
||||
attachTestButton(testButton) { durationField.normalize().toInt() }
|
||||
}
|
||||
|
||||
private fun bindActionSpinner(
|
||||
@@ -276,89 +349,6 @@ class SettingsActivity : Activity() {
|
||||
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) {
|
||||
@@ -384,21 +374,31 @@ class SettingsActivity : Activity() {
|
||||
private val testButton: Button = sectionView.findViewById(R.id.side_arrow_test_button)
|
||||
|
||||
private val decimalSeparator = DecimalFormatSymbols.getInstance().decimalSeparator
|
||||
private var lastValidDurationMs = settingsStore.defaultLongPressDurationMs()
|
||||
private var lastValidRepeatSpeed = NavButtonSettingsStore.DEFAULT_SIDE_ARROW_REPEAT_STEPS_PER_SECOND
|
||||
private var updatingDurationText = false
|
||||
private var updatingRepeatSpeedText = false
|
||||
private lateinit var durationField: NumericField
|
||||
private lateinit var repeatSpeedField: NumericField
|
||||
|
||||
fun bind() {
|
||||
sideArrowsCheckbox.isChecked = settingsStore.isSideArrowsEnabled()
|
||||
bindLongPressActionSpinner()
|
||||
applyRepeatSpeed(settingsStore.getSideArrowRepeatStepsPerSecond(), persist = false)
|
||||
bindRepeatSpeedInput()
|
||||
bindRepeatSpeedButtons()
|
||||
applyDuration(settingsStore.getSideArrowLongPressDurationMs(), persist = false)
|
||||
bindDurationInput()
|
||||
bindDurationButtons()
|
||||
attachTestButton(testButton, ::resolveDurationForUse)
|
||||
repeatSpeedField = NumericField(
|
||||
repeatSpeedInput,
|
||||
speedMinusButton,
|
||||
speedPlusButton,
|
||||
NavButtonSettingsStore.SIDE_ARROW_REPEAT_SPEED_STEP,
|
||||
parse = ::parseRepeatSpeed,
|
||||
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()
|
||||
|
||||
sideArrowsCheckbox.setOnCheckedChangeListener { _, isChecked ->
|
||||
@@ -422,100 +422,6 @@ class SettingsActivity : Activity() {
|
||||
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() {
|
||||
val enabled = sideArrowsCheckbox.isChecked
|
||||
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
|
||||
}
|
||||
|
||||
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? {
|
||||
return text
|
||||
?.trim()
|
||||
@@ -586,42 +460,23 @@ class SettingsActivity : Activity() {
|
||||
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
|
||||
|
||||
import android.os.Handler
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import java.util.WeakHashMap
|
||||
|
||||
@@ -27,6 +28,25 @@ internal class SideArrowLongPressController(
|
||||
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 {
|
||||
val wasTriggered = triggered[target] == true
|
||||
cancel(target)
|
||||
|
||||
@@ -86,7 +86,7 @@ internal class SystemUiNavHooks(
|
||||
registerSettingsReceiver()
|
||||
trackSamsungNavigationBarView(keyButtonView)
|
||||
configureAospSideArrowButton(keyButtonView)
|
||||
return@hookMethod handleNativeSideArrowTouch(keyButtonView, keyCode, motionEvent) {
|
||||
return@hookMethod sideArrowLongPresses.handleNativeTouch(keyButtonView, keyCode, motionEvent) {
|
||||
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) {
|
||||
try {
|
||||
findField(keyButtonView.javaClass, "mLongClicked").setBoolean(keyButtonView, true)
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
version=2.2
|
||||
version=2.4
|
||||
|
||||
Reference in New Issue
Block a user