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>
</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
android:name=".activities.about.AboutActivity"
android:label="@string/about">

@ -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)

@ -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<Habit>(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<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(
context: Context,
id: Int

@ -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)
}
}

@ -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)
}
}
}
}

@ -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)
}
}

@ -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)
}
}
}
}

@ -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<Habit>,
stacked: Boolean = true
private val habits: List<Habit>?,
private val habitGroups: List<HabitGroup>?,
stacked: Boolean
) : 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 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
}

@ -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,

@ -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

@ -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">
</appwidget-provider>

@ -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">
</appwidget-provider>

@ -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<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> {
val result = map1 // Step 1
for ((key, value) in map2) { // Step 2

Loading…
Cancel
Save