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/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 5da4d1fc8da..9a7da58faf8 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..2954fea0d25 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 @@ -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 @@ -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 @@ -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) @@ -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) + } } if (extras.containsKey(KEY_IS_BREAKOUT_ROOM)) { isBreakoutRoom = extras.getBoolean(KEY_IS_BREAKOUT_ROOM) @@ -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 } @@ -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() } @@ -1809,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/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..10b086452c6 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.DeclineCallReceiver 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,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 + val primaryAnswerIntent = if (isVideoCall) answerVideoPendingIntent else answerVoicePendingIntent + val notification = NotificationCompat.Builder(applicationContext, notificationChannelId) .setPriority(NotificationCompat.PRIORITY_HIGH) @@ -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 @@ -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 { override fun onSubscribe(d: Disposable) { // unused atm @@ -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 } } diff --git a/app/src/main/java/com/nextcloud/talk/receivers/DeclineCallReceiver.kt b/app/src/main/java/com/nextcloud/talk/receivers/DeclineCallReceiver.kt new file mode 100644 index 00000000000..e3e278611c4 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/receivers/DeclineCallReceiver.kt @@ -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) + } +}