diff --git a/app/src/main/java/org/isoron/uhabits/HabitBroadcastReceiver.java b/app/src/main/java/org/isoron/uhabits/HabitBroadcastReceiver.java index 846760e87..3942f8e52 100644 --- a/app/src/main/java/org/isoron/uhabits/HabitBroadcastReceiver.java +++ b/app/src/main/java/org/isoron/uhabits/HabitBroadcastReceiver.java @@ -100,7 +100,7 @@ public class HabitBroadcastReceiver extends BroadcastReceiver Long timestamp = intent.getLongExtra("timestamp", DateHelper.getStartOfToday()); Habit habit = Habit.get(ContentUris.parseId(data)); - habit.toggleRepetition(timestamp); + habit.repetitions.toggle(timestamp); habit.save(); dismissNotification(context, habit); @@ -137,7 +137,7 @@ public class HabitBroadcastReceiver extends BroadcastReceiver Long timestamp = intent.getLongExtra("timestamp", DateHelper.getStartOfToday()); Long reminderTime = intent.getLongExtra("reminderTime", DateHelper.getStartOfToday()); - if (habit.hasImplicitRepToday()) return; + if (habit.repetitions.hasImplicitRepToday()) return; habit.highlight = 1; habit.save(); 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 f70a841b4..53b4bbf1b 100644 --- a/app/src/main/java/org/isoron/uhabits/commands/EditHabitCommand.java +++ b/app/src/main/java/org/isoron/uhabits/commands/EditHabitCommand.java @@ -44,9 +44,9 @@ public class EditHabitCommand extends Command habit.save(); if (hasIntervalChanged) { - habit.deleteCheckmarksNewerThan(0); - habit.deleteStreaksNewerThan(0); - habit.deleteScoresNewerThan(0); + habit.checkmarks.deleteNewerThan(0); + habit.streaks.deleteNewerThan(0); + habit.scores.deleteNewerThan(0); } } @@ -57,9 +57,9 @@ public class EditHabitCommand extends Command habit.save(); if (hasIntervalChanged) { - habit.deleteCheckmarksNewerThan(0); - habit.deleteStreaksNewerThan(0); - habit.deleteScoresNewerThan(0); + habit.checkmarks.deleteNewerThan(0); + habit.streaks.deleteNewerThan(0); + habit.scores.deleteNewerThan(0); } } diff --git a/app/src/main/java/org/isoron/uhabits/commands/ToggleRepetitionCommand.java b/app/src/main/java/org/isoron/uhabits/commands/ToggleRepetitionCommand.java index 53a95438d..c19ff5a5d 100644 --- a/app/src/main/java/org/isoron/uhabits/commands/ToggleRepetitionCommand.java +++ b/app/src/main/java/org/isoron/uhabits/commands/ToggleRepetitionCommand.java @@ -33,7 +33,7 @@ public class ToggleRepetitionCommand extends Command @Override public void execute() { - habit.toggleRepetition(offset); + habit.repetitions.toggle(offset); } @Override diff --git a/app/src/main/java/org/isoron/uhabits/fragments/ListHabitsFragment.java b/app/src/main/java/org/isoron/uhabits/fragments/ListHabitsFragment.java index 2b1df0750..37c0e4e13 100644 --- a/app/src/main/java/org/isoron/uhabits/fragments/ListHabitsFragment.java +++ b/app/src/main/java/org/isoron/uhabits/fragments/ListHabitsFragment.java @@ -75,6 +75,7 @@ import org.isoron.uhabits.helpers.ReminderHelper; import org.isoron.uhabits.io.CSVExporter; import org.isoron.uhabits.loaders.HabitListLoader; import org.isoron.uhabits.models.Habit; +import org.isoron.uhabits.models.Score; import java.io.File; import java.util.Date; @@ -741,12 +742,12 @@ public class ListHabitsFragment extends Fragment { int score = loader.scores.get(habit.getId()); - if (score < Habit.HALF_STAR_CUTOFF) + if (score < Score.HALF_STAR_CUTOFF) { tvStar.setText(getString(R.string.fa_star_o)); tvStar.setTextColor(INACTIVE_COLOR); } - else if (score < Habit.FULL_STAR_CUTOFF) + else if (score < Score.FULL_STAR_CUTOFF) { tvStar.setText(getString(R.string.fa_star_half_o)); tvStar.setTextColor(INACTIVE_COLOR); diff --git a/app/src/main/java/org/isoron/uhabits/fragments/ShowHabitFragment.java b/app/src/main/java/org/isoron/uhabits/fragments/ShowHabitFragment.java index de2cc311d..812f832ef 100644 --- a/app/src/main/java/org/isoron/uhabits/fragments/ShowHabitFragment.java +++ b/app/src/main/java/org/isoron/uhabits/fragments/ShowHabitFragment.java @@ -34,6 +34,7 @@ import org.isoron.uhabits.R; import org.isoron.uhabits.ShowHabitActivity; import org.isoron.uhabits.helpers.ReminderHelper; import org.isoron.uhabits.models.Habit; +import org.isoron.uhabits.models.Score; import org.isoron.uhabits.views.HabitHistoryView; import org.isoron.uhabits.views.HabitScoreView; import org.isoron.uhabits.views.HabitStreakView; @@ -58,7 +59,7 @@ public class ShowHabitFragment extends Fragment implements DialogHelper.OnSavedL activity = (ShowHabitActivity) getActivity(); habit = activity.habit; - habit.updateCheckmarks(); + habit.checkmarks.rebuild(); if (android.os.Build.VERSION.SDK_INT >= 21) { @@ -81,7 +82,7 @@ public class ShowHabitFragment extends Fragment implements DialogHelper.OnSavedL tvStreaks.setTextColor(habit.color); scoreRing.setColor(habit.color); - scoreRing.setPercentage((float) habit.getScore() / Habit.MAX_SCORE); + scoreRing.setPercentage((float) habit.scores.getNewestValue() / Score.MAX_SCORE); streakView.setHabit(habit); scoreView.setHabit(habit); historyView.setHabit(habit); diff --git a/app/src/main/java/org/isoron/uhabits/io/CSVExporter.java b/app/src/main/java/org/isoron/uhabits/io/CSVExporter.java index ee0bde879..1feb81bb2 100644 --- a/app/src/main/java/org/isoron/uhabits/io/CSVExporter.java +++ b/app/src/main/java/org/isoron/uhabits/io/CSVExporter.java @@ -8,6 +8,7 @@ import com.activeandroid.Cache; import org.isoron.helpers.DateHelper; import org.isoron.uhabits.models.Habit; +import org.isoron.uhabits.models.Score; import java.io.File; import java.io.FileInputStream; @@ -54,7 +55,7 @@ public class CSVExporter public String formatScore(int score) { - return String.format("%.2f", ((float) score) / Habit.MAX_SCORE); + return String.format("%.2f", ((float) score) / Score.MAX_SCORE); } private void writeScores(String dirPath, Habit habit) throws IOException diff --git a/app/src/main/java/org/isoron/uhabits/loaders/HabitListLoader.java b/app/src/main/java/org/isoron/uhabits/loaders/HabitListLoader.java index bd7443626..204b5e243 100644 --- a/app/src/main/java/org/isoron/uhabits/loaders/HabitListLoader.java +++ b/app/src/main/java/org/isoron/uhabits/loaders/HabitListLoader.java @@ -144,8 +144,8 @@ public class HabitListLoader if (isCancelled()) return null; Long id = h.getId(); - newScores.put(id, h.getScore()); - newCheckmarks.put(id, h.getCheckmarks(dateFrom, dateTo)); + newScores.put(id, h.scores.getNewestValue()); + newCheckmarks.put(id, h.checkmarks.getValues(dateFrom, dateTo)); publishProgress(current++, newHabits.size()); } @@ -213,8 +213,8 @@ public class HabitListLoader Habit h = Habit.get(id); habits.put(id, h); - scores.put(id, h.getScore()); - checkmarks.put(id, h.getCheckmarks(dateFrom, dateTo)); + scores.put(id, h.scores.getNewestValue()); + checkmarks.put(id, h.checkmarks.getValues(dateFrom, dateTo)); return null; } diff --git a/app/src/main/java/org/isoron/uhabits/models/CheckmarkList.java b/app/src/main/java/org/isoron/uhabits/models/CheckmarkList.java new file mode 100644 index 000000000..61187ea30 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/models/CheckmarkList.java @@ -0,0 +1,175 @@ +/* Copyright (C) 2016 Alinson Santos Xavier + * + * This program 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. + * + * This program 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.models; + +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; + +import com.activeandroid.ActiveAndroid; +import com.activeandroid.Cache; +import com.activeandroid.query.Delete; +import com.activeandroid.query.Select; + +import org.isoron.helpers.DateHelper; + +import java.util.List; + +public class CheckmarkList +{ + private Habit habit; + + public CheckmarkList(Habit habit) + { + this.habit = habit; + } + + public void deleteNewerThan(long timestamp) + { + new Delete().from(Checkmark.class) + .where("habit = ?", habit.getId()) + .and("timestamp >= ?", timestamp) + .execute(); + } + + public int[] getValues(Long fromTimestamp, Long toTimestamp) + { + rebuild(); + + if(fromTimestamp > toTimestamp) return new int[0]; + + String query = "select value, timestamp from Checkmarks where " + + "habit = ? and timestamp >= ? and timestamp <= ?"; + + SQLiteDatabase db = Cache.openDatabase(); + String args[] = { habit.getId().toString(), fromTimestamp.toString(), + toTimestamp.toString() }; + Cursor cursor = db.rawQuery(query, args); + + long day = DateHelper.millisecondsInOneDay; + int nDays = (int) ((toTimestamp - fromTimestamp) / day) + 1; + int[] checks = new int[nDays]; + + if (cursor.moveToFirst()) + { + do + { + long timestamp = cursor.getLong(1); + int offset = (int) ((timestamp - fromTimestamp) / day); + checks[nDays - offset - 1] = cursor.getInt(0); + + } while (cursor.moveToNext()); + } + + cursor.close(); + return checks; + } + + public int[] getAllValues() + { + Repetition oldestRep = habit.repetitions.getOldest(); + if(oldestRep == null) return new int[0]; + + Long toTimestamp = DateHelper.getStartOfToday(); + Long fromTimestamp = oldestRep.timestamp; + return getValues(fromTimestamp, toTimestamp); + } + + public void rebuild() + { + long beginning; + long today = DateHelper.getStartOfToday(); + long day = DateHelper.millisecondsInOneDay; + + Checkmark newestCheckmark = getNewest(); + if (newestCheckmark == null) + { + Repetition oldestRep = habit.repetitions.getOldest(); + if (oldestRep == null) return; + + beginning = oldestRep.timestamp; + } + else + { + beginning = newestCheckmark.timestamp + day; + } + + if (beginning > today) return; + + long beginningExtended = beginning - (long) (habit.freqDen) * day; + List reps = habit.repetitions.selectFromTo(beginningExtended, today).execute(); + + int nDays = (int) ((today - beginning) / day) + 1; + int nDaysExtended = (int) ((today - beginningExtended) / day) + 1; + + int checks[] = new int[nDaysExtended]; + + // explicit checks + for (Repetition rep : reps) + { + int offset = (int) ((rep.timestamp - beginningExtended) / day); + checks[nDaysExtended - offset - 1] = 2; + } + + // implicit checks + for (int i = 0; i < nDays; i++) + { + int counter = 0; + + for (int j = 0; j < habit.freqDen; j++) + if (checks[i + j] == 2) counter++; + + if (counter >= habit.freqNum) checks[i] = Math.max(checks[i], 1); + } + + ActiveAndroid.beginTransaction(); + + try + { + for (int i = 0; i < nDays; i++) + { + Checkmark c = new Checkmark(); + c.habit = habit; + c.timestamp = today - i * day; + c.value = checks[i]; + c.save(); + } + + ActiveAndroid.setTransactionSuccessful(); + } finally + { + ActiveAndroid.endTransaction(); + } + } + + public Checkmark getNewest() + { + return new Select().from(Checkmark.class) + .where("habit = ?", habit.getId()) + .orderBy("timestamp desc") + .limit(1) + .executeSingle(); + } + + public int getCurrentValue() + { + rebuild(); + Checkmark c = getNewest(); + + if(c != null) return c.value; + else return 0; + } +} 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 23cc5296f..5bcc696f5 100644 --- a/app/src/main/java/org/isoron/uhabits/models/Habit.java +++ b/app/src/main/java/org/isoron/uhabits/models/Habit.java @@ -17,12 +17,9 @@ package org.isoron.uhabits.models; import android.annotation.SuppressLint; -import android.database.Cursor; -import android.database.sqlite.SQLiteDatabase; import android.net.Uri; import com.activeandroid.ActiveAndroid; -import com.activeandroid.Cache; import com.activeandroid.Model; import com.activeandroid.annotation.Column; import com.activeandroid.annotation.Table; @@ -33,21 +30,12 @@ import com.activeandroid.query.Update; import com.activeandroid.util.SQLiteUtils; import org.isoron.helpers.ColorHelper; -import org.isoron.helpers.Command; -import org.isoron.helpers.DateHelper; -import org.isoron.uhabits.R; -import java.util.ArrayList; import java.util.List; @Table(name = "Habits") public class Habit extends Model { - - public static final int HALF_STAR_CUTOFF = 9629750; - public static final int FULL_STAR_CUTOFF = 15407600; - public static final int MAX_SCORE = 19259500; - @Column(name = "name") public String name; @@ -81,20 +69,35 @@ public class Habit extends Model @Column(name = "archived") public Integer archived; + public StreakList streaks; + public ScoreList scores; + public RepetitionList repetitions; + public CheckmarkList checkmarks; + public Habit(Habit model) { copyAttributes(model); + initializeLists(); } public Habit() { this.color = ColorHelper.palette[5]; - this.position = Habit.getCount(); + this.position = Habit.count(); this.highlight = 0; this.archived = 0; this.freqDen = 7; this.freqNum = 3; this.reminderDays = 127; + initializeLists(); + } + + private void initializeLists() + { + streaks = new StreakList(this); + scores = new ScoreList(this); + repetitions = new RepetitionList(this); + checkmarks = new CheckmarkList(this); } public static Habit get(Long id) @@ -124,7 +127,7 @@ public class Habit extends Model return new Select().from(Habit.class).orderBy("position"); } - public static int getCount() + public static int count() { return select().count(); } @@ -177,7 +180,8 @@ public class Habit extends Model } ActiveAndroid.setTransactionSuccessful(); - } finally + } + finally { ActiveAndroid.endTransaction(); } @@ -205,22 +209,6 @@ public class Habit extends Model Habit.updateId(getId(), id); } - protected From selectReps() - { - return new Select().from(Repetition.class).where("habit = ?", getId()).orderBy("timestamp"); - } - - protected From selectRepsFromTo(long timeFrom, long timeTo) - { - return selectReps().and("timestamp >= ?", timeFrom).and("timestamp <= ?", timeTo); - } - - public boolean hasRep(long timestamp) - { - int count = selectReps().where("timestamp = ?", timestamp).count(); - return (count > 0); - } - public void cascadeDelete() { Long id = getId(); @@ -242,209 +230,6 @@ public class Habit extends Model } } - public void deleteReps(long timestamp) - { - new Delete().from(Repetition.class) - .where("habit = ?", getId()) - .and("timestamp = ?", timestamp) - .execute(); - } - - public void deleteCheckmarksNewerThan(long timestamp) - { - new Delete().from(Checkmark.class) - .where("habit = ?", getId()) - .and("timestamp >= ?", timestamp) - .execute(); - } - - public void deleteStreaksNewerThan(long timestamp) - { - new Delete().from(Streak.class) - .where("habit = ?", getId()) - .and("end >= ?", timestamp - DateHelper.millisecondsInOneDay) - .execute(); - } - - public int[] getCheckmarks(Long fromTimestamp, Long toTimestamp) - { - updateCheckmarks(); - - if(fromTimestamp > toTimestamp) return new int[0]; - - String query = "select value, timestamp from Checkmarks where " + - "habit = ? and timestamp >= ? and timestamp <= ?"; - - SQLiteDatabase db = Cache.openDatabase(); - String args[] = {getId().toString(), fromTimestamp.toString(), toTimestamp.toString()}; - Cursor cursor = db.rawQuery(query, args); - - long day = DateHelper.millisecondsInOneDay; - int nDays = (int) ((toTimestamp - fromTimestamp) / day) + 1; - int[] checks = new int[nDays]; - - if (cursor.moveToFirst()) - { - do - { - long timestamp = cursor.getLong(1); - int offset = (int) ((timestamp - fromTimestamp) / day); - checks[nDays - offset - 1] = cursor.getInt(0); - - } while (cursor.moveToNext()); - } - - cursor.close(); - return checks; - } - - public int[] getAllCheckmarks() - { - Repetition oldestRep = getOldestRep(); - if(oldestRep == null) return new int[0]; - - Long toTimestamp = DateHelper.getStartOfToday(); - Long fromTimestamp = oldestRep.timestamp; - return getCheckmarks(fromTimestamp, toTimestamp); - } - - public void updateCheckmarks() - { - long beginning; - long today = DateHelper.getStartOfToday(); - long day = DateHelper.millisecondsInOneDay; - - Checkmark newestCheckmark = getNewestCheckmark(); - if (newestCheckmark == null) - { - Repetition oldestRep = getOldestRep(); - if (oldestRep == null) return; - - beginning = oldestRep.timestamp; - } - else - { - beginning = newestCheckmark.timestamp + day; - } - - if (beginning > today) return; - - long beginningExtended = beginning - (long) (freqDen) * day; - List reps = selectRepsFromTo(beginningExtended, today).execute(); - - int nDays = (int) ((today - beginning) / day) + 1; - int nDaysExtended = (int) ((today - beginningExtended) / day) + 1; - - int checks[] = new int[nDaysExtended]; - - // explicit checks - for (Repetition rep : reps) - { - int offset = (int) ((rep.timestamp - beginningExtended) / day); - checks[nDaysExtended - offset - 1] = 2; - } - - // implicit checks - for (int i = 0; i < nDays; i++) - { - int counter = 0; - - for (int j = 0; j < freqDen; j++) - if (checks[i + j] == 2) counter++; - - if (counter >= freqNum) checks[i] = Math.max(checks[i], 1); - } - - ActiveAndroid.beginTransaction(); - - try - { - for (int i = 0; i < nDays; i++) - { - Checkmark c = new Checkmark(); - c.habit = this; - c.timestamp = today - i * day; - c.value = checks[i]; - c.save(); - } - - ActiveAndroid.setTransactionSuccessful(); - } finally - { - ActiveAndroid.endTransaction(); - } - } - - public Checkmark getNewestCheckmark() - { - return new Select().from(Checkmark.class) - .where("habit = ?", getId()) - .orderBy("timestamp desc") - .limit(1) - .executeSingle(); - } - - public int getCurrentCheckmarkStatus() - { - updateCheckmarks(); - Checkmark c = getNewestCheckmark(); - - if(c != null) return c.value; - else return 0; - } - - public int getCurrentStarStatus() - { - int score = getScore(); - - if(score >= FULL_STAR_CUTOFF) return 2; - else if(score >= HALF_STAR_CUTOFF) return 1; - else return 0; - } - - public int getRepsCount(int days) - { - long timeTo = DateHelper.getStartOfToday(); - long timeFrom = timeTo - DateHelper.millisecondsInOneDay * days; - return selectRepsFromTo(timeFrom, timeTo).count(); - } - - public boolean hasImplicitRepToday() - { - long today = DateHelper.getStartOfToday(); - int reps[] = getCheckmarks(today - DateHelper.millisecondsInOneDay, today); - return (reps[0] > 0); - } - - public Repetition getOldestRep() - { - return (Repetition) selectReps().limit(1).executeSingle(); - } - - public Repetition getOldestRepNewerThan(long timestamp) - { - return selectReps().where("timestamp > ?", timestamp).limit(1).executeSingle(); - } - - public void toggleRepetition(long timestamp) - { - if (hasRep(timestamp)) - { - deleteReps(timestamp); - } - else - { - Repetition rep = new Repetition(); - rep.habit = this; - rep.timestamp = timestamp; - rep.save(); - } - - deleteScoresNewerThan(timestamp); - deleteCheckmarksNewerThan(timestamp); - deleteStreaksNewerThan(timestamp); - } - public Uri getUri() { return Uri.parse(String.format("content://org.isoron.uhabits/habit/%d", getId())); @@ -466,204 +251,4 @@ public class Habit extends Model { return archived != 0; } - - public void toggleRepetitionToday() - { - toggleRepetition(DateHelper.getStartOfToday()); - } - - public Score getNewestScore() - { - return new Select().from(Score.class) - .where("habit = ?", getId()) - .orderBy("timestamp desc") - .limit(1) - .executeSingle(); - } - - public void deleteScoresNewerThan(long timestamp) - { - new Delete().from(Score.class) - .where("habit = ?", getId()) - .and("timestamp >= ?", timestamp) - .execute(); - } - - public Integer getScore() - { - int beginningScore; - long beginningTime; - - long today = DateHelper.getStartOfDay(DateHelper.getLocalTime()); - long day = DateHelper.millisecondsInOneDay; - - double freq = ((double) freqNum) / freqDen; - double multiplier = Math.pow(0.5, 1.0 / (14.0 / freq - 1)); - - Score newestScore = getNewestScore(); - if (newestScore == null) - { - Repetition oldestRep = getOldestRep(); - if (oldestRep == null) return 0; - beginningTime = oldestRep.timestamp; - beginningScore = 0; - } - else - { - beginningTime = newestScore.timestamp + day; - beginningScore = newestScore.score; - } - - long nDays = (today - beginningTime) / day; - if (nDays < 0) return newestScore.score; - - int reps[] = getCheckmarks(beginningTime, today); - - ActiveAndroid.beginTransaction(); - int lastScore = beginningScore; - - try - { - for (int i = 0; i < reps.length; i++) - { - Score s = new Score(); - s.habit = this; - s.timestamp = beginningTime + day * i; - s.score = (int) (lastScore * multiplier); - if (reps[reps.length - i - 1] == 2) - { - s.score += 1000000; - s.score = Math.min(s.score, MAX_SCORE); - } - s.save(); - - lastScore = s.score; - } - - ActiveAndroid.setTransactionSuccessful(); - } finally - { - ActiveAndroid.endTransaction(); - } - - return lastScore; - } - - public int[] getScores(Long fromTimestamp, Long toTimestamp, Integer divisor, Long offset) - { - String query = "select score from Score where habit = ? and timestamp > ? and " + - "timestamp <= ? and (timestamp - ?) % ? = 0 order by timestamp desc"; - - String params[] = {getId().toString(), fromTimestamp.toString(), toTimestamp.toString(), - offset.toString(), divisor.toString()}; - - SQLiteDatabase db = Cache.openDatabase(); - Cursor cursor = db.rawQuery(query, params); - - if(!cursor.moveToFirst()) return new int[0]; - - int k = 0; - int[] scores = new int[cursor.getCount()]; - - do - { - scores[k++] = cursor.getInt(0); - } - while (cursor.moveToNext()); - - cursor.close(); - return scores; - - } - - public int[] getAllScores(int divisor) - { - Repetition oldestRep = getOldestRep(); - if(oldestRep == null) return new int[0]; - - long fromTimestamp = oldestRep.timestamp; - long toTimestamp = DateHelper.getStartOfToday(); - return getScores(fromTimestamp, toTimestamp, divisor, toTimestamp); - } - - public List getStreaks() - { - updateStreaks(); - - return new Select().from(Streak.class) - .where("habit = ?", getId()) - .orderBy("end asc") - .execute(); - } - - public Streak getNewestStreak() - { - return new Select().from(Streak.class) - .where("habit = ?", getId()) - .orderBy("end desc") - .limit(1) - .executeSingle(); - } - - public void updateStreaks() - { - long beginning; - long today = DateHelper.getStartOfToday(); - long day = DateHelper.millisecondsInOneDay; - - Streak newestStreak = getNewestStreak(); - if (newestStreak == null) - { - Repetition oldestRep = getOldestRep(); - if (oldestRep == null) return; - - beginning = oldestRep.timestamp; - } - else - { - Repetition oldestRep = getOldestRepNewerThan(newestStreak.end); - if (oldestRep == null) return; - - beginning = oldestRep.timestamp; - } - - if (beginning > today) return; - - int checks[] = getCheckmarks(beginning, today); - ArrayList list = new ArrayList<>(); - - long current = beginning; - list.add(current); - - for (int i = 1; i < checks.length; i++) - { - current += day; - int j = checks.length - i - 1; - - if ((checks[j + 1] == 0 && checks[j] > 0)) list.add(current); - if ((checks[j + 1] > 0 && checks[j] == 0)) list.add(current - day); - } - - if (list.size() % 2 == 1) list.add(current); - - ActiveAndroid.beginTransaction(); - - try - { - for (int i = 0; i < list.size(); i += 2) - { - Streak streak = new Streak(); - streak.habit = this; - streak.start = list.get(i); - streak.end = list.get(i + 1); - streak.length = (streak.end - streak.start) / day + 1; - streak.save(); - } - - ActiveAndroid.setTransactionSuccessful(); - } finally - { - ActiveAndroid.endTransaction(); - } - } } diff --git a/app/src/main/java/org/isoron/uhabits/models/RepetitionList.java b/app/src/main/java/org/isoron/uhabits/models/RepetitionList.java new file mode 100644 index 000000000..6828101ab --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/models/RepetitionList.java @@ -0,0 +1,96 @@ +/* Copyright (C) 2016 Alinson Santos Xavier + * + * This program 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. + * + * This program 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.models; + +import com.activeandroid.query.Delete; +import com.activeandroid.query.From; +import com.activeandroid.query.Select; + +import org.isoron.helpers.DateHelper; + +public class RepetitionList +{ + + private Habit habit; + + public RepetitionList(Habit habit) + { + this.habit = habit; + } + + protected From select() + { + return new Select().from(Repetition.class) + .where("habit = ?", habit.getId()) + .orderBy("timestamp"); + } + + protected From selectFromTo(long timeFrom, long timeTo) + { + return select().and("timestamp >= ?", timeFrom).and("timestamp <= ?", timeTo); + } + + public boolean contains(long timestamp) + { + int count = select().where("timestamp = ?", timestamp).count(); + return (count > 0); + } + + public void delete(long timestamp) + { + new Delete().from(Repetition.class) + .where("habit = ?", habit.getId()) + .and("timestamp = ?", timestamp) + .execute(); + } + + public Repetition getOldestNewerThan(long timestamp) + { + return select().where("timestamp > ?", timestamp).limit(1).executeSingle(); + } + + public void toggle(long timestamp) + { + if (contains(timestamp)) + { + delete(timestamp); + } + else + { + Repetition rep = new Repetition(); + rep.habit = habit; + rep.timestamp = timestamp; + rep.save(); + } + + habit.scores.deleteNewerThan(timestamp); + habit.checkmarks.deleteNewerThan(timestamp); + habit.streaks.deleteNewerThan(timestamp); + } + + public Repetition getOldest() + { + return (Repetition) select().limit(1).executeSingle(); + } + + public boolean hasImplicitRepToday() + { + long today = DateHelper.getStartOfToday(); + int reps[] = habit.checkmarks.getValues(today - DateHelper.millisecondsInOneDay, today); + return (reps[0] > 0); + } +} 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 33f1723cc..44af97588 100644 --- a/app/src/main/java/org/isoron/uhabits/models/Score.java +++ b/app/src/main/java/org/isoron/uhabits/models/Score.java @@ -23,6 +23,10 @@ import com.activeandroid.annotation.Table; @Table(name = "Score") public class Score extends Model { + public static final int HALF_STAR_CUTOFF = 9629750; + public static final int FULL_STAR_CUTOFF = 15407600; + public static final int MAX_SCORE = 19259500; + @Column(name = "habit") public Habit habit; diff --git a/app/src/main/java/org/isoron/uhabits/models/ScoreList.java b/app/src/main/java/org/isoron/uhabits/models/ScoreList.java new file mode 100644 index 000000000..95b3c72db --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/models/ScoreList.java @@ -0,0 +1,160 @@ +/* Copyright (C) 2016 Alinson Santos Xavier + * + * This program 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. + * + * This program 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.models; + +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; + +import com.activeandroid.ActiveAndroid; +import com.activeandroid.Cache; +import com.activeandroid.query.Delete; +import com.activeandroid.query.Select; + +import org.isoron.helpers.DateHelper; + +public class ScoreList +{ + private Habit habit; + + public ScoreList(Habit habit) + { + this.habit = habit; + } + + public int getCurrentStarStatus() + { + int score = getNewestValue(); + + if(score >= Score.FULL_STAR_CUTOFF) return 2; + else if(score >= Score.HALF_STAR_CUTOFF) return 1; + else return 0; + } + + public Score getNewest() + { + return new Select().from(Score.class) + .where("habit = ?", habit.getId()) + .orderBy("timestamp desc") + .limit(1) + .executeSingle(); + } + + public void deleteNewerThan(long timestamp) + { + new Delete().from(Score.class) + .where("habit = ?", habit.getId()) + .and("timestamp >= ?", timestamp) + .execute(); + } + + public Integer getNewestValue() + { + int beginningScore; + long beginningTime; + + long today = DateHelper.getStartOfDay(DateHelper.getLocalTime()); + long day = DateHelper.millisecondsInOneDay; + + double freq = ((double) habit.freqNum) / habit.freqDen; + double multiplier = Math.pow(0.5, 1.0 / (14.0 / freq - 1)); + + Score newestScore = getNewest(); + if (newestScore == null) + { + Repetition oldestRep = habit.repetitions.getOldest(); + if (oldestRep == null) return 0; + beginningTime = oldestRep.timestamp; + beginningScore = 0; + } + else + { + beginningTime = newestScore.timestamp + day; + beginningScore = newestScore.score; + } + + long nDays = (today - beginningTime) / day; + if (nDays < 0) return newestScore.score; + + int reps[] = habit.checkmarks.getValues(beginningTime, today); + + ActiveAndroid.beginTransaction(); + int lastScore = beginningScore; + + try + { + for (int i = 0; i < reps.length; i++) + { + Score s = new Score(); + s.habit = habit; + s.timestamp = beginningTime + day * i; + s.score = (int) (lastScore * multiplier); + if (reps[reps.length - i - 1] == 2) + { + s.score += 1000000; + s.score = Math.min(s.score, Score.MAX_SCORE); + } + s.save(); + + lastScore = s.score; + } + + ActiveAndroid.setTransactionSuccessful(); + } finally + { + ActiveAndroid.endTransaction(); + } + + return lastScore; + } + + public int[] getAllValues(Long fromTimestamp, Long toTimestamp, Integer divisor, Long offset) + { + String query = "select score from Score where habit = ? and timestamp > ? and " + + "timestamp <= ? and (timestamp - ?) % ? = 0 order by timestamp desc"; + + String params[] = { habit.getId().toString(), fromTimestamp.toString(), + toTimestamp.toString(), offset.toString(), divisor.toString()}; + + SQLiteDatabase db = Cache.openDatabase(); + Cursor cursor = db.rawQuery(query, params); + + if(!cursor.moveToFirst()) return new int[0]; + + int k = 0; + int[] scores = new int[cursor.getCount()]; + + do + { + scores[k++] = cursor.getInt(0); + } + while (cursor.moveToNext()); + + cursor.close(); + return scores; + + } + + public int[] getAllValues(int divisor) + { + Repetition oldestRep = habit.repetitions.getOldest(); + if(oldestRep == null) return new int[0]; + + long fromTimestamp = oldestRep.timestamp; + long toTimestamp = DateHelper.getStartOfToday(); + return getAllValues(fromTimestamp, toTimestamp, divisor, toTimestamp); + } +} diff --git a/app/src/main/java/org/isoron/uhabits/models/StreakList.java b/app/src/main/java/org/isoron/uhabits/models/StreakList.java new file mode 100644 index 000000000..9459acab0 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/models/StreakList.java @@ -0,0 +1,127 @@ +/* Copyright (C) 2016 Alinson Santos Xavier + * + * This program 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. + * + * This program 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.models; + +import com.activeandroid.ActiveAndroid; +import com.activeandroid.query.Delete; +import com.activeandroid.query.Select; + +import org.isoron.helpers.DateHelper; + +import java.util.ArrayList; +import java.util.List; + +public class StreakList +{ + private Habit habit; + + public StreakList(Habit habit) + { + this.habit = habit; + } + + public List getAll() + { + rebuild(); + + return new Select().from(Streak.class) + .where("habit = ?", habit.getId()) + .orderBy("end asc") + .execute(); + } + + public Streak getNewest() + { + return new Select().from(Streak.class) + .where("habit = ?", habit.getId()) + .orderBy("end desc") + .limit(1) + .executeSingle(); + } + + public void rebuild() + { + long beginning; + long today = DateHelper.getStartOfToday(); + long day = DateHelper.millisecondsInOneDay; + + Streak newestStreak = getNewest(); + if (newestStreak == null) + { + Repetition oldestRep = habit.repetitions.getOldest(); + if (oldestRep == null) return; + + beginning = oldestRep.timestamp; + } + else + { + Repetition oldestRep = habit.repetitions.getOldestNewerThan(newestStreak.end); + if (oldestRep == null) return; + + beginning = oldestRep.timestamp; + } + + if (beginning > today) return; + + int checks[] = habit.checkmarks.getValues(beginning, today); + ArrayList list = new ArrayList<>(); + + long current = beginning; + list.add(current); + + for (int i = 1; i < checks.length; i++) + { + current += day; + int j = checks.length - i - 1; + + if ((checks[j + 1] == 0 && checks[j] > 0)) list.add(current); + if ((checks[j + 1] > 0 && checks[j] == 0)) list.add(current - day); + } + + if (list.size() % 2 == 1) list.add(current); + + ActiveAndroid.beginTransaction(); + + try + { + for (int i = 0; i < list.size(); i += 2) + { + Streak streak = new Streak(); + streak.habit = habit; + streak.start = list.get(i); + streak.end = list.get(i + 1); + streak.length = (streak.end - streak.start) / day + 1; + streak.save(); + } + + ActiveAndroid.setTransactionSuccessful(); + } + finally + { + ActiveAndroid.endTransaction(); + } + } + + + public void deleteNewerThan(long timestamp) + { + new Delete().from(Streak.class) + .where("habit = ?", habit.getId()) + .and("end >= ?", timestamp - DateHelper.millisecondsInOneDay) + .execute(); + } +} diff --git a/app/src/main/java/org/isoron/uhabits/views/CheckmarkView.java b/app/src/main/java/org/isoron/uhabits/views/CheckmarkView.java index e44096bce..223217aad 100644 --- a/app/src/main/java/org/isoron/uhabits/views/CheckmarkView.java +++ b/app/src/main/java/org/isoron/uhabits/views/CheckmarkView.java @@ -27,7 +27,6 @@ import android.text.Layout; import android.text.StaticLayout; import android.text.TextPaint; import android.util.AttributeSet; -import android.util.Log; import android.view.View; import org.isoron.helpers.ColorHelper; @@ -112,8 +111,8 @@ public class CheckmarkView extends View public void setHabit(Habit habit) { - this.check_status = habit.getCurrentCheckmarkStatus(); - this.star_status = habit.getCurrentStarStatus(); + this.check_status = habit.checkmarks.getCurrentValue(); + this.star_status = habit.scores.getCurrentStarStatus(); this.primaryColor = Color.argb(230, Color.red(habit.color), Color.green(habit.color), Color.blue(habit.color)); this.label = habit.name; updateLabel(); diff --git a/app/src/main/java/org/isoron/uhabits/views/HabitHistoryView.java b/app/src/main/java/org/isoron/uhabits/views/HabitHistoryView.java index 4d841b824..d99ba765b 100644 --- a/app/src/main/java/org/isoron/uhabits/views/HabitHistoryView.java +++ b/app/src/main/java/org/isoron/uhabits/views/HabitHistoryView.java @@ -23,7 +23,6 @@ import android.graphics.Paint; import android.graphics.Paint.Align; import android.graphics.Rect; import android.util.AttributeSet; -import android.util.Log; import org.isoron.helpers.ColorHelper; import org.isoron.helpers.DateHelper; @@ -189,7 +188,7 @@ public class HabitHistoryView extends ScrollableDataView return; } - checkmarks = habit.getAllCheckmarks(); + checkmarks = habit.checkmarks.getAllValues(); } updateDate(); diff --git a/app/src/main/java/org/isoron/uhabits/views/HabitScoreView.java b/app/src/main/java/org/isoron/uhabits/views/HabitScoreView.java index 3405342aa..248341059 100644 --- a/app/src/main/java/org/isoron/uhabits/views/HabitScoreView.java +++ b/app/src/main/java/org/isoron/uhabits/views/HabitScoreView.java @@ -28,6 +28,7 @@ import android.util.AttributeSet; import org.isoron.helpers.ColorHelper; import org.isoron.helpers.DateHelper; import org.isoron.uhabits.models.Habit; +import org.isoron.uhabits.models.Score; import java.text.SimpleDateFormat; import java.util.Locale; @@ -168,7 +169,7 @@ public class HabitScoreView extends ScrollableDataView return; } - scores = habit.getAllScores(BUCKET_SIZE * DateHelper.millisecondsInOneDay); + scores = habit.scores.getAllValues(BUCKET_SIZE * DateHelper.millisecondsInOneDay); } } @@ -177,13 +178,13 @@ public class HabitScoreView extends ScrollableDataView { Random random = new Random(); scores = new int[100]; - scores[0] = Habit.MAX_SCORE / 2; + scores[0] = Score.MAX_SCORE / 2; for(int i = 1; i < 100; i++) { - int step = Habit.MAX_SCORE / 10; + int step = Score.MAX_SCORE / 10; scores[i] = scores[i - 1] + random.nextInt(step * 2) - step; - scores[i] = Math.max(0, Math.min(Habit.MAX_SCORE, scores[i])); + scores[i] = Math.max(0, Math.min(Score.MAX_SCORE, scores[i])); } } @@ -220,7 +221,7 @@ public class HabitScoreView extends ScrollableDataView int offset = nColumns - k - 1 + getDataOffset(); if(offset < scores.length) score = scores[offset]; - double sRelative = ((double) score) / Habit.MAX_SCORE; + double sRelative = ((double) score) / Score.MAX_SCORE; int height = (int) (columnHeight * sRelative); rect.set(0, 0, baseSize, baseSize); diff --git a/app/src/main/java/org/isoron/uhabits/views/HabitStreakView.java b/app/src/main/java/org/isoron/uhabits/views/HabitStreakView.java index 646ead665..5872d96c9 100644 --- a/app/src/main/java/org/isoron/uhabits/views/HabitStreakView.java +++ b/app/src/main/java/org/isoron/uhabits/views/HabitStreakView.java @@ -166,7 +166,7 @@ public class HabitStreakView extends ScrollableDataView return; } - List streaks = habit.getStreaks(); + List streaks = habit.streaks.getAll(); int size = streaks.size(); startTimes = new long[size];