Files
navbuttons/app/src/main/java/se/ajpanton/navbuttons/SamsungHoneyspaceHooks.kt
T

434 lines
17 KiB
Kotlin

package se.ajpanton.navbuttons
import android.content.Context
import android.os.Handler
import android.os.SystemClock
import android.view.KeyEvent
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import io.github.libxposed.api.XposedInterface
import java.lang.reflect.Method
import java.util.Collections
import java.util.WeakHashMap
internal class SamsungHoneyspaceHooks(
private val hookMethod: (Method, XposedInterface.Hooker) -> Unit,
private val deoptimizeMethod: (Method?) -> Unit,
private val prepareLongPressHook: (Method) -> Unit,
private val configProvider: (NavButtonId) -> NavButtonConfig,
private val sideArrowsEnabled: () -> Boolean,
private val sideArrowLongPresses: SideArrowLongPressController,
private val registerSettingsReceiver: () -> Unit,
private val contextProvider: () -> Context?,
private val contextUpdater: (Context) -> Unit,
private val handlerProvider: () -> Handler?,
private val performAction: (NavButtonAction) -> Unit,
private val sendKeyPress: (Int) -> Unit,
private val log: (String) -> Unit,
) {
private val longPressTriggered = WeakHashMap<Any, Boolean>()
private val trackedNavContainers = Collections.newSetFromMap(WeakHashMap<Any, Boolean>())
private val nativeSideArrowStates = WeakHashMap<Any, Boolean>()
private val homeStockLongPressGuard = HomeStockLongPressGuard(
configProvider = configProvider,
longPressTriggered = { longPressTriggered[it] == true },
sendCanceledKeyUp = ::sendCanceledKeyUp,
)
fun install(classLoader: ClassLoader?): Boolean {
val navButtonClass = findClassIfExists(HONEYSPACE_NAV_BUTTON_VIEW, classLoader) ?: run {
log("Launcher class not found: $HONEYSPACE_NAV_BUTTON_VIEW")
return false
}
val onTouchEventMethod = findMethodIfExists(
navButtonClass,
"onTouchEvent",
MotionEvent::class.java,
) ?: run {
log("Launcher onTouchEvent() method not found.")
return false
}
val runnableClass = findClassIfExists(HONEYSPACE_LONG_PRESS_RUNNABLE, classLoader) ?: run {
log("Launcher long-press runnable not found: $HONEYSPACE_LONG_PRESS_RUNNABLE")
return false
}
val runMethod = findMethodIfExists(runnableClass, "run") ?: run {
log("Launcher run() method not found.")
return false
}
deoptimizeMethod(onTouchEventMethod)
prepareLongPressHook(runMethod)
hookSideArrowLayout(classLoader)
hookMethod(onTouchEventMethod) { chain ->
val navButtonView = chain.thisObject ?: return@hookMethod chain.proceed()
val motionEvent = chain.args[0] as? MotionEvent ?: return@hookMethod chain.proceed()
val keyCode = callMethod(navButtonView, "getKeyCode") as? Int ?: Int.MIN_VALUE
if (isSideArrowKeyCode(keyCode)) {
updateContextFromView(navButtonView)
registerSettingsReceiver()
trackNavContainerFrom(navButtonView as? View)
return@hookMethod handleNativeSideArrowTouch(navButtonView, keyCode, motionEvent) {
chain.proceed()
}
}
val buttonId = buttonIdForKeyCode(keyCode) ?: return@hookMethod chain.proceed()
updateContextFromView(navButtonView)
registerSettingsReceiver()
trackNavContainerFrom(navButtonView as? View)
when (motionEvent.actionMasked) {
MotionEvent.ACTION_DOWN -> {
longPressTriggered[navButtonView] = false
homeStockLongPressGuard.cancel(navButtonView)
homeStockLongPressGuard.clear(navButtonView)
val result = chain.proceed()
rescheduleHoneyspaceLongPress(
navButtonView,
runnableClass,
configProvider(buttonId).longPressDurationMs,
)
homeStockLongPressGuard.schedule(navButtonView, buttonId)
result
}
MotionEvent.ACTION_UP -> {
val triggered = longPressTriggered[navButtonView] == true
if (triggered) {
return@hookMethod proceedWithCanceledAction(motionEvent) {
chain.proceed()
}.also {
homeStockLongPressGuard.cancel(navButtonView)
longPressTriggered.remove(navButtonView)
homeStockLongPressGuard.clear(navButtonView)
}
}
val pressAction = configProvider(buttonId).pressAction
val stockHomePressCanceled =
buttonId == NavButtonId.HOME && homeStockLongPressGuard.isCanceled(navButtonView)
val shouldOverridePress =
pressAction != NavButtonAction.STOCK &&
((navButtonView as? View)?.isPressed == true || stockHomePressCanceled) &&
!triggered
if (shouldOverridePress) {
markLongClicked(navButtonView)
}
val result = chain.proceed()
if (stockHomePressCanceled && pressAction == NavButtonAction.STOCK) {
dispatchHomeTap(navButtonView)
}
if (shouldOverridePress) {
updateContextFromView(navButtonView)
performAction(pressAction)
}
homeStockLongPressGuard.cancel(navButtonView)
longPressTriggered.remove(navButtonView)
homeStockLongPressGuard.clear(navButtonView)
result
}
MotionEvent.ACTION_CANCEL -> {
homeStockLongPressGuard.cancel(navButtonView)
longPressTriggered.remove(navButtonView)
homeStockLongPressGuard.clear(navButtonView)
chain.proceed()
}
else -> chain.proceed()
}
}
hookMethod(runMethod) { chain ->
val caseId = getIntField(chain.thisObject, "b")
if (caseId != HONEYSPACE_LONG_PRESS_CASE) {
return@hookMethod chain.proceed()
}
val target = getFieldValue(chain.thisObject, "c")
if (!navButtonClass.isInstance(target)) {
return@hookMethod chain.proceed()
}
val navButtonView = target ?: return@hookMethod chain.proceed()
val keyCode = callMethod(navButtonView, "getKeyCode") as Int
if (isSideArrowKeyCode(keyCode)) {
return@hookMethod null
}
val buttonId = buttonIdForKeyCode(keyCode) ?: return@hookMethod chain.proceed()
val longPressAction = configProvider(buttonId).longPressAction
longPressTriggered[navButtonView] = true
if (longPressAction == NavButtonAction.STOCK) {
return@hookMethod chain.proceed()
}
updateContextFromView(navButtonView)
markLongClicked(navButtonView)
homeStockLongPressGuard.cancel(navButtonView)
performAction(longPressAction)
null
}
return true
}
fun refreshSideArrowLayouts(reason: String) {
val containers = trackedNavContainers.toList()
containers.forEach { container ->
refreshSideArrowLayout(container as? ViewGroup ?: return@forEach, reason)
scheduleDelayedSideArrowRefresh(container, reason)
}
}
private fun hookSideArrowLayout(classLoader: ClassLoader?) {
val containerClass = findClassIfExists(HONEYSPACE_NAV_BUTTON_CONTAINER, classLoader) ?: run {
log("Honeyspace side arrows container class not found: $HONEYSPACE_NAV_BUTTON_CONTAINER")
return
}
listOf(
"onAttachedToWindow",
"onLayout",
"onVisibilityChanged",
"onWindowVisibilityChanged",
"drawableStateChanged",
).forEach { methodName ->
findDeclaredMethodsNamed(containerClass, methodName).forEach { method ->
deoptimizeMethod(method)
hookMethod(method) { chain ->
val result = chain.proceed()
val container = chain.thisObject as? ViewGroup
?: return@hookMethod result
updateContextFromView(container)
registerSettingsReceiver()
trackNavContainer(container)
refreshSideArrowLayout(container, "after ${method.name}")
scheduleDelayedSideArrowRefresh(container, "after ${method.name}")
result
}
}
}
hookNativeSideArrowRefresh(containerClass)
}
private fun hookNativeSideArrowRefresh(containerClass: Class<*>) {
listOf("g", "e", "d").forEach { methodName ->
findMethodsNamed(containerClass, methodName).forEach { method ->
val parameterTypes = method.parameterTypes
val supported =
(methodName == "g" && parameterTypes.size == 1 && parameterTypes[0] == Boolean::class.javaPrimitiveType) ||
(methodName == "e" && parameterTypes.isEmpty()) ||
(methodName == "d" && parameterTypes.size == 1 && parameterTypes[0] == Float::class.javaPrimitiveType)
if (!supported) {
return@forEach
}
deoptimizeMethod(method)
hookMethod(method) { chain ->
val container = chain.thisObject as? ViewGroup
val result = chain.proceed()
if (container != null && sideArrowsEnabled()) {
restoreHoneyspaceSideArrowDrawables(container)
}
result
}
}
}
}
private fun trackNavContainerFrom(view: View?) {
var current = view?.parent as? View
repeat(HONEYSPACE_NAV_CONTAINER_PARENT_DEPTH) {
val target = current ?: return
if (isHoneyspaceNavButtonContainer(target)) {
trackNavContainer(target)
val container = target as ViewGroup
refreshSideArrowLayout(container, "from touch")
scheduleDelayedSideArrowRefresh(container, "from touch")
return
}
current = target.parent as? View
}
}
private fun trackNavContainer(container: View) {
if (trackedNavContainers.add(container)) {
container.addOnLayoutChangeListener { view, _, _, _, _, _, _, _, _ ->
refreshSideArrowLayout(view as ViewGroup, "layout listener")
}
}
}
private fun scheduleDelayedSideArrowRefresh(container: ViewGroup, reason: String) {
listOf(
HONEYSPACE_SIDE_ARROW_SHORT_REFRESH_DELAY_MS,
HONEYSPACE_SIDE_ARROW_LONG_REFRESH_DELAY_MS,
).forEach { delayMs ->
handlerProvider()?.postDelayed(
{ refreshSideArrowLayout(container, "$reason delayed ${delayMs}ms") },
delayMs,
)
}
}
private fun refreshSideArrowLayout(container: ViewGroup, reason: String) {
try {
applyNativeSideArrows(container, reason)
} catch (error: Throwable) {
log("Honeyspace side arrows refresh failed: ${error.javaClass.simpleName}")
}
}
@Suppress("UNCHECKED_CAST")
private fun applyNativeSideArrows(container: ViewGroup, reason: String) {
val buttonMap = getFieldValue(container, "i") as? MutableMap<Any, Any> ?: run {
log("Honeyspace side arrows native apply skipped: button map missing reason=$reason")
return
}
val enabled = sideArrowsEnabled()
val targetSlots = honeyspaceSideArrowSlots(container, enabled)
val currentSlots = getFieldValue(container, "u") as? BooleanArray
val alreadyApplied = nativeSideArrowStates[container] == enabled &&
if (enabled) {
buttonMap[HONEYSPACE_LEFT_ARROW_BUTTON_TYPE] != null &&
buttonMap[HONEYSPACE_RIGHT_ARROW_BUTTON_TYPE] != null &&
getIntField(container, "v") == 2 &&
currentSlots?.contentEquals(targetSlots) == true &&
honeyspaceSideArrowDrawablesPresent(buttonMap)
} else {
getIntField(container, "v") == 0 &&
currentSlots?.contentEquals(targetSlots) == true
}
if (alreadyApplied) {
return
}
HONEYSPACE_EXTRA_BUTTON_TYPES.forEach { buttonMap.remove(it) }
if (enabled) {
val created = HONEYSPACE_SIDE_ARROW_BUTTONS.all { arrow ->
createHoneyspaceExtraButton(container, arrow.type, arrow.keyCode, arrow.direction)
?.also { buttonMap[arrow.type] = it } != null
}
if (!created) {
log("Honeyspace side arrows native apply skipped: extra button creation failed reason=$reason")
return
}
setFieldIfExists(container, "u", targetSlots)
setFieldIfExists(container, "v", 2)
} else {
setFieldIfExists(container, "u", targetSlots)
setFieldIfExists(container, "v", 0)
}
nativeSideArrowStates[container] = enabled
callBooleanMethodIfExists(container, "g", false)
restoreHoneyspaceSideArrowDrawables(container)
}
private fun handleNativeSideArrowTouch(
view: Any,
keyCode: Int,
event: MotionEvent,
proceed: () -> Any?,
): Any? {
return when (event.actionMasked) {
MotionEvent.ACTION_DOWN -> {
sideArrowLongPresses.startAfter(view, keyCode, proceed)
}
MotionEvent.ACTION_UP,
MotionEvent.ACTION_CANCEL,
-> {
sideArrowLongPresses.finish(view)
proceed()
}
else -> proceed()
}
}
private fun markLongClicked(navButtonView: Any) {
try {
callMethod(
navButtonView,
"setLongClicked",
arrayOf(Boolean::class.javaPrimitiveType!!),
true,
)
} catch (error: Throwable) {
log("setLongClicked failed: ${error.javaClass.simpleName}")
}
}
private fun updateContextFromView(value: Any) {
val currentContext = (value as? View)?.context ?: return
contextUpdater(currentContext.applicationContext ?: currentContext)
}
private fun sendCanceledKeyUp(navButtonView: Any) {
if (callIntIntMethodIfExists(
navButtonView,
listOf("sendEvent", "sendKeyEvent"),
KeyEvent.ACTION_UP,
KeyEvent.FLAG_CANCELED or KeyEvent.FLAG_CANCELED_LONG_PRESS,
)
) {
return
}
try {
val inputManager = inputManagerForInjection(contextProvider()) ?: run {
log("InputManager unavailable for Honeyspace Home cancel.")
return
}
val now = SystemClock.uptimeMillis()
injectKeyEvent(
inputManager,
KeyEvent(
now,
now,
KeyEvent.ACTION_UP,
KeyEvent.KEYCODE_HOME,
0,
0,
KeyEvent.KEYCODE_UNKNOWN,
0,
KeyEvent.FLAG_CANCELED or KeyEvent.FLAG_CANCELED_LONG_PRESS,
),
)
} catch (error: Throwable) {
log("Honeyspace Home cancel failed: ${error.javaClass.simpleName}")
}
}
private fun dispatchHomeTap(navButtonView: Any) {
if (callIntIntMethodIfExists(navButtonView, listOf("sendEvent", "sendKeyEvent"), KeyEvent.ACTION_DOWN, 0) &&
callIntIntMethodIfExists(navButtonView, listOf("sendEvent", "sendKeyEvent"), KeyEvent.ACTION_UP, 0)
) {
return
}
sendKeyPress(KeyEvent.KEYCODE_HOME)
}
private inline fun proceedWithCanceledAction(
motionEvent: MotionEvent,
proceed: () -> Any?,
): Any? {
val originalAction = motionEvent.action
motionEvent.action = MotionEvent.ACTION_CANCEL
return try {
proceed()
} finally {
motionEvent.action = originalAction
}
}
}