diff options
Diffstat (limited to 'android/app/src')
13 files changed, 131 insertions, 68 deletions
diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml index cfe2c7c4df..f428b876c7 100644 --- a/android/app/src/debug/AndroidManifest.xml +++ b/android/app/src/debug/AndroidManifest.xml @@ -11,7 +11,7 @@ android:theme="@style/Theme.App.Starting" android:extractNativeLibs="true" android:allowBackup="false" - android:banner="@drawable/banner" + android:banner="@mipmap/ic_banner" android:name=".MullvadApplication" tools:ignore="DataExtractionRules,GoogleAppIndexingWarning"/> </manifest> diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 8dc3c4e385..24c430f444 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -3,7 +3,10 @@ <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.INTERNET" /> - <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" /> + <!-- Suppress warning, just using queries tag is not enough for our all, we need access to all + packages to allow the user to select apps for split tunneling --> + <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" + tools:ignore="QueryAllPackagesPermission" /> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <!-- https://developer.android.com/guide/components/fg-service-types#system-exempted --> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_SYSTEM_EXEMPTED" /> @@ -20,9 +23,12 @@ android:required="false" /> <uses-feature android:glEsVersion="0x00020000" android:required="false" /> + <application android:name=".MullvadApplication" + android:banner="@mipmap/ic_banner" android:allowBackup="false" - android:banner="@drawable/banner" + android:fullBackupContent="@xml/full_backup_content" + android:dataExtractionRules="@xml/data_extraction_rules" android:extractNativeLibs="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/notificationbanner/NotificationBanner.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/notificationbanner/NotificationBanner.kt index a2485f2e99..df7d3dede0 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/notificationbanner/NotificationBanner.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/notificationbanner/NotificationBanner.kt @@ -18,6 +18,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.testTag import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview @@ -118,23 +119,13 @@ private fun Notification(notificationBannerData: NotificationData) { .testTag(NOTIFICATION_BANNER) ) { val (status, textTitle, textMessage, actionIcon) = createRefs() - Box( - modifier = - Modifier.background( - color = - when (statusLevel) { - StatusLevel.Error -> MaterialTheme.colorScheme.error - StatusLevel.Warning -> MaterialTheme.colorScheme.warning - StatusLevel.Info -> MaterialTheme.colorScheme.surfaceContainer - }, - shape = CircleShape, - ) - .size(Dimens.notificationStatusIconSize) - .constrainAs(status) { - top.linkTo(textTitle.top) - start.linkTo(parent.start) - bottom.linkTo(textTitle.bottom) - } + NotificationDot( + statusLevel, + Modifier.constrainAs(status) { + top.linkTo(textTitle.top) + start.linkTo(parent.start) + bottom.linkTo(textTitle.bottom) + }, ) Text( text = title.uppercase(), @@ -173,24 +164,58 @@ private fun Notification(notificationBannerData: NotificationData) { ) } action?.let { - IconButton( + NotificationAction( + it.icon, + onClick = it.onClick, + contentDescription = it.contentDescription, modifier = Modifier.constrainAs(actionIcon) { - top.linkTo(parent.top) - end.linkTo(parent.end) - bottom.linkTo(parent.bottom) - } - .testTag(NOTIFICATION_BANNER_ACTION) - .padding(all = Dimens.notificationEndIconPadding), - onClick = it.onClick, - ) { - Icon( - modifier = Modifier.padding(Dimens.notificationIconPadding), - imageVector = it.icon, - contentDescription = it.contentDescription, - tint = MaterialTheme.colorScheme.onSurface, - ) - } + top.linkTo(parent.top) + end.linkTo(parent.end) + bottom.linkTo(parent.bottom) + }, + ) } } } + +@Composable +private fun NotificationDot(statusLevel: StatusLevel, modifier: Modifier) { + Box( + modifier = + modifier + .background( + color = + when (statusLevel) { + StatusLevel.Error -> MaterialTheme.colorScheme.error + StatusLevel.Warning -> MaterialTheme.colorScheme.warning + StatusLevel.Info -> MaterialTheme.colorScheme.surfaceContainer + }, + shape = CircleShape, + ) + .size(Dimens.notificationStatusIconSize) + ) +} + +@Composable +private fun NotificationAction( + imageVector: ImageVector, + contentDescription: String?, + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + IconButton( + modifier = + modifier + .testTag(NOTIFICATION_BANNER_ACTION) + .padding(all = Dimens.notificationEndIconPadding), + onClick = onClick, + ) { + Icon( + modifier = Modifier.padding(Dimens.notificationIconPadding), + imageVector = imageVector, + contentDescription = contentDescription, + tint = MaterialTheme.colorScheme.onSurface, + ) + } +} diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/RedeemVoucherDialog.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/RedeemVoucherDialog.kt index 3d49a7afca..04f3c05a11 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/RedeemVoucherDialog.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/RedeemVoucherDialog.kt @@ -41,6 +41,7 @@ import net.mullvad.mullvadvpn.compose.textfield.CustomTextField import net.mullvad.mullvadvpn.compose.util.MAX_VOUCHER_LENGTH import net.mullvad.mullvadvpn.compose.util.vouchersVisualTransformation import net.mullvad.mullvadvpn.constant.VOUCHER_LENGTH +import net.mullvad.mullvadvpn.lib.model.DAYS_PER_VOUCHER_MONTH import net.mullvad.mullvadvpn.lib.model.RedeemVoucherError import net.mullvad.mullvadvpn.lib.theme.AppTheme import net.mullvad.mullvadvpn.lib.theme.Dimens @@ -162,18 +163,18 @@ fun RedeemVoucherDialog( val message = stringResource( R.string.added_to_your_account, - when (days) { - 0 -> { + when { + days == 0 -> { stringResource(R.string.less_than_one_day) } - in 1..59 -> { + days < 2 * DAYS_PER_VOUCHER_MONTH -> { pluralStringResource(id = R.plurals.days, count = days, days) } else -> { pluralStringResource( id = R.plurals.months, - count = days / 30, - days / 30, + count = days / DAYS_PER_VOUCHER_MONTH, + days / DAYS_PER_VOUCHER_MONTH, ) } }, diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/extensions/ResourcesExtensions.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/extensions/ResourcesExtensions.kt index 11b41dd27a..9611897e76 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/extensions/ResourcesExtensions.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/extensions/ResourcesExtensions.kt @@ -2,20 +2,20 @@ package net.mullvad.mullvadvpn.compose.extensions import android.content.res.Resources import net.mullvad.mullvadvpn.R -import org.joda.time.DateTime import org.joda.time.Duration -import org.joda.time.Period + +private const val DAYS_IN_STANDARD_YEAR = 365 fun Resources.getExpiryQuantityString(accountExpiry: Duration): String { - val expiryPeriod = Period(DateTime.now(), accountExpiry) + val days = accountExpiry.standardDays.toInt() + val years = (accountExpiry.standardDays / DAYS_IN_STANDARD_YEAR).toInt() + return if (accountExpiry.millis <= 0) { getString(R.string.out_of_time) - } else if (expiryPeriod.years > 0) { - getRemainingText(this, R.plurals.years_left, expiryPeriod.years) - } else if (expiryPeriod.months >= 3) { - getRemainingText(this, R.plurals.months_left, expiryPeriod.months) - } else if (expiryPeriod.months > 0 || expiryPeriod.days >= 1) { - getRemainingText(this, R.plurals.days_left, expiryPeriod.days) + } else if (years > 1) { + getRemainingText(this, R.plurals.years_left, years) + } else if (days >= 1) { + getRemainingText(this, R.plurals.days_left, days) } else { getString(R.string.less_than_a_day_left) } diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/LoginScreen.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/LoginScreen.kt index 9dcd016767..7a3d338022 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/LoginScreen.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/LoginScreen.kt @@ -94,6 +94,9 @@ private fun PreviewLoginScreen( AppTheme { LoginScreen(state = state) } } +private const val TOP_SPACER_WEIGHT = 1f +private const val BOTTOM_SPACER_WEIGHT = 3f + @Destination<RootGraph>(style = LoginTransition::class) @Composable fun Login( @@ -177,7 +180,7 @@ private fun LoginScreen( .background(MaterialTheme.colorScheme.primary) .verticalScroll(scrollState) ) { - Spacer(modifier = Modifier.weight(1f)) + Spacer(modifier = Modifier.weight(TOP_SPACER_WEIGHT)) LoginIcon( state.loginState, modifier = @@ -185,7 +188,7 @@ private fun LoginScreen( .padding(bottom = Dimens.largePadding), ) LoginContent(state, onAccountNumberChange, onLoginClick, onDeleteHistoryClick) - Spacer(modifier = Modifier.weight(3f)) + Spacer(modifier = Modifier.weight(BOTTOM_SPACER_WEIGHT)) CreateAccountPanel(onCreateAccountClick, isEnabled = state.loginState is Idle) } } diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/textfield/CustomTextField.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/textfield/CustomTextField.kt index b64de576ee..5bdcc961e7 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/textfield/CustomTextField.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/textfield/CustomTextField.kt @@ -13,7 +13,6 @@ import androidx.compose.runtime.SideEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -23,8 +22,6 @@ import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.input.VisualTransformation -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch import net.mullvad.mullvadvpn.constant.EMPTY_STRING import net.mullvad.mullvadvpn.constant.NEWLINE_STRING @@ -53,9 +50,6 @@ fun CustomTextField( imeAction = ImeAction.Done, ), ) { - - val scope = rememberCoroutineScope() - // This is the same implementation as in BasicTextField.kt but with initial selection set at the // end of the text rather than in the beginning. // This is a fix for https://issuetracker.google.com/issues/272693535. @@ -97,16 +91,7 @@ fun CustomTextField( singleLine = true, placeholder = placeholderText?.let { { Text(text = it) } }, keyboardOptions = keyboardOptions, - keyboardActions = - KeyboardActions( - onDone = { - scope.launch { - // https://issuetracker.google.com/issues/305518328 - delay(100) - onSubmit(value) - } - } - ), + keyboardActions = KeyboardActions(onDone = { onSubmit(value) }), visualTransformation = visualTransformation, colors = colors, isError = !isValidValue, diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/EditApiAccessMethodViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/EditApiAccessMethodViewModel.kt index 4d85ae9868..48e4ac85ac 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/EditApiAccessMethodViewModel.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/EditApiAccessMethodViewModel.kt @@ -38,6 +38,7 @@ import net.mullvad.mullvadvpn.repository.ApiAccessRepository import net.mullvad.mullvadvpn.util.delayAtLeast import org.apache.commons.validator.routines.InetAddressValidator +@Suppress("TooManyFunctions") class EditApiAccessMethodViewModel( private val apiAccessRepository: ApiAccessRepository, private val inetAddressValidator: InetAddressValidator, diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SelectLocationViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SelectLocationViewModel.kt index c34b182aa6..4ddad8477b 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SelectLocationViewModel.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SelectLocationViewModel.kt @@ -43,6 +43,7 @@ import net.mullvad.mullvadvpn.usecase.customlists.CustomListActionUseCase import net.mullvad.mullvadvpn.usecase.customlists.CustomListsRelayItemUseCase import net.mullvad.mullvadvpn.usecase.customlists.FilterCustomListsRelayItemUseCase +@Suppress("TooManyFunctions") class SelectLocationViewModel( private val relayListFilterRepository: RelayListFilterRepository, private val availableProvidersUseCase: AvailableProvidersUseCase, diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/VpnSettingsViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/VpnSettingsViewModel.kt index 7829d0ce24..5ca136341e 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/VpnSettingsViewModel.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/VpnSettingsViewModel.kt @@ -43,6 +43,7 @@ sealed interface VpnSettingsSideEffect { data object NavigateToDnsDialog : VpnSettingsSideEffect } +@Suppress("TooManyFunctions") class VpnSettingsViewModel( private val repository: SettingsRepository, private val relayListRepository: RelayListRepository, diff --git a/android/app/src/main/res/xml/data_extraction_rules.xml b/android/app/src/main/res/xml/data_extraction_rules.xml new file mode 100644 index 0000000000..b762ef705c --- /dev/null +++ b/android/app/src/main/res/xml/data_extraction_rules.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + Sample data extraction rules file; uncomment and customize as necessary. + See https://developer.android.com/about/versions/12/backup-restore#xml-changes + for details. +--> +<data-extraction-rules> + <cloud-backup> + <exclude domain="root" /> + <exclude domain="file" /> + <exclude domain="database" /> + <exclude domain="sharedpref" /> + <exclude domain="external" /> + </cloud-backup> + <device-transfer> + <exclude domain="root" /> + <exclude domain="file" /> + <exclude domain="database" /> + <exclude domain="sharedpref" /> + <exclude domain="external" /> + </device-transfer> +</data-extraction-rules> diff --git a/android/app/src/main/res/xml/full_backup_content.xml b/android/app/src/main/res/xml/full_backup_content.xml new file mode 100644 index 0000000000..e88e6597d9 --- /dev/null +++ b/android/app/src/main/res/xml/full_backup_content.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + Full backup content file + See https://developer.android.com/guide/topics/manifest/application-element#fullBackupContent + for details. +--> +<full-backup-content> + <exclude domain="file" /> + <exclude domain="database" /> + <exclude domain="sharedpref" /> + <exclude domain="external" /> + <exclude domain="root" /> + <exclude domain="device_file" /> + <exclude domain="device_database" /> + <exclude domain="device_sharedpref" /> + <exclude domain="device_root" /> +</full-backup-content> diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/applist/ApplicationsProviderTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/applist/ApplicationsProviderTest.kt index 560dafb24a..efa97c6ab0 100644 --- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/applist/ApplicationsProviderTest.kt +++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/applist/ApplicationsProviderTest.kt @@ -1,6 +1,7 @@ package net.mullvad.mullvadvpn.applist import android.Manifest +import android.annotation.SuppressLint import android.content.pm.ApplicationInfo import android.content.pm.PackageManager import io.mockk.every @@ -22,6 +23,7 @@ class ApplicationsProviderTest { unmockkAll() } + @SuppressLint("UseCheckPermission") @Test fun `fetch all apps should work`() { val launchWithInternetPackageName = "launch_with_internet_package_name" @@ -75,6 +77,7 @@ class ApplicationsProviderTest { verifyAll { mockedPackageManager.getInstalledApplications(PackageManager.GET_META_DATA) + // Ensure checkPermission was invoked on all packages listOf( launchWithInternetPackageName, launchWithoutInternetPackageName, |
