Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/src/main/java/io/legado/app/help/config/AppConfig.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<SyncWorker>(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<SyncWorker>(interval, TimeUnit.MINUTES)
val request = OneTimeWorkRequestBuilder<SyncWorker>()
.setInitialDelay(interval, TimeUnit.MINUTES)
.setConstraints(constraints)
.build()
WorkManager.getInstance(context).enqueueUniquePeriodicWork(
UNIQUE_NAME,
ExistingPeriodicWorkPolicy.UPDATE,
wm.enqueueUniqueWork(
UNIQUE_LOOP,
ExistingWorkPolicy.REPLACE,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid replacing the running loop worker

When the selected interval is Immediate/5/10 minutes, the worker that calls scheduleNext() is still the unfinished unique work named sync_loop_work. Enqueuing the next request with ExistingWorkPolicy.REPLACE cancels any unfinished work with that same name, so each successful loop iteration cancels the currently returning worker instead of just scheduling after it; app/start rescheduling can also stop an in-flight loop sync. Use a non-canceling chain policy or a separate request name so the active sync is allowed to finish cleanly.

Useful? React with 👍 / 👎.

request
)
}

fun cancel(context: Context) {
WorkManager.getInstance(context).cancelUniqueWork(UNIQUE_NAME)
val wm = WorkManager.getInstance(context)
wm.cancelUniqueWork(UNIQUE_PERIODIC)
wm.cancelUniqueWork(UNIQUE_LOOP)
}
}
41 changes: 41 additions & 0 deletions app/src/main/res/values-zh-rHK/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -817,6 +817,47 @@
<string name="sync_book_progress_s">進入退出閱讀介面時同步閱讀進度</string>
<string name="sync_book_progress_plus_t">同步增強</string>
<string name="sync_book_progress_plus_s">重新進入頁面(息屏、從背景返回等)或網路恢復可用時同步雲端進度,同步新進度時會詢問</string>
<string name="sync_setting">增量同步</string>
<string name="sync_setting_s">通過 WebDAV 多端增量同步</string>
<string name="sync_enable">開啟同步</string>
<string name="sync_enable_s">定時以及應用程式啟動/退出時同步</string>
<string name="sync_interval">同步間隔</string>
<string name="sync_interval_s">最小間隔為即時(持續同步)</string>
<string name="sync_interval_immediate">即時</string>
<string name="sync_interval_5m">5分鐘</string>
<string name="sync_interval_10m">10分鐘</string>
<string name="sync_interval_30m">30分鐘</string>
<string name="sync_data_types">同步資料類型</string>
<string name="sync_data_types_s">選擇要同步的資料</string>
<string name="sync_conflict_mode">預設衝突策略</string>
<string name="sync_conflict_mode_s">本地和雲端都發生變化時採用的策略</string>
<string name="sync_actions">同步操作</string>
<string name="sync_now">立即同步</string>
<string name="sync_now_s">立即拉取並推送變更</string>
<string name="sync_enable_tip">請先開啟同步</string>
<string name="sync_last_status">上次同步狀態</string>
<string name="sync_last_status_s">暫無同步記錄</string>
<string name="sync_conflicts">衝突列表</string>
<string name="sync_conflicts_s">查看並解決同步衝突</string>
<string name="sync_type_books">書架(中繼資料)</string>
<string name="sync_type_bookmarks">書籤</string>
<string name="sync_type_bookgroups">書籍分組</string>
<string name="sync_type_readrecords">閱讀記錄</string>
<string name="sync_type_readconfigs">閱讀設定</string>
<string name="sync_conflict_manual">手動處理</string>
<string name="sync_conflict_keep_local">保留本地</string>
<string name="sync_conflict_keep_cloud">保留雲端</string>
<string name="sync_conflict">同步衝突</string>
<string name="conflict_keep_local">使用本地</string>
<string name="conflict_keep_cloud">使用雲端</string>
<string name="conflict_keep_both">保留兩者</string>
<string name="conflict_apply_all_local">全部使用本地</string>
<string name="conflict_apply_all_cloud">全部使用雲端</string>
<string name="conflict_clear_resolved">清除已解決</string>
<string name="conflict_empty">暫無待解決衝突</string>
<string name="conflict_record_key">記錄:%1$s</string>
<string name="conflict_resolved">已解決</string>
<string name="conflict_resolve_success">衝突已解決</string>
<string name="create_bookmark_error">建立書籤失敗</string>
<string name="single_url">單URL</string>
<string name="export_bookshelf">導出書單</string>
Expand Down
41 changes: 41 additions & 0 deletions app/src/main/res/values-zh-rTW/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -820,6 +820,47 @@
<string name="sync_book_progress_s">進入退出閱讀介面時同步閱讀進度</string>
<string name="sync_book_progress_plus_t">同步增強</string>
<string name="sync_book_progress_plus_s">重新進入頁面(息屏、從背景返回等)或網路恢復可用時同步雲端進度,同步新進度時會詢問</string>
<string name="sync_setting">增量同步</string>
<string name="sync_setting_s">透過 WebDAV 多端增量同步</string>
<string name="sync_enable">開啟同步</string>
<string name="sync_enable_s">定時以及應用程式啟動/退出時同步</string>
<string name="sync_interval">同步間隔</string>
<string name="sync_interval_s">最小間隔為即時(持續同步)</string>
<string name="sync_interval_immediate">即時</string>
<string name="sync_interval_5m">5分鐘</string>
<string name="sync_interval_10m">10分鐘</string>
<string name="sync_interval_30m">30分鐘</string>
<string name="sync_data_types">同步資料類型</string>
<string name="sync_data_types_s">選擇要同步的資料</string>
<string name="sync_conflict_mode">預設衝突策略</string>
<string name="sync_conflict_mode_s">本地和雲端都發生變化時採用的策略</string>
<string name="sync_actions">同步操作</string>
<string name="sync_now">立即同步</string>
<string name="sync_now_s">立即拉取並推送變更</string>
<string name="sync_enable_tip">請先開啟同步</string>
<string name="sync_last_status">上次同步狀態</string>
<string name="sync_last_status_s">暫無同步記錄</string>
<string name="sync_conflicts">衝突列表</string>
<string name="sync_conflicts_s">查看並解決同步衝突</string>
<string name="sync_type_books">書架(中繼資料)</string>
<string name="sync_type_bookmarks">書籤</string>
<string name="sync_type_bookgroups">書籍分組</string>
<string name="sync_type_readrecords">閱讀記錄</string>
<string name="sync_type_readconfigs">閱讀設定</string>
<string name="sync_conflict_manual">手動處理</string>
<string name="sync_conflict_keep_local">保留本地</string>
<string name="sync_conflict_keep_cloud">保留雲端</string>
<string name="sync_conflict">同步衝突</string>
<string name="conflict_keep_local">使用本地</string>
<string name="conflict_keep_cloud">使用雲端</string>
<string name="conflict_keep_both">保留兩者</string>
<string name="conflict_apply_all_local">全部使用本地</string>
<string name="conflict_apply_all_cloud">全部使用雲端</string>
<string name="conflict_clear_resolved">清除已解決</string>
<string name="conflict_empty">暫無待解決衝突</string>
<string name="conflict_record_key">記錄:%1$s</string>
<string name="conflict_resolved">已解決</string>
<string name="conflict_resolve_success">衝突已解決</string>
<string name="create_bookmark_error">建立書籤失敗</string>
<string name="single_url">單URL</string>
<string name="export_bookshelf">匯出書單</string>
Expand Down
41 changes: 41 additions & 0 deletions app/src/main/res/values-zh/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -830,6 +830,47 @@
<string name="sync_book_progress_s">进入退出阅读界面时同步阅读进度</string>
<string name="sync_book_progress_plus_t">同步增强</string>
<string name="sync_book_progress_plus_s">重新进入页面(息屏、后台返回等)或者网络变为可用时同步云端进度,同步新进度会询问</string>
<string name="sync_setting">增量同步</string>
<string name="sync_setting_s">通过 WebDAV 多端增量同步</string>
<string name="sync_enable">开启同步</string>
<string name="sync_enable_s">定时以及应用启动/退出时同步</string>
<string name="sync_interval">同步间隔</string>
<string name="sync_interval_s">最小间隔为即时(持续同步)</string>
<string name="sync_interval_immediate">即时</string>
<string name="sync_interval_5m">5分钟</string>
<string name="sync_interval_10m">10分钟</string>
<string name="sync_interval_30m">30分钟</string>
<string name="sync_data_types">同步数据类型</string>
<string name="sync_data_types_s">选择要同步的数据</string>
<string name="sync_conflict_mode">默认冲突策略</string>
<string name="sync_conflict_mode_s">本地和云端都发生变化时采用的策略</string>
<string name="sync_actions">同步操作</string>
<string name="sync_now">立即同步</string>
<string name="sync_now_s">立即拉取并推送变更</string>
<string name="sync_enable_tip">请先开启同步</string>
<string name="sync_last_status">上次同步状态</string>
<string name="sync_last_status_s">暂无同步记录</string>
<string name="sync_conflicts">冲突列表</string>
<string name="sync_conflicts_s">查看并解决同步冲突</string>
<string name="sync_type_books">书架(元数据)</string>
<string name="sync_type_bookmarks">书签</string>
<string name="sync_type_bookgroups">书籍分组</string>
<string name="sync_type_readrecords">阅读记录</string>
<string name="sync_type_readconfigs">阅读设置</string>
<string name="sync_conflict_manual">手动处理</string>
<string name="sync_conflict_keep_local">保留本地</string>
<string name="sync_conflict_keep_cloud">保留云端</string>
<string name="sync_conflict">同步冲突</string>
<string name="conflict_keep_local">使用本地</string>
<string name="conflict_keep_cloud">使用云端</string>
<string name="conflict_keep_both">保留两者</string>
<string name="conflict_apply_all_local">全部使用本地</string>
<string name="conflict_apply_all_cloud">全部使用云端</string>
<string name="conflict_clear_resolved">清除已解决</string>
<string name="conflict_empty">暂无待解决冲突</string>
<string name="conflict_record_key">记录:%1$s</string>
<string name="conflict_resolved">已解决</string>
<string name="conflict_resolve_success">冲突已解决</string>
<string name="create_bookmark_error">创建书签失败</string>
<string name="single_url">单 URL</string>
<string name="export_bookshelf">导出书单</string>
Expand Down
18 changes: 7 additions & 11 deletions app/src/main/res/values/arrays.xml
Original file line number Diff line number Diff line change
Expand Up @@ -209,21 +209,17 @@
</string-array>

<string-array name="sync_interval">
<item>15 minutes</item>
<item>30 minutes</item>
<item>1 hour</item>
<item>3 hours</item>
<item>6 hours</item>
<item>12 hours</item>
<item>@string/sync_interval_immediate</item>
<item>@string/sync_interval_5m</item>
<item>@string/sync_interval_10m</item>
<item>@string/sync_interval_30m</item>
</string-array>

<string-array name="sync_interval_values">
<item>15</item>
<item>0</item>
<item>5</item>
<item>10</item>
<item>30</item>
<item>60</item>
<item>180</item>
<item>360</item>
<item>720</item>
</string-array>

<string-array name="sync_data_types">
Expand Down
6 changes: 5 additions & 1 deletion app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -837,7 +837,11 @@
<string name="sync_enable">Enable sync</string>
<string name="sync_enable_s">Synchronize periodically and on app start / exit</string>
<string name="sync_interval">Sync interval</string>
<string name="sync_interval_s">Minimum interval is 15 minutes</string>
<string name="sync_interval_s">Minimum interval: immediately (syncs continuously)</string>
<string name="sync_interval_immediate">Immediately</string>
<string name="sync_interval_5m">5 minutes</string>
<string name="sync_interval_10m">10 minutes</string>
<string name="sync_interval_30m">30 minutes</string>
<string name="sync_data_types">Sync data types</string>
<string name="sync_data_types_s">Select which data to synchronize</string>
<string name="sync_conflict_mode">Default conflict strategy</string>
Expand Down
2 changes: 1 addition & 1 deletion app/src/main/res/xml/pref_config_sync.xml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
app:iconSpaceReserved="false" />

<io.legado.app.lib.prefs.NameListPreference
android:defaultValue="15"
android:defaultValue="5"
android:entries="@array/sync_interval"
android:entryValues="@array/sync_interval_values"
android:key="syncInterval"
Expand Down
9 changes: 3 additions & 6 deletions plan-V1.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ sync/tombstones/<table>_<key>.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()`。
Expand Down Expand Up @@ -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 变更时重排周期任务
Expand All @@ -185,7 +186,3 @@ M4 — UI/设置
- ConflictResolver 新增 keepBoth()(两端各留一份、消除冲突)
- SyncConflictDao 新增 observeAll()/pendingCount
- 字符串/数组资源、AndroidManifest 注册 ConflictActivity

### 未完成
M5 — 测试
- 冲突算法单测、迁移 androidTest、双模拟器同步演练未做