From dcde0be993b7ee9733cdca5fa275c13b5b778caa Mon Sep 17 00:00:00 2001 From: Jens Zalzala Date: Tue, 31 Mar 2026 20:25:08 -0500 Subject: [PATCH 1/5] Stop calls from ringing after answering. Switched call notification to NotificationCompat.CallStyle. This adds answer/decline buttons to the notification so that users can answer incoming calls. Adds caller avatar to notification. Signed-off-by: Jens Zalzala --- app/src/main/AndroidManifest.xml | 1 + .../nextcloud/talk/activities/CallActivity.kt | 7 ++ .../talk/activities/CallBaseActivity.java | 4 -- .../nextcloud/talk/jobs/NotificationWorker.kt | 68 ++++++++++++++++++- .../CallNotificationActionReceiver.kt | 28 ++++++++ 5 files changed, 102 insertions(+), 6 deletions(-) create mode 100644 app/src/main/java/com/nextcloud/talk/receivers/CallNotificationActionReceiver.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 5da4d1fc8da..037e52e4598 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -293,6 +293,7 @@ + diff --git a/app/src/main/java/com/nextcloud/talk/activities/CallActivity.kt b/app/src/main/java/com/nextcloud/talk/activities/CallActivity.kt index 753c72b08ca..f0ed538927d 100644 --- a/app/src/main/java/com/nextcloud/talk/activities/CallActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/activities/CallActivity.kt @@ -41,6 +41,7 @@ import android.view.OrientationEventListener import android.view.View import android.view.View.OnTouchListener import androidx.activity.result.contract.ActivityResultContracts +import androidx.core.app.NotificationManagerCompat import androidx.annotation.DrawableRes import androidx.appcompat.app.AlertDialog import androidx.compose.material3.MaterialTheme @@ -118,6 +119,7 @@ import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_CALL_WITHOUT_NOTIFICATION import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_CONVERSATION_NAME import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_CONVERSATION_PASSWORD import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_FROM_NOTIFICATION_START_CALL +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_NOTIFICATION_TIMESTAMP import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_IS_BREAKOUT_ROOM import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_IS_MODERATOR import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_MODIFIED_BASE_URL @@ -553,6 +555,11 @@ class CallActivity : CallBaseActivity() { if (extras.containsKey(KEY_FROM_NOTIFICATION_START_CALL)) { isIncomingCallFromNotification = extras.getBoolean(KEY_FROM_NOTIFICATION_START_CALL) + val notificationId = extras.getInt(KEY_NOTIFICATION_TIMESTAMP, 0) + if (notificationId != 0) { + // cancel the notification to stop the call ringing + NotificationManagerCompat.from(this).cancel(notificationId) + } } if (extras.containsKey(KEY_IS_BREAKOUT_ROOM)) { isBreakoutRoom = extras.getBoolean(KEY_IS_BREAKOUT_ROOM) diff --git a/app/src/main/java/com/nextcloud/talk/activities/CallBaseActivity.java b/app/src/main/java/com/nextcloud/talk/activities/CallBaseActivity.java index 1063c6c844d..6086b2d4628 100644 --- a/app/src/main/java/com/nextcloud/talk/activities/CallBaseActivity.java +++ b/app/src/main/java/com/nextcloud/talk/activities/CallBaseActivity.java @@ -8,7 +8,6 @@ import android.annotation.SuppressLint; import android.app.AppOpsManager; -import android.app.KeyguardManager; import android.app.PictureInPictureParams; import android.content.Context; import android.content.pm.PackageManager; @@ -76,10 +75,7 @@ void dismissKeyguard() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) { setShowWhenLocked(true); setTurnScreenOn(true); - KeyguardManager keyguardManager = (KeyguardManager) getSystemService(KEYGUARD_SERVICE); - keyguardManager.requestDismissKeyguard(this, null); } else { - getWindow().addFlags(WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD); getWindow().addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED); getWindow().addFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON); } diff --git a/app/src/main/java/com/nextcloud/talk/jobs/NotificationWorker.kt b/app/src/main/java/com/nextcloud/talk/jobs/NotificationWorker.kt index b705a8f8186..14b1a908cd9 100644 --- a/app/src/main/java/com/nextcloud/talk/jobs/NotificationWorker.kt +++ b/app/src/main/java/com/nextcloud/talk/jobs/NotificationWorker.kt @@ -45,6 +45,7 @@ import coil.request.ImageRequest import com.bluelinelabs.logansquare.LoganSquare import com.nextcloud.talk.BuildConfig import com.nextcloud.talk.R +import com.nextcloud.talk.activities.CallActivity import com.nextcloud.talk.activities.MainActivity import com.nextcloud.talk.api.NcApi import com.nextcloud.talk.application.NextcloudTalkApplication @@ -61,6 +62,7 @@ import com.nextcloud.talk.models.json.participants.Participant import com.nextcloud.talk.models.json.participants.ParticipantsOverall import com.nextcloud.talk.models.json.push.DecryptedPushMessage import com.nextcloud.talk.models.json.push.NotificationUser +import com.nextcloud.talk.receivers.CallNotificationActionReceiver import com.nextcloud.talk.receivers.DirectReplyReceiver import com.nextcloud.talk.receivers.DismissRecordingAvailableReceiver import com.nextcloud.talk.receivers.MarkAsReadReceiver @@ -95,7 +97,6 @@ import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_THREAD_ID import com.nextcloud.talk.utils.preferences.AppPreferences import io.reactivex.Observable import io.reactivex.Observer -import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.disposables.Disposable import io.reactivex.schedulers.Schedulers import okhttp3.JavaNetCookieJar @@ -268,11 +269,66 @@ class NotificationWorker(context: Context, workerParams: WorkerParameters) : Wor } ) + val pendingIntentFlags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT + } else { + PendingIntent.FLAG_UPDATE_CURRENT + } + + val answerVoiceBundle = Bundle(bundle).apply { putBoolean(BundleKeys.KEY_CALL_VOICE_ONLY, true) } + val answerVoicePendingIntent = PendingIntent.getActivity( + applicationContext, + requestCode + ANSWER_VOICE_REQUEST_OFFSET, + Intent(applicationContext, CallActivity::class.java).apply { + putExtras(answerVoiceBundle) + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + }, + pendingIntentFlags + ) + + val answerVideoBundle = Bundle(bundle).apply { putBoolean(BundleKeys.KEY_CALL_VOICE_ONLY, false) } + val answerVideoPendingIntent = PendingIntent.getActivity( + applicationContext, + requestCode + ANSWER_VIDEO_REQUEST_OFFSET, + Intent(applicationContext, CallActivity::class.java).apply { + putExtras(answerVideoBundle) + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + }, + pendingIntentFlags + ) + + val declinePendingIntent = PendingIntent.getBroadcast( + applicationContext, + requestCode + DECLINE_CALL_REQUEST_OFFSET, + Intent(applicationContext, CallNotificationActionReceiver::class.java).apply { + action = CallNotificationActionReceiver.ACTION_DECLINE_CALL + putExtra(KEY_NOTIFICATION_TIMESTAMP, pushMessage.timestamp.toInt()) + }, + pendingIntentFlags + ) + val soundUri = getCallRingtoneUri(applicationContext, appPreferences) val notificationChannelId = NotificationUtils.NotificationChannels.NOTIFICATION_CHANNEL_CALLS_V4.name val uri = signatureVerification.user!!.baseUrl!!.toUri() val baseUrl = uri.host + val callerPersonBuilder = Person.Builder() + .setName(conversation.displayName) + .setImportant(true) + if (conversation.type == ConversationEnums.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL) { + val avatarUrl = ApiUtils.getUrlForAvatar( + signatureVerification.user!!.baseUrl!!, + conversation.name, + false, + darkMode = DisplayUtils.isDarkModeOn(applicationContext) + ) + loadAvatarSync(avatarUrl, applicationContext)?.let { callerPersonBuilder.setIcon(it) } + } + val callerPerson = callerPersonBuilder.build() + + val isVideoCall = (conversation.callFlag and Participant.InCallFlags.WITH_VIDEO) > 0 + val primaryAnswerIntent = if (isVideoCall) answerVideoPendingIntent else answerVoicePendingIntent + val notification = NotificationCompat.Builder(applicationContext, notificationChannelId) .setPriority(NotificationCompat.PRIORITY_HIGH) @@ -289,6 +345,11 @@ class NotificationWorker(context: Context, workerParams: WorkerParameters) : Wor .setContentIntent(fullScreenPendingIntent) .setFullScreenIntent(fullScreenPendingIntent, true) .setSound(soundUri) + .setStyle( + NotificationCompat.CallStyle + .forIncomingCall(callerPerson, declinePendingIntent, primaryAnswerIntent) + .setIsVideo(isVideoCall) + ) .build() notification.flags = notification.flags or Notification.FLAG_INSISTENT @@ -299,7 +360,7 @@ class NotificationWorker(context: Context, workerParams: WorkerParameters) : Wor chatNetworkDataSource?.getRoom(userBeingCalled, roomToken = pushMessage.id!!) ?.subscribeOn(Schedulers.io()) - ?.observeOn(AndroidSchedulers.mainThread()) + ?.observeOn(Schedulers.io()) ?.subscribe(object : Observer { override fun onSubscribe(d: Disposable) { // unused atm @@ -1107,5 +1168,8 @@ class NotificationWorker(context: Context, workerParams: WorkerParameters) : Wor private const val TIMER_COUNT = 12 private const val TIMER_DELAY: Long = 5 private const val LINEBREAK: String = "\n" + private const val ANSWER_VOICE_REQUEST_OFFSET = 1 + private const val ANSWER_VIDEO_REQUEST_OFFSET = 2 + private const val DECLINE_CALL_REQUEST_OFFSET = 3 } } diff --git a/app/src/main/java/com/nextcloud/talk/receivers/CallNotificationActionReceiver.kt b/app/src/main/java/com/nextcloud/talk/receivers/CallNotificationActionReceiver.kt new file mode 100644 index 00000000000..1fe0c1bf41b --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/receivers/CallNotificationActionReceiver.kt @@ -0,0 +1,28 @@ +/* + * Nextcloud Talk - Android Client + * + * + * SPDX-FileCopyrightText: 2026 Jens Zalzala + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: GPL-3.0-or-later + * + */ +package com.nextcloud.talk.receivers + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import androidx.core.app.NotificationManagerCompat +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_NOTIFICATION_TIMESTAMP + +class CallNotificationActionReceiver : BroadcastReceiver() { + + override fun onReceive(context: Context, intent: Intent) { + val notificationId = intent.getIntExtra(KEY_NOTIFICATION_TIMESTAMP, 0) + NotificationManagerCompat.from(context).cancel(notificationId) + } + + companion object { + const val ACTION_DECLINE_CALL = "com.nextcloud.talk.ACTION_DECLINE_CALL" + } +} From 04606e3c045a085def3c896fea7f9c003df52733 Mon Sep 17 00:00:00 2001 From: Jens Zalzala Date: Tue, 14 Apr 2026 15:31:14 -0500 Subject: [PATCH 2/5] Renamed CallNotificationActionReceiver to DeclineCallReceiver. Signed-off-by: Jens Zalzala --- app/src/main/AndroidManifest.xml | 2 +- .../main/java/com/nextcloud/talk/jobs/NotificationWorker.kt | 5 ++--- ...NotificationActionReceiver.kt => DeclineCallReceiver.kt} | 6 +----- 3 files changed, 4 insertions(+), 9 deletions(-) rename app/src/main/java/com/nextcloud/talk/receivers/{CallNotificationActionReceiver.kt => DeclineCallReceiver.kt} (81%) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 037e52e4598..9a7da58faf8 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -293,7 +293,7 @@ - + diff --git a/app/src/main/java/com/nextcloud/talk/jobs/NotificationWorker.kt b/app/src/main/java/com/nextcloud/talk/jobs/NotificationWorker.kt index 14b1a908cd9..10b086452c6 100644 --- a/app/src/main/java/com/nextcloud/talk/jobs/NotificationWorker.kt +++ b/app/src/main/java/com/nextcloud/talk/jobs/NotificationWorker.kt @@ -62,7 +62,7 @@ import com.nextcloud.talk.models.json.participants.Participant import com.nextcloud.talk.models.json.participants.ParticipantsOverall import com.nextcloud.talk.models.json.push.DecryptedPushMessage import com.nextcloud.talk.models.json.push.NotificationUser -import com.nextcloud.talk.receivers.CallNotificationActionReceiver +import com.nextcloud.talk.receivers.DeclineCallReceiver import com.nextcloud.talk.receivers.DirectReplyReceiver import com.nextcloud.talk.receivers.DismissRecordingAvailableReceiver import com.nextcloud.talk.receivers.MarkAsReadReceiver @@ -300,8 +300,7 @@ class NotificationWorker(context: Context, workerParams: WorkerParameters) : Wor val declinePendingIntent = PendingIntent.getBroadcast( applicationContext, requestCode + DECLINE_CALL_REQUEST_OFFSET, - Intent(applicationContext, CallNotificationActionReceiver::class.java).apply { - action = CallNotificationActionReceiver.ACTION_DECLINE_CALL + Intent(applicationContext, DeclineCallReceiver::class.java).apply { putExtra(KEY_NOTIFICATION_TIMESTAMP, pushMessage.timestamp.toInt()) }, pendingIntentFlags diff --git a/app/src/main/java/com/nextcloud/talk/receivers/CallNotificationActionReceiver.kt b/app/src/main/java/com/nextcloud/talk/receivers/DeclineCallReceiver.kt similarity index 81% rename from app/src/main/java/com/nextcloud/talk/receivers/CallNotificationActionReceiver.kt rename to app/src/main/java/com/nextcloud/talk/receivers/DeclineCallReceiver.kt index 1fe0c1bf41b..f67544368b5 100644 --- a/app/src/main/java/com/nextcloud/talk/receivers/CallNotificationActionReceiver.kt +++ b/app/src/main/java/com/nextcloud/talk/receivers/DeclineCallReceiver.kt @@ -15,14 +15,10 @@ import android.content.Intent import androidx.core.app.NotificationManagerCompat import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_NOTIFICATION_TIMESTAMP -class CallNotificationActionReceiver : BroadcastReceiver() { +class DeclineCallReceiver : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { val notificationId = intent.getIntExtra(KEY_NOTIFICATION_TIMESTAMP, 0) NotificationManagerCompat.from(context).cancel(notificationId) } - - companion object { - const val ACTION_DECLINE_CALL = "com.nextcloud.talk.ACTION_DECLINE_CALL" - } } From be0bd8d0e9ffc484e2fc80026dbe08e1997bf154 Mon Sep 17 00:00:00 2001 From: Jens Zalzala Date: Tue, 14 Apr 2026 15:58:41 -0500 Subject: [PATCH 3/5] Don't enable the camera if call was answered via notification Signed-off-by: Jens Zalzala --- .../main/java/com/nextcloud/talk/activities/CallActivity.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/nextcloud/talk/activities/CallActivity.kt b/app/src/main/java/com/nextcloud/talk/activities/CallActivity.kt index f0ed538927d..8d2f22b5e75 100644 --- a/app/src/main/java/com/nextcloud/talk/activities/CallActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/activities/CallActivity.kt @@ -1038,7 +1038,10 @@ class CallActivity : CallBaseActivity() { binding!!.selfVideoViewWrapper.visibility = View.GONE } else if (permissionUtil!!.isCameraPermissionGranted()) { binding!!.selfVideoViewWrapper.visibility = View.VISIBLE - onCameraClick() + // don't enable the camera if call was answered via notification + if (!isIncomingCallFromNotification) { + onCameraClick() + } if (cameraEnumerator!!.deviceNames.isEmpty()) { binding!!.cameraButton.visibility = View.GONE } From ed89739d086ccdeeda96be62f8c6a743a74e6856 Mon Sep 17 00:00:00 2001 From: Marcel Hibbe Date: Wed, 15 Apr 2026 16:34:02 +0200 Subject: [PATCH 4/5] do not cancel the notifications in getRoom (but do it in processExtras) Do not cancel the notifications in getRoomnstead, but do it with NotificationManagerCompat.from(this).cancel(notificationId) in processExtras (see commit dcde0be9). It seems that things can go wrong that cancelExistingNotificationsForRoom was not reached which caused an indefinite ringing. The underlying cause why this is not reached is NOT fixed with this commit, but instead dismissing the notification in processExtras seems to be like a more reliable approach. Signed-off-by: Marcel Hibbe --- .../java/com/nextcloud/talk/activities/CallActivity.kt | 7 ------- 1 file changed, 7 deletions(-) diff --git a/app/src/main/java/com/nextcloud/talk/activities/CallActivity.kt b/app/src/main/java/com/nextcloud/talk/activities/CallActivity.kt index 8d2f22b5e75..01d434abf56 100644 --- a/app/src/main/java/com/nextcloud/talk/activities/CallActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/activities/CallActivity.kt @@ -1592,13 +1592,6 @@ class CallActivity : CallBaseActivity() { } ApplicationWideCurrentRoomHolder.getInstance().isInCall = true ApplicationWideCurrentRoomHolder.getInstance().isDialing = false - if (!TextUtils.isEmpty(roomToken)) { - cancelExistingNotificationsForRoom( - applicationContext, - conversationUser!!, - roomToken!! - ) - } if (!hasExternalSignalingServer) { pullSignalingMessages() } From 9106d40eacb43b9ad19131918593599c7098c7c2 Mon Sep 17 00:00:00 2001 From: Marcel Hibbe Date: Wed, 15 Apr 2026 16:38:30 +0200 Subject: [PATCH 5/5] update AUTHORS.md Signed-off-by: Marcel Hibbe --- AUTHORS.md | 1 + .../main/java/com/nextcloud/talk/activities/CallActivity.kt | 4 ++-- .../java/com/nextcloud/talk/receivers/DeclineCallReceiver.kt | 3 --- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/AUTHORS.md b/AUTHORS.md index ba63a0e2bee..c2c510aea60 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -30,6 +30,7 @@ * hidrohase * Jan-Christoph Borchardt * jld3103 +* Jens Zalzala * Joas Schilling * John Molakvoæ * Jos Poortvliet diff --git a/app/src/main/java/com/nextcloud/talk/activities/CallActivity.kt b/app/src/main/java/com/nextcloud/talk/activities/CallActivity.kt index 01d434abf56..2954fea0d25 100644 --- a/app/src/main/java/com/nextcloud/talk/activities/CallActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/activities/CallActivity.kt @@ -108,7 +108,6 @@ import com.nextcloud.talk.utils.ApiUtils import com.nextcloud.talk.utils.CapabilitiesUtil import com.nextcloud.talk.utils.CapabilitiesUtil.hasSpreedFeatureCapability import com.nextcloud.talk.utils.CapabilitiesUtil.isCallRecordingAvailable -import com.nextcloud.talk.utils.NotificationUtils.cancelExistingNotificationsForRoom import com.nextcloud.talk.utils.NotificationUtils.getCallRingtoneUri import com.nextcloud.talk.utils.ReceiverFlag import com.nextcloud.talk.utils.SpreedFeatures @@ -373,7 +372,7 @@ class CallActivity : CallBaseActivity() { private var isFrontCamera by mutableStateOf(true) - @SuppressLint("ClickableViewAccessibility") + @SuppressLint("ClickableViewAccessibility", "Detekt.LongMethod") override fun onCreate(savedInstanceState: Bundle?) { Log.d(TAG, "onCreate") super.onCreate(savedInstanceState) @@ -1812,6 +1811,7 @@ class CallActivity : CallBaseActivity() { fetchSignalingSettings() } + @Suppress("Detekt.NestedBlockDepth") @Subscribe(threadMode = ThreadMode.BACKGROUND) fun onMessageEvent(webSocketCommunicationEvent: WebSocketCommunicationEvent) { if (currentCallStatus === CallStatus.LEAVING) { diff --git a/app/src/main/java/com/nextcloud/talk/receivers/DeclineCallReceiver.kt b/app/src/main/java/com/nextcloud/talk/receivers/DeclineCallReceiver.kt index f67544368b5..e3e278611c4 100644 --- a/app/src/main/java/com/nextcloud/talk/receivers/DeclineCallReceiver.kt +++ b/app/src/main/java/com/nextcloud/talk/receivers/DeclineCallReceiver.kt @@ -1,11 +1,8 @@ /* * Nextcloud Talk - Android Client * - * - * SPDX-FileCopyrightText: 2026 Jens Zalzala * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: GPL-3.0-or-later - * */ package com.nextcloud.talk.receivers