diff options
Diffstat (limited to 'app/src/main/java')
14 files changed, 282 insertions, 99 deletions
diff --git a/app/src/main/java/foundation/e/privacycentralapp/DependencyContainer.kt b/app/src/main/java/foundation/e/privacycentralapp/DependencyContainer.kt index 76a9539..639e7b4 100644 --- a/app/src/main/java/foundation/e/privacycentralapp/DependencyContainer.kt +++ b/app/src/main/java/foundation/e/privacycentralapp/DependencyContainer.kt @@ -50,8 +50,7 @@ import kotlinx.coroutines.GlobalScope * * TODO: Test if this implementation is leaky. */ -class DependencyContainer constructor(val app: Application) { - +class DependencyContainer(val app: Application) { val context: Context by lazy { app.applicationContext } // Drivers @@ -89,10 +88,10 @@ class DependencyContainer constructor(val app: Application) { private val appListUseCase = AppListUseCase(appListsRepository) private val trackersStatisticsUseCase by lazy { - TrackersStatisticsUseCase(trackTrackersPrivacyModule, appListsRepository, context.resources) + TrackersStatisticsUseCase(trackTrackersPrivacyModule, blockTrackersPrivacyModule, appListsRepository, context.resources) } - private val trackersStateUseCase by lazy { + val trackersStateUseCase by lazy { TrackersStateUseCase(blockTrackersPrivacyModule, trackTrackersPrivacyModule, permissionsModule, localStateRepository, trackersRepository, appListsRepository, GlobalScope) } @@ -119,7 +118,7 @@ class DependencyContainer constructor(val app: Application) { } val trackersViewModelFactory by lazy { - TrackersViewModelFactory(trackersStatisticsUseCase, appListUseCase) + TrackersViewModelFactory(trackersStatisticsUseCase) } val appTrackersViewModelFactory by lazy { @@ -131,5 +130,7 @@ class DependencyContainer constructor(val app: Application) { trackersStateUseCase ipScramblingStateUseCase fakeLocationStateUseCase + + UpdateTrackersWorker.periodicUpdate(context) } } diff --git a/app/src/main/java/foundation/e/privacycentralapp/UpdateTrackersWorker.kt b/app/src/main/java/foundation/e/privacycentralapp/UpdateTrackersWorker.kt new file mode 100644 index 0000000..ba6bae9 --- /dev/null +++ b/app/src/main/java/foundation/e/privacycentralapp/UpdateTrackersWorker.kt @@ -0,0 +1,53 @@ +/* + * 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 <https://www.gnu.org/licenses/>. + */ + +package foundation.e.privacycentralapp + +import android.content.Context +import androidx.work.CoroutineWorker +import androidx.work.ExistingPeriodicWorkPolicy +import androidx.work.PeriodicWorkRequestBuilder +import androidx.work.WorkManager +import androidx.work.WorkerParameters +import java.time.temporal.ChronoUnit + +class UpdateTrackersWorker(appContext: Context, workerParams: WorkerParameters) : + CoroutineWorker(appContext, workerParams) { + + override suspend fun doWork(): Result { + val trackersStateUseCase = (applicationContext as PrivacyCentralApplication) + .dependencyContainer.trackersStateUseCase + + trackersStateUseCase.updateTrackers() + return Result.success() + } + + companion object { + fun periodicUpdate(context: Context) { + val request = PeriodicWorkRequestBuilder<UpdateTrackersWorker>( + ChronoUnit.WEEKS.duration, + ChronoUnit.DAYS.duration + ).build() + + WorkManager.getInstance(context).enqueueUniquePeriodicWork( + UpdateTrackersWorker::class.qualifiedName ?: "", + ExistingPeriodicWorkPolicy.REPLACE, + request + ) + } + } +} diff --git a/app/src/main/java/foundation/e/privacycentralapp/common/AppsAdapter.kt b/app/src/main/java/foundation/e/privacycentralapp/common/AppsAdapter.kt index d66ce76..07cf125 100644 --- a/app/src/main/java/foundation/e/privacycentralapp/common/AppsAdapter.kt +++ b/app/src/main/java/foundation/e/privacycentralapp/common/AppsAdapter.kt @@ -24,7 +24,7 @@ import android.widget.ImageView import android.widget.TextView import androidx.recyclerview.widget.RecyclerView import foundation.e.privacycentralapp.R -import foundation.e.privacymodules.permissions.data.ApplicationDescription +import foundation.e.privacycentralapp.domain.entities.AppWithCounts class AppsAdapter( private val itemsLayout: Int, @@ -34,15 +34,20 @@ class AppsAdapter( class ViewHolder(view: View) : RecyclerView.ViewHolder(view) { val appName: TextView = view.findViewById(R.id.title) - - fun bind(item: ApplicationDescription) { + val counts: TextView = view.findViewById(R.id.counts) + val icon: ImageView = view.findViewById(R.id.icon) + fun bind(item: AppWithCounts) { appName.text = item.label - - itemView.findViewById<ImageView>(R.id.icon).setImageDrawable(item.icon) + counts.text = itemView.context.getString( + R.string.trackers_app_trackers_counts, + item.blockedTrackersCount, + item.trackersCount + ) + icon.setImageDrawable(item.icon) } } - var dataSet: List<ApplicationDescription> = emptyList() + var dataSet: List<AppWithCounts> = emptyList() set(value) { field = value notifyDataSetChanged() diff --git a/app/src/main/java/foundation/e/privacycentralapp/data/repositories/AppListsRepository.kt b/app/src/main/java/foundation/e/privacycentralapp/data/repositories/AppListsRepository.kt index 4718923..3573d4f 100644 --- a/app/src/main/java/foundation/e/privacycentralapp/data/repositories/AppListsRepository.kt +++ b/app/src/main/java/foundation/e/privacycentralapp/data/repositories/AppListsRepository.kt @@ -21,6 +21,7 @@ import android.Manifest import android.content.Context import android.content.Intent import android.content.pm.ApplicationInfo +import android.content.pm.PackageManager import foundation.e.privacymodules.permissions.PermissionsPrivacyModule import foundation.e.privacymodules.permissions.data.ApplicationDescription import kotlinx.coroutines.CoroutineScope @@ -40,8 +41,10 @@ class AppListsRepository( coroutineScope.launch { val (visible, hidden) = splitVisibleToHidden(getAppsUsingInternet()) appDescriptions.emit( - visible.map { it.toApplicationDescription(withIcon = true) } - to hidden.map { it.toApplicationDescription() } + Pair( + visible.map { permissionsModule.buildApplicationDescription(it, withIcon = true) }, + hidden.map { permissionsModule.buildApplicationDescription(it, withIcon = false) }, + ) ) } return appDescriptions.map { it.first.sortedBy { app -> app.label.toString().lowercase() } } @@ -50,18 +53,29 @@ class AppListsRepository( return appDescriptions.value.second } + fun foldForHiddenSystemApp(appUid: Int, appValueGetter: (Int) -> Int): Int { + return if (appUid == dummySystemApp.uid) { + getHiddenSystemApps().fold(0) { acc, app -> + acc + appValueGetter(app.uid) + } + } else appValueGetter(appUid) + } + private val pm get() = context.packageManager private val appDescriptions = MutableStateFlow( - emptyList<ApplicationDescription>() to emptyList<ApplicationDescription>() + Pair( + emptyList<ApplicationDescription>(), + emptyList<ApplicationDescription>() + ) ) private fun getAppsUsingInternet(): List<ApplicationInfo> { - return pm.getInstalledApplications(0) - .filter { - permissionsModule.getPermissions(it.packageName) - .contains(Manifest.permission.INTERNET) - } + return pm.getInstalledPackages(PackageManager.GET_PERMISSIONS).filter { + it.requestedPermissions?.contains(Manifest.permission.INTERNET) == true + }.map { + it.applicationInfo + } } private fun isNotHiddenSystemApp(app: ApplicationInfo, launcherApps: List<String>): Boolean { @@ -94,11 +108,4 @@ class AppListsRepository( acc } } - - private fun ApplicationInfo.toApplicationDescription(withIcon: Boolean = true) = ApplicationDescription( - packageName = packageName, - uid = uid, - label = pm.getApplicationLabel(this), - icon = if (withIcon) pm.getApplicationIcon(packageName) else null - ) } diff --git a/app/src/main/java/foundation/e/privacycentralapp/data/repositories/TrackersRepository.kt b/app/src/main/java/foundation/e/privacycentralapp/data/repositories/TrackersRepository.kt index c7efa84..8216a19 100644 --- a/app/src/main/java/foundation/e/privacycentralapp/data/repositories/TrackersRepository.kt +++ b/app/src/main/java/foundation/e/privacycentralapp/data/repositories/TrackersRepository.kt @@ -20,13 +20,11 @@ package foundation.e.privacycentralapp.data.repositories import android.content.Context import android.util.Log import com.google.gson.Gson -import com.google.gson.annotations.SerializedName import foundation.e.privacymodules.trackers.Tracker import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory import retrofit2.http.GET import java.io.InputStreamReader -import java.lang.Exception class TrackersRepository(private val context: Context) { @@ -37,6 +35,12 @@ class TrackersRepository(private val context: Context) { initFromAssets() } + suspend fun update() { + val api = ETrackersApi.build() + val response = api.trackers() + trackers = mapper(response) + } + private fun initFromAssets() { try { val reader = InputStreamReader(context.getAssets().open("e_trackers.json"), "UTF-8") @@ -65,8 +69,7 @@ class TrackersRepository(private val context: Context) { id = id!!, hostnames = hostnames!!.toSet(), label = name!!, - description = description, - website = website, + exodusId = exodusId ) } } @@ -75,14 +78,14 @@ interface ETrackersApi { companion object { fun build(): ETrackersApi { val retrofit = Retrofit.Builder() - .baseUrl("TODO") + .baseUrl("https://gitlab.e.foundation/e/apps/tracker-list/-/raw/main/") .addConverterFactory(GsonConverterFactory.create()) .build() return retrofit.create(ETrackersApi::class.java) } } - @GET("TODO") + @GET("list/e_trackers.json") suspend fun trackers(): ETrackersResponse data class ETrackersResponse(val trackers: List<ETracker>) { @@ -90,13 +93,7 @@ interface ETrackersApi { val id: String?, val hostnames: List<String>?, val name: String?, - - val description: String?, - @SerializedName("creation_date") val creationDate: String?, - @SerializedName("code_signature") val codeSignature: String?, - @SerializedName("network_signature") val networkSignature: String?, - val website: String?, - val categories: List<String>?, + val exodusId: String? ) } } diff --git a/app/src/main/java/foundation/e/privacycentralapp/domain/entities/AppWithCounts.kt b/app/src/main/java/foundation/e/privacycentralapp/domain/entities/AppWithCounts.kt new file mode 100644 index 0000000..682dfc8 --- /dev/null +++ b/app/src/main/java/foundation/e/privacycentralapp/domain/entities/AppWithCounts.kt @@ -0,0 +1,50 @@ +/* + * 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 <https://www.gnu.org/licenses/>. + */ + +package foundation.e.privacycentralapp.domain.entities + +import android.graphics.drawable.Drawable +import foundation.e.privacymodules.permissions.data.ApplicationDescription + +data class AppWithCounts( + val packageName: String, + val uid: Int, + var label: CharSequence?, + var icon: Drawable?, + val isWhitelisted: Boolean = false, + val trackersCount: Int = 0, + val whiteListedTrackersCount: Int = 0 +) { + constructor( + app: ApplicationDescription, + isWhitelisted: Boolean, + trackersCount: Int, + whiteListedTrackersCount: Int + ) : + this( + packageName = app.packageName, + uid = app.uid, + label = app.label, + icon = app.icon, + isWhitelisted = isWhitelisted, + trackersCount = trackersCount, + whiteListedTrackersCount = whiteListedTrackersCount + ) + + val blockedTrackersCount get() = if (isWhitelisted) 0 + else trackersCount - whiteListedTrackersCount +} diff --git a/app/src/main/java/foundation/e/privacycentralapp/domain/usecases/TrackersStateUseCase.kt b/app/src/main/java/foundation/e/privacycentralapp/domain/usecases/TrackersStateUseCase.kt index 16a1a82..ecf2e7b 100644 --- a/app/src/main/java/foundation/e/privacycentralapp/domain/usecases/TrackersStateUseCase.kt +++ b/app/src/main/java/foundation/e/privacycentralapp/domain/usecases/TrackersStateUseCase.kt @@ -26,6 +26,7 @@ import foundation.e.privacymodules.trackers.IBlockTrackersPrivacyModule import foundation.e.privacymodules.trackers.ITrackTrackersPrivacyModule import foundation.e.privacymodules.trackers.Tracker import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.collect @@ -38,7 +39,7 @@ class TrackersStateUseCase( private val localStateRepository: LocalStateRepository, private val trackersRepository: TrackersRepository, private val appListsRepository: AppListsRepository, - coroutineScope: CoroutineScope + private val coroutineScope: CoroutineScope ) { private val _areAllTrackersBlocked = MutableStateFlow( @@ -106,4 +107,9 @@ class TrackersStateUseCase( updateAllTrackersBlockedState() } + + fun updateTrackers() = coroutineScope.launch { + trackersRepository.update() + trackersPrivacyModule.start(trackersRepository.trackers, enableNotification = false) + } } diff --git a/app/src/main/java/foundation/e/privacycentralapp/domain/usecases/TrackersStatisticsUseCase.kt b/app/src/main/java/foundation/e/privacycentralapp/domain/usecases/TrackersStatisticsUseCase.kt index 9a8b12a..ad8f565 100644 --- a/app/src/main/java/foundation/e/privacycentralapp/domain/usecases/TrackersStatisticsUseCase.kt +++ b/app/src/main/java/foundation/e/privacycentralapp/domain/usecases/TrackersStatisticsUseCase.kt @@ -20,18 +20,22 @@ package foundation.e.privacycentralapp.domain.usecases import android.content.res.Resources import foundation.e.privacycentralapp.R import foundation.e.privacycentralapp.data.repositories.AppListsRepository +import foundation.e.privacycentralapp.domain.entities.AppWithCounts import foundation.e.privacycentralapp.domain.entities.TrackersPeriodicStatistics +import foundation.e.privacymodules.trackers.IBlockTrackersPrivacyModule import foundation.e.privacymodules.trackers.ITrackTrackersPrivacyModule import foundation.e.privacymodules.trackers.Tracker import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.map import java.time.ZonedDateTime import java.time.format.DateTimeFormatter import java.time.temporal.ChronoUnit class TrackersStatisticsUseCase( private val trackTrackersPrivacyModule: ITrackTrackersPrivacyModule, + private val blockTrackersPrivacyModule: IBlockTrackersPrivacyModule, private val appListsRepository: AppListsRepository, private val resources: Resources ) { @@ -124,4 +128,23 @@ class TrackersStatisticsUseCase( return trackers.sortedBy { it.label.lowercase() } } + + fun getAppsWithCounts(): Flow<List<AppWithCounts>> { + val trackersCounts = trackTrackersPrivacyModule.getTrackersCountByApp() + return appListsRepository.getVisibleApps() + .map { apps -> + apps.map { app -> + AppWithCounts( + app, + blockTrackersPrivacyModule.isWhitelisted(app.uid), + appListsRepository.foldForHiddenSystemApp(app.uid) { + trackersCounts.getOrDefault(it, 0) + }, + appListsRepository.foldForHiddenSystemApp(app.uid) { + blockTrackersPrivacyModule.getWhiteList(it).size + } + ) + } + } + } } diff --git a/app/src/main/java/foundation/e/privacycentralapp/features/trackers/TrackersFeature.kt b/app/src/main/java/foundation/e/privacycentralapp/features/trackers/TrackersFeature.kt index e2eb58d..a606e49 100644 --- a/app/src/main/java/foundation/e/privacycentralapp/features/trackers/TrackersFeature.kt +++ b/app/src/main/java/foundation/e/privacycentralapp/features/trackers/TrackersFeature.kt @@ -22,11 +22,9 @@ import foundation.e.flowmvi.Actor import foundation.e.flowmvi.Reducer import foundation.e.flowmvi.SingleEventProducer import foundation.e.flowmvi.feature.BaseFeature +import foundation.e.privacycentralapp.domain.entities.AppWithCounts import foundation.e.privacycentralapp.domain.entities.TrackersPeriodicStatistics -import foundation.e.privacycentralapp.domain.usecases.AppListUseCase import foundation.e.privacycentralapp.domain.usecases.TrackersStatisticsUseCase -import foundation.e.privacymodules.permissions.data.ApplicationDescription -import foundation.e.privacymodules.trackers.Tracker import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOf @@ -52,13 +50,12 @@ class TrackersFeature( val dayStatistics: TrackersPeriodicStatistics? = null, val monthStatistics: TrackersPeriodicStatistics? = null, val yearStatistics: TrackersPeriodicStatistics? = null, - val apps: List<ApplicationDescription>? = null, - val trackers: List<Tracker> = emptyList() + val apps: List<AppWithCounts>? = null, ) sealed class SingleEvent { data class ErrorEvent(val error: String) : SingleEvent() - data class OpenAppDetailsEvent(val appDesc: ApplicationDescription) : SingleEvent() + data class OpenAppDetailsEvent(val appDesc: AppWithCounts) : SingleEvent() object NewStatisticsAvailableSingleEvent : SingleEvent() } @@ -75,9 +72,9 @@ class TrackersFeature( val yearStatistics: TrackersPeriodicStatistics? = null ) : Effect() data class AvailableAppsListEffect( - val apps: List<ApplicationDescription> + val apps: List<AppWithCounts> ) : Effect() - data class OpenAppDetailsEffect(val appDesc: ApplicationDescription) : Effect() + data class OpenAppDetailsEffect(val appDesc: AppWithCounts) : Effect() object QuickPrivacyDisabledWarningEffect : Effect() data class ErrorEffect(val message: String) : Effect() object NewStatisticsAvailablesEffect : Effect() @@ -87,8 +84,7 @@ class TrackersFeature( fun create( initialState: State = State(), coroutineScope: CoroutineScope, - trackersStatisticsUseCase: TrackersStatisticsUseCase, - appListUseCase: AppListUseCase + trackersStatisticsUseCase: TrackersStatisticsUseCase ) = TrackersFeature( initialState, coroutineScope, reducer = { state, effect -> @@ -106,7 +102,19 @@ class TrackersFeature( }, actor = { state, action -> when (action) { - Action.InitAction -> merge<TrackersFeature.Effect>( + Action.InitAction -> merge<Effect>( + flowOf(Effect.NewStatisticsAvailablesEffect), + trackersStatisticsUseCase.listenUpdates().map { + Effect.NewStatisticsAvailablesEffect + } + ) + + is Action.ClickAppAction -> flowOf( + state.apps?.find { it.packageName == action.packageName }?.let { + Effect.OpenAppDetailsEffect(it) + } ?: run { Effect.ErrorEffect("Can't find back app.") } + ) + is Action.FetchStatistics -> merge<Effect>( flow { trackersStatisticsUseCase.getDayMonthYearStatistics() .let { (day, month, year) -> @@ -114,36 +122,15 @@ class TrackersFeature( Effect.TrackersStatisticsLoadedEffect( dayStatistics = day, monthStatistics = month, - yearStatistics = year + yearStatistics = year, ) ) } }, - appListUseCase.getAppsUsingInternet().map { apps -> - Effect.AvailableAppsListEffect(apps) - }, - trackersStatisticsUseCase.listenUpdates().map { - Effect.NewStatisticsAvailablesEffect + trackersStatisticsUseCase.getAppsWithCounts().map { + Effect.AvailableAppsListEffect(it) } ) - - is Action.ClickAppAction -> flowOf( - state.apps?.find { it.packageName == action.packageName }?.let { - Effect.OpenAppDetailsEffect(it) - } ?: run { Effect.ErrorEffect("Can't find back app.") } - ) - is Action.FetchStatistics -> flow { - trackersStatisticsUseCase.getDayMonthYearStatistics() - .let { (day, month, year) -> - emit( - Effect.TrackersStatisticsLoadedEffect( - dayStatistics = day, - monthStatistics = month, - yearStatistics = year, - ) - ) - } - } } }, singleEventProducer = { _, _, effect -> diff --git a/app/src/main/java/foundation/e/privacycentralapp/features/trackers/TrackersFragment.kt b/app/src/main/java/foundation/e/privacycentralapp/features/trackers/TrackersFragment.kt index 3b22f89..0f686b4 100644 --- a/app/src/main/java/foundation/e/privacycentralapp/features/trackers/TrackersFragment.kt +++ b/app/src/main/java/foundation/e/privacycentralapp/features/trackers/TrackersFragment.kt @@ -18,10 +18,11 @@ package foundation.e.privacycentralapp.features.trackers import android.os.Bundle +import android.util.Log import android.view.View import android.widget.Toast -import androidx.fragment.app.add import androidx.fragment.app.commit +import androidx.fragment.app.replace import androidx.fragment.app.viewModels import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.LinearLayoutManager @@ -70,7 +71,7 @@ class TrackersFragment : } is TrackersFeature.SingleEvent.OpenAppDetailsEvent -> { requireActivity().supportFragmentManager.commit { - add<AppTrackersFragment>(R.id.container, args = AppTrackersFragment.buildArgs(event.appDesc.label.toString(), event.appDesc.packageName)) + replace<AppTrackersFragment>(R.id.container, args = AppTrackersFragment.buildArgs(event.appDesc.label.toString(), event.appDesc.packageName)) setReorderingAllowed(true) addToBackStack("apptrackers") } @@ -113,6 +114,7 @@ class TrackersFragment : } override fun onResume() { + Log.d("TestCounts", "OnResume") super.onResume() viewModel.submitAction(TrackersFeature.Action.FetchStatistics) } diff --git a/app/src/main/java/foundation/e/privacycentralapp/features/trackers/TrackersViewModel.kt b/app/src/main/java/foundation/e/privacycentralapp/features/trackers/TrackersViewModel.kt index e3a97cc..c2a1822 100644 --- a/app/src/main/java/foundation/e/privacycentralapp/features/trackers/TrackersViewModel.kt +++ b/app/src/main/java/foundation/e/privacycentralapp/features/trackers/TrackersViewModel.kt @@ -20,15 +20,13 @@ package foundation.e.privacycentralapp.features.trackers import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import foundation.e.privacycentralapp.common.Factory -import foundation.e.privacycentralapp.domain.usecases.AppListUseCase import foundation.e.privacycentralapp.domain.usecases.TrackersStatisticsUseCase import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.launch class TrackersViewModel( - private val trackersStatisticsUseCase: TrackersStatisticsUseCase, - private val appListUseCase: AppListUseCase + private val trackersStatisticsUseCase: TrackersStatisticsUseCase ) : ViewModel() { private val _actions = MutableSharedFlow<TrackersFeature.Action>() @@ -38,8 +36,7 @@ class TrackersViewModel( TrackersFeature.create( coroutineScope = viewModelScope, - trackersStatisticsUseCase = trackersStatisticsUseCase, - appListUseCase = appListUseCase + trackersStatisticsUseCase = trackersStatisticsUseCase ) } @@ -51,11 +48,10 @@ class TrackersViewModel( } class TrackersViewModelFactory( - private val trackersStatisticsUseCase: TrackersStatisticsUseCase, - private val appListUseCase: AppListUseCase + private val trackersStatisticsUseCase: TrackersStatisticsUseCase ) : Factory<TrackersViewModel> { override fun create(): TrackersViewModel { - return TrackersViewModel(trackersStatisticsUseCase, appListUseCase) + return TrackersViewModel(trackersStatisticsUseCase) } } diff --git a/app/src/main/java/foundation/e/privacycentralapp/features/trackers/apptrackers/AppTrackersFeature.kt b/app/src/main/java/foundation/e/privacycentralapp/features/trackers/apptrackers/AppTrackersFeature.kt index 16cd4a0..790a5a0 100644 --- a/app/src/main/java/foundation/e/privacycentralapp/features/trackers/apptrackers/AppTrackersFeature.kt +++ b/app/src/main/java/foundation/e/privacycentralapp/features/trackers/apptrackers/AppTrackersFeature.kt @@ -17,6 +17,7 @@ package foundation.e.privacycentralapp.features.trackers.apptrackers +import android.net.Uri import android.util.Log import foundation.e.flowmvi.Actor import foundation.e.flowmvi.Reducer @@ -63,17 +64,24 @@ class AppTrackersFeature( return null } } + + fun getTrackersCount() = trackers?.size ?: 0 + fun getBlockedTrackersCount(): Int = if (isBlockingActivated) + getTrackersCount() - (whitelist?.size ?: 0) + else 0 } sealed class SingleEvent { data class ErrorEvent(val error: Any) : SingleEvent() object NewStatisticsAvailableSingleEvent : SingleEvent() + data class OpenUrlEvent(val url: Uri) : SingleEvent() } sealed class Action { data class InitAction(val packageName: String) : Action() data class BlockAllToggleAction(val isBlocked: Boolean) : Action() data class ToggleTrackerAction(val tracker: Tracker, val isBlocked: Boolean) : Action() + data class ClickTracker(val tracker: Tracker) : Action() object FetchStatistics : Action() } @@ -87,9 +95,12 @@ class AppTrackersFeature( object NewStatisticsAvailablesEffect : Effect() data class QuickPrivacyUpdatedEffect(val enabled: Boolean) : Effect() object QuickPrivacyDisabledWarningEffect : Effect() + data class OpenUrlEffect(val url: Uri) : Effect() } companion object { + + private const val exodusBaseUrl = "https://reports.exodus-privacy.eu.org/fr/trackers/" fun create( initialState: State = State(), coroutineScope: CoroutineScope, @@ -179,6 +190,17 @@ class AppTrackersFeature( } ?: run { flowOf(Effect.ErrorEffect("No appDesc.")) } } else flowOf(Effect.NoEffect) } + is Action.ClickTracker -> { + flowOf( + action.tracker.exodusId?.let { + try { + Effect.OpenUrlEffect(Uri.parse(exodusBaseUrl + it)) + } catch (e: Exception) { + Effect.ErrorEffect("Invalid Url") + } + } ?: Effect.NoEffect + ) + } is Action.FetchStatistics -> flowOf( state.appDesc?.uid?.let { Effect.AvailableTrackersListEffect( @@ -196,6 +218,8 @@ class AppTrackersFeature( SingleEvent.ErrorEvent(R.string.apptrackers_error_quickprivacy_disabled) is Effect.NewStatisticsAvailablesEffect -> SingleEvent.NewStatisticsAvailableSingleEvent + is Effect.OpenUrlEffect -> + SingleEvent.OpenUrlEvent(effect.url) else -> null } } diff --git a/app/src/main/java/foundation/e/privacycentralapp/features/trackers/apptrackers/AppTrackersFragment.kt b/app/src/main/java/foundation/e/privacycentralapp/features/trackers/apptrackers/AppTrackersFragment.kt index 440edf7..8e2dc3b 100644 --- a/app/src/main/java/foundation/e/privacycentralapp/features/trackers/apptrackers/AppTrackersFragment.kt +++ b/app/src/main/java/foundation/e/privacycentralapp/features/trackers/apptrackers/AppTrackersFragment.kt @@ -17,6 +17,8 @@ package foundation.e.privacycentralapp.features.trackers.apptrackers +import android.content.ActivityNotFoundException +import android.content.Intent import android.os.Bundle import android.view.View import android.widget.Toast @@ -76,6 +78,12 @@ class AppTrackersFragment : is SingleEvent.NewStatisticsAvailableSingleEvent -> { viewModel.submitAction(Action.FetchStatistics) } + is SingleEvent.OpenUrlEvent -> + try { + startActivity(Intent(Intent.ACTION_VIEW, event.url)) + } catch (e: ActivityNotFoundException) { + displayToast("No application to see webpages") + } } } } @@ -104,14 +112,13 @@ class AppTrackersFragment : binding.trackers.apply { layoutManager = LinearLayoutManager(requireContext()) setHasFixedSize(true) - adapter = ToggleTrackersAdapter(R.layout.apptrackers_item_tracker_toggle) { tracker, isBlocked -> - viewModel.submitAction( - Action.ToggleTrackerAction( - tracker, - isBlocked - ) - ) - } + adapter = ToggleTrackersAdapter( + R.layout.apptrackers_item_tracker_toggle, + onToggleSwitch = { tracker, isBlocked -> + viewModel.submitAction(Action.ToggleTrackerAction(tracker, isBlocked)) + }, + onClickTitle = { viewModel.submitAction(Action.ClickTracker(it)) } + ) } } @@ -121,6 +128,14 @@ class AppTrackersFragment : } override fun render(state: State) { + + binding.trackersCountSummary.text = if (state.getTrackersCount() == 0) "" + else getString( + R.string.apptrackers_trackers_count_summary, + state.getBlockedTrackersCount(), + state.getTrackersCount() + ) + binding.blockAllToggle.isChecked = state.isBlockingActivated binding.trackersListTitle.isVisible = state.isBlockingActivated diff --git a/app/src/main/java/foundation/e/privacycentralapp/features/trackers/apptrackers/ToggleTrackersAdapter.kt b/app/src/main/java/foundation/e/privacycentralapp/features/trackers/apptrackers/ToggleTrackersAdapter.kt index 580a60c..e77b61f 100644 --- a/app/src/main/java/foundation/e/privacycentralapp/features/trackers/apptrackers/ToggleTrackersAdapter.kt +++ b/app/src/main/java/foundation/e/privacycentralapp/features/trackers/apptrackers/ToggleTrackersAdapter.kt @@ -17,11 +17,14 @@ package foundation.e.privacycentralapp.features.trackers.apptrackers +import android.text.SpannableString +import android.text.style.UnderlineSpan import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.Switch import android.widget.TextView +import androidx.core.content.ContextCompat import androidx.core.view.isVisible import androidx.recyclerview.widget.RecyclerView import foundation.e.privacycentralapp.R @@ -29,9 +32,9 @@ import foundation.e.privacymodules.trackers.Tracker class ToggleTrackersAdapter( private val itemsLayout: Int, - private val listener: (Tracker, Boolean) -> Unit -) : - RecyclerView.Adapter<ToggleTrackersAdapter.ViewHolder>() { + private val onToggleSwitch: (Tracker, Boolean) -> Unit, + private val onClickTitle: (Tracker) -> Unit +) : RecyclerView.Adapter<ToggleTrackersAdapter.ViewHolder>() { var isEnabled = true @@ -42,7 +45,17 @@ class ToggleTrackersAdapter( val toggleOverlay: View = view.findViewById(R.id.toggle_clicker) fun bind(item: Pair<Tracker, Boolean>, isEnabled: Boolean) { - title.text = item.first.label + val text = item.first.label + if (item.first.exodusId != null) { + title.setTextColor(ContextCompat.getColor(title.context, R.color.accent)) + val spannable = SpannableString(text) + spannable.setSpan(UnderlineSpan(), 0, spannable.length, 0) + title.text = spannable + } else { + title.setTextColor(ContextCompat.getColor(title.context, R.color.black)) + title.text = text + } + toggle.isChecked = item.second toggle.isEnabled = isEnabled toggleOverlay.isVisible = !isEnabled @@ -62,10 +75,14 @@ class ToggleTrackersAdapter( .inflate(itemsLayout, parent, false) val holder = ViewHolder(view) holder.toggle.setOnClickListener { - listener(dataSet[holder.adapterPosition].first, holder.toggle.isChecked) + onToggleSwitch(dataSet[holder.adapterPosition].first, holder.toggle.isChecked) } holder.toggleOverlay.setOnClickListener { - listener(dataSet[holder.adapterPosition].first, false) + onToggleSwitch(dataSet[holder.adapterPosition].first, false) + } + + holder.title.setOnClickListener { + onClickTitle(dataSet[holder.adapterPosition].first) } return holder |