Merge remote-tracking branch 'upstream/dev' into dev

pull/699/head
MarKco 5 years ago
commit 1db37a64c2

@ -78,8 +78,6 @@ android {
}
dependencies {
androidTestAnnotationProcessor "com.google.auto.factory:auto-factory:$AUTO_FACTORY_VERSION"
androidTestCompileOnly "com.google.auto.factory:auto-factory:$AUTO_FACTORY_VERSION"
androidTestImplementation "androidx.test.espresso:espresso-contrib:$ESPRESSO_VERSION"
androidTestImplementation "androidx.test.espresso:espresso-core:$ESPRESSO_VERSION"
androidTestImplementation "com.google.dagger:dagger:$DAGGER_VERSION"
@ -93,8 +91,6 @@ dependencies {
androidTestImplementation 'androidx.test.uiautomator:uiautomator:2.2.0'
androidTestImplementation 'androidx.test:rules:1.3.0'
androidTestImplementation project(":uhabits-core")
annotationProcessor "com.google.auto.factory:auto-factory:$AUTO_FACTORY_VERSION"
compileOnly "com.google.auto.factory:auto-factory:$AUTO_FACTORY_VERSION"
compileOnly "javax.annotation:jsr250-api:1.0"
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.1'
implementation "com.github.paolorotolo:appintro:3.4.0"

@ -22,6 +22,7 @@ package org.isoron.uhabits.activities.common.views;
import androidx.test.ext.junit.runners.*;
import androidx.test.filters.*;
import org.apache.commons.lang3.*;
import org.isoron.uhabits.*;
import org.isoron.uhabits.core.models.*;
import org.isoron.uhabits.core.ui.callbacks.*;
@ -56,9 +57,16 @@ public class HistoryChartTest extends BaseViewTest
habit = fixtures.createLongHabit();
today = new Timestamp(DateUtils.getStartOfToday());
Integer[] entries = habit
.getComputedEntries()
.getByInterval(today.minus(300), today)
.stream()
.map(Entry::getValue)
.toArray(Integer[]::new);
chart = new HistoryChart(targetContext);
chart.setSkipEnabled(true);
chart.setEntries(habit.getComputedEntries().getAllValues());
chart.setEntries(ArrayUtils.toPrimitive(entries));
chart.setColor(PaletteUtilsKt.toFixedAndroidColor(habit.getColor()));
measureView(chart, dpToPixels(400), dpToPixels(200));

@ -23,8 +23,8 @@ import androidx.test.ext.junit.runners.*;
import androidx.test.filters.*;
import org.isoron.uhabits.*;
import org.isoron.uhabits.activities.habits.show.views.*;
import org.isoron.uhabits.core.models.*;
import org.isoron.uhabits.core.ui.screens.habits.show.views.*;
import org.isoron.uhabits.utils.*;
import org.junit.*;
import org.junit.runner.*;
@ -49,8 +49,8 @@ public class ScoreChartTest extends BaseViewTest
fixtures.purgeHabits(habitList);
habit = fixtures.createLongHabit();
presenter = new ScoreCardPresenter(habit, prefs.getFirstWeekday());
ScoreCardViewModel model = presenter.present(0);
presenter = new ScoreCardPresenter();
ScoreCardViewModel model = presenter.present(habit, prefs.getFirstWeekday(), 0);
view = new ScoreChart(targetContext);
view.setScores(model.getScores());
@ -84,7 +84,7 @@ public class ScoreChartTest extends BaseViewTest
@Test
public void testRender_withMonthlyBucket() throws Throwable
{
ScoreCardViewModel model = presenter.present(2);
ScoreCardViewModel model = presenter.present(habit, prefs.getFirstWeekday(), 2);
view.setScores(model.getScores());
view.setBucketSize(model.getBucketSize());
view.invalidate();
@ -102,7 +102,7 @@ public class ScoreChartTest extends BaseViewTest
@Test
public void testRender_withYearlyBucket() throws Throwable
{
ScoreCardViewModel model = presenter.present(4);
ScoreCardViewModel model = presenter.present(habit, prefs.getFirstWeekday(), 4);
view.setScores(model.getScores());
view.setBucketSize(model.getBucketSize());
view.invalidate();

@ -25,6 +25,7 @@ import org.isoron.uhabits.BaseViewTest
import org.isoron.uhabits.R
import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.PaletteColor
import org.isoron.uhabits.core.models.Timestamp
import org.isoron.uhabits.core.utils.DateUtils
import org.junit.Test
import org.junit.runner.RunWith
@ -37,6 +38,7 @@ class HabitCardViewTest : BaseViewTest() {
private lateinit var view: HabitCardView
private lateinit var habit1: Habit
private lateinit var habit2: Habit
private lateinit var today: Timestamp
override fun setUp() {
super.setUp()
@ -44,11 +46,16 @@ class HabitCardViewTest : BaseViewTest() {
habit1 = fixtures.createLongHabit()
habit2 = fixtures.createLongNumericalHabit()
val today = DateUtils.getTodayWithOffset()
today = DateUtils.getTodayWithOffset()
val entries = habit1
.computedEntries
.getByInterval(today.minus(300), today)
.map { it.value }.toIntArray()
view = component.getHabitCardViewFactory().create().apply {
habit = habit1
values = habit1.computedEntries.getAllValues()
values = entries
score = habit1.scores.get(today).value
isSelected = false
buttonCount = 5
@ -73,9 +80,14 @@ class HabitCardViewTest : BaseViewTest() {
@Test
fun testRender_numerical() {
val entries = habit2
.computedEntries
.getByInterval(today.minus(300), today)
.map { it.value }.toIntArray()
view.apply {
habit = habit2
values = habit2.computedEntries.getAllValues()
values = entries
}
assertRenders(view, "$PATH/render_numerical.png")
}

@ -24,15 +24,16 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import org.isoron.uhabits.BaseViewTest
import org.isoron.uhabits.R
import org.isoron.uhabits.core.ui.screens.habits.show.views.FrequencyCardPresenter
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
@MediumTest
class FrequencyCardTest : BaseViewTest() {
class FrequencyCardViewTest : BaseViewTest() {
val PATH = "habits/show/FrequencyCard/"
private lateinit var view: FrequencyCard
private lateinit var view: FrequencyCardView
@Before
override fun setUp() {
@ -41,8 +42,8 @@ class FrequencyCardTest : BaseViewTest() {
view = LayoutInflater
.from(targetContext)
.inflate(R.layout.show_habit, null)
.findViewById<View>(R.id.frequencyCard) as FrequencyCard
view.update(FrequencyCardPresenter(habit, 0).present())
.findViewById<View>(R.id.frequencyCard) as FrequencyCardView
view.update(FrequencyCardPresenter().present(habit = habit, firstWeekday = 0))
measureView(view, 800f, 600f)
}

@ -24,14 +24,15 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import org.isoron.uhabits.BaseViewTest
import org.isoron.uhabits.R
import org.isoron.uhabits.core.ui.screens.habits.show.views.HistoryCardPresenter
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
@MediumTest
class HistoryCardTest : BaseViewTest() {
private lateinit var view: HistoryCard
class HistoryCardViewTest : BaseViewTest() {
private lateinit var view: HistoryCardView
val PATH = "habits/show/HistoryCard/"
@Before
@ -41,13 +42,13 @@ class HistoryCardTest : BaseViewTest() {
view = LayoutInflater
.from(targetContext)
.inflate(R.layout.show_habit, null)
.findViewById<View>(R.id.historyCard) as HistoryCard
.findViewById<View>(R.id.historyCard) as HistoryCardView
view.update(
HistoryCardPresenter(
HistoryCardPresenter().present(
habit = habit,
firstWeekday = 1,
isSkipEnabled = false
).present()
)
)
measureView(view, 800f, 600f)
}

@ -25,6 +25,7 @@ import androidx.test.filters.MediumTest
import org.hamcrest.Matchers.equalTo
import org.isoron.uhabits.BaseViewTest
import org.isoron.uhabits.R
import org.isoron.uhabits.core.ui.screens.habits.show.views.NotesCardViewModel
import org.junit.Assert.assertThat
import org.junit.Before
import org.junit.Test

@ -25,6 +25,7 @@ import androidx.test.filters.MediumTest
import org.isoron.uhabits.BaseViewTest
import org.isoron.uhabits.R
import org.isoron.uhabits.core.models.PaletteColor
import org.isoron.uhabits.core.ui.screens.habits.show.views.OverviewCardViewModel
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith

@ -24,15 +24,16 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import org.isoron.uhabits.BaseViewTest
import org.isoron.uhabits.R
import org.isoron.uhabits.core.ui.screens.habits.show.views.ScoreCardPresenter
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
@MediumTest
class ScoreCardTest : BaseViewTest() {
class ScoreCardViewTest : BaseViewTest() {
val PATH = "habits/show/ScoreCard/"
private lateinit var view: ScoreCard
private lateinit var view: ScoreCardView
@Before
override fun setUp() {
@ -41,8 +42,14 @@ class ScoreCardTest : BaseViewTest() {
view = LayoutInflater
.from(targetContext)
.inflate(R.layout.show_habit, null)
.findViewById<View>(R.id.scoreCard) as ScoreCard
view.update(ScoreCardPresenter(habit, 0).present(0))
.findViewById<View>(R.id.scoreCard) as ScoreCardView
view.update(
ScoreCardPresenter().present(
habit = habit,
firstWeekday = 0,
spinnerPosition = 0,
)
)
measureView(view, 800f, 600f)
}

@ -24,6 +24,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import org.isoron.uhabits.BaseViewTest
import org.isoron.uhabits.R
import org.isoron.uhabits.core.ui.screens.habits.show.views.StreakCardViewModel
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith

@ -23,7 +23,11 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import org.isoron.uhabits.BaseViewTest
import org.isoron.uhabits.R
import org.isoron.uhabits.core.models.Frequency
import org.isoron.uhabits.core.models.PaletteColor
import org.isoron.uhabits.core.models.Reminder
import org.isoron.uhabits.core.models.WeekdayList.EVERY_DAY
import org.isoron.uhabits.core.ui.screens.habits.show.views.SubtitleCardViewModel
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@ -44,11 +48,12 @@ class SubtitleCardViewTest : BaseViewTest() {
view.update(
SubtitleCardViewModel(
color = PaletteColor(7),
frequencyText = "3 times in 7 days",
frequency = Frequency(3, 7),
isNumerical = false,
question = "Did you meditate this morning?",
reminderText = "8:30 AM",
targetText = "",
reminder = Reminder(8, 30, EVERY_DAY),
unit = "",
targetValue = 0.0,
)
)
measureView(view, 800f, 200f)

@ -25,8 +25,6 @@ import android.content.res.*;
import androidx.annotation.*;
import androidx.appcompat.app.*;
import com.google.auto.factory.*;
import org.isoron.uhabits.R;
import org.isoron.uhabits.core.ui.callbacks.*;
import org.isoron.uhabits.inject.*;
@ -34,10 +32,9 @@ import org.isoron.uhabits.inject.*;
/**
* Dialog that asks the user confirmation before executing a delete operation.
*/
@AutoFactory(allowSubclasses = true)
public class ConfirmDeleteDialog extends AlertDialog
{
protected ConfirmDeleteDialog(@Provided @ActivityContext Context context,
public ConfirmDeleteDialog(@ActivityContext Context context,
@NonNull OnConfirmedCallback callback,
int quantity)
{

@ -25,16 +25,13 @@ import android.content.res.*;
import androidx.annotation.*;
import androidx.appcompat.app.*;
import com.google.auto.factory.*;
import org.isoron.uhabits.R;
import org.isoron.uhabits.*;
import org.isoron.uhabits.core.ui.callbacks.*;
import org.isoron.uhabits.inject.*;
@AutoFactory(allowSubclasses = true)
public class ConfirmSyncKeyDialog extends AlertDialog
{
protected ConfirmSyncKeyDialog(@Provided @ActivityContext Context context,
public ConfirmSyncKeyDialog(@ActivityContext Context context,
@NonNull OnConfirmedCallback callback)
{
super(context);

@ -36,6 +36,7 @@ import org.isoron.uhabits.core.models.*;
import org.isoron.uhabits.core.preferences.*;
import org.isoron.uhabits.core.tasks.*;
import org.isoron.uhabits.core.ui.callbacks.*;
import org.isoron.uhabits.core.ui.screens.habits.show.views.*;
import org.isoron.uhabits.utils.*;
import org.jetbrains.annotations.*;
@ -178,7 +179,12 @@ public class HistoryEditorDialog extends AppCompatDialogFragment
@Override
public void doInBackground()
{
checkmarks = habit.getComputedEntries().getAllValues();
HistoryCardViewModel model = new HistoryCardPresenter().present(
habit,
prefs.getFirstWeekday(),
prefs.isSkipEnabled()
);
checkmarks = model.getEntries();
}
@Override

@ -27,8 +27,8 @@ import androidx.appcompat.app.AppCompatActivity
import dagger.Lazy
import org.isoron.uhabits.R
import org.isoron.uhabits.activities.common.dialogs.ColorPickerDialogFactory
import org.isoron.uhabits.activities.common.dialogs.ConfirmDeleteDialogFactory
import org.isoron.uhabits.activities.common.dialogs.ConfirmSyncKeyDialogFactory
import org.isoron.uhabits.activities.common.dialogs.ConfirmDeleteDialog
import org.isoron.uhabits.activities.common.dialogs.ConfirmSyncKeyDialog
import org.isoron.uhabits.activities.common.dialogs.NumberPickerFactory
import org.isoron.uhabits.activities.habits.edit.HabitTypeDialog
import org.isoron.uhabits.activities.habits.list.views.HabitCardListAdapter
@ -91,8 +91,6 @@ class ListHabitsScreen
private val taskRunner: TaskRunner,
private val exportDBFactory: ExportDBTaskFactory,
private val importTaskFactory: ImportDataTaskFactory,
private val confirmDeleteDialogFactory: ConfirmDeleteDialogFactory,
private val confirmSyncKeyDialogFactory: ConfirmSyncKeyDialogFactory,
private val colorPickerFactory: ColorPickerDialogFactory,
private val numberPickerFactory: NumberPickerFactory,
private val behavior: Lazy<ListHabitsBehavior>
@ -172,7 +170,7 @@ class ListHabitsScreen
}
override fun showDeleteConfirmationScreen(callback: OnConfirmedCallback, quantity: Int) {
confirmDeleteDialogFactory.create(callback, quantity).show()
ConfirmDeleteDialog(activity, callback, quantity).show()
}
override fun showEditHabitsScreen(habits: List<Habit>) {
@ -250,7 +248,7 @@ class ListHabitsScreen
}
override fun showConfirmInstallSyncKey(callback: OnConfirmedCallback) {
confirmSyncKeyDialogFactory.create(callback).show()
ConfirmSyncKeyDialog(activity, callback).show()
}
private fun getExecuteString(command: Command): String? {

@ -28,8 +28,6 @@ import android.text.TextPaint
import android.view.HapticFeedbackConstants
import android.view.View
import android.view.View.MeasureSpec.EXACTLY
import com.google.auto.factory.AutoFactory
import com.google.auto.factory.Provided
import org.isoron.uhabits.R
import org.isoron.uhabits.core.models.Entry
import org.isoron.uhabits.core.models.Entry.Companion.NO
@ -43,11 +41,19 @@ import org.isoron.uhabits.utils.getFontAwesome
import org.isoron.uhabits.utils.showMessage
import org.isoron.uhabits.utils.sres
import org.isoron.uhabits.utils.toMeasureSpec
import javax.inject.Inject
class CheckmarkButtonViewFactory
@Inject constructor(
@ActivityContext val context: Context,
val preferences: Preferences
) {
fun create() = CheckmarkButtonView(context, preferences)
}
@AutoFactory
class CheckmarkButtonView(
@Provided @ActivityContext context: Context,
@Provided val preferences: Preferences
context: Context,
val preferences: Preferences
) : View(context),
View.OnClickListener,
View.OnLongClickListener {

@ -20,19 +20,26 @@
package org.isoron.uhabits.activities.habits.list.views
import android.content.Context
import com.google.auto.factory.AutoFactory
import com.google.auto.factory.Provided
import org.isoron.uhabits.core.models.Entry.Companion.UNKNOWN
import org.isoron.uhabits.core.models.Timestamp
import org.isoron.uhabits.core.preferences.Preferences
import org.isoron.uhabits.core.utils.DateUtils
import org.isoron.uhabits.inject.ActivityContext
import javax.inject.Inject
class CheckmarkPanelViewFactory
@Inject constructor(
@ActivityContext val context: Context,
val preferences: Preferences,
private val buttonFactory: CheckmarkButtonViewFactory
) {
fun create() = CheckmarkPanelView(context, preferences, buttonFactory)
}
@AutoFactory
class CheckmarkPanelView(
@Provided @ActivityContext context: Context,
@Provided preferences: Preferences,
@Provided private val buttonFactory: CheckmarkButtonViewFactory
context: Context,
preferences: Preferences,
private val buttonFactory: CheckmarkButtonViewFactory
) : ButtonPanelView<CheckmarkButtonView>(context, preferences) {
var values = IntArray(0)

@ -32,20 +32,28 @@ import androidx.recyclerview.widget.ItemTouchHelper.START
import androidx.recyclerview.widget.ItemTouchHelper.UP
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.auto.factory.AutoFactory
import com.google.auto.factory.Provided
import dagger.Lazy
import org.isoron.uhabits.R
import org.isoron.uhabits.activities.common.views.BundleSavedState
import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.inject.ActivityContext
import javax.inject.Inject
class HabitCardListViewFactory
@Inject constructor(
@ActivityContext val context: Context,
val adapter: HabitCardListAdapter,
val cardViewFactory: HabitCardViewFactory,
val controller: Lazy<HabitCardListController>
) {
fun create() = HabitCardListView(context, adapter, cardViewFactory, controller)
}
@AutoFactory
class HabitCardListView(
@Provided @ActivityContext context: Context,
@Provided private val adapter: HabitCardListAdapter,
@Provided private val cardViewFactory: HabitCardViewFactory,
@Provided private val controller: Lazy<HabitCardListController>
@ActivityContext context: Context,
private val adapter: HabitCardListAdapter,
private val cardViewFactory: HabitCardViewFactory,
private val controller: Lazy<HabitCardListController>
) : RecyclerView(context, null, R.attr.scrollableRecyclerViewStyle) {
var checkmarkCount: Int = 0

@ -34,8 +34,6 @@ import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
import android.widget.FrameLayout
import android.widget.LinearLayout
import android.widget.TextView
import com.google.auto.factory.AutoFactory
import com.google.auto.factory.Provided
import org.isoron.uhabits.R
import org.isoron.uhabits.activities.common.views.RingView
import org.isoron.uhabits.core.models.Habit
@ -47,13 +45,23 @@ import org.isoron.uhabits.inject.ActivityContext
import org.isoron.uhabits.utils.dp
import org.isoron.uhabits.utils.sres
import org.isoron.uhabits.utils.toThemedAndroidColor
import javax.inject.Inject
class HabitCardViewFactory
@Inject constructor(
@ActivityContext val context: Context,
private val checkmarkPanelFactory: CheckmarkPanelViewFactory,
private val numberPanelFactory: NumberPanelViewFactory,
private val behavior: ListHabitsBehavior
) {
fun create() = HabitCardView(context, checkmarkPanelFactory, numberPanelFactory, behavior)
}
@AutoFactory
class HabitCardView(
@Provided @ActivityContext context: Context,
@Provided private val checkmarkPanelFactory: CheckmarkPanelViewFactory,
@Provided private val numberPanelFactory: NumberPanelViewFactory,
@Provided private val behavior: ListHabitsBehavior
@ActivityContext context: Context,
private val checkmarkPanelFactory: CheckmarkPanelViewFactory,
private val numberPanelFactory: NumberPanelViewFactory,
private val behavior: ListHabitsBehavior
) : FrameLayout(context),
ModelObservable.Listener {

@ -28,8 +28,6 @@ import android.text.TextPaint
import android.view.View
import android.view.View.OnClickListener
import android.view.View.OnLongClickListener
import com.google.auto.factory.AutoFactory
import com.google.auto.factory.Provided
import org.isoron.uhabits.R
import org.isoron.uhabits.core.preferences.Preferences
import org.isoron.uhabits.inject.ActivityContext
@ -38,6 +36,7 @@ import org.isoron.uhabits.utils.StyledResources
import org.isoron.uhabits.utils.getFontAwesome
import org.isoron.uhabits.utils.showMessage
import java.text.DecimalFormat
import javax.inject.Inject
private val BOLD_TYPEFACE = Typeface.create("sans-serif-condensed", Typeface.BOLD)
private val NORMAL_TYPEFACE = Typeface.create("sans-serif-condensed", Typeface.NORMAL)
@ -55,10 +54,17 @@ fun Double.toShortString(): String = when {
else -> DecimalFormat("#.##").format(this)
}
@AutoFactory
class NumberButtonViewFactory
@Inject constructor(
@ActivityContext val context: Context,
val preferences: Preferences
) {
fun create() = NumberButtonView(context, preferences)
}
class NumberButtonView(
@Provided @ActivityContext context: Context,
@Provided val preferences: Preferences
@ActivityContext context: Context,
val preferences: Preferences
) : View(context),
OnClickListener,
OnLongClickListener {

@ -20,18 +20,25 @@
package org.isoron.uhabits.activities.habits.list.views
import android.content.Context
import com.google.auto.factory.AutoFactory
import com.google.auto.factory.Provided
import org.isoron.uhabits.core.models.Timestamp
import org.isoron.uhabits.core.preferences.Preferences
import org.isoron.uhabits.core.utils.DateUtils
import org.isoron.uhabits.inject.ActivityContext
import javax.inject.Inject
class NumberPanelViewFactory
@Inject constructor(
@ActivityContext val context: Context,
val preferences: Preferences,
val buttonFactory: NumberButtonViewFactory
) {
fun create() = NumberPanelView(context, preferences, buttonFactory)
}
@AutoFactory
class NumberPanelView(
@Provided @ActivityContext context: Context,
@Provided preferences: Preferences,
@Provided private val buttonFactory: NumberButtonViewFactory
@ActivityContext context: Context,
preferences: Preferences,
private val buttonFactory: NumberButtonViewFactory
) : ButtonPanelView<NumberButtonView>(context, preferences) {
var values = DoubleArray(0)

@ -30,20 +30,25 @@ import org.isoron.uhabits.AndroidDirFinder
import org.isoron.uhabits.HabitsApplication
import org.isoron.uhabits.activities.AndroidThemeSwitcher
import org.isoron.uhabits.activities.HabitsDirFinder
import org.isoron.uhabits.activities.common.dialogs.ConfirmDeleteDialogFactory
import org.isoron.uhabits.activities.common.dialogs.NumberPickerFactory
import org.isoron.uhabits.core.commands.Command
import org.isoron.uhabits.core.commands.CommandRunner
import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.preferences.Preferences
import org.isoron.uhabits.core.ui.screens.habits.show.ShowHabitBehavior
import org.isoron.uhabits.core.ui.screens.habits.show.ShowHabitMenuBehavior
import org.isoron.uhabits.core.ui.screens.habits.show.ShowHabitPresenter
import org.isoron.uhabits.intents.IntentFactory
class ShowHabitActivity : AppCompatActivity(), CommandRunner.Listener {
private val presenter = ShowHabitPresenter()
private lateinit var commandRunner: CommandRunner
private lateinit var menu: ShowHabitMenu
private lateinit var presenter: ShowHabitPresenter
private lateinit var view: ShowHabitView
private lateinit var habit: Habit
private lateinit var preferences: Preferences
private val scope = CoroutineScope(Dispatchers.Main)
@ -52,21 +57,15 @@ class ShowHabitActivity : AppCompatActivity(), CommandRunner.Listener {
val appComponent = (applicationContext as HabitsApplication).component
val habitList = appComponent.habitList
val habit = habitList.getById(ContentUris.parseId(intent.data!!))!!
val preferences = appComponent.preferences
habit = habitList.getById(ContentUris.parseId(intent.data!!))!!
preferences = appComponent.preferences
commandRunner = appComponent.commandRunner
AndroidThemeSwitcher(this, preferences).apply()
view = ShowHabitView(this)
presenter = ShowHabitPresenter(
context = this,
habit = habit,
preferences = appComponent.preferences,
)
val screen = ShowHabitScreen(
activity = this,
confirmDeleteDialogFactory = ConfirmDeleteDialogFactory { this },
habit = habit,
intentFactory = IntentFactory(),
numberPickerFactory = NumberPickerFactory(this),
@ -129,7 +128,12 @@ class ShowHabitActivity : AppCompatActivity(), CommandRunner.Listener {
fun refresh() {
scope.launch {
view.update(presenter.present())
view.update(
presenter.present(
habit = habit,
preferences = preferences,
)
)
}
}
}

@ -20,7 +20,7 @@
package org.isoron.uhabits.activities.habits.show
import org.isoron.uhabits.R
import org.isoron.uhabits.activities.common.dialogs.ConfirmDeleteDialogFactory
import org.isoron.uhabits.activities.common.dialogs.ConfirmDeleteDialog
import org.isoron.uhabits.activities.common.dialogs.HistoryEditorDialog
import org.isoron.uhabits.activities.common.dialogs.NumberPickerFactory
import org.isoron.uhabits.core.models.Habit
@ -36,7 +36,6 @@ import org.isoron.uhabits.widgets.WidgetUpdater
class ShowHabitScreen(
val activity: ShowHabitActivity,
val confirmDeleteDialogFactory: ConfirmDeleteDialogFactory,
val habit: Habit,
val intentFactory: IntentFactory,
val numberPickerFactory: NumberPickerFactory,
@ -79,7 +78,7 @@ class ShowHabitScreen(
}
override fun showDeleteConfirmationScreen(callback: OnConfirmedCallback) {
confirmDeleteDialogFactory.create(callback, 1).show()
ConfirmDeleteDialog(activity, callback, 1).show()
}
override fun close() {

@ -22,45 +22,10 @@ package org.isoron.uhabits.activities.habits.show
import android.content.Context
import android.view.LayoutInflater
import android.widget.FrameLayout
import org.isoron.uhabits.activities.habits.show.views.BarCardPresenter
import org.isoron.uhabits.activities.habits.show.views.BarCardViewModel
import org.isoron.uhabits.activities.habits.show.views.FrequencyCardPresenter
import org.isoron.uhabits.activities.habits.show.views.FrequencyCardViewModel
import org.isoron.uhabits.activities.habits.show.views.HistoryCardPresenter
import org.isoron.uhabits.activities.habits.show.views.HistoryCardViewModel
import org.isoron.uhabits.activities.habits.show.views.NotesCardPresenter
import org.isoron.uhabits.activities.habits.show.views.NotesCardViewModel
import org.isoron.uhabits.activities.habits.show.views.OverviewCardPresenter
import org.isoron.uhabits.activities.habits.show.views.OverviewCardViewModel
import org.isoron.uhabits.activities.habits.show.views.ScoreCardPresenter
import org.isoron.uhabits.activities.habits.show.views.ScoreCardViewModel
import org.isoron.uhabits.activities.habits.show.views.StreakCardViewModel
import org.isoron.uhabits.activities.habits.show.views.StreakCartPresenter
import org.isoron.uhabits.activities.habits.show.views.SubtitleCardPresenter
import org.isoron.uhabits.activities.habits.show.views.SubtitleCardViewModel
import org.isoron.uhabits.activities.habits.show.views.TargetCardPresenter
import org.isoron.uhabits.activities.habits.show.views.TargetCardViewModel
import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.PaletteColor
import org.isoron.uhabits.core.preferences.Preferences
import org.isoron.uhabits.core.ui.screens.habits.show.ShowHabitViewModel
import org.isoron.uhabits.databinding.ShowHabitBinding
import org.isoron.uhabits.utils.setupToolbar
data class ShowHabitViewModel(
val title: String = "",
val isNumerical: Boolean = false,
val color: PaletteColor = PaletteColor(1),
val subtitle: SubtitleCardViewModel,
val overview: OverviewCardViewModel,
val notes: NotesCardViewModel,
val target: TargetCardViewModel,
val streaks: StreakCardViewModel,
val scores: ScoreCardViewModel,
val frequency: FrequencyCardViewModel,
val history: HistoryCardViewModel,
val bar: BarCardViewModel,
)
class ShowHabitView(context: Context) : FrameLayout(context) {
private val binding = ShowHabitBinding.inflate(LayoutInflater.from(context))
@ -96,58 +61,3 @@ class ShowHabitView(context: Context) : FrameLayout(context) {
}
}
}
class ShowHabitPresenter(
val habit: Habit,
val context: Context,
val preferences: Preferences,
) {
private val subtitleCardPresenter = SubtitleCardPresenter(habit, context)
private val overviewCardPresenter = OverviewCardPresenter(habit)
private val notesCardPresenter = NotesCardPresenter(habit)
private val targetCardPresenter = TargetCardPresenter(
habit = habit,
firstWeekday = preferences.firstWeekday,
resources = context.resources,
)
private val streakCartPresenter = StreakCartPresenter(habit)
private val scoreCardPresenter = ScoreCardPresenter(
habit = habit,
firstWeekday = preferences.firstWeekday,
)
private val frequencyCardPresenter = FrequencyCardPresenter(
habit = habit,
firstWeekday = preferences.firstWeekday,
)
private val historyCardViewModel = HistoryCardPresenter(
habit = habit,
firstWeekday = preferences.firstWeekday,
isSkipEnabled = preferences.isSkipEnabled,
)
private val barCardPresenter = BarCardPresenter(
habit = habit,
firstWeekday = preferences.firstWeekday,
)
suspend fun present(): ShowHabitViewModel {
return ShowHabitViewModel(
title = habit.name,
color = habit.color,
isNumerical = habit.isNumerical,
subtitle = subtitleCardPresenter.present(),
overview = overviewCardPresenter.present(),
notes = notesCardPresenter.present(),
target = targetCardPresenter.present(),
streaks = streakCartPresenter.present(),
scores = scoreCardPresenter.present(
spinnerPosition = preferences.scoreCardSpinnerPosition
),
frequency = frequencyCardPresenter.present(),
history = historyCardViewModel.present(),
bar = barCardPresenter.present(
boolSpinnerPosition = preferences.barCardBoolSpinnerPosition,
numericalSpinnerPosition = preferences.barCardNumericalSpinnerPosition,
),
)
}
}

@ -1,123 +0,0 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker 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.
*
* Loop Habit Tracker 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 <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities.habits.show.views
import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.View
import android.widget.AdapterView
import android.widget.LinearLayout
import org.isoron.uhabits.activities.habits.show.views.ScoreCardPresenter.Companion.getTruncateField
import org.isoron.uhabits.core.models.Entry
import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.PaletteColor
import org.isoron.uhabits.core.utils.DateUtils
import org.isoron.uhabits.databinding.ShowHabitBarBinding
import org.isoron.uhabits.utils.toThemedAndroidColor
data class BarCardViewModel(
val entries: List<Entry>,
val bucketSize: Int,
val color: PaletteColor,
val isNumerical: Boolean,
val numericalSpinnerPosition: Int,
val boolSpinnerPosition: Int,
)
class BarCard(context: Context, attrs: AttributeSet) : LinearLayout(context, attrs) {
private var binding = ShowHabitBarBinding.inflate(LayoutInflater.from(context), this)
var onNumericalSpinnerPosition: (position: Int) -> Unit = {}
var onBoolSpinnerPosition: (position: Int) -> Unit = {}
fun update(data: BarCardViewModel) {
binding.barChart.setEntries(data.entries)
binding.barChart.setBucketSize(data.bucketSize)
val androidColor = data.color.toThemedAndroidColor(context)
binding.title.setTextColor(androidColor)
binding.barChart.setColor(androidColor)
if (data.isNumerical) {
binding.boolSpinner.visibility = GONE
} else {
binding.numericalSpinner.visibility = GONE
}
binding.numericalSpinner.onItemSelectedListener = null
binding.numericalSpinner.setSelection(data.numericalSpinnerPosition)
binding.numericalSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
onNumericalSpinnerPosition(position)
}
override fun onNothingSelected(parent: AdapterView<*>?) {
}
}
binding.boolSpinner.onItemSelectedListener = null
binding.boolSpinner.setSelection(data.boolSpinnerPosition)
binding.boolSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
onBoolSpinnerPosition(position)
}
override fun onNothingSelected(parent: AdapterView<*>?) {
}
}
}
}
class BarCardPresenter(
val habit: Habit,
val firstWeekday: Int,
) {
val numericalBucketSizes = intArrayOf(1, 7, 31, 92, 365)
val boolBucketSizes = intArrayOf(7, 31, 92, 365)
fun present(
numericalSpinnerPosition: Int,
boolSpinnerPosition: Int,
): BarCardViewModel {
val bucketSize = if (habit.isNumerical) {
numericalBucketSizes[numericalSpinnerPosition]
} else {
boolBucketSizes[boolSpinnerPosition]
}
val today = DateUtils.getToday()
val oldest = habit.computedEntries.getKnown().lastOrNull()?.timestamp ?: today
val entries = if (bucketSize == 1) {
habit.computedEntries.getByInterval(oldest, today).map {
if (it.value < 0) Entry(it.timestamp, 0) else it
}
} else {
habit.computedEntries.groupBy(
original = habit.computedEntries.getByInterval(oldest, today),
field = getTruncateField(bucketSize),
firstWeekday = firstWeekday,
isNumerical = habit.isNumerical,
)
}
return BarCardViewModel(
entries = entries,
bucketSize = bucketSize,
color = habit.color,
isNumerical = habit.isNumerical,
numericalSpinnerPosition = numericalSpinnerPosition,
boolSpinnerPosition = boolSpinnerPosition,
)
}
}

@ -0,0 +1,82 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker 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.
*
* Loop Habit Tracker 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 <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities.habits.show.views
import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.View
import android.widget.AdapterView
import android.widget.LinearLayout
import org.isoron.uhabits.core.ui.screens.habits.show.views.BarCardViewModel
import org.isoron.uhabits.databinding.ShowHabitBarBinding
import org.isoron.uhabits.utils.toThemedAndroidColor
class BarCardView(context: Context, attrs: AttributeSet) : LinearLayout(context, attrs) {
private var binding = ShowHabitBarBinding.inflate(LayoutInflater.from(context), this)
var onNumericalSpinnerPosition: (position: Int) -> Unit = {}
var onBoolSpinnerPosition: (position: Int) -> Unit = {}
fun update(data: BarCardViewModel) {
binding.barChart.setEntries(data.entries)
binding.barChart.setBucketSize(data.bucketSize)
val androidColor = data.color.toThemedAndroidColor(context)
binding.title.setTextColor(androidColor)
binding.barChart.setColor(androidColor)
if (data.isNumerical) {
binding.boolSpinner.visibility = GONE
} else {
binding.numericalSpinner.visibility = GONE
}
binding.numericalSpinner.onItemSelectedListener = null
binding.numericalSpinner.setSelection(data.numericalSpinnerPosition)
binding.numericalSpinner.onItemSelectedListener =
object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(
parent: AdapterView<*>?,
view: View?,
position: Int,
id: Long
) {
onNumericalSpinnerPosition(position)
}
override fun onNothingSelected(parent: AdapterView<*>?) {
}
}
binding.boolSpinner.onItemSelectedListener = null
binding.boolSpinner.setSelection(data.boolSpinnerPosition)
binding.boolSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(
parent: AdapterView<*>?,
view: View?,
position: Int,
id: Long
) {
onBoolSpinnerPosition(position)
}
override fun onNothingSelected(parent: AdapterView<*>?) {
}
}
}
}

@ -22,20 +22,11 @@ import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import android.widget.LinearLayout
import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.PaletteColor
import org.isoron.uhabits.core.models.Timestamp
import org.isoron.uhabits.core.ui.screens.habits.show.views.FrequencyCardViewModel
import org.isoron.uhabits.databinding.ShowHabitFrequencyBinding
import org.isoron.uhabits.utils.toThemedAndroidColor
import java.util.HashMap
data class FrequencyCardViewModel(
val frequency: HashMap<Timestamp, Array<Int>>,
val firstWeekday: Int,
val color: PaletteColor,
)
class FrequencyCard(context: Context, attrs: AttributeSet) : LinearLayout(context, attrs) {
class FrequencyCardView(context: Context, attrs: AttributeSet) : LinearLayout(context, attrs) {
private var binding = ShowHabitFrequencyBinding.inflate(LayoutInflater.from(context), this)
@ -47,16 +38,3 @@ class FrequencyCard(context: Context, attrs: AttributeSet) : LinearLayout(contex
binding.frequencyChart.setColor(androidColor)
}
}
class FrequencyCardPresenter(
val habit: Habit,
val firstWeekday: Int,
) {
fun present() = FrequencyCardViewModel(
color = habit.color,
frequency = habit.originalEntries.computeWeekdayFrequency(
isNumerical = habit.isNumerical
),
firstWeekday = firstWeekday,
)
}

@ -22,20 +22,11 @@ import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import android.widget.LinearLayout
import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.PaletteColor
import org.isoron.uhabits.core.ui.screens.habits.show.views.HistoryCardViewModel
import org.isoron.uhabits.databinding.ShowHabitHistoryBinding
import org.isoron.uhabits.utils.toThemedAndroidColor
data class HistoryCardViewModel(
val entries: IntArray,
val color: PaletteColor,
val firstWeekday: Int,
val isNumerical: Boolean,
val isSkipEnabled: Boolean,
)
class HistoryCard(context: Context, attrs: AttributeSet) : LinearLayout(context, attrs) {
class HistoryCardView(context: Context, attrs: AttributeSet) : LinearLayout(context, attrs) {
private var binding = ShowHabitHistoryBinding.inflate(LayoutInflater.from(context), this)
@ -57,17 +48,3 @@ class HistoryCard(context: Context, attrs: AttributeSet) : LinearLayout(context,
}
}
}
class HistoryCardPresenter(
val habit: Habit,
val firstWeekday: Int,
val isSkipEnabled: Boolean,
) {
fun present() = HistoryCardViewModel(
entries = habit.computedEntries.getAllValues(),
color = habit.color,
firstWeekday = firstWeekday,
isNumerical = habit.isNumerical,
isSkipEnabled = isSkipEnabled,
)
}

@ -23,11 +23,9 @@ import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import android.widget.LinearLayout
import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.ui.screens.habits.show.views.NotesCardViewModel
import org.isoron.uhabits.databinding.ShowHabitNotesBinding
data class NotesCardViewModel(val description: String)
class NotesCardView(context: Context, attrs: AttributeSet) : LinearLayout(context, attrs) {
private val binding = ShowHabitNotesBinding.inflate(LayoutInflater.from(context), this)
fun update(data: NotesCardViewModel) {
@ -40,9 +38,3 @@ class NotesCardView(context: Context, attrs: AttributeSet) : LinearLayout(contex
invalidate()
}
}
class NotesCardPresenter(val habit: Habit) {
fun present() = NotesCardViewModel(
description = habit.description,
)
}

@ -22,25 +22,12 @@ import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import android.widget.LinearLayout
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.invoke
import org.isoron.uhabits.R
import org.isoron.uhabits.core.models.Entry.Companion.YES_MANUAL
import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.PaletteColor
import org.isoron.uhabits.core.utils.DateUtils
import org.isoron.uhabits.core.ui.screens.habits.show.views.OverviewCardViewModel
import org.isoron.uhabits.databinding.ShowHabitOverviewBinding
import org.isoron.uhabits.utils.StyledResources
import org.isoron.uhabits.utils.toThemedAndroidColor
data class OverviewCardViewModel(
val color: PaletteColor,
val scoreMonthDiff: Float,
val scoreYearDiff: Float,
val scoreToday: Float,
val totalCount: Long,
)
class OverviewCardView(context: Context, attrs: AttributeSet) : LinearLayout(context, attrs) {
private val binding = ShowHabitOverviewBinding.inflate(LayoutInflater.from(context), this)
@ -71,26 +58,3 @@ class OverviewCardView(context: Context, attrs: AttributeSet) : LinearLayout(con
postInvalidate()
}
}
class OverviewCardPresenter(val habit: Habit) {
suspend fun present(): OverviewCardViewModel = Dispatchers.IO {
val today = DateUtils.getTodayWithOffset()
val lastMonth = today.minus(30)
val lastYear = today.minus(365)
val scores = habit.scores
val scoreToday = scores.get(today).value.toFloat()
val scoreLastMonth = scores.get(lastMonth).value.toFloat()
val scoreLastYear = scores.get(lastYear).value.toFloat()
val totalCount = habit.originalEntries.getKnown()
.filter { it.value == YES_MANUAL }
.count()
.toLong()
return@IO OverviewCardViewModel(
color = habit.color,
scoreToday = scoreToday,
scoreMonthDiff = scoreToday - scoreLastMonth,
scoreYearDiff = scoreToday - scoreLastYear,
totalCount = totalCount,
)
}
}

@ -1,114 +0,0 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker 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.
*
* Loop Habit Tracker 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 <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities.habits.show.views
import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.View
import android.widget.AdapterView
import android.widget.LinearLayout
import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.PaletteColor
import org.isoron.uhabits.core.models.Score
import org.isoron.uhabits.core.utils.DateUtils
import org.isoron.uhabits.core.utils.DateUtils.TruncateField.DAY
import org.isoron.uhabits.core.utils.DateUtils.TruncateField.MONTH
import org.isoron.uhabits.core.utils.DateUtils.TruncateField.QUARTER
import org.isoron.uhabits.core.utils.DateUtils.TruncateField.WEEK_NUMBER
import org.isoron.uhabits.core.utils.DateUtils.TruncateField.YEAR
import org.isoron.uhabits.databinding.ShowHabitScoreBinding
import org.isoron.uhabits.utils.toThemedAndroidColor
data class ScoreCardViewModel(
val scores: List<Score>,
val bucketSize: Int,
val spinnerPosition: Int,
val color: PaletteColor,
)
class ScoreCard(context: Context, attrs: AttributeSet) : LinearLayout(context, attrs) {
private var binding = ShowHabitScoreBinding.inflate(LayoutInflater.from(context), this)
var onSpinnerPosition: (position: Int) -> Unit = {}
fun update(data: ScoreCardViewModel) {
val androidColor = data.color.toThemedAndroidColor(context)
binding.title.setTextColor(androidColor)
binding.spinner.setSelection(data.spinnerPosition)
binding.scoreView.setScores(data.scores)
binding.scoreView.reset()
binding.scoreView.setBucketSize(data.bucketSize)
binding.scoreView.setColor(androidColor)
binding.spinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
onSpinnerPosition(position)
}
override fun onNothingSelected(parent: AdapterView<*>?) {
}
}
}
}
class ScoreCardPresenter(
val habit: Habit,
val firstWeekday: Int,
) {
companion object {
val BUCKET_SIZES = intArrayOf(1, 7, 31, 92, 365)
fun getTruncateField(bucketSize: Int): DateUtils.TruncateField {
when (bucketSize) {
1 -> return DAY
7 -> return WEEK_NUMBER
31 -> return MONTH
92 -> return QUARTER
365 -> return YEAR
else -> return MONTH
}
}
}
fun present(spinnerPosition: Int): ScoreCardViewModel {
val bucketSize = BUCKET_SIZES[spinnerPosition]
val today = DateUtils.getTodayWithOffset()
val oldest = habit.computedEntries.getKnown().lastOrNull()?.timestamp ?: today
val field = getTruncateField(bucketSize)
val scores = habit.scores.getByInterval(oldest, today).groupBy {
DateUtils.truncate(field, it.timestamp, firstWeekday)
}.map { (timestamp, scores) ->
Score(
timestamp,
scores.map {
it.value
}.average()
)
}.sortedBy {
it.timestamp
}.reversed()
return ScoreCardViewModel(
color = habit.color,
scores = scores,
bucketSize = bucketSize,
spinnerPosition = spinnerPosition,
)
}
}

@ -0,0 +1,59 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker 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.
*
* Loop Habit Tracker 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 <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities.habits.show.views
import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.View
import android.widget.AdapterView
import android.widget.LinearLayout
import org.isoron.uhabits.core.ui.screens.habits.show.views.ScoreCardViewModel
import org.isoron.uhabits.databinding.ShowHabitScoreBinding
import org.isoron.uhabits.utils.toThemedAndroidColor
class ScoreCardView(context: Context, attrs: AttributeSet) : LinearLayout(context, attrs) {
private var binding = ShowHabitScoreBinding.inflate(LayoutInflater.from(context), this)
var onSpinnerPosition: (position: Int) -> Unit = {}
fun update(data: ScoreCardViewModel) {
val androidColor = data.color.toThemedAndroidColor(context)
binding.title.setTextColor(androidColor)
binding.spinner.setSelection(data.spinnerPosition)
binding.scoreView.setScores(data.scores)
binding.scoreView.reset()
binding.scoreView.setBucketSize(data.bucketSize)
binding.scoreView.setColor(androidColor)
binding.spinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(
parent: AdapterView<*>?,
view: View?,
position: Int,
id: Long
) {
onSpinnerPosition(position)
}
override fun onNothingSelected(parent: AdapterView<*>?) {
}
}
}
}

@ -22,19 +22,10 @@ import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import android.widget.LinearLayout
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.invoke
import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.PaletteColor
import org.isoron.uhabits.core.models.Streak
import org.isoron.uhabits.core.ui.screens.habits.show.views.StreakCardViewModel
import org.isoron.uhabits.databinding.ShowHabitStreakBinding
import org.isoron.uhabits.utils.toThemedAndroidColor
data class StreakCardViewModel(
val color: PaletteColor,
val bestStreaks: List<Streak>
)
class StreakCardView(context: Context, attrs: AttributeSet) : LinearLayout(context, attrs) {
private val binding = ShowHabitStreakBinding.inflate(LayoutInflater.from(context), this)
fun update(data: StreakCardViewModel) {
@ -45,12 +36,3 @@ class StreakCardView(context: Context, attrs: AttributeSet) : LinearLayout(conte
postInvalidate()
}
}
class StreakCartPresenter(val habit: Habit) {
suspend fun present(): StreakCardViewModel = Dispatchers.IO {
return@IO StreakCardViewModel(
color = habit.color,
bestStreaks = habit.streaks.getBest(10),
)
}
}

@ -28,23 +28,13 @@ import android.widget.LinearLayout
import org.isoron.uhabits.R
import org.isoron.uhabits.activities.habits.list.views.toShortString
import org.isoron.uhabits.core.models.Frequency
import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.PaletteColor
import org.isoron.uhabits.core.ui.screens.habits.show.views.SubtitleCardViewModel
import org.isoron.uhabits.databinding.ShowHabitSubtitleBinding
import org.isoron.uhabits.utils.InterfaceUtils
import org.isoron.uhabits.utils.formatTime
import org.isoron.uhabits.utils.toThemedAndroidColor
import java.util.Locale
data class SubtitleCardViewModel(
val color: PaletteColor,
val frequencyText: String,
val isNumerical: Boolean,
val question: String,
val reminderText: String,
val targetText: String,
)
class SubtitleCardView(context: Context, attrs: AttributeSet) : LinearLayout(context, attrs) {
private val binding = ShowHabitSubtitleBinding.inflate(LayoutInflater.from(context), this)
@ -56,13 +46,19 @@ class SubtitleCardView(context: Context, attrs: AttributeSet) : LinearLayout(con
binding.reminderIcon.typeface = fontAwesome
}
@SuppressLint("SetTextI18n")
fun update(data: SubtitleCardViewModel) {
val color = data.color.toThemedAndroidColor(context)
binding.frequencyLabel.text = data.frequencyText
val reminder = data.reminder
binding.frequencyLabel.text = data.frequency.format(resources)
binding.questionLabel.setTextColor(color)
binding.questionLabel.text = data.question
binding.reminderLabel.text = data.reminderText
binding.targetText.text = data.targetText
binding.reminderLabel.text = if (reminder != null) {
formatTime(context, reminder.hour, reminder.minute)
} else {
resources.getString(R.string.reminder_off)
}
binding.targetText.text = "${data.targetValue.toShortString()} ${data.unit}"
binding.questionLabel.visibility = View.VISIBLE
binding.targetIcon.visibility = View.VISIBLE
@ -77,32 +73,9 @@ class SubtitleCardView(context: Context, attrs: AttributeSet) : LinearLayout(con
postInvalidate()
}
}
class SubtitleCardPresenter(
val habit: Habit,
val context: Context,
) {
val resources: Resources = context.resources
fun present(): SubtitleCardViewModel {
val reminderText = if (habit.hasReminder()) {
formatTime(context, habit.reminder!!.hour, habit.reminder!!.minute)!!
} else {
resources.getString(R.string.reminder_off)
}
return SubtitleCardViewModel(
color = habit.color,
frequencyText = habit.frequency.format(),
isNumerical = habit.isNumerical,
question = habit.question,
reminderText = reminderText,
targetText = "${habit.targetValue.toShortString()} ${habit.unit}",
)
}
@SuppressLint("StringFormatMatches")
private fun Frequency.format(): String {
private fun Frequency.format(resources: Resources): String {
val num = this.numerator
val den = this.denominator
if (num == den) {

@ -1,110 +0,0 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker 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.
*
* Loop Habit Tracker 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 <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities.habits.show.views
import android.content.Context
import android.content.res.Resources
import android.util.AttributeSet
import android.view.LayoutInflater
import android.widget.LinearLayout
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.invoke
import org.isoron.uhabits.R
import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.PaletteColor
import org.isoron.uhabits.core.utils.DateUtils
import org.isoron.uhabits.databinding.ShowHabitTargetBinding
import org.isoron.uhabits.utils.toThemedAndroidColor
import java.util.ArrayList
import java.util.Calendar
data class TargetCardViewModel(
val color: PaletteColor,
val values: List<Double> = listOf(),
val targets: List<Double> = listOf(),
val labels: List<String> = listOf(),
)
class TargetCardView(context: Context, attrs: AttributeSet) : LinearLayout(context, attrs) {
private val binding = ShowHabitTargetBinding.inflate(LayoutInflater.from(context), this)
fun update(data: TargetCardViewModel) {
val androidColor = data.color.toThemedAndroidColor(context)
binding.targetChart.setValues(data.values)
binding.targetChart.setTargets(data.targets)
binding.targetChart.setLabels(data.labels)
binding.title.setTextColor(androidColor)
binding.targetChart.setColor(androidColor)
postInvalidate()
}
}
class TargetCardPresenter(
val habit: Habit,
val firstWeekday: Int,
val resources: Resources,
) {
suspend fun present(): TargetCardViewModel = Dispatchers.IO {
val today = DateUtils.getTodayWithOffset()
val entries = habit.computedEntries
val valueToday = entries.get(today).value / 1e3
val valueThisWeek = entries.getThisWeekValue(firstWeekday, habit.isNumerical) / 1e3
val valueThisMonth = entries.getThisMonthValue(habit.isNumerical) / 1e3
val valueThisQuarter = entries.getThisQuarterValue(habit.isNumerical) / 1e3
val valueThisYear = entries.getThisYearValue(habit.isNumerical) / 1e3
val cal = DateUtils.getStartOfTodayCalendarWithOffset()
val daysInMonth = cal.getActualMaximum(Calendar.DAY_OF_MONTH)
val daysInQuarter = 91
val daysInYear = cal.getActualMaximum(Calendar.DAY_OF_YEAR)
val targetToday = habit.targetValue / habit.frequency.denominator
val targetThisWeek = targetToday * 7
val targetThisMonth = targetToday * daysInMonth
val targetThisQuarter = targetToday * daysInQuarter
val targetThisYear = targetToday * daysInYear
val values = ArrayList<Double>()
if (habit.frequency.denominator <= 1) values.add(valueToday)
if (habit.frequency.denominator <= 7) values.add(valueThisWeek)
values.add(valueThisMonth)
values.add(valueThisQuarter)
values.add(valueThisYear)
val targets = ArrayList<Double>()
if (habit.frequency.denominator <= 1) targets.add(targetToday)
if (habit.frequency.denominator <= 7) targets.add(targetThisWeek)
targets.add(targetThisMonth)
targets.add(targetThisQuarter)
targets.add(targetThisYear)
val labels = ArrayList<String>()
if (habit.frequency.denominator <= 1) labels.add(resources.getString(R.string.today))
if (habit.frequency.denominator <= 7) labels.add(resources.getString(R.string.week))
labels.add(resources.getString(R.string.month))
labels.add(resources.getString(R.string.quarter))
labels.add(resources.getString(R.string.year))
return@IO TargetCardViewModel(
color = habit.color,
values = values,
labels = labels,
targets = targets,
)
}
}

@ -0,0 +1,52 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker 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.
*
* Loop Habit Tracker 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 <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities.habits.show.views
import android.content.Context
import android.content.res.Resources
import android.util.AttributeSet
import android.view.LayoutInflater
import android.widget.LinearLayout
import org.isoron.uhabits.R
import org.isoron.uhabits.core.ui.screens.habits.show.views.TargetCardViewModel
import org.isoron.uhabits.databinding.ShowHabitTargetBinding
import org.isoron.uhabits.utils.toThemedAndroidColor
class TargetCardView(context: Context, attrs: AttributeSet) : LinearLayout(context, attrs) {
private val binding = ShowHabitTargetBinding.inflate(LayoutInflater.from(context), this)
fun update(data: TargetCardViewModel) {
val androidColor = data.color.toThemedAndroidColor(context)
binding.targetChart.setValues(data.values)
binding.targetChart.setTargets(data.targets)
binding.targetChart.setLabels(data.intervals.map { intervalToLabel(resources, it) })
binding.title.setTextColor(androidColor)
binding.targetChart.setColor(androidColor)
postInvalidate()
}
companion object {
fun intervalToLabel(resources: Resources, interval: Int) = when (interval) {
1 -> resources.getString(R.string.today)
7 -> resources.getString(R.string.week)
30 -> resources.getString(R.string.month)
91 -> resources.getString(R.string.quarter)
else -> resources.getString(R.string.year)
}
}
}

@ -19,9 +19,11 @@
package org.isoron.uhabits.inject;
import android.content.*;
import dagger.*;
import android.content.Context;
import dagger.Module;
import dagger.Provides;
@Module
public class ActivityContextModule

@ -19,9 +19,10 @@
package org.isoron.uhabits.inject;
import android.content.*;
import android.content.Context;
import dagger.*;
import dagger.Module;
import dagger.Provides;
@Module
public class AppContextModule

@ -26,7 +26,8 @@ import org.isoron.uhabits.core.tasks.*;
import java.util.*;
import dagger.*;
import dagger.Module;
import dagger.Provides;
@Module
public class AndroidTaskRunner implements TaskRunner

@ -21,10 +21,7 @@ package org.isoron.uhabits.tasks;
import android.content.*;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.auto.factory.*;
import androidx.annotation.*;
import org.isoron.uhabits.*;
import org.isoron.uhabits.core.tasks.*;
@ -33,7 +30,6 @@ import org.isoron.uhabits.utils.*;
import java.io.*;
@AutoFactory(allowSubclasses = true)
public class ExportDBTask implements Task
{
private String filename;
@ -46,8 +42,8 @@ public class ExportDBTask implements Task
@NonNull
private final Listener listener;
public ExportDBTask(@Provided @AppContext @NonNull Context context,
@Provided @NonNull AndroidDirFinder system,
public ExportDBTask(@AppContext @NonNull Context context,
@NonNull AndroidDirFinder system,
@NonNull Listener listener)
{
this.system = system;

@ -0,0 +1,33 @@
/*
* Copyright (C) 2016-2020 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker 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.
*
* Loop Habit Tracker 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 <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.tasks
import android.content.Context
import org.isoron.uhabits.AndroidDirFinder
import org.isoron.uhabits.inject.AppContext
import javax.inject.Inject
class ExportDBTaskFactory
@Inject constructor(
@AppContext private val context: Context,
private val system: AndroidDirFinder,
) {
fun create(listener: ExportDBTask.Listener) = ExportDBTask(context, system, listener)
}

@ -23,8 +23,6 @@ import android.util.*;
import androidx.annotation.NonNull;
import com.google.auto.factory.*;
import org.isoron.uhabits.core.io.*;
import org.isoron.uhabits.core.models.ModelFactory;
import org.isoron.uhabits.core.models.sqlite.SQLModelFactory;
@ -32,7 +30,6 @@ import org.isoron.uhabits.core.tasks.*;
import java.io.*;
@AutoFactory(allowSubclasses = true)
public class ImportDataTask implements Task
{
public static final int FAILED = 3;
@ -53,8 +50,8 @@ public class ImportDataTask implements Task
@NonNull
private final Listener listener;
public ImportDataTask(@Provided @NonNull GenericImporter importer,
@Provided @NonNull ModelFactory modelFactory,
public ImportDataTask(@NonNull GenericImporter importer,
@NonNull ModelFactory modelFactory,
@NonNull File file,
@NonNull Listener listener)
{

@ -0,0 +1,34 @@
/*
* Copyright (C) 2016-2020 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker 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.
*
* Loop Habit Tracker 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 <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.tasks
import org.isoron.uhabits.core.io.GenericImporter
import org.isoron.uhabits.core.models.ModelFactory
import java.io.File
import javax.inject.Inject
class ImportDataTaskFactory
@Inject constructor(
private val importer: GenericImporter,
private val modelFactory: ModelFactory,
) {
fun create(file: File, listener: ImportDataTask.Listener) =
ImportDataTask(importer, modelFactory, file, listener)
}

@ -24,6 +24,7 @@ import android.content.Context
import android.view.View
import org.isoron.uhabits.activities.common.views.HistoryChart
import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.ui.screens.habits.show.views.HistoryCardPresenter
import org.isoron.uhabits.utils.toThemedAndroidColor
import org.isoron.uhabits.widgets.views.GraphWidgetView
@ -42,12 +43,17 @@ class HistoryWidget(
val widgetView = view as GraphWidgetView
widgetView.setBackgroundAlpha(preferedBackgroundAlpha)
if (preferedBackgroundAlpha >= 255) widgetView.setShadowAlpha(0x4f)
val model = HistoryCardPresenter().present(
habit = habit,
isSkipEnabled = prefs.isSkipEnabled,
firstWeekday = prefs.firstWeekday,
)
(widgetView.dataView as HistoryChart).apply {
setFirstWeekday(firstWeekday)
setSkipEnabled(prefs.isSkipEnabled)
setColor(habit.color.toThemedAndroidColor(context))
setEntries(habit.computedEntries.getAllValues())
setNumerical(habit.isNumerical)
setFirstWeekday(model.firstWeekday)
setSkipEnabled(model.isSkipEnabled)
setColor(model.color.toThemedAndroidColor(context))
setEntries(model.entries)
setNumerical(model.isNumerical)
}
}

@ -22,8 +22,8 @@ package org.isoron.uhabits.widgets
import android.content.Context
import android.view.View
import org.isoron.uhabits.activities.common.views.ScoreChart
import org.isoron.uhabits.activities.habits.show.views.ScoreCardPresenter
import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.ui.screens.habits.show.views.ScoreCardPresenter
import org.isoron.uhabits.utils.toThemedAndroidColor
import org.isoron.uhabits.widgets.views.GraphWidgetView
@ -37,8 +37,12 @@ class ScoreWidget(
pendingIntentFactory.showHabit(habit)
override fun refreshData(view: View) {
val presenter = ScoreCardPresenter(habit, prefs.firstWeekday)
val viewModel = presenter.present(prefs.scoreCardSpinnerPosition)
val presenter = ScoreCardPresenter()
val viewModel = presenter.present(
habit = habit,
firstWeekday = prefs.firstWeekday,
spinnerPosition = prefs.scoreCardSpinnerPosition
)
val widgetView = view as GraphWidgetView
widgetView.setBackgroundAlpha(preferedBackgroundAlpha)
if (preferedBackgroundAlpha >= 255) widgetView.setShadowAlpha(0x4f)

@ -25,8 +25,9 @@ import android.view.ViewGroup.LayoutParams
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import kotlinx.coroutines.runBlocking
import org.isoron.uhabits.activities.common.views.TargetChart
import org.isoron.uhabits.activities.habits.show.views.TargetCardPresenter
import org.isoron.uhabits.activities.habits.show.views.TargetCardView.Companion.intervalToLabel
import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.ui.screens.habits.show.views.TargetCardPresenter
import org.isoron.uhabits.utils.toThemedAndroidColor
import org.isoron.uhabits.widgets.views.GraphWidgetView
@ -44,11 +45,11 @@ class TargetWidget(
widgetView.setBackgroundAlpha(preferedBackgroundAlpha)
if (preferedBackgroundAlpha >= 255) widgetView.setShadowAlpha(0x4f)
val chart = (widgetView.dataView as TargetChart)
val presenter = TargetCardPresenter(habit, prefs.firstWeekday, context.resources)
val data = presenter.present()
val presenter = TargetCardPresenter()
val data = presenter.present(habit, prefs.firstWeekday)
chart.setColor(data.color.toThemedAndroidColor(context))
chart.setTargets(data.targets)
chart.setLabels(data.labels)
chart.setLabels(data.intervals.map { intervalToLabel(context.resources, it) })
chart.setValues(data.values)
}

@ -62,17 +62,17 @@
style="@style/Card"
android:paddingTop="12dp"/>
<org.isoron.uhabits.activities.habits.show.views.ScoreCard
<org.isoron.uhabits.activities.habits.show.views.ScoreCardView
android:id="@+id/scoreCard"
style="@style/Card"
android:gravity="center"/>
<org.isoron.uhabits.activities.habits.show.views.BarCard
<org.isoron.uhabits.activities.habits.show.views.BarCardView
android:id="@+id/barCard"
style="@style/Card"
android:gravity="center"/>
<org.isoron.uhabits.activities.habits.show.views.HistoryCard
<org.isoron.uhabits.activities.habits.show.views.HistoryCardView
android:id="@+id/historyCard"
style="@style/Card"
android:gravity="center"
@ -82,7 +82,7 @@
android:id="@+id/streakCard"
style="@style/Card"/>
<org.isoron.uhabits.activities.habits.show.views.FrequencyCard
<org.isoron.uhabits.activities.habits.show.views.FrequencyCardView
android:id="@+id/frequencyCard"
style="@style/Card"/>

@ -3,9 +3,7 @@ apply plugin: 'java'
apply plugin: 'kotlin'
dependencies {
annotationProcessor "com.google.auto.factory:auto-factory:$AUTO_FACTORY_VERSION"
annotationProcessor "com.google.dagger:dagger:$DAGGER_VERSION"
compileOnly "com.google.auto.factory:auto-factory:$AUTO_FACTORY_VERSION"
compileOnly "com.google.dagger:dagger:$DAGGER_VERSION"
compileOnly 'javax.annotation:jsr250-api:1.0'
compileOnly 'org.jetbrains:annotations:18.0.0'

@ -18,6 +18,7 @@
*/
package org.isoron.uhabits.core.io
import org.isoron.uhabits.core.models.Entry
import org.isoron.uhabits.core.models.EntryList
import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.HabitList
@ -148,10 +149,10 @@ class HabitsCSVExporter(
val timeframe = getTimeframe()
val oldest = timeframe[0]
val newest = DateUtils.getToday()
val checkmarks: MutableList<IntArray> = ArrayList()
val checkmarks: MutableList<ArrayList<Entry>> = ArrayList()
val scores: MutableList<ArrayList<Score>> = ArrayList()
for (habit in selectedHabits) {
checkmarks.add(habit.computedEntries.getValues(oldest, newest))
checkmarks.add(ArrayList(habit.computedEntries.getByInterval(oldest, newest)))
scores.add(ArrayList(habit.scores.getByInterval(oldest, newest)))
}

@ -28,6 +28,7 @@ import java.util.ArrayList
import java.util.Calendar
import javax.annotation.concurrent.ThreadSafe
import kotlin.collections.set
import kotlin.math.max
import kotlin.math.min
@ThreadSafe
@ -79,45 +80,6 @@ open class EntryList {
return entriesByTimestamp.values.sortedBy { it.timestamp }.reversed()
}
/**
* Truncates the timestamps of all known entries, then aggregates their values. This function
* is used to generate bar plots where each bar shows the number of repetitions in a given week,
* month or year.
*
* For boolean habits, the value of the aggregated entry equals to the number of YES_MANUAL
* entries. For numerical habits, the value is the total sum. The field [firstWeekday] is only
* relevant when grouping by week.
*/
@Synchronized
open fun groupBy(
original: List<Entry>,
field: DateUtils.TruncateField,
firstWeekday: Int,
isNumerical: Boolean,
): List<Entry> {
val truncated = original.map {
Entry(it.timestamp.truncate(field, firstWeekday), it.value)
}
val timestamps = mutableListOf<Timestamp>()
val values = mutableListOf<Int>()
for (i in truncated.indices) {
if (i == 0 || timestamps.last() != truncated[i].timestamp) {
timestamps.add(truncated[i].timestamp)
values.add(0)
}
if (isNumerical) {
if (truncated[i].value > 0) {
values[values.lastIndex] += truncated[i].value
}
} else {
if (truncated[i].value == YES_MANUAL) {
values[values.lastIndex] += 1000
}
}
}
return timestamps.indices.map { Entry(timestamps[it], values[it]) }
}
/**
* Replaces all entries in this list by entries computed automatically from another list.
*
@ -189,88 +151,6 @@ open class EntryList {
return map
}
/**
* Returns the values of the entries that fall inside a certain interval of time. The values
* are returned in an array containing one integer value for each day of the interval. The
* first entry corresponds to the most recent day in the interval. Each subsequent entry
* corresponds to one day older than the previous entry. The boundaries of the time interval
* are included.
*/
@Deprecated("")
@Synchronized
fun getValues(from: Timestamp, to: Timestamp): IntArray {
if (from.isNewerThan(to)) throw IllegalArgumentException()
val nDays = from.daysUntil(to) + 1
val result = IntArray(nDays) { UNKNOWN }
getKnown().filter { entry ->
!entry.timestamp.isNewerThan(to) && !entry.timestamp.isOlderThan(from)
}.forEach { entry ->
val offset = entry.timestamp.daysUntil(to)
result[offset] = entry.value
}
return result
}
@Deprecated("")
@Synchronized
fun getAllValues(): IntArray {
val entries = getKnown()
if (entries.isEmpty()) return IntArray(0)
var (fromTimestamp, _) = entries.last()
val toTimestamp = DateUtils.getTodayWithOffset()
if (fromTimestamp.isNewerThan(toTimestamp)) fromTimestamp = toTimestamp
return getValues(fromTimestamp, toTimestamp)
}
@Deprecated("")
@Synchronized
open fun getThisWeekValue(firstWeekday: Int, isNumerical: Boolean): Int {
return getThisIntervalValue(
truncateField = DateUtils.TruncateField.WEEK_NUMBER,
firstWeekday = firstWeekday,
isNumerical = isNumerical
)
}
@Deprecated("")
@Synchronized
open fun getThisMonthValue(isNumerical: Boolean): Int {
return getThisIntervalValue(
truncateField = DateUtils.TruncateField.MONTH,
firstWeekday = Calendar.SATURDAY,
isNumerical = isNumerical
)
}
@Deprecated("")
@Synchronized
open fun getThisQuarterValue(isNumerical: Boolean): Int {
return getThisIntervalValue(
truncateField = DateUtils.TruncateField.QUARTER,
firstWeekday = Calendar.SATURDAY,
isNumerical = isNumerical
)
}
@Deprecated("")
@Synchronized
open fun getThisYearValue(isNumerical: Boolean): Int {
return getThisIntervalValue(
truncateField = DateUtils.TruncateField.YEAR,
firstWeekday = Calendar.SATURDAY,
isNumerical = isNumerical
)
}
private fun getThisIntervalValue(
truncateField: DateUtils.TruncateField,
firstWeekday: Int,
isNumerical: Boolean,
): Int {
val groups: List<Entry> = groupBy(getKnown(), truncateField, firstWeekday, isNumerical)
return if (groups.isEmpty()) 0 else groups[0].value
}
data class Interval(val begin: Timestamp, val center: Timestamp, val end: Timestamp) {
val length: Int
get() = begin.daysUntil(end) + 1
@ -377,3 +257,41 @@ open class EntryList {
}
}
}
/**
* Given a list of entries, truncates the timestamp of each entry (according to the field given),
* groups the entries according to this truncated timestamp, then creates a new entry (t,v) for
* each group, where t is the truncated timestamp and v is the sum of the values of all entries in
* the group.
*
* For numerical habits, non-positive entry values are converted to zero. For boolean habits, each
* YES_MANUAL value is converted to 1000 and all other values are converted to zero.
*
* The returned list is sorted by timestamp, with the newest entry coming first and the oldest entry
* coming last. If the original list has gaps in it (for example, weeks or months without any
* entries), then the list produced by this method will also have gaps.
*
* The argument [firstWeekday] is only relevant when truncating by week.
*/
fun List<Entry>.groupedSum(
truncateField: DateUtils.TruncateField,
firstWeekday: Int = Calendar.SATURDAY,
isNumerical: Boolean,
): List<Entry> {
return this.map { (timestamp, value) ->
if (isNumerical) {
Entry(timestamp, max(0, value))
} else {
Entry(timestamp, if (value == YES_MANUAL) 1000 else 0)
}
}.groupBy { entry ->
entry.timestamp.truncate(
truncateField,
firstWeekday,
)
}.entries.map { (timestamp, entries) ->
Entry(timestamp, entries.sumOf { it.value })
}.sortedBy { (timestamp, _) ->
- timestamp.unixTime
}
}

@ -21,9 +21,12 @@ package org.isoron.uhabits.core.models;
import org.apache.commons.lang3.builder.*;
import org.isoron.uhabits.core.utils.*;
import org.jetbrains.annotations.*;
import java.util.*;
import kotlin.*;
import static java.util.Calendar.*;
public final class Timestamp implements Comparable<Timestamp>

@ -26,7 +26,6 @@ import org.isoron.uhabits.core.models.EntryList
import org.isoron.uhabits.core.models.Frequency
import org.isoron.uhabits.core.models.Timestamp
import org.isoron.uhabits.core.models.sqlite.records.EntryRecord
import org.isoron.uhabits.core.utils.DateUtils
class SQLiteEntryList(database: Database) : EntryList() {
val repository = Repository(EntryRecord::class.java, database)
@ -79,16 +78,6 @@ class SQLiteEntryList(database: Database) : EntryList() {
return super.getKnown()
}
override fun groupBy(
original: List<Entry>,
field: DateUtils.TruncateField,
firstWeekday: Int,
isNumerical: Boolean
): List<Entry> {
loadRecords()
return super.groupBy(original, field, firstWeekday, isNumerical)
}
override fun recomputeFrom(originalEntries: EntryList, frequency: Frequency, isNumerical: Boolean) {
throw UnsupportedOperationException()
}

@ -21,15 +21,12 @@ package org.isoron.uhabits.core.tasks;
import androidx.annotation.*;
import com.google.auto.factory.*;
import org.isoron.uhabits.core.io.*;
import org.isoron.uhabits.core.models.*;
import java.io.*;
import java.util.*;
@AutoFactory(allowSubclasses = true)
public class ExportCSVTask implements Task
{
private String archiveFilename;
@ -45,7 +42,7 @@ public class ExportCSVTask implements Task
@NonNull
private final HabitList habitList;
public ExportCSVTask(@Provided @NonNull HabitList habitList,
public ExportCSVTask(@NonNull HabitList habitList,
@NonNull List<Habit> selectedHabits,
@NonNull File outputDir,
@NonNull Listener listener)

@ -0,0 +1,36 @@
/*
* Copyright (C) 2016-2020 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker 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.
*
* Loop Habit Tracker 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 <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.core.tasks
import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.HabitList
import java.io.File
import javax.inject.Inject
class ExportCSVTaskFactory
@Inject constructor(
val habitList: HabitList
) {
fun create(
selectedHabits: List<Habit>,
outputDir: File,
listener: ExportCSVTask.Listener,
) = ExportCSVTask(habitList, selectedHabits, outputDir, listener)
}

@ -21,6 +21,7 @@ package org.isoron.uhabits.core.ui.screens.habits.list;
import androidx.annotation.*;
import org.apache.commons.lang3.*;
import org.isoron.uhabits.core.*;
import org.isoron.uhabits.core.commands.*;
import org.isoron.uhabits.core.models.*;
@ -363,9 +364,12 @@ public class HabitCardListCache implements CommandRunner.Listener
if (targetId != null && !targetId.equals(id)) continue;
newData.scores.put(id, habit.getScores().get(today).getValue());
newData.checkmarks.put(
id,
habit.getComputedEntries().getValues(dateFrom, today));
Integer[] entries = habit.getComputedEntries()
.getByInterval(dateFrom, today)
.stream()
.map(Entry::getValue)
.toArray(Integer[]::new);
newData.checkmarks.put(id, ArrayUtils.toPrimitive(entries));
runner.publishProgress(this, position);
}

@ -21,8 +21,6 @@ package org.isoron.uhabits.core.ui.screens.habits.list;
import androidx.annotation.*;
import com.google.auto.factory.*;
import org.isoron.uhabits.core.models.*;
import org.isoron.uhabits.core.preferences.*;
import org.isoron.uhabits.core.utils.*;
@ -31,7 +29,6 @@ import org.isoron.uhabits.core.utils.*;
* Provides a list of hints to be shown at the application startup, and takes
* care of deciding when a new hint should be shown.
*/
@AutoFactory
public class HintList
{
private final Preferences prefs;
@ -44,7 +41,7 @@ public class HintList
*
* @param hints initial list of hints
*/
public HintList(@Provided @NonNull Preferences prefs,
public HintList(@NonNull Preferences prefs,
@NonNull String hints[])
{
this.prefs = prefs;

@ -0,0 +1,30 @@
/*
* Copyright (C) 2016-2020 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker 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.
*
* Loop Habit Tracker 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 <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.core.ui.screens.habits.list
import org.isoron.uhabits.core.preferences.Preferences
import javax.inject.Inject
class HintListFactory
@Inject constructor(
val preferences: Preferences,
) {
fun create(hints: Array<String>) = HintList(preferences, hints)
}

@ -0,0 +1,106 @@
/*
* Copyright (C) 2016-2020 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker 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.
*
* Loop Habit Tracker 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 <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.core.ui.screens.habits.show
import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.PaletteColor
import org.isoron.uhabits.core.preferences.Preferences
import org.isoron.uhabits.core.ui.screens.habits.show.views.BarCardPresenter
import org.isoron.uhabits.core.ui.screens.habits.show.views.BarCardViewModel
import org.isoron.uhabits.core.ui.screens.habits.show.views.FrequencyCardPresenter
import org.isoron.uhabits.core.ui.screens.habits.show.views.FrequencyCardViewModel
import org.isoron.uhabits.core.ui.screens.habits.show.views.HistoryCardPresenter
import org.isoron.uhabits.core.ui.screens.habits.show.views.HistoryCardViewModel
import org.isoron.uhabits.core.ui.screens.habits.show.views.NotesCardPresenter
import org.isoron.uhabits.core.ui.screens.habits.show.views.NotesCardViewModel
import org.isoron.uhabits.core.ui.screens.habits.show.views.OverviewCardPresenter
import org.isoron.uhabits.core.ui.screens.habits.show.views.OverviewCardViewModel
import org.isoron.uhabits.core.ui.screens.habits.show.views.ScoreCardPresenter
import org.isoron.uhabits.core.ui.screens.habits.show.views.ScoreCardViewModel
import org.isoron.uhabits.core.ui.screens.habits.show.views.StreakCardViewModel
import org.isoron.uhabits.core.ui.screens.habits.show.views.StreakCartPresenter
import org.isoron.uhabits.core.ui.screens.habits.show.views.SubtitleCardPresenter
import org.isoron.uhabits.core.ui.screens.habits.show.views.SubtitleCardViewModel
import org.isoron.uhabits.core.ui.screens.habits.show.views.TargetCardPresenter
import org.isoron.uhabits.core.ui.screens.habits.show.views.TargetCardViewModel
data class ShowHabitViewModel(
val title: String = "",
val isNumerical: Boolean = false,
val color: PaletteColor = PaletteColor(1),
val subtitle: SubtitleCardViewModel,
val overview: OverviewCardViewModel,
val notes: NotesCardViewModel,
val target: TargetCardViewModel,
val streaks: StreakCardViewModel,
val scores: ScoreCardViewModel,
val frequency: FrequencyCardViewModel,
val history: HistoryCardViewModel,
val bar: BarCardViewModel,
)
class ShowHabitPresenter {
fun present(
habit: Habit,
preferences: Preferences,
): ShowHabitViewModel {
return ShowHabitViewModel(
title = habit.name,
color = habit.color,
isNumerical = habit.isNumerical,
subtitle = SubtitleCardPresenter().present(
habit = habit,
),
overview = OverviewCardPresenter().present(
habit = habit,
),
notes = NotesCardPresenter().present(
habit = habit,
),
target = TargetCardPresenter().present(
habit = habit,
firstWeekday = preferences.firstWeekday,
),
streaks = StreakCartPresenter().present(
habit = habit,
),
scores = ScoreCardPresenter().present(
spinnerPosition = preferences.scoreCardSpinnerPosition,
habit = habit,
firstWeekday = preferences.firstWeekday,
),
frequency = FrequencyCardPresenter().present(
habit = habit,
firstWeekday = preferences.firstWeekday,
),
history = HistoryCardPresenter().present(
habit = habit,
firstWeekday = preferences.firstWeekday,
isSkipEnabled = preferences.isSkipEnabled,
),
bar = BarCardPresenter().present(
habit = habit,
firstWeekday = preferences.firstWeekday,
boolSpinnerPosition = preferences.barCardBoolSpinnerPosition,
numericalSpinnerPosition = preferences.barCardNumericalSpinnerPosition,
),
)
}
}

@ -0,0 +1,68 @@
/*
* Copyright (C) 2016-2020 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker 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.
*
* Loop Habit Tracker 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 <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.core.ui.screens.habits.show.views
import org.isoron.uhabits.core.models.Entry
import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.PaletteColor
import org.isoron.uhabits.core.models.groupedSum
import org.isoron.uhabits.core.utils.DateUtils
data class BarCardViewModel(
val boolSpinnerPosition: Int,
val bucketSize: Int,
val color: PaletteColor,
val entries: List<Entry>,
val isNumerical: Boolean,
val numericalSpinnerPosition: Int,
)
class BarCardPresenter {
val numericalBucketSizes = intArrayOf(1, 7, 31, 92, 365)
val boolBucketSizes = intArrayOf(7, 31, 92, 365)
fun present(
habit: Habit,
firstWeekday: Int,
numericalSpinnerPosition: Int,
boolSpinnerPosition: Int,
): BarCardViewModel {
val bucketSize = if (habit.isNumerical) {
numericalBucketSizes[numericalSpinnerPosition]
} else {
boolBucketSizes[boolSpinnerPosition]
}
val today = DateUtils.getToday()
val oldest = habit.computedEntries.getKnown().lastOrNull()?.timestamp ?: today
val entries = habit.computedEntries.getByInterval(oldest, today).groupedSum(
truncateField = ScoreCardPresenter.getTruncateField(bucketSize),
firstWeekday = firstWeekday,
isNumerical = habit.isNumerical,
)
return BarCardViewModel(
entries = entries,
bucketSize = bucketSize,
color = habit.color,
isNumerical = habit.isNumerical,
numericalSpinnerPosition = numericalSpinnerPosition,
boolSpinnerPosition = boolSpinnerPosition,
)
}
}

@ -0,0 +1,44 @@
/*
* Copyright (C) 2016-2020 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker 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.
*
* Loop Habit Tracker 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 <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.core.ui.screens.habits.show.views
import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.PaletteColor
import org.isoron.uhabits.core.models.Timestamp
import java.util.HashMap
data class FrequencyCardViewModel(
val color: PaletteColor,
val firstWeekday: Int,
val frequency: HashMap<Timestamp, Array<Int>>,
)
class FrequencyCardPresenter {
fun present(
habit: Habit,
firstWeekday: Int,
) = FrequencyCardViewModel(
color = habit.color,
frequency = habit.originalEntries.computeWeekdayFrequency(
isNumerical = habit.isNumerical
),
firstWeekday = firstWeekday,
)
}

@ -0,0 +1,53 @@
/*
* Copyright (C) 2016-2020 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker 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.
*
* Loop Habit Tracker 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 <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.core.ui.screens.habits.show.views
import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.PaletteColor
import org.isoron.uhabits.core.utils.DateUtils
data class HistoryCardViewModel(
val color: PaletteColor,
val entries: IntArray,
val firstWeekday: Int,
val isNumerical: Boolean,
val isSkipEnabled: Boolean,
)
class HistoryCardPresenter {
fun present(
habit: Habit,
firstWeekday: Int,
isSkipEnabled: Boolean,
): HistoryCardViewModel {
val today = DateUtils.getTodayWithOffset()
val oldest = habit.computedEntries.getKnown().lastOrNull()?.timestamp ?: today
val entries =
habit.computedEntries.getByInterval(oldest, today).map { it.value }.toIntArray()
return HistoryCardViewModel(
entries = entries,
color = habit.color,
firstWeekday = firstWeekday,
isNumerical = habit.isNumerical,
isSkipEnabled = isSkipEnabled,
)
}
}

@ -0,0 +1,32 @@
/*
* Copyright (C) 2016-2020 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker 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.
*
* Loop Habit Tracker 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 <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.core.ui.screens.habits.show.views
import org.isoron.uhabits.core.models.Habit
data class NotesCardViewModel(
val description: String,
)
class NotesCardPresenter {
fun present(habit: Habit) = NotesCardViewModel(
description = habit.description,
)
}

@ -0,0 +1,56 @@
/*
* Copyright (C) 2016-2020 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker 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.
*
* Loop Habit Tracker 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 <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.core.ui.screens.habits.show.views
import org.isoron.uhabits.core.models.Entry
import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.PaletteColor
import org.isoron.uhabits.core.utils.DateUtils
data class OverviewCardViewModel(
val color: PaletteColor,
val scoreMonthDiff: Float,
val scoreYearDiff: Float,
val scoreToday: Float,
val totalCount: Long,
)
class OverviewCardPresenter {
fun present(habit: Habit): OverviewCardViewModel {
val today = DateUtils.getTodayWithOffset()
val lastMonth = today.minus(30)
val lastYear = today.minus(365)
val scores = habit.scores
val scoreToday = scores.get(today).value.toFloat()
val scoreLastMonth = scores.get(lastMonth).value.toFloat()
val scoreLastYear = scores.get(lastYear).value.toFloat()
val totalCount = habit.originalEntries.getKnown()
.filter { it.value == Entry.YES_MANUAL }
.count()
.toLong()
return OverviewCardViewModel(
color = habit.color,
scoreToday = scoreToday,
scoreMonthDiff = scoreToday - scoreLastMonth,
scoreYearDiff = scoreToday - scoreLastYear,
totalCount = totalCount,
)
}
}

@ -0,0 +1,79 @@
/*
* Copyright (C) 2016-2020 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker 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.
*
* Loop Habit Tracker 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 <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.core.ui.screens.habits.show.views
import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.PaletteColor
import org.isoron.uhabits.core.models.Score
import org.isoron.uhabits.core.utils.DateUtils
data class ScoreCardViewModel(
val scores: List<Score>,
val bucketSize: Int,
val spinnerPosition: Int,
val color: PaletteColor,
)
class ScoreCardPresenter {
companion object {
val BUCKET_SIZES = intArrayOf(1, 7, 31, 92, 365)
fun getTruncateField(bucketSize: Int): DateUtils.TruncateField {
when (bucketSize) {
1 -> return DateUtils.TruncateField.DAY
7 -> return DateUtils.TruncateField.WEEK_NUMBER
31 -> return DateUtils.TruncateField.MONTH
92 -> return DateUtils.TruncateField.QUARTER
365 -> return DateUtils.TruncateField.YEAR
else -> return DateUtils.TruncateField.MONTH
}
}
}
fun present(
habit: Habit,
firstWeekday: Int,
spinnerPosition: Int,
): ScoreCardViewModel {
val bucketSize = BUCKET_SIZES[spinnerPosition]
val today = DateUtils.getTodayWithOffset()
val oldest = habit.computedEntries.getKnown().lastOrNull()?.timestamp ?: today
val field = getTruncateField(bucketSize)
val scores = habit.scores.getByInterval(oldest, today).groupBy {
DateUtils.truncate(field, it.timestamp, firstWeekday)
}.map { (timestamp, scores) ->
Score(
timestamp,
scores.map {
it.value
}.average()
)
}.sortedBy {
it.timestamp
}.reversed()
return ScoreCardViewModel(
color = habit.color,
scores = scores,
bucketSize = bucketSize,
spinnerPosition = spinnerPosition,
)
}
}

@ -0,0 +1,38 @@
/*
* Copyright (C) 2016-2020 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker 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.
*
* Loop Habit Tracker 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 <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.core.ui.screens.habits.show.views
import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.PaletteColor
import org.isoron.uhabits.core.models.Streak
data class StreakCardViewModel(
val color: PaletteColor,
val bestStreaks: List<Streak>
)
class StreakCartPresenter {
fun present(habit: Habit): StreakCardViewModel {
return StreakCardViewModel(
color = habit.color,
bestStreaks = habit.streaks.getBest(10),
)
}
}

@ -0,0 +1,49 @@
/*
* Copyright (C) 2016-2020 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker 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.
*
* Loop Habit Tracker 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 <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.core.ui.screens.habits.show.views
import org.isoron.uhabits.core.models.Frequency
import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.PaletteColor
import org.isoron.uhabits.core.models.Reminder
data class SubtitleCardViewModel(
val color: PaletteColor,
val frequency: Frequency,
val isNumerical: Boolean,
val question: String,
val reminder: Reminder?,
val targetValue: Double,
val unit: String,
)
class SubtitleCardPresenter {
fun present(
habit: Habit,
): SubtitleCardViewModel = SubtitleCardViewModel(
color = habit.color,
frequency = habit.frequency,
isNumerical = habit.isNumerical,
question = habit.question,
reminder = habit.reminder,
targetValue = habit.targetValue,
unit = habit.unit,
)
}

@ -0,0 +1,110 @@
/*
* Copyright (C) 2016-2020 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker 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.
*
* Loop Habit Tracker 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 <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.core.ui.screens.habits.show.views
import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.PaletteColor
import org.isoron.uhabits.core.models.groupedSum
import org.isoron.uhabits.core.utils.DateUtils
import java.util.ArrayList
import java.util.Calendar
data class TargetCardViewModel(
val color: PaletteColor,
val values: List<Double> = listOf(),
val targets: List<Double> = listOf(),
val intervals: List<Int> = listOf(),
)
class TargetCardPresenter {
fun present(
habit: Habit,
firstWeekday: Int,
): TargetCardViewModel {
val today = DateUtils.getTodayWithOffset()
val oldest = habit.computedEntries.getKnown().lastOrNull()?.timestamp ?: today
val entries = habit.computedEntries.getByInterval(oldest, today)
val valueToday = entries.groupedSum(
truncateField = DateUtils.TruncateField.DAY,
isNumerical = habit.isNumerical
).firstOrNull()?.value ?: 0
val valueThisWeek = entries.groupedSum(
truncateField = DateUtils.TruncateField.WEEK_NUMBER,
firstWeekday = firstWeekday,
isNumerical = habit.isNumerical
).firstOrNull()?.value ?: 0
val valueThisMonth = entries.groupedSum(
truncateField = DateUtils.TruncateField.MONTH,
isNumerical = habit.isNumerical
).firstOrNull()?.value ?: 0
val valueThisQuarter = entries.groupedSum(
truncateField = DateUtils.TruncateField.QUARTER,
isNumerical = habit.isNumerical
).firstOrNull()?.value ?: 0
val valueThisYear = entries.groupedSum(
truncateField = DateUtils.TruncateField.YEAR,
isNumerical = habit.isNumerical
).firstOrNull()?.value ?: 0
val cal = DateUtils.getStartOfTodayCalendarWithOffset()
val daysInMonth = cal.getActualMaximum(Calendar.DAY_OF_MONTH)
val daysInQuarter = 91
val daysInYear = cal.getActualMaximum(Calendar.DAY_OF_YEAR)
val targetToday = habit.targetValue / habit.frequency.denominator
val targetThisWeek = targetToday * 7
val targetThisMonth = targetToday * daysInMonth
val targetThisQuarter = targetToday * daysInQuarter
val targetThisYear = targetToday * daysInYear
val values = ArrayList<Double>()
if (habit.frequency.denominator <= 1) values.add(valueToday / 1e3)
if (habit.frequency.denominator <= 7) values.add(valueThisWeek / 1e3)
values.add(valueThisMonth / 1e3)
values.add(valueThisQuarter / 1e3)
values.add(valueThisYear / 1e3)
val targets = ArrayList<Double>()
if (habit.frequency.denominator <= 1) targets.add(targetToday)
if (habit.frequency.denominator <= 7) targets.add(targetThisWeek)
targets.add(targetThisMonth)
targets.add(targetThisQuarter)
targets.add(targetThisYear)
val intervals = ArrayList<Int>()
if (habit.frequency.denominator <= 1) intervals.add(1)
if (habit.frequency.denominator <= 7) intervals.add(7)
intervals.add(30)
intervals.add(91)
intervals.add(365)
return TargetCardViewModel(
color = habit.color,
values = values,
targets = targets,
intervals = intervals,
)
}
}

@ -81,13 +81,13 @@ public class WidgetBehavior
}
public void onIncrement(@NotNull Habit habit, @NotNull Timestamp timestamp, int amount) {
int currentValue = habit.getComputedEntries().getValues(timestamp, timestamp)[0];
int currentValue = habit.getComputedEntries().get(timestamp).getValue();
setValue(habit, timestamp, currentValue + amount);
notificationTray.cancel(habit);
}
public void onDecrement(@NotNull Habit habit, @NotNull Timestamp timestamp, int amount) {
int currentValue = habit.getComputedEntries().getValues(timestamp, timestamp)[0];
int currentValue = habit.getComputedEntries().get(timestamp).getValue();
setValue(habit, timestamp, currentValue - amount);
notificationTray.cancel(habit);
}

@ -65,27 +65,6 @@ class EntryListTest {
assertEquals(Entry(today.minus(5), 20), actual[5])
}
@Test
fun testGetValues() {
val entries = EntryList()
val today = DateUtils.getToday()
entries.add(Entry(today.minus(3), YES_MANUAL))
entries.add(Entry(today.minus(5), YES_MANUAL))
entries.add(Entry(today.minus(6), YES_MANUAL))
val expected = intArrayOf(
UNKNOWN, // 1
UNKNOWN, // 2
YES_MANUAL, // 3
UNKNOWN, // 4
YES_MANUAL, // 5
YES_MANUAL, // 6
UNKNOWN, // 7
)
assertThat(entries.getValues(today.minus(7), today.minus(1)), equalTo(expected))
}
@Test
fun testComputeBoolean() {
val today = DateUtils.getToday()
@ -163,10 +142,8 @@ class EntryListTest {
entries.add(Entry(reference.minus(offsets[it]), values[it]))
}
val byMonth = entries.groupBy(
original = entries.getKnown(),
field = DateUtils.TruncateField.MONTH,
firstWeekday = Calendar.SATURDAY,
val byMonth = entries.getKnown().groupedSum(
truncateField = DateUtils.TruncateField.MONTH,
isNumerical = true,
)
assertThat(byMonth.size, equalTo(17))
@ -174,10 +151,8 @@ class EntryListTest {
assertThat(byMonth[6], equalTo(Entry(Timestamp.from(2013, Calendar.DECEMBER, 1), 1988)))
assertThat(byMonth[12], equalTo(Entry(Timestamp.from(2013, Calendar.MAY, 1), 1271)))
val byQuarter = entries.groupBy(
original = entries.getKnown(),
field = DateUtils.TruncateField.QUARTER,
firstWeekday = Calendar.SATURDAY,
val byQuarter = entries.getKnown().groupedSum(
truncateField = DateUtils.TruncateField.QUARTER,
isNumerical = true,
)
assertThat(byQuarter.size, equalTo(6))
@ -185,10 +160,8 @@ class EntryListTest {
assertThat(byQuarter[3], equalTo(Entry(Timestamp.from(2013, Calendar.JULY, 1), 3838)))
assertThat(byQuarter[5], equalTo(Entry(Timestamp.from(2013, Calendar.JANUARY, 1), 4975)))
val byYear = entries.groupBy(
original = entries.getKnown(),
field = DateUtils.TruncateField.YEAR,
firstWeekday = Calendar.SATURDAY,
val byYear = entries.getKnown().groupedSum(
truncateField = DateUtils.TruncateField.YEAR,
isNumerical = true,
)
assertThat(byYear.size, equalTo(2))
@ -213,10 +186,8 @@ class EntryListTest {
entries.add(Entry(reference.minus(offsets[it]), YES_MANUAL))
}
val byMonth = entries.groupBy(
original = entries.getKnown(),
field = DateUtils.TruncateField.MONTH,
firstWeekday = Calendar.SATURDAY,
val byMonth = entries.getKnown().groupedSum(
truncateField = DateUtils.TruncateField.MONTH,
isNumerical = false,
)
assertThat(byMonth.size, equalTo(17))
@ -224,10 +195,8 @@ class EntryListTest {
assertThat(byMonth[6], equalTo(Entry(Timestamp.from(2013, Calendar.DECEMBER, 1), 7_000)))
assertThat(byMonth[12], equalTo(Entry(Timestamp.from(2013, Calendar.MAY, 1), 6_000)))
val byQuarter = entries.groupBy(
original = entries.getKnown(),
field = DateUtils.TruncateField.QUARTER,
firstWeekday = Calendar.SATURDAY,
val byQuarter = entries.getKnown().groupedSum(
truncateField = DateUtils.TruncateField.QUARTER,
isNumerical = false,
)
assertThat(byQuarter.size, equalTo(6))
@ -235,10 +204,8 @@ class EntryListTest {
assertThat(byQuarter[3], equalTo(Entry(Timestamp.from(2013, Calendar.JULY, 1), 17_000)))
assertThat(byQuarter[5], equalTo(Entry(Timestamp.from(2013, Calendar.JANUARY, 1), 20_000)))
val byYear = entries.groupBy(
original = entries.getKnown(),
field = DateUtils.TruncateField.YEAR,
firstWeekday = Calendar.SATURDAY,
val byYear = entries.getKnown().groupedSum(
truncateField = DateUtils.TruncateField.YEAR,
isNumerical = false,
)
assertThat(byYear.size, equalTo(2))

@ -19,6 +19,7 @@
package org.isoron.uhabits.core.ui.screens.habits.list;
import org.apache.commons.lang3.*;
import org.isoron.uhabits.core.*;
import org.isoron.uhabits.core.commands.*;
import org.isoron.uhabits.core.models.*;
@ -104,7 +105,11 @@ public class HabitCardListCacheTest extends BaseUnitTest
assertThat(cache.getScore(h.getId()), equalTo(score));
int[] actualCheckmarks = cache.getCheckmarks(h.getId());
int[] expectedCheckmarks = h.getComputedEntries().getValues(today.minus(9), today);
int[] expectedCheckmarks = ArrayUtils.toPrimitive(h.getComputedEntries()
.getByInterval(today.minus(9), today)
.stream()
.map(Entry::getValue)
.toArray(Integer[]::new));
assertThat(actualCheckmarks, equalTo(expectedCheckmarks));
}

Loading…
Cancel
Save