Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
9a94073
Implementation of Incoming calls
goncalocostamendes May 7, 2025
24b7f99
Self manage connection service working e2e
goncalocostamendes May 9, 2025
a054be7
Refactor of sample
goncalocostamendes May 9, 2025
49d0662
onCreateIncomingConnection() not being triggere
goncalocostamendes May 12, 2025
ebfad12
All functionality working
goncalocostamendes May 12, 2025
304faab
Remove all hardcoded values
goncalocostamendes May 12, 2025
7f1f5d1
Add info regarding how to implement ConnectionService
goncalocostamendes May 14, 2025
b29e488
Simulate call
goncalocostamendes May 15, 2025
00c8a4b
Migrate away from easypermissions
goncalocostamendes May 15, 2025
0ad40c4
Add FCM docs
goncalocostamendes May 16, 2025
5db451a
Add brief descrioption of sample
goncalocostamendes May 16, 2025
7b347ba
Use ExecutorService
goncalocostamendes May 19, 2025
c04d1ff
Add changes to audio device
goncalocostamendes May 20, 2025
48b3bfb
Remove request audio focus
goncalocostamendes May 20, 2025
4e60066
Mention how to retrieve google cloud token
goncalocostamendes May 21, 2025
4e4c3f6
Advances in ConnectionService sample
VZaphod Jun 13, 2025
9a4a2ca
Added answer and reject intents
VZaphod Jun 13, 2025
8188707
Support for foreground services microphone and camera
VZaphod Jun 13, 2025
c587d16
Removed posting notifications and requesting session credentials
VZaphod Jun 16, 2025
4973148
Organized some files
VZaphod Jun 16, 2025
7ccafda
Renamed connection service sample project folder
VZaphod Jun 16, 2025
8e616d6
Organized files
VZaphod Jun 16, 2025
e722928
Removed connection field from `VonageManager`
VZaphod Jun 16, 2025
2164be6
Remove subscriber when call ends
VZaphod Jun 16, 2025
f497f7c
Passing name and id arguments to the connection
VZaphod Jun 16, 2025
ab1dcc2
Removed duplicated `CallActionReceiver`
VZaphod Jun 16, 2025
d883a17
Code cleanup
VZaphod Jun 17, 2025
0624b1f
Removed FCM
VZaphod Jun 17, 2025
27ac807
Can place call checks
VZaphod Jun 17, 2025
ed1d2e8
Adjusted layout outlet margins
VZaphod Jun 17, 2025
407e31e
Updated readme
VZaphod Jun 17, 2025
e903e12
Reformulated readme
VZaphod Jun 17, 2025
4f551b7
Fixed readme identation
VZaphod Jun 17, 2025
703d18a
Updated Readme with system managed section
VZaphod Jun 18, 2025
0a3ebe4
Added `Audio Focus Management` section explaining how to enable the m…
VZaphod Jun 18, 2025
3b95cad
On unhold button
VZaphod Jun 19, 2025
f3629cc
Moved hold button
VZaphod Jun 19, 2025
03e4d34
improve audio focus management
manolovn Jun 19, 2025
4c5e394
Added hold management to readme
VZaphod Jun 20, 2025
422b423
Added `TelecomManager.ACTION_CHANGE_PHONE_ACCOUNTS` for easier debugging
VZaphod Jun 20, 2025
890c84a
Show and hide unhold button and holding state
VZaphod Jun 23, 2025
5dd80bc
Removed unused request audio focus
VZaphod Jun 23, 2025
7e6d125
More specific unhold behaviour change in readme
VZaphod Jun 23, 2025
08f4fdf
remove publish video when muted
manolovn Jun 23, 2025
97eba49
remove not needed removeCallNotification method
manolovn Jun 23, 2025
3bd71b7
add version to core-ktx in phone call detection kotlin project
manolovn Jun 23, 2025
9ddc95c
add basic video chat connectionservice workflow
manolovn Jun 23, 2025
807d171
updated basic video chat connectionservice workflow
manolovn Jun 23, 2025
d6d31db
changed package name to avoid inconsistencies
manolovn Jun 23, 2025
383c806
fixing inconsistency errors
manolovn Jun 23, 2025
c259b7e
Merge branch 'main' into VIDCS-3685
manolovn Jun 23, 2025
32aa27b
uncomment lib dependency
manolovn Jun 23, 2025
fab249b
vonage connection cleanup
manolovn Jun 24, 2025
1aa4fe0
add call style notification
manolovn Jun 24, 2025
60062a8
Added `Troubleshooting` section for bluetooth devices
VZaphod Jun 24, 2025
4e2f595
Updated troubleshoting section
VZaphod Jun 24, 2025
6a23b98
Make sure user is notified if credentials not hardcoded
goncalocostamendes Jun 26, 2025
74ce275
Add bluetooth permissions
goncalocostamendes Jun 26, 2025
6b4ee5f
Remove google-services.json file
goncalocostamendes Jun 26, 2025
a3e139f
Merge remote-tracking branch 'origin/main' into VIDCS-3685
goncalocostamendes Sep 30, 2025
09b5061
Remove google services dependencies
goncalocostamendes Sep 30, 2025
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
name: Build Basic-Video-Chat-ConnectionService-Java

on:
push:
branches: [main] # Just in case main was not up to date while merging PR
pull_request:
types: [opened, synchronize]

jobs:
run:
continue-on-error: true
runs-on: ubuntu-latest
strategy:
fail-fast: false
steps:
- name: checkout
uses: actions/checkout@v2

- name: Set up JDK
uses: actions/setup-java@v1
with:
java-version: 17

- name: Build
run: cd Basic-Video-Chat-ConnectionService-Java && ./gradlew app:assembleRelease && cd ..

15 changes: 15 additions & 0 deletions Basic-Video-Chat-ConnectionService-Java/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# intellij
*.iml

.gradle
/local.properties
/.idea/workspace.xml
/.idea/libraries
.DS_Store
/build
/captures
.externalNativeBuild
app/build

.settings/
app/jniLibs/
149 changes: 149 additions & 0 deletions Basic-Video-Chat-ConnectionService-Java/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
# Basic Video Chat with `ConnectionService`

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.

## Features

- Outgoing and incoming VoIP calls with fullscreen call UIs
- Integration with Android Telecom API for call and notification management
- Real-time video sessions using OpenTok
- Audio device selection and call hold capabilities
- Self-managed calls for a streamlined calling experience

## How It Works

The app customizes Android’s ConnectionService and Connection classes to:
- Place outgoing calls through a tailored PhoneAccount.
- Report incoming calls.
- Manage call state changes, notifications, foreground execution handling and audio routing.
- Hardcoded local OpenTok credentials.

## Configuration

1. **API Credentials:**
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.
In a production setup, these values should be provided from a secure server.

2. **PhoneAccount:**
The `PhoneAccountManager` registers the PhoneAccount necessary to interface with the Telecom API.

3. **Manifest Setup:**
Verify that `AndroidManifest.xml` includes all necessary permissions such as `CAMERA`, `RECORD_AUDIO`, `FOREGROUND_SERVICE`, `MANAGE_OWN_CALLS`, and `BIND_TELECOM_CONNECTION_SERVICE`.

Register the service in `AndroidManifest.xml` file.

```xml
<service
android:name=".connectionservice.VonageConnectionService"
android:exported="true"
android:permission="android.permission.BIND_TELECOM_CONNECTION_SERVICE"
android:foregroundServiceType="microphone|camera">
<intent-filter>
<action android:name="android.telecom.ConnectionService" />
</intent-filter>
</service>
```

4. **Audio Focus Management:**
When using ConnectionService, you need to configure the SDK to delegate audio focus control to your app:

```java
private AudioDeviceManager audioDeviceManager;
private BaseAudioDevice.AudioFocusManager audioFocusManager;

public void setupAudioFocusManager(Context context) {
audioDeviceManager = new AudioDeviceManager(context);
audioFocusManager = audioDeviceManager.getAudioFocusManager();

audioFocusManager.setRequestAudioFocus(false);
}

public void notifyAudioFocusIsActive() {
audioFocusManager.audioFocusActivated();
}

public void notifyAudioFocusIsInactive() {
audioFocusManager.audioFocusDeactivated();
}
```

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.

This delegation ensures proper audio routing and coordination with the Android Telecom system.


## Requirements

- Android API Level 26 or higher is recommended.
- Ensure proper runtime permissions are granted for camera, audio recording, and foreground service usage.

## Overview

A `ConnectionService` allows apps to manage VoIP or phone calls, whether they need to integrate with
the system dialer (system managed) or operate independently (self managed). By implementing this
service, a VoIP app can leverage Android’s Telecom APIs to provide features such as call switching,
unified audio route management, Bluetooth device support, and integration with companion devices
like smartwatches. This ensures calls are accessible and controllable across different devices, with
consistent user experiences for actions like answering, rejecting, or ending calls.

## TelecomManager and PhoneAccount

To initiate phone or VoIP calls, `TelecomManager` relies on a registered `PhoneAccount`. Register
your app’s `PhoneAccount` using `TelecomManager.registerPhoneAccount()`, and assign it the
`CAPABILITY_SELF_MANAGED` capability. This signals that your app will handle the connection logic
and call management independently.

## Implementing ConnectionService

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

Your implementation should override the following `ConnectionService` methods:
- [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.
- [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.
- [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.
- [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.

## Implementing [Connection](https://developer.android.com/reference/android/telecom/Connection)

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:
- Use `Connection#setAddress(Uri, int)` to specify the other party’s identifier. For phone calls, this should be a `PhoneAccount#SCHEME_TEL` URI.
- 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.
- Apply `Connection#PROPERTY_SELF_MANAGED` via `Connection#setConnectionProperties(int)` to indicate your app manages the call.
- If your app supports call hold, set `Connection#CAPABILITY_SUPPORT_HOLD` and `Connection#CAPABILITY_HOLD` using `Connection#setConnectionCapabilities(int)` to enable concurrent call scenarios.
- Call `Connection#setAudioModeIsVoip(true)` to inform the platform that the call is VoIP.
- 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`.

## Hold/Un hold calls

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.

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.

## Modifying the App for System Managed Calls

To adapt the app so that calls are managed by the system (system managed), follow these steps:

1. **Change the PhoneAccount capability:**
- Replace `PhoneAccount.CAPABILITY_SELF_MANAGED` with `PhoneAccount.CAPABILITY_CALL_PROVIDER` when registering your `PhoneAccount`.

2. **Use standard URI schemes:**
- When placing calls with `TelecomManager.placeCall()`, use `tel:` or `sip:` schemes in the destination URI.

3. **Update ConnectionService address parsing:**
- Ensure your `ConnectionService` implementation correctly parses and handles `tel:` or `sip:` addresses when creating connections.

4. **Activate the PhoneAccount in system settings:**
- The user must manually enable the call account in the system Settings app. You can launch the relevant screen with the following intent:

```java
startActivity(new Intent(TelecomManager.ACTION_CHANGE_PHONE_ACCOUNTS));
```

5. **Call history integration:**
- 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.

With these changes, your app’s calls will be fully integrated and managed by the Android system, providing a native calling experience.

## Troubleshooting

- Ensure connected bluetooth speakers have the call audio profile enabled. If that does not work, try disabling and re-enabling the bluetooth speaker.
4 changes: 4 additions & 0 deletions Basic-Video-Chat-ConnectionService-Java/app/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/build
config.gradle
*.jar
*.so
39 changes: 39 additions & 0 deletions Basic-Video-Chat-ConnectionService-Java/app/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
plugins {
id 'com.android.application'
}

apply {
from '../../commons.gradle'
}

android {
namespace "com.tokbox.sample.basicvideochatconnectionservice"
compileSdkVersion extCompileSdkVersion

defaultConfig {
applicationId "com.tokbox.sample.basicvideochatconnectionservice"
minSdkVersion extMinSdkVersion
targetSdkVersion extTargetSdkVersion
versionCode extVersionCode
versionName extVersionName
}

buildTypes {
release {
minifyEnabled extMinifyEnabled
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}

compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
}

dependencies {
implementation "com.opentok.android:opentok-android-sdk:${extOpentokSdkVersion}"
implementation "androidx.appcompat:appcompat:${extAppCompatVersion}"
implementation "androidx.constraintlayout:constraintlayout:${extConstraintLyoutVersion}"
implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.0.0'
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.tokbox.sample.basicvideochatconnectionservice" >

<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<uses-permission android:name="android.permission.READ_PHONE_NUMBERS" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.CALL_PHONE" />
<uses-permission android:name="android.permission.MANAGE_OWN_CALLS" />
<uses-permission android:name="android.permission.BIND_TELECOM_CONNECTION_SERVICE" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_PHONE_CALL" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CAMERA" />

<uses-feature android:name="android.hardware.camera" />
<uses-feature android:name="android.hardware.camera.autofocus" />

<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme" >
<activity
android:name="com.tokbox.sample.basicvideochatconnectionservice.MainActivity"
android:screenOrientation="portrait"
android:label="@string/app_name"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>

<meta-data
android:name="android.telecom.connectionService"
android:value="com.tokbox.sample.basicvideochatconnectionservice.connectionservice.VonageConnectionService">
</meta-data>

<service
android:name="com.tokbox.sample.basicvideochatconnectionservice.connectionservice.VonageConnectionService"
android:exported="true"
android:permission="android.permission.BIND_TELECOM_CONNECTION_SERVICE"
android:foregroundServiceType="microphone|camera">
<intent-filter>
<action android:name="android.telecom.ConnectionService" />
</intent-filter>
</service>

<receiver android:name="com.tokbox.sample.basicvideochatconnectionservice.CallActionReceiver"
android:exported="true">
<intent-filter>
<action android:name="com.tokbox.sample.basicvideochatconnectionservice.ACTION_ANSWER_CALL" />
<action android:name="com.tokbox.sample.basicvideochatconnectionservice.ACTION_REJECT_CALL" />
<action android:name="com.tokbox.sample.basicvideochatconnectionservice.ACTION_END_CALL" />
</intent-filter>
</receiver>
</application>

</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package com.tokbox.sample.basicvideochatconnectionservice;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.util.Log;

import com.tokbox.sample.basicvideochatconnectionservice.connectionservice.VonageConnection;
import com.tokbox.sample.basicvideochatconnectionservice.connectionservice.VonageConnectionHolder;

public class CallActionReceiver extends BroadcastReceiver {

private static final String TAG = "CallActionReceiver";

public static final String ACTION_ANSWER_CALL = "com.tokbox.sample.basicvideochatconnectionservice.ACTION_ANSWER_CALL";
public static final String ACTION_REJECT_CALL = "com.tokbox.sample.basicvideochatconnectionservice.ACTION_REJECT_CALL";
public static final String ACTION_END_CALL = "com.tokbox.sample.basicvideochatconnectionservice.ACTION_END_CALL";
public static final String ACTION_ANSWERED_CALL = "com.tokbox.sample.basicvideochatconnectionservice.ACTION_ANSWERED_CALL";
public static final String ACTION_INCOMING_CALL = "com.tokbox.sample.basicvideochatconnectionservice.ACTION_INCOMING_CALL";
public static final String ACTION_NOTIFY_INCOMING_CALL = "com.tokbox.sample.basicvideochatconnectionservice.ACTION_NOTIFY_INCOMING_CALL";
public static final String ACTION_REJECTED_CALL = "com.tokbox.sample.basicvideochatconnectionservice.ACTION_REJECTED_CALL";
public static final String ACTION_CALL_ENDED = "com.tokbox.sample.basicvideochatconnectionservice.ACTION_CALL_ENDED";
public static final String ACTION_CALL_HOLDING = "com.tokbox.sample.basicvideochatconnectionservice.ACTION_CALL_HOLDING";
public static final String ACTION_CALL_UNHOLDING = "com.tokbox.sample.basicvideochatconnectionservice.ACTION_CALL_UNHOLDING";

public static final int ACTION_ANSWER_CALL_ID = 2;
public static final int ACTION_REJECT_CALL_ID = 3;
public static final int ACTION_END_CALL_ID = 4;

@Override
public void onReceive(Context context, Intent intent) {
if (intent != null && intent.getAction() != null) {
String action = intent.getAction();
switch (action) {
case ACTION_ANSWER_CALL:
Log.d(TAG, "Action: Answer call");
answerCall();
break;
case ACTION_REJECT_CALL:
Log.d(TAG, "Action: Reject call");
rejectCall();
break;
case ACTION_END_CALL:
Log.d(TAG, "Action: End call");
endCall();
break;
default:
Log.w(TAG, "Unknown action: " + action);
break;
}
}
}

private void answerCall() {
VonageConnection connection = VonageConnectionHolder.getInstance().getConnection();

if (connection != null) {
Log.d(TAG, "Call answered");
connection.onAnswer();
} else {
Log.w(TAG, "No active connection to answer the call");
}
}

private void rejectCall() {
VonageConnection connection = VonageConnectionHolder.getInstance().getConnection();

if (connection != null) {
Log.d(TAG, "Call rejected");
connection.onReject();
} else {
Log.w(TAG, "No active connection to reject the call");
}
}

private void endCall() {
VonageConnection connection = VonageConnectionHolder.getInstance().getConnection();

if (connection != null) {
Log.d(TAG, "Call ended");
connection.onDisconnect();
} else {
Log.w(TAG, "No active connection to end the call");
}
}
}
Loading