Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,14 @@
<data android:scheme="content" />
<data android:scheme="file" />
</intent-filter>

<!-- Custom URI scheme for opening conversations from external apps/launchers -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="nextcloudtalk" />
</intent-filter>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The intent filter for opening talk conversation links can be removed because it does not work for Android 12+ devices.

</activity>

<activity
Expand Down
194 changes: 156 additions & 38 deletions app/src/main/java/com/nextcloud/talk/activities/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ package com.nextcloud.talk.activities

import android.app.KeyguardManager
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.provider.ContactsContract
import android.text.TextUtils
Expand All @@ -33,17 +34,16 @@ import com.nextcloud.talk.data.user.model.User
import com.nextcloud.talk.databinding.ActivityMainBinding
import com.nextcloud.talk.invitation.InvitationsActivity
import com.nextcloud.talk.lock.LockedActivity
import com.nextcloud.talk.models.json.conversations.RoomOverall
import com.nextcloud.talk.users.UserManager
import com.nextcloud.talk.utils.ApiUtils
import com.nextcloud.talk.utils.ClosedInterfaceImpl
import com.nextcloud.talk.utils.DeepLinkHandler
import com.nextcloud.talk.utils.SecurityUtils
import com.nextcloud.talk.utils.ShortcutManagerHelper
import com.nextcloud.talk.utils.bundle.BundleKeys
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_TOKEN
import io.reactivex.Observer
import io.reactivex.SingleObserver
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable
import io.reactivex.disposables.CompositeDisposable
import io.reactivex.schedulers.Schedulers
import javax.inject.Inject

Expand All @@ -60,6 +60,8 @@ class MainActivity :
@Inject
lateinit var userManager: UserManager

private val disposables = CompositeDisposable()

private val onBackPressedCallback = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
finish()
Expand Down Expand Up @@ -91,6 +93,11 @@ class MainActivity :
onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
}

override fun onDestroy() {
super.onDestroy()
disposables.dispose()
}

fun lockScreenIfConditionsApply() {
val keyguardManager = getSystemService(KEYGUARD_SERVICE) as KeyguardManager
if (keyguardManager.isKeyguardSecure && appPreferences.isScreenLocked) {
Expand Down Expand Up @@ -166,7 +173,8 @@ class MainActivity :
val user = userId.substringBeforeLast("@")
val baseUrl = userId.substringAfterLast("@")

if (currentUserProviderOld.currentUser.blockingGet()?.baseUrl!!.endsWith(baseUrl) == true) {
val currentUser = currentUserProviderOld.currentUser.blockingGet()
if (currentUser?.baseUrl?.endsWith(baseUrl) == true) {
startConversation(user)
} else {
Snackbar.make(
Expand Down Expand Up @@ -194,35 +202,28 @@ class MainActivity :
invite = userId
)

ncApi.createRoom(
val disposable = ncApi.createRoom(
credentials,
retrofitBucket.url,
retrofitBucket.queryMap
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(object : Observer<RoomOverall> {
override fun onSubscribe(d: Disposable) {
// unused atm
}

override fun onNext(roomOverall: RoomOverall) {
.subscribe(
{ roomOverall ->
if (isFinishing || isDestroyed) return@subscribe
val bundle = Bundle()
bundle.putString(KEY_ROOM_TOKEN, roomOverall.ocs!!.data!!.token)

val chatIntent = Intent(context, ChatActivity::class.java)
chatIntent.putExtras(bundle)
startActivity(chatIntent)
},
{ e ->
Log.e(TAG, "Error creating room", e)
}

override fun onError(e: Throwable) {
// unused atm
}

override fun onComplete() {
// unused atm
}
})
)
disposables.add(disposable)
}

override fun onNewIntent(intent: Intent) {
Expand All @@ -232,12 +233,17 @@ class MainActivity :
}

private fun handleIntent(intent: Intent) {
// Handle deep links first (nextcloudtalk:// scheme)
if (handleDeepLink(intent)) {
return
}

handleActionFromContact(intent)

val internalUserId = intent.extras?.getLong(BundleKeys.KEY_INTERNAL_USER_ID)

var user: User? = null
if (internalUserId != null) {
if (internalUserId != null && internalUserId != 0L) {
user = userManager.getUserWithId(internalUserId).blockingGet()
}

Expand All @@ -253,34 +259,146 @@ class MainActivity :
startActivity(chatIntent)
}
} else {
userManager.users.subscribe(object : SingleObserver<List<User>> {
override fun onSubscribe(d: Disposable) {
// unused atm
}

override fun onSuccess(users: List<User>) {
if (users.isNotEmpty()) {
ClosedInterfaceImpl().setUpPushTokenRegistration()
runOnUiThread {
val disposable = userManager.users
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ users ->
if (isFinishing || isDestroyed) return@subscribe
if (users.isNotEmpty()) {
ClosedInterfaceImpl().setUpPushTokenRegistration()
openConversationList()
}
} else {
runOnUiThread {
} else {
launchServerSelection()
}
},
{ e ->
Log.e(TAG, "Error loading existing users", e)
if (isFinishing || isDestroyed) return@subscribe
Toast.makeText(
context,
context.resources.getString(R.string.nc_common_error_sorry),
Toast.LENGTH_SHORT
).show()
}
)
disposables.add(disposable)
}
}

/**
* Handles deep link URIs for opening conversations.
*
* Supports:
* - nextcloudtalk://[user@]server/call/token
*
* @param intent The intent to process
* @return true if the intent was handled as a deep link, false otherwise
*/
private fun handleDeepLink(intent: Intent): Boolean {
val deepLinkResult = intent.data?.let { DeepLinkHandler.parseDeepLink(it) } ?: return false

Log.d(TAG, "Handling deep link: token=${deepLinkResult.roomToken}, server=${deepLinkResult.serverUrl}")

val disposable = userManager.users
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ users ->
if (isFinishing || isDestroyed) return@subscribe

if (users.isEmpty()) {
launchServerSelection()
return@subscribe
}
}

override fun onError(e: Throwable) {
Log.e(TAG, "Error loading existing users", e)
val targetUser = resolveTargetUser(users, deepLinkResult)

if (targetUser == null) {
Toast.makeText(
context,
context.resources.getString(R.string.nc_no_account_for_server),
Toast.LENGTH_LONG
).show()
openConversationList()
return@subscribe
}

if (userManager.setUserAsActive(targetUser).blockingGet()) {
// Report shortcut usage for ranking
targetUser.id?.let { userId ->
ShortcutManagerHelper.reportShortcutUsed(
context,
deepLinkResult.roomToken,
userId
)
}

if (isFinishing || isDestroyed) return@subscribe

val chatIntent = Intent(context, ChatActivity::class.java)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Login with two accounts Account A, B on talk. Set Account A as active user and create a shortcut for a conversation P. Switch active user to account B and then click on the shortcut - From the current implementation, switches the active user back to Account A and navigates to the intended chat. When you press back button, the conversations from account B are still loaded (conversation from target user are not shown). This creates a lot of bugs.

We could open the conversations list for the target user first and then try to navigate to the chat - then back press opens the conversation list for the target user.

chatIntent.putExtra(KEY_ROOM_TOKEN, deepLinkResult.roomToken)
chatIntent.putExtra(BundleKeys.KEY_INTERNAL_USER_ID, targetUser.id)
startActivity(chatIntent)
} else {
Toast.makeText(
context,
context.resources.getString(R.string.nc_common_error_sorry),
Toast.LENGTH_SHORT
).show()
}
},
{ e ->
Log.e(TAG, "Error loading users for deep link", e)
if (isFinishing || isDestroyed) return@subscribe
Toast.makeText(
context,
context.resources.getString(R.string.nc_common_error_sorry),
Toast.LENGTH_SHORT
).show()
}
})
)
disposables.add(disposable)

return true
}

/**
* Resolves which user account to use for a deep link.
*
* Priority:
* 1. User matching both username and server URL
* 2. User matching the server URL only
* 3. Current active user as fallback (if server matches)
*/
private fun resolveTargetUser(users: List<User>, deepLinkResult: DeepLinkHandler.DeepLinkResult): User? {
val deepLinkHost = Uri.parse(deepLinkResult.serverUrl).host?.lowercase()
if (deepLinkHost.isNullOrBlank()) {
return currentUserProviderOld.currentUser.blockingGet()
}

// Priority: exact match (username + server) > server match > current user fallback
val username = deepLinkResult.username
val exactMatch = if (username != null) {
users.find { user ->
val userHost = user.baseUrl?.let { Uri.parse(it).host?.lowercase() }
userHost == deepLinkHost && user.username?.lowercase() == username.lowercase()
}
} else {
null
}

val serverMatch = users.find { user ->
val userHost = user.baseUrl?.let { Uri.parse(it).host?.lowercase() }
userHost == deepLinkHost
}

val currentUser = currentUserProviderOld.currentUser.blockingGet()
val currentUserMatch = currentUser?.takeIf {
it.baseUrl?.let { url -> Uri.parse(url).host?.lowercase() } == deepLinkHost
}

return exactMatch ?: serverMatch ?: currentUserMatch
}

companion object {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ import com.nextcloud.talk.utils.FileUtils
import com.nextcloud.talk.utils.Mimetype
import com.nextcloud.talk.utils.NotificationUtils
import com.nextcloud.talk.utils.ParticipantPermissions
import com.nextcloud.talk.utils.ShortcutManagerHelper
import com.nextcloud.talk.utils.SpreedFeatures
import com.nextcloud.talk.utils.UserIdUtils
import com.nextcloud.talk.utils.bundle.BundleKeys
Expand Down Expand Up @@ -464,6 +465,11 @@ class ConversationsListActivity :
val isNoteToSelfAvailable = noteToSelf != null
handleNoteToSelfShortcut(isNoteToSelfAvailable, noteToSelf?.token ?: "")

// Update dynamic shortcuts for frequent/favorite conversations
currentUser?.let { user ->
ShortcutManagerHelper.updateDynamicShortcuts(context, list, user)
}

val pair = appPreferences.conversationListPositionAndOffset
layoutManager?.scrollToPositionWithOffset(pair.first, pair.second)
}.collect()
Expand Down Expand Up @@ -2076,6 +2082,14 @@ class ConversationsListActivity :
if (workInfo != null) {
when (workInfo.state) {
WorkInfo.State.SUCCEEDED -> {
currentUser?.id?.let { userId ->
ShortcutManagerHelper.disableConversationShortcut(
context,
conversation.token,
userId,
context.resources.getString(R.string.nc_shortcut_conversation_deleted)
)
}
showSnackbar(
String.format(
context.resources.getString(R.string.deleted_conversation),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import com.nextcloud.talk.utils.ApiUtils
import com.nextcloud.talk.utils.CapabilitiesUtil
import com.nextcloud.talk.utils.ConversationUtils
import com.nextcloud.talk.utils.ShareUtils
import com.nextcloud.talk.utils.ShortcutManagerHelper
import com.nextcloud.talk.utils.SpreedFeatures
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_INTERNAL_USER_ID
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_TOKEN
Expand Down Expand Up @@ -194,6 +195,10 @@ class ConversationsListBottomDialog(
dismiss()
}

binding.conversationAddToHomeScreen.setOnClickListener {
addConversationToHomeScreen()
}

if (conversation.hasArchived) {
binding.conversationArchiveText.setText(R.string.unarchive_conversation)
binding.conversationArchiveIcon.setImageResource(R.drawable.ic_unarchive_24px)
Expand Down Expand Up @@ -419,6 +424,14 @@ class ConversationsListBottomDialog(
if (workInfo != null) {
when (workInfo.state) {
WorkInfo.State.SUCCEEDED -> {
currentUser.id?.let { userId ->
ShortcutManagerHelper.disableConversationShortcut(
context,
conversation.token,
userId,
context.resources.getString(R.string.nc_shortcut_conversation_deleted)
)
}
activity.showSnackbar(
String.format(
context.resources.getString(R.string.left_conversation),
Expand Down Expand Up @@ -450,6 +463,16 @@ class ConversationsListBottomDialog(
dismiss()
}

private fun addConversationToHomeScreen() {
val success = ShortcutManagerHelper.requestPinShortcut(context, conversation, currentUser)
if (success) {
activity.showSnackbar(context.resources.getString(R.string.nc_shortcut_created))
} else {
activity.showSnackbar(context.resources.getString(R.string.nc_common_error_sorry))
}
dismiss()
}

private fun chatApiVersion(): Int =
ApiUtils.getChatApiVersion(currentUser.capabilities!!.spreedCapability!!, intArrayOf(ApiUtils.API_V1))

Expand Down
Loading