summaryrefslogtreecommitdiffhomepage
path: root/android/app/src
diff options
context:
space:
mode:
Diffstat (limited to 'android/app/src')
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/SplitTunnelingCell.kt105
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/List.kt90
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/extensions/LazyListExtensions.kt3
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/SplitTunnelingScreen.kt164
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/SplitTunnelingUiState.kt2
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/theme/Theme.kt17
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/theme/dimensions/Dimensions.kt6
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/ListItemsAdapter.kt117
-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/listitemview/ApplicationListItemView.kt76
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/widget/ApplicationImageView.kt2
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SplitTunnelingViewModel.kt8
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SplitTunnelingViewModelState.kt2
-rw-r--r--android/app/src/main/res/layout/app_list_item.xml39
-rw-r--r--android/app/src/main/res/layout/collapsed_title_layout.xml65
15 files changed, 236 insertions, 468 deletions
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 13e31a9337..adf097c2e9 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,74 +1,95 @@
package net.mullvad.mullvadvpn.compose.cell
+import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
-import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.defaultMinSize
+import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
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.ui.Alignment
+import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
-import androidx.compose.ui.res.dimensionResource
+import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
-import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import net.mullvad.mullvadvpn.R
-import net.mullvad.mullvadvpn.compose.component.ListItem
-import net.mullvad.mullvadvpn.compose.theme.MullvadBlue40
-import net.mullvad.mullvadvpn.compose.theme.MullvadWhite40
+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
@Preview
@Composable
fun PreviewTunnelingCell() {
- Column(modifier = Modifier.background(color = MullvadWhite40)) {
- SplitTunnelingCell("Mullvad VPN", "", false) {}
- SplitTunnelingCell("Mullvad VPN", "", true) {}
+ AppTheme {
+ Column(modifier = Modifier.background(color = MaterialTheme.colorScheme.background)) {
+ SplitTunnelingCell("Mullvad VPN", "", false)
+ SplitTunnelingCell("Mullvad VPN", "", true)
+ }
}
}
+@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun SplitTunnelingCell(
title: String,
packageName: String?,
isSelected: Boolean,
- onCellClicked: () -> Unit
+ modifier: Modifier = Modifier,
+ onCellClicked: () -> Unit = {}
) {
- val startPadding = dimensionResource(id = R.dimen.cell_left_padding)
- val endPadding = dimensionResource(id = R.dimen.cell_right_padding)
- val iconSize = dimensionResource(id = R.dimen.icon_size)
- Box(
+ Row(
modifier =
- Modifier.background(MullvadBlue40)
- .padding(top = 1.dp, bottom = 1.dp, start = startPadding, end = endPadding)
- .clickable { onCellClicked() }
+ modifier
+ .wrapContentHeight()
+ .defaultMinSize(minHeight = Dimens.listItemHeightExtra)
+ .fillMaxWidth()
+ .padding(vertical = Dimens.listItemDivider)
+ .background(MaterialTheme.colorScheme.primaryContainer)
+ .clickable(onClick = onCellClicked)
) {
- Row(
- verticalAlignment = Alignment.CenterVertically,
- ) {
- packageName?.let {
- AndroidView(
- factory = { context -> ApplicationImageView(context) },
- update = { applicationImageView ->
- applicationImageView.packageName = packageName
- },
- modifier = Modifier.size(width = iconSize, height = iconSize)
- )
- }
- ListItem(
- text = title,
- isLoading = false,
- iconResourceId =
- if (isSelected) {
- R.drawable.ic_icons_remove
- } else {
- R.drawable.ic_icons_add
- },
- background = MullvadBlue40,
- onClick = null
- )
- }
+ AndroidView(
+ factory = { context -> ApplicationImageView(context) },
+ update = { applicationImageView ->
+ applicationImageView.packageName = packageName ?: ""
+ },
+ onReset = {},
+ modifier =
+ Modifier.padding(start = Dimens.cellLeftPadding)
+ .align(Alignment.CenterVertically)
+ .size(width = Dimens.listIconSize, height = Dimens.listIconSize)
+ )
+ Text(
+ text = title,
+ style = MaterialTheme.typography.listItemText,
+ color = MaterialTheme.colorScheme.onPrimary,
+ modifier =
+ Modifier.weight(1f)
+ .padding(horizontal = Dimens.mediumPadding, vertical = Dimens.smallPadding)
+ .align(Alignment.CenterVertically)
+ )
+ Image(
+ painter =
+ painterResource(
+ id =
+ if (isSelected) {
+ R.drawable.ic_icons_remove
+ } else {
+ R.drawable.ic_icons_add
+ }
+ ),
+ contentDescription = null,
+ modifier =
+ Modifier.padding(end = Dimens.cellRightPadding)
+ .align(Alignment.CenterVertically)
+ .padding(horizontal = Dimens.loadingSpinnerPadding)
+ )
}
}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/List.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/List.kt
index 88fb4ccee8..f31e9052dd 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/List.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/List.kt
@@ -25,6 +25,7 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.constraintlayout.compose.ConstraintLayout
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.listItemSubText
import net.mullvad.mullvadvpn.compose.theme.typeface.listItemText
@@ -32,47 +33,49 @@ import net.mullvad.mullvadvpn.compose.theme.typeface.listItemText
@Preview
@Composable
fun PreviewListItem() {
- Column {
- ListItem(text = "No subtext No icon not loading", isLoading = false, onClick = {})
- ListItem(text = "No subtext No icon is loading", isLoading = true, onClick = {})
- ListItem(
- text = "No subtext With icon is loading",
- isLoading = true,
- iconResourceId = R.drawable.icon_close,
- onClick = {}
- )
- ListItem(
- text = "No subtext With icon not loading",
- isLoading = false,
- iconResourceId = R.drawable.icon_close,
- onClick = {}
- )
- ListItem(
- text = "With subtext with icon is loading",
- subText = "Subtext",
- isLoading = true,
- iconResourceId = R.drawable.icon_close,
- onClick = {}
- )
- ListItem(
- text = "With subtext no icon is loading",
- subText = "Subtext",
- isLoading = true,
- onClick = {}
- )
- ListItem(
- text = "With subtext with icon not loading",
- subText = "Subtext",
- isLoading = false,
- iconResourceId = R.drawable.icon_close,
- onClick = {}
- )
- ListItem(
- text = "With subtext no icon not loading",
- subText = "Subtext",
- isLoading = false,
- onClick = {}
- )
+ AppTheme {
+ Column {
+ ListItem(text = "No subtext No icon not loading", isLoading = false, onClick = {})
+ ListItem(text = "No subtext No icon is loading", isLoading = true, onClick = {})
+ ListItem(
+ text = "No subtext With icon is loading",
+ isLoading = true,
+ iconResourceId = R.drawable.icon_close,
+ onClick = {}
+ )
+ ListItem(
+ text = "No subtext With icon not loading",
+ isLoading = false,
+ iconResourceId = R.drawable.icon_close,
+ onClick = {}
+ )
+ ListItem(
+ text = "With subtext with icon is loading",
+ subText = "Subtext",
+ isLoading = true,
+ iconResourceId = R.drawable.icon_close,
+ onClick = {}
+ )
+ ListItem(
+ text = "With subtext no icon is loading",
+ subText = "Subtext",
+ isLoading = true,
+ onClick = {}
+ )
+ ListItem(
+ text = "With subtext with icon not loading",
+ subText = "Subtext",
+ isLoading = false,
+ iconResourceId = R.drawable.icon_close,
+ onClick = {}
+ )
+ ListItem(
+ text = "With subtext no icon not loading",
+ subText = "Subtext",
+ isLoading = false,
+ onClick = {}
+ )
+ }
}
}
@@ -89,17 +92,16 @@ fun ListItem(
height: Dp = Dimens.listItemHeight,
isLoading: Boolean,
@DrawableRes iconResourceId: Int? = null,
- background: Color? = null,
+ background: Color = MaterialTheme.colorScheme.primary,
onClick: (() -> Unit)?
) {
- val itemColor = background ?: MaterialTheme.colorScheme.primary
Box(
modifier =
Modifier.fillMaxWidth()
.padding(vertical = Dimens.listItemDivider)
.wrapContentHeight()
.defaultMinSize(minHeight = height)
- .background(itemColor),
+ .background(background)
) {
Column(
modifier =
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/extensions/LazyListExtensions.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/extensions/LazyListExtensions.kt
index 453ab50cdf..383b745746 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/extensions/LazyListExtensions.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/extensions/LazyListExtensions.kt
@@ -6,10 +6,11 @@ import androidx.compose.material.Divider
import androidx.compose.runtime.Composable
inline fun LazyListScope.itemWithDivider(
+ key: Any? = null,
contentType: Any? = null,
crossinline itemContent: @Composable LazyItemScope.() -> Unit
) =
- item(contentType = contentType) {
+ item(key = key, contentType = contentType) {
itemContent()
Divider()
}
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 22ecea3c71..8c5a295308 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,23 +1,23 @@
package net.mullvad.mullvadvpn.compose.screen
+import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
-import androidx.compose.material.CircularProgressIndicator
-import androidx.compose.material.Text
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
-import androidx.compose.ui.unit.sp
import me.onebone.toolbar.ScrollStrategy
import me.onebone.toolbar.rememberCollapsingToolbarScaffoldState
import net.mullvad.mullvadvpn.R
@@ -27,60 +27,61 @@ import net.mullvad.mullvadvpn.compose.cell.SplitTunnelingCell
import net.mullvad.mullvadvpn.compose.cell.SwitchComposeCell
import net.mullvad.mullvadvpn.compose.component.CollapsableAwareToolbarScaffold
import net.mullvad.mullvadvpn.compose.component.CollapsingTopBar
+import net.mullvad.mullvadvpn.compose.component.drawVerticalScrollbar
import net.mullvad.mullvadvpn.compose.extensions.itemWithDivider
import net.mullvad.mullvadvpn.compose.state.SplitTunnelingUiState
-import net.mullvad.mullvadvpn.compose.theme.MullvadBlue
-import net.mullvad.mullvadvpn.compose.theme.MullvadDarkBlue
-import net.mullvad.mullvadvpn.compose.theme.MullvadWhite
-import net.mullvad.mullvadvpn.compose.theme.MullvadWhite60
+import net.mullvad.mullvadvpn.compose.theme.AppTheme
+import net.mullvad.mullvadvpn.compose.theme.Dimens
@Preview
@Composable
fun PreviewSplitTunnelingScreen() {
- SplitTunnelingScreen(
- uiState =
- SplitTunnelingUiState.Data(
- excludedApps =
- listOf(
- AppData(
- packageName = "Package C",
- name = "TitleA",
- iconRes = R.drawable.icon_alert
+ AppTheme {
+ SplitTunnelingScreen(
+ uiState =
+ SplitTunnelingUiState.ShowAppList(
+ excludedApps =
+ listOf(
+ AppData(
+ packageName = "Package C",
+ name = "TitleA",
+ iconRes = R.drawable.icon_alert
+ ),
+ AppData(
+ packageName = "Package G",
+ name = "TitleB",
+ iconRes = R.drawable.icon_chevron,
+ )
),
- AppData(
- packageName = "Package G",
- name = "TitleB",
- iconRes = R.drawable.icon_chevron,
- )
- ),
- includedApps =
- listOf(
- AppData(
- packageName = "Package I",
- name = "TitleC",
- iconRes = R.drawable.icon_alert
- )
- ),
- showSystemApps = true
- )
- )
+ includedApps =
+ listOf(
+ AppData(
+ packageName = "Package I",
+ name = "TitleC",
+ iconRes = R.drawable.icon_alert
+ )
+ ),
+ showSystemApps = true
+ )
+ )
+ }
}
@Composable
+@OptIn(ExperimentalFoundationApi::class)
fun SplitTunnelingScreen(
uiState: SplitTunnelingUiState = SplitTunnelingUiState.Loading,
onShowSystemAppsClicked: (show: Boolean) -> Unit = {},
- addToExcluded: (packageName: String) -> Unit = {},
- removeFromExcluded: (packageName: String) -> Unit = {},
- onBackClick: () -> Unit = {},
+ onExcludeAppClick: (packageName: String) -> Unit = {},
+ onIncludeAppClick: (packageName: String) -> Unit = {},
+ onBackClick: () -> Unit = {}
) {
val state = rememberCollapsingToolbarScaffoldState()
val progress = state.toolbarState.progress
- val mediumPadding = dimensionResource(id = R.dimen.medium_padding)
- val progressSize = dimensionResource(id = R.dimen.progress_size)
+ val lazyListState = rememberLazyListState()
CollapsableAwareToolbarScaffold(
- backgroundColor = MullvadDarkBlue,
+ backgroundColor = MaterialTheme.colorScheme.background,
modifier = Modifier.fillMaxSize(),
state = state,
scrollStrategy = ScrollStrategy.ExitUntilCollapsed,
@@ -92,52 +93,57 @@ fun SplitTunnelingScreen(
whenExpanded = Alignment.BottomStart
)
CollapsingTopBar(
- backgroundColor = MullvadDarkBlue,
+ backgroundColor = MaterialTheme.colorScheme.background,
onBackClicked = { onBackClick() },
title = stringResource(id = R.string.split_tunneling),
progress = progress,
modifier = scaffoldModifier,
- backTitle = stringResource(id = R.string.settings_advanced)
+ backTitle = stringResource(id = R.string.settings_vpn)
)
},
) {
- LazyColumn(horizontalAlignment = Alignment.CenterHorizontally) {
- item(contentType = TYPE_DESCRIPTION) {
+ LazyColumn(
+ modifier = Modifier.drawVerticalScrollbar(state = lazyListState),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ state = lazyListState
+ ) {
+ item(key = KEY_DESCRIPTION, contentType = TYPE_DESCRIPTION) {
Text(
- color = MullvadWhite60,
+ style = MaterialTheme.typography.labelMedium,
text = stringResource(id = R.string.split_tunneling_description),
- fontSize = 13.sp,
modifier =
Modifier.padding(
- start = mediumPadding,
- end = mediumPadding,
- bottom = mediumPadding
+ start = Dimens.mediumPadding,
+ end = Dimens.mediumPadding,
+ bottom = Dimens.mediumPadding
)
)
}
when (uiState) {
SplitTunnelingUiState.Loading -> {
- item {
+ item(key = KEY_PROGRESS, contentType = TYPE_PROGRESS) {
CircularProgressIndicator(
- color = MullvadWhite,
- modifier = Modifier.size(width = progressSize, height = progressSize)
+ color = MaterialTheme.colorScheme.onBackground,
+ modifier =
+ Modifier.size(
+ width = Dimens.progressIndicatorSize,
+ height = Dimens.progressIndicatorSize
+ )
)
}
}
- is SplitTunnelingUiState.Data -> {
+ is SplitTunnelingUiState.ShowAppList -> {
if (uiState.excludedApps.isNotEmpty()) {
- itemWithDivider(contentType = TYPE_TITLE) {
+ itemWithDivider(key = KEY_EXCLUDED_APPLICATIONS, contentType = TYPE_TITLE) {
BaseCell(
title = {
Text(
text = stringResource(id = R.string.exclude_applications),
- color = Color.White,
- fontSize = 18.sp,
- fontWeight = FontWeight.SemiBold
+ style = MaterialTheme.typography.titleMedium
)
},
bodyView = {},
- background = MullvadBlue,
+ background = MaterialTheme.colorScheme.primary,
)
}
items(
@@ -148,33 +154,37 @@ fun SplitTunnelingScreen(
SplitTunnelingCell(
title = listItem.name,
packageName = listItem.packageName,
- isSelected = true
+ isSelected = true,
+ modifier = Modifier.animateItemPlacement().fillMaxWidth()
) {
- removeFromExcluded(listItem.packageName)
+ onIncludeAppClick(listItem.packageName)
}
}
- item { Spacer(modifier = Modifier.height(mediumPadding)) }
+ item(key = KEY_SPACER, contentType = TYPE_SPACER) {
+ Spacer(modifier = Modifier.height(Dimens.mediumPadding))
+ }
}
- itemWithDivider(contentType = TYPE_SWITCH_CELL) {
+ itemWithDivider(
+ key = KEY_SHOW_SYSTEM_APPLICATIONS,
+ contentType = TYPE_SWITCH_CELL
+ ) {
SwitchComposeCell(
title = stringResource(id = R.string.show_system_apps),
isToggled = uiState.showSystemApps,
onCellClicked = { newValue -> onShowSystemAppsClicked(newValue) }
)
}
- itemWithDivider(contentType = TYPE_TITLE) {
+ itemWithDivider(key = KEY_INCLUDED_APPLICATIONS, contentType = TYPE_TITLE) {
BaseCell(
title = {
Text(
text = stringResource(id = R.string.all_applications),
- color = Color.White,
- fontSize = 18.sp,
- fontWeight = FontWeight.SemiBold
+ style = MaterialTheme.typography.titleMedium
)
},
bodyView = {},
- background = MullvadBlue,
+ background = MaterialTheme.colorScheme.primary,
)
}
items(
@@ -185,9 +195,10 @@ fun SplitTunnelingScreen(
SplitTunnelingCell(
title = listItem.name,
packageName = listItem.packageName,
- isSelected = false
+ isSelected = false,
+ modifier = Modifier.animateItemPlacement().fillMaxWidth()
) {
- addToExcluded(listItem.packageName)
+ onExcludeAppClick(listItem.packageName)
}
}
}
@@ -196,7 +207,18 @@ fun SplitTunnelingScreen(
}
}
+// Content types, to improve the ability to reuse views
const val TYPE_DESCRIPTION = 1
const val TYPE_TITLE = 2
const val TYPE_SWITCH_CELL = 3
const val TYPE_APPLICATION = 4
+const val TYPE_SPACER = 5
+const val TYPE_PROGRESS = 6
+
+// Keys, also to improve re-usability of views
+const val KEY_DESCRIPTION = "description"
+const val KEY_EXCLUDED_APPLICATIONS = "excluded"
+const val KEY_SHOW_SYSTEM_APPLICATIONS = "show_system"
+const val KEY_INCLUDED_APPLICATIONS = "included"
+const val KEY_SPACER = "spacer"
+const val KEY_PROGRESS = "progress"
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/SplitTunnelingUiState.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/SplitTunnelingUiState.kt
index 903bb7afdd..d63afac400 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/SplitTunnelingUiState.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/SplitTunnelingUiState.kt
@@ -4,7 +4,7 @@ import net.mullvad.mullvadvpn.applist.AppData
sealed interface SplitTunnelingUiState {
object Loading : SplitTunnelingUiState
- data class Data(
+ data class ShowAppList(
val excludedApps: List<AppData> = emptyList(),
val includedApps: List<AppData> = emptyList(),
val showSystemApps: Boolean = false
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/theme/Theme.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/theme/Theme.kt
index 815cfbf88b..8749c7cb55 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/theme/Theme.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/theme/Theme.kt
@@ -25,7 +25,19 @@ private val MullvadTypography =
fontSize = TypeScale.TextBig,
fontWeight = FontWeight.Bold
),
- bodySmall = TextStyle(color = MullvadWhite, fontSize = TypeScale.TextSmall)
+ bodySmall = TextStyle(color = MullvadWhite, fontSize = TypeScale.TextSmall),
+ titleMedium =
+ TextStyle(
+ color = MullvadWhite,
+ fontSize = TypeScale.TextMediumPlus,
+ fontWeight = FontWeight.SemiBold
+ ),
+ labelMedium =
+ TextStyle(
+ color = MullvadWhite60,
+ fontSize = TypeScale.TextSmall,
+ fontWeight = FontWeight.SemiBold
+ )
)
private val MullvadColorPalette =
@@ -33,6 +45,9 @@ private val MullvadColorPalette =
primary = MullvadBlue,
secondary = MullvadDarkBlue,
tertiary = MullvadRed,
+ background = MullvadDarkBlue,
+ onBackground = MullvadWhite,
+ primaryContainer = MullvadBlue40,
onSurfaceVariant = MullvadWhite,
onPrimary = MullvadWhite
)
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/theme/dimensions/Dimensions.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/theme/dimensions/Dimensions.kt
index 04904ac410..57c9f173a9 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/theme/dimensions/Dimensions.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/theme/dimensions/Dimensions.kt
@@ -11,7 +11,11 @@ data class Dimensions(
val listItemHeightExtra: Dp = 60.dp,
val loadingSpinnerSize: Dp = 24.dp,
val loadingSpinnerStrokeWidth: Dp = 3.dp,
- val loadingSpinnerPadding: Dp = 12.dp
+ val loadingSpinnerPadding: Dp = 12.dp,
+ val cellLeftPadding: Dp = 22.dp,
+ val cellRightPadding: Dp = 16.dp,
+ val listIconSize: Dp = 24.dp,
+ val progressIndicatorSize: Dp = 60.dp
)
val defaultDimensions = Dimensions()
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/ListItemsAdapter.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/ListItemsAdapter.kt
deleted file mode 100644
index 4676a3532a..0000000000
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/ListItemsAdapter.kt
+++ /dev/null
@@ -1,117 +0,0 @@
-package net.mullvad.mullvadvpn.ui
-
-import android.view.ViewGroup
-import androidx.recyclerview.widget.AsyncDifferConfig
-import androidx.recyclerview.widget.AsyncListDiffer
-import androidx.recyclerview.widget.DiffUtil
-import androidx.recyclerview.widget.ListUpdateCallback
-import androidx.recyclerview.widget.RecyclerView
-import java.util.concurrent.atomic.AtomicLong
-import net.mullvad.mullvadvpn.model.ListItemData
-import net.mullvad.mullvadvpn.ui.listitemview.ActionListItemView
-import net.mullvad.mullvadvpn.ui.listitemview.ApplicationListItemView
-import net.mullvad.mullvadvpn.ui.listitemview.DividerGroupListItemView
-import net.mullvad.mullvadvpn.ui.listitemview.ListItemView
-import net.mullvad.mullvadvpn.ui.listitemview.PlainListItemView
-import net.mullvad.mullvadvpn.ui.listitemview.ProgressListItemView
-import net.mullvad.mullvadvpn.ui.listitemview.TwoActionListItemView
-
-class ListItemsAdapter : RecyclerView.Adapter<ListItemsAdapter.ViewHolder>() {
-
- var listItemListener: ListItemListener? = null
-
- protected var diffCallback: DiffCallback = DefaultDiffCallback()
-
- private val listDiffer: AsyncListDiffer<ListItemData> = createDiffer(diffCallback)
-
- fun setItems(items: List<ListItemData?>) = listDiffer.submitList(items)
-
- override fun onCreateViewHolder(
- parent: ViewGroup,
- @ListItemData.ItemType viewType: Int
- ): ListItemsAdapter.ViewHolder {
- return ViewHolder(
- when (viewType) {
- ListItemData.DIVIDER -> DividerGroupListItemView(parent.context)
- ListItemData.PROGRESS -> ProgressListItemView(parent.context)
- ListItemData.PLAIN -> PlainListItemView(parent.context)
- ListItemData.ACTION -> ActionListItemView(parent.context)
- ListItemData.APPLICATION -> ApplicationListItemView(parent.context)
- ListItemData.DOUBLE_ACTION -> TwoActionListItemView(parent.context)
- else -> throw IllegalArgumentException("View type '$viewType' is not supported")
- }
- )
- }
-
- override fun onBindViewHolder(holder: ViewHolder, position: Int) {
- (holder.itemView as ListItemView).update(getItem(position))
- (holder.itemView as ListItemView).listItemListener = listItemListener
- }
-
- override fun onViewRecycled(holder: ViewHolder) {
- super.onViewRecycled(holder)
- (holder.itemView as ListItemView).listItemListener = null
- }
-
- override fun getItemCount(): Int = listDiffer.currentList.size
-
- @ListItemData.ItemType override fun getItemViewType(position: Int): Int = getItem(position).type
-
- override fun getItemId(position: Int): Long = getId(getItem(position).identifier)
-
- private fun getItem(position: Int): ListItemData = listDiffer.currentList[position]
-
- private fun createDiffer(diffCallback: DiffCallback): AsyncListDiffer<ListItemData> {
- return AsyncListDiffer(getListUpdateCallback(), getConfig(diffCallback))
- }
-
- private fun getConfig(diffUtil: DiffCallback): AsyncDifferConfig<ListItemData> {
- return AsyncDifferConfig.Builder(diffUtil).build()
- }
-
- protected fun getListUpdateCallback(): ListUpdateCallback {
- return object : ListUpdateCallback {
- override fun onInserted(position: Int, count: Int) {
- notifyItemRangeInserted(position, count)
- }
-
- override fun onRemoved(position: Int, count: Int) {
- notifyItemRangeRemoved(position, count)
- }
-
- override fun onMoved(fromPosition: Int, toPosition: Int) {
- notifyItemMoved(fromPosition, toPosition)
- }
-
- override fun onChanged(position: Int, count: Int, payload: Any?) {
- notifyItemRangeChanged(position, count, payload)
- }
- }
- }
-
- internal class DefaultDiffCallback : DiffCallback() {
- override fun areItemsTheSame(oldItem: ListItemData, newItem: ListItemData): Boolean {
- return oldItem.type == newItem.type && oldItem.identifier == newItem.identifier
- }
-
- override fun areContentsTheSame(oldItem: ListItemData, newItem: ListItemData): Boolean {
- return oldItem == newItem
- }
-
- override fun getChangePayload(oldItem: ListItemData, newItem: ListItemData): Any {
- return Any()
- }
- }
-
- inner class ViewHolder(view: ListItemView) : RecyclerView.ViewHolder(view)
-
- companion object StableIdProvider {
- private val idCounter = AtomicLong(0)
- private val mapIds = hashMapOf<String, Long>()
-
- internal fun getId(stringId: String): Long =
- mapIds.computeIfAbsent(stringId) { idCounter.decrementAndGet() }
- }
-}
-
-typealias DiffCallback = DiffUtil.ItemCallback<ListItemData>
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 cc7eea32f3..910588e580 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
@@ -18,7 +18,7 @@ import org.koin.androidx.viewmodel.scope.viewModel
import org.koin.core.qualifier.named
import org.koin.core.scope.Scope
-class SplitTunnelingFragment : BaseFragment(R.layout.collapsed_title_layout) {
+class SplitTunnelingFragment : BaseFragment() {
private val scope: Scope =
getKoin().getOrCreateScope(APPS_SCOPE, named(APPS_SCOPE)).also { appsScope ->
getKoin().getScopeOrNull(SERVICE_CONNECTION_SCOPE)?.let { serviceConnectionScope ->
@@ -39,9 +39,9 @@ class SplitTunnelingFragment : BaseFragment(R.layout.collapsed_title_layout) {
val state = viewModel.uiState.collectAsState().value
SplitTunnelingScreen(
uiState = state,
- onShowSystemAppsClicked = viewModel::setShowSystemApps,
- addToExcluded = viewModel::addToExcluded,
- removeFromExcluded = viewModel::removeFromExcluded,
+ onShowSystemAppsClicked = viewModel::onShowSystemAppsClicked,
+ onExcludeAppClick = viewModel::onExcludeAppClick,
+ onIncludeAppClick = viewModel::onIncludeAppClick,
onBackClick = { activity?.onBackPressedDispatcher?.onBackPressed() }
)
}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/listitemview/ApplicationListItemView.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/listitemview/ApplicationListItemView.kt
deleted file mode 100644
index 138e9bfb88..0000000000
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/listitemview/ApplicationListItemView.kt
+++ /dev/null
@@ -1,76 +0,0 @@
-package net.mullvad.mullvadvpn.ui.listitemview
-
-import android.content.Context
-import android.content.pm.PackageManager
-import android.graphics.drawable.Drawable
-import android.util.AttributeSet
-import androidx.core.content.res.ResourcesCompat
-import androidx.core.view.isVisible
-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 net.mullvad.mullvadvpn.di.APPS_SCOPE
-import org.koin.core.component.KoinApiExtension
-import org.koin.core.scope.KoinScopeComponent
-import org.koin.core.scope.Scope
-import org.koin.core.scope.inject
-
-@OptIn(KoinApiExtension::class)
-class ApplicationListItemView
-@JvmOverloads
-constructor(
- context: Context,
- attrs: AttributeSet? = null,
- defStyleAttr: Int = R.attr.applicationListItemViewStyle,
- defStyleRes: Int = 0
-) : ActionListItemView(context, attrs, defStyleAttr, defStyleRes), KoinScopeComponent {
- private val viewScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
- private val iconManager: ApplicationsIconManager by inject()
-
- private var updateImageJob: Job? = null
-
- override val scope: Scope = getKoin().getScope(APPS_SCOPE)
-
- init {
- itemText.setTextAppearance(R.style.TextAppearance_Mullvad_Title2)
- updateImage(ResourcesCompat.getDrawable(resources, R.drawable.ic_icons_missing, null)!!)
- }
-
- override fun updateImage() {
- itemIcon.isVisible = true
- updateImageJob?.cancel()
- updateImageJob = viewScope.launch { loadImage()?.let { drawable -> updateImage(drawable) } }
- }
-
- override fun updateText() {
- itemData.text?.let { itemText.text = it }
- }
-
- 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(itemData.identifier)
- } catch (e: PackageManager.NameNotFoundException) {
- null
- }
- }
-
- private fun updateImage(drawable: Drawable) = itemIcon.setImageDrawable(drawable)
-}
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
index 53970b7eef..8c840357e0 100644
--- 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
@@ -16,9 +16,9 @@ import kotlinx.coroutines.withContext
import net.mullvad.mullvadvpn.R
import net.mullvad.mullvadvpn.applist.ApplicationsIconManager
import net.mullvad.mullvadvpn.di.APPS_SCOPE
+import org.koin.core.scope.inject
import org.koin.core.scope.KoinScopeComponent
import org.koin.core.scope.Scope
-import org.koin.core.scope.inject
class ApplicationImageView
@JvmOverloads
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SplitTunnelingViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SplitTunnelingViewModel.kt
index e2309dbabb..38803cfc21 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SplitTunnelingViewModel.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SplitTunnelingViewModel.kt
@@ -62,16 +62,16 @@ class SplitTunnelingViewModel(
splitTunneling.persist()
super.onCleared()
}
-
- fun removeFromExcluded(packageName: String) {
+
+ fun onIncludeAppClick(packageName: String) {
viewModelScope.launch(dispatcher) { splitTunneling.includeApp(packageName) }
}
- fun addToExcluded(packageName: String) {
+ fun onExcludeAppClick(packageName: String) {
viewModelScope.launch(dispatcher) { splitTunneling.excludeApp(packageName) }
}
- fun setShowSystemApps(show: Boolean) {
+ fun onShowSystemAppsClicked(show: Boolean) {
viewModelScope.launch(dispatcher) { showSystemApps.emit(show) }
}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SplitTunnelingViewModelState.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SplitTunnelingViewModelState.kt
index 8ea7106247..eb798eb2c5 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SplitTunnelingViewModelState.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SplitTunnelingViewModelState.kt
@@ -12,7 +12,7 @@ data class SplitTunnelingViewModelState(
return allApps
?.partition { appData -> excludedApps.contains(appData.packageName) }
?.let { (excluded, included) ->
- SplitTunnelingUiState.Data(
+ SplitTunnelingUiState.ShowAppList(
excludedApps = excluded,
includedApps =
if (showSystemApps) {
diff --git a/android/app/src/main/res/layout/app_list_item.xml b/android/app/src/main/res/layout/app_list_item.xml
deleted file mode 100644
index eebfccf88e..0000000000
--- a/android/app/src/main/res/layout/app_list_item.xml
+++ /dev/null
@@ -1,39 +0,0 @@
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:paddingHorizontal="@dimen/side_margin"
- android:background="@drawable/app_list_item_background"
- android:orientation="horizontal"
- android:gravity="center"
- android:clickable="true"
- android:focusable="true">
- <ProgressBar android:id="@+id/loading"
- android:layout_width="@dimen/app_list_item_icon_size"
- android:layout_height="@dimen/app_list_item_icon_size"
- android:layout_gravity="center"
- android:layout_marginRight="4dp"
- android:indeterminate="true"
- android:indeterminateOnly="true"
- android:indeterminateDuration="600"
- android:indeterminateDrawable="@drawable/icon_spinner"
- android:visibility="visible" />
- <ImageView android:id="@+id/icon"
- android:layout_width="@dimen/app_list_item_icon_size"
- android:layout_height="@dimen/app_list_item_icon_size"
- android:layout_gravity="center"
- android:layout_marginRight="4dp"
- android:visibility="gone" />
- <TextView android:id="@+id/name"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_weight="1"
- android:layout_marginHorizontal="8dp"
- android:layout_marginVertical="16dp"
- android:textColor="@color/white"
- android:textSize="@dimen/text_medium"
- android:text="" />
- <net.mullvad.mullvadvpn.ui.widget.CellSwitch android:id="@+id/excluded"
- android:layout_width="@dimen/cell_switch_width"
- android:layout_height="@dimen/cell_switch_height"
- android:layout_weight="0" />
-</LinearLayout>
diff --git a/android/app/src/main/res/layout/collapsed_title_layout.xml b/android/app/src/main/res/layout/collapsed_title_layout.xml
deleted file mode 100644
index 3d755ba65b..0000000000
--- a/android/app/src/main/res/layout/collapsed_title_layout.xml
+++ /dev/null
@@ -1,65 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto"
- xmlns:tools="http://schemas.android.com/tools"
- android:id="@+id/main_content"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:background="@color/darkBlue"
- android:fitsSystemWindows="false">
- <com.google.android.material.appbar.AppBarLayout android:id="@+id/appbar"
- android:layout_width="match_parent"
- android:layout_height="@dimen/expanded_toolbar_height"
- android:background="@color/darkBlue"
- android:fitsSystemWindows="false"
- android:padding="0dp"
- android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">
-
- <com.google.android.material.appbar.CollapsingToolbarLayout android:id="@+id/collapsing_toolbar"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:fitsSystemWindows="false"
- app:collapsedTitleGravity="center"
- app:collapsedTitleTextAppearance="@style/TextAppearance.Mullvad.CollapsingToolbar.Collapsed"
- app:contentScrim="@color/darkBlue"
- app:expandedTitleGravity="start|bottom"
- app:expandedTitleMarginBottom="20dp"
- app:expandedTitleMarginStart="22dp"
- app:expandedTitleMarginTop="0dp"
- app:expandedTitleTextAppearance="@style/TextAppearance.Mullvad.CollapsingToolbar.Expanded"
- app:layout_scrollFlags="scroll|exitUntilCollapsed"
- app:title="@string/split_tunneling">
-
- <com.google.android.material.appbar.MaterialToolbar android:id="@+id/toolbar"
- android:layout_width="match_parent"
- android:layout_height="?attr/actionBarSize"
- android:padding="0dp"
- app:layout_collapseMode="pin"
- app:layout_scrollFlags="scroll|enterAlways"
- android:contentInsetLeft="0dp"
- android:contentInsetStart="0dp"
- app:contentInsetLeft="0dp"
- app:contentInsetStart="0dp"
- android:contentInsetRight="0dp"
- android:contentInsetEnd="0dp"
- app:contentInsetRight="0dp"
- app:contentInsetEnd="0dp"
- app:contentInsetStartWithNavigation="0dp"
- app:popupTheme="@style/ThemeOverlay.AppCompat.Light" />
- <net.mullvad.mullvadvpn.ui.widget.BackButton android:id="@+id/back"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- app:layout_collapseMode="pin"
- app:layout_collapseParallaxMultiplier="0"
- app:layout_scrollFlags="enterAlwaysCollapsed|enterAlways"
- app:text="@string/settings_vpn" />
- </com.google.android.material.appbar.CollapsingToolbarLayout>
- </com.google.android.material.appbar.AppBarLayout>
- <androidx.recyclerview.widget.RecyclerView android:id="@+id/recyclerView"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- tools:listitem="@layout/app_list_item"
- tools:itemCount="15"
- app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
- app:layout_behavior="@string/appbar_scrolling_view_behavior" />
-</androidx.coordinatorlayout.widget.CoordinatorLayout>