Skip to content

Commit 28b2b47

Browse files
authored
Merge pull request #395 from Telzio/feature/android-self-managed
Implemented self managed mode support for Android
2 parents 6323479 + 8b00f5d commit 28b2b47

File tree

9 files changed

+116
-7
lines changed

9 files changed

+116
-7
lines changed

README.md

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,9 @@ Alternative on iOS you can perform setup in `AppDelegate.m`. Doing this allows c
121121
- `additionalPermissions`: [PermissionsAndroid] (optional)
122122
Any additional permissions you'd like your app to have at first launch. Can be used to simplify permission flows and avoid
123123
multiple popups to the user at different times.
124-
124+
- `selfManaged`: boolean (optional)
125+
When set to true, call keep will configure itself to run as a self managed connection service. This is an advanced topic, and it's best to refer to [Googles Documentation](https://developer.android.com/guide/topics/connectivity/telecom/selfManaged) on the matter.
126+
125127
`setup` calls internally `registerPhoneAccount` and `registerEvents`.
126128
127129
## Constants
@@ -688,6 +690,27 @@ RNCallKeep.addEventListener('didLoadWithEvents', (events) => {
688690
- `data`: object
689691
Object with data passed together with specific event so it can be handled in the same way like original event, for example `({ callUUID })` for `answerCall` event if `name` is `RNCallKeepPerformAnswerCallAction`
690692

693+
### - showIncomingCallUi
694+
695+
Android only.
696+
697+
Only when CallKeep is setup to be in self managed mode. Signals that the app must show an incoming call UI. The implementor must either call `displayIncomingCall` from react native or native android code to make this event fire.
698+
699+
```js
700+
RNCallKeep.addEventListener('showIncomingCallUi', ({ handle, callUUID, name }) => {
701+
702+
});
703+
```
704+
705+
The following values will match those initially passed to `displayIncomingCall`
706+
707+
- `handle` (string)
708+
- Phone number of the incoming caller.
709+
- `callUUID` (string)
710+
- The UUID of the call.
711+
- `name` (string)
712+
- Caller Name.
713+
691714
### - checkReachability
692715

693716
On Android when the application is in background, after a certain delay the OS will close every connection with informing about it.

actions.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ const RNCallKeepDidPerformDTMFAction = 'RNCallKeepDidPerformDTMFAction';
1515
const RNCallKeepProviderReset = 'RNCallKeepProviderReset';
1616
const RNCallKeepCheckReachability = 'RNCallKeepCheckReachability';
1717
const RNCallKeepDidLoadWithEvents = 'RNCallKeepDidLoadWithEvents';
18+
const RNCallKeepShowIncomingCallUi = 'RNCallKeepShowIncomingCallUi';
1819
const isIOS = Platform.OS === 'ios';
1920

2021
const didReceiveStartCallAction = handler => {
@@ -59,6 +60,9 @@ const checkReachability = handler =>
5960
const didLoadWithEvents = handler =>
6061
eventEmitter.addListener(RNCallKeepDidLoadWithEvents, handler);
6162

63+
const showIncomingCallUi = handler =>
64+
eventEmitter.addListener(RNCallKeepShowIncomingCallUi, (data) => handler(data));
65+
6266
export const emit = (eventName, payload) => eventEmitter.emit(eventName, payload);
6367

6468
export const listeners = {
@@ -74,4 +78,5 @@ export const listeners = {
7478
didResetProvider,
7579
checkReachability,
7680
didLoadWithEvents,
81+
showIncomingCallUi
7782
};

android/src/main/AndroidManifest.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,6 @@
55
<uses-permission android:name="android.permission.READ_PHONE_STATE"
66
android:maxSdkVersion="29" />
77
<uses-permission android:name="android.permission.READ_PHONE_NUMBERS" />
8+
<uses-permission android:name="android.permission.MANAGE_OWN_CALLS" />
9+
<uses-permission android:name="android.permission.READ_CALL_LOG" />
810
</manifest>

android/src/main/java/io/wazo/callkeep/Constants.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@ public class Constants {
1212
public static final String ACTION_UNHOLD_CALL = "ACTION_UNHOLD_CALL";
1313
public static final String ACTION_UNMUTE_CALL = "ACTION_UNMUTE_CALL";
1414
public static final String ACTION_WAKE_APP = "ACTION_WAKE_APP";
15+
public static final String ACTION_SHOW_INCOMING_CALL_UI = "ACTION_SHOW_INCOMING_CALL_UI";
1516

1617
public static final String EXTRA_CALL_NUMBER = "EXTRA_CALL_NUMBER";
18+
public static final String EXTRA_CALL_NUMBER_SCHEMA = "EXTRA_CALL_NUMBER_SCHEMA";
1719
public static final String EXTRA_CALL_UUID = "EXTRA_CALL_UUID";
1820
public static final String EXTRA_CALLER_NAME = "EXTRA_CALLER_NAME";
1921
// Can't use telecom.EXTRA_DISABLE_ADD_CALL ...

android/src/main/java/io/wazo/callkeep/RNCallKeepModule.java

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@
8585
import static io.wazo.callkeep.Constants.ACTION_AUDIO_SESSION;
8686
import static io.wazo.callkeep.Constants.ACTION_CHECK_REACHABILITY;
8787
import static io.wazo.callkeep.Constants.ACTION_WAKE_APP;
88+
import static io.wazo.callkeep.Constants.ACTION_SHOW_INCOMING_CALL_UI;
8889

8990
// @see https://github.com/kbagchiGWC/voice-quickstart-android/blob/9a2aff7fbe0d0a5ae9457b48e9ad408740dfb968/exampleConnectionService/src/main/java/com/twilio/voice/examples/connectionservice/VoiceConnectionServiceActivity.java
9091
public class RNCallKeepModule extends ReactContextBaseJavaModule {
@@ -93,7 +94,7 @@ public class RNCallKeepModule extends ReactContextBaseJavaModule {
9394

9495
private static final String E_ACTIVITY_DOES_NOT_EXIST = "E_ACTIVITY_DOES_NOT_EXIST";
9596
private static final String REACT_NATIVE_MODULE_NAME = "RNCallKeep";
96-
private static final String[] permissions = {
97+
private static String[] permissions = {
9798
Build.VERSION.SDK_INT < 30 ? Manifest.permission.READ_PHONE_STATE : Manifest.permission.READ_PHONE_NUMBERS,
9899
Manifest.permission.CALL_PHONE,
99100
Manifest.permission.RECORD_AUDIO
@@ -115,6 +116,10 @@ public RNCallKeepModule(ReactApplicationContext reactContext) {
115116
this.reactContext = reactContext;
116117
}
117118

119+
private boolean isSelfManaged() {
120+
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && _settings.hasKey("selfManaged") && _settings.getBoolean("selfManaged");
121+
}
122+
118123
@Override
119124
public String getName() {
120125
return REACT_NATIVE_MODULE_NAME;
@@ -127,6 +132,20 @@ public void setup(ReadableMap options) {
127132
VoiceConnectionService.setInitialized(true);
128133
this._settings = options;
129134

135+
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
136+
if(isSelfManaged()) {
137+
Log.d(TAG, "API Version supports self managed, and is enabled in setup");
138+
}
139+
else {
140+
Log.d(TAG, "API Version supports self managed, but it is not enabled in setup");
141+
}
142+
}
143+
144+
//If we're running in self managed mode we need fewer permissions.
145+
if(isSelfManaged()) {
146+
permissions = new String[]{ Manifest.permission.RECORD_AUDIO };
147+
}
148+
130149
if (isConnectionServiceAvailable()) {
131150
this.registerPhoneAccount();
132151
this.registerEvents();
@@ -196,6 +215,7 @@ public void startCall(String uuid, String number, String callerName) {
196215
Log.d(TAG, "startCall called, uuid: " + uuid + ", number: " + number + ", callerName: " + callerName);
197216

198217
if (!isConnectionServiceAvailable() || !hasPhoneAccount() || !hasPermissions() || number == null) {
218+
Log.d(TAG, "startCall ignored: " + isConnectionServiceAvailable() + ", " + hasPhoneAccount() + ", " + hasPermissions() + ", " + number);
199219
return;
200220
}
201221

@@ -341,7 +361,7 @@ public void reject(String message) {
341361
hasPhoneAccountPromise.resolve(false);
342362
}
343363
});
344-
return;
364+
return;
345365
}
346366

347367
promise.resolve(!hasPhoneAccount());
@@ -606,8 +626,13 @@ private void registerPhoneAccount(Context appContext) {
606626
this.initializeTelecomManager();
607627
String appName = this.getApplicationName(this.getAppContext());
608628

609-
PhoneAccount.Builder builder = new PhoneAccount.Builder(handle, appName)
610-
.setCapabilities(PhoneAccount.CAPABILITY_CALL_PROVIDER);
629+
PhoneAccount.Builder builder = new PhoneAccount.Builder(handle, appName);
630+
if(isSelfManaged()) {
631+
builder.setCapabilities(PhoneAccount.CAPABILITY_SELF_MANAGED);
632+
}
633+
else {
634+
builder.setCapabilities(PhoneAccount.CAPABILITY_CALL_PROVIDER);
635+
}
611636

612637
if (_settings != null && _settings.hasKey("imageName")) {
613638
int identifier = appContext.getResources().getIdentifier(_settings.getString("imageName"), "drawable", appContext.getPackageName());
@@ -669,6 +694,7 @@ private void registerReceiver() {
669694
intentFilter.addAction(ACTION_ONGOING_CALL);
670695
intentFilter.addAction(ACTION_AUDIO_SESSION);
671696
intentFilter.addAction(ACTION_CHECK_REACHABILITY);
697+
intentFilter.addAction(ACTION_SHOW_INCOMING_CALL_UI);
672698
LocalBroadcastManager.getInstance(this.reactContext).registerReceiver(voiceBroadcastReceiver, intentFilter);
673699
isReceiverRegistered = true;
674700
}
@@ -743,6 +769,12 @@ public void onReceive(Context context, Intent intent) {
743769
case ACTION_CHECK_REACHABILITY:
744770
sendEventToJS("RNCallKeepCheckReachability", null);
745771
break;
772+
case ACTION_SHOW_INCOMING_CALL_UI:
773+
args.putString("handle", attributeMap.get(EXTRA_CALL_NUMBER));
774+
args.putString("callUUID", attributeMap.get(EXTRA_CALL_UUID));
775+
args.putString("name", attributeMap.get(EXTRA_CALLER_NAME));
776+
sendEventToJS("RNCallKeepShowIncomingCallUi", args);
777+
break;
746778
case ACTION_WAKE_APP:
747779
Intent headlessIntent = new Intent(reactContext, RNCallKeepBackgroundMessagingService.class);
748780
headlessIntent.putExtra("callUUID", attributeMap.get(EXTRA_CALL_UUID));

android/src/main/java/io/wazo/callkeep/VoiceConnection.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
import static io.wazo.callkeep.Constants.EXTRA_CALLER_NAME;
4949
import static io.wazo.callkeep.Constants.EXTRA_CALL_NUMBER;
5050
import static io.wazo.callkeep.Constants.EXTRA_CALL_UUID;
51+
import static io.wazo.callkeep.Constants.ACTION_SHOW_INCOMING_CALL_UI;
5152

5253
@TargetApi(Build.VERSION_CODES.M)
5354
public class VoiceConnection extends Connection {
@@ -196,6 +197,12 @@ public void onReject() {
196197
destroy();
197198
}
198199

200+
@Override
201+
public void onShowIncomingCallUi() {
202+
Log.d(TAG, "onShowIncomingCallUi()");
203+
sendCallRequestToActivity(ACTION_SHOW_INCOMING_CALL_UI, handle);
204+
}
205+
199206
/*
200207
* Send call request to the RNCallKeepModule
201208
*/

android/src/main/java/io/wazo/callkeep/VoiceConnectionService.java

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
import android.telecom.ConnectionRequest;
4141
import android.telecom.ConnectionService;
4242
import android.telecom.DisconnectCause;
43+
import android.telecom.PhoneAccount;
4344
import android.telecom.PhoneAccountHandle;
4445
import android.telecom.TelecomManager;
4546
import android.util.Log;
@@ -62,6 +63,7 @@
6263
import static io.wazo.callkeep.Constants.ACTION_WAKE_APP;
6364
import static io.wazo.callkeep.Constants.EXTRA_CALLER_NAME;
6465
import static io.wazo.callkeep.Constants.EXTRA_CALL_NUMBER;
66+
import static io.wazo.callkeep.Constants.EXTRA_CALL_NUMBER_SCHEMA;
6567
import static io.wazo.callkeep.Constants.EXTRA_CALL_UUID;
6668
import static io.wazo.callkeep.Constants.EXTRA_DISABLE_ADD_CALL;
6769
import static io.wazo.callkeep.Constants.FOREGROUND_SERVICE_TYPE_MICROPHONE;
@@ -328,9 +330,39 @@ private Connection createConnection(ConnectionRequest request) {
328330

329331
Bundle extras = request.getExtras();
330332
HashMap<String, String> extrasMap = this.bundleToMap(extras);
331-
extrasMap.put(EXTRA_CALL_NUMBER, request.getAddress().toString());
333+
334+
String callerNumber = request.getAddress().toString();
335+
if (callerNumber.contains(":")) {
336+
//CallerNumber contains a schema which we'll separate out
337+
int schemaIndex = callerNumber.indexOf(":");
338+
String number = callerNumber.substring(schemaIndex + 1);
339+
String schema = callerNumber.substring(0, schemaIndex);
340+
341+
extrasMap.put(EXTRA_CALL_NUMBER, number);
342+
extrasMap.put(EXTRA_CALL_NUMBER_SCHEMA, schema);
343+
}
344+
else {
345+
extrasMap.put(EXTRA_CALL_NUMBER, callerNumber);
346+
}
347+
332348
VoiceConnection connection = new VoiceConnection(this, extrasMap);
333349
connection.setConnectionCapabilities(Connection.CAPABILITY_MUTE | Connection.CAPABILITY_SUPPORT_HOLD);
350+
351+
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
352+
Context context = getApplicationContext();
353+
TelecomManager telecomManager = (TelecomManager) context.getSystemService(context.TELECOM_SERVICE);
354+
PhoneAccount phoneAccount = telecomManager.getPhoneAccount(request.getAccountHandle());
355+
356+
//If the phone account is self managed, then this connection must also be self managed.
357+
if((phoneAccount.getCapabilities() & PhoneAccount.CAPABILITY_SELF_MANAGED) == PhoneAccount.CAPABILITY_SELF_MANAGED) {
358+
Log.d(TAG, "PhoneAccount is SELF_MANAGED, so connection will be too");
359+
connection.setConnectionProperties(Connection.PROPERTY_SELF_MANAGED);
360+
}
361+
else {
362+
Log.d(TAG, "PhoneAccount is not SELF_MANAGED, so connection won't be either");
363+
}
364+
}
365+
334366
connection.setInitializing();
335367
connection.setExtras(extras);
336368
currentConnections.put(extras.getString(EXTRA_CALL_UUID), connection);

index.d.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ declare module 'react-native-callkeep' {
1111
'didResetProvider' |
1212
'checkReachability' |
1313
'didPerformSetMutedCallAction' |
14-
'didLoadWithEvents';
14+
'didLoadWithEvents' |
15+
'showIncomingCallUi';
1516

1617
type HandleType = 'generic' | 'number' | 'email';
1718

@@ -32,6 +33,7 @@ declare module 'react-native-callkeep' {
3233
okButton: string,
3334
imageName?: string,
3435
additionalPermissions: string[],
36+
selfManaged?: boolean,
3537
foregroundService?: {
3638
channelId: string,
3739
channelName: string,

index.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,10 @@ class RNCallKeep {
274274
_setupAndroid = async (options) => {
275275
RNCallKeepModule.setup(options);
276276

277+
if (options.selfManaged) {
278+
return false;
279+
}
280+
277281
const showAccountAlert = await RNCallKeepModule.checkPhoneAccountPermission(options.additionalPermissions || []);
278282
const shouldOpenAccounts = await this._alert(options, showAccountAlert);
279283

0 commit comments

Comments
 (0)