Skip to content
Merged
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 AUTHORS.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
* hidrohase <github@honesz.net>
* Jan-Christoph Borchardt <hey@jancborchardt.net>
* jld3103 <jld3103yt@gmail.com>
* Jens Zalzala <jens@shakingearthdigital.com>
* Joas Schilling <coding@schilljs.com>
* John Molakvoæ <skjnldsv@protonmail.com>
* Jos Poortvliet <jos@opensuse.org>
Expand Down
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.DeclineCallReceiver" android:exported="false" />
<receiver android:name=".receivers.DismissRecordingAvailableReceiver" />
<receiver android:name=".receivers.ShareRecordingToChatReceiver" />

Expand Down
23 changes: 13 additions & 10 deletions app/src/main/java/com/nextcloud/talk/activities/CallActivity.kt
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 @@ -107,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
Expand All @@ -118,6 +118,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 @@ -371,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)
Expand Down Expand Up @@ -553,6 +554,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 Expand Up @@ -1031,7 +1037,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
}
Expand Down Expand Up @@ -1582,13 +1591,6 @@ class CallActivity : CallBaseActivity() {
}
ApplicationWideCurrentRoomHolder.getInstance().isInCall = true
ApplicationWideCurrentRoomHolder.getInstance().isDialing = false
if (!TextUtils.isEmpty(roomToken)) {
cancelExistingNotificationsForRoom(
applicationContext,
conversationUser!!,
roomToken!!
)
}
if (!hasExternalSignalingServer) {
pullSignalingMessages()
}
Expand Down Expand Up @@ -1809,6 +1811,7 @@ class CallActivity : CallBaseActivity() {
fetchSignalingSettings()
}

@Suppress("Detekt.NestedBlockDepth")
@Subscribe(threadMode = ThreadMode.BACKGROUND)
fun onMessageEvent(webSocketCommunicationEvent: WebSocketCommunicationEvent) {
if (currentCallStatus === CallStatus.LEAVING) {
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
67 changes: 65 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.DeclineCallReceiver
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,65 @@ 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, DeclineCallReceiver::class.java).apply {
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 +344,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 +359,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 +1167,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,21 @@
/*
* Nextcloud Talk - Android Client
*
* 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 DeclineCallReceiver : BroadcastReceiver() {

override fun onReceive(context: Context, intent: Intent) {
val notificationId = intent.getIntExtra(KEY_NOTIFICATION_TIMESTAMP, 0)
NotificationManagerCompat.from(context).cancel(notificationId)
}
}
Loading