Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,7 @@
<receiver android:name=".receivers.MarkAsReadReceiver" />
<receiver android:name=".receivers.DismissRecordingAvailableReceiver" />
<receiver android:name=".receivers.ShareRecordingToChatReceiver" />
<receiver android:name=".receivers.IncomingCallDeclineReceiver" />

<service
android:name=".utils.SyncService"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ class CallNotificationActivity : CallBaseActivity() {
private var isOneToOneCall: Boolean = true
private var conversationName: String? = null
private var internalUserId: Long = -1
private var answerFromNotification: Boolean = false

private var userBeingCalled: User? = null
private var leavingScreen = false
Expand All @@ -81,6 +82,7 @@ class CallNotificationActivity : CallBaseActivity() {
binding!!.conversationNameTextView.text = displayName
setupAvatar(isOneToOneCall, conversationName)
initClickListeners()
maybeAutoAnswerFromNotification()
setupNotificationCanceledRoutine()
}

Expand All @@ -93,6 +95,7 @@ class CallNotificationActivity : CallBaseActivity() {
isOneToOneCall = extras.getBoolean(KEY_ROOM_ONE_TO_ONE)
conversationName = extras.getString(BundleKeys.KEY_CONVERSATION_NAME, "")
internalUserId = extras.getLong(BundleKeys.KEY_INTERNAL_USER_ID)
answerFromNotification = extras.containsKey(KEY_CALL_VOICE_ONLY)
}

private fun setupAvatar(isOneToOneCall: Boolean, conversationName: String?) {
Expand Down Expand Up @@ -196,6 +199,13 @@ class CallNotificationActivity : CallBaseActivity() {
startActivity(callIntent)
}

private fun maybeAutoAnswerFromNotification() {
if (!answerFromNotification) {
return
}
proceedToCall()
}

private fun isInCallWithVideo(callFlag: Int): Boolean = (callFlag and Participant.InCallFlags.WITH_VIDEO) > 0

override fun onStop() {
Expand Down
81 changes: 64 additions & 17 deletions app/src/main/java/com/nextcloud/talk/jobs/NotificationWorker.kt
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ import com.nextcloud.talk.models.json.push.DecryptedPushMessage
import com.nextcloud.talk.models.json.push.NotificationUser
import com.nextcloud.talk.receivers.DirectReplyReceiver
import com.nextcloud.talk.receivers.DismissRecordingAvailableReceiver
import com.nextcloud.talk.receivers.IncomingCallDeclineReceiver
import com.nextcloud.talk.receivers.MarkAsReadReceiver
import com.nextcloud.talk.receivers.ShareRecordingToChatReceiver
import com.nextcloud.talk.users.UserManager
Expand All @@ -76,6 +77,7 @@ import com.nextcloud.talk.utils.bundle.BundleKeys
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_DISMISS_RECORDING_URL
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_FROM_NOTIFICATION_START_CALL
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_INTERNAL_USER_ID
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_CALL_VOICE_ONLY
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_MESSAGE_ID
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_NOTIFICATION_ID
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_NOTIFICATION_RESTRICT_DELETION
Expand Down Expand Up @@ -261,28 +263,73 @@ class NotificationWorker(context: Context, workerParams: WorkerParameters) : Wor
}
)

val answerIntent = Intent(context, CallNotificationActivity::class.java).apply {
putExtras(bundle)
putExtra(KEY_CALL_VOICE_ONLY, !isInCallWithVideo(conversation.callFlag))
flags = getIntentFlags()
}
val answerPendingIntent = PendingIntent.getActivity(
context,
requestCode + 1,
answerIntent,
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
} else {
PendingIntent.FLAG_UPDATE_CURRENT
}
)

val declineIntent = Intent(context, IncomingCallDeclineReceiver::class.java).apply {
putExtra(KEY_NOTIFICATION_TIMESTAMP, pushMessage.timestamp.toInt())
}
val declinePendingIntent = PendingIntent.getBroadcast(
context,
requestCode + 2,
declineIntent,
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
} else {
PendingIntent.FLAG_UPDATE_CURRENT
}
)

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 notification =
NotificationCompat.Builder(applicationContext, notificationChannelId)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setCategory(NotificationCompat.CATEGORY_CALL)
.setSmallIcon(R.drawable.ic_call_black_24dp)
.setSubText(baseUrl)
.setShowWhen(true)
.setWhen(pushMessage.timestamp)
.setContentTitle(EmojiCompat.get().process(pushMessage.subject))
// auto cancel is set to false because notification (including sound) should continue while
// CallNotificationActivity is active
.setAutoCancel(false)
.setOngoing(true)
.setContentIntent(fullScreenPendingIntent)
.setFullScreenIntent(fullScreenPendingIntent, true)
.setSound(soundUri)
.build()
val caller = Person.Builder()
.setName(EmojiCompat.get().process(pushMessage.subject))
.setImportant(true)
.build()

val builder = NotificationCompat.Builder(applicationContext, notificationChannelId)
.setPriority(NotificationCompat.PRIORITY_MAX)
.setCategory(NotificationCompat.CATEGORY_CALL)
.setSmallIcon(R.drawable.ic_call_black_24dp)
.setSubText(baseUrl)
.setShowWhen(true)
.setWhen(pushMessage.timestamp)
.setContentTitle(EmojiCompat.get().process(pushMessage.subject))
// auto cancel is set to false because notification (including sound) should continue while
// CallNotificationActivity is active
.setAutoCancel(false)
.setOngoing(true)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setContentIntent(fullScreenPendingIntent)
.setFullScreenIntent(fullScreenPendingIntent, true)
.setSound(soundUri)
.setColorized(true)

builder.setStyle(
NotificationCompat.CallStyle.forIncomingCall(
caller,
declinePendingIntent,
answerPendingIntent
)
)

val notification = builder.build()
notification.flags = notification.flags or Notification.FLAG_INSISTENT

sendNotification(pushMessage.timestamp.toInt(), notification)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2026 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 IncomingCallDeclineReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val notificationId = intent.getIntExtra(KEY_NOTIFICATION_TIMESTAMP, -1)
if (notificationId == -1) {
return
}
NotificationManagerCompat.from(context).cancel(notificationId)
}
}