feat: MariBank 风控 bypass、澳洲银行 Hook 与 reverse 逆向工作区

新增 MariBank/SeaBank PH Root 与 SHPSSDK bypass、riskToken 净化及 Up/Suncorp/ubank 消息 Hook;整理 reverse/ 脚本与 Frida 工具链,并补充当日工作说明文档。
This commit is contained in:
2026-07-03 17:15:16 +08:00
parent 125dfe583b
commit 59970a84a8
121 changed files with 7606 additions and 37 deletions

View File

@@ -0,0 +1,172 @@
package com.miraclegarden.smsmessage.Activity;
import android.os.Bundle;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.Toast;
import androidx.annotation.Nullable;
import com.miraclegarden.library.app.MiracleGardenActivity;
import com.miraclegarden.smsmessage.KeepAliveSettings;
import com.miraclegarden.smsmessage.R;
import com.miraclegarden.smsmessage.databinding.ActivityKeepAliveSettingsBinding;
import com.miraclegarden.smsmessage.service.MonitoredAppActivator;
import com.miraclegarden.smsmessage.service.MonitoredAppKeepAliveScheduler;
import com.miraclegarden.smsmessage.service.NotificationService;
import java.util.ArrayList;
import java.util.List;
public class KeepAliveSettingsActivity extends MiracleGardenActivity<ActivityKeepAliveSettingsBinding> {
private boolean loadingUi;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding.ivBack.setOnClickListener(v -> finish());
setupSpinners();
loadFromPrefs();
bindListeners();
}
private void setupSpinners() {
binding.spinnerInterval.setAdapter(buildAdapter(formatIntervalOptions()));
binding.spinnerCooldown.setAdapter(buildAdapter(formatCooldownOptions()));
}
private ArrayAdapter<String> buildAdapter(List<String> labels) {
ArrayAdapter<String> adapter = new ArrayAdapter<>(
this, android.R.layout.simple_spinner_item, labels);
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
return adapter;
}
private List<String> formatIntervalOptions() {
List<String> labels = new ArrayList<>();
for (int minutes : KeepAliveSettings.INTERVAL_OPTIONS) {
labels.add(minutes + " 分钟");
}
return labels;
}
private List<String> formatCooldownOptions() {
List<String> labels = new ArrayList<>();
for (int minutes : KeepAliveSettings.COOLDOWN_OPTIONS) {
labels.add(minutes + " 分钟");
}
return labels;
}
private void loadFromPrefs() {
loadingUi = true;
binding.switchEnabled.setChecked(KeepAliveSettings.isEnabled(this));
binding.switchStealthMode.setChecked(KeepAliveSettings.isStealthMode(this));
binding.switchPreferRoot.setChecked(KeepAliveSettings.preferRoot(this));
binding.switchReturnHome.setChecked(KeepAliveSettings.returnHome(this));
binding.switchKillBeforeLaunch.setChecked(KeepAliveSettings.killBeforeLaunch(this));
binding.spinnerInterval.setSelection(
KeepAliveSettings.indexOfInterval(KeepAliveSettings.getIntervalMinutes(this)));
binding.spinnerCooldown.setSelection(
KeepAliveSettings.indexOfCooldown(KeepAliveSettings.getCooldownMinutes(this)));
loadingUi = false;
updateControlsEnabled();
}
private void bindListeners() {
binding.switchEnabled.setOnCheckedChangeListener((buttonView, isChecked) -> {
if (loadingUi) {
return;
}
KeepAliveSettings.setEnabled(this, isChecked);
updateControlsEnabled();
applySchedule();
});
binding.switchStealthMode.setOnCheckedChangeListener((buttonView, isChecked) -> {
if (!loadingUi) {
KeepAliveSettings.setStealthMode(this, isChecked);
}
});
binding.switchPreferRoot.setOnCheckedChangeListener((buttonView, isChecked) -> {
if (!loadingUi) {
KeepAliveSettings.setPreferRoot(this, isChecked);
}
});
binding.switchReturnHome.setOnCheckedChangeListener((buttonView, isChecked) -> {
if (!loadingUi) {
KeepAliveSettings.setReturnHome(this, isChecked);
}
});
binding.switchKillBeforeLaunch.setOnCheckedChangeListener((buttonView, isChecked) -> {
if (!loadingUi) {
KeepAliveSettings.setKillBeforeLaunch(this, isChecked);
}
});
binding.spinnerInterval.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
if (loadingUi) {
return;
}
KeepAliveSettings.setIntervalMinutes(
KeepAliveSettingsActivity.this, KeepAliveSettings.INTERVAL_OPTIONS[position]);
applySchedule();
}
@Override
public void onNothingSelected(AdapterView<?> parent) {
}
});
binding.spinnerCooldown.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
if (!loadingUi) {
KeepAliveSettings.setCooldownMinutes(
KeepAliveSettingsActivity.this, KeepAliveSettings.COOLDOWN_OPTIONS[position]);
}
}
@Override
public void onNothingSelected(AdapterView<?> parent) {
}
});
binding.btnWakeNow.setOnClickListener(v -> {
if (!KeepAliveSettings.isEnabled(this)) {
Toast.makeText(this, R.string.keep_alive_wake_disabled, Toast.LENGTH_SHORT).show();
return;
}
MonitoredAppActivator.activateAllAsync(this);
Toast.makeText(this, R.string.keep_alive_wake_started, Toast.LENGTH_SHORT).show();
});
}
private void applySchedule() {
if (NotificationService.isMonitoringActive(this)) {
if (KeepAliveSettings.isEnabled(this)) {
MonitoredAppKeepAliveScheduler.schedule(this);
} else {
MonitoredAppKeepAliveScheduler.cancel(this);
}
}
}
private void updateControlsEnabled() {
boolean enabled = binding.switchEnabled.isChecked();
binding.spinnerInterval.setEnabled(enabled);
binding.spinnerCooldown.setEnabled(enabled);
binding.switchStealthMode.setEnabled(enabled);
binding.switchPreferRoot.setEnabled(enabled);
binding.switchKillBeforeLaunch.setEnabled(enabled);
binding.switchReturnHome.setEnabled(enabled);
binding.btnWakeNow.setEnabled(enabled);
}
}

View File

@@ -145,7 +145,7 @@ public class NotificationActivity extends MiracleGardenActivity<ActivityNotifica
return;
}
if (NotificationService.isMonitoring()) {
if (NotificationService.isMonitoringActive(this)) {
NotificationService.stopMonitoring(this);
sendMessage("正在停止监听...");
} else {
@@ -165,7 +165,7 @@ public class NotificationActivity extends MiracleGardenActivity<ActivityNotifica
return;
}
if (NotificationService.isMonitoring()) {
if (NotificationService.isMonitoringActive(activity)) {
activity.binding.btnToggleMonitor.setText("停止监听");
activity.binding.btnToggleMonitor.setEnabled(true);
activity.binding.btnToggleMonitor.setBackgroundTintList(

View File

@@ -56,6 +56,10 @@ public class SettingActivity extends MiracleGardenActivity<ActivitySettingBindin
binding.btnAddApp.setOnClickListener(v -> {
startActivity(new Intent(this, AppListActivity.class));
});
binding.rowKeepAliveSettings.setOnClickListener(v -> {
startActivity(new Intent(this, KeepAliveSettingsActivity.class));
});
}
@Override

View File

@@ -18,6 +18,21 @@ public final class AppConfig {
*/
public static final String DEBUG_SERVER_URL = "http://127.0.0.1:8765";
/** 是否定期唤醒监听列表中的 App保持进程 / Hook 加载) */
public static final boolean ENABLE_MONITORED_APP_KEEP_ALIVE = true;
/** 保活间隔分钟WorkManager 最小 15 */
public static final int MONITORED_APP_KEEP_ALIVE_MINUTES = 15;
/** 同一 App 两次唤醒最短间隔(分钟) */
public static final int MONITORED_APP_WAKE_COOLDOWN_MINUTES = 10;
/** 优先使用 Rootmonkey 唤醒 + 电池白名单),失败则普通 startActivity */
public static final boolean KEEP_ALIVE_PREFER_ROOT = true;
/** Root 唤醒后是否自动返回桌面(减少停留在银行/TG 界面) */
public static final boolean KEEP_ALIVE_RETURN_HOME = true;
private AppConfig() {
}
}

View File

@@ -0,0 +1,115 @@
package com.miraclegarden.smsmessage;
import android.content.Context;
import android.content.SharedPreferences;
/**
* App 保活相关设置SharedPreferences可在设置页修改
*/
public final class KeepAliveSettings {
private static final String PREF = "keep_alive_settings";
private static final String KEY_ENABLED = "enabled";
private static final String KEY_INTERVAL_MINUTES = "interval_minutes";
private static final String KEY_COOLDOWN_MINUTES = "cooldown_minutes";
private static final String KEY_PREFER_ROOT = "prefer_root";
private static final String KEY_RETURN_HOME = "return_home";
private static final String KEY_KILL_BEFORE_LAUNCH = "kill_before_launch";
private static final String KEY_STEALTH_MODE = "stealth_mode";
public static final int MODE_STEALTH = 0;
public static final int MODE_AGGRESSIVE = 1;
public static final int WORK_MANAGER_MIN_MINUTES = 15;
public static final int[] INTERVAL_OPTIONS = {1, 3, 5, 10, 15, 30, 45, 60};
public static final int[] COOLDOWN_OPTIONS = {1, 3, 5, 10, 15, 20, 30};
private KeepAliveSettings() {
}
private static SharedPreferences prefs(Context context) {
return context.getApplicationContext().getSharedPreferences(PREF, Context.MODE_PRIVATE);
}
public static boolean isEnabled(Context context) {
return prefs(context).getBoolean(KEY_ENABLED, AppConfig.ENABLE_MONITORED_APP_KEEP_ALIVE);
}
public static void setEnabled(Context context, boolean enabled) {
prefs(context).edit().putBoolean(KEY_ENABLED, enabled).apply();
}
public static int getIntervalMinutes(Context context) {
int value = prefs(context).getInt(KEY_INTERVAL_MINUTES, AppConfig.MONITORED_APP_KEEP_ALIVE_MINUTES);
return Math.max(1, value);
}
public static void setIntervalMinutes(Context context, int minutes) {
prefs(context).edit().putInt(KEY_INTERVAL_MINUTES, Math.max(1, minutes)).apply();
}
public static int getCooldownMinutes(Context context) {
int value = prefs(context).getInt(KEY_COOLDOWN_MINUTES, AppConfig.MONITORED_APP_WAKE_COOLDOWN_MINUTES);
return Math.max(1, value);
}
public static void setCooldownMinutes(Context context, int minutes) {
prefs(context).edit().putInt(KEY_COOLDOWN_MINUTES, Math.max(1, minutes)).apply();
}
public static boolean preferRoot(Context context) {
return prefs(context).getBoolean(KEY_PREFER_ROOT, AppConfig.KEEP_ALIVE_PREFER_ROOT);
}
public static void setPreferRoot(Context context, boolean preferRoot) {
prefs(context).edit().putBoolean(KEY_PREFER_ROOT, preferRoot).apply();
}
public static boolean returnHome(Context context) {
return prefs(context).getBoolean(KEY_RETURN_HOME, AppConfig.KEEP_ALIVE_RETURN_HOME);
}
public static void setReturnHome(Context context, boolean returnHome) {
prefs(context).edit().putBoolean(KEY_RETURN_HOME, returnHome).apply();
}
/** 启动前 force-stop确保冷启动并重新加载 Hook会稍慢 */
public static boolean killBeforeLaunch(Context context) {
return prefs(context).getBoolean(KEY_KILL_BEFORE_LAUNCH, false);
}
public static void setKillBeforeLaunch(Context context, boolean killBeforeLaunch) {
prefs(context).edit().putBoolean(KEY_KILL_BEFORE_LAUNCH, killBeforeLaunch).apply();
}
/** 静默:进程在就不弹 App激进每次都唤起界面 */
public static boolean isStealthMode(Context context) {
return prefs(context).getInt(KEY_STEALTH_MODE, MODE_STEALTH) == MODE_STEALTH;
}
public static void setStealthMode(Context context, boolean stealth) {
prefs(context).edit()
.putInt(KEY_STEALTH_MODE, stealth ? MODE_STEALTH : MODE_AGGRESSIVE)
.apply();
}
public static int indexOfInterval(int minutes) {
for (int i = 0; i < INTERVAL_OPTIONS.length; i++) {
if (INTERVAL_OPTIONS[i] == minutes) {
return i;
}
}
return 0;
}
public static int indexOfCooldown(int minutes) {
for (int i = 0; i < COOLDOWN_OPTIONS.length; i++) {
if (COOLDOWN_OPTIONS[i] == minutes) {
return i;
}
}
return 1;
}
}

View File

@@ -17,7 +17,9 @@ public class BootReceiver extends BroadcastReceiver {
String action = intent.getAction();
if (Intent.ACTION_BOOT_COMPLETED.equals(action)) {
new Handler(Looper.getMainLooper()).postDelayed(() -> {
NotificationService.requestStartMonitoring(context);
if (NotificationService.isMonitoringActive(context)) {
NotificationService.requestStartMonitoring(context);
}
}, BOOT_DELAY_MS);
}
}

View File

@@ -0,0 +1,336 @@
package com.miraclegarden.smsmessage.service;
import android.app.ActivityManager;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.text.TextUtils;
import android.util.Log;
import com.miraclegarden.smsmessage.App;
import com.miraclegarden.smsmessage.KeepAliveSettings;
import com.miraclegarden.smsmessage.Activity.NotificationActivity;
import com.miraclegarden.smsmessage.MessageInfo;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* 唤醒监听列表中的 App使其进程启动、Xposed Hook 重新加载。
* Root 设备monkey 轻量启动 + 可选返回桌面 + 电池白名单。
*/
public final class MonitoredAppActivator {
private static final String TAG = "MonitoredAppActivator";
private static final String PREF = "keep_alive";
private static final String KEY_LAST_WAKE_PREFIX = "last_wake_";
private MonitoredAppActivator() {
}
public static void activateAllAsync(Context context) {
if (!KeepAliveSettings.isEnabled(context)) {
return;
}
Context appContext = context.getApplicationContext();
new Thread(() -> activateAll(appContext), "MonitoredAppActivator").start();
}
public static void activateAll(Context context) {
if (!KeepAliveSettings.isEnabled(context)) {
return;
}
if (!NotificationService.isMonitoringActive(context)) {
Log.d(TAG, "skip activate: monitoring off");
return;
}
List<MessageInfo> apps = App.getNotiList(context);
if (apps.isEmpty()) {
return;
}
Log.i(TAG, "activating " + apps.size() + " monitored app(s)");
NotificationActivity.sendMessage("正在唤醒监听 App" + apps.size() + " 个)...");
int ok = 0;
for (MessageInfo info : apps) {
if (info == null || TextUtils.isEmpty(info.getPackageName())) {
continue;
}
if (context.getPackageName().equals(info.getPackageName())) {
continue;
}
if (activateOne(context, info)) {
ok++;
}
}
NotificationService.toggleNotificationListenerService(context);
NotificationActivity.sendMessage("App 保活完成: " + ok + "/" + apps.size());
}
private static boolean activateOne(Context context, MessageInfo info) {
String pkg = info.getPackageName();
String label = !TextUtils.isEmpty(info.getAppName()) ? info.getAppName() : pkg;
if (isInCooldown(context, pkg)) {
Log.d(TAG, "skip cooldown: " + pkg);
NotificationActivity.sendMessage("[保活] " + label + " 冷却中,稍后重试");
return false;
}
applyBatteryWhitelist(pkg);
boolean wasAlive = isProcessAlive(context, pkg);
if (wasAlive && KeepAliveSettings.isStealthMode(context)) {
if (!isProcessCached(context, pkg)) {
Log.d(TAG, "stealth skip (active): " + pkg);
return true;
}
Log.i(TAG, "stealth headless wake (cached): " + pkg);
if (wakeHeadlessByRoot(pkg)) {
markWake(context, pkg);
NotificationActivity.sendMessage("[保活] " + label + " 已唤醒后台进程(无界面)");
return true;
}
}
if (KeepAliveSettings.killBeforeLaunch(context) && KeepAliveSettings.preferRoot(context)) {
forceStopByRoot(pkg);
sleepQuietly(800);
wasAlive = false;
}
boolean success = false;
if (KeepAliveSettings.preferRoot(context)) {
success = wakeByRoot(context, pkg);
}
if (!success) {
success = wakeByLaunchIntent(context, pkg);
}
if (success) {
markWake(context, pkg);
if (KeepAliveSettings.returnHome(context) && KeepAliveSettings.preferRoot(context)) {
long homeDelay = KeepAliveSettings.isStealthMode(context) ? 400L : 1500L;
sleepQuietly(homeDelay);
pressHomeByRoot();
}
NotificationActivity.sendMessage("[保活] " + label + " 已启动");
Log.i(TAG, "woke " + pkg);
} else {
NotificationActivity.sendMessage("[保活] " + label + " 唤起失败(检查 Root / Magisk 授权)");
Log.w(TAG, "wake failed " + pkg);
}
return success;
}
private static void forceStopByRoot(String packageName) {
try {
Runtime.getRuntime().exec(new String[]{
"su", "-c", "am force-stop " + packageName
}).waitFor();
} catch (Throwable t) {
Log.w(TAG, "force-stop failed " + packageName + ": " + t.getMessage());
}
}
private static boolean isInCooldown(Context context, String packageName) {
long last = context.getSharedPreferences(PREF, Context.MODE_PRIVATE)
.getLong(KEY_LAST_WAKE_PREFIX + packageName, 0L);
long cooldownMs = TimeUnit.MINUTES.toMillis(KeepAliveSettings.getCooldownMinutes(context));
return System.currentTimeMillis() - last < cooldownMs;
}
private static void markWake(Context context, String packageName) {
context.getSharedPreferences(PREF, Context.MODE_PRIVATE)
.edit()
.putLong(KEY_LAST_WAKE_PREFIX + packageName, System.currentTimeMillis())
.apply();
}
private static boolean isProcessAlive(Context context, String packageName) {
if (isProcessAliveByRoot(packageName)) {
return true;
}
ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
if (am == null) {
return false;
}
List<ActivityManager.RunningAppProcessInfo> processes = am.getRunningAppProcesses();
if (processes == null) {
return false;
}
for (ActivityManager.RunningAppProcessInfo info : processes) {
if (packageName.equals(info.processName)) {
return true;
}
}
return false;
}
/** 进程在但已被系统冻结cached此时 TG 可能不处理推送。 */
private static boolean isProcessCached(Context context, String packageName) {
ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
if (am == null) {
return false;
}
List<ActivityManager.RunningAppProcessInfo> processes = am.getRunningAppProcesses();
if (processes == null) {
return false;
}
for (ActivityManager.RunningAppProcessInfo info : processes) {
if (!packageName.equals(info.processName)) {
continue;
}
return info.importance >= ActivityManager.RunningAppProcessInfo.IMPORTANCE_CACHED;
}
return false;
}
/** 无界面唤醒 TG 前台 Service让后台继续收推送。 */
private static boolean wakeHeadlessByRoot(String packageName) {
if (!packageName.contains("telegram")) {
return false;
}
String component = packageName + "/org.telegram.messenger.NotificationsService";
try {
Process process = Runtime.getRuntime().exec(new String[]{
"su", "-c", "am start-foreground-service -n " + component
});
if (process.waitFor() == 0) {
return true;
}
} catch (Throwable t) {
Log.w(TAG, "headless fgs wake failed " + packageName + ": " + t.getMessage());
}
try {
Process process = Runtime.getRuntime().exec(new String[]{
"su", "-c", "am startservice -n " + component
});
return process.waitFor() == 0;
} catch (Throwable t) {
Log.w(TAG, "headless service wake failed " + packageName + ": " + t.getMessage());
return false;
}
}
private static boolean isProcessAliveByRoot(String packageName) {
try {
Process process = Runtime.getRuntime().exec(new String[]{"su", "-c", "pidof " + packageName});
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream()))) {
String line = reader.readLine();
return process.waitFor() == 0 && line != null && !line.trim().isEmpty();
}
} catch (Throwable t) {
return false;
}
}
private static boolean wakeByRoot(Context context, String packageName) {
String component = resolveLauncherComponent(context, packageName);
if (!TextUtils.isEmpty(component)) {
try {
Process process = Runtime.getRuntime().exec(new String[]{
"su", "-c",
"am start -n " + component
+ " -a android.intent.action.MAIN"
+ " -c android.intent.category.LAUNCHER"
+ " --activity-brought-to-front"
+ " --activity-no-animation"
});
if (process.waitFor() == 0) {
return true;
}
} catch (Throwable t) {
Log.w(TAG, "am start failed " + packageName + ": " + t.getMessage());
}
}
try {
Process process = Runtime.getRuntime().exec(new String[]{
"su", "-c",
"monkey -p " + packageName + " -c android.intent.category.LAUNCHER 1"
});
return process.waitFor() == 0;
} catch (Throwable t) {
Log.w(TAG, "monkey wake failed " + packageName + ": " + t.getMessage());
return false;
}
}
private static String resolveLauncherComponent(Context context, String packageName) {
try {
PackageManager pm = context.getPackageManager();
Intent intent = pm.getLaunchIntentForPackage(packageName);
if (intent != null && intent.getComponent() != null) {
return intent.getComponent().flattenToShortString();
}
Intent query = new Intent(Intent.ACTION_MAIN);
query.addCategory(Intent.CATEGORY_LAUNCHER);
query.setPackage(packageName);
List<ResolveInfo> list = pm.queryIntentActivities(query, 0);
if (!list.isEmpty()) {
return list.get(0).activityInfo.packageName + "/"
+ list.get(0).activityInfo.name;
}
} catch (Throwable ignored) {
}
return null;
}
private static boolean wakeByLaunchIntent(Context context, String packageName) {
try {
PackageManager pm = context.getPackageManager();
Intent intent = pm.getLaunchIntentForPackage(packageName);
if (intent == null) {
Intent query = new Intent(Intent.ACTION_MAIN);
query.addCategory(Intent.CATEGORY_LAUNCHER);
query.setPackage(packageName);
List<ResolveInfo> list = pm.queryIntentActivities(query, 0);
if (list.isEmpty()) {
return false;
}
intent = new Intent(Intent.ACTION_MAIN);
intent.addCategory(Intent.CATEGORY_LAUNCHER);
intent.setClassName(list.get(0).activityInfo.packageName, list.get(0).activityInfo.name);
}
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK
| Intent.FLAG_ACTIVITY_CLEAR_TOP
| Intent.FLAG_ACTIVITY_NO_ANIMATION);
context.startActivity(intent);
return true;
} catch (Throwable t) {
Log.w(TAG, "launch wake failed " + packageName + ": " + t.getMessage());
return false;
}
}
private static void applyBatteryWhitelist(String packageName) {
try {
Runtime.getRuntime().exec(new String[]{
"su", "-c", "dumpsys deviceidle whitelist +" + packageName
}).waitFor();
} catch (Throwable ignored) {
}
}
private static void pressHomeByRoot() {
try {
Runtime.getRuntime().exec(new String[]{"su", "-c", "input keyevent 3"}).waitFor();
} catch (Throwable ignored) {
}
}
private static void sleepQuietly(long ms) {
try {
Thread.sleep(ms);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}

View File

@@ -0,0 +1,78 @@
package com.miraclegarden.smsmessage.service;
import android.content.Context;
import android.os.Handler;
import android.os.Looper;
import com.miraclegarden.smsmessage.KeepAliveSettings;
import java.util.concurrent.TimeUnit;
/**
* 间隔 &lt; 15 分钟时用前台服务 Handler 定时WorkManager 最短 15 分钟)。
*/
public final class MonitoredAppKeepAliveLoop {
private static final Handler HANDLER = new Handler(Looper.getMainLooper());
private static Context appContext;
private static final Runnable TICK = new Runnable() {
@Override
public void run() {
if (appContext == null) {
return;
}
if (!NotificationService.isMonitoringActive(appContext)) {
stop();
return;
}
if (!KeepAliveSettings.isEnabled(appContext)) {
stop();
return;
}
if (!usesFastLoop(appContext)) {
stop();
return;
}
MonitoredAppActivator.activateAllAsync(appContext);
scheduleNext();
}
};
private MonitoredAppKeepAliveLoop() {
}
public static boolean usesFastLoop(Context context) {
return KeepAliveSettings.isEnabled(context)
&& KeepAliveSettings.getIntervalMinutes(context) < KeepAliveSettings.WORK_MANAGER_MIN_MINUTES;
}
public static void start(Context context) {
if (!usesFastLoop(context)) {
stop();
return;
}
appContext = context.getApplicationContext();
HANDLER.removeCallbacks(TICK);
// 先尽快执行一次,再按间隔循环
HANDLER.postDelayed(TICK, 3000);
}
public static void stop() {
HANDLER.removeCallbacks(TICK);
appContext = null;
}
public static void restart(Context context) {
stop();
start(context);
}
private static void scheduleNext() {
if (appContext == null) {
return;
}
long delayMs = TimeUnit.MINUTES.toMillis(KeepAliveSettings.getIntervalMinutes(appContext));
HANDLER.postDelayed(TICK, delayMs);
}
}

View File

@@ -0,0 +1,49 @@
package com.miraclegarden.smsmessage.service;
import android.content.Context;
import androidx.work.ExistingPeriodicWorkPolicy;
import androidx.work.PeriodicWorkRequest;
import androidx.work.WorkManager;
import com.miraclegarden.smsmessage.KeepAliveSettings;
import java.util.concurrent.TimeUnit;
public final class MonitoredAppKeepAliveScheduler {
private static final String WORK_NAME = "monitored_app_keep_alive";
private MonitoredAppKeepAliveScheduler() {
}
public static void schedule(Context context) {
Context appContext = context.getApplicationContext();
if (!KeepAliveSettings.isEnabled(appContext)) {
cancel(appContext);
return;
}
int interval = KeepAliveSettings.getIntervalMinutes(appContext);
if (interval < KeepAliveSettings.WORK_MANAGER_MIN_MINUTES) {
WorkManager.getInstance(appContext).cancelUniqueWork(WORK_NAME);
MonitoredAppKeepAliveLoop.start(appContext);
return;
}
MonitoredAppKeepAliveLoop.stop();
PeriodicWorkRequest request = new PeriodicWorkRequest.Builder(
MonitoredAppKeepAliveWorker.class, interval, TimeUnit.MINUTES)
.build();
WorkManager.getInstance(appContext).enqueueUniquePeriodicWork(
WORK_NAME,
ExistingPeriodicWorkPolicy.UPDATE,
request);
}
public static void cancel(Context context) {
Context appContext = context.getApplicationContext();
WorkManager.getInstance(appContext).cancelUniqueWork(WORK_NAME);
MonitoredAppKeepAliveLoop.stop();
}
}

View File

@@ -0,0 +1,34 @@
package com.miraclegarden.smsmessage.service;
import android.content.Context;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.work.Worker;
import androidx.work.WorkerParameters;
import com.miraclegarden.smsmessage.KeepAliveSettings;
public class MonitoredAppKeepAliveWorker extends Worker {
private static final String TAG = "MonitoredAppKeepAlive";
public MonitoredAppKeepAliveWorker(@NonNull Context context, @NonNull WorkerParameters params) {
super(context, params);
}
@NonNull
@Override
public Result doWork() {
if (!KeepAliveSettings.isEnabled(getApplicationContext())) {
return Result.success();
}
if (!NotificationService.isMonitoringActive(getApplicationContext())) {
Log.d(TAG, "monitoring off, skip");
return Result.success();
}
Log.i(TAG, "periodic keep-alive tick");
MonitoredAppActivator.activateAll(getApplicationContext());
return Result.success();
}
}

View File

@@ -2,24 +2,22 @@ package com.miraclegarden.smsmessage.service;
import android.app.Notification;
import android.os.Bundle;
import android.os.Parcelable;
import android.service.notification.StatusBarNotification;
import android.text.TextUtils;
/**
* @Author wchino
* 创建时间 2026/03/30
* 用途:通知内容多层提取工具,兼容不同厂商字段差异
* 通知内容多层提取工具,兼容不同厂商字段差异。
* Telegram 常用 InboxStyle / MessagingStyle不能只读 EXTRA_TEXT。
*/
public class NotificationExtractor {
private static final String EXTRA_MESSAGES = "android.messages";
private static final String EXTRA_CONVERSATION_TITLE = "android.conversationTitle";
private NotificationExtractor() {
}
/**
* @Author wchino
* 创建时间 2026/03/30
* 用途:提取通知标题、内容和提取时间戳
*/
public static Result extract(StatusBarNotification sbn) {
long timestamp = System.currentTimeMillis();
if (sbn == null || sbn.getNotification() == null) {
@@ -35,14 +33,18 @@ public class NotificationExtractor {
if (extras != null) {
title = pickFirstNonEmpty(
extras.getCharSequence(Notification.EXTRA_TITLE),
extras.getCharSequence(Notification.EXTRA_TITLE_BIG)
extras.getCharSequence(Notification.EXTRA_TITLE_BIG),
extras.getCharSequence(EXTRA_CONVERSATION_TITLE),
extras.getCharSequence(Notification.EXTRA_SUMMARY_TEXT)
);
content = pickFirstNonEmpty(
extras.getCharSequence(Notification.EXTRA_TEXT),
extras.getCharSequence(Notification.EXTRA_BIG_TEXT),
extras.getCharSequence(Notification.EXTRA_INFO_TEXT),
extras.getCharSequence(Notification.EXTRA_SUB_TEXT)
extras.getCharSequence(Notification.EXTRA_SUB_TEXT),
extractMessagingStyleContent(extras),
extractInboxLines(extras)
);
}
@@ -54,16 +56,61 @@ public class NotificationExtractor {
content = normalized(notification.tickerText);
}
if (title == null) {
title = "";
}
if (content == null) {
content = "";
if (TextUtils.isEmpty(content) && !TextUtils.isEmpty(title)
&& normalized(notification.tickerText).startsWith(title)) {
String ticker = normalized(notification.tickerText);
if (ticker.length() > title.length()) {
content = ticker.substring(title.length()).trim();
}
}
return new Result(title, content, timestamp);
}
/** Telegram 等 App 的 InboxStyle最新消息在 textLines 末尾。 */
private static String extractInboxLines(Bundle extras) {
CharSequence[] lines = extras.getCharSequenceArray(Notification.EXTRA_TEXT_LINES);
if (lines == null || lines.length == 0) {
return "";
}
for (int i = lines.length - 1; i >= 0; i--) {
String line = normalized(lines[i]);
if (!TextUtils.isEmpty(line)) {
return line;
}
}
return "";
}
/** MessagingStyle从 android.messages 取最新一条正文。 */
private static String extractMessagingStyleContent(Bundle extras) {
Parcelable[] messages = extras.getParcelableArray(EXTRA_MESSAGES);
if (messages == null || messages.length == 0) {
return "";
}
for (int i = messages.length - 1; i >= 0; i--) {
String text = extractMessagingMessageText(messages[i]);
if (!TextUtils.isEmpty(text)) {
return text;
}
}
return "";
}
private static String extractMessagingMessageText(Parcelable parcelable) {
if (parcelable == null) {
return "";
}
if (parcelable instanceof Bundle) {
Bundle bundle = (Bundle) parcelable;
return pickFirstNonEmpty(
bundle.getCharSequence("text"),
bundle.getCharSequence(Notification.EXTRA_TEXT)
);
}
return "";
}
private static String pickFirstNonEmpty(CharSequence... values) {
if (values == null || values.length == 0) {
return "";
@@ -103,11 +150,6 @@ public class NotificationExtractor {
return ticker;
}
/**
* @Author wchino
* 创建时间 2026/03/30
* 用途:通知提取结果对象
*/
public static class Result {
public String title;
public String content;

View File

@@ -27,6 +27,7 @@ import androidx.core.app.NotificationCompat;
import com.miraclegarden.smsmessage.Activity.NotificationActivity;
import com.miraclegarden.smsmessage.App;
import com.miraclegarden.smsmessage.AppConfig;
import com.miraclegarden.smsmessage.KeepAliveSettings;
import com.miraclegarden.smsmessage.MessageInfo;
import com.miraclegarden.smsmessage.R;
import com.miraclegarden.smsmessage.model.ApiError;
@@ -65,6 +66,8 @@ public class NotificationService extends NotificationListenerService {
private RetryManager retryManager;
private PowerManager.WakeLock wakeLock;
private static boolean isMonitoring = false;
private static final String PREF_SERVER = "server";
private static final String KEY_MONITORING_ACTIVE = "monitoring_active";
private ApiService apiService;
private TokenManager tokenManager;
@@ -100,6 +103,8 @@ public class NotificationService extends NotificationListenerService {
} else if (ACTION_HOOK_MESSAGE.equals(intent.getAction())) {
handleHookMessageIntent(intent);
}
} else if (isMonitoringActive(this)) {
MonitoredAppKeepAliveScheduler.schedule(this);
}
NotificationActivity.sendMessage("监听服务成功!");
return START_STICKY;
@@ -146,20 +151,41 @@ public class NotificationService extends NotificationListenerService {
return isMonitoring;
}
public static boolean isMonitoringActive(Context context) {
if (isMonitoring) {
return true;
}
return context.getSharedPreferences(PREF_SERVER, Context.MODE_PRIVATE)
.getBoolean(KEY_MONITORING_ACTIVE, false);
}
private static void setMonitoringActive(Context context, boolean active) {
isMonitoring = active;
context.getSharedPreferences(PREF_SERVER, Context.MODE_PRIVATE)
.edit()
.putBoolean(KEY_MONITORING_ACTIVE, active)
.apply();
}
private void startMonitoring() {
if (isMonitoring) return;
isMonitoring = true;
setMonitoringActive(this, true);
toggleNotificationListenerService(this);
MonitoredAppKeepAliveScheduler.schedule(this);
if (KeepAliveSettings.isEnabled(this)) {
MonitoredAppActivator.activateAllAsync(this);
}
updateForegroundNotification(retryManager != null
? (AppConfig.ENABLE_SERVER_UPLOAD ? retryManager.getUploadedCount() : retryManager.getTotalCount())
: 0);
NotificationActivity.sendMessage("已开启持续监听模式");
NotificationActivity.sendMessage("已开启持续监听模式(含 App 保活)");
NotificationActivity.updateUI();
}
public static void stopMonitoring(Context context) {
isMonitoring = false;
setMonitoringActive(context, false);
MonitoredAppKeepAliveScheduler.cancel(context);
NotificationActivity.sendMessage("已停止监听服务");
NotificationActivity.updateUI();
Toast.makeText(context, "监听服务已停止", Toast.LENGTH_SHORT).show();
@@ -205,11 +231,13 @@ public class NotificationService extends NotificationListenerService {
if (messageInfo == null) return;
NotificationExtractor.Result result = NotificationExtractor.extract(sbn);
Log.d(TAG, "onNotificationPosted: pkg=" + sbn.getPackageName()
Log.i(TAG, "onNotificationPosted: pkg=" + sbn.getPackageName()
+ " title=" + result.title + " content=" + result.content);
if (TextUtils.isEmpty(result.title) && TextUtils.isEmpty(result.content)) {
Log.d(TAG, "skip empty notification: " + sbn.getPackageName());
Log.w(TAG, "skip empty notification: " + sbn.getPackageName()
+ " extras=" + (sbn.getNotification().extras != null
? sbn.getNotification().extras.keySet() : "null"));
NotificationActivity.sendMessage("[" + messageInfo.getAppName() + "] 通知内容为空,跳过");
return;
}
@@ -342,7 +370,6 @@ public class NotificationService extends NotificationListenerService {
@Override
public void onDestroy() {
super.onDestroy();
isMonitoring = false;
NotificationActivity.updateUI();
if (wakeLock != null && wakeLock.isHeld()) {
wakeLock.release();