Skip to content

Commit 6941db5

Browse files
Brazold3xvn
andauthored
feat: added background voip handle called in terminated app state when voip push is received (#570)
* fixes push notification cancelation * starting flutter engine from voip push in iOS * minor fixes for pub scoreing * tweaks * cleanup * background voip handler cleanup, dogfooding direct call added * commented closing of ws connection when paused * fixes push notification cancelation * starting flutter engine from voip push in iOS * cleanup * background voip handler cleanup, dogfooding direct call added * commented closing of ws connection when paused * provider changes * dogfooding flutter engine * documentation * linter fixes * Update docusaurus/docs/Flutter/05-advanced/02-ringing.mdx Co-authored-by: Deven Joshi <[email protected]> * fixes --------- Co-authored-by: Deven Joshi <[email protected]>
1 parent 12bfcb8 commit 6941db5

File tree

18 files changed

+269
-25
lines changed

18 files changed

+269
-25
lines changed

docusaurus/docs/Flutter/05-advanced/02-ringing.mdx

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -378,7 +378,49 @@ say you want to end all calls on the CallKit side, you can end them this way:
378378
StreamVideo.instance.pushNotificationManager?.endAllCalls();
379379
```
380380

381-
#### Step 6 - Add native code to the iOS project
381+
#### Step 6 - Add callback to handle call in terminated state
382+
383+
When an iOS app is terminated, the Flutter engine is not running. The engine needs to be started up to handle Stream call events whenever a call is received by the app. The Stream SDK performs the job of running a Flutter engine instance whenever a call is received. However, on the app side, a callback handle needs to be registered that will connect to `StreamVideo`.
384+
385+
```dart
386+
@pragma('vm:entry-point')
387+
Future<void> _backgroundVoipCallHandler() async {
388+
WidgetsFlutterBinding.ensureInitialized();
389+
390+
// Get stored user credentials
391+
var credentials = yourUserCredentialsGetMethod();
392+
if (credentials == null) return;
393+
394+
// Initialise StreamVideo
395+
StreamVideo(
396+
// ...
397+
// Make sure you initialise push notification manager
398+
pushNotificationManagerProvider: StreamVideoPushNotificationManager.create(
399+
iosPushProvider: const StreamVideoPushProvider.apn(
400+
name: 'your-ios-provider-name',
401+
),
402+
androidPushProvider: const StreamVideoPushProvider.firebase(
403+
name: 'your-fcm-provider',
404+
),
405+
pushParams: const StreamVideoPushParams(
406+
appName: kAppName,
407+
ios: IOSParams(iconName: 'IconMask'),
408+
),
409+
),
410+
);
411+
}
412+
```
413+
414+
The `_backgroundVoipCallHandler` method should then be set when StreamVideo is initialised:
415+
416+
```dart
417+
StreamVideo(
418+
...,
419+
backgroundVoipCallHandler: _backgroundVoipCallHandler,
420+
);
421+
```
422+
423+
#### Step 7 - Add native code to the iOS project
382424

383425
In your iOS project, add the following imports to your `AppDelegate.swift`:
384426

dogfooding/lib/app/user_auth_controller.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ class UserAuthController extends ChangeNotifier {
4545
/// Logs in the given [user] and returns the user credentials.
4646
Future<UserCredentials> login(User user) async {
4747
final tokenResponse = await _tokenService.loadToken(userId: user.id);
48+
await _prefs.setApiKey(tokenResponse.apiKey);
4849

4950
_authRepo ??=
5051
locator.get<UserAuthRepository>(param1: user, param2: tokenResponse);

dogfooding/lib/core/repos/app_preferences.dart

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ class AppPreferences {
1515
final SharedPreferences _prefs;
1616

1717
static const String _kUserCredentialsPref = 'user_credentials';
18+
static const String _kApiKeyPref = 'api_key';
1819

1920
UserCredentials? get userCredentials {
2021
final jsonString = _prefs.getString(_kUserCredentialsPref);
@@ -24,10 +25,19 @@ class AppPreferences {
2425
return UserCredentials.fromJson(json);
2526
}
2627

28+
String? get apiKey => _prefs.getString(_kApiKeyPref);
29+
2730
Future<bool> setUserCredentials(UserCredentials? credentials) {
2831
final jsonString = jsonEncode(credentials?.toJson());
2932
return _prefs.setString(_kUserCredentialsPref, jsonString);
3033
}
3134

32-
Future<bool> clearUserCredentials() => _prefs.remove(_kUserCredentialsPref);
35+
Future<bool> setApiKey(String apiKey) {
36+
return _prefs.setString(_kApiKeyPref, apiKey);
37+
}
38+
39+
Future<bool> clearUserCredentials() async {
40+
return await _prefs.remove(_kUserCredentialsPref) &&
41+
await _prefs.remove(_kApiKeyPref);
42+
}
3343
}

dogfooding/lib/di/injector.dart

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
// 📦 Package imports:
22
import 'package:flutter/foundation.dart';
3+
import 'package:flutter/material.dart';
34
// 🌎 Project imports:
45
import 'package:flutter_dogfooding/core/repos/app_preferences.dart';
56
import 'package:flutter_dogfooding/core/repos/user_chat_repository.dart';
@@ -19,6 +20,43 @@ import '../utils/consts.dart';
1920

2021
GetIt locator = GetIt.instance;
2122

23+
@pragma('vm:entry-point')
24+
Future<void> _backgroundVoipCallHandler() async {
25+
WidgetsFlutterBinding.ensureInitialized();
26+
final prefs = await SharedPreferences.getInstance();
27+
final appPrefs = AppPreferences(prefs: prefs);
28+
29+
final apiKey = appPrefs.apiKey;
30+
final userCredentials = appPrefs.userCredentials;
31+
32+
if (apiKey == null || userCredentials == null) {
33+
return;
34+
}
35+
36+
StreamVideo(
37+
apiKey,
38+
user: User(info: userCredentials.userInfo),
39+
userToken: userCredentials.token.rawValue,
40+
options: const StreamVideoOptions(
41+
logPriority: Priority.info,
42+
muteAudioWhenInBackground: true,
43+
muteVideoWhenInBackground: true,
44+
),
45+
pushNotificationManagerProvider: StreamVideoPushNotificationManager.create(
46+
iosPushProvider: const StreamVideoPushProvider.apn(
47+
name: 'flutter-apn',
48+
),
49+
androidPushProvider: const StreamVideoPushProvider.firebase(
50+
name: 'flutter-firebase',
51+
),
52+
pushParams: const StreamVideoPushParams(
53+
appName: kAppName,
54+
ios: IOSParams(iconName: 'IconMask'),
55+
),
56+
),
57+
);
58+
}
59+
2260
/// This class is responsible for registering dependencies
2361
/// and injecting them into the app.
2462
class AppInjector {
@@ -160,6 +198,7 @@ StreamVideo _initStreamVideo(
160198
appName: kAppName,
161199
ios: IOSParams(iconName: 'IconMask'),
162200
),
201+
backgroundVoipCallHandler: _backgroundVoipCallHandler,
163202
),
164203
);
165204

dogfooding/lib/screens/home_screen.dart

Lines changed: 88 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -55,15 +55,18 @@ class _HomeScreenState extends State<HomeScreen> {
5555
super.initState();
5656
}
5757

58-
Future<void> _getOrCreateCall() async {
58+
Future<void> _getOrCreateCall({List<String> memberIds = const []}) async {
5959
var callId = _callIdController.text;
6060
if (callId.isEmpty) callId = generateAlphanumericString(12);
6161

6262
unawaited(showLoadingIndicator(context));
6363
_call = _streamVideo.makeCall(type: kCallType, id: callId);
6464

6565
try {
66-
await _call!.getOrCreate();
66+
await _call!.getOrCreate(
67+
memberIds: memberIds,
68+
ringing: memberIds.isNotEmpty,
69+
);
6770
} catch (e, stk) {
6871
debugPrint('Error joining or creating call: $e');
6972
debugPrint(stk.toString());
@@ -75,6 +78,56 @@ class _HomeScreenState extends State<HomeScreen> {
7578
}
7679
}
7780

81+
Future<void> _directCall(BuildContext context) async {
82+
TextEditingController controller = TextEditingController();
83+
84+
return showDialog(
85+
context: context,
86+
builder: (context) {
87+
return AlertDialog(
88+
title: const Text('Enter ID of user you want to call'),
89+
content: Column(
90+
mainAxisSize: MainAxisSize.min,
91+
children: [
92+
TextField(
93+
controller: controller,
94+
decoration: const InputDecoration(hintText: "User id"),
95+
),
96+
const SizedBox(
97+
height: 8,
98+
),
99+
Align(
100+
alignment: Alignment.centerRight,
101+
child: ElevatedButton(
102+
style: ButtonStyle(
103+
shape: MaterialStatePropertyAll(
104+
RoundedRectangleBorder(
105+
borderRadius: BorderRadius.circular(8),
106+
),
107+
),
108+
backgroundColor: const MaterialStatePropertyAll<Color>(
109+
Color(0xFF005FFF),
110+
),
111+
),
112+
onPressed: () {
113+
Navigator.of(context).pop();
114+
_getOrCreateCall(memberIds: [controller.text]);
115+
},
116+
child: const Padding(
117+
padding: EdgeInsets.symmetric(vertical: 14),
118+
child: Text(
119+
'Call',
120+
style: TextStyle(color: Colors.white),
121+
),
122+
),
123+
),
124+
)
125+
],
126+
),
127+
);
128+
});
129+
}
130+
78131
@override
79132
void dispose() {
80133
_callIdController.dispose();
@@ -171,9 +224,6 @@ class _HomeScreenState extends State<HomeScreen> {
171224
borderRadius: BorderRadius.circular(8),
172225
),
173226
),
174-
backgroundColor: const MaterialStatePropertyAll<Color>(
175-
Color(0xFF005FFF),
176-
),
177227
),
178228
onPressed: _getOrCreateCall,
179229
child: const Padding(
@@ -182,6 +232,34 @@ class _HomeScreenState extends State<HomeScreen> {
182232
),
183233
),
184234
),
235+
const SizedBox(height: 8),
236+
Align(
237+
alignment: Alignment.centerLeft,
238+
child: Text(
239+
"Want to directly call someone?",
240+
style: theme.textTheme.bodyMedium?.copyWith(
241+
fontSize: 12,
242+
),
243+
),
244+
),
245+
const SizedBox(height: 8),
246+
SizedBox(
247+
width: double.infinity,
248+
child: ElevatedButton(
249+
style: ButtonStyle(
250+
shape: MaterialStatePropertyAll(
251+
RoundedRectangleBorder(
252+
borderRadius: BorderRadius.circular(8),
253+
),
254+
),
255+
),
256+
onPressed: () => _directCall(context),
257+
child: const Padding(
258+
padding: EdgeInsets.symmetric(vertical: 14),
259+
child: Text('Direct Call'),
260+
),
261+
),
262+
),
185263
],
186264
),
187265
),
@@ -213,14 +291,18 @@ class _JoinForm extends StatelessWidget {
213291
controller: callIdController,
214292
style: const TextStyle(color: Colors.white),
215293
decoration: InputDecoration(
294+
contentPadding: const EdgeInsets.symmetric(horizontal: 8),
216295
isDense: true,
217296
border: const OutlineInputBorder(
218297
borderRadius: BorderRadius.all(Radius.circular(8)),
219298
),
220299
hintText: 'Enter call id',
300+
221301
// suffix button to generate a random call id
222302
suffixIcon: IconButton(
223303
icon: const Icon(Icons.refresh),
304+
color: Colors.white,
305+
padding: EdgeInsets.zero,
224306
onPressed: () {
225307
// generate a 10 character nanoId for call id
226308
final callId = generateAlphanumericString(10);
@@ -256,6 +338,7 @@ class _JoinForm extends StatelessWidget {
256338
padding: EdgeInsets.symmetric(vertical: 14),
257339
child: Text(
258340
'Join call',
341+
style: TextStyle(color: Colors.white),
259342
),
260343
),
261344
);

packages/stream_video/lib/src/coordinator/coordinator_client.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import 'models/coordinator_events.dart';
1919
import 'models/coordinator_models.dart' as models;
2020

2121
abstract class CoordinatorClient {
22+
bool get isConnected;
2223
SharedEmitter<CoordinatorEvent> get events;
2324

2425
Future<Result<None>> connectUser(

packages/stream_video/lib/src/coordinator/open_api/coordinator_client_open_api.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,10 @@ class CoordinatorClientOpenApi extends CoordinatorClient {
8686

8787
@override
8888
SharedEmitter<CoordinatorEvent> get events => _events;
89+
90+
@override
91+
bool get isConnected => _ws?.isConnected ?? false;
92+
8993
final _events = MutableSharedEmitterImpl<CoordinatorEvent>();
9094

9195
final _connectionState = MutableStateEmitterImpl<CoordinatorConnectionState>(

packages/stream_video/lib/src/coordinator/retry/coordinator_client_retry.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,9 @@ class CoordinatorClientRetry extends CoordinatorClient {
116116
@override
117117
SharedEmitter<CoordinatorEvent> get events => _delegate.events;
118118

119+
@override
120+
bool get isConnected => _delegate.isConnected;
121+
119122
@override
120123
Future<Result<CallReceivedData>> getCall({
121124
required StreamCallCid callCid,

packages/stream_video/lib/src/stream_video.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -400,6 +400,7 @@ class StreamVideo {
400400
_logger.d(() => '[onAppState] state: $state');
401401
try {
402402
final activeCallCid = _state.activeCall.valueOrNull?.callCid;
403+
403404
if (state.isPaused && activeCallCid == null) {
404405
_logger.i(() => '[onAppState] close connection');
405406
_subscriptions.cancel(_idEvents);

packages/stream_video_flutter/android/build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ group 'io.getstream.video.flutter.stream_video_flutter'
22
version '1.0-SNAPSHOT'
33

44
buildscript {
5-
ext.kotlin_version = '1.6.10'
5+
ext.kotlin_version = '1.9.10'
66
repositories {
77
google()
88
mavenCentral()

0 commit comments

Comments
 (0)