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'),