From 68474559c8db567a3365cbe7117c89a5aab6bac9 Mon Sep 17 00:00:00 2001 From: Marcel Hibbe Date: Wed, 9 Sep 2026 15:15:37 +0200 Subject: [PATCH 1/3] fix(call): warn instead of blocking app when server has E2EE calls enabled Android previously showed a generic "outdated app" error whenever the server had end-to-end-encryption for calls enabled, since the server blocks unsupported client versions entirely. E2EE calling is not yet implemented on Android, so instead of blocking the whole app, detect the server's call E2EE capability and only warn the user when a call is actually started or answered, leaving the rest of the app usable. Mirrors the approach from talk-ios#2369. Assisted-by: Claude Code:claude-sonnet-5 Signed-off-by: Marcel Hibbe --- .../nextcloud/talk/activities/CallActivity.kt | 13 ++++++++ .../CallNotificationActivity.kt | 7 ++++ .../com/nextcloud/talk/chat/ChatActivity.kt | 4 +++ .../ConversationsListActivity.kt | 4 +++ .../nextcloud/talk/jobs/NotificationWorker.kt | 33 ++++++++++++++++++- .../nextcloud/talk/utils/CapabilitiesUtil.kt | 11 +++++++ app/src/main/res/values/strings.xml | 2 ++ 7 files changed, 73 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 00c6187549b..8988006e512 100644 --- a/app/src/main/java/com/nextcloud/talk/activities/CallActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/activities/CallActivity.kt @@ -455,6 +455,10 @@ class CallActivity : CallBaseActivity() { processExtras(intent.extras!!) conversationUser = currentUserProviderOld.currentUser.blockingGet() + if (warnAndFinishIfCallEndToEndEncryptionUnsupported()) { + return + } + credentials = ApiUtils.getCredentials(conversationUser!!.username, conversationUser!!.token) if (TextUtils.isEmpty(baseUrl)) { baseUrl = conversationUser!!.baseUrl @@ -477,6 +481,15 @@ class CallActivity : CallBaseActivity() { checkInitialDevicePermissions() } + private fun warnAndFinishIfCallEndToEndEncryptionUnsupported(): Boolean { + if (!CapabilitiesUtil.isCallEndToEndEncryptionEnabled(conversationUser?.capabilities?.spreedCapability)) { + return false + } + Toast.makeText(context, R.string.nc_call_e2ee_not_supported, Toast.LENGTH_LONG).show() + finish() + return true + } + private fun initCallRecordingViewModel(recordingState: Int) { callRecordingViewModel = ViewModelProvider(this, viewModelFactory).get( CallRecordingViewModel::class.java diff --git a/app/src/main/java/com/nextcloud/talk/callnotification/CallNotificationActivity.kt b/app/src/main/java/com/nextcloud/talk/callnotification/CallNotificationActivity.kt index aaad05b3aef..abcd3d9d022 100644 --- a/app/src/main/java/com/nextcloud/talk/callnotification/CallNotificationActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/callnotification/CallNotificationActivity.kt @@ -15,6 +15,7 @@ import android.os.Handler import android.os.Looper import android.util.Log import android.view.View +import android.widget.Toast import androidx.core.app.NotificationManagerCompat import autodagger.AutoInjector import com.nextcloud.talk.R @@ -29,6 +30,7 @@ import com.nextcloud.talk.extensions.loadUserAvatar import com.nextcloud.talk.models.json.participants.Participant import com.nextcloud.talk.users.UserManager import com.nextcloud.talk.utils.ApiUtils +import com.nextcloud.talk.utils.CapabilitiesUtil import com.nextcloud.talk.utils.CapabilitiesUtil.hasSpreedFeatureCapability import com.nextcloud.talk.utils.NotificationUtils import com.nextcloud.talk.utils.SpreedFeatures @@ -190,6 +192,11 @@ class CallNotificationActivity : CallBaseActivity() { } private fun proceedToCall() { + if (CapabilitiesUtil.isCallEndToEndEncryptionEnabled(userBeingCalled?.capabilities?.spreedCapability)) { + Toast.makeText(context, R.string.nc_call_e2ee_not_supported, Toast.LENGTH_LONG).show() + hangup() + return + } val callIntent = Intent(this, CallActivity::class.java) intent.putExtra(KEY_ROOM_ONE_TO_ONE, isOneToOneCall) callIntent.putExtras(intent.extras!!) diff --git a/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt b/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt index 0b174c37857..71a21eaaa15 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt @@ -3437,6 +3437,10 @@ class ChatActivity : private fun startACall(isVoiceOnlyCall: Boolean, callWithoutNotification: Boolean) { currentConversation?.let { if (::conversationUser.isInitialized) { + if (CapabilitiesUtil.isCallEndToEndEncryptionEnabled(spreedCapabilities)) { + Snackbar.make(binding.root, R.string.nc_call_e2ee_not_supported, Snackbar.LENGTH_LONG).show() + return + } val pp = ParticipantPermissions(spreedCapabilities, it) if (!pp.canStartCall() && currentConversation?.hasCall == false) { Snackbar.make(binding.root, R.string.startCallForbidden, Snackbar.LENGTH_LONG).show() diff --git a/app/src/main/java/com/nextcloud/talk/conversationlist/ConversationsListActivity.kt b/app/src/main/java/com/nextcloud/talk/conversationlist/ConversationsListActivity.kt index 059bba60338..006ebcf55e9 100644 --- a/app/src/main/java/com/nextcloud/talk/conversationlist/ConversationsListActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/conversationlist/ConversationsListActivity.kt @@ -1100,6 +1100,10 @@ class ConversationsListActivity : BaseActivity() { chatIntent.putExtras(bundle) if (currentUser != null) { + if (CapabilitiesUtil.isCallEndToEndEncryptionEnabled(currentUser?.capabilities?.spreedCapability)) { + showSnackbar(context.getString(R.string.nc_call_e2ee_not_supported)) + return@let + } val pp = ParticipantPermissions(currentUser?.capabilities?.spreedCapability, it) if (!pp.canStartCall() && selectedConversation?.hasCall == false) { Log.e(TAG, "Error starting call from conversations list: call is forbidden") 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 86cb13d8388..0d1f4928b38 100644 --- a/app/src/main/java/com/nextcloud/talk/jobs/NotificationWorker.kt +++ b/app/src/main/java/com/nextcloud/talk/jobs/NotificationWorker.kt @@ -76,6 +76,7 @@ import com.nextcloud.talk.receivers.ShareRecordingToChatReceiver import com.nextcloud.talk.users.UserManager import com.nextcloud.talk.utils.ActorAvatar import com.nextcloud.talk.utils.ApiUtils +import com.nextcloud.talk.utils.CapabilitiesUtil import com.nextcloud.talk.utils.CharacterAvatarUtils import com.nextcloud.talk.utils.ConversationUtils import com.nextcloud.talk.utils.DisplayUtils @@ -404,7 +405,14 @@ class NotificationWorker(context: Context, workerParams: WorkerParameters) : Wor override fun onNext(conversation: ConversationModel) { if (userManager.setUserAsActive(userBeingCalled!!).blockingGet()) { - prepareCallNotificationScreen(conversation) + if (CapabilitiesUtil.isCallEndToEndEncryptionEnabled( + userBeingCalled?.capabilities?.spreedCapability + ) + ) { + showEndToEndEncryptionUnsupportedNotification(conversation) + } else { + prepareCallNotificationScreen(conversation) + } } } @@ -1299,6 +1307,29 @@ class NotificationWorker(context: Context, workerParams: WorkerParameters) : Wor } } + private fun showEndToEndEncryptionUnsupportedNotification(conversation: ConversationModel) { + val notificationBuilder = NotificationCompat.Builder( + context!!, + NotificationUtils.NotificationChannels + .NOTIFICATION_CHANNEL_MESSAGES_V4.name + ) + + val intent = createMainActivityIntent() + + val notification: Notification = notificationBuilder + .setContentTitle(context!!.resources.getString(R.string.nc_call_e2ee_not_supported_title)) + .setContentText(context!!.resources.getString(R.string.nc_call_e2ee_not_supported)) + .setSmallIcon(R.drawable.ic_call_black_24dp) + .setOngoing(false) + .setAutoCancel(true) + .setPriority(NotificationCompat.PRIORITY_LOW) + .setContentIntent(createUniquePendingIntent(intent)) + .build() + + sendNotification(pushMessage.timestamp.toInt(), notification) + Log.d(TAG, "'end-to-end-encryption not supported' notification was created for ${conversation.token}") + } + private fun createMainActivityIntent(): Intent { val intent = Intent(context, MainActivity::class.java) intent.flags = getIntentFlags() diff --git a/app/src/main/java/com/nextcloud/talk/utils/CapabilitiesUtil.kt b/app/src/main/java/com/nextcloud/talk/utils/CapabilitiesUtil.kt index 5448fbed8dc..04ab84f0359 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/CapabilitiesUtil.kt +++ b/app/src/main/java/com/nextcloud/talk/utils/CapabilitiesUtil.kt @@ -327,6 +327,17 @@ object CapabilitiesUtil { fun isBanningAvailable(spreedCapabilities: SpreedCapability): Boolean = hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.BAN_V1) + fun isCallEndToEndEncryptionEnabled(spreedCapabilities: SpreedCapability?): Boolean { + if ( + spreedCapabilities?.config?.containsKey("call") == true && + spreedCapabilities.config!!["call"] != null && + spreedCapabilities.config!!["call"]!!.containsKey("end-to-end-encryption") + ) { + return spreedCapabilities.config!!["call"]!!["end-to-end-encryption"].toString().toBoolean() + } + return false + } + // endregion //region SpreedCapabilities that can't be used with federation as the settings for them are global diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9daacee5f3e..ebc44441459 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -641,6 +641,8 @@ How to translate with transifex: Keep Message deleted successfully, but it might have been leaked to other services You are not allowed to start a call + Calling unavailable + Calling is currently not supported because end-to-end encryption is enabled on the server You need to promote a new moderator before you can leave the conversation Room is retained successfully From 4461e8c994446faedb75ecf55f55030417b347d4 Mon Sep 17 00:00:00 2001 From: Marcel Hibbe Date: Wed, 9 Sep 2026 16:34:33 +0200 Subject: [PATCH 2/3] fix(call): include conversation name in E2EE-unsupported notification The heads-up notification shown for an incoming call that can't be answered because the server has call E2EE enabled previously used a generic title. Include the conversation name so the user can tell which conversation the call came from. Assisted-by: Claude Code:claude-sonnet-5 Signed-off-by: Marcel Hibbe --- .../java/com/nextcloud/talk/jobs/NotificationWorker.kt | 7 ++++++- app/src/main/res/values/strings.xml | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) 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 0d1f4928b38..4358cf0d83d 100644 --- a/app/src/main/java/com/nextcloud/talk/jobs/NotificationWorker.kt +++ b/app/src/main/java/com/nextcloud/talk/jobs/NotificationWorker.kt @@ -1317,7 +1317,12 @@ class NotificationWorker(context: Context, workerParams: WorkerParameters) : Wor val intent = createMainActivityIntent() val notification: Notification = notificationBuilder - .setContentTitle(context!!.resources.getString(R.string.nc_call_e2ee_not_supported_title)) + .setContentTitle( + String.format( + context!!.resources.getString(R.string.nc_call_e2ee_not_supported_title), + conversation.displayName + ) + ) .setContentText(context!!.resources.getString(R.string.nc_call_e2ee_not_supported)) .setSmallIcon(R.drawable.ic_call_black_24dp) .setOngoing(false) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ebc44441459..85b4dadbfbc 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -641,7 +641,7 @@ How to translate with transifex: Keep Message deleted successfully, but it might have been leaked to other services You are not allowed to start a call - Calling unavailable + Call from %s unavailable Calling is currently not supported because end-to-end encryption is enabled on the server You need to promote a new moderator before you can leave the conversation Room is retained successfully From 2a0ce0b8856a865f4cdc30050f6140db2c1ee553 Mon Sep 17 00:00:00 2001 From: Andy Scherzinger Date: Wed, 9 Sep 2026 20:04:34 +0200 Subject: [PATCH 3/3] fix(chat): drop the unused ContextCompat import The emoji picker removal left the import behind, which fails ktlint. Assisted-by: Claude Code:claude-opus-5 Signed-off-by: Andy Scherzinger --- .../main/java/com/nextcloud/talk/chat/MessageInputFragment.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/main/java/com/nextcloud/talk/chat/MessageInputFragment.kt b/app/src/main/java/com/nextcloud/talk/chat/MessageInputFragment.kt index 51dc3d0c75c..220b95633a1 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/MessageInputFragment.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/MessageInputFragment.kt @@ -37,7 +37,6 @@ import android.widget.RelativeLayout import android.widget.SeekBar import androidx.appcompat.view.ContextThemeWrapper import androidx.compose.material3.MaterialTheme -import androidx.core.content.ContextCompat import androidx.core.graphics.drawable.toDrawable import androidx.core.view.isVisible import androidx.core.widget.doAfterTextChanged