New user handling - #6787
New user handling#6787mahibi wants to merge 57 commits into
Conversation
📱 QA build
The QA build installs alongside a released Nextcloud app, so you can keep Downloading the file requires a GitHub account, so open this link on the |
|
Navigate logical layers of code changes, visualize relationships, and explore their blast radius. 📝 WalkthroughWalkthroughThe 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 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 ReviewSecurity architecture risk: 🟠 High · up to 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
Security review detailsSecurity Blast Radius
Security Findings and Attack Paths
Trust Boundaries and Controls
Resilience and Maintainability Implications
Hardening Proposals
🚥 Pre-merge checks | ✅ 3 | ❌ 1 | ❓ 1❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (3 passed)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to GitHub limitations.
🟡 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 winKey the theme cache by the theme inputs, not only
baseUrl.When two accounts share a
baseUrlbut have differentthemingCapabilityvalues, the first account populates this cache entry. The newViewThemeUtilsFactory.forUserthen 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
📒 Files selected for processing (143)
app/src/androidTest/java/com/nextcloud/talk/ui/LoginIT.javaapp/src/main/AndroidManifest.xmlapp/src/main/java/com/nextcloud/talk/account/AccountVerificationActivity.ktapp/src/main/java/com/nextcloud/talk/account/BrowserLoginActivity.ktapp/src/main/java/com/nextcloud/talk/account/ServerSelectionActivity.ktapp/src/main/java/com/nextcloud/talk/account/SwitchAccountActivity.ktapp/src/main/java/com/nextcloud/talk/account/data/io/LocalLoginDataSource.ktapp/src/main/java/com/nextcloud/talk/activities/BaseActivity.ktapp/src/main/java/com/nextcloud/talk/activities/CallActivity.ktapp/src/main/java/com/nextcloud/talk/activities/MainActivity.ktapp/src/main/java/com/nextcloud/talk/adapters/items/LoadMoreResultsItem.ktapp/src/main/java/com/nextcloud/talk/adapters/items/MessageResultItem.ktapp/src/main/java/com/nextcloud/talk/application/NextcloudTalkApplication.ktapp/src/main/java/com/nextcloud/talk/attachmentpreview/FileAttachmentPreviewFragment.ktapp/src/main/java/com/nextcloud/talk/callnotification/CallNotificationActivity.ktapp/src/main/java/com/nextcloud/talk/chat/ChatActivity.ktapp/src/main/java/com/nextcloud/talk/chat/MessageInputFragment.ktapp/src/main/java/com/nextcloud/talk/chat/MessageInputVoiceRecordingFragment.ktapp/src/main/java/com/nextcloud/talk/chat/ScheduledMessagesActivity.ktapp/src/main/java/com/nextcloud/talk/chat/viewmodels/ChatViewModel.ktapp/src/main/java/com/nextcloud/talk/chat/viewmodels/ScheduledMessagesViewModel.ktapp/src/main/java/com/nextcloud/talk/chooseaccount/ChooseAccountDialogCompose.ktapp/src/main/java/com/nextcloud/talk/chooseaccount/ui/StatusMessageSheet.ktapp/src/main/java/com/nextcloud/talk/chooseaccount/viewmodel/StatusMessageViewModel.ktapp/src/main/java/com/nextcloud/talk/chooseaccount/viewmodel/StatusViewModel.ktapp/src/main/java/com/nextcloud/talk/contacts/ContactsActivity.ktapp/src/main/java/com/nextcloud/talk/contacts/ContactsScreen.ktapp/src/main/java/com/nextcloud/talk/contacts/ContactsViewModel.ktapp/src/main/java/com/nextcloud/talk/contacts/components/ContactItemRow.ktapp/src/main/java/com/nextcloud/talk/contacts/components/ConversationCreationOptions.ktapp/src/main/java/com/nextcloud/talk/contextchat/ContextChatViewModel.ktapp/src/main/java/com/nextcloud/talk/conversation/RenameConversationDialogFragment.ktapp/src/main/java/com/nextcloud/talk/conversationcreation/ConversationCreationActivity.ktapp/src/main/java/com/nextcloud/talk/conversationcreation/ui/CreatedConversation.ktapp/src/main/java/com/nextcloud/talk/conversationcreation/viewmodel/ConversationCreationViewModel.ktapp/src/main/java/com/nextcloud/talk/conversationinfo/ConversationInfoActivity.ktapp/src/main/java/com/nextcloud/talk/conversationinfoedit/ConversationInfoEditActivity.ktapp/src/main/java/com/nextcloud/talk/conversationinfoedit/viewmodel/ConversationInfoEditViewModel.ktapp/src/main/java/com/nextcloud/talk/conversationlist/ConversationsListActivity.ktapp/src/main/java/com/nextcloud/talk/conversationlist/data/OfflineConversationsRepository.ktapp/src/main/java/com/nextcloud/talk/conversationlist/data/network/OfflineFirstConversationsRepository.ktapp/src/main/java/com/nextcloud/talk/conversationlist/viewmodels/ConversationsListViewModel.ktapp/src/main/java/com/nextcloud/talk/conversationtags/viewmodels/ConversationTagsViewModel.ktapp/src/main/java/com/nextcloud/talk/dagger/modules/ViewModelModule.ktapp/src/main/java/com/nextcloud/talk/data/user/UsersDao.ktapp/src/main/java/com/nextcloud/talk/data/user/UsersRepository.ktapp/src/main/java/com/nextcloud/talk/data/user/UsersRepositoryImpl.ktapp/src/main/java/com/nextcloud/talk/data/user/model/UserPartialUpdates.ktapp/src/main/java/com/nextcloud/talk/diagnosis/DiagnosisActivity.ktapp/src/main/java/com/nextcloud/talk/diagnosis/DiagnosisElement.ktapp/src/main/java/com/nextcloud/talk/diagnosis/DiagnosisViewModel.ktapp/src/main/java/com/nextcloud/talk/invitation/InvitationsActivity.ktapp/src/main/java/com/nextcloud/talk/invitation/adapters/InvitationsAdapter.ktapp/src/main/java/com/nextcloud/talk/jobs/CapabilitiesFetcher.ktapp/src/main/java/com/nextcloud/talk/jobs/ContactAddressBookWorker.ktapp/src/main/java/com/nextcloud/talk/jobs/DownloadFileToCacheWorker.ktapp/src/main/java/com/nextcloud/talk/jobs/LeaveConversationWorker.ktapp/src/main/java/com/nextcloud/talk/jobs/NotificationWorker.ktapp/src/main/java/com/nextcloud/talk/jobs/SignalingSettingsWorker.javaapp/src/main/java/com/nextcloud/talk/jobs/UploadAndShareFilesWorker.ktapp/src/main/java/com/nextcloud/talk/location/GeocodingActivity.ktapp/src/main/java/com/nextcloud/talk/location/LocationPickerActivity.ktapp/src/main/java/com/nextcloud/talk/location/viewmodels/LocationPickerViewModel.ktapp/src/main/java/com/nextcloud/talk/logger/ui/LogsActivity.ktapp/src/main/java/com/nextcloud/talk/mediaviewer/activities/MediaViewerActivity.ktapp/src/main/java/com/nextcloud/talk/mediaviewer/viewmodels/MediaViewerViewModel.ktapp/src/main/java/com/nextcloud/talk/messagesearch/MessageSearchActivity.ktapp/src/main/java/com/nextcloud/talk/messagesearch/MessageSearchViewModel.ktapp/src/main/java/com/nextcloud/talk/openconversations/ListOpenConversationsActivity.ktapp/src/main/java/com/nextcloud/talk/openconversations/viewmodels/OpenConversationsViewModel.ktapp/src/main/java/com/nextcloud/talk/polls/ui/PollCreateDialogFragment.ktapp/src/main/java/com/nextcloud/talk/polls/ui/PollLoadingFragment.ktapp/src/main/java/com/nextcloud/talk/polls/ui/PollMainDialogFragment.ktapp/src/main/java/com/nextcloud/talk/polls/ui/PollResultsFragment.ktapp/src/main/java/com/nextcloud/talk/polls/ui/PollVoteFragment.ktapp/src/main/java/com/nextcloud/talk/polls/viewmodels/PollCreateViewModel.ktapp/src/main/java/com/nextcloud/talk/polls/viewmodels/PollMainViewModel.ktapp/src/main/java/com/nextcloud/talk/polls/viewmodels/PollVoteViewModel.ktapp/src/main/java/com/nextcloud/talk/presenters/MentionAutocompletePresenter.javaapp/src/main/java/com/nextcloud/talk/profile/ProfileActivity.ktapp/src/main/java/com/nextcloud/talk/raisehand/viewmodel/RaiseHandViewModel.ktapp/src/main/java/com/nextcloud/talk/receivers/DirectReplyReceiver.ktapp/src/main/java/com/nextcloud/talk/receivers/DismissRecordingAvailableReceiver.ktapp/src/main/java/com/nextcloud/talk/receivers/MarkAsReadReceiver.ktapp/src/main/java/com/nextcloud/talk/receivers/ShareRecordingToChatReceiver.ktapp/src/main/java/com/nextcloud/talk/remotefilebrowser/activities/RemoteFileBrowserActivity.ktapp/src/main/java/com/nextcloud/talk/remotefilebrowser/viewmodels/RemoteFileBrowserItemsViewModel.ktapp/src/main/java/com/nextcloud/talk/settings/SettingsActivity.ktapp/src/main/java/com/nextcloud/talk/shareditems/activities/SharedItemsActivity.ktapp/src/main/java/com/nextcloud/talk/shareditems/adapters/SharedItemsAdapter.ktapp/src/main/java/com/nextcloud/talk/threadsoverview/ThreadsOverviewActivity.ktapp/src/main/java/com/nextcloud/talk/threadsoverview/viewmodels/ThreadsOverviewViewModel.ktapp/src/main/java/com/nextcloud/talk/translate/ui/TranslateActivity.ktapp/src/main/java/com/nextcloud/talk/translate/viewmodels/TranslateViewModel.ktapp/src/main/java/com/nextcloud/talk/ui/PlaybackSpeedControl.ktapp/src/main/java/com/nextcloud/talk/ui/chooseaccount/ChooseAccountShareToDialogFragment.ktapp/src/main/java/com/nextcloud/talk/ui/chooseaccount/ChooseAccountShareToViewModel.ktapp/src/main/java/com/nextcloud/talk/ui/chooseaccount/model/ChooseAccountShareToViewState.ktapp/src/main/java/com/nextcloud/talk/ui/dialog/AttachmentDialog.ktapp/src/main/java/com/nextcloud/talk/ui/dialog/AudioOutputDialog.ktapp/src/main/java/com/nextcloud/talk/ui/dialog/DateTimeCompose.ktapp/src/main/java/com/nextcloud/talk/ui/dialog/DialogBanListFragment.ktapp/src/main/java/com/nextcloud/talk/ui/dialog/FilterConversationFragment.ktapp/src/main/java/com/nextcloud/talk/ui/dialog/MoreCallActionsDialog.ktapp/src/main/java/com/nextcloud/talk/ui/dialog/SaveToStorageDialogFragment.ktapp/src/main/java/com/nextcloud/talk/ui/dialog/SetPhoneNumberDialogFragment.ktapp/src/main/java/com/nextcloud/talk/ui/theme/HostViewThemeUtils.ktapp/src/main/java/com/nextcloud/talk/ui/theme/MaterialSchemesProvider.ktapp/src/main/java/com/nextcloud/talk/ui/theme/MaterialSchemesProviderImpl.ktapp/src/main/java/com/nextcloud/talk/ui/theme/ThemeModule.ktapp/src/main/java/com/nextcloud/talk/ui/theme/ViewThemeUtilsFactory.ktapp/src/main/java/com/nextcloud/talk/users/DefaultAccountProvider.ktapp/src/main/java/com/nextcloud/talk/users/UserManager.ktapp/src/main/java/com/nextcloud/talk/utils/FileViewerUtils.ktapp/src/main/java/com/nextcloud/talk/utils/PickImage.ktapp/src/main/java/com/nextcloud/talk/utils/database/user/CurrentUserProvider.ktapp/src/main/java/com/nextcloud/talk/utils/database/user/CurrentUserProviderImpl.ktapp/src/main/java/com/nextcloud/talk/utils/database/user/CurrentUserProviderOld.ktapp/src/main/java/com/nextcloud/talk/utils/database/user/CurrentUserProviderOldImpl.ktapp/src/main/java/com/nextcloud/talk/utils/database/user/UserModule.ktapp/src/main/java/com/nextcloud/talk/utils/preview/ComposePreviewUtils.ktapp/src/main/java/com/nextcloud/talk/utils/preview/ComposePreviewUtilsDaos.ktapp/src/main/java/com/nextcloud/talk/utils/rx/SearchViewObservable.ktapp/src/main/java/com/nextcloud/talk/utils/ssl/KeyManager.javaapp/src/main/java/com/nextcloud/talk/viewmodels/CallRecordingViewModel.ktapp/src/main/res/layout/activity_message_search.xmlapp/src/main/res/layout/rv_item_load_more.xmlapp/src/main/res/layout/rv_item_search_message.xmlapp/src/main/res/menu/menu_search.xmlapp/src/main/res/values/dimens.xmlapp/src/main/res/values/strings.xmlapp/src/test/java/com/nextcloud/talk/contacts/ContactsViewModelTest.ktapp/src/test/java/com/nextcloud/talk/conversationcreation/ConversationCreationViewModelTest.ktapp/src/test/java/com/nextcloud/talk/conversationlist/data/network/ConversationListFreshnessIntegrationTest.ktapp/src/test/java/com/nextcloud/talk/data/user/UsersDaoPartialUpdateTest.ktapp/src/test/java/com/nextcloud/talk/data/user/UsersDaoRepairTest.ktapp/src/test/java/com/nextcloud/talk/data/user/UsersRepositoryImplTest.ktapp/src/test/java/com/nextcloud/talk/jobs/CapabilitiesFetcherTest.ktapp/src/test/java/com/nextcloud/talk/location/viewmodels/LocationPickerViewModelTest.ktapp/src/test/java/com/nextcloud/talk/messagesearch/MessageSearchHelperTest.ktapp/src/test/java/com/nextcloud/talk/users/DefaultAccountProviderTest.ktapp/src/test/java/com/nextcloud/talk/users/UserManagerTest.ktapp/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.
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>
90f3bd5 to
55bd468
Compare
There was a problem hiding this comment.
Actionable comments posted: 1
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Advanced
Run ID: 37aaaa81-c164-4722-b2f6-2c5f88147b9d
📒 Files selected for processing (7)
app/src/main/java/com/nextcloud/talk/conversation/RenameConversationDialogFragment.ktapp/src/main/java/com/nextcloud/talk/conversationinfoedit/viewmodel/ConversationInfoEditViewModel.ktapp/src/main/java/com/nextcloud/talk/conversationlist/ConversationsListActivity.ktapp/src/main/java/com/nextcloud/talk/conversationlist/viewmodels/ConversationsListViewModel.ktapp/src/main/java/com/nextcloud/talk/profile/ProfileActivity.ktapp/src/main/java/com/nextcloud/talk/settings/SettingsActivity.ktapp/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.
| 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 | ||
| } |
There was a problem hiding this comment.
🎯 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.
| 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 | |
| } |
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:
runBlockingon the main threadgetActiveUser())Solution
BundleKeys.KEY_INTERNAL_USER_ID(a Long). An open screen stays on its account.currentflag. 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.DefaultAccountProvider.Other related fixes found along the way
UnsafeIntentLaunch).CallActivityfinishing inonCreate();DialogBanListFragmenton rotation; restored fragments accessing the chat view model.isDialingis reset when a call screen closes before setup.MessageSearchActivity(unused) is removed.How to get the account
KEY_INTERNAL_USER_ID(theuser.id, a Long). For chats, useChatActivity.createIntent(context, userId, roomToken, extras). A missing id fails in debug builds (check) and is logged as an error in release builds.BaseActivity)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 setoverride val allowsDefaultAccount = true, and then fall back to the default account.@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 aftersetUpBoundUserOrFinish(). In Compose:viewModel(key = "x-${user.id}", factory = ViewModelFactoryWithParams(X::class.java) { factory.build(user) }). Never read the default account in a view model.userManager.userFlow(id). For a one-time read:userManager.getUserWithId(id).User(Parcelable) or its id as fragment arguments vianewInstance(…), never as constructor parameters (they break recreation). Theme:viewThemeUtils = hostViewThemeUtils(activity, viewThemeUtils).hostViewThemeUtils(context, fallback).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.getLongExtra(KEY_INTERNAL_USER_ID, 0L), and stop if no account is found. There's no fallback to the default account.DefaultAccountProvider.getDefaultUser()(suspend) orgetDefaultUserBlocking(). Make an account the default only on user actions, withuserManager.setUserAsActive(user).BaseActivityViewThemeUtilsFactory.forUser(user).Usercopy that was read earlier: it overwritescurrentandtoken. Use the partial updates (userManager.updateCapabilities,updateExternalSignalingServer,updateDisplayName,updateClientCertificate,updateCredentials). For new columns, add a partial entity class indata/user/model/UserPartialUpdates.ktand an@Update(entity = UserEntity::class)method.Testing
UserManager,DefaultAccountProvider,ChatViewModel,ContactsViewModel,CapabilitiesFetcher, plus Room tests for the partial updates and the startuprepair (in-memory database).
🏁 Checklist
/backport to stable-xx.x🤖 AI (if applicable)