diff --git a/app/src/main/java/foundation/e/advancedprivacy/KoinModule.kt b/app/src/main/java/foundation/e/advancedprivacy/KoinModule.kt index f5fb910ee130af4419ccd6b2bde46ddcfdb3fea8..20f78870834799f8ba6893bcc3f2dd20d8c55171 100644 --- a/app/src/main/java/foundation/e/advancedprivacy/KoinModule.kt +++ b/app/src/main/java/foundation/e/advancedprivacy/KoinModule.kt @@ -228,7 +228,7 @@ val appModule = module { viewModelOf(::InternetPrivacyViewModel) viewModelOf(::DashboardViewModel) - single { WeeklyReportViewFactory() } + single { WeeklyReportViewFactory(context = androidContext()) } single { NotificationsPresenter( diff --git a/app/src/main/java/foundation/e/advancedprivacy/domain/usecases/TrackersAndAppsListsUseCase.kt b/app/src/main/java/foundation/e/advancedprivacy/domain/usecases/TrackersAndAppsListsUseCase.kt index bdfef3b1ffa3454b2f9a67da9adf678e1008b7c5..c12a6cde691d91f020af10f2efc2e9bfcfb7d588 100644 --- a/app/src/main/java/foundation/e/advancedprivacy/domain/usecases/TrackersAndAppsListsUseCase.kt +++ b/app/src/main/java/foundation/e/advancedprivacy/domain/usecases/TrackersAndAppsListsUseCase.kt @@ -75,7 +75,7 @@ class TrackersAndAppsListsUseCase( private suspend fun getCountByEntityMaps(period: Period): CountByEntitiesMaps { val periodStart: Instant = period.getPeriodStart() - val trackersAndAppsIds = statsDatabase.getDistinctTrackerAndApp(periodStart) + val trackersAndAppsIds = statsDatabase.getDistinctTrackerAndApp(periodStart, Instant.now()) val trackersAndApps = mapIdsToEntities(trackersAndAppsIds) return foldToCountByEntityMaps(trackersAndApps) } diff --git a/app/src/main/java/foundation/e/advancedprivacy/domain/usecases/TrackersScreenUseCase.kt b/app/src/main/java/foundation/e/advancedprivacy/domain/usecases/TrackersScreenUseCase.kt index f0a2feff9b7f8b6467a4d6d4dd711a232fbba1a2..6cdb2d5ba00c91c03cf227e54a023dd2b01383f9 100644 --- a/app/src/main/java/foundation/e/advancedprivacy/domain/usecases/TrackersScreenUseCase.kt +++ b/app/src/main/java/foundation/e/advancedprivacy/domain/usecases/TrackersScreenUseCase.kt @@ -18,14 +18,20 @@ package foundation.e.advancedprivacy.domain.usecases import foundation.e.advancedprivacy.domain.repositories.LocalStateRepository +import foundation.e.advancedprivacy.features.trackers.Period +import foundation.e.advancedprivacy.features.trackers.TrackerTab import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.withContext class TrackersScreenUseCase( private val localStateRepository: LocalStateRepository, private val backgroundDispatcher: CoroutineDispatcher = Dispatchers.IO ) { + private val _goToTab = MutableSharedFlow>() + val goToTab: SharedFlow> = _goToTab suspend fun getLastPosition(): Int = withContext(backgroundDispatcher) { localStateRepository.getTrackersScreenLastPosition() @@ -43,8 +49,12 @@ class TrackersScreenUseCase( localStateRepository.trackersScreenTabStartPosition = -1 } - suspend fun preselectTab(periodPosition: Int, tabPosition: Int) = withContext(backgroundDispatcher) { - localStateRepository.setTrackersScreenLastPosition(periodPosition) - localStateRepository.trackersScreenTabStartPosition = tabPosition + suspend fun preselectTab(period: Period, tab: TrackerTab) = withContext(backgroundDispatcher) { + localStateRepository.setTrackersScreenLastPosition(period.ordinal) + localStateRepository.trackersScreenTabStartPosition = tab.ordinal + } + + suspend fun selectTab(period: Period, tab: TrackerTab) { + _goToTab.emit(period.ordinal to tab.ordinal) } } 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 ef572d929e91764e059cdb26a852d56158db6a2f..46338179d23c19bb4abf00ca8a87d1452f84aa75 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,6 +22,7 @@ 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.trackers.data.StatsDatabase import foundation.e.advancedprivacy.trackers.data.TrackersRepository import java.time.Duration import java.time.Instant @@ -40,6 +41,7 @@ class WeeklyReportUseCase( private val trackersRepository: TrackersRepository, private val appListRepository: AppListRepository, private val weeklyReportRepository: WeeklyReportLocalRepository, + private val statsDatabase: StatsDatabase, private val scope: CoroutineScope ) { private val _currentReport = MutableStateFlow(null) @@ -55,7 +57,7 @@ class WeeklyReportUseCase( } } - private suspend fun updateCurrent() { + suspend fun updateCurrent() { val weeklyReport = weeklyReportRepository.getLastWeeklyReport() ?: return val now = Instant.now() val endOfDisplay = weeklyReport.timestamp + displayDuration @@ -97,7 +99,7 @@ class WeeklyReportUseCase( result.reversed() } - private fun buildCandidates(endOfWeek: Instant, history: List): List { + private suspend fun buildCandidates(endOfWeek: Instant, history: List): List { val candidates = mutableListOf() addCallPerAppCandidates(candidates, endOfWeek) addNewTrackerCandidates(candidates, endOfWeek) @@ -143,37 +145,54 @@ class WeeklyReportUseCase( ) } - 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, - emptyList() + private suspend fun addNewTrackerCandidates(candidates: MutableList, endOfWeek: Instant) { + val startOfWeek = endOfWeek.minus(7, ChronoUnit.DAYS) + val startOfYear = endOfWeek.minus(365, ChronoUnit.DAYS) + + val trackerAppsHistoric = statsDatabase.getDistinctTrackerAndApp(startOfYear, startOfWeek) + val trackerAppsOfWeek = statsDatabase.getDistinctTrackerAndApp(startOfWeek, endOfWeek) + val historicTrackers = trackerAppsHistoric.map { it.first }.toSet() + + val newTrackersDetected = trackerAppsOfWeek.filter { it.first !in historicTrackers } + + val appsIntroducingNewTracker = newTrackersDetected.mapNotNull { appListRepository.getAppById(it.second) }.toSet() + // TODO dummy apps ?&& displayableApp != appListRepository + appsIntroducingNewTracker.forEach { app -> + candidates.add( + WeeklyReport( + endOfWeek, + WeeklyReport.StatType.NEW_TRACKER, + WeeklyReport.LabelId.NEW_TRACKER_1, + app.id, + emptyList() + ) ) - ) + } - candidates.add( - WeeklyReport( - endOfWeek, - WeeklyReport.StatType.NEW_TRACKER, - WeeklyReport.LabelId.NEW_TRACKER_2, - "", - listOf(Random.nextInt(4).toString()) + val newTrackersCount = newTrackersDetected.map { it.first }.toSet().size + if (newTrackersCount > 0) { + candidates.add( + WeeklyReport( + endOfWeek, + WeeklyReport.StatType.NEW_TRACKER, + WeeklyReport.LabelId.NEW_TRACKER_2, + "", + listOf(newTrackersCount.toString()) + ) ) - ) + } - candidates.add( - WeeklyReport( - endOfWeek, - WeeklyReport.StatType.NEW_TRACKER, - WeeklyReport.LabelId.NEW_TRACKER_3, - "", - listOf(Random.nextInt(4).toString()) + if (appsIntroducingNewTracker.isNotEmpty()) { + candidates.add( + WeeklyReport( + endOfWeek, + WeeklyReport.StatType.NEW_TRACKER, + WeeklyReport.LabelId.NEW_TRACKER_3, + "", + listOf(appsIntroducingNewTracker.size.toString()) + ) ) - ) + } } private fun addCallAndLeaksCandidates(candidates: MutableList, endOfWeek: Instant) { @@ -268,6 +287,10 @@ class WeeklyReportUseCase( ) } + private fun getStartOfWeek(endOfWeek: Instant): Instant { + return endOfWeek.minus(7, ChronoUnit.DAYS) + } + private fun WeeklyReport.toDisplayableReport(): DisplayableReport? { val weeklyReport = this return when (weeklyReport.labelId) { diff --git a/app/src/main/java/foundation/e/advancedprivacy/features/dashboard/DashboardViewModel.kt b/app/src/main/java/foundation/e/advancedprivacy/features/dashboard/DashboardViewModel.kt index 72c616d5121135326671eb7ea899b858dc6dab9e..c1e6e64df743035fc91b280832cb400b34b32769 100644 --- a/app/src/main/java/foundation/e/advancedprivacy/features/dashboard/DashboardViewModel.kt +++ b/app/src/main/java/foundation/e/advancedprivacy/features/dashboard/DashboardViewModel.kt @@ -130,12 +130,12 @@ class DashboardViewModel( } fun onClickViewAllApps() = viewModelScope.launch { - trackersScreenUseCase.preselectTab(Period.MONTH.ordinal, TrackerTab.APPS.ordinal) + trackersScreenUseCase.preselectTab(Period.MONTH, TrackerTab.APPS) _navigate.emit(DashboardFragmentDirections.gotoTrackersFragment()) } fun onClickViewAllTrackers() = viewModelScope.launch { - trackersScreenUseCase.preselectTab(Period.MONTH.ordinal, TrackerTab.TRACKERS.ordinal) + trackersScreenUseCase.preselectTab(Period.MONTH, TrackerTab.TRACKERS) _navigate.emit(DashboardFragmentDirections.gotoTrackersFragment()) } private suspend fun fetchStatistics() = withContext(Dispatchers.IO) { 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 5885f94cb35d0515d2269fb98a9eff54083cb7e6..2544e8459d77b4213199ebdf2c9b63000412c023 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 @@ -23,6 +23,7 @@ import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.widget.Toast import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.lifecycle.lifecycleScope @@ -33,6 +34,7 @@ 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.data.repositories.WeeklyReportLocalRepository import foundation.e.advancedprivacy.databinding.DebugWeeklyReportFragmentBinding import foundation.e.advancedprivacy.databinding.DebugWeeklyReportItemBinding import foundation.e.advancedprivacy.domain.entities.weeklyreport.DisplayableReport @@ -54,6 +56,7 @@ class DebugWeeklyReportFragment : Fragment(R.layout.debug_weekly_report_fragment private val trackersRepository: TrackersRepository by inject() private val notificationsPresenter: NotificationsPresenter by inject() private val reportsFactory: WeeklyReportViewFactory by inject() + private val weeklyReportRepository: WeeklyReportLocalRepository by inject() private lateinit var binding: DebugWeeklyReportFragmentBinding @@ -82,7 +85,9 @@ class DebugWeeklyReportFragment : Fragment(R.layout.debug_weekly_report_fragment displayableReport, LayoutInflater.from(holder.binding.container.context), holder.binding.container - ) + ) { + Toast.makeText(requireContext(), R.string.debug_weeklyreport_onclick_report_action, Toast.LENGTH_SHORT).show() + } }?.let { Log.d("DebugReport", "onBindViewHolder has a computed view") holder.binding.container.addView(it) @@ -116,6 +121,15 @@ class DebugWeeklyReportFragment : Fragment(R.layout.debug_weekly_report_fragment startActivity(Intent.createChooser(sendIntent, null)) } + + holder.binding.setAsCurrent.setOnClickListener { + lifecycleScope.launch { + report.first?.report?.let { + weeklyReportRepository.setLastWeeklyReport(it) + weeklyReportUseCase.updateCurrent() + } + } + } } } 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 7a6168725a2989968a67265dcb4c9ae99e17c1d8..43cf4c919486ed87d4a1d7b2fc027b5491da32d4 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 @@ -155,6 +155,8 @@ class TrackersFragment : NavToolbarFragment(R.layout.fragment_trackers) { ).show() } } + is TrackersViewModel.SingleEvent.GoToPeriod -> + binding.trackersPeriodsPager.currentItem = event.periodPosition } } @@ -202,7 +204,14 @@ class TrackersFragment : NavToolbarFragment(R.layout.fragment_trackers) { binding.weeklyreport.isVisible = report != null if (report != null) { - binding.weeklyreportContainer.addView(weeklyReportViewFactory.createView(report, layoutInflater, binding.weeklyreportContainer)) + binding.weeklyreportContainer.addView( + weeklyReportViewFactory.createView( + report, + layoutInflater, + binding.weeklyreportContainer, + viewModel::onClickWeeklyReportAction + ) + ) binding.weeklyreportShareTitle.text = weeklyReportViewFactory.getShareTitle(report) } } diff --git a/app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersPeriodFragment.kt b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersPeriodFragment.kt index 771c31d6485ed5e5ef8cf8480870f05c378c8fe4..2a3aafed29147b140eb5cedc3ed49e11ac5ba58f 100644 --- a/app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersPeriodFragment.kt +++ b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersPeriodFragment.kt @@ -176,6 +176,9 @@ class TrackersPeriodFragment : Fragment(R.layout.trackers_period_fragment) { ).show() } } + is SingleEvent.GoToTab -> { + binding.listsPager.currentItem = event.tabIndex + } } } diff --git a/app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersPeriodViewModel.kt b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersPeriodViewModel.kt index b416f2d820ce47cf479907725cf9a55a8ea0addf..bf6dc8d8e75e1f214f0e1571b607c3ae354d8478 100644 --- a/app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersPeriodViewModel.kt +++ b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersPeriodViewModel.kt @@ -27,6 +27,7 @@ import foundation.e.advancedprivacy.domain.entities.DisplayableApp import foundation.e.advancedprivacy.domain.usecases.TrackersAndAppsListsUseCase import foundation.e.advancedprivacy.domain.usecases.TrackersScreenUseCase import foundation.e.advancedprivacy.domain.usecases.TrackersStatisticsUseCase +import foundation.e.advancedprivacy.features.trackers.TrackersPeriodViewModel.SingleEvent.GoToTab import foundation.e.advancedprivacy.trackers.domain.entities.Tracker import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableSharedFlow @@ -66,28 +67,35 @@ class TrackersPeriodViewModel( val navigate = _navigate.asSharedFlow() suspend fun doOnStartedState() = withContext(Dispatchers.IO) { - trackersStatisticsUseCase.listenUpdates().collect { - trackersStatisticsUseCase.getGraphData(period).let { graphData -> - _state.update { - it.copy( - callsBlockedNLeaked = graphData.callsBlockedNLeaked, - periods = graphData.periods, - trackersCount = graphData.trackersCount, - trackersAllowedCount = graphData.trackersAllowedCount, - graduations = graphData.graduations - ) + launch { + trackersStatisticsUseCase.listenUpdates().collect { + trackersStatisticsUseCase.getGraphData(period).let { graphData -> + _state.update { + it.copy( + callsBlockedNLeaked = graphData.callsBlockedNLeaked, + periods = graphData.periods, + trackersCount = graphData.trackersCount, + trackersAllowedCount = graphData.trackersAllowedCount, + graduations = graphData.graduations + ) + } } - } - trackersAndAppsListsUseCase.getAppsAndTrackersCounts(period).let { lists -> - _state.update { - it.copy( - trackers = lists.trackers, - allApps = lists.allApps, - appsWithTrackers = lists.appsWithTrackers - ) + trackersAndAppsListsUseCase.getAppsAndTrackersCounts(period).let { lists -> + _state.update { + it.copy( + trackers = lists.trackers, + allApps = lists.allApps, + appsWithTrackers = lists.appsWithTrackers + ) + } + _refreshUiHeight.emit(Unit) } - _refreshUiHeight.emit(Unit) + } + } + launch { + trackersScreenUseCase.goToTab.collect { (_, tabPosition) -> + _singleEvents.emit(GoToTab(tabPosition)) } } } @@ -117,5 +125,6 @@ class TrackersPeriodViewModel( sealed class SingleEvent { data class ErrorEvent(val error: String) : SingleEvent() data class OpenUrl(val url: Uri) : SingleEvent() + data class GoToTab(val tabIndex: Int) : SingleEvent() } } 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 692dbbe95c31a041eae417bc49750d143fb39064..1b2a16bebb492e33570c28899013a666db551ba8 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 @@ -24,8 +24,10 @@ 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.entities.weeklyreport.WeeklyReport import foundation.e.advancedprivacy.domain.usecases.TrackersScreenUseCase import foundation.e.advancedprivacy.domain.usecases.WeeklyReportUseCase +import foundation.e.advancedprivacy.features.trackers.TrackersViewModel.SingleEvent.GoToPeriod import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow @@ -78,9 +80,16 @@ class TrackersViewModel( } } - suspend fun doOnStartedState(): Nothing = withContext(Dispatchers.IO) { - weeklyReportUseCase.currentReport.collect { report -> - _state.update { report } + suspend fun doOnStartedState() = withContext(Dispatchers.IO) { + launch { + weeklyReportUseCase.currentReport.collect { report -> + _state.update { report } + } + } + launch { + trackersScreenUseCase.goToTab.collect { (periodPosition, _) -> + _singleEvents.emit(GoToPeriod(periodPosition)) + } } } @@ -94,8 +103,24 @@ class TrackersViewModel( _singleEvents.emit(SingleEvent.OpenUrl(Uri.parse(URL_LEARN_MORE_ABOUT_TRACKERS))) } + fun onClickWeeklyReportAction() = viewModelScope.launch { + val report = state.value + when { + report?.report?.labelId == WeeklyReport.LabelId.NEW_TRACKER_1 && + report is DisplayableReport.ReportWithApp -> { + _navigate.emit(TrackersFragmentDirections.gotoAppTrackersFragment(appId = report.app.id)) + } + + report?.report?.labelId == WeeklyReport.LabelId.NEW_TRACKER_2 || + report?.report?.labelId == WeeklyReport.LabelId.NEW_TRACKER_3 -> { + trackersScreenUseCase.selectTab(Period.MONTH, TrackerTab.TRACKERS) + } + } + } + sealed class SingleEvent { data class ErrorEvent(val error: String) : SingleEvent() data class OpenUrl(val url: Uri) : SingleEvent() + data class GoToPeriod(val periodPosition: Int) : SingleEvent() } } diff --git a/app/src/main/java/foundation/e/advancedprivacy/features/weeklyreport/NewTrackersViewFactory.kt b/app/src/main/java/foundation/e/advancedprivacy/features/weeklyreport/NewTrackersViewFactory.kt new file mode 100644 index 0000000000000000000000000000000000000000..3dbe0af18401c286480f5e5b4d0b3441f70fbc8f --- /dev/null +++ b/app/src/main/java/foundation/e/advancedprivacy/features/weeklyreport/NewTrackersViewFactory.kt @@ -0,0 +1,205 @@ +/* + * 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.features.weeklyreport + +import android.content.Context +import android.graphics.drawable.Drawable +import android.text.method.LinkMovementMethod +import android.text.style.ClickableSpan +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.content.ContextCompat +import androidx.core.text.bold +import androidx.core.text.buildSpannedString +import androidx.core.text.inSpans +import foundation.e.advancedprivacy.R +import foundation.e.advancedprivacy.databinding.WeeklyreportItemNewTrackersBinding +import foundation.e.advancedprivacy.databinding.WeeklyreportItemNewTrackersForSharingBinding +import foundation.e.advancedprivacy.domain.entities.weeklyreport.DisplayableReport +import foundation.e.advancedprivacy.domain.entities.weeklyreport.DisplayableReport.ReportWithApp +import foundation.e.advancedprivacy.domain.entities.weeklyreport.WeeklyReport + +class NewTrackersViewFactory(private val context: Context) { + fun getShareTitle(report: DisplayableReport): CharSequence { + return context.getString( + when (report.report.labelId) { + WeeklyReport.LabelId.NEW_TRACKER_1 -> + R.string.weeklyreport_share_title_new_tracker_1 + + else -> + R.string.weeklyreport_share_title_base + } + ) + } + + fun createView(report: DisplayableReport, inflater: LayoutInflater, viewGroup: ViewGroup, onClick: () -> Unit): View? { + val binding = WeeklyreportItemNewTrackersBinding.inflate(inflater, viewGroup, false) + + val labelId = report.report.labelId + + binding.icon.setImageResource( + when (labelId) { + WeeklyReport.LabelId.NEW_TRACKER_1 -> R.drawable.ic_spy + WeeklyReport.LabelId.NEW_TRACKER_2 -> R.drawable.ic_shield_alert + WeeklyReport.LabelId.NEW_TRACKER_3 -> R.drawable.ic_discovered + else -> R.drawable.ic_shield_alert + } + ) + + binding.title.text = getTitle(report) + + binding.description.text = buildNewTrackersDescription(report, onClick) + binding.description.movementMethod = LinkMovementMethod.getInstance() + with(binding.viewDetails) { + setText( + when (labelId) { + WeeklyReport.LabelId.NEW_TRACKER_1 -> R.string.weeklyreport_label_new_tracker_1_cta + WeeklyReport.LabelId.NEW_TRACKER_2 -> R.string.weeklyreport_label_new_tracker_2_cta + WeeklyReport.LabelId.NEW_TRACKER_3 -> R.string.weeklyreport_label_new_tracker_3_cta + else -> R.string.empty + } + ) + setOnClickListener { onClick() } + } + + return binding.root + } + + fun createViewForSharing(report: DisplayableReport, inflater: LayoutInflater, viewGroup: ViewGroup): View? { + val binding = WeeklyreportItemNewTrackersForSharingBinding.inflate(inflater, viewGroup, false) + + binding.icon.setImageDrawable(getIconForNotification(report)) + + binding.title.text = getTitle(report) + + binding.description.text = buildNewTrackersDescription(report, null, true) + + return binding.root + } + + fun getTitle(report: DisplayableReport): CharSequence { + return context.getString( + when (report.report.labelId) { + WeeklyReport.LabelId.NEW_TRACKER_1 -> + R.string.weeklyreport_label_new_tracker_1_title + + WeeklyReport.LabelId.NEW_TRACKER_2 -> + R.string.weeklyreport_label_new_tracker_2_title + + WeeklyReport.LabelId.NEW_TRACKER_3 -> + R.string.weeklyreport_label_new_tracker_3_title + + else -> + R.string.empty + } + ) + } + + fun getDescriptionForNotification(report: DisplayableReport): String { + return buildNewTrackersDescription(report, null, forSharing = false).toString() + } + + fun getIconForNotification(report: DisplayableReport): Drawable? { + return ContextCompat.getDrawable( + context, + when (report.report.labelId) { + WeeklyReport.LabelId.NEW_TRACKER_1 -> R.drawable.ic_spy_yellow_bg + WeeklyReport.LabelId.NEW_TRACKER_2 -> R.drawable.ic_shield_alert_yellow_bg + WeeklyReport.LabelId.NEW_TRACKER_3 -> R.drawable.ic_discovered_yellow_bg + else -> R.drawable.ic_shield_alert_yellow_bg + } + ) + } + + private fun buildNewTrackersDescription( + report: DisplayableReport, + clickCallBack: (() -> Unit)?, + forSharing: Boolean = false + ): CharSequence { + val labelId = report.report.labelId + + return if (labelId == WeeklyReport.LabelId.NEW_TRACKER_1) { + val value = (report as? ReportWithApp)?.app?.label ?: "" + buildSpannedString { + append(context.getString(R.string.weeklyreport_label_new_tracker_1_description_1)) + append(" ") + if (forSharing) { + bold { + append(value) + } + } else { + inSpans( + object : ClickableSpan() { + override fun onClick(p0: View) { + clickCallBack?.invoke() + } + } + ) { + append(value) + } + } + append(" ") + append( + context.getString( + if (forSharing) { + R.string.weeklyreport_label_new_tracker_1_description_2_sharing + } else { + R.string.weeklyreport_label_new_tracker_1_description_2 + } + ) + ) + } + } else { + val valueLabel = context.getString( + when (labelId) { + WeeklyReport.LabelId.NEW_TRACKER_2 -> R.string.weeklyreport_label_new_tracker_2_description_1 + WeeklyReport.LabelId.NEW_TRACKER_3 -> R.string.weeklyreport_label_new_tracker_3_description_1 + else -> R.string.empty + }, + report.report.secondaryValues.firstOrNull() ?: "" + ) + + val description = context.getString( + when { + labelId == WeeklyReport.LabelId.NEW_TRACKER_2 && forSharing -> + R.string.weeklyreport_label_new_tracker_2_description_2_sharing + labelId == WeeklyReport.LabelId.NEW_TRACKER_2 -> + R.string.weeklyreport_label_new_tracker_2_description_2 + labelId == WeeklyReport.LabelId.NEW_TRACKER_3 && forSharing -> + R.string.weeklyreport_label_new_tracker_3_description_2_sharing + labelId == WeeklyReport.LabelId.NEW_TRACKER_3 -> + R.string.weeklyreport_label_new_tracker_3_description_2 + else -> R.string.empty + } + ) + + buildSpannedString { + if (forSharing) { + bold { + append(valueLabel) + } + } else { + append(valueLabel) + } + append(" ") + append(description) + } + } + } +} 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 b24b38ddbc54672f5ba99b3f3c3e6a20e144fd25..fb01781bf7bda7a059600ef23455730291fe9267 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 @@ -29,21 +29,31 @@ import android.view.View.MeasureSpec import android.view.ViewGroup import androidx.core.app.NotificationCompat import androidx.core.graphics.drawable.toBitmap +import foundation.e.advancedprivacy.R import foundation.e.advancedprivacy.databinding.WeeklyReportItemTextBinding import foundation.e.advancedprivacy.databinding.WeeklyReportShareTemplateBinding import foundation.e.advancedprivacy.domain.entities.weeklyreport.DisplayableReport +import foundation.e.advancedprivacy.domain.entities.weeklyreport.WeeklyReport + +class WeeklyReportViewFactory(private val context: Context) { + val newTrackersViewFactory: NewTrackersViewFactory by lazy { NewTrackersViewFactory(context) } -class WeeklyReportViewFactory() { fun getShareTitle(report: DisplayableReport): CharSequence { - return report.report.statType.name + return when (report.report.statType) { + WeeklyReport.StatType.NEW_TRACKER -> + newTrackersViewFactory.getShareTitle(report) + + else -> context.getString(R.string.weeklyreport_share_title_base) + } } - fun createView(report: DisplayableReport, inflater: LayoutInflater, viewGroup: ViewGroup): View { - val binding = WeeklyReportItemTextBinding.inflate(inflater, viewGroup, false) + fun createView(report: DisplayableReport, inflater: LayoutInflater, viewGroup: ViewGroup, onClick: () -> Unit): View { + return when (report.report.statType) { + WeeklyReport.StatType.NEW_TRACKER -> + newTrackersViewFactory.createView(report, inflater, viewGroup, onClick) - binding.title.text = getTitle(report) - binding.description.text = getDescription(report) - return binding.root + else -> null + } ?: createDefaultView(report, inflater, viewGroup) } fun createShareBmp(context: Context, report: DisplayableReport): Bitmap { @@ -61,7 +71,7 @@ class WeeklyReportViewFactory() { val layoutInflater = LayoutInflater.from(screenshotContext) val shareTemplateBinding = WeeklyReportShareTemplateBinding.inflate(layoutInflater) shareTemplateBinding.container.addView( - createView(report, layoutInflater, shareTemplateBinding.container) + createViewForSharing(report, layoutInflater, shareTemplateBinding.container) ) val view = shareTemplateBinding.root @@ -86,15 +96,47 @@ class WeeklyReportViewFactory() { fun populateNotification(report: DisplayableReport, builder: NotificationCompat.Builder) { builder.setContentTitle(getTitle(report)) - builder.setContentText(getDescription(report)) - getIcon(report)?.toBitmap()?.let { builder.setLargeIcon(it) } + builder.setContentText(getDescriptionForNotification(report).toString()) + getIconForNotification(report)?.toBitmap()?.let { builder.setLargeIcon(it) } + } + + private fun createViewForSharing(report: DisplayableReport, inflater: LayoutInflater, viewGroup: ViewGroup): View { + return when (report.report.statType) { + WeeklyReport.StatType.NEW_TRACKER -> + newTrackersViewFactory.createViewForSharing(report, inflater, viewGroup) + + else -> null + } ?: createDefaultView(report, inflater, viewGroup) + } + + private fun createDefaultView(report: DisplayableReport, inflater: LayoutInflater, viewGroup: ViewGroup): View { + val binding = WeeklyReportItemTextBinding.inflate(inflater, viewGroup, false) + + binding.title.text = getTitle(report) + binding.description.text = getDescriptionForNotification(report) + + return binding.root } private fun getTitle(report: DisplayableReport): CharSequence { - return report.report.labelId.name + return when (report.report.statType) { + WeeklyReport.StatType.NEW_TRACKER -> + newTrackersViewFactory.getTitle(report) + + else -> report.report.labelId.name + } + } + + private fun getDescriptionForNotification(report: DisplayableReport): String { + return when (report.report.statType) { + WeeklyReport.StatType.NEW_TRACKER -> + newTrackersViewFactory.getDescriptionForNotification(report) + + else -> getDefaultDescription(report).toString() + } } - private fun getDescription(report: DisplayableReport): CharSequence { + private fun getDefaultDescription(report: DisplayableReport): CharSequence { val primaryValue = when (report) { is DisplayableReport.ReportWithApp -> report.app.label is DisplayableReport.ReportText -> report.report.primaryValue @@ -104,9 +146,10 @@ 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 + private fun getIconForNotification(report: DisplayableReport): Drawable? { + return when (report.report.statType) { + WeeklyReport.StatType.NEW_TRACKER -> + newTrackersViewFactory.getIconForNotification(report) else -> null } } diff --git a/app/src/main/res/drawable/bg_circle_yellow.xml b/app/src/main/res/drawable/bg_circle_yellow.xml new file mode 100644 index 0000000000000000000000000000000000000000..c157281e1dd855089f751c249297a2a7064f3292 --- /dev/null +++ b/app/src/main/res/drawable/bg_circle_yellow.xml @@ -0,0 +1,21 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_discovered.xml b/app/src/main/res/drawable/ic_discovered.xml new file mode 100644 index 0000000000000000000000000000000000000000..2f59c597197bc6c14d397385f479fef7ca0424ba --- /dev/null +++ b/app/src/main/res/drawable/ic_discovered.xml @@ -0,0 +1,14 @@ + + + + diff --git a/app/src/main/res/drawable/ic_discovered_yellow_bg.xml b/app/src/main/res/drawable/ic_discovered_yellow_bg.xml new file mode 100644 index 0000000000000000000000000000000000000000..3cffdc9054ec1118956b07d5999ac6fb4b74fef4 --- /dev/null +++ b/app/src/main/res/drawable/ic_discovered_yellow_bg.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_shield_alert_yellow_bg.xml b/app/src/main/res/drawable/ic_shield_alert_yellow_bg.xml new file mode 100644 index 0000000000000000000000000000000000000000..1b957e9b5245dc5a85597de391c4535c88b9c691 --- /dev/null +++ b/app/src/main/res/drawable/ic_shield_alert_yellow_bg.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_spy.xml b/app/src/main/res/drawable/ic_spy.xml new file mode 100644 index 0000000000000000000000000000000000000000..d0afd990f654fcf2deff3aa887d9c6b97fa9a8db --- /dev/null +++ b/app/src/main/res/drawable/ic_spy.xml @@ -0,0 +1,33 @@ + + + + + + + + diff --git a/app/src/main/res/drawable/ic_spy_yellow_bg.xml b/app/src/main/res/drawable/ic_spy_yellow_bg.xml new file mode 100644 index 0000000000000000000000000000000000000000..56e88f40c92eb80076581939f31be7707f89446e --- /dev/null +++ b/app/src/main/res/drawable/ic_spy_yellow_bg.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_watch.xml b/app/src/main/res/drawable/ic_watch.xml new file mode 100644 index 0000000000000000000000000000000000000000..12dc93a948792a8858c6b446f46298da5063cc67 --- /dev/null +++ b/app/src/main/res/drawable/ic_watch.xml @@ -0,0 +1,18 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_watch_yellow_bg.xml b/app/src/main/res/drawable/ic_watch_yellow_bg.xml new file mode 100644 index 0000000000000000000000000000000000000000..9b3acac10bb063a0f955767864d4080ce94a2cd9 --- /dev/null +++ b/app/src/main/res/drawable/ic_watch_yellow_bg.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/debug_weekly_report_item.xml b/app/src/main/res/layout/debug_weekly_report_item.xml index 6c2478545376fc4a6c682abaa3bf3913a4ff48f1..9226bbcda29ab8b9d00fbcad71dce6a0006061fb 100644 --- a/app/src/main/res/layout/debug_weekly_report_item.xml +++ b/app/src/main/res/layout/debug_weekly_report_item.xml @@ -51,6 +51,13 @@ android:text="@string/debug_weekly_report_item_notification" android:layout_margin="4dp" /> + + + + > + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/weeklyreport_item_new_trackers_for_sharing.xml b/app/src/main/res/layout/weeklyreport_item_new_trackers_for_sharing.xml new file mode 100644 index 0000000000000000000000000000000000000000..3a4f878aeb60535063d4488de63146f8c1beea2e --- /dev/null +++ b/app/src/main/res/layout/weeklyreport_item_new_trackers_for_sharing.xml @@ -0,0 +1,68 @@ + + + + > + + + + \ No newline at end of file diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index d867387da3bf7cc7327898ae2c14d47d8d54dba0..1e4af532745811fc81b2e2534c8ea55909ca4353 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -159,6 +159,27 @@ Sehen Sie, wie /e/OS mein Gerät vor Tracking-Versuchen schützt! \nWarum mobile App-Tracker eine der größten Bedrohungen für Ihre Privatsphäre und Freiheit sind: Lesen Sie unseren Datenschutzleitfaden unter Wöchentlicher Bericht: https://e.foundation/wp-content/uploads/2025/01/White_Paper_-_Privacy_-_DE.pdf + + Schockiert? + + Verbreiten Sie die Nachricht! + Neuer Spion in der Stadt! + Diese Woche + hat eine neue Tracker eingeführt. + hat eine neue Tracker eingeführt! + + Tracker ansehen + Neue Überwachungsversuche: + %s neue Tracker + diese Woche entdeckt. + diese Woche auf meinem Smartphone entdeckt. + Alle ansehen + Neue Tracker entdeckt: + %s Apps + haben diese Woche neue versteckte Tracker hinzugefügt. + haben diese Woche neue versteckte Tracker auf meinem Smartphone hinzugefügt. + Alle ansehen + Wöchentlicher Bericht Eine wöchentliche Benachrichtigung, die eine Zusammenfassung der Tracker zeigt und Änderungen sowie Updates hervorhebt. \ No newline at end of file diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index ed9ebdcbc764df78caaf6364f150f6fb5fdd8502..baef26fc751889f69997c8f59b27bd87df159978 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -159,6 +159,27 @@ ¡Vea cómo /e/OS protege mi dispositivo de intentos de seguimiento! \nPor qué los rastreadores de aplicaciones móviles son una de las amenazas más impactantes para su privacidad y su libertad: lea nuestra Guía de Privacidad en Informe semanal: https://e.foundation/wp-content/uploads/2025/01/White_Paper_-_Privacy_-_ES.pdf + + ¿Impresionado? + + ¡Corre la voz! + ¡Nuevo espía en la ciudad! + Esta semana + introdujo un nuevo rastreador. + ¡introdujo un nuevo rastreador! + + Ver rastreador + Nuevos intentos de vigilancia: + %s nuevos rastreadores + detectados esta semana. + detectados esta semana en mi smartphone. + Ver todos + Nuevos rastreadores descubiertos: + %s aplicaciones + añadieron nuevos rastreadores ocultos esta semana. + añadieron nuevos rastreadores ocultos esta semana en mi smartphone. + Ver todos + Informe Semanal Una notificación semanal que muestra un resumen de los rastreadores, destacando cualquier cambio y actualización en ellos. \ No newline at end of file diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index adedad3bf180c43b9db3c5baf1c3c402b352c425..f554af56c38ba6de238c33a2de21995749672706 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -157,6 +157,27 @@ Voyez comment /e/OS protège mon appareil des tentatives de traçage ! \nPourquoi les trackers d\'applications mobiles sont l\'une des menaces les plus impactantes pour votre vie privée et votre liberté : lisez notre Guide de Confidentialité : Rapport Hebdomadaire : https://e.foundation/wp-content/uploads/2025/01/White_Paper_-_Privacy_-_FR.pdf + + Choqué ? + + Faites passer le mot ! + Nouvel espion en ville ! + Cette semaine + a introduit un nouveau pisteur. + a introduit un nouveau pisteur ! + + Voir le pisteur + Nouvelles tentatives de surveillance : + %s nouveaux pisteurs + détectés cette semaine. + détectés cette semaine sur mon smartphone. + Voir tous + Nouveaux pisteurs découverts : + %s applications + ont ajouté de nouveaux pisteurs cachés cette semaine. + ont ajouté de nouveaux pisteurs cachés cette semaine sur mon smartphone. + Voir tous + Rapport Hebdomadaire Une notification hebdomadaire présentant un résumé des pisteurs, mettant en évidence les changements et mises à jour. \ No newline at end of file diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index be7cc7a66561cf240b4382b7d45c88039c4acd93..3f033800617a4e9c024e471d0fe7f00cab0320b8 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -160,6 +160,27 @@ Rapporto settimanale: https://e.foundation/wp-content/uploads/2025/01/White_Paper_-_Privacy_-_IT.pdf + + Scosso? + + Diffondi la notizia! + Nuova spia in città! + Questa settimana + ha introdotto un nuovo tracker. + ha introdotto un nuovo tracker! + + Visualizza tracker + Nuovi tentativi di sorveglianza: + %s nuovi tracker + rilevati questa settimana. + rilevati questa settimana sul mio smartphone. + Visualizza tutti + Nuovi tracker scoperti: + %s app + hanno aggiunto nuovi tracker nascosti questa settimana. + hanno aggiunto nuovi tracker nascosti questa settimana sul mio smartphone. + Visualizza tutti + Rapporto Settimanale Una notifica settimanale che mostra un riepilogo dei tracker, evidenziando eventuali modifiche e aggiornamenti. \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 240694c71d81f73c8ca998a257a4339690b898c2..3f588521897efc2014d88a1325171583e2809fc1 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -56,4 +56,8 @@ @color/e_secondary_text_color_light @color/e_disabled_color_light + + + #0D8CFF + #FFDA00 \ No newline at end of file diff --git a/app/src/main/res/values/debug_strings.xml b/app/src/main/res/values/debug_strings.xml index b56bef65efc4cdb52ee5624bfdfa17846f687a79..1b365ce5f899a51cb0ceeaf98c47c3aaf0761697 100644 --- a/app/src/main/res/values/debug_strings.xml +++ b/app/src/main/res/values/debug_strings.xml @@ -22,5 +22,7 @@ Share Details Notification + Set as current Show all reports views + onClick report action \ 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 921f63cddca541ba96dc24e1d20aa0edce2f7aab..63c32c480fd5f218e0c2e12741f6be76b71dd18d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -193,6 +193,26 @@ Weekly report: murena.com + Shocked? + + Spread the word! + New Spy in Town! + This week + introduced a fresh tracker. + app introduced a fresh tracker! + + View tracker + New Surveillance Attempts: + %s new trackers + detected this week. + detected this week on my smartphone. + View all + New Trackers Discovered: + %s apps + added new hidden trackers this week. + added new hidden trackers this week on my smartphone. + View all + @string/app_name diff --git a/trackers/src/main/java/foundation/e/advancedprivacy/trackers/data/StatsDatabase.kt b/trackers/src/main/java/foundation/e/advancedprivacy/trackers/data/StatsDatabase.kt index 79196c6cb92e58e26ed2b6b68f5bcab0f3e533f1..49b9f4c1c60ebceadec7774cf54a0cc7d551d6f6 100644 --- a/trackers/src/main/java/foundation/e/advancedprivacy/trackers/data/StatsDatabase.kt +++ b/trackers/src/main/java/foundation/e/advancedprivacy/trackers/data/StatsDatabase.kt @@ -221,12 +221,12 @@ class StatsDatabase( } } - fun getDistinctTrackerAndApp(periodStart: Instant): List> { + fun getDistinctTrackerAndApp(periodStart: Instant, periodEnd: Instant): List> { synchronized(lock) { val db = readableDatabase val projection = arrayOf(COLUMN_NAME_APPID, COLUMN_NAME_TRACKER) - val selection = "$COLUMN_NAME_TIMESTAMP >= ?" - val selectionArg = arrayOf("" + periodStart.epochSecond) + val selection = "$COLUMN_NAME_TIMESTAMP >= ? AND $COLUMN_NAME_TIMESTAMP <= ?" + val selectionArg = arrayOf("" + periodStart.epochSecond, "" + periodEnd.epochSecond) val cursor = db.query( true, TABLE_NAME,