Merge branch 'td/voip' into 'main'

feat: background and terminated calls [android]

Closes #874

See merge request famedly/fluffychat!911
This commit is contained in:
Krille Fear 2022-09-10 11:45:17 +00:00
commit 9e64cc64dc
18 changed files with 421 additions and 195 deletions

View file

@ -25,7 +25,6 @@ import 'package:fluffychat/utils/matrix_sdk_extensions.dart/event_extension.dart
import 'package:fluffychat/utils/matrix_sdk_extensions.dart/ios_badge_client_extension.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions.dart/matrix_locals.dart';
import 'package:fluffychat/utils/platform_infos.dart';
import 'package:fluffychat/utils/voip/callkeep_manager.dart';
import 'package:fluffychat/widgets/matrix.dart';
import '../../utils/account_bundles.dart';
import '../../utils/localized_exception_extension.dart';
@ -180,14 +179,6 @@ class ChatController extends State<Chat> {
void initState() {
scrollController.addListener(_updateScrollController);
inputFocus.addListener(_inputFocusListener);
final voipPlugin = Matrix.of(context).voipPlugin;
if (voipPlugin != null) {
WidgetsBinding.instance.addPostFrameCallback((_) {
CallKeepManager().setVoipPlugin(voipPlugin);
CallKeepManager().initialize().catchError((_) => true);
});
}
super.initState();
}

View file

@ -22,6 +22,7 @@ import 'package:fluffychat/utils/platform_infos.dart';
import '../../../utils/account_bundles.dart';
import '../../utils/matrix_sdk_extensions.dart/matrix_file_extension.dart';
import '../../utils/url_launcher.dart';
import '../../utils/voip/callkeep_manager.dart';
import '../../widgets/fluffy_chat_app.dart';
import '../../widgets/matrix.dart';
import '../bootstrap/bootstrap_dialog.dart';
@ -53,6 +54,7 @@ enum ActiveFilter {
}
class ChatList extends StatefulWidget {
static BuildContext? contextForVoip;
const ChatList({Key? key}) : super(key: key);
@override
@ -361,7 +363,7 @@ class ChatListController extends State<ChatList>
scrollController.addListener(_onScroll);
_waitForFirstSync();
_hackyWebRTCFixForWeb();
CallKeepManager().initialize();
WidgetsBinding.instance.addPostFrameCallback((_) async {
searchServer = await Store().getItem(_serverStoreNamespace);
});
@ -670,7 +672,7 @@ class ChatListController extends State<ChatList>
}
void _hackyWebRTCFixForWeb() {
Matrix.of(context).voipPlugin?.context = context;
ChatList.contextForVoip = context;
}
Future<void> _checkTorBrowser() async {

View file

@ -22,9 +22,12 @@ import 'dart:math';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_foreground_task/flutter_foreground_task.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:flutter_webrtc/flutter_webrtc.dart';
import 'package:just_audio/just_audio.dart';
import 'package:matrix/matrix.dart';
import 'package:vibration/vibration.dart';
import 'package:wakelock/wakelock.dart';
import 'package:fluffychat/utils/platform_infos.dart';
@ -124,47 +127,47 @@ class Calling extends StatefulWidget {
}
class MyCallingPage extends State<Calling> {
Room? get room => call?.room;
Room? get room => call.room;
String get displayName => call?.displayName ?? '';
String get displayName => call.displayName ?? '';
String get callId => widget.callId;
CallSession? get call => widget.call;
CallSession get call => widget.call;
MediaStream? get localStream {
if (call != null && call!.localUserMediaStream != null) {
return call!.localUserMediaStream!.stream!;
if (call.localUserMediaStream != null) {
return call.localUserMediaStream!.stream!;
}
return null;
}
MediaStream? get remoteStream {
if (call != null && call!.getRemoteStreams.isNotEmpty) {
return call!.getRemoteStreams[0].stream!;
if (call.getRemoteStreams.isNotEmpty) {
return call.getRemoteStreams[0].stream!;
}
return null;
}
bool get speakerOn => call?.speakerOn ?? false;
bool get speakerOn => call.speakerOn;
bool get isMicrophoneMuted => call?.isMicrophoneMuted ?? false;
bool get isMicrophoneMuted => call.isMicrophoneMuted;
bool get isLocalVideoMuted => call?.isLocalVideoMuted ?? false;
bool get isLocalVideoMuted => call.isLocalVideoMuted;
bool get isScreensharingEnabled => call?.screensharingEnabled ?? false;
bool get isScreensharingEnabled => call.screensharingEnabled;
bool get isRemoteOnHold => call?.remoteOnHold ?? false;
bool get isRemoteOnHold => call.remoteOnHold;
bool get voiceonly => call == null || call?.type == CallType.kVoice;
bool get voiceonly => call.type == CallType.kVoice;
bool get connecting => call?.state == CallState.kConnecting;
bool get connecting => call.state == CallState.kConnecting;
bool get connected => call?.state == CallState.kConnected;
bool get connected => call.state == CallState.kConnected;
bool get mirrored => call?.facingMode == 'user';
bool get mirrored => call.facingMode == 'user';
List<WrappedMediaStream> get streams => call?.streams ?? [];
List<WrappedMediaStream> get streams => call.streams;
double? _localVideoHeight;
double? _localVideoWidth;
EdgeInsetsGeometry? _localVideoMargin;
@ -190,8 +193,6 @@ class MyCallingPage extends State<Calling> {
void initialize() async {
final call = this.call;
if (call == null) return;
call.onCallStateChanged.stream.listen(_handleCallState);
call.onCallEventChanged.stream.listen((event) {
if (event == CallEvent.kFeedsChanged) {
@ -220,7 +221,7 @@ class MyCallingPage extends State<Calling> {
const Duration(seconds: 2),
() => widget.onClear?.call(),
);
if (call?.type == CallType.kVideo) {
if (call.type == CallType.kVideo) {
try {
unawaited(Wakelock.disable());
} catch (_) {}
@ -230,7 +231,7 @@ class MyCallingPage extends State<Calling> {
@override
void dispose() {
super.dispose();
call?.cleanUp.call();
call.cleanUp.call();
}
void _resizeLocalVideo(Orientation orientation) {
@ -249,6 +250,14 @@ class MyCallingPage extends State<Calling> {
void _handleCallState(CallState state) {
Logs().v('CallingPage::handleCallState: ${state.toString()}');
if ({CallState.kConnected, CallState.kEnded}.contains(state)) {
try {
Vibration.vibrate(duration: 200);
} catch (e) {
Logs().e('[Dialer] could not vibrate for call updates');
}
}
if (mounted) {
setState(() {
_state = state;
@ -259,52 +268,69 @@ class MyCallingPage extends State<Calling> {
void _answerCall() {
setState(() {
call?.answer();
call.answer();
});
}
void _hangUp() {
setState(() {
if (call != null && (call?.isRinging ?? false)) {
call?.reject();
if (call.isRinging) {
call.reject();
} else {
call?.hangup();
call.hangup();
}
});
}
void _muteMic() {
setState(() {
call?.setMicrophoneMuted(!call!.isMicrophoneMuted);
call.setMicrophoneMuted(!call.isMicrophoneMuted);
});
}
void _screenSharing() {
void _screenSharing() async {
if (PlatformInfos.isAndroid) {
if (!call.screensharingEnabled) {
await FlutterForegroundTask.init(
androidNotificationOptions: AndroidNotificationOptions(
channelId: 'notification_channel_id',
channelName: 'Foreground Notification',
channelDescription: L10n.of(context)!.foregroundServiceRunning,
),
);
FlutterForegroundTask.startService(
notificationTitle: L10n.of(context)!.screenSharingTitle,
notificationText: L10n.of(context)!.screenSharingDetail);
} else {
FlutterForegroundTask.stopService();
}
}
setState(() {
call?.setScreensharingEnabled(!call!.screensharingEnabled);
call.setScreensharingEnabled(!call.screensharingEnabled);
});
}
void _remoteOnHold() {
setState(() {
call?.setRemoteOnHold(!call!.remoteOnHold);
call.setRemoteOnHold(!call.remoteOnHold);
});
}
void _muteCamera() {
setState(() {
call?.setLocalVideoMuted(!call!.isLocalVideoMuted);
call.setLocalVideoMuted(!call.isLocalVideoMuted);
});
}
void _switchCamera() async {
if (call!.localUserMediaStream != null) {
if (call.localUserMediaStream != null) {
await Helper.switchCamera(
call!.localUserMediaStream!.stream!.getVideoTracks()[0]);
call.localUserMediaStream!.stream!.getVideoTracks()[0]);
if (PlatformInfos.isMobile) {
call!.facingMode == 'user'
? call!.facingMode = 'environment'
: call!.facingMode = 'user';
call.facingMode == 'user'
? call.facingMode = 'environment'
: call.facingMode = 'user';
}
}
setState(() {});
@ -319,7 +345,7 @@ class MyCallingPage extends State<Calling> {
*/
List<Widget> _buildActionButtons(bool isFloating) {
if (isFloating || call == null) {
if (isFloating) {
return [];
}
@ -391,7 +417,7 @@ class MyCallingPage extends State<Calling> {
case CallState.kInviteSent:
case CallState.kCreateAnswer:
case CallState.kConnecting:
return call!.isOutgoing
return call.isOutgoing
? <Widget>[hangupButton]
: <Widget>[answerButton, hangupButton];
case CallState.kConnected:
@ -429,7 +455,7 @@ class MyCallingPage extends State<Calling> {
final stackWidgets = <Widget>[];
final call = this.call;
if (call == null || call.callHasEnded) {
if (call.callHasEnded) {
return stackWidgets;
}

View file

@ -1,3 +1,4 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
@ -6,6 +7,7 @@ import 'package:vrouter/vrouter.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/config/setting_keys.dart';
import 'package:fluffychat/utils/platform_infos.dart';
import 'package:fluffychat/utils/voip/callkeep_manager.dart';
import 'package:fluffychat/widgets/layouts/max_width_body.dart';
import 'package:fluffychat/widgets/matrix.dart';
import 'package:fluffychat/widgets/settings_switch_list_tile.dart';
@ -68,6 +70,17 @@ class SettingsChatView extends StatelessWidget {
defaultValue: AppConfig.experimentalVoip,
),
const Divider(height: 1),
if (Matrix.of(context).webrtcIsSupported && !kIsWeb)
ListTile(
title: Text(L10n.of(context)!.callingPermissions),
onTap: () =>
CallKeepManager().checkoutPhoneAccountSetting(context),
trailing: const Padding(
padding: EdgeInsets.all(16.0),
child: Icon(Icons.call),
),
),
const Divider(height: 1),
ListTile(
title: Text(L10n.of(context)!.emoteSettings),
onTap: () => VRouter.of(context).to('emotes'),

View file

@ -13,6 +13,7 @@ import 'package:fluffychat/config/setting_keys.dart';
import 'package:fluffychat/utils/client_manager.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions.dart/matrix_locals.dart';
import 'package:fluffychat/utils/platform_infos.dart';
import 'package:fluffychat/utils/voip/callkeep_manager.dart';
Future<void> pushHelper(
PushNotification notification, {
@ -110,11 +111,29 @@ Future<void> _tryPushHelper(
}
return;
}
Logs().v('Push helper got notification event.');
Logs().v('Push helper got notification event of type ${event.type}.');
if (!event.isEventTypeKnown) {
Logs()
.v('Push message event is from an unknown event type. Do not display.');
if (event.type.startsWith('m.call')) {
// make sure bg sync is on (needed to update hold, unhold events)
// prevent over write from app life cycle change
client.backgroundSync = true;
}
if (event.type == EventTypes.CallInvite) {
CallKeepManager().initialize();
} else if (event.type == EventTypes.CallHangup) {
client.backgroundSync = false;
}
if (event.type.startsWith('m.call') && event.type != EventTypes.CallInvite) {
Logs().v('Push message is a m.call but not invite. Do not display.');
return;
}
if ((event.type.startsWith('m.call') &&
event.type != EventTypes.CallInvite) ||
event.type == 'org.matrix.call.sdp_stream_metadata_changed') {
Logs().v('Push message was for a call, but not call invite.');
return;
}

View file

@ -1,42 +1,44 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:callkeep/callkeep.dart';
import 'package:flutter_foreground_task/flutter_foreground_task.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:matrix/matrix.dart';
import 'package:uuid/uuid.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:fluffychat/utils/voip_plugin.dart';
class CallKeeper {
CallKeeper(this.callKeepManager, this.uuid, this.number, this.call) {
call?.onCallStateChanged.stream.listen(_handleCallState);
CallKeeper(this.callKeepManager, this.call) {
call.onCallStateChanged.stream.listen(_handleCallState);
}
CallKeepManager callKeepManager;
String number;
String uuid;
bool held = false;
bool muted = false;
bool? held = false;
bool? muted = false;
bool connected = false;
CallSession? call;
CallSession call;
// update native caller to show what remote user has done.
void _handleCallState(CallState state) {
Logs().v('CallKeepManager::handleCallState: ${state.toString()}');
Logs().i('CallKeepManager::handleCallState: ${state.toString()}');
switch (state) {
case CallState.kConnecting:
Logs().v('callkeep connecting');
break;
case CallState.kConnected:
Logs().v('callkeep connected');
if (!connected) {
callKeepManager.answer(uuid);
callKeepManager.answer(call.callId);
} else {
callKeepManager.setMutedCall(uuid, false);
callKeepManager.setOnHold(uuid, false);
callKeepManager.setMutedCall(call.callId, false);
callKeepManager.setOnHold(call.callId, false);
}
break;
case CallState.kEnded:
callKeepManager.hangup(uuid);
callKeepManager.hangup(call.callId);
break;
/* TODO:
case CallState.kMuted:
@ -68,6 +70,8 @@ class CallKeeper {
}
}
Map<String?, CallKeeper> calls = <String?, CallKeeper>{};
class CallKeepManager {
factory CallKeepManager() {
return _instance;
@ -81,32 +85,29 @@ class CallKeepManager {
late FlutterCallkeep _callKeep;
VoipPlugin? _voipPlugin;
Map<String, CallKeeper> calls = <String, CallKeeper>{};
String newUUID() => const Uuid().v4();
String get appName => 'Famedly';
String get appName => 'FluffyChat';
Future<bool> get hasPhoneAccountEnabled async =>
await _callKeep.hasPhoneAccount();
Map<String, dynamic> get alertOptions => <String, dynamic>{
'alertTitle': 'Permissions required',
'alertDescription': '$appName needs to access your phone accounts!',
'alertDescription':
'Allow $appName to register as a calling account? This will allow calls to be handled by the native android dialer.',
'cancelButton': 'Cancel',
'okButton': 'ok',
// Required to get audio in background when using Android 11
'foregroundService': {
'channelId': 'com.famedly.talk',
'channelId': 'com.fluffy.fluffychat',
'channelName': 'Foreground service for my app',
'notificationTitle': '$appName is running on background',
'notificationIcon': 'mipmap/ic_notification_launcher',
},
'additionalPermissions': [''],
};
void setVoipPlugin(VoipPlugin plugin) {
if (kIsWeb) {
throw 'Not support callkeep for flutter web';
}
_voipPlugin = plugin;
_voipPlugin!.onIncomingCall = (CallSession call) async {
bool setupDone = false;
Future<void> showCallkitIncoming(CallSession call) async {
if (!setupDone) {
await _callKeep.setup(
null,
<String, dynamic>{
@ -116,47 +117,38 @@ class CallKeepManager {
'android': alertOptions,
},
backgroundMode: true);
await displayIncomingCall(call);
call.onCallStateChanged.stream.listen((state) {
if (state == CallState.kEnded) {
_callKeep.endAllCalls();
}
});
call.onCallEventChanged.stream.listen((event) {
}
setupDone = true;
await displayIncomingCall(call);
call.onCallStateChanged.stream.listen((state) {
if (state == CallState.kEnded) {
_callKeep.endAllCalls();
}
});
call.onCallEventChanged.stream.listen(
(event) {
if (event == CallEvent.kLocalHoldUnhold) {
Logs().i(
'Call hold event: local ${call.localHold}, remote ${call.remoteOnHold}');
}
});
};
},
);
}
void removeCall(String callUUID) {
void removeCall(String? callUUID) {
calls.remove(callUUID);
}
void addCall(String callUUID, CallKeeper callKeeper) {
void addCall(String? callUUID, CallKeeper callKeeper) {
if (calls.containsKey(callUUID)) return;
calls[callUUID] = callKeeper;
}
String findCallUUID(String number) {
var uuid = '';
calls.forEach((String key, CallKeeper item) {
if (item.number == number) {
uuid = key;
return;
}
});
return uuid;
}
void setCallHeld(String callUUID, bool held) {
void setCallHeld(String? callUUID, bool? held) {
calls[callUUID]!.held = held;
}
void setCallMuted(String callUUID, bool muted) {
void setCallMuted(String? callUUID, bool? muted) {
calls[callUUID]!.muted = muted;
}
@ -164,7 +156,7 @@ class CallKeepManager {
final callUUID = event.callUUID;
final number = event.handle;
Logs().v('[displayIncomingCall] $callUUID number: $number');
addCall(callUUID!, CallKeeper(this, callUUID, number!, null));
// addCall(callUUID, CallKeeper(this null));
}
void onPushKitToken(CallKeepPushKitToken event) {
@ -182,6 +174,7 @@ class CallKeepManager {
_callKeep.on(CallKeepPerformEndCallAction(), endCall);
_callKeep.on(CallKeepPushKitToken(), onPushKitToken);
_callKeep.on(CallKeepDidDisplayIncomingCall(), didDisplayIncomingCall);
Logs().i('[VOIP] Initialized');
}
Future<void> hangup(String callUUID) async {
@ -193,10 +186,10 @@ class CallKeepManager {
await _callKeep.rejectCall(callUUID);
}
Future<void> answer(String callUUID) async {
final keeper = calls[callUUID];
if (!keeper!.connected) {
await _callKeep.answerIncomingCall(callUUID);
Future<void> answer(String? callUUID) async {
final keeper = calls[callUUID]!;
if (!keeper.connected) {
await _callKeep.answerIncomingCall(callUUID!);
keeper.connected = true;
}
}
@ -212,27 +205,66 @@ class CallKeepManager {
}
Future<void> updateDisplay(String callUUID) async {
final number = calls[callUUID]!.number;
// Workaround because Android doesn't display well displayName, se we have to switch ...
if (isIOS) {
await _callKeep.updateDisplay(callUUID,
displayName: 'New Name', handle: number);
displayName: 'New Name', handle: callUUID);
} else {
await _callKeep.updateDisplay(callUUID,
displayName: number, handle: 'New Name');
displayName: callUUID, handle: 'New Name');
}
}
Future<CallKeeper> displayIncomingCall(CallSession call) async {
final callUUID = newUUID();
final callKeeper = CallKeeper(this, callUUID, call.displayName!, call);
addCall(callUUID, callKeeper);
await _callKeep.displayIncomingCall(callUUID, call.displayName!,
handleType: 'number', hasVideo: call.type == CallType.kVideo);
final callKeeper = CallKeeper(this, call);
addCall(call.callId, callKeeper);
await _callKeep.displayIncomingCall(
call.callId,
'${call.displayName!} (FluffyChat)',
localizedCallerName: '${call.displayName!} (FluffyChat)',
handleType: 'number',
hasVideo: call.type == CallType.kVideo,
);
return callKeeper;
}
Future<void> checkoutPhoneAccountSetting(BuildContext context) async {
showDialog(
context: context,
barrierDismissible: true,
useRootNavigator: false,
builder: (_) => AlertDialog(
title: Text(L10n.of(context)!.callingPermissions),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
onTap: () => openCallingAccountsPage(context),
title: Text(L10n.of(context)!.callingAccount),
subtitle: Text(L10n.of(context)!.callingAccountDetails),
trailing: const Icon(Icons.phone),
),
const Divider(),
ListTile(
onTap: () =>
FlutterForegroundTask.openSystemAlertWindowSettings(true),
title: Text(L10n.of(context)!.appearOnTop),
subtitle: Text(L10n.of(context)!.appearOnTopDetails),
trailing: const Icon(Icons.file_upload_rounded),
),
const Divider(),
ListTile(
onTap: () => openAppSettings(),
title: Text(L10n.of(context)!.otherCallingPermissions),
trailing: const Icon(Icons.mic),
),
],
),
),
);
}
void openCallingAccountsPage(BuildContext context) async {
await _callKeep.setup(context, <String, dynamic>{
'ios': <String, dynamic>{
'appName': appName,
@ -240,8 +272,11 @@ class CallKeepManager {
'android': alertOptions,
});
final hasPhoneAccount = await _callKeep.hasPhoneAccount();
Logs().e(hasPhoneAccount.toString());
if (!hasPhoneAccount) {
await _callKeep.hasDefaultPhoneAccount(context, alertOptions);
} else {
await _callKeep.openPhoneAccounts();
}
}
@ -250,8 +285,9 @@ class CallKeepManager {
final callUUID = event.callUUID;
final keeper = calls[event.callUUID]!;
if (!keeper.connected) {
Logs().e('answered');
// Answer Call
keeper.call!.answer();
keeper.call.answer();
keeper.connected = true;
}
Timer(const Duration(seconds: 1), () {
@ -261,13 +297,13 @@ class CallKeepManager {
Future<void> endCall(CallKeepPerformEndCallAction event) async {
final keeper = calls[event.callUUID];
keeper?.call?.hangup();
removeCall(event.callUUID!);
keeper?.call.hangup();
removeCall(event.callUUID);
}
Future<void> didPerformDTMFAction(CallKeepDidPerformDTMFAction event) async {
final keeper = calls[event.callUUID]!;
keeper.call?.sendDTMF(event.digits!);
keeper.call.sendDTMF(event.digits!);
}
Future<void> didReceiveStartCallAction(
@ -276,11 +312,11 @@ class CallKeepManager {
// @TODO: sometime we receive `didReceiveStartCallAction` with handle` undefined`
return;
}
final callUUID = event.callUUID ?? newUUID();
final callUUID = event.callUUID!;
if (event.callUUID == null) {
final call =
await _voipPlugin!.voip.inviteToCall(event.handle!, CallType.kVideo);
addCall(callUUID, CallKeeper(this, callUUID, call.displayName!, call));
addCall(callUUID, CallKeeper(this, call));
}
await _callKeep.startCall(callUUID, event.handle!, event.handle!);
Timer(const Duration(seconds: 1), () {
@ -290,23 +326,23 @@ class CallKeepManager {
Future<void> didPerformSetMutedCallAction(
CallKeepDidPerformSetMutedCallAction event) async {
final keeper = calls[event.callUUID]!;
if (event.muted ?? false) {
keeper.call?.setMicrophoneMuted(true);
final keeper = calls[event.callUUID];
if (event.muted!) {
keeper!.call.setMicrophoneMuted(true);
} else {
keeper.call?.setMicrophoneMuted(false);
keeper!.call.setMicrophoneMuted(false);
}
setCallMuted(event.callUUID!, event.muted!);
setCallMuted(event.callUUID, event.muted);
}
Future<void> didToggleHoldCallAction(
CallKeepDidToggleHoldAction event) async {
final keeper = calls[event.callUUID]!;
if (event.hold ?? false) {
keeper.call?.setRemoteOnHold(true);
final keeper = calls[event.callUUID];
if (event.hold!) {
keeper!.call.setRemoteOnHold(true);
} else {
keeper.call?.setRemoteOnHold(false);
keeper!.call.setRemoteOnHold(false);
}
setCallHeld(event.callUUID!, event.hold!);
setCallHeld(event.callUUID, event.hold);
}
}

View file

@ -4,24 +4,27 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:flutter_foreground_task/flutter_foreground_task.dart';
import 'package:flutter_webrtc/flutter_webrtc.dart' as webrtc_impl;
import 'package:matrix/matrix.dart';
import 'package:webrtc_interface/webrtc_interface.dart' hide Navigator;
import 'package:fluffychat/pages/chat_list/chat_list.dart';
import 'package:fluffychat/pages/dialer/dialer.dart';
import 'package:fluffychat/utils/platform_infos.dart';
import 'package:fluffychat/widgets/fluffy_chat_app.dart';
import '../../utils/famedlysdk_store.dart';
import '../../utils/voip/callkeep_manager.dart';
import '../../utils/voip/user_media_manager.dart';
class VoipPlugin extends WidgetsBindingObserver implements WebRTCDelegate {
VoipPlugin({required this.client, required this.context}) {
class VoipPlugin with WidgetsBindingObserver implements WebRTCDelegate {
final Client client;
VoipPlugin(this.client) {
voip = VoIP(client, this);
try {
Connectivity()
.onConnectivityChanged
.listen(_handleNetworkChanged)
.onError((e) => _currentConnectivity = ConnectivityResult.none);
} catch (e, s) {
Logs().w('Could not subscribe network updates', e, s);
}
Connectivity()
.onConnectivityChanged
.listen(_handleNetworkChanged)
.onError((e) => _currentConnectivity = ConnectivityResult.none);
Connectivity()
.checkConnectivity()
.then((result) => _currentConnectivity = result)
@ -29,24 +32,15 @@ class VoipPlugin extends WidgetsBindingObserver implements WebRTCDelegate {
if (!kIsWeb) {
final wb = WidgetsBinding.instance;
wb.addObserver(this);
didChangeAppLifecycleState(wb.lifecycleState!);
didChangeAppLifecycleState(wb.lifecycleState);
}
}
final Client client;
bool background = false;
bool speakerOn = false;
late VoIP voip;
ConnectivityResult? _currentConnectivity;
ValueChanged<CallSession>? onIncomingCall;
OverlayEntry? overlayEntry;
// hacky workaround: in order to have [Overlay.of] working on web, the context
// mus explicitly be re-assigned
//
// hours wasted: 5
BuildContext context;
void _handleNetworkChanged(ConnectivityResult result) async {
/// Got a new connectivity status!
if (_currentConnectivity != result) {
@ -58,17 +52,19 @@ class VoipPlugin extends WidgetsBindingObserver implements WebRTCDelegate {
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
void didChangeAppLifecycleState(AppLifecycleState? state) {
Logs().v('AppLifecycleState = $state');
background = !(state != AppLifecycleState.detached &&
state != AppLifecycleState.paused);
background = (state == AppLifecycleState.detached ||
state == AppLifecycleState.paused);
}
void addCallingOverlay(
BuildContext context, String callId, CallSession call) {
void addCallingOverlay(String callId, CallSession call) {
final context = kIsWeb
? ChatList.contextForVoip!
: FluffyChatApp.routerKey.currentContext!; // web is weird
if (overlayEntry != null) {
Logs().w('[VOIP] addCallingOverlay: The call session already exists?');
overlayEntry?.remove();
Logs().e('[VOIP] addCallingOverlay: The call session already exists?');
overlayEntry!.remove();
}
// Overlay.of(context) is broken on web
// falling back on a dialog
@ -103,7 +99,8 @@ class VoipPlugin extends WidgetsBindingObserver implements WebRTCDelegate {
MediaDevices get mediaDevices => webrtc_impl.navigator.mediaDevices;
@override
bool get isBackgroud => background;
// remove this from sdk once callkeep is stable
bool get isBackgroud => false;
@override
bool get isWeb => kIsWeb;
@ -119,9 +116,12 @@ class VoipPlugin extends WidgetsBindingObserver implements WebRTCDelegate {
return webrtc_impl.RTCVideoRenderer();
}
Future<bool> get hasCallingAccount async =>
kIsWeb ? false : await CallKeepManager().hasPhoneAccountEnabled;
@override
void playRingtone() async {
if (!background) {
if (!background && !await hasCallingAccount) {
try {
await UserMediaManager().startRingingTone();
} catch (_) {}
@ -130,7 +130,7 @@ class VoipPlugin extends WidgetsBindingObserver implements WebRTCDelegate {
@override
void stopRingtone() async {
if (!background) {
if (!background && !await hasCallingAccount) {
try {
await UserMediaManager().stopRingingTone();
} catch (_) {}
@ -139,19 +139,58 @@ class VoipPlugin extends WidgetsBindingObserver implements WebRTCDelegate {
@override
void handleNewCall(CallSession call) async {
/// Popup CallingPage for incoming call.
if (!background) {
addCallingOverlay(context, call.callId, call);
if (PlatformInfos.isAndroid) {
// probably works on ios too
final hasCallingAccount = await CallKeepManager().hasPhoneAccountEnabled;
if (call.direction == CallDirection.kIncoming &&
hasCallingAccount &&
call.type == CallType.kVoice) {
///Popup native telecom manager call UI for incoming call.
final callKeeper = CallKeeper(CallKeepManager(), call);
CallKeepManager().addCall(call.callId, callKeeper);
await CallKeepManager().showCallkitIncoming(call);
return;
} else {
try {
final wasForeground = await FlutterForegroundTask.isAppOnForeground;
await Store().setItem(
'wasForeground', wasForeground == true ? 'true' : 'false');
FlutterForegroundTask.setOnLockScreenVisibility(true);
FlutterForegroundTask.wakeUpScreen();
FlutterForegroundTask.launchApp();
} catch (e) {
Logs().e('VOIP foreground failed $e');
}
// use fallback flutter call pages for outgoing and video calls.
addCallingOverlay(call.callId, call);
try {
if (!hasCallingAccount) {
ScaffoldMessenger.of(FluffyChatApp.routerKey.currentContext!)
.showSnackBar(const SnackBar(
content: Text(
'No calling accounts found (used for native calls UI)',
)));
}
} catch (e) {
Logs().e('failed to show snackbar');
}
}
} else {
onIncomingCall?.call(call);
addCallingOverlay(call.callId, call);
}
}
@override
void handleCallEnded(CallSession session) async {
if (overlayEntry != null) {
overlayEntry?.remove();
overlayEntry!.remove();
overlayEntry = null;
if (PlatformInfos.isAndroid) {
FlutterForegroundTask.setOnLockScreenVisibility(false);
FlutterForegroundTask.stopService();
final wasForeground = await Store().getItem('wasForeground');
wasForeground == 'false' ? FlutterForegroundTask.minimizeApp() : null;
}
}
}

View file

@ -17,7 +17,7 @@ class FluffyChatApp extends StatefulWidget {
final Widget? testWidget;
final List<Client> clients;
final Map<String, String>? queryParameters;
static final GlobalKey<VRouterState> routerKey = GlobalKey<VRouterState>();
const FluffyChatApp({
Key? key,
this.testWidget,
@ -35,7 +35,6 @@ class FluffyChatApp extends StatefulWidget {
}
class FluffyChatAppState extends State<FluffyChatApp> {
GlobalKey<VRouterState>? _router;
bool? columnMode;
String? _initialUrl;
@ -67,14 +66,13 @@ class FluffyChatAppState extends State<FluffyChatApp> {
Logs().v('Set Column Mode = $isColumnMode');
WidgetsBinding.instance.addPostFrameCallback((_) {
setState(() {
_initialUrl = _router?.currentState?.url;
_initialUrl = FluffyChatApp.routerKey.currentState?.url;
columnMode = isColumnMode;
_router = GlobalKey<VRouterState>();
});
});
}
return VRouter(
key: _router,
key: FluffyChatApp.routerKey,
title: AppConfig.applicationName,
theme: theme,
scrollBehavior: CustomScrollBehavior(),
@ -86,7 +84,7 @@ class FluffyChatAppState extends State<FluffyChatApp> {
routes: AppRoutes(columnMode ?? false).routes,
builder: (context, child) => Matrix(
context: context,
router: _router,
router: FluffyChatApp.routerKey,
clients: widget.clients,
child: child,
),

View file

@ -13,6 +13,7 @@ import 'package:vrouter/vrouter.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions.dart/matrix_locals.dart';
import 'package:fluffychat/utils/platform_infos.dart';
import 'package:fluffychat/widgets/matrix.dart';
extension LocalNotificationsExtension on MatrixState {
@ -20,7 +21,9 @@ extension LocalNotificationsExtension on MatrixState {
final roomId = eventUpdate.roomID;
if (activeRoomId == roomId) {
if (kIsWeb && webHasFocus) return;
if (Platform.isLinux && DesktopLifecycle.instance.isActive.value) return;
if (PlatformInfos.isLinux && DesktopLifecycle.instance.isActive.value) {
return;
}
}
final room = client.getRoomById(roomId);
if (room == null) {

View file

@ -427,8 +427,7 @@ class MatrixState extends State<Matrix> with WidgetsBindingObserver {
voipPlugin = null;
return;
}
voipPlugin =
webrtcIsSupported ? VoipPlugin(client: client, context: context) : null;
voipPlugin = webrtcIsSupported ? VoipPlugin(client) : null;
}
bool _firstStartup = true;