From 54892a227a77839ee81df90df904675f958831a3 Mon Sep 17 00:00:00 2001 From: Guillaume Jacquart Date: Mon, 23 Oct 2023 15:55:11 +0000 Subject: epic18: tracker control while tor is activated. --- .../foundation/e/advancedprivacy/KoinModule.kt | 11 +- .../foundation/e/advancedprivacy/Notifications.kt | 4 +- .../data/repositories/LocalStateRepository.kt | 4 +- .../domain/entities/InternetPrivacyMode.kt | 29 -- .../domain/entities/ShowFeaturesWarning.kt | 4 +- .../domain/usecases/GetQuickPrivacyStateUseCase.kt | 11 +- .../domain/usecases/IpScramblingStateUseCase.kt | 99 +++---- .../features/dashboard/DashboardFragment.kt | 10 +- .../features/dashboard/DashboardState.kt | 4 +- .../internetprivacy/InternetPrivacyFragment.kt | 24 +- .../internetprivacy/InternetPrivacyState.kt | 4 +- .../internetprivacy/InternetPrivacyViewModel.kt | 16 +- .../e/advancedprivacy/widget/WidgetUI.kt | 6 +- .../domain/entities/FeatureServiceState.kt | 6 +- gradle/libs.versions.toml | 2 +- ipscrambling/README.md | 2 +- ipscrambling/build.gradle | 4 +- ipscrambling/dependencies.gradle | 61 ++-- ipscrambling/exportdependencies/publish.gradle | 6 +- .../exportdependencies/update_dependencies.md | 4 +- .../ipscrambler/IpScramblerModule.kt | 308 --------------------- .../e/advancedprivacy/ipscrambler/KoinModule.kt | 2 +- .../ipscrambler/OrbotServiceSupervisor.kt | 308 +++++++++++++++++++++ settings.gradle | 2 + trackers/build.gradle | 1 + .../TrackersServiceSupervisor.kt | 5 + trackersservicee/build.gradle | 9 +- .../service/TrackersServiceSupervisorImpl.kt | 7 + trackersservicestandalone/build.gradle | 9 +- .../service/TrackersServiceSupervisorImpl.kt | 11 +- .../trackers/service/usecases/ResolveDNSUseCase.kt | 33 ++- 31 files changed, 486 insertions(+), 520 deletions(-) delete mode 100644 app/src/main/java/foundation/e/advancedprivacy/domain/entities/InternetPrivacyMode.kt delete mode 100644 ipscrambling/src/main/java/foundation/e/advancedprivacy/ipscrambler/IpScramblerModule.kt create mode 100644 ipscrambling/src/main/java/foundation/e/advancedprivacy/ipscrambler/OrbotServiceSupervisor.kt diff --git a/app/src/main/java/foundation/e/advancedprivacy/KoinModule.kt b/app/src/main/java/foundation/e/advancedprivacy/KoinModule.kt index 20cefd5..fbf1252 100644 --- a/app/src/main/java/foundation/e/advancedprivacy/KoinModule.kt +++ b/app/src/main/java/foundation/e/advancedprivacy/KoinModule.kt @@ -42,8 +42,6 @@ import foundation.e.advancedprivacy.features.trackers.TrackersViewModel import foundation.e.advancedprivacy.features.trackers.apptrackers.AppTrackersViewModel import foundation.e.advancedprivacy.ipscrambler.ipScramblerModule import foundation.e.advancedprivacy.permissions.externalinterfaces.PermissionsPrivacyModuleImpl -import foundation.e.advancedprivacy.trackers.domain.externalinterfaces.TrackersServiceSupervisor -import foundation.e.advancedprivacy.trackers.service.TrackersServiceSupervisorImpl import foundation.e.advancedprivacy.trackers.service.trackerServiceModule import foundation.e.advancedprivacy.trackers.trackersModule import org.koin.android.ext.koin.androidContext @@ -122,11 +120,12 @@ val appModule = module { singleOf(::GetQuickPrivacyStateUseCase) single { IpScramblingStateUseCase( - ipScramblerModule = get(), + orbotServiceSupervisor = get(), permissionsPrivacyModule = get(), appDesc = get(named("AdvancedPrivacy")), localStateRepository = get(), appListsRepository = get(), + trackersServiceSupervisor = get(), coroutineScope = get() ) } @@ -138,12 +137,6 @@ val appModule = module { PermissionsPrivacyModuleImpl(context = androidContext()) } - single { - TrackersServiceSupervisorImpl( - context = androidContext(), - ) - } - viewModel { parameters -> val appListUseCase: AppListUseCase = get() val app = appListUseCase.getApp(parameters.get()) diff --git a/app/src/main/java/foundation/e/advancedprivacy/Notifications.kt b/app/src/main/java/foundation/e/advancedprivacy/Notifications.kt index 455b1a7..639ede4 100644 --- a/app/src/main/java/foundation/e/advancedprivacy/Notifications.kt +++ b/app/src/main/java/foundation/e/advancedprivacy/Notifications.kt @@ -29,7 +29,7 @@ import foundation.e.advancedprivacy.domain.entities.CHANNEL_FAKE_LOCATION_FLAG import foundation.e.advancedprivacy.domain.entities.CHANNEL_FIRST_BOOT import foundation.e.advancedprivacy.domain.entities.CHANNEL_IPSCRAMBLING_FLAG import foundation.e.advancedprivacy.domain.entities.CHANNEL_TRACKER_FLAG -import foundation.e.advancedprivacy.domain.entities.InternetPrivacyMode +import foundation.e.advancedprivacy.domain.entities.FeatureServiceState import foundation.e.advancedprivacy.domain.entities.MainFeatures import foundation.e.advancedprivacy.domain.entities.NOTIFICATION_FAKE_LOCATION_FLAG import foundation.e.advancedprivacy.domain.entities.NOTIFICATION_FIRST_BOOT @@ -105,7 +105,7 @@ object Notifications { }.launchIn(appScope) getQuickPrivacyStateUseCase.ipScramblingMode.map { - it != InternetPrivacyMode.REAL_IP + it != FeatureServiceState.OFF }.distinctUntilChanged().onEach { if (it) { showFlagNotification(appContext, MainFeatures.IP_SCRAMBLING) diff --git a/app/src/main/java/foundation/e/advancedprivacy/data/repositories/LocalStateRepository.kt b/app/src/main/java/foundation/e/advancedprivacy/data/repositories/LocalStateRepository.kt index abc4de0..c7d4a27 100644 --- a/app/src/main/java/foundation/e/advancedprivacy/data/repositories/LocalStateRepository.kt +++ b/app/src/main/java/foundation/e/advancedprivacy/data/repositories/LocalStateRepository.kt @@ -21,7 +21,7 @@ package foundation.e.advancedprivacy.data.repositories import android.content.Context import android.content.Intent import foundation.e.advancedprivacy.domain.entities.ApplicationDescription -import foundation.e.advancedprivacy.domain.entities.InternetPrivacyMode +import foundation.e.advancedprivacy.domain.entities.FeatureServiceState import foundation.e.advancedprivacy.domain.entities.LocationMode import foundation.e.advancedprivacy.domain.entities.ShowFeaturesWarning import kotlinx.coroutines.flow.MutableSharedFlow @@ -89,7 +89,7 @@ class LocalStateRepository(context: Context) { _ipScramblingSetting.update { enabled } } - val internetPrivacyMode: MutableStateFlow = MutableStateFlow(InternetPrivacyMode.REAL_IP) + val internetPrivacyMode: MutableStateFlow = MutableStateFlow(FeatureServiceState.OFF) private val _startVpnDisclaimer = MutableSharedFlow() suspend fun emitStartVpnDisclaimer(intent: Intent?) { diff --git a/app/src/main/java/foundation/e/advancedprivacy/domain/entities/InternetPrivacyMode.kt b/app/src/main/java/foundation/e/advancedprivacy/domain/entities/InternetPrivacyMode.kt deleted file mode 100644 index 986e798..0000000 --- a/app/src/main/java/foundation/e/advancedprivacy/domain/entities/InternetPrivacyMode.kt +++ /dev/null @@ -1,29 +0,0 @@ -/* - * 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 . - */ - -package foundation.e.advancedprivacy.domain.entities - -enum class InternetPrivacyMode { - REAL_IP, - HIDE_IP, - HIDE_IP_LOADING, - REAL_IP_LOADING; - - val isChecked get() = this == HIDE_IP || this == HIDE_IP_LOADING - - val isLoading get() = this == HIDE_IP_LOADING || this == REAL_IP_LOADING -} diff --git a/app/src/main/java/foundation/e/advancedprivacy/domain/entities/ShowFeaturesWarning.kt b/app/src/main/java/foundation/e/advancedprivacy/domain/entities/ShowFeaturesWarning.kt index 221f4e1..0d8e0e8 100644 --- a/app/src/main/java/foundation/e/advancedprivacy/domain/entities/ShowFeaturesWarning.kt +++ b/app/src/main/java/foundation/e/advancedprivacy/domain/entities/ShowFeaturesWarning.kt @@ -21,9 +21,11 @@ import android.content.Intent import android.os.Parcelable import kotlinx.parcelize.Parcelize -@Parcelize sealed class ShowFeaturesWarning : Parcelable { + @Parcelize object TrackersControl : ShowFeaturesWarning() + @Parcelize object FakeLocation : ShowFeaturesWarning() + @Parcelize data class IpScrambling(val startVpnDisclaimer: Intent? = null) : ShowFeaturesWarning() } diff --git a/app/src/main/java/foundation/e/advancedprivacy/domain/usecases/GetQuickPrivacyStateUseCase.kt b/app/src/main/java/foundation/e/advancedprivacy/domain/usecases/GetQuickPrivacyStateUseCase.kt index bc4871a..1b8f62c 100644 --- a/app/src/main/java/foundation/e/advancedprivacy/domain/usecases/GetQuickPrivacyStateUseCase.kt +++ b/app/src/main/java/foundation/e/advancedprivacy/domain/usecases/GetQuickPrivacyStateUseCase.kt @@ -19,7 +19,7 @@ package foundation.e.advancedprivacy.domain.usecases import foundation.e.advancedprivacy.data.repositories.LocalStateRepository import foundation.e.advancedprivacy.domain.entities.ApplicationDescription -import foundation.e.advancedprivacy.domain.entities.InternetPrivacyMode +import foundation.e.advancedprivacy.domain.entities.FeatureServiceState import foundation.e.advancedprivacy.domain.entities.LocationMode import foundation.e.advancedprivacy.domain.entities.QuickPrivacyState import foundation.e.advancedprivacy.domain.entities.TrackerMode @@ -41,13 +41,13 @@ class GetQuickPrivacyStateUseCase( when { !isBlockTrackers && locationMode == LocationMode.REAL_LOCATION && - internetPrivacyMode == InternetPrivacyMode.REAL_IP -> QuickPrivacyState.DISABLED + internetPrivacyMode == FeatureServiceState.OFF -> QuickPrivacyState.DISABLED isAllTrackersBlocked && locationMode != LocationMode.REAL_LOCATION && internetPrivacyMode in listOf( - InternetPrivacyMode.HIDE_IP, - InternetPrivacyMode.HIDE_IP_LOADING + FeatureServiceState.ON, + FeatureServiceState.STARTING ) -> QuickPrivacyState.FULL_ENABLED else -> QuickPrivacyState.ENABLED @@ -71,7 +71,8 @@ class GetQuickPrivacyStateUseCase( val locationMode: StateFlow = localStateRepository.locationMode - val ipScramblingMode: Flow = localStateRepository.internetPrivacyMode + val ipScramblingMode: Flow = + localStateRepository.internetPrivacyMode fun toggleTrackers(enabled: Boolean?) { val value = enabled ?: !localStateRepository.blockTrackers.value diff --git a/app/src/main/java/foundation/e/advancedprivacy/domain/usecases/IpScramblingStateUseCase.kt b/app/src/main/java/foundation/e/advancedprivacy/domain/usecases/IpScramblingStateUseCase.kt index 9c89329..79c79f7 100644 --- a/app/src/main/java/foundation/e/advancedprivacy/domain/usecases/IpScramblingStateUseCase.kt +++ b/app/src/main/java/foundation/e/advancedprivacy/domain/usecases/IpScramblingStateUseCase.kt @@ -23,74 +23,40 @@ import foundation.e.advancedprivacy.common.isStandaloneBuild import foundation.e.advancedprivacy.data.repositories.AppListsRepository import foundation.e.advancedprivacy.data.repositories.LocalStateRepository import foundation.e.advancedprivacy.domain.entities.ApplicationDescription -import foundation.e.advancedprivacy.domain.entities.InternetPrivacyMode -import foundation.e.advancedprivacy.domain.entities.InternetPrivacyMode.HIDE_IP -import foundation.e.advancedprivacy.domain.entities.InternetPrivacyMode.HIDE_IP_LOADING -import foundation.e.advancedprivacy.domain.entities.InternetPrivacyMode.REAL_IP -import foundation.e.advancedprivacy.domain.entities.InternetPrivacyMode.REAL_IP_LOADING +import foundation.e.advancedprivacy.domain.entities.FeatureServiceState import foundation.e.advancedprivacy.externalinterfaces.permissions.IPermissionsPrivacyModule -import foundation.e.advancedprivacy.ipscrambler.IpScramblerModule +import foundation.e.advancedprivacy.ipscrambler.OrbotServiceSupervisor +import foundation.e.advancedprivacy.trackers.domain.externalinterfaces.TrackersServiceSupervisor import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.channels.awaitClose -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.callbackFlow -import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch class IpScramblingStateUseCase( - private val ipScramblerModule: IpScramblerModule, + private val orbotServiceSupervisor: OrbotServiceSupervisor, private val permissionsPrivacyModule: IPermissionsPrivacyModule, private val appDesc: ApplicationDescription, private val localStateRepository: LocalStateRepository, private val appListsRepository: AppListsRepository, + private val trackersServiceSupervisor: TrackersServiceSupervisor, private val coroutineScope: CoroutineScope ) { - val internetPrivacyMode: StateFlow = callbackFlow { - val listener = object : IpScramblerModule.Listener { - override fun onStatusChanged(newStatus: IpScramblerModule.Status) { - trySend(map(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) } - }.stateIn( - scope = coroutineScope, - started = SharingStarted.Eagerly, - initialValue = REAL_IP - ) + val internetPrivacyMode: StateFlow = orbotServiceSupervisor.state init { + orbotServiceSupervisor.requestStatus() + coroutineScope.launch(Dispatchers.Default) { localStateRepository.ipScramblingSetting.collect { applySettings(it) } } - coroutineScope.launch(Dispatchers.IO) { - internetPrivacyMode.collect { - if ( - it == REAL_IP && - localStateRepository.internetPrivacyMode.value == REAL_IP_LOADING - ) { - // Wait for orbot to relax before allowing user to reactivate it. - delay(1000) - } - localStateRepository.internetPrivacyMode.value = it - } - } + orbotServiceSupervisor.state.map { + localStateRepository.internetPrivacyMode.value = it + }.launchIn(coroutineScope) } fun toggle(hideIp: Boolean) { @@ -102,7 +68,7 @@ class IpScramblingStateUseCase( } val bypassTorApps: Set get() { - var whitelist = ipScramblerModule.appList + var whitelist = orbotServiceSupervisor.appList if (getHiddenPackageNames().any { it in whitelist }) { val mutable = whitelist.toMutableSet() mutable.removeAll(getHiddenPackageNames()) @@ -120,7 +86,7 @@ class IpScramblingStateUseCase( fun toggleBypassTor(packageName: String) { val visibleList = bypassTorApps.toMutableSet() - val rawList = ipScramblerModule.appList.toMutableSet() + val rawList = orbotServiceSupervisor.appList.toMutableSet() if (visibleList.contains(packageName)) { if (packageName == appListsRepository.dummySystemApp.packageName) { @@ -139,24 +105,34 @@ class IpScramblingStateUseCase( rawList.add(packageName) } } - ipScramblerModule.appList = rawList + orbotServiceSupervisor.appList = rawList + } + + val availablesLocations: List = orbotServiceSupervisor.getAvailablesLocations().sorted() + + val exitCountry: String get() = orbotServiceSupervisor.getExitCountryCode() + + suspend fun setExitCountry(locationId: String) { + if (locationId != exitCountry) { + orbotServiceSupervisor.setExitCountryCode(locationId) + } } private suspend fun applySettings(isIpScramblingEnabled: Boolean) { val currentMode = localStateRepository.internetPrivacyMode.value when { - isIpScramblingEnabled && currentMode in setOf(REAL_IP, REAL_IP_LOADING) -> + isIpScramblingEnabled && currentMode in setOf(FeatureServiceState.OFF, FeatureServiceState.STOPPING) -> applyStartIpScrambling() - !isIpScramblingEnabled && currentMode in setOf(HIDE_IP, HIDE_IP_LOADING) -> - ipScramblerModule.stop() + !isIpScramblingEnabled && currentMode in setOf(FeatureServiceState.ON, FeatureServiceState.STARTING) -> + orbotServiceSupervisor.stop() else -> {} } } private suspend fun applyStartIpScrambling() { - val authorizeVpnIntent = ipScramblerModule.prepareAndroidVpn() + val authorizeVpnIntent = orbotServiceSupervisor.prepareAndroidVpn() if (authorizeVpnIntent == null) { localStateRepository.emitStartVpnDisclaimer(null) @@ -190,17 +166,8 @@ class IpScramblingStateUseCase( } fun startIpScrambling() { - localStateRepository.internetPrivacyMode.value = HIDE_IP_LOADING - ipScramblerModule.start(enableNotification = isStandaloneBuild) - } - - private fun map(status: IpScramblerModule.Status): InternetPrivacyMode { - return when (status) { - IpScramblerModule.Status.OFF -> REAL_IP - IpScramblerModule.Status.ON -> HIDE_IP - IpScramblerModule.Status.STARTING -> HIDE_IP_LOADING - IpScramblerModule.Status.STOPPING, - IpScramblerModule.Status.START_DISABLED -> REAL_IP_LOADING - } + localStateRepository.internetPrivacyMode.value = FeatureServiceState.STARTING + orbotServiceSupervisor.setDNSFilter((trackersServiceSupervisor.dnsFilterForIpScrambling)) + orbotServiceSupervisor.start(enableNotification = isStandaloneBuild) } } diff --git a/app/src/main/java/foundation/e/advancedprivacy/features/dashboard/DashboardFragment.kt b/app/src/main/java/foundation/e/advancedprivacy/features/dashboard/DashboardFragment.kt index 999955e..0a53c6c 100644 --- a/app/src/main/java/foundation/e/advancedprivacy/features/dashboard/DashboardFragment.kt +++ b/app/src/main/java/foundation/e/advancedprivacy/features/dashboard/DashboardFragment.kt @@ -34,7 +34,7 @@ import foundation.e.advancedprivacy.R import foundation.e.advancedprivacy.common.GraphHolder import foundation.e.advancedprivacy.common.NavToolbarFragment import foundation.e.advancedprivacy.databinding.FragmentDashboardBinding -import foundation.e.advancedprivacy.domain.entities.InternetPrivacyMode +import foundation.e.advancedprivacy.domain.entities.FeatureServiceState import foundation.e.advancedprivacy.domain.entities.LocationMode import foundation.e.advancedprivacy.domain.entities.QuickPrivacyState import foundation.e.advancedprivacy.domain.entities.TrackerMode @@ -186,11 +186,11 @@ class DashboardFragment : NavToolbarFragment(R.layout.fragment_dashboard) { binding.toggleIpscrambling.isChecked = state.ipScramblingMode.isChecked val isLoading = state.ipScramblingMode.isLoading binding.toggleIpscrambling.isEnabled = ( - state.ipScramblingMode != InternetPrivacyMode.REAL_IP_LOADING + state.ipScramblingMode != FeatureServiceState.STOPPING ) binding.stateIpAddress.text = getString( - if (state.ipScramblingMode == InternetPrivacyMode.HIDE_IP) R.string.dashboard_state_ipaddress_on + if (state.ipScramblingMode == FeatureServiceState.ON) R.string.dashboard_state_ipaddress_on else R.string.dashboard_state_ipaddress_off ) @@ -200,7 +200,7 @@ class DashboardFragment : NavToolbarFragment(R.layout.fragment_dashboard) { binding.stateIpAddress.setTextColor( getColor( requireContext(), - if (state.ipScramblingMode == InternetPrivacyMode.HIDE_IP) R.color.green_valid + if (state.ipScramblingMode == FeatureServiceState.ON) R.color.green_valid else R.color.red_off ) ) @@ -250,7 +250,7 @@ class DashboardFragment : NavToolbarFragment(R.layout.fragment_dashboard) { ) binding.internetActivityPrivacy.subTitle = getString( - if (state.ipScramblingMode == InternetPrivacyMode.HIDE_IP) R.string.dashboard_internet_activity_privacy_subtitle_on + if (state.ipScramblingMode == FeatureServiceState.ON) R.string.dashboard_internet_activity_privacy_subtitle_on else R.string.dashboard_internet_activity_privacy_subtitle_off ) diff --git a/app/src/main/java/foundation/e/advancedprivacy/features/dashboard/DashboardState.kt b/app/src/main/java/foundation/e/advancedprivacy/features/dashboard/DashboardState.kt index 8fc8767..069ff04 100644 --- a/app/src/main/java/foundation/e/advancedprivacy/features/dashboard/DashboardState.kt +++ b/app/src/main/java/foundation/e/advancedprivacy/features/dashboard/DashboardState.kt @@ -17,7 +17,7 @@ package foundation.e.advancedprivacy.features.dashboard -import foundation.e.advancedprivacy.domain.entities.InternetPrivacyMode +import foundation.e.advancedprivacy.domain.entities.FeatureServiceState import foundation.e.advancedprivacy.domain.entities.LocationMode import foundation.e.advancedprivacy.domain.entities.QuickPrivacyState import foundation.e.advancedprivacy.domain.entities.TrackerMode @@ -26,7 +26,7 @@ data class DashboardState( val quickPrivacyState: QuickPrivacyState = QuickPrivacyState.DISABLED, val trackerMode: TrackerMode = TrackerMode.VULNERABLE, val isLocationHidden: Boolean = false, - val ipScramblingMode: InternetPrivacyMode = InternetPrivacyMode.REAL_IP_LOADING, + val ipScramblingMode: FeatureServiceState = FeatureServiceState.STOPPING, val locationMode: LocationMode = LocationMode.REAL_LOCATION, val leakedTrackersCount: Int? = null, val trackersCount: Int? = null, diff --git a/app/src/main/java/foundation/e/advancedprivacy/features/internetprivacy/InternetPrivacyFragment.kt b/app/src/main/java/foundation/e/advancedprivacy/features/internetprivacy/InternetPrivacyFragment.kt index 1180af3..b4fc8a1 100644 --- a/app/src/main/java/foundation/e/advancedprivacy/features/internetprivacy/InternetPrivacyFragment.kt +++ b/app/src/main/java/foundation/e/advancedprivacy/features/internetprivacy/InternetPrivacyFragment.kt @@ -32,7 +32,7 @@ import foundation.e.advancedprivacy.common.NavToolbarFragment import foundation.e.advancedprivacy.common.ToggleAppsAdapter import foundation.e.advancedprivacy.common.setToolTipForAsterisk import foundation.e.advancedprivacy.databinding.FragmentInternetActivityPolicyBinding -import foundation.e.advancedprivacy.domain.entities.InternetPrivacyMode +import foundation.e.advancedprivacy.domain.entities.FeatureServiceState import kotlinx.coroutines.launch import org.koin.androidx.viewmodel.ext.android.viewModel import java.util.Locale @@ -136,19 +136,12 @@ class InternetPrivacyFragment : NavToolbarFragment(R.layout.fragment_internet_ac private fun render(state: InternetPrivacyState) { binding.radioUseHiddenIp.radiobutton.apply { - isChecked = state.mode in listOf( - InternetPrivacyMode.HIDE_IP, - InternetPrivacyMode.HIDE_IP_LOADING - ) - isEnabled = state.mode != InternetPrivacyMode.HIDE_IP_LOADING + isChecked = state.mode.isChecked + isEnabled = state.mode != FeatureServiceState.STARTING } binding.radioUseRealIp.radiobutton.apply { - isChecked = - state.mode in listOf( - InternetPrivacyMode.REAL_IP, - InternetPrivacyMode.REAL_IP_LOADING - ) - isEnabled = state.mode != InternetPrivacyMode.REAL_IP_LOADING + isChecked = !state.mode.isChecked + isEnabled = state.mode != FeatureServiceState.STOPPING } binding.ipscramblingSelectLocation.setSelection(state.selectedLocationPosition) @@ -157,7 +150,7 @@ class InternetPrivacyFragment : NavToolbarFragment(R.layout.fragment_internet_ac binding.apps.post { (binding.apps.adapter as ToggleAppsAdapter?)?.setData( list = state.getApps(), - isEnabled = state.mode == InternetPrivacyMode.HIDE_IP + isEnabled = state.mode == FeatureServiceState.ON ) } @@ -170,10 +163,7 @@ class InternetPrivacyFragment : NavToolbarFragment(R.layout.fragment_internet_ac ) when { - state.mode in listOf( - InternetPrivacyMode.HIDE_IP_LOADING, - InternetPrivacyMode.REAL_IP_LOADING - ) + state.mode.isLoading || state.availableApps.isEmpty() -> { binding.loader.visibility = View.VISIBLE viewIdsToHide.forEach { it.visibility = View.GONE } diff --git a/app/src/main/java/foundation/e/advancedprivacy/features/internetprivacy/InternetPrivacyState.kt b/app/src/main/java/foundation/e/advancedprivacy/features/internetprivacy/InternetPrivacyState.kt index 4d0fb38..e607d6c 100644 --- a/app/src/main/java/foundation/e/advancedprivacy/features/internetprivacy/InternetPrivacyState.kt +++ b/app/src/main/java/foundation/e/advancedprivacy/features/internetprivacy/InternetPrivacyState.kt @@ -18,10 +18,10 @@ package foundation.e.advancedprivacy.features.internetprivacy import foundation.e.advancedprivacy.domain.entities.ApplicationDescription -import foundation.e.advancedprivacy.domain.entities.InternetPrivacyMode +import foundation.e.advancedprivacy.domain.entities.FeatureServiceState data class InternetPrivacyState( - val mode: InternetPrivacyMode = InternetPrivacyMode.REAL_IP, + val mode: FeatureServiceState = FeatureServiceState.OFF, val availableApps: List = emptyList(), val bypassTorApps: Collection = emptyList(), val selectedLocation: String = "", diff --git a/app/src/main/java/foundation/e/advancedprivacy/features/internetprivacy/InternetPrivacyViewModel.kt b/app/src/main/java/foundation/e/advancedprivacy/features/internetprivacy/InternetPrivacyViewModel.kt index 059e11d..10530e1 100644 --- a/app/src/main/java/foundation/e/advancedprivacy/features/internetprivacy/InternetPrivacyViewModel.kt +++ b/app/src/main/java/foundation/e/advancedprivacy/features/internetprivacy/InternetPrivacyViewModel.kt @@ -22,11 +22,10 @@ import androidx.annotation.StringRes import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import foundation.e.advancedprivacy.R -import foundation.e.advancedprivacy.domain.entities.InternetPrivacyMode +import foundation.e.advancedprivacy.domain.entities.FeatureServiceState import foundation.e.advancedprivacy.domain.usecases.AppListUseCase import foundation.e.advancedprivacy.domain.usecases.GetQuickPrivacyStateUseCase import foundation.e.advancedprivacy.domain.usecases.IpScramblingStateUseCase -import foundation.e.advancedprivacy.ipscrambler.IpScramblerModule import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.MutableSharedFlow @@ -41,7 +40,6 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext class InternetPrivacyViewModel( - private val ipScramblerModule: IpScramblerModule, private val getQuickPrivacyStateUseCase: GetQuickPrivacyStateUseCase, private val ipScramblingStateUseCase: IpScramblingStateUseCase, private val appListUseCase: AppListUseCase @@ -56,7 +54,7 @@ class InternetPrivacyViewModel( private val _singleEvents = MutableSharedFlow() val singleEvents = _singleEvents.asSharedFlow() - val availablesLocationsIds = listOf("", *ipScramblerModule.getAvailablesLocations().sorted().toTypedArray()) + val availablesLocationsIds = listOf("", *ipScramblingStateUseCase.availablesLocations.toTypedArray()) init { viewModelScope.launch(Dispatchers.IO) { @@ -64,7 +62,7 @@ class InternetPrivacyViewModel( it.copy( mode = ipScramblingStateUseCase.internetPrivacyMode.value, availableLocationIds = availablesLocationsIds, - selectedLocation = ipScramblerModule.exitCountry + selectedLocation = ipScramblingStateUseCase.exitCountry ) } } @@ -90,7 +88,7 @@ class InternetPrivacyViewModel( launch { ipScramblingStateUseCase.internetPrivacyMode - .map { it == InternetPrivacyMode.HIDE_IP_LOADING } + .map { it == FeatureServiceState.STARTING } .debounce(WARNING_LOADING_LONG_DELAY) .collect { if (it) _singleEvents.emit( @@ -136,10 +134,8 @@ class InternetPrivacyViewModel( private suspend fun actionSelectLocation(action: Action.SelectLocationAction) = withContext(Dispatchers.IO) { val locationId = _state.value.availableLocationIds[action.position] - if (locationId != ipScramblerModule.exitCountry) { - ipScramblerModule.exitCountry = locationId - _state.update { it.copy(selectedLocation = locationId) } - } + ipScramblingStateUseCase.setExitCountry(locationId) + _state.update { it.copy(selectedLocation = locationId) } } sealed class SingleEvent { diff --git a/app/src/main/java/foundation/e/advancedprivacy/widget/WidgetUI.kt b/app/src/main/java/foundation/e/advancedprivacy/widget/WidgetUI.kt index bfd7d1a..1bd8693 100644 --- a/app/src/main/java/foundation/e/advancedprivacy/widget/WidgetUI.kt +++ b/app/src/main/java/foundation/e/advancedprivacy/widget/WidgetUI.kt @@ -31,7 +31,7 @@ import foundation.e.advancedprivacy.R import foundation.e.advancedprivacy.Widget import foundation.e.advancedprivacy.Widget.Companion.isDarkText import foundation.e.advancedprivacy.common.extensions.dpToPxF -import foundation.e.advancedprivacy.domain.entities.InternetPrivacyMode +import foundation.e.advancedprivacy.domain.entities.FeatureServiceState import foundation.e.advancedprivacy.domain.entities.QuickPrivacyState import foundation.e.advancedprivacy.domain.entities.TrackerMode import foundation.e.advancedprivacy.features.dashboard.DashboardFragmentArgs @@ -45,7 +45,7 @@ data class State( val quickPrivacyState: QuickPrivacyState = QuickPrivacyState.DISABLED, val trackerMode: TrackerMode = TrackerMode.VULNERABLE, val isLocationHidden: Boolean = false, - val ipScramblingMode: InternetPrivacyMode = InternetPrivacyMode.REAL_IP_LOADING, + val ipScramblingMode: FeatureServiceState = FeatureServiceState.STOPPING, val dayStatistics: List> = emptyList(), val activeTrackersCount: Int = 0, ) @@ -157,7 +157,7 @@ fun render( setTextViewText( R.id.state_ip_address, context.getString( - if (state.ipScramblingMode == InternetPrivacyMode.HIDE_IP) R.string.widget_state_ipaddress_on + if (state.ipScramblingMode == FeatureServiceState.ON) R.string.widget_state_ipaddress_on else R.string.widget_state_ipaddress_off ) ) diff --git a/core/src/main/java/foundation/e/advancedprivacy/domain/entities/FeatureServiceState.kt b/core/src/main/java/foundation/e/advancedprivacy/domain/entities/FeatureServiceState.kt index 6bfecbb..f079c56 100644 --- a/core/src/main/java/foundation/e/advancedprivacy/domain/entities/FeatureServiceState.kt +++ b/core/src/main/java/foundation/e/advancedprivacy/domain/entities/FeatureServiceState.kt @@ -17,5 +17,9 @@ package foundation.e.advancedprivacy.domain.entities enum class FeatureServiceState { - OFF, ON, STARTING, STOPPING + OFF, ON, STARTING, STOPPING; + + val isChecked get() = this == ON || this == STARTING + + val isLoading get() = this == STARTING || this == STOPPING } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 00bf753..ec4122e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -7,7 +7,7 @@ androidx-navigation = "2.5.3" androidx-test = "1.3.0" androidx-lifecycle = "2.5.0" androidx-room = "2.3.0" -orbotservice = "orbot-16.6.3-1" +orbotservice = "orbot-16.6.3-2" retrofit = "2.9.0" pcap4j = "1.8.2" diff --git a/ipscrambling/README.md b/ipscrambling/README.md index 777b6a1..be51828 100644 --- a/ipscrambling/README.md +++ b/ipscrambling/README.md @@ -22,7 +22,7 @@ This will put compiled AAR and pom file exposing their dependencies in the local repository (usually in ~/.m2/repository). -To push release on gitlab +To push release on gitlab (you will need a valid gitLabPrivateToken in ~/.gradle/gradle.properties) ./gradlew --console=verbose publish diff --git a/ipscrambling/build.gradle b/ipscrambling/build.gradle index 39efce7..29fed4f 100644 --- a/ipscrambling/build.gradle +++ b/ipscrambling/build.gradle @@ -50,7 +50,9 @@ dependencies { libs.bundles.koin, libs.bundles.kotlin.android.coroutines, libs.androidx.localbroadcast, + libs.pcap4j, + libs.timber ) implementation libs.e.orbotservice - //implementation project(':ipscrambling:orbotservice') + implementation project(':core') } diff --git a/ipscrambling/dependencies.gradle b/ipscrambling/dependencies.gradle index 412bf9a..31b6b78 100644 --- a/ipscrambling/dependencies.gradle +++ b/ipscrambling/dependencies.gradle @@ -1,5 +1,5 @@ ext { - versions = [ + orbotversions = [ android_material : "1.4.0", android_shell : "1.0.0", android_snowfall : "1.2.1", @@ -26,36 +26,37 @@ ext { tor_android : "0.4.7.10", pcap_core : "1.8.2", pcap_factory : "1.8.2", - orbot_service : libs.versions.orbotservice, + orbot_service : "orbot-16.6.3-2", //libs.versions.orbotservice, + orbot_service_dependencies : "orbot-16.6.3-1", ] - libs = [ - android_material : "com.google.android.material:material:$versions.android_material", - android_shell : "com.jaredrummler:android-shell:$versions.android_shell", - android_snowfall : "com.github.jetradarmobile:android-snowfall:$versions.android_snowfall", - android_volley : "com.android.volley:volley:$versions.android_volley", - androidx_appcompat : "androidx.appcompat:appcompat:$versions.androidx_appcompat", - androidx_constraint : "androidx.constraintlayout:constraintlayout:$versions.androidx_constraint", - androidx_coordinator : "androidx.coordinatorlayout:coordinatorlayout:$versions.androidx_coordinator", - androidx_core : "androidx.core:core:$versions.androidx_core", - androidx_leanback_core : "androidx.leanback:leanback:$versions.androidx_leanback", - androidx_leanback_paging : "androidx.leanback:leanback-paging:$versions.androidx_leanback_paging", - androidx_leanback_preferences : "androidx.leanback:leanback-preference:$versions.androidx_leanback", - androidx_leanback_tab : "androidx.leanback:leanback-tab:$versions.androidx_leanback_tab", - androidx_localbroadcast : "androidx.localbroadcastmanager:localbroadcastmanager:$versions.androidx_localbroadcast", - androidx_multidex : "androidx.multidex:multidex:$versions.androidx_multidex", - androidx_palette : "androidx.palette:palette:$versions.androidx_palette", - androidx_recyclerview : "androidx.recyclerview:recyclerview:$versions.androidx_recyclerview", - apl_appintro : "com.github.apl-devs:appintro:$versions.apl_appintro", - bclogic_pulsator : "pl.bclogic:pulsator4droid:$versions.bclogic_pulsator", - fastlane_screengrab : "tools.fastlane:screengrab:$versions.fastlane_screengrab", - guardian_geoip : "foundation.e:geoip:$versions.orbot_service", - guardian_jsocks : "com.gitlab.guardianproject:jsocksandroid:$versions.guardian_jsocks", - guardian_jtorctl : "info.guardianproject:jtorctl:$versions.guardian_jtorctl", - ipt_proxy : "foundation.e:OrbotIPtProxy:$versions.orbot_service", - portmapper : "com.offbynull.portmapper:portmapper:$versions.portmapper", - tor_android : "foundation.e:tor-android:$versions.orbot_service", - pcap_core : "org.pcap4j:pcap4j-core:$versions.pcap_core", - pcap_factory : "org.pcap4j:pcap4j-packetfactory-static:$versions.pcap_factory" + orbotlibs = [ + android_material : "com.google.android.material:material:$orbotversions.android_material", + android_shell : "com.jaredrummler:android-shell:$orbotversions.android_shell", + android_snowfall : "com.github.jetradarmobile:android-snowfall:$orbotversions.android_snowfall", + android_volley : "com.android.volley:volley:$orbotversions.android_volley", + androidx_appcompat : "androidx.appcompat:appcompat:$orbotversions.androidx_appcompat", + androidx_constraint : "androidx.constraintlayout:constraintlayout:$orbotversions.androidx_constraint", + androidx_coordinator : "androidx.coordinatorlayout:coordinatorlayout:$orbotversions.androidx_coordinator", + androidx_core : "androidx.core:core:$orbotversions.androidx_core", + androidx_leanback_core : "androidx.leanback:leanback:$orbotversions.androidx_leanback", + androidx_leanback_paging : "androidx.leanback:leanback-paging:$orbotversions.androidx_leanback_paging", + androidx_leanback_preferences : "androidx.leanback:leanback-preference:$orbotversions.androidx_leanback", + androidx_leanback_tab : "androidx.leanback:leanback-tab:$orbotversions.androidx_leanback_tab", + androidx_localbroadcast : "androidx.localbroadcastmanager:localbroadcastmanager:$orbotversions.androidx_localbroadcast", + androidx_multidex : "androidx.multidex:multidex:$orbotversions.androidx_multidex", + androidx_palette : "androidx.palette:palette:$orbotversions.androidx_palette", + androidx_recyclerview : "androidx.recyclerview:recyclerview:$orbotversions.androidx_recyclerview", + apl_appintro : "com.github.apl-devs:appintro:$orbotversions.apl_appintro", + bclogic_pulsator : "pl.bclogic:pulsator4droid:$orbotversions.bclogic_pulsator", + fastlane_screengrab : "tools.fastlane:screengrab:$orbotversions.fastlane_screengrab", + guardian_geoip : "foundation.e:geoip:$orbotversions.orbot_service_dependencies", + guardian_jsocks : "com.gitlab.guardianproject:jsocksandroid:$orbotversions.guardian_jsocks", + guardian_jtorctl : "info.guardianproject:jtorctl:$orbotversions.guardian_jtorctl", + ipt_proxy : "foundation.e:OrbotIPtProxy:$orbotversions.orbot_service_dependencies", + portmapper : "com.offbynull.portmapper:portmapper:$orbotversions.portmapper", + tor_android : "foundation.e:tor-android:$orbotversions.orbot_service_dependencies", + pcap_core : "org.pcap4j:pcap4j-core:$orbotversions.pcap_core", + pcap_factory : "org.pcap4j:pcap4j-packetfactory-static:$orbotversions.pcap_factory" ] } diff --git a/ipscrambling/exportdependencies/publish.gradle b/ipscrambling/exportdependencies/publish.gradle index 657ce22..3ff0f91 100644 --- a/ipscrambling/exportdependencies/publish.gradle +++ b/ipscrambling/exportdependencies/publish.gradle @@ -5,7 +5,7 @@ publishing { maven(MavenPublication) { groupId 'foundation.e' artifactId 'orbotservice' - version versions.orbot_service + version orbotversions.orbot_service artifact "$buildDir/outputs/aar/orbotservice-release.aar" pom.withXml { @@ -25,7 +25,7 @@ publishing { def ciApiV4Url = System.getenv("CI_API_V4_URL") if (ciJobToken != null) { maven { - url "${ciApiV4Url}/projects/1063/packages/maven" + url "${ciApiV4Url}/projects/900/packages/maven" credentials(HttpHeaderCredentials) { name = 'Job-Token' value = ciJobToken @@ -36,7 +36,7 @@ publishing { } } else { maven { - url "https://gitlab.e.foundation/api/v4/projects/1063/packages/maven" + url "https://gitlab.e.foundation/api/v4/projects/900/packages/maven" credentials(HttpHeaderCredentials) { name = "Private-Token" value = gitLabPrivateToken diff --git a/ipscrambling/exportdependencies/update_dependencies.md b/ipscrambling/exportdependencies/update_dependencies.md index 32ef58d..faf11f3 100644 --- a/ipscrambling/exportdependencies/update_dependencies.md +++ b/ipscrambling/exportdependencies/update_dependencies.md @@ -17,7 +17,7 @@ mvn install:install-file \ mvn deploy:deploy-file \ -DrepositoryId=gitlab-e-maven \ --Durl=https://gitlab.e.foundation/api/v4/projects/1063/packages/maven \ +-Durl=https://gitlab.e.foundation/api/v4/projects/900/packages/maven \ -DartifactId=[ARTIFACTID] \ -Dpackaging=aar \ -Dfile=[PATH] @@ -37,7 +37,7 @@ step 4 : mvn deploy:deploy-file \ -DrepositoryId=gitlab-e-maven \ --Durl=https://gitlab.e.foundation/api/v4/projects/1063/packages/maven \ +-Durl=https://gitlab.e.foundation/api/v4/projects/900/packages/maven \ -DartifactId=tor-android \ -Dpackaging=aar \ -Dfile="../../orbot/libs/tor-android-binary-release.aar diff --git a/ipscrambling/src/main/java/foundation/e/advancedprivacy/ipscrambler/IpScramblerModule.kt b/ipscrambling/src/main/java/foundation/e/advancedprivacy/ipscrambler/IpScramblerModule.kt deleted file mode 100644 index d1f01a0..0000000 --- a/ipscrambling/src/main/java/foundation/e/advancedprivacy/ipscrambler/IpScramblerModule.kt +++ /dev/null @@ -1,308 +0,0 @@ -/* - * 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 . - */ - -package foundation.e.advancedprivacy.ipscrambler - -import android.annotation.SuppressLint -import android.content.BroadcastReceiver -import android.content.Context -import android.content.Intent -import android.content.IntentFilter -import android.net.VpnService -import android.os.Bundle -import android.os.Handler -import android.os.Looper -import android.os.Message -import android.util.Log -import androidx.localbroadcastmanager.content.LocalBroadcastManager -import org.torproject.android.service.OrbotConstants -import org.torproject.android.service.OrbotConstants.ACTION_STOP_FOREGROUND_TASK -import org.torproject.android.service.OrbotService -import org.torproject.android.service.util.Prefs -import java.security.InvalidParameterException - -@SuppressLint("CommitPrefEdits") -class IpScramblerModule(private val context: Context) { - interface Listener { - fun onStatusChanged(newStatus: Status) - fun log(message: String) - fun onTrafficUpdate(upload: Long, download: Long, read: Long, write: Long) - } - - enum class Status { - OFF, ON, STARTING, STOPPING, START_DISABLED - } - companion object { - const val TAG = "IpScramblerModule" - - private val EXIT_COUNTRY_CODES = setOf("DE", "AT", "SE", "CH", "IS", "CA", "US", "ES", "FR", "BG", "PL", "AU", "BR", "CZ", "DK", "FI", "GB", "HU", "NL", "JP", "RO", "RU", "SG", "SK") - - // Key where exit country is stored by orbot service. - private const val PREFS_KEY_EXIT_NODES = "pref_exit_nodes" - // Copy of the package private OrbotService.NOTIFY_ID value. - // const val ORBOT_SERVICE_NOTIFY_ID_COPY = 1 - } - - private var currentStatus: Status? = null - private val listeners = mutableSetOf() - - private val localBroadcastReceiver: BroadcastReceiver = object : BroadcastReceiver() { - override fun onReceive(context: Context, intent: Intent) { - val action = intent.action ?: return - if (action == OrbotConstants.ACTION_RUNNING_SYNC) { - try { - intent.getStringExtra(OrbotConstants.EXTRA_STATUS)?.let { - val newStatus = Status.valueOf(it) - currentStatus = newStatus - } - } catch (e: Exception) { - Log.e(TAG, "Can't parse Orbot service status.") - } - return - } - - val msg = messageHandler.obtainMessage() - msg.obj = action - msg.data = intent.extras - messageHandler.sendMessage(msg) - } - } - - private val messageHandler: Handler = object : Handler(Looper.getMainLooper()) { - override fun handleMessage(msg: Message) { - val action = msg.obj as? String ?: return - val data = msg.data - when (action) { - OrbotConstants.LOCAL_ACTION_LOG -> - data.getString(OrbotConstants.LOCAL_EXTRA_LOG)?.let { newLog(it) } - - OrbotConstants.LOCAL_ACTION_BANDWIDTH -> { - trafficUpdate( - data.getLong("up", 0), - data.getLong("down", 0), - data.getLong("written", 0), - data.getLong("read", 0) - ) - } - - OrbotConstants.LOCAL_ACTION_PORTS -> { - httpProxyPort = data.getInt(OrbotService.EXTRA_HTTP_PROXY_PORT, -1) - socksProxyPort = data.getInt(OrbotService.EXTRA_SOCKS_PROXY_PORT, -1) - } - - OrbotConstants.LOCAL_ACTION_STATUS -> - data.getString(OrbotConstants.EXTRA_STATUS)?.let { - try { - val newStatus = Status.valueOf(it) - updateStatus(newStatus, force = true) - } catch (e: Exception) { - Log.e(TAG, "Can't parse Orbot service status.") - } - } - } - super.handleMessage(msg) - } - } - - init { - Prefs.setContext(context) - - val lbm = LocalBroadcastManager.getInstance(context) - lbm.registerReceiver( - localBroadcastReceiver, - IntentFilter(OrbotConstants.LOCAL_ACTION_STATUS) - ) - lbm.registerReceiver( - localBroadcastReceiver, - IntentFilter(OrbotConstants.LOCAL_ACTION_BANDWIDTH) - ) - lbm.registerReceiver( - localBroadcastReceiver, - IntentFilter(OrbotConstants.LOCAL_ACTION_LOG) - ) - lbm.registerReceiver( - localBroadcastReceiver, - IntentFilter(OrbotConstants.LOCAL_ACTION_PORTS) - ) - lbm.registerReceiver( - localBroadcastReceiver, - IntentFilter(OrbotConstants.ACTION_RUNNING_SYNC) - ) - - Prefs.getSharedPrefs(context).edit() - .putInt(OrbotConstants.PREFS_DNS_PORT, OrbotConstants.TOR_DNS_PORT_DEFAULT) - .apply() - } - - private fun updateStatus(status: Status, force: Boolean = false) { - if (force || status != currentStatus) { - currentStatus = status - listeners.forEach { - it.onStatusChanged(status) - } - } - } - - private fun isServiceRunning(): Boolean { - // Reset status, and then ask to refresh it synchronously. - currentStatus = Status.OFF - LocalBroadcastManager.getInstance(context) - .sendBroadcastSync(Intent(OrbotConstants.ACTION_CHECK_RUNNING_SYNC)) - return currentStatus != Status.OFF - } - - private fun newLog(message: String) { - listeners.forEach { it.log(message) } - } - - private fun trafficUpdate(upload: Long, download: Long, read: Long, write: Long) { - listeners.forEach { it.onTrafficUpdate(upload, download, read, write) } - } - - private fun sendIntentToService(action: String, extra: Bundle? = null) { - val intent = Intent(context, OrbotService::class.java) - intent.action = action - extra?.let { intent.putExtras(it) } - context.startService(intent) - } - - @SuppressLint("ApplySharedPref") - private fun saveTorifiedApps(packageNames: Collection) { - packageNames.joinToString("|") - Prefs.getSharedPrefs(context).edit().putString( - OrbotConstants.PREFS_KEY_TORIFIED, packageNames.joinToString("|") - ).commit() - - if (isServiceRunning()) { - sendIntentToService(OrbotConstants.ACTION_RESTART_VPN) - } - } - - private fun getTorifiedApps(): Set { - val list = Prefs.getSharedPrefs(context).getString(OrbotConstants.PREFS_KEY_TORIFIED, "") - ?.split("|") - return if (list == null || list == listOf("")) { - emptySet() - } else { - list.toSet() - } - } - - @SuppressLint("ApplySharedPref") - private fun setExitCountryCode(countryCode: String) { - val countryParam = when { - countryCode.isEmpty() -> "" - countryCode in EXIT_COUNTRY_CODES -> "{$countryCode}" - else -> throw InvalidParameterException( - "Only these countries are available: ${EXIT_COUNTRY_CODES.joinToString { ", " } }" - ) - } - - if (isServiceRunning()) { - val extra = Bundle() - extra.putString("exit", countryParam) - sendIntentToService(OrbotConstants.CMD_SET_EXIT, extra) - } else { - Prefs.getSharedPrefs(context) - .edit().putString(PREFS_KEY_EXIT_NODES, countryParam) - .commit() - } - } - - private fun getExitCountryCode(): String { - val raw = Prefs.getExitNodes() - return if (raw.isEmpty()) raw else raw.slice(1..2) - } - - fun prepareAndroidVpn(): Intent? { - return VpnService.prepare(context) - } - - fun start(enableNotification: Boolean) { - Prefs.enableNotification(enableNotification) - Prefs.putUseVpn(true) - Prefs.putStartOnBoot(true) - - sendIntentToService(OrbotConstants.ACTION_START) - sendIntentToService(OrbotConstants.ACTION_START_VPN) - } - - fun stop() { - updateStatus(Status.STOPPING) - - Prefs.putUseVpn(false) - Prefs.putStartOnBoot(false) - - sendIntentToService(OrbotConstants.ACTION_STOP_VPN) - sendIntentToService( - action = OrbotConstants.ACTION_STOP, - extra = Bundle().apply { putBoolean(ACTION_STOP_FOREGROUND_TASK, true) } - ) - stoppingWatchdog(5) - } - - private fun stoppingWatchdog(countDown: Int) { - Handler(Looper.getMainLooper()).postDelayed( - { - if (isServiceRunning() && countDown > 0) { - stoppingWatchdog(countDown - 1) - } else { - updateStatus(Status.OFF, force = true) - } - }, - 500 - ) - } - - fun requestStatus() { - if (isServiceRunning()) { - sendIntentToService(OrbotConstants.ACTION_STATUS) - } else { - updateStatus(Status.OFF, force = true) - } - } - - var appList: Set - get() = getTorifiedApps() - set(value) = saveTorifiedApps(value) - - var exitCountry: String - get() = getExitCountryCode() - set(value) = setExitCountryCode(value) - - fun getAvailablesLocations(): Set = EXIT_COUNTRY_CODES - - var httpProxyPort: Int = -1 - private set - - var socksProxyPort: Int = -1 - private set - - fun addListener(listener: Listener) { - listeners.add(listener) - } - fun removeListener(listener: Listener) { - listeners.remove(listener) - } - fun clearListeners() { - listeners.clear() - } - - fun onCleared() { - LocalBroadcastManager.getInstance(context).unregisterReceiver(localBroadcastReceiver) - } -} diff --git a/ipscrambling/src/main/java/foundation/e/advancedprivacy/ipscrambler/KoinModule.kt b/ipscrambling/src/main/java/foundation/e/advancedprivacy/ipscrambler/KoinModule.kt index bfb9b32..79aeb05 100644 --- a/ipscrambling/src/main/java/foundation/e/advancedprivacy/ipscrambler/KoinModule.kt +++ b/ipscrambling/src/main/java/foundation/e/advancedprivacy/ipscrambler/KoinModule.kt @@ -21,5 +21,5 @@ import org.koin.core.module.dsl.singleOf import org.koin.dsl.module val ipScramblerModule = module { - singleOf(::IpScramblerModule) + singleOf(::OrbotServiceSupervisor) } diff --git a/ipscrambling/src/main/java/foundation/e/advancedprivacy/ipscrambler/OrbotServiceSupervisor.kt b/ipscrambling/src/main/java/foundation/e/advancedprivacy/ipscrambler/OrbotServiceSupervisor.kt new file mode 100644 index 0000000..8813948 --- /dev/null +++ b/ipscrambling/src/main/java/foundation/e/advancedprivacy/ipscrambler/OrbotServiceSupervisor.kt @@ -0,0 +1,308 @@ +/* + * Copyright (C) 2023 MURENA SAS + * 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 . + */ + +package foundation.e.advancedprivacy.ipscrambler + +import android.annotation.SuppressLint +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.net.VpnService +import android.os.Bundle +import android.os.Handler +import android.os.Looper +import android.os.Message +import androidx.localbroadcastmanager.content.LocalBroadcastManager +import foundation.e.advancedprivacy.domain.entities.FeatureServiceState +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.pcap4j.packet.DnsPacket +import org.torproject.android.service.OrbotConstants +import org.torproject.android.service.OrbotConstants.ACTION_STOP_FOREGROUND_TASK +import org.torproject.android.service.OrbotService +import org.torproject.android.service.util.Prefs +import timber.log.Timber +import java.security.InvalidParameterException +import java.util.function.Function + +@SuppressLint("CommitPrefEdits") +class OrbotServiceSupervisor( + private val context: Context, + private val coroutineScope: CoroutineScope, +) { + private val _state = MutableStateFlow(FeatureServiceState.OFF) + val state: StateFlow = _state + + enum class Status { + OFF, ON, STARTING, STOPPING, START_DISABLED + } + companion object { + private val EXIT_COUNTRY_CODES = setOf("DE", "AT", "SE", "CH", "IS", "CA", "US", "ES", "FR", "BG", "PL", "AU", "BR", "CZ", "DK", "FI", "GB", "HU", "NL", "JP", "RO", "RU", "SG", "SK") + + // Key where exit country is stored by orbot service. + private const val PREFS_KEY_EXIT_NODES = "pref_exit_nodes" + // Copy of the package private OrbotService.NOTIFY_ID value. + // const val ORBOT_SERVICE_NOTIFY_ID_COPY = 1 + } + + private var currentStatus: Status? = null + + private val localBroadcastReceiver: BroadcastReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + val action = intent.action ?: return + if (action == OrbotConstants.ACTION_RUNNING_SYNC) { + try { + intent.getStringExtra(OrbotConstants.EXTRA_STATUS)?.let { + val newStatus = Status.valueOf(it) + currentStatus = newStatus + } + } catch (e: Exception) { + Timber.e("Can't parse Orbot service status.") + } + return + } + + val msg = messageHandler.obtainMessage() + msg.obj = action + msg.data = intent.extras + messageHandler.sendMessage(msg) + } + } + + private val messageHandler: Handler = object : Handler(Looper.getMainLooper()) { + override fun handleMessage(msg: Message) { + val action = msg.obj as? String ?: return + val data = msg.data + when (action) { + OrbotConstants.LOCAL_ACTION_PORTS -> { + httpProxyPort = data.getInt(OrbotService.EXTRA_HTTP_PROXY_PORT, -1) + socksProxyPort = data.getInt(OrbotService.EXTRA_SOCKS_PROXY_PORT, -1) + } + + OrbotConstants.LOCAL_ACTION_STATUS -> + data.getString(OrbotConstants.EXTRA_STATUS)?.let { + try { + val newStatus = Status.valueOf(it) + updateStatus(newStatus, force = true) + } catch (e: Exception) { + Timber.e("Can't parse Orbot service status.") + } + } + OrbotConstants.LOCAL_ACTION_LOG, + OrbotConstants.LOCAL_ACTION_BANDWIDTH -> {} // Unused in Advanced Privacy + } + super.handleMessage(msg) + } + } + + init { + Prefs.setContext(context) + + val lbm = LocalBroadcastManager.getInstance(context) + lbm.registerReceiver( + localBroadcastReceiver, + IntentFilter(OrbotConstants.LOCAL_ACTION_STATUS) + ) + lbm.registerReceiver( + localBroadcastReceiver, + IntentFilter(OrbotConstants.LOCAL_ACTION_BANDWIDTH) + ) + lbm.registerReceiver( + localBroadcastReceiver, + IntentFilter(OrbotConstants.LOCAL_ACTION_LOG) + ) + lbm.registerReceiver( + localBroadcastReceiver, + IntentFilter(OrbotConstants.LOCAL_ACTION_PORTS) + ) + lbm.registerReceiver( + localBroadcastReceiver, + IntentFilter(OrbotConstants.ACTION_RUNNING_SYNC) + ) + + Prefs.getSharedPrefs(context).edit() + .putInt(OrbotConstants.PREFS_DNS_PORT, OrbotConstants.TOR_DNS_PORT_DEFAULT) + .apply() + } + + private fun updateStatus(status: Status, force: Boolean = false) { + if (force || status != currentStatus) { + val newState = when (status) { + Status.OFF -> FeatureServiceState.OFF + Status.ON -> FeatureServiceState.ON + Status.STARTING -> FeatureServiceState.STARTING + Status.STOPPING, + Status.START_DISABLED -> FeatureServiceState.STOPPING + } + + coroutineScope.launch(Dispatchers.IO) { + _state.update { currentState -> + if (newState == FeatureServiceState.OFF && + currentState == FeatureServiceState.STOPPING + ) { + // Wait for orbot to relax before allowing user to reactivate it. + delay(1000) + } + newState + } + } + } + } + + private fun isServiceRunning(): Boolean { + // Reset status, and then ask to refresh it synchronously. + currentStatus = Status.OFF + LocalBroadcastManager.getInstance(context) + .sendBroadcastSync(Intent(OrbotConstants.ACTION_CHECK_RUNNING_SYNC)) + return currentStatus != Status.OFF + } + + private fun sendIntentToService(action: String, extra: Bundle? = null) { + val intent = Intent(context, OrbotService::class.java) + intent.action = action + extra?.let { intent.putExtras(it) } + context.startService(intent) + } + + @SuppressLint("ApplySharedPref") + private fun saveTorifiedApps(packageNames: Collection) { + packageNames.joinToString("|") + Prefs.getSharedPrefs(context).edit().putString( + OrbotConstants.PREFS_KEY_TORIFIED, packageNames.joinToString("|") + ).commit() + + if (isServiceRunning()) { + sendIntentToService(OrbotConstants.ACTION_RESTART_VPN) + } + } + + private fun getTorifiedApps(): Set { + val list = Prefs.getSharedPrefs(context).getString(OrbotConstants.PREFS_KEY_TORIFIED, "") + ?.split("|") + return if (list == null || list == listOf("")) { + emptySet() + } else { + list.toSet() + } + } + + @SuppressLint("ApplySharedPref") + suspend fun setExitCountryCode(countryCode: String) { + withContext(Dispatchers.IO) { + val countryParam = when { + countryCode.isEmpty() -> "" + countryCode in EXIT_COUNTRY_CODES -> "{$countryCode}" + else -> throw InvalidParameterException( + "Only these countries are available: ${EXIT_COUNTRY_CODES.joinToString { ", " }}" + ) + } + + if (isServiceRunning()) { + val extra = Bundle() + extra.putString("exit", countryParam) + sendIntentToService(OrbotConstants.CMD_SET_EXIT, extra) + } else { + Prefs.getSharedPrefs(context) + .edit().putString(PREFS_KEY_EXIT_NODES, countryParam) + .commit() + } + } + } + + fun getExitCountryCode(): String { + val raw = Prefs.getExitNodes() + return if (raw.isEmpty()) raw else raw.slice(1..2) + } + + fun prepareAndroidVpn(): Intent? { + return VpnService.prepare(context) + } + + fun setDNSFilter(shouldBlock: Function?) { + OrbotService.shouldBlock = shouldBlock + } + + fun start(enableNotification: Boolean) { + Prefs.enableNotification(enableNotification) + Prefs.putUseVpn(true) + Prefs.putStartOnBoot(true) + + sendIntentToService(OrbotConstants.ACTION_START) + sendIntentToService(OrbotConstants.ACTION_START_VPN) + } + + fun stop() { + if (!isServiceRunning()) return + + updateStatus(Status.STOPPING) + + Prefs.putUseVpn(false) + Prefs.putStartOnBoot(false) + + sendIntentToService(OrbotConstants.ACTION_STOP_VPN) + sendIntentToService( + action = OrbotConstants.ACTION_STOP, + extra = Bundle().apply { putBoolean(ACTION_STOP_FOREGROUND_TASK, true) } + ) + stoppingWatchdog(5) + } + + private fun stoppingWatchdog(countDown: Int) { + Handler(Looper.getMainLooper()).postDelayed( + { + if (isServiceRunning() && countDown > 0) { + stoppingWatchdog(countDown - 1) + } else { + updateStatus(Status.OFF, force = true) + } + }, + 500 + ) + } + + fun requestStatus() { + if (isServiceRunning()) { + sendIntentToService(OrbotConstants.ACTION_STATUS) + } else { + updateStatus(Status.OFF, force = true) + } + } + + var appList: Set + get() = getTorifiedApps() + set(value) = saveTorifiedApps(value) + + fun getAvailablesLocations(): Set = EXIT_COUNTRY_CODES + + var httpProxyPort: Int = -1 + private set + + var socksProxyPort: Int = -1 + private set + + fun onCleared() { + LocalBroadcastManager.getInstance(context).unregisterReceiver(localBroadcastReceiver) + } +} diff --git a/settings.gradle b/settings.gradle index 39e58c8..a25c6a8 100644 --- a/settings.gradle +++ b/settings.gradle @@ -18,12 +18,14 @@ include ':trackers' include ':permissionse' include ':permissionse:libs:hidden-apis-stub' include ':ipscrambling' +include ':ipscrambling:orbotservice' include ':trackersservicestandalone' include ':trackersservicee' dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { + mavenLocal() google() mavenCentral() maven { url "https://jitpack.io" } diff --git a/trackers/build.gradle b/trackers/build.gradle index 737db5a..69faabd 100644 --- a/trackers/build.gradle +++ b/trackers/build.gradle @@ -49,6 +49,7 @@ dependencies { libs.bundles.koin, libs.bundles.kotlin.android.coroutines, libs.google.gson, + libs.pcap4j, libs.retrofit, libs.retrofit.scalars, diff --git a/trackers/src/main/java/foundation/e/advancedprivacy/trackers/domain/externalinterfaces/TrackersServiceSupervisor.kt b/trackers/src/main/java/foundation/e/advancedprivacy/trackers/domain/externalinterfaces/TrackersServiceSupervisor.kt index d9674fc..79f721b 100644 --- a/trackers/src/main/java/foundation/e/advancedprivacy/trackers/domain/externalinterfaces/TrackersServiceSupervisor.kt +++ b/trackers/src/main/java/foundation/e/advancedprivacy/trackers/domain/externalinterfaces/TrackersServiceSupervisor.kt @@ -16,8 +16,13 @@ */ package foundation.e.advancedprivacy.trackers.domain.externalinterfaces +import org.pcap4j.packet.DnsPacket +import java.util.function.Function + interface TrackersServiceSupervisor { fun start(): Boolean fun stop(): Boolean fun isRunning(): Boolean + + val dnsFilterForIpScrambling: Function? } diff --git a/trackersservicee/build.gradle b/trackersservicee/build.gradle index f7725bf..e93d5d6 100644 --- a/trackersservicee/build.gradle +++ b/trackersservicee/build.gradle @@ -5,14 +5,11 @@ plugins { android { namespace 'foundation.e.advancedprivacy.trackers.service' - compileSdk 33 + compileSdkVersion buildConfig.compileSdk defaultConfig { - minSdk 24 - targetSdk 33 - - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - consumerProguardFiles "consumer-rules.pro" + minSdkVersion buildConfig.minSdk + targetSdkVersion buildConfig.targetSdk } buildTypes { diff --git a/trackersservicee/src/main/java/foundation/e/advancedprivacy/trackers/service/TrackersServiceSupervisorImpl.kt b/trackersservicee/src/main/java/foundation/e/advancedprivacy/trackers/service/TrackersServiceSupervisorImpl.kt index 3903db4..dcdf0d4 100644 --- a/trackersservicee/src/main/java/foundation/e/advancedprivacy/trackers/service/TrackersServiceSupervisorImpl.kt +++ b/trackersservicee/src/main/java/foundation/e/advancedprivacy/trackers/service/TrackersServiceSupervisorImpl.kt @@ -21,7 +21,9 @@ import android.content.Intent import foundation.e.advancedprivacy.trackers.domain.externalinterfaces.TrackersServiceSupervisor import foundation.e.advancedprivacy.trackers.service.TrackersService.Companion.ACTION_START import kotlinx.coroutines.isActive +import org.koin.core.module.dsl.bind import org.koin.core.module.dsl.factoryOf +import org.koin.core.module.dsl.singleOf import org.koin.dsl.module class TrackersServiceSupervisorImpl(private val context: Context) : TrackersServiceSupervisor { @@ -39,8 +41,13 @@ class TrackersServiceSupervisorImpl(private val context: Context) : TrackersServ override fun isRunning(): Boolean { return TrackersService.coroutineScope.isActive } + + override val dnsFilterForIpScrambling = null } val trackerServiceModule = module { factoryOf(::DNSBlocker) + singleOf(::TrackersServiceSupervisorImpl) { + bind() + } } diff --git a/trackersservicestandalone/build.gradle b/trackersservicestandalone/build.gradle index d5df422..ead9dbd 100644 --- a/trackersservicestandalone/build.gradle +++ b/trackersservicestandalone/build.gradle @@ -4,14 +4,11 @@ plugins { } android { - compileSdk 32 + compileSdkVersion buildConfig.compileSdk defaultConfig { - minSdk 26 - targetSdk 32 - - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - consumerProguardFiles "consumer-rules.pro" + minSdkVersion buildConfig.minSdk + targetSdkVersion buildConfig.targetSdk } buildTypes { diff --git a/trackersservicestandalone/src/main/java/foundation/e/advancedprivacy/trackers/service/TrackersServiceSupervisorImpl.kt b/trackersservicestandalone/src/main/java/foundation/e/advancedprivacy/trackers/service/TrackersServiceSupervisorImpl.kt index 25d3e2d..e2a6692 100644 --- a/trackersservicestandalone/src/main/java/foundation/e/advancedprivacy/trackers/service/TrackersServiceSupervisorImpl.kt +++ b/trackersservicestandalone/src/main/java/foundation/e/advancedprivacy/trackers/service/TrackersServiceSupervisorImpl.kt @@ -25,10 +25,16 @@ import foundation.e.advancedprivacy.trackers.service.data.RequestDNSRepository import foundation.e.advancedprivacy.trackers.service.usecases.ResolveDNSUseCase import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.MutableStateFlow +import org.koin.core.module.dsl.bind import org.koin.core.module.dsl.singleOf import org.koin.dsl.module +import org.pcap4j.packet.DnsPacket +import java.util.function.Function -class TrackersServiceSupervisorImpl(private val context: Context) : TrackersServiceSupervisor { +class TrackersServiceSupervisorImpl( + private val context: Context, + private val resolveDNSUseCase: ResolveDNSUseCase +) : TrackersServiceSupervisor { internal val state: MutableStateFlow = MutableStateFlow(FeatureServiceState.OFF) override fun start(): Boolean { @@ -54,6 +60,8 @@ class TrackersServiceSupervisorImpl(private val context: Context) : TrackersServ override fun isRunning(): Boolean { return state.value != FeatureServiceState.OFF } + + override val dnsFilterForIpScrambling = Function { dnsRequest -> resolveDNSUseCase.shouldBlock(dnsRequest) } } val trackerServiceModule = module { @@ -61,4 +69,5 @@ val trackerServiceModule = module { singleOf(::RequestDNSRepository) singleOf(::ResolveDNSUseCase) singleOf(::TunLooper) + singleOf(::TrackersServiceSupervisorImpl) { bind() } } diff --git a/trackersservicestandalone/src/main/java/foundation/e/advancedprivacy/trackers/service/usecases/ResolveDNSUseCase.kt b/trackersservicestandalone/src/main/java/foundation/e/advancedprivacy/trackers/service/usecases/ResolveDNSUseCase.kt index ac8aee0..b74b9cb 100644 --- a/trackersservicestandalone/src/main/java/foundation/e/advancedprivacy/trackers/service/usecases/ResolveDNSUseCase.kt +++ b/trackersservicestandalone/src/main/java/foundation/e/advancedprivacy/trackers/service/usecases/ResolveDNSUseCase.kt @@ -16,6 +16,7 @@ */ package foundation.e.advancedprivacy.trackers.service.usecases +import foundation.e.advancedprivacy.core.utils.runSuspendCatching import foundation.e.advancedprivacy.trackers.domain.usecases.FilterHostnameUseCase import foundation.e.advancedprivacy.trackers.service.data.NetworkDNSAddressRepository import foundation.e.advancedprivacy.trackers.service.data.RequestDNSRepository @@ -24,6 +25,7 @@ import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.GlobalScope import org.pcap4j.packet.DnsPacket import org.pcap4j.packet.namednumber.DnsRCode +import timber.log.Timber import java.net.DatagramPacket @OptIn(DelicateCoroutinesApi::class) @@ -39,16 +41,35 @@ class ResolveDNSUseCase( filterHostnameUseCase.writeLogJob(scope) } + fun shouldBlock(dnsRequest: DnsPacket?): DnsPacket? { + if (dnsRequest == null) return null + + return runSuspendCatching { + val host = dnsRequest.header.questions[0].qName.name + if (filterHostnameUseCase.shouldBlock(host)) { + dnsRequest.builder + .rCode(DnsRCode.NX_DOMAIN) + .response(true).build() + } else null + }.onFailure { + Timber.e(it, "Can't find host") + }.getOrNull() + } + suspend fun processDNS(dnsRequest: DnsPacket): DnsPacket? { - val host = dnsRequest.header.questions[0].qName.name - if (filterHostnameUseCase.shouldBlock(host)) { - return dnsRequest.builder - .rCode(DnsRCode.NX_DOMAIN) - .response(true).build() + val blockedDnsResponse = shouldBlock(dnsRequest) + + if (blockedDnsResponse != null) { + return blockedDnsResponse } val payload = dnsRequest.rawData - val packet = DatagramPacket(payload, payload.size, networkDNSAddressRepository.dnsAddress, DNS_PORT) + val packet = DatagramPacket( + payload, + payload.size, + networkDNSAddressRepository.dnsAddress, + DNS_PORT + ) return requestDNSRepository.processDNS(packet) } } -- cgit v1.2.1