Skip to content

Commit 1fd84eb

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 2a8b743 commit 1fd84eb

16 files changed

Lines changed: 1469 additions & 109 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: 100 additions & 11 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>
@@ -146,6 +147,7 @@ import com.nextcloud.talk.models.json.threads.ThreadInfo
146147
import com.nextcloud.talk.polls.ui.PollCreateDialogFragment
147148
import com.nextcloud.talk.polls.ui.PollMainDialogFragment
148149
import com.nextcloud.talk.remotefilebrowser.activities.RemoteFileBrowserActivity
150+
import com.nextcloud.talk.settings.SettingsActivity
149151
import com.nextcloud.talk.shareditems.activities.SharedItemsActivity
150152
import com.nextcloud.talk.signaling.SignalingMessageReceiver
151153
import com.nextcloud.talk.signaling.SignalingMessageSender
@@ -198,6 +200,7 @@ import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_START_CALL_AFTER_ROOM_SWIT
198200
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_SWITCH_TO_ROOM
199201
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_THREAD_ID
200202
import com.nextcloud.talk.utils.permissions.PlatformPermissionUtil
203+
import com.nextcloud.talk.utils.preferences.preferencestorage.DatabaseStorageModule
201204
import com.nextcloud.talk.utils.rx.DisposableSet
202205
import com.nextcloud.talk.utils.singletons.ApplicationWideCurrentRoomHolder
203206
import 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

Comments
 (0)