Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,14 @@ package eu.opencloud.android.presentation.thumbnails
import android.accounts.Account
import android.accounts.AccountManager
import android.net.Uri
import androidx.annotation.VisibleForTesting
import coil.ImageLoader
import coil.disk.DiskCache
import coil.memory.MemoryCache
import coil.util.DebugLogger
import eu.opencloud.android.MainApp.Companion.appContext
import eu.opencloud.android.data.ClientManager
import eu.opencloud.android.data.providers.SharedPreferencesProvider
import java.util.concurrent.ConcurrentHashMap
import eu.opencloud.android.domain.files.model.OCFile
import eu.opencloud.android.domain.files.model.OCFileWithSyncInfo
import eu.opencloud.android.domain.spaces.model.SpaceSpecial
Expand All @@ -50,14 +50,15 @@ import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import timber.log.Timber
import java.util.Locale
import java.util.concurrent.ConcurrentHashMap

object ThumbnailsRequester : KoinComponent {
private val clientManager: ClientManager by inject()
private val preferencesProvider: SharedPreferencesProvider by inject()

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

private const val THUMBNAIL_DISK_CACHE_SIZE: Long = 1024 * 1024 * 100 // 100MB
private const val AVATAR_HTTP_CACHE_SIZE: Long = 10L * 1024 * 1024 // 10MB
Expand Down Expand Up @@ -99,25 +100,43 @@ object ThumbnailsRequester : KoinComponent {
}

fun getPreviewUriForFile(file: OCFile, account: Account, etag: String? = null, width: Int = 1024, height: Int = 1024): String =
getPreviewUri(file.remotePath, etag ?: file.remoteEtag, account, width, height)
getPreviewUri(file, null, etag ?: file.remoteEtag, account, width, height)

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

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

private fun getPreviewUri(remotePath: String?, etag: String?, account: Account, width: Int, height: Int): String {
private fun getPreviewUri(file: OCFile, spaceWebDavUrl: String?, etag: String?, account: Account, width: Int, height: Int): String {
val baseUrl = accountBaseUrls.getOrPut(account.name) {
val accountManager = AccountManager.get(appContext)
accountManager.getUserData(account, eu.opencloud.android.lib.common.accounts.AccountUtils.Constants.KEY_OC_BASE_URL)
?.trimEnd('/')
.orEmpty()
}
return buildPreviewUri(baseUrl, file.remotePath, file.spaceId, spaceWebDavUrl, etag, width, height)
}

@VisibleForTesting
internal fun buildPreviewUri(
accountBaseUrl: String,
remotePath: String?,
spaceId: String?,
spaceWebDavUrl: String?,
etag: String?,
width: Int,
height: Int,
): String {
val previewBaseUrl = when {
!spaceWebDavUrl.isNullOrBlank() -> spaceWebDavUrl.trimEnd('/')
!spaceId.isNullOrBlank() -> "${accountBaseUrl.trimEnd('/')}/dav/spaces/${Uri.encode(spaceId, "\$")}"
else -> "${accountBaseUrl.trimEnd('/')}/webdav"
}
val path = if (remotePath?.startsWith("/") == true) remotePath else "/$remotePath"
val encodedPath = Uri.encode(path, "/")

return String.format(Locale.US, FILE_PREVIEW_URI, baseUrl, encodedPath, width, height, etag.orEmpty())
return String.format(Locale.US, FILE_PREVIEW_URI, previewBaseUrl, encodedPath, width, height, etag.orEmpty())
}

fun getContentAddressedImageLoader(): ImageLoader {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
/**
* openCloud Android client application
*
* Copyright (C) 2026 opencloud.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

package eu.opencloud.android.presentation.thumbnails

import android.net.Uri
import io.mockk.every
import io.mockk.mockkStatic
import io.mockk.unmockkStatic
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test

class ThumbnailsRequesterTest {

@Before
fun setUp() {
mockkStatic(Uri::class)
every { Uri.encode(any<String>(), any<String>()) } answers {
encodeSpaces(firstArg<String>())
}
}

@After
fun tearDown() {
unmockkStatic(Uri::class)
}

@Test
fun `preview uri for personal file uses legacy webdav path`() {
val uri = ThumbnailsRequester.buildPreviewUri(
accountBaseUrl = "https://server.url/",
remotePath = "/Photos/image.jpg",
spaceId = null,
spaceWebDavUrl = null,
etag = "etag",
width = 1024,
height = 768,
)

assertEquals(
"https://server.url/webdav/Photos/image.jpg?x=1024&y=768&c=etag&preview=1",
uri
)
}

@Test
fun `preview uri for space file uses space webdav url from sync info`() {
val uri = ThumbnailsRequester.buildPreviewUri(
accountBaseUrl = "https://server.url",
remotePath = "/MyFolder/test.jpg",
spaceId = "ignored-space-id",
spaceWebDavUrl = "https://server.url/dav/spaces/space-id\$opaque/",
etag = "space-etag",
width = 512,
height = 512,
)

assertEquals(
"https://server.url/dav/spaces/space-id\$opaque/MyFolder/test.jpg?x=512&y=512&c=space-etag&preview=1",
uri
)
}

@Test
fun `preview uri for space file falls back to account base url and space id`() {
val uri = ThumbnailsRequester.buildPreviewUri(
accountBaseUrl = "https://server.url/",
remotePath = "/MyFolder/test.jpg",
spaceId = "space-id\$opaque",
spaceWebDavUrl = null,
etag = "space-etag",
width = 256,
height = 256,
)

assertEquals(
"https://server.url/dav/spaces/space-id\$opaque/MyFolder/test.jpg?x=256&y=256&c=space-etag&preview=1",
uri
)
}

@Test
fun `preview uri preserves subfolders and encodes spaces`() {
val uri = ThumbnailsRequester.buildPreviewUri(
accountBaseUrl = "https://server.url",
remotePath = "/My Folder/test image.jpg",
spaceId = "space-id\$opaque",
spaceWebDavUrl = "https://server.url/dav/spaces/space-id\$opaque",
etag = null,
width = 1024,
height = 1024,
)

assertEquals(
"https://server.url/dav/spaces/space-id\$opaque/My%20Folder/test%20image.jpg?x=1024&y=1024&c=&preview=1",
uri
)
}

private fun encodeSpaces(value: String): String =
value.replace(" ", "%20")
}
Loading