Add AOSP side arrow support

This commit is contained in:
ajp_anton
2026-06-22 18:26:12 +00:00
parent 1fc63aef8d
commit 465dec356d
@@ -74,6 +74,12 @@ class NavButtons : XposedModule() {
private val aospTaskbarLongPressRunnables = WeakHashMap<Any, Runnable>()
private val aospTaskbarLongPressTriggered = WeakHashMap<Any, Boolean>()
private val aospTaskbarBackLongPressTriggered = WeakHashMap<Any, Boolean>()
private val trackedAospSystemUiInflaters = Collections.newSetFromMap(WeakHashMap<Any, Boolean>())
private val aospSystemUiSideArrowDirections = WeakHashMap<Any, SideArrowDirection>()
private val trackedAospTaskbarContainers = Collections.newSetFromMap(WeakHashMap<ViewGroup, Boolean>())
private val aospTaskbarButtons = WeakHashMap<ViewGroup, MutableMap<NavButtonId, View>>()
private val aospTaskbarSideArrows = WeakHashMap<ViewGroup, AospSideArrowViews>()
private val aospTaskbarSideButtonTranslations = WeakHashMap<View, Float>()
private val honeyspaceLongPressTriggered = WeakHashMap<Any, Boolean>()
private val honeyspaceHomeStockLongPressGuards = WeakHashMap<Any, Runnable>()
private val honeyspaceHomeStockPressCanceled = WeakHashMap<Any, Boolean>()
@@ -86,6 +92,8 @@ class NavButtons : XposedModule() {
private val sideArrowLayoutKinds = WeakHashMap<Any, SideArrowLayoutKind>()
private var foldedSideArrowProviderHookInstalled = false
private var samsungNavigationBarViewHookInstalled = false
private var aospSystemUiSideArrowHookInstalled = false
private var aospTaskbarTintHookInstalled = false
private var settingsChangedReceiverRegistered = false
private var privilegedActionReceiverRegistered = false
private var privilegedActionToken: String? = null
@@ -197,6 +205,7 @@ class NavButtons : XposedModule() {
sendEventMethods.forEach(::deoptimizeMethodIfPresent)
sendEventMethods.forEach(::hookSystemUiSendEvent)
hookFoldedSideArrows(classLoader)
hookAospSystemUiSideArrows(classLoader, keyButtonClass)
hook(onTouchEventMethod).intercept { chain ->
val keyButtonView = chain.thisObject ?: return@intercept chain.proceed()
@@ -431,6 +440,277 @@ class NavButtons : XposedModule() {
return installed
}
private fun hookAospSystemUiSideArrows(classLoader: ClassLoader?, keyButtonClass: Class<*>) {
if (aospSystemUiSideArrowHookInstalled || Build.MANUFACTURER.equals("samsung", ignoreCase = true)) {
return
}
val inflaterClass = findClassIfExists(AOSP_NAVIGATION_BAR_INFLATER_VIEW, classLoader) ?: return
aospSystemUiSideArrowHookInstalled = true
registerSettingsChangedReceiver()
findMethodIfExists(inflaterClass, "getDefaultLayout")?.let { method ->
deoptimizeMethodIfPresent(method)
hook(method).intercept { chain ->
chain.thisObject?.let(::trackAospSystemUiInflater)
val layout = chain.proceed() as? String ?: return@intercept null
val result = if (isSideArrowsEnabled()) {
addAospSystemUiSideArrows(layout)
} else {
removeAospSystemUiSideArrows(layout)
}
result
}
}
findMethodIfExists(inflaterClass, "inflateLayout", String::class.java)?.let { method ->
deoptimizeMethodIfPresent(method)
hook(method).intercept { chain ->
chain.thisObject?.let(::trackAospSystemUiInflater)
val layout = chain.args.getOrNull(0) as? String
if (layout != null) {
chain.args[0] = if (isSideArrowsEnabled()) {
addAospSystemUiSideArrows(layout)
} else {
removeAospSystemUiSideArrows(layout)
}
}
val result = chain.proceed()
result
}
}
findMethodIfExists(
inflaterClass,
"inflateButtons",
Array<String>::class.java,
ViewGroup::class.java,
Boolean::class.javaPrimitiveType!!,
Boolean::class.javaPrimitiveType!!,
)?.let { method ->
deoptimizeMethodIfPresent(method)
hook(method).intercept { chain ->
val tokens = (chain.args.getOrNull(0) as? Array<*>)
?.mapNotNull { it as? String }
?: return@intercept chain.proceed()
val parent = chain.args.getOrNull(1) as? ViewGroup
?: return@intercept chain.proceed()
val childCountBefore = parent.childCount
val result = chain.proceed()
trackInflatedAospSystemUiSideArrows(tokens, parent, childCountBefore)
result
}
}
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) {
trackedAospSystemUiInflaters.add(inflater)
(inflater as? View)?.let {
ensureContextFromView(it)
registerSettingsChangedReceiver()
}
}
private fun refreshTrackedAospSystemUiSideArrowLayouts() {
trackedAospSystemUiInflaters.toList().forEach { inflater ->
refreshAospSystemUiSideArrowLayout(inflater)
}
}
private fun refreshAospSystemUiSideArrowLayout(inflater: Any) {
try {
clearAospSystemUiInflaterViews(inflater)
callMethod(inflater, "inflateLayout", arrayOf(String::class.java), null)
} catch (error: Throwable) {
log("AOSP SystemUI side arrows refresh failed: ${error.javaClass.simpleName}")
}
}
private fun clearAospSystemUiInflaterViews(inflater: Any) {
callMethodIfExists(inflater, "clearDispatcherViews")
val view = inflater as? View ?: return
val ids = listOf("ends_group", "center_group").mapNotNull { name ->
view.resources.getIdentifier(name, "id", view.context.packageName).takeIf { it != 0 }
}
listOf("mHorizontal", "mVertical").forEach { fieldName ->
val root = getFieldValue(inflater, fieldName) as? ViewGroup ?: return@forEach
ids.forEach { id ->
(root.findViewById<View>(id) as? ViewGroup)?.removeAllViews()
}
}
}
private fun addAospSystemUiSideArrows(layout: String): String {
val parts = splitAospSystemUiLayout(layout) ?: return layout
val withoutArrows = parts.map { group ->
group.filterNot(::isAospSystemUiSideArrowToken).toMutableList()
}
val center = withoutArrows[1]
val homeIndex = center.indexOfFirst { layoutButtonName(it) == "home" }
if (homeIndex < 0) {
return layout
}
if (center.any { layoutButtonName(it) == "back" } &&
center.any { layoutButtonName(it) == "recent" }
) {
center.add(homeIndex, AOSP_SYSUI_LEFT_ARROW_BUTTON)
center.add(homeIndex + 2, AOSP_SYSUI_RIGHT_ARROW_BUTTON)
} else {
val leftButton = withoutArrows[0].firstOrNull(::isAospSystemUiPrimaryButton)
?: return layout
val rightButton = withoutArrows[2].firstOrNull(::isAospSystemUiPrimaryButton)
?: return layout
val homeButton = center[homeIndex]
withoutArrows[0].removeAll(::isAospSystemUiPrimaryButton)
withoutArrows[2].removeAll(::isAospSystemUiPrimaryButton)
center.clear()
center.addAll(
listOf(
layoutButtonName(leftButton),
AOSP_SYSUI_LEFT_ARROW_BUTTON,
layoutButtonName(homeButton),
AOSP_SYSUI_RIGHT_ARROW_BUTTON,
layoutButtonName(rightButton),
),
)
}
return joinAospSystemUiLayout(withoutArrows)
}
private fun removeAospSystemUiSideArrows(layout: String): String {
val parts = splitAospSystemUiLayout(layout) ?: return layout
return joinAospSystemUiLayout(
parts.map { group -> group.filterNot(::isAospSystemUiSideArrowToken) },
)
}
private fun splitAospSystemUiLayout(layout: String): List<List<String>>? {
val parts = layout.split(";", limit = 3)
if (parts.size != 3) {
return null
}
return parts.map { group -> group.split(",").filter { it.isNotBlank() } }
}
private fun joinAospSystemUiLayout(parts: List<List<String>>): String {
return parts.joinToString(";") { group -> group.joinToString(",") }
}
private fun isAospSystemUiSideArrowToken(token: String): Boolean {
val button = layoutButtonName(token)
return button == AOSP_SYSUI_LEFT_ARROW_BUTTON || button == AOSP_SYSUI_RIGHT_ARROW_BUTTON
}
private fun isAospSystemUiPrimaryButton(token: String): Boolean {
return when (layoutButtonName(token)) {
"back",
"recent",
-> true
else -> false
}
}
private fun layoutButtonName(token: String): String {
return token.substringBefore("[")
}
private fun trackInflatedAospSystemUiSideArrows(
tokens: List<String>,
parent: ViewGroup,
childCountBefore: Int,
) {
val directions = tokens.mapNotNull { token ->
when (layoutButtonName(token)) {
AOSP_SYSUI_LEFT_ARROW_BUTTON -> SideArrowDirection.LEFT
AOSP_SYSUI_RIGHT_ARROW_BUTTON -> SideArrowDirection.RIGHT
else -> null
}
}
if (directions.isEmpty()) {
return
}
val buttons = (childCountBefore until parent.childCount)
.flatMap { index -> findAospSystemUiKeyButtons(parent.getChildAt(index)) }
.toMutableList()
directions.forEach { direction ->
val keyCode = when (direction) {
SideArrowDirection.LEFT -> KeyEvent.KEYCODE_DPAD_LEFT
SideArrowDirection.RIGHT -> KeyEvent.KEYCODE_DPAD_RIGHT
}
val button = buttons.firstOrNull { getIntField(it, "mCode") == keyCode } ?: return@forEach
buttons.remove(button)
aospSystemUiSideArrowDirections[button] = direction
configureAospSystemUiSideArrowButton(button)
}
}
private fun findAospSystemUiKeyButtons(view: View): List<ImageView> {
val result = mutableListOf<ImageView>()
if (view is ImageView && getFieldValue(view, "mCode") != null) {
result.add(view)
}
if (view is ViewGroup) {
repeat(view.childCount) { index ->
result.addAll(findAospSystemUiKeyButtons(view.getChildAt(index)))
}
}
return result
}
private fun configureAospSystemUiSideArrowButton(value: Any?) {
val view = value as? ImageView ?: return
val direction = aospSystemUiSideArrowDirections[view] ?: return
ensureContextFromView(view)
createAospSystemUiSideArrowDrawable(view, direction)?.let(view::setImageDrawable)
}
private fun createAospSystemUiSideArrowDrawable(
value: Any?,
direction: SideArrowDirection,
): Drawable? {
val view = value as? View ?: return null
val classLoader = view.javaClass.classLoader ?: return null
return try {
val keyButtonDrawableClass = findClass(AOSP_KEY_BUTTON_DRAWABLE, classLoader)
val stateClass = findClass(AOSP_KEY_BUTTON_DRAWABLE_STATE, classLoader)
val state = stateClass
.getDeclaredConstructor(
Int::class.javaPrimitiveType,
Int::class.javaPrimitiveType,
Boolean::class.javaPrimitiveType,
Boolean::class.javaPrimitiveType,
)
.newInstance(Color.WHITE, Color.rgb(55, 58, 62), false, false)
keyButtonDrawableClass
.getDeclaredConstructor(Drawable::class.java, stateClass)
.newInstance(
sideArrowBitmapDrawable(
view.context,
direction,
Color.WHITE,
),
state,
) as? Drawable
} catch (error: Throwable) {
log("AOSP SystemUI side arrow drawable failed: ${error.javaClass.simpleName}")
null
}
}
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")
@@ -473,6 +753,8 @@ class NavButtons : XposedModule() {
deoptimizeMethodIfPresent(sendBackKeyEventMethod)
hookAospTaskbarButtonMapping(navButtonsViewControllerClass, controllerClass)
hookAospTaskbarTouchEvents()
hookAospTaskbarTintUpdates()
registerSettingsChangedReceiver()
hook(clickMethod).intercept { chain ->
val buttonId = buttonIdForAospTaskbarCode(chain.args[0] as? Int)
@@ -582,6 +864,7 @@ class NavButtons : XposedModule() {
view,
chain.args.getOrNull(1) as? Int,
chain.args.getOrNull(3),
chain.args.getOrNull(2) as? ViewGroup,
)
result
}
@@ -607,12 +890,281 @@ class NavButtons : XposedModule() {
}
}
private fun trackAospTaskbarButton(view: View, buttonCode: Int?, controller: Any?) {
private fun hookAospTaskbarTintUpdates() {
if (aospTaskbarTintHookInstalled) {
return
}
aospTaskbarTintHookInstalled = true
findDeclaredMethodsNamed(ImageView::class.java, "setImageTintList").forEach { method ->
deoptimizeMethodIfPresent(method)
hook(method).intercept { chain ->
val result = chain.proceed()
refreshAospTaskbarSideArrowTint(chain.thisObject as? ImageView)
result
}
}
findDeclaredMethodsNamed(ImageView::class.java, "setColorFilter").forEach { method ->
deoptimizeMethodIfPresent(method)
hook(method).intercept { chain ->
val result = chain.proceed()
refreshAospTaskbarSideArrowTint(chain.thisObject as? ImageView)
result
}
}
}
private fun trackAospTaskbarButton(
view: View,
buttonCode: Int?,
controller: Any?,
parent: ViewGroup?,
) {
val buttonId = buttonIdForAospTaskbarCode(buttonCode) ?: return
aospTaskbarButtonIds[view] = buttonId
if (controller != null) {
aospTaskbarControllers[view] = controller
}
ensureContextFromView(view)
registerSettingsChangedReceiver()
val container = (view.parent as? ViewGroup) ?: parent ?: return
aospTaskbarButtons.getOrPut(container) { mutableMapOf() }[buttonId] = view
trackAospTaskbarContainer(container)
refreshAospTaskbarSideArrows(container)
}
private fun trackAospTaskbarContainer(container: ViewGroup) {
if (trackedAospTaskbarContainers.add(container)) {
container.addOnLayoutChangeListener { view, _, _, _, _, _, _, _, _ ->
refreshAospTaskbarSideArrows(view as ViewGroup)
}
}
}
private fun refreshTrackedAospTaskbarSideArrows() {
trackedAospTaskbarContainers.toList().forEach { container ->
refreshAospTaskbarSideArrows(container)
}
}
private fun refreshAospTaskbarSideArrows(container: ViewGroup) {
try {
val buttons = aospTaskbarButtons[container].orEmpty()
val home = buttons[NavButtonId.HOME] as? ImageView ?: return
if (home.parent != container) {
return
}
if (!isSideArrowsEnabled()) {
removeAospSideArrows(container)
resetAospTaskbarSideButtonTranslations(container)
return
}
val arrows = aospTaskbarSideArrows.getOrPut(container) {
AospSideArrowViews(
createAospSideArrowView(container, home, SideArrowDirection.LEFT),
createAospSideArrowView(container, home, SideArrowDirection.RIGHT),
)
}
configureAospSideArrowView(arrows.left, home, SideArrowDirection.LEFT)
configureAospSideArrowView(arrows.right, home, SideArrowDirection.RIGHT)
placeAospSideArrows(container, home, arrows)
applyAospSideArrowTint(arrows, home)
} catch (error: Throwable) {
log("AOSP side arrows refresh failed: ${error.javaClass.simpleName}")
}
}
private fun createAospSideArrowView(
container: ViewGroup,
home: ImageView,
direction: SideArrowDirection,
): ImageView {
return ImageView(container.context).apply {
id = View.generateViewId()
isClickable = true
isFocusable = true
contentDescription = direction.name.lowercase()
setOnTouchListener { view, event ->
handleAospSideArrowTouch(
view,
if (direction == SideArrowDirection.LEFT) {
KeyEvent.KEYCODE_DPAD_LEFT
} else {
KeyEvent.KEYCODE_DPAD_RIGHT
},
event,
)
}
configureAospSideArrowView(this, home, direction)
}
}
private fun configureAospSideArrowView(
arrow: ImageView,
home: ImageView,
direction: SideArrowDirection,
) {
arrow.minimumWidth = home.minimumWidth
arrow.minimumHeight = home.minimumHeight
arrow.setPadding(0, home.paddingTop, 0, home.paddingBottom)
arrow.scaleType = ImageView.ScaleType.CENTER
arrow.setImageDrawable(sideArrowBitmapDrawable(arrow.context, direction, Color.WHITE))
}
private fun placeAospSideArrows(
container: ViewGroup,
home: ImageView,
arrows: AospSideArrowViews,
) {
val host = findAospTaskbarArrowHost(container) ?: return
val buttons = aospTaskbarButtons[container].orEmpty()
val sideButtons = listOfNotNull(buttons[NavButtonId.BACK], buttons[NavButtonId.RECENTS])
.filter { it.width > 0 && it.height > 0 }
if (home.width <= 0 || home.height <= 0 || sideButtons.size < 2) {
return
}
addAospTaskbarArrowToHost(host, arrows.left, home)
addAospTaskbarArrowToHost(host, arrows.right, home)
val homeCenter = centerInHost(home, host)
val leftButton = sideButtons.filter { centerInHost(it, host).x < homeCenter.x }
.maxByOrNull { centerInHost(it, host).x }
?: return
val rightButton = sideButtons.filter { centerInHost(it, host).x > homeCenter.x }
.minByOrNull { centerInHost(it, host).x }
?: return
translateAospTaskbarSideButton(leftButton, -aospTaskbarSideButtonShift(arrows.left))
translateAospTaskbarSideButton(rightButton, aospTaskbarSideButtonShift(arrows.right))
placeAospTaskbarArrow(arrows.left, midpoint(centerInHost(leftButton, host), homeCenter))
placeAospTaskbarArrow(arrows.right, midpoint(centerInHost(rightButton, host), homeCenter))
}
private fun translateAospTaskbarSideButton(button: View, shiftPx: Float) {
val originalTranslation = aospTaskbarSideButtonTranslations.getOrPut(button) { button.translationX }
button.translationX = originalTranslation + shiftPx
}
private fun aospTaskbarSideButtonShift(arrow: View): Float {
return navStarArrowIconSizePx(arrow.context) * AOSP_TASKBAR_SIDE_BUTTON_SHIFT_RATIO
}
private fun findAospTaskbarArrowHost(start: View): android.widget.FrameLayout? {
var current = start.parent as? View
repeat(5) {
if (current is android.widget.FrameLayout) {
return current as android.widget.FrameLayout
}
current = current?.parent as? View
}
return null
}
private fun addAospTaskbarArrowToHost(
host: android.widget.FrameLayout,
arrow: ImageView,
home: ImageView,
) {
val width = navStarArrowIconSizePx(arrow.context)
val height = home.height.coerceAtLeast(width)
if (arrow.parent != host) {
removeFromParent(arrow)
host.addView(arrow, android.widget.FrameLayout.LayoutParams(width, height))
} else {
arrow.layoutParams = (arrow.layoutParams as? android.widget.FrameLayout.LayoutParams)
?.apply {
this.width = width
this.height = height
}
?: android.widget.FrameLayout.LayoutParams(width, height)
}
}
private fun centerInHost(view: View, host: View): Point {
val viewLocation = IntArray(2)
val hostLocation = IntArray(2)
view.getLocationOnScreen(viewLocation)
host.getLocationOnScreen(hostLocation)
return Point(
viewLocation[0] - hostLocation[0] + view.width / 2,
viewLocation[1] - hostLocation[1] + view.height / 2,
)
}
private fun midpoint(first: Point, second: Point): Point {
return Point((first.x + second.x) / 2, (first.y + second.y) / 2)
}
private fun placeAospTaskbarArrow(arrow: View, center: Point) {
arrow.x = center.x - arrow.width / 2f
arrow.y = center.y - arrow.height / 2f
}
private fun removeAospSideArrows(container: ViewGroup) {
aospTaskbarSideArrows[container]?.let { arrows ->
removeFromParent(arrows.left)
removeFromParent(arrows.right)
}
}
private fun resetAospTaskbarSideButtonTranslations(container: ViewGroup) {
aospTaskbarButtons[container].orEmpty().values.forEach { button ->
aospTaskbarSideButtonTranslations.remove(button)?.let { button.translationX = it }
}
}
private fun removeFromParent(view: View) {
(view.parent as? ViewGroup)?.removeView(view)
}
private fun applyAospSideArrowTint(arrows: AospSideArrowViews, source: ImageView) {
listOf(arrows.left, arrows.right).forEach { arrow ->
arrow.imageTintList = source.imageTintList
arrow.colorFilter = source.colorFilter
arrow.imageAlpha = source.imageAlpha
}
}
private fun refreshAospTaskbarSideArrowTint(source: ImageView?) {
val container = source?.parent as? ViewGroup ?: return
val buttons = aospTaskbarButtons[container] ?: return
val arrows = aospTaskbarSideArrows[container] ?: return
if (source !in buttons.values) {
return
}
val tintSource = (buttons[NavButtonId.HOME] ?: buttons.values.firstOrNull()) as? ImageView ?: return
applyAospSideArrowTint(arrows, tintSource)
}
private fun handleAospSideArrowTouch(view: View, keyCode: Int, event: MotionEvent): Boolean {
updateContextFromView(view)
when (event.actionMasked) {
MotionEvent.ACTION_DOWN -> {
view.isPressed = true
sideArrowLongPressTriggered[view] = false
cancelSideArrowLongPress(view)
scheduleSideArrowLongPress(view, keyCode)
}
MotionEvent.ACTION_UP,
MotionEvent.ACTION_CANCEL,
-> {
val triggered = sideArrowLongPressTriggered[view] == true
cancelSideArrowLongPress(view)
sideArrowLongPressTriggered.remove(view)
view.isPressed = false
if (event.actionMasked == MotionEvent.ACTION_UP && !triggered) {
view.performClick()
sendSideArrowKeyPress(keyCode)
}
}
}
return true
}
private fun handleAospTaskbarTouch(view: View, event: MotionEvent): Boolean? {
@@ -1107,12 +1659,12 @@ class NavButtons : XposedModule() {
sideArrowLongPressTriggered[navButtonView] = true
when (getSideArrowLongPressAction()) {
SideArrowLongPressAction.NOTHING -> Unit
SideArrowLongPressAction.SINGLE_STEP -> sendKeyPress(keyCode)
SideArrowLongPressAction.SINGLE_STEP -> sendSideArrowKeyPress(keyCode)
SideArrowLongPressAction.MULTIPLE_STEPS -> {
sendKeyPress(keyCode)
sendSideArrowKeyPress(keyCode)
val repeatRunnable = object : Runnable {
override fun run() {
sendKeyPress(keyCode)
sendSideArrowKeyPress(keyCode)
handler.postDelayed(this, sideArrowRepeatIntervalMs())
}
}
@@ -1125,6 +1677,14 @@ class NavButtons : XposedModule() {
view.postDelayed(longPressRunnable, getSideArrowLongPressDurationMs().toLong())
}
private fun sendSideArrowKeyPress(keyCode: Int) {
if (context?.packageName == AOSP_LAUNCHER_PACKAGE) {
requestPrivilegedAction(PRIVILEGED_ACTION_SEND_KEY_PRESS, keyCode)
} else {
sendKeyPress(keyCode)
}
}
private fun cancelSideArrowLongPress(navButtonView: Any) {
val view = navButtonView as? View
sideArrowLongPressRunnables.remove(navButtonView)?.let { runnable ->
@@ -1680,7 +2240,9 @@ class NavButtons : XposedModule() {
remotePreferences?.getBoolean(NavButtonSettingsStore.KEY_SIDE_ARROWS_ENABLED, false) == true,
)
reInflateTrackedSamsungNavigationBars()
refreshTrackedAospSystemUiSideArrowLayouts()
refreshTrackedHoneyspaceSideArrowLayouts("settings changed")
refreshTrackedAospTaskbarSideArrows()
}
}
@@ -1720,6 +2282,12 @@ class NavButtons : XposedModule() {
when (intent.getStringExtra(EXTRA_PRIVILEGED_ACTION)) {
PRIVILEGED_ACTION_KILL_FOREGROUND_APP -> killForegroundApp()
PRIVILEGED_ACTION_TOGGLE_FLASHLIGHT -> toggleFlashlight()
PRIVILEGED_ACTION_SEND_KEY_PRESS -> {
val keyCode = intent.getIntExtra(EXTRA_PRIVILEGED_KEY_CODE, KeyEvent.KEYCODE_UNKNOWN)
if (keyCode != KeyEvent.KEYCODE_UNKNOWN) {
sendKeyPress(keyCode)
}
}
}
}
}
@@ -1783,12 +2351,13 @@ class NavButtons : XposedModule() {
return remotePreferences?.getString(NavButtonSettingsStore.KEY_PRIVILEGED_ACTION_TOKEN, null)
}
private fun requestPrivilegedAction(action: String) {
private fun requestPrivilegedAction(action: String, keyCode: Int? = null) {
val currentContext = context
if (currentContext == null) {
when (action) {
PRIVILEGED_ACTION_KILL_FOREGROUND_APP -> killForegroundApp()
PRIVILEGED_ACTION_TOGGLE_FLASHLIGHT -> toggleFlashlight()
PRIVILEGED_ACTION_SEND_KEY_PRESS -> keyCode?.let(::sendKeyPress)
}
return
}
@@ -1799,6 +2368,9 @@ class NavButtons : XposedModule() {
val intent = Intent(ACTION_PRIVILEGED_ACTION)
.setPackage(ANDROID_PACKAGE)
.putExtra(EXTRA_PRIVILEGED_ACTION, action)
if (keyCode != null) {
intent.putExtra(EXTRA_PRIVILEGED_KEY_CODE, keyCode)
}
if (token != null) {
intent.putExtra(EXTRA_PRIVILEGED_ACTION_TOKEN, token)
}
@@ -2710,6 +3282,11 @@ class NavButtons : XposedModule() {
val kind: SideArrowLayoutKind,
)
private data class AospSideArrowViews(
val left: ImageView,
val right: ImageView,
)
private class SideArrowDrawable(
private val direction: SideArrowDirection,
color: Int,
@@ -2807,12 +3384,23 @@ class NavButtons : XposedModule() {
const val AOSP_TASKBAR_RECENTS_BUTTON = 4
const val AOSP_NAVBAR_BUTTONS_VIEW_CONTROLLER =
"com.android.launcher3.taskbar.NavbarButtonsViewController"
const val AOSP_TASKBAR_SIDE_BUTTON_SHIFT_RATIO = 0.45f
const val AOSP_NAVIGATION_BAR_INFLATER_VIEW =
"com.android.systemui.navigationbar.views.NavigationBarInflaterView"
const val AOSP_KEY_BUTTON_DRAWABLE =
"com.android.systemui.navigationbar.views.buttons.KeyButtonDrawable"
const val AOSP_KEY_BUTTON_DRAWABLE_STATE =
"com.android.systemui.navigationbar.views.buttons.KeyButtonDrawable\$ShadowDrawableState"
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"
const val EXTRA_PRIVILEGED_ACTION_TOKEN = "privileged_action_token"
const val EXTRA_PRIVILEGED_KEY_CODE = "privileged_key_code"
const val PRIVILEGED_ACTION_KILL_FOREGROUND_APP = "kill_foreground_app"
const val PRIVILEGED_ACTION_TOGGLE_FLASHLIGHT = "toggle_flashlight"
const val PRIVILEGED_ACTION_SEND_KEY_PRESS = "send_key_press"
const val SIDE_ARROW_ICON_DP = 32f
const val SIDE_ARROW_GLYPH_SCALE = 0.8f
const val SIDE_ARROW_STROKE_WIDTH = 1.45f