Skip to content

Commit 325e996

Browse files
zerox80guruz
authored andcommitted
Fix Space thumbnail preview URLs
1 parent d274bff commit 325e996

2 files changed

Lines changed: 144 additions & 6 deletions

File tree

opencloudApp/src/main/java/eu/opencloud/android/presentation/thumbnails/ThumbnailsRequester.kt

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,14 @@ package eu.opencloud.android.presentation.thumbnails
2323
import android.accounts.Account
2424
import android.accounts.AccountManager
2525
import android.net.Uri
26+
import androidx.annotation.VisibleForTesting
2627
import coil.ImageLoader
2728
import coil.disk.DiskCache
2829
import coil.memory.MemoryCache
2930
import coil.util.DebugLogger
3031
import eu.opencloud.android.MainApp.Companion.appContext
3132
import eu.opencloud.android.data.ClientManager
3233
import eu.opencloud.android.data.providers.SharedPreferencesProvider
33-
import java.util.concurrent.ConcurrentHashMap
3434
import eu.opencloud.android.domain.files.model.OCFile
3535
import eu.opencloud.android.domain.files.model.OCFileWithSyncInfo
3636
import eu.opencloud.android.domain.spaces.model.SpaceSpecial
@@ -50,14 +50,15 @@ import org.koin.core.component.KoinComponent
5050
import org.koin.core.component.inject
5151
import timber.log.Timber
5252
import java.util.Locale
53+
import java.util.concurrent.ConcurrentHashMap
5354

5455
object ThumbnailsRequester : KoinComponent {
5556
private val clientManager: ClientManager by inject()
5657
private val preferencesProvider: SharedPreferencesProvider by inject()
5758

5859
// https://docs.opencloud.eu/docs/next/dev/server/services/thumbnails/information/#thumbnail-query-string-parameters
5960
private const val SPACE_SPECIAL_PREVIEW_URI = "%s?scalingup=0&a=1&x=%d&y=%d&c=%s&preview=1"
60-
private const val FILE_PREVIEW_URI = "%s/webdav%s?x=%d&y=%d&c=%s&preview=1"
61+
private const val FILE_PREVIEW_URI = "%s%s?x=%d&y=%d&c=%s&preview=1"
6162

6263
private const val THUMBNAIL_DISK_CACHE_SIZE: Long = 1024 * 1024 * 100 // 100MB
6364
private const val AVATAR_HTTP_CACHE_SIZE: Long = 10L * 1024 * 1024 // 10MB
@@ -99,25 +100,43 @@ object ThumbnailsRequester : KoinComponent {
99100
}
100101

101102
fun getPreviewUriForFile(file: OCFile, account: Account, etag: String? = null, width: Int = 1024, height: Int = 1024): String =
102-
getPreviewUri(file.remotePath, etag ?: file.remoteEtag, account, width, height)
103+
getPreviewUri(file, null, etag ?: file.remoteEtag, account, width, height)
103104

104105
fun getPreviewUriForFile(fileWithSyncInfo: OCFileWithSyncInfo, account: Account, width: Int = 1024, height: Int = 1024): String =
105-
getPreviewUriForFile(fileWithSyncInfo.file, account, null, width, height)
106+
getPreviewUri(fileWithSyncInfo.file, fileWithSyncInfo.space?.root?.webDavUrl, fileWithSyncInfo.file.remoteEtag, account, width, height)
106107

107108
fun getPreviewUriForSpaceSpecial(spaceSpecial: SpaceSpecial): String =
108109
String.format(Locale.US, SPACE_SPECIAL_PREVIEW_URI, spaceSpecial.webDavUrl, 1024, 1024, spaceSpecial.eTag)
109110

110-
private fun getPreviewUri(remotePath: String?, etag: String?, account: Account, width: Int, height: Int): String {
111+
private fun getPreviewUri(file: OCFile, spaceWebDavUrl: String?, etag: String?, account: Account, width: Int, height: Int): String {
111112
val baseUrl = accountBaseUrls.getOrPut(account.name) {
112113
val accountManager = AccountManager.get(appContext)
113114
accountManager.getUserData(account, eu.opencloud.android.lib.common.accounts.AccountUtils.Constants.KEY_OC_BASE_URL)
114115
?.trimEnd('/')
115116
.orEmpty()
116117
}
118+
return buildPreviewUri(baseUrl, file.remotePath, file.spaceId, spaceWebDavUrl, etag, width, height)
119+
}
120+
121+
@VisibleForTesting
122+
internal fun buildPreviewUri(
123+
accountBaseUrl: String,
124+
remotePath: String?,
125+
spaceId: String?,
126+
spaceWebDavUrl: String?,
127+
etag: String?,
128+
width: Int,
129+
height: Int,
130+
): String {
131+
val previewBaseUrl = when {
132+
!spaceWebDavUrl.isNullOrBlank() -> spaceWebDavUrl.trimEnd('/')
133+
!spaceId.isNullOrBlank() -> "${accountBaseUrl.trimEnd('/')}/dav/spaces/${Uri.encode(spaceId, "\$")}"
134+
else -> "${accountBaseUrl.trimEnd('/')}/webdav"
135+
}
117136
val path = if (remotePath?.startsWith("/") == true) remotePath else "/$remotePath"
118137
val encodedPath = Uri.encode(path, "/")
119138

120-
return String.format(Locale.US, FILE_PREVIEW_URI, baseUrl, encodedPath, width, height, etag.orEmpty())
139+
return String.format(Locale.US, FILE_PREVIEW_URI, previewBaseUrl, encodedPath, width, height, etag.orEmpty())
121140
}
122141

123142
fun getContentAddressedImageLoader(): ImageLoader {
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
/**
2+
* openCloud Android client application
3+
*
4+
* Copyright (C) 2026 opencloud.
5+
*
6+
* This program is free software: you can redistribute it and/or modify
7+
* it under the terms of the GNU General Public License version 2,
8+
* as published by the Free Software Foundation.
9+
*
10+
* This program is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU General Public License
16+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
17+
*/
18+
19+
package eu.opencloud.android.presentation.thumbnails
20+
21+
import android.net.Uri
22+
import io.mockk.every
23+
import io.mockk.mockkStatic
24+
import io.mockk.unmockkStatic
25+
import org.junit.After
26+
import org.junit.Assert.assertEquals
27+
import org.junit.Before
28+
import org.junit.Test
29+
30+
class ThumbnailsRequesterTest {
31+
32+
@Before
33+
fun setUp() {
34+
mockkStatic(Uri::class)
35+
every { Uri.encode(any<String>(), any<String>()) } answers {
36+
encodeSpaces(firstArg<String>())
37+
}
38+
}
39+
40+
@After
41+
fun tearDown() {
42+
unmockkStatic(Uri::class)
43+
}
44+
45+
@Test
46+
fun `preview uri for personal file uses legacy webdav path`() {
47+
val uri = ThumbnailsRequester.buildPreviewUri(
48+
accountBaseUrl = "https://server.url/",
49+
remotePath = "/Photos/image.jpg",
50+
spaceId = null,
51+
spaceWebDavUrl = null,
52+
etag = "etag",
53+
width = 1024,
54+
height = 768,
55+
)
56+
57+
assertEquals(
58+
"https://server.url/webdav/Photos/image.jpg?x=1024&y=768&c=etag&preview=1",
59+
uri
60+
)
61+
}
62+
63+
@Test
64+
fun `preview uri for space file uses space webdav url from sync info`() {
65+
val uri = ThumbnailsRequester.buildPreviewUri(
66+
accountBaseUrl = "https://server.url",
67+
remotePath = "/MyFolder/test.jpg",
68+
spaceId = "ignored-space-id",
69+
spaceWebDavUrl = "https://server.url/dav/spaces/space-id\$opaque/",
70+
etag = "space-etag",
71+
width = 512,
72+
height = 512,
73+
)
74+
75+
assertEquals(
76+
"https://server.url/dav/spaces/space-id\$opaque/MyFolder/test.jpg?x=512&y=512&c=space-etag&preview=1",
77+
uri
78+
)
79+
}
80+
81+
@Test
82+
fun `preview uri for space file falls back to account base url and space id`() {
83+
val uri = ThumbnailsRequester.buildPreviewUri(
84+
accountBaseUrl = "https://server.url/",
85+
remotePath = "/MyFolder/test.jpg",
86+
spaceId = "space-id\$opaque",
87+
spaceWebDavUrl = null,
88+
etag = "space-etag",
89+
width = 256,
90+
height = 256,
91+
)
92+
93+
assertEquals(
94+
"https://server.url/dav/spaces/space-id\$opaque/MyFolder/test.jpg?x=256&y=256&c=space-etag&preview=1",
95+
uri
96+
)
97+
}
98+
99+
@Test
100+
fun `preview uri preserves subfolders and encodes spaces`() {
101+
val uri = ThumbnailsRequester.buildPreviewUri(
102+
accountBaseUrl = "https://server.url",
103+
remotePath = "/My Folder/test image.jpg",
104+
spaceId = "space-id\$opaque",
105+
spaceWebDavUrl = "https://server.url/dav/spaces/space-id\$opaque",
106+
etag = null,
107+
width = 1024,
108+
height = 1024,
109+
)
110+
111+
assertEquals(
112+
"https://server.url/dav/spaces/space-id\$opaque/My%20Folder/test%20image.jpg?x=1024&y=1024&c=&preview=1",
113+
uri
114+
)
115+
}
116+
117+
private fun encodeSpaces(value: String): String =
118+
value.replace(" ", "%20")
119+
}

0 commit comments

Comments
 (0)