Implement score and frequency widgets for habit groups

pull/2020/head
Dharanish 1 year ago
parent 8fac8afadf
commit 6abea29736

@ -128,6 +128,15 @@
</intent-filter> </intent-filter>
</activity> </activity>
<activity
android:name=".widgets.activities.HabitAndGroupPickerDialog"
android:exported="true"
android:theme="@style/Theme.AppCompat.Light.Dialog">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
</intent-filter>
</activity>
<activity <activity
android:name=".activities.about.AboutActivity" android:name=".activities.about.AboutActivity"
android:label="@string/about"> android:label="@string/about">

@ -31,6 +31,7 @@ import android.net.Uri
import android.os.Build import android.os.Build
import org.isoron.uhabits.activities.habits.list.ListHabitsActivity import org.isoron.uhabits.activities.habits.list.ListHabitsActivity
import org.isoron.uhabits.activities.habits.show.ShowHabitActivity 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.AppScope
import org.isoron.uhabits.core.models.Habit import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.HabitGroup 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) = fun showHabitFillIn(habit: Habit) =
Intent().apply { Intent().apply {
data = Uri.parse(habit.uriString) data = Uri.parse(habit.uriString)

@ -27,6 +27,7 @@ import android.widget.RemoteViews
import org.isoron.uhabits.HabitsApplication import org.isoron.uhabits.HabitsApplication
import org.isoron.uhabits.R import org.isoron.uhabits.R
import org.isoron.uhabits.core.models.Habit 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.HabitGroupList
import org.isoron.uhabits.core.models.HabitList import org.isoron.uhabits.core.models.HabitList
import org.isoron.uhabits.core.models.HabitNotFoundException import org.isoron.uhabits.core.models.HabitNotFoundException
@ -114,12 +115,25 @@ abstract class BaseWidgetProvider : AppWidgetProvider() {
val selectedHabits = ArrayList<Habit>(selectedIds.size) val selectedHabits = ArrayList<Habit>(selectedIds.size)
for (id in selectedIds) { for (id in selectedIds) {
val h = habits.getById(id) ?: habitGroups.getHabitByID(id) val h = habits.getById(id) ?: habitGroups.getHabitByID(id)
?: throw HabitNotFoundException() if (h != null) {
selectedHabits.add(h) selectedHabits.add(h)
}
} }
return selectedHabits return selectedHabits
} }
protected fun getHabitGroupsFromWidgetId(widgetId: Int): List<HabitGroup> {
val selectedIds = widgetPrefs.getHabitIdsFromWidgetId(widgetId)
val selectedHabitGroups = ArrayList<HabitGroup>(selectedIds.size)
for (id in selectedIds) {
val hgr = habitGroups.getById(id)
if (hgr != null) {
selectedHabitGroups.add(hgr)
}
}
return selectedHabitGroups
}
protected abstract fun getWidgetFromId( protected abstract fun getWidgetFromId(
context: Context, context: Context,
id: Int id: Int

@ -25,32 +25,51 @@ import android.view.View
import org.isoron.platform.gui.toInt import org.isoron.platform.gui.toInt
import org.isoron.uhabits.activities.common.views.FrequencyChart import org.isoron.uhabits.activities.common.views.FrequencyChart
import org.isoron.uhabits.core.models.Habit 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.core.ui.views.WidgetTheme
import org.isoron.uhabits.widgets.views.GraphWidgetView import org.isoron.uhabits.widgets.views.GraphWidgetView
class FrequencyWidget( class FrequencyWidget private constructor(
context: Context, context: Context,
widgetId: Int, widgetId: Int,
private val habit: Habit, private val habit: Habit?,
private val habitGroup: HabitGroup?,
private val firstWeekday: Int, private val firstWeekday: Int,
stacked: Boolean = false stacked: Boolean
) : BaseWidget(context, widgetId, stacked) { ) : 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 defaultHeight: Int = 200
override val defaultWidth: Int = 200 override val defaultWidth: Int = 200
override fun getOnClickPendingIntent(context: Context): PendingIntent = override fun getOnClickPendingIntent(context: Context): PendingIntent {
pendingIntentFactory.showHabit(habit) return if (habit != null) {
pendingIntentFactory.showHabit(habit)
} else {
pendingIntentFactory.showHabitGroup(habitGroup!!)
}
}
override fun refreshData(v: View) { override fun refreshData(v: View) {
val widgetView = v as GraphWidgetView val widgetView = v as GraphWidgetView
widgetView.setTitle(habit.name) widgetView.setTitle(habit?.name ?: habitGroup!!.name)
widgetView.setBackgroundAlpha(preferedBackgroundAlpha) widgetView.setBackgroundAlpha(preferedBackgroundAlpha)
if (preferedBackgroundAlpha >= 255) widgetView.setShadowAlpha(0x4f) 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 { (widgetView.dataView as FrequencyChart).apply {
setFirstWeekday(firstWeekday) setFirstWeekday(firstWeekday)
setColor(WidgetTheme().color(habit.color).toInt()) setColor(WidgetTheme().color(color).toInt())
setIsNumerical(habit.isNumerical) setIsNumerical(isNumerical)
setFrequency(habit.originalEntries.computeWeekdayFrequency(habit.isNumerical)) setFrequency(frequency)
} }
} }

@ -24,15 +24,29 @@ import android.content.Context
class FrequencyWidgetProvider : BaseWidgetProvider() { class FrequencyWidgetProvider : BaseWidgetProvider() {
override fun getWidgetFromId(context: Context, id: Int): BaseWidget { override fun getWidgetFromId(context: Context, id: Int): BaseWidget {
val habits = getHabitsFromWidgetId(id) val habits = getHabitsFromWidgetId(id)
return if (habits.size == 1) { if (habits.isNotEmpty()) {
FrequencyWidget( return if (habits.size == 1) {
context, FrequencyWidget(
id, context,
habits[0], id,
preferences.firstWeekdayInt habits[0],
) preferences.firstWeekdayInt
)
} else {
StackWidget(context, id, StackWidgetType.FREQUENCY, habits)
}
} else { } 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)
}
} }
} }
} }

@ -25,42 +25,62 @@ import android.view.View
import org.isoron.platform.gui.toInt import org.isoron.platform.gui.toInt
import org.isoron.uhabits.activities.common.views.ScoreChart import org.isoron.uhabits.activities.common.views.ScoreChart
import org.isoron.uhabits.core.models.Habit 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.screens.habits.show.views.ScoreCardPresenter
import org.isoron.uhabits.core.ui.views.WidgetTheme import org.isoron.uhabits.core.ui.views.WidgetTheme
import org.isoron.uhabits.widgets.views.GraphWidgetView import org.isoron.uhabits.widgets.views.GraphWidgetView
class ScoreWidget( class ScoreWidget private constructor(
context: Context, context: Context,
id: Int, id: Int,
private val habit: Habit, private val habit: Habit?,
stacked: Boolean = false private val habitGroup: HabitGroup?,
stacked: Boolean
) : BaseWidget(context, id, stacked) { ) : 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 defaultHeight: Int = 300
override val defaultWidth: Int = 300 override val defaultWidth: Int = 300
override fun getOnClickPendingIntent(context: Context): PendingIntent = override fun getOnClickPendingIntent(context: Context): PendingIntent {
pendingIntentFactory.showHabit(habit) return if (habit != null) {
pendingIntentFactory.showHabit(habit)
} else {
pendingIntentFactory.showHabitGroup(habitGroup!!)
}
}
override fun refreshData(view: View) { override fun refreshData(view: View) {
val viewModel = ScoreCardPresenter.buildState( val viewModel = if (habit != null) {
habit = habit, ScoreCardPresenter.buildState(
firstWeekday = prefs.firstWeekdayInt, habit = habit,
spinnerPosition = prefs.scoreCardSpinnerPosition, firstWeekday = prefs.firstWeekdayInt,
theme = WidgetTheme() spinnerPosition = prefs.scoreCardSpinnerPosition,
) theme = WidgetTheme()
)
} else {
ScoreCardPresenter.buildState(
habitGroup = habitGroup!!,
firstWeekday = prefs.firstWeekdayInt,
spinnerPosition = prefs.scoreCardSpinnerPosition,
theme = WidgetTheme()
)
}
val widgetView = view as GraphWidgetView val widgetView = view as GraphWidgetView
widgetView.setBackgroundAlpha(preferedBackgroundAlpha) widgetView.setBackgroundAlpha(preferedBackgroundAlpha)
if (preferedBackgroundAlpha >= 255) widgetView.setShadowAlpha(0x4f) if (preferedBackgroundAlpha >= 255) widgetView.setShadowAlpha(0x4f)
val color = habit?.color ?: habitGroup!!.color
(widgetView.dataView as ScoreChart).apply { (widgetView.dataView as ScoreChart).apply {
setIsTransparencyEnabled(true) setIsTransparencyEnabled(true)
setBucketSize(viewModel.bucketSize) setBucketSize(viewModel.bucketSize)
setColor(WidgetTheme().color(habit.color).toInt()) setColor(WidgetTheme().color(color).toInt())
setScores(viewModel.scores) setScores(viewModel.scores)
} }
} }
override fun buildView() = override fun buildView() =
GraphWidgetView(context, ScoreChart(context)).apply { GraphWidgetView(context, ScoreChart(context)).apply {
setTitle(habit.name) setTitle(habit?.name ?: habitGroup!!.name)
} }
} }

@ -23,10 +23,19 @@ import android.content.Context
class ScoreWidgetProvider : BaseWidgetProvider() { class ScoreWidgetProvider : BaseWidgetProvider() {
override fun getWidgetFromId(context: Context, id: Int): BaseWidget { override fun getWidgetFromId(context: Context, id: Int): BaseWidget {
val habits = getHabitsFromWidgetId(id) val habits = getHabitsFromWidgetId(id)
return if (habits.size == 1) { if (habits.isNotEmpty()) {
ScoreWidget(context, id, habits[0]) return if (habits.size == 1) {
ScoreWidget(context, id, habits[0])
} else {
StackWidget(context, id, StackWidgetType.SCORE, habits)
}
} else { } 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)
}
} }
} }
} }

@ -28,14 +28,20 @@ import android.view.View
import android.widget.RemoteViews import android.widget.RemoteViews
import org.isoron.platform.utils.StringUtils import org.isoron.platform.utils.StringUtils
import org.isoron.uhabits.core.models.Habit import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.HabitGroup
class StackWidget( class StackWidget private constructor(
context: Context, context: Context,
widgetId: Int, widgetId: Int,
private val widgetType: StackWidgetType, private val widgetType: StackWidgetType,
private val habits: List<Habit>, private val habits: List<Habit>?,
stacked: Boolean = true private val habitGroups: List<HabitGroup>?,
stacked: Boolean
) : BaseWidget(context, widgetId, stacked) { ) : BaseWidget(context, widgetId, stacked) {
constructor(context: Context, widgetId: Int, widgetType: StackWidgetType, habits: List<Habit>, stacked: Boolean = true) : this(context, widgetId, widgetType, habits, null, stacked)
constructor(context: Context, widgetId: Int, widgetType: StackWidgetType, habitGroups: List<HabitGroup>, stacked: Boolean = true, isHabitGroups: Boolean = false) : this(context, widgetId, widgetType, null, habitGroups, stacked)
override val defaultHeight: Int = 0 override val defaultHeight: Int = 0
override val defaultWidth: Int = 0 override val defaultWidth: Int = 0
@ -55,7 +61,11 @@ class StackWidget(
val remoteViews = val remoteViews =
RemoteViews(context.packageName, StackWidgetType.getStackWidgetLayoutId(widgetType)) RemoteViews(context.packageName, StackWidgetType.getStackWidgetLayoutId(widgetType))
val serviceIntent = Intent(context, StackWidgetService::class.java) 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(AppWidgetManager.EXTRA_APPWIDGET_ID, id)
serviceIntent.putExtra(StackWidgetService.WIDGET_TYPE, widgetType.value) serviceIntent.putExtra(StackWidgetService.WIDGET_TYPE, widgetType.value)
@ -73,9 +83,14 @@ class StackWidget(
StackWidgetType.getStackWidgetAdapterViewId(widgetType), StackWidgetType.getStackWidgetAdapterViewId(widgetType),
StackWidgetType.getStackWidgetEmptyViewId(widgetType) StackWidgetType.getStackWidgetEmptyViewId(widgetType)
) )
val pendingIntentTemplate = if (habits != null) {
StackWidgetType.getPendingIntentTemplate(pendingIntentFactory, widgetType, habits)
} else {
StackWidgetType.getPendingIntentTemplate(pendingIntentFactory, widgetType, true)
}
remoteViews.setPendingIntentTemplate( remoteViews.setPendingIntentTemplate(
StackWidgetType.getStackWidgetAdapterViewId(widgetType), StackWidgetType.getStackWidgetAdapterViewId(widgetType),
StackWidgetType.getPendingIntentTemplate(pendingIntentFactory, widgetType, habits) pendingIntentTemplate
) )
return remoteViews return remoteViews
} }

@ -24,7 +24,6 @@ import org.isoron.uhabits.R
import org.isoron.uhabits.core.models.Habit import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.Timestamp import org.isoron.uhabits.core.models.Timestamp
import org.isoron.uhabits.intents.PendingIntentFactory import org.isoron.uhabits.intents.PendingIntentFactory
import java.lang.IllegalStateException
enum class StackWidgetType(val value: Int) { enum class StackWidgetType(val value: Int) {
CHECKMARK(0), FREQUENCY(1), SCORE(2), // habit strength widget 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( fun getIntentFillIn(
factory: PendingIntentFactory, factory: PendingIntentFactory,
widgetType: StackWidgetType, widgetType: StackWidgetType,

@ -43,6 +43,10 @@ class NumericalHabitPickerDialog : HabitPickerDialog() {
override fun getEmptyMessage() = R.string.no_numerical_habits override fun getEmptyMessage() = R.string.no_numerical_habits
} }
class HabitAndGroupPickerDialog : HabitPickerDialog() {
override fun shouldShowGroups(): Boolean = true
}
open class HabitPickerDialog : Activity() { open class HabitPickerDialog : Activity() {
private var widgetId = 0 private var widgetId = 0
@ -51,6 +55,8 @@ open class HabitPickerDialog : Activity() {
protected open fun shouldHideNumerical() = false protected open fun shouldHideNumerical() = false
protected open fun shouldHideBoolean() = false protected open fun shouldHideBoolean() = false
protected open fun shouldShowGroups() = false
protected open fun getEmptyMessage() = R.string.no_habits protected open fun getEmptyMessage() = R.string.no_habits
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@ -75,6 +81,10 @@ open class HabitPickerDialog : Activity() {
for (hgr in habitGroupList) { for (hgr in habitGroupList) {
if (hgr.isArchived) continue if (hgr.isArchived) continue
if (shouldShowGroups()) {
habitIds.add(hgr.id!!)
habitNames.add(hgr.name)
}
for (h in hgr.habitList) { for (h in hgr.habitList) {
if (h.isArchived) continue if (h.isArchived) continue

@ -27,7 +27,7 @@
android:previewImage="@drawable/widget_preview_frequency" android:previewImage="@drawable/widget_preview_frequency"
android:resizeMode="vertical|horizontal" android:resizeMode="vertical|horizontal"
android:updatePeriodMillis="3600000" android:updatePeriodMillis="3600000"
android:configure="org.isoron.uhabits.widgets.activities.HabitPickerDialog" android:configure="org.isoron.uhabits.widgets.activities.HabitAndGroupPickerDialog"
android:widgetCategory="home_screen"> android:widgetCategory="home_screen">
</appwidget-provider> </appwidget-provider>

@ -27,7 +27,7 @@
android:previewImage="@drawable/widget_preview_score" android:previewImage="@drawable/widget_preview_score"
android:resizeMode="vertical|horizontal" android:resizeMode="vertical|horizontal"
android:updatePeriodMillis="3600000" android:updatePeriodMillis="3600000"
android:configure="org.isoron.uhabits.widgets.activities.HabitPickerDialog" android:configure="org.isoron.uhabits.widgets.activities.HabitAndGroupPickerDialog"
android:widgetCategory="home_screen"> android:widgetCategory="home_screen">
</appwidget-provider> </appwidget-provider>

@ -54,14 +54,7 @@ class FrequencyCardPresenter {
firstWeekday: Int, firstWeekday: Int,
theme: Theme theme: Theme
): FrequencyCardState { ): FrequencyCardState {
val normalizedEntries = habitGroup.habitList.map { val frequencies = getFrequenciesFromHabitGroup(habitGroup)
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 FrequencyCardState( return FrequencyCardState(
color = habitGroup.color, color = habitGroup.color,
@ -72,6 +65,18 @@ class FrequencyCardPresenter {
) )
} }
fun getFrequenciesFromHabitGroup(habitGroup: HabitGroup): HashMap<Timestamp, Array<Int>> {
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 <K, V> mergeMaps(map1: HashMap<K, V>, map2: HashMap<K, V>, mergeFunction: (V, V) -> V): HashMap<K, V> { private fun <K, V> mergeMaps(map1: HashMap<K, V>, map2: HashMap<K, V>, mergeFunction: (V, V) -> V): HashMap<K, V> {
val result = map1 // Step 1 val result = map1 // Step 1
for ((key, value) in map2) { // Step 2 for ((key, value) in map2) { // Step 2

Loading…
Cancel
Save