Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
9cd7d55
feat(chat): integrate with Android Telecom
thecyberd3m0n Oct 5, 2026
16249bc
feat(chat): integrate with Android Telecom - unit tests
thecyberd3m0n Oct 5, 2026
581983e
feat(chat): integrate with Android Telecom - unit tests and fixes
thecyberd3m0n Oct 5, 2026
bb7ba7f
Merge branch 'master' into feat/Android-Telecom
thecyberd3m0n Oct 5, 2026
352cdce
feat(chat): integrate with Android Telecom - after review fixes
thecyberd3m0n Oct 5, 2026
643a1d8
feat(chat): integrate with Android Telecom - after review - unique re…
thecyberd3m0n Oct 5, 2026
74cbd5e
feat(chat): integrate with Android Telecom - after review fixes
thecyberd3m0n Oct 5, 2026
0b40eb5
Merge branch 'master' into feat/Android-Telecom
thecyberd3m0n Oct 6, 2026
21f4c32
Merge branch 'master' into feat/Android-Telecom
thecyberd3m0n Oct 8, 2026
18afcef
Merge branch 'master' into feat/Android-Telecom
thecyberd3m0n Oct 9, 2026
f2bae02
Merge branch 'master' into feat/Android-Telecom
thecyberd3m0n Oct 10, 2026
c2f5e13
Merge branch 'master' into feat/Android-Telecom
thecyberd3m0n Oct 13, 2026
66b5471
Merge branch 'master' into feat/Android-Telecom
thecyberd3m0n Oct 13, 2026
8380720
Add voice-only call option in CallNotificationActivity
thecyberd3m0n Oct 18, 2026
d1dba89
Copilot review fixes + synchronization issues
thecyberd3m0n Oct 18, 2026
0d67486
Merge branch 'master' into feat/Android-Telecom
thecyberd3m0n Oct 18, 2026
6b1c6bf
Merge branch 'master' into feat/Android-Telecom
thecyberd3m0n Oct 19, 2026
8b2c595
Merge branch 'master' into feat/Android-Telecom
thecyberd3m0n Oct 20, 2026
206bcd0
Merge branch 'master' into feat/Android-Telecom
thecyberd3m0n Oct 23, 2026
a935974
Merge branch 'master' into feat/Android-Telecom
thecyberd3m0n Oct 24, 2026
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
1 change: 1 addition & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,7 @@ configurations.configureEach {
}

dependencies {
implementation("androidx.core:core-telecom:1.0.0")
implementation("androidx.media3:media3-session:1.11.1")
kapt("org.jetbrains.kotlin:kotlin-metadata-jvm:$kotlinVersion")
implementation("androidx.room:room-testing-android:$roomVersion")
Expand Down
4 changes: 3 additions & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@
<uses-permission android:name="android.permission.READ_SYNC_SETTINGS" />
<uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS" />
<uses-permission android:name="android.permission.MANAGE_ACCOUNTS" />
<uses-permission android:name="android.permission.MANAGE_OWN_CALLS" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_PHONE_CALL" />

<uses-permission
android:name="android.permission.USE_CREDENTIALS"
Expand Down Expand Up @@ -366,7 +368,7 @@
<service
android:name=".services.CallForegroundService"
android:exported="false"
android:foregroundServiceType="microphone|camera" />
android:foregroundServiceType="phoneCall|microphone|camera" />

<service android:name=".services.UnifiedPushService"
android:exported="false">
Expand Down
135 changes: 89 additions & 46 deletions app/src/main/java/com/nextcloud/talk/activities/CallActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ import com.nextcloud.talk.call.MessageSenderMcu
import com.nextcloud.talk.call.MessageSenderNoMcu
import com.nextcloud.talk.call.MutableLocalCallParticipantModel
import com.nextcloud.talk.call.ReactionAnimator
import com.nextcloud.talk.call.TelecomManager
import com.nextcloud.talk.call.components.ParticipantGrid
import com.nextcloud.talk.call.components.SelfVideoView
import com.nextcloud.talk.call.components.screenshare.ScreenShareComponent
Expand Down Expand Up @@ -222,6 +223,9 @@ class CallActivity : CallBaseActivity() {
@Inject
var permissionUtil: PlatformPermissionUtil? = null

@Inject
lateinit var telecomManager: TelecomManager

@Inject
lateinit var viewModelFactory: ViewModelProvider.Factory
lateinit var callViewModel: CallViewModel
Expand Down Expand Up @@ -1631,6 +1635,7 @@ class CallActivity : CallBaseActivity() {
// The screen that started the call marked it as dialing, which hangup() would reset. Without resetting
// it here, chats would keep treating the app as being in a call.
ApplicationWideCurrentRoomHolder.getInstance().isDialing = false
telecomManager.endCurrentCall(roomToken)
}

super.onDestroy()
Expand Down Expand Up @@ -2247,6 +2252,7 @@ class CallActivity : CallBaseActivity() {

private fun hangup(shutDownView: Boolean, endCallForAll: Boolean) {
Log.d(TAG, "hangup! shutDownView=$shutDownView, endCallForAll=$endCallForAll")

joinRoomInitiated = false
if (shutDownView) {
setCallState(CallStatus.LEAVING)
Expand All @@ -2257,8 +2263,12 @@ class CallActivity : CallBaseActivity() {
dispose(nickSendingDisposable)
nickSendingDisposable = null

if (shutDownView) {
if (shutDownView || currentCallStatus == CallStatus.CALLING_TIMEOUT) {
terminateAudioVideo()
if (webSocketClient != null) {
webSocketClient!!.sendBye()
webSocketClient = null
}
}

val peerConnectionIdsToEnd: MutableList<String> = ArrayList(peerConnectionWrapperList.size)
Expand Down Expand Up @@ -2288,6 +2298,9 @@ class CallActivity : CallBaseActivity() {
}

hangupNetworkCalls(shutDownView, endCallForAll)
if (shutDownView) {
telecomManager.endCurrentCall(roomToken)
}
}

private fun terminateAudioVideo() {
Expand Down Expand Up @@ -2327,7 +2340,9 @@ class CallActivity : CallBaseActivity() {
}
localAudioTrack = null
localVideoTrack = null
if (TextUtils.isEmpty(credentials) && hasExternalSignalingServer) {
if (::conversationUser.isInitialized && conversationUser.id != null) {
WebSocketConnectionHelper.deleteExternalSignalingInstanceForUserEntity(conversationUser.id!!)
} else if (TextUtils.isEmpty(credentials) && hasExternalSignalingServer) {
WebSocketConnectionHelper.deleteExternalSignalingInstanceForUserEntity(-1)
}
}
Expand Down Expand Up @@ -2377,7 +2392,7 @@ class CallActivity : CallBaseActivity() {
startActivity(intent)
finish()
} else if (shutDownView) {
finish()
finishAndRemoveTask()
} else if (currentCallStatus === CallStatus.RECONNECTING ||
currentCallStatus === CallStatus.PUBLISHER_FAILED
) {
Expand All @@ -2386,52 +2401,55 @@ class CallActivity : CallBaseActivity() {
}

private fun openConversationListInPrimaryTask() {
val activityManager = getSystemService(Context.ACTIVITY_SERVICE) as? ActivityManager
val appTasks = activityManager
?.appTasks
?.filter { appTask -> appTask.taskInfo?.baseActivity?.packageName == packageName }
.orEmpty()

val primaryTask = appTasks.firstOrNull { appTask ->
val taskInfo = appTask.taskInfo
val baseActivityName = taskInfo?.baseActivity?.className
taskInfo != null &&
taskInfo.taskId != taskId &&
baseActivityName == MainActivity::class.java.name
} ?: appTasks.firstOrNull { appTask ->
val taskInfo = appTask.taskInfo
val baseActivityName = taskInfo?.baseActivity?.className
taskInfo != null &&
taskInfo.taskId != taskId &&
baseActivityName != CallActivity::class.java.name &&
baseActivityName != CallNotificationActivity::class.java.name
}

if (primaryTask != null) {
val intent = Intent(context, ConversationsListActivity::class.java).apply {
putExtra(KEY_INTERNAL_USER_ID, conversationUser.id)
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP)
}
primaryTask.startActivity(context, intent, null)
primaryTask.moveToFront()
val primaryTaskId = primaryTask.taskInfo?.taskId
appTasks
.filter { appTask ->
val taskInfo = appTask.taskInfo
val baseActivityName = taskInfo?.baseActivity?.className
val isCallTask = baseActivityName == CallActivity::class.java.name ||
baseActivityName == CallNotificationActivity::class.java.name
taskInfo != null && !isCallTask && taskInfo.taskId != primaryTaskId && taskInfo.taskId != taskId
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val activityManager = getSystemService(Context.ACTIVITY_SERVICE) as? ActivityManager
val appTasks = activityManager
?.appTasks
?.filter { appTask -> appTask.taskInfo?.baseActivity?.packageName == packageName }
.orEmpty()

val primaryTask = appTasks.firstOrNull { appTask ->
val taskInfo = appTask.taskInfo
val baseActivityName = taskInfo?.baseActivity?.className
taskInfo != null &&
taskInfo.taskId != taskId &&
baseActivityName == MainActivity::class.java.name
} ?: appTasks.firstOrNull { appTask ->
val taskInfo = appTask.taskInfo
val baseActivityName = taskInfo?.baseActivity?.className
taskInfo != null &&
taskInfo.taskId != taskId &&
baseActivityName != CallActivity::class.java.name &&
baseActivityName != CallNotificationActivity::class.java.name
}

if (primaryTask != null) {
val intent = Intent(context, ConversationsListActivity::class.java).apply {
putExtra(KEY_INTERNAL_USER_ID, conversationUser.id)
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP)
}
.forEach { it.finishAndRemoveTask() }
} else {
val mainIntent = Intent(context, MainActivity::class.java).apply {
action = Intent.ACTION_MAIN
addCategory(Intent.CATEGORY_LAUNCHER)
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
primaryTask.startActivity(context, intent, null)
primaryTask.moveToFront()
val primaryTaskId = primaryTask.taskInfo?.taskId
appTasks
.filter { appTask ->
val taskInfo = appTask.taskInfo
val baseActivityName = taskInfo?.baseActivity?.className
val isCallTask = baseActivityName == CallActivity::class.java.name ||
baseActivityName == CallNotificationActivity::class.java.name
taskInfo != null && !isCallTask && taskInfo.taskId != primaryTaskId && taskInfo.taskId != taskId
}
.forEach { it.finishAndRemoveTask() }
return
}
startActivity(mainIntent)
}

val mainIntent = Intent(context, MainActivity::class.java).apply {
action = Intent.ACTION_MAIN
addCategory(Intent.CATEGORY_LAUNCHER)
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
}
startActivity(mainIntent)
}

private fun startVideoCapture(isPortrait: Boolean) {
Expand Down Expand Up @@ -2976,7 +2994,31 @@ class CallActivity : CallBaseActivity() {
}
}

private fun activateTelecomCall() {
val disconnectHandler: () -> Unit = {
runOnUiThread {
if (!isFinishing && !isDestroyed && currentCallStatus != CallStatus.LEAVING) {
hangup(shutDownView = true, endCallForAll = false)
}
}
}

if (!telecomManager.hasActiveCall(roomToken)) {
telecomManager.addOutgoingCall(
displayName = conversationName.takeIf {
!it.isNullOrBlank()
} ?: getString(R.string.nc_app_product_name),
roomToken = roomToken ?: "",
isVideo = !isVoiceOnlyCall,
onDisconnectCall = disconnectHandler
)
} else {
telecomManager.setCallActive(roomToken, onDisconnectCall = disconnectHandler)
}
}

private fun handleCallStateInConversation() {
activateTelecomCall()
stopCallingSound()
if (binding!!.callStates.callStateRelativeLayout.visibility != View.INVISIBLE) {
binding!!.callStates.callStateRelativeLayout.visibility = View.INVISIBLE
Expand All @@ -2993,6 +3035,7 @@ class CallActivity : CallBaseActivity() {
}

private fun handleCallStateJoined() {
activateTelecomCall()
if (isIncomingCallFromNotification) {
binding!!.callStates.callStateTextView.setText(R.string.nc_call_incoming)
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import com.nextcloud.talk.errorhandling.ExceptionHandler
import com.nextcloud.talk.logger.LoggerImpl
import com.nextcloud.talk.account.BrowserLoginActivity
import com.nextcloud.talk.dagger.modules.ApplicationScope
import com.nextcloud.talk.call.TelecomManager
import com.nextcloud.talk.dagger.modules.BusModule
import com.nextcloud.talk.dagger.modules.ContextModule
import com.nextcloud.talk.dagger.modules.DaosModule
Expand Down Expand Up @@ -124,6 +125,9 @@ class NextcloudTalkApplication :
lateinit var logger: LoggerImpl
//endregion

@Inject
lateinit var telecomManager: TelecomManager

//region private methods
private fun installExceptionHandler() {
val defaultHandler = Thread.getDefaultUncaughtExceptionHandler()
Expand Down Expand Up @@ -215,11 +219,13 @@ class NextcloudTalkApplication :
securityKeyManager.init(this, securityKeyConfigBuilder.build())
initializeWebRtc()
buildComponent()

componentApplication.inject(this)

telecomManager.registerAppWithTelecom()
Comment thread
thecyberd3m0n marked this conversation as resolved.
DavUtils.registerCustomFactories()

Security.insertProviderAt(Conscrypt.newProvider(), 1)

componentApplication.inject(this)
installExceptionHandler()

ProcessLifecycleOwner.get().lifecycle.addObserver(object : DefaultLifecycleObserver {
Expand Down
Loading