diff options
-rw-r--r-- | packages/SystemUI/src/com/android/systemui/graphics/ImageLoader.kt | 52 | ||||
-rw-r--r-- | packages/SystemUI/tests/src/com/android/systemui/graphics/ImageLoaderContentProviderTest.kt | 109 |
2 files changed, 132 insertions, 29 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/graphics/ImageLoader.kt b/packages/SystemUI/src/com/android/systemui/graphics/ImageLoader.kt index ca43871415e6..25f99207ad28 100644 --- a/packages/SystemUI/src/com/android/systemui/graphics/ImageLoader.kt +++ b/packages/SystemUI/src/com/android/systemui/graphics/ImageLoader.kt @@ -27,7 +27,6 @@ import android.content.res.Resources import android.content.res.Resources.NotFoundException import android.graphics.Bitmap import android.graphics.ImageDecoder -import android.graphics.ImageDecoder.DecodeException import android.graphics.drawable.AdaptiveIconDrawable import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.Drawable @@ -39,7 +38,6 @@ import com.android.app.tracing.traceSection import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background -import java.io.IOException import javax.inject.Inject import kotlin.math.min import kotlinx.coroutines.CoroutineDispatcher @@ -54,7 +52,7 @@ class ImageLoader @Inject constructor( @Application private val defaultContext: Context, - @Background private val backgroundDispatcher: CoroutineDispatcher + @Background private val backgroundDispatcher: CoroutineDispatcher, ) { /** Source of the image data. */ @@ -103,7 +101,7 @@ constructor( source: Source, @Px maxWidth: Int = DEFAULT_MAX_SAFE_BITMAP_SIZE_PX, @Px maxHeight: Int = DEFAULT_MAX_SAFE_BITMAP_SIZE_PX, - allocator: Int = ImageDecoder.ALLOCATOR_DEFAULT + allocator: Int = ImageDecoder.ALLOCATOR_DEFAULT, ): Bitmap? = withContext(backgroundDispatcher) { loadBitmapSync(source, maxWidth, maxHeight, allocator) } @@ -127,14 +125,14 @@ constructor( source: Source, @Px maxWidth: Int = DEFAULT_MAX_SAFE_BITMAP_SIZE_PX, @Px maxHeight: Int = DEFAULT_MAX_SAFE_BITMAP_SIZE_PX, - allocator: Int = ImageDecoder.ALLOCATOR_DEFAULT + allocator: Int = ImageDecoder.ALLOCATOR_DEFAULT, ): Bitmap? { return try { loadBitmapSync( toImageDecoderSource(source, defaultContext), maxWidth, maxHeight, - allocator + allocator, ) } catch (e: NotFoundException) { Log.w(TAG, "Couldn't load resource $source", e) @@ -162,7 +160,7 @@ constructor( source: ImageDecoder.Source, @Px maxWidth: Int = DEFAULT_MAX_SAFE_BITMAP_SIZE_PX, @Px maxHeight: Int = DEFAULT_MAX_SAFE_BITMAP_SIZE_PX, - allocator: Int = ImageDecoder.ALLOCATOR_DEFAULT + allocator: Int = ImageDecoder.ALLOCATOR_DEFAULT, ): Bitmap? = traceSection("ImageLoader#loadBitmap") { return try { @@ -170,12 +168,11 @@ constructor( configureDecoderForMaximumSize(decoder, info.size, maxWidth, maxHeight) decoder.allocator = allocator } - } catch (e: IOException) { + } catch (e: Exception) { + // If we're loading an Uri, we can receive any exception from the other side. + // So we have to catch them all. Log.w(TAG, "Failed to load source $source", e) return null - } catch (e: DecodeException) { - Log.w(TAG, "Failed to decode source $source", e) - return null } } @@ -199,7 +196,7 @@ constructor( source: Source, @Px maxWidth: Int = DEFAULT_MAX_SAFE_BITMAP_SIZE_PX, @Px maxHeight: Int = DEFAULT_MAX_SAFE_BITMAP_SIZE_PX, - allocator: Int = ImageDecoder.ALLOCATOR_DEFAULT + allocator: Int = ImageDecoder.ALLOCATOR_DEFAULT, ): Drawable? = withContext(backgroundDispatcher) { loadDrawableSync(source, maxWidth, maxHeight, allocator) @@ -227,7 +224,7 @@ constructor( context: Context = defaultContext, @Px maxWidth: Int = DEFAULT_MAX_SAFE_BITMAP_SIZE_PX, @Px maxHeight: Int = DEFAULT_MAX_SAFE_BITMAP_SIZE_PX, - allocator: Int = ImageDecoder.ALLOCATOR_DEFAULT + allocator: Int = ImageDecoder.ALLOCATOR_DEFAULT, ): Drawable? = withContext(backgroundDispatcher) { loadDrawableSync(icon, context, maxWidth, maxHeight, allocator) @@ -254,7 +251,7 @@ constructor( source: Source, @Px maxWidth: Int = DEFAULT_MAX_SAFE_BITMAP_SIZE_PX, @Px maxHeight: Int = DEFAULT_MAX_SAFE_BITMAP_SIZE_PX, - allocator: Int = ImageDecoder.ALLOCATOR_DEFAULT + allocator: Int = ImageDecoder.ALLOCATOR_DEFAULT, ): Drawable? = traceSection("ImageLoader#loadDrawable") { return try { @@ -262,7 +259,7 @@ constructor( toImageDecoderSource(source, defaultContext), maxWidth, maxHeight, - allocator + allocator, ) ?: // If we have a resource, retry fallback using the "normal" Resource loading @@ -301,7 +298,7 @@ constructor( source: ImageDecoder.Source, @Px maxWidth: Int = DEFAULT_MAX_SAFE_BITMAP_SIZE_PX, @Px maxHeight: Int = DEFAULT_MAX_SAFE_BITMAP_SIZE_PX, - allocator: Int = ImageDecoder.ALLOCATOR_DEFAULT + allocator: Int = ImageDecoder.ALLOCATOR_DEFAULT, ): Drawable? = traceSection("ImageLoader#loadDrawable") { return try { @@ -309,12 +306,11 @@ constructor( configureDecoderForMaximumSize(decoder, info.size, maxWidth, maxHeight) decoder.allocator = allocator } - } catch (e: IOException) { + } catch (e: Exception) { + // If we're loading from an Uri, any exception can happen on the + // other side. We have to catch them all. Log.w(TAG, "Failed to load source $source", e) return null - } catch (e: DecodeException) { - Log.w(TAG, "Failed to decode source $source", e) - return null } } @@ -325,7 +321,7 @@ constructor( context: Context = defaultContext, @Px maxWidth: Int = DEFAULT_MAX_SAFE_BITMAP_SIZE_PX, @Px maxHeight: Int = DEFAULT_MAX_SAFE_BITMAP_SIZE_PX, - allocator: Int = ImageDecoder.ALLOCATOR_DEFAULT + allocator: Int = ImageDecoder.ALLOCATOR_DEFAULT, ): Drawable? = traceSection("ImageLoader#loadDrawable") { return when (icon.type) { @@ -341,7 +337,7 @@ constructor( ImageDecoder.createSource(it, icon.resId), maxWidth, maxHeight, - allocator + allocator, ) } // Fallback to non-ImageDecoder load if the attempt failed (e.g. the @@ -360,7 +356,7 @@ constructor( ImageDecoder.createSource(icon.dataBytes, icon.dataOffset, icon.dataLength), maxWidth, maxHeight, - allocator + allocator, ) } else -> { @@ -421,12 +417,10 @@ constructor( fun loadSizeSync(source: ImageDecoder.Source): Size? { return try { ImageDecoder.decodeHeader(source).size - } catch (e: IOException) { + } catch (e: Exception) { + // Any exception can happen when loading Uris, so we have to catch them all. Log.w(TAG, "Failed to load source $source", e) return null - } catch (e: DecodeException) { - Log.w(TAG, "Failed to decode source $source", e) - return null } } @@ -472,7 +466,7 @@ constructor( decoder: ImageDecoder, imgSize: Size, @Px maxWidth: Int, - @Px maxHeight: Int + @Px maxHeight: Int, ) { if (maxWidth == DO_NOT_RESIZE && maxHeight == DO_NOT_RESIZE) { return @@ -547,7 +541,7 @@ constructor( pm.getApplicationInfo( resPackage, PackageManager.MATCH_UNINSTALLED_PACKAGES or - PackageManager.GET_SHARED_LIBRARY_FILES + PackageManager.GET_SHARED_LIBRARY_FILES, ) if (ai != null) { return pm.getResourcesForApplication(ai) diff --git a/packages/SystemUI/tests/src/com/android/systemui/graphics/ImageLoaderContentProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/graphics/ImageLoaderContentProviderTest.kt new file mode 100644 index 000000000000..8d9fa6ad6e08 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/graphics/ImageLoaderContentProviderTest.kt @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.graphics + +import android.content.ContentProvider +import android.content.ContentValues +import android.content.Context +import android.database.Cursor +import android.net.Uri +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import androidx.test.rule.provider.ProviderTestRule +import com.android.systemui.SysuiTestCase +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.kosmos.testScope +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever + +const val AUTHORITY = "exception.provider.authority" +val TEST_URI = Uri.Builder().scheme("content").authority(AUTHORITY).path("path").build() + +@SmallTest +@kotlinx.coroutines.ExperimentalCoroutinesApi +@RunWith(AndroidJUnit4::class) +class ImageLoaderContentProviderTest : SysuiTestCase() { + + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + private val mockContext = mock<Context>() + private lateinit var imageLoader: ImageLoader + + @Rule + @JvmField + @Suppress("DEPRECATION") + public val providerTestRule = + ProviderTestRule.Builder(ExceptionThrowingContentProvider::class.java, AUTHORITY).build() + + @Before + fun setUp() { + whenever(mockContext.contentResolver).thenReturn(providerTestRule.resolver) + imageLoader = ImageLoader(mockContext, kosmos.testDispatcher) + } + + @Test(expected = IllegalArgumentException::class) + fun loadFromTestContentProvider_throwsException() { + // This checks if the resolution actually throws the exception from test provider. + mockContext.contentResolver.query(TEST_URI, null, null, null) + } + + @Test + fun loadFromRuntimeExceptionThrowingProvider_returnsNull() = + testScope.runTest { assertThat(imageLoader.loadBitmap(ImageLoader.Uri(TEST_URI))).isNull() } +} + +class ExceptionThrowingContentProvider : ContentProvider() { + override fun query( + uri: Uri, + projection: Array<out String>?, + selection: String?, + selectionArgs: Array<out String>?, + sortOrder: String?, + ): Cursor? { + throw IllegalArgumentException("Test exception") + } + + override fun getType(uri: Uri): String? { + throw IllegalArgumentException("Test exception") + } + + override fun insert(uri: Uri, values: ContentValues?): Uri? { + throw IllegalArgumentException("Test exception") + } + + override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?): Int { + throw IllegalArgumentException("Test exception") + } + + override fun update( + uri: Uri, + values: ContentValues?, + selection: String?, + selectionArgs: Array<out String>?, + ): Int { + throw IllegalArgumentException("Test exception") + } + + override fun onCreate(): Boolean = true +} |