Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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 @@ -293,6 +293,7 @@

<receiver android:name=".receivers.DirectReplyReceiver" />
<receiver android:name=".receivers.MarkAsReadReceiver" />
<receiver android:name=".receivers.CallNotificationActionReceiver" android:exported="false" />
<receiver android:name=".receivers.DismissRecordingAvailableReceiver" />
<receiver android:name=".receivers.ShareRecordingToChatReceiver" />

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should work and fix the issue with the ongoing ringing.

however it should have been covered by the call of cancelExistingNotificationsForRoom method in performCall, but there seems to be a bug that this is not triggered sometimes.
I will continue debugging in the evening or tomorrow to sort things out..

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i have some ideas what could go wrong, but it's not settled.

To simplify things, i suggest to keep your approach, but to remove the block


                            if (!TextUtils.isEmpty(roomToken)) {
                                cancelExistingNotificationsForRoom(
                                    applicationContext,
                                    conversationUser!!,
                                    roomToken!!
                                )
                            }

from the function getRoomAndContinue completely.
So even when other things go wrong, the ringing should definitely stop.

Just let me know if you want to do this or if i should take over from here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mahibi feel free to take over

}
}
if (extras.containsKey(KEY_IS_BREAKOUT_ROOM)) {
isBreakoutRoom = extras.getBoolean(KEY_IS_BREAKOUT_ROOM)
Expand Down
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is what was causing the lock screen to appear after answering a call.

Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
Expand Down
68 changes: 66 additions & 2 deletions app/src/main/java/com/nextcloud/talk/jobs/NotificationWorker.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Afaik call flags are still like this:
It's not possible to know if the caller has video enabled. The video flag is set if there is the possibility to use video, regardless if it's enabled or not.
So this will most of the times result in showing the video button even when the caller has no video enabled.

Would be less annoying if #708 would be solved.

@SystemKeeper @Ivansss how is this done on iOS?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In my testing it worked as intended:
If I initiate a video call, the video button shows, and if I initiate an audio call, it doesn't.

That may not handle edge cases, like the caller turning off their video, or switching from audio to video, but I think the most common use case is handled.

I'll wait to hear how it's handled on iOS though.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For iOS it's the same behavior that a call is offered as videocall.
However on iOS the camera is not activated automatically in this case but you have to enable it yourself. We should do the same.

In the prepareCall() method you find that onCameraClick() is called.
Could you please wrap this with the following check so it's only automatically enabled when the callscreen is not opened via a notification?:

            if (!isIncomingCallFromNotification) {
                onCameraClick()
            }

I think this is the expected behavior for most people.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in be0bd8d

I will say, however, that I personally don't like this behavior. I'm not used to having to click an extra button to enable video for video calls. I think a setting would be nice for this. I can create an issue for it after this is merged.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand for some people it's not the expected behavior but as it's privacy related we should disable it initially.
I asked my team and they also voted to disable it.

Some background: the call flag just tells that there is a device existing that could transmit video. It does not say video is enabled. So most of the time the called person will see "incoming video call", but actually the other participant does not have video enabled. In this case, most people will be confused to unveil their own video after accepting the call but don't the the other participants video.

val primaryAnswerIntent = if (isVideoCall) answerVideoPendingIntent else answerVoicePendingIntent

val notification =
NotificationCompat.Builder(applicationContext, notificationChannelId)
.setPriority(NotificationCompat.PRIORITY_HIGH)
Expand All @@ -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

Expand All @@ -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<ConversationModel> {
override fun onSubscribe(d: Disposable) {
// unused atm
Expand Down Expand Up @@ -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
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
* Nextcloud Talk - Android Client
*
*
* SPDX-FileCopyrightText: 2026 Jens Zalzala <jens@shakingearthdigital.com>
* 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() {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i suggest
DeclineCallReceiver
as this is the only usecase

Copy link
Copy Markdown
Contributor Author

@anakin78z anakin78z Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Initially I had it set up to add an additional action to the call notification to always give the option to answer with video or audio, but the way the extra action displayed I found it a bit cramped and inconsistent, so I removed it. We could, however, decide to add additional actions in the future, so I left this receiver more generic.

You can see how the action is being added on line 304 of NotificationWorker.kt: https://github.com/nextcloud/talk-android/pull/6015/changes/BASE..dcde0be993b7ee9733cdca5fa275c13b5b778caa#diff-b51bc8baa586c30bbd846a44639cba62c95cde0f586adb76db389f9e74635914R304

I can change the name if you don't think we'll add other actions. Or I could add some documentation to the Receiver explaining how it could be extended.

Edit: Actually, answering voice/video wouldn't have used this receiver. So it would have to be an unrelated action, and the only thing I can think of right now is maybe sending a text response instead of answering. But it's probably unlikely that we'd use this for anything other than declining calls for now.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guessed it was a leftover from other approaches.
I'd suggest to go with DeclineCallReceiver.
Then you could also delete the ACTION_DECLINE_CALL action.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 04606e3


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"
}
}
Loading