Skip to content

Commit 90a4fa3

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 Signed-off-by: rapterjet2004 <juliuslinus1@gmail.com>
1 parent 8a6ad4c commit 90a4fa3

File tree

16 files changed

+1456
-97
lines changed

16 files changed

+1456
-97
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>,
@@ -2826,11 +2829,14 @@ class ChatActivity :
28262829
)
28272830
}
28282831

2829-
private fun showConversationInfoScreen() {
2832+
private fun showConversationInfoScreen(focusBubbleSwitch: Boolean = false) {
28302833
val bundle = Bundle()
28312834

28322835
bundle.putString(KEY_ROOM_TOKEN, roomToken)
28332836
bundle.putBoolean(BundleKeys.KEY_ROOM_ONE_TO_ONE, isOneToOneConversation())
2837+
if (focusBubbleSwitch) {
2838+
bundle.putBoolean(BundleKeys.KEY_FOCUS_CONVERSATION_BUBBLE, true)
2839+
}
28342840

28352841
val upcomingEvent =
28362842
(chatViewModel.upcomingEventViewState.value as? ChatViewModel.UpcomingEventUIState.Success)?.event
@@ -2843,15 +2849,22 @@ class ChatActivity :
28432849
startActivity(intent)
28442850
}
28452851

2852+
private fun openBubbleSettings() {
2853+
val intent = Intent(this, SettingsActivity::class.java)
2854+
intent.putExtra(BundleKeys.KEY_FOCUS_BUBBLE_SETTINGS, true)
2855+
startActivity(intent)
2856+
}
2857+
28462858
private fun validSessionId(): Boolean =
28472859
currentConversation != null &&
28482860
sessionIdAfterRoomJoined?.isNotEmpty() == true &&
28492861
sessionIdAfterRoomJoined != "0"
28502862

28512863
@Suppress("Detekt.TooGenericExceptionCaught")
2852-
private fun cancelNotificationsForCurrentConversation() {
2864+
protected open fun cancelNotificationsForCurrentConversation() {
2865+
val isBubbleMode = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && isLaunchedFromBubble
28532866
if (conversationUser != null) {
2854-
if (!TextUtils.isEmpty(roomToken)) {
2867+
if (!TextUtils.isEmpty(roomToken) && !isBubbleMode) {
28552868
try {
28562869
NotificationUtils.cancelExistingNotificationsForRoom(
28572870
applicationContext,
@@ -3464,10 +3477,11 @@ class ChatActivity :
34643477
showThreadsItem.isVisible = !isChatThread() &&
34653478
hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.THREADS)
34663479

3467-
if (CapabilitiesUtil.isAbleToCall(spreedCapabilities) &&
3468-
!isChatThread() &&
3469-
!ConversationUtils.isNoteToSelfConversation(currentConversation)
3470-
) {
3480+
val createBubbleItem = menu.findItem(R.id.create_conversation_bubble)
3481+
createBubbleItem.isVisible = Build.VERSION.SDK_INT >= Build.VERSION_CODES.R &&
3482+
!isChatThread()
3483+
3484+
if (CapabilitiesUtil.isAbleToCall(spreedCapabilities) && !isChatThread()) {
34713485
conversationVoiceCallMenuItem = menu.findItem(R.id.conversation_voice_call)
34723486
conversationVideoMenuItem = menu.findItem(R.id.conversation_video_call)
34733487

@@ -3499,6 +3513,8 @@ class ChatActivity :
34993513
menu.removeItem(R.id.conversation_voice_call)
35003514
}
35013515

3516+
menu.findItem(R.id.create_conversation_bubble)?.isVisible = NotificationUtils.deviceSupportsBubbles
3517+
35023518
handleThreadNotificationIcon(menu.findItem(R.id.thread_notifications))
35033519
}
35043520
return true
@@ -3509,8 +3525,8 @@ class ChatActivity :
35093525
hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.THREADS)
35103526

35113527
val threadNotificationIcon = when (conversationThreadInfo?.attendee?.notificationLevel) {
3512-
1 -> R.drawable.outline_notifications_active_24
3513-
3 -> R.drawable.ic_baseline_notifications_off_24
3528+
NOTIFICATION_LEVEL_DEFAULT -> R.drawable.outline_notifications_active_24
3529+
NOTIFICATION_LEVEL_NEVER -> R.drawable.ic_baseline_notifications_off_24
35143530
else -> R.drawable.baseline_notifications_24
35153531
}
35163532
threadNotificationItem.icon = ContextCompat.getDrawable(context, threadNotificationIcon)
@@ -3569,6 +3585,11 @@ class ChatActivity :
35693585
true
35703586
}
35713587

3588+
R.id.create_conversation_bubble -> {
3589+
createConversationBubble()
3590+
true
3591+
}
3592+
35723593
else -> super.onOptionsItemSelected(item)
35733594
}
35743595

@@ -3649,6 +3670,73 @@ class ChatActivity :
36493670
)
36503671
}
36513672

3673+
@Suppress("ReturnCount")
3674+
private fun createConversationBubble() {
3675+
if (!NotificationUtils.deviceSupportsBubbles) {
3676+
Log.e(
3677+
TAG,
3678+
"createConversationBubble was called but device doesn't support it. It should not be possible " +
3679+
"to get here via UI!"
3680+
)
3681+
return
3682+
}
3683+
3684+
if (!appPreferences.areBubblesEnabled() || !NotificationUtils.areSystemBubblesEnabled(context)) {
3685+
// Do not replace with snackbar as it needs to survive screen change
3686+
Toast.makeText(
3687+
context,
3688+
getString(R.string.nc_conversation_notification_bubble_disabled),
3689+
Toast.LENGTH_SHORT
3690+
).show()
3691+
openBubbleSettings()
3692+
return
3693+
}
3694+
3695+
if (!appPreferences.areBubblesForced() && !isConversationBubbleEnabled()) {
3696+
// Do not replace with snackbar as it needs to survive screen change
3697+
Toast.makeText(
3698+
context,
3699+
getString(R.string.nc_conversation_notification_bubble_enable_conversation),
3700+
Toast.LENGTH_SHORT
3701+
).show()
3702+
showConversationInfoScreen(focusBubbleSwitch = true)
3703+
return
3704+
}
3705+
3706+
val conversationName = currentConversation?.displayName ?: getString(R.string.nc_app_name)
3707+
currentConversation?.let {
3708+
val bubbleInfo = NotificationUtils.BubbleInfo(
3709+
roomToken = roomToken,
3710+
conversationRemoteId = it.name,
3711+
conversationName = conversationName,
3712+
conversationUser = conversationUser,
3713+
isOneToOneConversation = isOneToOneConversation(),
3714+
credentials = credentials
3715+
)
3716+
3717+
NotificationUtils.createConversationBubble(
3718+
context = context,
3719+
bubbleInfo = bubbleInfo,
3720+
appPreferences = appPreferences,
3721+
lifecycleScope
3722+
)
3723+
}
3724+
}
3725+
3726+
private fun isConversationBubbleEnabled(): Boolean =
3727+
runCatching {
3728+
DatabaseStorageModule(conversationUser, roomToken).getBoolean(BUBBLE_SWITCH_KEY, false)
3729+
}.onFailure { e ->
3730+
when (e) {
3731+
is IOException -> Log.e(TAG, "Failed to read conversation bubble preference: IO error", e)
3732+
is IllegalStateException -> Log.e(
3733+
TAG,
3734+
"Failed to read conversation bubble preference: Invalid state",
3735+
e
3736+
)
3737+
}
3738+
}.isSuccess
3739+
36523740
@Suppress("Detekt.LongMethod")
36533741
private fun showThreadNotificationMenu() {
36543742
fun setThreadNotificationLevel(level: Int) {
@@ -3703,7 +3791,7 @@ class ChatActivity :
37033791
subtitle = null,
37043792
icon = R.drawable.ic_baseline_notifications_off_24,
37053793
onClick = {
3706-
setThreadNotificationLevel(3)
3794+
setThreadNotificationLevel(NOTIFICATION_LEVEL_NEVER)
37073795
}
37083796
)
37093797
)
@@ -4412,8 +4500,8 @@ class ChatActivity :
44124500
displayName = currentConversation?.displayName ?: ""
44134501
)
44144502
showSnackBar(roomToken)
4415-
} catch (e: Exception) {
4416-
Log.w(TAG, "File corresponding to the uri does not exist $shareUri", e)
4503+
} catch (e: IOException) {
4504+
Log.w(TAG, "File corresponding to the uri does not exist: IO error $shareUri", e)
44174505
downloadFileToCache(message, false) {
44184506
uploadFile(
44194507
fileUri = shareUri.toString(),
@@ -4899,6 +4987,8 @@ class ChatActivity :
48994987
private const val HTTP_FORBIDDEN = 403
49004988
private const val HTTP_NOT_FOUND = 404
49014989
private const val MESSAGE_PULL_LIMIT = 100
4990+
private const val NOTIFICATION_LEVEL_DEFAULT = 1
4991+
private const val NOTIFICATION_LEVEL_NEVER = 3
49024992
private const val INVITE_LENGTH = 6
49034993
private const val ACTOR_LENGTH = 6
49044994
private const val CHUNK_SIZE: Int = 10
@@ -4914,6 +5004,7 @@ class ChatActivity :
49145004
private const val CURRENT_AUDIO_POSITION_KEY = "CURRENT_AUDIO_POSITION"
49155005
private const val CURRENT_AUDIO_WAS_PLAYING_KEY = "CURRENT_AUDIO_PLAYING"
49165006
private const val RESUME_AUDIO_TAG = "RESUME_AUDIO_TAG"
5007+
private const val BUBBLE_SWITCH_KEY = "bubble_switch"
49175008
private const val FIVE_MINUTES_IN_SECONDS: Long = 300
49185009
private const val ROOM_TYPE_ONE_TO_ONE = "1"
49195010
private const val ACTOR_TYPE = "users"

0 commit comments

Comments
 (0)