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