From 5d0524a838149fda58c64c83ce0adfd64db0e96a Mon Sep 17 00:00:00 2001
From: Guillaume Jacquart <guillaume.jacquart@hoodbrains.com>
Date: Tue, 31 Aug 2021 15:44:34 +0000
Subject: Feature/ipscrambling

---
 .../e/privacycentralapp/DependencyContainer.kt     |   8 +
 .../privacycentralapp/common/ToggleAppsAdapter.kt  |  73 +++++++
 .../internetprivacy/InternetPrivacyFeature.kt      | 210 ++++++++++++++++++---
 .../internetprivacy/InternetPrivacyFragment.kt     | 163 +++++++++++++---
 .../internetprivacy/InternetPrivacyViewModel.kt    |  24 ++-
 .../layout/fragment_internet_activity_policy.xml   |  84 ++++++++-
 app/src/main/res/values/strings.xml                |   9 +
 7 files changed, 513 insertions(+), 58 deletions(-)
 create mode 100644 app/src/main/java/foundation/e/privacycentralapp/common/ToggleAppsAdapter.kt

(limited to 'app/src/main')

diff --git a/app/src/main/java/foundation/e/privacycentralapp/DependencyContainer.kt b/app/src/main/java/foundation/e/privacycentralapp/DependencyContainer.kt
index fcc2eaa..1ab848c 100644
--- a/app/src/main/java/foundation/e/privacycentralapp/DependencyContainer.kt
+++ b/app/src/main/java/foundation/e/privacycentralapp/DependencyContainer.kt
@@ -20,8 +20,11 @@ package foundation.e.privacycentralapp
 import android.app.Application
 import android.content.Context
 import android.os.Process
+import foundation.e.privacycentralapp.features.internetprivacy.InternetPrivacyViewModelFactory
 import foundation.e.privacycentralapp.features.location.FakeLocationViewModelFactory
 import foundation.e.privacycentralapp.features.location.LocationApiDelegate
+import foundation.e.privacymodules.ipscrambler.IpScramblerModule
+import foundation.e.privacymodules.ipscramblermodule.IIpScramblerModule
 import foundation.e.privacymodules.location.FakeLocation
 import foundation.e.privacymodules.location.IFakeLocation
 import foundation.e.privacymodules.permissions.PermissionsPrivacyModule
@@ -39,6 +42,7 @@ class DependencyContainer constructor(val app: Application) {
 
     private val fakeLocationModule: IFakeLocation by lazy { FakeLocation(app.applicationContext) }
     private val permissionsModule by lazy { PermissionsPrivacyModule(app.applicationContext) }
+    private val ipScramblerModule: IIpScramblerModule by lazy { IpScramblerModule(app.applicationContext) }
 
     private val appDesc by lazy {
         ApplicationDescription(
@@ -58,4 +62,8 @@ class DependencyContainer constructor(val app: Application) {
     }
 
     val blockerService = BlockerInterface.getInstance(context)
+
+    val internetPrivacyViewModelFactory by lazy {
+        InternetPrivacyViewModelFactory(ipScramblerModule, permissionsModule)
+    }
 }
diff --git a/app/src/main/java/foundation/e/privacycentralapp/common/ToggleAppsAdapter.kt b/app/src/main/java/foundation/e/privacycentralapp/common/ToggleAppsAdapter.kt
new file mode 100644
index 0000000..4f9a6fc
--- /dev/null
+++ b/app/src/main/java/foundation/e/privacycentralapp/common/ToggleAppsAdapter.kt
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2021 E FOUNDATION
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package foundation.e.privacycentralapp.common
+
+import android.annotation.SuppressLint
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ImageView
+import android.widget.Switch
+import android.widget.TextView
+import androidx.recyclerview.widget.RecyclerView
+import foundation.e.privacycentralapp.R
+import foundation.e.privacymodules.permissions.data.ApplicationDescription
+
+open class ToggleAppsAdapter(
+    private val listener: (String, Boolean) -> Unit
+) :
+    RecyclerView.Adapter<ToggleAppsAdapter.PermissionViewHolder>() {
+
+    class PermissionViewHolder(view: View) : RecyclerView.ViewHolder(view) {
+        val appName: TextView = view.findViewById(R.id.app_title)
+
+        @SuppressLint("UseSwitchCompatOrMaterialCode")
+        val togglePermission: Switch = view.findViewById(R.id.toggle)
+
+        fun bind(item: Pair<ApplicationDescription, Boolean>) {
+            appName.text = item.first.label
+            togglePermission.isChecked = item.second
+
+            itemView.findViewById<ImageView>(R.id.app_icon).setImageDrawable(item.first.icon)
+        }
+    }
+
+    var dataSet: List<Pair<ApplicationDescription, Boolean>> = emptyList()
+        set(value) {
+            field = value
+            notifyDataSetChanged()
+        }
+
+    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PermissionViewHolder {
+        val view = LayoutInflater.from(parent.context)
+            .inflate(R.layout.item_app_toggle, parent, false)
+        val holder = PermissionViewHolder(view)
+        holder.togglePermission.setOnCheckedChangeListener { _, isChecked ->
+            listener(dataSet[holder.adapterPosition].first.packageName, isChecked)
+        }
+        view.findViewById<Switch>(R.id.toggle)
+        return holder
+    }
+
+    override fun onBindViewHolder(holder: PermissionViewHolder, position: Int) {
+        val permission = dataSet[position]
+        holder.bind(permission)
+    }
+
+    override fun getItemCount(): Int = dataSet.size
+}
diff --git a/app/src/main/java/foundation/e/privacycentralapp/features/internetprivacy/InternetPrivacyFeature.kt b/app/src/main/java/foundation/e/privacycentralapp/features/internetprivacy/InternetPrivacyFeature.kt
index b34024e..41ce9ad 100644
--- a/app/src/main/java/foundation/e/privacycentralapp/features/internetprivacy/InternetPrivacyFeature.kt
+++ b/app/src/main/java/foundation/e/privacycentralapp/features/internetprivacy/InternetPrivacyFeature.kt
@@ -17,16 +17,25 @@
 
 package foundation.e.privacycentralapp.features.internetprivacy
 
+import android.Manifest
+import android.app.Activity
+import android.content.Intent
 import android.util.Log
 import foundation.e.flowmvi.Actor
 import foundation.e.flowmvi.Reducer
 import foundation.e.flowmvi.SingleEventProducer
 import foundation.e.flowmvi.feature.BaseFeature
-import foundation.e.privacycentralapp.dummy.DummyDataSource
-import foundation.e.privacycentralapp.dummy.InternetPrivacyMode
+import foundation.e.privacymodules.ipscramblermodule.IIpScramblerModule
+import foundation.e.privacymodules.permissions.PermissionsPrivacyModule
+import foundation.e.privacymodules.permissions.data.ApplicationDescription
 import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.callbackFlow
 import kotlinx.coroutines.flow.flow
 import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.merge
 
 // Define a state machine for Internet privacy feature
 class InternetPrivacyFeature(
@@ -43,11 +52,34 @@ class InternetPrivacyFeature(
     { message -> Log.d("InternetPrivacyFeature", message) },
     singleEventProducer
 ) {
-    data class State(val mode: InternetPrivacyMode)
+    data class State(
+        val mode: IIpScramblerModule.Status,
+        val availableApps: List<ApplicationDescription>,
+        val ipScrambledApps: Collection<String>,
+        val selectedLocation: String,
+        val availableLocationIds: List<String>
+    ) {
+
+        val isAllAppsScrambled get() = ipScrambledApps.isEmpty()
+        fun getScrambledApps(): List<Pair<ApplicationDescription, Boolean>> {
+            return availableApps
+                .filter { it.packageName in ipScrambledApps }
+                .map { it to true }
+        }
+
+        fun getApps(): List<Pair<ApplicationDescription, Boolean>> {
+            return availableApps
+                .filter { it.packageName !in ipScrambledApps }
+                .map { it to false }
+        }
+
+        val selectedLocationPosition get() = availableLocationIds.indexOf(selectedLocation)
+    }
 
     sealed class SingleEvent {
         object RealIPSelectedEvent : SingleEvent()
         object HiddenIPSelectedEvent : SingleEvent()
+        data class StartAndroidVpnActivityEvent(val intent: Intent) : SingleEvent()
         data class ErrorEvent(val error: String) : SingleEvent()
     }
 
@@ -55,53 +87,171 @@ class InternetPrivacyFeature(
         object LoadInternetModeAction : Action()
         object UseRealIPAction : Action()
         object UseHiddenIPAction : Action()
+        data class AndroidVpnActivityResultAction(val resultCode: Int) : Action()
+        data class ToggleAppIpScrambled(val packageName: String, val isIpScrambled: Boolean) : Action()
+        data class SelectLocationAction(val position: Int) : Action()
     }
 
     sealed class Effect {
-        data class ModeUpdatedEffect(val mode: InternetPrivacyMode) : Effect()
+        data class ModeUpdatedEffect(val mode: IIpScramblerModule.Status) : Effect()
+        object NoEffect : Effect()
+        data class ShowAndroidVpnDisclaimerEffect(val intent: Intent) : Effect()
+        data class IpScrambledAppsUpdatedEffect(val ipScrambledApps: Collection<String>) : Effect()
+        data class AvailableAppsListEffect(val apps: List<ApplicationDescription>) : Effect()
+        data class LocationSelectedEffect(val locationId: String) : Effect()
+        data class AvailableCountriesEffect(val availableLocationsIds: List<String>) : Effect()
         data class ErrorEffect(val message: String) : Effect()
     }
 
     companion object {
         fun create(
-            initialState: State = State(InternetPrivacyMode.REAL_IP),
-            coroutineScope: CoroutineScope
+            initialState: State = State(
+                IIpScramblerModule.Status.STOPPING,
+                availableApps = emptyList(),
+                ipScrambledApps = emptyList(),
+                availableLocationIds = emptyList(),
+                selectedLocation = ""
+            ),
+            coroutineScope: CoroutineScope,
+            ipScramblerModule: IIpScramblerModule,
+            permissionsModule: PermissionsPrivacyModule
         ) = InternetPrivacyFeature(
             initialState, coroutineScope,
             reducer = { state, effect ->
                 when (effect) {
                     is Effect.ModeUpdatedEffect -> state.copy(mode = effect.mode)
-                    is Effect.ErrorEffect -> state
+                    is Effect.IpScrambledAppsUpdatedEffect -> state.copy(ipScrambledApps = effect.ipScrambledApps)
+                    is Effect.AvailableAppsListEffect -> state.copy(availableApps = effect.apps)
+                    is Effect.AvailableCountriesEffect -> state.copy(availableLocationIds = effect.availableLocationsIds)
+                    is Effect.LocationSelectedEffect -> state.copy(selectedLocation = effect.locationId)
+                    else -> state
                 }
             },
-            actor = { _, action ->
-                when (action) {
-                    Action.LoadInternetModeAction -> flowOf(Effect.ModeUpdatedEffect(DummyDataSource.internetActivityMode.value))
-                    Action.UseHiddenIPAction, Action.UseRealIPAction -> flow {
-                        val success =
-                            DummyDataSource.setInternetPrivacyMode(if (action is Action.UseHiddenIPAction) InternetPrivacyMode.HIDE_IP else InternetPrivacyMode.REAL_IP)
-                        emit(
-                            if (success) Effect.ModeUpdatedEffect(DummyDataSource.internetActivityMode.value) else Effect.ErrorEffect(
-                                "Couldn't update internet mode"
-                            )
-                        )
-                    }
-                }
-            },
-            singleEventProducer = { _, action, effect ->
-                when (action) {
-                    Action.UseRealIPAction, Action.UseHiddenIPAction -> when (effect) {
-                        is Effect.ModeUpdatedEffect -> {
-                            if (effect.mode == InternetPrivacyMode.REAL_IP) {
-                                SingleEvent.RealIPSelectedEvent
+            actor = { state, action ->
+                when {
+                    action is Action.LoadInternetModeAction -> merge(
+                        callbackFlow {
+                            val listener = object : IIpScramblerModule.Listener {
+                                override fun onStatusChanged(newStatus: IIpScramblerModule.Status) {
+                                    offer(Effect.ModeUpdatedEffect(newStatus))
+                                }
+
+                                override fun log(message: String) {}
+                                override fun onTrafficUpdate(upload: Long, download: Long, read: Long, write: Long) {}
+                            }
+                            ipScramblerModule.addListener(listener)
+                            ipScramblerModule.requestStatus()
+                            awaitClose { ipScramblerModule.removeListener(listener) }
+                        },
+                        flow {
+                            // TODO: filter deactivated apps"
+                            val apps = permissionsModule.getInstalledApplications()
+                                .filter {
+                                    permissionsModule.getPermissions(it.packageName)
+                                        .contains(Manifest.permission.INTERNET)
+                                }.map {
+                                    it.icon = permissionsModule.getApplicationIcon(it.packageName)
+                                    it
+                                }.sortedWith(object : Comparator<ApplicationDescription> {
+                                    override fun compare(
+                                        p0: ApplicationDescription?,
+                                        p1: ApplicationDescription?
+                                    ): Int {
+                                        return if (p0?.icon != null && p1?.icon != null) {
+                                            p0.label.toString().compareTo(p1.label.toString())
+                                        } else if (p0?.icon == null) {
+                                            1
+                                        } else {
+                                            -1
+                                        }
+                                    }
+                                })
+                            emit(Effect.AvailableAppsListEffect(apps))
+                        },
+                        flowOf(Effect.IpScrambledAppsUpdatedEffect(ipScramblerModule.appList)),
+                        flow {
+                            val locationIds = mutableListOf("")
+                            locationIds.addAll(ipScramblerModule.getAvailablesLocations().sorted())
+                            emit(Effect.AvailableCountriesEffect(locationIds))
+                        },
+                        flowOf(Effect.LocationSelectedEffect(ipScramblerModule.exitCountry))
+                    ).flowOn(Dispatchers.Default)
+                    action is Action.AndroidVpnActivityResultAction ->
+                        if (action.resultCode == Activity.RESULT_OK) {
+                            if (state.mode in listOf(
+                                    IIpScramblerModule.Status.OFF,
+                                    IIpScramblerModule.Status.STOPPING
+                                )
+                            ) {
+                                ipScramblerModule.start()
+                                flowOf(Effect.ModeUpdatedEffect(IIpScramblerModule.Status.STARTING))
                             } else {
-                                SingleEvent.HiddenIPSelectedEvent
+                                flowOf(Effect.ErrorEffect("Vpn already started"))
                             }
+                        } else {
+                            flowOf(Effect.ErrorEffect("Vpn wasn't allowed to start"))
                         }
-                        is Effect.ErrorEffect -> {
-                            SingleEvent.ErrorEvent(effect.message)
+
+                    action is Action.UseRealIPAction && state.mode in listOf(
+                        IIpScramblerModule.Status.ON,
+                        IIpScramblerModule.Status.STARTING,
+                        IIpScramblerModule.Status.STOPPING
+                    ) -> {
+                        ipScramblerModule.stop()
+                        flowOf(Effect.ModeUpdatedEffect(IIpScramblerModule.Status.STOPPING))
+                    }
+                    action is Action.UseHiddenIPAction
+                        && state.mode in listOf(
+                            IIpScramblerModule.Status.OFF,
+                            IIpScramblerModule.Status.STOPPING
+                        ) -> {
+                        ipScramblerModule.prepareAndroidVpn()?.let {
+                            flowOf(Effect.ShowAndroidVpnDisclaimerEffect(it))
+                        } ?: run {
+                            ipScramblerModule.start()
+                            flowOf(Effect.ModeUpdatedEffect(IIpScramblerModule.Status.STARTING))
                         }
                     }
+
+                    action is Action.ToggleAppIpScrambled -> {
+                        val ipScrambledApps = mutableSetOf<String>()
+                        ipScrambledApps.addAll(ipScramblerModule.appList)
+                        if (action.isIpScrambled) {
+                            ipScrambledApps.add(action.packageName)
+                        } else {
+                            ipScrambledApps.remove(action.packageName)
+                        }
+                        ipScramblerModule.appList = ipScrambledApps
+                        flowOf(Effect.IpScrambledAppsUpdatedEffect(ipScrambledApps = ipScrambledApps))
+                    }
+                    action is Action.SelectLocationAction -> {
+                        val locationId = state.availableLocationIds[action.position]
+                        ipScramblerModule.exitCountry = locationId
+                        flowOf(Effect.LocationSelectedEffect(locationId))
+                    }
+                    else -> flowOf(Effect.NoEffect)
+                }
+            },
+            singleEventProducer = { _, action, effect ->
+                when {
+                    effect is Effect.ErrorEffect -> SingleEvent.ErrorEvent(effect.message)
+
+                    action is Action.UseHiddenIPAction
+                        && effect is Effect.ShowAndroidVpnDisclaimerEffect ->
+                        SingleEvent.StartAndroidVpnActivityEvent(effect.intent)
+
+                    // Action.UseRealIPAction, Action.UseHiddenIPAction -> when (effect) {
+                    //     is Effect.ModeUpdatedEffect -> {
+                    //         if (effect.mode == InternetPrivacyMode.REAL_IP) {
+                    //             SingleEvent.RealIPSelectedEvent
+                    //         } else {
+                    //             SingleEvent.HiddenIPSelectedEvent
+                    //         }
+                    //     }
+                    //     is Effect.ErrorEffect -> {
+                    //         SingleEvent.ErrorEvent(effect.message)
+                    //     }
+                    // }
                     else -> null
                 }
             }
diff --git a/app/src/main/java/foundation/e/privacycentralapp/features/internetprivacy/InternetPrivacyFragment.kt b/app/src/main/java/foundation/e/privacycentralapp/features/internetprivacy/InternetPrivacyFragment.kt
index 5baae81..22e63e3 100644
--- a/app/src/main/java/foundation/e/privacycentralapp/features/internetprivacy/InternetPrivacyFragment.kt
+++ b/app/src/main/java/foundation/e/privacycentralapp/features/internetprivacy/InternetPrivacyFragment.kt
@@ -19,22 +19,41 @@ package foundation.e.privacycentralapp.features.internetprivacy
 
 import android.os.Bundle
 import android.view.View
+import android.widget.AdapterView
+import android.widget.ArrayAdapter
+import android.widget.ProgressBar
 import android.widget.RadioButton
+import android.widget.Spinner
+import android.widget.TextView
 import android.widget.Toast
+import androidx.activity.result.contract.ActivityResultContracts
 import androidx.fragment.app.viewModels
 import androidx.lifecycle.lifecycleScope
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
 import foundation.e.flowmvi.MVIView
+import foundation.e.privacycentralapp.DependencyContainer
+import foundation.e.privacycentralapp.PrivacyCentralApplication
 import foundation.e.privacycentralapp.R
 import foundation.e.privacycentralapp.common.NavToolbarFragment
-import foundation.e.privacycentralapp.dummy.InternetPrivacyMode
+import foundation.e.privacycentralapp.common.ToggleAppsAdapter
+import foundation.e.privacycentralapp.extensions.viewModelProviderFactoryOf
+import foundation.e.privacymodules.ipscramblermodule.IIpScramblerModule
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.collect
+import java.util.Locale
 
 class InternetPrivacyFragment :
     NavToolbarFragment(R.layout.fragment_internet_activity_policy),
     MVIView<InternetPrivacyFeature.State, InternetPrivacyFeature.Action> {
 
-    private val viewModel: InternetPrivacyViewModel by viewModels()
+    private val dependencyContainer: DependencyContainer by lazy {
+        (this.requireActivity().application as PrivacyCentralApplication).dependencyContainer
+    }
+
+    private val viewModel: InternetPrivacyViewModel by viewModels {
+        viewModelProviderFactoryOf { dependencyContainer.internetPrivacyViewModelFactory.create() }
+    }
 
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
@@ -45,6 +64,8 @@ class InternetPrivacyFragment :
             viewModel.internetPrivacyFeature.singleEvents.collect { event ->
                 when (event) {
                     is InternetPrivacyFeature.SingleEvent.ErrorEvent -> displayToast(event.error)
+                    is InternetPrivacyFeature.SingleEvent.StartAndroidVpnActivityEvent ->
+                        launchAndroidVpnDisclaimer.launch(event.intent)
                     InternetPrivacyFeature.SingleEvent.HiddenIPSelectedEvent -> displayToast("Your IP is hidden")
                     InternetPrivacyFeature.SingleEvent.RealIPSelectedEvent -> displayToast("Your IP is visible to internet")
                 }
@@ -60,8 +81,28 @@ class InternetPrivacyFragment :
             .show()
     }
 
+    private val launchAndroidVpnDisclaimer = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
+        viewModel.submitAction(InternetPrivacyFeature.Action.AndroidVpnActivityResultAction(it.resultCode))
+    }
+
     override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
         super.onViewCreated(view, savedInstanceState)
+
+        listOf(R.id.recycler_view_scrambled, R.id.recycler_view_to_select).forEach { viewId ->
+            view.findViewById<RecyclerView>(viewId)?.apply {
+                layoutManager = LinearLayoutManager(requireContext())
+                setHasFixedSize(true)
+                adapter = ToggleAppsAdapter { packageName, isIpScrambled ->
+                    viewModel.submitAction(
+                        InternetPrivacyFeature.Action.ToggleAppIpScrambled(
+                            packageName,
+                            isIpScrambled
+                        )
+                    )
+                }
+            }
+        }
+
         bindClickListeners(view)
     }
 
@@ -70,38 +111,112 @@ class InternetPrivacyFragment :
     private fun bindClickListeners(fragmentView: View) {
         fragmentView.let {
             it.findViewById<RadioButton>(R.id.radio_use_real_ip)
-                .setOnClickListener { radioButton ->
-                    toggleIP(radioButton)
+                .setOnClickListener {
+                    viewModel.submitAction(InternetPrivacyFeature.Action.UseRealIPAction)
                 }
             it.findViewById<RadioButton>(R.id.radio_use_hidden_ip)
-                .setOnClickListener { radioButton ->
-                    toggleIP(radioButton)
+                .setOnClickListener {
+                    viewModel.submitAction(InternetPrivacyFeature.Action.UseHiddenIPAction)
                 }
         }
     }
 
-    private fun toggleIP(radioButton: View?) {
-        if (radioButton is RadioButton) {
-            val checked = radioButton.isChecked
-            when (radioButton.id) {
-                R.id.radio_use_real_ip ->
-                    if (checked) {
-                        viewModel.submitAction(InternetPrivacyFeature.Action.UseRealIPAction)
+    override fun render(state: InternetPrivacyFeature.State) {
+        view?.let {
+            it.findViewById<RadioButton>(R.id.radio_use_hidden_ip).apply {
+                isChecked = state.mode in listOf(
+                    IIpScramblerModule.Status.ON,
+                    IIpScramblerModule.Status.STARTING
+                )
+                isEnabled = state.mode != IIpScramblerModule.Status.STARTING
+            }
+            it.findViewById<RadioButton>(R.id.radio_use_real_ip)?.apply {
+                isChecked =
+                    state.mode in listOf(
+                    IIpScramblerModule.Status.OFF,
+                    IIpScramblerModule.Status.STOPPING
+                )
+                isEnabled = state.mode != IIpScramblerModule.Status.STOPPING
+            }
+            it.findViewById<TextView>(R.id.ipscrambling_tor_status)?.apply {
+                when (state.mode) {
+                    IIpScramblerModule.Status.STARTING -> {
+                        text = getString(R.string.ipscrambling_is_starting)
+                        visibility = View.VISIBLE
+                    }
+                    IIpScramblerModule.Status.STOPPING -> {
+                        text = getString(R.string.ipscrambling_is_stopping)
+                        visibility = View.VISIBLE
                     }
-                R.id.radio_use_hidden_ip ->
-                    if (checked) {
-                        viewModel.submitAction(InternetPrivacyFeature.Action.UseHiddenIPAction)
+                    else -> {
+                        text = ""
+                        visibility = View.GONE
                     }
+                }
             }
-        }
-    }
 
-    override fun render(state: InternetPrivacyFeature.State) {
-        view?.let {
-            it.findViewById<RadioButton>(R.id.radio_use_hidden_ip).isChecked =
-                state.mode == InternetPrivacyMode.HIDE_IP
-            it.findViewById<RadioButton>(R.id.radio_use_real_ip).isChecked =
-                state.mode == InternetPrivacyMode.REAL_IP
+            it.findViewById<Spinner>(R.id.ipscrambling_select_location)?.apply {
+                adapter = ArrayAdapter(
+                    requireContext(), android.R.layout.simple_spinner_item,
+                    state.availableLocationIds.map {
+                        if (it == "") {
+                            getString(R.string.ipscrambling_any_location)
+                        } else {
+                            Locale("", it).displayCountry
+                        }
+                    }
+                ).apply {
+                    setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
+                }
+
+                setOnItemSelectedListener(object : AdapterView.OnItemSelectedListener {
+                    override fun onItemSelected(parentView: AdapterView<*>, selectedItemView: View, position: Int, id: Long) {
+                        viewModel.submitAction(InternetPrivacyFeature.Action.SelectLocationAction(position))
+                    }
+
+                    override fun onNothingSelected(parentView: AdapterView<*>?) {}
+                })
+
+                setSelection(state.selectedLocationPosition)
+            }
+
+            it.findViewById<TextView>(R.id.ipscrambling_activated)?.apply {
+                text = getString(
+                    if (state.isAllAppsScrambled) R.string.ipscrambling_all_apps_scrambled
+                    else R.string.ipscrambling_only_selected_apps_scrambled
+                )
+            }
+
+            it.findViewById<RecyclerView>(R.id.recycler_view_scrambled)?.apply {
+                (adapter as ToggleAppsAdapter?)?.dataSet = state.getScrambledApps()
+            }
+            it.findViewById<RecyclerView>(R.id.recycler_view_to_select)?.apply {
+                (adapter as ToggleAppsAdapter?)?.dataSet = state.getApps()
+            }
+
+            val viewIdsToHide = listOf(
+                R.id.ipscrambling_activated,
+                R.id.recycler_view_scrambled,
+                R.id.ipscrambling_select_apps,
+                R.id.recycler_view_to_select,
+                R.id.ipscrambling_location
+            )
+            val progressBar = it.findViewById<ProgressBar>(R.id.ipscrambling_loading)
+
+            when {
+                state.mode in listOf(
+                    IIpScramblerModule.Status.STARTING,
+                    IIpScramblerModule.Status.STOPPING
+                )
+                    || state.availableApps.isEmpty() -> {
+                    progressBar?.visibility = View.VISIBLE
+                    viewIdsToHide.forEach { viewId -> it.findViewById<View>(viewId)?.visibility = View.GONE }
+                }
+                else -> {
+                    progressBar?.visibility = View.GONE
+                    viewIdsToHide.forEach { viewId -> it.findViewById<View>(viewId)?.visibility = View.VISIBLE }
+                }
+            }
         }
     }
 
diff --git a/app/src/main/java/foundation/e/privacycentralapp/features/internetprivacy/InternetPrivacyViewModel.kt b/app/src/main/java/foundation/e/privacycentralapp/features/internetprivacy/InternetPrivacyViewModel.kt
index b66b611..a6455ee 100644
--- a/app/src/main/java/foundation/e/privacycentralapp/features/internetprivacy/InternetPrivacyViewModel.kt
+++ b/app/src/main/java/foundation/e/privacycentralapp/features/internetprivacy/InternetPrivacyViewModel.kt
@@ -19,17 +19,27 @@ package foundation.e.privacycentralapp.features.internetprivacy
 
 import androidx.lifecycle.ViewModel
 import androidx.lifecycle.viewModelScope
+import foundation.e.privacycentralapp.common.Factory
+import foundation.e.privacymodules.ipscramblermodule.IIpScramblerModule
+import foundation.e.privacymodules.permissions.PermissionsPrivacyModule
 import kotlinx.coroutines.flow.MutableSharedFlow
 import kotlinx.coroutines.flow.asSharedFlow
 import kotlinx.coroutines.launch
 
-class InternetPrivacyViewModel : ViewModel() {
+class InternetPrivacyViewModel(
+    private val ipScramblerModule: IIpScramblerModule,
+    private val permissionsModule: PermissionsPrivacyModule
+) : ViewModel() {
 
     private val _actions = MutableSharedFlow<InternetPrivacyFeature.Action>()
     val actions = _actions.asSharedFlow()
 
     val internetPrivacyFeature: InternetPrivacyFeature by lazy {
-        InternetPrivacyFeature.create(coroutineScope = viewModelScope)
+        InternetPrivacyFeature.create(
+            coroutineScope = viewModelScope,
+            ipScramblerModule = ipScramblerModule,
+            permissionsModule = permissionsModule
+        )
     }
 
     fun submitAction(action: InternetPrivacyFeature.Action) {
@@ -38,3 +48,13 @@ class InternetPrivacyViewModel : ViewModel() {
         }
     }
 }
+
+class InternetPrivacyViewModelFactory(
+    private val ipScramblerModule: IIpScramblerModule,
+    private val permissionsModule: PermissionsPrivacyModule
+) :
+    Factory<InternetPrivacyViewModel> {
+    override fun create(): InternetPrivacyViewModel {
+        return InternetPrivacyViewModel(ipScramblerModule, permissionsModule)
+    }
+}
diff --git a/app/src/main/res/layout/fragment_internet_activity_policy.xml b/app/src/main/res/layout/fragment_internet_activity_policy.xml
index c3021df..12094ab 100644
--- a/app/src/main/res/layout/fragment_internet_activity_policy.xml
+++ b/app/src/main/res/layout/fragment_internet_activity_policy.xml
@@ -11,11 +11,14 @@
 
     <androidx.core.widget.NestedScrollView
         android:layout_height="match_parent"
-        android:layout_marginBottom="32dp"
         android:layout_width="match_parent"
         app:layout_behavior="@string/appbar_scrolling_view_behavior"
         >
-
+        <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:orientation="vertical"
+            >
         <LinearLayout
             android:layout_height="match_parent"
             android:layout_width="match_parent"
@@ -97,6 +100,83 @@
                     android:textSize="14sp"
                     />
             </RadioGroup>
+            <TextView android:id="@+id/ipscrambling_tor_status"
+                android:layout_height="wrap_content"
+                android:layout_width="match_parent"
+                android:paddingTop="24dp"
+                android:text="@string/ipscrambling_is_starting"
+                android:textColor="@color/black"
+                android:textSize="16sp"
+                android:visibility="gone"
+                />
+
+            <ProgressBar
+                android:id="@+id/ipscrambling_loading"
+                android:layout_width="48dp"
+                android:layout_height="48dp"
+                android:layout_margin="24dp"
+                android:layout_gravity="center"
+                />
+
+            <LinearLayout
+                android:id="@+id/ipscrambling_location"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:orientation="horizontal"
+                android:paddingTop="32dp"
+                >
+                <TextView
+                    android:id="@+id/ipscrambling_location_label"
+                    android:layout_height="wrap_content"
+                    android:layout_width="wrap_content" android:text="@string/ipscrambling_location_label"
+                    android:textColor="@color/black"
+                    android:textSize="16sp"
+                    />
+                <Spinner android:id="@+id/ipscrambling_select_location"
+                    android:layout_width="0dp"
+                    android:layout_height="wrap_content"
+                    android:layout_weight="1"
+                    />
+            </LinearLayout>
+
+            <TextView
+                android:id="@+id/ipscrambling_activated"
+                android:layout_height="wrap_content"
+                android:layout_width="match_parent"
+                android:paddingTop="32dp"
+                android:text="@string/ipscrambling_all_apps_scrambled"
+                android:textColor="@color/black"
+                android:paddingBottom="8dp"
+                android:textSize="16sp"
+                android:visibility="gone"
+                />
+        </LinearLayout>
+
+            <androidx.recyclerview.widget.RecyclerView
+                android:id="@+id/recycler_view_scrambled"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:visibility="gone"
+                />
+            <TextView
+                android:id="@+id/ipscrambling_select_apps"
+                android:layout_height="wrap_content"
+                android:layout_width="match_parent"
+                android:paddingStart="32dp"
+                android:paddingEnd="32dp"
+                android:paddingTop="32dp"
+                android:paddingBottom="8dp"
+                android:text="@string/ipscrambling_select_app"
+                android:textColor="@color/black"
+                android:textSize="16sp"
+                android:visibility="gone"
+                />
+            <androidx.recyclerview.widget.RecyclerView
+                android:id="@+id/recycler_view_to_select"
+                android:layout_width="match_parent"
+                android:layout_height="match_parent"
+                android:visibility="gone"
+                />
         </LinearLayout>
     </androidx.core.widget.NestedScrollView>
 </androidx.coordinatorlayout.widget.CoordinatorLayout>
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 3105ddb..d18ccf5 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -28,6 +28,15 @@
     <string name="hidden_ip">Hide IP address</string>
     <string name="i_am_anonymous">I am anonymous on the internet</string>
     <string name="i_am_exposing">I am exposing my real IP address</string>
+    <string name="ipscrambling_all_apps_scrambled">All apps use hidden IP</string>
+    <string name="ipscrambling_only_selected_apps_scrambled">Only the following apps use hidden IP</string>
+    <string name="ipscrambling_select_app">Select Apps to hide IP</string>
+    <string name="ipscrambling_is_starting">Tor is starting...</string>
+    <string name="ipscrambling_is_stopping">Tor is stopping...</string>
+    <string name="ipscrambling_location_label">Hidden IP\'s location</string>
+    <string name="ipscrambling_any_location">any</string>
+
+    ipscrambling_any_location
     <string name="permission_control_info">Manage and control apps requesting various permissions.</string>
 
     <string name="apps_allowed">%1$d of %2$d apps allowed</string>
-- 
cgit v1.2.1