Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion dogfooding/lib/app/app_content.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import 'dart:async';

import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
Expand Down Expand Up @@ -181,7 +183,11 @@ class _StreamDogFoodingAppContentState
void _observeFcmMessages() {
FirebaseMessaging.onBackgroundMessage(firebaseMessagingBackgroundHandler);
_compositeSubscription.add(
FirebaseMessaging.onMessage.listen(handleRemoteMessage),
FirebaseMessaging.onMessage.listen(
(message) => unawaited(
StreamVideo.instance.handleRingingFlowNotifications(message.data),
),
),
);
}

Expand Down
82 changes: 36 additions & 46 deletions dogfooding/lib/app/firebase_messaging_handler.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,62 +2,52 @@ import 'dart:async';

import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:stream_video_flutter/stream_video_flutter.dart';
import 'package:stream_video_push_notification/stream_video_push_notification.dart';

import '../core/repos/app_preferences.dart';
import '../core/repos/token_service.dart';
import '../di/injector.dart';
import '../firebase_options.dart';

// As this runs in a separate isolate, we need to setup the app again.
// On Android this runs in a separate isolate, which never ran main(), so the
// app has to be set up again from scratch. The SDK owns the client's lifecycle
// from there: observing the ringing events a background isolate can act on,
// and disposing once the user has answered, declined, or let the call time
// out.
@pragma('vm:entry-point')
Future<void> firebaseMessagingBackgroundHandler(RemoteMessage message) async {
// Initialise Firebase
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
// Initialise the app.
await AppInjector.init();

try {
// Return if the user is not logged in.
final prefs = locator.get<AppPreferences>();
final credentials = prefs.userCredentials;
if (credentials == null) return;

final tokenResponse = await locator.get<TokenService>().loadToken(
userId: credentials.userInfo.id,
environment: prefs.environment,
);

// Initialise the video client.
final streamVideo = AppInjector.registerStreamVideo(
tokenResponse,
credentials.userInfo.toUser(),
prefs.environment,
);

final subscription = streamVideo.observeCoreRingingEventsForBackground();

streamVideo.disposeAfterResolvingRinging(
disposingCallback: () {
subscription.cancel();
AppInjector.reset();
},
);

// Handle the message.
await handleRemoteMessage(message);
} catch (e, stk) {
debugPrint('Error handling remote message: $e');
debugPrint(stk.toString());
}

// Reset the injector once the message is handled.
return AppInjector.reset();

final ownsInjector = !locator.isRegistered<AppPreferences>();

await StreamVideoPushHandler.handleBackgroundMessage(
message,
createStreamVideo: () =>
_createStreamVideo(initialiseInjector: ownsInjector),
onDispose: ownsInjector ? AppInjector.reset : null,
);
}

Future<bool> handleRemoteMessage(RemoteMessage message) async {
final streamVideo = locator.get<StreamVideo>();
return streamVideo.handleRingingFlowNotifications(message.data);
/// Builds the client for the background isolate, or nothing when nobody is
/// logged in.
Future<StreamVideo?> _createStreamVideo({
required bool initialiseInjector,
}) async {
if (initialiseInjector) await AppInjector.init();

final prefs = locator.get<AppPreferences>();
final credentials = prefs.userCredentials;
if (credentials == null) return null;

final tokenResponse = await locator.get<TokenService>().loadToken(
userId: credentials.userInfo.id,
environment: prefs.environment,
);

return AppInjector.registerStreamVideo(
tokenResponse,
credentials.userInfo.toUser(),
prefs.environment,
);
}
18 changes: 18 additions & 0 deletions packages/stream_video_push_notification/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,24 @@

### ✅ Added

- Added `StreamVideoPushHandler.handleBackgroundMessage`, which simplifies the ringing setup by running a Stream ringing push through its whole background lifecycle from your Firebase background handler: it builds the client through the factory you give it, observes the ringing events a background isolate can act on, and disposes the client — along with whatever that factory set up — once the user has answered, declined, or let the call time out. It replaces the setup and teardown each integration had to write by hand. Pass `existingClient` if your app does not keep its client in the `StreamVideo` singleton.

```dart
@pragma('vm:entry-point')
Future<void> firebaseMessagingBackgroundHandler(RemoteMessage message) async {
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);

await StreamVideoPushHandler.handleBackgroundMessage(
message,
createStreamVideo: () async {
// Read credentials, fetch a token, build the client. Return null when
// nobody is logged in.
},
onDispose: () => MyDependencies.reset(),
);
}
```

- [Android] Added a Telecom integration for the ringing flow, which registers incoming and outgoing ringing calls with the platform's [Telecom stack](https://developer.android.com/develop/connectivity/telecom) through Jetpack Telecom. This gives the call proper audio focus and a place in the system call state, and lets it be answered or hung up from a paired watch, a car head unit or a Bluetooth headset. The incoming call notification and full-screen ringing UI are unchanged. It is **on by default from Android 17**: for an app targeting API 37 the platform will not play a ringtone from a service started by a push unless the call is in the Telecom stack, so ringing does not work correctly without it. The default follows the Android version of the device rather than your `targetSdk`, so it is on for any app running on Android 17 — if you target below API 37 the restriction does not apply to you and you can opt out with `AndroidPushConfiguration(telecom: TelecomPushConfiguration(enabled: false))`. It is **off by default below Android 17**, where ringing works either way, so an existing integration is unaffected unless you pass `enabled: true`.
- [iOS] Added `reportCallEnded`, which reports how a call ended to CallKit so it is listed correctly in the system Recents.

Expand Down
Loading