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:
commit
9e64cc64dc
18 changed files with 421 additions and 195 deletions
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue