From 95e68cbbe748f81af1113753c5b99929e3db9ea2 Mon Sep 17 00:00:00 2001 From: Guillaume Jacquart Date: Wed, 11 Oct 2023 16:36:02 +0000 Subject: epic18: Trackers control on standalone app (without Ipscrambling). --- .gitignore | 16 +- .idea/.gitignore | 3 - .idea/.name | 1 - .idea/codeStyles/Project.xml | 167 ------------- .idea/codeStyles/codeStyleConfig.xml | 5 - .idea/copyright/MURENA.xml | 6 - .idea/copyright/profiles_settings.xml | 3 - .idea/encodings.xml | 6 - .idea/inspectionProfiles/ktlint.xml | 7 - .idea/inspectionProfiles/profiles_settings.xml | 6 - .idea/runConfigurations/Build_system_app.xml | 13 -- .idea/vcs.xml | 6 - app/build.gradle | 3 + .../advancedprivacy/AdvancedPrivacyApplication.kt | 9 +- .../e/advancedprivacy/DependencyContainer.kt | 0 .../foundation/e/advancedprivacy/KoinModule.kt | 27 ++- .../foundation/e/advancedprivacy/Notifications.kt | 48 ++-- .../e/advancedprivacy/common/BuildFlavor.kt | 22 ++ .../e/advancedprivacy/common/WarningDialog.kt | 2 +- .../domain/usecases/FakeLocationStateUseCase.kt | 2 +- .../domain/usecases/IpScramblingStateUseCase.kt | 3 +- .../domain/usecases/TrackersStateUseCase.kt | 6 +- .../domain/usecases/TrackersStatisticsUseCase.kt | 4 +- app/src/main/res/values/strings.xml | 5 + build.gradle | 1 + .../core/utils/NotificationsHelper.kt | 35 +++ .../domain/entities/FeatureServiceState.kt | 21 ++ .../domain/entities/NotificationChannels.kt | 27 +++ .../domain/entities/NotificationContent.kt | 27 +++ .../permissions/APermissionsPrivacyModule.kt | 160 ------------- .../permissions/PermissionsPrivacyModuleBase.kt | 160 +++++++++++++ .../fakelocationdemo/MainActivity.kt | 4 +- gradle/libs.versions.toml | 6 +- .../permissions/PermissionsPrivacyModule.kt | 258 --------------------- .../permissions/PermissionsPrivacyModuleImpl.kt | 258 +++++++++++++++++++++ .../externalinterfaces/PermissionsPrivacyModule.kt | 70 ------ .../PermissionsPrivacyModuleImpl.kt | 70 ++++++ settings.gradle | 3 +- trackers/src/main/AndroidManifest.xml | 17 -- .../e/advancedprivacy/trackers/KoinModule.kt | 18 +- .../trackers/data/RemoteTrackersListRepository.kt | 2 +- .../advancedprivacy/trackers/data/StatsDatabase.kt | 12 +- .../trackers/data/TrackersRepository.kt | 2 +- .../TrackersServiceSupervisor.kt | 23 ++ .../trackers/domain/usecases/DNSBlocker.kt | 143 ------------ .../domain/usecases/FilterHostnameUseCase.kt | 108 +++++++++ .../trackers/domain/usecases/StatisticsUseCase.kt | 12 - .../trackers/domain/usecases/TrackersLogger.kt | 60 ----- .../domain/usecases/UpdateTrackerListUseCase.kt | 2 +- .../trackers/services/DNSBlockerService.kt | 68 ------ trackersservicee/.gitignore | 1 + trackersservicee/build.gradle | 43 ++++ trackersservicee/consumer-rules.pro | 0 trackersservicee/proguard-rules.pro | 21 ++ trackersservicee/src/main/AndroidManifest.xml | 35 +++ .../advancedprivacy/trackers/service/DNSBlocker.kt | 104 +++++++++ .../trackers/service/TrackersService.kt | 58 +++++ .../service/TrackersServiceSupervisorImpl.kt | 46 ++++ trackersservicestandalone/.gitignore | 1 + trackersservicestandalone/build.gradle | 43 ++++ trackersservicestandalone/consumer-rules.pro | 0 trackersservicestandalone/proguard-rules.pro | 21 ++ .../src/main/AndroidManifest.xml | 39 ++++ .../e/advancedprivacy/trackers/service/Config.kt | 44 ++++ .../trackers/service/TrackersService.kt | 127 ++++++++++ .../service/TrackersServiceSupervisorImpl.kt | 64 +++++ .../advancedprivacy/trackers/service/TunLooper.kt | 167 +++++++++++++ .../service/data/NetworkDNSAddressRepository.kt | 59 +++++ .../trackers/service/data/RequestDNSRepository.kt | 48 ++++ .../trackers/service/usecases/ResolveDNSUseCase.kt | 54 +++++ 70 files changed, 1821 insertions(+), 1091 deletions(-) delete mode 100644 .idea/.gitignore delete mode 100644 .idea/.name delete mode 100644 .idea/codeStyles/Project.xml delete mode 100644 .idea/codeStyles/codeStyleConfig.xml delete mode 100644 .idea/copyright/MURENA.xml delete mode 100644 .idea/copyright/profiles_settings.xml delete mode 100644 .idea/encodings.xml delete mode 100644 .idea/inspectionProfiles/ktlint.xml delete mode 100644 .idea/inspectionProfiles/profiles_settings.xml delete mode 100644 .idea/runConfigurations/Build_system_app.xml delete mode 100644 .idea/vcs.xml create mode 100644 app/src/main/java/foundation/e/advancedprivacy/DependencyContainer.kt create mode 100644 app/src/main/java/foundation/e/advancedprivacy/common/BuildFlavor.kt create mode 100644 core/src/main/java/foundation/e/advancedprivacy/core/utils/NotificationsHelper.kt create mode 100644 core/src/main/java/foundation/e/advancedprivacy/domain/entities/FeatureServiceState.kt create mode 100644 core/src/main/java/foundation/e/advancedprivacy/domain/entities/NotificationChannels.kt create mode 100644 core/src/main/java/foundation/e/advancedprivacy/domain/entities/NotificationContent.kt delete mode 100644 core/src/main/java/foundation/e/advancedprivacy/externalinterfaces/permissions/APermissionsPrivacyModule.kt create mode 100644 core/src/main/java/foundation/e/advancedprivacy/externalinterfaces/permissions/PermissionsPrivacyModuleBase.kt delete mode 100644 permissionse/src/main/java/foundation/e/advancedprivacy/externalinterfaces/permissions/PermissionsPrivacyModule.kt create mode 100644 permissionse/src/main/java/foundation/e/advancedprivacy/externalinterfaces/permissions/PermissionsPrivacyModuleImpl.kt delete mode 100644 permissionsstandalone/src/main/java/foundation/e/advancedprivacy/permissions/externalinterfaces/PermissionsPrivacyModule.kt create mode 100644 permissionsstandalone/src/main/java/foundation/e/advancedprivacy/permissions/externalinterfaces/PermissionsPrivacyModuleImpl.kt create mode 100644 trackers/src/main/java/foundation/e/advancedprivacy/trackers/domain/externalinterfaces/TrackersServiceSupervisor.kt delete mode 100644 trackers/src/main/java/foundation/e/advancedprivacy/trackers/domain/usecases/DNSBlocker.kt create mode 100644 trackers/src/main/java/foundation/e/advancedprivacy/trackers/domain/usecases/FilterHostnameUseCase.kt delete mode 100644 trackers/src/main/java/foundation/e/advancedprivacy/trackers/domain/usecases/TrackersLogger.kt delete mode 100644 trackers/src/main/java/foundation/e/advancedprivacy/trackers/services/DNSBlockerService.kt create mode 100644 trackersservicee/.gitignore create mode 100644 trackersservicee/build.gradle create mode 100644 trackersservicee/consumer-rules.pro create mode 100644 trackersservicee/proguard-rules.pro create mode 100644 trackersservicee/src/main/AndroidManifest.xml create mode 100644 trackersservicee/src/main/java/foundation/e/advancedprivacy/trackers/service/DNSBlocker.kt create mode 100644 trackersservicee/src/main/java/foundation/e/advancedprivacy/trackers/service/TrackersService.kt create mode 100644 trackersservicee/src/main/java/foundation/e/advancedprivacy/trackers/service/TrackersServiceSupervisorImpl.kt create mode 100644 trackersservicestandalone/.gitignore create mode 100644 trackersservicestandalone/build.gradle create mode 100644 trackersservicestandalone/consumer-rules.pro create mode 100644 trackersservicestandalone/proguard-rules.pro create mode 100644 trackersservicestandalone/src/main/AndroidManifest.xml create mode 100644 trackersservicestandalone/src/main/java/foundation/e/advancedprivacy/trackers/service/Config.kt create mode 100644 trackersservicestandalone/src/main/java/foundation/e/advancedprivacy/trackers/service/TrackersService.kt create mode 100644 trackersservicestandalone/src/main/java/foundation/e/advancedprivacy/trackers/service/TrackersServiceSupervisorImpl.kt create mode 100644 trackersservicestandalone/src/main/java/foundation/e/advancedprivacy/trackers/service/TunLooper.kt create mode 100644 trackersservicestandalone/src/main/java/foundation/e/advancedprivacy/trackers/service/data/NetworkDNSAddressRepository.kt create mode 100644 trackersservicestandalone/src/main/java/foundation/e/advancedprivacy/trackers/service/data/RequestDNSRepository.kt create mode 100644 trackersservicestandalone/src/main/java/foundation/e/advancedprivacy/trackers/service/usecases/ResolveDNSUseCase.kt diff --git a/.gitignore b/.gitignore index 09d358c..7d06206 100644 --- a/.gitignore +++ b/.gitignore @@ -6,21 +6,7 @@ build/ local.properties # IntelliJ .idea folder -/.idea/caches -/.idea/libraries -/.idea/misc.xml -/.idea/modules.xml -/.idea/workspace.xml -/.idea/navEditor.xml -.idea/tasks.xml -.idea/compiler.xml -/.idea/assetWizardSettings.xml -/.idea/jarRepositories.xml -/.idea/google-java-format.xml -/.idea/runConfigurations.xml -/.idea/dbnavigator.xml -/.idea/deploymentTargetDropDown.xml -/.idea/kotlinc.xml +.idea/ gradle.xml markdown-*.xml diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index 26d3352..0000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml diff --git a/.idea/.name b/.idea/.name deleted file mode 100644 index 5f54997..0000000 --- a/.idea/.name +++ /dev/null @@ -1 +0,0 @@ -AdvancedPrivacy diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml deleted file mode 100644 index 97c1063..0000000 --- a/.idea/codeStyles/Project.xml +++ /dev/null @@ -1,167 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml deleted file mode 100644 index 0f7bc51..0000000 --- a/.idea/codeStyles/codeStyleConfig.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - diff --git a/.idea/copyright/MURENA.xml b/.idea/copyright/MURENA.xml deleted file mode 100644 index 0c0ee95..0000000 --- a/.idea/copyright/MURENA.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/copyright/profiles_settings.xml b/.idea/copyright/profiles_settings.xml deleted file mode 100644 index e9f8efd..0000000 --- a/.idea/copyright/profiles_settings.xml +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/.idea/encodings.xml b/.idea/encodings.xml deleted file mode 100644 index 97626ba..0000000 --- a/.idea/encodings.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/inspectionProfiles/ktlint.xml b/.idea/inspectionProfiles/ktlint.xml deleted file mode 100644 index 7d04a74..0000000 --- a/.idea/inspectionProfiles/ktlint.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml deleted file mode 100644 index 64580d1..0000000 --- a/.idea/inspectionProfiles/profiles_settings.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - diff --git a/.idea/runConfigurations/Build_system_app.xml b/.idea/runConfigurations/Build_system_app.xml deleted file mode 100644 index 755d57a..0000000 --- a/.idea/runConfigurations/Build_system_app.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 94a25f7..0000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index ef54d45..216b81a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -97,6 +97,7 @@ android { persistent: "false", mainActivityIntentFilterCategory: "android.intent.category.LAUNCHER" ] + signingConfig signingConfigs.debug } } @@ -152,6 +153,8 @@ dependencies { implementation project(':trackers') implementation project(':ipscrambling') + eImplementation project(':trackersservicee') + standaloneImplementation project(':trackersservicestandalone') implementation ( libs.e.elib, diff --git a/app/src/main/java/foundation/e/advancedprivacy/AdvancedPrivacyApplication.kt b/app/src/main/java/foundation/e/advancedprivacy/AdvancedPrivacyApplication.kt index 0af2a0e..71fef00 100644 --- a/app/src/main/java/foundation/e/advancedprivacy/AdvancedPrivacyApplication.kt +++ b/app/src/main/java/foundation/e/advancedprivacy/AdvancedPrivacyApplication.kt @@ -18,7 +18,6 @@ package foundation.e.advancedprivacy import android.app.Application -import android.content.Intent import foundation.e.advancedprivacy.common.WarningDialog import foundation.e.advancedprivacy.domain.usecases.FakeLocationStateUseCase import foundation.e.advancedprivacy.domain.usecases.GetQuickPrivacyStateUseCase @@ -27,7 +26,6 @@ import foundation.e.advancedprivacy.domain.usecases.ShowFeaturesWarningUseCase import foundation.e.advancedprivacy.domain.usecases.TrackersStateUseCase import foundation.e.advancedprivacy.domain.usecases.TrackersStatisticsUseCase import foundation.e.advancedprivacy.externalinterfaces.permissions.IPermissionsPrivacyModule -import foundation.e.advancedprivacy.trackers.services.DNSBlockerService import foundation.e.advancedprivacy.trackers.services.UpdateTrackersWorker import foundation.e.lib.telemetry.Telemetry import kotlinx.coroutines.CoroutineScope @@ -70,12 +68,7 @@ class AdvancedPrivacyApplication : Application() { ) get(IpScramblingStateUseCase::class.java) - get(FakeLocationStateUseCase::class.java) get(TrackersStateUseCase::class.java) - - val intent = Intent(this, DNSBlockerService::class.java) - intent.action = DNSBlockerService.ACTION_START - intent.putExtra(DNSBlockerService.EXTRA_ENABLE_NOTIFICATION, false) - startService(intent) + get(FakeLocationStateUseCase::class.java) } } diff --git a/app/src/main/java/foundation/e/advancedprivacy/DependencyContainer.kt b/app/src/main/java/foundation/e/advancedprivacy/DependencyContainer.kt new file mode 100644 index 0000000..e69de29 diff --git a/app/src/main/java/foundation/e/advancedprivacy/KoinModule.kt b/app/src/main/java/foundation/e/advancedprivacy/KoinModule.kt index 3fbb636..20cefd5 100644 --- a/app/src/main/java/foundation/e/advancedprivacy/KoinModule.kt +++ b/app/src/main/java/foundation/e/advancedprivacy/KoinModule.kt @@ -22,6 +22,8 @@ import android.os.Process import foundation.e.advancedprivacy.core.coreModule import foundation.e.advancedprivacy.data.repositories.LocalStateRepository import foundation.e.advancedprivacy.domain.entities.ApplicationDescription +import foundation.e.advancedprivacy.domain.entities.CHANNEL_TRACKER_FLAG +import foundation.e.advancedprivacy.domain.entities.NotificationContent import foundation.e.advancedprivacy.domain.entities.ProfileType import foundation.e.advancedprivacy.domain.usecases.AppListUseCase import foundation.e.advancedprivacy.domain.usecases.FakeLocationStateUseCase @@ -39,7 +41,10 @@ import foundation.e.advancedprivacy.features.location.FakeLocationViewModel 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.PermissionsPrivacyModule +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 import org.koin.androidx.viewmodel.dsl.viewModel @@ -49,7 +54,7 @@ import org.koin.core.qualifier.named import org.koin.dsl.module val appModule = module { - includes(coreModule, trackersModule, fakelocationModule, ipScramblerModule) + includes(coreModule, trackersModule, fakelocationModule, ipScramblerModule, trackerServiceModule) factory { androidContext().resources } single { @@ -89,6 +94,16 @@ val appModule = module { ) } + single(named("notificationTrackerFlag")) { + NotificationContent( + channelId = CHANNEL_TRACKER_FLAG, + icon = R.drawable.ic_e_app_logo, + title = R.string.notifications_tracker_title, + description = R.string.notifications_tracker_content, + pendingIntent = null + ) + } + single { CityDataSource } singleOf(::AppListUseCase) @@ -120,7 +135,13 @@ val appModule = module { singleOf(::TrackersStatisticsUseCase) single { - PermissionsPrivacyModule(context = androidContext()) + PermissionsPrivacyModuleImpl(context = androidContext()) + } + + single { + TrackersServiceSupervisorImpl( + context = androidContext(), + ) } viewModel { parameters -> diff --git a/app/src/main/java/foundation/e/advancedprivacy/Notifications.kt b/app/src/main/java/foundation/e/advancedprivacy/Notifications.kt index cd85e9a..455b1a7 100644 --- a/app/src/main/java/foundation/e/advancedprivacy/Notifications.kt +++ b/app/src/main/java/foundation/e/advancedprivacy/Notifications.kt @@ -20,13 +20,21 @@ package foundation.e.advancedprivacy import android.app.NotificationChannel import android.app.NotificationManager -import android.app.PendingIntent import android.content.Context import androidx.annotation.StringRes import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat +import foundation.e.advancedprivacy.core.utils.notificationBuilder +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.MainFeatures +import foundation.e.advancedprivacy.domain.entities.NOTIFICATION_FAKE_LOCATION_FLAG +import foundation.e.advancedprivacy.domain.entities.NOTIFICATION_FIRST_BOOT +import foundation.e.advancedprivacy.domain.entities.NOTIFICATION_IPSCRAMBLING_FLAG +import foundation.e.advancedprivacy.domain.entities.NotificationContent import foundation.e.advancedprivacy.domain.usecases.GetQuickPrivacyStateUseCase import foundation.e.advancedprivacy.externalinterfaces.permissions.IPermissionsPrivacyModule import foundation.e.advancedprivacy.main.MainActivity @@ -37,14 +45,6 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach object Notifications { - const val CHANNEL_FIRST_BOOT = "first_boot_notification" - const val CHANNEL_FAKE_LOCATION_FLAG = "fake_location_flag" - const val CHANNEL_IPSCRAMBLING_FLAG = "ipscrambling_flag" - - const val NOTIFICATION_FIRST_BOOT = 1000 - const val NOTIFICATION_FAKE_LOCATION_FLAG = NOTIFICATION_FIRST_BOOT + 1 - const val NOTIFICATION_IPSCRAMBLING_FLAG = NOTIFICATION_FAKE_LOCATION_FLAG + 1 - fun showFirstBootNotification(context: Context) { createNotificationFirstBootChannel(context) val notificationBuilder: NotificationCompat.Builder = notificationBuilder( @@ -88,6 +88,14 @@ object Notifications { channelDescription = R.string.notifications_ipscrambling_channel_description ) + createNotificationFlagChannel( + context = appContext, + permissionsPrivacyModule = permissionsPrivacyModule, + channelId = CHANNEL_TRACKER_FLAG, + channelName = R.string.notifications_tracker_channel_name, + channelDescription = R.string.notifications_ipscrambling_channel_description + ) + getQuickPrivacyStateUseCase.isLocationHidden.onEach { if (it) { showFlagNotification(appContext, MainFeatures.FAKE_LOCATION) @@ -183,26 +191,4 @@ object Notifications { } NotificationManagerCompat.from(context).cancel(id) } - - private data class NotificationContent( - val channelId: String, - val icon: Int, - val title: Int, - val description: Int, - val pendingIntent: PendingIntent? - ) - - private fun notificationBuilder( - context: Context, - content: NotificationContent - ): NotificationCompat.Builder { - val builder = NotificationCompat.Builder(context, content.channelId) - .setSmallIcon(content.icon) - .setPriority(NotificationCompat.PRIORITY_LOW) - .setContentTitle(context.getString(content.title)) - .setStyle(NotificationCompat.BigTextStyle().bigText(context.getString(content.description))) - content.pendingIntent?.let { builder.setContentIntent(it) } - - return builder - } } diff --git a/app/src/main/java/foundation/e/advancedprivacy/common/BuildFlavor.kt b/app/src/main/java/foundation/e/advancedprivacy/common/BuildFlavor.kt new file mode 100644 index 0000000..ecb24ce --- /dev/null +++ b/app/src/main/java/foundation/e/advancedprivacy/common/BuildFlavor.kt @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2023 MURENA SAS + * + * 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.common + +import foundation.e.advancedprivacy.BuildConfig + +const val isStandaloneBuild: Boolean = BuildConfig.FLAVOR == "standalone" diff --git a/app/src/main/java/foundation/e/advancedprivacy/common/WarningDialog.kt b/app/src/main/java/foundation/e/advancedprivacy/common/WarningDialog.kt index 80fc760..589aa74 100644 --- a/app/src/main/java/foundation/e/advancedprivacy/common/WarningDialog.kt +++ b/app/src/main/java/foundation/e/advancedprivacy/common/WarningDialog.kt @@ -75,7 +75,7 @@ class WarningDialog : AppCompatActivity() { val feature = try { intent.getParcelableExtra(PARAM_FEATURE)!! } catch (e: Exception) { - Timber.e("Missing mandatory activity parameter", e) + Timber.e(e, "Missing mandatory activity parameter") finish() return } diff --git a/app/src/main/java/foundation/e/advancedprivacy/domain/usecases/FakeLocationStateUseCase.kt b/app/src/main/java/foundation/e/advancedprivacy/domain/usecases/FakeLocationStateUseCase.kt index 30c8e6b..983ba71 100644 --- a/app/src/main/java/foundation/e/advancedprivacy/domain/usecases/FakeLocationStateUseCase.kt +++ b/app/src/main/java/foundation/e/advancedprivacy/domain/usecases/FakeLocationStateUseCase.kt @@ -202,7 +202,7 @@ class FakeLocationStateUseCase( lastKnownLocation?.let { localListener.onLocationChanged(it) } } catch (se: SecurityException) { - Timber.e("Missing permission", se) + Timber.e(se, "Missing permission") } } } 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 27e7fe4..9c89329 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 @@ -19,6 +19,7 @@ package foundation.e.advancedprivacy.domain.usecases import android.content.Intent +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 @@ -190,7 +191,7 @@ class IpScramblingStateUseCase( fun startIpScrambling() { localStateRepository.internetPrivacyMode.value = HIDE_IP_LOADING - ipScramblerModule.start(enableNotification = false) // change the false ? + ipScramblerModule.start(enableNotification = isStandaloneBuild) } private fun map(status: IpScramblerModule.Status): InternetPrivacyMode { diff --git a/app/src/main/java/foundation/e/advancedprivacy/domain/usecases/TrackersStateUseCase.kt b/app/src/main/java/foundation/e/advancedprivacy/domain/usecases/TrackersStateUseCase.kt index ed15a41..9b79dcc 100644 --- a/app/src/main/java/foundation/e/advancedprivacy/domain/usecases/TrackersStateUseCase.kt +++ b/app/src/main/java/foundation/e/advancedprivacy/domain/usecases/TrackersStateUseCase.kt @@ -22,6 +22,7 @@ import foundation.e.advancedprivacy.data.repositories.LocalStateRepository import foundation.e.advancedprivacy.domain.entities.ApplicationDescription import foundation.e.advancedprivacy.trackers.data.WhitelistRepository import foundation.e.advancedprivacy.trackers.domain.entities.Tracker +import foundation.e.advancedprivacy.trackers.domain.externalinterfaces.TrackersServiceSupervisor import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch @@ -29,7 +30,8 @@ class TrackersStateUseCase( private val whitelistRepository: WhitelistRepository, private val localStateRepository: LocalStateRepository, private val appListsRepository: AppListsRepository, - coroutineScope: CoroutineScope + private val trackersServiceSupervisor: TrackersServiceSupervisor, + coroutineScope: CoroutineScope, ) { init { coroutineScope.launch { @@ -38,6 +40,8 @@ class TrackersStateUseCase( updateAllTrackersBlockedState() } } + + trackersServiceSupervisor.start() } private fun updateAllTrackersBlockedState() { diff --git a/app/src/main/java/foundation/e/advancedprivacy/domain/usecases/TrackersStatisticsUseCase.kt b/app/src/main/java/foundation/e/advancedprivacy/domain/usecases/TrackersStatisticsUseCase.kt index b0c9f39..3d6ade0 100644 --- a/app/src/main/java/foundation/e/advancedprivacy/domain/usecases/TrackersStatisticsUseCase.kt +++ b/app/src/main/java/foundation/e/advancedprivacy/domain/usecases/TrackersStatisticsUseCase.kt @@ -24,6 +24,7 @@ import foundation.e.advancedprivacy.data.repositories.AppListsRepository import foundation.e.advancedprivacy.domain.entities.AppWithCounts import foundation.e.advancedprivacy.domain.entities.ApplicationDescription import foundation.e.advancedprivacy.domain.entities.TrackersPeriodicStatistics +import foundation.e.advancedprivacy.trackers.data.StatsDatabase import foundation.e.advancedprivacy.trackers.data.TrackersRepository import foundation.e.advancedprivacy.trackers.data.WhitelistRepository import foundation.e.advancedprivacy.trackers.domain.entities.Tracker @@ -44,6 +45,7 @@ class TrackersStatisticsUseCase( private val whitelistRepository: WhitelistRepository, private val trackersRepository: TrackersRepository, private val appListsRepository: AppListsRepository, + private val statsDatabase: StatsDatabase, private val resources: Resources ) { fun initAppList() { @@ -52,7 +54,7 @@ class TrackersStatisticsUseCase( @OptIn(FlowPreview::class) fun listenUpdates(debounce: Duration = 1.seconds) = - statisticsUseCase.newDataAvailable + statsDatabase.newDataAvailable .throttleFirst(windowDuration = debounce) .onStart { emit(Unit) } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index fac1f75..ba3ba03 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -144,4 +144,9 @@ Real IP hidden This could impact the functioning of some applications. + Tracker control flag + Highlight that the trackers are actually logged and blocked by Advanced Privacy + Tracker control is on + This could impact the functioning of some applications. + diff --git a/build.gradle b/build.gradle index a57ee21..cd192ee 100644 --- a/build.gradle +++ b/build.gradle @@ -52,6 +52,7 @@ plugins { alias libs.plugins.android.application apply false alias libs.plugins.kotlin.kapt apply false alias libs.plugins.androidx.navigation.safeargs apply false + alias libs.plugins.android.library apply false } allprojects { diff --git a/core/src/main/java/foundation/e/advancedprivacy/core/utils/NotificationsHelper.kt b/core/src/main/java/foundation/e/advancedprivacy/core/utils/NotificationsHelper.kt new file mode 100644 index 0000000..29721b0 --- /dev/null +++ b/core/src/main/java/foundation/e/advancedprivacy/core/utils/NotificationsHelper.kt @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2023 MURENA SAS + * + * 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.core.utils + +import android.content.Context +import androidx.core.app.NotificationCompat +import foundation.e.advancedprivacy.domain.entities.NotificationContent + +fun notificationBuilder( + context: Context, + content: NotificationContent +): NotificationCompat.Builder { + val builder = NotificationCompat.Builder(context, content.channelId) + .setSmallIcon(content.icon) + .setPriority(NotificationCompat.PRIORITY_LOW) + .setContentTitle(context.getString(content.title)) + .setStyle(NotificationCompat.BigTextStyle().bigText(context.getString(content.description))) + content.pendingIntent?.let { builder.setContentIntent(it) } + + return builder +} 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 new file mode 100644 index 0000000..6bfecbb --- /dev/null +++ b/core/src/main/java/foundation/e/advancedprivacy/domain/entities/FeatureServiceState.kt @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2023 MURENA SAS + * + * 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 FeatureServiceState { + OFF, ON, STARTING, STOPPING +} diff --git a/core/src/main/java/foundation/e/advancedprivacy/domain/entities/NotificationChannels.kt b/core/src/main/java/foundation/e/advancedprivacy/domain/entities/NotificationChannels.kt new file mode 100644 index 0000000..4458e1d --- /dev/null +++ b/core/src/main/java/foundation/e/advancedprivacy/domain/entities/NotificationChannels.kt @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2023 MURENA SAS + * + * 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 + +const val CHANNEL_FIRST_BOOT = "first_boot_notification" +const val CHANNEL_FAKE_LOCATION_FLAG = "fake_location_flag" +const val CHANNEL_IPSCRAMBLING_FLAG = "ipscrambling_flag" +const val CHANNEL_TRACKER_FLAG = "tracker_flag" + +const val NOTIFICATION_FIRST_BOOT = 1000 +const val NOTIFICATION_FAKE_LOCATION_FLAG = NOTIFICATION_FIRST_BOOT + 1 +const val NOTIFICATION_IPSCRAMBLING_FLAG = NOTIFICATION_FAKE_LOCATION_FLAG + 1 +const val NOTIFICATION_TRACKER_FLAG = NOTIFICATION_IPSCRAMBLING_FLAG + 1 diff --git a/core/src/main/java/foundation/e/advancedprivacy/domain/entities/NotificationContent.kt b/core/src/main/java/foundation/e/advancedprivacy/domain/entities/NotificationContent.kt new file mode 100644 index 0000000..44b508d --- /dev/null +++ b/core/src/main/java/foundation/e/advancedprivacy/domain/entities/NotificationContent.kt @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2023 MURENA SAS + * + * 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 + +import android.app.PendingIntent + +data class NotificationContent( + val channelId: String, + val icon: Int, + val title: Int, + val description: Int, + val pendingIntent: PendingIntent? +) diff --git a/core/src/main/java/foundation/e/advancedprivacy/externalinterfaces/permissions/APermissionsPrivacyModule.kt b/core/src/main/java/foundation/e/advancedprivacy/externalinterfaces/permissions/APermissionsPrivacyModule.kt deleted file mode 100644 index 78f424b..0000000 --- a/core/src/main/java/foundation/e/advancedprivacy/externalinterfaces/permissions/APermissionsPrivacyModule.kt +++ /dev/null @@ -1,160 +0,0 @@ -/* - * Copyright (C) 2022 - 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.externalinterfaces.permissions - -import android.app.AppOpsManager -import android.content.Context -import android.content.pm.ApplicationInfo -import android.content.pm.PackageManager -import android.content.pm.PermissionInfo -import android.content.pm.PermissionInfo.PROTECTION_DANGEROUS -import android.graphics.drawable.Drawable -import android.os.Build -import android.util.Log -import foundation.e.advancedprivacy.domain.entities.AppOpModes -import foundation.e.advancedprivacy.domain.entities.ApplicationDescription -import foundation.e.advancedprivacy.domain.entities.PermissionDescription -import foundation.e.advancedprivacy.domain.entities.ProfileType - -/** - * Implementation of the commons functionality between privileged and standard - * versions of the module. - * @param context an Android context, to retrieve packageManager for example. - */ -abstract class APermissionsPrivacyModule(protected val context: Context) : IPermissionsPrivacyModule { - - companion object { - private const val TAG = "PermissionsModule" - } - - /** - * @see IPermissionsPrivacyModule.getInstalledApplications - */ - override fun getApplicationDescription(packageName: String, withIcon: Boolean): ApplicationDescription { - val appDesc = buildApplicationDescription(context.packageManager.getApplicationInfo(packageName, 0)) - if (withIcon) { - appDesc.icon = getApplicationIcon(appDesc.packageName) - } - return appDesc - } - - /** - * * @see IPermissionsPrivacyModule.getPermissions - */ - override fun getPermissions(packageName: String): List { - val packageInfo = context.packageManager.getPackageInfo(packageName, PackageManager.GET_PERMISSIONS) - return packageInfo.requestedPermissions?.asList() ?: emptyList() - } - - override fun getPermissionDescription(permissionName: String): PermissionDescription { - val info = context.packageManager.getPermissionInfo(permissionName, 0) - return PermissionDescription( - name = permissionName, - isDangerous = isPermissionsDangerous(info), - group = null, - label = info.loadLabel(context.packageManager), - description = info.loadDescription(context.packageManager) - ) - } - - /** - * @see IPermissionsPrivacyModule.isDangerousPermissionGranted - */ - override fun isDangerousPermissionGranted(packageName: String, permissionName: String): Boolean { - return context.packageManager - .checkPermission(permissionName, packageName) == PackageManager.PERMISSION_GRANTED - } - - // on google version, work only for the current package. - @Suppress("DEPRECATION") - override fun getAppOpMode( - appDesc: ApplicationDescription, - appOpPermissionName: String - ): AppOpModes { - - val appOps = context.getSystemService(Context.APP_OPS_SERVICE) as AppOpsManager - - val mode = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { - appOps.checkOpNoThrow( - appOpPermissionName, - - appDesc.uid, appDesc.packageName - ) - } else { - appOps.unsafeCheckOpNoThrow( - appOpPermissionName, - appDesc.uid, appDesc.packageName - ) - } - - return AppOpModes.getByModeValue(mode) - } - - override fun isPermissionsDangerous(permissionName: String): Boolean { - try { - val permissionInfo = context.packageManager.getPermissionInfo(permissionName, 0) - return isPermissionsDangerous(permissionInfo) - } catch (e: Exception) { - Log.w(TAG, "exception in isPermissionsDangerous(String)", e) - return false - } - } - - @Suppress("DEPRECATION") - private fun isPermissionsDangerous(permissionInfo: PermissionInfo): Boolean { - try { - return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) { - permissionInfo.protectionLevel and PROTECTION_DANGEROUS == 1 - } else { - permissionInfo.protection == PROTECTION_DANGEROUS - } - } catch (e: Exception) { - Log.w(TAG, "exception in isPermissionsDangerous(PermissionInfo)", e) - return false - } - } - - override fun buildApplicationDescription( - appInfo: ApplicationInfo, - profileId: Int, - profileType: ProfileType - ): - ApplicationDescription { - return ApplicationDescription( - packageName = appInfo.packageName, - uid = appInfo.uid, - label = getAppLabel(appInfo), - icon = null, - profileId = profileId, - profileType = profileType, - ) - } - - private fun getAppLabel(appInfo: ApplicationInfo): CharSequence { - return context.packageManager.getApplicationLabel(appInfo) - } - - fun getApplicationIcon(appInfo: ApplicationInfo): Drawable? { - return context.packageManager.getApplicationIcon(appInfo) - } - - override fun getApplicationIcon(packageName: String): Drawable? { - return context.packageManager.getApplicationIcon(packageName) - } -} diff --git a/core/src/main/java/foundation/e/advancedprivacy/externalinterfaces/permissions/PermissionsPrivacyModuleBase.kt b/core/src/main/java/foundation/e/advancedprivacy/externalinterfaces/permissions/PermissionsPrivacyModuleBase.kt new file mode 100644 index 0000000..27ba17f --- /dev/null +++ b/core/src/main/java/foundation/e/advancedprivacy/externalinterfaces/permissions/PermissionsPrivacyModuleBase.kt @@ -0,0 +1,160 @@ +/* + * Copyright (C) 2022 - 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.externalinterfaces.permissions + +import android.app.AppOpsManager +import android.content.Context +import android.content.pm.ApplicationInfo +import android.content.pm.PackageManager +import android.content.pm.PermissionInfo +import android.content.pm.PermissionInfo.PROTECTION_DANGEROUS +import android.graphics.drawable.Drawable +import android.os.Build +import android.util.Log +import foundation.e.advancedprivacy.domain.entities.AppOpModes +import foundation.e.advancedprivacy.domain.entities.ApplicationDescription +import foundation.e.advancedprivacy.domain.entities.PermissionDescription +import foundation.e.advancedprivacy.domain.entities.ProfileType + +/** + * Implementation of the commons functionality between privileged and standard + * versions of the module. + * @param context an Android context, to retrieve packageManager for example. + */ +abstract class PermissionsPrivacyModuleBase(protected val context: Context) : IPermissionsPrivacyModule { + + companion object { + private const val TAG = "PermissionsModule" + } + + /** + * @see IPermissionsPrivacyModule.getInstalledApplications + */ + override fun getApplicationDescription(packageName: String, withIcon: Boolean): ApplicationDescription { + val appDesc = buildApplicationDescription(context.packageManager.getApplicationInfo(packageName, 0)) + if (withIcon) { + appDesc.icon = getApplicationIcon(appDesc.packageName) + } + return appDesc + } + + /** + * * @see IPermissionsPrivacyModule.getPermissions + */ + override fun getPermissions(packageName: String): List { + val packageInfo = context.packageManager.getPackageInfo(packageName, PackageManager.GET_PERMISSIONS) + return packageInfo.requestedPermissions?.asList() ?: emptyList() + } + + override fun getPermissionDescription(permissionName: String): PermissionDescription { + val info = context.packageManager.getPermissionInfo(permissionName, 0) + return PermissionDescription( + name = permissionName, + isDangerous = isPermissionsDangerous(info), + group = null, + label = info.loadLabel(context.packageManager), + description = info.loadDescription(context.packageManager) + ) + } + + /** + * @see IPermissionsPrivacyModule.isDangerousPermissionGranted + */ + override fun isDangerousPermissionGranted(packageName: String, permissionName: String): Boolean { + return context.packageManager + .checkPermission(permissionName, packageName) == PackageManager.PERMISSION_GRANTED + } + + // on google version, work only for the current package. + @Suppress("DEPRECATION") + override fun getAppOpMode( + appDesc: ApplicationDescription, + appOpPermissionName: String + ): AppOpModes { + + val appOps = context.getSystemService(Context.APP_OPS_SERVICE) as AppOpsManager + + val mode = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + appOps.checkOpNoThrow( + appOpPermissionName, + + appDesc.uid, appDesc.packageName + ) + } else { + appOps.unsafeCheckOpNoThrow( + appOpPermissionName, + appDesc.uid, appDesc.packageName + ) + } + + return AppOpModes.getByModeValue(mode) + } + + override fun isPermissionsDangerous(permissionName: String): Boolean { + try { + val permissionInfo = context.packageManager.getPermissionInfo(permissionName, 0) + return isPermissionsDangerous(permissionInfo) + } catch (e: Exception) { + Log.w(TAG, "exception in isPermissionsDangerous(String)", e) + return false + } + } + + @Suppress("DEPRECATION") + private fun isPermissionsDangerous(permissionInfo: PermissionInfo): Boolean { + try { + return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) { + permissionInfo.protectionLevel and PROTECTION_DANGEROUS == 1 + } else { + permissionInfo.protection == PROTECTION_DANGEROUS + } + } catch (e: Exception) { + Log.w(TAG, "exception in isPermissionsDangerous(PermissionInfo)", e) + return false + } + } + + override fun buildApplicationDescription( + appInfo: ApplicationInfo, + profileId: Int, + profileType: ProfileType + ): + ApplicationDescription { + return ApplicationDescription( + packageName = appInfo.packageName, + uid = appInfo.uid, + label = getAppLabel(appInfo), + icon = null, + profileId = profileId, + profileType = profileType, + ) + } + + private fun getAppLabel(appInfo: ApplicationInfo): CharSequence { + return context.packageManager.getApplicationLabel(appInfo) + } + + fun getApplicationIcon(appInfo: ApplicationInfo): Drawable? { + return context.packageManager.getApplicationIcon(appInfo) + } + + override fun getApplicationIcon(packageName: String): Drawable? { + return context.packageManager.getApplicationIcon(packageName) + } +} diff --git a/fakelocation/fakelocationdemo/src/main/java/foundation/e/privacymodules/fakelocationdemo/MainActivity.kt b/fakelocation/fakelocationdemo/src/main/java/foundation/e/privacymodules/fakelocationdemo/MainActivity.kt index f2e10a4..c105ceb 100644 --- a/fakelocation/fakelocationdemo/src/main/java/foundation/e/privacymodules/fakelocationdemo/MainActivity.kt +++ b/fakelocation/fakelocationdemo/src/main/java/foundation/e/privacymodules/fakelocationdemo/MainActivity.kt @@ -37,7 +37,7 @@ import foundation.e.advancedprivacy.domain.entities.AppOpModes import foundation.e.advancedprivacy.domain.entities.ApplicationDescription import foundation.e.advancedprivacy.domain.entities.ProfileType import foundation.e.advancedprivacy.fakelocation.domain.usecases.FakeLocationModule -import foundation.e.advancedprivacy.permissions.externalinterfaces.PermissionsPrivacyModule +import foundation.e.advancedprivacy.permissions.externalinterfaces.PermissionsPrivacyModuleImpl import foundation.e.privacymodules.fakelocationdemo.databinding.ActivityMainBinding class MainActivity : AppCompatActivity() { @@ -46,7 +46,7 @@ class MainActivity : AppCompatActivity() { } private val fakeLocationModule: FakeLocationModule by lazy { FakeLocationModule(this) } - private val permissionsModule by lazy { PermissionsPrivacyModule(this) } + private val permissionsModule by lazy { PermissionsPrivacyModuleImpl(this) } private lateinit var binding: ActivityMainBinding diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b9925d4..00bf753 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -9,6 +9,7 @@ androidx-lifecycle = "2.5.0" androidx-room = "2.3.0" orbotservice = "orbot-16.6.3-1" retrofit = "2.9.0" +pcap4j = "1.8.2" [libraries] @@ -42,6 +43,8 @@ leakcanary = { group = "com.squareup.leakcanary", name = "leakcanary-android", v maplibre = { group = "org.maplibre.gl", name = "android-sdk", version = "10.2.0" } mockk = { group = "io.mockk", name = "mockk", version = "1.10.5" } mpandroidcharts = { group = "com.github.PhilJay", name = "MPAndroidChart", version = "v3.1.0" } +pcap4j = { group = "org.pcap4j", name = "pcap4j-core", version.ref = "pcap4j" } +pcap4j-packetfactory-static = { group = "org.pcap4j", name = "pcap4j-packetfactory-static", version.ref = "pcap4j" } retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" } retrofit-scalars = { group = "com.squareup.retrofit2", name = "converter-scalars", version.ref = "retrofit" } timber = { group = "com.jakewharton.timber", name = "timber", version = "5.0.1" } @@ -50,10 +53,11 @@ timber = { group = "com.jakewharton.timber", name = "timber", version = "5.0.1" koin = ["koin-core", "koin-android"] kotlin-android-coroutines = ["androidx-core-ktx", "kotlinx-coroutines"] - +pcap4j = ["pcap4j", "pcap4j-packetfactory-static"] [plugins] android-application = { id = "com.android.application", version = "7.2.1" } +android-library = { id = "com.android.library", version = "7.2.1" } androidx-navigation-safeargs = { id = "androidx.navigation.safeargs.kotlin", version.ref = "androidx-navigation" } benmanes-versions = { id = "com.github.ben-manes.versions", version = "0.38.0" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } diff --git a/permissionse/src/main/java/foundation/e/advancedprivacy/externalinterfaces/permissions/PermissionsPrivacyModule.kt b/permissionse/src/main/java/foundation/e/advancedprivacy/externalinterfaces/permissions/PermissionsPrivacyModule.kt deleted file mode 100644 index 59a20dd..0000000 --- a/permissionse/src/main/java/foundation/e/advancedprivacy/externalinterfaces/permissions/PermissionsPrivacyModule.kt +++ /dev/null @@ -1,258 +0,0 @@ -/* - * Copyright (C) 2021 E FOUNDATION, 2022 MURENA SAS - * - * 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.permissions.externalinterfaces - -import android.annotation.TargetApi -import android.app.AppOpsManager -import android.app.AppOpsManager.OP_NONE -import android.app.AppOpsManager.strOpToOp -import android.app.NotificationChannel -import android.content.Context -import android.content.pm.PackageInfo -import android.content.pm.PackageManager -import android.content.pm.UserInfo -import android.graphics.drawable.Drawable -import android.net.IConnectivityManager -import android.net.VpnManager -import android.net.VpnManager.TYPE_VPN_SERVICE -import android.os.Build -import android.os.ServiceManager -import android.os.UserHandle -import android.os.UserManager -import android.util.Log -import foundation.e.advancedprivacy.domain.entities.AppOpModes -import foundation.e.advancedprivacy.domain.entities.ApplicationDescription -import foundation.e.advancedprivacy.domain.entities.ProfileType.MAIN -import foundation.e.advancedprivacy.domain.entities.ProfileType.WORK -import foundation.e.advancedprivacy.externalinterfaces.permissions.APermissionsPrivacyModule - -/** - * Implements [IPermissionsPrivacyModule] with all privileges of a system app. - */ -class PermissionsPrivacyModule(context: Context) : APermissionsPrivacyModule(context) { - - private val appOpsManager: AppOpsManager - get() = context.getSystemService(Context.APP_OPS_SERVICE) as AppOpsManager - - /** - * @see IPermissionsPrivacyModule.toggleDangerousPermission - * Always return true, permission is set using privileged capacities. - */ - override fun toggleDangerousPermission( - appDesc: ApplicationDescription, - permissionName: String, - grant: Boolean - ): Boolean { - try { - if (grant) { - context.packageManager.grantRuntimePermission( - appDesc.packageName, - permissionName, - android.os.Process.myUserHandle() - ) - } else { - context.packageManager.revokeRuntimePermission( - appDesc.packageName, - permissionName, - android.os.Process.myUserHandle() - ) - } - } catch (e: Exception) { - Log.e("Permissions-e", "Exception while setting permission", e) - return false - } - - return true - } - - override fun setAppOpMode( - appDesc: ApplicationDescription, - appOpPermissionName: String, - status: AppOpModes - ): Boolean { - val op = strOpToOp(appOpPermissionName) - if (op != OP_NONE) { - appOpsManager.setMode(op, appDesc.uid, appDesc.packageName, status.modeValue) - } - return true - } - - override fun getApplications( - filter: ((PackageInfo) -> Boolean)? - ): List { - val pm = context.packageManager - val mainUserId = UserHandle.myUserId() - val workProfileId = getWorkProfile()?.id - - val userIds = listOf(mainUserId, workProfileId).filterNotNull() - return userIds.map { profileId -> - pm.getInstalledPackagesAsUser(PackageManager.GET_PERMISSIONS, profileId) - .filter { filter?.invoke(it) ?: true } - .map { - buildApplicationDescription( - appInfo = it.applicationInfo, - profileId = profileId, - profileType = if (profileId == mainUserId) MAIN else WORK - ) - } - }.flatten() - } - - override fun getApplicationIcon(app: ApplicationDescription): Drawable? { - return if (app.profileType == WORK) { - getWorkProfile()?.let { workProfile -> - val pm = context.packageManager - getApplicationIcon( - pm.getApplicationInfoAsUser(app.packageName, 0, workProfile.id) - )?.let { - pm.getUserBadgedIcon(it, workProfile.getUserHandle()) - } - } - } else getApplicationIcon(app.packageName) - } - - override fun setBlockable(notificationChannel: NotificationChannel) { - when (Build.VERSION.SDK_INT) { - 29 -> notificationChannel.setBlockableSystem(true) - 30, 31, 32, 33 -> notificationChannel.setBlockable(true) - else -> { - Log.e("Permissions-e", "Bad android sdk version") - } - } - } - - override fun setVpnPackageAuthorization(packageName: String): Boolean { - return when (Build.VERSION.SDK_INT) { - 29 -> setVpnPackageAuthorizationSDK29(packageName) - 30 -> setVpnPackageAuthorizationSDK30(packageName) - 31, 32, 33 -> setVpnPackageAuthorizationSDK32(packageName) - else -> { - Log.e("Permissions-e", "Bad android sdk version") - false - } - } - } - - @TargetApi(29) - private fun setVpnPackageAuthorizationSDK29(packageName: String): Boolean { - val service: IConnectivityManager = IConnectivityManager.Stub.asInterface( - ServiceManager.getService(Context.CONNECTIVITY_SERVICE) - ) - - try { - if (service.prepareVpn(null, packageName, UserHandle.myUserId())) { - // Authorize this app to initiate VPN connections in the future without user - // intervention. - service.setVpnPackageAuthorization(packageName, UserHandle.myUserId(), true) - return true - } - } catch (e: java.lang.Exception) { - Log.e("Permissions-e", "Exception while setting VpnPackageAuthorization", e) - } catch (e: NoSuchMethodError) { - Log.e("Permissions-e", "Bad android sdk version", e) - } - return false - } - - @TargetApi(30) - private fun setVpnPackageAuthorizationSDK30(packageName: String): Boolean { - val service: IConnectivityManager = IConnectivityManager.Stub.asInterface( - ServiceManager.getService(Context.CONNECTIVITY_SERVICE) - ) - - try { - if (service.prepareVpn(null, packageName, UserHandle.myUserId())) { - // Authorize this app to initiate VPN connections in the future without user - // intervention. - service.setVpnPackageAuthorization(packageName, UserHandle.myUserId(), TYPE_VPN_SERVICE) - return true - } - } catch (e: java.lang.Exception) { - Log.e("Permissions-e", "Exception while setting VpnPackageAuthorization", e) - } catch (e: NoSuchMethodError) { - Log.e("Permissions-e", "Bad android sdk version", e) - } - return false - } - - @TargetApi(31) - private fun setVpnPackageAuthorizationSDK32(packageName: String): Boolean { - val vpnManager = context.getSystemService(Context.VPN_MANAGEMENT_SERVICE) as VpnManager - - try { - if (vpnManager.prepareVpn(null, packageName, UserHandle.myUserId())) { - // Authorize this app to initiate VPN connections in the future without user - // intervention. - vpnManager.setVpnPackageAuthorization(packageName, UserHandle.myUserId(), TYPE_VPN_SERVICE) - return true - } - } catch (e: java.lang.Exception) { - Log.e("Permissions-e", "Exception while setting VpnPackageAuthorization", e) - } catch (e: NoSuchMethodError) { - Log.e("Permissions-e", "Bad android sdk version", e) - } - return false - } - - override fun getAlwaysOnVpnPackage(): String? { - return when (Build.VERSION.SDK_INT) { - 29, 30 -> getAlwaysOnVpnPackageSDK29() - 31, 32, 33 -> getAlwaysOnVpnPackageSDK32() - else -> { - Log.e("Permissions-e", "Bad android sdk version") - null - } - } - } - - @TargetApi(29) - private fun getAlwaysOnVpnPackageSDK29(): String? { - val service: IConnectivityManager = IConnectivityManager.Stub.asInterface( - ServiceManager.getService(Context.CONNECTIVITY_SERVICE) - ) - - return try { - service.getAlwaysOnVpnPackage(UserHandle.myUserId()) - } catch (e: java.lang.Exception) { - Log.e("Permissions-e", "Bad android sdk version ", e) - return null - } - } - - @TargetApi(31) - private fun getAlwaysOnVpnPackageSDK32(): String? { - val vpnManager = context.getSystemService(Context.VPN_MANAGEMENT_SERVICE) as VpnManager - return try { - vpnManager.getAlwaysOnVpnPackageForUser(UserHandle.myUserId()) - } catch (e: java.lang.Exception) { - Log.e("Permissions-e", "Bad android sdk version ", e) - return null - } - } - - private fun getWorkProfile(): UserInfo? { - val userManager: UserManager = context.getSystemService(UserManager::class.java) - val userId = UserHandle.myUserId() - for (user in userManager.getProfiles(UserHandle.myUserId())) { - if (user.id != userId && userManager.isManagedProfile(user.id)) { - return user - } - } - return null - } -} diff --git a/permissionse/src/main/java/foundation/e/advancedprivacy/externalinterfaces/permissions/PermissionsPrivacyModuleImpl.kt b/permissionse/src/main/java/foundation/e/advancedprivacy/externalinterfaces/permissions/PermissionsPrivacyModuleImpl.kt new file mode 100644 index 0000000..0d32bce --- /dev/null +++ b/permissionse/src/main/java/foundation/e/advancedprivacy/externalinterfaces/permissions/PermissionsPrivacyModuleImpl.kt @@ -0,0 +1,258 @@ +/* + * Copyright (C) 2021 E FOUNDATION, 2022 MURENA SAS + * + * 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.permissions.externalinterfaces + +import android.annotation.TargetApi +import android.app.AppOpsManager +import android.app.AppOpsManager.OP_NONE +import android.app.AppOpsManager.strOpToOp +import android.app.NotificationChannel +import android.content.Context +import android.content.pm.PackageInfo +import android.content.pm.PackageManager +import android.content.pm.UserInfo +import android.graphics.drawable.Drawable +import android.net.IConnectivityManager +import android.net.VpnManager +import android.net.VpnManager.TYPE_VPN_SERVICE +import android.os.Build +import android.os.ServiceManager +import android.os.UserHandle +import android.os.UserManager +import android.util.Log +import foundation.e.advancedprivacy.domain.entities.AppOpModes +import foundation.e.advancedprivacy.domain.entities.ApplicationDescription +import foundation.e.advancedprivacy.domain.entities.ProfileType.MAIN +import foundation.e.advancedprivacy.domain.entities.ProfileType.WORK +import foundation.e.advancedprivacy.externalinterfaces.permissions.PermissionsPrivacyModuleBase + +/** + * Implements [IPermissionsPrivacyModule] with all privileges of a system app. + */ +class PermissionsPrivacyModuleImpl(context: Context) : PermissionsPrivacyModuleBase(context) { + + private val appOpsManager: AppOpsManager + get() = context.getSystemService(Context.APP_OPS_SERVICE) as AppOpsManager + + /** + * @see IPermissionsPrivacyModule.toggleDangerousPermission + * Always return true, permission is set using privileged capacities. + */ + override fun toggleDangerousPermission( + appDesc: ApplicationDescription, + permissionName: String, + grant: Boolean + ): Boolean { + try { + if (grant) { + context.packageManager.grantRuntimePermission( + appDesc.packageName, + permissionName, + android.os.Process.myUserHandle() + ) + } else { + context.packageManager.revokeRuntimePermission( + appDesc.packageName, + permissionName, + android.os.Process.myUserHandle() + ) + } + } catch (e: Exception) { + Log.e("Permissions-e", "Exception while setting permission", e) + return false + } + + return true + } + + override fun setAppOpMode( + appDesc: ApplicationDescription, + appOpPermissionName: String, + status: AppOpModes + ): Boolean { + val op = strOpToOp(appOpPermissionName) + if (op != OP_NONE) { + appOpsManager.setMode(op, appDesc.uid, appDesc.packageName, status.modeValue) + } + return true + } + + override fun getApplications( + filter: ((PackageInfo) -> Boolean)? + ): List { + val pm = context.packageManager + val mainUserId = UserHandle.myUserId() + val workProfileId = getWorkProfile()?.id + + val userIds = listOf(mainUserId, workProfileId).filterNotNull() + return userIds.map { profileId -> + pm.getInstalledPackagesAsUser(PackageManager.GET_PERMISSIONS, profileId) + .filter { filter?.invoke(it) ?: true } + .map { + buildApplicationDescription( + appInfo = it.applicationInfo, + profileId = profileId, + profileType = if (profileId == mainUserId) MAIN else WORK + ) + } + }.flatten() + } + + override fun getApplicationIcon(app: ApplicationDescription): Drawable? { + return if (app.profileType == WORK) { + getWorkProfile()?.let { workProfile -> + val pm = context.packageManager + getApplicationIcon( + pm.getApplicationInfoAsUser(app.packageName, 0, workProfile.id) + )?.let { + pm.getUserBadgedIcon(it, workProfile.getUserHandle()) + } + } + } else getApplicationIcon(app.packageName) + } + + override fun setBlockable(notificationChannel: NotificationChannel) { + when (Build.VERSION.SDK_INT) { + 29 -> notificationChannel.setBlockableSystem(true) + 30, 31, 32, 33 -> notificationChannel.setBlockable(true) + else -> { + Log.e("Permissions-e", "Bad android sdk version") + } + } + } + + override fun setVpnPackageAuthorization(packageName: String): Boolean { + return when (Build.VERSION.SDK_INT) { + 29 -> setVpnPackageAuthorizationSDK29(packageName) + 30 -> setVpnPackageAuthorizationSDK30(packageName) + 31, 32, 33 -> setVpnPackageAuthorizationSDK32(packageName) + else -> { + Log.e("Permissions-e", "Bad android sdk version") + false + } + } + } + + @TargetApi(29) + private fun setVpnPackageAuthorizationSDK29(packageName: String): Boolean { + val service: IConnectivityManager = IConnectivityManager.Stub.asInterface( + ServiceManager.getService(Context.CONNECTIVITY_SERVICE) + ) + + try { + if (service.prepareVpn(null, packageName, UserHandle.myUserId())) { + // Authorize this app to initiate VPN connections in the future without user + // intervention. + service.setVpnPackageAuthorization(packageName, UserHandle.myUserId(), true) + return true + } + } catch (e: java.lang.Exception) { + Log.e("Permissions-e", "Exception while setting VpnPackageAuthorization", e) + } catch (e: NoSuchMethodError) { + Log.e("Permissions-e", "Bad android sdk version", e) + } + return false + } + + @TargetApi(30) + private fun setVpnPackageAuthorizationSDK30(packageName: String): Boolean { + val service: IConnectivityManager = IConnectivityManager.Stub.asInterface( + ServiceManager.getService(Context.CONNECTIVITY_SERVICE) + ) + + try { + if (service.prepareVpn(null, packageName, UserHandle.myUserId())) { + // Authorize this app to initiate VPN connections in the future without user + // intervention. + service.setVpnPackageAuthorization(packageName, UserHandle.myUserId(), TYPE_VPN_SERVICE) + return true + } + } catch (e: java.lang.Exception) { + Log.e("Permissions-e", "Exception while setting VpnPackageAuthorization", e) + } catch (e: NoSuchMethodError) { + Log.e("Permissions-e", "Bad android sdk version", e) + } + return false + } + + @TargetApi(31) + private fun setVpnPackageAuthorizationSDK32(packageName: String): Boolean { + val vpnManager = context.getSystemService(Context.VPN_MANAGEMENT_SERVICE) as VpnManager + + try { + if (vpnManager.prepareVpn(null, packageName, UserHandle.myUserId())) { + // Authorize this app to initiate VPN connections in the future without user + // intervention. + vpnManager.setVpnPackageAuthorization(packageName, UserHandle.myUserId(), TYPE_VPN_SERVICE) + return true + } + } catch (e: java.lang.Exception) { + Log.e("Permissions-e", "Exception while setting VpnPackageAuthorization", e) + } catch (e: NoSuchMethodError) { + Log.e("Permissions-e", "Bad android sdk version", e) + } + return false + } + + override fun getAlwaysOnVpnPackage(): String? { + return when (Build.VERSION.SDK_INT) { + 29, 30 -> getAlwaysOnVpnPackageSDK29() + 31, 32, 33 -> getAlwaysOnVpnPackageSDK32() + else -> { + Log.e("Permissions-e", "Bad android sdk version") + null + } + } + } + + @TargetApi(29) + private fun getAlwaysOnVpnPackageSDK29(): String? { + val service: IConnectivityManager = IConnectivityManager.Stub.asInterface( + ServiceManager.getService(Context.CONNECTIVITY_SERVICE) + ) + + return try { + service.getAlwaysOnVpnPackage(UserHandle.myUserId()) + } catch (e: java.lang.Exception) { + Log.e("Permissions-e", "Bad android sdk version ", e) + return null + } + } + + @TargetApi(31) + private fun getAlwaysOnVpnPackageSDK32(): String? { + val vpnManager = context.getSystemService(Context.VPN_MANAGEMENT_SERVICE) as VpnManager + return try { + vpnManager.getAlwaysOnVpnPackageForUser(UserHandle.myUserId()) + } catch (e: java.lang.Exception) { + Log.e("Permissions-e", "Bad android sdk version ", e) + return null + } + } + + private fun getWorkProfile(): UserInfo? { + val userManager: UserManager = context.getSystemService(UserManager::class.java) + val userId = UserHandle.myUserId() + for (user in userManager.getProfiles(UserHandle.myUserId())) { + if (user.id != userId && userManager.isManagedProfile(user.id)) { + return user + } + } + return null + } +} diff --git a/permissionsstandalone/src/main/java/foundation/e/advancedprivacy/permissions/externalinterfaces/PermissionsPrivacyModule.kt b/permissionsstandalone/src/main/java/foundation/e/advancedprivacy/permissions/externalinterfaces/PermissionsPrivacyModule.kt deleted file mode 100644 index 95f5ff0..0000000 --- a/permissionsstandalone/src/main/java/foundation/e/advancedprivacy/permissions/externalinterfaces/PermissionsPrivacyModule.kt +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright (C) 2022 MURENA SAS - * - * 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.permissions.externalinterfaces - -import android.app.NotificationChannel -import android.content.Context -import android.content.pm.PackageInfo -import android.content.pm.PackageManager -import android.graphics.drawable.Drawable -import foundation.e.advancedprivacy.domain.entities.AppOpModes -import foundation.e.advancedprivacy.domain.entities.ApplicationDescription -import foundation.e.advancedprivacy.externalinterfaces.permissions.APermissionsPrivacyModule - -/** - * Implements [IPermissionsPrivacyModule] using only API authorized on the PlayStore. - */ -class PermissionsPrivacyModule(context: Context) : APermissionsPrivacyModule(context) { - override fun getApplications( - filter: ((PackageInfo) -> Boolean)? - ): List { - return context.packageManager.getInstalledPackages(PackageManager.GET_PERMISSIONS) - .filter { filter?.invoke(it) == true } - .map { buildApplicationDescription(it.applicationInfo) } - } - - override fun getApplicationIcon(app: ApplicationDescription): Drawable? { - return getApplicationIcon(app.packageName) - } - - /** - * @see IPermissionsPrivacyModule.toggleDangerousPermission - * Return an ManualAction to go toggle manually the permission in the ap page of the settings. - */ - override fun toggleDangerousPermission( - appDesc: ApplicationDescription, - permissionName: String, - grant: Boolean - ): Boolean = false - - override fun setAppOpMode( - appDesc: ApplicationDescription, - appOpPermissionName: String, - status: AppOpModes - ): Boolean = false - - override fun setVpnPackageAuthorization(packageName: String): Boolean { - return false - } - - override fun getAlwaysOnVpnPackage(): String? { - return null - } - - override fun setBlockable(notificationChannel: NotificationChannel) {} -} diff --git a/permissionsstandalone/src/main/java/foundation/e/advancedprivacy/permissions/externalinterfaces/PermissionsPrivacyModuleImpl.kt b/permissionsstandalone/src/main/java/foundation/e/advancedprivacy/permissions/externalinterfaces/PermissionsPrivacyModuleImpl.kt new file mode 100644 index 0000000..d31bdf4 --- /dev/null +++ b/permissionsstandalone/src/main/java/foundation/e/advancedprivacy/permissions/externalinterfaces/PermissionsPrivacyModuleImpl.kt @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2022 MURENA SAS + * + * 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.permissions.externalinterfaces + +import android.app.NotificationChannel +import android.content.Context +import android.content.pm.PackageInfo +import android.content.pm.PackageManager +import android.graphics.drawable.Drawable +import foundation.e.advancedprivacy.domain.entities.AppOpModes +import foundation.e.advancedprivacy.domain.entities.ApplicationDescription +import foundation.e.advancedprivacy.externalinterfaces.permissions.PermissionsPrivacyModuleBase + +/** + * Implements [IPermissionsPrivacyModule] using only API authorized on the PlayStore. + */ +class PermissionsPrivacyModuleImpl(context: Context) : PermissionsPrivacyModuleBase(context) { + override fun getApplications( + filter: ((PackageInfo) -> Boolean)? + ): List { + return context.packageManager.getInstalledPackages(PackageManager.GET_PERMISSIONS) + .filter { filter?.invoke(it) == true } + .map { buildApplicationDescription(it.applicationInfo) } + } + + override fun getApplicationIcon(app: ApplicationDescription): Drawable? { + return getApplicationIcon(app.packageName) + } + + /** + * @see IPermissionsPrivacyModule.toggleDangerousPermission + * Return an ManualAction to go toggle manually the permission in the ap page of the settings. + */ + override fun toggleDangerousPermission( + appDesc: ApplicationDescription, + permissionName: String, + grant: Boolean + ): Boolean = false + + override fun setAppOpMode( + appDesc: ApplicationDescription, + appOpPermissionName: String, + status: AppOpModes + ): Boolean = false + + override fun setVpnPackageAuthorization(packageName: String): Boolean { + return false + } + + override fun getAlwaysOnVpnPackage(): String? { + return null + } + + override fun setBlockable(notificationChannel: NotificationChannel) {} +} diff --git a/settings.gradle b/settings.gradle index 0b4940e..39e58c8 100644 --- a/settings.gradle +++ b/settings.gradle @@ -18,7 +18,8 @@ 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) diff --git a/trackers/src/main/AndroidManifest.xml b/trackers/src/main/AndroidManifest.xml index 615d310..b980706 100644 --- a/trackers/src/main/AndroidManifest.xml +++ b/trackers/src/main/AndroidManifest.xml @@ -18,21 +18,4 @@ --> - - - - - - - - - - - - - - \ No newline at end of file diff --git a/trackers/src/main/java/foundation/e/advancedprivacy/trackers/KoinModule.kt b/trackers/src/main/java/foundation/e/advancedprivacy/trackers/KoinModule.kt index 0cfb69c..34b4e7a 100644 --- a/trackers/src/main/java/foundation/e/advancedprivacy/trackers/KoinModule.kt +++ b/trackers/src/main/java/foundation/e/advancedprivacy/trackers/KoinModule.kt @@ -21,13 +21,13 @@ import foundation.e.advancedprivacy.data.repositories.RemoteTrackersListReposito import foundation.e.advancedprivacy.trackers.data.StatsDatabase import foundation.e.advancedprivacy.trackers.data.TrackersRepository import foundation.e.advancedprivacy.trackers.data.WhitelistRepository -import foundation.e.advancedprivacy.trackers.domain.usecases.DNSBlocker +import foundation.e.advancedprivacy.trackers.domain.usecases.FilterHostnameUseCase import foundation.e.advancedprivacy.trackers.domain.usecases.StatisticsUseCase -import foundation.e.advancedprivacy.trackers.domain.usecases.TrackersLogger import foundation.e.advancedprivacy.trackers.domain.usecases.UpdateTrackerListUseCase import org.koin.android.ext.koin.androidContext import org.koin.core.module.dsl.factoryOf import org.koin.core.module.dsl.singleOf +import org.koin.core.qualifier.named import org.koin.dsl.module val trackersModule = module { @@ -58,15 +58,13 @@ val trackersModule = module { } factory { - DNSBlocker( - context = androidContext(), - trackersLogger = get(), + FilterHostnameUseCase( trackersRepository = get(), - whitelistRepository = get() + whitelistRepository = get(), + appDesc = get(named("AdvancedPrivacy")), + context = androidContext(), + database = get(), + appListsRepository = get() ) } - - factory { - TrackersLogger(statisticsUseCase = get()) - } } diff --git a/trackers/src/main/java/foundation/e/advancedprivacy/trackers/data/RemoteTrackersListRepository.kt b/trackers/src/main/java/foundation/e/advancedprivacy/trackers/data/RemoteTrackersListRepository.kt index c2c0768..64477b7 100644 --- a/trackers/src/main/java/foundation/e/advancedprivacy/trackers/data/RemoteTrackersListRepository.kt +++ b/trackers/src/main/java/foundation/e/advancedprivacy/trackers/data/RemoteTrackersListRepository.kt @@ -39,7 +39,7 @@ class RemoteTrackersListRepository { } return true } catch (e: IOException) { - Timber.e("While saving tracker file.", e) + Timber.e(e, "While saving tracker file.") } return false } diff --git a/trackers/src/main/java/foundation/e/advancedprivacy/trackers/data/StatsDatabase.kt b/trackers/src/main/java/foundation/e/advancedprivacy/trackers/data/StatsDatabase.kt index 6aa76cf..15ff813 100644 --- a/trackers/src/main/java/foundation/e/advancedprivacy/trackers/data/StatsDatabase.kt +++ b/trackers/src/main/java/foundation/e/advancedprivacy/trackers/data/StatsDatabase.kt @@ -32,6 +32,10 @@ import foundation.e.advancedprivacy.trackers.data.StatsDatabase.AppTrackerEntry. import foundation.e.advancedprivacy.trackers.data.StatsDatabase.AppTrackerEntry.COLUMN_NAME_TRACKER import foundation.e.advancedprivacy.trackers.data.StatsDatabase.AppTrackerEntry.TABLE_NAME import foundation.e.advancedprivacy.trackers.domain.entities.Tracker +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.withContext import timber.log.Timber import java.time.ZonedDateTime import java.time.format.DateTimeFormatter @@ -86,6 +90,9 @@ class StatsDatabase( COLUMN_NAME_APPID ) + private val _newDataAvailable = MutableSharedFlow() + val newDataAvailable: SharedFlow = _newDataAvailable + private val lock = Any() override fun onCreate(db: SQLiteDatabase) { @@ -316,7 +323,9 @@ class StatsDatabase( } } - fun logAccess(trackerId: String?, appId: String, blocked: Boolean) { + suspend fun logAccess(trackerId: String?, appId: String, blocked: Boolean) = withContext( + Dispatchers.IO + ) { synchronized(lock) { val currentHour = getCurrentHourTs() val db = writableDatabase @@ -364,6 +373,7 @@ class StatsDatabase( cursor.close() db.close() } + _newDataAvailable.emit(Unit) } private fun cursorToEntry(cursor: Cursor): StatEntry { diff --git a/trackers/src/main/java/foundation/e/advancedprivacy/trackers/data/TrackersRepository.kt b/trackers/src/main/java/foundation/e/advancedprivacy/trackers/data/TrackersRepository.kt index a7d5e49..fc57a8e 100644 --- a/trackers/src/main/java/foundation/e/advancedprivacy/trackers/data/TrackersRepository.kt +++ b/trackers/src/main/java/foundation/e/advancedprivacy/trackers/data/TrackersRepository.kt @@ -59,7 +59,7 @@ class TrackersRepository( reader.close() inputStream.close() } catch (e: Exception) { - Timber.e("While parsing trackers in assets", e) + Timber.e(e, "While parsing trackers in assets") } } 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 new file mode 100644 index 0000000..d9674fc --- /dev/null +++ b/trackers/src/main/java/foundation/e/advancedprivacy/trackers/domain/externalinterfaces/TrackersServiceSupervisor.kt @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2023 MURENA SAS + * + * 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.trackers.domain.externalinterfaces + +interface TrackersServiceSupervisor { + fun start(): Boolean + fun stop(): Boolean + fun isRunning(): Boolean +} diff --git a/trackers/src/main/java/foundation/e/advancedprivacy/trackers/domain/usecases/DNSBlocker.kt b/trackers/src/main/java/foundation/e/advancedprivacy/trackers/domain/usecases/DNSBlocker.kt deleted file mode 100644 index fb08910..0000000 --- a/trackers/src/main/java/foundation/e/advancedprivacy/trackers/domain/usecases/DNSBlocker.kt +++ /dev/null @@ -1,143 +0,0 @@ -/* - * Copyright (C) 2023 MURENA SAS - * Copyright (C) 2022 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.trackers.domain.usecases - -import android.content.Context -import android.content.pm.PackageManager -import android.net.LocalServerSocket -import android.system.ErrnoException -import android.system.Os -import android.system.OsConstants -import foundation.e.advancedprivacy.core.utils.runSuspendCatching -import foundation.e.advancedprivacy.trackers.data.TrackersRepository -import foundation.e.advancedprivacy.trackers.data.WhitelistRepository -import kotlinx.coroutines.CancellationException -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.isActive -import kotlinx.coroutines.launch -import timber.log.Timber -import java.io.BufferedReader -import java.io.InputStreamReader -import java.io.PrintWriter - -class DNSBlocker( - context: Context, - val trackersLogger: TrackersLogger, - private val trackersRepository: TrackersRepository, - private val whitelistRepository: WhitelistRepository -) { - private var resolverReceiver: LocalServerSocket? = null - private var eBrowserAppUid = -1 - - companion object { - private const val SOCKET_NAME = "foundation.e.advancedprivacy" - private const val E_BROWSER_DOT_SERVER = "chrome.cloudflare-dns.com" - } - - init { - initEBrowserDoTFix(context) - } - - private fun closeSocket() { - // Known bug and workaround that LocalServerSocket::close is not working well - // https://issuetracker.google.com/issues/36945762 - if (resolverReceiver != null) { - try { - Os.shutdown(resolverReceiver!!.fileDescriptor, OsConstants.SHUT_RDWR) - resolverReceiver!!.close() - resolverReceiver = null - } catch (e: ErrnoException) { - if (e.errno != OsConstants.EBADF) { - Timber.w("Socket already closed") - } else { - Timber.e(e, "Exception: cannot close DNS port on stop $SOCKET_NAME !") - } - } catch (e: Exception) { - Timber.e(e, "Exception: cannot close DNS port on stop $SOCKET_NAME !") - } - } - } - - fun listenJob(scope: CoroutineScope): Job = scope.launch(Dispatchers.IO) { - val resolverReceiver = runSuspendCatching { - LocalServerSocket(SOCKET_NAME) - }.getOrElse { - Timber.e(it, "Exception: cannot open DNS port on $SOCKET_NAME") - return@launch - } - - this@DNSBlocker.resolverReceiver = resolverReceiver - Timber.d("DNSFilterProxy running on port $SOCKET_NAME") - - while (isActive) { - runSuspendCatching { - val socket = resolverReceiver.accept() - val reader = BufferedReader(InputStreamReader(socket.inputStream)) - val line = reader.readLine() - val params = line.split(",").toTypedArray() - val output = socket.outputStream - val writer = PrintWriter(output, true) - val domainName = params[0] - val appUid = params[1].toInt() - var isBlocked = false - if (isEBrowserDoTBlockFix(appUid, domainName)) { - isBlocked = true - } else if (trackersRepository.isTracker(domainName)) { - val trackerId = trackersRepository.getTrackerId(domainName) - if (shouldBlock(appUid, trackerId)) { - writer.println("block") - isBlocked = true - } - trackersLogger.logAccess(trackerId, appUid, isBlocked) - } - if (!isBlocked) { - writer.println("pass") - } - socket.close() - }.onFailure { - if (it is CancellationException) { - closeSocket() - throw it - } else { - Timber.w(it, "Exception while listening DNS resolver") - } - } - } - } - - private fun initEBrowserDoTFix(context: Context) { - try { - eBrowserAppUid = - context.packageManager.getApplicationInfo("foundation.e.browser", 0).uid - } catch (e: PackageManager.NameNotFoundException) { - Timber.i(e, "no E Browser package found.") - } - } - - private fun isEBrowserDoTBlockFix(appUid: Int, hostname: String): Boolean { - return appUid == eBrowserAppUid && E_BROWSER_DOT_SERVER == hostname - } - - private fun shouldBlock(appUid: Int, trackerId: String?): Boolean { - return whitelistRepository.isBlockingEnabled && - !whitelistRepository.isWhiteListed(appUid, trackerId) - } -} diff --git a/trackers/src/main/java/foundation/e/advancedprivacy/trackers/domain/usecases/FilterHostnameUseCase.kt b/trackers/src/main/java/foundation/e/advancedprivacy/trackers/domain/usecases/FilterHostnameUseCase.kt new file mode 100644 index 0000000..e229cab --- /dev/null +++ b/trackers/src/main/java/foundation/e/advancedprivacy/trackers/domain/usecases/FilterHostnameUseCase.kt @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2023 MURENA SAS + * + * 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.trackers.domain.usecases + +import android.content.Context +import android.content.pm.PackageManager +import foundation.e.advancedprivacy.core.utils.runSuspendCatching +import foundation.e.advancedprivacy.data.repositories.AppListsRepository +import foundation.e.advancedprivacy.domain.entities.ApplicationDescription +import foundation.e.advancedprivacy.trackers.data.StatsDatabase +import foundation.e.advancedprivacy.trackers.data.TrackersRepository +import foundation.e.advancedprivacy.trackers.data.WhitelistRepository +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import timber.log.Timber +import java.util.concurrent.LinkedBlockingQueue + +class FilterHostnameUseCase( + private val trackersRepository: TrackersRepository, + private val whitelistRepository: WhitelistRepository, + context: Context, + private val appDesc: ApplicationDescription, + private val database: StatsDatabase, + private val appListsRepository: AppListsRepository, +) { + private var eBrowserAppUid = -1 + + companion object { + private const val E_BROWSER_DOT_SERVER = "chrome.cloudflare-dns.com" + } + + init { + initEBrowserDoTFix(context) + } + + fun shouldBlock(hostname: String, appUid: Int = appDesc.uid): Boolean { + var isBlocked = false + + if (isEBrowserDoTBlockFix(appUid, hostname)) { + isBlocked = true + } else if (trackersRepository.isTracker(hostname)) { + val trackerId = trackersRepository.getTrackerId(hostname) + if (shouldBlock(appUid, trackerId)) { + isBlocked = true + } + queue.offer(DetectedTracker(trackerId, appUid, isBlocked)) + } + return isBlocked + } + + private fun initEBrowserDoTFix(context: Context) { + try { + eBrowserAppUid = + context.packageManager.getApplicationInfo("foundation.e.browser", 0).uid + } catch (e: PackageManager.NameNotFoundException) { + Timber.i("no E Browser package found.") + } + } + + private fun isEBrowserDoTBlockFix(appUid: Int, hostname: String): Boolean { + return appUid == eBrowserAppUid && + E_BROWSER_DOT_SERVER == hostname + } + + private fun shouldBlock(appUid: Int, trackerId: String?): Boolean { + return whitelistRepository.isBlockingEnabled && + !whitelistRepository.isWhiteListed(appUid, trackerId) + } + + private val queue = LinkedBlockingQueue() + + private suspend fun logAccess(detectedTracker: DetectedTracker) { + appListsRepository.getApp(detectedTracker.appUid)?.let { app -> + database.logAccess(detectedTracker.trackerId, app.apId, detectedTracker.wasBlocked) + } + } + + fun writeLogJob(scope: CoroutineScope): Job { + return scope.launch(Dispatchers.IO) { + while (isActive) { + runSuspendCatching { + logAccess(queue.take()) + }.onFailure { + Timber.e(it, "writeLogLoop detectedTrackersQueue.take() interrupted: ") + } + } + } + } + + inner class DetectedTracker(var trackerId: String?, var appUid: Int, var wasBlocked: Boolean) +} diff --git a/trackers/src/main/java/foundation/e/advancedprivacy/trackers/domain/usecases/StatisticsUseCase.kt b/trackers/src/main/java/foundation/e/advancedprivacy/trackers/domain/usecases/StatisticsUseCase.kt index 55efeb9..e7a84b8 100644 --- a/trackers/src/main/java/foundation/e/advancedprivacy/trackers/domain/usecases/StatisticsUseCase.kt +++ b/trackers/src/main/java/foundation/e/advancedprivacy/trackers/domain/usecases/StatisticsUseCase.kt @@ -22,24 +22,12 @@ import foundation.e.advancedprivacy.data.repositories.AppListsRepository import foundation.e.advancedprivacy.domain.entities.ApplicationDescription import foundation.e.advancedprivacy.trackers.data.StatsDatabase import foundation.e.advancedprivacy.trackers.domain.entities.Tracker -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.SharedFlow import java.time.temporal.TemporalUnit class StatisticsUseCase( private val database: StatsDatabase, private val appListsRepository: AppListsRepository ) { - private val _newDataAvailable = MutableSharedFlow() - val newDataAvailable: SharedFlow = _newDataAvailable - - suspend fun logAccess(trackerId: String?, appUid: Int, blocked: Boolean) { - appListsRepository.getApp(appUid)?.let { app -> - database.logAccess(trackerId, app.apId, blocked) - _newDataAvailable.emit(Unit) - } - } - fun getTrackersCallsOnPeriod( periodsCount: Int, periodUnit: TemporalUnit diff --git a/trackers/src/main/java/foundation/e/advancedprivacy/trackers/domain/usecases/TrackersLogger.kt b/trackers/src/main/java/foundation/e/advancedprivacy/trackers/domain/usecases/TrackersLogger.kt deleted file mode 100644 index 411b4ab..0000000 --- a/trackers/src/main/java/foundation/e/advancedprivacy/trackers/domain/usecases/TrackersLogger.kt +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright (C) 2023 MURENA SAS - * Copyright (C) 2022 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.trackers.domain.usecases - -import foundation.e.advancedprivacy.core.utils.runSuspendCatching -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.isActive -import kotlinx.coroutines.launch -import timber.log.Timber -import java.util.concurrent.LinkedBlockingQueue - -class TrackersLogger( - private val statisticsUseCase: StatisticsUseCase, -) { - private val queue = LinkedBlockingQueue() - - fun logAccess(trackerId: String?, appUid: Int, wasBlocked: Boolean) { - queue.offer(DetectedTracker(trackerId, appUid, wasBlocked)) - } - - fun writeLogJob(scope: CoroutineScope): Job { - return scope.launch(Dispatchers.IO) { - while (isActive) { - runSuspendCatching { - logAccess(queue.take()) - }.onFailure { - Timber.e(it, "writeLogLoop detectedTrackersQueue.take() interrupted: ") - } - } - } - } - - private suspend fun logAccess(detectedTracker: DetectedTracker) { - statisticsUseCase.logAccess( - detectedTracker.trackerId, - detectedTracker.appUid, - detectedTracker.wasBlocked - ) - } - - inner class DetectedTracker(var trackerId: String?, var appUid: Int, var wasBlocked: Boolean) -} diff --git a/trackers/src/main/java/foundation/e/advancedprivacy/trackers/domain/usecases/UpdateTrackerListUseCase.kt b/trackers/src/main/java/foundation/e/advancedprivacy/trackers/domain/usecases/UpdateTrackerListUseCase.kt index 55da644..fa60431 100644 --- a/trackers/src/main/java/foundation/e/advancedprivacy/trackers/domain/usecases/UpdateTrackerListUseCase.kt +++ b/trackers/src/main/java/foundation/e/advancedprivacy/trackers/domain/usecases/UpdateTrackerListUseCase.kt @@ -40,7 +40,7 @@ class UpdateTrackerListUseCase( remoteTrackersListRepository.saveData(trackersRepository.eTrackerFile, api.trackers()) trackersRepository.initTrackersFile() } catch (e: Exception) { - Timber.e("While updating trackers", e) + Timber.e(e, "While updating trackers") } } } diff --git a/trackers/src/main/java/foundation/e/advancedprivacy/trackers/services/DNSBlockerService.kt b/trackers/src/main/java/foundation/e/advancedprivacy/trackers/services/DNSBlockerService.kt deleted file mode 100644 index 25539e1..0000000 --- a/trackers/src/main/java/foundation/e/advancedprivacy/trackers/services/DNSBlockerService.kt +++ /dev/null @@ -1,68 +0,0 @@ -/* - * 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.trackers.services - -import android.app.Service -import android.content.Intent -import android.os.IBinder -import foundation.e.advancedprivacy.trackers.domain.usecases.DNSBlocker -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.cancel -import org.koin.java.KoinJavaComponent.get - -class DNSBlockerService : Service() { - companion object { - const val ACTION_START = "foundation.e.privacymodules.trackers.intent.action.START" - const val EXTRA_ENABLE_NOTIFICATION = - "foundation.e.privacymodules.trackers.intent.extra.ENABLED_NOTIFICATION" - } - - private var coroutineScope = CoroutineScope(Dispatchers.IO) - private var dnsBlocker: DNSBlocker? = null - - override fun onBind(intent: Intent): IBinder? { - throw UnsupportedOperationException("Not yet implemented") - } - - override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - if (ACTION_START == intent?.action) { - if (intent.getBooleanExtra(EXTRA_ENABLE_NOTIFICATION, true)) { - ForegroundStarter.startForeground(this) - } - stop() - start() - } - return START_REDELIVER_INTENT - } - - private fun start() { - coroutineScope = CoroutineScope(Dispatchers.IO) - get(DNSBlocker::class.java).apply { - this@DNSBlockerService.dnsBlocker = this - trackersLogger.writeLogJob(coroutineScope) - listenJob(coroutineScope) - } - } - - private fun stop() { - kotlin.runCatching { coroutineScope.cancel() } - dnsBlocker = null - } -} diff --git a/trackersservicee/.gitignore b/trackersservicee/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/trackersservicee/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/trackersservicee/build.gradle b/trackersservicee/build.gradle new file mode 100644 index 0000000..f7725bf --- /dev/null +++ b/trackersservicee/build.gradle @@ -0,0 +1,43 @@ +plugins { + id 'com.android.library' + id 'org.jetbrains.kotlin.android' +} + +android { + namespace 'foundation.e.advancedprivacy.trackers.service' + compileSdk 33 + + defaultConfig { + minSdk 24 + targetSdk 33 + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles "consumer-rules.pro" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = '1.8' + } +} + +dependencies { + implementation project(":core") + implementation project(":trackers") + + implementation( + libs.androidx.core.ktx, + libs.bundles.koin, + libs.kotlinx.coroutines, + libs.timber, + ) +} diff --git a/trackersservicee/consumer-rules.pro b/trackersservicee/consumer-rules.pro new file mode 100644 index 0000000..e69de29 diff --git a/trackersservicee/proguard-rules.pro b/trackersservicee/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/trackersservicee/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/trackersservicee/src/main/AndroidManifest.xml b/trackersservicee/src/main/AndroidManifest.xml new file mode 100644 index 0000000..2290432 --- /dev/null +++ b/trackersservicee/src/main/AndroidManifest.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/trackersservicee/src/main/java/foundation/e/advancedprivacy/trackers/service/DNSBlocker.kt b/trackersservicee/src/main/java/foundation/e/advancedprivacy/trackers/service/DNSBlocker.kt new file mode 100644 index 0000000..6a2b218 --- /dev/null +++ b/trackersservicee/src/main/java/foundation/e/advancedprivacy/trackers/service/DNSBlocker.kt @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2023 MURENA SAS + * Copyright (C) 2022 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.trackers.service + +import android.net.LocalServerSocket +import android.system.ErrnoException +import android.system.Os +import android.system.OsConstants +import foundation.e.advancedprivacy.core.utils.runSuspendCatching +import foundation.e.advancedprivacy.trackers.domain.usecases.FilterHostnameUseCase +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import timber.log.Timber +import java.io.BufferedReader +import java.io.InputStreamReader +import java.io.PrintWriter + +class DNSBlocker( + val filterHostnameUseCase: FilterHostnameUseCase +) { + private var resolverReceiver: LocalServerSocket? = null + + companion object { + private const val SOCKET_NAME = "foundation.e.advancedprivacy" + } + + private fun closeSocket() { + // Known bug and workaround that LocalServerSocket::close is not working well + // https://issuetracker.google.com/issues/36945762 + if (resolverReceiver != null) { + try { + Os.shutdown(resolverReceiver!!.fileDescriptor, OsConstants.SHUT_RDWR) + resolverReceiver!!.close() + resolverReceiver = null + } catch (e: ErrnoException) { + if (e.errno != OsConstants.EBADF) { + Timber.w("Socket already closed") + } else { + Timber.e(e, "Exception: cannot close DNS port on stop $SOCKET_NAME !") + } + } catch (e: Exception) { + Timber.e(e, "Exception: cannot close DNS port on stop $SOCKET_NAME !") + } + } + } + + fun listenJob(scope: CoroutineScope): Job = scope.launch(Dispatchers.IO) { + val resolverReceiver = runSuspendCatching { + LocalServerSocket(SOCKET_NAME) + }.getOrElse { + Timber.e(it, "Exception: cannot open DNS port on $SOCKET_NAME") + return@launch + } + + this@DNSBlocker.resolverReceiver = resolverReceiver + Timber.d("DNSFilterProxy running on port $SOCKET_NAME") + + while (isActive) { + runSuspendCatching { + val socket = resolverReceiver.accept() + val reader = BufferedReader(InputStreamReader(socket.inputStream)) + val line = reader.readLine() + val params = line.split(",").toTypedArray() + val output = socket.outputStream + val writer = PrintWriter(output, true) + val domainName = params[0] + val appUid = params[1].toInt() + if (filterHostnameUseCase.shouldBlock(domainName, appUid)) { + writer.println("block") + } else { + writer.println("pass") + } + socket.close() + }.onFailure { + if (it is CancellationException) { + closeSocket() + throw it + } else { + Timber.w(it, "Exception while listening DNS resolver") + } + } + } + } +} diff --git a/trackersservicee/src/main/java/foundation/e/advancedprivacy/trackers/service/TrackersService.kt b/trackersservicee/src/main/java/foundation/e/advancedprivacy/trackers/service/TrackersService.kt new file mode 100644 index 0000000..5f573b0 --- /dev/null +++ b/trackersservicee/src/main/java/foundation/e/advancedprivacy/trackers/service/TrackersService.kt @@ -0,0 +1,58 @@ +/* + * 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.trackers.service + +import android.app.Service +import android.content.Intent +import android.os.IBinder +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.cancel +import org.koin.java.KoinJavaComponent.get + +class TrackersService : Service() { + companion object { + const val ACTION_START = "foundation.e.privacymodules.trackers.intent.action.START" + + var coroutineScope = CoroutineScope(Dispatchers.IO) + } + + override fun onBind(intent: Intent): IBinder? { + throw UnsupportedOperationException("Not yet implemented") + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + if (ACTION_START == intent?.action) { + stop() + start() + } + return START_REDELIVER_INTENT + } + + private fun start() { + coroutineScope = CoroutineScope(Dispatchers.IO) + get(DNSBlocker::class.java).apply { + filterHostnameUseCase.writeLogJob(coroutineScope) + listenJob(coroutineScope) + } + } + + private fun stop() { + kotlin.runCatching { coroutineScope.cancel() } + } +} 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 new file mode 100644 index 0000000..3903db4 --- /dev/null +++ b/trackersservicee/src/main/java/foundation/e/advancedprivacy/trackers/service/TrackersServiceSupervisorImpl.kt @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2023 MURENA SAS + * + * 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.trackers.service + +import android.content.Context +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.factoryOf +import org.koin.dsl.module + +class TrackersServiceSupervisorImpl(private val context: Context) : TrackersServiceSupervisor { + + override fun start(): Boolean { + val intent = Intent(context, TrackersService::class.java) + intent.action = ACTION_START + return context.startService(intent) != null + } + + override fun stop(): Boolean { + return context.stopService(Intent(context, TrackersService::class.java)) + } + + override fun isRunning(): Boolean { + return TrackersService.coroutineScope.isActive + } +} + +val trackerServiceModule = module { + factoryOf(::DNSBlocker) +} diff --git a/trackersservicestandalone/.gitignore b/trackersservicestandalone/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/trackersservicestandalone/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/trackersservicestandalone/build.gradle b/trackersservicestandalone/build.gradle new file mode 100644 index 0000000..d5df422 --- /dev/null +++ b/trackersservicestandalone/build.gradle @@ -0,0 +1,43 @@ +plugins { + id 'com.android.library' + id 'org.jetbrains.kotlin.android' +} + +android { + compileSdk 32 + + defaultConfig { + minSdk 26 + targetSdk 32 + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles "consumer-rules.pro" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = '1.8' + } +} + +dependencies { + implementation project(":core") + implementation project(":trackers") + + implementation( + libs.androidx.core.ktx, + libs.bundles.koin, + libs.bundles.pcap4j, + libs.kotlinx.coroutines, + libs.timber, + ) +} diff --git a/trackersservicestandalone/consumer-rules.pro b/trackersservicestandalone/consumer-rules.pro new file mode 100644 index 0000000..e69de29 diff --git a/trackersservicestandalone/proguard-rules.pro b/trackersservicestandalone/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/trackersservicestandalone/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/trackersservicestandalone/src/main/AndroidManifest.xml b/trackersservicestandalone/src/main/AndroidManifest.xml new file mode 100644 index 0000000..4bfa4eb --- /dev/null +++ b/trackersservicestandalone/src/main/AndroidManifest.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/trackersservicestandalone/src/main/java/foundation/e/advancedprivacy/trackers/service/Config.kt b/trackersservicestandalone/src/main/java/foundation/e/advancedprivacy/trackers/service/Config.kt new file mode 100644 index 0000000..d079e22 --- /dev/null +++ b/trackersservicestandalone/src/main/java/foundation/e/advancedprivacy/trackers/service/Config.kt @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2023 MURENA SAS + * + * 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.trackers.service + +internal object Config { + const val SESSION_NAME = "TrackersService" + + const val FALLBACK_DNS = "1.1.1.1" + const val VERBOSE = true + + const val VIRTUALDNS_IPV4 = "10.10.10.10" + const val VIRTUALDNS_IPV6 = "fdc8:1095:91e1:aaaa:aaaa:aaaa:aaaa:aaa1" + const val ADDRESS_IPV4 = "10.0.2.15" + const val ADDRESS_IPV6 = "fdc8:1095:91e1:aaaa:aaaa:aaaa:aaaa:aaa2" + + const val BLOCKED_IPV4 = "127.0.0.1" + const val BLOCKED_IPV6 = "::1" + + const val MTU = 3000 + const val LOCAL_RESOLVER_TTL = 60 + + const val MAX_RESOLVER_COUNT = 100 + + val DNS_SERVER_TO_CATCH_IPV4 = listOf( + "8.8.8.8", "8.8.4.4", "1.1.1.1" + ) + val DNS_SERVER_TO_CATCH_IPV6 = listOf( + "2001:4860:4860::8888", "2001:4860:4860::8844" + ) +} diff --git a/trackersservicestandalone/src/main/java/foundation/e/advancedprivacy/trackers/service/TrackersService.kt b/trackersservicestandalone/src/main/java/foundation/e/advancedprivacy/trackers/service/TrackersService.kt new file mode 100644 index 0000000..918977f --- /dev/null +++ b/trackersservicestandalone/src/main/java/foundation/e/advancedprivacy/trackers/service/TrackersService.kt @@ -0,0 +1,127 @@ +/* + * Copyright (C) 2023 MURENA SAS + * + * 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.trackers.service + +import android.content.Context +import android.content.Intent +import android.net.VpnService +import android.os.Build +import android.os.ParcelFileDescriptor +import foundation.e.advancedprivacy.core.utils.notificationBuilder +import foundation.e.advancedprivacy.domain.entities.FeatureServiceState +import foundation.e.advancedprivacy.domain.entities.NOTIFICATION_TRACKER_FLAG +import foundation.e.advancedprivacy.domain.entities.NotificationContent +import foundation.e.advancedprivacy.trackers.domain.externalinterfaces.TrackersServiceSupervisor +import foundation.e.advancedprivacy.trackers.service.Config.DNS_SERVER_TO_CATCH_IPV4 +import foundation.e.advancedprivacy.trackers.service.Config.DNS_SERVER_TO_CATCH_IPV6 +import foundation.e.advancedprivacy.trackers.service.Config.SESSION_NAME +import foundation.e.advancedprivacy.trackers.service.data.NetworkDNSAddressRepository +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import org.koin.core.qualifier.named +import org.koin.java.KoinJavaComponent.get +import timber.log.Timber + +class TrackersService : VpnService() { + companion object { + var coroutineScope = CoroutineScope(Dispatchers.IO) + + fun start(context: Context) { + prepare(context) + val intent = Intent(context, TrackersService::class.java) + context.startService(intent) + } + } + + private val networkDNSAddressRepository: NetworkDNSAddressRepository = get(NetworkDNSAddressRepository::class.java) + private val trackersServiceSupervisor: TrackersServiceSupervisorImpl = get( + TrackersServiceSupervisor::class.java + ) as TrackersServiceSupervisorImpl + + private val notificationTrackerFlag: NotificationContent = get(NotificationContent::class.java, named("notificationTrackerFlag")) + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + startVPN() + + startForeground( + NOTIFICATION_TRACKER_FLAG, + notificationBuilder( + context = this, + content = notificationTrackerFlag + ).build() + ) + trackersServiceSupervisor.state.value = FeatureServiceState.ON + + return START_STICKY + } + + override fun onDestroy() { + networkDNSAddressRepository.stop() + trackersServiceSupervisor.state.value = FeatureServiceState.OFF + super.onDestroy() + } + + private fun startVPN() { + val vpnInterface = initVPN() + + if (vpnInterface != null) { + networkDNSAddressRepository.start() + + coroutineScope = CoroutineScope(Dispatchers.IO) + get(TunLooper::class.java).apply { + listenJob(vpnInterface, coroutineScope) + } + } else { + Timber.e("Cannot get VPN interface") + } + } + + private fun initVPN(): ParcelFileDescriptor? { + val builder = Builder() + builder.setSession(SESSION_NAME) + // IPV4: + builder + .addAddress(Config.ADDRESS_IPV4, 24) + .addDnsServer(Config.VIRTUALDNS_IPV4) + .addRoute(Config.VIRTUALDNS_IPV4, 32) + + // IPV6 + builder + .addAddress(Config.ADDRESS_IPV6, 48) + .addDnsServer(Config.VIRTUALDNS_IPV6) + .addRoute(Config.VIRTUALDNS_IPV6, 128) + + DNS_SERVER_TO_CATCH_IPV4.forEach { + builder.addRoute(it, 32) + } + DNS_SERVER_TO_CATCH_IPV6.forEach { + builder.addRoute(it, 128) + } + + // TODO: block private DNS. + // TODO 20230821: seen in privateDNSFilter, bypass filter for google apps on Android 7/8 + + builder.addDisallowedApplication(packageName) + builder.setBlocking(true) + builder.setMtu(Config.MTU) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + builder.setMetered(false) // take over defaults from underlying network + } + + return builder.establish() + } +} 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 new file mode 100644 index 0000000..25d3e2d --- /dev/null +++ b/trackersservicestandalone/src/main/java/foundation/e/advancedprivacy/trackers/service/TrackersServiceSupervisorImpl.kt @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2023 MURENA SAS + * + * 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.trackers.service + +import android.content.Context +import android.content.Intent +import foundation.e.advancedprivacy.domain.entities.FeatureServiceState +import foundation.e.advancedprivacy.trackers.domain.externalinterfaces.TrackersServiceSupervisor +import foundation.e.advancedprivacy.trackers.service.data.NetworkDNSAddressRepository +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.singleOf +import org.koin.dsl.module + +class TrackersServiceSupervisorImpl(private val context: Context) : TrackersServiceSupervisor { + internal val state: MutableStateFlow = MutableStateFlow(FeatureServiceState.OFF) + + override fun start(): Boolean { + return if (!isRunning()) { + state.value = FeatureServiceState.STARTING + TrackersService.start(context) + true + } else false + } + + override fun stop(): Boolean { + return when (state.value) { + FeatureServiceState.ON -> { + state.value = FeatureServiceState.STOPPING + kotlin.runCatching { TrackersService.coroutineScope.cancel() } + context.stopService(Intent(context, TrackersService::class.java)) + true + } + else -> false + } + } + + override fun isRunning(): Boolean { + return state.value != FeatureServiceState.OFF + } +} + +val trackerServiceModule = module { + singleOf(::NetworkDNSAddressRepository) + singleOf(::RequestDNSRepository) + singleOf(::ResolveDNSUseCase) + singleOf(::TunLooper) +} diff --git a/trackersservicestandalone/src/main/java/foundation/e/advancedprivacy/trackers/service/TunLooper.kt b/trackersservicestandalone/src/main/java/foundation/e/advancedprivacy/trackers/service/TunLooper.kt new file mode 100644 index 0000000..7813c67 --- /dev/null +++ b/trackersservicestandalone/src/main/java/foundation/e/advancedprivacy/trackers/service/TunLooper.kt @@ -0,0 +1,167 @@ +/* + * Copyright (C) 2023 MURENA SAS + * + * 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.trackers.service + +import android.os.ParcelFileDescriptor +import foundation.e.advancedprivacy.trackers.service.usecases.ResolveDNSUseCase +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import org.pcap4j.packet.DnsPacket +import org.pcap4j.packet.IpPacket +import org.pcap4j.packet.IpSelector +import org.pcap4j.packet.IpV4Packet +import org.pcap4j.packet.IpV6Packet +import org.pcap4j.packet.UdpPacket +import org.pcap4j.packet.namednumber.IpNumber +import org.pcap4j.packet.namednumber.UdpPort +import timber.log.Timber +import java.io.DataOutputStream +import java.io.FileInputStream +import java.io.FileOutputStream +import java.io.IOException +import java.net.Inet6Address +import java.util.Arrays + +class TunLooper( + private val resolveDNSUseCase: ResolveDNSUseCase, +) { + private var vpnInterface: ParcelFileDescriptor? = null + private var fileInputStream: FileInputStream? = null + private var dataOutputStream: DataOutputStream? = null + + private fun closeStreams() { + fileInputStream?.close() + fileInputStream = null + + dataOutputStream?.close() + dataOutputStream = null + + vpnInterface?.close() + vpnInterface = null + } + + fun listenJob( + vpnInterface: ParcelFileDescriptor, + scope: CoroutineScope + ): Job = scope.launch(Dispatchers.IO) { + this@TunLooper.vpnInterface = vpnInterface + val fis = FileInputStream(vpnInterface.fileDescriptor) + this@TunLooper.fileInputStream = fis + dataOutputStream = DataOutputStream(FileOutputStream(vpnInterface.fileDescriptor)) + + while (isActive) { + runCatching { + val buffer = ByteArray(Config.MTU) + val pLen = fis.read(buffer) + + if (pLen > 0) { + scope.launch { handleIpPacket(buffer, pLen) } + } + }.onFailure { + if (it is CancellationException) { + closeStreams() + throw it + } else { + Timber.w(it, "while reading from VPN fd") + } + } + } + } + + private suspend fun handleIpPacket(buffer: ByteArray, pLen: Int) { + val pdata = Arrays.copyOf(buffer, pLen) + try { + val packet = IpSelector.newPacket(pdata, 0, pdata.size) + if (packet is IpPacket) { + val ipPacket = packet + if (isPacketDNS(ipPacket)) { + handleDnsPacket(ipPacket) + } + } + } catch (e: Exception) { + Timber.w(e, "Can't parse packet, ignore it.") + } + } + + private fun isPacketDNS(p: IpPacket): Boolean { + if (p.header.protocol === IpNumber.UDP) { + val up = p.payload as UdpPacket + return up.header.dstPort === UdpPort.DOMAIN + } + return false + } + + private suspend fun handleDnsPacket(ipPacket: IpPacket) { + try { + val udpPacket = ipPacket.payload as UdpPacket + val dnsRequest = udpPacket.payload as DnsPacket + val dnsResponse = resolveDNSUseCase.processDNS(dnsRequest) + + if (dnsResponse != null) { + val dnsBuilder = dnsResponse.builder + + val udpBuilder = UdpPacket.Builder(udpPacket) + .srcPort(udpPacket.header.dstPort) + .dstPort(udpPacket.header.srcPort) + .srcAddr(ipPacket.getHeader().getDstAddr()) + .dstAddr(ipPacket.getHeader().getSrcAddr()) + .correctChecksumAtBuild(true) + .correctLengthAtBuild(true) + .payloadBuilder(dnsBuilder) + + val respPacket: IpPacket? = if (ipPacket is IpV4Packet) { + val ipV4Packet = ipPacket + val ipv4Builder = IpV4Packet.Builder() + ipv4Builder + .version(ipV4Packet.header.version) + .protocol(ipV4Packet.header.protocol) + .tos(ipV4Packet.header.tos) + .srcAddr(ipV4Packet.header.dstAddr) + .dstAddr(ipV4Packet.header.srcAddr) + .correctChecksumAtBuild(true) + .correctLengthAtBuild(true) + .dontFragmentFlag(ipV4Packet.header.dontFragmentFlag) + .reservedFlag(ipV4Packet.header.reservedFlag) + .moreFragmentFlag(ipV4Packet.header.moreFragmentFlag) + .ttl(Integer.valueOf(64).toByte()) + .payloadBuilder(udpBuilder) + ipv4Builder.build() + } else if (ipPacket is IpV6Packet) { + IpV6Packet.Builder(ipPacket as IpV6Packet?) + .srcAddr(ipPacket.getHeader().getDstAddr() as Inet6Address) + .dstAddr(ipPacket.getHeader().getSrcAddr() as Inet6Address) + .payloadBuilder(udpBuilder) + .build() + } else null + + respPacket?.let { + try { + dataOutputStream?.write(it.rawData) + } catch (e: IOException) { + Timber.e(e, "error writing to VPN fd") + } + } + } + } catch (ioe: java.lang.Exception) { + Timber.e(ioe, "could not parse DNS packet") + } + } +} diff --git a/trackersservicestandalone/src/main/java/foundation/e/advancedprivacy/trackers/service/data/NetworkDNSAddressRepository.kt b/trackersservicestandalone/src/main/java/foundation/e/advancedprivacy/trackers/service/data/NetworkDNSAddressRepository.kt new file mode 100644 index 0000000..7c36ed2 --- /dev/null +++ b/trackersservicestandalone/src/main/java/foundation/e/advancedprivacy/trackers/service/data/NetworkDNSAddressRepository.kt @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2023 MURENA SAS + * + * 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.trackers.service.data + +import android.content.Context +import android.net.ConnectivityManager +import android.net.Network +import android.net.NetworkRequest +import foundation.e.advancedprivacy.trackers.service.Config +import java.net.InetAddress + +class NetworkDNSAddressRepository(private val context: Context) { + private val connectivityManager: ConnectivityManager = + context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + + fun start() { + connectivityManager.registerNetworkCallback( + NetworkRequest.Builder().build(), + networkCallback + ) + } + + fun stop() { + kotlin.runCatching { + connectivityManager.unregisterNetworkCallback(networkCallback) + } + } + + var dnsAddress: InetAddress = InetAddress.getByName(Config.FALLBACK_DNS) + private set + + private val networkCallback = object : ConnectivityManager.NetworkCallback() { + override fun onAvailable(network: Network) { + super.onAvailable(network) + connectivityManager.getLinkProperties(network) + ?.dnsServers?.firstOrNull { + it.hostAddress.let { + it != Config.VIRTUALDNS_IPV4 && it != Config.VIRTUALDNS_IPV6 + } + }?.let { + dnsAddress = InetAddress.getByName(it.hostAddress) + } + } + } +} diff --git a/trackersservicestandalone/src/main/java/foundation/e/advancedprivacy/trackers/service/data/RequestDNSRepository.kt b/trackersservicestandalone/src/main/java/foundation/e/advancedprivacy/trackers/service/data/RequestDNSRepository.kt new file mode 100644 index 0000000..d9370be --- /dev/null +++ b/trackersservicestandalone/src/main/java/foundation/e/advancedprivacy/trackers/service/data/RequestDNSRepository.kt @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2023 MURENA SAS + * + * 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.trackers.service.data + +import foundation.e.advancedprivacy.core.utils.runSuspendCatching +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.pcap4j.packet.DnsPacket +import timber.log.Timber +import java.net.DatagramPacket +import java.net.DatagramSocket + +class RequestDNSRepository { + + suspend fun processDNS(request: DatagramPacket): DnsPacket? = withContext(Dispatchers.IO) { + runSuspendCatching { + var response: DnsPacket? = null + val datagramSocket = DatagramSocket() + datagramSocket.send(request) + + // Await response from DNS server + val buf = ByteArray(1024) + val packet = DatagramPacket(buf, buf.size) + datagramSocket.receive(packet) + val dnsResp = packet.data + if (dnsResp != null) { + response = DnsPacket.newPacket(dnsResp, 0, dnsResp.size) + } + response + }.onFailure { + Timber.w(it, "Can't make DNS request.") + }.getOrNull() + } +} 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 new file mode 100644 index 0000000..ac8aee0 --- /dev/null +++ b/trackersservicestandalone/src/main/java/foundation/e/advancedprivacy/trackers/service/usecases/ResolveDNSUseCase.kt @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2023 MURENA SAS + * + * 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.trackers.service.usecases + +import foundation.e.advancedprivacy.trackers.domain.usecases.FilterHostnameUseCase +import foundation.e.advancedprivacy.trackers.service.data.NetworkDNSAddressRepository +import foundation.e.advancedprivacy.trackers.service.data.RequestDNSRepository +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.GlobalScope +import org.pcap4j.packet.DnsPacket +import org.pcap4j.packet.namednumber.DnsRCode +import java.net.DatagramPacket + +@OptIn(DelicateCoroutinesApi::class) +class ResolveDNSUseCase( + private val networkDNSAddressRepository: NetworkDNSAddressRepository, + private val filterHostnameUseCase: FilterHostnameUseCase, + private val requestDNSRepository: RequestDNSRepository, + private val scope: CoroutineScope = GlobalScope +) { + private val DNS_PORT = 53 + + init { + filterHostnameUseCase.writeLogJob(scope) + } + + 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 payload = dnsRequest.rawData + val packet = DatagramPacket(payload, payload.size, networkDNSAddressRepository.dnsAddress, DNS_PORT) + return requestDNSRepository.processDNS(packet) + } +} -- cgit v1.2.1