/* * Copyright (C) 2023 MURENA SAS * 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 . */ 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.FeatureState import foundation.e.advancedprivacy.domain.entities.QuickPrivacyState import foundation.e.advancedprivacy.domain.entities.TrackerMode import foundation.e.advancedprivacy.features.dashboard.DashboardFragmentArgs 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 import foundation.e.advancedprivacy.widget.WidgetCommandReceiver.Companion.PARAM_FEATURE_ENABLED data class State( val quickPrivacyState: QuickPrivacyState = QuickPrivacyState.DISABLED, val trackerMode: TrackerMode = TrackerMode.VULNERABLE, val isLocationHidden: Boolean = false, val ipScramblingMode: FeatureState = FeatureState.STOPPING, val dayStatistics: List> = 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 } ) ) val trackersEnabled = state.trackerMode != TrackerMode.VULNERABLE setImageViewResource( R.id.toggle_trackers, if (trackersEnabled) R.drawable.ic_switch_enabled else R.drawable.ic_switch_disabled ) setOnClickPendingIntent( R.id.toggle_trackers, PendingIntent.getBroadcast( context, REQUEST_CODE_TOGGLE_TRACKERS, Intent(context, WidgetCommandReceiver::class.java).apply { action = ACTION_TOGGLE_TRACKERS putExtra(PARAM_FEATURE_ENABLED, !trackersEnabled) }, 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 putExtra(PARAM_FEATURE_ENABLED, !state.isLocationHidden) }, 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 putExtra(PARAM_FEATURE_ENABLED, !state.ipScramblingMode.isChecked) }, FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT ) ) setTextViewText( R.id.state_ip_address, context.getString( if (state.ipScramblingMode == FeatureState.ON) 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 = MainActivity.deepLinkBuilder(context) .setDestination(R.id.trackersFragment) .createPendingIntent() 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 = MainActivity.deepLinkBuilder(context) .setDestination(R.id.dashboardFragment) .setArguments(DashboardFragmentArgs(highlightLeaks = index).toBundle()) .createPendingIntent() 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 } ) } }