summaryrefslogtreecommitdiffhomepage
path: root/android/src
diff options
context:
space:
mode:
authorAleksandr Granin <aleksandr@mullvad.net>2021-04-01 13:31:05 +0200
committerAleksandr Granin <aleksandr@mullvad.net>2021-04-01 13:31:05 +0200
commit563994ed9217e9d79b059a9da2410a6a68fe2169 (patch)
tree594f02a6eeb741a267ace7eb0970eabb92d467f9 /android/src
parenta487f5c04431013b884e1e5734370dc22e289279 (diff)
parent818a60da2ae1df45dc7d522bc244aeb9700e346e (diff)
downloadmullvadvpn-563994ed9217e9d79b059a9da2410a6a68fe2169.tar.xz
mullvadvpn-563994ed9217e9d79b059a9da2410a6a68fe2169.zip
Merge branch 'splittuneling-viewmodel'
Diffstat (limited to 'android/src')
-rw-r--r--android/src/main/kotlin/net/mullvad/mullvadvpn/applist/ViewIntent.kt9
-rw-r--r--android/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SplitTunnelingViewModel.kt156
-rw-r--r--android/src/main/res/drawable/ic_icons_add.xml9
-rw-r--r--android/src/main/res/drawable/ic_icons_remove.xml9
-rw-r--r--android/src/main/res/values-da/strings.xml1
-rw-r--r--android/src/main/res/values-de/strings.xml1
-rw-r--r--android/src/main/res/values-es/strings.xml1
-rw-r--r--android/src/main/res/values-fi/strings.xml1
-rw-r--r--android/src/main/res/values-fr/strings.xml1
-rw-r--r--android/src/main/res/values-it/strings.xml1
-rw-r--r--android/src/main/res/values-ja/strings.xml1
-rw-r--r--android/src/main/res/values-ko/strings.xml1
-rw-r--r--android/src/main/res/values-nb/strings.xml1
-rw-r--r--android/src/main/res/values-nl/strings.xml1
-rw-r--r--android/src/main/res/values-pl/strings.xml1
-rw-r--r--android/src/main/res/values-pt/strings.xml1
-rw-r--r--android/src/main/res/values-ru/strings.xml1
-rw-r--r--android/src/main/res/values-sv/strings.xml1
-rw-r--r--android/src/main/res/values-th/strings.xml1
-rw-r--r--android/src/main/res/values-tr/strings.xml1
-rw-r--r--android/src/main/res/values-zh-rCN/strings.xml1
-rw-r--r--android/src/main/res/values-zh-rTW/strings.xml1
-rw-r--r--android/src/main/res/values/strings.xml3
-rw-r--r--android/src/test/kotlin/net/mullvad/mullvadvpn/TestCoroutineRule.kt24
-rw-r--r--android/src/test/kotlin/net/mullvad/mullvadvpn/TestUtils.kt11
-rw-r--r--android/src/test/kotlin/net/mullvad/mullvadvpn/applist/ApplicationsIconManagerTest.kt2
-rw-r--r--android/src/test/kotlin/net/mullvad/mullvadvpn/applist/ApplicationsProviderTest.kt8
-rw-r--r--android/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/SplitTunnelingViewModelTest.kt225
28 files changed, 449 insertions, 25 deletions
diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/applist/ViewIntent.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/applist/ViewIntent.kt
new file mode 100644
index 0000000000..f1c15abbb8
--- /dev/null
+++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/applist/ViewIntent.kt
@@ -0,0 +1,9 @@
+package net.mullvad.mullvadvpn.applist
+
+import net.mullvad.mullvadvpn.model.ListItemData
+
+sealed class ViewIntent {
+ // In future we will have search intent
+ data class ChangeApplicationGroup(val item: ListItemData) : ViewIntent()
+ object ViewIsReady : ViewIntent()
+}
diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SplitTunnelingViewModel.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SplitTunnelingViewModel.kt
new file mode 100644
index 0000000000..98730de960
--- /dev/null
+++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SplitTunnelingViewModel.kt
@@ -0,0 +1,156 @@
+package net.mullvad.mullvadvpn.viewmodel
+
+import androidx.annotation.StringRes
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.SharedFlow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.asSharedFlow
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.flow.shareIn
+import kotlinx.coroutines.launch
+import net.mullvad.mullvadvpn.R
+import net.mullvad.mullvadvpn.applist.AppData
+import net.mullvad.mullvadvpn.applist.ApplicationsProvider
+import net.mullvad.mullvadvpn.applist.ViewIntent
+import net.mullvad.mullvadvpn.model.ListItemData
+import net.mullvad.mullvadvpn.model.WidgetState
+import net.mullvad.mullvadvpn.service.SplitTunneling
+
+class SplitTunnelingViewModel(
+ private val appsProvider: ApplicationsProvider,
+ private val splitTunneling: SplitTunneling,
+ dispatcher: CoroutineDispatcher
+) : ViewModel() {
+ private val listItemsSink = MutableSharedFlow<List<ListItemData>>(replay = 1)
+ // read-only public view
+ val listItems: SharedFlow<List<ListItemData>> = listItemsSink.asSharedFlow()
+
+ private val intentFlow = MutableSharedFlow<ViewIntent>()
+ private val isUIReady = CompletableDeferred<Unit>()
+ private val excludedApps: MutableMap<String, AppData> = mutableMapOf()
+ private val notExcludedApps: MutableMap<String, AppData> = mutableMapOf()
+
+ private val defaultListItems: List<ListItemData> = listOf(
+ createTextItem(R.string.split_tunneling_description)
+ // We will have search item in future
+ )
+
+ init {
+ viewModelScope.launch(dispatcher) {
+ listItemsSink.emit(defaultListItems + createDivider(0) + createProgressItem())
+ // this will be removed after changes on native to ignore enable parameter
+ if (!splitTunneling.enabled)
+ splitTunneling.enabled = true
+ fetchData()
+ }
+ viewModelScope.launch(dispatcher) {
+ intentFlow.shareIn(viewModelScope, SharingStarted.WhileSubscribed())
+ .collect(::handleIntents)
+ }
+ }
+
+ suspend fun processIntent(intent: ViewIntent) = intentFlow.emit(intent)
+
+ override fun onCleared() {
+ splitTunneling.persist()
+ super.onCleared()
+ }
+
+ private suspend fun handleIntents(viewIntent: ViewIntent) {
+ when (viewIntent) {
+ is ViewIntent.ChangeApplicationGroup -> {
+ viewIntent.item.action?.let {
+ if (excludedApps.containsKey(it.identifier)) {
+ removeFromExcluded(it.identifier)
+ } else {
+ addToExcluded(it.identifier)
+ }
+ publishList()
+ }
+ }
+ is ViewIntent.ViewIsReady -> isUIReady.complete(Unit)
+ }
+ }
+
+ private fun removeFromExcluded(packageName: String) {
+ excludedApps.remove(packageName)?.let { appInfo ->
+ notExcludedApps[packageName] = appInfo
+ splitTunneling.includeApp(packageName)
+ }
+ }
+
+ private fun addToExcluded(packageName: String) {
+ notExcludedApps.remove(packageName)?.let { appInfo ->
+ excludedApps[packageName] = appInfo
+ splitTunneling.excludeApp(packageName)
+ }
+ }
+
+ private suspend fun fetchData() {
+ appsProvider.getAppsList()
+ .partition { app -> splitTunneling.excludedAppList.contains(app.packageName) }
+ .let { (excludedAppsList, notExcludedAppsList) ->
+ // TODO: remove potential package names from splitTunneling list
+ // if they already uninstalled or filtered; but not in ViewModel
+ excludedAppsList.map { it.packageName to it }.toMap(excludedApps)
+ notExcludedAppsList.map { it.packageName to it }.toMap(notExcludedApps)
+ }
+ isUIReady.await()
+ publishList()
+ }
+
+ private suspend fun publishList() {
+ val listItems = ArrayList(defaultListItems)
+ if (excludedApps.isNotEmpty()) {
+ listItems += createDivider(0)
+ listItems += createMainItem(R.string.exclude_applications)
+ listItems += excludedApps.values.sortedBy { it.name }.map { info ->
+ createApplicationItem(info, true)
+ }
+ }
+ if (notExcludedApps.isNotEmpty()) {
+ listItems += createDivider(1)
+ listItems += createMainItem(R.string.all_applications)
+ listItems += notExcludedApps.values.sortedBy { it.name }.map { info ->
+ createApplicationItem(info, false)
+ }
+ }
+ listItemsSink.emit(listItems)
+ }
+
+ private fun createApplicationItem(appData: AppData, checked: Boolean): ListItemData =
+ ListItemData.build(appData.packageName) {
+ type = ListItemData.APPLICATION
+ text = appData.name
+ iconRes = appData.iconRes
+ action = ListItemData.ItemAction(appData.packageName)
+ widget = WidgetState.ImageState(
+ if (checked) R.drawable.ic_icons_remove else R.drawable.ic_icons_add
+ )
+ }
+
+ private fun createDivider(id: Int): ListItemData = ListItemData.build("space_$id") {
+ type = ListItemData.DIVIDER
+ }
+
+ private fun createMainItem(@StringRes text: Int): ListItemData =
+ ListItemData.build("header_$text") {
+ type = ListItemData.ACTION
+ textRes = text
+ }
+
+ private fun createTextItem(@StringRes text: Int): ListItemData =
+ ListItemData.build("text_$text") {
+ type = ListItemData.PLAIN
+ textRes = text
+ action = ListItemData.ItemAction(text.toString())
+ }
+
+ private fun createProgressItem(): ListItemData = ListItemData.build(identifier = "progress") {
+ type = ListItemData.PROGRESS
+ }
+}
diff --git a/android/src/main/res/drawable/ic_icons_add.xml b/android/src/main/res/drawable/ic_icons_add.xml
new file mode 100644
index 0000000000..97f0ca7fc7
--- /dev/null
+++ b/android/src/main/res/drawable/ic_icons_add.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path android:pathData="M13.05,5.66v5.29h5.29c0.513,0 0.99,0.398 0.99,0.99v0.12c0,0.578 -0.477,0.99 -0.99,0.99h-5.29v5.29c0,0.522 -0.412,0.989 -0.99,0.99l-0.12,-0.001c-0.59,0 -0.99,-0.467 -0.99,-0.989v-5.29H5.66c-0.534,0 -0.99,-0.427 -0.99,-0.99v-0.12c0,-0.559 0.456,-0.99 0.99,-0.99h5.29V5.66c0,-0.512 0.407,-0.99 0.99,-0.99h0.12c0.584,0 0.99,0.478 0.99,0.99zM12,24C5.373,24 0,18.627 0,12S5.373,0 12,0s12,5.373 12,12 -5.373,12 -12,12z"
+ android:fillColor="@android:color/white"
+ android:fillType="evenOdd" />
+</vector>
diff --git a/android/src/main/res/drawable/ic_icons_remove.xml b/android/src/main/res/drawable/ic_icons_remove.xml
new file mode 100644
index 0000000000..50b84ad42c
--- /dev/null
+++ b/android/src/main/res/drawable/ic_icons_remove.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path android:pathData="M13.05,10.95h5.29c0.513,0 0.99,0.398 0.99,0.99v0.12c0,0.578 -0.477,0.99 -0.99,0.99H5.66c-0.534,0 -0.99,-0.427 -0.99,-0.99v-0.12c0,-0.559 0.456,-0.99 0.99,-0.99H13.05zM12,24C5.373,24 0,18.627 0,12S5.373,0 12,0s12,5.373 12,12 -5.373,12 -12,12z"
+ android:fillColor="@android:color/white"
+ android:fillType="evenOdd" />
+</vector>
diff --git a/android/src/main/res/values-da/strings.xml b/android/src/main/res/values-da/strings.xml
index 242e49636e..ab5d62c02c 100644
--- a/android/src/main/res/values-da/strings.xml
+++ b/android/src/main/res/values-da/strings.xml
@@ -53,7 +53,6 @@
<string name="enter_voucher_code">Indtast kuponkode</string>
<string name="error_occurred">Der opstod en fejl.</string>
<string name="error_state">KUNNE IKKE SIKRE FORBINDELSEN</string>
- <string name="exclude_applications">Ekskluder applikationer</string>
<string name="failed_to_block_internet">Kunne ikke blokere al netværkstrafik. Løs problemet selv, eller rapporter problemet til os.</string>
<string name="failed_to_create_account">Kunne ikke oprette konto</string>
<string name="failed_to_generate_key">Kunne ikke generere en nøgle</string>
diff --git a/android/src/main/res/values-de/strings.xml b/android/src/main/res/values-de/strings.xml
index 10e8fc8ad4..b66b16b89e 100644
--- a/android/src/main/res/values-de/strings.xml
+++ b/android/src/main/res/values-de/strings.xml
@@ -53,7 +53,6 @@
<string name="enter_voucher_code">Gutscheincode eingeben</string>
<string name="error_occurred">Ein Fehler ist aufgetreten.</string>
<string name="error_state">SICHERE VERBINDUNG KONNTE NICHT HERGESTELLT WERDEN</string>
- <string name="exclude_applications">Anwendungen ausschließen</string>
<string name="failed_to_block_internet">Sperren des gesamten Netzwerk-Traffics fehlgeschlagen. Bitte beheben Sie den Fehler oder melden Sie uns das Problem.</string>
<string name="failed_to_create_account">Konto konnte nicht erstellt werden</string>
<string name="failed_to_generate_key">Fehler beim Generieren eines Schlüssels</string>
diff --git a/android/src/main/res/values-es/strings.xml b/android/src/main/res/values-es/strings.xml
index 801f624fd2..a8590b80da 100644
--- a/android/src/main/res/values-es/strings.xml
+++ b/android/src/main/res/values-es/strings.xml
@@ -53,7 +53,6 @@
<string name="enter_voucher_code">Escriba el código del cupón</string>
<string name="error_occurred">Se produjo un error.</string>
<string name="error_state">NO SE PUDO PROTEGER LA CONEXIÓN</string>
- <string name="exclude_applications">Excluir aplicaciones</string>
<string name="failed_to_block_internet">No se puede bloquear todo el tráfico de red. Intente solucionar el problema o póngase en contacto con nosotros.</string>
<string name="failed_to_create_account">No se puede crear la cuenta</string>
<string name="failed_to_generate_key">No se pudo generar una clave</string>
diff --git a/android/src/main/res/values-fi/strings.xml b/android/src/main/res/values-fi/strings.xml
index 13a5a3dfba..0c736a909c 100644
--- a/android/src/main/res/values-fi/strings.xml
+++ b/android/src/main/res/values-fi/strings.xml
@@ -53,7 +53,6 @@
<string name="enter_voucher_code">Syötä kuponkikoodi</string>
<string name="error_occurred">Ilmeni virhe.</string>
<string name="error_state">YHTEYDEN SUOJAAMINEN EPÄONNISTUI</string>
- <string name="exclude_applications">Sulje sovellukset pois</string>
<string name="failed_to_block_internet">Kaiken verkkoliikenteen estäminen epäonnistui. Käytä vianetsintää tai raportoi meille ongelmasta.</string>
<string name="failed_to_create_account">Tilin luonti epäonnistui</string>
<string name="failed_to_generate_key">Avaimen luominen epäonnistui</string>
diff --git a/android/src/main/res/values-fr/strings.xml b/android/src/main/res/values-fr/strings.xml
index 69f1f61641..c9c525e21f 100644
--- a/android/src/main/res/values-fr/strings.xml
+++ b/android/src/main/res/values-fr/strings.xml
@@ -53,7 +53,6 @@
<string name="enter_voucher_code">Saisissez un code de bon</string>
<string name="error_occurred">Une erreur est survenue.</string>
<string name="error_state">ÉCHEC DE LA SÉCURISATION DE LA CONNEXION</string>
- <string name="exclude_applications">Exclure des applications</string>
<string name="failed_to_block_internet">Impossible de bloquer tout le trafic du réseau. Veuillez dépanner ou nous signaler le problème.</string>
<string name="failed_to_create_account">Échec de la création du compte</string>
<string name="failed_to_generate_key">Échec de la génération de clé</string>
diff --git a/android/src/main/res/values-it/strings.xml b/android/src/main/res/values-it/strings.xml
index 0e3aa84ac0..f0f67ebc53 100644
--- a/android/src/main/res/values-it/strings.xml
+++ b/android/src/main/res/values-it/strings.xml
@@ -53,7 +53,6 @@
<string name="enter_voucher_code">Inserisci codice voucher</string>
<string name="error_occurred">Si è verificato un errore.</string>
<string name="error_state">IMPOSSIBILE STABILIRE UNA CONNESSIONE PROTETTA</string>
- <string name="exclude_applications">Escludi applicazioni</string>
<string name="failed_to_block_internet">Impossibile bloccare tutto il traffico di rete. Risolvi i problemi o segnalaceli.</string>
<string name="failed_to_create_account">Impossibile creare l\'account</string>
<string name="failed_to_generate_key">Impossibile generare una chiave</string>
diff --git a/android/src/main/res/values-ja/strings.xml b/android/src/main/res/values-ja/strings.xml
index 2f16a3f45f..d02642015e 100644
--- a/android/src/main/res/values-ja/strings.xml
+++ b/android/src/main/res/values-ja/strings.xml
@@ -53,7 +53,6 @@
<string name="enter_voucher_code">バウチャーコードを入力</string>
<string name="error_occurred">エラー発生。</string>
<string name="error_state">セキュリティ保護接続を確立できませんでした</string>
- <string name="exclude_applications">アプリケーションを除外する</string>
<string name="failed_to_block_internet">すべてのネットワーク通信をブロックできませんでした。問題に対処するか、当社に問題を報告してください。</string>
<string name="failed_to_create_account">アカウントを作成できませんでした</string>
<string name="failed_to_generate_key">鍵の生成に失敗しました</string>
diff --git a/android/src/main/res/values-ko/strings.xml b/android/src/main/res/values-ko/strings.xml
index 9b6b3a7b8f..c213690401 100644
--- a/android/src/main/res/values-ko/strings.xml
+++ b/android/src/main/res/values-ko/strings.xml
@@ -53,7 +53,6 @@
<string name="enter_voucher_code">바우처 코드 입력</string>
<string name="error_occurred">오류가 발생했습니다.</string>
<string name="error_state">보안 연결 실패</string>
- <string name="exclude_applications">애플리케이션 제외</string>
<string name="failed_to_block_internet">모든 네트워크 트래픽을 차단하지 못했습니다. 문제를 해결하거나 당사에 보고해 주세요.</string>
<string name="failed_to_create_account">계정을 만들지 못함</string>
<string name="failed_to_generate_key">키를 생성하지 못함</string>
diff --git a/android/src/main/res/values-nb/strings.xml b/android/src/main/res/values-nb/strings.xml
index b57149f17e..3dc45a2eff 100644
--- a/android/src/main/res/values-nb/strings.xml
+++ b/android/src/main/res/values-nb/strings.xml
@@ -53,7 +53,6 @@
<string name="enter_voucher_code">Skriv inn kupongkode</string>
<string name="error_occurred">Det oppstod en feil.</string>
<string name="error_state">KUNNE IKKE OPPRETTE SIKKER TILKOBLING</string>
- <string name="exclude_applications">Ekskluder applikasjoner</string>
<string name="failed_to_block_internet">Kunne ikke blokkere all nettverkstrafikk. Kjør feilsøking eller rapporter inn problemet til oss.</string>
<string name="failed_to_create_account">Kunne ikke opprette konto</string>
<string name="failed_to_generate_key">Generering av en nøkkel mislyktes</string>
diff --git a/android/src/main/res/values-nl/strings.xml b/android/src/main/res/values-nl/strings.xml
index ba6bc7914e..cab79e8986 100644
--- a/android/src/main/res/values-nl/strings.xml
+++ b/android/src/main/res/values-nl/strings.xml
@@ -53,7 +53,6 @@
<string name="enter_voucher_code">Voer vouchercode in</string>
<string name="error_occurred">Er is een fout opgetreden.</string>
<string name="error_state">VERBINDING BEVEILIGEN MISLUKT</string>
- <string name="exclude_applications">Toepassingen uitsluiten</string>
<string name="failed_to_block_internet">Kon alle netwerkverkeer niet blokkeren. Los problemen op of meld het aan ons.</string>
<string name="failed_to_create_account">Account aanmaken mislukt</string>
<string name="failed_to_generate_key">Sleutel genereren mislukt</string>
diff --git a/android/src/main/res/values-pl/strings.xml b/android/src/main/res/values-pl/strings.xml
index 51d69f9e6e..d108f0477d 100644
--- a/android/src/main/res/values-pl/strings.xml
+++ b/android/src/main/res/values-pl/strings.xml
@@ -53,7 +53,6 @@
<string name="enter_voucher_code">Wprowadź kod kuponu</string>
<string name="error_occurred">Wystąpił błąd.</string>
<string name="error_state">BŁĄD ZABEZPIECZANIA POŁĄCZENIA</string>
- <string name="exclude_applications">Wyklucz aplikacje</string>
<string name="failed_to_block_internet">Nie można zablokować całego ruchu sieciowego. Rozwiąż problem lub zgłoś go nam.</string>
<string name="failed_to_create_account">Nie można utworzyć konta</string>
<string name="failed_to_generate_key">Nie można wygenerować klucza</string>
diff --git a/android/src/main/res/values-pt/strings.xml b/android/src/main/res/values-pt/strings.xml
index 98f69b1f81..201e234894 100644
--- a/android/src/main/res/values-pt/strings.xml
+++ b/android/src/main/res/values-pt/strings.xml
@@ -53,7 +53,6 @@
<string name="enter_voucher_code">Introduza o código do voucher</string>
<string name="error_occurred">Ocorreu um erro.</string>
<string name="error_state">ERRO AO ESTABELECER LIGAÇÃO SEGURA</string>
- <string name="exclude_applications">Excluir aplicações</string>
<string name="failed_to_block_internet">Não foi possível bloquear todo o tráfego de rede. Experimente a resolução de problemas ou comunique-nos o problema.</string>
<string name="failed_to_create_account">Não foi possível criar a conta</string>
<string name="failed_to_generate_key">Não foi possível gerar uma chave</string>
diff --git a/android/src/main/res/values-ru/strings.xml b/android/src/main/res/values-ru/strings.xml
index 90a2384389..b641b7909b 100644
--- a/android/src/main/res/values-ru/strings.xml
+++ b/android/src/main/res/values-ru/strings.xml
@@ -53,7 +53,6 @@
<string name="enter_voucher_code">Введите код ваучера</string>
<string name="error_occurred">Произошла ошибка.</string>
<string name="error_state">НЕ УДАЛОСЬ УСТАНОВИТЬ БЕЗОПАСНОЕ ПОДКЛЮЧЕНИЕ</string>
- <string name="exclude_applications">Исключить приложения</string>
<string name="failed_to_block_internet">Не удалось заблокировать весь сетевой трафик. Устраните неисправность или сообщите нам о проблеме.</string>
<string name="failed_to_create_account">Не удалось создать учетную запись</string>
<string name="failed_to_generate_key">Не удалось сгенерировать ключ</string>
diff --git a/android/src/main/res/values-sv/strings.xml b/android/src/main/res/values-sv/strings.xml
index 2dcb0fc14d..a7ee171470 100644
--- a/android/src/main/res/values-sv/strings.xml
+++ b/android/src/main/res/values-sv/strings.xml
@@ -53,7 +53,6 @@
<string name="enter_voucher_code">Ange kupongkod</string>
<string name="error_occurred">Ett fel har inträffat.</string>
<string name="error_state">DET GICK INTE ATT SÄKRA ANSLUTNINGEN</string>
- <string name="exclude_applications">Exkludera applikationer</string>
<string name="failed_to_block_internet">Det gick inte att blockera all nätverkstrafik. Felsök eller anmäl problemet till oss.</string>
<string name="failed_to_create_account">Det gick inte att skapa konto</string>
<string name="failed_to_generate_key">Det gick inte att generera en nyckel</string>
diff --git a/android/src/main/res/values-th/strings.xml b/android/src/main/res/values-th/strings.xml
index cd52bdec98..28aac325b2 100644
--- a/android/src/main/res/values-th/strings.xml
+++ b/android/src/main/res/values-th/strings.xml
@@ -53,7 +53,6 @@
<string name="enter_voucher_code">ป้อนรหัสบัตรกำนัล</string>
<string name="error_occurred">เกิดข้อผิดพลาดขึ้น</string>
<string name="error_state">ไม่สามารถเชื่อมต่ออย่างปลอดภัยได้</string>
- <string name="exclude_applications">ไม่รวมแอปพลิเคชัน</string>
<string name="failed_to_block_internet">ไม่สามารถบล็อกการรับส่งข้อมูลทางเครือข่ายทั้งหมดได้ โปรดแก้ไขปัญหาหรือรายงานปัญหามาที่เรา</string>
<string name="failed_to_create_account">ไม่สามารถสร้างบัญชีได้</string>
<string name="failed_to_generate_key">ไม่สามารถสร้างคีย์ได้</string>
diff --git a/android/src/main/res/values-tr/strings.xml b/android/src/main/res/values-tr/strings.xml
index 9b57c370a7..23aa1c7c9c 100644
--- a/android/src/main/res/values-tr/strings.xml
+++ b/android/src/main/res/values-tr/strings.xml
@@ -53,7 +53,6 @@
<string name="enter_voucher_code">Kupon kodunu girin</string>
<string name="error_occurred">Bir hata oluştu.</string>
<string name="error_state">GÜVENLİ BAĞLANTI OLUŞTURULAMADI</string>
- <string name="exclude_applications">Uygulamaları hariç tut</string>
<string name="failed_to_block_internet">Tüm ağ trafiği engellenemedi. Lütfen sorunu çözmeyi deneyin veya bize bildirin.</string>
<string name="failed_to_create_account">Hesap oluşturulamadı</string>
<string name="failed_to_generate_key">Anahtar oluşturulamadı</string>
diff --git a/android/src/main/res/values-zh-rCN/strings.xml b/android/src/main/res/values-zh-rCN/strings.xml
index bb5448791d..8fc003a443 100644
--- a/android/src/main/res/values-zh-rCN/strings.xml
+++ b/android/src/main/res/values-zh-rCN/strings.xml
@@ -50,7 +50,6 @@
<string name="enter_voucher_code">输入优惠码</string>
<string name="error_occurred">出错了。</string>
<string name="error_state">无法保护连接</string>
- <string name="exclude_applications">排除应用程序</string>
<string name="failed_to_block_internet">无法阻止所有网络流量。请排除故障或向我们报告问题。</string>
<string name="failed_to_create_account">无法创建帐户</string>
<string name="failed_to_generate_key">无法生成密钥</string>
diff --git a/android/src/main/res/values-zh-rTW/strings.xml b/android/src/main/res/values-zh-rTW/strings.xml
index 911730158f..99a5e5b58d 100644
--- a/android/src/main/res/values-zh-rTW/strings.xml
+++ b/android/src/main/res/values-zh-rTW/strings.xml
@@ -53,7 +53,6 @@
<string name="enter_voucher_code">輸入憑證兌換碼</string>
<string name="error_occurred">發生錯誤了。</string>
<string name="error_state">保護連線失敗</string>
- <string name="exclude_applications">排除應用程式</string>
<string name="failed_to_block_internet">無法封鎖所有網路流量。請排除故障或向我們回報問題。</string>
<string name="failed_to_create_account">無法建立帳戶</string>
<string name="failed_to_generate_key">無法產生金鑰</string>
diff --git a/android/src/main/res/values/strings.xml b/android/src/main/res/values/strings.xml
index 46c2bdb464..b3a41d876c 100644
--- a/android/src/main/res/values/strings.xml
+++ b/android/src/main/res/values/strings.xml
@@ -175,7 +175,8 @@
<string name="confirm_public_dns">The DNS server you are trying to add might not work because
it is public. Currently we only support local DNS servers.</string>
<string name="add_anyway">Add anyway</string>
- <string name="exclude_applications">Exclude applications</string>
+ <string name="exclude_applications">Excluded applications</string>
+ <string name="all_applications">All applications</string>
<string name="account_url">https://mullvad.net/en/account</string>
<string name="wg_key_url">https://mullvad.net/en/account/ports</string>
<string name="create_account_url">https://mullvad.net/en/account/create</string>
diff --git a/android/src/test/kotlin/net/mullvad/mullvadvpn/TestCoroutineRule.kt b/android/src/test/kotlin/net/mullvad/mullvadvpn/TestCoroutineRule.kt
new file mode 100644
index 0000000000..1acdf9e577
--- /dev/null
+++ b/android/src/test/kotlin/net/mullvad/mullvadvpn/TestCoroutineRule.kt
@@ -0,0 +1,24 @@
+package net.mullvad.mullvadvpn
+
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.test.TestCoroutineDispatcher
+import kotlinx.coroutines.test.resetMain
+import kotlinx.coroutines.test.setMain
+import org.junit.rules.TestWatcher
+import org.junit.runner.Description
+
+class TestCoroutineRule(
+ val testDispatcher: TestCoroutineDispatcher = TestCoroutineDispatcher()
+) : TestWatcher() {
+
+ override fun starting(description: Description?) {
+ super.starting(description)
+ Dispatchers.setMain(testDispatcher)
+ }
+
+ override fun finished(description: Description?) {
+ super.finished(description)
+ Dispatchers.resetMain()
+ testDispatcher.cleanupTestCoroutines()
+ }
+}
diff --git a/android/src/test/kotlin/net/mullvad/mullvadvpn/TestUtils.kt b/android/src/test/kotlin/net/mullvad/mullvadvpn/TestUtils.kt
new file mode 100644
index 0000000000..4c4f043c06
--- /dev/null
+++ b/android/src/test/kotlin/net/mullvad/mullvadvpn/TestUtils.kt
@@ -0,0 +1,11 @@
+package net.mullvad.mullvadvpn
+
+import kotlin.test.assertTrue
+
+fun <T> assertLists(expected: List<T>, actual: List<T>, message: String? = null) = assertTrue(
+ expected.size == actual.size && expected.containsAll(actual) && actual.containsAll(expected),
+ message ?: """Expected list should have same size and contains same items.
+ | Expected(${expected.size}): $expected
+ | Actual(${actual.size}) : $actual
+ """.trimMargin()
+)
diff --git a/android/src/test/kotlin/net/mullvad/mullvadvpn/applist/ApplicationsIconManagerTest.kt b/android/src/test/kotlin/net/mullvad/mullvadvpn/applist/ApplicationsIconManagerTest.kt
index 6f871a28ee..e6d43621a1 100644
--- a/android/src/test/kotlin/net/mullvad/mullvadvpn/applist/ApplicationsIconManagerTest.kt
+++ b/android/src/test/kotlin/net/mullvad/mullvadvpn/applist/ApplicationsIconManagerTest.kt
@@ -86,7 +86,7 @@ class ApplicationsIconManagerTest {
}
@Test
- fun throw_exception_when_invoke_from_MainThread() {
+ fun test_throw_exception_when_invoke_from_MainThread() {
val testPackageName = "test"
every { mockedMainLooper.isCurrentThread } returns true
diff --git a/android/src/test/kotlin/net/mullvad/mullvadvpn/applist/ApplicationsProviderTest.kt b/android/src/test/kotlin/net/mullvad/mullvadvpn/applist/ApplicationsProviderTest.kt
index cb24158422..689047e565 100644
--- a/android/src/test/kotlin/net/mullvad/mullvadvpn/applist/ApplicationsProviderTest.kt
+++ b/android/src/test/kotlin/net/mullvad/mullvadvpn/applist/ApplicationsProviderTest.kt
@@ -7,6 +7,7 @@ import io.mockk.every
import io.mockk.mockk
import io.mockk.unmockkAll
import io.mockk.verifyAll
+import net.mullvad.mullvadvpn.assertLists
import org.junit.After
import org.junit.Test
@@ -42,11 +43,8 @@ class ApplicationsProviderTest {
val expected = listOf(
AppData(launchWithInternetPackageName, 0, launchWithInternetPackageName)
)
- assert(
- expected.size == result.size &&
- expected.containsAll(result) &&
- result.containsAll(expected)
- )
+
+ assertLists(expected, result)
verifyAll {
mockedPackageManager.getInstalledApplications(PackageManager.GET_META_DATA)
diff --git a/android/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/SplitTunnelingViewModelTest.kt b/android/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/SplitTunnelingViewModelTest.kt
new file mode 100644
index 0000000000..4bfd98f9da
--- /dev/null
+++ b/android/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/SplitTunnelingViewModelTest.kt
@@ -0,0 +1,225 @@
+package net.mullvad.mullvadvpn.viewmodel
+
+import androidx.annotation.StringRes
+import androidx.lifecycle.viewModelScope
+import io.mockk.Runs
+import io.mockk.every
+import io.mockk.just
+import io.mockk.mockk
+import io.mockk.unmockkAll
+import io.mockk.verify
+import io.mockk.verifyAll
+import java.util.concurrent.TimeUnit
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.test.runBlockingTest
+import net.mullvad.mullvadvpn.R
+import net.mullvad.mullvadvpn.TestCoroutineRule
+import net.mullvad.mullvadvpn.applist.AppData
+import net.mullvad.mullvadvpn.applist.ApplicationsProvider
+import net.mullvad.mullvadvpn.applist.ViewIntent
+import net.mullvad.mullvadvpn.assertLists
+import net.mullvad.mullvadvpn.model.ListItemData
+import net.mullvad.mullvadvpn.model.WidgetState
+import net.mullvad.mullvadvpn.service.SplitTunneling
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.rules.Timeout
+
+class SplitTunnelingViewModelTest {
+ @get:Rule
+ val testCoroutineRule = TestCoroutineRule()
+
+ @get:Rule
+ val timeout = Timeout(3000L, TimeUnit.MILLISECONDS)
+ private val mockedApplicationsProvider = mockk<ApplicationsProvider>()
+ private val mockedSplitTunneling = mockk<SplitTunneling>()
+ private lateinit var testSubject: SplitTunnelingViewModel
+
+ @Before
+ fun setup() {
+ every { mockedSplitTunneling.enabled } returns true
+ }
+
+ @After
+ fun tearDown() {
+ testSubject.viewModelScope.coroutineContext.cancel()
+ unmockkAll()
+ }
+
+ @Test
+ fun test_has_progress_on_start() = runBlockingTest(testCoroutineRule.testDispatcher) {
+ initTestSubject(emptyList())
+ val actualList: List<ListItemData> = testSubject.listItems.first()
+
+ val initialExpectedList = listOf(
+ createTextItem(R.string.split_tunneling_description),
+ createDivider(0),
+ createProgressItem()
+ )
+
+ assertLists(initialExpectedList, actualList)
+
+ verify(exactly = 1) {
+ mockedApplicationsProvider.getAppsList()
+ }
+ }
+
+ @Test
+ fun test_empty_app_list() = runBlockingTest(testCoroutineRule.testDispatcher) {
+ initTestSubject(emptyList())
+ testSubject.processIntent(ViewIntent.ViewIsReady)
+ val actualList = testSubject.listItems.first()
+ val expectedList = listOf(createTextItem(R.string.split_tunneling_description))
+ assertLists(expectedList, actualList)
+ }
+
+ @Test
+ fun test_apps_list_delivered() = runBlockingTest(testCoroutineRule.testDispatcher) {
+ val appExcluded = AppData("test.excluded", 0, "testName1")
+ val appNotExcluded = AppData("test.not.excluded", 0, "testName2")
+ every { mockedSplitTunneling.excludedAppList } returns listOf(appExcluded.packageName)
+
+ initTestSubject(listOf(appExcluded, appNotExcluded))
+ testSubject.processIntent(ViewIntent.ViewIsReady)
+
+ val actualList = testSubject.listItems.first()
+ val expectedList = listOf(
+ createTextItem(R.string.split_tunneling_description),
+ createDivider(0),
+ createMainItem(R.string.exclude_applications),
+ createApplicationItem(appExcluded, true),
+ createDivider(1),
+ createMainItem(R.string.all_applications),
+ createApplicationItem(appNotExcluded, false),
+ )
+
+ assertLists(expectedList, actualList)
+ verifyAll {
+ mockedSplitTunneling.enabled
+ mockedSplitTunneling.excludedAppList
+ }
+ }
+
+ @Test
+ fun test_remove_app_from_excluded() = runBlockingTest(testCoroutineRule.testDispatcher) {
+ val app = AppData("test", 0, "testName")
+ every { mockedSplitTunneling.excludedAppList } returns listOf(app.packageName)
+ every { mockedSplitTunneling.includeApp(app.packageName) } just Runs
+
+ initTestSubject(listOf(app))
+ testSubject.processIntent(ViewIntent.ViewIsReady)
+
+ val listBeforeAction = testSubject.listItems.first()
+ val expectedListBeforeAction = listOf(
+ createTextItem(R.string.split_tunneling_description),
+ createDivider(0),
+ createMainItem(R.string.exclude_applications),
+ createApplicationItem(app, true),
+ )
+
+ assertLists(expectedListBeforeAction, listBeforeAction)
+
+ val item = listBeforeAction.first { it.identifier == app.packageName }
+ testSubject.processIntent(ViewIntent.ChangeApplicationGroup(item))
+
+ val itemsAfterAction = testSubject.listItems.first()
+ val expectedList = listOf(
+ createTextItem(R.string.split_tunneling_description),
+ createDivider(1),
+ createMainItem(R.string.all_applications),
+ createApplicationItem(app, false),
+ )
+
+ assertLists(expectedList, itemsAfterAction)
+
+ verifyAll {
+ mockedSplitTunneling.enabled
+ mockedSplitTunneling.excludedAppList
+ mockedSplitTunneling.includeApp(app.packageName)
+ }
+ }
+
+ @Test
+ fun test_add_app_to_excluded() = runBlockingTest(testCoroutineRule.testDispatcher) {
+ val app = AppData("test", 0, "testName")
+ every { mockedSplitTunneling.excludedAppList } returns emptyList()
+ every { mockedSplitTunneling.excludeApp(app.packageName) } just Runs
+ initTestSubject(listOf(app))
+ testSubject.processIntent(ViewIntent.ViewIsReady)
+
+ val listBeforeAction = testSubject.listItems.first()
+ val expectedListBeforeAction = listOf(
+ createTextItem(R.string.split_tunneling_description),
+ createDivider(1),
+ createMainItem(R.string.all_applications),
+ createApplicationItem(app, false),
+ )
+
+ assertLists(expectedListBeforeAction, listBeforeAction)
+
+ val item = listBeforeAction.first { it.identifier == app.packageName }
+ testSubject.processIntent(ViewIntent.ChangeApplicationGroup(item))
+
+ val itemsAfterAction = testSubject.listItems.first()
+ val expectedList = listOf(
+ createTextItem(R.string.split_tunneling_description),
+ createDivider(0),
+ createMainItem(R.string.exclude_applications),
+ createApplicationItem(app, true),
+ )
+
+ assertLists(expectedList, itemsAfterAction)
+
+ verifyAll {
+ mockedSplitTunneling.enabled
+ mockedSplitTunneling.excludedAppList
+ mockedSplitTunneling.excludeApp(app.packageName)
+ }
+ }
+
+ private fun initTestSubject(appList: List<AppData>) {
+ every { mockedApplicationsProvider.getAppsList() } returns appList
+ testSubject = SplitTunnelingViewModel(
+ mockedApplicationsProvider,
+ mockedSplitTunneling,
+ testCoroutineRule.testDispatcher
+ )
+ }
+
+ private fun createApplicationItem(
+ appData: AppData,
+ checked: Boolean
+ ): ListItemData = ListItemData.build(appData.packageName) {
+ type = ListItemData.APPLICATION
+ text = appData.name
+ iconRes = appData.iconRes
+ action = ListItemData.ItemAction(appData.packageName)
+ widget = WidgetState.ImageState(
+ if (checked) R.drawable.ic_icons_remove else R.drawable.ic_icons_add
+ )
+ }
+
+ private fun createDivider(id: Int): ListItemData = ListItemData.build("space_$id") {
+ type = ListItemData.DIVIDER
+ }
+
+ private fun createMainItem(@StringRes text: Int): ListItemData =
+ ListItemData.build("header_$text") {
+ type = ListItemData.ACTION
+ textRes = text
+ }
+
+ private fun createTextItem(@StringRes text: Int): ListItemData =
+ ListItemData.build("text_$text") {
+ type = ListItemData.PLAIN
+ textRes = text
+ action = ListItemData.ItemAction(text.toString())
+ }
+
+ private fun createProgressItem(): ListItemData = ListItemData.build(identifier = "progress") {
+ type = ListItemData.PROGRESS
+ }
+}