diff --git a/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/VpnRemoteFeatures.kt b/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/VpnRemoteFeatures.kt index a9479f5d34dd..28c1ce82c793 100644 --- a/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/VpnRemoteFeatures.kt +++ b/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/VpnRemoteFeatures.kt @@ -40,6 +40,7 @@ import kotlinx.coroutines.launch scope = AppScope::class, featureName = "networkProtection", toggleStore = VpnRemoteFeaturesStore::class, + settingsStore = VpnRemoteSettingsStore::class, ) interface VpnRemoteFeatures { @Toggle.DefaultValue(true) diff --git a/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/VpnRemoteSettingsStore.kt b/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/VpnRemoteSettingsStore.kt new file mode 100644 index 000000000000..c3a18d0f5d1f --- /dev/null +++ b/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/VpnRemoteSettingsStore.kt @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2024 DuckDuckGo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.duckduckgo.networkprotection.impl + +import com.duckduckgo.app.di.AppCoroutineScope +import com.duckduckgo.common.utils.DispatcherProvider +import com.duckduckgo.di.scopes.AppScope +import com.duckduckgo.feature.toggles.api.FeatureSettings +import com.duckduckgo.feature.toggles.api.RemoteFeatureStoreNamed +import com.duckduckgo.networkprotection.api.NetworkProtectionState +import com.duckduckgo.networkprotection.store.db.CategorizedSystemApp +import com.duckduckgo.networkprotection.store.db.CategorizedSystemAppsDao +import com.squareup.anvil.annotations.ContributesBinding +import com.squareup.moshi.Json +import com.squareup.moshi.Moshi +import dagger.SingleInstanceIn +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import logcat.LogPriority +import logcat.asLog +import logcat.logcat + +@ContributesBinding(AppScope::class) +@RemoteFeatureStoreNamed(VpnRemoteFeatures::class) +@SingleInstanceIn(AppScope::class) +class VpnRemoteSettingsStore @Inject constructor( + @AppCoroutineScope private val coroutineScope: CoroutineScope, + private val dispatcherProvider: DispatcherProvider, + private val networkProtectionState: NetworkProtectionState, + private val categorizedSystemAppsDao: CategorizedSystemAppsDao, +) : FeatureSettings.Store { + + private val jsonAdapter = Moshi.Builder().build().adapter(SettingsModel::class.java) + + override fun store(jsonString: String) { + logcat { "Received configuration: $jsonString" } + + runCatching { + jsonAdapter.fromJson(jsonString)?.let { model -> + model.systemAppCategories.also { + if (it.isNotEmpty()) { + categorizedSystemAppsDao.upsertSystemAppCategories(it) + } + } + + // Restart VPN now that the lists were updated + coroutineScope.launch(dispatcherProvider.io()) { + networkProtectionState.restart() + } + } + }.onFailure { + logcat(LogPriority.WARN) { it.asLog() } + } + } + + data class SettingsModel( + @field:Json(name = "systemAppCategories") + val systemAppCategories: List, + ) +} diff --git a/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/di/NetworkProtectionModule.kt b/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/di/NetworkProtectionModule.kt index 138d8aeacd4f..982d03cc786e 100644 --- a/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/di/NetworkProtectionModule.kt +++ b/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/di/NetworkProtectionModule.kt @@ -31,6 +31,7 @@ import com.duckduckgo.networkprotection.store.NetworkProtectionPrefs import com.duckduckgo.networkprotection.store.RealNetPExclusionListRepository import com.duckduckgo.networkprotection.store.RealNetPGeoswitchingRepository import com.duckduckgo.networkprotection.store.RealNetworkProtectionPrefs +import com.duckduckgo.networkprotection.store.db.CategorizedSystemAppsDao import com.duckduckgo.networkprotection.store.db.NetPDatabase import com.duckduckgo.networkprotection.store.remote_config.NetPConfigTogglesDao import com.squareup.anvil.annotations.ContributesTo @@ -74,6 +75,14 @@ object DataModule { ): NetPGeoswitchingRepository { return RealNetPGeoswitchingRepository(networkProtectionPrefs, database.geoswitchingDao(), dispatcherProvider) } + + @Provides + @SingleInstanceIn(AppScope::class) + fun provideCategorizedSystemAppsDao( + database: NetPDatabase, + ): CategorizedSystemAppsDao { + return database.categorizedSystemAppsDao() + } } @Module diff --git a/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/exclusion/systemapps/SystemAppsExclusionRepository.kt b/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/exclusion/systemapps/SystemAppsExclusionRepository.kt index 3603e65bcfa1..3c6c0ddec823 100644 --- a/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/exclusion/systemapps/SystemAppsExclusionRepository.kt +++ b/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/exclusion/systemapps/SystemAppsExclusionRepository.kt @@ -32,6 +32,7 @@ import com.duckduckgo.networkprotection.impl.exclusion.systemapps.SystemAppsExcl import com.duckduckgo.networkprotection.impl.exclusion.systemapps.SystemAppsExclusionRepository.SystemAppCategory.Networking import com.duckduckgo.networkprotection.impl.exclusion.systemapps.SystemAppsExclusionRepository.SystemAppCategory.Others import com.duckduckgo.networkprotection.impl.settings.NetPSettingsLocalConfig +import com.duckduckgo.networkprotection.store.db.CategorizedSystemAppsDao import com.squareup.anvil.annotations.ContributesBinding import dagger.SingleInstanceIn import javax.inject.Inject @@ -68,6 +69,7 @@ class RealSystemAppsExclusionRepository @Inject constructor( private val packageManager: PackageManager, private val systemAppOverridesProvider: SystemAppOverridesProvider, private val dispatcherProvider: DispatcherProvider, + private val categorizedSystemAppsDao: CategorizedSystemAppsDao, ) : SystemAppsExclusionRepository { private val preferences: SharedPreferences by lazy { sharedPreferencesProvider.getSharedPreferences( @@ -147,38 +149,16 @@ class RealSystemAppsExclusionRepository @Inject constructor( includeCategory(Others) } - private fun getCommunicationSystemApps(): Set { - return setOf( - "com.android.calllogbackup", - "com.android.cellbroadcastreceiver", - "com.android.mms.service", - "com.android.phone", - "com.android.providers.contacts", - "com.android.providers.telephony", - "com.android.service.ims", - "com.google.android.apps.messaging", - "com.google.android.gms", - "com.google.android.telephony", - "org.codeaurora.ims", - ) + private fun getCommunicationSystemApps(): List { + return categorizedSystemAppsDao.getCommunicationSystemApps().map { it.packageName } } - private fun getNetworkingSystemApps(): Set { - return setOf( - "com.android.bluetooth", - "com.android.nfc", - "com.google.android.networkstack", - "com.google.android.networkstack.tethering", - ) + private fun getNetworkingSystemApps(): List { + return categorizedSystemAppsDao.getNetworkingSystemApps().map { it.packageName } } - private fun getMediaSystemApps(): Set { - return setOf( - "com.android.providers.media", - "com.google.android.providers.media.module", - "com.google.android.music", - "com.google.android.videos", - ) + private fun getMediaSystemApps(): List { + return categorizedSystemAppsDao.getMediaSystemApps().map { it.packageName } } private fun getOtherSystemApps(): Set { diff --git a/network-protection/network-protection-store/src/main/java/com/duckduckgo/networkprotection/store/db/CategorizedSystemAppsDao.kt b/network-protection/network-protection-store/src/main/java/com/duckduckgo/networkprotection/store/db/CategorizedSystemAppsDao.kt new file mode 100644 index 000000000000..7adc0c6dbe8d --- /dev/null +++ b/network-protection/network-protection-store/src/main/java/com/duckduckgo/networkprotection/store/db/CategorizedSystemAppsDao.kt @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2024 DuckDuckGo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.duckduckgo.networkprotection.store.db + +import androidx.room.Dao +import androidx.room.Entity +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.PrimaryKey +import androidx.room.Query +import androidx.room.Transaction + +@Dao +interface CategorizedSystemAppsDao { + @Query("SELECT * from netp_system_apps_categories where category = 'COMMUNICATION'") + fun getCommunicationSystemApps(): List + + @Query("SELECT * from netp_system_apps_categories where category = 'NETWORKING'") + fun getNetworkingSystemApps(): List + + @Query("SELECT * from netp_system_apps_categories where category = 'MEDIA'") + fun getMediaSystemApps(): List + + @Transaction + fun upsertSystemAppCategories( + systemAppCategories: List, + ) { + deleteSystemAppCategories() + insertSystemAppOverrides(systemAppCategories) + } + + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun insertSystemAppOverrides( + systemAppCategories: List, + ) + + @Query("DELETE from netp_system_apps_categories") + fun deleteSystemAppCategories() +} + +@Entity(tableName = "netp_system_apps_categories") +data class CategorizedSystemApp( + @PrimaryKey val packageName: String, + val category: SystemAppCategory, +) + +enum class SystemAppCategory { + COMMUNICATION, + NETWORKING, + MEDIA, + OTHERS, +} diff --git a/network-protection/network-protection-store/src/main/java/com/duckduckgo/networkprotection/store/db/NetPDatabase.kt b/network-protection/network-protection-store/src/main/java/com/duckduckgo/networkprotection/store/db/NetPDatabase.kt index dae759ca1b93..df1d78c5799a 100644 --- a/network-protection/network-protection-store/src/main/java/com/duckduckgo/networkprotection/store/db/NetPDatabase.kt +++ b/network-protection/network-protection-store/src/main/java/com/duckduckgo/networkprotection/store/db/NetPDatabase.kt @@ -21,6 +21,7 @@ import androidx.room.RoomDatabase import androidx.room.TypeConverter import androidx.room.TypeConverters import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase import com.duckduckgo.networkprotection.store.remote_config.NetPConfigToggle import com.duckduckgo.networkprotection.store.remote_config.NetPConfigTogglesDao import com.squareup.moshi.JsonAdapter @@ -29,11 +30,12 @@ import com.squareup.moshi.Types @Database( exportSchema = true, - version = 3, + version = 4, entities = [ NetPManuallyExcludedApp::class, NetPConfigToggle::class, NetPGeoswitchingLocation::class, + CategorizedSystemApp::class, ], ) @TypeConverters(NetpDatabaseConverters::class) @@ -41,10 +43,19 @@ abstract class NetPDatabase : RoomDatabase() { abstract fun exclusionListDao(): NetPExclusionListDao abstract fun configTogglesDao(): NetPConfigTogglesDao abstract fun geoswitchingDao(): NetPGeoswitchingDao + abstract fun categorizedSystemAppsDao(): CategorizedSystemAppsDao companion object { val ALL_MIGRATIONS: List - get() = emptyList() + get() = listOf(MIGRATION_3_4) + private val MIGRATION_3_4: Migration = object : Migration(3, 4) { + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL( + "CREATE TABLE IF NOT EXISTS `netp_system_apps_categories` (`packageName` TEXT NOT NULL," + + " `category` TEXT NOT NULL, PRIMARY KEY(`packageName`))", + ) + } + } } } @@ -64,4 +75,18 @@ object NetpDatabaseConverters { fun fromStringList(value: List): String { return stringListAdapter.toJson(value) } + + @TypeConverter + fun toSystemAppCategory(category: String): SystemAppCategory { + return try { + SystemAppCategory.valueOf(category) + } catch (ex: IllegalArgumentException) { + SystemAppCategory.OTHERS + } + } + + @TypeConverter + fun fromStage(stage: SystemAppCategory): String { + return stage.name + } }