diff --git a/coil-base/api/coil-base.api b/coil-base/api/coil-base.api index 8fa148c599..21ac1fc263 100644 --- a/coil-base/api/coil-base.api +++ b/coil-base/api/coil-base.api @@ -87,6 +87,7 @@ public abstract interface class coil/ImageLoader { public final class coil/ImageLoader$Builder { public fun (Landroid/content/Context;)V + public final fun addLastModifiedToFileCacheKey (Z)Lcoil/ImageLoader$Builder; public final fun allowHardware (Z)Lcoil/ImageLoader$Builder; public final fun allowRgb565 (Z)Lcoil/ImageLoader$Builder; public final fun availableMemoryPercentage (D)Lcoil/ImageLoader$Builder; @@ -108,6 +109,7 @@ public final class coil/ImageLoader$Builder { public final fun eventListener (Lcoil/EventListener;)Lcoil/ImageLoader$Builder; public final fun fallback (I)Lcoil/ImageLoader$Builder; public final fun fallback (Landroid/graphics/drawable/Drawable;)Lcoil/ImageLoader$Builder; + public final fun launchInterceptorChainOnMainThread (Z)Lcoil/ImageLoader$Builder; public final fun logger (Lcoil/util/Logger;)Lcoil/ImageLoader$Builder; public final fun memoryCachePolicy (Lcoil/request/CachePolicy;)Lcoil/ImageLoader$Builder; public final fun networkCachePolicy (Lcoil/request/CachePolicy;)Lcoil/ImageLoader$Builder; diff --git a/coil-base/src/androidTest/java/coil/RealImageLoaderTest.kt b/coil-base/src/androidTest/java/coil/RealImageLoaderTest.kt index 945dddbe95..48a1b755f0 100644 --- a/coil-base/src/androidTest/java/coil/RealImageLoaderTest.kt +++ b/coil-base/src/androidTest/java/coil/RealImageLoaderTest.kt @@ -38,6 +38,7 @@ import coil.util.Utils import coil.util.createMockWebServer import coil.util.decodeBitmapAsset import coil.util.getDrawableCompat +import coil.util.runBlockingTest import coil.util.size import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers @@ -88,6 +89,8 @@ class RealImageLoaderTest { callFactory = OkHttpClient(), eventListenerFactory = EventListener.Factory.NONE, componentRegistry = ComponentRegistry(), + addLastModifiedToFileCacheKey = true, + launchInterceptorChainOnMainThread = true, logger = null ) } @@ -397,6 +400,26 @@ class RealImageLoaderTest { } } + @Test + fun cachedValueIsResolvedSynchronously() = runBlockingTest { + val key = MemoryCache.Key("fake_key") + val fileName = "normal.jpg" + decodeAssetAndAddToMemoryCache(key, fileName) + + var isSuccessful = false + val request = ImageRequest.Builder(context) + .data("$SCHEME_FILE:///$ASSET_FILE_PATH_ROOT/$fileName") + .size(100, 100) + .precision(Precision.INEXACT) + .memoryCacheKey(key) + .target { isSuccessful = true } + .build() + imageLoader.enqueue(request).dispose() + + // isSuccessful should be synchronously set to true. + assertTrue(isSuccessful) + } + private fun testEnqueue(data: Any, expectedSize: PixelSize = PixelSize(80, 100)) { val imageView = ImageView(context) imageView.scaleType = ImageView.ScaleType.FIT_CENTER diff --git a/coil-base/src/androidTest/java/coil/fetch/FileFetcherTest.kt b/coil-base/src/androidTest/java/coil/fetch/FileFetcherTest.kt index a0bd3321e9..cb59daa54e 100644 --- a/coil-base/src/androidTest/java/coil/fetch/FileFetcherTest.kt +++ b/coil-base/src/androidTest/java/coil/fetch/FileFetcherTest.kt @@ -27,7 +27,7 @@ class FileFetcherTest { @Test fun basic() { - val fetcher = FileFetcher() + val fetcher = FileFetcher(true) val file = context.copyAssetToFile("normal.jpg") assertTrue(fetcher.handles(file)) @@ -44,7 +44,7 @@ class FileFetcherTest { @Test fun fileCacheKeyWithLastModified() { - val fetcher = FileFetcher() + val fetcher = FileFetcher(true) val file = context.copyAssetToFile("normal.jpg") file.setLastModified(1234L) @@ -55,4 +55,18 @@ class FileFetcherTest { assertNotEquals(secondKey, firstKey) } + + @Test + fun fileCacheKeyWithoutLastModified() { + val fetcher = FileFetcher(false) + val file = context.copyAssetToFile("normal.jpg") + + file.setLastModified(1234L) + val firstKey = fetcher.key(file) + + file.setLastModified(4321L) + val secondKey = fetcher.key(file) + + assertEquals(secondKey, firstKey) + } } diff --git a/coil-base/src/androidTest/java/coil/util/SystemCallbacksTest.kt b/coil-base/src/androidTest/java/coil/util/SystemCallbacksTest.kt index cc0f0f1540..2808b75cb0 100644 --- a/coil-base/src/androidTest/java/coil/util/SystemCallbacksTest.kt +++ b/coil-base/src/androidTest/java/coil/util/SystemCallbacksTest.kt @@ -68,6 +68,8 @@ class SystemCallbacksTest { callFactory = OkHttpClient(), eventListenerFactory = EventListener.Factory.NONE, componentRegistry = ComponentRegistry(), + addLastModifiedToFileCacheKey = true, + launchInterceptorChainOnMainThread = true, logger = null ) val systemCallbacks = SystemCallbacks(imageLoader, context) diff --git a/coil-base/src/main/java/coil/EventListener.kt b/coil-base/src/main/java/coil/EventListener.kt index fb0d65fe13..7e7b047f5c 100644 --- a/coil-base/src/main/java/coil/EventListener.kt +++ b/coil-base/src/main/java/coil/EventListener.kt @@ -1,6 +1,7 @@ package coil import android.graphics.Bitmap +import androidx.annotation.AnyThread import androidx.annotation.MainThread import androidx.annotation.WorkerThread import coil.annotation.ExperimentalCoilApi @@ -53,7 +54,7 @@ interface EventListener : ImageRequest.Listener { * * @param input The data that will be converted. */ - @WorkerThread + @AnyThread fun mapStart(request: ImageRequest, input: Any) {} /** @@ -62,7 +63,7 @@ interface EventListener : ImageRequest.Listener { * @param output The data after it has been converted. If there were no applicable mappers, * [output] will be the same as [ImageRequest.data]. */ - @WorkerThread + @AnyThread fun mapEnd(request: ImageRequest, output: Any) {} /** diff --git a/coil-base/src/main/java/coil/ImageLoader.kt b/coil-base/src/main/java/coil/ImageLoader.kt index 471ea68a46..b90c6f5b4e 100644 --- a/coil-base/src/main/java/coil/ImageLoader.kt +++ b/coil-base/src/main/java/coil/ImageLoader.kt @@ -14,6 +14,9 @@ import coil.bitmap.EmptyBitmapReferenceCounter import coil.bitmap.RealBitmapPool import coil.bitmap.RealBitmapReferenceCounter import coil.drawable.CrossfadeDrawable +import coil.fetch.Fetcher +import coil.intercept.Interceptor +import coil.map.Mapper import coil.memory.EmptyWeakMemoryCache import coil.memory.MemoryCache import coil.memory.RealWeakMemoryCache @@ -37,8 +40,11 @@ import coil.util.getDrawableCompat import coil.util.lazyCallFactory import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.MainCoroutineDispatcher +import kotlinx.coroutines.withContext import okhttp3.Call import okhttp3.OkHttpClient +import java.io.File /** * A service class that loads images by executing [ImageRequest]s. Image loaders handle caching, data fetching, @@ -133,7 +139,9 @@ interface ImageLoader { private var availableMemoryPercentage = Utils.getDefaultAvailableMemoryPercentage(applicationContext) private var bitmapPoolPercentage = Utils.getDefaultBitmapPoolPercentage() + private var addLastModifiedToFileCacheKey = true private var bitmapPoolingEnabled = true + private var launchInterceptorChainOnMainThread = true private var trackWeakReferences = true /** @@ -262,6 +270,19 @@ interface ImageLoader { this.defaults = this.defaults.copy(allowRgb565 = enable) } + /** + * Enables adding [File.lastModified] to the memory cache key when loading an image from a [File]. + * + * This allows subsequent requests that load the same file to miss the memory cache if the file has been updated. + * However, if the memory cache check occurs on the main thread (see [launchInterceptorChainOnMainThread]) + * calling [File.lastModified] will cause a strict mode violation. + * + * Default: true + */ + fun addLastModifiedToFileCacheKey(enable: Boolean) = apply { + this.addLastModifiedToFileCacheKey = enable + } + /** * Enables counting references to bitmaps so they can be automatically reused by a [BitmapPool] * when their reference count reaches zero. @@ -277,6 +298,31 @@ interface ImageLoader { this.bitmapPoolingEnabled = enable } + /** + * Enables launching the [Interceptor] chain on the main thread. + * + * If true, the [Interceptor] chain will be launched from [MainCoroutineDispatcher.immediate]. This allows + * the [ImageLoader] to check its memory cache and return a cached value synchronously if the request is + * started from the main thread. However, [Mapper.map] and [Fetcher.key] operations will be executed on the + * main thread as well, which has a performance cost. + * + * If false, the [Interceptor] chain will be launched from the request's [ImageRequest.dispatcher]. + * This will result in better UI performance, but values from the memory cache will not be resolved + * synchronously. + * + * The actual fetch + decode process always occurs on [ImageRequest.dispatcher] and is unaffected by this flag. + * + * It's worth noting that [Interceptor]s can also control which [CoroutineDispatcher] the + * memory cache is checked on by calling [Interceptor.Chain.proceed] inside a [withContext] block. + * Therefore if you set [launchInterceptorChainOnMainThread] to true, you can control which [ImageRequest]s + * check the memory cache synchronously at runtime. + * + * Default: true + */ + fun launchInterceptorChainOnMainThread(enable: Boolean) = apply { + this.launchInterceptorChainOnMainThread = enable + } + /** * Enables weak reference tracking of loaded images. * @@ -456,6 +502,8 @@ interface ImageLoader { callFactory = callFactory ?: buildDefaultCallFactory(), eventListenerFactory = eventListenerFactory ?: EventListener.Factory.NONE, componentRegistry = registry ?: ComponentRegistry(), + addLastModifiedToFileCacheKey = addLastModifiedToFileCacheKey, + launchInterceptorChainOnMainThread = launchInterceptorChainOnMainThread, logger = logger ) } diff --git a/coil-base/src/main/java/coil/RealImageLoader.kt b/coil-base/src/main/java/coil/RealImageLoader.kt index bce57e0123..5830bcbc06 100644 --- a/coil-base/src/main/java/coil/RealImageLoader.kt +++ b/coil-base/src/main/java/coil/RealImageLoader.kt @@ -14,12 +14,12 @@ import coil.fetch.BitmapFetcher import coil.fetch.ContentUriFetcher import coil.fetch.DrawableFetcher import coil.fetch.FileFetcher +import coil.fetch.HttpUriFetcher import coil.fetch.HttpUrlFetcher import coil.fetch.ResourceUriFetcher import coil.intercept.EngineInterceptor import coil.intercept.RealInterceptorChain import coil.map.FileUriMapper -import coil.map.HttpUriMapper import coil.map.ResourceIntMapper import coil.map.ResourceUriMapper import coil.map.StringMapper @@ -78,6 +78,8 @@ internal class RealImageLoader( callFactory: Call.Factory, private val eventListenerFactory: EventListener.Factory, componentRegistry: ComponentRegistry, + addLastModifiedToFileCacheKey: Boolean, + private val launchInterceptorChainOnMainThread: Boolean, val logger: Logger? ) : ImageLoader { @@ -92,13 +94,13 @@ internal class RealImageLoader( private val registry = componentRegistry.newBuilder() // Mappers .add(StringMapper()) - .add(HttpUriMapper()) .add(FileUriMapper()) .add(ResourceUriMapper(context)) .add(ResourceIntMapper(context)) // Fetchers + .add(HttpUriFetcher(callFactory)) .add(HttpUrlFetcher(callFactory)) - .add(FileFetcher()) + .add(FileFetcher(addLastModifiedToFileCacheKey)) .add(AssetUriFetcher(context)) .add(ContentUriFetcher(context)) .add(ResourceUriFetcher(context, drawableDecoder)) @@ -227,8 +229,15 @@ internal class RealImageLoader( size: Size, cached: Bitmap?, eventListener: EventListener - ): ImageResult = withContext(request.dispatcher) { - RealInterceptorChain(request, type, interceptors, 0, request, size, cached, eventListener).proceed(request) + ): ImageResult { + val chain = RealInterceptorChain(request, type, interceptors, 0, request, size, cached, eventListener) + return if (launchInterceptorChainOnMainThread) { + chain.proceed(request) + } else { + withContext(request.dispatcher) { + chain.proceed(request) + } + } } private suspend inline fun onSuccess( diff --git a/coil-base/src/main/java/coil/fetch/Fetcher.kt b/coil-base/src/main/java/coil/fetch/Fetcher.kt index c82c3e6463..d9f424e49f 100644 --- a/coil-base/src/main/java/coil/fetch/Fetcher.kt +++ b/coil-base/src/main/java/coil/fetch/Fetcher.kt @@ -16,7 +16,7 @@ import okio.BufferedSource * To accomplish this, fetchers fit into one of two types: * * - Uses the data as a key to fetch bytes from a remote source (e.g. network or disk) - * and exposes it as a [BufferedSource]. e.g. [HttpUrlFetcher] + * and exposes it as a [BufferedSource]. e.g. [HttpFetcher] * - Reads the data directly and translates it into a [Drawable]. e.g. [BitmapFetcher] */ interface Fetcher { diff --git a/coil-base/src/main/java/coil/fetch/FileFetcher.kt b/coil-base/src/main/java/coil/fetch/FileFetcher.kt index b313554e94..02fff00f4b 100644 --- a/coil-base/src/main/java/coil/fetch/FileFetcher.kt +++ b/coil-base/src/main/java/coil/fetch/FileFetcher.kt @@ -9,9 +9,11 @@ import okio.buffer import okio.source import java.io.File -internal class FileFetcher : Fetcher { +internal class FileFetcher(private val addLastModifiedToFileCacheKey: Boolean) : Fetcher { - override fun key(data: File) = "${data.path}:${data.lastModified()}" + override fun key(data: File): String { + return if (addLastModifiedToFileCacheKey) "${data.path}:${data.lastModified()}" else data.path + } override suspend fun fetch( pool: BitmapPool, diff --git a/coil-base/src/main/java/coil/fetch/HttpUrlFetcher.kt b/coil-base/src/main/java/coil/fetch/HttpFetcher.kt similarity index 73% rename from coil-base/src/main/java/coil/fetch/HttpUrlFetcher.kt rename to coil-base/src/main/java/coil/fetch/HttpFetcher.kt index 77219b742b..b0b3de4984 100644 --- a/coil-base/src/main/java/coil/fetch/HttpUrlFetcher.kt +++ b/coil-base/src/main/java/coil/fetch/HttpFetcher.kt @@ -1,10 +1,12 @@ package coil.fetch +import android.net.Uri import android.webkit.MimeTypeMap import androidx.annotation.VisibleForTesting import coil.bitmap.BitmapPool import coil.decode.DataSource import coil.decode.Options +import coil.map.Mapper import coil.network.HttpException import coil.size.Size import coil.util.await @@ -15,17 +17,38 @@ import okhttp3.HttpUrl import okhttp3.Request import okhttp3.ResponseBody -internal class HttpUrlFetcher(private val callFactory: Call.Factory) : Fetcher { +internal class HttpUriFetcher(callFactory: Call.Factory) : HttpFetcher(callFactory) { + + override fun handles(data: Uri) = data.scheme == "http" || data.scheme == "https" + + override fun key(data: Uri) = data.toString() + + override fun Uri.toHttpUrl(): HttpUrl = HttpUrl.get(toString()) +} + +internal class HttpUrlFetcher(callFactory: Call.Factory) : HttpFetcher(callFactory) { override fun key(data: HttpUrl) = data.toString() + override fun HttpUrl.toHttpUrl(): HttpUrl = this +} + +internal abstract class HttpFetcher(private val callFactory: Call.Factory) : Fetcher { + + /** + * Perform this conversion in a [Fetcher] instead of a [Mapper] so + * [HttpUriFetcher] can execute [HttpUrl.get] on a background thread. + */ + abstract fun T.toHttpUrl(): HttpUrl + override suspend fun fetch( pool: BitmapPool, - data: HttpUrl, + data: T, size: Size, options: Options ): FetchResult { - val request = Request.Builder().url(data).headers(options.headers) + val url = data.toHttpUrl() + val request = Request.Builder().url(url).headers(options.headers) val networkRead = options.networkCachePolicy.readEnabled val diskRead = options.diskCachePolicy.readEnabled @@ -52,7 +75,7 @@ internal class HttpUrlFetcher(private val callFactory: Call.Factory) : Fetcher referenceCounter.setValid(data.bitmap as Bitmap?, false) - is Bitmap -> referenceCounter.setValid(data, false) - } - } - - /** Allow pooling the successful drawable's bitmap. */ - private fun validateDrawable(drawable: Drawable) { - val bitmap = (drawable as? BitmapDrawable)?.bitmap - if (bitmap != null) { - // Mark this bitmap as valid for pooling (if it has not already been made invalid). - referenceCounter.setValid(bitmap, true) - - // Eagerly increment the bitmap's reference count to prevent it being pooled on another thread. - referenceCounter.increment(bitmap) - } - } - /** Compute the complex cache key for this request. */ @VisibleForTesting internal fun computeMemoryCacheKey( @@ -202,8 +187,7 @@ internal class EngineInterceptor( } /** Return true if [cacheValue]'s size satisfies the [request]. */ - @VisibleForTesting - internal fun isSizeValid( + private fun isSizeValid( cacheKey: MemoryCache.Key?, cacheValue: RealMemoryCache.Value, request: ImageRequest, @@ -267,9 +251,29 @@ internal class EngineInterceptor( return true } + /** Prevent pooling the input data's bitmap. */ + @Suppress("USELESS_CAST") + private fun invalidateData(data: Any) { + when (data) { + is BitmapDrawable -> referenceCounter.setValid(data.bitmap as Bitmap?, false) + is Bitmap -> referenceCounter.setValid(data, false) + } + } + + /** Allow pooling the successful drawable's bitmap. */ + private fun validateDrawable(drawable: Drawable) { + val bitmap = (drawable as? BitmapDrawable)?.bitmap + if (bitmap != null) { + // Mark this bitmap as valid for pooling (if it has not already been made invalid). + referenceCounter.setValid(bitmap, true) + + // Eagerly increment the bitmap's reference count to prevent it being pooled on another thread. + referenceCounter.increment(bitmap) + } + } + /** Load the [data] as a [Drawable]. Apply any [Transformation]s. */ - @VisibleForTesting - internal suspend inline fun execute( + private suspend inline fun execute( data: Any, fetcher: Fetcher, request: ImageRequest, @@ -290,7 +294,9 @@ internal class EngineInterceptor( coroutineContext.ensureActive() // Find the relevant decoder. - val isDiskOnlyPreload = type == REQUEST_TYPE_ENQUEUE && request.target == null && !request.memoryCachePolicy.writeEnabled + val isDiskOnlyPreload = type == REQUEST_TYPE_ENQUEUE && + request.target == null && + !request.memoryCachePolicy.writeEnabled val decoder = if (isDiskOnlyPreload) { // Skip decoding the result if we are preloading the data and writing to the memory cache is // disabled. Instead, we exhaust the source and return an empty result. diff --git a/coil-base/src/main/java/coil/intercept/Interceptor.kt b/coil-base/src/main/java/coil/intercept/Interceptor.kt index 83e1d7b522..3ac8deb1e2 100644 --- a/coil-base/src/main/java/coil/intercept/Interceptor.kt +++ b/coil-base/src/main/java/coil/intercept/Interceptor.kt @@ -8,6 +8,9 @@ import coil.size.Size /** * Observe, transform, short circuit, or retry requests to an [ImageLoader]'s image engine. + * + * NOTE: The interceptor chain is launched from the main thread by default. + * See [ImageLoader.Builder.launchInterceptorChainOnMainThread] for more information. */ @ExperimentalCoilApi interface Interceptor { diff --git a/coil-base/src/main/java/coil/map/HttpUriMapper.kt b/coil-base/src/main/java/coil/map/HttpUriMapper.kt deleted file mode 100644 index fd31dda2b1..0000000000 --- a/coil-base/src/main/java/coil/map/HttpUriMapper.kt +++ /dev/null @@ -1,11 +0,0 @@ -package coil.map - -import android.net.Uri -import okhttp3.HttpUrl - -internal class HttpUriMapper : Mapper { - - override fun handles(data: Uri) = data.scheme == "http" || data.scheme == "https" - - override fun map(data: Uri): HttpUrl = HttpUrl.get(data.toString()) -} diff --git a/coil-base/src/main/java/coil/network/HttpException.kt b/coil-base/src/main/java/coil/network/HttpException.kt index 9cd84d226a..19fc45481b 100644 --- a/coil-base/src/main/java/coil/network/HttpException.kt +++ b/coil-base/src/main/java/coil/network/HttpException.kt @@ -2,12 +2,12 @@ package coil.network -import coil.fetch.HttpUrlFetcher +import coil.fetch.HttpFetcher import okhttp3.Response /** * Exception for an unexpected, non-2xx HTTP response. * - * @see HttpUrlFetcher + * @see HttpFetcher */ class HttpException(val response: Response) : RuntimeException("HTTP ${response.code()}: ${response.message()}") diff --git a/coil-base/src/main/java/coil/network/NetworkObserver.kt b/coil-base/src/main/java/coil/network/NetworkObserver.kt index 688ed12f9e..8c8d0d3af2 100644 --- a/coil-base/src/main/java/coil/network/NetworkObserver.kt +++ b/coil-base/src/main/java/coil/network/NetworkObserver.kt @@ -68,7 +68,7 @@ internal interface NetworkObserver { private object EmptyNetworkObserver : NetworkObserver { - override val isOnline = true + override val isOnline get() = true override fun shutdown() {} } diff --git a/coil-base/src/test/java/coil/fetch/HttpUrlFetcherTest.kt b/coil-base/src/test/java/coil/fetch/HttpFetcherTest.kt similarity index 93% rename from coil-base/src/test/java/coil/fetch/HttpUrlFetcherTest.kt rename to coil-base/src/test/java/coil/fetch/HttpFetcherTest.kt index 26037f20d3..492b09df10 100644 --- a/coil-base/src/test/java/coil/fetch/HttpUrlFetcherTest.kt +++ b/coil-base/src/test/java/coil/fetch/HttpFetcherTest.kt @@ -2,6 +2,7 @@ package coil.fetch import android.content.Context import android.webkit.MimeTypeMap +import androidx.core.net.toUri import androidx.test.core.app.ApplicationProvider import coil.bitmap.BitmapPool import coil.size.PixelSize @@ -32,14 +33,13 @@ import kotlin.test.assertTrue @RunWith(RobolectricTestRunner::class) @OptIn(ExperimentalCoroutinesApi::class) -class HttpUrlFetcherTest { +class HttpFetcherTest { private lateinit var context: Context private lateinit var mainDispatcher: TestCoroutineDispatcher private lateinit var server: MockWebServer private lateinit var callFactory: Call.Factory private lateinit var pool: BitmapPool - private lateinit var fetcher: HttpUrlFetcher @Before fun before() { @@ -48,7 +48,6 @@ class HttpUrlFetcherTest { server = createMockWebServer(context, "normal.jpg") callFactory = OkHttpClient() pool = BitmapPool(0) - fetcher = HttpUrlFetcher(callFactory) } @After @@ -59,6 +58,7 @@ class HttpUrlFetcherTest { @Test fun `basic network URL fetch`() { + val fetcher = HttpUrlFetcher(callFactory) val url = server.url("/normal.jpg") assertTrue(fetcher.handles(url)) assertEquals(url.toString(), fetcher.key(url)) @@ -73,7 +73,8 @@ class HttpUrlFetcherTest { @Test fun `basic network URI fetch`() { - val uri = server.url("/normal.jpg") + val fetcher = HttpUriFetcher(callFactory) + val uri = server.url("/normal.jpg").toString().toUri() assertTrue(fetcher.handles(uri)) assertEquals(uri.toString(), fetcher.key(uri)) @@ -87,6 +88,8 @@ class HttpUrlFetcherTest { @Test fun `mime type is parsed correctly from content type`() { + val fetcher = HttpUriFetcher(callFactory) + // https://android.googlesource.com/platform/frameworks/base/+/61ae88e/core/java/android/webkit/MimeTypeMap.java#407 Shadows.shadowOf(MimeTypeMap.getSingleton()).addExtensionMimeTypMapping("svg", "image/svg+xml")