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 @@ -122,6 +122,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
205 changes: 167 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,157 @@ 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 uri = intent.data ?: return false
val deepLinkResult = DeepLinkHandler.parseDeepLink(uri) ?: 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)
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 serverUrl = deepLinkResult.serverUrl
val username = deepLinkResult.username

// Extract host from the deep link server URL for comparison
val deepLinkHost = Uri.parse(serverUrl).host?.lowercase()
if (deepLinkHost.isNullOrBlank()) {
return currentUserProviderOld.currentUser.blockingGet()
}

// If username is specified, try to find exact match (username + server)
if (username != null) {
val exactMatch = users.find { user ->
val userHost = user.baseUrl?.let { Uri.parse(it).host?.lowercase() }
userHost == deepLinkHost && user.username?.lowercase() == username.lowercase()
}
if (exactMatch != null) {
return exactMatch
}
}

// Find user matching the server URL (host match)
val matchingUser = users.find { user ->
val userHost = user.baseUrl?.let { Uri.parse(it).host?.lowercase() }
userHost == deepLinkHost
}
if (matchingUser != null) {
return matchingUser
}

// Fall back to current user only if their server matches
val currentUser = currentUserProviderOld.currentUser.blockingGet()
val currentUserHost = currentUser?.baseUrl?.let { Uri.parse(it).host?.lowercase() }
if (currentUserHost == deepLinkHost) {
return currentUser
}

return null
}

companion object {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,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 @@ -505,6 +506,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
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()
}

binding.conversationArchiveText.text = if (conversation.hasArchived) {
this.activity.resources.getString(R.string.unarchive_conversation)
} else {
Expand Down Expand Up @@ -448,6 +453,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