From 29512081db45a49ebd595a5d4b645f41f4116347 Mon Sep 17 00:00:00 2001 From: shenlong <139912620+shenlong-tanwen@users.noreply.github.com> Date: Tue, 4 Aug 2026 20:43:51 +0530 Subject: [PATCH] fix: use setRequireOriginal on SDK 29 and above (#29072) Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> --- .../alextran/immich/sync/MessagesImplBase.kt | 68 ++++++++++++++----- mobile/lib/domain/services/hash.service.dart | 3 +- 2 files changed, 54 insertions(+), 17 deletions(-) diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/sync/MessagesImplBase.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/sync/MessagesImplBase.kt index 18b771a613..25a849ad3a 100644 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/sync/MessagesImplBase.kt +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/sync/MessagesImplBase.kt @@ -4,7 +4,6 @@ import android.annotation.SuppressLint import android.content.ContentUris import android.content.Context import android.database.Cursor -import androidx.exifinterface.media.ExifInterface import android.os.Build import android.os.Bundle import android.os.ext.SdkExtensions @@ -30,6 +29,8 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Semaphore import kotlinx.coroutines.sync.withPermit import java.io.File +import java.io.IOException +import java.io.InputStream import java.security.MessageDigest import kotlin.coroutines.cancellation.CancellationException @@ -377,7 +378,11 @@ open class NativeSyncApiImplBase(context: Context) : ImmichPlugin(), ActivityAwa )?.use { cursor -> cursor.count.toLong() } ?: 0L - fun getAssetsForAlbum(albumId: String, updatedTimeCond: Long?, callback: (Result>) -> Unit) { + fun getAssetsForAlbum( + albumId: String, + updatedTimeCond: Long?, + callback: (Result>) -> Unit + ) { runSync(callback) { getAssetsForAlbum(albumId, updatedTimeCond) } } @@ -419,7 +424,7 @@ open class NativeSyncApiImplBase(context: Context) : ImmichPlugin(), ActivityAwa }.awaitAll() completeWhenActive(callback, Result.success(results)) - } catch (e: CancellationException) { + } catch (_: CancellationException) { completeWhenActive( callback, Result.failure( FlutterError( @@ -437,23 +442,20 @@ open class NativeSyncApiImplBase(context: Context) : ImmichPlugin(), ActivityAwa private suspend fun hashAsset(assetId: String): HashResult { return try { - val assetUri = ContentUris.withAppendedId( - MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL), - assetId.toLong() - ) - val digest = MessageDigest.getInstance("SHA-1") - ctx.contentResolver.openInputStream(assetUri)?.use { inputStream -> - var bytesRead: Int + openOriginalStream(assetId).use { inputStream -> val buffer = ByteArray(HASH_BUFFER_SIZE) - while (inputStream.read(buffer).also { bytesRead = it } > 0) { + while (true) { + val bytesRead = inputStream.read(buffer) + if (bytesRead == -1) break 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) + HashResult(assetId, null, Base64.encodeToString(digest.digest(), Base64.NO_WRAP)) + } catch (e: CancellationException) { + throw e } catch (e: SecurityException) { HashResult(assetId, "Permission denied accessing asset: ${e.message}", null) } catch (e: Exception) { @@ -461,6 +463,37 @@ open class NativeSyncApiImplBase(context: Context) : ImmichPlugin(), ActivityAwa } } + 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 + } + val uri = ContentUris.withAppendedId(collection, id) + val original = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + MediaStore.setRequireOriginal(uri) + } else { + uri + } + + return ctx.contentResolver.openInputStream(original) + ?: throw IOException("Cannot open original stream for asset $assetId") + } + + 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() { hashTask?.cancel() hashTask = null @@ -476,8 +509,11 @@ open class NativeSyncApiImplBase(context: Context) : ImmichPlugin(), ActivityAwa syncJob = CoroutineScope(Dispatchers.IO).launch { try { completeWhenActive(callback, Result.success(work())) - } catch (e: CancellationException) { - completeWhenActive(callback, Result.failure(FlutterError(SYNC_CANCELLED_CODE, "Sync cancelled", null))) + } catch (_: CancellationException) { + completeWhenActive( + callback, + Result.failure(FlutterError(SYNC_CANCELLED_CODE, "Sync cancelled", null)) + ) } catch (e: Exception) { completeWhenActive(callback, Result.failure(e)) } diff --git a/mobile/lib/domain/services/hash.service.dart b/mobile/lib/domain/services/hash.service.dart index b0dbf8fbea..8a573ee209 100644 --- a/mobile/lib/domain/services/hash.service.dart +++ b/mobile/lib/domain/services/hash.service.dart @@ -66,11 +66,12 @@ class HashService { await _hashAssets(pseudoAlbum, trashedToHash, isTrashed: true); } } - } on PlatformException catch (e) { + } on PlatformException catch (e, s) { if (e.code == _kHashCancelledCode) { _log.warning("Hashing cancelled by platform"); return; } + _log.severe("Native hashing failed: ${e.code}", e, s); } catch (e, s) { _log.severe("Error during hashing", e, s); }