Skip to content

Commit 1713d7b

Browse files
Merge pull request #542 from opentok/VIDCS-3685
VIDCS-3685: Connection Service integration
2 parents 2d9f03e + 09b5061 commit 1713d7b

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+2592
-2
lines changed
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
name: Build Basic-Video-Chat-ConnectionService-Java
2+
3+
on:
4+
push:
5+
branches: [main] # Just in case main was not up to date while merging PR
6+
pull_request:
7+
types: [opened, synchronize]
8+
9+
jobs:
10+
run:
11+
continue-on-error: true
12+
runs-on: ubuntu-latest
13+
strategy:
14+
fail-fast: false
15+
steps:
16+
- name: checkout
17+
uses: actions/checkout@v2
18+
19+
- name: Set up JDK
20+
uses: actions/setup-java@v1
21+
with:
22+
java-version: 17
23+
24+
- name: Build
25+
run: cd Basic-Video-Chat-ConnectionService-Java && ./gradlew app:assembleRelease && cd ..
26+
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# intellij
2+
*.iml
3+
4+
.gradle
5+
/local.properties
6+
/.idea/workspace.xml
7+
/.idea/libraries
8+
.DS_Store
9+
/build
10+
/captures
11+
.externalNativeBuild
12+
app/build
13+
14+
.settings/
15+
app/jniLibs/
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
# Basic Video Chat with `ConnectionService`
2+
3+
This project enables real-time video calling using a self-managed ConnectionService. The app handles outgoing and incoming calls, integrates with system Telecom APIs, and manages in-call notifications and audio routing.
4+
5+
## Features
6+
7+
- Outgoing and incoming VoIP calls with fullscreen call UIs
8+
- Integration with Android Telecom API for call and notification management
9+
- Real-time video sessions using OpenTok
10+
- Audio device selection and call hold capabilities
11+
- Self-managed calls for a streamlined calling experience
12+
13+
## How It Works
14+
15+
The app customizes Android’s ConnectionService and Connection classes to:
16+
- Place outgoing calls through a tailored PhoneAccount.
17+
- Report incoming calls.
18+
- Manage call state changes, notifications, foreground execution handling and audio routing.
19+
- Hardcoded local OpenTok credentials.
20+
21+
## Configuration
22+
23+
1. **API Credentials:**
24+
Update your `OpenTokConfig` with your `API_KEY`, `SESSION_ID`, and `TOKEN` variables. You can obtain these values from your [TokBox account](https://tokbox.com/account/#/) by creating a new session in the [Video API Playground](https://tokbox.com/developer/tools/playground/) site.
25+
In a production setup, these values should be provided from a secure server.
26+
27+
2. **PhoneAccount:**
28+
The `PhoneAccountManager` registers the PhoneAccount necessary to interface with the Telecom API.
29+
30+
3. **Manifest Setup:**
31+
Verify that `AndroidManifest.xml` includes all necessary permissions such as `CAMERA`, `RECORD_AUDIO`, `FOREGROUND_SERVICE`, `MANAGE_OWN_CALLS`, and `BIND_TELECOM_CONNECTION_SERVICE`.
32+
33+
Register the service in `AndroidManifest.xml` file.
34+
35+
```xml
36+
<service
37+
android:name=".connectionservice.VonageConnectionService"
38+
android:exported="true"
39+
android:permission="android.permission.BIND_TELECOM_CONNECTION_SERVICE"
40+
android:foregroundServiceType="microphone|camera">
41+
<intent-filter>
42+
<action android:name="android.telecom.ConnectionService" />
43+
</intent-filter>
44+
</service>
45+
```
46+
47+
4. **Audio Focus Management:**
48+
When using ConnectionService, you need to configure the SDK to delegate audio focus control to your app:
49+
50+
```java
51+
private AudioDeviceManager audioDeviceManager;
52+
private BaseAudioDevice.AudioFocusManager audioFocusManager;
53+
54+
public void setupAudioFocusManager(Context context) {
55+
audioDeviceManager = new AudioDeviceManager(context);
56+
audioFocusManager = audioDeviceManager.getAudioFocusManager();
57+
58+
audioFocusManager.setRequestAudioFocus(false);
59+
}
60+
61+
public void notifyAudioFocusIsActive() {
62+
audioFocusManager.audioFocusActivated();
63+
}
64+
65+
public void notifyAudioFocusIsInactive() {
66+
audioFocusManager.audioFocusDeactivated();
67+
}
68+
```
69+
70+
When delegating audio focus to the app, the SDK will stop its automatic audio routing. Instead, this routing logic will be handled by ConnectionService, which notifies the app about available audio devices through the Connection's CallEndpoint and CallAudioState APIs. Your app must implement support for audio device enumeration and selection logic.
71+
72+
This delegation ensures proper audio routing and coordination with the Android Telecom system.
73+
74+
75+
## Requirements
76+
77+
- Android API Level 26 or higher is recommended.
78+
- Ensure proper runtime permissions are granted for camera, audio recording, and foreground service usage.
79+
80+
## Overview
81+
82+
A `ConnectionService` allows apps to manage VoIP or phone calls, whether they need to integrate with
83+
the system dialer (system managed) or operate independently (self managed). By implementing this
84+
service, a VoIP app can leverage Android’s Telecom APIs to provide features such as call switching,
85+
unified audio route management, Bluetooth device support, and integration with companion devices
86+
like smartwatches. This ensures calls are accessible and controllable across different devices, with
87+
consistent user experiences for actions like answering, rejecting, or ending calls.
88+
89+
## TelecomManager and PhoneAccount
90+
91+
To initiate phone or VoIP calls, `TelecomManager` relies on a registered `PhoneAccount`. Register
92+
your app’s `PhoneAccount` using `TelecomManager.registerPhoneAccount()`, and assign it the
93+
`CAPABILITY_SELF_MANAGED` capability. This signals that your app will handle the connection logic
94+
and call management independently.
95+
96+
## Implementing ConnectionService
97+
98+
To handle outgoing and incoming calls, your app should use [TelecomManager.placeCall(Uri, Bundle)](https://developer.android.com/reference/android/telecom/TelecomManager#placeCall(android.net.Uri,%20android.os.Bundle)) for outgoing calls and [TelecomManager.addNewIncomingCall()](https://developer.android.com/reference/android/telecom/TelecomManager#addNewIncomingCall(android.telecom.PhoneAccountHandle,%20android.os.Bundle)) to notify the system of new incoming calls. When these APIs are called, the Telecom framework binds to your app’s `ConnectionService`.
99+
100+
Your implementation should override the following `ConnectionService` methods:
101+
- [onCreateOutgoingConnection()](https://developer.android.com/reference/android/telecom/ConnectionService#onCreateOutgoingConnection(android.telecom.PhoneAccountHandle,%20android.telecom.ConnectionRequest)): Invoked by Telecom to create a new [Connection](https://developer.android.com/reference/android/telecom/Connection) for an outgoing call initiated by your app.
102+
- [onCreateOutgoingConnectionFailed()](https://developer.android.com/reference/android/telecom/ConnectionService#onCreateOutgoingConnectionFailed(android.telecom.PhoneAccountHandle,%20android.telecom.ConnectionRequest)): Called if an outgoing call cannot be processed. Your app should not attempt to place the call.
103+
- [onCreateIncomingConnection()](https://developer.android.com/reference/android/telecom/ConnectionService#onCreateIncomingConnection(android.telecom.PhoneAccountHandle,%20android.telecom.ConnectionRequest)): Invoked to create a new [Connection](https://developer.android.com/reference/android/telecom/Connection) for an incoming call reported by your app.
104+
- [onCreateIncomingConnectionFailed()](https://developer.android.com/reference/android/telecom/ConnectionService#onCreateIncomingConnectionFailed(android.telecom.PhoneAccountHandle,%20android.telecom.ConnectionRequest)): Called if an incoming call cannot be handled. Your app should not show a notification and should silently reject the call.
105+
106+
## Implementing [Connection](https://developer.android.com/reference/android/telecom/Connection)
107+
108+
To represent calls in your app, extend the [Connection](https://developer.android.com/reference/android/telecom/Connection) class. When creating a new `Connection` instance to return from your [`ConnectionService`](https://developer.android.com/reference/android/telecom/ConnectionService), make sure to configure these properties:
109+
- Use `Connection#setAddress(Uri, int)` to specify the other party’s identifier. For phone calls, this should be a `PhoneAccount#SCHEME_TEL` URI.
110+
- Set the display name with `Connection#setCallerDisplayName(String, int)`, which will appear on Bluetooth and wearable devices—especially important if no phone number is used.
111+
- Apply `Connection#PROPERTY_SELF_MANAGED` via `Connection#setConnectionProperties(int)` to indicate your app manages the call.
112+
- If your app supports call hold, set `Connection#CAPABILITY_SUPPORT_HOLD` and `Connection#CAPABILITY_HOLD` using `Connection#setConnectionCapabilities(int)` to enable concurrent call scenarios.
113+
- Call `Connection#setAudioModeIsVoip(true)` to inform the platform that the call is VoIP.
114+
- Do not change the call state (e.g., with `Connection#setActive()` or `Connection#setOnHold()`) until the `Connection` has been added to Telecom by returning it from `onCreateOutgoingConnection` or `onCreateIncomingConnection`.
115+
116+
## Hold/Un hold calls
117+
118+
When receiving and answering an external call while the app is already in a call, ConnectionService will notify your app that the call changed to HOLDING state.
119+
120+
When an external call ended by the remote end the user has to manually unhold the call by pressing the unhold call button. If the external call is ended by the user, the app will automatically unhold the call and resume the audio playback.
121+
122+
## Modifying the App for System Managed Calls
123+
124+
To adapt the app so that calls are managed by the system (system managed), follow these steps:
125+
126+
1. **Change the PhoneAccount capability:**
127+
- Replace `PhoneAccount.CAPABILITY_SELF_MANAGED` with `PhoneAccount.CAPABILITY_CALL_PROVIDER` when registering your `PhoneAccount`.
128+
129+
2. **Use standard URI schemes:**
130+
- When placing calls with `TelecomManager.placeCall()`, use `tel:` or `sip:` schemes in the destination URI.
131+
132+
3. **Update ConnectionService address parsing:**
133+
- Ensure your `ConnectionService` implementation correctly parses and handles `tel:` or `sip:` addresses when creating connections.
134+
135+
4. **Activate the PhoneAccount in system settings:**
136+
- The user must manually enable the call account in the system Settings app. You can launch the relevant screen with the following intent:
137+
138+
```java
139+
startActivity(new Intent(TelecomManager.ACTION_CHANGE_PHONE_ACCOUNTS));
140+
```
141+
142+
5. **Call history integration:**
143+
- With system-managed mode, calls made and received through your app will appear in the device’s default phone app call history, just like native calls. This allows users to view, return, or manage these calls directly from the standard phone app, providing a more integrated experience.
144+
145+
With these changes, your app’s calls will be fully integrated and managed by the Android system, providing a native calling experience.
146+
147+
## Troubleshooting
148+
149+
- Ensure connected bluetooth speakers have the call audio profile enabled. If that does not work, try disabling and re-enabling the bluetooth speaker.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
/build
2+
config.gradle
3+
*.jar
4+
*.so
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
plugins {
2+
id 'com.android.application'
3+
}
4+
5+
apply {
6+
from '../../commons.gradle'
7+
}
8+
9+
android {
10+
namespace "com.tokbox.sample.basicvideochatconnectionservice"
11+
compileSdkVersion extCompileSdkVersion
12+
13+
defaultConfig {
14+
applicationId "com.tokbox.sample.basicvideochatconnectionservice"
15+
minSdkVersion extMinSdkVersion
16+
targetSdkVersion extTargetSdkVersion
17+
versionCode extVersionCode
18+
versionName extVersionName
19+
}
20+
21+
buildTypes {
22+
release {
23+
minifyEnabled extMinifyEnabled
24+
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
25+
}
26+
}
27+
28+
compileOptions {
29+
sourceCompatibility JavaVersion.VERSION_17
30+
targetCompatibility JavaVersion.VERSION_17
31+
}
32+
}
33+
34+
dependencies {
35+
implementation "com.opentok.android:opentok-android-sdk:${extOpentokSdkVersion}"
36+
implementation "androidx.appcompat:appcompat:${extAppCompatVersion}"
37+
implementation "androidx.constraintlayout:constraintlayout:${extConstraintLyoutVersion}"
38+
implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.0.0'
39+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
3+
package="com.tokbox.sample.basicvideochatconnectionservice" >
4+
5+
<uses-permission android:name="android.permission.CAMERA" />
6+
<uses-permission android:name="android.permission.INTERNET" />
7+
<uses-permission android:name="android.permission.RECORD_AUDIO" />
8+
<uses-permission android:name="android.permission.WAKE_LOCK" />
9+
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
10+
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
11+
<uses-permission android:name="android.permission.READ_PHONE_NUMBERS" />
12+
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
13+
<uses-permission android:name="android.permission.CALL_PHONE" />
14+
<uses-permission android:name="android.permission.MANAGE_OWN_CALLS" />
15+
<uses-permission android:name="android.permission.BIND_TELECOM_CONNECTION_SERVICE" />
16+
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
17+
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
18+
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
19+
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_PHONE_CALL" />
20+
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" />
21+
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CAMERA" />
22+
23+
<uses-feature android:name="android.hardware.camera" />
24+
<uses-feature android:name="android.hardware.camera.autofocus" />
25+
26+
<application
27+
android:allowBackup="true"
28+
android:icon="@mipmap/ic_launcher"
29+
android:label="@string/app_name"
30+
android:theme="@style/AppTheme" >
31+
<activity
32+
android:name="com.tokbox.sample.basicvideochatconnectionservice.MainActivity"
33+
android:screenOrientation="portrait"
34+
android:label="@string/app_name"
35+
android:exported="true">
36+
<intent-filter>
37+
<action android:name="android.intent.action.MAIN" />
38+
39+
<category android:name="android.intent.category.LAUNCHER" />
40+
</intent-filter>
41+
</activity>
42+
43+
<meta-data
44+
android:name="android.telecom.connectionService"
45+
android:value="com.tokbox.sample.basicvideochatconnectionservice.connectionservice.VonageConnectionService">
46+
</meta-data>
47+
48+
<service
49+
android:name="com.tokbox.sample.basicvideochatconnectionservice.connectionservice.VonageConnectionService"
50+
android:exported="true"
51+
android:permission="android.permission.BIND_TELECOM_CONNECTION_SERVICE"
52+
android:foregroundServiceType="microphone|camera">
53+
<intent-filter>
54+
<action android:name="android.telecom.ConnectionService" />
55+
</intent-filter>
56+
</service>
57+
58+
<receiver android:name="com.tokbox.sample.basicvideochatconnectionservice.CallActionReceiver"
59+
android:exported="true">
60+
<intent-filter>
61+
<action android:name="com.tokbox.sample.basicvideochatconnectionservice.ACTION_ANSWER_CALL" />
62+
<action android:name="com.tokbox.sample.basicvideochatconnectionservice.ACTION_REJECT_CALL" />
63+
<action android:name="com.tokbox.sample.basicvideochatconnectionservice.ACTION_END_CALL" />
64+
</intent-filter>
65+
</receiver>
66+
</application>
67+
68+
</manifest>
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package com.tokbox.sample.basicvideochatconnectionservice;
2+
3+
import android.content.BroadcastReceiver;
4+
import android.content.Context;
5+
import android.content.Intent;
6+
import android.util.Log;
7+
8+
import com.tokbox.sample.basicvideochatconnectionservice.connectionservice.VonageConnection;
9+
import com.tokbox.sample.basicvideochatconnectionservice.connectionservice.VonageConnectionHolder;
10+
11+
public class CallActionReceiver extends BroadcastReceiver {
12+
13+
private static final String TAG = "CallActionReceiver";
14+
15+
public static final String ACTION_ANSWER_CALL = "com.tokbox.sample.basicvideochatconnectionservice.ACTION_ANSWER_CALL";
16+
public static final String ACTION_REJECT_CALL = "com.tokbox.sample.basicvideochatconnectionservice.ACTION_REJECT_CALL";
17+
public static final String ACTION_END_CALL = "com.tokbox.sample.basicvideochatconnectionservice.ACTION_END_CALL";
18+
public static final String ACTION_ANSWERED_CALL = "com.tokbox.sample.basicvideochatconnectionservice.ACTION_ANSWERED_CALL";
19+
public static final String ACTION_INCOMING_CALL = "com.tokbox.sample.basicvideochatconnectionservice.ACTION_INCOMING_CALL";
20+
public static final String ACTION_NOTIFY_INCOMING_CALL = "com.tokbox.sample.basicvideochatconnectionservice.ACTION_NOTIFY_INCOMING_CALL";
21+
public static final String ACTION_REJECTED_CALL = "com.tokbox.sample.basicvideochatconnectionservice.ACTION_REJECTED_CALL";
22+
public static final String ACTION_CALL_ENDED = "com.tokbox.sample.basicvideochatconnectionservice.ACTION_CALL_ENDED";
23+
public static final String ACTION_CALL_HOLDING = "com.tokbox.sample.basicvideochatconnectionservice.ACTION_CALL_HOLDING";
24+
public static final String ACTION_CALL_UNHOLDING = "com.tokbox.sample.basicvideochatconnectionservice.ACTION_CALL_UNHOLDING";
25+
26+
public static final int ACTION_ANSWER_CALL_ID = 2;
27+
public static final int ACTION_REJECT_CALL_ID = 3;
28+
public static final int ACTION_END_CALL_ID = 4;
29+
30+
@Override
31+
public void onReceive(Context context, Intent intent) {
32+
if (intent != null && intent.getAction() != null) {
33+
String action = intent.getAction();
34+
switch (action) {
35+
case ACTION_ANSWER_CALL:
36+
Log.d(TAG, "Action: Answer call");
37+
answerCall();
38+
break;
39+
case ACTION_REJECT_CALL:
40+
Log.d(TAG, "Action: Reject call");
41+
rejectCall();
42+
break;
43+
case ACTION_END_CALL:
44+
Log.d(TAG, "Action: End call");
45+
endCall();
46+
break;
47+
default:
48+
Log.w(TAG, "Unknown action: " + action);
49+
break;
50+
}
51+
}
52+
}
53+
54+
private void answerCall() {
55+
VonageConnection connection = VonageConnectionHolder.getInstance().getConnection();
56+
57+
if (connection != null) {
58+
Log.d(TAG, "Call answered");
59+
connection.onAnswer();
60+
} else {
61+
Log.w(TAG, "No active connection to answer the call");
62+
}
63+
}
64+
65+
private void rejectCall() {
66+
VonageConnection connection = VonageConnectionHolder.getInstance().getConnection();
67+
68+
if (connection != null) {
69+
Log.d(TAG, "Call rejected");
70+
connection.onReject();
71+
} else {
72+
Log.w(TAG, "No active connection to reject the call");
73+
}
74+
}
75+
76+
private void endCall() {
77+
VonageConnection connection = VonageConnectionHolder.getInstance().getConnection();
78+
79+
if (connection != null) {
80+
Log.d(TAG, "Call ended");
81+
connection.onDisconnect();
82+
} else {
83+
Log.w(TAG, "No active connection to end the call");
84+
}
85+
}
86+
}

0 commit comments

Comments
 (0)