diff --git a/app/build.gradle b/app/build.gradle index ef9310d0b7e279cf12bbdee545de35fb80939643..2de6685eb06207e5e40f68259a49c5dbb2f17664 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -177,6 +177,7 @@ dependencies { libs.androidx.navigation.fragment, libs.androidx.navigation.ui, libs.androidx.viewpager2, + libs.androidx.work.ktx, libs.bundles.koin, diff --git a/app/src/main/java/foundation/e/advancedprivacy/AdvancedPrivacyApplication.kt b/app/src/main/java/foundation/e/advancedprivacy/AdvancedPrivacyApplication.kt index b640c9e7ea8d5a121c21d45ff342c8d016da0c48..f45fdec31a9b83ca11adc4316807102d79e14809 100644 --- a/app/src/main/java/foundation/e/advancedprivacy/AdvancedPrivacyApplication.kt +++ b/app/src/main/java/foundation/e/advancedprivacy/AdvancedPrivacyApplication.kt @@ -28,6 +28,7 @@ import foundation.e.advancedprivacy.domain.usecases.TrackersStateUseCase import foundation.e.advancedprivacy.domain.usecases.TrackersStatisticsUseCase import foundation.e.advancedprivacy.domain.usecases.VpnSupervisorUseCase import foundation.e.advancedprivacy.externalinterfaces.permissions.IPermissionsPrivacyModule +import foundation.e.advancedprivacy.externalinterfaces.workers.WeeklyReportWorker import foundation.e.advancedprivacy.trackers.services.UpdateTrackersWorker import foundation.e.lib.telemetry.Telemetry import kotlinx.coroutines.CoroutineScope @@ -49,6 +50,7 @@ class AdvancedPrivacyApplication : Application() { private fun initBackgroundSingletons() { UpdateTrackersWorker.periodicUpdate(this) + WeeklyReportWorker.scheduleNext(this) WarningDialog.startListening( get(ShowFeaturesWarningUseCase::class.java), diff --git a/app/src/main/java/foundation/e/advancedprivacy/KoinModule.kt b/app/src/main/java/foundation/e/advancedprivacy/KoinModule.kt index a7a3283ef41ebc3dfa935f0ab19746d4beb15656..a51d592451c2757ede422a63688aeef563ef8f18 100644 --- a/app/src/main/java/foundation/e/advancedprivacy/KoinModule.kt +++ b/app/src/main/java/foundation/e/advancedprivacy/KoinModule.kt @@ -25,6 +25,7 @@ import foundation.e.advancedprivacy.core.coreModule import foundation.e.advancedprivacy.data.repositories.AppListRepository import foundation.e.advancedprivacy.data.repositories.LocalStateRepositoryImpl import foundation.e.advancedprivacy.data.repositories.ResourcesRepository +import foundation.e.advancedprivacy.data.repositories.WeeklyReportLocalRepository import foundation.e.advancedprivacy.domain.entities.ApplicationDescription import foundation.e.advancedprivacy.domain.entities.DisplayableApp import foundation.e.advancedprivacy.domain.entities.ProfileType @@ -102,6 +103,13 @@ val appModule = module { LocalStateRepositoryImpl(context = androidContext()) } + single { + WeeklyReportLocalRepository( + context = androidContext(), + json = get() + ) + } + single(named("AdvancedPrivacy")) { ApplicationDescription( packageName = androidContext().packageName, diff --git a/app/src/main/java/foundation/e/advancedprivacy/data/repositories/WeeklyReportLocalRepository.kt b/app/src/main/java/foundation/e/advancedprivacy/data/repositories/WeeklyReportLocalRepository.kt new file mode 100644 index 0000000000000000000000000000000000000000..220a655c51b583f132cfa6466071c255fd46fbb4 --- /dev/null +++ b/app/src/main/java/foundation/e/advancedprivacy/data/repositories/WeeklyReportLocalRepository.kt @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2025 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.data.repositories + +import android.content.Context +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.stringPreferencesKey +import androidx.datastore.preferences.preferencesDataStore +import foundation.e.advancedprivacy.core.utils.getValue +import foundation.e.advancedprivacy.core.utils.removeKey +import foundation.e.advancedprivacy.domain.entities.weeklyreport.WeeklyReport +import kotlinx.serialization.json.Json +import timber.log.Timber + +class WeeklyReportLocalRepository( + context: Context, + private val json: Json +) { + + private val Context.dataStore: DataStore by preferencesDataStore(name = "weeklyreport_datastore") + private val store = context.dataStore + + private val weeklyReportsKey = stringPreferencesKey("weeklyReports") + + suspend fun setLastWeeklyReport(report: WeeklyReport) { + store.updateData { pref -> + val preferences = pref.toMutablePreferences() + val reports = preferences.get(weeklyReportsKey)?.let { + runCatching { json.decodeFromString>(it) }.getOrNull() + } ?: emptyList() + val updatedReport: List = reports + report + preferences.set(weeklyReportsKey, json.encodeToString(updatedReport)) + preferences + } + } + + suspend fun getLastWeeklyReport(): WeeklyReport? { + return getLast99Reports().lastOrNull() + } + + suspend fun getLast99Reports(): List { + return store.getValue(weeklyReportsKey)?.let { + runCatching { + json.decodeFromString>(it).takeLast(99) + }.onFailure { + Timber.e("Can't deserialize weeklyReports") + store.removeKey(weeklyReportsKey) + }.getOrNull() + } ?: emptyList() + } +} diff --git a/app/src/main/java/foundation/e/advancedprivacy/domain/entities/weeklyreport/WeeklyReport.kt b/app/src/main/java/foundation/e/advancedprivacy/domain/entities/weeklyreport/WeeklyReport.kt index b3998ca9b7d86f540de42e233b81e194a1aedebd..7e93b8e7a4d9b1d9469ec2388dfdf668b5f1867f 100644 --- a/app/src/main/java/foundation/e/advancedprivacy/domain/entities/weeklyreport/WeeklyReport.kt +++ b/app/src/main/java/foundation/e/advancedprivacy/domain/entities/weeklyreport/WeeklyReport.kt @@ -17,10 +17,14 @@ package foundation.e.advancedprivacy.domain.entities.weeklyreport +import foundation.e.advancedprivacy.core.utils.InstantSerializer +import java.time.Instant import kotlinx.serialization.Serializable @Serializable data class WeeklyReport( + @Serializable(with = InstantSerializer::class) + val timestamp: Instant, val statType: StatType, val labelId: LabelId, val primaryValue: String, diff --git a/app/src/main/java/foundation/e/advancedprivacy/domain/usecases/WeeklyReportUseCase.kt b/app/src/main/java/foundation/e/advancedprivacy/domain/usecases/WeeklyReportUseCase.kt index 2f2a79c4b754ba3e8e87eb9306c1105c87ac7891..ef572d929e91764e059cdb26a852d56158db6a2f 100644 --- a/app/src/main/java/foundation/e/advancedprivacy/domain/usecases/WeeklyReportUseCase.kt +++ b/app/src/main/java/foundation/e/advancedprivacy/domain/usecases/WeeklyReportUseCase.kt @@ -18,25 +18,76 @@ package foundation.e.advancedprivacy.domain.usecases import foundation.e.advancedprivacy.data.repositories.AppListRepository +import foundation.e.advancedprivacy.data.repositories.WeeklyReportLocalRepository import foundation.e.advancedprivacy.domain.entities.weeklyreport.DisplayableReport import foundation.e.advancedprivacy.domain.entities.weeklyreport.WeeklyReport import foundation.e.advancedprivacy.domain.entities.weeklyreport.WeeklyReportScore import foundation.e.advancedprivacy.trackers.data.TrackersRepository +import java.time.Duration +import java.time.Instant +import java.time.temporal.ChronoUnit import kotlin.random.Random +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch import kotlinx.coroutines.withContext class WeeklyReportUseCase( private val trackersRepository: TrackersRepository, - private val appListRepository: AppListRepository + private val appListRepository: AppListRepository, + private val weeklyReportRepository: WeeklyReportLocalRepository, + private val scope: CoroutineScope ) { + private val _currentReport = MutableStateFlow(null) + val currentReport: StateFlow = _currentReport + + private val displayDuration: Duration = Duration.ofDays(1) + + private var stopDisplayJob: Job? = null + + init { + scope.launch { + updateCurrent() + } + } + + private suspend fun updateCurrent() { + val weeklyReport = weeklyReportRepository.getLastWeeklyReport() ?: return + val now = Instant.now() + val endOfDisplay = weeklyReport.timestamp + displayDuration + if (now < endOfDisplay) { + _currentReport.value = weeklyReport.toDisplayableReport() + stopDisplayJob = scope.launch { + delay(endOfDisplay.toEpochMilli() - now.toEpochMilli()) + _currentReport.value = null + + stopDisplayJob = null + } + } else { + _currentReport.value = null + } + } + + suspend fun updateWeeklyReport() = withContext(Dispatchers.IO) { + val history = weeklyReportRepository.getLast99Reports() + val weeklyReport = buildCandidates(Instant.now(), history).first().weeklyReport + weeklyReportRepository.setLastWeeklyReport(weeklyReport) + + updateCurrent() + } + suspend fun debugGenerateReportsSinceWeeksAgo(weeksAgo: Int): List>> = withContext(Dispatchers.IO) { val history = mutableListOf() val result = mutableListOf>>() (weeksAgo downTo 0).map { - val candidates = buildCandidates(history) + val endOfWeek = Instant.now().minus(weeksAgo.toLong() * 7, ChronoUnit.DAYS) + val candidates = buildCandidates(endOfWeek, history) val weeklyReport = candidates.first().weeklyReport history.add(weeklyReport) @@ -46,23 +97,24 @@ class WeeklyReportUseCase( result.reversed() } - private fun buildCandidates(history: List): List { + private fun buildCandidates(endOfWeek: Instant, history: List): List { val candidates = mutableListOf() - addCallPerAppCandidates(candidates) - addNewTrackerCandidates(candidates) - addCallAndLeaksCandidates(candidates) - addTrackerWithMostAppsCandidates(candidates) + addCallPerAppCandidates(candidates, endOfWeek) + addNewTrackerCandidates(candidates, endOfWeek) + addCallAndLeaksCandidates(candidates, endOfWeek) + addTrackerWithMostAppsCandidates(candidates, endOfWeek) return candidates.map { computeScore(it, history) }.sortedBy { it.score } } - private fun addCallPerAppCandidates(candidates: MutableList) { + private fun addCallPerAppCandidates(candidates: MutableList, endOfWeek: Instant) { val anyApp = appListRepository.displayableApps.value.first().id val callsPerWeek = Random.nextInt(10000) val hoursInWeek = 7 * 24 candidates.add( WeeklyReport( + endOfWeek, WeeklyReport.StatType.CALLS_PER_APP, WeeklyReport.LabelId.CALLS_PER_APP_1, anyApp, @@ -72,6 +124,7 @@ class WeeklyReportUseCase( candidates.add( WeeklyReport( + endOfWeek, WeeklyReport.StatType.CALLS_PER_APP, WeeklyReport.LabelId.CALLS_PER_APP_2, anyApp, @@ -81,6 +134,7 @@ class WeeklyReportUseCase( candidates.add( WeeklyReport( + endOfWeek, WeeklyReport.StatType.CALLS_PER_APP, WeeklyReport.LabelId.CALLS_PER_APP_3, anyApp, @@ -89,10 +143,11 @@ class WeeklyReportUseCase( ) } - private fun addNewTrackerCandidates(candidates: MutableList) { + private fun addNewTrackerCandidates(candidates: MutableList, endOfWeek: Instant) { val anyApp2 = appListRepository.displayableApps.value.first().id candidates.add( WeeklyReport( + endOfWeek, WeeklyReport.StatType.NEW_TRACKER, WeeklyReport.LabelId.NEW_TRACKER_1, anyApp2, @@ -102,6 +157,7 @@ class WeeklyReportUseCase( candidates.add( WeeklyReport( + endOfWeek, WeeklyReport.StatType.NEW_TRACKER, WeeklyReport.LabelId.NEW_TRACKER_2, "", @@ -111,6 +167,7 @@ class WeeklyReportUseCase( candidates.add( WeeklyReport( + endOfWeek, WeeklyReport.StatType.NEW_TRACKER, WeeklyReport.LabelId.NEW_TRACKER_3, "", @@ -119,12 +176,13 @@ class WeeklyReportUseCase( ) } - private fun addCallAndLeaksCandidates(candidates: MutableList) { + private fun addCallAndLeaksCandidates(candidates: MutableList, endOfWeek: Instant) { val blockedRate = Random.nextInt(100).toString() val leaks = Random.nextInt(1000).toString() candidates.add( WeeklyReport( + endOfWeek, WeeklyReport.StatType.CALLS_AND_LEAKS, WeeklyReport.LabelId.CALLS_AND_LEAKS_1, blockedRate, @@ -133,6 +191,7 @@ class WeeklyReportUseCase( ) candidates.add( WeeklyReport( + endOfWeek, WeeklyReport.StatType.CALLS_AND_LEAKS, WeeklyReport.LabelId.CALLS_AND_LEAKS_2, blockedRate, @@ -141,9 +200,10 @@ class WeeklyReportUseCase( ) } - private fun addTrackerWithMostAppsCandidates(candidates: MutableList) { + private fun addTrackerWithMostAppsCandidates(candidates: MutableList, endOfWeek: Instant) { candidates.add( WeeklyReport( + endOfWeek, WeeklyReport.StatType.TRACKER_WITH_MOST_APPS, WeeklyReport.LabelId.TRACKER_WITH_MOST_APPS_1, "wtm_yahoo", diff --git a/app/src/main/java/foundation/e/advancedprivacy/externalinterfaces/workers/WeeklyReportWorker.kt b/app/src/main/java/foundation/e/advancedprivacy/externalinterfaces/workers/WeeklyReportWorker.kt new file mode 100644 index 0000000000000000000000000000000000000000..a3a7e2c2daf8f0a1c89435aa71a2eff97ee0860c --- /dev/null +++ b/app/src/main/java/foundation/e/advancedprivacy/externalinterfaces/workers/WeeklyReportWorker.kt @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2025 E FOUNDATION + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package foundation.e.advancedprivacy.externalinterfaces.workers + +import android.content.Context +import androidx.work.CoroutineWorker +import androidx.work.ExistingWorkPolicy +import androidx.work.OneTimeWorkRequestBuilder +import androidx.work.WorkManager +import androidx.work.WorkerParameters +import foundation.e.advancedprivacy.domain.usecases.WeeklyReportUseCase +import java.time.ZonedDateTime +import java.time.temporal.ChronoUnit +import java.time.temporal.WeekFields +import java.util.concurrent.TimeUnit +import org.koin.java.KoinJavaComponent.get +import timber.log.Timber + +class WeeklyReportWorker(appContext: Context, workerParams: WorkerParameters) : CoroutineWorker(appContext, workerParams) { + override suspend fun doWork(): Result { + Timber.d("WeeklyReportWorker starts computing this week report") + val weeklyReportUseCase: WeeklyReportUseCase = get(WeeklyReportUseCase::class.java) + weeklyReportUseCase.updateWeeklyReport() + scheduleNext(applicationContext) + return Result.success() + } + + companion object { + fun scheduleNext(appContext: Context) { + var next = ZonedDateTime.now() + next = next.with(WeekFields.of(appContext.resources.configuration.locales[0]).dayOfWeek(), 7) + next = next.truncatedTo(ChronoUnit.DAYS) + next = next.plus(11, ChronoUnit.HOURS) + + val delay = next.toEpochSecond() - ZonedDateTime.now().toEpochSecond() + Timber.d("Schedule Weeklyreport for $next, in $delay seconds") + + val request = OneTimeWorkRequestBuilder() + .setInitialDelay(delay, TimeUnit.SECONDS) + .build() + + WorkManager.getInstance(appContext).enqueueUniqueWork( + WeeklyReportWorker::class.qualifiedName ?: "", + ExistingWorkPolicy.REPLACE, + request + ) + } + } +} diff --git a/app/src/main/java/foundation/e/advancedprivacy/features/debug/DebugWeeklyReportFragment.kt b/app/src/main/java/foundation/e/advancedprivacy/features/debug/DebugWeeklyReportFragment.kt index 1d0178f3166c85d5d7173a1ab88ae23e787d8a0d..aebb20d3d71e476abcb51f6244fda867ab99b255 100644 --- a/app/src/main/java/foundation/e/advancedprivacy/features/debug/DebugWeeklyReportFragment.kt +++ b/app/src/main/java/foundation/e/advancedprivacy/features/debug/DebugWeeklyReportFragment.kt @@ -30,21 +30,29 @@ import androidx.recyclerview.widget.RecyclerView import foundation.e.advancedprivacy.R import foundation.e.advancedprivacy.common.BindingListAdapter import foundation.e.advancedprivacy.common.BindingViewHolder +import foundation.e.advancedprivacy.data.repositories.AppListRepository import foundation.e.advancedprivacy.databinding.DebugWeeklyReportFragmentBinding import foundation.e.advancedprivacy.databinding.DebugWeeklyReportItemBinding import foundation.e.advancedprivacy.domain.entities.weeklyreport.DisplayableReport +import foundation.e.advancedprivacy.domain.entities.weeklyreport.WeeklyReport import foundation.e.advancedprivacy.domain.entities.weeklyreport.WeeklyReportScore import foundation.e.advancedprivacy.domain.usecases.WeeklyReportUseCase import foundation.e.advancedprivacy.features.weeklyreport.WeeklyReportViewFactory +import foundation.e.advancedprivacy.trackers.data.TrackersRepository +import java.time.Instant +import kotlin.random.Random import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import org.koin.android.ext.android.inject class DebugWeeklyReportFragment : Fragment(R.layout.debug_weekly_report_fragment) { private val weeklyReportUseCase: WeeklyReportUseCase by inject() + private val appListRepository: AppListRepository by inject() + private val trackersRepository: TrackersRepository by inject() + private val reportsFactory: WeeklyReportViewFactory by inject() + private lateinit var binding: DebugWeeklyReportFragmentBinding - private val reportsFactory: WeeklyReportViewFactory by inject() private val weeklyReportsAdapter = object : BindingListAdapter< DebugWeeklyReportItemBinding, Pair> @@ -96,7 +104,7 @@ class DebugWeeklyReportFragment : Fragment(R.layout.debug_weekly_report_fragment binding.debugUpdateReport.setOnClickListener { lifecycleScope.launch(Dispatchers.IO) { - // TODO + weeklyReportUseCase.updateWeeklyReport() } } @@ -109,6 +117,12 @@ class DebugWeeklyReportFragment : Fragment(R.layout.debug_weekly_report_fragment } } } + + binding.showAllReportsViews.setOnClickListener { + weeklyReportsAdapter.dataSet = buildFakeReports().map { + it to emptyList() + } + } } private fun setupRecyclerView(recyclerView: RecyclerView) { @@ -118,4 +132,136 @@ class DebugWeeklyReportFragment : Fragment(R.layout.debug_weekly_report_fragment isNestedScrollingEnabled = false } } + + private fun buildFakeReports(): List { + val reports = mutableListOf() + val timestamp = Instant.now() + + // Call Per App + val anyApp = appListRepository.displayableApps.value.first() + val callsPerWeek = Random.nextInt(10000) + + val hoursInWeek = 7 * 24 + reports.add( + DisplayableReport.ReportWithApp( + WeeklyReport( + timestamp, + WeeklyReport.StatType.CALLS_PER_APP, + WeeklyReport.LabelId.CALLS_PER_APP_1, + anyApp.id, + listOf((callsPerWeek / hoursInWeek).toString()) + ), + anyApp + ) + ) + + reports.add( + DisplayableReport.ReportWithApp( + WeeklyReport( + timestamp, + WeeklyReport.StatType.CALLS_PER_APP, + WeeklyReport.LabelId.CALLS_PER_APP_2, + anyApp.id, + listOf((callsPerWeek / hoursInWeek).toString()) + ), + anyApp + ) + ) + reports.add( + DisplayableReport.ReportWithApp( + WeeklyReport( + timestamp, + WeeklyReport.StatType.CALLS_PER_APP, + WeeklyReport.LabelId.CALLS_PER_APP_3, + anyApp.id, + listOf((callsPerWeek / 7).toString()) + ), + anyApp + ) + ) + + // New Trackers + val anyApp2 = appListRepository.displayableApps.value.first() + reports.add( + DisplayableReport.ReportWithApp( + WeeklyReport( + timestamp, + WeeklyReport.StatType.NEW_TRACKER, + WeeklyReport.LabelId.NEW_TRACKER_1, + anyApp2.id, + emptyList() + ), + anyApp2 + ) + ) + + reports.add( + DisplayableReport.ReportText( + WeeklyReport( + timestamp, + WeeklyReport.StatType.NEW_TRACKER, + WeeklyReport.LabelId.NEW_TRACKER_2, + "", + listOf(Random.nextInt(4).toString()) + ) + ) + ) + + reports.add( + DisplayableReport.ReportText( + WeeklyReport( + timestamp, + WeeklyReport.StatType.NEW_TRACKER, + WeeklyReport.LabelId.NEW_TRACKER_3, + "", + listOf(Random.nextInt(4).toString()) + ) + ) + ) + + // Calls and Leaks + val blockedRate = Random.nextInt(100).toString() + val leaks = Random.nextInt(1000).toString() + reports.add( + DisplayableReport.ReportText( + WeeklyReport( + timestamp, + WeeklyReport.StatType.CALLS_AND_LEAKS, + WeeklyReport.LabelId.CALLS_AND_LEAKS_1, + blockedRate, + listOf(leaks) + ) + ) + ) + + reports.add( + DisplayableReport.ReportText( + WeeklyReport( + timestamp, + WeeklyReport.StatType.CALLS_AND_LEAKS, + WeeklyReport.LabelId.CALLS_AND_LEAKS_2, + blockedRate, + listOf(leaks) + ) + ) + ) + + // Tracker With Most Apps + trackersRepository.getTracker("wtm_yahoo")?.let { tracker -> + reports.add( + DisplayableReport.ReportWithTracker( + WeeklyReport( + timestamp, + WeeklyReport.StatType.TRACKER_WITH_MOST_APPS, + WeeklyReport.LabelId.TRACKER_WITH_MOST_APPS_1, + tracker.id, + listOf(Random.nextInt(20).toString()) + ), + tracker + ) + ) + } + + return reports + } } diff --git a/app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersFragment.kt b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersFragment.kt index ba65d8180a3312d1d62b00e1a3b7ff0b7810ed78..7d3afeb045d7c032cc6701cf91a9e7be77ff9a01 100644 --- a/app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersFragment.kt +++ b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersFragment.kt @@ -30,6 +30,7 @@ import android.text.style.UnderlineSpan import android.view.View import android.widget.Toast import androidx.core.content.ContextCompat +import androidx.core.view.isVisible import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle @@ -42,12 +43,16 @@ import foundation.e.advancedprivacy.common.extensions.findViewHolderForAdapterPo import foundation.e.advancedprivacy.common.extensions.safeNavigate import foundation.e.advancedprivacy.common.extensions.updatePagerHeightForChild import foundation.e.advancedprivacy.databinding.FragmentTrackersBinding +import foundation.e.advancedprivacy.features.weeklyreport.WeeklyReportViewFactory +import kotlin.getValue import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.launch +import org.koin.android.ext.android.inject import org.koin.androidx.viewmodel.ext.android.viewModel class TrackersFragment : NavToolbarFragment(R.layout.fragment_trackers) { private val viewModel: TrackersViewModel by viewModel() + private val weeklyReportViewFactory: WeeklyReportViewFactory by inject() private lateinit var binding: FragmentTrackersBinding private lateinit var pagerAdapter: TrackersPeriodAdapter @@ -104,6 +109,27 @@ class TrackersFragment : NavToolbarFragment(R.layout.fragment_trackers) { viewModel.navigate.collect(findNavController()::safeNavigate) } } + + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + viewModel.state.collect { report -> + if (report != null) { + weeklyReportViewFactory.createView(report, layoutInflater, binding.weeklyreportContainer)?.let { + binding.weeklyreportContainer.addView(it) + binding.weeklyreportContainer.isVisible = true + } + } else { + binding.weeklyreportContainer.isVisible = false + } + } + } + } + + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + viewModel.doOnStartedState() + } + } } } diff --git a/app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersViewModel.kt b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersViewModel.kt index b682adfbf2b7e664270b7a57c645b609f0d13869..692dbbe95c31a041eae417bc49750d143fb39064 100644 --- a/app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersViewModel.kt +++ b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersViewModel.kt @@ -23,13 +23,22 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.navigation.NavDirections import foundation.e.advancedprivacy.R +import foundation.e.advancedprivacy.domain.entities.weeklyreport.DisplayableReport import foundation.e.advancedprivacy.domain.usecases.TrackersScreenUseCase +import foundation.e.advancedprivacy.domain.usecases.WeeklyReportUseCase import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext -class TrackersViewModel(private val trackersScreenUseCase: TrackersScreenUseCase) : ViewModel() { +class TrackersViewModel( + private val trackersScreenUseCase: TrackersScreenUseCase, + private val weeklyReportUseCase: WeeklyReportUseCase +) : ViewModel() { private val _singleEvents = MutableSharedFlow() val singleEvents = _singleEvents.asSharedFlow() @@ -39,6 +48,9 @@ class TrackersViewModel(private val trackersScreenUseCase: TrackersScreenUseCase private val _refreshUiHeight = MutableSharedFlow() val refreshUiHeight = _refreshUiHeight.asSharedFlow() + private val _state = MutableStateFlow(null) + val state: StateFlow = _state + val positionsCount = 3 fun getPeriod(position: Int): Period { return when (position) { @@ -66,6 +78,12 @@ class TrackersViewModel(private val trackersScreenUseCase: TrackersScreenUseCase } } + suspend fun doOnStartedState(): Nothing = withContext(Dispatchers.IO) { + weeklyReportUseCase.currentReport.collect { report -> + _state.update { report } + } + } + fun onDisplayedItemChanged(position: Int) = viewModelScope.launch(Dispatchers.IO) { trackersScreenUseCase.savePosition(position) trackersScreenUseCase.resetTrackerTabStartPosition() diff --git a/app/src/main/java/foundation/e/advancedprivacy/features/weeklyreport/WeeklyReportViewFactory.kt b/app/src/main/java/foundation/e/advancedprivacy/features/weeklyreport/WeeklyReportViewFactory.kt index 6ef8ffe73debc53ad155ab962cc3435347bcd890..a0a7d711f53af2338905dbe2cd4d3184e6df94d7 100644 --- a/app/src/main/java/foundation/e/advancedprivacy/features/weeklyreport/WeeklyReportViewFactory.kt +++ b/app/src/main/java/foundation/e/advancedprivacy/features/weeklyreport/WeeklyReportViewFactory.kt @@ -17,6 +17,7 @@ package foundation.e.advancedprivacy.features.weeklyreport +import android.graphics.drawable.Drawable import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -27,11 +28,16 @@ class WeeklyReportViewFactory() { fun createView(report: DisplayableReport, inflater: LayoutInflater, viewGroup: ViewGroup): View? { val binding = WeeklyReportItemTextBinding.inflate(inflater, viewGroup, false) + binding.title.text = getTitle(report) binding.title.text = report.report.labelId.name binding.description.text = getDescription(report) return binding.root } + private fun getTitle(report: DisplayableReport): CharSequence { + return report.report.labelId.name + } + private fun getDescription(report: DisplayableReport): CharSequence { val primaryValue = when (report) { is DisplayableReport.ReportWithApp -> report.app.label @@ -41,4 +47,11 @@ class WeeklyReportViewFactory() { return "${report.report.statType.name} $primaryValue, ${report.report.secondaryValues.joinToString(", ")}" } + + private fun getIcon(report: DisplayableReport): Drawable? { + return when (report) { + is DisplayableReport.ReportWithApp -> report.app.icon + else -> null + } + } } diff --git a/app/src/main/res/layout/debug_weekly_report_fragment.xml b/app/src/main/res/layout/debug_weekly_report_fragment.xml index 1095808bfc77cc5e0015bb9fc948b0df250c1714..e0f1297b9a2b24bb63fab5d0ee02b98003b0c8c8 100644 --- a/app/src/main/res/layout/debug_weekly_report_fragment.xml +++ b/app/src/main/res/layout/debug_weekly_report_fragment.xml @@ -60,6 +60,13 @@ android:text="10" /> + + + + + AdvancedPrivacy debug tools + Weekly Report debug tools + Update report + Generate report of week ago: + Share + Details + Notification + Show all reports views + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9bb5ed80285a7b86b2ec88f63502fe6c7084fb8c..9ed1aa5f921e35f1a5bfc80fe4e106ea9c375a3e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -217,15 +217,4 @@ Highlight that the trackers are actually logged and blocked by Advanced Privacy Tracker control is on This could impact the functioning of some applications. - - - - AdvancedPrivacy debug tools - Weekly Report debug tools - Update report - Generate report of week ago: - Share - Details - Notification - diff --git a/core/build.gradle b/core/build.gradle index 1e6ad4fc05225d40efe703b92c5434aef3629072..c073c81281d6ae6c251473be0847d1801f6a7514 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -54,6 +54,7 @@ dependencies { libs.androidx.datastore.preferences, libs.bundles.koin, libs.kotlinx.coroutines, + libs.kotlinx.serialization, libs.timber ) } diff --git a/core/src/main/java/foundation/e/advancedprivacy/core/utils/InstantSerializer.kt b/core/src/main/java/foundation/e/advancedprivacy/core/utils/InstantSerializer.kt new file mode 100644 index 0000000000000000000000000000000000000000..d5b5f6383e2e1e95fc2090a6ba1de772a071b62b --- /dev/null +++ b/core/src/main/java/foundation/e/advancedprivacy/core/utils/InstantSerializer.kt @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2025 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.core.utils + +import java.time.Instant +import kotlinx.serialization.KSerializer +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder + +class InstantSerializer : KSerializer { + override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("Instant", PrimitiveKind.STRING) + override fun serialize(encoder: Encoder, value: Instant) { + encoder.encodeString(value.toString()) + } + override fun deserialize(decoder: Decoder): Instant { + return Instant.parse(decoder.decodeString()) + } +}