From c2ee5a55811b1bf49b3b0933ee0db35aa9edd75f Mon Sep 17 00:00:00 2001 From: ajp_anton Date: Mon, 22 Jun 2026 22:01:11 +0000 Subject: [PATCH] Support AOSP API 34 and Google APIs --- app/build.gradle.kts | 2 +- .../java/se/ajpanton/navbuttons/NavButtons.kt | 272 +++++++++++++----- .../main/resources/META-INF/xposed/scope.list | 1 + 3 files changed, 206 insertions(+), 69 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index b346a36..8e8cfaa 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -54,7 +54,7 @@ android { defaultConfig { applicationId = "se.ajpanton.navbuttons" - minSdk = 35 + minSdk = 34 targetSdk = 35 versionCode = project.findProperty("nbVersionCode")?.toString()?.toIntOrNull() ?: 1777070659 versionName = project.findProperty("nbVersionName")?.toString() ?: "0.3" diff --git a/app/src/main/java/se/ajpanton/navbuttons/NavButtons.kt b/app/src/main/java/se/ajpanton/navbuttons/NavButtons.kt index 534efb0..b4e7c77 100644 --- a/app/src/main/java/se/ajpanton/navbuttons/NavButtons.kt +++ b/app/src/main/java/se/ajpanton/navbuttons/NavButtons.kt @@ -48,14 +48,14 @@ import java.util.Collections import java.util.WeakHashMap class NavButtons : XposedModule() { - private val remotePreferences: SharedPreferences? by lazy(LazyThreadSafetyMode.NONE) { - runCatching { - getRemotePreferences(NavButtonSettingsStore.PREFERENCES_NAME) - }.getOrElse { error -> - log("Remote preferences unavailable: ${error.javaClass.simpleName}") - null - } - } + @Volatile + private var remotePreferences: SharedPreferences? = null + + @Volatile + private var remotePreferencesRetryAfterMs = 0L + + @Volatile + private var remotePreferencesLoading = false private var context: Context? = null private var mainHandler: Handler? = null @@ -140,7 +140,7 @@ class NavButtons : XposedModule() { return } - if (AOSP_LAUNCHER_PACKAGE == packageName) { + if (packageName in AOSP_LAUNCHER_PACKAGES) { if (aospTaskbarHookInstalled) { return } @@ -157,6 +157,56 @@ class NavButtons : XposedModule() { if (mainHandler == null) { mainHandler = Handler(Looper.getMainLooper()) } + startSettingsPreferencesLoad() + } + + private fun getSettingsPreferences(): SharedPreferences? { + remotePreferences?.let { return it } + if (Looper.myLooper() == Looper.getMainLooper()) { + startSettingsPreferencesLoad() + return null + } + return loadSettingsPreferences() + } + + private fun startSettingsPreferencesLoad() { + if (remotePreferences != null || remotePreferencesLoading) { + return + } + val now = SystemClock.uptimeMillis() + if (now < remotePreferencesRetryAfterMs) { + return + } + remotePreferencesLoading = true + Thread { + try { + repeat(REMOTE_PREFERENCES_LOAD_ATTEMPTS) { + if (loadSettingsPreferences() != null) { + return@Thread + } + Thread.sleep(REMOTE_PREFERENCES_RETRY_DELAY_MS) + } + } finally { + remotePreferencesLoading = false + } + }.start() + } + + private fun loadSettingsPreferences(): SharedPreferences? { + remotePreferences?.let { return it } + val now = SystemClock.uptimeMillis() + if (now < remotePreferencesRetryAfterMs) { + return null + } + return runCatching { + getRemotePreferences(NavButtonSettingsStore.PREFERENCES_NAME) + }.getOrElse { error -> + remotePreferencesRetryAfterMs = now + REMOTE_PREFERENCES_RETRY_DELAY_MS + log("Remote preferences unavailable: ${error.javaClass.simpleName}") + null + }?.also { + remotePreferences = it + } } private fun getCurrentApplication(): Context? { @@ -170,8 +220,8 @@ class NavButtons : XposedModule() { } private fun hookSystemUiNavigationButtons(classLoader: ClassLoader?): Boolean { - val keyButtonClass = findClassIfExists(SYSTEMUI_KEY_BUTTON_VIEW, classLoader) ?: run { - log("SystemUI key button class not found: $SYSTEMUI_KEY_BUTTON_VIEW") + val keyButtonClass = findFirstClassIfExists(SYSTEMUI_KEY_BUTTON_VIEW_CLASSES, classLoader) ?: run { + log("SystemUI key button class not found: ${SYSTEMUI_KEY_BUTTON_VIEW_CLASSES.joinToString()}") return false } val onTouchEventMethod = findMethodIfExists( @@ -184,7 +234,7 @@ class NavButtons : XposedModule() { } val runnableClass = findClassIfExists( - "$SYSTEMUI_KEY_BUTTON_VIEW\$1", + "${keyButtonClass.name}\$1", classLoader, ) ?: run { log("SystemUI long-press runnable not found.") @@ -378,6 +428,16 @@ class NavButtons : XposedModule() { Int::class.javaPrimitiveType!!, ) val systemReadyMethod = findMethodIfExists(phoneWindowManagerClass, "systemReady") + val keyDispatchMethods = findMethodsNamed(phoneWindowManagerClass, "interceptKeyBeforeDispatching") + .filter { method -> + method.returnType == Long::class.javaPrimitiveType && + method.parameterTypes.any { it == KeyEvent::class.java } + } + val keyQueueingMethods = findMethodsNamed(phoneWindowManagerClass, "interceptKeyBeforeQueueing") + .filter { method -> + method.returnType == Int::class.javaPrimitiveType && + method.parameterTypes.any { it == KeyEvent::class.java } + } if (systemReadyMethod != null) { deoptimizeMethodIfPresent(systemReadyMethod) @@ -390,6 +450,47 @@ class NavButtons : XposedModule() { installed = true } + keyDispatchMethods.forEach { method -> + deoptimizeMethodIfPresent(method) + hook(method).intercept { chain -> + val keyEvent = chain.args.firstOrNull { it is KeyEvent } as? KeyEvent + ?: return@intercept chain.proceed() + val buttonId = buttonIdForKeyCode(keyEvent.keyCode) ?: return@intercept chain.proceed() + val pressAction = getButtonConfig(buttonId).pressAction + if (pressAction == NavButtonAction.STOCK) { + return@intercept chain.proceed() + } + + updateContextFromField(chain.thisObject, "mContext") + if (keyEvent.action == KeyEvent.ACTION_UP && !keyEvent.isCanceled) { + performConfiguredAction(pressAction) + } + -1L + } + installed = true + } + + keyQueueingMethods.forEach { method -> + deoptimizeMethodIfPresent(method) + hook(method).intercept { chain -> + val keyEvent = chain.args.firstOrNull { it is KeyEvent } as? KeyEvent + ?: return@intercept chain.proceed() + val buttonId = buttonIdForKeyCode(keyEvent.keyCode) ?: return@intercept chain.proceed() + val pressAction = getButtonConfig(buttonId).pressAction + if (pressAction == NavButtonAction.STOCK) { + return@intercept chain.proceed() + } + + updateContextFromField(chain.thisObject, "mContext") + if (keyEvent.action == KeyEvent.ACTION_UP && !keyEvent.isCanceled) { + performConfiguredAction(pressAction) + } + val result = chain.proceed() as? Int ?: 0 + result and ACTION_PASS_TO_USER.inv() + } + installed = true + } + if (longPressOnHomeWithEvent != null) { deoptimizeMethodIfPresent(longPressOnHomeWithEvent) hook(longPressOnHomeWithEvent).intercept { chain -> @@ -441,10 +542,13 @@ class NavButtons : XposedModule() { } private fun hookAospSystemUiSideArrows(classLoader: ClassLoader?, keyButtonClass: Class<*>) { - if (aospSystemUiSideArrowHookInstalled || Build.MANUFACTURER.equals("samsung", ignoreCase = true)) { + if (aospSystemUiSideArrowHookInstalled || + Build.MANUFACTURER.equals("samsung", ignoreCase = true) || + Build.VERSION.SDK_INT < 35 + ) { return } - val inflaterClass = findClassIfExists(AOSP_NAVIGATION_BAR_INFLATER_VIEW, classLoader) ?: return + val inflaterClass = findFirstClassIfExists(AOSP_NAVIGATION_BAR_INFLATER_VIEW_CLASSES, classLoader) ?: return aospSystemUiSideArrowHookInstalled = true registerSettingsChangedReceiver() @@ -685,8 +789,8 @@ class NavButtons : XposedModule() { val view = value as? View ?: return null val classLoader = view.javaClass.classLoader ?: return null return try { - val keyButtonDrawableClass = findClass(AOSP_KEY_BUTTON_DRAWABLE, classLoader) - val stateClass = findClass(AOSP_KEY_BUTTON_DRAWABLE_STATE, classLoader) + val keyButtonDrawableClass = findFirstClass(AOSP_KEY_BUTTON_DRAWABLE_CLASSES, classLoader) + val stateClass = findFirstClass(AOSP_KEY_BUTTON_DRAWABLE_STATE_CLASSES, classLoader) val state = stateClass .getDeclaredConstructor( Int::class.javaPrimitiveType, @@ -743,10 +847,7 @@ class NavButtons : XposedModule() { "sendBackKeyEvent", Int::class.javaPrimitiveType!!, Boolean::class.javaPrimitiveType!!, - ) ?: run { - log("AOSP taskbar sendBackKeyEvent() method not found.") - return false - } + ) deoptimizeMethodIfPresent(clickMethod) deoptimizeMethodIfPresent(longClickMethod) @@ -791,35 +892,37 @@ class NavButtons : XposedModule() { true } - hook(sendBackKeyEventMethod).intercept { chain -> - val pressAction = getButtonConfig(NavButtonId.BACK).pressAction - val longPressAction = getButtonConfig(NavButtonId.BACK).longPressAction - val action = chain.args[0] as? Int ?: return@intercept chain.proceed() - val canceled = chain.args.getOrNull(1) as? Boolean ?: false - val customLongPress = longPressAction != NavButtonAction.STOCK - val longPressTriggered = aospTaskbarBackLongPressTriggered[chain.thisObject] == true + if (sendBackKeyEventMethod != null) { + hook(sendBackKeyEventMethod).intercept { chain -> + val pressAction = getButtonConfig(NavButtonId.BACK).pressAction + val longPressAction = getButtonConfig(NavButtonId.BACK).longPressAction + val action = chain.args[0] as? Int ?: return@intercept chain.proceed() + val canceled = chain.args.getOrNull(1) as? Boolean ?: false + val customLongPress = longPressAction != NavButtonAction.STOCK + val longPressTriggered = aospTaskbarBackLongPressTriggered[chain.thisObject] == true - if (pressAction == NavButtonAction.STOCK && !customLongPress) { - return@intercept chain.proceed() + if (pressAction == NavButtonAction.STOCK && !customLongPress) { + return@intercept chain.proceed() + } + + updateContextFromField(chain.thisObject, "mContext") + + if (action == KeyEvent.ACTION_UP) { + aospTaskbarBackLongPressTriggered.remove(chain.thisObject) + } + + if (customLongPress && action == KeyEvent.ACTION_DOWN) { + return@intercept null + } + + if (action == KeyEvent.ACTION_UP && !canceled && !longPressTriggered) { + performAospTaskbarAction( + chain.thisObject, + if (pressAction == NavButtonAction.STOCK) NavButtonAction.BACK else pressAction, + ) + } + null } - - updateContextFromField(chain.thisObject, "mContext") - - if (action == KeyEvent.ACTION_UP) { - aospTaskbarBackLongPressTriggered.remove(chain.thisObject) - } - - if (customLongPress && action == KeyEvent.ACTION_DOWN) { - return@intercept null - } - - if (action == KeyEvent.ACTION_UP && !canceled && !longPressTriggered) { - performAospTaskbarAction( - chain.thisObject, - if (pressAction == NavButtonAction.STOCK) NavButtonAction.BACK else pressAction, - ) - } - null } return true @@ -1678,7 +1781,7 @@ class NavButtons : XposedModule() { } private fun sendSideArrowKeyPress(keyCode: Int) { - if (context?.packageName == AOSP_LAUNCHER_PACKAGE) { + if (context?.packageName in AOSP_LAUNCHER_PACKAGES) { requestPrivilegedAction(PRIVILEGED_ACTION_SEND_KEY_PRESS, keyCode) } else { sendKeyPress(keyCode) @@ -1701,13 +1804,13 @@ class NavButtons : XposedModule() { private fun getSideArrowLongPressAction(): SideArrowLongPressAction { return SideArrowLongPressAction.fromStorageValue( - remotePreferences?.getString(NavButtonSettingsStore.KEY_SIDE_ARROW_LONG_PRESS_ACTION, null), + getSettingsPreferences()?.getString(NavButtonSettingsStore.KEY_SIDE_ARROW_LONG_PRESS_ACTION, null), ) } private fun getSideArrowLongPressDurationMs(): Int { return ( - remotePreferences?.getInt( + getSettingsPreferences()?.getInt( NavButtonSettingsStore.KEY_SIDE_ARROW_LONG_PRESS_DURATION_MS, ViewConfiguration.getLongPressTimeout(), ) ?: ViewConfiguration.getLongPressTimeout() @@ -1716,7 +1819,7 @@ class NavButtons : XposedModule() { private fun sideArrowRepeatIntervalMs(): Long { val speed = sanitizeSideArrowRepeatSpeed( - remotePreferences?.getFloat( + getSettingsPreferences()?.getFloat( NavButtonSettingsStore.KEY_SIDE_ARROW_REPEAT_STEPS_PER_SECOND, NavButtonSettingsStore.DEFAULT_SIDE_ARROW_REPEAT_STEPS_PER_SECOND, ) ?: NavButtonSettingsStore.DEFAULT_SIDE_ARROW_REPEAT_STEPS_PER_SECOND, @@ -1886,6 +1989,9 @@ class NavButtons : XposedModule() { } private fun hookFoldedSideArrows(classLoader: ClassLoader?) { + if (!Build.MANUFACTURER.equals("samsung", ignoreCase = true)) { + return + } hookFoldedLayoutProvider(classLoader) hookSamsungNavigationBarView(classLoader) registerSettingsChangedReceiver() @@ -2237,7 +2343,7 @@ class NavButtons : XposedModule() { } sideArrowsEnabledOverride = intent.getBooleanExtra( NavButtonSettingsStore.EXTRA_SIDE_ARROWS_ENABLED, - remotePreferences?.getBoolean(NavButtonSettingsStore.KEY_SIDE_ARROWS_ENABLED, false) == true, + getSettingsPreferences()?.getBoolean(NavButtonSettingsStore.KEY_SIDE_ARROWS_ENABLED, false) == true, ) reInflateTrackedSamsungNavigationBars() refreshTrackedAospSystemUiSideArrowLayouts() @@ -2327,7 +2433,7 @@ class NavButtons : XposedModule() { senderPackage: String?, senderUid: Int, ): Boolean { - if (senderPackage == AOSP_LAUNCHER_PACKAGE) { + if (senderPackage in AOSP_LAUNCHER_PACKAGES) { return true } @@ -2344,11 +2450,11 @@ class NavButtons : XposedModule() { return true } return context.packageManager.getPackagesForUid(senderUid) - ?.contains(AOSP_LAUNCHER_PACKAGE) == true + ?.any { it in AOSP_LAUNCHER_PACKAGES } == true } private fun readPrivilegedActionToken(): String? { - return remotePreferences?.getString(NavButtonSettingsStore.KEY_PRIVILEGED_ACTION_TOKEN, null) + return getSettingsPreferences()?.getString(NavButtonSettingsStore.KEY_PRIVILEGED_ACTION_TOKEN, null) } private fun requestPrivilegedAction(action: String, keyCode: Int? = null) { @@ -2361,7 +2467,7 @@ class NavButtons : XposedModule() { } return } - val token = remotePreferences?.getString( + val token = getSettingsPreferences()?.getString( NavButtonSettingsStore.KEY_PRIVILEGED_ACTION_TOKEN, null, ) @@ -2587,7 +2693,7 @@ class NavButtons : XposedModule() { private fun isSideArrowsEnabled(): Boolean { return sideArrowsEnabledOverride - ?: (remotePreferences?.getBoolean(NavButtonSettingsStore.KEY_SIDE_ARROWS_ENABLED, false) == true) + ?: (getSettingsPreferences()?.getBoolean(NavButtonSettingsStore.KEY_SIDE_ARROWS_ENABLED, false) == true) } private fun injectKeyEvent(inputManager: Any, keyEvent: KeyEvent) { @@ -2773,7 +2879,7 @@ class NavButtons : XposedModule() { } private fun getButtonConfig(buttonId: NavButtonId): NavButtonConfig { - val preferences = remotePreferences + val preferences = getSettingsPreferences() val defaultDurationMs = ViewConfiguration.getLongPressTimeout().coerceLongPressDuration() return NavButtonConfig( pressAction = NavButtonAction.fromStorageValue( @@ -2828,7 +2934,7 @@ class NavButtons : XposedModule() { return packageName == null || packageName == SYSTEMUI_PACKAGE || packageName == SAMSUNG_LAUNCHER_PACKAGE || - packageName == AOSP_LAUNCHER_PACKAGE || + packageName in AOSP_LAUNCHER_PACKAGES || packageName == defaultLauncher } @@ -3137,6 +3243,22 @@ class NavButtons : XposedModule() { } } + private fun findFirstClass(classNames: List, classLoader: ClassLoader?): Class<*> { + classNames.forEach { className -> + findClassIfExists(className, classLoader)?.let { return it } + } + throw ClassNotFoundException(classNames.joinToString()) + } + + private fun findFirstClassIfExists( + classNames: List, + classLoader: ClassLoader?, + ): Class<*>? { + return classNames.firstNotNullOfOrNull { className -> + findClassIfExists(className, classLoader) + } + } + private fun findMethodIfExists( clazz: Class<*>, methodName: String, @@ -3361,14 +3483,19 @@ class NavButtons : XposedModule() { const val ASSIST_INVOCATION_TYPE_HOME_BUTTON_LONG_PRESS = 5 const val INPUT_MANAGER_CLASS = "android.hardware.input.InputManager" const val INPUT_EVENT_INJECTION_SYNC_NONE = 0 + const val ACTION_PASS_TO_USER = 1 const val HOME_LONG_PRESS_DEDUP_WINDOW_MS = 1_000L + const val REMOTE_PREFERENCES_RETRY_DELAY_MS = 1_000L + const val REMOTE_PREFERENCES_LOAD_ATTEMPTS = 15 const val STOCK_LONG_PRESS_GUARD_EARLY_MS = 75 const val STOCK_LONG_PRESS_GUARD_MIN_DELAY_MS = 100 const val SYSTEMUI_PACKAGE = "com.android.systemui" const val ANDROID_PACKAGE = "android" const val SYSTEM_SERVER_PHONE_WINDOW_MANAGER = "com.android.server.policy.PhoneWindowManager" - const val SYSTEMUI_KEY_BUTTON_VIEW = - "com.android.systemui.navigationbar.views.buttons.KeyButtonView" + val SYSTEMUI_KEY_BUTTON_VIEW_CLASSES = listOf( + "com.android.systemui.navigationbar.views.buttons.KeyButtonView", + "com.android.systemui.navigationbar.buttons.KeyButtonView", + ) const val SAMSUNG_PHONE_LAYOUT_PROVIDER = "com.android.systemui.navigationbar.layout.LayoutProviderImpl" const val SAMSUNG_TABLET_LAYOUT_PROVIDER = @@ -3376,7 +3503,10 @@ class NavButtons : XposedModule() { const val SAMSUNG_NAVIGATION_BAR_VIEW = "com.android.systemui.navigationbar.views.SamsungNavigationBarView" const val SAMSUNG_LAUNCHER_PACKAGE = "com.sec.android.app.launcher" - const val AOSP_LAUNCHER_PACKAGE = "com.android.launcher3" + val AOSP_LAUNCHER_PACKAGES = setOf( + "com.android.launcher3", + "com.google.android.apps.nexuslauncher", + ) const val AOSP_TASKBAR_NAV_BUTTON_CONTROLLER = "com.android.launcher3.taskbar.TaskbarNavButtonController" const val AOSP_TASKBAR_BACK_BUTTON = 1 @@ -3385,12 +3515,18 @@ class NavButtons : XposedModule() { const val AOSP_NAVBAR_BUTTONS_VIEW_CONTROLLER = "com.android.launcher3.taskbar.NavbarButtonsViewController" const val AOSP_TASKBAR_SIDE_BUTTON_SHIFT_RATIO = 0.45f - const val AOSP_NAVIGATION_BAR_INFLATER_VIEW = - "com.android.systemui.navigationbar.views.NavigationBarInflaterView" - const val AOSP_KEY_BUTTON_DRAWABLE = - "com.android.systemui.navigationbar.views.buttons.KeyButtonDrawable" - const val AOSP_KEY_BUTTON_DRAWABLE_STATE = - "com.android.systemui.navigationbar.views.buttons.KeyButtonDrawable\$ShadowDrawableState" + val AOSP_NAVIGATION_BAR_INFLATER_VIEW_CLASSES = listOf( + "com.android.systemui.navigationbar.views.NavigationBarInflaterView", + "com.android.systemui.navigationbar.NavigationBarInflaterView", + ) + val AOSP_KEY_BUTTON_DRAWABLE_CLASSES = listOf( + "com.android.systemui.navigationbar.views.buttons.KeyButtonDrawable", + "com.android.systemui.navigationbar.buttons.KeyButtonDrawable", + ) + val AOSP_KEY_BUTTON_DRAWABLE_STATE_CLASSES = listOf( + "com.android.systemui.navigationbar.views.buttons.KeyButtonDrawable\$ShadowDrawableState", + "com.android.systemui.navigationbar.buttons.KeyButtonDrawable\$ShadowDrawableState", + ) const val AOSP_SYSUI_LEFT_ARROW_BUTTON = "key(21)" const val AOSP_SYSUI_RIGHT_ARROW_BUTTON = "key(22)" const val ACTION_PRIVILEGED_ACTION = diff --git a/app/src/main/resources/META-INF/xposed/scope.list b/app/src/main/resources/META-INF/xposed/scope.list index a0d47ee..9c65593 100644 --- a/app/src/main/resources/META-INF/xposed/scope.list +++ b/app/src/main/resources/META-INF/xposed/scope.list @@ -2,5 +2,6 @@ android system com.android.systemui com.android.launcher3 +com.google.android.apps.nexuslauncher com.sec.android.app.launcher com.samsung.systemui.navillera