Loading app/src/main/java/foundation/e/advancedprivacy/domain/entities/WeeklyReport.kt +21 −9 Original line number Diff line number Diff line Loading @@ -17,15 +17,27 @@ package foundation.e.advancedprivacy.domain.entities import foundation.e.advancedprivacy.trackers.domain.entities.Tracker data class WeeklyReport( val appsWithNewTrackers: List<AppWithTrackers>, val trackersWithCallsChanges: List<TrackerWithCount>, val newTrackers: List<Tracker> val statType: StatType, val labelId: LabelId, val value: List<String> ) { data class AppWithTrackers( val app: DisplayableApp, val trackers: List<Tracker> ) enum class StatType { CALLS_PER_APP, NEW_TRACKER, CALLS_AND_LEAKS, TRACKER_WITH_MOST_APPS } enum class LabelId { CALLS_PER_APP_1, CALLS_PER_APP_2, CALLS_PER_APP_3, NEW_TRACKER_1, NEW_TRACKER_2, NEW_TRACKER_3, CALLS_AND_LEAKS_1, CALLS_AND_LEAKS_2, TRACKER_WITH_MOST_APPS_1 } } app/src/main/java/foundation/e/advancedprivacy/domain/usecases/TrackersAndAppsListsUseCase.kt +1 −1 Original line number Diff line number Diff line Loading @@ -60,7 +60,7 @@ class TrackersAndAppsListsUseCase( } private suspend fun get5MostTrackedAppsLastMonth(): List<AppWithCount> { val countByApIds = statsDatabase.getCallsByAppIds(since = Period.MONTH.getPeriodStart().epochSecond) val countByApIds = statsDatabase.getCallsByAppIds(start = Period.MONTH.getPeriodStart(), end = Instant.now()) val countByApps = mutableMapOf<DisplayableApp, Int>() countByApIds.forEach { (apId, count) -> Loading app/src/main/java/foundation/e/advancedprivacy/domain/usecases/WeeklyReportUseCase.kt +332 −70 Original line number Diff line number Diff line Loading @@ -23,13 +23,16 @@ import foundation.e.advancedprivacy.data.repositories.AppListRepository import foundation.e.advancedprivacy.data.repositories.ResourcesRepository import foundation.e.advancedprivacy.data.repositories.WeeklyReportLocalRepository import foundation.e.advancedprivacy.domain.entities.DisplayableApp import foundation.e.advancedprivacy.domain.entities.TrackerWithCount import foundation.e.advancedprivacy.domain.entities.WeeklyReport import foundation.e.advancedprivacy.trackers.data.StatsDatabase import foundation.e.advancedprivacy.trackers.data.TrackersRepository import foundation.e.advancedprivacy.trackers.domain.entities.Tracker import java.time.Instant import java.time.temporal.ChronoUnit import kotlin.Pair import kotlin.String import kotlin.collections.List import kotlin.random.Random import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharedFlow Loading @@ -47,51 +50,82 @@ class WeeklyReportUseCase( val showWeeklyReportNotification: SharedFlow<String> = _showWeeklyReportNotification.asSharedFlow() suspend fun updateWeeklyReport() = withContext(Dispatchers.IO) { val weeklyReport = computeWeeklyReport(Instant.now()) weeklyReportRepository.setLastWeeklyReport(weeklyReport) buildLabel(weeklyReport)?.let { _showWeeklyReportNotification.emit(it) } // val weeklyReport = computeWeeklyReport(Instant.now()) // weeklyReportRepository.setLastWeeklyReport(weeklyReport) // buildLabel(weeklyReport)?.let { // _showWeeklyReportNotification.emit(it) // } } suspend fun getLast(): WeeklyReport { return weeklyReportRepository.getLastWeeklyReport() ?: WeeklyReport(emptyList(), emptyList(), emptyList()) suspend fun getLast(): WeeklyReport? { return history.lastOrNull() // return weeklyReportRepository.getLastWeeklyReport() } suspend fun getLastLabel(): String? { return buildLabel(getLast()) } private fun WeeklyReport.LabelId.toStringId(): Int = when (this) { WeeklyReport.LabelId.CALLS_PER_APP_1 -> R.string.weeklyreport_label_calls_per_app_1 WeeklyReport.LabelId.CALLS_PER_APP_2 -> R.string.weeklyreport_label_calls_per_app_2 WeeklyReport.LabelId.CALLS_PER_APP_3 -> R.string.weeklyreport_label_calls_per_app_3 WeeklyReport.LabelId.NEW_TRACKER_1 -> R.string.weeklyreport_label_new_tracker_1 WeeklyReport.LabelId.NEW_TRACKER_2 -> R.string.weeklyreport_label_new_tracker_2 WeeklyReport.LabelId.NEW_TRACKER_3 -> R.string.weeklyreport_label_new_tracker_3 WeeklyReport.LabelId.CALLS_AND_LEAKS_1 -> R.string.weeklyreport_label_call_and_leaks_1 WeeklyReport.LabelId.CALLS_AND_LEAKS_2 -> R.string.weeklyreport_label_call_and_leaks_2 WeeklyReport.LabelId.TRACKER_WITH_MOST_APPS_1 -> R.string.weeklyreport_label_tracker_with_most_apps_1 } private fun buildLabel(weeklyReport: WeeklyReport?): String? { if (weeklyReport == null) return null val (appLabel, appTracker) = weeklyReport.appsWithNewTrackers.firstOrNull()?.let { it.app.label to it.trackers.firstOrNull()?.label } ?: return null return resourcesRepository.getString( R.string.weeklyreport_notification_description, appLabel, appTracker, weeklyReport.newTrackers.firstOrNull()?.label weeklyReport.labelId.toStringId(), *weeklyReport.value.toTypedArray() ) } suspend fun debugGenerateReportWeeksAgo(weeksAgo: Int) = withContext(Dispatchers.IO) { val endOfWeek = Instant.now().minus(weeksAgo.toLong() * 7, ChronoUnit.DAYS) val weeklyReport = computeWeeklyReport(endOfWeek) weeklyReportRepository.setLastWeeklyReport(weeklyReport) buildLabel(weeklyReport)?.let { _showWeeklyReportNotification.emit(it) // val endOfWeek = Instant.now().minus(weeksAgo.toLong() * 7, ChronoUnit.DAYS) // val weeklyReport = computeWeeklyReport(endOfWeek) // weeklyReportRepository.setLastWeeklyReport(weeklyReport) // buildLabel(weeklyReport)?.let { // _showWeeklyReportNotification.emit(it) // } } var history = mutableListOf<WeeklyReport>() suspend fun debugGenerateReporsSinceWeeksAgo(weeksAgo: Int) = withContext(Dispatchers.IO) { history = mutableListOf<WeeklyReport>() val start = System.currentTimeMillis() (weeksAgo downTo 0).map { val endOfWeek = Instant.now().minus(it.toLong() * 7, ChronoUnit.DAYS) val weeklyReport = chooseBestStatAndLabelToDisplay(computeStats(endOfWeek)) history.add(weeklyReport) } private suspend fun computeWeeklyReport(endOfWeek: Instant): WeeklyReport = withContext(Dispatchers.IO) { Log.e("DEBUG-weekly", "buildWeeklyReport") Log.d("Debug-weekly", history.mapIndexed { index, report -> "week $index : ${buildLabel(report)}" }.joinToString("\n")) Log.d("Debug-weekly", "computed reports in : ${System.currentTimeMillis() - start}") } private data class Stats( val newTrackersDetected: List<Pair<String, String>>, val callsPerApp: Map<String, Int>, val callsAndLeaks: Pair<Int, Int>, val trackerWithMostApp: Pair<String, Int> ) private suspend fun computeStats(endOfWeek: Instant): Stats { Log.e("DEBUG-weekly", "computeStats") val startOfWeek = endOfWeek.minus(7, ChronoUnit.DAYS) // * New trackers detected // **Stat**: Trackers never seen, and the apps that bring them. val startOfYear = endOfWeek.minus(365, ChronoUnit.DAYS) val trackerAppsHistoric = // mapIdsToEntities( statsDatabase.getDistinctTrackerAndApp( Loading @@ -107,66 +141,294 @@ class WeeklyReportUseCase( ) // ) val newTrackerIds = trackerAppsOfWeek.map { it.first }.toSet() - trackerAppsHistoric.map { it.first }.toSet() Log.d("DebugReport", "$endOfWeek : newTrackers: $newTrackerIds") val historicTrackers = trackerAppsHistoric.map { it.first }.toSet() val newTrackersDetected = trackerAppsOfWeek.filter { it.first !in historicTrackers } // * Leaking rate (average on the week of the number of leak attempts per hour for an app) // **Stat** : most calls for an app last week, then divide by 7 or 7*24 // We don't handle correctly system app and compatibility app here. val callsPerApp = statsDatabase.getCallsByAppIds(startOfWeek, endOfWeek) // * Advanced Privacy efficiency: // **Stat**: call and leaks this week val callsAndLeaks = statsDatabase.getCallsAndLeaks(startOfWeek, endOfWeek) // **Stat**: tracker with most apps. // We don't handle correctly system app and compatibility app here. val trackerWithMostApp = (trackerAppsOfWeek + trackerAppsHistoric).groupBy { it.first }.maxBy { it.value.size }.let { it.key to it.value.size } val newTrackers = newTrackerIds.mapNotNull { trackersRepository.getTracker(it) return Stats( newTrackersDetected = newTrackersDetected, callsPerApp = callsPerApp, callsAndLeaks = callsAndLeaks, trackerWithMostApp = trackerWithMostApp ) } val newTrackerApps = (trackerAppsOfWeek.toSet() - trackerAppsHistoric.toSet()) data class WeeklyReportScore( val weeklyReport: WeeklyReport, val score: Long ) val appsWithNewTrackers = newTrackerApps.groupBy { it.second } .mapValues { it.value.map { it.first } }.let { Log.d("DebugReport", "$endOfWeek : appsWithNewTrackers: $it") it fun computeScore(weeklyReport: WeeklyReport, history: List<WeeklyReport>): WeeklyReportScore { // 52 weeks, 2 years --> 100 val baseShift = 100L val statShift = baseShift // 10^2 val statLabelShift = baseShift * statShift // 10^4 val statValueShift = baseShift * statLabelShift // 10^6 val statLabelValueShift = baseShift * statValueShift // 10^8 val statLabelAllValueShift = baseShift * statLabelValueShift // 10^10 var score = Random.nextLong(99) val statLabelAllValueRank = history.indexOfLast { it.statType == weeklyReport.statType && it.labelId == weeklyReport.labelId && it.value == weeklyReport.value } .mapNotNull { appListRepository.getInternetAppByApId(it.key)?.let { app -> WeeklyReport.AppWithTrackers( app = app, trackers = it.value.mapNotNull { trackerId -> trackersRepository.getTracker(trackerId) } if (statLabelAllValueRank != -1) { score += (statLabelAllValueRank + 1) * statLabelAllValueShift } val statLabelValueRank = history.indexOfLast { it.statType == weeklyReport.statType && it.labelId == weeklyReport.labelId && it.value.first() == weeklyReport.value.first() } if (statLabelValueRank != -1) { score += (statLabelValueRank + 1) * statLabelValueShift } val statValueRank = history.indexOfLast { it.statType == weeklyReport.statType && it.value.first() == weeklyReport.value.first() } if (statValueRank != -1) { score += (statValueRank + 1) * statValueShift } val statLabelRank = history.indexOfLast { it.statType == weeklyReport.statType && it.labelId == weeklyReport.labelId } if (statLabelRank != -1) { score += (statLabelRank + 1) * statLabelShift } val statRank = history.indexOfLast { it.statType == weeklyReport.statType } if (statRank != -1) { score += (statRank + 1) * statShift } return WeeklyReportScore( weeklyReport = weeklyReport, score = score ) } private suspend fun chooseBestStatAndLabelToDisplay(weekStats: Stats): WeeklyReport { // prepare state, label, value triples val candidates = mutableListOf<WeeklyReport>() // TODO, could move to "APP_WITH_MOST_CALLS" val (appWithMostCalls, calls) = weekStats.callsPerApp.maxBy { it.value }.let { (key, value) -> appListRepository.getAppById(key)?.label?.toString() to value } val hoursInWeek = 7 * 24 // val newTrackers = newTrackerApps.map { it.first }.distinct() // Log.d("DebugReport", "$endOfWeek : newTrackers: $newTrackers") Log.d("Debug-weekly", "callsPerApp: appWithMostCalls $appWithMostCalls calls: $calls ; ${weekStats.callsPerApp}") val maxCallsByTrackersHistory = statsDatabase.getMaxCallsByTrackerByWeek(startOfYear, startOfWeek) val maxCallsByTrackerOfWeek = statsDatabase.getCallsByTrackers(startOfWeek, endOfWeek) if (appWithMostCalls != null && calls > hoursInWeek) { candidates.add( WeeklyReport( WeeklyReport.StatType.CALLS_PER_APP, WeeklyReport.LabelId.CALLS_PER_APP_1, listOf(appWithMostCalls, (calls / hoursInWeek).toString()) ) ) val trackersWithCallsChanges = maxCallsByTrackerOfWeek.mapNotNull { kv -> val trackerId: String = kv.key val calls = kv.value.first candidates.add( WeeklyReport( WeeklyReport.StatType.CALLS_PER_APP, WeeklyReport.LabelId.CALLS_PER_APP_2, listOf(appWithMostCalls, (calls / hoursInWeek).toString()) ) ) } val historyCalls = maxCallsByTrackersHistory.get(trackerId)?.first if (appWithMostCalls != null && calls > 7) { candidates.add( WeeklyReport( WeeklyReport.StatType.CALLS_PER_APP, WeeklyReport.LabelId.CALLS_PER_APP_3, listOf(appWithMostCalls, (calls / 7).toString()) ) ) } if (historyCalls != null && historyCalls > 0 && calls > (2 * historyCalls)) { trackersRepository.getTracker(trackerId)?.let { TrackerWithCount( it, (100 * (calls - historyCalls)) / historyCalls weekStats.newTrackersDetected.map { it.second }.toSet().forEach { appId -> // val tracker = trackersRepository.getTracker(trackerId) val displayableApp = appListRepository.getAppById(appId) if (displayableApp != null // TODO dummy apps ?&& displayableApp != appListRepository. ) { candidates.add( WeeklyReport( WeeklyReport.StatType.NEW_TRACKER, WeeklyReport.LabelId.NEW_TRACKER_1, listOf(displayableApp.label.toString(), "") ) ) } } else { null } }.let { Log.d("DebugReport", "$endOfWeek : trackersWithCallsChanges: ${it.map { it.tracker.id }}") it weekStats.newTrackersDetected.groupBy { it.first }.keys.size.takeIf { it > 0 }?.let { trackersCount -> candidates.add( WeeklyReport( WeeklyReport.StatType.NEW_TRACKER, WeeklyReport.LabelId.NEW_TRACKER_2, listOf("", trackersCount.toString()) ) ) } weekStats.newTrackersDetected.groupBy { it.second }.keys.size.takeIf { it > 0 }?.let { appsCount -> candidates.add( WeeklyReport( WeeklyReport.StatType.NEW_TRACKER, WeeklyReport.LabelId.NEW_TRACKER_3, listOf("", appsCount.toString()) ) ) } // Log.e("DEBUG-weekly", "buildWeeklyReport: appsWithNewTrackers: $appsWithNewTrackers") // if (weekStats.callsAndLeaks.first > 0) { val blockedRate = ( ((weekStats.callsAndLeaks.first - weekStats.callsAndLeaks.second) * 100) / weekStats.callsAndLeaks.first ).toString() // val calls = weekStats.callsAndLeaks.first.toString() val leaks = weekStats.callsAndLeaks.second.toString() candidates.add( WeeklyReport( WeeklyReport.StatType.CALLS_AND_LEAKS, WeeklyReport.LabelId.CALLS_AND_LEAKS_1, listOf(blockedRate, leaks) ) ) candidates.add( WeeklyReport( WeeklyReport.StatType.CALLS_AND_LEAKS, WeeklyReport.LabelId.CALLS_AND_LEAKS_2, listOf(blockedRate, leaks) ) ) } val tracker = trackersRepository.getTracker(weekStats.trackerWithMostApp.first) val appCount = weekStats.trackerWithMostApp.second if (tracker != null && appCount > 1) { candidates.add( WeeklyReport( appsWithNewTrackers = appsWithNewTrackers, trackersWithCallsChanges = trackersWithCallsChanges, newTrackers = newTrackers WeeklyReport.StatType.TRACKER_WITH_MOST_APPS, WeeklyReport.LabelId.TRACKER_WITH_MOST_APPS_1, listOf(tracker.label, appCount.toString()) ) ) } // Sort candidates, regarding history. return candidates.map { computeScore(it, history) }.sortedBy { it.score }.let { val scores = it.map { "${it.score} for: ${buildLabel(it.weeklyReport) }" } Log.d("Debug-weekly", "Computed scores : \n${scores.joinToString("\n")}") it } .first().weeklyReport } // private suspend fun computeWeeklyReport(endOfWeek: Instant): WeeklyReport = withContext(Dispatchers.IO) { // Log.e("DEBUG-weekly", "buildWeeklyReport") // val startOfWeek = endOfWeek.minus(7, ChronoUnit.DAYS) // // val startOfYear = endOfWeek.minus(365, ChronoUnit.DAYS) // val trackerAppsHistoric = // mapIdsToEntities( // statsDatabase.getDistinctTrackerAndApp( // periodStart = startOfYear, // periodEnd = startOfWeek // ) // // ) // // val trackerAppsOfWeek = // mapIdsToEntities( // statsDatabase.getDistinctTrackerAndApp( // periodStart = startOfWeek, // periodEnd = endOfWeek // ) // // ) // // val newTrackerIds = trackerAppsOfWeek.map { it.first }.toSet() - trackerAppsHistoric.map { it.first }.toSet() // Log.d("DebugReport", "$endOfWeek : newTrackers: $newTrackerIds") // // val newTrackers = newTrackerIds.mapNotNull { // trackersRepository.getTracker(it) // } // // val newTrackerApps = (trackerAppsOfWeek.toSet() - trackerAppsHistoric.toSet()) // // val appsWithNewTrackers = newTrackerApps.groupBy { it.second } // .mapValues { // it.value.map { it.first } // }.let { // Log.d("DebugReport", "$endOfWeek : appsWithNewTrackers: $it") // it // } // .mapNotNull { // appListRepository.getInternetAppByApId(it.key)?.let { app -> // WeeklyReport.AppWithTrackers( // app = app, // trackers = it.value.mapNotNull { trackerId -> trackersRepository.getTracker(trackerId) } // ) // } // } // // // val newTrackers = newTrackerApps.map { it.first }.distinct() // // Log.d("DebugReport", "$endOfWeek : newTrackers: $newTrackers") // // val maxCallsByTrackersHistory = statsDatabase.getMaxCallsByTrackerByWeek(startOfYear, startOfWeek) // val maxCallsByTrackerOfWeek = statsDatabase.getCallsByTrackers(startOfWeek, endOfWeek) // // val trackersWithCallsChanges = maxCallsByTrackerOfWeek.mapNotNull { kv -> // val trackerId: String = kv.key // val calls = kv.value.first // // val historyCalls = maxCallsByTrackersHistory.get(trackerId)?.first // // if (historyCalls != null && historyCalls > 0 && calls > (2 * historyCalls)) { // trackersRepository.getTracker(trackerId)?.let { // TrackerWithCount( // it, // (100 * (calls - historyCalls)) / historyCalls // ) // } // } else { // null // } // }.let { // Log.d("DebugReport", "$endOfWeek : trackersWithCallsChanges: ${it.map { it.tracker.id }}") // it // } // // Log.e("DEBUG-weekly", "buildWeeklyReport: appsWithNewTrackers: $appsWithNewTrackers") // // WeeklyReport( // appsWithNewTrackers = appsWithNewTrackers, // trackersWithCallsChanges = trackersWithCallsChanges, // newTrackers = newTrackers // ) // } // TODO: Z; duplicate from TrackersAndAppsListsUsecase private suspend fun mapIdsToEntities(trackersAndAppsIds: List<Pair<String, String>>): List<Pair<Tracker, DisplayableApp>> { return trackersAndAppsIds.mapNotNull { (trackerId, apId) -> Loading app/src/main/java/foundation/e/advancedprivacy/features/weeklyreport/WeeklyReportFragment.kt +77 −89 File changed.Preview size limit exceeded, changes collapsed. Show changes app/src/main/java/foundation/e/advancedprivacy/features/weeklyreport/WeeklyReportViewModel.kt +5 −11 Original line number Diff line number Diff line Loading @@ -19,7 +19,6 @@ package foundation.e.advancedprivacy.features.weeklyreport import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import foundation.e.advancedprivacy.domain.entities.WeeklyReport import foundation.e.advancedprivacy.domain.usecases.WeeklyReportUseCase import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow Loading @@ -32,13 +31,7 @@ class WeeklyReportViewModel( private val weeklyReportUseCase: WeeklyReportUseCase ) : ViewModel() { private val _state = MutableStateFlow( WeeklyReportUiState( WeeklyReport( appsWithNewTrackers = emptyList(), trackersWithCallsChanges = emptyList(), newTrackers = emptyList() ) ) WeeklyReportUiState(weeklyReport = "") ) val state = _state.asStateFlow() Loading @@ -51,7 +44,7 @@ class WeeklyReportViewModel( suspend fun doOnStartedState() = withContext(Dispatchers.IO) { _state.update { WeeklyReportUiState(weeklyReportUseCase.getLast()) WeeklyReportUiState(weeklyReportUseCase.getLastLabel() ?: "NONE") } } Loading @@ -61,11 +54,12 @@ class WeeklyReportViewModel( } fun onClickDebugGenerateReport(weekAgo: Int) = viewModelScope.launch(Dispatchers.IO) { weeklyReportUseCase.debugGenerateReportWeeksAgo(weekAgo) // weeklyReportUseCase.debugGenerateReportWeeksAgo(weekAgo) weeklyReportUseCase.debugGenerateReporsSinceWeeksAgo(weekAgo) doOnStartedState() } } data class WeeklyReportUiState( val weeklyReport: WeeklyReport val weeklyReport: String ) Loading
app/src/main/java/foundation/e/advancedprivacy/domain/entities/WeeklyReport.kt +21 −9 Original line number Diff line number Diff line Loading @@ -17,15 +17,27 @@ package foundation.e.advancedprivacy.domain.entities import foundation.e.advancedprivacy.trackers.domain.entities.Tracker data class WeeklyReport( val appsWithNewTrackers: List<AppWithTrackers>, val trackersWithCallsChanges: List<TrackerWithCount>, val newTrackers: List<Tracker> val statType: StatType, val labelId: LabelId, val value: List<String> ) { data class AppWithTrackers( val app: DisplayableApp, val trackers: List<Tracker> ) enum class StatType { CALLS_PER_APP, NEW_TRACKER, CALLS_AND_LEAKS, TRACKER_WITH_MOST_APPS } enum class LabelId { CALLS_PER_APP_1, CALLS_PER_APP_2, CALLS_PER_APP_3, NEW_TRACKER_1, NEW_TRACKER_2, NEW_TRACKER_3, CALLS_AND_LEAKS_1, CALLS_AND_LEAKS_2, TRACKER_WITH_MOST_APPS_1 } }
app/src/main/java/foundation/e/advancedprivacy/domain/usecases/TrackersAndAppsListsUseCase.kt +1 −1 Original line number Diff line number Diff line Loading @@ -60,7 +60,7 @@ class TrackersAndAppsListsUseCase( } private suspend fun get5MostTrackedAppsLastMonth(): List<AppWithCount> { val countByApIds = statsDatabase.getCallsByAppIds(since = Period.MONTH.getPeriodStart().epochSecond) val countByApIds = statsDatabase.getCallsByAppIds(start = Period.MONTH.getPeriodStart(), end = Instant.now()) val countByApps = mutableMapOf<DisplayableApp, Int>() countByApIds.forEach { (apId, count) -> Loading
app/src/main/java/foundation/e/advancedprivacy/domain/usecases/WeeklyReportUseCase.kt +332 −70 Original line number Diff line number Diff line Loading @@ -23,13 +23,16 @@ import foundation.e.advancedprivacy.data.repositories.AppListRepository import foundation.e.advancedprivacy.data.repositories.ResourcesRepository import foundation.e.advancedprivacy.data.repositories.WeeklyReportLocalRepository import foundation.e.advancedprivacy.domain.entities.DisplayableApp import foundation.e.advancedprivacy.domain.entities.TrackerWithCount import foundation.e.advancedprivacy.domain.entities.WeeklyReport import foundation.e.advancedprivacy.trackers.data.StatsDatabase import foundation.e.advancedprivacy.trackers.data.TrackersRepository import foundation.e.advancedprivacy.trackers.domain.entities.Tracker import java.time.Instant import java.time.temporal.ChronoUnit import kotlin.Pair import kotlin.String import kotlin.collections.List import kotlin.random.Random import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharedFlow Loading @@ -47,51 +50,82 @@ class WeeklyReportUseCase( val showWeeklyReportNotification: SharedFlow<String> = _showWeeklyReportNotification.asSharedFlow() suspend fun updateWeeklyReport() = withContext(Dispatchers.IO) { val weeklyReport = computeWeeklyReport(Instant.now()) weeklyReportRepository.setLastWeeklyReport(weeklyReport) buildLabel(weeklyReport)?.let { _showWeeklyReportNotification.emit(it) } // val weeklyReport = computeWeeklyReport(Instant.now()) // weeklyReportRepository.setLastWeeklyReport(weeklyReport) // buildLabel(weeklyReport)?.let { // _showWeeklyReportNotification.emit(it) // } } suspend fun getLast(): WeeklyReport { return weeklyReportRepository.getLastWeeklyReport() ?: WeeklyReport(emptyList(), emptyList(), emptyList()) suspend fun getLast(): WeeklyReport? { return history.lastOrNull() // return weeklyReportRepository.getLastWeeklyReport() } suspend fun getLastLabel(): String? { return buildLabel(getLast()) } private fun WeeklyReport.LabelId.toStringId(): Int = when (this) { WeeklyReport.LabelId.CALLS_PER_APP_1 -> R.string.weeklyreport_label_calls_per_app_1 WeeklyReport.LabelId.CALLS_PER_APP_2 -> R.string.weeklyreport_label_calls_per_app_2 WeeklyReport.LabelId.CALLS_PER_APP_3 -> R.string.weeklyreport_label_calls_per_app_3 WeeklyReport.LabelId.NEW_TRACKER_1 -> R.string.weeklyreport_label_new_tracker_1 WeeklyReport.LabelId.NEW_TRACKER_2 -> R.string.weeklyreport_label_new_tracker_2 WeeklyReport.LabelId.NEW_TRACKER_3 -> R.string.weeklyreport_label_new_tracker_3 WeeklyReport.LabelId.CALLS_AND_LEAKS_1 -> R.string.weeklyreport_label_call_and_leaks_1 WeeklyReport.LabelId.CALLS_AND_LEAKS_2 -> R.string.weeklyreport_label_call_and_leaks_2 WeeklyReport.LabelId.TRACKER_WITH_MOST_APPS_1 -> R.string.weeklyreport_label_tracker_with_most_apps_1 } private fun buildLabel(weeklyReport: WeeklyReport?): String? { if (weeklyReport == null) return null val (appLabel, appTracker) = weeklyReport.appsWithNewTrackers.firstOrNull()?.let { it.app.label to it.trackers.firstOrNull()?.label } ?: return null return resourcesRepository.getString( R.string.weeklyreport_notification_description, appLabel, appTracker, weeklyReport.newTrackers.firstOrNull()?.label weeklyReport.labelId.toStringId(), *weeklyReport.value.toTypedArray() ) } suspend fun debugGenerateReportWeeksAgo(weeksAgo: Int) = withContext(Dispatchers.IO) { val endOfWeek = Instant.now().minus(weeksAgo.toLong() * 7, ChronoUnit.DAYS) val weeklyReport = computeWeeklyReport(endOfWeek) weeklyReportRepository.setLastWeeklyReport(weeklyReport) buildLabel(weeklyReport)?.let { _showWeeklyReportNotification.emit(it) // val endOfWeek = Instant.now().minus(weeksAgo.toLong() * 7, ChronoUnit.DAYS) // val weeklyReport = computeWeeklyReport(endOfWeek) // weeklyReportRepository.setLastWeeklyReport(weeklyReport) // buildLabel(weeklyReport)?.let { // _showWeeklyReportNotification.emit(it) // } } var history = mutableListOf<WeeklyReport>() suspend fun debugGenerateReporsSinceWeeksAgo(weeksAgo: Int) = withContext(Dispatchers.IO) { history = mutableListOf<WeeklyReport>() val start = System.currentTimeMillis() (weeksAgo downTo 0).map { val endOfWeek = Instant.now().minus(it.toLong() * 7, ChronoUnit.DAYS) val weeklyReport = chooseBestStatAndLabelToDisplay(computeStats(endOfWeek)) history.add(weeklyReport) } private suspend fun computeWeeklyReport(endOfWeek: Instant): WeeklyReport = withContext(Dispatchers.IO) { Log.e("DEBUG-weekly", "buildWeeklyReport") Log.d("Debug-weekly", history.mapIndexed { index, report -> "week $index : ${buildLabel(report)}" }.joinToString("\n")) Log.d("Debug-weekly", "computed reports in : ${System.currentTimeMillis() - start}") } private data class Stats( val newTrackersDetected: List<Pair<String, String>>, val callsPerApp: Map<String, Int>, val callsAndLeaks: Pair<Int, Int>, val trackerWithMostApp: Pair<String, Int> ) private suspend fun computeStats(endOfWeek: Instant): Stats { Log.e("DEBUG-weekly", "computeStats") val startOfWeek = endOfWeek.minus(7, ChronoUnit.DAYS) // * New trackers detected // **Stat**: Trackers never seen, and the apps that bring them. val startOfYear = endOfWeek.minus(365, ChronoUnit.DAYS) val trackerAppsHistoric = // mapIdsToEntities( statsDatabase.getDistinctTrackerAndApp( Loading @@ -107,66 +141,294 @@ class WeeklyReportUseCase( ) // ) val newTrackerIds = trackerAppsOfWeek.map { it.first }.toSet() - trackerAppsHistoric.map { it.first }.toSet() Log.d("DebugReport", "$endOfWeek : newTrackers: $newTrackerIds") val historicTrackers = trackerAppsHistoric.map { it.first }.toSet() val newTrackersDetected = trackerAppsOfWeek.filter { it.first !in historicTrackers } // * Leaking rate (average on the week of the number of leak attempts per hour for an app) // **Stat** : most calls for an app last week, then divide by 7 or 7*24 // We don't handle correctly system app and compatibility app here. val callsPerApp = statsDatabase.getCallsByAppIds(startOfWeek, endOfWeek) // * Advanced Privacy efficiency: // **Stat**: call and leaks this week val callsAndLeaks = statsDatabase.getCallsAndLeaks(startOfWeek, endOfWeek) // **Stat**: tracker with most apps. // We don't handle correctly system app and compatibility app here. val trackerWithMostApp = (trackerAppsOfWeek + trackerAppsHistoric).groupBy { it.first }.maxBy { it.value.size }.let { it.key to it.value.size } val newTrackers = newTrackerIds.mapNotNull { trackersRepository.getTracker(it) return Stats( newTrackersDetected = newTrackersDetected, callsPerApp = callsPerApp, callsAndLeaks = callsAndLeaks, trackerWithMostApp = trackerWithMostApp ) } val newTrackerApps = (trackerAppsOfWeek.toSet() - trackerAppsHistoric.toSet()) data class WeeklyReportScore( val weeklyReport: WeeklyReport, val score: Long ) val appsWithNewTrackers = newTrackerApps.groupBy { it.second } .mapValues { it.value.map { it.first } }.let { Log.d("DebugReport", "$endOfWeek : appsWithNewTrackers: $it") it fun computeScore(weeklyReport: WeeklyReport, history: List<WeeklyReport>): WeeklyReportScore { // 52 weeks, 2 years --> 100 val baseShift = 100L val statShift = baseShift // 10^2 val statLabelShift = baseShift * statShift // 10^4 val statValueShift = baseShift * statLabelShift // 10^6 val statLabelValueShift = baseShift * statValueShift // 10^8 val statLabelAllValueShift = baseShift * statLabelValueShift // 10^10 var score = Random.nextLong(99) val statLabelAllValueRank = history.indexOfLast { it.statType == weeklyReport.statType && it.labelId == weeklyReport.labelId && it.value == weeklyReport.value } .mapNotNull { appListRepository.getInternetAppByApId(it.key)?.let { app -> WeeklyReport.AppWithTrackers( app = app, trackers = it.value.mapNotNull { trackerId -> trackersRepository.getTracker(trackerId) } if (statLabelAllValueRank != -1) { score += (statLabelAllValueRank + 1) * statLabelAllValueShift } val statLabelValueRank = history.indexOfLast { it.statType == weeklyReport.statType && it.labelId == weeklyReport.labelId && it.value.first() == weeklyReport.value.first() } if (statLabelValueRank != -1) { score += (statLabelValueRank + 1) * statLabelValueShift } val statValueRank = history.indexOfLast { it.statType == weeklyReport.statType && it.value.first() == weeklyReport.value.first() } if (statValueRank != -1) { score += (statValueRank + 1) * statValueShift } val statLabelRank = history.indexOfLast { it.statType == weeklyReport.statType && it.labelId == weeklyReport.labelId } if (statLabelRank != -1) { score += (statLabelRank + 1) * statLabelShift } val statRank = history.indexOfLast { it.statType == weeklyReport.statType } if (statRank != -1) { score += (statRank + 1) * statShift } return WeeklyReportScore( weeklyReport = weeklyReport, score = score ) } private suspend fun chooseBestStatAndLabelToDisplay(weekStats: Stats): WeeklyReport { // prepare state, label, value triples val candidates = mutableListOf<WeeklyReport>() // TODO, could move to "APP_WITH_MOST_CALLS" val (appWithMostCalls, calls) = weekStats.callsPerApp.maxBy { it.value }.let { (key, value) -> appListRepository.getAppById(key)?.label?.toString() to value } val hoursInWeek = 7 * 24 // val newTrackers = newTrackerApps.map { it.first }.distinct() // Log.d("DebugReport", "$endOfWeek : newTrackers: $newTrackers") Log.d("Debug-weekly", "callsPerApp: appWithMostCalls $appWithMostCalls calls: $calls ; ${weekStats.callsPerApp}") val maxCallsByTrackersHistory = statsDatabase.getMaxCallsByTrackerByWeek(startOfYear, startOfWeek) val maxCallsByTrackerOfWeek = statsDatabase.getCallsByTrackers(startOfWeek, endOfWeek) if (appWithMostCalls != null && calls > hoursInWeek) { candidates.add( WeeklyReport( WeeklyReport.StatType.CALLS_PER_APP, WeeklyReport.LabelId.CALLS_PER_APP_1, listOf(appWithMostCalls, (calls / hoursInWeek).toString()) ) ) val trackersWithCallsChanges = maxCallsByTrackerOfWeek.mapNotNull { kv -> val trackerId: String = kv.key val calls = kv.value.first candidates.add( WeeklyReport( WeeklyReport.StatType.CALLS_PER_APP, WeeklyReport.LabelId.CALLS_PER_APP_2, listOf(appWithMostCalls, (calls / hoursInWeek).toString()) ) ) } val historyCalls = maxCallsByTrackersHistory.get(trackerId)?.first if (appWithMostCalls != null && calls > 7) { candidates.add( WeeklyReport( WeeklyReport.StatType.CALLS_PER_APP, WeeklyReport.LabelId.CALLS_PER_APP_3, listOf(appWithMostCalls, (calls / 7).toString()) ) ) } if (historyCalls != null && historyCalls > 0 && calls > (2 * historyCalls)) { trackersRepository.getTracker(trackerId)?.let { TrackerWithCount( it, (100 * (calls - historyCalls)) / historyCalls weekStats.newTrackersDetected.map { it.second }.toSet().forEach { appId -> // val tracker = trackersRepository.getTracker(trackerId) val displayableApp = appListRepository.getAppById(appId) if (displayableApp != null // TODO dummy apps ?&& displayableApp != appListRepository. ) { candidates.add( WeeklyReport( WeeklyReport.StatType.NEW_TRACKER, WeeklyReport.LabelId.NEW_TRACKER_1, listOf(displayableApp.label.toString(), "") ) ) } } else { null } }.let { Log.d("DebugReport", "$endOfWeek : trackersWithCallsChanges: ${it.map { it.tracker.id }}") it weekStats.newTrackersDetected.groupBy { it.first }.keys.size.takeIf { it > 0 }?.let { trackersCount -> candidates.add( WeeklyReport( WeeklyReport.StatType.NEW_TRACKER, WeeklyReport.LabelId.NEW_TRACKER_2, listOf("", trackersCount.toString()) ) ) } weekStats.newTrackersDetected.groupBy { it.second }.keys.size.takeIf { it > 0 }?.let { appsCount -> candidates.add( WeeklyReport( WeeklyReport.StatType.NEW_TRACKER, WeeklyReport.LabelId.NEW_TRACKER_3, listOf("", appsCount.toString()) ) ) } // Log.e("DEBUG-weekly", "buildWeeklyReport: appsWithNewTrackers: $appsWithNewTrackers") // if (weekStats.callsAndLeaks.first > 0) { val blockedRate = ( ((weekStats.callsAndLeaks.first - weekStats.callsAndLeaks.second) * 100) / weekStats.callsAndLeaks.first ).toString() // val calls = weekStats.callsAndLeaks.first.toString() val leaks = weekStats.callsAndLeaks.second.toString() candidates.add( WeeklyReport( WeeklyReport.StatType.CALLS_AND_LEAKS, WeeklyReport.LabelId.CALLS_AND_LEAKS_1, listOf(blockedRate, leaks) ) ) candidates.add( WeeklyReport( WeeklyReport.StatType.CALLS_AND_LEAKS, WeeklyReport.LabelId.CALLS_AND_LEAKS_2, listOf(blockedRate, leaks) ) ) } val tracker = trackersRepository.getTracker(weekStats.trackerWithMostApp.first) val appCount = weekStats.trackerWithMostApp.second if (tracker != null && appCount > 1) { candidates.add( WeeklyReport( appsWithNewTrackers = appsWithNewTrackers, trackersWithCallsChanges = trackersWithCallsChanges, newTrackers = newTrackers WeeklyReport.StatType.TRACKER_WITH_MOST_APPS, WeeklyReport.LabelId.TRACKER_WITH_MOST_APPS_1, listOf(tracker.label, appCount.toString()) ) ) } // Sort candidates, regarding history. return candidates.map { computeScore(it, history) }.sortedBy { it.score }.let { val scores = it.map { "${it.score} for: ${buildLabel(it.weeklyReport) }" } Log.d("Debug-weekly", "Computed scores : \n${scores.joinToString("\n")}") it } .first().weeklyReport } // private suspend fun computeWeeklyReport(endOfWeek: Instant): WeeklyReport = withContext(Dispatchers.IO) { // Log.e("DEBUG-weekly", "buildWeeklyReport") // val startOfWeek = endOfWeek.minus(7, ChronoUnit.DAYS) // // val startOfYear = endOfWeek.minus(365, ChronoUnit.DAYS) // val trackerAppsHistoric = // mapIdsToEntities( // statsDatabase.getDistinctTrackerAndApp( // periodStart = startOfYear, // periodEnd = startOfWeek // ) // // ) // // val trackerAppsOfWeek = // mapIdsToEntities( // statsDatabase.getDistinctTrackerAndApp( // periodStart = startOfWeek, // periodEnd = endOfWeek // ) // // ) // // val newTrackerIds = trackerAppsOfWeek.map { it.first }.toSet() - trackerAppsHistoric.map { it.first }.toSet() // Log.d("DebugReport", "$endOfWeek : newTrackers: $newTrackerIds") // // val newTrackers = newTrackerIds.mapNotNull { // trackersRepository.getTracker(it) // } // // val newTrackerApps = (trackerAppsOfWeek.toSet() - trackerAppsHistoric.toSet()) // // val appsWithNewTrackers = newTrackerApps.groupBy { it.second } // .mapValues { // it.value.map { it.first } // }.let { // Log.d("DebugReport", "$endOfWeek : appsWithNewTrackers: $it") // it // } // .mapNotNull { // appListRepository.getInternetAppByApId(it.key)?.let { app -> // WeeklyReport.AppWithTrackers( // app = app, // trackers = it.value.mapNotNull { trackerId -> trackersRepository.getTracker(trackerId) } // ) // } // } // // // val newTrackers = newTrackerApps.map { it.first }.distinct() // // Log.d("DebugReport", "$endOfWeek : newTrackers: $newTrackers") // // val maxCallsByTrackersHistory = statsDatabase.getMaxCallsByTrackerByWeek(startOfYear, startOfWeek) // val maxCallsByTrackerOfWeek = statsDatabase.getCallsByTrackers(startOfWeek, endOfWeek) // // val trackersWithCallsChanges = maxCallsByTrackerOfWeek.mapNotNull { kv -> // val trackerId: String = kv.key // val calls = kv.value.first // // val historyCalls = maxCallsByTrackersHistory.get(trackerId)?.first // // if (historyCalls != null && historyCalls > 0 && calls > (2 * historyCalls)) { // trackersRepository.getTracker(trackerId)?.let { // TrackerWithCount( // it, // (100 * (calls - historyCalls)) / historyCalls // ) // } // } else { // null // } // }.let { // Log.d("DebugReport", "$endOfWeek : trackersWithCallsChanges: ${it.map { it.tracker.id }}") // it // } // // Log.e("DEBUG-weekly", "buildWeeklyReport: appsWithNewTrackers: $appsWithNewTrackers") // // WeeklyReport( // appsWithNewTrackers = appsWithNewTrackers, // trackersWithCallsChanges = trackersWithCallsChanges, // newTrackers = newTrackers // ) // } // TODO: Z; duplicate from TrackersAndAppsListsUsecase private suspend fun mapIdsToEntities(trackersAndAppsIds: List<Pair<String, String>>): List<Pair<Tracker, DisplayableApp>> { return trackersAndAppsIds.mapNotNull { (trackerId, apId) -> Loading
app/src/main/java/foundation/e/advancedprivacy/features/weeklyreport/WeeklyReportFragment.kt +77 −89 File changed.Preview size limit exceeded, changes collapsed. Show changes
app/src/main/java/foundation/e/advancedprivacy/features/weeklyreport/WeeklyReportViewModel.kt +5 −11 Original line number Diff line number Diff line Loading @@ -19,7 +19,6 @@ package foundation.e.advancedprivacy.features.weeklyreport import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import foundation.e.advancedprivacy.domain.entities.WeeklyReport import foundation.e.advancedprivacy.domain.usecases.WeeklyReportUseCase import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow Loading @@ -32,13 +31,7 @@ class WeeklyReportViewModel( private val weeklyReportUseCase: WeeklyReportUseCase ) : ViewModel() { private val _state = MutableStateFlow( WeeklyReportUiState( WeeklyReport( appsWithNewTrackers = emptyList(), trackersWithCallsChanges = emptyList(), newTrackers = emptyList() ) ) WeeklyReportUiState(weeklyReport = "") ) val state = _state.asStateFlow() Loading @@ -51,7 +44,7 @@ class WeeklyReportViewModel( suspend fun doOnStartedState() = withContext(Dispatchers.IO) { _state.update { WeeklyReportUiState(weeklyReportUseCase.getLast()) WeeklyReportUiState(weeklyReportUseCase.getLastLabel() ?: "NONE") } } Loading @@ -61,11 +54,12 @@ class WeeklyReportViewModel( } fun onClickDebugGenerateReport(weekAgo: Int) = viewModelScope.launch(Dispatchers.IO) { weeklyReportUseCase.debugGenerateReportWeeksAgo(weekAgo) // weeklyReportUseCase.debugGenerateReportWeeksAgo(weekAgo) weeklyReportUseCase.debugGenerateReporsSinceWeeksAgo(weekAgo) doOnStartedState() } } data class WeeklyReportUiState( val weeklyReport: WeeklyReport val weeklyReport: String )