summaryrefslogtreecommitdiffhomepage
path: root/android/app/src
diff options
context:
space:
mode:
Diffstat (limited to 'android/app/src')
-rw-r--r--android/app/src/debug/AndroidManifest.xml2
-rw-r--r--android/app/src/main/AndroidManifest.xml10
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/notificationbanner/NotificationBanner.kt91
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/RedeemVoucherDialog.kt11
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/extensions/ResourcesExtensions.kt18
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/LoginScreen.kt7
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/textfield/CustomTextField.kt17
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/EditApiAccessMethodViewModel.kt1
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SelectLocationViewModel.kt1
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/VpnSettingsViewModel.kt1
-rw-r--r--android/app/src/main/res/xml/data_extraction_rules.xml21
-rw-r--r--android/app/src/main/res/xml/full_backup_content.xml16
-rw-r--r--android/app/src/test/kotlin/net/mullvad/mullvadvpn/applist/ApplicationsProviderTest.kt3
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,