From 54556b76e9f417098ddfd7de948bb9fea4de3d1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9A=D1=83=D1=80=D0=BD=D0=B0=D1=82=20=D0=90=D0=BD=D0=B4?= =?UTF-8?q?=D1=80=D0=B5=D0=B9?= Date: Sat, 18 Jul 2026 21:07:40 +0300 Subject: [PATCH] Remove stale photo search entries --- Android/qsferaApp/build.gradle | 4 +- .../presentation/cloud/CloudHubFragment.kt | 53 ++++++++++++++----- .../android/presentation/cloud/CloudModels.kt | 1 + .../cloud/CloudMediaSelectionTest.kt | 11 +++- 4 files changed, 54 insertions(+), 15 deletions(-) diff --git a/Android/qsferaApp/build.gradle b/Android/qsferaApp/build.gradle index 348f092c5..274aafee3 100644 --- a/Android/qsferaApp/build.gradle +++ b/Android/qsferaApp/build.gradle @@ -137,8 +137,8 @@ android { testInstrumentationRunner "eu.qsfera.android.utils.OCTestAndroidJUnitRunner" - versionCode = 37 - versionName = "1.3.9" + versionCode = 38 + versionName = "1.3.10" buildConfigField "String", gitRemote, "\"" + getGitOriginRemote() + "\"" buildConfigField "String", commitSHA1, "\"" + getLatestGitHash() + "\"" diff --git a/Android/qsferaApp/src/main/java/eu/qsfera/android/presentation/cloud/CloudHubFragment.kt b/Android/qsferaApp/src/main/java/eu/qsfera/android/presentation/cloud/CloudHubFragment.kt index 00d7a63a1..ca0ed4280 100644 --- a/Android/qsferaApp/src/main/java/eu/qsfera/android/presentation/cloud/CloudHubFragment.kt +++ b/Android/qsferaApp/src/main/java/eu/qsfera/android/presentation/cloud/CloudHubFragment.kt @@ -24,6 +24,7 @@ import eu.qsfera.android.R import eu.qsfera.android.data.ClientManager import eu.qsfera.android.data.executeRemoteOperation import eu.qsfera.android.data.providers.SharedPreferencesProvider +import eu.qsfera.android.domain.exceptions.FileNotFoundException import eu.qsfera.android.domain.files.FileRepository import eu.qsfera.android.domain.files.model.OCFile import eu.qsfera.android.domain.spaces.SpacesRepository @@ -544,6 +545,7 @@ class CloudHubFragment : Fragment(R.layout.fragment_cloud_hub) { ) } val davPrefix = "/remote.php/dav/files/$userId" + val hiddenMedia = hiddenMediaTombstones() val items = remoteFiles.mapNotNull { remote -> val mimeType = remote.mimeType ?: return@mapNotNull null if (!mimeType.startsWith("image/") && !mimeType.startsWith("video/")) return@mapNotNull null @@ -566,7 +568,7 @@ class CloudHubFragment : Fragment(R.layout.fragment_cloud_hub) { etag = remote.etag.orEmpty(), spaceId = spaceReference, ) - } + }.filterNot { it.tombstoneKey in hiddenMedia } return LoadedMediaPage(items = items, rawResultCount = remoteFiles.size) } @@ -822,19 +824,27 @@ class CloudHubFragment : Fragment(R.layout.fragment_cloud_hub) { viewLifecycleOwner.lifecycleScope.launch { val deletedItems = withContext(Dispatchers.IO) { items.filter { item -> - runCatching { - val storedFile = fileRepository.getFileByRemotePath(item.remotePath, accountName, item.spaceId) - if (storedFile?.id != null) { - fileRepository.deleteFiles(listOf(storedFile), removeOnlyLocalCopy = false) - } else { - executeRemoteOperation { - clientManager.getFileService(accountName).removeFile( - remotePath = item.remotePath, - spaceWebDavUrl = spacesRepository.getWebDavUrlForSpace(accountName, item.spaceId), - ) + val storedFile = fileRepository.getFileByRemotePath(item.remotePath, accountName, item.spaceId) + val remoteDeletedOrMissing = runCatching { + executeRemoteOperation { + clientManager.getFileService(accountName).removeFile( + remotePath = item.remotePath, + spaceWebDavUrl = spacesRepository.getWebDavUrlForSpace(accountName, item.spaceId), + ) + } + }.fold( + onSuccess = { true }, + onFailure = { error -> error is FileNotFoundException }, + ) + if (remoteDeletedOrMissing) { + storedFile?.takeIf { it.id != null }?.let { file -> + runCatching { + fileRepository.deleteFiles(listOf(file), removeOnlyLocalCopy = true) } } - }.isSuccess + rememberMediaTombstone(item) + } + remoteDeletedOrMissing } } if (!isAdded) return@launch @@ -854,6 +864,23 @@ class CloudHubFragment : Fragment(R.layout.fragment_cloud_hub) { } } + private fun hiddenMediaTombstones(): Set = preferencesProvider + .getString(PREFERENCE_HIDDEN_MEDIA, null) + .orEmpty() + .lineSequence() + .filter(String::isNotBlank) + .toSet() + + private fun rememberMediaTombstone(item: CloudMediaItem) { + val tombstones = LinkedHashSet(hiddenMediaTombstones()) + tombstones.remove(item.tombstoneKey) + tombstones.add(item.tombstoneKey) + while (tombstones.size > MAX_HIDDEN_MEDIA) { + tombstones.remove(tombstones.first()) + } + preferencesProvider.putString(PREFERENCE_HIDDEN_MEDIA, tombstones.joinToString("\n")) + } + private fun selectableMedia(): List { val media = filteredMedia() return when { @@ -1014,6 +1041,8 @@ class CloudHubFragment : Fragment(R.layout.fragment_cloud_hub) { private const val STATE_SELECTION_MODE = "cloud_media_selection_mode" private const val PREFERENCE_PHOTO_GRID_MODE = "cloud_photo_grid_mode" private const val PREFERENCE_SHOW_SCREENSHOTS = "cloud_photo_show_screenshots" + private const val PREFERENCE_HIDDEN_MEDIA = "cloud_hidden_media_tombstones" + private const val MAX_HIDDEN_MEDIA = 1_000 fun newInstance(section: CloudSection): CloudHubFragment = CloudHubFragment().apply { arguments = bundleOf(ARG_SECTION to section.wireValue) diff --git a/Android/qsferaApp/src/main/java/eu/qsfera/android/presentation/cloud/CloudModels.kt b/Android/qsferaApp/src/main/java/eu/qsfera/android/presentation/cloud/CloudModels.kt index 850cefe3d..c5c9274ac 100644 --- a/Android/qsferaApp/src/main/java/eu/qsfera/android/presentation/cloud/CloudModels.kt +++ b/Android/qsferaApp/src/main/java/eu/qsfera/android/presentation/cloud/CloudModels.kt @@ -25,6 +25,7 @@ data class CloudMediaItem( val isImage: Boolean get() = mimeType.startsWith("image/") val albumKey: String get() = "${spaceId.orEmpty()}::$parentPath" val selectionKey: String get() = "${spaceId.orEmpty()}::${webDavHref.ifBlank { remotePath }}" + val tombstoneKey: String get() = "$selectionKey::${etag.ifBlank { "no-etag" }}" fun toOCFile(owner: String): OCFile = OCFile( owner = owner, diff --git a/Android/qsferaApp/src/test/java/eu/qsfera/android/presentation/cloud/CloudMediaSelectionTest.kt b/Android/qsferaApp/src/test/java/eu/qsfera/android/presentation/cloud/CloudMediaSelectionTest.kt index 8c40069b5..6f2c5d444 100644 --- a/Android/qsferaApp/src/test/java/eu/qsfera/android/presentation/cloud/CloudMediaSelectionTest.kt +++ b/Android/qsferaApp/src/test/java/eu/qsfera/android/presentation/cloud/CloudMediaSelectionTest.kt @@ -44,11 +44,20 @@ class CloudMediaSelectionTest { assertEquals(listOf(first, second), selection.items) } - private fun media(path: String, spaceId: String? = null) = CloudMediaItem( + @Test + fun `tombstone changes when a file is uploaded again at the same path`() { + val deleted = media("/Camera/photo.jpg", etag = "old") + val uploadedAgain = media("/Camera/photo.jpg", etag = "new") + + assertFalse(deleted.tombstoneKey == uploadedAgain.tombstoneKey) + } + + private fun media(path: String, spaceId: String? = null, etag: String = "") = CloudMediaItem( remotePath = path, mimeType = "image/jpeg", size = 1L, modifiedAt = 1L, + etag = etag, spaceId = spaceId, ) }