diff --git a/app/src/main/java/se/ajpanton/navbuttons/NavButtons.kt b/app/src/main/java/se/ajpanton/navbuttons/NavButtons.kt index 24d2e2b..534efb0 100644 --- a/app/src/main/java/se/ajpanton/navbuttons/NavButtons.kt +++ b/app/src/main/java/se/ajpanton/navbuttons/NavButtons.kt @@ -74,6 +74,12 @@ class NavButtons : XposedModule() { private val aospTaskbarLongPressRunnables = WeakHashMap() private val aospTaskbarLongPressTriggered = WeakHashMap() private val aospTaskbarBackLongPressTriggered = WeakHashMap() + private val trackedAospSystemUiInflaters = Collections.newSetFromMap(WeakHashMap()) + private val aospSystemUiSideArrowDirections = WeakHashMap() + private val trackedAospTaskbarContainers = Collections.newSetFromMap(WeakHashMap()) + private val aospTaskbarButtons = WeakHashMap>() + private val aospTaskbarSideArrows = WeakHashMap() + private val aospTaskbarSideButtonTranslations = WeakHashMap() private val honeyspaceLongPressTriggered = WeakHashMap() private val honeyspaceHomeStockLongPressGuards = WeakHashMap() private val honeyspaceHomeStockPressCanceled = WeakHashMap() @@ -86,6 +92,8 @@ class NavButtons : XposedModule() { private val sideArrowLayoutKinds = WeakHashMap() 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::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(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>? { + 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>): 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, + 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 { + val result = mutableListOf() + 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