diff options
author | Guillaume Jacquart <guillaume.jacquart@hoodbrains.com> | 2022-03-21 17:13:10 +0000 |
---|---|---|
committer | Guillaume Jacquart <guillaume.jacquart@hoodbrains.com> | 2022-03-21 17:13:10 +0000 |
commit | 43e303886715d6115273cfba014a54805d3a1389 (patch) | |
tree | 799c478bff90fcada978801801b198873aad9338 /app/src/main/java | |
parent | d534cee490986771896f4fd2ca07742007ab6751 (diff) |
Add PVC Widget #5076
Diffstat (limited to 'app/src/main/java')
10 files changed, 403 insertions, 5 deletions
diff --git a/app/src/main/java/foundation/e/privacycentralapp/DependencyContainer.kt b/app/src/main/java/foundation/e/privacycentralapp/DependencyContainer.kt index 639e7b4..fa4a3e3 100644 --- a/app/src/main/java/foundation/e/privacycentralapp/DependencyContainer.kt +++ b/app/src/main/java/foundation/e/privacycentralapp/DependencyContainer.kt @@ -43,6 +43,7 @@ import foundation.e.privacymodules.permissions.PermissionsPrivacyModule import foundation.e.privacymodules.permissions.data.ApplicationDescription import foundation.e.privacymodules.trackers.api.BlockTrackersPrivacyModule import foundation.e.privacymodules.trackers.api.TrackTrackersPrivacyModule +import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.GlobalScope /** @@ -76,7 +77,7 @@ class DependencyContainer(val app: Application) { private val appListsRepository by lazy { AppListsRepository(permissionsModule, context, GlobalScope) } // Usecases - private val getQuickPrivacyStateUseCase by lazy { + val getQuickPrivacyStateUseCase by lazy { GetQuickPrivacyStateUseCase(localStateRepository) } private val ipScramblingStateUseCase by lazy { @@ -87,7 +88,7 @@ class DependencyContainer(val app: Application) { } private val appListUseCase = AppListUseCase(appListsRepository) - private val trackersStatisticsUseCase by lazy { + val trackersStatisticsUseCase by lazy { TrackersStatisticsUseCase(trackTrackersPrivacyModule, blockTrackersPrivacyModule, appListsRepository, context.resources) } @@ -126,11 +127,21 @@ class DependencyContainer(val app: Application) { } // Background + @FlowPreview fun initBackgroundSingletons() { trackersStateUseCase ipScramblingStateUseCase fakeLocationStateUseCase UpdateTrackersWorker.periodicUpdate(context) + + Widget.startListening( + context, + getQuickPrivacyStateUseCase, + ipScramblingStateUseCase, + trackersStatisticsUseCase, + trackersStateUseCase, + fakeLocationStateUseCase + ) } } diff --git a/app/src/main/java/foundation/e/privacycentralapp/PrivacyCentralApplication.kt b/app/src/main/java/foundation/e/privacycentralapp/PrivacyCentralApplication.kt index 28e96e0..2d90c93 100644 --- a/app/src/main/java/foundation/e/privacycentralapp/PrivacyCentralApplication.kt +++ b/app/src/main/java/foundation/e/privacycentralapp/PrivacyCentralApplication.kt @@ -19,12 +19,14 @@ package foundation.e.privacycentralapp import android.app.Application import com.mapbox.mapboxsdk.Mapbox +import kotlinx.coroutines.FlowPreview class PrivacyCentralApplication : Application() { // Initialize the dependency container. val dependencyContainer: DependencyContainer by lazy { DependencyContainer(this) } + @FlowPreview override fun onCreate() { super.onCreate() Mapbox.getTelemetry()?.setUserTelemetryRequestState(false) diff --git a/app/src/main/java/foundation/e/privacycentralapp/common/GraphHolder.kt b/app/src/main/java/foundation/e/privacycentralapp/common/GraphHolder.kt index db6bc7e..929d838 100644 --- a/app/src/main/java/foundation/e/privacycentralapp/common/GraphHolder.kt +++ b/app/src/main/java/foundation/e/privacycentralapp/common/GraphHolder.kt @@ -34,6 +34,7 @@ import com.github.mikephil.charting.highlight.Highlight import com.github.mikephil.charting.listener.OnChartValueSelectedListener import com.github.mikephil.charting.utils.MPPointF import foundation.e.privacycentralapp.R +import foundation.e.privacycentralapp.extensions.dpToPxF class GraphHolder(val barChart: BarChart, val context: Context, val isMarkerAbove: Boolean = true) { var data = emptyList<Int>() @@ -113,8 +114,6 @@ class GraphHolder(val barChart: BarChart, val context: Context, val isMarkerAbov } } -private fun Int.dpToPxF(context: Context): Float = this.toFloat() * context.resources.displayMetrics.density - class PeriodMarkerView(context: Context, private val isMarkerAbove: Boolean = true) : MarkerView(context, R.layout.chart_tooltip) { enum class ArrowPosition { LEFT, CENTER, RIGHT } diff --git a/app/src/main/java/foundation/e/privacycentralapp/domain/usecases/TrackersStatisticsUseCase.kt b/app/src/main/java/foundation/e/privacycentralapp/domain/usecases/TrackersStatisticsUseCase.kt index ad8f565..69dd0d8 100644 --- a/app/src/main/java/foundation/e/privacycentralapp/domain/usecases/TrackersStatisticsUseCase.kt +++ b/app/src/main/java/foundation/e/privacycentralapp/domain/usecases/TrackersStatisticsUseCase.kt @@ -34,7 +34,8 @@ import java.time.format.DateTimeFormatter import java.time.temporal.ChronoUnit class TrackersStatisticsUseCase( - private val trackTrackersPrivacyModule: ITrackTrackersPrivacyModule, + // TODO private + val trackTrackersPrivacyModule: ITrackTrackersPrivacyModule, private val blockTrackersPrivacyModule: IBlockTrackersPrivacyModule, private val appListsRepository: AppListsRepository, private val resources: Resources @@ -46,6 +47,7 @@ class TrackersStatisticsUseCase( offer(Unit) } } + trackTrackersPrivacyModule.addListener(listener) awaitClose { trackTrackersPrivacyModule.removeListener(listener) } } @@ -57,6 +59,10 @@ class TrackersStatisticsUseCase( ) to trackTrackersPrivacyModule.getTrackersCount() } + fun getDayTrackersCalls() = trackTrackersPrivacyModule.getPastDayTrackersCalls() + + fun getDayTrackersCount() = trackTrackersPrivacyModule.getPastDayTrackersCount() + private fun buildDayLabels(): List<String> { val formater = DateTimeFormatter.ofPattern( resources.getString(R.string.trackers_graph_hours_period_format) diff --git a/app/src/main/java/foundation/e/privacycentralapp/domain/usecases/UpdateWidgetUseCase.kt b/app/src/main/java/foundation/e/privacycentralapp/domain/usecases/UpdateWidgetUseCase.kt new file mode 100644 index 0000000..dab0b18 --- /dev/null +++ b/app/src/main/java/foundation/e/privacycentralapp/domain/usecases/UpdateWidgetUseCase.kt @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2022 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 <https://www.gnu.org/licenses/>. + */ + +package foundation.e.privacycentralapp.domain.usecases + +import foundation.e.privacycentralapp.data.repositories.LocalStateRepository +import foundation.e.privacymodules.trackers.ITrackTrackersPrivacyModule + +class UpdateWidgetUseCase( + private val localStateRepository: LocalStateRepository, + private val trackTrackersPrivacyModule: ITrackTrackersPrivacyModule, +) { + init { + trackTrackersPrivacyModule.addListener(object : ITrackTrackersPrivacyModule.Listener { + override fun onNewData() { + } + }) + } +} diff --git a/app/src/main/java/foundation/e/privacycentralapp/extensions/AnyExtension.kt b/app/src/main/java/foundation/e/privacycentralapp/extensions/AnyExtension.kt index a870d33..2074b69 100644 --- a/app/src/main/java/foundation/e/privacycentralapp/extensions/AnyExtension.kt +++ b/app/src/main/java/foundation/e/privacycentralapp/extensions/AnyExtension.kt @@ -24,3 +24,5 @@ fun Any.toText(context: Context) = when (this) { is String -> this else -> this.toString() } + +fun Int.dpToPxF(context: Context): Float = this.toFloat() * context.resources.displayMetrics.density diff --git a/app/src/main/java/foundation/e/privacycentralapp/features/dashboard/DashboardFragment.kt b/app/src/main/java/foundation/e/privacycentralapp/features/dashboard/DashboardFragment.kt index 41f6509..4d191bd 100644 --- a/app/src/main/java/foundation/e/privacycentralapp/features/dashboard/DashboardFragment.kt +++ b/app/src/main/java/foundation/e/privacycentralapp/features/dashboard/DashboardFragment.kt @@ -150,6 +150,8 @@ class DashboardFragment : else R.drawable.ic_shield_off ) + binding.togglePrivacyCentral.isChecked = state.isQuickPrivacyEnabled + val trackersEnabled = state.isQuickPrivacyEnabled && state.isAllTrackersBlocked binding.stateTrackers.text = getString( if (trackersEnabled) R.string.dashboard_state_trackers_on diff --git a/app/src/main/java/foundation/e/privacycentralapp/widget/Widget.kt b/app/src/main/java/foundation/e/privacycentralapp/widget/Widget.kt new file mode 100644 index 0000000..1969fe5 --- /dev/null +++ b/app/src/main/java/foundation/e/privacycentralapp/widget/Widget.kt @@ -0,0 +1,137 @@ +/* + * Copyright (C) 2022 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 <https://www.gnu.org/licenses/>. + */ + +package foundation.e.privacycentralapp + +import android.appwidget.AppWidgetManager +import android.appwidget.AppWidgetProvider +import android.content.Context +import foundation.e.privacycentralapp.domain.usecases.FakeLocationStateUseCase +import foundation.e.privacycentralapp.domain.usecases.GetQuickPrivacyStateUseCase +import foundation.e.privacycentralapp.domain.usecases.IpScramblingStateUseCase +import foundation.e.privacycentralapp.domain.usecases.TrackersStateUseCase +import foundation.e.privacycentralapp.domain.usecases.TrackersStatisticsUseCase +import foundation.e.privacycentralapp.widget.State +import foundation.e.privacycentralapp.widget.render +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.sample +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch + +/** + * Implementation of App Widget functionality. + */ +class Widget : AppWidgetProvider() { + @FlowPreview + override fun onUpdate( + context: Context, + appWidgetManager: AppWidgetManager, + appWidgetIds: IntArray + ) { + render(context, state.value, appWidgetManager) + } + + override fun onEnabled(context: Context) { + // Enter relevant functionality for when the first widget is created + } + + override fun onDisabled(context: Context) { + // Enter relevant functionality for when the last widget is disabled + } + + @FlowPreview + companion object { + private var updateWidgetJob: Job? = null + + private var state: StateFlow<State> = MutableStateFlow(State()) + + private fun initState( + getPrivacyStateUseCase: GetQuickPrivacyStateUseCase, + ipScramblingStateUseCase: IpScramblingStateUseCase, + trackersStatisticsUseCase: TrackersStatisticsUseCase, + trackersStateUseCase: TrackersStateUseCase, + fakeLocationStateUseCase: FakeLocationStateUseCase, + coroutineScope: CoroutineScope + ): StateFlow<State> { + + return combine( + getPrivacyStateUseCase.quickPrivacyEnabledFlow, + trackersStateUseCase.areAllTrackersBlocked, + fakeLocationStateUseCase.locationMode, + ipScramblingStateUseCase.internetPrivacyMode + ) { isQuickPrivacyEnabled, isAllTrackersBlocked, locationMode, internetPrivacyMode -> + + State( + isQuickPrivacyEnabled = isQuickPrivacyEnabled, + isAllTrackersBlocked = isAllTrackersBlocked, + locationMode = locationMode, + internetPrivacyMode = internetPrivacyMode + ) + }.sample(50) + .combine( + trackersStatisticsUseCase.listenUpdates() + .onStart { emit(Unit) } + .debounce(5000) + ) { state, _ -> + state.copy( + dayStatistics = trackersStatisticsUseCase.getDayTrackersCalls(), + activeTrackersCount = trackersStatisticsUseCase.getDayTrackersCount() + ) + }.stateIn( + scope = coroutineScope, + started = SharingStarted.Eagerly, + initialValue = State() + ) + } + + fun startListening( + appContext: Context, + getPrivacyStateUseCase: GetQuickPrivacyStateUseCase, + ipScramblingStateUseCase: IpScramblingStateUseCase, + trackersStatisticsUseCase: TrackersStatisticsUseCase, + trackersStateUseCase: TrackersStateUseCase, + fakeLocationStateUseCase: FakeLocationStateUseCase + ) { + state = initState( + getPrivacyStateUseCase, + ipScramblingStateUseCase, + trackersStatisticsUseCase, + trackersStateUseCase, + fakeLocationStateUseCase, + GlobalScope + ) + + updateWidgetJob?.cancel() + updateWidgetJob = GlobalScope.launch(Dispatchers.Main) { + state.collect { + render(appContext, it, AppWidgetManager.getInstance(appContext)) + } + } + } + } +} diff --git a/app/src/main/java/foundation/e/privacycentralapp/widget/WidgetCommandReceiver.kt b/app/src/main/java/foundation/e/privacycentralapp/widget/WidgetCommandReceiver.kt new file mode 100644 index 0000000..87e88df --- /dev/null +++ b/app/src/main/java/foundation/e/privacycentralapp/widget/WidgetCommandReceiver.kt @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2022 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 <https://www.gnu.org/licenses/>. + */ + +package foundation.e.privacycentralapp.widget + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import foundation.e.privacycentralapp.PrivacyCentralApplication + +class WidgetCommandReceiver : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + when (intent?.action) { + ACTION_TOGGLE_PRIVACY -> { + (context?.applicationContext as? PrivacyCentralApplication) + ?.dependencyContainer?.getQuickPrivacyStateUseCase?.toggle() + } + else -> {} + } + } + + companion object { + const val ACTION_TOGGLE_PRIVACY = "toggle_privacy" + } +} diff --git a/app/src/main/java/foundation/e/privacycentralapp/widget/WidgetUI.kt b/app/src/main/java/foundation/e/privacycentralapp/widget/WidgetUI.kt new file mode 100644 index 0000000..ae2238f --- /dev/null +++ b/app/src/main/java/foundation/e/privacycentralapp/widget/WidgetUI.kt @@ -0,0 +1,167 @@ +/* + * Copyright (C) 2022 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 <https://www.gnu.org/licenses/>. + */ + +package foundation.e.privacycentralapp.widget + +import android.app.PendingIntent +import android.app.PendingIntent.FLAG_UPDATE_CURRENT +import android.appwidget.AppWidgetManager +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.view.View +import android.widget.RemoteViews +import foundation.e.privacycentralapp.R +import foundation.e.privacycentralapp.Widget +import foundation.e.privacycentralapp.domain.entities.InternetPrivacyMode +import foundation.e.privacycentralapp.domain.entities.LocationMode +import foundation.e.privacycentralapp.extensions.dpToPxF +import foundation.e.privacycentralapp.main.MainActivity +import foundation.e.privacycentralapp.widget.WidgetCommandReceiver.Companion.ACTION_TOGGLE_PRIVACY +import kotlinx.coroutines.FlowPreview + +data class State( + val isQuickPrivacyEnabled: Boolean = false, + val isAllTrackersBlocked: Boolean = false, + val locationMode: LocationMode = LocationMode.REAL_LOCATION, + val internetPrivacyMode: InternetPrivacyMode = InternetPrivacyMode.REAL_IP, + val dayStatistics: List<Int> = emptyList(), + val activeTrackersCount: Int = 0, +) { + val isTrackersDenied get() = isQuickPrivacyEnabled && isAllTrackersBlocked + val isLocationHidden get() = isQuickPrivacyEnabled && locationMode != LocationMode.REAL_LOCATION +} + +@FlowPreview +fun render( + context: Context, + state: State, + appWidgetManager: AppWidgetManager, +) { + val views = RemoteViews(context.packageName, R.layout.widget) + views.apply { + setOnClickPendingIntent( + R.id.settings_btn, + PendingIntent.getActivity( + context, 0, Intent(context, MainActivity::class.java), FLAG_UPDATE_CURRENT + ) + ) + + setImageViewResource( + R.id.state_icon, + if (state.isQuickPrivacyEnabled) R.drawable.ic_shield_on else R.drawable.ic_shield_off + ) + setTextViewText( + R.id.state_label, + context.getString( + if (state.isQuickPrivacyEnabled) R.string.widget_state_title_on + else R.string.widget_state_title_off + ) + ) + setImageViewResource( + R.id.toggle_privacy_central, + if (state.isQuickPrivacyEnabled) R.drawable.ic_switch_enabled + else R.drawable.ic_switch_disabled + ) + + setOnClickPendingIntent( + R.id.toggle_privacy_central, + PendingIntent.getBroadcast( + context, + 0, + Intent(context, WidgetCommandReceiver::class.java).apply { + action = ACTION_TOGGLE_PRIVACY + }, + FLAG_UPDATE_CURRENT + ) + ) + + setTextViewText( + R.id.state_trackers, + context.getString( + if (state.isTrackersDenied) R.string.widget_state_trackers_on + else R.string.widget_state_trackers_off + ) + ) + + setTextViewText( + R.id.state_geolocation, + context.getString( + if (state.isLocationHidden) R.string.widget_state_geolocation_on + else R.string.widget_state_geolocation_off + ) + ) + + setTextViewText( + R.id.state_ip_address, + context.getString( + if (state.internetPrivacyMode != InternetPrivacyMode.HIDE_IP) + R.string.widget_state_ipaddress_off + else R.string.widget_state_title_on + ) + ) + + val loading = state.internetPrivacyMode in listOf( + InternetPrivacyMode.HIDE_IP_LOADING, + InternetPrivacyMode.REAL_IP_LOADING + ) + + setViewVisibility(R.id.state_ip_address, if (loading) View.GONE else View.VISIBLE) + + setViewVisibility(R.id.state_ip_address_loader, if (loading) View.VISIBLE else View.GONE) + + val graphHeightPx = 26.dpToPxF(context) + val maxValue = state.dayStatistics.maxOrNull().let { if (it == null || it == 0) 1 else it } + val ratio = graphHeightPx / maxValue + + state.dayStatistics.zip(barIds).forEach { (value, viewId) -> + val topPadding = graphHeightPx - value * ratio + setViewPadding(viewId, 0, topPadding.toInt(), 0, 0) + } + + setTextViewText(R.id.graph_legend, context.getString(R.string.widget_graph_trackers_legend, state.activeTrackersCount.toString())) + } + + appWidgetManager.updateAppWidget(ComponentName(context, Widget::class.java), views) +} + +private val barIds = listOf( + R.id.widget_graph_bar_0, + R.id.widget_graph_bar_1, + R.id.widget_graph_bar_2, + R.id.widget_graph_bar_3, + R.id.widget_graph_bar_4, + R.id.widget_graph_bar_5, + R.id.widget_graph_bar_6, + R.id.widget_graph_bar_7, + R.id.widget_graph_bar_8, + R.id.widget_graph_bar_9, + R.id.widget_graph_bar_10, + R.id.widget_graph_bar_11, + R.id.widget_graph_bar_12, + R.id.widget_graph_bar_13, + R.id.widget_graph_bar_14, + R.id.widget_graph_bar_15, + R.id.widget_graph_bar_16, + R.id.widget_graph_bar_17, + R.id.widget_graph_bar_18, + R.id.widget_graph_bar_19, + R.id.widget_graph_bar_20, + R.id.widget_graph_bar_21, + R.id.widget_graph_bar_22, + R.id.widget_graph_bar_23 +) |