From 404cc82348c7311ff1e377017efc409f04446642 Mon Sep 17 00:00:00 2001 From: Alinson Xavier Date: Thu, 18 Feb 2016 14:11:59 -0500 Subject: [PATCH] Load data asynchronously; cache checkmarks and streaks --- app/src/main/AndroidManifest.xml | 2 +- .../java/org/isoron/helpers/DialogHelper.java | 2 +- .../isoron/helpers/ReplayableActivity.java | 36 +- .../java/org/isoron/uhabits/MainActivity.java | 14 +- .../uhabits/dialogs/EditHabitFragment.java | 7 +- .../uhabits/dialogs/ListHabitsFragment.java | 288 +++++++++++--- .../uhabits/dialogs/ShowHabitFragment.java | 10 +- .../org/isoron/uhabits/models/Checkmark.java | 29 ++ .../java/org/isoron/uhabits/models/Habit.java | 359 ++++++++++++------ .../org/isoron/uhabits/models/Streak.java | 19 + .../uhabits/views/HabitHistoryView.java | 10 +- .../isoron/uhabits/views/HabitStreakView.java | 32 +- .../main/res/layout/list_habits_activity.xml | 4 +- .../main/res/layout/list_habits_fragment.xml | 8 + app/src/main/res/values/strings.xml | 1 + app/src/main/res/xml/preferences.xml | 7 +- 16 files changed, 616 insertions(+), 212 deletions(-) create mode 100644 app/src/main/java/org/isoron/uhabits/models/Checkmark.java create mode 100644 app/src/main/java/org/isoron/uhabits/models/Streak.java diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index cd3780be7..786a91380 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -21,7 +21,7 @@ + android:value="9"/> (); } - public void executeCommand(Command command) + public void executeCommand(Command command, Long refreshKey) { - executeCommand(command, false); - BackupManager.dataChanged("org.isoron.uhabits"); + executeCommand(command, false, refreshKey); } protected void undo() @@ -54,7 +54,7 @@ abstract public class ReplayableActivity extends Activity return; } Command last = redoList.pop(); - executeCommand(last, false); + executeCommand(last, false, null); } public void showToast(Integer stringId) @@ -65,14 +65,36 @@ abstract public class ReplayableActivity extends Activity toast.show(); } - - public void executeCommand(Command command, boolean clearRedoStack) + public void executeCommand(final Command command, Boolean clearRedoStack, + final Long refreshKey) { undoList.push(command); + if (undoList.size() > MAX_UNDO_LEVEL) undoList.removeLast(); if (clearRedoStack) redoList.clear(); - command.execute(); + new AsyncTask() + { + @Override + protected Void doInBackground(Void... params) + { + command.execute(); + return null; + } + + @Override + protected void onPostExecute(Void aVoid) + { + ReplayableActivity.this.onPostExecuteCommand(refreshKey); + BackupManager.dataChanged("org.isoron.uhabits"); + } + }.execute(); + + showToast(command.getExecuteStringId()); } + + public void onPostExecuteCommand(Long refreshKey) + { + } } diff --git a/app/src/main/java/org/isoron/uhabits/MainActivity.java b/app/src/main/java/org/isoron/uhabits/MainActivity.java index e3adc04f6..007cedf47 100644 --- a/app/src/main/java/org/isoron/uhabits/MainActivity.java +++ b/app/src/main/java/org/isoron/uhabits/MainActivity.java @@ -6,6 +6,7 @@ import android.content.SharedPreferences; import android.net.Uri; import android.os.Bundle; import android.preference.PreferenceManager; +import android.util.Log; import android.view.Menu; import android.view.MenuItem; @@ -33,6 +34,7 @@ public class MainActivity extends ReplayableActivity ReminderHelper.createReminderAlarms(MainActivity.this); showTutorial(); + } private void showTutorial() @@ -51,13 +53,6 @@ public class MainActivity extends ReplayableActivity } } - @Override - protected void onStart() - { - super.onStart(); - listHabitsFragment.notifyDataSetChanged(); - } - @Override public boolean onCreateOptionsMenu(Menu menu) { @@ -89,9 +84,8 @@ public class MainActivity extends ReplayableActivity } @Override - public void executeCommand(Command command) + public void onPostExecuteCommand(Long refreshKey) { - super.executeCommand(command); - listHabitsFragment.notifyDataSetChanged(); + listHabitsFragment.onPostExecuteCommand(refreshKey); } } diff --git a/app/src/main/java/org/isoron/uhabits/dialogs/EditHabitFragment.java b/app/src/main/java/org/isoron/uhabits/dialogs/EditHabitFragment.java index c4aea772b..3af652696 100644 --- a/app/src/main/java/org/isoron/uhabits/dialogs/EditHabitFragment.java +++ b/app/src/main/java/org/isoron/uhabits/dialogs/EditHabitFragment.java @@ -263,14 +263,19 @@ public class EditHabitFragment extends DialogFragment implements OnClickListener editor.putInt("pref_default_habit_freq_den", modified_habit.freq_den); editor.apply(); + Habit savedHabit = null; + if(mode == EDIT_MODE) + { command = originalHabit.new EditCommand(modified_habit); + savedHabit = originalHabit; + } if(mode == CREATE_MODE) command = new Habit.CreateCommand(modified_habit); if(onSavedListener != null) - onSavedListener.onSaved(command); + onSavedListener.onSaved(command, savedHabit); dismiss(); } 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 6b7936346..b6a0b08aa 100644 --- a/app/src/main/java/org/isoron/uhabits/dialogs/ListHabitsFragment.java +++ b/app/src/main/java/org/isoron/uhabits/dialogs/ListHabitsFragment.java @@ -7,7 +7,9 @@ import android.content.SharedPreferences; import android.graphics.Color; import android.graphics.Point; import android.graphics.Typeface; +import android.os.AsyncTask; import android.os.Bundle; +import android.os.Handler; import android.os.Vibrator; import android.preference.PreferenceManager; import android.util.DisplayMetrics; @@ -30,6 +32,7 @@ import android.widget.BaseAdapter; import android.widget.Button; import android.widget.LinearLayout; import android.widget.LinearLayout.LayoutParams; +import android.widget.ProgressBar; import android.widget.TextView; import com.mobeta.android.dslv.DragSortController; @@ -47,6 +50,7 @@ import org.isoron.uhabits.models.Habit; import java.util.Date; import java.util.GregorianCalendar; +import java.util.HashMap; import java.util.Locale; import java.util.TimeZone; @@ -54,6 +58,9 @@ public class ListHabitsFragment extends Fragment implements OnSavedListener, OnItemClickListener, OnLongClickListener, DropListener, OnClickListener { + + public static final int INACTIVE_COLOR = Color.rgb(230, 230, 230); + public interface OnHabitClickListener { void onHabitClicked(Habit habit); @@ -67,23 +74,39 @@ public class ListHabitsFragment extends Fragment private int tvNameWidth; private int button_count; private View llEmpty; + private ProgressBar progressBar; private OnHabitClickListener habitClickListener; private boolean short_toggle_enabled; + private HashMap habits; + private HashMap positionToHabit; + private HashMap checkmarks; + private HashMap scores; + + private Long lastLoadedTimestamp = null; + private AsyncTask currentFetchTask = null; + @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - View view = inflater.inflate(R.layout.list_habits_fragment, container, false); - DisplayMetrics dm = getResources().getDisplayMetrics(); int width = (int) (dm.widthPixels / dm.density); button_count = (int) ((width - 160) / 42); tvNameWidth = (int) ((width - 30 - button_count * 42) * dm.density); + habits = new HashMap<>(); + positionToHabit = new HashMap<>(); + checkmarks = new HashMap<>(); + scores = new HashMap<>(); + + View view = inflater.inflate(R.layout.list_habits_fragment, container, false); tvNameHeader = (TextView) view.findViewById(R.id.tvNameHeader); + progressBar = (ProgressBar) view.findViewById(R.id.progressBar); + progressBar.setVisibility(View.INVISIBLE); + adapter = new ListHabitsAdapter(getActivity()); listView = (DragSortListView) view.findViewById(R.id.listView); listView.setAdapter(adapter); @@ -107,7 +130,6 @@ public class ListHabitsFragment extends Fragment llEmpty = view.findViewById(R.id.llEmpty); updateEmptyMessage(); - setHasOptionsMenu(true); return view; } @@ -124,7 +146,14 @@ public class ListHabitsFragment extends Fragment public void onResume() { super.onResume(); - updateHeader(); + if(lastLoadedTimestamp == null || lastLoadedTimestamp != DateHelper.getStartOfToday()) + { + updateHeader(); + fetchAllHabits(); + updateEmptyMessage(); + } + + adapter.notifyDataSetChanged(); SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(activity); short_toggle_enabled = prefs.getBoolean("pref_short_toggle", false); @@ -156,6 +185,139 @@ public class ListHabitsFragment extends Fragment } } + private void fetchAllHabits() + { + if(currentFetchTask != null) currentFetchTask.cancel(true); + + currentFetchTask = new AsyncTask() + { + HashMap newHabits = Habit.getAll(); + HashMap newPositionToHabit = new HashMap<>(); + HashMap newCheckmarks = new HashMap<>(); + HashMap newScores = new HashMap<>(); + + @Override + protected Void doInBackground(Void... params) + { + long dateTo = DateHelper.getStartOfDay(DateHelper.getLocalTime()); + long dateFrom = dateTo - (button_count - 1) * DateHelper.millisecondsInOneDay; + int[] empty = new int[button_count]; + + for(Habit h : newHabits.values()) + { + newScores.put(h.getId(), 0); + newPositionToHabit.put(h.position, h); + newCheckmarks.put(h.getId(), empty); + } + + int current = 0; + for(int i = 0; i < newHabits.size(); i++) + { + if(isCancelled()) return null; + + Habit h = newPositionToHabit.get(i); + newScores.put(h.getId(), h.getScore()); + newCheckmarks.put(h.getId(), h.getCheckmarks(dateFrom, dateTo)); + + publishProgress(current++, newHabits.size()); + } + + commit(); + + return null; + } + + private void commit() + { + habits = newHabits; + positionToHabit = newPositionToHabit; + checkmarks = newCheckmarks; + scores = newScores; + } + + @Override + protected void onPreExecute() + { + progressBar.setIndeterminate(false); + progressBar.setProgress(0); + progressBar.setVisibility(View.VISIBLE); + } + + @Override + protected void onProgressUpdate(Integer... values) + { + progressBar.setMax(values[1]); + progressBar.setProgress(values[0]); + + if(lastLoadedTimestamp == null) + { + commit(); + adapter.notifyDataSetChanged(); + } + } + + @Override + protected void onPostExecute(Void aVoid) + { + if(isCancelled()) return; + + adapter.notifyDataSetChanged(); + updateEmptyMessage(); + + progressBar.setVisibility(View.INVISIBLE); + currentFetchTask = null; + lastLoadedTimestamp = DateHelper.getStartOfToday(); + } + + }; + + currentFetchTask.execute(); + } + + private void fetchHabit(final Long id) + { + new AsyncTask() + { + @Override + protected Void doInBackground(Void... params) + { + long dateTo = DateHelper.getStartOfDay(DateHelper.getLocalTime()); + long dateFrom = dateTo - (button_count - 1) * DateHelper.millisecondsInOneDay; + + Habit h = Habit.get(id); + habits.put(id, h); + scores.put(id, h.getScore()); + checkmarks.put(id, h.getCheckmarks(dateFrom, dateTo)); + + return null; + } + + @Override + protected void onPreExecute() + { + new Handler().postDelayed(new Runnable() + { + @Override + public void run() + { + if(getStatus() == Status.RUNNING) + { + progressBar.setIndeterminate(true); + progressBar.setVisibility(View.VISIBLE); + } + } + }, 500); + } + + @Override + protected void onPostExecute(Void aVoid) + { + progressBar.setVisibility(View.GONE); + adapter.notifyDataSetChanged(); + } + }.execute(); + } + @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { @@ -173,7 +335,7 @@ public class ListHabitsFragment extends Fragment getActivity().getMenuInflater().inflate(R.menu.list_habits_context, menu); AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo; - final Habit habit = Habit.get(info.id); + final Habit habit = habits.get(info.id); if(habit.isArchived()) menu.findItem(R.id.action_archive_habit).setVisible(false); @@ -197,7 +359,7 @@ public class ListHabitsFragment extends Fragment case R.id.action_show_archived: { Habit.setIncludeArchived(!Habit.isIncludeArchived()); - notifyDataSetChanged(); + fetchAllHabits(); activity.invalidateOptionsMenu(); return true; } @@ -212,7 +374,7 @@ public class ListHabitsFragment extends Fragment { AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuItem.getMenuInfo(); final int id = menuItem.getItemId(); - final Habit habit = Habit.get(info.id); + final Habit habit = habits.get(info.id); if (id == R.id.action_edit_habit) { @@ -224,12 +386,12 @@ public class ListHabitsFragment extends Fragment else if (id == R.id.action_archive_habit) { Command c = habit.new ArchiveCommand(); - executeCommand(c); + executeCommand(c, null); } else if (id == R.id.action_unarchive_habit) { Command c = habit.new UnarchiveCommand(); - executeCommand(c); + executeCommand(c, null); } return super.onContextItemSelected(menuItem); @@ -240,26 +402,28 @@ public class ListHabitsFragment extends Fragment { if (new Date().getTime() - lastLongClick < 1000) return; - Habit habit = Habit.getByPosition(position); + Habit habit = positionToHabit.get(position); habitClickListener.onHabitClicked(habit); } @Override - public void onSaved(Command command) + public void onSaved(Command command, Object savedObject) { - executeCommand(command); - ReminderHelper.createReminderAlarms(activity); - } + Habit h = (Habit) savedObject; - public void notifyDataSetChanged() - { - updateEmptyMessage(); + if(h == null) activity.executeCommand(command, null); + else activity.executeCommand(command, h.getId()); adapter.notifyDataSetChanged(); + + ReminderHelper.createReminderAlarms(activity); } private void updateEmptyMessage() { - llEmpty.setVisibility(Habit.getCount() > 0 ? View.GONE : View.VISIBLE); + if(lastLoadedTimestamp == null) + llEmpty.setVisibility(View.GONE); + else + llEmpty.setVisibility(habits.size() > 0 ? View.GONE : View.VISIBLE); } @Override @@ -286,24 +450,35 @@ public class ListHabitsFragment extends Fragment private void toggleCheck(View v) { - Habit habit = Habit.get((Long) v.getTag(R.string.habit_key)); + Habit habit = habits.get((Long) v.getTag(R.string.habit_key)); + int offset = (Integer) v.getTag(R.string.offset_key); long timestamp = DateHelper.getStartOfDay( DateHelper.getLocalTime() - offset * DateHelper.millisecondsInOneDay); - executeCommand(habit.new ToggleRepetitionCommand(timestamp)); + if(v.getTag(R.string.toggle_key).equals(2)) + updateCheck(habit.color, (TextView) v, 0); + else + updateCheck(habit.color, (TextView) v, 2); + + executeCommand(habit.new ToggleRepetitionCommand(timestamp), habit.getId()); } - private void executeCommand(Command c) + private void executeCommand(Command c, Long refreshKey) { - activity.executeCommand(c); + activity.executeCommand(c, refreshKey); } @Override public void drop(int from, int to) { + Habit fromHabit = positionToHabit.get(from); + Habit toHabit = positionToHabit.get(to); + positionToHabit.put(to, fromHabit); + positionToHabit.put(from, toHabit); + adapter.notifyDataSetChanged(); + Habit.reorder(from, to); - notifyDataSetChanged(); } @Override @@ -337,13 +512,13 @@ public class ListHabitsFragment extends Fragment @Override public int getCount() { - return Habit.getCount(); + return habits.size(); } @Override public Object getItem(int position) { - return Habit.getByPosition(position); + return positionToHabit.get(position); } @Override @@ -355,7 +530,7 @@ public class ListHabitsFragment extends Fragment @Override public View getView(int position, View view, ViewGroup parent) { - final Habit habit = (Habit) getItem(position); + final Habit habit = positionToHabit.get(position); if (view == null || (Long) view.getTag(R.id.KEY_TIMESTAMP) != DateHelper.getStartOfToday()) @@ -397,7 +572,6 @@ public class ListHabitsFragment extends Fragment LinearLayout llInner = (LinearLayout) view.findViewById(R.id.llInner); llInner.setTag(R.string.habit_key, habit.getId()); - int inactiveColor = Color.rgb(230, 230, 230); int activeColor = habit.color; tvName.setText(habit.name); @@ -413,17 +587,17 @@ public class ListHabitsFragment extends Fragment } else { - int score = habit.getScore(); + int score = scores.get(habit.getId()); if (score < Habit.HALF_STAR_CUTOFF) { tvStar.setText(context.getString(R.string.fa_star_o)); - tvStar.setTextColor(inactiveColor); + tvStar.setTextColor(INACTIVE_COLOR); } else if (score < Habit.FULL_STAR_CUTOFF) { tvStar.setText(context.getString(R.string.fa_star_half_o)); - tvStar.setTextColor(inactiveColor); + tvStar.setTextColor(INACTIVE_COLOR); } else { @@ -432,14 +606,10 @@ public class ListHabitsFragment extends Fragment } } - LinearLayout llButtons = (LinearLayout) view.findViewById(R.id.llButtons); int m = llButtons.getChildCount(); - long dateTo = DateHelper.getStartOfDay(DateHelper.getLocalTime()); - long dateFrom = dateTo - m * DateHelper.millisecondsInOneDay; - - int isChecked[] = habit.getReps(dateFrom, dateTo); + int isChecked[] = checkmarks.get(habit.getId()); for (int i = 0; i < m; i++) { @@ -447,28 +617,40 @@ public class ListHabitsFragment extends Fragment TextView tvCheck = (TextView) llButtons.getChildAt(i); tvCheck.setTag(R.string.habit_key, habit.getId()); tvCheck.setTag(R.string.offset_key, i); - - switch (isChecked[i]) - { - case 2: - tvCheck.setText(R.string.fa_check); - tvCheck.setTextColor(activeColor); - break; - - case 1: - tvCheck.setText(R.string.fa_check); - tvCheck.setTextColor(inactiveColor); - break; - - case 0: - tvCheck.setText(R.string.fa_times); - tvCheck.setTextColor(inactiveColor); - break; - } + updateCheck(activeColor, tvCheck, isChecked[i]); } return view; } + } + private void updateCheck(int activeColor, TextView tvCheck, int check) + { + switch (check) + { + case 2: + tvCheck.setText(R.string.fa_check); + tvCheck.setTextColor(activeColor); + tvCheck.setTag(R.string.toggle_key, 2); + break; + + case 1: + tvCheck.setText(R.string.fa_check); + tvCheck.setTextColor(INACTIVE_COLOR); + tvCheck.setTag(R.string.toggle_key, 1); + break; + + case 0: + tvCheck.setText(R.string.fa_times); + tvCheck.setTextColor(INACTIVE_COLOR); + tvCheck.setTag(R.string.toggle_key, 0); + break; + } + } + + public void onPostExecuteCommand(Long refreshKey) + { + if(refreshKey == null) fetchAllHabits(); + else fetchHabit(refreshKey); } } 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 b0eeef791..7b37a5a3e 100644 --- a/app/src/main/java/org/isoron/uhabits/dialogs/ShowHabitFragment.java +++ b/app/src/main/java/org/isoron/uhabits/dialogs/ShowHabitFragment.java @@ -46,6 +46,8 @@ public class ShowHabitFragment extends Fragment implements DialogHelper.OnSavedL activity = (ShowHabitActivity) getActivity(); habit = activity.habit; + habit.updateCheckmarks(); + if (android.os.Build.VERSION.SDK_INT >= 21) { int darkerHabitColor = ColorHelper.mixColors(habit.color, Color.BLACK, 0.75f); @@ -108,9 +110,13 @@ public class ShowHabitFragment extends Fragment implements DialogHelper.OnSavedL } @Override - public void onSaved(Command command) + public void onSaved(Command command, Object savedObject) { - activity.executeCommand(command); + Habit h = (Habit) savedObject; + + if(h == null) activity.executeCommand(command, null); + else activity.executeCommand(command, h.getId()); + ReminderHelper.createReminderAlarms(activity); activity.recreate(); } diff --git a/app/src/main/java/org/isoron/uhabits/models/Checkmark.java b/app/src/main/java/org/isoron/uhabits/models/Checkmark.java new file mode 100644 index 000000000..2d5ea0315 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/models/Checkmark.java @@ -0,0 +1,29 @@ +package org.isoron.uhabits.models; + +import com.activeandroid.Model; +import com.activeandroid.annotation.Column; +import com.activeandroid.annotation.Table; + +@Table(name = "Checkmarks") +public class Checkmark extends Model +{ + + public static final int UNCHECKED = 0; + public static final int CHECKED_IMPLICITLY = 1; + public static final int CHECKED_EXPLICITLY = 2; + + @Column(name = "habit") + public Habit habit; + + @Column(name = "timestamp") + public Long timestamp; + + /** + * Indicates whether there is a checkmark at the given timestamp or not, and whether the + * checkmark is explicit or implicit. An explicit checkmark indicates that there is a + * repetition at that day. An implicit checkmark indicates that there is no repetition at that + * day, but a repetition was not needed, due to the frequency of the habit. + */ + @Column(name = "value") + public Integer value; +} 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 996245d79..284375e75 100644 --- a/app/src/main/java/org/isoron/uhabits/models/Habit.java +++ b/app/src/main/java/org/isoron/uhabits/models/Habit.java @@ -3,9 +3,8 @@ package org.isoron.uhabits.models; import android.annotation.SuppressLint; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; -import android.os.AsyncTask; -import android.util.Log; +import com.activeandroid.ActiveAndroid; import com.activeandroid.Cache; import com.activeandroid.Model; import com.activeandroid.annotation.Column; @@ -21,6 +20,8 @@ import org.isoron.helpers.Command; import org.isoron.helpers.DateHelper; import org.isoron.uhabits.R; +import java.util.ArrayList; +import java.util.HashMap; import java.util.List; @Table(name = "Habits") @@ -83,6 +84,19 @@ public class Habit extends Model return Habit.load(Habit.class, id); } + public static HashMap getAll() + { + List habits = select().execute(); + HashMap map = new HashMap<>(); + + for(Habit h : habits) + { + map.put(h.getId(), h); + } + + return map; + } + @SuppressLint("DefaultLocale") public static void updateId(long oldId, long newId) { @@ -150,15 +164,25 @@ public class Habit extends Model public static void rebuildOrder() { - Log.d("X", "rebuilding order"); - List habits = select().execute(); - int i = 0; - for (Habit h : habits) + + ActiveAndroid.beginTransaction(); + try { - h.position = i++; - h.save(); + int i = 0; + for (Habit h : habits) + { + h.position = i++; + h.save(); + } + + ActiveAndroid.setTransactionSuccessful(); } + finally + { + ActiveAndroid.endTransaction(); + } + } public static void roundTimestamps() @@ -235,39 +259,127 @@ public class Habit extends Model .and("timestamp = ?", timestamp).execute(); } - public int[] getReps(long timeFrom, long timeTo) + 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(); + + 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()); + } + + return checks; + } + + public void updateCheckmarks() { - long timeFromExtended = timeFrom - (long) (freq_den) * DateHelper.millisecondsInOneDay; - List reps = selectRepsFromTo(timeFromExtended, timeTo).execute(); + long beginning; + long today = DateHelper.getStartOfToday(); + long day = DateHelper.millisecondsInOneDay; - int nDaysExtended = (int) ((timeTo - timeFromExtended) / DateHelper.millisecondsInOneDay); - int checkExtended[] = new int[nDaysExtended + 1]; + Checkmark newestCheckmark = getNewestCheckmark(); + if(newestCheckmark == null) + { + Repetition oldestRep = getOldestRep(); + if (oldestRep == null) return; - int nDays = (int) ((timeTo - timeFrom) / DateHelper.millisecondsInOneDay); + beginning = oldestRep.timestamp; + } + else + { + beginning = newestCheckmark.timestamp + day; + } - // mark explicit checks + if(beginning > today) + return; + + long beginningExtended = beginning - (long) (freq_den) * 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 - timeFrom) / DateHelper.millisecondsInOneDay); - checkExtended[nDays - offset] = 2; + int offset = (int) ((rep.timestamp - beginningExtended) / day); + checks[nDaysExtended - offset - 1] = 2; } - // marks implicit checks + // 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 (checks[i + j] == 2) counter++; - if (counter >= freq_num) checkExtended[i] = Math.max(checkExtended[i], 1); + if (counter >= freq_num) checks[i] = Math.max(checks[i], 1); } - int check[] = new int[nDays + 1]; - for (int i = 0; i < nDays + 1; i++) - check[i] = checkExtended[i]; + 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(); + } + } - return check; + public Checkmark getNewestCheckmark() + { + return new Select().from(Checkmark.class) + .where("habit = ?", getId()) + .orderBy("timestamp desc") + .limit(1) + .executeSingle(); } public int getRepsCount(int days) @@ -280,7 +392,7 @@ public class Habit extends Model public boolean hasImplicitRepToday() { long today = DateHelper.getStartOfToday(); - int reps[] = getReps(today - DateHelper.millisecondsInOneDay, today); + int reps[] = getCheckmarks(today - DateHelper.millisecondsInOneDay, today); return (reps[0] > 0); } @@ -289,12 +401,21 @@ public class Habit extends Model 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 + } + else { Repetition rep = new Repetition(); rep.habit = this; @@ -303,6 +424,8 @@ public class Habit extends Model } deleteScoresNewerThan(timestamp); + deleteCheckmarksNewerThan(timestamp); + deleteStreaksNewerThan(timestamp); } public void archive() @@ -311,7 +434,7 @@ public class Habit extends Model position = 9999; save(); - if(!isIncludeArchived()) Habit.rebuildOrder(); + Habit.rebuildOrder(); } public void unarchive() @@ -360,7 +483,8 @@ public class Habit extends Model if (oldestRep == null) return 0; beginningTime = oldestRep.timestamp; beginningScore = 0; - } else + } + else { beginningTime = newestScore.timestamp + day; beginningScore = newestScore.score; @@ -369,23 +493,34 @@ public class Habit extends Model long nDays = (today - beginningTime) / day; if (nDays < 0) return newestScore.score; - int reps[] = getReps(beginningTime, today); + int reps[] = getCheckmarks(beginningTime, today); + ActiveAndroid.beginTransaction(); int lastScore = beginningScore; - for (int i = 0; i < reps.length; i++) + + try { - Score s = new Score(); - s.habit = this; - s.timestamp = beginningTime + day * i; - s.score = (int) (lastScore * multiplier); - if (reps[reps.length - i - 1] == 2) + for (int i = 0; i < reps.length; i++) { - s.score += 1000000; - s.score = Math.min(s.score, 19259500); + 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; } - s.save(); - lastScore = s.score; + ActiveAndroid.setTransactionSuccessful(); + } + finally + { + ActiveAndroid.endTransaction(); } return lastScore; @@ -403,41 +538,89 @@ public class Habit extends Model offset, divisor).execute(); } - public long[] getStreaks() + public List 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 = ?"; + updateStreaks(); - 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;"; + return new Select() + .from(Streak.class) + .where("habit = ?", getId()) + .orderBy("end asc") + .execute(); + } - String args[] = {getId().toString()}; + public Streak getNewestStreak() + { + return new Select() + .from(Streak.class) + .where("habit = ?", getId()) + .orderBy("end desc") + .limit(1) + .executeSingle(); + } - SQLiteDatabase db = Cache.openDatabase(); - db.beginTransaction(); - db.execSQL(query, args); - Cursor cursor = db.rawQuery(query2, null); + 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; - long streaks[] = new long[cursor.getCount()]; - int current = 0; + int checks[] = getCheckmarks(beginning, today); + ArrayList list = new ArrayList<>(); - Log.d("Streaks", String.format("%d rows", cursor.getCount())); + long current = beginning; + list.add(current); - if (cursor.moveToFirst()) + for(int i = 1; i < checks.length; i++) { - do - { - streaks[current++] = cursor.getLong(0); - } while (cursor.moveToNext()); + 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); } - db.endTransaction(); - return streaks; + 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(); + } } public static class CreateCommand extends Command @@ -508,28 +691,9 @@ public class Habit extends Model habit.save(); if (hasIntervalChanged) { - new AsyncTask() - { - @Override - protected Integer doInBackground(Habit... habits) - { - // HACK: We wait one second before deleting old score, otherwise the view will - // trigger the very slow getScore on the main thread at the same time, or even - // before us. - try - { - Thread.sleep(1000); - } catch (InterruptedException e) - { - e.printStackTrace(); - } - - habits[0].deleteScoresNewerThan(0); - habits[0].getScore(); - - return 0; - } - }.execute(habit); + habit.deleteCheckmarksNewerThan(0); + habit.deleteStreaksNewerThan(0); + habit.deleteScoresNewerThan(0); } } @@ -540,26 +704,9 @@ public class Habit extends Model habit.save(); if (hasIntervalChanged) { - new AsyncTask() - { - @Override - protected Integer doInBackground(Habit... habits) - { - try - { - Thread.sleep(1000); - } catch (InterruptedException e) - { - e.printStackTrace(); - } - - habits[0].deleteScoresNewerThan(0); - habits[0].getScore(); - - return 0; - } - }.execute(habit); - + habit.deleteCheckmarksNewerThan(0); + habit.deleteStreaksNewerThan(0); + habit.deleteScoresNewerThan(0); } } diff --git a/app/src/main/java/org/isoron/uhabits/models/Streak.java b/app/src/main/java/org/isoron/uhabits/models/Streak.java new file mode 100644 index 000000000..17b223c8d --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/models/Streak.java @@ -0,0 +1,19 @@ +package org.isoron.uhabits.models; + +import com.activeandroid.Model; +import com.activeandroid.annotation.Column; + +public class Streak extends Model +{ + @Column(name = "habit") + public Habit habit; + + @Column(name = "start") + public Long start; + + @Column(name = "end") + public Long end; + + @Column(name = "length") + public Long length; +} 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 68c9bd082..2a561c2b6 100644 --- a/app/src/main/java/org/isoron/uhabits/views/HabitHistoryView.java +++ b/app/src/main/java/org/isoron/uhabits/views/HabitHistoryView.java @@ -13,17 +13,19 @@ import android.view.View; import org.isoron.helpers.ColorHelper; import org.isoron.helpers.DateHelper; import org.isoron.uhabits.R; +import org.isoron.uhabits.models.Checkmark; import org.isoron.uhabits.models.Habit; import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.GregorianCalendar; +import java.util.List; public class HabitHistoryView extends View { private Habit habit; - private int reps[]; + private int[] checks; private Context context; private Paint pSquareBg, pSquareFg, pTextHeader; @@ -93,7 +95,7 @@ public class HabitHistoryView extends View for (int i = 0; i < nColumns * 7; i++) dateFrom -= DateHelper.millisecondsInOneDay; - reps = habit.getReps(dateFrom, dateTo); + checks = habit.getCheckmarks(dateFrom, dateTo); } @Override @@ -159,7 +161,9 @@ public class HabitHistoryView extends View { if (!(i == nColumns - 1 && offsetWeeks == 0 && j > todayWeekday)) { - pSquareBg.setColor(colors[reps[k]]); + if(k >= checks.length) pSquareBg.setColor(colors[0]); + else pSquareBg.setColor(colors[checks[k]]); + canvas.drawRect(square, pSquareBg); canvas.drawText(Integer.toString(currentDate.get(Calendar.DAY_OF_MONTH)), square.centerX(), square.centerY() + squareTextOffset, pSquareFg); 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 ec02a7a40..eaf8bbd42 100644 --- a/app/src/main/java/org/isoron/uhabits/views/HabitStreakView.java +++ b/app/src/main/java/org/isoron/uhabits/views/HabitStreakView.java @@ -12,10 +12,10 @@ import android.view.View; import org.isoron.helpers.ColorHelper; import org.isoron.helpers.DateHelper; import org.isoron.uhabits.models.Habit; +import org.isoron.uhabits.models.Streak; import java.text.SimpleDateFormat; -import java.util.Calendar; -import java.util.GregorianCalendar; +import java.util.List; public class HabitStreakView extends View { @@ -23,10 +23,9 @@ public class HabitStreakView extends View private int columnWidth, columnHeight, nColumns; private Paint pText, pBar; - private long streaks[]; + private List streaks; private int dataOffset; - private long streakStart[], streakEnd[], streakLength[]; private long maxStreakLength; private int barHeaderHeight; @@ -68,17 +67,9 @@ public class HabitStreakView extends View 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 - + diff --git a/app/src/main/res/layout/list_habits_fragment.xml b/app/src/main/res/layout/list_habits_fragment.xml index f458d0c2b..e3970fc48 100644 --- a/app/src/main/res/layout/list_habits_fragment.xml +++ b/app/src/main/res/layout/list_habits_fragment.xml @@ -52,4 +52,12 @@ style="@style/habitsListButtonsPanelStyle"/> + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d89e8b58f..3bc9ea90e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -41,6 +41,7 @@ sans-serif + diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index 625b63db6..eb3cabc62 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -43,14 +43,9 @@ android:action="android.intent.action.VIEW" android:data="https://github.com/iSoron/uhabits"/> - - - - +