diff --git a/app/build.gradle b/app/build.gradle index 93d60fe16..5c9ef9d71 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -12,7 +12,7 @@ android { minSdkVersion 15 targetSdkVersion 25 - buildConfigField "Integer", "databaseVersion", "15" + buildConfigField "Integer", "databaseVersion", "18" buildConfigField "String", "databaseFilename", "\"uhabits.db\"" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" diff --git a/app/src/androidTest/java/org/isoron/uhabits/activities/habits/list/views/CheckmarkButtonViewTest.java b/app/src/androidTest/java/org/isoron/uhabits/activities/habits/list/views/CheckmarkButtonViewTest.java index b8bbc0bac..e30669414 100644 --- a/app/src/androidTest/java/org/isoron/uhabits/activities/habits/list/views/CheckmarkButtonViewTest.java +++ b/app/src/androidTest/java/org/isoron/uhabits/activities/habits/list/views/CheckmarkButtonViewTest.java @@ -91,97 +91,4 @@ public class CheckmarkButtonViewTest extends BaseViewTest { assertRenders(view, PATH + "render_unchecked.png"); } - -// @Test -// public void testLongClick() throws Exception -// { -// setOnToggleListener(); -// view.performLongClick(); -// waitForLatch(); -// assertRendersCheckedExplicitly(); -// } -// -// @Test -// public void testClick_withShortToggle_fromUnchecked() throws Exception -// { -// Preferences.getInstance().setShortToggleEnabled(true); -// view.setValue(Checkmark.UNCHECKED); -// setOnToggleListenerAndPerformClick(); -// assertRendersCheckedExplicitly(); -// } -// -// @Test -// public void testClick_withShortToggle_fromChecked() throws Exception -// { -// Preferences.getInstance().setShortToggleEnabled(true); -// view.setValue(Checkmark.CHECKED_EXPLICITLY); -// setOnToggleListenerAndPerformClick(); -// assertRendersUnchecked(); -// } -// -// @Test -// public void testClick_withShortToggle_withoutListener() throws Exception -// { -// Preferences.getInstance().setShortToggleEnabled(true); -// view.setValue(Checkmark.CHECKED_EXPLICITLY); -// view.setController(null); -// view.performClick(); -// assertRendersUnchecked(); -// } -// -// protected void setOnToggleListenerAndPerformClick() throws InterruptedException -// { -// setOnToggleListener(); -// view.performClick(); -// waitForLatch(); -// } -// -// @Test -// public void testClick_withoutShortToggle() throws Exception -// { -// Preferences.getInstance().setShortToggleEnabled(false); -// setOnInvalidToggleListener(); -// view.performClick(); -// waitForLatch(); -// assertRendersUnchecked(); -// } - -// protected void setOnInvalidToggleListener() -// { -// view.setController(new CheckmarkButtonView.Controller() -// { -// @Override -// public void onToggleCheckmark(CheckmarkButtonView view, long timestamp) -// { -// fail(); -// } -// -// @Override -// public void onInvalidToggle(CheckmarkButtonView v) -// { -// assertThat(v, equalTo(view)); -// latch.countDown(); -// } -// }); -// } - -// protected void setOnToggleListener() -// { -// view.setController(new CheckmarkButtonView.Controller() -// { -// @Override -// public void onToggleCheckmark(CheckmarkButtonView v, long t) -// { -// assertThat(v, equalTo(view)); -// assertThat(t, equalTo(DateUtils.getStartOfToday())); -// latch.countDown(); -// } -// -// @Override -// public void onInvalidToggle(CheckmarkButtonView view) -// { -// fail(); -// } -// }); -// } } \ No newline at end of file diff --git a/app/src/androidTest/java/org/isoron/uhabits/activities/habits/list/views/CheckmarkPanelViewTest.java b/app/src/androidTest/java/org/isoron/uhabits/activities/habits/list/views/CheckmarkPanelViewTest.java index 30e225a7a..8be616ef9 100644 --- a/app/src/androidTest/java/org/isoron/uhabits/activities/habits/list/views/CheckmarkPanelViewTest.java +++ b/app/src/androidTest/java/org/isoron/uhabits/activities/habits/list/views/CheckmarkPanelViewTest.java @@ -59,7 +59,7 @@ public class CheckmarkPanelViewTest extends BaseViewTest view = new CheckmarkPanelView(targetContext); view.setHabit(habit); - view.setCheckmarkValues(checkmarks); + view.setValues(checkmarks); view.setButtonCount(4); view.setColor(ColorUtils.getAndroidTestColor(7)); diff --git a/app/src/androidTest/java/org/isoron/uhabits/activities/habits/list/views/HabitCardViewTest.java b/app/src/androidTest/java/org/isoron/uhabits/activities/habits/list/views/HabitCardViewTest.java index 2ca386e18..162f56b9b 100644 --- a/app/src/androidTest/java/org/isoron/uhabits/activities/habits/list/views/HabitCardViewTest.java +++ b/app/src/androidTest/java/org/isoron/uhabits/activities/habits/list/views/HabitCardViewTest.java @@ -59,7 +59,7 @@ public class HabitCardViewTest extends BaseViewTest view = new HabitCardView(targetContext); view.setHabit(habit); - view.setCheckmarkValues(values); + view.setValues(values); view.setSelected(false); view.setScore(habit.getScores().getTodayValue()); view.setController(controller); diff --git a/app/src/androidTest/java/org/isoron/uhabits/espresso/MainActivityActions.java b/app/src/androidTest/java/org/isoron/uhabits/espresso/MainActivityActions.java index c03e9c9db..2fdd59c19 100644 --- a/app/src/androidTest/java/org/isoron/uhabits/espresso/MainActivityActions.java +++ b/app/src/androidTest/java/org/isoron/uhabits/espresso/MainActivityActions.java @@ -182,7 +182,7 @@ public class MainActivityActions try { - onView(allOf(withId(R.id.sFrequency), + onView(allOf(withId(R.id.spinner), withEffectiveVisibility(VISIBLE))).perform(click()); onData(allOf(instanceOf(String.class), startsWith("Custom"))) .inRoot(isPlatformPopup()) @@ -193,7 +193,7 @@ public class MainActivityActions // ignored } - onView(withId(R.id.tvFreqNum)).perform(replaceText(num)); - onView(withId(R.id.tvFreqDen)).perform(replaceText(den)); + onView(withId(R.id.numerator)).perform(replaceText(num)); + onView(withId(R.id.denominator)).perform(replaceText(den)); } } diff --git a/app/src/androidTest/java/org/isoron/uhabits/models/sqlite/SQLiteRepetitionListTest.java b/app/src/androidTest/java/org/isoron/uhabits/models/sqlite/SQLiteRepetitionListTest.java index 992c1e673..766271ada 100644 --- a/app/src/androidTest/java/org/isoron/uhabits/models/sqlite/SQLiteRepetitionListTest.java +++ b/app/src/androidTest/java/org/isoron/uhabits/models/sqlite/SQLiteRepetitionListTest.java @@ -37,6 +37,7 @@ import java.util.*; import static org.hamcrest.MatcherAssert.*; import static org.hamcrest.Matchers.*; import static org.hamcrest.core.IsNot.not; +import static org.isoron.uhabits.models.Checkmark.*; @RunWith(AndroidJUnit4.class) @MediumTest @@ -67,7 +68,7 @@ public class SQLiteRepetitionListTest extends BaseAndroidTest RepetitionRecord record = getByTimestamp(today + day); assertThat(record, is(nullValue())); - Repetition rep = new Repetition(today + day); + Repetition rep = new Repetition(today + day, CHECKED_EXPLICITLY); habit.getRepetitions().add(rep); record = getByTimestamp(today + day); diff --git a/app/src/androidTest/java/org/isoron/uhabits/widgets/views/CheckmarkWidgetViewTest.java b/app/src/androidTest/java/org/isoron/uhabits/widgets/views/CheckmarkWidgetViewTest.java index bb151334b..65c3fbef9 100644 --- a/app/src/androidTest/java/org/isoron/uhabits/widgets/views/CheckmarkWidgetViewTest.java +++ b/app/src/androidTest/java/org/isoron/uhabits/widgets/views/CheckmarkWidgetViewTest.java @@ -50,8 +50,8 @@ public class CheckmarkWidgetViewTest extends BaseViewTest habit = fixtures.createShortHabit(); view = new CheckmarkWidgetView(targetContext); int color = ColorUtils.getAndroidTestColor(habit.getColor()); - int score = habit.getScores().getTodayValue(); - float percentage = (float) score / Score.MAX_VALUE; + double score = habit.getScores().getTodayValue(); + float percentage = (float) score; view.setActiveColor(color); view.setCheckmarkValue(habit.getCheckmarks().getTodayValue()); diff --git a/app/src/main/assets/migrations/16.sql b/app/src/main/assets/migrations/16.sql new file mode 100644 index 000000000..7a7746f63 --- /dev/null +++ b/app/src/main/assets/migrations/16.sql @@ -0,0 +1,2 @@ +alter table Habits add column type integer not null default 0; +alter table Repetitions add column value integer not null default 2; \ No newline at end of file diff --git a/app/src/main/assets/migrations/17.sql b/app/src/main/assets/migrations/17.sql new file mode 100644 index 000000000..9c9a9b409 --- /dev/null +++ b/app/src/main/assets/migrations/17.sql @@ -0,0 +1,5 @@ +DROP TABLE Score; +CREATE TABLE Score (Id INTEGER PRIMARY KEY AUTOINCREMENT, habit INTEGER REFERENCES Habits(Id), score REAL, timestamp INTEGER); +CREATE INDEX idx_score_habit_timestamp on score(habit, timestamp); +delete from Streak; +delete from Checkmarks; \ No newline at end of file diff --git a/app/src/main/assets/migrations/18.sql b/app/src/main/assets/migrations/18.sql new file mode 100644 index 000000000..fa6318bcc --- /dev/null +++ b/app/src/main/assets/migrations/18.sql @@ -0,0 +1,3 @@ +alter table Habits add column target_type integer not null default 0; +alter table Habits add column target_value real not null default 0; +alter table Habits add column unit text not null default ""; \ No newline at end of file diff --git a/app/src/main/java/org/isoron/uhabits/activities/common/dialogs/WeekdayPickerDialog.java b/app/src/main/java/org/isoron/uhabits/activities/common/dialogs/WeekdayPickerDialog.java index 792e403f7..08b754df7 100644 --- a/app/src/main/java/org/isoron/uhabits/activities/common/dialogs/WeekdayPickerDialog.java +++ b/app/src/main/java/org/isoron/uhabits/activities/common/dialogs/WeekdayPickerDialog.java @@ -26,6 +26,7 @@ import android.support.v7.app.AlertDialog; import android.support.v7.app.*; import org.isoron.uhabits.*; +import org.isoron.uhabits.models.*; import org.isoron.uhabits.utils.*; /** @@ -35,7 +36,6 @@ public class WeekdayPickerDialog extends AppCompatDialogFragment implements DialogInterface.OnMultiChoiceClickListener, DialogInterface.OnClickListener { - private boolean[] selectedDays; private OnWeekdaysPickedListener listener; @@ -49,7 +49,8 @@ public class WeekdayPickerDialog extends AppCompatDialogFragment implements @Override public void onClick(DialogInterface dialog, int which) { - if (listener != null) listener.onWeekdaysPicked(selectedDays); + if (listener != null) + listener.onWeekdaysSet(new WeekdayList(selectedDays)); } @Override @@ -73,13 +74,13 @@ public class WeekdayPickerDialog extends AppCompatDialogFragment implements this.listener = listener; } - public void setSelectedDays(boolean[] selectedDays) + public void setSelectedDays(WeekdayList days) { - this.selectedDays = selectedDays; + this.selectedDays = days.toArray(); } public interface OnWeekdaysPickedListener { - void onWeekdaysPicked(boolean[] selectedDays); + void onWeekdaysSet(WeekdayList days); } } diff --git a/app/src/main/java/org/isoron/uhabits/activities/common/views/BarChart.java b/app/src/main/java/org/isoron/uhabits/activities/common/views/BarChart.java new file mode 100644 index 000000000..9367ed282 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/activities/common/views/BarChart.java @@ -0,0 +1,479 @@ +/* + * Copyright (C) 2016 Álinson Santos Xavier + * + * 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 . + */ + +package org.isoron.uhabits.activities.common.views; + +import android.content.*; +import android.graphics.*; +import android.support.annotation.*; +import android.util.*; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.activities.habits.list.views.*; +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.utils.*; + +import java.text.*; +import java.util.*; + +import static org.isoron.uhabits.utils.InterfaceUtils.*; + +public class BarChart extends ScrollableChart +{ + private static final PorterDuffXfermode XFERMODE_CLEAR = + new PorterDuffXfermode(PorterDuff.Mode.CLEAR); + + private static final PorterDuffXfermode XFERMODE_SRC = + new PorterDuffXfermode(PorterDuff.Mode.SRC); + + private Paint pGrid; + + private float em; + + private SimpleDateFormat dfMonth; + + private SimpleDateFormat dfDay; + + private SimpleDateFormat dfYear; + + private Paint pText, pGraph; + + private RectF rect, prevRect; + + private int baseSize; + + private int paddingTop; + + private float columnWidth; + + private int columnHeight; + + private int nColumns; + + private int textColor; + + private int gridColor; + + @Nullable + private List checkmarks; + + private int primaryColor; + + @Deprecated + private int bucketSize = 7; + + private int backgroundColor; + + private Bitmap drawingCache; + + private Canvas cacheCanvas; + + private boolean isTransparencyEnabled; + + private int skipYear = 0; + + private String previousYearText; + + private String previousMonthText; + + private double maxValue; + + private double target; + + public BarChart(Context context) + { + super(context); + init(); + } + + public BarChart(Context context, AttributeSet attrs) + { + super(context, attrs); + init(); + } + + public void populateWithRandomData() + { + Random random = new Random(); + List checkmarks = new LinkedList<>(); + + long timestamp = DateUtils.getStartOfToday(); + long day = DateUtils.millisecondsInOneDay; + + for (int i = 1; i < 100; i++) + { + int value = random.nextInt(1000); + checkmarks.add(new Checkmark(timestamp, value)); + timestamp -= day; + } + + setCheckmarks(checkmarks); + setTarget(0.5); + } + + @Deprecated + public void setBucketSize(int bucketSize) + { + this.bucketSize = bucketSize; + postInvalidate(); + } + + public void setCheckmarks(@NonNull List checkmarks) + { + this.checkmarks = checkmarks; + + maxValue = 1.0; + for (Checkmark c : checkmarks) + maxValue = Math.max(maxValue, c.getValue()); + maxValue = Math.ceil(maxValue / 1000 * 1.05) * 1000; + + postInvalidate(); + } + + public void setColor(int primaryColor) + { + this.primaryColor = primaryColor; + postInvalidate(); + } + + public void setIsTransparencyEnabled(boolean enabled) + { + this.isTransparencyEnabled = enabled; + initColors(); + requestLayout(); + } + + public void setTarget(double target) + { + this.target = target; + postInvalidate(); + } + + @Override + protected void onDraw(Canvas canvas) + { + super.onDraw(canvas); + Canvas activeCanvas; + + if (isTransparencyEnabled) + { + if (drawingCache == null) initCache(getWidth(), getHeight()); + + activeCanvas = cacheCanvas; + drawingCache.eraseColor(Color.TRANSPARENT); + } + else + { + activeCanvas = canvas; + } + + if (checkmarks == null) return; + + rect.set(0, 0, nColumns * columnWidth, columnHeight); + rect.offset(0, paddingTop); + + drawGrid(activeCanvas, rect); + + pText.setColor(textColor); + pGraph.setColor(primaryColor); + prevRect.setEmpty(); + + previousMonthText = ""; + previousYearText = ""; + skipYear = 0; + + for (int k = 0; k < nColumns; k++) + { + int offset = nColumns - k - 1 + getDataOffset(); + if (offset >= checkmarks.size()) continue; + + double value = checkmarks.get(offset).getValue(); + long timestamp = checkmarks.get(offset).getTimestamp(); + int height = (int) (columnHeight * value / maxValue); + + rect.set(0, 0, baseSize, height); + rect.offset(k * columnWidth + (columnWidth - baseSize) / 2, + paddingTop + columnHeight - height); + + drawValue(activeCanvas, rect, value); + drawBar(activeCanvas, rect, value); + + prevRect.set(rect); + rect.set(0, 0, columnWidth, columnHeight); + rect.offset(k * columnWidth, paddingTop); + + drawFooter(activeCanvas, rect, timestamp); + } + + if (activeCanvas != canvas) canvas.drawBitmap(drawingCache, 0, 0, null); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) + { + int width = MeasureSpec.getSize(widthMeasureSpec); + int height = MeasureSpec.getSize(heightMeasureSpec); + setMeasuredDimension(width, height); + } + + @Override + protected void onSizeChanged(int width, + int height, + int oldWidth, + int oldHeight) + { + if (height < 9) height = 200; + + float maxTextSize = getResources().getDimension(R.dimen.tinyTextSize); + float textSize = height * 0.06f; + pText.setTextSize(Math.min(textSize, maxTextSize)); + em = pText.getFontSpacing(); + + int footerHeight = (int) (3 * em); + paddingTop = (int) (em); + + baseSize = (height - footerHeight - paddingTop) / 12; + columnWidth = baseSize; + columnWidth = Math.max(columnWidth, getMaxDayWidth() * 1.5f); + columnWidth = Math.max(columnWidth, getMaxMonthWidth() * 1.2f); + + nColumns = (int) (width / columnWidth); + columnWidth = (float) width / nColumns; + setScrollerBucketSize((int) columnWidth); + + columnHeight = 12 * baseSize; + + float minStrokeWidth = dpToPixels(getContext(), 1); + pGraph.setTextSize(baseSize * 0.5f); + pGraph.setStrokeWidth(baseSize * 0.1f); + pGrid.setStrokeWidth(Math.min(minStrokeWidth, baseSize * 0.05f)); + + if (isTransparencyEnabled) initCache(width, height); + } + + private void drawBar(Canvas canvas, RectF rect, double value) + { + float margin = baseSize * 0.225f; + + int color = textColor; + if (value / 1000 >= target) color = primaryColor; + + rect.inset(-margin, 0); + setModeOrColor(pGraph, XFERMODE_CLEAR, backgroundColor); + canvas.drawRect(rect, pGraph); + + rect.inset(margin, 0); + setModeOrColor(pGraph, XFERMODE_SRC, color); + canvas.drawRect(rect, pGraph); + + if (isTransparencyEnabled) pGraph.setXfermode(XFERMODE_SRC); + } + + private void drawFooter(Canvas canvas, RectF rect, long currentDate) + { + String yearText = dfYear.format(currentDate); + String monthText = dfMonth.format(currentDate); + String dayText = dfDay.format(currentDate); + + GregorianCalendar calendar = DateUtils.getCalendar(currentDate); + pText.setColor(textColor); + + String text; + int year = calendar.get(Calendar.YEAR); + + boolean shouldPrintYear = true; + if (yearText.equals(previousYearText)) shouldPrintYear = false; + if (bucketSize >= 365 && (year % 2) != 0) shouldPrintYear = false; + + if (skipYear > 0) + { + skipYear--; + shouldPrintYear = false; + } + + if (shouldPrintYear) + { + previousYearText = yearText; + previousMonthText = ""; + + pText.setTextAlign(Paint.Align.CENTER); + canvas.drawText(yearText, rect.centerX(), rect.bottom + em * 2.2f, pText); + skipYear = 1; + } + + if (bucketSize < 365) + { + if (!monthText.equals(previousMonthText)) + { + previousMonthText = monthText; + text = monthText; + } + else + { + text = dayText; + } + + canvas.drawText(text, rect.centerX(), rect.bottom + em * 1.2f, + pText); + } + } + + private void drawGrid(Canvas canvas, RectF rGrid) + { + int nRows = 5; + float rowHeight = rGrid.height() / nRows; + + pText.setColor(textColor); + pGrid.setColor(gridColor); + + for (int i = 0; i < nRows; i++) + { + canvas.drawLine(rGrid.left, rGrid.top, rGrid.right, rGrid.top, + pGrid); + rGrid.offset(0, rowHeight); + } + + canvas.drawLine(rGrid.left, rGrid.top, rGrid.right, rGrid.top, pGrid); + } + + private void drawValue(Canvas canvas, RectF rect, double value) + { + if (value == 0) return; + + int activeColor = textColor; + if (value / 1000 >= target) + activeColor = primaryColor; + + String label = NumberButtonView.formatValue(value / 1000); + Rect rText = new Rect(); + pText.getTextBounds(label, 0, label.length(), rText); + + float offset = 0.5f * em; + float x = rect.centerX(); + float y = rect.top - offset; + int cap = (int) (-0.1f * em); + + rText.offset((int) x, (int) y); + rText.offset(-rText.width() / 2, 0); + rText.inset(3 * cap, cap); + + setModeOrColor(pText, XFERMODE_CLEAR, backgroundColor); + canvas.drawRect(rText, pText); + + setModeOrColor(pText, XFERMODE_SRC, activeColor); + canvas.drawText(label, x, y, pText); + } + + private float getMaxDayWidth() + { + float maxDayWidth = 0; + GregorianCalendar day = DateUtils.getStartOfTodayCalendar(); + + for (int i = 0; i < 28; i++) + { + day.set(Calendar.DAY_OF_MONTH, i); + float monthWidth = pText.measureText(dfMonth.format(day.getTime())); + maxDayWidth = Math.max(maxDayWidth, monthWidth); + } + + return maxDayWidth; + } + + private float getMaxMonthWidth() + { + float maxMonthWidth = 0; + GregorianCalendar day = DateUtils.getStartOfTodayCalendar(); + + for (int i = 0; i < 12; i++) + { + day.set(Calendar.MONTH, i); + float monthWidth = pText.measureText(dfMonth.format(day.getTime())); + maxMonthWidth = Math.max(maxMonthWidth, monthWidth); + } + + return maxMonthWidth; + } + + private void init() + { + initPaints(); + initColors(); + initDateFormats(); + initRects(); + } + + private void initCache(int width, int height) + { + if (drawingCache != null) drawingCache.recycle(); + drawingCache = + Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + cacheCanvas = new Canvas(drawingCache); + } + + private void initColors() + { + StyledResources res = new StyledResources(getContext()); + + primaryColor = Color.BLACK; + textColor = res.getColor(R.attr.mediumContrastTextColor); + gridColor = res.getColor(R.attr.lowContrastTextColor); + backgroundColor = res.getColor(R.attr.cardBackgroundColor); + } + + private void initDateFormats() + { + if (isInEditMode()) + { + dfYear = new SimpleDateFormat("yyyy", Locale.US); + dfMonth = new SimpleDateFormat("MMM", Locale.US); + dfDay = new SimpleDateFormat("d", Locale.US); + return; + } + + dfYear = DateFormats.fromSkeleton("yyyy"); + dfMonth = DateFormats.fromSkeleton("MMM"); + dfDay = DateFormats.fromSkeleton("d"); + } + + private void initPaints() + { + pText = new Paint(); + pText.setAntiAlias(true); + pText.setTextAlign(Paint.Align.CENTER); + + pGraph = new Paint(); + pGraph.setTextAlign(Paint.Align.CENTER); + pGraph.setAntiAlias(true); + + pGrid = new Paint(); + pGrid.setAntiAlias(true); + } + + private void initRects() + { + rect = new RectF(); + prevRect = new RectF(); + } + + private void setModeOrColor(Paint p, PorterDuffXfermode mode, int color) + { + if (isTransparencyEnabled) p.setXfermode(mode); + else p.setColor(color); + } +} diff --git a/app/src/main/java/org/isoron/uhabits/activities/common/views/HistoryChart.java b/app/src/main/java/org/isoron/uhabits/activities/common/views/HistoryChart.java index 620a2d59f..9dad8020e 100644 --- a/app/src/main/java/org/isoron/uhabits/activities/common/views/HistoryChart.java +++ b/app/src/main/java/org/isoron/uhabits/activities/common/views/HistoryChart.java @@ -38,6 +38,8 @@ public class HistoryChart extends ScrollableChart { private int[] checkmarks; + private int target; + private Paint pSquareBg, pSquareFg, pTextHeader; private float squareSpacing; @@ -85,6 +87,8 @@ public class HistoryChart extends ScrollableChart private float headerOverflow = 0; + private boolean isNumerical = false; + @NonNull private Controller controller; @@ -168,6 +172,11 @@ public class HistoryChart extends ScrollableChart this.controller = controller; } + public void setNumerical(boolean numerical) + { + isNumerical = numerical; + } + public void setIsBackgroundTransparent(boolean isBackgroundTransparent) { this.isBackgroundTransparent = isBackgroundTransparent; @@ -179,6 +188,12 @@ public class HistoryChart extends ScrollableChart this.isEditable = isEditable; } + public void setTarget(int target) + { + this.target = target; + postInvalidate(); + } + protected void initPaints() { pTextHeader = new Paint(); @@ -323,7 +338,16 @@ public class HistoryChart extends ScrollableChart int checkmarkOffset) { if (checkmarkOffset >= checkmarks.length) pSquareBg.setColor(colors[0]); - else pSquareBg.setColor(colors[checkmarks[checkmarkOffset]]); + else + { + int checkmark = checkmarks[checkmarkOffset]; + if(checkmark == 0) pSquareBg.setColor(colors[0]); + else if(checkmark < target) + { + pSquareBg.setColor(isNumerical ? textColor : colors[1]); + } + else pSquareBg.setColor(colors[2]); + } pSquareFg.setColor(reverseTextColor); canvas.drawRect(location, pSquareBg); @@ -347,6 +371,7 @@ public class HistoryChart extends ScrollableChart isEditable = false; checkmarks = new int[0]; controller = new Controller() {}; + target = 2; initColors(); initPaints(); diff --git a/app/src/main/java/org/isoron/uhabits/activities/common/views/ScoreChart.java b/app/src/main/java/org/isoron/uhabits/activities/common/views/ScoreChart.java index 902959684..fe6c38b41 100644 --- a/app/src/main/java/org/isoron/uhabits/activities/common/views/ScoreChart.java +++ b/app/src/main/java/org/isoron/uhabits/activities/common/views/ScoreChart.java @@ -108,15 +108,15 @@ public class ScoreChart extends ScrollableChart Random random = new Random(); scores = new LinkedList<>(); - int previous = Score.MAX_VALUE / 2; + double previous = 0.5f; long timestamp = DateUtils.getStartOfToday(); long day = DateUtils.millisecondsInOneDay; for (int i = 1; i < 100; i++) { - int step = Score.MAX_VALUE / 10; - int current = previous + random.nextInt(step * 2) - step; - current = Math.max(0, Math.min(Score.MAX_VALUE, current)); + double step = 0.1f; + double current = previous + random.nextDouble() * step * 2 - step; + current = Math.max(0, Math.min(1.0f, current)); scores.add(new Score(timestamp, current)); previous = current; timestamp -= day; @@ -187,11 +187,10 @@ public class ScoreChart extends ScrollableChart int offset = nColumns - k - 1 + getDataOffset(); if (offset >= scores.size()) continue; - int score = scores.get(offset).getValue(); + double score = scores.get(offset).getValue(); long timestamp = scores.get(offset).getTimestamp(); - double relativeScore = ((double) score) / Score.MAX_VALUE; - int height = (int) (columnHeight * relativeScore); + int height = (int) (columnHeight * score); rect.set(0, 0, baseSize, baseSize); rect.offset(k * columnWidth + (columnWidth - baseSize) / 2, diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/edit/BaseDialog.java b/app/src/main/java/org/isoron/uhabits/activities/habits/edit/BaseDialog.java deleted file mode 100644 index b4c228876..000000000 --- a/app/src/main/java/org/isoron/uhabits/activities/habits/edit/BaseDialog.java +++ /dev/null @@ -1,263 +0,0 @@ -/* - * Copyright (C) 2016 Álinson Santos Xavier - * - * 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 . - */ - -package org.isoron.uhabits.activities.habits.edit; - -import android.os.*; -import android.support.annotation.*; -import android.support.v7.app.*; -import android.text.format.*; -import android.view.*; - -import com.android.datetimepicker.time.*; - -import org.isoron.uhabits.*; -import org.isoron.uhabits.R; -import org.isoron.uhabits.activities.*; -import org.isoron.uhabits.activities.common.dialogs.*; -import org.isoron.uhabits.commands.*; -import org.isoron.uhabits.models.*; -import org.isoron.uhabits.preferences.*; - -import java.util.*; - -import butterknife.*; - -public abstract class BaseDialog extends AppCompatDialogFragment -{ - @Nullable - protected Habit originalHabit; - - @Nullable - protected Habit modifiedHabit; - - @Nullable - protected BaseDialogHelper helper; - - protected Preferences prefs; - - protected CommandRunner commandRunner; - - protected HabitList habitList; - - protected AppComponent appComponent; - - protected ModelFactory modelFactory; - - private ColorPickerDialogFactory colorPickerDialogFactory; - - @Override - public void onActivityCreated(Bundle savedInstanceState) - { - super.onActivityCreated(savedInstanceState); - - BaseActivity activity = (BaseActivity) getActivity(); - colorPickerDialogFactory = - activity.getComponent().getColorPickerDialogFactory(); - } - - @Override - public View onCreateView(LayoutInflater inflater, - ViewGroup container, - Bundle savedInstanceState) - { - View view = inflater.inflate(R.layout.edit_habit, container, false); - - HabitsApplication app = - (HabitsApplication) getContext().getApplicationContext(); - - appComponent = app.getComponent(); - prefs = appComponent.getPreferences(); - habitList = appComponent.getHabitList(); - commandRunner = appComponent.getCommandRunner(); - modelFactory = appComponent.getModelFactory(); - - ButterKnife.bind(this, view); - - helper = new BaseDialogHelper(this, view); - getDialog().setTitle(getTitle()); - initializeHabits(); - restoreSavedInstance(savedInstanceState); - helper.populateForm(modifiedHabit); - return view; - } - - @OnItemSelected(R.id.sFrequency) - public void onFrequencySelected(int position) - { - if (position < 0 || position > 4) throw new IllegalArgumentException(); - int freqNums[] = { 1, 1, 2, 5, 3 }; - int freqDens[] = { 1, 7, 7, 7, 7 }; - modifiedHabit.setFrequency( - new Frequency(freqNums[position], freqDens[position])); - helper.populateFrequencyFields(modifiedHabit); - } - - @Override - @SuppressWarnings("ConstantConditions") - public void onSaveInstanceState(Bundle outState) - { - super.onSaveInstanceState(outState); - outState.putInt("color", modifiedHabit.getColor()); - if (modifiedHabit.hasReminder()) - { - Reminder reminder = modifiedHabit.getReminder(); - outState.putInt("reminderMin", reminder.getMinute()); - outState.putInt("reminderHour", reminder.getHour()); - outState.putInt("reminderDays", reminder.getDays().toInteger()); - } - } - - protected abstract int getTitle(); - - protected abstract void initializeHabits(); - - protected void restoreSavedInstance(@Nullable Bundle bundle) - { - if (bundle == null) return; - modifiedHabit.setColor( - bundle.getInt("color", modifiedHabit.getColor())); - - modifiedHabit.setReminder(null); - - int hour = (bundle.getInt("reminderHour", -1)); - int minute = (bundle.getInt("reminderMin", -1)); - int days = (bundle.getInt("reminderDays", -1)); - - if (hour >= 0 && minute >= 0) - { - Reminder reminder = - new Reminder(hour, minute, new WeekdayList(days)); - modifiedHabit.setReminder(reminder); - } - } - - protected abstract void saveHabit(); - - @OnClick(R.id.buttonDiscard) - void onButtonDiscardClick() - { - dismiss(); - } - - @OnClick(R.id.tvReminderTime) - @SuppressWarnings("ConstantConditions") - void onDateSpinnerClick() - { - int defaultHour = 8; - int defaultMin = 0; - - if (modifiedHabit.hasReminder()) - { - Reminder reminder = modifiedHabit.getReminder(); - defaultHour = reminder.getHour(); - defaultMin = reminder.getMinute(); - } - - showTimePicker(defaultHour, defaultMin); - } - - @OnClick(R.id.buttonSave) - void onSaveButtonClick() - { - helper.parseFormIntoHabit(modifiedHabit); - if (!helper.validate(modifiedHabit)) return; - saveHabit(); - dismiss(); - } - - @OnClick(R.id.tvReminderDays) - @SuppressWarnings("ConstantConditions") - void onWeekdayClick() - { - if (!modifiedHabit.hasReminder()) return; - Reminder reminder = modifiedHabit.getReminder(); - - WeekdayPickerDialog dialog = new WeekdayPickerDialog(); - dialog.setListener(new OnWeekdaysPickedListener()); - dialog.setSelectedDays(reminder.getDays().toArray()); - dialog.show(getFragmentManager(), "weekdayPicker"); - } - - @OnClick(R.id.buttonPickColor) - void showColorPicker() - { - int color = modifiedHabit.getColor(); - ColorPickerDialog picker = colorPickerDialogFactory.create(color); - - picker.setListener(c -> { - prefs.setDefaultHabitColor(c); - modifiedHabit.setColor(c); - helper.populateColor(c); - }); - - picker.show(getFragmentManager(), "picker"); - } - - private void showTimePicker(int defaultHour, int defaultMin) - { - boolean is24HourMode = DateFormat.is24HourFormat(getContext()); - TimePickerDialog timePicker = - TimePickerDialog.newInstance(new OnTimeSetListener(), defaultHour, - defaultMin, is24HourMode); - timePicker.show(getFragmentManager(), "timePicker"); - } - - private class OnTimeSetListener - implements TimePickerDialog.OnTimeSetListener - { - @Override - public void onTimeCleared(RadialPickerLayout view) - { - modifiedHabit.clearReminder(); - helper.populateReminderFields(modifiedHabit); - } - - @Override - public void onTimeSet(RadialPickerLayout view, int hour, int minute) - { - Reminder reminder = - new Reminder(hour, minute, WeekdayList.EVERY_DAY); - modifiedHabit.setReminder(reminder); - helper.populateReminderFields(modifiedHabit); - } - } - - private class OnWeekdaysPickedListener - implements WeekdayPickerDialog.OnWeekdaysPickedListener - { - @Override - public void onWeekdaysPicked(boolean[] selectedDays) - { - if (isSelectionEmpty(selectedDays)) Arrays.fill(selectedDays, true); - - Reminder oldReminder = modifiedHabit.getReminder(); - modifiedHabit.setReminder( - new Reminder(oldReminder.getHour(), oldReminder.getMinute(), - new WeekdayList(selectedDays))); - helper.populateReminderFields(modifiedHabit); - } - - private boolean isSelectionEmpty(boolean[] selectedDays) - { - for (boolean d : selectedDays) if (d) return false; - return true; - } - } -} diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/edit/BaseDialogHelper.java b/app/src/main/java/org/isoron/uhabits/activities/habits/edit/BaseDialogHelper.java deleted file mode 100644 index 225b509ff..000000000 --- a/app/src/main/java/org/isoron/uhabits/activities/habits/edit/BaseDialogHelper.java +++ /dev/null @@ -1,195 +0,0 @@ -/* - * Copyright (C) 2016 Álinson Santos Xavier - * - * 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 . - */ - -package org.isoron.uhabits.activities.habits.edit; - -import android.annotation.*; -import android.support.v4.app.*; -import android.view.*; -import android.widget.*; - -import org.isoron.uhabits.R; -import org.isoron.uhabits.models.*; -import org.isoron.uhabits.utils.*; - -import butterknife.*; - -public class BaseDialogHelper -{ - private DialogFragment frag; - - @BindView(R.id.tvName) - TextView tvName; - - @BindView(R.id.tvDescription) - TextView tvDescription; - - @BindView(R.id.tvFreqNum) - TextView tvFreqNum; - - @BindView(R.id.tvFreqDen) - TextView tvFreqDen; - - @BindView(R.id.tvReminderTime) - TextView tvReminderTime; - - @BindView(R.id.tvReminderDays) - TextView tvReminderDays; - - @BindView(R.id.sFrequency) - Spinner sFrequency; - - @BindView(R.id.llCustomFrequency) - ViewGroup llCustomFrequency; - - @BindView(R.id.llReminderDays) - ViewGroup llReminderDays; - - public BaseDialogHelper(DialogFragment frag, View view) - { - this.frag = frag; - ButterKnife.bind(this, view); - } - - protected void populateForm(final Habit habit) - { - if (habit.getName() != null) tvName.setText(habit.getName()); - if (habit.getDescription() != null) - tvDescription.setText(habit.getDescription()); - - populateColor(habit.getColor()); - populateFrequencyFields(habit); - populateReminderFields(habit); - } - - void parseFormIntoHabit(Habit habit) - { - habit.setName(tvName.getText().toString().trim()); - habit.setDescription(tvDescription.getText().toString().trim()); - String freqNum = tvFreqNum.getText().toString(); - String freqDen = tvFreqDen.getText().toString(); - if (!freqNum.isEmpty() && !freqDen.isEmpty()) - { - int numerator = Integer.parseInt(freqNum); - int denominator = Integer.parseInt(freqDen); - habit.setFrequency(new Frequency(numerator, denominator)); - } - } - - void populateColor(int paletteColor) - { - tvName.setTextColor( - ColorUtils.getColor(frag.getContext(), paletteColor)); - } - - @SuppressLint("SetTextI18n") - void populateFrequencyFields(Habit habit) - { - int quickSelectPosition = -1; - - Frequency freq = habit.getFrequency(); - - if (freq.equals(Frequency.DAILY)) - quickSelectPosition = 0; - - else if (freq.equals(Frequency.WEEKLY)) - quickSelectPosition = 1; - - else if (freq.equals(Frequency.TWO_TIMES_PER_WEEK)) - quickSelectPosition = 2; - - else if (freq.equals(Frequency.FIVE_TIMES_PER_WEEK)) - quickSelectPosition = 3; - - if (quickSelectPosition >= 0) - showSimplifiedFrequency(quickSelectPosition); - - else showCustomFrequency(); - - tvFreqNum.setText(Integer.toString(freq.getNumerator())); - tvFreqDen.setText(Integer.toString(freq.getDenominator())); - } - - @SuppressWarnings("ConstantConditions") - void populateReminderFields(Habit habit) - { - if (!habit.hasReminder()) - { - tvReminderTime.setText(R.string.reminder_off); - llReminderDays.setVisibility(View.GONE); - return; - } - - Reminder reminder = habit.getReminder(); - - String time = - DateUtils.formatTime(frag.getContext(), reminder.getHour(), - reminder.getMinute()); - tvReminderTime.setText(time); - llReminderDays.setVisibility(View.VISIBLE); - - boolean weekdays[] = reminder.getDays().toArray(); - tvReminderDays.setText( - DateUtils.formatWeekdayList(frag.getContext(), weekdays)); - } - - private void showCustomFrequency() - { - sFrequency.setVisibility(View.GONE); - llCustomFrequency.setVisibility(View.VISIBLE); - } - - @SuppressLint("SetTextI18n") - private void showSimplifiedFrequency(int quickSelectPosition) - { - sFrequency.setVisibility(View.VISIBLE); - sFrequency.setSelection(quickSelectPosition); - llCustomFrequency.setVisibility(View.GONE); - } - - boolean validate(Habit habit) - { - Boolean valid = true; - - if (habit.getName().length() == 0) - { - tvName.setError( - frag.getString(R.string.validation_name_should_not_be_blank)); - valid = false; - } - - Frequency freq = habit.getFrequency(); - - if (freq.getNumerator() <= 0) - { - tvFreqNum.setError( - frag.getString(R.string.validation_number_should_be_positive)); - valid = false; - } - - if (freq.getNumerator() > freq.getDenominator()) - { - tvFreqNum.setError( - frag.getString(R.string.validation_at_most_one_rep_per_day)); - valid = false; - } - - return valid; - } -} diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/edit/CreateHabitDialog.java b/app/src/main/java/org/isoron/uhabits/activities/habits/edit/CreateHabitDialog.java deleted file mode 100644 index 5f34e2e40..000000000 --- a/app/src/main/java/org/isoron/uhabits/activities/habits/edit/CreateHabitDialog.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright (C) 2016 Álinson Santos Xavier - * - * 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 . - */ - -package org.isoron.uhabits.activities.habits.edit; - -import com.google.auto.factory.*; - -import org.isoron.uhabits.*; -import org.isoron.uhabits.commands.*; -import org.isoron.uhabits.models.*; - -@AutoFactory(allowSubclasses = true) -public class CreateHabitDialog extends BaseDialog -{ - @Override - protected int getTitle() - { - return R.string.create_habit; - } - - @Override - protected void initializeHabits() - { - modifiedHabit = modelFactory.buildHabit(); - modifiedHabit.setFrequency(Frequency.DAILY); - modifiedHabit.setColor( - prefs.getDefaultHabitColor(modifiedHabit.getColor())); - } - - @Override - protected void saveHabit() - { - Command command = appComponent - .getCreateHabitCommandFactory() - .create(habitList, modifiedHabit); - commandRunner.execute(command, null); - } -} diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/edit/EditHabitDialog.java b/app/src/main/java/org/isoron/uhabits/activities/habits/edit/EditHabitDialog.java index e9c1aca78..b3ed6bf3f 100644 --- a/app/src/main/java/org/isoron/uhabits/activities/habits/edit/EditHabitDialog.java +++ b/app/src/main/java/org/isoron/uhabits/activities/habits/edit/EditHabitDialog.java @@ -19,34 +19,242 @@ package org.isoron.uhabits.activities.habits.edit; +import android.content.*; +import android.os.*; +import android.support.annotation.*; +import android.support.v7.app.*; +import android.text.format.*; +import android.view.*; + +import com.android.datetimepicker.time.*; + import org.isoron.uhabits.*; +import org.isoron.uhabits.R; +import org.isoron.uhabits.activities.*; +import org.isoron.uhabits.activities.common.dialogs.*; +import org.isoron.uhabits.activities.habits.edit.views.*; import org.isoron.uhabits.commands.*; +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.preferences.*; + +import butterknife.*; -public class EditHabitDialog extends BaseDialog +import static android.view.View.*; + +public class EditHabitDialog extends AppCompatDialogFragment { + public static final String BUNDLE_HABIT_ID = "habitId"; + + public static final String BUNDLE_HABIT_TYPE = "habitType"; + + protected Habit originalHabit; + + protected Preferences prefs; + + protected CommandRunner commandRunner; + + protected HabitList habitList; + + protected AppComponent component; + + protected ModelFactory modelFactory; + + @BindView(R.id.namePanel) + NameDescriptionPanel namePanel; + + @BindView(R.id.reminderPanel) + ReminderPanel reminderPanel; + + @BindView(R.id.frequencyPanel) + FrequencyPanel frequencyPanel; + + @BindView(R.id.targetPanel) + TargetPanel targetPanel; + + private ColorPickerDialogFactory colorPickerDialogFactory; + @Override - protected int getTitle() + public int getTheme() { - return R.string.edit_habit; + return R.style.DialogWithTitle; } @Override - protected void initializeHabits() + public void onActivityCreated(Bundle savedInstanceState) { - Long habitId = (Long) getArguments().get("habitId"); - if (habitId == null) - throw new IllegalArgumentException("habitId must be specified"); + super.onActivityCreated(savedInstanceState); - originalHabit = habitList.getById(habitId); - modifiedHabit = modelFactory.buildHabit(); - modifiedHabit.copyFrom(originalHabit); + BaseActivity activity = (BaseActivity) getActivity(); + colorPickerDialogFactory = + activity.getComponent().getColorPickerDialogFactory(); } @Override - protected void saveHabit() + public View onCreateView(LayoutInflater inflater, + ViewGroup container, + Bundle savedInstanceState) + { + View view; + view = inflater.inflate(R.layout.edit_habit, container, false); + + initDependencies(); + ButterKnife.bind(this, view); + + getDialog().setTitle(getTitle()); + originalHabit = parseHabitFromArguments(); + + populateForm(); + setupReminderController(); + setupNameController(); + + return view; + } + + protected int getTitle() + { + if (originalHabit == null) return R.string.edit_habit; + else return R.string.create_habit; + } + + protected void saveHabit(@NonNull Habit habit) + { + if (originalHabit == null) + { + commandRunner.execute(component + .getCreateHabitCommandFactory() + .create(habitList, habit), null); + } + else + { + commandRunner.execute(component.getEditHabitCommandFactory(). + create(habitList, originalHabit, habit), originalHabit.getId()); + } + } + + private int getTypeFromArguments() + { + return getArguments().getInt(BUNDLE_HABIT_TYPE); + } + + private void initDependencies() + { + Context appContext = getContext().getApplicationContext(); + HabitsApplication app = (HabitsApplication) appContext; + + component = app.getComponent(); + prefs = component.getPreferences(); + habitList = component.getHabitList(); + commandRunner = component.getCommandRunner(); + modelFactory = component.getModelFactory(); + } + + @OnClick(R.id.buttonDiscard) + void onButtonDiscardClick() + { + dismiss(); + } + + @OnClick(R.id.buttonSave) + void onSaveButtonClick() { - Command command = appComponent.getEditHabitCommandFactory(). - create(habitList, originalHabit, modifiedHabit); - commandRunner.execute(command, originalHabit.getId()); + int type = getTypeFromArguments(); + + if (!namePanel.validate()) return; + if (type == Habit.YES_NO_HABIT && !frequencyPanel.validate()) return; + if (type == Habit.NUMBER_HABIT && !targetPanel.validate()) return; + + Habit habit = modelFactory.buildHabit(); + habit.setName(namePanel.getName()); + habit.setDescription(namePanel.getDescription()); + habit.setColor(namePanel.getColor()); + habit.setReminder(reminderPanel.getReminder()); + habit.setFrequency(frequencyPanel.getFrequency()); + habit.setUnit(targetPanel.getUnit()); + habit.setTargetValue(targetPanel.getTargetValue()); + habit.setType(type); + + saveHabit(habit); + dismiss(); + } + + @Nullable + private Habit parseHabitFromArguments() + { + Bundle arguments = getArguments(); + if (arguments == null) return null; + + Long id = (Long) arguments.get(BUNDLE_HABIT_ID); + if (id == null) return null; + + Habit habit = habitList.getById(id); + if (habit == null) throw new IllegalStateException(); + + return habit; + } + + private void populateForm() + { + Habit habit = modelFactory.buildHabit(); + habit.setFrequency(Frequency.DAILY); + habit.setColor(prefs.getDefaultHabitColor(habit.getColor())); + habit.setType(getTypeFromArguments()); + + if (originalHabit != null) habit.copyFrom(originalHabit); + + if (habit.isNumerical()) frequencyPanel.setVisibility(GONE); + else targetPanel.setVisibility(GONE); + + namePanel.populateFrom(habit); + frequencyPanel.setFrequency(habit.getFrequency()); + targetPanel.setTargetValue(habit.getTargetValue()); + targetPanel.setUnit(habit.getUnit()); + if (habit.hasReminder()) reminderPanel.setReminder(habit.getReminder()); + } + + private void setupNameController() + { + namePanel.setController(new NameDescriptionPanel.Controller() + { + @Override + public void onColorPickerClicked(int previousColor) + { + ColorPickerDialog picker = + colorPickerDialogFactory.create(previousColor); + + picker.setListener(c -> + { + prefs.setDefaultHabitColor(c); + namePanel.setColor(c); + }); + + picker.show(getFragmentManager(), "picker"); + } + }); + } + + private void setupReminderController() + { + reminderPanel.setController(new ReminderPanel.Controller() + { + @Override + public void onTimeClicked(int currentHour, int currentMin) + { + TimePickerDialog timePicker; + boolean is24HourMode = DateFormat.is24HourFormat(getContext()); + timePicker = + TimePickerDialog.newInstance(reminderPanel, currentHour, + currentMin, is24HourMode); + timePicker.show(getFragmentManager(), "timePicker"); + } + + @Override + public void onWeekdayClicked(WeekdayList currentDays) + { + WeekdayPickerDialog dialog = new WeekdayPickerDialog(); + dialog.setListener(reminderPanel); + dialog.setSelectedDays(currentDays); + dialog.show(getFragmentManager(), "weekdayPicker"); + } + }); } } diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/edit/EditHabitDialogFactory.java b/app/src/main/java/org/isoron/uhabits/activities/habits/edit/EditHabitDialogFactory.java index 481658ebf..7ca3ef391 100644 --- a/app/src/main/java/org/isoron/uhabits/activities/habits/edit/EditHabitDialogFactory.java +++ b/app/src/main/java/org/isoron/uhabits/activities/habits/edit/EditHabitDialogFactory.java @@ -26,6 +26,8 @@ import org.isoron.uhabits.models.*; import javax.inject.*; +import static org.isoron.uhabits.activities.habits.edit.EditHabitDialog.*; + public class EditHabitDialogFactory { @Inject @@ -33,14 +35,33 @@ public class EditHabitDialogFactory { } - public EditHabitDialog create(@NonNull Habit habit) + public EditHabitDialog createBoolean() + { + EditHabitDialog dialog = new EditHabitDialog(); + Bundle args = new Bundle(); + args.putInt(BUNDLE_HABIT_TYPE, Habit.YES_NO_HABIT); + dialog.setArguments(args); + return dialog; + } + + public EditHabitDialog createNumerical() + { + EditHabitDialog dialog = new EditHabitDialog(); + Bundle args = new Bundle(); + args.putInt(BUNDLE_HABIT_TYPE, Habit.NUMBER_HABIT); + dialog.setArguments(args); + return dialog; + } + + public EditHabitDialog edit(@NonNull Habit habit) { if (habit.getId() == null) throw new IllegalArgumentException("habit not saved"); EditHabitDialog dialog = new EditHabitDialog(); Bundle args = new Bundle(); - args.putLong("habitId", habit.getId()); + args.putLong(BUNDLE_HABIT_ID, habit.getId()); + args.putInt(BUNDLE_HABIT_TYPE, habit.getType()); dialog.setArguments(args); return dialog; } diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/edit/views/ExampleEditText.java b/app/src/main/java/org/isoron/uhabits/activities/habits/edit/views/ExampleEditText.java new file mode 100644 index 000000000..977d746ab --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/activities/habits/edit/views/ExampleEditText.java @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2016 Álinson Santos Xavier + * + * 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 . + */ + +package org.isoron.uhabits.activities.habits.edit.views; + +import android.content.*; +import android.support.annotation.*; +import android.text.*; +import android.util.*; +import android.view.*; +import android.widget.*; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.utils.*; + +import static org.isoron.uhabits.utils.AttributeSetUtils.*; + +/** + * An EditText that shows an example usage when there is no text + * currently set. The example disappears when the widget gains focus. + */ +public class ExampleEditText extends EditText + implements View.OnFocusChangeListener +{ + + private String example; + + private String realText; + + private int color; + + private int exampleColor; + + private int inputType; + + public ExampleEditText(Context context, @Nullable AttributeSet attrs) + { + super(context, attrs); + + if (attrs != null) + example = getAttribute(context, attrs, "example", ""); + + inputType = getInputType(); + realText = getText().toString(); + color = getCurrentTextColor(); + init(); + } + + public String getRealText() + { + if(hasFocus()) return getText().toString(); + else return realText; + } + + @Override + public void onFocusChange(View v, boolean hasFocus) + { + if (!hasFocus) realText = getText().toString(); + updateText(); + } + + public void setExample(String example) + { + this.example = example; + updateText(); + } + + public void setRealText(String realText) + { + this.realText = realText; + updateText(); + } + + private void init() + { + StyledResources sr = new StyledResources(getContext()); + exampleColor = sr.getColor(R.attr.mediumContrastTextColor); + setOnFocusChangeListener(this); + updateText(); + } + + private void updateText() + { + if (realText.isEmpty() && !isFocused()) + { + setTextColor(exampleColor); + setText(example); + setInputType(InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS); + } + else + { + setText(realText); + setTextColor(color); + setInputType(inputType); + } + + } +} diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/edit/views/FrequencyPanel.java b/app/src/main/java/org/isoron/uhabits/activities/habits/edit/views/FrequencyPanel.java new file mode 100644 index 000000000..cb4f86d56 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/activities/habits/edit/views/FrequencyPanel.java @@ -0,0 +1,164 @@ +/* + * Copyright (C) 2016 Álinson Santos Xavier + * + * 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 . + */ + +package org.isoron.uhabits.activities.habits.edit.views; + +import android.annotation.*; +import android.content.*; +import android.content.res.*; +import android.support.annotation.*; +import android.util.*; +import android.view.*; +import android.widget.*; + +import org.isoron.uhabits.R; +import org.isoron.uhabits.models.*; + +import butterknife.*; + +import static org.isoron.uhabits.R.id.*; + + +public class FrequencyPanel extends FrameLayout +{ + @BindView(numerator) + TextView tvNumerator; + + @BindView(R.id.denominator) + TextView tvDenominator; + + @BindView(R.id.spinner) + Spinner spinner; + + @BindView(R.id.customFreqPanel) + ViewGroup customFreqPanel; + + public FrequencyPanel(@NonNull Context context, + @Nullable AttributeSet attrs) + { + super(context, attrs); + + View view = inflate(context, R.layout.edit_habit_frequency, null); + ButterKnife.bind(this, view); + addView(view); + } + + @NonNull + public Frequency getFrequency() + { + String freqNum = tvNumerator.getText().toString(); + String freqDen = tvDenominator.getText().toString(); + + if (!freqNum.isEmpty() && !freqDen.isEmpty()) + { + int numerator = Integer.parseInt(freqNum); + int denominator = Integer.parseInt(freqDen); + return new Frequency(numerator, denominator); + } + + return Frequency.DAILY; + } + + @SuppressLint("SetTextI18n") + public void setFrequency(@NonNull Frequency freq) + { + int position = getQuickSelectPosition(freq); + + if (position >= 0) showSimplifiedFrequency(position); + else showCustomFrequency(); + + tvNumerator.setText(Integer.toString(freq.getNumerator())); + tvDenominator.setText(Integer.toString(freq.getDenominator())); + } + + @OnItemSelected(R.id.spinner) + public void onFrequencySelected(int position) + { + if (position < 0 || position > 4) throw new IllegalArgumentException(); + int freqNums[] = { 1, 1, 2, 5, 3 }; + int freqDens[] = { 1, 7, 7, 7, 7 }; + setFrequency(new Frequency(freqNums[position], freqDens[position])); + } + + public boolean validate() + { + boolean valid = true; + Resources res = getResources(); + + String freqNum = tvNumerator.getText().toString(); + String freqDen = tvDenominator.getText().toString(); + + if (freqDen.isEmpty()) + { + tvDenominator.setError( + res.getString(R.string.validation_show_not_be_blank)); + valid = false; + } + + if (freqNum.isEmpty()) + { + tvNumerator.setError( + res.getString(R.string.validation_show_not_be_blank)); + valid = false; + } + + if (!valid) return false; + + int numerator = Integer.parseInt(freqNum); + int denominator = Integer.parseInt(freqDen); + + if (numerator <= 0) + { + tvNumerator.setError( + res.getString(R.string.validation_number_should_be_positive)); + valid = false; + } + + if (numerator > denominator) + { + tvNumerator.setError( + res.getString(R.string.validation_at_most_one_rep_per_day)); + valid = false; + } + + return valid; + } + + private int getQuickSelectPosition(@NonNull Frequency freq) + { + if (freq.equals(Frequency.DAILY)) return 0; + if (freq.equals(Frequency.WEEKLY)) return 1; + if (freq.equals(Frequency.TWO_TIMES_PER_WEEK)) return 2; + if (freq.equals(Frequency.FIVE_TIMES_PER_WEEK)) return 3; + return -1; + } + + private void showCustomFrequency() + { + spinner.setVisibility(View.GONE); + customFreqPanel.setVisibility(View.VISIBLE); + } + + private void showSimplifiedFrequency(int quickSelectPosition) + { + spinner.setVisibility(View.VISIBLE); + spinner.setSelection(quickSelectPosition); + customFreqPanel.setVisibility(View.GONE); + } +} diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/edit/views/NameDescriptionPanel.java b/app/src/main/java/org/isoron/uhabits/activities/habits/edit/views/NameDescriptionPanel.java new file mode 100644 index 000000000..5c07aab0f --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/activities/habits/edit/views/NameDescriptionPanel.java @@ -0,0 +1,152 @@ +/* + * Copyright (C) 2016 Álinson Santos Xavier + * + * 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 . + */ + +package org.isoron.uhabits.activities.habits.edit.views; + +import android.content.*; +import android.content.res.*; +import android.os.*; +import android.support.annotation.*; +import android.util.*; +import android.view.*; +import android.widget.*; + +import org.isoron.uhabits.R; +import org.isoron.uhabits.activities.common.views.*; +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.utils.*; + +import butterknife.*; + + +public class NameDescriptionPanel extends FrameLayout +{ + @BindView(R.id.tvName) + EditText tvName; + + @BindView(R.id.tvDescription) + ExampleEditText tvDescription; + + private int color; + + @NonNull + private Controller controller; + + public NameDescriptionPanel(@NonNull Context context, + @Nullable AttributeSet attrs) + { + super(context, attrs); + + View view = inflate(context, R.layout.edit_habit_name, null); + ButterKnife.bind(this, view); + addView(view); + + controller = new Controller() {}; + } + + public int getColor() + { + return color; + } + + public void setColor(int color) + { + this.color = color; + tvName.setTextColor(ColorUtils.getColor(getContext(), color)); + } + + @NonNull + public String getDescription() + { + return tvDescription.getRealText().trim(); + } + + @NonNull + public String getName() + { + return tvName.getText().toString().trim(); + } + + public void populateFrom(@NonNull Habit habit) + { + Resources res = getResources(); + + if(habit.isNumerical()) + tvDescription.setExample(res.getString(R.string.example_question_numerical)); + else + tvDescription.setExample(res.getString(R.string.example_question_boolean)); + + setColor(habit.getColor()); + tvName.setText(habit.getName()); + tvDescription.setRealText(habit.getDescription()); + } + + public boolean validate() + { + Resources res = getResources(); + + if (getName().isEmpty()) + { + tvName.setError( + res.getString(R.string.validation_name_should_not_be_blank)); + return false; + } + + return true; + } + + @Override + protected void onRestoreInstanceState(Parcelable state) + { + BundleSavedState bss = (BundleSavedState) state; + setColor(bss.bundle.getInt("color")); + super.onRestoreInstanceState(bss.getSuperState()); + } + + @Override + protected Parcelable onSaveInstanceState() + { + Parcelable superState = super.onSaveInstanceState(); + Bundle bundle = new Bundle(); + bundle.putInt("color", color); + return new BundleSavedState(superState, bundle); + } + + @OnClick(R.id.buttonPickColor) + void showColorPicker() + { + controller.onColorPickerClicked(color); + } + + public void setController(@NonNull Controller controller) + { + this.controller = controller; + } + + public interface Controller + { + /** + * Called when the user has clicked the widget to select a new + * color for the habit. + * + * @param previousColor the color previously selected + */ + default void onColorPickerClicked(int previousColor) {} + } +} diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/edit/views/ReminderPanel.java b/app/src/main/java/org/isoron/uhabits/activities/habits/edit/views/ReminderPanel.java new file mode 100644 index 000000000..2c5fd5687 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/activities/habits/edit/views/ReminderPanel.java @@ -0,0 +1,194 @@ +/* + * Copyright (C) 2016 Álinson Santos Xavier + * + * 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 . + */ + +package org.isoron.uhabits.activities.habits.edit.views; + +import android.content.*; +import android.os.*; +import android.support.annotation.*; +import android.util.*; +import android.view.*; +import android.widget.*; + +import com.android.datetimepicker.time.*; + +import org.isoron.uhabits.R; +import org.isoron.uhabits.activities.common.dialogs.*; +import org.isoron.uhabits.activities.common.views.*; +import org.isoron.uhabits.models.*; + +import butterknife.*; + +import static org.isoron.uhabits.utils.DateUtils.*; + +public class ReminderPanel extends FrameLayout + implements TimePickerDialog.OnTimeSetListener, + WeekdayPickerDialog.OnWeekdaysPickedListener +{ + @BindView(R.id.tvReminderTime) + TextView tvReminderTime; + + @BindView(R.id.llReminderDays) + ViewGroup llReminderDays; + + @BindView(R.id.tvReminderDays) + TextView tvReminderDays; + + @Nullable + private Reminder reminder; + + @NonNull + private Controller controller; + + public ReminderPanel(@NonNull Context context, @Nullable AttributeSet attrs) + { + super(context, attrs); + + View view = inflate(context, R.layout.edit_habit_reminder, null); + ButterKnife.bind(this, view); + addView(view); + + controller = new Controller() {}; + setReminder(null); + } + + @Nullable + public Reminder getReminder() + { + return reminder; + } + + public void setReminder(@Nullable Reminder reminder) + { + this.reminder = reminder; + + if (reminder == null) + { + tvReminderTime.setText(R.string.reminder_off); + llReminderDays.setVisibility(View.GONE); + return; + } + + Context ctx = getContext(); + String time = formatTime(ctx, reminder.getHour(), reminder.getMinute()); + tvReminderTime.setText(time); + llReminderDays.setVisibility(View.VISIBLE); + + boolean weekdays[] = reminder.getDays().toArray(); + tvReminderDays.setText(formatWeekdayList(ctx, weekdays)); + } + + @Override + public void onTimeCleared(RadialPickerLayout view) + { + setReminder(null); + } + + @Override + public void onTimeSet(RadialPickerLayout view, int hour, int minute) + { + setReminder(new Reminder(hour, minute, WeekdayList.EVERY_DAY)); + } + + @Override + public void onWeekdaysSet(WeekdayList selectedDays) + { + if (reminder == null) return; + if (selectedDays.isEmpty()) selectedDays = WeekdayList.EVERY_DAY; + + setReminder(new Reminder(reminder.getHour(), reminder.getMinute(), + selectedDays)); + } + + public void setController(@NonNull Controller controller) + { + this.controller = controller; + } + + @Override + protected void onRestoreInstanceState(Parcelable state) + { + BundleSavedState bss = (BundleSavedState) state; + if (!bss.bundle.isEmpty()) + { + int days = bss.bundle.getInt("days"); + int hour = bss.bundle.getInt("hour"); + int minute = bss.bundle.getInt("minute"); + reminder = new Reminder(hour, minute, new WeekdayList(days)); + setReminder(reminder); + } + super.onRestoreInstanceState(bss.getSuperState()); + } + + @Override + protected Parcelable onSaveInstanceState() + { + Parcelable superState = super.onSaveInstanceState(); + Bundle bundle = new Bundle(); + if (reminder != null) + { + bundle.putInt("days", reminder.getDays().toInteger()); + bundle.putInt("hour", reminder.getHour()); + bundle.putInt("minute", reminder.getMinute()); + } + return new BundleSavedState(superState, bundle); + } + + @OnClick(R.id.tvReminderTime) + void onDateSpinnerClick() + { + int hour = 8; + int min = 0; + + if (reminder != null) + { + hour = reminder.getHour(); + min = reminder.getMinute(); + } + + controller.onTimeClicked(hour, min); + } + + @OnClick(R.id.tvReminderDays) + void onWeekdayClicked() + { + if (reminder == null) return; + controller.onWeekdayClicked(reminder.getDays()); + } + + public interface Controller + { + /** + * Called when the user has clicked the widget to change the time of + * the reminder. + * + * @param currentHour hour previously picked by the user + * @param currentMin minute previously picked by the user + */ + default void onTimeClicked(int currentHour, int currentMin) {} + + /** + * Called when the used has clicked the widget to change the days + * of the reminder. + * + * @param currentDays days previously selected by the user. + */ + default void onWeekdayClicked(WeekdayList currentDays) {} + } +} diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/edit/views/TargetPanel.java b/app/src/main/java/org/isoron/uhabits/activities/habits/edit/views/TargetPanel.java new file mode 100644 index 000000000..7319bb967 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/activities/habits/edit/views/TargetPanel.java @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2016 Álinson Santos Xavier + * + * 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 . + */ + +package org.isoron.uhabits.activities.habits.edit.views; + +import android.content.*; +import android.content.res.*; +import android.icu.text.*; +import android.support.annotation.*; +import android.util.*; +import android.view.*; +import android.widget.*; + +import org.isoron.uhabits.R; + +import butterknife.*; + + +public class TargetPanel extends FrameLayout +{ + private DecimalFormat valueFormatter = new DecimalFormat("#.##"); + + @BindView(R.id.tvUnit) + ExampleEditText tvUnit; + + @BindView(R.id.tvTargetCount) + TextView tvTargetValue; + + public TargetPanel(@NonNull Context context, @Nullable AttributeSet attrs) + { + super(context, attrs); + + View view = inflate(context, R.layout.edit_habit_target, null); + ButterKnife.bind(this, view); + addView(view); + } + + public double getTargetValue() + { + String sValue = tvTargetValue.getText().toString(); + return Double.parseDouble(sValue); + } + + public void setTargetValue(double targetValue) + { + tvTargetValue.setText(valueFormatter.format(targetValue)); + } + + public String getUnit() + { + return tvUnit.getRealText(); + } + + public void setUnit(String unit) + { + tvUnit.setRealText(unit); + } + + public boolean validate() + { + Resources res = getResources(); + String sValue = tvTargetValue.getText().toString(); + double value = Double.parseDouble(sValue); + + if (value <= 0) + { + tvTargetValue.setError( + res.getString(R.string.validation_number_should_be_positive)); + return false; + } + + return true; + } +} diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsComponent.java b/app/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsComponent.java index 554c22a3b..4f8161d40 100644 --- a/app/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsComponent.java +++ b/app/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsComponent.java @@ -32,19 +32,21 @@ import dagger.*; dependencies = { AppComponent.class }) public interface ListHabitsComponent { - CheckmarkButtonControllerFactory getCheckmarkButtonControllerFactory(); - HabitCardListAdapter getAdapter(); + CheckmarkButtonControllerFactory getCheckmarkButtonControllerFactory(); + ListHabitsController getController(); ListHabitsMenu getMenu(); + MidnightTimer getMidnightTimer(); + + NumberButtonControllerFactory getNumberButtonControllerFactory(); + ListHabitsRootView getRootView(); ListHabitsScreen getScreen(); ListHabitsSelectionMenu getSelectionMenu(); - - MidnightTimer getMidnightTimer(); } diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsController.java b/app/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsController.java index a52c4b4d6..efd734938 100644 --- a/app/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsController.java +++ b/app/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsController.java @@ -83,8 +83,7 @@ public class ListHabitsController @NonNull ReminderScheduler reminderScheduler, @NonNull TaskRunner taskRunner, @NonNull WidgetUpdater widgetUpdater, - @NonNull - ImportDataTaskFactory importTaskFactory, + @NonNull ImportDataTaskFactory importTaskFactory, @NonNull ExportCSVTaskFactory exportCSVFactory, @NonNull ExportDBTaskFactory exportDBFactory) { @@ -157,6 +156,26 @@ public class ListHabitsController })); } + @Override + public void onInvalidEdit() + { + screen.showMessage(R.string.long_press_to_edit); + } + + @Override + public void onEdit(@NonNull Habit habit, long timestamp) + { + CheckmarkList checkmarks = habit.getCheckmarks(); + double oldValue = checkmarks.getValues(timestamp, timestamp)[0]; + + screen.showNumberPicker(oldValue / 1000, habit.getUnit(), newValue -> { + newValue = Math.round(newValue * 1000); + commandRunner.execute( + new CreateRepetitionCommand(habit, timestamp, (int) newValue), + habit.getId()); + }); + } + @Override public void onInvalidToggle() diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsScreen.java b/app/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsScreen.java index 045505a90..2539b879d 100644 --- a/app/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsScreen.java +++ b/app/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsScreen.java @@ -23,6 +23,10 @@ import android.app.*; import android.content.*; import android.net.*; import android.support.annotation.*; +import android.support.v7.app.AlertDialog; +import android.text.*; +import android.view.*; +import android.widget.*; import org.isoron.uhabits.*; import org.isoron.uhabits.activities.*; @@ -36,30 +40,33 @@ import org.isoron.uhabits.models.*; import org.isoron.uhabits.utils.*; import java.io.*; +import java.lang.reflect.*; import javax.inject.*; +import static android.content.DialogInterface.*; import static android.os.Build.VERSION.*; import static android.os.Build.VERSION_CODES.*; +import static android.view.inputmethod.EditorInfo.*; @ActivityScope public class ListHabitsScreen extends BaseScreen implements CommandRunner.Listener { - public static final int RESULT_IMPORT_DATA = 1; + public static final int REQUEST_OPEN_DOCUMENT = 6; + + public static final int REQUEST_SETTINGS = 7; + + public static final int RESULT_BUG_REPORT = 4; public static final int RESULT_EXPORT_CSV = 2; public static final int RESULT_EXPORT_DB = 3; - public static final int RESULT_BUG_REPORT = 4; + public static final int RESULT_IMPORT_DATA = 1; public static final int RESULT_REPAIR_DB = 5; - public static final int REQUEST_OPEN_DOCUMENT = 6; - - public static final int REQUEST_SETTINGS = 7; - @Nullable private ListHabitsController controller; @@ -75,9 +82,6 @@ public class ListHabitsScreen extends BaseScreen @NonNull private final ConfirmDeleteDialogFactory confirmDeleteDialogFactory; - @NonNull - private final CreateHabitDialogFactory createHabitDialogFactory; - @NonNull private final FilePickerDialogFactory filePickerDialogFactory; @@ -98,18 +102,16 @@ public class ListHabitsScreen extends BaseScreen @NonNull IntentFactory intentFactory, @NonNull ThemeSwitcher themeSwitcher, @NonNull ConfirmDeleteDialogFactory confirmDeleteDialogFactory, - @NonNull CreateHabitDialogFactory createHabitDialogFactory, @NonNull FilePickerDialogFactory filePickerDialogFactory, @NonNull ColorPickerDialogFactory colorPickerFactory, @NonNull EditHabitDialogFactory editHabitDialogFactory) { super(activity); setRootView(rootView); - this.editHabitDialogFactory = editHabitDialogFactory; this.colorPickerFactory = colorPickerFactory; this.commandRunner = commandRunner; this.confirmDeleteDialogFactory = confirmDeleteDialogFactory; - this.createHabitDialogFactory = createHabitDialogFactory; + this.editHabitDialogFactory = editHabitDialogFactory; this.dirFinder = dirFinder; this.filePickerDialogFactory = filePickerDialogFactory; this.intentFactory = intentFactory; @@ -139,60 +141,7 @@ public class ListHabitsScreen extends BaseScreen if (requestCode == REQUEST_OPEN_DOCUMENT) onOpenDocumentResult(resultCode, data); - if (requestCode == REQUEST_SETTINGS) - onSettingsResult(resultCode); - } - - private void onSettingsResult(int resultCode) - { - if (controller == null) return; - - switch (resultCode) - { - case RESULT_IMPORT_DATA: - showImportScreen(); - break; - - case RESULT_EXPORT_CSV: - controller.onExportCSV(); - break; - - case RESULT_EXPORT_DB: - controller.onExportDB(); - break; - - case RESULT_BUG_REPORT: - controller.onSendBugReport(); - break; - - case RESULT_REPAIR_DB: - controller.onRepairDB(); - break; - } - } - - private void onOpenDocumentResult(int resultCode, Intent data) - { - if (controller == null) return; - if (resultCode != Activity.RESULT_OK) return; - - try - { - Uri uri = data.getData(); - ContentResolver cr = activity.getContentResolver(); - InputStream is = cr.openInputStream(uri); - - File cacheDir = activity.getExternalCacheDir(); - File tempFile = File.createTempFile("import", "", cacheDir); - - FileUtils.copy(is, tempFile); - controller.onImportData(tempFile, () -> tempFile.delete()); - } - catch (IOException e) - { - showMessage(R.string.could_not_import); - e.printStackTrace(); - } + if (requestCode == REQUEST_SETTINGS) onSettingsResult(resultCode); } public void setController(@Nullable ListHabitsController controller) @@ -224,7 +173,29 @@ public class ListHabitsScreen extends BaseScreen public void showCreateHabitScreen() { - activity.showDialog(createHabitDialogFactory.create(), "editHabit"); + Dialog dialog = new AlertDialog.Builder(activity) + .setTitle("Type of habit") + .setItems(R.array.habitTypes, (d, which) -> { + if(which == 0) showCreateBooleanHabitScreen(); + else showCreateNumericalHabitScreen(); + }) + .create(); + + dialog.show(); + } + + private void showCreateNumericalHabitScreen() + { + EditHabitDialog dialog; + dialog = editHabitDialogFactory.createNumerical(); + activity.showDialog(dialog, "editHabit"); + } + + public void showCreateBooleanHabitScreen() + { + EditHabitDialog dialog; + dialog = editHabitDialogFactory.createBoolean(); + activity.showDialog(dialog, "editHabit"); } public void showDeleteConfirmationScreen(ConfirmDeleteDialog.Callback callback) @@ -234,8 +205,9 @@ public class ListHabitsScreen extends BaseScreen public void showEditHabitScreen(Habit habit) { - EditHabitDialog dialog = editHabitDialogFactory.create(habit); - activity.showDialog(dialog, "editHabit"); + EditHabitDialog dialog; + dialog = editHabitDialogFactory.edit(habit); + activity.showDialog(dialog, "editNumericalHabit"); } public void showFAQScreen() @@ -278,7 +250,9 @@ public class ListHabitsScreen extends BaseScreen FilePickerDialog picker = filePickerDialogFactory.create(dir); if (controller != null) - picker.setListener(file -> controller.onImportData(file, () -> {})); + picker.setListener(file -> controller.onImportData(file, () -> + { + })); activity.showDialog(picker.getDialog()); } @@ -289,6 +263,74 @@ public class ListHabitsScreen extends BaseScreen activity.startActivity(intent); } + public void showNumberPicker(double value, + @NonNull String unit, + @NonNull NumberPickerCallback callback) + { + LayoutInflater inflater = activity.getLayoutInflater(); + View view = inflater.inflate(R.layout.number_picker_dialog, null); + + final NumberPicker picker; + final NumberPicker picker2; + final TextView tvUnit; + + picker = (NumberPicker) view.findViewById(R.id.picker); + picker2 = (NumberPicker) view.findViewById(R.id.picker2); + tvUnit = (TextView) view.findViewById(R.id.tvUnit); + + int intValue = (int) Math.round(value * 100); + + picker.setMinValue(0); + picker.setMaxValue(Integer.MAX_VALUE / 100); + picker.setValue(intValue / 100); + picker.setWrapSelectorWheel(false); + + picker2.setMinValue(0); + picker2.setMaxValue(19); + picker2.setFormatter(v -> String.format("%02d", 5 * v)); + picker2.setValue((intValue % 100) / 5); + refreshInitialValue(picker2); + + tvUnit.setText(unit); + + AlertDialog dialog = new AlertDialog.Builder(activity) + .setView(view) + .setTitle(R.string.change_value) + .setPositiveButton(android.R.string.ok, (d, which) -> + { + picker.clearFocus(); + double v = picker.getValue() + 0.05 * picker2.getValue(); + callback.onNumberPicked(v); + }) + .create(); + + InterfaceUtils.setupEditorAction(picker, (v, actionId, event) -> + { + if (actionId == IME_ACTION_DONE) + dialog.getButton(BUTTON_POSITIVE).performClick(); + return false; + }); + + dialog.show(); + } + + private void refreshInitialValue(NumberPicker picker2) + { + // Workaround for a bug on Android: + // https://code.google.com/p/android/issues/detail?id=35482 + try + { + Field f = NumberPicker.class.getDeclaredField("mInputText"); + f.setAccessible(true); + EditText inputText = (EditText) f.get(picker2); + inputText.setFilters(new InputFilter[0]); + } + catch (Exception e) + { + throw new RuntimeException(e); + } + } + public void showSettingsScreen() { Intent intent = intentFactory.startSettingsActivity(activity); @@ -300,4 +342,61 @@ public class ListHabitsScreen extends BaseScreen themeSwitcher.toggleNightMode(); activity.restartWithFade(); } + + private void onOpenDocumentResult(int resultCode, Intent data) + { + if (controller == null) return; + if (resultCode != Activity.RESULT_OK) return; + + try + { + Uri uri = data.getData(); + ContentResolver cr = activity.getContentResolver(); + InputStream is = cr.openInputStream(uri); + + File cacheDir = activity.getExternalCacheDir(); + File tempFile = File.createTempFile("import", "", cacheDir); + + FileUtils.copy(is, tempFile); + controller.onImportData(tempFile, () -> tempFile.delete()); + } + catch (IOException e) + { + showMessage(R.string.could_not_import); + e.printStackTrace(); + } + } + + private void onSettingsResult(int resultCode) + { + if (controller == null) return; + + switch (resultCode) + { + case RESULT_IMPORT_DATA: + showImportScreen(); + break; + + case RESULT_EXPORT_CSV: + controller.onExportCSV(); + break; + + case RESULT_EXPORT_DB: + controller.onExportDB(); + break; + + case RESULT_BUG_REPORT: + controller.onSendBugReport(); + break; + + case RESULT_REPAIR_DB: + controller.onRepairDB(); + break; + } + } + + public interface NumberPickerCallback + { + void onNumberPicked(double newValue); + } } diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/list/controllers/HabitCardController.java b/app/src/main/java/org/isoron/uhabits/activities/habits/list/controllers/HabitCardController.java index 01e2b4643..aa29f37c3 100644 --- a/app/src/main/java/org/isoron/uhabits/activities/habits/list/controllers/HabitCardController.java +++ b/app/src/main/java/org/isoron/uhabits/activities/habits/list/controllers/HabitCardController.java @@ -21,8 +21,8 @@ package org.isoron.uhabits.activities.habits.list.controllers; import android.support.annotation.*; -import org.isoron.uhabits.models.Habit; -import org.isoron.uhabits.activities.habits.list.views.HabitCardView; +import org.isoron.uhabits.activities.habits.list.views.*; +import org.isoron.uhabits.models.*; public class HabitCardController implements HabitCardView.Controller { @@ -32,6 +32,18 @@ public class HabitCardController implements HabitCardView.Controller @Nullable private Listener listener; + @Override + public void onEdit(@NonNull Habit habit, long timestamp) + { + if(listener != null) listener.onEdit(habit, timestamp); + } + + @Override + public void onInvalidEdit() + { + if(listener != null) listener.onInvalidEdit(); + } + @Override public void onInvalidToggle() { @@ -55,7 +67,9 @@ public class HabitCardController implements HabitCardView.Controller this.view = view; } - public interface Listener extends CheckmarkButtonController.Listener + public interface Listener extends CheckmarkButtonController.Listener, + NumberButtonController.Listener { + } } diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/list/controllers/HabitCardListController.java b/app/src/main/java/org/isoron/uhabits/activities/habits/list/controllers/HabitCardListController.java index d710c3572..0d62048bc 100644 --- a/app/src/main/java/org/isoron/uhabits/activities/habits/list/controllers/HabitCardListController.java +++ b/app/src/main/java/org/isoron/uhabits/activities/habits/list/controllers/HabitCardListController.java @@ -21,9 +21,9 @@ package org.isoron.uhabits.activities.habits.list.controllers; import android.support.annotation.*; -import org.isoron.uhabits.models.*; import org.isoron.uhabits.activities.habits.list.model.*; import org.isoron.uhabits.activities.habits.list.views.*; +import org.isoron.uhabits.models.*; /** * Controller responsible for receiving and processing the events generated by a @@ -75,6 +75,18 @@ public class HabitCardListController implements HabitCardListView.Controller habitListener.onHabitReorder(habitFrom, habitTo); } + @Override + public void onEdit(@NonNull Habit habit, long timestamp) + { + if (habitListener != null) habitListener.onEdit(habit, timestamp); + } + + @Override + public void onInvalidEdit() + { + if (habitListener != null) habitListener.onInvalidEdit(); + } + /** * Called when the user attempts to perform a toggle, but attempt is * rejected. @@ -172,7 +184,8 @@ public class HabitCardListController implements HabitCardListView.Controller if (selectionListener != null) selectionListener.onSelectionFinish(); } - public interface HabitListener extends CheckmarkButtonController.Listener + public interface HabitListener extends CheckmarkButtonController.Listener, + NumberButtonController.Listener { /** * Called when the user clicks a habit. diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/list/controllers/NumberButtonController.java b/app/src/main/java/org/isoron/uhabits/activities/habits/list/controllers/NumberButtonController.java new file mode 100644 index 000000000..0cd31fe20 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/activities/habits/list/controllers/NumberButtonController.java @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2016 Álinson Santos Xavier + * + * 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 . + */ + +package org.isoron.uhabits.activities.habits.list.controllers; + +import android.support.annotation.*; + +import com.google.auto.factory.*; + +import org.isoron.uhabits.activities.habits.list.views.*; +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.preferences.*; + +@AutoFactory +public class NumberButtonController +{ + @Nullable + private NumberButtonView view; + + @Nullable + private Listener listener; + + @NonNull + private final Preferences prefs; + + @NonNull + private Habit habit; + + private long timestamp; + + public NumberButtonController(@Provided @NonNull Preferences prefs, + @NonNull Habit habit, + long timestamp) + { + this.habit = habit; + this.timestamp = timestamp; + this.prefs = prefs; + } + + public void onClick() + { + if (prefs.isShortToggleEnabled()) performEdit(); + else performInvalidToggle(); + } + + public boolean onLongClick() + { + performEdit(); + return true; + } + + public void performInvalidToggle() + { + if (listener != null) listener.onInvalidEdit(); + } + + public void performEdit() + { + if (listener != null) listener.onEdit(habit, timestamp); + } + + public void setListener(@Nullable Listener listener) + { + this.listener = listener; + } + + public void setView(@Nullable NumberButtonView view) + { + this.view = view; + } + + public interface Listener + { + /** + * Called when the user's attempt to edit the value is rejected. + */ + void onInvalidEdit(); + + /** + * Called when a the user's attempt to edit the value has been accepted. + * @param habit the habit being edited + * @param timestamp the timestamp being edited + */ + void onEdit(@NonNull Habit habit, long timestamp); + } +} diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/list/model/HabitCardListAdapter.java b/app/src/main/java/org/isoron/uhabits/activities/habits/list/model/HabitCardListAdapter.java index 1245668a0..949794fbf 100644 --- a/app/src/main/java/org/isoron/uhabits/activities/habits/list/model/HabitCardListAdapter.java +++ b/app/src/main/java/org/isoron/uhabits/activities/habits/list/model/HabitCardListAdapter.java @@ -171,7 +171,7 @@ public class HabitCardListAdapter if (listView == null) return; Habit habit = cache.getHabitByPosition(position); - int score = cache.getScore(habit.getId()); + double score = cache.getScore(habit.getId()); int checkmarks[] = cache.getCheckmarks(habit.getId()); boolean selected = this.selected.contains(habit); diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/list/model/HabitCardListCache.java b/app/src/main/java/org/isoron/uhabits/activities/habits/list/model/HabitCardListCache.java index fbb4e51a6..9925be240 100644 --- a/app/src/main/java/org/isoron/uhabits/activities/habits/list/model/HabitCardListCache.java +++ b/app/src/main/java/org/isoron/uhabits/activities/habits/list/model/HabitCardListCache.java @@ -112,7 +112,7 @@ public class HabitCardListCache implements CommandRunner.Listener return filteredHabits.getOrder(); } - public int getScore(long habitId) + public double getScore(long habitId) { return data.scores.get(habitId); } @@ -221,7 +221,7 @@ public class HabitCardListCache implements CommandRunner.Listener public HashMap checkmarks; @NonNull - public HashMap scores; + public HashMap scores; /** * Creates a new CacheData without any content. @@ -252,7 +252,7 @@ public class HabitCardListCache implements CommandRunner.Listener { if (oldData.scores.containsKey(id)) scores.put(id, oldData.scores.get(id)); - else scores.put(id, 0); + else scores.put(id, 0.0); } } @@ -365,14 +365,14 @@ public class HabitCardListCache implements CommandRunner.Listener private void performUpdate(Long id, int position) { - Integer oldScore = data.scores.get(id); + double oldScore = data.scores.get(id); int[] oldCheckmarks = data.checkmarks.get(id); - Integer newScore = newData.scores.get(id); + double newScore = newData.scores.get(id); int[] newCheckmarks = newData.checkmarks.get(id); boolean unchanged = true; - if (!oldScore.equals(newScore)) unchanged = false; + if (oldScore != newScore) unchanged = false; if (!Arrays.equals(oldCheckmarks, newCheckmarks)) unchanged = false; if (unchanged) return; diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/list/views/CheckmarkButtonView.java b/app/src/main/java/org/isoron/uhabits/activities/habits/list/views/CheckmarkButtonView.java index 97d0c9fca..acc0e8b7e 100644 --- a/app/src/main/java/org/isoron/uhabits/activities/habits/list/views/CheckmarkButtonView.java +++ b/app/src/main/java/org/isoron/uhabits/activities/habits/list/views/CheckmarkButtonView.java @@ -20,6 +20,8 @@ package org.isoron.uhabits.activities.habits.list.views; import android.content.*; +import android.support.annotation.*; +import android.util.*; import android.view.*; import android.widget.*; @@ -28,6 +30,9 @@ import org.isoron.uhabits.activities.habits.list.controllers.*; import org.isoron.uhabits.models.*; import org.isoron.uhabits.utils.*; +import static org.isoron.uhabits.utils.AttributeSetUtils.*; +import static org.isoron.uhabits.utils.ColorUtils.*; + public class CheckmarkButtonView extends TextView { private int color; @@ -36,16 +41,31 @@ public class CheckmarkButtonView extends TextView private StyledResources res; - public CheckmarkButtonView(Context context) + public CheckmarkButtonView(@Nullable Context context) { super(context); init(); } + public CheckmarkButtonView(@Nullable Context context, + @Nullable AttributeSet attrs) + { + super(context, attrs); + init(); + + if (context != null && attrs != null) + { + int color = getIntAttribute(context, attrs, "color", 0); + int value = getIntAttribute(context, attrs, "value", 0); + setColor(getAndroidTestColor(color)); + setValue(value); + } + } + public void setColor(int color) { this.color = color; - postInvalidate(); + updateText(); } public void setController(final CheckmarkButtonController controller) diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/list/views/CheckmarkPanelView.java b/app/src/main/java/org/isoron/uhabits/activities/habits/list/views/CheckmarkPanelView.java index b569f9208..66061b83b 100644 --- a/app/src/main/java/org/isoron/uhabits/activities/habits/list/views/CheckmarkPanelView.java +++ b/app/src/main/java/org/isoron/uhabits/activities/habits/list/views/CheckmarkPanelView.java @@ -31,18 +31,23 @@ import org.isoron.uhabits.models.*; import org.isoron.uhabits.preferences.*; import org.isoron.uhabits.utils.*; +import java.util.*; + import static android.view.View.MeasureSpec.*; +import static org.isoron.uhabits.utils.AttributeSetUtils.*; +import static org.isoron.uhabits.utils.ColorUtils.*; -public class CheckmarkPanelView extends LinearLayout implements Preferences.Listener +public class CheckmarkPanelView extends LinearLayout + implements Preferences.Listener { - private static final int CHECKMARK_LEFT_TO_RIGHT = 0; + private static final int LEFT_TO_RIGHT = 0; - private static final int CHECKMARK_RIGHT_TO_LEFT = 1; + private static final int RIGHT_TO_LEFT = 1; @Nullable private Preferences prefs; - private int checkmarkValues[]; + private int values[]; private int nButtons; @@ -61,61 +66,89 @@ public class CheckmarkPanelView extends LinearLayout implements Preferences.List init(); } - public CheckmarkPanelView(Context context, AttributeSet attrs) + public CheckmarkPanelView(Context ctx, AttributeSet attrs) { - super(context, attrs); + super(ctx, attrs); init(); + + if (ctx != null && attrs != null) + { + int paletteColor = getIntAttribute(ctx, attrs, "color", 0); + setColor(getAndroidTestColor(paletteColor)); + setButtonCount(getIntAttribute(ctx, attrs, "button_count", 5)); + } + + if (isInEditMode()) initEditMode(); } public CheckmarkButtonView indexToButton(int i) { int position = i; - if (getCheckmarkOrder() == CHECKMARK_RIGHT_TO_LEFT) - position = nButtons - i - 1; + if (getCheckmarkOrder() == RIGHT_TO_LEFT) position = nButtons - i - 1; return (CheckmarkButtonView) getChildAt(position); } + @Override + public void onCheckmarkOrderChanged() + { + setupButtons(); + } + public void setButtonCount(int newButtonCount) { - if(nButtons != newButtonCount) + if (nButtons != newButtonCount) { nButtons = newButtonCount; - addCheckmarkButtons(); + addButtons(); } - setupCheckmarkButtons(); - } - - public void setCheckmarkValues(int[] checkmarkValues) - { - this.checkmarkValues = checkmarkValues; - setupCheckmarkButtons(); + setupButtons(); } public void setColor(int color) { this.color = color; - setupCheckmarkButtons(); + setupButtons(); } public void setController(Controller controller) { this.controller = controller; - setupCheckmarkButtons(); + setupButtons(); } public void setDataOffset(int dataOffset) { this.dataOffset = dataOffset; - setupCheckmarkButtons(); + setupButtons(); } public void setHabit(@NonNull Habit habit) { this.habit = habit; - setupCheckmarkButtons(); + setupButtons(); + } + + public void setValues(int[] values) + { + this.values = values; + setupButtons(); + } + + @Override + protected void onAttachedToWindow() + { + super.onAttachedToWindow(); + if (prefs != null) prefs.addListener(this); + } + + @Override + protected void onDetachedFromWindow() + { + if (prefs != null) prefs.removeListener(this); + super.onDetachedFromWindow(); } @Override @@ -133,7 +166,7 @@ public class CheckmarkPanelView extends LinearLayout implements Preferences.List super.onMeasure(widthSpec, heightSpec); } - private void addCheckmarkButtons() + private void addButtons() { removeAllViews(); @@ -143,21 +176,31 @@ public class CheckmarkPanelView extends LinearLayout implements Preferences.List private int getCheckmarkOrder() { - if (prefs == null) return CHECKMARK_LEFT_TO_RIGHT; - return prefs.shouldReverseCheckmarks() ? CHECKMARK_RIGHT_TO_LEFT : - CHECKMARK_LEFT_TO_RIGHT; + if (prefs == null) return LEFT_TO_RIGHT; + return prefs.shouldReverseCheckmarks() ? RIGHT_TO_LEFT : LEFT_TO_RIGHT; } private void init() { Context appContext = getContext().getApplicationContext(); - if(appContext instanceof HabitsApplication) + if (appContext instanceof HabitsApplication) { HabitsApplication app = (HabitsApplication) appContext; prefs = app.getComponent().getPreferences(); } setWillNotDraw(false); + values = new int[0]; + } + + private void initEditMode() + { + int values[] = new int[nButtons]; + + for (int i = 0; i < nButtons; i++) + values[i] = Math.min(2, new Random().nextInt(4)); + + setValues(values); } private void setupButtonControllers(long timestamp, @@ -178,7 +221,7 @@ public class CheckmarkPanelView extends LinearLayout implements Preferences.List buttonView.setController(buttonController); } - private void setupCheckmarkButtons() + private void setupButtons() { long timestamp = DateUtils.getStartOfToday(); long day = DateUtils.millisecondsInOneDay; @@ -187,34 +230,14 @@ public class CheckmarkPanelView extends LinearLayout implements Preferences.List for (int i = 0; i < nButtons; i++) { CheckmarkButtonView buttonView = indexToButton(i); - if(i + dataOffset >= checkmarkValues.length) break; - buttonView.setValue(checkmarkValues[i + dataOffset]); + if (i + dataOffset >= values.length) break; + buttonView.setValue(values[i + dataOffset]); buttonView.setColor(color); setupButtonControllers(timestamp, buttonView); timestamp -= day; } } - @Override - protected void onAttachedToWindow() - { - super.onAttachedToWindow(); - if(prefs != null) prefs.addListener(this); - } - - @Override - protected void onDetachedFromWindow() - { - if(prefs != null) prefs.removeListener(this); - super.onDetachedFromWindow(); - } - - @Override - public void onCheckmarkOrderChanged() - { - setupCheckmarkButtons(); - } - public interface Controller extends CheckmarkButtonController.Listener { diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardListView.java b/app/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardListView.java index bc784465e..f0a2852d4 100644 --- a/app/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardListView.java +++ b/app/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardListView.java @@ -84,17 +84,19 @@ public class HabitCardListView extends RecyclerView */ public View bindCardView(@NonNull HabitCardViewHolder holder, @NonNull Habit habit, - int score, + double score, int[] checkmarks, boolean selected) { HabitCardView cardView = (HabitCardView) holder.itemView; cardView.setHabit(habit); cardView.setSelected(selected); - cardView.setCheckmarkValues(checkmarks); - cardView.setCheckmarkCount(checkmarkCount); + cardView.setValues(checkmarks); + cardView.setButtonCount(checkmarkCount); cardView.setDataOffset(dataOffset); cardView.setScore(score); + cardView.setUnit(habit.getUnit()); + cardView.setThreshold(habit.getTargetValue()); if (controller != null) setupCardViewController(holder); return cardView; } diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardView.java b/app/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardView.java index d57e0a71c..e9ddcc96f 100644 --- a/app/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardView.java +++ b/app/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardView.java @@ -56,6 +56,9 @@ public class HabitCardView extends FrameLayout @BindView(R.id.checkmarkPanel) CheckmarkPanelView checkmarkPanel; + @BindView(R.id.numberPanel) + NumberPanelView numberPanel; + @BindView(R.id.innerFrame) LinearLayout innerFrame; @@ -92,28 +95,31 @@ public class HabitCardView extends FrameLayout new Handler(Looper.getMainLooper()).post(() -> refresh()); } - public void setCheckmarkCount(int checkmarkCount) + public void setButtonCount(int buttonCount) { - checkmarkPanel.setButtonCount(checkmarkCount); + checkmarkPanel.setButtonCount(buttonCount); + numberPanel.setButtonCount(buttonCount); } - public void setCheckmarkValues(int checkmarks[]) + public void setThreshold(double threshold) { - checkmarkPanel.setCheckmarkValues(checkmarks); - postInvalidate(); + numberPanel.setThreshold(threshold); } public void setController(Controller controller) { checkmarkPanel.setController(null); + numberPanel.setController(null); if (controller == null) return; checkmarkPanel.setController(controller); + numberPanel.setController(controller); } public void setDataOffset(int dataOffset) { this.dataOffset = dataOffset; checkmarkPanel.setDataOffset(dataOffset); + numberPanel.setDataOffset(dataOffset); } public void setHabit(@NonNull Habit habit) @@ -122,15 +128,16 @@ public class HabitCardView extends FrameLayout this.habit = habit; checkmarkPanel.setHabit(habit); + numberPanel.setHabit(habit); refresh(); attachToHabit(); postInvalidate(); } - public void setScore(int score) + public void setScore(double score) { - float percentage = (float) score / Score.MAX_VALUE; + float percentage = (float) score; scoreRing.setPercentage(percentage); scoreRing.setPrecision(1.0f / 16); postInvalidate(); @@ -143,6 +150,23 @@ public class HabitCardView extends FrameLayout updateBackground(isSelected); } + public void setUnit(String unit) + { + numberPanel.setUnit(unit); + } + + public void setValues(int values[]) + { + double dvalues[] = new double[values.length]; + for(int i = 0; i < values.length; i++) + dvalues[i] = (double) values[i] / 1000; + + checkmarkPanel.setValues(values); + numberPanel.setValues(dvalues); + numberPanel.setThreshold(10); + postInvalidate(); + } + public void triggerRipple(long timestamp) { long today = DateUtils.getStartOfToday(); @@ -191,7 +215,8 @@ public class HabitCardView extends FrameLayout inflate(context, R.layout.list_habits_card, this); ButterKnife.bind(this); - innerFrame.setOnTouchListener((v, event) -> { + innerFrame.setOnTouchListener((v, event) -> + { if (SDK_INT >= LOLLIPOP) v.getBackground().setHotspot(event.getX(), event.getY()); return false; @@ -205,15 +230,12 @@ public class HabitCardView extends FrameLayout { Random rand = new Random(); int color = ColorUtils.getAndroidTestColor(rand.nextInt(10)); - int[] values = new int[5]; - for (int i = 0; i < 5; i++) values[i] = rand.nextInt(3); - label.setText(EDIT_MODE_HABITS[rand.nextInt(EDIT_MODE_HABITS.length)]); label.setTextColor(color); scoreRing.setColor(color); scoreRing.setPercentage(rand.nextFloat()); checkmarkPanel.setColor(color); - checkmarkPanel.setCheckmarkValues(values); + numberPanel.setColor(color); } private void refresh() @@ -223,6 +245,12 @@ public class HabitCardView extends FrameLayout label.setTextColor(color); scoreRing.setColor(color); checkmarkPanel.setColor(color); + numberPanel.setColor(color); + + boolean isNumerical = habit.isNumerical(); + checkmarkPanel.setVisibility(isNumerical ? GONE : VISIBLE); + numberPanel.setVisibility(isNumerical ? VISIBLE : GONE); + postInvalidate(); } @@ -256,5 +284,9 @@ public class HabitCardView extends FrameLayout } } - public interface Controller extends CheckmarkPanelView.Controller {} + public interface Controller + extends CheckmarkPanelView.Controller, NumberPanelView.Controller + { + + } } diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/list/views/NumberButtonView.java b/app/src/main/java/org/isoron/uhabits/activities/habits/list/views/NumberButtonView.java new file mode 100644 index 000000000..4246cf8e2 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/activities/habits/list/views/NumberButtonView.java @@ -0,0 +1,189 @@ +/* + * Copyright (C) 2016 Álinson Santos Xavier + * + * 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 . + */ + +package org.isoron.uhabits.activities.habits.list.views; + +import android.content.*; +import android.content.res.*; +import android.graphics.*; +import android.icu.text.*; +import android.support.annotation.*; +import android.text.*; +import android.util.*; +import android.view.*; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.activities.habits.list.controllers.*; +import org.isoron.uhabits.utils.*; + +import static org.isoron.uhabits.utils.AttributeSetUtils.*; +import static org.isoron.uhabits.utils.ColorUtils.*; + +public class NumberButtonView extends View +{ + private static Typeface BOLD_TYPEFACE = + Typeface.create("sans-serif-condensed", Typeface.BOLD); + + private static Typeface NORMAL_TYPEFACE = + Typeface.create("sans-serif-condensed", Typeface.NORMAL); + + private int color; + + private double value; + + private double threshold; + + private String unit; + + private RectF rect; + + private TextPaint pRegular; + + private Resources res; + + private TextPaint pBold; + + private int lightGrey; + + private float em; + + private int darkGrey; + + public NumberButtonView(@Nullable Context context) + { + super(context); + init(); + } + + public NumberButtonView(@Nullable Context ctx, @Nullable AttributeSet attrs) + { + super(ctx, attrs); + init(); + + if (ctx != null && attrs != null) + { + int color = getIntAttribute(ctx, attrs, "color", 0); + int value = getIntAttribute(ctx, attrs, "value", 0); + int threshold = getIntAttribute(ctx, attrs, "threshold", 1); + String unit = getAttribute(ctx, attrs, "unit", "min"); + setColor(getAndroidTestColor(color)); + setThreshold(threshold); + setValue(value); + setUnit(unit); + } + } + + /** + * + * @param v + * @return + */ + public static String formatValue(double v) + { + if (v >= 1e9) return String.format("%.1fG", v / 1e9); + if (v >= 1e8) return String.format("%.0fM", v / 1e6); + if (v >= 1e7) return String.format("%.1fM", v / 1e6); + if (v >= 1e6) return String.format("%.1fM", v / 1e6); + if (v >= 1e5) return String.format("%.0fk", v / 1e3); + if (v >= 1e4) return String.format("%.1fk", v / 1e3); + if (v >= 1e3) return String.format("%.1fk", v / 1e3); + if (v >= 1e1) return new DecimalFormat("#.#").format(v); + return new DecimalFormat("#.##").format(v); + } + + public void setColor(int color) + { + this.color = color; + postInvalidate(); + } + + public void setController(final NumberButtonController controller) + { + setOnClickListener(v -> controller.onClick()); + setOnLongClickListener(v -> controller.onLongClick()); + } + + public void setThreshold(double threshold) + { + this.threshold = threshold; + postInvalidate(); + } + + public void setUnit(String unit) + { + this.unit = unit; + postInvalidate(); + } + + public void setValue(double value) + { + this.value = value; + postInvalidate(); + } + + @Override + protected void onDraw(Canvas canvas) + { + int activeColor = lightGrey; + if(value > 0 && value < threshold) activeColor = darkGrey; + if(value >= threshold) activeColor = color; + + pRegular.setColor(activeColor); + pBold.setColor(activeColor); + + String fv = formatValue(value); + + rect.set(0, 0, getWidth(), getHeight()); + canvas.drawText(fv, rect.centerX(), rect.centerY(), pBold); + + rect.offset(0, 1.2f * em); + canvas.drawText(unit, rect.centerX(), rect.centerY(), pRegular); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) + { + int width = (int) res.getDimension(R.dimen.checkmarkWidth); + int height = (int) res.getDimension(R.dimen.checkmarkHeight); + setMeasuredDimension(width, height); + } + + private void init() + { + StyledResources sr = new StyledResources(getContext()); + res = getContext().getResources(); + + rect = new RectF(); + pRegular = new TextPaint(); + pRegular.setTextSize(res.getDimension(R.dimen.smallerTextSize)); + pRegular.setTypeface(NORMAL_TYPEFACE); + pRegular.setAntiAlias(true); + pRegular.setTextAlign(Paint.Align.CENTER); + + pBold = new TextPaint(); + pBold.setTextSize(res.getDimension(R.dimen.smallTextSize)); + pBold.setTypeface(BOLD_TYPEFACE); + pBold.setAntiAlias(true); + pBold.setTextAlign(Paint.Align.CENTER); + + em = pBold.measureText("m"); + lightGrey = sr.getColor(R.attr.lowContrastTextColor); + darkGrey = sr.getColor(R.attr.mediumContrastTextColor); + } +} diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/list/views/NumberPanelView.java b/app/src/main/java/org/isoron/uhabits/activities/habits/list/views/NumberPanelView.java new file mode 100644 index 000000000..82edfdc97 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/activities/habits/list/views/NumberPanelView.java @@ -0,0 +1,261 @@ +/* + * Copyright (C) 2016 Álinson Santos Xavier + * + * 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 . + */ + +package org.isoron.uhabits.activities.habits.list.views; + +import android.content.*; +import android.support.annotation.*; +import android.util.*; +import android.widget.*; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.activities.habits.list.*; +import org.isoron.uhabits.activities.habits.list.controllers.*; +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.preferences.*; +import org.isoron.uhabits.utils.*; + +import java.util.*; + +import static android.view.View.MeasureSpec.*; +import static org.isoron.uhabits.utils.AttributeSetUtils.*; +import static org.isoron.uhabits.utils.ColorUtils.*; + +public class NumberPanelView extends LinearLayout + implements Preferences.Listener +{ + private static final int LEFT_TO_RIGHT = 0; + + private static final int RIGHT_TO_LEFT = 1; + + @Nullable + private Preferences prefs; + + private double values[]; + + private double threshold; + + private int nButtons; + + private int color; + + private Controller controller; + + private String unit; + + @NonNull + private Habit habit; + + private int dataOffset; + + public NumberPanelView(Context context) + { + super(context); + init(); + } + + public NumberPanelView(Context ctx, AttributeSet attrs) + { + super(ctx, attrs); + init(); + + if (ctx != null && attrs != null) + { + int paletteColor = getIntAttribute(ctx, attrs, "color", 0); + setColor(getAndroidTestColor(paletteColor)); + setButtonCount(getIntAttribute(ctx, attrs, "button_count", 5)); + setThreshold(getIntAttribute(ctx, attrs, "threshold", 1)); + setUnit(getAttribute(ctx, attrs, "unit", "min")); + } + + if(isInEditMode()) initEditMode(); + } + + public void setUnit(String unit) + { + this.unit = unit; + setupButtons(); + } + + public void initEditMode() + { + double values[] = new double[nButtons]; + for(int i = 0; i < nButtons; i++) + values[i] = new Random().nextDouble() * (threshold * 3); + setValues(values); + } + + public NumberButtonView indexToButton(int i) + { + int position = i; + if (getCheckmarkOrder() == RIGHT_TO_LEFT) position = nButtons - i - 1; + return (NumberButtonView) getChildAt(position); + } + + @Override + public void onCheckmarkOrderChanged() + { + setupButtons(); + } + + public void setButtonCount(int newButtonCount) + { + if (nButtons != newButtonCount) + { + nButtons = newButtonCount; + addButtons(); + } + + setupButtons(); + } + + public void setColor(int color) + { + this.color = color; + setupButtons(); + } + + public void setController(Controller controller) + { + this.controller = controller; + setupButtons(); + } + + public void setDataOffset(int dataOffset) + { + this.dataOffset = dataOffset; + setupButtons(); + } + + public void setHabit(@NonNull Habit habit) + { + this.habit = habit; + setupButtons(); + } + + public void setThreshold(double threshold) + { + this.threshold = threshold; + setupButtons(); + } + + public void setValues(double[] values) + { + this.values = values; + setupButtons(); + } + + @Override + protected void onAttachedToWindow() + { + super.onAttachedToWindow(); + if (prefs != null) prefs.addListener(this); + } + + @Override + protected void onDetachedFromWindow() + { + if (prefs != null) prefs.removeListener(this); + super.onDetachedFromWindow(); + } + + @Override + protected void onMeasure(int widthSpec, int heightSpec) + { + float buttonWidth = getResources().getDimension(R.dimen.checkmarkWidth); + float buttonHeight = + getResources().getDimension(R.dimen.checkmarkHeight); + + float width = buttonWidth * nButtons; + + widthSpec = makeMeasureSpec((int) width, EXACTLY); + heightSpec = makeMeasureSpec((int) buttonHeight, EXACTLY); + + super.onMeasure(widthSpec, heightSpec); + } + + private void addButtons() + { + removeAllViews(); + + for (int i = 0; i < nButtons; i++) + addView(new NumberButtonView(getContext())); + } + + private int getCheckmarkOrder() + { + if (prefs == null) return LEFT_TO_RIGHT; + return prefs.shouldReverseCheckmarks() ? RIGHT_TO_LEFT : LEFT_TO_RIGHT; + } + + private void init() + { + Context appContext = getContext().getApplicationContext(); + if (appContext instanceof HabitsApplication) + { + HabitsApplication app = (HabitsApplication) appContext; + prefs = app.getComponent().getPreferences(); + } + + setWillNotDraw(false); + values = new double[0]; + } + + private void setupButtonControllers(long timestamp, + NumberButtonView buttonView) + { + if (controller == null) return; + if (!(getContext() instanceof ListHabitsActivity)) return; + + ListHabitsActivity activity = (ListHabitsActivity) getContext(); + NumberButtonControllerFactory buttonControllerFactory = activity + .getListHabitsComponent() + .getNumberButtonControllerFactory(); + + NumberButtonController buttonController = + buttonControllerFactory.create(habit, timestamp); + buttonController.setListener(controller); + buttonController.setView(buttonView); + buttonView.setController(buttonController); + } + + private void setupButtons() + { + long timestamp = DateUtils.getStartOfToday(); + long day = DateUtils.millisecondsInOneDay; + timestamp -= day * dataOffset; + + for (int i = 0; i < nButtons; i++) + { + NumberButtonView buttonView = indexToButton(i); + if (i + dataOffset >= values.length) break; + buttonView.setValue(values[i + dataOffset]); + buttonView.setColor(color); + buttonView.setThreshold(threshold); + buttonView.setUnit(unit); + setupButtonControllers(timestamp, buttonView); + timestamp -= day; + } + } + + public interface Controller extends NumberButtonController.Listener + { + + } +} diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/show/ShowHabitRootView.java b/app/src/main/java/org/isoron/uhabits/activities/habits/show/ShowHabitRootView.java index a62056bb8..7c2f16364 100644 --- a/app/src/main/java/org/isoron/uhabits/activities/habits/show/ShowHabitRootView.java +++ b/app/src/main/java/org/isoron/uhabits/activities/habits/show/ShowHabitRootView.java @@ -59,6 +59,9 @@ public class ShowHabitRootView extends BaseRootView @BindView(R.id.historyCard) HistoryCard historyCard; + @BindView(R.id.barCard) + BarCard barCard; + @BindView(R.id.toolbar) Toolbar toolbar; @@ -149,6 +152,11 @@ public class ShowHabitRootView extends BaseRootView historyCard.setHabit(habit); streakCard.setHabit(habit); frequencyCard.setHabit(habit); + + if(habit.isNumerical()) + barCard.setHabit(habit); + else + barCard.setVisibility(GONE); } public interface Controller extends HistoryCard.Controller diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/show/ShowHabitScreen.java b/app/src/main/java/org/isoron/uhabits/activities/habits/show/ShowHabitScreen.java index 1c238c02e..ec61d3b8f 100644 --- a/app/src/main/java/org/isoron/uhabits/activities/habits/show/ShowHabitScreen.java +++ b/app/src/main/java/org/isoron/uhabits/activities/habits/show/ShowHabitScreen.java @@ -44,7 +44,8 @@ public class ShowHabitScreen extends BaseScreen public ShowHabitScreen(@NonNull BaseActivity activity, @NonNull Habit habit, @NonNull ShowHabitRootView view, - @NonNull EditHabitDialogFactory editHabitDialogFactory) + @NonNull + EditHabitDialogFactory editHabitDialogFactory) { super(activity); setRootView(view); @@ -71,8 +72,9 @@ public class ShowHabitScreen extends BaseScreen public void showEditHabitDialog() { - EditHabitDialog dialog = editHabitDialogFactory.create(habit); - activity.showDialog(dialog, "editHabit"); + activity.showDialog( + editHabitDialogFactory.edit(habit), + "editHabit"); } public void showEditHistoryDialog() diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/show/views/BarCard.java b/app/src/main/java/org/isoron/uhabits/activities/habits/show/views/BarCard.java new file mode 100644 index 000000000..edf876f33 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/activities/habits/show/views/BarCard.java @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2016 Álinson Santos Xavier + * + * 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 . + */ + +package org.isoron.uhabits.activities.habits.show.views; + +import android.content.*; +import android.support.annotation.*; +import android.util.*; +import android.widget.*; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.R; +import org.isoron.uhabits.activities.common.views.*; +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.tasks.*; +import org.isoron.uhabits.utils.*; + +import java.util.*; + +import butterknife.*; + +public class BarCard extends HabitCard +{ + @BindView(R.id.barChart) + BarChart chart; + + @BindView(R.id.title) + TextView title; + + @Nullable + private TaskRunner taskRunner; + + public BarCard(Context context) + { + super(context); + init(); + } + + public BarCard(Context context, AttributeSet attrs) + { + super(context, attrs); + init(); + } + + @Override + protected void refreshData() + { + if(taskRunner == null) return; + taskRunner.execute(new RefreshTask(getHabit())); + } + + private void init() + { + inflate(getContext(), R.layout.show_habit_bar, this); + ButterKnife.bind(this); + + Context appContext = getContext().getApplicationContext(); + if (appContext instanceof HabitsApplication) + { + HabitsApplication app = (HabitsApplication) appContext; + taskRunner = app.getComponent().getTaskRunner(); + } + + if (isInEditMode()) initEditMode(); + } + + private void initEditMode() + { + int color = ColorUtils.getAndroidTestColor(1); + title.setTextColor(color); + chart.setColor(color); + chart.populateWithRandomData(); + } + + private class RefreshTask implements Task + { + private final Habit habit; + + public RefreshTask(Habit habit) {this.habit = habit;} + + @Override + public void doInBackground() + { + long today = DateUtils.getStartOfToday(); + List checkmarks = + habit.getCheckmarks().getByInterval(0, today); + chart.setCheckmarks(checkmarks); + } + + @Override + public void onPreExecute() + { + int color = ColorUtils.getColor(getContext(), habit.getColor()); + title.setTextColor(color); + chart.setColor(color); + chart.setTarget(habit.getTargetValue()); + } + } +} diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/show/views/HistoryCard.java b/app/src/main/java/org/isoron/uhabits/activities/habits/show/views/HistoryCard.java index e2d9e6a4a..248285cbd 100644 --- a/app/src/main/java/org/isoron/uhabits/activities/habits/show/views/HistoryCard.java +++ b/app/src/main/java/org/isoron/uhabits/activities/habits/show/views/HistoryCard.java @@ -126,6 +126,11 @@ public class HistoryCard extends HabitCard int color = ColorUtils.getColor(getContext(), habit.getColor()); title.setTextColor(color); chart.setColor(color); + if(habit.isNumerical()) + { + chart.setTarget((int) (habit.getTargetValue() * 1000)); + chart.setNumerical(true); + } } } } diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/show/views/OverviewCard.java b/app/src/main/java/org/isoron/uhabits/activities/habits/show/views/OverviewCard.java index db423054b..080c8e4c6 100644 --- a/app/src/main/java/org/isoron/uhabits/activities/habits/show/views/OverviewCard.java +++ b/app/src/main/java/org/isoron/uhabits/activities/habits/show/views/OverviewCard.java @@ -105,9 +105,9 @@ public class OverviewCard extends HabitCard private void initEditMode() { color = ColorUtils.getAndroidTestColor(1); - cache.todayScore = Score.MAX_VALUE * 0.6f; - cache.lastMonthScore = Score.MAX_VALUE * 0.42f; - cache.lastYearScore = Score.MAX_VALUE * 0.75f; + cache.todayScore = 0.6f; + cache.lastMonthScore = 0.42f; + cache.lastYearScore = 0.75f; refreshColors(); refreshScore(); } @@ -121,11 +121,9 @@ public class OverviewCard extends HabitCard private void refreshScore() { - float todayPercentage = cache.todayScore / Score.MAX_VALUE; - float monthDiff = - todayPercentage - (cache.lastMonthScore / Score.MAX_VALUE); - float yearDiff = - todayPercentage - (cache.lastYearScore / Score.MAX_VALUE); + float todayPercentage = cache.todayScore; + float monthDiff = todayPercentage - cache.lastMonthScore; + float yearDiff = todayPercentage - cache.lastYearScore; scoreRing.setPercentage(todayPercentage); scoreLabel.setText(String.format("%.0f%%", todayPercentage * 100)); diff --git a/app/src/main/java/org/isoron/uhabits/commands/CreateRepetitionCommand.java b/app/src/main/java/org/isoron/uhabits/commands/CreateRepetitionCommand.java new file mode 100644 index 000000000..5422d7f1b --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/commands/CreateRepetitionCommand.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2016 Álinson Santos Xavier + * + * 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 . + */ + +package org.isoron.uhabits.commands; + +import android.support.annotation.*; + +import org.isoron.uhabits.models.*; + +/** + * Command to toggle a repetition. + */ +public class CreateRepetitionCommand extends Command +{ + @NonNull + private final Habit habit; + + private final long timestamp; + + private final int value; + + private Repetition previousRep; + + private Repetition newRep; + + public CreateRepetitionCommand(@NonNull Habit habit, + long timestamp, + int value) + { + this.timestamp = timestamp; + this.habit = habit; + this.value = value; + } + + @Override + public void execute() + { + RepetitionList reps = habit.getRepetitions(); + + previousRep = reps.getByTimestamp(timestamp); + if (previousRep != null) reps.remove(previousRep); + + newRep = new Repetition(timestamp, value); + reps.add(newRep); + + habit.invalidateNewerThan(timestamp); + } + + @NonNull + public Habit getHabit() + { + return habit; + } + + @Override + public void undo() + { + habit.getRepetitions().remove(newRep); + if (previousRep != null) habit.getRepetitions().add(previousRep); + habit.invalidateNewerThan(timestamp); + } +} \ No newline at end of file diff --git a/app/src/main/java/org/isoron/uhabits/commands/EditHabitCommand.java b/app/src/main/java/org/isoron/uhabits/commands/EditHabitCommand.java index 8d9605dbb..dd105fb99 100644 --- a/app/src/main/java/org/isoron/uhabits/commands/EditHabitCommand.java +++ b/app/src/main/java/org/isoron/uhabits/commands/EditHabitCommand.java @@ -42,6 +42,8 @@ public class EditHabitCommand extends Command private boolean hasFrequencyChanged; + private final boolean hasTargetChanged; + public EditHabitCommand(@Provided @NonNull ModelFactory modelFactory, @NonNull HabitList habitList, @NonNull Habit original, @@ -58,6 +60,9 @@ public class EditHabitCommand extends Command Frequency originalFreq = this.original.getFrequency(); Frequency modifiedFreq = this.modified.getFrequency(); hasFrequencyChanged = (!originalFreq.equals(modifiedFreq)); + hasTargetChanged = + (original.getTargetType() != modified.getTargetType() || + original.getTargetValue() != modified.getTargetValue()); } @Override @@ -97,11 +102,7 @@ public class EditHabitCommand extends Command private void invalidateIfNeeded(Habit habit) { - if (hasFrequencyChanged) - { - habit.getCheckmarks().invalidateNewerThan(0); - habit.getStreaks().invalidateNewerThan(0); - habit.getScores().invalidateNewerThan(0); - } + if (hasFrequencyChanged || hasTargetChanged) + habit.invalidateNewerThan(0); } } \ No newline at end of file diff --git a/app/src/main/java/org/isoron/uhabits/io/HabitsCSVExporter.java b/app/src/main/java/org/isoron/uhabits/io/HabitsCSVExporter.java index fc0a29c92..d89413d2f 100644 --- a/app/src/main/java/org/isoron/uhabits/io/HabitsCSVExporter.java +++ b/app/src/main/java/org/isoron/uhabits/io/HabitsCSVExporter.java @@ -178,7 +178,7 @@ public class HabitsCSVExporter long newest = DateUtils.getStartOfToday(); List checkmarks = new ArrayList<>(); - List scores = new ArrayList<>(); + List scores = new ArrayList<>(); for (Habit h : selectedHabits) { checkmarks.add(h.getCheckmarks().getValues(oldest, newest)); @@ -202,7 +202,7 @@ public class HabitsCSVExporter checksWriter.write(String.valueOf(checkmarks.get(j)[i])); checksWriter.write(DELIMITER); String score = - String.format("%.4f", ((float) scores.get(j)[i]) / Score.MAX_VALUE); + String.format("%.4f", ((float) scores.get(j)[i])); scoresWriter.write(score); scoresWriter.write(DELIMITER); } diff --git a/app/src/main/java/org/isoron/uhabits/models/Checkmark.java b/app/src/main/java/org/isoron/uhabits/models/Checkmark.java index 3ae1c4b1e..c434a4a87 100644 --- a/app/src/main/java/org/isoron/uhabits/models/Checkmark.java +++ b/app/src/main/java/org/isoron/uhabits/models/Checkmark.java @@ -51,6 +51,15 @@ public final class Checkmark private final long timestamp; + /** + * The value of the checkmark. + * + * For boolean habits, this equals either UNCHECKED, CHECKED_EXPLICITLY, + * or CHECKED_IMPLICITLY. + * + * For numerical habits, this number is stored in thousandths. That + * is, if the user enters value 1.50 on the app, it is stored as 1500. + */ private final int value; public Checkmark(long timestamp, int value) diff --git a/app/src/main/java/org/isoron/uhabits/models/CheckmarkList.java b/app/src/main/java/org/isoron/uhabits/models/CheckmarkList.java index 86488a8d1..5e71eb9e5 100644 --- a/app/src/main/java/org/isoron/uhabits/models/CheckmarkList.java +++ b/app/src/main/java/org/isoron/uhabits/models/CheckmarkList.java @@ -27,6 +27,9 @@ import java.io.*; import java.text.*; import java.util.*; +import static org.isoron.uhabits.models.Checkmark.CHECKED_EXPLICITLY; +import static org.isoron.uhabits.models.Checkmark.CHECKED_IMPLICITLY; + /** * The collection of {@link Checkmark}s belonging to a habit. */ @@ -239,7 +242,7 @@ public abstract class CheckmarkList for (Repetition rep : reps) { int offset = (int) ((rep.getTimestamp() - fromExtended) / day); - checks[nDaysExtended - offset - 1] = Checkmark.CHECKED_EXPLICITLY; + checks[nDaysExtended - offset - 1] = rep.getValue(); } for (int i = 0; i < nDays; i++) @@ -247,11 +250,11 @@ public abstract class CheckmarkList int counter = 0; for (int j = 0; j < freq.getDenominator(); j++) - if (checks[i + j] == 2) counter++; + if (checks[i + j] == CHECKED_EXPLICITLY) counter++; if (counter >= freq.getNumerator()) - if (checks[i] != Checkmark.CHECKED_EXPLICITLY) - checks[i] = Checkmark.CHECKED_IMPLICITLY; + if (checks[i] != CHECKED_EXPLICITLY) + checks[i] = CHECKED_IMPLICITLY; } List checkmarks = new LinkedList<>(); diff --git a/app/src/main/java/org/isoron/uhabits/models/Habit.java b/app/src/main/java/org/isoron/uhabits/models/Habit.java index 5ba711407..fbc6d5d88 100644 --- a/app/src/main/java/org/isoron/uhabits/models/Habit.java +++ b/app/src/main/java/org/isoron/uhabits/models/Habit.java @@ -28,14 +28,24 @@ import java.util.*; import javax.inject.*; +import static org.isoron.uhabits.models.Checkmark.*; + /** * The thing that the user wants to track. */ public class Habit { + public static final int AT_LEAST = 0; + + public static final int AT_MOST = 1; + public static final String HABIT_URI_FORMAT = "content://org.isoron.uhabits/habit/%d"; + public static final int NUMBER_HABIT = 1; + + public static final int YES_NO_HABIT = 0; + @Nullable private Long id; @@ -48,10 +58,8 @@ public class Habit @NonNull private Frequency frequency; - @NonNull - private Integer color; + private int color; - @NonNull private boolean archived; @NonNull @@ -60,12 +68,21 @@ public class Habit @NonNull private ScoreList scores; + private int targetType; + + private double targetValue; + + private int type; + @NonNull private RepetitionList repetitions; @NonNull private CheckmarkList checkmarks; + @NonNull + private String unit; + @Nullable private Reminder reminder; @@ -83,6 +100,12 @@ public class Habit this.color = 5; this.archived = false; this.frequency = new Frequency(3, 7); + this.type = YES_NO_HABIT; + this.name = ""; + this.description = ""; + this.targetType = AT_LEAST; + this.targetValue = 100; + this.unit = ""; checkmarks = factory.buildCheckmarkList(this); streaks = factory.buildStreakList(this); @@ -112,6 +135,10 @@ public class Habit this.archived = model.isArchived(); this.frequency = model.frequency; this.reminder = model.reminder; + this.type = model.type; + this.targetValue = model.targetValue; + this.targetType = model.targetType; + this.unit = model.unit; observable.notifyListeners(); } @@ -138,6 +165,13 @@ public class Habit return color; } + public boolean isCompletedToday() + { + int todayCheckmark = getCheckmarks().getTodayValue(); + if (isNumerical()) return todayCheckmark >= targetValue; + else return (todayCheckmark != UNCHECKED); + } + public void setColor(@NonNull Integer color) { this.color = color; @@ -232,6 +266,53 @@ public class Habit return streaks; } + public int getTargetType() + { + return targetType; + } + + public void setTargetType(int targetType) + { + if (targetType != AT_LEAST && targetType != AT_MOST) + throw new IllegalArgumentException(); + this.targetType = targetType; + } + + public double getTargetValue() + { + return targetValue; + } + + public void setTargetValue(double targetValue) + { + if(targetValue < 0) throw new IllegalArgumentException(); + this.targetValue = targetValue; + } + + public int getType() + { + return type; + } + + public void setType(int type) + { + if (type != YES_NO_HABIT && type != NUMBER_HABIT) + throw new IllegalArgumentException(); + + this.type = type; + } + + @NonNull + public String getUnit() + { + return unit; + } + + public void setUnit(@NonNull String unit) + { + this.unit = unit; + } + /** * Returns the public URI that identifies this habit * @@ -253,6 +334,13 @@ public class Habit return reminder != null; } + public void invalidateNewerThan(long timestamp) + { + getScores().invalidateNewerThan(timestamp); + getCheckmarks().invalidateNewerThan(timestamp); + getStreaks().invalidateNewerThan(timestamp); + } + public boolean isArchived() { return archived; @@ -263,6 +351,11 @@ public class Habit this.archived = archived; } + public boolean isNumerical() + { + return type == NUMBER_HABIT; + } + @Override public String toString() { @@ -272,6 +365,10 @@ public class Habit .append("description", description) .append("color", color) .append("archived", archived) + .append("type", type) + .append("targetType", targetType) + .append("targetValue", targetValue) + .append("unit", unit) .toString(); } } diff --git a/app/src/main/java/org/isoron/uhabits/models/HabitMatcher.java b/app/src/main/java/org/isoron/uhabits/models/HabitMatcher.java index afc8d6ff7..17198f279 100644 --- a/app/src/main/java/org/isoron/uhabits/models/HabitMatcher.java +++ b/app/src/main/java/org/isoron/uhabits/models/HabitMatcher.java @@ -23,8 +23,6 @@ import android.support.annotation.*; import java.util.*; -import static org.isoron.uhabits.models.Checkmark.*; - public class HabitMatcher { public static final HabitMatcher WITH_ALARM = new HabitMatcherBuilder() @@ -75,14 +73,8 @@ public class HabitMatcher { if (!isArchivedAllowed() && habit.isArchived()) return false; if (isReminderRequired() && !habit.hasReminder()) return false; - - if(!isCompletedAllowed()) - { - int todayCheckmark = habit.getCheckmarks().getTodayValue(); - if (todayCheckmark != UNCHECKED) return false; - } - - if(!allowedColors.contains(habit.getColor())) return false; + if (!isCompletedAllowed() && habit.isCompletedToday()) return false; + if (!allowedColors.contains(habit.getColor())) return false; return true; } } diff --git a/app/src/main/java/org/isoron/uhabits/models/Repetition.java b/app/src/main/java/org/isoron/uhabits/models/Repetition.java index 72e378205..274fe3517 100644 --- a/app/src/main/java/org/isoron/uhabits/models/Repetition.java +++ b/app/src/main/java/org/isoron/uhabits/models/Repetition.java @@ -30,6 +30,17 @@ public final class Repetition private final long timestamp; + /** + * The value of the repetition. + * + * For boolean habits, this equals either Checkmark.UNCHECKED, + * Checkmark.CHECKED_EXPLICITLY, or Checkmark.CHECKED_IMPLICITLY. + * + * For numerical habits, this number is stored in thousandths. That + * is, if the user enters value 1.50 on the app, it is stored as 1500. + */ + private final int value; + /** * Creates a new repetition with given parameters. *

@@ -38,9 +49,24 @@ public final class Repetition * * @param timestamp the time this repetition occurred. */ - public Repetition(long timestamp) + public Repetition(long timestamp, int value) { this.timestamp = timestamp; + this.value = value; + } + + @Override + public boolean equals(Object o) + { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Repetition that = (Repetition) o; + + return new EqualsBuilder() + .append(timestamp, that.timestamp) + .append(value, that.value) + .isEquals(); } public long getTimestamp() @@ -48,11 +74,26 @@ public final class Repetition return timestamp; } + public int getValue() + { + return value; + } + + @Override + public int hashCode() + { + return new HashCodeBuilder(17, 37) + .append(timestamp) + .append(value) + .toHashCode(); + } + @Override public String toString() { return new ToStringBuilder(this) .append("timestamp", timestamp) + .append("value", value) .toString(); } } diff --git a/app/src/main/java/org/isoron/uhabits/models/RepetitionList.java b/app/src/main/java/org/isoron/uhabits/models/RepetitionList.java index 07fa7b681..ec355267c 100644 --- a/app/src/main/java/org/isoron/uhabits/models/RepetitionList.java +++ b/app/src/main/java/org/isoron/uhabits/models/RepetitionList.java @@ -192,13 +192,11 @@ public abstract class RepetitionList if (rep != null) remove(rep); else { - rep = new Repetition(timestamp); + rep = new Repetition(timestamp, Checkmark.CHECKED_EXPLICITLY); add(rep); } - habit.getScores().invalidateNewerThan(timestamp); - habit.getCheckmarks().invalidateNewerThan(timestamp); - habit.getStreaks().invalidateNewerThan(timestamp); + habit.invalidateNewerThan(timestamp); return rep; } diff --git a/app/src/main/java/org/isoron/uhabits/models/Score.java b/app/src/main/java/org/isoron/uhabits/models/Score.java index c9ebdea14..fcfc8b56a 100644 --- a/app/src/main/java/org/isoron/uhabits/models/Score.java +++ b/app/src/main/java/org/isoron/uhabits/models/Score.java @@ -21,28 +21,25 @@ package org.isoron.uhabits.models; import org.apache.commons.lang3.builder.*; +import static java.lang.Math.*; + /** * Represents how strong a habit is at a certain date. */ public final class Score { - /** - * Maximum score value attainable by any habit. - */ - public static final int MAX_VALUE = 19259478; - /** * Timestamp of the day to which this score applies. Time of day should be * midnight (UTC). */ - private final Long timestamp; + private final long timestamp; /** * Value of the score. */ - private final Integer value; + private final double value; - public Score(Long timestamp, Integer value) + public Score(long timestamp, double value) { this.timestamp = timestamp; this.value = value; @@ -55,27 +52,20 @@ public final class Score * The frequency of the habit is the number of repetitions divided by the * length of the interval. For example, a habit that should be repeated 3 * times in 8 days has frequency 3.0 / 8.0 = 0.375. - *

- * The checkmarkValue should be UNCHECKED, CHECKED_IMPLICITLY or - * CHECK_EXPLICITLY. * * @param frequency the frequency of the habit * @param previousScore the previous score of the habit * @param checkmarkValue the value of the current checkmark * @return the current score */ - public static int compute(double frequency, - int previousScore, - int checkmarkValue) + public static double compute(double frequency, + double previousScore, + double checkmarkValue) { - double multiplier = Math.pow(0.5, 1.0 / (14.0 / frequency - 1)); - int score = (int) (previousScore * multiplier); + double multiplier = pow(0.5, frequency / 13.0); - if (checkmarkValue == Checkmark.CHECKED_EXPLICITLY) - { - score += 1000000; - score = Math.min(score, Score.MAX_VALUE); - } + double score = previousScore * multiplier; + score += checkmarkValue * (1 - multiplier); return score; } @@ -85,12 +75,12 @@ public final class Score return Long.signum(this.getTimestamp() - other.getTimestamp()); } - public Long getTimestamp() + public long getTimestamp() { return timestamp; } - public Integer getValue() + public double getValue() { return value; } diff --git a/app/src/main/java/org/isoron/uhabits/models/ScoreList.java b/app/src/main/java/org/isoron/uhabits/models/ScoreList.java index 0891cc01d..7eda54256 100644 --- a/app/src/main/java/org/isoron/uhabits/models/ScoreList.java +++ b/app/src/main/java/org/isoron/uhabits/models/ScoreList.java @@ -67,7 +67,7 @@ public abstract class ScoreList implements Iterable * * @return value of today's score */ - public int getTodayValue() + public double getTodayValue() { return getValue(DateUtils.getStartOfToday()); } @@ -81,7 +81,7 @@ public abstract class ScoreList implements Iterable * @param timestamp the timestamp of a day * @return score value for that day */ - public final int getValue(long timestamp) + public final double getValue(long timestamp) { compute(timestamp, timestamp); Score s = getComputedByTimestamp(timestamp); @@ -118,10 +118,10 @@ public abstract class ScoreList implements Iterable * @param to timestamp for the newest score * @return values for the scores inside the given interval */ - public final int[] getValues(long from, long to) + public final double[] getValues(long from, long to) { List scores = getByInterval(from, to); - int[] values = new int[scores.size()]; + double[] values = new double[scores.size()]; for(int i = 0; i < values.length; i++) values[i] = scores.get(i).getValue(); @@ -132,7 +132,7 @@ public abstract class ScoreList implements Iterable public List groupBy(DateUtils.TruncateField field) { computeAll(); - HashMap> groups = getGroupedValues(field); + HashMap> groups = getGroupedValues(field); List scores = groupsToAvgScores(groups); Collections.sort(scores, (s1, s2) -> s2.compareNewer(s1)); return scores; @@ -173,7 +173,7 @@ public abstract class ScoreList implements Iterable { String timestamp = dateFormat.format(s.getTimestamp()); String score = - String.format("%.4f", ((float) s.getValue()) / Score.MAX_VALUE); + String.format("%.4f", s.getValue()); out.write(String.format("%s,%s\n", timestamp, score)); } } @@ -263,7 +263,7 @@ public abstract class ScoreList implements Iterable * @param previousValue value of the score on the day immediately before the * interval begins */ - private void forceRecompute(long from, long to, int previousValue) + private void forceRecompute(long from, long to, double previousValue) { if(from > to) return; @@ -276,7 +276,18 @@ public abstract class ScoreList implements Iterable for (int i = 0; i < checkmarkValues.length; i++) { - int value = checkmarkValues[checkmarkValues.length - i - 1]; + double value = checkmarkValues[checkmarkValues.length - i - 1]; + + if(habit.isNumerical()) + { + value /= 1000; + value /= habit.getTargetValue(); + value = Math.min(1, value); + } + + if(!habit.isNumerical() && value > 0) + value = 1; + previousValue = Score.compute(freq, previousValue, value); scores.add(new Score(from + day * i, previousValue)); } @@ -285,9 +296,9 @@ public abstract class ScoreList implements Iterable } @NonNull - private HashMap> getGroupedValues(DateUtils.TruncateField field) + private HashMap> getGroupedValues(DateUtils.TruncateField field) { - HashMap> groups = new HashMap<>(); + HashMap> groups = new HashMap<>(); for (Score s : this) { @@ -296,26 +307,26 @@ public abstract class ScoreList implements Iterable if (!groups.containsKey(groupTimestamp)) groups.put(groupTimestamp, new ArrayList<>()); - groups.get(groupTimestamp).add((long) s.getValue()); + groups.get(groupTimestamp).add(s.getValue()); } return groups; } @NonNull - private List groupsToAvgScores(HashMap> groups) + private List groupsToAvgScores(HashMap> groups) { List scores = new LinkedList<>(); for (Long timestamp : groups.keySet()) { - long meanValue = 0L; - ArrayList groupValues = groups.get(timestamp); + double meanValue = 0.0; + ArrayList groupValues = groups.get(timestamp); - for (Long v : groupValues) meanValue += v; + for (Double v : groupValues) meanValue += v; meanValue /= groupValues.size(); - scores.add(new Score(timestamp, (int) meanValue)); + scores.add(new Score(timestamp, meanValue)); } return scores; diff --git a/app/src/main/java/org/isoron/uhabits/models/WeekdayList.java b/app/src/main/java/org/isoron/uhabits/models/WeekdayList.java index 21a35b97a..f6cbe4560 100644 --- a/app/src/main/java/org/isoron/uhabits/models/WeekdayList.java +++ b/app/src/main/java/org/isoron/uhabits/models/WeekdayList.java @@ -23,13 +23,12 @@ import java.util.*; public class WeekdayList { - public static WeekdayList EVERY_DAY = new WeekdayList(127); + public static final WeekdayList EVERY_DAY = new WeekdayList(127); private final boolean[] weekdays; public WeekdayList(int packedList) { - if(packedList == 0) packedList = 127; weekdays = new boolean[7]; int current = 1; @@ -42,16 +41,18 @@ public class WeekdayList public WeekdayList(boolean weekdays[]) { - boolean isEmpty = true; - for(boolean b : weekdays) if(b) isEmpty = false; - if(isEmpty) throw new IllegalArgumentException("empty list"); - this.weekdays = Arrays.copyOf(weekdays, 7); } + public boolean isEmpty() + { + for (boolean d : weekdays) if (d) return false; + return true; + } + public boolean[] toArray() { - return weekdays; + return Arrays.copyOf(weekdays, 7); } public int toInteger() diff --git a/app/src/main/java/org/isoron/uhabits/models/memory/MemoryHabitList.java b/app/src/main/java/org/isoron/uhabits/models/memory/MemoryHabitList.java index 601852122..32f1982c4 100644 --- a/app/src/main/java/org/isoron/uhabits/models/memory/MemoryHabitList.java +++ b/app/src/main/java/org/isoron/uhabits/models/memory/MemoryHabitList.java @@ -162,9 +162,9 @@ public class MemoryHabitList extends HabitList }; Comparator scoreComparator = (h1, h2) -> { - int s1 = h1.getScores().getTodayValue(); - int s2 = h2.getScores().getTodayValue(); - return Integer.compare(s2, s1); + double s1 = h1.getScores().getTodayValue(); + double s2 = h2.getScores().getTodayValue(); + return Double.compare(s2, s1); }; if (order == BY_POSITION) return null; diff --git a/app/src/main/java/org/isoron/uhabits/models/memory/MemoryRepetitionList.java b/app/src/main/java/org/isoron/uhabits/models/memory/MemoryRepetitionList.java index dbac82b40..cf1772e0b 100644 --- a/app/src/main/java/org/isoron/uhabits/models/memory/MemoryRepetitionList.java +++ b/app/src/main/java/org/isoron/uhabits/models/memory/MemoryRepetitionList.java @@ -86,7 +86,6 @@ public class MemoryRepetitionList extends RepetitionList oldestRep = rep; oldestTime = rep.getTimestamp(); } - } return oldestRep; @@ -106,7 +105,6 @@ public class MemoryRepetitionList extends RepetitionList newestRep = rep; newestTime = rep.getTimestamp(); } - } return newestRep; @@ -119,7 +117,6 @@ public class MemoryRepetitionList extends RepetitionList observable.notifyListeners(); } - @NonNull @Override public long getTotalCount() { diff --git a/app/src/main/java/org/isoron/uhabits/models/sqlite/SQLiteHabitList.java b/app/src/main/java/org/isoron/uhabits/models/sqlite/SQLiteHabitList.java index da8a6aee5..58a310eaa 100644 --- a/app/src/main/java/org/isoron/uhabits/models/sqlite/SQLiteHabitList.java +++ b/app/src/main/java/org/isoron/uhabits/models/sqlite/SQLiteHabitList.java @@ -280,9 +280,9 @@ public class SQLiteHabitList extends HabitList if(order == Order.BY_SCORE) { Collections.sort(habits, (lhs, rhs) -> { - int s1 = lhs.getScores().getTodayValue(); - int s2 = rhs.getScores().getTodayValue(); - return Integer.compare(s2, s1); + double s1 = lhs.getScores().getTodayValue(); + double s2 = rhs.getScores().getTodayValue(); + return Double.compare(s2, s1); }); } diff --git a/app/src/main/java/org/isoron/uhabits/models/sqlite/SQLiteRepetitionList.java b/app/src/main/java/org/isoron/uhabits/models/sqlite/SQLiteRepetitionList.java index 6278863e9..2794950f5 100644 --- a/app/src/main/java/org/isoron/uhabits/models/sqlite/SQLiteRepetitionList.java +++ b/app/src/main/java/org/isoron/uhabits/models/sqlite/SQLiteRepetitionList.java @@ -73,7 +73,7 @@ public class SQLiteRepetitionList extends RepetitionList public List getByInterval(long timeFrom, long timeTo) { check(habit.getId()); - String query = "select habit, timestamp " + + String query = "select habit, timestamp, value " + "from Repetitions " + "where habit = ? and timestamp >= ? and timestamp <= ? " + "order by timestamp"; @@ -93,7 +93,7 @@ public class SQLiteRepetitionList extends RepetitionList public Repetition getByTimestamp(long timestamp) { check(habit.getId()); - String query = "select habit, timestamp " + + String query = "select habit, timestamp, value " + "from Repetitions " + "where habit = ? and timestamp = ? " + "limit 1"; @@ -111,7 +111,7 @@ public class SQLiteRepetitionList extends RepetitionList public Repetition getOldest() { check(habit.getId()); - String query = "select habit, timestamp " + + String query = "select habit, timestamp, value " + "from Repetitions " + "where habit = ? " + "order by timestamp asc " + @@ -129,7 +129,7 @@ public class SQLiteRepetitionList extends RepetitionList public Repetition getNewest() { check(habit.getId()); - String query = "select habit, timestamp " + + String query = "select habit, timestamp, value " + "from Repetitions " + "where habit = ? " + "order by timestamp desc " + @@ -182,7 +182,6 @@ public class SQLiteRepetitionList extends RepetitionList return reps; } - @NonNull @Override public long getTotalCount() { diff --git a/app/src/main/java/org/isoron/uhabits/models/sqlite/SQLiteScoreList.java b/app/src/main/java/org/isoron/uhabits/models/sqlite/SQLiteScoreList.java index e44e7e6ed..77b3a2515 100644 --- a/app/src/main/java/org/isoron/uhabits/models/sqlite/SQLiteScoreList.java +++ b/app/src/main/java/org/isoron/uhabits/models/sqlite/SQLiteScoreList.java @@ -72,7 +72,7 @@ public class SQLiteScoreList extends ScoreList { statement.bindLong(1, habit.getId()); statement.bindLong(2, s.getTimestamp()); - statement.bindLong(3, s.getValue()); + statement.bindDouble(3, s.getValue()); statement.execute(); } diff --git a/app/src/main/java/org/isoron/uhabits/models/sqlite/records/HabitRecord.java b/app/src/main/java/org/isoron/uhabits/models/sqlite/records/HabitRecord.java index b4120a386..f60e3fe7e 100644 --- a/app/src/main/java/org/isoron/uhabits/models/sqlite/records/HabitRecord.java +++ b/app/src/main/java/org/isoron/uhabits/models/sqlite/records/HabitRecord.java @@ -22,8 +22,6 @@ package org.isoron.uhabits.models.sqlite.records; import android.annotation.*; import android.database.*; import android.support.annotation.*; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; import com.activeandroid.*; import com.activeandroid.annotation.*; @@ -44,7 +42,8 @@ public class HabitRecord extends Model implements SQLiteRecord public static String SELECT = "select id, color, description, freq_den, freq_num, " + "name, position, reminder_hour, reminder_min, " + - "highlight, archived, reminder_days from habits "; + "highlight, archived, reminder_days, type, target_type, " + + "target_value, unit from habits "; @Column(name = "name") public String name; @@ -82,6 +81,18 @@ public class HabitRecord extends Model implements SQLiteRecord @Column(name = "archived") public Integer archived; + @Column(name = "type") + public Integer type; + + @Column(name = "target_value") + public Double targetValue; + + @Column(name = "target_type") + public Integer targetType; + + @Column(name = "unit") + public String unit; + public HabitRecord() { } @@ -146,6 +157,11 @@ public class HabitRecord extends Model implements SQLiteRecord this.highlight = 0; this.color = model.getColor(); this.archived = model.isArchived() ? 1 : 0; + this.type = model.getType(); + this.targetType = model.getTargetType(); + this.targetValue = model.getTargetValue(); + this.unit = model.getUnit(); + Frequency freq = model.getFrequency(); this.freqNum = freq.getNumerator(); this.freqDen = freq.getDenominator(); @@ -177,6 +193,10 @@ public class HabitRecord extends Model implements SQLiteRecord highlight = c.getInt(9); archived = c.getInt(10); reminderDays = c.getInt(11); + type = c.getInt(12); + targetType = c.getInt(13); + targetValue = c.getDouble(14); + unit = c.getString(15); } public void copyTo(Habit habit) @@ -187,6 +207,10 @@ public class HabitRecord extends Model implements SQLiteRecord habit.setColor(this.color); habit.setArchived(this.archived != 0); habit.setId(this.getId()); + habit.setType(this.type); + habit.setTargetType(this.targetType); + habit.setTargetValue(this.targetValue); + habit.setUnit(this.unit); if (reminderHour != null && reminderMin != null) { diff --git a/app/src/main/java/org/isoron/uhabits/models/sqlite/records/RepetitionRecord.java b/app/src/main/java/org/isoron/uhabits/models/sqlite/records/RepetitionRecord.java index 5f831495f..874fd0954 100644 --- a/app/src/main/java/org/isoron/uhabits/models/sqlite/records/RepetitionRecord.java +++ b/app/src/main/java/org/isoron/uhabits/models/sqlite/records/RepetitionRecord.java @@ -38,6 +38,9 @@ public class RepetitionRecord extends Model implements SQLiteRecord @Column(name = "timestamp") public Long timestamp; + @Column(name = "value") + public int value; + public static RepetitionRecord get(Long id) { return RepetitionRecord.load(RepetitionRecord.class, id); @@ -46,16 +49,18 @@ public class RepetitionRecord extends Model implements SQLiteRecord public void copyFrom(Repetition repetition) { timestamp = repetition.getTimestamp(); + value = repetition.getValue(); } @Override public void copyFrom(Cursor c) { timestamp = c.getLong(1); + value = c.getInt(2); } public Repetition toRepetition() { - return new Repetition(timestamp); + return new Repetition(timestamp, value); } } diff --git a/app/src/main/java/org/isoron/uhabits/models/sqlite/records/ScoreRecord.java b/app/src/main/java/org/isoron/uhabits/models/sqlite/records/ScoreRecord.java index daac00d3f..69969edfb 100644 --- a/app/src/main/java/org/isoron/uhabits/models/sqlite/records/ScoreRecord.java +++ b/app/src/main/java/org/isoron/uhabits/models/sqlite/records/ScoreRecord.java @@ -46,13 +46,13 @@ public class ScoreRecord extends Model implements SQLiteRecord * Value of the score. */ @Column(name = "score") - public Integer score; + public Double score; @Override public void copyFrom(Cursor c) { timestamp = c.getLong(1); - score = c.getInt(2); + score = c.getDouble(2); } /** diff --git a/app/src/main/java/org/isoron/uhabits/utils/AttributeSetUtils.java b/app/src/main/java/org/isoron/uhabits/utils/AttributeSetUtils.java index c633a9b72..67c9de531 100644 --- a/app/src/main/java/org/isoron/uhabits/utils/AttributeSetUtils.java +++ b/app/src/main/java/org/isoron/uhabits/utils/AttributeSetUtils.java @@ -74,4 +74,14 @@ public class AttributeSetUtils if (number != null) return Float.parseFloat(number); else return defaultValue; } + + public static int getIntAttribute(@NonNull Context context, + @NonNull AttributeSet attrs, + @NonNull String name, + int defaultValue) + { + String number = getAttribute(context, attrs, name, null); + if (number != null) return Integer.parseInt(number); + else return defaultValue; + } } diff --git a/app/src/main/java/org/isoron/uhabits/utils/InterfaceUtils.java b/app/src/main/java/org/isoron/uhabits/utils/InterfaceUtils.java index 5db2a875a..2bf26c040 100644 --- a/app/src/main/java/org/isoron/uhabits/utils/InterfaceUtils.java +++ b/app/src/main/java/org/isoron/uhabits/utils/InterfaceUtils.java @@ -22,7 +22,10 @@ package org.isoron.uhabits.utils; import android.content.*; import android.content.res.*; import android.graphics.*; +import android.support.annotation.*; import android.util.*; +import android.view.*; +import android.widget.*; import java.util.*; @@ -39,8 +42,9 @@ public abstract class InterfaceUtils public static Typeface getFontAwesome(Context context) { - if(fontAwesome == null) - fontAwesome = Typeface.createFromAsset(context.getAssets(), "fontawesome-webfont.ttf"); + if(fontAwesome == null) fontAwesome = + Typeface.createFromAsset(context.getAssets(), + "fontawesome-webfont.ttf"); return fontAwesome; } @@ -69,4 +73,18 @@ public abstract class InterfaceUtils return false; } + public static void setupEditorAction(@NonNull ViewGroup parent, + @NonNull TextView.OnEditorActionListener listener) + { + for (int i = 0; i < parent.getChildCount(); i++) + { + View child = parent.getChildAt(i); + + if (child instanceof ViewGroup) + setupEditorAction((ViewGroup) child, listener); + + if (child instanceof TextView) + ((TextView) child).setOnEditorActionListener(listener); + } + } } diff --git a/app/src/main/java/org/isoron/uhabits/widgets/CheckmarkWidget.java b/app/src/main/java/org/isoron/uhabits/widgets/CheckmarkWidget.java index 3266d16a2..aa57e7371 100644 --- a/app/src/main/java/org/isoron/uhabits/widgets/CheckmarkWidget.java +++ b/app/src/main/java/org/isoron/uhabits/widgets/CheckmarkWidget.java @@ -52,8 +52,8 @@ public class CheckmarkWidget extends BaseWidget { CheckmarkWidgetView view = (CheckmarkWidgetView) v; int color = ColorUtils.getColor(getContext(), habit.getColor()); - int score = habit.getScores().getTodayValue(); - float percentage = (float) score / Score.MAX_VALUE; + double score = habit.getScores().getTodayValue(); + float percentage = (float) score; int checkmark = habit.getCheckmarks().getTodayValue(); view.setPercentage(percentage); diff --git a/app/src/main/res/layout/edit_habit.xml b/app/src/main/res/layout/edit_habit.xml index 007fe9500..da5357c7b 100644 --- a/app/src/main/res/layout/edit_habit.xml +++ b/app/src/main/res/layout/edit_habit.xml @@ -22,113 +22,33 @@ style="@style/dialogForm" xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" - tools:context=".activities.habits.edit.BaseDialog" + tools:context=".activities.habits.edit.EditHabitDialog" tools:ignore="MergeRootFrame"> - + android:layout_width="match_parent" + android:layout_height="wrap_content"/> - + - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + android:layout_width="match_parent" + android:layout_height="wrap_content"/> - + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/edit_habit_name.xml b/app/src/main/res/layout/edit_habit_name.xml new file mode 100644 index 000000000..70ad45cdc --- /dev/null +++ b/app/src/main/res/layout/edit_habit_name.xml @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/edit_habit_reminder.xml b/app/src/main/res/layout/edit_habit_reminder.xml new file mode 100644 index 000000000..d6dcdb10d --- /dev/null +++ b/app/src/main/res/layout/edit_habit_reminder.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/edit_habit_target.xml b/app/src/main/res/layout/edit_habit_target.xml new file mode 100644 index 000000000..d332cc822 --- /dev/null +++ b/app/src/main/res/layout/edit_habit_target.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/list_habits_button_preview.xml b/app/src/main/res/layout/list_habits_button_preview.xml new file mode 100644 index 000000000..5f2724a07 --- /dev/null +++ b/app/src/main/res/layout/list_habits_button_preview.xml @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/list_habits_card.xml b/app/src/main/res/layout/list_habits_card.xml index 7b8d0f54d..6109c7472 100644 --- a/app/src/main/res/layout/list_habits_card.xml +++ b/app/src/main/res/layout/list_habits_card.xml @@ -46,6 +46,11 @@ android:id="@+id/checkmarkPanel" style="@style/ListHabits.CheckmarkPanel"/> + + \ No newline at end of file diff --git a/app/src/main/res/layout/list_habits_panel_preview.xml b/app/src/main/res/layout/list_habits_panel_preview.xml new file mode 100644 index 000000000..e20ca4438 --- /dev/null +++ b/app/src/main/res/layout/list_habits_panel_preview.xml @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/number_picker_dialog.xml b/app/src/main/res/layout/number_picker_dialog.xml new file mode 100644 index 000000000..42f268591 --- /dev/null +++ b/app/src/main/res/layout/number_picker_dialog.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/show_habit_bar.xml b/app/src/main/res/layout/show_habit_bar.xml new file mode 100644 index 000000000..aeaf8f546 --- /dev/null +++ b/app/src/main/res/layout/show_habit_bar.xml @@ -0,0 +1,37 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/show_habit_history.xml b/app/src/main/res/layout/show_habit_history.xml index 9e89365c3..5763d2647 100644 --- a/app/src/main/res/layout/show_habit_history.xml +++ b/app/src/main/res/layout/show_habit_history.xml @@ -27,7 +27,7 @@ + android:text="@string/calendar"/> + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/show_habit_score.xml b/app/src/main/res/layout/show_habit_score.xml index c3e7f5d5b..34b946b82 100644 --- a/app/src/main/res/layout/show_habit_score.xml +++ b/app/src/main/res/layout/show_habit_score.xml @@ -40,7 +40,7 @@ android:layout_alignParentLeft="true" android:layout_alignParentStart="true" android:layout_alignParentTop="true" - android:text="@string/habit_strength"/> + android:text="@string/score"/> @string/custom_frequency + + Yes or No + Number + + @string/check @string/uncheck @@ -77,5 +82,17 @@ 365 + + At least + At most + + + + daily + weekly + montly + + 15 + 100 \ No newline at end of file diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index bb938b840..e8ddfd3df 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -19,7 +19,7 @@ 20dp - 42dp + 48dp 48dp 450dp 8dp diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1e94bea9a..186e46fe0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -19,7 +19,6 @@ --> - Loop Habit Tracker Habits Settings @@ -204,4 +203,16 @@ By score Download Export + Press-and-hold to change the + value + Change value + Calendar + Unit + Count + This field should not be blank + e.g. How many steps did you walk today? + e.g. steps + e.g. Did you exercise today? + Question + Target \ No newline at end of file diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index a456262a8..86435f56f 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -256,4 +256,8 @@ + + diff --git a/app/src/main/res/values/styles_dialog.xml b/app/src/main/res/values/styles_dialog.xml index 784a52886..3dc983310 100644 --- a/app/src/main/res/values/styles_dialog.xml +++ b/app/src/main/res/values/styles_dialog.xml @@ -89,7 +89,6 @@ \ No newline at end of file diff --git a/app/src/test/java/org/isoron/uhabits/activities/habits/list/ListHabitsScreenTest.java b/app/src/test/java/org/isoron/uhabits/activities/habits/list/ListHabitsScreenTest.java index da6c04587..425ca4f71 100644 --- a/app/src/test/java/org/isoron/uhabits/activities/habits/list/ListHabitsScreenTest.java +++ b/app/src/test/java/org/isoron/uhabits/activities/habits/list/ListHabitsScreenTest.java @@ -61,8 +61,6 @@ public class ListHabitsScreenTest extends BaseUnitTest private ConfirmDeleteDialogFactory confirmDeleteDialogFactory; - private CreateHabitDialogFactory createHabitDialogFactory; - private FilePickerDialogFactory filePickerDialogFactory; private IntentFactory intentFactory; @@ -73,7 +71,7 @@ public class ListHabitsScreenTest extends BaseUnitTest private ColorPickerDialogFactory colorPickerDialogFactory; - private EditHabitDialogFactory editHabitDialogFactory; + private EditHabitDialogFactory dialogFactory; private ThemeSwitcher themeSwitcher; @@ -92,15 +90,13 @@ public class ListHabitsScreenTest extends BaseUnitTest intentFactory = mock(IntentFactory.class); themeSwitcher = mock(ThemeSwitcher.class); confirmDeleteDialogFactory = mock(ConfirmDeleteDialogFactory.class); - createHabitDialogFactory = mock(CreateHabitDialogFactory.class); filePickerDialogFactory = mock(FilePickerDialogFactory.class); colorPickerDialogFactory = mock(ColorPickerDialogFactory.class); - editHabitDialogFactory = mock(EditHabitDialogFactory.class); + dialogFactory = mock(EditHabitDialogFactory.class); screen = spy(new ListHabitsScreen(activity, commandRunner, dirFinder, rootView, intentFactory, themeSwitcher, confirmDeleteDialogFactory, - createHabitDialogFactory, filePickerDialogFactory, - colorPickerDialogFactory, editHabitDialogFactory)); + filePickerDialogFactory, colorPickerDialogFactory, dialogFactory)); doNothing().when(screen).showMessage(anyInt()); @@ -111,15 +107,38 @@ public class ListHabitsScreenTest extends BaseUnitTest intent = mock(Intent.class); } +// @Test +// public void testCreateHabitScreen() +// { +// CreateBooleanHabitDialog dialog = mock(CreateBooleanHabitDialog.class); +// when(createHabitDialogFactory.create()).thenReturn(dialog); +// +// screen.showCreateHabitScreen(); +// +// verify(activity).showDialog(eq(dialog), any()); +// } + @Test - public void testCreateHabitScreen() + public void testOnAttached() { - CreateHabitDialog dialog = mock(CreateHabitDialog.class); - when(createHabitDialogFactory.create()).thenReturn(dialog); + screen.onAttached(); + verify(commandRunner).addListener(screen); + } - screen.showCreateHabitScreen(); + @Test + public void testOnCommand() + { + Command c = mock(Command.class); + when(c.getExecuteStringId()).thenReturn(R.string.toast_habit_deleted); + screen.onCommandExecuted(c, null); + verify(screen).showMessage(R.string.toast_habit_deleted); + } - verify(activity).showDialog(eq(dialog), any()); + @Test + public void testOnDetach() + { + screen.onDettached(); + verify(commandRunner).removeListener(screen); } @Test @@ -190,7 +209,7 @@ public class ListHabitsScreenTest extends BaseUnitTest public void testShowEditHabitScreen() { EditHabitDialog dialog = mock(EditHabitDialog.class); - when(editHabitDialogFactory.create(habit)).thenReturn(dialog); + when(dialogFactory.edit(habit)).thenReturn(dialog); screen.showEditHabitScreen(habit); verify(activity).showDialog(eq(dialog), any()); @@ -260,27 +279,4 @@ public class ListHabitsScreenTest extends BaseUnitTest verify(themeSwitcher).toggleNightMode(); verify(activity).restartWithFade(); } - - @Test - public void testOnAttached() - { - screen.onAttached(); - verify(commandRunner).addListener(screen); - } - - @Test - public void testOnDetach() - { - screen.onDettached(); - verify(commandRunner).removeListener(screen); - } - - @Test - public void testOnCommand() - { - Command c = mock(Command.class); - when(c.getExecuteStringId()).thenReturn(R.string.toast_habit_deleted); - screen.onCommandExecuted(c, null); - verify(screen).showMessage(R.string.toast_habit_deleted); - } } \ No newline at end of file diff --git a/app/src/test/java/org/isoron/uhabits/activities/habits/list/model/HabitCardListCacheTest.java b/app/src/test/java/org/isoron/uhabits/activities/habits/list/model/HabitCardListCacheTest.java index 2db4bf71a..b9965ad6c 100644 --- a/app/src/test/java/org/isoron/uhabits/activities/habits/list/model/HabitCardListCacheTest.java +++ b/app/src/test/java/org/isoron/uhabits/activities/habits/list/model/HabitCardListCacheTest.java @@ -106,7 +106,7 @@ public class HabitCardListCacheTest extends BaseUnitTest Habit h = habitList.getByPosition(3); assertNotNull(h.getId()); - int score = h.getScores().getTodayValue(); + double score = h.getScores().getTodayValue(); assertThat(cache.getHabitByPosition(3), equalTo(h)); assertThat(cache.getScore(h.getId()), equalTo(score)); diff --git a/app/src/test/java/org/isoron/uhabits/commands/CreateRepetitionCommandTest.java b/app/src/test/java/org/isoron/uhabits/commands/CreateRepetitionCommandTest.java new file mode 100644 index 000000000..45816c559 --- /dev/null +++ b/app/src/test/java/org/isoron/uhabits/commands/CreateRepetitionCommandTest.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2016 Álinson Santos Xavier + * + * 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 . + */ + +package org.isoron.uhabits.commands; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.utils.*; +import org.junit.*; + +import static junit.framework.Assert.*; +import static org.isoron.uhabits.models.Checkmark.CHECKED_EXPLICITLY; + +public class CreateRepetitionCommandTest extends BaseUnitTest +{ + + private CreateRepetitionCommand command; + + private Habit habit; + + private long today; + + @Override + @Before + public void setUp() + { + super.setUp(); + + habit = fixtures.createShortHabit(); + + today = DateUtils.getStartOfToday(); + command = new CreateRepetitionCommand(habit, today, 100); + } + + @Test + public void testExecuteUndoRedo() + { + RepetitionList reps = habit.getRepetitions(); + + Repetition rep = reps.getByTimestamp(today); + assertNotNull(rep); + assertEquals(CHECKED_EXPLICITLY, rep.getValue()); + + command.execute(); + rep = reps.getByTimestamp(today); + assertNotNull(rep); + assertEquals(100, rep.getValue()); + + command.undo(); + rep = reps.getByTimestamp(today); + assertNotNull(rep); + assertEquals(CHECKED_EXPLICITLY, rep.getValue()); + } +} diff --git a/app/src/test/java/org/isoron/uhabits/commands/EditHabitCommandTest.java b/app/src/test/java/org/isoron/uhabits/commands/EditHabitCommandTest.java index 1b6a5f447..0345d34c5 100644 --- a/app/src/test/java/org/isoron/uhabits/commands/EditHabitCommandTest.java +++ b/app/src/test/java/org/isoron/uhabits/commands/EditHabitCommandTest.java @@ -58,7 +58,7 @@ public class EditHabitCommandTest extends BaseUnitTest command = new EditHabitCommand(modelFactory, habitList, habit, modified); - int originalScore = habit.getScores().getTodayValue(); + double originalScore = habit.getScores().getTodayValue(); assertThat(habit.getName(), equalTo("original")); command.execute(); @@ -81,13 +81,13 @@ public class EditHabitCommandTest extends BaseUnitTest command = new EditHabitCommand(modelFactory, habitList, habit, modified); - int originalScore = habit.getScores().getTodayValue(); + double originalScore = habit.getScores().getTodayValue(); assertThat(habit.getName(), equalTo("original")); command.execute(); assertThat(habit.getName(), equalTo("modified")); assertThat(habit.getScores().getTodayValue(), - greaterThan(originalScore)); + lessThan(originalScore)); command.undo(); assertThat(habit.getName(), equalTo("original")); @@ -96,6 +96,6 @@ public class EditHabitCommandTest extends BaseUnitTest command.execute(); assertThat(habit.getName(), equalTo("modified")); assertThat(habit.getScores().getTodayValue(), - greaterThan(originalScore)); + lessThan(originalScore)); } } diff --git a/app/src/test/java/org/isoron/uhabits/models/ScoreListTest.java b/app/src/test/java/org/isoron/uhabits/models/ScoreListTest.java index 08b999114..f0295ad08 100644 --- a/app/src/test/java/org/isoron/uhabits/models/ScoreListTest.java +++ b/app/src/test/java/org/isoron/uhabits/models/ScoreListTest.java @@ -28,9 +28,12 @@ import java.util.*; import static org.hamcrest.CoreMatchers.*; import static org.hamcrest.MatcherAssert.*; +import static org.hamcrest.number.IsCloseTo.closeTo; public class ScoreListTest extends BaseUnitTest { + private static final double E = 1e-6; + private Habit habit; @Override @@ -46,43 +49,40 @@ public class ScoreListTest extends BaseUnitTest { toggleRepetitions(0, 20); - int expectedValues[] = { - 12629351, - 12266245, - 11883254, - 11479288, - 11053198, - 10603773, - 10129735, - 9629735, - 9102352, - 8546087, - 7959357, - 7340494, - 6687738, - 5999234, - 5273023, - 4507040, - 3699107, - 2846927, - 1948077, - 1000000 + double expectedValues[] = { + 0.655747, + 0.636894, + 0.617008, + 0.596033, + 0.573910, + 0.550574, + 0.525961, + 0.500000, + 0.472617, + 0.443734, + 0.413270, + 0.381137, + 0.347244, + 0.311495, + 0.273788, + 0.234017, + 0.192067, + 0.147820, + 0.101149, + 0.051922, }; - int actualValues[] = new int[expectedValues.length]; - int i = 0; for (Score s : habit.getScores()) - actualValues[i++] = s.getValue(); - - assertThat(actualValues, equalTo(expectedValues)); + assertThat(s.getValue(), closeTo(expectedValues[i++], E)); } @Test public void test_getTodayValue() { toggleRepetitions(0, 20); - assertThat(habit.getScores().getTodayValue(), equalTo(12629351)); + double actual = habit.getScores().getTodayValue(); + assertThat(actual, closeTo(0.655747, E)); } @Test @@ -90,37 +90,37 @@ public class ScoreListTest extends BaseUnitTest { toggleRepetitions(0, 20); - int expectedValues[] = { - 12629351, - 12266245, - 11883254, - 11479288, - 11053198, - 10603773, - 10129735, - 9629735, - 9102352, - 8546087, - 7959357, - 7340494, - 6687738, - 5999234, - 5273023, - 4507040, - 3699107, - 2846927, - 1948077, - 1000000, - 0, - 0, - 0 + double expectedValues[] = { + 0.655747, + 0.636894, + 0.617008, + 0.596033, + 0.573910, + 0.550574, + 0.525961, + 0.500000, + 0.472617, + 0.443734, + 0.413270, + 0.381137, + 0.347244, + 0.311495, + 0.273788, + 0.234017, + 0.192067, + 0.147820, + 0.101149, + 0.051922, + 0.000000, + 0.000000, + 0.000000 }; ScoreList scores = habit.getScores(); long current = DateUtils.getStartOfToday(); - for (int expectedValue : expectedValues) + for (double expectedValue : expectedValues) { - assertThat(scores.getValue(current), equalTo(expectedValue)); + assertThat(scores.getValue(current), closeTo(expectedValue, E)); current -= DateUtils.millisecondsInOneDay; } } @@ -133,23 +133,23 @@ public class ScoreListTest extends BaseUnitTest habit.getScores().groupBy(DateUtils.TruncateField.MONTH); assertThat(list.size(), equalTo(5)); - assertThat(list.get(0).getValue(), equalTo(14634077)); - assertThat(list.get(1).getValue(), equalTo(12969133)); - assertThat(list.get(2).getValue(), equalTo(10595391)); + assertThat(list.get(0).getValue(), closeTo(0.549096, E)); + assertThat(list.get(1).getValue(), closeTo(0.480098, E)); + assertThat(list.get(2).getValue(), closeTo(0.377885, E)); } @Test public void test_invalidateNewerThan() { - assertThat(habit.getScores().getTodayValue(), equalTo(0)); + assertThat(habit.getScores().getTodayValue(), closeTo(0.0, E)); toggleRepetitions(0, 2); - assertThat(habit.getScores().getTodayValue(), equalTo(1948077)); + assertThat(habit.getScores().getTodayValue(), closeTo(0.101149, E)); habit.setFrequency(new Frequency(1, 2)); habit.getScores().invalidateNewerThan(0); - assertThat(habit.getScores().getTodayValue(), equalTo(1974654)); + assertThat(habit.getScores().getTodayValue(), closeTo(0.051922, E)); } @Test @@ -157,16 +157,16 @@ public class ScoreListTest extends BaseUnitTest { Habit habit = fixtures.createShortHabit(); - String expectedCSV = "2015-01-25,0.2649\n" + - "2015-01-24,0.2205\n" + - "2015-01-23,0.2283\n" + - "2015-01-22,0.2364\n" + - "2015-01-21,0.1909\n" + - "2015-01-20,0.1439\n" + - "2015-01-19,0.0952\n" + - "2015-01-18,0.0986\n" + - "2015-01-17,0.1021\n" + - "2015-01-16,0.0519\n"; + String expectedCSV = "2015-01-25,0.2372\n" + + "2015-01-24,0.2096\n" + + "2015-01-23,0.2172\n" + + "2015-01-22,0.1889\n" + + "2015-01-21,0.1595\n" + + "2015-01-20,0.1291\n" + + "2015-01-19,0.0976\n" + + "2015-01-18,0.1011\n" + + "2015-01-17,0.0686\n" + + "2015-01-16,0.0349\n"; StringWriter writer = new StringWriter(); habit.getScores().writeCSV(writer); @@ -185,14 +185,17 @@ public class ScoreListTest extends BaseUnitTest long from = today - 4 * day; long to = today - 2 * day; - int[] expected = { - 11883254, - 11479288, - 11053198, + double[] expected = { + 0.617008, + 0.596033, + 0.573909, }; - int[] actual = habit.getScores().getValues(from, to); - assertThat(actual, equalTo(expected)); + double[] actual = habit.getScores().getValues(from, to); + assertThat(actual.length, equalTo(expected.length)); + + for(int i = 0; i < actual.length; i++) + assertThat(actual[i], closeTo(expected[i], E)); } private void toggleRepetitions(final int from, final int to) diff --git a/app/src/test/java/org/isoron/uhabits/models/ScoreTest.java b/app/src/test/java/org/isoron/uhabits/models/ScoreTest.java index 34b400150..0a94ad036 100644 --- a/app/src/test/java/org/isoron/uhabits/models/ScoreTest.java +++ b/app/src/test/java/org/isoron/uhabits/models/ScoreTest.java @@ -19,15 +19,17 @@ package org.isoron.uhabits.models; -import org.isoron.uhabits.BaseUnitTest; -import org.junit.Before; -import org.junit.Test; +import org.isoron.uhabits.*; +import org.junit.*; -import static org.hamcrest.CoreMatchers.equalTo; -import static org.junit.Assert.assertThat; +import static org.hamcrest.number.IsCloseTo.*; +import static org.isoron.uhabits.models.Score.*; +import static org.junit.Assert.*; public class ScoreTest extends BaseUnitTest { + private static final double E = 1e-6; + @Override @Before public void setUp() @@ -38,46 +40,30 @@ public class ScoreTest extends BaseUnitTest @Test public void test_compute_withDailyHabit() { - int checkmark = Checkmark.UNCHECKED; - assertThat(Score.compute(1, 0, checkmark), equalTo(0)); - assertThat(Score.compute(1, 5000000, checkmark), equalTo(4740387)); - assertThat(Score.compute(1, 10000000, checkmark), equalTo(9480775)); - assertThat(Score.compute(1, Score.MAX_VALUE, checkmark), - equalTo(18259478)); - - checkmark = Checkmark.CHECKED_IMPLICITLY; - assertThat(Score.compute(1, 0, checkmark), equalTo(0)); - assertThat(Score.compute(1, 5000000, checkmark), equalTo(4740387)); - assertThat(Score.compute(1, 10000000, checkmark), equalTo(9480775)); - assertThat(Score.compute(1, Score.MAX_VALUE, checkmark), - equalTo(18259478)); + int check = 1; + double freq = 1.0; + assertThat(compute(freq, 0, check), closeTo(0.051922, E)); + assertThat(compute(freq, 0.5, check), closeTo(0.525961, E)); + assertThat(compute(freq, 0.75, check), closeTo(0.762981, E)); - checkmark = Checkmark.CHECKED_EXPLICITLY; - assertThat(Score.compute(1, 0, checkmark), equalTo(1000000)); - assertThat(Score.compute(1, 5000000, checkmark), equalTo(5740387)); - assertThat(Score.compute(1, 10000000, checkmark), equalTo(10480775)); - assertThat(Score.compute(1, Score.MAX_VALUE, checkmark), - equalTo(Score.MAX_VALUE)); + check = 0; + assertThat(compute(freq, 0, check), closeTo(0, E)); + assertThat(compute(freq, 0.5, check), closeTo(0.474039, E)); + assertThat(compute(freq, 0.75, check), closeTo(0.711058, E)); } @Test public void test_compute_withNonDailyHabit() { - int checkmark = Checkmark.CHECKED_EXPLICITLY; - assertThat(Score.compute(1 / 3.0, 0, checkmark), equalTo(1000000)); - assertThat(Score.compute(1 / 3.0, 5000000, checkmark), - equalTo(5916180)); - assertThat(Score.compute(1 / 3.0, 10000000, checkmark), - equalTo(10832360)); - assertThat(Score.compute(1 / 3.0, Score.MAX_VALUE, checkmark), - equalTo(Score.MAX_VALUE)); + int check = 1; + double freq = 1 / 3.0; + assertThat(compute(freq, 0, check), closeTo(0.017616, E)); + assertThat(compute(freq, 0.5, check), closeTo(0.508808, E)); + assertThat(compute(freq, 0.75, check), closeTo(0.754404, E)); - assertThat(Score.compute(1 / 7.0, 0, checkmark), equalTo(1000000)); - assertThat(Score.compute(1 / 7.0, 5000000, checkmark), - equalTo(5964398)); - assertThat(Score.compute(1 / 7.0, 10000000, checkmark), - equalTo(10928796)); - assertThat(Score.compute(1 / 7.0, Score.MAX_VALUE, checkmark), - equalTo(Score.MAX_VALUE)); + check = 0; + assertThat(compute(freq, 0, check), closeTo(0.0, E)); + assertThat(compute(freq, 0.5, check), closeTo(0.491192, E)); + assertThat(compute(freq, 0.75, check), closeTo(0.736788, E)); } } diff --git a/app/src/test/java/org/isoron/uhabits/models/WeekdayListTest.java b/app/src/test/java/org/isoron/uhabits/models/WeekdayListTest.java index c8493cd87..406b6e39b 100644 --- a/app/src/test/java/org/isoron/uhabits/models/WeekdayListTest.java +++ b/app/src/test/java/org/isoron/uhabits/models/WeekdayListTest.java @@ -22,6 +22,7 @@ package org.isoron.uhabits.models; import org.isoron.uhabits.*; import org.junit.*; +import static junit.framework.Assert.*; import static org.hamcrest.MatcherAssert.*; import static org.hamcrest.core.IsEqual.*; @@ -48,6 +49,8 @@ public class WeekdayListTest extends BaseUnitTest public void testEmpty() { WeekdayList list = new WeekdayList(0); - assertThat(list.toArray(), equalTo(WeekdayList.EVERY_DAY.toArray())); + assertTrue(list.isEmpty()); + + assertFalse(WeekdayList.EVERY_DAY.isEmpty()); } }