diff --git a/app/src/main/java/io/legado/app/help/config/AppConfig.kt b/app/src/main/java/io/legado/app/help/config/AppConfig.kt index 0555a3734d..d2e0c6fa2c 100644 --- a/app/src/main/java/io/legado/app/help/config/AppConfig.kt +++ b/app/src/main/java/io/legado/app/help/config/AppConfig.kt @@ -834,7 +834,7 @@ object AppConfig : SharedPreferences.OnSharedPreferenceChangeListener { val syncEnabled get() = appCtx.getPrefBoolean(PreferKey.syncEnabled, false) val syncInterval: Int - get() = appCtx.getPrefString(PreferKey.syncInterval)?.toIntOrNull() ?: 15 + get() = appCtx.getPrefString(PreferKey.syncInterval)?.toIntOrNull() ?: 5 /** 需要同步的数据类型 */ val syncDataTypes: Set diff --git a/app/src/main/java/io/legado/app/help/sync/worker/SyncWorker.kt b/app/src/main/java/io/legado/app/help/sync/worker/SyncWorker.kt index eb0ca0d54e..cf6e539383 100644 --- a/app/src/main/java/io/legado/app/help/sync/worker/SyncWorker.kt +++ b/app/src/main/java/io/legado/app/help/sync/worker/SyncWorker.kt @@ -14,10 +14,11 @@ class SyncWorker( ) : CoroutineWorker(appContext, params) { override suspend fun doWork(): Result { - return if (SyncManager.syncWorker()) { - Result.success() - } else { - Result.retry() + val ok = SyncManager.syncWorker() + if (ok) { + WorkManagerHelper.scheduleNext(applicationContext) + return Result.success() } + return Result.retry() } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/help/sync/worker/WorkManagerHelper.kt b/app/src/main/java/io/legado/app/help/sync/worker/WorkManagerHelper.kt index e2fa1e8dab..3816fc8db9 100644 --- a/app/src/main/java/io/legado/app/help/sync/worker/WorkManagerHelper.kt +++ b/app/src/main/java/io/legado/app/help/sync/worker/WorkManagerHelper.kt @@ -3,7 +3,9 @@ package io.legado.app.help.sync.worker import android.content.Context import androidx.work.Constraints import androidx.work.ExistingPeriodicWorkPolicy +import androidx.work.ExistingWorkPolicy import androidx.work.NetworkType +import androidx.work.OneTimeWorkRequestBuilder import androidx.work.PeriodicWorkRequestBuilder import androidx.work.WorkManager import io.legado.app.help.sync.SyncConfig @@ -14,34 +16,70 @@ import java.util.concurrent.TimeUnit */ object WorkManagerHelper { - private const val UNIQUE_NAME = "sync_periodic_work" + private const val UNIQUE_PERIODIC = "sync_periodic_work" + private const val UNIQUE_LOOP = "sync_loop_work" - /** 最短周期(分钟), WorkManager 周期任务下限为 15 分钟 */ - private const val MIN_INTERVAL = 15L + /** WorkManager 周期任务最短周期(分钟), 低于此值用一次性任务自续期循环 */ + private const val MIN_PERIODIC = 15L /** * 按设置调度周期同步, 设置变更时重新调度 + * 间隔 >= 15 分钟: PeriodicWorkRequest; < 15 分钟(即时/5/10): OneTimeWorkRequest 自续期循环 */ fun schedule(context: Context) { if (!SyncConfig.enabled) { cancel(context) return } - val interval = SyncConfig.intervalMinutes.toLong().coerceAtLeast(MIN_INTERVAL) + val interval = SyncConfig.intervalMinutes.toLong() + val wm = WorkManager.getInstance(context) + if (interval >= MIN_PERIODIC) { + wm.cancelUniqueWork(UNIQUE_LOOP) + val constraints = Constraints.Builder() + .setRequiredNetworkType(NetworkType.CONNECTED) + .build() + val request = PeriodicWorkRequestBuilder(interval, TimeUnit.MINUTES) + .setConstraints(constraints) + .build() + wm.enqueueUniquePeriodicWork( + UNIQUE_PERIODIC, + ExistingPeriodicWorkPolicy.UPDATE, + request + ) + } else { + wm.cancelUniqueWork(UNIQUE_PERIODIC) + enqueueLoop(wm) + } + } + + /** + * 续期下一次循环同步, 由 SyncWorker 同步成功后调用 + */ + fun scheduleNext(context: Context) { + if (!SyncConfig.enabled) return + if (SyncConfig.intervalMinutes >= MIN_PERIODIC) return + enqueueLoop(WorkManager.getInstance(context)) + } + + private fun enqueueLoop(wm: WorkManager) { + val interval = SyncConfig.intervalMinutes.toLong().coerceAtLeast(0) val constraints = Constraints.Builder() .setRequiredNetworkType(NetworkType.CONNECTED) .build() - val request = PeriodicWorkRequestBuilder(interval, TimeUnit.MINUTES) + val request = OneTimeWorkRequestBuilder() + .setInitialDelay(interval, TimeUnit.MINUTES) .setConstraints(constraints) .build() - WorkManager.getInstance(context).enqueueUniquePeriodicWork( - UNIQUE_NAME, - ExistingPeriodicWorkPolicy.UPDATE, + wm.enqueueUniqueWork( + UNIQUE_LOOP, + ExistingWorkPolicy.REPLACE, request ) } fun cancel(context: Context) { - WorkManager.getInstance(context).cancelUniqueWork(UNIQUE_NAME) + val wm = WorkManager.getInstance(context) + wm.cancelUniqueWork(UNIQUE_PERIODIC) + wm.cancelUniqueWork(UNIQUE_LOOP) } } \ No newline at end of file diff --git a/app/src/main/res/values-zh-rHK/strings.xml b/app/src/main/res/values-zh-rHK/strings.xml index 10d813b091..db4ccaafd8 100644 --- a/app/src/main/res/values-zh-rHK/strings.xml +++ b/app/src/main/res/values-zh-rHK/strings.xml @@ -817,6 +817,47 @@ 進入退出閱讀介面時同步閱讀進度 同步增強 重新進入頁面(息屏、從背景返回等)或網路恢復可用時同步雲端進度,同步新進度時會詢問 + 增量同步 + 通過 WebDAV 多端增量同步 + 開啟同步 + 定時以及應用程式啟動/退出時同步 + 同步間隔 + 最小間隔為即時(持續同步) + 即時 + 5分鐘 + 10分鐘 + 30分鐘 + 同步資料類型 + 選擇要同步的資料 + 預設衝突策略 + 本地和雲端都發生變化時採用的策略 + 同步操作 + 立即同步 + 立即拉取並推送變更 + 請先開啟同步 + 上次同步狀態 + 暫無同步記錄 + 衝突列表 + 查看並解決同步衝突 + 書架(中繼資料) + 書籤 + 書籍分組 + 閱讀記錄 + 閱讀設定 + 手動處理 + 保留本地 + 保留雲端 + 同步衝突 + 使用本地 + 使用雲端 + 保留兩者 + 全部使用本地 + 全部使用雲端 + 清除已解決 + 暫無待解決衝突 + 記錄:%1$s + 已解決 + 衝突已解決 建立書籤失敗 單URL 導出書單 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 9db72401f2..3d665b7f1e 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -820,6 +820,47 @@ 進入退出閱讀介面時同步閱讀進度 同步增強 重新進入頁面(息屏、從背景返回等)或網路恢復可用時同步雲端進度,同步新進度時會詢問 + 增量同步 + 透過 WebDAV 多端增量同步 + 開啟同步 + 定時以及應用程式啟動/退出時同步 + 同步間隔 + 最小間隔為即時(持續同步) + 即時 + 5分鐘 + 10分鐘 + 30分鐘 + 同步資料類型 + 選擇要同步的資料 + 預設衝突策略 + 本地和雲端都發生變化時採用的策略 + 同步操作 + 立即同步 + 立即拉取並推送變更 + 請先開啟同步 + 上次同步狀態 + 暫無同步記錄 + 衝突列表 + 查看並解決同步衝突 + 書架(中繼資料) + 書籤 + 書籍分組 + 閱讀記錄 + 閱讀設定 + 手動處理 + 保留本地 + 保留雲端 + 同步衝突 + 使用本地 + 使用雲端 + 保留兩者 + 全部使用本地 + 全部使用雲端 + 清除已解決 + 暫無待解決衝突 + 記錄:%1$s + 已解決 + 衝突已解決 建立書籤失敗 單URL 匯出書單 diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index 86f7e5a05c..f2cbbd3357 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -830,6 +830,47 @@ 进入退出阅读界面时同步阅读进度 同步增强 重新进入页面(息屏、后台返回等)或者网络变为可用时同步云端进度,同步新进度会询问 + 增量同步 + 通过 WebDAV 多端增量同步 + 开启同步 + 定时以及应用启动/退出时同步 + 同步间隔 + 最小间隔为即时(持续同步) + 即时 + 5分钟 + 10分钟 + 30分钟 + 同步数据类型 + 选择要同步的数据 + 默认冲突策略 + 本地和云端都发生变化时采用的策略 + 同步操作 + 立即同步 + 立即拉取并推送变更 + 请先开启同步 + 上次同步状态 + 暂无同步记录 + 冲突列表 + 查看并解决同步冲突 + 书架(元数据) + 书签 + 书籍分组 + 阅读记录 + 阅读设置 + 手动处理 + 保留本地 + 保留云端 + 同步冲突 + 使用本地 + 使用云端 + 保留两者 + 全部使用本地 + 全部使用云端 + 清除已解决 + 暂无待解决冲突 + 记录:%1$s + 已解决 + 冲突已解决 创建书签失败 单 URL 导出书单 diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index e67d445674..b7cae5360c 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -209,21 +209,17 @@ - 15 minutes - 30 minutes - 1 hour - 3 hours - 6 hours - 12 hours + @string/sync_interval_immediate + @string/sync_interval_5m + @string/sync_interval_10m + @string/sync_interval_30m - 15 + 0 + 5 + 10 30 - 60 - 180 - 360 - 720 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 92e08a60bd..d548976504 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -837,7 +837,11 @@ Enable sync Synchronize periodically and on app start / exit Sync interval - Minimum interval is 15 minutes + Minimum interval: immediately (syncs continuously) + Immediately + 5 minutes + 10 minutes + 30 minutes Sync data types Select which data to synchronize Default conflict strategy diff --git a/app/src/main/res/xml/pref_config_sync.xml b/app/src/main/res/xml/pref_config_sync.xml index 26c9944d81..b54da5ea5c 100644 --- a/app/src/main/res/xml/pref_config_sync.xml +++ b/app/src/main/res/xml/pref_config_sync.xml @@ -19,7 +19,7 @@ app:iconSpaceReserved="false" /> _.json { deleted:true, deletedAt } ## 6. 调度与生命周期 - 新增 `androidx.work:work-runtime-ktx` 到 `gradle/libs.versions.toml` 与 `app/build.gradle`。 -- `sync/worker/SyncWorker.kt`:`CoroutineWorker`,`PeriodicWorkRequest`(最短 15min,用户间隔取 >=15min 档位)+ `NetworkType.CONNECTED` 约束;设置变更时重排。 +- `sync/worker/SyncWorker.kt`:`CoroutineWorker`,间隔档位为 **即时/5分钟/10分钟/30分钟**;`>=15min` 用 `PeriodicWorkRequest`(WorkManager 周期任务下限)+ `NetworkType.CONNECTED` 约束,`<15min`(即时/5/10)用 `OneTimeWorkRequest` 自续期循环(Worker 同步成功后 `scheduleNext` 续排下一轮,即时=连续同步);设置变更时重排。 - `App.kt:123` 处同步逻辑改为 `SyncManager.pullOnly()`(替换/并列现有 `downloadAllBookProgress`)。 - 进程退出推送:用 `ProcessLifecycleOwner` observer 或新建 `SyncLifecycleObserver` 在 `onStop` 触发 `pushOnly()`。 - 手动同步按钮调用 `syncNow()`。 @@ -170,7 +170,8 @@ M2 — 同步引擎核心 M3 — 调度 - WorkManager 依赖已加(gradle/libs.versions.toml work=2.10.0、app/build.gradle work-runtime-ktx + lifecycle-process) - SyncWorker.kt:CoroutineWorker,调 SyncManager.syncWorker(),失败返回 retry -- WorkManagerHelper.kt:PeriodicWorkRequest,间隔>=15min,NetworkType.CONNECTED 约束,UPDATE 策略重排/取消 +- WorkManagerHelper.kt:间隔>=15min 用 PeriodicWorkRequest,<15min(即时/5/10)用 OneTimeWorkRequest 自续期循环(scheduleNext),NetworkType.CONNECTED 约束,UPDATE 策略重排/取消 +- 同步间隔档位改为 即时(0)/5/10/30 分钟(values/arrays.xml + 四语言 strings),默认 5 分钟;WorkManagerHelper 双调度路径(periodic + loop) - SyncLifecycleObserver.kt:ProcessLifecycleOwner ON_STOP → SyncManager.syncOnStop() - App.kt:onCreate 调度周期任务、注册生命周期观察者、启动后 syncOnStart() - AppConfig:syncEnabled/syncInterval 变更时重排周期任务 @@ -185,7 +186,3 @@ M4 — UI/设置 - ConflictResolver 新增 keepBoth()(两端各留一份、消除冲突) - SyncConflictDao 新增 observeAll()/pendingCount - 字符串/数组资源、AndroidManifest 注册 ConflictActivity - -### 未完成 -M5 — 测试 -- 冲突算法单测、迁移 androidTest、双模拟器同步演练未做 \ No newline at end of file