Skip to content

Commit faed2d3

Browse files
fix(android): remove isRunning guard + add double-tap guard on Accept/Decline (#7215)
* merge feat.voip-lib * feat(voip): enhance call handling with UUID mapping and event listeners * Base call UI * feat(voip): integrate Zustand for call state management and enhance CallView UI * feat(voip): add simulateCall function for mock call handling in UI development * refactor(CallView): update button handlers and improve UI responsiveness * Add pause-shape-unfilled icon * Base CallHeader * toggleFocus * collapse buttons * Header components * Hide header when no call * Timer * Add use memo * Add voice call item on sidebar * cleanup * Temp use @rocket.chat/media-signaling from .tgz * cleanup * Check module and permissions to enable voip * Refactor stop method to use optional chaining for media signal listeners * voip push first test * Add VoIP call handling with pending call management - Implemented VoIP push notification handling in index.js, including storing call info for later processing. - Added CallKeep event handlers for answering and ending calls from a cold start. - Introduced a new CallIdUUID module to convert call IDs to deterministic UUIDs for compatibility with CallKit. - Created a pending call store to manage incoming calls when the app is not fully initialized. - Updated deep linking actions to include VoIP call handling. - Enhanced MediaSessionInstance to process pending calls and manage call states effectively. * Remove pending store and create getInitialEvents on app/index * Attempt to make iOS calls work from cold state * lint and format * Patch callkeep ios * Temp send iOS voip push token on gcm * Temp fix require cycle * chore: format code and fix lint issues [skip ci] * CallIDUUID module on android and voip push * Add setCallUUID on useCallStore to persist calls accepted on native Android * remove callkeep from notification * Android Incoming Call UI POC * Refactor VoIP handling: Migrate VoIP-related classes to a new package structure, removing deprecated modules and consolidating functionality. Update imports in MainApplication and NotificationIntentHandler to reflect changes. This cleanup enhances code organization and prepares for future VoIP feature enhancements. * Remove VoipForegroundService * cleanup and use caller instead of callerName * Cleanup and make iOS build again * Refactor VoIP handling: Remove unused event emissions for call answered and declined, switch from SharedPreferences to in-memory storage for pending VoIP call data, and update method signatures for better clarity. This cleanup enhances performance and prepares for future VoIP feature improvements. * Refactor VoIP handling: Introduce a new VoipPayload class to encapsulate call data, streamline notification processing, and enhance method signatures across the VoIP module. This update improves code clarity and prepares for future feature enhancements. * Migrate react-native-voip-push-notifications to VoipModule * Refactor VoIP module: Update package structure by moving VoipTurboPackage to the main package and removing the obsolete NativeVoipSpec class. Adjust imports in MainApplication and VoipModule to reflect these changes, enhancing code organization and maintainability. * Unify emitters * Move CallKeep listeners from MediaSessionInstance to getInitialEvents * Clear callkeep on endcall * Unify getInitialEvents logic * getInitialEvents -> MediaCallEvents * chore: format code and fix lint issues [skip ci] * feat(Android): Add full screen incoming call (#6977) * feat: Update call UI (#6990) * feat: Handle audio routing, e.g., Bluetooth headset vs. internal speaker switching (#6992) * fix: empty space when not on call (#6993) * feat: Dialpad (#7000) * action: organized translations * feat: start call (#7024) * chore: format code and fix lint issues * feat: Pre flight (#7038) * action: organized translations * feat: Receive voip push notifications from backend (#7045) * feat: Refactor media session handling and improve disconnect logic (#7065) * feat: Control incoming call from native (#7066) * feat: Voice message blocks (#7057) * feat: native accept success event (#7068) * feat(voip): call waiting, busy detection, and videoconf blocking (#7077) * action: organized translations * feat(voip): tap-to-hide call controls with animations (#7078) * feat(voip): navigate to call DM from message button and header (#7082) * feat(voip): tablet and landscape layout (#7110) * chore: develop into feat.voip-lib-new (RN 81 + Expo 54 + reanimated 4 + true-sheet + iOS 26) (#7114) * chore: format code and fix lint issues * feat(voip): android landscape layout for IncomingCallActivity (#7116) * Update agents files * feat(voip): Support a11y (#7106) * Fix content cutting on iOS on some edge cases * pods * Ignore .worktrees on jest * chore: Merge develop into feat.voip-lib-new (#7129) * fix(voip): show CallKit UI when call is active in background (#7128) * chore: Update media-signaling to 0.2.0 (#7153) * feat(voip): migrate iOS accept/reject from DDP to REST (#7124) * Fix icons * feat(voip): migrate Android accept/reject from DDP to REST (#7127) * test(voip): integration tests for CallView pipeline (#7161) * feat(voip): display video conf provider as subtitle (#7160) * fix(voip): CallView button grid and correct landscape/dialpad layouts (#7164) * fix(voip): prevent stale MMKV cache on Android first-install accept MMKVKeyManager.initialize ran in MainApplication.onCreate before the JS engine started and opened the default MMKV file via the Tencent 1.2 JAR when it was still empty. Tencent caches instances per-ID in a singleton registry, so that empty-state view was held for the rest of the process. JS later wrote credentials through react-native-mmkv (MMKV Core 2.0), which has its own separate registry. When a VoIP push arrived, Ejson.getMMKV() got the cached empty Tencent instance and reported "No userId found in MMKV for server". Closing and reopening the app cleared the cache, which is why only the very first call after install failed. Drop the open/verify block — the encryption key is already cached from SecureKeystore, so no MMKV handle is needed here. The first Tencent instance is now created inside Ejson.getMMKV() after JS has written, so it scans the file fresh. * fix(voip): prevent duplicate ringtone on Android incoming call (#7158) * fix(voip): set explicit snaps for NewMediaCall bottom sheet (#7165) * Update app/lib/services/voip/MediaSessionStore.ts Co-authored-by: Pierre Lehnen <55164754+pierre-lehnen-rc@users.noreply.github.com> * fix: make startVoipFork reactive to permissions-changed (#7151) * fix(android): remove MediaProjectionService from merged manifest (#7190) * fix(voip): Phone account creation (#7170) * feat: add Enable Mobile Ringing toggle in user preferences (#7155) * fix(voip): ship blockers for PushKit, licensing, outbound calls, push tokens (#7167) * fix(android): Play Store mic discoverability, safer FCM logs, avatar auth via headers (#7171) * fix(ios): serialize VoipService bridge statics (#7169) * fix(voip): Android DDP thread safety and VoipPayload bundle parity (#7168) * chore(voip): dead-code and hygiene sweep (#7174) * refactor(voip): decouple navigateToCallRoom from Redux and backfill REST/connect tests (#7176) * test(voip): tighten ringing endCall assertion and add VideoConf VoIP-lock saga coverage (#7177) * fix(ios): harden VoIP DDP WebSocket client on receive failures and TLS (#7173) * refactor(voip): MediaCallEvents Redux adapters and resetVoipState (#7178) * refactor(voip): decouple peer autocomplete from Redux; simplify NewMediaCall (#7175) * fix(ios): add NS_SWIFT_NAME to Challenge.runChallenge for Swift 6.2 compatibility Swift 6.2 (Xcode 26.x / macos-26 runner) auto-renames the Objective-C method runChallenge:didReceiveChallenge:completionHandler: to run(_:didReceive:completionHandler:) when imported into Swift. Add NS_SWIFT_NAME to explicitly pin the Swift import name, preventing the compiler from applying its heuristics. This keeps the existing Swift call site in DDPClient.swift working without changes. * fix(ios): cancel old URLSession/webSocketTask before reconnecting in DDPClient.connect (#7197) * fix(ios): add NSLock to nativeAcceptHandledCallIds and 10s REST timeout to handleNativeAccept (#7198) * feat(android): create VoipCallService with FOREGROUND_SERVICE_MICROPHONE (#7199) * fix(android): start VoipCallService on accept, stop on hangup/timeout, install end-call listener (#7200) * fix(voip): enable DM nav for users with SIP extension (#7203) * fix(android): handle null VoiceConnection in answerIncomingCall, notify JS (#7201) * fix(voip): resolve closure capture ordering in handleNativeAccept (#7209) * fix(android): integrate VoIP modules with SSL-pinned OkHttpClient (#7208) * fix(push): gate id and voipToken behind server version checks, fix VideoConf caller extra (#7210) * fix(voip): remove sensitive data from production logs (#7207) * fix(android): remove isRunning guard + add double-tap guard on Accept/Decline - VoipCallService: remove if (!isRunning) guard, call startForeground unconditionally (idempotent on Android, fixes Android 14+ foreground service requirement) - IncomingCallActivity: add AtomicBoolean guard on handleAccept/handleDecline to prevent double-tap from triggering multiple service starts --------- Co-authored-by: diegolmello <diegolmello@users.noreply.github.com> Co-authored-by: Pierre Lehnen <55164754+pierre-lehnen-rc@users.noreply.github.com>
1 parent 507fe3a commit faed2d3

276 files changed

Lines changed: 44325 additions & 658 deletions

File tree

Some content is hidden

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

.cursor/skills/agent-skills

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Subproject commit a4f602ffb4aeaf4199fa97b7162f9c9d1f655904

.gitignore

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,13 @@ e2e/e2e_account.ts
8484
**/e2e_account.ts
8585

8686
*.p8
87-
.claude/
8887
.worktrees/
8988
.omc/
89+
.claude/
90+
.agents/
91+
.cursor/
92+
skills-lock.json
93+
CLAUDE.local.md
94+
AGENTS.md
95+
.superset/
9096
.jest-cache/

CLAUDE.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,10 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
66

77
Rocket.Chat React Native mobile client. Single-package React Native app (not a monorepo) using Yarn 1.22.22 (npm won't work). Supports iOS 13.4+ and Android 6.0+.
88

9-
- React 19, React Native 0.79, Expo 53
9+
- React 19.1, React Native 0.81, Expo 54
1010
- TypeScript with strict mode, baseUrl set to `app/` (imports resolve from there)
11-
- Min Node: 22.14.0
11+
- Node: engines `>=18`, volta pins 24.13.1
12+
- Read UBIQUITOUS_LANGUAGE.md
1213

1314
## Commands
1415

__mocks__/react-native-callkeep.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
export default {
2+
setup: jest.fn(),
3+
canMakeMultipleCalls: jest.fn(),
4+
displayIncomingCall: jest.fn(),
5+
endCall: jest.fn(),
6+
setCurrentCallActive: jest.fn(),
7+
setAvailable: jest.fn(),
8+
addEventListener: jest.fn((event, callback) => ({
9+
remove: jest.fn()
10+
}))
11+
};

android/app/build.gradle

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,11 +148,15 @@ dependencies {
148148

149149
implementation "com.google.code.gson:gson:2.8.9"
150150
implementation "com.tencent:mmkv-static:1.2.10"
151+
implementation "com.github.bumptech.glide:glide:${rootProject.ext.glideVersion}"
151152
implementation 'com.facebook.soloader:soloader:0.10.4'
152153

153154
// For SecureKeystore (EncryptedSharedPreferences)
154155
implementation 'androidx.security:security-crypto:1.1.0'
155156

157+
testImplementation 'junit:junit:4.13.2'
158+
testImplementation 'org.robolectric:robolectric:4.14.1'
159+
156160
// For ProcessLifecycleOwner (app foreground detection)
157161
implementation 'androidx.lifecycle:lifecycle-process:2.8.7'
158162
}

android/app/src/main/AndroidManifest.xml

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,18 @@
1313
<!-- permissions related to jitsi call -->
1414
<uses-permission android:name="android.permission.BLUETOOTH" />
1515

16+
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
17+
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" />
18+
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_PHONE_CALL" />
19+
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
20+
<uses-permission android:name="android.permission.BIND_TELECOM_CONNECTION_SERVICE" />
21+
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
22+
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
23+
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" />
24+
<uses-permission android:name="android.permission.MANAGE_OWN_CALLS" />
25+
<uses-feature android:name="android.hardware.audio.output" />
26+
<uses-feature android:name="android.hardware.microphone" android:required="false" />
27+
1628
<!-- android 13 notifications -->
1729
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
1830

@@ -105,6 +117,51 @@
105117
<meta-data
106118
android:name="com.bugsnag.android.API_KEY"
107119
android:value="${BugsnagAPIKey}" />
120+
121+
<activity
122+
android:name="chat.rocket.reactnative.voip.IncomingCallActivity"
123+
android:exported="false"
124+
android:launchMode="singleInstance"
125+
android:showOnLockScreen="true"
126+
android:turnScreenOn="true"
127+
android:showWhenLocked="true"
128+
android:theme="@style/Theme.IncomingCall"
129+
android:excludeFromRecents="true"
130+
android:taskAffinity="chat.rocket.reactnative.voip" />
131+
132+
<receiver
133+
android:name="chat.rocket.reactnative.voip.VoipNotification$DeclineReceiver"
134+
android:enabled="true"
135+
android:exported="false" />
136+
137+
<service android:name="io.wazo.callkeep.VoiceConnectionService"
138+
android:label="Wazo"
139+
android:permission="android.permission.BIND_TELECOM_CONNECTION_SERVICE"
140+
android:exported="true"
141+
android:foregroundServiceType="microphone|phoneCall"
142+
>
143+
<intent-filter>
144+
<action android:name="android.telecom.ConnectionService" />
145+
</intent-filter>
146+
</service>
147+
148+
<service android:name="io.wazo.callkeep.RNCallKeepBackgroundMessagingService" />
149+
150+
<!-- VoIP foreground service for keeping audio calls alive in the background. -->
151+
<service
152+
android:name="chat.rocket.reactnative.voip.VoipCallService"
153+
android:enabled="true"
154+
android:exported="false"
155+
android:foregroundServiceType="microphone" />
156+
157+
<!-- react-native-webrtc ships MediaProjectionService (foregroundServiceType=mediaProjection)
158+
for screen sharing. We don't use screen sharing, and Android 15+ forbids starting
159+
restricted foreground service types from BOOT_COMPLETED receivers (expo-notifications
160+
merges one in). Play Console flags this as a crash risk. Strip it from the merged
161+
manifest. To re-enable, remove this block and set enableMediaProjectionService=true. -->
162+
<service
163+
android:name="com.oney.WebRTCModule.MediaProjectionService"
164+
tools:node="remove" />
108165
</application>
109166

110167
<queries>
292 Bytes
Binary file not shown.

android/app/src/main/java/chat/rocket/reactnative/MainActivity.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,20 +29,20 @@ class MainActivity : ReactActivity() {
2929
override fun onCreate(savedInstanceState: Bundle?) {
3030
RNBootSplash.init(this, R.style.BootTheme)
3131
super.onCreate(null)
32-
32+
3333
// Handle notification intents
3434
intent?.let { NotificationIntentHandler.handleIntent(this, it) }
3535
}
3636

3737
public override fun onNewIntent(intent: Intent) {
3838
super.onNewIntent(intent)
3939
setIntent(intent)
40-
40+
4141
// Handle notification intents when activity is already running
4242
NotificationIntentHandler.handleIntent(this, intent)
4343
}
4444

4545
override fun invokeDefaultOnBackPressed() {
4646
moveTaskToBack(true)
4747
}
48-
}
48+
}

android/app/src/main/java/chat/rocket/reactnative/MainApplication.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import chat.rocket.reactnative.storage.MMKVKeyManager;
2121
import chat.rocket.reactnative.storage.SecureStoragePackage;
2222
import chat.rocket.reactnative.notification.VideoConfTurboPackage
2323
import chat.rocket.reactnative.notification.PushNotificationTurboPackage
24+
import chat.rocket.reactnative.VoipTurboPackage
2425
import chat.rocket.reactnative.scroll.InvertedScrollPackage
2526

2627
/**
@@ -45,6 +46,7 @@ open class MainApplication : Application(), ReactApplication {
4546
add(WatermelonDBJSIPackage())
4647
add(VideoConfTurboPackage())
4748
add(PushNotificationTurboPackage())
49+
add(VoipTurboPackage())
4850
add(SecureStoragePackage())
4951
add(InvertedScrollPackage())
5052
}

android/app/src/main/java/chat/rocket/reactnative/networking/SSLPinningTurboModule.java

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,30 @@ public class SSLPinningTurboModule extends NativeSSLPinningSpec implements KeyCh
4141
private Promise promise;
4242
private static String alias;
4343
private static ReactApplicationContext reactContext;
44+
private static OkHttpClient sharedClient;
45+
46+
public static OkHttpClient getSharedOkHttpClient() {
47+
if (sharedClient != null) {
48+
return sharedClient;
49+
}
50+
if (alias != null) {
51+
OkHttpClient.Builder builder = new OkHttpClient.Builder()
52+
.connectTimeout(0, TimeUnit.MILLISECONDS)
53+
.readTimeout(0, TimeUnit.MILLISECONDS)
54+
.writeTimeout(0, TimeUnit.MILLISECONDS)
55+
.cookieJar(new ReactCookieJarContainer());
56+
57+
SSLSocketFactory sslSocketFactory = getSSLFactory(alias);
58+
X509TrustManager trustManager = getTrustManagerFactory();
59+
if (sslSocketFactory != null) {
60+
builder.sslSocketFactory(sslSocketFactory, trustManager);
61+
}
62+
63+
sharedClient = builder.build();
64+
return sharedClient;
65+
}
66+
return null;
67+
}
4468

4569
public SSLPinningTurboModule(ReactApplicationContext reactContext) {
4670
super(reactContext);
@@ -61,6 +85,10 @@ public void apply(OkHttpClient.Builder builder) {
6185
}
6286

6387
protected OkHttpClient getOkHttpClient() {
88+
OkHttpClient shared = getSharedOkHttpClient();
89+
if (shared != null) {
90+
return shared;
91+
}
6492
OkHttpClient.Builder builder = new OkHttpClient.Builder()
6593
.connectTimeout(0, TimeUnit.MILLISECONDS)
6694
.readTimeout(0, TimeUnit.MILLISECONDS)

0 commit comments

Comments
 (0)