Rename project to NavButtons and convert module to Kotlin
This commit is contained in:
@@ -34,7 +34,7 @@ if (releaseBuildRequested && (!hasSharedReleaseSigningConfig || !hasSharedReleas
|
|||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "se.ajpanton.killappnow"
|
namespace = "se.ajpanton.navbuttons"
|
||||||
compileSdk = 36
|
compileSdk = 36
|
||||||
dependenciesInfo {
|
dependenciesInfo {
|
||||||
includeInApk = false
|
includeInApk = false
|
||||||
@@ -53,11 +53,11 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "se.ajpanton.killappnow"
|
applicationId = "se.ajpanton.navbuttons"
|
||||||
minSdk = 35
|
minSdk = 35
|
||||||
targetSdk = 35
|
targetSdk = 35
|
||||||
versionCode = 1702
|
versionCode = 1
|
||||||
versionName = "17.2"
|
versionName = "0.1"
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
@@ -73,9 +73,6 @@ android {
|
|||||||
packaging {
|
packaging {
|
||||||
resources {
|
resources {
|
||||||
merges += "META-INF/xposed/*"
|
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 {
|
compileOptions {
|
||||||
|
|||||||
Vendored
+2
-2
@@ -22,7 +22,7 @@
|
|||||||
|
|
||||||
-dontobfuscate
|
-dontobfuscate
|
||||||
|
|
||||||
-keep class se.ajpanton.killappnow.KillAppNow
|
-keep class se.ajpanton.navbuttons.NavButtons
|
||||||
-keepclassmembers class se.ajpanton.killappnow.KillAppNow {
|
-keepclassmembers class se.ajpanton.navbuttons.NavButtons {
|
||||||
public <init>();
|
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>
|
<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>
|
<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
|
||||||
|
|||||||
+1
-1
@@ -13,5 +13,5 @@ dependencyResolutionManagement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
rootProject.name = "KillAppNow"
|
rootProject.name = "NavButtons"
|
||||||
include(":app")
|
include(":app")
|
||||||
|
|||||||
Reference in New Issue
Block a user