Add initial AOSP button support

This commit is contained in:
ajp_anton
2026-06-22 11:31:13 +00:00
parent e9686f6cb3
commit 1fc63aef8d
4 changed files with 512 additions and 0 deletions
+5
View File
@@ -3,6 +3,11 @@
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.KILL_BACKGROUND_PROCESSES" />
<queries>
<package android:name="org.lsposed.manager" />
</queries>
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
@@ -7,6 +7,7 @@ import android.view.ViewConfiguration
import androidx.annotation.StringRes
import io.github.libxposed.service.XposedService
import io.github.libxposed.service.XposedServiceHelper
import java.util.UUID
enum class NavButtonId(
val preferencePrefix: String,
@@ -68,6 +69,7 @@ class NavButtonSettingsStore(context: Context) {
)
init {
ensurePrivilegedActionToken()
registerRemotePreferencesListener(localPreferences)
}
@@ -197,6 +199,15 @@ class NavButtonSettingsStore(context: Context) {
}
}
private fun ensurePrivilegedActionToken() {
if (localPreferences.contains(KEY_PRIVILEGED_ACTION_TOKEN)) {
return
}
writeToPreferences(localPreferences, System.currentTimeMillis()) {
putString(KEY_PRIVILEGED_ACTION_TOKEN, UUID.randomUUID().toString())
}
}
private fun writeToPreferences(
preferences: SharedPreferences,
updatedAtMs: Long,
@@ -220,6 +231,7 @@ class NavButtonSettingsStore(context: Context) {
const val KEY_SIDE_ARROW_LONG_PRESS_ACTION = "side_arrow_long_press_action"
const val KEY_SIDE_ARROW_REPEAT_STEPS_PER_SECOND = "side_arrow_repeat_steps_per_second"
const val KEY_SIDE_ARROW_LONG_PRESS_DURATION_MS = "side_arrow_long_press_duration_ms"
const val KEY_PRIVILEGED_ACTION_TOKEN = "privileged_action_token"
private const val KEY_LAST_UPDATED_AT_MS = "last_updated_at_ms"
const val MIN_DURATION_MS = 100
@@ -358,6 +370,12 @@ class NavButtonSettingsStore(context: Context) {
editor.remove(KEY_SIDE_ARROWS_ENABLED)
}
if (source.contains(KEY_PRIVILEGED_ACTION_TOKEN)) {
editor.putString(KEY_PRIVILEGED_ACTION_TOKEN, source.getString(KEY_PRIVILEGED_ACTION_TOKEN, null))
} else {
editor.remove(KEY_PRIVILEGED_ACTION_TOKEN)
}
if (source.contains(KEY_LAST_UPDATED_AT_MS)) {
editor.putLong(KEY_LAST_UPDATED_AT_MS, source.getLong(KEY_LAST_UPDATED_AT_MS, 0L))
} else {
@@ -22,12 +22,15 @@ import android.graphics.drawable.Drawable
import android.hardware.camera2.CameraAccessException
import android.hardware.camera2.CameraCharacteristics
import android.hardware.camera2.CameraManager
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.os.Process
import android.os.SystemClock
import android.provider.Settings
import android.util.Log
import android.view.InputEvent
import android.view.HapticFeedbackConstants
import android.view.KeyEvent
import android.view.LayoutInflater
import android.view.MotionEvent
@@ -59,12 +62,18 @@ class NavButtons : XposedModule() {
private var systemUiHookInstalled = false
private var systemServerHookInstalled = false
private var launcherHookInstalled = false
private var aospTaskbarHookInstalled = false
private var lastHomeLongPressHandledAtMs = 0L
private var flashlightCameraId: String? = null
private var flashlightEnabled = false
private val systemUiLongPressTriggered = WeakHashMap<Any, Boolean>()
private val systemUiHomeStockLongPressGuards = WeakHashMap<Any, Runnable>()
private val systemUiHomeStockPressCanceled = WeakHashMap<Any, Boolean>()
private val aospTaskbarButtonIds = WeakHashMap<Any, NavButtonId>()
private val aospTaskbarControllers = WeakHashMap<Any, Any>()
private val aospTaskbarLongPressRunnables = WeakHashMap<Any, Runnable>()
private val aospTaskbarLongPressTriggered = WeakHashMap<Any, Boolean>()
private val aospTaskbarBackLongPressTriggered = WeakHashMap<Any, Boolean>()
private val honeyspaceLongPressTriggered = WeakHashMap<Any, Boolean>()
private val honeyspaceHomeStockLongPressGuards = WeakHashMap<Any, Runnable>()
private val honeyspaceHomeStockPressCanceled = WeakHashMap<Any, Boolean>()
@@ -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<Class<*>>,
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
@@ -1,4 +1,6 @@
android
system
com.android.systemui
com.android.launcher3
com.sec.android.app.launcher
com.samsung.systemui.navillera