diff options
| author | Jonatan Rhodin <jonatan.rhodin@mullvad.net> | 2025-03-26 11:53:25 +0100 |
|---|---|---|
| committer | Jonatan Rhodin <jonatan.rhodin@mullvad.net> | 2025-03-26 11:53:25 +0100 |
| commit | 430f8a3455aa75e61deebe3eea4e96c4360e147d (patch) | |
| tree | eda753e5489fe9a04b69902703a143e01aad9dc8 /android/app | |
| parent | 5a853f4799974f85bef6112407582355e43fae0d (diff) | |
| parent | e8a709876f6d67ac8f5c0a7f3b81487e3d73a3d3 (diff) | |
| download | mullvadvpn-430f8a3455aa75e61deebe3eea4e96c4360e147d.tar.xz mullvadvpn-430f8a3455aa75e61deebe3eea4e96c4360e147d.zip | |
Merge branch 'investigate-splittunneling-application-icon-droid-501'
Diffstat (limited to 'android/app')
7 files changed, 81 insertions, 48 deletions
diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 261cc9a48e..7f411e2a07 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -403,6 +403,8 @@ dependencies { implementation(libs.compose.destinations) ksp(libs.compose.destinations.ksp) + implementation(libs.accompanist.drawablepainter) + implementation(libs.kermit) implementation(libs.koin) implementation(libs.koin.android) 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 8215ccde69..c17cb9079b 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,6 +1,6 @@ package net.mullvad.mullvadvpn.compose.screen -import android.graphics.Bitmap +import android.graphics.drawable.Drawable import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick @@ -39,7 +39,7 @@ class SplitTunnelingScreenTest { onExcludeAppClick: (packageName: String) -> Unit = {}, onIncludeAppClick: (packageName: String) -> Unit = {}, onBackClick: () -> Unit = {}, - onResolveIcon: (String) -> Bitmap? = { null }, + onResolveIcon: (String) -> Drawable? = { null }, ) { setContentWithTheme { SplitTunnelingScreen( 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 15a83776d4..11904157e9 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,10 +1,13 @@ package net.mullvad.mullvadvpn.compose.cell -import android.graphics.Bitmap +import android.graphics.drawable.Drawable import androidx.compose.foundation.Image import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Remove @@ -19,21 +22,21 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ColorFilter -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.unit.dp +import com.google.accompanist.drawablepainter.rememberDrawablePainter import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import net.mullvad.mullvadvpn.R import net.mullvad.mullvadvpn.compose.component.SpacedColumn -import net.mullvad.mullvadvpn.compose.util.isBelowMaxBitmapSize +import net.mullvad.mullvadvpn.compose.util.isBelowMaxSize import net.mullvad.mullvadvpn.lib.theme.AppTheme import net.mullvad.mullvadvpn.lib.theme.Dimens +import net.mullvad.mullvadvpn.lib.theme.color.AlphaDisabled import net.mullvad.mullvadvpn.lib.theme.typeface.listItemText @Preview @@ -71,35 +74,23 @@ fun SplitTunnelingCell( enabled: Boolean, modifier: Modifier = Modifier, backgroundColor: Color = MaterialTheme.colorScheme.surfaceContainerHigh, - onResolveIcon: (String) -> Bitmap?, + onResolveIcon: (String) -> Drawable?, onCellClicked: () -> Unit, ) { - var icon by remember(packageName) { mutableStateOf<ImageBitmap?>(null) } + var icon by remember(packageName) { mutableStateOf<IconState>(IconState.Loading) } LaunchedEffect(packageName) { launch(Dispatchers.IO) { - val bitmap = onResolveIcon(packageName ?: "") - if (bitmap != null && bitmap.isBelowMaxBitmapSize()) { - icon = bitmap.asImageBitmap() - } + val drawable = onResolveIcon(packageName ?: "") + icon = + if (drawable != null && drawable.isBelowMaxSize()) { + IconState.Icon(drawable = drawable) + } else { + IconState.NoIcon + } } } BaseCell( - iconView = { - Image( - painter = - icon?.let { iconImage -> BitmapPainter(iconImage) } - ?: painterResource(id = R.drawable.ic_icons_missing), - contentDescription = null, - modifier = - Modifier.align(Alignment.CenterVertically).size(size = Dimens.listIconSize), - colorFilter = - if (icon == null) { - ColorFilter.tint(MaterialTheme.colorScheme.onSurface) - } else { - null - }, - ) - }, + iconView = { Icon(iconState = icon) }, headlineContent = { Text( text = title, @@ -130,3 +121,40 @@ fun SplitTunnelingCell( isRowEnabled = enabled, ) } + +@Composable +private fun RowScope.Icon(iconState: IconState) { + when (iconState) { + IconState.Loading -> + Box( + modifier = + Modifier.align(Alignment.CenterVertically) + .size(Dimens.listIconSize) + .clip(CircleShape) + .background(MaterialTheme.colorScheme.onSurface.copy(alpha = AlphaDisabled)) + ) + is IconState.Icon -> + Image( + painter = rememberDrawablePainter(drawable = iconState.drawable), + contentDescription = null, + modifier = + Modifier.align(Alignment.CenterVertically).size(size = Dimens.listIconSize), + ) + IconState.NoIcon -> + Image( + painter = painterResource(id = R.drawable.ic_icons_missing), + contentDescription = null, + modifier = + Modifier.align(Alignment.CenterVertically).size(size = Dimens.listIconSize), + colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onSurface), + ) + } +} + +private sealed class IconState { + object Loading : IconState() + + data class Icon(val drawable: Drawable) : IconState() + + object NoIcon : IconState() // Icon not found or icon too large +} 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 5061655003..bfd209b6ff 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,6 +1,6 @@ package net.mullvad.mullvadvpn.compose.screen -import android.graphics.Bitmap +import android.graphics.drawable.Drawable import androidx.compose.foundation.background import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize @@ -49,7 +49,7 @@ import net.mullvad.mullvadvpn.lib.theme.AppTheme import net.mullvad.mullvadvpn.lib.theme.Dimens import net.mullvad.mullvadvpn.lib.theme.color.AlphaDisabled import net.mullvad.mullvadvpn.lib.theme.color.AlphaVisible -import net.mullvad.mullvadvpn.util.getApplicationIconBitmapOrNull +import net.mullvad.mullvadvpn.util.getApplicationIconOrNull import net.mullvad.mullvadvpn.viewmodel.SplitTunnelingViewModel import org.koin.androidx.compose.koinViewModel @@ -77,9 +77,7 @@ fun SplitTunneling(navigator: DestinationsNavigator) { onExcludeAppClick = viewModel::onExcludeAppClick, onIncludeAppClick = viewModel::onIncludeAppClick, onBackClick = dropUnlessResumed { navigator.navigateUp() }, - onResolveIcon = { packageName -> - packageManager.getApplicationIconBitmapOrNull(packageName) - }, + onResolveIcon = { packageName -> packageManager.getApplicationIconOrNull(packageName) }, ) } @@ -91,7 +89,7 @@ fun SplitTunnelingScreen( onExcludeAppClick: (packageName: String) -> Unit, onIncludeAppClick: (packageName: String) -> Unit, onBackClick: () -> Unit, - onResolveIcon: (String) -> Bitmap?, + onResolveIcon: (String) -> Drawable?, ) { val focusManager = LocalFocusManager.current @@ -164,7 +162,7 @@ private fun LazyListScope.appList( onShowSystemAppsClick: (show: Boolean) -> Unit, onExcludeAppClick: (packageName: String) -> Unit, onIncludeAppClick: (packageName: String) -> Unit, - onResolveIcon: (String) -> Bitmap?, + onResolveIcon: (String) -> Drawable?, ) { if (state.excludedApps.isNotEmpty()) { headerItem( @@ -206,7 +204,7 @@ private fun LazyListScope.appItems( apps: List<AppData>, focusManager: FocusManager, onAppClick: (String) -> Unit, - onResolveIcon: (String) -> Bitmap?, + onResolveIcon: (String) -> Drawable?, enabled: Boolean, excluded: Boolean, ) { diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/util/Bitmap.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/util/Bitmap.kt deleted file mode 100644 index 9a3d030780..0000000000 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/util/Bitmap.kt +++ /dev/null @@ -1,7 +0,0 @@ -package net.mullvad.mullvadvpn.compose.util - -import android.graphics.Bitmap - -private const val MAX_BITMAP_SIZE_BYTES = 100 * 1024 * 1024 - -fun Bitmap.isBelowMaxBitmapSize(): Boolean = byteCount < MAX_BITMAP_SIZE_BYTES diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/util/Drawable.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/util/Drawable.kt new file mode 100644 index 0000000000..3985590879 --- /dev/null +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/util/Drawable.kt @@ -0,0 +1,13 @@ +package net.mullvad.mullvadvpn.compose.util + +import android.graphics.drawable.BitmapDrawable +import android.graphics.drawable.Drawable + +private const val MAX_BITMAP_SIZE_BYTES = 100 * 1024 * 1024 // 100MB + +fun Drawable.isBelowMaxSize(): Boolean = + if (this is BitmapDrawable) { + bitmap.byteCount < MAX_BITMAP_SIZE_BYTES + } else { + true + } diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/PackageManagerExtensions.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/PackageManagerExtensions.kt index 1e7721710e..fc6acfeb6c 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/PackageManagerExtensions.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/PackageManagerExtensions.kt @@ -1,12 +1,11 @@ package net.mullvad.mullvadvpn.util import android.content.pm.PackageManager -import android.graphics.Bitmap -import androidx.core.graphics.drawable.toBitmapOrNull +import android.graphics.drawable.Drawable -fun PackageManager.getApplicationIconBitmapOrNull(packageName: String): Bitmap? = +fun PackageManager.getApplicationIconOrNull(packageName: String): Drawable? = try { - getApplicationIcon(packageName).toBitmapOrNull() + getApplicationIcon(packageName) } catch (e: PackageManager.NameNotFoundException) { // Name not found is thrown if the application is not installed null |
