mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-09-27 03:41:47 +02:00
fix(download): route verification notifications to pending challenges
This commit is contained in:
@@ -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/verification_notification.dart';
|
||||
|
||||
class NotificationService {
|
||||
static final NotificationService _instance = NotificationService._internal();
|
||||
@@ -15,6 +16,8 @@ class NotificationService {
|
||||
final FlutterLocalNotificationsPlugin _notifications =
|
||||
FlutterLocalNotificationsPlugin();
|
||||
bool _isInitialized = false;
|
||||
Future<void>? _initialization;
|
||||
final verificationNotifications = VerificationNotificationRouter();
|
||||
bool _notificationPermissionRequested = false;
|
||||
AppLocalizations? _l10n;
|
||||
|
||||
@@ -43,9 +46,14 @@ class NotificationService {
|
||||
static const String libraryChannelDescription =
|
||||
'Shows local library scan progress';
|
||||
|
||||
Future<void> initialize() async {
|
||||
if (_isInitialized) return;
|
||||
Future<void> initialize() {
|
||||
if (_isInitialized) return Future.value();
|
||||
return _initialization ??= _initialize().whenComplete(() {
|
||||
_initialization = null;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _initialize() async {
|
||||
const androidSettings = AndroidInitializationSettings(
|
||||
'@mipmap/ic_launcher',
|
||||
);
|
||||
@@ -60,7 +68,16 @@ class NotificationService {
|
||||
iOS: iosSettings,
|
||||
);
|
||||
|
||||
await _notifications.initialize(settings: initSettings);
|
||||
await _notifications.initialize(
|
||||
settings: initSettings,
|
||||
onDidReceiveNotificationResponse: (response) {
|
||||
verificationNotifications.receive(response.payload);
|
||||
},
|
||||
);
|
||||
final launch = await _notifications.getNotificationAppLaunchDetails();
|
||||
if (launch?.didNotificationLaunchApp == true) {
|
||||
verificationNotifications.receive(launch?.notificationResponse?.payload);
|
||||
}
|
||||
|
||||
if (!kIsWeb && defaultTargetPlatform == TargetPlatform.android) {
|
||||
final androidImpl = _notifications
|
||||
@@ -126,6 +143,7 @@ class NotificationService {
|
||||
required String title,
|
||||
required String body,
|
||||
required NotificationDetails details,
|
||||
String? payload,
|
||||
}) async {
|
||||
if (!await _ensureNotificationPermission()) return;
|
||||
|
||||
@@ -135,6 +153,7 @@ class NotificationService {
|
||||
title: title,
|
||||
body: body,
|
||||
notificationDetails: details,
|
||||
payload: payload,
|
||||
);
|
||||
} on PlatformException catch (e) {
|
||||
final isNotificationsNotAllowed =
|
||||
@@ -305,7 +324,10 @@ class NotificationService {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> showVerificationRequired() async {
|
||||
Future<void> showVerificationRequired({
|
||||
required String extensionId,
|
||||
required String itemId,
|
||||
}) async {
|
||||
if (!_isInitialized) await initialize();
|
||||
unawaited(HapticFeedback.mediumImpact());
|
||||
|
||||
@@ -319,6 +341,11 @@ class NotificationService {
|
||||
id: verificationRequiredId,
|
||||
title: title,
|
||||
body: body,
|
||||
payload: VerificationNotification(
|
||||
extensionId: extensionId,
|
||||
itemId: itemId,
|
||||
tapId: 'dart:${DateTime.now().microsecondsSinceEpoch}',
|
||||
).encode(),
|
||||
details: _details(
|
||||
playSound: true,
|
||||
presentBadge: true,
|
||||
|
||||
@@ -203,6 +203,8 @@ class PlatformBridge {
|
||||
StreamController<ExtensionSessionGrantEvent>.broadcast();
|
||||
static final StreamController<void> _libraryStorageEvents =
|
||||
StreamController<void>.broadcast();
|
||||
static final StreamController<void> _verificationNotificationEvents =
|
||||
StreamController<void>.broadcast();
|
||||
static final StreamController<List<String>>
|
||||
_iosBackgroundDownloadExpirationEvents =
|
||||
StreamController<List<String>>.broadcast();
|
||||
@@ -223,6 +225,14 @@ class PlatformBridge {
|
||||
return _libraryStorageEvents.stream;
|
||||
}
|
||||
|
||||
static Stream<void> verificationNotificationEvents() {
|
||||
_ensureBackendEventHandler();
|
||||
return _verificationNotificationEvents.stream;
|
||||
}
|
||||
|
||||
static Future<String?> consumeVerificationNotification() =>
|
||||
_channel.invokeMethod<String>('consumeVerificationNotification');
|
||||
|
||||
static Stream<List<String>> iosBackgroundDownloadExpirationEvents() {
|
||||
_ensureBackendEventHandler();
|
||||
return _iosBackgroundDownloadExpirationEvents.stream;
|
||||
@@ -250,6 +260,9 @@ class PlatformBridge {
|
||||
case 'libraryStorageChanged':
|
||||
_libraryStorageEvents.add(null);
|
||||
return null;
|
||||
case 'extensionVerificationNotificationTapped':
|
||||
_verificationNotificationEvents.add(null);
|
||||
return null;
|
||||
case 'iosBackgroundDownloadExpired':
|
||||
final raw = call.arguments;
|
||||
var itemIds = const <String>[];
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
class VerificationNotification {
|
||||
const VerificationNotification({
|
||||
required this.extensionId,
|
||||
required this.itemId,
|
||||
required this.tapId,
|
||||
});
|
||||
|
||||
final String extensionId;
|
||||
final String itemId;
|
||||
final String tapId;
|
||||
|
||||
String encode() => jsonEncode({
|
||||
'kind': 'extension_verification',
|
||||
'extension_id': extensionId,
|
||||
'item_id': itemId,
|
||||
'tap_id': tapId,
|
||||
});
|
||||
|
||||
static VerificationNotification? parse(Object? payload) {
|
||||
try {
|
||||
final value = payload is String ? jsonDecode(payload) : payload;
|
||||
if (value is! Map || value['kind'] != 'extension_verification') {
|
||||
return null;
|
||||
}
|
||||
final extensionId = value['extension_id'];
|
||||
final itemId = value['item_id'];
|
||||
final tapId = value['tap_id'];
|
||||
if (extensionId is! String ||
|
||||
extensionId.trim().isEmpty ||
|
||||
itemId is! String ||
|
||||
itemId.trim().isEmpty ||
|
||||
tapId is! String ||
|
||||
tapId.trim().isEmpty) {
|
||||
return null;
|
||||
}
|
||||
return VerificationNotification(
|
||||
extensionId: extensionId.trim(),
|
||||
itemId: itemId.trim(),
|
||||
tapId: tapId.trim(),
|
||||
);
|
||||
} on FormatException {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Holds cold-start taps until extensions and queue restoration are ready.
|
||||
/// A repeated native/plugin delivery must not launch a second challenge.
|
||||
class VerificationNotificationRouter {
|
||||
final Map<String, VerificationNotification> _pending = {};
|
||||
final Set<String> _handled = {};
|
||||
final Set<String> _active = {};
|
||||
Future<void> Function(VerificationNotification)? _handler;
|
||||
|
||||
void setHandler(Future<void> Function(VerificationNotification)? handler) {
|
||||
_handler = handler;
|
||||
_drain();
|
||||
}
|
||||
|
||||
void receive(Object? payload) {
|
||||
final target = VerificationNotification.parse(payload);
|
||||
if (target == null ||
|
||||
_handled.contains(target.tapId) ||
|
||||
_active.contains(target.tapId)) {
|
||||
return;
|
||||
}
|
||||
_pending.putIfAbsent(target.tapId, () => target);
|
||||
_drain();
|
||||
}
|
||||
|
||||
void _drain() {
|
||||
final handler = _handler;
|
||||
if (handler == null) return;
|
||||
for (final target in _pending.values.toList()) {
|
||||
_pending.remove(target.tapId);
|
||||
_active.add(target.tapId);
|
||||
// The queue deduplicates flows per extension. A different provider's
|
||||
// notification must not wait behind an unrelated grant timeout.
|
||||
unawaited(
|
||||
Future<void>.sync(() => handler(target)).whenComplete(() {
|
||||
_active.remove(target.tapId);
|
||||
_handled.add(target.tapId);
|
||||
if (_handled.length > 32) _handled.remove(_handled.first);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user