/*
* Copyright (C) 2022 - 2023 MURENA SAS
* Copyright (C) 2021 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.domain.usecases
import android.content.res.Resources
import foundation.e.advancedprivacy.R
import foundation.e.advancedprivacy.common.throttleFirst
import foundation.e.advancedprivacy.data.repositories.AppListsRepository
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
import foundation.e.advancedprivacy.trackers.domain.usecases.StatisticsUseCase
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart
import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter
import java.time.temporal.ChronoUnit
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
class TrackersStatisticsUseCase(
private val statisticsUseCase: StatisticsUseCase,
private val whitelistRepository: WhitelistRepository,
private val trackersRepository: TrackersRepository,
private val appListsRepository: AppListsRepository,
private val statsDatabase: StatsDatabase,
private val resources: Resources
) {
fun initAppList() {
appListsRepository.apps()
}
@OptIn(FlowPreview::class)
fun listenUpdates(debounce: Duration = 1.seconds) =
statsDatabase.newDataAvailable
.throttleFirst(windowDuration = debounce)
.onStart { emit(Unit) }
fun getDayStatistics(): Pair {
return TrackersPeriodicStatistics(
callsBlockedNLeaked = statisticsUseCase.getTrackersCallsOnPeriod(24, ChronoUnit.HOURS),
periods = buildDayLabels(),
trackersCount = statisticsUseCase.getActiveTrackersByPeriod(24, ChronoUnit.HOURS),
graduations = buildDayGraduations(),
) to statisticsUseCase.getContactedTrackersCount()
}
fun getNonBlockedTrackersCount(): Flow {
return if (whitelistRepository.isBlockingEnabled)
appListsRepository.allApps().map { apps ->
val whiteListedTrackers = mutableSetOf()
val whiteListedApps = whitelistRepository.getWhiteListedApp()
apps.forEach { app ->
if (app in whiteListedApps) {
whiteListedTrackers.addAll(statisticsUseCase.getTrackers(listOf(app)))
} else {
whiteListedTrackers.addAll(getWhiteList(app))
}
}
whiteListedTrackers.size
}
else flowOf(statisticsUseCase.getContactedTrackersCount())
}
fun getMostLeakedApp(): ApplicationDescription? {
return statisticsUseCase.getMostLeakedApp(24, ChronoUnit.HOURS)
}
fun getDayTrackersCalls() = statisticsUseCase.getTrackersCallsOnPeriod(24, ChronoUnit.HOURS)
fun getDayTrackersCount() = statisticsUseCase.getActiveTrackersByPeriod(24, ChronoUnit.HOURS)
private fun buildDayGraduations(): List {
val formatter = DateTimeFormatter.ofPattern(
resources.getString(R.string.trackers_graph_hours_period_format)
)
val periods = mutableListOf()
var end = ZonedDateTime.now()
for (i in 1..24) {
val start = end.truncatedTo(ChronoUnit.HOURS)
periods.add(if (start.hour % 6 == 0) formatter.format(start) else null)
end = start.minus(1, ChronoUnit.MINUTES)
}
return periods.reversed()
}
private fun buildDayLabels(): List {
val formatter = DateTimeFormatter.ofPattern(
resources.getString(R.string.trackers_graph_hours_period_format)
)
val periods = mutableListOf()
var end = ZonedDateTime.now()
for (i in 1..24) {
val start = end.truncatedTo(ChronoUnit.HOURS)
periods.add("${formatter.format(start)} - ${formatter.format(end)}")
end = start.minus(1, ChronoUnit.MINUTES)
}
return periods.reversed()
}
private fun buildMonthLabels(): List {
val formater = DateTimeFormatter.ofPattern(
resources.getString(R.string.trackers_graph_days_period_format)
)
val periods = mutableListOf()
var day = ZonedDateTime.now().truncatedTo(ChronoUnit.DAYS)
for (i in 1..30) {
periods.add(formater.format(day))
day = day.minus(1, ChronoUnit.DAYS)
}
return periods.reversed()
}
private fun buildYearLabels(): List {
val formater = DateTimeFormatter.ofPattern(
resources.getString(R.string.trackers_graph_months_period_format)
)
val periods = mutableListOf()
var month = ZonedDateTime.now().truncatedTo(ChronoUnit.DAYS).withDayOfMonth(1)
for (i in 1..12) {
periods.add(formater.format(month))
month = month.minus(1, ChronoUnit.MONTHS)
}
return periods.reversed()
}
fun getDayMonthYearStatistics(): Triple {
return Triple(
TrackersPeriodicStatistics(
callsBlockedNLeaked = statisticsUseCase.getTrackersCallsOnPeriod(24, ChronoUnit.HOURS),
periods = buildDayLabels(),
trackersCount = statisticsUseCase.getActiveTrackersByPeriod(24, ChronoUnit.HOURS)
),
TrackersPeriodicStatistics(
callsBlockedNLeaked = statisticsUseCase.getTrackersCallsOnPeriod(30, ChronoUnit.DAYS),
periods = buildMonthLabels(),
trackersCount = statisticsUseCase.getActiveTrackersByPeriod(30, ChronoUnit.DAYS)
),
TrackersPeriodicStatistics(
callsBlockedNLeaked = statisticsUseCase.getTrackersCallsOnPeriod(12, ChronoUnit.MONTHS),
periods = buildYearLabels(),
trackersCount = statisticsUseCase.getActiveTrackersByPeriod(12, ChronoUnit.MONTHS)
)
)
}
suspend fun isWhiteListEmpty(app: ApplicationDescription): Boolean {
return appListsRepository.mapReduceForHiddenApps(
app = app,
map = { appDesc: ApplicationDescription ->
getWhiteList(appDesc).isEmpty()
},
reduce = { areEmpty -> areEmpty.all { it } }
)
}
suspend fun getCalls(app: ApplicationDescription): Pair {
return appListsRepository.mapReduceForHiddenApps(
app = app,
map = {
statisticsUseCase.getCalls(it, 24, ChronoUnit.HOURS)
},
reduce = { zip ->
zip.unzip().let { (blocked, leaked) ->
blocked.sum() to leaked.sum()
}
}
)
}
private fun getWhiteList(app: ApplicationDescription): List {
return whitelistRepository.getWhiteListForApp(app).mapNotNull {
trackersRepository.getTracker(it)
}
}
}