diff --git a/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt b/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt
index 382590f6a945..864df474b964 100644
--- a/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt
+++ b/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt
@@ -1575,7 +1575,13 @@ class FileDisplayActivity :
private fun handleRemovedFolder(syncFolderRemotePath: String?) {
DisplayUtils.showSnackMessage(this, R.string.sync_current_folder_was_removed, syncFolderRemotePath)
- browseToRoot()
+ fileListFragment?.let {
+ it.parentFolderFinder.getParentOnFirstParentRemoved(syncFolderRemotePath, storageManager)?.let { target ->
+ it.listDirectory(target, MainApp.isOnlyOnDevice())
+ updateActionBarTitleAndHomeButton(target)
+ file = target
+ }
+ }
}
private fun updateFileList(
@@ -1899,13 +1905,13 @@ class FileDisplayActivity :
// endregion
fun browseToRoot() {
- val listOfFiles = this.listOfFilesFragment
- if (listOfFiles != null) { // should never be null, indeed
+ listOfFilesFragment?.let {
val root = storageManager.getFileByPath(OCFile.ROOT_PATH)
- listOfFiles.resetSearchAttributes()
- file = listOfFiles.currentFile
+ it.resetSearchAttributes()
+ file = it.currentFile
startSyncFolderOperation(root, false)
}
+
binding.fabMain.setImageResource(R.drawable.ic_plus)
resetScrollingAndUpdateActionBar()
}
diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java b/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java
index ae434dbebba3..9a1d35e2dfb7 100644
--- a/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java
+++ b/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java
@@ -80,7 +80,6 @@
import com.owncloud.android.lib.resources.e2ee.ToggleEncryptionRemoteOperation;
import com.owncloud.android.lib.resources.files.SearchRemoteOperation;
import com.owncloud.android.lib.resources.files.ToggleFavoriteRemoteOperation;
-import com.owncloud.android.lib.resources.files.model.RemoteFile;
import com.owncloud.android.lib.resources.status.E2EVersion;
import com.owncloud.android.lib.resources.status.OCCapability;
import com.owncloud.android.lib.resources.status.Type;
@@ -106,6 +105,7 @@
import com.owncloud.android.ui.events.FavoriteEvent;
import com.owncloud.android.ui.events.FileLockEvent;
import com.owncloud.android.ui.events.SearchEvent;
+import com.owncloud.android.ui.fragment.helper.ParentFolderFinder;
import com.owncloud.android.ui.helpers.FileOperationsHelper;
import com.owncloud.android.ui.interfaces.OCFileListFragmentInterface;
import com.owncloud.android.ui.preview.PreviewImageFragment;
@@ -124,7 +124,6 @@
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
-import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
@@ -228,6 +227,7 @@ public class OCFileListFragment extends ExtendedListFragment implements
protected String mLimitToMimeType;
private FloatingActionButton mFabMain;
public static boolean isMultipleFileSelectedForCopyOrMove = false;
+ public final ParentFolderFinder parentFolderFinder = new ParentFolderFinder();
private static final Intent scanIntentExternalApp = new Intent("org.fairscan.app.action.SCAN_TO_PDF");
@@ -1019,7 +1019,6 @@ private void updateSortAndGridMenuItems() {
}
}
-
/**
* Call this, when the user presses the up button.
*
@@ -1029,62 +1028,27 @@ private void updateSortAndGridMenuItems() {
* return Count of folder levels browsed up.
*/
public int onBrowseUp() {
- if (mFile == null) {
+ if (mFile == null || mFile.isRootDirectory()) {
+ return 0;
+ }
+
+ final var result = parentFolderFinder.getParent(mFile, mContainerActivity.getStorageManager());
+ OCFile target = result.getSecond();
+
+ if (target == null) {
+ Log_OC.e(TAG, "onBrowseUp: could not resolve parent, staying put");
return 0;
}
- Pair result = getPreviousFile();
- mFile = result.second;
+ mFile = target;
setFileDepth(mFile);
- // since on browse down sets it to the false, browse up should set back to true if current search type is not NO_SEARCH
if (mFile.isRootDirectory() && currentSearchType != NO_SEARCH) {
searchFragment = true;
}
updateFileList();
- return result.first;
- }
-
- private Pair getPreviousFile() {
- if (mFile == null) {
- return new Pair<>(0, null);
- }
-
- FileDataStorageManager storageManager = mContainerActivity.getStorageManager();
- int moveCount = 0;
- String parentPath;
- OCFile parentDir;
-
- if (mFile.getParentId() != FileDataStorageManager.ROOT_PARENT_ID) {
- parentPath = new File(mFile.getRemotePath()).getParent();
- parentPath = ensureTrailingSeparator(parentPath);
- parentDir = storageManager.getFileByPath(parentPath);
- moveCount++;
- } else {
- parentDir = storageManager.getFileByPath(ROOT_PATH);
- parentPath = ROOT_PATH;
- }
-
- // Keep going up until we find a valid folder
- while (parentDir == null && !ROOT_PATH.equals(parentPath)) {
- parentPath = new File(parentPath).getParent();
- if (parentPath == null) {
- parentPath = ROOT_PATH; // fallback to root
- }
- parentPath = ensureTrailingSeparator(parentPath);
- parentDir = storageManager.getFileByPath(parentPath);
- moveCount++;
- }
-
- return new Pair<>(moveCount, parentDir);
- }
-
- private String ensureTrailingSeparator(String path) {
- if (path == null) {
- return ROOT_PATH;
- }
- return path.endsWith(OCFile.PATH_SEPARATOR) ? path : path + OCFile.PATH_SEPARATOR;
+ return result.getFirst();
}
private void updateFileList() {
diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/helper/ParentFolderFinder.kt b/app/src/main/java/com/owncloud/android/ui/fragment/helper/ParentFolderFinder.kt
new file mode 100644
index 000000000000..30bd4c4c49e3
--- /dev/null
+++ b/app/src/main/java/com/owncloud/android/ui/fragment/helper/ParentFolderFinder.kt
@@ -0,0 +1,132 @@
+/*
+ * Nextcloud - Android Client
+ *
+ * SPDX-FileCopyrightText: 2026 Alper Ozturk
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+package com.owncloud.android.ui.fragment.helper
+
+import com.owncloud.android.datamodel.FileDataStorageManager
+import com.owncloud.android.datamodel.OCFile
+import com.owncloud.android.lib.common.utils.Log_OC
+
+@Suppress("ReturnCount")
+class ParentFolderFinder {
+ companion object {
+ private const val TAG = "ParentFolderFinder"
+ }
+
+ /**
+ * User tries to move up but parent folder was deleted thus parent of parent
+ * will be used as destination else ROOT directory
+ */
+ fun getParentOnFirstParentRemoved(path: String?, storageManager: FileDataStorageManager?): OCFile? {
+ if (storageManager == null) {
+ Log_OC.e(TAG, "StorageManager is null")
+ return null
+ }
+
+ if (path.isNullOrEmpty() || path == OCFile.ROOT_PATH) {
+ Log_OC.w(TAG, "Path is null, empty, or already at root. Falling back to ROOT.")
+ return storageManager.getFileByEncryptedRemotePath(OCFile.ROOT_PATH)
+ }
+
+ return walkUpByPath(path, storageManager).second
+ }
+
+ fun getParent(file: OCFile?, storageManager: FileDataStorageManager?): Pair {
+ if (file == null || file.isRootDirectory) {
+ Log_OC.e(TAG, "File is null or already at root")
+ return 0 to file
+ }
+
+ if (storageManager == null) {
+ Log_OC.e(TAG, "StorageManager is null")
+ return 0 to file
+ }
+
+ // id based lookup
+ val parentId = file.parentId
+ if (parentId > FileDataStorageManager.ROOT_PARENT_ID && parentId != file.fileId) {
+ storageManager.getFileById(parentId)?.let { parentById ->
+ if (parentById.isFolder) {
+ return 1 to parentById
+ }
+ }
+
+ Log_OC.w(TAG, "ID lookup missed for parentId=$parentId, falling back to path walk")
+ }
+
+ // path-based walk up
+ return walkUpByPath(file.remotePath, storageManager)
+ }
+
+ /**
+ * Walks the remote path upward one segment at a time until a valid folder
+ * is found in the DB, or we reach root.
+ */
+ private fun walkUpByPath(initialPath: String?, storageManager: FileDataStorageManager): Pair {
+ var path = initialPath
+ var moveCount = 0
+
+ while (!path.isNullOrEmpty() && path != OCFile.ROOT_PATH) {
+ val parentPath = resolveParentPath(path)
+ moveCount++
+
+ if (parentPath == null) {
+ Log_OC.w(TAG, "Failed to resolve parent path for $path, fallback to root")
+ break
+ }
+
+ // Check if this resolved parent exists in DB
+ storageManager.getFileByEncryptedRemotePath(parentPath)?.let { candidate ->
+ if (candidate.isFolder) {
+ return moveCount to candidate
+ }
+ }
+
+ if (parentPath == OCFile.ROOT_PATH) {
+ Log_OC.w(TAG, "Root path reached but not found in DB loop, forcing fallback")
+ break
+ }
+
+ // Move up one more level for the next iteration
+ path = parentPath
+ }
+
+ // fallback to root
+ val root = storageManager.getFileByEncryptedRemotePath(OCFile.ROOT_PATH)
+ return moveCount to root
+ }
+
+ /**
+ * Returns the parent path of `path` with a guaranteed trailing "/",
+ * or `null` if `path` is already root or invalid.
+ *
+ *
+ * Correctly handles paths with or without a trailing separator, so both
+ * "/Foo/Bar" and "/Foo/Bar/" return "/Foo/".
+ */
+ private fun resolveParentPath(path: String?): String? {
+ if (path.isNullOrEmpty() || path == OCFile.ROOT_PATH) {
+ return null
+ }
+
+ // Strip trailing "/" before computing parent
+ val normalized = if (path.endsWith(OCFile.PATH_SEPARATOR)) {
+ path.substring(0, path.length - 1)
+ } else {
+ path
+ }
+
+ val lastSep = normalized.lastIndexOf(OCFile.PATH_SEPARATOR)
+ if (lastSep <= 0) {
+ Log_OC.w(TAG, "No separator found, or only one at the start")
+ return OCFile.ROOT_PATH
+ }
+
+ // e.g. "/Foo/Bar" → "/Foo/"
+ return normalized.substring(0, lastSep + 1)
+ }
+}
diff --git a/app/src/test/java/com/owncloud/android/ui/db/ParentFolderFinderTest.kt b/app/src/test/java/com/owncloud/android/ui/db/ParentFolderFinderTest.kt
new file mode 100644
index 000000000000..1128c7a6b751
--- /dev/null
+++ b/app/src/test/java/com/owncloud/android/ui/db/ParentFolderFinderTest.kt
@@ -0,0 +1,205 @@
+/*
+ * Nextcloud - Android Client
+ *
+ * SPDX-FileCopyrightText: 2026 Alper Ozturk
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+package com.owncloud.android.ui.db
+
+import com.owncloud.android.datamodel.FileDataStorageManager
+import com.owncloud.android.datamodel.OCFile
+import com.owncloud.android.ui.fragment.helper.ParentFolderFinder
+import com.owncloud.android.utils.MimeType
+import io.mockk.every
+import io.mockk.mockk
+import junit.framework.TestCase.assertEquals
+import junit.framework.TestCase.assertNull
+import org.junit.Before
+import org.junit.Test
+
+@Suppress("TooManyFunctions", "MagicNumber")
+class ParentFolderFinderTest {
+
+ private lateinit var storageManager: FileDataStorageManager
+ private lateinit var finder: ParentFolderFinder
+
+ @Before
+ fun setup() {
+ storageManager = mockk(relaxed = true)
+ finder = ParentFolderFinder()
+ }
+
+ @Test
+ fun getParentWithNullFileReturnsZeroAndNull() {
+ val result = finder.getParent(null, storageManager)
+
+ assertEquals(0, result.first)
+ assertNull(result.second)
+ }
+
+ @Test
+ fun getParentWithNullStorageManagerReturnsZeroAndFile() {
+ val file = OCFile("/test.txt")
+
+ val result = finder.getParent(file, null)
+
+ assertEquals(0, result.first)
+ assertEquals(file, result.second)
+ }
+
+ @Test
+ fun getParentWithValidParentIdReturnsOneAndParentFile() {
+ val parentFolder = OCFile("/parent/").apply {
+ fileId = 10
+ mimeType = MimeType.DIRECTORY
+ }
+ val childFile = OCFile("/parent/child.txt").apply {
+ fileId = 11
+ parentId = 10
+ }
+
+ every { storageManager.getFileById(10L) } returns parentFolder
+
+ val result = finder.getParent(childFile, storageManager)
+
+ assertEquals(1, result.first)
+ assertEquals(parentFolder, result.second)
+ }
+
+ @Test
+ fun getParentWithInvalidParentIdWalksUpPath() {
+ val parentFolder = OCFile("/parent/").apply {
+ fileId = 10
+ mimeType = MimeType.DIRECTORY
+ }
+ val childFile = OCFile("/parent/child.txt").apply {
+ fileId = 11
+ parentId = 99
+ remotePath = "/parent/child.txt"
+ }
+
+ every { storageManager.getFileById(99L) } returns null
+ every { storageManager.getFileByEncryptedRemotePath("/parent/") } returns parentFolder
+
+ val result = finder.getParent(childFile, storageManager)
+
+ assertEquals(1, result.first)
+ assertEquals(parentFolder, result.second)
+ }
+
+ @Test
+ fun getParentWalksMultipleLevelsUp() {
+ val grandParentFolder = OCFile("/parent/").apply {
+ fileId = 10
+ mimeType = MimeType.DIRECTORY
+ }
+ val childFile = OCFile("/parent/sub/child.txt").apply {
+ fileId = 12
+ parentId = 11
+ remotePath = "/parent/sub/child.txt"
+ }
+
+ every { storageManager.getFileById(11L) } returns null
+ every { storageManager.getFileByEncryptedRemotePath("/parent/sub/") } returns null
+ every { storageManager.getFileByEncryptedRemotePath("/parent/") } returns grandParentFolder
+
+ val result = finder.getParent(childFile, storageManager)
+
+ assertEquals(2, result.first)
+ assertEquals(grandParentFolder, result.second)
+ }
+
+ @Test
+ fun getParentWalksUpToRootAndReturnsRootFallback() {
+ val rootFolder = OCFile(OCFile.ROOT_PATH).apply {
+ fileId = 1
+ mimeType = MimeType.DIRECTORY
+ }
+ val childFile = OCFile("/a/b/c.txt").apply {
+ fileId = 12
+ parentId = 11
+ remotePath = "/a/b/c.txt"
+ }
+
+ every { storageManager.getFileById(11L) } returns null
+ every { storageManager.getFileByEncryptedRemotePath("/a/b/") } returns null
+ every { storageManager.getFileByEncryptedRemotePath("/a/") } returns null
+ every { storageManager.getFileByEncryptedRemotePath(OCFile.ROOT_PATH) } returns rootFolder
+
+ val result = finder.getParent(childFile, storageManager)
+
+ assertEquals(3, result.first)
+ assertEquals(rootFolder, result.second)
+ }
+
+ @Test
+ fun getParentWalksUpToRootWithTrailingSlash() {
+ val rootFolder = OCFile(OCFile.ROOT_PATH).apply {
+ fileId = 1
+ mimeType = MimeType.DIRECTORY
+ }
+ val childFolder = OCFile("/folder/").apply {
+ fileId = 2
+ parentId = 99
+ remotePath = "/folder/"
+ }
+
+ every { storageManager.getFileById(99L) } returns null
+ every { storageManager.getFileByEncryptedRemotePath(OCFile.ROOT_PATH) } returns rootFolder
+
+ val result = finder.getParent(childFolder, storageManager)
+
+ assertEquals(1, result.first)
+ assertEquals(rootFolder, result.second)
+ }
+
+ @Test
+ fun getParentWhenInFolderCAndCIsRemovedRemotelyShouldReturnFolderB() {
+ val folderB = OCFile("/A/B/").apply {
+ fileId = 20
+ mimeType = MimeType.DIRECTORY
+ }
+ val folderC = OCFile("/A/B/C/").apply {
+ fileId = 30
+ parentId = 20
+ remotePath = "/A/B/C/"
+ }
+
+ // Even if C is deleted remotely, we still have its object in memory,
+ // and its parentId still correctly maps to B in the local database.
+ every { storageManager.getFileById(20L) } returns folderB
+
+ val result = finder.getParent(folderC, storageManager)
+
+ assertEquals(1, result.first)
+ assertEquals(folderB, result.second)
+ }
+
+ @Test
+ fun getParentWhenInFolderCAndBIsRemovedRemotelyShouldReturnFolderA() {
+ val folderA = OCFile("/A/").apply {
+ fileId = 10
+ mimeType = MimeType.DIRECTORY
+ }
+ val folderC = OCFile("/A/B/C/").apply {
+ fileId = 30
+ parentId = 20 // Points to B
+ remotePath = "/A/B/C/"
+ }
+
+ // ID lookup for B fails because it was removed
+ every { storageManager.getFileById(20L) } returns null
+
+ // Iteration 1: Tries to find /A/B/ -> also fails
+ every { storageManager.getFileByEncryptedRemotePath("/A/B/") } returns null
+
+ // Iteration 2: Tries to find /A/ -> Succeeds!
+ every { storageManager.getFileByEncryptedRemotePath("/A/") } returns folderA
+
+ val result = finder.getParent(folderC, storageManager)
+
+ assertEquals(2, result.first)
+ assertEquals(folderA, result.second)
+ }
+}