Skip to content

Commit a0e6d3b

Browse files
authored
fix(voip): Phone account creation (#7170)
1 parent 62cb8bd commit a0e6d3b

5 files changed

Lines changed: 104 additions & 16 deletions

File tree

android/app/src/main/AndroidManifest.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@
138138
android:label="Wazo"
139139
android:permission="android.permission.BIND_TELECOM_CONNECTION_SERVICE"
140140
android:exported="true"
141-
android:foregroundServiceType="microphone"
141+
android:foregroundServiceType="microphone|phoneCall"
142142
>
143143
<intent-filter>
144144
<action android:name="android.telecom.ConnectionService" />

android/app/src/main/java/chat/rocket/reactnative/voip/VoipNotification.kt

Lines changed: 36 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -663,8 +663,8 @@ class VoipNotification(private val context: Context) {
663663
private fun registerCallWithTelecomManager(callId: String, caller: String) {
664664
try {
665665
// Validate inputs
666-
if (callId.isNullOrEmpty() || caller.isNullOrEmpty()) {
667-
Log.e(TAG, "Cannot register call with TelecomManager: callId is null or empty")
666+
if (callId.isEmpty() || caller.isEmpty()) {
667+
Log.e(TAG, "Cannot register call with TelecomManager: callId='$callId' caller='$caller' — empty values rejected")
668668
return
669669
}
670670

@@ -674,17 +674,17 @@ class VoipNotification(private val context: Context) {
674674
return
675675
}
676676

677-
// Get react-native-callkeep's PhoneAccountHandle
677+
// Build the PhoneAccountHandle using the same (ComponentName, packageName) pair
678+
// that react-native-callkeep uses (see patches/react-native-callkeep+4.3.16.patch)
679+
// so the JS-side and native-side PhoneAccount registrations share one handle.
680+
// The ID must be locale-stable; the localized label is only used for display below.
678681
val componentName = ComponentName(context.packageName, CALLKEEP_CONNECTION_SERVICE_CLASS)
679-
// react-native-callkeep typically uses the app package name as the account ID
680682
val phoneAccountHandle = PhoneAccountHandle(componentName, context.packageName)
681683

682-
// Check if PhoneAccount is registered
683-
val phoneAccount = telecomManager.getPhoneAccount(phoneAccountHandle)
684-
if (phoneAccount == null) {
685-
Log.w(TAG, "PhoneAccount not registered by react-native-callkeep yet. Call may not have full OS integration.")
686-
return
687-
}
684+
// Ensure the self-managed PhoneAccount is registered. FCM pushes can arrive before
685+
// JS boots and calls RNCallKeep.setup, so we must register from native too.
686+
// registerPhoneAccount is idempotent for the same handle.
687+
ensureSelfManagedPhoneAccountRegistered(telecomManager, phoneAccountHandle, getApplicationLabel())
688688

689689
// Create extras for the incoming call
690690
val extras = Bundle().apply {
@@ -702,12 +702,37 @@ class VoipNotification(private val context: Context) {
702702
telecomManager.addNewIncomingCall(phoneAccountHandle, extras)
703703
Log.d(TAG, "Successfully registered incoming call with TelecomManager: $callId")
704704
} catch (e: SecurityException) {
705-
Log.e(TAG, "SecurityException registering call with TelecomManager. MANAGE_OWN_CALLS permission may be missing.", e)
705+
Log.e(TAG, "SecurityException registering call with TelecomManager. Check MANAGE_OWN_CALLS/READ_PHONE_STATE grants and PhoneAccount registration.", e)
706706
} catch (e: Exception) {
707707
Log.e(TAG, "Failed to register call with TelecomManager", e)
708708
}
709709
}
710710

711+
private fun getApplicationLabel(): String {
712+
val applicationInfo = context.applicationInfo
713+
val stringId = applicationInfo.labelRes
714+
return if (stringId == 0) applicationInfo.nonLocalizedLabel?.toString() ?: context.packageName
715+
else context.getString(stringId)
716+
}
717+
718+
private fun ensureSelfManagedPhoneAccountRegistered(
719+
telecomManager: TelecomManager,
720+
handle: PhoneAccountHandle,
721+
label: String
722+
) {
723+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
724+
try {
725+
val account = PhoneAccount.builder(handle, label)
726+
.setCapabilities(PhoneAccount.CAPABILITY_SELF_MANAGED)
727+
.build()
728+
telecomManager.registerPhoneAccount(account)
729+
} catch (e: SecurityException) {
730+
Log.e(TAG, "SecurityException registering PhoneAccount. MANAGE_OWN_CALLS may be denied.", e)
731+
} catch (e: Exception) {
732+
Log.e(TAG, "Failed to register self-managed PhoneAccount", e)
733+
}
734+
}
735+
711736
/**
712737
* Shows incoming call notification with full-screen intent for locked devices
713738
* and heads-up notification for unlocked devices.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@
171171
"@types/ejson": "^2.1.3",
172172
"@types/i18n-js": "3.8.3",
173173
"@types/invariant": "2.2.37",
174-
"@types/jest": "^29.5.13",
174+
"@types/jest": "29.5.14",
175175
"@types/jsrsasign": "^10.5.8",
176176
"@types/lodash": "^4.14.188",
177177
"@types/react": "~19.1.0",

patches/react-native-callkeep+4.3.16.patch

Lines changed: 65 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,16 @@
11
diff --git a/node_modules/react-native-callkeep/android/src/main/java/io/wazo/callkeep/RNCallKeepModule.java b/node_modules/react-native-callkeep/android/src/main/java/io/wazo/callkeep/RNCallKeepModule.java
2-
index 025480a..70a1305 100644
2+
index 025480a..6a66858 100644
33
--- a/node_modules/react-native-callkeep/android/src/main/java/io/wazo/callkeep/RNCallKeepModule.java
44
+++ b/node_modules/react-native-callkeep/android/src/main/java/io/wazo/callkeep/RNCallKeepModule.java
5+
@@ -221,7 +221,7 @@ public class RNCallKeepModule extends ReactContextBaseJavaModule implements Life
6+
ComponentName cName = new ComponentName(context, VoiceConnectionService.class);
7+
String appName = this.getApplicationName(context);
8+
9+
- handle = new PhoneAccountHandle(cName, appName);
10+
+ handle = new PhoneAccountHandle(cName, context.getPackageName());
11+
telecomManager = (TelecomManager) context.getSystemService(Context.TELECOM_SERVICE);
12+
}
13+
514
@@ -434,11 +434,6 @@ public class RNCallKeepModule extends ReactContextBaseJavaModule implements Life
615
this.hasListeners = false;
716
}
@@ -26,11 +35,65 @@ index 025480a..70a1305 100644
2635
@ReactMethod
2736
public void startCall(String uuid, String number, String callerName, boolean hasVideo) {
2837
this.startCall(uuid, number, callerName, hasVideo, null);
38+
@@ -1196,9 +1186,14 @@ public class RNCallKeepModule extends ReactContextBaseJavaModule implements Life
39+
return true;
40+
}
41+
42+
- return isConnectionServiceAvailable() && telecomManager != null &&
43+
- hasPermissions() && telecomManager.getPhoneAccount(handle) != null &&
44+
- telecomManager.getPhoneAccount(handle).isEnabled();
45+
+ try {
46+
+ return isConnectionServiceAvailable() && telecomManager != null &&
47+
+ hasPermissions() && telecomManager.getPhoneAccount(handle) != null &&
48+
+ telecomManager.getPhoneAccount(handle).isEnabled();
49+
+ } catch (SecurityException e) {
50+
+ Log.w(TAG, "[RNCallKeepModule] hasPhoneAccount: SecurityException querying phone account", e);
51+
+ return false;
52+
+ }
53+
}
54+
55+
protected void registerReceiver() {
56+
diff --git a/node_modules/react-native-callkeep/android/src/main/java/io/wazo/callkeep/VoiceConnectionService.java b/node_modules/react-native-callkeep/android/src/main/java/io/wazo/callkeep/VoiceConnectionService.java
57+
index 5d58692..f4e4b1b 100644
58+
--- a/node_modules/react-native-callkeep/android/src/main/java/io/wazo/callkeep/VoiceConnectionService.java
59+
+++ b/node_modules/react-native-callkeep/android/src/main/java/io/wazo/callkeep/VoiceConnectionService.java
60+
@@ -317,7 +317,7 @@ public class VoiceConnectionService extends ConnectionService {
61+
notificationBuilder.setOngoing(true)
62+
.setContentTitle(foregroundSettings.getString("notificationTitle"))
63+
.setPriority(NotificationManager.IMPORTANCE_MIN)
64+
.setCategory(Notification.CATEGORY_SERVICE);
65+
66+
- Activity currentActivity = RNCallKeepModule.instance.getCurrentReactActivity();
67+
+ Activity currentActivity = RNCallKeepModule.instance != null ? RNCallKeepModule.instance.getCurrentReactActivity() : null;
68+
if (currentActivity != null) {
69+
@@ -459,18 +459,10 @@ public class VoiceConnectionService extends ConnectionService {
70+
connection.setConnectionCapabilities(Connection.CAPABILITY_MUTE | Connection.CAPABILITY_SUPPORT_HOLD);
71+
72+
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
73+
- Context context = getApplicationContext();
74+
- TelecomManager telecomManager = (TelecomManager) context.getSystemService(context.TELECOM_SERVICE);
75+
- PhoneAccount phoneAccount = telecomManager.getPhoneAccount(request.getAccountHandle());
76+
-
77+
- //If the phone account is self managed, then this connection must also be self managed.
78+
- if((phoneAccount.getCapabilities() & PhoneAccount.CAPABILITY_SELF_MANAGED) == PhoneAccount.CAPABILITY_SELF_MANAGED) {
79+
- Log.d(TAG, "[VoiceConnectionService] PhoneAccount is SELF_MANAGED, so connection will be too");
80+
- connection.setConnectionProperties(Connection.PROPERTY_SELF_MANAGED);
81+
- }
82+
- else {
83+
- Log.d(TAG, "[VoiceConnectionService] PhoneAccount is not SELF_MANAGED, so connection won't be either");
84+
- }
85+
+ // This app always registers its PhoneAccount with CAPABILITY_SELF_MANAGED.
86+
+ // Skip telecomManager.getPhoneAccount(...) which requires READ_PHONE_NUMBERS on
87+
+ // Android 11+ and isn't grantable from an FCM background context.
88+
+ connection.setConnectionProperties(Connection.PROPERTY_SELF_MANAGED);
89+
}
90+
91+
connection.setInitializing();
2992
diff --git a/node_modules/react-native-callkeep/ios/RNCallKeep/RNCallKeep.m b/node_modules/react-native-callkeep/ios/RNCallKeep/RNCallKeep.m
3093
index 786045f..a989d58 100644
3194
--- a/node_modules/react-native-callkeep/ios/RNCallKeep/RNCallKeep.m
3295
+++ b/node_modules/react-native-callkeep/ios/RNCallKeep/RNCallKeep.m
33-
@@ -171,6 +171,8 @@ - (void)sendEventWithNameWrapper:(NSString *)name body:(id)body {
96+
@@ -171,6 +171,8 @@ RCT_EXPORT_MODULE()
3497
body, @"data",
3598
nil
3699
];

yarn.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4591,7 +4591,7 @@
45914591
dependencies:
45924592
"@types/istanbul-lib-report" "*"
45934593

4594-
"@types/jest@^29.5.13":
4594+
"@types/jest@29.5.14":
45954595
version "29.5.14"
45964596
resolved "https://registry.yarnpkg.com/@types/jest/-/jest-29.5.14.tgz#2b910912fa1d6856cadcd0c1f95af7df1d6049e5"
45974597
integrity sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==

0 commit comments

Comments
 (0)