Skip to content

Commit

Permalink
feat: added background voip handle called in terminated app state whe…
Browse files Browse the repository at this point in the history
…n 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]>
  • Loading branch information
Brazol and deven98 authored Feb 6, 2024
1 parent 12bfcb8 commit 6941db5
Show file tree
Hide file tree
Showing 18 changed files with 269 additions and 25 deletions.
44 changes: 43 additions & 1 deletion docusaurus/docs/Flutter/05-advanced/02-ringing.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -378,7 +378,49 @@ say you want to end all calls on the CallKit side, you can end them this way:
StreamVideo.instance.pushNotificationManager?.endAllCalls();
```

#### Step 6 - Add native code to the iOS project
#### Step 6 - Add callback to handle call in terminated state

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`.

```dart
@pragma('vm:entry-point')
Future<void> _backgroundVoipCallHandler() async {
WidgetsFlutterBinding.ensureInitialized();
// Get stored user credentials
var credentials = yourUserCredentialsGetMethod();
if (credentials == null) return;
// Initialise StreamVideo
StreamVideo(
// ...
// Make sure you initialise push notification manager
pushNotificationManagerProvider: StreamVideoPushNotificationManager.create(
iosPushProvider: const StreamVideoPushProvider.apn(
name: 'your-ios-provider-name',
),
androidPushProvider: const StreamVideoPushProvider.firebase(
name: 'your-fcm-provider',
),
pushParams: const StreamVideoPushParams(
appName: kAppName,
ios: IOSParams(iconName: 'IconMask'),
),
),
);
}
```

The `_backgroundVoipCallHandler` method should then be set when StreamVideo is initialised:

```dart
StreamVideo(
...,
backgroundVoipCallHandler: _backgroundVoipCallHandler,
);
```

#### Step 7 - Add native code to the iOS project

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

Expand Down
1 change: 1 addition & 0 deletions dogfooding/lib/app/user_auth_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ class UserAuthController extends ChangeNotifier {
/// Logs in the given [user] and returns the user credentials.
Future<UserCredentials> login(User user) async {
final tokenResponse = await _tokenService.loadToken(userId: user.id);
await _prefs.setApiKey(tokenResponse.apiKey);

_authRepo ??=
locator.get<UserAuthRepository>(param1: user, param2: tokenResponse);
Expand Down
12 changes: 11 additions & 1 deletion dogfooding/lib/core/repos/app_preferences.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ class AppPreferences {
final SharedPreferences _prefs;

static const String _kUserCredentialsPref = 'user_credentials';
static const String _kApiKeyPref = 'api_key';

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

String? get apiKey => _prefs.getString(_kApiKeyPref);

Future<bool> setUserCredentials(UserCredentials? credentials) {
final jsonString = jsonEncode(credentials?.toJson());
return _prefs.setString(_kUserCredentialsPref, jsonString);
}

Future<bool> clearUserCredentials() => _prefs.remove(_kUserCredentialsPref);
Future<bool> setApiKey(String apiKey) {
return _prefs.setString(_kApiKeyPref, apiKey);
}

Future<bool> clearUserCredentials() async {
return await _prefs.remove(_kUserCredentialsPref) &&
await _prefs.remove(_kApiKeyPref);
}
}
39 changes: 39 additions & 0 deletions dogfooding/lib/di/injector.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// 📦 Package imports:
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
// 🌎 Project imports:
import 'package:flutter_dogfooding/core/repos/app_preferences.dart';
import 'package:flutter_dogfooding/core/repos/user_chat_repository.dart';
Expand All @@ -19,6 +20,43 @@ import '../utils/consts.dart';

GetIt locator = GetIt.instance;

@pragma('vm:entry-point')
Future<void> _backgroundVoipCallHandler() async {
WidgetsFlutterBinding.ensureInitialized();
final prefs = await SharedPreferences.getInstance();
final appPrefs = AppPreferences(prefs: prefs);

final apiKey = appPrefs.apiKey;
final userCredentials = appPrefs.userCredentials;

if (apiKey == null || userCredentials == null) {
return;
}

StreamVideo(
apiKey,
user: User(info: userCredentials.userInfo),
userToken: userCredentials.token.rawValue,
options: const StreamVideoOptions(
logPriority: Priority.info,
muteAudioWhenInBackground: true,
muteVideoWhenInBackground: true,
),
pushNotificationManagerProvider: StreamVideoPushNotificationManager.create(
iosPushProvider: const StreamVideoPushProvider.apn(
name: 'flutter-apn',
),
androidPushProvider: const StreamVideoPushProvider.firebase(
name: 'flutter-firebase',
),
pushParams: const StreamVideoPushParams(
appName: kAppName,
ios: IOSParams(iconName: 'IconMask'),
),
),
);
}

/// This class is responsible for registering dependencies
/// and injecting them into the app.
class AppInjector {
Expand Down Expand Up @@ -160,6 +198,7 @@ StreamVideo _initStreamVideo(
appName: kAppName,
ios: IOSParams(iconName: 'IconMask'),
),
backgroundVoipCallHandler: _backgroundVoipCallHandler,
),
);

Expand Down
93 changes: 88 additions & 5 deletions dogfooding/lib/screens/home_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -55,15 +55,18 @@ class _HomeScreenState extends State<HomeScreen> {
super.initState();
}

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

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

try {
await _call!.getOrCreate();
await _call!.getOrCreate(
memberIds: memberIds,
ringing: memberIds.isNotEmpty,
);
} catch (e, stk) {
debugPrint('Error joining or creating call: $e');
debugPrint(stk.toString());
Expand All @@ -75,6 +78,56 @@ class _HomeScreenState extends State<HomeScreen> {
}
}

Future<void> _directCall(BuildContext context) async {
TextEditingController controller = TextEditingController();

return showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: const Text('Enter ID of user you want to call'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
controller: controller,
decoration: const InputDecoration(hintText: "User id"),
),
const SizedBox(
height: 8,
),
Align(
alignment: Alignment.centerRight,
child: ElevatedButton(
style: ButtonStyle(
shape: MaterialStatePropertyAll(
RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
backgroundColor: const MaterialStatePropertyAll<Color>(
Color(0xFF005FFF),
),
),
onPressed: () {
Navigator.of(context).pop();
_getOrCreateCall(memberIds: [controller.text]);
},
child: const Padding(
padding: EdgeInsets.symmetric(vertical: 14),
child: Text(
'Call',
style: TextStyle(color: Colors.white),
),
),
),
)
],
),
);
});
}

@override
void dispose() {
_callIdController.dispose();
Expand Down Expand Up @@ -171,9 +224,6 @@ class _HomeScreenState extends State<HomeScreen> {
borderRadius: BorderRadius.circular(8),
),
),
backgroundColor: const MaterialStatePropertyAll<Color>(
Color(0xFF005FFF),
),
),
onPressed: _getOrCreateCall,
child: const Padding(
Expand All @@ -182,6 +232,34 @@ class _HomeScreenState extends State<HomeScreen> {
),
),
),
const SizedBox(height: 8),
Align(
alignment: Alignment.centerLeft,
child: Text(
"Want to directly call someone?",
style: theme.textTheme.bodyMedium?.copyWith(
fontSize: 12,
),
),
),
const SizedBox(height: 8),
SizedBox(
width: double.infinity,
child: ElevatedButton(
style: ButtonStyle(
shape: MaterialStatePropertyAll(
RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
),
onPressed: () => _directCall(context),
child: const Padding(
padding: EdgeInsets.symmetric(vertical: 14),
child: Text('Direct Call'),
),
),
),
],
),
),
Expand Down Expand Up @@ -213,14 +291,18 @@ class _JoinForm extends StatelessWidget {
controller: callIdController,
style: const TextStyle(color: Colors.white),
decoration: InputDecoration(
contentPadding: const EdgeInsets.symmetric(horizontal: 8),
isDense: true,
border: const OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(8)),
),
hintText: 'Enter call id',

// suffix button to generate a random call id
suffixIcon: IconButton(
icon: const Icon(Icons.refresh),
color: Colors.white,
padding: EdgeInsets.zero,
onPressed: () {
// generate a 10 character nanoId for call id
final callId = generateAlphanumericString(10);
Expand Down Expand Up @@ -256,6 +338,7 @@ class _JoinForm extends StatelessWidget {
padding: EdgeInsets.symmetric(vertical: 14),
child: Text(
'Join call',
style: TextStyle(color: Colors.white),
),
),
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import 'models/coordinator_events.dart';
import 'models/coordinator_models.dart' as models;

abstract class CoordinatorClient {
bool get isConnected;
SharedEmitter<CoordinatorEvent> get events;

Future<Result<None>> connectUser(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,10 @@ class CoordinatorClientOpenApi extends CoordinatorClient {

@override
SharedEmitter<CoordinatorEvent> get events => _events;

@override
bool get isConnected => _ws?.isConnected ?? false;

final _events = MutableSharedEmitterImpl<CoordinatorEvent>();

final _connectionState = MutableStateEmitterImpl<CoordinatorConnectionState>(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,9 @@ class CoordinatorClientRetry extends CoordinatorClient {
@override
SharedEmitter<CoordinatorEvent> get events => _delegate.events;

@override
bool get isConnected => _delegate.isConnected;

@override
Future<Result<CallReceivedData>> getCall({
required StreamCallCid callCid,
Expand Down
1 change: 1 addition & 0 deletions packages/stream_video/lib/src/stream_video.dart
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,7 @@ class StreamVideo {
_logger.d(() => '[onAppState] state: $state');
try {
final activeCallCid = _state.activeCall.valueOrNull?.callCid;

if (state.isPaused && activeCallCid == null) {
_logger.i(() => '[onAppState] close connection');
_subscriptions.cancel(_idEvents);
Expand Down
2 changes: 1 addition & 1 deletion packages/stream_video_flutter/android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ group 'io.getstream.video.flutter.stream_video_flutter'
version '1.0-SNAPSHOT'

buildscript {
ext.kotlin_version = '1.6.10'
ext.kotlin_version = '1.9.10'
repositories {
google()
mavenCentral()
Expand Down
2 changes: 1 addition & 1 deletion packages/stream_video_flutter/example/android/build.gradle
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
buildscript {
ext.kotlin_version = '1.8.10'
ext.kotlin_version = '1.9.10'
repositories {
google()
mavenCentral()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ group 'io.getstream.video.flutter.background.stream_video_flutter_background'
version '1.0-SNAPSHOT'

buildscript {
ext.kotlin_version = '1.6.10'
ext.kotlin_version = '1.9.10'
repositories {
google()
mavenCentral()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
buildscript {
ext.kotlin_version = '1.6.10'
ext.kotlin_version = '1.9.10'
repositories {
google()
mavenCentral()
Expand Down
Loading

0 comments on commit 6941db5

Please sign in to comment.