Call Deep Linking

Implementing Call Deep Linking

It’s common in mobile calling apps that a call can be started from a link, that should start the app and dial in immediately. This can be accomplished by universal links on iOS and App links on Android.

In this guide we’ll show you how you can handle such deep links to navigate to the call screen provided by our SDK. For the sake of simplicity, we’ll use the uni_links plugin that allows handling iOS and Android deep links in a similar way.

To get started, you first need to do some platform-specific configuration.

To handle deep links in Android you need to declare an intent filter in android/app/src/main/AndroidManifest.xml like it is shown in the example below:

<activity
    android:name=".MainActivity"
    android:exported="true">
    <intent-filter android:autoVerify="true" android:label="call_link">
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />
        <data
            android:host="[YOUR_HOST]"
            android:pathPrefix="/join/"
            android:scheme="https" />
    </intent-filter>
</activity>

You can learn more about this structure in the official documentation, but the gist of it is this:

  • Adding the <intent-filter> lets your Activity parse specific intents that come from inside or outside of your application.
  • By adding the appropriate <action> and <category> tags, you let your Activity open and consume data that’s browsable, such as URLs.
  • The most important part is the <data> tag. It allows you to parse the link in the format of https://[YOUR_HOST]/join/. Any URL that has that structure will be consumable by your Activity.

But to make your published app associate with a hosted and published website and to stop potential security issues, the website needs to know how to recognize your app.

You have to take your application package-id and its SHA-256 fingerprint and use it to generate an assetlinks.json file. You can find the full process and documentation on the official Android deep linking page.

Once you’ve generated these fingerprints - we recommend doing it for debug and releasekeystores and your CI if it uses a different keystore than local setup - you can create the assetlinks.json file:

[{ // first application
  "relation": ["delegate_permission/common.handle_all_urls"],
  "target": {
    "namespace": "android_app",
    "package_name": "com.my.application.debug",
    "sha256_cert_fingerprints": ["sha-256-fingerprint"]
  }
  },
  { // second application
  "relation": ["delegate_permission/common.handle_all_urls"],
  "target": {
    "namespace": "android_app",
    "package_name": "com.my.application",
    "sha256_cert_fingerprints": ["sha-256-fingerprint"]
  }
}]

Note that the relation and namespace parts are not as important as the rest and how each object represents one application.

Now that you have the file created, you need to upload it either to the root directory of your website, or to the .well-known directory.

In order to support universal links, you need to have paid Apple developer account. On the Apple developer website, you will need to add the “associated domains” for your app id.

Next, you need to enable the “Associated Domains” capability for your app in Xcode, and specify an app link, in the format applinks:[YOUR_HOST]. After that your ios/Runner/Runner.entitlements file should look like this:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>com.apple.developer.associated-domains</key>
    <array>
        <string>applinks:[YOUR_HOST]</string>
    </array>
</dict>
</plist>

Next, you need to upload apple-app-site-association file, either to the root directory of your website, or to the .well-known directory. The AASA (short for apple-app-site-association) is a JSON file that lives on your website and associates your domain with your native app.

In its simplest form, your AASA file should have the following format:

{
  "applinks": {
    "apps": [],
    "details": [{
      "appID": "[YOUR_TEAM_ID].[YOUR_BUNDLE_ID]",
      "paths": [
        "*"
      ]
    }]
  }
}

You can also specify exact paths if you want to have stricter control over which ones can invoke your app. You can also specify several apps on the same domain.

Before proceeding, please make sure that the uploaded file is a valid one, and it’s deployed at the right place. For this, you can use Apple’s validation tool.

To get started, you first need to add the uni_links plugin to your pubspec.yaml:

dependencies:
  # Other dependencies
  uni_links: <latest_version>

Next, use the snippet below to listen for incoming deep links and navigate to JoinScreen that we’ll cover in the next step:

import 'package:uni_links/uni_links.dart';

StreamSubscription<Uri?>? _deepLinkSubscription;

Future<void> _observeDeepLinks() async {
  // The app was in the background.
  if (!kIsWeb) {
    _deepLinkSubscription = uriLinkStream.listen((uri) {
      if (mounted && uri != null) _handleDeepLink(uri);
    });
  }

  // The app was terminated.
  try {
    final initialUri = await getInitialUri();
    if (initialUri != null) _handleDeepLink(initialUri);
  } catch (e) {
    debugPrint(e.toString());
  }
}

Future<void> _handleDeepLink(Uri uri) async {
  // Parse the call id from the deep link.
  final callId = uri.queryParameters['id'];
  if (callId == null) return;

  // return if the video user is not yet logged in.
  // Replace the getCurrentUser() method with your method to retrieve the current user
  final currentUser = getCurrentUser();
  if (currentUser == null) return;

  final streamVideo = StreamVideo.instance;
  final call = streamVideo.makeCall(callType: kCallType, id: callId);

  try {
    await call.getOrCreate();
  } catch (e, stk) {
    debugPrint('Error joining or creating call: $e');
    debugPrint(stk.toString());
    return;
  }

  // Your method to navigate to the lobby/call screen.
  navigateToCallScreen();
}

In this snippet, you:

  1. Handle the case when the app was in the background and is now brought to the foreground by a deep link.
  2. Handle the case when the app was terminated and is now started by a deep link.
  3. Extract the call ID from the deep link URL.
  4. Navigate to a screen that will handle the call.

While you can initialize the call on your call screen, you can also add an intermediary JoinScreen with the call initialization:

class JoinScreen extends StatefulWidget {
  const JoinScreen({
    super.key,
    required this.callId,
  });

  final String callId;

  @override
  State<JoinScreen> createState() => _JoinScreenState();
}

class _JoinScreenState extends State<JoinScreen> {
  var _isInProgress = false;

  @override
  void initState() {
    super.initState();
    _joinCall(widget.callId);
  }

  // Step 1
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Joining call'),
      ),
      body: Center(
        child: _isInProgress
            ? const CircularProgressIndicator(
          strokeWidth: 2,
        )
            : const SizedBox(),
      ),
    );
  }

  // Step 2
  Future<void> _joinCall(String callId) async {
    setState(() => _isInProgress = true);

    try {
      final call = StreamVideo.instance.makeCall(
        id: callId,
        callType: StreamCallType.defaultType(),
      );

      await call.join();

      await _navigateToCall(call);
    } catch (e) {
      debugPrint(e.toString());
    } finally {
      setState(() => _isInProgress = false);
    }
  }

  // Step 3
  Future<void> _navigateToCall(Call call) async {
    // Navigate to your call screen
  }
}

Here’s what you’re doing here, step-by-step:

  1. Showing a connecting screen for the call while it initializes.
  2. Joining the call with the call ID from the deep link.
  3. Navigating to your call screen.

Finally, run your application, generate a call link using the schema above, and try opening the link. It should automatically redirect to your app and open the call.

Alternatively, you can run the following commands from the command line:

# Android
adb shell 'am start -W -a android.intent.action.VIEW -c android.intent.category.BROWSABLE -d "https://[YOUR_HOST]/join/call123"'

# iOS
/usr/bin/xcrun simctl openurl booted "https://[YOUR_HOST]/join/call123"
© Getstream.io, Inc. All Rights Reserved.