@@ -11,6 +11,7 @@ package com.nextcloud.talk.activities
1111
1212import android.app.KeyguardManager
1313import android.content.Intent
14+ import android.net.Uri
1415import android.os.Bundle
1516import android.provider.ContactsContract
1617import android.text.TextUtils
@@ -33,17 +34,18 @@ import com.nextcloud.talk.data.user.model.User
3334import com.nextcloud.talk.databinding.ActivityMainBinding
3435import com.nextcloud.talk.invitation.InvitationsActivity
3536import com.nextcloud.talk.lock.LockedActivity
36- import com.nextcloud.talk.models.json.conversations.RoomOverall
3737import com.nextcloud.talk.users.UserManager
3838import com.nextcloud.talk.utils.ApiUtils
3939import com.nextcloud.talk.utils.ClosedInterfaceImpl
40+ import com.nextcloud.talk.utils.DeepLinkHandler
4041import com.nextcloud.talk.utils.SecurityUtils
4142import com.nextcloud.talk.utils.UnifiedPushUtils
43+ import com.nextcloud.talk.utils.ShortcutManagerHelper
4244import com.nextcloud.talk.utils.bundle.BundleKeys
4345import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_TOKEN
44- import io.reactivex.Observer
4546import io.reactivex.SingleObserver
4647import io.reactivex.android.schedulers.AndroidSchedulers
48+ import io.reactivex.disposables.CompositeDisposable
4749import io.reactivex.disposables.Disposable
4850import io.reactivex.schedulers.Schedulers
4951import javax.inject.Inject
@@ -61,6 +63,8 @@ class MainActivity :
6163 @Inject
6264 lateinit var userManager: UserManager
6365
66+ private val disposables = CompositeDisposable ()
67+
6468 private val onBackPressedCallback = object : OnBackPressedCallback (true ) {
6569 override fun handleOnBackPressed () {
6670 finish()
@@ -92,6 +96,11 @@ class MainActivity :
9296 onBackPressedDispatcher.addCallback(this , onBackPressedCallback)
9397 }
9498
99+ override fun onDestroy () {
100+ super .onDestroy()
101+ disposables.dispose()
102+ }
103+
95104 fun lockScreenIfConditionsApply () {
96105 val keyguardManager = getSystemService(KEYGUARD_SERVICE ) as KeyguardManager
97106 if (keyguardManager.isKeyguardSecure && appPreferences.isScreenLocked) {
@@ -167,7 +176,8 @@ class MainActivity :
167176 val user = userId.substringBeforeLast(" @" )
168177 val baseUrl = userId.substringAfterLast(" @" )
169178
170- if (currentUserProviderOld.currentUser.blockingGet()?.baseUrl!! .endsWith(baseUrl) == true ) {
179+ val currentUser = currentUserProviderOld.currentUser.blockingGet()
180+ if (currentUser?.baseUrl?.endsWith(baseUrl) == true ) {
171181 startConversation(user)
172182 } else {
173183 Snackbar .make(
@@ -195,35 +205,28 @@ class MainActivity :
195205 invite = userId
196206 )
197207
198- ncApi.createRoom(
208+ val disposable = ncApi.createRoom(
199209 credentials,
200210 retrofitBucket.url,
201211 retrofitBucket.queryMap
202212 )
203213 .subscribeOn(Schedulers .io())
204214 .observeOn(AndroidSchedulers .mainThread())
205- .subscribe(object : Observer <RoomOverall > {
206- override fun onSubscribe (d : Disposable ) {
207- // unused atm
208- }
209-
210- override fun onNext (roomOverall : RoomOverall ) {
215+ .subscribe(
216+ { roomOverall ->
217+ if (isFinishing || isDestroyed) return @subscribe
211218 val bundle = Bundle ()
212219 bundle.putString(KEY_ROOM_TOKEN , roomOverall.ocs!! .data!! .token)
213220
214221 val chatIntent = Intent (context, ChatActivity ::class .java)
215222 chatIntent.putExtras(bundle)
216223 startActivity(chatIntent)
224+ },
225+ { e ->
226+ Log .e(TAG , " Error creating room" , e)
217227 }
218-
219- override fun onError (e : Throwable ) {
220- // unused atm
221- }
222-
223- override fun onComplete () {
224- // unused atm
225- }
226- })
228+ )
229+ disposables.add(disposable)
227230 }
228231
229232 override fun onNewIntent (intent : Intent ) {
@@ -233,12 +236,17 @@ class MainActivity :
233236 }
234237
235238 private fun handleIntent (intent : Intent ) {
239+ // Handle deep links first (nextcloudtalk:// scheme)
240+ if (handleDeepLink(intent)) {
241+ return
242+ }
243+
236244 handleActionFromContact(intent)
237245
238246 val internalUserId = intent.extras?.getLong(BundleKeys .KEY_INTERNAL_USER_ID )
239247
240248 var user: User ? = null
241- if (internalUserId != null ) {
249+ if (internalUserId != null && internalUserId != 0L ) {
242250 user = userManager.getUserWithId(internalUserId).blockingGet()
243251 }
244252
@@ -288,6 +296,125 @@ class MainActivity :
288296 }
289297 }
290298
299+ /* *
300+ * Handles deep link URIs for opening conversations.
301+ *
302+ * Supports:
303+ * - nextcloudtalk://[user@]server/call/token
304+ *
305+ * @param intent The intent to process
306+ * @return true if the intent was handled as a deep link, false otherwise
307+ */
308+ private fun handleDeepLink (intent : Intent ): Boolean {
309+ val deepLinkResult = intent.data?.let { DeepLinkHandler .parseDeepLink(it) } ? : return false
310+
311+ val disposable = userManager.users
312+ .subscribeOn(Schedulers .io())
313+ .observeOn(AndroidSchedulers .mainThread())
314+ .subscribe(
315+ { users ->
316+ if (isFinishing || isDestroyed) return @subscribe
317+
318+ if (users.isEmpty()) {
319+ launchServerSelection()
320+ return @subscribe
321+ }
322+
323+ val targetUser = resolveTargetUser(users, deepLinkResult)
324+
325+ if (targetUser == null ) {
326+ Toast .makeText(
327+ context,
328+ context.resources.getString(R .string.nc_no_account_for_server),
329+ Toast .LENGTH_LONG
330+ ).show()
331+ openConversationList()
332+ return @subscribe
333+ }
334+
335+ if (userManager.setUserAsActive(targetUser).blockingGet()) {
336+ // Report shortcut usage for ranking
337+ targetUser.id?.let { userId ->
338+ ShortcutManagerHelper .reportShortcutUsed(
339+ context,
340+ deepLinkResult.roomToken,
341+ userId
342+ )
343+ }
344+
345+ if (isFinishing || isDestroyed) return @subscribe
346+
347+ // Open conversation list first so back press shows correct user's conversations
348+ val listIntent = Intent (context, ConversationsListActivity ::class .java)
349+ listIntent.addFlags(Intent .FLAG_ACTIVITY_CLEAR_TOP )
350+ listIntent.putExtra(BundleKeys .KEY_INTERNAL_USER_ID , targetUser.id)
351+
352+ val chatIntent = Intent (context, ChatActivity ::class .java)
353+ chatIntent.putExtra(KEY_ROOM_TOKEN , deepLinkResult.roomToken)
354+ chatIntent.putExtra(BundleKeys .KEY_INTERNAL_USER_ID , targetUser.id)
355+
356+ startActivities(arrayOf(listIntent, chatIntent))
357+ } else {
358+ Toast .makeText(
359+ context,
360+ context.resources.getString(R .string.nc_common_error_sorry),
361+ Toast .LENGTH_SHORT
362+ ).show()
363+ }
364+ },
365+ { e ->
366+ Log .e(TAG , " Error loading users for deep link" , e)
367+ if (isFinishing || isDestroyed) return @subscribe
368+ Toast .makeText(
369+ context,
370+ context.resources.getString(R .string.nc_common_error_sorry),
371+ Toast .LENGTH_SHORT
372+ ).show()
373+ }
374+ )
375+ disposables.add(disposable)
376+
377+ return true
378+ }
379+
380+ /* *
381+ * Resolves which user account to use for a deep link.
382+ *
383+ * Priority:
384+ * 1. User matching both username and server URL
385+ * 2. User matching the server URL only
386+ * 3. Current active user as fallback (if server matches)
387+ */
388+ private fun resolveTargetUser (users : List <User >, deepLinkResult : DeepLinkHandler .DeepLinkResult ): User ? {
389+ val deepLinkHost = Uri .parse(deepLinkResult.serverUrl).host?.lowercase()
390+ if (deepLinkHost.isNullOrBlank()) {
391+ return currentUserProviderOld.currentUser.blockingGet()
392+ }
393+
394+ // Priority: exact match (username + server) > server match > current user fallback
395+ val username = deepLinkResult.username
396+ val exactMatch = if (username != null ) {
397+ users.find { user ->
398+ val userHost = user.baseUrl?.let { Uri .parse(it).host?.lowercase() }
399+ userHost == deepLinkHost && user.username?.lowercase() == username.lowercase()
400+ }
401+ } else {
402+ null
403+ }
404+
405+ val serverMatch = users.find { user ->
406+ val userHost = user.baseUrl?.let { Uri .parse(it).host?.lowercase() }
407+ userHost == deepLinkHost
408+ }
409+
410+ val currentUser = currentUserProviderOld.currentUser.blockingGet()
411+ val currentUserMatch = currentUser?.takeIf {
412+ it.baseUrl?.let { url -> Uri .parse(url).host?.lowercase() } == deepLinkHost
413+ }
414+
415+ return exactMatch ? : serverMatch ? : currentUserMatch
416+ }
417+
291418 companion object {
292419 private val TAG = MainActivity ::class .java.simpleName
293420 }
0 commit comments