summaryrefslogtreecommitdiffhomepage
path: root/android/src/main
diff options
context:
space:
mode:
authorAleksandr Granin <aleksandr@mullvad.net>2021-03-29 16:31:31 +0200
committerAleksandr Granin <aleksandr@mullvad.net>2021-04-01 13:30:37 +0200
commit99a40bc34d559cbb0bf936ad510b97095a02d062 (patch)
treed40629748ec2fe8e643588eea62f58392623f4f6 /android/src/main
parenta487f5c04431013b884e1e5734370dc22e289279 (diff)
downloadmullvadvpn-99a40bc34d559cbb0bf936ad510b97095a02d062.tar.xz
mullvadvpn-99a40bc34d559cbb0bf936ad510b97095a02d062.zip
Create SplitTunneling ViewModel and tests
Diffstat (limited to 'android/src/main')
-rw-r--r--android/src/main/kotlin/net/mullvad/mullvadvpn/applist/ApplicationsProvider.kt8
-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.kt155
-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/strings.xml3
6 files changed, 189 insertions, 4 deletions
diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/applist/ApplicationsProvider.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/applist/ApplicationsProvider.kt
index a097ffd231..92af83aea3 100644
--- a/android/src/main/kotlin/net/mullvad/mullvadvpn/applist/ApplicationsProvider.kt
+++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/applist/ApplicationsProvider.kt
@@ -3,6 +3,8 @@ package net.mullvad.mullvadvpn.applist
import android.Manifest
import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.Deferred
class ApplicationsProvider(
private val packageManager: PackageManager,
@@ -14,15 +16,15 @@ class ApplicationsProvider(
!isSelfApplication(appInfo.packageName)
}
- fun getAppsList(): List<AppData> {
- return packageManager.getInstalledApplications(PackageManager.GET_META_DATA)
+ fun getAppsListAsync(): Deferred<List<AppData>> = CompletableDeferred(
+ packageManager.getInstalledApplications(PackageManager.GET_META_DATA)
.asSequence()
.filter(applicationFilterPredicate)
.map { info ->
AppData(info.packageName, info.icon, info.loadLabel(packageManager).toString())
}
.toList()
- }
+ )
private fun hasInternetPermission(packageName: String): Boolean {
return PackageManager.PERMISSION_GRANTED ==
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..c2b95b9c2a
--- /dev/null
+++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SplitTunnelingViewModel.kt
@@ -0,0 +1,155 @@
+package net.mullvad.mullvadvpn.viewmodel
+
+import androidx.annotation.StringRes
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.Dispatchers
+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
+) : 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(Dispatchers.Default) {
+ 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(Dispatchers.Default) {
+ 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.getAppsListAsync().await()
+ .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/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>