mirror of
https://gitlab.com/famedly/fluffychat.git
synced 2024-11-23 20:49:26 +01:00
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
b06684111e
@ -32,7 +32,7 @@ if (keystorePropertiesFile.exists()) {
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion 32
|
||||
compileSdkVersion 33
|
||||
|
||||
sourceSets {
|
||||
main.java.srcDirs += 'src/main/kotlin'
|
||||
|
@ -14,8 +14,6 @@
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH" />
|
||||
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
@ -23,10 +21,11 @@
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
|
||||
<uses-permission android:name="android.permission.CALL_PHONE" />
|
||||
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" />
|
||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
|
||||
|
||||
<uses-sdk
|
||||
tools:overrideLibrary="io.wazo.callkeep, net.touchcapture.qr.flutterqr, com.cloudwebrtc.webrtc, org.webrtc, com.it_nomads.fluttersecurestorage, com.pichillilorenzo.flutter_inappwebview, com.example.video_compress, com.otaliastudios.transcoder, com.otaliastudios.opengl, com.kineapps.flutter_file_dialog, com.llfbandit.record"/>
|
||||
|
||||
tools:overrideLibrary="io.wazo.callkeep, net.touchcapture.qr.flutterqr, com.cloudwebrtc.webrtc, org.webrtc, com.it_nomads.fluttersecurestorage, com.pichillilorenzo.flutter_inappwebview, com.example.video_compress, com.otaliastudios.transcoder, com.otaliastudios.opengl, com.kineapps.flutter_file_dialog, com.llfbandit.record, com.pravera.flutter_foreground_task"/>
|
||||
<application
|
||||
android:label="FluffyChat"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
@ -40,6 +39,8 @@
|
||||
android:theme="@style/LaunchTheme"
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||
android:hardwareAccelerated="true"
|
||||
android:showOnLockScreen="false"
|
||||
android:turnScreenOn="true"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
@ -109,6 +110,19 @@
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<service android:name="com.pravera.flutter_foreground_task.service.ForegroundService"
|
||||
android:foregroundServiceType="camera|microphone|mediaProjection">
|
||||
</service>
|
||||
|
||||
<service android:name="io.wazo.callkeep.VoiceConnectionService"
|
||||
android:label="Wazo"
|
||||
android:foregroundServiceType="camera|microphone|mediaProjection"
|
||||
android:permission="android.permission.BIND_TELECOM_CONNECTION_SERVICE">
|
||||
<intent-filter>
|
||||
<action android:name="android.telecom.ConnectionService" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<receiver android:name="org.unifiedpush.flutter.connector.UnifiedPushReceiver"
|
||||
tools:replace="android:enabled"
|
||||
android:enabled="false">
|
||||
|
@ -2891,6 +2891,15 @@
|
||||
},
|
||||
"user": "User",
|
||||
"custom": "Custom",
|
||||
"foregroundServiceRunning": "This notification appears when the foreground service is running.",
|
||||
"screenSharingTitle": "screen sharing",
|
||||
"screenSharingDetail": "You are sharing your screen in famedly",
|
||||
"callingPermissions": "Calling permissions",
|
||||
"callingAccount": "Calling account",
|
||||
"callingAccountDetails": "Allows FluffyChat to use the native android dialer app.",
|
||||
"appearOnTop": "Appear on top",
|
||||
"appearOnTopDetails": "Allows the app to appear on top (not needed if you already have Fluffychat setup as a calling account)",
|
||||
"otherCallingPermissions":"Microphone, camera and other FluffyChat permissions",
|
||||
"whyIsThisMessageEncrypted": "Why is this message unreadable?",
|
||||
"noKeyForThisMessage": "This can happen if the message was sent before you have signed in to your account at this device.\n\nIt is also possible that the sender has blocked your device or something went wrong with the internet connection.\n\nAre you able to read the message on another session? Then you can transfer the message from it! Go to Settings > Devices and make sure that your devices have verified each other. When you open the room the next time and both sessions are in the foreground, the keys will be transmitted automatically.\n\nDo you not want to loose the keys when logging out or switching devices? Make sure that you have enabled the chat backup in the settings.",
|
||||
"newGroup": "New group",
|
||||
|
15
ios/Podfile
15
ios/Podfile
@ -39,12 +39,17 @@ post_install do |installer|
|
||||
flutter_additional_ios_build_settings(target)
|
||||
target.build_configurations.each do |config|
|
||||
config.build_settings['ENABLE_BITCODE'] = 'NO'
|
||||
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '9.0'
|
||||
|
||||
# https://github.com/flutter-webrtc/flutter-webrtc/issues/713
|
||||
if target.name == "flutter_webrtc" || target.name == "WebRTC-SDK"
|
||||
config.build_settings['ONLY_ACTIVE_ARCH'] = 'YES'
|
||||
end
|
||||
# see https://github.com/flutter-webrtc/flutter-webrtc/issues/1054
|
||||
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '11.0'
|
||||
config.build_settings["EXCLUDED_ARCHS[sdk=iphonesimulator*]"] = 'arm64 i386'
|
||||
|
||||
config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [
|
||||
'$(inherited)',
|
||||
# dart: PermissionGroup.microphone
|
||||
'PERMISSION_MICROPHONE=1',
|
||||
]
|
||||
end
|
||||
end
|
||||
flutter_post_install(installer) if defined?(flutter_post_install)
|
||||
end
|
||||
|
@ -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;
|
||||
|
68
pubspec.lock
68
pubspec.lock
@ -296,7 +296,7 @@ packages:
|
||||
name: dart_webrtc
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.6"
|
||||
version: "1.0.7"
|
||||
dbus:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -538,6 +538,15 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.3.0"
|
||||
flutter_foreground_task:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "."
|
||||
ref: "td/forceOpenOnTop"
|
||||
resolved-ref: b5f429acbcddb8267d77dd2d76032357d78a725d
|
||||
url: "https://github.com/Techno-Disaster/flutter_foreground_task.git"
|
||||
source: git
|
||||
version: "3.8.2"
|
||||
flutter_highlight:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -743,7 +752,7 @@ packages:
|
||||
name: flutter_webrtc
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.9.0"
|
||||
version: "0.9.5"
|
||||
fuchsia_remote_debug_protocol:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
@ -1246,6 +1255,41 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.11.1"
|
||||
permission_handler:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: permission_handler
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "10.0.0"
|
||||
permission_handler_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: permission_handler_android
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "10.0.0"
|
||||
permission_handler_apple:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: permission_handler_apple
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "9.0.4"
|
||||
permission_handler_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: permission_handler_platform_interface
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.7.0"
|
||||
permission_handler_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: permission_handler_windows
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.1.0"
|
||||
petitparser:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1497,21 +1541,21 @@ packages:
|
||||
name: shared_preferences
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.13"
|
||||
version: "2.0.15"
|
||||
shared_preferences_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_android
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.11"
|
||||
version: "2.0.12"
|
||||
shared_preferences_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_ios
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
version: "2.1.1"
|
||||
shared_preferences_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1825,6 +1869,20 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.4.0"
|
||||
vibration:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: vibration
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.7.4-nullsafety.0"
|
||||
vibration_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vibration_web
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.6.3-nullsafety.0"
|
||||
video_compress:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
12
pubspec.yaml
12
pubspec.yaml
@ -35,6 +35,7 @@ dependencies:
|
||||
flutter_app_lock: ^2.0.0
|
||||
flutter_blurhash: ^0.7.0
|
||||
flutter_cache_manager: ^3.3.0
|
||||
flutter_foreground_task: ^3.8.2
|
||||
flutter_local_notifications: ^9.7.0
|
||||
flutter_localizations:
|
||||
sdk: flutter
|
||||
@ -48,7 +49,7 @@ dependencies:
|
||||
flutter_svg: ^0.22.0
|
||||
flutter_typeahead: ^4.0.0
|
||||
flutter_web_auth: ^0.4.0
|
||||
flutter_webrtc: 0.9.0 # Higher fails to build on iOS https://github.com/flutter-webrtc/flutter-webrtc/issues/1054
|
||||
flutter_webrtc: ^0.9.5
|
||||
future_loading_dialog: ^0.2.3
|
||||
geolocator: ^7.6.2
|
||||
handy_window: ^0.1.6
|
||||
@ -69,6 +70,7 @@ dependencies:
|
||||
native_imaging: ^0.1.0
|
||||
package_info_plus: ^1.3.0
|
||||
path_provider: ^2.0.9
|
||||
permission_handler: ^10.0.0
|
||||
pin_code_text_field: ^1.8.0
|
||||
provider: ^6.0.2
|
||||
punycode: ^1.0.0
|
||||
@ -87,6 +89,7 @@ dependencies:
|
||||
universal_html: ^2.0.8
|
||||
url_launcher: ^6.0.20
|
||||
uuid: ^3.0.6
|
||||
vibration: ^1.7.4-nullsafety.0
|
||||
video_compress: ^3.1.1
|
||||
video_player: ^2.2.18
|
||||
vrouter: ^1.2.0+21
|
||||
@ -154,6 +157,13 @@ dependency_overrides:
|
||||
path: packages/connectivity_plus/connectivity_plus_web
|
||||
# Until all dependencies are compatible. Missing: file_picker_cross, flutter_matrix_html
|
||||
ffi: ^2.0.0
|
||||
|
||||
# upstream request at https://github.com/Dev-hwang/flutter_foreground_task/pull/102
|
||||
flutter_foreground_task:
|
||||
git:
|
||||
url: https://github.com/Techno-Disaster/flutter_foreground_task.git
|
||||
ref: td/forceOpenOnTop
|
||||
|
||||
# fake secure storage plugin for Windows
|
||||
# See: https://gitlab.com/gitlab-com/gl-infra/reliability/-/issues/15161
|
||||
flutter_secure_storage_windows:
|
||||
|
@ -10,6 +10,7 @@
|
||||
#include <desktop_drop/desktop_drop_plugin.h>
|
||||
#include <desktop_lifecycle/desktop_lifecycle_plugin.h>
|
||||
#include <flutter_webrtc/flutter_web_r_t_c_plugin.h>
|
||||
#include <permission_handler_windows/permission_handler_windows_plugin.h>
|
||||
#include <record_windows/record_windows_plugin_c_api.h>
|
||||
#include <url_launcher_windows/url_launcher_windows.h>
|
||||
|
||||
@ -22,6 +23,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||
registry->GetRegistrarForPlugin("DesktopLifecyclePlugin"));
|
||||
FlutterWebRTCPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("FlutterWebRTCPlugin"));
|
||||
PermissionHandlerWindowsPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin"));
|
||||
RecordWindowsPluginCApiRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("RecordWindowsPluginCApi"));
|
||||
UrlLauncherWindowsRegisterWithRegistrar(
|
||||
|
@ -7,6 +7,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
|
||||
desktop_drop
|
||||
desktop_lifecycle
|
||||
flutter_webrtc
|
||||
permission_handler_windows
|
||||
record_windows
|
||||
url_launcher_windows
|
||||
)
|
||||
|
Loading…
Reference in New Issue
Block a user