summaryrefslogtreecommitdiffhomepage
path: root/android/app/src
diff options
context:
space:
mode:
Diffstat (limited to 'android/app/src')
-rw-r--r--android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/ChangelogDialogTest.kt74
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/ChangelogDialog.kt95
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/List.kt44
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/UiModule.kt25
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/ChangelogRepository.kt24
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/MainActivity.kt33
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/ChangelogDataProvider.kt23
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ChangelogViewModel.kt41
-rw-r--r--android/app/src/main/res/layout/main.xml7
-rw-r--r--android/app/src/main/res/values/dimensions.xml3
-rw-r--r--android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/ChangelogViewModelTest.kt97
11 files changed, 465 insertions, 1 deletions
diff --git a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/ChangelogDialogTest.kt b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/ChangelogDialogTest.kt
new file mode 100644
index 0000000000..dab5bf0a60
--- /dev/null
+++ b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/ChangelogDialogTest.kt
@@ -0,0 +1,74 @@
+package net.mullvad.mullvadvpn.compose.screen
+
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.test.performClick
+import io.mockk.MockKAnnotations
+import io.mockk.Runs
+import io.mockk.every
+import io.mockk.impl.annotations.MockK
+import io.mockk.just
+import io.mockk.verify
+import kotlinx.coroutines.flow.MutableStateFlow
+import net.mullvad.mullvadvpn.compose.component.AppTheme
+import net.mullvad.mullvadvpn.compose.component.ChangelogDialog
+import net.mullvad.mullvadvpn.viewmodel.ChangelogDialogUiState
+import net.mullvad.mullvadvpn.viewmodel.ChangelogViewModel
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+
+class ChangelogDialogTest {
+ @get:Rule
+ val composeTestRule = createComposeRule()
+
+ @MockK
+ lateinit var mockedViewModel: ChangelogViewModel
+
+ @Before
+ fun setup() {
+ MockKAnnotations.init(this)
+ }
+
+ @Test
+ fun testShowChangeLogWhenNeeded() {
+ // Arrange
+ every {
+ mockedViewModel.changelogDialogUiState
+ } returns MutableStateFlow(ChangelogDialogUiState.Show(listOf(CHANGELOG_ITEM)))
+ every {
+ mockedViewModel.dismissChangelogDialog()
+ } just Runs
+
+ composeTestRule.setContent {
+ AppTheme {
+ ChangelogDialog(
+ changesList = listOf(CHANGELOG_ITEM),
+ version = CHANGELOG_VERSION,
+ onDismiss = {
+ mockedViewModel.dismissChangelogDialog()
+ }
+ )
+ }
+ }
+
+ // Check changelog content showed within dialog
+ composeTestRule
+ .onNodeWithText(CHANGELOG_ITEM)
+ .assertExists()
+
+ // perform click on Got It button to check if dismiss occur
+ composeTestRule
+ .onNodeWithText(CHANGELOG_BUTTON_TEXT)
+ .performClick()
+
+ // Assert
+ verify { mockedViewModel.dismissChangelogDialog() }
+ }
+
+ companion object {
+ private const val CHANGELOG_BUTTON_TEXT = "Got it!"
+ private const val CHANGELOG_ITEM = "Changelog item"
+ private const val CHANGELOG_VERSION = "1234.5"
+ }
+}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/ChangelogDialog.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/ChangelogDialog.kt
new file mode 100644
index 0000000000..976abc5364
--- /dev/null
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/ChangelogDialog.kt
@@ -0,0 +1,95 @@
+package net.mullvad.mullvadvpn.compose.component
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.defaultMinSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.wrapContentHeight
+import androidx.compose.material.AlertDialog
+import androidx.compose.material.Button
+import androidx.compose.material.ButtonDefaults
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.colorResource
+import androidx.compose.ui.res.dimensionResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.font.FontStyle
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.sp
+import androidx.compose.ui.window.DialogProperties
+import net.mullvad.mullvadvpn.R
+
+@Composable
+fun ChangelogDialog(
+ changesList: List<String>,
+ version: String,
+ onDismiss: () -> Unit
+) {
+ AlertDialog(
+ onDismissRequest = {
+ onDismiss()
+ },
+ title = {
+ Text(
+ text = version,
+ color = colorResource(id = R.color.white),
+ fontSize = 30.sp,
+ fontStyle = FontStyle.Normal,
+ textAlign = TextAlign.Center,
+ modifier = Modifier
+ .fillMaxWidth()
+ )
+ },
+
+ text = {
+ Column {
+ Text(
+ text = stringResource(R.string.changes_dialog_subtitle),
+ fontSize = 18.sp,
+ color = Color.White,
+ modifier = Modifier
+ .padding(
+ vertical = dimensionResource(id = R.dimen.medium_padding)
+ )
+ )
+
+ changesList.forEach { changeItem ->
+ ChangeListItem(
+ text = changeItem
+ )
+ }
+ }
+ },
+ buttons = {
+ Button(
+ modifier = Modifier
+ .wrapContentHeight()
+ .padding(all = dimensionResource(id = R.dimen.medium_padding))
+ .defaultMinSize(
+ minHeight = dimensionResource(id = R.dimen.button_height)
+ )
+ .fillMaxWidth(),
+ colors = ButtonDefaults.buttonColors(
+ backgroundColor = colorResource(id = R.color.blue),
+ contentColor = colorResource(id = R.color.white)
+ ),
+ onClick = {
+ onDismiss()
+ }
+
+ ) {
+ Text(
+ text = stringResource(R.string.changes_dialog_dismiss_button),
+ fontSize = 18.sp
+ )
+ }
+ },
+ properties = DialogProperties(
+ dismissOnClickOutside = true,
+ dismissOnBackPress = true,
+ ),
+ backgroundColor = colorResource(id = R.color.darkBlue)
+ )
+}
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 3babb29a21..bd7aa0926b 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
@@ -5,6 +5,7 @@ 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.absolutePadding
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
@@ -16,9 +17,11 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.colorResource
+import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
+import androidx.constraintlayout.compose.ConstraintLayout
import net.mullvad.mullvadvpn.R
@Composable
@@ -73,3 +76,44 @@ fun ListItem(
}
}
}
+
+@Composable
+fun ChangeListItem(
+ text: String
+) {
+ ConstraintLayout {
+ val (bullet, changeLog) = createRefs()
+ val smallPadding = dimensionResource(id = R.dimen.small_padding)
+ Box(
+ modifier = Modifier
+ .constrainAs(bullet) {
+ top.linkTo(parent.top)
+ start.linkTo(parent.absoluteLeft)
+ }
+ ) {
+ Text(
+ text = "•",
+ fontSize = 14.sp,
+ color = Color.White
+ )
+ }
+ Box(
+ modifier = Modifier
+ .absolutePadding(left = dimensionResource(id = R.dimen.medium_padding))
+ .constrainAs(changeLog) {
+ top.linkTo(parent.top)
+ bottom.linkTo(parent.bottom, margin = smallPadding)
+ start.linkTo(parent.start)
+ end.linkTo(parent.end)
+ }
+ ) {
+ Text(
+ text = text,
+ fontSize = 14.sp,
+ color = Color.White,
+ modifier = Modifier
+
+ )
+ }
+ }
+}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/UiModule.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/UiModule.kt
index 1309a0e6cf..bdabbcaeff 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/UiModule.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/UiModule.kt
@@ -1,11 +1,15 @@
package net.mullvad.mullvadvpn.di
+import android.content.Context
+import android.content.SharedPreferences
import android.content.pm.PackageManager
import android.os.Messenger
import kotlinx.coroutines.Dispatchers
+import net.mullvad.mullvadvpn.BuildConfig
import net.mullvad.mullvadvpn.applist.ApplicationsIconManager
import net.mullvad.mullvadvpn.applist.ApplicationsProvider
import net.mullvad.mullvadvpn.ipc.EventDispatcher
+import net.mullvad.mullvadvpn.repository.ChangelogRepository
import net.mullvad.mullvadvpn.ui.notification.AccountExpiryNotification
import net.mullvad.mullvadvpn.ui.notification.TunnelStateNotification
import net.mullvad.mullvadvpn.ui.notification.VersionInfoNotification
@@ -13,11 +17,15 @@ import net.mullvad.mullvadvpn.ui.serviceconnection.AccountRepository
import net.mullvad.mullvadvpn.ui.serviceconnection.DeviceRepository
import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionManager
import net.mullvad.mullvadvpn.ui.serviceconnection.SplitTunneling
+import net.mullvad.mullvadvpn.util.ChangelogDataProvider
+import net.mullvad.mullvadvpn.util.IChangelogDataProvider
+import net.mullvad.mullvadvpn.viewmodel.ChangelogViewModel
import net.mullvad.mullvadvpn.viewmodel.ConnectViewModel
import net.mullvad.mullvadvpn.viewmodel.DeviceListViewModel
import net.mullvad.mullvadvpn.viewmodel.DeviceRevokedViewModel
import net.mullvad.mullvadvpn.viewmodel.LoginViewModel
import net.mullvad.mullvadvpn.viewmodel.SplitTunnelingViewModel
+import org.koin.android.ext.koin.androidApplication
import org.koin.android.ext.koin.androidContext
import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.core.qualifier.named
@@ -26,6 +34,10 @@ import org.koin.dsl.onClose
val uiModule = module {
+ single<SharedPreferences>(named(APP_PREFERENCES_NAME)) {
+ androidApplication().getSharedPreferences(APP_PREFERENCES_NAME, Context.MODE_PRIVATE)
+ }
+
single<PackageManager> { androidContext().packageManager }
single<String>(named(SELF_PACKAGE_NAME)) { androidContext().packageName }
@@ -43,6 +55,9 @@ val uiModule = module {
single { ServiceConnectionManager(androidContext()) }
single { androidContext().resources }
+ single { androidContext().assets }
+
+ single { ChangelogRepository(get(named(APP_PREFERENCES_NAME)), get()) }
single { AccountExpiryNotification(get()) }
single { TunnelStateNotification(get()) }
@@ -51,13 +66,23 @@ val uiModule = module {
single { AccountRepository(get()) }
single { DeviceRepository(get()) }
+ single<IChangelogDataProvider> { ChangelogDataProvider(get()) }
+
// View models
viewModel { ConnectViewModel() }
viewModel { DeviceRevokedViewModel(get(), get()) }
viewModel { DeviceListViewModel(get(), get()) }
viewModel { LoginViewModel(get(), get()) }
+ viewModel {
+ ChangelogViewModel(
+ get(),
+ BuildConfig.VERSION_CODE,
+ BuildConfig.ALWAYS_SHOW_CHANGELOG
+ )
+ }
}
const val APPS_SCOPE = "APPS_SCOPE"
const val SERVICE_CONNECTION_SCOPE = "SERVICE_CONNECTION_SCOPE"
const val SELF_PACKAGE_NAME = "SELF_PACKAGE_NAME"
+const val APP_PREFERENCES_NAME = "net.mullvad.mullvadvpn.app_preferences"
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/ChangelogRepository.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/ChangelogRepository.kt
new file mode 100644
index 0000000000..3e8150bf97
--- /dev/null
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/ChangelogRepository.kt
@@ -0,0 +1,24 @@
+package net.mullvad.mullvadvpn.repository
+
+import android.content.SharedPreferences
+import net.mullvad.mullvadvpn.util.IChangelogDataProvider
+
+private const val MISSING_VERSION_CODE = -1
+private const val NEWLINE_CHAR = '\n'
+private const val LAST_SHOWED_CHANGELOG_VERSION_CODE = "last_showed_changelog_version_code"
+
+class ChangelogRepository(
+ private val preferences: SharedPreferences,
+ private val dataProvider: IChangelogDataProvider
+) {
+ fun getVersionCodeOfMostRecentChangelogShowed(): Int {
+ return preferences.getInt(LAST_SHOWED_CHANGELOG_VERSION_CODE, MISSING_VERSION_CODE)
+ }
+
+ fun setVersionCodeOfMostRecentChangelogShowed(versionCode: Int) =
+ preferences.edit().putInt(LAST_SHOWED_CHANGELOG_VERSION_CODE, versionCode).apply()
+
+ fun getLastVersionChanges(): List<String> {
+ return dataProvider.getChangelog().split(NEWLINE_CHAR).filter { it.isNotEmpty() }
+ }
+}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/MainActivity.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/MainActivity.kt
index a72d74c917..becf82a69e 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/MainActivity.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/MainActivity.kt
@@ -12,6 +12,9 @@ import android.util.Log
import android.view.WindowManager
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
+import androidx.compose.runtime.collectAsState
+import androidx.compose.ui.platform.ComposeView
+import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.fragment.app.FragmentManager
@@ -28,6 +31,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withTimeoutOrNull
import net.mullvad.mullvadvpn.BuildConfig
import net.mullvad.mullvadvpn.R
+import net.mullvad.mullvadvpn.compose.component.ChangelogDialog
import net.mullvad.mullvadvpn.dataproxy.MullvadProblemReport
import net.mullvad.mullvadvpn.di.uiModule
import net.mullvad.mullvadvpn.model.AccountExpiry
@@ -39,6 +43,8 @@ import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionManager
import net.mullvad.mullvadvpn.util.SdkUtils.isNotificationPermissionGranted
import net.mullvad.mullvadvpn.util.UNKNOWN_STATE_DEBOUNCE_DELAY_MILLISECONDS
import net.mullvad.mullvadvpn.util.addDebounceForUnknownState
+import net.mullvad.mullvadvpn.viewmodel.ChangelogDialogUiState
+import net.mullvad.mullvadvpn.viewmodel.ChangelogViewModel
import org.koin.android.ext.android.getKoin
import org.koin.core.context.loadKoinModules
@@ -63,6 +69,7 @@ open class MainActivity : FragmentActivity() {
private lateinit var accountRepository: AccountRepository
private lateinit var deviceRepository: DeviceRepository
private lateinit var serviceConnectionManager: ServiceConnectionManager
+ private lateinit var changelogViewModel: ChangelogViewModel
override fun onCreate(savedInstanceState: Bundle?) {
loadKoinModules(uiModule)
@@ -71,6 +78,7 @@ open class MainActivity : FragmentActivity() {
accountRepository = get()
deviceRepository = get()
serviceConnectionManager = get()
+ changelogViewModel = get()
}
requestedOrientation = if (deviceIsTv) {
@@ -186,6 +194,31 @@ open class MainActivity : FragmentActivity() {
}
}
}
+ lifecycleScope.launch {
+ deviceRepository.deviceState
+ .flowWithLifecycle(lifecycle, Lifecycle.State.RESUMED)
+ .filter { it is DeviceState.LoggedIn || it is DeviceState.LoggedOut }
+ .collect { loadChangelogComponent() }
+ }
+ }
+
+ private fun loadChangelogComponent() {
+ findViewById<ComposeView>(R.id.compose_view).apply {
+ setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnDetachedFromWindow)
+ setContent {
+ val state = changelogViewModel.changelogDialogUiState.collectAsState().value
+ if (state is ChangelogDialogUiState.Show) {
+ ChangelogDialog(
+ changesList = state.changes,
+ version = BuildConfig.VERSION_NAME,
+ onDismiss = {
+ changelogViewModel.dismissChangelogDialog()
+ }
+ )
+ }
+ }
+ changelogViewModel.refreshChangelogDialogUiState()
+ }
}
@Suppress("DEPRECATION")
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/ChangelogDataProvider.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/ChangelogDataProvider.kt
new file mode 100644
index 0000000000..8ce36fd717
--- /dev/null
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/ChangelogDataProvider.kt
@@ -0,0 +1,23 @@
+package net.mullvad.mullvadvpn.util
+
+import android.content.res.AssetManager
+import android.util.Log
+import java.io.IOException
+
+private const val CHANGELOG_FILE = "en-US/default.txt"
+private const val EMPTY_DEFAULT_STRING_WHEN_UNABLE_TO_READ_CHANGELOG = ""
+
+class ChangelogDataProvider(var assets: AssetManager) : IChangelogDataProvider {
+ override fun getChangelog(): String {
+ return try {
+ assets.open(CHANGELOG_FILE).bufferedReader().use { it.readText() }
+ } catch (ex: IOException) {
+ Log.e("mullvad", "Unable to read bundled changelog file.")
+ EMPTY_DEFAULT_STRING_WHEN_UNABLE_TO_READ_CHANGELOG
+ }
+ }
+}
+
+interface IChangelogDataProvider {
+ fun getChangelog(): String
+}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ChangelogViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ChangelogViewModel.kt
new file mode 100644
index 0000000000..003eb962ad
--- /dev/null
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ChangelogViewModel.kt
@@ -0,0 +1,41 @@
+package net.mullvad.mullvadvpn.viewmodel
+
+import androidx.lifecycle.ViewModel
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import net.mullvad.mullvadvpn.repository.ChangelogRepository
+
+class ChangelogViewModel(
+ private val changelogRepository: ChangelogRepository,
+ private val buildVersionCode: Int,
+ private val alwaysShowChangelog: Boolean
+) : ViewModel() {
+ private val _changelogDialogUiState =
+ MutableStateFlow<ChangelogDialogUiState>(ChangelogDialogUiState.Hide)
+ val changelogDialogUiState = _changelogDialogUiState.asStateFlow()
+
+ fun refreshChangelogDialogUiState() {
+ val shouldShowChangelogDialog = alwaysShowChangelog || changelogRepository
+ .getVersionCodeOfMostRecentChangelogShowed() < buildVersionCode
+ _changelogDialogUiState.value = if (shouldShowChangelogDialog) {
+ val changelogList = changelogRepository.getLastVersionChanges()
+ if (changelogList.isNotEmpty()) {
+ ChangelogDialogUiState.Show(changelogList)
+ } else {
+ ChangelogDialogUiState.Hide
+ }
+ } else {
+ ChangelogDialogUiState.Hide
+ }
+ }
+
+ fun dismissChangelogDialog() {
+ changelogRepository.setVersionCodeOfMostRecentChangelogShowed(buildVersionCode)
+ _changelogDialogUiState.value = ChangelogDialogUiState.Hide
+ }
+}
+
+sealed class ChangelogDialogUiState {
+ data class Show(val changes: List<String>) : ChangelogDialogUiState()
+ object Hide : ChangelogDialogUiState()
+}
diff --git a/android/app/src/main/res/layout/main.xml b/android/app/src/main/res/layout/main.xml
index 7839409631..8e7356e766 100644
--- a/android/app/src/main/res/layout/main.xml
+++ b/android/app/src/main/res/layout/main.xml
@@ -2,4 +2,9 @@
android:id="@+id/main_fragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
- android:filterTouchesWhenObscured="true" />
+ android:filterTouchesWhenObscured="true">
+ <!-- this component added to be able show compose dialog -->
+ <androidx.compose.ui.platform.ComposeView android:id="@+id/compose_view"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/zero_size" />
+</FrameLayout>
diff --git a/android/app/src/main/res/values/dimensions.xml b/android/app/src/main/res/values/dimensions.xml
index ca7ff1ac81..4f35637a64 100644
--- a/android/app/src/main/res/values/dimensions.xml
+++ b/android/app/src/main/res/values/dimensions.xml
@@ -48,6 +48,9 @@
<dimen name="expanded_toolbar_height">104dp</dimen>
<dimen name="information_icon_size">28dp</dimen>
<dimen name="information_action_margin">20dp</dimen>
+ <dimen name="medium_padding">16dp</dimen>
+ <dimen name="small_padding">8dp</dimen>
+ <dimen name="zero_size">0px</dimen>
<!-- Switch Dimens-->
<dimen name="switch_width">46dp</dimen>
<dimen name="switch_height">30dp</dimen>
diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/ChangelogViewModelTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/ChangelogViewModelTest.kt
new file mode 100644
index 0000000000..eafac23bfc
--- /dev/null
+++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/ChangelogViewModelTest.kt
@@ -0,0 +1,97 @@
+package net.mullvad.mullvadvpn.viewmodel
+
+import app.cash.turbine.test
+import io.mockk.MockKAnnotations
+import io.mockk.Runs
+import io.mockk.every
+import io.mockk.impl.annotations.MockK
+import io.mockk.just
+import io.mockk.mockkStatic
+import io.mockk.unmockkAll
+import io.mockk.verify
+import junit.framework.Assert
+import kotlin.test.assertEquals
+import kotlinx.coroutines.test.runBlockingTest
+import net.mullvad.mullvadvpn.repository.ChangelogRepository
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+
+class ChangelogViewModelTest {
+
+ @MockK
+ private lateinit var mockedChangelogRepository: ChangelogRepository
+
+ private lateinit var viewModel: ChangelogViewModel
+
+ @Before
+ fun setup() {
+ MockKAnnotations.init(this)
+ mockkStatic(EVENT_NOTIFIER_EXTENSION_CLASS)
+ every {
+ mockedChangelogRepository.setVersionCodeOfMostRecentChangelogShowed(any())
+ } just Runs
+ viewModel = ChangelogViewModel(mockedChangelogRepository, 1, false)
+ }
+
+ @After
+ fun teardown() {
+ unmockkAll()
+ }
+
+ @Test
+ fun testInitialState() = runBlockingTest {
+ // Arrange, Act, Assert
+ viewModel.changelogDialogUiState.test {
+ Assert.assertEquals(ChangelogDialogUiState.Hide, awaitItem())
+ }
+ }
+
+ @Test
+ fun testShowAndDismissChangelogDialog() = runBlockingTest {
+ viewModel.changelogDialogUiState.test {
+ // Arrange
+ val fakeList = listOf("test")
+ every {
+ mockedChangelogRepository.getVersionCodeOfMostRecentChangelogShowed()
+ } returns -1
+ every { mockedChangelogRepository.getLastVersionChanges() } returns fakeList
+
+ // Assert initial ui state
+ assertEquals(ChangelogDialogUiState.Hide, awaitItem())
+
+ // Refresh and verify that the dialog should be shown
+ viewModel.refreshChangelogDialogUiState()
+ assertEquals(ChangelogDialogUiState.Show(fakeList), awaitItem())
+
+ // Dismiss dialog and verify that the dialog should be hidden
+ viewModel.dismissChangelogDialog()
+ assertEquals(ChangelogDialogUiState.Hide, awaitItem())
+ verify { mockedChangelogRepository.setVersionCodeOfMostRecentChangelogShowed(1) }
+ }
+ }
+
+ @Test
+ fun testShowCaseChangelogWithEmptyListDialog() = runBlockingTest {
+ viewModel.changelogDialogUiState.test {
+ // Arrange
+ val fakeEmptyList = emptyList<String>()
+ every {
+ mockedChangelogRepository.getVersionCodeOfMostRecentChangelogShowed()
+ } returns -1
+ every { mockedChangelogRepository.getLastVersionChanges() } returns fakeEmptyList
+
+ // Assert initial ui state
+ assertEquals(ChangelogDialogUiState.Hide, awaitItem())
+
+ // Refresh and verify that the Ui state remain same due list being empty
+ viewModel.refreshChangelogDialogUiState()
+ expectNoEvents()
+ }
+ }
+
+ companion object {
+ private const val EVENT_NOTIFIER_EXTENSION_CLASS =
+ "net.mullvad.talpid.util.EventNotifierExtensionsKt"
+ }
+}