summaryrefslogtreecommitdiff
path: root/app/src/main/java/foundation/e
diff options
context:
space:
mode:
authorGuillaume Jacquart <guillaume.jacquart@hoodbrains.com>2022-03-21 17:13:10 +0000
committerGuillaume Jacquart <guillaume.jacquart@hoodbrains.com>2022-03-21 17:13:10 +0000
commit43e303886715d6115273cfba014a54805d3a1389 (patch)
tree799c478bff90fcada978801801b198873aad9338 /app/src/main/java/foundation/e
parentd534cee490986771896f4fd2ca07742007ab6751 (diff)
Add PVC Widget #5076
Diffstat (limited to 'app/src/main/java/foundation/e')
-rw-r--r--app/src/main/java/foundation/e/privacycentralapp/DependencyContainer.kt15
-rw-r--r--app/src/main/java/foundation/e/privacycentralapp/PrivacyCentralApplication.kt2
-rw-r--r--app/src/main/java/foundation/e/privacycentralapp/common/GraphHolder.kt3
-rw-r--r--app/src/main/java/foundation/e/privacycentralapp/domain/usecases/TrackersStatisticsUseCase.kt8
-rw-r--r--app/src/main/java/foundation/e/privacycentralapp/domain/usecases/UpdateWidgetUseCase.kt33
-rw-r--r--app/src/main/java/foundation/e/privacycentralapp/extensions/AnyExtension.kt2
-rw-r--r--app/src/main/java/foundation/e/privacycentralapp/features/dashboard/DashboardFragment.kt2
-rw-r--r--app/src/main/java/foundation/e/privacycentralapp/widget/Widget.kt137
-rw-r--r--app/src/main/java/foundation/e/privacycentralapp/widget/WidgetCommandReceiver.kt39
-rw-r--r--app/src/main/java/foundation/e/privacycentralapp/widget/WidgetUI.kt167
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
+)