Skip to content

Commit 6ed3553

Browse files
arlexTechrapterjet2004
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 93cd1c3 commit 6ed3553

File tree

16 files changed

+1467
-110
lines changed

16 files changed

+1467
-110
lines changed

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 & 13 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>
@@ -165,6 +166,7 @@ import com.nextcloud.talk.models.json.signaling.settings.SignalingSettingsOveral
165166
import com.nextcloud.talk.models.json.threads.ThreadInfo
166167
import com.nextcloud.talk.polls.ui.PollCreateDialogFragment
167168
import com.nextcloud.talk.remotefilebrowser.activities.RemoteFileBrowserActivity
169+
import com.nextcloud.talk.settings.SettingsActivity
168170
import com.nextcloud.talk.shareditems.activities.SharedItemsActivity
169171
import com.nextcloud.talk.signaling.SignalingMessageReceiver
170172
import com.nextcloud.talk.signaling.SignalingMessageSender
@@ -217,6 +219,7 @@ import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_START_CALL_AFTER_ROOM_SWIT
217219
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_SWITCH_TO_ROOM
218220
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_THREAD_ID
219221
import com.nextcloud.talk.utils.permissions.PlatformPermissionUtil
222+
import com.nextcloud.talk.utils.preferences.preferencestorage.DatabaseStorageModule
220223
import com.nextcloud.talk.utils.rx.DisposableSet
221224
import com.nextcloud.talk.utils.singletons.ApplicationWideCurrentRoomHolder
222225
import com.nextcloud.talk.webrtc.WebSocketConnectionHelper
@@ -260,7 +263,7 @@ import kotlin.math.roundToInt
260263

261264
@Suppress("TooManyFunctions", "LargeClass", "LongMethod")
262265
@AutoInjector(NextcloudTalkApplication::class)
263-
class ChatActivity :
266+
open class ChatActivity :
264267
BaseActivity(),
265268
MessagesListAdapter.OnLoadMoreListener,
266269
MessagesListAdapter.Formatter<Date>,
@@ -2814,11 +2817,14 @@ class ChatActivity :
28142817
)
28152818
}
28162819

2817-
private fun showConversationInfoScreen() {
2820+
private fun showConversationInfoScreen(focusBubbleSwitch: Boolean = false) {
28182821
val bundle = Bundle()
28192822

28202823
bundle.putString(KEY_ROOM_TOKEN, roomToken)
28212824
bundle.putBoolean(BundleKeys.KEY_ROOM_ONE_TO_ONE, isOneToOneConversation())
2825+
if (focusBubbleSwitch) {
2826+
bundle.putBoolean(BundleKeys.KEY_FOCUS_CONVERSATION_BUBBLE, true)
2827+
}
28222828

28232829
val upcomingEvent =
28242830
(chatViewModel.upcomingEventViewState.value as? ChatViewModel.UpcomingEventUIState.Success)?.event
@@ -2831,15 +2837,22 @@ class ChatActivity :
28312837
startActivity(intent)
28322838
}
28332839

2840+
private fun openBubbleSettings() {
2841+
val intent = Intent(this, SettingsActivity::class.java)
2842+
intent.putExtra(BundleKeys.KEY_FOCUS_BUBBLE_SETTINGS, true)
2843+
startActivity(intent)
2844+
}
2845+
28342846
private fun validSessionId(): Boolean =
28352847
currentConversation != null &&
28362848
sessionIdAfterRoomJoined?.isNotEmpty() == true &&
28372849
sessionIdAfterRoomJoined != "0"
28382850

28392851
@Suppress("Detekt.TooGenericExceptionCaught")
2840-
private fun cancelNotificationsForCurrentConversation() {
2852+
protected open fun cancelNotificationsForCurrentConversation() {
2853+
val isBubbleMode = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && isLaunchedFromBubble
28412854
if (conversationUser != null) {
2842-
if (!TextUtils.isEmpty(roomToken)) {
2855+
if (!TextUtils.isEmpty(roomToken) && !isBubbleMode) {
28432856
try {
28442857
NotificationUtils.cancelExistingNotificationsForRoom(
28452858
applicationContext,
@@ -3452,10 +3465,11 @@ class ChatActivity :
34523465
showThreadsItem.isVisible = !isChatThread() &&
34533466
hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.THREADS)
34543467

3455-
if (CapabilitiesUtil.isAbleToCall(spreedCapabilities) &&
3456-
!isChatThread() &&
3457-
!ConversationUtils.isNoteToSelfConversation(currentConversation)
3458-
) {
3468+
val createBubbleItem = menu.findItem(R.id.create_conversation_bubble)
3469+
createBubbleItem.isVisible = Build.VERSION.SDK_INT >= Build.VERSION_CODES.R &&
3470+
!isChatThread()
3471+
3472+
if (CapabilitiesUtil.isAbleToCall(spreedCapabilities) && !isChatThread()) {
34593473
conversationVoiceCallMenuItem = menu.findItem(R.id.conversation_voice_call)
34603474
conversationVideoMenuItem = menu.findItem(R.id.conversation_video_call)
34613475

@@ -3487,6 +3501,8 @@ class ChatActivity :
34873501
menu.removeItem(R.id.conversation_voice_call)
34883502
}
34893503

3504+
menu.findItem(R.id.create_conversation_bubble)?.isVisible = NotificationUtils.deviceSupportsBubbles
3505+
34903506
handleThreadNotificationIcon(menu.findItem(R.id.thread_notifications))
34913507
}
34923508
return true
@@ -3497,8 +3513,8 @@ class ChatActivity :
34973513
hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.THREADS)
34983514

34993515
val threadNotificationIcon = when (conversationThreadInfo?.attendee?.notificationLevel) {
3500-
1 -> R.drawable.outline_notifications_active_24
3501-
3 -> R.drawable.ic_baseline_notifications_off_24
3516+
NOTIFICATION_LEVEL_DEFAULT -> R.drawable.outline_notifications_active_24
3517+
NOTIFICATION_LEVEL_NEVER -> R.drawable.ic_baseline_notifications_off_24
35023518
else -> R.drawable.baseline_notifications_24
35033519
}
35043520
threadNotificationItem.icon = ContextCompat.getDrawable(context, threadNotificationIcon)
@@ -3557,6 +3573,11 @@ class ChatActivity :
35573573
true
35583574
}
35593575

3576+
R.id.create_conversation_bubble -> {
3577+
createConversationBubble()
3578+
true
3579+
}
3580+
35603581
else -> super.onOptionsItemSelected(item)
35613582
}
35623583

@@ -3637,6 +3658,73 @@ class ChatActivity :
36373658
)
36383659
}
36393660

3661+
@Suppress("ReturnCount")
3662+
private fun createConversationBubble() {
3663+
if (!NotificationUtils.deviceSupportsBubbles) {
3664+
Log.e(
3665+
TAG,
3666+
"createConversationBubble was called but device doesn't support it. It should not be possible " +
3667+
"to get here via UI!"
3668+
)
3669+
return
3670+
}
3671+
3672+
if (!appPreferences.areBubblesEnabled() || !NotificationUtils.areSystemBubblesEnabled(context)) {
3673+
// Do not replace with snackbar as it needs to survive screen change
3674+
Toast.makeText(
3675+
context,
3676+
getString(R.string.nc_conversation_notification_bubble_disabled),
3677+
Toast.LENGTH_SHORT
3678+
).show()
3679+
openBubbleSettings()
3680+
return
3681+
}
3682+
3683+
if (!appPreferences.areBubblesForced() && !isConversationBubbleEnabled()) {
3684+
// Do not replace with snackbar as it needs to survive screen change
3685+
Toast.makeText(
3686+
context,
3687+
getString(R.string.nc_conversation_notification_bubble_enable_conversation),
3688+
Toast.LENGTH_SHORT
3689+
).show()
3690+
showConversationInfoScreen(focusBubbleSwitch = true)
3691+
return
3692+
}
3693+
3694+
val conversationName = currentConversation?.displayName ?: getString(R.string.nc_app_name)
3695+
currentConversation?.let {
3696+
val bubbleInfo = NotificationUtils.BubbleInfo(
3697+
roomToken = roomToken,
3698+
conversationRemoteId = it.name,
3699+
conversationName = conversationName,
3700+
conversationUser = conversationUser,
3701+
isOneToOneConversation = isOneToOneConversation(),
3702+
credentials = credentials
3703+
)
3704+
3705+
NotificationUtils.createConversationBubble(
3706+
context = context,
3707+
bubbleInfo = bubbleInfo,
3708+
appPreferences = appPreferences,
3709+
lifecycleScope
3710+
)
3711+
}
3712+
}
3713+
3714+
private fun isConversationBubbleEnabled(): Boolean =
3715+
runCatching {
3716+
DatabaseStorageModule(conversationUser, roomToken).getBoolean(BUBBLE_SWITCH_KEY, false)
3717+
}.onFailure { e ->
3718+
when (e) {
3719+
is IOException -> Log.e(TAG, "Failed to read conversation bubble preference: IO error", e)
3720+
is IllegalStateException -> Log.e(
3721+
TAG,
3722+
"Failed to read conversation bubble preference: Invalid state",
3723+
e
3724+
)
3725+
}
3726+
}.getOrDefault(false)
3727+
36403728
@Suppress("Detekt.LongMethod")
36413729
private fun showThreadNotificationMenu() {
36423730
fun setThreadNotificationLevel(level: Int) {
@@ -3691,7 +3779,7 @@ class ChatActivity :
36913779
subtitle = null,
36923780
icon = R.drawable.ic_baseline_notifications_off_24,
36933781
onClick = {
3694-
setThreadNotificationLevel(3)
3782+
setThreadNotificationLevel(NOTIFICATION_LEVEL_NEVER)
36953783
}
36963784
)
36973785
)
@@ -4400,8 +4488,8 @@ class ChatActivity :
44004488
displayName = currentConversation?.displayName ?: ""
44014489
)
44024490
showSnackBar(roomToken)
4403-
} catch (e: Exception) {
4404-
Log.w(TAG, "File corresponding to the uri does not exist $shareUri", e)
4491+
} catch (e: IOException) {
4492+
Log.w(TAG, "File corresponding to the uri does not exist: IO error $shareUri", e)
44054493
downloadFileToCache(message, false) {
44064494
uploadFile(
44074495
fileUri = shareUri.toString(),
@@ -4887,6 +4975,8 @@ class ChatActivity :
48874975
private const val HTTP_FORBIDDEN = 403
48884976
private const val HTTP_NOT_FOUND = 404
48894977
private const val MESSAGE_PULL_LIMIT = 100
4978+
private const val NOTIFICATION_LEVEL_DEFAULT = 1
4979+
private const val NOTIFICATION_LEVEL_NEVER = 3
48904980
private const val INVITE_LENGTH = 6
48914981
private const val ACTOR_LENGTH = 6
48924982
private const val CHUNK_SIZE: Int = 10
@@ -4902,6 +4992,7 @@ class ChatActivity :
49024992
private const val CURRENT_AUDIO_POSITION_KEY = "CURRENT_AUDIO_POSITION"
49034993
private const val CURRENT_AUDIO_WAS_PLAYING_KEY = "CURRENT_AUDIO_PLAYING"
49044994
private const val RESUME_AUDIO_TAG = "RESUME_AUDIO_TAG"
4995+
private const val BUBBLE_SWITCH_KEY = "bubble_switch"
49054996
private const val FIVE_MINUTES_IN_SECONDS: Long = 300
49064997
private const val ROOM_TYPE_ONE_TO_ONE = "1"
49074998
private const val ACTOR_TYPE = "users"

0 commit comments

Comments
 (0)