mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-07-25 21:40:57 +02:00
fix(downloads): defer verification challenge until app is foregrounded
When a download hit a verification challenge while the app was in the background, the browser launch was attempted immediately (blocked or silently dropped by the OS) and the 5-minute grant timeout kept running, so the item failed without the user ever seeing the CAPTCHA. Wait for AppLifecycleState.resumed before opening the challenge and before starting the grant timeout, and post a local notification asking the user to return to the app. If the process is killed while waiting, the persisted queue already restores the item as queued on next launch.
This commit is contained in:
@@ -6213,6 +6213,18 @@ abstract class AppLocalizations {
|
||||
/// **'Downloads Finished ({completed} done, {failed} failed)'**
|
||||
String notifDownloadsFinished(int completed, int failed);
|
||||
|
||||
/// Notification title shown when a download needs the user to complete a verification challenge but the app is in the background
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Verification required'**
|
||||
String get notifVerificationRequiredTitle;
|
||||
|
||||
/// Notification body prompting the user to return to the app to solve a verification challenge
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Open the app to complete verification and resume downloads'**
|
||||
String get notifVerificationRequiredBody;
|
||||
|
||||
/// Notification title when all downloads finish successfully
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
||||
@@ -3685,6 +3685,13 @@ class AppLocalizationsAr extends AppLocalizations {
|
||||
return 'Downloads Finished ($completed done, $failed failed)';
|
||||
}
|
||||
|
||||
@override
|
||||
String get notifVerificationRequiredTitle => 'Verification required';
|
||||
|
||||
@override
|
||||
String get notifVerificationRequiredBody =>
|
||||
'Open the app to complete verification and resume downloads';
|
||||
|
||||
@override
|
||||
String get notifAllDownloadsComplete => 'All Downloads Complete';
|
||||
|
||||
|
||||
@@ -3733,6 +3733,13 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
return 'Downloads abgeschlossen ($completed fertig, $failed fehlgeschlagen)';
|
||||
}
|
||||
|
||||
@override
|
||||
String get notifVerificationRequiredTitle => 'Verification required';
|
||||
|
||||
@override
|
||||
String get notifVerificationRequiredBody =>
|
||||
'Open the app to complete verification and resume downloads';
|
||||
|
||||
@override
|
||||
String get notifAllDownloadsComplete => 'Alle Downloads abgeschlossen';
|
||||
|
||||
|
||||
@@ -3685,6 +3685,13 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
return 'Downloads Finished ($completed done, $failed failed)';
|
||||
}
|
||||
|
||||
@override
|
||||
String get notifVerificationRequiredTitle => 'Verification required';
|
||||
|
||||
@override
|
||||
String get notifVerificationRequiredBody =>
|
||||
'Open the app to complete verification and resume downloads';
|
||||
|
||||
@override
|
||||
String get notifAllDownloadsComplete => 'All Downloads Complete';
|
||||
|
||||
|
||||
@@ -3685,6 +3685,13 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
return 'Downloads Finished ($completed done, $failed failed)';
|
||||
}
|
||||
|
||||
@override
|
||||
String get notifVerificationRequiredTitle => 'Verification required';
|
||||
|
||||
@override
|
||||
String get notifVerificationRequiredBody =>
|
||||
'Open the app to complete verification and resume downloads';
|
||||
|
||||
@override
|
||||
String get notifAllDownloadsComplete => 'All Downloads Complete';
|
||||
|
||||
|
||||
@@ -3785,6 +3785,13 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
return 'Téléchargements terminés ($completed terminé, $failed en échec)';
|
||||
}
|
||||
|
||||
@override
|
||||
String get notifVerificationRequiredTitle => 'Verification required';
|
||||
|
||||
@override
|
||||
String get notifVerificationRequiredBody =>
|
||||
'Open the app to complete verification and resume downloads';
|
||||
|
||||
@override
|
||||
String get notifAllDownloadsComplete =>
|
||||
'Tous les téléchargements sont terminés';
|
||||
|
||||
@@ -3685,6 +3685,13 @@ class AppLocalizationsHi extends AppLocalizations {
|
||||
return 'Downloads Finished ($completed done, $failed failed)';
|
||||
}
|
||||
|
||||
@override
|
||||
String get notifVerificationRequiredTitle => 'Verification required';
|
||||
|
||||
@override
|
||||
String get notifVerificationRequiredBody =>
|
||||
'Open the app to complete verification and resume downloads';
|
||||
|
||||
@override
|
||||
String get notifAllDownloadsComplete => 'All Downloads Complete';
|
||||
|
||||
|
||||
@@ -3668,6 +3668,13 @@ class AppLocalizationsId extends AppLocalizations {
|
||||
return 'Downloads Finished ($completed done, $failed failed)';
|
||||
}
|
||||
|
||||
@override
|
||||
String get notifVerificationRequiredTitle => 'Verification required';
|
||||
|
||||
@override
|
||||
String get notifVerificationRequiredBody =>
|
||||
'Open the app to complete verification and resume downloads';
|
||||
|
||||
@override
|
||||
String get notifAllDownloadsComplete => 'All Downloads Complete';
|
||||
|
||||
|
||||
@@ -3672,6 +3672,13 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
return 'Downloads Finished ($completed done, $failed failed)';
|
||||
}
|
||||
|
||||
@override
|
||||
String get notifVerificationRequiredTitle => 'Verification required';
|
||||
|
||||
@override
|
||||
String get notifVerificationRequiredBody =>
|
||||
'Open the app to complete verification and resume downloads';
|
||||
|
||||
@override
|
||||
String get notifAllDownloadsComplete => 'All Downloads Complete';
|
||||
|
||||
|
||||
@@ -3670,6 +3670,13 @@ class AppLocalizationsKo extends AppLocalizations {
|
||||
return 'Downloads Finished ($completed done, $failed failed)';
|
||||
}
|
||||
|
||||
@override
|
||||
String get notifVerificationRequiredTitle => 'Verification required';
|
||||
|
||||
@override
|
||||
String get notifVerificationRequiredBody =>
|
||||
'Open the app to complete verification and resume downloads';
|
||||
|
||||
@override
|
||||
String get notifAllDownloadsComplete => 'All Downloads Complete';
|
||||
|
||||
|
||||
@@ -3685,6 +3685,13 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
return 'Downloads Finished ($completed done, $failed failed)';
|
||||
}
|
||||
|
||||
@override
|
||||
String get notifVerificationRequiredTitle => 'Verification required';
|
||||
|
||||
@override
|
||||
String get notifVerificationRequiredBody =>
|
||||
'Open the app to complete verification and resume downloads';
|
||||
|
||||
@override
|
||||
String get notifAllDownloadsComplete => 'All Downloads Complete';
|
||||
|
||||
|
||||
@@ -3685,6 +3685,13 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
return 'Downloads Finished ($completed done, $failed failed)';
|
||||
}
|
||||
|
||||
@override
|
||||
String get notifVerificationRequiredTitle => 'Verification required';
|
||||
|
||||
@override
|
||||
String get notifVerificationRequiredBody =>
|
||||
'Open the app to complete verification and resume downloads';
|
||||
|
||||
@override
|
||||
String get notifAllDownloadsComplete => 'All Downloads Complete';
|
||||
|
||||
|
||||
@@ -3740,6 +3740,13 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
return 'Загрузка завершена ($completed завершено, $failed не удалось)';
|
||||
}
|
||||
|
||||
@override
|
||||
String get notifVerificationRequiredTitle => 'Verification required';
|
||||
|
||||
@override
|
||||
String get notifVerificationRequiredBody =>
|
||||
'Open the app to complete verification and resume downloads';
|
||||
|
||||
@override
|
||||
String get notifAllDownloadsComplete => 'Все загрузки завершены';
|
||||
|
||||
|
||||
@@ -3716,6 +3716,13 @@ class AppLocalizationsTr extends AppLocalizations {
|
||||
return 'Downloads Finished ($completed done, $failed failed)';
|
||||
}
|
||||
|
||||
@override
|
||||
String get notifVerificationRequiredTitle => 'Verification required';
|
||||
|
||||
@override
|
||||
String get notifVerificationRequiredBody =>
|
||||
'Open the app to complete verification and resume downloads';
|
||||
|
||||
@override
|
||||
String get notifAllDownloadsComplete => 'All Downloads Complete';
|
||||
|
||||
|
||||
@@ -3738,6 +3738,13 @@ class AppLocalizationsUk extends AppLocalizations {
|
||||
return 'Завантаження завершено ($completed завершено, $failed не вдалося)';
|
||||
}
|
||||
|
||||
@override
|
||||
String get notifVerificationRequiredTitle => 'Verification required';
|
||||
|
||||
@override
|
||||
String get notifVerificationRequiredBody =>
|
||||
'Open the app to complete verification and resume downloads';
|
||||
|
||||
@override
|
||||
String get notifAllDownloadsComplete => 'Усі завантаження завершено';
|
||||
|
||||
|
||||
@@ -3685,6 +3685,13 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
return 'Downloads Finished ($completed done, $failed failed)';
|
||||
}
|
||||
|
||||
@override
|
||||
String get notifVerificationRequiredTitle => 'Verification required';
|
||||
|
||||
@override
|
||||
String get notifVerificationRequiredBody =>
|
||||
'Open the app to complete verification and resume downloads';
|
||||
|
||||
@override
|
||||
String get notifAllDownloadsComplete => 'All Downloads Complete';
|
||||
|
||||
|
||||
@@ -4782,6 +4782,14 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"notifVerificationRequiredTitle": "Verification required",
|
||||
"@notifVerificationRequiredTitle": {
|
||||
"description": "Notification title shown when a download needs the user to complete a verification challenge but the app is in the background"
|
||||
},
|
||||
"notifVerificationRequiredBody": "Open the app to complete verification and resume downloads",
|
||||
"@notifVerificationRequiredBody": {
|
||||
"description": "Notification body prompting the user to return to the app to solve a verification challenge"
|
||||
},
|
||||
"notifAllDownloadsComplete": "All Downloads Complete",
|
||||
"@notifAllDownloadsComplete": {
|
||||
"description": "Notification title when all downloads finish successfully"
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'dart:async';
|
||||
import 'dart:math';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:connectivity_plus/connectivity_plus.dart';
|
||||
@@ -2095,10 +2096,39 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Completes when the app is in the foreground. Verification challenges
|
||||
/// can only be handled there: launching a browser from the background is
|
||||
/// blocked by the OS and the challenge would expire unseen.
|
||||
Future<void> _waitForForeground() async {
|
||||
if (WidgetsBinding.instance.lifecycleState == AppLifecycleState.resumed) {
|
||||
return;
|
||||
}
|
||||
final completer = Completer<void>();
|
||||
final listener = AppLifecycleListener(
|
||||
onResume: () {
|
||||
if (!completer.isCompleted) completer.complete();
|
||||
},
|
||||
);
|
||||
try {
|
||||
await completer.future;
|
||||
} finally {
|
||||
listener.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> _openVerificationAndWait(String extensionId) async {
|
||||
final normalizedExtensionId = extensionId.trim();
|
||||
if (normalizedExtensionId.isEmpty) return false;
|
||||
|
||||
if (WidgetsBinding.instance.lifecycleState != AppLifecycleState.resumed) {
|
||||
_log.i(
|
||||
'Verification required for $normalizedExtensionId while app is in '
|
||||
'background; deferring challenge until the app is foregrounded',
|
||||
);
|
||||
unawaited(_notificationService.showVerificationRequired());
|
||||
await _waitForForeground();
|
||||
}
|
||||
|
||||
final grantEventFuture = PlatformBridge.extensionSessionGrantEvents()
|
||||
.where((event) => event.extensionId == normalizedExtensionId)
|
||||
.first
|
||||
|
||||
@@ -334,6 +334,46 @@ class NotificationService {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> showVerificationRequired() async {
|
||||
if (!_isInitialized) await initialize();
|
||||
unawaited(HapticFeedback.mediumImpact());
|
||||
|
||||
final title =
|
||||
_l10n?.notifVerificationRequiredTitle ?? 'Verification required';
|
||||
final body =
|
||||
_l10n?.notifVerificationRequiredBody ??
|
||||
'Open the app to complete verification and resume downloads';
|
||||
|
||||
const androidDetails = AndroidNotificationDetails(
|
||||
channelId,
|
||||
channelName,
|
||||
channelDescription: channelDescription,
|
||||
importance: Importance.defaultImportance,
|
||||
priority: Priority.defaultPriority,
|
||||
autoCancel: true,
|
||||
playSound: true,
|
||||
icon: '@mipmap/ic_launcher',
|
||||
);
|
||||
|
||||
const iosDetails = DarwinNotificationDetails(
|
||||
presentAlert: true,
|
||||
presentBadge: true,
|
||||
presentSound: true,
|
||||
);
|
||||
|
||||
const details = NotificationDetails(
|
||||
android: androidDetails,
|
||||
iOS: iosDetails,
|
||||
);
|
||||
|
||||
await _showSafely(
|
||||
id: downloadProgressId,
|
||||
title: title,
|
||||
body: body,
|
||||
details: details,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> showQueueCanceled({required int canceledCount}) async {
|
||||
if (!_isInitialized) await initialize();
|
||||
if (canceledCount <= 0) return;
|
||||
|
||||
Reference in New Issue
Block a user