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) <noreply@anthropic.com>
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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<Unit> = 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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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", // 密码管理
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user