Improve AOSP side arrow support

This commit is contained in:
ajp_anton
2026-06-23 13:04:42 +00:00
parent c2ee5a5581
commit 60769980cd
2 changed files with 116 additions and 35 deletions
+21 -4
View File
@@ -13,16 +13,33 @@ It lets you choose what should happen when Back, Home, or Recents is pressed or
- Kill the foreground app.
- Toggle auto-rotate.
- Toggle the flashlight.
- Add optional side-arrow buttons for moving the text cursor left and right.
- Configure side-arrow long-press behavior, including repeated cursor movement.
- Test long-press durations from the settings screen.
## Requirements
- Android device using three-button navigation.
- LSPosed.
- Xposed API 101 or newer.
- The module enabled for System UI.
- LSPosed with Xposed API 101 or newer.
- Android 14 / API 34 or newer.
- The module enabled for the relevant navigation packages.
This has only been tested on a Samsung Galaxy Z Fold5 running One UI 8.0 / Android 16. Compatibility may vary between Android versions and vendor System UI implementations.
The module declares scope entries for:
- `android` and `system`, used for privileged actions such as killing the foreground app.
- `com.android.systemui`, used for phone navigation bars.
- `com.android.launcher3` and `com.google.android.apps.nexuslauncher`, used for AOSP/Google taskbar navigation.
- `com.sec.android.app.launcher` and `com.samsung.systemui.navillera`, used for Samsung/One UI navigation.
## Tested On
- Samsung Galaxy Z Fold5 running One UI 8.0 / Android 16 with Magisk and LSPosed.
- AOSP emulator API 34 / Android 14 with Magisk and LSPosed.
- AOSP emulator API 35 / Android 15 with Magisk and LSPosed.
- AOSP emulator API 36 / Android 16 with Magisk and LSPosed.
- Android 16 Google APIs emulator with Magisk and LSPosed.
The emulator tests covered button press overrides, long-press overrides with custom duration, side-arrow visibility, side-arrow tinting, enabling/disabling side arrows, and side-arrow long-press repeat behavior where supported by the emulator navigation implementation. Actual device testing with others than the Galaxy Z Fold 5 has not been done. Compatibility may vary between Android versions and vendor System UI / launcher implementations.
## AI Assistance
@@ -94,6 +94,7 @@ class NavButtons : XposedModule() {
private var samsungNavigationBarViewHookInstalled = false
private var aospSystemUiSideArrowHookInstalled = false
private var aospTaskbarTintHookInstalled = false
private var aospSystemUiDarkIntensity = 0f
private var settingsChangedReceiverRegistered = false
private var privilegedActionReceiverRegistered = false
private var privilegedActionToken: String? = null
@@ -543,14 +544,14 @@ class NavButtons : XposedModule() {
private fun hookAospSystemUiSideArrows(classLoader: ClassLoader?, keyButtonClass: Class<*>) {
if (aospSystemUiSideArrowHookInstalled ||
Build.MANUFACTURER.equals("samsung", ignoreCase = true) ||
Build.VERSION.SDK_INT < 35
Build.MANUFACTURER.equals("samsung", ignoreCase = true)
) {
return
}
val inflaterClass = findFirstClassIfExists(AOSP_NAVIGATION_BAR_INFLATER_VIEW_CLASSES, classLoader) ?: return
aospSystemUiSideArrowHookInstalled = true
registerSettingsChangedReceiver()
hookAospSystemUiDarkIntensity(classLoader)
findMethodIfExists(inflaterClass, "getDefaultLayout")?.let { method ->
deoptimizeMethodIfPresent(method)
@@ -572,14 +573,14 @@ class NavButtons : XposedModule() {
chain.thisObject?.let(::trackAospSystemUiInflater)
val layout = chain.args.getOrNull(0) as? String
if (layout != null) {
chain.args[0] = if (isSideArrowsEnabled()) {
val updatedLayout = if (isSideArrowsEnabled()) {
addAospSystemUiSideArrows(layout)
} else {
removeAospSystemUiSideArrows(layout)
}
runCatching { chain.args[0] = updatedLayout }
}
val result = chain.proceed()
result
chain.proceed()
}
}
@@ -605,17 +606,6 @@ class NavButtons : XposedModule() {
}
}
findMethodIfExists(keyButtonClass, "setImageDrawable", Drawable::class.java)?.let { method ->
deoptimizeMethodIfPresent(method)
hook(method).intercept { chain ->
aospSystemUiSideArrowDirections[chain.thisObject]?.let { direction ->
createAospSystemUiSideArrowDrawable(chain.thisObject, direction)?.let { drawable ->
chain.args[0] = drawable
}
}
chain.proceed()
}
}
}
private fun trackAospSystemUiInflater(inflater: Any) {
@@ -762,6 +752,34 @@ class NavButtons : XposedModule() {
}
}
private fun hookAospSystemUiDarkIntensity(classLoader: ClassLoader?) {
val transitionsClass = findFirstClassIfExists(
AOSP_NAVIGATION_BAR_TRANSITIONS_CLASSES,
classLoader,
) ?: return
findMethodIfExists(
transitionsClass,
"applyDarkIntensity",
Float::class.javaPrimitiveType!!,
)?.let { method ->
deoptimizeMethodIfPresent(method)
hook(method).intercept { chain ->
val result = chain.proceed()
aospSystemUiDarkIntensity = (chain.args.getOrNull(0) as? Float)?.coerceIn(0f, 1f)
?: aospSystemUiDarkIntensity
updateAospSystemUiSideArrowDarkIntensity()
result
}
}
}
private fun updateAospSystemUiSideArrowDarkIntensity() {
aospSystemUiSideArrowDirections.keys.toList().forEach { view ->
callFloatMethodIfExists(view, "setDarkIntensity", aospSystemUiDarkIntensity)
setFieldIfExists(view, "mDarkIntensity", aospSystemUiDarkIntensity)
}
}
private fun findAospSystemUiKeyButtons(view: View): List<ImageView> {
val result = mutableListOf<ImageView>()
if (view is ImageView && getFieldValue(view, "mCode") != null) {
@@ -779,7 +797,10 @@ class NavButtons : XposedModule() {
val view = value as? ImageView ?: return
val direction = aospSystemUiSideArrowDirections[view] ?: return
ensureContextFromView(view)
createAospSystemUiSideArrowDrawable(view, direction)?.let(view::setImageDrawable)
createAospSystemUiSideArrowDrawable(view, direction)?.let {
view.setImageDrawable(it)
callFloatMethodIfExists(view, "setDarkIntensity", aospSystemUiDarkIntensity)
}
}
private fun createAospSystemUiSideArrowDrawable(
@@ -798,17 +819,18 @@ class NavButtons : XposedModule() {
Boolean::class.javaPrimitiveType,
Boolean::class.javaPrimitiveType,
)
.apply { isAccessible = true }
.newInstance(Color.WHITE, Color.rgb(55, 58, 62), false, false)
keyButtonDrawableClass
val drawable = keyButtonDrawableClass
.getDeclaredConstructor(Drawable::class.java, stateClass)
.apply { isAccessible = true }
.newInstance(
sideArrowBitmapDrawable(
view.context,
direction,
Color.WHITE,
),
sideArrowBitmapDrawable(view.context, direction, Color.WHITE),
state,
) as? Drawable
drawable?.also {
callFloatMethodIfExists(it, "setDarkIntensity", aospSystemUiDarkIntensity)
}
} catch (error: Throwable) {
log("AOSP SystemUI side arrow drawable failed: ${error.javaClass.simpleName}")
null
@@ -1113,7 +1135,7 @@ class NavButtons : XposedModule() {
) {
arrow.minimumWidth = home.minimumWidth
arrow.minimumHeight = home.minimumHeight
arrow.setPadding(0, home.paddingTop, 0, home.paddingBottom)
arrow.setPadding(0, 0, 0, 0)
arrow.scaleType = ImageView.ScaleType.CENTER
arrow.setImageDrawable(sideArrowBitmapDrawable(arrow.context, direction, Color.WHITE))
}
@@ -1204,8 +1226,14 @@ class NavButtons : XposedModule() {
}
private fun placeAospTaskbarArrow(arrow: View, center: Point) {
arrow.x = center.x - arrow.width / 2f
arrow.y = center.y - arrow.height / 2f
val width = arrow.width.takeIf { it > 0 }
?: arrow.layoutParams?.width?.takeIf { it > 0 }
?: 0
val height = arrow.height.takeIf { it > 0 }
?: arrow.layoutParams?.height?.takeIf { it > 0 }
?: 0
arrow.x = center.x - width / 2f
arrow.y = center.y - height / 2f
}
private fun removeAospSideArrows(container: ViewGroup) {
@@ -1901,9 +1929,23 @@ class NavButtons : XposedModule() {
NavButtonAction.HOME -> sendKeyPress(KeyEvent.KEYCODE_HOME)
NavButtonAction.ASSISTANT -> sendKeyPress(KeyEvent.KEYCODE_ASSIST)
NavButtonAction.RECENTS -> sendKeyPress(KeyEvent.KEYCODE_APP_SWITCH)
NavButtonAction.KILL_FOREGROUND_APP -> killForegroundApp()
NavButtonAction.KILL_FOREGROUND_APP ->
performPrivilegedActionOrDirect(PRIVILEGED_ACTION_KILL_FOREGROUND_APP)
NavButtonAction.TOGGLE_AUTO_ROTATE -> toggleAutoRotate()
NavButtonAction.TOGGLE_FLASHLIGHT -> toggleFlashlight()
NavButtonAction.TOGGLE_FLASHLIGHT ->
performPrivilegedActionOrDirect(PRIVILEGED_ACTION_TOGGLE_FLASHLIGHT)
}
}
private fun performPrivilegedActionOrDirect(action: String) {
if (context?.packageName == ANDROID_PACKAGE) {
when (action) {
PRIVILEGED_ACTION_KILL_FOREGROUND_APP -> killForegroundApp()
PRIVILEGED_ACTION_TOGGLE_FLASHLIGHT -> toggleFlashlight()
}
} else {
requestPrivilegedAction(action)
}
}
@@ -2837,7 +2879,10 @@ class NavButtons : XposedModule() {
return resolveInfo?.activityInfo?.packageName
}
private fun getTopPackageName(): String? = getTopPackageFromActivityTaskManager()
private fun getTopPackageName(): String? {
return getTopPackageFromActivityTaskManager()
?: getTopPackageFromRunningTasks()
}
private fun getTopPackageFromActivityTaskManager(): String? {
return try {
@@ -2862,6 +2907,21 @@ class NavButtons : XposedModule() {
return pkg ?: getPackageNameFromActivityInfo(getFieldValue(taskInfo, "topActivityInfo"))
}
@Suppress("DEPRECATION")
private fun getTopPackageFromRunningTasks(): String? {
val activityManager = context?.getSystemService(Context.ACTIVITY_SERVICE) as? ActivityManager
?: return null
return try {
activityManager.getRunningTasks(1)
.firstOrNull()
?.topActivity
?.packageName
} catch (error: Throwable) {
log("ActivityManager.getRunningTasks failed: ${error.javaClass.simpleName}")
null
}
}
private fun getFieldValue(target: Any?, fieldName: String): Any? {
if (target == null) {
return null
@@ -3519,6 +3579,10 @@ class NavButtons : XposedModule() {
"com.android.systemui.navigationbar.views.NavigationBarInflaterView",
"com.android.systemui.navigationbar.NavigationBarInflaterView",
)
val AOSP_NAVIGATION_BAR_TRANSITIONS_CLASSES = listOf(
"com.android.systemui.navigationbar.NavigationBarTransitions",
"com.android.systemui.navigationbar.views.NavigationBarTransitions",
)
val AOSP_KEY_BUTTON_DRAWABLE_CLASSES = listOf(
"com.android.systemui.navigationbar.views.buttons.KeyButtonDrawable",
"com.android.systemui.navigationbar.buttons.KeyButtonDrawable",
@@ -3527,8 +3591,8 @@ class NavButtons : XposedModule() {
"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 AOSP_SYSUI_LEFT_ARROW_BUTTON = "key(21:)"
const val AOSP_SYSUI_RIGHT_ARROW_BUTTON = "key(22:)"
const val ACTION_PRIVILEGED_ACTION =
"se.ajpanton.navbuttons.action.PRIVILEGED_ACTION"
const val EXTRA_PRIVILEGED_ACTION = "privileged_action"