Add AOSP side arrow support
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user