diff options
Diffstat (limited to 'android/app/src')
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> |
