Address document provider review feedback

pull/26802/head
DreamingCodes 2026-06-03 10:06:30 -07:00
parent 0ee5e73063
commit 029cc3274c
No known key found for this signature in database
GPG Key ID: 294D2672488EAE23
3 changed files with 314 additions and 139 deletions

View File

@ -6,6 +6,7 @@ import android.database.sqlite.SQLiteDatabase
import android.graphics.Point
import android.os.ParcelFileDescriptor
import android.util.Log
import android.webkit.MimeTypeMap
import app.alextran.immich.core.HttpClientManager
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.OkHttpClient
@ -21,6 +22,7 @@ private const val SYNC_PREFS_NAME = "immich.cloud_provider"
data class ImmichAsset(
val id: String,
val originalFileName: String,
val mimeType: String,
val dateTakenMillis: Long,
val width: Int,
@ -71,14 +73,19 @@ object ImmichCloudRepository {
private fun getServerUrl(): String? = HttpClientManager.getServerUrl()
private fun getServerUrls(): List<String> = HttpClientManager.getServerUrls()
private fun getClient(): OkHttpClient = HttpClientManager.getClient()
private fun buildUrl(path: String): okhttp3.HttpUrl? {
val base = getServerUrl() ?: return null
private fun buildUrl(base: String, path: String): okhttp3.HttpUrl? {
val baseWithoutTrailingApi = base.removeSuffix("/api").removeSuffix("/")
return "$baseWithoutTrailingApi/api$path".toHttpUrlOrNull()
}
private fun buildUrls(path: String): List<okhttp3.HttpUrl> {
return getServerUrls().mapNotNull { buildUrl(it, path) }
}
private fun getDatabaseFile(): File? {
val dbFile = File(appContext.dataDir, "app_flutter/immich.sqlite")
return if (dbFile.exists()) dbFile else null
@ -147,9 +154,8 @@ object ImmichCloudRepository {
}
return try {
val url = buildUrl("/users/me") ?: return (getServerUrl() ?: "Immich")
val request = Request.Builder().url(url).get().build()
val response = getClient().newCall(request).execute()
val response = executeFirstSuccessful("/users/me") { Request.Builder().url(it).get().build() }
?: return getServerUrl() ?: "Immich"
if (response.isSuccessful) {
val json = JSONObject(response.body?.string() ?: "")
val email = json.optString("email", "")
@ -161,8 +167,10 @@ object ImmichCloudRepository {
else -> getServerUrl() ?: "Immich"
}
syncPrefs.edit().putString("account_name", accountName).apply()
response.close()
accountName
} else {
response.close()
getServerUrl() ?: "Immich"
}
} catch (e: Exception) {
@ -217,9 +225,9 @@ object ImmichCloudRepository {
val cursor = db.rawQuery(
"""
SELECT r.id, r.type, r.created_at, r.width, r.height,
r.duration_in_seconds, r.is_favorite,
COALESCE(e.file_size, 1) AS file_size,
SELECT DISTINCT r.id, r.type, r.created_at, r.width, r.height,
r.duration_in_seconds, r.is_favorite, r.name,
e.file_size,
COALESCE(e.orientation, '0') AS orientation
FROM remote_asset_entity r
LEFT JOIN remote_exif_entity e ON e.asset_id = r.id
@ -259,29 +267,17 @@ object ImmichCloudRepository {
): QueryResult {
Log.d(TAG, "queryAlbumAssets: albumId=$albumId, pageSize=$pageSize, pageToken=$pageToken")
return try {
val url = buildUrl("/albums/$albumId") ?: return QueryResult(emptyList(), null)
val request = Request.Builder().url(url).get().build()
val response = getClient().newCall(request).execute()
if (!response.isSuccessful) {
Log.e(TAG, "queryAlbumAssets API failed: ${response.code}")
response.close()
return QueryResult(emptyList(), null)
}
val body = response.body?.string() ?: "{}"
response.close()
val obj = JSONObject(body)
val assetsArr = obj.optJSONArray("assets") ?: return QueryResult(emptyList(), null)
val offset = pageToken?.toIntOrNull() ?: 0
val end = minOf(offset + pageSize, assetsArr.length())
val assets = mutableListOf<ImmichAsset>()
for (i in offset until end) {
val a = assetsArr.getJSONObject(i)
assets.add(assetFromApiJson(a))
}
val nextToken = if (end < assetsArr.length()) end.toString() else null
val offset = pageToken?.toLongOrNull() ?: 0L
val assets = queryAssetsFromDb(
"""
JOIN remote_album_asset_entity aa ON aa.asset_id = r.id
WHERE aa.album_id = ? AND r.visibility = 0 AND r.deleted_at IS NULL
""",
arrayOf(albumId),
pageSize,
offset
)
val nextToken = if (assets.size == pageSize) (offset + pageSize).toString() else null
Log.d(TAG, "queryAlbumAssets: returning ${assets.size} assets, nextToken=$nextToken")
QueryResult(assets, nextToken)
} catch (e: Exception) {
@ -311,12 +307,13 @@ object ImmichCloudRepository {
val height = if (c.isNull(4)) 0 else c.getInt(4)
val durationSec = if (c.isNull(5)) 0 else c.getInt(5)
val isFavorite = c.getInt(6) != 0
val fileSize = c.getLong(7)
val orientationStr = c.getString(8)
val originalFileName = c.getString(7)
val fileSize = if (c.isNull(8)) 0L else c.getLong(8)
val orientationStr = c.getString(9)
val isImage = typeInt == 1
val orientation = orientationStr.toIntOrNull() ?: 0
val sizeBytes = if (fileSize > 0) fileSize else 1L
val sizeBytes = fileSize.coerceAtLeast(0L)
val dateTakenMillis = try {
java.time.Instant.parse(createdAtStr).toEpochMilli()
@ -333,7 +330,8 @@ object ImmichCloudRepository {
return ImmichAsset(
id = id,
mimeType = if (isImage) "image/jpeg" else "video/mp4",
originalFileName = originalFileName,
mimeType = inferMimeType(originalFileName, isImage),
dateTakenMillis = dateTakenMillis,
width = width,
height = height,
@ -351,8 +349,8 @@ object ImmichCloudRepository {
val cursor = db.rawQuery(
"""
SELECT r.id, r.type, r.created_at, r.width, r.height,
r.duration_in_seconds, r.is_favorite,
COALESCE(e.file_size, 1) AS file_size,
r.duration_in_seconds, r.is_favorite, r.name,
e.file_size,
COALESCE(e.orientation, '0') AS orientation
FROM remote_asset_entity r
LEFT JOIN remote_exif_entity e ON e.asset_id = r.id
@ -372,15 +370,38 @@ object ImmichCloudRepository {
}
fun queryAlbums(): List<ImmichAlbum> {
return try {
val url = buildUrl("/albums") ?: return emptyList()
val request = Request.Builder().url(url).get().build()
val response = getClient().newCall(request).execute()
if (!response.isSuccessful) {
Log.e(TAG, "queryAlbums API failed: ${response.code}")
response.close()
return emptyList()
val db = openDatabase()
if (db != null) {
try {
val albums = mutableListOf<ImmichAlbum>()
db.rawQuery(
"""
SELECT a.id, a.name, COUNT(aa.asset_id) AS media_count,
a.thumbnail_asset_id, a.updated_at
FROM remote_album_entity a
JOIN remote_album_asset_entity aa ON aa.album_id = a.id
GROUP BY a.id, a.name, a.thumbnail_asset_id, a.updated_at
HAVING media_count > 0
ORDER BY a.name COLLATE NOCASE
""".trimIndent(),
null
).use { cursor ->
while (cursor.moveToNext()) {
albums.add(albumFromCursor(cursor))
}
}
Log.d(TAG, "queryAlbums: returning ${albums.size} albums from local DB")
return albums
} catch (e: Exception) {
Log.e(TAG, "queryAlbums DB error", e)
} finally {
db.close()
}
}
return try {
val response = executeFirstSuccessful("/albums") { Request.Builder().url(it).get().build() }
?: return emptyList()
val body = response.body?.string() ?: "[]"
response.close()
val arr = JSONArray(body)
@ -411,6 +432,31 @@ object ImmichCloudRepository {
}
}
fun getAlbumById(albumId: String): ImmichAlbum? {
val db = openDatabase() ?: return null
return try {
db.rawQuery(
"""
SELECT a.id, a.name, COUNT(aa.asset_id) AS media_count,
a.thumbnail_asset_id, a.updated_at
FROM remote_album_entity a
LEFT JOIN remote_album_asset_entity aa ON aa.album_id = a.id
WHERE a.id = ?
GROUP BY a.id, a.name, a.thumbnail_asset_id, a.updated_at
LIMIT 1
""".trimIndent(),
arrayOf(albumId)
).use { cursor ->
if (cursor.moveToFirst()) albumFromCursor(cursor) else null
}
} catch (e: Exception) {
Log.e(TAG, "getAlbumById error", e)
null
} finally {
db.close()
}
}
fun queryPeople(): List<ImmichPerson> {
val now = System.currentTimeMillis()
cachedPeople?.let { cached ->
@ -456,6 +502,34 @@ object ImmichCloudRepository {
}
}
fun getPersonById(personId: String): ImmichPerson? {
val db = openDatabase() ?: return null
return try {
db.rawQuery(
"""
SELECT id, name
FROM person_entity
WHERE id = ? AND is_hidden = 0
LIMIT 1
""".trimIndent(),
arrayOf(personId)
).use { cursor ->
if (cursor.moveToFirst()) {
ImmichPerson(
id = cursor.getString(0),
name = cursor.getString(1),
coverAssetId = "person:$personId"
)
} else null
}
} catch (e: Exception) {
Log.e(TAG, "getPersonById error", e)
null
} finally {
db.close()
}
}
fun queryPersonAssets(
personId: String,
pageSize: Int = 1000,
@ -463,37 +537,17 @@ object ImmichCloudRepository {
): QueryResult {
Log.d(TAG, "queryPersonAssets: personId=$personId, pageSize=$pageSize, pageToken=$pageToken")
return try {
val page = pageToken?.toIntOrNull() ?: 1
val url = buildUrl("/search/metadata") ?: return QueryResult(emptyList(), null)
val jsonBody = JSONObject().apply {
put("personIds", org.json.JSONArray().put(personId))
put("page", page)
put("size", pageSize)
}
val mediaType = "application/json".toMediaType()
val requestBody = jsonBody.toString().toRequestBody(mediaType)
val request = Request.Builder().url(url).post(requestBody).build()
val response = getClient().newCall(request).execute()
if (!response.isSuccessful) {
Log.e(TAG, "queryPersonAssets API failed: ${response.code}")
response.close()
return QueryResult(emptyList(), null)
}
val body = response.body?.string() ?: "{}"
response.close()
val result = JSONObject(body)
val assetsObj = result.optJSONObject("assets") ?: return QueryResult(emptyList(), null)
val items = assetsObj.optJSONArray("items") ?: return QueryResult(emptyList(), null)
val total = assetsObj.optInt("total", 0)
val assets = mutableListOf<ImmichAsset>()
for (i in 0 until items.length()) {
val a = items.getJSONObject(i)
assets.add(assetFromApiJson(a))
}
val fetched = (page - 1) * pageSize + assets.size
val nextToken = if (fetched < total) (page + 1).toString() else null
val offset = pageToken?.toLongOrNull() ?: 0L
val assets = queryAssetsFromDb(
"""
JOIN asset_face_entity af ON af.asset_id = r.id
WHERE af.person_id = ? AND r.visibility = 0 AND r.deleted_at IS NULL
""",
arrayOf(personId),
pageSize,
offset
)
val nextToken = if (assets.size == pageSize) (offset + pageSize).toString() else null
Log.d(TAG, "queryPersonAssets: returning ${assets.size} assets, nextToken=$nextToken")
QueryResult(assets, nextToken)
} catch (e: Exception) {
@ -508,7 +562,7 @@ object ImmichCloudRepository {
val isImage = type == "IMAGE"
val createdAt = a.optString("fileCreatedAt", a.optString("createdAt", ""))
val exifInfo = a.optJSONObject("exifInfo")
val fileSize = exifInfo?.optLong("fileSizeInByte", 1) ?: 1L
val fileSize = exifInfo?.optLong("fileSizeInByte", 0) ?: 0L
val orientation = exifInfo?.optString("orientation", "0")?.toIntOrNull() ?: 0
val width = exifInfo?.optInt("exifImageWidth", 0) ?: 0
val height = exifInfo?.optInt("exifImageHeight", 0) ?: 0
@ -517,11 +571,12 @@ object ImmichCloudRepository {
return ImmichAsset(
id = id,
mimeType = if (isImage) "image/jpeg" else "video/mp4",
originalFileName = a.optString("originalFileName", id),
mimeType = inferMimeType(a.optString("originalFileName", ""), isImage),
dateTakenMillis = parseIso8601(createdAt),
width = width,
height = height,
sizeBytes = if (fileSize > 0) fileSize else 1L,
sizeBytes = fileSize.coerceAtLeast(0L),
durationMillis = durationMillis,
isFavorite = a.optBoolean("isFavorite", false),
orientation = orientation,
@ -529,6 +584,34 @@ object ImmichCloudRepository {
)
}
fun searchAssets(query: String, pageSize: Int = 100): QueryResult {
if (query.isBlank()) return QueryResult(emptyList(), null)
return try {
val response = executeFirstSuccessful("/search/smart") { url ->
val body = JSONObject().apply {
put("query", query)
put("page", 1)
put("size", pageSize)
put("visibility", "timeline")
}.toString().toRequestBody("application/json".toMediaType())
Request.Builder().url(url).post(body).build()
} ?: return QueryResult(emptyList(), null)
val result = JSONObject(response.body?.string() ?: "{}")
response.close()
val assetsObj = result.optJSONObject("assets") ?: return QueryResult(emptyList(), null)
val items = assetsObj.optJSONArray("items") ?: return QueryResult(emptyList(), null)
val assets = mutableListOf<ImmichAsset>()
for (i in 0 until items.length()) {
assets.add(assetFromApiJson(items.getJSONObject(i)))
}
QueryResult(assets, null)
} catch (e: Exception) {
Log.e(TAG, "searchAssets error", e)
QueryResult(emptyList(), null)
}
}
private fun parseIso8601(dateStr: String): Long {
return try {
java.time.Instant.parse(dateStr).toEpochMilli()
@ -547,23 +630,14 @@ object ImmichCloudRepository {
fun openMedia(assetId: String): ParcelFileDescriptor? {
if (assetId.startsWith("person:")) {
val personId = assetId.removePrefix("person:")
val url = buildUrl("/people/$personId/thumbnail") ?: return null
val request = Request.Builder().url(url).get().build()
return downloadToTempFile(request, "person_thumb_$personId")
return downloadToTempFile("/people/$personId/thumbnail", "person_thumb_$personId")
}
val url = buildUrl("/assets/$assetId/original") ?: return null
val request = Request.Builder().url(url).get().build()
return downloadToTempFile(request, "media_$assetId")
return downloadToTempFile("/assets/$assetId/original", "media_$assetId")
}
private fun downloadToTempFile(request: Request, prefix: String): ParcelFileDescriptor? {
private fun downloadToTempFile(path: String, prefix: String): ParcelFileDescriptor? {
return try {
val response = getClient().newCall(request).execute()
if (!response.isSuccessful) {
Log.e(TAG, "Download to temp failed: ${response.code}")
response.close()
return null
}
val response = executeFirstSuccessful(path) { Request.Builder().url(it).get().build() } ?: return null
val tempFile = java.io.File.createTempFile(prefix, null, appContext.cacheDir)
response.body?.byteStream()?.use { input ->
tempFile.outputStream().use { output ->
@ -571,7 +645,11 @@ object ImmichCloudRepository {
}
}
response.close()
ParcelFileDescriptor.open(tempFile, ParcelFileDescriptor.MODE_READ_ONLY)
val fd = ParcelFileDescriptor.open(tempFile, ParcelFileDescriptor.MODE_READ_ONLY)
if (!tempFile.delete()) {
Log.w(TAG, "Failed to unlink temp file: ${tempFile.absolutePath}")
}
fd
} catch (e: Exception) {
Log.e(TAG, "downloadToTempFile error", e)
null
@ -581,16 +659,78 @@ object ImmichCloudRepository {
fun openPreview(assetId: String, size: Point): ParcelFileDescriptor? {
if (assetId.startsWith("person:")) {
val personId = assetId.removePrefix("person:")
val url = buildUrl("/people/$personId/thumbnail") ?: return null
val request = Request.Builder().url(url).get().build()
return downloadToTempFile(request, "person_thumb_$personId")
return downloadToTempFile("/people/$personId/thumbnail", "person_thumb_$personId")
}
val sizeParam = if (size.x <= 250 && size.y <= 250) "thumbnail" else "preview"
val url = buildUrl("/assets/$assetId/thumbnail") ?: return null
val urlWithParams = url.newBuilder()
.addQueryParameter("size", sizeParam)
.build()
val request = Request.Builder().url(urlWithParams).get().build()
return downloadToTempFile(request, "preview_${assetId}_$sizeParam")
return downloadToTempFile("/assets/$assetId/thumbnail?size=$sizeParam", "preview_${assetId}_$sizeParam")
}
private fun queryAssetsFromDb(
joinAndWhere: String,
whereArgs: Array<String>,
pageSize: Int,
offset: Long
): List<ImmichAsset> {
val db = openDatabase() ?: return emptyList()
return try {
val args = whereArgs.toMutableList()
args.add(pageSize.toString())
args.add(offset.toString())
val cursor = db.rawQuery(
"""
SELECT r.id, r.type, r.created_at, r.width, r.height,
r.duration_in_seconds, r.is_favorite, r.name,
e.file_size,
COALESCE(e.orientation, '0') AS orientation
FROM remote_asset_entity r
LEFT JOIN remote_exif_entity e ON e.asset_id = r.id
$joinAndWhere
ORDER BY COALESCE(r.local_date_time, r.created_at) DESC
LIMIT ? OFFSET ?
""".trimIndent(),
args.toTypedArray()
)
val assets = mutableListOf<ImmichAsset>()
cursor.use {
while (it.moveToNext()) assets.add(assetFromCursor(it))
}
assets
} catch (e: Exception) {
Log.e(TAG, "queryAssetsFromDb error", e)
emptyList()
} finally {
db.close()
}
}
private fun albumFromCursor(cursor: android.database.Cursor): ImmichAlbum {
return ImmichAlbum(
id = cursor.getString(0),
displayName = cursor.getString(1),
mediaCount = cursor.getInt(2),
coverAssetId = if (cursor.isNull(3)) null else cursor.getString(3),
dateTakenMillis = parseIso8601(cursor.getString(4))
)
}
private fun inferMimeType(fileName: String, isImage: Boolean): String {
val extension = fileName.substringAfterLast('.', "").lowercase()
val mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension)
if (mimeType != null) return mimeType
return if (isImage) "image/jpeg" else "video/mp4"
}
private fun executeFirstSuccessful(path: String, requestBuilder: (okhttp3.HttpUrl) -> Request): okhttp3.Response? {
for (url in buildUrls(path)) {
try {
val response = getClient().newCall(requestBuilder(url)).execute()
if (response.isSuccessful) return response
Log.e(TAG, "Request failed for ${url.host}: ${response.code}")
response.close()
} catch (e: Exception) {
Log.e(TAG, "Request failed for ${url.host}", e)
}
}
return null
}
}

View File

@ -1,14 +1,17 @@
package app.alextran.immich.cloudprovider
import android.content.res.AssetFileDescriptor
import android.content.ContentResolver
import android.database.Cursor
import android.database.MatrixCursor
import android.graphics.Point
import android.os.Bundle
import android.os.CancellationSignal
import android.os.ParcelFileDescriptor
import android.provider.DocumentsContract
import android.provider.DocumentsProvider
import android.util.Log
import android.webkit.MimeTypeMap
import app.alextran.immich.R
private const val TAG = "ImmichDocProvider"
@ -45,7 +48,8 @@ class ImmichDocumentProvider : DocumentsProvider() {
add(
DocumentsContract.Root.COLUMN_FLAGS,
DocumentsContract.Root.FLAG_SUPPORTS_SEARCH or
DocumentsContract.Root.FLAG_SUPPORTS_IS_CHILD
DocumentsContract.Root.FLAG_SUPPORTS_IS_CHILD or
DocumentsContract.Root.FLAG_SUPPORTS_RECENTS
)
add(DocumentsContract.Root.COLUMN_MIME_TYPES, "image/* video/*")
}
@ -99,8 +103,7 @@ class ImmichDocumentProvider : DocumentsProvider() {
}
documentId.startsWith(ALBUM_PREFIX) -> {
val albumId = documentId.removePrefix(ALBUM_PREFIX)
val albums = ImmichCloudRepository.queryAlbums()
val album = albums.find { it.id == albumId }
val album = ImmichCloudRepository.getAlbumById(albumId)
if (album != null) {
result.newRow().apply {
add(DocumentsContract.Document.COLUMN_DOCUMENT_ID, documentId)
@ -114,8 +117,7 @@ class ImmichDocumentProvider : DocumentsProvider() {
}
documentId.startsWith(PERSON_PREFIX) -> {
val personId = documentId.removePrefix(PERSON_PREFIX)
val people = ImmichCloudRepository.queryPeople()
val person = people.find { it.id == personId }
val person = ImmichCloudRepository.getPersonById(personId)
if (person != null) {
result.newRow().apply {
add(DocumentsContract.Document.COLUMN_DOCUMENT_ID, documentId)
@ -143,8 +145,30 @@ class ImmichDocumentProvider : DocumentsProvider() {
parentDocumentId: String,
projection: Array<out String>?,
sortOrder: String?
): Cursor {
return queryChildDocuments(parentDocumentId, projection, pageSize = 500, pageToken = null)
}
override fun queryChildDocuments(
parentDocumentId: String,
projection: Array<out String>?,
queryArgs: Bundle?
): Cursor {
val pageSize = queryArgs?.getInt(ContentResolver.QUERY_ARG_LIMIT)?.takeIf { it > 0 } ?: 500
val pageToken = queryArgs?.getInt(ContentResolver.QUERY_ARG_OFFSET)
?.takeIf { it > 0 }
?.toString()
return queryChildDocuments(parentDocumentId, projection, pageSize, pageToken)
}
private fun queryChildDocuments(
parentDocumentId: String,
projection: Array<out String>?,
pageSize: Int,
pageToken: String?
): Cursor {
val result = MatrixCursor(resolveDocumentProjection(projection))
var nextPageToken: String? = null
when (parentDocumentId) {
ROOT_DOC_ID -> {
@ -174,7 +198,8 @@ class ImmichDocumentProvider : DocumentsProvider() {
}
}
ALL_PHOTOS_DOC_ID -> {
val queryResult = ImmichCloudRepository.queryAllAssets(pageSize = 500)
val queryResult = ImmichCloudRepository.queryAllAssets(pageSize = pageSize, pageToken = pageToken)
nextPageToken = queryResult.nextPageToken
for (asset in queryResult.assets) {
addAssetRow(result, asset)
}
@ -208,13 +233,23 @@ class ImmichDocumentProvider : DocumentsProvider() {
else -> {
if (parentDocumentId.startsWith(ALBUM_PREFIX)) {
val albumId = parentDocumentId.removePrefix(ALBUM_PREFIX)
val queryResult = ImmichCloudRepository.queryAlbumAssets(albumId = albumId, pageSize = 500)
val queryResult = ImmichCloudRepository.queryAlbumAssets(
albumId = albumId,
pageSize = pageSize,
pageToken = pageToken
)
nextPageToken = queryResult.nextPageToken
for (asset in queryResult.assets) {
addAssetRow(result, asset)
}
} else if (parentDocumentId.startsWith(PERSON_PREFIX)) {
val personId = parentDocumentId.removePrefix(PERSON_PREFIX)
val queryResult = ImmichCloudRepository.queryPersonAssets(personId = personId, pageSize = 500)
val queryResult = ImmichCloudRepository.queryPersonAssets(
personId = personId,
pageSize = pageSize,
pageToken = pageToken
)
nextPageToken = queryResult.nextPageToken
for (asset in queryResult.assets) {
addAssetRow(result, asset)
}
@ -222,6 +257,9 @@ class ImmichDocumentProvider : DocumentsProvider() {
}
}
result.extras = Bundle().apply {
putBoolean(DocumentsContract.EXTRA_LOADING, nextPageToken != null)
}
return result
}
@ -246,19 +284,27 @@ class ImmichDocumentProvider : DocumentsProvider() {
return AssetFileDescriptor(fd, 0, AssetFileDescriptor.UNKNOWN_LENGTH)
}
override fun queryRecentDocuments(
rootId: String,
projection: Array<out String>?
): Cursor {
val result = MatrixCursor(resolveDocumentProjection(projection))
val queryResult = ImmichCloudRepository.queryAllAssets(pageSize = 100)
for (asset in queryResult.assets) {
addAssetRow(result, asset)
}
return result
}
override fun querySearchDocuments(
rootId: String,
query: String,
projection: Array<out String>?
): Cursor {
val result = MatrixCursor(resolveDocumentProjection(projection))
val queryResult = ImmichCloudRepository.queryAllAssets(pageSize = 100)
val queryResult = ImmichCloudRepository.searchAssets(query, pageSize = 100)
for (asset in queryResult.assets) {
if (asset.mimeType.contains(query, ignoreCase = true) ||
asset.id.contains(query, ignoreCase = true)
) {
addAssetRow(result, asset)
}
addAssetRow(result, asset)
}
return result
}
@ -282,9 +328,10 @@ class ImmichDocumentProvider : DocumentsProvider() {
val timestamp = java.time.Instant.ofEpochMilli(asset.dateTakenMillis)
.atZone(java.time.ZoneId.systemDefault())
.toLocalDateTime()
val displayName = "${if (asset.isImage) "IMG" else "VID"}_${
val fallbackDisplayName = "${if (asset.isImage) "IMG" else "VID"}_${
timestamp.format(java.time.format.DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss"))
}.$extension"
val displayName = asset.originalFileName.ifBlank { fallbackDisplayName }
var flags = DocumentsContract.Document.FLAG_SUPPORTS_THUMBNAIL
cursor.newRow().apply {
@ -298,25 +345,8 @@ class ImmichDocumentProvider : DocumentsProvider() {
}
private fun mimeTypeToExtension(mimeType: String): String {
return when (mimeType) {
"image/jpeg" -> "jpg"
"image/png" -> "png"
"image/gif" -> "gif"
"image/webp" -> "webp"
"image/heic" -> "heic"
"image/heif" -> "heif"
"image/avif" -> "avif"
"image/tiff" -> "tiff"
"image/bmp" -> "bmp"
"image/svg+xml" -> "svg"
"video/mp4" -> "mp4"
"video/quicktime" -> "mov"
"video/x-msvideo" -> "avi"
"video/x-matroska" -> "mkv"
"video/webm" -> "webm"
"video/3gpp" -> "3gp"
else -> mimeType.substringAfterLast("/", "bin")
}
return MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType)
?: mimeType.substringAfterLast("/", "bin")
}
companion object {

View File

@ -100,9 +100,14 @@ object HttpClientManager {
fun getServerUrl(): String? {
if (!initialized) return null
val json = prefs.getString(PREFS_SERVER_URLS, null) ?: return null
val urls = Json.decodeFromString<List<String>>(json)
return urls.firstOrNull { it.toHttpUrlOrNull() != null }?.trimEnd('/')
return getServerUrls().firstOrNull()
}
fun getServerUrls(): List<String> {
if (!initialized) return emptyList()
val json = prefs.getString(PREFS_SERVER_URLS, null) ?: return emptyList()
return Json.decodeFromString<List<String>>(json)
.mapNotNull { it.toHttpUrlOrNull()?.toString()?.trimEnd('/') }
}
fun initialize(context: Context) {