Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix android.resource Uris that point to a vector drawable fail to load #51

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,22 @@ class RealImageLoaderIntegrationTest {
testGet(data)
}

@Test
fun resourceUriVectorSamePackage() {
val data = Uri.parse("${ContentResolver.SCHEME_ANDROID_RESOURCE}:https://${context.packageName}/${R.drawable.ic_android}")
val expectedSize = PixelSize(100, 100)
testLoad(data, expectedSize)
testGet(data, expectedSize)
}

@Test
fun resourceUriVectorDifferentPackage() {
val data = Uri.parse("android.resource:https://com.android.messaging/drawable/abc_ic_ab_back_material")
val expectedSize = PixelSize(100, 100)
testLoad(data, expectedSize)
testGet(data, expectedSize)
}

@Test
fun file() {
val data = copyNormalImageAssetToCacheDir()
Expand Down
168 changes: 168 additions & 0 deletions coil-base/src/androidTest/java/coil/fetch/UriFetcherTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,29 @@
package coil.fetch

import android.content.Context
import android.content.pm.PackageManager
import android.net.Uri
import androidx.core.net.toUri
import androidx.test.core.app.ApplicationProvider
import coil.bitmappool.BitmapPool
import coil.bitmappool.FakeBitmapPool
import coil.base.test.R
import coil.size.PixelSize
import coil.util.createOptions
import kotlinx.coroutines.runBlocking
import okio.buffer
import okio.sink
import okio.source
import org.hamcrest.CoreMatchers.startsWith
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.rules.ExpectedException
import java.io.File
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertNotEquals
import kotlin.test.assertNotNull
import kotlin.test.assertTrue

class UriFetcherTest {
Expand All @@ -29,6 +35,10 @@ class UriFetcherTest {
private lateinit var loader: UriFetcher
private lateinit var pool: BitmapPool

@Rule
@JvmField
var thrown: ExpectedException = ExpectedException.none()

@Before
fun before() {
loader = UriFetcher(context)
Expand Down Expand Up @@ -67,6 +77,33 @@ class UriFetcherTest {
assertFalse(result.source.exhausted())
}

@Test
fun basicAndroidResourceVectorFetchSamePackage() {
val resourceId = R.drawable.ic_android
val uri = Uri.parse("android.resource:https://${context.packageName}/$resourceId")
assertTrue(loader.handles(uri))

val result = runBlocking {
loader.fetch(pool, uri, PixelSize(100, 100), createOptions())
}

assertTrue(result is SourceResult)
assertFalse(result.source.exhausted())
}

@Test
fun basicAndroidResourceVectorFetchDifferentPackage() {
val uri = Uri.parse("android.resource:https://com.android.messaging/drawable/abc_ic_ab_back_material")
assertTrue(loader.handles(uri))

val result = runBlocking {
loader.fetch(pool, uri, PixelSize(100, 100), createOptions())
}

assertTrue(result is SourceResult)
assertFalse(result.source.exhausted())
}

@Test
fun basicExtractPath() {
val uri = Uri.parse("file:https:///android_asset/something.jpg")
Expand Down Expand Up @@ -125,4 +162,135 @@ class UriFetcherTest {
val uri = Uri.parse("content:https://fake/content/path")
assertEquals(uri.toString(), loader.key(uri))
}

@Test
fun findResourceIdFromUri() {
val resourceId = 12345
val uri = Uri.parse("android.resource:https://com.android.messaging/$resourceId")
val result = loader.findResourceIdFromUri(uri)
assertEquals(resourceId, result)
}

@Test
fun notResourceIdFromUri() {
val uri = Uri.parse("android.resource:https://com.android.messaging/")
thrown.expect(IllegalArgumentException::class.java)
thrown.expectMessage(startsWith("Failed to find resource id"))
loader.findResourceIdFromUri(uri)
}

@Test
fun findContextForSamePackage() {
val resourceId = 12345
val uri = Uri.parse("android.resource:https://${context.packageName}/$resourceId")
val expectedContext = context
val targetPackage = uri.authority
targetPackage?.let {
val result = loader.findContextForPackage(context, uri, it)
assertEquals(expectedContext, result)
}
}

@Test
fun findContextForDifferentPackage() {
val resourceId = 12345
val uri = Uri.parse("android.resource:https://com.android.messaging/$resourceId")
val expectedContext = context
val targetPackage = uri.authority
targetPackage?.let {
val result = loader.findContextForPackage(context, uri, it)
assertNotNull(result)
assertNotEquals(expectedContext, result)
}
}

@Test
fun unknownContextForDifferentPackage() {
val resourceId = 12345
val uri = Uri.parse("android.resource:https://com.test.beta/$resourceId")
val targetPackage = uri.authority
targetPackage?.let {
thrown.expect(PackageManager.NameNotFoundException::class.java)
thrown.expectMessage(startsWith("Failed to find target package on device for"))
loader.findContextForPackage(context, uri, it)
}
}

@Test
fun findResourceIdFromTypeAndNameResourceUriSamePackage() {
val uri = Uri.parse("android.resource:https://${context.packageName}/drawable/ic_android")
val targetPackage = uri.authority
targetPackage?.let {
val targetContext = loader.findContextForPackage(context, uri, it)
val result = loader.findResourceIdFromTypeAndNameResourceUri(targetContext, uri)
assertNotNull(result)
}
}

@Test
fun findResourceIdFromTypeAndNameResourceUriDifferentPackage() {
val uri = Uri.parse("android.resource:https://com.android.messaging/drawable/abc_ic_ab_back_material")
val targetPackage = uri.authority
targetPackage?.let {
val targetContext = loader.findContextForPackage(context, uri, it)
val result = loader.findResourceIdFromTypeAndNameResourceUri(targetContext, uri)
assertNotNull(result)
}
}

@Test
fun unknownResourceIdFromTypeAndNameResourceUriSamePackage() {
val uri = Uri.parse("android.resource:https://${context.packageName}/drawable/not_exist")
val targetPackage = uri.authority
targetPackage?.let {
val targetContext = loader.findContextForPackage(context, uri, it)
thrown.expect(IllegalArgumentException::class.java)
thrown.expectMessage(startsWith("Failed to find name resource"))
loader.findResourceIdFromTypeAndNameResourceUri(targetContext, uri)
}
}

@Test
fun unknownResourceIdFromTypeAndNameResourceUriDifferentPackage() {
val uri = Uri.parse("android.resource:https://com.android.messaging/mipmap/not_exist")
val targetPackage = uri.authority
targetPackage?.let {
val targetContext = loader.findContextForPackage(context, uri, it)
thrown.expect(IllegalArgumentException::class.java)
thrown.expectMessage(startsWith("Failed to find name resource"))
loader.findResourceIdFromTypeAndNameResourceUri(targetContext, uri)
}
}

@Test
fun basicExtractResourceId() {
val resourceId = R.drawable.ic_android
val uri = Uri.parse("android.resource:https://${context.packageName}/$resourceId")
val result = loader.extractResourceId(context, uri)
assertEquals(resourceId, result)
}

@Test
fun notResourceIdFromTypeAndNameResourceUri() {
val uri = Uri.parse("android.resource:https://com.android.messaging/")
thrown.expect(IllegalArgumentException::class.java)
thrown.expectMessage(startsWith("Failed to find resource or unrecognized Uri format"))
loader.findResourceIdFromTypeAndNameResourceUri(context, uri)
}

@Test
fun emptyExtractResourceId() {
val uri = Uri.parse("android.resource:https://coil.sample/")
thrown.expect(IllegalArgumentException::class.java)
thrown.expectMessage(startsWith("Failed to find resource id for"))
loader.extractResourceId(context, uri)
}

@Test
fun nonAndroidResourceUriExtractResourceId() {
val uri = Uri.parse("android.resource:https://fake/file/path")
thrown.expect(IllegalArgumentException::class.java)
thrown.expectMessage(startsWith("Failed to find name resource id"))
loader.extractResourceId(context, uri)
}
}
96 changes: 96 additions & 0 deletions coil-base/src/main/java/coil/fetch/UriFetcher.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,20 @@ package coil.fetch

import android.content.ContentResolver
import android.content.Context
import android.content.pm.PackageManager
import android.content.res.AssetManager
import android.content.res.Resources
import android.net.Uri
import androidx.annotation.DrawableRes
import androidx.annotation.VisibleForTesting
import androidx.collection.arraySetOf
import androidx.core.net.toFile
import coil.bitmappool.BitmapPool
import coil.decode.DataSource
import coil.decode.Options
import coil.size.Size
import coil.util.Utils
import coil.util.getDrawableCompat
import okio.buffer
import okio.source

Expand All @@ -19,7 +24,21 @@ internal class UriFetcher(
) : Fetcher<Uri> {

companion object {

private const val ASSET_FILE_PATH_SEGMENT = "android_asset"
/**
* [Resources.getIdentifier] documents that it will return 0 and
* that 0 is not a valid resouce id.
*/
private const val ANDROID_PACKAGE_NAME = "android"
private const val MISSING_RESOURCE_ID = 0
// android.resource:https://<package_name>/<type>/<name>.
private const val NAME_URI_PATH_SEGMENTS = 2
private const val TYPE_PATH_SEGMENT_INDEX = 0
private const val NAME_PATH_SEGMENT_INDEX = 1
// android.resource:https://<package_name>/<resource_id>
private const val ID_PATH_SEGMENTS = 1
private const val RESOURCE_ID_SEGMENT_INDEX = 0

private val SUPPORTED_SCHEMES = arraySetOf(
ContentResolver.SCHEME_ANDROID_RESOURCE,
Expand All @@ -42,6 +61,19 @@ internal class UriFetcher(
size: Size,
options: Options
): FetchResult {
if (data.scheme == ContentResolver.SCHEME_ANDROID_RESOURCE) {
data.authority?.let { packageName ->
val targetContext = findContextForPackage(context, data, packageName)
val resourceId = extractResourceId(targetContext, data)
val drawable = targetContext.getDrawableCompat(resourceId)
val inputStream = Utils.getInputStreamFromBitmap(Utils.getBitmapFromDrawable(drawable, size))
return SourceResult(
source = inputStream.source().buffer(),
mimeType = targetContext.contentResolver.getType(data),
dataSource = DataSource.DISK)
}
}

val assetPath = extractAssetPath(data)
val inputStream = if (assetPath != null) {
context.assets.open(assetPath)
Expand Down Expand Up @@ -78,4 +110,68 @@ internal class UriFetcher(

return path
}

/** Return the resource ID from Uri. Else, return null. */
@VisibleForTesting
internal fun extractResourceId(context: Context, source: Uri): Int {
val segments = source.pathSegments
return when {
segments.count() == NAME_URI_PATH_SEGMENTS -> findResourceIdFromTypeAndNameResourceUri(context, source)
segments.count() == ID_PATH_SEGMENTS -> findResourceIdFromUri(source)
else -> throw IllegalArgumentException("Failed to find resource id for: $source")
}
}

/** Return context for source Uri. */
@VisibleForTesting
internal fun findContextForPackage(mContext: Context, source: Uri, packageName: String): Context {
if (packageName == mContext.packageName) {
return mContext
}

try {
return mContext.createPackageContext(packageName, /*flags=*/ 0)
} catch (e: PackageManager.NameNotFoundException) {
// The parent APK holds the correct context if the resource is located in a split
if (packageName.contains(mContext.packageName)) {
return mContext
}
throw PackageManager.NameNotFoundException(
"Failed to find target package on device for : $source")
}
}

// android.resource:https://com.android.camera2/mipmap/logo_camera_color
@DrawableRes
@VisibleForTesting
internal fun findResourceIdFromTypeAndNameResourceUri(context: Context, source: Uri): Int {
val segments = source.pathSegments
val packageName = source.authority
if (segments.isNotEmpty() && segments.count() == 2) {
val typeName = segments[TYPE_PATH_SEGMENT_INDEX]
val resourceName = segments[NAME_PATH_SEGMENT_INDEX]
var result = context.resources.getIdentifier(resourceName, typeName, packageName)
if (result == MISSING_RESOURCE_ID) {
result = Resources.getSystem().getIdentifier(resourceName, typeName, ANDROID_PACKAGE_NAME)
}
if (result == MISSING_RESOURCE_ID) {
throw IllegalArgumentException("Failed to find name resource id for: $source")
}
return result
} else {
throw IllegalArgumentException("Failed to find resource or unrecognized Uri format for: $source")
}
}

// android.resource:https://com.android.camera2/123456
@DrawableRes
@VisibleForTesting
internal fun findResourceIdFromUri(source: Uri): Int {
val segments = source.pathSegments
if (segments.isNotEmpty()) {
return segments[RESOURCE_ID_SEGMENT_INDEX].toInt()
} else {
throw IllegalArgumentException("Failed to find resource id for: $source")
}
}
}
Loading