Files
notiMessage/app/src/main/java/com/miraclegarden/smsmessage/service/NotificationService.java
Mars 125dfe583b feat: Telegram 双通道抓取、PC 调试台与 Hook 后台转发修复
v2.2.1 更新说明:

- 新增 xposed-module(Telegram/微信/SQLite Hook),双 APK + LSPosed 作用域

- HookMessageReceiver 后台直接 DebugForwarder + goAsync,修复 notiMessage 退后台丢消息

- MessageLogStore 日志持久化;AppConfig 调试/上传开关;PC 调试台 debug-server

- 健康检查去掉联网限制;通知/Hook 通道增加诊断日志

- 安装脚本 install-full/configure-lsposed/start-debug-server;文档 CHANGELOG + HOOK_GUIDE
2026-07-02 16:50:12 +08:00

407 lines
16 KiB
Java

package com.miraclegarden.smsmessage.service;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.content.pm.ServiceInfo;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.os.PowerManager;
import android.provider.Settings;
import android.service.notification.NotificationListenerService;
import android.service.notification.StatusBarNotification;
import android.text.TextUtils;
import android.util.Log;
import android.widget.Toast;
import androidx.annotation.NonNull;
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.MessageInfo;
import com.miraclegarden.smsmessage.R;
import com.miraclegarden.smsmessage.model.ApiError;
import com.miraclegarden.smsmessage.model.UploadNotificationResponse;
import com.miraclegarden.smsmessage.network.ApiService;
import com.miraclegarden.smsmessage.network.DebugForwarder;
import com.miraclegarden.smsmessage.network.TokenManager;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
public class NotificationService extends NotificationListenerService {
private static final String TAG = "NotificationService";
private static final int FOREGROUND_NOTIFICATION_ID = 9001;
private static final String CHANNEL_ID = "notification_listener_channel";
private static final MediaType JSON = MediaType.parse("application/json; charset=utf-8");
private static final String WAKELOCK_TAG = "NotificationService:WakeLock";
private static final OkHttpClient HTTP_CLIENT = new OkHttpClient.Builder()
.connectTimeout(15, TimeUnit.SECONDS)
.writeTimeout(15, TimeUnit.SECONDS)
.readTimeout(15, TimeUnit.SECONDS)
.build();
private RetryManager retryManager;
private PowerManager.WakeLock wakeLock;
private static boolean isMonitoring = false;
private ApiService apiService;
private TokenManager tokenManager;
@Override
public void onCreate() {
super.onCreate();
createNotificationChannel();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
startForeground(FOREGROUND_NOTIFICATION_ID, buildForegroundNotification(0, false), ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE);
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
startForeground(FOREGROUND_NOTIFICATION_ID, buildForegroundNotification(0, false), ServiceInfo.FOREGROUND_SERVICE_TYPE_NONE);
} else {
startForeground(FOREGROUND_NOTIFICATION_ID, buildForegroundNotification(0, false));
}
apiService = new ApiService(this);
tokenManager = TokenManager.getInstance(this);
retryManager = new RetryManager(this);
if (AppConfig.ENABLE_SERVER_UPLOAD) {
retryManager.setUploadCallback(this::performUpload);
retryManager.restoreFromPersistence();
}
}
private static final String ACTION_HOOK_MESSAGE = "ACTION_HOOK_MESSAGE";
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if (intent != null) {
if ("ACTION_START_MONITORING".equals(intent.getAction())) {
startMonitoring();
} else if (ACTION_HOOK_MESSAGE.equals(intent.getAction())) {
handleHookMessageIntent(intent);
}
}
NotificationActivity.sendMessage("监听服务成功!");
return START_STICKY;
}
public static void submitFromHook(Context context, MessageInfo messageInfo,
String title, String content, long timestamp, String source) {
Intent intent = new Intent(context, NotificationService.class);
intent.setAction(ACTION_HOOK_MESSAGE);
intent.putExtra("hook_package", messageInfo.getPackageName());
intent.putExtra("hook_title", title);
intent.putExtra("hook_content", content);
intent.putExtra("hook_timestamp", timestamp);
intent.putExtra("hook_source", source);
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.startForegroundService(intent);
} else {
context.startService(intent);
}
Log.d(TAG, "submitFromHook: service started for " + messageInfo.getPackageName());
} catch (Exception e) {
Log.e(TAG, "submitFromHook failed: " + e.getMessage(), e);
NotificationActivity.sendMessage("[Hook] 服务启动失败: " + e.getMessage());
}
}
private void handleHookMessageIntent(Intent intent) {
String packageName = intent.getStringExtra("hook_package");
MessageInfo messageInfo = App.getMessageByNotiList(this, packageName);
if (messageInfo == null) {
return;
}
String source = intent.getStringExtra("hook_source");
NotificationExtractor.Result result = new NotificationExtractor.Result(
intent.getStringExtra("hook_title"),
intent.getStringExtra("hook_content"),
intent.getLongExtra("hook_timestamp", System.currentTimeMillis())
);
submitNotification(messageInfo, result, source != null ? source : "hook");
}
public static boolean isMonitoring() {
return isMonitoring;
}
private void startMonitoring() {
if (isMonitoring) return;
isMonitoring = true;
toggleNotificationListenerService(this);
updateForegroundNotification(retryManager != null
? (AppConfig.ENABLE_SERVER_UPLOAD ? retryManager.getUploadedCount() : retryManager.getTotalCount())
: 0);
NotificationActivity.sendMessage("已开启持续监听模式");
NotificationActivity.updateUI();
}
public static void stopMonitoring(Context context) {
isMonitoring = false;
NotificationActivity.sendMessage("已停止监听服务");
NotificationActivity.updateUI();
Toast.makeText(context, "监听服务已停止", Toast.LENGTH_SHORT).show();
}
private void acquireWakeLockForUpload() {
if (wakeLock != null && wakeLock.isHeld()) return;
PowerManager pm = (PowerManager) getSystemService(POWER_SERVICE);
if (pm != null) {
wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, WAKELOCK_TAG);
wakeLock.acquire(30_000);
}
}
private void releaseWakeLockAfterUpload() {
if (wakeLock != null && wakeLock.isHeld()) {
wakeLock.release();
}
}
public static void requestStartMonitoring(Context context) {
toggleNotificationListenerService(context);
try {
Intent intent = new Intent(context, NotificationService.class);
intent.setAction("ACTION_START_MONITORING");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.startForegroundService(intent);
} else {
context.startService(intent);
}
} catch (Exception e) {
Log.e(TAG, "启动监听失败", e);
}
}
@Override
public void onNotificationPosted(StatusBarNotification sbn) {
if (sbn == null) return;
if (sbn.getPackageName().equals(getPackageName())) return;
MessageInfo messageInfo = App.getMessageByNotiList(this, sbn.getPackageName());
if (messageInfo == null) return;
NotificationExtractor.Result result = NotificationExtractor.extract(sbn);
Log.d(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());
NotificationActivity.sendMessage("[" + messageInfo.getAppName() + "] 通知内容为空,跳过");
return;
}
if (result.title.contains("正在运行") || result.title.contains("正在监听")) {
return;
}
NotificationActivity.sendMessage(result.title + " " + result.content);
submitNotification(messageInfo, result, "notification");
}
private void submitNotification(MessageInfo messageInfo, NotificationExtractor.Result result,
String debugSource) {
DebugForwarder.forward(this, messageInfo, result.title, result.content,
result.timestamp, debugSource);
if (!AppConfig.ENABLE_SERVER_UPLOAD) {
recordLocalCapture();
return;
}
if (!tokenManager.isLoggedIn()) {
NotificationActivity.sendMessage("未登录,请先登录");
return;
}
if (TextUtils.isEmpty(messageInfo.getBankInfoId())) {
NotificationActivity.sendMessage("该应用未关联银行账户,请先配置");
return;
}
try {
JSONObject jsonObject = new JSONObject();
jsonObject.put("bankInfoId", messageInfo.getBankInfoId());
JSONObject dataObject = new JSONObject();
dataObject.put("title", result.title);
dataObject.put("context", result.content);
dataObject.put("timestamp", result.timestamp);
jsonObject.put("data", dataObject);
jsonObject.put("name", messageInfo.getName());
jsonObject.put("code", messageInfo.getCode());
jsonObject.put("remark", messageInfo.getRemark());
jsonObject.put("appName", messageInfo.getAppName());
jsonObject.put("packageName", messageInfo.getPackageName());
retryManager.enqueue(jsonObject.toString());
} catch (JSONException e) {
Log.e(TAG, "JSON构建失败", e);
NotificationActivity.sendMessage("JSON构建失败: " + e.getMessage());
}
}
private void recordLocalCapture() {
if (retryManager != null) {
retryManager.recordLocalCapture();
updateForegroundNotification(retryManager.getTotalCount());
}
NotificationActivity.updateUI();
}
private void performUpload(String jsonPayload, RetryManager.UploadResultListener listener) {
acquireWakeLockForUpload();
// 使用新的API服务上传
apiService.uploadNotification(jsonPayload, new ApiService.ApiCallback<UploadNotificationResponse>() {
@Override
public void onSuccess(UploadNotificationResponse result) {
releaseWakeLockAfterUpload();
String message = "提交成功";
if (result.getId() != null) {
message += " (ID: " + result.getId().substring(0, Math.min(8, result.getId().length())) + "...)";
}
if (result.getDuplicateOfEmailSyncRecordId() != null) {
message += " [已去重]";
}
NotificationActivity.sendMessage(message);
listener.onSuccess();
updateForegroundNotification(retryManager.getUploadedCount());
}
@Override
public void onError(ApiError error) {
releaseWakeLockAfterUpload();
String errorMsg = error.getMessage();
if (error.isTokenExpired()) {
errorMsg = "登录已失效,请重新登录";
tokenManager.clear();
}
NotificationActivity.sendMessage("提交失败: " + errorMsg);
listener.onFailure(errorMsg);
}
});
}
@Override
public void onListenerDisconnected() {
Log.w(TAG, "通知监听断开,尝试恢复");
NotificationActivity.sendMessage("通知监听断开,尝试恢复...");
toggleNotificationListenerService(this);
new Handler(Looper.getMainLooper()).postDelayed(() -> {
if (!isNotificationListenerEnabled()) {
notifyNlsDisabled();
}
}, 2000);
}
private boolean isNotificationListenerEnabled() {
String flat = Settings.Secure.getString(
getContentResolver(),
"enabled_notification_listeners");
if (TextUtils.isEmpty(flat)) {
return false;
}
return flat.contains(getPackageName());
}
private void notifyNlsDisabled() {
Log.w(TAG, "通知监听已被禁用,向用户发出警告");
NotificationActivity.sendMessage("警告:通知监听已被禁用,请重新开启!");
Intent intent = new Intent(Settings.ACTION_NOTIFICATION_LISTENER_SETTINGS);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
}
@Override
public void onDestroy() {
super.onDestroy();
isMonitoring = false;
NotificationActivity.updateUI();
if (wakeLock != null && wakeLock.isHeld()) {
wakeLock.release();
wakeLock = null;
}
if (retryManager != null) {
retryManager.destroy();
}
}
public static void toggleNotificationListenerService(Context context) {
ComponentName thisComponent = new ComponentName(context, NotificationService.class);
PackageManager pm = context.getPackageManager();
pm.setComponentEnabledSetting(thisComponent,
PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
PackageManager.DONT_KILL_APP);
pm.setComponentEnabledSetting(thisComponent,
PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
PackageManager.DONT_KILL_APP);
}
private void createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel channel = new NotificationChannel(
CHANNEL_ID,
"通知监听服务",
NotificationManager.IMPORTANCE_LOW
);
channel.setDescription("保持通知监听服务运行");
channel.setShowBadge(false);
NotificationManager nm = getSystemService(NotificationManager.class);
if (nm != null) {
nm.createNotificationChannel(channel);
}
}
}
private Notification buildForegroundNotification(int count, boolean monitoring) {
String title = monitoring ? "持续监听中" : "通知监听服务";
String countLabel = AppConfig.ENABLE_SERVER_UPLOAD ? "已上传" : "已抓取";
String text = monitoring
? countLabel + " " + count + " 条 · 持续运行中"
: countLabel + " " + count + "";
return new NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle(title)
.setContentText(text)
.setSmallIcon(R.mipmap.app_logo)
.setOngoing(true)
.setPriority(NotificationCompat.PRIORITY_LOW)
.setCategory(NotificationCompat.CATEGORY_SERVICE)
.build();
}
private void updateForegroundNotification(int count) {
NotificationManager nm = getSystemService(NotificationManager.class);
if (nm != null) {
nm.notify(FOREGROUND_NOTIFICATION_ID, buildForegroundNotification(count, isMonitoring));
}
}
}