Skip to content

Commit

Permalink
Add ImageSource.Metadata. (#1060)
Browse files Browse the repository at this point in the history
* Add ImageSource.Metadata.

* Add annotations.

* Docs.

* Refactor.
  • Loading branch information
colinrtwhite committed Dec 22, 2021
1 parent a09c34f commit b693246
Show file tree
Hide file tree
Showing 12 changed files with 239 additions and 144 deletions.
37 changes: 28 additions & 9 deletions coil-base/api/coil-base.api
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,11 @@ public final class coil/ImageLoaders {
public abstract interface annotation class coil/annotation/ExperimentalCoilApi : java/lang/annotation/Annotation {
}

public final class coil/decode/AssetMetadata : coil/decode/ImageSource$Metadata {
public fun <init> (Ljava/lang/String;)V
public final fun getFileName ()Ljava/lang/String;
}

public final class coil/decode/BitmapFactoryDecoder : coil/decode/Decoder {
public fun <init> (Lcoil/decode/ImageSource;Lcoil/request/Options;)V
public fun <init> (Lcoil/decode/ImageSource;Lcoil/request/Options;Lkotlinx/coroutines/sync/Semaphore;)V
Expand All @@ -147,6 +152,11 @@ public final class coil/decode/BitmapFactoryDecoder$Factory : coil/decode/Decode
public fun hashCode ()I
}

public final class coil/decode/ContentMetadata : coil/decode/ImageSource$Metadata {
public fun <init> (Landroid/net/Uri;)V
public final fun getUri ()Landroid/net/Uri;
}

public final class coil/decode/DataSource : java/lang/Enum {
public static final field DISK Lcoil/decode/DataSource;
public static final field MEMORY Lcoil/decode/DataSource;
Expand Down Expand Up @@ -193,23 +203,32 @@ public abstract interface class coil/decode/Decoder$Factory {
public abstract class coil/decode/ImageSource : java/io/Closeable {
public abstract fun file ()Ljava/io/File;
public abstract fun fileOrNull ()Ljava/io/File;
public abstract fun getMetadata ()Lcoil/decode/ImageSource$Metadata;
public abstract fun source ()Lokio/BufferedSource;
public abstract fun sourceOrNull ()Lokio/BufferedSource;
public abstract fun uriOrNull ()Landroid/net/Uri;
}

public abstract class coil/decode/ImageSource$Metadata {
public fun <init> ()V
}

public final class coil/decode/ImageSources {
public static final fun create (Ljava/io/File;)Lcoil/decode/ImageSource;
public static final fun create (Ljava/io/File;Ljava/lang/String;)Lcoil/decode/ImageSource;
public static final fun create (Ljava/io/File;Ljava/lang/String;Ljava/io/Closeable;)Lcoil/decode/ImageSource;
public static final fun create (Ljava/io/File;Ljava/lang/String;Ljava/io/Closeable;Landroid/net/Uri;)Lcoil/decode/ImageSource;
public static final fun create (Ljava/io/File;Ljava/lang/String;Ljava/io/Closeable;Lcoil/decode/ImageSource$Metadata;)Lcoil/decode/ImageSource;
public static final fun create (Lokio/BufferedSource;Landroid/content/Context;)Lcoil/decode/ImageSource;
public static final fun create (Lokio/BufferedSource;Landroid/content/Context;Landroid/net/Uri;)Lcoil/decode/ImageSource;
public static final fun create (Lokio/BufferedSource;Landroid/content/Context;Lcoil/decode/ImageSource$Metadata;)Lcoil/decode/ImageSource;
public static final fun create (Lokio/BufferedSource;Ljava/io/File;)Lcoil/decode/ImageSource;
public static final fun create (Lokio/BufferedSource;Ljava/io/File;Landroid/net/Uri;)Lcoil/decode/ImageSource;
public static synthetic fun create$default (Ljava/io/File;Ljava/lang/String;Ljava/io/Closeable;Landroid/net/Uri;ILjava/lang/Object;)Lcoil/decode/ImageSource;
public static synthetic fun create$default (Lokio/BufferedSource;Landroid/content/Context;Landroid/net/Uri;ILjava/lang/Object;)Lcoil/decode/ImageSource;
public static synthetic fun create$default (Lokio/BufferedSource;Ljava/io/File;Landroid/net/Uri;ILjava/lang/Object;)Lcoil/decode/ImageSource;
public static final fun create (Lokio/BufferedSource;Ljava/io/File;Lcoil/decode/ImageSource$Metadata;)Lcoil/decode/ImageSource;
public static synthetic fun create$default (Ljava/io/File;Ljava/lang/String;Ljava/io/Closeable;ILjava/lang/Object;)Lcoil/decode/ImageSource;
public static synthetic fun create$default (Ljava/io/File;Ljava/lang/String;Ljava/io/Closeable;Lcoil/decode/ImageSource$Metadata;ILjava/lang/Object;)Lcoil/decode/ImageSource;
public static synthetic fun create$default (Lokio/BufferedSource;Landroid/content/Context;Lcoil/decode/ImageSource$Metadata;ILjava/lang/Object;)Lcoil/decode/ImageSource;
public static synthetic fun create$default (Lokio/BufferedSource;Ljava/io/File;Lcoil/decode/ImageSource$Metadata;ILjava/lang/Object;)Lcoil/decode/ImageSource;
}

public final class coil/decode/ResourceMetadata : coil/decode/ImageSource$Metadata {
public fun <init> (Ljava/lang/String;I)V
public final fun getPackageName ()Ljava/lang/String;
public final fun getResId ()I
}

public abstract interface class coil/disk/DiskCache {
Expand Down
131 changes: 96 additions & 35 deletions coil-base/src/main/java/coil/decode/ImageSource.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import android.content.Context
import android.graphics.ImageDecoder
import android.media.MediaMetadataRetriever
import android.net.Uri
import androidx.annotation.DrawableRes
import coil.annotation.ExperimentalCoilApi
import coil.fetch.Fetcher
import coil.util.closeQuietly
Expand All @@ -24,53 +25,99 @@ import java.io.File
* @param diskCacheKey An optional cache key for the [file] in the disk cache.
* @param closeable An optional closeable reference that will
* be closed when the image source is closed.
* @param uri An optional [Uri] that resolves to the image data.
*/
@JvmOverloads
@JvmName("create")
fun ImageSource(
file: File,
diskCacheKey: String? = null,
closeable: Closeable? = null,
uri: Uri? = null
): ImageSource = FileImageSource(file, diskCacheKey, closeable, uri)
): ImageSource = FileImageSource(file, diskCacheKey, closeable, null)

/**
* Create a new [ImageSource] backed by a [File].
*
* @param file The file to read from.
* @param diskCacheKey An optional cache key for the [file] in the disk cache.
* @param closeable An optional closeable reference that will
* be closed when the image source is closed.
* @param metadata Metadata for this image source.
*/
@ExperimentalCoilApi
@JvmName("create")
fun ImageSource(
file: File,
diskCacheKey: String? = null,
closeable: Closeable? = null,
metadata: ImageSource.Metadata? = null,
): ImageSource = FileImageSource(file, diskCacheKey, closeable, metadata)

/**
* Create a new [ImageSource] backed by a [BufferedSource].
*
* @param source The buffered source to read from.
* @param context A context used to resolve a safe cache directory.
*/
@JvmName("create")
fun ImageSource(
source: BufferedSource,
context: Context,
): ImageSource = SourceImageSource(source, context.safeCacheDir, null)

/**
* Create a new [ImageSource] backed by a [BufferedSource].
*
* @param source The buffered source to read from.
* @param context A context used to resolve a safe cache directory.
* @param uri An optional [Uri] that resolves to the image data.
* @param metadata Metadata for this image source.
*/
@JvmOverloads
@ExperimentalCoilApi
@JvmName("create")
fun ImageSource(
source: BufferedSource,
context: Context,
uri: Uri? = null
): ImageSource = SourceImageSource(source, context.safeCacheDir, uri)
metadata: ImageSource.Metadata? = null,
): ImageSource = SourceImageSource(source, context.safeCacheDir, metadata)

/**
* Create a new [ImageSource] backed by a [BufferedSource].
*
* @param source The buffered source to read from.
* @param cacheDirectory The directory to create temporary files in
* if [ImageSource.file] is called.
* @param uri An optional [Uri] that resolves to the image data.
*/
@JvmOverloads
@JvmName("create")
fun ImageSource(
source: BufferedSource,
cacheDirectory: File,
uri: Uri? = null
): ImageSource = SourceImageSource(source, cacheDirectory, uri)
): ImageSource = SourceImageSource(source, cacheDirectory, null)

/**
* Create a new [ImageSource] backed by a [BufferedSource].
*
* @param source The buffered source to read from.
* @param cacheDirectory The directory to create temporary files in
* if [ImageSource.file] is called.
* @param metadata Metadata for this image source.
*/
@ExperimentalCoilApi
@JvmName("create")
fun ImageSource(
source: BufferedSource,
cacheDirectory: File,
metadata: ImageSource.Metadata? = null,
): ImageSource = SourceImageSource(source, cacheDirectory, metadata)

/**
* Provides access to the image data to be decoded.
*/
sealed class ImageSource : Closeable {

/**
* Return the [Metadata] for this [ImageSource].
*/
@ExperimentalCoilApi
abstract val metadata: Metadata?

/**
* Return a [BufferedSource] to read this [ImageSource].
*/
Expand All @@ -96,26 +143,52 @@ sealed class ImageSource : Closeable {
abstract fun fileOrNull(): File?

/**
* If available, return a [Uri] which resolves to the location of the image data.
* A marker class for metadata for an [ImageSource].
*
* **Heavily prefer** using [source] or [file] to decode the image's data instead of relying
* on information provided in the metadata. It's the responsibility of a [Fetcher] to create a
* [BufferedSource] or [File] that can be easily read irrespective of where the image data is
* located. A [Decoder] should be as decoupled as possible from where the image is being fetched
* from.
*
* **Heavily prefer** using [source] or [file] to decode the image's data instead of this method
* as there's no uniform way to read the data for a [Uri]. It's the responsibility of a
* [Fetcher] to create a [BufferedSource] or [File] that can be easily read irrespective of
* where the image data is located.
* This method is provided as a way to pass information to decoders that don't support decoding
* a [BufferedSource] and want to avoid creating a temporary file (e.g. [ImageDecoder],
* [MediaMetadataRetriever], etc.).
*
* This method is provided as a way to use decoders that don't support decoding a
* [BufferedSource] and want to avoid creating a temporary file (e.g. [MediaMetadataRetriever],
* [ImageDecoder], etc.).
* @see AssetMetadata
* @see ContentMetadata
* @see ResourceMetadata
*/
@ExperimentalCoilApi
abstract fun uriOrNull(): Uri?
abstract class Metadata
}

/**
* Metadata containing the [fileName] of an Android asset.
*/
@ExperimentalCoilApi
class AssetMetadata(val fileName: String) : ImageSource.Metadata()

/**
* Metadata containing the [uri] of `content` URI.
*/
@ExperimentalCoilApi
class ContentMetadata(val uri: Uri) : ImageSource.Metadata()

/**
* Metadata containing the [packageName] and [resId] of an Android resource.
*/
@ExperimentalCoilApi
class ResourceMetadata(
val packageName: String,
@DrawableRes val resId: Int
) : ImageSource.Metadata()

internal class FileImageSource(
internal val file: File,
internal val diskCacheKey: String?,
private val closeable: Closeable?,
private val uri: Uri?
override val metadata: Metadata?
) : ImageSource() {

private var isClosed = false
Expand All @@ -142,12 +215,6 @@ internal class FileImageSource(

override fun fileOrNull() = file()

@Synchronized
override fun uriOrNull(): Uri? {
assertNotClosed()
return uri
}

@Synchronized
override fun close() {
isClosed = true
Expand All @@ -163,7 +230,7 @@ internal class FileImageSource(
internal class SourceImageSource(
source: BufferedSource,
private val cacheDirectory: File,
private val uri: Uri?
override val metadata: Metadata?
) : ImageSource() {

private var isClosed = false
Expand Down Expand Up @@ -201,12 +268,6 @@ internal class SourceImageSource(
return file
}

@Synchronized
override fun uriOrNull(): Uri? {
assertNotClosed()
return uri
}

@Synchronized
override fun close() {
isClosed = true
Expand Down
8 changes: 6 additions & 2 deletions coil-base/src/main/java/coil/fetch/AssetUriFetcher.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package coil.fetch
import android.net.Uri
import android.webkit.MimeTypeMap
import coil.ImageLoader
import coil.decode.AssetMetadata
import coil.decode.DataSource
import coil.decode.ImageSource
import coil.request.Options
Expand All @@ -18,10 +19,13 @@ internal class AssetUriFetcher(

override suspend fun fetch(): FetchResult {
val path = data.pathSegments.drop(1).joinToString("/")
val inputStream = options.context.assets.open(path)

return SourceResult(
source = ImageSource(inputStream.source().buffer(), options.context, data),
source = ImageSource(
source = options.context.assets.open(path).source().buffer(),
context = options.context,
metadata = AssetMetadata(data.lastPathSegment!!)
),
mimeType = MimeTypeMap.getSingleton().getMimeTypeFromUrl(path),
dataSource = DataSource.DISK
)
Expand Down
12 changes: 8 additions & 4 deletions coil-base/src/main/java/coil/fetch/ContentUriFetcher.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package coil.fetch

import android.content.ContentResolver
import android.content.ContentResolver.EXTRA_SIZE
import android.content.ContentResolver.SCHEME_CONTENT
import android.graphics.Point
import android.net.Uri
import android.os.Build.VERSION.SDK_INT
Expand All @@ -11,6 +12,7 @@ import android.provider.ContactsContract.Contacts
import android.provider.MediaStore
import androidx.annotation.VisibleForTesting
import coil.ImageLoader
import coil.decode.ContentMetadata
import coil.decode.DataSource
import coil.decode.ImageSource
import coil.request.Options
Expand Down Expand Up @@ -43,7 +45,11 @@ internal class ContentUriFetcher(
}

return SourceResult(
source = ImageSource(inputStream.source().buffer(), options.context, data),
source = ImageSource(
source = inputStream.source().buffer(),
context = options.context,
metadata = ContentMetadata(data)
),
mimeType = contentResolver.getType(data),
dataSource = DataSource.DISK
)
Expand Down Expand Up @@ -86,8 +92,6 @@ internal class ContentUriFetcher(
return ContentUriFetcher(data, options)
}

private fun isApplicable(data: Uri): Boolean {
return data.scheme == ContentResolver.SCHEME_CONTENT
}
private fun isApplicable(data: Uri) = data.scheme == SCHEME_CONTENT
}
}
10 changes: 6 additions & 4 deletions coil-base/src/main/java/coil/fetch/ResourceUriFetcher.kt
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
package coil.fetch

import android.content.ContentResolver
import android.content.ContentResolver.SCHEME_ANDROID_RESOURCE
import android.net.Uri
import android.util.TypedValue
import android.webkit.MimeTypeMap
import coil.ImageLoader
import coil.decode.DataSource
import coil.decode.ImageSource
import coil.decode.ResourceMetadata
import coil.request.Options
import coil.util.DrawableUtils
import coil.util.getDrawableCompat
Expand Down Expand Up @@ -65,7 +66,8 @@ internal class ResourceUriFetcher(
SourceResult(
source = ImageSource(
source = resources.openRawResource(resId).source().buffer(),
context = context
context = context,
metadata = ResourceMetadata(packageName, resId)
),
mimeType = mimeType,
dataSource = DataSource.DISK
Expand All @@ -74,7 +76,7 @@ internal class ResourceUriFetcher(
}

private fun throwInvalidUriException(data: Uri): Nothing {
throw IllegalStateException("Invalid ${ContentResolver.SCHEME_ANDROID_RESOURCE} URI: $data")
throw IllegalStateException("Invalid $SCHEME_ANDROID_RESOURCE URI: $data")
}

class Factory : Fetcher.Factory<Uri> {
Expand All @@ -85,7 +87,7 @@ internal class ResourceUriFetcher(
}

private fun isApplicable(data: Uri): Boolean {
return data.scheme == ContentResolver.SCHEME_ANDROID_RESOURCE
return data.scheme == SCHEME_ANDROID_RESOURCE
}
}

Expand Down

0 comments on commit b693246

Please sign in to comment.