package expo.modules.medialibrary.next.objects.asset.delegates import android.content.ContentValues import android.content.Context import androidx.exifinterface.media.ExifInterface import android.net.Uri import android.os.Build import android.os.Bundle import android.provider.MediaStore import androidx.annotation.RequiresApi import expo.modules.medialibrary.next.exceptions.AssetPropertyNotFoundException import expo.modules.medialibrary.next.exceptions.ContentResolverNotObtainedException import expo.modules.medialibrary.next.extensions.getOrThrow import expo.modules.medialibrary.next.extensions.resolver.copyUriContent import expo.modules.medialibrary.next.extensions.resolver.insertPendingAsset import expo.modules.medialibrary.next.extensions.resolver.publishPendingAsset import expo.modules.medialibrary.next.extensions.resolver.queryAssetDisplayName import expo.modules.medialibrary.next.extensions.resolver.queryAssetDuration import expo.modules.medialibrary.next.extensions.resolver.queryAssetHeight import expo.modules.medialibrary.next.extensions.resolver.queryAssetWidth import expo.modules.medialibrary.next.extensions.resolver.queryAssetData import expo.modules.medialibrary.next.extensions.resolver.queryAssetDateModified import expo.modules.medialibrary.next.extensions.resolver.queryAssetDateTaken import expo.modules.medialibrary.next.extensions.resolver.queryAssetIsFavorite import expo.modules.medialibrary.next.extensions.resolver.queryAssetMediaStoreItem import expo.modules.medialibrary.next.extensions.resolver.safeUpdate import expo.modules.medialibrary.next.extensions.resolver.updateRelativePath import expo.modules.medialibrary.next.extensions.resolver.updateRelativePathAndName import expo.modules.medialibrary.next.objects.wrappers.RelativePath import expo.modules.medialibrary.next.objects.album.Album import expo.modules.medialibrary.next.objects.asset.Asset import expo.modules.medialibrary.next.objects.asset.EXIF_TAGS import expo.modules.medialibrary.next.objects.asset.deleters.AssetDeleter import expo.modules.medialibrary.next.objects.asset.factories.AssetFactory import expo.modules.medialibrary.next.extensions.resolver.queryAlbumTitle import expo.modules.medialibrary.next.extensions.resolver.queryAssetBucketId import expo.modules.medialibrary.next.objects.asset.AssetMapper import expo.modules.medialibrary.next.objects.asset.factories.buildUniqueDisplayName import expo.modules.medialibrary.next.objects.asset.movers.AssetMover import expo.modules.medialibrary.next.objects.wrappers.MediaType import expo.modules.medialibrary.next.objects.wrappers.MimeType import expo.modules.medialibrary.next.permissions.MediaStorePermissionsDelegate import expo.modules.medialibrary.next.records.AssetInfo import expo.modules.medialibrary.next.records.Location import expo.modules.medialibrary.next.records.Shape import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ensureActive import kotlinx.coroutines.withContext import java.lang.ref.WeakReference import kotlin.collections.component1 import kotlin.collections.component2 import kotlin.let @RequiresApi(Build.VERSION_CODES.Q) class AssetModernDelegate( override val contentUri: Uri, val assetDeleter: AssetDeleter, val assetMover: AssetMover, val assetMapper: AssetMapper, val mediaStorePermissionsDelegate: MediaStorePermissionsDelegate, val assetFactory: AssetFactory, context: Context ) : AssetDelegate { private val contextRef = WeakReference(context) private val contentResolver get() = contextRef .getOrThrow() .contentResolver ?: throw ContentResolverNotObtainedException() override suspend fun getCreationTime(): Long? { val mediaStoreDateTaken = contentResolver.queryAssetDateTaken(contentUri) return assetMapper.mapCreationTime(mediaStoreDateTaken) } override suspend fun getDuration(): Long? { if (getMediaType() != MediaType.VIDEO) { return null } val mediaStoreDuration = contentResolver.queryAssetDuration(contentUri) return assetMapper.mapDuration(mediaStoreDuration) } override suspend fun getFilename(): String = contentResolver.queryAssetDisplayName(contentUri) ?: throw AssetPropertyNotFoundException("Filename") override suspend fun getHeight(): Int { val mediaStoreHeight = contentResolver.queryAssetHeight(contentUri) return assetMapper.mapHeight(mediaStoreHeight, contentUri) ?: throw AssetPropertyNotFoundException("Height") } override suspend fun getWidth(): Int { val mediaStoreWidth = contentResolver.queryAssetWidth(contentUri) return assetMapper.mapWidth(mediaStoreWidth, contentUri) ?: throw AssetPropertyNotFoundException("Width") } override suspend fun getShape(): Shape? { val width = getWidth() val height = getHeight() return Shape(width, height).takeIf { width > 0 && height > 0 } } override suspend fun getMediaType(): MediaType = MediaType.fromContentUri(contentUri) override suspend fun getModificationTime(): Long? { val mediaStoreDateModified = contentResolver.queryAssetDateModified(contentUri) return assetMapper.mapModificationTime(mediaStoreDateModified) } override suspend fun getUri(): Uri { // e.g. storage/emulated/0/Android/data/expo/files/[ROOT_ALBUM]/[ALBUM_NAME] val mediaStoreData = contentResolver.queryAssetData(contentUri) // e.g. file:///storage/emulated/0/Android/data/expo/files/[ROOT_ALBUM]/[ALBUM_NAME] return assetMapper.mapUri(mediaStoreData) ?: throw AssetPropertyNotFoundException("Uri") } override suspend fun getInfo(): AssetInfo { return contentResolver.queryAssetMediaStoreItem(contentUri)?.let { assetMapper.toDto(it) } ?: throw AssetPropertyNotFoundException("Info") } override suspend fun getFavorite(): Boolean = assetMapper.mapIsFavorite(contentResolver.queryAssetIsFavorite(contentUri)) override suspend fun setFavorite(isFavorite: Boolean): Unit = withContext(Dispatchers.IO) { mediaStorePermissionsDelegate.requestMediaLibraryWritePermission(listOf(contentUri)) val values = ContentValues().apply { put(MediaStore.MediaColumns.IS_FAVORITE, if (isFavorite) 1 else 0) } contentResolver.safeUpdate(contentUri, values) } override suspend fun getMimeType(): MimeType { return contentResolver.getType(contentUri)?.let { MimeType(it) } ?: MimeType.from(getUri()) } override suspend fun getAlbums(): List { val albumId = contentResolver.queryAssetBucketId(contentUri)?.toString() ?: return emptyList() if (contentResolver.queryAlbumTitle(albumId) == null) { return emptyList() } return listOf(Album(albumId, assetDeleter, assetFactory, assetMover, contextRef.getOrThrow())) } override suspend fun getLocation(): Location? = contentResolver.openInputStream(contentUri)?.use { stream -> ExifInterface(stream) .latLong ?.let { (lat, long) -> Location(lat, long) } } override suspend fun getExif(): Bundle = withContext(Dispatchers.IO) { if (getMediaType() != MediaType.IMAGE) { return@withContext Bundle() } val exifBundle = Bundle() contentResolver.openInputStream(contentUri)?.use { stream -> ensureActive() val exifInterface = ExifInterface(stream) for ((type, name) in EXIF_TAGS) { if (exifInterface.getAttribute(name) != null) { when (type) { "string" -> exifBundle.putString(name, exifInterface.getAttribute(name)) "int" -> exifBundle.putInt(name, exifInterface.getAttributeInt(name, 0)) "double" -> exifBundle.putDouble(name, exifInterface.getAttributeDouble(name, 0.0)) } } } } return@withContext exifBundle } override suspend fun delete() = withContext(Dispatchers.IO) { assetDeleter.delete(contentUri) } override suspend fun move(relativePath: RelativePath) { mediaStorePermissionsDelegate.requestMediaLibraryWritePermission(listOf(contentUri)) try { contentResolver.updateRelativePath(contentUri, relativePath) } catch (e: IllegalStateException) { if (e.message?.contains("Failed to build unique file", ignoreCase = true) == true) { val uniqueName = buildUniqueDisplayName(getUri()) contentResolver.updateRelativePathAndName(contentUri, relativePath, uniqueName) } else { throw e } } } override suspend fun copy(relativePath: RelativePath): Asset = copyInternal(relativePath, forceUniqueName = false) private suspend fun copyInternal(relativePath: RelativePath, forceUniqueName: Boolean): Asset = withContext(Dispatchers.IO) { val displayName = if (forceUniqueName) { buildUniqueDisplayName(getUri()) } else { getUri().toString() } val newAssetUri = contentResolver.insertPendingAsset( displayName, getMimeType(), relativePath ) return@withContext try { ensureActive() contentResolver.copyUriContent(contentUri, newAssetUri) ensureActive() contentResolver.publishPendingAsset(newAssetUri) assetFactory.create(newAssetUri) } catch (e: IllegalStateException) { contentResolver.delete(newAssetUri, null, null) // It occurs when trying to create too many assets with the same filename in the same album. // By default, the Content Resolver can resolve this issue for up to 32 assets, but then it throws this exception. val isCollisionError = e.message?.contains("Failed to build unique file", ignoreCase = true) == true if (isCollisionError && !forceUniqueName) { copyInternal(relativePath, forceUniqueName = true) } else { throw e } } } }