From 1951b76a7cc90ad5efa5efe3b0625c930182a932 Mon Sep 17 00:00:00 2001 From: wchino Date: Sun, 5 Apr 2026 22:28:53 +0800 Subject: [PATCH] =?UTF-8?q?=E9=80=9A=E7=9F=A5=E6=9C=8D=E5=8A=A1=E6=A0=B8?= =?UTF-8?q?=E5=BF=83=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- app/src/main/AndroidManifest.xml | 38 +- .../service/NotificationService.java | 346 +++++++++++++----- 2 files changed, 299 insertions(+), 85 deletions(-) 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)); + } + } }