summaryrefslogtreecommitdiffhomepage
path: root/android/app/src
diff options
context:
space:
mode:
authorJonatan Rhodin <jonatan.rhodin@mullvad.net>2023-05-29 13:20:32 +0200
committerJonatan Rhodin <jonatan.rhodin@mullvad.net>2023-06-09 10:31:21 +0200
commitf220345dc31a298e4734b985d785cfdd24f03bea (patch)
tree5a61991627ade27df670c5932941a5cc50d57b65 /android/app/src
parenta298585757ad1c8e6c65cb405db9ce9a31dd0b33 (diff)
downloadmullvadvpn-f220345dc31a298e4734b985d785cfdd24f03bea.tar.xz
mullvadvpn-f220345dc31a298e4734b985d785cfdd24f03bea.zip
Replace ApplicationImageView with compose implementation
Diffstat (limited to 'android/app/src')
-rw-r--r--android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/SplitTunnelingScreenTest.kt4
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/applist/ApplicationsIconManager.kt9
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/SplitTunnelingCell.kt34
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/SplitTunnelingScreen.kt13
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/fragment/SplitTunnelingFragment.kt8
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/widget/ApplicationImageView.kt69
-rw-r--r--android/app/src/test/kotlin/net/mullvad/mullvadvpn/applist/ApplicationsIconManagerTest.kt32
7 files changed, 71 insertions, 98 deletions
diff --git a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/SplitTunnelingScreenTest.kt b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/SplitTunnelingScreenTest.kt
index dc011abd05..8b2f2394c0 100644
--- a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/SplitTunnelingScreenTest.kt
+++ b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/SplitTunnelingScreenTest.kt
@@ -1,5 +1,6 @@
package net.mullvad.mullvadvpn.compose.screen
+import android.graphics.Bitmap
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
@@ -22,10 +23,11 @@ import org.koin.dsl.module
class SplitTunnelingScreenTest {
@get:Rule val composeTestRule = createComposeRule()
+ private val mockBitmap: Bitmap = Bitmap.createBitmap(10, 10, Bitmap.Config.ARGB_8888)
private val testModule = module {
single {
mockk<ApplicationsIconManager>().apply {
- every { getAppIcon(any()) } returns mockk(relaxed = true)
+ every { getAppIcon(any()) } returns mockBitmap
}
}
}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/applist/ApplicationsIconManager.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/applist/ApplicationsIconManager.kt
index ebfbc1f379..e1ff07022c 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/applist/ApplicationsIconManager.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/applist/ApplicationsIconManager.kt
@@ -1,22 +1,23 @@
package net.mullvad.mullvadvpn.applist
import android.content.pm.PackageManager
-import android.graphics.drawable.Drawable
+import android.graphics.Bitmap
import android.os.Looper
import androidx.annotation.WorkerThread
import androidx.collection.LruCache
+import androidx.core.graphics.drawable.toBitmap
class ApplicationsIconManager(private val packageManager: PackageManager) {
- private val iconsCache = LruCache<String, Drawable>(500)
+ private val iconsCache = LruCache<String, Bitmap>(500)
@WorkerThread
@Throws(PackageManager.NameNotFoundException::class)
- fun getAppIcon(packageName: String): Drawable {
+ fun getAppIcon(packageName: String): Bitmap {
check(!Looper.getMainLooper().isCurrentThread) { "Should not be called from MainThread" }
iconsCache.get(packageName)?.let {
return it
}
- return packageManager.getApplicationIcon(packageName).also {
+ return packageManager.getApplicationIcon(packageName).toBitmap().also {
iconsCache.put(packageName, it)
}
}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/SplitTunnelingCell.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/SplitTunnelingCell.kt
index bab0a2caa5..48694332b7 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/SplitTunnelingCell.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/SplitTunnelingCell.kt
@@ -1,5 +1,6 @@
package net.mullvad.mullvadvpn.compose.cell
+import android.graphics.Bitmap
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
@@ -12,25 +13,29 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
-import androidx.compose.runtime.Composable
+import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.ImageBitmap
+import androidx.compose.ui.graphics.asImageBitmap
+import androidx.compose.ui.graphics.painter.BitmapPainter
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
-import androidx.compose.ui.viewinterop.AndroidView
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
import net.mullvad.mullvadvpn.R
import net.mullvad.mullvadvpn.compose.theme.AppTheme
import net.mullvad.mullvadvpn.compose.theme.Dimens
import net.mullvad.mullvadvpn.compose.theme.typeface.listItemText
-import net.mullvad.mullvadvpn.ui.widget.ApplicationImageView
+import org.koin.androidx.compose.get
@Preview
@Composable
-fun PreviewTunnelingCell() {
+private fun PreviewTunnelingCell() {
AppTheme {
Column(modifier = Modifier.background(color = MaterialTheme.colorScheme.background)) {
- SplitTunnelingCell("Mullvad VPN", "", false)
- SplitTunnelingCell("Mullvad VPN", "", true)
+ SplitTunnelingCell(title = "Mullvad VPN", packageName = "", isSelected = false)
+ SplitTunnelingCell(title = "Mullvad VPN", packageName = "", isSelected = true)
}
}
}
@@ -41,8 +46,16 @@ fun SplitTunnelingCell(
packageName: String?,
isSelected: Boolean,
modifier: Modifier = Modifier,
+ onResolveIcon: (String) -> Bitmap? = { null },
onCellClicked: () -> Unit = {}
) {
+ var icon by remember(packageName) { mutableStateOf<ImageBitmap?>(null) }
+ LaunchedEffect(packageName) {
+ launch(Dispatchers.IO) {
+ val bitmap = onResolveIcon(packageName ?: "")
+ icon = bitmap?.asImageBitmap()
+ }
+ }
Row(
modifier =
modifier
@@ -53,11 +66,10 @@ fun SplitTunnelingCell(
.background(MaterialTheme.colorScheme.primaryContainer)
.clickable(onClick = onCellClicked)
) {
- AndroidView(
- factory = { context -> ApplicationImageView(context) },
- update = { applicationImageView ->
- applicationImageView.packageName = packageName ?: ""
- },
+ Image(
+ painter = icon?.let { iconImage -> BitmapPainter(iconImage) }
+ ?: painterResource(id = R.drawable.ic_icons_missing),
+ contentDescription = null,
modifier =
Modifier.padding(start = Dimens.cellStartPadding)
.align(Alignment.CenterVertically)
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/SplitTunnelingScreen.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/SplitTunnelingScreen.kt
index 3c3044a268..e624170479 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/SplitTunnelingScreen.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/SplitTunnelingScreen.kt
@@ -1,5 +1,6 @@
package net.mullvad.mullvadvpn.compose.screen
+import android.graphics.Bitmap
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
@@ -35,6 +36,7 @@ import net.mullvad.mullvadvpn.compose.extensions.itemWithDivider
import net.mullvad.mullvadvpn.compose.state.SplitTunnelingUiState
import net.mullvad.mullvadvpn.compose.theme.AppTheme
import net.mullvad.mullvadvpn.compose.theme.Dimens
+import org.koin.androidx.compose.get
@Preview
@Composable
@@ -48,7 +50,7 @@ fun PreviewSplitTunnelingScreen() {
AppData(
packageName = "my.package.a",
name = "TitleA",
- iconRes = R.drawable.icon_alert
+ iconRes = R.drawable.icon_alert,
),
AppData(
packageName = "my.package.b",
@@ -77,7 +79,8 @@ fun SplitTunnelingScreen(
onShowSystemAppsClick: (show: Boolean) -> Unit = {},
onExcludeAppClick: (packageName: String) -> Unit = {},
onIncludeAppClick: (packageName: String) -> Unit = {},
- onBackClick: () -> Unit = {}
+ onBackClick: () -> Unit = {},
+ onResolveIcon: (String) -> Bitmap? = { null },
) {
val state = rememberCollapsingToolbarScaffoldState()
val progress = state.toolbarState.progress
@@ -161,7 +164,8 @@ fun SplitTunnelingScreen(
title = listItem.name,
packageName = listItem.packageName,
isSelected = true,
- modifier = Modifier.animateItemPlacement().fillMaxWidth()
+ modifier = Modifier.animateItemPlacement().fillMaxWidth(),
+ onResolveIcon = onResolveIcon
) {
onIncludeAppClick(listItem.packageName)
}
@@ -205,7 +209,8 @@ fun SplitTunnelingScreen(
title = listItem.name,
packageName = listItem.packageName,
isSelected = false,
- modifier = Modifier.animateItemPlacement().fillMaxWidth()
+ modifier = Modifier.animateItemPlacement().fillMaxWidth(),
+ onResolveIcon = onResolveIcon
) {
onExcludeAppClick(listItem.packageName)
}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/fragment/SplitTunnelingFragment.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/fragment/SplitTunnelingFragment.kt
index 59328dc273..854167a03b 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/fragment/SplitTunnelingFragment.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/fragment/SplitTunnelingFragment.kt
@@ -7,13 +7,16 @@ import android.view.ViewGroup
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.platform.ComposeView
import net.mullvad.mullvadvpn.R
+import net.mullvad.mullvadvpn.applist.ApplicationsIconManager
import net.mullvad.mullvadvpn.compose.screen.SplitTunnelingScreen
import net.mullvad.mullvadvpn.compose.theme.AppTheme
import net.mullvad.mullvadvpn.viewmodel.SplitTunnelingViewModel
+import org.koin.android.ext.android.inject
import org.koin.androidx.viewmodel.ext.android.viewModel
class SplitTunnelingFragment : BaseFragment() {
private val viewModel: SplitTunnelingViewModel by viewModel()
+ private val applicationsIconManager: ApplicationsIconManager by inject()
override fun onCreateView(
inflater: LayoutInflater,
@@ -29,7 +32,10 @@ class SplitTunnelingFragment : BaseFragment() {
onShowSystemAppsClick = viewModel::onShowSystemAppsClick,
onExcludeAppClick = viewModel::onExcludeAppClick,
onIncludeAppClick = viewModel::onIncludeAppClick,
- onBackClick = { activity?.onBackPressedDispatcher?.onBackPressed() }
+ onBackClick = { activity?.onBackPressedDispatcher?.onBackPressed() },
+ onResolveIcon = { packageName ->
+ applicationsIconManager.getAppIcon(packageName)
+ }
)
}
}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/widget/ApplicationImageView.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/widget/ApplicationImageView.kt
deleted file mode 100644
index 43a8a58fbe..0000000000
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/widget/ApplicationImageView.kt
+++ /dev/null
@@ -1,69 +0,0 @@
-package net.mullvad.mullvadvpn.ui.widget
-
-import android.content.Context
-import android.content.pm.PackageManager
-import android.graphics.drawable.Drawable
-import android.util.AttributeSet
-import androidx.appcompat.widget.AppCompatImageView
-import androidx.core.content.res.ResourcesCompat
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.Job
-import kotlinx.coroutines.SupervisorJob
-import kotlinx.coroutines.cancelChildren
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
-import net.mullvad.mullvadvpn.R
-import net.mullvad.mullvadvpn.applist.ApplicationsIconManager
-import org.koin.core.component.KoinComponent
-import org.koin.core.component.inject
-
-class ApplicationImageView
-@JvmOverloads
-constructor(
- context: Context,
- attrs: AttributeSet? = null,
- defStyleAttr: Int = R.attr.applicationListItemViewStyle,
-) : AppCompatImageView(context, attrs, defStyleAttr), KoinComponent {
- private val viewScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
- private val iconManager: ApplicationsIconManager by inject()
-
- private var updateImageJob: Job? = null
-
- var packageName: String = ""
- set(value) {
- field = value
- updateImage()
- }
-
- init {
- updateImage(ResourcesCompat.getDrawable(resources, R.drawable.ic_icons_missing, null)!!)
- }
-
- private fun updateImage() {
- updateImageJob?.cancel()
- updateImageJob = viewScope.launch { loadImage()?.let { drawable -> updateImage(drawable) } }
- }
-
- override fun onAttachedToWindow() {
- super.onAttachedToWindow()
- updateImage()
- }
-
- override fun onDetachedFromWindow() {
- super.onDetachedFromWindow()
- updateImageJob?.cancel()
- viewScope.coroutineContext.cancelChildren()
- }
-
- private suspend fun loadImage(): Drawable? =
- withContext(Dispatchers.Default) {
- try {
- iconManager.getAppIcon(packageName)
- } catch (e: PackageManager.NameNotFoundException) {
- null
- }
- }
-
- private fun updateImage(drawable: Drawable) = setImageDrawable(drawable)
-}
diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/applist/ApplicationsIconManagerTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/applist/ApplicationsIconManagerTest.kt
index 8db700f452..3e26a37c2c 100644
--- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/applist/ApplicationsIconManagerTest.kt
+++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/applist/ApplicationsIconManagerTest.kt
@@ -1,8 +1,10 @@
package net.mullvad.mullvadvpn.applist
import android.content.pm.PackageManager
+import android.graphics.Bitmap
import android.graphics.drawable.Drawable
import android.os.Looper
+import androidx.core.graphics.drawable.toBitmap
import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkStatic
@@ -22,6 +24,7 @@ class ApplicationsIconManagerTest {
@Before
fun setUp() {
mockkStatic(Looper::class)
+ mockkStatic(DRAWABLE_EXTENSION_CLASS)
every { Looper.getMainLooper() } returns mockedMainLooper
}
@@ -33,13 +36,16 @@ class ApplicationsIconManagerTest {
@Test
fun test_first_time_load_icon_from_PM() {
val testPackageName = "test"
- val mockedDrawable = mockk<Drawable>()
+ val mockedBitmap = mockk<Bitmap>()
+ val mockedDrawable = mockk<Drawable>().apply { every { toBitmap() } returns mockedBitmap }
every { mockedPackageManager.getApplicationIcon(testPackageName) } returns mockedDrawable
every { mockedMainLooper.isCurrentThread } returns false
+ every { mockedDrawable.intrinsicWidth } returns 0
+ every { mockedDrawable.intrinsicHeight } returns 0
val result = testSubject.getAppIcon(testPackageName)
- assertEquals(mockedDrawable, result)
+ assertEquals(mockedBitmap, result)
verify {
mockedMainLooper.isCurrentThread
mockedPackageManager.getApplicationIcon(testPackageName)
@@ -49,15 +55,18 @@ class ApplicationsIconManagerTest {
@Test
fun test_second_time_load_icon_from_cache() {
val testPackageName = "test"
- val mockedDrawable = mockk<Drawable>()
+ val mockedBitmap = mockk<Bitmap>()
+ val mockedDrawable = mockk<Drawable>().apply { every { toBitmap() } returns mockedBitmap }
every { mockedPackageManager.getApplicationIcon(testPackageName) } returns mockedDrawable
every { mockedMainLooper.isCurrentThread } returns false
+ every { mockedDrawable.intrinsicWidth } returns 0
+ every { mockedDrawable.intrinsicHeight } returns 0
val result = testSubject.getAppIcon(testPackageName)
val result2 = testSubject.getAppIcon(testPackageName)
- assertEquals(mockedDrawable, result)
- assertEquals(mockedDrawable, result2)
+ assertEquals(mockedBitmap, result)
+ assertEquals(mockedBitmap, result2)
verify(exactly = 2) { mockedMainLooper.isCurrentThread }
verify(exactly = 1) { mockedPackageManager.getApplicationIcon(testPackageName) }
}
@@ -65,16 +74,19 @@ class ApplicationsIconManagerTest {
@Test
fun test_second_time_load_icon_from_PM_after_clear() {
val testPackageName = "test"
- val mockedDrawable = mockk<Drawable>()
+ val mockedBitmap = mockk<Bitmap>()
+ val mockedDrawable = mockk<Drawable>().apply { every { toBitmap() } returns mockedBitmap }
every { mockedPackageManager.getApplicationIcon(testPackageName) } returns mockedDrawable
every { mockedMainLooper.isCurrentThread } returns false
+ every { mockedDrawable.intrinsicWidth } returns 0
+ every { mockedDrawable.intrinsicHeight } returns 0
val result = testSubject.getAppIcon(testPackageName)
testSubject.dispose()
val result2 = testSubject.getAppIcon(testPackageName)
- assertEquals(mockedDrawable, result)
- assertEquals(mockedDrawable, result2)
+ assertEquals(mockedBitmap, result)
+ assertEquals(mockedBitmap, result2)
verify(exactly = 2) {
mockedMainLooper.isCurrentThread
mockedPackageManager.getApplicationIcon(testPackageName)
@@ -91,4 +103,8 @@ class ApplicationsIconManagerTest {
}
verify { mockedMainLooper.isCurrentThread }
}
+
+ companion object {
+ private const val DRAWABLE_EXTENSION_CLASS = "androidx.core.graphics.drawable.DrawableKt"
+ }
}