diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index fce362d..c4b21c4 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -8,6 +8,16 @@
+
+
+
+
+
+
+
+
+
+
@@ -20,6 +30,7 @@
+ tools:targetApi="34">
+
+
+
+
-
+
+
+
+
+
+
-
\ No newline at end of file
+
diff --git a/app/src/main/java/com/miraclegarden/smsmessage/service/NotificationService.java b/app/src/main/java/com/miraclegarden/smsmessage/service/NotificationService.java
index 9ee580e..98ab314 100644
--- a/app/src/main/java/com/miraclegarden/smsmessage/service/NotificationService.java
+++ b/app/src/main/java/com/miraclegarden/smsmessage/service/NotificationService.java
@@ -1,36 +1,46 @@
package com.miraclegarden.smsmessage.service;
-import android.annotation.SuppressLint;
import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
import android.content.ComponentName;
-import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
-import android.database.Cursor;
-import android.net.Uri;
+import android.content.pm.ServiceInfo;
import android.os.Build;
-import android.os.Bundle;
+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.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.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.FormBody;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
@@ -39,119 +49,291 @@ 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);
+ retryManager.setUploadCallback(this::performUpload);
+ retryManager.restoreFromPersistence();
+ }
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
+ if (intent != null && "ACTION_START_MONITORING".equals(intent.getAction())) {
+ startMonitoring();
+ }
NotificationActivity.sendMessage("监听服务成功!");
- return super.onStartCommand(intent, flags, startId);
+ return START_STICKY;
}
+ public static boolean isMonitoring() {
+ return isMonitoring;
+ }
+
+ private void startMonitoring() {
+ if (isMonitoring) return;
+ isMonitoring = true;
+
+ toggleNotificationListenerService(this);
+ updateForegroundNotification(retryManager != null ? retryManager.getUploadedCount() : 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");
+ context.startService(intent);
+ } catch (Exception e) {
+ Log.e(TAG, "启动监听失败", e);
+ }
+ }
@Override
public void onNotificationPosted(StatusBarNotification sbn) {
- getSbnByNotificatinList(sbn);
+ if (sbn == null) return;
- }
+ if (sbn.getPackageName().equals(getPackageName())) return;
- public void getSbnByNotificatinList(StatusBarNotification sbn) {
MessageInfo messageInfo = App.getMessageByNotiList(this, sbn.getPackageName());
- if (messageInfo != null) {
- initData(messageInfo, sbn);
+ if (messageInfo == null) return;
+
+ NotificationExtractor.Result result = NotificationExtractor.extract(sbn);
+
+ if (TextUtils.isEmpty(result.title) && TextUtils.isEmpty(result.content)) {
+ NotificationActivity.sendMessage("[" + messageInfo.getAppName() + "] 通知内容为空,跳过");
+ return;
}
+
+ if (result.title.contains("正在运行") || result.title.contains("正在监听")) {
+ return;
+ }
+
+ NotificationActivity.sendMessage(result.title + " " + result.content);
+ submitNotification(messageInfo, result);
}
- private String text = "";
-
- private void initData(MessageInfo messageInfo, StatusBarNotification sbn) {
- Bundle bundle = sbn.getNotification().extras;
- String title = bundle.getString(Notification.EXTRA_TITLE, "获取标题失败!");
- String context = bundle.getString(Notification.EXTRA_TEXT, "获取内容失败!");
- if (context.equals("获取内容失败!")) {
- if (sbn.getNotification().tickerText != null) {
- context = sbn.getNotification().tickerText.toString();
- }
+ private void submitNotification(MessageInfo messageInfo, NotificationExtractor.Result result) {
+ // 检查是否已登录
+ if (!tokenManager.isLoggedIn()) {
+ NotificationActivity.sendMessage("未登录,请先登录");
+ return;
}
- NotificationActivity.sendMessage(title + " " + context);
- if (!text.equals(context)) {
- if (!title.equals("获取标题失败!") && !context.equals("获取内容失败!") && !title.contains("正在运行")) {
- NotificationActivity.sendMessage("准备发送服务器:成功");
- Submit(messageInfo,title, context);
- }
+
+ // 检查是否关联了银行账户
+ if (TextUtils.isEmpty(messageInfo.getBankInfoId())) {
+ NotificationActivity.sendMessage("该应用未关联银行账户,请先配置");
+ return;
}
- }
- public static final MediaType JSON = MediaType.parse("application/json; charset=utf-8");
-
- public void Submit(MessageInfo messageInfo,String title, String context) {
- OkHttpClient client = new OkHttpClient();
- SharedPreferences sharedPreferences = getSharedPreferences("server", MODE_PRIVATE);
try {
-
JSONObject jsonObject = new JSONObject();
+ // 根据文档,bankInfoId 是建议字段
+ jsonObject.put("bankInfoId", messageInfo.getBankInfoId());
+
+ // data 字段包含通知内容
+ 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());
- JSONObject jsonObject1 = new JSONObject();
- jsonObject1.put("title", title+System.currentTimeMillis());
- jsonObject1.put("context", context+System.currentTimeMillis());
- jsonObject.put("data", jsonObject1);
-
- String json = jsonObject.toString();
-
- RequestBody body = RequestBody.create(json, JSON);
-
- // 构建请求
- Request request = new Request.Builder()
- .url(sharedPreferences.getString("host", ""))
- .post(body)
- .build();
-
- client.newCall(request).enqueue(new Callback() {
- @Override
- public void onFailure(@NonNull Call call, @NonNull IOException e) {
- NotificationActivity.sendMessage("提交失败:" + e);
- }
-
- @Override
- public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException {
- if (response.isSuccessful()) {
- String str = response.body().string();
- NotificationActivity.sendMessage(title + "提交成功:" + str);
- text = context;
- return;
- }
- NotificationActivity.sendMessage("提交失败:");
- }
- });
+ String payload = jsonObject.toString();
+ NotificationActivity.sendMessage("准备发送服务器:成功");
+ retryManager.enqueue(payload);
} catch (JSONException e) {
- throw new RuntimeException(e);
+ Log.e(TAG, "JSON构建失败", e);
+ NotificationActivity.sendMessage("JSON构建失败: " + e.getMessage());
}
}
- /**
- * 监听断开
- */
+ private void performUpload(String jsonPayload, RetryManager.UploadResultListener listener) {
+ acquireWakeLockForUpload();
+
+ // 使用新的API服务上传
+ apiService.uploadNotification(jsonPayload, new ApiService.ApiCallback() {
+ @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() {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
- // 通知侦听器断开连接 - 请求重新绑定
- requestRebind(new ComponentName(this, NotificationListenerService.class));
+ 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();
}
}
- /**
- * @param context 反正第二次启动失败
- */
public static void toggleNotificationListenerService(Context context) {
+ ComponentName thisComponent = new ComponentName(context, NotificationService.class);
PackageManager pm = context.getPackageManager();
- pm.setComponentEnabledSetting(new ComponentName(context, NotificationService.class),
- PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP);
-
- pm.setComponentEnabledSetting(new ComponentName(context, NotificationService.class),
- PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP);
+ 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 uploadedCount, boolean monitoring) {
+ String title = monitoring ? "持续监听中" : "通知监听服务";
+ String text = monitoring
+ ? "已上传 " + uploadedCount + " 条 · 持续运行中"
+ : "已上传 " + uploadedCount + " 条";
+
+ 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 uploadedCount) {
+ NotificationManager nm = getSystemService(NotificationManager.class);
+ if (nm != null) {
+ nm.notify(FOREGROUND_NOTIFICATION_ID, buildForegroundNotification(uploadedCount, isMonitoring));
+ }
+ }
}