mirror of
https://github.com/immich-app/immich.git
synced 2026-07-03 19:35:22 -07:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3c1e3d8abb |
@@ -4,7 +4,7 @@ import android.annotation.SuppressLint
|
|||||||
import android.content.ContentUris
|
import android.content.ContentUris
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.database.Cursor
|
import android.database.Cursor
|
||||||
import androidx.exifinterface.media.ExifInterface
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.ext.SdkExtensions
|
import android.os.ext.SdkExtensions
|
||||||
@@ -30,6 +30,7 @@ import kotlinx.coroutines.launch
|
|||||||
import kotlinx.coroutines.sync.Semaphore
|
import kotlinx.coroutines.sync.Semaphore
|
||||||
import kotlinx.coroutines.sync.withPermit
|
import kotlinx.coroutines.sync.withPermit
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.io.InputStream
|
||||||
import java.security.MessageDigest
|
import java.security.MessageDigest
|
||||||
import kotlin.coroutines.cancellation.CancellationException
|
import kotlin.coroutines.cancellation.CancellationException
|
||||||
|
|
||||||
@@ -52,6 +53,7 @@ open class NativeSyncApiImplBase(context: Context) : ImmichPlugin(), ActivityAwa
|
|||||||
private const val MAX_CONCURRENT_HASH_OPERATIONS = 16
|
private const val MAX_CONCURRENT_HASH_OPERATIONS = 16
|
||||||
private val hashSemaphore = Semaphore(MAX_CONCURRENT_HASH_OPERATIONS)
|
private val hashSemaphore = Semaphore(MAX_CONCURRENT_HASH_OPERATIONS)
|
||||||
private const val HASHING_CANCELLED_CODE = "HASH_CANCELLED"
|
private const val HASHING_CANCELLED_CODE = "HASH_CANCELLED"
|
||||||
|
private const val HASH_ORIGINAL_UNAVAILABLE_CODE = "HASH_ORIGINAL_UNAVAILABLE"
|
||||||
private const val SYNC_CANCELLED_CODE = "SYNC_CANCELLED"
|
private const val SYNC_CANCELLED_CODE = "SYNC_CANCELLED"
|
||||||
|
|
||||||
// MediaStore.Files.FileColumns.SPECIAL_FORMAT — S Extensions 21+
|
// MediaStore.Files.FileColumns.SPECIAL_FORMAT — S Extensions 21+
|
||||||
@@ -275,7 +277,7 @@ open class NativeSyncApiImplBase(context: Context) : ImmichPlugin(), ActivityAwa
|
|||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.w(TAG, "Failed to parse image header for asset $assetId", e)
|
Log.w(TAG, "Failed to parse image header for asset $assetId", e)
|
||||||
}
|
}
|
||||||
// if mimeType is webp but not animated, its just an image.
|
// if mimeType is webp but not animated, it's just an image.
|
||||||
return PlatformAssetPlaybackStyle.IMAGE
|
return PlatformAssetPlaybackStyle.IMAGE
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -377,7 +379,11 @@ open class NativeSyncApiImplBase(context: Context) : ImmichPlugin(), ActivityAwa
|
|||||||
)?.use { cursor -> cursor.count.toLong() } ?: 0L
|
)?.use { cursor -> cursor.count.toLong() } ?: 0L
|
||||||
|
|
||||||
|
|
||||||
fun getAssetsForAlbum(albumId: String, updatedTimeCond: Long?, callback: (Result<List<PlatformAsset>>) -> Unit) {
|
fun getAssetsForAlbum(
|
||||||
|
albumId: String,
|
||||||
|
updatedTimeCond: Long?,
|
||||||
|
callback: (Result<List<PlatformAsset>>) -> Unit
|
||||||
|
) {
|
||||||
runSync(callback) { getAssetsForAlbum(albumId, updatedTimeCond) }
|
runSync(callback) { getAssetsForAlbum(albumId, updatedTimeCond) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -419,7 +425,7 @@ open class NativeSyncApiImplBase(context: Context) : ImmichPlugin(), ActivityAwa
|
|||||||
}.awaitAll()
|
}.awaitAll()
|
||||||
|
|
||||||
completeWhenActive(callback, Result.success(results))
|
completeWhenActive(callback, Result.success(results))
|
||||||
} catch (e: CancellationException) {
|
} catch (_: CancellationException) {
|
||||||
completeWhenActive(
|
completeWhenActive(
|
||||||
callback, Result.failure(
|
callback, Result.failure(
|
||||||
FlutterError(
|
FlutterError(
|
||||||
@@ -436,31 +442,63 @@ open class NativeSyncApiImplBase(context: Context) : ImmichPlugin(), ActivityAwa
|
|||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun hashAsset(assetId: String): HashResult {
|
private suspend fun hashAsset(assetId: String): HashResult {
|
||||||
|
val digest = MessageDigest.getInstance("SHA-1")
|
||||||
|
openOriginalStream(assetId).use { inputStream ->
|
||||||
|
var bytesRead: Int
|
||||||
|
val buffer = ByteArray(HASH_BUFFER_SIZE)
|
||||||
|
while (inputStream.read(buffer).also { bytesRead = it } > 0) {
|
||||||
|
currentCoroutineContext().ensureActive()
|
||||||
|
digest.update(buffer, 0, bytesRead)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val hashString = Base64.encodeToString(digest.digest(), Base64.NO_WRAP)
|
||||||
|
return HashResult(assetId, null, hashString)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun openOriginalStream(assetId: String): InputStream {
|
||||||
|
val id = assetId.toLong()
|
||||||
|
val collection = when (getMediaType(id)) {
|
||||||
|
MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO -> MediaStore.Video.Media.EXTERNAL_CONTENT_URI
|
||||||
|
else -> MediaStore.Images.Media.EXTERNAL_CONTENT_URI
|
||||||
|
}
|
||||||
|
var uri: Uri = ContentUris.withAppendedId(collection, id)
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
uri = MediaStore.setRequireOriginal(uri)
|
||||||
|
}
|
||||||
|
|
||||||
return try {
|
return try {
|
||||||
val assetUri = ContentUris.withAppendedId(
|
ctx.contentResolver.openInputStream(uri)
|
||||||
MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL),
|
?: throw FlutterError(
|
||||||
assetId.toLong()
|
HASH_ORIGINAL_UNAVAILABLE_CODE,
|
||||||
)
|
"Cannot open original stream for asset $assetId",
|
||||||
|
null
|
||||||
val digest = MessageDigest.getInstance("SHA-1")
|
)
|
||||||
ctx.contentResolver.openInputStream(assetUri)?.use { inputStream ->
|
} catch (e: FlutterError) {
|
||||||
var bytesRead: Int
|
throw e
|
||||||
val buffer = ByteArray(HASH_BUFFER_SIZE)
|
|
||||||
while (inputStream.read(buffer).also { bytesRead = it } > 0) {
|
|
||||||
currentCoroutineContext().ensureActive()
|
|
||||||
digest.update(buffer, 0, bytesRead)
|
|
||||||
}
|
|
||||||
} ?: return HashResult(assetId, "Cannot open input stream for asset", null)
|
|
||||||
|
|
||||||
val hashString = Base64.encodeToString(digest.digest(), Base64.NO_WRAP)
|
|
||||||
HashResult(assetId, null, hashString)
|
|
||||||
} catch (e: SecurityException) {
|
|
||||||
HashResult(assetId, "Permission denied accessing asset: ${e.message}", null)
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
HashResult(assetId, "Failed to hash asset: ${e.message}", null)
|
throw FlutterError(
|
||||||
|
HASH_ORIGINAL_UNAVAILABLE_CODE,
|
||||||
|
"Failed to open original stream for asset $assetId: ${e.message}",
|
||||||
|
e.stackTraceToString()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun getMediaType(id: Long): Int =
|
||||||
|
getCursor(
|
||||||
|
MediaStore.VOLUME_EXTERNAL,
|
||||||
|
"${MediaStore.MediaColumns._ID} = ?",
|
||||||
|
arrayOf(id.toString()),
|
||||||
|
arrayOf(MediaStore.Files.FileColumns.MEDIA_TYPE)
|
||||||
|
)?.use { cursor ->
|
||||||
|
if (cursor.moveToFirst()) {
|
||||||
|
cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.MEDIA_TYPE))
|
||||||
|
} else {
|
||||||
|
MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE
|
||||||
|
}
|
||||||
|
} ?: MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE
|
||||||
|
|
||||||
fun cancelHashing() {
|
fun cancelHashing() {
|
||||||
hashTask?.cancel()
|
hashTask?.cancel()
|
||||||
hashTask = null
|
hashTask = null
|
||||||
@@ -476,8 +514,11 @@ open class NativeSyncApiImplBase(context: Context) : ImmichPlugin(), ActivityAwa
|
|||||||
syncJob = CoroutineScope(Dispatchers.IO).launch {
|
syncJob = CoroutineScope(Dispatchers.IO).launch {
|
||||||
try {
|
try {
|
||||||
completeWhenActive(callback, Result.success(work()))
|
completeWhenActive(callback, Result.success(work()))
|
||||||
} catch (e: CancellationException) {
|
} catch (_: CancellationException) {
|
||||||
completeWhenActive(callback, Result.failure(FlutterError(SYNC_CANCELLED_CODE, "Sync cancelled", null)))
|
completeWhenActive(
|
||||||
|
callback,
|
||||||
|
Result.failure(FlutterError(SYNC_CANCELLED_CODE, "Sync cancelled", null))
|
||||||
|
)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
completeWhenActive(callback, Result.failure(e))
|
completeWhenActive(callback, Result.failure(e))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,11 +66,12 @@ class HashService {
|
|||||||
await _hashAssets(pseudoAlbum, trashedToHash, isTrashed: true);
|
await _hashAssets(pseudoAlbum, trashedToHash, isTrashed: true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} on PlatformException catch (e) {
|
} on PlatformException catch (e, s) {
|
||||||
if (e.code == _kHashCancelledCode) {
|
if (e.code == _kHashCancelledCode) {
|
||||||
_log.warning("Hashing cancelled by platform");
|
_log.warning("Hashing cancelled by platform");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
_log.severe("Native hashing failed: ${e.code}", e, s);
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
_log.severe("Error during hashing", e, s);
|
_log.severe("Error during hashing", e, s);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user