mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-10-10 18:04:06 +02:00
fix(background): keep library scans and update downloads running with the screen off
Android froze the process once the activity stopped, because only track downloads held a foreground service. A dataSync BackgroundWorkService with a partial wake lock now covers library scans and in-app APK downloads, and stops when the last task ends or the system timeout fires. Its notification shows the existing localized progress, so no duplicate appears. If Android refuses the service, work continues as before. On iOS a scan holds its own background task; on expiry it cancels the scan cleanly instead of being suspended.
This commit is contained in:
1 parent
ad9670eb46
commit
6e6d9dd00e
8 files changed
+455
-25
No files matched your search
@@ -634,6 +634,9 @@ class LocalLibraryNotifier extends Notifier<LocalLibraryState> {
|
||||
scanningSourceId: activeSourceId,
|
||||
);
|
||||
_resetScanNotificationTracking();
|
||||
// Hold before the first progress notification so Android shows a single
|
||||
// foreground-service notification; released in the final cleanup below.
|
||||
await _notificationService.beginLibraryScanWork();
|
||||
if (_shouldShowScanProgressNotification(
|
||||
progress: 0,
|
||||
totalFiles: 0,
|
||||
@@ -944,6 +947,9 @@ class LocalLibraryNotifier extends Notifier<LocalLibraryState> {
|
||||
);
|
||||
await _showScanFailedNotification(e.toString());
|
||||
} finally {
|
||||
await _notificationService.endBackgroundWork(
|
||||
NotificationService.libraryScanWorkKind,
|
||||
);
|
||||
if (securityAccess != null) {
|
||||
await PlatformBridge.stopAccessingIosBookmark(securityAccess);
|
||||
_log.i('Stopped iOS security-scoped access');
|
||||
|
||||
@@ -6,6 +6,7 @@ import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
import 'package:spotiflac_android/constants/app_info.dart';
|
||||
import 'package:spotiflac_android/l10n/app_localizations.dart';
|
||||
import 'package:spotiflac_android/services/platform_bridge.dart';
|
||||
import 'package:spotiflac_android/services/verification_notification.dart';
|
||||
|
||||
class NotificationService {
|
||||
@@ -30,6 +31,72 @@ class NotificationService {
|
||||
String get embeddingMetadataLabel =>
|
||||
_l10n?.notifEmbeddingMetadata ?? 'Embedding metadata...';
|
||||
|
||||
static const String libraryScanWorkKind = 'library_scan';
|
||||
static const String updateDownloadWorkKind = 'app_update';
|
||||
|
||||
// Kinds whose progress the Android foreground-service notification shows,
|
||||
// replacing the plugin's progress notification so only one is visible.
|
||||
final Set<String> _serviceNotificationKinds = {};
|
||||
|
||||
/// Keeps user-started work running when the screen turns off (Android
|
||||
/// foreground service, iOS background task). Pair with [endBackgroundWork].
|
||||
Future<void> beginBackgroundWork(String kind, {required String title}) async {
|
||||
if (!Platform.isAndroid && !Platform.isIOS) return;
|
||||
try {
|
||||
final started = await PlatformBridge.startBackgroundWork(
|
||||
kind,
|
||||
title: title,
|
||||
);
|
||||
if (started && Platform.isAndroid) _serviceNotificationKinds.add(kind);
|
||||
} catch (e) {
|
||||
debugPrint('Background work $kind could not start: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> endBackgroundWork(String kind) async {
|
||||
if (!Platform.isAndroid && !Platform.isIOS) return;
|
||||
_serviceNotificationKinds.remove(kind);
|
||||
try {
|
||||
await PlatformBridge.stopBackgroundWork(kind);
|
||||
} catch (e) {
|
||||
debugPrint('Background work $kind could not stop: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> beginLibraryScanWork() => beginBackgroundWork(
|
||||
libraryScanWorkKind,
|
||||
title: _l10n?.notifScanningLibrary ?? 'Scanning local library',
|
||||
);
|
||||
|
||||
Future<void> beginUpdateDownloadWork({required String version}) =>
|
||||
beginBackgroundWork(
|
||||
updateDownloadWorkKind,
|
||||
title:
|
||||
_l10n?.notifDownloadingUpdate(version) ??
|
||||
'Downloading ${AppInfo.appName} v$version',
|
||||
);
|
||||
|
||||
Future<bool> _showInBackgroundWork(
|
||||
String kind, {
|
||||
required String title,
|
||||
required String body,
|
||||
required int progress,
|
||||
}) async {
|
||||
if (!_serviceNotificationKinds.contains(kind)) return false;
|
||||
try {
|
||||
await PlatformBridge.updateBackgroundWork(
|
||||
kind,
|
||||
title: title,
|
||||
text: body,
|
||||
progress: progress,
|
||||
);
|
||||
return true;
|
||||
} catch (e) {
|
||||
debugPrint('Background work $kind notification failed: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static const int downloadProgressId = 1;
|
||||
static const int updateDownloadId = 2;
|
||||
static const int libraryScanId = 3;
|
||||
@@ -402,10 +469,19 @@ class NotificationService {
|
||||
final body = (currentFile != null && currentFile.isNotEmpty)
|
||||
? '$progressBody\n$currentFile'
|
||||
: progressBody;
|
||||
final title = _l10n?.notifScanningLibrary ?? 'Scanning local library';
|
||||
if (await _showInBackgroundWork(
|
||||
libraryScanWorkKind,
|
||||
title: title,
|
||||
body: body,
|
||||
progress: percentage,
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
|
||||
await _showSafely(
|
||||
id: libraryScanId,
|
||||
title: _l10n?.notifScanningLibrary ?? 'Scanning local library',
|
||||
title: title,
|
||||
body: body,
|
||||
details: _details(library: true, progress: percentage),
|
||||
);
|
||||
@@ -473,15 +549,25 @@ class NotificationService {
|
||||
final percentage = total > 0 ? (received * 100 ~/ total) : 0;
|
||||
final receivedMB = (received / 1024 / 1024).toStringAsFixed(1);
|
||||
final totalMB = (total / 1024 / 1024).toStringAsFixed(1);
|
||||
final title =
|
||||
_l10n?.notifDownloadingUpdate(version) ??
|
||||
'Downloading ${AppInfo.appName} v$version';
|
||||
final body =
|
||||
_l10n?.notifUpdateProgress(receivedMB, totalMB, percentage) ??
|
||||
'$receivedMB / $totalMB MB • $percentage%';
|
||||
if (await _showInBackgroundWork(
|
||||
updateDownloadWorkKind,
|
||||
title: title,
|
||||
body: body,
|
||||
progress: total > 0 ? percentage : -1,
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
|
||||
await _showSafely(
|
||||
id: updateDownloadId,
|
||||
title:
|
||||
_l10n?.notifDownloadingUpdate(version) ??
|
||||
'Downloading ${AppInfo.appName} v$version',
|
||||
body:
|
||||
_l10n?.notifUpdateProgress(receivedMB, totalMB, percentage) ??
|
||||
'$receivedMB / $totalMB MB • $percentage%',
|
||||
title: title,
|
||||
body: body,
|
||||
details: _details(progress: percentage),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1328,6 +1328,42 @@ class PlatformBridge {
|
||||
await _channel.invokeMethod('stopDownloadService');
|
||||
}
|
||||
|
||||
/// Keeps user-started work of [kind] alive with the screen off: an Android
|
||||
/// foreground service, or an iOS background-task grace period. Returns
|
||||
/// whether the platform accepted it.
|
||||
static Future<bool> startBackgroundWork(
|
||||
String kind, {
|
||||
required String title,
|
||||
String text = '',
|
||||
}) async {
|
||||
final started = await _channel.invokeMethod<bool>('startBackgroundWork', {
|
||||
'kind': kind,
|
||||
'title': title,
|
||||
'text': text,
|
||||
});
|
||||
return started ?? false;
|
||||
}
|
||||
|
||||
/// Updates the Android work notification; [progress] is a percentage, or
|
||||
/// negative while indeterminate.
|
||||
static Future<void> updateBackgroundWork(
|
||||
String kind, {
|
||||
required String title,
|
||||
String text = '',
|
||||
int progress = -1,
|
||||
}) async {
|
||||
await _channel.invokeMethod('updateBackgroundWork', {
|
||||
'kind': kind,
|
||||
'title': title,
|
||||
'text': text,
|
||||
'progress': progress,
|
||||
});
|
||||
}
|
||||
|
||||
static Future<void> stopBackgroundWork(String kind) async {
|
||||
await _channel.invokeMethod('stopBackgroundWork', {'kind': kind});
|
||||
}
|
||||
|
||||
static Future<void> updateDownloadServiceProgress({
|
||||
required String trackName,
|
||||
required String artistName,
|
||||
|
||||
@@ -69,26 +69,38 @@ class _UpdateDialogState extends State<UpdateDialog> {
|
||||
|
||||
final notificationService = NotificationService();
|
||||
|
||||
final filePath = await ApkDownloader.downloadApk(
|
||||
url: apkUrl,
|
||||
// Without a foreground service the download freezes when the screen
|
||||
// turns off; its notification then carries the progress.
|
||||
await notificationService.beginUpdateDownloadWork(
|
||||
version: widget.updateInfo.version,
|
||||
expectedSha256: widget.updateInfo.apkSha256,
|
||||
onProgress: (received, total) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_progress = total > 0 ? received / total : 0;
|
||||
final receivedMB = formatMegabytes(received);
|
||||
final totalMB = formatMegabytes(total);
|
||||
_statusText = '$receivedMB / $totalMB MB';
|
||||
});
|
||||
}
|
||||
notificationService.showUpdateDownloadProgress(
|
||||
version: widget.updateInfo.version,
|
||||
received: received,
|
||||
total: total,
|
||||
);
|
||||
},
|
||||
);
|
||||
final String? filePath;
|
||||
try {
|
||||
filePath = await ApkDownloader.downloadApk(
|
||||
url: apkUrl,
|
||||
version: widget.updateInfo.version,
|
||||
expectedSha256: widget.updateInfo.apkSha256,
|
||||
onProgress: (received, total) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_progress = total > 0 ? received / total : 0;
|
||||
final receivedMB = formatMegabytes(received);
|
||||
final totalMB = formatMegabytes(total);
|
||||
_statusText = '$receivedMB / $totalMB MB';
|
||||
});
|
||||
}
|
||||
notificationService.showUpdateDownloadProgress(
|
||||
version: widget.updateInfo.version,
|
||||
received: received,
|
||||
total: total,
|
||||
);
|
||||
},
|
||||
);
|
||||
} finally {
|
||||
await notificationService.endBackgroundWork(
|
||||
NotificationService.updateDownloadWorkKind,
|
||||
);
|
||||
}
|
||||
|
||||
if (filePath != null) {
|
||||
await notificationService.cancelUpdateNotification();
|
||||
|
||||
Reference in new issue
Block a user