diff --git a/app/src/main/java/foundation/e/advancedprivacy/AdvancedPrivacyApplication.kt b/app/src/main/java/foundation/e/advancedprivacy/AdvancedPrivacyApplication.kt index 7b7353b8d8d9e70938e16d2adb2eb35fb0be1629..5b298b7e5a3001b4d921871a3999d023a90912ee 100644 --- a/app/src/main/java/foundation/e/advancedprivacy/AdvancedPrivacyApplication.kt +++ b/app/src/main/java/foundation/e/advancedprivacy/AdvancedPrivacyApplication.kt @@ -28,7 +28,6 @@ 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.domain.usecases.WeeklyReportUseCase -import foundation.e.advancedprivacy.externalinterfaces.workers.WeeklyReportWorker import foundation.e.advancedprivacy.trackers.data.TrackersRepository import foundation.e.advancedprivacy.trackers.services.UpdateTrackersWorker import foundation.e.lib.telemetry.Telemetry @@ -39,12 +38,15 @@ import kotlinx.coroutines.withContext import org.koin.android.ext.koin.androidContext import org.koin.core.context.startKoin import org.koin.java.KoinJavaComponent.get +import timber.log.Timber class AdvancedPrivacyApplication : Application() { override fun onCreate() { super.onCreate() Telemetry.init(BuildConfig.SENTRY_DSN, this, true) + Timber.plant(Timber.DebugTree()) + startKoin { androidContext(this@AdvancedPrivacyApplication) modules(appModule) @@ -59,7 +61,6 @@ class AdvancedPrivacyApplication : Application() { get(TrackersRepository::class.java).initTrackersFile() UpdateTrackersWorker.periodicUpdate(this@AdvancedPrivacyApplication) - WeeklyReportWorker.scheduleNext(this@AdvancedPrivacyApplication) 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 20f78870834799f8ba6893bcc3f2dd20d8c55171..e87242dadbfcd53ac92880ead109b9bf275b1321 100644 --- a/app/src/main/java/foundation/e/advancedprivacy/KoinModule.kt +++ b/app/src/main/java/foundation/e/advancedprivacy/KoinModule.kt @@ -45,6 +45,7 @@ import foundation.e.advancedprivacy.domain.usecases.TrackersStatisticsUseCase import foundation.e.advancedprivacy.domain.usecases.WeeklyReportUseCase import foundation.e.advancedprivacy.dummy.CityDataSource import foundation.e.advancedprivacy.externalinterfaces.permissions.IPermissionsPrivacyModule +import foundation.e.advancedprivacy.externalinterfaces.workers.WeeklyReportWorkerScheduler import foundation.e.advancedprivacy.features.dashboard.DashboardViewModel import foundation.e.advancedprivacy.features.internetprivacy.InternetPrivacyViewModel import foundation.e.advancedprivacy.features.location.FakeLocationViewModel @@ -138,6 +139,8 @@ val appModule = module { single { CityDataSource } single { ResourcesRepository(androidContext()) } + single { WeeklyReportWorkerScheduler(androidContext()) } + singleOf(::FakeLocationStateUseCase) single { 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 index 220a655c51b583f132cfa6466071c255fd46fbb4..fd760089702671bfd59f35b157c5154cafd644f0 100644 --- a/app/src/main/java/foundation/e/advancedprivacy/data/repositories/WeeklyReportLocalRepository.kt +++ b/app/src/main/java/foundation/e/advancedprivacy/data/repositories/WeeklyReportLocalRepository.kt @@ -64,4 +64,8 @@ class WeeklyReportLocalRepository( }.getOrNull() } ?: emptyList() } + + suspend fun clearAllReports() { + return store.removeKey(weeklyReportsKey) + } } 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 7e93b8e7a4d9b1d9469ec2388dfdf668b5f1867f..3fc97bcda6dfc25967e9e657976866ed6d4d1ae7 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 @@ -19,6 +19,8 @@ package foundation.e.advancedprivacy.domain.entities.weeklyreport import foundation.e.advancedprivacy.core.utils.InstantSerializer import java.time.Instant +import java.time.LocalDate +import java.time.ZoneId import kotlinx.serialization.Serializable @Serializable @@ -30,6 +32,10 @@ data class WeeklyReport( val primaryValue: String, val secondaryValues: List ) { + fun getDate(): LocalDate { + return timestamp.atZone(ZoneId.systemDefault()).toLocalDate() + } + @Serializable enum class StatType { CALLS_PER_APP, 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 26f16e57aac9b24b7c88fea269e0a3ba13ca9c04..735095e6da9995c8b382449b69fd19d00f1a89c2 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 @@ -22,18 +22,19 @@ import foundation.e.advancedprivacy.data.repositories.WeeklyReportLocalRepositor 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.externalinterfaces.workers.WeeklyReportWorkerScheduler import foundation.e.advancedprivacy.trackers.data.StatsDatabase import foundation.e.advancedprivacy.trackers.data.TrackersRepository -import java.time.Duration +import java.time.DayOfWeek +import java.time.DayOfWeek.SUNDAY import java.time.Instant +import java.time.LocalDate import java.time.temporal.ChronoUnit import kotlin.collections.component1 import kotlin.collections.component2 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 @@ -43,44 +44,42 @@ class WeeklyReportUseCase( private val trackersRepository: TrackersRepository, private val appListRepository: AppListRepository, private val weeklyReportRepository: WeeklyReportLocalRepository, + private val weeklyReportWorkerScheduler: WeeklyReportWorkerScheduler, private val statsDatabase: StatsDatabase, private val scope: CoroutineScope ) { + companion object { + val REPORT_DAY_OF_WEEK: DayOfWeek = SUNDAY + const val REPORT_HOUR: Long = 11L + } private val _currentReport = MutableStateFlow(null) val currentReport: StateFlow = _currentReport - private val displayDuration: Duration = Duration.ofDays(1) - - private var stopDisplayJob: Job? = null - fun listen() { scope.launch { updateCurrent() + val currentReport = currentReport.value?.report + weeklyReportWorkerScheduler.scheduleNext(currentReport) + weeklyReportWorkerScheduler.scheduleDismiss(currentReport) } } suspend fun updateCurrent() { val weeklyReport = weeklyReportRepository.getLastWeeklyReport() ?: return - val now = Instant.now() - val endOfDisplay = weeklyReport.timestamp + displayDuration - if (now < endOfDisplay) { + if (LocalDate.now() == weeklyReport.getDate()) { _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) { + suspend fun updateWeeklyReport(): WeeklyReport = withContext(Dispatchers.IO) { val history = weeklyReportRepository.getLast99Reports() val weeklyReport = buildCandidates(Instant.now(), history).first().weeklyReport weeklyReportRepository.setLastWeeklyReport(weeklyReport) updateCurrent() + weeklyReport } suspend fun debugGenerateReportsSinceWeeksAgo(weeksAgo: Int): List>> = 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 index a3a7e2c2daf8f0a1c89435aa71a2eff97ee0860c..80d8fe49b923c52ed1bcc1044bb7304f0d35fc0e 100644 --- a/app/src/main/java/foundation/e/advancedprivacy/externalinterfaces/workers/WeeklyReportWorker.kt +++ b/app/src/main/java/foundation/e/advancedprivacy/externalinterfaces/workers/WeeklyReportWorker.kt @@ -23,10 +23,13 @@ import androidx.work.ExistingWorkPolicy import androidx.work.OneTimeWorkRequestBuilder import androidx.work.WorkManager import androidx.work.WorkerParameters +import foundation.e.advancedprivacy.domain.entities.weeklyreport.WeeklyReport import foundation.e.advancedprivacy.domain.usecases.WeeklyReportUseCase +import java.time.LocalDate +import java.time.ZoneId import java.time.ZonedDateTime import java.time.temporal.ChronoUnit -import java.time.temporal.WeekFields +import java.time.temporal.TemporalAdjusters.nextOrSame import java.util.concurrent.TimeUnit import org.koin.java.KoinJavaComponent.get import timber.log.Timber @@ -34,31 +37,77 @@ 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 weeklyReportWorkerScheduler: WeeklyReportWorkerScheduler = get(WeeklyReportWorkerScheduler::class.java) val weeklyReportUseCase: WeeklyReportUseCase = get(WeeklyReportUseCase::class.java) - weeklyReportUseCase.updateWeeklyReport() - scheduleNext(applicationContext) + val newReport = weeklyReportUseCase.updateWeeklyReport() + weeklyReportWorkerScheduler.scheduleNext(newReport) + weeklyReportWorkerScheduler.scheduleDismiss(newReport) 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 - ) +class WeeklyReportDismissWorker(appContext: Context, workerParams: WorkerParameters) : CoroutineWorker(appContext, workerParams) { + override suspend fun doWork(): Result { + Timber.d("WeeklyReportDismissWorker will dismiss any displayed WeeklyReport") + val weeklyReportUseCase: WeeklyReportUseCase = get(WeeklyReportUseCase::class.java) + weeklyReportUseCase.updateCurrent() + return Result.success() + } +} + +class WeeklyReportWorkerScheduler(private val appContext: Context) { + fun scheduleNext(lastReport: WeeklyReport?) { + val now = ZonedDateTime.now() + var nextDate: LocalDate = now.toLocalDate() + + nextDate.with(nextOrSame(WeeklyReportUseCase.REPORT_DAY_OF_WEEK)) + + // Report has already been created today, + // next report will be scheduled for next week + if (nextDate == lastReport?.getDate() && now.hour >= WeeklyReportUseCase.REPORT_HOUR) { + nextDate = nextDate.plus(7, ChronoUnit.DAYS) } + + var next = nextDate.atStartOfDay(ZoneId.systemDefault()) + next = next.plus(WeeklyReportUseCase.REPORT_HOUR, ChronoUnit.HOURS) + + val delay = next.toEpochSecond() - 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 + ) + } + + fun scheduleDismiss(currentReport: WeeklyReport?) { + if (currentReport == null) { + return + } + + val dismiss = ZonedDateTime.ofInstant(currentReport.timestamp, ZoneId.systemDefault()) + .plus(1, ChronoUnit.DAYS) + .truncatedTo(ChronoUnit.DAYS) + + val delay = dismiss.toEpochSecond() - ZonedDateTime.now().toEpochSecond() + if (delay < 0) { + return + } + + Timber.d("Schedule Dismiss of WeeklyReport of ${currentReport.timestamp} at $dismiss, in $delay seconds") + val request = OneTimeWorkRequestBuilder() + .setInitialDelay(delay, TimeUnit.SECONDS) + .build() + + WorkManager.getInstance(appContext).enqueueUniqueWork( + WeeklyReportDismissWorker::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 2544e8459d77b4213199ebdf2c9b63000412c023..57453f3b85b6081f6a2e2dfa6736b04ee9156fdc 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 @@ -147,6 +147,12 @@ class DebugWeeklyReportFragment : Fragment(R.layout.debug_weekly_report_fragment } } + binding.debugClearAllReport.setOnClickListener { + lifecycleScope.launch(Dispatchers.IO) { + weeklyReportRepository.clearAllReports() + } + } + binding.debugGenerateOldReport.setOnClickListener { lifecycleScope.launch { val count = binding.debugWeeksAgo.text.toString().toInt() 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 e0f1297b9a2b24bb63fab5d0ee02b98003b0c8c8..713163ae07483f2bbe827b2c97858540732d4aa8 100644 --- a/app/src/main/res/layout/debug_weekly_report_fragment.xml +++ b/app/src/main/res/layout/debug_weekly_report_fragment.xml @@ -25,13 +25,27 @@ android:layout_width="match_parent" android:orientation="vertical" > - + + + + AdvancedPrivacy debug tools Weekly Report debug tools Update report + Clear all reports Generate report of week ago: Share Details