aboutsummaryrefslogtreecommitdiffstats
path: root/app/src/main/java/foundation/e/advancedprivacy/widget
diff options
context:
space:
mode:
Diffstat (limited to 'app/src/main/java/foundation/e/advancedprivacy/widget')
-rw-r--r--app/src/main/java/foundation/e/advancedprivacy/widget/Widget.kt156
-rw-r--r--app/src/main/java/foundation/e/advancedprivacy/widget/WidgetCommandReceiver.kt42
-rw-r--r--app/src/main/java/foundation/e/advancedprivacy/widget/WidgetUI.kt381
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
+ }
+ )
+ }
+}