Wire nav button actions into module
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
.gradle/
|
.gradle/
|
||||||
|
.kotlin/
|
||||||
build/
|
build/
|
||||||
app/build/
|
app/build/
|
||||||
local.properties
|
local.properties
|
||||||
|
|||||||
@@ -56,8 +56,8 @@ android {
|
|||||||
applicationId = "se.ajpanton.navbuttons"
|
applicationId = "se.ajpanton.navbuttons"
|
||||||
minSdk = 35
|
minSdk = 35
|
||||||
targetSdk = 35
|
targetSdk = 35
|
||||||
versionCode = 1
|
versionCode = 1777035207
|
||||||
versionName = "0.1"
|
versionName = "0.1-1777035207"
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
@@ -84,6 +84,7 @@ android {
|
|||||||
dependencies {
|
dependencies {
|
||||||
|
|
||||||
implementation("androidx.annotation:annotation:1.7.1")
|
implementation("androidx.annotation:annotation:1.7.1")
|
||||||
|
implementation("io.github.libxposed:service:101.0.0")
|
||||||
implementation("org.lsposed.hiddenapibypass:hiddenapibypass:6.1")
|
implementation("org.lsposed.hiddenapibypass:hiddenapibypass:6.1")
|
||||||
compileOnly("io.github.libxposed:api:101.0.0")
|
compileOnly("io.github.libxposed:api:101.0.0")
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,12 @@
|
|||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/Theme.NavButtons">
|
android:theme="@style/Theme.NavButtons">
|
||||||
|
|
||||||
|
<provider
|
||||||
|
android:name="io.github.libxposed.service.XposedProvider"
|
||||||
|
android:authorities="${applicationId}.XposedService"
|
||||||
|
android:exported="true"
|
||||||
|
tools:ignore="ExportedContentProvider" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".SettingsActivity"
|
android:name=".SettingsActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
package se.ajpanton.navbuttons
|
package se.ajpanton.navbuttons
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import android.util.Log
|
||||||
import android.view.ViewConfiguration
|
import android.view.ViewConfiguration
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
|
import io.github.libxposed.service.XposedService
|
||||||
|
import io.github.libxposed.service.XposedServiceHelper
|
||||||
|
|
||||||
enum class NavButtonId(
|
enum class NavButtonId(
|
||||||
val preferencePrefix: String,
|
val preferencePrefix: String,
|
||||||
@@ -36,12 +40,17 @@ data class NavButtonConfig(
|
|||||||
)
|
)
|
||||||
|
|
||||||
class NavButtonSettingsStore(context: Context) {
|
class NavButtonSettingsStore(context: Context) {
|
||||||
private val preferences = context.applicationContext.getSharedPreferences(
|
private val localPreferences = context.applicationContext.getSharedPreferences(
|
||||||
PREFERENCES_NAME,
|
PREFERENCES_NAME,
|
||||||
Context.MODE_PRIVATE,
|
Context.MODE_PRIVATE,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
init {
|
||||||
|
registerRemotePreferencesListener(localPreferences)
|
||||||
|
}
|
||||||
|
|
||||||
fun getConfig(buttonId: NavButtonId): NavButtonConfig {
|
fun getConfig(buttonId: NavButtonId): NavButtonConfig {
|
||||||
|
val preferences = currentPreferences()
|
||||||
return NavButtonConfig(
|
return NavButtonConfig(
|
||||||
pressAction = NavButtonAction.fromStorageValue(
|
pressAction = NavButtonAction.fromStorageValue(
|
||||||
preferences.getString(pressActionKey(buttonId), null),
|
preferences.getString(pressActionKey(buttonId), null),
|
||||||
@@ -59,21 +68,21 @@ class NavButtonSettingsStore(context: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun setPressAction(buttonId: NavButtonId, action: NavButtonAction) {
|
fun setPressAction(buttonId: NavButtonId, action: NavButtonAction) {
|
||||||
preferences.edit()
|
persistChanges {
|
||||||
.putString(pressActionKey(buttonId), action.storageValue)
|
putString(pressActionKey(buttonId), action.storageValue)
|
||||||
.apply()
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setLongPressAction(buttonId: NavButtonId, action: NavButtonAction) {
|
fun setLongPressAction(buttonId: NavButtonId, action: NavButtonAction) {
|
||||||
preferences.edit()
|
persistChanges {
|
||||||
.putString(longPressActionKey(buttonId), action.storageValue)
|
putString(longPressActionKey(buttonId), action.storageValue)
|
||||||
.apply()
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setLongPressDurationMs(buttonId: NavButtonId, durationMs: Int) {
|
fun setLongPressDurationMs(buttonId: NavButtonId, durationMs: Int) {
|
||||||
preferences.edit()
|
persistChanges {
|
||||||
.putInt(longPressDurationKey(buttonId), sanitizeDuration(durationMs))
|
putInt(longPressDurationKey(buttonId), sanitizeDuration(durationMs))
|
||||||
.apply()
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun defaultLongPressDurationMs(): Int {
|
fun defaultLongPressDurationMs(): Int {
|
||||||
@@ -84,11 +93,40 @@ class NavButtonSettingsStore(context: Context) {
|
|||||||
return durationMs.coerceIn(MIN_LONG_PRESS_DURATION_MS, MAX_LONG_PRESS_DURATION_MS)
|
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 {
|
companion object {
|
||||||
|
private const val TAG = "NavButtons"
|
||||||
const val PREFERENCES_NAME = "nav_button_settings"
|
const val PREFERENCES_NAME = "nav_button_settings"
|
||||||
const val KEY_PRESS_ACTION = "press_action"
|
const val KEY_PRESS_ACTION = "press_action"
|
||||||
const val KEY_LONG_PRESS_ACTION = "long_press_action"
|
const val KEY_LONG_PRESS_ACTION = "long_press_action"
|
||||||
const val KEY_LONG_PRESS_DURATION_MS = "long_press_duration_ms"
|
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 MIN_LONG_PRESS_DURATION_MS = 100
|
||||||
const val MAX_LONG_PRESS_DURATION_MS = 10_000
|
const val MAX_LONG_PRESS_DURATION_MS = 10_000
|
||||||
@@ -105,8 +143,110 @@ class NavButtonSettingsStore(context: Context) {
|
|||||||
fun longPressDurationKey(buttonId: NavButtonId): String =
|
fun longPressDurationKey(buttonId: NavButtonId): String =
|
||||||
key(buttonId, KEY_LONG_PRESS_DURATION_MS)
|
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 {
|
private fun key(buttonId: NavButtonId, suffix: String): String {
|
||||||
return "${buttonId.preferencePrefix}_$suffix"
|
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.ComponentName
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.content.SharedPreferences
|
||||||
import android.content.pm.ApplicationInfo
|
import android.content.pm.ApplicationInfo
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
|
import android.os.Bundle
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
|
import android.os.Process
|
||||||
|
import android.os.SystemClock
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.KeyEvent
|
import android.view.KeyEvent
|
||||||
|
import android.view.MotionEvent
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import android.view.ViewConfiguration
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import io.github.libxposed.api.XposedModule
|
import io.github.libxposed.api.XposedModule
|
||||||
import io.github.libxposed.api.XposedModuleInterface
|
import io.github.libxposed.api.XposedModuleInterface
|
||||||
import org.lsposed.hiddenapibypass.HiddenApiBypass
|
import org.lsposed.hiddenapibypass.HiddenApiBypass
|
||||||
import java.lang.reflect.Field
|
import java.lang.reflect.Field
|
||||||
import java.lang.reflect.Method
|
import java.lang.reflect.Method
|
||||||
|
import java.util.WeakHashMap
|
||||||
|
|
||||||
class NavButtons : XposedModule() {
|
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 context: Context? = null
|
||||||
private var mainHandler: Handler? = null
|
private var mainHandler: Handler? = null
|
||||||
private var systemUiHookInstalled = false
|
private var systemUiHookInstalled = false
|
||||||
|
private var systemServerHookInstalled = false
|
||||||
private var launcherHookInstalled = 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) {
|
override fun onModuleLoaded(param: XposedModuleInterface.ModuleLoadedParam) {
|
||||||
if (!HiddenApiBypass.addHiddenApiExemptions("")) {
|
if (!HiddenApiBypass.addHiddenApiExemptions("")) {
|
||||||
@@ -39,13 +60,23 @@ class NavButtons : XposedModule() {
|
|||||||
handleTargetPackage(param.packageName, param.classLoader)
|
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?) {
|
private fun handleTargetPackage(packageName: String, classLoader: ClassLoader?) {
|
||||||
if (SYSTEMUI_PACKAGE == packageName) {
|
if (SYSTEMUI_PACKAGE == packageName) {
|
||||||
if (systemUiHookInstalled) {
|
if (systemUiHookInstalled) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
initContextAndHandler()
|
initContextAndHandler()
|
||||||
systemUiHookInstalled = hookSystemUiBackLongPress(classLoader)
|
systemUiHookInstalled = hookSystemUiNavigationButtons(classLoader)
|
||||||
|
trace("SystemUI hooks installed=$systemUiHookInstalled")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,6 +86,7 @@ class NavButtons : XposedModule() {
|
|||||||
}
|
}
|
||||||
initContextAndHandler()
|
initContextAndHandler()
|
||||||
launcherHookInstalled = hookHoneyspaceTaskbar(classLoader)
|
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(
|
val runnableClass = findClassIfExists(
|
||||||
"com.android.systemui.navigationbar.views.buttons.KeyButtonView\$1",
|
"$SYSTEMUI_KEY_BUTTON_VIEW\$1",
|
||||||
classLoader,
|
classLoader,
|
||||||
) ?: run {
|
) ?: run {
|
||||||
log("SystemUI long-press runnable not found.")
|
log("SystemUI long-press runnable not found.")
|
||||||
@@ -90,31 +135,358 @@ class NavButtons : XposedModule() {
|
|||||||
log("SystemUI run() method not found.")
|
log("SystemUI run() method not found.")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
val sendEventMethods = findMethodsNamed(keyButtonClass, "sendEvent").filter { method ->
|
||||||
|
val parameterTypes = method.parameterTypes
|
||||||
|
parameterTypes.isNotEmpty() && parameterTypes[0] == Int::class.javaPrimitiveType
|
||||||
|
}
|
||||||
|
|
||||||
|
deoptimizeMethodIfPresent(onTouchEventMethod)
|
||||||
prepareLongPressHook(runMethod)
|
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 ->
|
hook(runMethod).intercept { chain ->
|
||||||
val keyButtonView = getFieldValue(chain.thisObject, "this\$0")
|
val keyButtonView = getFieldValue(chain.thisObject, "this\$0")
|
||||||
?: return@intercept chain.proceed()
|
?: return@intercept chain.proceed()
|
||||||
val code = getIntField(keyButtonView, "mCode")
|
val buttonId = buttonIdForKeyCode(getIntField(keyButtonView, "mCode"))
|
||||||
if (code != KeyEvent.KEYCODE_BACK) {
|
?: return@intercept chain.proceed()
|
||||||
|
val longPressAction = getButtonConfig(buttonId).longPressAction
|
||||||
|
if (longPressAction == NavButtonAction.STOCK) {
|
||||||
return@intercept chain.proceed()
|
return@intercept chain.proceed()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (buttonId == NavButtonId.HOME) {
|
||||||
|
traceHome("KeyButtonView\$1.run", "long=${longPressAction.storageValue}")
|
||||||
|
}
|
||||||
updateContextFromView(keyButtonView)
|
updateContextFromView(keyButtonView)
|
||||||
|
systemUiLongPressTriggered[keyButtonView] = true
|
||||||
|
cancelSystemUiKeyPress(keyButtonView)
|
||||||
markSystemUiLongClicked(keyButtonView)
|
markSystemUiLongClicked(keyButtonView)
|
||||||
killForegroundApp()
|
if (buttonId == NavButtonId.HOME) {
|
||||||
|
performHomeLongPressAction(longPressAction)
|
||||||
|
} else {
|
||||||
|
performConfiguredAction(longPressAction)
|
||||||
|
}
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
return true
|
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 {
|
private fun hookHoneyspaceTaskbar(classLoader: ClassLoader?): Boolean {
|
||||||
val navButtonClass = findClassIfExists(HONEYSPACE_NAV_BUTTON_VIEW, classLoader) ?: run {
|
val navButtonClass = findClassIfExists(HONEYSPACE_NAV_BUTTON_VIEW, classLoader) ?: run {
|
||||||
log("Launcher class not found: $HONEYSPACE_NAV_BUTTON_VIEW")
|
log("Launcher class not found: $HONEYSPACE_NAV_BUTTON_VIEW")
|
||||||
return false
|
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 {
|
val runnableClass = findClassIfExists(HONEYSPACE_LONG_PRESS_RUNNABLE, classLoader) ?: run {
|
||||||
log("Launcher long-press runnable not found: $HONEYSPACE_LONG_PRESS_RUNNABLE")
|
log("Launcher long-press runnable not found: $HONEYSPACE_LONG_PRESS_RUNNABLE")
|
||||||
return false
|
return false
|
||||||
@@ -125,8 +497,77 @@ class NavButtons : XposedModule() {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
deoptimizeMethodIfPresent(onTouchEventMethod)
|
||||||
prepareLongPressHook(runMethod)
|
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 ->
|
hook(runMethod).intercept { chain ->
|
||||||
val caseId = getIntField(chain.thisObject, "b")
|
val caseId = getIntField(chain.thisObject, "b")
|
||||||
if (caseId != HONEYSPACE_LONG_PRESS_CASE) {
|
if (caseId != HONEYSPACE_LONG_PRESS_CASE) {
|
||||||
@@ -141,20 +582,30 @@ class NavButtons : XposedModule() {
|
|||||||
val navButtonView = target ?: return@intercept chain.proceed()
|
val navButtonView = target ?: return@intercept chain.proceed()
|
||||||
|
|
||||||
val keyCode = callMethod(navButtonView, "getKeyCode") as Int
|
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()
|
return@intercept chain.proceed()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (buttonId == NavButtonId.HOME) {
|
||||||
|
traceHome("Honeyspace runnable", "long=${longPressAction.storageValue}")
|
||||||
|
}
|
||||||
updateContextFromView(navButtonView)
|
updateContextFromView(navButtonView)
|
||||||
markHoneyspaceLongClicked(navButtonView)
|
markHoneyspaceLongClicked(navButtonView)
|
||||||
killForegroundApp()
|
performConfiguredAction(longPressAction)
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun markSystemUiLongClicked(keyButtonView: Any) {
|
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) {
|
private fun markHoneyspaceLongClicked(navButtonView: Any) {
|
||||||
@@ -175,6 +626,21 @@ class NavButtons : XposedModule() {
|
|||||||
context = currentContext.applicationContext ?: currentContext
|
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() {
|
private fun killForegroundApp() {
|
||||||
if (mainHandler == null) {
|
if (mainHandler == null) {
|
||||||
initContextAndHandler()
|
initContextAndHandler()
|
||||||
@@ -277,6 +743,34 @@ class NavButtons : XposedModule() {
|
|||||||
return value as? Int ?: Int.MIN_VALUE
|
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? {
|
private fun getPackageNameFromComponent(component: Any?): String? {
|
||||||
return (component as? ComponentName)?.packageName
|
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) {
|
private fun log(message: String) {
|
||||||
log(Log.INFO, TAG, message)
|
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) {
|
private fun prepareLongPressHook(runMethod: Method) {
|
||||||
deoptimizeMethodIfPresent(
|
deoptimizeMethodIfPresent(
|
||||||
findMethodIfExists(Handler::class.java, "dispatchMessage", android.os.Message::class.java),
|
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)
|
@Throws(NoSuchMethodException::class)
|
||||||
private fun findMethod(
|
private fun findMethod(
|
||||||
clazz: Class<*>,
|
clazz: Class<*>,
|
||||||
@@ -419,17 +1196,24 @@ class NavButtons : XposedModule() {
|
|||||||
return findMethod(target.javaClass, methodName, *parameterTypes).invoke(target, *args)
|
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 {
|
private companion object {
|
||||||
|
const val DEBUG_TRACE = true
|
||||||
const val TAG = "NavButtons"
|
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 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 SAMSUNG_LAUNCHER_PACKAGE = "com.sec.android.app.launcher"
|
||||||
const val HONEYSPACE_NAV_BUTTON_VIEW =
|
const val HONEYSPACE_NAV_BUTTON_VIEW =
|
||||||
"com.honeyspace.ui.honeypots.taskbar.presentation.NavigationBarKeyButtonView"
|
"com.honeyspace.ui.honeypots.taskbar.presentation.NavigationBarKeyButtonView"
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
|
android
|
||||||
com.android.systemui
|
com.android.systemui
|
||||||
com.sec.android.app.launcher
|
com.sec.android.app.launcher
|
||||||
|
|||||||
Reference in New Issue
Block a user