From 1fc63aef8dd6c8a28a631e8bb7c764676259100b Mon Sep 17 00:00:00 2001 From: ajp_anton Date: Mon, 22 Jun 2026 11:31:13 +0000 Subject: [PATCH] Add initial AOSP button support --- app/src/main/AndroidManifest.xml | 5 + .../navbuttons/NavButtonSettingsStore.kt | 18 + .../java/se/ajpanton/navbuttons/NavButtons.kt | 487 ++++++++++++++++++ .../main/resources/META-INF/xposed/scope.list | 2 + 4 files changed, 512 insertions(+) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index cb1a8fe..6855b7a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -3,6 +3,11 @@ xmlns:tools="http://schemas.android.com/tools"> + + + + + () private val systemUiHomeStockLongPressGuards = WeakHashMap() private val systemUiHomeStockPressCanceled = WeakHashMap() + private val aospTaskbarButtonIds = WeakHashMap() + private val aospTaskbarControllers = WeakHashMap() + private val aospTaskbarLongPressRunnables = WeakHashMap() + private val aospTaskbarLongPressTriggered = WeakHashMap() + private val aospTaskbarBackLongPressTriggered = WeakHashMap() private val honeyspaceLongPressTriggered = WeakHashMap() private val honeyspaceHomeStockLongPressGuards = WeakHashMap() private val honeyspaceHomeStockPressCanceled = WeakHashMap() @@ -78,6 +87,8 @@ class NavButtons : XposedModule() { private var foldedSideArrowProviderHookInstalled = false private var samsungNavigationBarViewHookInstalled = false private var settingsChangedReceiverRegistered = false + private var privilegedActionReceiverRegistered = false + private var privilegedActionToken: String? = null private var sideArrowsEnabledOverride: Boolean? = null override fun onModuleLoaded(param: XposedModuleInterface.ModuleLoadedParam) { @@ -121,6 +132,14 @@ class NavButtons : XposedModule() { return } + if (AOSP_LAUNCHER_PACKAGE == packageName) { + if (aospTaskbarHookInstalled) { + return + } + initContextAndHandler() + aospTaskbarHookInstalled = hookAospTaskbar(classLoader) + return + } } private fun initContextAndHandler() { @@ -349,6 +368,18 @@ class NavButtons : XposedModule() { Long::class.javaPrimitiveType!!, Int::class.javaPrimitiveType!!, ) + val systemReadyMethod = findMethodIfExists(phoneWindowManagerClass, "systemReady") + + if (systemReadyMethod != null) { + deoptimizeMethodIfPresent(systemReadyMethod) + hook(systemReadyMethod).intercept { chain -> + val result = chain.proceed() + updateContextFromField(chain.thisObject, "mContext") + registerPrivilegedActionReceiver() + result + } + installed = true + } if (longPressOnHomeWithEvent != null) { deoptimizeMethodIfPresent(longPressOnHomeWithEvent) @@ -400,6 +431,253 @@ class NavButtons : XposedModule() { return installed } + private fun hookAospTaskbar(classLoader: ClassLoader?): Boolean { + val controllerClass = findClassIfExists(AOSP_TASKBAR_NAV_BUTTON_CONTROLLER, classLoader) ?: run { + log("AOSP taskbar controller class not found: $AOSP_TASKBAR_NAV_BUTTON_CONTROLLER") + return false + } + val navButtonsViewControllerClass = findClassIfExists( + AOSP_NAVBAR_BUTTONS_VIEW_CONTROLLER, + classLoader, + ) + val clickMethod = findMethodIfExists( + controllerClass, + "onButtonClick", + Int::class.javaPrimitiveType!!, + View::class.java, + ) ?: run { + log("AOSP taskbar onButtonClick() method not found.") + return false + } + val longClickMethod = findMethodIfExists( + controllerClass, + "onButtonLongClick", + Int::class.javaPrimitiveType!!, + View::class.java, + ) ?: run { + log("AOSP taskbar onButtonLongClick() method not found.") + return false + } + val sendBackKeyEventMethod = findMethodIfExists( + controllerClass, + "sendBackKeyEvent", + Int::class.javaPrimitiveType!!, + Boolean::class.javaPrimitiveType!!, + ) ?: run { + log("AOSP taskbar sendBackKeyEvent() method not found.") + return false + } + + deoptimizeMethodIfPresent(clickMethod) + deoptimizeMethodIfPresent(longClickMethod) + deoptimizeMethodIfPresent(sendBackKeyEventMethod) + hookAospTaskbarButtonMapping(navButtonsViewControllerClass, controllerClass) + hookAospTaskbarTouchEvents() + + hook(clickMethod).intercept { chain -> + val buttonId = buttonIdForAospTaskbarCode(chain.args[0] as? Int) + ?: return@intercept chain.proceed() + val pressAction = getButtonConfig(buttonId).pressAction + if (pressAction == NavButtonAction.STOCK) { + return@intercept chain.proceed() + } + + updateContextFromTaskbar(chain.thisObject, chain.args.getOrNull(1)) + performAospTaskbarAction(chain.thisObject, pressAction) + null + } + + hook(longClickMethod).intercept { chain -> + val buttonId = buttonIdForAospTaskbarCode(chain.args[0] as? Int) + ?: return@intercept chain.proceed() + val longPressAction = getButtonConfig(buttonId).longPressAction + if (longPressAction == NavButtonAction.STOCK) { + return@intercept chain.proceed() + } + + val view = chain.args.getOrNull(1) + if (view != null && aospTaskbarButtonIds.containsKey(view)) { + return@intercept true + } + + updateContextFromTaskbar(chain.thisObject, chain.args.getOrNull(1)) + if (buttonId == NavButtonId.HOME) { + performAospTaskbarHomeLongPressAction(chain.thisObject, longPressAction) + } else { + performAospTaskbarAction(chain.thisObject, longPressAction) + } + 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 (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 + } + + return true + } + + private fun hookAospTaskbarButtonMapping( + navButtonsViewControllerClass: Class<*>?, + controllerClass: Class<*>, + ) { + if (navButtonsViewControllerClass == null) { + log("AOSP navbar buttons controller class not found: $AOSP_NAVBAR_BUTTONS_VIEW_CONTROLLER") + return + } + + listOf( + arrayOf( + Int::class.javaPrimitiveType!!, + Int::class.javaPrimitiveType!!, + ViewGroup::class.java, + controllerClass, + Int::class.javaPrimitiveType!!, + ), + arrayOf( + Int::class.javaPrimitiveType!!, + Int::class.javaPrimitiveType!!, + ViewGroup::class.java, + controllerClass, + Int::class.javaPrimitiveType!!, + Int::class.javaPrimitiveType!!, + ), + ).forEach { parameterTypes -> + val addButtonMethod = findMethodIfExists( + navButtonsViewControllerClass, + "addButton", + *parameterTypes, + ) ?: return@forEach + deoptimizeMethodIfPresent(addButtonMethod) + hook(addButtonMethod).intercept { chain -> + val result = chain.proceed() + val view = result as? View ?: return@intercept result + trackAospTaskbarButton( + view, + chain.args.getOrNull(1) as? Int, + chain.args.getOrNull(3), + ) + result + } + } + } + + private fun hookAospTaskbarTouchEvents() { + val onTouchEventMethod = findMethodIfExists( + View::class.java, + "onTouchEvent", + MotionEvent::class.java, + ) ?: run { + log("AOSP taskbar View.onTouchEvent() method not found.") + return + } + + deoptimizeMethodIfPresent(onTouchEventMethod) + hook(onTouchEventMethod).intercept { chain -> + val view = chain.thisObject as? View ?: return@intercept chain.proceed() + val event = chain.args.getOrNull(0) as? MotionEvent ?: return@intercept chain.proceed() + val overrideResult = handleAospTaskbarTouch(view, event) + overrideResult ?: chain.proceed() + } + } + + private fun trackAospTaskbarButton(view: View, buttonCode: Int?, controller: Any?) { + val buttonId = buttonIdForAospTaskbarCode(buttonCode) ?: return + aospTaskbarButtonIds[view] = buttonId + if (controller != null) { + aospTaskbarControllers[view] = controller + } + } + + private fun handleAospTaskbarTouch(view: View, event: MotionEvent): Boolean? { + val buttonId = aospTaskbarButtonIds[view] ?: return null + val controller = aospTaskbarControllers[view] ?: return null + val config = getButtonConfig(buttonId) + if (config.longPressAction == NavButtonAction.STOCK) { + return null + } + + when (event.actionMasked) { + MotionEvent.ACTION_DOWN -> { + aospTaskbarLongPressTriggered[view] = false + if (buttonId == NavButtonId.BACK) { + aospTaskbarBackLongPressTriggered[controller] = false + } + scheduleAospTaskbarLongPress(view, controller, buttonId, config) + } + + MotionEvent.ACTION_UP, + MotionEvent.ACTION_CANCEL, + -> { + val triggered = aospTaskbarLongPressTriggered[view] == true + cancelAospTaskbarLongPress(view) + aospTaskbarLongPressTriggered.remove(view) + if (event.actionMasked == MotionEvent.ACTION_CANCEL && buttonId == NavButtonId.BACK) { + aospTaskbarBackLongPressTriggered.remove(controller) + } + if (triggered) { + return true + } + } + } + + return null + } + + private fun scheduleAospTaskbarLongPress( + view: View, + controller: Any, + buttonId: NavButtonId, + config: NavButtonConfig, + ) { + cancelAospTaskbarLongPress(view) + val longPressRunnable = Runnable { + aospTaskbarLongPressTriggered[view] = true + if (buttonId == NavButtonId.BACK) { + aospTaskbarBackLongPressTriggered[controller] = true + } + updateContextFromTaskbar(controller, view) + view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS) + if (buttonId == NavButtonId.HOME) { + performAospTaskbarHomeLongPressAction(controller, config.longPressAction) + } else { + performAospTaskbarAction(controller, config.longPressAction) + } + } + aospTaskbarLongPressRunnables[view] = longPressRunnable + view.postDelayed(longPressRunnable, config.longPressDurationMs.toLong()) + } + + private fun cancelAospTaskbarLongPress(view: View) { + aospTaskbarLongPressRunnables.remove(view)?.let(view::removeCallbacks) + } + private fun hookHoneyspaceTaskbar(classLoader: ClassLoader?): Boolean { val navButtonClass = findClassIfExists(HONEYSPACE_NAV_BUTTON_VIEW, classLoader) ?: run { log("Launcher class not found: $HONEYSPACE_NAV_BUTTON_VIEW") @@ -937,6 +1215,14 @@ class NavButtons : XposedModule() { context = currentContext.applicationContext ?: currentContext } + private fun updateContextFromTaskbar(controller: Any?, view: Any?) { + if (view is View) { + updateContextFromView(view) + return + } + updateContextFromField(controller, "mContext") + } + private fun updateContextFromField(target: Any?, fieldName: String) { val currentContext = getFieldValue(target, fieldName) as? Context ?: return context = currentContext.applicationContext ?: currentContext @@ -958,6 +1244,70 @@ class NavButtons : XposedModule() { } } + private fun performAospTaskbarHomeLongPressAction(controller: Any?, action: NavButtonAction) { + if (wasHomeLongPressHandledRecently()) { + return + } + noteHomeLongPressHandled() + performAospTaskbarAction(controller, action) + } + + private fun performAospTaskbarAction(controller: Any?, action: NavButtonAction) { + when (action) { + NavButtonAction.STOCK, + NavButtonAction.NONE, + -> Unit + + NavButtonAction.BACK -> { + if (!callAospTaskbarMethod(controller, "executeBack", arrayOf(KeyEvent::class.java), null)) { + performConfiguredAction(action) + } + } + + NavButtonAction.HOME -> { + if (!callAospTaskbarMethod(controller, "navigateHome", emptyArray())) { + performConfiguredAction(action) + } + } + + NavButtonAction.ASSISTANT -> { + if (!callAospTaskbarMethod(controller, "onLongPressHome", emptyArray())) { + performConfiguredAction(action) + } + } + + NavButtonAction.RECENTS -> { + if (!callAospTaskbarMethod(controller, "navigateToOverview", emptyArray())) { + performConfiguredAction(action) + } + } + + NavButtonAction.KILL_FOREGROUND_APP -> requestPrivilegedAction(PRIVILEGED_ACTION_KILL_FOREGROUND_APP) + + NavButtonAction.TOGGLE_FLASHLIGHT -> requestPrivilegedAction(PRIVILEGED_ACTION_TOGGLE_FLASHLIGHT) + + NavButtonAction.TOGGLE_AUTO_ROTATE -> performConfiguredAction(action) + } + } + + private fun callAospTaskbarMethod( + controller: Any?, + methodName: String, + parameterTypes: Array>, + vararg args: Any?, + ): Boolean { + if (controller == null) { + return false + } + return try { + callMethod(controller, methodName, parameterTypes, *args) + true + } catch (error: Throwable) { + log("AOSP taskbar $methodName failed: ${error.javaClass.simpleName}") + false + } + } + private fun sendKeyPress(keyCode: Int) { try { val inputManager = callStaticMethod( @@ -1316,6 +1666,9 @@ class NavButtons : XposedModule() { return } val currentContext = context ?: return + if (currentContext.packageName == ANDROID_PACKAGE) { + return + } val receiver = object : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { @@ -1343,6 +1696,115 @@ class NavButtons : XposedModule() { } } + private fun registerPrivilegedActionReceiver() { + if (privilegedActionReceiverRegistered) { + return + } + val currentContext = context ?: getCurrentApplication() ?: return + context = currentContext.applicationContext ?: currentContext + + val receiver = object : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + if (intent?.action != ACTION_PRIVILEGED_ACTION || + context == null || + !isAllowedPrivilegedActionSender( + context, + intent, + getSentFromPackageCompat(), + getSentFromUidCompat(), + ) + ) { + return + } + this@NavButtons.context = context.applicationContext ?: context + when (intent.getStringExtra(EXTRA_PRIVILEGED_ACTION)) { + PRIVILEGED_ACTION_KILL_FOREGROUND_APP -> killForegroundApp() + PRIVILEGED_ACTION_TOGGLE_FLASHLIGHT -> toggleFlashlight() + } + } + } + + try { + currentContext.registerReceiver( + receiver, + IntentFilter(ACTION_PRIVILEGED_ACTION), + Context.RECEIVER_EXPORTED, + ) + privilegedActionToken = readPrivilegedActionToken() + privilegedActionReceiverRegistered = true + } catch (error: Throwable) { + log("Privileged action receiver failed", error) + } + } + + private fun BroadcastReceiver.getSentFromUidCompat(): Int { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + sentFromUid + } else { + Process.INVALID_UID + } + } + + private fun BroadcastReceiver.getSentFromPackageCompat(): String? { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + sentFromPackage + } else { + null + } + } + + private fun isAllowedPrivilegedActionSender( + context: Context, + intent: Intent, + senderPackage: String?, + senderUid: Int, + ): Boolean { + if (senderPackage == AOSP_LAUNCHER_PACKAGE) { + return true + } + + val expectedToken = privilegedActionToken ?: readPrivilegedActionToken()?.also { + privilegedActionToken = it + } + if (expectedToken != null && + expectedToken == intent.getStringExtra(EXTRA_PRIVILEGED_ACTION_TOKEN) + ) { + return true + } + + if (senderUid == Process.SYSTEM_UID) { + return true + } + return context.packageManager.getPackagesForUid(senderUid) + ?.contains(AOSP_LAUNCHER_PACKAGE) == true + } + + private fun readPrivilegedActionToken(): String? { + return remotePreferences?.getString(NavButtonSettingsStore.KEY_PRIVILEGED_ACTION_TOKEN, null) + } + + private fun requestPrivilegedAction(action: String) { + val currentContext = context + if (currentContext == null) { + when (action) { + PRIVILEGED_ACTION_KILL_FOREGROUND_APP -> killForegroundApp() + PRIVILEGED_ACTION_TOGGLE_FLASHLIGHT -> toggleFlashlight() + } + return + } + val token = remotePreferences?.getString( + NavButtonSettingsStore.KEY_PRIVILEGED_ACTION_TOKEN, + null, + ) + val intent = Intent(ACTION_PRIVILEGED_ACTION) + .setPackage(ANDROID_PACKAGE) + .putExtra(EXTRA_PRIVILEGED_ACTION, action) + if (token != null) { + intent.putExtra(EXTRA_PRIVILEGED_ACTION_TOKEN, token) + } + currentContext.sendBroadcast(intent) + } + private fun trackSamsungNavigationBarView(startView: Any) { var current: View? = startView as? View ?: return repeat(SAMSUNG_NAV_VIEW_PARENT_DEPTH) { @@ -1766,6 +2228,15 @@ class NavButtons : XposedModule() { } } + private fun buttonIdForAospTaskbarCode(buttonCode: Int?): NavButtonId? { + return when (buttonCode) { + AOSP_TASKBAR_BACK_BUTTON -> NavButtonId.BACK + AOSP_TASKBAR_HOME_BUTTON -> NavButtonId.HOME + AOSP_TASKBAR_RECENTS_BUTTON -> NavButtonId.RECENTS + else -> null + } + } + private fun getPackageNameFromComponent(component: Any?): String? { return (component as? ComponentName)?.packageName } @@ -1785,6 +2256,7 @@ class NavButtons : XposedModule() { return packageName == null || packageName == SYSTEMUI_PACKAGE || packageName == SAMSUNG_LAUNCHER_PACKAGE || + packageName == AOSP_LAUNCHER_PACKAGE || packageName == defaultLauncher } @@ -2316,6 +2788,7 @@ class NavButtons : XposedModule() { 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" @@ -2326,6 +2799,20 @@ 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" + const val AOSP_TASKBAR_NAV_BUTTON_CONTROLLER = + "com.android.launcher3.taskbar.TaskbarNavButtonController" + const val AOSP_TASKBAR_BACK_BUTTON = 1 + const val AOSP_TASKBAR_HOME_BUTTON = 2 + const val AOSP_TASKBAR_RECENTS_BUTTON = 4 + const val AOSP_NAVBAR_BUTTONS_VIEW_CONTROLLER = + "com.android.launcher3.taskbar.NavbarButtonsViewController" + const val ACTION_PRIVILEGED_ACTION = + "se.ajpanton.navbuttons.action.PRIVILEGED_ACTION" + const val EXTRA_PRIVILEGED_ACTION = "privileged_action" + const val EXTRA_PRIVILEGED_ACTION_TOKEN = "privileged_action_token" + const val PRIVILEGED_ACTION_KILL_FOREGROUND_APP = "kill_foreground_app" + const val PRIVILEGED_ACTION_TOGGLE_FLASHLIGHT = "toggle_flashlight" const val SIDE_ARROW_ICON_DP = 32f const val SIDE_ARROW_GLYPH_SCALE = 0.8f const val SIDE_ARROW_STROKE_WIDTH = 1.45f diff --git a/app/src/main/resources/META-INF/xposed/scope.list b/app/src/main/resources/META-INF/xposed/scope.list index 3f5b083..a0d47ee 100644 --- a/app/src/main/resources/META-INF/xposed/scope.list +++ b/app/src/main/resources/META-INF/xposed/scope.list @@ -1,4 +1,6 @@ android +system com.android.systemui +com.android.launcher3 com.sec.android.app.launcher com.samsung.systemui.navillera