feat(app): 1:1 voice and video calls
End-to-end encrypted calls between two contacts over WebRTC. Call signalling (offer, answer, hang-up) travels as ordinary Signal-encrypted messages through the relay; media is DTLS-SRTP, peer to peer when possible and through the operator's TURN server otherwise. - Non-trickle ICE (candidates gathered for up to 3 s), 45 s ring timeout, busy handling, and a call log entry in the conversation. - The system call UI rings even when the app is closed: CallKit from a PushKit VoIP push on iOS, a full-screen incoming call from an FCM wake-up on Android. The wake-up carries no caller; the app learns who is calling from the queued offer once it is unlocked. - New permissions: camera for video, audio routing and Bluetooth headsets, network state; iOS background modes audio and voip. Push on Android: a fresh install now picks UnifiedPush when a distributor is installed and FCM otherwise, instead of leaving wake-ups off. Without a wake-up Android puts the app to sleep and messages wait until it is opened. Users can switch at any time in Settings.
This commit is contained in:
@@ -20,6 +20,7 @@ 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/calls/call_ringer.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';
|
||||
@@ -28,8 +29,14 @@ 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.
|
||||
/// Budíček hovoru (`k=call`) místo ní rovnou zazvoní; kdo
|
||||
/// volá, appka zjistí až po odemčení z nabídky ve frontě.
|
||||
@pragma('vm:entry-point')
|
||||
Future<void> sepotFcmBackgroundWake(RemoteMessage message) async {
|
||||
if (message.data['k'] == 'call') {
|
||||
await CallRinger.ringFromPush();
|
||||
return;
|
||||
}
|
||||
await Firebase.initializeApp();
|
||||
await PushNotifier.showWakeNotification();
|
||||
}
|
||||
@@ -52,8 +59,11 @@ class PushBootstrap {
|
||||
|
||||
/// 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<String> readMode() async => await readStoredMode() ?? 'off';
|
||||
|
||||
/// Uložený režim, nebo null, když ho uživatel ani appka ještě nevybrali
|
||||
/// — to odlišuje čerstvou instalaci od vědomého „vypnuto".
|
||||
static Future<String?> readStoredMode() => _store.read(key: _modeKey);
|
||||
|
||||
static Future<void> writeMode(String mode) =>
|
||||
_store.write(key: _modeKey, value: mode);
|
||||
|
||||
@@ -13,8 +13,11 @@
|
||||
/// killed-state startu `--unifiedpush-bg` bez UI); tahle třída je
|
||||
/// most bootstrap ↔ messaging + API pro nastavení.
|
||||
///
|
||||
/// FCM „fast mode" (opt-in) je druhá cesta (viz [setModeFcm]); default je
|
||||
/// degooglovaný UnifiedPush.
|
||||
/// Výchozí režim na Androidu vybírá [restoreMode] při prvním startu: s
|
||||
/// UnifiedPush distributorem UnifiedPush, jinak FCM. Bez budíčku Android
|
||||
/// appku na pozadí uspí a zprávy čekají, než ji člověk otevře — to je pro
|
||||
/// messenger horší než token u Googlu (obsah ani odesílatel budíčkem nejde).
|
||||
/// Uživatel může kdykoli přepnout v Nastavení.
|
||||
library;
|
||||
|
||||
import 'dart:async';
|
||||
@@ -24,6 +27,7 @@ import 'dart:typed_data';
|
||||
|
||||
import 'package:firebase_core/firebase_core.dart';
|
||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||
import 'package:sepot/core/calls/call_ringer.dart';
|
||||
import 'package:sepot/core/push/apns_ios.dart';
|
||||
import 'package:sepot/core/push/push_bootstrap.dart';
|
||||
import 'package:sepot/core/push/push_notifier.dart';
|
||||
@@ -80,11 +84,25 @@ class PushService {
|
||||
}
|
||||
}
|
||||
|
||||
void _registerApnsToken(String token) {
|
||||
_messaging.registerPush(_registration(
|
||||
String? _apnsToken;
|
||||
|
||||
/// APNs token pro zprávy + VoIP token pro hovory v jedné registraci
|
||||
/// (relay drží obojí u zařízení).
|
||||
Future<void> _registerApnsToken(String token) async {
|
||||
_apnsToken = token;
|
||||
final voip = await CallRinger.voipToken();
|
||||
final reg = _registration(
|
||||
pb.RegisterPush_Platform.PLATFORM_APNS,
|
||||
Uint8List.fromList(utf8.encode(token)),
|
||||
));
|
||||
);
|
||||
if (voip != null) reg.voipToken = utf8.encode(voip);
|
||||
_messaging.registerPush(reg);
|
||||
}
|
||||
|
||||
/// PushKit vydal (nový) VoIP token — přeregistrovat, pokud už APNs běží.
|
||||
Future<void> refreshVoipToken() async {
|
||||
final token = _apnsToken;
|
||||
if (token != null) await _registerApnsToken(token);
|
||||
}
|
||||
|
||||
/// iOS nativní push (APNs) — zapne registraci u Apple; token přijde
|
||||
@@ -166,8 +184,8 @@ class PushService {
|
||||
return ok;
|
||||
}
|
||||
|
||||
/// FCM fast mode — OPT-IN: registrační token zařízení jde Googlu
|
||||
/// (obsah budíčků nikdy, ťuk je konstantní `{"v":1}`).
|
||||
/// FCM: registrační token zařízení jde Googlu (obsah budíčků nikdy,
|
||||
/// ťuk je konstantní `{"v":1}`).
|
||||
Future<void> setModeFcm() async {
|
||||
await init();
|
||||
try {
|
||||
@@ -222,6 +240,10 @@ class PushService {
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (await PushBootstrap.readStoredMode() == null) {
|
||||
await _chooseDefaultMode();
|
||||
return;
|
||||
}
|
||||
switch (await currentMode()) {
|
||||
case 'up':
|
||||
await enable();
|
||||
@@ -235,4 +257,23 @@ class PushService {
|
||||
await init(); // aspoň attach (killed-state notifikace)
|
||||
}
|
||||
}
|
||||
|
||||
/// První start na Androidu (nebo instalace z doby, kdy byl push
|
||||
/// opt-in a nikdo ho nezapnul). Kdo má nainstalovaný UnifiedPush
|
||||
/// distributor, vybral si degooglovanou cestu sám — dostane ji.
|
||||
/// Ostatním zapneme FCM. Telefon bez Google služeb token nedostane;
|
||||
/// pak zapíšeme „off", ať se to nezkouší při každém startu — seznam
|
||||
/// chatů na chybějící budíčky upozorní sám.
|
||||
Future<void> _chooseDefaultMode() async {
|
||||
try {
|
||||
if ((await currentMode()) == 'up' ||
|
||||
(await distributors()).isNotEmpty) {
|
||||
if (await setModeUnifiedPush()) return;
|
||||
}
|
||||
await setModeFcm();
|
||||
} catch (_) {
|
||||
await init();
|
||||
await PushBootstrap.writeMode('off');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user