From a1f05714bae4f5a2a2e88d5f94570a6aa3d07d9f Mon Sep 17 00:00:00 2001 From: Alinson Xavier Date: Mon, 16 Mar 2015 11:45:36 -0400 Subject: [PATCH] Compatibility with older devices; more statistics --- app/build.gradle | 2 +- app/src/main/AndroidManifest.xml | 32 +- .../datetimepicker/date/DatePickerDialog.java | 26 +- .../java/org/isoron/helpers/DateHelper.java | 3 +- .../java/org/isoron/uhabits/MainActivity.java | 3 +- .../org/isoron/uhabits/ShowHabitActivity.java | 22 +- .../uhabits/dialogs/ListHabitsFragment.java | 1 - .../uhabits/dialogs/ShowHabitFragment.java | 35 +- .../java/org/isoron/uhabits/models/Habit.java | 939 +++++++++--------- .../uhabits/views/HabitHistoryView.java | 7 +- .../isoron/uhabits/views/HabitStreakView.java | 128 +++ .../org/isoron/uhabits/views/RingView.java | 67 ++ app/src/main/res/drawable/card_background.xml | 39 + .../habits_list_header_background.xml | 26 + .../res/layout/date_picker_done_button.xml | 2 +- app/src/main/res/layout/edit_habit.xml | 19 +- .../main/res/layout/list_habits_fragment.xml | 3 +- app/src/main/res/layout/show_habit.xml | 35 +- app/src/main/res/values-v21/styles.xml | 15 + .../res/values-v21/styles_list_habits.xml | 20 + app/src/main/res/values/dimens.xml | 2 + app/src/main/res/values/strings.xml | 14 + app/src/main/res/values/styles.xml | 5 +- .../main/res/values/styles_list_habits.xml | 6 +- 24 files changed, 893 insertions(+), 558 deletions(-) create mode 100644 app/src/main/java/org/isoron/uhabits/views/HabitStreakView.java create mode 100644 app/src/main/java/org/isoron/uhabits/views/RingView.java create mode 100644 app/src/main/res/drawable/card_background.xml create mode 100644 app/src/main/res/drawable/habits_list_header_background.xml create mode 100644 app/src/main/res/values-v21/styles_list_habits.xml diff --git a/app/build.gradle b/app/build.gradle index f842fe0c3..d5b4f26fe 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -6,7 +6,7 @@ android { defaultConfig { applicationId "org.isoron.uhabits" - minSdkVersion 21 + minSdkVersion 15 targetSdkVersion 22 } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 535a63029..376c34db9 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,49 +1,51 @@ - + android:versionName="1.0"> + android:minSdkVersion="14" + android:targetSdkVersion="21"/> - + + android:theme="@style/AppTheme"> + + android:value="uhabits.db"/> + + android:value="6"/> + android:launchMode="singleInstance"> - - - + + - + + android:parentActivityName=".MainActivity"> + android:value="org.isoron.uhabits.MainActivity"/> diff --git a/app/src/main/java/com/android/datetimepicker/date/DatePickerDialog.java b/app/src/main/java/com/android/datetimepicker/date/DatePickerDialog.java index 4f5b41904..b6581ecf9 100644 --- a/app/src/main/java/com/android/datetimepicker/date/DatePickerDialog.java +++ b/app/src/main/java/com/android/datetimepicker/date/DatePickerDialog.java @@ -91,8 +91,6 @@ public class DatePickerDialog extends DialogFragment implements private TextView mYearView; private DayPickerView mDayPickerView; private YearPickerView mYearPickerView; - private Button mDoneButton; - private Button mClearButton; private int mCurrentView = UNINITIALIZED; @@ -245,25 +243,31 @@ public class DatePickerDialog extends DialogFragment implements animation2.setDuration(ANIMATION_DURATION); mAnimator.setOutAnimation(animation2); - mDoneButton = (Button) view.findViewById(R.id.done); - mDoneButton.setOnClickListener(new OnClickListener() { + Button mDoneButton = (Button) view.findViewById(R.id.done); + mDoneButton.setOnClickListener(new OnClickListener() + { @Override - public void onClick(View v) { + public void onClick(View v) + { tryVibrate(); - if (mCallBack != null) { + if (mCallBack != null) + { mCallBack.onDateSet(DatePickerDialog.this, mCalendar.get(Calendar.YEAR), mCalendar.get(Calendar.MONTH), mCalendar.get(Calendar.DAY_OF_MONTH)); } dismiss(); } }); - - mClearButton = (Button) view.findViewById(R.id.clear); - mClearButton.setOnClickListener(new OnClickListener() { + + Button mClearButton = (Button) view.findViewById(R.id.clear); + mClearButton.setOnClickListener(new OnClickListener() + { @Override - public void onClick(View v) { + public void onClick(View v) + { tryVibrate(); - if (mCallBack != null) { + if (mCallBack != null) + { mCallBack.onDateCleared(DatePickerDialog.this); } dismiss(); diff --git a/app/src/main/java/org/isoron/helpers/DateHelper.java b/app/src/main/java/org/isoron/helpers/DateHelper.java index 8f46a9711..4d8e199cf 100644 --- a/app/src/main/java/org/isoron/helpers/DateHelper.java +++ b/app/src/main/java/org/isoron/helpers/DateHelper.java @@ -40,8 +40,7 @@ public class DateHelper public static int differenceInDays(Date from, Date to) { long milliseconds = getStartOfDay(to.getTime()) - getStartOfDay(from.getTime()); - int days = (int) (milliseconds / millisecondsInOneDay); - return days; + return (int) (milliseconds / millisecondsInOneDay); } public static String differenceInWords(Date from, Date to) diff --git a/app/src/main/java/org/isoron/uhabits/MainActivity.java b/app/src/main/java/org/isoron/uhabits/MainActivity.java index abe2d3745..7db75aa54 100644 --- a/app/src/main/java/org/isoron/uhabits/MainActivity.java +++ b/app/src/main/java/org/isoron/uhabits/MainActivity.java @@ -83,7 +83,8 @@ public class MainActivity extends Activity { super.onCreate(savedInstanceState); - getActionBar().setElevation(5); + if (android.os.Build.VERSION.SDK_INT >= 21) + getActionBar().setElevation(5); setContentView(R.layout.list_habits_activity); diff --git a/app/src/main/java/org/isoron/uhabits/ShowHabitActivity.java b/app/src/main/java/org/isoron/uhabits/ShowHabitActivity.java index 6116452b4..c1555de47 100644 --- a/app/src/main/java/org/isoron/uhabits/ShowHabitActivity.java +++ b/app/src/main/java/org/isoron/uhabits/ShowHabitActivity.java @@ -16,22 +16,24 @@ public class ShowHabitActivity extends Activity { public Habit habit; - private ShowHabitFragment showHabitFragment; - @Override + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - - getActionBar().setElevation(5); - Uri data = getIntent().getData(); - habit = Habit.get(ContentUris.parseId(data)); - getActionBar().setTitle(habit.name); - getActionBar().setBackgroundDrawable(new ColorDrawable(habit.color)); + + Uri data = getIntent().getData(); + habit = Habit.get(ContentUris.parseId(data)); + getActionBar().setTitle(habit.name); + + if (android.os.Build.VERSION.SDK_INT >= 21) + { + getActionBar().setElevation(5); + getActionBar().setBackgroundDrawable(new ColorDrawable(habit.color)); + } + setContentView(R.layout.show_habit_activity); - showHabitFragment = (ShowHabitFragment) getFragmentManager().findFragmentById( - R.id.fragment2); } @Override diff --git a/app/src/main/java/org/isoron/uhabits/dialogs/ListHabitsFragment.java b/app/src/main/java/org/isoron/uhabits/dialogs/ListHabitsFragment.java index e7a53dd84..e4e63c126 100644 --- a/app/src/main/java/org/isoron/uhabits/dialogs/ListHabitsFragment.java +++ b/app/src/main/java/org/isoron/uhabits/dialogs/ListHabitsFragment.java @@ -338,7 +338,6 @@ public class ListHabitsFragment extends Fragment implements OnSavedListener, OnI Intent intent = new Intent(getActivity(), ShowHabitActivity.class); intent.setData(Uri.parse("content://org.isoron.uhabits/habit/" + habit.getId())); - getActivity().getWindow().setExitTransition(new Explode()); startActivity(intent); } diff --git a/app/src/main/java/org/isoron/uhabits/dialogs/ShowHabitFragment.java b/app/src/main/java/org/isoron/uhabits/dialogs/ShowHabitFragment.java index 72a6b1f5b..8997d9ed4 100644 --- a/app/src/main/java/org/isoron/uhabits/dialogs/ShowHabitFragment.java +++ b/app/src/main/java/org/isoron/uhabits/dialogs/ShowHabitFragment.java @@ -10,6 +10,8 @@ import org.isoron.uhabits.R; import org.isoron.uhabits.ShowHabitActivity; import org.isoron.uhabits.models.Habit; import org.isoron.uhabits.views.HabitHistoryView; +import org.isoron.uhabits.views.HabitStreakView; +import org.isoron.uhabits.views.RingView; import android.app.Fragment; import android.graphics.Color; @@ -27,9 +29,8 @@ import android.widget.TextView; public class ShowHabitFragment extends Fragment { protected ShowHabitActivity activity; - private Habit habit; - @Override + @Override public void onStart() { super.onStart(); @@ -43,24 +44,34 @@ public class ShowHabitFragment extends Fragment View view = inflater.inflate(R.layout.show_habit, container, false); activity = (ShowHabitActivity) getActivity(); - habit = activity.habit; + Habit habit = activity.habit; - int darkerHabitColor = ColorHelper.mixColors(habit.color, Color.BLACK, 0.75f); - activity.getWindow().setStatusBarColor(darkerHabitColor); + if (android.os.Build.VERSION.SDK_INT >= 21) + { + int darkerHabitColor = ColorHelper.mixColors(habit.color, Color.BLACK, 0.75f); + activity.getWindow().setStatusBarColor(darkerHabitColor); + } TextView tvHistory = (TextView) view.findViewById(R.id.tvHistory); TextView tvOverview = (TextView) view.findViewById(R.id.tvOverview); + TextView tvStreaks= (TextView) view.findViewById(R.id.tvStreaks); tvHistory.setTextColor(habit.color); tvOverview.setTextColor(habit.color); - - TextView tvStrength = (TextView) view.findViewById(R.id.tvStrength); - tvStrength.setText(String.format("%.2f%%", ((float) habit.getScore() / Habit.MAX_SCORE) * 100)); - + tvStreaks.setTextColor(habit.color); + + LinearLayout llOverview = (LinearLayout) view.findViewById(R.id.llOverview); + llOverview.addView(new RingView(activity, 200, habit.color, ((float) habit.getScore() / Habit.MAX_SCORE), "Habit strength")); + LinearLayout llHistory = (LinearLayout) view.findViewById(R.id.llHistory); + HabitHistoryView hhv = new HabitHistoryView(activity, habit, + (int) activity.getResources().getDimension(R.dimen.square_size)); + llHistory.addView(hhv); + + LinearLayout llStreaks = (LinearLayout) view.findViewById(R.id.llStreaks); + HabitStreakView hsv = new HabitStreakView(activity, habit, + (int) activity.getResources().getDimension(R.dimen.square_size)); + llStreaks.addView(hsv); - HabitHistoryView hhv = new HabitHistoryView(activity, habit, 40); - llHistory.addView(hhv); - return view; } } 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 54ec3f0e2..3ab6ac635 100644 --- a/app/src/main/java/org/isoron/uhabits/models/Habit.java +++ b/app/src/main/java/org/isoron/uhabits/models/Habit.java @@ -1,14 +1,11 @@ package org.isoron.uhabits.models; -import java.util.List; - -import org.isoron.helpers.ColorHelper; -import org.isoron.helpers.Command; -import org.isoron.helpers.DateHelper; -import org.isoron.uhabits.R; - import android.annotation.SuppressLint; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.util.Log; +import com.activeandroid.Cache; import com.activeandroid.Model; import com.activeandroid.annotation.Column; import com.activeandroid.annotation.Table; @@ -18,467 +15,479 @@ import com.activeandroid.query.Select; 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.List; + @Table(name = "Habits") public class Habit extends Model { - /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * - * Fields * - * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ - public static final int HALF_STAR_CUTOFF = 5999000; - public static final int FULL_STAR_CUTOFF = 12973000; - public static final int MAX_SCORE = 19259500; - - @Column(name = "name") - public String name; - - @Column(name = "description") - public String description; - - @Column(name = "freq_num") - public Integer freq_num; - - @Column(name = "freq_den") - public Integer freq_den; - - @Column(name = "color") - public Integer color; - - @Column(name = "position") - public Integer position; - - @Column(name = "reminder_hour") - public Integer reminder_hour; - - @Column(name = "reminder_min") - public Integer reminder_min; - - @Column(name = "highlight") - public Integer highlight; - - /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * - * Commands * - * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ - - public static class CreateCommand extends Command - { - private Habit model; - private Long savedId; - - public CreateCommand(Habit model) - { - this.model = model; - } - - @Override - public void execute() - { - Habit savedHabit = new Habit(model); - if(savedId == null) - { - savedHabit.save(); - savedId = savedHabit.getId(); - } - else - { - savedHabit.save(savedId); - } - } - - @Override - public void undo() - { - Habit.get(savedId).delete(); - } - - @Override - public Integer getExecuteStringId() - { - return R.string.toast_habit_created; - } - - @Override - public Integer getUndoStringId() - { - return R.string.toast_habit_deleted; - } - - } - - public class EditCommand extends Command - { - private Habit original; - private Habit modified; - private long savedId; - private boolean hasIntervalChanged; - - public EditCommand(Habit modified) - { - this.savedId = getId(); - this.modified = new Habit(modified); - this.original = new Habit(Habit.this); - - hasIntervalChanged = (this.original.freq_den != this.modified.freq_den - || this.original.freq_num != this.modified.freq_num); - } - - public void execute() - { - Habit habit = Habit.get(savedId); - habit.copyAttributes(modified); - habit.save(); - if(hasIntervalChanged) - habit.deleteScoresNewerThan(0); - } - - public void undo() - { - Habit habit = Habit.get(savedId); - habit.copyAttributes(original); - habit.save(); - if(hasIntervalChanged) - habit.deleteScoresNewerThan(0); - } - - public Integer getExecuteStringId() - { - return R.string.toast_habit_changed; - } - - public Integer getUndoStringId() - { - return R.string.toast_habit_changed_back; - } - } - - public class ToggleRepetitionCommand extends Command - { - private Long offset; - - public ToggleRepetitionCommand(long offset) - { - this.offset = offset; - } - - @Override - public void execute() - { - toggleRepetition(offset); - } - - @Override - public void undo() - { - execute(); - } - } - - /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * - * Accessors * - * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ - - public Habit(Habit model) - { - copyAttributes(model); - } - - public void copyAttributes(Habit model) - { - this.name = model.name; - this.description = model.description; - this.freq_num = model.freq_num; - this.freq_den = model.freq_den; - this.color = model.color; - this.position = model.position; - this.reminder_hour = model.reminder_hour; - this.reminder_min = model.reminder_min; - this.highlight = model.highlight; - } - - public Habit() - { - this.color = ColorHelper.palette[11]; - this.position = Habit.getCount(); - this.highlight = 0; - } - - public static Habit get(Long id) - { - return Habit.load(Habit.class, id); - } - - public void save(Long id) - { - save(); - Habit.updateId(getId(), id); - } - - @SuppressLint("DefaultLocale") - public static void updateId(long oldId, long newId) - { - SQLiteUtils.execSql(String.format( - "update Habits set Id = %d where Id = %d", newId, oldId)); - } - - protected static From select() - { - return new Select().from(Habit.class).orderBy("position"); - } - - public static int getCount() - { - return select().count(); - } - - public static Habit getByPosition(int position) - { - return select().offset(position).executeSingle(); - } - - public static java.util.List getHabits() - { - return select().execute(); - } - - public static java.util.List getHighlightedHabits() - { - return select().where("highlight = 1").orderBy("reminder_hour desc, reminder_min desc") - .execute(); - } - - public static java.util.List getHabitsWithReminder() - { - return select().where("reminder_hour is not null").execute(); - } - - - /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * - * Repetitions * - * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ - - 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 boolean hasRepToday() - { - return hasRep(DateHelper.getStartOfToday()); - } - - public void deleteReps(long timestamp) - { - new Delete().from(Repetition.class).where("habit = ?", getId()) - .and("timestamp = ?", timestamp).execute(); - } - - public int[] getReps(long timeFrom, long timeTo) - { - long timeFromExtended = timeFrom - (long)(freq_den) * DateHelper.millisecondsInOneDay; - List reps = selectRepsFromTo(timeFromExtended, timeTo).execute(); - - int nDaysExtended = (int) ((timeTo - timeFromExtended) / DateHelper.millisecondsInOneDay); - int checkExtended[] = new int[nDaysExtended + 1]; - - int nDays = (int) ((timeTo - timeFrom) / DateHelper.millisecondsInOneDay); - - // mark explicit checks - for (Repetition rep : reps) - { - int offset = (int) ((rep.timestamp - timeFrom) / DateHelper.millisecondsInOneDay); - checkExtended[nDays - offset] = 2; - } - - // marks implicit checks - for(int i=0; i= freq_num) - checkExtended[i] = Math.max(checkExtended[i], 1); - } - - int check[] = new int[nDays + 1]; - for(int i=0; i 0); - } - - public Repetition getOldestRep() - { - return (Repetition) selectReps().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); - } - - public void toggleRepetitionToday() - { - toggleRepetition(DateHelper.getStartOfToday()); - } - - /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * - * Scoring * - * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ - - 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) freq_num) / freq_den; - 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[] = getReps(beginningTime, today); - - int lastScore = beginningScore; - 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, 19259500); - } - s.save(); - - lastScore = s.score; - } - - return lastScore; - } - - - /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * - * Ordering * - * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ - - public static void reorder(int from, int to) - { - if(from == to) - return; - - Habit h = Habit.getByPosition(from); - if(to < from) - new Update(Habit.class).set("position = position + 1") - .where("position >= ? and position < ?", to, from) - .execute(); - else - new Update(Habit.class).set("position = position - 1") - .where("position > ? and position <= ?", from, to) - .execute(); - - h.position = to; - h.save(); - } - - public static void rebuildOrder() - { - List habits = select().execute(); - int i = 0; - for (Habit h : habits) - { - h.position = i++; - h.save(); - } - } - - public static void roundTimestamps() - { - List reps = new Select().from(Repetition.class).execute(); - for (Repetition r : reps) - { - r.timestamp = DateHelper.getStartOfDay(r.timestamp); - r.save(); - } - } - - /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * - * Statistics * - * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ - - public static int getStarCount() - { - String args[] = {}; - return SQLiteUtils.intQuery( - "select count(*) from (select score, max(timestamp) from " + - "score group by habit) as scores where scores.score >= " - + Integer.toString(12973000), args); - - } + public static final int FULL_STAR_CUTOFF = 12973000; + public static final int MAX_SCORE = 19259500; + + @Column(name = "name") + public String name; + + @Column(name = "description") + public String description; + + @Column(name = "freq_num") + public Integer freq_num; + + @Column(name = "freq_den") + public Integer freq_den; + + @Column(name = "color") + public Integer color; + + @Column(name = "position") + public Integer position; + + @Column(name = "reminder_hour") + public Integer reminder_hour; + + @Column(name = "reminder_min") + public Integer reminder_min; + + @Column(name = "highlight") + public Integer highlight; + + public Habit(Habit model) + { + copyAttributes(model); + } + + public Habit() + { + this.color = ColorHelper.palette[11]; + this.position = Habit.getCount(); + this.highlight = 0; + } + + public static Habit get(Long id) + { + return Habit.load(Habit.class, id); + } + + @SuppressLint("DefaultLocale") + public static void updateId(long oldId, long newId) + { + SQLiteUtils.execSql(String.format( + "update Habits set Id = %d where Id = %d", newId, oldId)); + } + + protected static From select() + { + return new Select().from(Habit.class).orderBy("position"); + } + + public static int getCount() + { + return select().count(); + } + + public static Habit getByPosition(int position) + { + return select().offset(position).executeSingle(); + } + + public static java.util.List getHabits() + { + return select().execute(); + } + + public static java.util.List getHighlightedHabits() + { + return select().where("highlight = 1").orderBy("reminder_hour desc, reminder_min desc") + .execute(); + } + + public static java.util.List getHabitsWithReminder() + { + return select().where("reminder_hour is not null").execute(); + } + + public static void reorder(int from, int to) + { + if (from == to) + return; + + Habit h = Habit.getByPosition(from); + if (to < from) + new Update(Habit.class).set("position = position + 1") + .where("position >= ? and position < ?", to, from) + .execute(); + else + new Update(Habit.class).set("position = position - 1") + .where("position > ? and position <= ?", from, to) + .execute(); + + h.position = to; + h.save(); + } + + public static void rebuildOrder() + { + List habits = select().execute(); + int i = 0; + for (Habit h : habits) + { + h.position = i++; + h.save(); + } + } + + public static void roundTimestamps() + { + List reps = new Select().from(Repetition.class).execute(); + for (Repetition r : reps) + { + r.timestamp = DateHelper.getStartOfDay(r.timestamp); + r.save(); + } + } + + public static int getStarCount() + { + String args[] = {}; + return SQLiteUtils.intQuery( + "select count(*) from (select score, max(timestamp) from " + + "score group by habit) as scores where scores.score >= " + + Integer.toString(12973000), args); + + } + + public void copyAttributes(Habit model) + { + this.name = model.name; + this.description = model.description; + this.freq_num = model.freq_num; + this.freq_den = model.freq_den; + this.color = model.color; + this.position = model.position; + this.reminder_hour = model.reminder_hour; + this.reminder_min = model.reminder_min; + this.highlight = model.highlight; + } + + + public void save(Long id) + { + save(); + 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 boolean hasRepToday() + { + return hasRep(DateHelper.getStartOfToday()); + } + + public void deleteReps(long timestamp) + { + new Delete().from(Repetition.class).where("habit = ?", getId()) + .and("timestamp = ?", timestamp).execute(); + } + + public int[] getReps(long timeFrom, long timeTo) + { + long timeFromExtended = timeFrom - (long) (freq_den) * DateHelper.millisecondsInOneDay; + List reps = selectRepsFromTo(timeFromExtended, timeTo).execute(); + + int nDaysExtended = (int) ((timeTo - timeFromExtended) / DateHelper.millisecondsInOneDay); + int checkExtended[] = new int[nDaysExtended + 1]; + + int nDays = (int) ((timeTo - timeFrom) / DateHelper.millisecondsInOneDay); + + // mark explicit checks + for (Repetition rep : reps) + { + int offset = (int) ((rep.timestamp - timeFrom) / DateHelper.millisecondsInOneDay); + checkExtended[nDays - offset] = 2; + } + + // marks implicit checks + for (int i = 0; i < nDays; i++) + { + int counter = 0; + + for (int j = 0; j < freq_den; j++) + if (checkExtended[i + j] == 2) counter++; + + if (counter >= freq_num) + checkExtended[i] = Math.max(checkExtended[i], 1); + } + + int check[] = new int[nDays + 1]; + for (int i = 0; i < nDays + 1; i++) + check[i] = checkExtended[i]; + + return check; + } + + public boolean hasImplicitRepToday() + { + long today = DateHelper.getStartOfToday(); + int reps[] = getReps(today - DateHelper.millisecondsInOneDay, today); + return (reps[0] > 0); + } + + public Repetition getOldestRep() + { + return (Repetition) selectReps().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); + } + + 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) freq_num) / freq_den; + 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[] = getReps(beginningTime, today); + + int lastScore = beginningScore; + 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, 19259500); + } + s.save(); + + lastScore = s.score; + } + + return lastScore; + } + + public long[] getStreaks() + { + String query = "create temporary table T as select distinct r1.habit as habit, r1.timestamp as time,\n" + + " (select count(*) from repetitions r2 where r1.habit = r2.habit and\n" + + " (r1.timestamp = r2.timestamp - 24*60*60*1000 or\n" + + " r1.timestamp = r2.timestamp + 24*60*60*1000)) as neighbors\n" + + "from repetitions r1 where r1.habit = ?"; + + String query2 = + "select time from T, (select 0 union select 1) where neighbors = 0 union all\n" + + "select time from T where neighbors = 1 order by time;"; + + String args[] = {getId().toString()}; + + SQLiteDatabase db = Cache.openDatabase(); + db.beginTransaction(); + db.execSQL(query, args); + Cursor cursor = db.rawQuery(query2, null); + + long streaks[] = new long[cursor.getCount()]; + int current = 0; + + Log.d("Streaks", String.format("%d rows", cursor.getCount())); + + if (cursor.moveToFirst()) + { + do + { + streaks[current++] = cursor.getLong(0); + } while (cursor.moveToNext()); + } + + db.endTransaction(); + return streaks; + } + + public static class CreateCommand extends Command + { + private Habit model; + private Long savedId; + + public CreateCommand(Habit model) + { + this.model = model; + } + + @Override + public void execute() + { + Habit savedHabit = new Habit(model); + if (savedId == null) + { + savedHabit.save(); + savedId = savedHabit.getId(); + } else + { + savedHabit.save(savedId); + } + } + + @Override + public void undo() + { + Habit.get(savedId).delete(); + } + + @Override + public Integer getExecuteStringId() + { + return R.string.toast_habit_created; + } + + @Override + public Integer getUndoStringId() + { + return R.string.toast_habit_deleted; + } + + } + + public class EditCommand extends Command + { + private Habit original; + private Habit modified; + private long savedId; + private boolean hasIntervalChanged; + + public EditCommand(Habit modified) + { + this.savedId = getId(); + this.modified = new Habit(modified); + this.original = new Habit(Habit.this); + + hasIntervalChanged = (this.original.freq_den != this.modified.freq_den + || this.original.freq_num != this.modified.freq_num); + } + + public void execute() + { + Habit habit = Habit.get(savedId); + habit.copyAttributes(modified); + habit.save(); + if (hasIntervalChanged) + habit.deleteScoresNewerThan(0); + } + + public void undo() + { + Habit habit = Habit.get(savedId); + habit.copyAttributes(original); + habit.save(); + if (hasIntervalChanged) + habit.deleteScoresNewerThan(0); + } + + public Integer getExecuteStringId() + { + return R.string.toast_habit_changed; + } + + public Integer getUndoStringId() + { + return R.string.toast_habit_changed_back; + } + } + + public class ToggleRepetitionCommand extends Command + { + private Long offset; + + public ToggleRepetitionCommand(long offset) + { + this.offset = offset; + } + + @Override + public void execute() + { + toggleRepetition(offset); + } + + @Override + public void undo() + { + execute(); + } + } } 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 1ec8e24eb..c7e958499 100644 --- a/app/src/main/java/org/isoron/uhabits/views/HabitHistoryView.java +++ b/app/src/main/java/org/isoron/uhabits/views/HabitHistoryView.java @@ -30,8 +30,7 @@ public class HabitHistoryView extends View private Context context; private Paint pSquareBg, pSquareFg, pTextHeader; - private int width, height; - private int squareSize, squareSpacing; + private int squareSize, squareSpacing; private int nColumns, offsetWeeks; private int colorPrimary, colorPrimaryBrighter, grey; @@ -78,9 +77,7 @@ public class HabitHistoryView extends View @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { - width = w; - height = h; - nColumns = (w / squareSize) - 1; + nColumns = (w / squareSize) - 1; fetchReps(); } diff --git a/app/src/main/java/org/isoron/uhabits/views/HabitStreakView.java b/app/src/main/java/org/isoron/uhabits/views/HabitStreakView.java new file mode 100644 index 000000000..c90a96bd0 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/views/HabitStreakView.java @@ -0,0 +1,128 @@ +package org.isoron.uhabits.views; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Rect; +import android.view.View; + +import org.isoron.helpers.ColorHelper; +import org.isoron.helpers.DateHelper; +import org.isoron.uhabits.models.Habit; + +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.GregorianCalendar; + +public class HabitStreakView extends View +{ + private Habit habit; + private int columnWidth, columnHeight, nColumns; + + private Paint pText, pBar; + private long streaks[]; + + private long streakStart[], streakEnd[], streakLength[]; + private long maxStreakLength; + + private int barHeaderHeight; + + private int[] colors; + + public HabitStreakView(Context context, Habit habit, int columnWidth) + { + super(context); + this.habit = habit; + this.columnWidth = columnWidth; + + pText = new Paint(); + pText.setColor(Color.LTGRAY); + pText.setTextAlign(Paint.Align.CENTER); + pText.setTextSize(columnWidth * 0.5f); + pText.setAntiAlias(true); + + pBar = new Paint(); + pBar.setTextAlign(Paint.Align.CENTER); + pBar.setTextSize(columnWidth * 0.5f); + pBar.setAntiAlias(true); + + columnHeight = 8 * columnWidth; + barHeaderHeight = columnWidth; + + colors = new int[4]; + + colors[0] = Color.rgb(230, 230, 230); + colors[3] = habit.color; + colors[1] = ColorHelper.mixColors(colors[0], colors[3], 0.66f); + colors[2] = ColorHelper.mixColors(colors[0], colors[3], 0.33f); + + fetchStreaks(); + } + + private void fetchStreaks() + { + streaks = habit.getStreaks(); + streakStart = new long[streaks.length / 2]; + streakEnd = new long[streaks.length / 2]; + streakLength = new long[streaks.length / 2]; + + for(int i=0; i + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/habits_list_header_background.xml b/app/src/main/res/drawable/habits_list_header_background.xml new file mode 100644 index 000000000..a935ad4de --- /dev/null +++ b/app/src/main/res/drawable/habits_list_header_background.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/date_picker_done_button.xml b/app/src/main/res/layout/date_picker_done_button.xml index 3794a08ce..4cd09235b 100644 --- a/app/src/main/res/layout/date_picker_done_button.xml +++ b/app/src/main/res/layout/date_picker_done_button.xml @@ -26,7 +26,7 @@ android:layout_height="wrap_content" android:layout_weight="1" android:minHeight="48dp" - android:text="Clear" + android:text="@string/clear" android:textSize="@dimen/done_label_size" android:textColor="@color/done_text_color" /> diff --git a/app/src/main/res/layout/edit_habit.xml b/app/src/main/res/layout/edit_habit.xml index 2430609a2..714f7337d 100644 --- a/app/src/main/res/layout/edit_habit.xml +++ b/app/src/main/res/layout/edit_habit.xml @@ -29,7 +29,7 @@ + android:text="@string/repeat" /> + android:text="@string/times_every" /> + android:text="@string/days" /> + android:text="@string/reminder" />