From cade5e360ea7ebe742a0f41eeecd149cc5a61ec8 Mon Sep 17 00:00:00 2001 From: ajp_anton Date: Wed, 22 Apr 2026 23:13:24 +0000 Subject: [PATCH] Rename project to NavButtons and convert module to Kotlin --- app/build.gradle.kts | 11 +- app/proguard-rules.pro | 4 +- .../se/ajpanton/killappnow/KillAppNow.java | 421 ----------------- .../java/se/ajpanton/navbuttons/NavButtons.kt | 439 ++++++++++++++++++ app/src/main/res/values/strings.xml | 4 +- .../resources/META-INF/xposed/java_init.list | 2 +- settings.gradle.kts | 2 +- 7 files changed, 449 insertions(+), 434 deletions(-) delete mode 100644 app/src/main/java/se/ajpanton/killappnow/KillAppNow.java create mode 100644 app/src/main/java/se/ajpanton/navbuttons/NavButtons.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index c26c768..b41ee72 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -34,7 +34,7 @@ if (releaseBuildRequested && (!hasSharedReleaseSigningConfig || !hasSharedReleas } android { - namespace = "se.ajpanton.killappnow" + namespace = "se.ajpanton.navbuttons" compileSdk = 36 dependenciesInfo { includeInApk = false @@ -53,11 +53,11 @@ android { } defaultConfig { - applicationId = "se.ajpanton.killappnow" + applicationId = "se.ajpanton.navbuttons" minSdk = 35 targetSdk = 35 - versionCode = 1702 - versionName = "17.2" + versionCode = 1 + versionName = "0.1" } buildTypes { @@ -73,9 +73,6 @@ android { packaging { resources { merges += "META-INF/xposed/*" - // Not needed as long as we don't use reflection with Kotlin - excludes.add("**/*.kotlin_builtins") - excludes.add("**/*.kotlin_module") } } compileOptions { diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 9418c21..4233291 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -22,7 +22,7 @@ -dontobfuscate --keep class se.ajpanton.killappnow.KillAppNow --keepclassmembers class se.ajpanton.killappnow.KillAppNow { +-keep class se.ajpanton.navbuttons.NavButtons +-keepclassmembers class se.ajpanton.navbuttons.NavButtons { public (); } diff --git a/app/src/main/java/se/ajpanton/killappnow/KillAppNow.java b/app/src/main/java/se/ajpanton/killappnow/KillAppNow.java deleted file mode 100644 index 2f0abf1..0000000 --- a/app/src/main/java/se/ajpanton/killappnow/KillAppNow.java +++ /dev/null @@ -1,421 +0,0 @@ -package se.ajpanton.killappnow; - -import android.app.ActivityManager; -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.content.pm.ApplicationInfo; -import android.content.pm.PackageManager; -import android.content.pm.ResolveInfo; -import android.os.Handler; -import android.os.Looper; -import android.util.Log; -import android.view.KeyEvent; -import android.view.View; -import android.widget.Toast; - -import androidx.annotation.NonNull; - -import java.lang.reflect.Field; -import java.lang.reflect.Method; - -import io.github.libxposed.api.XposedModule; -import org.lsposed.hiddenapibypass.HiddenApiBypass; - -public class KillAppNow extends XposedModule { - - private static final String TAG = "KillAppNow"; - private static final String SYSTEMUI_PACKAGE = "com.android.systemui"; - private static final String SAMSUNG_LAUNCHER_PACKAGE = "com.sec.android.app.launcher"; - private static final String HONEYSPACE_NAV_BUTTON_VIEW = - "com.honeyspace.ui.honeypots.taskbar.presentation.NavigationBarKeyButtonView"; - private static final String HONEYSPACE_LONG_PRESS_RUNNABLE = "f.a"; - private static final int HONEYSPACE_LONG_PRESS_CASE = 8; - - private Context mContext; - private Handler mMainHandler; - private boolean mSystemUiHookInstalled; - private boolean mLauncherHookInstalled; - - public KillAppNow() { - // LSPosed loads modern module entries reflectively via a public no-arg constructor. - } - - @Override - public void onModuleLoaded(@NonNull ModuleLoadedParam param) { - if (!HiddenApiBypass.addHiddenApiExemptions("")) { - log("HiddenApiBypass could not be enabled."); - } - } - - @Override - public void onPackageLoaded(@NonNull PackageLoadedParam param) { - handleTargetPackage(param.getPackageName(), param.getDefaultClassLoader()); - } - - @Override - public void onPackageReady(@NonNull PackageReadyParam param) { - handleTargetPackage(param.getPackageName(), param.getClassLoader()); - } - - private void handleTargetPackage(String packageName, ClassLoader classLoader) { - if (SYSTEMUI_PACKAGE.equals(packageName)) { - if (mSystemUiHookInstalled) { - return; - } - initContextAndHandler(); - mSystemUiHookInstalled = hookSystemUiBackLongPress(classLoader); - return; - } - - if (SAMSUNG_LAUNCHER_PACKAGE.equals(packageName)) { - if (mLauncherHookInstalled) { - return; - } - initContextAndHandler(); - mLauncherHookInstalled = hookHoneyspaceTaskbar(classLoader); - } - } - - private void initContextAndHandler() { - if (mContext == null) { - mContext = getCurrentApplication(); - } - if (mMainHandler == null) { - mMainHandler = new Handler(Looper.getMainLooper()); - } - } - - private Context getCurrentApplication() { - try { - Object application = callStaticMethod( - findClass("android.app.ActivityThread", null), - "currentApplication"); - return application instanceof Context ? (Context) application : null; - } catch (Throwable t) { - log("currentApplication lookup failed: " + t.getClass().getSimpleName()); - return null; - } - } - - private boolean hookSystemUiBackLongPress(ClassLoader classLoader) { - Class runnableClass = findClassIfExists( - "com.android.systemui.navigationbar.views.buttons.KeyButtonView$1", classLoader); - if (runnableClass == null) { - log("SystemUI long-press runnable not found."); - return false; - } - - Method runMethod = findMethodIfExists(runnableClass, "run"); - if (runMethod == null) { - log("SystemUI run() method not found."); - return false; - } - - deoptimizeMethodIfPresent(findMethodIfExists(Handler.class, "dispatchMessage", android.os.Message.class)); - deoptimizeMethodIfPresent(findMethodIfExists(Handler.class, "handleCallback", android.os.Message.class)); - deoptimizeMethodIfPresent(runMethod); - - hook(runMethod).intercept(chain -> { - Object keyButtonView = getFieldValue(chain.getThisObject(), "this$0"); - int code = getIntField(keyButtonView, "mCode"); - if (code != KeyEvent.KEYCODE_BACK) { - return chain.proceed(); - } - - updateContextFromView(keyButtonView); - markSystemUiLongClicked(keyButtonView); - killForegroundApp(); - return null; - }); - return true; - } - - private boolean hookHoneyspaceTaskbar(ClassLoader classLoader) { - Class navButtonClass = findClassIfExists(HONEYSPACE_NAV_BUTTON_VIEW, classLoader); - if (navButtonClass == null) { - log("Launcher class not found: " + HONEYSPACE_NAV_BUTTON_VIEW); - return false; - } - - Class runnableClass = findClassIfExists(HONEYSPACE_LONG_PRESS_RUNNABLE, classLoader); - if (runnableClass == null) { - log("Launcher long-press runnable not found: " + HONEYSPACE_LONG_PRESS_RUNNABLE); - return false; - } - - Method runMethod = findMethodIfExists(runnableClass, "run"); - if (runMethod == null) { - log("Launcher run() method not found."); - return false; - } - - deoptimizeMethodIfPresent(findMethodIfExists(Handler.class, "dispatchMessage", android.os.Message.class)); - deoptimizeMethodIfPresent(findMethodIfExists(Handler.class, "handleCallback", android.os.Message.class)); - deoptimizeMethodIfPresent(runMethod); - - hook(runMethod).intercept(chain -> { - int caseId = getIntField(chain.getThisObject(), "b"); - if (caseId != HONEYSPACE_LONG_PRESS_CASE) { - return chain.proceed(); - } - - Object target = getFieldValue(chain.getThisObject(), "c"); - if (!navButtonClass.isInstance(target)) { - return chain.proceed(); - } - - int keyCode = ((Integer) callMethod(target, "getKeyCode")).intValue(); - if (keyCode != KeyEvent.KEYCODE_BACK) { - return chain.proceed(); - } - - updateContextFromView(target); - markHoneyspaceLongClicked(target); - killForegroundApp(); - return null; - }); - return true; - } - - private void markSystemUiLongClicked(Object keyButtonView) { - setBooleanField(keyButtonView, "mLongClicked", true); - } - - private void markHoneyspaceLongClicked(Object navButtonView) { - try { - callMethod(navButtonView, "setLongClicked", new Class[]{boolean.class}, true); - } catch (Throwable t) { - log("setLongClicked failed: " + t.getClass().getSimpleName()); - } - } - - private void updateContextFromView(Object view) { - View v = (View) view; - Context ctx = v.getContext(); - Context appCtx = ctx.getApplicationContext(); - mContext = appCtx != null ? appCtx : ctx; - } - - private void killForegroundApp() { - if (mMainHandler == null) { - initContextAndHandler(); - } - if (mMainHandler == null) { - log("Main handler unavailable."); - return; - } - mMainHandler.post(() -> { - try { - if (mContext == null) { - log("Context unavailable, cannot kill app."); - return; - } - ActivityManager am = (ActivityManager) mContext.getSystemService(Context.ACTIVITY_SERVICE); - String defaultLauncher = getDefaultLauncherPackageName(); - String foregroundApp = getTopPackageName(); - if (foregroundApp != null && !shouldSkipPackage(foregroundApp, defaultLauncher)) { - String appLabel = getApplicationLabel(foregroundApp, mContext.getPackageManager()); - log("Killing: " + foregroundApp + " (" + appLabel + ")"); - callMethod(am, "forceStopPackage", new Class[]{String.class}, foregroundApp); - showToast("Killed: " + appLabel); - } else { - log("Nothing to kill."); - showToast("Nothing to kill."); - } - } catch (Throwable t) { - Log.e(TAG, "Error in killForegroundApp", t); - log("Error in killForegroundApp", t); - } - }); - } - - private String getDefaultLauncherPackageName() { - Intent intent = new Intent(Intent.ACTION_MAIN); - intent.addCategory(Intent.CATEGORY_HOME); - ResolveInfo resolveInfo = mContext.getPackageManager() - .resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY); - if (resolveInfo != null) { - return resolveInfo.activityInfo.packageName; - } - return null; - } - - private String getTopPackageName() { - return getTopPackageFromActivityTaskManager(); - } - - private String getTopPackageFromActivityTaskManager() { - try { - Object service = callStaticMethod( - findClass("android.app.ActivityTaskManager", null), - "getService"); - Object taskInfo = callMethod(service, "getFocusedRootTaskInfo"); - return extractPackageFromTaskInfo(taskInfo); - } catch (Throwable t) { - log("ActivityTaskManager.getFocusedRootTaskInfo failed: " + t.getClass().getSimpleName()); - return null; - } - } - - private String extractPackageFromTaskInfo(Object taskInfo) { - if (taskInfo == null) { - return null; - } - - String pkg = getPackageNameFromComponent(getFieldValue(taskInfo, "topActivity")); - if (pkg != null) { - return pkg; - } - return getPackageNameFromActivityInfo(getFieldValue(taskInfo, "topActivityInfo")); - } - - private Object getFieldValue(Object target, String fieldName) { - if (target == null) { - return null; - } - try { - return findField(target.getClass(), fieldName).get(target); - } catch (Throwable t) { - return null; - } - } - - private int getIntField(Object target, String fieldName) { - Object value = getFieldValue(target, fieldName); - return value instanceof Integer ? (Integer) value : Integer.MIN_VALUE; - } - - private String getPackageNameFromComponent(Object component) { - if (component instanceof ComponentName) { - return ((ComponentName) component).getPackageName(); - } - return null; - } - - private String getPackageNameFromActivityInfo(Object info) { - if (info == null) { - return null; - } - try { - Object pkg = findField(info.getClass(), "packageName").get(info); - return pkg != null ? pkg.toString() : null; - } catch (Throwable t) { - return null; - } - } - - private boolean shouldSkipPackage(String packageName, String defaultLauncher) { - if (packageName == null) { - return true; - } - if (SYSTEMUI_PACKAGE.equals(packageName)) { - return true; - } - if (SAMSUNG_LAUNCHER_PACKAGE.equals(packageName)) { - return true; - } - return defaultLauncher != null && defaultLauncher.equals(packageName); - } - - private String getApplicationLabel(String packageName, PackageManager pm) { - try { - ApplicationInfo appInfo = pm.getApplicationInfo(packageName, 0); - return (String) pm.getApplicationLabel(appInfo); - } catch (PackageManager.NameNotFoundException e) { - return packageName; - } - } - - private void showToast(String message) { - try { - Toast.makeText(mContext, message, Toast.LENGTH_SHORT).show(); - } catch (Throwable t) { - log("Toast failed: " + t.getClass().getSimpleName()); - } - } - - private void log(String message) { - log(Log.INFO, TAG, message); - } - - private void log(String message, Throwable t) { - log(Log.ERROR, TAG, message, t); - } - - private void deoptimizeMethodIfPresent(Method method) { - if (method != null) { - deoptimize(method); - } - } - - private Class findClass(String className, ClassLoader classLoader) throws ClassNotFoundException { - return Class.forName(className, false, classLoader); - } - - private Class findClassIfExists(String className, ClassLoader classLoader) { - try { - return findClass(className, classLoader); - } catch (Throwable t) { - return null; - } - } - - private Method findMethodIfExists(Class clazz, String methodName, Class... parameterTypes) { - try { - return findMethod(clazz, methodName, parameterTypes); - } catch (Throwable t) { - return null; - } - } - - private Method findMethod(Class clazz, String methodName, Class... parameterTypes) - throws NoSuchMethodException { - for (Class current = clazz; current != null; current = current.getSuperclass()) { - try { - Method method = current.getDeclaredMethod(methodName, parameterTypes); - method.setAccessible(true); - return method; - } catch (NoSuchMethodException ignored) { - // Continue searching the class hierarchy. - } - } - throw new NoSuchMethodException(clazz.getName() + "#" + methodName); - } - - private Field findField(Class clazz, String fieldName) throws NoSuchFieldException { - for (Class current = clazz; current != null; current = current.getSuperclass()) { - try { - Field field = current.getDeclaredField(fieldName); - field.setAccessible(true); - return field; - } catch (NoSuchFieldException ignored) { - // Continue searching the class hierarchy. - } - } - throw new NoSuchFieldException(clazz.getName() + "#" + fieldName); - } - - private Object callStaticMethod(Class clazz, String methodName) - throws ReflectiveOperationException { - return findMethod(clazz, methodName).invoke(null); - } - - private Object callMethod(Object target, String methodName) - throws ReflectiveOperationException { - return callMethod(target, methodName, new Class[0]); - } - - private Object callMethod(Object target, String methodName, Class[] parameterTypes, Object... args) - throws ReflectiveOperationException { - return findMethod(target.getClass(), methodName, parameterTypes).invoke(target, args); - } - - private void setBooleanField(Object target, String fieldName, boolean value) { - try { - findField(target.getClass(), fieldName).setBoolean(target, value); - } catch (Throwable t) { - log("Failed to set " + fieldName, t); - } - } -} diff --git a/app/src/main/java/se/ajpanton/navbuttons/NavButtons.kt b/app/src/main/java/se/ajpanton/navbuttons/NavButtons.kt new file mode 100644 index 0000000..5f8bcb4 --- /dev/null +++ b/app/src/main/java/se/ajpanton/navbuttons/NavButtons.kt @@ -0,0 +1,439 @@ +package se.ajpanton.navbuttons + +import android.app.ActivityManager +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.pm.ApplicationInfo +import android.content.pm.PackageManager +import android.os.Handler +import android.os.Looper +import android.util.Log +import android.view.KeyEvent +import android.view.View +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 + +class NavButtons : XposedModule() { + + private var context: Context? = null + private var mainHandler: Handler? = null + private var systemUiHookInstalled = false + private var launcherHookInstalled = false + + override fun onModuleLoaded(param: XposedModuleInterface.ModuleLoadedParam) { + if (!HiddenApiBypass.addHiddenApiExemptions("")) { + log("HiddenApiBypass could not be enabled.") + } + } + + override fun onPackageLoaded(param: XposedModuleInterface.PackageLoadedParam) { + handleTargetPackage(param.packageName, param.defaultClassLoader) + } + + override fun onPackageReady(param: XposedModuleInterface.PackageReadyParam) { + handleTargetPackage(param.packageName, param.classLoader) + } + + private fun handleTargetPackage(packageName: String, classLoader: ClassLoader?) { + if (SYSTEMUI_PACKAGE == packageName) { + if (systemUiHookInstalled) { + return + } + initContextAndHandler() + systemUiHookInstalled = hookSystemUiBackLongPress(classLoader) + return + } + + if (SAMSUNG_LAUNCHER_PACKAGE == packageName) { + if (launcherHookInstalled) { + return + } + initContextAndHandler() + launcherHookInstalled = hookHoneyspaceTaskbar(classLoader) + } + } + + private fun initContextAndHandler() { + if (context == null) { + context = getCurrentApplication() + } + if (mainHandler == null) { + mainHandler = Handler(Looper.getMainLooper()) + } + } + + private fun getCurrentApplication(): Context? { + return try { + val application = callStaticMethod(findClass("android.app.ActivityThread", null), "currentApplication") + application as? Context + } catch (t: Throwable) { + log("currentApplication lookup failed: ${t.javaClass.simpleName}") + null + } + } + + private fun hookSystemUiBackLongPress(classLoader: ClassLoader?): Boolean { + val runnableClass = findClassIfExists( + "com.android.systemui.navigationbar.views.buttons.KeyButtonView\$1", + classLoader, + ) ?: run { + log("SystemUI long-press runnable not found.") + return false + } + + val runMethod = findMethodIfExists(runnableClass, "run") ?: run { + log("SystemUI run() method not found.") + return false + } + + prepareLongPressHook(runMethod) + + 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) { + return@intercept chain.proceed() + } + + updateContextFromView(keyButtonView) + markSystemUiLongClicked(keyButtonView) + killForegroundApp() + null + } + return true + } + + 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 runnableClass = findClassIfExists(HONEYSPACE_LONG_PRESS_RUNNABLE, classLoader) ?: run { + log("Launcher long-press runnable not found: $HONEYSPACE_LONG_PRESS_RUNNABLE") + return false + } + + val runMethod = findMethodIfExists(runnableClass, "run") ?: run { + log("Launcher run() method not found.") + return false + } + + prepareLongPressHook(runMethod) + + hook(runMethod).intercept { chain -> + val caseId = getIntField(chain.thisObject, "b") + if (caseId != HONEYSPACE_LONG_PRESS_CASE) { + return@intercept chain.proceed() + } + + val target = getFieldValue(chain.thisObject, "c") + if (!navButtonClass.isInstance(target)) { + return@intercept chain.proceed() + } + + val navButtonView = target ?: return@intercept chain.proceed() + + val keyCode = callMethod(navButtonView, "getKeyCode") as Int + if (keyCode != KeyEvent.KEYCODE_BACK) { + return@intercept chain.proceed() + } + + updateContextFromView(navButtonView) + markHoneyspaceLongClicked(navButtonView) + killForegroundApp() + null + } + return true + } + + private fun markSystemUiLongClicked(keyButtonView: Any) { + setBooleanField(keyButtonView, "mLongClicked", true) + } + + private fun markHoneyspaceLongClicked(navButtonView: Any) { + try { + callMethod( + navButtonView, + "setLongClicked", + arrayOf(Boolean::class.javaPrimitiveType!!), + true, + ) + } catch (t: Throwable) { + log("setLongClicked failed: ${t.javaClass.simpleName}") + } + } + + private fun updateContextFromView(view: Any) { + val currentContext = (view as View).context + context = currentContext.applicationContext ?: currentContext + } + + private fun killForegroundApp() { + if (mainHandler == null) { + initContextAndHandler() + } + + val handler = mainHandler + if (handler == null) { + log("Main handler unavailable.") + return + } + + handler.post { + try { + val currentContext = context + if (currentContext == null) { + log("Context unavailable, cannot kill app.") + return@post + } + + val activityManager = + currentContext.getSystemService(Context.ACTIVITY_SERVICE) as? ActivityManager + if (activityManager == null) { + log("ActivityManager unavailable.") + return@post + } + + val defaultLauncher = getDefaultLauncherPackageName() + val foregroundApp = getTopPackageName() + if (foregroundApp != null && !shouldSkipPackage(foregroundApp, defaultLauncher)) { + val appLabel = getApplicationLabel(foregroundApp, currentContext.packageManager) + log("Killing: $foregroundApp ($appLabel)") + callMethod( + activityManager, + "forceStopPackage", + arrayOf(String::class.java), + foregroundApp, + ) + showToast("Killed: $appLabel") + } else { + log("Nothing to kill.") + showToast("Nothing to kill.") + } + } catch (t: Throwable) { + Log.e(TAG, "Error in killForegroundApp", t) + log("Error in killForegroundApp", t) + } + } + } + + private fun getDefaultLauncherPackageName(): String? { + val currentContext = context ?: return null + val intent = Intent(Intent.ACTION_MAIN).apply { + addCategory(Intent.CATEGORY_HOME) + } + val resolveInfo = currentContext.packageManager.resolveActivity( + intent, + PackageManager.MATCH_DEFAULT_ONLY, + ) + return resolveInfo?.activityInfo?.packageName + } + + private fun getTopPackageName(): String? = getTopPackageFromActivityTaskManager() + + private fun getTopPackageFromActivityTaskManager(): String? { + return try { + val service = callStaticMethod( + findClass("android.app.ActivityTaskManager", null), + "getService", + ) ?: return null + val taskInfo = callMethod(service, "getFocusedRootTaskInfo") + extractPackageFromTaskInfo(taskInfo) + } catch (t: Throwable) { + log("ActivityTaskManager.getFocusedRootTaskInfo failed: ${t.javaClass.simpleName}") + null + } + } + + private fun extractPackageFromTaskInfo(taskInfo: Any?): String? { + if (taskInfo == null) { + return null + } + + val pkg = getPackageNameFromComponent(getFieldValue(taskInfo, "topActivity")) + return pkg ?: getPackageNameFromActivityInfo(getFieldValue(taskInfo, "topActivityInfo")) + } + + private fun getFieldValue(target: Any?, fieldName: String): Any? { + if (target == null) { + return null + } + return try { + findField(target.javaClass, fieldName).get(target) + } catch (_: Throwable) { + null + } + } + + private fun getIntField(target: Any?, fieldName: String): Int { + val value = getFieldValue(target, fieldName) + return value as? Int ?: Int.MIN_VALUE + } + + private fun getPackageNameFromComponent(component: Any?): String? { + return (component as? ComponentName)?.packageName + } + + private fun getPackageNameFromActivityInfo(info: Any?): String? { + if (info == null) { + return null + } + return try { + findField(info.javaClass, "packageName").get(info)?.toString() + } catch (_: Throwable) { + null + } + } + + private fun shouldSkipPackage(packageName: String?, defaultLauncher: String?): Boolean { + return packageName == null || + packageName == SYSTEMUI_PACKAGE || + packageName == SAMSUNG_LAUNCHER_PACKAGE || + packageName == defaultLauncher + } + + private fun getApplicationLabel(packageName: String, packageManager: PackageManager): String { + return try { + val appInfo: ApplicationInfo = packageManager.getApplicationInfo(packageName, 0) + packageManager.getApplicationLabel(appInfo).toString() + } catch (_: PackageManager.NameNotFoundException) { + packageName + } + } + + private fun showToast(message: String) { + val currentContext = context ?: return + try { + Toast.makeText(currentContext, message, Toast.LENGTH_SHORT).show() + } catch (t: Throwable) { + log("Toast failed: ${t.javaClass.simpleName}") + } + } + + private fun log(message: String) { + log(Log.INFO, TAG, message) + } + + private fun log(message: String, throwable: Throwable) { + log(Log.ERROR, TAG, message, throwable) + } + + private fun deoptimizeMethodIfPresent(method: Method?) { + if (method != null) { + deoptimize(method) + } + } + + private fun prepareLongPressHook(runMethod: Method) { + deoptimizeMethodIfPresent( + findMethodIfExists(Handler::class.java, "dispatchMessage", android.os.Message::class.java), + ) + deoptimizeMethodIfPresent( + findMethodIfExists(Handler::class.java, "handleCallback", android.os.Message::class.java), + ) + deoptimizeMethodIfPresent(runMethod) + } + + private fun findClass(className: String, classLoader: ClassLoader?): Class<*> { + return Class.forName(className, false, classLoader) + } + + private fun findClassIfExists(className: String, classLoader: ClassLoader?): Class<*>? { + return try { + findClass(className, classLoader) + } catch (_: Throwable) { + null + } + } + + private fun findMethodIfExists( + clazz: Class<*>, + methodName: String, + vararg parameterTypes: Class<*>, + ): Method? { + return try { + findMethod(clazz, methodName, *parameterTypes) + } catch (_: Throwable) { + null + } + } + + @Throws(NoSuchMethodException::class) + private fun findMethod( + clazz: Class<*>, + methodName: String, + vararg parameterTypes: Class<*>, + ): Method { + var current: Class<*>? = clazz + while (current != null) { + try { + return current.getDeclaredMethod(methodName, *parameterTypes).apply { + isAccessible = true + } + } catch (_: NoSuchMethodException) { + current = current.superclass + } + } + throw NoSuchMethodException("${clazz.name}#$methodName") + } + + @Throws(NoSuchFieldException::class) + private fun findField(clazz: Class<*>, fieldName: String): Field { + var current: Class<*>? = clazz + while (current != null) { + try { + return current.getDeclaredField(fieldName).apply { + isAccessible = true + } + } catch (_: NoSuchFieldException) { + current = current.superclass + } + } + throw NoSuchFieldException("${clazz.name}#$fieldName") + } + + @Throws(ReflectiveOperationException::class) + private fun callStaticMethod(clazz: Class<*>, methodName: String): Any? { + return findMethod(clazz, methodName).invoke(null) + } + + @Throws(ReflectiveOperationException::class) + private fun callMethod(target: Any, methodName: String): Any? { + return callMethod(target, methodName, emptyArray()) + } + + @Throws(ReflectiveOperationException::class) + private fun callMethod( + target: Any, + methodName: String, + parameterTypes: Array>, + vararg args: Any?, + ): Any? { + 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 TAG = "NavButtons" + const val SYSTEMUI_PACKAGE = "com.android.systemui" + const val SAMSUNG_LAUNCHER_PACKAGE = "com.sec.android.app.launcher" + const val HONEYSPACE_NAV_BUTTON_VIEW = + "com.honeyspace.ui.honeypots.taskbar.presentation.NavigationBarKeyButtonView" + const val HONEYSPACE_LONG_PRESS_RUNNABLE = "f.a" + const val HONEYSPACE_LONG_PRESS_CASE = 8 + } +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 028cf62..ae72fff 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,4 +1,4 @@ - KillAppNow + NavButtons Kill foreground app on "Back" button long-press. - \ No newline at end of file + diff --git a/app/src/main/resources/META-INF/xposed/java_init.list b/app/src/main/resources/META-INF/xposed/java_init.list index 1a0cb86..cc33467 100644 --- a/app/src/main/resources/META-INF/xposed/java_init.list +++ b/app/src/main/resources/META-INF/xposed/java_init.list @@ -1 +1 @@ -se.ajpanton.killappnow.KillAppNow +se.ajpanton.navbuttons.NavButtons diff --git a/settings.gradle.kts b/settings.gradle.kts index 529f072..295208d 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -13,5 +13,5 @@ dependencyResolutionManagement { } } -rootProject.name = "KillAppNow" +rootProject.name = "NavButtons" include(":app")