diff --git a/uhabits-android/src/main/AndroidManifest.xml b/uhabits-android/src/main/AndroidManifest.xml index 36f3928c0..1d9dc6620 100644 --- a/uhabits-android/src/main/AndroidManifest.xml +++ b/uhabits-android/src/main/AndroidManifest.xml @@ -128,6 +128,15 @@ + + + + + + diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/intents/PendingIntentFactory.kt b/uhabits-android/src/main/java/org/isoron/uhabits/intents/PendingIntentFactory.kt index 03775abeb..45a70cd14 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/intents/PendingIntentFactory.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/intents/PendingIntentFactory.kt @@ -31,6 +31,7 @@ import android.net.Uri import android.os.Build import org.isoron.uhabits.activities.habits.list.ListHabitsActivity import org.isoron.uhabits.activities.habits.show.ShowHabitActivity +import org.isoron.uhabits.activities.habits.show.ShowHabitGroupActivity import org.isoron.uhabits.core.AppScope import org.isoron.uhabits.core.models.Habit import org.isoron.uhabits.core.models.HabitGroup @@ -124,6 +125,15 @@ class PendingIntentFactory ) } + fun showHabitGroupTemplate(): PendingIntent { + return getActivity( + context, + 0, + Intent(context, ShowHabitGroupActivity::class.java), + getIntentTemplateFlags() + ) + } + fun showHabitFillIn(habit: Habit) = Intent().apply { data = Uri.parse(habit.uriString) diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/widgets/BaseWidgetProvider.kt b/uhabits-android/src/main/java/org/isoron/uhabits/widgets/BaseWidgetProvider.kt index a93b81de8..a22fbb4ae 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/widgets/BaseWidgetProvider.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/widgets/BaseWidgetProvider.kt @@ -27,6 +27,7 @@ import android.widget.RemoteViews import org.isoron.uhabits.HabitsApplication import org.isoron.uhabits.R import org.isoron.uhabits.core.models.Habit +import org.isoron.uhabits.core.models.HabitGroup import org.isoron.uhabits.core.models.HabitGroupList import org.isoron.uhabits.core.models.HabitList import org.isoron.uhabits.core.models.HabitNotFoundException @@ -114,12 +115,25 @@ abstract class BaseWidgetProvider : AppWidgetProvider() { val selectedHabits = ArrayList(selectedIds.size) for (id in selectedIds) { val h = habits.getById(id) ?: habitGroups.getHabitByID(id) - ?: throw HabitNotFoundException() - selectedHabits.add(h) + if (h != null) { + selectedHabits.add(h) + } } return selectedHabits } + protected fun getHabitGroupsFromWidgetId(widgetId: Int): List { + val selectedIds = widgetPrefs.getHabitIdsFromWidgetId(widgetId) + val selectedHabitGroups = ArrayList(selectedIds.size) + for (id in selectedIds) { + val hgr = habitGroups.getById(id) + if (hgr != null) { + selectedHabitGroups.add(hgr) + } + } + return selectedHabitGroups + } + protected abstract fun getWidgetFromId( context: Context, id: Int diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/widgets/FrequencyWidget.kt b/uhabits-android/src/main/java/org/isoron/uhabits/widgets/FrequencyWidget.kt index 88c6b7655..9ae960695 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/widgets/FrequencyWidget.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/widgets/FrequencyWidget.kt @@ -25,32 +25,51 @@ import android.view.View import org.isoron.platform.gui.toInt import org.isoron.uhabits.activities.common.views.FrequencyChart import org.isoron.uhabits.core.models.Habit +import org.isoron.uhabits.core.models.HabitGroup +import org.isoron.uhabits.core.ui.screens.habits.show.views.FrequencyCardPresenter import org.isoron.uhabits.core.ui.views.WidgetTheme import org.isoron.uhabits.widgets.views.GraphWidgetView -class FrequencyWidget( +class FrequencyWidget private constructor( context: Context, widgetId: Int, - private val habit: Habit, + private val habit: Habit?, + private val habitGroup: HabitGroup?, private val firstWeekday: Int, - stacked: Boolean = false + stacked: Boolean ) : BaseWidget(context, widgetId, stacked) { + + constructor(context: Context, widgetId: Int, habit: Habit, firstWeekday: Int, stacked: Boolean = false) : this(context, widgetId, habit, null, firstWeekday, stacked) + constructor(context: Context, widgetId: Int, habitGroup: HabitGroup, firstWeekday: Int, stacked: Boolean = false) : this(context, widgetId, null, habitGroup, firstWeekday, stacked) + override val defaultHeight: Int = 200 override val defaultWidth: Int = 200 - override fun getOnClickPendingIntent(context: Context): PendingIntent = - pendingIntentFactory.showHabit(habit) + override fun getOnClickPendingIntent(context: Context): PendingIntent { + return if (habit != null) { + pendingIntentFactory.showHabit(habit) + } else { + pendingIntentFactory.showHabitGroup(habitGroup!!) + } + } override fun refreshData(v: View) { val widgetView = v as GraphWidgetView - widgetView.setTitle(habit.name) + widgetView.setTitle(habit?.name ?: habitGroup!!.name) widgetView.setBackgroundAlpha(preferedBackgroundAlpha) if (preferedBackgroundAlpha >= 255) widgetView.setShadowAlpha(0x4f) + val color = habit?.color ?: habitGroup!!.color + val isNumerical = habit?.isNumerical ?: true + val frequency = if (habit != null) { + habit.originalEntries.computeWeekdayFrequency(habit.isNumerical) + } else { + FrequencyCardPresenter.getFrequenciesFromHabitGroup(habitGroup!!) + } (widgetView.dataView as FrequencyChart).apply { setFirstWeekday(firstWeekday) - setColor(WidgetTheme().color(habit.color).toInt()) - setIsNumerical(habit.isNumerical) - setFrequency(habit.originalEntries.computeWeekdayFrequency(habit.isNumerical)) + setColor(WidgetTheme().color(color).toInt()) + setIsNumerical(isNumerical) + setFrequency(frequency) } } diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/widgets/FrequencyWidgetProvider.kt b/uhabits-android/src/main/java/org/isoron/uhabits/widgets/FrequencyWidgetProvider.kt index 698f002f4..dc2f18fd2 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/widgets/FrequencyWidgetProvider.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/widgets/FrequencyWidgetProvider.kt @@ -24,15 +24,29 @@ import android.content.Context class FrequencyWidgetProvider : BaseWidgetProvider() { override fun getWidgetFromId(context: Context, id: Int): BaseWidget { val habits = getHabitsFromWidgetId(id) - return if (habits.size == 1) { - FrequencyWidget( - context, - id, - habits[0], - preferences.firstWeekdayInt - ) + if (habits.isNotEmpty()) { + return if (habits.size == 1) { + FrequencyWidget( + context, + id, + habits[0], + preferences.firstWeekdayInt + ) + } else { + StackWidget(context, id, StackWidgetType.FREQUENCY, habits) + } } else { - StackWidget(context, id, StackWidgetType.FREQUENCY, habits) + val habitGroups = getHabitGroupsFromWidgetId(id) + return if (habitGroups.size == 1) { + FrequencyWidget( + context, + id, + habitGroups[0], + preferences.firstWeekdayInt + ) + } else { + StackWidget(context, id, StackWidgetType.FREQUENCY, habitGroups, true) + } } } } diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/widgets/ScoreWidget.kt b/uhabits-android/src/main/java/org/isoron/uhabits/widgets/ScoreWidget.kt index 8be9dfed5..b6786cb66 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/widgets/ScoreWidget.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/widgets/ScoreWidget.kt @@ -25,42 +25,62 @@ import android.view.View import org.isoron.platform.gui.toInt import org.isoron.uhabits.activities.common.views.ScoreChart import org.isoron.uhabits.core.models.Habit +import org.isoron.uhabits.core.models.HabitGroup import org.isoron.uhabits.core.ui.screens.habits.show.views.ScoreCardPresenter import org.isoron.uhabits.core.ui.views.WidgetTheme import org.isoron.uhabits.widgets.views.GraphWidgetView -class ScoreWidget( +class ScoreWidget private constructor( context: Context, id: Int, - private val habit: Habit, - stacked: Boolean = false + private val habit: Habit?, + private val habitGroup: HabitGroup?, + stacked: Boolean ) : BaseWidget(context, id, stacked) { + constructor(context: Context, id: Int, habit: Habit, stacked: Boolean = false) : this(context, id, habit, null, stacked) + constructor(context: Context, id: Int, habitGroup: HabitGroup, stacked: Boolean = false) : this(context, id, null, habitGroup, stacked) + override val defaultHeight: Int = 300 override val defaultWidth: Int = 300 - override fun getOnClickPendingIntent(context: Context): PendingIntent = - pendingIntentFactory.showHabit(habit) + override fun getOnClickPendingIntent(context: Context): PendingIntent { + return if (habit != null) { + pendingIntentFactory.showHabit(habit) + } else { + pendingIntentFactory.showHabitGroup(habitGroup!!) + } + } override fun refreshData(view: View) { - val viewModel = ScoreCardPresenter.buildState( - habit = habit, - firstWeekday = prefs.firstWeekdayInt, - spinnerPosition = prefs.scoreCardSpinnerPosition, - theme = WidgetTheme() - ) + val viewModel = if (habit != null) { + ScoreCardPresenter.buildState( + habit = habit, + firstWeekday = prefs.firstWeekdayInt, + spinnerPosition = prefs.scoreCardSpinnerPosition, + theme = WidgetTheme() + ) + } else { + ScoreCardPresenter.buildState( + habitGroup = habitGroup!!, + firstWeekday = prefs.firstWeekdayInt, + spinnerPosition = prefs.scoreCardSpinnerPosition, + theme = WidgetTheme() + ) + } val widgetView = view as GraphWidgetView widgetView.setBackgroundAlpha(preferedBackgroundAlpha) if (preferedBackgroundAlpha >= 255) widgetView.setShadowAlpha(0x4f) + val color = habit?.color ?: habitGroup!!.color (widgetView.dataView as ScoreChart).apply { setIsTransparencyEnabled(true) setBucketSize(viewModel.bucketSize) - setColor(WidgetTheme().color(habit.color).toInt()) + setColor(WidgetTheme().color(color).toInt()) setScores(viewModel.scores) } } override fun buildView() = GraphWidgetView(context, ScoreChart(context)).apply { - setTitle(habit.name) + setTitle(habit?.name ?: habitGroup!!.name) } } diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/widgets/ScoreWidgetProvider.kt b/uhabits-android/src/main/java/org/isoron/uhabits/widgets/ScoreWidgetProvider.kt index 160054da1..794413219 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/widgets/ScoreWidgetProvider.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/widgets/ScoreWidgetProvider.kt @@ -23,10 +23,19 @@ import android.content.Context class ScoreWidgetProvider : BaseWidgetProvider() { override fun getWidgetFromId(context: Context, id: Int): BaseWidget { val habits = getHabitsFromWidgetId(id) - return if (habits.size == 1) { - ScoreWidget(context, id, habits[0]) + if (habits.isNotEmpty()) { + return if (habits.size == 1) { + ScoreWidget(context, id, habits[0]) + } else { + StackWidget(context, id, StackWidgetType.SCORE, habits) + } } else { - StackWidget(context, id, StackWidgetType.SCORE, habits) + val habitGroups = getHabitGroupsFromWidgetId(id) + return if (habitGroups.size == 1) { + ScoreWidget(context, id, habitGroups[0]) + } else { + StackWidget(context, id, StackWidgetType.SCORE, habitGroups, true) + } } } } diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/widgets/StackWidget.kt b/uhabits-android/src/main/java/org/isoron/uhabits/widgets/StackWidget.kt index 6c59c1e1d..47e906538 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/widgets/StackWidget.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/widgets/StackWidget.kt @@ -28,14 +28,20 @@ import android.view.View import android.widget.RemoteViews import org.isoron.platform.utils.StringUtils import org.isoron.uhabits.core.models.Habit +import org.isoron.uhabits.core.models.HabitGroup -class StackWidget( +class StackWidget private constructor( context: Context, widgetId: Int, private val widgetType: StackWidgetType, - private val habits: List, - stacked: Boolean = true + private val habits: List?, + private val habitGroups: List?, + stacked: Boolean ) : BaseWidget(context, widgetId, stacked) { + + constructor(context: Context, widgetId: Int, widgetType: StackWidgetType, habits: List, stacked: Boolean = true) : this(context, widgetId, widgetType, habits, null, stacked) + constructor(context: Context, widgetId: Int, widgetType: StackWidgetType, habitGroups: List, stacked: Boolean = true, isHabitGroups: Boolean = false) : this(context, widgetId, widgetType, null, habitGroups, stacked) + override val defaultHeight: Int = 0 override val defaultWidth: Int = 0 @@ -55,7 +61,11 @@ class StackWidget( val remoteViews = RemoteViews(context.packageName, StackWidgetType.getStackWidgetLayoutId(widgetType)) val serviceIntent = Intent(context, StackWidgetService::class.java) - val habitIds = StringUtils.joinLongs(habits.map { it.id!! }.toLongArray()) + val habitIds = if (habits != null) { + StringUtils.joinLongs(habits.map { it.id!! }.toLongArray()) + } else { + StringUtils.joinLongs(habitGroups!!.map { it.id!! }.toLongArray()) + } serviceIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, id) serviceIntent.putExtra(StackWidgetService.WIDGET_TYPE, widgetType.value) @@ -73,9 +83,14 @@ class StackWidget( StackWidgetType.getStackWidgetAdapterViewId(widgetType), StackWidgetType.getStackWidgetEmptyViewId(widgetType) ) + val pendingIntentTemplate = if (habits != null) { + StackWidgetType.getPendingIntentTemplate(pendingIntentFactory, widgetType, habits) + } else { + StackWidgetType.getPendingIntentTemplate(pendingIntentFactory, widgetType, true) + } remoteViews.setPendingIntentTemplate( StackWidgetType.getStackWidgetAdapterViewId(widgetType), - StackWidgetType.getPendingIntentTemplate(pendingIntentFactory, widgetType, habits) + pendingIntentTemplate ) return remoteViews } diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/widgets/StackWidgetType.kt b/uhabits-android/src/main/java/org/isoron/uhabits/widgets/StackWidgetType.kt index 1e3a5d217..b8929f5f6 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/widgets/StackWidgetType.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/widgets/StackWidgetType.kt @@ -24,7 +24,6 @@ import org.isoron.uhabits.R import org.isoron.uhabits.core.models.Habit import org.isoron.uhabits.core.models.Timestamp import org.isoron.uhabits.intents.PendingIntentFactory -import java.lang.IllegalStateException enum class StackWidgetType(val value: Int) { CHECKMARK(0), FREQUENCY(1), SCORE(2), // habit strength widget @@ -95,6 +94,17 @@ enum class StackWidgetType(val value: Int) { } } + fun getPendingIntentTemplate( + factory: PendingIntentFactory, + widgetType: StackWidgetType, + isHabitGroups: Boolean + ): PendingIntent { + return when (widgetType) { + CHECKMARK, HISTORY, STREAKS, TARGET -> throw RuntimeException() + FREQUENCY, SCORE -> factory.showHabitGroupTemplate() + } + } + fun getIntentFillIn( factory: PendingIntentFactory, widgetType: StackWidgetType, diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/widgets/activities/HabitPickerDialog.kt b/uhabits-android/src/main/java/org/isoron/uhabits/widgets/activities/HabitPickerDialog.kt index b9d1bd26a..2f2dc9682 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/widgets/activities/HabitPickerDialog.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/widgets/activities/HabitPickerDialog.kt @@ -43,6 +43,10 @@ class NumericalHabitPickerDialog : HabitPickerDialog() { override fun getEmptyMessage() = R.string.no_numerical_habits } +class HabitAndGroupPickerDialog : HabitPickerDialog() { + override fun shouldShowGroups(): Boolean = true +} + open class HabitPickerDialog : Activity() { private var widgetId = 0 @@ -51,6 +55,8 @@ open class HabitPickerDialog : Activity() { protected open fun shouldHideNumerical() = false protected open fun shouldHideBoolean() = false + + protected open fun shouldShowGroups() = false protected open fun getEmptyMessage() = R.string.no_habits override fun onCreate(savedInstanceState: Bundle?) { @@ -75,6 +81,10 @@ open class HabitPickerDialog : Activity() { for (hgr in habitGroupList) { if (hgr.isArchived) continue + if (shouldShowGroups()) { + habitIds.add(hgr.id!!) + habitNames.add(hgr.name) + } for (h in hgr.habitList) { if (h.isArchived) continue diff --git a/uhabits-android/src/main/res/xml/widget_frequency_info.xml b/uhabits-android/src/main/res/xml/widget_frequency_info.xml index 65cdd1e92..6058570fe 100644 --- a/uhabits-android/src/main/res/xml/widget_frequency_info.xml +++ b/uhabits-android/src/main/res/xml/widget_frequency_info.xml @@ -27,7 +27,7 @@ android:previewImage="@drawable/widget_preview_frequency" android:resizeMode="vertical|horizontal" android:updatePeriodMillis="3600000" - android:configure="org.isoron.uhabits.widgets.activities.HabitPickerDialog" + android:configure="org.isoron.uhabits.widgets.activities.HabitAndGroupPickerDialog" android:widgetCategory="home_screen"> \ No newline at end of file diff --git a/uhabits-android/src/main/res/xml/widget_score_info.xml b/uhabits-android/src/main/res/xml/widget_score_info.xml index 60ba4d64d..9dd728edb 100644 --- a/uhabits-android/src/main/res/xml/widget_score_info.xml +++ b/uhabits-android/src/main/res/xml/widget_score_info.xml @@ -27,7 +27,7 @@ android:previewImage="@drawable/widget_preview_score" android:resizeMode="vertical|horizontal" android:updatePeriodMillis="3600000" - android:configure="org.isoron.uhabits.widgets.activities.HabitPickerDialog" + android:configure="org.isoron.uhabits.widgets.activities.HabitAndGroupPickerDialog" android:widgetCategory="home_screen"> \ No newline at end of file diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/views/FrequencyCard.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/views/FrequencyCard.kt index 66a71b1bd..37ee7788f 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/views/FrequencyCard.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/views/FrequencyCard.kt @@ -54,14 +54,7 @@ class FrequencyCardPresenter { firstWeekday: Int, theme: Theme ): FrequencyCardState { - val normalizedEntries = habitGroup.habitList.map { - it.computedEntries.normalizeEntries(it.isNumerical, it.frequency, it.targetValue) - } - val frequencies = normalizedEntries.map { - it.computeWeekdayFrequency(isNumerical = true) - }.reduce { acc, hashMap -> - mergeMaps(acc, hashMap) { value1, value2 -> addArray(value1, value2) } - } + val frequencies = getFrequenciesFromHabitGroup(habitGroup) return FrequencyCardState( color = habitGroup.color, @@ -72,6 +65,18 @@ class FrequencyCardPresenter { ) } + fun getFrequenciesFromHabitGroup(habitGroup: HabitGroup): HashMap> { + val normalizedEntries = habitGroup.habitList.map { + it.computedEntries.normalizeEntries(it.isNumerical, it.frequency, it.targetValue) + } + val frequencies = normalizedEntries.map { + it.computeWeekdayFrequency(isNumerical = true) + }.reduce { acc, hashMap -> + mergeMaps(acc, hashMap) { value1, value2 -> addArray(value1, value2) } + } + return frequencies + } + private fun mergeMaps(map1: HashMap, map2: HashMap, mergeFunction: (V, V) -> V): HashMap { val result = map1 // Step 1 for ((key, value) in map2) { // Step 2