-
-
Notifications
You must be signed in to change notification settings - Fork 303
feat: Add deep link and launcher shortcut support for conversations #5664
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
|
|
@@ -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 | ||
|
|
||
|
|
@@ -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() | ||
|
|
@@ -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) { | ||
|
|
@@ -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( | ||
|
|
@@ -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) { | ||
|
|
@@ -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() | ||
| } | ||
|
|
||
|
|
@@ -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) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 { | ||
|
|
||
There was a problem hiding this comment.
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.