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 aospTaskbarLongPressRunnables = WeakHashMap<Any, Runnable>()
|
||||||
private val aospTaskbarLongPressTriggered = WeakHashMap<Any, Boolean>()
|
private val aospTaskbarLongPressTriggered = WeakHashMap<Any, Boolean>()
|
||||||
private val aospTaskbarBackLongPressTriggered = 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 honeyspaceLongPressTriggered = WeakHashMap<Any, Boolean>()
|
||||||
private val honeyspaceHomeStockLongPressGuards = WeakHashMap<Any, Runnable>()
|
private val honeyspaceHomeStockLongPressGuards = WeakHashMap<Any, Runnable>()
|
||||||
private val honeyspaceHomeStockPressCanceled = WeakHashMap<Any, Boolean>()
|
private val honeyspaceHomeStockPressCanceled = WeakHashMap<Any, Boolean>()
|
||||||
@@ -86,6 +92,8 @@ class NavButtons : XposedModule() {
|
|||||||
private val sideArrowLayoutKinds = WeakHashMap<Any, SideArrowLayoutKind>()
|
private val sideArrowLayoutKinds = WeakHashMap<Any, SideArrowLayoutKind>()
|
||||||
private var foldedSideArrowProviderHookInstalled = false
|
private var foldedSideArrowProviderHookInstalled = false
|
||||||
private var samsungNavigationBarViewHookInstalled = false
|
private var samsungNavigationBarViewHookInstalled = false
|
||||||
|
private var aospSystemUiSideArrowHookInstalled = false
|
||||||
|
private var aospTaskbarTintHookInstalled = false
|
||||||
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
|
||||||
@@ -197,6 +205,7 @@ class NavButtons : XposedModule() {
|
|||||||
sendEventMethods.forEach(::deoptimizeMethodIfPresent)
|
sendEventMethods.forEach(::deoptimizeMethodIfPresent)
|
||||||
sendEventMethods.forEach(::hookSystemUiSendEvent)
|
sendEventMethods.forEach(::hookSystemUiSendEvent)
|
||||||
hookFoldedSideArrows(classLoader)
|
hookFoldedSideArrows(classLoader)
|
||||||
|
hookAospSystemUiSideArrows(classLoader, keyButtonClass)
|
||||||
|
|
||||||
hook(onTouchEventMethod).intercept { chain ->
|
hook(onTouchEventMethod).intercept { chain ->
|
||||||
val keyButtonView = chain.thisObject ?: return@intercept chain.proceed()
|
val keyButtonView = chain.thisObject ?: return@intercept chain.proceed()
|
||||||
@@ -431,6 +440,277 @@ class NavButtons : XposedModule() {
|
|||||||
return installed
|
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 {
|
private fun hookAospTaskbar(classLoader: ClassLoader?): Boolean {
|
||||||
val controllerClass = findClassIfExists(AOSP_TASKBAR_NAV_BUTTON_CONTROLLER, classLoader) ?: run {
|
val controllerClass = findClassIfExists(AOSP_TASKBAR_NAV_BUTTON_CONTROLLER, classLoader) ?: run {
|
||||||
log("AOSP taskbar controller class not found: $AOSP_TASKBAR_NAV_BUTTON_CONTROLLER")
|
log("AOSP taskbar controller class not found: $AOSP_TASKBAR_NAV_BUTTON_CONTROLLER")
|
||||||
@@ -473,6 +753,8 @@ class NavButtons : XposedModule() {
|
|||||||
deoptimizeMethodIfPresent(sendBackKeyEventMethod)
|
deoptimizeMethodIfPresent(sendBackKeyEventMethod)
|
||||||
hookAospTaskbarButtonMapping(navButtonsViewControllerClass, controllerClass)
|
hookAospTaskbarButtonMapping(navButtonsViewControllerClass, controllerClass)
|
||||||
hookAospTaskbarTouchEvents()
|
hookAospTaskbarTouchEvents()
|
||||||
|
hookAospTaskbarTintUpdates()
|
||||||
|
registerSettingsChangedReceiver()
|
||||||
|
|
||||||
hook(clickMethod).intercept { chain ->
|
hook(clickMethod).intercept { chain ->
|
||||||
val buttonId = buttonIdForAospTaskbarCode(chain.args[0] as? Int)
|
val buttonId = buttonIdForAospTaskbarCode(chain.args[0] as? Int)
|
||||||
@@ -582,6 +864,7 @@ class NavButtons : XposedModule() {
|
|||||||
view,
|
view,
|
||||||
chain.args.getOrNull(1) as? Int,
|
chain.args.getOrNull(1) as? Int,
|
||||||
chain.args.getOrNull(3),
|
chain.args.getOrNull(3),
|
||||||
|
chain.args.getOrNull(2) as? ViewGroup,
|
||||||
)
|
)
|
||||||
result
|
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
|
val buttonId = buttonIdForAospTaskbarCode(buttonCode) ?: return
|
||||||
aospTaskbarButtonIds[view] = buttonId
|
aospTaskbarButtonIds[view] = buttonId
|
||||||
if (controller != null) {
|
if (controller != null) {
|
||||||
aospTaskbarControllers[view] = controller
|
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? {
|
private fun handleAospTaskbarTouch(view: View, event: MotionEvent): Boolean? {
|
||||||
@@ -1107,12 +1659,12 @@ class NavButtons : XposedModule() {
|
|||||||
sideArrowLongPressTriggered[navButtonView] = true
|
sideArrowLongPressTriggered[navButtonView] = true
|
||||||
when (getSideArrowLongPressAction()) {
|
when (getSideArrowLongPressAction()) {
|
||||||
SideArrowLongPressAction.NOTHING -> Unit
|
SideArrowLongPressAction.NOTHING -> Unit
|
||||||
SideArrowLongPressAction.SINGLE_STEP -> sendKeyPress(keyCode)
|
SideArrowLongPressAction.SINGLE_STEP -> sendSideArrowKeyPress(keyCode)
|
||||||
SideArrowLongPressAction.MULTIPLE_STEPS -> {
|
SideArrowLongPressAction.MULTIPLE_STEPS -> {
|
||||||
sendKeyPress(keyCode)
|
sendSideArrowKeyPress(keyCode)
|
||||||
val repeatRunnable = object : Runnable {
|
val repeatRunnable = object : Runnable {
|
||||||
override fun run() {
|
override fun run() {
|
||||||
sendKeyPress(keyCode)
|
sendSideArrowKeyPress(keyCode)
|
||||||
handler.postDelayed(this, sideArrowRepeatIntervalMs())
|
handler.postDelayed(this, sideArrowRepeatIntervalMs())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1125,6 +1677,14 @@ class NavButtons : XposedModule() {
|
|||||||
view.postDelayed(longPressRunnable, getSideArrowLongPressDurationMs().toLong())
|
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) {
|
private fun cancelSideArrowLongPress(navButtonView: Any) {
|
||||||
val view = navButtonView as? View
|
val view = navButtonView as? View
|
||||||
sideArrowLongPressRunnables.remove(navButtonView)?.let { runnable ->
|
sideArrowLongPressRunnables.remove(navButtonView)?.let { runnable ->
|
||||||
@@ -1680,7 +2240,9 @@ class NavButtons : XposedModule() {
|
|||||||
remotePreferences?.getBoolean(NavButtonSettingsStore.KEY_SIDE_ARROWS_ENABLED, false) == true,
|
remotePreferences?.getBoolean(NavButtonSettingsStore.KEY_SIDE_ARROWS_ENABLED, false) == true,
|
||||||
)
|
)
|
||||||
reInflateTrackedSamsungNavigationBars()
|
reInflateTrackedSamsungNavigationBars()
|
||||||
|
refreshTrackedAospSystemUiSideArrowLayouts()
|
||||||
refreshTrackedHoneyspaceSideArrowLayouts("settings changed")
|
refreshTrackedHoneyspaceSideArrowLayouts("settings changed")
|
||||||
|
refreshTrackedAospTaskbarSideArrows()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1720,6 +2282,12 @@ class NavButtons : XposedModule() {
|
|||||||
when (intent.getStringExtra(EXTRA_PRIVILEGED_ACTION)) {
|
when (intent.getStringExtra(EXTRA_PRIVILEGED_ACTION)) {
|
||||||
PRIVILEGED_ACTION_KILL_FOREGROUND_APP -> killForegroundApp()
|
PRIVILEGED_ACTION_KILL_FOREGROUND_APP -> killForegroundApp()
|
||||||
PRIVILEGED_ACTION_TOGGLE_FLASHLIGHT -> toggleFlashlight()
|
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)
|
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
|
val currentContext = context
|
||||||
if (currentContext == null) {
|
if (currentContext == null) {
|
||||||
when (action) {
|
when (action) {
|
||||||
PRIVILEGED_ACTION_KILL_FOREGROUND_APP -> killForegroundApp()
|
PRIVILEGED_ACTION_KILL_FOREGROUND_APP -> killForegroundApp()
|
||||||
PRIVILEGED_ACTION_TOGGLE_FLASHLIGHT -> toggleFlashlight()
|
PRIVILEGED_ACTION_TOGGLE_FLASHLIGHT -> toggleFlashlight()
|
||||||
|
PRIVILEGED_ACTION_SEND_KEY_PRESS -> keyCode?.let(::sendKeyPress)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -1799,6 +2368,9 @@ class NavButtons : XposedModule() {
|
|||||||
val intent = Intent(ACTION_PRIVILEGED_ACTION)
|
val intent = Intent(ACTION_PRIVILEGED_ACTION)
|
||||||
.setPackage(ANDROID_PACKAGE)
|
.setPackage(ANDROID_PACKAGE)
|
||||||
.putExtra(EXTRA_PRIVILEGED_ACTION, action)
|
.putExtra(EXTRA_PRIVILEGED_ACTION, action)
|
||||||
|
if (keyCode != null) {
|
||||||
|
intent.putExtra(EXTRA_PRIVILEGED_KEY_CODE, keyCode)
|
||||||
|
}
|
||||||
if (token != null) {
|
if (token != null) {
|
||||||
intent.putExtra(EXTRA_PRIVILEGED_ACTION_TOKEN, token)
|
intent.putExtra(EXTRA_PRIVILEGED_ACTION_TOKEN, token)
|
||||||
}
|
}
|
||||||
@@ -2710,6 +3282,11 @@ class NavButtons : XposedModule() {
|
|||||||
val kind: SideArrowLayoutKind,
|
val kind: SideArrowLayoutKind,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
private data class AospSideArrowViews(
|
||||||
|
val left: ImageView,
|
||||||
|
val right: ImageView,
|
||||||
|
)
|
||||||
|
|
||||||
private class SideArrowDrawable(
|
private class SideArrowDrawable(
|
||||||
private val direction: SideArrowDirection,
|
private val direction: SideArrowDirection,
|
||||||
color: Int,
|
color: Int,
|
||||||
@@ -2807,12 +3384,23 @@ class NavButtons : XposedModule() {
|
|||||||
const val AOSP_TASKBAR_RECENTS_BUTTON = 4
|
const val AOSP_TASKBAR_RECENTS_BUTTON = 4
|
||||||
const val AOSP_NAVBAR_BUTTONS_VIEW_CONTROLLER =
|
const val AOSP_NAVBAR_BUTTONS_VIEW_CONTROLLER =
|
||||||
"com.android.launcher3.taskbar.NavbarButtonsViewController"
|
"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 =
|
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"
|
||||||
const val EXTRA_PRIVILEGED_ACTION_TOKEN = "privileged_action_token"
|
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_KILL_FOREGROUND_APP = "kill_foreground_app"
|
||||||
const val PRIVILEGED_ACTION_TOGGLE_FLASHLIGHT = "toggle_flashlight"
|
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_ICON_DP = 32f
|
||||||
const val SIDE_ARROW_GLYPH_SCALE = 0.8f
|
const val SIDE_ARROW_GLYPH_SCALE = 0.8f
|
||||||
const val SIDE_ARROW_STROKE_WIDTH = 1.45f
|
const val SIDE_ARROW_STROKE_WIDTH = 1.45f
|
||||||
|
|||||||
Reference in New Issue
Block a user