Skip to content

New user handling - #6787

Open
mahibi wants to merge 57 commits into
masterfrom
fixUserManagement
Open

mahibi wants to merge 57 commits into
masterfrom
fixUserManagement

Conversation

@mahibi

@mahibi mahibi commented Sep 28, 2026 •

Copy link
Copy Markdown
Collaborator

Bind screens and background work to their account

Problem

Screens, view models, workers and receivers read a global "current user" (CurrentUserProviderOld / CurrentUserProvider). Anything that changed it switched open screens to another account, or mixed the data of account A with the credentials of account B: an incoming call, a notification tap, the account switcher, share-to, a deep link.
The providers also had their own bugs:

  • non thread-safe caching
  • runBlocking on the main thread
  • writes during reads (self-heal in getActiveUser())
  • stale values

Solution

  • Every screen, view model, worker and receiver is bound to an explicit account, passed as BundleKeys.KEY_INTERNAL_USER_ID (a Long). An open screen stays on its account.
  • The global user is now a "default account": the last used one, stored as the current flag. It is read only by entry points: app start, share-to, deep links without an account, the account switcher and the phone book integration. It changes on user actions: the account switcher, a notification tap, a deep link, opening the conversation list of an account, and joining a call. The conversation list follows it when it comes back from the background.
  • Theming is per account: screens use the server colors of their own account.
  • Clean-up and data layer:
    • The current user providers are replaced by DefaultAccountProvider.
    • Reading the active user no longer writes. A one-time repair at startup, a single SQL statement, fixes several accounts marked as active.
    • Background jobs and Settings only write the columns they change (Room partial entities). They can no longer reset the default account or the token by saving an old copy of the account.

Other related fixes found along the way

  • Switching accounts no longer opens a second app instance, and no longer briefly shows the previous account's conversations.
  • A created conversation opens only once. Links to the own server open for the screen's account.
  • An edited status message survives rotation. The account switcher's status belongs to the current default account.
  • After picture in picture during a call for another account, the list shows that account.
  • The share-to chooser keeps the chosen account. The list no longer relaunches an intent received from other apps as-is (lint UnsafeIntentLaunch).
  • Sharing to Talk without an account opens the login instead of closing silently.
  • Crashes: CallActivity finishing in onCreate(); DialogBanListFragment on rotation; restored fragments accessing the chat view model.
  • isDialing is reset when a call screen closes before setup.
  • An upload whose account was removed fails cleanly.
  • A camera or picker result for the conversation avatar is no longer lost after process death.
  • The address search uses the chat's account theme.
  • Injected view models are real view models, so they survive rotation and are cleared.
  • MessageSearchActivity (unused) is removed.

How to get the account

Where How
Start an activity Always put KEY_INTERNAL_USER_ID (the user.id, a Long). For chats, use ChatActivity.createIntent(context, userId, roomToken, extras). A missing id fails in debug builds (check) and is logged as an error in release builds.
Activity (BaseActivity) In onCreate(): super.onCreate() → inject(this) → user = setUpBoundUserOrFinish() ?: return → the rest. This loads the account, applies its theme, and finishes the activity if the account doesn't exist. Only entry points set override val allowsDefaultAccount = true, and then fall back to the default account.
View model @AssistedInject constructor(…, @Assisted user: User) with an @AssistedFactory interface Factory { fun build(user: User): X }. In the activity: val viewModel by assistedViewModels { factory.build(user) }, used only after setUpBoundUserOrFinish(). In Compose: viewModel(key = "x-${user.id}", factory = ViewModelFactoryWithParams(X::class.java) { factory.build(user) }). Never read the default account in a view model.
Observe account changes (capabilities, token, …) userManager.userFlow(id). For a one-time read: userManager.getUserWithId(id).
Fragment / dialog Pass the User (Parcelable) or its id as fragment arguments via newInstance(…), never as constructor parameters (they break recreation). Theme: viewThemeUtils = hostViewThemeUtils(activity, viewThemeUtils).
Custom view / adapter Take the account from the host screen. Theme: hostViewThemeUtils(context, fallback).
Worker Put the id into the input data (putLong(KEY_INTERNAL_USER_ID, user.id)). In the worker: userManager.getUserWithId(inputData.getLong(KEY_INTERNAL_USER_ID,0L)), and fail the work if it's null.
Receiver / notification action / service Put the id into the (pending) intent. Read it with getLongExtra(KEY_INTERNAL_USER_ID, 0L), and stop if no account is found. There's no fallback to the default account.
Entry point without an account context DefaultAccountProvider.getDefaultUser() (suspend) or getDefaultUserBlocking(). Make an account the default only on user actions, with userManager.setUserAsActive(user).
Theme outside BaseActivity ViewThemeUtilsFactory.forUser(user).
Save account data Never save a whole User copy that was read earlier: it overwrites current and token. Use the partial updates (userManager.updateCapabilities, updateExternalSignalingServer, updateDisplayName, updateClientCertificate, updateCredentials). For new columns, add a partial entity class in data/user/model/UserPartialUpdates.kt and an @Update(entity = UserEntity::class) method.

Testing

  • Unit tests: UserManager, DefaultAccountProvider, ChatViewModel, ContactsViewModel, CapabilitiesFetcher, plus Room tests for the partial updates and the startup
    repair (in-memory database).
  • Manual, with two or three accounts on different servers:
    • An open chat of A stays on A during an incoming call for B; the call uses B.
    • A notification tap for B opens B, and back goes to B's list.
    • The account switcher: one task, no leftover screens of A.
    • Rotation keeps the account.
    • The launcher opens the last used account.
    • Share-to with a switch to another account.
    • Picture in picture during a call for another account.

🏁 Checklist

  • ⛑️ Tests (unit and/or integration) are included or not needed
  • 🔖 Capability is checked or not needed
  • 🔙 Backport requests are created or not needed: /backport to stable-xx.x
  • 📅 Milestone is set
  • 🌸 PR title is meaningful (if it should be in the changelog: is it meaningful to users?)

🤖 AI (if applicable)

  • The content of this PR was partly or fully generated using AI

@mahibi mahibi added this to the 25.1.0 milestone Sep 28, 2026
@mahibi mahibi self-assigned this Sep 28, 2026
@github-actions

github-actions Bot commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

📱 QA build

Download app-qa-debug.apk
QR code Open the QR code for this download
Commit 55bd468
Version 6787
Available until 7 days after this build

The QA build installs alongside a released Nextcloud app, so you can keep
using your existing install while testing.

Downloading the file requires a GitHub account, so open this link on the
device you want to test on, or transfer the APK to it.

@mahibi mahibi mentioned this pull request Sep 29, 2026
1 of 6 tasks
@mahibi mahibi added the 3. to review Waiting for reviews label Sep 29, 2026
@mahibi
mahibi marked this pull request as ready for review September 29, 2026 12:47
@mahibi mahibi changed the title Fix user management New user handling Sep 29, 2026
@coderabbitai

coderabbitai Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

📝 Walkthrough

Walkthrough

The change makes account identity explicit across activities, view models, conversation flows, and background work. It adds default-account lookup, per-user data updates, and repair for multiple active accounts. Account IDs are passed through navigation and worker inputs. Feature view models receive their account user directly. Theme utilities can resolve the host activity’s theme. The message-search activity and related UI resources are removed.

Priority: ➖ Normal

Merge Risk: 🟡 Moderate · up to 55bd4

Shares that arrive for a different account can lose their attached files, and restored poll dialogs may still use stale credentials. Fix both before merging.

Security Architecture Review

Security architecture risk: 🟠 High · up to 55bd4

An installed app can direct an externally accessible screen toward a locally configured account. This change also allows that selection to become the default for later accountless actions. Account binding reduces accidental mixing once a screen opens, but the account-selection boundary needs review.

Retained concerns

  • High · security · observed: An externally supplied local account ID can now make that account the durable default, affecting later entry points without an account context.
  • Medium · security · inferred: An asynchronous activation of the screen’s captured account can overwrite a newer default-account choice; the resume transition has no demonstrated ordering with that write.
Security review details

Security Blast Radius

  • inferred — The independently attackable boundary is an installed app able to launch the exported activity; the affected scope is the configured accounts and subsequent accountless actions within that app installation, not demonstrated access to their data by the sending app.

Security Findings and Attack Paths

  • observed — An external intent can supply a local account ID to the exported list; when that account is not current, the new onCreate path requests its activation, changing the default used by accountless entry points.

Trust Boundaries and Controls

  • observed — Once selected, the screen uses its bound account for credentials and navigation. The changed onNewIntent path constructs a fresh explicit intent rather than forwarding the external intent’s flags; neither control authenticates the account choice.

Resilience and Maintainability Implications

  • inferred — The asynchronous onCreate activation and onRestart’s separate default-account read leave a possible stale-selection race. The single-statement database update protects flag consistency, not the intent or ordering of competing selections.

Hardening Proposals

  • proposed — Separate externally accepted share and ecosystem inputs from the internal account-ID contract; permit an externally initiated default-account change only through an explicit trusted selection path.
🚥 Pre-merge checks | ✅ 3 | ❌ 1 | ❓ 1

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 8.42% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 297 functions across 53 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title is related to account handling but is too broad and does not clearly describe the main change from global current-user handling to explicit account binding. Replace it with a specific title such as "Bind screens and background work to explicit accounts".
✅ Passed checks (3 passed)
Check name Status Explanation
Description check ✅ Passed The description clearly explains the problem, solution, related fixes, account-handling rules, and testing plan. It includes the checklist and AI disclosure. The missing screenshots and TODO sections …
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
  • Fix all pre-merge checks with AI

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to GitHub limitations.

⚠️ Outside diff range comments (1)

🟡 Minor · Key the theme cache by the theme inputs, not only baseUrl. · MaterialSchemesProviderImpl.kt:32-33

app/src/main/java/com/nextcloud/talk/ui/theme/MaterialSchemesProviderImpl.kt:32-33
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Key the theme cache by the theme inputs, not only baseUrl.

When two accounts share a baseUrl but have different themingCapability values, the first account populates this cache entry. The new ViewThemeUtilsFactory.forUser then gives the second account the first account’s colors. Include the account and relevant capability state in the cache key, or calculate schemes without this cache. The new per-user factory makes this existing cache behavior affect account-scoped views.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: ccc46ac3-e2bd-4831-a031-4f07e432752e

📥 Commits

Reviewing files that changed from the base of the PR and between f704b95 and 90f3bd5.

📒 Files selected for processing (143)
  • app/src/androidTest/java/com/nextcloud/talk/ui/LoginIT.java
  • app/src/main/AndroidManifest.xml
  • app/src/main/java/com/nextcloud/talk/account/AccountVerificationActivity.kt
  • app/src/main/java/com/nextcloud/talk/account/BrowserLoginActivity.kt
  • app/src/main/java/com/nextcloud/talk/account/ServerSelectionActivity.kt
  • app/src/main/java/com/nextcloud/talk/account/SwitchAccountActivity.kt
  • app/src/main/java/com/nextcloud/talk/account/data/io/LocalLoginDataSource.kt
  • app/src/main/java/com/nextcloud/talk/activities/BaseActivity.kt
  • app/src/main/java/com/nextcloud/talk/activities/CallActivity.kt
  • app/src/main/java/com/nextcloud/talk/activities/MainActivity.kt
  • app/src/main/java/com/nextcloud/talk/adapters/items/LoadMoreResultsItem.kt
  • app/src/main/java/com/nextcloud/talk/adapters/items/MessageResultItem.kt
  • app/src/main/java/com/nextcloud/talk/application/NextcloudTalkApplication.kt
  • app/src/main/java/com/nextcloud/talk/attachmentpreview/FileAttachmentPreviewFragment.kt
  • app/src/main/java/com/nextcloud/talk/callnotification/CallNotificationActivity.kt
  • app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt
  • app/src/main/java/com/nextcloud/talk/chat/MessageInputFragment.kt
  • app/src/main/java/com/nextcloud/talk/chat/MessageInputVoiceRecordingFragment.kt
  • app/src/main/java/com/nextcloud/talk/chat/ScheduledMessagesActivity.kt
  • app/src/main/java/com/nextcloud/talk/chat/viewmodels/ChatViewModel.kt
  • app/src/main/java/com/nextcloud/talk/chat/viewmodels/ScheduledMessagesViewModel.kt
  • app/src/main/java/com/nextcloud/talk/chooseaccount/ChooseAccountDialogCompose.kt
  • app/src/main/java/com/nextcloud/talk/chooseaccount/ui/StatusMessageSheet.kt
  • app/src/main/java/com/nextcloud/talk/chooseaccount/viewmodel/StatusMessageViewModel.kt
  • app/src/main/java/com/nextcloud/talk/chooseaccount/viewmodel/StatusViewModel.kt
  • app/src/main/java/com/nextcloud/talk/contacts/ContactsActivity.kt
  • app/src/main/java/com/nextcloud/talk/contacts/ContactsScreen.kt
  • app/src/main/java/com/nextcloud/talk/contacts/ContactsViewModel.kt
  • app/src/main/java/com/nextcloud/talk/contacts/components/ContactItemRow.kt
  • app/src/main/java/com/nextcloud/talk/contacts/components/ConversationCreationOptions.kt
  • app/src/main/java/com/nextcloud/talk/contextchat/ContextChatViewModel.kt
  • app/src/main/java/com/nextcloud/talk/conversation/RenameConversationDialogFragment.kt
  • app/src/main/java/com/nextcloud/talk/conversationcreation/ConversationCreationActivity.kt
  • app/src/main/java/com/nextcloud/talk/conversationcreation/ui/CreatedConversation.kt
  • app/src/main/java/com/nextcloud/talk/conversationcreation/viewmodel/ConversationCreationViewModel.kt
  • app/src/main/java/com/nextcloud/talk/conversationinfo/ConversationInfoActivity.kt
  • app/src/main/java/com/nextcloud/talk/conversationinfoedit/ConversationInfoEditActivity.kt
  • app/src/main/java/com/nextcloud/talk/conversationinfoedit/viewmodel/ConversationInfoEditViewModel.kt
  • app/src/main/java/com/nextcloud/talk/conversationlist/ConversationsListActivity.kt
  • app/src/main/java/com/nextcloud/talk/conversationlist/data/OfflineConversationsRepository.kt
  • app/src/main/java/com/nextcloud/talk/conversationlist/data/network/OfflineFirstConversationsRepository.kt
  • app/src/main/java/com/nextcloud/talk/conversationlist/viewmodels/ConversationsListViewModel.kt
  • app/src/main/java/com/nextcloud/talk/conversationtags/viewmodels/ConversationTagsViewModel.kt
  • app/src/main/java/com/nextcloud/talk/dagger/modules/ViewModelModule.kt
  • app/src/main/java/com/nextcloud/talk/data/user/UsersDao.kt
  • app/src/main/java/com/nextcloud/talk/data/user/UsersRepository.kt
  • app/src/main/java/com/nextcloud/talk/data/user/UsersRepositoryImpl.kt
  • app/src/main/java/com/nextcloud/talk/data/user/model/UserPartialUpdates.kt
  • app/src/main/java/com/nextcloud/talk/diagnosis/DiagnosisActivity.kt
  • app/src/main/java/com/nextcloud/talk/diagnosis/DiagnosisElement.kt
  • app/src/main/java/com/nextcloud/talk/diagnosis/DiagnosisViewModel.kt
  • app/src/main/java/com/nextcloud/talk/invitation/InvitationsActivity.kt
  • app/src/main/java/com/nextcloud/talk/invitation/adapters/InvitationsAdapter.kt
  • app/src/main/java/com/nextcloud/talk/jobs/CapabilitiesFetcher.kt
  • app/src/main/java/com/nextcloud/talk/jobs/ContactAddressBookWorker.kt
  • app/src/main/java/com/nextcloud/talk/jobs/DownloadFileToCacheWorker.kt
  • app/src/main/java/com/nextcloud/talk/jobs/LeaveConversationWorker.kt
  • app/src/main/java/com/nextcloud/talk/jobs/NotificationWorker.kt
  • app/src/main/java/com/nextcloud/talk/jobs/SignalingSettingsWorker.java
  • app/src/main/java/com/nextcloud/talk/jobs/UploadAndShareFilesWorker.kt
  • app/src/main/java/com/nextcloud/talk/location/GeocodingActivity.kt
  • app/src/main/java/com/nextcloud/talk/location/LocationPickerActivity.kt
  • app/src/main/java/com/nextcloud/talk/location/viewmodels/LocationPickerViewModel.kt
  • app/src/main/java/com/nextcloud/talk/logger/ui/LogsActivity.kt
  • app/src/main/java/com/nextcloud/talk/mediaviewer/activities/MediaViewerActivity.kt
  • app/src/main/java/com/nextcloud/talk/mediaviewer/viewmodels/MediaViewerViewModel.kt
  • app/src/main/java/com/nextcloud/talk/messagesearch/MessageSearchActivity.kt
  • app/src/main/java/com/nextcloud/talk/messagesearch/MessageSearchViewModel.kt
  • app/src/main/java/com/nextcloud/talk/openconversations/ListOpenConversationsActivity.kt
  • app/src/main/java/com/nextcloud/talk/openconversations/viewmodels/OpenConversationsViewModel.kt
  • app/src/main/java/com/nextcloud/talk/polls/ui/PollCreateDialogFragment.kt
  • app/src/main/java/com/nextcloud/talk/polls/ui/PollLoadingFragment.kt
  • app/src/main/java/com/nextcloud/talk/polls/ui/PollMainDialogFragment.kt
  • app/src/main/java/com/nextcloud/talk/polls/ui/PollResultsFragment.kt
  • app/src/main/java/com/nextcloud/talk/polls/ui/PollVoteFragment.kt
  • app/src/main/java/com/nextcloud/talk/polls/viewmodels/PollCreateViewModel.kt
  • app/src/main/java/com/nextcloud/talk/polls/viewmodels/PollMainViewModel.kt
  • app/src/main/java/com/nextcloud/talk/polls/viewmodels/PollVoteViewModel.kt
  • app/src/main/java/com/nextcloud/talk/presenters/MentionAutocompletePresenter.java
  • app/src/main/java/com/nextcloud/talk/profile/ProfileActivity.kt
  • app/src/main/java/com/nextcloud/talk/raisehand/viewmodel/RaiseHandViewModel.kt
  • app/src/main/java/com/nextcloud/talk/receivers/DirectReplyReceiver.kt
  • app/src/main/java/com/nextcloud/talk/receivers/DismissRecordingAvailableReceiver.kt
  • app/src/main/java/com/nextcloud/talk/receivers/MarkAsReadReceiver.kt
  • app/src/main/java/com/nextcloud/talk/receivers/ShareRecordingToChatReceiver.kt
  • app/src/main/java/com/nextcloud/talk/remotefilebrowser/activities/RemoteFileBrowserActivity.kt
  • app/src/main/java/com/nextcloud/talk/remotefilebrowser/viewmodels/RemoteFileBrowserItemsViewModel.kt
  • app/src/main/java/com/nextcloud/talk/settings/SettingsActivity.kt
  • app/src/main/java/com/nextcloud/talk/shareditems/activities/SharedItemsActivity.kt
  • app/src/main/java/com/nextcloud/talk/shareditems/adapters/SharedItemsAdapter.kt
  • app/src/main/java/com/nextcloud/talk/threadsoverview/ThreadsOverviewActivity.kt
  • app/src/main/java/com/nextcloud/talk/threadsoverview/viewmodels/ThreadsOverviewViewModel.kt
  • app/src/main/java/com/nextcloud/talk/translate/ui/TranslateActivity.kt
  • app/src/main/java/com/nextcloud/talk/translate/viewmodels/TranslateViewModel.kt
  • app/src/main/java/com/nextcloud/talk/ui/PlaybackSpeedControl.kt
  • app/src/main/java/com/nextcloud/talk/ui/chooseaccount/ChooseAccountShareToDialogFragment.kt
  • app/src/main/java/com/nextcloud/talk/ui/chooseaccount/ChooseAccountShareToViewModel.kt
  • app/src/main/java/com/nextcloud/talk/ui/chooseaccount/model/ChooseAccountShareToViewState.kt
  • app/src/main/java/com/nextcloud/talk/ui/dialog/AttachmentDialog.kt
  • app/src/main/java/com/nextcloud/talk/ui/dialog/AudioOutputDialog.kt
  • app/src/main/java/com/nextcloud/talk/ui/dialog/DateTimeCompose.kt
  • app/src/main/java/com/nextcloud/talk/ui/dialog/DialogBanListFragment.kt
  • app/src/main/java/com/nextcloud/talk/ui/dialog/FilterConversationFragment.kt
  • app/src/main/java/com/nextcloud/talk/ui/dialog/MoreCallActionsDialog.kt
  • app/src/main/java/com/nextcloud/talk/ui/dialog/SaveToStorageDialogFragment.kt
  • app/src/main/java/com/nextcloud/talk/ui/dialog/SetPhoneNumberDialogFragment.kt
  • app/src/main/java/com/nextcloud/talk/ui/theme/HostViewThemeUtils.kt
  • app/src/main/java/com/nextcloud/talk/ui/theme/MaterialSchemesProvider.kt
  • app/src/main/java/com/nextcloud/talk/ui/theme/MaterialSchemesProviderImpl.kt
  • app/src/main/java/com/nextcloud/talk/ui/theme/ThemeModule.kt
  • app/src/main/java/com/nextcloud/talk/ui/theme/ViewThemeUtilsFactory.kt
  • app/src/main/java/com/nextcloud/talk/users/DefaultAccountProvider.kt
  • app/src/main/java/com/nextcloud/talk/users/UserManager.kt
  • app/src/main/java/com/nextcloud/talk/utils/FileViewerUtils.kt
  • app/src/main/java/com/nextcloud/talk/utils/PickImage.kt
  • app/src/main/java/com/nextcloud/talk/utils/database/user/CurrentUserProvider.kt
  • app/src/main/java/com/nextcloud/talk/utils/database/user/CurrentUserProviderImpl.kt
  • app/src/main/java/com/nextcloud/talk/utils/database/user/CurrentUserProviderOld.kt
  • app/src/main/java/com/nextcloud/talk/utils/database/user/CurrentUserProviderOldImpl.kt
  • app/src/main/java/com/nextcloud/talk/utils/database/user/UserModule.kt
  • app/src/main/java/com/nextcloud/talk/utils/preview/ComposePreviewUtils.kt
  • app/src/main/java/com/nextcloud/talk/utils/preview/ComposePreviewUtilsDaos.kt
  • app/src/main/java/com/nextcloud/talk/utils/rx/SearchViewObservable.kt
  • app/src/main/java/com/nextcloud/talk/utils/ssl/KeyManager.java
  • app/src/main/java/com/nextcloud/talk/viewmodels/CallRecordingViewModel.kt
  • app/src/main/res/layout/activity_message_search.xml
  • app/src/main/res/layout/rv_item_load_more.xml
  • app/src/main/res/layout/rv_item_search_message.xml
  • app/src/main/res/menu/menu_search.xml
  • app/src/main/res/values/dimens.xml
  • app/src/main/res/values/strings.xml
  • app/src/test/java/com/nextcloud/talk/contacts/ContactsViewModelTest.kt
  • app/src/test/java/com/nextcloud/talk/conversationcreation/ConversationCreationViewModelTest.kt
  • app/src/test/java/com/nextcloud/talk/conversationlist/data/network/ConversationListFreshnessIntegrationTest.kt
  • app/src/test/java/com/nextcloud/talk/data/user/UsersDaoPartialUpdateTest.kt
  • app/src/test/java/com/nextcloud/talk/data/user/UsersDaoRepairTest.kt
  • app/src/test/java/com/nextcloud/talk/data/user/UsersRepositoryImplTest.kt
  • app/src/test/java/com/nextcloud/talk/jobs/CapabilitiesFetcherTest.kt
  • app/src/test/java/com/nextcloud/talk/location/viewmodels/LocationPickerViewModelTest.kt
  • app/src/test/java/com/nextcloud/talk/messagesearch/MessageSearchHelperTest.kt
  • app/src/test/java/com/nextcloud/talk/users/DefaultAccountProviderTest.kt
  • app/src/test/java/com/nextcloud/talk/users/UserManagerTest.kt
  • app/src/test/java/com/nextcloud/talk/viewmodels/CallRecordingViewModelTest.kt
💤 Files with no reviewable changes (19)
  • app/src/main/res/values/dimens.xml
  • app/src/main/java/com/nextcloud/talk/account/ServerSelectionActivity.kt
  • app/src/main/java/com/nextcloud/talk/utils/database/user/CurrentUserProviderOld.kt
  • app/src/main/res/layout/rv_item_search_message.xml
  • app/src/main/res/menu/menu_search.xml
  • app/src/main/res/layout/activity_message_search.xml
  • app/src/main/AndroidManifest.xml
  • app/src/main/java/com/nextcloud/talk/logger/ui/LogsActivity.kt
  • app/src/main/java/com/nextcloud/talk/adapters/items/LoadMoreResultsItem.kt
  • app/src/main/java/com/nextcloud/talk/adapters/items/MessageResultItem.kt
  • app/src/main/java/com/nextcloud/talk/utils/database/user/CurrentUserProviderOldImpl.kt
  • app/src/main/java/com/nextcloud/talk/utils/database/user/CurrentUserProvider.kt
  • app/src/main/res/layout/rv_item_load_more.xml
  • app/src/main/res/values/strings.xml
  • app/src/main/java/com/nextcloud/talk/messagesearch/MessageSearchActivity.kt
  • app/src/main/java/com/nextcloud/talk/messagesearch/MessageSearchViewModel.kt
  • app/src/main/java/com/nextcloud/talk/utils/rx/SearchViewObservable.kt
  • app/src/main/java/com/nextcloud/talk/utils/database/user/CurrentUserProviderImpl.kt
  • app/src/main/java/com/nextcloud/talk/utils/database/user/UserModule.kt

Included review availability: This review used your included allowance. Your plan provides up to 4 included reviews per hour; 3 remain after this review.

Comment thread app/src/main/java/com/nextcloud/talk/profile/ProfileActivity.kt Outdated
Chat and call screens read the global active user instead of the
account they were opened for. Any setUserAsActive() while such a screen
was open (e.g. an incoming call for another account) silently switched
it to the other account or mixed data and credentials of both.

- ChatActivity/ChatViewModel are bound to the user id passed in the
  intent (KEY_INTERNAL_USER_ID); all chat launches go through
  ChatActivity.createIntent() and pass the user id
- CallActivity, RaiseHandViewModel and CallRecordingViewModel use the
  user of the call
- NotificationWorker no longer switches the active account on incoming
  calls
- UserManager.userFlow(id) observes a single user
- getActiveUser() no longer writes to the database on every read; the
  "multiple active users" self-heal runs once at app start
- setUserAsActive() publishes the stored row
- CurrentUserProviderOldImpl: fix racy cache initialization

Assisted-by: Claude Code:claude-opus-5
Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
- Switching accounts opens the conversation list of the new account in
  a cleared task, so no screen of the previous account stays in the
  back stack (account dialog, SwitchAccountActivity, ecosystem account
  handoff)
- ConversationsListActivity keeps its account across new intents and
  opens a fresh instance for another account
- Notification receivers require the user id instead of falling back
  to the active account; the recording actions now pass it
- LeaveConversationWorker, UploadAndShareFilesWorker and
  DownloadFileToCacheWorker use the user id from their input data

Assisted-by: Claude Code:claude-opus-5
Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
ConversationsListViewModel, ConversationTagsViewModel and
ThreadsOverviewViewModel get the user of their screen via assisted
injection instead of reading the active account at construction time.
FilterConversationFragment receives the user as argument.

Opening the conversation list of an account makes it the last used
(default) account, which the account switcher and status views use.

Assisted-by: Claude Code:claude-opus-5
Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
ConversationInfoActivity and ConversationInfoEditActivity use the user
id they were started with, and pass it on to the screens they open.
ConversationInfoEditViewModel and DialogBanListFragment get the user
from their screen.

Assisted-by: Claude Code:claude-opus-5
Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
…account

SharedItemsActivity, MediaViewerActivity and RemoteFileBrowserActivity
use the user id they were started with; all launchers pass it.
RemoteFileBrowserItemsViewModel gets the user via assisted injection.

Assisted-by: Claude Code:claude-opus-5
Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
- MessageSearchViewModel gets the user of its screen
- The poll dialogs use the user they are created with (the main poll
  dialog already received it but ignored it) and pass it to their
  view models

Assisted-by: Claude Code:claude-opus-5
Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
Scheduled messages, reminders, mention autocomplete, location sharing,
translation and context chat use the user of the chat they belong to
instead of the active account.

Assisted-by: Claude Code:claude-opus-5
Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
…eir account

ContactsViewModel, ConversationCreationViewModel and
OpenConversationsViewModel get the user of their screen via assisted
injection. Contacts, conversation creation, open conversations and
invitations use the user id they were started with; all launchers pass
it.

Assisted-by: Claude Code:claude-opus-5
Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
SettingsActivity, ProfileActivity and DiagnosisActivity use the user id
they were started with; the conversation list and settings pass it.
DiagnosisViewModel gets the user via assisted injection and the
diagnosis account section shows that user.

Assisted-by: Claude Code:claude-opus-5
Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
Screens were themed with the server colors of the active account, so a
screen of another account (e.g. an incoming call for it) used the wrong
colors.

- ViewThemeUtilsFactory creates ViewThemeUtils for a given account
- Activities bound to an account apply its theme right after injection
  (BaseActivity.applyUserTheme)
- Fragments, dialogs and views use the ViewThemeUtils of their hosting
  activity (hostViewThemeUtils)

Assisted-by: Claude Code:claude-opus-5
Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
All screens, workers and receivers now use the account they were
started for, so the "current user" providers are only needed where
there is no account context (entry points, share-to, account switcher,
default theme). Replace CurrentUserProviderOld and CurrentUserProvider
by DefaultAccountProvider, which makes that meaning explicit, and
rename UserManager.getCurrentUser()/currentUserFlow to
getDefaultUser()/defaultUserFlow.

Assisted-by: Claude Code:claude-opus-5
Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
Assisted-by: Claude Code:claude-opus-5
Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
…s after switching

The singleton conversations repository kept the observed account in a
global state that getRooms() only updated asynchronously, so the room
list of a newly opened account first emitted the conversations of the
previously shown account. roomListFlow() now takes the account id.

Assisted-by: Claude Code:claude-opus-5
Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
FLAG_ACTIVITY_NEW_TASK never matches an existing task because the app's
activities have an empty task affinity, so FLAG_ACTIVITY_NEW_TASK |
FLAG_ACTIVITY_CLEAR_TASK started a second app instance. Use
FLAG_ACTIVITY_CLEAR_TOP instead, which recreates the conversation list
of the current task for the new account and closes all screens above
it.

Assisted-by: Claude Code:claude-opus-5
Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
The user is loaded synchronously, so wrapping the setup in
lifecycleScope.launch and runCatching only hid that onStart()/onResume()
rely on it running within onCreate(). Run it sequentially and close the
chat if its account doesn't exist anymore.

Assisted-by: Claude Code:claude-opus-5
Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
ChatViewModel received its user twice: as id via assisted injection and
as a copy through initData(), which ChatActivity additionally kept for
itself. The view model now gets the user via assisted injection and
observes it, initData() derives credentials and URL from it, and
ChatActivity.conversationUser reads it from the view model, so activity
and view model always see the same, current values.

Assisted-by: Claude Code:claude-opus-5
Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
The user of the chat is loaded at the start of onCreate() and the chat
is closed if it doesn't exist, so it is always available afterwards.
Only onDestroy() still checks it, as it is also called in that case.

Assisted-by: Claude Code:claude-opus-5
Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
Activities bound to an account all repeated the same lookup and null
handling, and applyUserTheme() loaded the same user a second time.
BaseActivity now provides boundUser (loaded once and shared with
applyUserTheme()) and requireBoundUserOrFinish(), and injects
UserManager for its subclasses.

Assisted-by: Claude Code:claude-opus-5
Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
The activity isn't started from anywhere anymore, since the message
search moved into the chat. Remove it together with its view model,
adapter items, layouts, menu and the resources only it used.
MessageSearchHelper is still used by the chat and the conversation list.

Assisted-by: Claude Code:claude-opus-5
Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
BrowserLoginActivity, SharedItemsActivity and the account dialog
injected their view models directly as fields, so the view models were
not kept across configuration changes and never cleared, which left
their coroutines running. Create them through the view model factory
instead, scoped to their activity. Also remove commented-out code from
ViewModelModule.

Assisted-by: Claude Code:claude-opus-5
Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
The status message sheet initialized its view model with the stored
status every time it was composed, so after a rotation an unsaved edit
was replaced by the stored status. Initialize it only once per opening
of the sheet.

Assisted-by: Claude Code:claude-opus-5
Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
Taking part in a call is an active use of its account, like opening one
of its notifications, so it becomes the last used account. A call that
only rings or is declined doesn't change it.

Assisted-by: Claude Code:claude-opus-5
Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
The injection of a subclass also injects BaseActivity's viewThemeUtils
and replaced the account theme with the one of the default account.
Since the theme was only applied once, screens of a non-default account
(e.g. an incoming call) were shown with the default account's colors.
Keep the account theme and assign it again after the subclass injection.

Assisted-by: Claude Code:claude-opus-5
Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
roomViewState kept its Success, so a recreated conversation list (e.g.
after rotation) received it again and reopened the chat, now that
ContactsViewModel is kept across configuration changes. In the contacts
screen, every contact row opened the chat on Success, during
composition. Reset the state once it's handled, and open the chat from
a single place in ContactsActivity.

Assisted-by: Claude Code:claude-opus-5
Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
BaseActivity decided with the default account whether a link points to
the own server. A chat of another account therefore opened links to its
server in the browser, or, with two accounts on the same server, opened
the conversation as the default account. Use the account of the screen
if it has one.

Assisted-by: Claude Code:claude-opus-5
Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
…ult account

The status view models read the default account themselves when they were
created and kept it for the lifetime of the hosting activity. When the default
account changed while the conversation list stayed open (e.g. after a call for
another account), the account dialog showed and changed the status of the old
account.

The dialog now reads the default account once and passes it to the view models
via assisted injection. They are keyed by the account id, so a new pair is
created when the default account changes.

Assisted-by: Claude Code:claude-opus-5
Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
…he list

The conversation list stayed bound to the account it was opened for. After
joining a call for another account, which makes that account the default, and
switching to picture in picture, the list of the previous account was shown
while the account switcher already showed the called account as active.

When the list returns from the background and the default account changed in
the meantime, it now switches to that account. It does not switch while a
message is forwarded or shared, which would lose it.

Assisted-by: Claude Code:claude-opus-5
Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
…ccount

When an intent for another account reached the conversation list via
onNewIntent, the list relaunched the received intent unchanged. The activity is
exported, so that intent can come from another app, including flags like URI
permission grants (lint UnsafeIntentLaunch).

The list now starts a new explicit intent for itself that only takes over the
action, data and extras of the received one.

Assisted-by: Claude Code:claude-opus-5
Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
Background jobs and settings read a user, waited for the server and then wrote
back the whole row, including the "current" flag and the token as they were
when the user was read. If the default account changed in the meantime (e.g.
by a notification tap while the capabilities sync ran), the change was undone
or two accounts ended up marked as active, which hid accounts in the account
switcher. Reading the active user no longer repairs this, so it lasted until
the next app start.

Capabilities, signaling settings, display name, client certificate and
credentials are now stored with updates that only write their own columns.

Assisted-by: Claude Code:claude-opus-5
Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
… chooser

After switching the account, the share-to chooser recreated the conversation
list. The recreated list kept its intent with the previous account's id and its
view models, so it showed the previous account again and made it the default
again. Shared content went to the previous account.

The chooser now replaces the list by a new one for the chosen account, which
gets the shared content and the read access to shared files.

Assisted-by: Claude Code:claude-opus-5
Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
Chat and conversation list mark the app as dialing before starting a call,
which hangup() resets. Since onDestroy() only cleans up calls that were set up,
a call screen closing early (missing account or data, unsupported call
encryption) left the dialing state set, so chats no longer left their room when
paused.

Assisted-by: Claude Code:claude-opus-5
Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
DialogBanListFragment only had a constructor with parameters, so the fragment
manager could not recreate it after a configuration change or process death
and the app crashed with an InstantiationException, e.g. when rotating the
device while the ban list was open.

The room token and the account are now passed as fragment arguments.

Assisted-by: Claude Code:claude-opus-5
Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
When the account of an upload no longer existed, e.g. because it was removed
while the upload waited for a retry, the worker threw before the file name,
the notification manager and the placeholder ids were set. The failure handling
then threw again, so neither the failure notification was shown nor the
placeholder message marked as failed.

The placeholder ids are now read first, a missing account fails the upload
directly, and the failure notification no longer depends on the upload having
started.

Assisted-by: Claude Code:claude-opus-5
Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
The startup repair counted the active users, read the active one and then set
it as the only active user in separate statements. A switch of the default
account in between (e.g. by a notification tap during app start) was undone.

The repair is now a single UPDATE that keeps only the active user with the
highest id, which is the one getActiveUser() returns.

Assisted-by: Claude Code:claude-opus-5
Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
The location picker started the address search without the account, so it
used the colors of the default account instead of the chat's account. The
account is now passed on and the address search is bound to it.

Assisted-by: Claude Code:claude-opus-5
Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
The id written to the intent keeps an activity on its account across
configuration changes, but not after process death, where the system restores
the original intent.

Assisted-by: Claude Code:claude-opus-5
Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
The avatar picker was only created once the conversation had loaded from the
server. When the activity was recreated after process death, e.g. while the
camera was open, the camera or image picker result arrived before the
conversation loaded and was silently dropped.

The picker is now created with the bound account in onCreate().

Assisted-by: Claude Code:claude-opus-5
Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
Sharing to Talk or opening it from another Nextcloud app goes directly to the
conversation list. Without any account, the list closed immediately without
any feedback. It now opens MainActivity, which starts the login.

MainActivity ignores accounts scheduled for deletion, so it doesn't open the
conversation list again while the last account is being removed, which has no
default account then.

Assisted-by: Claude Code:claude-opus-5
Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
The partial user updates are now @update(entity = UserEntity::class) methods
with partial entity classes instead of hand-written UPDATE queries. They still
only write their own columns.

Assisted-by: Claude Code:claude-opus-5
Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
The color scheme cache was keyed by the server URL only. Accounts on the same
server can have different colors (e.g. a personal primary color), so with
per-account theming the second account got the colors of the first one.
Changed server colors were also only used after an app restart.

The cache is now keyed by the theming capability.

Assisted-by: Claude Code:claude-opus-5
Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
The rename dialog creates its own ConversationInfoEditViewModel without
initializing it, so renameRoom() had no account and returned without sending
the request or showing an error.

The dialog now gets the account of the conversation list as an argument and
passes it to renameRoom().

Assisted-by: Claude Code:claude-opus-5
Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
The conversation list assigns its bound account in onCreate() before anything
reads it, so it is now a lateinit property instead of a nullable one. This
removes the null checks and the unreachable "currentUser was null" branch.

Assisted-by: Claude Code:claude-opus-5
Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
Profile and settings load their account again, in onResume() and after the
capabilities sync, and then accessed it with !!. When the account had been
removed in the meantime, this crashed. They now finish instead.

Assisted-by: Claude Code:claude-opus-5
Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
Signed-off-by: Marcel Hibbe <dev@mhibbe.de>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: 37aaaa81-c164-4722-b2f6-2c5f88147b9d

📥 Commits

Reviewing files that changed from the base of the PR and between 90f3bd5 and 55bd468.

📒 Files selected for processing (7)
  • app/src/main/java/com/nextcloud/talk/conversation/RenameConversationDialogFragment.kt
  • app/src/main/java/com/nextcloud/talk/conversationinfoedit/viewmodel/ConversationInfoEditViewModel.kt
  • app/src/main/java/com/nextcloud/talk/conversationlist/ConversationsListActivity.kt
  • app/src/main/java/com/nextcloud/talk/conversationlist/viewmodels/ConversationsListViewModel.kt
  • app/src/main/java/com/nextcloud/talk/profile/ProfileActivity.kt
  • app/src/main/java/com/nextcloud/talk/settings/SettingsActivity.kt
  • app/src/main/java/com/nextcloud/talk/ui/theme/MaterialSchemesProviderImpl.kt

Included review availability: This review used your included allowance. Your plan provides up to 4 included reviews per hour; 3 remain after this review.

Comment on lines +245 to +258
val newUserId = intent.getLongExtra(KEY_INTERNAL_USER_ID, 0L)
if (newUserId != 0L && newUserId != currentUser.id) {
// The list is bound to one account, so open a fresh instance for the other one. The activity is
// exported, so the received intent is not launched again as it is: the new one only targets this
// activity and takes over no flags like URI permission grants.
val accountIntent = Intent(this, ConversationsListActivity::class.java).apply {
action = intent.action
data = intent.data
intent.extras?.let { putExtras(it) }
}
finish()
startActivity(accountIntent)
return
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Grant the URI read permission again when an onNewIntent share switches accounts.

This branch rebuilds the intent with action, data, and extras only. It does not copy type or clipData, and it does not add FLAG_GRANT_READ_URI_PERMISSION. Suppose a share intent (ACTION_SEND with clipData URIs) arrives through onNewIntent for a different account. The new list then loses the shared files. extractFilesFromClipData falls back to intent.data, which is null for most shares. The read grant also ends when this activity finishes. continueShareWithAccount already handles these fields correctly. Reuse that function here.

Proposed fix
-            val accountIntent = Intent(this, ConversationsListActivity::class.java).apply {
-                action = intent.action
-                data = intent.data
-                intent.extras?.let { putExtras(it) }
-            }
-            finish()
-            startActivity(accountIntent)
+            setIntent(intent)
+            continueShareWithAccount(newUserId)
             return
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
val newUserId = intent.getLongExtra(KEY_INTERNAL_USER_ID, 0L)
if (newUserId != 0L && newUserId != currentUser.id) {
// The list is bound to one account, so open a fresh instance for the other one. The activity is
// exported, so the received intent is not launched again as it is: the new one only targets this
// activity and takes over no flags like URI permission grants.
val accountIntent = Intent(this, ConversationsListActivity::class.java).apply {
action = intent.action
data = intent.data
intent.extras?.let { putExtras(it) }
}
finish()
startActivity(accountIntent)
return
}
val newUserId = intent.getLongExtra(KEY_INTERNAL_USER_ID, 0L)
if (newUserId != 0L && newUserId != currentUser.id) {
// The list is bound to one account, so open a fresh instance for the other one. The activity is
// exported, so the received intent is not launched again as it is: the new one only targets this
// activity and takes over no flags like URI permission grants.
setIntent(intent)
continueShareWithAccount(newUserId)
return
}

@github-actions

Copy link
Copy Markdown
Contributor

Codacy

Lint

TypemasterPR
Warnings138138
Errors1817

SpotBugs

CategoryBaseNew
Bad practice77
Correctness1111
Dodgy code4545
Internationalization33
Malicious code vulnerability33
Performance88
Security1111
Total8888

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

3. to review Waiting for reviews AI assisted

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant