Wire nav button actions into module
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
.gradle/
|
||||
.kotlin/
|
||||
build/
|
||||
app/build/
|
||||
local.properties
|
||||
|
||||
@@ -56,8 +56,8 @@ android {
|
||||
applicationId = "se.ajpanton.navbuttons"
|
||||
minSdk = 35
|
||||
targetSdk = 35
|
||||
versionCode = 1
|
||||
versionName = "0.1"
|
||||
versionCode = 1777035207
|
||||
versionName = "0.1-1777035207"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
@@ -84,6 +84,7 @@ android {
|
||||
dependencies {
|
||||
|
||||
implementation("androidx.annotation:annotation:1.7.1")
|
||||
implementation("io.github.libxposed:service:101.0.0")
|
||||
implementation("org.lsposed.hiddenapibypass:hiddenapibypass:6.1")
|
||||
compileOnly("io.github.libxposed:api:101.0.0")
|
||||
|
||||
|
||||
@@ -14,6 +14,12 @@
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.NavButtons">
|
||||
|
||||
<provider
|
||||
android:name="io.github.libxposed.service.XposedProvider"
|
||||
android:authorities="${applicationId}.XposedService"
|
||||
android:exported="true"
|
||||
tools:ignore="ExportedContentProvider" />
|
||||
|
||||
<activity
|
||||
android:name=".SettingsActivity"
|
||||
android:exported="true"
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
package se.ajpanton.navbuttons
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.util.Log
|
||||
import android.view.ViewConfiguration
|
||||
import androidx.annotation.StringRes
|
||||
import io.github.libxposed.service.XposedService
|
||||
import io.github.libxposed.service.XposedServiceHelper
|
||||
|
||||
enum class NavButtonId(
|
||||
val preferencePrefix: String,
|
||||
@@ -36,12 +40,17 @@ data class NavButtonConfig(
|
||||
)
|
||||
|
||||
class NavButtonSettingsStore(context: Context) {
|
||||
private val preferences = context.applicationContext.getSharedPreferences(
|
||||
private val localPreferences = context.applicationContext.getSharedPreferences(
|
||||
PREFERENCES_NAME,
|
||||
Context.MODE_PRIVATE,
|
||||
)
|
||||
|
||||
init {
|
||||
registerRemotePreferencesListener(localPreferences)
|
||||
}
|
||||
|
||||
fun getConfig(buttonId: NavButtonId): NavButtonConfig {
|
||||
val preferences = currentPreferences()
|
||||
return NavButtonConfig(
|
||||
pressAction = NavButtonAction.fromStorageValue(
|
||||
preferences.getString(pressActionKey(buttonId), null),
|
||||
@@ -59,21 +68,21 @@ class NavButtonSettingsStore(context: Context) {
|
||||
}
|
||||
|
||||
fun setPressAction(buttonId: NavButtonId, action: NavButtonAction) {
|
||||
preferences.edit()
|
||||
.putString(pressActionKey(buttonId), action.storageValue)
|
||||
.apply()
|
||||
persistChanges {
|
||||
putString(pressActionKey(buttonId), action.storageValue)
|
||||
}
|
||||
}
|
||||
|
||||
fun setLongPressAction(buttonId: NavButtonId, action: NavButtonAction) {
|
||||
preferences.edit()
|
||||
.putString(longPressActionKey(buttonId), action.storageValue)
|
||||
.apply()
|
||||
persistChanges {
|
||||
putString(longPressActionKey(buttonId), action.storageValue)
|
||||
}
|
||||
}
|
||||
|
||||
fun setLongPressDurationMs(buttonId: NavButtonId, durationMs: Int) {
|
||||
preferences.edit()
|
||||
.putInt(longPressDurationKey(buttonId), sanitizeDuration(durationMs))
|
||||
.apply()
|
||||
persistChanges {
|
||||
putInt(longPressDurationKey(buttonId), sanitizeDuration(durationMs))
|
||||
}
|
||||
}
|
||||
|
||||
fun defaultLongPressDurationMs(): Int {
|
||||
@@ -84,11 +93,40 @@ class NavButtonSettingsStore(context: Context) {
|
||||
return durationMs.coerceIn(MIN_LONG_PRESS_DURATION_MS, MAX_LONG_PRESS_DURATION_MS)
|
||||
}
|
||||
|
||||
private fun currentPreferences(): SharedPreferences {
|
||||
return remotePreferences ?: localPreferences
|
||||
}
|
||||
|
||||
private fun persistChanges(write: SharedPreferences.Editor.() -> Unit) {
|
||||
val updatedAtMs = System.currentTimeMillis()
|
||||
writeToPreferences(localPreferences, updatedAtMs, write)
|
||||
val currentRemotePreferences = remotePreferences ?: return
|
||||
try {
|
||||
writeToPreferences(currentRemotePreferences, updatedAtMs, write)
|
||||
} catch (error: Throwable) {
|
||||
remotePreferences = null
|
||||
Log.w(TAG, "Remote preferences write failed", error)
|
||||
}
|
||||
}
|
||||
|
||||
private fun writeToPreferences(
|
||||
preferences: SharedPreferences,
|
||||
updatedAtMs: Long,
|
||||
write: SharedPreferences.Editor.() -> Unit,
|
||||
) {
|
||||
val editor = preferences.edit()
|
||||
editor.write()
|
||||
editor.putLong(KEY_LAST_UPDATED_AT_MS, updatedAtMs)
|
||||
editor.apply()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "NavButtons"
|
||||
const val PREFERENCES_NAME = "nav_button_settings"
|
||||
const val KEY_PRESS_ACTION = "press_action"
|
||||
const val KEY_LONG_PRESS_ACTION = "long_press_action"
|
||||
const val KEY_LONG_PRESS_DURATION_MS = "long_press_duration_ms"
|
||||
private const val KEY_LAST_UPDATED_AT_MS = "last_updated_at_ms"
|
||||
|
||||
const val MIN_LONG_PRESS_DURATION_MS = 100
|
||||
const val MAX_LONG_PRESS_DURATION_MS = 10_000
|
||||
@@ -105,8 +143,110 @@ class NavButtonSettingsStore(context: Context) {
|
||||
fun longPressDurationKey(buttonId: NavButtonId): String =
|
||||
key(buttonId, KEY_LONG_PRESS_DURATION_MS)
|
||||
|
||||
@Volatile
|
||||
private var remotePreferences: SharedPreferences? = null
|
||||
|
||||
@Volatile
|
||||
private var remoteListenerRegistered = false
|
||||
|
||||
private val actionKeys by lazy(LazyThreadSafetyMode.NONE) {
|
||||
NavButtonId.entries.flatMap { buttonId ->
|
||||
listOf(
|
||||
pressActionKey(buttonId),
|
||||
longPressActionKey(buttonId),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private val durationKeys by lazy(LazyThreadSafetyMode.NONE) {
|
||||
NavButtonId.entries.map(::longPressDurationKey)
|
||||
}
|
||||
|
||||
private fun key(buttonId: NavButtonId, suffix: String): String {
|
||||
return "${buttonId.preferencePrefix}_$suffix"
|
||||
}
|
||||
|
||||
private fun registerRemotePreferencesListener(localPreferences: SharedPreferences) {
|
||||
if (remoteListenerRegistered) {
|
||||
return
|
||||
}
|
||||
synchronized(this) {
|
||||
if (remoteListenerRegistered) {
|
||||
return
|
||||
}
|
||||
remoteListenerRegistered = true
|
||||
}
|
||||
|
||||
try {
|
||||
XposedServiceHelper.registerListener(object : XposedServiceHelper.OnServiceListener {
|
||||
override fun onServiceBind(service: XposedService) {
|
||||
val boundRemotePreferences = service.getRemotePreferences(PREFERENCES_NAME)
|
||||
syncPreferences(localPreferences, boundRemotePreferences)
|
||||
remotePreferences = boundRemotePreferences
|
||||
}
|
||||
|
||||
override fun onServiceDied(service: XposedService) {
|
||||
remotePreferences = null
|
||||
}
|
||||
})
|
||||
} catch (error: Throwable) {
|
||||
remoteListenerRegistered = false
|
||||
Log.w(TAG, "Remote preferences listener registration failed", error)
|
||||
}
|
||||
}
|
||||
|
||||
private fun syncPreferences(
|
||||
localPreferences: SharedPreferences,
|
||||
remotePreferences: SharedPreferences,
|
||||
) {
|
||||
val localUpdatedAtMs = localPreferences.getLong(KEY_LAST_UPDATED_AT_MS, 0L)
|
||||
val remoteUpdatedAtMs = remotePreferences.getLong(KEY_LAST_UPDATED_AT_MS, 0L)
|
||||
val localHasConfig = hasStoredConfig(localPreferences)
|
||||
val remoteHasConfig = hasStoredConfig(remotePreferences)
|
||||
|
||||
when {
|
||||
localUpdatedAtMs > remoteUpdatedAtMs -> copyPreferences(localPreferences, remotePreferences)
|
||||
remoteUpdatedAtMs > localUpdatedAtMs -> copyPreferences(remotePreferences, localPreferences)
|
||||
localHasConfig && !remoteHasConfig -> copyPreferences(localPreferences, remotePreferences)
|
||||
remoteHasConfig && !localHasConfig -> copyPreferences(remotePreferences, localPreferences)
|
||||
localHasConfig && remoteHasConfig -> copyPreferences(localPreferences, remotePreferences)
|
||||
}
|
||||
}
|
||||
|
||||
private fun hasStoredConfig(preferences: SharedPreferences): Boolean {
|
||||
return actionKeys.any(preferences::contains) || durationKeys.any(preferences::contains)
|
||||
}
|
||||
|
||||
private fun copyPreferences(source: SharedPreferences, target: SharedPreferences) {
|
||||
val editor = target.edit()
|
||||
|
||||
for (key in actionKeys) {
|
||||
if (source.contains(key)) {
|
||||
editor.putString(key, source.getString(key, null))
|
||||
} else {
|
||||
editor.remove(key)
|
||||
}
|
||||
}
|
||||
|
||||
for (key in durationKeys) {
|
||||
if (source.contains(key)) {
|
||||
editor.putInt(key, source.getInt(key, sanitizeDuration(ViewConfiguration.getLongPressTimeout())))
|
||||
} else {
|
||||
editor.remove(key)
|
||||
}
|
||||
}
|
||||
|
||||
if (source.contains(KEY_LAST_UPDATED_AT_MS)) {
|
||||
editor.putLong(KEY_LAST_UPDATED_AT_MS, source.getLong(KEY_LAST_UPDATED_AT_MS, 0L))
|
||||
} else {
|
||||
editor.remove(KEY_LAST_UPDATED_AT_MS)
|
||||
}
|
||||
|
||||
editor.apply()
|
||||
}
|
||||
|
||||
private fun sanitizeDuration(durationMs: Int): Int {
|
||||
return durationMs.coerceIn(MIN_LONG_PRESS_DURATION_MS, MAX_LONG_PRESS_DURATION_MS)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,26 +4,47 @@ import android.app.ActivityManager
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.os.Process
|
||||
import android.os.SystemClock
|
||||
import android.util.Log
|
||||
import android.view.KeyEvent
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.ViewConfiguration
|
||||
import android.widget.Toast
|
||||
import io.github.libxposed.api.XposedModule
|
||||
import io.github.libxposed.api.XposedModuleInterface
|
||||
import org.lsposed.hiddenapibypass.HiddenApiBypass
|
||||
import java.lang.reflect.Field
|
||||
import java.lang.reflect.Method
|
||||
import java.util.WeakHashMap
|
||||
|
||||
class NavButtons : XposedModule() {
|
||||
private val remotePreferences: SharedPreferences? by lazy(LazyThreadSafetyMode.NONE) {
|
||||
runCatching {
|
||||
getRemotePreferences(NavButtonSettingsStore.PREFERENCES_NAME)
|
||||
}.getOrElse { error ->
|
||||
log("Remote preferences unavailable: ${error.javaClass.simpleName}")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private var context: Context? = null
|
||||
private var mainHandler: Handler? = null
|
||||
private var systemUiHookInstalled = false
|
||||
private var systemServerHookInstalled = false
|
||||
private var launcherHookInstalled = false
|
||||
private var lastHomeLongPressHandledAtMs = 0L
|
||||
private val systemUiLongPressTriggered = WeakHashMap<Any, Boolean>()
|
||||
private val systemUiHomeStockLongPressGuards = WeakHashMap<Any, Runnable>()
|
||||
private val systemUiHomeStockPressCanceled = WeakHashMap<Any, Boolean>()
|
||||
private val honeyspaceLongPressTriggered = WeakHashMap<Any, Boolean>()
|
||||
|
||||
override fun onModuleLoaded(param: XposedModuleInterface.ModuleLoadedParam) {
|
||||
if (!HiddenApiBypass.addHiddenApiExemptions("")) {
|
||||
@@ -39,13 +60,23 @@ class NavButtons : XposedModule() {
|
||||
handleTargetPackage(param.packageName, param.classLoader)
|
||||
}
|
||||
|
||||
override fun onSystemServerStarting(param: XposedModuleInterface.SystemServerStartingParam) {
|
||||
if (systemServerHookInstalled) {
|
||||
return
|
||||
}
|
||||
initContextAndHandler()
|
||||
systemServerHookInstalled = hookSystemServerHomeLongPress(param.classLoader)
|
||||
trace("System server hooks installed=$systemServerHookInstalled")
|
||||
}
|
||||
|
||||
private fun handleTargetPackage(packageName: String, classLoader: ClassLoader?) {
|
||||
if (SYSTEMUI_PACKAGE == packageName) {
|
||||
if (systemUiHookInstalled) {
|
||||
return
|
||||
}
|
||||
initContextAndHandler()
|
||||
systemUiHookInstalled = hookSystemUiBackLongPress(classLoader)
|
||||
systemUiHookInstalled = hookSystemUiNavigationButtons(classLoader)
|
||||
trace("SystemUI hooks installed=$systemUiHookInstalled")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -55,6 +86,7 @@ class NavButtons : XposedModule() {
|
||||
}
|
||||
initContextAndHandler()
|
||||
launcherHookInstalled = hookHoneyspaceTaskbar(classLoader)
|
||||
trace("Honeyspace hooks installed=$launcherHookInstalled")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,9 +109,22 @@ class NavButtons : XposedModule() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun hookSystemUiBackLongPress(classLoader: ClassLoader?): Boolean {
|
||||
private fun hookSystemUiNavigationButtons(classLoader: ClassLoader?): Boolean {
|
||||
val keyButtonClass = findClassIfExists(SYSTEMUI_KEY_BUTTON_VIEW, classLoader) ?: run {
|
||||
log("SystemUI key button class not found: $SYSTEMUI_KEY_BUTTON_VIEW")
|
||||
return false
|
||||
}
|
||||
val onTouchEventMethod = findMethodIfExists(
|
||||
keyButtonClass,
|
||||
"onTouchEvent",
|
||||
MotionEvent::class.java,
|
||||
) ?: run {
|
||||
log("SystemUI onTouchEvent() method not found.")
|
||||
return false
|
||||
}
|
||||
|
||||
val runnableClass = findClassIfExists(
|
||||
"com.android.systemui.navigationbar.views.buttons.KeyButtonView\$1",
|
||||
"$SYSTEMUI_KEY_BUTTON_VIEW\$1",
|
||||
classLoader,
|
||||
) ?: run {
|
||||
log("SystemUI long-press runnable not found.")
|
||||
@@ -90,31 +135,358 @@ class NavButtons : XposedModule() {
|
||||
log("SystemUI run() method not found.")
|
||||
return false
|
||||
}
|
||||
val sendEventMethods = findMethodsNamed(keyButtonClass, "sendEvent").filter { method ->
|
||||
val parameterTypes = method.parameterTypes
|
||||
parameterTypes.isNotEmpty() && parameterTypes[0] == Int::class.javaPrimitiveType
|
||||
}
|
||||
|
||||
deoptimizeMethodIfPresent(onTouchEventMethod)
|
||||
prepareLongPressHook(runMethod)
|
||||
sendEventMethods.forEach(::deoptimizeMethodIfPresent)
|
||||
sendEventMethods.forEach(::hookSystemUiSendEvent)
|
||||
|
||||
hook(onTouchEventMethod).intercept { chain ->
|
||||
val keyButtonView = chain.thisObject ?: return@intercept chain.proceed()
|
||||
val motionEvent = chain.args[0] as? MotionEvent ?: return@intercept chain.proceed()
|
||||
val buttonId = buttonIdForKeyCode(getIntField(keyButtonView, "mCode"))
|
||||
?: return@intercept chain.proceed()
|
||||
|
||||
when (motionEvent.actionMasked) {
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
systemUiLongPressTriggered[keyButtonView] = false
|
||||
cancelSystemUiHomeStockLongPressGuard(keyButtonView)
|
||||
systemUiHomeStockPressCanceled.remove(keyButtonView)
|
||||
if (buttonId == NavButtonId.HOME) {
|
||||
val config = getButtonConfig(buttonId)
|
||||
traceHome(
|
||||
"KeyButtonView.onTouchEvent DOWN",
|
||||
"press=${config.pressAction.storageValue} long=${config.longPressAction.storageValue} duration=${config.longPressDurationMs}",
|
||||
)
|
||||
}
|
||||
val result = chain.proceed()
|
||||
rescheduleSystemUiLongPress(keyButtonView, buttonId)
|
||||
scheduleSystemUiHomeStockLongPressGuard(keyButtonView, buttonId)
|
||||
result
|
||||
}
|
||||
|
||||
MotionEvent.ACTION_UP -> {
|
||||
if (systemUiLongPressTriggered[keyButtonView] == true) {
|
||||
if (buttonId == NavButtonId.HOME) {
|
||||
traceHome("KeyButtonView.onTouchEvent UP", "converted to CANCEL after long press")
|
||||
}
|
||||
return@intercept proceedWithCanceledAction(motionEvent) {
|
||||
chain.proceed()
|
||||
}.also {
|
||||
cancelSystemUiHomeStockLongPressGuard(keyButtonView)
|
||||
systemUiLongPressTriggered.remove(keyButtonView)
|
||||
systemUiHomeStockPressCanceled.remove(keyButtonView)
|
||||
}
|
||||
}
|
||||
|
||||
val buttonConfig = getButtonConfig(buttonId)
|
||||
val pressAction = buttonConfig.pressAction
|
||||
val stockHomePressCanceled =
|
||||
buttonId == NavButtonId.HOME && systemUiHomeStockPressCanceled[keyButtonView] == true
|
||||
val shouldOverridePress =
|
||||
pressAction != NavButtonAction.STOCK &&
|
||||
((keyButtonView as? View)?.isPressed == true || stockHomePressCanceled) &&
|
||||
getFieldValue(keyButtonView, "mLongClicked") != true
|
||||
|
||||
if (shouldOverridePress) {
|
||||
markSystemUiLongClicked(keyButtonView)
|
||||
}
|
||||
|
||||
val result = chain.proceed()
|
||||
if (stockHomePressCanceled && pressAction == NavButtonAction.STOCK) {
|
||||
dispatchSystemUiHomeTap(keyButtonView)
|
||||
}
|
||||
if (shouldOverridePress) {
|
||||
updateContextFromView(keyButtonView)
|
||||
performConfiguredAction(pressAction)
|
||||
}
|
||||
if (buttonId == NavButtonId.HOME) {
|
||||
traceHome(
|
||||
"KeyButtonView.onTouchEvent UP",
|
||||
"override=$shouldOverridePress ${systemUiHomeState(keyButtonView)}",
|
||||
)
|
||||
}
|
||||
cancelSystemUiHomeStockLongPressGuard(keyButtonView)
|
||||
systemUiHomeStockPressCanceled.remove(keyButtonView)
|
||||
result
|
||||
}
|
||||
|
||||
MotionEvent.ACTION_CANCEL -> {
|
||||
if (buttonId == NavButtonId.HOME) {
|
||||
traceHome("KeyButtonView.onTouchEvent CANCEL", "clearing long-press state")
|
||||
}
|
||||
cancelSystemUiHomeStockLongPressGuard(keyButtonView)
|
||||
systemUiLongPressTriggered.remove(keyButtonView)
|
||||
systemUiHomeStockPressCanceled.remove(keyButtonView)
|
||||
chain.proceed()
|
||||
}
|
||||
|
||||
else -> chain.proceed()
|
||||
}
|
||||
}
|
||||
|
||||
hook(runMethod).intercept { chain ->
|
||||
val keyButtonView = getFieldValue(chain.thisObject, "this\$0")
|
||||
?: return@intercept chain.proceed()
|
||||
val code = getIntField(keyButtonView, "mCode")
|
||||
if (code != KeyEvent.KEYCODE_BACK) {
|
||||
val buttonId = buttonIdForKeyCode(getIntField(keyButtonView, "mCode"))
|
||||
?: return@intercept chain.proceed()
|
||||
val longPressAction = getButtonConfig(buttonId).longPressAction
|
||||
if (longPressAction == NavButtonAction.STOCK) {
|
||||
return@intercept chain.proceed()
|
||||
}
|
||||
|
||||
if (buttonId == NavButtonId.HOME) {
|
||||
traceHome("KeyButtonView\$1.run", "long=${longPressAction.storageValue}")
|
||||
}
|
||||
updateContextFromView(keyButtonView)
|
||||
systemUiLongPressTriggered[keyButtonView] = true
|
||||
cancelSystemUiKeyPress(keyButtonView)
|
||||
markSystemUiLongClicked(keyButtonView)
|
||||
killForegroundApp()
|
||||
if (buttonId == NavButtonId.HOME) {
|
||||
performHomeLongPressAction(longPressAction)
|
||||
} else {
|
||||
performConfiguredAction(longPressAction)
|
||||
}
|
||||
null
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun hookSystemUiSendEvent(sendEventMethod: Method) {
|
||||
hook(sendEventMethod).intercept { chain ->
|
||||
val keyButtonView = chain.thisObject ?: return@intercept chain.proceed()
|
||||
val buttonId = buttonIdForKeyCode(getIntField(keyButtonView, "mCode"))
|
||||
?: return@intercept chain.proceed()
|
||||
if (buttonId != NavButtonId.HOME) {
|
||||
return@intercept chain.proceed()
|
||||
}
|
||||
|
||||
val action = chain.args.firstOrNull() as? Int ?: return@intercept chain.proceed()
|
||||
val longPressAction = getButtonConfig(buttonId).longPressAction
|
||||
if (longPressAction == NavButtonAction.STOCK) {
|
||||
return@intercept chain.proceed()
|
||||
}
|
||||
|
||||
if (action == KeyEvent.ACTION_DOWN && sendEventFlags(chain.args).hasFlag(KeyEvent.FLAG_LONG_PRESS)) {
|
||||
traceHome(
|
||||
"KeyButtonView.sendEvent",
|
||||
"suppressed stock long-press action=${keyActionName(action)} " +
|
||||
"args=${sendEventArgsToTraceString(chain.args)} ${systemUiHomeState(keyButtonView)}",
|
||||
)
|
||||
return@intercept null
|
||||
}
|
||||
|
||||
chain.proceed()
|
||||
}
|
||||
}
|
||||
|
||||
private fun hookSystemServerHomeLongPress(classLoader: ClassLoader?): Boolean {
|
||||
val phoneWindowManagerClass = findClassIfExists(SYSTEM_SERVER_PHONE_WINDOW_MANAGER, classLoader)
|
||||
?: run {
|
||||
log("PhoneWindowManager class not found: $SYSTEM_SERVER_PHONE_WINDOW_MANAGER")
|
||||
return false
|
||||
}
|
||||
|
||||
var installed = false
|
||||
|
||||
val longPressOnHomeWithEvent = findMethodIfExists(
|
||||
phoneWindowManagerClass,
|
||||
"handleLongPressOnHome",
|
||||
KeyEvent::class.java,
|
||||
)
|
||||
val longPressOnHomeLegacy = findMethodIfExists(
|
||||
phoneWindowManagerClass,
|
||||
"handleLongPressOnHome",
|
||||
Int::class.javaPrimitiveType!!,
|
||||
Long::class.javaPrimitiveType!!,
|
||||
)
|
||||
val launchAssistActionMethod = findMethodIfExists(
|
||||
phoneWindowManagerClass,
|
||||
"launchAssistAction",
|
||||
String::class.java,
|
||||
Int::class.javaPrimitiveType!!,
|
||||
Long::class.javaPrimitiveType!!,
|
||||
Int::class.javaPrimitiveType!!,
|
||||
)
|
||||
|
||||
if (longPressOnHomeWithEvent != null) {
|
||||
deoptimizeMethodIfPresent(longPressOnHomeWithEvent)
|
||||
hook(longPressOnHomeWithEvent).intercept { chain ->
|
||||
val longPressAction = getButtonConfig(NavButtonId.HOME).longPressAction
|
||||
traceHome("PhoneWindowManager.handleLongPressOnHome(KeyEvent)", "long=${longPressAction.storageValue}")
|
||||
if (longPressAction == NavButtonAction.STOCK) {
|
||||
return@intercept chain.proceed()
|
||||
}
|
||||
updateContextFromField(chain.thisObject, "mContext")
|
||||
performHomeLongPressAction(longPressAction)
|
||||
null
|
||||
}
|
||||
installed = true
|
||||
}
|
||||
|
||||
if (longPressOnHomeLegacy != null) {
|
||||
deoptimizeMethodIfPresent(longPressOnHomeLegacy)
|
||||
hook(longPressOnHomeLegacy).intercept { chain ->
|
||||
val longPressAction = getButtonConfig(NavButtonId.HOME).longPressAction
|
||||
traceHome("PhoneWindowManager.handleLongPressOnHome(int,long)", "long=${longPressAction.storageValue}")
|
||||
if (longPressAction == NavButtonAction.STOCK) {
|
||||
return@intercept chain.proceed()
|
||||
}
|
||||
updateContextFromField(chain.thisObject, "mContext")
|
||||
performHomeLongPressAction(longPressAction)
|
||||
null
|
||||
}
|
||||
installed = true
|
||||
}
|
||||
|
||||
if (launchAssistActionMethod != null) {
|
||||
deoptimizeMethodIfPresent(launchAssistActionMethod)
|
||||
hook(launchAssistActionMethod).intercept { chain ->
|
||||
val invocationType = chain.args[3] as? Int ?: ASSIST_INVOCATION_TYPE_UNKNOWN
|
||||
traceHome("PhoneWindowManager.launchAssistAction", "invocationType=$invocationType")
|
||||
if (invocationType != ASSIST_INVOCATION_TYPE_HOME_BUTTON_LONG_PRESS) {
|
||||
return@intercept chain.proceed()
|
||||
}
|
||||
|
||||
val longPressAction = getButtonConfig(NavButtonId.HOME).longPressAction
|
||||
if (longPressAction == NavButtonAction.STOCK) {
|
||||
return@intercept chain.proceed()
|
||||
}
|
||||
|
||||
traceHome("PhoneWindowManager.launchAssistAction", "blocked long=${longPressAction.storageValue}")
|
||||
updateContextFromField(chain.thisObject, "mContext")
|
||||
performHomeLongPressAction(longPressAction)
|
||||
null
|
||||
}
|
||||
installed = true
|
||||
}
|
||||
return installed
|
||||
}
|
||||
|
||||
private fun hookSystemUiHomeLongClickIfPresent(navigationBarClass: Class<*>?) {
|
||||
if (navigationBarClass == null) {
|
||||
log("SystemUI navigation bar class not found: $SYSTEMUI_NAVIGATION_BAR")
|
||||
return
|
||||
}
|
||||
|
||||
val onHomeLongClickMethod = findMethodIfExists(
|
||||
navigationBarClass,
|
||||
"onHomeLongClick",
|
||||
View::class.java,
|
||||
) ?: run {
|
||||
log("SystemUI onHomeLongClick() method not found.")
|
||||
return
|
||||
}
|
||||
|
||||
deoptimizeMethodIfPresent(onHomeLongClickMethod)
|
||||
hook(onHomeLongClickMethod).intercept { chain ->
|
||||
val longPressAction = getButtonConfig(NavButtonId.HOME).longPressAction
|
||||
if (longPressAction == NavButtonAction.STOCK) {
|
||||
return@intercept chain.proceed()
|
||||
}
|
||||
|
||||
traceHome("NavigationBar.onHomeLongClick", "long=${longPressAction.storageValue}")
|
||||
val homeView = chain.args[0] as? View
|
||||
if (homeView != null) {
|
||||
updateContextFromView(homeView)
|
||||
systemUiLongPressTriggered[homeView] = true
|
||||
markSystemUiLongClicked(homeView)
|
||||
abortSystemUiNavigationGesture(chain.thisObject, homeView)
|
||||
}
|
||||
performHomeLongPressAction(longPressAction)
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
private fun hookSystemUiAssistStartIfPresent(assistManagerClass: Class<*>?) {
|
||||
if (assistManagerClass == null) {
|
||||
log("SystemUI assist manager class not found: $SYSTEMUI_ASSIST_MANAGER")
|
||||
return
|
||||
}
|
||||
|
||||
val startAssistMethod = findMethodIfExists(
|
||||
assistManagerClass,
|
||||
"startAssist",
|
||||
Bundle::class.java,
|
||||
) ?: run {
|
||||
log("SystemUI startAssist(Bundle) method not found.")
|
||||
return
|
||||
}
|
||||
|
||||
deoptimizeMethodIfPresent(startAssistMethod)
|
||||
hook(startAssistMethod).intercept { chain ->
|
||||
val args = chain.args[0] as? Bundle ?: return@intercept chain.proceed()
|
||||
val invocationType = args.getInt(ASSIST_INVOCATION_TYPE_KEY, ASSIST_INVOCATION_TYPE_UNKNOWN)
|
||||
traceHome("AssistManager.startAssist", "invocationType=$invocationType")
|
||||
if (invocationType != ASSIST_INVOCATION_TYPE_HOME_BUTTON_LONG_PRESS) {
|
||||
return@intercept chain.proceed()
|
||||
}
|
||||
|
||||
val longPressAction = getButtonConfig(NavButtonId.HOME).longPressAction
|
||||
if (longPressAction == NavButtonAction.STOCK) {
|
||||
return@intercept chain.proceed()
|
||||
}
|
||||
|
||||
traceHome("AssistManager.startAssist", "blocked long=${longPressAction.storageValue}")
|
||||
performHomeLongPressAction(longPressAction)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun hookSystemUiOverviewProxyStartAssistantIfPresent(overviewProxyServiceClass: Class<*>?) {
|
||||
if (overviewProxyServiceClass == null) {
|
||||
log("SystemUI overview proxy service class not found: $SYSTEMUI_OVERVIEW_PROXY_SERVICE")
|
||||
return
|
||||
}
|
||||
|
||||
val startAssistantMethod = findMethodIfExists(
|
||||
overviewProxyServiceClass,
|
||||
"startAssistant",
|
||||
Bundle::class.java,
|
||||
) ?: run {
|
||||
log("SystemUI OverviewProxyService.startAssistant(Bundle) method not found.")
|
||||
return
|
||||
}
|
||||
|
||||
deoptimizeMethodIfPresent(startAssistantMethod)
|
||||
hook(startAssistantMethod).intercept { chain ->
|
||||
val args = chain.args[0] as? Bundle ?: return@intercept chain.proceed()
|
||||
val invocationType = args.getInt(ASSIST_INVOCATION_TYPE_KEY, ASSIST_INVOCATION_TYPE_UNKNOWN)
|
||||
traceHome("OverviewProxyService.startAssistant", "invocationType=$invocationType")
|
||||
if (invocationType != ASSIST_INVOCATION_TYPE_HOME_BUTTON_LONG_PRESS) {
|
||||
return@intercept chain.proceed()
|
||||
}
|
||||
|
||||
val longPressAction = getButtonConfig(NavButtonId.HOME).longPressAction
|
||||
if (longPressAction == NavButtonAction.STOCK) {
|
||||
return@intercept chain.proceed()
|
||||
}
|
||||
|
||||
traceHome("OverviewProxyService.startAssistant", "blocked long=${longPressAction.storageValue}")
|
||||
performHomeLongPressAction(longPressAction)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun hookHoneyspaceTaskbar(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
|
||||
@@ -125,8 +497,77 @@ class NavButtons : XposedModule() {
|
||||
return false
|
||||
}
|
||||
|
||||
deoptimizeMethodIfPresent(onTouchEventMethod)
|
||||
prepareLongPressHook(runMethod)
|
||||
|
||||
hook(onTouchEventMethod).intercept { chain ->
|
||||
val navButtonView = chain.thisObject ?: return@intercept chain.proceed()
|
||||
val motionEvent = chain.args[0] as? MotionEvent ?: return@intercept chain.proceed()
|
||||
val buttonId = buttonIdForKeyCode(callMethod(navButtonView, "getKeyCode") as? Int ?: Int.MIN_VALUE)
|
||||
?: return@intercept chain.proceed()
|
||||
|
||||
when (motionEvent.actionMasked) {
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
honeyspaceLongPressTriggered[navButtonView] = false
|
||||
if (buttonId == NavButtonId.HOME) {
|
||||
val config = getButtonConfig(buttonId)
|
||||
traceHome(
|
||||
"Honeyspace.onTouchEvent DOWN",
|
||||
"press=${config.pressAction.storageValue} long=${config.longPressAction.storageValue} duration=${config.longPressDurationMs}",
|
||||
)
|
||||
}
|
||||
val result = chain.proceed()
|
||||
rescheduleHoneyspaceLongPress(navButtonView, runnableClass, buttonId)
|
||||
result
|
||||
}
|
||||
|
||||
MotionEvent.ACTION_UP -> {
|
||||
val longPressTriggered = honeyspaceLongPressTriggered[navButtonView] == true
|
||||
if (longPressTriggered) {
|
||||
if (buttonId == NavButtonId.HOME) {
|
||||
traceHome("Honeyspace.onTouchEvent UP", "converted to CANCEL after long press")
|
||||
}
|
||||
return@intercept proceedWithCanceledAction(motionEvent) {
|
||||
chain.proceed()
|
||||
}.also {
|
||||
honeyspaceLongPressTriggered.remove(navButtonView)
|
||||
}
|
||||
}
|
||||
|
||||
val pressAction = getButtonConfig(buttonId).pressAction
|
||||
val shouldOverridePress =
|
||||
pressAction != NavButtonAction.STOCK &&
|
||||
(navButtonView as? View)?.isPressed == true &&
|
||||
!longPressTriggered
|
||||
|
||||
if (shouldOverridePress) {
|
||||
markHoneyspaceLongClicked(navButtonView)
|
||||
}
|
||||
|
||||
val result = chain.proceed()
|
||||
if (shouldOverridePress) {
|
||||
updateContextFromView(navButtonView)
|
||||
performConfiguredAction(pressAction)
|
||||
}
|
||||
honeyspaceLongPressTriggered.remove(navButtonView)
|
||||
if (buttonId == NavButtonId.HOME) {
|
||||
traceHome("Honeyspace.onTouchEvent UP", "override=$shouldOverridePress")
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
MotionEvent.ACTION_CANCEL -> {
|
||||
if (buttonId == NavButtonId.HOME) {
|
||||
traceHome("Honeyspace.onTouchEvent CANCEL", "clearing long-press state")
|
||||
}
|
||||
honeyspaceLongPressTriggered.remove(navButtonView)
|
||||
chain.proceed()
|
||||
}
|
||||
|
||||
else -> chain.proceed()
|
||||
}
|
||||
}
|
||||
|
||||
hook(runMethod).intercept { chain ->
|
||||
val caseId = getIntField(chain.thisObject, "b")
|
||||
if (caseId != HONEYSPACE_LONG_PRESS_CASE) {
|
||||
@@ -141,20 +582,30 @@ class NavButtons : XposedModule() {
|
||||
val navButtonView = target ?: return@intercept chain.proceed()
|
||||
|
||||
val keyCode = callMethod(navButtonView, "getKeyCode") as Int
|
||||
if (keyCode != KeyEvent.KEYCODE_BACK) {
|
||||
val buttonId = buttonIdForKeyCode(keyCode) ?: return@intercept chain.proceed()
|
||||
val longPressAction = getButtonConfig(buttonId).longPressAction
|
||||
honeyspaceLongPressTriggered[navButtonView] = true
|
||||
if (longPressAction == NavButtonAction.STOCK) {
|
||||
return@intercept chain.proceed()
|
||||
}
|
||||
|
||||
if (buttonId == NavButtonId.HOME) {
|
||||
traceHome("Honeyspace runnable", "long=${longPressAction.storageValue}")
|
||||
}
|
||||
updateContextFromView(navButtonView)
|
||||
markHoneyspaceLongClicked(navButtonView)
|
||||
killForegroundApp()
|
||||
performConfiguredAction(longPressAction)
|
||||
null
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun markSystemUiLongClicked(keyButtonView: Any) {
|
||||
setBooleanField(keyButtonView, "mLongClicked", true)
|
||||
try {
|
||||
findField(keyButtonView.javaClass, "mLongClicked").setBoolean(keyButtonView, true)
|
||||
} catch (t: Throwable) {
|
||||
log("Failed to set mLongClicked", t)
|
||||
}
|
||||
}
|
||||
|
||||
private fun markHoneyspaceLongClicked(navButtonView: Any) {
|
||||
@@ -175,6 +626,21 @@ class NavButtons : XposedModule() {
|
||||
context = currentContext.applicationContext ?: currentContext
|
||||
}
|
||||
|
||||
private fun updateContextFromField(target: Any?, fieldName: String) {
|
||||
val currentContext = getFieldValue(target, fieldName) as? Context ?: return
|
||||
context = currentContext.applicationContext ?: currentContext
|
||||
}
|
||||
|
||||
private fun performConfiguredAction(action: NavButtonAction) {
|
||||
when (action) {
|
||||
NavButtonAction.STOCK,
|
||||
NavButtonAction.NONE,
|
||||
-> Unit
|
||||
|
||||
NavButtonAction.KILL_FOREGROUND_APP -> killForegroundApp()
|
||||
}
|
||||
}
|
||||
|
||||
private fun killForegroundApp() {
|
||||
if (mainHandler == null) {
|
||||
initContextAndHandler()
|
||||
@@ -277,6 +743,34 @@ class NavButtons : XposedModule() {
|
||||
return value as? Int ?: Int.MIN_VALUE
|
||||
}
|
||||
|
||||
private fun getButtonConfig(buttonId: NavButtonId): NavButtonConfig {
|
||||
val preferences = remotePreferences
|
||||
val defaultDurationMs = ViewConfiguration.getLongPressTimeout().coerceLongPressDuration()
|
||||
return NavButtonConfig(
|
||||
pressAction = NavButtonAction.fromStorageValue(
|
||||
preferences?.getString(NavButtonSettingsStore.pressActionKey(buttonId), null),
|
||||
),
|
||||
longPressAction = NavButtonAction.fromStorageValue(
|
||||
preferences?.getString(NavButtonSettingsStore.longPressActionKey(buttonId), null),
|
||||
),
|
||||
longPressDurationMs = (
|
||||
preferences?.getInt(
|
||||
NavButtonSettingsStore.longPressDurationKey(buttonId),
|
||||
defaultDurationMs,
|
||||
) ?: defaultDurationMs
|
||||
).coerceLongPressDuration(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun buttonIdForKeyCode(keyCode: Int): NavButtonId? {
|
||||
return when (keyCode) {
|
||||
KeyEvent.KEYCODE_BACK -> NavButtonId.BACK
|
||||
KeyEvent.KEYCODE_HOME -> NavButtonId.HOME
|
||||
KeyEvent.KEYCODE_APP_SWITCH -> NavButtonId.RECENTS
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
private fun getPackageNameFromComponent(component: Any?): String? {
|
||||
return (component as? ComponentName)?.packageName
|
||||
}
|
||||
@@ -317,6 +811,27 @@ class NavButtons : XposedModule() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun trace(message: String) {
|
||||
if (!DEBUG_TRACE) {
|
||||
return
|
||||
}
|
||||
val fullMessage =
|
||||
"[proc=${currentProcessLabel()} pid=${Process.myPid()} uptime=${SystemClock.uptimeMillis()}] $message"
|
||||
Log.i(TAG, fullMessage)
|
||||
log(fullMessage)
|
||||
}
|
||||
|
||||
private fun traceHome(source: String, details: String) {
|
||||
trace("HOME $source :: $details")
|
||||
}
|
||||
|
||||
private fun currentProcessLabel(): String {
|
||||
return runCatching {
|
||||
callStaticMethod(findClass("android.app.ActivityThread", null), "currentProcessName")
|
||||
?.toString()
|
||||
}.getOrNull() ?: context?.packageName ?: "unknown"
|
||||
}
|
||||
|
||||
private fun log(message: String) {
|
||||
log(Log.INFO, TAG, message)
|
||||
}
|
||||
@@ -331,6 +846,193 @@ class NavButtons : XposedModule() {
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
private fun noteHomeLongPressHandled() {
|
||||
lastHomeLongPressHandledAtMs = SystemClock.uptimeMillis()
|
||||
}
|
||||
|
||||
private fun wasHomeLongPressHandledRecently(): Boolean {
|
||||
return SystemClock.uptimeMillis() - lastHomeLongPressHandledAtMs < HOME_LONG_PRESS_DEDUP_WINDOW_MS
|
||||
}
|
||||
|
||||
private fun Int.coerceLongPressDuration(): Int {
|
||||
return coerceIn(
|
||||
NavButtonSettingsStore.MIN_DURATION_MS,
|
||||
NavButtonSettingsStore.MAX_DURATION_MS,
|
||||
)
|
||||
}
|
||||
|
||||
private fun performHomeLongPressAction(action: NavButtonAction) {
|
||||
if (wasHomeLongPressHandledRecently()) {
|
||||
traceHome("performHomeLongPressAction", "deduped action=${action.storageValue}")
|
||||
return
|
||||
}
|
||||
noteHomeLongPressHandled()
|
||||
traceHome("performHomeLongPressAction", "executing action=${action.storageValue}")
|
||||
performConfiguredAction(action)
|
||||
}
|
||||
|
||||
private fun cancelSystemUiKeyPress(keyButtonView: Any) {
|
||||
if (buttonIdForKeyCode(getIntField(keyButtonView, "mCode")) == NavButtonId.HOME &&
|
||||
systemUiHomeStockPressCanceled[keyButtonView] == true
|
||||
) {
|
||||
traceHome("cancelSystemUiKeyPress", "stock key was already canceled by guard")
|
||||
abortSystemUiKeyGesture(keyButtonView)
|
||||
return
|
||||
}
|
||||
|
||||
traceHome("cancelSystemUiKeyPress", "sending canceled key-up")
|
||||
abortSystemUiKeyGesture(keyButtonView)
|
||||
sendSystemUiCanceledKeyUp(keyButtonView)
|
||||
}
|
||||
|
||||
private fun scheduleSystemUiHomeStockLongPressGuard(keyButtonView: Any, buttonId: NavButtonId) {
|
||||
if (buttonId != NavButtonId.HOME) {
|
||||
return
|
||||
}
|
||||
if (getButtonConfig(buttonId).longPressAction == NavButtonAction.STOCK) {
|
||||
return
|
||||
}
|
||||
|
||||
val view = keyButtonView as? View ?: return
|
||||
val delayMs = (ViewConfiguration.getLongPressTimeout() - STOCK_LONG_PRESS_GUARD_EARLY_MS)
|
||||
.coerceAtLeast(STOCK_LONG_PRESS_GUARD_MIN_DELAY_MS)
|
||||
val guard = Runnable {
|
||||
if (!view.isPressed || systemUiLongPressTriggered[keyButtonView] == true) {
|
||||
return@Runnable
|
||||
}
|
||||
traceHome("stock long-press guard", "canceling original Home key after ${delayMs}ms")
|
||||
systemUiHomeStockPressCanceled[keyButtonView] = true
|
||||
sendSystemUiCanceledKeyUp(keyButtonView)
|
||||
}
|
||||
|
||||
systemUiHomeStockLongPressGuards[keyButtonView] = guard
|
||||
view.postDelayed(guard, delayMs.toLong())
|
||||
}
|
||||
|
||||
private fun cancelSystemUiHomeStockLongPressGuard(keyButtonView: Any) {
|
||||
val guard = systemUiHomeStockLongPressGuards.remove(keyButtonView) ?: return
|
||||
(keyButtonView as? View)?.removeCallbacks(guard)
|
||||
}
|
||||
|
||||
private fun abortSystemUiKeyGesture(keyButtonView: Any) {
|
||||
try {
|
||||
callMethod(keyButtonView, "abortCurrentGesture")
|
||||
} catch (_: Throwable) {
|
||||
// Older/newer SystemUI variants may not expose this helper.
|
||||
}
|
||||
}
|
||||
|
||||
private fun sendSystemUiCanceledKeyUp(keyButtonView: Any) {
|
||||
try {
|
||||
callMethod(
|
||||
keyButtonView,
|
||||
"sendEvent",
|
||||
arrayOf(Int::class.javaPrimitiveType!!, Int::class.javaPrimitiveType!!),
|
||||
KeyEvent.ACTION_UP,
|
||||
KeyEvent.FLAG_CANCELED or KeyEvent.FLAG_CANCELED_LONG_PRESS,
|
||||
)
|
||||
} catch (error: Throwable) {
|
||||
log("sendEvent cancel failed: ${error.javaClass.simpleName}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun dispatchSystemUiHomeTap(keyButtonView: Any) {
|
||||
traceHome("dispatchSystemUiHomeTap", "sending stock tap after guard cancel")
|
||||
try {
|
||||
callMethod(
|
||||
keyButtonView,
|
||||
"sendEvent",
|
||||
arrayOf(Int::class.javaPrimitiveType!!, Int::class.javaPrimitiveType!!),
|
||||
KeyEvent.ACTION_DOWN,
|
||||
0,
|
||||
)
|
||||
callMethod(
|
||||
keyButtonView,
|
||||
"sendEvent",
|
||||
arrayOf(Int::class.javaPrimitiveType!!, Int::class.javaPrimitiveType!!),
|
||||
KeyEvent.ACTION_UP,
|
||||
0,
|
||||
)
|
||||
} catch (error: Throwable) {
|
||||
log("Guarded HOME tap failed: ${error.javaClass.simpleName}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun abortSystemUiNavigationGesture(navigationBar: Any?, homeView: View) {
|
||||
try {
|
||||
callMethod(homeView, "abortCurrentGesture")
|
||||
} catch (_: Throwable) {
|
||||
// Ignore variants without this helper on the home button view.
|
||||
}
|
||||
|
||||
val navigationBarView = getFieldValue(navigationBar, "mView")
|
||||
try {
|
||||
callMethod(navigationBarView ?: return, "abortCurrentGesture")
|
||||
} catch (_: Throwable) {
|
||||
// Ignore variants without this helper on the navigation bar view.
|
||||
}
|
||||
}
|
||||
|
||||
private fun rescheduleSystemUiLongPress(keyButtonView: Any, buttonId: NavButtonId) {
|
||||
val view = keyButtonView as? View ?: return
|
||||
val longPressRunnable = getFieldValue(keyButtonView, "mCheckLongPress") as? Runnable ?: return
|
||||
view.removeCallbacks(longPressRunnable)
|
||||
view.postDelayed(longPressRunnable, getButtonConfig(buttonId).longPressDurationMs.toLong())
|
||||
}
|
||||
|
||||
private fun rescheduleHoneyspaceLongPress(
|
||||
navButtonView: Any,
|
||||
runnableClass: Class<*>,
|
||||
buttonId: NavButtonId,
|
||||
) {
|
||||
val view = navButtonView as? View ?: return
|
||||
val longPressRunnable = findHoneyspaceLongPressRunnable(navButtonView, runnableClass) ?: return
|
||||
view.removeCallbacks(longPressRunnable)
|
||||
view.postDelayed(longPressRunnable, getButtonConfig(buttonId).longPressDurationMs.toLong())
|
||||
}
|
||||
|
||||
private fun findHoneyspaceLongPressRunnable(
|
||||
navButtonView: Any,
|
||||
runnableClass: Class<*>,
|
||||
): Runnable? {
|
||||
var current: Class<*>? = navButtonView.javaClass
|
||||
while (current != null) {
|
||||
current.declaredFields.forEach { field ->
|
||||
try {
|
||||
field.isAccessible = true
|
||||
val value = field.get(navButtonView) ?: return@forEach
|
||||
if (!runnableClass.isInstance(value)) {
|
||||
return@forEach
|
||||
}
|
||||
if (getIntField(value, "b") != HONEYSPACE_LONG_PRESS_CASE) {
|
||||
return@forEach
|
||||
}
|
||||
if (getFieldValue(value, "c") !== navButtonView) {
|
||||
return@forEach
|
||||
}
|
||||
return value as Runnable
|
||||
} catch (_: Throwable) {
|
||||
return@forEach
|
||||
}
|
||||
}
|
||||
current = current.superclass
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun prepareLongPressHook(runMethod: Method) {
|
||||
deoptimizeMethodIfPresent(
|
||||
findMethodIfExists(Handler::class.java, "dispatchMessage", android.os.Message::class.java),
|
||||
@@ -365,6 +1067,81 @@ class NavButtons : XposedModule() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun findMethodsNamed(clazz: Class<*>, methodName: String): List<Method> {
|
||||
val methodsBySignature = linkedMapOf<String, Method>()
|
||||
var current: Class<*>? = clazz
|
||||
while (current != null) {
|
||||
current.declaredMethods
|
||||
.filter { it.name == methodName }
|
||||
.forEach { method ->
|
||||
method.isAccessible = true
|
||||
methodsBySignature.putIfAbsent(method.toGenericString(), method)
|
||||
}
|
||||
current = current.superclass
|
||||
}
|
||||
return methodsBySignature.values.toList()
|
||||
}
|
||||
|
||||
private fun systemUiHomeState(keyButtonView: Any): String {
|
||||
return "pressed=${(keyButtonView as? View)?.isPressed == true} " +
|
||||
"longClicked=${getFieldValue(keyButtonView, "mLongClicked") == true} " +
|
||||
"triggered=${systemUiLongPressTriggered[keyButtonView] == true} " +
|
||||
"stockCanceled=${systemUiHomeStockPressCanceled[keyButtonView] == true}"
|
||||
}
|
||||
|
||||
private fun sendEventArgsToTraceString(args: Any?): String {
|
||||
val values = argsToList(args)
|
||||
return values.mapIndexed { index, arg ->
|
||||
if (index == 0 && arg is Int) {
|
||||
keyActionName(arg)
|
||||
} else if (index == 1 && arg is Int) {
|
||||
"flags=0x${arg.toString(16)}"
|
||||
} else {
|
||||
traceValue(arg)
|
||||
}
|
||||
}.joinToString(prefix = "[", postfix = "]")
|
||||
}
|
||||
|
||||
private fun sendEventFlags(args: Any?): Int {
|
||||
return argsToList(args).getOrNull(1) as? Int ?: 0
|
||||
}
|
||||
|
||||
private fun argsToList(args: Any?): List<*> {
|
||||
return when (args) {
|
||||
is Array<*> -> args.asList()
|
||||
is Iterable<*> -> args.toList()
|
||||
null -> emptyList<Any?>()
|
||||
else -> listOf(args)
|
||||
}
|
||||
}
|
||||
|
||||
private fun traceValue(value: Any?): String {
|
||||
return when (value) {
|
||||
is Long,
|
||||
is Float,
|
||||
is Double,
|
||||
is Boolean,
|
||||
is String,
|
||||
is Int,
|
||||
null,
|
||||
-> value.toString()
|
||||
|
||||
else -> value.javaClass.simpleName
|
||||
}
|
||||
}
|
||||
|
||||
private fun keyActionName(action: Int): String {
|
||||
return when (action) {
|
||||
KeyEvent.ACTION_DOWN -> "DOWN"
|
||||
KeyEvent.ACTION_UP -> "UP"
|
||||
else -> action.toString()
|
||||
}
|
||||
}
|
||||
|
||||
private fun Int.hasFlag(flag: Int): Boolean {
|
||||
return this and flag == flag
|
||||
}
|
||||
|
||||
@Throws(NoSuchMethodException::class)
|
||||
private fun findMethod(
|
||||
clazz: Class<*>,
|
||||
@@ -419,17 +1196,24 @@ class NavButtons : XposedModule() {
|
||||
return findMethod(target.javaClass, methodName, *parameterTypes).invoke(target, *args)
|
||||
}
|
||||
|
||||
private fun setBooleanField(target: Any, fieldName: String, value: Boolean) {
|
||||
try {
|
||||
findField(target.javaClass, fieldName).setBoolean(target, value)
|
||||
} catch (t: Throwable) {
|
||||
log("Failed to set $fieldName", t)
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val DEBUG_TRACE = true
|
||||
const val TAG = "NavButtons"
|
||||
const val ASSIST_INVOCATION_TYPE_KEY = "invocation_type"
|
||||
const val ASSIST_INVOCATION_TYPE_UNKNOWN = 0
|
||||
const val ASSIST_INVOCATION_TYPE_HOME_BUTTON_LONG_PRESS = 5
|
||||
const val HOME_LONG_PRESS_DEDUP_WINDOW_MS = 1_000L
|
||||
const val STOCK_LONG_PRESS_GUARD_EARLY_MS = 75
|
||||
const val STOCK_LONG_PRESS_GUARD_MIN_DELAY_MS = 100
|
||||
const val SYSTEMUI_PACKAGE = "com.android.systemui"
|
||||
const val SYSTEM_SERVER_PHONE_WINDOW_MANAGER = "com.android.server.policy.PhoneWindowManager"
|
||||
const val SYSTEMUI_ASSIST_MANAGER = "com.android.systemui.assist.AssistManager"
|
||||
const val SYSTEMUI_KEY_BUTTON_VIEW =
|
||||
"com.android.systemui.navigationbar.views.buttons.KeyButtonView"
|
||||
const val SYSTEMUI_NAVIGATION_BAR =
|
||||
"com.android.systemui.navigationbar.NavigationBar"
|
||||
const val SYSTEMUI_OVERVIEW_PROXY_SERVICE =
|
||||
"com.android.systemui.recents.OverviewProxyService"
|
||||
const val SAMSUNG_LAUNCHER_PACKAGE = "com.sec.android.app.launcher"
|
||||
const val HONEYSPACE_NAV_BUTTON_VIEW =
|
||||
"com.honeyspace.ui.honeypots.taskbar.presentation.NavigationBarKeyButtonView"
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
android
|
||||
com.android.systemui
|
||||
com.sec.android.app.launcher
|
||||
|
||||
Reference in New Issue
Block a user