Skip to content

Commit 65ff843

Browse files
authored
[PM-15057] Add utility for loading FIDO2 icons (#4371)
1 parent 26a7876 commit 65ff843

File tree

2 files changed

+207
-0
lines changed

2 files changed

+207
-0
lines changed
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package com.x8bit.bitwarden.ui.autofill.fido2.util
2+
3+
import android.content.Context
4+
import androidx.annotation.DrawableRes
5+
import androidx.core.graphics.drawable.IconCompat
6+
import com.bumptech.glide.Glide
7+
import com.x8bit.bitwarden.ui.platform.components.model.IconData
8+
import timber.log.Timber
9+
import java.util.concurrent.CancellationException
10+
import java.util.concurrent.ExecutionException
11+
12+
/**
13+
* Creates an IconCompat from an IconData, or falls back to a default resource if IconData is null
14+
* or is not of type Network.
15+
*
16+
* @param iconData The IconData to create the IconCompat from.
17+
* @param defaultResourceId The resource ID of the default icon to use if IconData is null or not of
18+
* type Network.
19+
* @return An IconCompat created from the IconData or the default resource.
20+
*/
21+
suspend fun Context.createFido2IconCompatFromIconDataOrDefault(
22+
iconData: IconData?,
23+
@DrawableRes defaultResourceId: Int,
24+
): IconCompat = if (iconData != null && iconData is IconData.Network) {
25+
createFido2IconCompatFromRemoteUriOrDefaultResource(
26+
uri = iconData.uri,
27+
defaultResourceId = defaultResourceId,
28+
)
29+
} else {
30+
createFido2IconCompatFromResource(defaultResourceId)
31+
}
32+
33+
/**
34+
* Creates an IconCompat from a drawable resource ID.
35+
*/
36+
fun Context.createFido2IconCompatFromResource(@DrawableRes resourceId: Int) =
37+
IconCompat.createWithResource(this, resourceId)
38+
39+
// futureTargetBitmap.get() is a blocking call so this function must be called from a coroutine.
40+
@Suppress("RedundantSuspendModifier")
41+
private suspend fun Context.createFido2IconCompatFromRemoteUriOrDefaultResource(
42+
uri: String,
43+
@DrawableRes defaultResourceId: Int,
44+
): IconCompat {
45+
val futureTargetBitmap = Glide
46+
.with(this)
47+
.asBitmap()
48+
.load(uri)
49+
.placeholder(defaultResourceId)
50+
.submit()
51+
return try {
52+
IconCompat.createWithBitmap(futureTargetBitmap.get())
53+
} catch (e: CancellationException) {
54+
Timber.e(e, "Cancellation exception while loading icon.")
55+
IconCompat.createWithResource(this, defaultResourceId)
56+
} catch (e: ExecutionException) {
57+
Timber.e(e, "Execution exception while loading icon.")
58+
IconCompat.createWithResource(this, defaultResourceId)
59+
} catch (e: InterruptedException) {
60+
Timber.e(e, "Interrupted while loading icon.")
61+
IconCompat.createWithResource(this, defaultResourceId)
62+
}
63+
}
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
package com.x8bit.bitwarden.ui.autofill.fido2.util
2+
3+
import android.content.Context
4+
import android.graphics.Bitmap
5+
import android.graphics.drawable.Icon
6+
import android.net.Uri
7+
import androidx.core.graphics.drawable.IconCompat
8+
import com.bumptech.glide.Glide
9+
import com.x8bit.bitwarden.ui.platform.components.model.IconData
10+
import io.mockk.every
11+
import io.mockk.mockk
12+
import io.mockk.mockkStatic
13+
import io.mockk.unmockkStatic
14+
import io.mockk.verify
15+
import kotlinx.coroutines.test.runTest
16+
import org.junit.jupiter.api.AfterEach
17+
import org.junit.jupiter.api.BeforeEach
18+
import org.junit.jupiter.api.Test
19+
import java.util.concurrent.CancellationException
20+
import java.util.concurrent.ExecutionException
21+
22+
class ContextExtensionsTest {
23+
24+
val mockContext = mockk<Context>()
25+
26+
@BeforeEach
27+
fun setUp() {
28+
mockkStatic(
29+
Glide::class,
30+
Icon::class,
31+
IconCompat::class,
32+
)
33+
}
34+
35+
@AfterEach
36+
fun tearDown() {
37+
unmockkStatic(
38+
Glide::class,
39+
Icon::class,
40+
IconCompat::class,
41+
Uri::class,
42+
)
43+
}
44+
45+
@Suppress("MaxLineLength")
46+
@Test
47+
fun `createFido2IconCompatFromIconDataOrDefault should return default icon when IconData is null`() =
48+
runTest {
49+
every { IconCompat.createWithResource(mockContext, 0) } returns IconCompat()
50+
mockContext.createFido2IconCompatFromIconDataOrDefault(
51+
iconData = null,
52+
defaultResourceId = 0,
53+
)
54+
verify { IconCompat.createWithResource(mockContext, 0) }
55+
}
56+
57+
@Suppress("MaxLineLength")
58+
@Test
59+
fun `createFido2IconCompatFromIconDataOrDefault should return default icon when IconData is not Network type`() =
60+
runTest {
61+
every { IconCompat.createWithResource(mockContext, 0) } returns IconCompat()
62+
mockContext.createFido2IconCompatFromIconDataOrDefault(
63+
iconData = IconData.Local(0),
64+
defaultResourceId = 0,
65+
)
66+
verify { IconCompat.createWithResource(mockContext, 0) }
67+
}
68+
69+
@Suppress("MaxLineLength")
70+
@Test
71+
fun `createFido2IconCompatFromIconDataOrDefault should load icon from URI when IconData is Network type`() =
72+
runTest {
73+
val mockBitmap = mockk<Bitmap>()
74+
setupMockGlide(mockBitmap)
75+
every { IconCompat.createWithBitmap(mockBitmap) } returns mockk()
76+
mockContext.createFido2IconCompatFromIconDataOrDefault(
77+
iconData = IconData.Network("https://www.mockuri.com", 0),
78+
defaultResourceId = 0,
79+
)
80+
verify { IconCompat.createWithBitmap(mockBitmap) }
81+
}
82+
83+
@Suppress("MaxLineLength")
84+
@Test
85+
fun `createFido2IconCompatFromIconDataOrDefault should return default icon when loading from URI throws CancellationException`() =
86+
runTest {
87+
val mockBitmap = mockk<Bitmap>()
88+
setupMockGlide(mockBitmap)
89+
every { IconCompat.createWithResource(mockContext, 0) } returns mockk()
90+
every { IconCompat.createWithBitmap(mockBitmap) } throws CancellationException()
91+
mockContext.createFido2IconCompatFromIconDataOrDefault(
92+
iconData = IconData.Network("https://www.mockuri.com", 0),
93+
defaultResourceId = 0,
94+
)
95+
verify { IconCompat.createWithResource(mockContext, 0) }
96+
}
97+
98+
@Suppress("MaxLineLength")
99+
@Test
100+
fun `createFido2IconCompatFromIconDataOrDefault should return default icon when loading from URI throws ExecutionException`() =
101+
runTest {
102+
val mockBitmap = mockk<Bitmap>()
103+
setupMockGlide(mockBitmap)
104+
every { IconCompat.createWithResource(mockContext, 0) } returns mockk()
105+
every {
106+
IconCompat.createWithBitmap(mockBitmap)
107+
} throws ExecutionException("message", Throwable())
108+
109+
mockContext.createFido2IconCompatFromIconDataOrDefault(
110+
iconData = IconData.Network("https://www.mockuri.com", 0),
111+
defaultResourceId = 0,
112+
)
113+
verify { IconCompat.createWithResource(mockContext, 0) }
114+
}
115+
116+
@Suppress("MaxLineLength")
117+
@Test
118+
fun `createFido2IconCompatFromIconDataOrDefault should return default icon when loading from URI throws InterruptedException`() =
119+
runTest {
120+
val mockBitmap = mockk<Bitmap>()
121+
setupMockGlide(mockBitmap)
122+
every { IconCompat.createWithResource(mockContext, 0) } returns mockk()
123+
every { IconCompat.createWithBitmap(mockBitmap) } throws InterruptedException()
124+
mockContext.createFido2IconCompatFromIconDataOrDefault(
125+
iconData = IconData.Network("https://www.mockuri.com", 0),
126+
defaultResourceId = 0,
127+
)
128+
verify { IconCompat.createWithResource(mockContext, 0) }
129+
}
130+
131+
private fun setupMockGlide(mockBitmap: Bitmap) {
132+
every { Glide.with(mockContext) } returns mockk {
133+
every { asBitmap() } returns mockk {
134+
every { load(any<String>()) } returns mockk {
135+
every { placeholder(0) } returns mockk {
136+
every { submit() } returns mockk {
137+
every { get() } returns mockBitmap
138+
}
139+
}
140+
}
141+
}
142+
}
143+
}
144+
}

0 commit comments

Comments
 (0)