diff --git a/.gitignore b/.gitignore index 09d358cd8a361f093527dcd2e4cea0facd30bffa..7d06206273ba6892b956a3676d74abd052e129cd 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 26d33521af10bcc7fd8cea344038eaaeb78d0ef5..0000000000000000000000000000000000000000 --- 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 5f549977a4944693264276ba3c22bfca11d87ea9..0000000000000000000000000000000000000000 --- 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 97c10636cec5769dba39114874c9ea1a334c76b5..0000000000000000000000000000000000000000 --- 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 0f7bc519db610a2e6290d61d592e9fdac90c8cde..0000000000000000000000000000000000000000 --- 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 0c0ee9579c9773827c14bb0998f39ee27eeb7cbf..0000000000000000000000000000000000000000 --- 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 e9f8efd4e3ec61461c34d557eaa07eb86256cb4b..0000000000000000000000000000000000000000 --- 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 97626ba45445dc9f3afa66e6a149914dc39e3df6..0000000000000000000000000000000000000000 --- 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 7d04a74be832bef488c654b23ba0f59b41eabdd2..0000000000000000000000000000000000000000 --- 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 64580d1432035d6dfc7ddfe1a9813932a9b4fb9e..0000000000000000000000000000000000000000 --- 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 755d57a7de4491567957ecdd894cbbedd5d56db4..0000000000000000000000000000000000000000 --- 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 94a25f7f4cb416c083d265558da75d457237d671..0000000000000000000000000000000000000000 --- 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 ef54d456a756585cc6207eddb535458a3c8738be..216b81afe368aaa1b3fc2a1a349cd230cc434721 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 0af2a0e9e96921a511e9473a37db38c92bbb92fc..71fef00595991b21a3ffc634f8204ad7b09b5fd8 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 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/app/src/main/java/foundation/e/advancedprivacy/KoinModule.kt b/app/src/main/java/foundation/e/advancedprivacy/KoinModule.kt index 3fbb6360b6d0769a2ad2891137de1a6f3ab6edb8..20cefd50e026f1038b3e832d2e6b9fb2a3296545 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 cd85e9a6dc628a9d029d17a2a0b71315c3a1d9d6..455b1a783a237f6646b682f57abd5347867cc8e9 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 0000000000000000000000000000000000000000..ecb24cede8676c0990f6d55d5cb800920a3fed2c --- /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 80fc76016cf0471862a561c632f8d966f6382388..589aa74f4df013ef8bb0b75f3753f078526ba1d4 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 30c8e6ba393b229e67ab0262c8781389937193bc..983ba714e3cd9b500b7ef973211744367af68b36 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 27e7fe40236ff03b12bca1ac59aeec77f313d8c4..9c893290d24e1f7a41e00874c06af8130a7d4b08 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 ed15a4148e8c53905d80438a60a77834afe5d48f..9b79dcc121478e71f9bbcca25458fd79e1c89002 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 b0c9f39114c638101eef35a10ac2fd8646a36b9b..3d6ade0f212e876d53e8ce229cb85727afa93286 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 fac1f753f5b765a76d0bdf816f58cd1ba281db56..ba3ba03e79daf32a26957323fb145442d0241355 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 a57ee217038642a3a9864c45c99cdad110c9a9ae..cd192ee3e40931a454a2b3ad6b5490915e887af7 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 0000000000000000000000000000000000000000..29721b0bace329b5112b8f4ec64608ef1ba3705d --- /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 0000000000000000000000000000000000000000..6bfecbb928e750d38c419a6aad30dfa55748db90 --- /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 0000000000000000000000000000000000000000..4458e1df7b9c8745ad36bc0dae23993c60a88221 --- /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 0000000000000000000000000000000000000000..44b508d0d1e381379cb95b15b11f1ed4dfad6284 --- /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/PermissionsPrivacyModuleBase.kt similarity index 98% rename from core/src/main/java/foundation/e/advancedprivacy/externalinterfaces/permissions/APermissionsPrivacyModule.kt rename to core/src/main/java/foundation/e/advancedprivacy/externalinterfaces/permissions/PermissionsPrivacyModuleBase.kt index 78f424b4ba6b1532df803d575bfe6e5c7fe35cd9..27ba17f55617f76ec5bafff7858448f2244821c7 100644 --- a/core/src/main/java/foundation/e/advancedprivacy/externalinterfaces/permissions/APermissionsPrivacyModule.kt +++ b/core/src/main/java/foundation/e/advancedprivacy/externalinterfaces/permissions/PermissionsPrivacyModuleBase.kt @@ -37,7 +37,7 @@ import foundation.e.advancedprivacy.domain.entities.ProfileType * versions of the module. * @param context an Android context, to retrieve packageManager for example. */ -abstract class APermissionsPrivacyModule(protected val context: Context) : IPermissionsPrivacyModule { +abstract class PermissionsPrivacyModuleBase(protected val context: Context) : IPermissionsPrivacyModule { companion object { private const val TAG = "PermissionsModule" 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 f2e10a441c1f8bbd840f32da93090963aeef544a..c105ceb0d1e948c33a508d5c2520fd7f35c7a437 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 b9925d41dde5f01578fb08e6f987605248b06580..00bf753e7559800419f49148c193ed4d4bdc32fa 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/PermissionsPrivacyModuleImpl.kt similarity index 98% rename from permissionse/src/main/java/foundation/e/advancedprivacy/externalinterfaces/permissions/PermissionsPrivacyModule.kt rename to permissionse/src/main/java/foundation/e/advancedprivacy/externalinterfaces/permissions/PermissionsPrivacyModuleImpl.kt index 59a20dd724bc24bb28313fb963b9434d576321ea..0d32bce6ed526c7ad8fa8018fb27f2a0a73a399b 100644 --- a/permissionse/src/main/java/foundation/e/advancedprivacy/externalinterfaces/permissions/PermissionsPrivacyModule.kt +++ b/permissionse/src/main/java/foundation/e/advancedprivacy/externalinterfaces/permissions/PermissionsPrivacyModuleImpl.kt @@ -39,12 +39,12 @@ 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 +import foundation.e.advancedprivacy.externalinterfaces.permissions.PermissionsPrivacyModuleBase /** * Implements [IPermissionsPrivacyModule] with all privileges of a system app. */ -class PermissionsPrivacyModule(context: Context) : APermissionsPrivacyModule(context) { +class PermissionsPrivacyModuleImpl(context: Context) : PermissionsPrivacyModuleBase(context) { private val appOpsManager: AppOpsManager get() = context.getSystemService(Context.APP_OPS_SERVICE) as AppOpsManager diff --git a/permissionsstandalone/src/main/java/foundation/e/advancedprivacy/permissions/externalinterfaces/PermissionsPrivacyModule.kt b/permissionsstandalone/src/main/java/foundation/e/advancedprivacy/permissions/externalinterfaces/PermissionsPrivacyModuleImpl.kt similarity index 95% rename from permissionsstandalone/src/main/java/foundation/e/advancedprivacy/permissions/externalinterfaces/PermissionsPrivacyModule.kt rename to permissionsstandalone/src/main/java/foundation/e/advancedprivacy/permissions/externalinterfaces/PermissionsPrivacyModuleImpl.kt index 95f5ff098a788df01ce902a1e27be22625d581ab..d31bdf441f2f3e135afcb5ce5fb4cbe4ab68ce8f 100644 --- a/permissionsstandalone/src/main/java/foundation/e/advancedprivacy/permissions/externalinterfaces/PermissionsPrivacyModule.kt +++ b/permissionsstandalone/src/main/java/foundation/e/advancedprivacy/permissions/externalinterfaces/PermissionsPrivacyModuleImpl.kt @@ -24,12 +24,12 @@ 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 +import foundation.e.advancedprivacy.externalinterfaces.permissions.PermissionsPrivacyModuleBase /** * Implements [IPermissionsPrivacyModule] using only API authorized on the PlayStore. */ -class PermissionsPrivacyModule(context: Context) : APermissionsPrivacyModule(context) { +class PermissionsPrivacyModuleImpl(context: Context) : PermissionsPrivacyModuleBase(context) { override fun getApplications( filter: ((PackageInfo) -> Boolean)? ): List { diff --git a/settings.gradle b/settings.gradle index 0b4940e5b0e7cceed2cdc65a2f95c37f75884758..39e58c8d881a885a74cd33f8e24395213d42188f 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 615d310657480dbb4d898586a455733f58e9bc1f..b98070636d81833a33b8e422a87483a366ca47b2 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 0cfb69cf050a3307ffe7a0d0d3be67fbda9a3bf8..34b4e7a863f6ae85a931acaa15efc06a1b1b5d5b 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 c2c0768ee7cfeb433fa99d90cf127f3485cd874b..64477b7eabd50b360d6cca4d3a43132caff3d321 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 6aa76cfd2151f2b3f992718bfaba553000720156..15ff8137a687bc542e12c1fec53052a83d8a86cd 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 a7d5e49deaea8e016f56bc32b796681ff1b09a99..fc57a8ef8502cd0a26099d875eccbce9a6d7999a 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 0000000000000000000000000000000000000000..d9674fc96764fbe9e952f2da7eaf615fb38bc948 --- /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/FilterHostnameUseCase.kt b/trackers/src/main/java/foundation/e/advancedprivacy/trackers/domain/usecases/FilterHostnameUseCase.kt new file mode 100644 index 0000000000000000000000000000000000000000..e229cab98775cab2393dd009a6f497d5057332db --- /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 55efeb99da82e3f56b18457fcdb2264e0a777f25..e7a84b8fa7ba3d03058b8fc7eddbb5c19a5470ac 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 411b4ab606aaac4f19c3f56b3a8f7b058c265795..0000000000000000000000000000000000000000 --- 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 55da6449a062e5513bd9d08548e2c4b58250421b..fa60431cb7475340b587aeeb9ed37d72f294fd71 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/trackersservicee/.gitignore b/trackersservicee/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..42afabfd2abebf31384ca7797186a27a4b7dbee8 --- /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 0000000000000000000000000000000000000000..f7725bfb40b02f6674d2141a960949da09392613 --- /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 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/trackersservicee/proguard-rules.pro b/trackersservicee/proguard-rules.pro new file mode 100644 index 0000000000000000000000000000000000000000..481bb434814107eb79d7a30b676d344b0df2f8ce --- /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 0000000000000000000000000000000000000000..22904321c585cae229f15f6be0e799d597ae043c --- /dev/null +++ b/trackersservicee/src/main/AndroidManifest.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/trackers/src/main/java/foundation/e/advancedprivacy/trackers/domain/usecases/DNSBlocker.kt b/trackersservicee/src/main/java/foundation/e/advancedprivacy/trackers/service/DNSBlocker.kt similarity index 65% rename from trackers/src/main/java/foundation/e/advancedprivacy/trackers/domain/usecases/DNSBlocker.kt rename to trackersservicee/src/main/java/foundation/e/advancedprivacy/trackers/service/DNSBlocker.kt index fb089103d4183d085da0a4c29eb03a6f55391bf0..6a2b2189bcd2c52ff53194e695fb9ad3087433a3 100644 --- a/trackers/src/main/java/foundation/e/advancedprivacy/trackers/domain/usecases/DNSBlocker.kt +++ b/trackersservicee/src/main/java/foundation/e/advancedprivacy/trackers/service/DNSBlocker.kt @@ -16,17 +16,14 @@ * along with this program. If not, see . */ -package foundation.e.advancedprivacy.trackers.domain.usecases +package foundation.e.advancedprivacy.trackers.service -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 foundation.e.advancedprivacy.trackers.domain.usecases.FilterHostnameUseCase import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -39,21 +36,12 @@ import java.io.InputStreamReader import java.io.PrintWriter class DNSBlocker( - context: Context, - val trackersLogger: TrackersLogger, - private val trackersRepository: TrackersRepository, - private val whitelistRepository: WhitelistRepository + val filterHostnameUseCase: FilterHostnameUseCase ) { 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() { @@ -97,18 +85,9 @@ class DNSBlocker( 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) { + if (filterHostnameUseCase.shouldBlock(domainName, appUid)) { + writer.println("block") + } else { writer.println("pass") } socket.close() @@ -122,22 +101,4 @@ class DNSBlocker( } } } - - 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/services/DNSBlockerService.kt b/trackersservicee/src/main/java/foundation/e/advancedprivacy/trackers/service/TrackersService.kt similarity index 71% rename from trackers/src/main/java/foundation/e/advancedprivacy/trackers/services/DNSBlockerService.kt rename to trackersservicee/src/main/java/foundation/e/advancedprivacy/trackers/service/TrackersService.kt index 25539e18777121008c527c8cbe55578b2740d60e..5f573b0fa9d5d4fa8e3e283870e4e3cd24ead7e6 100644 --- a/trackers/src/main/java/foundation/e/advancedprivacy/trackers/services/DNSBlockerService.kt +++ b/trackersservicee/src/main/java/foundation/e/advancedprivacy/trackers/service/TrackersService.kt @@ -15,27 +15,22 @@ * 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 +package foundation.e.advancedprivacy.trackers.service 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() { +class TrackersService : 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 + var coroutineScope = CoroutineScope(Dispatchers.IO) + } override fun onBind(intent: Intent): IBinder? { throw UnsupportedOperationException("Not yet implemented") @@ -43,9 +38,6 @@ class DNSBlockerService : Service() { 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() } @@ -55,14 +47,12 @@ class DNSBlockerService : Service() { private fun start() { coroutineScope = CoroutineScope(Dispatchers.IO) get(DNSBlocker::class.java).apply { - this@DNSBlockerService.dnsBlocker = this - trackersLogger.writeLogJob(coroutineScope) + filterHostnameUseCase.writeLogJob(coroutineScope) listenJob(coroutineScope) } } private fun stop() { kotlin.runCatching { coroutineScope.cancel() } - dnsBlocker = null } } 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 0000000000000000000000000000000000000000..3903db48cecd5ef23a6bc7b35f4a182b5bc16bc1 --- /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 0000000000000000000000000000000000000000..42afabfd2abebf31384ca7797186a27a4b7dbee8 --- /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 0000000000000000000000000000000000000000..d5df422cef4245ca9145e38086e4bd2eda622056 --- /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 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/trackersservicestandalone/proguard-rules.pro b/trackersservicestandalone/proguard-rules.pro new file mode 100644 index 0000000000000000000000000000000000000000..481bb434814107eb79d7a30b676d344b0df2f8ce --- /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 0000000000000000000000000000000000000000..4bfa4eb0292be2fccbeb6c381b1f1bb879f94982 --- /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 0000000000000000000000000000000000000000..d079e22e5f427e783ab0cadc6faf5c408658d9fb --- /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 0000000000000000000000000000000000000000..918977f677a8ec33b6274dd86993ece2664a77b6 --- /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 0000000000000000000000000000000000000000..25d3e2d46e3cd9fce4f09d6425c89240feeceb8b --- /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 0000000000000000000000000000000000000000..7813c67487563136ab0d083afc09917afca5dc06 --- /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 0000000000000000000000000000000000000000..7c36ed2e4170432e5a03f8b2a82f1f94b791d077 --- /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 0000000000000000000000000000000000000000..d9370be529b432acfc850a7c53c37765d87b69f6 --- /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 0000000000000000000000000000000000000000..ac8aee03fb9752d7438a1372f59623a098f84e2b --- /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) + } +}