Load data asynchronously; cache checkmarks and streaks

pull/30/head
Alinson S. Xavier 10 years ago
parent be58d29672
commit 404cc82348

@ -21,7 +21,7 @@
<meta-data <meta-data
android:name="AA_DB_VERSION" android:name="AA_DB_VERSION"
android:value="7"/> android:value="9"/>
<meta-data <meta-data
android:name="com.google.android.backup.api_key" android:name="com.google.android.backup.api_key"

@ -29,7 +29,7 @@ public abstract class DialogHelper
public static interface OnSavedListener public static interface OnSavedListener
{ {
public void onSaved(Command command); public void onSaved(Command command, Object savedObject);
} }
public static void showSoftKeyboard(View view) public static void showSoftKeyboard(View view)

@ -2,6 +2,7 @@ package org.isoron.helpers;
import android.app.Activity; import android.app.Activity;
import android.app.backup.BackupManager; import android.app.backup.BackupManager;
import android.os.AsyncTask;
import android.os.Bundle; import android.os.Bundle;
import android.widget.Toast; import android.widget.Toast;
@ -26,10 +27,9 @@ abstract public class ReplayableActivity extends Activity
redoList = new LinkedList<>(); redoList = new LinkedList<>();
} }
public void executeCommand(Command command) public void executeCommand(Command command, Long refreshKey)
{ {
executeCommand(command, false); executeCommand(command, false, refreshKey);
BackupManager.dataChanged("org.isoron.uhabits");
} }
protected void undo() protected void undo()
@ -54,7 +54,7 @@ abstract public class ReplayableActivity extends Activity
return; return;
} }
Command last = redoList.pop(); Command last = redoList.pop();
executeCommand(last, false); executeCommand(last, false, null);
} }
public void showToast(Integer stringId) public void showToast(Integer stringId)
@ -65,14 +65,36 @@ abstract public class ReplayableActivity extends Activity
toast.show(); toast.show();
} }
public void executeCommand(final Command command, Boolean clearRedoStack,
public void executeCommand(Command command, boolean clearRedoStack) final Long refreshKey)
{ {
undoList.push(command); undoList.push(command);
if (undoList.size() > MAX_UNDO_LEVEL) undoList.removeLast(); if (undoList.size() > MAX_UNDO_LEVEL) undoList.removeLast();
if (clearRedoStack) redoList.clear(); if (clearRedoStack) redoList.clear();
command.execute(); new AsyncTask<Void, Void, Void>()
{
@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()); showToast(command.getExecuteStringId());
} }
public void onPostExecuteCommand(Long refreshKey)
{
}
} }

@ -6,6 +6,7 @@ import android.content.SharedPreferences;
import android.net.Uri; import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.preference.PreferenceManager; import android.preference.PreferenceManager;
import android.util.Log;
import android.view.Menu; import android.view.Menu;
import android.view.MenuItem; import android.view.MenuItem;
@ -33,6 +34,7 @@ public class MainActivity extends ReplayableActivity
ReminderHelper.createReminderAlarms(MainActivity.this); ReminderHelper.createReminderAlarms(MainActivity.this);
showTutorial(); showTutorial();
} }
private void showTutorial() private void showTutorial()
@ -51,13 +53,6 @@ public class MainActivity extends ReplayableActivity
} }
} }
@Override
protected void onStart()
{
super.onStart();
listHabitsFragment.notifyDataSetChanged();
}
@Override @Override
public boolean onCreateOptionsMenu(Menu menu) public boolean onCreateOptionsMenu(Menu menu)
{ {
@ -89,9 +84,8 @@ public class MainActivity extends ReplayableActivity
} }
@Override @Override
public void executeCommand(Command command) public void onPostExecuteCommand(Long refreshKey)
{ {
super.executeCommand(command); listHabitsFragment.onPostExecuteCommand(refreshKey);
listHabitsFragment.notifyDataSetChanged();
} }
} }

@ -263,14 +263,19 @@ public class EditHabitFragment extends DialogFragment implements OnClickListener
editor.putInt("pref_default_habit_freq_den", modified_habit.freq_den); editor.putInt("pref_default_habit_freq_den", modified_habit.freq_den);
editor.apply(); editor.apply();
Habit savedHabit = null;
if(mode == EDIT_MODE) if(mode == EDIT_MODE)
{
command = originalHabit.new EditCommand(modified_habit); command = originalHabit.new EditCommand(modified_habit);
savedHabit = originalHabit;
}
if(mode == CREATE_MODE) if(mode == CREATE_MODE)
command = new Habit.CreateCommand(modified_habit); command = new Habit.CreateCommand(modified_habit);
if(onSavedListener != null) if(onSavedListener != null)
onSavedListener.onSaved(command); onSavedListener.onSaved(command, savedHabit);
dismiss(); dismiss();
} }

@ -7,7 +7,9 @@ import android.content.SharedPreferences;
import android.graphics.Color; import android.graphics.Color;
import android.graphics.Point; import android.graphics.Point;
import android.graphics.Typeface; import android.graphics.Typeface;
import android.os.AsyncTask;
import android.os.Bundle; import android.os.Bundle;
import android.os.Handler;
import android.os.Vibrator; import android.os.Vibrator;
import android.preference.PreferenceManager; import android.preference.PreferenceManager;
import android.util.DisplayMetrics; import android.util.DisplayMetrics;
@ -30,6 +32,7 @@ import android.widget.BaseAdapter;
import android.widget.Button; import android.widget.Button;
import android.widget.LinearLayout; import android.widget.LinearLayout;
import android.widget.LinearLayout.LayoutParams; import android.widget.LinearLayout.LayoutParams;
import android.widget.ProgressBar;
import android.widget.TextView; import android.widget.TextView;
import com.mobeta.android.dslv.DragSortController; import com.mobeta.android.dslv.DragSortController;
@ -47,6 +50,7 @@ import org.isoron.uhabits.models.Habit;
import java.util.Date; import java.util.Date;
import java.util.GregorianCalendar; import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.Locale; import java.util.Locale;
import java.util.TimeZone; import java.util.TimeZone;
@ -54,6 +58,9 @@ public class ListHabitsFragment extends Fragment
implements OnSavedListener, OnItemClickListener, OnLongClickListener, DropListener, implements OnSavedListener, OnItemClickListener, OnLongClickListener, DropListener,
OnClickListener OnClickListener
{ {
public static final int INACTIVE_COLOR = Color.rgb(230, 230, 230);
public interface OnHabitClickListener public interface OnHabitClickListener
{ {
void onHabitClicked(Habit habit); void onHabitClicked(Habit habit);
@ -67,23 +74,39 @@ public class ListHabitsFragment extends Fragment
private int tvNameWidth; private int tvNameWidth;
private int button_count; private int button_count;
private View llEmpty; private View llEmpty;
private ProgressBar progressBar;
private OnHabitClickListener habitClickListener; private OnHabitClickListener habitClickListener;
private boolean short_toggle_enabled; private boolean short_toggle_enabled;
private HashMap<Long, Habit> habits;
private HashMap<Integer, Habit> positionToHabit;
private HashMap<Long, int[]> checkmarks;
private HashMap<Long, Integer> scores;
private Long lastLoadedTimestamp = null;
private AsyncTask<Void, Integer, Void> currentFetchTask = null;
@Override @Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) Bundle savedInstanceState)
{ {
View view = inflater.inflate(R.layout.list_habits_fragment, container, false);
DisplayMetrics dm = getResources().getDisplayMetrics(); DisplayMetrics dm = getResources().getDisplayMetrics();
int width = (int) (dm.widthPixels / dm.density); int width = (int) (dm.widthPixels / dm.density);
button_count = (int) ((width - 160) / 42); button_count = (int) ((width - 160) / 42);
tvNameWidth = (int) ((width - 30 - button_count * 42) * dm.density); 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); tvNameHeader = (TextView) view.findViewById(R.id.tvNameHeader);
progressBar = (ProgressBar) view.findViewById(R.id.progressBar);
progressBar.setVisibility(View.INVISIBLE);
adapter = new ListHabitsAdapter(getActivity()); adapter = new ListHabitsAdapter(getActivity());
listView = (DragSortListView) view.findViewById(R.id.listView); listView = (DragSortListView) view.findViewById(R.id.listView);
listView.setAdapter(adapter); listView.setAdapter(adapter);
@ -107,7 +130,6 @@ public class ListHabitsFragment extends Fragment
llEmpty = view.findViewById(R.id.llEmpty); llEmpty = view.findViewById(R.id.llEmpty);
updateEmptyMessage(); updateEmptyMessage();
setHasOptionsMenu(true); setHasOptionsMenu(true);
return view; return view;
} }
@ -124,7 +146,14 @@ public class ListHabitsFragment extends Fragment
public void onResume() public void onResume()
{ {
super.onResume(); super.onResume();
updateHeader(); if(lastLoadedTimestamp == null || lastLoadedTimestamp != DateHelper.getStartOfToday())
{
updateHeader();
fetchAllHabits();
updateEmptyMessage();
}
adapter.notifyDataSetChanged();
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(activity); SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(activity);
short_toggle_enabled = prefs.getBoolean("pref_short_toggle", false); 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<Void, Integer, Void>()
{
HashMap<Long, Habit> newHabits = Habit.getAll();
HashMap<Integer, Habit> newPositionToHabit = new HashMap<>();
HashMap<Long, int[]> newCheckmarks = new HashMap<>();
HashMap<Long, Integer> 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<Void, Void, Void>()
{
@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 @Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) 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); getActivity().getMenuInflater().inflate(R.menu.list_habits_context, menu);
AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo; AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo;
final Habit habit = Habit.get(info.id); final Habit habit = habits.get(info.id);
if(habit.isArchived()) if(habit.isArchived())
menu.findItem(R.id.action_archive_habit).setVisible(false); menu.findItem(R.id.action_archive_habit).setVisible(false);
@ -197,7 +359,7 @@ public class ListHabitsFragment extends Fragment
case R.id.action_show_archived: case R.id.action_show_archived:
{ {
Habit.setIncludeArchived(!Habit.isIncludeArchived()); Habit.setIncludeArchived(!Habit.isIncludeArchived());
notifyDataSetChanged(); fetchAllHabits();
activity.invalidateOptionsMenu(); activity.invalidateOptionsMenu();
return true; return true;
} }
@ -212,7 +374,7 @@ public class ListHabitsFragment extends Fragment
{ {
AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuItem.getMenuInfo(); AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuItem.getMenuInfo();
final int id = menuItem.getItemId(); 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) if (id == R.id.action_edit_habit)
{ {
@ -224,12 +386,12 @@ public class ListHabitsFragment extends Fragment
else if (id == R.id.action_archive_habit) else if (id == R.id.action_archive_habit)
{ {
Command c = habit.new ArchiveCommand(); Command c = habit.new ArchiveCommand();
executeCommand(c); executeCommand(c, null);
} }
else if (id == R.id.action_unarchive_habit) else if (id == R.id.action_unarchive_habit)
{ {
Command c = habit.new UnarchiveCommand(); Command c = habit.new UnarchiveCommand();
executeCommand(c); executeCommand(c, null);
} }
return super.onContextItemSelected(menuItem); return super.onContextItemSelected(menuItem);
@ -240,26 +402,28 @@ public class ListHabitsFragment extends Fragment
{ {
if (new Date().getTime() - lastLongClick < 1000) return; if (new Date().getTime() - lastLongClick < 1000) return;
Habit habit = Habit.getByPosition(position); Habit habit = positionToHabit.get(position);
habitClickListener.onHabitClicked(habit); habitClickListener.onHabitClicked(habit);
} }
@Override @Override
public void onSaved(Command command) public void onSaved(Command command, Object savedObject)
{ {
executeCommand(command); Habit h = (Habit) savedObject;
ReminderHelper.createReminderAlarms(activity);
}
public void notifyDataSetChanged() if(h == null) activity.executeCommand(command, null);
{ else activity.executeCommand(command, h.getId());
updateEmptyMessage();
adapter.notifyDataSetChanged(); adapter.notifyDataSetChanged();
ReminderHelper.createReminderAlarms(activity);
} }
private void updateEmptyMessage() 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 @Override
@ -286,24 +450,35 @@ public class ListHabitsFragment extends Fragment
private void toggleCheck(View v) 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); int offset = (Integer) v.getTag(R.string.offset_key);
long timestamp = DateHelper.getStartOfDay( long timestamp = DateHelper.getStartOfDay(
DateHelper.getLocalTime() - offset * DateHelper.millisecondsInOneDay); 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 @Override
public void drop(int from, int to) 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); Habit.reorder(from, to);
notifyDataSetChanged();
} }
@Override @Override
@ -337,13 +512,13 @@ public class ListHabitsFragment extends Fragment
@Override @Override
public int getCount() public int getCount()
{ {
return Habit.getCount(); return habits.size();
} }
@Override @Override
public Object getItem(int position) public Object getItem(int position)
{ {
return Habit.getByPosition(position); return positionToHabit.get(position);
} }
@Override @Override
@ -355,7 +530,7 @@ public class ListHabitsFragment extends Fragment
@Override @Override
public View getView(int position, View view, ViewGroup parent) 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) != if (view == null || (Long) view.getTag(R.id.KEY_TIMESTAMP) !=
DateHelper.getStartOfToday()) DateHelper.getStartOfToday())
@ -397,7 +572,6 @@ public class ListHabitsFragment extends Fragment
LinearLayout llInner = (LinearLayout) view.findViewById(R.id.llInner); LinearLayout llInner = (LinearLayout) view.findViewById(R.id.llInner);
llInner.setTag(R.string.habit_key, habit.getId()); llInner.setTag(R.string.habit_key, habit.getId());
int inactiveColor = Color.rgb(230, 230, 230);
int activeColor = habit.color; int activeColor = habit.color;
tvName.setText(habit.name); tvName.setText(habit.name);
@ -413,17 +587,17 @@ public class ListHabitsFragment extends Fragment
} }
else else
{ {
int score = habit.getScore(); int score = scores.get(habit.getId());
if (score < Habit.HALF_STAR_CUTOFF) if (score < Habit.HALF_STAR_CUTOFF)
{ {
tvStar.setText(context.getString(R.string.fa_star_o)); tvStar.setText(context.getString(R.string.fa_star_o));
tvStar.setTextColor(inactiveColor); tvStar.setTextColor(INACTIVE_COLOR);
} }
else if (score < Habit.FULL_STAR_CUTOFF) else if (score < Habit.FULL_STAR_CUTOFF)
{ {
tvStar.setText(context.getString(R.string.fa_star_half_o)); tvStar.setText(context.getString(R.string.fa_star_half_o));
tvStar.setTextColor(inactiveColor); tvStar.setTextColor(INACTIVE_COLOR);
} }
else else
{ {
@ -432,14 +606,10 @@ public class ListHabitsFragment extends Fragment
} }
} }
LinearLayout llButtons = (LinearLayout) view.findViewById(R.id.llButtons); LinearLayout llButtons = (LinearLayout) view.findViewById(R.id.llButtons);
int m = llButtons.getChildCount(); int m = llButtons.getChildCount();
long dateTo = DateHelper.getStartOfDay(DateHelper.getLocalTime()); int isChecked[] = checkmarks.get(habit.getId());
long dateFrom = dateTo - m * DateHelper.millisecondsInOneDay;
int isChecked[] = habit.getReps(dateFrom, dateTo);
for (int i = 0; i < m; i++) for (int i = 0; i < m; i++)
{ {
@ -447,28 +617,40 @@ public class ListHabitsFragment extends Fragment
TextView tvCheck = (TextView) llButtons.getChildAt(i); TextView tvCheck = (TextView) llButtons.getChildAt(i);
tvCheck.setTag(R.string.habit_key, habit.getId()); tvCheck.setTag(R.string.habit_key, habit.getId());
tvCheck.setTag(R.string.offset_key, i); tvCheck.setTag(R.string.offset_key, i);
updateCheck(activeColor, tvCheck, isChecked[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;
}
} }
return view; 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);
} }
} }

@ -46,6 +46,8 @@ public class ShowHabitFragment extends Fragment implements DialogHelper.OnSavedL
activity = (ShowHabitActivity) getActivity(); activity = (ShowHabitActivity) getActivity();
habit = activity.habit; habit = activity.habit;
habit.updateCheckmarks();
if (android.os.Build.VERSION.SDK_INT >= 21) if (android.os.Build.VERSION.SDK_INT >= 21)
{ {
int darkerHabitColor = ColorHelper.mixColors(habit.color, Color.BLACK, 0.75f); int darkerHabitColor = ColorHelper.mixColors(habit.color, Color.BLACK, 0.75f);
@ -108,9 +110,13 @@ public class ShowHabitFragment extends Fragment implements DialogHelper.OnSavedL
} }
@Override @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); ReminderHelper.createReminderAlarms(activity);
activity.recreate(); activity.recreate();
} }

@ -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;
}

@ -3,9 +3,8 @@ package org.isoron.uhabits.models;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.database.Cursor; import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteDatabase;
import android.os.AsyncTask;
import android.util.Log;
import com.activeandroid.ActiveAndroid;
import com.activeandroid.Cache; import com.activeandroid.Cache;
import com.activeandroid.Model; import com.activeandroid.Model;
import com.activeandroid.annotation.Column; import com.activeandroid.annotation.Column;
@ -21,6 +20,8 @@ import org.isoron.helpers.Command;
import org.isoron.helpers.DateHelper; import org.isoron.helpers.DateHelper;
import org.isoron.uhabits.R; import org.isoron.uhabits.R;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List; import java.util.List;
@Table(name = "Habits") @Table(name = "Habits")
@ -83,6 +84,19 @@ public class Habit extends Model
return Habit.load(Habit.class, id); return Habit.load(Habit.class, id);
} }
public static HashMap<Long, Habit> getAll()
{
List<Habit> habits = select().execute();
HashMap<Long, Habit> map = new HashMap<>();
for(Habit h : habits)
{
map.put(h.getId(), h);
}
return map;
}
@SuppressLint("DefaultLocale") @SuppressLint("DefaultLocale")
public static void updateId(long oldId, long newId) public static void updateId(long oldId, long newId)
{ {
@ -150,15 +164,25 @@ public class Habit extends Model
public static void rebuildOrder() public static void rebuildOrder()
{ {
Log.d("X", "rebuilding order");
List<Habit> habits = select().execute(); List<Habit> habits = select().execute();
int i = 0;
for (Habit h : habits) ActiveAndroid.beginTransaction();
try
{ {
h.position = i++; int i = 0;
h.save(); for (Habit h : habits)
{
h.position = i++;
h.save();
}
ActiveAndroid.setTransactionSuccessful();
} }
finally
{
ActiveAndroid.endTransaction();
}
} }
public static void roundTimestamps() public static void roundTimestamps()
@ -235,39 +259,127 @@ public class Habit extends Model
.and("timestamp = ?", timestamp).execute(); .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; long beginning;
List<Repetition> reps = selectRepsFromTo(timeFromExtended, timeTo).execute(); long today = DateHelper.getStartOfToday();
long day = DateHelper.millisecondsInOneDay;
int nDaysExtended = (int) ((timeTo - timeFromExtended) / DateHelper.millisecondsInOneDay); Checkmark newestCheckmark = getNewestCheckmark();
int checkExtended[] = new int[nDaysExtended + 1]; 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<Repetition> 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) for (Repetition rep : reps)
{ {
int offset = (int) ((rep.timestamp - timeFrom) / DateHelper.millisecondsInOneDay); int offset = (int) ((rep.timestamp - beginningExtended) / day);
checkExtended[nDays - offset] = 2; checks[nDaysExtended - offset - 1] = 2;
} }
// marks implicit checks // implicit checks
for (int i = 0; i < nDays; i++) for (int i = 0; i < nDays; i++)
{ {
int counter = 0; int counter = 0;
for (int j = 0; j < freq_den; j++) 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]; ActiveAndroid.beginTransaction();
for (int i = 0; i < nDays + 1; i++)
check[i] = checkExtended[i]; 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) public int getRepsCount(int days)
@ -280,7 +392,7 @@ public class Habit extends Model
public boolean hasImplicitRepToday() public boolean hasImplicitRepToday()
{ {
long today = DateHelper.getStartOfToday(); long today = DateHelper.getStartOfToday();
int reps[] = getReps(today - DateHelper.millisecondsInOneDay, today); int reps[] = getCheckmarks(today - DateHelper.millisecondsInOneDay, today);
return (reps[0] > 0); return (reps[0] > 0);
} }
@ -289,12 +401,21 @@ public class Habit extends Model
return (Repetition) selectReps().limit(1).executeSingle(); return (Repetition) selectReps().limit(1).executeSingle();
} }
public Repetition getOldestRepNewerThan(long timestamp)
{
return selectReps()
.where("timestamp > ?", timestamp)
.limit(1)
.executeSingle();
}
public void toggleRepetition(long timestamp) public void toggleRepetition(long timestamp)
{ {
if (hasRep(timestamp)) if (hasRep(timestamp))
{ {
deleteReps(timestamp); deleteReps(timestamp);
} else }
else
{ {
Repetition rep = new Repetition(); Repetition rep = new Repetition();
rep.habit = this; rep.habit = this;
@ -303,6 +424,8 @@ public class Habit extends Model
} }
deleteScoresNewerThan(timestamp); deleteScoresNewerThan(timestamp);
deleteCheckmarksNewerThan(timestamp);
deleteStreaksNewerThan(timestamp);
} }
public void archive() public void archive()
@ -311,7 +434,7 @@ public class Habit extends Model
position = 9999; position = 9999;
save(); save();
if(!isIncludeArchived()) Habit.rebuildOrder(); Habit.rebuildOrder();
} }
public void unarchive() public void unarchive()
@ -360,7 +483,8 @@ public class Habit extends Model
if (oldestRep == null) return 0; if (oldestRep == null) return 0;
beginningTime = oldestRep.timestamp; beginningTime = oldestRep.timestamp;
beginningScore = 0; beginningScore = 0;
} else }
else
{ {
beginningTime = newestScore.timestamp + day; beginningTime = newestScore.timestamp + day;
beginningScore = newestScore.score; beginningScore = newestScore.score;
@ -369,23 +493,34 @@ public class Habit extends Model
long nDays = (today - beginningTime) / day; long nDays = (today - beginningTime) / day;
if (nDays < 0) return newestScore.score; if (nDays < 0) return newestScore.score;
int reps[] = getReps(beginningTime, today); int reps[] = getCheckmarks(beginningTime, today);
ActiveAndroid.beginTransaction();
int lastScore = beginningScore; int lastScore = beginningScore;
for (int i = 0; i < reps.length; i++)
try
{ {
Score s = new Score(); for (int i = 0; i < reps.length; i++)
s.habit = this;
s.timestamp = beginningTime + day * i;
s.score = (int) (lastScore * multiplier);
if (reps[reps.length - i - 1] == 2)
{ {
s.score += 1000000; Score s = new Score();
s.score = Math.min(s.score, 19259500); 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; return lastScore;
@ -403,41 +538,89 @@ public class Habit extends Model
offset, divisor).execute(); offset, divisor).execute();
} }
public long[] getStreaks() public List<Streak> getStreaks()
{ {
String query = updateStreaks();
"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 = return new Select()
"select time from T, (select 0 union select 1) where neighbors = 0 union all\n" + .from(Streak.class)
"select time from T where neighbors = 1 order by time;"; .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(); public void updateStreaks()
db.beginTransaction(); {
db.execSQL(query, args); long beginning;
Cursor cursor = db.rawQuery(query2, null); 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 checks[] = getCheckmarks(beginning, today);
int current = 0; ArrayList<Long> 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 current += day;
{ int j = checks.length - i - 1;
streaks[current++] = cursor.getLong(0);
} while (cursor.moveToNext()); 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(); if(list.size() % 2 == 1)
return streaks; 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 public static class CreateCommand extends Command
@ -508,28 +691,9 @@ public class Habit extends Model
habit.save(); habit.save();
if (hasIntervalChanged) if (hasIntervalChanged)
{ {
new AsyncTask<Habit, Integer, Integer>() habit.deleteCheckmarksNewerThan(0);
{ habit.deleteStreaksNewerThan(0);
@Override habit.deleteScoresNewerThan(0);
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);
} }
} }
@ -540,26 +704,9 @@ public class Habit extends Model
habit.save(); habit.save();
if (hasIntervalChanged) if (hasIntervalChanged)
{ {
new AsyncTask<Habit, Integer, Integer>() habit.deleteCheckmarksNewerThan(0);
{ habit.deleteStreaksNewerThan(0);
@Override habit.deleteScoresNewerThan(0);
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);
} }
} }

@ -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;
}

@ -13,17 +13,19 @@ import android.view.View;
import org.isoron.helpers.ColorHelper; import org.isoron.helpers.ColorHelper;
import org.isoron.helpers.DateHelper; import org.isoron.helpers.DateHelper;
import org.isoron.uhabits.R; import org.isoron.uhabits.R;
import org.isoron.uhabits.models.Checkmark;
import org.isoron.uhabits.models.Habit; import org.isoron.uhabits.models.Habit;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.Calendar; import java.util.Calendar;
import java.util.GregorianCalendar; import java.util.GregorianCalendar;
import java.util.List;
public class HabitHistoryView extends View public class HabitHistoryView extends View
{ {
private Habit habit; private Habit habit;
private int reps[]; private int[] checks;
private Context context; private Context context;
private Paint pSquareBg, pSquareFg, pTextHeader; private Paint pSquareBg, pSquareFg, pTextHeader;
@ -93,7 +95,7 @@ public class HabitHistoryView extends View
for (int i = 0; i < nColumns * 7; i++) for (int i = 0; i < nColumns * 7; i++)
dateFrom -= DateHelper.millisecondsInOneDay; dateFrom -= DateHelper.millisecondsInOneDay;
reps = habit.getReps(dateFrom, dateTo); checks = habit.getCheckmarks(dateFrom, dateTo);
} }
@Override @Override
@ -159,7 +161,9 @@ public class HabitHistoryView extends View
{ {
if (!(i == nColumns - 1 && offsetWeeks == 0 && j > todayWeekday)) 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.drawRect(square, pSquareBg);
canvas.drawText(Integer.toString(currentDate.get(Calendar.DAY_OF_MONTH)), canvas.drawText(Integer.toString(currentDate.get(Calendar.DAY_OF_MONTH)),
square.centerX(), square.centerY() + squareTextOffset, pSquareFg); square.centerX(), square.centerY() + squareTextOffset, pSquareFg);

@ -12,10 +12,10 @@ import android.view.View;
import org.isoron.helpers.ColorHelper; import org.isoron.helpers.ColorHelper;
import org.isoron.helpers.DateHelper; import org.isoron.helpers.DateHelper;
import org.isoron.uhabits.models.Habit; import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.models.Streak;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.Calendar; import java.util.List;
import java.util.GregorianCalendar;
public class HabitStreakView extends View public class HabitStreakView extends View
{ {
@ -23,10 +23,9 @@ public class HabitStreakView extends View
private int columnWidth, columnHeight, nColumns; private int columnWidth, columnHeight, nColumns;
private Paint pText, pBar; private Paint pText, pBar;
private long streaks[]; private List<Streak> streaks;
private int dataOffset; private int dataOffset;
private long streakStart[], streakEnd[], streakLength[];
private long maxStreakLength; private long maxStreakLength;
private int barHeaderHeight; private int barHeaderHeight;
@ -68,17 +67,9 @@ public class HabitStreakView extends View
private void fetchStreaks() private void fetchStreaks()
{ {
streaks = habit.getStreaks(); 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<streaks.length / 2; i++) for(Streak s : streaks)
{ maxStreakLength = Math.max(maxStreakLength, s.length);
streakStart[i] = streaks[i * 2];
streakEnd[i] = streaks[i * 2 + 1];
streakLength[i] = (streakEnd[i] - streakStart[i]) / DateHelper.millisecondsInOneDay + 1;
maxStreakLength = Math.max(maxStreakLength, streakLength[i]);
}
} }
@Override @Override
@ -103,16 +94,17 @@ public class HabitStreakView extends View
float lineHeight = pText.getFontSpacing(); float lineHeight = pText.getFontSpacing();
float barHeaderOffset = lineHeight * 0.4f; float barHeaderOffset = lineHeight * 0.4f;
int start = Math.max(0, streakStart.length - nColumns - dataOffset); int nStreaks = streaks.size();
int start = Math.max(0, nStreaks - nColumns - dataOffset);
SimpleDateFormat dfMonth = new SimpleDateFormat("MMM"); SimpleDateFormat dfMonth = new SimpleDateFormat("MMM");
String previousMonth = ""; String previousMonth = "";
for (int offset = 0; offset < nColumns && start+offset < streakStart.length; offset++) for (int offset = 0; offset < nColumns && start+offset < nStreaks; offset++)
{ {
String month = dfMonth.format(streakStart[start+offset]); String month = dfMonth.format(streaks.get(start+offset).start);
long l = streakLength[offset+start]; long l = streaks.get(offset+start).length;
double lRelative = ((double) l) / maxStreakLength; double lRelative = ((double) l) / maxStreakLength;
pBar.setColor(colors[(int) Math.floor(lRelative*3)]); pBar.setColor(colors[(int) Math.floor(lRelative*3)]);
@ -122,7 +114,7 @@ public class HabitStreakView extends View
r.offset(offset * columnWidth, barHeaderHeight + columnHeight - height); r.offset(offset * columnWidth, barHeaderHeight + columnHeight - height);
canvas.drawRect(r, pBar); canvas.drawRect(r, pBar);
canvas.drawText(Long.toString(streakLength[offset+start]), r.centerX(), r.top - barHeaderOffset, pBar); canvas.drawText(Long.toString(l), r.centerX(), r.top - barHeaderOffset, pBar);
if(!month.equals(previousMonth)) if(!month.equals(previousMonth))
canvas.drawText(month, r.centerX(), r.bottom + lineHeight * 1.2f, pText); canvas.drawText(month, r.centerX(), r.bottom + lineHeight * 1.2f, pText);
@ -166,7 +158,7 @@ public class HabitStreakView extends View
private boolean move(float dx) private boolean move(float dx)
{ {
int newDataOffset = dataOffset + (int) (dx / columnWidth); int newDataOffset = dataOffset + (int) (dx / columnWidth);
newDataOffset = Math.max(0, Math.min(streakStart.length - nColumns, newDataOffset)); newDataOffset = Math.max(0, Math.min(streaks.size() - nColumns, newDataOffset));
if (newDataOffset != dataOffset) if (newDataOffset != dataOffset)
{ {

@ -1,4 +1,4 @@
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/container" android:id="@+id/container"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -13,4 +13,4 @@
android:layout_height="match_parent" android:layout_height="match_parent"
tools:layout="@layout/list_habits_fragment" /> tools:layout="@layout/list_habits_fragment" />
</FrameLayout> </RelativeLayout>

@ -52,4 +52,12 @@
style="@style/habitsListButtonsPanelStyle"/> style="@style/habitsListButtonsPanelStyle"/>
</LinearLayout> </LinearLayout>
<ProgressBar
android:id="@+id/progressBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="?android:attr/progressBarStyleHorizontal"
android:layout_marginTop="37dp"
/>
</RelativeLayout> </RelativeLayout>

@ -41,6 +41,7 @@
<string name="day_of_week_label_typeface">sans-serif</string> <string name="day_of_week_label_typeface">sans-serif</string>
<string name="habit_key"></string> <string name="habit_key"></string>
<string name="offset_key"></string> <string name="offset_key"></string>
<string name="toggle_key"></string>
<item name="KEY_TIMESTAMP" type="id"/> <item name="KEY_TIMESTAMP" type="id"/>

@ -43,14 +43,9 @@
android:action="android.intent.action.VIEW" android:action="android.intent.action.VIEW"
android:data="https://github.com/iSoron/uhabits"/> android:data="https://github.com/iSoron/uhabits"/>
</Preference> </Preference>
</PreferenceCategory>
<PreferenceCategory
android:key="pref_key_misc"
android:title="Miscellaneous">
<Preference android:title="Replay app introduction"> <Preference android:title="View app introduction">
<intent <intent
android:targetClass="org.isoron.uhabits.IntroActivity" android:targetClass="org.isoron.uhabits.IntroActivity"
android:targetPackage="org.isoron.uhabits"/> android:targetPackage="org.isoron.uhabits"/>

Loading…
Cancel
Save