mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-08-27 13:22:49 +02:00
fix(download): recover queue after worker timeout (#524)
This commit is contained in:
@@ -467,8 +467,58 @@ class DownloadService : Service() {
|
||||
*/
|
||||
override fun onTimeout(startId: Int, fgsType: Int) {
|
||||
android.util.Log.w("DownloadService", "Foreground service timeout reached (6 hours limit). Stopping service.")
|
||||
|
||||
stopForegroundService()
|
||||
|
||||
stopNativeWorkerForSystemTimeout()
|
||||
}
|
||||
|
||||
private fun stopNativeWorkerForSystemTimeout() {
|
||||
if (!hasNativeWorkerState()) {
|
||||
stopForegroundService()
|
||||
return
|
||||
}
|
||||
nativeWorkerCancelRequested = true
|
||||
// Supersede the coroutine before cancelling it. Its catch/finally
|
||||
// blocks must not publish a skipped/finished state over the recovery
|
||||
// snapshot written below.
|
||||
nativeWorkerGeneration++
|
||||
val activeItemIds = synchronized(nativeWorkerItems) {
|
||||
nativeWorkerItems
|
||||
.filter { NativeWorkerPolicy.statusAfterWorkerStop(it.status) != it.status }
|
||||
.map { it.itemId }
|
||||
}
|
||||
for (itemId in activeItemIds) {
|
||||
try {
|
||||
Gobackend.cancelDownload(itemId)
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
}
|
||||
NativeDownloadFinalizer.cancelActiveWork()
|
||||
nativeWorkerJob?.cancel(
|
||||
CancellationException("Native queue stopped by Android timeout")
|
||||
)
|
||||
synchronized(nativeWorkerItems) {
|
||||
for (item in nativeWorkerItems) {
|
||||
val recoveredStatus = NativeWorkerPolicy.statusAfterWorkerStop(item.status)
|
||||
if (recoveredStatus == item.status) continue
|
||||
item.status = recoveredStatus
|
||||
item.progress = 0.0
|
||||
item.bytesReceived = 0L
|
||||
item.bytesTotal = 0L
|
||||
item.error = ""
|
||||
item.resultJson = null
|
||||
}
|
||||
}
|
||||
nativeWorkerCurrentItemId = ""
|
||||
writeNativeWorkerSnapshot(
|
||||
isRunning = false,
|
||||
isPaused = false,
|
||||
currentItemId = "",
|
||||
message = "Android background time limit reached",
|
||||
includeItems = true,
|
||||
)
|
||||
// Cancellation and the final recovery snapshot were handled above.
|
||||
// Only tear down the foreground-service resources here.
|
||||
stopForegroundService(cancelNativeWorker = false)
|
||||
}
|
||||
|
||||
private fun createNotificationChannel() {
|
||||
@@ -981,7 +1031,9 @@ class DownloadService : Service() {
|
||||
)
|
||||
}
|
||||
} catch (e: CancellationException) {
|
||||
if (nativeWorkerCancelRequested) {
|
||||
if (nativeWorkerCancelRequested &&
|
||||
generation == nativeWorkerGeneration
|
||||
) {
|
||||
updateNativeWorkerItem(request.itemId) {
|
||||
it.status = "skipped"
|
||||
it.error = "Cancelled"
|
||||
|
||||
@@ -68,4 +68,9 @@ internal object NativeWorkerPolicy {
|
||||
completed: Int,
|
||||
failed: Int,
|
||||
): Boolean = !cancelRequested && completed + failed > 0
|
||||
|
||||
fun statusAfterWorkerStop(status: String): String = when (status) {
|
||||
"preparing", "downloading", "finalizing" -> "queued"
|
||||
else -> status
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,4 +149,15 @@ class NativeWorkerPolicyTest {
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun stoppedWorkerRequeuesOnlyInFlightItems() {
|
||||
listOf("preparing", "downloading", "finalizing").forEach { status ->
|
||||
assertEquals("queued", NativeWorkerPolicy.statusAfterWorkerStop(status))
|
||||
}
|
||||
assertEquals("queued", NativeWorkerPolicy.statusAfterWorkerStop("queued"))
|
||||
assertEquals("completed", NativeWorkerPolicy.statusAfterWorkerStop("completed"))
|
||||
assertEquals("failed", NativeWorkerPolicy.statusAfterWorkerStop("failed"))
|
||||
assertEquals("skipped", NativeWorkerPolicy.statusAfterWorkerStop("skipped"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,6 +75,17 @@ bool canStartForegroundDownloadForLifecycle(AppLifecycleState? lifecycleState) {
|
||||
return lifecycleState == AppLifecycleState.resumed;
|
||||
}
|
||||
|
||||
String nativeWorkerStatusAfterSnapshotStop({
|
||||
required bool workerRunning,
|
||||
required String status,
|
||||
}) {
|
||||
if (workerRunning) return status;
|
||||
return switch (status) {
|
||||
'preparing' || 'downloading' || 'finalizing' => 'queued',
|
||||
_ => status,
|
||||
};
|
||||
}
|
||||
|
||||
/// Keeps a download in its finalizing state until its durable Library record
|
||||
/// has been written. If persistence fails, completion is deliberately not
|
||||
/// published so the queue can surface the error instead of losing the file
|
||||
|
||||
@@ -863,6 +863,7 @@ extension _DownloadQueueNativeWorker on DownloadQueueNotifier {
|
||||
if (itemSnapshots.isEmpty) {
|
||||
return;
|
||||
}
|
||||
final workerRunning = snapshot['is_running'] == true;
|
||||
|
||||
for (final itemSnapshot in itemSnapshots) {
|
||||
final itemId = itemSnapshot['item_id']?.toString() ?? '';
|
||||
@@ -872,7 +873,10 @@ extension _DownloadQueueNativeWorker on DownloadQueueNotifier {
|
||||
final context = contexts[itemId];
|
||||
if (context == null) continue;
|
||||
|
||||
final status = itemSnapshot['status']?.toString() ?? 'queued';
|
||||
final status = nativeWorkerStatusAfterSnapshotStop(
|
||||
workerRunning: workerRunning,
|
||||
status: itemSnapshot['status']?.toString() ?? 'queued',
|
||||
);
|
||||
final progress = ((itemSnapshot['progress'] as num?)?.toDouble() ?? 0.0)
|
||||
.clamp(0.0, 1.0)
|
||||
.toDouble();
|
||||
|
||||
@@ -51,4 +51,42 @@ void main() {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('native worker stop recovery', () {
|
||||
test('requeues every in-flight snapshot state after the worker stops', () {
|
||||
for (final status in const ['preparing', 'downloading', 'finalizing']) {
|
||||
expect(
|
||||
nativeWorkerStatusAfterSnapshotStop(
|
||||
workerRunning: false,
|
||||
status: status,
|
||||
),
|
||||
'queued',
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('preserves terminal states and live worker progress', () {
|
||||
expect(
|
||||
nativeWorkerStatusAfterSnapshotStop(
|
||||
workerRunning: false,
|
||||
status: 'completed',
|
||||
),
|
||||
'completed',
|
||||
);
|
||||
expect(
|
||||
nativeWorkerStatusAfterSnapshotStop(
|
||||
workerRunning: false,
|
||||
status: 'failed',
|
||||
),
|
||||
'failed',
|
||||
);
|
||||
expect(
|
||||
nativeWorkerStatusAfterSnapshotStop(
|
||||
workerRunning: true,
|
||||
status: 'downloading',
|
||||
),
|
||||
'downloading',
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user