From d1029029bc426af490f355b77a75aa7f302eb5a5 Mon Sep 17 00:00:00 2001 From: Falk Loewert Date: Sun, 18 Oct 2026 21:53:53 +0200 Subject: [PATCH 1/2] Show unlock action for the file owner The server allows the owner of a file to unlock TYPE_USER locks (LockService::canUnlock), so a stale lock created by another client - e.g. the Text app, whose locks have the owner "text" - can be released by the user. The client only offered the action when the lock owner matched the current user, leaving users without a way to clear such locks until the lock timeout expires (which defaults to unlimited). Signed-off-by: Falk Loewert --- .../com/nextcloud/android/files/FileLockingHelper.kt | 4 +++- .../nextcloud/android/files/FileLockingHelperTest.kt | 11 +++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/nextcloud/android/files/FileLockingHelper.kt b/app/src/main/java/com/nextcloud/android/files/FileLockingHelper.kt index 5e3c7a66a964..c864147c8129 100644 --- a/app/src/main/java/com/nextcloud/android/files/FileLockingHelper.kt +++ b/app/src/main/java/com/nextcloud/android/files/FileLockingHelper.kt @@ -19,6 +19,8 @@ object FileLockingHelper { if (!file.isLocked || file.lockOwnerId == null || file.lockType != FileLockType.MANUAL) { return false } - return file.lockOwnerId == userId + // Lock owner can always unlock. The file owner can as well: server-side + // LockService::canUnlock allows TYPE_USER locks for the file owner. + return file.lockOwnerId == userId || file.ownerId == userId } } diff --git a/app/src/test/java/com/nextcloud/android/files/FileLockingHelperTest.kt b/app/src/test/java/com/nextcloud/android/files/FileLockingHelperTest.kt index d105e99cfc8c..ec603758d81b 100644 --- a/app/src/test/java/com/nextcloud/android/files/FileLockingHelperTest.kt +++ b/app/src/test/java/com/nextcloud/android/files/FileLockingHelperTest.kt @@ -34,6 +34,17 @@ class FileLockingHelperTest { Assert.assertFalse(FileLockingHelper.canUserUnlockFile(USER_NAME, file)) } + @Test + fun fileOwnerCanUnlockLockOfAnotherClient() { + val file = OCFile("/foo.md").apply { + isLocked = true + lockOwnerId = "text" + lockType = FileLockType.MANUAL + ownerId = USER_NAME + } + Assert.assertTrue(FileLockingHelper.canUserUnlockFile(USER_NAME, file)) + } + @Test fun typeNotManual_cannotUnlock() { val file = OCFile("/foo.md").apply { From cde78a397d86b26e35cbad202ae0f3af1e78653f Mon Sep 17 00:00:00 2001 From: Falk Loewert Date: Mon, 19 Oct 2026 20:58:59 +0200 Subject: [PATCH 2/2] Allow file owners to unlock app locks and fix hidden action list App locks created by the Text or OnlyOffice apps are of type TYPE_APP. For those the server does not expose a lock owner and reports the lock type as COLLABORATIVE, so the previous check (lock owner equals the current user, MANUAL lock type) never matched and the unlock action stayed hidden even for the owner of the file. LockService::canUnlock accepts a TYPE_USER unlock request from the file owner for TYPE_USER and TYPE_APP locks, so mirror that on the client and only keep TOKEN locks restricted. Additionally the lock info row was appended to the action list before displayActions() ran. That method returns early when the list is not empty, which left the bottom sheet without any action at all for locked files. Render the actions first and append the lock info afterwards. Signed-off-by: Falk Loewert --- .../android/files/FileLockingHelper.kt | 12 +++++++---- .../ui/fileactions/FileActionsBottomSheet.kt | 4 +++- .../android/files/FileLockingHelperTest.kt | 21 +++++++++++++++++++ 3 files changed, 32 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/nextcloud/android/files/FileLockingHelper.kt b/app/src/main/java/com/nextcloud/android/files/FileLockingHelper.kt index c864147c8129..ad685f961009 100644 --- a/app/src/main/java/com/nextcloud/android/files/FileLockingHelper.kt +++ b/app/src/main/java/com/nextcloud/android/files/FileLockingHelper.kt @@ -16,11 +16,15 @@ object FileLockingHelper { */ @JvmStatic fun canUserUnlockFile(userId: String, file: OCFile): Boolean { - if (!file.isLocked || file.lockOwnerId == null || file.lockType != FileLockType.MANUAL) { + if (!file.isLocked) { return false } - // Lock owner can always unlock. The file owner can as well: server-side - // LockService::canUnlock allows TYPE_USER locks for the file owner. - return file.lockOwnerId == userId || file.ownerId == userId + if (file.lockType == FileLockType.MANUAL && file.lockOwnerId == userId) { + return true + } + // App locks (Text, OnlyOffice, ...) carry no lock owner id and are reported as + // COLLABORATIVE. The file owner may still unlock them: LockService::canUnlock + // accepts any TYPE_USER request from the file owner. + return file.lockType != FileLockType.TOKEN && file.ownerId == userId } } diff --git a/app/src/main/java/com/nextcloud/ui/fileactions/FileActionsBottomSheet.kt b/app/src/main/java/com/nextcloud/ui/fileactions/FileActionsBottomSheet.kt index 9ea47476ce89..200a2b24cd1f 100644 --- a/app/src/main/java/com/nextcloud/ui/fileactions/FileActionsBottomSheet.kt +++ b/app/src/main/java/com/nextcloud/ui/fileactions/FileActionsBottomSheet.kt @@ -128,10 +128,12 @@ class FileActionsBottomSheet : when (state) { is FileActionsViewModel.UiState.LoadedForSingleFile -> { loadFileThumbnail(state.titleFile) + // displayActions aborts when the action list is not empty, so it has to + // run before the lock info row is appended. + displayActions(state.titleFile, state.actions) if (state.lockInfo != null) { displayLockInfo(state.lockInfo) } - displayActions(state.titleFile, state.actions) displayTitle(state.titleFile) } diff --git a/app/src/test/java/com/nextcloud/android/files/FileLockingHelperTest.kt b/app/src/test/java/com/nextcloud/android/files/FileLockingHelperTest.kt index ec603758d81b..5e77e32da19b 100644 --- a/app/src/test/java/com/nextcloud/android/files/FileLockingHelperTest.kt +++ b/app/src/test/java/com/nextcloud/android/files/FileLockingHelperTest.kt @@ -45,6 +45,27 @@ class FileLockingHelperTest { Assert.assertTrue(FileLockingHelper.canUserUnlockFile(USER_NAME, file)) } + @Test + fun fileOwnerCanUnlockAppLock() { + val file = OCFile("/foo.docx").apply { + isLocked = true + lockOwnerId = null + lockType = FileLockType.COLLABORATIVE + ownerId = USER_NAME + } + Assert.assertTrue(FileLockingHelper.canUserUnlockFile(USER_NAME, file)) + } + + @Test + fun fileOwnerCannotUnlockTokenLock() { + val file = OCFile("/foo.docx").apply { + isLocked = true + lockType = FileLockType.TOKEN + ownerId = USER_NAME + } + Assert.assertFalse(FileLockingHelper.canUserUnlockFile(USER_NAME, file)) + } + @Test fun typeNotManual_cannotUnlock() { val file = OCFile("/foo.md").apply {