Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit ed824732 authored by Guillaume Jacquart's avatar Guillaume Jacquart
Browse files

WIP industrialization

parent 028f177e
Loading
Loading
Loading
Loading
+25 −71
Original line number Diff line number Diff line
@@ -32,97 +32,51 @@ import com.google.gson.JsonObject
import com.google.gson.JsonSerializationContext
import com.google.gson.JsonSerializer
import foundation.e.advancedprivacy.core.utils.getValue
import foundation.e.advancedprivacy.core.utils.removeKey
import foundation.e.advancedprivacy.core.utils.setValue
import foundation.e.advancedprivacy.domain.entities.DisplayableApp
import foundation.e.advancedprivacy.domain.entities.WeeklyReport
import foundation.e.advancedprivacy.trackers.data.TrackersRepository
import foundation.e.advancedprivacy.trackers.domain.entities.Tracker
import kotlinx.coroutines.flow.first
import kotlinx.serialization.json.Json
import java.lang.reflect.Type
import timber.log.Timber

class WeeklyReportLocalRepository(
    context: Context,
    private val trackersRepository: TrackersRepository,
    private val appListRepository: AppListRepository
    private val json: Json,
) {

    private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "weeklyreport_datastore")
    private val store = context.dataStore

    private val lastWeeklyReportKey = stringPreferencesKey("lastWeeklyReport")

    private val trackerSerializer = object : JsonSerializer<Tracker> {
        override fun serialize(src: Tracker?, typeOfSrc: Type, context: JsonSerializationContext): JsonElement {
            return if (src == null) {
                JsonNull.INSTANCE
            } else {
                try {
                    val o = JsonObject()
                    o.addProperty("id", src.id)
                    o
                } catch (e: Exception) {
                    Timber.w(e, "Can't serialiaze Tracker")
                    JsonNull.INSTANCE
                }
            }
        }
    }

    private val trackerDeserializer = object : JsonDeserializer<Tracker> {
        override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): Tracker? {
            return try {
                val id = json.asJsonObject.get("id").asJsonPrimitive.asString
                trackersRepository.getTracker(id)
            } catch (e: Exception) {
                Timber.w(e, "Can't deserialize Tracker")
                null
            }
        }
    }

    private val displayableAppSerializer = object : JsonSerializer<DisplayableApp> {
        override fun serialize(src: DisplayableApp?, typeOfSrc: Type, context: JsonSerializationContext): JsonElement {
            return if (src == null) {
                JsonNull.INSTANCE
            } else {
                try {
                    val o = JsonObject()
                    o.addProperty("apId", src.apps.first().apId)
                    o
                } catch (e: Exception) {
                    Timber.w(e, "Can't serialiaze DispayableApp")
                    JsonNull.INSTANCE
                }
            }
        }
    }

    private val displayableAppDeserializer = object : JsonDeserializer<DisplayableApp> {
        override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): DisplayableApp? {
            return try {
                val apId = json.asJsonObject.get("apId").asJsonPrimitive.asString
                appListRepository.getInternetAppByApId(apId)
            } catch (e: Exception) {
                Timber.w(e, "Can't deserialize Tracker")
                null
            }
        }
    }

    private val gson: Gson = GsonBuilder()
        .registerTypeAdapter(DisplayableApp::class.java, displayableAppSerializer)
        .registerTypeAdapter(DisplayableApp::class.java, displayableAppDeserializer)
        .registerTypeAdapter(Tracker::class.java, trackerSerializer)
        .registerTypeAdapter(Tracker::class.java, trackerDeserializer)
        .create()
    private val weeklyReportsKey = stringPreferencesKey("weeklyReports")

    suspend fun setLastWeeklyReport(report: WeeklyReport) {
        store.setValue(lastWeeklyReportKey, gson.toJson(report))
        store.updateData { pref ->
            val preferences = pref.toMutablePreferences()
            val reports = preferences.get(weeklyReportsKey)?.let {
                runCatching { json.decodeFromString<List<WeeklyReport>>(it) }.getOrNull()
            }?: emptyList()
            val updatedReport: List<WeeklyReport> = reports + report
            preferences.set(weeklyReportsKey, json.encodeToString(updatedReport))
            preferences
        }
    }

    suspend fun getLastWeeklyReport(): WeeklyReport? {
        return store.getValue(lastWeeklyReportKey)?.let {
            gson.fromJson<WeeklyReport>(it, WeeklyReport::class.java)
        return getLast99Reports().lastOrNull()
    }

    suspend fun getLast99Reports(): List<WeeklyReport> {
        return store.getValue(weeklyReportsKey)?.let {
            runCatching {
                json.decodeFromString<List<WeeklyReport>>(it).takeLast(99)
            }.onFailure {
                Timber.e("Can't deserialize weeklyReports")
                store.removeKey(weeklyReportsKey)
            }.getOrNull()
        }?: emptyList()
    }
}
+25 −1
Original line number Diff line number Diff line
@@ -17,11 +17,18 @@

package foundation.e.advancedprivacy.domain.entities

import foundation.e.advancedprivacy.trackers.domain.entities.Tracker
import kotlinx.serialization.Serializable


@Serializable
data class WeeklyReport(
    val statType: StatType,
    val labelId: LabelId,
    val value: List<String>
    val primaryValue: String,
    val secondaryValues: List<String>,
) {
    @Serializable
    enum class StatType {
        CALLS_PER_APP,
        NEW_TRACKER,
@@ -29,6 +36,7 @@ data class WeeklyReport(
        TRACKER_WITH_MOST_APPS
    }

    @Serializable
    enum class LabelId {
        CALLS_PER_APP_1,
        CALLS_PER_APP_2,
@@ -41,3 +49,19 @@ data class WeeklyReport(
        TRACKER_WITH_MOST_APPS_1
    }
}

sealed class DisplayableReport {
    abstract val report: WeeklyReport

    data class ReportText(
        override val report: WeeklyReport
    ): DisplayableReport()

    data class ReportWithApp(
        override val report: WeeklyReport,
        val app: DisplayableApp
    ): DisplayableReport()
    data class ReportWithTracker(
        override val report: WeeklyReport,
        val tracker: Tracker
    ): DisplayableReport
+53 −33
Original line number Diff line number Diff line
@@ -23,6 +23,7 @@ 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.DisplayableReport
import foundation.e.advancedprivacy.domain.entities.WeeklyReport
import foundation.e.advancedprivacy.trackers.data.StatsDatabase
import foundation.e.advancedprivacy.trackers.data.TrackersRepository
@@ -50,16 +51,33 @@ 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 history = weeklyReportRepository.getLast99Reports()
        val weeklyReport = chooseBestStatAndLabelToDisplay(computeStats(Instant.now()), history)
        weeklyReportRepository.setLastWeeklyReport(weeklyReport)
        buildLabel(weeklyReport)?.let {
            _showWeeklyReportNotification.emit(it)
        }
    }

    suspend fun getLast(): DisplayableReport? {
        val weeklyReport = weeklyReportRepository.getLastWeeklyReport()?: return null
        return when(weeklyReport.labelId) {
            WeeklyReport.LabelId.CALLS_PER_APP_1,
            WeeklyReport.LabelId.CALLS_PER_APP_2,
            WeeklyReport.LabelId.CALLS_PER_APP_3,
            WeeklyReport.LabelId.NEW_TRACKER_1  ->
                appListRepository.getAppById(weeklyReport.primaryValue)?.let { app ->
                    DisplayableReport.ReportWithApp(weeklyReport, app)
                }
            WeeklyReport.LabelId.NEW_TRACKER_2,
            WeeklyReport.LabelId.NEW_TRACKER_3,
            WeeklyReport.LabelId.CALLS_AND_LEAKS_1,
            WeeklyReport.LabelId.CALLS_AND_LEAKS_2 -> DisplayableReport.ReportText(weeklyReport)

    suspend fun getLast(): WeeklyReport? {
        return history.lastOrNull()
        // return weeklyReportRepository.getLastWeeklyReport()
            WeeklyReport.LabelId.TRACKER_WITH_MOST_APPS_1 -> trackersRepository.getTracker(weeklyReport.primaryValue)?.let { tracker ->
                DisplayableReport.ReportWithTracker(weeklyReport, tracker)
            }
        }
    }

    suspend fun getLastLabel(): String? {
@@ -87,23 +105,14 @@ class WeeklyReportUseCase(
        )
    }

    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)
//        }
    }

    var history = mutableListOf<WeeklyReport>()
    suspend fun debugGenerateReporsSinceWeeksAgo(weeksAgo: Int) = withContext(Dispatchers.IO) {
        history = mutableListOf<WeeklyReport>()
        var 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))
            val weeklyReport = chooseBestStatAndLabelToDisplay(computeStats(endOfWeek), history)
            history.add(weeklyReport)
        }

@@ -188,7 +197,8 @@ class WeeklyReportUseCase(
        val statLabelAllValueRank = history.indexOfLast {
            it.statType == weeklyReport.statType &&
                it.labelId == weeklyReport.labelId &&
                it.value == weeklyReport.value
                it.primaryValue == weeklyReport.primaryValue &&
                it.secondaryValues == weeklyReport.secondaryValues
        }
        if (statLabelAllValueRank != -1) {
            score += (statLabelAllValueRank + 1) * statLabelAllValueShift
@@ -197,14 +207,14 @@ class WeeklyReportUseCase(
        val statLabelValueRank = history.indexOfLast {
            it.statType == weeklyReport.statType &&
                it.labelId == weeklyReport.labelId &&
                it.value.first() == weeklyReport.value.first()
                it.primaryValue == weeklyReport.primaryValue
        }
        if (statLabelValueRank != -1) {
            score += (statLabelValueRank + 1) * statLabelValueShift
        }

        val statValueRank = history.indexOfLast {
            it.statType == weeklyReport.statType && it.value.first() == weeklyReport.value.first()
            it.statType == weeklyReport.statType && it.primaryValue == weeklyReport.primaryValue
        }
        if (statValueRank != -1) {
            score += (statValueRank + 1) * statValueShift
@@ -230,14 +240,15 @@ class WeeklyReportUseCase(
        )
    }

    private suspend fun chooseBestStatAndLabelToDisplay(weekStats: Stats): WeeklyReport {
    private suspend fun chooseBestStatAndLabelToDisplay(weekStats: Stats, history: List<WeeklyReport>): 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
//            appListRepository.getAppById(key)?.label?.toString() to value
            appListRepository.getAppById(key)?.id to value
        }
        val hoursInWeek = 7 * 24

@@ -248,7 +259,8 @@ class WeeklyReportUseCase(
                WeeklyReport(
                    WeeklyReport.StatType.CALLS_PER_APP,
                    WeeklyReport.LabelId.CALLS_PER_APP_1,
                    listOf(appWithMostCalls, (calls / hoursInWeek).toString())
                    appWithMostCalls,
                    listOf((calls / hoursInWeek).toString())
                )
            )

@@ -256,7 +268,8 @@ class WeeklyReportUseCase(
                WeeklyReport(
                    WeeklyReport.StatType.CALLS_PER_APP,
                    WeeklyReport.LabelId.CALLS_PER_APP_2,
                    listOf(appWithMostCalls, (calls / hoursInWeek).toString())
                    appWithMostCalls,
                    listOf((calls / hoursInWeek).toString())
                )
            )
        }
@@ -266,7 +279,8 @@ class WeeklyReportUseCase(
                WeeklyReport(
                    WeeklyReport.StatType.CALLS_PER_APP,
                    WeeklyReport.LabelId.CALLS_PER_APP_3,
                    listOf(appWithMostCalls, (calls / 7).toString())
                    appWithMostCalls,
                    listOf((calls / 7).toString())
                )
            )
        }
@@ -280,7 +294,8 @@ class WeeklyReportUseCase(
                    WeeklyReport(
                        WeeklyReport.StatType.NEW_TRACKER,
                        WeeklyReport.LabelId.NEW_TRACKER_1,
                        listOf(displayableApp.label.toString(), "")
                        displayableApp.id,
                        emptyList()
                    )
                )
            }
@@ -290,7 +305,8 @@ class WeeklyReportUseCase(
                WeeklyReport(
                    WeeklyReport.StatType.NEW_TRACKER,
                    WeeklyReport.LabelId.NEW_TRACKER_2,
                    listOf("", trackersCount.toString())
                    "",
                    listOf(trackersCount.toString())
                )
            )
        }
@@ -300,7 +316,8 @@ class WeeklyReportUseCase(
                WeeklyReport(
                    WeeklyReport.StatType.NEW_TRACKER,
                    WeeklyReport.LabelId.NEW_TRACKER_3,
                    listOf("", appsCount.toString())
                    "",
                    listOf(appsCount.toString())
                )
            )
        }
@@ -317,14 +334,16 @@ class WeeklyReportUseCase(
                WeeklyReport(
                    WeeklyReport.StatType.CALLS_AND_LEAKS,
                    WeeklyReport.LabelId.CALLS_AND_LEAKS_1,
                    listOf(blockedRate, leaks)
                    blockedRate,
                    listOf(leaks)
                )
            )
            candidates.add(
                WeeklyReport(
                    WeeklyReport.StatType.CALLS_AND_LEAKS,
                    WeeklyReport.LabelId.CALLS_AND_LEAKS_2,
                    listOf(blockedRate, leaks)
                    blockedRate,
                    listOf(leaks)
                )
            )
        }
@@ -336,7 +355,8 @@ class WeeklyReportUseCase(
                WeeklyReport(
                    WeeklyReport.StatType.TRACKER_WITH_MOST_APPS,
                    WeeklyReport.LabelId.TRACKER_WITH_MOST_APPS_1,
                    listOf(tracker.label, appCount.toString())
                    tracker.id,
                    listOf(appCount.toString())
                )
            )
        }
+39 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 MURENA SAS
 *
 * 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.advancedprivacy.features.weeklyreport

import android.view.LayoutInflater
import android.view.ViewGroup
import foundation.e.advancedprivacy.databinding.WeeklyreportItemCallsperappBinding
import foundation.e.advancedprivacy.domain.entities.WeeklyReport

class WeeklyReportViewFactory {
    fun createView(report: WeeklyReport, inflater: LayoutInflater, viewGroup: ViewGroup) {
        when {
            report.statType == WeeklyReport.StatType.CALLS_PER_APP -> {

            }
        }
    }

    fun createCallsPerAppView(report: WeeklyReport, inflater: LayoutInflater, viewGroup: ViewGroup) {
        val binding = WeeklyreportItemCallsperappBinding.inflate(inflater, viewGroup, true)


    }
}
+5 −0
Original line number Diff line number Diff line
@@ -48,6 +48,11 @@
            android:text="@string/trackers_info"
            />

        <FrameLayout
            android:id="@+id/weeklyreport_container"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
        />
        <com.google.android.material.tabs.TabLayout
            android:id="@+id/trackers_periods_tabs"
            android:layout_width="match_parent"
Loading