diff --git a/README.md b/README.md index d488e2a42c..4b531d958b 100644 --- a/README.md +++ b/README.md @@ -168,16 +168,16 @@ The following is a list of all third-party dependencies included as part of the - Purpose: The WorkManager is used to schedule deferrable, asynchronous tasks that must be run reliably. - License: Apache 2.0 +- **Coil** + - https://github.com/coil-kt/coil + - Purpose: Image loading and caching. + - License: Apache 2.0 + - **Dagger Hilt** - https://github.com/google/dagger - Purpose: Dependency injection framework. - License: Apache 2.0 -- **Glide** - - https://github.com/bumptech/glide - - Purpose: Image loading and caching. - - License: BSD, part MIT and Apache 2.0 - - **kotlinx.collections.immutable** - https://github.com/Kotlin/kotlinx.collections.immutable - Purpose: Immutable collection interfaces and implementation prototypes for Kotlin. diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 9364dbc4b5..682e66af91 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -275,9 +275,8 @@ dependencies { implementation(libs.androidx.splashscreen) implementation(libs.androidx.work.runtime.ktx) implementation(libs.bitwarden.sdk) - implementation(libs.bumptech.glide) - implementation(libs.bumptech.glide.okhttp) - ksp(libs.bumptech.glide.compiler) + implementation(libs.coil.coil) + implementation(libs.coil.coil.network.okhttp) implementation(libs.google.hilt.android) ksp(libs.google.hilt.compiler) implementation(libs.kotlinx.collections.immutable) diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index d4963ef3c0..5935936883 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -55,13 +55,6 @@ kotlinx.serialization.KSerializer serializer(...); } -################################################################################ -# Glide -################################################################################ - --keep public class * implements com.bumptech.glide.module.GlideModule --keep public class * extends com.bumptech.glide.module.AppGlideModule - ################################################################################ # JNA ################################################################################ diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/BitwardenApplication.kt b/app/src/main/kotlin/com/x8bit/bitwarden/BitwardenApplication.kt index 6ce37bdf52..6e6bd75728 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/BitwardenApplication.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/BitwardenApplication.kt @@ -1,6 +1,9 @@ package com.x8bit.bitwarden import android.app.Application +import coil3.ImageLoader +import coil3.PlatformContext +import coil3.SingletonImageLoader import com.bitwarden.annotation.OmitFromCoverage import com.bitwarden.data.manager.flightrecorder.FlightRecorderManager import com.x8bit.bitwarden.data.auth.manager.AuthRequestNotificationManager @@ -11,6 +14,7 @@ import com.x8bit.bitwarden.data.platform.manager.event.OrganizationEventManager import com.x8bit.bitwarden.data.platform.manager.network.NetworkConfigManager import com.x8bit.bitwarden.data.platform.manager.restriction.RestrictionManager import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository +import dagger.Lazy import dagger.hilt.android.HiltAndroidApp import timber.log.Timber import javax.inject.Inject @@ -20,7 +24,7 @@ import javax.inject.Inject */ @OmitFromCoverage @HiltAndroidApp -class BitwardenApplication : Application() { +class BitwardenApplication : Application(), SingletonImageLoader.Factory { // Inject classes here that must be triggered on startup but are not otherwise consumed by // other callers. @Inject @@ -50,6 +54,9 @@ class BitwardenApplication : Application() { @Inject lateinit var environmentRepository: EnvironmentRepository + @Inject + lateinit var imageLoader: Lazy + override fun onCreate() { super.onCreate() // These must be initialized in order to ensure that the restrictionManager does not @@ -62,4 +69,6 @@ class BitwardenApplication : Application() { super.onLowMemory() Timber.w("onLowMemory") } + + override fun newImageLoader(context: PlatformContext): ImageLoader = imageLoader.get() } diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/credentials/util/ContextExtensions.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/credentials/util/ContextExtensions.kt deleted file mode 100644 index 304f3c6c38..0000000000 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/credentials/util/ContextExtensions.kt +++ /dev/null @@ -1,63 +0,0 @@ -package com.x8bit.bitwarden.ui.credentials.util - -import android.content.Context -import androidx.annotation.DrawableRes -import androidx.core.graphics.drawable.IconCompat -import com.bitwarden.ui.platform.components.icon.model.IconData -import com.bumptech.glide.Glide -import timber.log.Timber -import java.util.concurrent.CancellationException -import java.util.concurrent.ExecutionException - -/** - * Creates an IconCompat from an IconData, or falls back to a default resource if IconData is null - * or is not of type Network. - * - * @param iconData The IconData to create the IconCompat from. - * @param defaultResourceId The resource ID of the default icon to use if IconData is null or not of - * type Network. - * @return An IconCompat created from the IconData or the default resource. - */ -suspend fun Context.createFido2IconCompatFromIconDataOrDefault( - iconData: IconData?, - @DrawableRes defaultResourceId: Int, -): IconCompat = if (iconData != null && iconData is IconData.Network) { - createFido2IconCompatFromRemoteUriOrDefaultResource( - uri = iconData.uri, - defaultResourceId = defaultResourceId, - ) -} else { - createFido2IconCompatFromResource(defaultResourceId) -} - -/** - * Creates an IconCompat from a drawable resource ID. - */ -fun Context.createFido2IconCompatFromResource(@DrawableRes resourceId: Int) = - IconCompat.createWithResource(this, resourceId) - -// futureTargetBitmap.get() is a blocking call so this function must be called from a coroutine. -@Suppress("RedundantSuspendModifier") -private suspend fun Context.createFido2IconCompatFromRemoteUriOrDefaultResource( - uri: String, - @DrawableRes defaultResourceId: Int, -): IconCompat { - val futureTargetBitmap = Glide - .with(this) - .asBitmap() - .load(uri) - .placeholder(defaultResourceId) - .submit() - return try { - IconCompat.createWithBitmap(futureTargetBitmap.get()) - } catch (e: CancellationException) { - Timber.e(e, "Cancellation exception while loading icon.") - IconCompat.createWithResource(this, defaultResourceId) - } catch (e: ExecutionException) { - Timber.e(e, "Execution exception while loading icon.") - IconCompat.createWithResource(this, defaultResourceId) - } catch (e: InterruptedException) { - Timber.e(e, "Interrupted while loading icon.") - IconCompat.createWithResource(this, defaultResourceId) - } -} diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/glide/BitwardenAppGlideModule.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/glide/BitwardenAppGlideModule.kt deleted file mode 100644 index 9ca5654b41..0000000000 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/glide/BitwardenAppGlideModule.kt +++ /dev/null @@ -1,75 +0,0 @@ -package com.x8bit.bitwarden.ui.platform.glide - -import android.content.Context -import com.bitwarden.annotation.OmitFromCoverage -import com.bitwarden.network.ssl.createMtlsOkHttpClient -import com.bumptech.glide.Glide -import com.bumptech.glide.Registry -import com.bumptech.glide.annotation.GlideModule -import com.bumptech.glide.integration.okhttp3.OkHttpUrlLoader -import com.bumptech.glide.load.model.GlideUrl -import com.bumptech.glide.module.AppGlideModule -import com.x8bit.bitwarden.data.platform.manager.CertificateManager -import com.x8bit.bitwarden.data.platform.manager.network.NetworkCookieManager -import dagger.hilt.EntryPoint -import dagger.hilt.InstallIn -import dagger.hilt.android.EntryPointAccessors -import dagger.hilt.components.SingletonComponent -import java.io.InputStream - -/** - * Custom Glide module for the Bitwarden app that configures Glide to use an OkHttpClient - * with mTLS (mutual TLS) support. - * - * This ensures that all icon/image loading requests through Glide present the client certificate - * for mutual TLS authentication, allowing them to pass through Cloudflare's mTLS checks. - * - * The configuration mirrors the SSL setup used in RetrofitsImpl for API calls. - */ -@OmitFromCoverage -@GlideModule -class BitwardenAppGlideModule : AppGlideModule() { - - /** - * Entry point to access Hilt-provided dependencies from non-Hilt managed classes. - */ - @EntryPoint - @InstallIn(SingletonComponent::class) - interface BitwardenGlideEntryPoint { - /** - * Provides access to the [CertificateManager] for mTLS certificate management. - */ - fun certificateManager(): CertificateManager - - /** - * Provides access to the [NetworkCookieManager] for cookie-based authentication. - */ - fun networkCookieManager(): NetworkCookieManager - } - - override fun registerComponents(context: Context, glide: Glide, registry: Registry) { - // Get CertificateManager from Hilt - val entryPoint = EntryPointAccessors.fromApplication( - context = context.applicationContext, - entryPoint = BitwardenGlideEntryPoint::class.java, - ) - val certificateManager = entryPoint.certificateManager() - val networkCookieManager = entryPoint.networkCookieManager() - - // Build OkHttpClient with mTLS and cookie support - val client = certificateManager - .createMtlsOkHttpClient() - .newBuilder() - .addNetworkInterceptor(GlideCookieInterceptor(networkCookieManager)) - .build() - - // Register OkHttpUrlLoader that uses our mTLS + cookie OkHttpClient - registry.replace( - GlideUrl::class.java, - InputStream::class.java, - OkHttpUrlLoader.Factory(client), - ) - } - - override fun isManifestParsingEnabled(): Boolean = false -} diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/glide/GlideCookieInterceptor.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/image/ImageCookieInterceptor.kt similarity index 83% rename from app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/glide/GlideCookieInterceptor.kt rename to app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/image/ImageCookieInterceptor.kt index bbe0b8d163..855942527f 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/glide/GlideCookieInterceptor.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/image/ImageCookieInterceptor.kt @@ -1,4 +1,4 @@ -package com.x8bit.bitwarden.ui.platform.glide +package com.x8bit.bitwarden.ui.platform.image import com.bitwarden.network.exception.CookieRedirectException import com.bitwarden.network.provider.CookieProvider @@ -9,16 +9,16 @@ private const val HEADER_COOKIE = "Cookie" private const val HTTP_302 = 302 /** - * Interceptor that attaches cookies to Glide image requests for enterprise environments - * requiring cookie-based authentication. + * Interceptor that attaches cookies to image requests for enterprise environments requiring + * cookie-based authentication. * * Unlike [com.bitwarden.network.interceptor.CookieInterceptor], this interceptor does not * trigger cookie acquisition. It throws [CookieRedirectException] on HTTP 302 responses - * to prevent Glide from following redirects and caching invalid content. + * to prevent following redirects and caching invalid content. * * @property cookieProvider Provider for retrieving cookies by hostname. */ -class GlideCookieInterceptor( +class ImageCookieInterceptor( private val cookieProvider: CookieProvider, ) : Interceptor { diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/image/di/ImageModule.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/image/di/ImageModule.kt new file mode 100644 index 0000000000..32cec2cd1c --- /dev/null +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/image/di/ImageModule.kt @@ -0,0 +1,55 @@ +package com.x8bit.bitwarden.ui.platform.image.di + +import android.content.Context +import coil3.ImageLoader +import coil3.network.okhttp.OkHttpNetworkFetcherFactory +import com.bitwarden.network.ssl.createMtlsOkHttpClient +import com.x8bit.bitwarden.data.platform.manager.CertificateManager +import com.x8bit.bitwarden.data.platform.manager.network.NetworkCookieManager +import com.x8bit.bitwarden.ui.platform.image.ImageCookieInterceptor +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +/** + * Provides dependencies for loading images. + */ +@Module +@InstallIn(SingletonComponent::class) +object ImageModule { + /** + * Provides the [ImageLoader] used by Coil, configured to use an OkHttpClient with (mutual TLS) + * and cookie support. + * + * This ensures that all icon/image loading requests through Coil present the client + * certificate for mutual TLS authentication, allowing them to pass through Cloudflare's mTLS + * checks. + * + * The configuration mirrors the SSL setup used for non-image API calls. + */ + @Provides + @Singleton + fun provideImageLoader( + @ApplicationContext context: Context, + certificateManager: CertificateManager, + networkCookieManager: NetworkCookieManager, + ): ImageLoader = ImageLoader + .Builder(context = context) + .components { + this.add( + OkHttpNetworkFetcherFactory( + callFactory = { + certificateManager + .createMtlsOkHttpClient() + .newBuilder() + .addNetworkInterceptor(ImageCookieInterceptor(networkCookieManager)) + .build() + }, + ), + ) + } + .build() +} diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/credentials/util/ContextExtensionsTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/credentials/util/ContextExtensionsTest.kt deleted file mode 100644 index 1cbee345e4..0000000000 --- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/credentials/util/ContextExtensionsTest.kt +++ /dev/null @@ -1,144 +0,0 @@ -package com.x8bit.bitwarden.ui.credentials.util - -import android.content.Context -import android.graphics.Bitmap -import android.graphics.drawable.Icon -import android.net.Uri -import androidx.core.graphics.drawable.IconCompat -import com.bitwarden.ui.platform.components.icon.model.IconData -import com.bumptech.glide.Glide -import io.mockk.every -import io.mockk.mockk -import io.mockk.mockkStatic -import io.mockk.unmockkStatic -import io.mockk.verify -import kotlinx.coroutines.test.runTest -import org.junit.jupiter.api.AfterEach -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import java.util.concurrent.CancellationException -import java.util.concurrent.ExecutionException - -class ContextExtensionsTest { - - val mockContext = mockk() - - @BeforeEach - fun setUp() { - mockkStatic( - Glide::class, - Icon::class, - IconCompat::class, - ) - } - - @AfterEach - fun tearDown() { - unmockkStatic( - Glide::class, - Icon::class, - IconCompat::class, - Uri::class, - ) - } - - @Suppress("MaxLineLength") - @Test - fun `createFido2IconCompatFromIconDataOrDefault should return default icon when IconData is null`() = - runTest { - every { IconCompat.createWithResource(mockContext, 0) } returns IconCompat() - mockContext.createFido2IconCompatFromIconDataOrDefault( - iconData = null, - defaultResourceId = 0, - ) - verify { IconCompat.createWithResource(mockContext, 0) } - } - - @Suppress("MaxLineLength") - @Test - fun `createFido2IconCompatFromIconDataOrDefault should return default icon when IconData is not Network type`() = - runTest { - every { IconCompat.createWithResource(mockContext, 0) } returns IconCompat() - mockContext.createFido2IconCompatFromIconDataOrDefault( - iconData = IconData.Local(0), - defaultResourceId = 0, - ) - verify { IconCompat.createWithResource(mockContext, 0) } - } - - @Suppress("MaxLineLength") - @Test - fun `createFido2IconCompatFromIconDataOrDefault should load icon from URI when IconData is Network type`() = - runTest { - val mockBitmap = mockk() - setupMockGlide(mockBitmap) - every { IconCompat.createWithBitmap(mockBitmap) } returns mockk() - mockContext.createFido2IconCompatFromIconDataOrDefault( - iconData = IconData.Network("https://www.mockuri.com", 0), - defaultResourceId = 0, - ) - verify { IconCompat.createWithBitmap(mockBitmap) } - } - - @Suppress("MaxLineLength") - @Test - fun `createFido2IconCompatFromIconDataOrDefault should return default icon when loading from URI throws CancellationException`() = - runTest { - val mockBitmap = mockk() - setupMockGlide(mockBitmap) - every { IconCompat.createWithResource(mockContext, 0) } returns mockk() - every { IconCompat.createWithBitmap(mockBitmap) } throws CancellationException() - mockContext.createFido2IconCompatFromIconDataOrDefault( - iconData = IconData.Network("https://www.mockuri.com", 0), - defaultResourceId = 0, - ) - verify { IconCompat.createWithResource(mockContext, 0) } - } - - @Suppress("MaxLineLength") - @Test - fun `createFido2IconCompatFromIconDataOrDefault should return default icon when loading from URI throws ExecutionException`() = - runTest { - val mockBitmap = mockk() - setupMockGlide(mockBitmap) - every { IconCompat.createWithResource(mockContext, 0) } returns mockk() - every { - IconCompat.createWithBitmap(mockBitmap) - } throws ExecutionException("message", Throwable()) - - mockContext.createFido2IconCompatFromIconDataOrDefault( - iconData = IconData.Network("https://www.mockuri.com", 0), - defaultResourceId = 0, - ) - verify { IconCompat.createWithResource(mockContext, 0) } - } - - @Suppress("MaxLineLength") - @Test - fun `createFido2IconCompatFromIconDataOrDefault should return default icon when loading from URI throws InterruptedException`() = - runTest { - val mockBitmap = mockk() - setupMockGlide(mockBitmap) - every { IconCompat.createWithResource(mockContext, 0) } returns mockk() - every { IconCompat.createWithBitmap(mockBitmap) } throws InterruptedException() - mockContext.createFido2IconCompatFromIconDataOrDefault( - iconData = IconData.Network("https://www.mockuri.com", 0), - defaultResourceId = 0, - ) - verify { IconCompat.createWithResource(mockContext, 0) } - } - - private fun setupMockGlide(mockBitmap: Bitmap) { - every { Glide.with(mockContext) } returns mockk { - every { asBitmap() } returns mockk { - every { load(any()) } returns mockk { - every { placeholder(0) } returns mockk { - every { submit() } returns mockk { - every { get() } returns mockBitmap - } - } - } - } - } - } -} diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/glide/BitwardenAppGlideModuleTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/glide/BitwardenAppGlideModuleTest.kt deleted file mode 100644 index b8828f4520..0000000000 --- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/glide/BitwardenAppGlideModuleTest.kt +++ /dev/null @@ -1,60 +0,0 @@ -package com.x8bit.bitwarden.ui.platform.glide - -import org.junit.jupiter.api.Assertions.assertNotNull -import org.junit.jupiter.api.Assertions.assertTrue -import org.junit.jupiter.api.Test - -/** - * Test class for [BitwardenAppGlideModule] to verify mTLS configuration is properly applied - * to Glide without requiring a real mTLS server. - * - * These tests verify the module's structure and that it can be instantiated. - * Full integration testing requires running the app and checking logcat for - * "BitwardenGlide" logs when images are loaded. - */ -class BitwardenAppGlideModuleTest { - - @Test - fun `BitwardenAppGlideModule should be instantiable`() { - // Verify the module can be created - val module = BitwardenAppGlideModule() - - assertNotNull(module) - } - - @Test - fun `BitwardenAppGlideModule should have EntryPoint interface for Hilt dependency injection`() { - // Verify the Hilt EntryPoint interface exists for accessing CertificateManager - val entryPointInterface = BitwardenAppGlideModule::class.java - .declaredClasses - .firstOrNull { it.simpleName == "BitwardenGlideEntryPoint" } - - assertNotNull(entryPointInterface) - } - - @Test - fun `BitwardenGlideEntryPoint should declare certificateManager method`() { - // Verify the EntryPoint has the required method to access CertificateManager - val entryPointInterface = BitwardenAppGlideModule::class.java - .declaredClasses - .firstOrNull { it.simpleName == "BitwardenGlideEntryPoint" } - - val methods = requireNotNull(entryPointInterface).declaredMethods - val hasCertificateManagerMethod = methods.any { it.name == "certificateManager" } - - assertTrue(hasCertificateManagerMethod) - } - - @Test - fun `BitwardenGlideEntryPoint should declare networkCookieManager method`() { - val entryPointInterface = BitwardenAppGlideModule::class.java - .declaredClasses - .firstOrNull { it.simpleName == "BitwardenGlideEntryPoint" } - - val methods = requireNotNull(entryPointInterface).declaredMethods - val hasNetworkCookieManagerMethod = - methods.any { it.name == "networkCookieManager" } - - assertTrue(hasNetworkCookieManagerMethod) - } -} diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/glide/GlideCookieInterceptorTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/image/ImageCookieInterceptorTest.kt similarity index 97% rename from app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/glide/GlideCookieInterceptorTest.kt rename to app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/image/ImageCookieInterceptorTest.kt index 6c2767bfcc..a84402ed4e 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/glide/GlideCookieInterceptorTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/image/ImageCookieInterceptorTest.kt @@ -1,4 +1,4 @@ -package com.x8bit.bitwarden.ui.platform.glide +package com.x8bit.bitwarden.ui.platform.image import com.bitwarden.network.exception.CookieRedirectException import com.bitwarden.network.interceptor.FakeInterceptorChain @@ -15,11 +15,11 @@ import org.junit.jupiter.api.Assertions.assertNull import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows -class GlideCookieInterceptorTest { +class ImageCookieInterceptorTest { private val mockCookieProvider: CookieProvider = mockk() - private val interceptor = GlideCookieInterceptor( + private val interceptor = ImageCookieInterceptor( cookieProvider = mockCookieProvider, ) diff --git a/authenticator/build.gradle.kts b/authenticator/build.gradle.kts index 27a586467d..d54b6b9400 100644 --- a/authenticator/build.gradle.kts +++ b/authenticator/build.gradle.kts @@ -227,7 +227,6 @@ dependencies { implementation(libs.androidx.splashscreen) implementation(libs.androidx.work.runtime.ktx) implementation(libs.bitwarden.sdk) - implementation(libs.bumptech.glide) implementation(platform(libs.google.firebase.bom)) implementation(libs.google.firebase.cloud.messaging) implementation(libs.google.firebase.crashlytics) diff --git a/authenticator/proguard-rules.pro b/authenticator/proguard-rules.pro index 73aed837ee..bf1c020260 100644 --- a/authenticator/proguard-rules.pro +++ b/authenticator/proguard-rules.pro @@ -46,13 +46,6 @@ kotlinx.serialization.KSerializer serializer(...); } -################################################################################ -# Glide -################################################################################ - --keep public class * implements com.bumptech.glide.module.GlideModule --keep public class * extends com.bumptech.glide.module.AppGlideModule - ################################################################################ # Google Protobuf generated files ################################################################################ diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 908fb8b615..8dfec98215 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -30,11 +30,10 @@ androidxSecurityCrypto = "1.1.0" androidxSplash = "1.2.0" androidxWork = "2.12.0" bitwardenSdk = "4.0.0-9318-3b2c8fc3" +coil = "3.6.3" crashlytics = "3.0.8" detekt = "1.23.8" firebaseBom = "35.0.0" -glide = "5.0.9" -glideCompose = "1.0.0-beta01" googleBilling = "9.1.0" googleMlkitTextRecognition = "16.0.1" googleGuava = "33.7.2-jre" @@ -99,9 +98,10 @@ androidx-security-crypto = { module = "androidx.security:security-crypto", versi androidx-splashscreen = { module = "androidx.core:core-splashscreen", version.ref = "androidxSplash" } androidx-work-runtime-ktx = { module = "androidx.work:work-runtime-ktx", version.ref = "androidxWork" } bitwarden-sdk = { module = "com.bitwarden:sdk-android", version.ref = "bitwardenSdk" } -bumptech-glide = { module = "com.github.bumptech.glide:compose", version.ref = "glideCompose" } -bumptech-glide-okhttp = { module = "com.github.bumptech.glide:okhttp3-integration", version.ref = "glide" } -bumptech-glide-compiler = { module = "com.github.bumptech.glide:ksp", version.ref = "glide" } +coil-coil = { module = "io.coil-kt.coil3:coil", version.ref = "coil" } +coil-coil-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coil" } +coil-coil-gif = { module = "io.coil-kt.coil3:coil-gif", version.ref = "coil" } +coil-coil-network-okhttp = { module = "io.coil-kt.coil3:coil-network-okhttp", version.ref = "coil" } detekt-detekt-formatting = { module = "io.gitlab.arturbosch.detekt:detekt-formatting", version.ref = "detekt" } detekt-detekt-rules = { module = "io.gitlab.arturbosch.detekt:detekt-rules-libraries", version.ref = "detekt" } google-billing = { module = "com.android.billingclient:billing", version.ref = "googleBilling" } diff --git a/ui/build.gradle.kts b/ui/build.gradle.kts index 09a4745640..5d819095bb 100644 --- a/ui/build.gradle.kts +++ b/ui/build.gradle.kts @@ -75,7 +75,8 @@ dependencies { implementation(libs.androidx.core.ktx) implementation(libs.androidx.credentials) implementation(libs.androidx.navigation.compose) - implementation(libs.bumptech.glide) + implementation(libs.coil.coil.compose) + implementation(libs.coil.coil.gif) implementation(libs.kotlinx.serialization) implementation(libs.kotlinx.coroutines.core) implementation(libs.kotlinx.collections.immutable) diff --git a/ui/src/main/kotlin/com/bitwarden/ui/platform/components/icon/BitwardenIcon.kt b/ui/src/main/kotlin/com/bitwarden/ui/platform/components/icon/BitwardenIcon.kt index 237d7295fc..7a74bf2032 100644 --- a/ui/src/main/kotlin/com/bitwarden/ui/platform/components/icon/BitwardenIcon.kt +++ b/ui/src/main/kotlin/com/bitwarden/ui/platform/components/icon/BitwardenIcon.kt @@ -4,21 +4,19 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import coil3.compose.AsyncImage import com.bitwarden.ui.platform.base.util.nullableTestTag import com.bitwarden.ui.platform.components.icon.model.IconData import com.bitwarden.ui.platform.components.util.rememberVectorPainter -import com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi -import com.bumptech.glide.integration.compose.GlideImage -import com.bumptech.glide.integration.compose.placeholder /** - * Represents a Bitwarden icon that is either locally loaded or loaded using glide. + * Represents a Bitwarden icon that is either locally loaded or loaded using Coil. * * @param iconData Label for the text field. * @param modifier A [Modifier] for the composable. * @param tint the color to be applied as the tint for the icon. */ -@OptIn(ExperimentalGlideComposeApi::class) @Composable fun BitwardenIcon( iconData: IconData, @@ -27,14 +25,14 @@ fun BitwardenIcon( ) { when (iconData) { is IconData.Network -> { - GlideImage( + val fallbackPainter = painterResource(id = iconData.fallbackIconRes) + AsyncImage( model = iconData.uri, - failure = placeholder(iconData.fallbackIconRes), contentDescription = iconData.contentDescription?.invoke(), + placeholder = fallbackPainter, + error = fallbackPainter, modifier = modifier.nullableTestTag(tag = iconData.testTag), - ) { - it.placeholder(iconData.fallbackIconRes) - } + ) } is IconData.Local -> { diff --git a/ui/src/main/kotlin/com/bitwarden/ui/platform/components/image/BitwardenGifImage.kt b/ui/src/main/kotlin/com/bitwarden/ui/platform/components/image/BitwardenGifImage.kt index 05a9b61d38..bc8e185f57 100644 --- a/ui/src/main/kotlin/com/bitwarden/ui/platform/components/image/BitwardenGifImage.kt +++ b/ui/src/main/kotlin/com/bitwarden/ui/platform/components/image/BitwardenGifImage.kt @@ -1,32 +1,25 @@ package com.bitwarden.ui.platform.components.image import androidx.annotation.DrawableRes -import androidx.appcompat.content.res.AppCompatResources import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalContext -import com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi -import com.bumptech.glide.integration.compose.GlideImage +import coil3.compose.AsyncImage /** * A composable that displays a gif image. * * The content will also be scaled to fit the image to the container. */ -@OptIn(ExperimentalGlideComposeApi::class) @Composable fun BitwardenGifImage( @DrawableRes resId: Int, modifier: Modifier = Modifier, contentDescription: String? = null, ) { - // The need for this workaround of loading into a drawable first is related to issue reported - // [here](https://github.com/bumptech/glide/issues/3751) - val context = LocalContext.current - val drawable = AppCompatResources.getDrawable(context, resId) - GlideImage( - model = drawable, + // Coil automatically registers the `coil-gif` decoder, which handles the animation. + AsyncImage( + model = resId, contentDescription = contentDescription, contentScale = ContentScale.Fit, modifier = modifier,