From c2fba02048bb0b4092052c583345b9495b486332 Mon Sep 17 00:00:00 2001 From: Song <168455@qq.com> Date: Wed, 8 Jul 2026 15:52:18 +0800 Subject: [PATCH] fix: retry transient push failures + blacklist noisy apps Investigated 9 failed forwards on device: all "connection closed" to api.day.app, while ping/curl to the server succeed -> not network-blocked, but OkHttp transient connection drops that aren't retried (POST read-phase connection close is not covered by retryOnConnectionFailure). - BarkClient.push: manual retry up to 3x (600/1200ms backoff) on IOException (connection closed/timeout/TLS reset); HTTP 4xx/5xx not retried. - AppRulesStore: add recorder/gallery/find-device/password-manager to default blacklist; versioned migration (bl_version) merges the new entries into existing users' blacklist (add-only). - version 1.5.0 -> 1.5.1 (code 8 -> 9); CLAUDE.md synced. Co-Authored-By: Claude Opus 4.8 (1M context) --- CLAUDE.md | 6 ++-- app/build.gradle.kts | 4 +-- .../java/com/a2i/forwarder/core/BarkClient.kt | 33 ++++++++++++++++--- .../com/a2i/forwarder/store/AppRulesStore.kt | 26 ++++++++++++++- 4 files changed, 58 insertions(+), 11 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index a5ef99f..9c3e94f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -75,7 +75,7 @@ NotifyListenerService (系统通知入口) | `core/CodeExtractor.kt` | 三段正则:关键词+数字、数字+关键词、纯数字 4-8 位。检测到全 `*****` 时直接返回 null。 | | `core/AdFilter.kt` | 内置营销关键词 + 用户自定义关键词(`settings.adKeywords`)。 | | `core/AppParsers.kt` | 微信/QQ/Telegram 的 `clickUrl` 映射 + 重要消息关键词(@我、验证码、通话等)。 | -| `core/BarkClient.kt` | OkHttp POST 到 `${server}/push`,超时 10/10/15s。 | +| `core/BarkClient.kt` | OkHttp POST 到 `${server}/push`,超时 10/10/15s;对 connection closed / 超时 / TLS 中断等瞬时 IOException 自动重试 3 次(退避 600/1200ms),HTTP 4xx/5xx 不重试。 | | `core/RetryWorker.kt` | WorkManager 周期任务,每 15 分钟重发 `pending` 队列中的失败消息。 | | `core/LogStore.kt` | 最近 300 条日志 + 失败重发队列(≤100)。DataStore 持久化。 | | `core/IconExporter.kt` | 一键导出所有已安装 App 图标到用户选择的 SAF 目录(用于部署到 iOS Bark 图床)。 | @@ -86,7 +86,7 @@ NotifyListenerService (系统通知入口) | `core/SmsFallbackForwarder.kt` | 断网时把紧要通知(验证码/来电)用短信发到 iPhone,5 分钟限频 + 余额检查。 | | `core/UpdateChecker.kt` | 查 GitHub Releases 最新版(24h 节流)→ 语义比较 → 下载 APK → FileProvider 调起系统安装器。 | | `store/SettingsStore.kt` | Bark 服务器列表、全局开关、广告关键词等。`MutableStateFlow` + DataStore。 | -| `store/AppRulesStore.kt` | 黑/白名单模式 + 默认黑名单(系统 App + 三星剪贴板/键盘 + GMS)。 | +| `store/AppRulesStore.kt` | 黑/白名单模式 + 默认黑名单(系统 App + 输入法 + 三星剪贴板/键盘 + GMS + 录音机/相册/查找/密码管理);默认黑名单带版本号 `bl_version`,升级时把新增噪音包合并进老用户黑名单(只增不删)。 | | `ui/nav/AppNav.kt` | 4 个底部 Tab + 日志页。 | | `ui/theme/*` | 固定品牌配色(`Color.kt`)、自定义 `Typography`(`Type.kt`),`A2iTheme` 关闭动态取色、统一深浅色方案(`Theme.kt`)。 | | `ui/screens/*.kt` | HomeScreen(品牌横幅 + 运行状态 + 统计 + 更新横幅 + 短信挂起提醒)、AppManageScreen、FilterScreen、SettingsScreen(Bark 服务 / 应用图标 / 断网短信兜底 / 关于与更新)、LogScreen、UpdateDialog(应用内更新弹窗)。 | @@ -111,7 +111,7 @@ NotifyListenerService (系统通知入口) ## 版本与发布 -- 当前版本:`versionCode = 8`、`versionName = "1.5.0"`(位于 `app/build.gradle.kts`)。**每次改动后同步递增 versionCode/versionName,并同步更新本节。** +- 当前版本:`versionCode = 9`、`versionName = "1.5.1"`(位于 `app/build.gradle.kts`)。**每次改动后同步递增 versionCode/versionName,并同步更新本节。** - 远端仓库:`https://github.com/lsxf/a2i.git`,默认分支 `main`。 - 提交策略:`.gitignore` 已排除 `app/build`、`.gradle`、`.kotlin`、`local.properties`、`.claude/`、`do.ps1`、`gh-token.txt`(发版用 token)、`*.token`、截图与临时文件,**不要**提交它们。 - 发版流程: diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 6569a03..4827e25 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -13,8 +13,8 @@ android { applicationId = "com.a2i.forwarder" minSdk = 34 targetSdk = 36 - versionCode = 8 - versionName = "1.5.0" + versionCode = 9 + versionName = "1.5.1" } signingConfigs { diff --git a/app/src/main/java/com/a2i/forwarder/core/BarkClient.kt b/app/src/main/java/com/a2i/forwarder/core/BarkClient.kt index 62cf58e..3fcac99 100644 --- a/app/src/main/java/com/a2i/forwarder/core/BarkClient.kt +++ b/app/src/main/java/com/a2i/forwarder/core/BarkClient.kt @@ -3,6 +3,7 @@ package com.a2i.forwarder.core import com.a2i.forwarder.model.BarkMessage import com.a2i.forwarder.model.BarkServer import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay import kotlinx.coroutines.withContext import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json @@ -10,6 +11,7 @@ import okhttp3.MediaType.Companion.toMediaType import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.RequestBody.Companion.toRequestBody +import java.io.IOException import java.util.concurrent.TimeUnit class BarkClient(private val server: BarkServer) { @@ -24,16 +26,37 @@ class BarkClient(private val server: BarkServer) { suspend fun push(msg: BarkMessage): Result = withContext(Dispatchers.IO) { runCatching { require(server.deviceKey.isNotBlank()) { "device key 为空" } - val body = json.encodeToString(msg).toRequestBody(JSON) - val url = server.server.trim().removeSuffix("/") + "/push" - val req = Request.Builder().url(url).post(body).build() - http.newCall(req).execute().use { res -> - if (!res.isSuccessful) error("HTTP ${res.code}") + doPushWithRetry(msg) + } + } + + /** + * 带重试的推送:对 connection closed / 超时 / TLS 中断等瞬时 IOException 自动重试。 + * OkHttp 的 retryOnConnectionFailure 不覆盖 POST 读响应阶段的连接中断,故在此手动补。 + * HTTP 4xx/5xx 是服务端拒绝,重试无意义,直接抛出。 + */ + private suspend fun doPushWithRetry(msg: BarkMessage) { + val body = json.encodeToString(msg).toRequestBody(JSON) + val url = server.server.trim().removeSuffix("/") + "/push" + val req = Request.Builder().url(url).post(body).build() + var lastError: IOException? = null + repeat(MAX_ATTEMPTS) { attempt -> + try { + http.newCall(req).execute().use { res -> + if (res.isSuccessful) return + error("HTTP ${res.code}") + } + } catch (e: IOException) { + lastError = e + if (attempt < MAX_ATTEMPTS - 1) delay(BACKOFF_MS.getOrElse(attempt) { 1000L }) } } + throw lastError ?: error("推送失败") } companion object { private val JSON = "application/json; charset=utf-8".toMediaType() + private const val MAX_ATTEMPTS = 3 + private val BACKOFF_MS = longArrayOf(600L, 1200L) } } diff --git a/app/src/main/java/com/a2i/forwarder/store/AppRulesStore.kt b/app/src/main/java/com/a2i/forwarder/store/AppRulesStore.kt index b611186..748c63a 100644 --- a/app/src/main/java/com/a2i/forwarder/store/AppRulesStore.kt +++ b/app/src/main/java/com/a2i/forwarder/store/AppRulesStore.kt @@ -4,6 +4,7 @@ import android.content.Context import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.intPreferencesKey import androidx.datastore.preferences.core.stringPreferencesKey import androidx.datastore.preferences.core.stringSetPreferencesKey import androidx.datastore.preferences.preferencesDataStore @@ -25,6 +26,7 @@ class AppRulesStore( val MODE = stringPreferencesKey("mode") val BLACK = stringSetPreferencesKey("blacklist") val WHITE = stringSetPreferencesKey("whitelist") + val BL_VERSION = intPreferencesKey("bl_version") } val mode = MutableStateFlow(RuleMode.BLACKLIST) @@ -35,7 +37,14 @@ class AppRulesStore( scope.launch { ds.data.collect { p -> mode.value = runCatching { RuleMode.valueOf(p[K.MODE] ?: "BLACKLIST") }.getOrDefault(RuleMode.BLACKLIST) - blacklist.value = p[K.BLACK] ?: defaultBlacklist + // 默认黑名单版本迁移:把新增的噪音包补进老用户的黑名单(只增不删) + val ver = p[K.BL_VERSION] ?: 1 + var black = p[K.BLACK] ?: defaultBlacklist + if (ver < DEFAULT_BL_VERSION) { + black = black + blAddedV2 + ds.edit { it[K.BLACK] = black; it[K.BL_VERSION] = DEFAULT_BL_VERSION } + } + blacklist.value = black whitelist.value = p[K.WHITE] ?: emptySet() } } @@ -69,6 +78,16 @@ class AppRulesStore( } companion object { + private const val DEFAULT_BL_VERSION = 2 + + // 版本 2 新增的噪音包(用于老用户迁移,只补不删) + private val blAddedV2 = setOf( + "com.android.soundrecorder", // 录音机 + "com.google.android.apps.photos", // 相册 + "com.xiaomi.finddevice", // 查找手机 + "com.miui.passwords", // 密码管理 + ) + val defaultBlacklist = setOf( // Android 系统 "com.android.systemui", @@ -91,6 +110,11 @@ class AppRulesStore( "com.google.android.gms.persistent", // a2i 自身 "com.a2i.forwarder", + // [v2] 纯噪音 App:转发无意义,默认屏蔽 + "com.android.soundrecorder", // 录音机 + "com.google.android.apps.photos", // 相册 + "com.xiaomi.finddevice", // 查找手机 + "com.miui.passwords", // 密码管理 ) } }