Rename project to NavButtons and convert module to Kotlin
This commit is contained in:
@@ -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 {
|
||||
|
||||
Vendored
+2
-2
@@ -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 <init>();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<Class<*>>,
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
<resources>
|
||||
<string name="app_name">KillAppNow</string>
|
||||
<string name="app_name">NavButtons</string>
|
||||
<string name="xposed_desc">Kill foreground app on "Back" button long-press.</string>
|
||||
</resources>
|
||||
</resources>
|
||||
|
||||
@@ -1 +1 @@
|
||||
se.ajpanton.killappnow.KillAppNow
|
||||
se.ajpanton.navbuttons.NavButtons
|
||||
|
||||
Reference in New Issue
Block a user