Skip to content

Commit 7112cb7

Browse files
arlexTechmahibi
authored andcommitted
- Refactoring addBubble for clarity and reducing nesting
- Refactoring createConversationBubble to use best practices, keeping ChatActivity.kt simple - Refactoring NotificationWorker functions related to bubbling to now properly follow the builder pattern - better error handling of edge cases Signed-off-by: rapterjet2004 <juliuslinus1@gmail.com>
1 parent bdc6e57 commit 7112cb7

16 files changed

Lines changed: 1475 additions & 112 deletions

app/src/main/AndroidManifest.xml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,13 @@
173173
android:name=".chat.ChatActivity"
174174
android:theme="@style/AppTheme" />
175175

176+
<activity
177+
android:name=".chat.BubbleActivity"
178+
android:theme="@style/AppTheme"
179+
android:allowEmbedded="true"
180+
android:resizeableActivity="true"
181+
android:documentLaunchMode="always" />
182+
176183
<activity
177184
android:name=".activities.CallActivity"
178185
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation"
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
/*
2+
* Nextcloud Talk - Android Client
3+
*
4+
* SPDX-FileCopyrightText: 2025 Alexandre Wery <nextcloud-talk-android@alwy.be>
5+
* SPDX-License-Identifier: GPL-3.0-or-later
6+
*/
7+
8+
package com.nextcloud.talk.chat
9+
10+
import android.content.Context
11+
import android.content.Intent
12+
import android.os.Bundle
13+
import androidx.activity.OnBackPressedCallback
14+
import com.nextcloud.talk.R
15+
import com.nextcloud.talk.activities.MainActivity
16+
import com.nextcloud.talk.utils.bundle.BundleKeys
17+
18+
class BubbleActivity : ChatActivity() {
19+
20+
override fun onCreate(savedInstanceState: Bundle?) {
21+
super.onCreate(savedInstanceState)
22+
supportActionBar?.setDisplayHomeAsUpEnabled(true)
23+
supportActionBar?.setHomeAsUpIndicator(R.drawable.ic_talk)
24+
supportActionBar?.setDisplayShowHomeEnabled(true)
25+
findViewById<androidx.appcompat.widget.Toolbar>(R.id.chat_toolbar)?.setNavigationOnClickListener {
26+
openConversationList()
27+
}
28+
29+
onBackPressedDispatcher.addCallback(
30+
this,
31+
object : OnBackPressedCallback(true) {
32+
override fun handleOnBackPressed() {
33+
moveTaskToBack(false)
34+
}
35+
}
36+
)
37+
}
38+
39+
override fun onPrepareOptionsMenu(menu: android.view.Menu): Boolean {
40+
super.onPrepareOptionsMenu(menu)
41+
42+
menu.findItem(R.id.create_conversation_bubble)?.isVisible = false
43+
menu.findItem(R.id.open_conversation_in_app)?.isVisible = true
44+
45+
return true
46+
}
47+
48+
override fun onOptionsItemSelected(item: android.view.MenuItem): Boolean =
49+
when (item.itemId) {
50+
R.id.open_conversation_in_app -> {
51+
openInMainApp()
52+
true
53+
}
54+
android.R.id.home -> {
55+
openConversationList()
56+
true
57+
}
58+
else -> super.onOptionsItemSelected(item)
59+
}
60+
61+
private fun openInMainApp() {
62+
val intent = Intent(this, MainActivity::class.java).apply {
63+
action = Intent.ACTION_MAIN
64+
addCategory(Intent.CATEGORY_LAUNCHER)
65+
putExtras(this@BubbleActivity.intent)
66+
conversationUser?.id?.let { putExtra(BundleKeys.KEY_INTERNAL_USER_ID, it) }
67+
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
68+
}
69+
startActivity(intent)
70+
}
71+
72+
private fun openConversationList() {
73+
val intent = Intent(this, MainActivity::class.java).apply {
74+
action = Intent.ACTION_MAIN
75+
addCategory(Intent.CATEGORY_LAUNCHER)
76+
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
77+
}
78+
startActivity(intent)
79+
}
80+
81+
@Deprecated("Deprecated in Java")
82+
override fun onSupportNavigateUp(): Boolean {
83+
openInMainApp()
84+
return true
85+
}
86+
87+
companion object {
88+
fun newIntent(context: Context, roomToken: String, conversationName: String?): Intent =
89+
Intent(context, BubbleActivity::class.java).apply {
90+
putExtra(BundleKeys.KEY_ROOM_TOKEN, roomToken)
91+
conversationName?.let { putExtra(BundleKeys.KEY_CONVERSATION_NAME, it) }
92+
action = Intent.ACTION_VIEW
93+
flags = Intent.FLAG_ACTIVITY_NEW_DOCUMENT or Intent.FLAG_ACTIVITY_MULTIPLE_TASK
94+
}
95+
}
96+
}

app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt

Lines changed: 104 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
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>
@@ -145,6 +146,7 @@ import com.nextcloud.talk.models.json.threads.ThreadInfo
145146
import com.nextcloud.talk.polls.ui.PollCreateDialogFragment
146147
import com.nextcloud.talk.polls.ui.PollMainDialogFragment
147148
import com.nextcloud.talk.remotefilebrowser.activities.RemoteFileBrowserActivity
149+
import com.nextcloud.talk.settings.SettingsActivity
148150
import com.nextcloud.talk.shareditems.activities.SharedItemsActivity
149151
import com.nextcloud.talk.signaling.SignalingMessageReceiver
150152
import com.nextcloud.talk.signaling.SignalingMessageSender
@@ -197,6 +199,7 @@ import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_START_CALL_AFTER_ROOM_SWIT
197199
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_SWITCH_TO_ROOM
198200
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_THREAD_ID
199201
import com.nextcloud.talk.utils.permissions.PlatformPermissionUtil
202+
import com.nextcloud.talk.utils.preferences.preferencestorage.DatabaseStorageModule
200203
import com.nextcloud.talk.utils.rx.DisposableSet
201204
import com.nextcloud.talk.utils.singletons.ApplicationWideCurrentRoomHolder
202205
import com.nextcloud.talk.webrtc.WebSocketConnectionHelper
@@ -232,7 +235,7 @@ import kotlin.math.roundToInt
232235

233236
@Suppress("TooManyFunctions", "LargeClass", "LongMethod")
234237
@AutoInjector(NextcloudTalkApplication::class)
235-
class ChatActivity :
238+
open class ChatActivity :
236239
BaseActivity(),
237240
CallStartedMessageInterface {
238241

@@ -2356,11 +2359,14 @@ class ChatActivity :
23562359
)
23572360
}
23582361

2359-
private fun showConversationInfoScreen() {
2362+
private fun showConversationInfoScreen(focusBubbleSwitch: Boolean = false) {
23602363
val bundle = Bundle()
23612364

23622365
bundle.putString(KEY_ROOM_TOKEN, roomToken)
23632366
bundle.putBoolean(BundleKeys.KEY_ROOM_ONE_TO_ONE, isOneToOneConversation())
2367+
if (focusBubbleSwitch) {
2368+
bundle.putBoolean(BundleKeys.KEY_FOCUS_CONVERSATION_BUBBLE, true)
2369+
}
23642370

23652371
val upcomingEvent =
23662372
(chatViewModel.upcomingEventViewState.value as? ChatViewModel.UpcomingEventUIState.Success)?.event
@@ -2373,15 +2379,22 @@ class ChatActivity :
23732379
startActivity(intent)
23742380
}
23752381

2382+
private fun openBubbleSettings() {
2383+
val intent = Intent(this, SettingsActivity::class.java)
2384+
intent.putExtra(BundleKeys.KEY_FOCUS_BUBBLE_SETTINGS, true)
2385+
startActivity(intent)
2386+
}
2387+
23762388
private fun validSessionId(): Boolean =
23772389
currentConversation != null &&
23782390
sessionIdAfterRoomJoined?.isNotEmpty() == true &&
23792391
sessionIdAfterRoomJoined != "0"
23802392

23812393
@Suppress("Detekt.TooGenericExceptionCaught")
2382-
private fun cancelNotificationsForCurrentConversation() {
2394+
protected open fun cancelNotificationsForCurrentConversation() {
2395+
val isBubbleMode = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && isLaunchedFromBubble
23832396
if (conversationUser != null) {
2384-
if (!TextUtils.isEmpty(roomToken)) {
2397+
if (!TextUtils.isEmpty(roomToken) && !isBubbleMode) {
23852398
try {
23862399
NotificationUtils.cancelExistingNotificationsForRoom(
23872400
applicationContext,
@@ -2717,10 +2730,11 @@ class ChatActivity :
27172730
showThreadsItem.isVisible = !isChatThread() &&
27182731
hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.THREADS)
27192732

2720-
if (CapabilitiesUtil.isAbleToCall(spreedCapabilities) &&
2721-
!isChatThread() &&
2722-
!ConversationUtils.isNoteToSelfConversation(currentConversation)
2723-
) {
2733+
val createBubbleItem = menu.findItem(R.id.create_conversation_bubble)
2734+
createBubbleItem.isVisible = Build.VERSION.SDK_INT >= Build.VERSION_CODES.R &&
2735+
!isChatThread()
2736+
2737+
if (CapabilitiesUtil.isAbleToCall(spreedCapabilities) && !isChatThread()) {
27242738
conversationVoiceCallMenuItem = menu.findItem(R.id.conversation_voice_call)
27252739
conversationVideoMenuItem = menu.findItem(R.id.conversation_video_call)
27262740

@@ -2752,6 +2766,8 @@ class ChatActivity :
27522766
menu.removeItem(R.id.conversation_voice_call)
27532767
}
27542768

2769+
menu.findItem(R.id.create_conversation_bubble)?.isVisible = NotificationUtils.deviceSupportsBubbles
2770+
27552771
handleThreadNotificationIcon(menu.findItem(R.id.thread_notifications))
27562772
}
27572773
return true
@@ -2762,7 +2778,7 @@ class ChatActivity :
27622778
hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.THREADS)
27632779

27642780
val threadNotificationIcon = when (conversationThreadInfo?.attendee?.notificationLevel) {
2765-
NOTIFICATION_LEVEL_ALWAYS -> R.drawable.outline_notifications_active_24
2781+
NOTIFICATION_LEVEL_DEFAULT -> R.drawable.outline_notifications_active_24
27662782
NOTIFICATION_LEVEL_NEVER -> R.drawable.ic_baseline_notifications_off_24
27672783
else -> R.drawable.baseline_notifications_24
27682784
}
@@ -2822,6 +2838,11 @@ class ChatActivity :
28222838
true
28232839
}
28242840

2841+
R.id.create_conversation_bubble -> {
2842+
createConversationBubble()
2843+
true
2844+
}
2845+
28252846
else -> super.onOptionsItemSelected(item)
28262847
}
28272848

@@ -2902,6 +2923,73 @@ class ChatActivity :
29022923
)
29032924
}
29042925

2926+
@Suppress("ReturnCount")
2927+
private fun createConversationBubble() {
2928+
if (!NotificationUtils.deviceSupportsBubbles) {
2929+
Log.e(
2930+
TAG,
2931+
"createConversationBubble was called but device doesn't support it. It should not be possible " +
2932+
"to get here via UI!"
2933+
)
2934+
return
2935+
}
2936+
2937+
if (!appPreferences.areBubblesEnabled() || !NotificationUtils.areSystemBubblesEnabled(context)) {
2938+
// Do not replace with snackbar as it needs to survive screen change
2939+
Toast.makeText(
2940+
context,
2941+
getString(R.string.nc_conversation_notification_bubble_disabled),
2942+
Toast.LENGTH_SHORT
2943+
).show()
2944+
openBubbleSettings()
2945+
return
2946+
}
2947+
2948+
if (!appPreferences.areBubblesForced() && !isConversationBubbleEnabled()) {
2949+
// Do not replace with snackbar as it needs to survive screen change
2950+
Toast.makeText(
2951+
context,
2952+
getString(R.string.nc_conversation_notification_bubble_enable_conversation),
2953+
Toast.LENGTH_SHORT
2954+
).show()
2955+
showConversationInfoScreen(focusBubbleSwitch = true)
2956+
return
2957+
}
2958+
2959+
val conversationName = currentConversation?.displayName ?: getString(R.string.nc_app_name)
2960+
currentConversation?.let {
2961+
val bubbleInfo = NotificationUtils.BubbleInfo(
2962+
roomToken = roomToken,
2963+
conversationRemoteId = it.name,
2964+
conversationName = conversationName,
2965+
conversationUser = conversationUser,
2966+
isOneToOneConversation = isOneToOneConversation(),
2967+
credentials = credentials
2968+
)
2969+
2970+
NotificationUtils.createConversationBubble(
2971+
context = context,
2972+
bubbleInfo = bubbleInfo,
2973+
appPreferences = appPreferences,
2974+
lifecycleScope
2975+
)
2976+
}
2977+
}
2978+
2979+
private fun isConversationBubbleEnabled(): Boolean =
2980+
runCatching {
2981+
DatabaseStorageModule(conversationUser, roomToken).getBoolean(BUBBLE_SWITCH_KEY, false)
2982+
}.onFailure { e ->
2983+
when (e) {
2984+
is IOException -> Log.e(TAG, "Failed to read conversation bubble preference: IO error", e)
2985+
is IllegalStateException -> Log.e(
2986+
TAG,
2987+
"Failed to read conversation bubble preference: Invalid state",
2988+
e
2989+
)
2990+
}
2991+
}.getOrDefault(false)
2992+
29052993
@Suppress("Detekt.LongMethod")
29062994
private fun showThreadNotificationMenu() {
29072995
fun setThreadNotificationLevel(level: Int) {
@@ -3601,8 +3689,8 @@ class ChatActivity :
36013689
displayName = currentConversation?.displayName ?: ""
36023690
)
36033691
showSnackBar(roomToken)
3604-
} catch (e: Exception) {
3605-
Log.w(TAG, "File corresponding to the uri does not exist $shareUri", e)
3692+
} catch (e: IOException) {
3693+
Log.w(TAG, "File corresponding to the uri does not exist: IO error $shareUri", e)
36063694
downloadFileToCache(message, false) {
36073695
uploadFile(
36083696
fileUri = shareUri.toString(),
@@ -3955,12 +4043,13 @@ class ChatActivity :
39554043
private const val HTTP_FORBIDDEN = 403
39564044
private const val HTTP_NOT_FOUND = 404
39574045
private const val MESSAGE_PULL_LIMIT = 100
4046+
private const val NOTIFICATION_LEVEL_DEFAULT = 1
4047+
private const val NOTIFICATION_LEVEL_NEVER = 3
4048+
private const val NOTIFICATION_LEVEL_ALWAYS = 1
4049+
private const val NOTIFICATION_LEVEL_MENTION_AND_CALLS = 2
39584050
private const val INVITE_LENGTH = 6
39594051
private const val ACTOR_LENGTH = 6
39604052
private const val CHUNK_SIZE: Int = 10
3961-
private const val NOTIFICATION_LEVEL_ALWAYS = 1
3962-
private const val NOTIFICATION_LEVEL_MENTION_AND_CALLS = 2
3963-
private const val NOTIFICATION_LEVEL_NEVER = 3
39644053
private const val ONE_SECOND_IN_MILLIS = 1000
39654054
private const val WHITESPACE = " "
39664055
private const val COMMA = ", "
@@ -3973,6 +4062,7 @@ class ChatActivity :
39734062
private const val CURRENT_AUDIO_POSITION_KEY = "CURRENT_AUDIO_POSITION"
39744063
private const val CURRENT_AUDIO_WAS_PLAYING_KEY = "CURRENT_AUDIO_PLAYING"
39754064
private const val RESUME_AUDIO_TAG = "RESUME_AUDIO_TAG"
4065+
private const val BUBBLE_SWITCH_KEY = "bubble_switch"
39764066
private const val FIVE_MINUTES_IN_SECONDS: Long = 300
39774067
private const val ROOM_TYPE_ONE_TO_ONE = "1"
39784068
private const val ACTOR_TYPE = "users"

0 commit comments

Comments
 (0)