Add local release workflow and foldable home guard

This commit is contained in:
ajp_anton
2026-06-16 21:28:34 +00:00
parent 27a1367fc1
commit 7239c28284
4 changed files with 134 additions and 129 deletions
+1
View File
@@ -2,4 +2,5 @@
.kotlin/ .kotlin/
build/ build/
app/build/ app/build/
local/
local.properties local.properties
+2 -2
View File
@@ -56,8 +56,8 @@ android {
applicationId = "se.ajpanton.navbuttons" applicationId = "se.ajpanton.navbuttons"
minSdk = 35 minSdk = 35
targetSdk = 35 targetSdk = 35
versionCode = 1777070659 versionCode = project.findProperty("nbVersionCode")?.toString()?.toIntOrNull() ?: 1777070659
versionName = "0.3" versionName = project.findProperty("nbVersionName")?.toString() ?: "0.3"
} }
buildTypes { buildTypes {
@@ -10,7 +10,6 @@ import android.content.pm.PackageManager
import android.hardware.camera2.CameraAccessException import android.hardware.camera2.CameraAccessException
import android.hardware.camera2.CameraCharacteristics import android.hardware.camera2.CameraCharacteristics
import android.hardware.camera2.CameraManager import android.hardware.camera2.CameraManager
import android.os.Bundle
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.os.Process import android.os.Process
@@ -52,6 +51,8 @@ class NavButtons : XposedModule() {
private val systemUiHomeStockLongPressGuards = WeakHashMap<Any, Runnable>() private val systemUiHomeStockLongPressGuards = WeakHashMap<Any, Runnable>()
private val systemUiHomeStockPressCanceled = WeakHashMap<Any, Boolean>() private val systemUiHomeStockPressCanceled = WeakHashMap<Any, Boolean>()
private val honeyspaceLongPressTriggered = WeakHashMap<Any, Boolean>() private val honeyspaceLongPressTriggered = WeakHashMap<Any, Boolean>()
private val honeyspaceHomeStockLongPressGuards = WeakHashMap<Any, Runnable>()
private val honeyspaceHomeStockPressCanceled = WeakHashMap<Any, Boolean>()
override fun onModuleLoaded(param: XposedModuleInterface.ModuleLoadedParam) { override fun onModuleLoaded(param: XposedModuleInterface.ModuleLoadedParam) {
if (!HiddenApiBypass.addHiddenApiExemptions("")) { if (!HiddenApiBypass.addHiddenApiExemptions("")) {
@@ -374,111 +375,6 @@ class NavButtons : XposedModule() {
return installed 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 { private fun hookHoneyspaceTaskbar(classLoader: ClassLoader?): Boolean {
val navButtonClass = findClassIfExists(HONEYSPACE_NAV_BUTTON_VIEW, classLoader) ?: run { val navButtonClass = findClassIfExists(HONEYSPACE_NAV_BUTTON_VIEW, classLoader) ?: run {
log("Launcher class not found: $HONEYSPACE_NAV_BUTTON_VIEW") log("Launcher class not found: $HONEYSPACE_NAV_BUTTON_VIEW")
@@ -516,6 +412,8 @@ class NavButtons : XposedModule() {
when (motionEvent.actionMasked) { when (motionEvent.actionMasked) {
MotionEvent.ACTION_DOWN -> { MotionEvent.ACTION_DOWN -> {
honeyspaceLongPressTriggered[navButtonView] = false honeyspaceLongPressTriggered[navButtonView] = false
cancelHoneyspaceHomeStockLongPressGuard(navButtonView)
honeyspaceHomeStockPressCanceled.remove(navButtonView)
if (buttonId == NavButtonId.HOME) { if (buttonId == NavButtonId.HOME) {
val config = getButtonConfig(buttonId) val config = getButtonConfig(buttonId)
traceHome( traceHome(
@@ -525,6 +423,7 @@ class NavButtons : XposedModule() {
} }
val result = chain.proceed() val result = chain.proceed()
rescheduleHoneyspaceLongPress(navButtonView, runnableClass, buttonId) rescheduleHoneyspaceLongPress(navButtonView, runnableClass, buttonId)
scheduleHoneyspaceHomeStockLongPressGuard(navButtonView, buttonId)
result result
} }
@@ -537,14 +436,18 @@ class NavButtons : XposedModule() {
return@intercept proceedWithCanceledAction(motionEvent) { return@intercept proceedWithCanceledAction(motionEvent) {
chain.proceed() chain.proceed()
}.also { }.also {
cancelHoneyspaceHomeStockLongPressGuard(navButtonView)
honeyspaceLongPressTriggered.remove(navButtonView) honeyspaceLongPressTriggered.remove(navButtonView)
honeyspaceHomeStockPressCanceled.remove(navButtonView)
} }
} }
val pressAction = getButtonConfig(buttonId).pressAction val pressAction = getButtonConfig(buttonId).pressAction
val stockHomePressCanceled =
buttonId == NavButtonId.HOME && honeyspaceHomeStockPressCanceled[navButtonView] == true
val shouldOverridePress = val shouldOverridePress =
pressAction != NavButtonAction.STOCK && pressAction != NavButtonAction.STOCK &&
(navButtonView as? View)?.isPressed == true && ((navButtonView as? View)?.isPressed == true || stockHomePressCanceled) &&
!longPressTriggered !longPressTriggered
if (shouldOverridePress) { if (shouldOverridePress) {
@@ -552,13 +455,21 @@ class NavButtons : XposedModule() {
} }
val result = chain.proceed() val result = chain.proceed()
if (stockHomePressCanceled && pressAction == NavButtonAction.STOCK) {
dispatchHoneyspaceHomeTap(navButtonView)
}
if (shouldOverridePress) { if (shouldOverridePress) {
updateContextFromView(navButtonView) updateContextFromView(navButtonView)
performConfiguredAction(pressAction) performConfiguredAction(pressAction)
} }
cancelHoneyspaceHomeStockLongPressGuard(navButtonView)
honeyspaceLongPressTriggered.remove(navButtonView) honeyspaceLongPressTriggered.remove(navButtonView)
honeyspaceHomeStockPressCanceled.remove(navButtonView)
if (buttonId == NavButtonId.HOME) { if (buttonId == NavButtonId.HOME) {
traceHome("Honeyspace.onTouchEvent UP", "override=$shouldOverridePress") traceHome(
"Honeyspace.onTouchEvent UP",
"override=$shouldOverridePress ${honeyspaceHomeState(navButtonView)}",
)
} }
result result
} }
@@ -567,7 +478,9 @@ class NavButtons : XposedModule() {
if (buttonId == NavButtonId.HOME) { if (buttonId == NavButtonId.HOME) {
traceHome("Honeyspace.onTouchEvent CANCEL", "clearing long-press state") traceHome("Honeyspace.onTouchEvent CANCEL", "clearing long-press state")
} }
cancelHoneyspaceHomeStockLongPressGuard(navButtonView)
honeyspaceLongPressTriggered.remove(navButtonView) honeyspaceLongPressTriggered.remove(navButtonView)
honeyspaceHomeStockPressCanceled.remove(navButtonView)
chain.proceed() chain.proceed()
} }
@@ -601,6 +514,7 @@ class NavButtons : XposedModule() {
} }
updateContextFromView(navButtonView) updateContextFromView(navButtonView)
markHoneyspaceLongClicked(navButtonView) markHoneyspaceLongClicked(navButtonView)
cancelHoneyspaceHomeStockLongPressGuard(navButtonView)
performConfiguredAction(longPressAction) performConfiguredAction(longPressAction)
null null
} }
@@ -1037,6 +951,35 @@ class NavButtons : XposedModule() {
(keyButtonView as? View)?.removeCallbacks(guard) (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) { private fun abortSystemUiKeyGesture(keyButtonView: Any) {
try { try {
callMethod(keyButtonView, "abortCurrentGesture") 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) { private fun dispatchSystemUiHomeTap(keyButtonView: Any) {
traceHome("dispatchSystemUiHomeTap", "sending stock tap after guard cancel") traceHome("dispatchSystemUiHomeTap", "sending stock tap after guard cancel")
try { try {
@@ -1081,19 +1063,14 @@ class NavButtons : XposedModule() {
} }
} }
private fun abortSystemUiNavigationGesture(navigationBar: Any?, homeView: View) { private fun dispatchHoneyspaceHomeTap(navButtonView: Any) {
try { traceHome("dispatchHoneyspaceHomeTap", "sending stock tap after guard cancel")
callMethod(homeView, "abortCurrentGesture") if (callIntIntMethodIfExists(navButtonView, listOf("sendEvent", "sendKeyEvent"), KeyEvent.ACTION_DOWN, 0) &&
} catch (_: Throwable) { callIntIntMethodIfExists(navButtonView, listOf("sendEvent", "sendKeyEvent"), KeyEvent.ACTION_UP, 0)
// Ignore variants without this helper on the home button view. ) {
} return
val navigationBarView = getFieldValue(navigationBar, "mView")
try {
callMethod(navigationBarView ?: return, "abortCurrentGesture")
} catch (_: Throwable) {
// Ignore variants without this helper on the navigation bar view.
} }
sendKeyPress(KeyEvent.KEYCODE_HOME)
} }
private fun rescheduleSystemUiLongPress(keyButtonView: Any, buttonId: NavButtonId) { private fun rescheduleSystemUiLongPress(keyButtonView: Any, buttonId: NavButtonId) {
@@ -1199,6 +1176,12 @@ class NavButtons : XposedModule() {
"stockCanceled=${systemUiHomeStockPressCanceled[keyButtonView] == true}" "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 { private fun sendEventArgsToTraceString(args: Any?): String {
val values = argsToList(args) val values = argsToList(args)
return values.mapIndexed { index, arg -> return values.mapIndexed { index, arg ->
@@ -1306,10 +1289,35 @@ class NavButtons : XposedModule() {
return findMethod(target.javaClass, methodName, *parameterTypes).invoke(target, *args) return findMethod(target.javaClass, methodName, *parameterTypes).invoke(target, *args)
} }
private fun callIntIntMethodIfExists(
target: Any,
methodNames: List<String>,
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 { private companion object {
const val DEBUG_TRACE = true const val DEBUG_TRACE = false
const val TAG = "NavButtons" const val TAG = "NavButtons"
const val ASSIST_INVOCATION_TYPE_KEY = "invocation_type"
const val ASSIST_INVOCATION_TYPE_UNKNOWN = 0 const val ASSIST_INVOCATION_TYPE_UNKNOWN = 0
const val ASSIST_INVOCATION_TYPE_HOME_BUTTON_LONG_PRESS = 5 const val ASSIST_INVOCATION_TYPE_HOME_BUTTON_LONG_PRESS = 5
const val INPUT_MANAGER_CLASS = "android.hardware.input.InputManager" const val INPUT_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 STOCK_LONG_PRESS_GUARD_MIN_DELAY_MS = 100
const val SYSTEMUI_PACKAGE = "com.android.systemui" const val SYSTEMUI_PACKAGE = "com.android.systemui"
const val SYSTEM_SERVER_PHONE_WINDOW_MANAGER = "com.android.server.policy.PhoneWindowManager" 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 = const val SYSTEMUI_KEY_BUTTON_VIEW =
"com.android.systemui.navigationbar.views.buttons.KeyButtonView" "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 SAMSUNG_LAUNCHER_PACKAGE = "com.sec.android.app.launcher"
const val HONEYSPACE_NAV_BUTTON_VIEW = const val HONEYSPACE_NAV_BUTTON_VIEW =
"com.honeyspace.ui.honeypots.taskbar.presentation.NavigationBarKeyButtonView" "com.honeyspace.ui.honeypots.taskbar.presentation.NavigationBarKeyButtonView"
+1
View File
@@ -0,0 +1 @@
version=0.3