Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
864de27
fix(user): bind chat and call screens to their account
mahibi Sep 28, 2026
52cd170
fix(user): bind account switching and background work to an account
mahibi Sep 28, 2026
aef0a51
fix(user): bind conversation list screens to their account
mahibi Sep 28, 2026
774e164
fix(user): bind conversation info screens to their account
mahibi Sep 28, 2026
d097f4a
fix(user): bind shared items, media viewer and file browser to their …
mahibi Sep 28, 2026
bc55ff8
fix(user): bind message search and polls to their account
mahibi Sep 28, 2026
ff34a99
fix(user): bind chat helper screens to their account
mahibi Sep 28, 2026
3d2eec6
fix(user): bind contacts, conversation creation and invitations to th…
mahibi Sep 28, 2026
50d1114
fix(user): bind settings, profile and diagnosis to their account
mahibi Sep 28, 2026
383b8ff
fix(theming): theme screens with the server colors of their account
mahibi Sep 28, 2026
5a0218b
refactor(user): replace current user providers by DefaultAccountProvider
mahibi Sep 28, 2026
c19872d
docs(user): document default account use of the phone book integration
mahibi Sep 28, 2026
7f9382c
fix(conversationlist): don't show the previous account's conversation…
mahibi Sep 28, 2026
ff4aa7f
fix(user): don't open a second task when switching accounts
mahibi Sep 28, 2026
e9bfdb9
refactor(chat): resolve the chat user without a pseudo coroutine
mahibi Sep 28, 2026
8a17aae
refactor(chat): let ChatViewModel own the user of the chat
mahibi Sep 28, 2026
0e93305
refactor(chat): remove always-true user checks in ChatActivity
mahibi Sep 28, 2026
9b5558a
refactor(user): resolve the bound user of activities in BaseActivity
mahibi Sep 28, 2026
aa4a0c7
fix(user): report activities started without their account
mahibi Sep 28, 2026
d535c09
refactor(conversationcreation): pass the user directly to the view model
mahibi Sep 28, 2026
14244a4
refactor(conversationinfo): resolve the user without a pseudo coroutine
mahibi Sep 28, 2026
484bf69
refactor(conversationinfo): inline local aliases of conversationUser
mahibi Sep 28, 2026
ca93114
refactor(user): assign the bound user directly to the activity property
mahibi Sep 28, 2026
c6f9de9
refactor(user): set up the bound user first in onCreate() of all acti…
mahibi Sep 28, 2026
19c4843
refactor(user): let entry points fall back to the default account in …
mahibi Sep 28, 2026
0af5fae
fix(call): don't crash when CallActivity closes during onCreate()
mahibi Sep 29, 2026
7da1824
refactor(chat): remove always-true user checks in scheduled messages
mahibi Sep 29, 2026
46d6f28
refactor: create assisted view models with a common delegate
mahibi Sep 29, 2026
2243a02
fix(chat): don't access the chat view model when restoring the messag…
mahibi Sep 29, 2026
fb82646
fix(theming): theme restored fragments with the account of their acti…
mahibi Sep 29, 2026
36fc573
refactor: remove unused MessageSearchActivity
mahibi Sep 29, 2026
85232b1
fix: create injected view models as real view models
mahibi Sep 29, 2026
5dfd682
fix(status): keep an edited status message across rotation
mahibi Sep 29, 2026
570ef11
feat(call): make the account of a joined call the default account
mahibi Sep 29, 2026
51a643f
fix(theming): don't lose the account theme to the subclass injection
mahibi Sep 29, 2026
f912259
fix(contacts): open a created conversation only once
mahibi Sep 29, 2026
a033ad1
fix(user): open links for the account of the screen
mahibi Sep 29, 2026
f3b7d8f
fix(status): bind the account dialog's status view models to the defa…
mahibi Sep 29, 2026
ab8e233
fix(conversationlist): follow the default account when returning to t…
mahibi Sep 29, 2026
227eda0
fix(conversationlist): don't relaunch a received intent for another a…
mahibi Sep 29, 2026
4ee3bed
fix(user): don't reset the default account by saving old user copies
mahibi Sep 29, 2026
3998e7c
fix(share): keep the chosen account when switching it in the share-to…
mahibi Sep 29, 2026
dba80ae
fix(call): reset the dialing state when CallActivity closes before setup
mahibi Sep 29, 2026
448ad08
fix(conversationinfo): don't crash when the ban list is recreated
mahibi Sep 29, 2026
d64a849
fix(upload): fail cleanly when the account of an upload doesn't exist
mahibi Sep 29, 2026
c65c520
fix(user): repair multiple active users in a single statement
mahibi Sep 29, 2026
1dbbaa3
fix(location): theme the address search with the account of the chat
mahibi Sep 29, 2026
084795e
remove unused val
mahibi Sep 29, 2026
6153b12
docs(user): describe when the default account fallback is kept
mahibi Sep 29, 2026
e8c3b9c
fix(conversationinfoedit): create the avatar picker right away
mahibi Sep 29, 2026
5309ae7
fix(user): open the login when sharing to Talk without an account
mahibi Sep 29, 2026
3df2e53
refactor(user): use Room partial entities for partial user updates
mahibi Sep 29, 2026
fdb570b
fix(theming): key the color scheme cache by the theming capability
mahibi Sep 29, 2026
f00b356
fix(conversationlist): rename conversations with the account of the list
mahibi Sep 29, 2026
747ea8f
refactor(conversationlist): make the account of the list non-null
mahibi Sep 29, 2026
b83bb6d
fix(profile): close profile and settings when their account was removed
mahibi Sep 29, 2026
55bd468
suppress large class for now
mahibi Sep 29, 2026
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
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ public void login() throws InterruptedException {

activityScenario.onActivity(activity -> {
assertEquals(loginName,
Objects.requireNonNull(activity.currentUserProviderOld.getCurrentUser().blockingGet()).getUserId());
Objects.requireNonNull(activity.defaultAccountProvider.getDefaultUserBlocking()).getUserId());
});
}
}
4 changes: 0 additions & 4 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -233,10 +233,6 @@
android:name=".remotefilebrowser.activities.RemoteFileBrowserActivity"
android:theme="@style/AppTheme" />

<activity
android:name=".messagesearch.MessageSearchActivity"
android:theme="@style/AppTheme" />

<activity
android:name=".location.LocationPickerActivity"
android:theme="@style/AppTheme" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import androidx.annotation.StringRes
import androidx.lifecycle.lifecycleScope
import androidx.work.Data
import androidx.work.OneTimeWorkRequest
import com.nextcloud.talk.utils.bundle.BundleKeys
import com.nextcloud.talk.utils.setExpeditedIfSupported
import androidx.work.WorkInfo
import androidx.work.WorkManager
Expand Down Expand Up @@ -78,9 +79,6 @@ class AccountVerificationActivity : BaseActivity() {
@Inject
lateinit var ncApiCoroutines: NcApiCoroutines

@Inject
lateinit var userManager: UserManager

@Inject
lateinit var cookieManager: CookieManager

Expand Down Expand Up @@ -440,6 +438,7 @@ class AccountVerificationActivity : BaseActivity() {
ApplicationWideMessageHolder.MessageType.ACCOUNT_WAS_IMPORTED
}
val intent = Intent(context, ConversationsListActivity::class.java)
intent.putExtra(BundleKeys.KEY_INTERNAL_USER_ID, userToSetAsActive.id)
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
startActivity(intent)
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@ import android.content.Intent
import android.content.pm.ActivityInfo
import android.os.Bundle
import androidx.activity.OnBackPressedCallback
import androidx.activity.viewModels
import androidx.core.net.toUri
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import autodagger.AutoInjector
Expand All @@ -39,7 +41,9 @@ class BrowserLoginActivity : BaseActivity() {
private lateinit var binding: ActivityWebViewLoginBinding

@Inject
lateinit var viewModel: BrowserLoginActivityViewModel
lateinit var viewModelFactory: ViewModelProvider.Factory

private val viewModel: BrowserLoginActivityViewModel by viewModels { viewModelFactory }

private var reauthorizeAccount = false
private val onBackPressedCallback = object : OnBackPressedCallback(true) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ import com.nextcloud.talk.data.network.NetworkMonitor
import com.nextcloud.talk.databinding.ActivityServerSelectionBinding
import com.nextcloud.talk.models.json.capabilities.CapabilitiesOverall
import com.nextcloud.talk.models.json.generic.StatusDto
import com.nextcloud.talk.users.UserManager
import com.nextcloud.talk.utils.AccountUtils
import com.nextcloud.talk.utils.ApiUtils
import com.nextcloud.talk.utils.CapabilitiesUtil
Expand All @@ -63,9 +62,6 @@ class ServerSelectionActivity : BaseActivity() {
@Inject
lateinit var ncApi: NcApi

@Inject
lateinit var userManager: UserManager

@Inject
lateinit var networkMonitor: NetworkMonitor

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ import com.nextcloud.talk.data.user.model.User
import com.nextcloud.talk.databinding.ActivitySwitchAccountBinding
import com.nextcloud.talk.models.ImportAccount
import com.nextcloud.talk.models.json.participants.ParticipantDto
import com.nextcloud.talk.conversationlist.ConversationsListActivity
import com.nextcloud.talk.conversationlist.DirectShareHelper
import com.nextcloud.talk.users.UserManager
import com.nextcloud.talk.utils.AccountUtils.findAvailableAccountsOnDevice
import com.nextcloud.talk.utils.AccountUtils.getInformationFromAccount
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_BASE_URL
Expand All @@ -44,9 +44,6 @@ import javax.inject.Inject
class SwitchAccountActivity : BaseActivity() {
private lateinit var binding: ActivitySwitchAccountBinding

@Inject
lateinit var userManager: UserManager

@Inject
lateinit var cookieManager: CookieManager

Expand Down Expand Up @@ -86,6 +83,18 @@ class SwitchAccountActivity : BaseActivity() {
supportActionBar?.title = resources!!.getString(R.string.nc_select_an_account)
}

private fun switchToAccount(user: User) {
lifecycleScope.launch {
if (userManager.setUserAsActive(user)) {
DirectShareHelper.removeAllShareTargetShortcuts(this@SwitchAccountActivity)
cookieManager.cookieStore.removeAll()
val intent = ConversationsListActivity.createAccountSwitchIntent(this@SwitchAccountActivity, user.id!!)
startActivity(intent)
finish()
}
}
}

@Suppress("Detekt.NestedBlockDepth")
override fun onResume() {
super.onResume()
Expand All @@ -95,13 +104,7 @@ class SwitchAccountActivity : BaseActivity() {
if (isAccountImport) {
reauthorizeFromImport(item.account)
} else {
lifecycleScope.launch {
if (userManager.setUserAsActive(item.user!!)) {
DirectShareHelper.removeAllShareTargetShortcuts(this@SwitchAccountActivity)
cookieManager.cookieStore.removeAll()
finish()
}
}
switchToAccount(item.user!!)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,15 @@ import kotlinx.coroutines.runBlocking
class LocalLoginDataSource(val userManager: UserManager, val appPreferences: AppPreferences, val context: Context) {

fun updateUser(loginData: LoginCompletion) {
val currentUser = runBlocking { userManager.getCurrentUser() }
val currentUser = runBlocking { userManager.getDefaultUser() }
if (currentUser != null) {
currentUser.clientCertificate = appPreferences.temporaryClientCertAlias
currentUser.token = loginData.appPassword
runBlocking { userManager.updateOrCreateUser(currentUser) }
runBlocking {
userManager.updateCredentials(
currentUser.id!!,
loginData.appPassword,
appPreferences.temporaryClientCertAlias
)
}
}
}

Expand Down
109 changes: 99 additions & 10 deletions app/src/main/java/com/nextcloud/talk/activities/BaseActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -31,29 +31,33 @@ import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import autodagger.AutoInjector
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.nextcloud.talk.BuildConfig
import com.nextcloud.talk.R
import com.nextcloud.talk.account.AccountVerificationActivity
import com.nextcloud.talk.account.BrowserLoginActivity
import com.nextcloud.talk.account.ServerSelectionActivity
import com.nextcloud.talk.account.SwitchAccountActivity
import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.chat.ChatActivity
import com.nextcloud.talk.data.user.model.User
import com.nextcloud.talk.events.CertificateEvent
import com.nextcloud.talk.events.RemoteWipeEvent
import com.nextcloud.talk.lock.LockedActivity
import com.nextcloud.talk.users.DefaultAccountProvider
import com.nextcloud.talk.users.UserManager
import com.nextcloud.talk.utils.SecurityUtils
import com.nextcloud.talk.ui.theme.ViewThemeUtils
import com.nextcloud.talk.ui.theme.ViewThemeUtilsFactory
import com.nextcloud.talk.utils.DisplayUtils
import com.nextcloud.talk.utils.FileViewerUtils
import com.nextcloud.talk.utils.UriUtils
import com.nextcloud.talk.utils.adjustUIForAPILevel35
import com.nextcloud.talk.utils.bundle.BundleKeys
import com.nextcloud.talk.utils.database.user.CurrentUserProvider
import com.nextcloud.talk.utils.database.user.CurrentUserProviderOld
import com.nextcloud.talk.utils.message.MessageUtils
import com.nextcloud.talk.utils.preferences.AppPreferences
import com.nextcloud.talk.logger.Logger
import com.nextcloud.talk.utils.ssl.TrustManager
import kotlinx.coroutines.runBlocking
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
Expand All @@ -80,22 +84,98 @@ open class BaseActivity : AppCompatActivity() {
@Inject
lateinit var viewThemeUtils: ViewThemeUtils

@Inject
lateinit var viewThemeUtilsFactory: ViewThemeUtilsFactory

@Inject
lateinit var messageUtils: MessageUtils

@Inject
lateinit var context: Context

@Deprecated("Use CurrentUserProvider instead")
@Inject
lateinit var currentUserProviderOld: CurrentUserProviderOld
lateinit var defaultAccountProvider: DefaultAccountProvider

@Inject
lateinit var currentUserProvider: CurrentUserProvider
lateinit var userManager: UserManager

@Inject
lateinit var logger: Logger

/**
* Whether this activity may be started without [BundleKeys.KEY_INTERNAL_USER_ID] and then uses the default
* account. Only for entry points without an account context, like the conversation list on app start. For
* all other activities a missing id is a bug.
*/
protected open val allowsDefaultAccount: Boolean = false

/**
* Internal id of the account this activity was started for, taken from [BundleKeys.KEY_INTERNAL_USER_ID].
* Without it, the default account is used and its id is written to the intent, so an activity recreated after a
* configuration change stays on the same account. After process death the system restores the original intent,
* so the activity uses the default account at that time. That is only intended for activities with
* [allowsDefaultAccount]: for all others it fails in debug builds and is logged as an error in release builds.
* Returns 0 if there is no account at all.
*/
protected fun resolveUserIdFromIntent(): Long {
val userId = intent.getLongExtra(BundleKeys.KEY_INTERNAL_USER_ID, 0L)
if (userId != 0L) {
return userId
}
if (!allowsDefaultAccount) {
val message = "${javaClass.simpleName} was started without ${BundleKeys.KEY_INTERNAL_USER_ID}"
check(!BuildConfig.DEBUG) { message }
Log.e(TAG, "$message, using the default account")
}
return (defaultAccountProvider.getDefaultUserBlocking()?.id ?: 0L).also {
intent.putExtra(BundleKeys.KEY_INTERNAL_USER_ID, it)
}
}

/**
* The account this activity was started for (see [resolveUserIdFromIntent]), or null if it doesn't exist. For
* activities with [allowsDefaultAccount], a requested account that doesn't exist (anymore) falls back to the
* default account. Loaded once on first access, which must be after injection.
*/
private val boundUserLazy = lazy {
runBlocking { userManager.getUserWithId(resolveUserIdFromIntent()) }
?: if (allowsDefaultAccount) defaultAccountProvider.getDefaultUserBlocking() else null
}
private val boundUser: User? by boundUserLazy

/**
* Returns [boundUser], or finishes the activity and returns null if the account doesn't exist.
*/
private fun requireBoundUserOrFinish(): User? =
boundUser ?: run {
Log.e(TAG, "No user found for id ${resolveUserIdFromIntent()}")
if (allowsDefaultAccount) {
startActivity(Intent(this, MainActivity::class.java))
}
finish()
null
}

private var userViewThemeUtils: ViewThemeUtils? = null

/**
* Themes this activity, and the fragments and dialogs it hosts, with the server colors of [boundUser]. The theme
* is created once and assigned again on every call, because the injection of a subclass also injects
* [viewThemeUtils] and overwrites it with the theme of the default account. Reusing the instance keeps fragments
* that already took it over on the same one.
*/
private fun applyUserTheme() {
val themeUtils = userViewThemeUtils
?: boundUser?.let { viewThemeUtilsFactory.forUser(it) }?.also { userViewThemeUtils = it }
themeUtils?.let { viewThemeUtils = it }
}

/**
* Resolves [boundUser] and applies its theme, or finishes the activity and returns null if the account doesn't
* exist. Must be called right after injection, before anything else is set up.
*/
protected fun setUpBoundUserOrFinish(): User? = requireBoundUserOrFinish()?.also { applyUserTheme() }

open val appBarLayoutType: AppBarLayoutType
get() = AppBarLayoutType.TOOLBAR

Expand Down Expand Up @@ -130,6 +210,11 @@ open class BaseActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this)
logger.i(this::class.java.simpleName, "onCreate")
// Fragments restored in super.onCreate() take over viewThemeUtils there, so apply the theme of the account
// before. Without an account in the intent, the injected theme of the default account is the right one.
if (intent.hasExtra(BundleKeys.KEY_INTERNAL_USER_ID)) {
applyUserTheme()
}
adjustUIForAPILevel35()
super.onCreate(savedInstanceState)

Expand Down Expand Up @@ -326,7 +411,10 @@ open class BaseActivity : AppCompatActivity() {
}

override fun startActivity(intent: Intent) {
val user = currentUserProviderOld.currentUser.blockingGet()
// Links to the own server are opened for the account of this screen. Screens without an account (boundUser
// never loaded) use the default account, without resolving one from their intent.
val user = (if (boundUserLazy.isInitialized()) boundUser else null)
?: defaultAccountProvider.getDefaultUserBlocking()
if (intent.data != null && TextUtils.equals(intent.action, Intent.ACTION_VIEW)) {
val uri = intent.data.toString()
if (user?.baseUrl != null && uri.startsWith(user.baseUrl!!)) {
Expand All @@ -344,10 +432,11 @@ open class BaseActivity : AppCompatActivity() {
fileViewerUtils.openFileInFilesApp(uri, UriUtils.extractInstanceInternalFileFileIdNew(uri))
} else if (UriUtils.isInstanceInternalTalkUrl(user.baseUrl!!, uri)) {
// https://cloud.nextcloud.com/call/123456789
val bundle = Bundle()
bundle.putString(BundleKeys.KEY_ROOM_TOKEN, UriUtils.extractRoomTokenFromTalkUrl(uri))
val chatIntent = Intent(context, ChatActivity::class.java)
chatIntent.putExtras(bundle)
val chatIntent = ChatActivity.createIntent(
context,
user.id!!,
UriUtils.extractRoomTokenFromTalkUrl(uri)
)
chatIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
startActivity(chatIntent)
} else {
Expand Down
Loading
Loading