Repository navigation
Improve intent handling - #6989
Merged
Merged
Conversation
ShareActivity is now the entry point of the share sheet and its direct share targets. It copies the shared files into the cache of the app, so the chat and the upload don't depend on the read access of the other app and no media permission is needed, and opens the list in share mode with that content. A direct share target is only preselected if the app published it, uris and files that can't be read are left out, and the copies are kept until their upload is done. The chosen chat opens in the running task of the app, or in a new one with the conversation list below it, and the attachment preview asks before uploading instead of a dialog in the list. Assisted-by: Claude Code:claude-opus-5-5 Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
…MainActivity MainActivity only routed the intents it got on to other screens. It is removed: the launcher activity is now an alias of the conversation list under its old name, which home-screen icons and pinned shortcuts refer to, and a launcher tap brings the task of the app to the front by itself. - Entry points: the conversation list has an internal alias for the intents the app starts itself (InternalEntryPoint). Deep links are taken through DeepLinkActivity. - Deep links and contact chats: DeepLinkActivity, the entry point for nextcloudtalk:// links and the chat action of phone book contacts, passes their uri on to the list. The list routes them in DeepLinkRoutingViewModel: it asks before joining a conversation that is not in the local list and before starting a conversation with a contact, doesn't use an account scheduled for deletion, and keeps a question across configuration changes and process death. A link that opens no conversation does nothing. - Notifications: a notification opens its chat, the invitations or the list of its account directly, as a new back stack built with TaskStackBuilder, with the list of the account below. Their pending intents and the microphone action of picture-in-picture are immutable. A room token is checked with the token pattern. - Shortcuts: the conversation shortcuts of the list and of notifications open the deep link of their conversation, and old ones that only had extras are given it. - Tasks: the app opens its chats in the running task of the app, removes the tasks of the app that are only left in the recents, and tolerates a task that was removed meanwhile (AppTasks). The app is restarted in a fresh task, and without an account the list opens the login. The crash screen also tolerates removed tasks when it removes the other tasks of the app. - Screen lock: the key is created once when the app starts with the conversation list instead of whenever it is resumed, and the check creates it when it is missing, e.g. when the app is started at a chat of a notification. - The unused start of a call from the extras of a notification, the layout of MainActivity and ActionBarProvider are removed. Assisted-by: Claude Code:claude-opus-5-5 Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
The launch screen was the window background of the application theme. The conversation list, the launcher activity, now uses the splash screen of androidx.core:core-splashscreen, which Android only shows when the app is launched with the list, e.g. not when a notification opens a chat. A deep link that starts the app keeps it until its chat covers the list or it asks whether to join, at most 2 seconds. Switching the account no longer replaces the list at the root of the task, which Android showed with the color of the splash screen: the list switches itself in place. The theme is taken from #6937. Assisted-by: Claude Code:claude-opus-5-5 Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
The conversation list, which routes a deep link, showed the lock screen and routed the link at the same time. The routing now waits until the list is resumed after unlocking. Assisted-by: Claude Code:claude-opus-5-5 Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
BaseActivity.startActivity turned any ACTION_VIEW of a call link of the own server into a ChatActivity intent, also the explicit forward of a link to DeepLinkActivity, which then skipped the routing of the link. Explicit intents now start the screen they name unchanged. Assisted-by: Claude Code:claude-opus-5-5 Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
…nlocked The pending shortcut token was used by the room list collector, which runs regardless of the lifecycle of the list. The token is now only used while the list is resumed and not covered by the lock screen, as DeepLinkRouting does. After unlocking, the list is resumed and the room list emitted again, so the conversation opens then. Assisted-by: Claude Code:claude-opus-5-5 Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
isTrustedShareUri only treated a uri as the app's own when its authority equaled the package name, which missed the other providers of the app, e.g. of the ImagePicker library, and uris with a user id prefix. The user id prefix is now ignored, and only the shared attachments and copies of shared files of its FileProvider are taken from the own app. Assisted-by: Claude Code:claude-opus-5-5 Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
…ain intent onRestart relaunched the list with a copy of its own intent. If the list was created for a deep link, the new list routed that link again: a declined "Join conversation?" was asked again, the "no account" toast showed again, or the old chat reopened and switched the default account back. For a contact chat link Talk can't read, the read permission grant made startActivity throw a SecurityException. Forwarding and sharing are already excluded there, so the new list gets a plain intent with only the account. Assisted-by: Claude Code:claude-opus-5-5 Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
BaseActivity writes the default account into a launcher intent, so that a recreation after a configuration change stays on that account. But the recreation reuses the same intent, and removeExtrasOfApp removed the account again because the component is the launcher alias. The list then bound the default account of that time, while its view models stayed on the previous one: e.g. after picking another account while sharing, rotating and going back, the list showed the conversations of one account with the other account, and opened them with it. The intent is now set to the one of the internal entry point with the account, as showNewIntent does. The intents of other apps that the list doesn't show were passed on before, and their extras of the app removed. Assisted-by: Claude Code:claude-opus-5-5 Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
…s chat opens The deep link routing made the account of the conversation active while its chat was still held back, e.g. by the lock screen covering the list. After unlocking, the list of the previous account ran onRestart first, saw the new default account and relaunched for it with its old intent. The pending chat was dropped with the finishing list, so only the list of the other account showed. The same happened when the default changed while the list was stopped. The account is no longer made active there. The list of the account that the chat opens above makes it active in onCreate. Assisted-by: Claude Code:claude-opus-5-5 Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
Assisted-by: Claude Code:claude-opus-5-5 Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
…or deep link started The launcher is an alias of the conversation list with the standard launch mode. A task that a notification or DeepLinkActivity started has no MAIN/LAUNCHER root intent, so a launcher tap added another list on top of it instead of bringing it to the front, and each list ran the start-up code again. A list that the launcher starts on top of a running task of the app, not as its root, now finishes right away, which shows that task. A list that another app starts in its own task with the launcher intent is still shown. taskIdCompat moves from CallActivity to AppTasks for that check. Assisted-by: Claude Code:claude-opus-5-5 Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
Whether the app was locked depended on the lock timeout, and going to the background reset the time of the last use also while the app was locked. The app is now locked until LockedActivity is unlocked by authenticating: while it is locked, every screen that is resumed shows the lock screen again. The time of the last use isn't reset while the app is locked, so it is still locked after its process was killed. Assisted-by: Claude Code:claude-opus-5-5 Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
ShareActivity copies each shared file into the cache and passes on the FileProvider uri of the copy. FileUtils.getFileFromUri, used by the attachment preview and UploadAndShareFilesWorker, copied every content uri into the cache again, and that second copy was never deleted. So a shared file needed twice its size in the cache, and a share could fail when free space ran short. getFileFromUri now returns the file behind an own FileProvider uri of the copies of shared files or the shared attachments, resolved like the check of shared uris, instead of copying it again. isSharedAttachmentProviderPath becomes sharedAttachmentProviderFile, which returns that file. Assisted-by: Claude Code:claude-opus-5-5 Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
…ng it Leaving or deleting a conversation restarted the app with the launcher list, which shows the default account. A chat opened from a notification, the media notification or the audio player of another account doesn't make that account the default anymore, so afterwards the list of the default account showed. The same happened after a call of another account when no task of the app was left. Leaving or deleting a conversation now recreates the list in the task for the conversation's account (createAccountSwitchIntent with CLEAR_TOP), which also closes the screens above it. NEW_TASK isn't used there: with the empty task affinity it would start a second task when the list in the task was started from the launcher. A call without a task of the app left starts the list of its account in a new task. Assisted-by: Claude Code:claude-opus-5-5 Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
The tasks of the app were found by the alias in the base intent of their root. Before Android 10, the base intent of a task that an alias started names the target of the alias instead, so no task of the app was found: openChatInAppTask always took its fallback, which closed the screens open in the app task instead of opening the chat on top, and CallActivity could take a share task as the primary task and remove the real app task. The root is now taken from origActivity, which names the alias on every version, before the base intent. Assisted-by: Claude Code:claude-opus-5-5 Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
…ecents Before Android 10, a task relaunched from the recents gets its intent with the target of the alias it was started through, so the list of a deep link that DeepLinkActivity passed on took it for a VIEW intent of another app. It passed the old link on to DeepLinkActivity again, and the forward dropped FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY, which bypassed the history guard of DeepLinkRouting: the link was routed again. A VIEW intent launched from the recents is no longer passed on, as its link was taken already. The list shows without routing it. Assisted-by: Claude Code:claude-opus-5-5 Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
…rtcut The conversation shortcuts, also those of notifications, opened their conversation through a nextcloudtalk:// deep link, which names only the host and user name of the account. With two accounts of the same user on one host, e.g. on another path or port, a shortcut of one account opened or asked to join the conversation on the other. The update of the shortcuts of earlier versions turned exact notification shortcuts into such links. The system starts a shortcut as the app that published it, so the shortcuts now target the internal entry point of the conversation list with the account and room token as extras, like the direct share targets. The shortcuts of earlier versions, with extras or a deep link, are given this intent, with the account and conversation of their id. Only legacy home screen shortcuts, which the launcher starts as itself, keep the deep link. The list now takes the room token of a shortcut once: it opened the conversation again whenever the list was resumed, e.g. when going back from it. A token not opened yet is kept with the instance state. Assisted-by: Claude Code:claude-opus-5-5 Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
The shared files were copied in a blocking loop. Leaving ShareActivity cancelled the coroutine, but every file was still copied to the end and then left in the cache, as copyToCache also caught the cancellation. A recreation of ShareActivity, e.g. after the font scale or locale changed, started a second full copy into a new directory. The copy now stops between files and buffers when it is cancelled, and removes the copies of the share. ShareActivity handles all configuration changes itself, as it only shows a progress indicator. Assisted-by: Claude Code:claude-opus-5-5 Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
The copies of shared files were only pruned when a later share with a file started. Prune the done shares also at app start, and remove the copies of a share right away when it is left in the conversation list or in the attachment preview, unless an upload of them is unfinished. Assisted-by: Claude Code:claude-opus-5-5 Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
…eated After confirming to start a conversation with a contact, a failed createRoom, e.g. offline or rejected by the server, or a response without a conversation token, silently stayed in the conversation list. Let the failure reach the routing, which shows the error, as for any failed step. Assisted-by: Claude Code:claude-opus-5-5 Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
The list is no longer a single singleTask activity, so the setup meant for once per start of the app, the push registration and the key of the screen lock, ran for every new list, e.g. for a notification, a share or another account. Run it once per process. Assisted-by: Claude Code:claude-opus-5-5 Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
The user id of a contact chat was read from any data row of the passed uri, e.g. the email address of a contact, which has the form of a user id at a server as well. Read it only from a row of Talk's contact chat type. Assisted-by: Claude Code:claude-opus-5-5 Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
A list that finishes in onCreate, e.g. for a launcher tap on a task of the app or an intent it passes on, has neither its account nor its deep link routing. The system doesn't pass new intents to a finishing activity; guard onNewIntent anyway instead of reading them uninitialized. Assisted-by: Claude Code:claude-opus-5-5 Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
The system clears the cache when the storage runs low, also while the uploads of the copies still wait for the network. Keep the copies in the files directory instead, which the app prunes itself, and exclude them from backups. Assisted-by: Claude Code:claude-opus-5-5 Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
…st intent Assisted-by: Claude Code:claude-opus-5-5 Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
…pository The routing of contact chats built the create room request itself, as ContactsRepository.createRoom already does. Assisted-by: Claude Code:claude-opus-5-5 Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
setUpBoundUserOrFinish already opens the login when there is no account, so the list doesn't read the accounts on the main thread for it beforehand. Assisted-by: Claude Code:claude-opus-5-5 Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
Assisted-by: Claude Code:claude-opus-5-5 Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
The room token of the chat intent was checked with the token pattern where it enters the app, but not by the chat itself. A chat started without a valid token now finishes instead of crashing, and a room switch only goes to a valid token. Assisted-by: Claude Code:claude-opus-5-5 Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
…user
getFileFromUri didn't recognise a uri with the user id prefix of the own user
("0@authority") as one of the app's copies and copied it into the cache by
its name, where a file of the same name would have been taken instead, and
its upload got no tag of its share, so its copy could be removed while the
upload still waited. They now drop a prefix of the own user. A uri of
another user, e.g. of the app in another profile, isn't taken for one of the
app's copies.
Assisted-by: Claude Code:claude-opus-5-5
Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
…move The removal of the copies asked WorkManager for the uploads of each share on its own, one after another, when the app starts and when a share starts. It now asks once for all of them. Assisted-by: Claude Code:claude-opus-5-5 Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
… and shared files AGENTS.md asks for brief comments matching the surrounding code. Assisted-by: Claude Code:claude-opus-5-5 Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
…the recents A task a shortcut started keeps its intent, so when its list was gone and the task was relaunched from the recents, the conversation of the shortcut was opened again. Assisted-by: Claude Code:claude-opus-5-5 Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
When the default account changed while the list was in the background, the list switched to it in onRestart, before onNewIntent() delivered e.g. a shortcut or deep link, which was then lost with the old list. The check is now armed when the list is stopped and done on resume, and not for a new intent, which chooses its account itself. A list that switches doesn't load its data anymore. It waits while the lock screen covers the list: the list of the default account would start above it and open a second lock screen. Assisted-by: Claude Code:claude-opus-5-5 Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
Leaving the app while the lock screen is shown, e.g. for the screen lock of the device, set the lock check for its return, which the screen below then ran after unlocking: with no timeout it locked again right away. Assisted-by: Claude Code:claude-opus-5-5 Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
A list that passed a share on to ShareActivity still removed the copies its uris named when it finished, which could be those of another share in progress. Assisted-by: Claude Code:claude-opus-5-5 Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
The routing of deep links and the list each read the flag of a relaunch from the recents themselves, which all have to agree on. Assisted-by: Claude Code:claude-opus-5-5 Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
…Android 12 on From Android 12 on, cloud backups and device transfers of an app targeting it ignore the full backup rules and only follow the data extraction rules, so the copies were backed up. Assisted-by: Claude Code:claude-opus-5-5 Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
The conversation of a shortcut that wasn't listed yet opened whenever a sync added it while the list stayed shown, also minutes later. It now opens at the latest when the sync of the list ends, reading the stored conversations, or a snackbar tells that it wasn't found, also on a slow network. A search started meanwhile drops it, so it doesn't open over what the user does. The list keeps it in its saved state only when it is recreated, not when it was left: before Android 9, the state is saved before the list is stopped, which restored a conversation the user had left. Assisted-by: Claude Code:claude-opus-5-5 Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
…r room Opening the chat of an audio message from the player of a chat put the new chat on top of the shown one, so chats piled up, each joined to its room. The chat of another room now replaces the shown one. A thread and the chat of its room stay, as Back leads from one to the other. Assisted-by: Claude Code:claude-opus-5-5 Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
The call recognised the task of a share by the action of its intent, beside AppTasks, which tells it by its root. Assisted-by: Claude Code:claude-opus-5-5 Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
…lf shortcut The note to self shortcut opened its chat with the focus in the message input, which isn't needed. The extra, and the state the conversation list kept for it while the conversation of a shortcut was pending, are removed. Assisted-by: Claude Code:claude-opus-5-5 Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
It holds the conversation of any shortcut that started the list, the conversation shortcuts as well as the direct share targets. Assisted-by: Claude Code:claude-opus-5-5 Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
The photo picker and the file picker grant the read access to a picked file only to the chat that picked it, until it is closed or the process is killed. An upload that waited for the network, e.g. while the app was in the background, then couldn't read its file and failed with a SecurityException. The access is now persisted when the file is sent, and released when its upload ends or is cancelled, unless another upload of the file still needs it, and for leftovers when the app starts. Assisted-by: Claude Code:claude-opus-5-5 Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
Since Android 13, the system shows the splash screen of a start that isn't one of the launcher as a plain color, without the icon of the app. The conversation list keeps it while it routes a link that started the app, so a link showed a blue screen without the logo. DeepLinkActivity now asks for the splash screen with the icon. Assisted-by: Claude Code:claude-opus-5-5 Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
…ithout unlocking Without a fingerprint, or after the biometric prompt failed, the lock screen shows the screen lock of the device. Leaving it with Back returned to the lock screen, which asked again right away, so the screen lock came back and the app couldn't be left. It now leaves the app like Back on the lock screen. Closing the biometric prompt with Back keeps the lock screen instead of asking for the screen lock of the device, and its callbacks run on the main thread. Assisted-by: Claude Code:claude-opus-5-5 Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
…a link A nextcloudtalk:// link to a known conversation opened its chat right away, and the chat of a voice room joins its call as soon as it has joined the room. Such a link now opens the voice room through the conversation list, like a conversation shortcut, which asks whether to join the call, as when it is opened from the list. Assisted-by: Claude Code:claude-opus-5-5 Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
…list LoginIT started the app through the launcher intent into an ActivityScenario<Activity>, which has no defaultAccountProvider, so the android tests didn't compile. The launcher alias opens the conversation list, so the scenario is typed with ConversationsListActivity. Assisted-by: Claude Code:claude-opus-5-5 Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
room-testing-android and junit-ktx were dependencies of the app, so it shipped junit and hamcrest-core 1.3. The android test APK leaves out what the app already has and is held to its versions, so it lacked org.hamcrest.Matchers and Espresso's withId() failed with a NoClassDefFoundError. Both are only used by the android tests and are now dependencies of those. The changed dependencies need the checksum of core-common-2.1.0.pom. Assisted-by: Claude Code:claude-opus-5-5 Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
Lint takes RecentTaskInfo.origActivity for the field of TaskInfo, which was added in API 29, and reports it as a new API. RecentTaskInfo declares the field itself since API 1 and only inherits it since API 29, and the bytecode reads it from RecentTaskInfo, so it exists on every supported version. The check is suppressed for the getter that reads it. Assisted-by: Claude Code:claude-opus-5-5 Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
Result.Retry is restricted to the library group of WorkManager, so lint reported its use in UploadAndShareFilesWorker. The result is now compared with Result.retry(), which equals every retry, so the access to the picked file is still kept for a retry and released otherwise. Assisted-by: Claude Code:claude-opus-5-5 Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
AndyScherzinger
approved these changes
Nov 3, 2026
Contributor
Contributor
📱 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Reworks how the app is opened: from links, shares, shortcuts and notifications. The goal is more consistent navigation and more reliable behaviour.
Changes
nextcloudtalk://links are handled in one place. For conversations and calls the user hasn't joined yet, the app asks before continuing. The splash screen now shows the app logo when the app isopened from a link.
Testing
🏁 Checklist
/backport to stable-xx.x🤖 AI (if applicable)