Improve AOSP side arrow support
This commit is contained in:
@@ -13,16 +13,33 @@ It lets you choose what should happen when Back, Home, or Recents is pressed or
|
|||||||
- Kill the foreground app.
|
- Kill the foreground app.
|
||||||
- Toggle auto-rotate.
|
- Toggle auto-rotate.
|
||||||
- Toggle the flashlight.
|
- 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.
|
- Test long-press durations from the settings screen.
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
- Android device using three-button navigation.
|
- Android device using three-button navigation.
|
||||||
- LSPosed.
|
- LSPosed with Xposed API 101 or newer.
|
||||||
- Xposed API 101 or newer.
|
- Android 14 / API 34 or newer.
|
||||||
- The module enabled for System UI.
|
- 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
|
## AI Assistance
|
||||||
|
|
||||||
|
|||||||
@@ -94,6 +94,7 @@ class NavButtons : XposedModule() {
|
|||||||
private var samsungNavigationBarViewHookInstalled = false
|
private var samsungNavigationBarViewHookInstalled = false
|
||||||
private var aospSystemUiSideArrowHookInstalled = false
|
private var aospSystemUiSideArrowHookInstalled = false
|
||||||
private var aospTaskbarTintHookInstalled = false
|
private var aospTaskbarTintHookInstalled = false
|
||||||
|
private var aospSystemUiDarkIntensity = 0f
|
||||||
private var settingsChangedReceiverRegistered = false
|
private var settingsChangedReceiverRegistered = false
|
||||||
private var privilegedActionReceiverRegistered = false
|
private var privilegedActionReceiverRegistered = false
|
||||||
private var privilegedActionToken: String? = null
|
private var privilegedActionToken: String? = null
|
||||||
@@ -543,14 +544,14 @@ class NavButtons : XposedModule() {
|
|||||||
|
|
||||||
private fun hookAospSystemUiSideArrows(classLoader: ClassLoader?, keyButtonClass: Class<*>) {
|
private fun hookAospSystemUiSideArrows(classLoader: ClassLoader?, keyButtonClass: Class<*>) {
|
||||||
if (aospSystemUiSideArrowHookInstalled ||
|
if (aospSystemUiSideArrowHookInstalled ||
|
||||||
Build.MANUFACTURER.equals("samsung", ignoreCase = true) ||
|
Build.MANUFACTURER.equals("samsung", ignoreCase = true)
|
||||||
Build.VERSION.SDK_INT < 35
|
|
||||||
) {
|
) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
val inflaterClass = findFirstClassIfExists(AOSP_NAVIGATION_BAR_INFLATER_VIEW_CLASSES, classLoader) ?: return
|
val inflaterClass = findFirstClassIfExists(AOSP_NAVIGATION_BAR_INFLATER_VIEW_CLASSES, classLoader) ?: return
|
||||||
aospSystemUiSideArrowHookInstalled = true
|
aospSystemUiSideArrowHookInstalled = true
|
||||||
registerSettingsChangedReceiver()
|
registerSettingsChangedReceiver()
|
||||||
|
hookAospSystemUiDarkIntensity(classLoader)
|
||||||
|
|
||||||
findMethodIfExists(inflaterClass, "getDefaultLayout")?.let { method ->
|
findMethodIfExists(inflaterClass, "getDefaultLayout")?.let { method ->
|
||||||
deoptimizeMethodIfPresent(method)
|
deoptimizeMethodIfPresent(method)
|
||||||
@@ -572,14 +573,14 @@ class NavButtons : XposedModule() {
|
|||||||
chain.thisObject?.let(::trackAospSystemUiInflater)
|
chain.thisObject?.let(::trackAospSystemUiInflater)
|
||||||
val layout = chain.args.getOrNull(0) as? String
|
val layout = chain.args.getOrNull(0) as? String
|
||||||
if (layout != null) {
|
if (layout != null) {
|
||||||
chain.args[0] = if (isSideArrowsEnabled()) {
|
val updatedLayout = if (isSideArrowsEnabled()) {
|
||||||
addAospSystemUiSideArrows(layout)
|
addAospSystemUiSideArrows(layout)
|
||||||
} else {
|
} else {
|
||||||
removeAospSystemUiSideArrows(layout)
|
removeAospSystemUiSideArrows(layout)
|
||||||
}
|
}
|
||||||
|
runCatching { chain.args[0] = updatedLayout }
|
||||||
}
|
}
|
||||||
val result = chain.proceed()
|
chain.proceed()
|
||||||
result
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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) {
|
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> {
|
private fun findAospSystemUiKeyButtons(view: View): List<ImageView> {
|
||||||
val result = mutableListOf<ImageView>()
|
val result = mutableListOf<ImageView>()
|
||||||
if (view is ImageView && getFieldValue(view, "mCode") != null) {
|
if (view is ImageView && getFieldValue(view, "mCode") != null) {
|
||||||
@@ -779,7 +797,10 @@ class NavButtons : XposedModule() {
|
|||||||
val view = value as? ImageView ?: return
|
val view = value as? ImageView ?: return
|
||||||
val direction = aospSystemUiSideArrowDirections[view] ?: return
|
val direction = aospSystemUiSideArrowDirections[view] ?: return
|
||||||
ensureContextFromView(view)
|
ensureContextFromView(view)
|
||||||
createAospSystemUiSideArrowDrawable(view, direction)?.let(view::setImageDrawable)
|
createAospSystemUiSideArrowDrawable(view, direction)?.let {
|
||||||
|
view.setImageDrawable(it)
|
||||||
|
callFloatMethodIfExists(view, "setDarkIntensity", aospSystemUiDarkIntensity)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createAospSystemUiSideArrowDrawable(
|
private fun createAospSystemUiSideArrowDrawable(
|
||||||
@@ -798,17 +819,18 @@ class NavButtons : XposedModule() {
|
|||||||
Boolean::class.javaPrimitiveType,
|
Boolean::class.javaPrimitiveType,
|
||||||
Boolean::class.javaPrimitiveType,
|
Boolean::class.javaPrimitiveType,
|
||||||
)
|
)
|
||||||
|
.apply { isAccessible = true }
|
||||||
.newInstance(Color.WHITE, Color.rgb(55, 58, 62), false, false)
|
.newInstance(Color.WHITE, Color.rgb(55, 58, 62), false, false)
|
||||||
keyButtonDrawableClass
|
val drawable = keyButtonDrawableClass
|
||||||
.getDeclaredConstructor(Drawable::class.java, stateClass)
|
.getDeclaredConstructor(Drawable::class.java, stateClass)
|
||||||
|
.apply { isAccessible = true }
|
||||||
.newInstance(
|
.newInstance(
|
||||||
sideArrowBitmapDrawable(
|
sideArrowBitmapDrawable(view.context, direction, Color.WHITE),
|
||||||
view.context,
|
|
||||||
direction,
|
|
||||||
Color.WHITE,
|
|
||||||
),
|
|
||||||
state,
|
state,
|
||||||
) as? Drawable
|
) as? Drawable
|
||||||
|
drawable?.also {
|
||||||
|
callFloatMethodIfExists(it, "setDarkIntensity", aospSystemUiDarkIntensity)
|
||||||
|
}
|
||||||
} catch (error: Throwable) {
|
} catch (error: Throwable) {
|
||||||
log("AOSP SystemUI side arrow drawable failed: ${error.javaClass.simpleName}")
|
log("AOSP SystemUI side arrow drawable failed: ${error.javaClass.simpleName}")
|
||||||
null
|
null
|
||||||
@@ -1113,7 +1135,7 @@ class NavButtons : XposedModule() {
|
|||||||
) {
|
) {
|
||||||
arrow.minimumWidth = home.minimumWidth
|
arrow.minimumWidth = home.minimumWidth
|
||||||
arrow.minimumHeight = home.minimumHeight
|
arrow.minimumHeight = home.minimumHeight
|
||||||
arrow.setPadding(0, home.paddingTop, 0, home.paddingBottom)
|
arrow.setPadding(0, 0, 0, 0)
|
||||||
arrow.scaleType = ImageView.ScaleType.CENTER
|
arrow.scaleType = ImageView.ScaleType.CENTER
|
||||||
arrow.setImageDrawable(sideArrowBitmapDrawable(arrow.context, direction, Color.WHITE))
|
arrow.setImageDrawable(sideArrowBitmapDrawable(arrow.context, direction, Color.WHITE))
|
||||||
}
|
}
|
||||||
@@ -1204,8 +1226,14 @@ class NavButtons : XposedModule() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun placeAospTaskbarArrow(arrow: View, center: Point) {
|
private fun placeAospTaskbarArrow(arrow: View, center: Point) {
|
||||||
arrow.x = center.x - arrow.width / 2f
|
val width = arrow.width.takeIf { it > 0 }
|
||||||
arrow.y = center.y - arrow.height / 2f
|
?: 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) {
|
private fun removeAospSideArrows(container: ViewGroup) {
|
||||||
@@ -1901,9 +1929,23 @@ class NavButtons : XposedModule() {
|
|||||||
NavButtonAction.HOME -> sendKeyPress(KeyEvent.KEYCODE_HOME)
|
NavButtonAction.HOME -> sendKeyPress(KeyEvent.KEYCODE_HOME)
|
||||||
NavButtonAction.ASSISTANT -> sendKeyPress(KeyEvent.KEYCODE_ASSIST)
|
NavButtonAction.ASSISTANT -> sendKeyPress(KeyEvent.KEYCODE_ASSIST)
|
||||||
NavButtonAction.RECENTS -> sendKeyPress(KeyEvent.KEYCODE_APP_SWITCH)
|
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_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
|
return resolveInfo?.activityInfo?.packageName
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getTopPackageName(): String? = getTopPackageFromActivityTaskManager()
|
private fun getTopPackageName(): String? {
|
||||||
|
return getTopPackageFromActivityTaskManager()
|
||||||
|
?: getTopPackageFromRunningTasks()
|
||||||
|
}
|
||||||
|
|
||||||
private fun getTopPackageFromActivityTaskManager(): String? {
|
private fun getTopPackageFromActivityTaskManager(): String? {
|
||||||
return try {
|
return try {
|
||||||
@@ -2862,6 +2907,21 @@ class NavButtons : XposedModule() {
|
|||||||
return pkg ?: getPackageNameFromActivityInfo(getFieldValue(taskInfo, "topActivityInfo"))
|
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? {
|
private fun getFieldValue(target: Any?, fieldName: String): Any? {
|
||||||
if (target == null) {
|
if (target == null) {
|
||||||
return null
|
return null
|
||||||
@@ -3519,6 +3579,10 @@ class NavButtons : XposedModule() {
|
|||||||
"com.android.systemui.navigationbar.views.NavigationBarInflaterView",
|
"com.android.systemui.navigationbar.views.NavigationBarInflaterView",
|
||||||
"com.android.systemui.navigationbar.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(
|
val AOSP_KEY_BUTTON_DRAWABLE_CLASSES = listOf(
|
||||||
"com.android.systemui.navigationbar.views.buttons.KeyButtonDrawable",
|
"com.android.systemui.navigationbar.views.buttons.KeyButtonDrawable",
|
||||||
"com.android.systemui.navigationbar.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.views.buttons.KeyButtonDrawable\$ShadowDrawableState",
|
||||||
"com.android.systemui.navigationbar.buttons.KeyButtonDrawable\$ShadowDrawableState",
|
"com.android.systemui.navigationbar.buttons.KeyButtonDrawable\$ShadowDrawableState",
|
||||||
)
|
)
|
||||||
const val AOSP_SYSUI_LEFT_ARROW_BUTTON = "key(21)"
|
const val AOSP_SYSUI_LEFT_ARROW_BUTTON = "key(21:)"
|
||||||
const val AOSP_SYSUI_RIGHT_ARROW_BUTTON = "key(22)"
|
const val AOSP_SYSUI_RIGHT_ARROW_BUTTON = "key(22:)"
|
||||||
const val ACTION_PRIVILEGED_ACTION =
|
const val ACTION_PRIVILEGED_ACTION =
|
||||||
"se.ajpanton.navbuttons.action.PRIVILEGED_ACTION"
|
"se.ajpanton.navbuttons.action.PRIVILEGED_ACTION"
|
||||||
const val EXTRA_PRIVILEGED_ACTION = "privileged_action"
|
const val EXTRA_PRIVILEGED_ACTION = "privileged_action"
|
||||||
|
|||||||
Reference in New Issue
Block a user