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 | |
| parent | 5a853f4799974f85bef6112407582355e43fae0d (diff) | |
| parent | e8a709876f6d67ac8f5c0a7f3b81487e3d73a3d3 (diff) | |
| download | mullvadvpn-430f8a3455aa75e61deebe3eea4e96c4360e147d.tar.xz mullvadvpn-430f8a3455aa75e61deebe3eea4e96c4360e147d.zip | |
Merge branch 'investigate-splittunneling-application-icon-droid-501'
11 files changed, 121 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 diff --git a/android/gradle/libs.versions.toml b/android/gradle/libs.versions.toml index 864af8f07a..e57d72f0c0 100644 --- a/android/gradle/libs.versions.toml +++ b/android/gradle/libs.versions.toml @@ -33,6 +33,7 @@ compose-destinations = "2.1.0" compose-constraintlayout = "1.1.1" compose-material3 = "1.3.1" compose-material-tv = "1.1.0-alpha01" +drawablepainter = "0.37.2" # Update suppression for 'InvalidPackage' in config/lint.xml grpc = "1.71.0" @@ -122,6 +123,7 @@ compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling", version.ref = compose-ui-tooling-android-preview = { module = "androidx.compose.ui:ui-tooling-preview-android", version.ref = "compose" } compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview", version.ref = "compose" } compose-ui-util = { module = "androidx.compose.ui:ui-util", version.ref = "compose" } +accompanist-drawablepainter = { module = "com.google.accompanist:accompanist-drawablepainter", version.ref = "drawablepainter" } # gRPC grpc-okhttp = { module = "io.grpc:grpc-okhttp", version.ref = "grpc" } diff --git a/android/gradle/verification-keyring.keys b/android/gradle/verification-keyring.keys index 015c0e7ede..d13f73a2b1 100644 --- a/android/gradle/verification-keyring.keys +++ b/android/gradle/verification-keyring.keys @@ -445,6 +445,35 @@ xV+8+asS+ID6jUp5FJ4izH6U90j4iiBfqIu+UEw+0gvD+TVqpqcj5pNlgU45HA== -----END PGP PUBLIC KEY BLOCK----- +pub 1188B69F6D6259CA +uid Chris Banes <chris@banes.me> + +sub 0888B86856F9D71A +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQENBF2hcBkBCAC2H5WcFoeByKBUAjRDjmP+5P6FRsZjLe6c1wy7G1ha6/EQUVK4 +gZUZYE9W7l/4QKHvAu4PvFWdD+5FXGZuB/2kw348CtabAlJTehm1QlPt5//ODkxB +fWPPz3uHBo5PQJZuLpStQn+aEkTTHK6Sk5033e6fa7mV5X/c8pTmTzkunG58NFbj +5VSVbks4pbafKoMY7rSN0/I2hEApCjB9tx/7DJuQ9gbaGhmabnhBTnwHXEV1/hIF +369lfiqeoeqDMOKj99C6KFD/NPRrRLfoPRpqL3LPsPp7P+TzyZN2q89Xqn2ysyI9 +jDtrlssEXLskU5kA8fVa179V/QR6QtrJ29m/ABEBAAG0HENocmlzIEJhbmVzIDxj +aHJpc0BiYW5lcy5tZT65AQ0EXaFwGQEIALaSv8Um4LBbyPM8PIfcSwl5sqBQal1n +w7461u8gGAi3uikn62Q7IGO3SRRuJcR6hkY5Dyq1qPwEPmcBjK0Nlx1NTtk4TwDT +ZsxexNV40qQ+BuZhCb0NmiQBRBY7o3GiFa8mqcNeTNnIxvqMYInIYcvHps3r77QN +Y8PZznfDNpJkuD/vvoFnDjncdaQd+Kia2gqAFHQMw6DnV1oCyPx38CqK10rw3DPt +yYo/sVo67mVLJZvoXx255DbKNATKSrhiuCIsK9r+lIvY9zXoI2cJFsNA1QSiDkt1 +yjW1us89+LMt5dVCanq0bfhprU4J3ZYqzUS1sVAekVY2jRw/ssidX+sAEQEAAYkB +PAQYAQgAJhYhBGS5sJ8WSqC/iHQuthGItp9tYlnKBQJdoXAZAhsMBQkDwmcAAAoJ +EBGItp9tYlnKg94H/2GsZGvWK8+dA4y0AmCtP4B6/WAUonGBMmUH1Ax8GaPKIQuC +ePBbQI5v2dGBqUOTjTh/qcpwfTHSMSWFRTv2HZDP82gY/tAHJrKZ14+Pr1JB4Nvd +PIj60dKFfyveC3t8TJ4kVjKvvDzGY7oHsZ1OmW1Nytsbe6Taj57UY8v67zpLRCEb +ZHIEo4hYtehEIlCWi7M2qJmXtWK/wU3bw0C8KL4Kun968+0005iOdQ/Iig4oI4K/ +UW2HkfQM1AwuwJlsc8uXUvH4A3kkVHTGnvvi4aTMtDWPKpdNu/eufWwuMrS6Gxdu +v7wjtN+wTGesv0OYCfxHScTi49XbBbU3PBq+pxQ= +=Lz+t +-----END PGP PUBLIC KEY BLOCK----- + + pub 1427500BB1D27520 uid ExoQuery <ExoQueryUser@gmail.com> diff --git a/android/gradle/verification-metadata.keys.xml b/android/gradle/verification-metadata.keys.xml index 28ccbbc9fc..6894f5c7ec 100644 --- a/android/gradle/verification-metadata.keys.xml +++ b/android/gradle/verification-metadata.keys.xml @@ -96,6 +96,7 @@ <trusted-key id="600EA202B1EC682F4A788E5AAC7A514BC9F9BB70" group="io.opencensus"/> <trusted-key id="60200AC4AE761F1614D6C46766D68DAA073BE985" group="org.slf4j"/> <trusted-key id="648190996EC0930A6D7D49A978178478013521D0" group="com.facebook"/> + <trusted-key id="64B9B09F164AA0BF88742EB61188B69F6D6259CA" group="com.google.accompanist" name="accompanist-drawablepainter" version="0.37.2"/> <trusted-key id="694621A7227D8D5289699830ABE9F3126BB741C1" group="^com[.]google($|([.].*))" regex="true"/> <trusted-key id="696B6199A2A9D8C29CE78CC0D041CAD2E452550F" group="com.google.protobuf"/> <trusted-key id="6DD3B8C64EF75253BEB2C53AD908A43FB7EC07AC" group="jakarta.activation"/> diff --git a/android/gradle/verification-metadata.xml b/android/gradle/verification-metadata.xml index 29a3983128..26f1014d59 100644 --- a/android/gradle/verification-metadata.xml +++ b/android/gradle/verification-metadata.xml @@ -2944,6 +2944,14 @@ <sha256 value="a8e32e17575d0188c1f3e2e57bb95db09793883c8853fde828b63ecb1f99a63a" origin="Generated by Gradle"/> </artifact> </component> + <component group="com.google.accompanist" name="accompanist-drawablepainter" version="0.37.2"> + <artifact name="accompanist-drawablepainter-0.37.2.aar"> + <sha256 value="f65d76ba4d169186d79015603b680571224c1df7727864b8a796315a960595a8" origin="Generated by Gradle"/> + </artifact> + <artifact name="accompanist-drawablepainter-0.37.2.module"> + <sha256 value="c4cd79129d9809d40296a8ead6b33770287e192daa5ce1e316fab6a53bc64976" origin="Generated by Gradle"/> + </artifact> + </component> <component group="com.google.android" name="annotations" version="4.1.1.4"> <artifact name="annotations-4.1.1.4.jar"> <sha256 value="ba734e1e84c09d615af6a09d33034b4f0442f8772dec120efb376d86a565ae15" origin="Generated by Gradle"/> |
