Address document provider review feedback
parent
0ee5e73063
commit
029cc3274c
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue