11/*
22 * Nextcloud Talk - Android Client
33 *
4+ * SPDX-FileCopyrightText: 2025 Alexandre Wery <nextcloud-talk-android@alwy.be>
45 * SPDX-FileCopyrightText: 2024 Christian Reiner <foss@christian-reiner.info>
56 * SPDX-FileCopyrightText: 2024 Parneet Singh <gurayaparneet@gmail.com>
67 * SPDX-FileCopyrightText: 2024 Giacomo Pacini <giacomo@paciosoft.com>
@@ -146,6 +147,7 @@ import com.nextcloud.talk.models.json.threads.ThreadInfo
146147import com.nextcloud.talk.polls.ui.PollCreateDialogFragment
147148import com.nextcloud.talk.polls.ui.PollMainDialogFragment
148149import com.nextcloud.talk.remotefilebrowser.activities.RemoteFileBrowserActivity
150+ import com.nextcloud.talk.settings.SettingsActivity
149151import com.nextcloud.talk.shareditems.activities.SharedItemsActivity
150152import com.nextcloud.talk.signaling.SignalingMessageReceiver
151153import com.nextcloud.talk.signaling.SignalingMessageSender
@@ -198,6 +200,7 @@ import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_START_CALL_AFTER_ROOM_SWIT
198200import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_SWITCH_TO_ROOM
199201import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_THREAD_ID
200202import com.nextcloud.talk.utils.permissions.PlatformPermissionUtil
203+ import com.nextcloud.talk.utils.preferences.preferencestorage.DatabaseStorageModule
201204import com.nextcloud.talk.utils.rx.DisposableSet
202205import com.nextcloud.talk.utils.singletons.ApplicationWideCurrentRoomHolder
203206import com.nextcloud.talk.webrtc.WebSocketConnectionHelper
@@ -233,7 +236,7 @@ import kotlin.math.roundToInt
233236
234237@Suppress(" TooManyFunctions" , " LargeClass" , " LongMethod" )
235238@AutoInjector(NextcloudTalkApplication ::class )
236- class ChatActivity :
239+ open class ChatActivity :
237240 BaseActivity (),
238241 CallStartedMessageInterface {
239242
@@ -2374,11 +2377,14 @@ class ChatActivity :
23742377 )
23752378 }
23762379
2377- private fun showConversationInfoScreen () {
2380+ private fun showConversationInfoScreen (focusBubbleSwitch : Boolean = false ) {
23782381 val bundle = Bundle ()
23792382
23802383 bundle.putString(KEY_ROOM_TOKEN , roomToken)
23812384 bundle.putBoolean(BundleKeys .KEY_ROOM_ONE_TO_ONE , isOneToOneConversation())
2385+ if (focusBubbleSwitch) {
2386+ bundle.putBoolean(BundleKeys .KEY_FOCUS_CONVERSATION_BUBBLE , true )
2387+ }
23822388
23832389 val upcomingEvent =
23842390 (chatViewModel.upcomingEventViewState.value as ? ChatViewModel .UpcomingEventUIState .Success )?.event
@@ -2391,15 +2397,22 @@ class ChatActivity :
23912397 startActivity(intent)
23922398 }
23932399
2400+ private fun openBubbleSettings () {
2401+ val intent = Intent (this , SettingsActivity ::class .java)
2402+ intent.putExtra(BundleKeys .KEY_FOCUS_BUBBLE_SETTINGS , true )
2403+ startActivity(intent)
2404+ }
2405+
23942406 private fun validSessionId (): Boolean =
23952407 currentConversation != null &&
23962408 sessionIdAfterRoomJoined?.isNotEmpty() == true &&
23972409 sessionIdAfterRoomJoined != " 0"
23982410
23992411 @Suppress(" Detekt.TooGenericExceptionCaught" )
2400- private fun cancelNotificationsForCurrentConversation () {
2412+ protected open fun cancelNotificationsForCurrentConversation () {
2413+ val isBubbleMode = Build .VERSION .SDK_INT >= Build .VERSION_CODES .S && isLaunchedFromBubble
24012414 if (conversationUser != null ) {
2402- if (! TextUtils .isEmpty(roomToken)) {
2415+ if (! TextUtils .isEmpty(roomToken) && ! isBubbleMode ) {
24032416 try {
24042417 NotificationUtils .cancelExistingNotificationsForRoom(
24052418 applicationContext,
@@ -2735,10 +2748,11 @@ class ChatActivity :
27352748 showThreadsItem.isVisible = ! isChatThread() &&
27362749 hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures .THREADS )
27372750
2738- if (CapabilitiesUtil .isAbleToCall(spreedCapabilities) &&
2739- ! isChatThread() &&
2740- ! ConversationUtils .isNoteToSelfConversation(currentConversation)
2741- ) {
2751+ val createBubbleItem = menu.findItem(R .id.create_conversation_bubble)
2752+ createBubbleItem.isVisible = Build .VERSION .SDK_INT >= Build .VERSION_CODES .R &&
2753+ ! isChatThread()
2754+
2755+ if (CapabilitiesUtil .isAbleToCall(spreedCapabilities) && ! isChatThread()) {
27422756 conversationVoiceCallMenuItem = menu.findItem(R .id.conversation_voice_call)
27432757 conversationVideoMenuItem = menu.findItem(R .id.conversation_video_call)
27442758
@@ -2770,6 +2784,8 @@ class ChatActivity :
27702784 menu.removeItem(R .id.conversation_voice_call)
27712785 }
27722786
2787+ menu.findItem(R .id.create_conversation_bubble)?.isVisible = NotificationUtils .deviceSupportsBubbles
2788+
27732789 handleThreadNotificationIcon(menu.findItem(R .id.thread_notifications))
27742790 }
27752791 return true
@@ -2840,6 +2856,11 @@ class ChatActivity :
28402856 true
28412857 }
28422858
2859+ R .id.create_conversation_bubble -> {
2860+ createConversationBubble()
2861+ true
2862+ }
2863+
28432864 else -> super .onOptionsItemSelected(item)
28442865 }
28452866
@@ -2920,6 +2941,73 @@ class ChatActivity :
29202941 )
29212942 }
29222943
2944+ @Suppress(" ReturnCount" )
2945+ private fun createConversationBubble () {
2946+ if (! NotificationUtils .deviceSupportsBubbles) {
2947+ Log .e(
2948+ TAG ,
2949+ " createConversationBubble was called but device doesn't support it. It should not be possible " +
2950+ " to get here via UI!"
2951+ )
2952+ return
2953+ }
2954+
2955+ if (! appPreferences.areBubblesEnabled() || ! NotificationUtils .areSystemBubblesEnabled(context)) {
2956+ // Do not replace with snackbar as it needs to survive screen change
2957+ Toast .makeText(
2958+ context,
2959+ getString(R .string.nc_conversation_notification_bubble_disabled),
2960+ Toast .LENGTH_SHORT
2961+ ).show()
2962+ openBubbleSettings()
2963+ return
2964+ }
2965+
2966+ if (! appPreferences.areBubblesForced() && ! isConversationBubbleEnabled()) {
2967+ // Do not replace with snackbar as it needs to survive screen change
2968+ Toast .makeText(
2969+ context,
2970+ getString(R .string.nc_conversation_notification_bubble_enable_conversation),
2971+ Toast .LENGTH_SHORT
2972+ ).show()
2973+ showConversationInfoScreen(focusBubbleSwitch = true )
2974+ return
2975+ }
2976+
2977+ val conversationName = currentConversation?.displayName ? : getString(R .string.nc_app_name)
2978+ currentConversation?.let {
2979+ val bubbleInfo = NotificationUtils .BubbleInfo (
2980+ roomToken = roomToken,
2981+ conversationRemoteId = it.name,
2982+ conversationName = conversationName,
2983+ conversationUser = conversationUser,
2984+ isOneToOneConversation = isOneToOneConversation(),
2985+ credentials = credentials
2986+ )
2987+
2988+ NotificationUtils .createConversationBubble(
2989+ context = context,
2990+ bubbleInfo = bubbleInfo,
2991+ appPreferences = appPreferences,
2992+ lifecycleScope
2993+ )
2994+ }
2995+ }
2996+
2997+ private fun isConversationBubbleEnabled (): Boolean =
2998+ runCatching {
2999+ DatabaseStorageModule (conversationUser, roomToken).getBoolean(BUBBLE_SWITCH_KEY , false )
3000+ }.onFailure { e ->
3001+ when (e) {
3002+ is IOException -> Log .e(TAG , " Failed to read conversation bubble preference: IO error" , e)
3003+ is IllegalStateException -> Log .e(
3004+ TAG ,
3005+ " Failed to read conversation bubble preference: Invalid state" ,
3006+ e
3007+ )
3008+ }
3009+ }.getOrDefault(false )
3010+
29233011 @Suppress(" Detekt.LongMethod" )
29243012 private fun showThreadNotificationMenu () {
29253013 fun setThreadNotificationLevel (level : Int ) {
@@ -3538,7 +3626,6 @@ class ChatActivity :
35383626
35393627 if (noteToSelfConversation != null ) {
35403628 var shareUri: Uri ? = null
3541- val data: HashMap <String , String >?
35423629 var metaData = " "
35433630 var objectId = " "
35443631 if (message.hasFileAttachment) {
@@ -3608,8 +3695,8 @@ class ChatActivity :
36083695 displayName = currentConversation?.displayName ? : " "
36093696 )
36103697 showSnackBar(roomToken)
3611- } catch (e: Exception ) {
3612- Log .w(TAG , " File corresponding to the uri does not exist $shareUri " , e)
3698+ } catch (e: IOException ) {
3699+ Log .w(TAG , " File corresponding to the uri does not exist: IO error $shareUri " , e)
36133700 downloadFileToCache(message, false ) {
36143701 uploadFile(
36153702 fileUri = shareUri.toString(),
@@ -3962,6 +4049,7 @@ class ChatActivity :
39624049 private const val HTTP_FORBIDDEN = 403
39634050 private const val HTTP_NOT_FOUND = 404
39644051 private const val MESSAGE_PULL_LIMIT = 100
4052+ private const val NOTIFICATION_LEVEL_DEFAULT = 1
39654053 private const val INVITE_LENGTH = 6
39664054 private const val ACTOR_LENGTH = 6
39674055 private const val CHUNK_SIZE : Int = 10
@@ -3980,6 +4068,7 @@ class ChatActivity :
39804068 private const val CURRENT_AUDIO_POSITION_KEY = " CURRENT_AUDIO_POSITION"
39814069 private const val CURRENT_AUDIO_WAS_PLAYING_KEY = " CURRENT_AUDIO_PLAYING"
39824070 private const val RESUME_AUDIO_TAG = " RESUME_AUDIO_TAG"
4071+ private const val BUBBLE_SWITCH_KEY = " bubble_switch"
39834072 private const val FIVE_MINUTES_IN_SECONDS : Long = 300
39844073 private const val ROOM_TYPE_ONE_TO_ONE = " 1"
39854074 private const val ACTOR_TYPE = " users"
0 commit comments