923a6a01bd
Registration failures from the distributor were swallowed, so push looked enabled while nothing arrived. Without wake-ups Android suspends the app and messages only show up once the user opens it — which looks like broken delivery even though the relay has them queued. The failure is now remembered and surfaced in Settings, and the chat list carries a dismissible line explaining the state.
151 lines
5.5 KiB
Dart
151 lines
5.5 KiB
Dart
/// Vstupní vrstva UnifiedPush — registruje se z main() PŘED runApp,
|
|
/// protože Android budí zabitou appku s argumentem `--unifiedpush-bg`
|
|
/// a bez UI: callbacky pak musí existovat dřív, než (ne)vznikne widget
|
|
/// tree.
|
|
///
|
|
/// Dvě situace při příchodu budíčku:
|
|
/// - appka běží (messaging nastartován) → wake: připoj WS, vylej frontu
|
|
/// - killed/zamčeno → DB je za PINem, nic nejde dešifrovat →
|
|
/// jen generická lokální notifikace (PushNotifier); zprávy se
|
|
/// stáhnou, až uživatel appku odemkne
|
|
///
|
|
/// Nový endpoint doručený na pozadí se NEDÁ poslat relayi (vyžaduje
|
|
/// autentizovaný WS + krypto z DB) → uloží se do OS keystore a odešle
|
|
/// při příštím plném startu (PushService.init ho vyzvedne).
|
|
library;
|
|
|
|
import 'dart:async';
|
|
import 'dart:io' show Platform;
|
|
|
|
import 'package:firebase_core/firebase_core.dart';
|
|
import 'package:firebase_messaging/firebase_messaging.dart';
|
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
|
import 'package:sepot/core/push/apns_ios.dart';
|
|
import 'package:sepot/core/push/push_notifier.dart';
|
|
import 'package:sepot/core/push/push_service.dart';
|
|
import 'package:unifiedpush/unifiedpush.dart';
|
|
|
|
/// FCM budíček se zabitou appkou — plugin spouští handler ve vlastním
|
|
/// background isolate. Stejný model jako UnifiedPush killed-state:
|
|
/// DB je zamčená, nic nejde dešifrovat → jen generická notifikace.
|
|
@pragma('vm:entry-point')
|
|
Future<void> sepotFcmBackgroundWake(RemoteMessage message) async {
|
|
await Firebase.initializeApp();
|
|
await PushNotifier.showWakeNotification();
|
|
}
|
|
|
|
class PushBootstrap {
|
|
PushBootstrap._();
|
|
|
|
/// True = proces nastartoval Android kvůli pushi (`--unifiedpush-bg`),
|
|
/// bez UI. main() pak NEvolá runApp.
|
|
static bool backgroundStart = false;
|
|
|
|
static PushService? _service;
|
|
|
|
static const _pendingEndpointKey = 'pending_push_endpoint';
|
|
static const _modeKey = 'push_mode'; // 'off' | 'up' | 'fcm'
|
|
static const _registrationFailedKey = 'push_registration_failed';
|
|
static const _store = FlutterSecureStorage(
|
|
aOptions: AndroidOptions(encryptedSharedPreferences: true),
|
|
);
|
|
|
|
/// Režim pushů žije v OS keystore (NE v SQLCipher DB) — musí být
|
|
/// čitelný před unlockem, aby main() věděl, zda registrovat FCM.
|
|
static Future<String> readMode() async =>
|
|
await _store.read(key: _modeKey) ?? 'off';
|
|
|
|
static Future<void> writeMode(String mode) =>
|
|
_store.write(key: _modeKey, value: mode);
|
|
|
|
/// Odmítl distributor registraci? Selhání se dřív jen spolklo, takže
|
|
/// uživatel měl v Nastavení „push zapnutý", ale budíčky nechodily a
|
|
/// zprávy čekaly, než appku otevře. Nastavení si to přečte a řekne to.
|
|
static Future<bool> registrationFailed() async =>
|
|
await _store.read(key: _registrationFailedKey) == '1';
|
|
|
|
static Future<void> _setRegistrationFailed(bool failed) => failed
|
|
? _store.write(key: _registrationFailedKey, value: '1')
|
|
: _store.delete(key: _registrationFailedKey);
|
|
|
|
static Future<void> init(List<String> args) async {
|
|
backgroundStart = args.contains('--unifiedpush-bg');
|
|
|
|
// iOS: nativní APNs most. onWake musí fungovat i v killed-state
|
|
// (iOS probudí engine tichým pushem), proto handler registrujeme tady,
|
|
// před runApp. Messaging se napojí přes attach()/PushService.init.
|
|
if (Platform.isIOS) {
|
|
ApnsBridge.onWake = _wake;
|
|
ApnsBridge.ensureHandler();
|
|
} else {
|
|
await UnifiedPush.initialize(
|
|
onNewEndpoint: _onNewEndpoint,
|
|
onRegistrationFailed: (_, _) =>
|
|
unawaited(_setRegistrationFailed(true)),
|
|
onUnregistered: _onUnregistered,
|
|
onMessage: _onMessage,
|
|
);
|
|
}
|
|
|
|
// Firebase se dotkne Googlu (installations FID) → inicializovat
|
|
// VÝHRADNĚ když si uživatel FCM režim sám zapnul.
|
|
if (!Platform.isLinux && await readMode() == 'fcm') {
|
|
try {
|
|
await Firebase.initializeApp();
|
|
FirebaseMessaging.onBackgroundMessage(sepotFcmBackgroundWake);
|
|
} catch (_) {
|
|
// bez google-services konfigurace / na desktopu — FCM nedostupné
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Napojí plnou appku (messaging vrstvu) na už zaregistrované callbacky.
|
|
static void attach(PushService service) {
|
|
_service = service;
|
|
}
|
|
|
|
/// Sdílené probuzení (UnifiedPush i APNs): appka běží → připoj WS a
|
|
/// vylej frontu; killed/zamčeno → generická notifikace bez obsahu.
|
|
static void _wake() {
|
|
final s = _service;
|
|
if (s != null && s.canWake) {
|
|
s.wake();
|
|
} else {
|
|
unawaited(PushNotifier.showWakeNotification());
|
|
}
|
|
}
|
|
|
|
static void _onMessage(PushMessage message, String instance) {
|
|
// Obsah budíčku se ZÁMĚRNĚ ignoruje (konstantní ťuk {"v":1}).
|
|
final s = _service;
|
|
if (s != null && s.canWake) {
|
|
s.wake();
|
|
} else {
|
|
unawaited(PushNotifier.showWakeNotification());
|
|
}
|
|
}
|
|
|
|
static void _onNewEndpoint(PushEndpoint endpoint, String instance) {
|
|
unawaited(_setRegistrationFailed(false)); // registrace se povedla
|
|
final s = _service;
|
|
if (s != null) {
|
|
s.handleNewEndpoint(endpoint.url);
|
|
} else {
|
|
unawaited(_store.write(key: _pendingEndpointKey, value: endpoint.url));
|
|
}
|
|
}
|
|
|
|
static void _onUnregistered(String instance) {
|
|
_service?.handleUnregistered();
|
|
}
|
|
|
|
/// Vyzvedne (a smaže) endpoint uložený během background startu.
|
|
static Future<String?> takePendingEndpoint() async {
|
|
final value = await _store.read(key: _pendingEndpointKey);
|
|
if (value != null) {
|
|
await _store.delete(key: _pendingEndpointKey);
|
|
}
|
|
return value;
|
|
}
|
|
}
|