From 7239c2828443ac83997510bdb96d95009cbea5d0 Mon Sep 17 00:00:00 2001 From: ajp_anton Date: Tue, 16 Jun 2026 21:28:34 +0000 Subject: [PATCH] Add local release workflow and foldable home guard --- .gitignore | 1 + app/build.gradle.kts | 4 +- .../java/se/ajpanton/navbuttons/NavButtons.kt | 257 +++++++++--------- version.properties | 1 + 4 files changed, 134 insertions(+), 129 deletions(-) create mode 100644 version.properties diff --git a/.gitignore b/.gitignore index ce988d8..0598101 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ .kotlin/ build/ app/build/ +local/ local.properties diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 2bda488..b346a36 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 = 1777070659 - versionName = "0.3" + versionCode = project.findProperty("nbVersionCode")?.toString()?.toIntOrNull() ?: 1777070659 + versionName = project.findProperty("nbVersionName")?.toString() ?: "0.3" } buildTypes { diff --git a/app/src/main/java/se/ajpanton/navbuttons/NavButtons.kt b/app/src/main/java/se/ajpanton/navbuttons/NavButtons.kt index 6f7dfc6..3ab64c1 100644 --- a/app/src/main/java/se/ajpanton/navbuttons/NavButtons.kt +++ b/app/src/main/java/se/ajpanton/navbuttons/NavButtons.kt @@ -10,7 +10,6 @@ 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 @@ -52,6 +51,8 @@ class NavButtons : XposedModule() { private val systemUiHomeStockLongPressGuards = WeakHashMap() private val systemUiHomeStockPressCanceled = WeakHashMap() private val honeyspaceLongPressTriggered = WeakHashMap() + private val honeyspaceHomeStockLongPressGuards = WeakHashMap() + private val honeyspaceHomeStockPressCanceled = WeakHashMap() override fun onModuleLoaded(param: XposedModuleInterface.ModuleLoadedParam) { if (!HiddenApiBypass.addHiddenApiExemptions("")) { @@ -374,111 +375,6 @@ class NavButtons : XposedModule() { return installed } - private fun hookSystemUiHomeLongClickIfPresent(navigationBarClass: Class<*>?) { - if (navigationBarClass == null) { - log("SystemUI navigation bar class not found: $SYSTEMUI_NAVIGATION_BAR") - return - } - - val onHomeLongClickMethod = findMethodIfExists( - navigationBarClass, - "onHomeLongClick", - View::class.java, - ) ?: run { - log("SystemUI onHomeLongClick() method not found.") - return - } - - deoptimizeMethodIfPresent(onHomeLongClickMethod) - hook(onHomeLongClickMethod).intercept { chain -> - val longPressAction = getButtonConfig(NavButtonId.HOME).longPressAction - if (longPressAction == NavButtonAction.STOCK) { - return@intercept chain.proceed() - } - - traceHome("NavigationBar.onHomeLongClick", "long=${longPressAction.storageValue}") - val homeView = chain.args[0] as? View - if (homeView != null) { - updateContextFromView(homeView) - systemUiLongPressTriggered[homeView] = true - markSystemUiLongClicked(homeView) - abortSystemUiNavigationGesture(chain.thisObject, homeView) - } - performHomeLongPressAction(longPressAction) - true - } - } - - private fun hookSystemUiAssistStartIfPresent(assistManagerClass: Class<*>?) { - if (assistManagerClass == null) { - log("SystemUI assist manager class not found: $SYSTEMUI_ASSIST_MANAGER") - return - } - - val startAssistMethod = findMethodIfExists( - assistManagerClass, - "startAssist", - Bundle::class.java, - ) ?: run { - log("SystemUI startAssist(Bundle) method not found.") - return - } - - deoptimizeMethodIfPresent(startAssistMethod) - hook(startAssistMethod).intercept { chain -> - val args = chain.args[0] as? Bundle ?: return@intercept chain.proceed() - val invocationType = args.getInt(ASSIST_INVOCATION_TYPE_KEY, ASSIST_INVOCATION_TYPE_UNKNOWN) - traceHome("AssistManager.startAssist", "invocationType=$invocationType") - if (invocationType != ASSIST_INVOCATION_TYPE_HOME_BUTTON_LONG_PRESS) { - return@intercept chain.proceed() - } - - val longPressAction = getButtonConfig(NavButtonId.HOME).longPressAction - if (longPressAction == NavButtonAction.STOCK) { - return@intercept chain.proceed() - } - - traceHome("AssistManager.startAssist", "blocked long=${longPressAction.storageValue}") - performHomeLongPressAction(longPressAction) - null - } - } - - private fun hookSystemUiOverviewProxyStartAssistantIfPresent(overviewProxyServiceClass: Class<*>?) { - if (overviewProxyServiceClass == null) { - log("SystemUI overview proxy service class not found: $SYSTEMUI_OVERVIEW_PROXY_SERVICE") - return - } - - val startAssistantMethod = findMethodIfExists( - overviewProxyServiceClass, - "startAssistant", - Bundle::class.java, - ) ?: run { - log("SystemUI OverviewProxyService.startAssistant(Bundle) method not found.") - return - } - - deoptimizeMethodIfPresent(startAssistantMethod) - hook(startAssistantMethod).intercept { chain -> - val args = chain.args[0] as? Bundle ?: return@intercept chain.proceed() - val invocationType = args.getInt(ASSIST_INVOCATION_TYPE_KEY, ASSIST_INVOCATION_TYPE_UNKNOWN) - traceHome("OverviewProxyService.startAssistant", "invocationType=$invocationType") - if (invocationType != ASSIST_INVOCATION_TYPE_HOME_BUTTON_LONG_PRESS) { - return@intercept chain.proceed() - } - - val longPressAction = getButtonConfig(NavButtonId.HOME).longPressAction - if (longPressAction == NavButtonAction.STOCK) { - return@intercept chain.proceed() - } - - traceHome("OverviewProxyService.startAssistant", "blocked long=${longPressAction.storageValue}") - performHomeLongPressAction(longPressAction) - null - } - } - private fun hookHoneyspaceTaskbar(classLoader: ClassLoader?): Boolean { val navButtonClass = findClassIfExists(HONEYSPACE_NAV_BUTTON_VIEW, classLoader) ?: run { log("Launcher class not found: $HONEYSPACE_NAV_BUTTON_VIEW") @@ -516,6 +412,8 @@ class NavButtons : XposedModule() { when (motionEvent.actionMasked) { MotionEvent.ACTION_DOWN -> { honeyspaceLongPressTriggered[navButtonView] = false + cancelHoneyspaceHomeStockLongPressGuard(navButtonView) + honeyspaceHomeStockPressCanceled.remove(navButtonView) if (buttonId == NavButtonId.HOME) { val config = getButtonConfig(buttonId) traceHome( @@ -525,6 +423,7 @@ class NavButtons : XposedModule() { } val result = chain.proceed() rescheduleHoneyspaceLongPress(navButtonView, runnableClass, buttonId) + scheduleHoneyspaceHomeStockLongPressGuard(navButtonView, buttonId) result } @@ -537,14 +436,18 @@ class NavButtons : XposedModule() { return@intercept proceedWithCanceledAction(motionEvent) { chain.proceed() }.also { + cancelHoneyspaceHomeStockLongPressGuard(navButtonView) honeyspaceLongPressTriggered.remove(navButtonView) + honeyspaceHomeStockPressCanceled.remove(navButtonView) } } val pressAction = getButtonConfig(buttonId).pressAction + val stockHomePressCanceled = + buttonId == NavButtonId.HOME && honeyspaceHomeStockPressCanceled[navButtonView] == true val shouldOverridePress = pressAction != NavButtonAction.STOCK && - (navButtonView as? View)?.isPressed == true && + ((navButtonView as? View)?.isPressed == true || stockHomePressCanceled) && !longPressTriggered if (shouldOverridePress) { @@ -552,13 +455,21 @@ class NavButtons : XposedModule() { } val result = chain.proceed() + if (stockHomePressCanceled && pressAction == NavButtonAction.STOCK) { + dispatchHoneyspaceHomeTap(navButtonView) + } if (shouldOverridePress) { updateContextFromView(navButtonView) performConfiguredAction(pressAction) } + cancelHoneyspaceHomeStockLongPressGuard(navButtonView) honeyspaceLongPressTriggered.remove(navButtonView) + honeyspaceHomeStockPressCanceled.remove(navButtonView) if (buttonId == NavButtonId.HOME) { - traceHome("Honeyspace.onTouchEvent UP", "override=$shouldOverridePress") + traceHome( + "Honeyspace.onTouchEvent UP", + "override=$shouldOverridePress ${honeyspaceHomeState(navButtonView)}", + ) } result } @@ -567,7 +478,9 @@ class NavButtons : XposedModule() { if (buttonId == NavButtonId.HOME) { traceHome("Honeyspace.onTouchEvent CANCEL", "clearing long-press state") } + cancelHoneyspaceHomeStockLongPressGuard(navButtonView) honeyspaceLongPressTriggered.remove(navButtonView) + honeyspaceHomeStockPressCanceled.remove(navButtonView) chain.proceed() } @@ -601,6 +514,7 @@ class NavButtons : XposedModule() { } updateContextFromView(navButtonView) markHoneyspaceLongClicked(navButtonView) + cancelHoneyspaceHomeStockLongPressGuard(navButtonView) performConfiguredAction(longPressAction) null } @@ -1037,6 +951,35 @@ class NavButtons : XposedModule() { (keyButtonView as? View)?.removeCallbacks(guard) } + private fun scheduleHoneyspaceHomeStockLongPressGuard(navButtonView: Any, buttonId: NavButtonId) { + if (buttonId != NavButtonId.HOME) { + return + } + if (getButtonConfig(buttonId).longPressAction == NavButtonAction.STOCK) { + return + } + + val view = navButtonView as? View ?: return + val delayMs = (ViewConfiguration.getLongPressTimeout() - STOCK_LONG_PRESS_GUARD_EARLY_MS) + .coerceAtLeast(STOCK_LONG_PRESS_GUARD_MIN_DELAY_MS) + val guard = Runnable { + if (!view.isPressed || honeyspaceLongPressTriggered[navButtonView] == true) { + return@Runnable + } + traceHome("Honeyspace stock long-press guard", "canceling original Home key after ${delayMs}ms") + honeyspaceHomeStockPressCanceled[navButtonView] = true + sendHoneyspaceCanceledKeyUp(navButtonView) + } + + honeyspaceHomeStockLongPressGuards[navButtonView] = guard + view.postDelayed(guard, delayMs.toLong()) + } + + private fun cancelHoneyspaceHomeStockLongPressGuard(navButtonView: Any) { + val guard = honeyspaceHomeStockLongPressGuards.remove(navButtonView) ?: return + (navButtonView as? View)?.removeCallbacks(guard) + } + private fun abortSystemUiKeyGesture(keyButtonView: Any) { try { callMethod(keyButtonView, "abortCurrentGesture") @@ -1059,6 +1002,45 @@ class NavButtons : XposedModule() { } } + private fun sendHoneyspaceCanceledKeyUp(navButtonView: Any) { + if (callIntIntMethodIfExists( + navButtonView, + listOf("sendEvent", "sendKeyEvent"), + KeyEvent.ACTION_UP, + KeyEvent.FLAG_CANCELED or KeyEvent.FLAG_CANCELED_LONG_PRESS, + ) + ) { + return + } + + try { + val inputManager = callStaticMethod( + findClass(INPUT_MANAGER_CLASS, null), + "getInstance", + ) ?: run { + log("InputManager unavailable for Honeyspace Home cancel.") + return + } + val now = SystemClock.uptimeMillis() + injectKeyEvent( + inputManager, + KeyEvent( + now, + now, + KeyEvent.ACTION_UP, + KeyEvent.KEYCODE_HOME, + 0, + 0, + KeyEvent.KEYCODE_UNKNOWN, + 0, + KeyEvent.FLAG_CANCELED or KeyEvent.FLAG_CANCELED_LONG_PRESS, + ), + ) + } catch (error: Throwable) { + log("Honeyspace Home cancel failed: ${error.javaClass.simpleName}") + } + } + private fun dispatchSystemUiHomeTap(keyButtonView: Any) { traceHome("dispatchSystemUiHomeTap", "sending stock tap after guard cancel") try { @@ -1081,19 +1063,14 @@ class NavButtons : XposedModule() { } } - private fun abortSystemUiNavigationGesture(navigationBar: Any?, homeView: View) { - try { - callMethod(homeView, "abortCurrentGesture") - } catch (_: Throwable) { - // Ignore variants without this helper on the home button view. - } - - val navigationBarView = getFieldValue(navigationBar, "mView") - try { - callMethod(navigationBarView ?: return, "abortCurrentGesture") - } catch (_: Throwable) { - // Ignore variants without this helper on the navigation bar view. + private fun dispatchHoneyspaceHomeTap(navButtonView: Any) { + traceHome("dispatchHoneyspaceHomeTap", "sending stock tap after guard cancel") + if (callIntIntMethodIfExists(navButtonView, listOf("sendEvent", "sendKeyEvent"), KeyEvent.ACTION_DOWN, 0) && + callIntIntMethodIfExists(navButtonView, listOf("sendEvent", "sendKeyEvent"), KeyEvent.ACTION_UP, 0) + ) { + return } + sendKeyPress(KeyEvent.KEYCODE_HOME) } private fun rescheduleSystemUiLongPress(keyButtonView: Any, buttonId: NavButtonId) { @@ -1199,6 +1176,12 @@ class NavButtons : XposedModule() { "stockCanceled=${systemUiHomeStockPressCanceled[keyButtonView] == true}" } + private fun honeyspaceHomeState(navButtonView: Any): String { + return "pressed=${(navButtonView as? View)?.isPressed == true} " + + "triggered=${honeyspaceLongPressTriggered[navButtonView] == true} " + + "stockCanceled=${honeyspaceHomeStockPressCanceled[navButtonView] == true}" + } + private fun sendEventArgsToTraceString(args: Any?): String { val values = argsToList(args) return values.mapIndexed { index, arg -> @@ -1306,10 +1289,35 @@ class NavButtons : XposedModule() { return findMethod(target.javaClass, methodName, *parameterTypes).invoke(target, *args) } + private fun callIntIntMethodIfExists( + target: Any, + methodNames: List, + first: Int, + 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 + } + } + } + return false + } + private companion object { - const val DEBUG_TRACE = true + const val DEBUG_TRACE = false const val TAG = "NavButtons" - 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" @@ -1319,13 +1327,8 @@ class NavButtons : XposedModule() { const val STOCK_LONG_PRESS_GUARD_MIN_DELAY_MS = 100 const val SYSTEMUI_PACKAGE = "com.android.systemui" const val SYSTEM_SERVER_PHONE_WINDOW_MANAGER = "com.android.server.policy.PhoneWindowManager" - const val SYSTEMUI_ASSIST_MANAGER = "com.android.systemui.assist.AssistManager" const val SYSTEMUI_KEY_BUTTON_VIEW = "com.android.systemui.navigationbar.views.buttons.KeyButtonView" - const val SYSTEMUI_NAVIGATION_BAR = - "com.android.systemui.navigationbar.NavigationBar" - const val SYSTEMUI_OVERVIEW_PROXY_SERVICE = - "com.android.systemui.recents.OverviewProxyService" const val SAMSUNG_LAUNCHER_PACKAGE = "com.sec.android.app.launcher" const val HONEYSPACE_NAV_BUTTON_VIEW = "com.honeyspace.ui.honeypots.taskbar.presentation.NavigationBarKeyButtonView" diff --git a/version.properties b/version.properties new file mode 100644 index 0000000..5971fc4 --- /dev/null +++ b/version.properties @@ -0,0 +1 @@ +version=0.3