diff options
author | Guillaume Jacquart <guillaume.jacquart@hoodbrains.com> | 2023-05-09 06:00:43 +0000 |
---|---|---|
committer | Guillaume Jacquart <guillaume.jacquart@hoodbrains.com> | 2023-05-09 06:00:43 +0000 |
commit | 5a432ecde520ee039786848296e5227571404158 (patch) | |
tree | 077eafb42d5d2d18b2ffc03bc93d9a8654377774 /app/src/main/java/foundation/e/advancedprivacy/widget | |
parent | a348c8196a643e4f5853587133b05e3ec2cd0faa (diff) | |
parent | a8874167f663885f2d3371801cf03681576ac817 (diff) | |
download | advanced-privacy-5a432ecde520ee039786848296e5227571404158.tar.gz |
Merge branch '1200-move_package_to_advanced_privacy' into 'main'
1200: rename everything to AdvancedPrivacy
See merge request e/os/advanced-privacy!128
Diffstat (limited to 'app/src/main/java/foundation/e/advancedprivacy/widget')
3 files changed, 579 insertions, 0 deletions
diff --git a/app/src/main/java/foundation/e/advancedprivacy/widget/Widget.kt b/app/src/main/java/foundation/e/advancedprivacy/widget/Widget.kt new file mode 100644 index 0000000..a4272e2 --- /dev/null +++ b/app/src/main/java/foundation/e/advancedprivacy/widget/Widget.kt @@ -0,0 +1,156 @@ +/* + * 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.advancedprivacy + +import android.appwidget.AppWidgetManager +import android.appwidget.AppWidgetProvider +import android.content.Context +import android.os.Bundle +import foundation.e.advancedprivacy.domain.usecases.GetQuickPrivacyStateUseCase +import foundation.e.advancedprivacy.domain.usecases.TrackersStatisticsUseCase +import foundation.e.advancedprivacy.widget.State +import foundation.e.advancedprivacy.widget.render +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.merge +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.sample +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch +import java.time.temporal.ChronoUnit + +/** + * Implementation of App Widget functionality. + */ +class Widget : AppWidgetProvider() { + + 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 + } + + companion object { + private var updateWidgetJob: Job? = null + + private var state: StateFlow<State> = MutableStateFlow(State()) + + private const val DARK_TEXT_KEY = "foundation.e.blisslauncher.WIDGET_OPTION_DARK_TEXT" + var isDarkText = false + + @OptIn(FlowPreview::class) + private fun initState( + getPrivacyStateUseCase: GetQuickPrivacyStateUseCase, + trackersStatisticsUseCase: TrackersStatisticsUseCase, + coroutineScope: CoroutineScope + ): StateFlow<State> { + + return combine( + getPrivacyStateUseCase.quickPrivacyState, + getPrivacyStateUseCase.trackerMode, + getPrivacyStateUseCase.isLocationHidden, + getPrivacyStateUseCase.ipScramblingMode, + ) { quickPrivacyState, trackerMode, isLocationHidden, ipScramblingMode -> + + State( + quickPrivacyState = quickPrivacyState, + trackerMode = trackerMode, + isLocationHidden = isLocationHidden, + ipScramblingMode = ipScramblingMode + ) + }.sample(50) + .combine( + merge( + trackersStatisticsUseCase.listenUpdates() + .onStart { emit(Unit) } + .debounce(5000), + flow { + while (true) { + emit(Unit) + delay(ChronoUnit.HOURS.duration.toMillis()) + } + } + + ) + ) { state, _ -> + state.copy( + dayStatistics = trackersStatisticsUseCase.getDayTrackersCalls(), + activeTrackersCount = trackersStatisticsUseCase.getDayTrackersCount() + ) + }.stateIn( + scope = coroutineScope, + started = SharingStarted.Eagerly, + initialValue = State() + ) + } + + @OptIn(DelicateCoroutinesApi::class) + fun startListening( + appContext: Context, + getPrivacyStateUseCase: GetQuickPrivacyStateUseCase, + trackersStatisticsUseCase: TrackersStatisticsUseCase, + ) { + state = initState( + getPrivacyStateUseCase, + trackersStatisticsUseCase, + GlobalScope + ) + + updateWidgetJob?.cancel() + updateWidgetJob = GlobalScope.launch(Dispatchers.Main) { + state.collect { + render(appContext, it, AppWidgetManager.getInstance(appContext)) + } + } + } + } + + override fun onAppWidgetOptionsChanged( + context: Context, + appWidgetManager: AppWidgetManager, + appWidgetId: Int, + newOptions: Bundle? + ) { + super.onAppWidgetOptionsChanged(context, appWidgetManager, appWidgetId, newOptions) + if (newOptions != null) { + isDarkText = newOptions.getBoolean(DARK_TEXT_KEY) + } + render(context, state.value, appWidgetManager) + } +} diff --git a/app/src/main/java/foundation/e/advancedprivacy/widget/WidgetCommandReceiver.kt b/app/src/main/java/foundation/e/advancedprivacy/widget/WidgetCommandReceiver.kt new file mode 100644 index 0000000..f68a59c --- /dev/null +++ b/app/src/main/java/foundation/e/advancedprivacy/widget/WidgetCommandReceiver.kt @@ -0,0 +1,42 @@ +/* + * 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.advancedprivacy.widget + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import foundation.e.advancedprivacy.AdvancedPrivacyApplication + +class WidgetCommandReceiver : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + val getQuickPrivacyStateUseCase = (context?.applicationContext as? AdvancedPrivacyApplication)?.dependencyContainer?.getQuickPrivacyStateUseCase + + when (intent?.action) { + ACTION_TOGGLE_TRACKERS -> getQuickPrivacyStateUseCase?.toggleTrackers() + ACTION_TOGGLE_LOCATION -> getQuickPrivacyStateUseCase?.toggleLocation() + ACTION_TOGGLE_IPSCRAMBLING -> getQuickPrivacyStateUseCase?.toggleIpScrambling() + else -> {} + } + } + + companion object { + const val ACTION_TOGGLE_TRACKERS = "toggle_trackers" + const val ACTION_TOGGLE_LOCATION = "toggle_location" + const val ACTION_TOGGLE_IPSCRAMBLING = "toggle_ipscrambling" + } +} diff --git a/app/src/main/java/foundation/e/advancedprivacy/widget/WidgetUI.kt b/app/src/main/java/foundation/e/advancedprivacy/widget/WidgetUI.kt new file mode 100644 index 0000000..cb7fe5c --- /dev/null +++ b/app/src/main/java/foundation/e/advancedprivacy/widget/WidgetUI.kt @@ -0,0 +1,381 @@ +/* + * 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.advancedprivacy.widget + +import android.app.PendingIntent +import android.app.PendingIntent.FLAG_IMMUTABLE +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.advancedprivacy.R +import foundation.e.advancedprivacy.Widget +import foundation.e.advancedprivacy.Widget.Companion.isDarkText +import foundation.e.advancedprivacy.common.extensions.dpToPxF +import foundation.e.advancedprivacy.domain.entities.InternetPrivacyMode +import foundation.e.advancedprivacy.domain.entities.QuickPrivacyState +import foundation.e.advancedprivacy.domain.entities.TrackerMode +import foundation.e.advancedprivacy.main.MainActivity +import foundation.e.advancedprivacy.widget.WidgetCommandReceiver.Companion.ACTION_TOGGLE_IPSCRAMBLING +import foundation.e.advancedprivacy.widget.WidgetCommandReceiver.Companion.ACTION_TOGGLE_LOCATION +import foundation.e.advancedprivacy.widget.WidgetCommandReceiver.Companion.ACTION_TOGGLE_TRACKERS + +data class State( + val quickPrivacyState: QuickPrivacyState = QuickPrivacyState.DISABLED, + val trackerMode: TrackerMode = TrackerMode.VULNERABLE, + val isLocationHidden: Boolean = false, + val ipScramblingMode: InternetPrivacyMode = InternetPrivacyMode.REAL_IP_LOADING, + val dayStatistics: List<Pair<Int, Int>> = emptyList(), + val activeTrackersCount: Int = 0, +) + +fun render( + context: Context, + state: State, + appWidgetManager: AppWidgetManager, +) { + val views = RemoteViews(context.packageName, R.layout.widget) + applyDarkText(context, state, views) + views.apply { + val openPIntent = PendingIntent.getActivity( + context, + REQUEST_CODE_DASHBOARD, + Intent(context, MainActivity::class.java), + FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT + ) + setOnClickPendingIntent(R.id.settings_btn, openPIntent) + setOnClickPendingIntent(R.id.widget_container, openPIntent) + + setTextViewText( + R.id.state_label, + context.getString( + when (state.quickPrivacyState) { + QuickPrivacyState.DISABLED -> R.string.widget_state_title_off + QuickPrivacyState.FULL_ENABLED -> R.string.widget_state_title_on + QuickPrivacyState.ENABLED -> R.string.widget_state_title_custom + } + ) + ) + + setImageViewResource( + R.id.toggle_trackers, + if (state.trackerMode == TrackerMode.VULNERABLE) + R.drawable.ic_switch_disabled + else R.drawable.ic_switch_enabled + ) + + setOnClickPendingIntent( + R.id.toggle_trackers, + PendingIntent.getBroadcast( + context, + REQUEST_CODE_TOGGLE_TRACKERS, + Intent(context, WidgetCommandReceiver::class.java).apply { + action = ACTION_TOGGLE_TRACKERS + }, + FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT + ) + ) + + setTextViewText( + R.id.state_trackers, + context.getString( + when (state.trackerMode) { + TrackerMode.DENIED -> R.string.widget_state_trackers_on + TrackerMode.VULNERABLE -> R.string.widget_state_trackers_off + TrackerMode.CUSTOM -> R.string.widget_state_trackers_custom + } + ) + ) + + setImageViewResource( + R.id.toggle_location, + if (state.isLocationHidden) R.drawable.ic_switch_enabled + else R.drawable.ic_switch_disabled + ) + + setOnClickPendingIntent( + R.id.toggle_location, + PendingIntent.getBroadcast( + context, + REQUEST_CODE_TOGGLE_LOCATION, + Intent(context, WidgetCommandReceiver::class.java).apply { + action = ACTION_TOGGLE_LOCATION + }, + FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT + ) + ) + + setTextViewText( + R.id.state_geolocation, + context.getString( + if (state.isLocationHidden) R.string.widget_state_geolocation_on + else R.string.widget_state_geolocation_off + ) + ) + + setImageViewResource( + R.id.toggle_ipscrambling, + if (state.ipScramblingMode.isChecked) R.drawable.ic_switch_enabled + else R.drawable.ic_switch_disabled + ) + + setOnClickPendingIntent( + R.id.toggle_ipscrambling, + PendingIntent.getBroadcast( + context, + REQUEST_CODE_TOGGLE_IPSCRAMBLING, + Intent(context, WidgetCommandReceiver::class.java).apply { + action = ACTION_TOGGLE_IPSCRAMBLING + }, + FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT + ) + ) + + setTextViewText( + R.id.state_ip_address, + context.getString( + if (state.ipScramblingMode == InternetPrivacyMode.HIDE_IP) R.string.widget_state_ipaddress_on + else R.string.widget_state_ipaddress_off + ) + ) + + val loading = state.ipScramblingMode.isLoading + + 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) + + if (state.dayStatistics.all { it.first == 0 && it.second == 0 }) { + setViewVisibility(R.id.graph, View.GONE) + setViewVisibility(R.id.graph_legend, View.GONE) + setViewVisibility(R.id.graph_empty, View.VISIBLE) + setViewVisibility(R.id.graph_legend_values, View.GONE) + setViewVisibility(R.id.graph_view_trackers_btn, View.GONE) + } else { + setViewVisibility(R.id.graph, View.VISIBLE) + setViewVisibility(R.id.graph_legend, View.VISIBLE) + setViewVisibility(R.id.graph_empty, View.GONE) + setViewVisibility(R.id.graph_legend_values, View.VISIBLE) + setViewVisibility(R.id.graph_view_trackers_btn, View.VISIBLE) + + val pIntent = PendingIntent.getActivity( + context, + REQUEST_CODE_TRACKERS, + MainActivity.createTrackersIntent(context), + FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT + ) + + setOnClickPendingIntent(R.id.graph_view_trackers_btn, pIntent) + + val graphHeightPx = 26.dpToPxF(context) + val maxValue = + state.dayStatistics + .map { it.first + it.second } + .maxOrNull() + .let { if (it == null || it == 0) 1 else it } + val ratio = graphHeightPx / maxValue + + state.dayStatistics.forEachIndexed { index, (blocked, leaked) -> + // blocked (the bar below) + val middlePadding = graphHeightPx - blocked * ratio + setViewPadding(blockedBarIds[index], 0, middlePadding.toInt(), 0, 0) + + // leaked (the bar above) + val topPadding = graphHeightPx - (blocked + leaked) * ratio + setViewPadding(leakedBarIds[index], 0, topPadding.toInt(), 0, 0) + + val highlightPIntent = PendingIntent.getActivity( + context, REQUEST_CODE_HIGHLIGHT + index, + MainActivity.createHighlightLeaksIntent(context, index), + FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT + ) + setOnClickPendingIntent(containerBarIds[index], highlightPIntent) + } + + 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 containerBarIds = listOf( + R.id.widget_graph_bar_container_0, + R.id.widget_graph_bar_container_1, + R.id.widget_graph_bar_container_2, + R.id.widget_graph_bar_container_3, + R.id.widget_graph_bar_container_4, + R.id.widget_graph_bar_container_5, + R.id.widget_graph_bar_container_6, + R.id.widget_graph_bar_container_7, + R.id.widget_graph_bar_container_8, + R.id.widget_graph_bar_container_9, + R.id.widget_graph_bar_container_10, + R.id.widget_graph_bar_container_11, + R.id.widget_graph_bar_container_12, + R.id.widget_graph_bar_container_13, + R.id.widget_graph_bar_container_14, + R.id.widget_graph_bar_container_15, + R.id.widget_graph_bar_container_16, + R.id.widget_graph_bar_container_17, + R.id.widget_graph_bar_container_18, + R.id.widget_graph_bar_container_19, + R.id.widget_graph_bar_container_20, + R.id.widget_graph_bar_container_21, + R.id.widget_graph_bar_container_22, + R.id.widget_graph_bar_container_23, +) + +private val blockedBarIds = 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 +) + +private val leakedBarIds = listOf( + R.id.widget_leaked_graph_bar_0, + R.id.widget_leaked_graph_bar_1, + R.id.widget_leaked_graph_bar_2, + R.id.widget_leaked_graph_bar_3, + R.id.widget_leaked_graph_bar_4, + R.id.widget_leaked_graph_bar_5, + R.id.widget_leaked_graph_bar_6, + R.id.widget_leaked_graph_bar_7, + R.id.widget_leaked_graph_bar_8, + R.id.widget_leaked_graph_bar_9, + R.id.widget_leaked_graph_bar_10, + R.id.widget_leaked_graph_bar_11, + R.id.widget_leaked_graph_bar_12, + R.id.widget_leaked_graph_bar_13, + R.id.widget_leaked_graph_bar_14, + R.id.widget_leaked_graph_bar_15, + R.id.widget_leaked_graph_bar_16, + R.id.widget_leaked_graph_bar_17, + R.id.widget_leaked_graph_bar_18, + R.id.widget_leaked_graph_bar_19, + R.id.widget_leaked_graph_bar_20, + R.id.widget_leaked_graph_bar_21, + R.id.widget_leaked_graph_bar_22, + R.id.widget_leaked_graph_bar_23 +) + +private const val REQUEST_CODE_DASHBOARD = 1 +private const val REQUEST_CODE_TRACKERS = 3 +private const val REQUEST_CODE_TOGGLE_TRACKERS = 4 +private const val REQUEST_CODE_TOGGLE_LOCATION = 5 +private const val REQUEST_CODE_TOGGLE_IPSCRAMBLING = 6 +private const val REQUEST_CODE_HIGHLIGHT = 100 + +fun applyDarkText(context: Context, state: State, views: RemoteViews) { + views.apply { + listOf( + R.id.state_label, + R.id.graph_legend_blocked, + R.id.graph_legend_allowed, + + ) + .forEach { + setTextColor( + it, + context.getColor(if (isDarkText) R.color.on_surface_disabled_light else R.color.on_primary_medium_emphasis) + ) + } + setTextColor( + R.id.widget_title, + context.getColor(if (isDarkText) R.color.on_surface_medium_emphasis_light else R.color.on_surface_high_emphasis) + ) + listOf( + R.id.state_trackers, + R.id.state_geolocation, + R.id.state_ip_address, + R.id.graph_legend, + R.id.graph_view_trackers_btn + ) + .forEach { + setTextColor( + it, + context.getColor(if (isDarkText) R.color.on_surface_medium_emphasis_light else R.color.on_primary_high_emphasis) + ) + } + + listOf( + R.id.trackers_label, + R.id.geolocation_label, + R.id.ip_address_label, + R.id.graph_empty + + ) + .forEach { + setTextColor( + it, + context.getColor(if (isDarkText) R.color.on_surface_disabled_light else R.color.on_primary_disabled) + ) + } + setTextViewCompoundDrawables( + R.id.graph_view_trackers_btn, + 0, + 0, + if (isDarkText) R.drawable.ic_chevron_right_24dp_light else R.drawable.ic_chevron_right_24dp, + 0 + ) + setImageViewResource( + R.id.settings_btn, + if (isDarkText) R.drawable.ic_settings_light else R.drawable.ic_settings + ) + setImageViewResource( + R.id.state_icon, + if (isDarkText) { + if (state.quickPrivacyState.isEnabled()) R.drawable.ic_shield_on_light + else R.drawable.ic_shield_off_light + } else { + if (state.quickPrivacyState.isEnabled()) R.drawable.ic_shield_on_white + else R.drawable.ic_shield_off_white + } + ) + } +} |