Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit c8a8c2b1 authored by Guillaume Jacquart's avatar Guillaume Jacquart
Browse files

Merge branch 'feature/ipscrambling' into 'master'

Feature/ipscrambling

See merge request e/privacy-central/privacycentralapp!7
parents daea2f95 5d0524a8
Loading
Loading
Loading
Loading
Loading
+3 −0
Original line number Diff line number Diff line
@@ -95,6 +95,9 @@ dependencies {
    googleImplementation project(":privacymodulesgoogle")
    // include the e specific version of the modules, just for the e flavor
    eImplementation project(":privacymodulese")

    implementation 'foundation.e:privacymodule.tor:0.1.0'

    implementation project(":flow-mvi")
    implementation Libs.Kotlin.stdlib
    implementation Libs.AndroidX.coreKtx
+8 −0
Original line number Diff line number Diff line
@@ -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)
    }
}
+73 −0
Original line number Diff line number Diff line
/*
 * 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
}
+180 −30
Original line number Diff line number Diff line
@@ -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"
                            )
                        )
            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) }
                        },
            singleEventProducer = { _, action, effect ->
                when (action) {
                    Action.UseRealIPAction, Action.UseHiddenIPAction -> when (effect) {
                        is Effect.ModeUpdatedEffect -> {
                            if (effect.mode == InternetPrivacyMode.REAL_IP) {
                                SingleEvent.RealIPSelectedEvent
                        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 {
                                flowOf(Effect.ErrorEffect("Vpn already started"))
                            }
                        } else {
                            flowOf(Effect.ErrorEffect("Vpn wasn't allowed to start"))
                        }

                    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 {
                                SingleEvent.HiddenIPSelectedEvent
                            ipScrambledApps.remove(action.packageName)
                        }
                        ipScramblerModule.appList = ipScrambledApps
                        flowOf(Effect.IpScrambledAppsUpdatedEffect(ipScrambledApps = ipScrambledApps))
                    }
                        is Effect.ErrorEffect -> {
                            SingleEvent.ErrorEvent(effect.message)
                    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
                }
            }
Loading