Skip to content

Commit 8f46d7b

Browse files
arlexTechrapterjet2004
authored andcommitted
- Refactoring addBubble for clarity and reducing nesting
- Refactoring createConversationBubble to use best practices, keeping ChatActivity.kt simple Signed-off-by: rapterjet2004 <juliuslinus1@gmail.com>
1 parent 6799085 commit 8f46d7b

16 files changed

Lines changed: 1407 additions & 82 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: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
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(this, object : OnBackPressedCallback(true) {
30+
override fun handleOnBackPressed() {
31+
moveTaskToBack(false)
32+
}
33+
})
34+
}
35+
36+
override fun onPrepareOptionsMenu(menu: android.view.Menu): Boolean {
37+
super.onPrepareOptionsMenu(menu)
38+
39+
menu.findItem(R.id.create_conversation_bubble)?.isVisible = false
40+
menu.findItem(R.id.open_conversation_in_app)?.isVisible = true
41+
42+
return true
43+
}
44+
45+
override fun onOptionsItemSelected(item: android.view.MenuItem): Boolean =
46+
when (item.itemId) {
47+
R.id.open_conversation_in_app -> {
48+
openInMainApp()
49+
true
50+
}
51+
android.R.id.home -> {
52+
openConversationList()
53+
true
54+
}
55+
else -> super.onOptionsItemSelected(item)
56+
}
57+
58+
private fun openInMainApp() {
59+
val intent = Intent(this, MainActivity::class.java).apply {
60+
action = Intent.ACTION_MAIN
61+
addCategory(Intent.CATEGORY_LAUNCHER)
62+
putExtras(this@BubbleActivity.intent)
63+
conversationUser?.id?.let { putExtra(BundleKeys.KEY_INTERNAL_USER_ID, it) }
64+
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
65+
}
66+
startActivity(intent)
67+
}
68+
69+
private fun openConversationList() {
70+
val intent = Intent(this, MainActivity::class.java).apply {
71+
action = Intent.ACTION_MAIN
72+
addCategory(Intent.CATEGORY_LAUNCHER)
73+
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
74+
}
75+
startActivity(intent)
76+
}
77+
78+
@Deprecated("Deprecated in Java")
79+
override fun onSupportNavigateUp(): Boolean {
80+
openInMainApp()
81+
return true
82+
}
83+
84+
companion object {
85+
fun newIntent(context: Context, roomToken: String, conversationName: String?): Intent =
86+
Intent(context, BubbleActivity::class.java).apply {
87+
putExtra(BundleKeys.KEY_ROOM_TOKEN, roomToken)
88+
conversationName?.let { putExtra(BundleKeys.KEY_CONVERSATION_NAME, it) }
89+
action = Intent.ACTION_VIEW
90+
flags = Intent.FLAG_ACTIVITY_NEW_DOCUMENT or Intent.FLAG_ACTIVITY_MULTIPLE_TASK
91+
}
92+
}
93+
}

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

Lines changed: 101 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>,
@@ -2827,11 +2830,14 @@ class ChatActivity :
28272830
)
28282831
}
28292832

2830-
private fun showConversationInfoScreen() {
2833+
private fun showConversationInfoScreen(focusBubbleSwitch: Boolean = false) {
28312834
val bundle = Bundle()
28322835

28332836
bundle.putString(KEY_ROOM_TOKEN, roomToken)
28342837
bundle.putBoolean(BundleKeys.KEY_ROOM_ONE_TO_ONE, isOneToOneConversation())
2838+
if (focusBubbleSwitch) {
2839+
bundle.putBoolean(BundleKeys.KEY_FOCUS_CONVERSATION_BUBBLE, true)
2840+
}
28352841

28362842
val upcomingEvent =
28372843
(chatViewModel.upcomingEventViewState.value as? ChatViewModel.UpcomingEventUIState.Success)?.event
@@ -2844,15 +2850,22 @@ class ChatActivity :
28442850
startActivity(intent)
28452851
}
28462852

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

28522864
@Suppress("Detekt.TooGenericExceptionCaught")
2853-
private fun cancelNotificationsForCurrentConversation() {
2865+
protected open fun cancelNotificationsForCurrentConversation() {
2866+
val isBubbleMode = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && isLaunchedFromBubble
28542867
if (conversationUser != null) {
2855-
if (!TextUtils.isEmpty(roomToken)) {
2868+
if (!TextUtils.isEmpty(roomToken) && !isBubbleMode) {
28562869
try {
28572870
NotificationUtils.cancelExistingNotificationsForRoom(
28582871
applicationContext,
@@ -3465,10 +3478,11 @@ class ChatActivity :
34653478
showThreadsItem.isVisible = !isChatThread() &&
34663479
hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.THREADS)
34673480

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

@@ -3500,6 +3514,8 @@ class ChatActivity :
35003514
menu.removeItem(R.id.conversation_voice_call)
35013515
}
35023516

3517+
menu.findItem(R.id.create_conversation_bubble)?.isVisible = NotificationUtils.deviceSupportsBubbles
3518+
35033519
handleThreadNotificationIcon(menu.findItem(R.id.thread_notifications))
35043520
}
35053521
return true
@@ -3510,8 +3526,8 @@ class ChatActivity :
35103526
hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.THREADS)
35113527

35123528
val threadNotificationIcon = when (conversationThreadInfo?.attendee?.notificationLevel) {
3513-
1 -> R.drawable.outline_notifications_active_24
3514-
3 -> R.drawable.ic_baseline_notifications_off_24
3529+
NOTIFICATION_LEVEL_DEFAULT -> R.drawable.outline_notifications_active_24
3530+
NOTIFICATION_LEVEL_NEVER -> R.drawable.ic_baseline_notifications_off_24
35153531
else -> R.drawable.baseline_notifications_24
35163532
}
35173533
threadNotificationItem.icon = ContextCompat.getDrawable(context, threadNotificationIcon)
@@ -3570,6 +3586,11 @@ class ChatActivity :
35703586
true
35713587
}
35723588

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

@@ -3650,6 +3671,70 @@ class ChatActivity :
36503671
)
36513672
}
36523673

3674+
@Suppress("ReturnCount")
3675+
private fun createConversationBubble() {
3676+
if (!NotificationUtils.deviceSupportsBubbles) {
3677+
Log.e(TAG, "createConversationBubble was called but device doesnt support it. It should not be possible " +
3678+
"to get here via UI!")
3679+
return
3680+
}
3681+
3682+
if (!appPreferences.areBubblesEnabled() || !NotificationUtils.areSystemBubblesEnabled(context)) {
3683+
// Do not replace with snackbar as it needs to survive screen change
3684+
Toast.makeText(
3685+
context,
3686+
getString(R.string.nc_conversation_notification_bubble_disabled),
3687+
Toast.LENGTH_SHORT
3688+
).show()
3689+
openBubbleSettings()
3690+
return
3691+
}
3692+
3693+
if (!appPreferences.areBubblesForced()) {
3694+
val conversationAllowsBubbles = isConversationBubbleEnabled()
3695+
if (!conversationAllowsBubbles) {
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+
3707+
val conversationName = currentConversation?.displayName ?: getString(R.string.nc_app_name)
3708+
currentConversation?.let {
3709+
NotificationUtils.createConversationBubble(
3710+
context = context,
3711+
bubbleInfo = NotificationUtils.BubbleInfo(
3712+
roomToken = roomToken,
3713+
conversationRemoteId = it.name,
3714+
conversationName = conversationName,
3715+
conversationUser = conversationUser,
3716+
isOneToOneConversation = isOneToOneConversation(),
3717+
credentials = credentials
3718+
),
3719+
appPreferences = appPreferences,
3720+
lifecycleScope
3721+
)
3722+
}
3723+
}
3724+
3725+
private fun isConversationBubbleEnabled(): Boolean {
3726+
val user = conversationUser ?: return false
3727+
return try {
3728+
DatabaseStorageModule(user, roomToken).getBoolean(BUBBLE_SWITCH_KEY, false)
3729+
} catch (e: IOException) {
3730+
Log.e(TAG, "Failed to read conversation bubble preference: IO error", e)
3731+
false
3732+
} catch (e: IllegalStateException) {
3733+
Log.e(TAG, "Failed to read conversation bubble preference: Invalid state", e)
3734+
false
3735+
}
3736+
}
3737+
36533738
@Suppress("Detekt.LongMethod")
36543739
private fun showThreadNotificationMenu() {
36553740
fun setThreadNotificationLevel(level: Int) {
@@ -3704,7 +3789,7 @@ class ChatActivity :
37043789
subtitle = null,
37053790
icon = R.drawable.ic_baseline_notifications_off_24,
37063791
onClick = {
3707-
setThreadNotificationLevel(3)
3792+
setThreadNotificationLevel(NOTIFICATION_LEVEL_NEVER)
37083793
}
37093794
)
37103795
)
@@ -4413,8 +4498,8 @@ class ChatActivity :
44134498
displayName = currentConversation?.displayName ?: ""
44144499
)
44154500
showSnackBar(roomToken)
4416-
} catch (e: Exception) {
4417-
Log.w(TAG, "File corresponding to the uri does not exist $shareUri", e)
4501+
} catch (e: IOException) {
4502+
Log.w(TAG, "File corresponding to the uri does not exist: IO error $shareUri", e)
44184503
downloadFileToCache(message, false) {
44194504
uploadFile(
44204505
fileUri = shareUri.toString(),
@@ -4900,6 +4985,8 @@ class ChatActivity :
49004985
private const val HTTP_FORBIDDEN = 403
49014986
private const val HTTP_NOT_FOUND = 404
49024987
private const val MESSAGE_PULL_LIMIT = 100
4988+
private const val NOTIFICATION_LEVEL_DEFAULT = 1
4989+
private const val NOTIFICATION_LEVEL_NEVER = 3
49034990
private const val INVITE_LENGTH = 6
49044991
private const val ACTOR_LENGTH = 6
49054992
private const val CHUNK_SIZE: Int = 10
@@ -4915,6 +5002,7 @@ class ChatActivity :
49155002
private const val CURRENT_AUDIO_POSITION_KEY = "CURRENT_AUDIO_POSITION"
49165003
private const val CURRENT_AUDIO_WAS_PLAYING_KEY = "CURRENT_AUDIO_PLAYING"
49175004
private const val RESUME_AUDIO_TAG = "RESUME_AUDIO_TAG"
5005+
private const val BUBBLE_SWITCH_KEY = "bubble_switch"
49185006
private const val FIVE_MINUTES_IN_SECONDS: Long = 300
49195007
private const val ROOM_TYPE_ONE_TO_ONE = "1"
49205008
private const val ACTOR_TYPE = "users"

0 commit comments

Comments
 (0)